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