US3S2L7——世界空间下计算法线纹理贴图

关键知识点补充

  1. 模型空间下的切线数据

    模型数据中的切线数据为 float4​ 类型的(四维向量),其中的 w​ 表示副切线的方向
    用法线和切线叉乘得到的副切线方向可能有两个(如下图的两个方向的灰色线),用切线数据中的 w​ 与之相乘确定副切线方向

    image

  2. Unity 当中的法线纹理类型

    当我们把纹理类型设置为 Normal map(法线贴图)时,
    我们可以使用Unity提供的内置函数 UnpackNormal​ 来得到正确的法线方向。
    该函数内部不仅可以进行 法线分量=像素分量×21法线分量 = 像素分量 \times 2 - 1的逆运算,
    还会进行解压运算(Unity 会根据不同平台对法线纹理进行压缩)

    image

  3. 法线纹理属性命名一般为 _BumpMap​(凸块贴图),
    并声明一个我们还会声明一个名为 _BumpScale​(凸块缩放))的 float​ 属性,它主要用于控制凹凸程度

    当它为 0 时,表示没有法线效果,法线的影响会被消除
    当它为 1 时,表示使用法线贴图中的原始法线信息,没有缩放
    我们可以根据实际需求调整它的值,来达到视觉上令人满意的效果

  4. 如果使用的凹凸纹理不是法线纹理,而是高度纹理

    我们需要进行如下设置:

    图片类型设置为 Normal map(法线贴图)并勾选 Create from Grayscale(从灰度创建)
    这样我们就可以把高度纹理当成切线空间下的法线纹理处理了

    多出的参数分别为:

    image

    • Bumpiness(颠簸值):控制凹凸程度

    • Filtering(过滤模式):决定凹凸程度的算法

      image

      • Sharp:滤波生成法线
      • Smooth:平滑的生成法线

在世界空间下计算实现法线纹理Shader

主要思路:

  • 在顶点着色器中计算切线空间到世界空间的变换矩阵
  • 在片元着色器中进行法线采样转换

Shader 属性相关

和 切线空间下计算法线纹理贴图 的属性一致

  • 漫反射颜色
  • 单张纹理
  • 法线纹理
  • 凹凸程度(一般使用 0~1 即可)
  • 高光反射颜色
  • 光泽度
1
2
3
4
5
6
7
8
9
Properties
{
_MainColor("MainColor", Color) = (1, 1, 1, 1) //漫反射颜色
_MainTex("MainTex", 2D) = ""{} //主要纹理贴图
_BumpMap("BumpMap", 2D) = ""{} //凹凸纹理贴图
_BumpScale("BumpScale", Range(0, 1)) = 1 //凹凸程度
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) //高光反射颜色
_SpecularNum("SpecularNum", Range(0, 20)) = 18 //光泽度
}

结构体相关

  • 传入顶点着色器的结构体

    可以直接使用 UnityCG.cginc​ 中的 appdata_full​
    其中包含了我们需要的顶点、法线、切线、纹理坐标相关数据

    1
    2
    3
    4
    5
    6
    7
    8
    // UnityCG.cginc内定义,无需自己再定义
    struct appdata_full {
    float4 vertex : POSITION; //模型空间中的顶点位置
    float4 tangent : TANGENT; //顶点切线
    float3 normal : NORMAL; //顶点法线
    float4 texcoord : TEXCOORD0; //第一组纹理坐标
    // ...
    };
  • 传入片元着色器的结构体

    自定义结构体,其中包含 裁剪空间下坐标、主纹理和法线纹理的uv坐标、世界空间下顶点位置、切线空间转换到世界空间的变换矩阵

    1
    2
    3
    4
    5
    6
    7
    struct v2f
    {
    float4 pos: SV_POSITION;
    float4 uv: TEXCOORD0; // 使用float4同时存储主要纹理的uv(xy存储)和法线纹理的uv(zw存储)
    float3 worldPos: TEXCOORD1; // 顶点相对于世界坐标的位置,用于之后的视角方向的计算
    float3x3 rotation: TEXCOORD2; // 切线空间到世界空间的变换矩阵
    };

