本文主要解決一個問題:
如何在OpenGL中模擬三種光源類型沉颂?
引言
之前的文章中纯露,我們把光源定義成空間中的一點。效果確實不錯,但是還不足以模擬現(xiàn)實世界中的大部分光源痊臭。一個簡單的例子睛约,它無法模擬太陽光。在本章中馋辈,我們會介紹3中模擬真實世界中光源的模型,使用這三種模型我們可以模擬絕大部分的光源。這三種光源模型是:方向光锰什、點光源、聚光燈丁逝。
我們先從方向光開始汁胆,然后是點光源,最后是聚光燈霜幼。
方向光(Directional Light)
方向光模型模擬的是一個非常遠的地方發(fā)射出來的光嫩码。在非常遠的距離上,到物體上就近似于平行罪既。想想太陽光铸题,太陽距離地球大約1.5億公里,地球的半徑是6378公里琢感,算起來丢间,太陽光照射的角度范圍大約只有0.0024度,照到地球的時候和平行也沒什么區(qū)別了驹针。
由于光線都是平行照射烘挫,光照效果也就和光源位置無關(guān)。所以柬甥,方向光的模型需要的是一個方向參數(shù)而不是位置饮六,著色器在計算的時候也幾乎相同其垄。我們來改一下光源結(jié)構(gòu):
struct Light{
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
void main(){
...
vec3 lightDir = normalize(-light.direction);
...
}
注意我們要光線方向的反方向用于計算角度。那為啥不直接指定反方向呢卤橄?這是一個習慣的問題绿满,說到方向光窟扑,我們最直接的反應(yīng)就是光線方向棒口,這最符合我們的邏輯認識。
要觀察方向光的效果辜膝,我們需要在之前顯示3D盒子章節(jié)中的10個盒子无牵。回憶一下之前的章節(jié)厂抖,首先我們要需要10個不同的位置:
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個把模型從局部空間轉(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è)置七蜘。當然,之前對光源位置的引用也都需要刪除墙懂。
lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);
編譯運行橡卤,如果程序沒有問題,你看到的場景應(yīng)該是類似這樣的:
看上去像是從天上有光照下來把這些箱子照亮了损搬。如果你顯示的不對碧库,完整的源代碼在這里。
點光源(Point Light)
方向光通常用來模擬整個場景接收的全局光照巧勤,但是除了全局光照之外嵌灰,我們通常還需要一些小的光源,例如一個燈泡之類的颅悉。這些光源就是點光源沽瞭。點光源常常被設(shè)置在某個位置上,然后隨著離距離的變遠光照強度變小剩瓶。
之前的章節(jié)里驹溃,我們用到了最簡單的點光源。這個點光源有個缺點延曙,就是光照不會隨著距離減弱豌鹤,反而好像是越來越強了,這顯然是不符合常理的搂鲫。在大多數(shù)3D場景中傍药,我們希望的點光源是像現(xiàn)實生活中那樣磺平,只能照亮周圍一小片區(qū)域魂仍。
如果實現(xiàn)過之前章節(jié)中的10個盒子拐辽,你可能會注意到盒子背面的亮度和前面的亮度是一致的,因為我們沒有對光照的強度進行衰減擦酌。你是對的俱诸!光照應(yīng)該是隨著距離越遠越弱。
衰減(Attenuation)
隨著距離的變遠光照強度減弱的過程我們稱之為衰減赊舶。一個簡單的方法是直接采用線性衰減:設(shè)定一個衰減比例睁搭,隨著距離減少強度。但是這種衰減不符合現(xiàn)實的情況笼平,現(xiàn)實情況是光照會在短距離之內(nèi)迅速衰減园骆,然后緩慢衰減直至消失。沒錯寓调,這更像是一種二次衰減模型锌唾。
幸運的是,有一些聰明的前輩高人已經(jīng)將這個衰減公式給計算出來了夺英。我們直接就能使用:
這里的d表示距離(distance)晌涕,是片元到光源的距離。公式中包含了3個常數(shù)因子痛悯,分別是Kc, Kl和Kq余黎。這三個因子分別是常數(shù)衰減指數(shù)、線性衰減指數(shù)载萌、二次項衰減指數(shù)惧财。
因為二次項的衰減會比前面的線性和常數(shù)衰減快很多,造成的結(jié)果就是在離光源近的地方會很亮扭仁,然后離開光源可缚,亮度迅速衰減,到一定程度后衰減又會減慢斋枢。整個過程看起來就是像是這個樣子:
3個衰減因子到底應(yīng)該選多少呢帘靡?
衰減因子取決于很多因素:環(huán)境、你期望光源覆蓋的范圍瓤帚、光的類型等等描姚。大多數(shù)情況下,這是一個經(jīng)驗和微調(diào)的問題戈次。下面一張表里給出了覆蓋范圍和衰減因子的取值關(guān)系轩勘,這些值是非常好的微調(diào)基準值。
就像你看到的這樣怯邪,Kc永遠是1绊寻,Kl隨著覆蓋范圍增大變得非常小,而Kq變小的就更快了。有時間試試這些值澄步,對渲染場景的影響冰蘑。在本文中,我們選擇覆蓋范圍是50村缸。
實現(xiàn)點光源效果祠肥。光源的位置屬性,再往Light結(jié)構(gòu)中添加三個float變量表示3個不同的衰減因子梯皿,這些因子可以通過主函數(shù)設(shè)置仇箱。
struct Light{
//vec3 direction;
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
然后我們就可以在主函數(shù)中設(shè)置這些值了。對照上面的表东羹,我們的3個衰減因子分別設(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)用到光照中去也非常簡單,只要計算出衰減值属提,然后乘上ambient渊额,diffuse和specular分量就行了。
先計算衰減值垒拢。我們要用到片元距離光源的距離旬迹,這就要用到GLSL內(nèi)置的length函數(shù)了,這個函數(shù)作用是計算一個向量的長度求类,我們把光源位置和片元位置做減法就可以得到這個向量奔垦。
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é)果不對,請下載源碼進行對比尸疆。
聚光燈(Spot Light)
最后一種常見的光源類型是聚光燈椿猎。聚光燈模型,模擬的是手電筒寿弱,探照燈之類可以把光匯聚到一個方向的光源犯眠。它用到了平行光和點光源的部分內(nèi)容,我們在設(shè)置聚光燈的時候症革,需要設(shè)置其位置和朝向筐咧,并且光照強度會隨著距離而減小。特別的地方是噪矛,聚光燈的光只會對某個方向上的有限圓錐角的物體有照亮效果量蕊,如下圖所示:
- LightDir(光照方向):表示光源到片元的方向
- SpotDir(聚光燈朝向):表示聚光燈前方的方向,也就是影響方向艇挨。
- Phi ?:范圍角度残炮,所有在這個角度范圍之外的物體都不會被照亮
- Theta θ:光照方向和聚光燈朝向的夾角,用來計算光照強度
實現(xiàn)一個手電筒
聚光燈需要位置缩滨、朝向和范圍角度势就,因此泉瞻,我們要在光源結(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角度值來代替原本的角度袖牙,因為這樣比較簡單。我們可以直接計算光照方向和聚光燈方向的點積抱完,然后和這個數(shù)值進行比較從而得出該點是否接收光照這個結(jié)論贼陶。
現(xiàn)在刃泡,我們在片元著色器里計算片元和光源之間的方向與聚光燈朝向之間的夾角是否超過了照射范圍:
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)烘贴。
編譯運行禁添,你會看到類似這樣的效果:
如果不對,請下載源碼進行比對桨踪。
看上去有點假老翘,有沒有這感覺?
平滑邊緣
為了創(chuàng)建一個平滑的邊緣效果锻离,我們需要改變一下聚光燈的模型铺峭,模擬聚光燈的內(nèi)錐角和外錐角。計算方式也會有所改變汽纠。
假設(shè)內(nèi)錐角為?卫键,外錐角為γ, 光照角度為θ虱朵,我們的光照強度的計算公式就是:
其中:?為(內(nèi)錐角-外錐角)的cos值莉炉。結(jié)果I 就表示當前片元的光照強度。
讓我們來看計算代碼:
//在光源結(jié)構(gòu)體中添加外錐角成員
struct Light{
...
float outerCutOff;
};
//聚光燈
float theta = dot(lightDir, normalize(-light.direction)); //計算片元角度的cos值
float epsilon = light.cutOff - light.outerCutOff; //計算epsilon的值碴犬,用內(nèi)錐角的cos值減去外錐角的cos值
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0); //根據(jù)公式計算光照強度絮宁,并限制結(jié)果的范圍
diffuse *= intensity;
specular *= instensity;
強度被限制在0到1之間,這是必要的服协,因為theta-light.outerCutOff的值可能是負數(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)));
編譯運行,如果運行沒問題遭顶,你所看到的結(jié)果應(yīng)該是這個樣子:
如果你的顯示不正確张峰,歡迎參考源碼。
總結(jié)
本章中棒旗,我們學習了3中光照模型喘批,分別是:方向光撩荣,點光源和聚光燈。方向光最簡單饶深,只有一個方向參數(shù)餐曹。點光源稍微復雜點,有位置和衰減度兩個參數(shù)敌厘。最復雜的是聚光燈台猴,不僅有方向、衰減度俱两,還有內(nèi)錐角和外錐角的區(qū)分饱狂。不過,功夫不負有心人宪彩,我們終于弄出點有趣的效果來了休讳。
參考資料
www.learnopengl.com(非常好的網(wǎng)站,歡迎學習)