本文最后更新于:2025年4月17日 下午
Unity 的阴影
阴影是如何实现的
我们先可以考虑真实生活中阴影是如何产生的。当一个光源发射的一条光线遇到一个不透明物体时,这条光线就不可以再继续照亮其他物体(这里不考虑光线反射)。因此这个物体就会向它旁边的物体投射阴影,那些阴影区域的产生是因为光线无法到达这些区域。
在实时渲染中,我们最常使用的是一种名为 Shadow Map 的技术。这种技术理解起来非常简单,它会首先把摄像机的位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是那些摄像机看不到的地方。而 Unity 就是使用的这种技术。
在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity 就会为该光源计算它的阴影映射纹理(shadow map)。这张阴影映射纹理本质上也是一张深度图,它记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置(深度信息)。

那么,在计算阴影映射纹理时,我们如何判定距离它最近的表面位置呢?一种方法是,先把摄像机放置到光源的位置上,然后按正常的渲染流程,即调用 Base Pass 和 Additional Pass 来更新深度信息,得到阴影映射纹理。但这种方法会对性能造成一定的浪费,因为我们实际上仅仅需要深度信息而已,而 Base Pass 和 Additional Pass 中往往涉及很多复杂的光照模型计算。因此,Unity 选择使用一个额外的 Pass 来专门更新光源的阴影映射纹理,这个 Pass 就是 LightMode 标签被设置为 ShadowCaster 的 Pass。这个 Pass 的渲染目标不是帧缓冲,而是阴影映射纹理(或深度纹理)。Unity 首先把摄像机放置到光源的位置上,然后调用该 Pass,通过对顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。因此,当开启了光源的阴影效果后,底层渲染引擎首先会在当前渲染物体的 Unity Shader 中找到 LightMode 为 ShadowCaster 的 Pass,如果没有,它就会在 Fallback 指定的 Unity Shader 中继续寻找,如果仍然没有找到,该物体就无法向其他物体投射阴影(但它仍然可以接收来自其他物体的阴影)。当找到了一个 LightMode 为 ShadowCaster 的 Pass 后,Unity 会使用该 Pass 来更新光源的阴影映射纹理。
在传统的阴影映射纹理的实现中,我们会在正常渲染的 Pass 中把顶点位置变换到光源空间下,以得到它在光源空间中的三维位置信息。然后使用 xy 分量对阴影映射纹理进行采样,得到阴影映射纹理中该位置的深度信息。如果该深度值小于该顶点的深度值(通常由 z 分量得到),那么说明该点位于阴影中。但 Unity 使用了不同于这种传统的阴影采样技术,即屏幕空间的阴影映射技术(Screenspace Shadow Map)。屏幕空间的阴影映射原本是延迟渲染中产生阴影的方法。需要注意的是,并不是所有的平台 Unity 都会使用这种技术。这是因为,屏幕空间的阴影映射需要显卡支持 MRT,而有些移动平台不支持这种特性。
当使用了屏幕空间的阴影映射技术时,Unity 首先会通过调用 LightMode 为 ShadowCaster 的 Pass 来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理.然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却处于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需要在 Shader 中对阴影图进行采样。由于阴影图是屏幕空间下的,因此我们首先需要把表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。
总结一下,一个物体接收来自其他物体的阴影,以及它向其他物体投射阴影是两个过程。
- 如果我们想要一个物体接收来自其他物体的阴影,就必须在 Shader 中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。
- 如果我们想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。在 Unity 中,这个过程是通过为该物体执行 LightMode 为 ShadowCaster 的 Pass 来实现的。如果使用了屏幕空间的投影映射技术,Unity 还会使用这个 Pass 产生一张摄像机的深度纹理。
不透明物体的阴影
1. 让物体投射阴影
在 Unity 中,为了让场景中可以产生阴影,我们首先需要让平行光可以收集阴影信息。这需要在光源的 Light 组件中开启阴影。我们可以通过设置物体的 Mesh Renderer 组件中的 Cast Shadows 和 Receive Shadows 来选择是否让一个物体投射或接收阴影。


