From d8a33c7bfae2e0068487d0cda7845748daff8818 Mon Sep 17 00:00:00 2001 From: Nathan Vegdahl Date: Sat, 13 May 2017 03:23:55 -0700 Subject: [PATCH] Added a blue noise mask generator as a sub-crate. --- Cargo.lock | 5 + Cargo.toml | 4 + src/main.rs | 2 +- sub_crates/blue_noise_mask/Cargo.toml | 10 ++ sub_crates/blue_noise_mask/build.rs | 233 ++++++++++++++++++++++++++ sub_crates/blue_noise_mask/src/lib.rs | 4 + 6 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 sub_crates/blue_noise_mask/Cargo.toml create mode 100644 sub_crates/blue_noise_mask/build.rs create mode 100644 sub_crates/blue_noise_mask/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 5c113ef..bf5fd4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,6 +22,10 @@ name = "bitflags" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "blue_noise_mask" +version = "0.1.0" + [[package]] name = "c_vec" version = "1.0.12" @@ -153,6 +157,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "psychopath" version = "0.1.0" dependencies = [ + "blue_noise_mask 0.1.0", "clap 2.23.2 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", "float4 0.1.0", diff --git a/Cargo.toml b/Cargo.toml index eb4233f..c8847ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "sub_crates/blue_noise_mask", "sub_crates/float4", "sub_crates/halton", "sub_crates/math3d", @@ -36,6 +37,9 @@ lazy_static = "0.2" openexr = { git = "https://github.com/cessen/openexr-rs", rev = "612fc6c81c031970ffddcab15509236711613de8" } # Local crate dependencies +[dependencies.blue_noise_mask] +path = "sub_crates/blue_noise_mask" + [dependencies.float4] path = "sub_crates/float4" diff --git a/src/main.rs b/src/main.rs index af81b6b..baaefcd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ - +extern crate blue_noise_mask; extern crate float4; extern crate halton; extern crate math3d; diff --git a/sub_crates/blue_noise_mask/Cargo.toml b/sub_crates/blue_noise_mask/Cargo.toml new file mode 100644 index 0000000..555623e --- /dev/null +++ b/sub_crates/blue_noise_mask/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "blue_noise_mask" +version = "0.1.0" +authors = ["Nathan Vegdahl "] +license = "MIT" +build = "build.rs" + +[lib] +name = "blue_noise_mask" +path = "src/lib.rs" diff --git a/sub_crates/blue_noise_mask/build.rs b/sub_crates/blue_noise_mask/build.rs new file mode 100644 index 0000000..a61776c --- /dev/null +++ b/sub_crates/blue_noise_mask/build.rs @@ -0,0 +1,233 @@ +// Generate Blue Noise Mask tables. + +use std::cmp::Ordering; +use std::env; +use std::fs::File; +use std::io::Write; +use std::ops::{Index, IndexMut}; +use std::path::Path; + + +const WINDOW_RADIUS: isize = 6; +const FILTER_WIDTH: f32 = 1.0; + +// These are specified in powers of two (2^N) for fast wrapping +// in the generated Rust code. +const NUM_MASKS_POW: usize = 7; // 128 +const MASK_SIZE_POW: usize = 7; // 128 + +fn main() { + let out_dir = env::var("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("blue_noise_masks.rs"); + let mut f = File::create(&dest_path).unwrap(); + + // Generate masks + let masks = (0..(1 << NUM_MASKS_POW) as u32).map(|i| blue_noise_mask(1 << MASK_SIZE_POW, i)).collect::>(); + + // Write the beginning bits of the file + f.write_all(format!(r#" +// This file is automatically generated. + +// Compute points of the Halton sequence with with Faure-permutations for different bases. + +pub const NUM_MASKS: u32 = {}; +pub const MASK_SIZE: u32 = {}; +const NUM_MASKS_WRAP_BITMASK: u32 = {}; +const MASK_SIZE_WRAP_BITMASK: u32 = {}; +const MASK_POINTS: u32 = {}; + +pub fn get_point(mask_i: u32, x: u32, y: u32) -> f32 {{ + let mask_i = mask_i & NUM_MASKS_WRAP_BITMASK; + let x = x & MASK_SIZE_WRAP_BITMASK; + let y = y & MASK_SIZE_WRAP_BITMASK; + + unsafe {{ *MASKS.get_unchecked(((mask_i * MASK_POINTS) + (y * MASK_SIZE) + x) as usize) }} +}} +"#, + 1 << NUM_MASKS_POW, + 1 << MASK_SIZE_POW, + (1 << NUM_MASKS_POW) - 1, + (1 << MASK_SIZE_POW) - 1, + (1 << MASK_SIZE_POW) * (1 << MASK_SIZE_POW), + ).as_bytes()).unwrap(); + + // Write the mask data + f.write_all(format!(r#" +const MASKS: [f32; {}] = [ + "#, (1 << MASK_SIZE_POW) * (1 << MASK_SIZE_POW) * (1 << NUM_MASKS_POW)).as_bytes()).unwrap(); + + for mask in masks.iter() { + for v in mask.data.iter() { + f.write_all(format!(r#" {:.8}, +"#, v).as_bytes()).unwrap(); + } + } + + f.write_all(format!(r#" +]; +"#).as_bytes()).unwrap(); +} + + + +/// Creates a blue noise mask +fn blue_noise_mask(tile_size: usize, seed: u32) -> Image { + let mut image = Image::new(tile_size, tile_size); + + for (i, v) in image.data.iter_mut().enumerate() { + *v = hash_u32_to_f32(i as u32, seed); + } + + // High pass and remap + for _ in 0..2 { + high_pass_filter(&mut image, WINDOW_RADIUS, FILTER_WIDTH); + remap_values(&mut image); + } + + image +} + +/// High pass filter for an Image +fn high_pass_filter(image: &mut Image, window_radius: isize, filter_width: f32) { + // Precompute filter convolution matrix + let conv = { + let mut conv = Image::new(window_radius as usize * 2 + 1, window_radius as usize * 2 + 1); + for j in (-window_radius)..window_radius { + for i in (-window_radius)..window_radius { + let n = (((j*j) + (i*i)) as f32).sqrt(); + //let n = (j.abs() + i.abs()) as f32; + conv.set_wrapped(gauss(n, filter_width), i,j); + } + } + + let inv_total = 1.0f32 / conv.data.iter().sum::(); + for v in conv.data.iter_mut() { + *v *= inv_total; + } + + conv + }; + + // Compute the low-pass image + let mut low_pass_img = Image::new(image.width, image.height); + for y in 0..image.height { + for x in 0..image.width { + for j in (-window_radius as isize)..window_radius { + for i in (-window_radius as isize)..window_radius { + let b = y as isize + j; + let a = x as isize + i; + let alpha = conv.get_wrapped(i, j); + low_pass_img[(x as isize, y as isize)] += image.get_wrapped(a, b) * alpha; + } + } + } + } + + // Subtract low pass from original + for i in 0..image.data.len() { + image.data[i] -= low_pass_img.data[i] - 0.5; + } +} + +/// Remaps the values in an Image to be linearly distributed within [0, 1] +fn remap_values(image: &mut Image) { + let mut vals = Vec::with_capacity(image.width * image.height); + for y in 0..image.height { + for x in 0..image.width { + vals.push((image[(x as isize, y as isize)], x, y)); + } + } + vals.sort_by(|a, b| { + if a < b { + Ordering::Less + } else { + Ordering::Greater + } + }); + let inc = 1.0 / (image.data.len() - 1) as f32; + let mut nor_v = 0.0; + for v in vals.iter() { + image[(v.1 as isize, v.2 as isize)] = nor_v; + nor_v += inc; + } +} + +/// Normal distribution +fn gauss(x: f32, sd: f32) -> f32 { + let norm = 1.0 / (2.0 * std::f32::consts::PI * sd * sd).sqrt(); + let dist = ((-x * x) / (2.0 * sd * sd)).exp(); + norm * dist +} + +/// Returns a random float in [0, 1] based on 'n' and a seed. +/// Generally use n for getting a bunch of different random +/// numbers, and use seed to vary between runs. +pub fn hash_u32_to_f32(n: u32, seed: u32) -> f32 { + let mut hash = n; + + for _ in 0..8 { + hash = hash.wrapping_mul(1936502639); + hash ^= hash.wrapping_shr(16); + hash = hash.wrapping_add(seed); + } + + const INV_MAX: f32 = 1.0 / std::u32::MAX as f32; + return hash as f32 * INV_MAX; +} + +struct Image { + data: Vec, + width: usize, + height: usize, +} + +impl Image { + fn new(width: usize, height: usize) -> Image { + Image { + data: vec![0.0; width * height], + width: width, + height: height, + } + } + + fn get_wrapped(&self, mut ix: isize, mut iy: isize) -> f32 { + while ix < 0 { + ix += self.width as isize; + } + while iy < 1 { + iy += self.height as isize; + } + + let x = ix as usize % self.width; + let y = iy as usize % self.height; + + self.data[y * self.width + x] + } + + fn set_wrapped(&mut self, v: f32, mut ix: isize, mut iy: isize){ + while ix < 0 { + ix += self.width as isize; + } + while iy < 1 { + iy += self.height as isize; + } + + let x = ix as usize % self.width; + let y = iy as usize % self.height; + + self.data[y * self.width + x] = v + } +} + +impl Index<(isize, isize)> for Image { + type Output = f32; + fn index(&self, index: (isize, isize)) -> &f32 { + &self.data[index.1 as usize * self.width + index.0 as usize] + } +} + +impl IndexMut<(isize, isize)> for Image { + fn index_mut(&mut self, index: (isize, isize)) -> &mut f32 { + &mut self.data[index.1 as usize * self.width + index.0 as usize] + } +} \ No newline at end of file diff --git a/sub_crates/blue_noise_mask/src/lib.rs b/sub_crates/blue_noise_mask/src/lib.rs new file mode 100644 index 0000000..d75a756 --- /dev/null +++ b/sub_crates/blue_noise_mask/src/lib.rs @@ -0,0 +1,4 @@ +#![allow(dead_code)] + +// Include the file generated by the build.rs script +include!(concat!(env!("OUT_DIR"), "/blue_noise_masks.rs"));