Vulkan_Shader_Day02—光照(基礎光照_Phong Lighting Model)

基礎光照

現實世界的光照是極其複雜的,而且會受到諸多因素的影響,這是我們有限的計算能力所無法模擬的。因此OpenGL的光照使用的是簡化的模型,對現實的情況進行近似,這樣處理起來會更容易一些,而且看起來也差不多一樣。這些光照模型都是基於我們對光的物理特性的理解。其中一個模型被稱爲馮氏光照模型(Phong Lighting Model)。馮氏光照模型的主要結構由3個分量組成:環境(Ambient)、漫反射(Diffuse)和鏡面(Specular)光照。下面這張圖展示了這些光照分量看起來的樣子:
在這裏插入圖片描述

  1. 環境光照(Ambient
    Lighting)
    :即使在黑暗的情況下,世界上通常也仍然有一些光亮(月亮、遠處的光),所以物體幾乎永遠不會是完全黑暗的。爲了模擬這個,我們會使用一個環境光照常量,它永遠會給物體一些顏色。
  2. 漫反射光照(Diffuse Lighting):模擬光源對物體的方向性影響(Directional
    Impact)。它是馮氏光照模型中視覺上最顯著的分量。物體的某一部分越是正對着光源,它就會越亮。
  3. 鏡面光照(Specular Lighting):模擬有光澤物體上面出現的亮點。鏡面光照的顏色相比於物體的顏色會更傾向於光的顏色。

爲了創建有趣的視覺場景,我們希望模擬至少這三種光照分量。我們將以最簡單的一個開始:環境光照。

一、環境光照

光通常都不是來自於同一個光源,而是來自於我們周圍分散的很多光源,即使它們可能並不是那麼顯而易見。光的一個屬性是,它可以向很多方向發散並反彈,從而能夠到達不是非常直接臨近的點。所以,光能夠在其它的表面上反射,對一個物體產生間接的影響。考慮到這種情況的算法叫做全局照明(Global Illumination)算法,但是這種算法既開銷高昂又極其複雜。

由於我們現在對那種又複雜又開銷高昂的算法不是很感興趣,所以我們將會先使用一個簡化的全局照明模型,即環境光照。正如你在上一節所學到的,我們使用一個很小的常量(光照)顏色,添加到物體片段的最終顏色中,這樣子的話即便場景中沒有直接的光源也能看起來存在有一些發散的光。

把環境光照添加到場景裏非常簡單。我們用光的顏色乘以一個很小的常量環境因子,再乘以物體的顏色,然後將最終結果作爲片段的顏色:

void main() {
    vec3 ambient = ambientStrength * fragBaseLight;
    vec3 result = ambient * fragColor;
    outColor = vec4(result, 1.0);
}

如果你現在運行你的程序,你會注意到馮氏光照的第一個階段已經應用到你的物體上了。這個物體非常暗,但由於應用了環境光照,也不是完全黑的。它看起來應該像這樣:
在這裏插入圖片描述

二、漫反射光照

環境光照本身不能提供最有趣的結果,但是漫反射光照就能開始對物體產生顯著的視覺影響了。漫反射光照使物體上與光線方向越接近的片段能從光源處獲得更多的亮度。爲了能夠更好的理解漫反射光照,請看下圖:
在這裏插入圖片描述
圖左上方有一個光源,它所發出的光線落在物體的一個片段上。我們需要測量這個光線是以什麼角度接觸到這個片段的。如果光線垂直於物體表面,這束光對物體的影響會最大化(譯註:更亮)。爲了測量光線和片段的角度,我們使用一個叫做法向量(Normal Vector)的東西,它是垂直於片段表面的一個向量(這裏以黃色箭頭表示),我們在後面再講這個東西。這兩個向量之間的角度很容易就能夠通過點乘計算出來。

