use noise::{NoiseFn, Perlin, Simplex, Value as ValueNoise, Worley};
use xxhash_rust::xxh3::Xxh3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum NoiseKind {
White,
Value,
Perlin,
Simplex,
Worley,
}
impl NoiseKind {
pub(super) fn tag(self) -> u8 {
match self {
NoiseKind::White => 0,
NoiseKind::Value => 1,
NoiseKind::Perlin => 2,
NoiseKind::Simplex => 3,
NoiseKind::Worley => 4,
}
}
pub(super) fn parse(s: &str) -> Option<NoiseKind> {
Some(match s {
"white" => NoiseKind::White,
"value" => NoiseKind::Value,
"perlin" => NoiseKind::Perlin,
"simplex" => NoiseKind::Simplex,
"worley" => NoiseKind::Worley,
_ => return None,
})
}
}
pub(super) enum Sampler {
White(u32),
Value(ValueNoise),
Perlin(Perlin),
Simplex(Simplex),
Worley(Worley),
}
impl Sampler {
pub(super) fn build(kind: NoiseKind, seed: u32) -> Self {
match kind {
NoiseKind::White => Sampler::White(seed),
NoiseKind::Value => Sampler::Value(ValueNoise::new(seed)),
NoiseKind::Perlin => Sampler::Perlin(Perlin::new(seed)),
NoiseKind::Simplex => Sampler::Simplex(Simplex::new(seed)),
NoiseKind::Worley => Sampler::Worley(Worley::new(seed)),
}
}
pub(super) fn sample(&self, x: f64, y: f64) -> f64 {
match self {
Sampler::White(seed) => white_hash(x, y, *seed) * 2.0 - 1.0,
Sampler::Value(n) => n.get([fold(x), fold(y)]).clamp(-1.0, 1.0),
Sampler::Perlin(n) => n.get([fold(x), fold(y)]).clamp(-1.0, 1.0),
Sampler::Simplex(n) => n.get([fold(x), fold(y)]).clamp(-1.0, 1.0),
Sampler::Worley(n) => 1.0 - 2.0 * n.get([fold(x), fold(y)]).clamp(0.0, 1.0),
}
}
}
const LATTICE_PERIOD: f64 = 256.0;
const FOLD_ABOVE: f64 = (1u64 << 28) as f64;
fn fold(v: f64) -> f64 {
if v.abs() < FOLD_ABOVE {
v
} else if v.is_finite() {
v.rem_euclid(LATTICE_PERIOD)
} else {
0.0
}
}
pub(super) fn white_hash(x: f64, y: f64, seed: u32) -> f64 {
let mut h = Xxh3::new();
h.update(&seed.to_le_bytes());
h.update(&x.to_bits().to_le_bytes());
h.update(&y.to_bits().to_le_bytes());
let v = h.digest();
((v >> 11) as f64) * (1.0 / ((1u64 << 53) as f64))
}
pub(super) fn fbm(
sampler: &Sampler,
x: f64,
y: f64,
octaves: u32,
lacunarity: f64,
gain: f64,
) -> f64 {
if octaves <= 1 {
return sampler.sample(x, y);
}
let mut sum = 0.0;
let mut amp = 1.0;
let mut freq = 1.0;
let mut norm = 0.0;
for _ in 0..octaves {
sum += sampler.sample(x * freq, y * freq) * amp;
norm += amp;
amp *= gain;
freq *= lacunarity;
}
if norm > 0.0 {
sum / norm
} else {
0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lattice_kinds_repeat_every_256_units() {
for kind in [NoiseKind::Value, NoiseKind::Perlin, NoiseKind::Worley] {
let s = Sampler::build(kind, 7);
for (x, y) in [(0.25, 0.75), (13.5, -4.25), (-100.125, 60.0)] {
assert_eq!(
s.sample(x, y),
s.sample(x + LATTICE_PERIOD * 3.0, y - LATTICE_PERIOD * 5.0),
"{kind:?} at ({x}, {y})",
);
}
}
}
#[test]
fn coordinates_below_the_bound_are_untouched() {
for v in [0.0, -1.5, 1e6, FOLD_ABOVE - 1.0, -(FOLD_ABOVE - 1.0)] {
assert_eq!(fold(v), v);
}
}
#[test]
fn folded_coordinates_fit_a_32_bit_isize() {
let ceiling = i32::MAX as f64 / 2.0;
for v in [
FOLD_ABOVE,
-FOLD_ABOVE,
2.4e9,
-2.4e9,
f64::MAX,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NAN,
] {
let folded = fold(v);
assert!(
folded.is_finite() && folded.abs() < ceiling,
"fold({v}) = {folded}",
);
}
}
#[test]
fn deep_world_coordinates_sample_instead_of_aborting() {
for kind in [
NoiseKind::White,
NoiseKind::Value,
NoiseKind::Perlin,
NoiseKind::Simplex,
NoiseKind::Worley,
] {
let s = Sampler::build(kind, 1);
let v = fbm(&s, 2.4e9, -2.4e9, 4, 2.1, 0.5);
assert!(v.is_finite() && (-1.0..=1.0).contains(&v), "{kind:?}: {v}");
}
}
#[test]
fn folding_preserves_the_field_for_axis_aligned_lattices() {
for kind in [NoiseKind::Value, NoiseKind::Perlin, NoiseKind::Worley] {
let s = Sampler::build(kind, 3);
let far = FOLD_ABOVE * 16.0;
assert_eq!(
s.sample(far + 0.5, far + 0.25),
s.sample(0.5, 0.25),
"{kind:?}"
);
}
}
}