從0開始的OpenGL學習(十五)-多光源

本文主要解決一個問題:

如何在場景中實現(xiàn)多個光源?

引言

在之前的文章中,我們學了很多OpenGL中的光照知識,包括馮氏著色、材質(zhì)、光照貼圖以及不同類型的光源模型等等。本文中,我們要把這些知識都組合起來,在場景中創(chuàng)造6個光源。我們要創(chuàng)造1個的方向光,4個點光源以及1個聚光燈(手電筒),然后看看整個場景會是什么樣子。

封裝光源操作

為了使用多個光源,我們將會把光照計算的操作封裝進GLSL函數(shù)中。如果你是一個新手,可能覺得這不是必要的操作。如果你有一些經(jīng)驗,將代碼封裝成函數(shù)是一件自然而然的事情,這樣做不僅結(jié)構(gòu)清晰,而且易于使用。

我們已經(jīng)學了很多GLSL的語法,但是封裝函數(shù)還沒有學到。不過不用擔心,GLSL中的函數(shù)和C中的函數(shù)很相似,都需要一個函數(shù)名,一個返回值,在調(diào)用之前需要聲明等等。對于三種不同的光源模型,我們定義了3個不同的函數(shù),分別是:CalcDirLight,CalcPointLight和CalcSpotLight。

想想在一個場景中,很多的光源照射到同一個物體上時,物體會呈現(xiàn)出什么樣子?多種光的效果會疊加起來,呈現(xiàn)出一種混合的狀態(tài),我們試著來總結(jié)一個流程:

  1. 一個顏色向量表示片元的輸出顏色
  2. 計算每個光源對輸出顏色的影響,將所有的結(jié)果相加。
  3. 將所有結(jié)果的和傳遞給片元顏色作為最終結(jié)果

用偽代碼表示這個過程就是這樣:

void main(){
  vec3 output = vec3(0,0);
  output += 計算方向光的函數(shù);
  for (int i = 0; i < 點光源數(shù)量; ++i)
    output += 計算點光源的函數(shù);
  output += 計算聚光燈的函數(shù);
  FragColor = vec4(output, 1.0);
}

在實現(xiàn)的過程,實際的代碼可能與這個不同,不必拘泥于這個代碼形式,思路是這樣就不會有問題。接下來,我們來定義一些計算不同光源對片元顏色產(chǎn)生影響的函數(shù)。

方向光

函數(shù)形式非常簡單,只需根據(jù)輸入的參數(shù)計算方向光對當前片元顏色的影響并返回結(jié)果就行了。不過首先,我們要來定義一個方向光源的結(jié)構(gòu)體。

//方向光源
struct DirLight{
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
uniform DirLight dirLight;

之后,將這個方向光源作為參數(shù)傳遞到計算光照的函數(shù)中:

vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);

可以看到,這個函數(shù)需要一個DirLight的對象,法線參數(shù),以及觀察方向。如果你非常熟悉之前的代碼,那么實現(xiàn)這個函數(shù)對你來說就輕而易舉。

vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir) {
    vec3 lightDir = normalize(-light.direction);
    //環(huán)境光
    vec3 ambient = light.ambient * vec3 (texture(material.diffuse, TexCoords));

    //漫反射
    float diff = max(dot(normal, lightDir), 0.0);
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));

    //鏡面高光
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    vec3 specular = light.specular * spec * vec3 (texture(material.specular, TexCoords));

    return (ambient + diffuse + specular);
}

基本上都是復制粘貼之前的代碼,然后將代碼整理一下的結(jié)果。

點光源

和方向光一樣,我們先要定義一個點光源的結(jié)構(gòu),然后創(chuàng)建4個點光源。不同的是,我們采用數(shù)組的方式來創(chuàng)建4個點光源。具體實現(xiàn)如下:

//點光源
struct PointLight{
    vec3 position;

