本文主要解決一個(gè)問題:
如何在OpenGL中模擬三種光源類型?
引言
之前的文章中,我們把光源定義成空間中的一點(diǎn)。效果確實(shí)不錯,但是還不足以模擬現(xiàn)實(shí)世界中的大部分光源。一個(gè)簡單的例子,它無法模擬太陽光。在本章中,我們會介紹3中模擬真實(shí)世界中光源的模型,使用這三種模型我們可以模擬絕大部分的光源。這三種光源模型是:方向光、點(diǎn)光源、聚光燈。
我們先從方向光開始,然后是點(diǎn)光源,最后是聚光燈。
方向光(Directional Light)
方向光模型模擬的是一個(gè)非常遠(yuǎn)的地方發(fā)射出來的光。在非常遠(yuǎn)的距離上,到物體上就近似于平行。想想太陽光,太陽距離地球大約1.5億公里,地球的半徑是6378公里,算起來,太陽光照射的角度范圍大約只有0.0024度,照到地球的時(shí)候和平行也沒什么區(qū)別了。

由于光線都是平行照射,光照效果也就和光源位置無關(guān)。所以,方向光的模型需要的是一個(gè)方向參數(shù)而不是位置,著色器在計(jì)算的時(shí)候也幾乎相同。我們來改一下光源結(jié)構(gòu):
struct Light{
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
void main(){
...
vec3 lightDir = normalize(-light.direction);
...
}
注意我們要光線方向的反方向用于計(jì)算角度。那為啥不直接指定反方向呢?這是一個(gè)習(xí)慣的問題,說到方向光,我們最直接的反應(yīng)就是光線方向,這最符合我們的邏輯認(rèn)識。
要觀察方向光的效果,我們需要在之前顯示3D盒子章節(jié)中的10個(gè)盒子?;貞浺幌轮暗恼鹿?jié),首先我們要需要10個(gè)不同的位置:
glm::vec3 cubePositions[] = {
glm::vec3( 0.0f, 0.0f, 0.0f),
glm::vec3( 2.0f, 5.0f, -15.0f),
glm::vec3(-1.5f, -2.2f, -2.5f),
glm::vec3(-3.8f, -2.0f, -12.3f),
glm::vec3( 2.4f, -0.4f, -3.5f),
glm::vec3(-1.7f, 3.0f, -7.5f),
glm::vec3( 1.3f, -2.0f, -2.5f),
glm::vec3( 1.5f, 2.0f, -2.5f),
glm::vec3( 1.5f, 0.2f, -1.5f),
glm::vec3(-1.3f, 1.0f, -1.5f)
};
還需要10個(gè)把模型從局部空間轉(zhuǎn)換到世界空間的模型矩陣:
for(unsigned int i = 0; i < 10; i++)
{
glm::mat4 model;
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * i;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
lightingShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
最后,別忘了設(shè)置方向光,你可以在主循環(huán)外面,也可以在主循環(huán)里面設(shè)置。當(dāng)然,之前對光源位置的引用也都需要刪除。
lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);
編譯運(yùn)行,如果程序沒有問題,你看到的場景應(yīng)該是類似這樣的:

看上去像是從天上有光照下來把這些箱子照亮了。如果你顯示的不對,完整的源代碼在這里。
點(diǎn)光源(Point Light)
方向光通常用來模擬整個(gè)場景接收的全局光照,但是除了全局光照之外,我們通常還需要一些小的光源,例如一個(gè)燈泡之類的。這些光源就是點(diǎn)光源。點(diǎn)光源常常被設(shè)置在某個(gè)位置上,然后隨著離距離的變遠(yuǎn)光照強(qiáng)度變小。

