1use crate::noise::ImprovedNoise;
4use crate::random::RandomSource;
5use std::array;
6use std::simd::cmp::SimdPartialEq;
7use std::simd::num::SimdFloat;
8use std::simd::{Select, Simd};
9use steel_math::clamped_lerp;
10
11const BASE_SCALE: f64 = 684.412;
12
13#[derive(Debug, Clone)]
14struct SmearedLayer {
15 noise: ImprovedNoise,
16 frequency: f64,
17 amplitude: f32,
18 fudge_y_scale: f64,
19}
20
21#[derive(Debug, Clone)]
22struct SmearedStack {
23 layers: Box<[SmearedLayer]>,
24}
25
26impl SmearedStack {
27 fn create(
28 random: &mut RandomSource,
29 first_octave: i32,
30 smear_y_scale: f64,
31 mut value_factor: f64,
32 ) -> Self {
33 let octaves = (-first_octave + 1) as usize;
34 value_factor /= 2.0_f64.powi(octaves as i32) - 1.0;
35 let mut frequency = 1.0;
36 let mut layers = Vec::with_capacity(octaves);
37
38 for _ in (0..octaves).rev() {
40 layers.push(SmearedLayer {
41 noise: ImprovedNoise::new(random),
42 frequency,
43 amplitude: value_factor as f32,
44 fudge_y_scale: smear_y_scale * frequency,
45 });
46 frequency *= 0.5;
47 value_factor *= 2.0;
48 }
49
50 Self {
51 layers: layers.into_boxed_slice(),
52 }
53 }
54
55 #[inline]
56 fn sample(&self, x: f64, y: f64, z: f64) -> f32 {
57 let mut value = 0.0_f32;
58 for layer in &self.layers {
59 value += layer.amplitude
60 * layer.noise.smeared_noise(
61 x * layer.frequency,
62 y * layer.frequency,
63 z * layer.frequency,
64 layer.fudge_y_scale,
65 );
66 }
67 value
68 }
69
70 #[inline]
71 fn sample_y_simd<const N: usize>(&self, x: f64, ys: Simd<f64, N>, z: f64) -> Simd<f32, N> {
72 let mut value = Simd::splat(0.0_f32);
73 for layer in &self.layers {
74 value += Simd::splat(layer.amplitude)
75 * layer.noise.smeared_noise_y_simd(
76 x * layer.frequency,
77 ys * Simd::splat(layer.frequency),
78 z * layer.frequency,
79 layer.fudge_y_scale,
80 );
81 }
82 value
83 }
84}
85
86#[derive(Debug, Clone)]
91pub struct BlendedNoise {
92 min_limit_noise: SmearedStack,
93 max_limit_noise: SmearedStack,
94 main_noise: SmearedStack,
95 xz_multiplier: f64,
96 y_multiplier: f64,
97 main_xz_scale: f64,
98 main_y_scale: f64,
99}
100
101impl BlendedNoise {
102 #[must_use]
104 pub fn new(
105 random: &mut RandomSource,
106 xz_scale: f64,
107 y_scale: f64,
108 xz_factor: f64,
109 y_factor: f64,
110 smear_scale_multiplier: f64,
111 ) -> Self {
112 let xz_multiplier = BASE_SCALE * xz_scale;
113 let y_multiplier = BASE_SCALE * y_scale;
114 let limit_smear_scale_y = y_multiplier * smear_scale_multiplier;
115 let main_smear_scale_y = limit_smear_scale_y / y_factor;
116
117 Self {
118 min_limit_noise: SmearedStack::create(
119 random,
120 -15,
121 limit_smear_scale_y,
122 f64::from(0.999_984_74_f32),
123 ),
124 max_limit_noise: SmearedStack::create(
125 random,
126 -15,
127 limit_smear_scale_y,
128 f64::from(0.999_984_74_f32),
129 ),
130 main_noise: SmearedStack::create(random, -7, main_smear_scale_y, 12.75),
131 xz_multiplier,
132 y_multiplier,
133 main_xz_scale: xz_multiplier / xz_factor,
134 main_y_scale: y_multiplier / y_factor,
135 }
136 }
137
138 #[inline]
139 #[must_use]
141 #[expect(
142 clippy::float_cmp,
143 reason = "Vanilla selects exact lerp endpoints before interpolation"
144 )]
145 pub fn compute(&self, block_x: f64, block_y: f64, block_z: f64) -> f32 {
146 let limit_x = block_x * self.xz_multiplier;
147 let limit_y = block_y * self.y_multiplier;
148 let limit_z = block_z * self.xz_multiplier;
149 let main = self.main_noise.sample(
150 block_x * self.main_xz_scale,
151 block_y * self.main_y_scale,
152 block_z * self.main_xz_scale,
153 );
154 let alpha = (main + 0.5_f32).clamp(0.0, 1.0);
155 if alpha == 0.0 {
156 return self.min_limit_noise.sample(limit_x, limit_y, limit_z);
157 }
158 if alpha == 1.0 {
159 return self.max_limit_noise.sample(limit_x, limit_y, limit_z);
160 }
161 let minimum = self.min_limit_noise.sample(limit_x, limit_y, limit_z);
162 let maximum = self.max_limit_noise.sample(limit_x, limit_y, limit_z);
163 clamped_lerp(minimum, maximum, alpha)
164 }
165
166 #[inline]
167 fn compute_y_simd<const N: usize>(
168 &self,
169 block_x: f64,
170 block_ys: Simd<f64, N>,
171 block_z: f64,
172 ) -> Simd<f32, N> {
173 let limit_x = block_x * self.xz_multiplier;
174 let limit_ys = block_ys * Simd::splat(self.y_multiplier);
175 let limit_z = block_z * self.xz_multiplier;
176 let main = self.main_noise.sample_y_simd(
177 block_x * self.main_xz_scale,
178 block_ys * Simd::splat(self.main_y_scale),
179 block_z * self.main_xz_scale,
180 );
181 let alpha = (main + Simd::splat(0.5_f32))
182 .simd_max(Simd::splat(0.0))
183 .simd_min(Simd::splat(1.0));
184 if alpha.simd_eq(Simd::splat(0.0)).all() {
185 return self
186 .min_limit_noise
187 .sample_y_simd(limit_x, limit_ys, limit_z);
188 }
189 if alpha.simd_eq(Simd::splat(1.0)).all() {
190 return self
191 .max_limit_noise
192 .sample_y_simd(limit_x, limit_ys, limit_z);
193 }
194 let minimum = self
195 .min_limit_noise
196 .sample_y_simd(limit_x, limit_ys, limit_z);
197 let maximum = self
198 .max_limit_noise
199 .sample_y_simd(limit_x, limit_ys, limit_z);
200 let interpolated = minimum + alpha * (maximum - minimum);
201 let result = alpha
202 .simd_eq(Simd::splat(0.0))
203 .select(minimum, interpolated);
204 alpha.simd_eq(Simd::splat(1.0)).select(maximum, result)
205 }
206
207 pub fn compute_column(&self, block_x: i32, block_ys: &[i32], block_z: i32, out: &mut [f32]) {
209 let len = block_ys.len().min(out.len());
210 let mut index = 0;
211 #[cfg(target_feature = "avx512f")]
213 while index + 8 <= len {
214 let ys: Simd<f64, 8> =
215 Simd::from_array(array::from_fn(|lane| f64::from(block_ys[index + lane])));
216 out[index..index + 8].copy_from_slice(
217 &self
218 .compute_y_simd(f64::from(block_x), ys, f64::from(block_z))
219 .to_array(),
220 );
221 index += 8;
222 }
223 while index + 4 <= len {
224 let ys: Simd<f64, 4> =
225 Simd::from_array(array::from_fn(|lane| f64::from(block_ys[index + lane])));
226 out[index..index + 4].copy_from_slice(
227 &self
228 .compute_y_simd(f64::from(block_x), ys, f64::from(block_z))
229 .to_array(),
230 );
231 index += 4;
232 }
233 if index + 2 <= len {
234 let ys: Simd<f64, 2> =
235 Simd::from_array(array::from_fn(|lane| f64::from(block_ys[index + lane])));
236 out[index..index + 2].copy_from_slice(
237 &self
238 .compute_y_simd(f64::from(block_x), ys, f64::from(block_z))
239 .to_array(),
240 );
241 index += 2;
242 }
243 for (&block_y, value) in block_ys[index..len].iter().zip(&mut out[index..len]) {
244 *value = self.compute(f64::from(block_x), f64::from(block_y), f64::from(block_z));
245 }
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::BlendedNoise;
252 use crate::random::{RandomSource, legacy_random::LegacyRandom};
253
254 #[test]
255 fn vanilla_lerp_endpoint_and_main_coordinate_rounding() {
256 let mut random = RandomSource::Legacy(LegacyRandom::from_seed(0));
257 let noise = BlendedNoise::new(&mut random, 0.25, 0.125, 80.0, 160.0, 8.0);
258
259 for (x, y, z, expected) in [
260 (0.0, -56.0, -10_000.0, 0.410_753_88_f32),
261 (20_000_068.0, 296.0, -19_999_796.0, 0.013_388_243_f32),
262 ] {
263 assert_eq!(noise.compute(x, y, z).to_bits(), expected.to_bits());
264 }
265 }
266
267 #[test]
268 fn compute_column_matches_scalar_lanes() {
269 let mut random = RandomSource::Legacy(LegacyRandom::from_seed(0));
270 let noise = BlendedNoise::new(&mut random, 0.25, 0.125, 80.0, 160.0, 8.0);
271 let ys = [
272 -64, -56, -48, -40, -32, -24, -16, -8, 0, 8, 16, 24, 32, 40, 48, 56, 64,
273 ];
274 let mut column = [0.0_f32; 17];
275 for len in 0..=ys.len() {
277 noise.compute_column(20_000_068, &ys, -19_999_796, &mut column[..len]);
278 for (&y, &value) in ys.iter().zip(&column[..len]) {
279 assert_eq!(
280 value.to_bits(),
281 noise
282 .compute(20_000_068.0, f64::from(y), -19_999_796.0)
283 .to_bits(),
284 "length={len}, Y={y}"
285 );
286 }
287 }
288 }
289
290 #[test]
291 fn simd_endpoint_shortcuts_preserve_each_lane() {
292 use std::array;
293 use std::simd::{Simd, num::SimdFloat};
294
295 let mut saw_minimum = false;
296 let mut saw_maximum = false;
297 let mut saw_mixed = false;
298 for seed in [0, 42, 13579] {
299 let mut random = RandomSource::Legacy(LegacyRandom::from_seed(seed));
300 let noise = BlendedNoise::new(&mut random, 0.25, 0.125, 80.0, 160.0, 8.0);
301 for x in [-20_000_068.0, -128.0, 0.0, 128.0, 20_000_068.0] {
302 for z in [-19_999_796.0, -256.0, 0.0, 256.0, 19_999_796.0] {
303 for base_y in (-64..256).step_by(64) {
304 let ys = Simd::from_array(array::from_fn::<_, 8, _>(|lane| {
305 f64::from(base_y) + lane as f64 * 8.0
306 }));
307 let main = noise.main_noise.sample_y_simd(
308 x * noise.main_xz_scale,
309 ys * Simd::splat(noise.main_y_scale),
310 z * noise.main_xz_scale,
311 );
312 let all_minimum = main.reduce_max() <= -0.5;
313 let all_maximum = main.reduce_min() >= 0.5;
314 saw_minimum |= all_minimum;
315 saw_maximum |= all_maximum;
316 saw_mixed |= !all_minimum && !all_maximum;
317 let actual = noise.compute_y_simd(x, ys, z);
318 for (lane, y) in ys.to_array().into_iter().enumerate() {
319 assert_eq!(
320 actual[lane].to_bits(),
321 noise.compute(x, y, z).to_bits(),
322 "seed {seed}, ({x}, {y}, {z})"
323 );
324 }
325 }
326 }
327 }
328 }
329 assert!(saw_minimum && saw_maximum && saw_mixed);
330 }
331}