US3S6L3——让物体投射阴影

感受 Fallback 的作用

我们新建一个材质球,将其的 Shader 设置为我们之前实现的多种光源综合实现 Shader

并将该材质球赋值给较大的立方体使用,我们会发现该立方体不再投射阴影也不再接受阴影,如下图
左边的立方体使用 Unity 的默认 Shader,既可以接受阴影和也可以投射阴影
右边的立法体使用之前实现的 多种光源综合实现 Shader,可以发现其既不接受阴影和也不投射阴影

image

  1. 不投射阴影的原因:该 Shader 中没有 LightMode​ 为 ShaderCaster​ 的 Pass​,无法参与光源的阴影映射纹理的计算
  2. 不接收阴影的原因:该 Shader 并没有对阴影映射相关纹理进行采样,没有进行阴影相关颜色运算

我们之前学习理论知识时提到过,Unity 会寻找 LightMode​ 为 ShaderCaster​ 的 Pass​ 来进行处理,
如果该 Shader​ 没有该 Pass​,会在它 FallBack​ 指定的 Shader​ 中寻找,直到找到为止

我们现在在该 Shader​ 最后加上 FallBack "Specular"​,便可以让该立方体投射阴影

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Shader "TeachShader/Lesson64"
{
Properties
{
// ...
}
SubShader
{
// Bass Pass 基础渲染通道
Pass { /*...*/ }

// Additional Pass 附加渲染通道
Pass { /*...*/ }
}

Fallback "Specular"
}

显示效果:

image

可见,现在使用我们的自己实现的 Shader 的立方体可以投射阴影了

让物体投射阴影

物体向其它物体投射阴影的关键点是:

  1. 需要实现 LightMode​(灯光模式)为 ShadowCaster​(阴影投射)的 Pass​(渲染通道)
    这样该物体才能参与到光源的阴影映射纹理计算中

  2. 一个编译指令,一个内置文件,三个关键宏

    • 编译指令:#pragma multi_compile_shadowcaster

      该编译指令时告诉 Unity 编译器生成多个着色器变体,用于支持不同类型的阴影(SM,SSSM等等)
      可以确保着色器能够在所有可能的阴影投射模式下正确渲染

    • 内置文件:#include "UnityCG.cginc"

      ​UnityCG.cginc​ 中包含了关键的阴影计算相关的宏

    • 三个关键宏:

      • V2F_SHADOW_CASTER​:顶点到片元着色器阴影投射结构体数据宏

        这个宏定义了一些标准的成员变量,这些变量用于在阴影投射路径中传递顶点数据到片元着色器,我们主要在结构体中使用

      • TRANSFER_SHADOW_CASTER_NORMALOFFSET​:转移阴影投射器法线偏移宏

        用于在顶点着色器中计算和传递阴影投射所需的变量,主要做了:

        1. 将对象空间的顶点位置转换为裁剪空间的位置
        2. 考虑法线偏移,以减轻阴影失真问题,尤其是在处理自阴影时
        3. 传递顶点的投影空间位置,用于后续的阴影计算

        我们主要在顶点着色器中使用

      • SHADOW_CASTER_FRAGMENT​:阴影投射片元宏

        将深度值写入到阴影映射纹理中,我们主要在片元着色器中使用

  3. 利用这些内容在 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
    // ShadowCaster Pass 投影阴影通道
    Pass
    {
    Tags { "LightMode" = "ShadowCaster" }

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    // 告诉Unity编译器生成多个着色器变体,用于支持不同类型的阴影(SM,SSSM等等),确保着色器能够在所有可能的阴影投射模式下正确渲染
    #pragma multi_compile_shadowcaster

    #include "UnityCG.cginc"

    struct v2f
    {
    V2F_SHADOW_CASTER; // 顶点到片元着色器阴影投射结构体数据宏,定义了一些标准的成员变量,这些变量用于在阴影投射路径中传递顶点数据到片元着色器
    };

    v2f vert(appdata_base v)
    {
    v2f data;
    TRANSFER_SHADOW_CASTER_NORMALOFFSET(data); // 转移阴影投射器法线偏移宏,用于在顶点着色器中计算和传递阴影投射所需的变量
    return data;
    }

    fixed4 frag(v2f i) : SV_Target
    {
    SHADOW_CASTER_FRAGMENT(i); //阴影投射片元宏,将深度值写入到阴影映射纹理中
    }
    ENDCG
    }

显示效果:

image

可见,现在我们不使用 Unity 已经实现的 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/Lesson66ForwardLighting"
{
Properties
{
_MainColor("MainColor", Color) = (1, 1, 1, 1) //材质的漫反射颜色
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) //材质高光反射颜色
_SpecularNum("SpecularNum", Range(0, 20)) = 0.5 //光泽度
}
SubShader
{
// Bass Pass 基础渲染通道
Pass
{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 帮助我们编译所有光照变体,并确保光照衰减相关的变量能够正确复制到对应的变量中
#pragma multi_compile_fwdbase

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

struct v2f
{
float4 svPos : SV_POSITION; //裁剪空间下的顶点坐标
float3 wNormal : NORMAL; //世界空间下的法线
float3 wPos : TEXCOORD0; //世界空间下的顶点坐标
};

fixed4 _MainColor; //属性设置的漫反射颜色
fixed4 _SpecularColor; //属性设置的材质高光颜色
float _SpecularNum; //属性设置的光泽度

//计算兰伯特光照模型 颜色相关函数(逐片元)
//参数:
// wNormal: 世界空间下顶点的法线信息
fixed3 getFragLambertColor(in float3 wNormal)
{
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); //将光源0的位置标准化,得到方向,用于计算夹角
//兰伯特光照模型的实现,这里的颜色计算只取rgb,不考虑透明度的情况
fixed3 color = _LightColor0.rgb * _MainColor.rgb * max(0, dot(wNormal, lightDir));
return color;
}

//计算Blinn-Phong高光反射光照模型 颜色相关函数(逐片元)
//参数:
// wPos: 世界空间下顶点坐标
// wNormal: 世界空间下顶点的法线信息
fixed3 getFragSpecularColor(in float3 wPos, in float3 wNormal)
{
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - wPos); //计算观察方向
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); //标准化光源方向
float3 halfA = normalize(viewDir + lightDir); //将光源方向和观察方向相加得到其半角向量,并标准化
//Blinn-Phong高光反射模型的实现,这里的颜色计算只取rgb,不考虑透明度的情况
fixed3 color = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(wNormal, halfA)), _SpecularNum);
return color;
}

