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        Self::with_noise_offset_scale(random, 256.0)
41    }
42
43    /// Creates a simplex sampler whose random offsets are discarded.
44    ///
45    /// Vanilla still consumes the three offset samples when the scale is zero.
46    pub fn new_without_noise_offset<R: Random>(random: &mut R) -> Self {
47        Self::with_noise_offset_scale(random, 0.0)
48    }
49
50    fn with_noise_offset_scale<R: Random>(random: &mut R, noise_offset_scale: f64) -> Self {
51        let xo = random.next_f64() * noise_offset_scale;
52        let yo = random.next_f64() * noise_offset_scale;
53        let zo = random.next_f64() * noise_offset_scale;
54
55        let mut p = [0i32; 512];
56
57        // Initialize identity permutation
58        for (i, val) in p.iter_mut().enumerate().take(256) {
59            *val = i as i32;
60        }
61
62        // Fisher-Yates shuffle matching vanilla's loop
63        for i in 0..256 {
64            let offset = random.next_i32_bounded((256 - i) as i32) as usize;
65            p.swap(i, offset + i);
66        }
67
68        // Mirror first 256 entries to second half (matching vanilla)
69        for i in 0..256 {
70            p[i + 256] = p[i];
71        }
72
73        Self { p, xo, yo, zo }
74    }
75
76    #[inline]
77    const fn p(&self, x: i32) -> i32 {
78        self.p[(x & 0xFF) as usize]
79    }
80
81    /// Sample 2D simplex noise at the given coordinates.
82    ///
83    /// Returns a value typically in the range `[-1, 1]` (scaled by 70).
84    #[must_use]
85    pub fn get_value_2d(&self, xin: f64, yin: f64) -> f32 {
86        let s = (xin + yin) * F2;
87        let i = fast_floor(xin + s);
88        let j = fast_floor(yin + s);
89        let t = f64::from(i + j) * G2;
90        let x0 = xin - (f64::from(i) - t);
91        let y0 = yin - (f64::from(j) - t);
92
93        // Determine which simplex triangle we're in
94        let (i1, j1) = if x0 > y0 { (1, 0) } else { (0, 1) };
95
96        let x1 = x0 - f64::from(i1) + G2;
97        let y1 = y0 - f64::from(j1) + G2;
98        let x2 = x0 - 1.0 + 2.0 * G2;
99        let y2 = y0 - 1.0 + 2.0 * G2;
100
101        let ii = i & 0xFF;
102        let jj = j & 0xFF;
103        let gi0 = (self.p(ii + self.p(jj)) % 12) as usize;
104        let gi1 = (self.p(ii + i1 + self.p(jj + j1)) % 12) as usize;
105        let gi2 = (self.p(ii + 1 + self.p(jj + 1)) % 12) as usize;
106
107        let n0 = corner_noise_3d(gi0, x0, y0, 0.0, 0.5);
108        let n1 = corner_noise_3d(gi1, x1, y1, 0.0, 0.5);
109        let n2 = corner_noise_3d(gi2, x2, y2, 0.0, 0.5);
110
111        (70.0 * (n0 + n1 + n2)) as f32
112    }
113
114    /// Skewing factor for 3D simplex: `1/3`
115    const F3: f64 = 1.0 / 3.0;
116    /// Unskewing factor for 3D simplex: `1/6`
117    const G3: f64 = 1.0 / 6.0;
118
119    /// Sample 3D simplex noise at the given coordinates.
120    ///
121    /// Returns a value typically in the range `[-1, 1]` (scaled by 32).
122    #[must_use]
123    #[expect(
124        clippy::many_single_char_names,
125        reason = "matches vanilla simplex noise math notation"
126    )]
127    pub fn get_value_3d(&self, xin: f64, yin: f64, zin: f64) -> f32 {
128        let s = (xin + yin + zin) * Self::F3;
129        let i = fast_floor(xin + s);
130        let j = fast_floor(yin + s);
131        let k = fast_floor(zin + s);
132        let t = f64::from(i + j + k) * Self::G3;
133        let x0 = xin - (f64::from(i) - t);
134        let y0 = yin - (f64::from(j) - t);
135        let z0 = zin - (f64::from(k) - t);
136
137        // Determine which simplex tetrahedron we're in
138        let (i1, j1, k1, i2, j2, k2) = if x0 >= y0 {
139            if y0 >= z0 {
140                (1, 0, 0, 1, 1, 0)
141            } else if x0 >= z0 {
142                (1, 0, 0, 1, 0, 1)
143            } else {
144                (0, 0, 1, 1, 0, 1)
145            }
146        } else if y0 < z0 {
147            (0, 0, 1, 0, 1, 1)
148        } else if x0 < z0 {
149            (0, 1, 0, 0, 1, 1)
150        } else {
151            (0, 1, 0, 1, 1, 0)
152        };
153
154        let x1 = x0 - f64::from(i1) + Self::G3;
155        let y1 = y0 - f64::from(j1) + Self::G3;
156        let z1 = z0 - f64::from(k1) + Self::G3;
157        let x2 = x0 - f64::from(i2) + Self::F3;
158        let y2 = y0 - f64::from(j2) + Self::F3;
159        let z2 = z0 - f64::from(k2) + Self::F3;
160        let x3 = x0 - 1.0 + 0.5;
161        let y3 = y0 - 1.0 + 0.5;
162        let z3 = z0 - 1.0 + 0.5;
163
164        let ii = i & 0xFF;
165        let jj = j & 0xFF;
166        let kk = k & 0xFF;
167        let gi0 = (self.p(ii + self.p(jj + self.p(kk))) % 12) as usize;
168        let gi1 = (self.p(ii + i1 + self.p(jj + j1 + self.p(kk + k1))) % 12) as usize;
169        let gi2 = (self.p(ii + i2 + self.p(jj + j2 + self.p(kk + k2))) % 12) as usize;
170        let gi3 = (self.p(ii + 1 + self.p(jj + 1 + self.p(kk + 1))) % 12) as usize;
171
172        let n0 = corner_noise_3d(gi0, x0, y0, z0, 0.6);
173        let n1 = corner_noise_3d(gi1, x1, y1, z1, 0.6);
174        let n2 = corner_noise_3d(gi2, x2, y2, z2, 0.6);
175        let n3 = corner_noise_3d(gi3, x3, y3, z3, 0.6);
176
177        (32.0 * (n0 + n1 + n2 + n3)) as f32
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::random::legacy_random::LegacyRandom;
185
186    #[test]
187    fn test_simplex_noise_deterministic() {
188        let mut rng = LegacyRandom::from_seed(42);
189        let noise1 = SimplexNoise::new(&mut rng);
190
191        let mut rng = LegacyRandom::from_seed(42);
192        let noise2 = SimplexNoise::new(&mut rng);
193
194        for i in 0..10 {
195            let x = f64::from(i) * 13.7;
196            let z = f64::from(i) * 7.3;
197            // Determinism test: identical inputs must produce identical outputs
198            assert_eq!(noise1.get_value_2d(x, z), noise2.get_value_2d(x, z));
199        }
200    }
201
202    #[test]
203    fn test_simplex_2d_spatial_variation() {
204        let mut rng = LegacyRandom::from_seed(0);
205        let noise = SimplexNoise::new(&mut rng);
206
207        let values: Vec<f32> = (0..20)
208            .map(|i| noise.get_value_2d(f64::from(i) * 50.0, f64::from(i) * 30.0))
209            .collect();
210
211        let min = values.iter().copied().fold(f32::INFINITY, f32::min);
212        let max = values.iter().copied().fold(f32::NEG_INFINITY, f32::max);
213        assert!(max - min > 0.01, "2D simplex should have spatial variation");
214    }
215
216    /// Verify the end-islands noise initialization: seed 0, consumeCount(17292).
217    #[test]
218    fn test_end_islands_noise_init() {
219        let mut rng = LegacyRandom::from_seed(0);
220        rng.consume_count(17292);
221        let noise = SimplexNoise::new(&mut rng);
222
223        // Verify the noise produces a finite, non-zero value at a known coordinate
224        let v = noise.get_value_2d(10.0, 10.0);
225        assert!(v.is_finite(), "Noise should produce a finite value");
226        assert!(
227            v.abs() > 1e-10,
228            "Noise at (10, 10) should be non-zero, got {v}"
229        );
230    }
231}