1. 程式人生 > >Directx11教程十二之NormalMap(法線貼圖)

Directx11教程十二之NormalMap(法線貼圖)

本教程的構架如下:


本節的構架比前幾個教程的構架多出一個LightClass,還有我的FOV角由90度變為45度, 這個類主要用於儲存AmbientLight,DiffuseLight,LightDirection等光照相關的引數.

一,LightMap(法線貼圖)的簡介.

在介紹法線貼圖的由來前,先看看一個在漫反射光下的有紋理的立方體,
是不是感覺不夠真實?是不是感覺紋理隆起感不夠強?那為什麼隆起感不夠強,因為這個立方體Shader光照的編寫是建立光滑表面的法向量下的

,由光滑表面的法向量建立起的光照效果當然不夠逼真,不能模擬真實世界隆起面的光照效果,自然而然的讓隆起感大打折扣,真實實在不足,

這時有兩種方法可以解決:

第一種方法是:建立起在3D世界空間真正有隆起面的模型,不過這樣這個模型的面數將多的要嚇死人,也許一個普通的坑坑窪窪的牆面都要幾十萬個面,

這是顯示卡效能絕對不允許的。這時候第二種方法應需求而生。

第二種方法就是:建立起一個面的法線貼圖,法線貼圖中的畫素可以說是儲存著那個面每個畫素的法向量(嚴格說僅僅是代表,下面給出原因),給出了面真正隆起時的法向量,依照隆起面的法向量來計算光照效果,將大大增加光照的細節,增加隆起感,真實感.

下面給出法線貼圖的原形:


     看這就是法線貼圖,那麼為什麼法線總體上是顯示藍色的,假設此NormalMap畫素為(rn,gn,bn,an),由於bn比rn和gn大的多,在RGB中佔比大,所以呈現藍色.

看看NormalMap(法線貼圖)的立體體,顯示了每個畫素(rn,gn,bn)表示的法向量(x,y,z):


假設每個畫素代表的單位法向量為(x,y,z),由於單位法向量的長度為1,即x*x+y*y+z*z=1,那麼 -1=<x<=1,  -1<=y<=1,- 1<=z<=1

接下來我們慢慢揭示每個畫素顏色和其代表的單位法向量之間的關係.

這裡紋理每個畫素為(r,g,b), 0<=r<=255,0<=g<=255,0<=b<=255,有同學說D3D11裡畫素顏色不是0.0和1.0之間?怎麼變為0和255了,我這裡寫的是未經壓縮的,D3D11的顏色是經過原來顏色值壓縮的(每個顏色分量除以255).

上面說到  -1=<x<=1,  -1<=y<=1,- 1<=z<=1,單獨拿x出來說,可推出0.0<0.5x+0.5<=1.0, 然後推出     0 =<(0.5x+0.5)*255<=255

也就是 r=(0.5x+0.5)*255;  同理有g=(0.5y+0.5)*255     ;b=(0.5z+0.5)*255;

那麼我們知道了畫素顏色是如何由單位法向量得到的,那怎麼由畫素顏色得到單位法向量呢?很簡單,用逆函式計算

x=2*r/255-1;         y=2*g/255-1;   z=2*b/255-1;

然而在D3D11裡或者說在D3D11Shader裡顏色畫素是經過壓縮的,也就是r,g,b已經除以255了,那麼

x=2*r-1;     y=2*g-1;   z=2*b-1;

終於求出每個畫素的法向量,但是事情還沒結束,因為每個面都有其法向量貼圖,每個法向量貼圖代表的空間都不一樣,

可以看看這張圖,這張圖描述的是每張法線貼圖的切線空間


法線貼圖中用畫素計算出的法向量的值是建立在每個法線貼圖的切線空間的,不可能直接拿來計算,那麼就得想辦法,將法向量從切線空間(texture space/tangent space)變為模型空間(object space),當然,之後還要從模型空間變到世界空間(world space),因為光照的計算是放在世界空間進行的,

那麼怎麼將法向量從切線空間變到模型空間,再變到世界空間呢?

先看看這裡,先假設在切線空間有是三個座標向量,T(單位切向量),B(單位輔助法向量),N(單位法向量),

下面是變換矩陣公式:


那麼問題又來了,我們怎麼得到T向量和B向量呢?

先看圖:



假設一個三角面有頂點v0,v1,v2

e0=v1-v0,e1=v2-v0

e0和e1為3D三角面的兩個邊向量,e0(e0x,e0y,e0z),,e1(e1x,e1y,e1z)

e0和e1分別對應的三角面紋理向量為(△U0,△V0)=(u1-u0),(△U1,△V1)=(u2-u0,v2-v0);

e0=△U0*T+△V0B;

e1=△U1*T+△V1B;

由上面兩條等式變為矩陣形式



兩邊同時乘以的逆矩陣得到



    行列式那章的計算公式:

  

