use crate::Real;
#[inline]
const fn hash3(ix: i32, iy: i32, iz: i32, seed: u32) -> u32 {
let mut h = seed;
h = h.wrapping_add((ix as u32).wrapping_mul(0x9E37_79B1));
h = h.wrapping_add((iy as u32).wrapping_mul(0x85EB_CA6B));
h = h.wrapping_add((iz as u32).wrapping_mul(0xC2B2_AE35));
h ^= h >> 16;
h = h.wrapping_mul(0x85EB_CA6B);
h ^= h >> 13;
h = h.wrapping_mul(0xC2B2_AE35);
h ^= h >> 16;
h
}
const GRAD12: [[i8; 3]; 12] = [
[1, 1, 0],
[-1, 1, 0],
[1, -1, 0],
[-1, -1, 0],
[1, 0, 1],
[-1, 0, 1],
[1, 0, -1],
[-1, 0, -1],
[0, 1, 1],
[0, -1, 1],
[0, 1, -1],
[0, -1, -1],
];
const LATTICE_LIMIT: f64 = 16_777_216.0;
#[inline]
fn lattice_index<R: Real>(floored: R) -> i32 {
debug_assert!(
floored.abs() < R::from_f64(LATTICE_LIMIT),
"noise coordinate beyond the exactly-representable lattice range"
);
floored.as_f32() as i32
}
#[inline]
fn fade<R: Real>(t: R) -> (R, R) {
let six = R::from_f64(6.0);
let fifteen = R::from_f64(15.0);
let ten = R::from_f64(10.0);
let thirty = R::from_f64(30.0);
let t2 = t * t;
let t3 = t2 * t;
let u = t3 * (t * (t * six - fifteen) + ten);
let d = t - R::ONE;
(u, thirty * t2 * d * d)
}
#[inline]
pub(crate) fn perlin<R: Real>(p: [R; 3], seed: u32) -> (R, [R; 3]) {
let base = [p[0].floor(), p[1].floor(), p[2].floor()];
let i = [
lattice_index(base[0]),
lattice_index(base[1]),
lattice_index(base[2]),
];
let t = [p[0] - base[0], p[1] - base[1], p[2] - base[2]];
let (ux, dux) = fade(t[0]);
let (uy, duy) = fade(t[1]);
let (uz, duz) = fade(t[2]);
let mut value = R::ZERO;
let mut grad = [R::ZERO; 3];
for cz in 0..2i32 {
for cy in 0..2i32 {
for cx in 0..2i32 {
let g = GRAD12[(hash3(i[0] + cx, i[1] + cy, i[2] + cz, seed) % 12) as usize];
let gv = [
R::from_f64(f64::from(g[0])),
R::from_f64(f64::from(g[1])),
R::from_f64(f64::from(g[2])),
];
let d = [
t[0] - R::from_f64(f64::from(cx)),
t[1] - R::from_f64(f64::from(cy)),
t[2] - R::from_f64(f64::from(cz)),
];
let dot = gv[0] * d[0] + gv[1] * d[1] + gv[2] * d[2];
let (wx, dwx) = if cx == 1 {
(ux, dux)
} else {
(R::ONE - ux, -dux)
};
let (wy, dwy) = if cy == 1 {
(uy, duy)
} else {
(R::ONE - uy, -duy)
};
let (wz, dwz) = if cz == 1 {
(uz, duz)
} else {
(R::ONE - uz, -duz)
};
let w = wx * wy * wz;
value += w * dot;
grad[0] += dwx * wy * wz * dot + w * gv[0];
grad[1] += wx * dwy * wz * dot + w * gv[1];
grad[2] += wx * wy * dwz * dot + w * gv[2];
}
}
}
(value, grad)
}
pub(crate) const PERLIN_BOUND: f64 = 2.0;
const OCTAVE_OFFSET: [f64; 3] = [
0.618_033_988_749_894_8, 0.381_966_011_250_105_15, 0.236_067_977_499_789_7, ];
#[inline]
pub(crate) fn fbm<R: Real>(
p: [R; 3],
seed: u32,
octaves: u32,
lacunarity: R,
gain: R,
frequency: R,
) -> (R, [R; 3]) {
let mut value = R::ZERO;
let mut grad = [R::ZERO; 3];
let mut freq = frequency;
let mut amp = R::ONE;
for octave in 0..octaves {
let k = R::from_f64(f64::from(octave));
let q = [
p[0] * freq + k * R::from_f64(OCTAVE_OFFSET[0]),
p[1] * freq + k * R::from_f64(OCTAVE_OFFSET[1]),
p[2] * freq + k * R::from_f64(OCTAVE_OFFSET[2]),
];
let (v, g) = perlin(q, seed);
value += amp * v;
let chain = amp * freq;
grad[0] += chain * g[0];
grad[1] += chain * g[1];
grad[2] += chain * g[2];
freq *= lacunarity;
amp *= gain;
}
(value, grad)
}
#[inline]
pub(crate) fn fbm_bound<R: Real>(octaves: u32, gain: R) -> R {
let mut total = R::ZERO;
let mut amp = R::from_f64(PERLIN_BOUND);
for _ in 0..octaves {
total += amp;
amp *= gain;
}
total
}
#[cfg(test)]
mod tests {
#![allow(clippy::float_cmp)]
use super::*;
use alloc::vec::Vec;
#[test]
fn hash3_matches_committed_values() {
let cases = [
(0, 0, 0, 0u32),
(1, 0, 0, 0u32),
(0, 1, 0, 0u32),
(0, 0, 1, 0u32),
(-1, -1, -1, 0u32),
(-7, 13, -29, 0x5EED_1234u32),
(i32::MIN, i32::MAX, 0, 0x5EED_1234u32),
];
let got: Vec<u32> = cases
.iter()
.map(|&(x, y, z, s)| hash3(x, y, z, s))
.collect();
assert_eq!(
got,
[
0x0000_0000,
0x11FD_02EB,
0xCB72_770F,
0x0C66_C024,
0x6AB1_7FCE,
0x738F_6E3F,
0x43BC_BA29,
]
);
}
#[test]
fn perlin_is_zero_at_lattice_points() {
for z in -3..=3 {
for y in -3..=3 {
for x in -3..=3 {
let p = [f64::from(x), f64::from(y), f64::from(z)];
let (v, _) = perlin(p, 0x5EED_1234);
assert!(v.abs() < 1e-15, "perlin{p:?} = {v}");
}
}
}
}
#[test]
fn perlin_gradient_matches_central_difference() {
let h = 1e-6f64;
for i in 0..40 {
let f = f64::from(i);
let p = [0.137 + f * 0.313, -0.241 + f * 0.517, 0.359 - f * 0.211_f64];
let (_, analytic) = perlin(p, 0x5EED_1234);
for axis in 0..3 {
let mut lo = p;
let mut hi = p;
lo[axis] -= h;
hi[axis] += h;
let numeric = (perlin(hi, 0x5EED_1234).0 - perlin(lo, 0x5EED_1234).0) / (2.0 * h);
assert!(
(analytic[axis] - numeric).abs() < 1e-6,
"axis {axis} at {p:?}: analytic {} vs numeric {numeric}",
analytic[axis]
);
}
}
}
#[test]
fn noise_stays_within_the_provable_bound() {
let mut observed = 0.0f64;
for i in 0..60 {
for j in 0..60 {
for k in 0..12 {
let p = [
-6.0 + f64::from(i) * 0.211,
-6.0 + f64::from(j) * 0.211,
-1.0 + f64::from(k) * 0.211,
];
let (v, _) = perlin(p, 0x5EED_1234);
observed = observed.max(v.abs());
}
}
}
assert!(observed <= PERLIN_BOUND, "observed {observed}");
std::println!("measured: max |perlin| over 43200 samples = {observed:.6}");
}
#[test]
fn single_octave_fbm_reduces_to_one_noise_call() {
let p = [0.31, -0.72, 0.44f64];
let (v, g) = fbm(p, 0x5EED_1234, 1, 2.0, 1.0, 1.0);
let (v0, g0) = perlin(p, 0x5EED_1234);
assert_eq!(v, v0);
assert_eq!(g, g0);
}
#[test]
fn fbm_gradient_matches_central_difference() {
let h = 1e-6f64;
for i in 0..20 {
let f = f64::from(i);
let p = [0.137 + f * 0.41, 0.0, 0.359 - f * 0.23f64];
let (_, analytic) = fbm(p, 0x5EED_1234, 4, 2.0, 0.5, 0.25);
for axis in [0usize, 2] {
let mut lo = p;
let mut hi = p;
lo[axis] -= h;
hi[axis] += h;
let numeric = (fbm(hi, 0x5EED_1234, 4, 2.0, 0.5, 0.25).0
- fbm(lo, 0x5EED_1234, 4, 2.0, 0.5, 0.25).0)
/ (2.0 * h);
assert!(
(analytic[axis] - numeric).abs() < 1e-6,
"axis {axis} at {p:?}: {} vs {numeric}",
analytic[axis]
);
}
}
}
#[test]
fn fbm_bound_is_the_geometric_sum() {
assert_eq!(fbm_bound::<f64>(4, 0.5), 3.75);
}
}