Skip to content

Commit

Permalink
feat: create SamplerTrait and two Samplers: blue noise and random
Browse files Browse the repository at this point in the history
  • Loading branch information
Walther committed Mar 3, 2024
1 parent 402f3e0 commit abbdeda
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 66 deletions.
7 changes: 4 additions & 3 deletions clovers-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ name = "clovers-cli"
path = "src/main.rs"

[lib]
# For testing purposes
name = "clovers_draw_cpu"
path = "src/draw_cpu.rs"
name = "clovers_runtime"
path = "src/lib.rs"

[dependencies]
# Internal
Expand All @@ -23,6 +22,7 @@ clovers = { path = "../clovers", features = [
], default-features = false }

# External
blue-noise-sampler = "0.1.0"
clap = { version = "4.5.1", features = ["std", "derive"] }
human_format = "1.1.0"
humantime = "2.1.0"
Expand All @@ -32,6 +32,7 @@ indicatif = { version = "0.17.8", features = [
"rayon",
], default-features = false }
palette = { version = "0.7.5", features = ["serializing"] }
paste = { version = "1.0.14" }
rand = { version = "0.8.5", features = ["small_rng"], default-features = false }
rayon = "1.9.0"
serde = { version = "1.0.197", features = ["derive"], default-features = false }
Expand Down
5 changes: 3 additions & 2 deletions clovers-cli/benches/draw_cpu.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use clovers::scenes::{initialize, Scene, SceneFile};
use clovers::RenderOpts;
use clovers_draw_cpu::draw;
use clovers_runtime::draw_cpu::draw;
use clovers_runtime::sampler::Sampler;
use divan::{black_box, AllocProfiler};

#[global_allocator]
Expand All @@ -26,7 +27,7 @@ fn draw_cornell(bencher: divan::Bencher) {
bencher
.with_inputs(get_cornell)
.counter(1u32)
.bench_values(|scene| black_box(draw(OPTS, &scene)))
.bench_values(|scene| black_box(draw(OPTS, &scene, Sampler::Random)))
}

