US3S7L1——标准漫反射Shader

标准漫反射Shader

目前我们已经完成了光源和阴影的主要知识点学习,包括多光源、阴影、光照衰减等等知识
已经可以在 Shader 中处理光和阴影相关的效果了,那么我们将结合所学的知识实现一个标准的漫反射 Shader
该 Shader 其实就是一个带有法线(世界空间中计算,因为全局效果的表现更准确,详见:US3S2L7——世界空间下计算法线纹理贴图)
的基于 Phong 光照模型(去掉高光反射)的支持多光源和阴影的Shader

说是标准,其实就是一个常用 Shader 而已

制作常用漫反射Shader

  1. 新建一个 Shader,取名叫 BumpedDiffuse​(凹凸漫反射)

  2. 复用 世界空间下计算法线纹理贴图的 中 Shader 代码,粘贴到新建文件中

    具体代码,详见:US3S2L7——世界空间下计算法线纹理贴图

  3. 加入渲染标签 Tags { "RenderType"="Opaque" "Queue"="Geometry"}

    渲染类型设置为不透明的、渲染队列设置为几何队列(不透明的几何体通常使用该队列)

    1
    2
    3
    4
    5
    SubShader
    {
    Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }
    Pass {/*...*/}
    }
  4. 删除高光反射相关代码

  5. 加入阴影、衰减相关代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct v2f
    {
    float4 pos: SV_POSITION;
    float4 uv: TEXCOORD0; // 使用float4同时存储主要纹理的uv(xy存储)和法线纹理的uv(zw存储)
    float4 tangentToWorld0: TEXCOORD1; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第一行
    float4 tangentToWorld1: TEXCOORD2; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第二行
    float4 tangentToWorld2: TEXCOORD3; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第三行
    SHADOW_COORDS(4) // 阴影坐标变量宏,因为前面有4个TEXCOORD变量,故这里参数填写4
    };
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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;
    // 得到世界空间下的顶点位置,用于之后在片元中计算视角方向(基于世界空间下)
    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.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);
    // 阴影坐标转换宏
    TRANSFER_SHADOW(data)

    return data;
    }
    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
    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 = 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));
    // 计算光照和阴影综合衰减值
    UNITY_LIGHT_ATTENUATION(atten, i, worldPos);
    // 最终颜色
    fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor * atten;

    return fixed4(color.rgb, 1);
    }
  6. 加入附加渲染通道,实现多光源效果

    可以直接复用 Bass Pass 的内容,只是需要修改 LightMode​ 和编译指令,再加上线性混合,同时注意修改光源方向相关计算代码并删除环境光的叠加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 附加渲染通道 Additional Pass
    Pass
    {
    Tags { "LightMode" = "ForwardAdd" }
    Blend One One

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile_fwdadd
    //...
    ENDCG
    }
    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
    fixed4 frag (v2f i) : SV_Target
    {
    // 计算世界空间下光的方向和视角方向
    float3 worldPos = float3(i.tangentToWorld0.w, i.tangentToWorld1.w, i.tangentToWorld2.w);
    #if defined(_DIRECTIONAL_LIGHT)
    fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz); // 平行光的光源方向
    #else
    fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz - worldPos); // 非平行光的光源方向
    #endif
    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 = float3(
    dot(i.tangentToWorld0.xyz, tangentNormal),
    dot(i.tangentToWorld1.xyz, tangentNormal),
    dot(i.tangentToWorld2.xyz, tangentNormal)
    );

    // 根据以上数据计算光照模型颜色
    fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _Color.rgb; //反射率
    // 漫反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
    fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, lightDir));
    // 计算光照和阴影综合衰减值
    UNITY_LIGHT_ATTENUATION(atten, i, worldPos);
    // 最终颜色
    fixed3 color = lambertColor * atten;

    return fixed4(color.rgb, 1);
    }
  7. 加入 FallBack "Diffuse"​,实现阴影投射

    1
    Fallback "Diffuse"

显示效果:

image

可以看到,这个 Shader 拥有法线带来的凹凸效果,拥有多光源的光照效果,同时可以接收并投射阴影

