概述
在之前的项目几种简单的阴影实现方法 中,提到了用面片实现阴影的方式。 当时提到,这种方式比较受限,只适用于平整的地面。最近在回顾这篇文章的时候,当看到第三部分的球体阴影时,忽然想到,是不是可以使用球体阴影的思想,来解决面片阴影的限制呢?
思路
基本的思路是:在C#代码中,实时将角色的世界坐标传递给Shader,作为圆形面片阴影的圆心。然后,在Shader的片元着色器中,计算该片元距离圆心的距离,如果在半径范围内,就是处于阴影区域;否则,就不处于阴影区域。
实验
首先搭建好方便实验的场景。新建一个球体模拟角色,新建两个立方体模拟地面和台阶。如下图所示:
新建C#脚本,将圆形面片的圆心位置和半径传递给Shader:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 using UnityEngine;public class PlaneShadow : MonoBehaviour { public float Radius; public float ShadowFalloff; void Update ( ) { Shader.SetGlobalVector("_CenterPos" , transform.position); Shader.SetGlobalFloat("_CenterRadius" , Radius * Radius); Shader.SetGlobalFloat("_ShadowFalloff" , ShadowFalloff); } }
在上述代码中,ShadowFalloff变量是用来控制阴影从圆心向四周衰减的。圆心处阴影最强,往四周逐渐减弱。
为地面和台阶部分新建Shader。接受C#中传递过来的数据。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 Shader "RoadOfShader/2.3-PlaneShadow/Plane Shadow" { Properties { _MainTex ("Main Tex" , 2 D) = "white" { } } SubShader { Tags { "Queue" = "Geometry" "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "IgnoreProjector" = "True" } Pass { Tags { "LightMode" = "UniversalForward" } Cull Off HLSLPROGRAM #pragma prefer_hlslcc gles #pragma exclude_renderers d3d11_9x #pragma target 2.0 #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" #pragma vertex vert #pragma fragment frag struct Attributes { float4 positionOS: POSITION; float2 uv: TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float2 uv: TEXCOORD0; float3 positionWS: TEXCOORD1; float4 positionCS: SV_POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; float4 _CenterPos; float _CenterRadius; half _ShadowFalloff; CBUFFER_START(UnityPerMaterial) float4 _MainTex_ST; CBUFFER_END TEXTURE2D(_MainTex ) ; SAMPLER(sampler_MainTex ) ; Varyings vert(Attributes input) { Varyings output = (Varyings)0 ; UNITY_SETUP_INSTANCE_ID(input ) ; UNITY_TRANSFER_INSTANCE_ID(input , output ) ; UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output ) ; VertexPositionInputs vertexInput = GetVertexPositionInputs(input .positionOS .xyz ) ; output.positionWS = vertexInput.positionWS; output.positionCS = vertexInput.positionCS; output.uv = TRANSFORM_TEX(input .uv , _MainTex ) ; return output; } half4 frag(Varyings input): SV_Target { UNITY_SETUP_INSTANCE_ID(input ) ; UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input ) ; float3 toCenter = _CenterPos . xyz - input.positionWS; float sqrDistanceXZ = dot(toCenter.xz, toCenter.xz); half atten = (sqrDistanceXZ / _CenterRadius) / _ShadowFalloff; return atten; } ENDHLSL } } }
在片元着色器中,计算顶点和圆心的方向向量,点乘toCenter的xz向量,得到xz平面上方向向量长度的平方。根据方向向量的平方与半径的平方相比较,计算得到一个比率,代表阴影的强度,在和_ShadowFalloff计算之后,得到最终的阴影衰减atten。直接输出atten,效果如下:
黑色部分是阴影的区域,其余白色的部分是正常的区域。下面,改一下Shader代码,使用阴影衰减与采样得到的颜色相乘,得到带阴影的效果。注意改动片元着色器,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 half4 frag(Varyings input): SV_Target { UNITY_SETUP_INSTANCE_ID(input ) ; UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input ) ; float3 toSphere = _CenterPos . xyz - input.positionWS; float sqrDistanceXZ = dot(toSphere.xz, toSphere.xz); half atten = (sqrDistanceXZ / _CenterRadius) / _ShadowFalloff; half4 col = SAMPLE_TEXTURE2D(_MainTex , sampler_MainTex , input .uv ) ; col *= atten; return col; }
效果如下:
可以发现,颜色过渡曝光。这是为什么呢?
1 half atten = (sqrDistanceXZ / _CenterRadius) / _ShadowFalloff;
观察Shader代码发现,在计算atten的时候,没有做限制。随着顶点距离圆心的位置逐渐变远,sqrDistanceXZ趋于无穷大,导致atten会趋于无穷大。这显然不符合我们的预期。可以想到的是,对最终的结果做以下限制,当顶点距离圆心的距离超过半径时,就不会受到阴影的影响了。可以使用saturate()函数来达到这一目的:
1 half atten = saturate((sqrDistanceXZ / _CenterRadius) / _ShadowFalloff);
此时的效果如下:
效果看起来还不错,与原本的圆形面片阴影的效果大致相同。
将模拟角色的球体往左移动到台阶正上方的效果如下:
可以发现,台阶部分有一条垂直的阴影,一直延伸到了地面之下。这是因为,我们在上面计算距离的时候,只使用了xz分量,没有使用y分量,导致在台阶的垂直面上,得到的距离都相同。结合实际的项目经验来说,台阶的高度应该是很小的,所以在地面之上的台阶部分(上图中A区)的阴影是可以接受的。不可接收到,是地面之下的台阶(上图中的B区)的阴影。我们要想办法去除地面之下的台阶阴影。
可以想到的办法是,计算顶点与圆心在竖直方向上的距离差,只有满足一定的条件才会受到阴影的影响。
在C#中,将竖直方向上的阴影距离范围传递给Shader:
1 Shader .SetGlobalFloat ("_HeightRange" , HeightRange);
在片元着色器中,在竖直方向上计算一个遮罩值,用来控制哪些区域会受到阴影的影响:
1 2 3 4 5 6 7 8 9 10 11 half4 frag(Varyings input): SV_Target { UNITY_SETUP_INSTANCE_ID(input ) ; UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input ) ; float3 toSphere = _CenterPos . xyz - input.positionWS; float sqrDistanceXZ = dot(toSphere.xz, toSphere.xz); float yMask = saturate(step(toSphere.y, 0 ) + step(_HeightRange, toSphere.y)); return yMask; }
效果如下所示:
在上图中,黑色区域是竖直方向上可能受到阴影影响的区域,与我们上面的设想相符。
结合之前的效果,修改Shader代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 half4 frag(Varyings input): SV_Target { UNITY_SETUP_INSTANCE_ID(input ) ; UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input ) ; float3 toSphere = _CenterPos . xyz - input.positionWS; float sqrDistanceXZ = dot(toSphere.xz, toSphere.xz); float yMask = step(toSphere.y, 0 ) + step(_HeightRange, toSphere.y); half atten = (sqrDistanceXZ / _CenterRadius) / _ShadowFalloff; half4 col = SAMPLE_TEXTURE2D(_MainTex , sampler_MainTex , input .uv ) ; col *= saturate(atten + yMask); return col; }
最终的效果如下所示:
最终的代码如下
总结
回顾一开始说的用面片实现阴影的问题,经过我们的改进,现在也适用于非平整的地面了。
参考