Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 93, Boids: a still of the agents / three rules plate as the atlas renders it, in the lattices accent.

PL. 93  ·  LATTICES / AGENTS / THREE RULES

Boids

Craig Reynolds, 1987

OPEN THE LIVE PLATE ▸

DEFINITION

separation: steer away from neighbours closer than d
alignment: steer toward the mean heading of neighbours
cohesion: steer toward the mean position of neighbours

NOTES

Three rules, each of them local, and a flock appears that none of them mentions. Reynolds wrote the first version at Symbolics in 1986 on a Lisp machine and published it at SIGGRAPH the following year, and the argument he was making was about animation: you cannot key-frame a thousand birds, and you do not have to, because flocking is not a shape being imposed from outside but a consequence of every bird minding only the handful of birds near it. No boid can see the flock. The plate reports the order parameter — the length of the mean heading vector, 0 when the birds point everywhere and 1 when they all point the same way — and announces the crossings, because the transition between those states is sharp and it is not scripted either.

PROVENANCE

Origin
C. W. Reynolds, “Flocks, Herds, and Schools: A Distributed Behavioral Model”, Computer Graphics 21(4) (SIGGRAPH ’87 Proceedings), 1987, 25–34
Written
At Symbolics in 1986, in Symbolics Common Lisp on a 3600 Lisp Machine; first published the following year. Reynolds received an Academy Scientific and Engineering Award in 1998 for the line of work it began
Standing
Public domain — no patent found covering the model. Any filing contemporary with the 1986–87 work would in any case have expired by about 2007
Constants
The three weights are Reynolds’ three rules. Separation alone gives a gas, alignment alone a stream, cohesion alone a knot
Source
doi:10.1145/37402.37406

HOUDINI · VEX

The same published mathematics as a Detail Wrangle body. Paste it into a Wrangle with Run Over set to Detail; every constant is the published value plus a tweak channel, so Create Spare Parameters gives a slider that starts where the paper does.

// FORMA — PL. 93 · BOIDS — Craig Reynolds, 1987
//   separation: steer away from neighbours closer than d
//   alignment: steer toward the mean heading of neighbours
//   cohesion: steer toward the mean position of neighbours
// Paste into a Detail Wrangle (Run Over: Detail), no inputs needed.
// Written from the published mathematics, not adapted from any code.
// Constants arrive at their published values. Press the node's Create
// Spare Parameters button and every tweak becomes a slider — starting
// at 0, the published figure, and moving in the constant's own units.
// https://forma-gen.com/#plate=boids

float p_count = 200 + chf('count_tweak');     // boids · live 60 .. 420
float p_sep   = 1.2 + chf('sep_tweak');       // separation · live 0 .. 2.5
float p_align = 1 + chf('align_tweak');       // alignment · live 0 .. 2.5
float p_coh   = 0.7 + chf('coh_tweak');       // cohesion · live 0 .. 2.5
float p_near  = 0.09 + chf('near_tweak');     // neighbourhood · live 0.04 .. 0.2

// The plate's own colour: FORMA's LATTICES accent as a cosine ramp,
// brightest near t = 0 and t = 1, near-black around t = 0.5.
vector forma_ramp(float t){
  return set(
    0.46 + 0.5 * cos(6.28318530718 * (t + 0)),
    0.4185 + 0.4549 * cos(6.28318530718 * (t + 0.05)),
    0.1389 + 0.151 * cos(6.28318530718 * (t + 0.1)));
}

// Reynolds' three rules run on a torus, and each bird's recent path emitted as
// its own polyline — the trail is the deliverable here, because a flock drawn
// as instantaneous dashes is a still of something whose whole subject is
// motion. The flock is warmed before anything is recorded, so the geometry is
// the settled behaviour rather than the scatter it started from.
//
// Separation, alignment and cohesion are the three published rules and the
// three constants. Positions are unit coordinates on the torus, scaled at the
// end, so the neighbourhood radius means the same thing whatever the plate
// size. Deterministic: the starting scatter is random(counted seed).
//
// waived: trail — the plate's trail length is how many frames of history the
// canvas keeps; here every recorded step becomes a vertex, so the port's
// forma_record below is the same quantity stated as geometry.
int   N            = int(rint(p_count));
int   forma_warm   = 240;              // steps run before recording begins
int   forma_record = 120;              // steps recorded as trail vertices
float forma_step   = 0.0022;           // the plate's own advance per frame