顶点着色器回调函数内容

执行步骤:

  1. 顶点坐标模型转裁剪

  2. 单张纹理和法线纹理 UV坐标缩放偏移计算

  3. 模型空间下顶点转换到世界空间下,使用 unity_ObjectToWorld​ 矩阵转换(之后在片元着色器中用于计算视角方向)

  4. 使用 UnityObjectToWorldNormal​ 和 UnityObjectToWorldDir​ 将模型空间下的法线、切线转换到世界空间下

  5. 计算世界空间下的副切线

    用世界空间下的法线和切线进行叉乘,再乘以切线中的 w​(确定副切线方向)

  6. 构建模型空间到切线空间的变换矩阵

    (切线副切线法线)\begin{pmatrix} | & | & | \\ 切线 & 副切线 & 法线 \\ | & | & | \\ \end{pmatrix}

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
// 注:appdata_full不需要自定义,#include "UnityCG.cginc"即可使用,这里仅用于展示其成员
struct appdata_full {
float4 vertex : POSITION; //模型空间中的顶点位置
float4 tangent : TANGENT; //顶点切线
float3 normal : NORMAL; //顶点法线
float4 texcoord : TEXCOORD0; //第一组纹理坐标
//...
};

struct v2f
{
float4 pos: SV_POSITION;
float4 uv: TEXCOORD0; // 使用float4同时存储主要纹理的uv(xy存储)和法线纹理的uv(zw存储)
float3 worldPos: TEXCOORD1; // 顶点相对于世界坐标的位置,用于之后的视角方向的计算
float3x3 rotation: TEXCOORD2; // 切线空间到世界空间的变换矩阵
};

float4 _MainTex_ST; //颜色纹理的缩放和平移
float4 _BumpMap_ST; //法线纹理的缩放和平移

v2f vert (appdata_full v)
{
v2f data;

data.pos = UnityObjectToClipPos(v.vertex); //计算裁剪空间下顶点坐标
// 分别计算主纹理和法线纹理的缩放平移
data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
data.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
// 得到世界空间下的顶点位置,用于之后在片元中计算视角方向(基于世界空间下)
data.worldPos = mul(unity_ObjectToWorld, v.vertex);

// 将模型空间下的法线、切线转换到世界空间下,并计算世界空间下的副切线
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent);
float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * v.tangent.w;
// 构建模型空间到世界空间的变换矩阵
data.rotation = float3x3(
worldTangent.x, worldBinormal.x, worldNormal.x,
worldTangent.y, worldBinormal.y, worldNormal.y,
worldTangent.z, worldBinormal.z, worldNormal.z
);

return data;
}

片元着色器回调函数内容

执行步骤:

  1. 通过 _WorldSpaceLightPos0​ 计算世界空间下光的方向,通过 UnityWorldSpaceViewDir​ 世界空间下的视角方向
  2. 取出法线贴图中的法线信息(利用纹理采样函数 tex2D​)
  3. 利用内置的 UnpackNormal​ 函数对法线信息进行逆运算以及可能的解压
  4. 用得到的切线空间的法线数据 乘以 BumpScale​ 来控制凹凸程度
  5. 将计算完毕后的切线空间下的法线转换到世界空间下
  6. 得到单张纹理颜色和漫反射颜色的叠加颜色
  7. 用世界空间下的 光方向、视角方向、法线方向 进行BlinnPhong光照模型计算
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
struct v2f
{
float4 pos: SV_POSITION;
float4 uv: TEXCOORD0; // 使用float4同时存储主要纹理的uv(xy存储)和法线纹理的uv(zw存储)
float3 worldPos: TEXCOORD1; // 顶点相对于世界坐标的位置,用于之后的视角方向的计算
float3x3 rotation: TEXCOORD2; // 切线空间到世界空间的变换矩阵
};

float4 _MainColor; //漫反射颜色
sampler2D _MainTex; //颜色纹理
sampler2D _BumpMap; //法线纹理
float _BumpScale; //凹凸程度
float4 _SpecularColor; //高光颜色
fixed _SpecularNum; //光泽度

