Skip to main content

kvas_engine/
noise.rs

1//! Procedural noise for scripts, shaders-on-the-CPU and generated textures.
2//!
3//! Everything is a pure function of its inputs (no global state), so it is safe to call from
4//! script libraries and always deterministic for a given `seed`.
5//!
6//! ```
7//! use kvas_engine::noise::{Noise, NoiseKind};
8//! let (x, z) = (1.0, 2.0);
9//! let n = Noise { kind: NoiseKind::Ridged, frequency: 0.8, octaves: 5, ..Default::default() };
10//! let height = n.sample2(x, z);          // -1..1
11//! let mask = n.sample01(x, z, 0.0);      // 0..1
12//! ```
13
14use crate::choice_enum;
15use glam::{Vec2, Vec3};
16
17choice_enum!(
18    /// Basic noise flavours; all can be combined with fractal octaves and domain warping.
19    NoiseKind { Value, Perlin, Worley, Ridged, Billow, Cellular } default Perlin
20);
21
22fn hash(mut x: u32) -> u32 {
23    x ^= x >> 16;
24    x = x.wrapping_mul(0x7feb_352d);
25    x ^= x >> 15;
26    x = x.wrapping_mul(0x846c_a68b);
27    x ^ (x >> 16)
28}
29
30fn lattice(x: i32, y: i32, z: i32, seed: u32) -> u32 {
31    hash(hash(hash(x as u32 ^ seed.wrapping_mul(0x9e37_79b1)).wrapping_add(y as u32)).wrapping_add(z as u32))
32}
33
34fn unit(h: u32) -> f32 {
35    (h >> 8) as f32 / 16_777_216.0
36}
37
38fn fade(t: f32) -> f32 {
39    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
40}
41
42fn lerp(a: f32, b: f32, t: f32) -> f32 {
43    a + (b - a) * t
44}
45
46/// Smooth value noise, -1..1.
47pub fn value3(p: Vec3, seed: u32) -> f32 {
48    if !p.is_finite() {
49        return 0.0;
50    }
51    let (i, f) = (p.floor(), p - p.floor());
52    let (x, y, z) = (i.x as i32, i.y as i32, i.z as i32);
53    let (u, v, w) = (fade(f.x), fade(f.y), fade(f.z));
54    let c = |dx: i32, dy: i32, dz: i32| unit(lattice(x.wrapping_add(dx), y.wrapping_add(dy), z.wrapping_add(dz), seed)) * 2.0 - 1.0;
55    lerp(
56        lerp(lerp(c(0, 0, 0), c(1, 0, 0), u), lerp(c(0, 1, 0), c(1, 1, 0), u), v),
57        lerp(lerp(c(0, 0, 1), c(1, 0, 1), u), lerp(c(0, 1, 1), c(1, 1, 1), u), v),
58        w,
59    )
60}
61
62fn gradient(h: u32) -> Vec3 {
63    // 12 edge directions of a cube, as in Perlin's improved noise.
64    const G: [Vec3; 12] = [
65        Vec3::new(1.0, 1.0, 0.0),
66        Vec3::new(-1.0, 1.0, 0.0),
67        Vec3::new(1.0, -1.0, 0.0),
68        Vec3::new(-1.0, -1.0, 0.0),
69        Vec3::new(1.0, 0.0, 1.0),
70        Vec3::new(-1.0, 0.0, 1.0),
71        Vec3::new(1.0, 0.0, -1.0),
72        Vec3::new(-1.0, 0.0, -1.0),
73        Vec3::new(0.0, 1.0, 1.0),
74        Vec3::new(0.0, -1.0, 1.0),
75        Vec3::new(0.0, 1.0, -1.0),
76        Vec3::new(0.0, -1.0, -1.0),
77    ];
78    G[(h % 12) as usize]
79}
80
81/// Classic gradient (Perlin) noise, roughly -1..1.
82pub fn perlin3(p: Vec3, seed: u32) -> f32 {
83    if !p.is_finite() {
84        return 0.0;
85    }
86    let (i, f) = (p.floor(), p - p.floor());
87    let (x, y, z) = (i.x as i32, i.y as i32, i.z as i32);
88    let (u, v, w) = (fade(f.x), fade(f.y), fade(f.z));
89    let d = |dx: i32, dy: i32, dz: i32| {
90        gradient(lattice(x.wrapping_add(dx), y.wrapping_add(dy), z.wrapping_add(dz), seed))
91            .dot(f - Vec3::new(dx as f32, dy as f32, dz as f32))
92    };
93    lerp(
94        lerp(lerp(d(0, 0, 0), d(1, 0, 0), u), lerp(d(0, 1, 0), d(1, 1, 0), u), v),
95        lerp(lerp(d(0, 0, 1), d(1, 0, 1), u), lerp(d(0, 1, 1), d(1, 1, 1), u), v),
96        w,
97    ) * 0.9
98}
99
100/// Distances to the nearest and second nearest feature point (Worley / cellular noise).
101pub fn worley3(p: Vec3, seed: u32) -> (f32, f32) {
102    if !p.is_finite() {
103        return (0.0, 0.0);
104    }
105    let (i, f) = (p.floor(), p - p.floor());
106    let (mut f1, mut f2) = (8.0f32, 8.0f32);
107    for dz in -1..=1 {
108        for dy in -1..=1 {
109            for dx in -1..=1 {
110                let (x, y, z) = ((i.x as i32).wrapping_add(dx), (i.y as i32).wrapping_add(dy), (i.z as i32).wrapping_add(dz));
111                let point =
112                    Vec3::new(unit(lattice(x, y, z, seed)), unit(lattice(x, y, z, seed ^ 0x51ed)), unit(lattice(x, y, z, seed ^ 0xa2b3)));
113                let d = (Vec3::new(dx as f32, dy as f32, dz as f32) + point - f).length();
114                if d < f1 {
115                    f2 = f1;
116                    f1 = d;
117                } else if d < f2 {
118                    f2 = d;
119                }
120            }
121        }
122    }
123    (f1, f2)
124}
125
126/// Full-featured fractal noise. `sample*` return -1..1, `sample01*` return 0..1.
127#[derive(Clone, Copy, Debug, PartialEq)]
128pub struct Noise {
129    /// Base noise family used for each octave.
130    pub kind: NoiseKind,
131    /// Deterministic lattice seed.
132    pub seed: u32,
133    /// Features per world unit.
134    pub frequency: f32,
135    /// Number of layered octaves (1 = plain noise).
136    pub octaves: u32,
137    /// Frequency multiplier between octaves.
138    pub lacunarity: f32,
139    /// Amplitude multiplier between octaves.
140    pub gain: f32,
141    /// Domain warp strength (0 = off): bends the lookup position with more noise.
142    pub warp: f32,
143}
144
145impl Default for Noise {
146    fn default() -> Self {
147        Self { kind: NoiseKind::Perlin, seed: 0, frequency: 1.0, octaves: 4, lacunarity: 2.0, gain: 0.5, warp: 0.0 }
148    }
149}
150
151impl Noise {
152    /// Creates fractal noise of the chosen family with default sampling parameters.
153    pub fn new(kind: NoiseKind) -> Self {
154        Self { kind, ..Default::default() }
155    }
156    /// Builder: changes the deterministic lattice seed.
157    pub fn with_seed(mut self, seed: u32) -> Self {
158        self.seed = seed;
159        self
160    }
161    /// Builder: sets features per input unit.
162    pub fn with_frequency(mut self, f: f32) -> Self {
163        self.frequency = f;
164        self
165    }
166    /// Builder: sets the octave count; sampling clamps it to a bounded range.
167    pub fn with_octaves(mut self, o: u32) -> Self {
168        self.octaves = o;
169        self
170    }
171    /// Builder: sets domain-warp strength; zero disables it.
172    pub fn with_warp(mut self, w: f32) -> Self {
173        self.warp = w;
174        self
175    }
176
177    fn single(&self, p: Vec3, seed: u32) -> f32 {
178        match self.kind {
179            NoiseKind::Value => value3(p, seed),
180            NoiseKind::Perlin => perlin3(p, seed),
181            NoiseKind::Worley => {
182                let (f1, _) = worley3(p, seed);
183                1.0 - 2.0 * f1.min(1.0)
184            }
185            NoiseKind::Cellular => {
186                let (f1, f2) = worley3(p, seed);
187                ((f2 - f1) * 2.0).min(1.0) * 2.0 - 1.0
188            }
189            NoiseKind::Ridged => 1.0 - 2.0 * perlin3(p, seed).abs().min(1.0),
190            NoiseKind::Billow => 2.0 * perlin3(p, seed).abs().min(1.0) - 1.0,
191        }
192    }
193
194    fn fractal(&self, p: Vec3, seed: u32) -> f32 {
195        let (mut sum, mut amp, mut norm, mut q) = (0.0, 1.0, 0.0, p);
196        for o in 0..self.octaves.clamp(1, 32) {
197            sum += amp * self.single(q, seed.wrapping_add(o * 101));
198            norm += amp;
199            amp *= self.gain;
200            q *= self.lacunarity;
201        }
202        sum / norm
203    }
204
205    /// Samples three-dimensional fractal noise, nominally in -1..1.
206    /// Octaves are bounded to 1..32; non-finite inputs or results return zero.
207    pub fn sample3(&self, x: f32, y: f32, z: f32) -> f32 {
208        let mut p = Vec3::new(x, y, z) * self.frequency;
209        if self.warp > 0.0 {
210            let offset = Vec3::new(
211                self.fractal(p, self.seed ^ 0x1111),
212                self.fractal(p + Vec3::splat(17.3), self.seed ^ 0x2222),
213                self.fractal(p + Vec3::splat(41.7), self.seed ^ 0x3333),
214            );
215            p += offset * self.warp;
216        }
217        let sample = self.fractal(p, self.seed);
218        if sample.is_finite() {
219            sample.clamp(-1.0, 1.0)
220        } else {
221            0.0
222        }
223    }
224
225    /// Samples the XY plane of the three-dimensional noise at Z = 0.
226    pub fn sample2(&self, x: f32, y: f32) -> f32 {
227        self.sample3(x, y, 0.0)
228    }
229
230    /// Samples noise remapped and clamped into 0..1.
231    pub fn sample01(&self, x: f32, y: f32, z: f32) -> f32 {
232        self.sample3(x, y, z) * 0.5 + 0.5
233    }
234
235    /// Divergence-free 2D flow field (for swirling particles, smoke, water).
236    pub fn curl2(&self, x: f32, y: f32) -> Vec2 {
237        let e = 0.01 / self.frequency.max(1e-3);
238        let dx = (self.sample2(x + e, y) - self.sample2(x - e, y)) / (2.0 * e);
239        let dy = (self.sample2(x, y + e) - self.sample2(x, y - e)) / (2.0 * e);
240        Vec2::new(dy, -dx)
241    }
242
243    /// RGBA8 image with two colours blended by the noise value (a at 0, b at 1).
244    /// Returns an empty image on invalid dimensions. Use [`Self::try_image`] for errors.
245    pub fn image(&self, width: u32, height: u32, a: [f32; 3], b: [f32; 3]) -> Vec<u8> {
246        self.try_image(width, height, a, b).unwrap_or_default()
247    }
248
249    /// Generates RGBA8 pixels with checked dimensions and a 16-million-pixel budget.
250    /// Rejects zero sizes, non-finite colours and allocation-size overflow.
251    pub fn try_image(&self, width: u32, height: u32, a: [f32; 3], b: [f32; 3]) -> Result<Vec<u8>, String> {
252        let pixels = (width as usize).checked_mul(height as usize).ok_or("image dimensions overflow")?;
253        if pixels == 0 || pixels > 16_777_216 || a.iter().chain(b.iter()).any(|c| !c.is_finite()) {
254            return Err("noise image requires finite colours and 1..16777216 pixels".into());
255        }
256        let mut out = Vec::with_capacity(pixels * 4);
257        for y in 0..height {
258            for x in 0..width {
259                let t = self.sample01(x as f32 / width as f32, y as f32 / height as f32, 0.0);
260                for c in 0..3 {
261                    out.push(((lerp(a[c], b[c], t)).clamp(0.0, 1.0) * 255.0) as u8);
262                }
263                out.push(255);
264            }
265        }
266        Ok(out)
267    }
268}
269
270/// Prefix for serialized procedural noise texture keys.
271pub const TEXTURE_PREFIX: &str = "noise:";
272
273/// A texture description that fits in `MeshRenderer.texture`, e.g. from a script:
274/// `mr.texture = NoiseTexture::new(Noise::new(NoiseKind::Worley)).key();`
275#[derive(Clone, Copy, Debug, PartialEq)]
276pub struct NoiseTexture {
277    /// Fractal noise parameters sampled across the generated square.
278    pub noise: Noise,
279    /// Square output image edge length in pixels.
280    pub size: u32,
281    /// RGB colour at noise value zero, normally in 0..1.
282    pub dark: [f32; 3],
283    /// RGB colour at noise value one, normally in 0..1.
284    pub light: [f32; 3],
285}
286
287impl NoiseTexture {
288    /// Creates a 256-pixel grayscale texture descriptor for this noise.
289    pub fn new(noise: Noise) -> Self {
290        Self { noise, size: 256, dark: [0.05, 0.05, 0.06], light: [0.95, 0.95, 0.97] }
291    }
292
293    /// Builder: replaces the low/high RGB colours.
294    pub fn colors(mut self, dark: [f32; 3], light: [f32; 3]) -> Self {
295        self.dark = dark;
296        self.light = light;
297        self
298    }
299
300    /// Encodes the complete descriptor as a procedural texture cache key.
301    pub fn key(&self) -> String {
302        use crate::reflect::Choice;
303        let n = &self.noise;
304        format!(
305            "{TEXTURE_PREFIX}{}:{},{},{},{},{},{},{},{},{},{},{},{},{}",
306            NoiseKind::OPTIONS[n.kind.to_index()],
307            n.seed,
308            n.frequency,
309            n.octaves,
310            n.lacunarity,
311            n.gain,
312            n.warp,
313            self.size,
314            self.dark[0],
315            self.dark[1],
316            self.dark[2],
317            self.light[0],
318            self.light[1],
319            self.light[2]
320        )
321    }
322
323    /// Parses a procedural texture key; malformed input returns None.
324    pub fn parse(key: &str) -> Option<NoiseTexture> {
325        use crate::reflect::Choice;
326        let (kind, rest) = key.strip_prefix(TEXTURE_PREFIX)?.split_once(':')?;
327        let kind = NoiseKind::OPTIONS.iter().position(|o| *o == kind).map(NoiseKind::from_index)?;
328        let n: Vec<&str> = rest.split(',').collect();
329        if n.len() != 13 {
330            return None;
331        }
332        let real = |i: usize| n[i].parse::<f32>().ok().filter(|v| v.is_finite());
333        Some(NoiseTexture {
334            noise: Noise {
335                kind,
336                seed: n[0].parse().ok()?,
337                frequency: real(1)?,
338                octaves: n[2].parse().ok()?,
339                lacunarity: real(3)?,
340                gain: real(4)?,
341                warp: real(5)?,
342            },
343            size: n[6].parse::<u32>().ok()?.clamp(8, 2048),
344            dark: [real(7)?, real(8)?, real(9)?],
345            light: [real(10)?, real(11)?, real(12)?],
346        })
347    }
348
349    /// Generates tightly packed RGBA8 pixels in row-major order, clamping size to 8..2048.
350    pub fn rgba(&self) -> Vec<u8> {
351        let size = self.size.clamp(8, 2048);
352        self.noise.image(size, size, self.dark, self.light)
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    #[test]
361    fn deterministic_and_in_range() {
362        for kind in [NoiseKind::Value, NoiseKind::Perlin, NoiseKind::Worley, NoiseKind::Ridged, NoiseKind::Billow, NoiseKind::Cellular] {
363            let n = Noise { kind, octaves: 3, warp: 0.4, ..Default::default() }.with_seed(7);
364            let (mut lo, mut hi) = (f32::MAX, f32::MIN);
365            for i in 0..2000 {
366                let (x, y) = ((i % 50) as f32 * 0.37, (i / 50) as f32 * 0.41);
367                let v = n.sample2(x, y);
368                assert!((-1.0..=1.0).contains(&v), "{kind:?} out of range: {v}");
369                assert_eq!(v, n.sample2(x, y), "{kind:?} not deterministic");
370                lo = lo.min(v);
371                hi = hi.max(v);
372            }
373            assert!(hi - lo > 0.3, "{kind:?} is nearly constant ({lo}..{hi})");
374        }
375    }
376
377    #[test]
378    fn different_seeds_differ_and_noise_is_continuous() {
379        let a = Noise::default().with_seed(1).sample2(3.3, 4.4);
380        let b = Noise::default().with_seed(2).sample2(3.3, 4.4);
381        assert_ne!(a, b);
382        let n = Noise::default();
383        assert!((n.sample2(1.0, 1.0) - n.sample2(1.0005, 1.0)).abs() < 0.01);
384    }
385
386    #[test]
387    fn texture_key_roundtrip() {
388        let t = NoiseTexture::new(Noise::new(NoiseKind::Worley).with_seed(5).with_frequency(8.0)).colors([0.1, 0.2, 0.3], [0.9, 0.8, 0.7]);
389        assert_eq!(NoiseTexture::parse(&t.key()), Some(t));
390        assert_eq!(t.rgba().len(), (t.size * t.size * 4) as usize);
391    }
392}