Skip to main content

steel_math/noise_math/
gradient.rs

1use std::ops::{Add, Mul, Neg, Sub};
2use std::simd::Simd;
3#[cfg(target_feature = "avx512f")]
4use std::simd::{
5    Select,
6    cmp::{SimdPartialEq, SimdPartialOrd},
7};
8
9/// Gradient vectors shared between Perlin and simplex noise (from vanilla `SimplexNoise.GRADIENT`).
10pub const GRADIENT: [[f64; 3]; 16] = [
11    [1.0, 1.0, 0.0],
12    [-1.0, 1.0, 0.0],
13    [1.0, -1.0, 0.0],
14    [-1.0, -1.0, 0.0],
15    [1.0, 0.0, 1.0],
16    [-1.0, 0.0, 1.0],
17    [1.0, 0.0, -1.0],
18    [-1.0, 0.0, -1.0],
19    [0.0, 1.0, 1.0],
20    [0.0, -1.0, 1.0],
21    [0.0, 1.0, -1.0],
22    [0.0, -1.0, -1.0],
23    [1.0, 1.0, 0.0],
24    [0.0, -1.0, 1.0],
25    [-1.0, 1.0, 0.0],
26    [0.0, -1.0, -1.0],
27];
28
29/// See `GRADIENT` for details. This is a f32 version of the gradient table for use in f32 noise functions
30pub const GRADIENT_F32: [[f32; 3]; 16] = gradient_f32();
31
32/// Dot product of gradient vector and offset vector.
33#[inline]
34#[must_use]
35pub fn dot(g: &[f64; 3], x: f64, y: f64, z: f64) -> f64 {
36    g[0] * x + g[1] * y + g[2] * z
37}
38/// f32 N-lane gradient dot product.
39///
40/// AVX-512 builds evaluate Minecraft's 16-entry `GRADIENT` table branchlessly
41/// from the hash bits. Baseline builds assemble component vectors from the
42/// table, which avoids expensive mask work on current non-AVX-512 targets.
43#[inline]
44#[must_use]
45pub fn grad_dot_simd<const N: usize>(
46    hashes: [usize; N],
47    x: Simd<f32, N>,
48    y: Simd<f32, N>,
49    z: Simd<f32, N>,
50) -> Simd<f32, N>
51where
52    Simd<f32, N>: Mul<Output = Simd<f32, N>>
53        + Add<Output = Simd<f32, N>>
54        + Sub<Output = Simd<f32, N>>
55        + Neg<Output = Simd<f32, N>>,
56{
57    #[cfg(target_feature = "avx512f")]
58    {
59        let hash_lanes = Simd::<i64, N>::from_array(hashes.map(|value| (value & 15) as i64));
60        let u_component = hash_lanes.simd_lt(Simd::splat(8)).select(x, y);
61        let v_component = hash_lanes.simd_lt(Simd::splat(4)).select(
62            y,
63            (hash_lanes.simd_eq(Simd::splat(12)) | hash_lanes.simd_eq(Simd::splat(14)))
64                .select(x, z),
65        );
66        let signed_u = (hash_lanes & Simd::splat(1))
67            .simd_eq(Simd::splat(0))
68            .select(u_component, -u_component);
69        let signed_v = (hash_lanes & Simd::splat(2))
70            .simd_eq(Simd::splat(0))
71            .select(v_component, -v_component);
72        signed_u + signed_v
73    }
74
75    #[cfg(not(target_feature = "avx512f"))]
76    {
77        let gradients = hashes.map(|hash| GRADIENT_F32[hash & 15]);
78        let gx = Simd::from_array(gradients.map(|gradient| gradient[0]));
79        let gy = Simd::from_array(gradients.map(|gradient| gradient[1]));
80        let gz = Simd::from_array(gradients.map(|gradient| gradient[2]));
81        gx * x + gy * y + gz * z
82    }
83}
84
85/// Calculate the f32 dot product used by vanilla Perlin noise.
86#[expect(clippy::inline_always, reason = "hot-path noise primitive")]
87#[inline(always)]
88#[must_use]
89pub fn grad_dot(hash: usize, x: f32, y: f32, z: f32) -> f32 {
90    let g = &GRADIENT_F32[hash & 15];
91    g[0] * x + g[1] * y + g[2] * z
92}
93
94/// Compute corner noise contribution for a simplex vertex.
95#[inline]
96#[must_use]
97pub fn corner_noise_3d(index: usize, x: f64, y: f64, z: f64, base: f64) -> f64 {
98    let t0 = base - x * x - y * y - z * z;
99    if t0 < 0.0 {
100        0.0
101    } else {
102        let t0 = t0 * t0;
103        t0 * t0 * dot(&GRADIENT[index], x, y, z)
104    }
105}
106
107const fn gradient_f32() -> [[f32; 3]; 16] {
108    let mut result = [[0.0; 3]; 16];
109    let mut i = 0;
110
111    while i < GRADIENT.len() {
112        result[i] = [
113            GRADIENT[i][0] as f32,
114            GRADIENT[i][1] as f32,
115            GRADIENT[i][2] as f32,
116        ];
117        i += 1;
118    }
119
120    result
121}