法线纹理在游戏生产过程中使用的频率非常高,在游戏中,尤其是手游中,模型面数过高是非常吃性能的。
在模型方面,美术在建模时会先创建一个高面数、高精度的精模,用精模渲染出一张法线贴图后,再创建一个低面数的简模,通过给简模附加法线贴图的方法,来达到使简模看起来像精模的目的。
这样就会产生一个问题,但是现在虽然看起来像精模了,但是光照效果怎么处理呢?最终光照上去,不还是会露馅吗?
这就需要我们在处理shader时,将法线贴图的法线方向当成真的法线方向计算进去。这里举例说明下法线贴图处理的常用方式。
法线分为模型空间下的法线和切线空间下的法线两种,一般来说,我们使用切线空间下的法线比较多。切线空间,就是以模型真正顶点的切线(Tangent)、法线(Normal)和副法线(Binormal)组成的坐标空间(TBN)空间,切线空间的XYZ轴方向分别对应TBN三条归一化后的向量。我们在法线贴图中,保存的是相对于该切线空间坐标系下,我们想要模拟的法线方向向量(下图中红线所指方向)。
其中的切线、法线都可以在Shader中的顶点信息中直接访问到,而副法线则需要用切线和法线的叉积算出来。
Binormal = Tangent x Normal
下图中的坐标系就是该顶点的切线空间,模型中每一个顶点都有一个自己的切线空间坐标系。
如果法线贴图中不需要模拟这个点的法线方向,该顶点对应的法线方向就是Z轴所指的方向。这样的话法线贴图中该点保存的向量就是(0,0,1),映射到法线贴图上的RGB 就是(0.5,0.5,1)看起来是淡蓝色(下面讲如何映射)。因为模拟精模时绝大部分法线并没有特别明显的变化,所以法线贴图整体看起来都是淡蓝色。
如下图:
(在shader中颜色值范围会映射到 [0,1]之间,(0.5,0.5,1)对应的颜色值就是(128,128,255))
由于颜色像素中储存颜色的区间是 [0,1],而法线方向的取值范围是[-1,1],所以我们在将法线方向储存为法线贴图上面的像素时,需要经过映射。
公式为:
Pixel = (normal + 1) / 2
反过来当我们需要从法线贴图上读取法线向量时,需要从像素颜色反算出法线向量。
公式为:
Normal = pixel x 2 – 1
关于法线贴图的原理,在切线空间下的法线,其Z轴分量永远是正方向,所以在法线贴图中,我们只需要保存该模拟法线方向的XY方向的分量。然后通过相应的解压缩算法获得Z方向分量。
在Unity中,解压方式为:
所以我们在法线贴图中只需要保存两个颜色通道,对图片进行压缩,减小法线贴图占用的内存空间。
在实际使用法线贴图的过程中,尤其是用法线贴图处理光照或者UV、顶点动画时,最重要的是处理好切线空间到世界空间坐标系的转换。
1.在顶点着色器中计算出切线空间到世界空间的变换矩阵
v2f vert(a2v v){
v2f o;
o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
//世界坐标
float3 worldPos = mul(_Object2World,v.vertex).xyz;
//世界法线
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
//世界切线
float3 worldTanget = UnityObjectToWorldDir(v.tangent.xyz);
//法线叉乘切线获得副切线
float3 worldBinormal = cross(worldNormal,worldTanget) * v.vertex.w;
//保存从当前顶点的切线空间坐标系到世界空间坐标系的变换矩阵
o.T2W0 = float4(worldTanget.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.T2W1 = float4(worldTanget.y,worldBinormal.y,worldNormal.y,worldPos.y);
o.T2W2 = float4(worldTanget.z,worldBinormal.z,worldNormal.z,worldPos.z);
return o;
}
2.在片元着色器中再将切线空间的法线转换成世界空间。
float4 frag(v2f i):SV_Target{
//UnpackNormal 方法用来解压法线贴图上面采样的结果,获得切线空间下的法线信息
float3 tangentNormal = UnpackNormal(tex2D(NormalMap,i.uv)).rgb;
//将法线贴图上面采样到的法线信息转换到世界空间
float3 worldNormal = normalize(float3(dot(i.T2W0.xyz,tangentNormal),dot(i.T2W1.xyz,tangentNormal),dot(i.T2W2.xyz,tangentNormal)));
}
其中tex2D可以获得相应uv采样坐标下的法线贴图的RGB颜色值,再用UnpackNormal 方法映射出法线的xyz坐标。
关于坐标系变换矩阵的组成方法,我们可以假设该顶点在世界空间下的坐标为 O’ ,该切线空间的三个方向的坐标轴向量在世界空间下的表示分别为X’,Y’,Z’。在该切线空间下任意一点的坐标可以表示为 (A’,B’,C’),则其在世界空间下的表示(A,B,C)就有:
经过齐次变换后就有:
所以变换矩阵就是:
其中:
(Xx,Xy,Xz)是切线空间的x轴(Tangent)向量在世界空间下的表示。
(Yx,Yy,Yz)是切线空间的y轴(Binormal)向量在世界空间下的表示。
(Zx,Zy,Zz)是切线空间的z轴(Normal)向量在世界空间下的表示。
(Ox,Oy,Oz)是该切线空间的原点在世界空间下的坐标。(对应该顶点的世界坐标)