Skip to main content

steel_worldgen/noise/
simplex_noise.rs

1//! Simplex noise implementation matching vanilla Minecraft's `SimplexNoise.java`.
2//!
3//! Used by the End islands density function for terrain generation in The End dimension.
4//! Supports 2D and 3D sampling with the same gradient vectors as Perlin noise.
5
6use crate::random::Random;
7use steel_math::{corner_noise_3d, fast_floor};
8
9#[expect(
10    clippy::unreadable_literal,
11    reason = "exact mathematical constant; underscores would obscure precision"
12)]
13const SQRT_3: f64 = 1.7320508075688772;
14/// Skewing factor for 2D simplex: `0.5 * (sqrt(3) - 1)`
15const F2: f64 = 0.5 * (SQRT_3 - 1.0);
16/// Unskewing factor for 2D simplex: `(3 - sqrt(3)) / 6`
17const G2: f64 = (3.0 - SQRT_3) / 6.0;
18
19/// Simplex noise generator matching vanilla's `SimplexNoise.java`.
20///
21/// Unlike `ImprovedNoise` which uses 256-byte permutation tables, this uses a
22/// 512-entry `i32` permutation table (first 256 entries shuffled, mirrored to second half).
23#[derive(Debug, Clone)]
24pub struct SimplexNoise {
25    p: [i32; 512],
26    /// X offset for the noise coordinates.
27    pub xo: f64,
28    /// Y offset for the noise coordinates.
29    pub yo: f64,
30    /// Z offset for the noise coordinates.
31    pub zo: f64,
32}
33
34impl SimplexNoise {
35    /// Create a new simplex noise generator from a random source.
36    ///
37    /// Matches vanilla's `SimplexNoise(RandomSource)` constructor:
38    /// consumes 3 doubles for offsets, then shuffles a 256-entry permutation table.
39    pub fn new<R: Random>(random: &mut R) -> Self {
40        let xo = random.next_f64() * 256.0;
41        let yo = random.next_f64() * 256.0;
42        let zo = random.next_f64() * 256.0;
43
44        let mut p = [0i32; 512];
45
46        // Initialize identity permutation
47        for (i, val) in p.iter_mut().enumerate().take(256) {
48            *val = i as i32;
49        }
50
51        // Fisher-Yates shuffle matching vanilla's loop
52        for i in 0..256 {
53            let offset = random.next_i32_bounded((256 - i) as i32) as usize;
54            p.swap(i, offset + i);
55        }
56
57        // Mirror first 256 entries to second half (matching vanilla)
58        for i in 0..256 {
59            p[i + 256] = p[i];
60        }
61
62        Self { p, xo, yo, zo }
63    }
64
65    #[inline]
66    const fn p(&self, x: i32) -> i32 {
67        self.p[(x & 0xFF) as usize]
68    }
69
70    /// Sample 2D simplex noise at the given coordinates.
71    ///
72    /// Returns a value typically in the range `[-1, 1]` (scaled by 70).
73    #[must_use]
74    pub fn get_value_2d(&self, xin: f64, yin: f64) -> f64 {
75        let s = (xin + yin) * F2;
76        let i = fast_floor(xin + s);
77        let j = fast_floor(yin + s);
78        let t = f64::from(i + j) * G2;
79        let x0 = xin - (f64::from(i) - t);
80        let y0 = yin - (f64::from(j) - t);
81
82        // Determine which simplex triangle we're in
83        let (i1, j1) = if x0 > y0 { (1, 0) } else { (0, 1) };
84
85        let x1 = x0 - f64::from(i1) + G2;
86        let y1 = y0 - f64::from(j1) + G2;
87        let x2 = x0 - 1.0 + 2.0 * G2;
88        let y2 = y0 - 1.0 + 2.0 * G2;
89
90        let ii = i & 0xFF;
91        let jj = j & 0xFF;
92        let gi0 = (self.p(ii + self.p(jj)) % 12) as usize;
93        let gi1 = (self.p(ii + i1 + self.p(jj + j1)) % 12) as usize;
94        let gi2 = (self.p(ii + 1 + self.p(jj + 1)) % 12) as usize;
95
96        let n0 = corner_noise_3d(gi0, x0, y0, 0.0, 0.5);
97        let n1 = corner_noise_3d(gi1, x1, y1, 0.0, 0.5);
98        let n2 = corner_noise_3d(gi2, x2, y2, 0.0, 0.5);
99
100        70.0 * (n0 + n1 + n2)
101    }
102
103    /// Skewing factor for 3D simplex: `1/3`
104    const F3: f64 = 1.0 / 3.0;
105    /// Unskewing factor for 3D simplex: `1/6`
106    const G3: f64 = 1.0 / 6.0;
107
108    /// Sample 3D simplex noise at the given coordinates.
109    ///
110    /// Returns a value typically in the range `[-1, 1]` (scaled by 32).
111    #[must_use]
112    #[expect(
113        clippy::many_single_char_names,
114        reason = "matches vanilla simplex noise math notation"
115    )]
116    pub fn get_value_3d(&self, xin: f64, yin: f64, zin: f64) -> f64 {
117        let s = (xin + yin + zin) * Self::F3;
118        let i = fast_floor(xin + s);
119        let j = fast_floor(yin + s);
120        let k = fast_floor(zin + s);
121        let t = f64::from(i + j + k) * Self::G3;
122        let x0 = xin - (f64::from(i) - t);
123        let y0 = yin - (f64::from(j) - t);
124        let z0 = zin - (f64::from(k) - t);
125
126        // Determine which simplex tetrahedron we're in
127        let (i1, j1, k1, i2, j2, k2) = if x0 >= y0 {
128            if y0 >= z0 {
129                (1, 0, 0, 1, 1, 0)
130            } else if x0 >= z0 {
131                (1, 0, 0, 1, 0, 1)
132            } else {
133                (0, 0, 1, 1, 0, 1)
134            }
135        } else if y0 < z0 {
136            (0, 0, 1, 0, 1, 1)
137        } else if x0 < z0 {
138            (0, 1, 0, 0, 1, 1)
139        } else {
140            (0, 1, 0, 1, 1, 0)
141        };
142
143        let x1 = x0 - f64::from(i1) + Self::G3;
144        let y1 = y0 - f64::from(j1) + Self::G3;
145        let z1 = z0 - f64::from(k1) + Self::G3;
146        let x2 = x0 - f64::from(i2) + Self::F3;
147        let y2 = y0 - f64::from(j2) + Self::F3;
148        let z2 = z0 - f64::from(k2) + Self::F3;
149        let x3 = x0 - 1.0 + 0.5;
150        let y3 = y0 - 1.0 + 0.5;
151        let z3 = z0 - 1.0 + 0.5;
152
153        let ii = i & 0xFF;
154        let jj = j & 0xFF;
155        let kk = k & 0xFF;
156        let gi0 = (self.p(ii + self.p(jj + self.p(kk))) % 12) as usize;
157        let gi1 = (self.p(ii + i1 + self.p(jj + j1 + self.p(kk + k1))) % 12) as usize;
158        let gi2 = (self.p(ii + i2 + self.p(jj + j2 + self.p(kk + k2))) % 12) as usize;
159        let gi3 = (self.p(ii + 1 + self.p(jj + 1 + self.p(kk + 1))) % 12) as usize;
160
161        let n0 = corner_noise_3d(gi0, x0, y0, z0, 0.6);
162        let n1 = corner_noise_3d(gi1, x1, y1, z1, 0.6);
163        let n2 = corner_noise_3d(gi2, x2, y2, z2, 0.6);
164        let n3 = corner_noise_3d(gi3, x3, y3, z3, 0.6);
165
166        32.0 * (n0 + n1 + n2 + n3)
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::random::legacy_random::LegacyRandom;
174
175    #[test]
176    fn test_simplex_noise_deterministic() {
177        let mut rng = LegacyRandom::from_seed(42);
178        let noise1 = SimplexNoise::new(&mut rng);
179
180        let mut rng = LegacyRandom::from_seed(42);
181        let noise2 = SimplexNoise::new(&mut rng);
182
183        for i in 0..10 {
184            let x = f64::from(i) * 13.7;
185            let z = f64::from(i) * 7.3;
186            #[expect(
187                clippy::float_cmp,
188                reason = "determinism test: identical inputs must produce bit-identical outputs"
189            )]
190            // Determinism test: identical inputs must produce identical outputs
191            {
192                assert_eq!(noise1.get_value_2d(x, z), noise2.get_value_2d(x, z));
193            }
194        }
195    }
196
197    #[test]
198    fn test_simplex_2d_spatial_variation() {
199        let mut rng = LegacyRandom::from_seed(0);
200        let noise = SimplexNoise::new(&mut rng);
201
202        let values: Vec<f64> = (0..20)
203            .map(|i| noise.get_value_2d(f64::from(i) * 50.0, f64::from(i) * 30.0))
204            .collect();
205
206        let min = values.iter().copied().fold(f64::INFINITY, f64::min);
207        let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
208        assert!(max - min > 0.01, "2D simplex should have spatial variation");
209    }
210
211    /// Verify the end-islands noise initialization: seed 0, consumeCount(17292).
212    #[test]
213    fn test_end_islands_noise_init() {
214        let mut rng = LegacyRandom::from_seed(0);
215        rng.consume_count(17292);
216        let noise = SimplexNoise::new(&mut rng);
217
218        // Verify the noise produces a finite, non-zero value at a known coordinate
219        let v = noise.get_value_2d(10.0, 10.0);
220        assert!(v.is_finite(), "Noise should produce a finite value");
221        assert!(
222            v.abs() > 1e-10,
223            "Noise at (10, 10) should be non-zero, got {v}"
224        );
225    }
226}