Skip to content

Commit

Permalink
Add WrappingBlueNoise to generate tiling blue noise
Browse files Browse the repository at this point in the history
  • Loading branch information
benfrankel authored and arlyon committed Jan 7, 2021
1 parent 77903a0 commit 1050242
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 15 deletions.
18 changes: 17 additions & 1 deletion benches/basic.rs
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -22,6 +22,10 @@ fn init_time(c: &mut Criterion) {
let _x = BlueNoise::<Pcg64Mcg>::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();
}

Expand All @@ -40,6 +44,18 @@ fn execution_time(c: &mut Criterion) {
let x = BlueNoise::<Pcg64Mcg>::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::<Pcg64Mcg>::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::<Pcg64Mcg>::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::<Pcg64Mcg>::new(1000.0, 1000.0, 1.0);
group.bench_function("wrapping 1000x1000x1.0", |b| b.iter(|| x.clone().count()));

group.finish();
}

Expand Down
216 changes: 202 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -174,6 +187,11 @@ impl<R: Rng> BlueNoise<R> {
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 {
Expand All @@ -186,7 +204,6 @@ impl<R: Rng> BlueNoise<R> {
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)
Expand All @@ -199,12 +216,23 @@ impl<R: Rng> BlueNoise<R> {
.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;
Expand All @@ -223,17 +251,6 @@ impl<R: Rng> BlueNoise<R> {
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<R: Rng> Iterator for BlueNoise<R> {
Expand Down Expand Up @@ -266,14 +283,185 @@ impl<R: Rng> Iterator for BlueNoise<R> {
}
}

/// 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<R: Rng>(BlueNoise<R>);

impl<R: Rng + SeedableRng> WrappingBlueNoise<R> {
/// 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<R: Rng> WrappingBlueNoise<R> {
/// 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::<Pcg64Mcg>::new(10.0, 10.0, 1.0);
/// let first_10 = noise.with_seed(25).take(10).collect::<Vec<_>>();
///
/// // make sure to re-initialise your seed!
/// noise.reset().with_seed(25);
/// let reset_10 = noise.take(10).collect::<Vec<_>>();
///
/// 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<R: Rng> Iterator for WrappingBlueNoise<R> {
type Item = Vec2;

fn next(&mut self) -> Option<Self::Item> {
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::<f32>() * (self.0.active_points.len() - 1) as f32;
let parent = self.0.active_points[index as usize];

let seed = self.0.rng.gen::<f32>();
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]
fn get_points() {
let noise = BlueNoise::<Pcg64Mcg>::new(100.0, 100.0, 1.0);
assert!(noise.count() > 0);
}

#[test]
fn get_points_wrapping() {
let noise = WrappingBlueNoise::<Pcg64Mcg>::new(100.0, 100.0, 1.0);
assert!(noise.count() > 0);
}
}

0 comments on commit 1050242

Please sign in to comment.