本文是個人學習記錄,學習建議看教程 https://learnopengl-cn.github.io/
非常感謝原作者JoeyDeVries和多為中文翻譯者提供的優質教程
的內容為插入注釋,可以先跳過
投光物(光源)
我們目前使用的光照都來自於空間中的一個點,它能給我們不錯的效果,但現實世界中,我們有很多種類的光照,每種的表現都不同
將光投射(Cast)到物體的光源叫做投光物(Light Caster),我們將會討論幾種不同類型的投光物,學會模擬不同種類的光源是又一個能夠進一步豐富場景的工具
我們首先將會討論平行光(Directional Light),接下來是點光源(Point Light),它是我們之前學習的光源的拓展,最后我們將會討論聚光(Spotlight)
之后我們將討論如何將這些不同種類的光照類型整合到一個多光源場景之中
平行光
當一個光源處於很遠的地方時,來自光源的每條光線就會近似於互相平行,不論物體和/或者觀察者的位置,看起來好像所有的光都來自於同一個方向
當我們使用一個假設光源處於無限遠處的模型時,它就被稱為平行光,因為它的所有光線都有着相同的方向,它與光源的位置是沒有關系的。
比如,太陽距離我們並不是無限遠,但它已經遠到在光照計算中可以把它視為無限遠了,所以來自太陽的所有光線將被模擬為平行光線

因為所有的光線都是平行的,所以物體與光源的相對位置是不重要的,因為對場景中每一個物體光的方向都是一致的。由於光的位置向量保持一致,場景中每個物體的光照計算將會是類似的。
我們可以定義一個光線方向向量而不是位置向量來模擬一個平行光。着色器的計算基本保持不變,但這次我們將直接使用光的direction向量而不是通過direction來計算lightDir向量。
//片段着色器
struct Light {
// vec3 position; // 使用平行光就不再需要了
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
//main里
vec3 lightDir = normalize(-light.direction);
注意我們首先對light.direction向量取反。我們目前使用的光照計算需求一個從片段至光源的光線方向,但人們更習慣定義平行光為一個從光源出發的全局方向,所以我們需要對全局光照方向向量取反來改變它的方向,它現在是一個指向光源的方向向量了(記得對向量進行標准化)
最終的lightDir向量將和以前一樣用在漫反射和鏡面光計算中
為了清楚地展示平行光對多個物體具有相同的影響,我們將會再次使用坐標系統博客里最后的那個箱子派對的場景,我們先定義了十個不同的箱子位置
glm::vec3 cubePositions[] = {
glm::vec3( 0.0f, 0.0f, 0.0f),
glm::vec3( 2.0f, 5.0f, -15.0f),
glm::vec3(-1.5f, -2.2f, -2.5f),
glm::vec3(-3.8f, -2.0f, -12.3f),
glm::vec3( 2.4f, -0.4f, -3.5f),
glm::vec3(-1.7f, 3.0f, -7.5f),
glm::vec3( 1.3f, -2.0f, -2.5f),
glm::vec3( 1.5f, 2.0f, -2.5f),
glm::vec3( 1.5f, 0.2f, -1.5f),
glm::vec3(-1.3f, 1.0f, -1.5f)
};
並對每個箱子都生成了一個不同的模型矩陣,每個模型矩陣都包含了對應的局部-世界坐標變換:
for(unsigned int i = 0; i < 10; i++)
{
glm::mat4 model;
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * i;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
lightingShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
同時,不要忘記定義光源的方向(注意我們將方向定義為從光源出發的方向,你可以很容易看到光的方向朝下)。
lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);
我們一直將光的位置和位置向量定義為vec3,一些人會喜歡將所有的向量都定義為vec4,當我們將位置向量定義為一個vec4時,很重要的一點是要將w分量設置為1.0,這樣變換和投影才能正確應用,然而,當我們定義一個方向向量為vec4的時候,我們不想讓位移有任何的效果(因為它僅僅代表的是方向),所以我們將w分量設置為0.0
方向向量就會像這樣來表示:vec4(0.2f, 1.0f, 0.3f, 0.0f)
這也可以作為一個快速檢測光照類型的工具:你可以檢測w分量是否等於1.0,來檢測它是否是光的位置向量;w分量等於0.0,則它是光的方向向量,這樣就能根據這個來調整光照計算了:
if(lightVector.w == 0.0) // 注意浮點數據類型的誤差
// 執行平行光照計算
else if(lightVector.w == 1.0)
// 根據光源的位置做光照計算(與上一節一樣)
這正是舊OpenGL(固定函數式)決定光源是平行光還是位置光源(Positional Light Source)的方法,並根據它來調整光照
如果你現在編譯程序,在場景中自由移動,你就可以看到好像有一個太陽一樣的光源對所有的物體投光

部分源碼如下
//平行光片段着色器
#version 330 core
struct Material {
sampler2D diffuse;
sampler2D specular;
float shininess;
};
struct Light {
// vec3 position; // 使用平行光就不再需要了
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
in vec2 TexCoords;
in vec3 Normal;
in vec3 FragPos;
out vec4 FragColor;
uniform Material material;
uniform Light light;
uniform vec3 objectColor;
uniform vec3 lightColor;
uniform vec3 lightPos;
uniform vec3 viewPos;
float specularStrength = 0.5;
void main()
{
// 環境光
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
// 漫反射
vec3 norm = normalize(Normal);
//vec3 lightDir = normalize(lightPos - FragPos);
vec3 lightDir = normalize(-light.direction);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
// 鏡面光
vec3 viewDir = normalize(viewPos - FragPos);
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));
vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}
//平行光渲染循環
while (!glfwWindowShouldClose(window))
{
// 幀時間差
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 輸入
processInput(window);
// 渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
lightingShader.use();
lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);
lightingShader.setVec3("viewPos", camera.Position);
// 光照屬性
lightingShader.setVec3("light.ambient", 0.2f, 0.2f, 0.2f);
lightingShader.setVec3("light.diffuse", 0.7f, 0.7f, 0.7f);
lightingShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f);
// 材質屬性
lightingShader.setFloat("material.shininess", 32.0f);
// 初始化view transformations
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
lightingShader.setMat4("projection", projection);
lightingShader.setMat4("view", view);
// 初始化world transformations
glm::mat4 model = glm::mat4();
lightingShader.setMat4("model", model);
// 綁定漫反射貼圖
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);
// 綁定鏡面貼圖
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, specularMap);
// 渲染方塊
//glBindVertexArray(cubeVAO);
//glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(cubeVAO);
for (unsigned int i = 0; i < 10; i++)
{
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * i;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
lightingShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
// 現在有了平行光,不需要光源立方體
//lampShader.use();
//// 設置模型、視圖和投影矩陣uniform
//lampShader.setMat4("projection", projection);
//lampShader.setMat4("view", view);
//model = glm::mat4(1.0f);
//model = glm::translate(model, lightPos);
//model = glm::scale(model, glm::vec3(0.2f)); // 小點的立方體
//lampShader.setMat4("model", model);
//// 繪制燈立方體對象
//glBindVertexArray(lightVAO);
//glDrawArrays(GL_TRIANGLES, 0, 36);
// 交換緩沖並查詢IO事件
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &cubeVAO);
glDeleteVertexArrays(1, &lightVAO);
glDeleteBuffers(1, &VBO);
glfwTerminate();
return 0;
}
點光源
除了平行光之外我們也需要一些分散在場景中的點光源(Point Light),點光源是處於世界中某一個位置的光源,它會朝着所有方向發光,但光線會隨着距離逐漸衰減,比如作為光源的燈泡和火把,它們都是點光源

