Vulkan_PBR—基於物理的渲染基礎

本文主要介紹shader部分,至於main代碼,如果你看過之前章節,應該可輕鬆實現,基礎理論主要參照 Learn OpenGL–PBR,本部分做簡述及BRDF實現

1.理論

PBR,或者用更通俗一些的稱呼是指基於物理的渲染(Physically Based Rendering),它指的是一些在不同程度上都基於與現實世界的物理原理更相符的基本理論所構成的渲染技術的集合。正因爲基於物理的渲染目的便是爲了使用一種更符合物理學規律的方式來模擬光線,因此這種渲染方式與我們原來的Phong或者Blinn-Phong光照算法相比總體上看起來要更真實一些。除了看起來更好些以外,由於它與物理性質非常接近,因此我們(尤其是美術師們)可以直接以物理參數爲依據來編寫表面材質,而不必依靠粗劣的修改與調整來讓光照效果看上去正常。使用基於物理參數的方法來編寫材質還有一個更大的好處,就是不論光照條件如何,這些材質看上去都會是正確的,而在非PBR的渲染管線當中有些東西就不會那麼真實了。

2.微平面模型

所有的PBR技術都基於微平面理論。這項理論認爲,達到微觀尺度之後任何平面都可以用被稱爲微平面(Microfacets)的細小鏡面來進行描繪。根據平面粗糙程度的不同,這些細小鏡面的取向排列可以相當不一致:
在這裏插入圖片描述
產生的效果就是:一個平面越是粗糙,這個平面上的微平面的排列就越混亂。這些微小鏡面這樣無序取向排列的影響就是,當我們特指鏡面光/鏡面反射時,入射光線更趨向於向完全不同的方向發散(Scatter)開來,進而產生出分佈範圍更廣泛的鏡面反射。而與之相反的是,對於一個光滑的平面,光線大體上會更趨向於向同一個方向反射,造成更小更銳利的反射:
在這裏插入圖片描述
在微觀尺度下,沒有任何平面是完全光滑的。然而由於這些微平面已經微小到無法逐像素的繼續對其進行區分,因此我們只有假設一個粗糙度(Roughness)參數,然後用統計學的方法來概略的估算微平面的粗糙程度。我們可以基於一個平面的粗糙度來計算出某個向量的方向與微平面平均取向方向一致的概率。這個向量便是位於光線向量l和視線向量v之間的中間向量(Halfway Vector)。它的計算方法如下:
在這裏插入圖片描述
微平面的取向方向與中間向量的方向越是一致,鏡面反射的效果就越是強烈越是銳利。然後再加上一個介於0到1之間的粗糙度參數(roughness),這樣我們就能概略的估算微平面的取向情況了:

			uint32_t objcount = 5;
			for (uint32_t x = 0; x < objcount; x++) {
				glm::vec3 pos = glm::vec3(float(x - (objcount / 2.0f)) * 2.5f, 0.0f, 0.0f);
				mat.params.roughness = glm::clamp((float)x / (float)objcount, 0.1f, 1.0f);
				vkCmdPushConstants(drawCmdBuffers[i], pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(glm::vec3), &pos);
				vkCmdPushConstants(drawCmdBuffers[i], pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(glm::vec3), sizeof(Material::PushBlock), &mat);
				vkCmdDrawIndexed(drawCmdBuffers[i], models.objects[models.objectIndex].indexCount, 1, 0, 0, 0);
			}

在這裏插入圖片描述

我們可以看到,較高的粗糙度值顯示出來的鏡面反射的輪廓要更大一些。與之相反地,較小的粗糙值顯示出的鏡面反射輪廓則更小更銳利。

首先我們來看一下頂點着色器,以便後續主要介紹的片元着色器可以順利進行:

#version 450

layout (location = 0) in vec3 inPos;
layout (location = 1) in vec3 inNormal;

layout (binding = 0) uniform UBO 
{
	mat4 projection;
	mat4 model;
	mat4 view;
	vec3 camPos;
} ubo;

layout (location = 0) out vec3 outWorldPos;
layout (location = 1) out vec3 outNormal;

layout(push_constant) uniform PushConsts {
	vec3 objPos;
} pushConsts;

out gl_PerVertex 
{
	vec4 gl_Position;
};

void main() 
{
	vec3 locPos = vec3(ubo.model * vec4(inPos, 1.0));
	outWorldPos = locPos + pushConsts.objPos;
	outNormal = mat3(ubo.model) * inNormal;
	gl_Position =  ubo.projection * ubo.view * vec4(outWorldPos, 1.0);
}

