Skip to content
Skillv1.0.0

noise-and-fields

Use when a generated texture shows a visible seam or repeats obviously, when procedural detail vanishes at 2 m, or when picking between Perlin/worley/ridged/fbm. Provides a tileable, sin-free, period-

by Amon20044(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from Amon20044/threejs-skills (nodes/10-material/noise-and-fields/SKILL.md). Install upstream with npx skills add Amon20044/threejs-skills --skill noise-and-fields. Copyright stays with the author.

Noise and Fields

Intent

Provide the primitive every procedural surface is built from: noise that wraps exactly at the tile boundary, hashes that do not band on real GPUs, and a vocabulary of field types (fbm, ridged, billow, worley, voronoi edges, domain warp) with a stated use for each.

Everything downstream — textures, terrain, cloud decks, crack networks, wear masks — is a composition of these.

When to use

  • Generating any texture, height field, mask, or scatter distribution on the GPU.
  • Any time a surface must tile seamlessly across a wall, a ground plane, or a mesh with repeating UVs.

When not to

  • CPU-side one-shot values — use the seeded PRNG (deterministic-random) instead. GPU noise is for fields, indexed by position; the PRNG is for streams.

Contract

Every function takes a period in lattice cells and wraps its hash lattice with mod(). A texture generated over uv ∈ [0,1) with p = uv * per therefore tiles seamlessly. Octaves double both frequency and period, which keeps the whole fbm stack seamless rather than just its first octave.

float owNoise (vec2 p, vec2 per);                          // periodic Perlin, ~[-1,1]
float owNoise01(vec2 p, vec2 per);                         // [0,1]
float owValue (vec2 p, vec2 per);                          // blockier value noise
float owFbm   (vec2 p, vec2 per, int oct, float gain);     // ~[-1,1]
float owFbm01 (vec2 p, vec2 per, int oct, float gain);     // [0,1]
float owRidged(vec2 p, vec2 per, int oct, float gain);     // sharp creases
float owBillow(vec2 p, vec2 per, int oct, float gain);     // puffy clumps
vec2  owWarp  (vec2 p, vec2 per, float amp, int oct);      // domain warp
vec4  owWorley(vec2 p, vec2 per, float jitter);            // .x=F1 .y=F2 .zw=cell id
float owVoronoiEdge(vec2 p, vec2 per, float jitter);       // distance to cell edge
float owCracks(vec2 p, vec2 per, float j, float w, float b);
float owScratches(vec2 p, vec2 per, float stretch, float k, float thin);

The period argument is not optional and not decorative. Passing a period that does not divide the lattice is the single most common cause of a visible seam.

Implementation

Hashes: sin-free

float owHash12(vec2 p){
  vec3 p3 = fract(vec3(p.xyx) * 0.1031);
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.x + p3.y) * p3.z);
}
vec2 owHash22(vec2 p){
  vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.xx + p3.yz) * p3.zy);
}

fract(sin(dot(p, k)) * 43758.5453) is the hash everyone reaches for first. It bands visibly on Apple GPUs at high lattice coordinatessin() precision degrades once the argument gets large, and a 1024-cell lattice gets there. The Dave Hoskins style hashes above are pure integer-ish float arithmetic and stay well-distributed everywhere.

Periodic gradient noise

vec2 owGrad2(vec2 i, vec2 per){
  float a = owHash12(mod(i, per) + 0.317) * 6.28318530718;
  return vec2(cos(a), sin(a));
}

float owNoise(vec2 p, vec2 per){
  vec2 i = floor(p), f = fract(p);
  vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);   // quintic: C2 continuous
  float a = dot(owGrad2(i + vec2(0,0), per), f - vec2(0,0));
  float b = dot(owGrad2(i + vec2(1,0), per), f - vec2(1,0));
  float c = dot(owGrad2(i + vec2(0,1), per), f - vec2(0,1));
  float d = dot(owGrad2(i + vec2(1,1), per), f - vec2(1,1));
  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y) * 1.4142;
}

