Godot引擎的shader language是Godot引擎中一個十分強力的工具,
透過編寫shader script在GPU的渲染層面影響2D或3D圖形繪製的結果,
能實現的效果十分多樣,例如樹/草的擺動、水面的波動等等。
由於現階段我較常接觸的是2D的shader,因此本文也會較多集中在2D上,
而本文使用的為4.1.3 stable Godot,不同版本的Godot可能會有不同的語法。
首先,學習shader language中最關鍵的關注點為三 :
一、fragment函式
二、vertex函式
三、uniform變數
一和二的fragment函式和vertex函式在shader script中,其角色類似於gdscript中的Ready函式以及Process函式,是shader script的Entry Point,當然,fragment函式和vertex函式與Ready函式和Process函式的邏輯並不太相同,這裡只是一個比喻。
而uniform是shader script中的一種變數類型,是整個shader script的參數,在gdscript中可以修改uniform變數,shader script運行的結果也會被影響。
fragment函式 和 vertex函式
fragment函式的重點在於RGBA,亦即顏色。
而vertex函式的重點在於位置。
兩個函式的本身並不難理解,
但正在使用這些函式之前,就必須認識一個關鍵變數,UV。
UV
先說一下什麼是UV,這裡的UV不是紫外線(Ultraviolet),U和V是貼圖(Texture)上的座標,
類似於數學課學幾何時會用到的X軸和Y軸,U = X,V = Y。
但為什麼要用U和V呢 ? 原因在於一般在描述3D 模型時X、Y和Z通常會用來描述3D空間的座標,
如果在描述貼圖時也用XY的話會十分容易混淆,當然在2D的場合下比較沒有相關的問題,
畢竟在2D下的大多數情況下,渲染每一個像素時的U軸和V軸都和實際情形下的X軸和Y軸平行,
因此,UV與XY的混用也不會對相關描述在太大的影響,但在3D的情境下就不太一樣。
回來shader script的部分,之所以要先介紹什麼是UV,
原因在於fragment函式 和 vertex函式中能引用變數UV,
它的函意為該fragment函式 或 vertex函式所應用在貼圖上的哪一個分段,
光用說的有點難以理解,以下圖為例。
在上圖中,貼圖被格線分成了不同的碎片,由左至右,由上至下,
最左的U為0,而最右的U為1 ; 最上的V為0,而最下的V為1。
(有趣的是shader script中的UV物件裡的屬性名稱也是叫x和y,例如UV.x和UV.y)
當引用UV變數時,可以利用UV.x來進行根據貼圖座標來執行顏色修改。
例如 :
上述代碼的用途是,如果貼圖分段的UV.x大於特定值,
便修改顏色至RGBA(0.2 , 0.2 , 0.2 , 1),
以及修改至原本貼圖的顏色並增加或減少特定顏色變量,效果如下。
(請留意一點,理論上UV的x值以及y值必然在0至1之間,不會小於0或大於1)
代碼中的COLOR十分容易理解,COLOR為該分段最終的渲染出來的結果。
如果你在script中想把該分段修改成紅色,便設定COLOR為vec(1,0,0,1),
但如果你希望該分段變回原本的顏色,那只需要重新把COLOR設定為texture(TEXTURE)。
至於vertex函式的部分,則用於調整毎一個分段的方位,
可以用於放大貼圖、翻轉貼圖、彎曲貼圖等等。
看到這裡大概已介紹了一些shader的基礎,
但我相信大多數人除非天資聰慧,否則一般的Programmer知道了這些資訊之後應該仍然是一頭霧水,難以實際應用,
例如if(UV.x > 0.5)代表的是如果分段的座標X軸大於0.5就執行,
但現在UV的x是多少,數據的流動是如何,這此問題大概也很難理解。
其原因在於shader script的運行邏輯不太直覺,與平日寫的程式語言並不能用相同的思考模式。
CPU vs GPU
這裡便必須加插入一段CPU(中央處理器)與GPU(顯示卡)之間的差異性,
文章開頭有提及,shader script是運行在GPU上,而一般平時寫的程式語言則是運行在CPU上。
這一個區別使得shader script和common script有着不一樣的特性。
CPU和GPU的最大差別在於CPU用於處理運算的核心數量較少,一般4至8個核心再多也就16或24個核心,
但GPU的核心數卻是最少上千,最多也有上萬個核心。
CPU有着較少但較強的核心,GPU則有較多但較弱的核心。
這一特性使得CPU和GPU的工作模式有着天壤之別,CPU擅長單一執行緒來完成任務,
而GPU則擅長多執行緒來完成任務。

圖片來源於 NVIDIA CUDA C++ programming guide P. 24
如果任務是要用零件組成一台波音747的話,CPU工作小組會找一個手腳飛快的員工,
單獨的一個一個零件進行組裝,如果是比較優秀的工作領導者(程式設計師),
有可能會分成不同的段落,
並分別由數位手腳飛快的員工來組裝不同的部分並最後把不同段落合併以完成飛機的組裝(即多執行緒)。
但如果是GPU工作小組來處理的話,會把所有零件做好分段,
並派出幾千位手腳較慢的員工分別並同時的完成該段落的零件組,
並最後同時把零件放上波音747的骨架便完成了。
把場景拉回圖形渲染上,如果要由CPU進行渲染就必要由左至右由上至下,
一格一格像素畫出來。而相反GPU則會把圖形分段,並同時把各分段完成,
除非CPU單一核心的速度遠超過GPU單一核心,
否則把渲染工作分派給GPU會是更好更有效率的做法。
理解到CPU和GPU的差異之後,便可以重新回到shader script的話題。
平時在撰寫程式語言腳本時,一般的程序員多多少少會習慣用一條直線的方式來理解程式碼,
就算是一個for loop也會有先後順序,即使是做Web dev的程序員較常用多執行緒和異步執行等設計模式。
但我相信用前後順序來理解程式碼,並在大腦中模擬編譯後的數據流動和改變,
以決定下一步打code的方向依然是不少程序員的習慣。
但在shader script以及其他著色器語言的編寫下,就必需要放下這一套思考模式。
而是所有的代碼都是同時執行的,UV.x由0至1都是同時存在於GPU中的,
而COLOR和TEXTURE變數的值在不同分段中彼此也不存在前後因果關係,
COLOR可以同時是vec4(1,0,0,1)也可以同時時是vec4(0,0,1,1),
可以理解成一個雞塊(貼圖)裹粉(shder script)的過程,
總不會雞塊裹粉是由左上到右下順序着沾上粉,而是一整個面一起被沾上粉才對。
因此,shader language 的翻譯「着色器語言」是翻譯得頗為貼切的。
uniform 變數
至於uniform變數,只需要在shader script 的宣告變數時加上uniform的前綴便可。
加入uniform前綴後,我們就可以在外部修改該變數的值,
除此之外,可以在變數名稱後加入冒號及變數提示(在下述代碼中為 source_color),
以便在Godot Engine的Editor中修改。
加入變數提示後,回到Godot Engine的Editor中可以在右方Inspector -> CanvasItem -> Material-> Shader Parameters中修直接選取顏色。如沒有變數提示的話, 則會改為直接輸入數字。
在Game Loop中想更改uniform變數的話,
可以使用 _node_name.material.set_shader_parameter(“para_name”, value)修改數值。
但請留意在C#中,要修改uniform變數的話需要使用(_node_name.Material as ShaderMaterial).SetShaderParameter(“para_name”,(Value Type)value);。
由於C#對類型敏感,因此要先把Material轉換為ShaderMaterial,再使用SetShaderParameter。