1use 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;
14const F2: f64 = 0.5 * (SQRT_3 - 1.0);
16const G2: f64 = (3.0 - SQRT_3) / 6.0;
18
19#[derive(Debug, Clone)]
24pub struct SimplexNoise {
25 p: [i32; 512],
26 pub xo: f64,
28 pub yo: f64,
30 pub zo: f64,
32}
33
34impl SimplexNoise {
35 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 for (i, val) in p.iter_mut().enumerate().take(256) {
48 *val = i as i32;
49 }
50
51 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 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 #[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 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 const F3: f64 = 1.0 / 3.0;
105 const G3: f64 = 1.0 / 6.0;
107
108 #[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 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 {
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 #[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 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}