本文主要解決一個問題:
如何在場景中實現多個光源?
引言
在之前的文章中,我們學了很多OpenGL中的光照知識,包括馮氏著色、材質、光照貼圖以及不同類型的光源模型等等。本文中,我們要把這些知識都組合起來,在場景中創造6個光源。我們要創造1個的方向光,4個點光源以及1個聚光燈(手電筒),然后看看整個場景會是什么樣子。
封裝光源操作
為了使用多個光源,我們將會把光照計算的操作封裝進GLSL函數中。如果你是一個新手,可能覺得這不是必要的操作。如果你有一些經驗,將代碼封裝成函數是一件自然而然的事情,這樣做不僅結構清晰,而且易于使用。
我們已經學了很多GLSL的語法,但是封裝函數還沒有學到。不過不用擔心,GLSL中的函數和C中的函數很相似,都需要一個函數名,一個返回值,在調用之前需要聲明等等。對于三種不同的光源模型,我們定義了3個不同的函數,分別是:CalcDirLight,CalcPointLight和CalcSpotLight。
想想在一個場景中,很多的光源照射到同一個物體上時,物體會呈現出什么樣子?多種光的效果會疊加起來,呈現出一種混合的狀態,我們試著來總結一個流程:
- 一個顏色向量表示片元的輸出顏色
- 計算每個光源對輸出顏色的影響,將所有的結果相加。
- 將所有結果的和傳遞給片元顏色作為最終結果
用偽代碼表示這個過程就是這樣:
void main(){
vec3 output = vec3(0,0);
output += 計算方向光的函數;
for (int i = 0; i < 點光源數量; ++i)
output += 計算點光源的函數;
output += 計算聚光燈的函數;
FragColor = vec4(output, 1.0);
}
在實現的過程,實際的代碼可能與這個不同,不必拘泥于這個代碼形式,思路是這樣就不會有問題。接下來,我們來定義一些計算不同光源對片元顏色產生影響的函數。
方向光
函數形式非常簡單,只需根據輸入的參數計算方向光對當前片元顏色的影響并返回結果就行了。不過首先,我們要來定義一個方向光源的結構體。
//方向光源
struct DirLight{
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform DirLight dirLight;
之后,將這個方向光源作為參數傳遞到計算光照的函數中:
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
可以看到,這個函數需要一個DirLight的對象,法線參數,以及觀察方向。如果你非常熟悉之前的代碼,那么實現這個函數對你來說就輕而易舉。
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir) {
vec3 lightDir = normalize(-light.direction);
//環境光
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);
}
基本上都是復制粘貼之前的代碼,然后將代碼整理一下的結果。
點光源
和方向光一樣,我們先要定義一個點光源的結構,然后創建4個點光源。不同的是,我們采用數組的方式來創建4個點光源。具體實現如下:
//點光源
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];
如你所見,定義數組的語法也和C類似,筆者覺得會C語言真是太幸運了。當然,我們可以把所有的數據放到一個光源結構中,這樣所有的光源都能使用同一個結構。但筆者更傾向于定義不同的結構,這樣更簡潔,擴展性更好,占用的空間也更少。
計算光照的原型如下:
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
實現的方式也和之前的代碼一樣,我們復制粘貼過來,然后做些修改:
//計算點光源的影響
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir){
//環境光
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;
}
依舊沒什么花頭,就是前面已經實現過的代碼。
聚光燈
這里我們可以偷個懶,因為前一篇文章中,我們最后實現的就是聚光燈,之前又是新增數據結構,沒有改之前的Light結構,這里我們只需要將原有的Light結構換個名字成SpotLight就直接獲得了一個聚光燈的結構。
然后,定義一個聚光燈的處理函數如下:
//計算聚光燈的影響
vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir){
//環境光
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的值,用內錐角的cos值減去外錐角的cos值
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0); //根據公式計算光照強度,并限制結果的范圍
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,觀察方向也不需要計算,直接可以使用了,省了不少事。
整合
根據之前分析的結構,將代碼補充完整:
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);
}
在主函數中設置結構體中的元素你已經很熟悉了,那么怎么設置數組中的元素呢?答案很簡單,還是C語言的語法,請看下面的代碼:
lightingShader.setFloat("pointLights[0].constant", 1.0f);
沒錯,像C語言訪問數組那樣訪問一個元素,然后設置其值。
別忘了還有點光源的位置我們沒設置,快來看看我們把這些點光源放在哪里:
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)));
為光源賦值之后,我們還要把其余的光源也創建出來,代碼也很簡單,和我們之前創建多個盒子的時候沒什么兩樣。
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之后再繪制光源立方體。
編譯運行,如果沒錯,你看到的場景應該是這樣的:
仔細觀察,還有手電筒的效果哦!如果效果不一樣,請下載源碼進行比對。
總結
本文并沒有介紹什么新的知識,只是將已有知識進行整合使用,實現一些效果,你也可以在本文的基礎上改變光源的顏色值,看看場景會變成什么稀奇古怪的樣子,這是一個很有趣的過程。
參考資料
www.learnopengl.com(非常好的網站,推薦學習)