Skip to main content

motion_core/
noise.rs

1//! Stateless integer-hashed procedural noise with analytic spatial derivatives.
2//!
3//! This is the single noise implementation shared by animation, particles, ink and
4//! filters. Fractal sampling validates finite signed-32-bit lattice support.
5//! Spatial primitives use f64; [`film_grain`] uses the display renderer's
6//! f32 sequence. WGSL twins use f32 interpolation, so hash equality is bitwise
7//! while continuous samples require a numerical tolerance. Large coordinates
8//! lose fractional resolution in f32 and are not a cross-backend precision promise.
9//!
10//! ```
11//! use motion_core::noise::{Fractal, fbm, perlin2, perlin2_d};
12//! let point = [0.25, -0.75];
13//! let (value, gradient) = perlin2_d(point, 42);
14//! assert_eq!(value, perlin2(point, 42));
15//! let cloud = fbm(|point| perlin2(point, 42), point, Fractal::default())?;
16//! assert!(cloud.is_finite() && gradient.iter().all(|value| value.is_finite()));
17//! # Ok::<(), motion_core::ValidationError>(())
18//! ```
19//!
20//! Warping is independent of the subsequently sampled material:
21//! ```
22//! use motion_core::noise::{DomainWarp, domain_warp, simplex2, worley2, curl2};
23//! let point = domain_warp([0.37, -1.23], DomainWarp {
24//!     seed: 42, strength: 0.8, ..DomainWarp::default()
25//! })?;
26//! let cloud = simplex2(point, 19);
27//! let (nearest, second) = worley2(point, 19);
28//! let velocity = curl2(point, 19);
29//! assert!(cloud.is_finite() && nearest <= second);
30//! assert!(velocity.iter().all(|value| value.is_finite()));
31//! # Ok::<(), motion_core::ValidationError>(())
32//! ```
33
34mod advanced;
35mod film;
36
37pub use advanced::{DomainWarp, domain_warp, perlin3, ridged2, simplex2, turbulence2, worley2};
38
39pub use film::{film_grain, film_grain_multi};
40
41const GRADIENTS: [[f64; 2]; 12] = [
42    [1.0, 0.0],
43    [0.8660254037844386, 0.5],
44    [0.5, 0.8660254037844386],
45    [0.0, 1.0],
46    [-0.5, 0.8660254037844386],
47    [-0.8660254037844386, 0.5],
48    [-1.0, 0.0],
49    [-0.8660254037844386, -0.5],
50    [-0.5, -0.8660254037844386],
51    [0.0, -1.0],
52    [0.5, -0.8660254037844386],
53    [0.8660254037844386, -0.5],
54];
55
56/// Fixed wrapping 32-bit avalanche hash.
57pub fn hash32(mut value: u32) -> u32 {
58    value ^= value >> 16;
59    value = value.wrapping_mul(0x7feb_352d);
60    value ^= value >> 15;
61    value = value.wrapping_mul(0x846c_a68b);
62    value ^ (value >> 16)
63}
64
65/// Hashes a signed lattice coordinate and seed using only wrapping integers.
66pub fn hash2(horizontal: i32, vertical: i32, seed: u32) -> u32 {
67    hash32(
68        (horizontal as u32).wrapping_mul(0x9e37_79b9)
69            ^ (vertical as u32).wrapping_mul(0x85eb_ca6b)
70            ^ seed,
71    )
72}
73
74/// Quintic-interpolated lattice noise in [-1,1], with no retained history.
75/// Unsupported nonfinite or out-of-lattice coordinates return zero.
76pub fn value2(point: [f64; 2], seed: u32) -> f64 {
77    if !lattice_supported(point) {
78        return 0.0;
79    }
80    let cell = [point[0].floor(), point[1].floor()];
81    let amount = [point[0] - cell[0], point[1] - cell[1]]
82        .map(|value| value * value * value * (value * (value * 6.0 - 15.0) + 10.0));
83    let value = |horizontal: i32, vertical: i32| {
84        f64::from(hash2(horizontal, vertical, seed)) / f64::from(u32::MAX) * 2.0 - 1.0
85    };
86    let horizontal = cell[0] as i32;
87    let vertical = cell[1] as i32;
88    let top = value(horizontal, vertical) * (1.0 - amount[0])
89        + value(horizontal.wrapping_add(1), vertical) * amount[0];
90    let bottom = value(horizontal, vertical.wrapping_add(1)) * (1.0 - amount[0])
91        + value(horizontal.wrapping_add(1), vertical.wrapping_add(1)) * amount[0];
92    top * (1.0 - amount[1]) + bottom * amount[1]
93}
94
95/// Hashes a signed three-dimensional lattice coordinate with wrapping integers.
96pub fn hash3(horizontal: i32, vertical: i32, depth: i32, seed: u32) -> u32 {
97    hash32(
98        (horizontal as u32).wrapping_mul(0x9e37_79b9)
99            ^ (vertical as u32).wrapping_mul(0x85eb_ca6b)
100            ^ (depth as u32).wrapping_mul(0xc2b2_ae35)
101            ^ seed,
102    )
103}
104
105fn fade(value: f64) -> (f64, f64) {
106    (
107        value * value * value * (value * (value * 6.0 - 15.0) + 10.0),
108        30.0 * value * value * (value - 1.0) * (value - 1.0),
109    )
110}
111
112/// Value noise and its analytic [horizontal, vertical] derivative.
113/// The returned value is bit-identical to `value2`.
114pub fn value2_d(point: [f64; 2], seed: u32) -> (f64, [f64; 2]) {
115    if !lattice_supported(point) {
116        return (0.0, [0.0; 2]);
117    }
118    let cell = point.map(f64::floor);
119    let (horizontal, dh) = fade(point[0] - cell[0]);
120    let (vertical, dv) = fade(point[1] - cell[1]);
121    let sample = |dx: i32, dy: i32| {
122        f64::from(hash2(
123            (cell[0] as i32).wrapping_add(dx),
124            (cell[1] as i32).wrapping_add(dy),
125            seed,
126        )) / f64::from(u32::MAX)
127            * 2.0
128            - 1.0
129    };
130    let samples = [sample(0, 0), sample(1, 0), sample(0, 1), sample(1, 1)];
131    let top = samples[0] * (1.0 - horizontal) + samples[1] * horizontal;
132    let bottom = samples[2] * (1.0 - horizontal) + samples[3] * horizontal;
133    (
134        value2(point, seed),
135        [
136            dh * ((samples[1] - samples[0]) * (1.0 - vertical)
137                + (samples[3] - samples[2]) * vertical),
138            dv * (bottom - top),
139        ],
140    )
141}
142
143/// Whether every coordinate is finite and inside the signed 32-bit lattice.
144pub fn lattice_supported(point: [f64; 2]) -> bool {
145    point.into_iter().all(|value| {
146        value.is_finite() && value >= f64::from(i32::MIN) && value < f64::from(i32::MAX)
147    })
148}
149
150/// Quintic gradient noise with twelve equally spaced unit gradients.
151/// Unsupported nonfinite or out-of-lattice coordinates return zero.
152pub fn perlin2(point: [f64; 2], seed: u32) -> f64 {
153    perlin2_d(point, seed).0
154}
155
156/// Divergence-free two-dimensional curl of a shared Perlin potential.
157/// Returns [d potential / dy, -d potential / dx], without finite differences.
158pub fn curl2(point: [f64; 2], seed: u32) -> [f64; 2] {
159    let derivative = perlin2_d(point, seed).1;
160    [derivative[1], -derivative[0]]
161}
162
163/// Perlin noise and its analytic spatial derivative, without finite differencing.
164/// Unsupported nonfinite or out-of-lattice coordinates return zero.
165pub fn perlin2_d(point: [f64; 2], seed: u32) -> (f64, [f64; 2]) {
166    if !lattice_supported(point) {
167        return (0.0, [0.0; 2]);
168    }
169    let cell = point.map(f64::floor);
170    let fraction = [point[0] - cell[0], point[1] - cell[1]];
171    let (horizontal, dh) = fade(fraction[0]);
172    let (vertical, dv) = fade(fraction[1]);
173    let corner = |dx: i32, dy: i32| {
174        let gradient = GRADIENTS[(hash2(
175            (cell[0] as i32).wrapping_add(dx),
176            (cell[1] as i32).wrapping_add(dy),
177            seed,
178        ) % 12) as usize];
179        (
180            gradient[0] * (fraction[0] - f64::from(dx))
181                + gradient[1] * (fraction[1] - f64::from(dy)),
182            gradient,
183        )
184    };
185    let corners = [corner(0, 0), corner(1, 0), corner(0, 1), corner(1, 1)];
186    let row = |left: usize, right: usize| {
187        let value = corners[left].0 * (1.0 - horizontal) + corners[right].0 * horizontal;
188        let derivative = [
189            corners[left].1[0] * (1.0 - horizontal)
190                + corners[right].1[0] * horizontal
191                + dh * (corners[right].0 - corners[left].0),
192            corners[left].1[1] * (1.0 - horizontal) + corners[right].1[1] * horizontal,
193        ];
194        (value, derivative)
195    };
196    let top = row(0, 1);
197    let bottom = row(2, 3);
198    (
199        top.0 * (1.0 - vertical) + bottom.0 * vertical,
200        [
201            top.1[0] * (1.0 - vertical) + bottom.1[0] * vertical,
202            top.1[1] * (1.0 - vertical) + bottom.1[1] * vertical + dv * (bottom.0 - top.0),
203        ],
204    )
205}
206
207/// Bounded normalized fractal Brownian motion parameters.
208#[derive(Clone, Copy, Debug, PartialEq)]
209pub struct Fractal {
210    /// Number of octaves, in 1..=8.
211    pub octaves: u8,
212    /// Frequency multiplier, in 1..=8.
213    pub lacunarity: f64,
214    /// Successive amplitude multiplier, in 0..=1.
215    pub gain: f64,
216}
217
218impl Default for Fractal {
219    fn default() -> Self {
220        Self {
221            octaves: 4,
222            lacunarity: 2.0,
223            gain: 0.5,
224        }
225    }
226}
227
228impl Fractal {
229    /// Rejects nonfinite, unbounded or empty fractal configurations.
230    pub fn validate(self) -> Result<(), crate::ValidationError> {
231        if !(1..=8).contains(&self.octaves)
232            || !self.lacunarity.is_finite()
233            || !(1.0..=8.0).contains(&self.lacunarity)
234            || !self.gain.is_finite()
235            || !(0.0..=1.0).contains(&self.gain)
236        {
237            return Err(crate::ValidationError::invalid(
238                "noise",
239                "fractal",
240                "Require 1..=8 octaves, lacunarity 1..=8 and gain 0..=1",
241            ));
242        }
243        Ok(())
244    }
245}
246
247/// Evaluates amplitude-normalized fBm, rejecting invalid coordinates or samples.
248/// Each octave invokes the supplied stateless noise function exactly once.
249pub fn fbm(
250    sample: impl Fn([f64; 2]) -> f64,
251    mut point: [f64; 2],
252    fractal: Fractal,
253) -> Result<f64, crate::ValidationError> {
254    fractal.validate()?;
255    let mut amplitude = 1.0;
256    let mut total = 0.0;
257    let mut weight = 0.0;
258    for _ in 0..fractal.octaves {
259        if !lattice_supported(point) {
260            return Err(crate::ValidationError::invalid(
261                "noise",
262                "point",
263                "Fractal coordinates exceed signed 32-bit lattice support",
264            ));
265        }
266        let value = sample(point);
267        if !value.is_finite() {
268            return Err(crate::ValidationError::invalid(
269                "noise",
270                "sample",
271                "Noise samples must be finite",
272            ));
273        }
274        total += value * amplitude;
275        weight += amplitude;
276        amplitude *= fractal.gain;
277        point = point.map(|value| value * fractal.lacunarity);
278    }
279    let value = total / weight;
280    if !value.is_finite() {
281        return Err(crate::ValidationError::invalid(
282            "noise",
283            "sample",
284            "Fractal accumulation must remain finite",
285        ));
286    }
287    Ok(value)
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn hashes_and_value_noise_are_stable() {
296        assert_eq!(hash32(0), 0);
297        assert_eq!(hash32(1), 0x6889_90c0);
298        assert_eq!(hash2(-3, 7, 19), 1_084_314_139);
299        assert_eq!(hash3(-3, 7, 1, 19), 3_845_583_026);
300        assert!((value2([-3.4, 2.7], 42) - 0.0184252545877103).abs() < 1e-15);
301        assert!((value2([10.125, -2.75], 42) + 0.4827757548364509).abs() < 1e-15);
302        assert_eq!(hash3(-3, 7, 0, 19), hash2(-3, 7, 19));
303        assert_ne!(hash3(-3, 7, 1, 19), hash3(-3, 7, 0, 19));
304        for point in [[-3.4, 2.7], [0.0, 0.0], [10.125, -2.75]] {
305            assert_eq!(value2(point, 42).to_bits(), value2_d(point, 42).0.to_bits());
306        }
307    }
308
309    #[test]
310    fn analytic_derivatives_match_central_differences_and_join_cells() {
311        for (sample, derivative) in [
312            (
313                value2 as fn([f64; 2], u32) -> f64,
314                value2_d as fn([f64; 2], u32) -> (f64, [f64; 2]),
315            ),
316            (perlin2, perlin2_d),
317        ] {
318            for point in [[-3.4, 2.7], [0.0, 0.0], [10.125, -2.75], [1.0, -2.0]] {
319                let actual = derivative(point, 42).1;
320                for axis in 0..2 {
321                    let mut before = point;
322                    let mut after = point;
323                    before[axis] -= 1e-5;
324                    after[axis] += 1e-5;
325                    let expected = (sample(after, 42) - sample(before, 42)) / 2e-5;
326                    assert!(
327                        (actual[axis] - expected).abs() < 1e-7,
328                        "{point:?} {axis}: {actual:?} {expected}"
329                    );
330                }
331            }
332            let before = derivative([1.0 - 1e-8, 0.37], 17);
333            let after = derivative([1.0 + 1e-8, 0.37], 17);
334            assert!((before.0 - after.0).abs() < 1e-7);
335            for axis in 0..2 {
336                assert!((before.1[axis] - after.1[axis]).abs() < 1e-6);
337            }
338        }
339    }
340
341    #[test]
342    fn noise_statistics_and_fractal_limits() {
343        for sample in [value2 as fn([f64; 2], u32) -> f64, perlin2] {
344            let mut sum = 0.0;
345            let mut squared = 0.0;
346            for index in 0..8192 {
347                let value = sample(
348                    [f64::from(index % 128) * 0.37, f64::from(index / 128) * 0.41],
349                    733,
350                );
351                assert!(value.is_finite() && value.abs() <= 1.0);
352                sum += value;
353                squared += value * value;
354            }
355            assert!((sum / 8192.0).abs() < 0.06);
356            assert!(squared / 8192.0 > 0.01);
357        }
358        assert_eq!(fbm(|_| 0.75, [0.0; 2], Fractal::default()).unwrap(), 0.75);
359        for octaves in [0, 9, 255] {
360            assert!(
361                fbm(
362                    |_| 0.0,
363                    [0.0; 2],
364                    Fractal {
365                        octaves,
366                        ..Fractal::default()
367                    }
368                )
369                .is_err()
370            );
371        }
372        assert!(fbm(|_| f64::NAN, [0.0; 2], Fractal::default()).is_err());
373        assert!(fbm(|_| 0.0, [f64::INFINITY, 0.0], Fractal::default()).is_err());
374        assert!(fbm(|_| 0.0, [1e9; 2], Fractal::default()).is_err());
375    }
376}