Feedback Paint Trails
by GameDev.net · GLSL ES 3.00 (WebGL2) · 25 Aug 2026
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.
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.
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
uniform float uDecay; // @param 0.90..0.999 = 0.985 "Trail decay"
uniform float uRadius; // @param 4.0..48.0 = 16.0 "Brush radius"
uniform vec3 uColor; // @param 0.0..1.0 = 0.15, 0.75, 0.95 color "Brush color"
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
vec4 prev = texture(iChannel0, uv, 0.0);
// Decay + small neighbor blur for the feedback trail.
vec4 blur = sampleBlur(iChannel0, uv, iResolution.xy);
vec4 trail = mix(blur, prev, 0.12) * uDecay;
// Add a brush stamp where the mouse is, or a deterministic demo path when idle.
vec2 cursor;
if (iMouse.z > 0.0) {
cursor = (iMouse.xy - 0.5 * iResolution.xy) / iResolution.y;
} else {
float t = iTime * 1.3;
cursor = vec2(sin(t) * 0.6, cos(t * 0.7) * 0.35);
}
vec4 stamp = paintStamp(p, cursor, uRadius / iResolution.y, uColor);
// Premultiplied-ish blend: stamp over trail.
vec3 outColor = mix(trail.rgb, stamp.rgb, stamp.a);
float outAlpha = max(trail.a, stamp.a);
fragColor = vec4(outColor, outAlpha);
}
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.
Discussion