Skip to main content
GameDev.net gamedev.net

Feedback Paint Trails

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

Use in your engine

Run the shader to adjust these controls.

A buffer reads itself, fades it, blurs it slightly, and stamps a brush on top. The result is a persistent paint trail that smears over time.

What it demonstrates

Buffer-to-buffer feedback, where Buffer A samples itself as iChannel0. A practical multi-pass pattern: simulation in Buffer A, display in Image. Mapping mouse input to a persistent canvas.

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.
Common
float hash(vec2 p) {
    return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}

float smin(float a, float b, float k) {
    float h = max(k - abs(a - b), 0.0) / k;
    return min(a, b) - h * h * k * 0.25;
}

float sdCircle(vec2 p, vec2 c, float r) {
    return length(p - c) - r;
}

// Stamp shape: a soft-edged circle with a tiny highlight offset.
vec4 paintStamp(vec2 p, vec2 c, float radius, vec3 color) {
    float d = length(p - c);
    float alpha = smoothstep(radius, 0.0, d);
    float highlight = smoothstep(radius * 0.35, 0.0, length(p - c - radius * 0.3));
    color += vec3(0.18) * highlight;
    return vec4(color, alpha);
}

// Read a 4-neighbor average from the buffer; this is the "feedback" smear.
vec4 sampleBlur(sampler2D buf, vec2 uv, vec2 res) {
    vec2 texel = 1.0 / res;
    vec4 c = texture(buf, uv, 0.0);
    c += texture(buf, uv + vec2(texel.x, 0.0), 0.0) * 0.25;
    c += texture(buf, uv - vec2(texel.x, 0.0), 0.0) * 0.25;
    c += texture(buf, uv + vec2(0.0, texel.y), 0.0) * 0.25;
    c += texture(buf, uv - vec2(0.0, texel.y), 0.0) * 0.25;
    return c * 0.5;
}
Buffer A3
1 uniform float uDecay; // @param 0.90..0.999 = 0.985 "Trail decay"
2 uniform float uRadius; // @param 4.0..48.0 = 16.0 "Brush radius"
3 uniform vec3 uColor; // @param 0.0..1.0 = 0.15, 0.75, 0.95 color "Brush color"
4
5 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
6 vec2 uv = fragCoord / iResolution.xy;
7 vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
8
9 vec4 prev = texture(iChannel0, uv, 0.0);
10
11 // Decay + small neighbor blur for the feedback trail.
12 vec4 blur = sampleBlur(iChannel0, uv, iResolution.xy);
13 vec4 trail = mix(blur, prev, 0.12) * uDecay;
14
15 // Add a brush stamp where the mouse is, or a deterministic demo path when idle.
16 vec2 cursor;
17 if (iMouse.z > 0.0) {
18 cursor = (iMouse.xy - 0.5 * iResolution.xy) / iResolution.y;
19 } else {
20 float t = iTime * 1.3;
21 cursor = vec2(sin(t) * 0.6, cos(t * 0.7) * 0.35);
22 }
23
24 vec4 stamp = paintStamp(p, cursor, uRadius / iResolution.y, uColor);
25
26 // Premultiplied-ish blend: stamp over trail.
27 vec3 outColor = mix(trail.rgb, stamp.rgb, stamp.a);
28 float outAlpha = max(trail.a, stamp.a);
29
30 fragColor = vec4(outColor, outAlpha);
31 }
32

Inputs for this pass

  • iChannel0 Buffer A
  • iChannel1 Empty
  • iChannel2 Empty
  • iChannel3 Empty
Main Image
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    fragColor = vec4(texture(iChannel0, uv, 0.0).rgb, 1.0);
}

Inputs for this pass

  • iChannel0 Buffer A
  • iChannel1 Empty
  • iChannel2 Empty
  • iChannel3 Empty

Learn from this shader

How it works

Buffer A samples the previous frame from iChannel0, mixes in a four-neighbor blur, and multiplies by a decay factor. It then paints a soft circle at the current cursor position, or an orbiting demo point when the mouse is not held. The image pass just displays the buffer RGB.

Try changing

Push decay toward 1.0 for longer trails, or lower for shorter smears. Increase the brush radius to fill the canvas faster. Add multiple brush stamps in the buffer pass for a particle-trail effect. Replace the blur with a directional blur to make motion smear in one direction.

Using it in a game

Feedback buffers are the classic trick for magic trails, laser burn, footstep fading, water ripples, and even some post-processing effects. The idea is always the same: the output of the last frame becomes the input of the next, slightly modified.

Explore the techniques

Continue with curated explanations and progressively related examples.

LicenseMIT
Views0
Forks0

Discussion

Loading comments...