Unity URP 溶解 Shader 实战:clip、阴影同步、燃烧边缘光(附在线预览)

0 阅读6分钟

溶解效果:边缘发光

溶解几乎是每个游戏都会用到的特效:角色死亡化成灰、道具被传送走、场景里的东西被“烧”掉。网上的写法很多,但大多数只讲了 clip 那一行,放进 URP 项目一跑就会发现:物体溶掉了,地上的影子还是完整的。

这篇用 Unity 2022.3 + URP 14 从头写一个能直接用的溶解 Shader,把几个常见的坑一次讲清楚:

  • 溶解的原理:为什么本质是 Alpha Test
  • 进度为 0 时有杂点、为 1 时有残留怎么办
  • 影子、深度图怎么跟着一起溶
  • 燃烧的发光边怎么做,为什么 HDR 颜色“怎么调都发白”

文末有在线预览(浏览器里直接拖进度条看效果)和完整源码。

一、溶解就是带阈值的 Alpha Test

给表面每个点一个 0~1 的“溶解值” v(最简单就是采样一张噪声图),再给一个全局进度 p。规则只有一条:

v < p 的像素丢掉。

进度从 0 涨到 1,越来越多的像素满足条件,物体就从噪声值最小的地方开始破洞,最后完全消失。HLSL 里丢像素用 clip(x):参数小于 0 时,这个片元不写颜色、不写深度。

所以溶解后的物体仍然是不透明的——每个像素要么完整存在,要么完全不存在。它不需要透明队列,也不需要排序,用 "Queue"="AlphaTest"(2450)就行:让完全不透明的物体先画、先写好深度,更多被挡住的溶解像素能被提前剔除。

噪声溶解,进度 0.15 / 0.4 / 0.6 / 0.8

进度从左到右 0.15、0.4、0.6、0.8。注意地上的影子和物体同步镂空,背面的内壁被压暗。

二、坑 1:进度 0 有杂点,进度 1 有残留

噪声贴图的取值很少真正到 0 或 1。直接 clip(v - p),进度为 0 时可能已经裁掉了几个像素,进度为 1 时还剩几个“顽固像素”。办法是把进度区间稍微外扩:

// 返回 0~1 的“溶解值”:值越小越早消失
half DissolveValue(float2 uv)
{
    return SAMPLE_TEXTURE2D(_NoiseMap, sampler_NoiseMap, uv * _NoiseMap_ST.xy + _NoiseMap_ST.zw).r;
}

// 把进度从 [0,1] 稍微外扩,保证 0 时完全可见、1 时完全消失
half DissolveClip(half value, out half edge)
{
    half p = _Progress * (1.0h + _EdgeWidth + 0.02h) - _EdgeWidth - 0.01h;
    half d = value - p;          // d < 0:已经溶掉
    clip(d);
    // 消失前沿附近的一条带子:d 从 0 到 EdgeWidth,edge 从 1 降到 0
    edge = 1.0h - saturate(d / max(_EdgeWidth, 1e-4h));
    return d;
}

外扩量里还算上了发光边的宽度 _EdgeWidth,保证进度为 0 时连发光边都不出现。函数顺便输出 edge(离溶解前沿有多近),下面做发光要用。

三、坑 2:影子没有跟着溶

这是最常见的问题。只在 Forward Pass 里 clip,屏幕上的物体会破洞,但阴影贴图是 ShadowCaster Pass 画的,它不知道你裁掉了什么,影子还是完整的。开了 SSAO 或者用到深度图的话,深度图里也会留下一个完整的“幽灵”。

解决办法:溶解计算写成公共函数,放进一个 .hlsl,所有写深度的 Pass(ForwardLit、ShadowCaster、DepthOnly)都调用它。

// ShadowCaster / DepthOnly 的片元只做一件事:同样算一遍然后 clip
half4 fragClip(Varyings i) : SV_Target
{
    half edge;
    DissolveClip(DissolveValue(i.uv), edge);
    return 0;
}

两个顺带的注意点:

  1. 材质参数(CBUFFER)也要放在这个公共文件里。 同一个 Shader 所有 Pass 的 UnityPerMaterial 布局必须完全一样,否则 SRP Batcher 会判这个 Shader 不兼容。也别用 UsePass "Universal Render Pipeline/Lit/ShadowCaster" 偷懒,借来的 Pass 带着 Lit 自己的 CBUFFER,一样会破坏合批。
  2. 自己写 ShadowCaster 时,只 include Shadows.hlsl 会报 undeclared identifier 'LerpWhiteTo'。 Lit 是通过 LitInput.hlsl 间接包含了它,自己写要先 include Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl。

