版本記錄
| 版本號 | 時間 |
|---|---|
| V1.0 | 2017.10.30 |
前言
OpenGL 圖形庫項目中一直也沒用過,最近也想學(xué)著使用這個圖形庫,感覺還是很有意思,也就自然想著好好的總結(jié)一下,希望對大家能有所幫助。
1. OpenGL 圖形庫使用(一) —— 概念基礎(chǔ)
2. OpenGL 圖形庫使用(二) —— 渲染模式、對象、擴(kuò)展和狀態(tài)機(jī)
3. OpenGL 圖形庫使用(三) —— 著色器、數(shù)據(jù)類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
6. OpenGL 圖形庫使用(六) —— 變換
7. OpenGL 圖形庫的使用(七)—— 坐標(biāo)系統(tǒng)之五種不同的坐標(biāo)系統(tǒng)(一)
8. OpenGL 圖形庫的使用(八)—— 坐標(biāo)系統(tǒng)之3D效果(二)
9. OpenGL 圖形庫的使用(九)—— 攝像機(jī)(一)
10. OpenGL 圖形庫的使用(十)—— 攝像機(jī)(二)
11. OpenGL 圖形庫的使用(十一)—— 光照之顏色
基礎(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)處的光),所以物體幾乎永遠(yuǎn)不會是完全黑暗的。為了模擬這個,我們會使用一個環(huán)境光照常量,它永遠(yuǎn)會給物體一些顏色。
漫反射光照(Diffuse Lighting):模擬光源對物體的方向性影響(Directional Impact)。它是馮氏光照模型中視覺上最顯著的分量。物體的某一部分越是正對著光源,它就會越亮。
鏡面光照(Specular Lighting):模擬有光澤物體上面出現(xiàn)的亮點(diǎn)。鏡面光照的顏色相比于物體的顏色會更傾向于光的顏色。
為了創(chuàng)建有趣的視覺場景,我們希望模擬至少這三種光照分量。我們將以最簡單的一個開始:環(huán)境光照。
環(huán)境光照
光通常都不是來自于同一個光源,而是來自于我們周圍分散的很多光源,即使它們可能并不是那么顯而易見。光的一個屬性是,它可以向很多方向發(fā)散并反彈,從而能夠到達(dá)不是非常直接臨近的點(diǎn)。所以,光能夠在其它的表面上反射,對一個物體產(chǎn)生間接的影響。考慮到這種情況的算法叫做全局照明(Global Illumination)算法,但是這種算法既開銷高昂又極其復(fù)雜。
由于我們現(xiàn)在對那種又復(fù)雜又開銷高昂的算法不是很感興趣,所以我們將會先使用一個簡化的全局照明模型,即環(huán)境光照。正如你在上一節(jié)所學(xué)到的,我們使用一個很小的常量(光照)顏色,添加到物體片段的最終顏色中,這樣子的話即便場景中沒有直接的光源也能看起來存在有一些發(fā)散的光。
把環(huán)境光照添加到場景里非常簡單。我們用光的顏色乘以一個很小的常量環(huán)境因子,再乘以物體的顏色,然后將最終結(jié)果作為片段的顏色:
void main()
{
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
FragColor = vec4(result, 1.0);
}
如果你現(xiàn)在運(yùn)行你的程序,你會注意到馮氏光照的第一個階段已經(jīng)應(yīng)用到你的物體上了。這個物體非常暗,但由于應(yīng)用了環(huán)境光照(注意光源立方體沒受影響是因為我們對它使用了另一個著色器),也不是完全黑的。它看起來應(yīng)該像這樣:

漫反射光照
環(huán)境光照本身不能提供最有趣的結(jié)果,但是漫反射光照就能開始對物體產(chǎn)生顯著的視覺影響了。漫反射光照使物體上與光線方向越接近的片段能從光源處獲得更多的亮度。為了能夠更好的理解漫反射光照,請看下圖:

