日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

Learn OpenGL (十二):投光物

發(fā)布時(shí)間:2023/11/27 生活经验 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Learn OpenGL (十二):投光物 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

平行光

當(dāng)一個(gè)光源處于很遠(yuǎn)的地方時(shí),來自光源的每條光線就會(huì)近似于互相平行。不論物體和/或者觀察者的位置,看起來好像所有的光都來自于同一個(gè)方向。當(dāng)我們使用一個(gè)假設(shè)光源處于無限遠(yuǎn)處的模型時(shí),它就被稱為定向光,因?yàn)樗乃泄饩€都有著相同的方向,它與光源的位置是沒有關(guān)系的。

定向光非常好的一個(gè)例子就是太陽。太陽距離我們并不是無限遠(yuǎn),但它已經(jīng)遠(yuǎn)到在光照計(jì)算中可以把它視為無限遠(yuǎn)了。所以來自太陽的所有光線將被模擬為平行光線,我們可以在下圖看到:

因?yàn)樗械墓饩€都是平行的,所以物體與光源的相對位置是不重要的,因?yàn)閷鼍爸忻恳粋€(gè)物體光的方向都是一致的。由于光的位置向量保持一致,場景中每個(gè)物體的光照計(jì)算將會(huì)是類似的。

我們可以定義一個(gè)光線方向向量而不是位置向量來模擬一個(gè)定向光。著色器的計(jì)算基本保持不變,但這次我們將直接使用光的direction向量而不是通過direction來計(jì)算lightDir向量。

struct Light {// vec3 position; // 使用定向光就不再需要了vec3 direction;vec3 ambient;vec3 diffuse;vec3 specular;
};
...
void main()
{vec3 lightDir = normalize(-light.direction);...
}

注意我們首先對light.direction向量取反。我們目前使用的光照計(jì)算需求一個(gè)從片段光源的光線方向,但人們更習(xí)慣定義定向光為一個(gè)光源出發(fā)的全局方向。所以我們需要對全局光照方向向量取反來改變它的方向,它現(xiàn)在是一個(gè)指向光源的方向向量了。而且,記得對向量進(jìn)行標(biāo)準(zhǔn)化,假設(shè)輸入向量為一個(gè)單位向量是很不明智的。

最終的lightDir向量將和以前一樣用在漫反射和鏡面光計(jì)算中。

為了清楚地展示定向光對多個(gè)物體具有相同的影響,我們將會(huì)再次使用坐標(biāo)系統(tǒng)章節(jié)最后的那個(gè)箱子派對的場景。如果你錯(cuò)過了派對,我們先定義了十個(gè)不同的箱子位置,并對每個(gè)箱子都生成了一個(gè)不同的模型矩陣,每個(gè)模型矩陣都包含了對應(yīng)的局部-世界坐標(biāo)變換:

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í),不要忘記定義光源的方向(注意我們將方向定義為光源出發(fā)的方向,你可以很容易看到光的方向朝下)。

lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);

如果你現(xiàn)在編譯程序,在場景中自由移動(dòng),你就可以看到好像有一個(gè)太陽一樣的光源對所有的物體投光。你能注意到漫反射和鏡面光分量的反應(yīng)都好像在天空中有一個(gè)光源的感覺嗎?它會(huì)看起來像這樣:

你可以在這里找到程序的所有代碼。

點(diǎn)光源

定向光對于照亮整個(gè)場景的全局光源是非常棒的,但除了定向光之外我們也需要一些分散在場景中的點(diǎn)光源(Point Light)。點(diǎn)光源是處于世界中某一個(gè)位置的光源,它會(huì)朝著所有方向發(fā)光,但光線會(huì)隨著距離逐漸衰減。想象作為投光物的燈泡和火把,它們都是點(diǎn)光源。

在之前的教程中,我們一直都在使用一個(gè)(簡化的)點(diǎn)光源。我們在給定位置有一個(gè)光源,它會(huì)從它的光源位置開始朝著所有方向散射光線。然而,我們定義的光源模擬的是永遠(yuǎn)不會(huì)衰減的光線,這看起來像是光源亮度非常的強(qiáng)。在大部分的3D模擬中,我們都希望模擬的光源僅照亮光源附近的區(qū)域而不是整個(gè)場景。