fixed4 frag (v2f i) : SV_Target
{
// 计算世界空间下光的方向和视角方向
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
// 通过法线纹理采样并解压缩,再乘以凹凸系数,得到切线空间下法线数据
float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal *= _BumpScale;
// 将切线空间下法线数据转换到世界空间下
float3 worldNormal = mul(i.rotation, tangentNormal);

// 根据以上数据计算光照模型颜色
fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor.rgb; //反射率
// 漫反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, lightDir));
// 高光反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
float3 halfA = normalize(viewDir + lightDir); //半角向量
fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfA)), _SpecularNum);
// 最终颜色
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

return fixed4(color.rgb, 1);
}

代码示例

完整 Shader 代码如下:

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
Shader "TeachShader/Lesson52"
{
Properties
{
_MainColor("MainColor", Color) = (1, 1, 1, 1)
_MainTex("MainTex", 2D) = ""{}
_BumpMap("BumpMap", 2D) = ""{}
_BumpScale("BumpScale", Range(0, 1)) = 1
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1)
_SpecularNum("SpecularNum", Range(0, 20)) = 18
}
SubShader
{
Pass
{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"

struct v2f
{
float4 pos: SV_POSITION;
float4 uv: TEXCOORD0; // 使用float4同时存储主要纹理的uv(xy存储)和法线纹理的uv(zw存储)
float3 worldPos: TEXCOORD1; // 顶点相对于世界坐标的位置,用于之后的视角方向的计算
float3x3 rotation: TEXCOORD2; // 切线空间到世界空间的变换矩阵
};

float4 _MainColor; //漫反射颜色
sampler2D _MainTex; //颜色纹理
float4 _MainTex_ST; //颜色纹理的缩放和平移
sampler2D _BumpMap; //法线纹理
float4 _BumpMap_ST; //法线纹理的缩放和平移
float _BumpScale; //凹凸程度
float4 _SpecularColor; //高光颜色
fixed _SpecularNum; //光泽度

v2f vert (appdata_full v)
{
v2f data;

data.pos = UnityObjectToClipPos(v.vertex); //计算裁剪空间下顶点坐标
// 分别计算主纹理和法线纹理的缩放平移
data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
data.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
// 得到世界空间下的顶点位置,用于之后在片元中计算视角方向(基于世界空间下)
data.worldPos = mul(unity_ObjectToWorld, v.vertex);

// 将模型空间下的法线、切线转换到世界空间下,并计算世界空间下的副切线
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent);
float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * v.tangent.w;
// 构建模型空间到世界空间的变换矩阵
data.rotation = float3x3(
worldTangent.x, worldBinormal.x, worldNormal.x,
worldTangent.y, worldBinormal.y, worldNormal.y,
worldTangent.z, worldBinormal.z, worldNormal.z
);

return data;
}

fixed4 frag (v2f i) : SV_Target
{
// 计算世界空间下光的方向和视角方向
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
// 通过法线纹理采样并解压缩,再乘以凹凸系数,得到切线空间下法线数据
float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal *= _BumpScale;
// 将切线空间下法线数据转换到世界空间下
float3 worldNormal = mul(i.rotation, tangentNormal);

// 根据以上数据计算光照模型颜色
fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor.rgb; //反射率
// 漫反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, lightDir));
// 高光反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
float3 halfA = normalize(viewDir + lightDir); //半角向量
fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfA)), _SpecularNum);
// 最终颜色
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

return fixed4(color.rgb, 1);
}
ENDCG
}
}
}

显示效果(左图为不使用法线贴图的方块,右下切线空间下计算法线贴图的方块,右上世界空间下计算法线贴图的方块):

image

关于光照方向的计算方式

计算光照存在两种方式:

  1. 模拟定向光源

    直接得到 _WorldSpaceLightPos0​ 光照位置 作为光照方向,
    表示光线是平行的,而不是从特定点发射
    一般模拟太阳光效果 采用这种方式

  2. 模拟点光源

    用光照位置 _WorldSpaceLightPos0​ 减去 顶点坐标,
    表示光线是从特定点发射的,并朝着顶点方向
    一般定点光源 采用这种方式

