1. 程式人生 > >Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第十九章:法線貼圖

Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第十九章:法線貼圖

inverse 它的 dex matrix 情況 nor 內存 unit prism

原文:Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第十九章:法線貼圖

學習目標

  1. 理解為什麽需要法線貼圖;
  2. 學習法線貼圖如何保存;
  3. 學習法線貼圖如何創建;
  4. 學習法線貼圖中的法向量的坐標系統是如何與物體空間的三角形的坐標系統關聯的;
  5. 學習如何在頂點和像素著色器中實現法線貼圖。


1 使用法線貼圖的原因

找到一種方法在光滑的平面上,顯示出更多的細節(比如粗糙的磚塊)。
如果使用曲面細分是可以增加實際的細節的,但是我們還是需要一種方法來指定新增加的頂點的法向量。如果直接根據光照來烘焙紋理,這種方法如果燈光移動後,效果就會出問題。
所以要使用法線貼圖:
技術分享圖片



2 法線貼圖

一個法線貼圖是一張紋理,其每個通道保存x,y,z坐標值,所以每個像素保存了一個法線向量:
技術分享圖片
一個單位向量其每個組件值的值域為[?1, 1],我們可以經過下面的運算,將其轉換到0-255:
技術分享圖片
如果要再將其準換回[?1, 1],就對每個通道執行:
技術分享圖片

我們不需要自己去做壓縮操作,PhotoShop的插件可以幫忙把圖像轉化成法線貼圖。但是在著色器中,我們需要自己做解壓縮操作:

float3 normalT = gNormalMap.Sample(gTriLinearSam, pin.Tex);

normalT每個組件的值域為0 ≤ r, g, b ≤ 1;所以該函數已經為我們做了一半的解壓縮操作,我們只需要再將其轉換到[?1, 1]即可:
技術分享圖片

// Uncompress each component from [0,1] to [-1,1].
normalT = 2.0f*normalT - 1.0f;

Photoshop的插件可以在 https://developer.nvidia.com/nvidia-texture-tools-adobe-photoshop 下載到;還有其它一些創建法線貼圖的工具:http://www.crazybump.com/ 和 http://shadermap.com/home/ ;還有一些工具可以從高分辨率模型上創建法線貼圖:https://www.nvidia.com/object/melody_home.html 。

如果你要使用壓縮紋理格式保存法線貼圖,使用BC7 (DXGI_FORMAT_BC7_UNORM)格式是最好的效果,它可以減少由壓縮法線貼圖造成的錯誤。對於BC6和BC7格式,DirectX SDK有一個例子叫“BC6HBC7EncoderDecoder11”,它可以將你的法線貼圖轉換到BC6或者BC7。



3 紋理/切線空間

紋理通過平移和旋轉後貼到三角形上後,合並三角形的法向量N,我們在三角形所在的平面上生成一個3D TBN-basis的坐標系,叫做紋理空間或者切線空間。註意該空間對於不同三角形是不一樣的。
技術分享圖片
法線貼圖的法向量是在紋理空間定義的,但是燈光是在世界坐標系下的,所以我們需要將它們轉換到同一個坐標系下才能正確計算光照。所以首先我們要紋理空間關聯到它的物體局部坐標系中。令v0, v1, 和 v2定義一個3D三角形的三個頂點,對應的紋理坐標為(u0, v0), (u1, v1), 和(u2, v2)。令e0 = v1 ? v0和e1 = v2 ? v0是三角形的兩條邊,並且對於的紋理三角形的兩條邊:(Δu0, Δv0) = (u1 ? u0, v1 ? v0) 和 (Δu1, Δv1) = (u2 ? u0, v2 ? v0) :
技術分享圖片
表達了向量坐標關聯到物體空間,我們得到矩陣方程:
技術分享圖片
我們知道三角形頂點的物體空間坐標,也知道邊的物體空間坐標:
技術分享圖片
我們也知道紋理坐標:
技術分享圖片
解T和B的物體空間坐標:
技術分享圖片
綜上所述,我們使用逆矩陣
技術分享圖片技術分享圖片
向量T和B在物體坐標系中不是單位長度,如果有扭曲,它們也不是正交的。
T,V和N向量代表了切線,次法線和法線向量。



4 頂點的切線空間

