3D Cellular Automata — Voxel Life and Volumetric Rules
Conway's Game of Life lives on a flat grid, but nothing in the mathematics of cellular automata restricts them to two dimensions. Replace pixels with voxels, replace the Moore neighbourhood with its cubic-lattice equivalent, and the same birth/survival logic that produces gliders and oscillators in 2-D produces coral-like growths, crystalline lattices, and slowly breathing spheres of light in 3-D. This article builds a 3D cellular automaton from first principles — neighbourhoods, rule notation, sparse storage, and a WebGL renderer you can run in the browser.
1. The Cubic Lattice and 3D Neighbourhoods
A 3D cellular automaton replaces the 2-D grid of cells with a cubic lattice of voxels, each holding a discrete state (typically binary: alive or dead). At every generation, every voxel is updated simultaneously based on the states of the voxels surrounding it — a local rule applied identically everywhere, exactly as in 2-D, just with one more axis of neighbours to sum.
The two standard neighbourhood shapes generalise directly from their 2-D counterparts:
- Von Neumann neighbourhood (3D): the 6 face-adjacent voxels (±x, ±y, ±z). Compact and computationally cheap, but produces automata with strong axis-aligned symmetry.
- Moore neighbourhood (3D): all 26 surrounding voxels in the 3×3×3 cube minus the centre (3³ − 1 = 26). This is the direct analogue of the 8-neighbour Moore neighbourhood used by Conway's Game of Life, and it is the neighbourhood used by almost all popular "3D Life" rules.
N(x,y,z) = { (x+dx, y+dy, z+dz) : dx,dy,dz ∈ {-1,0,1}, (dx,dy,dz) ≠ (0,0,0) }
|N| = 3³ − 1 = 26 neighbours // vs. 8 in 2-D Moore
The jump from 8 to 26 neighbours is the single most important fact about 3D CA: the live-neighbour count can now range from 0 to 26 instead of 0 to 8, so birth/survival thresholds must be re-tuned — rules copied verbatim from 2-D (like B3/S23) produce a lattice that is either permanently empty or fills solid almost immediately, because a 3-neighbour birth threshold is far too easy to satisfy in a 26-neighbour space.
2. Rule Notation: Birth/Survival in 3D
3D CA rules are written with the same B/S (birth/survival) convention as 2-D Life-like automata, extended to the 0–26 range of possible neighbour counts:
example — 4555: B5/S45 // classic "4555" rule
a dead voxel is born if it has exactly 5 live neighbours
a live voxel survives if it has 4 or 5 live neighbours
otherwise the voxel dies (isolation or overcrowding)
Because the neighbour count space is so much larger than in 2-D, the qualitative behaviour of 3D automata is extremely sensitive to small changes in the B/S thresholds. Small birth sets and tight survival bands (like 4555) tend to produce stable, coral-like or crystalline structures that stop growing once they reach a steady-state shape. Wider survival bands tend to produce runaway growth that fills the entire lattice, while narrow ones tend toward total extinction — the "interesting" region of rule-space is a thin band, much like Wolfram's edge-of-chaos boundary for elementary CA.
Neighbourhood range is also often generalised beyond the immediate 3×3×3 cube. A Moore neighbourhood of range r includes every voxel within Chebyshev distance r, giving (2r + 1)³ − 1 neighbours — range 2 already yields 124 neighbours, enough to model much smoother, more organic growth than the blocky range-1 automata.
3. Famous 3D Rules: 4555, Amoeba, Pyroclastic
4555 (B5/S45) — Stable Coral Growth
Discovered by Carter Bays in the 1980s during a systematic search for 3D analogues of Life, 4555 is the most widely cited 3D CA rule. Seeded with a small random cluster, it grows into stable, coral-like or crystalline formations that eventually stop expanding — a "Class II" 3D automaton in Wolfram's terminology, settling into static or slowly oscillating equilibrium rather than chaotic or unbounded growth.
Amoeba (B5678/S45678)
A wide survival band (4–8) paired with a wide birth band (5–8) produces blob-like masses that pulse, extend pseudopod-like protrusions, and slowly drift — visually reminiscent of amoeboid organisms, hence the name. Because both bands are wide, small local fluctuations can persist indefinitely without collapsing or exploding, giving it much more dynamic, "alive" behaviour than the static 4555 rule.
Pyroclastic (B45678/S56789+ neighbourhood range 1)
Named for its resemblance to billowing ash clouds, Pyroclastic uses very wide birth and survival bands, generating dense, roiling volumes that expand rapidly from a small seed before settling into large, cloud-like static structures. It demonstrates how, unlike 2-D Life where wide bands almost always mean "everything dies or everything fills," the extra dimension gives wide-band 3D rules room to form complex internal cavities and shells rather than simple solid blocks.
4. Sparse Storage: Octrees and Hash Sets
A dense 3-D boolean array of side N costs O(N³) memory — a modest 256³ grid is already 16.7 million cells, and most 3D CA states are sparse: the vast majority of the lattice is empty, especially early in a simulation or with stable rules like 4555 that never fill more than a thin shell of active voxels. Two data structures dominate practical implementations:
-
Hash set of live coordinates: store only
{x,y,z}triples (packed into a single 32- or 64-bit integer key) for live cells in aSetor hash map. Neighbour counting becomes 26 hash lookups per live cell and its neighbours — efficient when the live population is a small fraction of the bounding volume, and this is exactly the pattern used by high-performance implementations of Conway's Game of Life extended to "hashlife"-style sparse representations. - Sparse voxel octree (SVO): recursively subdivide space into 8 octants, storing only branches that contain live voxels and collapsing empty subtrees to a single "empty" leaf. Octrees trade lookup speed for much better memory scaling on clustered, spatially coherent populations — the same structure used in ray-traced voxel renderers and in Minecraft- style engines for chunk storage.
key = (x & 0x1FFFFF) | ((y & 0x1FFFFF) << 21) | ((z & 0x1FFFFF) << 42)
// supports coordinates in [-1048576, 1048575] per axis in a single 63-bit key
5. From Voxels to Surfaces: Marching Cubes
Rendering live voxels as individual cubes is simple and fast, but for smoother, more organic visuals — especially with wide-band rules like Amoeba and Pyroclastic — it helps to extract a continuous isosurface from the discrete voxel field. The marching cubes algorithm (Lorensen & Cline, 1987) does exactly this:
- Treat each 2×2×2 block of voxel corners as a "cube" and compute an 8-bit index from which corners are inside vs. outside the surface (live vs. dead, or above/below a density threshold for fractional-state automata).
- Look up the index in a precomputed table of 256 possible triangle configurations (reducible to 15 unique cases by symmetry) to get the triangle edges crossing that cube.
- Interpolate vertex positions along each crossing edge (linear or using a smoothed density field) and emit the triangles.
For a purely binary voxel CA, marching cubes at threshold 0.5 produces the classic "blocky-but-triangulated" surface; smoothing the CA state field first (e.g. by low-pass filtering the boolean grid into a continuous density) yields the rounded, biological- looking shells often used in generative-art renders of 3D CA.
6. JavaScript: A 3D CA Step Function
A minimal dense-array implementation, generalising the 2-D Game of Life step function to a cubic lattice with a configurable B/S rule string:
class CellularAutomaton3D {
constructor(N, birth, survive) {
this.N = N; // grid is N x N x N
this.birth = new Set(birth); // e.g. [5] for B5
this.survive = new Set(survive); // e.g. [4,5] for S45
this.grid = new Uint8Array(N * N * N);
}
idx(x, y, z) { return (x * this.N + y) * this.N + z; }
countNeighbours(x, y, z) {
const { N, grid } = this;
let n = 0;
for (let dx = -1; dx <= 1; dx++)
for (let dy = -1; dy <= 1; dy++)
for (let dz = -1; dz <= 1; dz++) {
if (dx === 0 && dy === 0 && dz === 0) continue;
const nx = (x + dx + N) % N; // toroidal wrap
const ny = (y + dy + N) % N;
const nz = (z + dz + N) % N;
n += grid[this.idx(nx, ny, nz)];
}
return n;
}
step() {
const { N, grid, birth, survive } = this;
const next = new Uint8Array(N * N * N);
for (let x = 0; x < N; x++)
for (let y = 0; y < N; y++)
for (let z = 0; z < N; z++) {
const i = this.idx(x, y, z);
const n = this.countNeighbours(x, y, z);
const alive = grid[i] === 1;
next[i] = (alive ? survive.has(n) : birth.has(n)) ? 1 : 0;
}
this.grid = next;
}
}
// 4555: B5/S45
const ca = new CellularAutomaton3D(32, [5], [4, 5]);
This dense implementation is fine up to a few tens of voxels per
axis; beyond that, switch to the sparse hash-set representation
from Section 4 so that step() only visits live cells
and their neighbours rather than the full N³ volume.
7. Rendering with Three.js Instancing
Rendering thousands of independent cube meshes at 60fps is impractical with one draw call per voxel — the standard technique is instanced rendering, where a single cube geometry and material is drawn once per live voxel using a GPU instance buffer, collapsing the whole lattice into one draw call:
const geometry = new THREE.BoxGeometry(0.9, 0.9, 0.9);
const material = new THREE.MeshStandardMaterial({ color: 0x0ea5e9 });
const maxVoxels = N * N * N;
const mesh = new THREE.InstancedMesh(geometry, material, maxVoxels);
const dummy = new THREE.Object3D();
function syncInstances(ca) {
let count = 0;
const half = ca.N / 2;
for (let x = 0; x < ca.N; x++)
for (let y = 0; y < ca.N; y++)
for (let z = 0; z < ca.N; z++)
if (ca.grid[ca.idx(x, y, z)]) {
dummy.position.set(x - half, y - half, z - half);
dummy.updateMatrix();
mesh.setMatrixAt(count++, dummy.matrix);
}
mesh.count = count; // only draw the live voxels
mesh.instanceMatrix.needsUpdate = true;
}
Calling ca.step() followed by syncInstances(ca)
on a timer (or throttled to every few animation frames, since
rule-based growth reads well at 5–10 generations per second)
produces a live, rotatable voxel-life visualisation. For lattices
in the hundreds-per-axis range, move the neighbour-counting step
itself onto the GPU as a compute-style fragment shader operating on
a 3D texture, since InstancedMesh alone only solves
the rendering half of the performance problem.
Try it live
Explore a rotatable, real-time 3D cellular automaton in the browser — watch voxel colonies grow, stabilise, and pulse under different birth/survival rules.
8. Applications: Growth, Terrain, Art
Procedural Growth and Biology
3D CA are a natural fit for modelling diffusion-limited aggregation, coral and mineral growth, and tumour proliferation: cells occupy or vacate voxels based on local nutrient or neighbour density, producing branching or shell-like structures without any explicit geometric rule — the shape is an emergent consequence of the local update alone, just as in 2-D reaction-diffusion systems but with an extra spatial degree of freedom for the growth to explore.
Procedural Terrain and Caves
Voxel game engines commonly seed a 3-D grid with random noise and run a small number of CA passes with a smoothing-oriented rule (roughly "solid if ≥13 of 26 neighbours are solid") to erode sharp noise into smooth, believable cave systems and overhangs — a direct 3-D extension of the 2-D "birth 5 / survival 4" cellular automaton technique long used for 2-D roguelike dungeon generation.
Generative Art and Music Visualisation
Because 3D CA states can be rendered as translucent, colour-mapped voxel fields or converted to a marching-cubes isosurface, they are a popular substrate for generative art: mapping neighbour count or "age since birth" to hue and opacity produces glassy, coral-like, or crystalline renders, and driving the birth/survival thresholds from an audio signal turns the automaton into a real-time, rule-based music visualiser.