第二种光照方向代码类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fixed4 frag (v2f i) : SV_Target
{
// 计算世界空间下光的方向和视角方向
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos); //这里得到的光源方向就是类似于点光源的
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
// 通过法线纹理采样并解压缩,再乘以凹凸系数,得到切线空间下法线数据
float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal *= _BumpScale;
// 将切线空间下法线数据转换到世界空间下
float3 worldNormal = mul(i.rotation, tangentNormal);

// 根据以上数据计算光照模型颜色
fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor.rgb; //反射率
// 漫反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, lightDir));
// 高光反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
float3 halfA = normalize(viewDir + lightDir); //半角向量
fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfA)), _SpecularNum);
// 最终颜色
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

return fixed4(color.rgb, 1);
}

效果对比(左为使用平行光源方向的方块,右为使用点光源方向的方块):

image

可以看见,左侧方块的受光面的亮度是均匀的,
而右侧的亮度呈现右下更亮,左上较暗的趋势(这是因为它以光源和顶点的方向计算光照)

修改凹凸系数的计算方式,让法线系数不影响光照

目前的 Shader 写法中,凹凸系数 BumpScale​ 会影响光照,凹凸系数越小,光照越暗

image

我们目前这种直接让 法线×凹凸系数法线 \times 凹凸系数 的计算方式,并不是一个标准算法
因为当凹凸系数趋近于 0 时,会影响光照模型的计算(法线也趋于0了,导致计算出来的颜色是偏黑的)

为了让凹凸系数不影响光的效果,有一种专门的算法

  1. 只让法线中的 xy 乘以凹凸系数

    1
    tangentNormal.xy *= _BumpScale;
  2. 同时保证法线为单位向量(让法线不会为0,而是趋近于顶点法线)

    若要让法线在 xy 乘以凹凸系数后还要保持为单位向量,就需要让 z 分量进行改变
    根据单位向量的性质可得:

    (x,y,z)为单位向量x2+y2+z2=1z2=1(x2+y2)z=1(x2+y2)\because (x,y,z)为单位向量 \\ \therefore x^2 + y^2 + z^2 = 1 \\ \therefore z^2 = 1 - (x^2 + y^2) \\ \therefore z = \sqrt{1 - (x^2 + y^2)}

    又因为 x2+y2=(x,y)(x,y)x^2 + y^2 = (x,y) \cdot (x,y),因此代码内可实现为:

    1
    tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

    其中,saturate​ 函数是保证计算结果在 0~1 之间的

通过这样的计算,当凹凸系数在 0~1 之间变化时,会保证法线为单位向量,这样就不会影响光照表现了

注意:这种算法并不是来自真实的物理规律,只是为了 “看起来正常”

因此,根据以上规则,片元着色器的 Shader 代码修改为:

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
fixed4 frag (v2f i) : SV_Target
{
// 计算世界空间下光的方向和视角方向
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
// 通过法线纹理采样并解压缩,再乘以凹凸系数,得到切线空间下法线数据
float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
float3 tangentNormal = UnpackNormal(packedNormal);
// 使用新方法计算法线,避免凹凸系数影响光照
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// 将切线空间下法线数据转换到世界空间下
float3 worldNormal = mul(i.rotation, tangentNormal);

// 根据以上数据计算光照模型颜色
fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor.rgb; //反射率
// 漫反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, lightDir));
// 高光反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
float3 halfA = normalize(viewDir + lightDir); //半角向量
fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfA)), _SpecularNum);
// 最终颜色
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

return fixed4(color.rgb, 1);
}

效果如下(左侧方块的凹凸系数为1,右侧方块的凹凸系数为0.3):

image

可见,现在的凹凸系数可以在不影响光照的情况下,改变模型凹凸感了

提高性能的写法

我们目前在 v2f​ 结构体中,世界坐标顶点位置和变换矩阵使用了
float3​ 和 float3x3​ 的两个变量来存储