也就是已經一個面的三個頂點的座標和紋理座標,就可以求出T,B向量,

不過這章千萬的注意的是:  法向量n由區域性空間變到世界空間是乘以世界變換矩陣的逆矩陣的轉置矩陣

                                          而切向量t由區域性空間變換到世界空間是乘以世界變換矩陣.

最後貼出我求出切向量和輔助法向量的程式碼,之後在叉乘計算出第三個向量Normal,原來那個normal不要了

void ModelClass::CalculateTangentBinormal(TempVertexType vertex1, TempVertexType vertex2, TempVertexType vertex3,
	VectorType& tangent, VectorType& binormal)
{

	float Edge1[3], Edge2[3];
	float TexEdge1[2], TexEdge2[2];
    
	//計算面的兩個向量
	//邊向量1
	Edge1[0] = vertex2.x - vertex1.x; //E0X
	Edge1[1] = vertex2.y - vertex1.y; //E0Y
	Edge1[2] = vertex2.z - vertex1.z; //E0Z

	//邊向量2
	Edge2[0] = vertex3.x - vertex1.x; //E1X
	Edge2[1] = vertex3.y - vertex1.y; //E1Y
	Edge2[2] = vertex3.z - vertex1.z; //E1Z

	//紋理邊向量1
	TexEdge1[0] = vertex2.u - vertex1.u; //U0
	TexEdge1[1] = vertex2.v - vertex1.v; //V0

	//紋理邊向量2
	TexEdge2[0] = vertex3.u - vertex1.u; //U1
	TexEdge2[1] = vertex3.v - vertex1.v; //V1

	//求出TB在模型空間座標的方程係數
	float den = 1.0f / (TexEdge1[0] * TexEdge2[1] - TexEdge1[1] * TexEdge2[0]);

	//求出Tangent
	tangent.x=den*(TexEdge2[1] * Edge1[0] - TexEdge1[1] * Edge2[0]);
	tangent.y=den*(TexEdge2[1] * Edge1[1] - TexEdge1[1] * Edge2[1]);
	tangent.z=den*(TexEdge2[1] * Edge1[2] - TexEdge1[1] * Edge2[2]);

	//求出Binormal
	binormal.x = den*(-TexEdge2[0] * Edge1[0] + TexEdge1[0] * Edge2[0]);
	binormal.y = den*(-TexEdge2[0] * Edge1[1] + TexEdge1[0] * Edge2[1]);
	binormal.z= den*(-TexEdge2[0] * Edge1[2] + TexEdge1[0] * Edge2[2]);

}
我的Shader程式碼
<pre name="code" class="cpp">Texture2D ShaderTexture[3];  //紋理資源陣列
SamplerState SampleType:register(s0);   //取樣方式

//VertexShader
cbuffer CBMatrix:register(b0)
{
	matrix World;
	matrix View;
	matrix Proj;
	matrix WorldInvTranpose;
};

cbuffer CBLight:register(b1)
{
	float4 AmbientColor;
	float4 DiffuseColor;
	float3 LightDirection;
	float pad;
};

struct VertexIn
{
	float3 Pos:POSITION;
	float2 Tex:TEXCOORD0;  //多重紋理可以用其它數字
	float3 Normal:NORMAL;
	float3 Tangent:TANGENT;
	float3 Binormal:BINORMAL;
};


struct VertexOut
{
	float4 Pos:SV_POSITION;
	float2 Tex:TEXCOORD0;
	float3 Normal_W:NORMAL;
	float3 Tangent_W:TANGENT;
	float3 Binormal_W:BINORMAL;
};


VertexOut VS(VertexIn ina)
{
	VertexOut outa;

	//變換座標到齊次裁剪空間(CVV)
	outa.Pos = mul(float4(ina.Pos,1.0f), World);
	outa.Pos = mul(outa.Pos, View);
	outa.Pos = mul(outa.Pos, Proj);
	outa.Tex= ina.Tex;

	//將法線量由區域性空間變換到世界空間,並進行規格化
	outa.Normal_W = mul(ina.Normal, (float3x3)WorldInvTranpose);
	outa.Normal_W = normalize(outa.Normal_W);

	//將切向量由區域性空間變換到世界空間,並且進行規格化
	outa.Tangent_W = mul(ina.Tangent,(float3x3)World);
	outa.Tangent_W = normalize(outa.Tangent_W);


	//將切向量由區域性空間變換到世界空間,並且進行規格化
	outa.Binormal_W = mul(ina.Binormal, (float3x3)World);
	outa.Binormal_W = normalize(outa.Binormal_W);

	return outa;
}


