
做项目的时候经常要衰落场景、做死亡消散、拆解保护罩或者让怪物被“烧成灰”这时候消融效果就是最顺手的那一类Shader方案。所谓消融Dissolve核心就一句话让物体表面按某种规则从“完整”到“消失”中间带一圈高亮的过渡边缘看起来就像被一点点烧掉或溶解掉。这个效果我在Unity里反复写过很多次从最早Built-in管线里的Surface Shader到后面URP里用Shader Graph搭再到纯手写HLSL踩过不少坑也攒了一些能直接用的套路。这篇就围绕【动态着色】这个关键词把Unity实现消融效果的完整思路和可复现代码整理出来适合正在做特效、战斗表现或场景交互需求的Unity开发者参考。1. 消融效果的底层逻辑与方案选型1.1 三种常见实现方案的对比先把方案摊开看。消融效果的“消失”表现业界主流做法有三条路线。第一种是透明度混合Alpha Blend也就是把材质的透明度从1渐变成0。这个方案实现起来最简单Adjust一下Alpha值就行。但问题也很致命消融边缘没法做出“燃烧高亮”的效果而且半透明物体在处理遮挡和深度关系时排序很容易乱物体和场景穿插时穿帮概率极高。我试用过一次给机械敌人做粉碎消散结果身体各部件互相透视立刻放弃。第二种是顶点动画加粒子替代也就是把模型顶点沿法线方向往外推同时播放一个粒子爆发效果视觉上像“爆开”。这个方案的优点是动态感很强适合爆炸、碎散类的表现。但它本质上不是“消融”而是“碎裂”而且需要对模型顶点数据做额外处理对美术资产的规格有要求。第三种就是今天要重点展开的保留颜色贴图的噪声裁剪Noise Clipping方案。核心思路是利用噪声图的灰度值作为“消融掩码”通过一个随时间增长的阈值参数把掩码低于阈值的像素直接裁掉clip/discard。因为噪声图是不规则的所以边缘不是一条直线而是参差不齐的“烧蚀”状态再配合smoothstep在阈值附近做渐变就能渲染出一圈烫红的边缘。这是商业项目中用得最多的方案也是动态着色在消融效果中最典型的体现。1.2 为什么Shader裁剪方案最适合动态着色裁剪方案能长期胜出是因为它在“表现力”和“可控性”之间取得了最好的平衡。先说表现力。噪声图本身就是灰度渐变加随机斑驳的形态采样之后天然具备自然的高位与低位区域。随着阈值从0增加到1被裁掉的区域不是均匀收缩而是沿着噪声里那些不规则的“岛”和“沟壑”逐步扩散形成类似烧焦、锈蚀、能量瓦解的随机感。再加上边缘发光、灼烧颜色渐变视觉层次非常丰富。再说可控性。整个消融过程的一切变化都集中在一个参数上——阈值_DissolveAmount。从脚本侧传入0到1的数值就能精确控制“消融到哪一步”。配合不同的噪声贴图和边缘色能刷出火焰燃烧、冰霜溶解、沙尘风化、次元剥离等完全不同的风格。这套思路做一次后面不管换哪种表现改参数就行。而且这个方案对性能很友好。片元着色器里就是一次tex2D采样加一次clip计算量几乎可以忽略不计。对移动端也很友好我从PICO一体机到主流手机都实测过没有任何压力。2. 核心原理拆解噪声决定细节阈值控制节奏2.1 噪声贴图的选择Perlin噪声还是Voronoi图这一步很关键因为噪声图直接决定消融边缘的“性格”。Perlin噪声也就是常说的柏林噪声的特点是平滑自然、连续渐变用来做火焰燃烧、物体腐蚀这类有机过程的消融效果非常合适。Perlin噪声的灰度值会形成类似云朵一样的斑块消融时边缘会呈现棉絮状的参差感烧焦的过渡也比较柔和。Voronoi图细胞噪声/Worley噪声的特点是一格一格的晶胞结构边缘分明消融时会出现“一块一块脱落”的视觉适合做冰雪融化、蜂巢结构、能量块碎裂这类偏向几何风格的效果。实际使用中如果Art那边没有现成的噪声贴图可以在网上找灰度噪声素材或者直接拿Photoshop里的“云彩滤镜”和“铜板雕刻”滤镜顺手生成。有一点要记住贴图导入Unity后要确认勾选了Alpha Is Transparency、Wrap Mode设置为Repeat同时把sRGB采样关掉尤其是在URP里如果当Mask用建议直接Unpack后在Shader内部处理避免颜色空间坑。如果不想依赖外部贴图也可以直接在Shader里用代码生成噪声比如采三层不同频率的Perlin噪声叠加但这种做法会多一些计算量移动端谨慎使用。2.2 clip()与smoothstep协同边缘渐变的实现细节Shader裁剪的核心API就是clip()函数。在HLSL里如果传入clip()的参数小于0这个片元会被直接丢弃完全不可见。这是实现消融的基石。但单纯的clip裁剪只能得到“有/无”的硬边界没法得到好看的过渡边缘。所以要在阈值附近做两步处理。第一步用smoothstep把阈值附近生成一个渐变量作为边缘光强。假设当前片元的噪声值是noise消融进度是_DissolveAmount我们可以这样计算float dissolveLine smoothstep(_DissolveAmount - _EdgeWidth, _DissolveAmount _EdgeWidth, noise);这个dissolveLine在噪声值远小于阈值时为0远大于阈值时为1中间会有一个平滑过渡带。用1减去它再乘以边缘色就得到了精确的边缘光。第二步注意边缘光的颜色一般要做两层渐变。最外圈是烧红/烧白的高亮色往里一点是暗红或暗紫色。这样视觉上会有“由外到内从炽热到暗淡”的温度感非常像真实燃烧。我这边常用的做法是float edgeMask 1.0 - dissolveLine; float glow pow(edgeMask, 2.0); // 外层更亮 float core pow(edgeMask, 4.0); // 内核范围更收 color.rgb _EdgeColor.rgb * glow * _EdgeIntensity; color.rgb _BurnColor.rgb * core * _BurnIntensity;这样的多层边缘叠加比单一smoothstep要立体得多燃烧边缘的高光和暗部层次都在。2.3 动态着色从脚本侧控制消融进度“动态着色”的“动态”两个字核心就在于这个参数是怎么随时间变化的。最直接的方式是在C#脚本里每帧更新Shader属性。但这里有个非常关键的性能细节不要直接调用material.SetFloat()因为这个方法会创建材质实例可能造成额外的Draw Call和GC。更稳妥的做法是用MaterialPropertyBlock或者直接把材质设为可复用的实例并只在需要时更新。MaterialPropertyBlock的好处是它允许同一个材质球、不同物体各自维护一份参数副本不会破坏合批。比如场景里有100个敌人都能挂同一个材质然后通过MaterialPropertyBlock各自控制消融进度性能表现非常好。对于粒子系统伴随、残骸掉落等复杂交互也是推荐用这个方案因为它把“状态”和“材质”解耦了。C#侧的控制逻辑简单来说就是一个协程从0增加到1同时控制消融阈值和切换碰撞体public IEnumerator PlayDissolve(float duration) { float t 0f; while (t 1f) { t Time.deltaTime / duration; _mpb.SetFloat(_DissolveAmount, Mathf.Clamp01(t)); // 每帧把参数应用给Renderer _renderer.SetPropertyBlock(_mpb); yield return null; } }这里的核心思路是真正渲染时Shader只负责读取参数并执行裁剪变化过程完全由业务侧管理。这样脚本层可以做很多扩展比如消融到50%时触发怪物死亡音效、90%时触发掉落物生成等。3. 完整实操从Shader到场景配置一键复现3.1 手写Shader的完整代码与逐行解读下面给出一份标准的、可在Built-in管线和URP下都能使用的完整Shader代码。我习惯写得结构化一些这样后来维护的人一看就懂。Shader Custom/DissolveEffect { Properties { _MainTex (Albedo (RGB), 2D) white {} _NoiseMap (Noise Map, 2D) white {} _DissolveAmount (Dissolve Amount, Range(0,1)) 0 _EdgeWidth (Edge Width, Range(0,0.5)) 0.05 _EdgeColor (Edge Color, Color) (1,0.4,0.2,1) _BurnColor (Burn Color, Color) (0.5,0,0,1) _EdgeIntensity (Edge Intensity, Range(0,5)) 2 } SubShader { Tags { RenderTypeOpaque RenderPipelineUniversalPipeline } Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; }; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); TEXTURE2D(_NoiseMap); SAMPLER(sampler_NoiseMap); CBUFFER_START(UnityPerMaterial) float _DissolveAmount; float _EdgeWidth; float4 _EdgeColor; float4 _BurnColor; float _EdgeIntensity; CBUFFER_END Varyings vert(Attributes IN) { Varyings OUT; OUT.positionHCS TransformObjectToHClip(IN.positionOS.xyz); OUT.uv IN.uv; return OUT; } half4 frag(Varyings IN) : SV_Target { float noise SAMPLE_TEXTURE2D(_NoiseMap, sampler_NoiseMap, IN.uv).r; clip(noise - _DissolveAmount); float dissolveLine smoothstep( _DissolveAmount - _EdgeWidth, _DissolveAmount _EdgeWidth, noise); float edgeMask 1.0 - dissolveLine; float glow pow(edgeMask, 2.0); half4 albedo SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv); half3 finalColor albedo.rgb; finalColor _EdgeColor.rgb * glow * _EdgeIntensity; finalColor _BurnColor.rgb * pow(edgeMask, 4.0) * 1.5; return half4(finalColor, 1.0); } ENDHLSL } } }代码不复杂但有几个点得解释清楚。首先为什么要用HLSL而不是Surface ShaderSurface Shader在URP下兼容性越来越差而HLSL可以同时兼容Built-in和URP只要把光照相关代码替换为对应的Library即可。这份Shader里没有做复杂光照只保留最朴素的纹理与边缘色。如果要做PBR光照可以在URP里加Lighting.hlsl的引用再把BRDF函数接入。其次注意Clip指令放在了最前面噪声值小于阈值的片元直接丢弃。如果后续还想让消融边缘的“半透明烟尘”更自然这里可以扩展为float alpha saturate((noise - _DissolveAmount) / (_EdgeWidth * 0.5)); clip(alpha - 0.5); // 在返回时用alpha混合但这样会切换到透明队列会带来排序问题。老实说不透明队列配合裁剪已经能覆盖90%的项目需求透明烟尘这种细节需求一般靠额外粒子系统补不要硬塞进材质里反而好控制。3.2 场景搭建与参数调试步骤第一步准备模型和贴图。Unity内置的Cube、Sphere都可以测试如果要做人形怪物消融请务必有一套装备贴图规范的模型UV不要重叠否则消融区域会拉伸。第二步创建材质把DissolveEffect Shader拖上去。这里有个我踩过坑的地方噪声贴图的Wrap Mode改为Repeat如果默认Clamp在边缘处会出现发丝状亮线。第三步调整核心参数。初始建议_DissolveAmount设为0_EdgeWidth 0.05_EdgeColor偏橙红色1,0.45,0.2,1_BurnColor偏暗红0.7,0.1,0,1_EdgeIntensity 2。然后在场景里滑动数值观察变化。第四步挂脚本。写一个简单的控制脚本设定按下空格键播放消融。完整控制脚本如下using System.Collections; using UnityEngine; [RequireComponent(typeof(Renderer))] public class DissolveController : MonoBehaviour { [SerializeField] private float duration 1.5f; [SerializeField] private AnimationCurve curve AnimationCurve.EaseInOut(0, 0, 1, 1); private Renderer _renderer; private MaterialPropertyBlock _mpb; private static readonly int DissolveAmount Shader.PropertyToID(_DissolveAmount); private void Awake() { _renderer GetComponentRenderer(); _mpb new MaterialPropertyBlock(); // 初始化时同步一下当前值防止编辑器调整后场景值失真 _renderer.GetPropertyBlock(_mpb); } [ContextMenu(Play Dissolve)] public void Play() { StartCoroutine(PlayDissolveRoutine()); } private IEnumerator PlayDissolveRoutine() { float t 0f; while (t 1f) { t Time.deltaTime / duration; float value curve.Evaluate(Mathf.Clamp01(t)); _mpb.SetFloat(DissolveAmount, value); _renderer.SetPropertyBlock(_mpb); yield return null; } } }注意脚本里的AnimationCurve。这个节点很有意思不是直接线性上升而是用EaseInOut曲线让消融开始时和收尾时都更柔和视觉上会比“全程匀速”更高级。你也可以调整出“前期缓慢燃烧后期瞬间崩解”的节奏。第五步处理阴影。如果物体在消融过程还需要接收和投射阴影要注意Unity的ShadowCaster Pass不会自动继承这个裁剪逻辑。也就是说可能出现“物体已经消融了但场景里还留下一个完整的黑色阴影”的鬼影现象。解决办法是给SubShader再加一个ShadowCaster Pass同步执行clip。Pass { Name ShadowCaster Tags { LightMode ShadowCaster } HLSLPROGRAM #pragma vertex vertShadow #pragma fragment fragShadow #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl TEXTURE2D(_NoiseMap); SAMPLER(sampler_NoiseMap); CBUFFER_START(UnityPerMaterial) float _DissolveAmount; CBUFFER_END struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings vertShadow(Attributes IN) { Varyings OUT; OUT.positionHCS TransformObjectToHClip(IN.positionOS.xyz); OUT.uv IN.uv; return OUT; } half4 fragShadow(Varyings IN) : SV_Target { float noise SAMPLE_TEXTURE2D(_NoiseMap, sampler_NoiseMap, IN.uv).r; clip(noise - _DissolveAmount); return 0; } ENDHLSL }这个Pass看起来体积不小但模板固定复制粘贴改个名就行。如果忘写了消融效果肯定会露怯。3.3 进阶多噪声叠加、方向性消融与顶点色局部控制基础版能跑通之后想做得更“贵”一点可以从三个方向升级。第一个方向是多噪声叠加。单张噪声图容易出现重复感尤其是大面积模型上Tile到一定程度后纹理周期非常明显。这时候可以采样第二张噪声用不同Tiling和偏移动态合成float noiseA SAMPLE_TEXTURE2D(_NoiseMap, sampler_NoiseMap, IN.uv * 1.0).r; float noiseB SAMPLE_TEXTURE2D(_NoiseMap, sampler_NoiseMap, IN.uv * 3.7 _Time.y * 0.05).r; float noise saturate(noiseA * 0.7 noiseB * 0.3);注意第二张要加Time做偏移这样效果就会“活”起来边缘一直在细微爬动很像真实的火舌舔舐。第二个方向是方向性消融。比如想做出“从脚底往头顶烧上去”的效果可以把模型高度作为一个附加因子。用世界坐标Y轴减去阈值的一部分float heightMask saturate((worldPos.y - _DissolveAmount * 3.0) * 2.0); noise * heightMask;这样物体从底盘开始消失往上升的方向蔓延适合做传送门、复活石这类效果。第三个方向是顶点色局部控制。美术可以在需要“先烧的地方”把顶点色涂黑Shader里用顶点色乘噪声值这样同样的阈值条件下不同区域的消融先后顺序就有了艺术化控制。这个做法在高级特效资产里非常常见强烈推荐。4. 常见问题与排查技巧实录4.1 消融边缘出现“锯齿闪烁”或“颗粒噪点”这个问题很多新手都会碰到表现为边缘不干净甚至出现像电视雪花一样的小点。排查思路有三个方向。第一确认噪声贴图的压缩格式和过滤模式。我建议把噪声贴图的Generate Mip Maps关掉压缩格式在移动端可以直接用RGBA Compressed等但不要用带Crunch的压缩因为Crunch压缩出来的高频细节会有严重失真。同时把Filter Mode设为Bilinear或TrilinearPoint过滤会让边缘特别硬。第二检查颜色空间。如果项目使用Linear线性颜色空间半透明或渐变采样可能会出现Gamma校正错误尤其是在边缘渐变色上特别明显。可以在Shader里对边缘色做Linear转换或者直接把采样噪声的sRGB采样关闭。URP里我习惯在属性标记为[NoScaleOffset]然后在frag里手动处理。第三如果是在移动端确保没有开启不必要的MSAA与后处理同时叠加有时候MSAA配合clip会在边缘产生噪点需要把RenderType改为TransparentCutout并开启AlphaToMask。4.2 物体消融了但阴影还在这是最高频的问题之一原因前面已经提过默认的ShadowCaster Pass不知道消融逻辑。解决方案就是在Shader里补一个并发执行clip的ShadowCaster Pass代码模板上面给了直接加进去。加完之后在Lighting窗口重新Bake或实时阴影都会同步更新。还有一个小坑如果模型是SkinnedMeshRenderer比如怪物阴影裁剪逻辑同样适用但要注意不要把噪声图采样UV和动态骨骼的UV弄混SkinnedMesh的UV在动画形变后不会变放心用。4.3 粒子系统和消融不同步很多项目希望消融到一半时物体“炸开”出火焰粒子到结尾时残骸掉落。这里最容易翻车的是粒子已经开始播了消融才刚过1/3。解决方案是在C#侧把消融过程抽象成“事件点”而不是“一个参数从头到尾”。具体做法是写一个事件回调public System.Actionfloat OnDissolveProgress; public System.Action OnDissolveHalf; // 阈值50%触发 public System.Action OnDissolveComplete; // 阈值100%触发然后在协程里每帧检查Progress是否跨越了特定阈值触发对应事件。这样粒子、音效、物理掉落都能与消融进度精确对齐。我做得最多的一套组合拳就是消融进度到60%时模型关闭碰撞体、开启布娃娃模拟到80%时触发爆炸粒子100%时销毁GameObject。整套流程下来观感非常顺滑。4.4 消融效果在URP和移动端适配的几个注意点如果项目已经切到URP使用上面代码时要注意全部使用HLSL不要混用CGPROGRAM。URP下如果要做Lit光照效果需要额外引入UniversalInput.hlsl和Lighting.hlsl。这里我只给了最朴素的Unlit版本性能最优也足够应对大多数消融需求。移动端适配更要注意带宽。即使噪声图只是单通道Android平台也会按RGBA加载一倍内存开销。如果很在意可以用ASTC压缩格式并降低噪声图分辨率1024x1024完全够用不要动不动上4096。还有一点如果后期加了Bloom后处理消融边缘的炽热色会有很明显的辉光效果视觉上提升很大。建议在URP Volume里开Bloom阈值调稍高一点只让边缘高亮部分发光这样烧灼感会特别有质感。5. 项目应用评估与扩展建议5.1 典型应用场景的具体搭配消融效果的适用面非常广。我整理了几种最常遇到的场景组合方便你直接对着选参数。场景噪声图风格边缘色灼烧色额外细节火焰燃烧Perlin噪声(1.0, 0.5, 0.1) 橙红(0.4, 0.02, 0) 深红开Bloom粒子火焰冰霜溶解Voronoi噪声(0.7, 0.9, 1.0) 淡蓝(0.1, 0.2, 0.4) 冰蓝边缘加半透明冰晶敌人死亡消散Perlin噪声顶点色(1.0, 0.8, 0.4) 金色(0.2, 0, 0.3) 紫色尾部拖光带次元传送门Voronoi高频流动(0.2, 0.9, 1.0) 青色(0, 0.1, 0.4) 深蓝UV旋转方向性消融这里面最出效果的是“冰霜溶解”搭配半透明冰晶粒子边缘如果再加上一点法向扰动看起来就像真的被冰冻后粉碎一样。做朝向物体的镜头冲击也很有氛围。5.2 性能评估一块钱成本换高级表现从性能消耗看消融Shader几乎是性价比之王。片元阶段只有两次纹理采样主纹理加噪声一次clip两次pow和若干加法。在中低端手机上一个屏幕同时出现5-6个消融中的物体非常流畅远低于半透明粒子叠加的负载。但要注意合批问题。如果用MaterialPropertyBlock传递参数合批基本不受影响如果用了material.SetFloat让材质变成实例每个实例都会有独立的Shader Property块强行打断合批。所以再次强调多人/多物体场景直接上MaterialPropertyBlock方案这个习惯越早养成越好。5.3 跟Unity新版本特性的结合现在Unity 6和URP 17已经普及Shader Graph里也内置了Dissolve相关节点模板。但我个人的体会是手写HLSL代码依然有不可替代的优势逻辑透明、便于版本控制、不依赖巨型Shader资源而且代码复用性极高。哪怕项目组里其他人习惯用Shader Graph我建议也要保留一份手写Shader作为性能基线和兜底方案。新版URP还支持了Render Graph和GPU Resident Drawer这对手写Shader更友好因为能更精确地控制Pass和CBUFFER布局。如果项目准备升级把手写Shader都迁移到HLSLCBSConstant Buffer结构后续维护会很省心。6. 写在最后几个值得留住的实操习惯做消融效果这几年下来我最大的感触是这个效果的入门门槛不高但天花板完全取决于细节的讲究程度。一个小技巧给消融边缘的_EdgeColor做HDR颜色URP里直接用Color属性上的HDR模式配合Bloom后处理不用加任何粒子光靠材质颜色就能把“炽热感”烧出来。这是成本最低的提升手段建议一开始就把边缘色设为(2,1,0.3,1)这种超亮值而不是(1,0.5,0.1,1)。另一个习惯SV_Target输出时边缘高亮区域的Alpha不要设成0否则在移动端某些GPU上会触发隐藏的Alpha测试反而降低性能。直接保持1.0裁剪逻辑交给clip就好。最后再分享一个排查思路如果遇到效果没问题但性能突然暴降先用Frame Debugger看看是不是因为Shader变体爆炸ShadowCaster和主Pass各开了一套变体加上多纹理、多平台宏很容易膨胀。给Shader加#pragma multi_compile_instancing等优化指令再配合Unity Shader Variant工具裁剪掉用不到的变体性能就能稳回来。希望这套拆解和代码能给你省点时间把更多精力花在创意的打磨上。