float R = p_near;
float R2 = R * R;
float SEP = R * 0.45, SEP2 = SEP * SEP;

float px[], py[], hx[], hy[], hue[];
resize(px, N);  resize(py, N);  resize(hx, N);  resize(hy, N);  resize(hue, N);
int seed = 1987;
for (int i = 0; i < N; i++){
    px[i] = random(seed);  seed++;
    py[i] = random(seed);  seed++;
    float ang = random(seed) * 2.0 * PI;  seed++;
    hx[i] = cos(ang);  hy[i] = sin(ang);
    hue[i] = random(seed);  seed++;
}

// one polyline per bird, opened only once recording starts
int prim[];
resize(prim, N);
int open[];
resize(open, N);
for (int i = 0; i < N; i++){ prim[i] = -1;  open[i] = 0; }

for (int step = 0; step < forma_warm + forma_record; step++){
    float nhx[], nhy[];
    resize(nhx, N);  resize(nhy, N);
    for (int i = 0; i < N; i++){
        float sx = 0, sy = 0, ax = 0, ay = 0, cx = 0, cy = 0;
        int seen = 0;
        for (int j = 0; j < N; j++){
            if (j == i) continue;
            // shortest offset across the torus in each axis
            float dx = px[j] - px[i];
            if (dx >  0.5) dx -= 1.0;
            if (dx < -0.5) dx += 1.0;
            float dy = py[j] - py[i];
            if (dy >  0.5) dy -= 1.0;
            if (dy < -0.5) dy += 1.0;
            float d2 = dx * dx + dy * dy;
            if (d2 > R2) continue;
            seen++;
            ax += hx[j];  ay += hy[j];          // alignment: their headings
            cx += dx;     cy += dy;             // cohesion: toward their centre
            if (d2 < SEP2 && d2 > 1e-9){        // separation: away, harder when closer
                sx -= dx / d2 * 1e-3;  sy -= dy / d2 * 1e-3;
            }
        }
        float bx = hx[i], by = hy[i];
        if (seen > 0){
            float f = float(seen);
            bx += (ax / f - hx[i]) * p_align * 0.08 + (cx / f) * p_coh * 0.9;
            by += (ay / f - hy[i]) * p_align * 0.08 + (cy / f) * p_coh * 0.9;
        }
        bx += sx * p_sep;  by += sy * p_sep;
        float m = length(set(bx, by, 0.0));
        if (m < 1e-9) m = 1.0;
        nhx[i] = bx / m;  nhy[i] = by / m;
    }
    hx = nhx;  hy = nhy;

    for (int i = 0; i < N; i++){
        float ox = px[i], oy = py[i];
        px[i] = px[i] + hx[i] * forma_step;
        py[i] = py[i] + hy[i] * forma_step;
        if (px[i] < 0) px[i] += 1.0;
        if (px[i] >= 1.0) px[i] -= 1.0;
        if (py[i] < 0) py[i] += 1.0;
        if (py[i] >= 1.0) py[i] -= 1.0;

        if (step < forma_warm) continue;
        // a step across the seam is not a segment the bird flew, so the line
        // is lifted and restarted rather than drawn back over the whole flock
        if (abs(px[i] - ox) > 0.5 || abs(py[i] - oy) > 0.5) open[i] = 0;
        if (open[i] == 0){
            prim[i] = addprim(0, "polyline");
            open[i] = 1;
            int p0 = addpoint(0, set(px[i], -py[i], 0.0));
            setpointattrib(0, "Cd", p0, forma_ramp(0.82 + hue[i] * 0.24));
            addvertex(0, prim[i], p0);
            continue;
        }
        int pt = addpoint(0, set(px[i], -py[i], 0.0));
        setpointattrib(0, "Cd", pt, forma_ramp(0.82 + hue[i] * 0.24));
        addvertex(0, prim[i], pt);
    }
}