Directx11教程十二之NormalMap(法线贴图)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_29523119/article/details/52776960

本教程的构架如下:





本节的构架比前几个教程的构架多出一个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;
}



我的源代码链接如下:

点击打开链接




阅读更多
换一批

没有更多推荐了,返回首页