白纸一张

三十而立,四十而不惑

0%

(十五)面片阴影的一些改进思考

概述

在之前的项目几种简单的阴影实现方法中,提到了用面片实现阴影的方式。当时提到,这种方式比较受限,只适用于平整的地面。最近在回顾这篇文章的时候,当看到第三部分的球体阴影时,忽然想到,是不是可以使用球体阴影的思想,来解决面片阴影的限制呢?

思路

基本的思路是:在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", 2D) = "white" { }
}
SubShader
{
Tags
{
"Queue" = "Geometry" "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "IgnoreProjector" = "True"
}

Pass
{
Tags
{
"LightMode" = "UniversalForward"
}
Cull Off

HLSLPROGRAM
// Required to compile gles 2.0 with standard SRP library
// All shaders must be compiled with HLSLcc and currently only gles is not using HLSLcc by default
#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;
}

最终的效果如下所示:

最终的代码如下

总结

回顾一开始说的用面片实现阴影的问题,经过我们的改进,现在也适用于非平整的地面了。

参考

-------------本文结束感谢您的阅读-------------