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 Quilez 和 Pol Jeremias 這兩位非常有才華的人創建的。
什麼是著色器
著色器(Shaders)是一門功能強大的程式語言,最初的作用是為 3D 場景中的物體著色。 如今,著色器的用途非常廣泛。 著色器程式通常在電腦的圖形處理單元 (GPU) 上運行,它們運行的方式是並行的。
提示:了解著色器(Shaders)在 GPU 上並行運行非常重要。 你的程式將會同時內在Shadertoy中的每個像素上獨立地運行。
高級著色語言(HLSL) 和 OpenGL 著色語言 (GLSL) 等著色器語言是用於對 GPU 渲染管道進行編程的最常用語言。 他們有著類似於C
程式語言的語法。
當你在玩Minecraft這種遊戲的時候,只需要一個2D螢幕(例如:電腦顯示器或者手機螢幕),你就能體驗著色器會為你構建的3D世界。著色器語言也能夠通過動態地改變虛擬世界中的光線與物體的交互以及物體的渲染從而改變虛擬世界的樣子。這個YouTube影片展示了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。
提示:學習此教程的過程中,需要確保你的瀏覽器像Google瀏覽器一樣支援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程式碼的編輯器,然後我們按照圖片上標註的數字順序,依次介紹它們的作用。
- 用來展示程式碼效果的畫布(Canvas)。你的著色器程式碼會並行地在上面的每個像素上運行。
- 左邊:將時間重置為0。中間:暫停/播放著色器動畫。右邊:頁面載入後經過的時間。
- 每秒鐘幀數(FPS)會讓你了解你的電腦是如何運行這些著色器的。一般在60FPS左右徘徊。
- Canvas的高寬解析度。這些值被指定在「iResolution」全局變數中去。
- 左邊:點擊錄製視屏,再次點擊停止即可保存。中間:調整音量鍵。右邊:進入全螢幕模式。
- 點擊「+」按鈕添加新的腳本。緩衝(A,B,C,D)可以通過Shadertoy提供的Channels互相訪問。使用「Common」分享著色器片段。使用「音量」當你需要編寫一個可以播放音樂的著色器。使用「穹頂」按鈕當你需要一個起起釘。
- 點擊小箭頭查看Shadertoy提供的全局變數列表。
- 點擊小箭頭將你的著色器程式碼編譯,輸出到畫布上。也可以使用Alt+Enter 或者 Option+Enter 作為快捷鍵。你可以點擊「編譯到」文本看看編譯後的程式碼。
- Shadertoy 提供了四個通道,他們可以通過全局變數「 iChannel0」,「 iChannel1」等進行互相訪問。點擊一個通道向其中添加紋理或者為你的著色器添加鍵盤,音量,攝影機等交互行為。
- 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
。它接受兩個參數:fragColor
和fragCoord
。
你或許會對in
和out
兩個關鍵字感到疑惑。對於Shadertoy來說,我們只需要在mainImage
函數中的關心這些關鍵字。還記得我說過GPU是怎麼樣運行我們用著色器渲染管線的嗎?把in
和out
想像成輸入和輸出: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的知識, 真希望你還能夠讀到這裡。我第一次學習著色器時,感覺它就是一片完全陌生的領域,與我之前學過的知識完全不一樣,但同時也充滿了期待和挑戰。在下一篇文章中,我將會討論如何用著色器語言創建物體形狀以及動畫!