1use crate::random::Random;
6use std::ops;
7use std::simd::Simd;
8use std::simd::cmp::{SimdPartialEq, SimdPartialOrd};
9#[cfg(target_feature = "avx512f")]
10use std::simd::f64x4;
11use std::simd::num::{SimdFloat, SimdInt, SimdUint};
12use std::simd::ptr::SimdConstPtr;
13use std::simd::{Mask, Select, SimdCast, SimdElement, StdFloat};
14use steel_math::{
15 GRADIENT, fast_floor, fast_floor_simd, grad_dot, grad_dot_simd, lerp2, lerp3, lerp3_simd,
16 smoothstep, smoothstep_derivative, smoothstep_simd,
17};
18
19#[derive(Debug, Clone)]
25pub struct ImprovedNoise {
26 p: [u8; 256],
28 pub xo: f64,
30 pub yo: f64,
32 pub zo: f64,
34 yo_floor: i32,
35 yo_fraction: f64,
36 zo_floor: i32,
37 zo_fraction: f64,
38}
39
40impl ImprovedNoise {
41 pub fn new<R: Random>(random: &mut R) -> Self {
46 let xo = random.next_f64() * 256.0;
47 let yo = random.next_f64() * 256.0;
48 let zo = random.next_f64() * 256.0;
49
50 let mut p = [0u8; 256];
51 #[expect(
52 clippy::needless_range_loop,
53 reason = "index is used as the initial permutation value"
54 )]
55 for i in 0..256 {
56 p[i] = i as u8;
57 }
58
59 for i in 0..256 {
61 let offset = random.next_i32_bounded((256 - i) as i32) as usize;
62 p.swap(i, i + offset);
63 }
64
65 let yo_floor = fast_floor(yo);
66 let yo_fraction = yo - f64::from(yo_floor);
67 let zo_floor = fast_floor(zo);
68 let zo_fraction = zo - f64::from(zo_floor);
69
70 Self {
71 p,
72 xo,
73 yo,
74 zo,
75 yo_floor,
76 yo_fraction,
77 zo_floor,
78 zo_fraction,
79 }
80 }
81
82 #[inline]
86 #[must_use]
87 pub fn noise(&self, x: f64, y: f64, z: f64) -> f64 {
88 let x = x + self.xo;
89 let y = y + self.yo;
90 let z = z + self.zo;
91
92 let xf = fast_floor(x);
93 let yf = fast_floor(y);
94 let zf = fast_floor(z);
95
96 let xr = x - f64::from(xf);
97 let yr = y - f64::from(yf);
98 let zr = z - f64::from(zf);
99
100 self.sample_and_lerp(xf, yf, zf, xr, yr, zr, yr)
101 }
102
103 #[inline]
105 #[must_use]
106 pub fn noise_simd<F, const N: usize>(
107 &self,
108 x: Simd<F, N>,
109 y: Simd<F, N>,
110 z: Simd<F, N>,
111 ) -> Simd<F, N>
112 where
113 F: SimdElement + SimdCast,
114 Simd<F, N>: SimdFloat<Cast<i32> = Simd<i32, N>>
115 + SimdPartialOrd
116 + SimdPartialEq<Mask = Mask<<F as SimdElement>::Mask, N>>
117 + ops::Add<Output = Simd<F, N>>
118 + ops::Sub<Output = Simd<F, N>>
119 + ops::Mul<Output = Simd<F, N>>
120 + ops::Neg<Output = Simd<F, N>>,
121 {
122 let x = x + Simd::splat(self.xo).cast();
123 let y = y + Simd::splat(self.yo).cast();
124 let z = z + Simd::splat(self.zo).cast();
125
126 let xf = fast_floor_simd::<F, i32, N>(x);
127 let yf = fast_floor_simd::<F, i32, N>(y);
128 let zf = fast_floor_simd::<F, i32, N>(z);
129
130 let xr = x - xf.cast();
131 let yr = y - yf.cast();
132 let zr = z - zf.cast();
133
134 self.sample_and_lerp_simd(xf, yf, zf, xr, yr, zr, yr)
135 }
136
137 #[inline]
139 #[must_use]
140 pub fn noise_xz(&self, x: f64, z: f64) -> f64 {
141 let x = x + self.xo;
142 let z = z + self.zo;
143
144 let xf = fast_floor(x);
145 let zf = fast_floor(z);
146
147 let xr = x - f64::from(xf);
148 let zr = z - f64::from(zf);
149
150 self.sample_and_lerp(
151 xf,
152 self.yo_floor,
153 zf,
154 xr,
155 self.yo_fraction,
156 zr,
157 self.yo_fraction,
158 )
159 }
160
161 #[inline]
163 #[must_use]
164 pub fn noise_xy(&self, x: f64, y: f64) -> f64 {
165 let x = x + self.xo;
166 let y = y + self.yo;
167
168 let xf = fast_floor(x);
169 let yf = fast_floor(y);
170
171 let xr = x - f64::from(xf);
172 let yr = y - f64::from(yf);
173
174 self.sample_and_lerp(xf, yf, self.zo_floor, xr, yr, self.zo_fraction, yr)
175 }
176
177 #[must_use]
182 pub fn noise_with_derivative(
183 &self,
184 x: f64,
185 y: f64,
186 z: f64,
187 derivative_out: &mut [f64; 3],
188 ) -> f64 {
189 let x = x + self.xo;
190 let y = y + self.yo;
191 let z = z + self.zo;
192
193 let xf = fast_floor(x);
194 let yf = fast_floor(y);
195 let zf = fast_floor(z);
196
197 let xr = x - f64::from(xf);
198 let yr = y - f64::from(yf);
199 let zr = z - f64::from(zf);
200
201 self.sample_with_derivative(xf, yf, zf, xr, yr, zr, derivative_out)
202 }
203
204 #[must_use]
214 #[expect(
215 clippy::similar_names,
216 reason = "yr_fudge and y_fudge match vanilla naming"
217 )]
218 pub fn noise_with_y_scale(&self, x: f64, y: f64, z: f64, y_scale: f64, y_fudge: f64) -> f64 {
219 let x = x + self.xo;
220 let y = y + self.yo;
221 let z = z + self.zo;
222
223 let xf = fast_floor(x);
224 let yf = fast_floor(y);
225 let zf = fast_floor(z);
226
227 let xr = x - f64::from(xf);
228 let yr = y - f64::from(yf);
229 let zr = z - f64::from(zf);
230
231 #[expect(
233 clippy::if_not_else,
234 reason = "matches vanilla's conditional structure"
235 )]
236 let yr_fudge = if y_scale != 0.0 {
237 let fudge_limit = if y_fudge >= 0.0 && y_fudge < yr {
238 y_fudge
239 } else {
240 yr
241 };
242 (fudge_limit / y_scale + f64::from(1.0e-7_f32)).floor() * y_scale
244 } else {
245 0.0
246 };
247 self.sample_and_lerp(xf, yf, zf, xr, yr - yr_fudge, zr, yr)
248 }
249
250 #[expect(clippy::too_many_arguments, reason = "matches vanilla signature")]
257 fn sample_and_lerp(
258 &self,
259 x: i32,
260 y: i32,
261 z: i32,
262 xr: f64,
263 yr: f64,
264 zr: f64,
265 yr_original: f64,
266 ) -> f64 {
267 let (d000, d100, d010, d110, d001, d101, d011, d111) = {
268 #[cfg(target_feature = "avx512f")]
269 {
270 let x = x as u8;
271 let y = y as u8;
272 let z = z as u8;
273 let x0 = self.p[x as usize];
274 let x1 = self.p[x.wrapping_add(1) as usize];
275
276 let xy00 = self.p[x0.wrapping_add(y) as usize];
277 let xy01 = self.p[x0.wrapping_add(y).wrapping_add(1) as usize];
278 let xy10 = self.p[x1.wrapping_add(y) as usize];
279 let xy11 = self.p[x1.wrapping_add(y).wrapping_add(1) as usize];
280
281 let h_z0 = [
282 self.p[xy00.wrapping_add(z) as usize] as usize,
283 self.p[xy10.wrapping_add(z) as usize] as usize,
284 self.p[xy01.wrapping_add(z) as usize] as usize,
285 self.p[xy11.wrapping_add(z) as usize] as usize,
286 ];
287 let h_z1 = [
288 self.p[xy00.wrapping_add(z).wrapping_add(1) as usize] as usize,
289 self.p[xy10.wrapping_add(z).wrapping_add(1) as usize] as usize,
290 self.p[xy01.wrapping_add(z).wrapping_add(1) as usize] as usize,
291 self.p[xy11.wrapping_add(z).wrapping_add(1) as usize] as usize,
292 ];
293
294 let xr_v = f64x4::from_array([xr, xr - 1.0, xr, xr - 1.0]);
295 let yr_v = f64x4::from_array([yr, yr, yr - 1.0, yr - 1.0]);
296 let zr_v0 = f64x4::splat(zr);
297 let zr_v1 = f64x4::splat(zr - 1.0);
298
299 let [d000, d100, d010, d110] = grad_dot_simd(h_z0, xr_v, yr_v, zr_v0).to_array();
300 let [d001, d101, d011, d111] = grad_dot_simd(h_z1, xr_v, yr_v, zr_v1).to_array();
301 (d000, d100, d010, d110, d001, d101, d011, d111)
302 }
303
304 #[cfg(not(target_feature = "avx512f"))]
305 {
306 let px1 = x.wrapping_add(1);
307 let py1 = y.wrapping_add(1);
308 let pz1 = z.wrapping_add(1);
309 let xr1 = xr - 1.0;
310 let yr1 = yr - 1.0;
311 let zr1 = zr - 1.0;
312
313 let d000 = grad_dot_flat(&self.p, x, y, z, xr, yr, zr);
314 let d100 = grad_dot_flat(&self.p, px1, y, z, xr1, yr, zr);
315 let d010 = grad_dot_flat(&self.p, x, py1, z, xr, yr1, zr);
316 let d110 = grad_dot_flat(&self.p, px1, py1, z, xr1, yr1, zr);
317 let d001 = grad_dot_flat(&self.p, x, y, pz1, xr, yr, zr1);
318 let d101 = grad_dot_flat(&self.p, px1, y, pz1, xr1, yr, zr1);
319 let d011 = grad_dot_flat(&self.p, x, py1, pz1, xr, yr1, zr1);
320 let d111 = grad_dot_flat(&self.p, px1, py1, pz1, xr1, yr1, zr1);
321 (d000, d100, d010, d110, d001, d101, d011, d111)
322 }
323 };
324
325 let alpha_x = smoothstep(xr);
326 let alpha_y = smoothstep(yr_original);
327 let alpha_z = smoothstep(zr);
328 lerp3(
329 alpha_x, alpha_y, alpha_z, d000, d100, d010, d110, d001, d101, d011, d111,
330 )
331 }
332
333 #[inline]
334 fn p_simd<const N: usize>(&self, idx: Simd<u8, N>) -> Simd<u8, N> {
335 let offset = idx.cast::<usize>();
336 let p = Simd::splat(self.p.as_ptr()).wrapping_add(offset);
337 unsafe { Simd::gather_ptr(p) }
340 }
341
342 #[expect(clippy::too_many_arguments, reason = "matches vanilla signature")]
344 fn sample_and_lerp_simd<F, const N: usize>(
345 &self,
346 x: Simd<i32, N>,
347 y: Simd<i32, N>,
348 z: Simd<i32, N>,
349 xr: Simd<F, N>,
350 yr: Simd<F, N>,
351 zr: Simd<F, N>,
352 yr_original: Simd<F, N>,
353 ) -> Simd<F, N>
354 where
355 F: SimdElement + SimdCast,
356 Simd<F, N>: ops::Mul<Output = Simd<F, N>>
357 + ops::Add<Output = Simd<F, N>>
358 + ops::Sub<Output = Simd<F, N>>
359 + ops::Neg<Output = Simd<F, N>>,
360 {
361 let x = x.cast::<u8>();
362 let y = y.cast::<u8>();
363 let z = z.cast::<u8>();
364 let x0 = self.p_simd(x);
366 let x1 = self.p_simd(x + Simd::splat(1));
367
368 let xy00 = self.p_simd(x0 + y);
369 let xy01 = self.p_simd(x0 + y + Simd::splat(1));
370 let xy10 = self.p_simd(x1 + y);
371 let xy11 = self.p_simd(x1 + y + Simd::splat(1));
372
373 let h000 = self.p_simd(xy00 + z).cast::<usize>().to_array();
374 let h100 = self.p_simd(xy10 + z).cast::<usize>().to_array();
375 let h010 = self.p_simd(xy01 + z).cast::<usize>().to_array();
376 let h110 = self.p_simd(xy11 + z).cast::<usize>().to_array();
377 let h001 = self
378 .p_simd(xy00 + z + Simd::splat(1))
379 .cast::<usize>()
380 .to_array();
381 let h101 = self
382 .p_simd(xy10 + z + Simd::splat(1))
383 .cast::<usize>()
384 .to_array();
385 let h011 = self
386 .p_simd(xy01 + z + Simd::splat(1))
387 .cast::<usize>()
388 .to_array();
389 let h111 = self
390 .p_simd(xy11 + z + Simd::splat(1))
391 .cast::<usize>()
392 .to_array();
393
394 let d000 = grad_dot_simd(h000, xr, yr, zr);
396 let d100 = grad_dot_simd(h100, xr - Simd::splat(1.0).cast::<F>(), yr, zr);
397 let d010 = grad_dot_simd(h010, xr, yr - Simd::splat(1.0).cast::<F>(), zr);
398 let d110 = grad_dot_simd(
399 h110,
400 xr - Simd::splat(1.0).cast::<F>(),
401 yr - Simd::splat(1.0).cast::<F>(),
402 zr,
403 );
404 let d001 = grad_dot_simd(h001, xr, yr, zr - Simd::splat(1.0).cast::<F>());
405 let d101 = grad_dot_simd(
406 h101,
407 xr - Simd::splat(1.0).cast::<F>(),
408 yr,
409 zr - Simd::splat(1.0).cast::<F>(),
410 );
411 let d011 = grad_dot_simd(
412 h011,
413 xr,
414 yr - Simd::splat(1.0).cast::<F>(),
415 zr - Simd::splat(1.0).cast::<F>(),
416 );
417 let d111 = grad_dot_simd(
418 h111,
419 xr - Simd::splat(1.0).cast::<F>(),
420 yr - Simd::splat(1.0).cast::<F>(),
421 zr - Simd::splat(1.0).cast::<F>(),
422 );
423
424 let x_alpha = smoothstep_simd(xr);
426 let y_alpha = smoothstep_simd(yr_original);
427 let z_alpha = smoothstep_simd(zr);
428
429 lerp3_simd(
430 x_alpha, y_alpha, z_alpha, d000, d100, d010, d110, d001, d101, d011, d111,
431 )
432 }
433
434 #[inline]
439 #[must_use]
440 pub fn noise_with_y_scale_simd<const N: usize>(
441 &self,
442 x: f64,
443 ys: Simd<f64, N>,
444 z: f64,
445 y_scale: f64,
446 y_fudges: Simd<f64, N>,
447 ) -> Simd<f64, N> {
448 let x = x + self.xo;
450 let z = z + self.zo;
451 let xf = fast_floor(x);
452 let zf = fast_floor(z);
453 let xr = x - f64::from(xf);
454 let zr = z - f64::from(zf);
455
456 let ys = ys + Simd::splat(self.yo);
458 let ys_floor = fast_floor_simd::<f64, i32, N>(ys);
459 let yrs = ys - ys_floor.cast();
460
461 let yr_fudge: Simd<f64, N> = if y_scale == 0.0 {
463 Simd::splat(0.0)
464 } else {
465 let y_scale_v: Simd<f64, N> = Simd::splat(y_scale);
466 let zero: Simd<f64, N> = Simd::splat(0.0);
467 let mask = y_fudges.simd_ge(zero) & y_fudges.simd_lt(yrs);
468 let fudge_limits = mask.select(y_fudges, yrs);
469 let epsilon: Simd<f64, N> = Simd::splat(f64::from(1.0e-7_f32));
470 ((fudge_limits / y_scale_v) + epsilon).floor() * y_scale_v
471 };
472
473 let yrs_adjusted = yrs - yr_fudge;
474
475 self.sample_and_lerp_y_simd(xf, zf, xr, zr, ys_floor, yrs_adjusted, yrs)
476 }
477
478 #[expect(
481 clippy::too_many_arguments,
482 reason = "mirrors scalar sample_and_lerp with Nx SIMD y-batching"
483 )]
484 #[inline]
485 fn sample_and_lerp_y_simd<const N: usize>(
486 &self,
487 xf: i32,
488 zf: i32,
489 xr: f64,
490 zr: f64,
491 ys_floor: Simd<i32, N>,
492 yrs: Simd<f64, N>,
493 yrs_original: Simd<f64, N>,
494 ) -> Simd<f64, N> {
495 let xf = xf as u8;
496 let zf = zf as u8;
497 let x0 = self.p[xf as usize];
499 let x1 = self.p[xf.wrapping_add(1) as usize];
500
501 let yf = ys_floor.cast();
502
503 let mut h000 = [0usize; N];
505 let mut h100 = [0usize; N];
506 let mut h010 = [0usize; N];
507 let mut h110 = [0usize; N];
508 let mut h001 = [0usize; N];
509 let mut h101 = [0usize; N];
510 let mut h011 = [0usize; N];
511 let mut h111 = [0usize; N];
512
513 for i in 0..N {
514 let y = yf[i];
515 let xy00 = self.p[x0.wrapping_add(y) as usize];
516 let xy01 = self.p[x0.wrapping_add(y).wrapping_add(1) as usize];
517 let xy10 = self.p[x1.wrapping_add(y) as usize];
518 let xy11 = self.p[x1.wrapping_add(y).wrapping_add(1) as usize];
519 h000[i] = self.p[xy00.wrapping_add(zf) as usize] as usize;
520 h100[i] = self.p[xy10.wrapping_add(zf) as usize] as usize;
521 h010[i] = self.p[xy01.wrapping_add(zf) as usize] as usize;
522 h110[i] = self.p[xy11.wrapping_add(zf) as usize] as usize;
523 h001[i] = self.p[xy00.wrapping_add(zf).wrapping_add(1) as usize] as usize;
524 h101[i] = self.p[xy10.wrapping_add(zf).wrapping_add(1) as usize] as usize;
525 h011[i] = self.p[xy01.wrapping_add(zf).wrapping_add(1) as usize] as usize;
526 h111[i] = self.p[xy11.wrapping_add(zf).wrapping_add(1) as usize] as usize;
527 }
528
529 let xr_v: Simd<f64, N> = Simd::splat(xr);
531 let zr_v: Simd<f64, N> = Simd::splat(zr);
532 let one: Simd<f64, N> = Simd::splat(1.0);
533 let xr_m1 = xr_v - one;
534 let yr_m1 = yrs - one;
535 let zr_m1 = zr_v - one;
536
537 let d000 = grad_dot_simd(h000, xr_v, yrs, zr_v);
538 let d100 = grad_dot_simd(h100, xr_m1, yrs, zr_v);
539 let d010 = grad_dot_simd(h010, xr_v, yr_m1, zr_v);
540 let d110 = grad_dot_simd(h110, xr_m1, yr_m1, zr_v);
541 let d001 = grad_dot_simd(h001, xr_v, yrs, zr_m1);
542 let d101 = grad_dot_simd(h101, xr_m1, yrs, zr_m1);
543 let d011 = grad_dot_simd(h011, xr_v, yr_m1, zr_m1);
544 let d111 = grad_dot_simd(h111, xr_m1, yr_m1, zr_m1);
545
546 let x_alpha: Simd<f64, N> = Simd::splat(smoothstep(xr));
548 let y_alpha = smoothstep_simd(yrs_original);
549 let z_alpha: Simd<f64, N> = Simd::splat(smoothstep(zr));
550
551 lerp3_simd(
552 x_alpha, y_alpha, z_alpha, d000, d100, d010, d110, d001, d101, d011, d111,
553 )
554 }
555
556 #[expect(clippy::too_many_arguments, reason = "matches vanilla signature")]
558 fn sample_with_derivative(
559 &self,
560 x: i32,
561 y: i32,
562 z: i32,
563 xr: f64,
564 yr: f64,
565 zr: f64,
566 derivative_out: &mut [f64; 3],
567 ) -> f64 {
568 let x = x as u8;
569 let y = y as u8;
570 let z = z as u8;
571
572 let x0 = self.p[x as usize];
573 let x1 = self.p[x.wrapping_add(1) as usize];
574 let xy00 = self.p[x0.wrapping_add(y) as usize];
575 let xy01 = self.p[x0.wrapping_add(y).wrapping_add(1) as usize];
576 let xy10 = self.p[x1.wrapping_add(y) as usize];
577 let xy11 = self.p[x1.wrapping_add(y).wrapping_add(1) as usize];
578
579 let h000 = self.p[xy00.wrapping_add(z) as usize] as usize;
580 let h100 = self.p[xy10.wrapping_add(z) as usize] as usize;
581 let h010 = self.p[xy01.wrapping_add(z) as usize] as usize;
582 let h110 = self.p[xy11.wrapping_add(z) as usize] as usize;
583 let h001 = self.p[xy00.wrapping_add(z).wrapping_add(1) as usize] as usize;
584 let h101 = self.p[xy10.wrapping_add(z).wrapping_add(1) as usize] as usize;
585 let h011 = self.p[xy01.wrapping_add(z).wrapping_add(1) as usize] as usize;
586 let h111 = self.p[xy11.wrapping_add(z).wrapping_add(1) as usize] as usize;
587
588 let g000 = Simd::from_array(GRADIENT[h000 & 15]);
589 let g100 = Simd::from_array(GRADIENT[h100 & 15]);
590 let g010 = Simd::from_array(GRADIENT[h010 & 15]);
591 let g110 = Simd::from_array(GRADIENT[h110 & 15]);
592 let g001 = Simd::from_array(GRADIENT[h001 & 15]);
593 let g101 = Simd::from_array(GRADIENT[h101 & 15]);
594 let g011 = Simd::from_array(GRADIENT[h011 & 15]);
595 let g111 = Simd::from_array(GRADIENT[h111 & 15]);
596
597 let d000 = grad_dot(h000, xr, yr, zr);
599 let d100 = grad_dot(h100, xr - 1.0, yr, zr);
600 let d010 = grad_dot(h010, xr, yr - 1.0, zr);
601 let d110 = grad_dot(h110, xr - 1.0, yr - 1.0, zr);
602 let d001 = grad_dot(h001, xr, yr, zr - 1.0);
603 let d101 = grad_dot(h101, xr - 1.0, yr, zr - 1.0);
604 let d011 = grad_dot(h011, xr, yr - 1.0, zr - 1.0);
605 let d111 = grad_dot(h111, xr - 1.0, yr - 1.0, zr - 1.0);
606
607 let alpha_x = smoothstep(xr);
608 let alpha_y = smoothstep(yr);
609 let alpha_z = smoothstep(zr);
610
611 let d1_v = lerp3_simd(
613 Simd::splat(alpha_x),
614 Simd::splat(alpha_y),
615 Simd::splat(alpha_z),
616 g000,
617 g100,
618 g010,
619 g110,
620 g001,
621 g101,
622 g011,
623 g111,
624 );
625
626 let d2x = lerp2(
628 alpha_y,
629 alpha_z,
630 d100 - d000,
631 d110 - d010,
632 d101 - d001,
633 d111 - d011,
634 );
635 let d2y = lerp2(
636 alpha_z,
637 alpha_x,
638 d010 - d000,
639 d011 - d001,
640 d110 - d100,
641 d111 - d101,
642 );
643 let d2z = lerp2(
644 alpha_x,
645 alpha_y,
646 d001 - d000,
647 d101 - d100,
648 d011 - d010,
649 d111 - d110,
650 );
651
652 let x_sd = smoothstep_derivative(xr);
653 let y_sd = smoothstep_derivative(yr);
654 let z_sd = smoothstep_derivative(zr);
655
656 derivative_out[0] += d1_v[0] + x_sd * d2x;
658 derivative_out[1] += d1_v[1] + y_sd * d2y;
659 derivative_out[2] += d1_v[2] + z_sd * d2z;
660 lerp3(
661 alpha_x, alpha_y, alpha_z, d000, d100, d010, d110, d001, d101, d011, d111,
662 )
663 }
664}
665
666#[cfg(not(target_feature = "avx512f"))]
668#[inline]
669fn grad_dot_flat(p: &[u8; 256], px: i32, py: i32, pz: i32, fx: f64, fy: f64, fz: f64) -> f64 {
670 let qx = (px & 0xFF) as u8;
671 let qy = (py & 0xFF) as u8;
672 let qz = (pz & 0xFF) as u8;
673 let a = p[qx as usize];
674 let b = p[a.wrapping_add(qy) as usize];
675 let hash = p[b.wrapping_add(qz) as usize];
676 grad_dot(hash as usize, fx, fy, fz)
677}
678
679#[cfg(test)]
680mod tests {
681 use super::*;
682 use crate::random::xoroshiro::Xoroshiro;
683 use std::simd::f64x4;
684
685 #[test]
686 fn test_noise_with_y_scale_4x_matches_scalar() {
687 let mut rng = Xoroshiro::from_seed(42);
688 let noise = ImprovedNoise::new(&mut rng);
689
690 let test_x_zs: &[(f64, f64)] = &[
692 (0.0, 0.0),
693 (1.5, 3.7),
694 (-5.2, 100.3),
695 (0.001, -0.001),
696 (1000.0, -500.0),
697 ];
698 let test_ys: &[[f64; 4]] = &[
699 [0.0, 1.0, 2.0, 3.0],
700 [64.0, 64.5, 65.0, 65.5],
701 [-5.0, -2.5, 0.0, 2.5],
702 [0.25, 0.5, 0.75, 1.0],
703 [-100.0, -50.0, 50.0, 100.0],
704 ];
705 let y_scales = [0.0, 1.0, 8.0];
706
707 for &(x, z) in test_x_zs {
708 for ys in test_ys {
709 for &y_scale in &y_scales {
710 let y_fudges: [f64; 4] = if y_scale == 0.0 {
711 [0.0; 4]
712 } else {
713 *ys };
715
716 let simd_result = noise.noise_with_y_scale_simd(
717 x,
718 f64x4::from_array(*ys),
719 z,
720 y_scale,
721 f64x4::from_array(y_fudges),
722 );
723
724 for i in 0..4 {
725 let scalar = noise.noise_with_y_scale(x, ys[i], z, y_scale, y_fudges[i]);
726 let simd_val = simd_result[i];
727 assert!(
728 (scalar - simd_val).abs() < 1e-14,
729 "Mismatch at x={x}, y={}, z={z}, y_scale={y_scale}: \
730 scalar={scalar}, simd={simd_val}, diff={}",
731 ys[i],
732 (scalar - simd_val).abs(),
733 );
734 }
735 }
736 }
737 }
738 }
739
740 #[test]
741 fn test_noise_simd_matches_scalar() {
742 let mut rng = Xoroshiro::from_seed(42);
743 let noise = ImprovedNoise::new(&mut rng);
744
745 let batches = [
746 (
747 [0.0, 1.25, -5.5, 1000.75],
748 [0.0, 64.5, -20.25, 255.75],
749 [0.0, -30.75, 4096.5, -1000.25],
750 ),
751 (
752 [
753 255.25 - noise.xo,
754 256.25 - noise.xo,
755 -1.75 - noise.xo,
756 -256.25 - noise.xo,
757 ],
758 [
759 255.5 - noise.yo,
760 256.5 - noise.yo,
761 -1.5 - noise.yo,
762 -256.5 - noise.yo,
763 ],
764 [
765 255.75 - noise.zo,
766 256.75 - noise.zo,
767 -1.25 - noise.zo,
768 -256.25 - noise.zo,
769 ],
770 ),
771 ];
772
773 for (xs, ys, zs) in batches {
774 let simd = noise.noise_simd(
775 f64x4::from_array(xs),
776 f64x4::from_array(ys),
777 f64x4::from_array(zs),
778 );
779 for i in 0..4 {
780 let scalar = noise.noise(xs[i], ys[i], zs[i]);
781 #[expect(
782 clippy::float_cmp,
783 reason = "SIMD path must be bit-identical to scalar noise for vanilla determinism"
784 )]
785 let matches = scalar == simd[i];
786 assert!(
787 matches,
788 "Mismatch at ({}, {}, {}): scalar={}, simd={}",
789 xs[i], ys[i], zs[i], scalar, simd[i],
790 );
791 }
792 }
793 }
794
795 #[test]
796 fn test_noise_with_y_scale_simd8_matches_scalar() {
797 use std::simd::f64x8;
798
799 let mut rng = Xoroshiro::from_seed(42);
800 let noise = ImprovedNoise::new(&mut rng);
801
802 let test_x_zs: &[(f64, f64)] = &[
803 (0.0, 0.0),
804 (1.5, 3.7),
805 (-5.2, 100.3),
806 (0.001, -0.001),
807 (1000.0, -500.0),
808 ];
809 let test_ys: &[[f64; 8]] = &[
810 [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
811 [64.0, 64.5, 65.0, 65.5, 66.0, 66.5, 67.0, 67.5],
812 [-5.0, -2.5, 0.0, 2.5, 5.0, 7.5, 10.0, 12.5],
813 [0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
814 [-100.0, -50.0, -25.0, -10.0, 10.0, 25.0, 50.0, 100.0],
815 ];
816 let y_scales = [0.0, 1.0, 8.0];
817
818 for &(x, z) in test_x_zs {
819 for ys in test_ys {
820 for &y_scale in &y_scales {
821 let y_fudges: [f64; 8] = if y_scale == 0.0 { [0.0; 8] } else { *ys };
822
823 let simd_result = noise.noise_with_y_scale_simd(
824 x,
825 f64x8::from_array(*ys),
826 z,
827 y_scale,
828 f64x8::from_array(y_fudges),
829 );
830
831 for i in 0..8 {
832 let scalar = noise.noise_with_y_scale(x, ys[i], z, y_scale, y_fudges[i]);
833 let simd_val = simd_result[i];
834 assert!(
835 (scalar - simd_val).abs() < 1e-14,
836 "Mismatch at x={x}, y={}, z={z}, y_scale={y_scale}: \
837 scalar={scalar}, simd={simd_val}, diff={}",
838 ys[i],
839 (scalar - simd_val).abs(),
840 );
841 }
842 }
843 }
844 }
845 }
846
847 #[test]
848 fn test_improved_noise_deterministic() {
849 let mut rng1 = Xoroshiro::from_seed(12345);
850 let mut rng2 = Xoroshiro::from_seed(12345);
851
852 let noise1 = ImprovedNoise::new(&mut rng1);
853 let noise2 = ImprovedNoise::new(&mut rng2);
854
855 #[expect(
857 clippy::float_cmp,
858 reason = "determinism test: identical seeds must produce bit-identical offsets"
859 )]
860 {
861 assert_eq!(noise1.xo, noise2.xo);
862 assert_eq!(noise1.yo, noise2.yo);
863 assert_eq!(noise1.zo, noise2.zo);
864 }
865 assert_eq!(noise1.p, noise2.p);
866
867 let v1 = noise1.noise(100.0, 64.0, 100.0);
869 let v2 = noise2.noise(100.0, 64.0, 100.0);
870 assert!((v1 - v2).abs() < 1e-15);
871 }
872
873 #[test]
874 fn scalar_noise_wraps_corner_coordinates_at_i32_max() {
875 let mut rng = Xoroshiro::from_seed(42);
876 let mut noise = ImprovedNoise::new(&mut rng);
877 noise.xo = 0.0;
878 noise.yo = 0.0;
879 noise.zo = 0.0;
880
881 let _ = noise.noise(
882 f64::from(i32::MAX),
883 f64::from(i32::MAX),
884 f64::from(i32::MAX),
885 );
886 }
887
888 #[test]
889 fn test_noise_matches_zero_y_scale_path() {
890 let mut rng = Xoroshiro::from_seed(42);
891 let noise = ImprovedNoise::new(&mut rng);
892
893 for (x, y, z) in [
894 (0.0, 0.0, 0.0),
895 (1.25, 64.5, -30.75),
896 (-1000.0, -20.25, 4096.5),
897 ] {
898 assert!(
899 (noise.noise(x, y, z) - noise.noise_with_y_scale(x, y, z, 0.0, 0.0)).abs() < 1e-15
900 );
901 }
902 }
903
904 #[test]
905 fn test_zero_axis_helpers_match_full_noise() {
906 let mut rng = Xoroshiro::from_seed(12_345);
907 let noise = ImprovedNoise::new(&mut rng);
908 let samples = [
909 (0.0, 0.0),
910 (1.25, -30.75),
911 (-1000.0, 4096.5),
912 (33_554_431.5, -33_554_432.25),
913 (-0.000_000_1, 0.000_000_1),
914 ];
915
916 for &(a, b) in &samples {
917 #[expect(
918 clippy::float_cmp,
919 reason = "zero-axis helpers must be bit-identical to the full scalar path"
920 )]
921 {
922 assert_eq!(noise.noise_xz(a, b), noise.noise(a, 0.0, b));
923 assert_eq!(noise.noise_xy(a, b), noise.noise(a, b, 0.0));
924 }
925 }
926 }
927
928 #[test]
929 fn test_improved_noise_range() {
930 let mut rng = Xoroshiro::from_seed(42);
931 let noise = ImprovedNoise::new(&mut rng);
932
933 for x in -10..10 {
935 for z in -10..10 {
936 let v = noise.noise(f64::from(x) * 10.0, 64.0, f64::from(z) * 10.0);
937 assert!(
939 (-1.5..=1.5).contains(&v),
940 "Noise value {v} at ({x}, {z}) out of expected range",
941 );
942 }
943 }
944 }
945
946 #[test]
947 fn test_improved_noise_spatial_variation() {
948 let mut rng = Xoroshiro::from_seed(42);
949 let noise = ImprovedNoise::new(&mut rng);
950
951 let v1 = noise.noise(0.0, 0.0, 0.0);
953 let v2 = noise.noise(100.0, 0.0, 0.0);
954 let v3 = noise.noise(0.0, 100.0, 0.0);
955 let v4 = noise.noise(0.0, 0.0, 100.0);
956
957 #[expect(
959 clippy::float_cmp,
960 reason = "intentional exact equality check to detect degenerate constant noise"
961 )]
962 let all_same = v1 == v2 && v2 == v3 && v3 == v4;
963 assert!(!all_same, "All noise values are the same - unexpected");
964 }
965
966 #[test]
967 fn test_noise_with_derivative_matches_noise() {
968 let mut rng = Xoroshiro::from_seed(42);
969 let noise = ImprovedNoise::new(&mut rng);
970
971 for &(x, y, z) in &[
974 (0.0, 0.0, 0.0),
975 (1.5, 2.3, 3.7),
976 (-5.2, 64.0, 100.3),
977 (0.25, 0.25, 0.25),
978 ] {
979 let v1 = noise.noise(x, y, z);
980 let mut deriv = [0.0; 3];
981 let v2 = noise.noise_with_derivative(x, y, z, &mut deriv);
982 assert!(
983 (v1 - v2).abs() < 1e-12,
984 "Value mismatch at ({x}, {y}, {z}): {v1} vs {v2}",
985 );
986 }
987 }
988
989 #[test]
990 fn test_noise_with_derivative_produces_derivatives() {
991 let mut rng = Xoroshiro::from_seed(42);
992 let noise = ImprovedNoise::new(&mut rng);
993
994 let mut deriv = [0.0; 3];
995 let _ = noise.noise_with_derivative(1.5, 2.3, 3.7, &mut deriv);
996
997 let any_nonzero = deriv.iter().any(|&d| d.abs() > 1e-15);
999 assert!(any_nonzero, "All derivatives are zero: {deriv:?}");
1000 }
1001
1002 #[test]
1003 fn test_noise_with_derivative_accumulates() {
1004 let mut rng = Xoroshiro::from_seed(42);
1005 let noise = ImprovedNoise::new(&mut rng);
1006
1007 let mut deriv = [0.0; 3];
1009 let _ = noise.noise_with_derivative(1.5, 2.3, 3.7, &mut deriv);
1010 let first = deriv;
1011
1012 let _ = noise.noise_with_derivative(4.1, 5.2, 6.3, &mut deriv);
1014 let mut deriv2 = [0.0; 3];
1015 let _ = noise.noise_with_derivative(4.1, 5.2, 6.3, &mut deriv2);
1016
1017 for i in 0..3 {
1018 let expected = first[i] + deriv2[i];
1019 assert!(
1020 (deriv[i] - expected).abs() < 1e-12,
1021 "Derivative[{i}] not accumulated: {0} vs expected {expected}",
1022 deriv[i],
1023 );
1024 }
1025 }
1026}