资讯动态

Unity 2D图片溶解扩散效果Shader实现与调参详解

发布时间:2026/10/23 4:54:02 来源:尧图企业网站定制
做2D游戏的时候你有没有遇到过这种情况角色死亡、怪物消失、场景切换这些视觉节点如果只是直接SetActive(false)或者做个简单的渐隐玩家会觉得干巴巴的。所以动画师和程序经常会求一个效果——让一张图像被某种神秘力量从内部“啃食”掉缺口沿着边缘不断向外蔓延同时还带一圈高亮边框。这就是“Unity2D图片溶解扩散效果shader”最典型的应用场景。这篇文章就是把我在实现这个效果时踩过的坑、用过的公式、最终稳定的Shader代码全部整理出来。适合已经写过基础Unity Shader、但不太清楚溶解扩散怎么和2D Sprite配合的开发者也适合只想拿到完整代码直接改参数的美术或全栈型程序。看完你至少能实现从指定中心点向外扩散的溶解效果、带燃烧色边缘的腐蚀效果、以及一套用于动态控制进度的C#脚本。1. 效果拆解与核心设计思路1.1 一个溶解效果由哪三部分构成先说清楚“溶解扩散”不等于普通的透明度变化。它需要三个东西协同工作一是进度值。它是一个0到1的数值驱动整个效果的进行0表示图片完整无损1表示图片彻底消失。脚本侧要做的事情基本就是不断把这个数从0推到1剩下的交给Shader。二是噪声纹理。溶解的效果之所以看起来“破碎”、有毛边而不是一个严格几何的圆形空洞靠的就是一张噪声图给不同像素随机的“抵抗力”。噪声值高说明这个像素晚一点被融化噪声值低说明它提前消失。三是距离因子。有了噪声还需要让溶解行为有方向性——从一个点开始往外扩散。越靠近中心的像素越弱越早被消掉越靠边缘的像素越顽强能挺到最后。这个距离因子就是把“静止的噪声溶解”变成“有扩散感的溶解”的关键。如果你把溶解效果想象成一锅正在烧开的汤进度值是火力大小噪声图就是汤里一个个气泡的位置距离因子则是气泡从中心开始冒还是从锅边开始冒——这三件事的比例稍微调一调视觉气质就完全不一样。1.2 为什么不用粒子特效或简单的Alpha裁剪有人会问做个溶解扩散是不是用粒子特效拼一下更快答案是应急可以但粒子和贴图配合会产生严重的“两张皮”问题。粒子只能模拟碎屑但贴图本身没有变化玩家一眼就看穿BOSS站在原地只是多了些飞灰。而Shader方案直接对图片本身做逐像素裁剪图片的轮廓是实时破开的碎屑表现可以和shader边缘颜色、燃烧色配合视觉上是一体的。另一个常见做法是给Sprite写一个简单Shader用clip()直接根据主纹理某个通道裁剪像素再加一个进度条。这个版本比粒子强但因为它没有引入距离因子溶解是从整张图所有地方随机同时发生的看起来更像是“图片在风化剥落”而不是“从某个点向外扩散”。如果关卡设计师想表达的是“力量从角色胸口向外爆发”这种无方向溶解就不对了。所以最终我还是选择了“噪声采样 距离修正 阈值裁剪”的组合方式。1.3 关键技术选型噪声纹理的获取方式解决“噪声从哪来”是我实现的第一个环节。最省事的方式是Photoshop里用“滤镜—渲染—云彩”生成一张灰度图存成JPG或者PNG。注意一定要用灰度图R、G、B三个通道的值保持一致或者只采样R通道都行彩色的噪声图可能会导致某一个通道的分布不均匀。但项目里的美术资源常常不方便临时导出另一位组员的做法是在C#里运行时生成噪声纹理。生成一次之后缓存到内存或者保存成Asset效果非常稳定还不需要依赖外部图片。下面这段代码就是运行时生成Perlin噪声纹理的老写法参考价值很大Texture2D GenerateNoiseTexture(int width, int height) { Texture2D tex new Texture2D(width, height, TextureFormat.RGBA32, false); var pixels new Color[width * height]; float scale 0.1f; for (int y 0; y height; y) { for (int x 0; x width; x) { float noise Mathf.PerlinNoise(x * scale, y * scale); pixels[y * width x] new Color(noise, noise, noise, 1f); } } tex.SetPixels(pixels); tex.Apply(); tex.wrapMode TextureWrapMode.Repeat; tex.filterMode FilterMode.Bilinear; return tex; }这段代码虽然只是生成一张常规Perlin噪声图但很够用。如果希望溶解的碎块更有“像素感”可以把FilterMode改成Point再配合把图片的MeshType调成FullRect来用。噪声纹理的WrapMode一定要Repeat否则UV超过1的时候会拉出透明边缘。2. Shader核心实现与逐段讲解2.1 搭建2D Sprite Shader的基础框架在Unity里给Sprite做自定义Shader最稳的起点是参照内置的Sprites/Default。它有透明的渲染队列、Cull Off、ZWrite Off并且使用Blend SrcAlpha OneMinusSrcAlpha。这些状态标签对2D精灵来说缺一不可透明队列保证半透明图片和UI能正常合批Cull Off保证Sprite翻转时不会出现面片被剔除的诡异情况ZWrite Off则是避免半透明物体互相遮挡时产生深度排序错误。基础框架的Properties需要包含主纹理_MainTex、噪声纹理_NoiseTex、进度值_Progress、扩散中心_CenterX/_CenterY、边缘宽度_EdgeWidth、边缘颜色_EdgeColor。每个属性声明我都会加_HDR关键字——边缘颜色用HDR可以让后续配合Bloom后处理产生灼烧感很强的发光边。Shader的头部标签建议写成Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue }IgnoreProjector很少有人写但2D精灵经常被场景里的投影器或者阴影系统干扰加上它之后能避免屏幕上出现不明多边形。2.2 核心溶解公式距离因子与噪声叠加顶点着色器不需要做什么复杂变换常规做法是UnityObjectToClipPos加TRANSFORM_TEX。真正的关键在片元着色器中。第一步先求当前像素到扩散中心的UV距离float2 center float2(_CenterX, _CenterY); float dist distance(i.uv, center); float spreadFactor saturate(dist * _SpreadStrength);这里的_SpreadStrength是扩散强度的缩放系数。数值越大从中心到边缘的距离梯度被拉得越陡溶解的扩散感越强数值过小整个溶解过程会更接近整体随机风化。经过实测_SpreadStrength取值在2到5之间时视觉效果最自然中心的小孔会很快形成然后像波纹一样推向四周。第二步采样噪声纹理并叠加距离因子float noise tex2D(_NoiseTex, i.uv * _NoiseScale).r; float dissolveValue noise * 0.7 spreadFactor * 0.3;这两行代码是整个溶解效果的核心公式。noise提供了随机破碎感spreadFactor保证中心先碎、边缘后碎。两个数值的混合比例可以按项目调想让碎块更多就把noise系数调大想让扩散的“泡泡”更大更圆就把spreadFactor系数调大。我一般倾向于噪声占六到七成边缘扩散感占三到四成。2.3 用clip还是用alpha裁剪以及为什么看到这一步最大的分歧在于“怎么让被溶解的像素消失”。两种方案我都试过先分清楚再选。方案一是直接调用clip(dissolveValue - _Progress)。当dissolveValue - _Progress 0该片元被移出渲染管线。这一步执行的效率很高但有一个副作用像素是硬切走的边缘如果有半透明像素就会有一点锯齿感。这是绝大多数本地溶解Shader的做法。方案二是不用clip而是把最终透明度乘上一个阈值判断的结果float alpha smoothstep(_Progress, _Progress _EdgeWidth, dissolveValue); col.a * alpha;这种做法边缘过渡更柔和在和场景里的粒子效果叠加时更自然。缺点是即使alpha为0的像素仍然要执行后续的颜色混合计算在大量Sprite同屏时GPU开销略高于Clip。但对2D游戏来说这种差距可以忽略。我现在的最终版是两套结合用clip把无用的像素从渲染管线里移出去省填率同时用smoothstep控制边缘的渐变宽度解决clip的锯齿感。2.4 边缘光与燃烧色给溶解增加“灼烧感”纯粹的溶解像是贴图被橡皮擦擦掉缺少反馈。真正的Visual Feedback要有一圈高亮的边缘颜色让玩家觉得“这里正在被消耗”。边缘区域可以通过smoothstep定位。保留下来的像素中dissolveValue越接近_Progress越靠近溶解边缘。于是float edge 1.0 - smoothstep(_Progress, _Progress _EdgeWidth, dissolveValue);这段代码里edge在溶解边界处接近1在完整保留区域接近0。然后我们把边缘颜色混合进主纹理颜色fixed3 burnColor lerp(col.rgb, _EdgeColor.rgb, edge * _BurnIntensity); col.rgb burnColor;_BurnIntensity控制边缘色的渗透强度。如果设为0.6左右边缘会出现一圈明显的暖色过渡如果设为1溶解交界处的原图几乎会被完全替换成边缘色视觉上很像金属被烧红的状态。边缘宽度建议控制在0.03到0.08之间太宽了像描边太窄了看不到。3. 完整Shader代码与配套控制脚本3.1 可直接复制的完整Shader下面直接给出我整理过的完整版本整体没用什么高级语法兼容性很好。把这段代码存成2DDissolveSpread.shader赋给SpriteRenderer的Material即可看到完整效果Shader Custom/2DDissolveSpread { Properties { [PerRendererData] _MainTex (Sprite Texture, 2D) white {} _NoiseTex (Dissolve Noise, 2D) white {} _Progress (Dissolve Progress, Range(0, 1)) 0 _NoiseScale (Noise Scale, Float) 1 _CenterX (Spread Center X, Range(0, 1)) 0.5 _CenterY (Spread Center Y, Range(0, 1)) 0.5 _SpreadStrength (Spread Strength, Range(0.1, 10)) 3 [HDR] _EdgeColor (Edge Color, Color) (1, 0.4, 0, 1) _EdgeWidth (Edge Width, Range(0.001, 0.3)) 0.05 _BurnIntensity (Burn Intensity, Range(0, 1)) 0.6 } SubShader { Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue } Blend SrcAlpha OneMinusSrcAlpha Cull Off ZWrite Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; fixed4 color : COLOR; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; fixed4 color : COLOR; }; sampler2D _MainTex; sampler2D _NoiseTex; float4 _MainTex_ST; fixed4 _MainColor; fixed4 _EdgeColor; float _Progress; float _NoiseScale; float _CenterX; float _CenterY; float _SpreadStrength; float _EdgeWidth; float _BurnIntensity; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); o.color v.color; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv) * i.color; if (col.a 0.01) discard; float2 center float2(_CenterX, _CenterY); float dist distance(i.uv, center); float spreadFactor saturate(dist * _SpreadStrength); float noise tex2D(_NoiseTex, i.uv * _NoiseScale).r; float dissolveValue noise * 0.7 spreadFactor * 0.3; // 硬裁剪移出无用像素 clip(dissolveValue - _Progress); // 边界渐变宽度 float edge 1.0 - smoothstep(_Progress, _Progress _EdgeWidth, dissolveValue); // 混合燃烧色 fixed3 burnColor lerp(col.rgb, _EdgeColor.rgb, edge * _BurnIntensity); col.rgb burnColor; // 平滑半透明边缘 col.a * 1.0 - edge * _EdgeWidth * 10.0; return col; } ENDCG } } }使用的时候有两点建议第一给SpriteRenderer挂上这个材质之后不要直接改sharedMaterial因为多个Sprite共用同一个材质会造成属性互相干扰。正确做法是用material属性或者MaterialPropertyBlock。第二如果场景里存在多个使用同一种Shader但进度不同的Sprite可以在C#中给每个对象单独设置材质也可以全部用MaterialPropertyBlock第二种方案更省材质实例但从SetFloat到Shader生效会有一个极小的内存分配需要避免在Update里高频调用。我通常的做法是只在关键帧更新progress波动不追求每帧都平滑变化。3.2 C#脚本控制溶解进度和中心点拿到Shader之后还需要一个C#脚本控制进度以及动态设置扩散中心。扩散中心并不是写死的一张图片的UV(0.5, 0.5)那样所有角色都是从肚脐冒烟。实际需求可能是点击怪物身上某个位置溶解从那里开始或者角色被击中时溶解从枪口方向开始蔓延。这就需要把鼠标点击的世界坐标转换成Sprite的UV坐标。算法的核心是下面这段Vector3 worldPos Camera.main.ScreenToWorldPoint(Input.mousePosition); Vector3 localPos transform.InverseTransformPoint(worldPos); SpriteRenderer sr GetComponentSpriteRenderer(); Sprite sp sr.sprite; if (sp null) return; Rect rect sp.textureRect; // 将局部坐标映射到该Sprite图集内的UV从而得到0~1的UV坐标 float u (localPos.x - sp.bounds.min.x) / sp.bounds.size.x; float v (localPos.y - sp.bounds.min.y) / sp.bounds.size.y; material.SetFloat(_CenterX, u); material.SetFloat(_CenterY, v);这里面有个必须注意的坑如果Sprite是图片图集中的一块通常美术会打图集sp.bounds并不是整张纹理的边界而是这个Sprite在图片里的局部边界这个映射依然成立因为UV本来就是在Sprite的纹理图集坐标系下算的。但不要直接访问sp.uv去求比例不同Unity版本的图集处理细节会变直接通过bounds的min和max求比例最稳。完整的控制脚本如下它同时支持外部传入中心坐标和自动播放扩散动画using UnityEngine; using System.Collections; [RequireComponent(typeof(SpriteRenderer))] public class DissolveEffect : MonoBehaviour { [SerializeField] private float duration 2f; [SerializeField] private bool playOnStart true; private SpriteRenderer spriteRenderer; private Material mat; private void Awake() { spriteRenderer GetComponentSpriteRenderer(); mat spriteRenderer.material; } private void Start() { if (playOnStart) { PlayFromCenter(); } } public void PlayFromCenter() { StopAllCoroutines(); StartCoroutine(PlayDissolve(new Vector2(0.5f, 0.5f))); } public void PlayFromMouse() { Vector3 mouseWorld Camera.main.ScreenToWorldPoint(Input.mousePosition); Vector2 uv WorldToSpriteUV(mouseWorld); StopAllCoroutines(); StartCoroutine(PlayDissolve(uv)); } private IEnumerator PlayDissolve(Vector2 uv) { mat.SetFloat(_CenterX, uv.x); mat.SetFloat(_CenterY, uv.y); mat.SetFloat(_Progress, 0f); float timer 0f; while (timer duration) { timer Time.deltaTime; mat.SetFloat(_Progress, Mathf.Clamp01(timer / duration)); yield return null; } } private Vector2 WorldToSpriteUV(Vector3 worldPos) { Vector3 localPos transform.InverseTransformPoint(worldPos); Sprite sp spriteRenderer.sprite; if (sp null) return new Vector2(0.5f, 0.5f); float u Mathf.InverseLerp(sp.bounds.min.x, sp.bounds.max.x, localPos.x); float v Mathf.InverseLerp(sp.bounds.min.y, sp.bounds.max.y, localPos.y); return new Vector2(u, v); } }Mathf.InverseLerp本质就是把一个坐标值按区间映射到0~1比手算除法要稳健也更好读。这个脚本已经经过实际测试放在任意场景里挂到SpriteRenderer上就能看到效果。3.3 扩散形态的进阶玩法基础版本完成后可以根据项目需求扩展出几种不同表现。我在实际调的时候发现改动很小的参数就能得到完全不同的视觉效果这里列出三种常见的扩展方向。方向扩散如果不希望从一点向四周均匀扩散而是像墨汁滴进水里那样沿一个方向蔓延最简单方式是修改距离公式。把球形的distance换成X轴方向上的绝对差值float spreadFactor saturate(abs(i.uv.x - _CenterX) * _SpreadStrength);这样溶解表现为从左往右扫过像一扇逐渐打开的门。把abs(i.uv.y - _CenterY)换成Y轴版本就是从下往上溶解常用于“画面从下到上刷出/消失”的场景。环形波纹扩散如果你想要的效果是溶解边界呈环状推进用多个同心圆做过渡那可以额外引入一层正弦扰动float dist distance(i.uv, center); float wave sin(dist * 20 - _Progress * 6.28) * 0.05; float dissolveValue noise * 0.7 spreadFactor * 0.3 wave;加了一层波函数之后溶解边缘会像声波一样一格一格往外跳配合高性能的Bloom后处理观感提升很大。但这个变体对移动端性能不太友好因为sin会对所有像素计算同屏大量精灵时建议只在关键角色身上使用。恢复效果把进度的方向反过来。正常溶解是从0到1图片从完整到消失恢复效果则是从1到0图片从消失状态一点一点长回来。C#脚本那边只需要把_Progress用反向差值驱动即可。当然Shader里的copy逻辑要保留因为消失后的像素组不会自动回填。实际做角色“复活”演出时这个效果很常用看起来像是能量重新凝聚成实体。3.4 噪声纹理的细节与参数推荐噪声纹理的采样强度和频率直接决定了溶解碎块是“马赛克式”还是“棉絮式”。我测试过几个典型配置整理成表格方便大家参考。预期风格NoiseScaleSpreadStrengthEdgeWidthBurnIntensity细腻粒子感1.0~2.02.0~3.00.030.5大块破碎感0.4~0.71.5~2.50.080.8平滑水墨感2.5~4.03.0~5.00.120.3霓虹光晕感1.5~2.02.5~3.50.11.0参数不要迷信固定值放在具体的项目里要看图片本身的分辨率。高分辨率大图需要较低的NoiseScale才能看到完整的大块破碎感低分辨率小图需要较高的NoiseScale才能看到细小的颗粒感。一个快速的调参口诀是让溶解边缘的“最小碎块尺寸”大致等于你期望的像素阈值。例如希望碎块最小为8x8像素那么NoiseScale要保证噪声纹理的一个纹素覆盖大约8x8个屏幕像素。具体推导过程依赖屏幕DPI通常靠肉眼调整。4. 使用过程中常见的问题与排查记录4.1 图片边缘出现半透明雾边或白边这个问题出现频率最高尤其是在美术从PNG导入Sprite后直接挂Shader时。原因多是图片本身是剪影或者带透明通道在边缘处原本就存在一圈预乘Alpha残留溶解时半透明像素的边缘色被放大形成白边或黑边。解决思路一般分两步走。第一步在Shader入口处做一个预过滤把alpha值小于0.05的边缘像素直接通过discard丢弃这样它们在溶解前就已经不参与计算。第二步是把主纹理的压缩格式改一下。不要用默认的无压缩RGBA尤其在移动端用ASTC或ETC2格式时半透明边缘很常见检查美术原始素材在导入面板里的“Alpha Is Transparency”选项勾选之后边缘的残留会被Unity Alpha处理流程自动清理。4.2 性能问题同屏大量溶解精灵时帧率骤降Shader本身的复杂度很低主要开销其实是噪声纹理采样和Clip带来的分支判断。但如果同一屏有几十个Sprite同时在溶解每个都各自采样一张噪声图、执行一次clip、跑一次smoothstep性能就会开始吃紧。我的优化顺序是这样首先把噪声图的尺寸控制到256x256。溶解效果属于高频噪声256和1024在视觉上几乎看不出差距但采样的缓存命中率差异巨大。其次Shader里不要用太多次的tex2D采样很多人喜欢在噪声图里采样RGB三通道做三张噪声混合视觉确实更好但性能也会变成三倍消耗。常规情况下可以只采样R通道一次再用一个数学扰动函数在片元内部生成第二层变化float noiseA tex2D(_NoiseTex, i.uv * _NoiseScale).r; float noiseB frac(sin(dot(i.uv * _NoiseScale, float2(12.9898, 78.233))) * 43758.5453); float dissolveValue noiseA * 0.6 noiseB * 0.1 spreadFactor * 0.3;noiseB这行本质是伪随机Hash能带来一种类似高噪点的细节性能代价小到忽略不计。但是注意它有一个劣势没有空间连续性变化非常剧烈如果用它做主导会导致溶解边缘太碎。只让它占10%左右刚好能丰富细节又不破掉主结构。另一条优化路径是开启GPU Instancing。2D Sprite的合批本来就比较依赖图集如果同一张图集里的多个Sprite同时做溶解Unity有希望通过Instance属性来自动合批。Shader里需要加入#pragma instancing_options并关掉[PerRendererData]中受批处理影响的属性。不过SpriteRenderer本身的合批方式不完全等同于MeshRenderer实际合批情况要结合Frame Debugger确认我在这里提醒一下不要盲信合批结果。4.3 常见问题速查表问题现象可能原因排查与解决溶解纹路太均匀看不出扩散方向spreadFactor权重太小提高SpreadStrength到3-5降低噪声混合权重到0.5边缘颜色太强像描边而不是烧灼BurnIntensity过高降到0.3~0.5并把EdgeWidth提高进度到0.7之后溶解速度突然变慢中心区域已经消失但噪声图高值区域过多调节NoiseScale或换一张更“双峰分布”的噪声图Start方法里调SetFloat没生效修改了sharedMaterial导致其他对象受影响改为通过material或MaterialPropertyBlock修改URP项目里显示紫色内置渲染管线的Shader在URP下不兼容改用URP版本Shader或使用Shader Graph建立等效效果图片有透明区域但溶解时透明区域也产生色块主纹理Sampler过滤了透明区域在片元开头增加clip(col.a - 0.01)或者关闭Generate MipMaps4.4 URP项目迁移要点Unity近几年的新项目很多默认就是URP。内置管线的Shader直接拖过去会显示紫色甚至直接被吞掉原因就是URP的渲染管线和内置管线的Shader变体导入流程不兼容。迁移方案有两个一是把上面整套逻辑放进Shader Graph重新连线溶解公式的关键节点就是“Noise Sample Add SmoothStep Lerp Clip”。这种方法对美术友好调参直观但它生成的代码量和性能并不完全透明。二是把内置CGPROGRAM改写成HLSLPROGRAM引用URP的Common.hlsl再把UnityObjectToClipPos换成TransformObjectToHClip。我通常建议团队里既懂Shader又懂动效的程序选二这样可以保留代码可维护性且URP中的SRP Batcher对同一个Shader在属性一致时合批效率更高。URP下还有一个容易踩的坑URP会默认开启SRP Batcher但要求所有材质属性都声明在CBUFFER中。如果你的溶解中心坐标或者进度设置了PerRendererData却没有放到CBUFFERSRP Batcher会悄悄禁用掉这些属性的合批。很多项目性能莫名下降查到最后都是这个原因。所以URP版本要确保把自定义属性统一包进CBUFFER_START(UnityPerMaterial)到CBUFFER_END区间中。5. 我的个人调参心得与最后提醒这个溶解扩散效果我陆陆续续调了三个版本才稳定下来。第一版没有扩散因子只有噪声和clip结果画面毫无方向性像成千上万只虫子在啃同一块饼干。第二版加入了均匀的距离计算但把扩散中心和边缘宽度写死导致换了几张Sprite之后效果完全失衡。第三版就是把参数全部放开并且用C#脚本去驱动终于达到了“换图也好用”的状态。如果让我只分享一条经验那就是溶解效果不要追求一步到位先把最基础的噪声溶解在编辑器里调到好看再叠加扩散中心最后才考虑边缘灼烧和Bloom。顺序反了你会被一堆变量同时干扰根本分不清问题出在噪声图还是扩散强度上。另外Shader参数名和C#的SetFloat字符串必须逐字对齐_Progress少一个下划线就会让你多排查半小时这种低级错误建议通过常量字符串或者MaterialPropertyBlock封装起来避免手工复制。最后再补充一个实用小技巧如果项目里图片的透明区域不重要但经常出现“溶解外部区域还在渲染”的情况可以在贴图导入设置里把“Alpha Is Transparency”打开并且把RGBA压缩格式改为ETC2而非ASTC这样透明边缘的质量和性能会更平衡。这个小改动在手机上尤其明显亲测有效。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