The mod(i, per) inside the gradient lookup is the whole trick. The quintic fade is not cosmetic: the cubic f*f*(3-2f) is only C1, and its second-derivative discontinuity shows up as a faint grid in a normal map derived from the height — invisible in the height itself, obvious once lit.

fbm and its relatives

float owFbm(vec2 p, vec2 per, int oct, float gain){
  float s = 0.0, a = 0.5, n = 0.0;
  for (int i = 0; i < 10; i++){
    if (i >= oct) break;              // GLSL ES 1.00 needs a constant bound
    s += a * owNoise(p, per);
    n += a;
    p *= 2.0; per *= 2.0; a *= gain;  // period doubles WITH frequency
  }
  return s / max(n, 1e-4);            // normalise: gain-independent range
}
variant shape use for
owFbm smooth, cloud-like broad tonal variation, dust, general grain
owRidged (1-|n|)² — sharp creases rock, cracks, mountain silhouettes
owBillow |n| — puffy clumps rust blooms, clay, cumulus
owValue blocky, axis-aligned cell-ish tint variation, brick colour jitter

Normalising by the accumulated amplitude n is what makes gain a shape control rather than a brightness control — change gain and the contrast changes, not the mean.

Worley and the edge distance

vec4 owWorley(vec2 p, vec2 per, float jitter){
  vec2 ip = floor(p), fp = fract(p);
  float f1 = 8.0, f2 = 8.0;
  vec2 id = vec2(0.0);
  for (int y = -1; y <= 1; y++) for (int x = -1; x <= 1; x++){
    vec2 g = vec2(float(x), float(y));
    vec2 cell = mod(ip + g, per);
    vec2 o = owHash22(cell + 0.771) * jitter + (1.0 - jitter) * 0.5;
    vec2 r = g + o - fp;
    float d = dot(r, r);
    if (d < f1){ f2 = f1; f1 = d; id = owHash22(cell + 3.117); }
    else if (d < f2){ f2 = d; }
  }
  return vec4(sqrt(f1), sqrt(f2), id);
}

Returning the cell id in .zw is what makes worley useful beyond blobs: it gives every cell a stable random pair, so pebbles get individual colours, tiles get individual heights, and a step() on .z selects a random subset of cells without a second noise lookup.

For crack networks, F2 - F1 is the obvious choice and it is the wrong one — it produces soft, uneven-width lines that read as a lumpy net. Use Quilez's two-pass distance to the cell edge:

float owVoronoiEdge(vec2 p, vec2 per, float jitter){
  // pass 1: find the owning cell
  vec2 ip = floor(p), fp = fract(p);
  vec2 mr = vec2(0.0), mg = vec2(0.0);
  float md = 8.0;
  for (int y = -1; y <= 1; y++) for (int x = -1; x <= 1; x++){
    vec2 g = vec2(float(x), float(y));
    vec2 o = owHash22(mod(ip + g, per) + 0.771) * jitter + (1.0 - jitter) * 0.5;
    vec2 r = g + o - fp;
    float d = dot(r, r);
    if (d < md){ md = d; mr = r; mg = g; }
  }
  // pass 2: distance to the perpendicular bisector with each neighbour — a
  // 5x5 search, because the true edge can belong to a cell two steps away
  md = 8.0;
  for (int y = -2; y <= 2; y++) for (int x = -2; x <= 2; x++){
    vec2 g = mg + vec2(float(x), float(y));
    vec2 o = owHash22(mod(ip + g, per) + 0.771) * jitter + (1.0 - jitter) * 0.5;
    vec2 r = g + o - fp;
    vec2 diff = r - mr;
    if (dot(diff, diff) > 1e-5)
      md = min(md, dot(0.5 * (mr + r), normalize(diff)));
  }
  return md;
}

Then break the network so it reads as damage rather than as a mesh:

