Shadertoy 教程 Part 1 - 介紹

Note: This series blog was translated from Nathan Vaughn's Shaders Language Tutorial and has been authorized by the author. If reprinted or reposted, please be sure to mark the original link and description in the key position of the article after obtaining the author’s consent as well as the translator's. If the article is helpful to you, click this Donation link to buy the author a cup of coffee.
說明:該系列博文翻譯自Nathan Vaughn着色器語言教程。文章已經獲得作者翻譯授權,如有轉載請務必在取得作者譯者同意之後在文章的重點位置標明原文鏈接以及說明。如果你覺得文章對你有幫助,點擊此打賞鏈接請作者喝一杯咖啡。

朋友們,你們好。我最近對着色器着迷了。 今天,我將討論如何使用 Shadertoy 這件驚人在線工具創建像素着色器,該工具由 Inigo QuilezPol Jeremias 這兩位非常有才華的人創建的。

什麼是着色器

着色器(Shaders)是一門功能強大的程序語言,最初的作用是爲 3D 場景中的物體着色。 如今,着色器的用途非常廣泛。 着色器程序通常在計算機的圖形處理單元 (GPU) 上運行,它們運行的方式是並行的。

提示:瞭解着色器(Shaders)在 GPU 上並行運行非常重要。 你的程序將會同時內在Shadertoy中的每個像素上獨立地運行。

高級着色語言(HLSL) 和 OpenGL 着色語言 (GLSL) 等着色器語言是用於對 GPU 渲染管道進行編程的最常用語言。 他們有着類似於C編程語言的語法。

當你在玩Minecraft這種遊戲的時候,只需要一個2D屏幕(例如:電腦顯示器或者手機屏幕),你就能體驗着色器會爲你構建的3D世界。着色器語言也能夠通過動態地改變虛擬世界中的光線與物體的交互以及物體的渲染從而改變虛擬世界的樣子。這個油管視頻展示了10種着色器程序,它們使得同一個遊戲看起來完全不同。該示例向我們展示了着色器程序語言之美。

我們經常見到的着色器有兩種:頂點着色器和片元着色器。頂點着色器用來創建3D模型的頂點,例如球體,立方體,大象,或者3D遊戲中的主角等。來自頂點着色器的信息被傳遞給幾何着色器,幾何着色器會在片元着色器之前對它們進行處理或者做一些額外的操作。你一定不太想知道幾何着色器的太多東西。最後一部分是片元着色器。片元着色器會計算出每一個像素最終呈現的顏色或者決定該像素是否需要呈現在屏幕上。

圖形渲染管線的各個階段-- Learn OpenGL

我們來舉個例子,假設我們有一個頂點着色器,它被用來在屏幕上繪製一個三角形的各個頂點。一旦它們被傳遞給片元着色器,每個頂點之間的顏色會就會被自動填充。GPU非常擅長處理顏色插值。一旦一種顏色被分配給了頂點,GPU就會在每個頂點之間進行顏色插值處理,填充整個三角形。

Unity或者Unreal這種遊戲引擎中,構建3D遊戲對頂點着色器和片元着色器的使用是非常依賴的。Unity在着色器之上進行了抽象,創建了一門獨特的語言ShaderLab, 它是一種基於HLSL的高級語言,旨在幫助開發者更容易地編寫遊戲。此外,Unity還提供了一個虛擬工具Shader Graph,讓開發者無需編寫代碼就可以構建自己的着色器。如果你在Google上搜索“Unity shaders”,就能找到上百種各種功能的着色器。你可以使用着色器讓物體發光,讓人物變得透明,甚至可以製造一些“圖片效果”爲你的遊戲增添色彩,總之探索着色器的道路千千萬萬條。

你可能經常聽到片元着色器也被稱爲“像素着色器”,實際上使用術語“片元着色器”是更爲準確的,因爲着色器可以防止像素被繪製在屏幕上。在一些應用上例如Shadertoy,你往往只是在往屏幕上繪製像素,所以在那種情況之下,我們才把片元着色器叫做像素着色器。

着色器同時也爲你的遊戲提供着色或者光照效果,但它的功能還不僅於此。既然着色器運行在GPU上,爲什麼不利用它的並行優勢呢?你可以在GPU而不是CPU上創建非常複雜的計算着色器。事實上,Tensorflow.js就是在瀏覽器上利用GPU的優勢,讓他們更快地訓練機器學習模型。