Cast Shadows 可以被设置为开启(On)或关闭(Off)。如果开启了 Cast Shadows 属性,那么 Unity 就会把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。正如之前所说,这个过程是通过为该物体执行 LightMode 为 ShadowCaster 的 Pass 来实现的。Receive Shadows 则可以选择是否让物体接收来自其他物体的阴影。如果没有开启 Receive Shadows,那么当我们调用 Unity 的内置宏和变量计算阴影时,这些宏通过判断该物体没有开启接收阴影的功能,就不会在内部为我们计算阴影。
如果 Unity Shader 中没有 LightMode 为 ShadowCaster 的 Pass,Unity 会在 Fallback 回调的 Shader 中查找 LightMode 为 ShadowCaster 的 Pass,如果还是没有找到的话,就会在此回调的 Shader 中的 Fallback 回调中查找。
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
| //Pass to render object as a shadow caster Pass { Name "ShadowCaster" 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 o; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) return o; }
float4 frag(v2f i) : SV_Target { SHADOW_CASTER_FRAGMENT(i) } ENDCG }
|
上面的代码中有一些宏和指令是我们之前没有遇到过的,但它们的作用就是把深度信息写入到渲染目标中。这个 Pass 的渲染目标可以是光源的阴影映射纹理,也可以是摄像机的深度纹理。
2. 让物体接收阴影
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
| Shader "Custom/Chapter9/Chapter9-Shadow" { Properties { _Diffuse("Diffuse", Color) = (1, 1, 1, 1) _Specular("Specular", Color) = (1, 1, 1, 1) _Gloss("Gloss", Range(8.0, 256)) = 20 } SubShader { Tags { "RenderType"="Opaque" }
Pass { // Pass for ambient light & first pixel light(directional light) Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
//Apparently need to add this declaration #pragma multi_compile_fwdbase
#pragma vertex vert #pragma fragment frag
#include "Lighting.cginc" #include "AutoLight.cginc"
fixed4 _Diffuse; fixed4 _Specular; float _Gloss;
struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; };
struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; SHADOW_COORDS(2) };
v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); //可以使用顶点变换矩阵的逆转置矩阵对法线进行变换,首先得到模型空间到世界空间的变换矩阵的 //逆矩阵_World2Object,然后通过调换它在mul函数中的位置,得到和转置矩阵相同的矩阵乘法 //o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
//Pass shadow coordinates to pixel shader TRANSFER_SHADOW(o);
return o; }
fixed4 frag(v2f i) : SV_TARGET { //Use shadow coordinates to sample shadow map fixed shadow = SHADOW_ATTENUATION(i);
//Get ambient term fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); //Compute diffuse term fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir)); fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); fixed3 halfDir = normalize(worldLightDir + viewDir); //Compute Specular term fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal,halfDir)), _Gloss); //The attenuation of directional light is awaly 1 fixed atten = 1.0; return fixed4(ambient + shadow * (diffuse +specular) * atten, 1.0); } ENDCG }
Pass{ //Pass for other pixel lights Tags {"LightMode" = "ForwardAdd"}
Blend One One
CGPROGRAM
#pragma vertex vert #pragma fragment frag
//Apparently need to add this declaration #pragma multi_compile_fwdadd
#include "AutoLight.cginc" #include "Lighting.cginc"
fixed4 _Diffuse; fixed4 _Specular; float _Gloss;
struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; };
struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; };
v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); //可以使用顶点变换矩阵的逆转置矩阵对法线进行变换,首先得到模型空间到世界空间的变换矩阵的 //逆矩阵_World2Object,然后通过调换它在mul函数中的位置,得到和转置矩阵相同的矩阵乘法 //o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; return o; }
fixed4 frag(v2f i) : SV_TARGET { fixed3 worldNormal = normalize(i.worldNormal);
#ifdef USING_DERECTIONAL_LIGHT fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); #else fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz); #endif
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir)); fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); fixed3 halfDir = normalize(worldLightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal,halfDir)), _Gloss);
#ifdef USING_DIRECTIONAL_LIGHT fixed atten = 1.0; #else float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz; fixed atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL; #endif return fixed4((diffuse +specular) * atten, 1.0); } ENDCG } } FallBack "Specular" }
|
我们在顶点着色器的输出结构体 v2f 中添加了一个内置宏 SHADOW_COORDS,这个宏的作用是声明一个用于对阴影纹理采样的坐标。这个宏的参数是下一个可用的插值寄存器的索引值。在顶点着色器返回之前添加另一个内置宏 TRANSFER_SHADOW,这个宏用于在顶点着色器中计算上一步声明的阴影纹理坐标。接着,我们在片元着色器中计算阴影值,这同样使用了一个内置宏 SHADOW_ATTENUATION。SHADOW_COORDS、TRANSFER_SHADOW 和 SHADOW_ATTENUATION 是计算阴影时的“三剑客”。这些内置宏帮助我们在必要时计算光源的阴影。
在前向渲染中,宏 SHADOW_COOEDS 实际上就是声明了一个名为 _ShadowCoord 的阴影纹理坐标变量。而 TRANSFER_SHADOW 的实现会根据平台不同而有所差异。如果当前平台可以使用屏幕空间的阴影映射技术(通过判断是否定义了 UNITY_NO_SCREENSPACE_SHADOWS 来得到),TRANSFER_SHADOW 会调用内置的 ComputeScreenPos 函数来计算 _ShadowCoord;如果该平台不支持屏幕空间的阴影映射技术,就会使用传统的阴影映射技术,TRANSFER_SHADOW 会把顶点坐标从模型空间变换到光源空间后存储到 _ShadowCoord 中。然后,SHADOW_ATTENUATION 负责使用 _ShadowCoord 对相关的纹理进行采样,得到阴影信息。当关闭了阴影后,SHADOW_COORDS 和 TRANSFER_SHADOW 实际没有任何作用,而 SHADOW_ATTENUATION 会直接等同于数值 1。
需要注意的是,这些宏中会使用上下文变量来进行相关计算,例如 TRANSFER_SHADOW 会使用 v.vertex 或 a.pos 来计算坐标,因此为了能够让这些宏正确工作,我们需要保证自定义的变量名和这些宏中使用的变量名相匹配。我们需要保证:a2f 结构体中的顶点坐标变量名必须是 vertex,顶点着色器的输入结构体 a2v 必须命名为 v,且 v2f 中的顶点位置变量必须命名为 pos。
使用帧调试器查看阴影绘制过程
绘制场景的渲染事件可以分为 4 个部分:UpdateDepthTexture,即更新摄像机的深度纹理;RenderShadowmap,即渲染得到平行光的阴影映射纹理;CollectShadows,即根据深度纹理和阴影映射纹理得到屏幕空间的阴影图;

