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 Self::with_noise_offset_scale(random, 256.0)
41 }
42
43 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 for (i, val) in p.iter_mut().enumerate().take(256) {
59 *val = i as i32;
60 }
61
62 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 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 #[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 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 const F3: f64 = 1.0 / 3.0;
116 const G3: f64 = 1.0 / 6.0;
118
119 #[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 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 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 #[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 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}