It is the central-difference curl of a three-channel hash-based value-noise
vector potential, normalised by the stencil's analytic maximum. Written out,
with s = (position − scroll·time) · frequency in lattice space and
d = TURBULENCE_DIFFERENCE_CELLS:
The /4 is the normalisation: each bracket is a difference of two differences
of values in [−1, 1], so it lies in [−4, 4], and hence
|componentᵢ| ≤ |amplitude| always. That bound is what the tests assert.
Limitations — all of them, stated plainly
It is value noise, not Perlin or simplex noise. The lattice values are
scalars hashed from the integer cell corners and interpolated; there are no
gradients. Value noise is cheaper and easier to bound, but it has visible
axis-aligned structure and a blockier spectrum than gradient noise. It
is one octave: there is no fBm/fractal sum, so there is exactly one eddy
size, 1 / frequency world units. Octaves are staged (they multiply the
96-hash cost per sample by the octave count).
The curl is a finite difference, not an analytic derivative, and the
field is not divergence-free in any useful sense. Central-difference
operators commute, so the divergence measured with this very stencil and
step is zero — measured, 3.4 × 10⁻¹⁵, i.e. float rounding. That is a
lattice identity, not a property of the field: measured with a step of
0.37·d instead, the divergence is ≈ 0.54 at the defaults, which is not
small. No incompressibility claim is made and none is tested. Treat
"curl of a potential" as a description of the construction — it is what
makes the field swirl instead of converging on sinks — not as physics.
amplitude is an upper bound, not a typical magnitude. The worst case
needs four lattice corners to hit ±1 in the right pattern. Measured over
20 000 seeded sample points (60 000 components) spread over ±50 world units,
at frequencies 0.25, 1 and 4: the RMS of a single component is
≈ 0.186·amplitude and the largest component seen is ≈ 0.76·amplitude,
essentially independent of frequency. Author with that factor in mind, or
measure your own configuration; tests/fields.test.ts pins a conservative
version of the same statement.
Time does nothing unless TurbulenceFieldOptions.scroll is set.
See that option: the pattern is frozen and advected, not evolving.
The lattice is unbounded but not infinite-precision. Cell indices go
through ToUint32 inside the hash, so coordinates beyond ±2³¹ cells alias
back onto the pattern. At the default frequency that is ±2 billion world
units.
Non-finite input gives NaN out. Nothing is guarded on the hot path.
The field is smooth: the quintic fade makes each ψc C², so the sampled
acceleration is C² in space and Lipschitz — no popping as a particle
crosses a cell boundary.
Determinism (§33)
Math.random is never called. The constructor draws exactly threeuint32s from new SeededRandom(seed) — one salt per channel — and every
lattice value afterwards is an integer hash of (cell x, cell y, cell z, salt) built from Math.imul, xor and shift, all of which ECMAScript defines
bit-exactly on every engine. The same seed therefore gives the same field
everywhere and forever, and two fields with the same seed are
indistinguishable. The seed is a uint32 under the same ToUint32 reduction
SeededRandom documents.
Cost
One sample evaluates 12 scalar noises × 8 lattice hashes = 96 hashes. It
is by a wide margin the most expensive field here; wrap it in
volumeField to pay for it only where it shows.
§27's turbulence/noise: a smooth, deterministic, seed-driven swirl field.
Exactly what this is (honesty statement, WP-9.2)
It is the central-difference curl of a three-channel hash-based value-noise vector potential, normalised by the stencil's analytic maximum. Written out, with
s = (position − scroll·time) · frequencyin lattice space andd =TURBULENCE_DIFFERENCE_CELLS:The
/4is the normalisation: each bracket is a difference of two differences of values in[−1, 1], so it lies in[−4, 4], and hence|componentᵢ| ≤ |amplitude|always. That bound is what the tests assert.Limitations — all of them, stated plainly
1 / frequencyworld units. Octaves are staged (they multiply the 96-hash cost per sample by the octave count).0.37·dinstead, the divergence is ≈ 0.54 at the defaults, which is not small. No incompressibility claim is made and none is tested. Treat "curl of a potential" as a description of the construction — it is what makes the field swirl instead of converging on sinks — not as physics.amplitudeis an upper bound, not a typical magnitude. The worst case needs four lattice corners to hit±1in the right pattern. Measured over 20 000 seeded sample points (60 000 components) spread over ±50 world units, at frequencies 0.25, 1 and 4: the RMS of a single component is ≈ 0.186·amplitude and the largest component seen is ≈ 0.76·amplitude, essentially independent of frequency. Author with that factor in mind, or measure your own configuration;tests/fields.test.tspins a conservative version of the same statement.ToUint32inside the hash, so coordinates beyond ±2³¹ cells alias back onto the pattern. At the default frequency that is ±2 billion world units.NaNout. Nothing is guarded on the hot path.ψcC², so the sampled acceleration is C² in space and Lipschitz — no popping as a particle crosses a cell boundary.Determinism (§33)
Math.randomis never called. The constructor draws exactly threeuint32s fromnew SeededRandom(seed)— one salt per channel — and every lattice value afterwards is an integer hash of(cell x, cell y, cell z, salt)built fromMath.imul, xor and shift, all of which ECMAScript defines bit-exactly on every engine. The same seed therefore gives the same field everywhere and forever, and two fields with the same seed are indistinguishable. The seed is a uint32 under the sameToUint32reduction SeededRandom documents.Cost
One sample evaluates 12 scalar noises × 8 lattice hashes = 96 hashes. It is by a wide margin the most expensive field here; wrap it in volumeField to pay for it only where it shows.