ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Unity消融效果Shader实现:噪声裁剪与动态着色实战

Unity消融效果Shader实现:噪声裁剪与动态着色实战 做游戏的人应该都刷到过类似画面怪物死亡后身体从某个点开始被灼烧剥落边缘蹦出火红的碎屑几秒后整块模型化为灰烬或者是Boss从虚空中“生长”出来材质像被水溶解一样层层浮现。这个效果在Unity里一般叫Dissolve或者“消融效果”。如果你去搜英文教程关键词通常是Dissolve Shader或者Burn Out。它本质上是Shader层面的动态着色问题用噪声图对一个逐渐变化的阈值做比较然后不断“裁掉”像素同时给镂空边缘上色。这篇文章就把这个效果的实现路径完整讲一遍。我会先拆需求再说核心原理然后分别给一个Shader Graph快速版本和一个手写Shader版本最后把动态着色、边缘颜色变化和踩坑细节都补齐。无论你是刚接触Shader的新手还是要在项目里落地这个效果的开发者都能直接照着操作。1. 效果拆解与实现思路消融不仅仅是“消失”在动手之前先别急着找代码。消融这个效果看起来简单但真要做得好看里面的几层表现必须拆清楚。否则做出来的东西大概率是“贴图硬切”干巴巴的没有温度。1.1 消融效果里其实包含了三层表现第一层是镂空。模型不是整个儿突然消失也不是从一端到另一端整体渐隐而是像被腐蚀一样表面出现大量大小不一、位置随机的小孔洞然后孔洞逐渐扩大、连成片最终整块不见。给观众的感觉是模型正在被什么东西“吃”掉而不是单纯地淡出。第二层是边缘高光。镂空区域的边界通常不是暗的而是亮的甚至带一点灼烧感。这个不只是为了好看更是为了让观感更真实——生活中燃烧、融化、电焊的边缘都会发光发红人眼对这种“边缘亮光”非常敏感。很多新手只做第一层镂空不做这层边缘效果会大打折扣。第三层才是动态着色。同一个消融进度下如果边界颜色永远不变看几分钟就腻了。真实项目里往往需要边界色从亮白色逐渐过渡到橙红色或者从蓝色火焰转到黄色火焰甚至边缘色像火焰一样持续流动、脉动。这种通过参数在Shader运行期主动控制颜色变化的手法就是标题里说的“动态着色”。1.2 为什么一定要走Shader方案而不是做一张渐隐贴图有人说我直接用模型蒙版贴图控制透明度或者用脚本把模型的顶点往下沉也能做出“消失”的感觉。这话没错但这种方案有个很现实的问题一旦模型面数密集、材质复杂或者需要和多套贴图配合传统透明度方案就会遇到深度排序、半透明混合顺序错乱、性能下降等一堆麻烦。使用Shader里的“阈值裁剪”方案本质上是让GPU在每个像素上独立判断“这个像素要不要画出来”。不画出来的部分直接不生成颜色也不需要额外的半透明排序。对于角色死亡、怪物刷新、建筑生成这种瞬间大量顶点参与但又不希望产生透明叠加问题的场景这个方案是性价比最高的选择。再一个原因是动态着色在Shader里特别好做。如果你用脚本逐帧去改材质颜色代码逻辑简单但每帧要提交大量参数给GPU而把这些计算放到Shader内部用噪声值、消融进度、时间值组合运算就可以在GPU内部完成颜色渐变既灵活又省CPU开销。这也是我推荐大家把这类效果交给Shader来处理的核心原因。1.3 常见实现方案横向对比选对路线再动手根据表现效果和开发阶段市面上常见的消融实现大概可以分成下面几类。我把各自的特点整理成一张表方便你按项目需求做选型。方案名称实现思路优点缺点适用场景透明度渐隐通过脚本/动画不断降低材质Alpha实现最简单半透明混合排序问题多缺少撕裂质感UI、占位、临时表现顶点动画下沉让顶点沿着法线或模型空间移动可配合物理交互网格形变容易穿模且不是像素级剥落机械变形、流体模拟噪声图阈值裁剪Shader中用噪声采样结果与动态阈值比较并丢弃像素像素级随机剥落性能好不透明边缘锯齿需要额外优化阴影需要特殊处理消融/溶解/现身最推荐Shader Graph托拽基于可视化节点思路同上调试直观非程序员友好复杂逻辑时节点连线混乱版本间差异大快速原型、美术同学自行调整2. 核心原理解读噪声、Clip与Alpha Clipping到底在做什么如果你想用这个效果但又完全不理解背后的原理后面遇到问题会很难排查。我尽量用通俗的方式把“为什么”讲清楚。2.1 clip函数GPU版的门禁系统Shader渲染一个物体时并不是所有像素都会被画出来。像素最终能否出现在屏幕上由片元着色器决定。我们可以在片元着色器里调用一个函数clip(value);这个函数的意思是如果传入的value小于0就直接丢弃当前这个像素不进入后续输出阶段。你可以把它理解成一道门槛值大于等于0放行小于0剔除。消融效果正是利用了这个机制。我们把一张噪声图上的值和“当前消融进度”相减结果作为clip的输入。消融进度从0慢慢增大模型表面那些噪声值比较低的地方就会先小于0从而先被裁掉随着进度继续变大越来越多的像素被丢弃最终整个模型消失。这就是为什么这个效果在Unity里能保持“不透明”像素不是变成半透明而是干脆没了。不透明渲染不涉及混合排序排序压力会小很多。代价则是镂空边缘是硬切的后续要有心理准备去处理锯齿和阴影问题。2.2 如果不用噪声图会出现什么尴尬情况假设你偷懒不使用噪声图直接拿一个固定的UV坐标值或顶点位置来做裁剪会怎样结果就是模型按照某种规则“整整齐齐”地消失要么从半边一次性切开要么一层一层像切西瓜。这种效果偶尔有用但绝大多数项目要的是自然、随机、细碎的剥落感平整的切割线非常出戏。所以我们需要一张噪声图来提供“随机密码”。噪声图说白了就是一张灰度图每个像素点的亮度值看起来是乱序的但相邻像素之间有平滑过渡不会像雪花屏那样高频闪烁。Shader读取噪声图后每个像素拿到一个属于自己的“命运数值”配合动态阈值表现的顺序就像有了随机感。这里我特别提醒一个常见误区不要用纯随机的白噪声图。白噪声虽然每个像素都不同但相邻像素没有连续性镂空时会呈现密密麻麻的椒盐噪点非常脏。而Perlin噪声、Simplex噪声这类“分形噪声”图是连续变化的剥落时会形成一块块自然的水渍状、云朵状边缘。你如果找不到合适的噪声图可以搜索“Perlin noise map下载”或者用Photoshop里的“云彩”滤镜生成一张灰度图凑合一下。2.3 动态着色数据的来源和处理流程动态着色听上去高大上其实数据来源不外乎三个材质参数、噪声图采样结果、内置时间变量。材质参数例如“消融进度”_DissolveAmount由C#脚本或动画每帧更新噪声采样结果每个像素的随机权重决定这个像素在哪个阶段被裁掉内置时间例如Shader里的_Time.y用来驱动边缘颜色脉动或流动效果。Shader拿到这些数据之后要完成两件事第一根据“噪声阈值比较”来决定是否裁掉当前像素第二根据每个像素的噪声值和进度值的差值计算出“当前像素距离消融边界有多近”。这个“距离”非常关键因为边界发光、动态着色都会用到它。说白了动态着色的本质就是“找边界然后给边界附近的像素套一层可变化的颜色”。3. 快速上手Shader Graph版消融与动态着色如果你不是特别熟悉手写Shader用Shader Graph搭一个消融效果会非常直观。下面这个方法基于URPShader Graph版本建议使用Unity 2021.3或更高。这里同样包含动态着色的核心搭建过程照着连就能跑。3.1 Shader Graph里的裁剪方案选型打开Unity后先准备一个基础场景放一个Cube或一个人物模型作为载体。然后在Project窗口中右键选择Create - Shader Graph - URP - Unlit Shader Graph。名字随意例如DissolveEffect。在Shader Graph编辑界面里先点击左侧的Graph Inspector打开Graph Settings面板找到Surface Type把它从Opaque改成Alpha Clip。这一步是关键它会在最终生成的Shader里加入Alpha Clipping逻辑我们才可以用“阈值裁剪”方式实现硬镂空。注意不要把它改成Transparent那样会走半透明混合路线排序会很麻烦。改完之后你会在Master Stack的Fragment区域看到多了一个Alpha Clip Threshold输入端口。这个端口就是我们要打通的终点。3.2 核心节点连线噪声、阈值与边缘色接下来在编辑区创建几个节点Sample Texture 2D节点用于采样噪声图。你可以把Properties面板里新建的Texture2D类型属性拖到图中或者直接新建一个Texture2D属性命名为_NoiseTex赋值一张灰度噪声图。创建一个Float类型属性命名为_DissolveAmount范围0到1它是每帧控制消融进度的参数。再创建一个Vector1或Float节点用于控制边缘宽度_EdgeWidth。连线逻辑如下把Sample Texture 2D的R通道输出连接到Master Stack中Fragment的Alpha端口把_DissolveAmount属性连接到Alpha Clip Threshold端口。这样当_DissolveAmount不断增大时噪声图里较暗的像素会先满足“Alpha低于阈值”的条件被裁掉形成消融镂空效果。但是到这里只有镂空没有动态着色。继续创建节点用Noise R通道减去_DissolveAmount得到一个差值将差值接入Smoothstep节点Min设为0Max设为_EdgeWidth把Smoothstep输出接一个One Minus节点让它翻转一下。这个翻转后的值就是“边界强度”。当像素处于消融边界附近时这个值接近1离边界较远或模型完好区域接近0。拿这个边界强度乘以一个发光色再加到最终Base Color上就能看到镂空边缘发光。为了动态着色可以创建两个Color属性例如_InnerEdgeColor和_OuterEdgeColor再用_DissolveAmount或_Time.y驱动Lerp节点在两个颜色之间插值乘到边界强度上。这样你就得到了一个随着消融进度不断变化颜色的消融效果。3.3 关键参数调节建议把Shader Graph保存后创建对应材质应用到Cube或模型上然后手动拖拽_DissolveAmount滑条就能看到效果。调参数的时候我建议先从这几个地方入手_EdgeWidth不要一开始给太大0.01到0.05通常就够太大边缘会很糊边缘发光色优先选暖色系比如白色偏黄、亮橙、火红会比冷色更符合“灼烧”观感如果噪声图的重复感太强可以在Sample Texture 2D节点上扩大Tiling比如把UV的Tiling设为2到5倍如果你要做从无到有的“现身”效果把进度从1减到0播放即可效果同样成立。Shader Graph版适合快速验证想法。但你要把它用在实际项目里或者想彻底搞清楚内部逻辑还是得看一眼手写Shader的代码。4. 手写Shader实现从代码层面吃透消融和动态着色Shader Graph虽然方便但有些项目管线不支持或者你需要更灵活的定制逻辑这时候手写Shader就体现出优势了。下面给一份适合内置渲染管线的Surface Shader直接用能跑。4.1 一个可直接运行的内置管线消融Shader在Project窗口右键创建Shader - Standard Surface Shader然后替换成下面代码。这份代码里保留了基础主纹理、法线贴图增加了噪声裁剪、边缘发光和双色动态过渡。Shader Custom/DissolveEffect { Properties { _MainTex (Albedo (RGB), 2D) white {} _BumpMap (Normal Map, 2D) bump {} _NoiseTex (Noise Texture, 2D) white {} _DissolveAmount (Dissolve Amount, Range(0, 1)) 0 _EdgeWidth (Edge Width, Range(0.001, 0.2)) 0.05 _EdgeColor (Edge Color, Color) (1, 0.6, 0.2, 1) _OuterEdgeColor (Outer Edge Color, Color) (1, 0.1, 0, 1) [Toggle]_ColorFlow (Color Flow With Progress, Float) 1 } SubShader { Tags { RenderTypeOpaque QueueGeometry } LOD 200 CGPROGRAM #pragma surface surf Lambert addshadow #pragma target 3.0 sampler2D _MainTex; sampler2D _BumpMap; sampler2D _NoiseTex; float _DissolveAmount; float _EdgeWidth; fixed4 _EdgeColor; fixed4 _OuterEdgeColor; float _ColorFlow; struct Input { float2 uv_MainTex; float2 uv_NoiseTex; }; void surf (Input IN, inout SurfaceOutput o) { fixed4 c tex2D(_MainTex, IN.uv_MainTex); o.Albedo c.rgb; o.Normal UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex)); // 采样噪声图的R通道作为每个像素的“随机密度” float noise tex2D(_NoiseTex, IN.uv_NoiseTex).r; // 核心如果噪声值小于当前消融进度就丢弃这个像素 clip(noise - _DissolveAmount); // 计算当前像素距离边界的程度值约接近1说明越靠近镂空边缘 float edge 1.0 - smoothstep(0.0, _EdgeWidth, noise - _DissolveAmount); // 动态着色根据消融进度在两个边缘色之间插值也可以叠加时间产生流动感 float flow _DissolveAmount; if (_ColorFlow 0.0) { flow 0.5; } half3 finalEdgeColor lerp(_EdgeColor.rgb, _OuterEdgeColor.rgb, flow); // 把边缘发光叠加到自发光上不影响原有贴图的漫反射颜色 o.Emission finalEdgeColor * edge; } ENDCG } FallBack Diffuse }这份Shader用到的是Surface Shader它会自动处理很多光照细节。加上了addshadow指令让模型能正常投射阴影。不过这里有一个容易踩的坑我们放到第五部分详细说。4.2 关键代码逐段解释先看采样噪声图这一行float noise tex2D(_NoiseTex, IN.uv_NoiseTex).r;为什么要取R通道因为噪声图一般只有亮度信息RGB三个通道完全相同或者差异很小用R通道就够了省去不必要的带宽。看裁剪这一行clip(noise - _DissolveAmount);当noise等于0.3_DissolveAmount是0.6那noise - _DissolveAmount -0.3小于0当前像素被剔除。也就是说消融进度才进行到60%但该像素噪声值比较低暗部提前“死”了。随着进度从0到1所有噪声值小于等于当前进度的像素都会被逐渐丢掉。接下来是边缘计算float edge 1.0 - smoothstep(0.0, _EdgeWidth, noise - _DissolveAmount);smoothstep把noise - _DissolveAmount这个差值从0到_EdgeWidth之间做一个平滑过渡。如果noise - _DissolveAmount很小意味着噪声刚刚越过阈值即像素处在镂空边缘此时smoothstep接近0取反后edge接近1如果差值远大于EdgeWidth说明这个像素离边界还很远edge接近0。这样我们就可以拿edge作为遮罩让边缘发光而中心区域不受影响。最后是真正的动态着色部分float flow _DissolveAmount; half3 finalEdgeColor lerp(_EdgeColor.rgb, _OuterEdgeColor.rgb, flow);它是让颜色随消融进度连续变化。刚开始消融时边缘偏向_EdgeColor的颜色消融越接近结束越接近_OuterEdgeColor。比如可以把_EdgeColor设成白色偏黄把_OuterEdgeColor设成暗红色观看时就会感觉火焰先亮后暗、最终熄灭整个“灼烧”过程有一个从剧烈到冷却的情绪曲线。如果你希望颜色闪烁、脉动把_Time.y加入插值系数里即可。4.3 C#脚本如何驱动消融进度Shader本身不会自己动我们需要在C#里给_DissolveAmount不断赋新值。最直接的方式如下using UnityEngine; public class DissolveController : MonoBehaviour { [Range(0, 1)] public float dissolveAmount 0f; public float duration 1.5f; private Material mat; private void Awake() { var renderer GetComponentRenderer(); mat renderer.material; } private void Update() { mat.SetFloat(_DissolveAmount, dissolveAmount); } public void PlayDissolve() { StartCoroutine(DissolveCoroutine()); } private System.Collections.IEnumerator DissolveCoroutine() { float timer 0f; while (timer duration) { timer Time.deltaTime; dissolveAmount Mathf.Lerp(0f, 1f, timer / duration); yield return null; } dissolveAmount 1f; } }这里提醒一下GetComponentRenderer().material会创建一个材质实例在编辑器停止时不会自动还原。如果你只是测试问题不大但如果你在正式项目里频繁调用建议改用MaterialPropertyBlock避免产生不必要的材质拷贝和SetPass Call。这个方法后面也会再提到。4.4 动态着色的两个进阶变体除了上面代码里的双色渐变我在项目里还常用另外两种动态着色方式效果也不错。第一种是让边缘色沿模型表面流动。做法是给边缘颜色的乘数里加入UV和时间比如在Shader里把finalEdgeColor与一个基于IN.uv_NoiseTex - _Time.y * speed产生的噪声再乘一次。这样镂空边界上会出现彩色沿着边界蠕动的效果看起来像有能量在往里钻。第二种是用距离模型中心点的距离来控制颜色顺序。比如销毁一个圆形的球体时让边缘色从中心向外从蓝到红渐变形成“内部爆炸向外扩散”的感觉。实现上需要比较模型空间坐标和中心点位置把它作为动态着色的插值权重。这两种思路都不是必须的但能让你在同类效果中做出差异化有需要时可以直接往Shader里添加变量继续扩展。5. 常见问题与实战排查真正能落地的细节这个部分才是真正拉开档次的地方。下面这些坑很多是我自己踩过之后才明白的列出来给你当参考。5.1 阴影不跟着镂空走怎么办很多人在物体自身上看到了消融效果但物体投射到地面的阴影还是完整的一个形状非常出戏。原因是Surface Shader生成的ShadowCaster Pass默认不会执行surf函数里的clip逻辑所以阴影不会跟着镂空走。虽然在代码里加了addshadow但它只保证能投射阴影并不会自动执行材质里的裁剪逻辑。解决思路有两种。第一种如果你在用URP直接把消融效果放到Shader Graph里并打开Alpha ClippingURP的SRP Batcher会自动把Alpha Clip Threshold作用到ShadowCaster Pass阴影会跟着消融走。这是最省心的方案。第二种如果你用的是内置渲染管线和手写Surface Shader就需要在Shader中额外写ShadowCaster Pass把采样噪声和clip的代码复制过去。这里建议你不要偷懒只把属性复制过去还要确保使用相同的_NoiseTex和_DissolveAmount否则阴影和物体不同步。实际写起来比较啰嗦所以我个人推荐老项目里如果坚持内置管线可以权衡一下到底要不要表现投阴影的消融很多游戏里角色死后连着投阴影一并关闭也问题不大。5.2 消融边缘锯齿严重怎么让它更细腻clip导致像素被硬切边缘必然会出现锯齿尤其在离镜头近的时候特别明显。想要改善有两条路线。第一用边缘发光掩盖锯齿。给镂空边缘叠一圈比较亮的Emission观众视线会被亮光吸引锯齿问题就被视觉上缓解了。这个技巧几乎所有成熟项目都在用属于性价比最高的方案。第二把硬裁剪改成半透明混边。比如不直接clip而是在边界附近用Noise值控制透明度做渐变混合。这样边缘会软一些但物体要变成透明队列深度和排序问题又会回来需要权衡场景复杂度。如果项目有MSAA抗锯齿或者管线里带了后处理抗锯齿也可以让硬裁剪的边缘平滑不少。最忌讳的是既要求像素级硬镂空又希望边缘像羽毛一样完全无锯齿两者本质上是冲突的。5.3 一个对象播放销毁效果时模型本身的重心会跳动吗这里指的是物理相关的问题。如果你在消融过程中还让对象受重力下落当部分顶点被裁剪后刚体的碰撞体仍然是完整网格角色不会因为“被烧穿”而失去平衡。这个问题往往不在Shader范畴而在策划的表现需求里。如果你的游戏需要表现“怪物边死边倒下”的物理感建议拆两步播放消融效果的同时用脚本逐渐移除刚体约束或者等播放到一定百分比后让Rigidbody开始接受重力并缓慢倒地。如果直接在Update中每帧禁用一个新碰撞体反而会出现抖动。这类效果需要美工和程序配合但无论如何消融着色本身是纯视觉不要期望它会自己联动物理系统。5.4 多物体同时消融时性能有哪些隐患消融效果的Shader本身并不重但如果场景里有几十个角色同时在消融就需要注意几个点。一是材质实例数量。如果你在脚本里直接用renderer.material每个物体都会new一份材质批次会急剧上升。推荐用MaterialPropertyBlock下面是一个可以照抄的简化版本using UnityEngine; public class DissolvePropertyBlock : MonoBehaviour { private Renderer rd; private MaterialPropertyBlock block; private static readonly int DissolveAmountID Shader.PropertyToID(_DissolveAmount); private void Awake() { rd GetComponentRenderer(); block new MaterialPropertyBlock(); } public void SetDissolve(float value) { rd.GetPropertyBlock(block); block.SetFloat(DissolveAmountID, value); rd.SetPropertyBlock(block); } }这样多个物体即使共用同一个材质实例也能各自显示不同的消融进度不会打断合批。二是噪声图尺寸别塞太大。噪声图用256x256或者512x512足够设置成Repeat并且关闭Generate Mip Maps或者让它采样同一级Mip否则模型缩小到一定距离后噪点会变得模糊镂空位置也跟着飘忽不定。我一般把Noise贴图的Aniso Level设为1不用太高。三是裁掉的像素不要太快把摄像机视角堵住。如果一个巨大物体贴近摄像机消融虽然很多像素被剔除了但透明部分和可见部分仍然参与Overdraw检查极端情况下性能反而比普通不透明渲染更差。如果你的效果是特效演员通常播放完一次之后要把Renderer关闭或禁用对象不要让它空转。5.5 镂空区域总出现“方块感”或“木纹感”怎么排查这个问题的本质是噪声图重复频率不匹配模型UV导致消融边界整齐地按贴图的Tiling周期排列一眼就能看出贴图的格子。解决方法是把_NoiseTex的Tiling从默认的1改成2到3倍不要让噪声图的基础频率与模型主贴图的频率完全一致。还可以用两张不同频率的噪声图乘在一起采样比如一张大尺度控制整体剥落区块一张高频细节控制细小碎屑这样镂空效果会更接近真实燃烧。如果你使用的是Cube这类低模UV本身很简单容易出现“整面同时消失”的观感。这时候可以改用模型的世界坐标作为噪声采样UV比如在Surface Shader的Input里加入float3 worldPos;然后用worldPos.xz * _NoiseScale去采样噪声图。这样即使模型UV很粗糙镂空也无序感更强。最后的实际操作经验把我用过的心得做一个小结。第一消融效果的精华不只是镂空而是边缘动态着色。如果你只做了clip不做edge glow会给人一种“模型在漏气”的廉价感边缘色加上了画面档次立刻高不少。第二播放结束之后一定要记着处理对象状态该关闭Renderer关闭Renderer该回收进对象池就回收别让一个已经裁成空壳的模型继续留在场景里消耗Draw Call。第三别执着于让阴影和主模型完全同步除非你的美术表现非常依赖投影大多数情况下在消融中逐渐取消投影就能蒙混过关还能省一个Shader Pass的维护成本。另外再分享一个小技巧动态着色不一定只用在Shader内部。你可以在C#端用AnimationCurve控制消融进度同时用Gradient控制_EdgeColor随时间从亮白变黄变红。这样你在外部可以像调动画关键帧一样微调“火焰温度”而Shader内部几乎不需要改动。这对美术调参特别友好推荐一试。
返回列表