在之前的教程中,我們一直都在使用一個(簡化的)點光源,我們在給定位置有一個光源,它會從它的光源位置開始朝着所有方向散射光線,但我們之前定義的光源模擬的是永遠不會衰減的光線
如果你將10個箱子加入到上一節光照場景中,你會注意到在最后面的箱子和在燈面前的箱子都以相同的強度被照亮,並沒有定義一個公式來將光隨距離衰減,我們希望點光源強度有適當的隨着距離增加而衰減,才能更好的模擬真實環境
衰減
隨着光線傳播距離的增長逐漸削減光的強度通常叫做衰減(Attenuation),其中一種方式是使用一個線性方程,這樣的方程能夠隨着距離的增長線性地減少光的強度
然而,這樣的線性方程通常會看起來比較假,在現實世界中,燈在近處通常會非常亮,但隨着距離的增加光源的亮度一開始會下降非常快,但在遠處時剩余的光強度就會下降的非常緩慢了,所以,我們需要一個不同的公式來減少光的強度
幸運的是一些聰明的人已經幫我們解決了這個問題,下面這個公式根據片段距光源的距離計算了衰減值,之后我們會將它乘以光的強度向量:

在這里d代表了片段距光源的距離,接下來為了計算衰減值,我們定義3個可配置項:
常數項Kc、一次項Kl和二次項Kq
- 常數項Kc通常保持為1.0,它的主要作用是保證分母永遠不會比1小,否則的話在某些距離上它反而會增加強度,這肯定不是我們想要的效果
- 一次項Kl會與距離值相乘,以線性的方式減少強度
- 二次項Kq會與距離的平方相乘,讓光源以二次遞減的方式減少強度,二次項在距離比較小的時候影響會比一次項小很多,但當距離值比較大的時候它就會比一次項更大了
由於二次項的存在,光線會在大部分時候以線性的方式衰退,直到距離變得足夠大,讓二次項超過一次項,光的強度會以更慢的速度下降
下面這張圖顯示了在100的距離內衰減的效果:

這正是我們想要的
但是,該對這三個項設置什么值呢?正確地設定它們的值取決於很多因素:環境、希望光覆蓋的距離、光的類型等,在大多數情況下,這都是經驗的問題
下面這個表格顯示了模擬一個接近真實的,覆蓋特定半徑(距離)的光源時,這些項可能取的一些值
第一列指定的是在給定的三項時光所能覆蓋的距離,這些值是大多數光源很好的起始點,它們由Ogre3D的Wiki所提供:
| 距離 | 常數項 | 一次項 | 二次項 |
|---|---|---|---|
| 7 | 1.0 | 0.7 | 1.8 |
| 13 | 1.0 | 0.35 | 0.44 |
| 20 | 1.0 | 0.22 | 0.20 |
| 32 | 1.0 | 0.14 | 0.07 |
| 50 | 1.0 | 0.09 | 0.032 |
| 65 | 1.0 | 0.07 | 0.017 |
| 100 | 1.0 | 0.045 | 0.0075 |
| 160 | 1.0 | 0.027 | 0.0028 |
| 200 | 1.0 | 0.022 | 0.0019 |
| 325 | 1.0 | 0.014 | 0.0007 |
| 600 | 1.0 | 0.007 | 0.0002 |
| 3250 | 1.0 | 0.0014 | 0.000007 |
你可以看到,常數項Kc在所有的情況下都是1.0,一次項Kl為了覆蓋更遠的距離通常都很小,二次項Kq甚至更小。嘗試對這些值進行實驗,看看它們在你的實現中有什么效果
在我們的環境中,32到100的距離對大多數的光源都足夠了
代碼實現
為了實現衰減,在片段着色器中我們還需要三個額外的值:也就是公式中的常數項、一次項和二次項
它們最好儲存在之前定義的Light結構體中,注意我們使用上一篇博客中計算lightDir的方法,而不是上面平行光部分的
vec3 lightDir = normalize(lightPos - FragPos);
//片段着色器
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
然后我們將在OpenGL中設置這些項:我們希望光源能夠覆蓋50的距離,所以我們會使用表格中對應的常數項、一次項和二次項:
//main.cpp
lightingShader.setFloat("light.constant", 1.0f);
lightingShader.setFloat("light.linear", 0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);
在片段着色器中實現衰減還是比較直接的:我們根據公式計算衰減值,之后再分別乘以環境光、漫反射和鏡面光分量
我們仍需要公式中距光源的距離,還記得我們是怎么計算一個向量的長度的嗎?我們可以通過獲取片段和光源之間的向量差,並獲取結果向量的長度作為距離項
我們可以使用GLSL內建的length函數來完成這一點:
//片段着色器
//衰減
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;
運行程序

部分源碼
//點光源片段着色器
#version 330 core
struct Material {
sampler2D diffuse;
sampler2D specular;
float shininess;
};
struct Light {
vec3 position;
//vec3 direction; 點光源就不需要了
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant; //常數項Kc
float linear; //一次項Kl
float quadratic; //二次項Kq
};
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;
out vec4 FragColor;
uniform Material material;
uniform Light light;
//uniform vec3 objectColor;
//uniform vec3 lightColor;
//uniform vec3 lightPos;
uniform vec3 viewPos;
float specularStrength = 0.5;
void main()
{
// 環境光
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
// 漫反射
vec3 norm = normalize(Normal);
//vec3 lightDir = normalize(lightPos - FragPos);
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 viewDir = normalize(viewPos - FragPos);
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;
vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}
//點光源渲染循環
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
while (!glfwWindowShouldClose(window))
{
// 幀時間差
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 輸入
processInput(window);
// 渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
lightingShader.use();
//lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f); //平行光
lightingShader.setVec3("light.position", lightPos);
lightingShader.setVec3("viewPos", camera.Position);
// 光照屬性
lightingShader.setVec3("light.ambient", 0.2f, 0.2f, 0.2f);
lightingShader.setVec3("light.diffuse", 0.7f, 0.7f, 0.7f);
lightingShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("light.constant", 1.0f);
lightingShader.setFloat("light.linear", 0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);
// 材質屬性
lightingShader.setFloat("material.shininess", 32.0f);
// 初始化view transformations
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
lightingShader.setMat4("projection", projection);
lightingShader.setMat4("view", view);
// 初始化world transformations
glm::mat4 model = glm::mat4();
lightingShader.setMat4("model", model);
// 綁定漫反射貼圖
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);
// 綁定鏡面貼圖
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, specularMap);
// 渲染方塊
//glBindVertexArray(cubeVAO);
//glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(cubeVAO);
for (unsigned int i = 0; i < 10; i++)
{
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * i;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
lightingShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
// 渲染點光源立方體
lampShader.use();
// 設置模型、視圖和投影矩陣uniform
lampShader.setMat4("projection", projection);
lampShader.setMat4("view", view);
model = glm::mat4(1.0f);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f)); // 小點的立方體
lampShader.setMat4("model", model);
// 繪制光源立方體對象
glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
// 交換緩沖並查詢IO事件
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &cubeVAO);
glDeleteVertexArrays(1, &lightVAO);
glDeleteBuffers(1, &VBO);
glfwTerminate();
return 0;
}
聚光
我們要討論的最后一種類型的光是聚光(Spotlight)。聚光是位於環境中某個位置的光源,它只朝一個特定方向而不是所有方向照射光線。這樣的結果就是只有在聚光方向的特定半徑內的物體才會被照亮,其它的物體都會保持黑暗,比如路燈和手電筒
OpenGL中聚光是用一個世界空間位置、一個方向和一個切光角(Cutoff Angle)來表示的,切光角指定了聚光的半徑(是圓錐的半徑),對於每個片段,我們會計算片段是否位於聚光的切光方向之間(也就是在錐形內),如果是的話,我們就會相應地照亮片段