1
2
3
4
5
6
7
struct v2f
{
float4 pos: SV_POSITION;
float4 uv: TEXCOORD0; // 使用float4同时存储主要纹理的uv(xy存储)和法线纹理的uv(zw存储)
float3 worldPos: TEXCOORD1; // 顶点相对于世界坐标的位置,用于之后的视角方向的计算
float3x3 rotation: TEXCOORD2; // 切线空间到世界空间的变换矩阵
};

但是在很多世界空间下计算法线贴图的 Shader 中,往往会使用 3 个 float4​ 类型的变量来存储它们

这样做的目的是因为:这种写法在很多情况下可以提高性能,因为它更好地与GPU的硬件架构匹配
float4​ 类型的寄存器是非常高效的,因为现代GPU通常会以 4 分量的向量为基本单位进行并行计算
float3x3​ 矩阵相对来说需要更多的寄存器和指令来表示和计算

因此,v2f​ 结构体可以修改为:

1
2
3
4
5
6
7
8
9
10
11
struct v2f
{
float4 pos: SV_POSITION;
float4 uv: TEXCOORD0; // 使用float4同时存储主要纹理的uv(xy存储)和法线纹理的uv(zw存储)
float4 tangentToWorld0: TEXCOORD0; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第一行
float4 tangentToWorld1: TEXCOORD1; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第二行
float4 tangentToWorld2: TEXCOORD2; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第三行

// float3 worldPos: TEXCOORD1; // 顶点相对于世界坐标的位置,用于之后的视角方向的计算
// float3x3 rotation: TEXCOORD2; // 切线空间到世界空间的变换矩阵
};

其中,tangentToWorld0​ ~ tangentToWorld2​ 的 xyz​ 用来存储整个切线空间到世界空间的变换矩阵
tangentToWorld0​ ~ tangentToWorld2​ 的 w​ 用来存储顶点相对于世界坐标的位置

对应的,顶点着色器和片元着色器需要修改为:

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
55
56
57
58
59
60
61
62
63
v2f vert (appdata_full v)
{
v2f data;

data.pos = UnityObjectToClipPos(v.vertex); //计算裁剪空间下顶点坐标
// 分别计算主纹理和法线纹理的缩放平移
data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
data.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
// 得到世界空间下的顶点位置,用于之后在片元中计算视角方向(基于世界空间下)
//data.worldPos = mul(unity_ObjectToWorld, v.vertex);
float3 worldPos = mul(unity_ObjectToWorld, v.vertex);

// 将模型空间下的法线、切线转换到世界空间下,并计算世界空间下的副切线
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent);
float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * v.tangent.w;
// 构建模型空间到世界空间的变换矩阵
//data.rotation = float3x3(
// worldTangent.x, worldBinormal.x, worldNormal.x,
// worldTangent.y, worldBinormal.y, worldNormal.y,
// worldTangent.z, worldBinormal.z, worldNormal.z
//);
// 构建新的更高性能的数据
data.tangentToWorld0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
data.tangentToWorld1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
data.tangentToWorld2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

return data;
}

fixed4 frag (v2f i) : SV_Target
{
// 计算世界空间下光的方向和视角方向
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
// 从三个float3数据提取出worldPos信息;
float3 worldPos = float3(i.tangentToWorld0.w, i.tangentToWorld1.w, i.tangentToWorld2.w);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// 通过法线纹理采样并解压缩,再乘以凹凸系数,得到切线空间下法线数据
float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// 将切线空间下法线数据转换到世界空间下
//float3 worldNormal = mul(rotation, tangentNormal);
// 直接使用三个float3的xyz数据转换,这里本质就是矩阵相乘
float3 worldNormal = float3(
dot(i.tangentToWorld0.xyz, tangentNormal),
dot(i.tangentToWorld1.xyz, tangentNormal),
dot(i.tangentToWorld2.xyz, tangentNormal)
);

// 根据以上数据计算光照模型颜色
fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor.rgb; //反射率
// 漫反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, lightDir));
// 高光反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
float3 halfA = normalize(viewDir + lightDir); //半角向量
fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfA)), _SpecularNum);
// 最终颜色
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

