diff --git a/benches/basic.rs b/benches/basic.rs index 2303b00..34ea382 100644 --- a/benches/basic.rs +++ b/benches/basic.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use bluenoise::BlueNoise; +use bluenoise::{BlueNoise, WrappingBlueNoise}; use rand_pcg::Pcg64Mcg; fn init_time(c: &mut Criterion) { @@ -22,6 +22,10 @@ fn init_time(c: &mut Criterion) { let _x = BlueNoise::::new(1000.0, 1000.0, 1.0); }) }); + + // No need to benchmark the initialization of WrappingBlueNoise, since it's + // just initialized as a newtype around BlueNoise. + group.finish(); } @@ -40,6 +44,18 @@ fn execution_time(c: &mut Criterion) { let x = BlueNoise::::new(1000.0, 1000.0, 1.0); group.bench_function("1000x1000x1.0", |b| b.iter(|| x.clone().count())); + // generating roughly 80 points + let x = WrappingBlueNoise::::new(10.0, 10.0, 1.0); + group.bench_function("wrapping 10x10x1.0", |b| b.iter(|| x.clone().count())); + + // generating roughly 7,500 points + let x = WrappingBlueNoise::::new(100.0, 100.0, 1.0); + group.bench_function("wrapping 100x100x1.0", |b| b.iter(|| x.clone().count())); + + // generating roughly 750,000 points + let x = WrappingBlueNoise::::new(1000.0, 1000.0, 1.0); + group.bench_function("wrapping 1000x1000x1.0", |b| b.iter(|| x.clone().count())); + group.finish(); } diff --git a/src/lib.rs b/src/lib.rs index 62bd172..9c5f551 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,19 @@ //! println!("{}, {}", point.x, point.y); //! } //! ``` +//! +//! ``` +//! use bluenoise::WrappingBlueNoise; +//! use rand::SeedableRng; +//! use rand_pcg::Pcg64Mcg; +//! +//! let mut noise = WrappingBlueNoise::from_rng(50.0, 50.0, 10.0, Pcg64Mcg::seed_from_u64(10)); +//! let noise = noise.with_samples(10); +//! +//! for point in noise.take(10) { +//! println!("{}, {}", point.x, point.y); +//! } +//! ``` #![deny( dead_code, @@ -174,6 +187,11 @@ impl BlueNoise { self } + /// Compute the distance between two points + fn distance(&self, point: Vec2, target: Vec2) -> f32 { + point.distance(target) + } + /// Check if a position is far enough away from /// nearby previously created points. fn is_valid(&self, point: Vec2) -> bool { @@ -186,7 +204,6 @@ impl BlueNoise { let x = (point.x / self.cell_size) as usize; x.saturating_sub(2)..(x + 3).min(self.grid_width) }; - let y_range = { let y = (point.y / self.cell_size) as usize; y.saturating_sub(2)..(y + 3).min(self.grid_height) @@ -199,12 +216,23 @@ impl BlueNoise { .get(y * self.grid_width + x) .expect("Ended up out of bounds when fetching point.") { - Some(target) => (*target - point).length_squared() >= self.radius_squared, + Some(target) => self.distance(point, *target) >= self.radius_squared, None => true, } }) } + /// Get some nearby point + fn get_nearby(&mut self, position: Vec2, seed: f32, sample: u32) -> Vec2 { + let offset = seed + sample as f32 / self.max_samples as f32; + let theta = 2.0 * PI * offset; + let radius = self.radius + 0.001; + Vec2::new( + position.x + radius * theta.cos(), + position.y + radius * theta.sin(), + ) + } + /// Get the index for a given position fn grid_index(&self, position: Vec2) -> usize { let y = self.grid_width * (position.y / self.cell_size) as usize; @@ -223,17 +251,6 @@ impl BlueNoise { self.active_points.push(position); position } - - /// Get some nearby point - fn get_nearby(&mut self, position: Vec2, seed: f32, sample: u32) -> Vec2 { - let offset = seed + sample as f32 / self.max_samples as f32; - let theta = 2.0 * PI * offset; - let radius = self.radius + 0.001; - Vec2::new( - position.x + radius * theta.cos(), - position.y + radius * theta.sin(), - ) - } } impl Iterator for BlueNoise { @@ -266,9 +283,174 @@ impl Iterator for BlueNoise { } } +/// Provides a source of `WrappingBlueNoise` in a given area at some +/// density, where the distance between two points wraps around the +/// edges of the box. This can be used to generate tiling blue noise. +#[derive(Debug, Clone)] +pub struct WrappingBlueNoise(BlueNoise); + +impl WrappingBlueNoise { + /// Creates a new instance of `WrappingBlueNoise`. + /// + /// * `width`: The width of the box to generate inside. + /// * `height`: The height of the box to generate inside. + /// * `min_radius`: The minimum distance between points. + #[must_use = "This is quite expensive to initialise. You can iterate over it to consume it."] + pub fn new(width: f32, height: f32, min_radius: f32) -> Self { + Self(BlueNoise::new(width, height, min_radius)) + } + + /// Creates a new instance of `WrappingBlueNoise`. + /// + /// * `width`: The width of the box to generate inside. + /// * `height`: The height of the box to generate inside. + /// * `min_radius`: The minimum distance between points. + /// * `seed`: Value to seed the rng with + #[must_use = "This is quite expensive to initialise. You can iterate over it to consume it."] + pub fn from_seed(width: f32, height: f32, min_radius: f32, seed: u64) -> Self { + Self(BlueNoise::from_seed(width, height, min_radius, seed)) + } + + /// A builder function to seed the rng with a specific + /// value. + /// + /// For an example, see the `WrappingBlueNoise` examples. + pub fn with_seed(&mut self, seed: u64) -> &mut Self { + self.0.with_seed(seed); + self + } +} + +impl WrappingBlueNoise { + /// Creates a new instance of `WrappingBlueNoise`. + /// + /// * `width`: The width of the box to generate inside. + /// * `height`: The height of the box to generate inside. + /// * `min_radius`: The minimum distance between points. + /// * `rng`: Rng to use + #[must_use = "This is quite expensive to initialise. You can iterate over it to consume it."] + pub fn from_rng(width: f32, height: f32, min_radius: f32, rng: R) -> Self { + Self(BlueNoise::from_rng(width, height, min_radius, rng)) + } + + /// A builder function to set the maximum number of + /// samples to be when attempting to find new points. + /// + /// For an example, see the `WrappingBlueNoise` examples. + pub fn with_samples(&mut self, max_samples: u32) -> &mut Self { + self.0.with_samples(max_samples); + self + } + + /// A builder function to set the minimum radius between + /// points. + /// + /// For an example, see the `WrappingBlueNoise` examples. + pub fn with_min_radius(&mut self, min_radius: f32) -> &mut Self { + self.0.with_min_radius(min_radius); + self + } + + /// Resets the generator to begin creating noise from the beginning. + /// This will not reset the prng so if you want deterministic ordering, + /// make sure to set it explicitly. + /// + /// ``` + /// use bluenoise::WrappingBlueNoise; + /// use rand_pcg::Pcg64Mcg; + /// + /// let mut noise = WrappingBlueNoise::::new(10.0, 10.0, 1.0); + /// let first_10 = noise.with_seed(25).take(10).collect::>(); + /// + /// // make sure to re-initialise your seed! + /// noise.reset().with_seed(25); + /// let reset_10 = noise.take(10).collect::>(); + /// + /// assert_eq!(first_10, reset_10); + /// ``` + pub fn reset(&mut self) -> &mut Self { + self.0.reset(); + self + } + + /// Compute the distance between two points + fn distance(&self, point: Vec2, target: Vec2) -> f32 { + let diff = { + let tmp = (target - point).abs(); + tmp.min(Vec2::new(self.0.width, self.0.height) - tmp) + }; + diff.length_squared() + } + + /// Check if a position is far enough away from + /// nearby previously created points. + fn is_valid(&self, point: Vec2) -> bool { + let x_range = { + let x = (point.x / self.0.cell_size) as isize; + ((x - 2)..(x + 3)).map(|x| x.rem_euclid(self.0.grid_width as isize) as usize) + }; + let y_range = { + let y = (point.y / self.0.cell_size) as isize; + ((y - 2)..(y + 3)).map(|y| y.rem_euclid(self.0.grid_height as isize) as usize) + }; + + x_range.cartesian_product(y_range).all(|(x, y)| { + // if there is a point, check if it is further than our min radius + match self + .0 + .grid + .get(y * self.0.grid_width + x) + .expect("Ended up out of bounds when fetching point.") + { + Some(target) => self.distance(point, *target) >= self.0.radius_squared, + None => true, + } + }) + } + + /// Get some nearby point + fn get_nearby(&mut self, position: Vec2, seed: f32, sample: u32) -> Vec2 { + let nearby = self.0.get_nearby(position, seed, sample); + Vec2::new( + nearby.x.rem_euclid(self.0.width), + nearby.y.rem_euclid(self.0.height), + ) + } +} + +impl Iterator for WrappingBlueNoise { + type Item = Vec2; + + fn next(&mut self) -> Option { + if !self.0.init { + self.0.init = true; + let x = self.0.rng.gen_range(0.0..self.0.width); + let y = self.0.rng.gen_range(0.0..self.0.height); + return Some(self.0.insert_point(Vec2::new(x, y))); + } + + while !self.0.active_points.is_empty() { + let index = self.0.rng.gen::() * (self.0.active_points.len() - 1) as f32; + let parent = self.0.active_points[index as usize]; + + let seed = self.0.rng.gen::(); + for sample in 0..self.0.max_samples { + let point = self.get_nearby(parent, seed, sample); + if self.is_valid(point) { + return Some(self.0.insert_point(point)); + } + } + + self.0.active_points.remove(index as usize); + } + + None + } +} + #[cfg(test)] mod test { - use crate::BlueNoise; + use crate::{BlueNoise, WrappingBlueNoise}; use rand_pcg::Pcg64Mcg; #[test] @@ -276,4 +458,10 @@ mod test { let noise = BlueNoise::::new(100.0, 100.0, 1.0); assert!(noise.count() > 0); } + + #[test] + fn get_points_wrapping() { + let noise = WrappingBlueNoise::::new(100.0, 100.0, 1.0); + assert!(noise.count() > 0); + } }