着色器真的是一門非常強大的程序!

什麼是Shadertoy

在接下來的博文中,我將向諸位介紹Shadertoy。Shadertoy是一個幫助用戶創建並且分享像素着色器的網站。類似於上編寫HTML, CSS和JavaScript在線編輯器Codepen

提示:學習此教程的過程中,需要確保你的瀏覽器像谷歌瀏覽器一樣支持WebGL 2.0。

Shadertoy利用了WebGL API 調用GPU的能力去瀏覽器中繪製圖像。WebGL 讓你用GLSL編寫着色器並且支持硬件加速。因此,你可以利用GPU的並行計算能力,在同一塊屏幕上同時地去操作所有像素。你還記得我們在使用HTML Canvas API時調用的ctx.getContext('2d')函數嗎?Shadertoy 使用了webgl上下文,它在屏幕上繪製像素有着更高質量的表現。

告警:雖然Shadertoy用GPU提升了視覺效果,但是當你運行復雜計算的着色器程序時,你的電腦也可能會出現卡頓的現象。請確保你的電腦能夠運行這些着色器程序,並且要記住,它們會非常耗電。

現代3D遊戲引擎例如Unity和Unreal以及3D建模軟件例如Blender運行非常順暢,因爲它們同時使用了頂點着色器和片元着色器,此外它們還做了很多優化的工作。在Shadertoy中,你是無法使用頂點着色器的。你只能依賴一些算法例如:光線步進(Ray Marching)以及等符號距離場函數(Signed Distance Fields Functions)去渲染3D場景,這些計算是非常昂貴的。

在Shadertoy中編寫的着色器不一定能夠在其他環境(如Unity)中運行,這點需要特別注意。你需要將GLSL代碼語法翻譯成你的目標環境(例如 Unity)支持的代碼(如HLSL)。Shadertoy同時提供了一些全局變量,這些變量在其他環境中也是不支持的。但別就此止步不前!只需要額外的做一些工作,例如對着色器做一些小小的修改,就可以把他們應用到你的遊戲或者建模軟件中去。在使用自己喜歡的遊戲引擎或者建模軟件之前,Shadertoy讓我們能夠預先體驗着色器的魅力。

Shadertoy能讓用戶使用GLSL編寫着色器,幫助我們數學地思考問題。繪製3D場景時需要很多矢量計算,這是一場智力的模擬賽,你可以向你的朋友們炫耀你的技巧。如果你去Shadertoy網站上瀏覽過,你就會發現許許多多美麗的作品,這些作品僅僅是一堆代碼和數學組成的。一但你掌握了Shadertoy,你會發現它真的很有意思!

Shadertoy 功能介紹

Shadertoy爲你封裝好了HTML canvas和webgl,你需要關心的就是用GLSL編程語言編寫着色器代碼邏輯。但是Shadertoy不允許你編寫頂點着色器而只允許你編寫像素着色器,它提供的環境只能夠讓我們體驗着色器的“片元”部分,這意味着只能在整塊畫布上地操作所有像素。

在Shadertoy的頂部導航欄上,你可以點擊“新建”來創建一個着色器。

讓我們分析屏幕上所有我們能看到的東西吧。我們可以看到右手邊是一個用來編寫GLSL代碼的編輯器,然後我們按照圖片上標註的數字順序,依次介紹它們的作用。

  1. 用來展示代碼效果的畫布(Canvas)。你的着色器代碼會並行地在上面的每個像素上運行。
  2. 左邊:將時間重置爲0。中間:暫停/播放着色器動畫。右邊:頁面加載後經過的時間。
  3. 每秒鐘幀數(FPS)會讓你瞭解你的電腦是如何運行這些着色器的。一般在60FPS左右徘徊。
  4. Canvas的高寬分辨率。這些值被指定在“iResolution”全局變量中去。
  5. 左邊:點擊錄製視屏,再次點擊停止即可保存。中間:調整音量鍵。右邊:進入全屏模式。
  6. 點擊“+”按鈕添加新的腳本。緩衝(A,B,C,D)可以通過Shadertoy提供的Channels互相訪問。使用“Common”分享着色器片段。使用“音量”當你需要編寫一個可以播放音樂的着色器。使用“穹頂”按鈕當你需要一個起起釘。
  7. 點擊小箭頭查看Shadertoy提供的全局變量列表。
  8. 點擊小箭頭將你的着色器代碼編譯,輸出到畫布上。也可以使用Alt+Enter 或者 Option+Enter 作爲快捷鍵。你可以點擊“編譯到”文本看看編譯後的代碼。
  9. Shadertoy 提供了四個通道,他們可以通過全局變量“ iChannel0”,“ iChannel1”等進行互相訪問。點擊一個通道向其中添加紋理或者爲你的着色器添加鍵盤,音量,攝像頭等交互行爲。
  10. Shadertoy 提供了修改代碼字體大小的選項。如果你點擊問號標記,你就能看到關於編譯器運行你代碼的信息了。此外你還可以看看有什麼功能或者輸入被加入到了Shadertoy中。