之前的章節(jié)里,我們用到了最簡單的點(diǎn)光源。這個(gè)點(diǎn)光源有個(gè)缺點(diǎn),就是光照不會隨著距離減弱,反而好像是越來越強(qiáng)了,這顯然是不符合常理的。在大多數(shù)3D場景中,我們希望的點(diǎn)光源是像現(xiàn)實(shí)生活中那樣,只能照亮周圍一小片區(qū)域。
如果實(shí)現(xiàn)過之前章節(jié)中的10個(gè)盒子,你可能會注意到盒子背面的亮度和前面的亮度是一致的,因?yàn)槲覀儧]有對光照的強(qiáng)度進(jìn)行衰減。你是對的!光照應(yīng)該是隨著距離越遠(yuǎn)越弱。
衰減(Attenuation)
隨著距離的變遠(yuǎn)光照強(qiáng)度減弱的過程我們稱之為衰減。一個(gè)簡單的方法是直接采用線性衰減:設(shè)定一個(gè)衰減比例,隨著距離減少強(qiáng)度。但是這種衰減不符合現(xiàn)實(shí)的情況,現(xiàn)實(shí)情況是光照會在短距離之內(nèi)迅速衰減,然后緩慢衰減直至消失。沒錯,這更像是一種二次衰減模型。
幸運(yùn)的是,有一些聰明的前輩高人已經(jīng)將這個(gè)衰減公式給計(jì)算出來了。我們直接就能使用:

這里的d表示距離(distance),是片元到光源的距離。公式中包含了3個(gè)常數(shù)因子,分別是Kc, Kl和Kq。這三個(gè)因子分別是常數(shù)衰減指數(shù)、線性衰減指數(shù)、二次項(xiàng)衰減指數(shù)。
因?yàn)槎雾?xiàng)的衰減會比前面的線性和常數(shù)衰減快很多,造成的結(jié)果就是在離光源近的地方會很亮,然后離開光源,亮度迅速衰減,到一定程度后衰減又會減慢。整個(gè)過程看起來就是像是這個(gè)樣子:

3個(gè)衰減因子到底應(yīng)該選多少呢?
衰減因子取決于很多因素:環(huán)境、你期望光源覆蓋的范圍、光的類型等等。大多數(shù)情況下,這是一個(gè)經(jīng)驗(yàn)和微調(diào)的問題。下面一張表里給出了覆蓋范圍和衰減因子的取值關(guān)系,這些值是非常好的微調(diào)基準(zhǔn)值。

就像你看到的這樣,Kc永遠(yuǎn)是1,Kl隨著覆蓋范圍增大變得非常小,而Kq變小的就更快了。有時(shí)間試試這些值,對渲染場景的影響。在本文中,我們選擇覆蓋范圍是50。
實(shí)現(xiàn)點(diǎn)光源效果。光源的位置屬性,再往Light結(jié)構(gòu)中添加三個(gè)float變量表示3個(gè)不同的衰減因子,這些因子可以通過主函數(shù)設(shè)置。
struct Light{
//vec3 direction;
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
然后我們就可以在主函數(shù)中設(shè)置這些值了。對照上面的表,我們的3個(gè)衰減因子分別設(shè)置為:1.0f, 0.09f, 0.032f。
lightingShader.setFloat("light.constant", 1.0f);
lightingShader.setFloat("light.linear", 0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);
將衰減值應(yīng)用到光照中去也非常簡單,只要計(jì)算出衰減值,然后乘上ambient,diffuse和specular分量就行了。
先計(jì)算衰減值。我們要用到片元距離光源的距離,這就要用到GLSL內(nèi)置的length函數(shù)了,這個(gè)函數(shù)作用是計(jì)算一個(gè)向量的長度,我們把光源位置和片元位置做減法就可以得到這個(gè)向量。
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;

如果你顯示的結(jié)果不對,請下載源碼進(jìn)行對比。
聚光燈(Spot Light)
最后一種常見的光源類型是聚光燈。聚光燈模型,模擬的是手電筒,探照燈之類可以把光匯聚到一個(gè)方向的光源。它用到了平行光和點(diǎn)光源的部分內(nèi)容,我們在設(shè)置聚光燈的時(shí)候,需要設(shè)置其位置和朝向,并且光照強(qiáng)度會隨著距離而減小。特別的地方是,聚光燈的光只會對某個(gè)方向上的有限圓錐角的物體有照亮效果,如下圖所示:

- LightDir(光照方向):表示光源到片元的方向
- SpotDir(聚光燈朝向):表示聚光燈前方的方向,也就是影響方向。
- Phi ?:范圍角度,所有在這個(gè)角度范圍之外的物體都不會被照亮
- Theta θ:光照方向和聚光燈朝向的夾角,用來計(jì)算光照強(qiáng)度
實(shí)現(xiàn)一個(gè)手電筒
聚光燈需要位置、朝向和范圍角度,因此,我們要在光源結(jié)構(gòu)體中添加這些成員。
struct Light {
vec3 position;
vec3 direction;
float cutOff;
...
};
設(shè)置這些值:
lightingShader.setVec3("light.position",camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
可以看到,我們是用cos角度值來代替原本的角度,因?yàn)檫@樣比較簡單。我們可以直接計(jì)算光照方向和聚光燈方向的點(diǎn)積,然后和這個(gè)數(shù)值進(jìn)行比較從而得出該點(diǎn)是否接收光照這個(gè)結(jié)論。
現(xiàn)在,我們在片元著色器里計(jì)算片元和光源之間的方向與聚光燈朝向之間的夾角是否超過了照射范圍:
float theta = dot(lightDir, normalize(-light.direction));
if (theta > light.cutOff) { //在照射范圍內(nèi)
}
else
FragColor = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);
注意,cos的值隨著角度變大而逐漸變小,所以判斷的方式是theta>light.cutOff表示在照射范圍內(nèi)。
編譯運(yùn)行,你會看到類似這樣的效果:

