tower / interior-flight / files / tools / noise.mjs
Branch interior-flight · default branch · all branches
noise.mjs
| 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 | } |