Documentation
    Preparing search index...

    Function turbulenceField

    • §27's turbulence/noise: a smooth, deterministic, seed-driven swirl field.

      const gust = turbulenceField(1337, { frequency: 0.5, amplitude: 3 });
      

      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:

      ψc(s)  = trilinear-interpolated value noise, channel c ∈ {x, y, z},
               quintic fade, lattice values hashed from the seed;  |ψc| ≤ 1
      
      aₓ = (amplitude / 4) · [ (ψz(s + d·ŷ) − ψz(s − d·ŷ)) − (ψy(s + d·ẑ) − ψy(s − d·ẑ)) ]
      a_y = (amplitude / 4) · [ (ψx(s + d·ẑ) − ψx(s − d·ẑ)) − (ψz(s + d·x̂) − ψz(s − d·x̂)) ]
      a_z = (amplitude / 4) · [ (ψy(s + d·x̂) − ψy(s − d·x̂)) − (ψx(s + d·ŷ) − ψx(s − d·ŷ)) ]
      

      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.

      • 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.

      Math.random is never called. The constructor draws exactly three uint32s 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.

      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.

      Parameters

      Returns ParticleForceField

      RangeError if seed is not finite (via SeededRandom), if frequency is not finite or not > 0, or if amplitude or any component of scroll is not finite.