@vgpu/wgsl-std4 symbolsView source ↗

Symbols in this topic

@vgpu/wgsl-std/noise/simplex

Pure WGSL simplex noise (simplex2d, simplex3d) and amplitude-normalized fBM (fbmSimplex2d, fbmSimplex3d) for resolver-managed shaders. Import these for smooth gradient noise — clouds, terrain, water, plasma — when you want the cheaper, higher-frequency sibling of @vgpu/wgsl-std/noise/perlin. For cell/edge patterns use @vgpu/wgsl-std/noise (Voronoi) instead.

Import

WGSL
1
import { simplex2d, simplex3d, fbmSimplex2d, fbmSimplex3d } from "@vgpu/wgsl-std/noise/simplex";

Signature

WGSL
1
2
3
4
export fn simplex2d(position: vec2f) -> f32;
export fn simplex3d(position: vec3f) -> f32;
export fn fbmSimplex2d(position: vec2f, octaves: i32, lacunarity: f32, gain: f32) -> f32;
export fn fbmSimplex3d(position: vec3f, octaves: i32, lacunarity: f32, gain: f32) -> f32;

Parameters

ParamTypeRequiredDefaultNotes
positionvec2f2D sample position for simplex2d / fbmSimplex2d. One unit is one lattice cell; scale the input to set the feature size.
positionvec3f3D sample position for simplex3d / fbmSimplex3d. Passing time as Z animates a 2D field, but see the note on unbounded time below.
octavesi32Number of fBM octaves. Silently clamped to [1, 16]: an unbounded dynamic loop count is a GPU-hang risk. 1 returns exactly the base noise.
lacunarityf32Per-octave frequency multiplier. Not clamped. 2.0 is the usual choice; a slightly irrational value such as 2.17 avoids octaves lining up on the lattice.
gainf32Per-octave amplitude multiplier. Silently clamped to [0, 1]: a negative gain would break the amplitude sum the range guarantee rests on. 0.5 is the usual choice; 0 collapses to a single octave.

Returns: f32 in the open interval (-1, 1) — never clipped, for every finite input, including all fBM parameter combinations. The bound is a proof, not an observation: the normalizers (98.0 for 2D, 76.0 for 3D) sit just below 1 / sup of the raw kernel sum (0.0100802047 and 0.0130071572), so abs(simplex2d) <= 0.98786 and abs(simplex3d) <= 0.98854; fBM divides by the sum of the amplitudes, so the bound survives octaves.

Throws: These WGSL declarations do not throw. resolveShader() can still throw VGPU-WGSL-SYM-NOEXPORT for a misspelled import (note the module is @vgpu/wgsl-std/noise/simplex, not @vgpu/wgsl-std/noise), VGPU-WGSL-PKG-NOTFOUND if the package import cannot be resolved, or validation errors such as VGPU-WGSL-NAGA-UNKNOWN if the calling WGSL is invalid. This module reaches @vgpu/wgsl-std/hash through a private gradient core, so package resolution must be able to resolve that package export too.

Measured field properties

fnguaranteed rangemax after normalizationobserved maxsigmamax slopehashes per call
simplex2d(-1, 1)0.987860.98780.533≈6.953 × pcg2d
simplex3d(-1, 1)0.988540.98840.388≈6.534 × pcg3d

Extremes are rare: with sigma ≈ 0.39–0.53 most samples sit well inside the range. Remap, do not saturatesaturate(simplex3d(p)) throws away the whole negative half of the field. Use remap(-0.6, 0.6, 0.0, 1.0, value) from @vgpu/wgsl-std/math and clamp afterwards if you need [0, 1].

Examples

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
const simplexWgsl = `
import { simplex3d } from "@vgpu/wgsl-std/noise/simplex";
import { remap, saturate } from "@vgpu/wgsl-std/math";
 
// A single octave, remapped into [0, 1] for use as a mask.
fn smoke(position: vec2f, time: f32) -> f32 {
  let value = simplex3d(vec3f(position * 3.0, time * 0.2));
  return saturate(remap(-0.6, 0.6, 0.0, 1.0, value));
}
`;
 
