Skip to main content

polydat_nodes/
noise.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Coherent noise functions: Perlin, simplex, fractal Brownian motion.
5//!
6//! Unlike hash functions (which produce uncorrelated "white noise"),
7//! coherent noise produces values that vary smoothly — nearby inputs
8//! yield similar outputs. This is essential for generating realistic
9//! time-series data, spatial fields, and any workload where adjacent
10//! coordinates should have correlated values.
11//!
12//! The permutation table is built at init time from a seed. The noise
13//! evaluation runs at cycle time.
14//!
15//! Inputs are u64 coordinates mapped to a float domain via scaling.
16//! Outputs are f64 in [-1, 1] (raw noise) or [0, 1] (normalized).
17
18// Imports of `PolydatNode` / `Value` live in the `#[cfg(test)]`
19// module — the macro pulls in everything it needs by absolute path.
20
21// =================================================================
22// Permutation table (init-time artifact)
23// =================================================================
24
25pub use polydat::numeric::noise::{
26    PermTable, fbm_1d, fbm_2d, perlin_1d_algo, perlin_2d_algo, simplex_2d_algo,
27};
28
29// =================================================================
30// Polydat Nodes
31// =================================================================
32
33fn perlin_1d_jit_constants(node: &Perlin1d) -> Vec<u64> {
34    vec![node.perm.perm.as_ptr() as u64, node.frequency.to_bits()]
35}
36fn perlin_2d_jit_constants(node: &Perlin2d) -> Vec<u64> {
37    vec![node.perm.perm.as_ptr() as u64, node.frequency.to_bits()]
38}
39fn simplex_2d_jit_constants(node: &Simplex2d) -> Vec<u64> {
40    vec![node.perm.perm.as_ptr() as u64, node.frequency.to_bits()]
41}
42
43/// 1D Perlin noise.
44///
45/// Signature: `(input: u64) -> (f64)`
46///
47/// The u64 input is scaled to the float domain by `frequency`.
48/// Output is in [-1, 1]. For [0, 1], compose with a remap node.
49
50#[polydat::polydat_node(category = Noise, jit_constants = perlin_1d_jit_constants)]
51fn perlin_1d(
52    input: u64,
53    seed: polydat::derive_support::Const<u64>,
54    frequency: polydat::derive_support::Const<f64>,
55    #[poly_const(PermTable::new, from = seed)] perm: &PermTable,
56) -> f64 {
57    perlin_1d_algo(perm, input as f64 * *frequency)
58}
59
60#[polydat::polydat_node(category = Noise, jit_constants = perlin_2d_jit_constants)]
61fn perlin_2d(
62    x: u64,
63    y: u64,
64    seed: polydat::derive_support::Const<u64>,
65    frequency: polydat::derive_support::Const<f64>,
66    #[poly_const(PermTable::new, from = seed)] perm: &PermTable,
67) -> f64 {
68    perlin_2d_algo(perm, x as f64 * *frequency, y as f64 * *frequency)
69}
70
71#[polydat::polydat_node(category = Noise, jit_constants = simplex_2d_jit_constants)]
72fn simplex_2d(
73    x: u64,
74    y: u64,
75    seed: polydat::derive_support::Const<u64>,
76    frequency: polydat::derive_support::Const<f64>,
77    #[poly_const(PermTable::new, from = seed)] perm: &PermTable,
78) -> f64 {
79    simplex_2d_algo(perm, x as f64 * *frequency, y as f64 * *frequency)
80}
81
82// =================================================================
83// Fractal Brownian motion primitives
84// =================================================================
85
86fn fractal_noise_1d_jit_constants(node: &FractalNoise1d) -> Vec<u64> {
87    vec![
88        node.perm.perm.as_ptr() as u64,
89        node.frequency.to_bits(),
90        node.octaves,
91    ]
92}
93fn fractal_noise_2d_jit_constants(node: &FractalNoise2d) -> Vec<u64> {
94    vec![
95        node.perm.perm.as_ptr() as u64,
96        node.frequency.to_bits(),
97        node.octaves,
98    ]
99}
100
101/// 1D fractal Brownian motion: layered Perlin noise with decreasing
102/// amplitude at each octave. Produces rich, natural-looking signals.
103/// Output is f64, roughly in [-1, 1]. Lacunarity is fixed at 2.0 and
104/// persistence at 0.5 (standard FBM parameters).
105///
106/// `octaves` is 1 to 64, for both fractal nodes. Zero octaves is
107/// `0 / 0`, NaN. Each octave halves the amplitude, so past about 53 an
108/// octave's term is below the resolution of the sum and changes
109/// nothing; far past that the doubling frequency overflows to infinity
110/// and the sample is NaN. An octave count is also work per call, one
111/// noise sample each, so an unbounded one is a hang, and a count passed
112/// through `as u32` would truncate `2^53 + 1` octaves to one.
113/// Sixty-four keeps every octave that can contribute.
114#[polydat::polydat_node(category = Noise, jit_constants = fractal_noise_1d_jit_constants)]
115fn fractal_noise_1d(
116    input: u64,
117    seed: polydat::derive_support::Const<u64>,
118    frequency: polydat::derive_support::Const<f64>,
119    #[poly_default(4u64)]
120    #[constraint(RangeU64 { min: 1, max: 64 })]
121    octaves: polydat::derive_support::Const<u64>,
122    #[poly_const(PermTable::new, from = seed)] perm: &PermTable,
123) -> f64 {
124    fbm_1d(perm, input as f64, *frequency, *octaves as u32)
125}
126
127/// 2D fractal Brownian motion: layered Perlin noise in 2D. Produces
128/// terrain-like spatial variation. Lacunarity is fixed at 2.0 and
129/// persistence at 0.5 (standard FBM parameters). `octaves` is 1 to 64,
130/// as for `fractal_noise_1d`.
131#[polydat::polydat_node(category = Noise, jit_constants = fractal_noise_2d_jit_constants)]
132fn fractal_noise_2d(
133    x: u64,
134    y: u64,
135    seed: polydat::derive_support::Const<u64>,
136    frequency: polydat::derive_support::Const<f64>,
137    #[poly_default(4u64)]
138    #[constraint(RangeU64 { min: 1, max: 64 })]
139    octaves: polydat::derive_support::Const<u64>,
140    #[poly_const(PermTable::new, from = seed)] perm: &PermTable,
141) -> f64 {
142    fbm_2d(perm, x as f64, y as f64, *frequency, *octaves as u32)
143}
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use polydat::ast::{PolydatNode, Value};
148
149    #[test]
150    fn perlin_1d_bounded() {
151        let node = Perlin1d::new(42, 0.01);
152        let mut out = [Value::None];
153        for i in 0..1000u64 {
154            node.eval(&[Value::U64(i)], &mut out);
155            let v = out[0].as_f64();
156            assert!((-1.0..=1.0).contains(&v), "out of range: {v} at i={i}");
157        }
158    }
159
160    #[test]
161    fn perlin_1d_smooth() {
162        // Adjacent inputs should produce similar (not identical) values
163        let node = Perlin1d::new(42, 0.01);
164        let mut prev = [Value::None];
165        let mut curr = [Value::None];
166        node.eval(&[Value::U64(100)], &mut prev);
167        let mut large_jumps = 0;
168        for i in 101..200u64 {
169            node.eval(&[Value::U64(i)], &mut curr);
170            let diff = (curr[0].as_f64() - prev[0].as_f64()).abs();
171            if diff > 0.5 {
172                large_jumps += 1;
173            }
174            prev[0] = curr[0].clone();
175        }
176        // With frequency 0.01, adjacent samples should rarely jump more than 0.5
177        assert!(large_jumps < 5, "too many large jumps: {large_jumps}");
178    }
179
180    #[test]
181    fn perlin_1d_deterministic() {
182        let node = Perlin1d::new(42, 0.1);
183        let mut out1 = [Value::None];
184        let mut out2 = [Value::None];
185        node.eval(&[Value::U64(123)], &mut out1);
186        node.eval(&[Value::U64(123)], &mut out2);
187        assert_eq!(out1[0].as_f64(), out2[0].as_f64());
188    }
189
190    #[test]
191    fn perlin_1d_different_seeds() {
192        let a = Perlin1d::new(1, 0.1);
193        let b = Perlin1d::new(2, 0.1);
194        let mut out_a = [Value::None];
195        let mut out_b = [Value::None];
196        let mut differ = false;
197        for i in 0..100u64 {
198            a.eval(&[Value::U64(i)], &mut out_a);
199            b.eval(&[Value::U64(i)], &mut out_b);
200            if (out_a[0].as_f64() - out_b[0].as_f64()).abs() > 0.01 {
201                differ = true;
202                break;
203            }
204        }
205        assert!(differ, "different seeds should produce different noise");
206    }
207
208    #[test]
209    fn perlin_2d_bounded() {
210        let node = Perlin2d::new(42, 0.01);
211        let mut out = [Value::None];
212        for x in 0..50u64 {
213            for y in 0..50u64 {
214                node.eval(&[Value::U64(x), Value::U64(y)], &mut out);
215                let v = out[0].as_f64();
216                assert!((-1.5..=1.5).contains(&v), "out of range: {v} at ({x},{y})");
217            }
218        }
219    }
220
221    #[test]
222    fn perlin_2d_smooth() {
223        let node = Perlin2d::new(42, 0.01);
224        let mut prev = [Value::None];
225        let mut curr = [Value::None];
226        node.eval(&[Value::U64(100), Value::U64(100)], &mut prev);
227        let mut large_jumps = 0;
228        for i in 101..150u64 {
229            node.eval(&[Value::U64(i), Value::U64(100)], &mut curr);
230            let diff = (curr[0].as_f64() - prev[0].as_f64()).abs();
231            if diff > 0.5 {
232                large_jumps += 1;
233            }
234            prev[0] = curr[0].clone();
235        }
236        assert!(large_jumps < 5, "too many large jumps: {large_jumps}");
237    }
238
239    #[test]
240    fn simplex_2d_bounded() {
241        let node = Simplex2d::new(42, 0.01);
242        let mut out = [Value::None];
243        for x in 0..50u64 {
244            for y in 0..50u64 {
245                node.eval(&[Value::U64(x), Value::U64(y)], &mut out);
246                let v = out[0].as_f64();
247                assert!((-1.5..=1.5).contains(&v), "out of range: {v}");
248            }
249        }
250    }
251
252    #[test]
253    fn fractal_1d_bounded() {
254        let node = FractalNoise1d::new(42, 0.01, 4);
255        let mut out = [Value::None];
256        for i in 0..500u64 {
257            node.eval(&[Value::U64(i)], &mut out);
258            let v = out[0].as_f64();
259            assert!((-1.5..=1.5).contains(&v), "out of range: {v}");
260        }
261    }
262
263    #[test]
264    fn fractal_1d_more_detail_than_single_octave() {
265        // FBM with 4 octaves should have more high-frequency variation
266        // than a single octave
267        let single = Perlin1d::new(42, 0.01);
268        let fbm = FractalNoise1d::new(42, 0.01, 4);
269        let mut s_out = [Value::None];
270        let mut f_out = [Value::None];
271        let mut s_changes = 0.0;
272        let mut f_changes = 0.0;
273        let mut s_prev = 0.0;
274        let mut f_prev = 0.0;
275        for i in 0..500u64 {
276            single.eval(&[Value::U64(i)], &mut s_out);
277            fbm.eval(&[Value::U64(i)], &mut f_out);
278            if i > 0 {
279                s_changes += (s_out[0].as_f64() - s_prev).abs();
280                f_changes += (f_out[0].as_f64() - f_prev).abs();
281            }
282            s_prev = s_out[0].as_f64();
283            f_prev = f_out[0].as_f64();
284        }
285        // FBM should have more total variation (higher frequency detail)
286        assert!(
287            f_changes > s_changes * 0.8,
288            "FBM should have comparable or more detail: single={s_changes}, fbm={f_changes}"
289        );
290    }
291
292    #[test]
293    fn fractal_2d_bounded() {
294        let node = FractalNoise2d::new(42, 0.01, 3);
295        let mut out = [Value::None];
296        for x in 0..30u64 {
297            for y in 0..30u64 {
298                node.eval(&[Value::U64(x), Value::U64(y)], &mut out);
299                let v = out[0].as_f64();
300                assert!((-1.5..=1.5).contains(&v), "out of range: {v}");
301            }
302        }
303    }
304}