四、坑 3:破洞后看起来像纸壳

物体被挖出洞以后,会透过洞看到它的内壁。默认 Cull Back 把内壁剔掉了,看起来像一张纸。把剔除改成 Cull Off,再在片元里判断正反面:背面翻转法线、压暗颜色,就像一个被掏空的实心物体。

half4 frag(Varyings i, FRONT_FACE_TYPE face : FRONT_FACE_SEMANTIC) : SV_Target
{
    half edge;
    DissolveClip(DissolveValue(i.uv), edge);

    half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv).rgb * _BaseColor.rgb;
    // 背面(内壁):法线翻过来,颜色压暗
    half3 normalWS = normalize(i.normalWS) * IS_FRONT_VFACE(face, 1.0h, -1.0h);
    albedo *= IS_FRONT_VFACE(face, 1.0h, 0.35h);
    ...
}

五、燃烧的发光边

记 d = v − p:d 刚好大于 0 的像素“即将被溶掉”,它们就是前沿。上面的 edge = 1 − saturate(d / w) 在前沿处是 1,往里 w 的距离降到 0。用它去查一张从暗红到亮黄的 Ramp 贴图,就是燃烧的颜色分布:

// 前沿附近先“烧焦”变暗,再在最前沿发光
albedo *= 1.0h - 0.85h * smoothstep(0.0h, 0.6h, edge);
half3 color = SimpleLighting(albedo, i.positionWS, normalWS, _Smoothness);

half3 glow = _EdgeColor.rgb * SAMPLE_TEXTURE2D(_EdgeRamp, sampler_EdgeRamp, float2(edge, 0.5)).rgb;
// edge 的平方:亮度集中在最前沿,往里快速衰减
color += glow * edge * edge;

两个细节让效果可信很多:

  • 乘 edge * edge 而不是 edge:平方让光集中成一条细亮线,而不是一片光晕。
  • 前沿后面先压暗:模拟纸张燃烧时火线前面先烤焦的那一圈,比单纯加一条亮边真实得多。

为什么发光颜色“怎么调都发白”

_EdgeColor 要标 [HDR],分量可以填到 4、5。URP 开 HDR 时颜色缓冲是浮点格式,能存下大于 1 的值,后处理 Bloom 把超出阈值的部分模糊、叠回画面,才有“溢光”。

没开后处理(或者相机没勾 Post Processing)时,大于 1 的颜色在输出时被截断成白色,你只能看到一条刺眼的白边。所以要在 Volume 里加 Bloom(阈值 1 左右)和 Tonemapping,相机勾上 Post Processing。

三种溶解形状:噪声、方向、球形

换一种溶解值就是换一种效果。左:噪声;中:沿方向从上往下(掺了一点噪声让切口不规则);右:从一个点向外扩散,适合“被击中的位置先开始瓦解”。

六、几个调参的经验

  • 噪声跟着 UV 走:UV 拉伸的地方斑块也会被拉长。想要均匀的斑块,用三平面采样或对象空间 3D 噪声。
  • 进度变化不匀速:fBm 噪声的值大多集中在中间,进度在 0.3~0.7 时变化最快。想要匀速,在 C# 里用 AnimationCurve 驱动进度。
  • 远处溶解边缘糊掉:低级 mip 把噪声平均成一片灰。噪声贴图关掉 mipmap,或用 SAMPLE_TEXTURE2D_LOD 限制级别。
  • 锯齿:clip 是硬边,MSAA 救不了它(MSAA 只对几何边缘多采样)。可以用 Alpha to Coverage 或后处理抗锯齿。
  • 运行时改参数:单个物体用材质实例或 MaterialPropertyBlock;注意 MaterialPropertyBlock 会让物体退出 SRP Batcher 合批,大量物体同时溶解时更推荐材质实例。

在线预览和完整源码

这篇是《URP 渲染实战》第 18 章的一部分。整本书 30 章、两百多个案例都在网上免费公开,每张图都能在浏览器里实时调参:gametech.byte888.com/urp/

有问题欢迎评论区交流。