return fixed4(color.rgb, 1);
}

因为 3×33 \times 3 矩阵乘以一个三行向量的计算过程就相当于 3×33 \times 3 矩阵取三行分量分别去和向量点乘,
因此片元着色器内部不需要去重新构建一个 float3x3​ 转换矩阵,直接对三行分量分别点乘即可

因此,最终 Shader 代码是这样的:

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
Shader "TeachShader/Lesson52"
{
Properties
{
_MainColor("MainColor", Color) = (1, 1, 1, 1)
_MainTex("MainTex", 2D) = ""{}
_BumpMap("BumpMap", 2D) = ""{}
_BumpScale("BumpScale", Range(0, 1)) = 1
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1)
_SpecularNum("SpecularNum", Range(0, 20)) = 18
}
SubShader
{
Pass
{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"

struct v2f
{
float4 pos: SV_POSITION;
float4 uv: TEXCOORD0; // 使用float4同时存储主要纹理的uv(xy存储)和法线纹理的uv(zw存储)
float4 tangentToWorld0: TEXCOORD1; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第一行
float4 tangentToWorld1: TEXCOORD2; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第二行
float4 tangentToWorld2: TEXCOORD3; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第三行

// float3 worldPos: TEXCOORD1; // 顶点相对于世界坐标的位置,用于之后的视角方向的计算
// float3x3 rotation: TEXCOORD2; // 切线空间到世界空间的变换矩阵
};

float4 _MainColor; //漫反射颜色
sampler2D _MainTex; //颜色纹理
float4 _MainTex_ST; //颜色纹理的缩放和平移
sampler2D _BumpMap; //法线纹理
float4 _BumpMap_ST; //法线纹理的缩放和平移
float _BumpScale; //凹凸程度
float4 _SpecularColor; //高光颜色
fixed _SpecularNum; //光泽度

v2f vert (appdata_full v)
{
v2f data;

data.pos = UnityObjectToClipPos(v.vertex); //计算裁剪空间下顶点坐标
// 分别计算主纹理和法线纹理的缩放平移
data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
data.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
// 得到世界空间下的顶点位置,用于之后在片元中计算视角方向(基于世界空间下)
//data.worldPos = mul(unity_ObjectToWorld, v.vertex);
float3 worldPos = mul(unity_ObjectToWorld, v.vertex);

// 将模型空间下的法线、切线转换到世界空间下,并计算世界空间下的副切线
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent);
float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * v.tangent.w;
// 构建模型空间到世界空间的变换矩阵
//data.rotation = float3x3(
// worldTangent.x, worldBinormal.x, worldNormal.x,
// worldTangent.y, worldBinormal.y, worldNormal.y,
// worldTangent.z, worldBinormal.z, worldNormal.z
//);
data.tangentToWorld0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
data.tangentToWorld1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
data.tangentToWorld2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

return data;
}

fixed4 frag (v2f i) : SV_Target
{
// 计算世界空间下光的方向和视角方向
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
float3 worldPos = float3(i.tangentToWorld0.w, i.tangentToWorld1.w, i.tangentToWorld2.w);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// 通过法线纹理采样并解压缩,再乘以凹凸系数,得到切线空间下法线数据
float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// 将切线空间下法线数据转换到世界空间下
//float3 worldNormal = mul(rotation, tangentNormal);
float3 worldNormal = float3(
dot(i.tangentToWorld0.xyz, tangentNormal),
dot(i.tangentToWorld1.xyz, tangentNormal),
dot(i.tangentToWorld2.xyz, tangentNormal)
);

// 根据以上数据计算光照模型颜色
fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor.rgb; //反射率
// 漫反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, lightDir));
// 高光反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
float3 halfA = normalize(viewDir + lightDir); //半角向量
fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfA)), _SpecularNum);
// 最终颜色
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

return fixed4(color.rgb, 1);
}
ENDCG
}
}
}