home/ modules/ simulating-natural-systems

Simulating natural systems: automata, flow fields, and growth

  • learner can implement cellular automata (Game of Life, Langton's Ant, continuous CA) from local neighbour rules
  • learner can build Perlin flow fields and noise fields that steer motion organically
  • learner can grow organic structures via diffusion-limited aggregation and circle packing

Build a 'nature of code' simulation suite: a Game-of-Life/Langton's-Ant automaton, a Perlin flow field steering particles, and a growth process (DLA or circle packing), each running as a self-contained live sketch.

Nature simulations are the visual backbone of an audiovisual live set: an automaton pulsing on the beat, a flow field carrying particles through a breakdown, a dendritic growth blooming over a build. This module turns three families of natural-system algorithms into self-contained sketches you can drop into a projection rig and mutate live — where every sketch must keep running while you edit it, so clean state handling matters as much as the rule itself.

The arc starts with the most constrained system: grid automata. Begin from the local-neighbour-rule model and its non-negotiable two-phase update (compute all next states, then apply), instantiate it as Conway’s two survival/birth rules, then loosen it twice — Langton’s Ant swaps the synchronous grid for a single agent writing its memory into the environment, and the continuous-value averaging CA trades discrete states for fluid wave propagation. Next you leave the grid’s lockstep for continuous space: 2D noise traversal (nested loops, per-axis seeds, the typewriter-style row reset) generates smooth fields, noise-driven grid displacement clusters shapes organically, and mapping noise to angles yields the flow field that steers your particles. Finally, growth: DLA’s snap-to-nearest-neighbour accretion and collision-frozen circle packing each build structure no one designed.

The nine required atoms gate the capstone directly — each of its three sketches fails without its rule set, and the noise-traversal mechanics recur in all field work, which is why the two-phase CA update and 2D noise loop are drilled to automaticity. Supporting atoms deepen rather than gate: the random-walk lineage behind DLA, emergence as the unifying idea, organic geometry as the aesthetic frame, plus pointers toward the Nature of Code track and camera-input extensions.

Walkthrough

Emergence — complex behaviour from simple local rules — is the visual backbone of an AV set. We’ll build three families of natural-system sketches, each self-contained. Paste each into the p5 web editor and press Run. Each keeps running while you edit — clean state handling matters as much as the rule.

1 — Conway’s Game of Life (a grid automaton). The rule is entirely local: a cell lives if it has 2–3 live neighbours, is born with exactly 3. The non-negotiable trick is the two-phase update — compute all next states from the current grid, then swap ([[cellular-automata]], [[game-of-life-rules]]).

let g, cols, rows, s = 8;
function setup() {
  createCanvas(400, 400); noStroke(); frameRate(12);
  cols = width / s; rows = height / s;
  g = Array.from({ length: cols }, () => Array.from({ length: rows }, () => (random() < 0.3 ? 1 : 0)));
}
function draw() {
  background(15);
  for (let i = 0; i < cols; i++) for (let j = 0; j < rows; j++) if (g[i][j]) { fill(120, 255, 180); rect(i * s, j * s, s - 1, s - 1); }
  let next = g.map(a => a.slice());               // phase 1: compute into a copy
  for (let i = 0; i < cols; i++) for (let j = 0; j < rows; j++) {
    let n = 0;
    for (let a = -1; a <= 1; a++) for (let b = -1; b <= 1; b++) if (a || b) n += g[(i + a + cols) % cols][(j + b + rows) % rows];
    next[i][j] = (g[i][j] && (n === 2 || n === 3)) || (!g[i][j] && n === 3) ? 1 : 0;
  }
  g = next;                                        // phase 2: apply
}

2 — Langton’s Ant (an agent that writes to its world). Swap the synchronous grid for a single agent: on a white cell turn right, on black turn left, flip the cell, step forward. From two rules, ~10,000 steps of chaos then a sudden ordered “highway” — emergence you can watch ([[langtons-ant-emergent-order]]).

let grid = {}, x, y, dir, s = 5;
function setup() { createCanvas(400, 400); noStroke(); x = 40; y = 40; dir = 0; }
function draw() {
  for (let k = 0; k < 200; k++) {                 // many steps/frame
    let key = x + ',' + y, on = grid[key];
    dir = (dir + (on ? 3 : 1)) % 4;               // right on black, left on white
    grid[key] = !on;
    fill(on ? 15 : 255); rect(x * s % width, y * s % height, s, s);
    x += [1, 0, -1, 0][dir]; y += [0, 1, 0, -1][dir];
    x = (x + 80) % 80; y = (y + 80) % 80;
  }
}

3 — a noise flow field steering particles. Continuous space now: map 2D noise to an angle at every point, and let particles ride the field. This is the workhorse of organic motion ([[perlin-noise-flow-field]], [[2d-noise-field]]).

let ps = [];
function setup() { createCanvas(400, 400); for (let i = 0; i < 500; i++) ps.push({ x: random(width), y: random(height) }); }
function draw() {
  background(15, 20); stroke(180, 220, 255, 60);
  for (let p of ps) {
    let a = noise(p.x * 0.005, p.y * 0.005) * TWO_PI * 4;
    let nx = p.x + cos(a), ny = p.y + sin(a);
    line(p.x, p.y, nx, ny);
    p.x = (nx + width) % width; p.y = (ny + height) % height;
  }
}

4 — a continuous-value CA (fluid waves). Loosen Conway from discrete to continuous: each cell relaxes toward the average of its neighbours, and disturbances propagate like ripples — a different emergent texture from the same local-averaging idea ([[continuous-ca]]).

let f, cols, rows, s = 8;  // s must divide the canvas evenly (400/8 = 50) so the grid loops stay in bounds
function setup() {
  createCanvas(400, 400); noStroke();
  cols = width / s; rows = height / s;
  f = Array.from({ length: cols }, () => Array.from({ length: rows }, () => random()));
}
function draw() {
  let next = f.map(a => a.slice());
  for (let i = 0; i < cols; i++) for (let j = 0; j < rows; j++) {
    let sum = 0, c = 0;
    for (let a = -1; a <= 1; a++) for (let b = -1; b <= 1; b++) { let ni = i + a, nj = j + b; if (ni >= 0 && ni < cols && nj >= 0 && nj < rows) { sum += f[ni][nj]; c++; } }
    next[i][j] = sum / c;                          // relax toward neighbour average
    fill(next[i][j] * 255, 120, 255 - next[i][j] * 200); rect(i * s, j * s, s, s);
  }
  if (frameCount % 30 === 0) f[floor(random(cols))][floor(random(rows))] = random() < 0.5 ? 0 : 1; // poke it
  else f = next;
}

5 — collision-frozen growth (circle packing). Structure no one designed: drop a circle, grow it until it touches another, freeze it, repeat. Accretion builds an organic, gap-filling pattern ([[circle-packing-collision]] if present).

let circles = [];
function setup() { createCanvas(400, 400); noStroke(); }
function draw() {
  background(12);
  for (let attempt = 0; attempt < 5; attempt++) {
    let c = { x: random(width), y: random(height), r: 1, grow: true };
    for (let o of circles) if (dist(c.x, c.y, o.x, o.y) < o.r + 2) { c.grow = false; break; }
    if (c.grow) circles.push(c);
  }
  for (let c of circles) {
    if (c.grow) { for (let o of circles) if (o !== c && dist(c.x, c.y, o.x, o.y) < c.r + o.r) { c.grow = false; break; } if (c.x - c.r < 0 || c.x + c.r > width || c.y - c.r < 0 || c.y + c.r > height) c.grow = false; if (c.grow) c.r += 0.5; }
    fill((c.r * 6) % 255, 150, 255); ellipse(c.x, c.y, c.r * 2);
  }
}

6 — a suite, staged (the capstone). Three self-contained systems is the deliverable — the automaton, the flow field, and a growth process, each a live sketch you’d drop into a rig and mutate on stage. Here they’re staged as one composited scene (CA texture behind, flow-field particles over it) to show they combine:

let ps = [], t = 0;
function setup() { createCanvas(400, 400); for (let i = 0; i < 400; i++) ps.push({ x: random(width), y: random(height) }); }
function draw() {
  background(10, 25);
  noStroke();                                      // background: noise-field cells (CA-like texture)
  for (let y = 0; y < height; y += 20) for (let x = 0; x < width; x += 20) { let n = noise(x * 0.01, y * 0.01, t); fill(30, 40 + n * 40, 80, 90); rect(x, y, 20, 20); }
  stroke(180, 230, 255, 90);                       // foreground: flow-field particles
  for (let p of ps) { let a = noise(p.x * 0.006, p.y * 0.006, t) * TWO_PI * 4; let nx = p.x + cos(a) * 1.5, ny = p.y + sin(a) * 1.5; line(p.x, p.y, nx, ny); p.x = (nx + width) % width; p.y = (ny + height) % height; }
  t += 0.005;
}

What good looks like. Systems that surprise you — Life should produce gliders and still-lifes you didn’t place, the flow field should braid particles into currents, growth should fill space organically. The craft is in the staging: one system as a bed, one as motion, colour and density chosen so the emergence reads rather than turns to visual noise. If a sketch freezes, it’s usually a state bug (the two-phase update, or an unbounded grow loop) — which is exactly why clean state matters when you’re editing it live. (Skill map: live-visualist Domain A4 — simulation and emergence.)

Now make it yours. Seed Life with a glider gun instead of random. Colour Langton’s ant cells by visit count. Drive the flow-field noise scale from mouseX. Make circle-packing colour by radius. Composite a different pair of systems in the capstone.

Runnable examples

Generated from the context/ instrument corpus by concept (redistributable idioms only). Do not edit — regenerate with gen-module-examples.mjs.

modulation-warp

osc(4).modulate(src(o0), 0.6).out(o0)

hydra-0022 · CC0-1.0

s0.initP5(); src(s0).modulate(noize(), 0.3).out()

p5live-0038 · CC0-1.0

feedback-trail

osc(4).modulate(src(o0), 0.6).out(o0)

hydra-0022 · CC0-1.0

function draw(){ fill(0, 20); rect(0, 0, width, height); circle(mouseX, mouseY, 40) }

p5live-0003 · CC0-1.0

noise-field

noise(4, 0.1).out()

hydra-0002 · CC0-1.0

float h21(vec2 p){return fract(sin(dot(p,vec2(12.9898,78.233)))*43758.5453);}

glsl-0013 · public-domain

flow-field

let ang = noise(x*0.01, y*0.01) * TWO_PI

p5live-0008 · CC0-1.0

blur-soften

filter(BLUR, 4)

p5live-0073 · CC0-1.0

noise-drift

let x = noise(frameCount*0.002)*width, y = noise(frameCount*0.003)*height

p5live-0007 · CC0-1.0

Atoms in this module

Required — these gate the capstone

Cellular automata generate emergent global patterns from cells that update on local neighbour rules
Concept L2 First instrument HF
Conway's Game of Life produces biological patterns from two simple rules about neighbor count
Principle L3 Craft H
Langton's Ant produces ordered structure from two purely local per-step rules
Concept L3 Craft H
Cellular automata with continuous state values (0–255) produce fluid wave-like patterns
Concept L3 Craft H
Perlin noise mapped to angles at each grid cell creates a smooth flow field that steers particles or arrows organically
Concept L3 Craft H
2D Perlin noise maps x,y coordinates to smooth organic texture fields
Procedure L3 Craft H
A 2D Perlin noise field can displace every point in a regular grid to create organic cloud-like structure
Concept L2 First instrument H
Diffusion-limited aggregation grows organic dendritic clusters by snapping each new particle onto the nearest existing one
Concept L3 Craft H
Circle packing fills a region by growing circles until they touch, then freezing each on collision
Concept L3 Craft H

Supporting — enrichment, not gating

An iterative random walk accumulates small random steps to produce an organic wandering line
Concept L2 First instrument H
The Nature of Code is a 12-chapter, 67-video p5.js track on simulating natural systems in code
Fact L2 First instrument H
Emergence is complex organized behavior arising from many simple local interactions
Concept L3 Craft H
Organic geometry builds natural-looking forms from strictly geometric primitives
Concept L2 First instrument HL
Frame differencing detects motion by comparing pixel values between consecutive video frames
Concept L3 Craft H
Domain-warping noise with noise is the core organic visual move — turbulent, liquid, marbled
Principle L2 First instrument HG