iT邦幫忙

2022 iThome 鐵人賽

DAY 27
0
Software Development

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

Day27: WebGL Shader—透過Shader製作光暈:Shader傳值的原理

  • 分享至 

  • xImage
  •  

本篇內容

  1. vertexNormal 是什麼?
  2. 兩個Shader之間的傳值方式
  3. WebGLProgram 添加變數的特性
  4. vertexNormal 從哪裡來?
  5. 視覺化SphereGeometry 的法線向量位置

什麼是「神秘的程式碼」

上一篇,我們加上了神秘的程式碼,突然之間球體就有了光暈。

+ 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.);
	}

沒有修改之前:

https://ithelp.ithome.com.tw/upload/images/20221016/201425059WLJ3ScJZW.png

修改之後:

https://ithelp.ithome.com.tw/upload/images/20221016/20142505rAo1ZCdJ0A.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. fragment shader宣告了一個varying ,代表vertexNormal 是一個三維變數,而且每個像素的vertexNormal 可能都不太一樣。vertexNormal 有數值嗎?如果有的話,數值是從哪裡來的?怎麼都沒有看到它被賦值?
  2. vertexNormal 被放進了函式dot() 的參數。dot()是在哪裡宣告的?它的作用是什麼?
  3. 除了這兩個不確定以外,我們確定intensity 是一個浮點數,它後來乘上了一個三維數值,形成atmosphere
  4. atmosphere 最終用作計算gl_FragColor的顏色,所以atmosphere 是一個顏色。
  5. gl_FragColor的顏色除了atmosphere 以外,還被加上了一點點的藍色(vec4(0.,0.,0.2,1.)

這五點中,最大的謎團就是第一點跟第二點。基本上,只要解開第一點跟第二點,我們就可以破解光暈的實作原理了。

前兩篇,我們先講原理,再討論實作方式。這樣的作法非常教科書。但很多時候我們在Shader研究一個原理,順序往往是倒過來的。意思是,我們常常會先看到一個很好的Shader程式碼,但不知道其原理。

所以從本篇開始,我會從程式碼去推敲其作用,並且延伸其作用的概念,最後講解整個原理。

好的,我們針對第一點跟第二點開始講解,而本篇(Day27)將解開第一點,下一篇(Day28)將解開第二點。

vertexNormal 有值嗎?如果有的話,數值是從哪裡來的?

vertexNormal 有值嗎:vertexNormal 是什麼?

先觀察vertexNormal。它的來源,其實是來自vertex shader。

// vertex shader
varying vec3 vertexNormal; // <=從這裡來
void main(void){
  vertexNormal = normal; // <=被賦予normal數值
  gl_Position=projectionMatrix*modelViewMatrix*vec4(position, 1.0);
}

vertexNormal 在vertext shader被宣告,而被賦了normal值。

現在我們越看越含糊了,你會問:

  1. 為什麼vertex shader可以將數值傳送給的fragment shader?前者每一個錨點執行一次,後者每顆像素執行一次,那到底是什麼時候傳值的?
  2. normal是打哪裡來的變數?

兩個問題我下面回答:

vertexNormal 有值嗎:兩個Shader之間的傳值

事實上,vertex shader可以將錨點資料傳送給fragment shader。我們在「Day10: three.js 前端視覺特效工程師實戰:全球戰情室—貼圖原理」有提到:當fragment shader要「採樣(sample)」材質圖來幫每一顆像素上色時,必須透過vertext shader來確認UV位置。如此一來,才能從材質圖中找到正確的位置採樣。

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

由此可知,vertex shader一直都能夠將重要的數值傳遞給fragment shader加做使用。當然,如果fragment shader要「接住」來自vertex shader的變數,仍需要在fragment shader宣告同樣名字的變數。

// vertexShader
varying vec3 vertexNormal; // 宣告了vertexNormal
void main(void){
  vertexNormal = normal;
  gl_Position=projectionMatrix*modelViewMatrix*vec4(position, 1.0);
}
// fragmentShader
varying vec3 vertexNormal; // <== 雖然vertex shader有宣告,但我們在fragment shader也需要宣告同樣的數值
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.);
}

宣告時,我們宣告它是varying,而且是一個vec3。它之所以是varing,是因為varying最大的特色,是他在執行每一個錨點時,值都不太相同。而這樣的特性剛好跟uniform 相反。有興趣可以從前一篇「Day24: WebGL Shader——快認識速Shader並使用Variable Qualifiers」深入理解。

vertexNormal 有值嗎:varying 傳值的邏輯

varying可以在每一次vertex shader計算錨點時,依據錨點的資訊(例如位置、UV、法線)變化。

所以如果你有一個平面,平面有四個頂點,那麼vertex shader會執行四次,以取得渲染範圍,與此同時,vertex shader也會傳送位置、UV、法線到fragment shader。

一旦fragment shader接收到數值之後,可以依照像素的位置,「推估」出像素所在的模性位置其UV、法線跟位置本身。推估的方式就是利用插值(Interpolation),這個邏輯就是用位置去推算像素所在位置的值。

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

