tower / cascadia / files / tools / noise.mjs
Branch cascadia · default branch · all branches
noise.mjs
1de9dc7c 2026-09-07 Pavel Safronov | 1 | /** |
| 2 | * Deterministic value noise with fBm, wrapping on the y (theta) axis. | |
| 3 | * | |
| 4 | * The map is a cylinder: y wraps and x does not. Noise that ignores that leaves a | |
| 5 | * visible seam down the map, so the lattice wraps at a caller-supplied period. | |
| 6 | */ | |
| 7 | ||
| 8 | /** | |
| 9 | * 32-bit integer hash. Uses Math.imul throughout: the obvious float version | |
| 10 | * silently loses precision on large products and leaves axis-aligned streaks | |
| 11 | * across the terrain. | |
| 12 | */ | |
| 13 | function hash(x, y, seed) { | |
| 14 | let h = Math.imul(x | 0, 374761393) ^ Math.imul(y | 0, 668265263) ^ Math.imul(seed | 0, 1274126177) | |
| 15 | h = Math.imul(h ^ (h >>> 15), 2246822519) | |
| 16 | h = Math.imul(h ^ (h >>> 13), 3266489917) | |
| 17 | return ((h ^ (h >>> 16)) >>> 0) / 4294967295 | |
| 18 | } | |
| 19 | ||
| 20 | const smooth = (t) => t * t * (3 - 2 * t) | |
| 21 | ||
| 22 | /** Value noise at (x, y) on a lattice, wrapping y every `wrapY` cells. */ | |
| 23 | function value(x, y, seed, wrapY) { | |
| 24 | const xi = Math.floor(x) | |
| 25 | const yi = Math.floor(y) | |
| 26 | const xf = smooth(x - xi) | |
| 27 | const yf = smooth(y - yi) | |
| 28 | const wrap = (v) => (wrapY > 0 ? ((v % wrapY) + wrapY) % wrapY : v) | |
| 29 | const y0 = wrap(yi) | |
| 30 | const y1 = wrap(yi + 1) | |
| 31 | const a = hash(xi, y0, seed) | |
| 32 | const b = hash(xi + 1, y0, seed) | |
| 33 | const c = hash(xi, y1, seed) | |
| 34 | const d = hash(xi + 1, y1, seed) | |
| 35 | return (a + (b - a) * xf) * (1 - yf) + (c + (d - c) * xf) * yf | |
| 36 | } | |
| 37 | ||
| 38 | /** Fractal Brownian motion in [0,1]. `wrapY` is the lattice period on the y axis. */ | |
| 39 | export function fbm(x, y, { octaves = 4, seed = 1, wrapY = 0, lacunarity = 2, gain = 0.5 } = {}) { | |
| 40 | let sum = 0 | |
| 41 | let amp = 1 | |
| 42 | let norm = 0 | |
| 43 | let freq = 1 | |
| 44 | for (let o = 0; o < octaves; o++) { | |
| 45 | sum += value(x * freq, y * freq, seed + o * 101, wrapY * freq) * amp | |
| 46 | norm += amp | |
| 47 | amp *= gain | |
| 48 | freq *= lacunarity | |
| 49 | } | |
| 50 | return sum / norm | |
| 51 | } | |
| 52 | ||
| 53 | /** Ridged fBm — absolute-valued and inverted, which makes mountain crests. */ | |
| 54 | export function ridged(x, y, opts = {}) { | |
| 55 | const { octaves = 4 } = opts | |
| 56 | let sum = 0 | |
| 57 | let amp = 1 | |
| 58 | let norm = 0 | |
| 59 | let freq = 1 | |
| 60 | for (let o = 0; o < octaves; o++) { | |
| 61 | const v = 1 - Math.abs(value(x * freq, y * freq, (opts.seed ?? 1) + o * 211, (opts.wrapY ?? 0) * freq) * 2 - 1) | |
| 62 | sum += v * v * amp | |
| 63 | norm += amp | |
| 64 | amp *= 0.5 | |
| 65 | freq *= 2 | |
| 66 | } | |
| 67 | return sum / norm | |
| 68 | } |