Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 64, Curl Noise: a still of the field / divergence-free plate as the atlas renders it, in the fields accent.

PL. 64  ·  FIELDS / FIELD / DIVERGENCE-FREE

Curl Noise

Robert Bridson, Jim Houriham & Marcus Nordenstam, 2007

OPEN THE LIVE PLATE ▸

DEFINITION

v = ∇ × ψ,  in 2D: v = (∂ψ/∂y, −∂ψ/∂x)
∇ · v = 0 identically

NOTES

Take a noise field, call it a stream function, and use its perpendicular gradient as a velocity. The curl of any field is divergence-free by construction, so this flow can never compress: particles swirl indefinitely and never pile into the sinks that an angle read straight out of noise always produces. Bridson introduced it for smoke and water that had to look turbulent without being simulated. Compare plate 23, which does read the angle directly — the difference is visible as bunching.

PROVENANCE

Origin
R. Bridson, J. Houriham & M. Nordenstam, "Curl-Noise for Procedural Fluid Flow", ACM Transactions on Graphics 26(3), SIGGRAPH 2007
Standing
Public domain — a vector identity applied to noise
Constants
Particle count is locked: it is the cost of every advection step
Source
doi:10.1145/1239451.1239497

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. 64 · CURL NOISE — Robert Bridson, Jim Houriham & Marcus Nordenstam, 2007
//   v = ∇ × ψ,  in 2D: v = (∂ψ/∂y, −∂ψ/∂x)
//   ∇ · v = 0 identically
// 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=curl

float p_scale = 2.2 + chf('scale_tweak');     // field frequency · live 0.6 .. 6
float p_speed = 1.1 + chf('speed_tweak');     // step length · live 0.3 .. 3
float p_count = 900 + chf('count_tweak');     // particles · live 300 .. 2000

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

// Bridson's curl noise: the fbm field taken as a stream function ψ, each
// particle advected along its perpendicular gradient (∂ψ/∂y, −∂ψ/∂x) by the
// plate's own central differences at the plate's own eps — divergence-free
// by the vector identity, which is why the worms swirl forever and never
// bunch. Trails are polylines over the plate's opening burst of 70 steps —
// the same total, paced seven a frame over ten frames since 0.28 rather than
// all in the paint the card first appears in;
// a particle leaving the frame respawns on a fresh prim. Colour is the
// plate's bright-lobe mapping per particle. Deterministic on the plate's
// own counted seed.
//
// The same recorded substitution as flowfield: VEX has no bit shifts, so
// the lattice hash under the value noise is VEX's random() — the field
// differs from the page's in layout, not statistics. Helpers carry this
// port's own prefix so both noise plates can share one scene.
float W = 560.0, H = 560.0;
int forma_steps = 70;

function float forma_cn_hash2(int x, y){ return random(x * 1619 + y * 31337); }
function float forma_cn_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_cn_hash2(xi, yi),     forma_cn_hash2(xi + 1, yi),     u),
                lerp(forma_cn_hash2(xi, yi + 1), forma_cn_hash2(xi + 1, yi + 1), u), v);
}
function float forma_cn_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_cn_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);
float eps = 0.008;                // the plate's own derivative step, in field units
int rc = 2007;                    // 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(0.84 + 0.22 * hue);
    int prim = addprim(0, "polyline");
    for (int s = 0; s <= forma_steps; s++){
        // canvas y runs down; negated so the worms turn 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.4);
        addvertex(0, prim, pt);
        float fx = x / W * p_scale, fy = y / H * p_scale;
        float dpdx = (forma_cn_fbm(fx + eps, fy, 4) - forma_cn_fbm(fx - eps, fy, 4)) / (2.0 * eps);
        float dpdy = (forma_cn_fbm(fx, fy + eps, 4) - forma_cn_fbm(fx, fy - eps, 4)) / (2.0 * eps);
        float m = sqrt(dpdx * dpdx + dpdy * dpdy);
        if (m == 0.0) m = 1.0;
        x += dpdy / m * sc;       // v = (∂ψ/∂y, −∂ψ/∂x): the perpendicular gradient
        y += -dpdx / m * sc;
        if (x < 0.0 || x > W || y < 0.0 || y > H){
            x = random(rc) * W;  rc++;
            y = random(rc) * H;  rc++;
            prim = addprim(0, "polyline");
        }
    }
}