src/noise.rs 2.9 K · 93 lines · raw · history

1 use bevy::prelude::*;
2
3 use crate::world::hash2;
4
5 #[derive(Clone, Copy, Debug)]
6 pub struct Noise {
7 seed: u32,
8 }
9
10 impl Noise {
11 pub fn new(seed: u32) -> Self {
12 Self { seed }
13 }
14
15 fn gradient(&self, cell: IVec2) -> Vec2 {
16 let h = hash2(
17 (cell.x as u32).wrapping_add(self.seed.wrapping_mul(0x632B_E5AB)),
18 (cell.y as u32) ^ self.seed.rotate_left(16),
19 );
20 Vec2::from_angle(h as f32 / u32::MAX as f32 * std::f32::consts::TAU)
21 }
22
23 pub fn sample(&self, p: Vec2) -> f32 {
24 let cell = p.floor();
25 let f = p - cell;
26 let cell = cell.as_ivec2();
27 let corner = |offset: IVec2| self.gradient(cell + offset).dot(f - offset.as_vec2());
28 let u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
29 let bottom = corner(IVec2::new(0, 0)).lerp(corner(IVec2::new(1, 0)), u.x);
30 let top = corner(IVec2::new(0, 1)).lerp(corner(IVec2::new(1, 1)), u.x);
31 (bottom.lerp(top, u.y) * std::f32::consts::SQRT_2).clamp(-1.0, 1.0)
32 }
33
34 pub fn fbm(&self, p: Vec2, octaves: u32) -> f32 {
35 let (mut sum, mut amplitude, mut frequency, mut total) = (0.0, 1.0, 1.0, 0.0);
36 for octave in 0..octaves {
37 let layer = Noise::new(self.seed.wrapping_add(octave));
38 sum += layer.sample(p * frequency) * amplitude;
39 total += amplitude;
40 amplitude *= 0.5;
41 frequency *= 2.0;
42 }
43 sum / total
44 }
45 }
46
47 #[cfg(test)]
48 mod tests {
49 use super::*;
50
51 fn grid() -> impl Iterator<Item = Vec2> {
52 (0..40).flat_map(|y| (0..40).map(move |x| Vec2::new(x as f32 * 0.37, y as f32 * 0.29)))
53 }
54
55 #[test]
56 fn noise_is_deterministic_per_seed() {
57 let (a, b, c) = (Noise::new(3), Noise::new(3), Noise::new(4));
58 assert!(grid().all(|p| a.fbm(p, 4) == b.fbm(p, 4)));
59 assert!(grid().any(|p| a.fbm(p, 4) != c.fbm(p, 4)));
60 }
61
62 #[test]
63 fn noise_stays_in_range() {
64 let noise = Noise::new(9);
65 assert!(grid().all(|p| (-1.0..=1.0).contains(&noise.sample(p))));
66 assert!(grid().all(|p| (-1.0..=1.0).contains(&noise.fbm(p, 5))));
67 }
68
69 #[test]
70 fn noise_is_zero_on_lattice_points() {
71 let noise = Noise::new(1);
72 assert_eq!(noise.sample(Vec2::new(5.0, -3.0)), 0.0);
73 }
74
75 // Terrain is thresholded from noise; discontinuities would show up as speckled, noisy borders.
76 #[test]
77 fn noise_is_continuous() {
78 let noise = Noise::new(5);
79 for p in grid() {
80 let d = (noise.sample(p) - noise.sample(p + Vec2::splat(0.001))).abs();
81 assert!(d < 0.01, "jump of {d} at {p}");
82 }
83 }
84
85 #[test]
86 fn negative_coordinates_work() {
87 let noise = Noise::new(2);
88 let values: Vec<f32> = (0..20)
89 .map(|i| noise.sample(Vec2::new(-i as f32 * 0.7 - 0.3, -2.4)))
90 .collect();
91 assert!(values.iter().any(|v| *v != 0.0));
92 }
93 }