我们首先来看第一个部分:更新摄像机的深度纹理。下图给出了正方体对深度纹理的更新结果。

在上图中,Unity 调用了 Custom/Chapter9-Shadow,SubShader #2 来更新深度纹理,即 Chapter9-Shadow 中的第二个 SubShader。尽管 Chapter9-Shadow 中只定义了一个 SubShader,但正如我们之前所说,Unity 会在它的 Fallback 中找到第二个 SubShader,并使用其中 LightMode 为 ShadowCaster 的 Pass 来更新摄像机的深度纹理。同样,在第二个部分,即渲染得到平行光的阴影映射纹理的过程中,Unity 也是调用了这个 Pass 来得到光源的阴影映射纹理。
在第三个部分中,Unity 会根据之前两步的结果得到屏幕空间的阴影图,如下图所示:

这张图已经包含了最终屏幕上所有阴影区域的阴影。在最后一个部分中,如果物体所使用的 Shader 包含了对这张阴影图的采样就会得到阴影效果。下图给出了这个部分 Unity 是如何一步步绘制出有阴影的画面效果的。

统一管理光照衰减和阴影
在上一篇中我们已经讲过如何在 Unity Shader 的前向渲染路径中计算光照衰减——在 Base Pass 中,平行光的衰减因子总是等于 1,而在 Additional Pass 中,我们需要判断该 Pass 处理的光源类型,再使用内置变量和宏计算衰减因子。实际上,光照衰减和阴影对物体最终的渲染结果的影响本质上是相同的——我们都是把光照衰减因子和阴影值及光照结果相乘得到最终的渲染结果。那么,是不是可以有一个方法可以同时计算两个信息呢?好消息是,Unity 在 Shader 里提供了这样的功能,这主要是通过内置的 UNITY_LIGHT_ATTENUATION 宏来实现的。
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
| Shader "Custom/Chapter9/Chapter9-AttenuationAndShadowUseBuildInFunctions" { Properties { _Diffuse("Diffuse", Color) = (1, 1, 1, 1) _Specular("Specular", Color) = (1, 1, 1, 1) _Gloss("Gloss", Range(8.0, 256)) = 20 } SubShader { Tags { "RenderType"="Opaque" }
Pass { // Pass for ambient light & first pixel light(directional light) Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
//Apparently need to add this declaration #pragma multi_compile_fwdbase
#pragma vertex vert #pragma fragment frag
#include "Lighting.cginc" #include "AutoLight.cginc"
fixed4 _Diffuse; fixed4 _Specular; float _Gloss;
struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; };
struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; SHADOW_COORDS(2) };
v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); //可以使用顶点变换矩阵的逆转置矩阵对法线进行变换,首先得到模型空间到世界空间的变换矩阵的 //逆矩阵_World2Object,然后通过调换它在mul函数中的位置,得到和转置矩阵相同的矩阵乘法 //o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
//Pass shadow coordinates to pixel shader TRANSFER_SHADOW(o);
return o; }
fixed4 frag(v2f i) : SV_TARGET {
//Get ambient term fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); //Compute diffuse term fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir)); fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); fixed3 halfDir = normalize(worldLightDir + viewDir); //Compute Specular term fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal,halfDir)), _Gloss); //fixed atten = 1.0; //UNITY_LIGHT_ATTENUATION not only compute attenuation,but also shadow infos UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); return fixed4(ambient + (diffuse +specular) * atten, 1.0); } ENDCG }
Pass{ //Pass for other pixel lights Tags {"LightMode" = "ForwardAdd"}
Blend One One
CGPROGRAM
#pragma vertex vert #pragma fragment frag
//Apparently need to add this declaration #pragma multi_compile_fwdadd
#include "AutoLight.cginc" #include "Lighting.cginc"
fixed4 _Diffuse; fixed4 _Specular; float _Gloss;
struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; };
struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; };
v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); //可以使用顶点变换矩阵的逆转置矩阵对法线进行变换,首先得到模型空间到世界空间的变换矩阵的 //逆矩阵_World2Object,然后通过调换它在mul函数中的位置,得到和转置矩阵相同的矩阵乘法 //o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_SHADOW(o); return o; }
fixed4 frag(v2f i) : SV_TARGET { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir)); fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); fixed3 halfDir = normalize(worldLightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal,halfDir)), _Gloss); //fixed atten = 1.0; UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); return fixed4((diffuse +specular) * atten, 1.0); } ENDCG } } FallBack "Specular" }
|
UNITY_LIGHT_ATTENUATION 是 Unity 内置的用于计算光照衰减和阴影的宏,我们可以在内置的 AutoLight.cginc 里找到它的相关声明。它接受三个参数,它会将光照衰减和阴影值相乘后的结果存储到第一个参数中。注意到,我们并没有在代码中声明第一个参数 atten,这是因为 UNITY_LIGHT_ATTENUATION 会帮我们声明这个变量。它的第二个参数是结构体 v2f,这个参数会传递给上面讲过的 SHADOW_ATTENUATION,用来计算阴影值。而第三个参数是世界空间的坐标,这个参数会用于计算光源空间下的坐标,再对光照衰减纹理采样来得到光照衰减。Unity 针对不同光源类型、是否启用 cookie 等不同情况声明了多个版本的 UNITY_LIGHT_ATTENUATION。这些不同版本的声明是保证我们可以通过这样一个简单的代码来得到正确结果的关键。
由于使用了 UNITY_LIGHT_ATTENUATION,我们的 Base Pass 和 Additional Pass 的代码得以统一——我们不需要在 Bass Pass 里单独处理阴影,也不需要在 Additional Pass 中判断光源类型来处理光照衰减,一切都只需要通过 UNITY_LIGHT_ATTENUATION 来完成即可。这正是 Unity 内置文件的魅力所在。如果我们希望可以在 Additional Pass 中添加阴影效果,就需要使用 #pragma multi_compile_fwdadd_fullshadows 编译指令来代替 Additional Pass 中的 #pragma multi_compile_fwdadd 指令。这样一来,Unity 也会为这些额外的逐像素光源计算阴影,并传递给 Shader。
透明度物体的阴影
我们从一开始就强调,想要在 Unity 里让物体能够向其他物体投射阴影,一定要在它使用的 Unity Shader 中提供一个 LightMode 为 ShadowCaster 的 Pass。在前面的例子中,我们使用内置的 VertexLit 中提供的 ShadowCaster 来投射阴影。VertexLit 中的 ShadowCaster 实现很简单,它会正常渲染整个物体,然后把深度结果输出到一张深度图或阴影映射纹理中。对于大多数不透明物体来说,把 Fallback 设为 VertexLit 就可以得到正确的阴影。但对于透明物体来说,我们就需要小心处理它的阴影。透明物体的实现通常会使用透明度测试或透明度混合,我们需要小心设置这些物体的 Fallback。
透明度测试的处理比较简单,但如果我们仍然直接使用 VertexLit、Diffuse、Specular 等作为回调,往往无法得到正确的阴影。这是因为透明度测试需要在片元着色器中舍弃某些片元,而 VertexLit 中的阴影投射纹理并没有进行这样的操作。
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
| Shader "Custom/Chapter9/Chapter9-AlphaTestWithShadow" { Properties { _Color ("Main Tint", Color) = (1,1,1,1) _MainTex ("Main Tex", 2D) = "white" {} _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5 } SubShader { Tags { "Queue"="AlphaTest" "IgnoreProhector"="True" "RenderType"="TransparentCutout" } Pass { Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert #pragma fragment frag
#include "Lighting.cginc" #include "AutoLight.cginc"
fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _Cutoff;
struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; };
struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; float2 uv : TEXCOORD2; SHADOW_COORDS(3) };
v2f vert(a2v v){ v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); TRANSFER_SHADOW(o); return o; }
fixed4 frag(v2f i) : SV_Target{ fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed4 texColor = tex2D(_MainTex, i.uv);
clip(texColor.a - _Cutoff); //Equal to //if((texColor.a - _Cutoff) < 0.0){ // discard; //} fixed3 albedo = texColor.rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir)); UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); return fixed4(ambient + diffuse * atten, 1.0); }
ENDCG } } FallBack "Transparent/Cutout/VertexLit" }
|
在结构体 v2f 中,由于我们已经占用了 3 个插值寄存器(使用 TEXCOORD0、TEXCOORD1 和 TEXCOORD2 修饰的变量),因此 SHADOW_COORDS 中传入的参数是 3,这意味着,阴影纹理坐标将占用第四个插值寄存器 TEXCOOERD3。内置的 VertexLit 中提供的 ShadowCaster 的 Pass 中没有进行任何透明度测试的计算,因此,它会把整个物体的深度信息渲染到深度图和阴影映射纹理中。因此,如果我们想要得到经过透明度测试后的阴影效果,就需要一个有透明度测试功能的 ShadowCaster Pass。所以,我们把 Fallback 设置为 Transparent/Cutout/VertexLit,它的 ShadowCaster Pass 计算了透明度测试,因此会把裁剪后的物体深度信息写入深度图和阴影映射纹理中。但需要注意的是,由于 Transparent/Cutout/VertexLit 中计算透明度测试时,使用了名为 _Cutoff 的属性,因此,这要求我们的 Shader 中也必须提供名为 _Cutoff 的属性。否则,同样无法得到正确的阴影结果。

