use noise::NoiseFn;
use rayon::prelude::*;
use std::f64::consts::TAU;
pub struct ToroidalNoise<N> {
noise: N,
pub frequency: f64,
}
impl<N: NoiseFn<f64, 4>> ToroidalNoise<N> {
pub fn new(noise: N, frequency: f64) -> Self {
Self { noise, frequency }
}
pub fn get(&self, u: f64, v: f64) -> f64 {
let nx = (TAU * u).cos() * self.frequency;
let ny = (TAU * u).sin() * self.frequency;
let nz = (TAU * v).cos() * self.frequency;
let nw = (TAU * v).sin() * self.frequency;
self.noise.get([nx, ny, nz, nw])
}
pub fn get_offset(&self, u: f64, v: f64, du: f64, dv: f64) -> f64 {
self.get(u + du, v + dv)
}
#[inline]
pub fn get_precomputed(&self, nx: f64, ny: f64, nz: f64, nw: f64) -> f64 {
self.noise.get([nx, ny, nz, nw])
}
}
pub fn sample_grid_into<N: NoiseFn<f64, 4> + Sync>(
noise: &ToroidalNoise<N>,
width: u32,
height: u32,
out: &mut Vec<f64>,
) {
let w = width as usize;
let h = height as usize;
let freq = noise.frequency;
let col_cos: Vec<f64> = (0..w)
.map(|x| (TAU * x as f64 / w as f64).cos() * freq)
.collect();
let col_sin: Vec<f64> = (0..w)
.map(|x| (TAU * x as f64 / w as f64).sin() * freq)
.collect();
let row_cos: Vec<f64> = (0..h)
.map(|y| (TAU * y as f64 / h as f64).cos() * freq)
.collect();
let row_sin: Vec<f64> = (0..h)
.map(|y| (TAU * y as f64 / h as f64).sin() * freq)
.collect();
out.clear();
out.resize(w * h, 0.0);
out.par_chunks_mut(w).enumerate().for_each(|(y, row)| {
let nz = row_cos[y];
let nw = row_sin[y];
for (x, slot) in row.iter_mut().enumerate() {
*slot = noise.get_precomputed(col_cos[x], col_sin[x], nz, nw);
}
});
}
pub fn sample_grid<N: NoiseFn<f64, 4> + Sync>(
noise: &ToroidalNoise<N>,
width: u32,
height: u32,
) -> Vec<f64> {
let mut out = Vec::new();
sample_grid_into(noise, width, height, &mut out);
out
}
#[inline]
pub fn normalize(v: f64) -> f64 {
v * 0.5 + 0.5
}
#[inline]
pub fn bilinear_sample_torus(grid: &[f64], w: usize, h: usize, u: f64, v: f64) -> f64 {
let u = u.rem_euclid(1.0);
let v = v.rem_euclid(1.0);
let px = u * w as f64;
let py = v * h as f64;
let x0 = px as usize % w;
let y0 = py as usize % h;
let x1 = (x0 + 1) % w;
let y1 = (y0 + 1) % h;
let fx = px.fract();
let fy = py.fract();
let v00 = grid[y0 * w + x0];
let v10 = grid[y0 * w + x1];
let v01 = grid[y1 * w + x0];
let v11 = grid[y1 * w + x1];
v00 * (1.0 - fx) * (1.0 - fy) + v10 * fx * (1.0 - fy) + v01 * (1.0 - fx) * fy + v11 * fx * fy
}
pub(crate) fn toroidal_voronoi(u: f64, v: f64, scale: f64, seed: u32) -> (f64, f64, i64, i64) {
let n = scale.round().max(1.0) as i64;
let su = u * scale;
let sv = v * scale;
let gi = su.floor() as i64;
let gj = sv.floor() as i64;
let mut f1 = f64::MAX;
let mut f2 = f64::MAX;
let mut best_i = gi;
let mut best_j = gj;
for di in -2i64..=2 {
for dj in -2i64..=2 {
let ni = (gi + di).rem_euclid(n);
let nj = (gj + dj).rem_euclid(n);
let jx = 0.15 + 0.70 * cell_hash(ni, nj, seed);
let jy = 0.15 + 0.70 * cell_hash(nj, ni, seed.wrapping_add(17));
let cx = (ni as f64 + jx) / scale;
let cy = (nj as f64 + jy) / scale;
let mut dx = (u - cx).abs();
let mut dy = (v - cy).abs();
if dx > 0.5 {
dx = 1.0 - dx;
}
if dy > 0.5 {
dy = 1.0 - dy;
}
let d = (dx * dx + dy * dy).sqrt();
if d < f1 {
f2 = f1;
f1 = d;
best_i = ni;
best_j = nj;
} else if d < f2 {
f2 = d;
}
}
}
(f1, f2, best_i, best_j)
}
pub(crate) fn cell_hash(bx: i64, by: i64, seed: u32) -> f64 {
let mut h = seed as u64;
h ^= (bx as u64).wrapping_mul(6_364_136_223_846_793_005);
h ^= (by as u64).wrapping_mul(1_442_695_040_888_963_407);
h ^= h >> 33;
h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
h ^= h >> 33;
(h as f64) * (1.0 / u64::MAX as f64)
}
#[cfg(test)]
mod tests {
use super::*;
use noise::Perlin;
#[test]
fn samples_vary_with_frequency() {
let noise = ToroidalNoise::new(Perlin::new(1), 4.0);
let samples = sample_grid(&noise, 64, 64);
let mean = samples.iter().sum::<f64>() / samples.len() as f64;
let variance =
samples.iter().map(|&s| (s - mean).powi(2)).sum::<f64>() / samples.len() as f64;
let stddev = variance.sqrt();
assert!(
stddev > 0.1,
"noise has almost no variation (stddev={stddev:.4}); torus radius is likely wrong"
);
}
#[test]
fn tiles_seamlessly() {
let noise = ToroidalNoise::new(Perlin::new(42), 3.0);
for v in [0.0, 0.25, 0.5, 0.75] {
let at_0 = noise.get(0.0, v);
let at_1 = noise.get(1.0, v);
assert!(
(at_0 - at_1).abs() < 1e-10,
"horizontal seam at v={v}: {at_0} != {at_1}"
);
}
for u in [0.0, 0.25, 0.5, 0.75] {
let at_0 = noise.get(u, 0.0);
let at_1 = noise.get(u, 1.0);
assert!(
(at_0 - at_1).abs() < 1e-10,
"vertical seam at u={u}: {at_0} != {at_1}"
);
}
}
}