    float constant;
    float linear;
    float quadratic;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
#define NR_POINT_LIGHTS 4
uniform PointLight pointLight[NR_POINT_LIGHTS];

如你所見,定義數(shù)組的語法也和C類似,筆者覺得會C語言真是太幸運了。當然,我們可以把所有的數(shù)據(jù)放到一個光源結(jié)構(gòu)中,這樣所有的光源都能使用同一個結(jié)構(gòu)。但筆者更傾向于定義不同的結(jié)構(gòu),這樣更簡潔,擴展性更好,占用的空間也更少。

計算光照的原型如下:

vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir); 

實現(xiàn)的方式也和之前的代碼一樣,我們復制粘貼過來,然后做些修改:

//計算點光源的影響
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir){
    //環(huán)境光
    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));

    //漫反射光
    vec3 norm = normalize(normal);
    vec3 lightDir = normalize(light.position - FragPos);  
        
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));

    //鏡面高光
    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));

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

    return ambient + diffuse + specular;
}

依舊沒什么花頭,就是前面已經(jīng)實現(xiàn)過的代碼。

聚光燈

這里我們可以偷個懶,因為前一篇文章中,我們最后實現(xiàn)的就是聚光燈,之前又是新增數(shù)據(jù)結(jié)構(gòu),沒有改之前的Light結(jié)構(gòu),這里我們只需要將原有的Light結(jié)構(gòu)換個名字成SpotLight就直接獲得了一個聚光燈的結(jié)構(gòu)。

然后,定義一個聚光燈的處理函數(shù)如下:

//計算聚光燈的影響
vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir){
   //環(huán)境光
    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));

    //漫反射光
    vec3 norm = normalize(normal);
    vec3 lightDir = normalize(light.position - fragPos);  
        
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));

    //鏡面高光
    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));

    //聚光燈
    float theta = dot(lightDir, normalize(-light.direction));   //計算片元角度的cos值
    float epsilon = light.cutOff - light.outerCutOff;   //計算epsilon的值,用內(nèi)錐角的cos值減去外錐角的cos值
    float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);   //根據(jù)公式計算光照強度,并限制結(jié)果的范圍

    diffuse *= intensity;
    specular *= intensity;

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

    return ambient + diffuse + specular;
}

修改了一些變量名,比如normal,觀察方向也不需要計算,直接可以使用了,省了不少事。

整合

根據(jù)之前分析的結(jié)構(gòu),將代碼補充完整:

void main()
{
    vec3 norm = normalize(Normal);
    vec3 viewDir = normalize(viewPos - FragPos);

    //方向光
    vec3 result = CalcDirLight(dirLight, norm, viewDir);

    //點光源
    for(int i = 0; i < NR_POINT_LIGHTS; ++i)
        result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);

    //聚光燈
    result += CalcSpotLight(spotLight, norm, FragPos, viewDir);
 
    FragColor = vec4(result, 1.0f);
}

在主函數(shù)中設(shè)置結(jié)構(gòu)體中的元素你已經(jīng)很熟悉了,那么怎么設(shè)置數(shù)組中的元素呢?答案很簡單,還是C語言的語法,請看下面的代碼:

lightingShader.setFloat("pointLights[0].constant", 1.0f);

沒錯,像C語言訪問數(shù)組那樣訪問一個元素,然后設(shè)置其值。

別忘了還有點光源的位置我們沒設(shè)置,快來看看我們把這些點光源放在哪里:

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)
}; 

接下來,我們就要為著色器中的這些元素賦值了。你可能會想,這里面這么多元素,難道要一個一個去賦值嗎,有沒有更好的方法?不過很遺憾,目前來說我們還沒有簡單的賦值方法,只能手動賦值,為了避免手寫的枯燥,筆者在這里把賦值的代碼貼出來:

/*
為光源賦值
*/
// 方向光
lightingShader.setVec3("dirLight.direction", -0.2f, -1.0f, -0.3f);
lightingShader.setVec3("dirLight.ambient", 0.05f, 0.05f, 0.05f);
lightingShader.setVec3("dirLight.diffuse", 0.4f, 0.4f, 0.4f);
lightingShader.setVec3("dirLight.specular", 0.5f, 0.5f, 0.5f);
// 點光源1
lightingShader.setVec3("pointLights[0].position", pointLightPositions[0]);
lightingShader.setVec3("pointLights[0].ambient", 0.05f, 0.05f, 0.05f);
lightingShader.setVec3("pointLights[0].diffuse", 0.8f, 0.8f, 0.8f);
lightingShader.setVec3("pointLights[0].specular", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("pointLights[0].constant", 1.0f);
lightingShader.setFloat("pointLights[0].linear", 0.09);
lightingShader.setFloat("pointLights[0].quadratic", 0.032);
// 點光源2
lightingShader.setVec3("pointLights[1].position", pointLightPositions[1]);
lightingShader.setVec3("pointLights[1].ambient", 0.05f, 0.05f, 0.05f);
lightingShader.setVec3("pointLights[1].diffuse", 0.8f, 0.8f, 0.8f);
lightingShader.setVec3("pointLights[1].specular", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("pointLights[1].constant", 1.0f);
lightingShader.setFloat("pointLights[1].linear", 0.09);
lightingShader.setFloat("pointLights[1].quadratic", 0.032);
// 點光源3
lightingShader.setVec3("pointLights[2].position", pointLightPositions[2]);
lightingShader.setVec3("pointLights[2].ambient", 0.05f, 0.05f, 0.05f);
lightingShader.setVec3("pointLights[2].diffuse", 0.8f, 0.8f, 0.8f);
lightingShader.setVec3("pointLights[2].specular", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("pointLights[2].constant", 1.0f);
lightingShader.setFloat("pointLights[2].linear", 0.09);
lightingShader.setFloat("pointLights[2].quadratic", 0.032);
// 點光源4
lightingShader.setVec3("pointLights[3].position", pointLightPositions[3]);
lightingShader.setVec3("pointLights[3].ambient", 0.05f, 0.05f, 0.05f);
lightingShader.setVec3("pointLights[3].diffuse", 0.8f, 0.8f, 0.8f);
lightingShader.setVec3("pointLights[3].specular", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("pointLights[3].constant", 1.0f);
lightingShader.setFloat("pointLights[3].linear", 0.09);
lightingShader.setFloat("pointLights[3].quadratic", 0.032);
// 聚光燈
lightingShader.setVec3("spotLight.position", camera.Position);
lightingShader.setVec3("spotLight.direction", camera.Front);
lightingShader.setVec3("spotLight.ambient", 0.0f, 0.0f, 0.0f);
lightingShader.setVec3("spotLight.diffuse", 1.0f, 1.0f, 1.0f);
lightingShader.setVec3("spotLight.specular", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("spotLight.constant", 1.0f);    lightingShader.setFloat("spotLight.linear", 0.09);
lightingShader.setFloat("spotLight.quadratic", 0.032);
lightingShader.setFloat("spotLight.cutOff", glm::cos(glm::radians(12.5f)));
lightingShader.setFloat("spotLight.outerCutOff", glm::cos(glm::radians(15.0f)));

為光源賦值之后,我們還要把其余的光源也創(chuàng)建出來,代碼也很簡單,和我們之前創(chuàng)建多個盒子的時候沒什么兩樣。

glBindVertexArray(lightVAO);
for (unsigned int i = 0; i < 4; ++i) {
    glm::mat4 model2;
    model2 = glm::translate(model2, pointLightPositions[i]);
    model2 = glm::scale(model2, glm::vec3(0.2f));
    lampShader.setMat4("model", glm::value_ptr(model2));
    glDrawArrays(GL_TRIANGLES, 0, 36);
}

這里只有一點要注意,就是要使用光源VAO之后再繪制光源立方體。

編譯運行,如果沒錯,你看到的場景應(yīng)該是這樣的:

運行效果

仔細觀察,還有手電筒的效果哦!如果效果不一樣,請下載源碼進行比對。

總結(jié)

本文并沒有介紹什么新的知識,只是將已有知識進行整合使用,實現(xiàn)一些效果,你也可以在本文的基礎(chǔ)上改變光源的顏色值,看看場景會變成什么稀奇古怪的樣子,這是一個很有趣的過程。

下一篇
目錄
上一篇

參考資料

www.learnopengl.com(非常好的網(wǎng)站,推薦學習)

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

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

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