本文最后更新于:2025年4月17日 下午
Unity Shader 中的内置变量(时间篇)
动画效果往往都是把时间添加到一些变量的计算中,以便在时间变化时画面也可以随之变化。Unity Shader 提供了一系列关于时间的内置变量来允许我们方便地在 Shader 中访问运行时间,实现各种动画效果。下表给出了这些内置的时间变量。
名称
类型
描述
_Time
float4
t 是自该场景加载开始所经过的时间,4 个分量的值分别是(t/20, t, 2t, 3t)
_SinTime
float4
t 是时间的正弦值,4 个分量的值分别是(t/8, t/4, t/2, t)
_CosTime
float4
t 是时间的余弦值,4 个分量的值分别是(t/8, t/4, t/2, t)
unity_DeltaTime
float4
dt 是时间增量,4 个分量的值分别是(dt, 1/dt, smoothDt, 1/smoothDt)
纹理动画
纹理动画在游戏中的应用非常广泛。尤其在各种资源都比较局限的移动平台上,我们往往会使用纹理动画来代替复杂的粒子系统等模拟各种动画效果。
序列帧动画
最常见的纹理动画之一就是序列帧动画。序列帧动画的原理非常简单,它像放电影一样,依次播放一系列关键帧图像,当播放速度达到一定数值时,看起来就是一个连续的动画。它的优点在于灵活性很强,我们不需要进行任何物理计算就可以得到非常细腻的动画效果。而它的缺点也很明显,由于序列帧中每张关键帧图像都不一样,因此要制作一张出色的序列帧纹理所需要的美术工程量也比较大。
要想实现序列帧动画,我们要先提供一张包含了关键帧图像的图像。
上述图像包含了 8 $\times$ 8 张关键帧图像,它们的大小相同,而且播放顺序为从左到右、从上到下。下图给出了不同时刻播放的不同动画效果。
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 Shader "Custom/Chapter11/Chapter11-ImageSequenceAnimation" { Properties { _Color ("Color Tint", Color) = (1, 1, 1, 1) _MainTex ("Image Sequence", 2D) = "white" {} _HorizontalAmount ("Horizontal Amount", Float) = 8 _VerticalAmount ("Vertical Amount", Float) = 8 _Speed ("Speed", Range(1, 100)) = 30 } SubShader { Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"} Pass { Tags {"LightMode"="ForwardBase"} ZWrite Off Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; float _HorizontalAmount; float _VerticalAmount; float _Speed; struct a2v { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert(a2v v){ v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); return o; } fixed4 frag(v2f i) : SV_TARGET { float time = floor(_Time.y * _Speed); float row = floor(time / _HorizontalAmount); float column = time - row * _HorizontalAmount; //half2 uv = float2(i.uv.x / _HorizontalAmount, i.uv.y / _VerticalAmount); //uv.x += column / _HorizontalAmount; //uv.y -= row / _VerticalAmount; half2 uv = i.uv + half2(column, -row); uv.x /= _HorizontalAmount; uv.y /= _VerticalAmount; fixed4 c = tex2D(_MainTex, uv); c.rgb *= _Color; return c; } ENDCG } } FallBack "Transparent/VertexLit" }
_MainTex 就是包含了所有关键帧图像的纹理。_HorizontalAmount 和 _VerticalAmount 分别代表了该图像在水平方向和竖直方向包含的关键帧图像的个数。而 _Speed 属性用于控制序列帧动画的播放速度。由于序列帧图像通常包含了透明通道,因此可以被当成是一个半透明对象。在这里我们使用半透明的“标配”来设置它的 SubShader 标签,即把 Queue 和 RenderType 设置成 Transparent,把 IgnoreProjector 设置为 True。在 Pass 中,我们使用 Blend 命令来开启并设置混合模式,同时关闭了深度写入。
要播放帧动画,从本质上来说,我们需要计算出每个时刻需要播放的关键帧在纹理中的位置。而由于序列帧纹理都是按行按列排列的,因此这个位置可以认为是该关键帧所在的行列索引数。因此,在片元着色器代码的前三行中我们计算了行列数,其中使用了 Unity 内置时间变量 _Time,_Time.y 就是自该场景加载后所经过的时间。我们首先把 _Time.y 和速度属性 _Speed 相乘来得到模拟的时间,并使用 CG 的 floor 函数对结果值取整来得到整数时间 time。然后,使用 time 除以 _HorizontalAmount 的结果值的商来作为当前对应的行索引,除法结果的余数则是列索引。接下来,我们需要使用行列索引值来构建真正的采样坐标。由于序列帧图像包含了许多关键帧图像,这意味着采样坐标需要映射到每个关键帧图像的坐标范围内。我们可以首先把原纹理坐标 i.uv 按行数和列数进行等分,得到每个子图像的纹理坐标范围。然后,我们需要使用当前的行列数对上面的结果进行偏移,得到当前子图像的纹理坐标。需要注意的是,对竖直方向的坐标偏移需要使用减法,这是因为在 Unity 中纹理坐标竖直方向的顺序(从下到上逐渐增大)和序列帧纹理中的顺序(播放顺序是从上到下)是相反的。这对应了上面代码中注释掉的代码部分。我们可以把上述过程中的除法整合到一起,就得到了注释下方的代码。这样,我们就得到了真正的纹理采样坐标。
滚动的背景
很多 2D 游戏都使用了不断滚动的背景来模拟游戏角色在场景中的穿梭,这些背景往往包含了多个层(layers)来模拟一种视差效果。而这些背景的实现往往就是利用了纹理动画。
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 Shader "Custom/Chapter11/Chapter11-ScrollingBackground" { Properties { _MainTex ("Base Layer (RGB)", 2D) = "white" {} _DetailTex ("2nd Layer (RGB)", 2D) = "white" {} _ScrollX ("Base layer Scroll Speed", Float) = 1.0 _Scroll2X ("2nd layer Scroll Speed", Float) = 1.0 _Multiplier ("Layer Multiplier", Float) = 1 } SubShader { Tags {"RenderType"="Opaque" "Queue"="Geometry"} Pass { Tags {"LightMode"="ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; float4 _MainTex_ST; sampler2D _DetailTex; float4 _DetailTex_ST; float _ScrollX; float _Scroll2X; float _Multiplier; struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float4 uv : TEXCOORD0; }; v2f vert(a2v v){ v2f o; o.pos = UnityObjectToClipPos(v.vertex); //frac函数返回标量或每个矢量中各分量的小数部分 o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y); o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0.0) * _Time.y); return o; } fixed4 frag(v2f i) : SV_TARGET { fixed4 firstLayer = tex2D(_MainTex, i.uv.xy); fixed4 secondLayer = tex2D(_DetailTex, i.uv.zw); fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a); c.rgb *= _Multiplier; return c; } ENDCG } } FallBack "VertexLit" }
其中,_MainTex 和 _DetailTex 分别是第一层(较远)和第二层(较近)的背景纹理,而 _ScrollX 和 _Scroll2X 对应了各自的水平滚动速度。_Multiplier 参数则用于控制纹理的整体亮度。我们首先进行了最基本的顶点变换,把顶点从模型空间变换到裁剪空间中。然后我们计算了两层背景纹理的纹理坐标。为此,我们首先利用 TRANSFORM_TEX 来得到初始的纹理坐标。然后,我们利用内置的 _Time.y 变量在水平方向上对纹理坐标进行偏移,以此来达到滚动的效果。我们把两张纹理的纹理坐标存储在同一个变量 o.uv 中,以减少占用的插值寄存器空间。
我们首先分别利用 i.uv.xy 和 i.uv.zw 对两张背景纹理进行采样。然后,使用第二层纹理的透明通道来混合两张纹理,这使用了 CG 的 lerp 函数。最后,我们使用 _Multiplier 参数和输出颜色进行相乘,以调整背景亮度。
顶点动画
流动的河流
河流的模拟是顶点动画最常见的应用之一。它的原理通常就是使用正弦函数等来模拟水流的波动效果。
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 Shader "Custom/Chapter11/Chapter11-Water" { Properties { _MainTex ("Main Tex", 2D) = "white" {} _Color ("Color Tint", Color) = (1, 1, 1, 1) _Magnitude ("Distortion Magnitude", Float) = 1 _Frequency ("Distortion Frequency", Float) = 1 _InvWaveLength ("Distortion Inverse Wave Length", Float) = 10 _Speed ("Speed", Float) = 0.5 } SubShader { // Need to disable batching because of the vertex animation Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"} Pass { Tags { "LightMode"="ForwardBase" } ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Cull Off CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; float _Magnitude; float _Frequency; float _InvWaveLength; float _Speed; struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert(a2v v) { v2f o; float4 offset; offset.yzw = float3(0.0, 0.0, 0.0); offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude; o.pos = UnityObjectToClipPos(v.vertex + offset); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); o.uv += float2(0.0, _Time.y * _Speed); return o; } fixed4 frag(v2f i) : SV_Target { fixed4 c = tex2D(_MainTex, i.uv); c.rgb *= _Color.rgb; return c; } ENDCG } } FallBack "Transparent/VertexLit" }
其中,_MainTex 是河流纹理,_Color 用于控制整体颜色,_Magnitude 用于控制水流波动的幅度,_Frequency 用于控制波动频率,_InvWaveLength 用于控制波长的倒数(_InvWaveLength 越大,波长越小),_Speed 用于控制河流纹理的移动速度。在上面的设置中,我们除了为透明效果设置 Queue、IgnoreProjector 和 RenderType 外,还设置了一个新的标签——DisableBatching 。一些 SubShader 在使用 Unity 的批处理功能时会出现问题,这时可以通过该标签来直接指明是否对该 SubShader 使用批处理。而这些需要特殊处理的 Shader 通常就是指包含了模型空间的顶点动画的 Shader。这是因为,批处理会合并所有相关的模型,而这些模型各自的模型空间就会丢失。而在本例中,我们需要在物体的模型空间下对顶点位置进行偏移。因此,在这里需要取消对该 Shader 的批处理操作。
我们关闭了深度写入,开启并设置了混合模式,并关闭了剔除功能。这是为了让水流的每个面都能显示。
我们首先计算顶点位移量。我们只希望对顶点的 x 方向进行位移,因此 yzw 的位移量被设置为 0。然后,我们利用 _Frequency 属性和内置的 _Time.y 变量来控制正弦函数的频率。为了让不同位置具有不同的位移,我们对上述结果加上了模型空间下的位置分量,并乘以 _InvWaveLength 来控制波长。最后,我们对结果值乘以 _Magnitude 属性来控制波动幅度,得到最终的位移。剩下的工作,我们只需要把位移量添加到顶点位置上,在进行正常的顶点变换即可。我们还进行了纹理动画,即使用 _Time.y 和 _Speed 来控制在水平方向上的纹理动画。
广告牌
另一种常见的顶点动画就是广告牌技术(Billboarding) 。广告牌技术会根据视角方向来旋转一个被纹理着色的多边形(通常就是简单的四边形,这个多边形就是广告牌),使得多边形看起来好像总是面对着摄像机。广告牌技术被用于很多应用,比如渲染烟雾、云朵、闪光效果等。
广告牌技术的本质就是构建旋转矩阵,而我们知道一个变换矩阵需要三个基向量。广告牌技术使用的基向量通常就是表面法线(normal)、指向上的方向(up)以及 指向右的方向(right) 。除此之外,我们还需要指定一个锚点(anchor location) ,这个锚点在旋转过程中是固定不变的,以此来确定多边形在空间中的位置。
广告牌技术的难点在于,如何根据需求来构建 3 个互相正交的基向量。计算过程通常是,我们首先会通过初始计算得到目标的表面法线(例如就是视角方向)和指向上的方向,而两者往往是不垂直的。但是,两者其中之一是固定的,例如当模拟草丛时,我们希望广告牌的指向上的方向永远是(0, 1, 0),而法线方向应该随视角发生变化;而当模拟粒子效果时,我们希望广告牌的法线方向是固定的,即总是指向视角方向,指向上的方向则可以发生变化。我们假设法线方向是固定的,首先,我们根据初始的表面法线和指向上的方向来计算出目标方向的指向右的方向(通过叉积操作):
$$
right = up \times normal
$$
对其归一化后,再由法线方向和指向右的方向计算出正交的指向上的方向即可:
$$
up’ = normal \times right
$$
至此,我们就可以得到用于旋转的三个正交基了。下图给出了上述计算过程的图示。如果指向上的方向是固定的,计算过程也是类似的。
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 Shader "Custom/Chapter11/Chapter11-Billboard" { Properties { _MainTex ("Main Tex", 2D) = "white" {} _Color ("Color Tint", Color) = (1, 1, 1, 1) _VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1 } SubShader { // Need to disable batching because of the vertex animation Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"} Pass { Tags {"LightMode" = "ForwardBase"} ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Cull Off CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; fixed _VerticalBillboarding; struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert (a2v v) { v2f o; // Suppose the center in object space is fixed float3 center = float3(0, 0, 0); float3 viewer = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1)); float3 normalDir = viewer - center; // If _VerticalBillboarding equals 1, we use the desired view dir as the normal dir // Which means the normal dir is fixed // Or if _VerticalBillboarding equals 0, the y of normal is 0 // Which means the up dir is fixed normalDir.y =normalDir.y * _VerticalBillboarding; normalDir = normalize(normalDir); // Get the approximate up dir // If normal dir is already towards up, then the up dir is towards front float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0); float3 rightDir = normalize(cross(upDir, normalDir)); upDir = normalize(cross(normalDir, rightDir)); // Use the three vectors to rotate the quad float3 centerOffs = v.vertex.xyz - center; float3 localPos = center + rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.z; o.pos = UnityObjectToClipPos(float4(localPos, 1)); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 c = tex2D (_MainTex, i.uv); c.rgb *= _Color.rgb; return c; } ENDCG } } FallBack "Transparent/VertexLit" }
其中,_MainTex 是广告牌显示的透明纹理,_Color 用于控制显示整体颜色,_VerticalBillboarding 则用于调整是固定法线还是固定指向上的方向,即约束垂直方向的程度。在广告牌技术中,我们需要使用物体的模型空间下的位置来作为锚点进行计算,为了让模型各自的模型空间不丢失,我们需要取消对该 Shader 的批处理操作。我们关闭了深度写入,开启并设置了混合模式,并关闭了剔除功能,这是为了让广告牌的每个面都能显示。
顶点着色器是我们的核心,所有的计算都是再模型空间下进行的。我们首先选择模型空间的原点作为广告牌的锚点,并利用内置变量获取模型空间下的视角位置,然后开始计算 3 个正交矢量。首先,我们根据观察位置和锚点计算目标法线方向,并根据 _VerticalBillboarding 属性来控制垂直方向上的约束度。当 _VerticalBillboarding 为 1 时,意味着法线方向固定为视角方向;当 _VerticalBillboarding 为 0 时,意味着向上方向固定为(0, 1, 0)。最后,我们需要对计算得到的法线方向进行归一化操作来得到单位矢量。接着,我们得到了粗略的向上方向。为了防止法线方向和向上方向平行(如果平行,那么叉积得到的结果将是错误的),我们对法线方向的 y 分量进行判断,以得到合适的向上方向。然后,根据法线方向和粗略的向上方向得到向右方向,并对结果进行归一化。但由于此时向上的方向还是不准确的,我们又根据准确的法线方向和向右方向得到最后的向上方向。这样,我们得到了所需的 3 个正交基向量。我们根据原始的位置相对于锚点的偏移量以及 3 个正交基矢量,以计算得到新的顶点位置,最后,把模型空间的顶点位置变换到裁剪空间中。
需要说明的是,在上面的例子中,我们使用的是 Unity 自带的四边形(Quad)来作为广告牌,而不能使用自带的平面(Plane)。这是因为,我们的代码是建立在一个竖直摆放的多边形的基础上的,也就是说,这个多边形的顶点结构需要满足在模型空间下是竖直排列的。只有这样,我们才能使用 v.vertex 来计算得到正确的相对于中心的位置偏移量。
注意事项
顶点动画虽然非常灵活有效,但有一些注意事项需要在此提醒一下。
首先,如果我们在模型空间下进行了一些顶点动画,那么批处理往往就会破坏这种动画效果。这时,我们可以通过 SubShader 中的 DisableBatching 标签来强制取消对该 Unity Shader 的批处理。然而,取消批处理会带来一定的性能下降,增加了 Draw Call,因此我们应该尽量避免使用模型空间的中心来作为锚点,我们可以利用顶点颜色来存储每个顶点到锚点的距离值,这种做法在商业游戏中很常见。
其次,如果我们想要对包含了顶点动画的物体添加阴影,那么如果仍然使用内置的 Diffuse 等包含的阴影的 Pass 来渲染,就得不到正确的阴影效果(这里指的是无法向其他物体正确地投射阴影)。这是因为,Unity 的阴影绘制需要调用一个 ShadowCasterPass,而如果直接使用这些内置的 ShadowCasterPass,这个 Pass 中并没有进行相关的顶点动画,因此 Unity 会仍然按照原来的顶点位置来计算阴影,这并不是我们希望看到的。这时,我们就需要提供一个自定义的 ShadowCasterPass,在这个 Pass 中,我们将进行同样的顶点变换过程。需要注意的是,在前面的实现中,如果涉及半透明物体我们都把 Fallback 设置成了 Transparent/VertexLit,而 Transparent/VertexLit 没有定义 ShadowCasterPass,因此也就不会产生阴影。
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 Shader "Custom/Chapter11/Chapter11-VertexAnimationWithShadow" { Properties { _MainTex ("Main Tex", 2D) = "white" {} _Color ("Color Tint", Color) = (1, 1, 1, 1) _Magnitude ("Distortion Magnitude", Float) = 1 _Frequency ("Distortion Frequency", Float) = 1 _InvWaveLength ("Distortion Inverse Wave Length", Float) = 10 _Speed ("Speed", Float) = 0.5 } SubShader { // Need to disable batching because of the vertex animation Tags {"DisableBatching"="True"} Pass { Tags { "LightMode"="ForwardBase" } Cull Off CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; float _Magnitude; float _Frequency; float _InvWaveLength; float _Speed; struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert(a2v v) { v2f o; float4 offset; offset.yzw = float3(0.0, 0.0, 0.0); offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude; o.pos = UnityObjectToClipPos(v.vertex + offset); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); o.uv += float2(0.0, _Time.y * _Speed); return o; } fixed4 frag(v2f i) : SV_Target { fixed4 c = tex2D(_MainTex, i.uv); c.rgb *= _Color.rgb; return c; } ENDCG } // Pass to render object as a shadow caster Pass { Tags { "LightMode" = "ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #include "UnityCG.cginc" float _Magnitude; float _Frequency; float _InvWaveLength; float _Speed; struct v2f { V2F_SHADOW_CASTER; }; v2f vert(appdata_base v) { v2f o; float4 offset; offset.yzw = float3(0.0, 0.0, 0.0); offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude; v.vertex = v.vertex + offset; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) return o; } fixed4 frag(v2f i) : SV_Target { SHADOW_CASTER_FRAGMENT(i) } ENDCG } } FallBack "VertexLit" }
阴影投射的重点在于我们需要按正常 Pass 的处理来剔除片元或进行顶点动画,以便阴影可以和物体正常渲染的结果相匹配。在自定义的阴影投射的 Pass 中,我们通常会使用 Unity 提供的内置宏 V2F_SHADOW_CASTER、TRANSFER_SHADOW_CASTER_NORMALOFFSET(旧版本中会使用 TRANSFER_SHADOW_CASTER)和 SHADOW_CASTER_FRAGMENT 来计算阴影投射时需要的各种变量,而我们可以只关注自定义计算的部分。在上面的代码中,我们首先在 v2f 结构体中利用 V2F_SHADOW_CASTER 来定义阴影投射所需要定义的变量。随后,在顶点着色器中,我们首先按之前对顶点的处理方法计算顶点的偏移量,不同的是,我们直接把偏移值加到顶点位置变量中,再使用 TRANSFER_SHADOW_CASTER_NORMALOFFSET 来让 Unity 为我们完成剩下的事情。再片元着色器中,我们直接使用 SHADOW_CASTER_FRAGMENT 来让 Unity 自动完成阴影投射的部分,把结果输出到深度图和阴影映射纹理中。
通过 Unity 提供的这 3 个内置宏(在 UnityCG.cginc 文件中被定义),我们可以方便地自定义需要的阴影投射的 Pass,但由于这些宏里需要使用一些特定的输入变量,因此我们需要保证为它们提供了这些变量。例如,TRANSFER_SHADOW_CASTER_NORMALOFFSET 会使用名称 v 作为输入结构体,v 中需要包含顶点位置 v.vertex 和顶点法线 v.normal 的信息,我们可以直接使用内置的 appdata_base 结构体,它包含了这些必需的顶点变量。如果我们需要进行顶点动画,可以在顶点着色器中直接修改 v.vertex,再传递给 TRANSFER_SHADOW_CASTER_NORMALOFFSET 即可。