如果你將10個(gè)箱子加入到上一節(jié)光照場景中,你會(huì)注意到在最后面的箱子和在燈面前的箱子都以相同的強(qiáng)度被照亮,并沒有定義一個(gè)公式來將光隨距離衰減。我們希望在后排的箱子與前排的箱子相比僅僅是被輕微地照亮。

衰減

隨著光線傳播距離的增長逐漸削減光的強(qiáng)度通常叫做衰減(Attenuation)。隨距離減少光強(qiáng)度的一種方式是使用一個(gè)線性方程。這樣的方程能夠隨著距離的增長線性地減少光的強(qiáng)度,從而讓遠(yuǎn)處的物體更暗。然而,這樣的線性方程通常會(huì)看起來比較假。在現(xiàn)實(shí)世界中,燈在近處通常會(huì)非常亮,但隨著距離的增加光源的亮度一開始會(huì)下降非常快,但在遠(yuǎn)處時(shí)剩余的光強(qiáng)度就會(huì)下降的非常緩慢了。所以,我們需要一個(gè)不同的公式來減少光的強(qiáng)度。

幸運(yùn)的是一些聰明的人已經(jīng)幫我們解決了這個(gè)問題。下面這個(gè)公式根據(jù)片段距光源的距離計(jì)算了衰減值,之后我們會(huì)將它乘以光的強(qiáng)度向量:

在這里dd代表了片段距光源的距離。接下來為了計(jì)算衰減值,我們定義3個(gè)(可配置的)項(xiàng):常數(shù)項(xiàng)KcKc、一次項(xiàng)KlKl和二次項(xiàng)KqKq。

  • 常數(shù)項(xiàng)通常保持為1.0,它的主要作用是保證分母永遠(yuǎn)不會(huì)比1小,否則的話在某些距離上它反而會(huì)增加強(qiáng)度,這肯定不是我們想要的效果。
  • 一次項(xiàng)會(huì)與距離值相乘,以線性的方式減少強(qiáng)度。
  • 二次項(xiàng)會(huì)與距離的平方相乘,讓光源以二次遞減的方式減少強(qiáng)度。二次項(xiàng)在距離比較小的時(shí)候影響會(huì)比一次項(xiàng)小很多,但當(dāng)距離值比較大的時(shí)候它就會(huì)比一次項(xiàng)更大了。

由于二次項(xiàng)的存在,光線會(huì)在大部分時(shí)候以線性的方式衰退,直到距離變得足夠大,讓二次項(xiàng)超過一次項(xiàng),光的強(qiáng)度會(huì)以更快的速度下降。這樣的結(jié)果就是,光在近距離時(shí)亮度很高,但隨著距離變遠(yuǎn)亮度迅速降低,最后會(huì)以更慢的速度減少亮度。下面這張圖顯示了在100的距離內(nèi)衰減的效果:

你可以看到光在近距離的時(shí)候有著最高的強(qiáng)度,但隨著距離增長,它的強(qiáng)度明顯減弱,并緩慢地在距離大約100的時(shí)候強(qiáng)度接近0。這正是我們想要的。

選擇正確的值

但是,該對這三個(gè)項(xiàng)設(shè)置什么值呢?正確地設(shè)定它們的值取決于很多因素:環(huán)境、希望光覆蓋的距離、光的類型等。在大多數(shù)情況下,這都是經(jīng)驗(yàn)的問題,以及適量的調(diào)整。下面這個(gè)表格顯示了模擬一個(gè)(大概)真實(shí)的,覆蓋特定半徑(距離)的光源時(shí),這些項(xiàng)可能取的一些值。第一列指定的是在給定的三項(xiàng)時(shí)光所能覆蓋的距離。這些值是大多數(shù)光源很好的起始點(diǎn),它們由Ogre3D的Wiki所提供:

距離常數(shù)項(xiàng)一次項(xiàng)二次項(xiàng)
71.00.71.8
131.00.350.44
201.00.220.20
321.00.140.07
501.00.090.032
651.00.070.017
1001.00.0450.0075
1601.00.0270.0028
2001.00.0220.0019
3251.00.0140.0007
6001.00.0070.0002
32501.00.00140.000007

