本節書摘來自異步社區《OpenGL ES 2.0游戲開發(上卷):基礎技術和典型案例》一書中的第6章,第6.5節光照的每頂點計算與每片元計算,作者 吳亞峰,更多章節內容可以訪問云棲社區“異步社區”公眾號查看
6.5 光照的每頂點計算與每片元計算
OpenGL ES 2.0游戲開發(上卷):基礎技術和典型案例
細心的讀者會發現,本章前面的案例都是在頂點著色器中進行光照計算的。這是由于在頂點著色器中對每個頂點進行光照計算后得到頂點的最終光照強度,再由管線插值后傳入片元著色器以計算片元的顏色,這樣一方面效率比較高;另一方面產生的光照效果也不錯。
但由于這種計算方式插值的是基于頂點計算后的光照強度,因此在要求很高,希望有非常細膩光照效果的場合下就略顯粗糙了。本節將介紹另一種光照計算方式,其首先將插值后的法向量數據傳入片元著色器,然后在片元著色器中進行光照計算。這種新的方式也稱為每片元光照,可以取得為更細膩的光照效果。
進行案例開發之前需要首先了解一下本節兩個案例(Sample6_9和Sample6_10)的運行效果,具體情況如圖6-21所示。

圖6-21中左側是每片元計算一次光照的案例Sample6_9的運行效果,右側是每頂點計算一次光照的案例Sample6_10的運行效果。從兩幅圖的對比中可以看出,每片元執行一次光照使過渡更平滑,沒有明顯的邊緣。另外,僅從圖上觀察可能區別還不是很明顯,筆者建議讀者用真機運行一下兩個案例,將光源設置在不同的位置觀察比較,區別會更明顯。
了解了兩個案例的運行效果后,就可以進行開發了。實際上這兩個案例主要是將前面6.2.5小節中的案例Sample6_5復制并修改了部分代碼而成的。其中Sample6_10僅修改了Java代碼中切割球面的角度以及繪制球體的次數,沒有本質變化,這里不再贅述,需要的讀者請參考隨書光盤中的源代碼。
而案例Sample6_9除了也進行了Sample6_10的Java代碼改動外,還大面積修改了頂點著色器與片元著色器,具體情況如下所列。
(1)首先介紹Sample6_9中修改后的頂點著色器,其具體代碼如下。
1 uniform mat4 uMVPMatrix; //總變換矩陣
2 attribute vec3 aPosition; //頂點位置
3 attribute vec3 aNormal; //法向量
4 varying vec3 vPosition; //用于傳遞給片元著色器的頂點位置
5 varying vec3 vNormal; //用于傳遞給片元著色器的法向量
6 void main(){
7 gl_Position = uMVPMatrix * vec4(aPosition,1);//根據總變換矩陣計算此次繪制此頂點位置
8 vPosition = aPosition; //將頂點的位置傳給片元著色器
9 vNormal = aNormal; //將法向量傳給片元著色器
10 }
從上述代碼中可以看出,頂點著色器比改動前簡單多了,沒有了計算光照的大量代碼,同時增加了將法向量通過易變變量傳入片元著色器的代碼。
(2)介紹完頂點著色器后,接著就應該介紹改動后的片元著色器了,其具體代碼如下。
1 precision mediump float; //給出默認浮點精度
2 uniform float uR; //球的半徑
3 uniform vec3 uLightLocation; //光源位置
4 uniform mat4 uMMatrix; //變換矩陣
5 uniform vec3 uCamera; //攝像機位置
6 varying vec3 vPosition; //接收從頂點著色器傳遞過來的頂點位置
7 varying vec3 vNormal; //接收從頂點著色器傳遞過來的法向量
8 void pointLight( //定位光光照計算的方法
9 in vec3 normal, //法向量
10 inout vec4 ambient, //環境光最終強度
11 inout vec4 diffuse, //散射光最終強度
12 inout vec4 specular, //鏡面光最終強度
13 in vec3 lightLocation, //光源位置
14 in vec4 lightAmbient, //環境光強度
15 in vec4 lightDiffuse, //散射光強度
16 in vec4 lightSpecular //鏡面光強度
17 ){
18 ambient=lightAmbient; //直接得出環境光的最終強度
19 vec3 normalTarget=vPosition+normal; //計算變換后的法向量
20 vec3 newNormal=(uMMatrix*vec4(normalTarget,1)).xyz-(uMMatrix*vec4(vPosition,1)).xyz;
21 newNormal=normalize(newNormal); //對法向量規格化
22 //計算從表面點到攝像機的向量
23 vec3 eye= normalize(uCamera-(uMMatrix*vec4(vPosition,1)).xyz);
24 //計算從表面點到光源位置的向量vp
25 vec3 vp= normalize(lightLocation-(uMMatrix*vec4(vPosition,1)).xyz);
26 vp=normalize(vp);//格式化vp
27 vec3 halfVector=normalize(vp+eye); //求視線與光線的半向量
28 float shininess=50.0; //粗糙度,越小越光滑
29 float nDotViewPosition=max(0.0,dot(newNormal,vp));//求法向量與vp的點積與0的最大值
30 diffuse=lightDiffuse*nDotViewPosition; //計算散射光的最終強度
31 float nDotViewHalfVector=dot(newNormal,halfVector); //法線與半向量的點積
32 float powerFactor=max(0.0,pow(nDotViewHalfVector,shininess));//鏡面反射光強度因子
33 specular=lightSpecular*powerFactor; //計算鏡面光的最終強度
34 }
35 void main() {
36 ……//此處省略了計算片元顏色值的代碼,請讀者自行查看隨書光盤中的源代碼
37 vec4 ambient,diffuse,specular; //用來接收3個通道最終強度的變量
38 pointLight(normalize(vNormal),ambient,diffuse,specular,uLightLocation, //計算定位光各通道強度
39 vec4(0.15,0.15,0.15,1.0),vec4(0.8,0.8,0.8,1.0),vec4(0.7,0.7,0.7,1.0));
40 //綜合3個通道光的最終強度及片元的顏色計算出最終片元的顏色并傳遞給管線
41 gl_FragColor=finalColor*ambient + finalColor*diffuse + finalColor*specular;
42 }
讀者應該發現,上述片元著色器中的很多代碼都是本章前面案例中多次出現過的,只不過前面的案例中都是在頂點著色器中出現,而這里挪到了片元著色器中。因此,每片元計算光照與每頂點計算光照算法并沒有本質區別,只是代碼執行的位置不同、效果與效率不同而已。實際開發中讀者應該權衡速度、效果的要求,選用合適的計算策略。