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
是從哪裡來的。
既然已經解釋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()
如果不懂,我就用圖片說明:
現在我們有一個解析度超級低的球體,我們從正Y軸往下看這個場景,也會看到鏡頭位於正Z的位置:
這顆球有很多個面,每一個頂點都有一個法線輻射狀指向四方。
每一個法線,都可以拆成x,y,z數值
如果光看球體於xz剖面來看,可以發現面對鏡頭的頂點,z值最大,離正z軸(0,0,1)最相近,內積相乘之後最大。背對鏡頭的頂點z值最小(因為指向負z),離(0,0,1)最遙遠,相乘之後最小。
如果全部變成了負數,又加上1.05,就會變成這樣:
這張圖可以看見,接近鏡頭的頂點數值最小,相反的,背對鏡頭的錨點,數值最大。
上面這些流程,也再次解釋了為什麼會出現光暈:
所以說,我們回頭看到一開始討論的這行程式碼:
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
就是像素所在位置的法線單位向量(根據上一篇)vertexNormal
以及正Z軸單位向量的內積(透過dot()
),所以說,如果該像素的法線越接近正z軸,則數值越接近1;如果該像素的法線越遠離正Z軸,則數值越接近-1。dot()
會回傳-1到1之間的數值(視法線方向而定)。而它加上了負值(1.05 - dot()
就是-dot() + 1.05
),變成1(偏離正z軸)到-1(接近正z軸)的數值。intensity
代表法線跟z軸的相似程度。若法線剛好跟正z軸同方向,則為0.05,如果相反則為2.05而這樣的數值,達到了光暈的效果。光暈就是邊界比較明亮(intensity
數值大),中心比較陰暗的效果:
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.);
而身為視覺特效,內光暈基本上是創造地球最重要的特效之一,當我們理解內光暈之後,才能夠加以發揮。
本次的內光暈,純粹使用intensity
來控制而已,就像這張圖一樣,只有線性的變化。
但我們還可以有很多種變化,例如:
透過mod()
做出階梯的視覺感
mod函式:
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.);
}
阻尼器是你?!
更對比的內光暈
透過pow()
產生指數的變化
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.);
}
前後對稱的球體
透過sin()
限制數值由-1到1
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.);
}
詭異的環
由於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.);
}
比較柔和的漸層
透過ceil()產生階層感。
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.);
}
https://codepen.io/umas-sunavan/pen/QWrYqwq?editors=1010
本篇解釋許多核心概念。並且用圖解的方式解釋函式的成因。
我們透過normal來製作球體特效。
本篇我們學到:
dot()
的作用Intensity
生成顏色下一篇我們將解決視角問題。現在還有視角問題,意思是:一旦鏡頭離開正Z軸視角,我們的內光暈就破功了。我們將在下一篇探討。