在上述頂點着色器中採用推入式常量來定義了不同位置,並以此來產生多個小球,並在不用小球上產生不同效果。

3.能量守恆

微平面近似法使用了這樣一種形式的能量守恆(Energy Conservation):出射光線的能量永遠不能超過入射光線的能量(發光面除外)。如圖示我們可以看到,隨着粗糙度的上升鏡面反射區域的會增加,但是鏡面反射的亮度卻會下降。如果不管反射輪廓的大小而讓每個像素的鏡面反射強度(Specular Intensity)都一樣的話,那麼粗糙的平面就會放射出過多的能量,而這樣就違背了能量守恆定律。這也就是爲什麼正如我們看到的一樣,光滑平面的鏡面反射更強烈而粗糙平面的反射更昏暗。

爲了遵守能量守恆定律,我們需要對漫反射光和鏡面反射光之間做出明確的區分。當一束光線碰撞到一個表面的時候,它就會分離成一個折射部分和一個反射部分。反射部分就是會直接反射開來而不會進入平面的那部分光線,這就是我們所說的鏡面光照。而折射部分就是餘下的會進入表面並被吸收的那部分光線,這也就是我們所說的漫反射光照。

這裏還有一些細節需要處理,因爲當光線接觸到一個表面的時候折射光是不會立即就被吸收的。通過物理學我們可以得知,光線實際上可以被認爲是一束沒有耗盡就不停向前運動的能量,而光束是通過碰撞的方式來消耗能量。每一種材料都是由無數微小的粒子所組成,這些粒子都能如下圖所示一樣與光線發生碰撞。這些粒子在每次的碰撞中都可以吸收光線所攜帶的一部分或者是全部的能量而後轉變成爲熱量。
在這裏插入圖片描述

一般來說,並非所有能量都會被全部吸收,而光線也會繼續沿着(基本上)隨機的方向發散,然後再和其他的粒子碰撞直至能量完全耗盡或者再次離開這個表面。而光線脫離物體表面後將會協同構成該表面的(漫反射)顏色。不過在基於物理的渲染之中我們進行了簡化,假設對平面上的每一點所有的折射光都會被完全吸收而不會散開。而有一些被稱爲次表面散射(Subsurface Scattering)技術的着色器技術將這個問題考慮了進去,它們顯著的提升了一些諸如皮膚,大理石或者蠟質這樣材質的視覺效果,不過伴隨而來的則是性能下降代價。

對於金屬(Metallic)表面,當討論到反射與折射的時候還有一個細節需要注意。金屬表面對光的反應與非金屬材料(也被稱爲介電質(Dielectrics)材料)表面相比是不同的。它們遵從的反射與折射原理是相同的,但是所有的折射光都會被直接吸收而不會散開,只留下反射光或者說鏡面反射光。亦即是說,金屬表面不會顯示出漫反射顏色。由於金屬與電介質之間存在這樣明顯的區別,因此它們兩者在PBR渲染管線中被區別處理,而我們將在文章的後面進一步詳細探討這個問題。

反射光與折射光之間的這個區別使我們得到了另一條關於能量守恆的經驗結論:反射光與折射光它們二者之間是互斥的關係。無論何種光線,其被材質表面所反射的能量將無法再被材質吸收。因此,諸如折射光這樣的餘下的進入表面之中的能量正好就是我們計算完反射之後餘下的能量。

我們按照能量守恆的關係,首先計算鏡面反射部分,它的值等於入射光線被反射的能量所佔的百分比。然後折射光部分就可以直接由鏡面反射部分計算得出:

float kS = calculateSpecularComponent(...); // 反射/鏡面 部分
float kD = 1.0 - ks;                        // 折射/漫反射 部分

這樣我們就能在遵守能量守恆定律的前提下知道入射光線的反射部分與折射部分所佔的總量了。按照這種方法折射/漫反射與反射/鏡面反射所佔的份額都不會超過1.0,如此就能保證它們的能量總和永遠不會超過入射光線的能量。而這些都是我們在前面的光照教程中沒有考慮的問題。

4.BRDF

在這裏插入圖片描述
BRDF,或者說雙向反射分佈函數,它接受入射(光)方向ωi,出射(觀察)方向ωo,平面法線n以及一個用來表示微平面粗糙程度的參數a作爲函數的輸入參數。BRDF可以近似的求出每束光線對一個給定了材質屬性的平面上最終反射出來的光線所作出的貢獻程度。舉例來說,如果一個平面擁有完全光滑的表面(比如鏡面),那麼對於所有的入射光線ωi(除了一束以外)而言BRDF函數都會返回0.0 ,只有一束與出射光線ωo擁有相同(被反射)角度的光線會得到1.0這個返回值。

