Godot 4 如何製作帶發光燒蝕邊緣的溶解 Shader,並為像素畫加入傳送效果
How to make a dissolve shader in Godot 4 (glowing burn edge + pixel-art teleport)
文章示範如何在 Godot 4 製作 2D 溶解 Shader,以噪聲和閾值讓精靈逐步消失,並在溶解邊緣加入發光效果。另提供適合像素畫的版本,讓邊緣保持清晰,並配合較低幀率的動畫呈現傳送效果。
A dissolve is one of the cheapest ways to make a 2D game feel finished: enemies burn away on death, items fade out on pickup, a character teleports out. It looks fancy, but the whole thing is about 15 lines of fragment shader: a noise value, a threshold, and a thin band where you paint a glowing edge.
Here's how it works in Godot 4, with the actual shader I ship, plus a second version that stays crisp on pixel art.
1. The idea: noise vs. a threshold
Give every pixel a noise value between 0 and 1. Pick a threshold, amt. Any pixel whose noise is below the threshold disappears. Raise the threshold from 0 to 1 and the sprite eats itself away in organic blobs instead of fading uniformly.
That's it. Everything else is making the edge look hot.
2. Noise without a texture
You can use a NoiseTexture2D, but I prefer procedural noise in a shader include. Nothing to import, nothing to forget to assign, and it works at any resolution. This is a standard value noise plus fBm (several octaves added together):
// vfx_noise.gdshaderinc
float vfx_hash(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float vfx_noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f); // smoothstep curve between cells
return mix(mix(vfx_hash(i), vfx_hash(i + vec2(1.0, 0.0)), u.x),
mix(vfx_hash(i + vec2(0.0, 1.0)), vfx_hash(i + vec2(1.0, 1.0)), u.x), u.y);
}
float vfx_fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
for (int k = 0; k < 5; k++) {
v += a * vfx_noise(p);
p = p * 2.03 + vec2(1.7, 9.2); // shift each octave so they don't line up
a *= 0.5;
}
return v;
}
Single-octave noise gives you round, boring blobs. Five octaves give you the ragged, burnt-paper edge. Including it in a shader is one line: #include "res://addons/stylized_vfx/shaders/vfx_noise.gdshaderinc".
3. The dissolve shader
shader_type canvas_item;
#include "res://addons/stylized_vfx/shaders/vfx_noise.gdshaderinc"
// Put this material on any Sprite2D / TextureRect / Control.
uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.4;
uniform bool auto_animate = true;
uniform float anim_speed = 0.3;
uniform float noise_scale = 6.0;
uniform float edge_width : hint_range(0.0, 0.2) = 0.06;
uniform vec4 edge_color : source_color = vec4(1.0, 0.55, 0.1, 1.0);
uniform vec4 edge_color_hot : source_color = vec4(1.0, 0.95, 0.6, 1.0);
uniform float edge_glow : hint_range(0.0, 5.0) = 2.0;
void fragment() {
vec4 tex = COLOR; // COLOR already holds texture * modulate
float amt = auto_animate ? (0.5 - 0.5 * cos(TIME * anim_speed * TAU)) * 1.08 - 0.04 : dissolve_amount;
float n = vfx_fbm(UV * noise_scale);
float d = n - mix(0.12, 0.88, amt);
float keep = step(0.0, d);
float e = 1.0 - smoothstep(0.0, edge_width, d);
vec3 ec = mix(edge_color.rgb, edge_color_hot.rgb, smoothstep(0.5, 1.0, 1.0 - e));
vec3 c = mix(tex.rgb, ec * edge_glow, e * keep * step(0.001, amt));
COLOR = vec4(c, tex.a * keep);
}
Line by line, the parts that matter:
-
d = n - mix(0.12, 0.88, amt): this is the "distance" from the burn front. Negative means burnt, positive means still there. Themix(0.12, 0.88, ...)remap is the first gotcha: fBm values bunch up around 0.5 and almost never reach 0 or 1. If you threshold against rawamt, the first and last 10% of the slider do nothing. Remapping makes the whole 0 to 1 range useful. -
keep = step(0.0, d): a hard cut. Multiplying alpha by it removes burnt pixels. -
e = 1.0 - smoothstep(0.0, edge_width, d): 1 right at the burn front, fading to 0 a little way into the surviving pixels. That's the glowing band. -
Two edge colours:
ecblends from orange to near-white as you get closer to the front, so the very edge looks hottest, the way real embers do. -
ec * edge_glow: multiplying past 1.0 makes the edge bloom if you have glow enabled in yourWorldEnvironment(or just look over-bright if you don't, which still reads well). -
step(0.001, amt): without this, a sprite atamt = 0still shows a faint edge wherever the noise happens to be low. This hides the edge until the dissolve actually starts.
Swap edge_color to cyan and edge_color_hot to white and the same shader becomes a magic or sci-fi dissolve. Lower noise_scale for bigger chunks, raise it for fine ash.
4. Driving it from gameplay
auto_animate uses a cosine ping-pong so the effect loops in the editor, which is handy for tuning. In a game you want it off, and you tween dissolve_amount yourself, for example on an enemy's death:
func die() -> void:
var mat := $Sprite2D.material as ShaderMaterial
mat.set_shader_parameter("auto_animate", false)
var tw := create_tween()
tw.tween_property(mat, "shader_parameter/dissolve_amount", 1.0, 0.6).from(0.0)
tw.tween_callback(queue_free)
Gotcha #2: shared materials. If ten enemies use the same ShaderMaterial, killing one dissolves all ten. Tick Local to Scene on the material (resource_local_to_scene = true in the .tscn) so every instance gets its own copy.
5. The pixel-art version
Run the shader above on a 32x32 sprite scaled up 3x and it looks wrong: the burn edge is smooth and sub-pixel while the sprite is chunky, and the motion is buttery while the rest of your game animates at 8 to 12 fps. For pixel art you need to fix both:
shader_type canvas_item;
#include "res://addons/pixel_vfx/shaders/px_common.gdshaderinc"
// Pixel dissolve / teleport for any sprite. Works per texel, so it stays crisp.
uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.4;
uniform bool auto_animate = true;
uniform float anim_speed = 0.35;
uniform float fps = 12.0;
uniform float noise_scale = 0.45;
uniform bool teleport_mode = true; // dissolve top-down with a beam
uniform float edge_width : hint_range(0.0, 0.3) = 0.12;
uniform vec4 edge_color_1 : source_color = vec4(0.4, 0.9, 1.0, 1.0);
uniform vec4 edge_color_2 : source_color = vec4(1.0, 1.0, 1.0, 1.0);
uniform vec4 beam_color : source_color = vec4(0.4, 0.9, 1.0, 0.6);
void fragment() {
vec4 tex = COLOR;
vec2 cell = floor(UV / TEXTURE_PIXEL_SIZE); // which texel are we in?
float t = px_step_time(TIME, fps); // floor(TIME * fps) / fps
float amt = auto_animate ? (0.5 - 0.5 * cos(t * anim_speed * TAU)) * 1.2 - 0.1 : dissolve_amount;
float n = px_noise(cell * noise_scale) * 0.7 + px_hash(cell) * 0.3;
if (teleport_mode) n = mix(n, UV.y, 0.65);
float d = n - amt;
vec4 outc = tex;
if (d < 0.0) {
outc.a = 0.0;
if (teleport_mode && amt > 0.05 && amt < 0.95 && px_hash(vec2(cell.x, floor(t * 8.0))) > 0.55) outc = beam_color;
} else if (d < edge_width && amt > 0.0 && tex.a > 0.5) {
outc.rgb = d < edge_width * 0.5 ? edge_color_2.rgb : edge_color_1.rgb;
}
COLOR = outc;
}
What changed and why:
-
cell = floor(UV / TEXTURE_PIXEL_SIZE):TEXTURE_PIXEL_SIZEis1 / texture size, so this gives you integer texel coordinates. Sampling noise per cell instead of per screen pixel means every texel is either fully there or fully gone. No half-pixels, ever. -
px_step_time: quantizes time to 12 fps so the dissolve "ticks" like a hand-animated sprite. -
noise * 0.7 + hash * 0.3: smooth noise alone gives clumps; mixing in 30% per-texel random gives that crumbly, sparkly pixel break-up. - No smoothstep, two flat colours: the edge is two hard bands (white nearest the front, cyan behind). Pixel art wants a palette, not a gradient.
-
Teleport mode:
mix(n, UV.y, 0.65)biases the noise with height, so the sprite dissolves top-down. Burnt columns randomly flashbeam_color(re-rolled 8 times a second per column), which sells the "beamed out" look. -
tex.a > 0.5: only paint edge colour on pixels that were opaque, so you don't draw a halo into the sprite's transparent border.
The slime on the far left is mid-teleport. Remember to set texture filtering to Nearest on the sprite (or in Project Settings > Rendering > Textures) or none of this stays crisp.
Wrap-up
Noise, a threshold, and a thin band above the threshold for the edge. Remap the threshold so the whole slider works, make the material local to scene, and for pixel art sample per texel and step time. Copy the snippets above and you have a dissolve for every sprite in your game.
If you'd rather drop in ready-made scenes, both shaders come from my packs:
- Godot 4 Stylized VFX Pack Vol.1 ($2, MIT): the glowing dissolve plus fire, smoke, lightning, electric arc, sword slash, hit spark, magic portal, shield, rain and snow, all shader/particle code with no textures. The dissolve, fire and hit spark are also in the free Stylized VFX Lite.
- Godot 4 Pixel Art VFX Pack Vol.1 ($2, MIT): the pixel teleport dissolve, hit flash, palette swap + outline, explosions, fire, lightning, heal auras and more.
- Everything, including the new PS1 Shader Kit, is in the Godot 4 Mega Bundle ($14.99).
Disclosure: I sell the packs linked above (no affiliate links). The code was written with an AI coding assistant and tested by hand in Godot 4.5.1 (Compatibility renderer). All images are real in-engine captures.
來源:DEV Community · dev.to


