Unity-Shader之法线贴图使用注意事项

法线纹理在游戏生产过程中使用的频率非常高,在游戏中,尤其是手游中,模型面数过高是非常吃性能的。

在模型方面,美术在建模时会先创建一个高面数、高精度的精模,用精模渲染出一张法线贴图后,再创建一个低面数的简模,通过给简模附加法线贴图的方法,来达到使简模看起来像精模的目的。

这样就会产生一个问题,但是现在虽然看起来像精模了,但是光照效果怎么处理呢?最终光照上去,不还是会露馅吗?

这就需要我们在处理shader时,将法线贴图的法线方向当成真的法线方向计算进去。这里举例说明下法线贴图处理的常用方式。

法线分为模型空间下的法线和切线空间下的法线两种,一般来说,我们使用切线空间下的法线比较多。切线空间,就是以模型真正顶点的切线(Tangent)、法线(Normal)和副法线(Binormal)组成的坐标空间(TBN)空间,切线空间的XYZ轴方向分别对应TBN三条归一化后的向量。我们在法线贴图中,保存的是相对于该切线空间坐标系下,我们想要模拟的法线方向向量(下图中红线所指方向)。
其中的切线、法线都可以在Shader中的顶点信息中直接访问到,而副法线则需要用切线和法线的叉积算出来。

Binormal = Tangent x Normal
下图中的坐标系就是该顶点的切线空间,模型中每一个顶点都有一个自己的切线空间坐标系。

Unity-Shader之法线贴图使用注意事项_第1张图片

如果法线贴图中不需要模拟这个点的法线方向,该顶点对应的法线方向就是Z轴所指的方向。这样的话法线贴图中该点保存的向量就是(0,0,1),映射到法线贴图上的RGB 就是(0.5,0.5,1)看起来是淡蓝色(下面讲如何映射)。因为模拟精模时绝大部分法线并没有特别明显的变化,所以法线贴图整体看起来都是淡蓝色。
如下图:

(在shader中颜色值范围会映射到 [0,1]之间,(0.5,0.5,1)对应的颜色值就是(128,128,255))
Unity-Shader之法线贴图使用注意事项_第2张图片

由于颜色像素中储存颜色的区间是 [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)就有:

Unity-Shader之法线贴图使用注意事项_第3张图片

经过齐次变换后就有:

Unity-Shader之法线贴图使用注意事项_第4张图片

所以变换矩阵就是:

这里写图片描述

其中:
(Xx,Xy,Xz)是切线空间的x轴(Tangent)向量在世界空间下的表示。
(Yx,Yy,Yz)是切线空间的y轴(Binormal)向量在世界空间下的表示。
(Zx,Zy,Zz)是切线空间的z轴(Normal)向量在世界空间下的表示。
(Ox,Oy,Oz)是该切线空间的原点在世界空间下的坐标。(对应该顶点的世界坐标)

你可能感兴趣的:(Shader)