iT邦幫忙

2022 iThome 鐵人賽

DAY 28
0
Software Development

30天成為鍵盤麥可貝:前端視覺特效開發實戰系列 第 28

Day28: WebGL Shader—透過Shader製作光暈:光暈原理與多種變化形式

  • 分享至 

  • xImage
  •  

Shader是前端視覺特效的重要戰場,而本篇所介紹的shader,不僅只是說明怎麼辦到,還要解釋期原理,並且帶出我們在鐵人賽一開始所打下的重要基礎。

Shader不能只是看人家怎麼做,還必須親自應用,才能夠有更好的發揮。而要親自應用時,必須理解底層的原理,才能操作得如魚得水。而這也是我的設計這個順序的想法。

上一篇,我們釐清了vertexNormal 的前世今生,而本篇必須接著釐清最後一塊拼圖:dot()是在哪裡宣告的?它的作用是什麼?

釐清之後,就可以根據前一篇破解的謎團,以及本篇破解的謎團,把這段程式碼順一遍。

快速回顧上一篇

varying vec3 vertexNormal;
void main(void){
+		float intensity = 1.05 - dot(vertexNormal, vec3(0.,0.,1.));
		vec3 atmosphere = vec3(.3, .6, 1.) * intensity;
		gl_FragColor=vec4(0.,0.,0.2,1.);
		gl_FragColor=vec4(atmosphere,0.) + vec4(0.,0.,0.2,1.);
}

上一篇,我們得出了vertexNormal 是從哪裡來的。

https://ithelp.ithome.com.tw/upload/images/20221016/20142505dQWU1o9qZl.png

既然已經解釋vertexNormal,本篇將繼續解答第二個疑問:dot()是在哪裡宣告的?它的作用是什麼?

dot()是在哪裡宣告的?它的作用是什麼?

dot()怎來的:解釋dot()

我們在「Day8: Three.js 你有被光速踢過嗎?解析3D界的黃猿——光的底層原理與介紹」其實有提到也同樣叫做dot()的函式。不同的是,那時提及的dot,是three.js中Vector3提供的函式。

dot()的作用,是計算兩個向量有多相近。兩向量越是相近,越是趨近於1。最終如果是同方向,那就是1。相反的,如果兩個向量越是相反,則越趨近-1,如果完全相反,那就是-1。概念很簡單吧?

而在GLSL提供的函式dot() 也是同樣的概念,我們給它兩個參數,它將回傳1~-1區間的結果,來表示兩個向量的相近程度。dot其實就是Dot Product的簡稱,中文叫作內積,是相當實用的函式。

If A, B are vectors: (Ax, Ay), (Bx, By)

dotProduct = |A| • |B| • cosθ

dot()怎來的:圖解dot()

如果不懂,我就用圖片說明:

  1. 現在我們有一個解析度超級低的球體,我們從正Y軸往下看這個場景,也會看到鏡頭位於正Z的位置:

    https://ithelp.ithome.com.tw/upload/images/20221016/20142505PIGYnHZow0.png

    這顆球有很多個面,每一個頂點都有一個法線輻射狀指向四方。

  2. 每一個法線,都可以拆成x,y,z數值

    https://ithelp.ithome.com.tw/upload/images/20221016/20142505cUFq9cUaPG.png

  3. 如果光看球體於xz剖面來看,可以發現面對鏡頭的頂點,z值最大,離正z軸(0,0,1)最相近,內積相乘之後最大。背對鏡頭的頂點z值最小(因為指向負z),離(0,0,1)最遙遠,相乘之後最小。

    https://ithelp.ithome.com.tw/upload/images/20221016/2014250536cPgrFrWu.png

  4. 如果全部變成了負數,又加上1.05,就會變成這樣:

    https://ithelp.ithome.com.tw/upload/images/20221016/20142505AnAAFDH8jo.png

    這張圖可以看見,接近鏡頭的頂點數值最小,相反的,背對鏡頭的錨點,數值最大。

