Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 23, Flow Field: a still of the advection / vector field plate as the atlas renders it, in the fields accent.

PL. 23  ·  FIELDS / ADVECTION / VECTOR FIELD

Flow Field

Standard practice; after Perlin's vector fields

OPEN THE LIVE PLATE ▸

DEFINITION

θ(p) = 2π · fbm(p)
pₙ₊₁ = pₙ + s·(cos θ, sin θ)

NOTES

Read an angle out of a noise field at every point, then let particles drift along it leaving trails. The technique is the backbone of a whole genre of plotter art: the field is smooth, so neighbouring particles follow nearly the same path and the drawing organises itself into visible currents.

PROVENANCE

Origin
Common practice; the underlying idea is Perlin's vector noise
Standing
Public domain
Constants
Low turn scale gives laminar flow; high gives turbulence

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. 23 · FLOW FIELD — Standard practice; after Perlin's vector fields
//   θ(p) = 2π · fbm(p)
//   pₙ₊₁ = pₙ + s·(cos θ, sin θ)
// 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=flowfield

float p_scale = 2.4 + chf('scale_tweak');     // field frequency · live 0.5 .. 8
float p_speed = 1.2 + chf('speed_tweak');     // step length · live 0.3 .. 4
float p_count = 1200 + chf('count_tweak');    // particles · live 200 .. 3000
float p_turn  = 4 + chf('turn_tweak');        // turn scale · live 0.5 .. 10

// The plate's own colour: FORMA's FIELDS 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.3031 + 0.3294 * cos(6.28318530718 * (t + 0.05)),
    0.11 + 0.1196 * cos(6.28318530718 * (t + 0.1)));
}

// Particles dropped into a noise field, each reading its heading from fbm at
// its own position and stepping along it — the plate's algorithm exactly,
// with each particle's walk emitted as a polyline trail instead of smeared
// into a fading buffer. The trails run the plate's own opening burst of 80
// steps, which is the developed picture the plate reaches over its first ten
// frames — the burst is the same 80 it always was, spread eight a frame since
// 0.28 so scrolling the card into view no longer costs one 83 ms paint; a particle
// that leaves the frame respawns where the plate respawns it and starts a
// fresh trail prim. Trail colour is the plate's own ramp(hue) draw, hue per
// particle. Deterministic: positions, hues and respawns are random(counted
// seed) on the plate's own seed.
//
// One substitution, on the record: the page's lattice hash is exact-int32
// bit mixing, and VEX has no bit shifts — so the hash under the value noise
// is VEX's own random() over the same lattice. The field differs from the
// page's in layout, not in statistics; everything above the hash — the
// smoothstep bilinear value noise, the four-octave fbm, the angle map — is
// the kit's own, ported line for line. Helpers carry a per-port prefix so
// this and curl's copy can share one Houdini scene.
float W = 560.0, H = 560.0;
int forma_steps = 80;

function float forma_ff_hash2(int x, y){ return random(x * 1619 + y * 31337); }
function float forma_ff_vnoise(float x, y){
    int xi = int(floor(x)), yi = int(floor(y));
    float xf = x - floor(x), yf = y - floor(y);
    float u = xf * xf * (3.0 - 2.0 * xf), v = yf * yf * (3.0 - 2.0 * yf);
    return lerp(lerp(forma_ff_hash2(xi, yi),     forma_ff_hash2(xi + 1, yi),     u),
                lerp(forma_ff_hash2(xi, yi + 1), forma_ff_hash2(xi + 1, yi + 1), u), v);
}
function float forma_ff_fbm(float x, y; int oct){
    float sum = 0.0, amp = 0.5, norm = 0.0, xx = x, yy = y;
    for (int i = 0; i < oct; i++){
        sum += amp * forma_ff_vnoise(xx, yy);
        norm += amp;  amp *= 0.5;  xx *= 2.0;  yy *= 2.0;
    }
    return sum / norm;
}

int count = int(rint(p_count));
float sc = p_speed * (W / 300.0);
int rc = 23;                      // the plate's own seed, counted upward

for (int q = 0; q < count; q++){
    float x = random(rc) * W;  rc++;
    float y = random(rc) * H;  rc++;
    float hue = random(rc);  rc++;
    vector col = forma_ramp(hue);
    int prim = addprim(0, "polyline");
    for (int s = 0; s <= forma_steps; s++){
        // canvas y runs down; negated so the currents bend as the plate shows them
        int pt = addpoint(0, set(x - W / 2.0, (H / 2.0) - y, 0.0));
        setpointattrib(0, "Cd", pt, col);
        setpointattrib(0, "Alpha", pt, 0.35);
        addvertex(0, prim, pt);
        float a = forma_ff_fbm(x / W * p_scale, y / H * p_scale, 4) * M_PI * p_turn;
        x += cos(a) * sc;
        y += sin(a) * sc;
        if (x < 0.0 || x > W || y < 0.0 || y > H){
            // off the frame: respawn where the plate respawns, on a fresh trail
            x = random(rc) * W;  rc++;
            y = random(rc) * H;  rc++;
            prim = addprim(0, "polyline");
        }
    }
}