1use std::simd::Simd;
7use std::simd::cmp::SimdPartialOrd;
8
9use crate::noise::PerlinNoise;
10use crate::random::RandomSource;
11use steel_math::{clamped_lerp, clamped_lerp_simd, wrap, wrap_simd};
12
13const COORDINATE_SCALE: f64 = 684.412;
15
16#[derive(Debug, Clone)]
20pub struct BlendedNoise {
21 min_limit_noise: PerlinNoise,
22 max_limit_noise: PerlinNoise,
23 main_noise: PerlinNoise,
24 xz_multiplier: f64,
25 y_multiplier: f64,
26 xz_factor: f64,
27 y_factor: f64,
28 smear_scale_multiplier: f64,
29 max_value: f64,
30}
31
32impl BlendedNoise {
33 #[must_use]
38 pub fn new(
39 random: &mut RandomSource,
40 xz_scale: f64,
41 y_scale: f64,
42 xz_factor: f64,
43 y_factor: f64,
44 smear_scale_multiplier: f64,
45 ) -> Self {
46 let min_limit_noise = PerlinNoise::create_legacy_for_nether(random, -15, &[1.0; 16]);
48 let max_limit_noise = PerlinNoise::create_legacy_for_nether(random, -15, &[1.0; 16]);
49 let main_noise = PerlinNoise::create_legacy_for_nether(random, -7, &[1.0; 8]);
50
51 let xz_multiplier = COORDINATE_SCALE * xz_scale;
52 let y_multiplier = COORDINATE_SCALE * y_scale;
53 let max_value = min_limit_noise.max_broken_value(y_multiplier);
54
55 Self {
56 min_limit_noise,
57 max_limit_noise,
58 main_noise,
59 xz_multiplier,
60 y_multiplier,
61 xz_factor,
62 y_factor,
63 smear_scale_multiplier,
64 max_value,
65 }
66 }
67
68 #[must_use]
70 pub fn compute(&self, block_x: f64, block_y: f64, block_z: f64) -> f64 {
71 let limit_x = block_x * self.xz_multiplier;
72 let limit_y = block_y * self.y_multiplier;
73 let limit_z = block_z * self.xz_multiplier;
74 let main_x = limit_x / self.xz_factor;
75 let main_y = limit_y / self.y_factor;
76 let main_z = limit_z / self.xz_factor;
77 let limit_smear = self.y_multiplier * self.smear_scale_multiplier;
78 let main_smear = limit_smear / self.y_factor;
79
80 let mut main_noise_value = 0.0;
82 let mut pow = 1.0;
83 for i in 0..8 {
84 if let Some(noise) = self.main_noise.get_octave_noise(i) {
85 main_noise_value += noise.noise_with_y_scale(
86 wrap(main_x * pow),
87 wrap(main_y * pow),
88 wrap(main_z * pow),
89 main_smear * pow,
90 main_y * pow,
91 ) / pow;
92 }
93 pow /= 2.0;
94 }
95
96 let factor = f64::midpoint(main_noise_value / 10.0, 1.0);
98 let is_max = factor >= 1.0;
99 let is_min = factor <= 0.0;
100
101 let mut blend_min = 0.0;
103 let mut blend_max = 0.0;
104 pow = 1.0;
105 for i in 0..16 {
106 let wx = wrap(limit_x * pow);
107 let wy = wrap(limit_y * pow);
108 let wz = wrap(limit_z * pow);
109 let y_scale_pow = limit_smear * pow;
110
111 if !is_max && let Some(noise) = self.min_limit_noise.get_octave_noise(i) {
112 blend_min += noise.noise_with_y_scale(wx, wy, wz, y_scale_pow, limit_y * pow) / pow;
113 }
114
115 if !is_min && let Some(noise) = self.max_limit_noise.get_octave_noise(i) {
116 blend_max += noise.noise_with_y_scale(wx, wy, wz, y_scale_pow, limit_y * pow) / pow;
117 }
118
119 pow /= 2.0;
120 }
121
122 clamped_lerp(blend_min / 512.0, blend_max / 512.0, factor) / 128.0
123 }
124
125 #[inline]
132 #[must_use]
133 pub fn compute_simd<const N: usize>(
134 &self,
135 block_x: f64,
136 block_ys: [f64; N],
137 block_z: f64,
138 ) -> [f64; N] {
139 let limit_x = block_x * self.xz_multiplier;
140 let limit_ys = Simd::from_array(block_ys) * Simd::splat(self.y_multiplier);
141 let limit_z = block_z * self.xz_multiplier;
142 let main_x = limit_x / self.xz_factor;
143 let main_ys = limit_ys / Simd::splat(self.y_factor);
144 let main_z = limit_z / self.xz_factor;
145 let limit_smear = self.y_multiplier * self.smear_scale_multiplier;
146 let main_smear = limit_smear / self.y_factor;
147
148 let mut main_noise_values = Simd::splat(0.0);
149 let mut pow = 1.0;
150 for i in 0..8 {
151 if let Some(noise) = self.main_noise.get_octave_noise(i) {
152 let pow_v = Simd::splat(pow);
153 let scaled_ys = main_ys * pow_v;
154 main_noise_values += noise.noise_with_y_scale_simd(
155 wrap(main_x * pow),
156 wrap_simd(scaled_ys),
157 wrap(main_z * pow),
158 main_smear * pow,
159 scaled_ys,
160 ) / pow_v;
161 }
162 pow /= 2.0;
163 }
164
165 let factors = (main_noise_values / Simd::splat(10.0) + Simd::splat(1.0)) / Simd::splat(2.0);
166
167 let all_max = factors.simd_ge(Simd::splat(1.0)).all();
168 let all_min = factors.simd_le(Simd::splat(0.0)).all();
169
170 let mut blend_min = Simd::splat(0.0);
171 let mut blend_max = Simd::splat(0.0);
172 pow = 1.0;
173 for i in 0..16 {
174 let pow_v = Simd::splat(pow);
175 let scaled_ys = limit_ys * pow_v;
176 let wx = wrap(limit_x * pow);
177 let wys = wrap_simd(scaled_ys);
178 let wz = wrap(limit_z * pow);
179 let y_scale_pow = limit_smear * pow;
180
181 if !all_max && let Some(noise) = self.min_limit_noise.get_octave_noise(i) {
182 blend_min +=
183 noise.noise_with_y_scale_simd(wx, wys, wz, y_scale_pow, scaled_ys) / pow_v;
184 }
185
186 if !all_min && let Some(noise) = self.max_limit_noise.get_octave_noise(i) {
187 blend_max +=
188 noise.noise_with_y_scale_simd(wx, wys, wz, y_scale_pow, scaled_ys) / pow_v;
189 }
190
191 pow /= 2.0;
192 }
193
194 let min_scaled = blend_min / Simd::splat(512.0);
195 let max_scaled = blend_max / Simd::splat(512.0);
196 let result = clamped_lerp_simd(min_scaled, max_scaled, factors) / Simd::splat(128.0);
197 result.to_array()
198 }
199
200 pub fn compute_column(&self, block_x: i32, block_ys: &[i32], block_z: i32, out: &mut [f64]) {
204 let count = block_ys.len().min(out.len());
205 let block_x = f64::from(block_x);
206 let block_z = f64::from(block_z);
207 let mut processed = 0;
208
209 let chunks_4 = count / 4;
211 for chunk in 0..chunks_4 {
212 let base = chunk * 4;
213 let batch_ys = [
214 f64::from(block_ys[base]),
215 f64::from(block_ys[base + 1]),
216 f64::from(block_ys[base + 2]),
217 f64::from(block_ys[base + 3]),
218 ];
219 out[base..base + 4].copy_from_slice(&self.compute_simd(block_x, batch_ys, block_z));
220 }
221 processed += chunks_4 * 4;
222
223 if count - processed >= 2 {
225 let batch_ys = [
226 f64::from(block_ys[processed]),
227 f64::from(block_ys[processed + 1]),
228 ];
229 out[processed..processed + 2]
230 .copy_from_slice(&self.compute_simd(block_x, batch_ys, block_z));
231 processed += 2;
232 }
233
234 for i in processed..count {
236 out[i] = self.compute(block_x, f64::from(block_ys[i]), block_z);
237 }
238 }
239
240 #[inline]
242 #[must_use]
243 pub const fn max_value(&self) -> f64 {
244 self.max_value
245 }
246
247 #[inline]
249 #[must_use]
250 pub fn min_value(&self) -> f64 {
251 -self.max_value
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::random::xoroshiro::Xoroshiro;
259
260 fn make_source(seed: u64) -> RandomSource {
261 RandomSource::Xoroshiro(Xoroshiro::from_seed(seed))
262 }
263
264 #[test]
265 fn test_compute_4x_matches_scalar() {
266 let bn = BlendedNoise::new(&mut make_source(42), 1.0, 1.0, 80.0, 160.0, 8.0);
267
268 let test_cases: &[(f64, [f64; 4], f64)] = &[
270 (0., [0., 8., 16., 24.], 0.),
271 (16., [32., 40., 48., 56.], 16.),
272 (-8., [-64., -32., 0., 32.], 8.),
273 (100., [60., 64., 68., 72.], -50.),
274 (0., [-4., -2., 0., 2.], 0.),
275 ];
276
277 for &(x, ys, z) in test_cases {
278 let simd = bn.compute_simd(x, ys, z);
279 for i in 0..4 {
280 let scalar = bn.compute(x, ys[i], z);
281 assert!(
282 (scalar - simd[i]).abs() < 1e-12,
283 "Mismatch at ({x}, {}, {z}): scalar={scalar}, simd={}, diff={}",
284 ys[i],
285 simd[i],
286 (scalar - simd[i]).abs(),
287 );
288 }
289 }
290 }
291
292 #[test]
293 fn test_compute_column_matches_scalar() {
294 let bn = BlendedNoise::new(&mut make_source(42), 1.0, 1.0, 80.0, 160.0, 8.0);
295
296 let block_ys: Vec<i32> = (0..49).map(|cy| (cy - 8) * 8).collect();
298
299 let scalar_results: Vec<f64> = block_ys
300 .iter()
301 .map(|&y| bn.compute(0., f64::from(y), 0.))
302 .collect();
303
304 let mut column_results = vec![0.0; block_ys.len()];
305 bn.compute_column(0, &block_ys, 0, &mut column_results);
306
307 for (i, &y) in block_ys.iter().enumerate() {
308 assert!(
309 (scalar_results[i] - column_results[i]).abs() < 1e-12,
310 "Column mismatch at y={y}: scalar={}, column={}, diff={}",
311 scalar_results[i],
312 column_results[i],
313 (scalar_results[i] - column_results[i]).abs(),
314 );
315 }
316 }
317
318 #[test]
319 fn test_blended_noise_deterministic() {
320 let bn1 = BlendedNoise::new(&mut make_source(12345), 1.0, 1.0, 80.0, 160.0, 8.0);
321 let bn2 = BlendedNoise::new(&mut make_source(12345), 1.0, 1.0, 80.0, 160.0, 8.0);
322
323 let v1 = bn1.compute(0., 64., 0.);
324 let v2 = bn2.compute(0., 64., 0.);
325 assert!(
326 (v1 - v2).abs() < 1e-15,
327 "BlendedNoise not deterministic: {v1} vs {v2}",
328 );
329 }
330
331 #[test]
332 fn test_blended_noise_spatial_variation() {
333 let bn = BlendedNoise::new(&mut make_source(42), 1.0, 1.0, 80.0, 160.0, 8.0);
334
335 let values: Vec<f64> = (-5..5)
336 .map(|x| bn.compute(f64::from(x * 16), 64., 0.))
337 .collect();
338
339 let min = values.iter().copied().fold(f64::INFINITY, f64::min);
340 let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
341 assert!(
342 max - min > 1e-6,
343 "BlendedNoise should have variation: {values:?}"
344 );
345 }
346
347 #[test]
348 fn test_blended_noise_range() {
349 let bn = BlendedNoise::new(&mut make_source(42), 1.0, 1.0, 80.0, 160.0, 8.0);
350
351 for x in -10..10 {
352 for y in (-4..20).step_by(4) {
353 let v = bn.compute(f64::from(x * 16), f64::from(y * 4), f64::from(x * 16));
354 assert!(
355 v.abs() <= bn.max_value() + 0.01,
356 "BlendedNoise value {v} exceeds max {} at ({}, {}, {})",
357 bn.max_value(),
358 x * 16,
359 y * 4,
360 x * 16,
361 );
362 }
363 }
364 }
365}