Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 62, Space Colonisation: a still of the growth / space colonisation plate as the atlas renders it, in the lattices accent.

PL. 62  ·  LATTICES / GROWTH / SPACE COLONISATION

Space Colonisation

Adam Runions, Brendan Lane & Przemysław Prusinkiewicz, 2007

OPEN THE LIVE PLATE ▸

DEFINITION

each attractor pulls its nearest node
nodes step toward the mean pull
attractors die inside the kill radius

NOTES

Scatter points where a plant could grow, then let the branches compete for them: every attractor tugs on its nearest node, each node steps toward the average tug, and any attractor approached closely enough is consumed. That is the whole algorithm, and it produces leaf venation, tree crowns and root systems depending only on how the attractors are scattered. Growth claims territory — hence colonisation — and when the field is spent, the plate scatters a fresh one.

PROVENANCE

Origin
A. Runions, B. Lane & P. Prusinkiewicz, "Modeling Trees with a Space Colonization Algorithm", Eurographics Workshop on Natural Phenomena, 2007
Standing
Public domain — implemented from the published description
Constants
The attractor count is locked: it is the cost of every growth step

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. 62 · SPACE COLONISATION — Adam Runions, Brendan Lane & Przemysław Prusinkiewicz, 2007
//   each attractor pulls its nearest node
//   nodes step toward the mean pull
//   attractors die inside the kill radius
// 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=venation

float p_pts  = 300 + chf('pts_tweak');      // attractors · live 150 .. 500
float p_step = 6 + chf('step_tweak');       // growth step · live 4 .. 10
float p_kill = 1.8 + chf('kill_tweak');     // kill radius (× step) · live 1 .. 4

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

// Runions, Lane & Prusinkiewicz's space colonisation, grown to its finish
// in one cook: every attractor tugs on its nearest node, each node steps
// toward the average tug, and any attractor an advancing tip comes close
// enough to is consumed. The plate iterates twice a frame; this iterates
// until the field is spent or the tips stall against each other, which is
// the finished tree the plate holds on screen before rescattering.
//
// The domain is the plate's own canvas at its 560-unit reference width
// (the width its line weights were tuned against), root at the bottom
// centre, y negated onto Houdini's up so the tree grows upward. Branches
// are two-point polyline prims, parent to child, coloured by the plate's
// three growth bands with its stroke alpha carried as Alpha; the attractors
// the finished tree never claimed are dim points carrying the plate's own
// underlay alpha. Deterministic: the scatter is random(counted seed) on the
// plate's first-epoch seed, so every cook is the same tree.
float W = 560.0, H = 560.0;
int forma_maxnodes = 3600;

int rc = 2007;                   // seeded(2007 + epoch·4241) at epoch 0
int npts = int(rint(p_pts));
float ax[], ay[];
for (int i = 0; i < npts; i++){
    push(ax, W * (0.08 + 0.84 * random(rc)));  rc++;
    push(ay, H * (0.06 + 0.68 * random(rc)));  rc++;
}
float nx[], ny[];
int   npar[];
push(nx, W / 2.0);  push(ny, H * 0.94);  push(npar, -1);

float stp = p_step, kill2 = (stp * p_kill) * (stp * p_kill);
int stall = 0;
while (len(ax) > 0 && stall <= 8 && len(nx) < forma_maxnodes){
    int ns = len(nx);
    float px[], py[];
    int   pc[];
    resize(px, ns);  resize(py, ns);  resize(pc, ns);
    // every attractor pulls its nearest node, unit-normalised — the mean of
    // directions, not of offsets, which is what keeps thin twigs straight
    for (int a = 0; a < len(ax); a++){
        int bi = 0;
        float bd = 1e30;
        for (int i = 0; i < ns; i++){
            float dx = ax[a] - nx[i], dy = ay[a] - ny[i];
            float d2 = dx * dx + dy * dy;
            if (d2 < bd){ bd = d2;  bi = i; }
        }
        float d = sqrt(bd);
        if (d == 0.0) d = 1.0;
        px[bi] += (ax[a] - nx[bi]) / d;
        py[bi] += (ay[a] - ny[bi]) / d;
        pc[bi]++;
    }
    int grew = 0;
    for (int i = 0; i < ns; i++){
        if (!pc[i]) continue;
        float m = sqrt(px[i] * px[i] + py[i] * py[i]);
        if (m == 0.0) m = 1.0;
        push(nx, nx[i] + px[i] / m * stp);
        push(ny, ny[i] + py[i] / m * stp);
        push(npar, i);
        grew = 1;
    }
    if (grew){
        // attractors die inside the kill radius of the nodes just grown
        float kx[], ky[];
        for (int a = 0; a < len(ax); a++){
            int dead = 0;
            for (int i = ns; i < len(nx); i++){
                float dx = ax[a] - nx[i], dy = ay[a] - ny[i];
                if (dx * dx + dy * dy < kill2){ dead = 1;  break; }
            }
            if (!dead){ push(kx, ax[a]);  push(ky, ay[a]); }
        }
        ax = kx;  ay = ky;
        stall = 0;
    } else stall++;
}

// the field the finished tree never claimed, as the plate leaves it showing
for (int a = 0; a < len(ax); a++){
    int pt = addpoint(0, set(ax[a] - W / 2.0, (H / 2.0) - ay[a], 0.0));
    setpointattrib(0, "Cd", pt, forma_ramp(0.9));
    setpointattrib(0, "Alpha", pt, 0.3);
}
// the tree: one prim per branch segment, banded through the growth's thirds
int N = len(nx);
for (int i = 1; i < N; i++){
    int band = min(2, i * 3 / N);
    vector c = forma_ramp(0.84 + 0.1 * float(band));
    int pr = npar[i];
    int p0 = addpoint(0, set(nx[pr] - W / 2.0, (H / 2.0) - ny[pr], 0.0));
    int p1 = addpoint(0, set(nx[i]  - W / 2.0, (H / 2.0) - ny[i],  0.0));
    setpointattrib(0, "Cd", p0, c);
    setpointattrib(0, "Cd", p1, c);
    setpointattrib(0, "Alpha", p0, 0.85);
    setpointattrib(0, "Alpha", p1, 0.85);
    addprim(0, "polyline", p0, p1);
}