console.log(simplexWgsl.includes("smoke"));
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const cloudWgsl = `
import { fbmSimplex3d } from "@vgpu/wgsl-std/noise/simplex";
import { remap, saturate } from "@vgpu/wgsl-std/math";
 
// Animated clouds: fBM over a domain-warped field.
// Offsets are >= 2 units apart so the three warp components are decorrelated.
fn cloudCoverage(position: vec3f, time: f32) -> f32 {
  let drift = vec3f(position.xy + vec2f(time * 0.02, 0.0), position.z + time * 0.05);
 
  // Domain warp: displace the sample point with a low-frequency field.
  let warp = vec3f(
    fbmSimplex3d(drift, 3, 2.0, 0.5),
    fbmSimplex3d(drift + vec3f(37.2, 11.7, 5.3), 3, 2.0, 0.5),
    fbmSimplex3d(drift + vec3f(-19.4, 42.1, 23.8), 3, 2.0, 0.5),
  );
 
  // Detail octaves on the warped domain, then shape coverage.
  let field = fbmSimplex3d(drift + warp * 0.45, 5, 2.0, 0.5);
  let coverage = saturate(remap(-0.15, 0.65, 0.0, 1.0, field));
  return coverage * coverage * (3.0 - 2.0 * coverage);
}
`;
 
console.log(cloudWgsl.includes("cloudCoverage"));
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const shapedWgsl = `
import { simplex3d } from "@vgpu/wgsl-std/noise/simplex";
 
// Turbulence (billowy): sum abs(noise) per octave -> [0, 1).
// Must be per-octave; abs(fbmSimplex3d(...)) is NOT the same thing.
fn turbulenceSimplex3d(position: vec3f, octaves: i32) -> f32 {
  var sum = 0.0;
  var amplitude = 1.0;
  var weight = 0.0;
  var sample = position;
  for (var i = 0; i < octaves; i = i + 1) {
    sum = sum + amplitude * abs(simplex3d(sample));
    weight = weight + amplitude;
    sample = sample * 2.0;
    amplitude = amplitude * 0.5;
  }
  return sum / weight;
}
 
// Ridged (terrain): replace abs(n) with a squared (1.0 - abs(n)), per octave.
fn ridgedSimplex3d(position: vec3f, octaves: i32) -> f32 {
  var sum = 0.0;
  var amplitude = 1.0;
  var weight = 0.0;
  var sample = position;
  for (var i = 0; i < octaves; i = i + 1) {
    let ridge = 1.0 - abs(simplex3d(sample));
    sum = sum + amplitude * ridge * ridge;
    weight = weight + amplitude;
    sample = sample * 2.0;
    amplitude = amplitude * 0.5;
  }
  return sum / weight;
}
`;
 
console.log(shapedWgsl.includes("ridgedSimplex3d"));
TypeScript
1
2
3
4
5
6
7
8
9
10
const seededWgsl = `
import { simplex2d } from "@vgpu/wgsl-std/noise/simplex";
 
// There is no seed parameter: offset the input instead, by >= 2.0 units.
fn twoIndependentFields(position: vec2f) -> vec2f {
  return vec2f(simplex2d(position), simplex2d(position + vec2f(37.0, -19.0)));
}
`;
 
console.log(seededWgsl.includes("twoIndependentFields"));

