我們?cè)谇懊娴慕坛讨幸呀?jīng)學(xué)習(xí)了許多關(guān)于OpenGL 光照的知識(shí),其中包括馮氏照明模型(Phong shading)、光照材質(zhì)(Materials)、光照?qǐng)D(Lighting maps)以及各種投光物(Light casters)。本教程將結(jié)合上述所學(xué)的知識(shí),創(chuàng)建一個(gè)包含六個(gè)光源的場(chǎng)景。我們將模擬一個(gè)類(lèi)似陽(yáng)光的平行光(Directional light)和4個(gè)定點(diǎn)光(Point lights)以及一個(gè)手電筒(Flashlight).
要在場(chǎng)景中使用多光源我們需要封裝一些GLSL函數(shù)用來(lái)計(jì)算光照。如果我們對(duì)每個(gè)光源都去寫(xiě)一遍光照計(jì)算的代碼,這將是一件令人惡心的事情,并且這些放在main函數(shù)中的代碼將難以理解,所以我們將一些操作封裝為函數(shù)。
GLSL中的函數(shù)與C語(yǔ)言的非常相似,它需要一個(gè)函數(shù)名、一個(gè)返回值類(lèi)型。并且在調(diào)用前必須提前聲明。接下來(lái)我們將為下面的每一種光照來(lái)寫(xiě)一個(gè)函數(shù)。
當(dāng)我們?cè)趫?chǎng)景中使用多個(gè)光源時(shí)一般使用以下途徑:創(chuàng)建一個(gè)代表輸出顏色的向量。每一個(gè)光源都對(duì)輸出顏色貢獻(xiàn)一些顏色。因此,場(chǎng)景中的每個(gè)光源將進(jìn)行獨(dú)立運(yùn)算,并且運(yùn)算結(jié)果都對(duì)最終的輸出顏色有一定影響。
下面是使用這種方式進(jìn)行多光源運(yùn)算的一般結(jié)構(gòu):
out vec4 color;
void main()
{
// 定義輸出顏色
vec3 output;
// 將平行光的運(yùn)算結(jié)果顏色添加到輸出顏色
output += someFunctionToCalculateDirectionalLight();
// 同樣,將定點(diǎn)光的運(yùn)算結(jié)果顏色添加到輸出顏色
for(int i = 0; i < nr_of_point_lights; i++)
output += someFunctionToCalculatePointLight();
// 添加其他光源的計(jì)算結(jié)果顏色(如投射光)
output += someFunctionToCalculateSpotLight();
color = vec4(output, 1.0);
}
即使對(duì)每一種光源的運(yùn)算實(shí)現(xiàn)不同,但此算法的結(jié)構(gòu)一般是與上述出入不大的。我們將定義幾個(gè)用于計(jì)算各個(gè)光源的函數(shù),并將這些函數(shù)的結(jié)算結(jié)果(返回顏色)添加到輸出顏色向量中。例如,兩個(gè)光源靠近一個(gè)片段,則它們的綜合作用下片段將會(huì)比只有一個(gè)光源靠近的情況下明亮。
平行光(Directional light)
我們要在片段著色器中定義一個(gè)函數(shù)用來(lái)計(jì)算平行光在對(duì)應(yīng)的照射點(diǎn)上的光照顏色,這個(gè)函數(shù)需要幾個(gè)參數(shù)并返回一個(gè)計(jì)算平行光照結(jié)果的顏色。
首先我們需要設(shè)置一系列用于表示平行光的變量,我們可以將這些變量定義在一個(gè)叫做DirLight的結(jié)構(gòu)體中,并定義一個(gè)這個(gè)結(jié)構(gòu)體類(lèi)型的uniform變量。這些需要設(shè)置的變量與上節(jié)中設(shè)置的相似。
struct DirLIght {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform DIrLIght dirLight;
之后我們可以將dirLight這個(gè)uniform變量作為下面這個(gè)函數(shù)原型的參數(shù)。
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
和C/C++一樣,我們調(diào)用一個(gè)函數(shù)的前提是這個(gè)函數(shù)在調(diào)用前已經(jīng)被聲明過(guò)(此例中我們是在main函數(shù)中調(diào)用)。通常情況下我們都將函數(shù)定義在main函數(shù)之后,為了能在main函數(shù)中調(diào)用這些函數(shù),我們就必須在main函數(shù)之前聲明這些函數(shù)的原型,這就和我們寫(xiě)C語(yǔ)言是一樣的。
你已經(jīng)知道,這個(gè)函數(shù)需要一個(gè)DirLight和兩個(gè)其他的向量作為參數(shù)來(lái)計(jì)算光照。如果你看過(guò)之前的教程的話,你會(huì)覺(jué)得下面的函數(shù)定義得一點(diǎn)也不意外:
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
vec3 lightDir = normalize(-light.direction);
// Diffuse shading
float diff = max(dot(normal, lightDir), 0.0);
// Specular shading
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// Combine results
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
return (ambient + diffuse + specular);
}
我們從之前的教程中復(fù)制了代碼,并用兩個(gè)向量來(lái)作為函數(shù)參數(shù)來(lái)計(jì)算出平行光的光照顏色向量,該結(jié)果是一個(gè)由該平行光的環(huán)境反射、漫反射和鏡面反射的各個(gè)分量組成的一個(gè)向量。
定點(diǎn)光(Point light)
和計(jì)算平行光一樣,我們同樣需要定義一個(gè)函數(shù)用于計(jì)算定點(diǎn)光照。同樣的,我們定義一個(gè)包含定點(diǎn)光源所需屬性的結(jié)構(gòu)體:
struct PointLight {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
#define NR_POINT_LIGHTS 4
uniform PointLight pointLights[NR_POINT_LIGHTS];
現(xiàn)在我們有了4個(gè)PointLight結(jié)構(gòu)體對(duì)象了。
我們同樣可以簡(jiǎn)單粗暴地定義一個(gè)大號(hào)的結(jié)構(gòu)體(而不是為每一種類(lèi)型的光源定義一個(gè)結(jié)構(gòu)體),它包含所有類(lèi)型光源所需要屬性變量。并且將這個(gè)結(jié)構(gòu)體應(yīng)用與所有的光照計(jì)算函數(shù),在各個(gè)光照結(jié)算時(shí)忽略不需要的屬性變量。然而,就我個(gè)人來(lái)說(shuō)更喜歡分開(kāi)定義,這樣可以省下一些內(nèi)存,因?yàn)槎x一個(gè)大號(hào)的光源結(jié)構(gòu)體在計(jì)算過(guò)程中會(huì)有用不到的變量。
vec3 CalcPointLight (PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// Diffuse shading
float diff = max(dot(normal, lightDir), 0.0);
// Specular shading
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(vewDir, reflectDir), 0.0), material.shininess);
// Attenuation
float distance = length(light.position - fragPos);
float attenuation = 1..0f / (light.constant + light.linear * distance + light.quadratic * distance * distance)
// Combine results
vec3 ambinet = light.ambinet * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return (ambient + diffuse + specular);
}
有了這個(gè)函數(shù)我們就可以在main函數(shù)中調(diào)用它來(lái)代替寫(xiě)很多個(gè)計(jì)算點(diǎn)光源的代碼了。通過(guò)循環(huán)調(diào)用此函數(shù)就能實(shí)現(xiàn)同樣的效果,當(dāng)然代碼更簡(jiǎn)潔。
把它們放到一起
我們現(xiàn)在定義了用于計(jì)算平行光和定點(diǎn)光的函數(shù),現(xiàn)在我們把這些代碼放到一起,寫(xiě)入文章開(kāi)始的一般結(jié)構(gòu)中:
void main()
{
// 一些屬性
vec3 norm = normalize(Normal);
vec3 viewDir = normalize(viewPos - FragPos);
// 第一步,計(jì)算平行光照
vec3 result = CalcDirLight(dirLight, norm, viewDir);
// 第二步,計(jì)算頂點(diǎn)光照
for(int i = 0; i < NR_POINT_LIGHTS; i++)
result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);
// 第三部,計(jì)算 Spot light
//result += CalcSpotLight(spotLight, norm, FragPos, viewDir);
color = vec4(result, 1.0);
}
每一個(gè)光源的運(yùn)算結(jié)果都添加到了輸出顏色上,輸出顏色包含了此場(chǎng)景中的所有光源的影響。如果你想實(shí)現(xiàn)手電筒的光照效果,同樣的把計(jì)算結(jié)果添加到輸出顏色上。我在這里就把CalcSpotLight的實(shí)現(xiàn)留作個(gè)讀者們的練習(xí)吧。
設(shè)置平行光結(jié)構(gòu)體的uniform值和之前所講過(guò)的方式?jīng)]什么兩樣,但是你可能想知道如何設(shè)置場(chǎng)景中PointLight結(jié)構(gòu)體的uniforms變量數(shù)組。我們之前并未討論過(guò)如何做這件事。
慶幸的是,這并不是什么難題。設(shè)置uniform變量數(shù)組和設(shè)置單個(gè)uniform變量值是相似的,只需要用一個(gè)合適的下標(biāo)就能夠檢索到數(shù)組中我們想要的uniform變量了。
glUniform1f(glGetUniformLocation(lightingShader.Program, "pointLights[0].constant"), 1.0f);
這樣我們檢索到pointLights數(shù)組中的第一個(gè)PointLight結(jié)構(gòu)體元素,同時(shí)也可以獲取到該結(jié)構(gòu)體中的各個(gè)屬性變量。不幸的是這一位置我們還需要手動(dòng)對(duì)這個(gè)四個(gè)光源的每一個(gè)屬性都進(jìn)行設(shè)置,這樣手動(dòng)設(shè)置這28個(gè)uniform變量是相當(dāng)乏味的工作。你可以嘗試去定義個(gè)光源類(lèi)來(lái)為你設(shè)置這些uniform屬性來(lái)減少你的工作,但這依舊不能改變?nèi)ピO(shè)置每個(gè)uniform屬性的事實(shí)。
別忘了,我們還需要為每個(gè)光源設(shè)置它們的位置。這里,我們定義一個(gè)glm::vec3類(lèi)的數(shù)組來(lái)包含這些點(diǎn)光源的坐標(biāo):
glm::vec3 pointLightPositions[] = {
glm::vec3 (0.7f, 0.2f, 2.0f),
glm::vec3 (2.3f, -3.3f, -4.0f),
glm::vec3 (-4.0f, 2.0f, -12.0f),
glm::vec3 (0.0f, 0.0f, -3.0f)
};
同時(shí)我們還需要根據(jù)這些光源的位置在場(chǎng)景中繪制4個(gè)表示光源的立方體,這樣的工作我們?cè)谥暗慕坛讨幸呀?jīng)做過(guò)了。
如果你在還使用了手電筒的話,將所有的光源結(jié)合起來(lái)看上去應(yīng)該和下圖差不多:

你可以在此處獲取本教程的源代碼,同時(shí)可以查看頂點(diǎn)著色器和片段著色器的代碼。
以及我的項(xiàng)目文件
上面的圖片的光源都是使用默認(rèn)的屬性的效果,如果你嘗試對(duì)光源屬性做出各種修改嘗試的話,會(huì)出現(xiàn)很多有意思的畫(huà)面。很多藝術(shù)家和場(chǎng)景編輯器都提供大量的按鈕或方式來(lái)修改光照以使用各種環(huán)境。使用最簡(jiǎn)單的光照屬性的改變我們就足已創(chuàng)建有趣的視覺(jué)效果:

相信你現(xiàn)在已經(jīng)對(duì)OpenGL的光照有很好的理解了。有了這些知識(shí)我們便可以創(chuàng)建豐富有趣的環(huán)境和氛圍了??煸囋嚫淖兯械膶傩缘闹祦?lái)創(chuàng)建你的光照環(huán)境吧!
練習(xí)
- 創(chuàng)建一個(gè)表示手電筒光的結(jié)構(gòu)體Spotlight并實(shí)現(xiàn)CalcSpotLight(…)函數(shù):
vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// Diffuse shading
float diff = max(dot(normal, lightDir), 0.0);
// Specular shading
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// Attenuation
float distance = length(light.position - fragPos);
float attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
// Spotlight intensity
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
// Combine results
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
ambient *= attenuation * intensity;
diffuse *= attenuation * intensity;
specular *= attenuation * intensity;
return (ambient + diffuse + specular);
}
- 你能通過(guò)調(diào)節(jié)不同的光照屬性來(lái)重新創(chuàng)建一個(gè)不同的氛圍嗎?
desert場(chǎng)景

注意,在lamp.frag中添加uniform vec3 lampColor,來(lái)通過(guò)程序指定光源的顏色。
其他場(chǎng)景類(lèi)似。