Falling Sand — Cellular Automaton With Gravity
March 2026
Click to pour sand. Watch it fall, pile, and cascade. This is a cellular automaton where each cell follows one rule: if there's empty space below, fall into it. From that single mechanic, dunes form, avalanches cascade, and sand flows like liquid through channels you carve.
Live Demo
Click and drag to draw. Watch sand cascade through structures you build.
The Grid Model
Falling sand simulations are cellular automata — a grid where each cell holds a state (empty, sand, water, wall) and updates based on what's around it. Unlike particle systems that track individual positions, cellular automata work on a fixed grid where matter moves between cells.
The key insight is that the grid itself stores position. A sand particle at (x, y) doesn't need to remember velocity or acceleration — it just checks: is there space below? If yes, move. The grid enforces conservation of matter: a particle leaves one cell and enters another. No orphaned particles, no position drift.
This is the code that initializes the grid:
const CELL_SIZE = 4;
const COLS = Math.floor(canvas.width / CELL_SIZE);
const ROWS = Math.floor(canvas.height / CELL_SIZE);
// 0 = empty, 1 = sand, 2 = water, 3 = wall
let grid = new Array(ROWS).fill(null).map(() => new Array(COLS).fill(0));
Each cell is just an integer. The rendering pass interprets these integers as colors, but the simulation only cares about: what type is this cell, and what's in its neighbors?
The Gravity Step
Every frame, the simulation iterates through the grid and lets particles fall. The simplest rule: if the cell below is empty, move down.
function updateSand(grid, x, y) {
// If this isn't sand, skip
if (grid[y][x] !== SAND) return;
// Check directly below
if (y + 1 < ROWS && grid[y + 1][x] === EMPTY) {
grid[y + 1][x] = SAND;
grid[y][x] = EMPTY;
return;
}
// Try diagonal falls (left or right)
...
}
There's a subtlety: the update order matters. If we sweep top-to-bottom, a sand particle might fall multiple rows in one frame, looking like it teleports. The fix is to sweep bottom-to-top:
for (let y = ROWS - 2; y >= 0; y--) {
for (let x = 0; x < COLS; x++) {
updateSand(grid, x, y);
}
}
Now each particle moves at most once per frame. The result is a cascading avalanche where particles topple one position at a time, building natural-looking piles.
Diagonal Dispersion
Real sand doesn't stack vertically — it forms dunes and slopes. When a sand particle can't fall straight down, it tries to fall diagonally. This creates natural repose angles:
// Can't fall straight down, try diagonal
let dir = Math.random() < 0.5 ? -1 : 1; // Random bias
// Try left or right first, then opposite
if (canMoveDiagonal(grid, x, y, dir)) {
grid[y + 1][x + dir] = SAND;
grid[y][x] = EMPTY;
} else if (canMoveDiagonal(grid, x, y, -dir)) {
grid[y + 1][x - dir] = SAND;
grid[y][x] = EMPTY;
}
The random direction bias prevents systematic left/right behavior. Without this, all sand would avalanche the same direction, creating artificial-looking patterns. The randomness produces organic pile formation.
The recheck after one direction fails ensures particles fall somewhere if space exists. This is what makes sand flow around obstacles — it doesn't get stuck when blocked on one side.
Water Behavior
Water adds horizontal spread to the fall mechanic. Sand checks below, then diagonally down. Water checks below, then left/right at its own level:
function updateWater(grid, x, y) {
// Fall down
if (y + 1 < ROWS && grid[y + 1][x] === EMPTY) {
grid[y + 1][x] = WATER;
grid[y][x] = EMPTY;
return;
}
// Spread horizontally
let dir = Math.random() < 0.5 ? -1 : 1;
if (canMoveHorizontal(grid, x, y, dir)) {
grid[y][x + dir] = WATER;
grid[y][x] = EMPTY;
} else if (canMoveHorizontal(grid, x, y, -dir)) {
grid[y][x - dir] = WATER;
grid[y][x] = EMPTY;
}
}
Water at its own level (not falling) spreads indefinitely. This creates pools, pressure equalization, and flow through channels. Combined with sand, you get sedimentation — sand sinks through water, displacing it upward.
What This Reveals
Falling sand reveals how simple local rules produce complex global behavior. Each cell only sees its immediate neighbors. It has no concept of "pile," "avalanche," or "flow." Yet these emerge from uniform application of gravity and displacement.
The cellular automaton model is distinct from particle systems. Particles carry position and velocity as state. Cellular automata cells carry only type — position is implicit in the grid. This makes cellular automata memory-efficient and deterministic (same grid state always produces same next state). But it limits resolution: you can't have fractional positions or sub-cell movement.
The tradeoff is worth it for sand. Granular materials naturally occupy discrete positions. The grid model matches the physics: sand grains don't overlap, they displace. The simulation captures this without complex collision detection or integration schemes.
Finally, falling sand demonstrates emergence. The rules say only "fall if space below." The result shows dunes, avalanches, hourglass flow, and liquid-like behavior. The system doesn't encode these phenomena — they arise from the rules applied uniformly across the grid.