上面這些流程,也再次解釋了為什麼會出現光暈:

https://ithelp.ithome.com.tw/upload/images/20221016/20142505ZzKi7x5nBQ.png

我們釐清了關鍵問題,現在順一遍程式碼

所以說,我們回頭看到一開始討論的這行程式碼:

varying vec3 vertexNormal;
void main(void){
+		float intensity = 1.05 - dot(vertexNormal, vec3(0.,0.,1.));
		vec3 atmosphere = vec3(.3, .6, 1.) * intensity;
		gl_FragColor=vec4(0.,0.,0.2,1.);
		gl_FragColor=vec4(atmosphere,0.) + vec4(0.,0.,0.2,1.);
}

綜合目前的所討論的結果,我們就可以推敲這行程式碼的意圖:

  1. vertexNormal 就是像素所在位置的法線單位向量(根據上一篇)
  2. 它計算vertexNormal以及正Z軸單位向量的內積(透過dot()),所以說,如果該像素的法線越接近正z軸,則數值越接近1;如果該像素的法線越遠離正Z軸,則數值越接近-1。
  3. 由第二點可得知,dot() 會回傳-1到1之間的數值(視法線方向而定)。而它加上了負值(1.05 - dot()就是-dot() + 1.05),變成1(偏離正z軸)到-1(接近正z軸)的數值。
  4. 1~-1的數值加上1.05,使數值區間變成2.05(偏離正z軸)到0.05(接近正z軸)。
  5. 所以說,intensity代表法線跟z軸的相似程度。若法線剛好跟正z軸同方向,則為0.05,如果相反則為2.05

而這樣的數值,達到了光暈的效果。光暈就是邊界比較明亮(intensity 數值大),中心比較陰暗的效果:

https://ithelp.ithome.com.tw/upload/images/20221016/20142505X0JRZXI9lS.png

透過Intensity生成顏色

現在我們已經知道Intensity使得每個像素呈現的數值不同,而這將生成光暈。

但這並沒有完整的解釋光暈是如何從一個數值(intensity)變成顏色的。以下解釋:

透過Intensity生成顏色

觀察intensity後續怎麼被使用的話,我們可以看到,它乘上了一個三維數值,並被宣告成atmosphere

varying vec3 vertexNormal;
void main(void){
  float intensity = 1.05 - dot(vertexNormal, vec3(0.,0.,1.));
  vec3 atmosphere = vec3(.3, .6, 1.) * intensity;
  gl_FragColor=vec4(atmosphere,0.) + vec4(0.2,0.2,0.4,1.);
}

基本上,atmosphere 就是藍色(vec3(.3, .6, 1.) )乘上像素的亮度,使得藍色在球體最靠近鏡頭的地方黯淡,而在球體靠近邊界的地方明亮,形成內光暈。

透過atmosphere將顏色加到球體上

有了atmosphere ,只要再加上球體本身的顏色(vec4(0.2,0.2,0.4,1.)),就可以計算出每顆像素最終的顏色。所以最後一行正是在疊加顏色。

gl_FragColor=vec4(atmosphere,0.) + vec4(0.2,0.2,0.4,1.);

成品

https://ithelp.ithome.com.tw/upload/images/20221016/20142505InwhAoPrh9.png

延伸嘗試

而身為視覺特效,內光暈基本上是創造地球最重要的特效之一,當我們理解內光暈之後,才能夠加以發揮。

本次的內光暈,純粹使用intensity來控制而已,就像這張圖一樣,只有線性的變化。

https://ithelp.ithome.com.tw/upload/images/20221016/20142505AXkE8hSA4e.png

