在游戏开发中,UI特效是提升视觉表现力的重要手段。今天我要分享的是在Laya引擎中实现2D精灵溶解效果的Shader技术,这个效果特别适合用于角色消失、物品销毁、场景过渡等场景。不同于简单的淡入淡出,溶解效果通过程序化噪声控制消失过程,配合边缘发光可以创造出火焰燃烧、冰霜消融等丰富的视觉效果。
这个Dissolve2DShader是我在实际项目中多次优化后的成果,支持两种噪声源(内置算法和外部贴图),边缘颜色和宽度可自由调节,通过参数组合能实现至少12种不同的溶解风格。下面我会从原理到实践完整解析这个Shader的实现,包括你可能遇到的性能问题和解决方案。
这个Shader的核心控制参数可以分为三大类:
溶解控制参数:
u_DissolveAmount (0-1):控制溶解进度,0表示完全显示,1表示完全消失。这个参数是动画驱动的关键,通常配合Tween制作消失/出现动画u_DissolveEdgeRange (建议0.05-0.3):边缘宽度,值越大发光区域越宽。注意这个值会影响边缘颜色的可见范围u_DissolveEdgeSoft (0-1):边缘柔和度,0产生锐利边缘,1则完全平滑过渡边缘颜色参数:
u_DissolveEdgeColor (Vector4):使用RGBA格式定义边缘发光色。alpha通道控制透明度,合理设置可以创建半透明光晕效果噪声源参数:
u_DissolveUseNoiseTex (0/1):切换噪声源,0使用内置算法,1使用外部贴图u_DissolveNoiseTexture:噪声贴图资源,需要是灰度图u_DissolveNoiseTiling (Vector2):控制噪声贴图的平铺次数,值越大噪声图案越小通过不同参数组合可以实现多种风格的效果:
火焰效果:
typescript复制mat.shaderData.setVector(colorID, new Laya.Vector4(1, 0.3, 0, 0.8)); // 橙红色带透明度
mat.shaderData.setNumber(rangeID, 0.25); // 较宽边缘模拟火焰扩散
mat.shaderData.setNumber(softID, 0.7); // 柔和边缘模拟火焰模糊
电子消散:
typescript复制mat.shaderData.setVector(colorID, new Laya.Vector4(0, 0.8, 1, 1)); // 亮蓝色
mat.shaderData.setNumber(rangeID, 0.1); // 窄边缘模拟电流
mat.shaderData.setNumber(softID, 0.2); // 硬边缘增强科技感
腐蚀效果:
typescript复制mat.shaderData.setVector(colorID, new Laya.Vector4(0.3, 0.8, 0.2, 1)); // 病态绿色
mat.shaderData.setNumber(rangeID, 0.15);
mat.shaderData.setNumber(softID, 0.4);
mat.shaderData.setNumber(useNoiseID, 1); // 必须使用噪声贴图
IDE拖拽方式:
纯代码方式:
typescript复制const mat = new Laya.Material();
mat.setShaderName("Dissolve2DShader");
// 获取参数ID(性能优化关键)
const amountID = Laya.Shader3D.propertyNameToID("u_DissolveAmount");
const colorID = Laya.Shader3D.propertyNameToID("u_DissolveEdgeColor");
// 设置初始值
mat.shaderData.setNumber(amountID, 0);
mat.shaderData.setVector(colorID, new Laya.Vector4(1,0,0,1));
// 应用到UI组件
image.material = mat;
关键技巧:Shader3D.propertyNameToID()获取参数ID比直接传字符串效率更高,特别是在频繁更新的场景中。
基础消失动画:
typescript复制Laya.Tween.to(mat.shaderData,
{getProperty:()=>mat.shaderData.getNumber(amountID),
setProperty:(v)=>mat.shaderData.setNumber(amountID, v)},
{to:1, duration:1500, ease:Laya.Ease.quadIn}
);
脉冲效果:
typescript复制let pulseSpeed = 0.03;
Laya.timer.frameLoop(1, this, ()=>{
const current = mat.shaderData.getNumber(amountID);
const newValue = current + (current>0.7 ? -pulseSpeed : pulseSpeed);
mat.shaderData.setNumber(amountID, Math.min(Math.max(newValue, 0), 0.7));
});
序列动画:
typescript复制// 先变红再溶解
Laya.Tween.to(edgeColor, {x:1,y:0,z:0}, 500)
.to(mat.shaderData, {getProperty:()=>..., setProperty:...},
{to:1, duration:1000}, 500);
Shader内置了三种噪声算法:
Hash噪声:
glsl复制float hash(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
这是最基础的伪随机数生成器,通过坐标变换和点积运算产生混沌效果。
Value噪声:
glsl复制float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f*f*(3.0-2.0*f); // smoothstep
float a = hash(i);
float b = hash(i + vec2(1,0));
//...其他角采样
return mix(mix(a,b,f.x), mix(c,d,f.x), f.y);
}
通过网格点采样+双线性插值产生连续噪声,smoothstep使过渡更自然。
FBM噪声:
glsl复制float fbm(vec2 p) {
float v=0., a=0.5;
for(int i=0; i<4; i++) {
v += a * noise(p);
p *= 2.;
a *= 0.5;
}
return v;
}
多层噪声叠加产生自然纹理,是内置算法的默认噪声源。
核心逻辑分三步:
glsl复制float n = u_UseNoiseTex>0.5 ?
texture2D(u_NoiseTex, uv*u_Tiling).r :
fbm(uv*3.0);
glsl复制if(n < u_Amount) discard;
glsl复制float edge = smoothstep(u_Amount, u_Amount+u_Range, n);
edge = mix(edge, 1.0, u_Soft);
gl_FragColor = mix(u_EdgeColor, origColor, edge);
技术细节:smoothstep产生0-1的渐变,mix函数根据soft参数控制边缘硬度,最终颜色通过mix混合原色和边缘色。
在Redmi Note 10上的测试数据:
| 方案 | 平均帧率 | 内存占用 | 适用场景 |
|---|---|---|---|
| 内置FBM | 54fps | 低 | 简单效果 |
| 512x512噪声图 | 60fps | 2MB | 复杂图案 |
| 1024x1024噪声图 | 58fps | 8MB | 高清需求 |
| 多物体使用 | 45fps | - | 建议合批 |
glsl复制if(origColor.a < 0.01) discard;
原纹理的透明区域不会参与溶解计算,确保UI透明度正确传递。
precision mediump floattypescript复制// 全屏遮罩溶解转场
const mask = new Laya.Image();
mask.size(Laya.stage.width, Laya.stage.height);
mask.material = dissolveMat;
Laya.Tween.to(mask.material.shaderData, ..., {
to:1,
duration:800,
complete:()=>{ mask.destroy(); }
});
typescript复制// 火球术命中效果
const hit = new Laya.Animation();
hit.loadAnimation("hit.ani");
hit.pos(x,y);
hit.material = dissolveMat;
// 设置火焰参数
hit.material.shaderData.setVector(colorID, fireColor);
hit.material.shaderData.setNumber(amountID, 0);
// 播放动画同时溶解
hit.play();
Laya.Tween.to(hit.material.shaderData, ..., {to:1, duration:300});
typescript复制// 金币收集动画
coin.on(Laya.Event.CLICK, ()=>{
// 先放大再溶解
Laya.Tween.to(coin, {scaleX:1.2,scaleY:1.2}, 200)
.to(coin.material.shaderData, ..., {to:1, duration:400});
// 延迟销毁
Laya.timer.once(600, this, ()=>coin.destroy());
});
在实际项目中,这个Shader已经应用于角色死亡、宝箱开启、场景切换等20+处效果。通过参数调节可以快速实现不同的美术风格,相比使用序列帧动画,内存占用降低了70%以上。特别是在需要动态控制溶解速度的场景,Shader方案提供了更大的灵活性。