Vulkan_法線映射、視差映射、陡視差映射和視差遮擋映射

本部分主要實現基於紋理信息的多種紋理映射方法:法向映射、視差映射、陡視差映射和視差遮擋映射(質量最好,性能最差)來模擬深度。

貼圖技術

首先,本部分場景僅是簡單的創建一個平面及加載紋理,基本的vulkan加載創建等就不再囉嗦,直接進入主題GLSL實現。

下面的頂點和片元着色器是視差映射和自陰影的基礎模板:頂點着色器把光照向量和攝像機向量變換到切空間。片元着色器調用視差映射的相關函數,然後計算自陰影係數,並計算最終光照後的顏色值。

首先來看一下:頂點着色器

#version 450

layout (location = 0) in vec3 inPos;
layout (location = 1) in vec2 inUV;
layout (location = 2) in vec3 inNormal;
layout (location = 3) in vec3 inTangent;
layout (location = 4) in vec3 inBiTangent;

layout (binding = 0) uniform UBO 
{
	mat4 projection;
	mat4 view;
	mat4 model;
	vec4 lightPos;
	vec4 cameraPos;
} ubo;

layout (location = 0) out vec2 outUV;
layout (location = 1) out vec3 outTangentLightPos;
layout (location = 2) out vec3 outTangentViewPos;
layout (location = 3) out vec3 outTangentFragPos;

void main(void) 
{
	gl_Position = ubo.projection * ubo.view * ubo.model * vec4(inPos, 1.0f);
	outTangentFragPos = vec3(ubo.model * vec4(inPos, 1.0));   
	outUV = inUV;
		
	vec3 T = normalize(mat3(ubo.model) * inTangent);
	vec3 B = normalize(mat3(ubo.model) * inBiTangent);
	vec3 N = normalize(mat3(ubo.model) * inNormal);
	mat3 TBN = transpose(mat3(T, B, N));

	outTangentLightPos = TBN * ubo.lightPos.xyz;
	outTangentViewPos  = TBN * ubo.cameraPos.xyz;
	outTangentFragPos  = TBN * outTangentFragPos;
}

此處很簡單,都是之前有介紹過的內容,其中TBN矩陣的轉換原理不明白的可以參照前文GLSL-TBN矩陣

接下來我們逐步來看片元着色器,其中入口main及管線數據如下:

#version 450

layout (binding = 1) uniform sampler2D sColorMap;
layout (binding = 2) uniform sampler2D sNormalHeightMap;

layout (binding = 3) uniform UBO 
{
	float heightScale;
	float parallaxBias;
	float numLayers;
	int mappingMode;
} ubo;

layout (location = 0) in vec2 inUV;
layout (location = 1) in vec3 inTangentLightPos;
layout (location = 2) in vec3 inTangentViewPos;
layout (location = 3) in vec3 inTangentFragPos;

layout (location = 0) out vec4 outColor;

void main(void) 
{
	vec3 V = normalize(inTangentViewPos - inTangentFragPos);
	vec2 uv = inUV;

	if (ubo.mappingMode == 0) {
		//"常規貼圖"
		outColor = texture(sColorMap, inUV);
	} else {
	    //case 1: //"法線貼圖"
		switch(ubo.mappingMode) {
			case 2:	//"視差貼圖"
				uv = parallaxMapping(inUV, V);
				break;
			case 3:	//"陡峭視差貼圖"
				uv = steepParallaxMapping(inUV, V);
				break;
			case 4: //"視差阻塞貼圖"
				uv = parallaxOcclusionMapping(inUV, V);
				break;
		}

		//在(可能)丟棄前執行取樣:這是爲了避免非均勻控制流中的隱式導數。
		vec3 normalHeightMapLod = textureLod(sNormalHeightMap, uv, 0.0).rgb;
		vec3 color = texture(sColorMap, uv).rgb;

		//在紋理座標異常處捨棄片段
		if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
			discard;
		}

		vec3 N = normalize(normalHeightMapLod * 2.0 - 1.0);
		vec3 L = normalize(inTangentLightPos - inTangentFragPos);
		vec3 R = reflect(-L, N);
		vec3 H = normalize(L + V);  
   
		vec3 ambient = 0.3 * color; //環境光
		vec3 diffuse = max(dot(L, N), 0.0) * color;//漫反射
		vec3 specular = vec3(0.15) * pow(max(dot(N, H), 0.0), 32.0);//鏡面反射

		outColor = vec4(ambient + diffuse + specular, 1.0f);
	}	
}