但我們還可以有很多種變化,例如:

  • 透過mod()做出階梯的視覺感

    mod函式:

    https://ithelp.ithome.com.tw/upload/images/20221016/20142505bJWipctw4N.png

    varying vec3 vertexNormal;
    void main(void){
      float intensity = 1.05 - dot(vertexNormal, vec3(0.,0.,1.));
    -	vec3 atmosphere = vec3(.3, .6, 1.) * intensity;
    + vec3 atmosphere = vec3(.6, .4, 0.) * mod(intensity, 0.1) * 5.; // <= 使用mod限制範圍
      gl_FragColor=vec4(atmosphere,0.) + vec4(0.8,0.6,0.1,1.);
    }
    

    https://ithelp.ithome.com.tw/upload/images/20221016/201425052Umkx2IBPa.png

    阻尼器是你?!

  • 更對比的內光暈

    透過pow()產生指數的變化

    https://ithelp.ithome.com.tw/upload/images/20221016/20142505LTfIzcplq4.png

    varying vec3 vertexNormal;
    void main(void){
      float intensity = 1.05 - dot(vertexNormal, vec3(0.,0.,1.));
    -	vec3 atmosphere = vec3(.3, .6, 1.) * intensity;
    + vec3 atmosphere = vec3(.3, .6, 1.) * pow(intensity,2.);
      gl_FragColor=vec4(atmosphere,0.) + vec4(0.2,0.2,0.4,1.);
    }
    

    https://ithelp.ithome.com.tw/upload/images/20221016/20142505NeixSyJfLt.png

  • 前後對稱的球體

    透過sin()限制數值由-1到1

    https://ithelp.ithome.com.tw/upload/images/20221016/20142505IQpd9iGRFg.png

    varying vec3 vertexNormal;
    void main(void){
      float intensity = 1.5 - dot(vertexNormal, vec3(0.,0.,1.));
    -	vec3 atmosphere = vec3(.3, .6, 1.) * intensity;
    + vec3 atmosphere = vec3(.3, .6, 1.) * sin(intensity);
      gl_FragColor=vec4(atmosphere,0.) + vec4(0.2,0.2,0.4,1.);
    }
    

    Untitled

  • 詭異的環

    由於sin()只能在1單位內變化,我們可以放大它的變化。

    varying vec3 vertexNormal;
    void main(void){
    	float intensity = 1.45 - dot(vertexNormal, vec3(0.,0.,1.));
    -	vec3 atmosphere = vec3(.3, .6, 1.) * intensity;
    +	vec3 atmosphere = vec3(.3, .6, 1.) * sin(intensity*20.);
    	gl_FragColor=vec4(atmosphere,0.) + vec4(0.2,0.2,0.4,1.);
    }
    

    Untitled

  • 比較柔和的漸層

    透過ceil()產生階層感。

    https://ithelp.ithome.com.tw/upload/images/20221016/20142505dutq8OTYId.png

    varying vec3 vertexNormal;
    void main(void){
      float intensity = 1.45 - dot(vertexNormal, vec3(0.,0.,1.));
    -	vec3 atmosphere = vec3(.3, .6, 1.) * intensity;
    + vec3 atmosphere = vec3(.3, .6, 1.) * ceil(intensity*10.)/10.;
      gl_FragColor=vec4(atmosphere,0.) + vec4(0.2,0.2,0.4,1.);
    }
    

    Untitled

CodePen

https://codepen.io/umas-sunavan/pen/QWrYqwq?editors=1010

小結

本篇解釋許多核心概念。並且用圖解的方式解釋函式的成因。

我們透過normal來製作球體特效。

本篇我們學到:

  1. dot()的作用
  2. Intensity 生成顏色
  3. 利用向量生成多種樣式的球體

下一篇我們將解決視角問題。現在還有視角問題,意思是:一旦鏡頭離開正Z軸視角,我們的內光暈就破功了。我們將在下一篇探討。

Untitled

參考資料

經典大作Book of shader的說明

GLSL提供的函式

WebGLProgram的說明


上一篇
Day27: WebGL Shader—透過Shader製作光暈:Shader傳值的原理
下一篇
Day29: WebGL Shader—用Shader做全視角內光暈、星球材質
系列文
30天成為鍵盤麥可貝:前端視覺特效開發實戰31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言