ARTICLE DETAIL

资讯详情

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

Unity消融效果Shader教程:从原理到Shader Graph完整实现

Unity消融效果Shader教程:从原理到Shader Graph完整实现 1. 消融效果到底在做什么从需求到拆解游戏里要表现怪物死亡碎成灰烬、角色进行空间传送、场景入口崩塌最常见的一招就是消融效果。说白了就是在一个Shader的控制下物体表面不是整个消失而是从随机位置开始一片一片地剥落、溶解最终完全不见。做这个效果不一定要堆一堆粒子系统用一个动态着色的Shader就能办到这也是“Unity 实现消融效果”这类需求里很典型的方向。消融效果的核心简单总结就两件事第一给每个表面片元一个随机的“生命值”第二让这个生命值随动画进度统一下降生命值低于阈值的片元不再显示剩下还活着的片元继续显示。所以你需要一张能提供随机信息的纹理通常叫噪声图或者溶解蒙版它作用在每个片元上就是“寿命分布”的来源。这种实现方式比起用脚本去改网格顶点、或者把模型拆成面片再删掉要省事得多而且能实时作用在任何有UV的网格上。无论是角色、场景道具还是特效用的简单面片都可以直接套。做完这篇教程后你会完整得到一版能用的Dissolve材质也顺便理解动态着色这一整类效果的实现思路。1.1 为什么大家都在用Dissolve而不是拆模型——从效果机制说起我最初接触这个需求是做怪物死亡反馈。你去问美术他们会说怪物被打败后不要瞬间消失也不要只会播放一个普通死亡动画然后淡出而是要像被撕裂、被烧掉一样从表面开始慢慢碎裂碎成齑粉。这时候通常能想到的办法有好几个用粒子系统模拟碎片、用Scale动画控制物体缩小、或者直接在Shader里做溶解。用粒子系统确实能模拟出碎片飞散的效果但美术需要单独制作很多碎片模型还要单独处理碎片之间的碰撞、旋转、下落等运动逻辑很难做出那种“从表面逐渐剥离”的连续感。用Scale动画更省但是整个物体等比缩小消失效果非常生硬一眼就能看穿给不了那种“正在被摧毁”的感觉。而Shader里做消融本质是把渲染像素的裁剪阈值和一张噪声贴图关联起来可以用很小的代价获得“每一帧都在变化”的动态细节。这套机制的可行性依赖于渲染管线对片元裁剪的原生支持。一个片元着色器负责算出最终颜色如果结果不符合条件就可以用clip指令直接把这个像素丢弃。像素一旦被丢弃这个位置就不会写入颜色缓冲正好实现表面“缺一块”的效果。丢弃判断和随时间变化的标量结合以后整个物体就能呈现出非常有层次的消失过程噪声越乱消失边缘越自然完全不用预先拆模型。1.2 动态着色到底“动态”在哪里把消融效果归类为“动态着色”是因为真正发生变化的是每个像素的最终颜色状态。刚开始物体表面是完整的主贴图颜色消融开始后不同片元因为噪声值差异有的被裁掉有的保留有的处于即将被裁掉的边缘带。我给边缘带叠一层高亮颜色或烧焦色后就会看到非常明显的颜色渐变和状态变化整个着色过程完全是动态的。这套方案最大的好处是通用性不需要额外建模不需要烘焙顶点动画只需要一张灰度噪声图、两个Float参数就能实现基础效果。对美术来说它直观、可控对程序来说它不引入大量运行时开销。可以把物体消融到只剩一小块再调低阈值让它“复活”也可以把它做成随距离渐变的区域融化效果衍生能力特别强。2. 动手实现从0开始写一个最简消融Shader2.1 需要的素材和准备先准备一个Unity工程建议至少是Unity 2021或更新的LTS版本。版本不用太高核心逻辑在传统渲染管线里也完全可用。我目前测试的是Unity 2022.3 LTS用Unity 2020以后的版本操作基本一致。需要准备的素材主要是两种贴图。第一张是噪声贴图我使用的是Perlin Noise灰度图你可以用Photoshop生成也可以用Unity的Shader Graph创建一个Gradient Noise节点再保存到本地。更省事的办法是直接在Asset Store里下载一套免费的噪声图。注意不要让噪声图的值只集中在0或1两个极端最好是灰度分布覆盖0到1之间这样可以保留中间渐变地带让消融边缘出现更好的“撕裂感”。第二张是主贴图用来表现物体表面的原始颜色细节这张图没有特殊要求。然后创建一个Cube或者使用一个人形模型用于测试。通常模型高度在1.8米左右时一个Cube看起来效果也够直观。无论用什么模型都需要保证它有正确的UV因为我们要采样贴图UV决定了每一片噪声落在模型表面的位置。2.2 用代码还是Shader Graph现在新项目里Shader Graph使用率很高尤其URP管线项目做特效时可视化节点比手写Shader更容易上手。但手写Shader的优点是可读性强、性能可控、出问题时容易精确定位。消融效果的原理本身很简单我更建议先手写一遍把变量和流程吃透再去理解Shader Graph里的每一个节点到底在做什么。所以我这次教程的主线选择手写一个简单的Unlit Shader并在文章后面给出Shader Graph的实现思路两边需求都覆盖。为什么先做Unlit而不是Lit因为消融的核心逻辑是噪声采样、阈值裁剪、颜色混合这三个逻辑和光照完全没有关系。先用Unlit把效果跑通后续要带光照也只是在一个Lit模板基础上做同样的事思路不会变。2.3 最简版本完整Shader下面给出一版可以直接用的最简消融Shader我把它放在一个Unlit Pass里代码非常短适合放在Unity里逐步调试。请新建一个Shader文件把下面的内容复制进去再创建材质并挂到一个Cube上。你还需要把噪声贴图和主贴图拖到材质面板对应位置。Shader Custom/DissolveBasic { Properties { _MainTex (Main Texture, 2D) white {} _NoiseTex (Noise Texture, 2D) white {} _DissolveAmount (Dissolve Amount, Range(0, 1)) 0 _EdgeColor (Edge Color, Color) (1.0, 0.6, 0.2, 1.0) _EdgeWidth (Edge Width, Range(0, 0.2)) 0.05 } SubShader { Tags { RenderType Opaque Queue Geometry } LOD 200 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_ST; sampler2D _NoiseTex; float4 _NoiseTex_ST; float _DissolveAmount; fixed4 _EdgeColor; float _EdgeWidth; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag(v2f i) : SV_Target { fixed4 albedo tex2D(_MainTex, i.uv); float noise tex2D(_NoiseTex, i.uv * _NoiseTex_ST.xy).r; clip(noise - _DissolveAmount); float edge 1.0 - smoothstep(_DissolveAmount - _EdgeWidth, _DissolveAmount, noise); fixed3 finalColor lerp(albedo.rgb, _EdgeColor.rgb, edge * 0.9); return fixed4(finalColor, 1.0); } ENDCG } } }如果你的模型表面不需要其它光照效果这个版本已经可以直接跑。下面逐段解析一下关键部分在干什么以及为什么这么写。2.4 逐行拆解最核心的几行先看Properties里的五个参数。_MainTex是主贴图决定物体表面的颜色信息它并不参与消融判断。_NoiseTex是核心输入它的某个通道代表物体表面每个位置的随机状态我习惯用R通道。_DissolveAmount是进度取值范围0到10时物体完整显示1时几乎全部被裁掉。_EdgeColor是溶解边缘的颜色_EdgeWidth是边缘过渡宽度。再看片段着色器里的核心三行float noise tex2D(_NoiseTex, i.uv * _NoiseTex_ST.xy).r; clip(noise - _DissolveAmount); float edge 1.0 - smoothstep(_DissolveAmount - _EdgeWidth, _DissolveAmount, noise);第一步采样噪声贴图并取R通道。第二步把噪声值和溶解进度相减如果结果小于0就丢弃片元。换句话说当_DissolveAmount从0往1增加灰度值小于当前阈值的片元会先消失而灰度值偏亮的区域能坚持到更后期最终形成参差不齐的消融边界。第三步比较关键用smoothstep把位于边界附近的噪声区间映射成一个0到1的渐变值。当噪声值稳定大于阈值比如是0.9而阈值是0.6时smoothstep结果接近1当噪声值恰好卡在阈值附近它会给出中间灰度的边缘值。这样后续lerp才能知道哪些像素靠近溶解线需要叠高亮色。这里有一个很重要的经验不要在边缘判断里直接写step函数。你要知道step是硬切会把过渡变成生硬的锯齿导致边缘像拿刀切出来一样。用smoothstep则更接近“从完全不透明渐变到完全被裁掉”的自然感视觉上会柔和得多。真实项目里这个平滑宽度不需要很大0.02到0.08之间已经足够。3. 效果调优从基础消融到好看的烧焦边缘3.1 用脚本控制消融进度你当然不可能在材质面板上手动拖这个值来做游戏演示所以脚本控制是必须的。最简单的控制方式是为Renderer的材质设置Float参数。下面是一段非常简便的控制器脚本using UnityEngine; public class DissolveController : MonoBehaviour { public float duration 2f; private Material material; private float currentTime; void Start() { Renderer rd GetComponentRenderer(); material rd.material; // 注意这会实例化一个新材质不会污染磁盘资源 currentTime 0f; } void Update() { currentTime Time.deltaTime / duration; material.SetFloat(_DissolveAmount, Mathf.Clamp01(currentTime)); } }这里有一个很多新手都会踩的坑直接用sharedMaterial.SetFloat去改材质参数。sharedMaterial是所有同材质物体共享的你改一次场景里所有使用同一份原始材质的物体都会一起改动。如果只放一个测试Cube时不容易察觉一旦多个怪物共用材质就会一个怪物死亡、全场尸体一起消融。正确做法是取renderer.material也就是材质实例或者改用MaterialPropertyBlock。MaterialPropertyBlock更适合大批物体共用一套Shader、但各自进度不同的场景它不会额外创建材质实例这样在渲染性能和内存管理方面都有优势。对初学者而言用material实例已经能解决90%的问题而到了需要优化大量实例的项目时再引入PropertyBlock也不晚。3.2 增加烧焦色和发光带默认效果把主贴图颜色直接lerp到_EdgeColor确实能看出边缘在发光但看起来会有一点像边缘描了一层发光线缺少烧焦质感。我建议加两步细化第一让靠近边缘的正常区域逐渐过渡到烧焦色第二对边缘色增加受噪声影响的强度让发光带产生不规则抖动。具体做法是增加第二个颜色属性_BurnColor然后让主色先向烧焦色过渡再在最边缘位置叠加高亮发光色。可以参考下面这段代码fixed4 _BurnColor; float edge 1.0 - smoothstep(_DissolveAmount - _EdgeWidth, _DissolveAmount, noise); float burnGlow smoothstep(_DissolveAmount - _EdgeWidth * 2.0, _DissolveAmount, noise) * (1.0 - edge); fixed3 burnColor lerp(albedo.rgb, _BurnColor.rgb, burnGlow * 0.8); fixed3 finalColor lerp(burnColor, _EdgeColor.rgb, edge * 0.9);这个思路其实是按“热影响区”的方式来建模的。可以想象在切割金属时切口附近先会有一个变暗变色区域然后才是高温发白的亮边。着色逻辑上最靠近溶解线的留存片元才有edge值接近1的边缘高亮往外一点虽然还没被裁掉但离被裁掉不远了这段区域就是深色烧焦带。加了这一步以后消融画面的层次感会明显提升。3.3 控制噪声细节和纹理平铺频率不少同学会遇到这样的问题边缘呈一团脏噪点看不出“撕裂感”。这通常是因为噪声纹理频率过高或过低。如果你把噪声图平铺很小纹理被放大后大面积是黑色或白色那消融就是一块块掉感觉像抽签抽中哪块删掉哪块。如果噪声频率太高画面会变得细碎像被虫子啃过不够厚重。我习惯根据模型实际尺寸来调整NoiseTex的Tiling。一个普通角色高度在1.8米左右时噪声平铺参数用2,2比较合适。如果是大型建筑或山体Tiling可能要到5,5以上。这个是纯经验值每个项目都会有差异套到模型上以后需要反复预览对比。如果你希望在表面多一层细节碎裂感还可以额外加一张细节噪声贴图用来对主噪声的UV做偏移扰动。这么一改会让溶解边缘更有“向同一方向蔓延”的不规则感代价是片元着色器里多一次采样和一次加法对移动端也能接受。3.4 控制消融起始位置和方向有些场景里不希望物体整体凭空乱融而希望从某个点开始蔓延例如脚底被毒液侵蚀、中心被能量引爆。这时就不能再只用同一个全局阈值可以额外传入一个世界空间坐标然后计算物体现有片元距离该点的距离得到一个距离衰减值把它叠加到_DissolveAmount上。具体思路是距离越近等效溶解阈值越低于是物体先从这个点开始消失。用代码实现时需要把片元的世界坐标传进来这是完全可做的。时间有限的话可以先直接用全局阈值把主流程跑顺再迭代需求。消融效果的扩展方向还有很多比如沿法线方向做局部偏移、在消除的洞内生成烟雾粒子等那些是在这个基础上自然长出来的玩法。4. Shader Graph实现方式可视化节点的连接与取舍4.1 Shader Graph节点思路如果你不习惯写代码用Shader Graph也完全能实现一个不错的消融效果。首先在Project窗口里Create一个Shader Graph我建议选择URP Lit类型这样出来就支持光照便于测试角色模型。Shader Graph里通常已经内置了Gradient Noise节点或Perlin Noise节点我们可以完全不依赖外部噪声图。节点连接思路如下第一步把主贴图颜色接到Lit主节点的Base Color和Surface Description。第二步新建一个Gradient Noise节点把Scale设为适当的值再将其输出连到Step节点的一个输入。第三步新建一个Vector1变量作为Dissolve Amount把它也接入Step节点。第四步把Step的输出接到Lit主节点的Alpha Clip Threshold参数上。一定要额外勾选主节点的Alpha Clip选项并把Surface Type保持为Opaque否则透明裁剪不会生效。为了加出边缘发光效果可以再创建一个SmoothStep节点。将噪声值连到In将Dissolve Amount和Dissolve Amount EdgeWidth分别作为Edge1和Edge2。SmoothStep的输出用OneMinus反转后接一个Emission颜色然后连到底色的Emission。这样边缘一圈就会有柔光效果整体连接过程比写代码容易理解但这个环节最容易出错的地方是忘掉AlphaClip选项导致设置好了阈值但物体完全不会消失。4.2 Shader Graph与手写Shader怎么选个人经验是如果一个消融效果只需要两到三个贴图采样和几个数学节点用Shader Graph没有问题改动参数很快就能预览尤其适合美术人员自己调参。但如果效果涉及多层噪声、顶点偏移、复杂的边缘颜色计算或者在多个平台上遇到性能问题时我还是建议回到手写Shader因为你可以精确控制中间变量和指令数也方便打Log式排查。另外Shader Graph生成的代码并不是人类一看就懂的当它有几十个节点时中间会生成大量临时变量移动端Profiler里开Deep Profile才容易定位是哪部分消耗。手写Shader在这方面的控制力更强。反过来纯粹做原型验证时Shader Graph可以把半天的工作量压缩到十分钟两边各有各的价值。5. 常见问题与坑真实项目里容易遇到的事5.1 为什么设置了裁剪但看起来像半透明很大概率是你把Shader的Queue标签写成了Transparent或者同时又开启了Blend。消融效果本质上应该保持不透明渲染因为我们需要GPU把被裁掉的片元直接丢弃而不是用Alpha混合来模拟渐变。一旦使用混合会出现局部透明区域仍然产生深度排序问题还会让渲染物体的内部穿插显示异常。所以核心技巧是对于消融物体尽量保持Tags里的RenderType为OpaqueQueue为Geometry。如果需要叠加特效也可以让特效自身错层处理尽量不要把消融材质本身变成半透明材质。真实项目里角色消融通常需要和其它透明特效叠在一起但角色只要保持Opaque渲染被裁掉的部分就不会阻挡特效保留的部分正常显示观感上是正确的。5.2 阴影为什么还是完整的一整块这个问题很多人都会遇到只是在纯色场景里容易忽略。原因是ShadowCaster Pass默认没有执行我们的裁剪逻辑它只会按照模型的原始几何范围投射阴影贴图。所以物体表面哪怕已经被溶掉一半阴影贴图里仍然是一整块完整的模型轮廓。最简单的解决办法是增加一个自定义ShadowCaster Pass并在片元着色器里同样做一次噪声采样和clip。但这里涉及的Unity内置宏比较多代码稍微弯绕对新手不友好。我在真实项目里更常用的做法是如果只追求表现先关掉消融物体的自阴影投射使用一个简单的假阴影面片做底部接触阴影这样可以避免明显的逻辑错误。如果表现要求很高就需要把ShadowCaster Pass补齐确保阴影也随消融进度残缺。接收阴影的问题同样处理因为主Pass里已经做了clip物体表面被裁掉的片元不会再接收阴影所以自阴影不会出现“被裁掉区域仍然有阴影”的明显穿帮。剩下要解决的主要是自己投射到别人身上的阴影那才需要ShadowCaster。5.3 URP下代码报错CGPROGRAM失效怎么办现在Unity项目普遍转向URP如果你建的是URP工程仍然把旧的CGPROGRAM和UnityCG.cginc拿过来会看到很多内置变量报错。原因很简单URP的Shader库用HLSL替代了旧的CG兼容层很多内置光照宏都不再默认支持。解决方式有两种。第一种如果只是想快速出效果建议直接用URP的Lit Shader或Shader Graph。先建一个Lit材质再配合一个Step节点控制AlphaClipThreshold这是最省时间的路。第二种如果一定要保留手写方式可以把Pass改成HLSLPROGRAM并引用URP的ShaderLibrary即“Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl”。这种做法能精确控制管线兼容性但写起来要细心处理多个宏分支。实际项目里我更建议团队直接统一使用Shader Graph处理这类特效让美术也能调参程序则专注于解决复杂问题。这个选择并没有标准答案关键在于项目里的团队成员都熟悉哪条链路。5.4 移动端性能与合批问题消融效果本身并不消耗太高性能真正要注意的是片元着色器的带宽。多数情况下主贴图采样一次、噪声贴图采样一次总共两个采样动作对当前主流图形设备来说很快。但如果你额外叠加了多层细节噪声、大尺寸噪声纹理和复杂颜色运算移动端的填充率压力就会明显上涨。尤其当物体在屏幕上占比很大时比如全屏清屏技能每个像素都要执行多次采样和数学计算发热会变得很明显。我建议在移动端项目里只保留一层低频噪声采样用平铺参数控制在2左右边缘宽度适度加大。如果你需要很多碎片细节可以把噪声图的平铺值调大但不要无限堆多层噪声。至于合批MaterialPropertyBlock不会破坏GPU Instancing但传统动态合批会因为它而中断因为相同Shader会产生不同参数。实际游戏里场上同时存在几个消融物体并不需要担心合批只有当一帧里同时有几十个物体都在做消融而且是低端设备时才需要考虑用实例化Shader来规避状态切换开销。6. 最后再分享一点我个人的体会在项目里维护过一段时间消融效果后我的体会是最容易出问题的往往不是Shader本身而是使用场景里对规范和表现的预期不统一。美术希望边缘有很浓的烧灼感程序希望尽量节省指令数最后双方需要一起在真机上反复打磨噪声平铺、颜色和宽度的组合。我沉淀下来的一套做法是把Shader参数全部用MaterialProperty暴露出来把噪声贴图和Tiling也做成可配置项再写一个通用的DissolveController支持单次消融、定时复位、循环呼吸溶解三种模式。这样后续美术换一个人形怪物、一个建筑倒塌或一个溶解出场特效时不需要重新写逻辑只要替换贴图和微调参数就行。这也是消融效果最有意思的地方它看起来很简单却能在很多交互反馈里做出完全不一样的观感。如果你是第一次尝试做这个效果建议先照着最简Shader跑通然后调一个喜欢的边缘色再写一个脚本做进度控制。不要急着追求复杂的灼烧色、阴影残缺和URP兼容先观察最基础的原理是否能在屏幕上表现得清楚再一步步往上叠细节这样你收获的才是最稳定的实现方式。
返回列表