Unity Shader - Planar Shadow - 平面阴影

141 篇文章 33 订阅
49 篇文章 0 订阅


整体运行效果

在这里插入图片描述


思路

将模型压扁后,绘制即可

在这里插入图片描述

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(模板测试、模板缓存) 来解决

Shader

这样我们再运行看看效果:
在这里插入图片描述

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=wpp 中,就得 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 相关得文章我之前也写过一些,想了解的话,可以看看:


完整 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

  • 9
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值