默认情况下,一个模型走的是顶点法线,一个游戏模型的顶点一般很少,所以法线细节也偏低。

使用法线贴图后,在片元阶段就能应用逐像素的法线,这样对于物体表面细节的还原就更加真实。

物体表面的顶点法线

上图显示的是物体顶点法线,其中蓝色的是法线,绿色的是切线(Tangent),红色的是副法线(或者叫副切线)。这三根线构成了当前顶点的切线空间,我们在切线空间中描述模型的表面信息,并传递到法线贴图中。一般情况下,我们可以认为法线指向z轴,切线指向x轴,父法线指向y轴

在PC端上,法线贴图一般会用DXT/BC进行压缩,只保留红通道,所以需要在Shader中使用UnpackNormal方法,这个方法不仅会还原法线贴图,同时还会将偏移值从[0,1]转到[-1,1],因为法线本来记录的就是每个像素的法线偏移量,取值是-1到1的。

一个物体的切线是引擎自动给出的,在Unity中,切线的方向跟物体uv的u方向一致。而物体的副法线是法线和切线通过叉乘计算出来的。

想要应用法线贴图,我们需要先在顶点着色器中将顶点在世界空间下的法线、切线、副切线信息传递到片元着色器中。

下面对于法线的应用,在ForwardBase和ForwardAdd两个Pass都要实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Properties
{
//...
_NormalMap("NormalMap", 2D) = "bump" {}//法线贴图默认值是Bump
_NormalIntensity("NormalIntensity", Range(0.0, 5.0)) = 1.0
}

struct appdata
{
//...
float4 tangent : TANGENT;//tangent是一个四维向量,它的第四位记录了不同平台的次法线的翻转值
};

struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal_dir : TEXCOORD1;//世界空间法线
float3 tangent_dir : TEXCOORD3;//世界空间切线
float3 binormal_dir : TEXCOORD4;//世界空间副法线
float3 pos_world : TEXCOORD2;//世界空间顶点,用来在片元Shader中计算视线方向
};
sampler2D _NormalMap;
float _NormalIntensity;

v2f vert (appdata v)
{
v2f o;
//...
o.normal_dir = normalize(mul(float4(v.normal,0.0), unity_WorldToObject).xyz);
o.tangent_dir = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);
o.binormal_dir = normalize(cross(o.normal_dir, o.tangent_dir)) * v.tangent.w;//使用tangent.w来解决不同平台次法线的翻转问题
//...
return o;
}
half4 frag (v2f i) : SV_Target
{
half4 base_color = tex2D(_MainTex, i.uv);
half4 ao_color = tex2D(_AOMap, i.uv);
half4 spec_mask = tex2D(_SpecMask, i.uv);
half4 normal_map = tex2D(_NormalMap, i.uv);
half3 normal_data = UnpackNormal(normal_map);//xyz分别记载了世界空间下当前像素切线、副法线、法线的偏移量
normal_data.xy = normal_data.xy * _NormalIntensity;//法线强度偏移的是切线和副切线的值

//应用法线贴图
half3 normal_dir = normalize(i.normal_dir);
half3 tangent_dir = normalize(i.tangent_dir);
half3 binormal_dir = normalize(i.binormal_dir);
float3x3 TBN = float3x3(tangent_dir, binormal_dir, normal_dir);
normal_dir = normalize(mul(normal_data, TBN));//注意要右乘TBN矩阵
//将从法线贴图中获取到的normal_data应用到normal_dir中,这里使用一个不使用TBN矩阵的更易懂的公式
//normal_dir = normalize(tangent_dir * normal_data.x * _NormalIntensity + binormal_dir * normal_data.y * _NormalIntensity + normal_dir * normal_data.z);
//...
}