use alloc::vec::Vec;
use core::f32::consts::FRAC_1_SQRT_2;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use crate::error::ZipError;
use crate::math::floor_f32;
const GRAD2: [[f32; 2]; 8] = [
[1.0, 0.0],
[-1.0, 0.0],
[0.0, 1.0],
[0.0, -1.0],
[FRAC_1_SQRT_2, FRAC_1_SQRT_2],
[-FRAC_1_SQRT_2, FRAC_1_SQRT_2],
[FRAC_1_SQRT_2, -FRAC_1_SQRT_2],
[-FRAC_1_SQRT_2, -FRAC_1_SQRT_2],
];
#[derive(Clone, Copy)]
pub struct PerlinNoise {
perm: [u8; 512],
}
impl core::fmt::Debug for PerlinNoise {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PerlinNoise")
.field("perm", &&self.perm[..8])
.finish_non_exhaustive()
}
}
impl PerlinNoise {
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn new(seed: u64) -> Self {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut perm = [0u8; 512];
for (i, p) in perm.iter_mut().enumerate().take(256) {
*p = i as u8;
}
for i in (1..256).rev() {
let j = rng.gen_range(0..=i);
perm.swap(i, j);
}
for i in 0..256 {
perm[256 + i] = perm[i];
}
Self { perm }
}
#[inline]
fn fade(t: f32) -> f32 {
t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
}
#[inline]
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + t * (b - a)
}
#[inline]
fn grad(hash: usize, x: f32, y: f32) -> f32 {
let g = &GRAD2[hash & 7];
g[0] * x + g[1] * y
}
#[inline]
#[must_use]
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss
)]
pub fn noise2d(&self, x: f32, y: f32) -> f32 {
let xi = floor_f32(x) as i32;
let yi = floor_f32(y) as i32;
let xf = x - xi as f32;
let yf = y - yi as f32;
let xi = (xi & 255) as usize;
let yi = (yi & 255) as usize;
let aa = self.perm[self.perm[xi] as usize + yi] as usize;
let ab = self.perm[self.perm[xi] as usize + yi + 1] as usize;
let ba = self.perm[self.perm[xi + 1] as usize + yi] as usize;
let bb = self.perm[self.perm[xi + 1] as usize + yi + 1] as usize;
let g00 = Self::grad(aa, xf, yf);
let g10 = Self::grad(ba, xf - 1.0, yf);
let g01 = Self::grad(ab, xf, yf - 1.0);
let g11 = Self::grad(bb, xf - 1.0, yf - 1.0);
let u = Self::fade(xf);
let v = Self::fade(yf);
Self::lerp(Self::lerp(g00, g10, u), Self::lerp(g01, g11, u), v)
}
#[inline]
#[must_use]
pub fn fbm(&self, x: f32, y: f32, octaves: u32, persistence: f32, lacunarity: f32) -> f32 {
let mut total = 0.0_f32;
let mut amplitude = 1.0_f32;
let mut frequency = 1.0_f32;
let mut max_value = 0.0_f32;
for _ in 0..octaves {
total += self.noise2d(x * frequency, y * frequency) * amplitude;
max_value += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
if max_value > 0.0 {
total / max_value
} else {
0.0
}
}
}
pub fn generate_perlin_2d(
width: usize,
height: usize,
seed: u64,
scale: f32,
octaves: u32,
) -> Result<Vec<f32>, ZipError> {
generate_perlin_advanced(width, height, seed, scale, octaves, 0.5, 2.0)
}
#[allow(clippy::cast_precision_loss)]
pub fn generate_perlin_advanced(
width: usize,
height: usize,
seed: u64,
scale: f32,
octaves: u32,
persistence: f32,
lacunarity: f32,
) -> Result<Vec<f32>, ZipError> {
if !(scale.is_finite() && scale > 0.0) || octaves == 0 {
return Err(ZipError::InvalidParameter);
}
width
.checked_mul(height)
.ok_or(ZipError::InvalidParameter)?;
let noise = PerlinNoise::new(seed);
let rcp_scale = 1.0 / scale;
let row = move |y: usize| -> Vec<f32> {
let ny = y as f32 * rcp_scale;
(0..width)
.map(|x| {
let nx = x as f32 * rcp_scale;
let v = (noise.fbm(nx, ny, octaves, persistence, lacunarity) + 1.0) * 0.5;
v.clamp(0.0, 1.0)
})
.collect()
};
#[cfg(feature = "parallel")]
let out = (0..height).into_par_iter().flat_map_iter(row).collect();
#[cfg(not(feature = "parallel"))]
let out = (0..height).flat_map(row).collect();
Ok(out)
}
#[allow(clippy::cast_precision_loss)]
pub fn generate_fbm_1d(
n: usize,
seed: u64,
scale: f32,
octaves: u32,
persistence: f32,
lacunarity: f32,
) -> Result<Vec<f32>, ZipError> {
if !(scale.is_finite() && scale > 0.0) || octaves == 0 {
return Err(ZipError::InvalidParameter);
}
if n == 0 {
return Ok(Vec::new());
}
let inv_n = 1.0 / n as f32;
Ok((0..n)
.map(|i| {
let mut sum = 0.0_f32;
let mut amplitude = 1.0_f32;
let mut frequency = scale;
let mut max_amp = 0.0_f32;
let x = i as f32 * inv_n;
for _ in 0..octaves {
sum += amplitude * value_noise_1d(x * frequency, seed);
max_amp += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
if max_amp > 0.0 {
sum / max_amp
} else {
sum
}
})
.collect())
}
#[allow(clippy::cast_possible_truncation)]
fn value_noise_1d(x: f32, seed: u64) -> f32 {
let x0 = floor_f32(x);
let x1 = x0 + 1.0;
let t = x - x0;
let s = t * t * (3.0 - 2.0 * t);
let g0 = hash_to_unit(x0 as i32, seed);
let g1 = hash_to_unit(x1 as i32, seed);
g0 * (1.0 - s) + g1 * s
}
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
fn hash_to_unit(x: i32, seed: u64) -> f32 {
let mut h = (x as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15);
h ^= seed.wrapping_mul(0x85eb_ca6b_c2b2_ae35);
h ^= h >> 33;
h = h.wrapping_mul(0xc2b2_ae35_9e37_79b9);
h ^= h >> 33;
let f = (h & 0x00ff_ffff) as f32 / 8_388_608.0; f - 1.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn perlin_deterministic_and_seed_sensitive() {
let a = generate_perlin_2d(64, 64, 42, 10.0, 4).unwrap();
let b = generate_perlin_2d(64, 64, 42, 10.0, 4).unwrap();
let c = generate_perlin_2d(64, 64, 43, 10.0, 4).unwrap();
assert_eq!(a, b);
assert_ne!(a, c);
assert_eq!(a.len(), 64 * 64);
}
#[test]
fn perlin_range() {
let noise = generate_perlin_2d(128, 128, 123, 20.0, 4).unwrap();
for &v in &noise {
assert!((0.0..=1.0).contains(&v), "Value {v} out of range [0, 1]");
}
}
#[test]
fn perlin_rejects_invalid_parameters() {
assert_eq!(
generate_perlin_2d(4, 4, 1, 0.0, 4),
Err(ZipError::InvalidParameter)
);
assert_eq!(
generate_perlin_2d(4, 4, 1, f32::NAN, 4),
Err(ZipError::InvalidParameter)
);
assert_eq!(
generate_perlin_2d(4, 4, 1, 10.0, 0),
Err(ZipError::InvalidParameter)
);
assert_eq!(
generate_perlin_2d(usize::MAX, 2, 1, 10.0, 1),
Err(ZipError::InvalidParameter)
);
assert!(generate_perlin_2d(0, 5, 1, 10.0, 1).unwrap().is_empty());
}
#[test]
fn noise_is_zero_on_lattice() {
let p = PerlinNoise::new(7);
for x in -3..4 {
for y in -3..4 {
#[allow(clippy::cast_precision_loss)]
let v = p.noise2d(x as f32, y as f32);
assert_eq!(v, 0.0, "lattice ({x}, {y})");
}
}
assert_eq!(p.fbm(1.0, 2.0, 0, 0.5, 2.0), 0.0);
}
#[test]
fn fbm_1d_contract() {
let a = generate_fbm_1d(256, 9, 4.0, 3, 0.5, 2.0).unwrap();
let b = generate_fbm_1d(256, 9, 4.0, 3, 0.5, 2.0).unwrap();
assert_eq!(a, b);
assert_eq!(a.len(), 256);
for &v in &a {
assert!((-1.0..=1.0).contains(&v));
}
assert!(generate_fbm_1d(0, 1, 1.0, 1, 0.5, 2.0).unwrap().is_empty());
assert_eq!(
generate_fbm_1d(8, 1, 0.0, 1, 0.5, 2.0),
Err(ZipError::InvalidParameter)
);
assert_eq!(
generate_fbm_1d(8, 1, 1.0, 0, 0.5, 2.0),
Err(ZipError::InvalidParameter)
);
}
}