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);
}
}