Sprite Dissolve Effect
by GameDev.net · GLSL ES 3.00 (WebGL2) · 25 Aug 2026
Run the shader to adjust these controls.
What it demonstrates
A dissolve is a threshold against a noise field, and an emissive rim drawn where the threshold is currently passing. That is the whole effect, and it is one of the most reusable things in a game: destruction, teleports, spawn-ins, cloaking and damage states are all the same two lines with different colours and timings.
Shader inputs
void mainImage(out vec4 fragColor, in vec2 fragCoord)
Called once per pixel. Write the colour to fragColor.
-
iResolutionvec3 - Viewport size in pixels (z is the pixel aspect ratio).
-
iTimefloat - Seconds since the shader started.
-
iTimeDeltafloat - Seconds since the previous frame.
-
iFrameRatefloat - Frames per second, smoothed.
-
iFrameint - Frames rendered since the start.
-
iMousevec4 - Mouse position: xy while held, zw of the last click.
-
iDatevec4 - Year, month, day, and seconds within the day.
-
iChannel0sampler2D - Texture bound to channel 0.
-
iChannel1sampler2D - Texture bound to channel 1.
-
iChannel2sampler2D - Texture bound to channel 2.
-
iChannel3sampler2D - Texture bound to channel 3.
-
iChannelResolutionvec3[4] - Pixel size of each bound channel texture.
-
iChannelTimefloat[4] - Playback time of each channel, in seconds.
-
iSampleRatefloat - Audio sample rate, always 44100.
Main Image5
uniform float uAmount; // @param 0.0..1.0 = 0.45 "Dissolve amount"
uniform float uEdgeWidth; // @param 0.02..0.25 = 0.10 "Edge width"
uniform vec3 uEdgeColor; // @param 0.0..1.0 = 1.0, 0.58, 0.18 color "Edge colour"
uniform float uNoiseScale; // @param 2.0..24.0 = 8.0 "Noise scale"
float hash21(vec2 cell) {
return fract(sin(dot(cell, vec2(12.9898, 78.233))) * 43758.5453);
}
float valueNoise(vec2 p) {
vec2 cell = floor(p);
vec2 local = fract(p);
vec2 weight = local * local * (3.0 - 2.0 * local);
float bottom = mix(hash21(cell), hash21(cell + vec2(1.0, 0.0)), weight.x);
float top = mix(hash21(cell + vec2(0.0, 1.0)), hash21(cell + vec2(1.0, 1.0)), weight.x);
return mix(bottom, top, weight.y);
}
float dissolveField(vec2 uv) {
return 0.65 * valueNoise(uv * uNoiseScale)
+ 0.35 * valueNoise(uv * uNoiseScale * 2.7 + 11.0);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y + 0.5;
vec4 sprite = texture(iChannel0, uv);
float field = dissolveField(uv);
float threshold = mix(-0.05, 1.05, uAmount);
float keep = smoothstep(threshold, threshold + uEdgeWidth, field);
float burn = keep - smoothstep(threshold + uEdgeWidth,
threshold + uEdgeWidth * 2.4, field);
float alpha = sprite.a * keep;
vec3 color = mix(sprite.rgb, uEdgeColor, clamp(burn, 0.0, 1.0));
color += uEdgeColor * burn * 1.4;
vec3 backdrop = mix(vec3(0.05, 0.06, 0.09), vec3(0.11, 0.13, 0.18), uv.y);
fragColor = vec4(mix(backdrop, color, alpha), 1.0);
}
Inputs for this pass
-
iChannel0
textures/sprite-demo-v1.png
- iChannel1 Empty
- iChannel2 Empty
- iChannel3 Empty
Learn from this shader
How it works
Noise is sampled per fragment and compared against a threshold driven by the dissolve amount. Fragments below the threshold are cut, and the comparison is a smoothstep rather than a step so the boundary is antialiased at any zoom. The burning edge is the difference between two thresholds slightly apart: one where a fragment only just survived, zero everywhere else, which is why the glow follows the dissolve front rather than the sprite outline. The cut multiplies the sprite's own alpha instead of replacing it, so holes in the artwork stay holes. The threshold range overshoots zero and one so both extremes are reachable.
Try changing
Sweep Dissolve amount from zero to one and watch the sprite go without the glow ever detaching from the front. Widen Edge width for a slow smoulder, narrow it for a sharp burn. Raise Noise scale for fine grain, lower it for large chunks. Set Edge colour cold for a teleport instead of a fire.
Using it in a game
Drive the amount from a timeline or a health value and this is production ready. Expect a sprite in channel zero with straight alpha; premultiplied art needs the multiply removed first. In a lit 3D pipeline the same field usually goes to alpha clipping, and the rim to an emissive output so bloom picks it up.
Explore the techniques
Continue with curated explanations and progressively related examples.
Discussion