float4 PS(VertexOut outa) : SV_Target
{
	float4 BasePixel; 
	float3 BumpNormal;  //隆起法向量
	float4 color;

	//增加漫反射光顏色
	color = AmbientColor;

	//求每個畫素的紋理畫素顏色
    BasePixel = ShaderTexture[0].Sample(SampleType, outa.Tex);

	//求每個畫素的隆起法向量(切線空間)
	BumpNormal=(float3)ShaderTexture[1].Sample(SampleType, outa.Tex);
	BumpNormal = (2.0f*BumpNormal) - 1.0f;

	//-----求出TBN矩陣(已經和世界變換矩陣結合在一起)--------
	float3 N = outa.Normal_W;
	float3 T = outa.Tangent_W;
	float3 B = outa.Binormal_W;
    
	//將隆起法向量由切線空間變換到區域性空間,再到世界空間,然後規格化
	BumpNormal= mul(BumpNormal,float3x3(T,B,N));
	BumpNormal = normalize(BumpNormal);

	//求出漫反射因子
	float3 InvLightDirection = -LightDirection;
	float DiffuseFactor = saturate(dot(BumpNormal, InvLightDirection));
    color += DiffuseColor*DiffuseFactor;
	color = saturate(color);

	//乘以基礎紋理顏色
	color = color*BasePixel;

	return color;
}
程式執行結果:

是不是跟比剛開始那個增強了隆起感和真實感,而且三角形面數未曾改變,執行效能很好,法線貼圖可謂是增強隆起感真實感並節省效能的技術啊.

後面我補充兩點:

第一,求出法線貼圖中的法線向量變換到世界空間的兩種寫法,這裡有些歧異,我和教程裡的作者的寫法不同,依照D3D11龍書的我覺得第一種寫法才是

對的,教程作者的是錯的。

第二是,就是教程中計算TBN也是也是錯的,按照D3D11龍書,應該是每個頂點的切向量應該是分享該頂點的所有面的面切向量的平均切向量,就像由面法線量計算頂點法向量那樣,而本教程可能是為了省事,計算出每個三角面的面切向量,然後充當這個三角面的三個頂點的頂點切向量,方法上本質上也是錯的。

第三,D3D11龍書的做法是得到每個頂點的頂點法向量和頂點切向量,然後變換到世界空間在計算出每個畫素的的輔助切向量,求出TBN矩陣,而本教程是完全拋棄了頂點法線量,由錯誤方法的面切向量和麵輔助法向量來計算出頂點法向量,有點無語。

float4 PS(VertexOut outa) : SV_Target
{
	float4 BasePixel; 
	float3 BumpNormal;  //隆起法向量
	float4 color;
	float DiffuseFactor;  //漫反射因子
	float SpecularFactor; //鏡面反射因子
	float4 Specular; //鏡面反射顏色
	float4 SpecularIntensity; //鏡面強度

	//增加漫反射光顏色
	color = AmbientColor;

	//求每個畫素的紋理畫素顏色
    BasePixel = ShaderTexture[0].Sample(SampleType, outa.Tex);


	//求每個畫素的隆起法向量(切線空間)
	BumpNormal=(float3)ShaderTexture[1].Sample(SampleType, outa.Tex);
	BumpNormal = (2.0f*BumpNormal) - 1.0f;


	//-----求出TBN矩陣(已經和世界變換矩陣結合在一起)--------
	float3 N = outa.Normal_W;
	float3 T = outa.Tangent_W;
	float3 B = outa.Binormal_W;
    
	//將隆起法向量由切線空間變換到區域性空間,再到世界空間,然後規格化
	<span style="color:#ff0000;">BumpNormal= mul(BumpNormal,float3x3(T,B,N));
	//BumpNormal = N + BumpNormal.x*T + BumpNormal.y*B;</span>
	BumpNormal = normalize(BumpNormal);

	//求出漫反射因子
	float3 InvLightDirection = -LightDirection;
	 DiffuseFactor = saturate(dot(BumpNormal, InvLightDirection));
    color += DiffuseColor*DiffuseFactor;
	color = saturate(color);

	//乘以基礎紋理顏色
	color = color*BasePixel;

	//如果漫射因子為正時,漫反射光和鏡面才存在意義
	if (DiffuseFactor > 0)
	{

		//求出入射光的反射向量,此時的法線量應該為法線貼圖的法向量,別把引數位置搞反了
		float3 ReflectLightDir = normalize(reflect(LightDirection,BumpNormal));
		//float3 ReflectLightDir = normalize(2 * DiffuseFactor*BumpNormal - InvLightDirection);
		//求每個畫素的鏡面強度
		SpecularIntensity = ShaderTexture[2].Sample(SampleType, outa.Tex);

		//求出鏡面反射因子
		SpecularFactor = pow(saturate(dot(outa.LookDirection, ReflectLightDir)), SpecularPow);

		//求出鏡面顏色,為什麼這裡用不上SpecularColor??
		Specular = SpecularFactor  *SpecularIntensity;
		color = color + Specular;  //確實只有鏡面光被計算 才加上
		
	}
	color = saturate(color);

	
	return color;
}


我的原始碼連結如下: