Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 55, Poisson-Disk Sampling: a still of the sampling / blue noise plate as the atlas renders it, in the lattices accent.

PL. 55  ·  LATTICES / SAMPLING / BLUE NOISE

Poisson-Disk Sampling

Robert Bridson, 2007 · dart throwing after Robert L. Cook, 1986

OPEN THE LIVE PLATE ▸

DEFINITION

no two samples closer than r
candidates ring an active sample: r ≤ d < 2r
k failures retire the sample

NOTES

Random but never crowded: every sample keeps its neighbours at least r away, which is the "blue noise" the eye reads as even texture with no pattern. The photoreceptors of the monkey retina are packed this way, and renderers sample this way to trade aliasing for quiet noise. Cook proved why it works; Bridson found the linear-time construction this plate runs — grow outward from one seed, ringing each sample with candidates until it retires.

PROVENANCE

Origin
R. Bridson, "Fast Poisson Disk Sampling in Arbitrary Dimensions", SIGGRAPH sketches, 2007; R. L. Cook, "Stochastic Sampling in Computer Graphics", ACM Transactions on Graphics 5(1), 1986
Standing
Public domain — published algorithms, implemented from the papers
Constants
The gap is the whole definition; tries below ~10 leave visible holes
Source
doi:10.1145/1278780.1278807

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. 55 · POISSON-DISK SAMPLING — Robert Bridson, 2007 · dart throwing after Robert L. Cook, 1986
//   no two samples closer than r
//   candidates ring an active sample: r ≤ d < 2r
//   k failures retire the sample
// 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=poisson

float p_gap   = 22 + chf('gap_tweak');        // r — minimum gap · live 14 .. 40
float p_tries = 30 + chf('tries_tweak');      // k — candidates · live 5 .. 30

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

// Bridson's fast Poisson-disk sampling run until every active sample has
// retired: candidates ring an active sample between r and 2r, a background
// grid of cell r/√2 makes the clearance test constant-time, and k failures
// retire the sample — the plate's own construction on the plate's own seed,
// so every cook grows the same blue noise from the same first dart. One
// point per sample, coloured by acceptance order, which is growth order.
// Domain is the plate's 560-unit reference canvas, where the gap constant
// is already in scale. The plate's faint underlay, its HILITE head and the
// exclusion-disk cursor are the reveal's devices, not the sampling, and are
// not ported.
float W = 560.0, H = 560.0, m = 8.0;

float r = p_gap;
int k = int(rint(p_tries));
float cell = r / sqrt(2.0);
int gw = max(1, int(ceil(W / cell))), gh = max(1, int(ceil(H / cell)));
int grid[];
resize(grid, gw * gh);
for (int i = 0; i < gw * gh; i++) grid[i] = -1;

int rc = 613;                     // the plate's own seed, counted upward
float ptx[], pty[];
int active[];

// the first dart, in the plate's own central window
float sx = W * (0.3 + 0.4 * random(rc));  rc++;
float sy = H * (0.3 + 0.4 * random(rc));  rc++;
grid[int(sy / cell) * gw + int(sx / cell)] = 0;
push(ptx, sx);  push(pty, sy);  push(active, 0);

while (len(active) > 0){
    int ai = int(random(rc) * len(active));  rc++;
    if (ai >= len(active)) ai = len(active) - 1;
    float qx = ptx[active[ai]], qy = pty[active[ai]];
    int placed = 0;
    for (int a = 0; a < k; a++){
        float ang = random(rc) * M_PI * 2.0;  rc++;
        float rad = r * (1.0 + random(rc));  rc++;
        float x = qx + rad * cos(ang), y = qy + rad * sin(ang);
        if (x < m || y < m || x > W - m || y > H - m) continue;
        // clearance against the 5×5 neighbourhood — one sample per cell at most
        int gx = int(x / cell), gy = int(y / cell), clear = 1;
        for (int dy = -2; dy <= 2 && clear; dy++){
            for (int dx = -2; dx <= 2; dx++){
                int cxx = gx + dx, cyy = gy + dy;
                if (cxx < 0 || cyy < 0 || cxx >= gw || cyy >= gh) continue;
                int i = grid[cyy * gw + cxx];
                if (i >= 0){
                    float ddx = ptx[i] - x, ddy = pty[i] - y;
                    if (sqrt(ddx * ddx + ddy * ddy) < r){ clear = 0;  break; }
                }
            }
        }
        if (!clear) continue;
        grid[gy * gw + gx] = len(ptx);
        push(active, len(ptx));
        push(ptx, x);  push(pty, y);
        placed = 1;
        break;
    }
    if (!placed) removeindex(active, ai);
}

int N = len(ptx);
for (int i = 0; i < N; i++){
    // canvas y runs down; negated about centre so growth spreads as the plate shows it
    int pt = addpoint(0, set(ptx[i] - W / 2.0, (H / 2.0) - pty[i], 0.0));
    setpointattrib(0, "Cd", pt, forma_ramp(0.84 + 0.2 * float(i) / float(N)));
    setpointattrib(0, "Alpha", pt, 0.85);
}

AFTER EFFECTS · DECLINED

Points, not a path: a blue-noise sample is the spacing between dots, and joining them in any order draws a figure the sampling never made.