1use crate::random::Random;
6use std::ops::{Add, Mul, Neg, Sub};
7use std::simd::Simd;
8use std::simd::cmp::{SimdPartialEq, SimdPartialOrd};
9use std::simd::num::{SimdFloat, SimdInt, SimdUint};
10use std::simd::ptr::SimdConstPtr;
11use std::simd::{Mask, Select, StdFloat};
12use steel_math::{
13 GRADIENT_F32, fast_floor, fast_floor_simd, grad_dot, grad_dot_simd, lerp, lerp_simd, lerp2,
14 lerp2_simd, lerp3, lerp3_simd, smoothstep, smoothstep_derivative, smoothstep_simd,
15};
16
17#[derive(Debug, Clone)]
23pub struct ImprovedNoise {
24 p: [u8; 256],
26 pub xo: f64,
28 pub yo: f64,
30 pub zo: f64,
32 yo_floor: i32,
33 yo_fraction: f64,
34 zo_floor: i32,
35 zo_fraction: f64,
36}
37
38impl ImprovedNoise {
39 pub fn new<R: Random>(random: &mut R) -> Self {
44 let xo = random.next_f64() * 256.0;
45 let yo = random.next_f64() * 256.0;
46 let zo = random.next_f64() * 256.0;
47
48 let mut p = [0u8; 256];
49 #[expect(
50 clippy::needless_range_loop,
51 reason = "index is used as the initial permutation value"
52 )]
53 for i in 0..256 {
54 p[i] = i as u8;
55 }
56
57 for i in 0..256 {
59 let offset = random.next_i32_bounded((256 - i) as i32) as usize;
60 p.swap(i, i + offset);
61 }
62
63 let yo_floor = fast_floor(yo);
64 let yo_fraction = yo - f64::from(yo_floor);
65 let zo_floor = fast_floor(zo);
66 let zo_fraction = zo - f64::from(zo_floor);
67
68 Self {
69 p,
70 xo,
71 yo,
72 zo,
73 yo_floor,
74 yo_fraction,
75 zo_floor,
76 zo_fraction,
77 }
78 }
79
80 #[inline]
85 #[must_use]
86 pub fn noise(&self, x: f64, y: f64, z: f64) -> f32 {
87 let x = steel_math::wrap(x) + self.xo;
88 let y = steel_math::wrap(y) + self.yo;
89 let z = steel_math::wrap(z) + self.zo;
90 let floor_x = fast_floor(x);
91 let floor_y = fast_floor(y);
92 let floor_z = fast_floor(z);
93 self.sample_and_lerp(
94 floor_x,
95 floor_y,
96 floor_z,
97 (x - f64::from(floor_x)) as f32,
98 (y - f64::from(floor_y)) as f32,
99 (z - f64::from(floor_z)) as f32,
100 (y - f64::from(floor_y)) as f32,
101 )
102 }
103
104 #[inline]
106 #[must_use]
107 pub fn noise_xz(&self, x: f64, z: f64) -> f32 {
108 let x = steel_math::wrap(x) + self.xo;
109 let z = steel_math::wrap(z) + self.zo;
110 let floor_x = fast_floor(x);
111 let floor_z = fast_floor(z);
112 self.sample_and_lerp(
113 floor_x,
114 self.yo_floor,
115 floor_z,
116 (x - f64::from(floor_x)) as f32,
117 self.yo_fraction as f32,
118 (z - f64::from(floor_z)) as f32,
119 self.yo_fraction as f32,
120 )
121 }
122
123 #[inline]
125 #[must_use]
126 pub fn noise_xy(&self, x: f64, y: f64) -> f32 {
127 let x = steel_math::wrap(x) + self.xo;
128 let y = steel_math::wrap(y) + self.yo;
129 let floor_x = fast_floor(x);
130 let floor_y = fast_floor(y);
131 self.sample_and_lerp(
132 floor_x,
133 floor_y,
134 self.zo_floor,
135 (x - f64::from(floor_x)) as f32,
136 (y - f64::from(floor_y)) as f32,
137 self.zo_fraction as f32,
138 (y - f64::from(floor_y)) as f32,
139 )
140 }
141
142 #[inline]
144 #[must_use]
145 pub fn noise_y_simd<const N: usize>(&self, x: f64, ys: Simd<f64, N>, z: f64) -> Simd<f32, N> {
146 let x = steel_math::wrap(x) + self.xo;
147 let z = steel_math::wrap(z) + self.zo;
148 let ys = steel_math::wrap_simd(ys) + Simd::splat(self.yo);
149 let floor_x = fast_floor(x);
150 let floor_z = fast_floor(z);
151 let floor_ys = fast_floor_simd::<f64, i32, N>(ys);
152 let relative_ys = (ys - floor_ys.cast()).cast();
153
154 self.sample_and_lerp_y_simd(
155 floor_x,
156 floor_ys,
157 floor_z,
158 (x - f64::from(floor_x)) as f32,
159 relative_ys,
160 (z - f64::from(floor_z)) as f32,
161 relative_ys,
162 )
163 }
164
165 #[inline]
168 #[must_use]
169 pub fn smeared_noise(
170 &self,
171 original_x: f64,
172 original_y: f64,
173 original_z: f64,
174 fudge_y_scale: f64,
175 ) -> f32 {
176 let x = steel_math::wrap(original_x) + self.xo;
177 let y = steel_math::wrap(original_y) + self.yo;
178 let z = steel_math::wrap(original_z) + self.zo;
179 let floor_x = fast_floor(x);
180 let floor_y = fast_floor(y);
181 let floor_z = fast_floor(z);
182 let relative_y = y - f64::from(floor_y);
183 let fudge_limit = if original_y >= 0.0 && original_y < relative_y {
184 original_y
185 } else {
186 relative_y
187 };
188 let fudge = (fudge_limit / fudge_y_scale + f64::from(1.0e-7_f32)).floor() * fudge_y_scale;
189 self.sample_and_lerp(
190 floor_x,
191 floor_y,
192 floor_z,
193 (x - f64::from(floor_x)) as f32,
194 (relative_y - fudge) as f32,
195 (z - f64::from(floor_z)) as f32,
196 relative_y as f32,
197 )
198 }
199
200 #[inline]
202 #[must_use]
203 pub fn smeared_noise_xz(&self, x: f64, z: f64, fudge_y_scale: f64) -> f32 {
204 self.smeared_noise(x, 0.0, z, fudge_y_scale)
205 }
206
207 #[inline]
209 #[must_use]
210 pub fn smeared_noise_xy(&self, x: f64, y: f64, fudge_y_scale: f64) -> f32 {
211 self.smeared_noise(x, y, 0.0, fudge_y_scale)
212 }
213
214 #[inline]
219 #[must_use]
220 pub fn smeared_noise_y_simd<const N: usize>(
221 &self,
222 original_x: f64,
223 original_ys: Simd<f64, N>,
224 original_z: f64,
225 fudge_y_scale: f64,
226 ) -> Simd<f32, N> {
227 let x = steel_math::wrap(original_x) + self.xo;
228 let ys = steel_math::wrap_simd(original_ys) + Simd::splat(self.yo);
229 let z = steel_math::wrap(original_z) + self.zo;
230 let floor_x = fast_floor(x);
231 let floor_ys = fast_floor_simd::<f64, i32, N>(ys);
232 let floor_z = fast_floor(z);
233 let relative_ys = ys - floor_ys.cast();
234 let zero = Simd::splat(0.0);
235 let fudge_limits = (original_ys.simd_ge(zero) & original_ys.simd_lt(relative_ys))
236 .select(original_ys, relative_ys);
237 let fudge = (fudge_limits / Simd::splat(fudge_y_scale)
238 + Simd::splat(f64::from(1.0e-7_f32)))
239 .floor()
240 * Simd::splat(fudge_y_scale);
241
242 self.sample_and_lerp_y_simd(
243 floor_x,
244 floor_ys,
245 floor_z,
246 (x - f64::from(floor_x)) as f32,
247 (relative_ys - fudge).cast(),
248 (z - f64::from(floor_z)) as f32,
249 relative_ys.cast(),
250 )
251 }
252
253 #[expect(
254 clippy::too_many_arguments,
255 reason = "matches vanilla PerlinNoise.sampleAndLerp"
256 )]
257 #[cfg_attr(
258 not(target_feature = "avx512f"),
259 expect(
260 clippy::similar_names,
261 reason = "coordinate offsets mirror vanilla's eight Perlin corners"
262 )
263 )]
264 fn sample_and_lerp(
265 &self,
266 x: i32,
267 y: i32,
268 z: i32,
269 relative_x: f32,
270 relative_y: f32,
271 relative_z: f32,
272 original_relative_y: f32,
273 ) -> f32 {
274 #[cfg(target_feature = "avx512f")]
275 let [d000, d100, d010, d110, d001, d101, d011, d111] =
276 self.corner_gradients_simd(x, y, z, relative_x, relative_y, relative_z);
277
278 #[cfg(not(target_feature = "avx512f"))]
279 let [d000, d100, d010, d110, d001, d101, d011, d111] = {
280 let x1 = x.wrapping_add(1);
281 let y1 = y.wrapping_add(1);
282 let z1 = z.wrapping_add(1);
283 let relative_x1 = relative_x - 1.0;
284 let relative_y1 = relative_y - 1.0;
285 let relative_z1 = relative_z - 1.0;
286
287 let d000 = grad_dot_flat(&self.p, x, y, z, relative_x, relative_y, relative_z);
288 let d100 = grad_dot_flat(&self.p, x1, y, z, relative_x1, relative_y, relative_z);
289 let d010 = grad_dot_flat(&self.p, x, y1, z, relative_x, relative_y1, relative_z);
290 let d110 = grad_dot_flat(&self.p, x1, y1, z, relative_x1, relative_y1, relative_z);
291 let d001 = grad_dot_flat(&self.p, x, y, z1, relative_x, relative_y, relative_z1);
292 let d101 = grad_dot_flat(&self.p, x1, y, z1, relative_x1, relative_y, relative_z1);
293 let d011 = grad_dot_flat(&self.p, x, y1, z1, relative_x, relative_y1, relative_z1);
294 let d111 = grad_dot_flat(&self.p, x1, y1, z1, relative_x1, relative_y1, relative_z1);
295 [d000, d100, d010, d110, d001, d101, d011, d111]
296 };
297 let x_alpha = smoothstep(relative_x);
298 let y_alpha = smoothstep(original_relative_y);
299 let z_alpha = smoothstep(relative_z);
300 let xz0 = lerp2(x_alpha, y_alpha, d000, d100, d010, d110);
301 let xz1 = lerp2(x_alpha, y_alpha, d001, d101, d011, d111);
302 lerp(z_alpha, xz0, xz1)
303 }
304
305 #[cfg(any(test, target_feature = "avx512f"))]
307 #[inline]
308 fn corner_gradients_simd(
309 &self,
310 x: i32,
311 y: i32,
312 z: i32,
313 relative_x: f32,
314 relative_y: f32,
315 relative_z: f32,
316 ) -> [f32; 8] {
317 let x = x as u8;
318 let y = y as u8;
319 let z = z as u8;
320 let x0 = self.p[x as usize];
321 let x1 = self.p[x.wrapping_add(1) as usize];
322 let xy = [
323 self.p[x0.wrapping_add(y) as usize],
324 self.p[x1.wrapping_add(y) as usize],
325 self.p[x0.wrapping_add(y).wrapping_add(1) as usize],
326 self.p[x1.wrapping_add(y).wrapping_add(1) as usize],
327 ];
328 let xs = Simd::from_array([relative_x, relative_x - 1.0, relative_x, relative_x - 1.0]);
329 let ys = Simd::from_array([relative_y, relative_y, relative_y - 1.0, relative_y - 1.0]);
330 let sample_face = |face_z: u8, relative_z| {
331 let gradients =
332 xy.map(|xy| GRADIENT_F32[self.p[xy.wrapping_add(face_z) as usize] as usize & 15]);
333 let gx = Simd::from_array(gradients.map(|g| g[0]));
334 let gy = Simd::from_array(gradients.map(|g| g[1]));
335 let gz = Simd::from_array(gradients.map(|g| g[2]));
336 (gx * xs + gy * ys + gz * Simd::splat(relative_z)).to_array()
338 };
339 let low = sample_face(z, relative_z);
340 let high = sample_face(z.wrapping_add(1), relative_z - 1.0);
341 [
342 low[0], low[1], low[2], low[3], high[0], high[1], high[2], high[3],
343 ]
344 }
345
346 #[inline]
347 fn column_corner_hashes(&self, x0: u8, x1: u8, y: u8, z: u8) -> [usize; 8] {
348 let xy = [
349 self.p[x0.wrapping_add(y) as usize],
350 self.p[x1.wrapping_add(y) as usize],
351 self.p[x0.wrapping_add(y).wrapping_add(1) as usize],
352 self.p[x1.wrapping_add(y).wrapping_add(1) as usize],
353 ];
354 let low = xy.map(|xy| self.p[xy.wrapping_add(z) as usize] as usize);
355 let high = xy.map(|xy| self.p[xy.wrapping_add(z).wrapping_add(1) as usize] as usize);
356 [
357 low[0], low[1], low[2], low[3], high[0], high[1], high[2], high[3],
358 ]
359 }
360
361 #[expect(
362 clippy::too_many_arguments,
363 reason = "mirrors scalar sample_and_lerp with SIMD Y lanes"
364 )]
365 #[inline]
366 fn sample_and_lerp_y_simd<const N: usize>(
367 &self,
368 x: i32,
369 ys: Simd<i32, N>,
370 z: i32,
371 relative_x: f32,
372 relative_ys: Simd<f32, N>,
373 relative_z: f32,
374 original_relative_ys: Simd<f32, N>,
375 ) -> Simd<f32, N> {
376 let wrapped_ys = ys.cast::<u8>();
377 if wrapped_ys.simd_eq(Simd::splat(wrapped_ys[0])).all() {
379 self.sample_and_lerp_y_simd_kernel::<N, true>(
380 x,
381 ys,
382 z,
383 relative_x,
384 relative_ys,
385 relative_z,
386 original_relative_ys,
387 )
388 } else {
389 self.sample_and_lerp_y_simd_kernel::<N, false>(
390 x,
391 ys,
392 z,
393 relative_x,
394 relative_ys,
395 relative_z,
396 original_relative_ys,
397 )
398 }
399 }
400
401 #[expect(
402 clippy::too_many_arguments,
403 reason = "mirrors scalar sample_and_lerp with SIMD Y lanes"
404 )]
405 #[expect(
406 clippy::similar_names,
407 reason = "coordinate offsets mirror vanilla's eight Perlin corners"
408 )]
409 #[inline]
410 fn sample_and_lerp_y_simd_kernel<const N: usize, const SHARED_Y: bool>(
411 &self,
412 x: i32,
413 ys: Simd<i32, N>,
414 z: i32,
415 relative_x: f32,
416 relative_ys: Simd<f32, N>,
417 relative_z: f32,
418 original_relative_ys: Simd<f32, N>,
419 ) -> Simd<f32, N> {
420 let x = x as u8;
421 let z = z as u8;
422 let x0 = self.p[x as usize];
423 let x1 = self.p[x.wrapping_add(1) as usize];
424 let ys = ys.cast::<u8>();
425
426 let mut h000 = [0_usize; N];
427 let mut h100 = [0_usize; N];
428 let mut h010 = [0_usize; N];
429 let mut h110 = [0_usize; N];
430 let mut h001 = [0_usize; N];
431 let mut h101 = [0_usize; N];
432 let mut h011 = [0_usize; N];
433 let mut h111 = [0_usize; N];
434
435 for lane in 0..N {
436 let y = ys[if SHARED_Y { 0 } else { lane }];
437 let hashes = self.column_corner_hashes(x0, x1, y, z);
438 h000[lane] = hashes[0];
439 h100[lane] = hashes[1];
440 h010[lane] = hashes[2];
441 h110[lane] = hashes[3];
442 h001[lane] = hashes[4];
443 h101[lane] = hashes[5];
444 h011[lane] = hashes[6];
445 h111[lane] = hashes[7];
446 }
447
448 let relative_xs = Simd::splat(relative_x);
449 let relative_zs = Simd::splat(relative_z);
450 let one = Simd::splat(1.0);
451 let relative_x1 = relative_xs - one;
452 let relative_y1 = relative_ys - one;
453 let relative_z1 = relative_zs - one;
454
455 let d000 = grad_dot_simd(h000, relative_xs, relative_ys, relative_zs);
456 let d100 = grad_dot_simd(h100, relative_x1, relative_ys, relative_zs);
457 let d010 = grad_dot_simd(h010, relative_xs, relative_y1, relative_zs);
458 let d110 = grad_dot_simd(h110, relative_x1, relative_y1, relative_zs);
459 let d001 = grad_dot_simd(h001, relative_xs, relative_ys, relative_z1);
460 let d101 = grad_dot_simd(h101, relative_x1, relative_ys, relative_z1);
461 let d011 = grad_dot_simd(h011, relative_xs, relative_y1, relative_z1);
462 let d111 = grad_dot_simd(h111, relative_x1, relative_y1, relative_z1);
463
464 let x_alpha = smoothstep_simd(relative_xs);
465 let y_alpha = smoothstep_simd(original_relative_ys);
466 let z_alpha = smoothstep_simd(relative_zs);
467 let xz0 = lerp2_simd(x_alpha, y_alpha, d000, d100, d010, d110);
468 let xz1 = lerp2_simd(x_alpha, y_alpha, d001, d101, d011, d111);
469 lerp_simd(z_alpha, xz0, xz1)
470 }
471
472 #[inline]
474 #[must_use]
475 pub fn noise_simd<const N: usize>(
476 &self,
477 x: Simd<f32, N>,
478 y: Simd<f32, N>,
479 z: Simd<f32, N>,
480 ) -> Simd<f32, N>
481 where
482 Simd<f32, N>: SimdFloat<Cast<i32> = Simd<i32, N>>
483 + SimdPartialOrd<Mask = Mask<i32, N>>
484 + SimdPartialEq<Mask = Mask<i32, N>>
485 + Add<Output = Simd<f32, N>>
486 + Sub<Output = Simd<f32, N>>
487 + Mul<Output = Simd<f32, N>>
488 + Neg<Output = Simd<f32, N>>,
489 {
490 let x = x + Simd::splat(self.xo).cast();
491 let y = y + Simd::splat(self.yo).cast();
492 let z = z + Simd::splat(self.zo).cast();
493
494 let xf = fast_floor_simd::<f32, i32, N>(x);
495 let yf = fast_floor_simd::<f32, i32, N>(y);
496 let zf = fast_floor_simd::<f32, i32, N>(z);
497
498 let xr = x - xf.cast();
499 let yr = y - yf.cast();
500 let zr = z - zf.cast();
501
502 self.sample_and_lerp_simd(xf, yf, zf, xr, yr, zr, yr)
503 }
504
505 #[must_use]
510 pub fn noise_with_derivative(
511 &self,
512 x: f64,
513 y: f64,
514 z: f64,
515 derivative_out: &mut [f32; 3],
516 ) -> f32 {
517 let x = steel_math::wrap(x) + self.xo;
518 let y = steel_math::wrap(y) + self.yo;
519 let z = steel_math::wrap(z) + self.zo;
520
521 let xf = fast_floor(x);
522 let yf = fast_floor(y);
523 let zf = fast_floor(z);
524
525 let xr = (x - f64::from(xf)) as f32;
526 let yr = (y - f64::from(yf)) as f32;
527 let zr = (z - f64::from(zf)) as f32;
528
529 self.sample_with_derivative(xf, yf, zf, xr, yr, zr, derivative_out)
530 }
531
532 #[must_use]
542 #[expect(
543 clippy::similar_names,
544 reason = "yr_fudge and y_fudge match vanilla naming"
545 )]
546 pub fn noise_with_y_scale(&self, x: f64, y: f64, z: f64, y_scale: f64, y_fudge: f64) -> f32 {
547 let x = x + self.xo;
548 let y = y + self.yo;
549 let z = z + self.zo;
550
551 let xf = fast_floor(x);
552 let yf = fast_floor(y);
553 let zf = fast_floor(z);
554
555 let xr = x - f64::from(xf);
556 let yr = y - f64::from(yf);
557 let zr = z - f64::from(zf);
558
559 #[expect(
561 clippy::if_not_else,
562 reason = "matches vanilla's conditional structure"
563 )]
564 let yr_fudge = if y_scale != 0.0 {
565 let fudge_limit = if y_fudge >= 0.0 && y_fudge < yr {
566 y_fudge
567 } else {
568 yr
569 };
570 (fudge_limit / y_scale + f64::from(1.0e-7_f32)).floor() * y_scale
572 } else {
573 0.0
574 };
575 self.sample_and_lerp(
576 xf,
577 yf,
578 zf,
579 xr as f32,
580 (yr - yr_fudge) as f32,
581 zr as f32,
582 yr as f32,
583 )
584 }
585
586 #[inline]
587 fn p_simd<const N: usize>(&self, idx: Simd<u8, N>) -> Simd<u8, N> {
588 let offset = idx.cast::<usize>();
589 let p = Simd::splat(self.p.as_ptr()).wrapping_add(offset);
590 unsafe { Simd::gather_ptr(p) }
593 }
594
595 #[expect(clippy::too_many_arguments, reason = "matches vanilla signature")]
597 fn sample_and_lerp_simd<const N: usize>(
598 &self,
599 x: Simd<i32, N>,
600 y: Simd<i32, N>,
601 z: Simd<i32, N>,
602 xr: Simd<f32, N>,
603 yr: Simd<f32, N>,
604 zr: Simd<f32, N>,
605 yr_original: Simd<f32, N>,
606 ) -> Simd<f32, N>
607 where
608 Simd<f32, N>: Mul<Output = Simd<f32, N>>
609 + Add<Output = Simd<f32, N>>
610 + Sub<Output = Simd<f32, N>>
611 + Neg<Output = Simd<f32, N>>,
612 {
613 let x = x.cast::<u8>();
614 let y = y.cast::<u8>();
615 let z = z.cast::<u8>();
616 let x0 = self.p_simd(x);
618 let x1 = self.p_simd(x + Simd::splat(1));
619
620 let xy00 = self.p_simd(x0 + y);
621 let xy01 = self.p_simd(x0 + y + Simd::splat(1));
622 let xy10 = self.p_simd(x1 + y);
623 let xy11 = self.p_simd(x1 + y + Simd::splat(1));
624
625 let h000 = self.p_simd(xy00 + z).cast::<usize>().to_array();
626 let h100 = self.p_simd(xy10 + z).cast::<usize>().to_array();
627 let h010 = self.p_simd(xy01 + z).cast::<usize>().to_array();
628 let h110 = self.p_simd(xy11 + z).cast::<usize>().to_array();
629 let h001 = self
630 .p_simd(xy00 + z + Simd::splat(1))
631 .cast::<usize>()
632 .to_array();
633 let h101 = self
634 .p_simd(xy10 + z + Simd::splat(1))
635 .cast::<usize>()
636 .to_array();
637 let h011 = self
638 .p_simd(xy01 + z + Simd::splat(1))
639 .cast::<usize>()
640 .to_array();
641 let h111 = self
642 .p_simd(xy11 + z + Simd::splat(1))
643 .cast::<usize>()
644 .to_array();
645
646 let d000 = grad_dot_simd(h000, xr, yr, zr);
648 let d100 = grad_dot_simd(h100, xr - Simd::splat(1.0), yr, zr);
649 let d010 = grad_dot_simd(h010, xr, yr - Simd::splat(1.0), zr);
650 let d110 = grad_dot_simd(h110, xr - Simd::splat(1.0), yr - Simd::splat(1.0), zr);
651 let d001 = grad_dot_simd(h001, xr, yr, zr - Simd::splat(1.0));
652 let d101 = grad_dot_simd(h101, xr - Simd::splat(1.0), yr, zr - Simd::splat(1.0));
653 let d011 = grad_dot_simd(h011, xr, yr - Simd::splat(1.0), zr - Simd::splat(1.0));
654 let d111 = grad_dot_simd(
655 h111,
656 xr - Simd::splat(1.0),
657 yr - Simd::splat(1.0),
658 zr - Simd::splat(1.0),
659 );
660
661 let x_alpha = smoothstep_simd(xr);
663 let y_alpha = smoothstep_simd(yr_original);
664 let z_alpha = smoothstep_simd(zr);
665
666 lerp3_simd(
667 x_alpha, y_alpha, z_alpha, d000, d100, d010, d110, d001, d101, d011, d111,
668 )
669 }
670
671 #[inline]
676 #[must_use]
677 pub fn noise_with_y_scale_simd<const N: usize>(
678 &self,
679 x: f64,
680 ys: Simd<f64, N>,
681 z: f64,
682 y_scale: f64,
683 y_fudges: Simd<f64, N>,
684 ) -> Simd<f32, N> {
685 let x = x + self.xo;
687 let z = z + self.zo;
688 let xf = fast_floor(x);
689 let zf = fast_floor(z);
690 let xr = x - f64::from(xf);
691 let zr = z - f64::from(zf);
692
693 let ys = ys + Simd::splat(self.yo);
695 let ys_floor = fast_floor_simd::<f64, i32, N>(ys);
696 let yrs = ys - ys_floor.cast();
697
698 let yr_fudge: Simd<f64, N> = if y_scale == 0.0 {
700 Simd::splat(0.0)
701 } else {
702 let y_scale_v: Simd<f64, N> = Simd::splat(y_scale);
703 let zero: Simd<f64, N> = Simd::splat(0.0);
704 let mask = y_fudges.simd_ge(zero) & y_fudges.simd_lt(yrs);
705 let fudge_limits = mask.select(y_fudges, yrs);
706 let epsilon: Simd<f64, N> = Simd::splat(f64::from(1.0e-7_f32));
707 ((fudge_limits / y_scale_v) + epsilon).floor() * y_scale_v
708 };
709
710 let yrs_adjusted = yrs - yr_fudge;
711
712 self.sample_and_lerp_y_simd(
713 xf,
714 ys_floor,
715 zf,
716 xr as f32,
717 yrs_adjusted.cast(),
718 zr as f32,
719 yrs.cast(),
720 )
721 }
722
723 #[expect(clippy::too_many_arguments, reason = "matches vanilla signature")]
725 fn sample_with_derivative(
726 &self,
727 x: i32,
728 y: i32,
729 z: i32,
730 xr: f32,
731 yr: f32,
732 zr: f32,
733 derivative_out: &mut [f32; 3],
734 ) -> f32 {
735 let x = x as u8;
736 let y = y as u8;
737 let z = z as u8;
738
739 let x0 = self.p[x as usize];
740 let x1 = self.p[x.wrapping_add(1) as usize];
741 let xy00 = self.p[x0.wrapping_add(y) as usize];
742 let xy01 = self.p[x0.wrapping_add(y).wrapping_add(1) as usize];
743 let xy10 = self.p[x1.wrapping_add(y) as usize];
744 let xy11 = self.p[x1.wrapping_add(y).wrapping_add(1) as usize];
745
746 let h000 = self.p[xy00.wrapping_add(z) as usize] as usize;
747 let h100 = self.p[xy10.wrapping_add(z) as usize] as usize;
748 let h010 = self.p[xy01.wrapping_add(z) as usize] as usize;
749 let h110 = self.p[xy11.wrapping_add(z) as usize] as usize;
750 let h001 = self.p[xy00.wrapping_add(z).wrapping_add(1) as usize] as usize;
751 let h101 = self.p[xy10.wrapping_add(z).wrapping_add(1) as usize] as usize;
752 let h011 = self.p[xy01.wrapping_add(z).wrapping_add(1) as usize] as usize;
753 let h111 = self.p[xy11.wrapping_add(z).wrapping_add(1) as usize] as usize;
754
755 let g000 = Simd::from_array(GRADIENT_F32[h000 & 15]);
756 let g100 = Simd::from_array(GRADIENT_F32[h100 & 15]);
757 let g010 = Simd::from_array(GRADIENT_F32[h010 & 15]);
758 let g110 = Simd::from_array(GRADIENT_F32[h110 & 15]);
759 let g001 = Simd::from_array(GRADIENT_F32[h001 & 15]);
760 let g101 = Simd::from_array(GRADIENT_F32[h101 & 15]);
761 let g011 = Simd::from_array(GRADIENT_F32[h011 & 15]);
762 let g111 = Simd::from_array(GRADIENT_F32[h111 & 15]);
763
764 let d000 = grad_dot(h000, xr, yr, zr);
766 let d100 = grad_dot(h100, xr - 1.0, yr, zr);
767 let d010 = grad_dot(h010, xr, yr - 1.0, zr);
768 let d110 = grad_dot(h110, xr - 1.0, yr - 1.0, zr);
769 let d001 = grad_dot(h001, xr, yr, zr - 1.0);
770 let d101 = grad_dot(h101, xr - 1.0, yr, zr - 1.0);
771 let d011 = grad_dot(h011, xr, yr - 1.0, zr - 1.0);
772 let d111 = grad_dot(h111, xr - 1.0, yr - 1.0, zr - 1.0);
773
774 let alpha_x = smoothstep(xr);
775 let alpha_y = smoothstep(yr);
776 let alpha_z = smoothstep(zr);
777
778 let d1_v = lerp3_simd(
780 Simd::splat(alpha_x),
781 Simd::splat(alpha_y),
782 Simd::splat(alpha_z),
783 g000,
784 g100,
785 g010,
786 g110,
787 g001,
788 g101,
789 g011,
790 g111,
791 );
792
793 let d2x = lerp2(
795 alpha_y,
796 alpha_z,
797 d100 - d000,
798 d110 - d010,
799 d101 - d001,
800 d111 - d011,
801 );
802 let d2y = lerp2(
803 alpha_z,
804 alpha_x,
805 d010 - d000,
806 d011 - d001,
807 d110 - d100,
808 d111 - d101,
809 );
810 let d2z = lerp2(
811 alpha_x,
812 alpha_y,
813 d001 - d000,
814 d101 - d100,
815 d011 - d010,
816 d111 - d110,
817 );
818
819 let x_sd = smoothstep_derivative(xr);
820 let y_sd = smoothstep_derivative(yr);
821 let z_sd = smoothstep_derivative(zr);
822
823 derivative_out[0] += d1_v[0] + x_sd * d2x;
825 derivative_out[1] += d1_v[1] + y_sd * d2y;
826 derivative_out[2] += d1_v[2] + z_sd * d2z;
827 lerp3(
828 alpha_x, alpha_y, alpha_z, d000, d100, d010, d110, d001, d101, d011, d111,
829 )
830 }
831}
832
833#[inline]
835const fn gradient_hash(p: &[u8; 256], px: i32, py: i32, pz: i32) -> usize {
836 let qx = (px & 0xFF) as u8;
837 let qy = (py & 0xFF) as u8;
838 let qz = (pz & 0xFF) as u8;
839 let a = p[qx as usize];
840 let b = p[a.wrapping_add(qy) as usize];
841 p[b.wrapping_add(qz) as usize] as usize
842}
843
844#[inline]
845fn grad_dot_flat(p: &[u8; 256], px: i32, py: i32, pz: i32, fx: f32, fy: f32, fz: f32) -> f32 {
846 grad_dot(gradient_hash(p, px, py, pz), fx, fy, fz)
847}
848
849#[cfg(test)]
850mod tests {
851 use super::*;
852 use crate::random::xoroshiro::Xoroshiro;
853 use std::simd::{f64x4, f64x8};
854
855 #[test]
856 fn corner_gradient_batches_match_scalar_at_permutation_boundaries() {
857 let mut random = Xoroshiro::from_seed(42);
858 let noise = ImprovedNoise::new(&mut random);
859 for x in [-257_i32, -1, 0, 255, 256, i32::MAX] {
860 for y in [-1_i32, 0, 255, i32::MAX] {
861 for z in [-1_i32, 0, 255, i32::MAX] {
862 for [rx, ry, rz] in [[0.0_f32, 0.0, 0.0], [0.125, -0.25, 0.75], [0.9, 0.7, 0.3]]
863 {
864 let actual = noise.corner_gradients_simd(x, y, z, rx, ry, rz);
865 for (corner, actual) in actual.into_iter().enumerate() {
866 let dx = (corner & 1) as i32;
867 let dy = ((corner >> 1) & 1) as i32;
868 let dz = ((corner >> 2) & 1) as i32;
869 let expected = grad_dot_flat(
870 &noise.p,
871 x.wrapping_add(dx),
872 y.wrapping_add(dy),
873 z.wrapping_add(dz),
874 rx - dx as f32,
875 ry - dy as f32,
876 rz - dz as f32,
877 );
878 assert_eq!(
879 actual.to_bits(),
880 expected.to_bits(),
881 "({x}, {y}, {z}), corner {corner}"
882 );
883 }
884 }
885 }
886 }
887 }
888 }
889
890 #[test]
891 fn test_noise_with_y_scale_4x_matches_scalar() {
892 let mut rng = Xoroshiro::from_seed(42);
893 let noise = ImprovedNoise::new(&mut rng);
894
895 let test_x_zs: &[(f64, f64)] = &[
897 (0.0, 0.0),
898 (1.5, 3.7),
899 (-5.2, 100.3),
900 (0.001, -0.001),
901 (1000.0, -500.0),
902 ];
903 let test_ys: &[[f64; 4]] = &[
904 [0.0, 1.0, 2.0, 3.0],
905 [64.0, 64.5, 65.0, 65.5],
906 [-5.0, -2.5, 0.0, 2.5],
907 [0.25, 0.5, 0.75, 1.0],
908 [-100.0, -50.0, 50.0, 100.0],
909 ];
910 let y_scales = [0.0, 1.0, 8.0];
911
912 for &(x, z) in test_x_zs {
913 for ys in test_ys {
914 for &y_scale in &y_scales {
915 let y_fudges: [f64; 4] = if y_scale == 0.0 {
916 [0.0; 4]
917 } else {
918 *ys };
920
921 let simd_result = noise.noise_with_y_scale_simd(
922 x,
923 f64x4::from_array(*ys),
924 z,
925 y_scale,
926 f64x4::from_array(y_fudges),
927 );
928
929 for i in 0..4 {
930 let scalar = noise.noise_with_y_scale(x, ys[i], z, y_scale, y_fudges[i]);
931 let simd_val = simd_result[i];
932 assert!(
933 (scalar - simd_val).abs() < 1e-14,
934 "Mismatch at x={x}, y={}, z={z}, y_scale={y_scale}: \
935 scalar={scalar}, simd={simd_val}, diff={}",
936 ys[i],
937 (scalar - simd_val).abs(),
938 );
939 }
940 }
941 }
942 }
943 }
944
945 #[test]
946 fn smeared_noise_y_simd_matches_scalar() {
947 let mut rng = Xoroshiro::from_seed(42);
948 let noise = ImprovedNoise::new(&mut rng);
949 let columns = [
950 [-64.0, -56.0, -48.0, -40.0, -32.0, -24.0, -16.0, -8.0],
951 [0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 0.9375].map(|y| y - noise.yo),
952 ];
953 for ys in columns {
954 let simd = noise.smeared_noise_y_simd(
955 20_000_068.0,
956 f64x8::from_array(ys),
957 -19_999_796.0,
958 5475.296,
959 );
960
961 for (&y, &value) in ys.iter().zip(simd.as_array()) {
962 assert_eq!(
963 value.to_bits(),
964 noise
965 .smeared_noise(20_000_068.0, y, -19_999_796.0, 5475.296)
966 .to_bits(),
967 "Y={y}"
968 );
969 }
970 }
971 }
972
973 #[test]
974 fn test_noise_y_simd_matches_scalar() {
975 let mut rng = Xoroshiro::from_seed(42);
976 let noise = ImprovedNoise::new(&mut rng);
977 let columns = [
978 (0.0, 0.0, [0.0, 1.25, -5.5, 1000.75]),
979 (0.0, 0.0, [0.125, 0.25, 0.5, 0.75].map(|y| y - noise.yo)),
980 (
981 0.0,
982 0.0,
983 [0.125, 256.25, 512.5, 768.75].map(|y| y - noise.yo),
984 ),
985 (
986 255.25 - noise.xo,
987 -256.25 - noise.zo,
988 [
989 255.5 - noise.yo,
990 256.5 - noise.yo,
991 -1.5 - noise.yo,
992 -256.5 - noise.yo,
993 ],
994 ),
995 (33_554_432.0, -33_554_432.0, [64.0, 64.5, 65.0, 65.5]),
996 ];
997
998 for (x, z, ys) in columns {
999 let simd = noise.noise_y_simd(x, f64x4::from_array(ys), z);
1000 for (lane, y) in ys.into_iter().enumerate() {
1001 assert_eq!(simd[lane].to_bits(), noise.noise(x, y, z).to_bits());
1002 }
1003 }
1004 }
1005
1006 #[test]
1007 fn test_zero_axis_helpers_match_full_noise() {
1008 let mut rng = Xoroshiro::from_seed(12_345);
1009 let noise = ImprovedNoise::new(&mut rng);
1010 let samples = [
1011 (0.0, 0.0),
1012 (1.25, -30.75),
1013 (-1000.0, 4096.5),
1014 (33_554_431.5, -33_554_432.25),
1015 (-0.000_000_1, 0.000_000_1),
1016 ];
1017
1018 for &(a, b) in &samples {
1019 assert_eq!(noise.noise_xz(a, b), noise.noise(a, 0.0, b));
1020 assert_eq!(noise.noise_xy(a, b), noise.noise(a, b, 0.0));
1021 }
1022 }
1023
1024 #[test]
1025 fn test_zero_axis_smeared_helpers_match_full_noise() {
1026 let mut rng = Xoroshiro::from_seed(12_345);
1027 let noise = ImprovedNoise::new(&mut rng);
1028 let fudge_y_scale = 5475.296;
1029 let samples = [(0.0, 0.0), (1.25, -30.75), (-1000.0, 4096.5)];
1030
1031 for &(a, b) in &samples {
1032 assert_eq!(
1033 noise.smeared_noise_xz(a, b, fudge_y_scale),
1034 noise.smeared_noise(a, 0.0, b, fudge_y_scale),
1035 );
1036 assert_eq!(
1037 noise.smeared_noise_xy(a, b, fudge_y_scale),
1038 noise.smeared_noise(a, b, 0.0, fudge_y_scale),
1039 );
1040 }
1041 }
1042
1043 #[test]
1044 fn test_noise_with_y_scale_simd8_matches_scalar() {
1045 use std::simd::f64x8;
1046
1047 let mut rng = Xoroshiro::from_seed(42);
1048 let noise = ImprovedNoise::new(&mut rng);
1049
1050 let test_x_zs: &[(f64, f64)] = &[
1051 (0.0, 0.0),
1052 (1.5, 3.7),
1053 (-5.2, 100.3),
1054 (0.001, -0.001),
1055 (1000.0, -500.0),
1056 ];
1057 let test_ys: &[[f64; 8]] = &[
1058 [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
1059 [64.0, 64.5, 65.0, 65.5, 66.0, 66.5, 67.0, 67.5],
1060 [-5.0, -2.5, 0.0, 2.5, 5.0, 7.5, 10.0, 12.5],
1061 [0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
1062 [-100.0, -50.0, -25.0, -10.0, 10.0, 25.0, 50.0, 100.0],
1063 ];
1064 let y_scales = [0.0, 1.0, 8.0];
1065
1066 for &(x, z) in test_x_zs {
1067 for ys in test_ys {
1068 for &y_scale in &y_scales {
1069 let y_fudges: [f64; 8] = if y_scale == 0.0 { [0.0; 8] } else { *ys };
1070
1071 let simd_result = noise.noise_with_y_scale_simd(
1072 x,
1073 f64x8::from_array(*ys),
1074 z,
1075 y_scale,
1076 f64x8::from_array(y_fudges),
1077 );
1078
1079 for i in 0..8 {
1080 let scalar = noise.noise_with_y_scale(x, ys[i], z, y_scale, y_fudges[i]);
1081 let simd_val = simd_result[i];
1082 assert!(
1083 (scalar - simd_val).abs() < 1e-14,
1084 "Mismatch at x={x}, y={}, z={z}, y_scale={y_scale}: \
1085 scalar={scalar}, simd={simd_val}, diff={}",
1086 ys[i],
1087 (scalar - simd_val).abs(),
1088 );
1089 }
1090 }
1091 }
1092 }
1093 }
1094
1095 #[test]
1096 fn test_improved_noise_deterministic() {
1097 let mut rng1 = Xoroshiro::from_seed(12345);
1098 let mut rng2 = Xoroshiro::from_seed(12345);
1099
1100 let noise1 = ImprovedNoise::new(&mut rng1);
1101 let noise2 = ImprovedNoise::new(&mut rng2);
1102
1103 assert_eq!(noise1.xo, noise2.xo);
1105 assert_eq!(noise1.yo, noise2.yo);
1106 assert_eq!(noise1.zo, noise2.zo);
1107 assert_eq!(noise1.p, noise2.p);
1108
1109 let v1 = noise1.noise(100.0, 64.0, 100.0);
1111 let v2 = noise2.noise(100.0, 64.0, 100.0);
1112 assert!((v1 - v2).abs() < 1e-15);
1113 }
1114
1115 #[test]
1116 fn scalar_noise_wraps_corner_coordinates_at_i32_max() {
1117 let mut rng = Xoroshiro::from_seed(42);
1118 let mut noise = ImprovedNoise::new(&mut rng);
1119 noise.xo = 0.0;
1120 noise.yo = 0.0;
1121 noise.zo = 0.0;
1122
1123 let _ = noise.noise(
1124 f64::from(i32::MAX),
1125 f64::from(i32::MAX),
1126 f64::from(i32::MAX),
1127 );
1128 }
1129
1130 #[test]
1131 fn test_improved_noise_range() {
1132 let mut rng = Xoroshiro::from_seed(42);
1133 let noise = ImprovedNoise::new(&mut rng);
1134
1135 for x in -10..10 {
1137 for z in -10..10 {
1138 let v = noise.noise(f64::from(x) * 10.0, 64.0, f64::from(z) * 10.0);
1139 assert!(
1141 (-1.5..=1.5).contains(&v),
1142 "Noise value {v} at ({x}, {z}) out of expected range",
1143 );
1144 }
1145 }
1146 }
1147
1148 #[test]
1149 fn test_improved_noise_spatial_variation() {
1150 let mut rng = Xoroshiro::from_seed(42);
1151 let noise = ImprovedNoise::new(&mut rng);
1152
1153 let v1 = noise.noise(0.0, 0.0, 0.0);
1155 let v2 = noise.noise(100.0, 0.0, 0.0);
1156 let v3 = noise.noise(0.0, 100.0, 0.0);
1157 let v4 = noise.noise(0.0, 0.0, 100.0);
1158
1159 #[expect(
1161 clippy::float_cmp,
1162 reason = "intentional exact equality check to detect degenerate constant noise"
1163 )]
1164 let all_same = v1 == v2 && v2 == v3 && v3 == v4;
1165 assert!(!all_same, "All noise values are the same - unexpected");
1166 }
1167
1168 #[test]
1169 fn test_noise_with_derivative_produces_derivatives() {
1170 let mut rng = Xoroshiro::from_seed(42);
1171 let noise = ImprovedNoise::new(&mut rng);
1172
1173 let mut deriv = [0.0; 3];
1174 let _ = noise.noise_with_derivative(1.5, 2.3, 3.7, &mut deriv);
1175
1176 let any_nonzero = deriv.iter().any(|&d| d.abs() > 1e-15);
1178 assert!(any_nonzero, "All derivatives are zero: {deriv:?}");
1179 }
1180
1181 #[test]
1182 fn test_noise_with_derivative_accumulates() {
1183 let mut rng = Xoroshiro::from_seed(42);
1184 let noise = ImprovedNoise::new(&mut rng);
1185
1186 let mut deriv = [0.0; 3];
1188 let _ = noise.noise_with_derivative(1.5, 2.3, 3.7, &mut deriv);
1189 let first = deriv;
1190
1191 let _ = noise.noise_with_derivative(4.1, 5.2, 6.3, &mut deriv);
1193 let mut deriv2 = [0.0; 3];
1194 let _ = noise.noise_with_derivative(4.1, 5.2, 6.3, &mut deriv2);
1195
1196 for i in 0..3 {
1197 let expected = first[i] + deriv2[i];
1198 assert!(
1199 (deriv[i] - expected).abs() < 1e-12,
1200 "Derivative[{i}] not accumulated: {0} vs expected {expected}",
1201 deriv[i],
1202 );
1203 }
1204 }
1205}