use image::{GrayImage, ImageBuffer, Luma};
use indicatif::{ProgressBar, ProgressStyle};
use rustfft::FftPlanner;
use rustfft::num_complex::Complex;
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Clone)]
pub struct BlueNoiseConfig {
pub width: usize,
pub height: usize,
pub sigma: f32,
pub initial_density: f32,
pub seed: Option<u32>,
pub verbose: bool,
}
impl Default for BlueNoiseConfig {
fn default() -> Self {
Self {
width: 64,
height: 64,
sigma: 1.9,
initial_density: 0.1,
seed: None,
verbose: false,
}
}
}
#[derive(Debug, Clone)]
pub struct BlueNoiseResult {
pub data: Vec<u8>,
pub width: usize,
pub height: usize,
}
#[derive(Error, Debug)]
pub enum GeneratorError {
#[error("Width and height must be positive and small enough to process safely")]
InvalidDimensions,
#[error("Sigma must be positive")]
InvalidSigma,
#[error("Initial density must be between 0 and 1")]
InvalidDensity,
#[error("Failed to save image: {0}")]
ImageSaveError(#[from] image::ImageError),
#[error("Generation failed to converge")]
ConvergenceError,
}
pub type Result<T> = std::result::Result<T, GeneratorError>;
struct SeededRandom {
seed: u32,
}
impl SeededRandom {
fn new(seed: Option<u32>) -> Self {
Self {
seed: seed.unwrap_or_else(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u32
}),
}
}
fn next(&mut self) -> f32 {
self.seed = self.seed.wrapping_add(0x6D2B79F5);
let mut t = self.seed ^ (self.seed >> 15);
t = t.wrapping_mul(1 | self.seed);
t ^= t.wrapping_add(t.wrapping_mul(t ^ (t >> 7)).wrapping_mul(61 | t));
let bits = t ^ (t >> 14);
Self::normalize(bits)
}
fn normalize(bits: u32) -> f32 {
((bits >> 8) as f32) / 16_777_216.0
}
fn index_below(&mut self, upper_bound: usize) -> usize {
debug_assert!(upper_bound > 0);
((self.next() * upper_bound as f32) as usize).min(upper_bound - 1)
}
}
pub struct BlueNoiseGenerator {
max_iterations_multiplier: usize,
threshold_map_levels: usize,
width: usize,
height: usize,
area: usize,
sigma: f32,
initial_density: f32,
verbose: bool,
random: SeededRandom,
bitmap: Vec<u8>,
rank: Vec<i32>,
energy: Vec<f32>,
ones_count: usize,
use_fft: bool,
gaussian_kernel: Vec<f32>,
gaussian_kernel_freq: Option<Vec<Complex<f32>>>,
progress: Option<ProgressBar>,
}
impl BlueNoiseGenerator {
const MAX_ITERATIONS_MULTIPLIER: usize = 10;
const THRESHOLD_MAP_LEVELS: usize = 256;
fn is_power_of_two(n: usize) -> bool {
n > 0 && (n & (n - 1)) == 0
}
pub fn new(config: BlueNoiseConfig) -> Result<Self> {
if config.width == 0 || config.height == 0 {
return Err(GeneratorError::InvalidDimensions);
}
if config.sigma <= 0.0 {
return Err(GeneratorError::InvalidSigma);
}
if config.initial_density <= 0.0 || config.initial_density >= 1.0 {
return Err(GeneratorError::InvalidDensity);
}
if config.width > u32::MAX as usize || config.height > u32::MAX as usize {
return Err(GeneratorError::InvalidDimensions);
}
let area = config
.width
.checked_mul(config.height)
.ok_or(GeneratorError::InvalidDimensions)?;
if area > i32::MAX as usize || area.checked_mul(Self::MAX_ITERATIONS_MULTIPLIER).is_none() {
return Err(GeneratorError::InvalidDimensions);
}
let use_fft = Self::is_power_of_two(config.width) && Self::is_power_of_two(config.height);
let progress = if config.verbose {
Some(ProgressBar::new(100))
} else {
None
};
if let Some(pb) = &progress {
pb.set_style(
ProgressStyle::default_bar()
.template("[{elapsed_precise}] {bar:40.cyan/blue} {pos:>3}% {msg}")
.unwrap()
.progress_chars("##-"),
);
}
let mut generator = Self {
max_iterations_multiplier: Self::MAX_ITERATIONS_MULTIPLIER,
threshold_map_levels: Self::THRESHOLD_MAP_LEVELS,
width: config.width,
height: config.height,
area,
sigma: config.sigma,
initial_density: config.initial_density,
verbose: config.verbose,
random: SeededRandom::new(config.seed),
bitmap: vec![0; area],
rank: vec![0; area],
energy: vec![0.0; area],
ones_count: 0,
use_fft,
gaussian_kernel: Vec::new(),
gaussian_kernel_freq: None,
progress,
};
generator.gaussian_kernel = generator.create_gaussian_kernel();
if use_fft {
generator.gaussian_kernel_freq =
Some(generator.fft_2d_forward(&generator.gaussian_kernel));
}
Ok(generator)
}
fn create_gaussian_kernel(&self) -> Vec<f32> {
let mut kernel = vec![0.0f32; self.area];
let divisor = 2.0 * self.sigma * self.sigma;
for y in 0..self.height {
for x in 0..self.width {
let dx = x.min(self.width - x) as f32;
let dy = y.min(self.height - y) as f32;
let dist_sq = dx * dx + dy * dy;
kernel[y * self.width + x] = (-dist_sq / divisor).exp();
}
}
let sum: f32 = kernel.iter().sum();
for val in kernel.iter_mut() {
*val /= sum;
}
kernel
}
fn fft_2d_forward(&self, data: &[f32]) -> Vec<Complex<f32>> {
let mut complex_data: Vec<Complex<f32>> =
data.iter().map(|&x| Complex::new(x, 0.0)).collect();
let mut planner = FftPlanner::new();
let fft = planner.plan_fft_forward(self.width);
for y in 0..self.height {
let start = y * self.width;
let end = start + self.width;
fft.process(&mut complex_data[start..end]);
}
let fft = planner.plan_fft_forward(self.height);
let mut column = vec![Complex::new(0.0, 0.0); self.height];
for x in 0..self.width {
for y in 0..self.height {
column[y] = complex_data[y * self.width + x];
}
fft.process(&mut column);
for y in 0..self.height {
complex_data[y * self.width + x] = column[y];
}
}
complex_data
}
fn fft_2d_inverse(&self, complex_data: &[Complex<f32>]) -> Vec<f32> {
let mut data = complex_data.to_vec();
let mut planner = FftPlanner::new();
let ifft = planner.plan_fft_inverse(self.height);
let mut column = vec![Complex::new(0.0, 0.0); self.height];
for x in 0..self.width {
for y in 0..self.height {
column[y] = data[y * self.width + x];
}
ifft.process(&mut column);
for y in 0..self.height {
data[y * self.width + x] = column[y];
}
}
let ifft = planner.plan_fft_inverse(self.width);
for y in 0..self.height {
let start = y * self.width;
let end = start + self.width;
ifft.process(&mut data[start..end]);
}
data.iter().map(|c| c.re / (self.area as f32)).collect()
}
fn gaussian_blur_fft(&self, data: &[u8]) -> Vec<f32> {
let float_data: Vec<f32> = data.iter().map(|&x| x as f32).collect();
let data_freq = self.fft_2d_forward(&float_data);
let kernel_freq = self.gaussian_kernel_freq.as_ref().unwrap();
let result_freq: Vec<Complex<f32>> = data_freq
.iter()
.zip(kernel_freq.iter())
.map(|(d, k)| d * k)
.collect();
self.fft_2d_inverse(&result_freq)
}
fn gaussian_blur_spatial(&self, data: &[u8]) -> Vec<f32> {
let mut blurred = vec![0.0f32; self.area];
for (source_idx, &source) in data.iter().enumerate().take(self.area) {
let value = source as f32;
if value == 0.0 {
continue;
}
for (target_idx, target) in blurred.iter_mut().enumerate() {
let kernel_idx = self.kernel_index_for_delta(source_idx, target_idx);
*target += value * self.gaussian_kernel[kernel_idx];
}
}
blurred
}
fn gaussian_blur(&self, data: &[u8]) -> Vec<f32> {
if self.use_fft {
self.gaussian_blur_fft(data)
} else {
self.gaussian_blur_spatial(data)
}
}
fn find_tightest_cluster(&self) -> Option<usize> {
let mut max_energy = f32::NEG_INFINITY;
let mut max_idx = None;
for i in 0..self.area {
if self.bitmap[i] == 1 && self.energy[i] > max_energy {
max_energy = self.energy[i];
max_idx = Some(i);
}
}
max_idx
}
fn find_largest_void(&self) -> Option<usize> {
let mut min_energy = f32::INFINITY;
let mut min_idx = None;
for i in 0..self.area {
if self.bitmap[i] == 0 && self.energy[i] < min_energy {
min_energy = self.energy[i];
min_idx = Some(i);
}
}
min_idx
}
fn count_ones(&self) -> usize {
self.ones_count
}
fn set_bit(&mut self, idx: usize, value: u8) {
let old_value = self.bitmap[idx];
if old_value == value {
return;
}
self.bitmap[idx] = value;
self.ones_count = self.ones_count + value as usize - old_value as usize;
}
fn set_bit_and_update_energy(&mut self, idx: usize, value: u8) {
let old_value = self.bitmap[idx];
if old_value == value {
return;
}
self.set_bit(idx, value);
self.apply_energy_delta(idx, value as f32 - old_value as f32);
}
fn kernel_index_for_delta(&self, source_idx: usize, target_idx: usize) -> usize {
let source_x = source_idx % self.width;
let source_y = source_idx / self.width;
let target_x = target_idx % self.width;
let target_y = target_idx / self.width;
let dx = if target_x >= source_x {
target_x - source_x
} else {
self.width - source_x + target_x
};
let dy = if target_y >= source_y {
target_y - source_y
} else {
self.height - source_y + target_y
};
dy * self.width + dx
}
fn apply_energy_delta(&mut self, idx: usize, delta: f32) {
for target_idx in 0..self.area {
let kernel_idx = self.kernel_index_for_delta(idx, target_idx);
self.energy[target_idx] += delta * self.gaussian_kernel[kernel_idx];
}
}
fn recalculate_ones_count(&mut self) {
self.ones_count = self.bitmap.iter().map(|&x| x as usize).sum();
}
fn recalculate_energy(&mut self) {
self.energy = self.gaussian_blur(&self.bitmap);
}
fn phase0_generate_initial_pattern(&mut self) -> Result<()> {
if self.verbose
&& let Some(pb) = &self.progress
{
pb.set_message("Phase 0: Generating initial pattern");
pb.set_position(0);
}
let target_points = (self.area as f32 * self.initial_density) as usize;
while self.count_ones() < target_points {
let idx = self.random.index_below(self.area);
if self.bitmap[idx] == 0 {
self.set_bit(idx, 1);
}
}
self.recalculate_energy();
let max_iterations = self
.area
.checked_mul(self.max_iterations_multiplier)
.ok_or(GeneratorError::InvalidDimensions)?;
let mut iterations = 0;
while iterations < max_iterations {
iterations += 1;
let cluster_idx = self
.find_tightest_cluster()
.ok_or(GeneratorError::ConvergenceError)?;
self.set_bit_and_update_energy(cluster_idx, 0);
let void_idx = self
.find_largest_void()
.ok_or(GeneratorError::ConvergenceError)?;
if void_idx == cluster_idx {
self.set_bit_and_update_energy(cluster_idx, 1);
break;
}
self.set_bit_and_update_energy(void_idx, 1);
}
if self.verbose
&& let Some(pb) = &self.progress
{
pb.set_position(20);
}
Ok(())
}
fn phase1_serialize_initial_points(&mut self) -> Result<()> {
if self.verbose
&& let Some(pb) = &self.progress
{
pb.set_message("Phase 1: Serializing initial points");
pb.set_position(20);
}
let mut rank_counter = self.count_ones() as i32 - 1;
while self.count_ones() > 0 {
let cluster_idx = self
.find_tightest_cluster()
.ok_or(GeneratorError::ConvergenceError)?;
self.rank[cluster_idx] = rank_counter;
rank_counter -= 1;
self.set_bit_and_update_energy(cluster_idx, 0);
}
if self.verbose
&& let Some(pb) = &self.progress
{
pb.set_position(40);
}
Ok(())
}
fn phase2_fill_to_half(&mut self, prototype: &[u8], initial_points: usize) -> Result<()> {
if self.verbose
&& let Some(pb) = &self.progress
{
pb.set_message("Phase 2: Filling to half capacity");
pb.set_position(40);
}
self.bitmap.copy_from_slice(prototype);
self.recalculate_ones_count();
self.recalculate_energy();
let mut rank_counter = initial_points as i32;
let half_area = self.area / 2;
while self.count_ones() < half_area {
let void_idx = self
.find_largest_void()
.ok_or(GeneratorError::ConvergenceError)?;
self.rank[void_idx] = rank_counter;
rank_counter += 1;
self.set_bit_and_update_energy(void_idx, 1);
}
if self.verbose
&& let Some(pb) = &self.progress
{
pb.set_position(60);
}
Ok(())
}
fn phase3_fill_to_completion(&mut self, mut rank_counter: i32) -> Result<()> {
if self.verbose
&& let Some(pb) = &self.progress
{
pb.set_message("Phase 3: Filling to completion");
pb.set_position(60);
}
for i in 0..self.area {
self.bitmap[i] = 1 - self.bitmap[i];
}
self.recalculate_ones_count();
self.recalculate_energy();
while rank_counter < self.area as i32 {
let cluster_idx = self
.find_tightest_cluster()
.ok_or(GeneratorError::ConvergenceError)?;
self.rank[cluster_idx] = rank_counter;
rank_counter += 1;
self.set_bit_and_update_energy(cluster_idx, 0);
}
if self.verbose
&& let Some(pb) = &self.progress
{
pb.set_position(80);
}
Ok(())
}
fn phase4_convert_to_threshold_map(&self) -> Vec<u8> {
if self.verbose
&& let Some(pb) = &self.progress
{
pb.set_message("Phase 4: Converting to threshold map");
pb.set_position(80);
}
let output: Vec<u8> = self
.rank
.iter()
.map(|&r| ((r as usize * self.threshold_map_levels) / self.area) as u8)
.collect();
if self.verbose
&& let Some(pb) = &self.progress
{
pb.set_position(100);
pb.finish_with_message("Blue noise generation complete");
}
output
}
pub fn generate(mut self) -> Result<BlueNoiseResult> {
let start_time = std::time::Instant::now();
if self.verbose {
println!(
"Generating {}�{} blue noise texture...",
self.width, self.height
);
println!(
"Using {} Gaussian blur",
if self.use_fft {
"FFT-optimized"
} else {
"spatial"
}
);
}
self.phase0_generate_initial_pattern()?;
let prototype = self.bitmap.clone();
let initial_points = self.count_ones();
if self.verbose {
println!("Initial pattern: {} points", initial_points);
}
self.phase1_serialize_initial_points()?;
self.phase2_fill_to_half(&prototype, initial_points)?;
let half_area = self.area / 2;
self.phase3_fill_to_completion(half_area as i32)?;
let data = self.phase4_convert_to_threshold_map();
if self.verbose {
let elapsed = start_time.elapsed();
println!(
"Blue noise generation complete in {:.2}s",
elapsed.as_secs_f32()
);
}
Ok(BlueNoiseResult {
data,
width: self.width,
height: self.height,
})
}
}
pub fn save_blue_noise_to_png<P: AsRef<Path>>(result: &BlueNoiseResult, filename: P) -> Result<()> {
let img: GrayImage = ImageBuffer::from_fn(result.width as u32, result.height as u32, |x, y| {
let idx = y as usize * result.width + x as usize;
Luma([result.data[idx]])
});
img.save(&filename)?;
println!(
"Saved blue noise texture to {}",
filename.as_ref().display()
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_seeded_random_deterministic() {
let mut rng1 = SeededRandom::new(Some(42));
let mut rng2 = SeededRandom::new(Some(42));
for _ in 0..100 {
assert_eq!(rng1.next(), rng2.next());
}
}
#[test]
fn test_seeded_random_range() {
let mut rng = SeededRandom::new(Some(12345));
for _ in 0..5_000_000 {
let val = rng.next();
assert!((0.0..1.0).contains(&val), "out of range: {val}");
}
for bits in [
u32::MAX,
u32::MAX - 1,
u32::MAX - 127,
4_294_967_168, 0,
1,
] {
let val = SeededRandom::normalize(bits);
assert!((0.0..1.0).contains(&val), "boundary {bits} -> {val}");
}
assert!(SeededRandom::normalize(u32::MAX) < 1.0);
}
#[test]
fn test_seeded_random_index_below_never_returns_upper_bound() {
let mut rng = SeededRandom::new(Some(12345));
assert_eq!(rng.index_below(1), 0);
for _ in 0..1000 {
assert!(rng.index_below(262_144) < 262_144);
}
}
#[test]
fn test_is_power_of_two() {
assert!(BlueNoiseGenerator::is_power_of_two(1));
assert!(BlueNoiseGenerator::is_power_of_two(2));
assert!(BlueNoiseGenerator::is_power_of_two(4));
assert!(BlueNoiseGenerator::is_power_of_two(8));
assert!(BlueNoiseGenerator::is_power_of_two(16));
assert!(BlueNoiseGenerator::is_power_of_two(64));
assert!(BlueNoiseGenerator::is_power_of_two(128));
assert!(BlueNoiseGenerator::is_power_of_two(256));
assert!(!BlueNoiseGenerator::is_power_of_two(0));
assert!(!BlueNoiseGenerator::is_power_of_two(3));
assert!(!BlueNoiseGenerator::is_power_of_two(5));
assert!(!BlueNoiseGenerator::is_power_of_two(100));
assert!(!BlueNoiseGenerator::is_power_of_two(255));
}
#[test]
fn test_config_validation() {
let config = BlueNoiseConfig {
width: 64,
height: 64,
sigma: 1.9,
initial_density: 0.1,
seed: Some(42),
verbose: false,
};
assert!(BlueNoiseGenerator::new(config).is_ok());
let config = BlueNoiseConfig {
width: 0,
height: 64,
..Default::default()
};
assert!(BlueNoiseGenerator::new(config).is_err());
let config = BlueNoiseConfig {
width: 64,
height: 0,
..Default::default()
};
assert!(BlueNoiseGenerator::new(config).is_err());
let config = BlueNoiseConfig {
width: 64,
height: 64,
sigma: -1.0,
..Default::default()
};
assert!(BlueNoiseGenerator::new(config).is_err());
let config = BlueNoiseConfig {
width: 64,
height: 64,
initial_density: 0.0,
..Default::default()
};
assert!(BlueNoiseGenerator::new(config).is_err());
let config = BlueNoiseConfig {
width: 64,
height: 64,
initial_density: 1.0,
..Default::default()
};
assert!(BlueNoiseGenerator::new(config).is_err());
let config = BlueNoiseConfig {
width: usize::MAX,
height: 2,
..Default::default()
};
assert!(BlueNoiseGenerator::new(config).is_err());
let config = BlueNoiseConfig {
width: i32::MAX as usize + 1,
height: 1,
..Default::default()
};
assert!(BlueNoiseGenerator::new(config).is_err());
}
#[test]
fn test_generate_small_texture() {
let config = BlueNoiseConfig {
width: 16,
height: 16,
sigma: 1.5,
seed: Some(42),
verbose: false,
..Default::default()
};
let generator = BlueNoiseGenerator::new(config).unwrap();
let result = generator.generate().unwrap();
assert_eq!(result.width, 16);
assert_eq!(result.height, 16);
assert_eq!(result.data.len(), 256);
assert!(result.data.iter().any(|&val| val > 0));
assert!(result.data.iter().any(|&val| val < 255));
}
#[test]
fn test_incremental_energy_matches_full_recalculation() {
let config = BlueNoiseConfig {
width: 8,
height: 8,
sigma: 1.5,
seed: Some(42),
verbose: false,
..Default::default()
};
let mut generator = BlueNoiseGenerator::new(config).unwrap();
generator.set_bit(0, 1);
generator.recalculate_energy();
generator.set_bit_and_update_energy(17, 1);
let incremental = generator.energy.clone();
generator.recalculate_energy();
for (incremental, recalculated) in incremental.iter().zip(generator.energy.iter()) {
assert!((incremental - recalculated).abs() < 0.000_01);
}
}
#[test]
fn test_generate_reproducible() {
let config1 = BlueNoiseConfig {
width: 32,
height: 32,
seed: Some(12345),
verbose: false,
..Default::default()
};
let config2 = BlueNoiseConfig {
width: 32,
height: 32,
seed: Some(12345),
verbose: false,
..Default::default()
};
let gen1 = BlueNoiseGenerator::new(config1).unwrap();
let gen2 = BlueNoiseGenerator::new(config2).unwrap();
let result1 = gen1.generate().unwrap();
let result2 = gen2.generate().unwrap();
assert_eq!(result1.data, result2.data);
}
#[test]
fn test_generate_different_seeds() {
let config1 = BlueNoiseConfig {
width: 32,
height: 32,
seed: Some(111),
verbose: false,
..Default::default()
};
let config2 = BlueNoiseConfig {
width: 32,
height: 32,
seed: Some(222),
verbose: false,
..Default::default()
};
let gen1 = BlueNoiseGenerator::new(config1).unwrap();
let gen2 = BlueNoiseGenerator::new(config2).unwrap();
let result1 = gen1.generate().unwrap();
let result2 = gen2.generate().unwrap();
assert_ne!(result1.data, result2.data);
}
#[test]
fn test_generate_power_of_two_uses_fft() {
let config = BlueNoiseConfig {
width: 64,
height: 64,
seed: Some(42),
verbose: false,
..Default::default()
};
let generator = BlueNoiseGenerator::new(config).unwrap();
assert!(generator.use_fft);
assert!(generator.gaussian_kernel_freq.is_some());
}
#[test]
fn test_generate_non_power_of_two_no_fft() {
let config = BlueNoiseConfig {
width: 50,
height: 50,
seed: Some(42),
verbose: false,
..Default::default()
};
let generator = BlueNoiseGenerator::new(config).unwrap();
assert!(!generator.use_fft);
assert!(generator.gaussian_kernel_freq.is_none());
}
#[test]
fn test_threshold_map_distribution() {
let config = BlueNoiseConfig {
width: 32,
height: 32,
seed: Some(99),
verbose: false,
..Default::default()
};
let generator = BlueNoiseGenerator::new(config).unwrap();
let result = generator.generate().unwrap();
let mut histogram = vec![0usize; 256];
for &val in &result.data {
histogram[val as usize] += 1;
}
let non_empty_bins = histogram.iter().filter(|&&count| count > 0).count();
assert!(non_empty_bins > 200, "Expected diverse distribution");
}
#[test]
fn test_rectangular_texture() {
let config = BlueNoiseConfig {
width: 32,
height: 16,
seed: Some(42),
verbose: false,
..Default::default()
};
let generator = BlueNoiseGenerator::new(config).unwrap();
let result = generator.generate().unwrap();
assert_eq!(result.width, 32);
assert_eq!(result.height, 16);
assert_eq!(result.data.len(), 512);
}
#[test]
fn test_default_config() {
let config = BlueNoiseConfig::default();
assert_eq!(config.width, 64);
assert_eq!(config.height, 64);
assert_eq!(config.sigma, 1.9);
assert_eq!(config.initial_density, 0.1);
assert_eq!(config.seed, None);
assert!(!config.verbose);
}
}