Notes

  • Kernel radius² is 0.5, not the widespread 0.6. The canonical Gustavson/webgl-noise value has a support radius of 0.775, which exceeds the reach of the 3-corner (2D) / 4-corner (3D) traversal, so corners that still carry a non-zero contribution get dropped at a simplex face: a genuine C0 crack. Measured with a dense line scan (step 1e-6): max |Δv| is 9.5e-5 / 4.6e-5 (raw field) at 0.6 versus 4.8e-8 / 2.9e-8 at 0.5, i.e. ~1000× worse, ~0.8% of the normalized range. It is invisible in a flat colour ramp and obvious as a seam in normals or high-contrast ramps. 0.5 is not merely "smaller and safer": it is exactly the largest radius² for which every dropped corner is already outside the kernel (equality is attained, in 2D, at d = (G2 - 0.5, 0.5)). Do not "fix" this back to 0.6tests/simplex.test.ts fails loudly, by design, if you do.
  • Simplex vs Perlin at the same input scale: ~2.5× the slope and ~1.7× the sigma. simplex3d(p) is not "perlin3d(p) but faster" — it is a higher-frequency, higher-contrast field. When migrating, scale p by ~0.4–0.5 (simplex3d(p * 0.45)) to get a comparable look.
  • Cost: simplex3d costs 4 pcg3d hashes against perlin3d's 8, so it is roughly half the price per call — but a fair comparison has to account for the frequency difference above: matching Perlin's look means sampling a scaled-down domain, not a cheaper field. A 6-octave fbmSimplex3d is 24 pcg3d hashes; the cloud recipe above (3 warp fields at 3 octaves plus 5 detail octaves) is ~14 simplex3d calls, i.e. ~56 hashes per pixel. Prefer simplex2d where the third axis is only animation.
  • Seeding is by input offset; there is no seed parameter (same convention as voronoi2d/voronoi3d). Measured Pearson correlation between p and p + offset: (0.5,0,0) → 0.400, (1,0,0) → −0.043, (2,0,0) → 0.0017, (17,0,0) → −0.0004, (101,53,7) → 0.0035. Any offset of ≥ 2.0 units on some axis decorrelates; sub-cell offsets do not. Keep offsets in the tens–thousands, not 1e6, because of the f32 mantissa.
  • Period is 2³² cells — the gradient index comes from pcg2d/pcg3d over the integer cell, not from a float mod 289 permutation, so there is no visible tiling.
  • f32 domain: the fractional part p - floor(p) is quantized to ~2⁻²⁴·|p|, so detail visibly bands beyond |p| ≈ 1e4 and vanishes entirely at |p| >= 2²³. fBM multiplies the effective coordinate by lacunarity^(octaves - 1) (6 octaves at λ=2 → 32×), so it reaches that limit 32× sooner. An animation that feeds unbounded time into the third axis degrades after long sessions — wrap or scale time (fract(time * 0.05) * 20.0, or reset a session clock), do not let it grow without bound.
  • Unlike Perlin, simplex has no zero on the integer grid: the simplex lattice is sheared, so integer coordinates are generically interior points. The field is exactly 0 at each lattice vertex of the sheared grid (e.g. simplex3d(vec3f(0.5)) is 0.0), because there the gradient offset is the zero vector and every other corner lies outside the kernel. Do not build a lattice-zero assumption on top of simplex2d/simplex3d.
  • This module is pure WGSL: no @group, no @binding, no overrides, no entry points, no hidden state. It contains no lookup table (no array<...>) and no sin/cos/sqrt/inverseSqrt/pow: those have implementation-defined accuracy, so an angle-based gradient would drift per driver. Gradient selection is therefore bit-identical on every backend; only the final interpolation may differ by a few ulp.
  • The module returns a scalar field only. It does not choose colours, coverage curves, warp strength, animation speed, or material policy — the recipes above are examples to copy and tune, not policy the module enforces.
  • Algorithmic references (no code copied, no third-party licence obligation incurred): Perlin 2001, Hardware-accelerated procedural texturing / simplex construction; Stefan Gustavson, Simplex noise demystified. The US patent on simplex noise (US 6,867,776 B2) expired in 2022.
  • See also: @vgpu/wgsl-std/noise/perlin (perlin2d/perlin3d/fbmPerlin2d/fbmPerlin3d — smoother, exactly 0 on the integer lattice, ~2.5× less slope), @vgpu/wgsl-std/noise (Voronoi cells), @vgpu/wgsl-std/math (remap, saturate), @vgpu/wgsl-std/hash (pcg2d, pcg3d).