如果不對,請下載源碼進(jìn)行比對。
看上去有點(diǎn)假,有沒有這感覺?
平滑邊緣
為了創(chuàng)建一個(gè)平滑的邊緣效果,我們需要改變一下聚光燈的模型,模擬聚光燈的內(nèi)錐角和外錐角。計(jì)算方式也會有所改變。
假設(shè)內(nèi)錐角為?,外錐角為γ, 光照角度為θ,我們的光照強(qiáng)度的計(jì)算公式就是:

其中:?為(內(nèi)錐角-外錐角)的cos值。結(jié)果I 就表示當(dāng)前片元的光照強(qiáng)度。
讓我們來看計(jì)算代碼:
//在光源結(jié)構(gòu)體中添加外錐角成員
struct Light{
...
float outerCutOff;
};
//聚光燈
float theta = dot(lightDir, normalize(-light.direction)); //計(jì)算片元角度的cos值
float epsilon = light.cutOff - light.outerCutOff; //計(jì)算epsilon的值,用內(nèi)錐角的cos值減去外錐角的cos值
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0); //根據(jù)公式計(jì)算光照強(qiáng)度,并限制結(jié)果的范圍
diffuse *= intensity;
specular *= instensity;
強(qiáng)度被限制在0到1之間,這是必要的,因?yàn)閠heta-light.outerCutOff的值可能是負(fù)數(shù)。
最后,設(shè)置內(nèi)錐角度為12.5度,外錐角度為17.5度,設(shè)置代碼如下:
lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
lightingShader.setFloat("light.outerCutOff", glm::cos(glm::radians(17.5f)));
編譯運(yùn)行,如果運(yùn)行沒問題,你所看到的結(jié)果應(yīng)該是這個(gè)樣子:

如果你的顯示不正確,歡迎參考源碼。
總結(jié)
本章中,我們學(xué)習(xí)了3中光照模型,分別是:方向光,點(diǎn)光源和聚光燈。方向光最簡單,只有一個(gè)方向參數(shù)。點(diǎn)光源稍微復(fù)雜點(diǎn),有位置和衰減度兩個(gè)參數(shù)。最復(fù)雜的是聚光燈,不僅有方向、衰減度,還有內(nèi)錐角和外錐角的區(qū)分。不過,功夫不負(fù)有心人,我們終于弄出點(diǎn)有趣的效果來了。
參考資料
www.learnopengl.com(非常好的網(wǎng)站,歡迎學(xué)習(xí))