圖左上方有一個光源,它所發(fā)出的光線落在物體的一個片段上。我們需要測量這個光線是以什么角度接觸到這個片段的。如果光線垂直于物體表面,這束光對物體的影響會最大化(譯注:更亮)。為了測量光線和片段的角度,我們使用一個叫做法向量(Normal Vector)的東西,它是垂直于片段表面的一個向量(這里以黃色箭頭表示),我們在后面再講這個東西。這兩個向量之間的角度很容易就能夠通過點(diǎn)乘計算出來。
你可能記得在變換那一節(jié)教程里,我們知道兩個單位向量的夾角越小,它們點(diǎn)乘的結(jié)果越傾向于1。當(dāng)兩個向量的夾角為90度的時候,點(diǎn)乘會變?yōu)?。這同樣適用于θ,θ越大,光對片段顏色的影響就應(yīng)該越小。
點(diǎn)乘返回一個標(biāo)量,我們可以用它計算光線對片段顏色的影響。不同片段朝向光源的方向的不同,這些片段被照亮的情況也不同。
所以,計算漫反射光照需要什么?
- 法向量:一個垂直于頂點(diǎn)表面的向量。
- 定向的光線:作為光源的位置與片段的位置之間向量差的方向向量。為了計算這個光線,我們需要光的位置向量和片段的位置向量。
法向量
法向量是一個垂直于頂點(diǎn)表面的(單位)向量。由于頂點(diǎn)本身并沒有表面(它只是空間中一個獨(dú)立的點(diǎn)),我們利用它周圍的頂點(diǎn)來計算出這個頂點(diǎn)的表面。我們能夠使用一個小技巧,使用叉乘對立方體所有的頂點(diǎn)計算法向量,但是由于3D立方體不是一個復(fù)雜的形狀,所以我們可以簡單地把法線數(shù)據(jù)手工添加到頂點(diǎn)數(shù)據(jù)中。更新后的頂點(diǎn)數(shù)據(jù)數(shù)組可以在這里找到。試著去想象一下,這些法向量真的是垂直于立方體各個平面的表面的(一個立方體由6個平面組成)。
由于我們向頂點(diǎn)數(shù)組添加了額外的數(shù)據(jù),所以我們應(yīng)該更新光照的頂點(diǎn)著色器:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
...
現(xiàn)在我們已經(jīng)向每個頂點(diǎn)添加了一個法向量并更新了頂點(diǎn)著色器,我們還要更新頂點(diǎn)屬性指針。注意,燈使用同樣的頂點(diǎn)數(shù)組作為它的頂點(diǎn)數(shù)據(jù),然而燈的著色器并沒有使用新添加的法向量。我們不需要更新燈的著色器或者是屬性的配置,但是我們必須至少修改一下頂點(diǎn)屬性指針來適應(yīng)新的頂點(diǎn)數(shù)組的大小:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
我們只想使用每個頂點(diǎn)的前三個float,并且忽略后三個float,所以我們只需要把步長參數(shù)改成float大小的6倍就行了。
雖然對燈的著色器使用不能完全利用的頂點(diǎn)數(shù)據(jù)看起來不是那么高效,但這些頂點(diǎn)數(shù)據(jù)已經(jīng)從箱子對象載入后開始就儲存在GPU的內(nèi)存里了,所以我們并不需要儲存新數(shù)據(jù)到GPU內(nèi)存中。這實際上比給燈專門分配一個新的VBO更高效了。
所有光照的計算都是在片段著色器里進(jìn)行,所以我們需要將法向量由頂點(diǎn)著色器傳遞到片段著色器。我們這么做:
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
Normal = aNormal;
}
接下來,在片段著色器中定義相應(yīng)的輸入變量:
in vec3 Normal;
計算漫反射光照
我們現(xiàn)在對每個頂點(diǎn)都有了法向量,但是我們?nèi)匀恍枰庠吹奈恢孟蛄亢推蔚奈恢孟蛄?。由于光源的位置是一個靜態(tài)變量,我們可以簡單地在片段著色器中把它聲明為uniform:
uniform vec3 lightPos;
然后在渲染循環(huán)中(渲染循環(huán)的外面也可以,因為它不會改變)更新uniform。我們使用在前面聲明的lightPos向量作為光源位置:
lightingShader.setVec3("lightPos", lightPos);
最后,我們還需要片段的位置。我們會在世界空間中進(jìn)行所有的光照計算,因此我們需要一個在世界空間中的頂點(diǎn)位置。我們可以通過把頂點(diǎn)位置屬性乘以模型矩陣(不是觀察和投影矩陣)來把它變換到世界空間坐標(biāo)。這個在頂點(diǎn)著色器中很容易完成,所以我們聲明一個輸出變量,并計算它的世界空間坐標(biāo):
out vec3 FragPos;
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = aNormal;
}
最后,在片段著色器中添加相應(yīng)的輸入變量。
in vec3 FragPos;
現(xiàn)在,所有需要的變量都設(shè)置好了,我們可以在片段著色器中添加光照計算了。
我們需要做的第一件事是計算光源和片段位置之間的方向向量。前面提到,光的方向向量是光源位置向量與片段位置向量之間的向量差。你可能記得在變換教程中,我們能夠簡單地通過讓兩個向量相減的方式計算向量差。我們同樣希望確保所有相關(guān)向量最后都轉(zhuǎn)換為單位向量,所以我們把法線和最終的方向向量都進(jìn)行標(biāo)準(zhǔn)化:
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
當(dāng)計算光照時我們通常不關(guān)心一個向量的模長或它的位置,我們只關(guān)心它們的方向。所以,幾乎所有的計算都使用單位向量完成,因為這簡化了大部分的計算(比如點(diǎn)乘)。所以當(dāng)進(jìn)行光照計算時,確保你總是對相關(guān)向量進(jìn)行標(biāo)準(zhǔn)化,來保證它們是真正地單位向量。忘記對向量進(jìn)行標(biāo)準(zhǔn)化是一個十分常見的錯誤。
下一步,我們對norm和lightDir向量進(jìn)行點(diǎn)乘,計算光源對當(dāng)前片段實際的漫發(fā)射影響。結(jié)果值再乘以光的顏色,得到漫反射分量。兩個向量之間的角度越大,漫反射分量就會越?。?/p>
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
如果兩個向量之間的角度大于90度,點(diǎn)乘的結(jié)果就會變成負(fù)數(shù),這樣會導(dǎo)致漫反射分量變?yōu)樨?fù)數(shù)。為此,我們使用max函數(shù)返回兩個參數(shù)之間較大的參數(shù),從而保證漫反射分量不會變成負(fù)數(shù)。負(fù)數(shù)顏色的光照是沒有定義的,所以最好避免它,除非你是那種古怪的藝術(shù)家。
現(xiàn)在我們有了環(huán)境光分量和漫反射分量,我們把它們相加,然后把結(jié)果乘以物體的顏色,來獲得片段最后的輸出顏色。
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);
如果你的應(yīng)用(和著色器)編譯成功了,你可能看到類似的輸出:

你可以看到使用了漫反射光照,立方體看起來就真的像個立方體了。嘗試在你的腦中想象一下法向量,并在立方體周圍移動,注意觀察法向量和光的方向向量之間的夾角越大,片段就會越暗。
如果你在哪卡住了,可以在這里對比一下完整的源代碼。
最后一件事
現(xiàn)在我們已經(jīng)把法向量從頂點(diǎn)著色器傳到了片段著色器??墒牵壳捌沃骼锏挠嬎愣际窃谑澜缈臻g坐標(biāo)中進(jìn)行的。所以,我們是不是應(yīng)該把法向量也轉(zhuǎn)換為世界空間坐標(biāo)?基本正確,但是這不是簡單地把它乘以一個模型矩陣就能搞定的。
首先,法向量只是一個方向向量,不能表達(dá)空間中的特定位置。同時,法向量沒有齊次坐標(biāo)(頂點(diǎn)位置中的w分量)。這意味著,位移不應(yīng)該影響到法向量。因此,如果我們打算把法向量乘以一個模型矩陣,我們就要從矩陣中移除位移部分,只選用模型矩陣左上角3×3的矩陣(注意,我們也可以把法向量的w分量設(shè)置為0,再乘以4×4矩陣;這同樣可以移除位移)。對于法向量,我們只希望對它實施縮放和旋轉(zhuǎn)變換。
其次,如果模型矩陣執(zhí)行了不等比縮放,頂點(diǎn)的改變會導(dǎo)致法向量不再垂直于表面了。因此,我們不能用這樣的模型矩陣來變換法向量。下面的圖展示了應(yīng)用了不等比縮放的模型矩陣對法向量的影響:

每當(dāng)我們應(yīng)用一個不等比縮放時(注意:等比縮放不會破壞法線,因為法線的方向沒被改變,僅僅改變了法線的長度,而這很容易通過標(biāo)準(zhǔn)化來修復(fù)),法向量就不會再垂直于對應(yīng)的表面了,這樣光照就會被破壞。
修復(fù)這個行為的訣竅是使用一個為法向量專門定制的模型矩陣。這個矩陣稱之為法線矩陣(Normal Matrix),它使用了一些線性代數(shù)的操作來移除對法向量錯誤縮放的影響。如果你想知道這個矩陣是如何計算出來的,建議去閱讀這個文章。
法線矩陣被定義為「模型矩陣左上角的逆矩陣的轉(zhuǎn)置矩陣」。真是拗口,如果你不明白這是什么意思,別擔(dān)心,我們還沒有討論逆矩陣(Inverse Matrix)和轉(zhuǎn)置矩陣(Transpose Matrix)。注意,大部分的資源都會將法線矩陣定義為應(yīng)用到模型-觀察矩陣(Model-view Matrix)上的操作,但是由于我們只在世界空間中進(jìn)行操作(不是在觀察空間),我們只使用模型矩陣。
在頂點(diǎn)著色器中,我們可以使用inverse和transpose
函數(shù)自己生成這個法線矩陣,這兩個函數(shù)對所有類型矩陣都有效。注意我們還要把被處理過的矩陣強(qiáng)制轉(zhuǎn)換為3×3矩陣,來保證它失去了位移屬性以及能夠乘以vec3的法向量。
Normal = mat3(transpose(inverse(model))) * aNormal;
在漫反射光照部分,光照表現(xiàn)并沒有問題,這是因為我們沒有對物體本身執(zhí)行任何縮放操作,所以并不是必須要使用一個法線矩陣,僅僅讓模型矩陣乘以法線也可以。可是,如果你進(jìn)行了不等比縮放,使用法線矩陣去乘以法向量就是必不可少的了。
即使是對于著色器來說,逆矩陣也是一個開銷比較大的運(yùn)算,因此,只要可能就應(yīng)該避免在著色器中進(jìn)行逆矩陣運(yùn)算,它們必須為你場景中的每個頂點(diǎn)都進(jìn)行這樣的處理。用作學(xué)習(xí)目這樣做是可以的,但是對于一個對效率有要求的應(yīng)用來說,在繪制之前你最好用CPU計算出法線矩陣,然后通過uniform把值傳遞給著色器(像模型矩陣一樣)。
鏡面光照
如果你還沒被這些光照計算搞得精疲力盡,我們就再把鏡面高光(Specular Highlight)加進(jìn)來,這樣馮氏光照才算完整。
和漫反射光照一樣,鏡面光照也是依據(jù)光的方向向量和物體的法向量來決定的,但是它也依賴于觀察方向,例如玩家是從什么方向看著這個片段的。鏡面光照是基于光的反射特性。如果我們想象物體表面像一面鏡子一樣,那么,無論我們從哪里去看那個表面所反射的光,鏡面光照都會達(dá)到最大化。你可以從下面的圖片看到效果:

我們通過反射法向量周圍光的方向來計算反射向量。然后我們計算反射向量和視線方向的角度差,如果夾角越小,那么鏡面光的影響就會越大。它的作用效果就是,當(dāng)我們?nèi)タ垂獗晃矬w所反射的那個方向的時候,我們會看到一個高光。
觀察向量是鏡面光照附加的一個變量,我們可以使用觀察者世界空間位置和片段的位置來計算它。之后,我們計算鏡面光強(qiáng)度,用它乘以光源的顏色,再將它加上環(huán)境光和漫反射分量。
我們選擇在世界空間進(jìn)行光照計算,但是大多數(shù)人趨向于在觀察空間進(jìn)行光照計算。在觀察空間計算的好處是,觀察者的位置總是(0, 0, 0),所以這樣你直接就獲得了觀察者位置。可是我發(fā)現(xiàn)在學(xué)習(xí)的時候在世界空間中計算光照更符合直覺。如果你仍然希望在觀察空間計算光照的話,你需要將所有相關(guān)的向量都用觀察矩陣進(jìn)行變換(記得也要改變法線矩陣)。
為了得到觀察者的世界空間坐標(biāo),我們簡單地使用攝像機(jī)對象的位置坐標(biāo)代替(它當(dāng)然就是觀察者)。所以我們把另一個uniform添加到片段著色器,把相應(yīng)的攝像機(jī)位置坐標(biāo)傳給片段著色器:
uniform vec3 viewPos;
lightingShader.setVec3("viewPos", camera.Position);
現(xiàn)在我們已經(jīng)獲得所有需要的變量,可以計算高光強(qiáng)度了。首先,我們定義一個鏡面強(qiáng)度(Specular Intensity)變量,給鏡面高光一個中等亮度顏色,讓它不要產(chǎn)生過度的影響。
float specularStrength = 0.5;
如果我們把它設(shè)置為1.0f,我們會得到一個非常亮的鏡面光分量,這對于一個珊瑚色的立方體來說有點(diǎn)太多了。下一節(jié)教程中我們會討論如何合理設(shè)置這些光照強(qiáng)度,以及它們是如何影響物體的。下一步,我們計算視線方向向量,和對應(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;
我們先計算視線方向與反射方向的點(diǎn)乘(并確保它不是負(fù)值),然后取它的32次冪。這個32是高光的反光度(Shininess)。一個物體的反光度越高,反射光的能力越強(qiáng),散射得越少,高光點(diǎn)就會越小。在下面的圖片里,你會看到不同反光度的視覺效果影響:

我們不希望鏡面成分過于顯眼,所以我們把指數(shù)保持為32。剩下的最后一件事情是把它加到環(huán)境光分量和漫反射分量里,再用結(jié)果乘以物體的顏色:
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
我們現(xiàn)在為馮氏光照計算了全部的光照分量。根據(jù)你的視角,你可以看到類似下面的畫面:

你可以在這里找到完整源碼。
在光照著色器的早期,開發(fā)者曾經(jīng)在頂點(diǎn)著色器中實現(xiàn)馮氏光照模型。在頂點(diǎn)著色器中做光照的優(yōu)勢是,相比片段來說,頂點(diǎn)要少得多,因此會更高效,所以(開銷大的)光照計算頻率會更低。然而,頂點(diǎn)著色器中的最終顏色值是僅僅只是那個頂點(diǎn)的顏色值,片段的顏色值是由插值光照顏色所得來的。結(jié)果就是這種光照看起來不會非常真實,除非使用了大量頂點(diǎn)。在頂點(diǎn)著色器中實現(xiàn)的馮氏光照模型叫做
Gouraud著色(Gouraud Shading),而不是馮氏著色(Phong Shading)。記住,由于插值,這種光照看起來有點(diǎn)遜色。馮氏著色能產(chǎn)生更平滑的光照效果。
現(xiàn)在你應(yīng)該能夠看到著色器的強(qiáng)大之處了。只用很少的信息,著色器就能計算出光照如何影響到所有物體的片段顏色。
后記
未完,待續(xù)~~~

