說明:跟着learnopengl的內容學習,不是純翻譯,只是自己整理記錄。
強烈推薦原文,無論是內容還是排版。 原文鏈接
本文地址:http://blog.csdn.net/aganlengzi/article/details/50579032
同一場景中的多種光源 Multiple lights
在前面的教程中,我們學習了很多關於OpenGL中光照的只是——Phong光照模型,材質,光照映射和不同的光照效果。在本次的教程中,我們將會學到如何將前面學到的各種光源組合到一起——將六種光源放置在同一個場景中。它們分別是一個類似於太陽的平行光源,四個分散在場景中的點光源,還有一個聚光燈(上一個教程中講的聚光燈)。
爲了在場景中使用多個光源,我們能要先寫我們自己的GLSL功能函數。這樣做的原因不說自明:如果所有光照效果的計算函數都在main函數中來寫,會很亂,而且複雜難懂。
GLSL中的函數和C函數十分相似。函數名、返回值等,同樣地,在使用之前需要首先聲明。在下面的代碼中,我們將會爲每種光源創建一個實現其功能的函數。
在一個場景中使用不同的光源的方法大致可以描述如下:我們有一個顏色向量代表片段的輸出顏色。對每種光源,它對這個片段的貢獻值都會疊加在最後的顏色向量上。所以,每種光源都需要首先單獨計算出其顏色分量,然後再將它們整合產生最後的整體結果。下面的代碼展示了整體的流程:
out vec4 color;
void main()
{
// define an output color value
vec3 output;
// add the directional light's contribution to the output
output += someFunctionToCalculateDirectionalLight();
// Do the same for all point lights
for(int i = 0; i < nr_of_point_lights; i++)
output += someFunctionToCalculatePointLight();
// And add others lights as well (like spotlights)
output += someFunctionToCalculateSpotLight();
color = vec4(output, 1.0);
}
實際的代碼可能和上面的過程不太一致,但是本質上還是相同的。我們爲上面的每種光源定義了計算的函數並且將每種光源的計算結果疊加到最終輸出的顏色向量上。所以場景中的每個片段的最終顏色效果都是場景中所有光源共同作用產生的效果。
平行光 Directional light
我們想要做的是在片段處理程序中定義一個對給定片段計算平行光對其產生的作用效果的函數:它根據相關參數的輸入計算最終產生的平行光的作用效果的顏色向量。
首先,我們需要設定爲了完成平行光作用效果計算所需要的參數。我們把它們組織成如下所示的DirLight結構體中。並且聲明一個這個結構體類型的uniform類型的變量:
struct DirLight {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform DirLight dirLight;
然後我們可以將一個DirLight類型的變量當做一個函數的參數,像下面這種形式:
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
上面聲明的函數中,函數名是CalcDirLight返回一個vec3類型的向量,它需要傳遞三個參數,一個是DirLight類型的結構體變量,另外兩個是vec3類型的向量。下面是它的函數體實現:
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);
}
上面的代碼和前面教程中實現平行光效果的代碼一致,只是這裏我們將其封裝成了一個函數,具體的過程是:分別計算環境光、漫反射光和鏡面反射光,並將其疊加後返回。
點光源 Point light
對於點光源的實現過程和上面平行光源的實現過程相類似,我們首先也爲其定義一個計算點光源作用效果所需參數的結構體PoinLight,其中還包括了實現衰減效果所需要的參數。如下所示:
struct PointLight {
vec3 position;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
#define NR_POINT_LIGHTS 4
uniform PointLight pointLights[NR_POINT_LIGHTS];
如你所見,我們利用一條預編譯指令定義了場景中我們想要實現的點光源的數量。然後利用這個數值創建了一個PointLight數組,這和C中的代碼風格還是幾乎一致的。現在,我們有四個點光源數據了。
點光源的計算函數聲明如下:
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
而其具體的實現也是和之前的教程大致相同:
// Calculates the color when using a point light.
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(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));
// 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;
diffuse *= attenuation;
specular *= attenuation;
return (ambient + diffuse + specular);
}
這樣的抽象和以點光源數組形式的實現能夠做到充分的代碼複用和解耦。
組合起來 Putting it all together
上面我們已經定義了平行光和點光源,現在讓我們按照之前介紹的流程將它們組合起來吧:
void main()
{
// Properties
vec3 norm = normalize(Normal);
vec3 viewDir = normalize(viewPos - FragPos);
// Phase 1: Directional lighting
vec3 result = CalcDirLight(dirLight, norm, viewDir);
// Phase 2: Point lights
for(int i = 0; i < NR_POINT_LIGHTS; i++)
result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);
// Phase 3: Spot light
//result += CalcSpotLight(spotLight, norm, FragPos, viewDir);
color = vec4(result, 1.0);
}
正如前面所說,每種光源都對最終的效果有自己的貢獻值。場景中每個片段的最終效果是所有光源共同作用產生的效果。上面還說到可以添加一個聚光燈,就是上次教程中實現的手電筒,這個作者留作練習了。
接下來就是爲片段處理程序中我們定義的那些uniform類型的變量(一個DirLight類型的結構體和一個PointLight數組)賦值了,這是在OpenGL程序中完成的,就像之前使用的當時一樣,這次唯一的不同就是,我們現在要爲數組中的元素進行賦值。這該怎麼做?
幸運的是,這不是太繁。只需要在指定要被賦值的uniform名稱的時候按照具體的數組形式來指定就好了,像下面這樣:
glUniform1f(glGetUniformLocation(lightingShader.Program, "pointLights[0].constant"), 1.0f);
只不過這行代碼類似的代碼行很多,因爲我們要爲我們設定的每一個uniform光源變量的每個屬性都進行相應的賦值操作。但好像也只能這麼做。
另外,我們還需要指定這四個點光源的位置,不能讓它們在同一個位置,否則設置一個就夠了。所以我們定義了一個glm:vec3類型的數組。其中的每個元素代表一個點光源的位置:
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)
};
還有就是,我們需要在場景中繪出4個代表點光源的小立方體,這在模型矩陣中類似前面的做法就好了。
最終的效果是這樣的:
如你所見,場景中有類似於太陽的平行光光源,並且有四個點光源還有一個從觀察者照向場景的聚光燈(手電筒)。看上去還行吧?
源代碼在這兒,包括 main, shader.ver 和 shader.frag。
作者建議可以將代碼進行屬性值的修改以得到不同的效果。下面是一些效果:
到目前爲止,我們已經學習了光宇OpenGL光照的很多內容,並且對其有了很好的理解。具備了這些知識,我們已經可以創建出非常有趣的場景了。