文章目录
整体运行效果
思路
将模型压扁后,绘制即可
Shader
写成 shader 代码就是
// 平面上的任意一点
float4 _PlanePos;
// 平面法线
float4 _PlaneNor;
...
float3 OffsetToPlanarShadowPos(float3 wpos) {
// 方向光的位置方位,(单位向量)
float3 L = _WorldSpaceLightPos0.xyz;
// 平面法线,(单位向量)
float3 N = _PlaneNor;
float d1 = dot(L, N);
float d2 = dot(wpos - _PlanePos.xyz, N);
return wpos + -L * (d2 / d1);
}
为了简单起见,我们直接使用加多一个 pass 来绘制 Planner Shadow 即可
看看执行效果:
右侧的图,可以看到阴影绘制的是一个网格
问题
从上面的效果图可以发现两个问题,我们看看下面 Gif 动画会更明显:
- Planar Shadow 与 平面地表得 z-fighting 问题
- 绘制半透明阴影的重叠问题
Z-Fighting,解决:按法线方向偏移一丢丢
Shader
// 平面上的任意一点
float4 _PlanePos;
// 平面法线
float4 _PlaneNor;
...
float3 OffsetToPlanarShadowPos(float3 wpos) {
// 方向光的位置方位,(单位向量)
float3 L = _WorldSpaceLightPos0.xyz;
// 平面法线,(单位向量)
float3 N = _PlaneNor;
float d1 = dot(L, N);
float d2 = dot(wpos - _PlanePos.xyz, N);
return wpos + -L * (d2 / d1) + N * _PlaneNor.w;
}
注意上面添加了一个:+ N * _PlaneNor.w
,_PlaneNor.w
通常很小的值就可以了,我这填了个:0.01
再绘制看看效果:
OK,Z-Fighting 解决了
绘制 Alpha 混合重叠,解决:使用 stencil buffer 来规避
但是发现半透明阴影的 alpha 区域重叠了
为何出现这个问题
如果对象只有单一一个模型,那么这个问题稍微没那么好重现
刚刚好我们这个模型有好几个 SubMesh:身体、头发、皮肤、武器,等
所以不同的网格的绘制都是半透明混合:Blend SrcAlpha OneMinusSrcAlpha 的话,肯定会有上述的重叠区域的问题
解决方法
使用 Stencil(模板测试、模板缓存) 来解决
这样我们再运行看看效果:
OK,没问题了
关于 stencil 的文章我之前也有写过:
阴影距离淡出
在玩王者荣耀的时候,我们都可以发现:阴影显示的不是完整的,根据了一个固定的距离就淡出了
Shader 代码主要查看:函数的返回值 fade
fixed OffsetToPlanarShadowPosAndFade(inout float3 wpos) {
// 方向光的位置方位,(单位向量)
float3 L = _WorldSpaceLightPos0.xyz;
// 平面法线,(单位向量)
float3 N = _PlaneNor;
float d1 = dot(L, N);
float d2 = dot(wpos - _PlanePos.xyz, N);
float new_pos_offset_len = d2 / d1;
wpos += -L * new_pos_offset_len;
// 在平面法线发向上偏移一丢丢
wpos += N * _PlaneNor.w;
// 计算 fade (这部分也可以抽离到另一个函数来处理,但是 L 也可以使用上面的,所以暂时写在这里)
if (_PlanePos.w > 0) {
float3 wpos_h_0 = -N * d2;
float3 wpos_h_0_2_new_wpos = wpos - wpos_h_0;
float len = pow(dot(wpos_h_0_2_new_wpos, wpos_h_0_2_new_wpos), 2);
float cfg_len = _PlanePos.w * _PlanePos.w;
fixed fade = smoothstep(0, 1, 1 - saturate(len / cfg_len));
// fixed fade = 1 - saturate(len / cfg_len);
return fade;
} else {
return 1;
}
}
结合下图来看
思路就是:
v
=
w
p
′
−
p
′
v = wp'-p'
v=wp′−p′ 中,就得
v
v
v 的长度,再除以 _PlanePos.w
即可
最终 fade 值:fade = length(v) / _PlanePos.w
_PlanePos.w
目前我传入了 30 的值
但上面的 shader 中,我们去掉 length(v)
的运算来优化性能,因为length 里面有 sqrt, div 除法
再运行看看效果:
用在 URP 中的 Base Shadow 中
因为上述的方式是添加额外的 pass 来处理的,但是添加 pass 的话,就会打断合批
而我们可以使用 CommandBuffer 来指定在某个提供的事件来绘制所有 Planar Shadow,而且 URP 的合拼严格上来说不是为了减少 DC,而是为了减少 Draw API 调用前的 SetState API
因为 SetState (设置绘制状态值)的数据比较多,而且不能一次设置好,要分多次
所以 URP 使用了 CBuffer 只设置与上次绘制对象使用的 shader 中不同的 CBuffer 数据来 SetState
(意思:如果相同的 Shader,相同的参数,可以不用 SetState 就绘制下一个对象,只是再次调用一次 Draw API 即可,如果不同网格,只要设置好使用的 VBO 即可)
Unity 中得操作如下:
一般的再 URP 的话,可以直接将材质设置到 URP Renderer 中的 Base Shadow 的 Overrides->Materials 材质槽即可
注意在 Event 中选择:After Rendering Opaques(在所有的不透明对象渲染完后再绘制阴影的意思)
这个 Event 的功能在 Unity 的底层使用了 CommandBuffer 的来处理的
CommadnBuffer 相关得文章我之前也写过一些,想了解的话,可以看看:
- Unity CommandBuffer 测试自定义替换渲染
- Unity Shader PostProcessing - 6 - GaussianBlur 高斯模糊+CommandBuffer使用做一些其他的特效
- Unity CommandBuffer或是Camera重定向RenderTarget的ColorBuffer & DepthBuffer
- Unity CommandBuffer.SetViewProjectionMatrixes & CommandBuffer.DrawMesh 测试对Shader中默认的MVP设置
完整 Pass
Pass { // Planar Shadow
Name "PlanarShadow"
Blend SrcAlpha OneMinusSrcAlpha
Cull Front
ZWrite Off
Stencil {
Ref [_PlanarStencilValue]
Comp NotEqual
Pass Replace
Fail Keep
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma fragmentoption ARB_precision_hint_fastest // 最快速精度
// 平面上的任意一点
float4 _PlanePos;
// 平面法线
float4 _PlaneNor;
fixed4 _PlanarShadowColor;
struct appdata {
float4 vertex : POSITION;
};
struct v2f {
float4 pos : SV_POSITION;
fixed fade : TEXCOORD0;
};
fixed OffsetToPlanarShadowPosAndFade(inout float3 wpos) {
// 方向光的位置方位,(单位向量)
float3 L = _WorldSpaceLightPos0.xyz;
// 平面法线,(单位向量)
float3 N = _PlaneNor;
float d1 = dot(L, N);
float d2 = dot(wpos - _PlanePos.xyz, N);
float new_pos_offset_len = d2 / d1;
wpos += -L * new_pos_offset_len;
// 在平面法线发向上偏移一丢丢
wpos += N * _PlaneNor.w;
// 计算 fade (这部分也可以抽离到另一个函数来处理,但是 L 也可以使用上面的,所以暂时写在这里)
if (_PlanePos.w > 0) {
float3 wpos_h_0 = -N * d2;
float3 wpos_h_0_2_new_wpos = wpos - wpos_h_0;
float len = pow(dot(wpos_h_0_2_new_wpos, wpos_h_0_2_new_wpos), 2);
float cfg_len = _PlanePos.w * _PlanePos.w;
fixed fade = smoothstep(0, 1, 1 - saturate(len / cfg_len));
// fixed fade = 1 - saturate(len / cfg_len);
return fade;
} else {
return 1;
}
}
v2f vert (appdata v) {
v2f o;
float3 wpos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed fade = OffsetToPlanarShadowPosAndFade(wpos);
o.pos = UnityWorldToClipPos(wpos);
o.fade = fade;
return o;
}
fixed4 frag (v2f i) : SV_Target {
fixed4 col = _PlanarShadowColor;
col.a *= i.fade;
return col;
}
ENDCG
}
Planar Shadow 的优缺点
- 优点:阴影边界可以很清晰,很 sharp,有点 shadowVolume 那种精细度,还可以节省 RT(RenderTexture,相比 shadowMap 就需要 RT)
- 缺点:如果游戏中地形有高低,就不适合这种方式,王者荣耀之所以适合,是因为整个可行走地形都是水平的,就不会出现:腾空与低地表,或是陷于高地表内的情况
Backup Project
备份用,不公开模型,代码再上面都公开了
testing_planar_shadow
References
- 详解平面阴影 Planar Shadow (概念篇) - 这几篇后续有使用构建压扁矩阵的构建,说是更加优雅的方式,但我个人觉得没有上面画图上的几何意义的来得直观
- 详解平面阴影 Planar Shadow(方向光篇) - 还没去看过,后续再看
- 详解平面阴影 Planar Shadow(点光源篇) - 还没去看过,后续再看
- Planar Shadow中Shadow Matrix的推导
- 《3D数学基础:图形与游戏开发》- 8.4.2 向任意直线或平面投影 - page 100
- 即時 3D 繪圖的陰影效果 [Part 1]