但是,这样的结果仍然有一些问题,例如出现了一些不应该透过光的部分。出现这种情况的原因是,默认情况下把物体渲染到深度图和阴影映射纹理中仅考虑物体的正面。但对于本例的正方体来说,由于一些面完全背对光源,因此这些面的深度信息没有加入到阴影映射纹理的计算中。为了得到正确的结果,我们可以将正方体的 Mesh Renderer 组件中的 Cast Shadows 属性设置为 Two Sided,强制 Unity 在计算阴影映射纹理时计算所有面的深度信息。

与透明度测试的物体相比,想要为使用透明度混合的物体添加阴影是一件比较复杂的事情。事实上,所有内置的透明度混合的 Unity Shader,如 Transparent/VertexLit 等,都没有包含阴影投射的 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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| Shader "Custom/Chapter9/Chapter9-AlphaBlendWithShadow" { Properties { _Color ("Main Tint", Color) = (1,1,1,1) _MainTex ("Main Tex", 2D) = "white" {} _AlphaScale ("Alpha Scale", Range(0, 1)) = 1 } SubShader { Tags { "Queue"="Transparent" "IgnoreProhector"="True" "RenderType"="Transparent" } Pass { Tags { "LightMode" = "ForwardBase" } ZWrite Off Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert #pragma fragment frag
#include "Lighting.cginc" #include "AutoLight.cginc"
fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _AlphaScale;
struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; };
struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; float2 uv : TEXCOORD2; SHADOW_COORDS(3) };
v2f vert(a2v v){ v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); TRANSFER_SHADOW(o); return o; }
fixed4 frag(v2f i) : SV_Target{ fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed4 texColor = tex2D(_MainTex, i.uv); fixed3 albedo = texColor.rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir)); UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); return fixed4(ambient + diffuse * atten, texColor.a * _AlphaScale); }
ENDCG } } FallBack "Transparent/VertexLit" }
|

Unity 会这样处理半透明物体是有它的原因的。由于透明度混合需要关闭深度写入,由此带来的问题也影响了阴影的生成。总体来说,要想为这些半透明物体产生正确的阴影,需要在每个光源空间下仍然严格按照从后往前的顺序进行渲染,这会让阴影处理变得非常复杂,而且也会影响性能。因此,在 Unity 中,所有内置的半透明 Shader 是不会产生任何阴影效果的。当然,我们也可以使用一些方法来强制为半透明物体生成阴影,这可以通过把它们的 Fallback 设置为 VertexLit、Diffuse 这些不透明物体使用的 Unity Shader,这样 Unity 就会在它的 Fallback 找到一个阴影投射的 Pass。然后,我们可以通过物体的 Mesh Renderer 组件上的 Cast Shadows 和 Receive Shadows 选项来控制是否需要向其他物体投射或接收阴影。

可以看出,此时右侧平面的阴影投射到了半透明的立方体上,但它不会再穿透立方体把阴影投射到下方的平面上,这其实是不正确的。同时,立方体也可以把自身的阴影投射到下面的平面上。