WebGLProgram
添加變數的特性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.);
}
沒有修改之前:
修改之後:
這之間發生什麼事?為什麼加上這幾行,就會出現光暈?背後的原理是什麼?本篇將釐清這「神秘程式碼」的邏輯。
我們再重新看一次程式碼:
+ 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.);
}
已目前我們所能釐清的事情來看,我們知道幾件事情:
varying
,代表vertexNormal
是一個三維變數,而且每個像素的vertexNormal
可能都不太一樣。vertexNormal
有數值嗎?如果有的話,數值是從哪裡來的?怎麼都沒有看到它被賦值?vertexNormal
被放進了函式dot()
的參數。dot()
是在哪裡宣告的?它的作用是什麼?intensity
是一個浮點數,它後來乘上了一個三維數值,形成atmosphere
atmosphere
最終用作計算gl_FragColor
的顏色,所以atmosphere
是一個顏色。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
值。
現在我們越看越含糊了,你會問:
normal
是打哪裡來的變數?兩個問題我下面回答:
vertexNormal
有值嗎:兩個Shader之間的傳值事實上,vertex shader可以將錨點資料傳送給fragment shader。我們在「Day10: three.js 前端視覺特效工程師實戰:全球戰情室—貼圖原理」有提到:當fragment shader要「採樣(sample)」材質圖來幫每一顆像素上色時,必須透過vertext shader來確認UV位置。如此一來,才能從材質圖中找到正確的位置採樣。
由此可知,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),這個邏輯就是用位置去推算像素所在位置的值。
插值可以「推估」自身的數值,這使得fragment shader可以推估出varying
的數值。
而至於它為何之所以是vec3
,則是因為它在vertex shader的源頭變數是vec3。兩者得相符才能順利傳值。
總之,以上解釋vertex shader跟fragment shader之間傳遞數值的關係。我們可以用一張圖解釋:
從圖片可以看到,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,同時也能夠添加變數。雖然我們看不到,但我們很肯定它將會在編譯之前先宣告。
這也使得我們可以任意取用,畢竟編譯之前怎麼宣告都可以。
這也是為什麼當你Shader出現Error時,會有一大堆你明明沒宣告過的東西。
這就是底層原因。所以normal
是出自WebGLProgram
,那WebGLProgram
給它什麼數值呢?
如果你翻開three.js官網的話,答案很快就會出現:
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就可以存取normal
、position
以及uv
。
所以關於vertexNormal
怎麼來的,我們已經解開神秘面紗了。vertexNormal
最源頭,是從three.js中的SphereGeometry
來的。
如果我們在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
實例化球體時所建立的。
講到這裡,我們就已經破解了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.);
}
這個問題,將在下一篇解答。雖然我們還沒完全釐清神秘程式碼,但我們在釐清問題時,已經認識了好多原理。這些原理包含:
varying
的數值WebGLProgram
在編譯vertext shader以及fragment shader之前,會prepend特定變數Geometry
數值SphereGeometry
的法線向量位置