完整 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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
Shader "TeachShader/BumpedDiffuse"
{
Properties
{
_Color("MainColor", Color) = (1, 1, 1, 1)
_MainTex("MainTex", 2D) = ""{}
_BumpMap("BumpMap", 2D) = ""{}
_BumpScale("BumpScale", Range(0, 1)) = 1
}
SubShader
{
Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }

// 基础渲染通道 Base Pass
Pass
{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase

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

struct v2f
{
float4 pos: SV_POSITION;
float4 uv: TEXCOORD0; // 使用float4同时存储主要纹理的uv(xy存储)和法线纹理的uv(zw存储)
float4 tangentToWorld0: TEXCOORD1; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第一行
float4 tangentToWorld1: TEXCOORD2; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第二行
float4 tangentToWorld2: TEXCOORD3; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第三行
SHADOW_COORDS(4) // 阴影坐标变量宏,因为前面有4个TEXCOORD变量,故这里参数填写4
};

float4 _Color; //漫反射颜色
sampler2D _MainTex; //颜色纹理
float4 _MainTex_ST; //颜色纹理的缩放和平移
sampler2D _BumpMap; //法线纹理
float4 _BumpMap_ST; //法线纹理的缩放和平移
float _BumpScale; //凹凸程度

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;
// 得到世界空间下的顶点位置,用于之后在片元中计算视角方向(基于世界空间下)
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.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);
// 阴影坐标转换宏
TRANSFER_SHADOW(data)

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 = float3(
dot(i.tangentToWorld0.xyz, tangentNormal),
dot(i.tangentToWorld1.xyz, tangentNormal),
dot(i.tangentToWorld2.xyz, tangentNormal)
);

// 根据以上数据计算光照模型颜色
fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _Color.rgb; //反射率
// 漫反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, lightDir));
// 计算光照和阴影综合衰减值
UNITY_LIGHT_ATTENUATION(atten, i, worldPos);
// 最终颜色
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor * atten;

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

// 附加渲染通道 Additional Pass
Pass
{
Tags { "LightMode" = "ForwardAdd" }
Blend One One

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd

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

struct v2f
{
float4 pos: SV_POSITION;
float4 uv: TEXCOORD0; // 使用float4同时存储主要纹理的uv(xy存储)和法线纹理的uv(zw存储)
float4 tangentToWorld0: TEXCOORD1; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第一行
float4 tangentToWorld1: TEXCOORD2; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第二行
float4 tangentToWorld2: TEXCOORD3; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第三行
SHADOW_COORDS(4) // 阴影坐标变量宏,因为前面有4个TEXCOORD变量,故这里参数填写4
};

float4 _Color; //漫反射颜色
sampler2D _MainTex; //颜色纹理
float4 _MainTex_ST; //颜色纹理的缩放和平移
sampler2D _BumpMap; //法线纹理
float4 _BumpMap_ST; //法线纹理的缩放和平移
float _BumpScale; //凹凸程度

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;
// 得到世界空间下的顶点位置,用于之后在片元中计算视角方向(基于世界空间下)
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.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);
// 阴影坐标转换宏
TRANSFER_SHADOW(data)

return data;
}

fixed4 frag (v2f i) : SV_Target
{
// 计算世界空间下光的方向和视角方向
float3 worldPos = float3(i.tangentToWorld0.w, i.tangentToWorld1.w, i.tangentToWorld2.w);
#if defined(_DIRECTIONAL_LIGHT)
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz); // 平行光的光源方向
#else
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz - worldPos); // 非平行光的光源方向
#endif
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 = float3(
dot(i.tangentToWorld0.xyz, tangentNormal),
dot(i.tangentToWorld1.xyz, tangentNormal),
dot(i.tangentToWorld2.xyz, tangentNormal)
);

// 根据以上数据计算光照模型颜色
fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _Color.rgb; //反射率
// 漫反射光照计算:这里需要使用已经计算完毕的切线数据和光照方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, lightDir));
// 计算光照和阴影综合衰减值
UNITY_LIGHT_ATTENUATION(atten, i, worldPos);
// 最终颜色
fixed3 color = lambertColor * atten;

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

Fallback "Diffuse"
}