fn get_cornell<'scene>() -> Scene<'scene> {
Expand Down
101 changes: 56 additions & 45 deletions clovers-cli/src/draw_cpu.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use clovers::wavelength::random_wavelength;
use clovers::Vec2;
use clovers::{
colorize::colorize, normals::normal_map, ray::Ray, scenes::Scene, Float, RenderOpts,
};
Expand All @@ -10,23 +12,34 @@ use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use rayon::prelude::*;

use crate::sampler::blue::BlueSampler;
use crate::sampler::random::RandomSampler;
use crate::sampler::{Sample, Sampler, SamplerTrait};

/// The main drawing function, returns a `Vec<Srgb>` as a pixelbuffer.
pub fn draw(opts: RenderOpts, scene: &Scene) -> Vec<Srgb<u8>> {
pub fn draw(opts: RenderOpts, scene: &Scene, sampler: Sampler) -> Vec<Srgb<u8>> {
let width = opts.width as usize;
let height = opts.height as usize;
let bar = progress_bar(&opts);

let pixelbuffer: Vec<Srgb<u8>> = (0..height)
.into_par_iter()
.map(|row_index| {
let mut sampler_rng = SmallRng::from_entropy();
let mut sampler: Box<dyn SamplerTrait> = match sampler {
Sampler::Blue => Box::new(BlueSampler::new(&opts)),
Sampler::Random => Box::new(RandomSampler::new(&mut sampler_rng)),
};

let mut rng = SmallRng::from_entropy();
let mut row = Vec::with_capacity(width);

for index in 0..width {
let index = index + row_index * width;
if opts.normalmap {
row.push(render_pixel_normalmap(scene, &opts, index, &mut rng));
} else {
row.push(render_pixel(scene, &opts, index, &mut rng));
row.push(render_pixel(scene, &opts, index, &mut rng, &mut *sampler));
}
}
bar.inc(1);
Expand All @@ -39,16 +52,40 @@ pub fn draw(opts: RenderOpts, scene: &Scene) -> Vec<Srgb<u8>> {
}

// Render a single pixel, including possible multisampling
fn render_pixel(scene: &Scene, opts: &RenderOpts, index: usize, rng: &mut SmallRng) -> Srgb<u8> {
fn render_pixel(
scene: &Scene,
opts: &RenderOpts,
index: usize,
rng: &mut SmallRng,
sampler: &mut dyn SamplerTrait,
) -> Srgb<u8> {
let (x, y, width, height) = index_to_params(opts, index);
let mut color: Xyz<E> = Xyz::new(0.0, 0.0, 0.0);
for _sample in 0..opts.samples {
if let Some(s) = sample(scene, x, y, width, height, rng, opts.max_depth) {
color += s
let pixel_location = Vec2::new(x, y);
let canvas_size = Vec2::new(width, height);
let max_depth = opts.max_depth;
let mut pixel_color: Xyz<E> = Xyz::new(0.0, 0.0, 0.0);
for sample in 0..opts.samples {
let Sample {
pixel_offset,
lens_offset,
time,
wavelength,
} = sampler.sample(x as i32, y as i32, sample as i32);
let pixel_uv: Vec2 = Vec2::new(
(pixel_location.x + pixel_offset.x) / canvas_size.x,
(pixel_location.y + pixel_offset.y) / canvas_size.y,
);
// note get_ray wants uv 0..1 location
let ray: Ray = scene
.camera
.get_ray(pixel_uv, lens_offset, time, wavelength);
let sample_color: Xyz<E> = colorize(&ray, scene, 0, max_depth, rng);
if sample_color.x.is_finite() && sample_color.y.is_finite() && sample_color.z.is_finite() {
pixel_color += sample_color;
}
}
color /= opts.samples as Float;
let color: Srgb = color.adapt_into();
pixel_color /= opts.samples as Float;
let color: Srgb = pixel_color.adapt_into();
let color: Srgb<u8> = color.into_format();
color
}
Expand All @@ -61,47 +98,21 @@ fn render_pixel_normalmap(
rng: &mut SmallRng,
) -> Srgb<u8> {
let (x, y, width, height) = index_to_params(opts, index);
let color: LinSrgb = sample_normalmap(scene, x, y, width, height, rng);
let color: LinSrgb = {
let pixel_location = Vec2::new(x / width, y / height);
let lens_offset = Vec2::new(0.0, 0.0);
let wavelength = random_wavelength(rng);
let time = rng.gen();
let ray: Ray = scene
.camera
.get_ray(pixel_location, lens_offset, time, wavelength);
normal_map(&ray, scene, rng)
};
let color: Srgb = color.into_color_unclamped();
let color: Srgb<u8> = color.into_format();
color
}

// Get a single sample for a single pixel in the scene, normalmap mode.
fn sample_normalmap(
scene: &Scene,
x: Float,
y: Float,
width: Float,
height: Float,
rng: &mut SmallRng,
) -> LinSrgb {
let u = x / width;
let v = y / height;
let ray: Ray = scene.camera.get_ray(u, v, rng);
normal_map(&ray, scene, rng)
}

/// Get a single sample for a single pixel in the scene. Has slight jitter for antialiasing when multisampling.
fn sample(
scene: &Scene,
x: Float,
y: Float,
width: Float,
height: Float,
rng: &mut SmallRng,
max_depth: u32,
) -> Option<Xyz<E>> {
let u = (x + rng.gen::<Float>()) / width;
let v = (y + rng.gen::<Float>()) / height;
let ray: Ray = scene.camera.get_ray(u, v, rng);
let color: Xyz<E> = colorize(&ray, scene, 0, max_depth, rng);
if color.x.is_finite() && color.y.is_finite() && color.z.is_finite() {
return Some(color);
}
None
}

fn index_to_params(opts: &RenderOpts, index: usize) -> (Float, Float, Float, Float) {
let x = (index % (opts.width as usize)) as Float;
let y = (index / (opts.width as usize)) as Float;
Expand Down
2 changes: 2 additions & 0 deletions clovers-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod draw_cpu;
pub mod sampler;
13 changes: 12 additions & 1 deletion clovers-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use tracing_subscriber::fmt::time::UtcTime;
use clovers::*;
mod draw_cpu;
mod json_scene;
mod sampler;
use sampler::Sampler;

// Configure CLI parameters
#[derive(Parser)]
Expand Down Expand Up @@ -56,6 +58,9 @@ struct Opts {
/// Render a normal map only. Experimental feature.
#[clap(long)]
normalmap: bool,
/// Sampler to use for rendering. Experimental feature.
#[clap(long, default_value = "random")]
sampler: Sampler,
}

fn main() -> Result<(), Box<dyn Error>> {
Expand All @@ -70,6 +75,7 @@ fn main() -> Result<(), Box<dyn Error>> {
gpu,
debug,
normalmap,
sampler,
} = Opts::parse();

if debug {
Expand All @@ -91,10 +97,15 @@ fn main() -> Result<(), Box<dyn Error>> {
println!();
println!("{width}x{height} resolution");
println!("{samples} samples per pixel");
println!("using the {sampler} sampler");
println!("{max_depth} max bounce depth");
println!(); // Empty line before progress bar
}

if sampler == Sampler::Blue && !([1, 2, 4, 8, 16, 32, 128, 256].contains(&samples)) {
panic!("the blue sampler only supports the following sample-per-pixel counts: [1, 2, 4, 8, 16, 32, 128, 256]");
}

let renderopts: RenderOpts = RenderOpts {
width,
height,
Expand All @@ -119,7 +130,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let start = Instant::now();
let pixelbuffer = match gpu {
// Note: live progress bar printed within draw_cpu::draw
false => draw_cpu::draw(renderopts, &scene),
false => draw_cpu::draw(renderopts, &scene, sampler),
true => unimplemented!("GPU accelerated rendering is currently unimplemented"),
};
info!("Drawing a pixelbuffer finished");
Expand Down
40 changes: 40 additions & 0 deletions clovers-cli/src/sampler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use std::fmt::Display;

use clap::ValueEnum;
use clovers::{wavelength::Wavelength, Float, Vec2};

pub mod blue;
pub mod random;

pub trait SamplerTrait<'scene> {
// TODO: better types
fn sample(&mut self, i: i32, j: i32, index: i32) -> Sample;
}

/// A collection of random values to be used for each sample. Returned as a struct to ensure the correct sampling order for the underlying source of randomness.
pub struct Sample {
/// Intra-pixel x,y offset. Used for antialiasing.
pub pixel_offset: Vec2,
/// The x,y offset used in the lens equations for aperture / depth-of-field simulation
pub lens_offset: Vec2,
/// The time of the ray, in range 0..1
pub time: Float,
/// Wavelength of the ray
pub wavelength: Wavelength,
}

#[derive(Clone, Debug, PartialEq, ValueEnum)]
pub enum Sampler {
Blue,
Random,
}

impl Display for Sampler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Sampler::Blue => "blue",
Sampler::Random => "random",
};
write!(f, "{s}")
}
}
96 changes: 96 additions & 0 deletions clovers-cli/src/sampler/blue.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use clovers::{wavelength::sample_wavelength, Float, RenderOpts, Vec2};

use super::{Sample, SamplerTrait};

pub struct BlueSampler {
get: fn(i32, i32, i32, i32) -> Float,
}

impl<'scene> BlueSampler {
pub fn new(opts: &'scene RenderOpts) -> Self {
let get = match opts.samples {
1 => blue_sample_spp1,
2 => blue_sample_spp2,
4 => blue_sample_spp4,
8 => blue_sample_spp8,
16 => blue_sample_spp16,
32 => blue_sample_spp32,
64 => blue_sample_spp64,
128 => blue_sample_spp128,
256 => blue_sample_spp256,
_ => unimplemented!(
"blue sampler only supports sample-per-pixel counts that are powers of two of and up to 256"
),
};
Self { get }
}
}

impl<'scene> SamplerTrait<'scene> for BlueSampler {
fn sample(&mut self, i: i32, j: i32, index: i32) -> Sample {
let pixel_offset = Vec2::new((self.get)(i, j, index, 0), (self.get)(i, j, index, 1));
let lens_offset = Vec2::new((self.get)(i, j, index, 2), (self.get)(i, j, index, 3));
let time = (self.get)(i, j, index, 4);
// TODO: verify uniformity & correctness for math?
let wavelength = sample_wavelength((self.get)(i, j, index, 5));

Sample {
pixel_offset,
lens_offset,
time,
wavelength,
}
}
}

macro_rules! define_blue_sampler {
($spp:ident) => {
::paste::paste! {
pub fn [<blue_sample_ $spp>](
mut pixel_i: i32,
mut pixel_j: i32,
mut sample_index: i32,
mut sample_dimension: i32) -> Float {
use blue_noise_sampler::$spp::*;

// Adapted from <https://dl.acm.org/doi/10.1145/3306307.3328191> and <https://github.com/Jasper-Bekkers/blue-noise-sampler>

// wrap arguments
pixel_i &= 127;
pixel_j &= 127;
sample_index &= 255;
sample_dimension &= 255;

// xor index based on optimized ranking
// jb: 1spp blue noise has all 0 in g_blueNoiseRankingTile so we can skip the load
let index = sample_dimension + (pixel_i + pixel_j * 128) * 8;
let index = index as usize;
let ranked_sample_index = sample_index ^ RANKING_TILE[index];

// fetch value in sequence
let index = sample_dimension + ranked_sample_index * 256;
let index = index as usize;
let value = SOBOL[index];

// If the dimension is optimized, xor sequence value based on optimized scrambling
let index = (sample_dimension % 8) + (pixel_i + pixel_j * 128) * 8;
let index = index as usize;
let value = value ^ SCRAMBLING_TILE[index];

// convert to float and return
let v: Float = (0.5 + value as Float) / 256.0;
v
}
}
};
}

define_blue_sampler!(spp1);
define_blue_sampler!(spp2);
define_blue_sampler!(spp4);
define_blue_sampler!(spp8);
define_blue_sampler!(spp16);
define_blue_sampler!(spp32);
define_blue_sampler!(spp64);
define_blue_sampler!(spp128);
define_blue_sampler!(spp256);
Loading

0 comments on commit abbdeda

Please sign in to comment.