BRDF基於我們之前所探討過的微平面理論來近似的求得材質的反射與折射屬性。對於一個BRDF,爲了實現物理學上的可信度,它必須遵守能量守恆定律,也就是說反射光線的總和永遠不能超過入射光線的總量。嚴格上來說,同樣採用ωi和ωo作爲輸入參數的 Blinn-Phong光照模型也被認爲是一個BRDF。然而由於Blinn-Phong模型並沒有遵循能量守恆定律,因此它不被認爲是基於物理的渲染。現在已經有很好幾種BRDF都能近似的得出物體表面對於光的反應,但是幾乎所有實時渲染管線使用的都是一種被稱爲Cook-Torrance BRDF模型。

Cook-Torrance BRDF兼有漫反射和鏡面反射兩個部分:
在這裏插入圖片描述
這裏的kd是早先提到過的入射光線中被折射部分的能量所佔的比率,而ks是被反射部分的比率。BRDF的左側表示的是漫反射部分,這裏用flambert來表示。它被稱爲Lambertian漫反射,這和我們之前在漫反射着色中使用的常數因子類似,用如下的公式來表示:
在這裏插入圖片描述
表示表面顏色(回想一下漫反射表面紋理)。除以π是爲了對漫反射光進行標準化,因爲前面含有BRDF的積分方程是受π影響的。

  • 這個Lambertian漫反射和我們之前經常使用的漫反射到底有什麼關係:之前我們是用表面法向量與光照方向向量進行點乘,然後再將結果與平面顏色相乘得到漫反射參數。點乘依然還在,但是卻不在BRDF之內,而是轉變成爲了Lo積分末公式末尾處的n⋅ωi 。
    目前存在着許多不同類型的模型來實現BRDF的漫反射部分,大多看上去都相當真實,但是相應的運算開銷也非常的昂貴。不過按照Epic公司給出的結論,Lambertian漫反射模型已經足夠應付大多數實時渲染的用途了。

BRDF的鏡面反射部分要稍微更高級一些,它的形式如下所示:
在這裏插入圖片描述

Cook-Torrance BRDF的鏡面反射部分包含三個函數,此外分母部分還有一個標準化因子 。字母D,F與G分別代表着一種類型的函數,各個函數分別用來近似的計算出表面反射特性的一個特定部分。三個函數分別爲正態分佈函數(Normal Distribution Function)菲涅爾方程(Fresnel Rquation)幾何函數(Geometry Function)

  • 正態分佈函數:估算在受到表面粗糙度的影響下,取向方向與中間向量一致的微平面的數量。這是用來估算微平面的主要函數。
  • 幾何函數:描述了微平面自成陰影的屬性。當一個平面相對比較粗糙的時候,平面表面上的微平面有可能擋住其他的微平面從而減少表面所反射的光線。
  • 菲涅爾方程:菲涅爾方程描述的是在不同的表面角下表面所反射的光線所佔的比率。

以上的每一種函數都是用來估算相應的物理參數的,而且你會發現用來實現相應物理機制的每種函數都有不止一種形式。它們有的非常真實,有的則性能高效。你可以按照自己的需求任意選擇自己想要的函數的實現方法。

4.1 正態分佈函數

正態分佈函數D,或者說鏡面分佈,從統計學上近似的表示了與某些(中間)向量h取向一致的微平面的比率。舉例來說,假設給定向量h,如果我們的微平面中有35%與向量h取向一致,則正態分佈函數或者說NDF將會返回0.35。目前有很多種NDF都可以從統計學上來估算微平面的總體取向度,只要給定一些粗糙度的參數以及一個我們馬上將會要用到的參數Trowbridge-Reitz GGX:
在這裏插入圖片描述
在這裏h表示用來與平面上微平面做比較用的中間向量,而a表示表面粗糙度。
如果我們把h當成是不同粗糙度參數下,平面法向量和光線方向向量之間的中間向量的話,我們可以得到如下圖示的效果:

當粗糙度很低(也就是說表面很光滑)的時候,與中間向量取向一致的微平面會高度集中在一個很小的半徑範圍內。由於這種集中性,NDF最終會生成一個非常明亮的斑點。但是當表面比較粗糙的時候,微平面的取向方向會更加的隨機。你將會發現與h向量取向一致的微平面分佈在一個大得多的半徑範圍內,但是同時較低的集中性也會讓我們的最終效果顯得更加灰暗。
在這裏插入圖片描述
使用GLSL代碼編寫的Trowbridge-Reitz GGX正態分佈函數是下面這個樣子的:

