Skip to content

Helix Noise — Rust Reference

The helix-noise crate is a Rust port of the JavaScript reference library: a divergence-free helical (Beltrami) spectral flow field you can evaluate grid-free at any point in space and time. Its vorticity (curl) and vector potential come out in closed form, which makes it useful for curl-noise particle advection, procedural vector textures, animated tileable flow fields, and GPU flow shaders.

The crate has zero runtime dependencies and no threads or I/O in the hot path, so it compiles cleanly to WebAssembly.

Ports: JavaScript · Python · Rust (this page) · Shaders · React · Project home

Registry & source: crates.io · API docs on docs.rs · GitHub source

Install

sh
cargo add helix-noise

Or add it to Cargo.toml:

toml
[dependencies]
helix-noise = "0.1"

Quickstart

rust
use helix_noise::{HelixField, HelixOptions};

// Build a field. Override any option; leave the rest at their defaults.
let field = HelixField::new(HelixOptions { seed: 42, modes: 48, ..Default::default() });

// Velocity at a point.
let u = field.sample(1.0, 2.0, 3.0);

// Velocity animated in time.
let u_t = field.sample_t(1.0, 2.0, 3.0, 0.5);

// Velocity + vorticity together (one pass).
let (u, w) = field.sample_uw(1.0, 2.0, 3.0, 0.0);

// Velocity + analytic vector potential together.
let (u, a) = field.sample_ua(1.0, 2.0, 3.0, 0.0);

HelixField::create(opts) is a convenience alias for HelixField::new(opts).

Custom spectrum

Override the default |k|^-slope power law with any callable:

rust
use helix_noise::{HelixField, HelixOptions};
let field = HelixField::new(HelixOptions {
    spectrum: Some(Box::new(|k: f64| (-k).exp())),
    ..Default::default()
});

Options

Every field has a sensible default; build one with HelixOptions::default() and override individual fields with struct-update syntax.

FieldTypeDefaultMeaning
modesusize48Number of helical modes. Sample cost is O(modes).
slopef641.6Spectral slope s: amplitude ~ |k|^-s (steeper = bigger swirls).
helicityf640.0p in [-1, 1]: energy split between +/- helical states.
coherencef640.0lambda in [0, 1]: phases random → structured.
kminf641.0Smallest wavenumber (largest structures).
kmaxf646.2Largest wavenumber (finest detail).
centersi643Focus points the coherent phases organize toward.
amplitudef641.0Output scale (field is normalized to unit RMS speed first).
tileableboolfalseSnap wavevectors to the integer lattice → exactly -periodic.
seedu321PRNG seed (0 is treated as 1).
layoutLayoutLayout::FibonacciFibonacci (low-discrepancy) or Random (i.i.d. ensemble).
churnf641.0Time-evolution rate: eddy-turnover churn + structure sweep.
decayf640.0Viscosity nu >= 0: amplitudes decay as e^(-nu k² t).
anisotropyf640.0Direction stretch along axis (<0 streaks, >0 layers).
axis[f64; 3][0, 0, 1]Anisotropy axis.
spectrumOption<Box<dyn Fn(f64) -> f64>>NoneCustom amplitude law; overrides |k|^-slope.

The spectrum field type is aliased as SpectrumFn (Box<dyn Fn(f64) -> f64>).

API

MethodReturnsDescription
HelixField::new(opts) / create(opts)HelixFieldBuild a field.
sample(x, y, z)[f64; 3]Velocity at time 0.
sample_t(x, y, z, t)[f64; 3]Velocity at time t.
sample_uw(x, y, z, t)([f64; 3], [f64; 3])Velocity and vorticity.
sample_ua(x, y, z, t)([f64; 3], [f64; 3])Velocity and vector potential.
vorticity(x, y, z, t)[f64; 3]Curl of the velocity.
helicity_density(x, y, z, t)f64u · w.
potential(x, y, z, t)[f64; 3]Vector potential A with curl(A) = u.
relative_helicity(ng)f64Mean relative helicity over an ng³ grid, in [-1, 1].
bake3d(n, t)Vec<f32> RGBA volume: (u.x, u.y, u.z, u·w).
bake2d(nx, ny, z, t)Vec<f32>nx·ny RGBA slice at constant z.
bake_potential3d(n, t)Vec<f32> RGBA volume: (A, u·w).
with_boundary(sdf, opts)BoundedFieldConstrain the field with a free-slip SDF obstacle.
glsl(&opts)StringEmit a self-contained GLSL ES 3.00 / WebGL2 shader.
mode_snapshot()ModeSnapshotRead-only snapshot of the built spectral arrays.
modes()usizeNumber of modes.
options()&HelixOptionsThe resolved options this field was built from.

mode_snapshot() returns a ModeSnapshot — a Clone/Debug struct exposing the per-mode arrays (kx, ky, kz, km, a, s, ph, om, the transverse frame e1*/e2*, plus nu, scale, and the mode count n) for diagnostics, serialization, and cross-port parity checks.

Boundaries (free-slip SDF)