上一節,我們衍生出了逐三角形的切線空間,如果我們使用它來進行法線貼圖映射,物體表面會產生三角形化的效果。所以我們定義逐頂點的切向量,然後進行均值計算來模擬光滑平面:
1、任意頂點V的切向量T通過所有共享它的三角形切向量的平均值來獲取;
2、任意頂點的次切向量B通過所有共享它的三角形次切向量的平均值來獲取。

通常情況下,進行均值運算後,TBN-bases需要標準正交化,所以向量要進行正交運算和轉換為單位長度。這個通常使用Gram-Schmidt步驟。代碼可以在下面網站中找到,對任意三角網格創建逐向量的切線空間:http://www.terathon.com/code/tangent.html 。

在我們的系統中,我們不需要直接保存次切向量B到內存,可以通過計算獲得B = N × T,所以頂點結構為:

struct Vertex
{
	XMFLOAT3 Pos;
	XMFLOAT3 Normal;
	XMFLOAT2 Tex;
	XMFLOAT3 TangentU;
};

回顧我們在GeometryGenerator中創建網格的步驟,計算紋理空間的切線T。向量Y在盒子或者格子網格中非常容易計算。對於圓柱體和球體,每個頂點的切向量可以通過兩個點P(u, v)然後計算?p/?u來獲得(其中u使用的是u的紋理坐標)。
技術分享圖片



5 切線空間和物體空間之間的轉換

現在網格的每個頂點我們有一個標準正交的TBN-basis,並且關聯到物體空間。我們可以通過下面的變換矩陣進行轉化:
技術分享圖片
因為它是標準正交的,所以它的逆矩陣就是它的轉置矩陣,所以從物體空間到切線空間為:
技術分享圖片
在著色器代碼中,我們需要將它們轉換到世界坐標系中:
技術分享圖片
因為矩陣的乘法具有結合律,所以:
技術分享圖片
並且:
技術分享圖片
所以要從切線空間轉換到世界坐標系,我們只需要在世界坐標系下描述切線方向軸,即可得到變換矩陣。

因為我們只需要轉換向量,所以我們只需要一個3x3矩陣。



6 法線貼圖的著色器代碼

我們總結一下實現的步驟:
1、通過各種工具或者軟件創建法線貼圖並保存到圖像文件,在程序初始化的時候讀取文件創建紋理;
2、對每個三角形,計算它的切向量T;
3、在頂點著色器中,轉換法向量和切向量到世界坐標系中,並且輸出到像素著色器;
4、使用差值後的切向量和法向量,我們在三角形表面的每個像素點創建TBN-basis,然後用它們將采樣到的法向量變換到世界坐標系。然後就可以使用它來進行光照計算。

為了幫助我們實現法線貼圖,我們在Common.hlsl添加了下面的函數:

//--------------------------------------------------------------------
// Transforms a normal map sample to world space.
//--------------------------------------------------------------------
float3 NormalSampleToWorldSpace(float3 normalMapSample,
	float3 unitNormalW,
	float3 tangentW)
{
	// Uncompress each component from [0,1] to [-1,1].
	float3 normalT = 2.0f*normalMapSample - 1.0f;
	
	// Build orthonormal basis.
	float3 N = unitNormalW;
	float3 T = normalize(tangentW - dot(tangentW, N)*N);
	float3 B = cross(N, T);
	float3x3 TBN = float3x3(T, B, N);
	
	// Transform from tangent space to world space.
	float3 bumpedNormalW = mul(normalT, TBN);
	return bumpedNormalW;
}

這個函數在像素著色器中可以這樣使用:

float3 normalMapSample = gNormalMap.Sample(samLinear, pin.Tex).rgb;
float3 bumpedNormalW = NormalSampleToWorldSpace(
	normalMapSample,
	pin.NormalW,
	pin.TangentW);

可能有兩行不太好理解的是:

float3 N = unitNormalW;
float3 T = normalize(tangentW - dot(tangentW, N)*N);

結果差值運算後,切向量和法向量可能不是標準正交的,這個代碼確保T和N是標準正交的
技術分享圖片

完整的著色器代碼如下:

//*********************************************************************
// Default.hlsl by Frank Luna (C) 2015 All Rights Reserved.
//*********************************************************************
// Defaults for number of lights.
#ifndef NUM_DIR_LIGHTS
	#define NUM_DIR_LIGHTS 3
#endif
#ifndef NUM_POINT_LIGHTS
	#define NUM_POINT_LIGHTS 0
#endif
#ifndef NUM_SPOT_LIGHTS
	#define NUM_SPOT_LIGHTS 0
#endif

// Include common HLSL code.
#include “Common.hlsl”

struct VertexIn
{
	float3 PosL : POSITION;
	float3 NormalL : NORMAL;
	float2 TexC : TEXCOORD;
	float3 TangentU : TANGENT;
};

struct VertexOut
{
	float4 PosH : SV_POSITION;
	float3 PosW : POSITION;
	float3 NormalW : NORMAL;
	float3 TangentW : TANGENT;
	float2 TexC : TEXCOORD;
};

VertexOut VS(VertexIn vin)
{
	VertexOut vout = (VertexOut)0.0f;
	
	// Fetch the material data.
	MaterialData matData = gMaterialData[gMaterialIndex];
	
	// Transform to world space.
	float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
	vout.PosW = posW.xyz;
	
	// Assumes nonuniform scaling; otherwise, need to use
	// inverse-transpose of world matrix.
	vout.NormalW = mul(vin.NormalL, (float3x3)gWorld);
	vout.TangentW = mul(vin.TangentU, (float3x3)gWorld);
	
	// Transform to homogeneous clip space.
	vout.PosH = mul(posW, gViewProj);
	
	// Output vertex attributes for interpolation across triangle.
	float4 texC = mul(float4(vin.TexC, 0.0f, 1.0f), gTexTransform);
	vout.TexC = mul(texC, matData.MatTransform).xy;
	
	return vout;
}

float4 PS(VertexOut pin) : SV_Target
{
	// Fetch the material data.
	MaterialData matData = gMaterialData[gMaterialIndex];
	float4 diffuseAlbedo = matData.DiffuseAlbedo;
	float3 fresnelR0 = matData.FresnelR0;
	float roughness = matData.Roughness;
	uint diffuseMapIndex = matData.DiffuseMapIndex;
	uint normalMapIndex = matData.NormalMapIndex;

	// Interpolating normal can unnormalize it, so renormalize it.
	pin.NormalW = normalize(pin.NormalW);
	float4 normalMapSample = gTextureMaps[normalMapIndex].Sample(gsamAnisotropicWrap, pin.TexC);
	float3 bumpedNormalW = NormalSampleToWorldSpace(
		normalMapSample.rgb, pin.NormalW,
		pin.TangentW);
		
	// Uncomment to turn off normal mapping.
	//bumpedNormalW = pin.NormalW;
	// Dynamically look up the texture in the array.
	diffuseAlbedo *= gTextureMaps[diffuseMapIndex].Sample(gsamAnisotropicWrap, pin.TexC);
	
	// Vector from point being lit to eye.
	float3 toEyeW = normalize(gEyePosW - pin.PosW);
	
	// Light terms.
	float4 ambient = gAmbientLight*diffuseAlbedo;
	
	// Alpha channel stores shininess at per-pixel level.
	const float shininess = (1.0f - roughness) * normalMapSample.a;
	Material mat = { diffuseAlbedo, fresnelR0, shininess };
	float3 shadowFactor = 1.0f;
	float4 directLight = ComputeLighting(gLights, mat, pin.PosW,
	bumpedNormalW, toEyeW, shadowFactor);
	float4 litColor = ambient + directLight;

	// Add in specular reflections.
	float3 r = reflect(-toEyeW, bumpedNormalW);
	float4 reflectionColor = gCubeMap.Sample(gsamLinearWrap, r);
	float3 fresnelFactor = SchlickFresnel(fresnelR0, bumpedNormalW, r);
	litColor.rgb += shininess * fresnelFactor * reflectionColor.rgb;
	
	// Common convention to take alpha from diffuse albedo.
	litColor.a = diffuseAlbedo.a;
	
	return litColor;
}

其中bumpedNormalW不僅用以光照計算,還用以反射計算。另外alpha通道還可以用來保存發光度,用來控制逐像素的發光程度。
技術分享圖片



7 總結

  1. 法線貼圖的策略就是,保存物體的法線到一張紋理中,然後使用逐像素的法線來進行計算;
  2. 法線貼圖就是各個通道來分別保存法向量的x y z,它可以通過多種工具制作生成;
  3. 法線貼圖中的法向量是在紋理坐標系下的,如果要進行光照計算,需要將它轉換到世界坐標系下,TBN-bases可以幫助每個頂點的法向量從紋理坐標轉換到世界坐標系。


8 練習

Introduction to 3D Game Programming with DirectX 12 學習筆記之 --- 第十九章:法線貼圖