我們知道兩個單位向量的夾角越小,它們點乘的結果越傾向於1。當兩個向量的夾角爲90度的時候,點乘會變爲0。這同樣適用於θ,θ越大,光對片段顏色的影響就應該越小。

  • 注意,爲了(只)得到兩個向量夾角的餘弦值,我們使用的是單位向量(長度爲1的向量),所以我們需要確保所有的向量都是標準化的,否則點乘返回的就不僅僅是餘弦值了
    點乘返回一個標量,我們可以用它計算光線對片段顏色的影響。不同片段朝向光源的方向的不同,這些片段被照亮的情況也不同。

所以,計算漫反射光照需要什麼?

  • 法向量:一個垂直於頂點表面的向量。
  • 定向的光線:作爲光源的位置與片段的位置之間向量差的方向向量。爲了計算這個光線,我們需要光的位置向量和片段的位置向量。

1.法向量
上一章節中已經添加進着色器中。
2.計算漫反射光照
我們現在對每個頂點都有了法向量,但是我們仍然需要光源的位置向量和片段的位置向量。由於光源的位置是一個靜態變量,我們可以在UniformBufferObject中添加glm::vec3 lightPos來保存光源位置:

struct UniformBufferObject {
	...
	glm::vec3 lightPos;
};

並且定義一個全局光源位置:

glm::vec3 lightPos= glm::vec3(1.0f, 1.0f, 1.0f);

然後在渲染循環中更新uniform。我們使用在前面聲明的lightPos向量作爲光源位置:

void updateUniformBuffer() {
		...
		ubo.lightPos = lightPos;
		...

最後,我們還需要片段的位置。我們會在世界空間中進行所有的光照計算,因此我們需要一個在世界空間中的頂點位置。我們可以通過把頂點位置屬性乘以模型矩陣(不是觀察和投影矩陣)來把它變換到世界空間座標。這個在頂點着色器中很容易完成,所以我們聲明一個輸出變量,並計算它的世界空間座標:

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(binding = 0) uniform UniformBufferObject {
    ...
	vec3 lightPos;
} ubo;

...
layout(location = 5) out vec3 lightPos;
layout(location = 6) out vec3 fragPos;

void main() {
    ...
	fragPos =vec3( ubo.model * vec4(inPosition, 1.0));;
    ...
	lightPos=ubo.lightPos;
}

最後,在片段着色器中添加相應的輸入變量。

layout(location = 5) in vec3 lightPos;
layout(location = 6) in vec3 fragPos;

現在,所有需要的變量都設置好了,我們可以在片段着色器中添加光照計算了。

我們需要做的第一件事是計算光源和片段位置之間的方向向量。前面提到,光的方向向量是光源位置向量與片段位置向量之間的向量差。你可能記得在變換教程中,我們能夠簡單地通過讓兩個向量相減的方式計算向量差。我們同樣希望確保所有相關向量最後都轉換爲單位向量,所以我們把法線和最終的方向向量都進行標準化:

void main() {
    ...
	vec3 norm = normalize(fragNormal);
    vec3 lightDir = normalize(lightPos - fragPos);
	...
}
  • 注:當計算光照時我們通常不關心一個向量的模長或它的位置,我們只關心它們的方向。所以,幾乎所有的計算都使用單位向量完成,因爲這簡化了大部分的計算(比如點乘)。所以當進行光照計算時,確保你總是對相關向量進行標準化,來保證它們是真正地單位向量。忘記對向量進行標準化是一個十分常見的錯誤。
    下一步,我們對norm和lightDir向量進行點乘,計算光源對當前片段實際的漫發射影響。結果值再乘以光的顏色,得到漫反射分量。兩個向量之間的角度越大,漫反射分量就會越小:
	float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * fragBaseLight;

如果兩個向量之間的角度大於90度,點乘的結果就會變成負數,這樣會導致漫反射分量變爲負數。爲此,我們使用max函數返回兩個參數之間較大的參數,從而保證漫反射分量不會變成負數。負數顏色的光照是沒有定義的,所以最好避免它,除非你是那種古怪的藝術家。

現在我們有了環境光分量和漫反射分量,我們把它們相加,然後把結果乘以物體的顏色,來獲得片段最後的輸出顏色。

    vec3 result = (ambient + diffuse) * fragColor;
    outColor = vec4(result, 1.0);

如果你的應用(和着色器)編譯成功了,你可能看到類似的輸出:
在這裏插入圖片描述
3.法線矩陣
此處並不是我們想看到的效果,因爲目前片段着色器裏的計算都是在世界空間座標中進行的。所以,我們是不是應該把法向量也轉換爲世界空間座標?基本正確,但是這不是簡單地把它乘以一個模型矩陣就能搞定的。
修復這個行爲的訣竅是使用一個爲法向量專門定製的模型矩陣。這個矩陣稱之爲法線矩陣(Normal Matrix),它使用了一些線性代數的操作來移除對法向量錯誤縮放的影響。如果你想知道這個矩陣是如何計算出來的,建議去閱讀這個文章。

法線矩陣被定義爲「模型矩陣左上角的逆矩陣的轉置矩陣」。真是拗口,如果你不明白這是什麼意思,別擔心,我們還沒有討論逆矩陣(Inverse Matrix)和轉置矩陣(Transpose Matrix)。注意,大部分的資源都會將法線矩陣定義爲應用到模型-觀察矩陣(Model-view Matrix)上的操作,但是由於我們只在世界空間中進行操作(不是在觀察空間),我們只使用模型矩陣。

在頂點着色器中,我們可以使用inverse和transpose函數自己生成這個法線矩陣,這兩個函數對所有類型矩陣都有效。注意我們還要把被處理過的矩陣強制轉換爲3×3矩陣,來保證它失去了位移屬性以及能夠乘以vec3的法向量。

 fragNormal = mat3(transpose(inverse(ubo.model))) *inNormal;

編譯着色器,再次運行,可以看到如下效果:
在這裏插入圖片描述

三、鏡面光照

如果你還沒被這些光照計算搞得精疲力盡,我們就再把鏡面高光(Specular Highlight)加進來,這樣馮氏光照纔算完整。

和漫反射光照一樣,鏡面光照也是依據光的方向向量和物體的法向量來決定的,但是它也依賴於觀察方向,例如玩家是從什麼方向看着這個片段的。鏡面光照是基於光的反射特性。如果我們想象物體表面像一面鏡子一樣,那麼,無論我們從哪裏去看那個表面所反射的光,鏡面光照都會達到最大化。你可以從下面的圖片看到效果:
在這裏插入圖片描述
我們通過反射法向量周圍光的方向來計算反射向量。然後我們計算反射向量和視線方向的角度差,如果夾角越小,那麼鏡面光的影響就會越大。它的作用效果就是,當我們去看光被物體所反射的那個方向的時候,我們會看到一個高光。

觀察向量是鏡面光照附加的一個變量,我們可以使用觀察者世界空間位置和片段的位置來計算它。之後,我們計算鏡面光強度,用它乘以光源的顏色,再將它加上環境光和漫反射分量。

爲了得到觀察者的世界空間座標,我們簡單地使用攝像機對象的位置座標代替(它當然就是觀察者)。所以我們定義鏡面反射強度 float specularStrength
和相機位置glm::vec3 viewPos添加到UniformBufferObject中,本章完整UniformBufferObject 結構體如下。

struct UniformBufferObject {
	glm::mat4 model;
	glm::mat4 view;
	glm::mat4 proj;
	glm::vec3 baseLight;
	float ambientStrength;
	glm::vec3 lightPos;
	float specularStrength;
	glm::vec3 viewPos;
};

定義一個全局的鏡面強度光照係數

float specularStrength = 0.8f;//鏡面強度

之後在updateUniformBuffer函數中更新數據

	void updateUniformBuffer() {
		...
		ubo.specularStrength = specularStrength;
		ubo.viewPos = camera.Position;
		...
	}

接下來,我們在頂點着色器中更新數據,其完整的頂着色器(shader.vert)如下:

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(binding = 0) uniform UniformBufferObject {
    mat4 model;
    mat4 view;
    mat4 proj;
	vec3 baseLight;
	float ambientStrength;
	vec3 lightPos;
	float specularStrength ;
	vec3 viewPos;
} ubo;

layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 2) in vec3 inNormal;
layout(location = 3) in vec2 inTexCoord;