Wrap a field with a signed-distance obstacle. The bounded velocity is the curl of a ramped vector potential — divergence-free by construction, tangent to the wall, zero inside the obstacle, and identical to the base field beyond the influence band.

rust
use helix_noise::{HelixField, HelixOptions, BoundaryOptions};

let field = HelixField::new(HelixOptions::default());
let sphere = |x: f64, y: f64, z: f64|
    ((x - 3.0).powi(2) + (y - 3.0).powi(2) + (z - 3.0).powi(2)).sqrt() - 1.2;

let bounded = field.with_boundary(sphere, BoundaryOptions { thickness: 0.9, ..Default::default() });
let u = bounded.sample(2.0, 2.0, 2.0, 0.0);

BoundedField mirrors the sampling surface of the base field, with the obstacle applied: sample, sample_uw, vorticity, helicity_density, and potential (each taking (x, y, z, t)). Bounded vorticity is taken by central differences of the bounded velocity itself.

BoundaryOptions

FieldTypeDefaultMeaning
thicknessf641.0Width of the influence band, in world units. Clamped to >= 1e-9.
fd_stepf641e-3Finite-difference step for numerical gradients (SDF gradient and bounded vorticity).
gradientOption<Box<dyn Fn(f64, f64, f64) -> [f64; 3]>>NoneOptional analytic SDF gradient grad(d); when absent, central differences are used.

GLSL / GPU

Emit a self-contained GLSL ES 3.00 / WebGL2 shader that evaluates this exact field on the GPU. The mode arrays are baked as GLSL constants; the shader does not regenerate the RNG.

rust
use helix_noise::{HelixField, HelixOptions, GlslOptions};
let field = HelixField::new(HelixOptions::default());
let src = field.glsl(&GlslOptions { name: "myFlow".into(), ..Default::default() });
// `src` defines vec3 myFlow(vec3 p) / (vec3 p, float t) and myFlowCurl, ready to paste
// into a GLSL ES 3.00 / WebGL2 shader.

GlslOptions

FieldTypeDefaultMeaning
nameString"helixNoise"Base function name. Sanitized to [A-Za-z0-9_].
precisionusize7Significant digits for baked float literals.
curlbooltrueAlso emit the <name>Curl (vorticity) pair.
potentialboolfalseAlso emit the <name>Pot (vector potential) pair.

WebAssembly

The core has no threads and no I/O in the hot path, so it compiles cleanly to wasm32-unknown-unknown. To expose it to JavaScript, add wasm-bindgen in your own binding crate (it is deliberately not a dependency here) and wrap the API, e.g.:

rust
use wasm_bindgen::prelude::*;
use helix_noise::{HelixField, HelixOptions};

#[wasm_bindgen]
pub struct Field(HelixField);

#[wasm_bindgen]
impl Field {
    #[wasm_bindgen(constructor)]
    pub fn new(seed: u32, modes: usize) -> Field {
        Field(HelixField::new(HelixOptions { seed, modes, ..Default::default() }))
    }
    pub fn sample(&self, x: f64, y: f64, z: f64) -> Vec<f64> {
        self.0.sample(x, y, z).to_vec()
    }
}

Then build with wasm-pack build.

Parity

This crate is a port of the JavaScript reference with numerical parity: the deterministic mulberry32 mode-construction stream is bit-identical across languages, so a field built with the same options and seed reproduces the reference values to floating-point tolerance (transcendental functions differ by ~1 ULP).

The crate's tests/ directory rebuilds a shared fixture (parity_fixture.json) — six configurations covering defaults, helical/coherent, random/anisotropic, viscous decay in time, tileable, and a boundary case — and asserts the full mode arrays, u/w/A samples, relative helicity, and bake sums all match the JS reference within abs+rel 1e-9 (1e-7 for float32 bake sums). A further test checks the emitted GLSL for the default config against the reference shader.

sh
cargo test

The library version is exposed as the VERSION constant. Two further public items are the TAU constant () and ga() (the golden angle, π · (3 − √5)), used by the Fibonacci layout. The deterministic PRNG itself is public as Mulberry32 (Mulberry32::new(seed) / Mulberry32::seeded(seed) / next_f64()), so you can reproduce the exact mulberry32 draw stream the mode construction consumes.

Scope

v0.1 covers the spectral engine, the free-slip SDF boundary, and the GLSL emitter. The particle "atom" engine of the JS reference (createAtoms) is a documented follow-up and is not yet ported — it is out of scope for this release.

Relative to the JavaScript reference and the Python port, this crate also omits the batched samplers (sampleMany / sampleManyUW), in-place set() re-tuning, and selfTest(): sample in your own loop, rebuild with HelixField::new, and rely on cargo test for validation. The single-point sampling surface, boundaries, bakes, and GLSL emit are at full parity.

License

MIT © Rifat Jumagulov. Port of the JavaScript helix-noise library.


See also: JavaScript reference · Python port · Shaders · Project home · crates.io · docs.rs · GitHub

Released under the MIT License.