---
title: @vgpu/wgsl-std/noise/simplex
description: 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.
---

# @vgpu/wgsl-std/noise/simplex



## Import

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

## Signature

```wgsl
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

| Param      | Type    | Required | Default | Notes                                                                                                                                                                                                       |
| ---------- | ------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| position   | `vec2f` | ✔        | —       | 2D sample position for `simplex2d` / `fbmSimplex2d`. One unit is one lattice cell; scale the input to set the feature size.                                                                                 |
| position   | `vec3f` | ✔        | —       | 3D sample position for `simplex3d` / `fbmSimplex3d`. Passing time as Z animates a 2D field, but see the note on unbounded time below.                                                                       |
| octaves    | `i32`   | ✔        | —       | Number of fBM octaves. **Silently clamped to `[1, 16]`**: an unbounded dynamic loop count is a GPU-hang risk. `1` returns exactly the base noise.                                                           |
| lacunarity | `f32`   | ✔        | —       | Per-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.                                                |
| gain       | `f32`   | ✔        | —       | Per-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

| fn          | guaranteed range | max after normalization | observed max | sigma     | max slope | hashes per call |
| ----------- | ---------------- | ----------------------- | ------------ | --------- | --------- | --------------- |
| `simplex2d` | `(-1, 1)`        | 0.98786                 | 0.9878       | **0.533** | ≈6.95     | 3 × `pcg2d`     |
| `simplex3d` | `(-1, 1)`        | 0.98854                 | 0.9884       | **0.388** | ≈6.53     | 4 × `pcg3d`     |

Extremes are rare: with sigma ≈ 0.39–0.53 most samples sit well inside the range. **Remap, do not saturate** — `saturate(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

```ts
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"));
```

```ts
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"));
```

```ts
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"));
```

```ts
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.6`** — `tests/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`).


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)