顏色
我們在現(xiàn)實生活中看到某一物體的顏色并不是這個物體真正擁有的顏色,而是它所反射的(Reflected)顏色。當(dāng)我們在OpenGL中創(chuàng)建一個光源時,我們希望給光源一個顏色。當(dāng)我們把光源的顏色與物體的顏色值相乘,所得到的就是這個物體所反射的顏色
(也就是我們所感知到的顏色)。
顏色計算 把光源的顏色與物體的顏色值相乘
基礎(chǔ)光照
現(xiàn)實的光照非常復(fù)雜,因此OpenGL的光照使用的是簡化的模型,對現(xiàn)實的情況進(jìn)行近似,這樣處理起來會更容易一些,而且看起來也差不多一樣。其中一個模型被稱為馮氏光照模型(Phong Lighting Model)。
馮氏光照模型的主要結(jié)構(gòu)由3個分量組成:環(huán)境(Ambient)、漫反射 (Diffuse)和鏡面(Specular)光照
- 環(huán)境光照(Ambient Lighting):物體幾乎永遠(yuǎn)不會是完全黑暗的。所以環(huán)境光照一般是個常量
- 漫反射光照(Diffuse Lighting):模擬光源對物體的方向性影響,物體的某一部分越是正對著光源,它就會越亮。
- 鏡面光照(Specular Lighting):模擬有光澤物體上面出現(xiàn)的亮點。鏡面光照的顏色相比于物體的顏色會更傾向于光的顏色。
光照公式
最終片段顏色:環(huán)境顏色+漫反射顏色+鏡面反射顏色
- 環(huán)境顏色 = 光源的環(huán)境光顏色 × 物體的環(huán)境材質(zhì)顏色
- 漫反射顏色 = 光源的漫反射光顏色 × 物體的漫反射材質(zhì)顏色 × 漫反射因子
漫反射
DiffuseFactor = max(0, dot(N, L))
-
鏡面反射顏色 = 光源的鏡面光顏色 × 物體的鏡面材質(zhì)顏色 × 鏡面反射因子
鏡面光照
R=reflect(L, N)
SpecularFactor = power(max(0, dot(R,V)), shininess)
我們的測試模型如下紅色正方體
下面是正常正方體光照圖
頂點著色器
attribute vec3 beginPostion; ///開始位置
attribute vec3 vertexColor;
uniform mat4 u_mvpMatrix;
varying lowp vec3 vary_vertexColor;
void main(){
gl_Position =u_mvpMatrix * vec4(beginPostion, 1.0);
vary_vertexColor = vertexColor;
}
片段著色器
precision lowp float;
varying lowp vec3 vary_vertexColor;
void main()
{
gl_FragColor =vec4( vary_vertexColor,1.0);
}
環(huán)境光照
我們使用一個很小的常量(光照)顏色,添加到物體片段的最終顏色中,這樣子的話即便場景中沒有直接的光源也能看起來存在有一些發(fā)散的光。
把環(huán)境光照添加到場景里非常簡單。我們用光的顏色乘以一個很小的常量環(huán)境因子,再乘以物體的顏色,然后將最終結(jié)果作為片段的顏色:
頂點著色器
attribute vec3 beginPostion; ///開始位置
attribute vec3 vertexColor;
uniform mat4 u_mvpMatrix;
varying lowp vec3 vary_vertexColor;
void main(){
gl_Position =u_mvpMatrix * vec4(beginPostion, 1.0);
vary_vertexColor = vertexColor;
}
片段著色器
precision lowp float;
varying lowp vec3 vary_vertexColor;
uniform vec4 ambientLight; ///環(huán)境光
void main()
{
float ambientStrength = 0.2;
vec4 ambient = ambientStrength * ambientLight;
gl_FragColor = ambient * vec4( vary_vertexColor,1.0);;
}
漫反射光照
漫反射光照使物體上與光線方向越接近的片段能從光源處獲得更多的亮度。為了能夠更好的理解漫反射光照,請看下圖:
計算漫反射光照需要:
+ 法向量:一個垂直于頂點表面的向量。
+ 定向的光線:作為光源的位置與片段的位置之間向量差的方向向量。為了計算這個光線,我們需要光的位置向量和片段的位置向量。
法向量一般作為頂點的屬性傳入頂點著色器中
attribute vec3 aNormal; //法向量
所有光照的計算都是在片段著色器里進(jìn)行,所以我們需要將法向量由頂點著色器傳遞到片段著色器。我們這么做:
varying lowp vec3 normal;
void main(){
normal = aNormal;;
}
我們現(xiàn)在對每個頂點都有了法向量,但是我們?nèi)匀恍枰庠吹奈恢孟蛄亢推蔚奈恢孟蛄俊S捎诠庠吹奈恢檬且粋€靜態(tài)變量,我們可以簡單地在片段著色器中把它聲明為uniform。然后在渲染循環(huán)中(渲染循環(huán)的外面也可以,因為它不會改變)更新uniform。
最后,我們還需要片段的位置。我們會在世界空間中進(jìn)行所有的光照計算,因此我們需要一個在世界空間中的頂點位置。我們可以通過把頂點位置屬性乘以模型矩陣(不是觀察和投影矩陣)來把它變換到世界空間坐標(biāo)。這個在頂點著色器中很容易完成,所以我們聲明一個輸出變量,并計算它的世界空間坐標(biāo):
attribute vec3 beginPostion; ///開始位置
attribute vec3 vertexColor;
attribute vec3 aNormal; //法向量
uniform mat4 u_mvpMatrix;
uniform mat4 u_model;
uniform mat4 u_inverModel;
varying lowp vec3 normal;
varying lowp vec3 FragPos;
varying lowp vec3 vary_vertexColor;
void main(){
gl_Position =u_mvpMatrix *u_model* vec4(beginPostion, 1.0);
vary_vertexColor = vertexColor;
FragPos = vec3(u_model * vec4(beginPostion, 1.0));
normal = aNormal;;
}
最后,在片段著色器中添加相應(yīng)的輸入變量。現(xiàn)在,所有需要的變量都設(shè)置好了,我們可以在片段著色器中添加光照計算了。
我們需要做的第一件事是計算光源和片段位置之間的方向向量。前面提到,光的方向向量是光源位置向量與片段位置向量之間的向量差。我們希望確保所有相關(guān)向量最后都轉(zhuǎn)換為單位向量,所以我們把法線和最終的方向向量都進(jìn)行標(biāo)準(zhǔn)化:
void main()
{
vec3 norm = normalize(normal);
vec3 lightDir = normalize(lightPos - FragPos);
}
一步,我們對norm和lightDir向量進(jìn)行點乘,計算光源對當(dāng)前片段實際的漫發(fā)射影響。結(jié)果值再乘以光的顏色,得到漫反射分量。兩個向量之間的角度越大,漫反射分量就會越小:
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
如果兩個向量之間的角度大于90度,點乘的結(jié)果就會變成負(fù)數(shù),這樣會導(dǎo)致漫反射分量變?yōu)樨?fù)數(shù)。為此,我們使用max函數(shù)返回兩個參數(shù)之間較大的參數(shù),從而保證漫反射分量不會變成負(fù)數(shù)。現(xiàn)在我們有了環(huán)境光分量和漫反射分量,我們把它們相加,然后把結(jié)果乘以物體的顏色,來獲得片段最后的輸出顏色。
vec3 diffuse = diff * lightColor;
gl_FragColor =vec4(diffuse,1.0) * vec4( vary_vertexColor,1.0);;
現(xiàn)在我們已經(jīng)把法向量從頂點著色器傳到了片段著色器。可是,目前片段著色器里的計算都是在世界空間坐標(biāo)中進(jìn)行的。所以,我們是不是應(yīng)該把法向量也轉(zhuǎn)換為世界空間坐標(biāo)?基本正確,但是這不是簡單地把它乘以一個模型矩陣就能搞定的。
首先,法向量只是一個方向向量,不能表達(dá)空間中的特定位置。同時,法向量沒有齊次坐標(biāo)(頂點位置中的w分量)。這意味著,位移不應(yīng)該影響到法向量。因此,如果我們打算把法向量乘以一個模型矩陣,我們就要從矩陣中移除位移部分,只選用模型矩陣左上角3×3的矩陣(注意,我們也可以把法向量的w分量設(shè)置為0,再乘以4×4矩陣;這同樣可以移除位移)。對于法向量,我們只希望對它實施縮放和旋轉(zhuǎn)變換。
其次,如果模型矩陣執(zhí)行了不等比縮放,頂點的改變會導(dǎo)致法向量不再垂直于表面了。因此,我們不能用這樣的模型矩陣來變換法向量。下面的圖展示了應(yīng)用了不等比縮放的模型矩陣對法向量的影響:
每當(dāng)我們應(yīng)用一個不等比縮放時(注意:等比縮放不會破壞法線,因為法線的方向沒被改變,僅僅改變了法線的長度,而這很容易通過標(biāo)準(zhǔn)化來修復(fù)),法向量就不會再垂直于對應(yīng)的表面了,這樣光照就會被破壞。
修復(fù)這個行為的訣竅是使用一個為法向量專門定制的模型矩陣。這個矩陣稱之為法線矩陣(Normal Matrix),「模型矩陣左上角的逆矩陣的轉(zhuǎn)置矩陣」。
在頂點著色器中,我們可以使用inverse和transpose函數(shù)自己生成這個法線矩陣,這兩個函數(shù)對所有類型矩陣都有效。注意我們還要把被處理過的矩陣強制轉(zhuǎn)換為3×3矩陣,來保證它失去了位移屬性以及能夠乘以vec3的法向量。
uniform mat4 u_inverModel;
void main(){
normal = mat3(u_inverModel) * aNormal;;
}
給頂點傳值代碼
bool isSuccess = YES;
mode = GLKMatrix4InvertAndTranspose(mode,&isSuccess);
glUniformMatrix4fv(self.bindObject->uniforms[CubeDiffuseLightingUniformLocationInvermodel], 1, 0,mode.m);
如果你進(jìn)行了不等比縮放,使用法線矩陣去乘以法向量就是必不可少的了。即使是對于著色器來說,逆矩陣也是一個開銷比較大的運算,因此,對于一個對效率有要求的應(yīng)用來說,在繪制之前你最好用CPU計算出法線矩陣,然后通過uniform把值傳遞給著色器(像模型矩陣一樣)
完整shader 代碼
頂點著色器
precision lowp float;
uniform vec3 lightPos; ///光源位置
uniform vec3 lightColor; ///光源位置
varying lowp vec3 normal;
varying lowp vec3 FragPos;
varying lowp vec3 vary_vertexColor;
void main()
{
vec3 norm = normalize(normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
///環(huán)境光的使用
gl_FragColor =vec4(diffuse,1.0) * vec4( vary_vertexColor,1.0);;
}
片段著色器
attribute vec3 beginPostion; ///開始位置
attribute vec3 vertexColor;
attribute vec3 aNormal; //法向量
uniform mat4 u_mvpMatrix;
uniform mat4 u_model;
uniform mat4 u_inverModel;
varying lowp vec3 normal;
varying lowp vec3 FragPos;
varying lowp vec3 vary_vertexColor;
void main(){
gl_Position =u_mvpMatrix *u_model* vec4(beginPostion, 1.0);
vary_vertexColor = vertexColor;
FragPos = vec3(u_model * vec4(beginPostion, 1.0));
normal = mat3(u_inverModel) * aNormal;;
}
- 這里沒有探討具體求解法線矩陣
鏡面光照
和漫反射光照一樣,鏡面光照也是依據(jù)光的方向向量和物體的法向量來決定的,但是它也依賴于觀察方向,例如玩家是從什么方向看著這個片段的。鏡面光照是基于光的反射特性。如果我們想象物體表面像一面鏡子一樣,那么,無論我們從哪里去看那個表面所反射的光,鏡面光照都會達(dá)到最大化。你可以從下面的圖片看到效果:
從上圖中我們知道求鏡面光照需要的參數(shù)有
光源位置
法向量
-
眼睛的位置
我們通過反射法向量周圍光的方向來計算反射向量。然后我們計算反射向量和視線方向的角度差,如果夾角越小,那么鏡面光的影響就會越大。它的作用效果就是,當(dāng)我們?nèi)タ垂獗晃矬w所反射的那個方向的時候,我們會看到一個高光。
觀察向量是鏡面光照附加的一個變量,我們可以使用觀察者世界空間位置和片段的位置來計算它。之后,我們計算鏡面光強度,用它乘以光源的顏色,再將它加上環(huán)境光和漫反射分量。
為了得到觀察者的世界空間坐標(biāo),我們簡單地使用攝像機(jī)對象的位置坐標(biāo)代替(它當(dāng)然就是觀察者)。所以我們把另一個uniform添加到片段著色器,把相應(yīng)的攝像機(jī)位置坐標(biāo)傳給片段著色器:
uniform vec3 viewPos;
現(xiàn)在我們已經(jīng)獲得所有需要的變量,可以計算高光強度了。首先,我們定義一個鏡面強度(Specular Intensity)變量,給鏡面高光一個中等亮度顏色,讓它不要產(chǎn)生過度的影響。
float specularStrength = 0.5;
下一步,我們計算視線方向向量,和對應(yīng)的沿著法線軸的反射向量:
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
需要注意的是我們對lightDir向量進(jìn)行了取反。reflect函數(shù)要求第一個向量是從光源指向片段位置的向量,但是lightDir當(dāng)前正好相反,是從片段指向光源(由先前我們計算lightDir向量時,減法的順序決定)。為了保證我們得到正確的reflect向量,我們通過對lightDir向量取反來獲得相反的方向。第二個參數(shù)要求是一個法向量,所以我們提供的是已標(biāo)準(zhǔn)化的norm向量。
剩下要做的是計算鏡面分量。下面的代碼完成了這件事:
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
我們先計算視線方向與反射方向的點乘(并確保它不是負(fù)值),然后取它的32次冪。這個32是高光的反光度(Shininess)。一個物體的反光度越高,反射光的能力越強,散射得越少,高光點就會越小。
shader 代碼
attribute vec3 beginPostion; ///開始位置
attribute vec3 vertexColor;
attribute vec3 aNormal; //法向量
uniform mat4 u_mvpMatrix;
uniform mat4 u_model;
uniform mat4 u_inverModel;
varying lowp vec3 normal;
varying lowp vec3 FragPos;
varying lowp vec3 vary_vertexColor;
void main(){
gl_Position =u_mvpMatrix *u_model* vec4(beginPostion, 1.0);
vary_vertexColor = vertexColor;
FragPos = vec3(u_model * vec4(beginPostion, 1.0));
normal = mat3(u_inverModel) * aNormal;;
}
precision lowp float;
uniform vec3 lightPos; ///光源位置
uniform vec3 lightColor; ///光源位置
uniform vec3 viewPos;
varying lowp vec3 normal;
varying lowp vec3 FragPos;
varying lowp vec3 vary_vertexColor;
void main()
{
float specularStrength = 0.5;
vec3 norm = normalize(normal);
vec3 lightDir = normalize(lightPos - FragPos);
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 256.0);
vec3 specular = specularStrength * spec * lightColor;
///環(huán)境光的使用
gl_FragColor =vec4(specular,1.0) * vec4( vary_vertexColor,1.0);;
}