fuzzyworld

tower / files / src / terrain / noise.ts

noise.ts

4.0 KB · history · blame

1/**
2 * Seeded noise, evaluated on the cylinder itself. See world-generation.md §5.3, §5.5.
3 *
4 * **Why 3D noise for a 2D surface.** The surface wraps around theta. v1 sampled 2D
5 * value noise on the unrolled map and had to snap the lattice to a whole number of
6 * cells around the circumference, or the terrain tore at the seam (D-019). Sampling
7 * 3D simplex noise at the actual point on the cylinder, (R cos θ, R sin θ, z), has no
8 * seam to tear: theta = 0 and theta = 2π are the same point in space, so they get the
9 * same value by construction.
10 *
11 * Everything here is deterministic. There is no Math.random: randomness comes from a
12 * seed, through `mulberry32`, and the same seed gives the same world on every machine.
13 */
14import { createNoise3D, type NoiseFunction3D } from 'simplex-noise'
15import { RADIUS_M } from '../canon/constants.js'
16
17/**
18 * 32-bit integer hash of up to three integers. Math.imul throughout: the obvious
19 * float version silently loses precision on large products (D-019).
20 */
21export function hash3(a: number, b: number, c: number): number {
22 let h = Math.imul(a | 0, 374761393) ^ Math.imul(b | 0, 668265263) ^ Math.imul(c | 0, 1274126177)
23 h = Math.imul(h ^ (h >>> 15), 2246822519)
24 h = Math.imul(h ^ (h >>> 13), 3266489917)
25 return (h ^ (h >>> 16)) >>> 0
26}
27
28/** A seeded PRNG in [0, 1). Small, fast, and good enough for building permutation tables. */
29export function mulberry32(seed: number): () => number {
30 let a = seed >>> 0
31 return () => {
32 a = (a + 0x6d2b79f5) | 0
33 let t = Math.imul(a ^ (a >>> 15), 1 | a)
34 t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
35 return ((t ^ (t >>> 14)) >>> 0) / 4294967296
36 }
37}
38
39const tables = new Map<number, NoiseFunction3D>()
40
41/** One simplex field per seed, built once per thread. */
42export function simplex(seed: number): NoiseFunction3D {
43 let n = tables.get(seed)
44 if (!n) {
45 n = createNoise3D(mulberry32(seed))
46 tables.set(seed, n)
47 }
48 return n
49}
50
51/** A point on the cylinder, in metres, precomputed once per sample. */
52export interface CylPoint { x: number; y: number; z: number }
53
54export function cylPoint(theta: number, z: number): CylPoint {
55 return { x: RADIUS_M * Math.cos(theta), y: RADIUS_M * Math.sin(theta), z }
56}
57
58/**
59 * Octave offsets, so successive octaves of one field do not line up on the same
60 * lattice. Irrational-ish and large relative to one cell.
61 */
62const OX = [0, 131.7, -217.3, 311.9, -419.1, 523.3, -631.7, 739.9, -843.1, 953.3,
63 -1061.7, 1171.9, -1277.3, 1381.1, -1489.7, 1597.3]
64
65/**
66 * Fractal simplex noise, in roughly [-1, 1].
67 *
68 * `minWavelengthM` is the level of detail. Octaves shorter than it are skipped, and
69 * the sum is still normalised by the *full* amplitude, so dropping an octave removes
70 * detail without shifting the value — a coarse tile and a fine tile of the same
71 * ground agree to within the octaves the coarse one cannot show anyway.
72 */
73export function fbm(
74 seed: number, p: CylPoint, scaleM: number, octaves: number, minWavelengthM = 0, gain = 0.5,
75): number {
76 const n = simplex(seed)
77 let sum = 0, amp = 1, norm = 0, wl = scaleM
78 for (let o = 0; o < octaves; o++) {
79 norm += amp
80 if (wl >= minWavelengthM) {
81 const f = 1 / wl
82 const off = OX[o]!
83 sum += n(p.x * f + off, p.y * f - off, p.z * f + off * 0.5) * amp
84 }
85 amp *= gain
86 wl /= 2
87 }
88 return sum / norm
89}
90
91/**
92 * Ridged fractal noise in [0, 1]: absolute-valued and inverted, which makes crests.
93 * Same level-of-detail rule as `fbm`.
94 */
95export function ridged(
96 seed: number, p: CylPoint, scaleM: number, octaves: number, minWavelengthM = 0,
97): number {
98 const n = simplex(seed)
99 let sum = 0, amp = 1, norm = 0, wl = scaleM
100 for (let o = 0; o < octaves; o++) {
101 norm += amp
102 if (wl >= minWavelengthM) {
103 const f = 1 / wl
104 const off = OX[o]!
105 const v = 1 - Math.abs(n(p.x * f - off, p.y * f + off, p.z * f - off * 0.5))
106 sum += v * v * amp
107 } else {
108 // A skipped ridged octave still contributes its mean, or coarse tiles sag.
109 sum += 0.47 * amp
110 }
111 amp *= 0.5
112 wl /= 2
113 }
114 return sum / norm
115}