Skip to main content

polydat_core/numeric/
noise.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Coherent noise: the permutation table and the Perlin, simplex, and
5//! fractal Brownian motion algorithms, the bodies of the noise nodes
6//! and of their native lowerings.
7
8/// A permutation table for noise functions. Built from a seed at init
9/// time, immutable thereafter. The table is doubled (512 entries) to
10/// avoid modular indexing.
11pub struct PermTable {
12    /// The doubled table: 256 entries repeated, so an index needs no modulus.
13    pub perm: [u8; 512],
14}
15
16impl PermTable {
17    /// The table a seed determines: a Fisher-Yates shuffle driven by xxh3.
18    pub fn new(seed: u64) -> Self {
19        use xxhash_rust::xxh3::xxh3_64;
20        let mut p: Vec<u8> = (0..=255).collect();
21        // Fisher-Yates shuffle seeded by hash chain
22        let mut s = seed;
23        for i in (1..256).rev() {
24            s = xxh3_64(&s.to_le_bytes());
25            let j = (s as usize) % (i + 1);
26            p.swap(i, j);
27        }
28        let mut perm = [0u8; 512];
29        for i in 0..512 {
30            perm[i] = p[i & 255];
31        }
32        Self { perm }
33    }
34
35    #[inline]
36    /// The entry at `i` modulo 256.
37    pub fn hash(&self, i: i32) -> u8 {
38        self.perm[(i & 255) as usize]
39    }
40}
41
42// =================================================================
43// Perlin noise primitives
44// =================================================================
45
46#[inline]
47fn fade(t: f64) -> f64 {
48    // 6t^5 - 15t^4 + 10t^3 (improved Perlin smoothstep)
49    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
50}
51
52#[inline]
53fn lerp(t: f64, a: f64, b: f64) -> f64 {
54    a + t * (b - a)
55}
56
57#[inline]
58fn grad1d(hash: u8, x: f64) -> f64 {
59    if hash & 1 == 0 { x } else { -x }
60}
61
62#[inline]
63fn grad2d(hash: u8, x: f64, y: f64) -> f64 {
64    match hash & 3 {
65        0 => x + y,
66        1 => -x + y,
67        2 => x - y,
68        _ => -x - y,
69    }
70}
71
72/// Evaluate 1D Perlin noise at a given point.
73pub fn perlin_1d_algo(perm: &PermTable, x: f64) -> f64 {
74    let xi = x.floor() as i32;
75    let xf = x - x.floor();
76    let u = fade(xf);
77
78    let a = perm.hash(xi);
79    let b = perm.hash(xi.wrapping_add(1));
80
81    lerp(u, grad1d(a, xf), grad1d(b, xf - 1.0))
82}
83
84/// Evaluate 2D Perlin noise at a given point.
85pub fn perlin_2d_algo(perm: &PermTable, x: f64, y: f64) -> f64 {
86    let xi = x.floor() as i32;
87    let yi = y.floor() as i32;
88    let xf = x - x.floor();
89    let yf = y - y.floor();
90
91    let u = fade(xf);
92    let v = fade(yf);
93
94    let aa = perm.hash((perm.hash(xi) as i32).wrapping_add(yi));
95    let ab = perm.hash((perm.hash(xi) as i32).wrapping_add(yi).wrapping_add(1));
96    let ba = perm.hash((perm.hash(xi.wrapping_add(1)) as i32).wrapping_add(yi));
97    let bb = perm.hash(
98        (perm.hash(xi.wrapping_add(1)) as i32)
99            .wrapping_add(yi)
100            .wrapping_add(1),
101    );
102
103    lerp(
104        v,
105        lerp(u, grad2d(aa, xf, yf), grad2d(ba, xf - 1.0, yf)),
106        lerp(u, grad2d(ab, xf, yf - 1.0), grad2d(bb, xf - 1.0, yf - 1.0)),
107    )
108}
109
110// =================================================================
111// Simplex noise 2D
112// =================================================================
113
114const F2: f64 = 0.3660254037844386; // (sqrt(3) - 1) / 2
115const G2: f64 = 0.21132486540518713; // (3 - sqrt(3)) / 6
116
117/// Evaluate 2D simplex noise at a given point.
118pub fn simplex_2d_algo(perm: &PermTable, x: f64, y: f64) -> f64 {
119    let s = (x + y) * F2;
120    let i = (x + s).floor() as i32;
121    let j = (y + s).floor() as i32;
122
123    let t = (i + j) as f64 * G2;
124    let x0 = x - (i as f64 - t);
125    let y0 = y - (j as f64 - t);
126
127    let (i1, j1) = if x0 > y0 { (1, 0) } else { (0, 1) };
128
129    let x1 = x0 - i1 as f64 + G2;
130    let y1 = y0 - j1 as f64 + G2;
131    let x2 = x0 - 1.0 + 2.0 * G2;
132    let y2 = y0 - 1.0 + 2.0 * G2;
133
134    let gi0 = perm.hash(i.wrapping_add(perm.hash(j) as i32));
135    let gi1 = perm.hash(
136        i.wrapping_add(i1)
137            .wrapping_add(perm.hash(j.wrapping_add(j1)) as i32),
138    );
139    let gi2 = perm.hash(
140        i.wrapping_add(1)
141            .wrapping_add(perm.hash(j.wrapping_add(1)) as i32),
142    );
143
144    let mut n0 = 0.0;
145    let t0 = 0.5 - x0 * x0 - y0 * y0;
146    if t0 > 0.0 {
147        let t0 = t0 * t0;
148        n0 = t0 * t0 * grad2d(gi0, x0, y0);
149    }
150
151    let mut n1 = 0.0;
152    let t1 = 0.5 - x1 * x1 - y1 * y1;
153    if t1 > 0.0 {
154        let t1 = t1 * t1;
155        n1 = t1 * t1 * grad2d(gi1, x1, y1);
156    }
157
158    let mut n2 = 0.0;
159    let t2 = 0.5 - x2 * x2 - y2 * y2;
160    if t2 > 0.0 {
161        let t2 = t2 * t2;
162        n2 = t2 * t2 * grad2d(gi2, x2, y2);
163    }
164
165    // Scale to [-1, 1]
166    70.0 * (n0 + n1 + n2)
167}
168
169impl crate::derive_support::PolydatSetup for PermTable {}
170
171/// FBM lacunarity (frequency multiplier per octave). Standard value.
172const FBM_LACUNARITY: f64 = 2.0;
173/// FBM persistence (amplitude multiplier per octave). Standard value.
174const FBM_PERSISTENCE: f64 = 0.5;
175
176/// 1D fractal Brownian motion: `octaves` layers of Perlin noise, each
177/// at twice the frequency and half the amplitude of the last.
178pub fn fbm_1d(perm: &PermTable, base_x: f64, frequency: f64, octaves: u32) -> f64 {
179    let mut total = 0.0;
180    let mut freq = frequency;
181    let mut amp = 1.0;
182    let mut max_amp = 0.0;
183
184    for _ in 0..octaves {
185        total += perlin_1d_algo(perm, base_x * freq) * amp;
186        max_amp += amp;
187        freq *= FBM_LACUNARITY;
188        amp *= FBM_PERSISTENCE;
189    }
190
191    // Normalize to [-1, 1]
192    total / max_amp
193}
194
195/// 2D fractal Brownian motion over Perlin noise, as [`fbm_1d`].
196pub fn fbm_2d(perm: &PermTable, base_x: f64, base_y: f64, frequency: f64, octaves: u32) -> f64 {
197    let mut total = 0.0;
198    let mut freq = frequency;
199    let mut amp = 1.0;
200    let mut max_amp = 0.0;
201
202    for _ in 0..octaves {
203        total += perlin_2d_algo(perm, base_x * freq, base_y * freq) * amp;
204        max_amp += amp;
205        freq *= FBM_LACUNARITY;
206        amp *= FBM_PERSISTENCE;
207    }
208
209    total / max_amp
210}