v2f vert (appdata_base v)
{
v2f v2fData;
v2fData.svPos = UnityObjectToClipPos(v.vertex); //顶点转换到裁剪空间
v2fData.wNormal = UnityObjectToWorldNormal(v.normal); //法线转换到世界空间
v2fData.wPos = mul(unity_ObjectToWorld, v.vertex).xyz; //顶点转换到世界空间
return v2fData;
}

fixed4 frag (v2f i) : SV_Target
{
//计算Blinn-Phong式光照模型需要的各种颜色
fixed3 lambertColor = getFragLambertColor(i.wNormal); //计算漫反射
fixed3 specularColor = getFragSpecularColor(i.wPos, i.wNormal); //计算高光反射颜色
fixed atten = 1; //衰减值,由于Bass Pass只处理平行光,因此衰减值默认为1
//计算Blinn-Phong式光照模型颜色,衰减值 需要乘以 漫反射颜色 和 高光反射颜色 的和
fixed3 blinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + specularColor) * atten;

return fixed4(blinnPhongColor.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 svPos : SV_POSITION; //裁剪空间下的顶点坐标
float3 wNormal : NORMAL; //世界空间下的法线
float3 wPos : TEXCOORD0; //世界空间下的顶点坐标
};

fixed4 _MainColor; //属性设置的漫反射颜色
fixed4 _SpecularColor; //属性设置的材质高光颜色
float _SpecularNum; //属性设置的光泽度

v2f vert (appdata_base v)
{
v2f v2fData;
v2fData.svPos = UnityObjectToClipPos(v.vertex); //顶点转换到裁剪空间
v2fData.wNormal = UnityObjectToWorldNormal(v.normal); //法线转换到世界空间
v2fData.wPos = mul(unity_ObjectToWorld, v.vertex).xyz; //顶点转换到世界空间
return v2fData;
}

fixed4 frag (v2f i) : SV_Target
{
// 漫反射颜色的计算
fixed3 worldNormal = normalize(i.wNormal);
// 光的方向
#if defined(_DIRECTIONAL_LIGHT)
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); // 平行光 光的方向就是它的位置
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.wPos); // 点光源和聚光灯 光的方向 是 光的位置 - 顶点位置
#endif
fixed3 diffuse = _LightColor0.rgb * _MainColor.rgb * max(0, dot(worldNormal, worldLightDir));

// 高光反射颜色的计算
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.wPos.xyz); // 视角方向
fixed3 halfDir = normalize(worldLightDir + viewDir); // 半角向量
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfDir)), _SpecularNum);

// 衰减值的计算
#ifdef USING_DIRECTIONAL_LIGHT //如果光源是平行光,Unity就会定义USING_DIRECTIONAL_LIGHT这个宏,因此会进入这段逻辑
// 平行光逻辑
fixed atten = 1;
#else
// 如果未定义USING_DIRECTIONAL_LIGHT,则说明此光源非平行光
#if defined(POINT)
// 点光源逻辑
float3 lightCoord = mul(unity_WorldToLight, float4(i.wPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).xx).UNITY_ATTEN_CHANNEL;
#elif defined(SPOT)
// 聚光灯逻辑
float4 lightCoord = mul(unity_WorldToLight, float4(i.wPos, 1));
fixed atten = (lightCoord.z > 0) *
tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w *
tex2D(_LightTextureB0, dot(lightCoord, lightCoord).xx).UNITY_ATTEN_CHANNEL;
#else
// 其他逻辑
fixed atten = 1;
#endif
#endif

// 附加渲染通道内不需要再加上环境光颜色了,因为它只需要计算一次,而之前已经在基础渲染通道中计算了
return fixed4((diffuse + specular) * atten, 1);
}
ENDCG
}

// ShadowCaster Pass 投影阴影通道
Pass
{
Tags { "LightMode" = "ShadowCaster" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster

#include "UnityCG.cginc"

struct v2f
{
V2F_SHADOW_CASTER;
};

v2f vert(appdata_base v)
{
v2f data;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(data);
return data;
}

fixed4 frag(v2f i) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i);
}
ENDCG
}
}
}

投射阴影的实现建议

由于投射阴影相关的代码较为通用,因此建议大家不用自己去实现相关 Shader 代码
直接通过 FallBack​ 调用 Unity 中默认 Shader 中的相关代码即可

除非需要自定义投影的效果,或者 Shader 执行了模型顶点的偏移,需要让阴影同步呈现效果,例如:

  • US3S9L6——顶点动画的注意事项 中的让顶点动画物体投射正确的阴影部分,就使用了自定义的投影通道
  • US4L8-3——消融效果 中让阴影和模型同步出现消融效果