ARTICLE DETAIL

资讯详情

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

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

Unity Shader消融效果实战:动态着色与噪声纹理裁剪 看到“动态着色”和“消融效果”这两个词放在一起懂行的朋友应该已经猜到了这又是一篇Shader实操笔记。消融效果在游戏里太常见了不管是角色死亡、场景坍塌、还是Boss出场时的撕裂感本质上都是同一套东西——用噪声纹理驱动片元裁剪让物体从完整到破碎、从实心到消散。这些年我在Unity里做过不下十种消融变体今天把最核心的那套实现思路、完整代码和调参经验一次性说清楚。这篇文章适合两类人一类是刚接触Shader、被各种术语劝退的新手我会把原理掰开揉碎讲明白另一类是被项目逼着做各种消融变体的老手可以直接跳到第三章和第四章那里有边缘光、粒子配合和一堆坑的解决方案。不管你是用内置渲染管线还是URP这套思路都能落地最后我会给出迁移要点。1. 消融效果的本质先搞懂“动态着色”到底是什么动手写代码之前得先把“动态着色”这个概念讲透。很多人以为消融效果就是让物体透明度变成0其实完全不是这么回事。透明渲染在Unity里一直是性能杀手因为半透明物体没法写入深度缓冲只能从后往前排序渲染物体一多合批就废了Overdraw飙升移动端直接卡成PPT。更重要的是如果只是调透明度物体仍然占据原来的空间玩家能看穿模型内部的结构——那种“被切了一刀”的干净利落感靠透明度根本做不出来。消融效果的底层逻辑是像素级别的动态裁剪。每一帧渲染时Shader会对模型的每个片元可以理解为屏幕上每个像素对应的模型表面点做判断这个片元是显示还是丢弃。丢弃的片元直接不参与后续任何计算不写入颜色缓冲也不写入深度缓冲效果上就好像这个位置的材质被“挖”掉了。配合噪声纹理作为判断依据就能做出从边缘向中心逐渐瓦解的动画。这个方案的厉害之处在于它完全绕开了透明排序的问题——被裁剪掉的片元就是不存在了剩下的片元全部走不透明渲染管线性能和普通物体几乎没差别。我在移动端做过压力测试同时消融50个中等面数的角色帧率稳定在60帧这在透明渲染方案里是不可想象的。1.1 动态着色的核心机制与适用场景动态着色这个词听起来高大上其实就是“材质属性随时间或输入变化”的统称。消融、溶解、波纹、渐变、描边脉动都属于动态着色的范畴。它们的共同点是渲染本身是静态的Shader但通过传入时间变量、脚本参数或者纹理坐标让每个片元的颜色、透明度、裁剪状态在运行时动态改变。消融效果具体能用在哪些地方我列举几个实际项目里验证过的场景角色死亡消散不管是怪物被打败后的碎尸消散还是主角被击败时的退出演出消融都能让画面更干净。配合粒子发射灰烬和光点质感会更高级。场景物件生成地下城入口缓缓浮现、墙面的符文逐渐亮起、建筑从虚到实“长”出来用消融的逆过程从外向内恢复就能实现。破坏与坍塌演出桥梁断裂后断面处做消融比直接播放碎块飞散动画更有“能量感”。很多手游的Boss出场动画也用这套先消融出轮廓再生成实体。技能特效冰冻蔓延、时空扭曲、毒液腐蚀本质上都是消融的变体——只是把裁剪模式换成了“从某点向外扩散”。选型的时候有一个建议如果你的项目对性能要求很严例如要在中低端安卓机上同时跑多个消融物体那么“噪声纹理Clip裁剪”是默认首选如果你追求极致的视觉表现比如要做出多层溶解带渐变色的效果那可以考虑在裁剪的基础上叠加额外的边缘光Pass。1.2 为什么选择噪声纹理作为消融的“模板”消融效果的核心问题只有一个怎么决定每个片元是保留还是丢弃如果用一个简单的变量比如物体的高度、距离中心的距离来决定效果就是“一刀切”——整个平面均匀被切掉看起来像激光切割而不是自然的消融。当然有些场景你确实要这种“硬切”效果但绝大多数消融都应该带不规则感边缘坑坑洼洼像被虫蛀过一样。噪声纹理就是用来提供这种不规则性的。最常用的是Perlin噪声和Value噪声它们的共同特征是值在空间分布上连续但在数值上有随机波动。连续意味着视觉上不会出现杂乱的黑白噪点随机波动意味着消融边缘不会是平直的线条。这里有一个新手容易犯的误区直接用随机数贴图。随机数贴图比如白噪声每一个像素的亮度都完全独立采样出来就是满屏雪花裁出来的边缘奇碎无比还带严重的锯齿闪烁。Perlin噪声这类平滑噪声才是正确的选择它保证相邻区域的噪声值相近裁剪出来的边缘就是自然的破碎块状。实际项目中我更推荐用三维噪声纹理或者二维噪声做UV延续采样。Unity Shader自带的三维噪声例如PerlinNoiseWaves效果很好但在移动端纹理带宽略高。折中的方案是生成一张2048x2048的二维Perlin噪声贴图然后采样时让UV乘以一个较大的倍数比如20到50这样噪声在模型表面被重复多次每个小块区域的消融碎片感就出来了。至于为什么不直接用模型UV原始坐标——因为UV跨度太小的话噪声特征不够密集大块大块地消失视觉上像泡面碗盖被掀开不够“碎”。2. Shader实现核心代码与参数详解原理讲完了直接上代码。我给一段基于Unity内置渲染管线Built-In Render Pipeline的完整Surface Shader这是最容易上手的写法适合新手和独立开发者。项目用URP的朋友别急后面有一小节专门说迁移。Shader Custom/DissolveEffect { Properties { _MainTex (Albedo (RGB), 2D) white {} _NoiseTex (Noise Texture, 2D) white {} _Amount (Dissolve Amount, Range(0, 1)) 0 _EdgeWidth (Edge Width, Range(0, 0.5)) 0.05 _EdgeColor (Edge Color, Color) (1, 0.5, 0, 1) } SubShader { Tags { RenderTypeOpaque } LOD 200 Cull Off CGPROGRAM #pragma surface surf Lambert addshadow #pragma target 3.0 sampler2D _MainTex; sampler2D _NoiseTex; float _Amount; float _EdgeWidth; fixed4 _EdgeColor; struct Input { float2 uv_MainTex; float2 uv_NoiseTex; }; void surf (Input IN, inout SurfaceOutput o) { // 采样噪声纹理的R通道 float noise tex2D(_NoiseTex, IN.uv_NoiseTex * 15).r; // 裁剪噪声值小于当前阈值的片元直接丢弃 clip(noise - _Amount); // 计算过渡带根据噪声值与阈值的差值按边过渡带宽做归一化 float edgeFactor saturate((noise - _Amount) / max(_EdgeWidth, 0.001)); // 过渡带内混合边缘光颜色 fixed3 edgeGlow _EdgeColor.rgb * (1 - edgeFactor) * smoothstep(0, 1, 1 - edgeFactor); fixed4 c tex2D(_MainTex, IN.uv_MainTex); o.Albedo c.rgb edgeGlow; o.Alpha c.a; } ENDCG } FallBack Diffuse }代码不长但每一行都有讲究。我用Step By Step的方式拆开解释。第一步采样噪声纹理。tex2D(_NoiseTex, IN.uv_NoiseTex * 15)这里的* 15是UV缩放把噪声贴图在模型表面重复15次让碎片感更细碎。数值越大碎片块数越多数值越小消融越像大块斑驳剥落。这个值我建议做成可调的不同物体差异很大石碑用8衣物用20皮肤用30甚至更高。第二步Clip裁剪。clip(noise - _Amount)相当于C语言里的if (noise - _Amount 0) discard;。当_Amount是0时噪声值小于0的片元被丢弃——如果噪声纹理的值域是0到1那么没有任何片元被丢弃物体完好。当_Amount慢慢增加到1时绝大多数片元都会被丢弃物体彻底消失。所以脚本里要做消融动画本质就是_Amount从0平滑增加到1。第三步边缘光计算。如果你只想做最基础的消融到clip就结束了。但完全干巴巴的消失不够好看于是我在边缘加了一圈暖色光晕。edgeFactor表示当前片元距离裁剪边缘的“安全距离”值为0说明它刚好处于要被裁剪掉的边界值为1说明它离边缘很远。1 - edgeFactor就是边缘强度离边缘越近越亮。max(_EdgeWidth, 0.001)是为了防止除零——当_EdgeWidth设为0时不至于让整个Shader崩掉。2.1 从原理到代码为什么Surface Shader更适合入门Surface Shader是Unity封装好的一层“快捷写Shader”的方案它把光照、雾效、阴影等大量繁琐的底层逻辑全部自动生成你只需要写表面属性颜色、金属度、粗糙度和自定义的裁剪逻辑。我之所以给入门读者推荐Surface Shader是因为它可以让你把100%的精力都投入到消融裁剪的核心逻辑里而不需要关心光照模型怎么写。覆盖阴影也是一个关键点。在Surface Shader里addshadow指令会自动生成阴影投射Pass让被消融的区域也能正确地投射阴影。如果你写的是顶点片元着色器Vertex/Fragment Shader你需要额外自己实现ShadowCaster Pass并且要把裁剪逻辑在阴影Pass里再写一遍否则会出现一个巨大的矩形阴影块——这是新手几乎必踩的坑。说到Surface Shader的缺点主要有两个一是它依赖内置光照管线的自动生成代码对自定义渲染需求例如自定义雾效、自定义深度不够灵活二是在URP里Surface Shader全部失效需要改写。所以我的建议是新手先用Surface Shader理解原理项目要上线了再切换到顶点片元着色器做性能优化和渲染管线适配。2.2 核心参数逐一拆解把每个滑杆都调到最优Shader面板上的四个参数是调效果的关键我不能只告诉你有这几个参数得告诉你每个参数该往哪个方向调、为什么这么调。_Amount消融程度这个是最核心的动画变量。取值范围0到1脚本动态修改它就能驱动整个消融过程。通常我会配合协程或者DOTween来平滑过渡一秒内从0渐变到1视觉效果最舒服。需要注意的是噪声纹理的值域分布会影响消融节奏如果噪声纹理大多数像素值集中在0.5附近那消融在0.4到0.6之间会突然加速视觉上像“跳变”。要解决这个问题可以用Photoshop或ShaderLab里的pow函数对噪声值做重映射把值分布拉均匀。_EdgeWidth边缘宽度控制消融边缘的发光带宽度。数值越大发光带越宽视觉冲击力越强数值太小发光带只有一像素宽截图好看动起来基本看不见。我通常设在0.02到0.1之间具体看模型大小和摄像机距离。大模型比如建筑需要更大的值小模型比如武器则适当缩小。_EdgeColor边缘颜色消融边缘的光晕颜色。暖色橙、红给人“燃烧”的感觉冷色蓝、青给人“冰封”或“能量涌动”的感觉。如果想做高级一点可以用渐变纹理Ramp Texture代替单一颜色让边缘从内部到外部有更丰富的色阶变化。不过我实测发现单一颜色加_EdgeWidth控制其实已经足够用了——因为边缘本身很窄渐变色的提升幅度有限但多一张纹理的管理成本是实打实的。_NoiseTex噪声纹理我在工程里准备了三张噪声贴图一张是经典的Perlin噪声用于普通的溶解一张是FBM分形布朗运动叠加后的噪声用于让边缘更碎片化、有更多细节一张是偏带状分布的噪声用于做“雷电击穿”式的纵向裂缝消融。建议美术同学也多做几张不同类型的噪声图效果差异很大值得花时间调试。2.3 URP环境下的迁移要点如果你项目用的是URPSurface Shader要全部重写为Shader Graph或者手写HLSL。手写的话核心逻辑和上面几乎一样但有几个改动点需要留意。第一Tags要改成RenderPipelineUniversalPipeline。第二Surface Output结构体需要从SurfaceOutput换成SurfaceData并且需要自己实现光照模型最简单的方式是使用UniversalFragmentPBR函数。第三裁剪逻辑照搬但要注意在URP里clip()函数对GPU分支优化更敏感建议把裁剪放在最前面用clip做完立即丢弃避免后续光栅化和着色器计算浪费性能。我的建议是项目用URP的直接上Shader Graph。虽然代码看着更“硬核”但Shader Graph对非图形学出身的开发者友好太多而且URP官方对Shader Graph优化做得很好SRP Batcher兼容性也是默认就处理好。Shader Graph里实现消融的思路和代码是一回事用噪声节点采样接一个Step或Clamp节点做阈值判断连到Alpha Clip Threshold上再取噪声与阈值差值接一个Lerp做边缘发光。3. 进阶玩法边缘光、残影与粒子的组合拳基础消融做完之后90%的效果已经出来了。但如果你想让画面质感再上一个台阶下面这些进阶技巧值得尝试。这些都是在项目实战里调出来的不是从文档里抄来的理论。3.1 从“消失”到“燃烧”边缘光照的强化技巧刚才代码里的边缘光比较简单只有一个单色叠加。如果想要更接近“燃烧”的质感有三个细节可以优化。细节一双层边缘带。在_EdgeColor外层再叠加一层更亮的中心色通过二次采样同一个噪声纹理但偏移一点点UV坐标实现。这样边缘会有“内部高亮、外部暗红”的双层渐变看起来更像真实的燃烧层。实现方式是先计算edgeFactor然后用pow(edgeFactor, 2)作为第二层光的插值因子颜色用橙黄渐变。细节二沿法线方向的偏移和扩张。消融边缘通常是平的看起来有一点纸片感。可以读取模型法线让裁剪阈值沿法线方向稍微偏移。Shader里用o.NormalSurface Shader输出里的法线方向乘一个很小的数加到采样UV或阈值计算上。这个技巧让边缘有一种“鼓起来”的立体感尤其是用在皮肤、布料这类软表面上观感很好。细节三时间变量驱动的颜色脉动。给边缘颜色加上_Time.y参与运算让发光强度有一个呼吸的节奏。燃烧边缘往往不是恒定亮度而是在剧烈脉动。代码里乘sin(_Time.y * 30) * 0.3 0.7一类的因子即可。注意参数别调太大否则动态看起来会很闪、很廉价。3.2 配合粒子做真正的“消散”演出Shader做得再好如果物体是凭空消失缺少物理感的“碎片落下”和“能量释放”还是会觉得不够带感。我的经验是消融Shader负责“物体的消失”粒子系统负责“消失后的余韵”两者必须搭配才能做出完整的演出。常用组合方式是这样在物体消融的同时从消融边缘位置可以通过脚本计算获取模型顶点位置并筛选阈值附近的顶点或者预先放置空物体作为粒子发射锚点持续发射两类粒子。一类是快速向外扩散的烟雾粒子颜色和消融边缘色一致透明度随速度衰减模拟燃烧物的升腾。另一类是细小荧光粒子或碎屑粒子走物理模拟从模型表面飞溅模拟能量崩解。实现上有个小技巧在_Amount从0到1的过程中每帧用OnWillRenderObject或者Update里读取当前Shader的_Amount值把剩余未消融的模型表面顶点位置采样出来作为粒子的出生点。这样粒子和消融边缘的贴合度能达到像素级观感极其流畅。当然如果嫌复杂也有简单粗暴的替代方案——在模型中心放一个粒子发射器调节粒子遮罩让粒子只在特定方向发射效果也不错但贴合度不够完美。4. 实操踩坑实录常见问题与排查技巧这部分是我最想说的。写Shader的坑太特殊了出问题不是“报错”而是“效果不对”新手排查起来特别绝望。我整理了这四年里遇到的高频问题直接给方案别再走弯路。4.1 高频问题与解决方案速查表先放一张速查表然后挑最核心的几个详细讲。症状可能原因解决方案物体全部消失了无论怎么调Amount都是空的采样UV错误或噪声纹理格式不对检查噪声纹理是否设为Repeat模式把UV乘以系数放开再试消融边缘出现严重的黑色裂缝或锯齿过渡带计算非线性边缘处值跳变用saturate和smoothstep对edgeFactor做平滑处理开启MSAA抗锯齿消融过程中模型出现大量面片飘浮模型本身由多个子网格组成被分别裁剪检查模型导入设置确认所有子网格的包围盒正确生成或者让美术合并网格阴影消失很慢或者出现整块黑色阴影阴影Pass里没有写裁剪逻辑在Shader的ShadowCaster Pass里同样加入clip判断并同步读取_Amount移动端出现大面积闪烁噪声纹理精度太低16位纹理有带宽溢出切换噪声纹理为RGBA Half格式或者改用过程函数生成噪声避免贴图采样模型内部被看到消融后能看到内部三角面模型没有开法线翻转的双面渲染添加Cull Off或者导入模型时开启Double Sided关于“全部消失”这个问题最容易让人抓狂。刚写好Shader拖到材质上模型瞬间就没了。排查思路是clip(noise - _Amount)里假如_Amount默认是0那么只要噪声纹理采样结果全部小于0片元就会被全裁。什么情况会让噪声全部小于0最常见的就是UV采样坐标错了——噪声纹理Wrap Mode如果不是RepeatUV超出0到1范围的部分会取边界值通常噪声纹理在边界是黑色也就是采样值为0于是0 - 0 0不满足clip条件但如果是灰色区域就完蛋了。所以第一件事就是检查噪声纹理的Wrap Mode务必设置为Repeat。关于黑色裂缝消融边缘出现黑边几乎都是边缘光计算没有正确融合造成的。注意看我代码里o.Albedo c.rgb edgeGlow这一行——这里用的是加法混合而不是覆盖。如果上一帧片元被裁剪下一帧因为数值抖动又被显示就会出现原始的黑暗边缘。解决办法是用lerp(c.rgb, _EdgeColor.rgb, glowMask)代替加法让边缘光的强度完全由transition带决定而不是叠加到原色上。关于阴影问题如果只用Surface Shader的addshadow阴影Pass的裁剪是自动同步的。但一旦你改写成顶点片元着色器或者Shader Graph就必须手动处理。这个坑太隐蔽了我还记得第一次遇到时地面上一片矩形黑影随着消融进程缓缓变淡怎么都想不明白是啥——直到我意识到ShadowCaster Pass里压根没有clip指令阴影源还在用原始模型但因为光照计算在一个Pass里嘛阴影的形状是完整的只是被亮度变化“模拟”出了淡出效果。所以记住凡是改了主Pass的Shader但凡还要阴影就必须把裁剪逻辑同步写进ShadowCaster Pass。4.2 性能优化与移动端适配的细节消融Shader本身性能开销不大但有一些细节在移动端会放大十倍必须注意。第一减少纹理采样次数。我的示例代码里采样了两张纹理主纹理和噪声纹理这是基础中的基础。如果你叠加了第二层噪声、渐变纹理每层都是一次带宽消耗。移动端纹理带宽非常金贵我实测过在低端安卓机上每多采样一张2K纹理整体渲染耗时增加约0.3到0.5毫秒。结论是能过程生成的就用函数生成例如frac(sin(dot(...)))能复用R通道的就不要另开一张纹理。第二区分低端机和高端机。我的做法是写两个变体DISSOLVE_SIMPLE和DISSOLVE_FANCY高端机开FANCY带边缘双层光、法线偏移低端机走SIMPLE只有基础裁剪。用Unity Shader的multi_compile指令在运行时切换代价是ShaderLab编译时间变长和包体增加但换来的是中低端机的稳定体验完全值得。第三利用GPU实例化。如果你在一个场景里同时消融大量同类物体例如丧尸潮死亡可以在Shader里启用#pragma multi_compile_instancing并确保_Amount是实例化属性。这样每个物体的消融进度独立但合批不掉性能。这里有个小坑实例化属性默认只支持float、vector等简单类型如果你的_Amount是float直接加UNITY_INSTANCING_BUFFER_START/END包裹即可完全兼容。第四注意零点抖动。当_Amount接近1时噪声阈值趋近于所有片元的最大值边缘只剩零星几个片元在闪烁。这是因为精度问题导致的取舍抖动。解决办法在Shader里加一层LOD判断当_Amount超过0.95时直接强制全部裁剪视觉上已经几乎不可察觉但能省下最后的渲染开销。4.3 一个调试的“笨办法”但极其高效写Shader最痛苦的是看不见中间变量。你算出edgeFactor但不知道它到底是0.2还是0.8就没法判断为什么边缘光不对。我的土办法是把变量直接输出到Albedo上。具体操作就是临时把o.Albedo改成你要看的变量。比如我想看噪声纹理采样效果就写o.Albedo fixed4(noise, noise, noise, 1)这样模型表面直接变成灰度图一眼就能看出噪声纹理在模型上的分布是否合理、有没有明显的采样拉伸或重复感。想调边缘光的范围和亮度就输出edgeFactor想看在UV重复次数下的分布就输出frac(IN.uv_NoiseTex * 15)。这个方法比任何图形调试工具都好用尤其在没有RenderDoc的时候。调试完记得把输出改回来就行我一次次靠这个“笨办法”在半个小时内解决一个看似不可能的Shader Bug。5. 项目中的实际调参记录三组场景的参考数值写到这里给一个从实际项目里整理出来的调参记录方便大家直接照葫芦画瓢。这三个场景覆盖了90%的消融需求拿去用再微调就行。场景类型噪声UV缩放EdgeWidthEdgeColorAmount动画时长粒子系统配合角色死亡消散200.03橙红(1, 0.4, 0.1)1.2秒烟雾碎屑场景建筑生成80.06蓝青(0.2, 0.8, 1)2.5秒上升能量粒子技能腐蚀腐蚀300.02紫绿(0.6, 0.2, 1)0.8秒喷溅微粒关于角色死亡消散的调参心得这里的关键是“先快后慢”的Amount曲线。直接线性插值看起来太机械我用Mathf.SmoothStep(0, 1, t)配合Mathf.Pow(t, 1.5f)让前0.4秒消融较慢、积累张力然后0.6到1之间加速扩散配合角色下沉和粒子爆炸比匀速消失有冲击力得多。边缘宽度在近景特写时调到0.05全景战斗时改回0.02避免远处看成一圈糊。关于建筑生成的调参心得建筑消融一般要体现“从虚到实”的庄严感Amount动画时长要长一点我用2.5秒。EdgeColor选择冷色系和城市环境的暖光形成强烈对比。这里要注意的是建筑体量大UV坐标跨度大如果噪声UV缩放太小比如6以下会出现大面积扁平色块像贴了马赛克建议8到10之间。另外建筑通常有大量窗户、梁柱等子网格务必确认所有子网格的包围盒和顶点色一致否则某些部位会提前消失。关于技能腐蚀的调参心得技能特效的消融往往要配合技能半径随技能时间扩散所以_Amount不能用全局变量而要从技能中心点计算距离场再混入噪声因子。最简单的做法是在脚本里每帧计算技能半径对应的裁剪阈值传一个_DissolveCenter世界坐标进Shader在片元中计算到中心的距离并与阈值混合。这个变体在MOBA手游里很常见表现手法是“从某一点被侵蚀扩散到全屏”。实际调配中我发现一个非常反直觉的现象_Amount动画时长对视觉观感的影响远小于_EdgeColor的亮度。把边缘颜色亮度提升到2甚至3HDR范围配合Bloom后处理整个消融质感能瞬间提升一个档次。如果你的项目开了Bloom务必把边缘颜色的强度调高这样才能在HDR亮度下被Bloom捕捉到产生真正的发光效果。如果不开Bloom单纯提高亮度只会让颜色泛白这时候应该转而增加边缘宽度而不是亮度。说到这里再分享一个关于本次所有代码和参数的提醒不同Unity版本之间Shader编译器的优化行为会有细微差异。我遇到过同一个Shader在Unity 2021上表现正常升到2022后边缘光出现一格一格的条纹排查了两天发现是编译器把smoothstep里的一个中间计算提前了精度丢失。最后用static const强制内联算出错的部分解决。如果你也在升级版本后遇到Shader效果突变别急着怀疑渲染管线先检查有没有精度相关的隐式转换——half和float混用最容易出这种幺蛾子。最后Shader这条路没有捷径只能多写多调。这套消融Shader的直接改造空间很大把clip换成alpha blend就是消融溶解把噪声换成距离场就是区域逐层侵蚀把_Amount接到鼠标位置就是互动挖洞。动手改一改比照着文章看十遍有价值得多。
返回列表