Moving the Game of Life automaton to the GPU unlocks million-cell grids
at 60 FPS. Each pixel in a texture represents one cell. A fragment
shader reads the texture, counts neighbours, and writes the next state —
all in parallel across every GPU thread. Two textures alternate
(ping-pong) so the shader reads from the old state and writes to the
new.
1Create ping-pong render targets
import * as THREE from
'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js';
const SIZE = 1024; // grid is SIZE × SIZE cells function makeRT() {
return new THREE.WebGLRenderTarget(SIZE, SIZE, { minFilter:
THREE.NearestFilter, magFilter: THREE.NearestFilter, format:
THREE.RGBAFormat, type: THREE.UnsignedByteType, wrapS:
THREE.RepeatWrapping, // toroidal wrapping wrapT:
THREE.RepeatWrapping, }); } // Two targets — we alternate read/write
each frame let rtA = makeRT(); let rtB = makeRT(); // Helper to swap
them function swap() { [rtA, rtB] = [rtB, rtA]; }
NearestFilter is critical — we must not blend texel
values when sampling neighbours. Bilinear would corrupt the binary
cell state.
2Seed the initial state
// Create a random-noise texture (each pixel R channel = alive/dead)
const data = new Uint8Array(SIZE * SIZE * 4); for (let i = 0; i <
SIZE * SIZE; i++) { const alive = Math.random() < 0.3 ? 255 : 0; //
30 % alive data[i * 4 + 0] = alive; // R = alive data[i * 4 + 3] =
255; // A = opaque } const seedTex = new THREE.DataTexture(data, SIZE,
SIZE, THREE.RGBAFormat); seedTex.needsUpdate = true; // Paint the seed
into rtA via a plain copy pass const copyMat = new
THREE.MeshBasicMaterial({ map: seedTex }); const quad = new
THREE.Mesh(new THREE.PlaneGeometry(2, 2), copyMat); const simCam = new
THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); const simScene = new
THREE.Scene(); simScene.add(quad); renderer.setRenderTarget(rtA);
renderer.render(simScene, simCam); renderer.setRenderTarget(null);
function stepSimulation() { // READ from rtA, WRITE to rtB
simMat.uniforms.u_state.value = rtA.texture;
renderer.setRenderTarget(rtB); renderer.render(simScene, simCam);
renderer.setRenderTarget(null); // Now rtB has the new generation —
swap for next frame swap(); } function animate() {
requestAnimationFrame(animate); // Run several simulation steps per
visual frame for faster patterns for (let s = 0; s < 2; s++)
stepSimulation(); // Display the current state (rtA after swap = the
freshly written rtB) displayMat.uniforms.u_tex.value = rtA.texture;
renderer.render(displayScene, displayCam); }
Always read from one render target and write to the other. Swapping
pointers is O(1) and avoids expensive texture copies.
5Display pass and mouse interaction
// Display shader: map alive=white, dead=dark-green const displayFrag
= /* glsl */ ` precision highp float; uniform sampler2D u_tex; varying
vec2 vUv; void main() { float v = texture2D(u_tex, vUv).r; vec3 col =
mix(vec3(0.05, 0.07, 0.05), vec3(0.28, 0.93, 0.43), v); gl_FragColor =
vec4(col, 1.0); } `; // Mouse: paint live cells on click/drag
canvas.addEventListener('pointermove', e => { if (e.buttons === 0)
return; const uv = { x: e.offsetX / canvas.width, y: 1 - e.offsetY /
canvas.height }; // Write a 5×5 block of live cells around the mouse
position const px = Math.floor(uv.x * SIZE); const py =
Math.floor(uv.y * SIZE); const tex = rtA.texture; // For persistent
editing, use a DataTexture + readRenderTargetPixels approach // or
simply sample the state at that position in the shader using a u_brush
uniform console.info(`Paint at cell (${px}, ${py})`); });
For interactive painting, pass the brush position as a
vec2 uniform to the simulation shader and set cells
within a radius to alive — no CPU readback required.
Frequently Asked Questions
What will I learn in this tutorial?
Run Conway's Game of Life entirely on the GPU using WebGL ping-pong render targets and a 50-line GLSL fragment shader.
What topics are covered in this tutorial?
This tutorial covers: Create ping-pong render targets, Seed the initial state, Write the Game-of-Life fragment shader, Simulation pass with a fullscreen quad, Display pass and interaction.
What tools and technologies does this tutorial use?
This tutorial uses WebGL, GLSL, Three.js, Ping-pong Textures.
How long does this tutorial take?
This tutorial takes approximately 45 minutes to complete.
What prerequisites do I need before starting?
This is a Intermediate-level tutorial — no special preparation beyond basic JavaScript is assumed.