你可以看到,常數(shù)項(xiàng)KcKc在所有的情況下都是1.0。一次項(xiàng)KlKl為了覆蓋更遠(yuǎn)的距離通常都很小,二次項(xiàng)KqKq甚至更小。嘗試對這些值進(jìn)行實(shí)驗(yàn),看看它們在你的實(shí)現(xiàn)中有什么效果。在我們的環(huán)境中,32到100的距離對大多數(shù)的光源都足夠了。

實(shí)現(xiàn)衰減

為了實(shí)現(xiàn)衰減,在片段著色器中我們還需要三個(gè)額外的值:也就是公式中的常數(shù)項(xiàng)、一次項(xiàng)和二次項(xiàng)。它們最好儲(chǔ)存在之前定義的Light結(jié)構(gòu)體中。注意我們使用上一節(jié)中計(jì)算lightDir的方法,而不是上面定向光部分的。

struct Light {vec3 position;  vec3 ambient;vec3 diffuse;vec3 specular;float constant;float linear;float quadratic;
};

然后我們將在OpenGL中設(shè)置這些項(xiàng):我們希望光源能夠覆蓋50的距離,所以我們會(huì)使用表格中對應(yīng)的常數(shù)項(xiàng)、一次項(xiàng)和二次項(xiàng):

lightingShader.setFloat("light.constant",  1.0f);
lightingShader.setFloat("light.linear",    0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);

在片段著色器中實(shí)現(xiàn)衰減還是比較直接的:我們根據(jù)公式計(jì)算衰減值,之后再分別乘以環(huán)境光、漫反射和鏡面光分量。

我們?nèi)孕枰街芯喙庠吹木嚯x,還記得我們是怎么計(jì)算一個(gè)向量的長度的嗎?我們可以通過獲取片段和光源之間的向量差,并獲取結(jié)果向量的長度作為距離項(xiàng)。我們可以使用GLSL內(nèi)建的length函數(shù)來完成這一點(diǎn):

float distance    = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));

接下來,我們將包含這個(gè)衰減值到光照計(jì)算中,將它分別乘以環(huán)境光、漫反射和鏡面光顏色。

我們可以將環(huán)境光分量保持不變,讓環(huán)境光照不會(huì)隨著距離減少,但是如果我們使用多于一個(gè)的光源,所有的環(huán)境光分量將會(huì)開始疊加,所以在這種情況下我們也希望衰減環(huán)境光照。簡單實(shí)驗(yàn)一下,看看什么才能在你的環(huán)境中效果最好。

ambient  *= attenuation; 
diffuse  *= attenuation;
specular *= attenuation;

如果你運(yùn)行程序的話,你會(huì)獲得這樣的結(jié)果:

你可以看到,只有前排的箱子被照亮的,距離最近的箱子是最亮的。后排的箱子一點(diǎn)都沒有照亮,因?yàn)樗鼈冸x光源實(shí)在是太遠(yuǎn)了。你可以在這里找到程序的代碼。

點(diǎn)光源就是一個(gè)能夠配置位置和衰減的光源。它是我們光照工具箱中的又一個(gè)光照類型。

聚光

我們要討論的最后一種類型的光是聚光(Spotlight)。聚光是位于環(huán)境中某個(gè)位置的光源,它只朝一個(gè)特定方向而不是所有方向照射光線。這樣的結(jié)果就是只有在聚光方向的特定半徑內(nèi)的物體才會(huì)被照亮,其它的物體都會(huì)保持黑暗。聚光很好的例子就是路燈或手電筒。

OpenGL中聚光是用一個(gè)世界空間位置、一個(gè)方向和一個(gè)切光角(Cutoff Angle)來表示的,切光角指定了聚光的半徑(譯注:是圓錐的半徑不是距光源距離那個(gè)半徑)。對于每個(gè)片段,我們會(huì)計(jì)算片段是否位于聚光的切光方向之間(也就是在錐形內(nèi)),如果是的話,我們就會(huì)相應(yīng)地照亮片段。下面這張圖會(huì)讓你明白聚光是如何工作的:

  • LightDir:從片段指向光源的向量。
  • SpotDir:聚光所指向的方向。
  • Phi?:指定了聚光半徑的切光角。落在這個(gè)角度之外的物體都不會(huì)被這個(gè)聚光所照亮。
  • Thetaθ:LightDir向量和SpotDir向量之間的夾角。在聚光內(nèi)部的話θ值應(yīng)該比?值小。

