Skip to main content

banished_core/
noise.rs

1//! The engine's Perlin noise, which the terrain generator uses.
2//!
3//! It is value noise: a hash gives a value for each integer point, the values are smoothed over the
4//! 3 × 3 neighbours and joined with cosine interpolation, and several octaves are added. Every
5//! float operation is single precision and in the original's order, and `cosf` is the C runtime's
6//! ([`crate::crt::cosf`]), so the values match the original to the last bit.
7//!
8//! ```
9//! use banished_core::noise::PerlinNoise;
10//!
11//! let noise = PerlinNoise::new(0.5, 4.0, 17, 31);
12//! let value = noise.generate(12.5, 40.25);
13//! assert!(value.is_finite());
14//! ```
15//!
16//! Original: `Utility::PerlinNoise`, Runtime `0x18010ad80` to `0x18010b520`.
17
18#[derive(Debug, Clone, Copy, PartialEq)]
19/// A noise field: its persistence, octaves and two seeds.
20pub struct PerlinNoise {
21    /// How much each octave adds: octave `i` is scaled by `persistence` to the power `i`.
22    pub persistence: f32,
23    /// The number of octaves. The loop runs while `(float)i < octaves`.
24    pub octaves: f32,
25    /// The first seed of the hash.
26    pub seed_x: i32,
27    /// The second seed of the hash.
28    pub seed_y: i32,
29}
30
31const PI: f32 = std::f32::consts::PI;
32
33fn cosf(x: f32) -> f32 {
34    crate::crt::cosf(x)
35}
36
37/// `powf` for the exponents the generator uses (small non-negative integers).
38fn powf(base: f32, exp: f32) -> f32 {
39    (base as f64).powf(exp as f64) as f32
40}
41
42impl PerlinNoise {
43    /// A noise field with these settings.
44    ///
45    /// Original: `PerlinNoise(float, float, int, int)`, Runtime `0x18010b500`.
46    pub fn new(persistence: f32, octaves: f32, seed_x: i32, seed_y: i32) -> Self {
47        PerlinNoise {
48            persistence,
49            octaves,
50            seed_x,
51            seed_y,
52        }
53    }
54
55    #[inline]
56    fn hash(&self, x: i32, y: i32) -> u32 {
57        (self.seed_y.wrapping_add(y) as u32)
58            .wrapping_mul(0x39)
59            .wrapping_add(self.seed_x as u32)
60            .wrapping_add(x as u32)
61    }
62
63    #[inline]
64    fn value(n: u32) -> f32 {
65        let n = n ^ n.wrapping_mul(0x2000);
66        let m = n
67            .wrapping_mul(n)
68            .wrapping_mul(0x3d73)
69            .wrapping_add(0xc0ae5)
70            .wrapping_mul(n)
71            .wrapping_add(0xd208dd0d)
72            & 0x7fff_ffff;
73        1.0 - m as f32 * 9.313_226e-10
74    }
75
76    /// The hashed value at an integer point, from -1 to 1.
77    ///
78    /// Original: `Random(int, int)`, Runtime `0x18010ad80`.
79    pub fn random(&self, x: i32, y: i32) -> f32 {
80        Self::value(self.hash(x, y))
81    }
82
83    /// The value at an integer point, smoothed with its 8 neighbours: corners / 16 + edges / 8 +
84    /// centre / 4, added in the original's order.
85    ///
86    /// Original: `SmoothNoise(int, int)`, Runtime `0x18010ae50`.
87    pub fn smooth_noise(&self, x: i32, y: i32) -> f32 {
88        let c = self.hash(x, y);
89        let v = |d: i32| Self::value(c.wrapping_add(d as u32));
90        let corners = v(-0x38) + v(-0x3a) + v(0x38) + v(0x3a);
91        let sides = v(1) + v(-1) + v(-0x39) + v(0x39);
92        corners * 0.0625 + sides * 0.125 + v(0) * 0.25
93    }
94
95    /// The noise at a point: every octave, interpolated and added.
96    ///
97    /// Original: `Generate(float, float)`, Runtime `0x18010b240`.
98    pub fn generate(&self, x: f32, y: f32) -> f32 {
99        let mut total = 0.0f32;
100        let mut i = 0i32;
101        while (i as f32) < self.octaves {
102            let freq = powf(2.0, i as f32);
103            let (fx, fy) = (freq * x, freq * y);
104            let (ix, iy) = (fx as i32, fy as i32);
105            let a = (1.0 - cosf((fx - ix as f32) * PI)) * 0.5;
106            let b = (1.0 - cosf((fy - iy as f32) * PI)) * 0.5;
107            let top = self.smooth_noise(ix, iy) * (1.0 - a) + self.smooth_noise(ix + 1, iy) * a;
108            let bottom =
109                self.smooth_noise(ix, iy + 1) * (1.0 - a) + self.smooth_noise(ix + 1, iy + 1) * a;
110            let v = top * (1.0 - b) + bottom * b;
111            total += v * powf(self.persistence, i as f32);
112            i += 1;
113        }
114        total
115    }
116}
117
118#[cfg(test)]
119mod golden {
120    //! Values produced by the original Runtime DLL under emulation (tools/emu/check_random.py).
121    use super::*;
122
123    #[test]
124    fn matches_original_engine() {
125        let n = PerlinNoise::new(0.5, 5.0, 12345, -6789);
126        for (x, y, expected) in [
127            (0.0f32, 0.0f32, 0.6994447112083435f32),
128            (1.25, 7.5, -0.048602547496557236),
129            (100.75, 33.1, 0.27891477942466736),
130            (511.5, 255.25, 0.1838667392730713),
131        ] {
132            assert_eq!(n.generate(x, y).to_bits(), expected.to_bits(), "({x}, {y})");
133        }
134    }
135}