??我們目前使用的光都來自于空間中的一個點
light.position
,但現(xiàn)實生活中光源種類繁多菊值,不僅僅是一個點光源這么簡單抄淑。我們這節(jié)就來學(xué)習(xí)一下如何在OpenGL中實現(xiàn)各種光源。學(xué)會模擬不同種類的光源是又一個能夠進(jìn)一步豐富場景的工具馋记。??由淺入深号坡,我們將在這節(jié)學(xué)習(xí)定向光(Directional Light)懊烤、點光源(Point Light)、聚光(Spotlight)三種比較簡單的光源宽堆。
定向光
??定向光模擬的其實就是光源在無限遠(yuǎn)處時腌紧,到達(dá)物體表面的所有光的方向可以認(rèn)為是相互平行的,例如太陽光畜隶,此時在光照計算中與光源的位置已無多大關(guān)系壁肋,只需知道光的方向即可。
??為了實現(xiàn)定向光籽慢,我們需要一個記錄光的方向的變量浸遗,但不需要記錄光的位置,所以光源結(jié)構(gòu)體的成員有所改變:
struct Light {
//vec3 position;
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
??在有了光的方向之后箱亿,我們就不用利用光源位置和片段位置求光的方向乙帮,直接可以用來與法向量點乘求漫反射光照了,計算鏡面光照也同理极景,直接可用察净,但要切記,將方向向量標(biāo)準(zhǔn)化:
void main()
{
...
//vec3 lightDir = normalize(light.position - fragPos);
float diff = max(dot(normalize(Normal), normalize(-light.direction) ),0.0);
vec3 diffuse = light.diffuse * diff * texture(material.diffuse, texCoords).rgb;
vec3 viewDir = normalize(viewPos - fragPos);
vec3 reflectDir = reflect(normalize(light.direction), normalize(Normal));
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = light.specular * spec * texture(material.specular, texCoords).rgb;
...
}
??還有一點需要注意的是盼樟,我們設(shè)置的光的方向向量是從光源出發(fā)的氢卡,而之前我們利用向量相減求的方向向量是指向光源的。在點乘計算中晨缴,我們也是需要指向光源的方向向量才能獲得正確的夾角的余弦值译秦。所以需要對其進(jìn)行負(fù)值化。
??在提供光源方向之前击碗,我們可以嘗試渲染多幾個箱子筑悴,這樣看定向光的效果更明顯,我們先在一個glm::vec3
數(shù)組中定義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)
};
??我們需要在渲染循環(huán)里在實現(xiàn)一個循環(huán)稍途,在這個循環(huán)里把10個箱子逐一渲染出來阁吝,首先是對模型矩陣進(jìn)行位移處理,把箱子移到正確的位置上械拍,然后對模型矩陣進(jìn)行旋轉(zhuǎn)突勇,讓每個箱子旋轉(zhuǎn)不同的角度,這樣能讓定向光對比效果更明顯坷虑,最后傳遞模型矩陣甲馋,執(zhí)行渲染指令:
//在渲染循環(huán)里
for (unsigned int i = 0; i < 10; i++)
{
modelMat = glm::mat4(1.0);
modelMat = glm::translate(modelMat, cubePositions[i]); //位移
float angle = 20.0f * i;
modelMat = glm::rotate(modelMat, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f)); //旋轉(zhuǎn)
glUniformMatrix4fv(glGetUniformLocation(shader.ID, "model"), 1, GL_FALSE, glm::value_ptr(modelMat));
glDrawArrays(GL_TRIANGLES, 0, 36);
}
??這樣我們就擁有了10個不同位置,不同旋轉(zhuǎn)角度的箱子了迄损。最后一步就是傳遞光源方向給光源結(jié)構(gòu)體了:
????????shader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);
??我們選擇了一個從上指向下的光源方向定躏,模擬的就是高處無限遠(yuǎn)處有光源的情況。
??噢對了,為了防止產(chǎn)生視覺上的干擾痊远,需要把我們之前實體化的光源給取消掉绑谣,因為我們現(xiàn)在已經(jīng)沒用到那個位置上的光源了。
??你能注意到漫反射和鏡面光分量的反應(yīng)都好像在天空中有一個光源的感覺嗎拗引?
點光源
??在上節(jié)中借宵,我們提供的是一個光源的位置,然后根據(jù)該位置進(jìn)行各種光照計算矾削,這就有點類似于點光源壤玫,但點光源應(yīng)比它更嚴(yán)謹(jǐn),除了點發(fā)光以外哼凯,還有一個非常重要的點需要考慮欲间,那就是光線衰減的問題。我們之前用的那個光源断部,并沒考慮光衰減的問題猎贴,無論離光源的位置遠(yuǎn)還是近,其受到的光照效果是一致的蝴光,因為我們的計算只考慮了方向她渴,并未考慮距離。
衰減
??我們想要的效果是蔑祟,隨著光傳播的距離逐漸削弱光的強度趁耗。我們想到最簡單的做法就是光強隨著距離線性減少,然而疆虚,這樣的線性方程通晨涟埽看起來比較假。在現(xiàn)實世界中径簿,燈在近處通常會非常亮罢屈,但隨著距離的增加光源的亮度一開始會下降非常快篇亭,但在遠(yuǎn)處時剩余的光強度就會下降的非常緩慢了缠捌。我們需要一條公式來模擬這種情況,好在巨人已經(jīng)幫我們解決了這個問題:
- 常數(shù)項Kc:通常保持1.0暗赶,主要作用就是保證分母永遠(yuǎn)不會比1小鄙币,否則在某些距離上反而會增加強度,蹂随。
- 一次項Kl:一次項與距離值相乘,以線性的方式減少強度
- 二次項Kq:二次項與距離的平方相乘因惭,讓光源以二次遞減的方式減少強度岳锁。
??二次項一般會比一次項小很多,這可以在某段距離區(qū)間內(nèi)蹦魔,一次項的影響會比二次項的影響大很多激率,此時對應(yīng)的是近距離情況咳燕,光的亮度還比較亮,亮度下降不明顯乒躺;而超過這個區(qū)間后招盲,二次項的影響開始超過一次項,此時對應(yīng)的是遠(yuǎn)距離情況嘉冒,光的亮度迅速降低曹货,最后在超遠(yuǎn)距離時(運算結(jié)果無限靠近0),亮度降低的速度就變得很慢讳推。面這張圖顯示了在100的距離內(nèi)衰減的效果:
??你可以看到光在近距離的時候有著最高的強度顶籽,但隨著距離增長,它的強度明顯減弱银觅,并緩慢地在距離大約100的時候強度接近0礼饱。這正是我們想要的。
??既然如此究驴,如何選擇一次項和二次項的系數(shù)將會對光照的效果造成很大的影響镊绪。正確地設(shè)定它們的值更多的是取決于經(jīng)驗,下面這個表格顯示了模擬一個(大概)真實的洒忧,覆蓋特定半徑(距離)的光源時镰吆,這些項可能取的一些值。第一列指定的是在給定的三項時光所能覆蓋的距離跑慕。這些值是大多數(shù)光源很好的起始點万皿,它們由Ogre3D的Wiki所提供:
距離 | 常數(shù)項 | 一次項 | 二次項 |
---|---|---|---|
7 | 1.0 | 0.7 | 1.8 |
13 | 1.0 | 0.35 | 0.44 |
20 | 1.0 | 0.22 | 0.20 |
32 | 1.0 | 0.14 | 0.07 |
50 | 1.0 | 0.09 | 0.032 |
65 | 1.0 | 0.07 | 0.017 |
100 | 1.0 | 0.045 | 0.0075 |
160 | 1.0 | 0.027 | 0.0028 |
200 | 1.0 | 0.022 | 0.0019 |
325 | 1.0 | 0.014 | 0.0007 |
600 | 1.0 | 0.007 | 0.0002 |
3250 | 1.0 | 0.0014 | 0.000007 |
??可以看到要想光輻射的距離越遠(yuǎn),其一次項和二次項系數(shù)就要更小核行。
實現(xiàn)點光源和衰減
??我們需要對光源結(jié)構(gòu)體做出一定的修改牢硅,取消光的方向向量,增加記錄光位置的變量芝雪,并增加三個記錄衰減系數(shù)的浮點型變量:
struct Light {
vec3 position;
//vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
//衰減
float constant;
float linear;
float quadratic;
};
??如此减余,我們需要重新計算光的方向向量,用來計算漫反射光照和鏡面光照:
void main()
{
...
vec3 lightDir = normalize(light.position - fragPos);
float diff = max(dot(normalize(Normal), lightDir ),0.0);
vec3 diffuse = light.diffuse * diff * texture(material.diffuse, texCoords).rgb;
vec3 viewDir = normalize(viewPos - fragPos);
vec3 reflectDir = reflect(-lightDir, normalize(Normal));
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = light.specular * spec * texture(material.specular, texCoords).rgb;
...
vec3 result = ambient + diffuse + specular;
fragColor = vec4(result, 1.0);
}
??在獲得三種光照計算結(jié)果后惩系,對它們進(jìn)行衰減處理位岔,首先計算片段與光源的距離,然后使用光源結(jié)構(gòu)里的三個衰減系數(shù)堡牡,按照公式求得結(jié)果抒抬,最后把結(jié)果與三種光照分別相乘,就得到光線衰減的效果了晤柄。OpenGL提供了length()
函數(shù)來計算兩個位置的距離擦剑。
void main()
{
...
float distance = length(light.position - fragPos);
float attenuation = 1/(light.constant+light.linear*distance+light.quadratic*distance*distance);
ambient*=attenuation;
diffuse*=attenuation;
specular*=attenuation;
vec3 result = ambient + diffuse + specular;
fragColor = vec4(result, 1.0);
}
??我們還需要在主函數(shù)中提供三個衰減系數(shù)與光源的位置,我們希望光源能夠覆蓋50的距離,所以我們會使用表格中對應(yīng)的常數(shù)項惠勒、一次項和二次項:
shader.setVec3("light.position", lightPos);
shader.setFloat("light.constant", 1.0f);
shader.setFloat("light.linear", 0.09f);
shader.setFloat("light.quadratic", 0.032f);
??可以看到只有最前面的箱子被照亮了赚抡,其他箱子都因為距離太遠(yuǎn)而呈現(xiàn)黑色。
聚光
??聚光是位于空間中某一個位置的光源纠屋,只對某個方向范圍照射光線涂臣,而非像點光源一般四周發(fā)光。這樣只有進(jìn)入到照射范圍內(nèi)的物體才會被照亮售担,其余物體保持黑暗赁遗。與之相似的例子就是手電筒或是路燈。
??一般而言灼舍,聚光的光照范圍倒圓錐體吼和,那么判斷一個片段位置是否在該圓錐體范圍內(nèi)也不難,首先要確立該范圍大小我們需要知道骑素,光源位置炫乓,光源方向(位置在圓錐體的高)和一個切光角,切光角指定了聚光的半徑:
??如圖献丑,SpotDir即為光源方向末捣,? 即為切光角。要判斷一個片段位置是否在該圓錐體范圍內(nèi)创橄,我們還需要片段的位置箩做,然后光源位置與片段位置相減,得到光線方向向量(LightDir)妥畏,然后求該向量與光源方向向量的余弦值(點乘)邦邦,如果計算所得的余弦值大于切光角的余弦值,那么就可以認(rèn)為該片段在光照范圍內(nèi)醉蚁,否則就不在燃辖。為什么是大于呢?可以去查看余弦曲線网棍。
手電筒
??我們可以嘗試實現(xiàn)一個跟隨玩家攝像機(jī)位置的手電筒黔龟,這似乎不難,只要我們能獲知攝像機(jī)的位置和攝像機(jī)的正前方的方向向量滥玷。在學(xué)習(xí)攝像機(jī)時氏身,我們先前就已經(jīng)在攝像機(jī)類里提供了供外部訪問的攝像機(jī)位置變量和正前方向量。我們隨時可以拿出來傳遞給光源結(jié)構(gòu)體作為其光源的位置和光源方向惑畴。但在此之前蛋欣,我們先要在光源結(jié)構(gòu)體里定義這些變量:
struct Light {
vec3 position;
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float cutOff; //切光角
////衰減
//float constant;
//float linear;
//float quadratic;
};
??要注意的是,我們使用了浮點值來表示切光角桨菜,這意味著當(dāng)我們需要傳遞切光角時豁状,傳遞的不是其角度值捉偏,而是其余弦值倒得,因為方便計算泻红,如果我們在這里使用的是角度值,那么在計算了光線方向與光源方向的點乘值后霞掺,需要對其結(jié)果進(jìn)行反余弦(arcos)才能跟切光角進(jìn)行比較谊路,反余弦是一個開銷很大的計算,盡量避免菩彬。
??然后是計算光線方向與光源方向的點乘值缠劝,并將它和切光角比較,來決定是否在光照范圍:
void main()
{
vec3 lightDir = normalize(light.position - fragPos); //光線方向
float theta = dot(lightDir, -light.direction);
vec3 result;
if(theta>light.cutOff)
{
//計算各種光照
}
// 否則骗灶,使用環(huán)境光惨恭,讓場景在聚光之外時不至于完全黑暗
else result = light.ambient * texture(material.diffuse, texCoords).rgb;
fragColor = vec4(result, 1.0);
}
??我們可以在主函數(shù)把攝像機(jī)的位置即向前向量傳遞給光源結(jié)構(gòu)體,然后自定一個光照范圍耙旦,我們可以直接傳遞余弦值脱羡,也可以提供角度值然后利用glm::cos()
函數(shù)計算其余弦值,余弦的計算還是比反余弦輕松很多的免都。
shader.setVec3("light.position", camera.Position);
shader.setVec3("light.direction", camera.Front);
shader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
??運行程序锉罐,你將會看到一個聚光,它僅會照亮聚光圓錐內(nèi)的片段绕娘∨Ч妫看起來像是這樣的:
??這個效果其實并不是很好,因為聚光有一圈硬邊险领,就是說在圈內(nèi)就是亮侨舆,一出圈外就是暗,缺少由亮到暗平滑過渡的這種效果绢陌。一個真實的聚光將會在邊緣處逐漸減少亮度挨下。
平滑/軟化邊緣
??為了實現(xiàn)這種平滑過渡的效果,我們還需要一個外圓錐下面,比現(xiàn)有的圓錐稍大一點复颈,從內(nèi)圓錐到外圓錐光逐漸變暗,直到外圓錐邊界沥割。
??創(chuàng)建一個外圓錐的步驟與剛才創(chuàng)建圓錐的步驟一致耗啦,提供一個余弦值,該余弦值代表的是外圓錐向量(圓錐的母線)與聚光方向的夾角机杜。如果一個片段處于內(nèi)外圓錐之間帜讲,將會給它計算一個0.0到1.0之間的強度值。如果片段在內(nèi)圓錐內(nèi)椒拗,強度值為1.0似将,如果在外圓錐外获黔,強度值為0.0。
??有一條已被證明的公式可以幫助我們完成這個計算
??? (Epsilon)是內(nèi)(?)和外圓錐(γ)之間的余弦值差(?=??γ)在验,最終的I值就是在當(dāng)前片段聚光的強度玷氏。
θ | θ(角度) | ?(內(nèi)光切) | ?(角度) | γ(外光切) | γ(角度) | ? | I |
---|---|---|---|---|---|---|---|
0.87 | 30 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 | 0.87 - 0.82 / 0.09 = 0.56 |
0.9 | 26 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 | 0.9 - 0.82 / 0.09 = 0.89 |
0.97 | 14 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 | 0.97 - 0.82 / 0.09 = 1.67 |
0.83 | 34 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 | 0.83 - 0.82 / 0.09 = 0.11 |
0.64 | 50 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 | 0.64 - 0.82 / 0.09 = -2.0 |
0.966 | 15 | 0.9978 | 12.5 | 0.953 | 17.5 | 0.966 - 0.953 = 0.0448 | 0.966 - 0.953 / 0.0448 = 0.29 |
??可以看到這條公式,當(dāng)θ 在內(nèi)圓錐內(nèi)時腋舌,結(jié)果大于1.0盏触,而在內(nèi)外圓錐之間時,結(jié)果在0.0到1.0之間块饺,而在外圓錐外時赞辩,結(jié)果小于0.0。我們需要對這個結(jié)果進(jìn)行一定的約束授艰,使其小于0.0時輸出0.0辨嗽,大于1.0時輸出1.0。glsl提供了clamp
函數(shù)幫我們完成這件事:
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
??其中theta
就是式子中的θ淮腾,epsilon
就是式子中的?糟需,light.outerCutOff
就是光源結(jié)構(gòu)體新增的成員變量,負(fù)責(zé)記錄外圓錐的余弦值γ来破。
??最后篮灼,給出內(nèi)外圓錐的余弦值:
shader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
shader.setFloat("light.outCutOff", glm::cos(glm::radians(17.5)));