所以我們要做的就是計(jì)算LightDir向量和SpotDir向量之間的點(diǎn)積(還記得它會(huì)返回兩個(gè)單位向量夾角的余弦值嗎?),并將它與切光角?值對比。你現(xiàn)在應(yīng)該了解聚光究竟是什么了,下面我們將以手電筒的形式創(chuàng)建一個(gè)聚光。

手電筒

手電筒(Flashlight)是一個(gè)位于觀察者位置的聚光,通常它都會(huì)瞄準(zhǔn)玩家視角的正前方。基本上說,手電筒就是普通的聚光,但它的位置和方向會(huì)隨著玩家的位置和朝向不斷更新。

所以,在片段著色器中我們需要的值有聚光的位置向量(來計(jì)算光的方向向量)、聚光的方向向量和一個(gè)切光角。我們可以將它們儲(chǔ)存在Light結(jié)構(gòu)體中:

struct Light {vec3  position;vec3  direction;float cutOff;...
};

接下來我們將合適的值傳到著色器中:

lightingShader.setVec3("light.position",  camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff",   glm::cos(glm::radians(12.5f)));

你可以看到,我們并沒有給切光角設(shè)置一個(gè)角度值,反而是用角度值計(jì)算了一個(gè)余弦值,將余弦結(jié)果傳遞到片段著色器中。這樣做的原因是在片段著色器中,我們會(huì)計(jì)算LightDirSpotDir向量的點(diǎn)積,這個(gè)點(diǎn)積返回的將是一個(gè)余弦值而不是角度值,所以我們不能直接使用角度值和余弦值進(jìn)行比較。為了獲取角度值我們需要計(jì)算點(diǎn)積結(jié)果的反余弦,這是一個(gè)開銷很大的計(jì)算。所以為了節(jié)約一點(diǎn)性能開銷,我們將會(huì)計(jì)算切光角對應(yīng)的余弦值,并將它的結(jié)果傳入片段著色器中。由于這兩個(gè)角度現(xiàn)在都由余弦角來表示了,我們可以直接對它們進(jìn)行比較而不用進(jìn)行任何開銷高昂的計(jì)算。

接下來就是計(jì)算θθ值,并將它和切光角??對比,來決定是否在聚光的內(nèi)部:

float theta = dot(lightDir, normalize(-light.direction));if(theta > light.cutOff) 
{       // 執(zhí)行光照計(jì)算
}
else  // 否則,使用環(huán)境光,讓場景在聚光之外時(shí)不至于完全黑暗color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);

我們首先計(jì)算了lightDir和取反的direction向量(取反的是因?yàn)槲覀兿胱屜蛄恐赶蚬庠炊皇菑墓庠闯霭l(fā))之間的點(diǎn)積。記住要對所有的相關(guān)向量標(biāo)準(zhǔn)化。

你可能奇怪為什么在if條件中使用的是 > 符號(hào)而不是 < 符號(hào)。theta不應(yīng)該比光的切光角更小才是在聚光內(nèi)部嗎?這并沒有錯(cuò),但不要忘記角度值現(xiàn)在都由余弦值來表示的。一個(gè)0度的角度表示的是1.0的余弦值,而一個(gè)90度的角度表示的是0.0的余弦值,你可以在下圖中看到:

你現(xiàn)在可以看到,余弦值越接近1.0,它的角度就越小。這也就解釋了為什么theta要比切光值更大了。切光值目前設(shè)置為12.5的余弦,約等于0.9978,所以在0.9979到1.0內(nèi)的theta值才能保證片段在聚光內(nèi),從而被照亮。

運(yùn)行程序,你將會(huì)看到一個(gè)聚光,它僅會(huì)照亮聚光圓錐內(nèi)的片段。看起來像是這樣的:

你可以在這里獲得全部源碼。

但這仍看起來有些假,主要是因?yàn)榫酃庥幸蝗τ策叀.?dāng)一個(gè)片段遇到聚光圓錐的邊緣時(shí),它會(huì)完全變暗,沒有一點(diǎn)平滑的過渡。一個(gè)真實(shí)的聚光將會(huì)在邊緣處逐漸減少亮度。

