
做游戏的时候消融真的是个特别好用的效果角色死亡化为灰烬、场景切换时空洞崩塌、Boss战后尸体慢慢消散、甚至连UI弹层都可以用它做腐蚀出现。我之前在项目里还见过有人拿它做传送门的过度动画效果意外地好。很多新手拿到这种效果第一反应是去资源商店找插件其实不用一个自定义Shader加一段简单脚本就能搞定而且可控性更强。这篇博客就把我在实际项目中打磨过的Unity消融效果完整实现思路拆开聊一聊。不废话核心原理先交代清楚消融效果本质是动态着色透明度裁剪。动态着色指物体从完整到消失的过程中靠近消融边界的那一圈要变色烧焦边缘、发光的灼烧边等而不是让贴图直接淡入淡出。透明度裁剪负责哪块像素先消失这块是通过噪声纹理的采样值和一个不断变化的阈值做比较来控制的。适合谁来参考刚接触Shader但已经写过最简单的顶点/片元着色器的新手、想在项目里快速落地进阶渲染效果的老手、以及想理解这类效果背后通用遮罩阈值边缘插值思路的开发者。下面会从原理、Shader结果、C#控制脚本、到头疼的问题排查一条线说清楚所有代码都是在Unity 2021.3 LTS 内置渲染管线Built-in里跑的URP版本后面单独说。1. 消融效果背后的核心原理拆解1.1 一张噪声图是如何决定谁先消失的先想一个问题同一个物体上所有的像素点凭什么有的先消失、有的后消失如果所有像素一起消失那只是普通的透明度淡出。要做出从某个地方开始一点点烧掉的效果就需要每个像素点有一个独立的、预先决定好的消失顺序。这个顺序就是噪声纹理提供的。噪声图是一张黑白灰图灰度值可以理解为该位置的耐烧程度值高的位置偏白最后消失值低的位置偏黑最先消失。具体实现方式是在片元着色器里采样噪声图得到一个0到1之间的值然后和一个全局的消融阈值我习惯叫BurnAmount做比较。关键来了当噪声值小于这个阈值时直接通过clip函数把这个像素裁掉当噪声值大于阈值时像素保留。随着阈值从0涨到1噪声图上那些偏黑的区域就会先被裁掉再慢慢波及到偏白的区域视觉上就形成了像用烧红的东西撩过纸张一样逐渐蚕食的效果。这一招在很多进阶Shader里都有影子冰面裂纹下的溶解、雪地脚印留下的压痕、甚至次世代游戏里墙面被子弹打中留下焦痕本质都是采样纹理 - 比较阈值 - 决定保留或舍弃这套逻辑。所以选择什么类型的噪声图直接决定了消融的形状观感。最常见的是Perlin噪声图灰阶过渡平缓烧出来的边缘像云朵或烟雾一样柔软如果用Voronoi图细胞噪声消融出来的是一格一格的蜂窝状碎块适合机械、科幻风格的碎片化消散如果用条纹状的纹理还能做出像百叶窗一样逐行消失的效果。我第一次做的时候随手拿了一张云朵纹理当噪声图用效果居然也还行所以不用被噪声两个字吓到本质上它就是一张用来当顺序表的灰度图。1.2 clip指令的工作机制与透明度测试的局限弄清clip指令等于弄清了这个效果的一半。clip函数在HLSL/CG里的作用是如果传入的数值小于0就把当前片元丢弃不再执行后面任何颜色计算也不会写入深度缓冲、颜色缓冲。用大白话说就是这个像素直接不画了。你可能觉得这不就相当于alpha test吗确实从渲染管线的分类上讲这属于透明度测试Alpha Test和透明度混合Alpha Blend有着本质区别。透明度混合是半透明叠加需要排序、需要关深度写入性能开销大而透明度测试是要么完全不透明、要么完全丢弃完全不涉及排序问题深度写入也照常进行。这个特性对消融效果太重要了因为我们希望剩下的那部分物体表面看起来是完全不透明的实心材质而不是能看到背后的半透明玻璃。不过透明度测试也有它的经典毛病边缘锯齿。因为像素只有保留和丢弃两种状态没有任何过渡所以消融边缘会呈现明显的像素锯齿。解决办法主要有两个方向一是做一些抗锯齿处理比如让噪声边缘加一层很窄的透明度渐变然后开启AlphaToCoverage这是在MSAA下的常用技巧二是在消融边缘加一圈高亮的灼烧颜色把视觉注意力吸引到亮点上锯齿感就会弱很多。我在移动端项目里更偏向第二种方案后面实现部分会细讲。另外要注意clip是基于逐像素的指令所以消融边缘的细节和模型本身的顶点密度无关。哪怕模型只有几十个顶点只要UV是连续的消融出来的边缘照样可以很精细因为裁剪发生在光栅化之后的每一个片元上。这个特性让消融效果特别适合那些面数不高的场景物件。1.3 边缘着色用lerp插值把烧焦边给标出来如果只是单纯裁掉像素效果其实很单薄缺少质感。真正让消融效果看起来贵的是消失边缘的那一圈颜色过渡。这就是动态着色里的动态二字所在。做法不复杂在片元着色器里我们已知当前像素的噪声采样值也已知当前的消融阈值两者相减会得到一个差把这个差值记为burn。如果burn小于0像素被裁掉不用管。如果burn在0到某个设定宽度范围内说明这个像素正处于消融边界上此时我们把颜色从灼烧边缘色过渡到正常纹理色。如果burn大于设定宽度像素离边界还很远正常输出纹理颜色就行。这个过渡用smoothstep或者lerp就能做。通常会用两层颜色靠近外侧即将被裁掉的区域是偏黑的焦色往里一些是炽热的亮橙色或红色再往里过渡到模型的正常贴图视觉上就很像金属被烧红后慢慢冷却的样子。用数学表达就是把burn映射成0到1的边缘权重用这个权重在焦黑 - 灼烧亮色 - 正常贴图这三个状态之间做插值。有的项目还会在消融边缘加自发光效果也就是把灼烧颜色直接加到最终颜色上或者通过Emission通道配合Bloom后处理让边缘亮起来。这类细节优化不多花多少性能但对画面质感的提升特别明显属于性价比很高的投入。2. 从零开始实现一个可用的消融Shader2.1 Shader属性的规划从参数设计看用户体验写Shader之前先理清要暴露给美术和策划哪些参数。参数设计得好不好直接决定这个东西落地时好不好用。我自己一般至少会留出这六个参数参数名类型作用_MainTex2D物体的主纹理_NoiseTex2D噪声纹理用来决定消融顺序_BurnAmountRange(0,1)全局消融阈值脚本控制的核心参数_BurnWidthRange(0,0.5)边缘过渡宽度控制烧焦边有多宽_BurnColorColor灼烧边缘主色亮橙色或红色_BurnFirstColorColor灼烧内圈焦黑色有的项目还会加一个_EdgeIntensity来控制发光强度还有的会加一个_BurnTex用来单独控制烧焦区域上的纹理细节。整体思路是一致的尽量把表现层的东西丢给材质面板把时间层的东西丢给脚本。美术调色的时候不用碰代码程序只管驱动_BurnAmount这个核心变量协作起来舒服很多。顺带一说参数的命名规范一定要统一。很多人喜欢用拼音缩写比如RZSL之类短期自己看得懂过两周回来看就不知道是什么了。我这里用的是当时项目里约定的风格稍微有点长但胜在自解释。在多人协作的团队里命名即注释这不是小事情。2.2 完整Shader代码与关键行解读下面给出我在内置渲染管线上用的一个基础版本核心逻辑全都写在了一个Pass里Shader Custom/DissolveEffect { Properties { _MainTex (主纹理, 2D) white {} _NoiseTex (噪声纹理, 2D) white {} _BurnAmount (消融强度, Range(0.0, 1.0)) 0.0 _BurnWidth (边缘宽度, Range(0.0, 0.2)) 0.05 _BurnColor (灼烧边缘色, Color) (1.0, 0.6, 0.2, 1.0) _BurnFirstColor (焦黑内圈色, Color) (0.1, 0.05, 0.02, 1.0) } SubShader { Tags { QueueAlphaTest RenderTypeTransparentCutout IgnoreProjectorTrue } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_ST; sampler2D _NoiseTex; float4 _NoiseTex_ST; float _BurnAmount; float _BurnWidth; fixed4 _BurnColor; fixed4 _BurnFirstColor; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uvMain : TEXCOORD0; float2 uvNoise : TEXCOORD1; }; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uvMain TRANSFORM_TEX(v.uv, _MainTex); o.uvNoise TRANSFORM_TEX(v.uv, _NoiseTex); return o; } fixed4 frag (v2f i) : SV_Target { // 采样噪声图r通道作为消融判断依据 fixed noise tex2D(_NoiseTex, i.uvNoise).r; // 核心裁剪噪声值小于阈值的位置像素被丢弃 clip(noise - _BurnAmount); // 计算当前像素距离消融边界的程度 float burn noise - _BurnAmount; float edgeRange saturate(burn / _BurnWidth); // 正常纹理颜色 fixed4 albedo tex2D(_MainTex, i.uvMain); // 在焦黑内圈色和灼烧边缘色之间按edgeRange插值 fixed4 burnColor lerp(_BurnFirstColor, _BurnColor, edgeRange); // 再根据edgeRange从灼烧边过渡回正常纹理 fixed4 finalColor albedo; finalColor.rgb lerp(burnColor.rgb, albedo.rgb, edgeRange); return finalColor; } ENDCG } } }逐段说一下关键点。首先是顶点着色器里用了两次TRANSFORM_TEX分别处理主纹理和噪声纹理的UV缩放偏移。这里有个常见的偷懒做法直接用i.uv去采样噪声图。不仔细想好像没问题但一旦材质上对主纹理和噪声图的Tiling/Offset设置不一致消融形状会跟着主纹理一起拉伸出现明显的方向性。两个纹理各自走各自的TRANSFORM_TEX美术调参数时也更自由。片元着色器里最核心的是三行。第一行是clip(noise - _BurnAmount)它决定像素生死。第二行是float burn noise - _BurnAmount把裁剪后的残余量算出来这个值一定大于0。第三行是float edgeRange saturate(burn / _BurnWidth)把残余量映射到0到1的范围内越接近消融边界值越接近0离边界超过_BurnWidth值就是1。之后用这个edgeRange做两次lerp就能得到焦黑 - 灼烧亮色 - 正常纹理的连续过渡。这里面有个细节值得琢磨为什么计算burn的时候用减法而不是直接用噪声图的值因为我们需要的是当前像素离边界有多远这个距离感。减法得到的burn天然是0到某个最大值之间的数再除以_BurnWidth就变成0到1的归一化权重逻辑非常干净。初学者容易犯的错误是直接用noise值去lerp那样会把整张纹理都染上灼烧色而不是只有边界一圈。2.3 在材质面板上调试参数的实用技巧Shader写完之后先别急着接脚本在材质面板上手动拖_BurnAmount滑块确认效果正确了再往下走。这一步能排除大量Shader写错的锅被甩给脚本的乌龙。调试时有几个观察重点。第一把_BurnAmount拖到0时物体应该保持完整只有极少数特别黑的噪声点会被裁掉这取决于你的噪声图是否有纯黑区域如果噪声图整体偏亮0的时候不应该有任何裁切。第二拖到0.5左右物体应该出现明显的大片消失区域且消失边缘有一圈清楚的过渡色。第三拖到1附近绝大多数像素被裁掉只剩零散亮点此时边缘色应该不太明显了因为整个物体都快没了。如果发现消融边缘特别脏、有奇怪的颜色条纹大概率是edgeRange计算出了问题或者_BurnWidth的值设得太小。另外如果你的模型本身带有比较复杂的UV重叠噪声图采样可能出现同一个UV对应模型上多个分离区域的问题表现是消融时好几个地方同时开始。解决办法通常是把噪声图换成立方体映射CubeMap采样或者是用一个三维噪声函数代替纹理采样。不过对大多数角色和场景物件来说UV重叠的情况不多这条可以当作扩展思路记着。3. 用C#脚本驱动消融过程3.1 基础版消融控制器从材质属性到动画曲线Shader解决的是怎么画的问题脚本解决的是怎么动的问题。下面这个脚本是我在项目里用得最多的一个基础版控制器支持延时开始、时长设置、循环播放和反向恢复using System.Collections; using UnityEngine; public class DissolveController : MonoBehaviour { [SerializeField] private Material material; [SerializeField] private float duration 2f; [SerializeField] private float delay 0f; [SerializeField] private AnimationCurve curve AnimationCurve.Linear(0f, 0f, 1f, 1f); [SerializeField] private bool startOnAwake true; [SerializeField] private bool disableWhenFinished false; private static readonly int BurnAmount Shader.PropertyToID(_BurnAmount); private Coroutine routine; private void Awake() { if (material null) { var renderer GetComponentRenderer(); material renderer.material; } material.SetFloat(BurnAmount, 0f); if (startOnAwake) Play(); } public void Play() { if (routine ! null) StopCoroutine(routine); routine StartCoroutine(PlayRoutine()); } public void Reverse() { if (routine ! null) StopCoroutine(routine); routine StartCoroutine(ReverseRoutine()); } private IEnumerator PlayRoutine() { if (delay 0f) yield return new WaitForSeconds(delay); float elapsed 0f; while (elapsed duration) { elapsed Time.deltaTime; float t Mathf.Clamp01(elapsed / duration); material.SetFloat(BurnAmount, curve.Evaluate(t)); yield return null; } material.SetFloat(BurnAmount, 1.05f); if (disableWhenFinished) gameObject.SetActive(false); } private IEnumerator ReverseRoutine() { float elapsed 0f; while (elapsed duration) { elapsed Time.deltaTime; float t Mathf.Clamp01(elapsed / duration); material.SetFloat(BurnAmount, 1.05f - curve.Evaluate(t)); yield return null; } material.SetFloat(BurnAmount, 0f); } }这里有几个实操细节。第一脚本导入到Inspector面板后材质一定要通过Renderer.material来获取实例而不是直接拿一个共享材质。直接用共享材质的话场景里所有用了同一份材质资源的物体会同时被改变做测试时经常看到一堆物体一起消失就是这个问题。Renderer.material会自动生成一个材质实例互不影响。第二完成时给_BurnAmount设置1.05而不是1.0是为了保证噪声图里所有像素哪怕是接近1的亮白区域也必定会被裁掉。如果只设置1.0恰好有像素的噪声值等于1.0理论上这个像素永远不会被clip掉会在最后留下一个顽固的亮点。多出0.05的余量这些边角情况就直接被吞掉了。第三AnimationCurve用起来比线性插值舒服得多。策划想调前慢后快还是先加速后减速直接在Inspector面板里拉曲线就行完全不用改代码。我在上面给了默认的Linear曲线实际项目里通常会把曲线调成先缓后急前面的消融看起来缓慢稳定最后阶段快速崩解更有视觉冲击力。3.2 多个物体同步消融的正确姿势很多场景需要的不是消融一个物体而是让一片区域的多个物体同时开始消散比如Boss的铠甲碎片、整栋建筑的多个模块。如果给每个物体挂一个DissolveController再各自调用倒也能跑但控制起来很别扭尤其是从中心向外扩散消融这种需求。我的做法是做一个静态工具类或者单例管理器统一管理一批目标的消融进度。核心思想是把消融的整体进度交给一个中央控制器每个物体根据自身与中心的距离来计算出各自的延迟时间然后依次执行public static class DissolveUtility { public static void DissolveGroup(Renderer[] targets, Vector3 center, float maxRadius, float duration) { for (int i 0; i targets.Length; i) { float distance Vector3.Distance(targets[i].bounds.center, center); float normalizedDistance Mathf.Clamp01(distance / maxRadius); // 距离中心越远延迟越久 var controller targets[i].GetComponentDissolveController(); controller.delay normalizedDistance * duration * 0.4f; controller.duration duration * 0.6f; controller.Play(); } } }这样做的好处是代码里不用关心具体每个物体的消融细节只要传入中心点、最大半径、总时长三个参数剩下的距离判断、延迟分配都由工具类完成。视觉效果上就是一道从中心扩散开来的消融波特别适合做场景崩塌或区域净化类的玩法。这个思路还能继续扩展比如用Perlin噪声给每个物体加随机偏移让波扩散不那么整齐避免机械感。3.3 不依赖贴图的程序化噪声Mathf.PerlinNoise动态生成纹理有些时候项目里找不到合适的噪声贴图或者美术提的需求是每次进入游戏消融形状都不一样。这种情况下动态生成一张噪声纹理是最省事的方案。Unity的Mathf.PerlinNoise可以直接在C#里采样Perlin噪声值我们只需要把它写进一张Texture2D的像素里运行时赋给材质就行public static Texture2D GeneratePerlinNoiseTexture(int width, int height, float scale) { Texture2D texture new Texture2D(width, height, TextureFormat.RGBA32, false); texture.wrapMode TextureWrapMode.Repeat; texture.filterMode FilterMode.Bilinear; for (int y 0; y height; y) { for (int x 0; x width; x) { float nx (float)x / width * scale; float ny (float)y / height * scale; float value Mathf.PerlinNoise(nx, ny); texture.SetPixel(x, y, new Color(value, value, value, 1f)); } } texture.Apply(); return texture; }有几个细节值得注意。首先scale参数决定了噪声的颗粒大小scale设小一点噪声图上的大块明暗区域会更大更平滑消融出来的是大块的斑驳scale设大一点明暗变化频繁消融边缘会更加细碎。其次生成完纹理后一定要调用Apply()否则像素数据不会真正上传到GPU材质上采样永远是空白的这是我第一次写这个函数时踩过的坑排查了半天。最后如果固定种子生成每次进游戏形状都一样如果要每次不同需要在调用Mathf.PerlinNoise之前给一个随机的偏移量。这套方案还有一个好处既然纹理是运行时生成的就完全可以控制生成逻辑。比如用两个不同频率的Perlin噪声叠加得到更自然的分形噪声消融边缘会非常丰富比单张噪声图好看得多。代价是生成的纹理在低端手机上会增加一点GPU内存256x256一张图通常不超过几百KB绝大多数场景下可以忽略不计。4. 从能跑到好用性能、阴影与渲染管线适配4.1 阴影投射中的消融必须处理的ShadowCaster Pass很多人在普通场景下做消融一切正常一开阴影就露馅物体本身都已经消融掉一半了它投射在地上的影子却还是完整的毫无变化。等到主物体全部消失影子才啪地一下不见特别出戏。原因很简单默认的阴影投射通道ShadowCaster Pass执行的是模型原本的渲染逻辑根本没做透明度测试。我们需要单独写一个ShadowCaster Pass在里面重复一遍消融的判断逻辑让它知道哪些像素应该被丢弃、哪些应该保留。不写这个Pass的话主相机看到的画面是消融后的但阴影相机看到的仍然是完整的模型。Unity内置管线的ShadowCaster Pass可以这样处理最简单的做法是直接use Unity的ShadowCaster Pass再重写裁剪逻辑Pass { Name ShadowCaster Tags { LightMode ShadowCaster } CGPROGRAM #pragma vertex vertShadow #pragma fragment fragShadow #pragma multi_compile_shadowcaster #include UnityCG.cginc sampler2D _NoiseTex; float4 _NoiseTex_ST; float _BurnAmount; struct v2fShadow { V2F_SHADOW_CASTER; float2 uvNoise : TEXCOORD1; }; v2fShadow vertShadow(appdata_base v) { v2fShadow o; o.uvNoise TRANSFORM_TEX(v.texcoord, _NoiseTex); TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) return o; } fixed4 fragShadow(v2fShadow i) : SV_Target { fixed noise tex2D(_NoiseTex, i.uvNoise).r; clip(noise - _BurnAmount); SHADOW_CASTER_FRAGMENT(i) } ENDCG }这里用了Unity内置的TRANSFER_SHADOW_CASTER_NORMALOFFSET和SHADOW_CASTER_FRAGMENT宏它们会帮我们处理好阴影偏移和深度写入的细节比我手写裁剪和偏移靠谱得多。注意顶点着色器的输入用了appdata_base里面包含了position、normal、texcoord阴影投射需要法线来做深度偏移所以基础数据要齐全。写完这个Pass之后会发现消融物体的影子终于能跟着主物体的形状一起烧掉了。这一点在角色死亡、场景破坏这类玩法中尤其重要因为玩家对影子的异常非常敏感一个完整阴影配上半截消融的模型瞬间出戏。4.2 双面消融的坑Cull Off之后注意法线翻转如果消融用在平面物体上比如一块告示牌、一片纸屑、一块布料默认的背面剔除Cull Back会导致物体翻转过来就完全看不见了。这时需要把渲染状态改成Cull Off让正反两面都渲染。直觉上改动很小但实际坑不少。最明显的是片元着色器里采样主纹理的时候如果用模型的UV那么背面和正面的纹理方向是镜像的视觉上没啥大问题但如果你在片元着色器里依赖模型法线方向做某些计算比如边缘光、菲涅尔效果就会看到背面和正面亮度不一致非常突兀。另一个更隐蔽的问题是Cull Off会让每个像素被绘制两次正面一次、背面一次 片元着色器的执行开销直接翻倍。如果消融用在大量小物件的性能敏感场景要评估是否值得。我一般只在单个平面物件上使用Cull Off对角色和建筑这类封闭几何体继续用Cull Back因为封闭几何体打开Cull Off后背面的消融判断其实是被前表面挡住的画了也看不见纯属浪费性能。4.3 URP管线下要改的东西项目如果用的是URPUniversal Render Pipeline上面这段Shader不能直接用。URP有自己的Shader库和关键字系统CGPROGRAM换成HLSLPROGRAM内置函数也要换成URP的版本。这里给一个最小可用的URP版本思路不把整段代码堆出来但关键改动会讲清楚第一包含文件从UnityCG.cginc换成Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl。第二顶点到裁剪坐标的函数从UnityObjectToClipPos换成TransformObjectToHClip。第三URP下Shader的Tags里要加上RenderPipelineUniversalPipeline否则URP会跳过你的Shader或者灰屏。第四URP默认支持SRP Batcher如果打算跑SRP BatcherShader里声明的所有材质属性都需要放在CBUFFER块中这个新手特别容易漏漏了之后SRP Batcher不生效Draw Call会肉眼可见地增多。URP版本的消融Shader在性能和兼容性上有它的优势比如能更好地和URP的后处理栈配合。但如果你还在用内置渲染管线就不必急于迁移逻辑和思路完全一致改起来也算不上伤筋动骨。4.4 性能分析与移动端优化建议消融效果本身的开销并不算大多了一次噪声纹理采样加一次clip判断加几次插值计算。这放在现代GPU上都是毛毛雨。真正可能拖慢性能的往往是阴影投射和像素填满率。阴影投射高分辨率的方向光阴影贴图如果覆盖了多个消融物体每个消融物体的阴影Pass都会重新执行一遍clip判断。物体数量一多开销就会被放大。建议给阴影Pass里的噪声采样也加上LOD或降采样控制或者干脆在性能紧张时关闭小物件的阴影投射。像素填满率当消融阈值很高、物体上只剩零散亮点的时候GPU依然会对物体原本覆盖的所有像素执行片元着色器逻辑只是大部分像素都被clip掉了。所以消融对overdraw并没有显著改善。解决思路是配合剔除当_BurnAmount超过0.9后可以直接把Renderer关掉或者通过LOD机制切换到低面数版本。移动端移动端GPU的fillrate和带宽都比较吃紧噪声纹理尽量用低分辨率256或128采样时用Trilinear或Bilinear就好不要开各向异性过滤。条件允许的话用质量更低的RGB565格式而不是RGBA32能省不少内存带宽。最后再分享一个我在实际项目里总结的小技巧如果消融的物体是一个完整的角色最好把消融效果做成从脚底逐渐向上烧不要整体均匀消失。实现手法特别简单在Shader里把_BurnAmount和模型的世界空间Y坐标做一个组合底部区域的阈值更高、优先消失顶部阈值低、最后消失这样看起来就像角色从下往上被火焰吞噬比整体同时消失有表现力得多。具体的组合公式可以根据项目需要调整核心思想是消融进度不再是一刀切的全局标量而是可以随空间位置变化的场。这个思路一旦打开配合距离场、高度场、噪声场能玩的消融花样还有很多。这套东西做完之后其实不只是消融效果本身你相当于掌握了一整套阈值 遮罩 插值的渲染工具箱以后做冰面溶解、做魔法符文显现、做废墟被时间沙尘覆盖改改遮罩来源和颜色插值规则就能快速套用。好的效果从来不是靠一个孤立的技巧而是靠一套能灵活组合的基础能力。希望这篇内容对你有帮助也欢迎坑里见。