// 正態分佈函數
float D_GGX(float dotNH, float roughness)
{
	float alpha = roughness * roughness;
	float alpha2 = alpha * alpha;
	float denom = dotNH * dotNH * (alpha2 - 1.0) + 1.0;
	return (alpha2)/(PI * denom*denom); 
}

4.2 幾何函數

幾何函數從統計學上近似的求得了微平面間相互遮蔽的比率,這種相互遮蔽會損耗光線的能量。
在這裏插入圖片描述
與NDF類似,幾何函數採用一個材料的粗糙度參數作爲輸入參數,粗糙度較高的表面其微平面間相互遮蔽的概率就越高。我們將要使用的幾何函數是GGX與Schlick-Beckmann近似的結合體,因此又稱爲Schlick-GGX:
在這裏插入圖片描述
使用GLSL編寫的幾何函數代碼如下:

// 幾何函數
float G_SchlicksmithGGX(float dotNL, float dotNV, float roughness)
{
	float r = (roughness + 1.0);
	float k = (r*r) / 8.0;
	float GL = dotNL / (dotNL * (1.0 - k) + k);
	float GV = dotNV / (dotNV * (1.0 - k) + k);
	return GL * GV;
}

4.3 菲涅爾方程

菲涅爾方程描述的是被反射的光線對比光線被折射的部分所佔的比率,這個比率會隨着我們觀察的角度不同而不同。當光線碰撞到一個表面的時候,菲涅爾方程會根據觀察角度告訴我們被反射的光線所佔的百分比。利用這個反射比率和能量守恆原則,我們可以直接得出光線被折射的部分以及光線剩餘的能量。

當垂直觀察的時候,任何物體或者材質表面都有一個基礎反射率(Base Reflectivity),但是如果以一定的角度往平面上看的時候所有反光都會變得明顯起來。你可以自己嘗試一下,用垂直的視角觀察你自己的木製/金屬桌面,此時一定只有最基本的反射性。但是如果你從近乎90度(譯註:應該是指和法線的夾角)的角度觀察的話反光就會變得明顯的多。如果從理想的90度視角觀察,所有的平面理論上來說都能完全的反射光線。這種現象因菲涅爾而聞名,並體現在了菲涅爾方程之中。

菲涅爾方程是一個相當複雜的方程式,不過幸運的是菲涅爾方程可以用Fresnel-Schlick近似法求得近似解:
在這裏插入圖片描述
F0表示平面的基礎反射率,它是利用所謂折射指數(Indices of Refraction)或者說IOR計算得出的。然後正如你可以從球體表面看到的那樣,我們越是朝球面掠角的方向上看(此時視線和表面法線的夾角接近90度)菲涅爾現象就越明顯,反光就越強:
在這裏插入圖片描述

使用GLSL編寫的菲涅爾方程代碼如下:

// 菲涅爾方程
vec3 F_Schlick(float cosTheta, float metallic)
{
	vec3 F0 = mix(vec3(0.04), materialcolor(), metallic); // * material.specular
	vec3 F = F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); 
	return F;    
}

使用GLSL編寫的雙向反射分佈函數(BRDF)代碼如下:

// 雙向反射分佈函數,它的作用是基於表面材質屬性來對入射輻射率進行縮放或者加權
vec3 BRDF(vec3 L, vec3 V, vec3 N, float metallic, float roughness)
{
	// 預先計算向量和點積	
	vec3 H = normalize (V + L);
	float dotNV = clamp(dot(N, V), 0.0, 1.0);
	float dotNL = clamp(dot(N, L), 0.0, 1.0);
	float dotLH = clamp(dot(L, H), 0.0, 1.0);
	float dotNH = clamp(dot(N, H), 0.0, 1.0);

	// 白色光色
	vec3 lightColor = vec3(1.0);

	vec3 color = vec3(0.0);

	if (dotNL > 0.0)
	{
		float rroughness = max(0.05, roughness);
		// D = 正態分佈函數 (微觀層面的分佈)
		float D = D_GGX(dotNH, roughness); 
		// G = 幾何函數 (Microfacets shadowing)
		float G = G_SchlicksmithGGX(dotNL, dotNV, roughness);
		// F = 菲涅爾方程 (反射率取決於入射角)
		vec3 F = F_Schlick(dotNV, metallic);

		vec3 spec = D * F * G / (4.0 * dotNL * dotNV);

		color += spec * dotNL * lightColor;
	}

	return color;
}

