光照-06.多光源(Multiple lights)

我們?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)似。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 現(xiàn)實(shí)世界的光照是極其復(fù)雜的,而且會(huì)受到諸多因素的影響,這是以目前我們所擁有的處理能力無(wú)法模擬的。因此OpenGL的...
    IceMJ閱讀 2,130評(píng)論 1 6
  • 麻木地活著 生無(wú)可戀? 沒(méi)有痛苦矛盾的生活 或許只存在于夢(mèng)中 而夢(mèng)也并非每個(gè)都如此美好 如果我自己也不信我能做好 ...
    角落蜷縮閱讀 229評(píng)論 0 0
  • 今天看到這則新聞 有點(diǎn)心疼 每年的中秋 都是家人團(tuán)聚的時(shí)光 家中的父母更是盼著兒女回來(lái)共聚一堂 可是老人在個(gè)把月前...
    馬田心Martinc手作閱讀 252評(píng)論 2 1
  • 或許我們?cè)鵁o(wú)數(shù)次幻想過(guò)自己以后的生活 從我們記事開(kāi)始 我們就在不停的幻想 小學(xué)的時(shí)候我們幻想沒(méi)有留堂作業(yè)還有一群好...
    JoJo777閱讀 215評(píng)論 0 0
  • 到讀圖035,2017年這幾個(gè)月拍的照片,就選完了。然后從2016年選。 生活家們喜歡在老街開(kāi)店,街道慢慢變成特色...
    漢口張叔叔閱讀 208評(píng)論 0 0

友情鏈接更多精彩內(nèi)容