
写在前面
阴影是计算机图形学中一个很重要的部分,阴影的加入使得物体更加具有立体感,也有助于我们理解物体间的相互位置关系和大小。
实时阴影的实现方法有很多种,shadowMap适用性最好,但性能开销也大,有时候我们的项目其实并不需要那么通用的阴影,我们只需要一个“适用某些特定场合”的一个“看起来正确”的实时阴影。
本文所说的,就是一种利用顶点投射的方法实现的实时阴影技术,在一些阴影质量要求不高,地面平整的项目是一个非常合适的方案,现在很火的手游《王者荣耀》就用了类似的技术。
(经提醒,这个技术叫平面投影阴影(Planar Projected Shadows)技术,由Jim Blinn 1988年提出。http://www.twinklingstar.cn/2015/1717/tech-of-shadows/#21_Blinns)
原理
忽略自身阴影不谈,如果我们只考虑物体在地面上的阴影的话,其实就可以把这个问题简单概括为求一个物体每一个顶点在某个平面上的投影位置了。
公式推导
说到求投影,我当时首先想到的就是点积,后来经群友提醒,其实可以更进一步简化为求相似三角形,这样理解起来似乎还更简单些,以下是推导过程,为了简化计算,我们在二维空间内进行推导。
根据已知条件,我们可以得到一个这样的题目:已知平面坐标系内一个单位向量L(Lx,Ly),坐标系内一点M(Mx,My),求点M沿着L方向 在y = h上的投影位置P,如下图所示:

根据相似三角形定理,我们很容易可以得出下面的式子:

于是我们有:

在shader中实现
有了公式以后,剩下的就简单了,我们只需要在shader中多写一个pass,在这个pass中把所有的顶点移动到投影的位置进行渲染即可,注意要转换到世界空间中进行计算,核心代码如下:
//阴影pass
Pass
{
Name "Shadow"
//用使用模板测试以保证alpha显示正确
Stencil
{
Ref 0
Comp equal
Pass incrWrap
Fail keep
ZFail keep
}
//透明混合模式
Blend SrcAlpha OneMinusSrcAlpha
//关闭深度写入
ZWrite off
//深度稍微偏移防止阴影与地面穿插
Offset -1 , 0
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 color : COLOR;
};
float4 _LightDir;
float4 _ShadowColor;
float _ShadowFalloff;
float _ShadowY;//地面高度
float3 ShadowProjectPos(float4 vertPos)
{
float3 shadowPos;
//得到顶点的世界空间坐标
float3 worldPos = mul(unity_ObjectToWorld , vertPos).xyz;
//灯光方向
float3 lightDir = normalize(_LightDir.xyz);
//阴影的世界空间坐标(低于地面的部分不做改变)
shadowPos.y = min(worldPos.y , _ShadowY);
shadowPos.xz = worldPos.xz - lightDir.xz * max(0 , worldPos.y - _ShadowY) / lightDir.y;
return shadowPos;
}
v2f vert (appdata v)
{
v2f o;
//得到阴影的世界空间坐标
float3 shadowPos = ShadowProjectPos(v.vertex);
//转换到裁切空间
o.vertex = UnityWorldToClipPos(shadowPos);
//得到中心点世界坐标
float3 center =float3(unity_ObjectToWorld[0].w , _ShadowY , unity_ObjectToWorld[2].w);
//计算阴影衰减
float falloff = 1-saturate(distance(shadowPos , center) * _ShadowFalloff);
//阴影颜色
o.color = _ShadowColor;
o.color.a *= falloff;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return i.color;
}
ENDCG
}
其中_LightDir.xyz是灯光方向,_LightDir.w是地面高度,_ShadowColor为阴影颜色,这几个值可以设一个全局变量对场景中的所有物体统一赋值。
重叠的面会导致透明混合结果错误,用Stencil解决,最后用顶点和中心点的距离算一个阴影衰减
最终效果如下:


网友评论