Shadertoy爲我們編寫GLSL 代碼提供了環境,並且爲我們預設了默認的變量,函數和一些其他功能,這些功能使得它稍微與其他環境中編寫的GLSL代碼有所區別。Shadertoy爲你開發着色器的過程提供了便利,例如:變量“iTime”,就能讓你訪問頁面加載後過去的時間。

理解着色器代碼

在Shaderoy中新建第一個着色器,可以看到下面的一段代碼:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  // Normalized pixel coordinates (from 0 to 1)
  vec2 uv = fragCoord/iResolution.xy;

  vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));

  // Output to screen
  fragColor = vec4(col,1.0);
}

通過點擊我們之前提到過的小箭頭或者快捷鍵Alt+Center 或者Option+Enter 來運行這段代碼。

如果你之前沒有寫過着色器,也沒關係。我會盡量想你解釋清楚GLSL的語法,幫助你在Shadertoy中編寫自己的着色器。現在。相信你一定注意到了這是一種靜態類型的語言。和C, C++ 或者Java和C#一樣,GLSL也使用類型。這些的類型包括:bool(布爾),int(整數),float(浮點數),以及vec(矢量)。GLSL強制要求換行前有逗號,否則,編譯器會拋出錯誤。

前面的這小段代碼,我們定義了一個mainImage函數,這個函數必須出現在Shadertoy着色器中。這個函數什麼都不返回,因此它的返回類型就是void。它接受兩個參數:fragColorfragCoord

你或許會對inout兩個關鍵字感到疑惑。對於Shadertoy來說,我們只需要在mainImage函數中的關心這些關鍵字。還記得我說過GPU是怎麼樣運行我們用着色器渲染管線的嗎?把inout想象成輸入和輸出:Shadertoy給我們一個輸入,我們返回一個顏色作爲輸出。

讓我們簡單地修改一下模板代碼。

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  // Normalized pixel coordinates (from 0 to 1)
  vec2 uv = fragCoord/iResolution.xy;

  vec3 col = vec3(0., 0., 1.); // RGB values

  // Output to screen
  fragColor = vec4(col,1.0);
}

運行着色器程序,我們就會看到一個純藍色的畫布。着色器在畫布上同時並行地繪製出每一個像素,你需要特別注意這點,因爲它非常重要。現在你需要思考的是如何根據每個像素的座標改變它們顏色從而能創作出驚人的作品。

在着色器中,我們用0~1之間的任意值來指定RGB(紅,綠,藍)。如果顏色值是0到255,你可以將他們除以255來得到同樣的結果。

那麼,我們已經知道如何改變畫布上的顏色,但實際上在着色器中到底發生了什麼呢?第一行的mainImage函數聲明瞭一個變量uv,類型爲vec2。如果你還記得學校教你的矢量計算,你就會知道vec2表示我們有兩個元素x和y。如果變量是用vec3類型的,我們會多出一個z元素。

你們在學校中一定學過3D座標系統。我們可以在一片紙上或者其他屏幕畫出一個3D座標系統來。在2D平面呈現3D場景確實有些困難,所以,傑出的數學家們爲我們創建了一套3D座標系統可以更好的幫助我們可視化3D空間。

我們可以把着色器中的矢量看作是可以裝填1或者4個元素的數組,有時候,矢量中包含3D空間中的XYZ座標位置信息,有時候又包含了RGB顏色信息。因此,下面的等式是成立的。

