Shader Language (GLSL) for Newbie 用 WebGL 或 Godot 學 Shader

這是我發在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 的三角形執行按鈕。

View post on imgur.com

View post on imgur.com

說明

每次要畫一個 pixel 時都會呼叫一次 mainImage()。vec4 fragColor 是在 mainImage 要指定的顏色值,fragCoord 是當前的像素座標。iResolution 是 ShaderToy 內建的參數,就是畫面的長寬 pixel。

用 Godot 要怎麼寫?

在 Godot 中,同樣的 function 是這樣寫的。

View post on imgur.com

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。

View post on imgur.com

View post on imgur.com

在 Godot 裏,語法也差不多。差別是多了個可以從外部更改的 resolution 變數,取得作畫範圍的像素長寬值。

View post on imgur.com

轉換了 uv.x 後,畫出來的陰影就是個圓弧形,而非橢圓弧形的了。

把原點改到中間

如果我們想把原點放在中央,而非左上或左下,只要加一行座標轉換就可以了。

View post on imgur.com

這樣圓心就在中間了。

View post on imgur.com

Godot 的版本

View post on imgur.com

把漸層變成純黑色的圓形

要畫一個圓,直覺上的寫法大概是這樣 (警告! 下面是錯誤寫法!)

View post on imgur.com

上面的程式是可以動的,但沒人會這樣寫。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。

View post on imgur.com

這樣就可以不用 if-else 畫出實心黑色圓了。

View post on imgur.com

 

用內建數學函式畫一條雷射光!

下面會在畫面中間畫出一道雷射光,原理請看裡面的說明 (只有一行)。

View post on imgur.com

View post on imgur.com

 

朝中央變細的雷射光

接下來加點變化,我們更改 laser_coeff,讓該值越靠近中間就越細,方法就是直接乘 abs(uv.x),因為 uv.x 越中間就越接近 0。

View post on imgur.com

View post on imgur.com

 

日出般的雷射光

我們也可以反過來,讓雷射越靠中間就越粗,看起來也很像日出效果了。

 

View post on imgur.com

View post on imgur.com

 

變成一支光箭

把右邊變短,左邊變長,讓它看起來像一支光箭。

View post on imgur.com

View post on imgur.com

 

射往右邊的光箭

從左邊射到右邊,只需要用 iTime 改變 uv.x 的值就好。iTime 是 ShaderToy 提供的參數,就是左邊撥放器看到的撥放時間 (秒)。這個程式只會射一次,要再看一次需要點重頭開始的按鈕。

View post on imgur.com

在 Godot 上,時間參數是執行時從 Script 提供的。

Godot shader

View post on imgur.com

Godot GDScript

View post on imgur.com

利用 mob/sin/cos 來做一直射的曲線雷射

最後,讓直線的移動變成曲線,只要加一行改變 uv.y 就好。

View post on imgur.com