插值可以「推估」自身的數值,這使得fragment shader可以推估出varying的數值。

而至於它為何之所以是vec3,則是因為它在vertex shader的源頭變數是vec3。兩者得相符才能順利傳值。

總之,以上解釋vertex shader跟fragment shader之間傳遞數值的關係。我們可以用一張圖解釋:

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

從圖片可以看到,fragment shader的vertexNormal 是從vertex shader來的,但vertexNormal的數值又是normal給的,那normal的數值從哪裡來?接下來我們繼續追溯這個來源。

vertexNormal 有值嗎:被偷偷加上去(Prepend)的程式碼

目前我們從程式碼可以得知:vertexNormal的數值來自normal,而normal則不知道來自何方。我們沒有宣告,它也沒有出現在任何一處。

為什麼會有這個變數?誰宣告它?它的資料怎麼來?

答案是:當shader在編譯時,早就被偷偷宣告變數了。

// vertexShader
varying vec3 vertexNormal; // <== 雖然vertex shader有宣告,但我們在fragment shader也需要宣告同樣的數值
void main(void){
  vertexNormal = normal;
  gl_Position=projectionMatrix*modelViewMatrix*vec4(position, 1.0);
}

當我們在three.js實例化WebGLRenderer 時,WebGLRenderer 就負責WebgL渲染的大小事。WebGLRenderer渲染畫面以外,也令WebGLProgram 在編譯vertext shader以及fragment shader之前,宣告並加上了特定的變數。

總共有哪些變數?可以參考WebGLProgram的說明WebGLProgram掌控其底下的fragment shader以及vertex shader,同時也能夠添加變數。雖然我們看不到,但我們很肯定它將會在編譯之前先宣告。

這也使得我們可以任意取用,畢竟編譯之前怎麼宣告都可以。

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

這也是為什麼當你Shader出現Error時,會有一大堆你明明沒宣告過的東西。

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

這就是底層原因。所以normal是出自WebGLProgram,那WebGLProgram給它什麼數值呢?

如果你翻開three.js官網的話,答案很快就會出現:

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

normal連同position跟uv,從geometry而來。

而這裡的Geometry是指哪裡呢?指的是我們javascript的程式碼中,由three.js實例化SphereGeometry時就定義出來的球形。

const geo = new SphereGeometry(5,50,50) // <=從這裡取得
const mat = new ShaderMaterial({
  fragmentShader: fragText,
  vertexShader: vertText,
})
const mesh = new Mesh(geo, mat)

SphereGeometry 在實例化時,就會產生形狀所需的錨點。這跟其它種Geometry 一樣,以一個PlaneGeometry來說至少會產生四個頂點,一個BoxGeometry至少要有八個錨點。

這些錨點將會在實例化時生成,除了生成位置(position)資訊以外,也會生成法線(normal)以及UV(uv)。但當我們建立Mesh時,vertex shader就可以存取normalposition以及uv

所以關於vertexNormal怎麼來的,我們已經解開神秘面紗了。vertexNormal最源頭,是從three.js中的SphereGeometry 來的。

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

如果我們在three.js加上一個helper,觀察其normal的向量,我們可以看到法線的方向。

const addSphere = async () => {
	const vertex = document.getElementById('vertexShader').innerHTML
	const fragment = document.getElementById('fragmentShader').innerHTML
    const geo = new THREE.SphereGeometry(5,50,50)
    const mat = new THREE.ShaderMaterial({
		fragmentShader: fragment,
		vertexShader: vertex
	  })
  const mesh = new THREE.Mesh(geo, mat)
  scene.add(mesh)
+	const helper = new VertexNormalsHelper( mesh, 1, 0xff0000 );
+	scene.add( helper );
  return mesh
}

加上程式碼之後,就可以看到,這些normal的向量。這些向量是在我們透過SphereGeometry 實例化球體時所建立的。

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

講到這裡,我們就已經破解了vertexNormal 的由來。vertexNormal 就是球體的法線。

雖然如此,我們仍然必須釐清第二個問題:vertexNormal 被放進了函式dot() 的參數。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.);
}

這個問題,將在下一篇解答。雖然我們還沒完全釐清神秘程式碼,但我們在釐清問題時,已經認識了好多原理。這些原理包含:

  1. fragment透過插值(Interpolation)取得任何類別為varying的數值
  2. WebGLProgram在編譯vertext shader以及fragment shader之前,會prepend特定變數
  3. prepend的數值中,會有來自three.js的Geometry數值
  4. SphereGeometry 的法線向量位置

參考資料

fragment shader的插值方式

VertexNormalsHelper


上一篇
Day26: WebGL Shader—透過Shader製作光暈:速成篇
下一篇
Day28: WebGL Shader—透過Shader製作光暈:光暈原理與多種變化形式
系列文
30天成為鍵盤麥可貝:前端視覺特效開發實戰31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言