平滑/軟化邊緣

為了創(chuàng)建一種看起來邊緣平滑的聚光,我們需要模擬聚光有一個(gè)內(nèi)圓錐(Inner Cone)和一個(gè)外圓錐(Outer Cone)。我們可以將內(nèi)圓錐設(shè)置為上一部分中的那個(gè)圓錐,但我們也需要一個(gè)外圓錐,來讓光從內(nèi)圓錐逐漸減暗,直到外圓錐的邊界。

為了創(chuàng)建一個(gè)外圓錐,我們只需要再定義一個(gè)余弦值來代表聚光方向向量和外圓錐向量(等于它的半徑)的夾角。然后,如果一個(gè)片段處于內(nèi)外圓錐之間,將會(huì)給它計(jì)算出一個(gè)0.0到1.0之間的強(qiáng)度值。如果片段在內(nèi)圓錐之內(nèi),它的強(qiáng)度就是1.0,如果在外圓錐之外強(qiáng)度值就是0.0。

我們可以用下面這個(gè)公式來計(jì)算這個(gè)值:

?

I=θ?γ?I=θ?γ?

這里??(Epsilon)是內(nèi)(??)和外圓錐(γγ)之間的余弦值差(?=??γ?=??γ)。最終的II值就是在當(dāng)前片段聚光的強(qiáng)度。

很難去表現(xiàn)這個(gè)公式是怎么工作的,所以我們用一些實(shí)例值來看看:

θθθθ(角度)??(內(nèi)光切)??(角度)γγ(外光切)γγ(角度)??II
0.87300.91250.82350.91 - 0.82 = 0.090.87 - 0.82 / 0.09 = 0.56
0.9260.91250.82350.91 - 0.82 = 0.090.9 - 0.82 / 0.09 = 0.89
0.97140.91250.82350.91 - 0.82 = 0.090.97 - 0.82 / 0.09 = 1.67
0.83340.91250.82350.91 - 0.82 = 0.090.83 - 0.82 / 0.09 = 0.11
0.64500.91250.82350.91 - 0.82 = 0.090.64 - 0.82 / 0.09 = -2.0
0.966150.997812.50.95317.50.966 - 0.953 = 0.04480.966 - 0.953 / 0.0448 = 0.29

你可以看到,我們基本是在內(nèi)外余弦值之間根據(jù)θθ插值。如果你仍不明白發(fā)生了什么,不必?fù)?dān)心,只需要記住這個(gè)公式就好了,在你更聰明的時(shí)候再回來看看。

我們現(xiàn)在有了一個(gè)在聚光外是負(fù)的,在內(nèi)圓錐內(nèi)大于1.0的,在邊緣處于兩者之間的強(qiáng)度值了。如果我們正確地約束(Clamp)這個(gè)值,在片段著色器中就不再需要if-else了,我們能夠使用計(jì)算出來的強(qiáng)度值直接乘以光照分量:

float theta     = dot(lightDir, normalize(-light.direction));
float epsilon   = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);    
...
// 將不對環(huán)境光做出影響,讓它總是能有一點(diǎn)光
diffuse  *= intensity;
specular *= intensity;
...

注意我們使用了clamp函數(shù),它把第一個(gè)參數(shù)約束(Clamp)在了0.0到1.0之間。這保證強(qiáng)度值不會(huì)在[0, 1]區(qū)間之外。

確定你將outerCutOff值添加到了Light結(jié)構(gòu)體之中,并在程序中設(shè)置它的uniform值。下面的圖片中,我們使用的內(nèi)切光角是12.5,外切光角是17.5:

啊,這樣看起來就好多了。稍微對內(nèi)外切光角實(shí)驗(yàn)一下,嘗試創(chuàng)建一個(gè)更能符合你需求的聚光。你可以在這里找到程序的源碼。

這樣的手電筒/聚光類型的燈光非常適合恐怖游戲,結(jié)合定向光和點(diǎn)光源,環(huán)境就會(huì)開始被照亮了。在下一節(jié)的教程中,我們將會(huì)結(jié)合我們至今討論的所有光照和技巧。

總結(jié)

以上是生活随笔為你收集整理的Learn OpenGL (十二):投光物的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。