Unity消融效果实战:基于噪声贴图与透明度裁剪的Shader实现
发布时间:2026/10/6 3:04:47 作者:尧图编辑部 阅读量:1,286

在游戏里角色死亡后飘散的灰烬、怪物被法术命中后燃烧殆尽、场景中冰块慢慢融化成水——这些效果大部分都是同一类技术Unity里的消融效果。简单说消融就是让一个物体从完整状态逐渐消失而实现的核心手段就是对贴图采样结果做动态着色处理再配合透明度裁剪Clip把像素一片一片切掉。这篇文章我会从Shader原理讲到C#侧控制再到边缘发光、噪声贴图进阶最后附带我实际项目中踩过的坑和排查思路。不管你是刚入门Shader的新手还是已经在写材质的老手这套流程都可以直接抄作业。1. 动手之前先弄清楚消融效果到底在做什么1.1 消融效果的常见实现路径消融效果在游戏引擎里有很多种做法最常用的有三类。第一类是基于透明度裁剪Alpha Test / Clip。Shader里采样一张噪声贴图把噪声值作为透明度当透明度低于某个阈值时用clip()函数直接丢弃该像素。这样物体表面就会像被啃掉一样出现不规则的镂空区域。阈值从0到1渐变时镂空区域逐渐扩大看起来就是物体在逐渐消失。这也是绝大多数消融效果的基础方案性能开销很小移动端也能跑。第二类是基于顶点偏移Vertex Offset 背面剔除。让物体的顶点朝法线方向逐渐向外或向内移动同时配合透明度的变化模拟物体像沙子一样坍塌、剥落。这个方案比Clip更立体但顶点数不足时容易穿帮对模型面数有一定要求。第三类是基于溶解Dissolve 粒子补偿。本质还是Clip方案但额外在镂空边缘生成粒子系统把那些消失的部分用粒子的形式表现出来视觉上更有冲击力。比如怪物死亡时身体边消失边冒火星就是消融Shader和粒子特效配合的结果。这个方案在动作游戏里最常见。这篇文章主要讲的就是第一类方案——基于噪声贴图、透明度裁剪、可动态调整阈值的Unity消融效果。原因很简单它适用范围最广、性能最可控、代码量最小而且在这个基础上加边缘发光、加溶解方向、加第二张细节贴图都特别方便。1.2 动态着色的核心是什么所谓动态着色我理解下来其实就是在Shader运行过程中通过外部传入的参数通常是一个float变量不断改变贴图采样结果让同一个材质在每一帧呈现出不同的颜色和透明度状态。它和静态贴图的本质区别是颜色不是一成不变的而是被时间、脚本、事件驱动着变化。拿消融来说核心动态参数就是消融进度Dissolve Amount。这个值一般从1渐变到0或者从0渐变到1取决于你定义的是融化掉多少还是剩余多少。我们采样噪声贴图后可以给采样结果做各种映射比如让黑白噪声变成有层次的色阶再和阈值比较决定像素去留。我还见过很多新手问为什么我的消融是整体变透明而不是不规则消失这就是因为Shader里直接用了alpha通道做clip但alpha在模型UV上是均匀的没有噪声扰动所以整片整片地消失。正确的思路应该是把一张噪声贴图或者高度图放到Shader里用它作为哪个地方先消失、哪个地方后消失的依据。这样物体的消失是带不规则边缘的观感上才像真实的燃烧、融化。2. Unity Shader从零实现基础消融2.1 准备工程与Shader结构在Unity里新建一个标准3D工程然后把默认的Standard Shader相关的材质先放一边我们直接创建一个Shader文件Create - Shader - Unlit Shader命名为DissolveEffect。双击打开后默认代码会是一个带主贴图的基本Unlit Shader我们把它改造成消融版本。先说一下Shader的整体框架。一个能在Unity里用的Shader至少包含Shader{}、Properties{}、SubShader{}、Pass{}这几层。Properties是暴露给Inspector面板的参数SubShader是为了兼容不同渲染平台比如真机和高版本平台可以各写一套Pass里才是真正的顶点片元着色逻辑。我习惯把消融相关参数都写在Properties里这样美术同事直接在材质面板上调参数不用碰代码Properties { _MainTex (RGB颜色贴图, 2D) white {} _NoiseTex (噪声贴图(控制消融形状), 2D) white {} _DissolveAmount (消融进度, Range(0, 1)) 0 _DissolveRange (消融边缘宽度, Range(0, 0.5)) 0.05 _EdgeColor (消融残影颜色, Color) (1, 0.4, 0, 1) }这里面_NoiseTex是整个效果的关键。它不需要是高清贴图一张128x128或者256x256的灰阶噪声图就够用。噪声上那些不规则的斑点、纹理会成为消融边缘混乱感的来源。如果美术那边没有合适的噪声图直接在Photoshop里生成云彩滤镜或者用Unity自带的PerlinNoise算法程序化生成一张都可以。2.2 采样、裁剪与阈值渐变进入Pass内的片元函数frag真正决定消融效果的核心逻辑其实只有五六行fixed4 frag (v2f i) : SV_Target { // 采样颜色贴图得到物体本来的颜色 fixed4 col tex2D(_MainTex, i.uv); // 采样噪声贴图得到每个像素的消失优先级 float noise tex2D(_NoiseTex, i.uv).r; // 把阈值和噪声做差小于0的部分直接丢弃 clip(noise - _DissolveAmount); return col; }我逐行讲一下。_DissolveAmount是外部传入的消融进度初始设为0时noise - 0基本都是正值所以所有像素都保留物体显示完整当_DissolveAmount逐渐增大到1时大多数噪声值小的像素被丢弃物体就慢慢镂空消失了。噪声贴图的作用就是把裂缝从哪里开始变成每个像素各有不同的规则而不是均匀地从边缘开始剥落。这里有几个细节值得注意。第一clip(noise - _DissolveAmount)这个写法是跳过光照的简单Unlit方案如果你想物体受到光照需要再算上一套漫反射和光照模型比如用Lambert或者放到Standard管线里。第二噪声值建议取RGB通道里的R通道就好没必要三个通道都采样毕竟我们只关心灰度变化。第三裁掉的像素是彻底的消失不会留下半透明残影如果你想要那种烧焦边缘还带着火星余烬的感觉需要在边缘区域单独叠加颜色——下面第三部分我会详细讲。Shader写好后在场景里放一个立方体或者导入一个角色模型把材质拖上去。在材质的Instpector面板里可以看到DissolveAmount的滑动条手动拖动它物体就会开始扭曲、消失。到这一步一个可用的基础消融效果已经完成了。3. 动态着色进阶边缘发光与燃烧质感基础消融的效果是物体被挖掉一块但说实在的观感比较干。游戏里让人眼前一亮的消融几乎都带边缘效果——比如烈焰烧灼的亮边、冰块融化的水渍光边。这一节的思路就是在噪声值和阈值非常接近的地方即即将消失的边界输出一个额外的亮色让边缘区域发光发亮。3.1 边缘发光的算法思路利用前面采样的noise和阈值_DissolveAmount我们可以算出距离被裁剪的临界值还有多远。通俗说我们判断某个像素是否处在边缘带里。代码可以这样扩展float edge smoothstep(_DissolveAmount, _DissolveAmount _DissolveRange, noise); float edgeFactor 1.0 - saturate(edge);_DissolveRange是边缘带的宽度。当noise值非常接近阈值时edgeFactor接近1说明这个像素正处于消融边缘当noise远超阈值时edgeFactor接近0说明这个像素还很安全。然后用这个edgeFactor去混合_EdgeColorcol.rgb lerp(col.rgb, _EdgeColor, edgeFactor);实际操作时我还喜欢在边缘色上叠加一个pow处理让亮边更锐利edgeFactor pow(edgeFactor, 0.5);如果是烧灼效果就让_EdgeColor配合一个从左到右渐变的贴图模拟温度从中心向边缘递减的感觉。如果你做的是冰融效果可以把_EdgeColor调成淡蓝色再配合半透明白色噪声效果会非常像冰层从内部裂开、表面渗水。3.2 第二种消融玩法噪声扰动与方向性消融除了最基础的全局一起消失实际项目中还经常要处理从下往上消失从中心向外扩散这类需求。比如机器人从脚下开始被分解成光点或者角色进入传送门后从头顶开始化成粒子。这就要给Shader加入方向控制。做法不复杂在片元函数里我们用模型顶点的本地坐标或者世界坐标去影响噪声采样的结果。举个从下往上消失的例子float heightMask saturate(i.localPos.y / _ModelHeight); float finalNoise lerp(noise, 0.0, heightMask); clip(finalNoise - _DissolveAmount);简单解释一下模型底部y0附近heightMask接近0finalNoise基本等于原始噪声容易保留模型顶部y值越大heightMask越接近1finalNoise被压到接近0所以最先消失。这样物体就是从顶部开始逐渐向下融化而不是整个随机消失。还有一种玩法是让消融区域沿着UV方向扫过这在3D UI或者技能特效里很常见。比如装备图标从中间撕开露出下一层属性本质就是让_DissolveAmount配合一个基于UV.x或UV.y的坐标做权重。我做过一个项目里装备升阶动画就是用这个方案先用噪声把旧装备从右上角向左下角逐渐溶解掉新装备再从反方向亮出来玩家看着非常有蜕变的感受。4. 实际项目中遇到的坑和排查技巧这一节聊聊我在真机调试和项目交付中实际踩过的坑。你写完Shader后在Scene视图里拖动滑动条看着没问题不代表打包到真机上也没问题。很多消融效果翻车都是翻在这些细节上。4.1 模型变黑洞背面裁剪导致镂空穿帮第一个高频问题模型消融到一半时从某些角度能看到模型内部是空的或者露馅出穿帮的背面。这其实不是Shader的逻辑错了而是渲染时背面剔除Cull导致的视觉盲区。默认Shader会Cull Back就是不渲染模型的背面面片。当正面像素被大量裁剪掉后透过镂空处看到的背面也没有像素内部结构就全空了。排查思路很简单把Pass里的Cull改为Cull Off同时把模型的渲染改成双面渲染。这样镂空处能看到内部的面片内部细节会保留观感会好很多。但也要注意双面渲染会增加一倍左右的顶点处理开销对低端机不友好。所以更好的做法是给模型加一层内壳——复制一个模型翻转法线专门渲染内部。内壳不参与消融Shader只作为填充结构这样既能保持消融效果又不会产生空心感。方案优点缺点适用场景Cull Off双面渲染实现简单直接配置顶点和像素处理翻倍特效模型、小型角色内壳模型性能可控效果稳定需要额外美术资源大型模型、长时间消融增加网格细分消融边缘更平滑顶点数激增对轮廓精度要求高时4.2 消融到一半整个模型发黑第二个问题消融进行到一半模型表面突然发黑或者整体颜色暗沉。这多半是动态光照和clip的交互问题。当你使用clip裁剪像素时Shader的Alpha通道通常没有被正确写入深度缓冲或者阴影接收出现异常导致环境光遮蔽AO没有正确计算模型像被罩了一层黑纱。解决方法有两个。一是检查Shader里是否正常声明了Tags和光照模式。如果是Unlit Shader建议开头加上Tags {QueueTransparent RenderTypeTransparentCutout}Queue设成Transparent可以让透明裁剪过的物体在渲染顺序上排到普通不透明物体之后减少被其他物体遮挡时看不到的问题。二是如果你用的是URP要注意_DissolveAmount变化时阴影贴图上也会出现一块一块的黑洞。那个黑洞其实是阴影接收区域和clip区域没同步导致的可以设置材质的ShadowMode为Off或者单独生成影子用的Shader变体。4.3 CPU负担和粒子特效的内存泄露消融效果本身是GPU计算性能其实不高。但很多项目里消融一旦做完就会在物体位置上催发一个粒子特效灰烬、火星、烟雾。我遇到过一个玩家在局内连续击杀敌人每次击杀都触发粒子特效结果内存越用越高最后卡到只有十几帧。用Profiler一查是粒子系统的Play/Stop没有正确清理。排查下来发现是代码里每次消融结束都只Play()特效没有监听粒子系统的Stopped事件去回收对象。后来我封装了一个特效池粒子播完自动回到池子里下次复用。这里分享一个实操经验消融Shadrer里如果有_EdgeColor相关的变量尽量不要在C#侧每帧都修改它。如果你每帧都去material.SetColor会对材质实例造成大量GC压力。正确做法是只在消融开始时设置一次后面的阈值渐变用MaterialPropertyBlock来驱动减少材质实例的复制开销。5. 更多玩法与我的工程化经验5.1 第二张噪声贴图与双Phase消融如果你想做一种先烧掉外层皮再融化内部结构的效果可以考虑采样两张噪声贴图。第一张噪声控制外层消失第二张噪声控制内部消失两张图的阈值错开0.1~0.2的节奏。实际操作时我会把第一张噪声明暗对比做得强烈一些第二张做得细碎一些先用外层粗犷的裂缝剥离外壳再用第二张细碎噪声把内部拆成漫天粉末。代码层面就是在Shader里多声明一张_NoiseTex2然后在片元函数里对两次clip结果做AND逻辑float noise1 tex2D(_NoiseTex, i.uv).r; float noise2 tex2D(_NoiseTex2, i.uv).r; clip(min(noise1 - _DissolveAmount, noise2 - (_DissolveAmount - 0.15)));注意第二张噪声的阈值慢了0.15所以内部会晚一点开始消融。这种双Phase消融在Boss死亡、建筑崩塌这类重场面里特别出效果。5.2 与URP渲染管线的适配现在Unity新项目基本默认用URP通用渲染管线。URP下写Shader和内置管线的差异主要在于Include文件和光照函数。如果你直接拷贝旧管线的Shader到URP工程里大概率会报错。URP下创建Shader的模板是Create - Shader - URP - Unlit Shader核心差异在于要#include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl。我用下来感觉URP的消融Shader更干净因为URP的Lighting模型更模块化很多内置变量直接有语义定义。不过有一个点坑URP的clip配合MSAA抗锯齿时边缘容易有白边。白边的本质是MSAA在边缘像素上有多重采样而clip的判定是逐像素的所以抗锯齿的混合结果里出现原始颜色和透明交替。解决办法是Shader里加一个粗锯齿的边缘平滑或者把RT的MSAA关掉用TAA做抗锯齿。视觉对比下来TAA配合消融边缘反而更自然。5.3 我的一些工程化建议最后分享一下项目里的组织方式。我把消融Shader通常封装成一套可以共用的特效Shader暴露出来的参数统一规范_DissolveAmount统一约定为0表示完整1表示完全消失_DissolveRange控制在0.01到0.2之间太大会显得整个物体半透明太小则边缘锯齿明显_NoiseScale控制噪声贴图的平铺系数根据模型大小调整否则小模型贴图拉伸严重C#侧我会写一个简单的DissolveController脚本挂在需要消融的物体上用协程控制进度public class DissolveController : MonoBehaviour { public Material mat; public float duration 1.0f; public void StartDissolve() { StartCoroutine(DissolveRoutine()); } IEnumerator DissolveRoutine() { float t 0f; while (t duration) { t Time.deltaTime; float amount Mathf.Lerp(0f, 1f, t / duration); mat.SetFloat(_DissolveAmount, amount); yield return null; } } }注意协程结束后一定要把dissolveAmount归位避免物体消失后下一帧又突然出现。如果是池化物体在生成前重置amount为0否则会出现新刷出来的怪是碎的这种低级事故。我个人在实际操作中的体会是消融效果在视觉上讲究节奏感——玩家期待的是从某个点开始快速扩展而不是全屏均匀消失。你在调整参数的时候宁可把_DissolveRange调小一点让边缘更锐利也不要为了华丽把边缘宽度拉满那样反而像物体在变透明不像在溶解。做特效就是这样技术实现是一回事最终画面体验又是另一回事多花点时间调参数比多写几行Shader代码更出效果。