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#[polydat::polydat_node(category = Noise, jit_constants = fractal_noise_1d_jit_constants)]
106fn fractal_noise_1d(
107    input: u64,
108    seed: polydat::derive_support::Const<u64>,
109    frequency: polydat::derive_support::Const<f64>,
110    #[poly_default(4u64)] octaves: polydat::derive_support::Const<u64>,
111    #[poly_const(PermTable::new, from = seed)] perm: &PermTable,
112) -> f64 {
113    fbm_1d(perm, input as f64, *frequency, *octaves as u32)
114}
115
116/// 2D fractal Brownian motion: layered Perlin noise in 2D. Produces
117/// terrain-like spatial variation. Lacunarity is fixed at 2.0 and
118/// persistence at 0.5 (standard FBM parameters).
119#[polydat::polydat_node(category = Noise, jit_constants = fractal_noise_2d_jit_constants)]
120fn fractal_noise_2d(
121    x: u64,
122    y: u64,
123    seed: polydat::derive_support::Const<u64>,
124    frequency: polydat::derive_support::Const<f64>,
125    #[poly_default(4u64)] octaves: polydat::derive_support::Const<u64>,
126    #[poly_const(PermTable::new, from = seed)] perm: &PermTable,
127) -> f64 {
128    fbm_2d(perm, x as f64, y as f64, *frequency, *octaves as u32)
129}
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use polydat::ast::{PolydatNode, Value};
134
135    #[test]
136    fn perlin_1d_bounded() {
137        let node = Perlin1d::new(42, 0.01);
138        let mut out = [Value::None];
139        for i in 0..1000u64 {
140            node.eval(&[Value::U64(i)], &mut out);
141            let v = out[0].as_f64();
142            assert!((-1.0..=1.0).contains(&v), "out of range: {v} at i={i}");
143        }
144    }
145
146    #[test]
147    fn perlin_1d_smooth() {
148        // Adjacent inputs should produce similar (not identical) values
149        let node = Perlin1d::new(42, 0.01);
150        let mut prev = [Value::None];
151        let mut curr = [Value::None];
152        node.eval(&[Value::U64(100)], &mut prev);
153        let mut large_jumps = 0;
154        for i in 101..200u64 {
155            node.eval(&[Value::U64(i)], &mut curr);
156            let diff = (curr[0].as_f64() - prev[0].as_f64()).abs();
157            if diff > 0.5 {
158                large_jumps += 1;
159            }
160            prev[0] = curr[0].clone();
161        }
162        // With frequency 0.01, adjacent samples should rarely jump more than 0.5
163        assert!(large_jumps < 5, "too many large jumps: {large_jumps}");
164    }
165
166    #[test]
167    fn perlin_1d_deterministic() {
168        let node = Perlin1d::new(42, 0.1);
169        let mut out1 = [Value::None];
170        let mut out2 = [Value::None];
171        node.eval(&[Value::U64(123)], &mut out1);
172        node.eval(&[Value::U64(123)], &mut out2);
173        assert_eq!(out1[0].as_f64(), out2[0].as_f64());
174    }
175
176    #[test]
177    fn perlin_1d_different_seeds() {
178        let a = Perlin1d::new(1, 0.1);
179        let b = Perlin1d::new(2, 0.1);
180        let mut out_a = [Value::None];
181        let mut out_b = [Value::None];
182        let mut differ = false;
183        for i in 0..100u64 {
184            a.eval(&[Value::U64(i)], &mut out_a);
185            b.eval(&[Value::U64(i)], &mut out_b);
186            if (out_a[0].as_f64() - out_b[0].as_f64()).abs() > 0.01 {
187                differ = true;
188                break;
189            }
190        }
191        assert!(differ, "different seeds should produce different noise");
192    }
193
194    #[test]
195    fn perlin_2d_bounded() {
196        let node = Perlin2d::new(42, 0.01);
197        let mut out = [Value::None];
198        for x in 0..50u64 {
199            for y in 0..50u64 {
200                node.eval(&[Value::U64(x), Value::U64(y)], &mut out);
201                let v = out[0].as_f64();
202                assert!((-1.5..=1.5).contains(&v), "out of range: {v} at ({x},{y})");
203            }
204        }
205    }
206
207    #[test]
208    fn perlin_2d_smooth() {
209        let node = Perlin2d::new(42, 0.01);
210        let mut prev = [Value::None];
211        let mut curr = [Value::None];
212        node.eval(&[Value::U64(100), Value::U64(100)], &mut prev);
213        let mut large_jumps = 0;
214        for i in 101..150u64 {
215            node.eval(&[Value::U64(i), Value::U64(100)], &mut curr);
216            let diff = (curr[0].as_f64() - prev[0].as_f64()).abs();
217            if diff > 0.5 {
218                large_jumps += 1;
219            }
220            prev[0] = curr[0].clone();
221        }
222        assert!(large_jumps < 5, "too many large jumps: {large_jumps}");
223    }
224
225    #[test]
226    fn simplex_2d_bounded() {
227        let node = Simplex2d::new(42, 0.01);
228        let mut out = [Value::None];
229        for x in 0..50u64 {
230            for y in 0..50u64 {
231                node.eval(&[Value::U64(x), Value::U64(y)], &mut out);
232                let v = out[0].as_f64();
233                assert!((-1.5..=1.5).contains(&v), "out of range: {v}");
234            }
235        }
236    }
237
238    #[test]
239    fn fractal_1d_bounded() {
240        let node = FractalNoise1d::new(42, 0.01, 4);
241        let mut out = [Value::None];
242        for i in 0..500u64 {
243            node.eval(&[Value::U64(i)], &mut out);
244            let v = out[0].as_f64();
245            assert!((-1.5..=1.5).contains(&v), "out of range: {v}");
246        }
247    }
248
249    #[test]
250    fn fractal_1d_more_detail_than_single_octave() {
251        // FBM with 4 octaves should have more high-frequency variation
252        // than a single octave
253        let single = Perlin1d::new(42, 0.01);
254        let fbm = FractalNoise1d::new(42, 0.01, 4);
255        let mut s_out = [Value::None];
256        let mut f_out = [Value::None];
257        let mut s_changes = 0.0;
258        let mut f_changes = 0.0;
259        let mut s_prev = 0.0;
260        let mut f_prev = 0.0;
261        for i in 0..500u64 {
262            single.eval(&[Value::U64(i)], &mut s_out);
263            fbm.eval(&[Value::U64(i)], &mut f_out);
264            if i > 0 {
265                s_changes += (s_out[0].as_f64() - s_prev).abs();
266                f_changes += (f_out[0].as_f64() - f_prev).abs();
267            }
268            s_prev = s_out[0].as_f64();
269            f_prev = f_out[0].as_f64();
270        }
271        // FBM should have more total variation (higher frequency detail)
272        assert!(
273            f_changes > s_changes * 0.8,
274            "FBM should have comparable or more detail: single={s_changes}, fbm={f_changes}"
275        );
276    }
277
278    #[test]
279    fn fractal_2d_bounded() {
280        let node = FractalNoise2d::new(42, 0.01, 3);
281        let mut out = [Value::None];
282        for x in 0..30u64 {
283            for y in 0..30u64 {
284                node.eval(&[Value::U64(x), Value::U64(y)], &mut out);
285                let v = out[0].as_f64();
286                assert!((-1.5..=1.5).contains(&v), "out of range: {v}");
287            }
288        }
289    }
290}