在2D游戏开发中,图片溶解扩散效果是一种极具视觉冲击力的动态表现手法。当角色死亡、物品消失或场景转换时,物体表面呈现从局部到整体的像素级消散过程,如同被火焰灼烧或沙粒飘散。这种效果在独立游戏《Hollow Knight》的BOSS战、《Dead Cells》的道具交互中都有经典应用。
实现原理上,溶解效果的本质是对纹理像素进行阈值筛选。我们通过Shader中的片段着色器,将每个像素的灰度值与动态变化的溶解阈值进行比对。当像素灰度值低于当前阈值时,该像素被丢弃(呈现透明状态);高于阈值则保留。通过随时间推移调整阈值,就能实现从完整图像到完全消失的渐进过程。
扩散效果则是在溶解基础上增加了边缘特效。通常采用梯度纹理(Gradient Texture)或噪声纹理(Noise Texture)作为溶解过程的遮罩。当像素处于溶解边缘时,根据其与阈值的距离差值,叠加发光、变色或粒子效果。这种处理使得物体消失时不是简单的"擦除",而是带有能量逸散的动态观感。
在Unity ShaderLab语法中,我们首先声明可调节的公开属性:
shader复制Properties {
_MainTex ("Base Texture", 2D) = "white" {}
_NoiseTex ("Noise Texture", 2D) = "gray" {}
_DissolveThreshold ("Dissolve Threshold", Range(0,1)) = 0
_EdgeWidth ("Edge Width", Range(0,0.2)) = 0.05
_EdgeColor ("Edge Color", Color) = (1,0.8,0,1)
_EdgeBrightness ("Edge Brightness", Float) = 3
}
关键参数说明:
_NoiseTex:使用Perlin噪声图作为溶解遮罩,确保消散边缘的自然随机性_DissolveThreshold:0到1的动态阈值,控制溶解进度_EdgeWidth:边缘特效的宽度占比,影响光晕范围_EdgeColor:通常设置为橙黄色系模拟灼烧效果_EdgeBrightness:边缘发光强度系数顶点阶段主要完成基础坐标变换和UV传递:
hlsl复制v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.noiseUV = TRANSFORM_TEX(v.uv, _NoiseTex);
return o;
}
这里特别为噪声纹理设置了独立的UV坐标noiseUV,支持平铺(Tiling)和偏移(Offset)参数调节。实际项目中建议对主纹理和噪声纹理使用不同的UV缩放,避免效果重复感。
片段着色器的核心是阈值比较与透明度控制:
hlsl复制fixed4 frag (v2f i) : SV_Target {
// 采样主纹理和噪声纹理
fixed4 col = tex2D(_MainTex, i.uv);
fixed noise = tex2D(_NoiseTex, i.noiseUV).r;
// 基础溶解判断
clip(noise - _DissolveThreshold);
// 边缘检测
float edge = smoothstep(_DissolveThreshold, _DissolveThreshold + _EdgeWidth, noise);
fixed3 edgeColor = edge * _EdgeColor * _EdgeBrightness;
// 最终颜色合成
fixed4 finalColor = col;
finalColor.rgb += edgeColor;
return finalColor;
}
clip()函数是关键,当传入值为负时会直接丢弃该片段。通过将噪声值与阈值比较,实现像素级的显示控制。这里使用噪声图的R通道作为判断依据,因为单通道纹理在内存和采样效率上更有优势。
基础溶解的视觉表现较为生硬,我们通过边缘检测算法增加过渡效果:
hlsl复制float edgeFactor = saturate((noise - _DissolveThreshold) / _EdgeWidth);
fixed3 edgeEffect = _EdgeColor * (1 - edgeFactor) * _EdgeBrightness;
smoothstep函数创建了0到1的平滑过渡区间,参数分别为区间起点、终点和输入值。当噪声值处于阈值到阈值+宽度区间时,edge值从0渐变到1,形成自然的羽化边缘。
实际测试中发现,直接使用step函数会产生锯齿状边缘。改用smoothstep并配合适当的EdgeWidth(建议0.02-0.1之间)能获得更柔和的过渡。
在C#脚本中控制阈值变化实现自动溶解:
csharp复制[RequireComponent(typeof(SpriteRenderer))]
public class DissolveController : MonoBehaviour {
[SerializeField] float dissolveDuration = 2f;
[SerializeField] AnimationCurve dissolveCurve;
private Material material;
private float timer;
void Start() {
material = GetComponent<SpriteRenderer>().material;
timer = 0;
}
void Update() {
timer += Time.deltaTime;
float progress = Mathf.Clamp01(timer / dissolveDuration);
material.SetFloat("_DissolveThreshold", dissolveCurve.Evaluate(progress));
if (progress >= 1) {
Destroy(gameObject);
}
}
}
关键点:
为增强视觉效果,可在阈值达到0.5时触发粒子发射:
csharp复制[SerializeField] ParticleSystem dissolveParticles;
void Update() {
// ...阈值更新逻辑
if (!particlesEmitted && progress > 0.5f) {
var particles = Instantiate(dissolveParticles, transform.position, Quaternion.identity);
particles.Play();
particlesEmitted = true;
}
}
粒子系统应设置为:
不同噪声类型产生迥异的溶解风格:
建议使用16x16或32x32的小尺寸噪声图,通过平铺产生无重复图案。启用Bilinear滤波确保平滑过渡。
进阶实现可分离RGB通道进行独立溶解:
hlsl复制float dissolveR = tex2D(_NoiseTex, i.noiseUV * 1.2).r;
float dissolveG = tex2D(_NoiseTex, i.noiseUV * 0.8).g;
float dissolveB = tex2D(_NoiseTex, i.noiseUV * 1.5).b;
clip(dissolveR - _DissolveThreshold);
clip(dissolveG - _DissolveThreshold * 0.8);
clip(dissolveB - _DissolveThreshold * 1.2);
这种处理会产生色彩分离效果,适合表现电子设备故障或魔法解构等场景。
在溶解边缘添加顶点位移可创造"燃烧卷曲"效果:
hlsl复制#ifdef VERTEX_DISPLACEMENT
if (noise < _DissolveThreshold + _EdgeWidth * 2) {
float displaceFactor = 1 - saturate((noise - _DissolveThreshold) / (_EdgeWidth * 2));
v.vertex.xyz += v.normal * displaceFactor * _DisplaceAmount;
}
#endif
需在材质面板开启VERTEX_DISPLACEMENT关键字,并配合法线贴图使用效果更佳。
在2015款MacBook Pro上测试(分辨率1920x1080):
主要性能消耗来自:
确保所有使用该材质的对象:
shader复制#pragma multi_compile_instancing
实测显示,开启Instancing后,绘制100个溶解对象仅增加0.3ms开销。
针对Android/iOS设备建议:
hlsl复制precision mediump float;
hlsl复制float edge = step(_DissolveThreshold, noise) - step(_DissolveThreshold + _EdgeWidth, noise);
在Redmi Note 8 Pro上测试,优化后版本保持60FPS可渲染约50个同时溶解的对象。
典型实现流程:
csharp复制IEnumerator PlayDeathEffect() {
// 替换材质
originalMaterial = renderer.material;
renderer.material = dissolveMaterial;
// 设置初始参数
dissolveMaterial.SetTexture("_NoiseTex", deathNoiseTexture);
dissolveMaterial.SetFloat("_DissolveThreshold", 0);
// 动画循环
float timer = 0;
while (timer < dissolveDuration) {
timer += Time.deltaTime;
float progress = timer / dissolveDuration;
dissolveMaterial.SetFloat("_DissolveThreshold", progress);
if (progress > 0.7f) {
collider.enabled = false;
}
yield return null;
}
Destroy(gameObject);
}
全屏溶解转场实现要点:
shader复制v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.screenPos = ComputeScreenPos(o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
float2 screenUV = i.screenPos.xy / i.screenPos.w;
fixed noise = tex2D(_NoiseTex, screenUV * _NoiseScale).r;
clip(noise - _GlobalThreshold);
// ...边缘处理
}
这种方案适合关卡切换、回忆闪回等情景,可通过调整NoiseScale控制溶解颗粒粗细。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 溶解边缘锯齿明显 | 噪声纹理分辨率过低 EdgeWidth设置太小 |
使用更高清噪声图 增大EdgeWidth至0.05以上 |
| 效果完全不显示 | 材质未正确赋值 Shader编译错误 |
检查材质Inspector面板 查看Console报错信息 |
| 移动端显示异常 | 精度问题 ES2.0不支持特性 |
添加precision声明 移除fwidth等高级函数 |
| 溶解方向不理想 | 噪声图类型不匹配 | 尝试旋转UV坐标 更换Voronoi噪声 |
| 性能突然下降 | 未启用批处理 过多动态对象 |
开启GPU Instancing 使用对象池管理 |
可视化噪声图:临时修改Shader输出噪声值而非最终颜色,检查分布是否均匀
hlsl复制return fixed4(noise, noise, noise, 1);
阈值调试模式:添加滑块控制实时调节
csharp复制[Range(0,1)] public float debugThreshold;
void OnValidate() { if (material) material.SetFloat("_DissolveThreshold", debugThreshold); }
边缘宽度标记:用不同颜色显示边缘区域
hlsl复制if (noise < _DissolveThreshold + _EdgeWidth) return fixed4(1,0,0,1);
性能分析:通过Frame Debugger查看每个Draw Call的耗时,重点关注片段着色器复杂度
将阈值从1动画到0,配合缩放效果可实现物体组装生成:
csharp复制material.SetFloat("_DissolveThreshold", 1 - progress);
transform.localScale = Vector3.one * progress;
添加遮罩纹理控制溶解区域:
hlsl复制float mask = tex2D(_MaskTex, i.uv).r;
clip(noise - _DissolveThreshold * mask);
根据碰撞点动态修改阈值:
csharp复制void OnCollisionEnter(Collision col) {
foreach (ContactPoint contact in col.contacts) {
material.SetVector("_DissolveCenter", contact.point);
material.SetFloat("_LocalDissolve", 1);
}
}
Shader中计算像素到碰撞点的距离,作为额外的溶解因素。