此處定義了多中貼圖映射的方法入口。

1、常規貼圖(texture mapping)

其中最簡單的便是常規貼圖(ubo.mappingMode == 0),直接採樣紋理貼圖:
可見如下效果:
在這裏插入圖片描述

2、法線映射(Normal mapping)

在三維計算機圖形學中,法線貼圖(Normal mapping)是一種模擬凹凸處光照效果的技術,是凸凹貼圖的一種實現。法線貼圖可以在不添加多邊形的前提下,爲模型添加細節。常見的使用場景是爲低多邊形模型改善外觀、添加細節,此時的法線貼圖一般根據高多邊形模型或高度貼圖生成。
法線貼圖通常以普通RGB圖像的形式存儲,其中的R、G、B分量分別對應法線的X、Y、Z座標。

在我們的程序中,從main中我們可以看到,在法線映射的時候(ubo.mappingMode == 1),我們直接textureLod使用顯式的細節級別執行紋理查找從高度貼圖中進行採樣。執行結果如下圖:
在這裏插入圖片描述
從上圖中,我們可以明顯的看出在相對於常規貼圖,法線貼圖具有明顯的凹凸改善效果。

3、視差映射(parallax mapping)

視差映射技術的主要任務是修改紋理座標,讓平面看起來像是立體的。主要計算都是在Fragment Shader中進行。看看下面的圖片。水平線0.0表示完全沒有凹陷的深度,水平線1.0表示凹陷的最大深度。實際的幾何體並沒改變,其實一直都在0.0水平線上。圖中的曲線代表了高度圖中存儲的高度數據。

設當前點片元是圖片中用黃色方塊高亮出來的那個點,這個點的紋理座標是T0。向量V是從攝像機到點的方向向量。用座標T0在高度圖上採樣,你能得到這個點的高度值H(T0)=0.55。這個值不是0,所以點並不是在表面上,而是凹陷下去的。所以你得把向量V繼續延長直到與高度圖定義出來的表面最近的一個交點。這個交點我們說它的深度就是H(T1),它的紋理座標就是T1。所以我們就應該用T1的紋理座標去對顏色和法線貼圖進行採樣。

所以說,所有視差映射技術的主要目的,就是要精確的計算攝像機的向量V和高度圖定義出來的表面的交點
在這裏插入圖片描述
視差映射的計算是在切空間進行的(跟法線映射一樣)。所以指向光源的向量(L)和指向攝像機的向量(V)應該先被變換到切空間。在用視差映射計算出來新的紋理座標之後,你可以用這個座標來計算自陰影,可以從漫反射貼圖讀取顏色以及從發現貼圖讀取法線。

在着色器中代碼爲

//"視差貼圖"
vec2 parallaxMapping(vec2 uv, vec3 viewDir) 
{
	float height = 1.0 - textureLod(sNormalHeightMap, uv, 0.0).a;
	vec2 p = viewDir.xy * (height * (ubo.heightScale * 0.5) + ubo.parallaxBias) / viewDir.z;
	return uv - p;  
}

運行,可見如下效果:
在這裏插入圖片描述
可以看到像比如法線貼圖,視差貼圖更進一步的處理了圖片的凹凸細節。

4、陡視差映射(steep parallax mapping)

陡峭視差映射,不像簡單的視差映射近似,並不只是簡單粗暴的對紋理座標進行偏移而不檢查合理性和關聯性,會檢查結果是否接近於正確值。這種方法的核心思想是把表面的深度切分成等距的若干層。然後從最頂端的一層開始採樣高度圖,每一次會沿着V的方向偏移紋理座標。如果點已經低於了表面(當前的層的深度大於採樣出的深度),停止檢查並且使用最後一次採樣的紋理座標作爲結果。

陡峭視差映射的工作方式在下面的圖片上舉例。深度被分割成8個層,每層的高度值是0.125。每層的紋理座標偏移是V.xy/V.z * scale/numLayers。從頂層黃色方塊的位置開始檢查,下面是手動計算步驟:

  1. 層的深度爲0,高度圖深度H(T0)大約爲0.75。採樣到的深度大於層的深度,所以開始下一次迭代。
  2. 沿着V方向偏移紋理座標,選定下一層。層深度爲0.125,高度圖深度H(T1)大約爲0.625。採樣到的深度大於層的深度,所以開始下一次迭代。
  3. 沿着V方向偏移紋理座標,選定下一層。層深度爲0.25,高度圖深度H(T2)大約爲0.4。採樣到的深度大於層的深度,所以開始下一次迭代。
  4. 沿着V方向偏移紋理座標,選定下一層。層深度爲0.375,高度圖深度H(T3)大約爲0.2。採樣到的深度小於層的深度,所以向量V上的當前點在表面之下。我們找到了紋理座標Tp=T3是實際交點的近似點。

在這裏插入圖片描述
從上圖你能看到,其實紋理座標T3還是離交點挺遠的。但是這個紋理座標已經比視差映射要接近正確結果了。如果你想得到更精確的結果,增加層的數量。

陡峭視差映射的主要優勢在於它把深度切分成了有限數量的層。如果層數很多,那性能就會低。但如果層數少,就會有明顯的鋸齒現象產生,就像下面這張圖一樣。你也可以根據攝像機向量V和多邊形法向N之間的夾角來動態的決定層的數量。

//"陡峭視差貼圖"
vec2 steepParallaxMapping(vec2 uv, vec3 viewDir) 
{
	float layerDepth = 1.0 / ubo.numLayers;
	float currLayerDepth = 0.0;
	vec2 deltaUV = viewDir.xy * ubo.heightScale / (viewDir.z * ubo.numLayers);
	vec2 currUV = uv;
	float height = 1.0 - textureLod(sNormalHeightMap, currUV, 0.0).a;
	for (int i = 0; i < ubo.numLayers; i++) {
		currLayerDepth += layerDepth;
		currUV -= deltaUV;
		height = 1.0 - textureLod(sNormalHeightMap, currUV, 0.0).a;
		if (height < currLayerDepth) {
			break;
		}
	}
	return currUV;
}

運行,可見這種視圖看起來會更加逼真,顯示效果會好點:
在這裏插入圖片描述

5、視差遮擋映射( parallax occlusion mapping)

視差遮蔽映射(POM)是陡峭視差映射的另一個改進版本。
在這裏插入圖片描述
視差遮蔽映射簡單的對陡峭視差映射的結果進行插值。請看上圖,POM使用相交之後的層深度(0.375,陡峭視差映射停止迭代的層),上一個採樣深度H(T2)和下一個採樣深度H(T3)。從圖片中你能看到,視差遮蔽映射的插值結果是在視向量V和H(T2)和H(T3)高度的連線的交點上。這個交點已經足夠接近實際交點(標記爲綠色的點)了。

圖片對應的手動計算步驟:

  1. nextHeight = H(T3) - currentLayerHeight
  2. prevHeight = H(T2) - (currentLayerHeight - layerHeight)
  3. weight = nextHeight / (nextHeight - prevHeight)
  4. Tp = T(T2) weight + T(T3) (1.0 - weight)

視差遮蔽映射可以使用相對較少的採樣次數產生很好的結果。但視差遮蔽映射比浮雕視差映射更容易跳過高度圖中的小細節,也更容易在高度圖數據產生大幅度的變化時得到錯誤的結果。

//"視差遮蔽貼圖"
vec2 parallaxOcclusionMapping(vec2 uv, vec3 viewDir) 
{
	float layerDepth = 1.0 / ubo.numLayers;
	float currLayerDepth = 0.0;
	vec2 deltaUV = viewDir.xy * ubo.heightScale / (viewDir.z * ubo.numLayers);
	vec2 currUV = uv;
	float height = 1.0 - textureLod(sNormalHeightMap, currUV, 0.0).a;
	for (int i = 0; i < ubo.numLayers; i++) {
		currLayerDepth += layerDepth;
		currUV -= deltaUV;
		height = 1.0 - textureLod(sNormalHeightMap, currUV, 0.0).a;
		if (height < currLayerDepth) {
			break;
		}
	}
	vec2 prevUV = currUV + deltaUV;
	float nextDepth = height - currLayerDepth;
	float prevDepth = 1.0 - textureLod(sNormalHeightMap, prevUV, 0.0).a - currLayerDepth + layerDepth;
	return mix(currUV, prevUV, nextDepth / (nextDepth - prevDepth));
}

在這裏插入圖片描述

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