use noise::NoiseFn;
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<N: NoiseFn<f64, 4>>(
noise: &ToroidalNoise<N>,
width: u32,
height: u32,
) -> 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();
let mut out = Vec::with_capacity(w * h);
for y in 0..h {
let nz = row_cos[y];
let nw = row_sin[y];
for x in 0..w {
out.push(noise.get_precomputed(col_cos[x], col_sin[x], nz, nw));
}
}
out
}
#[inline]
pub fn normalize(v: f64) -> f64 {
v * 0.5 + 0.5
}
#[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}"
);
}
}
}