尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity消融效果核心原理与Shader实现:从噪声裁剪到动态着色

Unity消融效果核心原理与Shader实现:从噪声裁剪到动态着色 打开Unity工程想给角色或者场景物件做一个从无到有、从有到无的过渡动画很多人的第一反应是用动画控制透明度或者直接缩放。但真正落地到项目里你会发现这两种方案都太“干”了——透明度变化没有质感缩放又显得很假。这时候“Unity 消融效果”就派上用场了。它本质上是基于噪声纹理对片元做动态裁剪配合“动态着色”的边缘发光和燃烧色过渡让物体像被能量腐蚀、被火焰吞噬、或者被冰块封冻后又碎裂消失一样自然。这篇博文我打算把消融效果的原理、Shader实现、参数调优和踩坑记录完整写一遍适合已经能看懂基础Shader语法、想在游戏里加入这种特殊效果的开发者参考哪怕是刚接触Shader的新手跟着步骤走也能跑出一个能用的版本。1. 消融效果的底层逻辑与整体设计思路1.1 消融效果到底在做什么先说大白话版本的原理。消融效果本质上是在片元着色器阶段“扔掉”一部分像素。GPU渲染一个物体时每个像素都会经过一遍片元着色器我们在这里判断如果某个位置对应的噪声值低于当前消融进度就直接把该片元丢弃clip或discard。于是物体表面就会出现一块块空洞随着进度从0涨到1空洞越来越多、越来越大直到整个物体从视野中消失。这里有个关键点裁剪依据不是主纹理的透明度而是一张独立的噪声纹理。为什么必须用噪声而不是直接用主纹理的alpha因为主纹理是物体本身的贴图它的alpha通道通常做不了那种“大面积随机破碎”的效果。比如你想让一个铁桶从左下角往右上角慢慢腐蚀掉边缘还要有不规则的破碎锯齿这种形态用普通贴图的alpha根本控制不出来。而噪声纹理里每一像素的灰度值都不同天然带有随机起伏拿它跟消融进度做比较画面上就会形成不规则的、带毛刺感的消融边界。至于动态着色它是画龙点睛的部分。单纯把像素扔掉会出现一个问题——消融边缘是一条生硬的细线颜色和非消融区域完全一致看起来像被剪刀裁掉一样。动态着色就是在消融边界附近采样一个渐变区间把边界处的颜色替换成火焰橙、冰蓝或者能量紫等特定颜色再加一层发光强度让消融中的物体看起来像是边缘被“烧红”或者“充能”了。这就是为什么很多游戏里的消失/出场特效看起来很有“能量感”核心就在这个边缘着色。1.2 动态着色在整个方案里的定位很多初学者会把消融效果理解成一个“裁切开关”认为把clip判断写进Shader就算完成了。实际在项目里真正拉开效果差距的反而是动态着色那部分。我给消融效果的视觉目标拆成三层第一层是消融区域的空间分布决定哪里先消失、哪里后消失这是噪声纹理和消融进度控制的第二层是边缘过渡质量决定边界是粗糙还是柔和、有没有光晕这是边缘宽度参数和smoothstep函数控制的第三层是燃烧/能量感的表现力决定整个特效是否有“温度”和“冲击力”这是边缘颜色、燃烧色叠加和发光强度控制的。这三层里第二、三层都属于动态着色的范畴。你可以这样理解噪声裁剪是骨架动态着色是皮肉。没有动态着色的消融是一具干巴巴的骨架有了动态着色的消融才配叫“效果”。我见过不少项目在初期实现消融时直接跳过边缘着色等美术提了“看起来太假了”的需求之后才回来补结果又要从头调整参数反而多花一倍时间。倒不如一开始就把动态着色纳入主流程。1.3 为什么优先选噪声纹理做裁剪依据不是随便拿一张灰蒙蒙的图就能当噪声用的。消融效果对噪声纹理有两个硬性要求一是灰度值分布要足够随机不能有明显的规律性条纹二是纹理的平铺效果要自然采样时不能让玩家一眼看出重复边界。满足这两个要求可以先试试Unity自带的资源商店里那些免费噪声贴图比如Perlin噪声和Voronoi噪声。Perlin噪声的优点是过渡平滑产生的消融边缘比较“柔和”适合冰封、石化这类慢慢裂开的效果Voronoi噪声的优点是细胞状结构明显破碎感更强适合爆炸碎片、能量瓦解这类激烈效果。如果你的美术风格比较特殊也可以直接从Shader里生成噪声比如用一小段代码在片元着色器里叠几次正弦函数模拟出伪随机效果这能省一张贴图但性能和视觉质量普遍不如直接采样贴图来得好我后面会细说。2. 手写Shader与Shader Graph的方案选型2.1 手写Shader的优势和适用场景实现消融效果的方案有两条主流路线一条是在Shader Graph里用节点连线拼出来另一条是直接上手写顶点片元Shader。我个人写项目特效时大部分情况会选择手写原因很实际手写Shader的代码可读性和可维护性对后期项目交接太重要了。一个完整的消融Shader不算注释也就七八十行其中真正核心的才二十多行。代码文件放到项目里任何人打开都能清楚看到消融进度是怎么参与运算的、边缘颜色是怎么混合的、哪个参数控制边缘宽度。而Shader Graph在节点少的时候确实直观一旦加上边缘渐变、噪声扭曲、多个颜色混合这些需求节点图会迅速膨胀到几十个节点密密麻麻的连线维护起来非常痛苦有时候想改一个数值要在节点堆里翻半天找对应节点。另外手写Shader对粒子的支持也更友好。比如我想让消融边缘同时“喷”出一些火星粒子可以在C#脚本里实时读取当前消融进度和物体世界坐标再结合粒子系统的生命周期做联动。手写Shader这边通过一个简单的SetFloat就能驱动而Shader Graph在外部参数对接上虽然也能做但性能开销和部署成本普遍要更高一些。2.2 Shader Graph的快速方案与局限也不是说Shader Graph就不能用。如果项目本身已经全面跑URP管线美术又希望自己调参那用Shader Graph搭一个消融效果是完全可以的。核心节点就几个Sample Texture 2D取噪声值用Step节点做裁剪判断再用Lerp节点混合边缘颜色。如果只是做原型验证半小时搞定。但Shader Graph有几个绕不过去的局限。首先是动态着色部分的灵活性不够想做边缘颜色随消融进度渐变的复杂曲线在节点图里要拆成好几个Lerp和Smoothstep叠来叠去玩起来很绕。其次是移动端兼容性Shader Graph生成的代码有时会带一些宏分支和额外的插值器开销某些老机型上可能莫名其妙的掉帧。所以我更推荐把Shader Graph用在技术验证和美术快速出图上正式项目上稳定性和可控性优先还是手写为上。2.3 选型的核心判断标准我总结了一套选型判断标准分享出来可以帮你做决定如果你的团队里美术需要频繁调参、项目又只跑URP管线可以选择Shader Graph如果你需要精确控制边缘的每一个像素、要跟粒子系统做精细化联动、项目还要兼容不同渲染管线或者老设备手写Shader是唯一合理的选择。还有一点值得提手写Shader并不等于降低效率。一个消融Shader在游戏里的性能损耗其实挺低的主要是片元着色器里多了一次噪声纹理采样和几次运算。真正影响性能的反而是裁剪之后物体的包围盒还在批处理没法很好地合并所以如果你需要同时消融几百个物体要注意规划一下合批策略这属于性能优化范畴我放到后面第五节讲。3. 实例驱动手写一个完整的消融Shader3.1 Shader基础框架和属性定义现在就进入核心实操环节。下面这段Shader代码可以在Unity的Built-in渲染管线里直接跑如果你用的是URP把CGPROGRAM换成HLSLPROGRAM、内置函数换成URP对应的函数即可核心逻辑是一样的。先定义属性块。这里有几个参数非常关键_DissolveAmount是消融进度0表示完整显示1表示完全消失_EdgeWidth控制消融边缘的宽度范围值越小边缘越细越锐利_EdgeColor是消融边缘的高亮色比如火焰的明黄色_BurnColor是燃烧叠加色比如火焰的暗红色_NoiseTex是噪声纹理_MainTex是物体本身的主纹理。Shader Custom/DissolveEffect { Properties { _MainTex (Albedo Texture, 2D) white {} _NoiseTex (Noise Texture, 2D) white {} _DissolveAmount (Dissolve Amount, Range(0, 1)) 0 _EdgeWidth (Edge Width, Range(0, 0.5)) 0.1 _EdgeColor (Edge Color, Color) (1, 0.8, 0.2, 1) _BurnColor (Burn Color, Color) (1, 0.3, 0, 1) _BurnIntensity (Burn Intensity, Range(0, 10)) 2 } SubShader { Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue } Pass { Blend SrcAlpha OneMinusSrcAlpha ZWrite Off CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_ST; sampler2D _NoiseTex; float _DissolveAmount; float _EdgeWidth; fixed4 _EdgeColor; fixed4 _BurnColor; float _BurnIntensity; 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 { float noise tex2D(_NoiseTex, i.uv).r; clip(noise - _DissolveAmount); fixed4 albedo tex2D(_MainTex, i.uv); float edge smoothstep(_DissolveAmount, _DissolveAmount _EdgeWidth, noise); float edgeFactor 1.0 - edge; float heat _BurnIntensity * edgeFactor; fixed3 finalColor albedo.rgb _BurnColor.rgb * heat; finalColor lerp(finalColor, _EdgeColor, edgeFactor * 0.8); fixed4 output fixed4(finalColor, albedo.a); output.a * edgeFactor; return output; } ENDCG } } }我先解释一下这里几个关键写法的用意。clip(noise - _DissolveAmount)这一行是消融的核心判断当噪声值小于当前进度时该片元直接被GPU丢弃不再执行后续任何计算。我把边缘着色的计算放在了clip之后这样只有尚未被丢弃的像素才会继续执行能在片元较多时节省一部分GPU指令。3.2 边缘渐变和燃烧色混合的数学原理接下来的smoothstep(_DissolveAmount, _DissolveAmount _EdgeWidth, noise)这行代码是整个动态着色的核心。smoothstep会在前后两个阈值之间做平滑插值返回值从0渐变到1。当噪声值刚好等于消融进度时返回值是0当噪声值达到进度加边缘宽度时返回值是1。我用1.0 - edge拿到一个反向的因子edgeFactor这个因子在消融边缘处为1越往外越接近0正好用来做“距离边缘多近”的度量。然后我把这个edgeFactor用来叠加颜色。heat _BurnIntensity * edgeFactor就是让边缘附近的像素受到更多的燃烧色影响离边缘越远越不明显。实际渲染出来就是一条亮橙色的“燃烧带”紧贴在消融边缘上看起来像金属被烧到发红快要熔化的状态。这里的_BurnIntensity控制过热感强烈程度数值越大燃烧带越宽、越亮。3.3 C#脚本驱动消融进度Shader本身只是计算工具它还差一个“遥控器”来告诉它当前消融进度是多少。我用一个简单的C#脚本挂在物体上通过协程把Progress从0平滑增加到1并实时塞给材质。using System.Collections; using UnityEngine; public class DissolveController : MonoBehaviour { public Material dissolveMaterial; [Range(0, 1)] public float dissolveAmount 0f; public float duration 2f; public AnimationCurve speedCurve AnimationCurve.Linear(0, 0, 1, 1); private float currentProgress 0f; public void StartDissolve() { StartCoroutine(DissolveRoutine()); } public void ResetDissolve() { currentProgress 0f; dissolveMaterial.SetFloat(_DissolveAmount, 0f); } private IEnumerator DissolveRoutine() { while (currentProgress 1f) { currentProgress Time.deltaTime / duration; float evaluated speedCurve.Evaluate(Mathf.Clamp01(currentProgress)); dissolveMaterial.SetFloat(_DissolveAmount, evaluated); yield return null; } dissolveMaterial.SetFloat(_DissolveAmount, 1f); } }为什么要用AnimationCurve而不是直接用线性插值因为在实际视觉中消融过程很少是匀速的。火焰吞噬物体通常是先慢后快最后一下猛然烧完冰封碎裂则是前期慢中段突然加速最后崩解。用AnimationCurve可以让美术在Inspector里直观地画出消融速度随时间变化的曲线完全不用改代码就能调出不同的消融“手感”。这里也有一个非常容易踩的坑同一个材质被多个物体共享时SetFloat会改到所有物体上。如果你用这个Controller控制一个大型场景里的多个物体同时消融他们会一起开始一起结束完全没有错落感。解决办法有两个一是每个物体实例化一份材质new Material(sharedMaterial)再做材质清理避免内存泄漏二是利用材质PropertyBlock给每个物体单独传_DissolveAmount这样既能保持合批又互不干扰。我更推荐后者尤其是场景里可能同时出现几十个消融物件时用PropertyBlock能从根上避免材质实例泛滥。3.4 给模型加上两次Pass解决阴影和剔除问题用透明队列做消融还有一个经典问题物体消融到一半的时候阴影怎么处理如果沿用原来的ShadowCaster Pass它会按正常不透明物体绘制阴影消融掉的地方仍然会投出完整阴影看起来非常出戏。解决办法是给Shader再加一个ShadowCaster Pass里面同样采样噪声贴图做clip判断。我这里不贴完整阴影Pass了核心思路就是复制片元着色器在返回阴影深度值之前先跑一遍同样的噪声采样和clip判断让消融掉的区域不再投出阴影。这种做法在很多商业游戏里也是标配。另外如果你用的是URP管线还可以在Shader里加#pragma multi_compile _ _SHADOWS_SOFT等预编译指令处理软阴影细节更多这里先不展开了。4. 参数调优与效果进阶4.1 噪声纹理缩放对消融形态的影响同样的Shader换一张噪声贴图、换一个Tiling值消融出来的形态能差出十万八千里。_NoiseTex的Tiling值直接决定了噪声在整个模型表面上的“密度”。如果Tiling是1噪声纹理被拉伸到整个物体上消融边界往往是几块大面积的破碎适合做石碑风化、大型机器人瓦解如果Tiling放到5到10噪声更细碎消融边界会呈细密的颗粒状适合做液体蒸发、粒子化消失。我给项目调参数的经验是先确定这个效果是做“宏观消失”还是“微观分解”。宏观消失就是观众能明显看出物体被切掉几大块碎片感强这种把Tiling往小了调微观分解是观众看到物体表面像砂粒一样一点一点剥落这种把Tiling往大了调。至于具体数值需要结合模型的UV尺寸来判断一个角色模型和一个山体模型即使Tiling数值一样视觉密度也完全不同所以一定要在场景里实时看效果不能只盯着参数面板。4.2 边缘颜色和燃烧色的色彩设计动态着色的颜色设计是有规律可循的。消融物体的主色调决定之后边缘色通常选主色调的高亮补色燃烧色选主色调的暖色补色。举个例子一个深蓝色金属箱边缘色选青白色燃烧色选橙红色对比就非常强烈视觉效果很有“能量共鸣”的感觉如果做成岩浆效果就选暗红色基底边缘色用亮黄色燃烧色用暗红色。看起来是玄学其实背后就是色彩构成里经典的冷暖对比逻辑。我这里给一个常用参数组合魔幻传送门风格_EdgeColor设成(1, 0.9, 0.5)_BurnColor设成(0.6, 0.1, 0.9)_BurnIntensity设为1.5火焰吞噬风格_EdgeColor设成(1, 0.9, 0.3)_BurnColor设成(1, 0.2, 0)_BurnIntensity设为2.5冰封碎裂风格_EdgeColor设成(0.7, 0.9, 1)_BurnColor设成(0.2, 0.4, 1)_BurnIntensity设为1.0。这套组合我实测在不同项目里都挺稳的可以直接拿来当起点再微调。4.3 进阶玩法1UV扭曲让消融边缘更加生动静态噪声纹理的消融边缘虽然不规则但看久了会发现边界形态固定不变缺少流动感。我们可以对采样坐标加一层随时间变化的扭曲让边缘像活物一样蠕动。做法是在片元着色器里对UV加一个基于时间的偏移再叠加一次低频噪声。float2 uv i.uv; float distortion sin(uv.y * 30.0 _Time.y * 2.0) * 0.02; uv distortion; float noise tex2D(_NoiseTex, uv).r;这个写法的原理是把UV沿y方向做周期性弯折弯折幅度用正弦函数控制让原本直线排列的纹理采样点在垂直方向上错动。渲染出来的结果就是消融边缘会有轻微的水波状摆动。如果想让不同区域的摆动方向不一样可以再叠加一层噪声作为扭曲权重幅度更随机看起来更自然。但注意UV扭曲也不是越强越好。幅度超过0.1之后边缘会抖得厉害容易出现像素闪烁或者纹理拉伸的感觉一般控制在0.02到0.05之间比较安全。这个值需要在真机上多测几种分辨率和设备因为不同屏幕密度对抖动感知差异挺大的。4.4 进阶玩法2多频噪声叠加做细节层次单张噪声最让人头疼的是消融到中段时边缘形态会比较单调大面积区域同时被裁掉缺少大小块的层次感。解决办法是用两张不同频率的噪声叠加。低频率噪声决定大块的消融轮廓高频率噪声决定边缘的细碎颗粒两者一叠加消融形态就从“一张平滑破洞的纸”变成“一块被虫蛀得很不均匀的木头”。代码上很简单把原先采样一次噪声改成采样两次再混合float lowFreq tex2D(_NoiseTex, i.uv * 1.0).r; float highFreq tex2D(_NoiseTex, i.uv * 6.0).r; float noise saturate(lowFreq * 0.7 highFreq * 0.8);这里权重分配很有讲究。低频噪声权重太高消融轮廓太规整高频噪声权重太高边缘细碎到像电视雪花反而失去“块状感”。我一般按7比8来配让高频噪声稍微多于低频细碎感够足同时保留低频提供的大块轮廓。最终阈值判断仍然用混合后的noise值所有逻辑跟单张噪声完全一致只是输入源变丰富了。5. 常见问题与排查技巧实录5.1 消融边缘出现锯齿和马赛克出现锯齿的核心原因通常是噪声纹理分辨率不够。当噪声贴图被放大到整个模型表面时相邻像素的灰度差会形成明显的块状感消融边缘自然跟着变成一段段的折线锯齿。解决办法不复杂换一张分辨率更高的噪声贴图建议至少512x512起步如果消融区域经常在远景呈现1024会更稳。还有一个隐蔽的坑纹理导入设置里的压缩格式。Unity默认把纹理压缩成DXT5或ASTC格式压缩过程会损失精度导致噪声值出现不连续跳变。我在项目里会把噪声纹理专门加一个导出设置Import Type选TextureTexture Shape选2DCompression选NoneFilter Mode选Bilinear双线性插值Wrap Mode选Repeat。尤其是Wrap Mode如果错设成Clamp噪声在UV坐标接近0或1的边界区域会产生一条明显的持久亮线消融物体会在边缘留一圈“残影”特别难看。5.2 透明排序错乱模型穿帮消融效果必须给物体设置透明队列代码里的QueueTransparent就是为了这个。但透明队列本身有一个老问题透明物体不写入深度缓冲ZWrite Off两个透明物体互相遮挡时谁先画谁后画的排序会很别扭。如果你场景里有多个半透明物体它们之间可能会互相“穿透”看起来很假。这里没有完美的通用解法但有几个能明显改善的实操策略。一是尽量让消融物体靠近屏幕中心避免跟其他半透明物体大面积交叠二是把消融物体的RenderQueue调整成跟其他透明物体错开的数值比如设成3000到3500之间让它总能先于或晚于某些物体渲染三是如果消融效果比较核心可以用两个Pass第一个Pass只写深度第二个Pass再正常渲染颜色能缓解一部分排序问题。不过第二个Pass会在移动端增加一倍开销具体取舍看项目需求。5.3 移动端性能告急消融Shader在移动端有几个性能杀手需要特别盯着。第一个是噪声纹理的采样次数每多一次采样就多一次带宽消耗尤其是高分辨率屏下像素数量大采样开销被成倍放大。所以多频噪声玩法要谨慎用在低端机上。第二个是clip指令的布局GPU虽然对clip有硬件优化但如果条件分支内还有大量运算某些老芯片还是会卡。我的优化经验是先尽量把噪声采样和clip判断放在片元函数最前段裁掉的像素尽早退出减少无效运算。第三个坑是半透明物体的Overdraw。消融过程中物体会有一段“半透明”状态此时物体内部所有像素都会参与混合计算远处透过来的场景也会被多次混合移动端GPU的填充率压力不小。如果场景里同时有大量消融物体建议限制同时消融的数量或者用距离裁剪超出摄像机一定范围直接禁用消融效果。5.4 关于共享材质和Project刷新最后分享两个实际开发中很容易被坑的细节。第一个是共享材质的批量污染问题。我在3.3节里提过用PropertyBlock解决这里再展开一点如果你在脚本里直接对renderer.material.SetFloat(_DissolveAmount, value)Unity会默认自动实例化一份材质这在编辑器里方便但在正式包里会不断创建新材质导致内存上涨。如果你只在God模式下用或者做原型验证问题不大但正式项目我强烈建议走MaterialPropertyBlock路线或者由你自己管理一份材质实例。第二个是修改Shader后场景里物体变成粉色或者紫色。这通常是Shader编译出错了最常见的错误是变量名拼写不一致、语义Semantic标签写错或者某个函数在目标渲染管线里根本不存在。排查顺序是打开Console窗口看具体报错行号重点检查Properties里的变量名和CGPROGRAM里的声明是否一字不差——我经常因为下划线前后写反了排查半天。5.5 常见问题速查表问题现象直接原因处理建议消融边缘是方块状锯齿噪声纹理分辨率低或压缩格式有损换成512x以上无损噪声贴图消融到一半时物体全黑边缘色和燃烧色数值过低调高_EdgeColor和_BurnColor亮度物体完全不显示_DissolveAmount初始值大于等于1初始化设为0运行时再驱动所有同材质物体一起消融共享材质被SetFloat全局修改改用MaterialPropertyBlock或实例材质阴影位置出现“鬼影”阴影Pass没做消融裁剪添加带clip的ShadowCaster Pass金属/高光物体消融后发灰边缘色只叠了漫反射颜色在光照模型后叠加边缘色写在最后我做消融效果这条路走了不少弯路最开始的版本直接拿主纹理alpha做裁剪效果惨不忍睹后来换成噪声纹理又因为没有做边缘着色被美术一眼打回。踩过几次坑之后才慢慢理解消融效果的核心不在于“让物体消失”而在于“让消失的过程变得有说服力”。说服力来自边缘的细节——那个逐渐烧红然后熄灭的过渡带那个随时间蠕动的破碎边界那些看似复杂但原理并不玄妙的动态着色技巧。如果你正打算在项目里加入消融效果我的建议是先用手写Shader跑通基础版把clip、smoothstep、噪声采样这几个核心概念吃透再逐步叠加动态着色和进阶玩法。等这套基础扎实了再回头看Shader Graph你会发现节点连线反而成了限制发挥的枷锁。行了这篇就写到这里如果你在实际复现过程中遇到什么怪异问题欢迎按照上面的排查表先自查多半都能找到答案。
返回列表