LightDir:從片段指向光源的向量SpotDir:聚光所指向的方向Phiϕ:指定了聚光半徑的切光角,落在這個角度之外的物體都不會被這個聚光所照亮。Thetaθ:LightDir向量和SpotDir向量之間的夾角,在聚光內部的話θ值應該比ϕ值小
所以我們要做的就是計算LightDir向量和SpotDir向量之間的點積(返回兩個單位向量夾角的余弦值),並將它與切光角ϕ值對比
你現在應該了解聚光究竟是什么了,下面我們將以手電筒的形式創建一個聚光
手電筒
手電筒(Flashlight)是一個位於觀察者位置的聚光,通常它都會瞄准玩家視角的正前方
手電筒就是普通的聚光,但它的位置和方向會隨着玩家的位置和朝向不斷更新
所以,在片段着色器中我們需要的值有聚光的位置向量(來計算光的方向向量)、聚光的方向向量和一個切光角。我們可以將它們儲存在Light結構體中:
//片段着色器
struct Light {
vec3 position;
vec3 direction;
float cutOff;
...
};
接下來我們將合適的值傳到着色器中:
lightingShader.setVec3("light.position", camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
你可以看到,我們並沒有給切光角設置一個角度值,反而是用角度值計算了一個余弦值,將余弦結果傳遞到片段着色器中
這樣做的原因是:在片段着色器中,我們會計算LightDir和SpotDir向量的點積,這個點積返回的將是一個余弦值而不是角度值,所以我們不能直接使用角度值和余弦值進行比較,為了獲取角度值我們需要計算點積結果的反余弦,這是一個開銷很大的計算
所以為了節約一點性能開銷,我們將會計算切光角對應的余弦值,並將它的結果傳入片段着色器中,由於這兩個角度現在都由余弦角來表示了,我們可以直接對它們進行比較而不用進行任何開銷高昂的計算
接下來就是計算θ值,並將它和切光角ϕ對比,來決定是否在聚光的內部:
float theta = dot(lightDir, normalize(-light.direction));
if(theta > light.cutOff)
{
// 執行光照計算
}
else // 否則,使用環境光,讓場景在聚光之外時不至於完全黑暗
color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);
我們首先計算了lightDir和取反的direction向量(取反的是因為我們想讓向量指向光源而不是從光源出發)之間的點積,記住要對所有的相關向量標准化
你可能奇怪為什么在if條件中使用的是 > 符號而不是 < 符號,theta不應該比光的切光角更小才是在聚光內部嗎?這並沒有錯,但別忘了角度值現在都由余弦值來表示的

好了,運行程序