color.r = color.x
color.g = color.y
color.b = color.z
color.a = color.w

是的,變量可以爲vec4類型,其中最後的一個元素用w或者a來訪問。a是Alpha的縮寫,因爲顏色可有一個Alpha通道和RGB值是一樣。而w,我猜可能是因爲z已經是最後一個字母,所以取的是x字母之前的字母w🤷。

變量uv並非任何單詞的縮寫,但通常它用來指代映射紋理到3D物體上的紋理映射(UV Mapping)技術。紋理映射(UV mapping) 更適合應用到能夠使用頂點着色器的環境中。但是你仍然可以在Shadertoy中使用紋理數據。

fragCoord變量代表着Canvas的X和Y 座標。左下腳的座標位置是起點(0,0),右上角的座標位置是(iResolution.x, iResolution.y)。iResolution.xy除以fragCoord,我們就能歸一化像素的座標從0到1.

請注意,要在兩個相同類型的矢量之間做數學計算並不困難。我們只需要分別將裏面的元素各自獨立的進行計算就可以了。

uv = fragCoord/iResolution.xy

// The above is the same as:
uv.x = fragCoord.x/iResolution.x
uv.y = fragCoord.y/iResolution.y

當我們使用iResolution.xy這種寫法時, .xy部分其實就是代表了XY元素。這樣我們就可以不依靠整個向量而獨立地爲每個元素進行計算。這種做法在vec3類型的時候也是一樣的。

根據一篇來自Stack Overflow 的文章的說法,z元素表示的是像素的比率,大多數情況下是1.0。儘管這樣,你也不是很經常地會使用到z元素。

在定義矢量時我們可以用一些快捷的方法。下面這段代碼會將整個畫布鋪滿黑色。

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  // Normalized pixel coordinates (from 0 to 1)
  vec2 uv = fragCoord/iResolution.xy;

  vec3 col = vec3(0); // Same as vec3(0, 0, 0)

  // Output to screen
  fragColor = vec4(col,1.0);
}

當我們定義一個向量,如果你只是指定了第一個值,着色器代碼會很聰明地用第一個值補齊其他剩下的值。因此,vec3(0)這種寫法,與vec3(0, 0, 0)是相同的效果。

提示:如果你嘗試用小於0的值作爲輸出的顏色,它會被強制提升爲0. 同樣,任何大於1的值也會被強制改爲1. 這些自動限定的行爲會發生在片元着色器最後輸出顏色的過程中。

你需要記住的是,在Shadertoy中或者其他環境裏面,調試工作基本上是用眼睛看的。不會有那種console.log打印方法來拯救你,需要用顏色來幫助我們調試。

讓我們試試在屏幕上展示下面代碼的結果:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  // Normalized pixel coordinates (from 0 to 1)
  vec2 uv = fragCoord/iResolution.xy;

  vec3 col = vec3(uv, 0); // This is the same as vec3(uv.x, uv.y, 0)

  // Output to screen
  fragColor = vec4(col,1.0);
}

運行以上代碼我們最終就得到了下面這幅由黑色、紅色、綠色以及黃色的畫布。

看起來還挺漂亮的,但如何理解它的調試功能呢?uv變量代表畫布上每個像素點的座標,它們的值在0到1之間。我們就以屏幕上的四個(左上,右上,左下,右下)特殊的像素點來說明吧。它們的座標位置分別是(0, 1), (1, 1), (0, 0), (1, 0); 在col變量中我們保存(uv.x, uv.y, 0), 把四個點的位置代入變量:首先是左上角的值就是(0, 1, 0),其次是右上角值爲(1, 1, 0), 再次是左下角(0, 0, 0),最後是右下角(1, 1, 0)。哈哈,發現了嗎,這四個值代表的顏色分別就是綠色(左上)、黃色(右上)、黑色(左下)和右下(紅色)是不是與我們在屏幕上看到的顏色一樣呢。

讓顏色指引你去調試整個過程吧!

總結

好了!在這篇文章中我提到了很多關於着色器以及Shadertoy的知識, 真希望你還能夠讀到這裏。我第一次學習着色器時,感覺它就是一片完全陌生的領域,與我之前學過的知識完全不一樣,但同時也充滿了期待和挑戰。在下一篇文章中,我將會討論如何用着色器語言創建物體形狀以及動畫!

引用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章