Skip to main content

proof_engine/math/
noise.rs

1//! Noise functions for the Proof Engine mathematical renderer.
2//!
3//! Implementations: Perlin 1-D / 2-D / 3-D, Simplex 2-D / 3-D, Cellular / Worley,
4//! Curl (divergence-free), Domain Warping, Ridged Multifractal, Billow, and
5//! Fractional Brownian Motion over each.  All functions are stateless, deterministic,
6//! and seeded by a compile-time permutation table.
7
8use glam::{Vec2, Vec3};
9
10// ─────────────────────────────────────────────────────────────────────────────
11// Permutation table — Ken Perlin's canonical p256, doubled to avoid modular
12// indexing in hot paths.
13// ─────────────────────────────────────────────────────────────────────────────
14
15const PERM_BASE: [u8; 256] = [
16    151,160,137, 91, 90, 15,131, 13,201, 95, 96, 53,194,233,  7,225,
17    140, 36,103, 30, 69,142,  8, 99, 37,240, 21, 10, 23,190,  6,148,
18    247,120,234, 75,  0, 26,197, 62, 94,252,219,203,117, 35, 11, 32,
19     57,177, 33, 88,237,149, 56, 87,174, 20,125,136,171,168, 68,175,
20     74,165, 71,134,139, 48, 27,166, 77,146,158,231, 83,111,229,122,
21     60,211,133,230,220,105, 92, 41, 55, 46,245, 40,244,102,143, 54,
22     65, 25, 63,161,  1,216, 80, 73,209, 76,132,187,208, 89, 18,169,
23    200,196,135,130,116,188,159, 86,164,100,109,198,173,186,  3, 64,
24     52,217,226,250,124,123,  5,202, 38,147,118,126,255, 82, 85,212,
25    207,206, 59,227, 47, 16, 58, 17,182,189, 28, 42,223,183,170,213,
26    119,248,152,  2, 44,154,163, 70,221,153,101,155,167, 43,172,  9,
27    129, 22, 39,253, 19, 98,108,110, 79,113,224,232,178,185,112,104,
28    218,246, 97,228,251, 34,242,193,238,210,144, 12,191,179,162,241,
29     81, 51,145,235,249, 14,239,107, 49,192,214, 31,181,199,106,157,
30    184, 84,204,176,115,121, 50, 45,127,  4,150,254,138,236,205, 93,
31    222,114, 67, 29, 24, 72,243,141,128,195, 78, 66,215, 61,156,180,
32];
33
34const P: [u8; 512] = {
35    let mut out = [0u8; 512];
36    let mut i = 0usize;
37    while i < 256 { out[i] = PERM_BASE[i]; out[i + 256] = PERM_BASE[i]; i += 1; }
38    out
39};
40
41#[inline(always)] fn p(i: usize) -> usize { P[i & 511] as usize }
42#[inline(always)] fn fade(t: f32) -> f32 { t * t * t * (t * (t * 6.0 - 15.0) + 10.0) }
43#[inline(always)] fn lerp(a: f32, b: f32, t: f32) -> f32 { a + t * (b - a) }
44
45// ─────────────────────────────────────────────────────────────────────────────
46// 1-D Perlin noise
47// ─────────────────────────────────────────────────────────────────────────────
48
49#[inline(always)]
50fn grad1(h: usize, x: f32) -> f32 { if h & 1 == 0 { x } else { -x } }
51
52/// 1-D Perlin noise.  Output ≈ [-1, 1].
53pub fn perlin1(x: f32) -> f32 {
54    let xi = x.floor() as i32 as usize & 255;
55    let xf = x - x.floor();
56    let u = fade(xf);
57    lerp(grad1(p(xi), xf), grad1(p(xi + 1), xf - 1.0), u)
58}
59
60// ─────────────────────────────────────────────────────────────────────────────
61// 2-D Perlin noise
62// ─────────────────────────────────────────────────────────────────────────────
63
64#[inline(always)]
65fn grad2(h: usize, x: f32, y: f32) -> f32 {
66    match h & 3 { 0 => x + y, 1 => -x + y, 2 => x - y, _ => -x - y }
67}
68
69/// 2-D Perlin noise.  Output ≈ [-1, 1].
70pub fn perlin2(x: f32, y: f32) -> f32 {
71    let xi = x.floor() as i32 as usize & 255;
72    let yi = y.floor() as i32 as usize & 255;
73    let xf = x - x.floor();
74    let yf = y - y.floor();
75    let u = fade(xf); let v = fade(yf);
76
77    let aa = p(p(xi) + yi);       let ba = p(p(xi + 1) + yi);
78    let ab = p(p(xi) + yi + 1);   let bb = p(p(xi + 1) + yi + 1);
79
80    lerp(
81        lerp(grad2(aa, xf, yf),       grad2(ba, xf - 1.0, yf),       u),
82        lerp(grad2(ab, xf, yf - 1.0), grad2(bb, xf - 1.0, yf - 1.0), u),
83        v,
84    )
85}
86
87// ─────────────────────────────────────────────────────────────────────────────
88// 3-D Perlin noise
89// ─────────────────────────────────────────────────────────────────────────────
90
91#[inline(always)]
92fn grad3(h: usize, x: f32, y: f32, z: f32) -> f32 {
93    match h & 15 {
94        0  =>  x + y,  1 => -x + y,  2 =>  x - y,  3 => -x - y,
95        4  =>  x + z,  5 => -x + z,  6 =>  x - z,  7 => -x - z,
96        8  =>  y + z,  9 => -y + z, 10 =>  y - z, 11 => -y - z,
97        12 =>  y + x, 13 => -y + z, 14 =>  y - x,  _ => -y - z,
98    }
99}
100
101/// 3-D Perlin noise.  Output ≈ [-1, 1].
102pub fn perlin3(x: f32, y: f32, z: f32) -> f32 {
103    let xi = x.floor() as i32 as usize & 255;
104    let yi = y.floor() as i32 as usize & 255;
105    let zi = z.floor() as i32 as usize & 255;
106    let xf = x - x.floor(); let yf = y - y.floor(); let zf = z - z.floor();
107    let u = fade(xf); let v = fade(yf); let w = fade(zf);
108
109    let aaa = p(p(p(xi)+yi)+zi);        let baa = p(p(p(xi+1)+yi)+zi);
110    let aba = p(p(p(xi)+yi+1)+zi);      let bba = p(p(p(xi+1)+yi+1)+zi);
111    let aab = p(p(p(xi)+yi)+zi+1);      let bab = p(p(p(xi+1)+yi)+zi+1);
112    let abb = p(p(p(xi)+yi+1)+zi+1);    let bbb = p(p(p(xi+1)+yi+1)+zi+1);
113
114    let x1 = lerp(grad3(aaa,xf,yf,zf),   grad3(baa,xf-1.0,yf,zf),   u);
115    let x2 = lerp(grad3(aba,xf,yf-1.0,zf),grad3(bba,xf-1.0,yf-1.0,zf),u);
116    let x3 = lerp(grad3(aab,xf,yf,zf-1.0),grad3(bab,xf-1.0,yf,zf-1.0),u);
117    let x4 = lerp(grad3(abb,xf,yf-1.0,zf-1.0),grad3(bbb,xf-1.0,yf-1.0,zf-1.0),u);
118
119    lerp(lerp(x1, x2, v), lerp(x3, x4, v), w)
120}
121
122// ─────────────────────────────────────────────────────────────────────────────
123// 2-D Simplex noise
124// ─────────────────────────────────────────────────────────────────────────────
125
126const F2: f32 = 0.366025403784439;  // (sqrt(3) - 1) / 2
127const G2: f32 = 0.211324865405187;  // (3 - sqrt(3)) / 6
128
129fn grad2s(h: usize, x: f32, y: f32) -> f32 {
130    let h4 = h & 7;
131    let (u, v) = if h4 < 4 { (x, y) } else { (y, x) };
132    let su = if h4 & 1 != 0 { -u } else { u };
133    let sv = if h4 & 2 != 0 { -2.0 * v } else { 2.0 * v };
134    su + sv
135}
136
137/// 2-D Simplex noise.  Output ≈ [-1, 1].  Sharper features than Perlin.
138pub fn simplex2(x: f32, y: f32) -> f32 {
139    let s = (x + y) * F2;
140    let i = (x + s).floor() as i32;
141    let j = (y + s).floor() as i32;
142    let t = (i + j) as f32 * G2;
143    let x0 = x - (i as f32 - t);
144    let y0 = y - (j as f32 - t);
145
146    let (i1, j1) = if x0 > y0 { (1i32, 0i32) } else { (0i32, 1i32) };
147    let x1 = x0 - i1 as f32 + G2;
148    let y1 = y0 - j1 as f32 + G2;
149    let x2 = x0 - 1.0 + 2.0 * G2;
150    let y2 = y0 - 1.0 + 2.0 * G2;
151
152    let ii = (i & 255) as usize;
153    let jj = (j & 255) as usize;
154    let gi0 = p(ii          + p(jj));
155    let gi1 = p(ii + i1 as usize + p(jj + j1 as usize));
156    let gi2 = p(ii + 1      + p(jj + 1));
157
158    let contrib = |gi: usize, dx: f32, dy: f32| -> f32 {
159        let t = 0.5 - dx * dx - dy * dy;
160        if t < 0.0 { 0.0 } else { let t2 = t * t; t2 * t2 * grad2s(gi, dx, dy) }
161    };
162    70.0 * (contrib(gi0, x0, y0) + contrib(gi1, x1, y1) + contrib(gi2, x2, y2))
163}
164
165// ─────────────────────────────────────────────────────────────────────────────
166// 3-D Simplex noise
167// ─────────────────────────────────────────────────────────────────────────────
168
169const F3: f32 = 1.0 / 3.0;
170const G3: f32 = 1.0 / 6.0;
171
172/// 3-D Simplex noise.  Output ≈ [-1, 1].
173pub fn simplex3(x: f32, y: f32, z: f32) -> f32 {
174    let s = (x + y + z) * F3;
175    let i = (x + s).floor() as i32;
176    let j = (y + s).floor() as i32;
177    let k = (z + s).floor() as i32;
178    let t = (i + j + k) as f32 * G3;
179    let x0 = x - (i as f32 - t);
180    let y0 = y - (j as f32 - t);
181    let z0 = z - (k as f32 - t);
182
183    let (i1,j1,k1,i2,j2,k2) = if x0 >= y0 {
184        if y0 >= z0 { (1,0,0, 1,1,0) } else if x0 >= z0 { (1,0,0, 1,0,1) } else { (0,0,1, 1,0,1) }
185    } else {
186        if y0 < z0 { (0,0,1, 0,1,1) } else if x0 < z0 { (0,1,0, 0,1,1) } else { (0,1,0, 1,1,0) }
187    };
188
189    let g3 = G3;
190    let (x1,y1,z1) = (x0-i1 as f32+g3,   y0-j1 as f32+g3,   z0-k1 as f32+g3);
191    let (x2,y2,z2) = (x0-i2 as f32+2.0*g3, y0-j2 as f32+2.0*g3, z0-k2 as f32+2.0*g3);
192    let (x3,y3,z3) = (x0-1.0+3.0*g3,     y0-1.0+3.0*g3,     z0-1.0+3.0*g3);
193
194    let ii = (i & 255) as usize; let jj = (j & 255) as usize; let kk = (k & 255) as usize;
195    let gi0 = p(ii          + p(jj          + p(kk)));
196    let gi1 = p(ii+i1 as usize + p(jj+j1 as usize + p(kk+k1 as usize)));
197    let gi2 = p(ii+i2 as usize + p(jj+j2 as usize + p(kk+k2 as usize)));
198    let gi3 = p(ii+1        + p(jj+1        + p(kk+1)));
199
200    let contrib = |gi: usize, dx: f32, dy: f32, dz: f32| -> f32 {
201        let t = 0.6 - dx*dx - dy*dy - dz*dz;
202        if t < 0.0 { 0.0 } else { let t2 = t*t; t2*t2*grad3(gi, dx, dy, dz) }
203    };
204    32.0 * (contrib(gi0,x0,y0,z0) + contrib(gi1,x1,y1,z1) + contrib(gi2,x2,y2,z2) + contrib(gi3,x3,y3,z3))
205}
206
207// ─────────────────────────────────────────────────────────────────────────────
208// Fractional Brownian Motion (fBm)
209// ─────────────────────────────────────────────────────────────────────────────
210
211/// Classic 2-D fBm over Perlin noise.
212///
213/// # Arguments
214/// * `persistence` – amplitude multiplier per octave (0.5 = halved each time)
215/// * `lacunarity`  – frequency multiplier per octave (2.0 = doubled each time)
216pub fn fbm(x: f32, y: f32, octaves: u8, persistence: f32, lacunarity: f32) -> f32 {
217    let mut v = 0.0f32; let mut amp = 1.0f32; let mut freq = 1.0f32; let mut norm = 0.0f32;
218    for _ in 0..octaves {
219        v += perlin2(x * freq, y * freq) * amp; norm += amp; amp *= persistence; freq *= lacunarity;
220    }
221    v / norm
222}
223
224/// 3-D fBm over Perlin noise.
225pub fn fbm3(x: f32, y: f32, z: f32, octaves: u8, persistence: f32, lacunarity: f32) -> f32 {
226    let mut v = 0.0f32; let mut amp = 1.0f32; let mut freq = 1.0f32; let mut norm = 0.0f32;
227    for _ in 0..octaves {
228        v += perlin3(x*freq,y*freq,z*freq) * amp; norm += amp; amp *= persistence; freq *= lacunarity;
229    }
230    v / norm
231}
232
233/// 2-D fBm over Simplex noise — tighter, more crystalline features.
234pub fn fbm_simplex(x: f32, y: f32, octaves: u8, persistence: f32, lacunarity: f32) -> f32 {
235    let mut v = 0.0f32; let mut amp = 1.0f32; let mut freq = 1.0f32; let mut norm = 0.0f32;
236    for _ in 0..octaves {
237        v += simplex2(x*freq, y*freq) * amp; norm += amp; amp *= persistence; freq *= lacunarity;
238    }
239    v / norm
240}
241
242/// 1-D fBm over Perlin noise — useful for audio modulation and time curves.
243pub fn fbm1(t: f32, octaves: u8, persistence: f32, lacunarity: f32) -> f32 {
244    let mut v = 0.0f32; let mut amp = 1.0f32; let mut freq = 1.0f32; let mut norm = 0.0f32;
245    for _ in 0..octaves {
246        v += perlin1(t * freq) * amp; norm += amp; amp *= persistence; freq *= lacunarity;
247    }
248    v / norm
249}
250
251// ─────────────────────────────────────────────────────────────────────────────
252// Ridged Multifractal noise
253// ─────────────────────────────────────────────────────────────────────────────
254
255/// Ridged multifractal — sharp ridges, excellent for lightning, cracks, and mountain terrain.
256/// Output ≈ [0, 1].
257pub fn ridged(x: f32, y: f32, octaves: u8, lacunarity: f32, gain: f32, offset: f32) -> f32 {
258    let mut value = 0.0f32;
259    let mut freq = 1.0f32;
260    let mut amp = 0.5f32;
261    let mut weight = 1.0f32;
262
263    for _ in 0..octaves {
264        let mut signal = perlin2(x * freq, y * freq);
265        signal = (offset - signal.abs()).powi(2);
266        signal *= weight;
267        weight = (signal * gain).clamp(0.0, 1.0);
268        value += signal * amp;
269        freq  *= lacunarity;
270        amp   *= 0.5;
271    }
272    value
273}
274
275/// Ridged 3-D variant — useful for volumetric density fields.
276pub fn ridged3(x: f32, y: f32, z: f32, octaves: u8, lacunarity: f32, gain: f32) -> f32 {
277    let mut value = 0.0f32;
278    let mut freq  = 1.0f32;
279    let mut amp   = 0.5f32;
280    let mut weight = 1.0f32;
281
282    for _ in 0..octaves {
283        let mut signal = perlin3(x*freq, y*freq, z*freq);
284        signal = (1.0 - signal.abs()).powi(2);
285        signal *= weight;
286        weight = (signal * gain).clamp(0.0, 1.0);
287        value += signal * amp;
288        freq  *= lacunarity;
289        amp   *= 0.5;
290    }
291    value
292}
293
294// ─────────────────────────────────────────────────────────────────────────────
295// Billow noise
296// ─────────────────────────────────────────────────────────────────────────────
297
298/// Billow noise — absolute-value fBm, gives cloud and pillow shapes.
299/// Output ≈ [-1, 1].
300pub fn billow(x: f32, y: f32, octaves: u8, persistence: f32, lacunarity: f32) -> f32 {
301    let mut v = 0.0f32; let mut amp = 1.0f32; let mut freq = 1.0f32; let mut norm = 0.0f32;
302    for _ in 0..octaves {
303        v += (perlin2(x*freq, y*freq).abs() * 2.0 - 1.0) * amp;
304        norm += amp; amp *= persistence; freq *= lacunarity;
305    }
306    v / norm
307}
308
309// ─────────────────────────────────────────────────────────────────────────────
310// Turbulence
311// ─────────────────────────────────────────────────────────────────────────────
312
313/// 1-D turbulence (abs fBm) — good for entropy ripple and heat haze.
314pub fn turbulence1(t: f32, octaves: u8) -> f32 {
315    let mut v = 0.0f32; let mut amp = 1.0f32; let mut freq = 1.0f32; let mut norm = 0.0f32;
316    for _ in 0..octaves {
317        v += perlin1(t * freq).abs() * amp; norm += amp; amp *= 0.5; freq *= 2.0;
318    }
319    v / norm
320}
321
322/// 2-D turbulence — classic signed-to-abs fBm, used for marble veining.
323pub fn turbulence(x: f32, y: f32, octaves: u8) -> f32 {
324    let mut v = 0.0f32; let mut amp = 1.0f32; let mut freq = 1.0f32; let mut norm = 0.0f32;
325    for _ in 0..octaves {
326        v += perlin2(x * freq, y * freq).abs() * amp; norm += amp; amp *= 0.5; freq *= 2.0;
327    }
328    v / norm
329}
330
331// ─────────────────────────────────────────────────────────────────────────────
332// Cellular / Worley noise
333// ─────────────────────────────────────────────────────────────────────────────
334
335/// Pseudo-random point within integer grid cell (ix, iy).
336fn cell_point_2d(ix: i32, iy: i32) -> (f32, f32) {
337    let h = ((ix as i64).wrapping_mul(1031) ^ (iy as i64).wrapping_mul(1099)) as u64;
338    let h = h.wrapping_mul(0x9e3779b97f4a7c15_u64);
339    let px = ((h >> 32) as f32) / u32::MAX as f32;
340    let py = ((h & 0xFFFF_FFFF) as f32) / u32::MAX as f32;
341    (ix as f32 + px, iy as f32 + py)
342}
343
344/// Pseudo-random point in 3-D integer grid cell.
345fn cell_point_3d(ix: i32, iy: i32, iz: i32) -> (f32, f32, f32) {
346    let h = ((ix as i64).wrapping_mul(1031)
347        ^ (iy as i64).wrapping_mul(1099)
348        ^ (iz as i64).wrapping_mul(853)) as u64;
349    let h1 = h.wrapping_mul(0x9e3779b97f4a7c15_u64);
350    let h2 = h1.wrapping_mul(0x6c62272e07bb0142_u64);
351    let h3 = h2.wrapping_mul(0x62b821756295c58d_u64);
352    (
353        ix as f32 + (h1 >> 32) as f32 / u32::MAX as f32,
354        iy as f32 + (h2 >> 32) as f32 / u32::MAX as f32,
355        iz as f32 + (h3 >> 32) as f32 / u32::MAX as f32,
356    )
357}
358
359/// Cellular / Worley noise.  Returns `(f1, f2)` — distances to nearest and
360/// second-nearest cell point.
361pub fn worley2(x: f32, y: f32) -> (f32, f32) {
362    let ix = x.floor() as i32;
363    let iy = y.floor() as i32;
364    let mut f1 = f32::MAX;
365    let mut f2 = f32::MAX;
366    for dy in -2..=2i32 {
367        for dx in -2..=2i32 {
368            let (px, py) = cell_point_2d(ix + dx, iy + dy);
369            let d = ((x - px).powi(2) + (y - py).powi(2)).sqrt();
370            if d < f1 { f2 = f1; f1 = d; } else if d < f2 { f2 = d; }
371        }
372    }
373    (f1, f2)
374}
375
376/// 3-D Worley noise.
377pub fn worley3(x: f32, y: f32, z: f32) -> (f32, f32) {
378    let ix = x.floor() as i32; let iy = y.floor() as i32; let iz = z.floor() as i32;
379    let mut f1 = f32::MAX; let mut f2 = f32::MAX;
380    for dz in -2..=2i32 { for dy in -2..=2i32 { for dx in -2..=2i32 {
381        let (px,py,pz) = cell_point_3d(ix+dx, iy+dy, iz+dz);
382        let d = ((x-px).powi(2)+(y-py).powi(2)+(z-pz).powi(2)).sqrt();
383        if d < f1 { f2 = f1; f1 = d; } else if d < f2 { f2 = d; }
384    }}}
385    (f1, f2)
386}
387
388/// Nearest-cell Worley, clamped to [0, 1].
389pub fn worley_f1(x: f32, y: f32) -> f32 { worley2(x, y).0.clamp(0.0, 1.0) }
390
391/// `f2 - f1` — highlights cell borders, useful for vein / crack patterns.
392pub fn worley_crackle(x: f32, y: f32) -> f32 { let (f1,f2) = worley2(x,y); (f2-f1).clamp(0.0,1.0) }
393
394/// Worley fBm — octave-stacked cellular noise for complex surface patterns.
395pub fn worley_fbm(x: f32, y: f32, octaves: u8, persistence: f32, lacunarity: f32) -> f32 {
396    let mut v = 0.0f32; let mut amp = 1.0f32; let mut freq = 1.0f32; let mut norm = 0.0f32;
397    for _ in 0..octaves {
398        let (f1, _) = worley2(x * freq, y * freq);
399        v += f1 * amp; norm += amp; amp *= persistence; freq *= lacunarity;
400    }
401    v / norm
402}
403
404// ─────────────────────────────────────────────────────────────────────────────
405// Curl / Divergence-free noise  (perfect for fluid, smoke, and particle trails)
406// ─────────────────────────────────────────────────────────────────────────────
407
408/// 2-D curl noise.  Returns a velocity vector tangent to the gradient of the
409/// underlying Perlin field — zero divergence means no sources/sinks.
410pub fn curl2(x: f32, y: f32, frequency: f32) -> Vec2 {
411    let eps = 0.001f32;
412    let f = frequency;
413    let dndx = (perlin2((x+eps)*f, y*f) - perlin2((x-eps)*f, y*f)) / (2.0*eps);
414    let dndy = (perlin2(x*f, (y+eps)*f) - perlin2(x*f, (y-eps)*f)) / (2.0*eps);
415    // curl in 2-D = rotate gradient 90°: (∂n/∂y, -∂n/∂x)
416    Vec2::new(dndy, -dndx)
417}
418
419/// 2-D curl noise with fBm underneath — richer turbulent flows.
420pub fn curl2_fbm(x: f32, y: f32, frequency: f32, octaves: u8) -> Vec2 {
421    let eps = 0.001f32;
422    let f = frequency;
423    let noise = |nx: f32, ny: f32| fbm(nx, ny, octaves, 0.5, 2.0);
424    let dndx = (noise((x+eps)*f, y*f) - noise((x-eps)*f, y*f)) / (2.0*eps);
425    let dndy = (noise(x*f, (y+eps)*f) - noise(x*f, (y-eps)*f)) / (2.0*eps);
426    Vec2::new(dndy, -dndx)
427}
428
429/// 3-D curl noise — divergence-free velocity field in 3-D.
430/// Uses three separate Perlin potentials for the three curl components.
431pub fn curl3(pos: Vec3, frequency: f32) -> Vec3 {
432    let eps = 0.001f32;
433    let f = frequency;
434    let (x,y,z) = (pos.x, pos.y, pos.z);
435
436    // Potential fields Px, Py, Pz (using different offsets to decorrelate)
437    let px = |a: f32, b: f32, c: f32| perlin3(a*f + 7.3, b*f + 1.7, c*f + 4.1);
438    let py = |a: f32, b: f32, c: f32| perlin3(a*f + 2.9, b*f + 8.5, c*f + 3.2);
439    let pz = |a: f32, b: f32, c: f32| perlin3(a*f + 5.1, b*f + 6.3, c*f + 9.7);
440
441    // curl = (∂Pz/∂y - ∂Py/∂z, ∂Px/∂z - ∂Pz/∂x, ∂Py/∂x - ∂Px/∂y)
442    let dpz_dy = (pz(x,y+eps,z) - pz(x,y-eps,z)) / (2.0*eps);
443    let dpy_dz = (py(x,y,z+eps) - py(x,y,z-eps)) / (2.0*eps);
444    let dpx_dz = (px(x,y,z+eps) - px(x,y,z-eps)) / (2.0*eps);
445    let dpz_dx = (pz(x+eps,y,z) - pz(x-eps,y,z)) / (2.0*eps);
446    let dpy_dx = (py(x+eps,y,z) - py(x-eps,y,z)) / (2.0*eps);
447    let dpx_dy = (px(x,y+eps,z) - px(x,y-eps,z)) / (2.0*eps);
448
449    Vec3::new(dpz_dy - dpy_dz, dpx_dz - dpz_dx, dpy_dx - dpx_dy)
450}
451
452// ─────────────────────────────────────────────────────────────────────────────
453// Domain Warping
454// ─────────────────────────────────────────────────────────────────────────────
455
456/// Domain-warped fBm — feeds noise output back into coordinates.
457/// Produces the swirling, self-similar patterns seen in marble, flame, and
458/// procedural nebulae.  `warp_strength` controls how much to offset coordinates.
459pub fn domain_warp(x: f32, y: f32, octaves: u8, warp_strength: f32) -> f32 {
460    let qx = fbm(x,         y,         octaves, 0.5, 2.0);
461    let qy = fbm(x + 5.2,   y + 1.3,   octaves, 0.5, 2.0);
462    let rx = fbm(x + warp_strength * qx + 1.7,  y + warp_strength * qy + 9.2, octaves, 0.5, 2.0);
463    let ry = fbm(x + warp_strength * qx + 8.3,  y + warp_strength * qy + 2.8, octaves, 0.5, 2.0);
464    fbm(x + warp_strength * rx, y + warp_strength * ry, octaves, 0.5, 2.0)
465}
466
467/// Two-level domain warp — deeper self-similarity, heavier compute cost.
468pub fn domain_warp2(x: f32, y: f32, octaves: u8, strength: f32) -> f32 {
469    let w1 = domain_warp(x, y, octaves, strength * 0.5);
470    domain_warp(x + w1 * strength, y + w1 * strength * 0.7, octaves, strength * 0.5)
471}
472
473/// 3-D domain warp.
474pub fn domain_warp3(pos: Vec3, octaves: u8, strength: f32) -> f32 {
475    let qx = perlin3(pos.x, pos.y, pos.z);
476    let qy = perlin3(pos.x + 3.7, pos.y + 1.9, pos.z + 7.1);
477    let qz = perlin3(pos.x + 8.2, pos.y + 5.4, pos.z + 2.3);
478    fbm3(pos.x + strength * qx, pos.y + strength * qy, pos.z + strength * qz,
479         octaves, 0.5, 2.0)
480}
481
482// ─────────────────────────────────────────────────────────────────────────────
483// 1-D helpers
484// ─────────────────────────────────────────────────────────────────────────────
485
486/// 1-D value noise (smooth random walk), good for audio modulation.
487pub fn noise1(t: f32) -> f32 {
488    let i = t.floor() as i64;
489    let f = t - t.floor();
490    let a = hash_1d(i);
491    let b = hash_1d(i + 1);
492    lerp(a, b, fade(f))
493}
494
495fn hash_1d(i: i64) -> f32 {
496    let x = (i ^ (i >> 13)) as u64;
497    let x = x.wrapping_mul(0x9e3779b97f4a7c15);
498    ((x >> 32) as f32) / u32::MAX as f32
499}
500
501// ─────────────────────────────────────────────────────────────────────────────
502// Tiled noise
503// ─────────────────────────────────────────────────────────────────────────────
504
505/// Tiled 2-D Perlin noise — wraps seamlessly at `(tile_w, tile_h)`.
506/// Useful for seamless texture generation.
507pub fn tiled_perlin2(x: f32, y: f32, tile_w: u32, tile_h: u32) -> f32 {
508    let tw = tile_w as usize;
509    let th = tile_h as usize;
510    let xi  = x.floor() as i32 as usize;
511    let yi  = y.floor() as i32 as usize;
512    let xf  = x - x.floor();
513    let yf  = y - y.floor();
514    let u = fade(xf); let v = fade(yf);
515    let aa = p(p(xi     % tw) + yi     % th);
516    let ba = p(p((xi+1) % tw) + yi     % th);
517    let ab = p(p(xi     % tw) + (yi+1) % th);
518    let bb = p(p((xi+1) % tw) + (yi+1) % th);
519    lerp(
520        lerp(grad2(aa, xf, yf),       grad2(ba, xf-1.0, yf),       u),
521        lerp(grad2(ab, xf, yf-1.0),   grad2(bb, xf-1.0, yf-1.0),   u),
522        v,
523    )
524}
525
526// ─────────────────────────────────────────────────────────────────────────────
527// Convenience wrappers
528// ─────────────────────────────────────────────────────────────────────────────
529
530/// Remap from Perlin's [-1, 1] to [0, 1].
531#[inline] pub fn to_01(n: f32)   -> f32 { n * 0.5 + 0.5 }
532/// Remap from [0, 1] to [-1, 1].
533#[inline] pub fn from_01(n: f32) -> f32 { n * 2.0 - 1.0 }
534
535/// Sample 3-D Perlin noise at a world position.
536pub fn sample3(pos: Vec3, frequency: f32) -> f32 {
537    perlin3(pos.x * frequency, pos.y * frequency, pos.z * frequency)
538}
539
540/// Sample 3-D fBm at a world position.
541pub fn sample_fbm3(pos: Vec3, frequency: f32, octaves: u8) -> f32 {
542    fbm3(pos.x*frequency, pos.y*frequency, pos.z*frequency, octaves, 0.5, 2.0)
543}
544
545/// Sample curl noise at a 3-D world position.
546pub fn sample_curl3(pos: Vec3, frequency: f32) -> Vec3 { curl3(pos, frequency) }
547
548/// Smooth noise at a position with no configuration — sensible defaults.
549/// Output in [0, 1].
550pub fn quick_noise(x: f32, y: f32) -> f32 {
551    to_01(fbm(x, y, 4, 0.5, 2.0))
552}
553
554/// Animated noise — samples a moving "slice" through 3-D space.
555/// `time` advances the z coordinate, giving smooth noise animation.
556pub fn animated_noise(x: f32, y: f32, time: f32, frequency: f32, octaves: u8) -> f32 {
557    fbm3(x * frequency, y * frequency, time * 0.1, octaves, 0.5, 2.0)
558}
559
560/// Generate a gradient field direction from noise — useful for steering particles.
561pub fn gradient_2d(x: f32, y: f32, frequency: f32) -> Vec2 {
562    let eps = 0.001f32;
563    let f = frequency;
564    let dndx = (perlin2((x+eps)*f, y*f) - perlin2((x-eps)*f, y*f)) / (2.0*eps);
565    let dndy = (perlin2(x*f, (y+eps)*f) - perlin2(x*f, (y-eps)*f)) / (2.0*eps);
566    Vec2::new(dndx, dndy)
567}
568
569/// Generate a 3-D gradient vector from noise.
570pub fn gradient_3d(pos: Vec3, frequency: f32) -> Vec3 {
571    let eps = 0.001f32;
572    let f = frequency;
573    let (x,y,z) = (pos.x, pos.y, pos.z);
574    let gx = (perlin3((x+eps)*f,y*f,z*f) - perlin3((x-eps)*f,y*f,z*f)) / (2.0*eps);
575    let gy = (perlin3(x*f,(y+eps)*f,z*f) - perlin3(x*f,(y-eps)*f,z*f)) / (2.0*eps);
576    let gz = (perlin3(x*f,y*f,(z+eps)*f) - perlin3(x*f,y*f,(z-eps)*f)) / (2.0*eps);
577    Vec3::new(gx, gy, gz)
578}