float owCracks(vec2 p, vec2 per, float jitter, float width, float breakUp){
  vec2 wp = owWarp(p, per, 0.20, 3);            // warp first: no straight edges
  float e = owVoronoiEdge(wp, per, jitter);
  float c = 1.0 - smoothstep(0.0, width, e);
  float mask = owFbm01(p * 1.7 + 11.3, per * 1.7, 4, 0.55);
  c *= smoothstep(breakUp, breakUp + 0.28, mask);   // cracks must TERMINATE
  return clamp(c, 0.0, 1.0);
}

A complete, unbroken voronoi network is instantly readable as procedural. Real cracks start somewhere, run, and stop. The mask multiply is what buys that.

Domain warp

vec2 owWarp(vec2 p, vec2 per, float amp, int oct){
  vec2 q = vec2(owFbm(p + vec2(1.7, 9.2), per, oct, 0.5),
                owFbm(p + vec2(8.3, 2.8), per, oct, 0.5));
  return p + amp * q;
}

The cheapest way to make any field stop looking like noise. Amplitudes: 0.1–0.3 for "slightly organic", 0.5–1.2 for "flowing", above 2 for "melted". Warping is periodic because the fbm driving it is.

Anisotropy that survives tiling

Scratches and brushed metal need stretched features. A rotation breaks the lattice wrap; an integer shear does not:

/** k and stretch must be INTEGERS or the lattice no longer wraps on `per`. */
vec2 owShear(vec2 p, float k, float stretch){ return vec2(p.x + p.y * k, p.y * stretch); }
vec2 owShearPer(vec2 per, float stretch){ return vec2(per.x, per.y * stretch); }

float owScratches(vec2 p, vec2 per, float stretch, float k, float thin){
  vec2 q = owShear(p, k, stretch);
  vec2 qper = owShearPer(per, stretch);
  float n = owFbm01(q, qper, 4, 0.5);
  // a thin band of the fbm, not a threshold: gives lines with soft ends
  return smoothstep(thin, thin + 0.06, n) * (1.0 - smoothstep(thin + 0.06, thin + 0.2, n));
}

Nyquist: the reason your detail disappears at 2 m

This is the most expensive lesson in the node.

A 1024 px tile spanning 0.25 m gives 0.244 mm per texel. With p = uv * 8, a term written at p * K puts 8K feature cells across 1024 texels — that is 128/K texels per cell.

K texels per feature result
8 16 solid, survives several mips
20 6.4 ~1.6 mm — the practical floor
24 5.3 marginal
40 3.2 salt-and-pepper dither at mip 0, flat grey at mip 1

The failure presents as "sandpaper in a close-up, featureless at 2 m", and the instinct is to add more high-frequency terms, which makes it strictly worse. The fix is to cap every band at K ≈ 20 and give the surviving bands real amplitude instead.

// 3.9 mm pits and 1.6 mm grains — both wide enough to survive two mip levels
vec4 pores = owWorley(p * 8.0,  P * 8.0,  1.0);
vec4 grit  = owWorley(p * 20.0, P * 20.0, 1.0);

// Proud grains as solid rounded bumps, not threshold specks:
float gritA = smoothstep(0.34, 0.08, pores.x) * step(0.38, pores.z);
float pit   = smoothstep(0.26, 0.00, pores.x) * step(0.72, pores.w);

Note step(0.38, pores.z) — the cell id selecting which cells get a grain. That is free variation that a second noise octave would have cost a full evaluation.

Verify

node kit/testing/tiling.mjs --surface all
  • Seamless. Bake at 512, tile 2×2, compare column 511 against column 0 and row 511 against row 0. Max per-channel delta ≤ 1/255.
  • Seamless in the normal map too. The height can wrap while its Sobel does not, if the Sobel samples off the edge without RepeatWrapping. Check the normal texture with the same test.
  • No banding at high lattice coordinates. Evaluate the hash over p ∈ [0, 4096]² and histogram: flat within 2 %. A sin-based hash fails visibly here.
  • fbm range. owFbm01 over 10⁶ samples stays in [0,1]; mean 0.5 ± 0.02 for every gain ∈ [0.4, 0.7].
  • Nyquist audit. For each surface, list every p * K term and compute 128/K × (worldSize/0.25) texels per feature. Nothing below 5.
  • Mip survival. Render the tile at 1 m, 2 m, 4 m, 8 m. Compute per-image luminance variance; it must decay smoothly, not collapse between two adjacent distances.
  • Determinism. Same seed, same GPU, two bakes → bit-identical.
  • Cracks terminate. Threshold the crack field and count connected components; a broken network has many, a mesh has one.