部分源碼
//聚光片段着色器
#version 330 core
struct Material {
sampler2D diffuse;
sampler2D specular;
float shininess;
};
struct Light {
vec3 position;
vec3 direction;
float cutOff;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant; //常數項Kc
float linear; //一次項Kl
float quadratic; //二次項Kq
};
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;
out vec4 FragColor;
uniform Material material;
uniform Light light;
//uniform vec3 objectColor;
//uniform vec3 lightColor;
//uniform vec3 lightPos;
uniform vec3 viewPos;
float specularStrength = 0.5;
void main()
{
vec3 lightDir = normalize(light.position - FragPos);
// 檢測是否在范圍內
float theta = dot(lightDir, normalize(-light.direction));
if(theta > light.cutOff) // 執行光照計算
{
// 環境光
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
// 漫反射
vec3 norm = normalize(Normal);
//vec3 lightDir = normalize(lightPos - FragPos);
//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 viewDir = normalize(viewPos - FragPos);
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;
vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}
else // 否則,使用環境光,讓場景在聚光之外時不至於完全黑暗
{
FragColor = vec4(light.ambient * texture(material.diffuse, TexCoords).rgb, 1.0);
}
}
//聚光渲染循環
while (!glfwWindowShouldClose(window))
{
// 幀時間差
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 輸入
processInput(window);
// 渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
lightingShader.use();
//lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f); //平行光
lightingShader.setVec3("light.position", camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
lightingShader.setVec3("viewPos", camera.Position);
// 光照屬性
lightingShader.setVec3("light.ambient", 0.2f, 0.2f, 0.2f);
lightingShader.setVec3("light.diffuse", 0.7f, 0.7f, 0.7f);
lightingShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("light.constant", 1.0f);
lightingShader.setFloat("light.linear", 0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);
// 材質屬性
lightingShader.setFloat("material.shininess", 32.0f);
// 初始化view transformations
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
lightingShader.setMat4("projection", projection);
lightingShader.setMat4("view", view);
// 初始化world transformations
glm::mat4 model = glm::mat4();
lightingShader.setMat4("model", model);
// 綁定漫反射貼圖
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);
// 綁定鏡面貼圖
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, specularMap);
// 渲染方塊
//glBindVertexArray(cubeVAO);
//glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(cubeVAO);
for (unsigned int i = 0; i < 10; i++)
{
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * i;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
lightingShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
//聚光,也不需要光源了
//// 渲染點光源立方體
//lampShader.use();
//// 設置模型、視圖和投影矩陣uniform
//lampShader.setMat4("projection", projection);
//lampShader.setMat4("view", view);
//model = glm::mat4(1.0f);
//model = glm::translate(model, lightPos);
//model = glm::scale(model, glm::vec3(0.2f)); // 小點的立方體
//lampShader.setMat4("model", model);
//// 繪制光源立方體對象
//glBindVertexArray(lightVAO);
//glDrawArrays(GL_TRIANGLES, 0, 36);
// 交換緩沖並查詢IO事件
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &cubeVAO);
glDeleteVertexArrays(1, &lightVAO);
glDeleteBuffers(1, &VBO);
glfwTerminate();
return 0;
}
平滑聚光邊緣
我們的手電筒的光略顯生硬,現在我們為了創建一種看起來邊緣平滑的聚光,我們需要模擬聚光有一個內圓錐(Inner Cone)和一個外圓錐(Outer Cone),我們可以將內圓錐設置為上一部分中的那個圓錐,但我們也需要一個外圓錐,來讓光從內圓錐逐漸減暗,直到外圓錐的邊界
為了創建一個外圓錐,我們只需要再定義一個余弦值來代表聚光方向向量和外圓錐向量(等於它的半徑)的夾角,然后,如果一個片段處於內外圓錐之間,將會給它計算出一個0.0到1.0之間的強度值,如果片段在內圓錐之內,它的強度就是1.0,如果在外圓錐之外強度值就是0.0
我們可以用下面這個公式來計算這個值:

這里ϵ是內(ϕ)和外圓錐(γ)之間的余弦值差(ϵ=ϕ−γ),最終的I值就是在當前片段聚光的強度。
很難去表現這個公式是怎么工作的,所以我們用一些實例值來看看:
θ |
θ(角度) |
ϕ(內光切) |
ϕ(角度) |
γ(外光切) |
γ(角度) |
ϵϵ | II |
|---|---|---|---|---|---|---|---|
| 0.87 | 30 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 | 0.87 - 0.82 / 0.09 = 0.56 |
| 0.9 | 26 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 | 0.9 - 0.82 / 0.09 = 0.89 |
| 0.97 | 14 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 | 0.97 - 0.82 / 0.09 = 1.67 |
| 0.83 | 34 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 | 0.83 - 0.82 / 0.09 = 0.11 |
| 0.64 | 50 | 0.91 | 25 | 0.82 | 35 | 0.91 - 0.82 = 0.09 | 0.64 - 0.82 / 0.09 = -2.0 |
| 0.966 | 15 | 0.9978 | 12.5 | 0.953 | 17.5 | 0.966 - 0.953 = 0.0448 | 0.966 - 0.953 / 0.0448 = 0.29 |
你可以看到,我們基本是在內外余弦值之間根據θ插值
如果你仍不明白發生了什么,不必擔心,只需要記住這個公式就好了,在你更聰明的時候再回來看看
我們現在有了一個在聚光外是負的,在內圓錐內大於1.0的,在邊緣處於兩者之間的強度值了,如果我們正確地約束(Clamp)這個值,在片段着色器中就不再需要if-else了,我們能夠使用計算出來的強度值直接乘以光照分量:
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
...
// 將不對環境光做出影響,讓它總是能有一點光
diffuse *= intensity;
specular *= intensity;
...
注意我們使用了clamp函數,它把第一個參數約束(Clamp)在了0.0到1.0之間,這保證強度值不會在[0, 1]區間之外
確定你將outerCutOff值添加到了Light結構體之中,並在程序中設置它的uniform值
我們使用的內切光角是12.5,外切光角是17.5:

部分源碼
//片段着色器
#version 330 core
struct Material {
sampler2D diffuse;
sampler2D specular;
float shininess;
};
struct Light {
vec3 position;
vec3 direction;
float cutOff;
float outerCutOff;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant; //常數項Kc
float linear; //一次項Kl
float quadratic; //二次項Kq
};
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;
out vec4 FragColor;
uniform Material material;
uniform Light light;
//uniform vec3 objectColor;
//uniform vec3 lightColor;
//uniform vec3 lightPos;
uniform vec3 viewPos;
float specularStrength = 0.5;
void main()
{
// 環境光
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
// 漫反射
vec3 norm = normalize(Normal);
//vec3 lightDir = normalize(lightPos - FragPos);
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 viewDir = normalize(viewPos - FragPos);
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));
float epsilon = (light.cutOff - light.outerCutOff);
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;
vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}
多光源
現在我們將結合之前學過的所有知識,創建一個包含六個光源的場景,我們將模擬一個類似太陽的平行光光源,四個分散在場景中的點光源,以及一個手電筒
為了在場景中使用多個光源,我們希望將光照計算封裝到GLSL函數中
GLSL中的函數和C函數很相似,它有一個函數名、一個返回值類型,如果函數不是在main函數之前聲明的,我們還必須在代碼文件頂部聲明一個原型,我們對每個光照類型都創建一個不同的函數:平行光、點光源和聚光
當我們在場景中使用多個光源時,通常使用以下方法:我們需要有一個單獨的顏色向量代表片段的輸出顏色,對於每一個光源,它對片段的貢獻顏色將會加到片段的輸出顏色向量上,所以場景中的每個光源都會計算它們各自對片段的影響,並結合為一個最終的輸出顏色
out vec4 FragColor;
void main()
{
// 定義一個輸出顏色值
vec3 output;
// 將平行光的貢獻加到輸出中
output += someFunctionToCalculateDirectionalLight();
// 對所有的點光源也做相同的事情
for(int i = 0; i < nr_of_point_lights; i++)
output += someFunctionToCalculatePointLight();
// 也加上其它的光源(比如聚光)
output += someFunctionToCalculateSpotLight();
FragColor = vec4(output, 1.0);
}
實際的代碼對每一種實現都可能不同,但大體的結構都是差不多的,我們定義了幾個函數,用來計算每個光源的影響,並將最終的結果顏色加到輸出顏色向量上,例如,如果兩個光源都很靠近一個片段,那么它們所結合的貢獻將會形成一個比單個光源照亮時更加明亮的片段
平行光
我么需要在片段着色器中定義一個函數來計算平行光對相應片段的貢獻:它接受一些參數並計算一個平行光照顏色。
首先,我們需要定義一個平行光源最少所需要的變量,我們可以將這些變量儲存在一個叫做DirLight的結構體中,並將它定義為一個uniform,和上面寫的是基本一樣的
struct DirLight {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform DirLight dirLight;
接下來我們可以將dirLight傳入一個有着一下原型的函數。
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
和C/C++一樣,如果我們想調用一個函數(這里是在main函數中調用),這個函數需要在調用者的行數之前被定義過,在這個例子中我們更喜歡在main函數以下定義函數,所以上面要求就不滿足了。所以,我們需要在main函數之上定義函數的原型,這和C語言中是一樣的
你可以看到,這個函數需要一個DirLight結構體和其它兩個向量來進行計算。如果你認真完成了上一節的話,這個函數的內容應該理解起來很容易:
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
vec3 lightDir = normalize(-light.direction);
// 漫反射着色
float diff = max(dot(normal, lightDir), 0.0);
// 鏡面光着色
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// 合並結果
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);
}
我們基本上只是從上一節中復制了代碼,並使用函數參數的兩個向量來計算平行光的貢獻向量,最終環境光、漫反射和鏡面光的貢獻將會合並為單個顏色向量返回
點光源
和平行光一樣,我們也希望定義一個用於計算點光源對相應片段貢獻,以及衰減,的函數。同樣,我們定義一個包含了點光源所需所有變量的結構體:
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];
你可以看到,我們在GLSL中使用了預處理指令來定義了我們場景中點光源的數量,接着我們使用了這個NR_POINT_LIGHTS常量來創建了一個PointLight結構體的數組,GLSL中的數組和C數組一樣,可以使用一對方括號來創建,現在我們有四個待填充數據的PointLight結構體
我們也可以定義一個大的結構體(而不是為每種類型的光源定義不同的結構體),包含所有不同種光照類型所需的變量,並將這個結構體用到所有的函數中,只需要忽略用不到的變量就行了
個人覺得當前的方法會更直觀一點,不僅能夠節省一些代碼,而且由於不是所有光照類型都需要所有的變量,這樣也能節省一些內存
點光源函數的原型如下:
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
這個函數從參數中獲取所需的所有數據,並返回一個代表該點光源對片段的顏色貢獻的vec3。我們再一次聰明地從之前的教程中復制粘貼代碼,完成了下面這樣的函數:
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// 漫反射着色
float diff = max(dot(normal, lightDir), 0.0);
// 鏡面光着色
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// 衰減
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
// 合並結果
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);
}
將這些功能抽象到這樣一個函數中的優點是,我們能夠不用重復的代碼而很容易地計算多個點光源的光照了。在main函數中,我們只需要創建一個循環,遍歷整個點光源數組,對每個點光源調用CalcPointLight就可以了。
聚光
不再贅述,定義結構體:
struct SpotLight {
vec3 position;
vec3 direction;
float cutOff;
float outerCutOff;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// 漫反射着色
float diff = max(dot(normal, lightDir), 0.0);
// 鏡面光着色
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// 衰減
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
// 聚光強度
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
// 合並結果
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);
}
現在我們的片段着色器是這樣的:
#version 330 core
out vec4 FragColor;
struct Material {
sampler2D diffuse;
sampler2D specular;
float shininess;
};
struct DirLight {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
struct PointLight {
vec3 position;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
struct SpotLight {
vec3 position;
vec3 direction;
float cutOff;
float outerCutOff;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
#define NR_POINT_LIGHTS 4
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;
uniform vec3 viewPos;
uniform DirLight dirLight;
uniform PointLight pointLights[NR_POINT_LIGHTS];
uniform SpotLight spotLight;
uniform Material material;
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
void main()
{
// 屬性
vec3 norm = normalize(Normal);
vec3 viewDir = normalize(viewPos - FragPos);
// 1.平行光
vec3 result = CalcDirLight(dirLight, norm, viewDir);
// 2.點光源
for(int i = 0; i < NR_POINT_LIGHTS; i++)
result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);
// 3.手電筒
result += CalcSpotLight(spotLight, norm, FragPos, viewDir);
FragColor = vec4(result, 1.0);
}
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
vec3 lightDir = normalize(-light.direction);
// 漫反射着色
float diff = max(dot(normal, lightDir), 0.0);
// 鏡面光着色
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// 合並結果
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);
}
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// 漫反射着色
float diff = max(dot(normal, lightDir), 0.0);
// 鏡面光着色
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// 衰減
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
// 合並結果
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);
}
vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// 漫反射着色
float diff = max(dot(normal, lightDir), 0.0);
// 鏡面光着色
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// 衰減
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
// 聚光強度
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
// 合並結果
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);
}
合並結果
現在我們已經定義了一個計算平行光的函數和一個計算點光源的函數了,我們可以將它們合並放到main函數中。
void main()
{
// 屬性
vec3 norm = normalize(Normal);
vec3 viewDir = normalize(viewPos - FragPos);
// 1.平行光
vec3 result = CalcDirLight(dirLight, norm, viewDir);
// 2.點光源
for(int i = 0; i < NR_POINT_LIGHTS; i++)
result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);
// 3.手電筒
result += CalcSpotLight(spotLight, norm, FragPos, viewDir);
FragColor = vec4(result, 1.0);
}
每個光源類型都將它們的貢獻加到了最終的輸出顏色上,直到所有的光源都處理完了,最終的顏色包含了場景中所有光源的顏色影響所合並的結果
設置平行光結構體的uniform應該非常熟悉了,但是你可能會在想我們該如何設置點光源的uniform值,因為點光源的uniform現在是一個PointLight的數組了,其實這並不復雜
lightingShader.setFloat("pointLights[0].constant", 1.0f);
在這里我們索引了pointLights數組中的第一個PointLight,並獲取了constant變量的位置。但這也意味着不幸的是我們必須對這四個點光源手動設置uniform值,這讓點光源本身就產生了28個uniform調用,非常冗長
你也可以嘗試將這些抽象出去一點,定義一個點光源類,讓它來為你設置uniform值,但最后你仍然要用這種方式設置所有光源的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)
};
接下來我們從pointLights數組中索引對應的PointLight,將它的position值設置為剛剛定義的位置值數組中的其中一個,同時我們還要保證現在繪制的是四個燈立方體而不是僅僅一個,只要對每個燈物體創建一個不同的模型矩陣就可以了,和我們之前對箱子的處理類似
glBindVertexArray(lightVAO);
for (unsigned int i = 0; i < 4; i++)
{
model = glm::mat4(1.0f);
model = glm::translate(model, pointLightPositions[i]);
model = glm::scale(model, glm::vec3(0.2f)); // Make it a smaller cube
lampShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
運行程序:

OpenGl光照基礎學習到此結束
源碼:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include "shader_s.h"
#include "camera.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);
unsigned int loadTexture(char const * path);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
// camera
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
bool firstMouse = true;
// timing
float deltaTime = 0.0f; // 當前幀與上一幀的時間差
float lastFrame = 0.0f; // 上一幀的時間
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
int main()
{
// glfw初始化
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// glfw創建窗口
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
glfwSetCursorPosCallback(window, mouse_callback);
glfwSetScrollCallback(window, scroll_callback);
// 捕捉鼠標
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// glad加載所有OpenGL函數指針
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
glEnable(GL_DEPTH_TEST);
Shader lightingShader("colorVertex.txt", "colorFragment.txt");// 起什么名字自己定
Shader lampShader("lampVertex.txt", "lampFragment.txt");
float vertices[] = {
// positions // normals // texture coords
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f
};
// 立方體位置
glm::vec3 cubePositions[] = {
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(2.0f, 5.0f, -15.0f),
glm::vec3(-1.5f, -2.2f, -2.5f),
glm::vec3(-3.8f, -2.0f, -12.3f),
glm::vec3(2.4f, -0.4f, -3.5f),
glm::vec3(-1.7f, 3.0f, -7.5f),
glm::vec3(1.3f, -2.0f, -2.5f),
glm::vec3(1.5f, 2.0f, -2.5f),
glm::vec3(1.5f, 0.2f, -1.5f),
glm::vec3(-1.3f, 1.0f, -1.5f)
};
// 點光源位置
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)
};
unsigned int VBO;
glGenBuffers(1, &VBO);
unsigned int cubeVAO;
glGenVertexArrays(1, &cubeVAO);
glBindVertexArray(cubeVAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
unsigned int lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
unsigned int diffuseMap = loadTexture("container2.png");
unsigned int specularMap = loadTexture("container2_specular.png");
lightingShader.use();
lightingShader.setInt("material.diffuse", 0);
lightingShader.setInt("material.specular", 1);
//渲染循環
while (!glfwWindowShouldClose(window))
{
// 幀時間差
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 輸入
processInput(window);
// 渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
lightingShader.use();
lightingShader.setVec3("light.position", camera.Position);
lightingShader.setFloat("material.shininess", 32.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)));
// 初始化view transformations
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
lightingShader.setMat4("projection", projection);
lightingShader.setMat4("view", view);
// 初始化world transformations
glm::mat4 model = glm::mat4();
lightingShader.setMat4("model", model);
// 綁定漫反射貼圖
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);
// 綁定鏡面貼圖
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, specularMap);
// 渲染方塊
glBindVertexArray(cubeVAO);
for (unsigned int i = 0; i < 10; i++)
{
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * i;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
lightingShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
lampShader.use();
lampShader.setMat4("projection", projection);
lampShader.setMat4("view", view);;
glBindVertexArray(lightVAO);
for (unsigned int i = 0; i < 4; i++)
{
model = glm::mat4(1.0f);
model = glm::translate(model, pointLightPositions[i]);
model = glm::scale(model, glm::vec3(0.2f)); // Make it a smaller cube
lampShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
// 交換緩沖並查詢IO事件
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &cubeVAO);
glDeleteVertexArrays(1, &lightVAO);
glDeleteBuffers(1, &VBO);
glfwTerminate();
return 0;
}
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//是否按下了返回鍵
glfwSetWindowShouldClose(window, true);
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.ProcessKeyboard(FORWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.ProcessKeyboard(LEFT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.ProcessKeyboard(RIGHT, deltaTime);
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(xoffset, yoffset);
}
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
camera.ProcessMouseScroll(yoffset);
}
unsigned int loadTexture(char const * path)
{
unsigned int textureID;
glGenTextures(1, &textureID);
int width, height, nrComponents;
unsigned char *data = stbi_load(path, &width, &height, &nrComponents, 0);
if (data)
{
GLenum format;
if (nrComponents == 1)
format = GL_RED;
else if (nrComponents == 3)
format = GL_RGB;
else if (nrComponents == 4)
format = GL_RGBA;
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
stbi_image_free(data);
}
else
{
std::cout << "Texture failed to load at path: " << path << std::endl;
stbi_image_free(data);
}
return textureID;
}
