Skip to main content
GameDev.net gamedev.net

Sprite Dissolve Effect

by GameDev.net · GLSL ES 3.00 (WebGL2) · 25 Aug 2026

Use in your engine

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.

Source Revision 1

Author notes are linked to specific lines.

Shader inputs

void mainImage(out vec4 fragColor, in vec2 fragCoord)

Called once per pixel. Write the colour to fragColor.

iResolution vec3
Viewport size in pixels (z is the pixel aspect ratio).
iTime float
Seconds since the shader started.
iTimeDelta float
Seconds since the previous frame.
iFrameRate float
Frames per second, smoothed.
iFrame int
Frames rendered since the start.
iMouse vec4
Mouse position: xy while held, zw of the last click.
iDate vec4
Year, month, day, and seconds within the day.
iChannel0 sampler2D
Texture bound to channel 0.
iChannel1 sampler2D
Texture bound to channel 1.
iChannel2 sampler2D
Texture bound to channel 2.
iChannel3 sampler2D
Texture bound to channel 3.
iChannelResolution vec3[4]
Pixel size of each bound channel texture.
iChannelTime float[4]
Playback time of each channel, in seconds.
iSampleRate float
Audio sample rate, always 44100.
Main Image5
1 uniform float uAmount; // @param 0.0..1.0 = 0.45 "Dissolve amount"
2 uniform float uEdgeWidth; // @param 0.02..0.25 = 0.10 "Edge width"
3 uniform vec3 uEdgeColor; // @param 0.0..1.0 = 1.0, 0.58, 0.18 color "Edge colour"
4 uniform float uNoiseScale; // @param 2.0..24.0 = 8.0 "Noise scale"
5
6 float hash21(vec2 cell) {
7 return fract(sin(dot(cell, vec2(12.9898, 78.233))) * 43758.5453);
8 }
9
10 float valueNoise(vec2 p) {
11 vec2 cell = floor(p);
12 vec2 local = fract(p);
13 vec2 weight = local * local * (3.0 - 2.0 * local);
14 float bottom = mix(hash21(cell), hash21(cell + vec2(1.0, 0.0)), weight.x);
15 float top = mix(hash21(cell + vec2(0.0, 1.0)), hash21(cell + vec2(1.0, 1.0)), weight.x);
16 return mix(bottom, top, weight.y);
17 }
18
19 float dissolveField(vec2 uv) {
20 return 0.65 * valueNoise(uv * uNoiseScale)
21 + 0.35 * valueNoise(uv * uNoiseScale * 2.7 + 11.0);
22 }
23
24 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
25 vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y + 0.5;
26 vec4 sprite = texture(iChannel0, uv);
27
28 float field = dissolveField(uv);
29 float threshold = mix(-0.05, 1.05, uAmount);
30 float keep = smoothstep(threshold, threshold + uEdgeWidth, field);
31 float burn = keep - smoothstep(threshold + uEdgeWidth,
32 threshold + uEdgeWidth * 2.4, field);
33
34 float alpha = sprite.a * keep;
35 vec3 color = mix(sprite.rgb, uEdgeColor, clamp(burn, 0.0, 1.0));
36 color += uEdgeColor * burn * 1.4;
37
38 vec3 backdrop = mix(vec3(0.05, 0.06, 0.09), vec3(0.11, 0.13, 0.18), uv.y);
39 fragColor = vec4(mix(backdrop, color, alpha), 1.0);
40 }
41

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.

LicenseMIT
Views2
Forks1

Discussion

Loading comments...