國辦政府網(wǎng)站建設(shè)鄭州關(guān)鍵詞網(wǎng)站優(yōu)化排名
引言
?我們目前使用的所有光照都來自于一個單獨的光源,這是空間中的一個點。它的效果不錯,但是在真實世界,我們有多種類型的光,它們每個表現(xiàn)都不同。一個光源把光投射到物體上,叫做投光。這個教程里我們討論幾種不同的投光類型。學(xué)習(xí)模擬不同的光源是你未來豐富你的場景的另一個工具。
?我們首先討論定向光(directional light),接著是作為之前學(xué)到知識的擴(kuò)展的點光(point light),最后我們討論聚光(Spotlight)。下面的教程我們會把這幾種不同的光類型整合到一個場景中。
定向光
?當(dāng)一個光源很遠(yuǎn)的時候,來自光源的每條光線接近于平行。這看起來就像所有的光線來自于同一個方向,無論物體和觀察者在哪兒。當(dāng)一個光源被設(shè)置為無限遠(yuǎn)時,它被稱為定向光(Directional Light),因為所有的光線都有著同一個方向;它會獨立于光源的位置。
?我們知道的定向光源的一個好例子是,太陽。太陽和我們不是無限遠(yuǎn),但它也足夠遠(yuǎn)了,在計算光照的時候,我們感覺它就像無限遠(yuǎn)。在下面的圖片里,來自于太陽的所有的光線都被定義為平行光:
?因為所有的光線都是平行的,對于場景中的每個物體光的方向都保持一致,物體和光源的位置保持怎樣的關(guān)系都無所謂。由于光的方向向量保持一致,光照計算會和場景中的其他物體相似。
?我們可以通過定義一個光的方向向量,來模擬這樣一個定向光,而不是使用光的位置向量。著色器計算保持大致相同的要求,這次我們直接使用光的方向向量來代替用lightDir向量和position向量的計算:
struct Light
{// vec3 position; // 現(xiàn)在不在需要光源位置了,因為它是無限遠(yuǎn)的vec3 direction;vec3 ambient;vec3 diffuse;vec3 specular;
};
...
void main()
{vec3 lightDir = normalize(-light.direction);...
}
?注意,我們首先對light.direction向量取反。目前我們使用的光照計算需要光的方向作為一個來自片段朝向的光源的方向,但是人們通常更習(xí)慣定義一個定向光作為一個全局方向,它從光源發(fā)出。所以我們必須對全局光的方向向量取反來改變它的方向;它現(xiàn)在是一個方向向量指向光源。同時,確保對向量進(jìn)行標(biāo)準(zhǔn)化處理,因為假定輸入的向量就是一個單位向量是不明智的。
?作為結(jié)果的lightDir向量被使用在diffuse和specular計算之前。
?為了清晰地強(qiáng)調(diào)一個定向光對所有物體都有同樣的影響,我們再次訪問坐標(biāo)系教程結(jié)尾部分的箱子場景。例子里我們先定義10個不同的箱子位置,為每個箱子生成不同的模型矩陣,每個模型矩陣包含相應(yīng)的本地到世界變換:
for(GLuint i = 0; i < 10; i++)
{model = glm::mat4();model = glm::translate(model, cubePositions[i]);GLfloat angle = 20.0f * i;model = glm::rotate(model, angle, glm::vec3(1.0f, 0.3f, 0.5f));glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));glDrawArrays(GL_TRIANGLES, 0, 36);
}
?同時,不要忘記定義光源的方向(注意,我們把方向定義為:從光源處發(fā)出的方向;在下面,你可以快速看到光的方向的指向):
GLint lightDirPos = glGetUniformLocation(lightingShader.Program, "light.direction");
glUniform3f(lightDirPos, -0.2f, -1.0f, -0.3f);
?我們已經(jīng)把光的位置和方向向量傳遞為vec3,但是有些人去想更喜歡把所有的向量設(shè)置為vec4.當(dāng)定義位置向量為vec4的時候,把w元素設(shè)置為1.0非常重要,這樣平移和投影才會合理的被應(yīng)用。然而,當(dāng)定義一個方向向量為vec4時,我們并不想讓平移發(fā)揮作用(因為它們除了代表方向,其他什么也不是)所以我們把w元素設(shè)置為0.0。
?方向向量被表示為:vec4(0.2f, 1.0f, 0.3f, 0.0f)。這可以作為簡單檢查光的類型的方法:你可以檢查w元素是否等于1.0,查看我們現(xiàn)在所擁有的光的位置向量,w是否等于0.0,我們有一個光的方向向量,所以根據(jù)那個調(diào)整計算方法:
else if(lightVector.w == 1.0) // 像上一個教程一樣執(zhí)行頂點光照計算
?有趣的事實:這就是舊OpenGL(固定函數(shù)式)決定一個光源是一個定向光還是位置光源,更具這個修改它的光照。
?如果你現(xiàn)在編譯應(yīng)用,飛躍場景,它看起來像有一個太陽一樣的光源,把光拋到物體身上。你可以看到diffuse和specular元素都對該光源進(jìn)行反射了,就像天空上有一個光源嗎?看起來就像這樣:
點光源
?定向光作為全局光可以照亮整個場景,這非常棒,但是另一方面除了定向光,我們通常也需要幾個點光源(Point Light),在場景里發(fā)亮。點光是一個在時間里有位置的光源,它向所有方向發(fā)光,光線隨距離增加逐漸變暗。想象燈泡和火炬作為投光物,它們可以扮演點光的角色。
?之前的教程我們已經(jīng)使用了(最簡單的)點光。我們指定了一個光源以及其所在的位置,它從這個位置向所有方向發(fā)散光線。然而,我們定義的光源所模擬光線的強(qiáng)度卻不會因為距離變遠(yuǎn)而衰減,這使得看起來像是光源亮度極強(qiáng)。在大多數(shù)3D仿真場景中,我們更希望去模擬一個僅僅能照亮靠近光源點附近場景的光源,而不是照亮整個場景的光源。
?如果你把10個箱子添加到之前教程的光照場景中,你會注意到黑暗中的每個箱子都會有同樣的亮度,就像箱子在光照的前面;沒有公式定義光的距離衰減。我們想讓黑暗中與光源比較近的箱子被輕微地照亮。
衰減
?隨著光線穿越距離的變遠(yuǎn)使得亮度也相應(yīng)地減少的現(xiàn)象,通常稱之為衰減(Attenuation)。一種隨著距離減少亮度的方式是使用線性等式。這樣的一個隨著距離減少亮度的線性方程,可以使遠(yuǎn)處的物體更暗。然而,這樣的線性方程效果會有點假。在真實世界,通常光在近處時非常亮,但是一個光源的亮度,開始的時候減少的非???#xff0c;之后隨著距離的增加,減少的速度會慢下來。我們需要一種不同的方程來減少光的亮度。
?幸運的是一些聰明人已經(jīng)早就把它想到了。下面的方程把一個片段的光的亮度除以一個已經(jīng)計算出來的衰減值,這個值根據(jù)光源的遠(yuǎn)近得到:
?由于二次項的光會以線性方式減少,指導(dǎo)距離足夠大的時候,就會超過一次項,之后,光的亮度會減少的更快。最后的效果就是光在近距離時,非常量,但是距離變遠(yuǎn)亮度迅速降低,最后亮度降低速度再次變慢。下面的圖展示了在100以內(nèi)的范圍,這樣的衰減效果。
?你可以看到當(dāng)距離很近的時候光有最強(qiáng)的亮度,但是隨著距離增大,亮度明顯減弱,大約接近100的時候,就會慢下來。這就是我們想要的。
選擇正確的值
?但是,我們把這三個項設(shè)置為什么值呢?正確的值的設(shè)置由很多因素決定:環(huán)境、你希望光所覆蓋的距離范圍、光的類型等。大多數(shù)場合,這是經(jīng)驗的問題,也要適度調(diào)整。下面的表格展示一些各項的值,它們模擬現(xiàn)實(某種類型的)光源,覆蓋特定的半徑(距離)。第一欄定義一個光的距離,它覆蓋所給定的項。這些值是大多數(shù)光的良好開始,它是來自O(shè)gre3D的維基的禮物:
?就像你所看到的,常數(shù)項Kc一直都是1.0。一次項Kl為了覆蓋更遠(yuǎn)的距離通常很小,二次項Kq就更小了。嘗試用這些值進(jìn)行實驗,看看它們在你的實現(xiàn)中各自的效果。我們的環(huán)境中,32到100的距離對大多數(shù)光通常就足夠了。
實現(xiàn)衰減
?為了實現(xiàn)衰減,在著色器中我們會需要三個額外數(shù)值:也就是公式的常量、一次項和二次項。最好把它們儲存在之前定義的Light結(jié)構(gòu)體中。要注意的是我們計算lightDir,就是在前面的教程中我們所做的,不是像之前的定向光的那部分。
struct Light
{vec3 position;vec3 ambient;vec3 diffuse;vec3 specular;float constant;float linear;float quadratic;
};
?然后,我們在OpenGL中設(shè)置這些項:我們希望光覆蓋50的距離,所以我們會使用上面的表格中合適的常數(shù)項、一次項和二次項:
glUniform1f(glGetUniformLocation(lightingShader.Program, "light.constant"), 1.0f);
glUniform1f(glGetUniformLocation(lightingShader.Program, "light.linear"), 0.09);
glUniform1f(glGetUniformLocation(lightingShader.Program, "light.quadratic"), 0.032);
?在片段著色器中實現(xiàn)衰減很直接:我們根據(jù)公式簡單的計算衰減值,在乘以ambient、diffuse和specular元素。
?我們需要將光源的距離提供給公式;還記得我們是怎樣計算向量的長度嗎?我們可以通過獲取片段和光源之間的不同向量把向量的長度結(jié)果作為距離項。我們可以使用GLSL的內(nèi)建length函數(shù)做這件事:
float distance = length(light.position - FragPos);
float attenuation = 1.0f / (light.constant + light.linear*distance +light.quadratic*(distance*distance));
?然后,我們在光照計算中,通過把衰減值乘以ambient、diffuse和specular顏色,包含這個衰減值。
?我們可以可以把a(bǔ)mbient元素留著不變,這樣amient光照就不會隨著距離減少,但是如果我們使用多余1個的光源,所有的ambient元素會開始疊加,因此這種情況,我們希望ambient光照也衰減。簡單的調(diào)試出對于你的環(huán)境來說最好的效果。
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
?你可以看到現(xiàn)在只有最近處的箱子的前面被照得最亮。后面的箱子一點都沒被照亮,因為它們距離光源太遠(yuǎn)了。
?定點光就是一個可配的置位置和衰減值應(yīng)用到光照計算中。還有另一種類型光可用于我們照明庫當(dāng)中。
代碼
?片段著色器:
#version 330 core
struct Material
{sampler2D diffuse;sampler2D specular;sampler2D emission;float shininess;
};
uniform Material material;struct Light
{vec3 position;vec3 ambient;vec3 diffuse;vec3 specular;float constant;float linear;float quadratic;
};
uniform Light light;// 輸入頂點位置和法向量
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;// 輸出頂點的顏色
out vec4 color;// 頂點本身的顏色
uniform vec3 objectColor;
// 光源的顏色和位置
uniform vec3 lightColor;
uniform vec3 lightPos;
// 觀察者的位置(世界坐標(biāo))
uniform vec3 viewPos;void main()
{float distance = length(light.position - FragPos);float attenuation = 1.0f / (light.constant + light.linear*distance +light.quadratic*(distance*distance));// 環(huán)境光vec3 ambient = light.ambient * vec3(texture(material.emission, TexCoords));// 漫反射光vec3 norm = normalize(Normal);vec3 lightDir = normalize(lightPos - FragPos);float diff = max(dot(norm, lightDir), 0.0);vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));// 鏡面高光vec3 viewDir = normalize(viewPos - FragPos);vec3 reflectDir = reflect(-lightDir, norm); float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords)); ambient *= attenuation;diffuse *= attenuation;specular *= attenuation;vec3 result = ambient + diffuse + specular;color = vec4(result, 1.0f);
}
聚光
?我們要討論的最后一種類型光是聚光(Spotlight)。聚光是一種位于環(huán)境中某處的光源,它不是向所有方向照射,而是只朝某個方向照射。結(jié)果是只有一個聚光照射方向的確定半徑內(nèi)的物體才會被照亮,其他的都保持黑暗。聚光的好例子是路燈或手電筒。
?OpenGL中的聚光用世界空間位置,一個方向和一個指定了聚光半徑的切光角來表示。我們計算的每個片段,如果片段在聚光的切光方向之間(就是在圓錐體內(nèi)),我們就會把片段照亮。下面的圖可以讓你明白聚光是如何工作的:
?所以我們大致要做的是,計算LightDir向量和SpotDir向量的點乘(返回兩個單位向量的點乘,還記得嗎?),然后在和切光角?
對比?,F(xiàn)在你應(yīng)該明白聚光是我們下面將創(chuàng)建的手電筒的范例。
手電筒
?手電筒(Flashlight)是一個坐落在觀察者位置的聚光,通常瞄準(zhǔn)玩家透視圖的前面?;旧险f,一個手電筒是一個普通的聚光,但是根據(jù)玩家的位置和方向持續(xù)的更新它的位置和方向。
?所以我們需要為片段著色器提供的值,是聚光的位置向量(來計算光的方向坐標(biāo)),聚光的方向向量和切光角。我們可以把這些值儲存在Light結(jié)構(gòu)體中:
struct Light
{vec3 position;vec3 direction;float cutOff;...
}
?下面我們把這些適當(dāng)?shù)闹祩鹘o著色器:
glUniform3f(lightPosLoc, camera.Position.x, camera.Position.y, camera.Position.z);
glUniform3f(lightSpotdirLoc, camera.Front.x, camera.Front.y, camera.Front.z);
glUniform1f(lightSpotCutOffLoc, glm::cos(glm::radians(12.5f)));
?你可以看到,我們?yōu)榍泄饨窃O(shè)置一個角度,但是我們根據(jù)一個角度計算了余弦值,把這個余弦結(jié)果傳給了片段著色器。這么做的原因是在片段著色器中,我們計算LightDir和SpotDir向量的點乘,而點乘返回一個余弦值,不是一個角度,所以我們不能直接把一個角度和余弦值對比。為了獲得這個角度,我們必須計算點乘結(jié)果的反余弦,這個操作開銷是很大的。所以為了節(jié)約一些性能,我們先計算給定切光角的余弦值,然后把結(jié)果傳遞給片段著色器。由于每個角度都被表示為余弦了,我們可以直接對比它們,而不用進(jìn)行任何開銷高昂的操作。
?現(xiàn)在剩下要做的是計算θ值,用它和?值對比,以決定我們是否在或不在聚光的內(nèi)部:
float theta = dot(lightDir, normalize(-light.direction));
if(theta > light.cutOff)
{// 執(zhí)行光照計算
}
else // 否則使用環(huán)境光,使得場景不至于完全黑暗
color = vec4(light.ambient*vec3(texture(material.diffuse,TexCoords)), 1.0f);
?我們首先計算lightDir和取反的direction向量的點乘(它是取反過的因為我們想要向量指向光源,而不是從光源作為指向出發(fā)點。譯注:前面的specular教程中作者卻用了相反的表示方法,這里讀者可以選擇喜歡的表達(dá)方式)。確保對所有相關(guān)向量進(jìn)行了標(biāo)準(zhǔn)化處理。
?你可能奇怪為什么if條件中使用>符號而不是<符號。為了在聚光以內(nèi),theta不是應(yīng)該比光的切光值更小嗎?這沒錯,但是不要忘了,角度值是以余弦值來表示的,一個0度的角表示為1.0的余弦值,當(dāng)一個角是90度的時候被表示為0.0的余弦值,你可以在這里看到:
?現(xiàn)在你可以看到,余弦越是接近1.0,角度就越小。這就解釋了為什么θ需要比切光值更大了。切光值當(dāng)前被設(shè)置為12.5的余弦,它等于0.9978,所以θ的余弦值在0.9979和1.0之間,片段會在聚光內(nèi),被照亮。
運行應(yīng)用,在聚光內(nèi)的片段才會被照亮。這看起來像這樣:
?它看起來仍然有點假,原因是聚光有了一個硬邊。片段著色器一旦到達(dá)了聚光的圓錐邊緣,它就立刻黑了下來,卻沒有任何平滑減弱的過度。一個真實的聚光的光會在它的邊界處平滑減弱的。
?片段著色器源碼:
#version 330 core
struct Material {sampler2D diffuse;sampler2D specular;float shininess;
}; struct Light {vec3 position;vec3 direction;float cutOff;float constant;float linear;float quadratic;vec3 ambient;vec3 diffuse;vec3 specular;
}; in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;out vec4 color;uniform vec3 viewPos;
uniform Material material;
uniform Light light;void main()
{vec3 lightDir = normalize(light.position - FragPos);// Check if lighting is inside the spotlight conefloat theta = dot(lightDir, normalize(-light.direction)); if(theta > light.cutOff) // Remember that we're working with angles as cosines instead of degrees so a '>' is used.{ // Ambientvec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));// Diffuse vec3 norm = normalize(Normal); float diff = max(dot(norm, lightDir), 0.0);vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords)); // Specularvec3 viewDir = normalize(viewPos - FragPos);vec3 reflectDir = reflect(-lightDir, norm); float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));// Attenuationfloat distance = length(light.position - FragPos);float attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * (distance * distance)); // ambient *= attenuation; // Also remove attenuation from ambient, because if we move too far, the light in spotlight would then be darker than outside (since outside spotlight we have ambient lighting).diffuse *= attenuation;specular *= attenuation; color = vec4(ambient + diffuse + specular, 1.0f); }else // else, use ambient light so scene isn't completely dark outside the spotlight.color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0f);
}