平面投影

  • 平面投影是计算出来的,不需要实时光,消耗很小,王者荣耀等许多游戏都是用的这种,已经是比较成熟好用的东西了,拿来记录学习一下。

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
Shader "Custom/PlanarShadow_Circle"
{
Properties
{
_Tint("_Tint", Color) = (1,1,1,1)
_MainTex("_MainTex (albedo)", 2D) = "white" {}

[Header(Alpha)]
[Toggle(_CLIPPING)] _Clipping ("Alpha Clipping", Float) = 1
_Cutoff("_Cutoff (Alpha Cutoff)", Range(0.0, 1.0)) = 0.5 // alpha clip threshold

[Header(Shadow)]
// _GroundHeight("_GroundHeight", Range(-100, 100)) = 0
_GroundHeight("_GroundHeight", Float) = 0
_ShadowColor("_ShadowColor", Color) = (0,0,0,1)
_ShadowFalloff("_ShadowFalloff", Range(0,1)) = 0.05
_LightDir("_LightDir", Vector) = (4.83,8.61,-5.33,0)
_ShadowShowDis("_ShadowShowDis", Float) = 2

// Blending state
[HideInInspector] _SrcBlend("__src", Float) = 1.0
[HideInInspector] _DstBlend("__dst", Float) = 0.0
[HideInInspector] _ZWrite("__zw", Float) = 1.0
[HideInInspector] _Cull("__cull", Float) = 2.0
}
SubShader
{
// Planar Shadows平面阴影
Pass
{
Name "PlanarShadow"

//用使用模板测试以保证alpha显示正确
Stencil
{
Ref 0
Comp equal
Pass incrWrap
Fail keep
ZFail keep
}

Cull Off

//透明混合模式
Blend SrcAlpha OneMinusSrcAlpha

//关闭深度写入
ZWrite off

//深度稍微偏移防止阴影与地面穿插
Offset -1 , 0

HLSLPROGRAM
#pragma shader_feature _CLIPPING
#pragma shader_feature _ALPHATEST_ON
#pragma shader_feature _ALPHAPREMULTIPLY_ON

#include "UnityCG.cginc"

#pragma vertex vert
#pragma fragment frag

float _GroundHeight;
float4 _ShadowColor;
float4 _LightDir;
float _ShadowFalloff;
half4 _Tint;
sampler2D _MainTex;
float4 _MainTex_ST;
float _Clipping;
half _Cutoff;
float _ShadowShowDis;

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float4 vertex : SV_POSITION;
float4 color : COLOR;
float2 uv : TEXCOORD0;
};

float3 ShadowProjectPos(float4 vertPos)
{
float3 shadowPos;

//得到顶点的世界空间坐标
float3 worldPos = mul(unity_ObjectToWorld , vertPos).xyz;

//灯光方向
float3 lightDir = normalize(_LightDir.xyz);
//float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);

//阴影的世界空间坐标(低于地面的部分不做改变)
shadowPos.y = min(worldPos .y , _GroundHeight);
shadowPos.xz = worldPos .xz - lightDir.xz * max(0 , worldPos .y - _GroundHeight) / lightDir.y;

return shadowPos;
}

float GetAlpha (v2f i) {
float alpha = _Tint.a * tex2D(_MainTex, i.uv.xy).a;
return alpha;
}

v2f vert (appdata v)
{
v2f o;

//得到阴影的世界空间坐标
float3 shadowPos = ShadowProjectPos(v.vertex);

//转换到裁切空间
o.vertex = UnityWorldToClipPos(shadowPos);

//得到中心点世界坐标
float3 center = float3(unity_ObjectToWorld[0].w , _GroundHeight , unity_ObjectToWorld[2].w);
//计算阴影衰减
float falloff = 1-saturate(distance(shadowPos , center) * _ShadowFalloff);
float edge_distance = distance(shadowPos , center);
if(edge_distance > _ShadowShowDis)
{
o.color.a = 0;
}
else
{
//阴影颜色
o.color = _ShadowColor;
o.color.a *= falloff;
}
o.uv = TRANSFORM_TEX(v.uv, _MainTex);

o.uv = TRANSFORM_TEX(v.uv, _MainTex);

return o;
}

fixed4 frag (v2f i) : SV_Target
{
if (_Clipping)
{
float alpha = GetAlpha(i);
i.color.a *= step(_Cutoff, alpha);
}
return i.color;
}
ENDHLSL
}
}
}

过程推理

将模型的顶点沿着光照方向投影到固定平面上,也就是在顶点着色器中做一遍坐标变换,然后我增加了一个非常简单的功能,就是超过某个距离的阴影会被裁剪掉,或者说 alpha 设置为 0。

中间发生的插曲

我在第一次测试的时候用的是标准的 Cube 测试的,可是得到的结果很离谱,如下
离谱

其实 Shader 是没有问题的,只是 Cube 顶点太少了,插值到片元的时候就不能产生预想中的平滑的效果。

参考链接