layout(location = 0) out vec3 fragColor;
layout(location = 1) out vec3 fragNormal;
layout(location = 2) out vec2 fragTexCoord;
layout(location = 3) out vec3 fragBaseLight;
layout(location = 4) out float ambientStrength;
layout(location = 5) out vec3 lightPos;
layout(location = 6) out vec3 fragPos;
layout(location = 7) out float specularStrength;
layout(location = 8) out vec3 viewPos;

void main() {
    gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
	fragPos =vec3( ubo.model * vec4(inPosition, 1.0));;
    fragColor = inColor;
    fragNormal = mat3(transpose(inverse(ubo.model))) *inNormal;
	fragTexCoord = inTexCoord;
	fragBaseLight = ubo.baseLight;
	ambientStrength= ubo.ambientStrength;
	lightPos=ubo.lightPos;
	specularStrength=ubo.specularStrength;
	viewPos=ubo.viewPos;

}

下一步,我們計算視線方向向量,和對應的沿着法線軸的反射向量:

	vec3 viewDir = normalize(viewPos - fragPos);
    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 * fragBaseLight;

我們先計算視線方向與反射方向的點乘(並確保它不是負值),然後取它的32次冪。這個32是高光的反光度(Shininess)。一個物體的反光度越高,反射光的能力越強,散射得越少,高光點就會越小。在下面的圖片裏,你會看到不同反光度的視覺效果影響:
在這裏插入圖片描述
我們不希望鏡面成分過於顯眼,所以我們把指數保持爲32。剩下的最後一件事情是把它加到環境光分量和漫反射分量裏,再用結果乘以物體的顏色:

    vec3 result = (ambient + diffuse + specular) * fragColor;
    outColor = vec4(result, 1.0);

其完整的片元着色器(shader.frag)如下

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(binding = 1) uniform sampler2D texSampler;

layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec3 fragNormal;
layout(location = 2) in vec2 fragTexCoord;
layout(location = 3) in vec3 fragBaseLight;
layout(location = 4) in float ambientStrength;
layout(location = 5) in vec3 lightPos;
layout(location = 6) in vec3 fragPos;
layout(location = 7) in float specularStrength;
layout(location = 8) in vec3 viewPos;

layout(location = 0) out vec4 outColor;

void main() {

    //Ambient Lighting
    vec3 ambient = ambientStrength * fragBaseLight;

	//Diffuse Lighting
	vec3 norm = normalize(fragNormal);
    vec3 lightDir = normalize(lightPos - fragPos);
	float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * fragBaseLight;

	//Specular Lighting
	vec3 viewDir = normalize(viewPos - fragPos);
    vec3 reflectDir = reflect(-lightDir, norm);
	float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
    vec3 specular = specularStrength * spec * fragBaseLight;

    vec3 result = (ambient + diffuse + specular) * fragColor;
    outColor = vec4(result, 1.0);

    //outColor = vec4(fragTexCoord, 0.0, 1.0);
	//outColor = vec4(fragColor * texture(texSampler, fragTexCoord).rgb, 1.0);
    //outColor = texture(texSampler, fragTexCoord)* vec4(lightColor, 1.0);
}

我們現在爲馮氏光照計算了全部的光照分量。調整你的視角,你可以看到類似下面的畫面:
在這裏插入圖片描述
此外您可以關閉漫反射。

    vec3 result = (ambient + specular) * fragColor;
    outColor = vec4(result, 1.0);

再次運行,可以看到如下效果:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章