FEATURED · 精选文章

Unity Shader消融效果完全指南:原理、Shader Graph到性能优化

发布时间 / 2026/9/4 18:47:51
来源 / 创域科博编辑部
栏目 / 资讯中心
Unity Shader消融效果完全指南:原理、Shader Graph到性能优化 消融效果是游戏里最常见也最适合入门的“动态着色”案例之一怪物流血死亡、物体从空气中缓缓浮现、场景建筑被能量场蒸发背后都是一套几乎相同的Shader逻辑。我不止一次在项目评审里看到有人把所有玩法都用粒子拖尾硬堆结果一个“死亡散开”特效拆成七八个播放器性能开销大还不跟手实际上用Shader裁剪配合噪声纹理几十行代码就能做出实时、可控、能复用的溶解效果。这篇博文就基于我在实际项目中反复打磨过的方案把从原理到代码、从Shader Graph到URP迁移再到性能优化的完整链路拆开讲一遍保证你看完能直接抄作业。1. 消融效果的整体设计思路先想清楚屏幕上发生了什么1.1 消融的本质不是“逐渐消失”而是“逐像素裁剪”很多新手第一次做消融第一反应是改透明度或者让物体整体缩小这两条路都有明显问题透明度只能做到整片变淡没法形成从边缘开始碎裂、剥离的质感整体缩小又会丢掉物体本身的轮廓细节看起来像缩成一个点而不是被“溶解掉”。消融效果的正确理解方式是画面里每一个像素都有资格独立判断自己是留下还是消失。物体的表面不是一张完整的皮而是一块块可以被单独“切掉”的碎片。只要GPU在渲染每个像素时能做一次判断决定“这个位置该不该画”消融效果就成立了。这个判断动作在Shader里有一个原生指令叫clipHLSL里叫discard。clip会把小于0的像素直接丢弃后续的深度写入、颜色混合统统不再执行。GPU拿到一个片元发现它的某个计算值小于0就会像用剪刀裁纸一样把这个片元剪掉屏幕上那个位置就直接透出背景。所以消融效果落地的第一步是找到一个能驱动这个“剪刀”的信号源。最简单的方案是用一个全局标量做阈值所有像素拿自己的某个特征和阈值比较大于等于就保留小于就裁掉。问题马上来了如果所有像素都拿同一个特征去比整张模型会同时消失或同时保留跟场景里放一个物体然后突然拔掉电源没有区别没有任何“从某一块开始烧起来”的动态过程。1.2 为什么噪声纹理比普通UV渐变重要得多要让消融看起来是从物体表面的随机位置起燃、蔓延而不是像渐变滤镜一样横扫就必须给每个像素一个互不相同、空间上不规则、但有连续性的值。这就是噪声纹理存在的意义。噪声纹理本质上是一张“灰度随机地形图”越白的区域值越接近1越黑的区域值越接近0相邻像素之间的值过渡自然但整体上呈现出碎云、烟雾或者大理石纹路一样的不规则分布。我们把这张图的R通道作为每个像素的“个性值”配合外部阈值做比较阈值0.0时所有噪声值0的像素都在模型完整。阈值0.3时噪声值小于0.3的地方被裁掉模型上出现第一批“孔洞”而且这些孔洞集中在噪声纹理偏黑的片区。阈值0.6时孔洞连成片模型只剩下噪声值偏高的“高地区域”还在渲染。阈值1.0时几乎全部像素都被裁空模型完全消失。因为噪声纹理是连续变化的像素不是独立随机消失而是会形成“从几块裂口开始像火焰吞噬纸片一样向外扩”的视觉效果。这就在视觉上完成了从“整体消失”到“局部蔓延溶解”的质变。1.3 一个完整消融效果的四个必要组成部分把整个效果拆开看想稳定复现一个“动态着色”消融至少需要四样东西协同工作缺一个都会出现明显的视觉破绽主纹理物体本来长什么样决定消融过程中未溶解区域的画面。通常直接采样_MainTex再乘基础色即可。噪声纹理溶解的“地形图”控制哪些区域先消失、哪些区域后消失。灰度图的对比度、平铺Tiling都会直接影响碎裂形状。阈值参数可随时间从0变化到1的溶解进度。它像一把不断收紧的剪刀把噪声值低于当前阈值的像素依次裁掉。边缘发光/烧焦叠加层光裁剪会露出干净的材质边缘看起来像剪辑工具直接切开的缺少“被烧掉”的灼烧感。所以需要在阈值临界区叠加一个渐变颜色带也就是常说的边缘着色。我在项目里把这四部分封装成一个通用工具任何需要“出现/消失动画”的模型都走同一套Shader只是换不同噪声贴图和颜色。后续的动态虚拟演播、数字孪生场景切换、转场特效都可以基于这套思路扩展比每次单独做一套材质高效得多。2. Shader Graph快速搭一个能跑的消融材质2.1 URP下用Shader Graph实现节点比想象中少如果你的项目已经是URP渲染管线直接新建URP Shader Graph是最快路径。不需要写一行代码用可视化节点就能把第1章的核心链路搭出来。下面是我常用的基础节点清单和连接方式节点作用使用的节点关键连接采样主纹理Sample Texture 2DTexture连接_MainTexUV连接默认UV采样噪声纹理Sample Texture 2DTexture连接_DissolveTexUV连接默认UV可加Tiling提取噪声R通道Split取输出的R分量阈值裁剪SubtractClip噪声R - _Threshold结果接入Clip的In边缘范围计算SmoothstepEdge1_ThresholdEdge2_Threshold _EdgeWidthIn噪声R边缘色叠加One MinusLerp翻转边缘值按遮罩混合主纹理与_EdgeColor最终输出PBR Master/Unlit MasterBase Color接收Lerp结果连线逻辑和手写Shader完全对应先裁掉噪声值小于阈值的像素再对保留区域的边界做一圈颜色混合。这里有个很多人忽略的点Clip节点在Shader Graph里位于Math分类下它的语义是“只要输入向量的任意分量小于0就丢弃当前像素”。所以把噪声R - 阈值的结果直接送进去就能等价实现CG里的clip(noise - threshold)。2.2 暴露参数与边缘效果的调节技巧在Shader Graph的Blackboard面板里我一般会暴露这几个属性命名尽量和手写Shader保持一致方便之后美术换用代码版材质时无缝对接属性名类型默认值/范围作用_MainTexTexture2D默认白贴图模型主纹理_DissolveTexTexture2D默认灰噪点图溶解噪声_DissolveTileVector2(1,1)噪声平铺次数_ThresholdFloat0~1当前溶解进度_EdgeWidthFloat0~0.3边缘过渡带宽_EdgeColorColor橙红/亮蓝燃烧边缘的颜色_EdgeIntensityFloat1~5边缘发光强度实际调参有一个非常实用的法则_EdgeWidth不要直接给一个固定值要结合噪声纹理的明暗分布判断。如果噪声黑白过渡很陡像硬噪点那么_EdgeWidth0.05就已经能形成明显宽边如果噪声是柔和的大块烟雾状可能需要把_EdgeWidth调到0.1以上才能看到一圆圈光晕。用Smoothstep处理边缘的好处是它自带一个从0到1的软过渡不会出现一条一道尖锐的硬边视觉上更像高温灼烧的渐变带。2.3 主节点用Lit还是Unlit正确选择省掉一堆麻烦Shader Graph的Master节点选择会影响最终成品能否被光照影响、能否接收阴影、能否参与场景后处理。我做消融效果时有一个倾向如果是需要被场景光照照亮的战斗角色或场景物件选Lit主节点这样模型残留在消融过程中依然能保持正确的明暗不会出现一个“纸片感”的发光体挂在场景里。选Lit时会面临一个隐藏问题默认的Shader Graph材质表面是Opaque裁剪功能在Surface Options中把Alpha Clip Threshold打开并设一个极低的值比如0.01同时需要把Surface Type保持在Opaque。很多人在这里选成了Transparent结果边缘出现半透明混合的脏边其实是把“裁剪丢弃”和“透明度混合”的概念搞混了。消融应该用Opaque Alpha Clip不是Transparent Alpha Blend两回事。如果做的只是纯特效辅助几何体、或者物体本身不需要实时光响应那Unlit主节点反而更省少算光照性能更好边缘色还可以做得更亮更饱和。需求越简单越不要为了“看起来高级”而强行上Lit。3. 手写Shader代码从Built-in到URP都通用的完整实现3.1 为什么最终还是要会写代码Shader Graph虽然上手快但一旦遇到这些情况就抓瞎需要把消融逻辑嵌进一个复杂的多Pass Shader里做轮播高亮、需要兼容老项目的Built-in管线、需要给阴影投射Pass也加上同款裁剪保证影子同步消失、需要控制在粒子系统或骨骼网格上的材质块……Graph能做的事有限而且节点数量一多维护成本会直线上升。所以我建议团队里至少有一个核心TA或图形程序能写出下面这套可运行代码Shader Graph拿来给美术快速调参代码版本负责最终交付到不同管线。3.2 一份可直接运行的Surface Shader消融代码下面这份代码是典型的内置管线写法基于Surface Shader同时兼容了光照、阴影、雾效等常规渲染需求。它没有依赖URP的包拿回老项目里也能直接用URP项目可以把它作为理解底层逻辑的参考或者直接改造成HLSL的ShaderLab写法。Shader Custom/DissolveSurface { Properties { _MainTex (Albedo (RGB), 2D) white {} _Color (Color, Color) (1,1,1,1) _DissolveTex (Dissolve Noise, 2D) white {} _Threshold (Dissolve Threshold, Range(0,1)) 0 _EdgeWidth (Edge Width, Range(0.001,0.5)) 0.05 _EdgeColor (Edge Color, Color) (1,0.4,0,1) _EdgeIntensity (Edge Intensity, Range(0,5)) 2 } SubShader { Tags { RenderTypeOpaque QueueGeometry } LOD 200 Cull Off CGPROGRAM #pragma surface surf Standard fullforwardshadows addshadow #pragma target 3.0 sampler2D _MainTex; sampler2D _DissolveTex; fixed4 _Color; float _Threshold; float _EdgeWidth; fixed4 _EdgeColor; float _EdgeIntensity; struct Input { float2 uv_MainTex; float2 uv_DissolveTex; }; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 albedo tex2D(_MainTex, IN.uv_MainTex) * _Color; // 采样噪声R通道 float noise tex2D(_DissolveTex, IN.uv_DissolveTex).r; // 核心裁剪噪声小于阈值时丢弃 clip(noise - _Threshold); // 计算边缘遮罩 float edge smoothstep(_Threshold, _Threshold _EdgeWidth, noise); float edgeMask 1 - edge; // 叠加边缘色 albedo.rgb lerp(albedo.rgb, _EdgeColor.rgb, edgeMask * _EdgeIntensity); o.Albedo albedo.rgb; o.Alpha albedo.a; } ENDCG } FallBack Diffuse }代码的核心只有三个动作采样噪声、裁剪、边缘叠加。clip(noise - _Threshold)是第一道保险smoothstep负责生成一条宽度可控的渐变过渡带lerp把边缘色压到临界区上。edgeMask在越靠近裁剪线的地方越接近1边缘色越明显离开裁剪线没多远就迅速归零模型主体颜色不受影响。3.3 Surface Shader与顶点片元Shader的取舍很多人看到这可能会问现在不是流行Vert/Frag写渲染吗为什么还在用Surface Shader我的看法是消融效果本质上是一个“材质上色裁剪”的需求不涉及自定义的多Pass光处理、不涉及特殊顶点动画时用Surface Shader可以免费拿到Unity内置的全局光照、阴影、雾效处理。Surface Shader并不是“落后”只是Abstract层更高适合快速出效果。如果项目用的是URP或者你需要完全掌控渲染流程比如做草丛、体积云、水面这类有大量自定义光照的Shader那就得写成HLSL的Vertex/Fragment格式。但底层裁剪概念和方法完全一致把surf里那段clip(noise - _Threshold)搬到frag里即可。正因为概念是统一的我才建议先跑通这段Surface Shader你的目标不是炫技而是先把效果做出来看清楚。实际上在URP里如果不用Shader Graph我更推荐用一段精简的HLSL Unlit Shader避免URP自带光照常量与内置管线的兼容问题。但这里有个细节Unlit Shader不会处理主光源光照放到场景里如果周围都是暗色环境模型会显得偏暗无质感。所以URP场景下做消融要么选择Shader Graph的Lit模式要么在HLSL里套用URP的UniversalFragmentPBR前者省力后者上限高。新手建议直接走Shader Graph路线。4. C#侧的动态进度控制为什么你调的Threshold总是又卡又跳4.1 用MaterialPropertyBlock而不是改materialShader代码写好后C#侧只需要做一件事随时间改变_Threshold的值。这一步看似简单踩坑的人却非常多。最典型的错误是写成这样GetComponentRenderer().material.SetFloat(_Threshold, value);material属性每次访问都会在内存里创建一个新的材质实例如果你每帧都在Update里写这一行等于每帧都克隆一份材质帧率会肉眼可见地掉而且内存里会出现一堆无法自动回收的材质垃圾。多人同时消融时这个问题会放大到卡顿不可忽略。正确姿势是使用MaterialPropertyBlockpublic class DissolveController : MonoBehaviour { public MeshRenderer targetRenderer; public float duration 1.5f; private MaterialPropertyBlock _mpb; private float _progress; private bool _isDissolving; void Awake() { _mpb new MaterialPropertyBlock(); targetRenderer.GetPropertyBlock(_mpb); } public void BeginDissolve() { _progress 0f; _isDissolving true; } void Update() { if (!_isDissolving) return; _progress Time.deltaTime / duration; float threshold Mathf.Clamp01(_progress); _mpb.SetFloat(_Threshold, threshold); targetRenderer.SetPropertyBlock(_mpb); if (threshold 1f) { _isDissolving false; // 这里可以关闭模型或触发对象回收 } } }这样写的好处是参数只存在GPU侧的属性块里不会创建新材质多个物体共享同一个材质球时也不会互相污染。一个低频关键点注意Shader里暴露的属性名和C#里写的字符串必须完全一致多一个下划线、大小写不一致都会静默失败。4.2 溶解速度为什么“看起来忽快忽慢”如果你直接把进度_progress从0线性加到1然后把_progress赋给_Threshold可能会发现溶解并不是均匀地从边缘向里推进而是前半段变化很快后半段拖很久。这个问题的根源是噪声纹理的直方图分布不均匀大部分噪声值密集分布在0.4到0.7之间。相当于你给一个非线性“地形图”加一个线性上升的“水位线”淹没面积当然不是匀速增长。解决方式有两条路。如果美术可以控制噪声纹理就尽量让灰度分布均匀但大多数情况下噪声已经定了更实际的做法是调整C#侧的进度曲线。我通常会准备一个动画曲线字段让美术直接用AnimationCurve把进度映射成阈值或者先做一个非线性映射比如把阈值设计为1 - Mathf.Pow(1 - _progress, 1.5)让后半段溶解速度加快视觉上更“干脆”。具体指数要根据噪声图调没有通用默认值。4.3 效率、合批与多物体消融同步问题批量处理多个物体同时消融时每个物体都调SetPropertyBlock没问题但如果场景里有几十上百个使用相同材质和相同进度的物体合批就会因为每个物体有不同的属性块而被打断。对于真正大量出现的对象可以采取折中方案放弃逐物体的属性块把进度写在一个全局Shader变量里所有物体共享同一个_Threshold。但这样所有物体会同步消失少了很多随机错落的生动感。我的惯用做法是Shader里再加一个可选的_RandomSeed属性C#侧给每个物体初始化时设定一个随机的起始阈值偏移。物体A从0开始消物体B从0.2开始消共用一根全局进度曲线。这样合批依然可能被打断但如果物体数量不超过几十个开销可以忽略超过百量级时就得考虑把随机偏移写进顶点色或者用实例化属性数组来替代逐物体PropertyBlock。5. 实操中遇到的高频问题与排查技巧5.1 阈值已经到1了为什么模型还有残留碎片这是我被问过最多次的问题。现象是溶解进度已经到1模型按理论应该完全消失但屏幕上总残留几块顽固的碎面怎么调阈值都不干净。原因几乎都是噪声纹理的颜色空间或者压缩采样惹的祸。如果噪声贴图导入设置里勾选了sRGB (Color Texture)Unity会将采样结果做一次Gamma到Linear的转换噪声R通道本来该按纯数据处理却被当成颜色做了非线性映射阈值1对应不到采样峰值导致永远有少量高亮区域残留。解决办法是把噪声纹理的导入类型设为Default并取消勾选sRGB让Shader拿到的是原始线性数据。另一个隐蔽原因在纹理压缩格式上某些移动端压缩格式ASTC、ETC2对灰度图会有较大精度损失原本最大值接近1的区域可能被压到0.98在阈值为1时仍能通过clip(noise - 1)的判断。这要求把噪声的高光区做得尽量饱满或者C#侧在进度结束时强制隐藏/销毁物体用硬逻辑做收尾而不是单靠Shader裁干净。5.2 模型从背面看会“露馅”很多模型默认只渲染正面三角形Cull Back消融进行到中后期时你会从背面看到大量空洞里透出模型内部的后表面视觉上很乱。解决方式很简单给SubShader加Cull Off让正反面都被渲染。需要注意的是双面渲染会让填充率上升极端情况下会带来额外的性能开销所以只在确实需要双面可见的模型上开启。更深一层的问题是阴影如果物体投射阴影使用的ShadowCaster Pass没有做同款裁剪你可能会看到地面上有一块完好无损的影子而模型已经烧掉一半。解决方式是让ShadowCaster Pass也做一遍同样的噪声采样与clip逻辑或者使用fullforwardshadows addshadow让Surface Shader自动生成带主Pass逻辑的阴影Pass。在实际项目里阴影与消融不同步是特别容易穿帮的细节我见过很多团队把特效做完了才发现地面阴影纹丝不动又重新返工。5.3 常见问题速查表现象根本原因解决办法模型半边直接消失没有蔓延感噪声贴图的灰度对比度太低或者噪声平铺过大没有细节换云状/烟雾状灰度纹理调小Tiling或调高对比度消融边缘没有光晕像硬切_EdgeWidth值过小或Smoothstep的Edge2小于Edge1确保_EdgeWidth0.001调大到0.05~0.15观察Threshold到1仍有残片噪声贴图sRGB未关或纹理压缩精度损失取消纹理sRGB选项C#侧结束强制隐藏模型背面空洞穿帮默认单面渲染SubShader加Cull Off阴影不跟着溶解ShadowCaster Pass未裁剪Surface Shader加addshadowVertex/Fragment Shader补阴影Pass半透明脏边材质被设置成了TransparentAlpha Blend改成OpaqueAlpha Clip多物体溶解进程乱套每个物体各自计时进度不一致统一由管理器下发进度或加随机起始偏移用material每帧赋值导致卡顿反复创建材质实例改用MaterialPropertyBlock5.4 从消融到更多“动态着色”玩法如果理解了消融的原理你会发现它能很自然地扩展出一整类动态着色效果。阈值裁剪只是其中一种把“裁剪”换成“UV偏移”可以让模型表面出现能量流动把“阈值”替换为“距离场的距离值”可以做出刀光切割的截面效果把“边缘色”替换成“透明渐变”就演化成了传送门里的物体浮现效果。我在自己的技术栈里保持着一条习惯所有特殊材质Shader里都单独抽出“噪声输入 阈值参数 边缘渐层”三个公共属性组。不管从外界看效果多复杂核心都是这三个模块在互动只是把裁剪换成了透明度、把纯裁剪换成了UV扰动、把颜色混合换成了顶点偏移而已。这样既能统一团队内部各种效果的参数规范也方便把一个经过验证的Shader快速改成另一套效果省掉大量从零开始的调试时间。关于消融还有一个经常被忽视的小技巧制作“从空气里生成物体”的反向效果时不需要再写一个Reverse Shader把同一个Shader的阈值从1往0倒放即可配合边缘色从冷色调改成暖色调一个“浮现”用得上的材质就出来了。我个人处理这类需求时从来都是同一个材质球进度方向和时间曲线不同而已美术只需要在C#控制脚本里维护一个Reversing开关实际演示效果比重新做一套更好看。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