Feedback and Simulation
Ping-pong buffers, Game of Life, and reaction-diffusion
Every shader we have written so far has been stateless. Each frame computes fresh, forgetting everything that came before. But some effects need memory: ripples that propagate, cells that live and die, patterns that evolve over time.
Feedback is the key. We render to a texture, then read from that texture in the next frame, creating a loop where output becomes input. This transforms shaders from calculators into simulators.
The Ping-Pong Technique
We cannot read from and write to the same texture simultaneously. WebGL forbids it, and for good reason: the results would be undefined. Instead, we use two textures and swap them each frame.
- Read from texture A, write results to texture B
- Display texture B
- Next frame: read from B, write to A
- Display A
- Repeat, alternating which texture is source and which is destination
This is the ping-pong technique. The textures bounce data back and forth, each frame building on the last.
Ping-Pong Buffer Concept
Buffers alternate roles each frame
The pattern is fundamental to GPU simulation. Any cellular automaton, physics simulation, or feedback effect uses some variation of this approach.
Conway's Game of Life
The Game of Life is a cellular automaton where each cell is either alive or dead. The next generation follows simple rules:
- A live cell with 2 or 3 live neighbors survives
- A dead cell with exactly 3 live neighbors becomes alive
- All other cells die or stay dead
int countNeighbors(vec2 uv, vec2 texelSize) {
int count = 0;
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
if (x == 0 && y == 0) continue;
vec2 neighbor = uv + vec2(float(x), float(y)) * texelSize;
if (texture2D(u_state, neighbor).r > 0.5) count++;
}
}
return count;
}
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec2 texelSize = 1.0 / u_resolution;
bool alive = texture2D(u_state, uv).r > 0.5;
int neighbors = countNeighbors(uv, texelSize);
bool nextAlive = alive
? (neighbors == 2 || neighbors == 3)
: (neighbors == 3);
gl_FragColor = vec4(vec3(nextAlive ? 1.0 : 0.0), 1.0);
}Conway's Game of Life
Click or drag to draw live cells
From these simple rules emerge gliders, oscillators, and structures of astonishing complexity. The simulation runs entirely on the GPU, updating millions of cells per frame.
Ripple Simulation
Water ripples follow the wave equation. At each point, the new height depends on the current height, the previous height, and the heights of neighbors:
We need two pieces of state per pixel: current height and previous height. We can pack both into different channels of the same texture, or use separate textures.
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec2 texelSize = 1.0 / u_resolution;
float current = texture2D(u_state, uv).r;
float previous = texture2D(u_state, uv).g;
float left = texture2D(u_state, uv - vec2(texelSize.x, 0.0)).r;
float right = texture2D(u_state, uv + vec2(texelSize.x, 0.0)).r;
float up = texture2D(u_state, uv + vec2(0.0, texelSize.y)).r;
float down = texture2D(u_state, uv - vec2(0.0, texelSize.y)).r;
float c = 0.5;
float next = 2.0 * current - previous + c * c * (left + right + up + down - 4.0 * current);
next *= 0.99; // damping
gl_FragColor = vec4(next, current, 0.0, 1.0);
}Ripple Simulation
Click to create ripples. Lower damping = faster decay.
Click to create disturbances. The ripples spread outward, reflect off boundaries, and interfere with each other. All computed in real-time on the GPU.
Reaction-Diffusion
Reaction-diffusion systems model two chemicals that diffuse through space while reacting with each other. The Gray-Scott model produces mesmerizing patterns:
Where:
- and are chemical concentrations
- and are diffusion rates
- is the feed rate (how fast A is replenished)
- is the kill rate (how fast B decays)
The term is the Laplacian, computed the same way as in edge detection: the sum of neighbors minus 4 times the center.
Reaction-Diffusion
Click to add chemical B. Different presets produce different patterns.
Different parameter combinations produce spots, stripes, spirals, and labyrinthine patterns. These same equations describe patterns on animal skins, chemical oscillations, and morphogenesis in biology.
Implementation Details
Setting up ping-pong rendering in WebGL requires:
- Two framebuffers: Each with an attached texture
- Initialization: Seed the first texture with initial state
- The render loop: Alternate binding framebuffers and textures
- A final pass: Render the result texture to the screen
// Conceptual pseudocode
let currentBuffer = bufferA;
let nextBuffer = bufferB;
function frame() {
// Render simulation step
gl.bindFramebuffer(gl.FRAMEBUFFER, nextBuffer.fbo);
gl.bindTexture(gl.TEXTURE_2D, currentBuffer.texture);
drawQuad(simulationShader);
// Swap buffers
[currentBuffer, nextBuffer] = [nextBuffer, currentBuffer];
// Display result
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, currentBuffer.texture);
drawQuad(displayShader);
}The display shader can be a simple passthrough, or it can apply additional effects like color mapping or post-processing.
Precision and Stability
Floating-point textures provide better precision for simulations but are not universally supported. When using 8-bit textures, you may need to:
- Scale values to fit the 0-255 range
- Accept some quantization artifacts
- Use multiple channels for higher precision
Stability is another concern. Simulations can explode if parameters are too extreme. The ripple equation needs damping to prevent energy from building up. Reaction-diffusion needs balanced feed and kill rates.
Test your simulations at different resolutions. Some effects change character dramatically at high resolution, while others become unstable.
Beyond Simple Feedback
These techniques extend to many other effects:
- Fluid simulation: Velocity and pressure fields advected and diffused
- Particle systems: Positions and velocities stored in textures
- Procedural growth: L-systems and diffusion-limited aggregation
- Audio visualization: Previous frame influences current, creating trails
Any system with state that evolves over time can potentially run on the GPU using ping-pong buffers.
Key Takeaways
- Feedback effects require reading from previous frame state
- Ping-pong uses two textures, alternating read and write each frame
- Conway's Game of Life: count neighbors, apply birth/survival rules
- Ripple simulation: wave equation with current and previous heights
- Reaction-diffusion: two chemicals diffuse and react, producing patterns
- Stability requires damping, bounded parameters, and appropriate precision
- The pattern generalizes to fluid simulation, particles, and any evolving system