這是我發在PTT的文章,搬回來留個備份。
UV
講 shader 一定要先提的是 UV 座標。UV其實就是一個長方形的XY軸座標,差別是UV的座標值是在[0,1]之間,所以UV(0.5,0.5)就是平面的正中央。用實際的例子,一個畫布長寬 (600,400),某個點 a1 位置 (300, 100),a1的 uv 是 (300/600, 100/400) = (0.5, 0.25)。某說法是,UV這名字的由來,就只是因為這二個字母是在三維的(X,Y,Z)前,沒別的意義。UV 的原點,在 WebGL 是在左下角,在 Godot 是在左上角,並不一定。
顏色
再來是顏色,shader用RGB加上一個透明度的四個值代表顏色。四個值都在[0,1]之間,大於1都算1,小於0就是0。白色就是(1,1,1,1),黑色就是(0,0,0,1)。
因為UV座標和顏色的值都是[0,1]之間,所以馬上可以寫一個簡單的程式測試圓點在哪。
最簡單的 Shader 程式
開啟 https://www.shadertoy.com/ ,點右上的 New,在右邊編輯框內打入下面程式後按下面 compile 的三角形執行按鈕。
說明
每次要畫一個 pixel 時都會呼叫一次 mainImage()。vec4 fragColor 是在 mainImage 要指定的顏色值,fragCoord 是當前的像素座標。iResolution 是 ShaderToy 內建的參數,就是畫面的長寬 pixel。
用 Godot 要怎麼寫?
在 Godot 中,同樣的 function 是這樣寫的。
UV 是 global variable 不用計算,COLOR 也是類似的 variable。因為都算是 GLSL,長的都差不多。在 Godot 中要寫 shader,最快的方法是建立一個 ColorRect Node,填入 Rect 的長寬 (size) 顏色 (color)。再到右邊 Inspector 的 CanvasItem -> Material -> ShaderMaterial -> New Shader 就可以開始寫了。
比較 ShaderToy 和 Godot 的執行畫面,ShaderToy 的黑色區域在左下角,Godot 在左上角,代表二者的原點位置不同。
將橢圓弧形變成圓弧形
因為畫面不是正方形,而是長方形,所以看的出來畫出的漸層並非正圓弧,而是橢圓形弧形的。要解決 uv 的二個值最大都是 1,代表的真實長度卻不同的問題,我們可以重新計算 x 或 y 值。一般來說 resolution.y 比較短 (橫向畫面),所以當作 1,照比例調整 uv.x。
在 Godot 裏,語法也差不多。差別是多了個可以從外部更改的 resolution 變數,取得作畫範圍的像素長寬值。
轉換了 uv.x 後,畫出來的陰影就是個圓弧形,而非橢圓弧形的了。
把原點改到中間
如果我們想把原點放在中央,而非左上或左下,只要加一行座標轉換就可以了。
這樣圓心就在中間了。
Godot 的版本
把漸層變成純黑色的圓形
要畫一個圓,直覺上的寫法大概是這樣 (警告! 下面是錯誤寫法!)
上面的程式是可以動的,但沒人會這樣寫。Shader 是控制顯卡的語言,顯卡的特性除了大量的平行運算外,另一點是它非常不擅長處理判斷式。用了 if-else 或其他類似的任何語法 (ex: a = (a>b)? c:d),都會大幅的拖累效能。所以在 Shader 中是看不到 if-else 的。
不用判斷式的前題下,Shader 有提供幾個數學運算的函式,可以組合代替 if-else
- step(edge, val) – (val>=edge? 1.0: 0.0)
- clamp(val, min, max) – 回傳 val,但最大不超過 max, 最小不低於 min
- floor(val) – 取的最接近但不超過 val 的整數
- ceil(val) – 取的最接近且超過 val 的整數
- mod(x,y) – x/y 的餘數
- fract(val) – 取得小數值
- sin
- cos
- smoothstep(edge0, edge1, val) – 類似 step,但在 edge0 和 edge1 之間時,會平滑的回傳 (0,1) 之間的值。
- sign(val) – 判斷 val 的正負值,正值回傳1.0,反之-1.0。
- mix(val1, val2, weight) – 照權重將 val1 val2 混合回傳。
- abs(x) – if x<0 return x*-1.0
在這裡可以用 step 來做一個 mask。
這樣就可以不用 if-else 畫出實心黑色圓了。
用內建數學函式畫一條雷射光!
下面會在畫面中間畫出一道雷射光,原理請看裡面的說明 (只有一行)。
朝中央變細的雷射光
接下來加點變化,我們更改 laser_coeff,讓該值越靠近中間就越細,方法就是直接乘 abs(uv.x),因為 uv.x 越中間就越接近 0。
日出般的雷射光
我們也可以反過來,讓雷射越靠中間就越粗,看起來也很像日出效果了。
變成一支光箭
把右邊變短,左邊變長,讓它看起來像一支光箭。
射往右邊的光箭
從左邊射到右邊,只需要用 iTime 改變 uv.x 的值就好。iTime 是 ShaderToy 提供的參數,就是左邊撥放器看到的撥放時間 (秒)。這個程式只會射一次,要再看一次需要點重頭開始的按鈕。
在 Godot 上,時間參數是執行時從 Script 提供的。
Godot shader
Godot GDScript
利用 mob/sin/cos 來做一直射的曲線雷射
最後,讓直線的移動變成曲線,只要加一行改變 uv.y 就好。