整體片元着色器如下:

#version 450

layout (location = 0) in vec3 inWorldPos;
layout (location = 1) in vec3 inNormal;

layout (binding = 0) uniform UBO 
{
	mat4 projection;
	mat4 model;
	mat4 view;
	vec3 camPos;
} ubo;

layout (binding = 1) uniform UBOShared {
	vec4 lights[4];
} uboParams;

layout (location = 0) out vec4 outColor;

layout(push_constant) uniform PushConsts {
	layout(offset = 12) float roughness;
	layout(offset = 16) float metallic;
	layout(offset = 20) float r;
	layout(offset = 24) float g;
	layout(offset = 28) float b;
} material;

const float PI = 3.14159265359;

//#define ROUGHNESS_PATTERN 1

vec3 materialcolor()
{
	return vec3(material.r, material.g, material.b);
}

// 正態分佈函數
float D_GGX(float dotNH, float roughness)
{
	float alpha = roughness * roughness;
	float alpha2 = alpha * alpha;
	float denom = dotNH * dotNH * (alpha2 - 1.0) + 1.0;
	return (alpha2)/(PI * denom*denom); 
}

// 幾何函數
float G_SchlicksmithGGX(float dotNL, float dotNV, float roughness)
{
	float r = (roughness + 1.0);
	float k = (r*r) / 8.0;
	float GL = dotNL / (dotNL * (1.0 - k) + k);
	float GV = dotNV / (dotNV * (1.0 - k) + k);
	return GL * GV;
}

// 菲涅爾方程
vec3 F_Schlick(float cosTheta, float metallic)
{
	vec3 F0 = mix(vec3(0.04), materialcolor(), metallic); // * material.specular
	vec3 F = F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); 
	return F;    
}

// 雙向反射分佈函數,它的作用是基於表面材質屬性來對入射輻射率進行縮放或者加權
vec3 BRDF(vec3 L, vec3 V, vec3 N, float metallic, float roughness)
{
	// 預先計算向量和點積	
	vec3 H = normalize (V + L);
	float dotNV = clamp(dot(N, V), 0.0, 1.0);
	float dotNL = clamp(dot(N, L), 0.0, 1.0);
	float dotLH = clamp(dot(L, H), 0.0, 1.0);
	float dotNH = clamp(dot(N, H), 0.0, 1.0);

	// 白色光色
	vec3 lightColor = vec3(1.0);

	vec3 color = vec3(0.0);

	if (dotNL > 0.0)
	{
		float rroughness = max(0.05, roughness);
		// D = 正態分佈函數 (微觀層面的分佈)
		float D = D_GGX(dotNH, roughness); 
		// G = 幾何函數 (Microfacets shadowing)
		float G = G_SchlicksmithGGX(dotNL, dotNV, roughness);
		// F = 菲涅爾方程 (反射率取決於入射角)
		vec3 F = F_Schlick(dotNV, metallic);

		vec3 spec = D * F * G / (4.0 * dotNL * dotNV);

		color += spec * dotNL * lightColor;
	}

	return color;
}

void main()
{		  
	vec3 N = normalize(inNormal);
	vec3 V = normalize(ubo.camPos - inWorldPos);

	float roughness = material.roughness;

	// 基於頂點位置將條紋圖案添加到粗糙度中(效果對比用)
#ifdef ROUGHNESS_PATTERN
	roughness = max(roughness, step(fract(inWorldPos.y * 2.02), 0.5));
#endif

	// 鏡面的貢獻
	vec3 Lo = vec3(0.0);
	for (int i = 0; i < uboParams.lights.length(); i++) {
		vec3 L = normalize(uboParams.lights[i].xyz - inWorldPos);
		Lo += BRDF(L, V, N, material.metallic, roughness);
	};

	// 考慮環境光
	vec3 color = materialcolor() * 0.02;
	color += Lo;

	// Gamma矯正
	color = pow(color, vec3(0.4545));

	outColor = vec4(color, 1.0);
}

編譯shader,運行,可看到如下效果:
在這裏插入圖片描述
,這樣看來效果不是太明顯,因此可以將片元着色器中開啓基於頂點位置將條紋圖案添加到粗糙度:

define ROUGHNESS_PATTERN 1

編譯shader後運行,調整視角,可見:
在這裏插入圖片描述

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