Failure modes

symptom cause fix
Visible seam at the tile edge period not passed, or not doubled per octave thread per through everything
Seam only in the lighting normal map baked without RepeatWrapping on the height RT set wrap on the scratch target
Diagonal banding on Apple GPUs sin()-based hash Hoskins-style hash
Faint grid in the normal map cubic fade instead of quintic f³(f(6f-15)+10)
Sandpaper close up, flat at 2 m features above Nyquist cap at ~6 texels/feature, raise amplitude
Cracks read as a net F2-F1, and no break-up mask owVoronoiEdge + fbm mask
Brushed metal seams rotation used for anisotropy integer shear
Changing gain changes brightness fbm not normalised by accumulated amplitude divide by n
Detail looks like static in motion high-frequency term aliasing under TAA lower K; TAA cannot fix sub-texel content

Extend

  • kit/glsl/noise.glsl.js — the full library as an exportable GLSL string.
  • Baking these fields into PBR texture sets: texture-forge.
  • Composing them into named surfaces: surface-authoring.
  • The 3-D CPU-side equivalents for mesh deformation: organic-forms.

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/amon20044-threejs-skills-noise-and-fields/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

amon20044-threejs-skills-noise-and-fields.ocm.jsonjson
{
  "ocm": "1",
  "id": "amon20044-threejs-skills-noise-and-fields",
  "kind": "skill",
  "name": "noise-and-fields",
  "description": "Use when a generated texture shows a visible seam or repeats obviously, when procedural detail vanishes at 2 m, or when picking between Perlin/worley/ridged/fbm. Provides a tileable, sin-free, period-aware GLSL noise library.",
  "publisher": "Amon20044",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "general"
    ],
    "tags": [
      "skill-md",
      "glsl",
      "noise",
      "procedural",
      "tiling",
      "github"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "Use when a generated texture shows a visible seam or repeats obviously, when procedural detail vanishes at 2 m, or when picking between Perlin/worley/ridged/fbm. Provides a tileable, sin-free, period-aware GLSL noise library."
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "github",
      "repository": "https://github.com/Amon20044/threejs-skills",
      "path": "nodes/10-material/noise-and-fields/SKILL.md",
      "ref": "62a308ab61b77c56de471c58d1bbdb9b6144cc76",
      "url": "https://github.com/Amon20044/threejs-skills/blob/62a308ab61b77c56de471c58d1bbdb9b6144cc76/nodes/10-material/noise-and-fields/SKILL.md",
      "key": "Amon20044/threejs-skills/nodes/10-material/noise-and-fields/SKILL.md"
    }
  },
  "instructions": "# Noise and Fields\n\n## Intent\n\nProvide the primitive every procedural surface is built from: noise that **wraps\nexactly** at the tile boundary, hashes that do not band on real GPUs, and a vocabulary of\nfield types (fbm, ridged, billow, worley, voronoi edges, domain warp) with a stated use\nfor each.\n\nEverything downstream — textures, terrain, cloud decks, crack networks, wear masks —\nis a composition of these.\n\n## When to use\n\n- Generating any texture, height field, mask, or scatter distribution on the GPU.\n- Any time a surface must tile seamlessly across a wall, a ground plane, or a mesh with\n",
  "cost": {
    "context_tokens": 3110
  }
}

Fetch it by URL: GET /api/v1/registry/amon20044-threejs-skills-noise-and-fields/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.