現實世界的光照是極其複雜的,而且會受到諸多因素的影響,這是我們有限的計算能力所無法模擬的。因此OpenGL的光照使用的是簡化的模型,對現實的情況進行近似,這樣處理起來會更容易一些,而且看起來也差不多一樣。這些光照模型都是基於我們對光的物理特性的理解。其中一個模型被稱爲馮氏光照模型(Phong Lighting Model)。
馮氏光照模型的主要結構由3個分量組成:環境(Ambient)、漫反射(Diffuse)和鏡面(Specular)光照。下面這張圖展示了這些光照分量看起來的樣子:
- 環境光照(Ambient Lighting):即使在黑暗的情況下,世界上通常也仍然有一些光亮(月亮、遠處的光),所以物體幾乎永遠不會是完全黑暗的。爲了模擬這個,我們會使用一個環境光照常量,它永遠會給物體一些顏色。
- 漫反射光照(Diffuse Lighting):模擬光源對物體的方向性影響(Directional Impact)。它是馮氏光照模型中視覺上最顯著的分量。物體的某一部分越是正對着光源,它就會越亮。
- 鏡面光照(Specular Lighting):模擬有光澤物體上面出現的亮點。鏡面光照的顏色相比於物體的顏色會更傾向於光的顏色。
環境光照
光通常都不是來自於同一個光源,而是來自於我們周圍分散的很多光源,即使它們可能並不是那麼顯而易見。光的一個屬性是,它可以向很多方向發散並反彈,從而能夠到達不是非常直接臨近的點。所以,光能夠在其它的表面上反射,對一個物體產生間接的影響。考慮到這種情況的算法叫做全局照明(Global Illumination)算法,但是這種算法既開銷高昂又極其複雜。
由於我們現在對那種又複雜又開銷高昂的算法不是很感興趣,所以我們將會先使用一個簡化的全局照明模型,即環境光照。我們使用一個很小的常量(光照)顏色,添加到物體片段的最終顏色中,這樣子的話即便場景中沒有直接的光源也能看起來存在有一些發散的光。
把環境光照添加到場景裏非常簡單。我們用光的顏色乘以一個很小的常量環境因子,再乘以物體的顏色,然後將最終結果作爲片段的顏色:
void main()
{
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
FragColor = vec4(result, 1.0);
}
如果你現在運行你的程序,你會注意到馮氏光照的第一個階段已經應用到你的物體上了。這個物體非常暗,但由於應用了環境光照(注意光源立方體沒受影響是因爲我們對它使用了另一個着色器),也不是完全黑的。它看起來應該像這樣:
漫反射光照
環境光照本身不能提供最有趣的結果,但是漫反射光照就能開始對物體產生顯著的視覺影響了。
漫反射光照使物體上與光線方向越接近的片段能從光源處獲得更多的亮度。爲了能夠更好的理解漫反射光照,請看下圖:
圖左上方有一個光源,它所發出的光線落在物體的一個片段上。我們需要測量這個光線是以什麼角度接觸到這個片段的。如果光線垂直於物體表面,這束光對物體的影響會最大化(譯註:更亮)。爲了測量光線和片段的角度,我們使用一個叫做法向量(Normal Vector)的東西,它是垂直於片段表面的一個向量(這裏以黃色箭頭表示)。這兩個向量之間的角度很容易就能夠通過點乘計算出來。
在變換那一節教程裏,我們知道兩個單位向量的夾角越小,它們點乘的結果越傾向於1。當兩個向量的夾角爲90度的時候,點乘會變爲0。這同樣適用於θ,θ越大,光對片段顏色的影響就應該越小。
點乘返回一個標量(由於兩個向量是單位向量,意味着返回的就是餘弦值),我們可以用它計算光線對片段顏色的影響。不同片段朝向光源的方向的不同,這些片段被照亮的情況也不同。
所以,計算漫反射光照需要什麼?
- 法向量:一個垂直於頂點表面的向量。
- 定向的光線:作爲光源的位置與片段的位置之間向量差的方向向量。爲了計算這個光線,我們需要光的位置向量和片段的位置向量。
法向量是一個垂直於頂點表面的(單位)向量。由於頂點本身並沒有表面(它只是空間中一個獨立的點),我們利用它周圍的頂點來計算出這個頂點的表面。我們能夠使用一個小技巧,使用叉乘對立方體所有的頂點計算法向量,但是由於3D立方體不是一個複雜的形狀,所以我們可以簡單地把法線數據手工添加到頂點數據中。更新後的頂點數據數組:
float vertices[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f
};
試着去想象一下,這些法向量真的是垂直於立方體各個平面的表面的(一個立方體由6個平面組成)。
由於我們向頂點數組添加了額外的數據,所以我們應該更新光照的頂點着色器:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
...
現在我們已經向每個頂點添加了一個法向量並更新了頂點着色器,我們還要更新頂點屬性指針。注意,燈使用同樣的頂點數組作爲它的頂點數據,然而燈的着色器並沒有使用新添加的法向量。我們不需要更新燈的着色器或者是屬性的配置,但是我們必須至少修改一下頂點屬性指針來適應新的頂點數組的大小:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
我們只想使用每個頂點的前三個float,並且忽略後三個float,所以我們只需要把步長參數改成float大小的6倍就行了。
所有光照的計算都是在片段着色器裏進行,所以我們需要將法向量由頂點着色器傳遞到片段着色器。
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
Normal = aNormal;
}
計算漫反射光照
我們現在對每個頂點都有了法向量,但是我們仍然需要光源的位置向量和片段的位置向量。由於光源的位置是一個靜態變量,我們可以簡單地在受光照的物體的片段着色器中把它聲明爲uniform:
uniform vec3 lightPos;
然後在渲染循環中(渲染循環的外面也可以,因爲它不會改變)更新uniform。我們使用在前面聲明的lightPos向量作爲光源位置:
lightingShader.setVec3("lightPos", lightPos);
最後,我們還需要片段的位置。我們會在世界空間中進行所有的光照計算,因此我們需要一個在世界空間中的頂點位置。我們可以通過把頂點位置屬性乘以模型矩陣(不是觀察和投影矩陣)來把它變換到世界空間座標(在世界座標系下處理完就夠了)。這個在頂點着色器中很容易完成,所以我們聲明一個輸出變量,並計算它的世界空間座標:
out vec3 FragPos;
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = aNormal;
}
最後,在片段着色器中添加相應的輸入變量。
in vec3 FragPos;
我們需要做的第一件事是計算光源和片段位置之間的方向向量。
前面提到,光的方向向量是光源位置向量與片段位置向量之間的向量差。在變換教程中,我們能夠簡單地通過讓兩個向量相減的方式計算向量差。我們同樣希望確保所有相關向量最後都轉換爲單位向量,所以我們把受光照物體的片段着色器的法線和最終的方向向量都進行標準化:
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
下一步,我們對norm和lightDir向量進行點乘(都標準化爲單位向量了,兩個向量點乘實際得到的就是兩向量夾角的餘弦值),計算光源對當前片段實際的漫發射影響。
結果值再乘以光的顏色,得到漫反射分量。
兩個向量之間的角度越大,漫反射分量就會越小:
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
如果兩個向量之間的角度大於90度,點乘的結果就會變成負數,這樣會導致漫反射分量變爲負數。
爲此,我們使用max函數返回兩個參數之間較大的參數,從而保證漫反射分量不會變成負數。負數顏色的光照是沒有定義的,所以最好避免它,除非你是那種古怪的藝術家。
現在我們有了環境光分量和漫反射分量,我們把它們相加,然後把結果乘以物體的顏色,來獲得片段最後的輸出顏色。
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);
如果你的應用(和着色器)編譯成功了,你可能看到類似的輸出:
你可以看到使用了漫反射光照,立方體看起來就真的像個立方體了。嘗試在你的腦中想象一下法向量,並在立方體周圍移動,注意觀察法向量和光的方向向量之間的夾角越大,片段就會越暗。
最後一件事
現在我們已經把法向量從頂點着色器傳到了片段着色器。可是,目前片段着色器裏的計算都是在世界空間座標中進行的。所以,我們是不是應該把法向量也轉換爲世界空間座標?基本正確,但是這不是簡單地把它乘以一個模型矩陣就能搞定的。
首先,法向量只是一個方向向量,不能表達空間中的特定位置。同時,法向量沒有齊次座標(頂點位置中的w分量)。這意味着,位移不應該影響到法向量。因此,如果我們打算把法向量乘以一個模型矩陣,我們就要從矩陣中移除位移部分,只選用模型矩陣左上角3×3的矩陣(注意,我們也可以把法向量的w分量設置爲0,再乘以4×4矩陣;這同樣可以移除位移)。對於法向量,我們只希望對它實施縮放和旋轉變換。
其次,如果模型矩陣執行了不等比縮放,頂點的改變會導致法向量不再垂直於表面了。因此,我們不能用這樣的模型矩陣來變換法向量。下面的圖展示了應用了不等比縮放的模型矩陣對法向量的影響:
每當我們應用一個不等比縮放時(注意:等比縮放不會破壞法線,因爲法線的方向沒被改變,僅僅改變了法線的長度,而這很容易通過標準化來修復),法向量就不會再垂直於對應的表面了,這樣光照就會被破壞。
修復這個行爲的訣竅是使用一個爲法向量專門定製的模型矩陣。這個矩陣稱之爲法線矩陣(Normal Matrix),它使用了一些線性代數的操作來移除對法向量錯誤縮放的影響。如果你想知道這個矩陣是如何計算出來的,建議去閱讀這個文章。
法線矩陣被定義爲「模型矩陣左上角的逆矩陣的轉置矩陣」。真是拗口,如果你不明白這是什麼意思,別擔心,我們還沒有討論逆矩陣(Inverse Matrix)和轉置矩陣(Transpose Matrix)。
注意,大部分的資源都會將法線矩陣定義爲應用到模型-觀察矩陣(Model-view Matrix)上的操作,但是由於我們只在世界空間中進行操作(不是在觀察空間),我們只使用模型矩陣。
在頂點着色器中,我們可以使用inverse和transpose函數自己生成這個法線矩陣,這兩個函數對所有類型矩陣都有效。注意我們還要把被處理過的矩陣強制轉換爲3×3矩陣,來保證它失去了位移屬性以及能夠乘以vec3的法向量。
Normal = mat3(transpose(inverse(model))) * aNormal;
在漫反射光照部分,光照表現並沒有問題,這是因爲我們沒有對物體本身執行任何縮放操作,所以並不是必須要使用一個法線矩陣,僅僅讓模型矩陣乘以法線也可以。
可是,如果你進行了不等比縮放,使用法線矩陣去乘以法向量就是必不可少的了。
鏡面反射
把鏡面高光(Specular Highlight)加進來,這樣馮氏光照纔算完整。
和漫反射光照一樣,鏡面光照也是依據光的方向向量和物體的法向量來決定的,但是它也依賴於觀察方向,例如玩家是從什麼方向看着這個片段的。鏡面光照是基於光的反射特性。如果我們想象物體表面像一面鏡子一樣,那麼,無論我們從哪裏去看那個表面所反射的光,鏡面光照都會達到最大化。你可以從下面的圖片看到效果:
我們通過反射法向量周圍光的方向來計算反射向量。然後我們計算反射向量和視線方向的角度差,如果夾角越小,那麼鏡面光的影響就會越大。它的作用效果就是,當我們去看光被物體所反射的那個方向的時候,我們會看到一個高光。
觀察向量是鏡面光照附加的一個變量,我們可以使用觀察者世界空間位置和片段的位置來計算它。
之後,我們計算鏡面光強度,用它乘以光源的顏色,再將它加上環境光和漫反射分量。
爲了得到觀察者的世界空間座標,我們簡單地使用攝像機對象的位置座標代替(它當然就是觀察者)。所以我們把另一個uniform添加到片段着色器,把相應的攝像機位置座標傳給片段着色器:
uniform vec3 viewPos;
lightingShader.setVec3("viewPos", camera.Position);
現在我們已經獲得所有需要的變量,可以計算高光強度了。
首先,我們定義一個鏡面強度(Specular Intensity)變量,給鏡面高光一箇中等亮度顏色,讓它不要產生過度的影響。
float specularStrength = 0.5;
如果我們把它設置爲1.0f,我們會得到一個非常亮的鏡面光分量,這對於一個珊瑚色的立方體來說有點太多了。下一節教程中我們會討論如何合理設置這些光照強度,以及它們是如何影響物體的。下一步,我們計算視線方向向量,和對應的沿着法線軸的反射向量:
vec3 viewDir = normalize(viewPos - FragPos);
// lightDir對應上圖的黑線,計算出的反射向量reflectDir對應上圖的橘黃線
vec3 reflectDir = reflect(-lightDir, norm);
需要注意的是我們對lightDir向量進行了取反。
reflect函數要求第一個向量是從光源指向片段位置的向量,但是lightDir當前正好相反,是從片段指向光源(由先前我們計算lightDir向量時,減法的順序決定)。爲了保證我們得到正確的reflect向量,我們通過對lightDir向量取反來獲得相反的方向。
第二個參數要求是一個法向量,所以我們提供的是已標準化的norm向量。
剩下要做的是計算鏡面分量。下面的代碼完成了這件事:
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
我們先計算視線方向與反射方向的點乘(並確保它不是負值),然後取它的32次冪。這個32是高光的反光度(Shininess)。一個物體的反光度越高,反射光的能力越強,散射得越少,高光點就會越小。在下面的圖片裏,你會看到不同反光度的視覺效果影響:
我們不希望鏡面成分過於顯眼,所以我們把指數保持爲32。剩下的最後一件事情是把它加到環境光分量和漫反射分量裏,再用結果乘以物體的顏色:
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
我們現在爲馮氏光照計算了全部的光照分量。根據你的視角,你可以看到類似下面的畫面:
完整源碼
main.cpp
#define STB_IMAGE_IMPLEMENTATION
#include <glad/glad.h>
#include <glfw3.h>
#include <stb_image.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include "../include/camera.h"
#include "../include/shader.h"
#include <iostream>
#include <opencv.hpp>
using namespace cv;
void processInput(GLFWwindow *window);
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void mouse_callback(GLFWwindow *window, double xpos, double ypos);
void scroll_callback(GLFWwindow *wihdow, double xoffset, double yoffset);
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;
// 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;
// lighting
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (NULL == window)
{
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);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// 深度測試
glEnable(GL_DEPTH_TEST);
Shader lightingShader("src/color.vs", "src/color.fs");
Shader lampShader("src/1.lamp.vs", "src/light.fs"); // 實例化光源對象
float vertices[] = {
// 位置 // 法線向量
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f
};
unsigned int VBO, cubeVAO;
glGenVertexArrays(1, &cubeVAO);
glGenBuffers(1, &VBO);
glBindVertexArray(cubeVAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
unsigned int lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 其實不用再次綁定了,因爲上一個物體已經綁定了,沒有解綁,繪製一個一樣的長方體
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
while (!glfwWindowShouldClose(window))
{
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
processInput(window);
// 渲染
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清理顏色緩衝和深度緩衝
// 設置uniform變量之前先啓動着色器
lightingShader.use();
lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("lightColor", 1.0f, 0.5f, 1.0f);
lightingShader.setVec3("lightPos", lightPos);
lightingShader.setVec3("viewPos", camera.Position);
// 先模型矩陣,再觀察矩陣,最後投影矩陣
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix(); // 通過lookat矩陣獲取觀察矩陣
lightingShader.setMat4("projection", projection);
lightingShader.setMat4("view", view);
glm::mat4 model = glm::mat4(1.0f);
lightingShader.setMat4("model", model);
// 渲染立方體
glBindVertexArray(cubeVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
// 繪製光源對象
lampShader.use();
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;
}
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; // 顛倒了,因爲y座標從下到上,圖像的uv座標
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(xoffset, yoffset);
}
void scroll_callback(GLFWwindow *window, double xoffset, double yoffset)
{
camera.ProcessMouseScroll(yoffset);
}
1.lamp.vs
#version 330 core
layout(location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
light.fs
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f); // 將向量的四個分量設置爲1.0f
}
color.vs
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 Normal; // 法線向量
out vec3 FragPos; // 片段位置
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
// 注意乘法要從右往左讀,即先乘模型矩陣,再觀察矩陣,最後乘以透視投影矩陣
FragPos = vec3(model * vec4(aPos, 1.0f)); // 轉換到世界座標系下
Normal = mat3(transpose(inverse(model))) * aNormal; // 把法線向量轉換到世界座標系下
gl_Position = projection * view * vec4(FragPos, 1.0f);
}
color.fs
#version 330 core
in vec3 Normal; // 法線向量
in vec3 FragPos; // 片段位置
out vec4 FragColor;
uniform vec3 lightPos; // 光源位置
uniform vec3 viewPos; // 觀察者(攝像機對象)的位置座標
uniform vec3 objectColor; // 物體顏色
uniform vec3 lightColor; // 光源顏色
void main()
{
// ambient
float ambientStrength = 0.1; // 環境光照,確保場景中沒有直接的光源也能有一些發散的光
vec3 ambient = ambientStrength * lightColor; // 環境分量顏色值
// diffuse(漫反射)
vec3 norm = normalize(Normal); // 標準化法線向量
vec3 lightDir = normalize(lightPos - FragPos); // 標準化光照方向向量,世界座標系下的片段位置座標指向光源點(OA-OB = BA)
float diff = max(dot(norm, lightDir), 0.0); // 計算法線與光照方向向量夾角餘弦值
vec3 diffuse = diff * lightColor; // 計算每一個片段光照漫反射分量的顏色值
// specular(鏡面反射)
float specularStrength = 0.5; // 鏡面強度
vec3 viewDir = normalize(viewPos - FragPos); // 片段到觀察者的方向向量
vec3 reflectDir = reflect(-lightDir, norm); // 沿着法線軸的反射向量
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32); // 計算反射向量與觀察者向量的夾角餘弦值的32倍
vec3 specular = specularStrength * spec * lightColor; // 鏡面分量
vec3 result = (ambient + diffuse + specular) * objectColor; // 計算光照環境分量與漫反射分量作用下物體的最終顏色值
FragColor = vec4(result, 1.0f);
}
練習
在觀察空間(而不是世界空間)中計算馮氏光照:
// Vertex shader:
// ================
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 FragPos;
out vec3 Normal;
out vec3 LightPos;
uniform vec3 lightPos; // we now define the uniform in the vertex shader and pass the 'view space' lightpos to the fragment shader. lightPos is currently in world space.
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
FragPos = vec3(view * model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(view * model))) * aNormal;
LightPos = vec3(view * vec4(lightPos, 1.0)); // Transform world-space light position to view-space light position
}
// Fragment shader:
// ================
#version 330 core
out vec4 FragColor;
in vec3 FragPos;
in vec3 Normal;
in vec3 LightPos; // extra in variable, since we need the light position in view space we calculate this in the vertex shader
uniform vec3 lightColor;
uniform vec3 objectColor;
void main()
{
// ambient
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(LightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// specular
float specularStrength = 0.5;
vec3 viewDir = normalize(-FragPos); // the viewer is always at (0,0,0) in view-space, so viewDir is (0,0,0) - Position => -Position
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
}
嘗試實現一個Gouraud着色(而不是馮氏着色)。如果你做對了話,立方體的光照應該會看起來有些奇怪,嘗試推理爲什麼它會看起來這麼奇怪:
// Vertex shader:
// ================
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 LightingColor; // resulting color from lighting calculations
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
// gouraud shading
// ------------------------
vec3 Position = vec3(model * vec4(aPos, 1.0));
vec3 Normal = mat3(transpose(inverse(model))) * aNormal;
// ambient
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - Position);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// specular
float specularStrength = 1.0; // this is set higher to better show the effect of Gouraud shading
vec3 viewDir = normalize(viewPos - Position);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
LightingColor = ambient + diffuse + specular;
}
// Fragment shader:
// ================
#version 330 core
out vec4 FragColor;
in vec3 LightingColor;
uniform vec3 objectColor;
void main()
{
FragColor = vec4(LightingColor * objectColor, 1.0);
}