Skip to content

Commit

Permalink
Merge pull request #201 from Walther/blue-noise-sampling
Browse files Browse the repository at this point in the history
feat: blue noise sampling
  • Loading branch information
Walther authored Jun 9, 2024
2 parents d0318dc + db1a919 commit 5f630ce
Show file tree
Hide file tree
Showing 26 changed files with 425 additions and 142 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Build release
run: cargo build --release --verbose
- name: Render all test images
run: just all-scenes
run: just all-scenes --samples 1
- uses: actions/upload-artifact@v3
with:
name: renders
Expand Down
8 changes: 4 additions & 4 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ scene:
cargo run --bin clovers-cli --release -- --input scenes/scene.json -w 1920 -h 1080

# Render all the test scenes available in the repository
all-scenes:
all-scenes *ARGS:
DATE=$(date -u +%s); \
mkdir -p renders/$DATE; \
for scene in $(ls scenes/ |grep json); \
do just cli -s 1 --input scenes/$scene --output renders/$DATE/${scene%.json}.png; \
do just cli --input scenes/$scene --output renders/$DATE/${scene%.json}.png {{ARGS}}; \
done;

# Profiling helper
Expand All @@ -32,8 +32,8 @@ test:
cargo nextest run --cargo-quiet

# Run all benchmarks
bench:
cargo bench --quiet
bench *ARGS:
cargo bench --quiet {{ARGS}}

# Verify no_std compatibility
nostd:
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

Currently, this project is highly experimental. Things change all the time.

- `clovers/` contains the library
- `clovers-cli/` contains the CLI application
- `clovers/` contains the core library and types
- `clovers-cli/` contains the CLI application & runtime

The automatically built documentation is hosted at <https://walther.github.io/clovers/clovers/>.

Expand Down Expand Up @@ -43,3 +43,4 @@ Making this renderer would not have been possible without the availability of an
- [Physically Meaningful Rendering using Tristimulus Colours](https://doi.org/10.1111/cgf.12676)
- [Hero Wavelength Spectral Sampling](https://doi.org/10.1111/cgf.12419)
- [How to interpret the sRGB color space](https://color.org/chardata/rgb/sRGB.pdf)
- [Physically Based Rendering: From Theory To Implementation. 4ed](https://pbr-book.org/)
12 changes: 7 additions & 5 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,17 +22,20 @@ clovers = { path = "../clovers", features = [
], default-features = false }

# External
clap = { version = "4.5.1", features = ["std", "derive"] }
blue-noise-sampler = "0.1.0"
clap = { version = "4.5.4", features = ["std", "derive"] }
human_format = "1.1.0"
humantime = "2.1.0"
image = { version = "0.24.9", features = ["png"], default-features = false }
img-parts = "0.3.0"
indicatif = { version = "0.17.8", features = [
"rayon",
], default-features = false }
nalgebra = { version = "0.32.4" }
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"
rayon = "1.10.0"
serde = { version = "1.0.197", features = ["derive"], default-features = false }
serde_json = { version = "1.0", features = ["alloc"], default-features = false }
time = { version = "0.3.34", 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
31 changes: 14 additions & 17 deletions clovers/src/colorize.rs → clovers-cli/src/colorize.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
//! An opinionated colorize method. Given a [Ray] and a [Scene], evaluates the ray's path and returns a color.
use crate::{
use clovers::{
hitable::HitableTrait,
materials::MaterialType,
pdf::{HitablePDF, MixturePDF, PDFTrait, PDF},
ray::Ray,
scenes::Scene,
spectrum::spectrum_xyz_to_p,
wavelength::{wavelength_into_xyz, Wavelength},
wavelength::wavelength_into_xyz,
Float, EPSILON_SHADOW_ACNE,
};
use nalgebra::Unit;
Expand All @@ -16,14 +16,18 @@ use palette::{
};
use rand::rngs::SmallRng;

/// The main coloring function. Sends a [`Ray`] to the [`Scene`], sees if it hits anything, and eventually returns a color. Taking into account the [Material](crate::materials::Material) that is hit, the method recurses with various adjustments, with a new [`Ray`] started from the location that was hit.
use crate::sampler::SamplerTrait;

/// The main coloring function. Sends a [`Ray`] to the [`Scene`], sees if it hits anything, and eventually returns a color. Taking into account the [Material](clovers::materials::Material) that is hit, the method recurses with various adjustments, with a new [`Ray`] started from the location that was hit.
#[must_use]
#[allow(clippy::only_used_in_recursion)] // TODO: use sampler in more places!
pub fn colorize(
ray: &Ray,
scene: &Scene,
depth: u32,
max_depth: u32,
rng: &mut SmallRng,
sampler: &dyn SamplerTrait,
) -> Xyz<E> {
let bg: Xyz = scene.background_color.into_color_unclamped();
let bg: Xyz<E> = bg.adapt_into();
Expand Down Expand Up @@ -52,15 +56,18 @@ pub fn colorize(
hit_record.v,
hit_record.position,
);
let emitted = adjust_emitted(emitted, ray.wavelength);
let tint: Xyz<E> = wavelength_into_xyz(ray.wavelength);
let emitted = emitted * tint;

// Do we scatter?
let Some(scatter_record) = hit_record.material.scatter(ray, &hit_record, rng) else {
// No scatter, early return the emitted color only
return emitted;
};
// We have scattered, and received an attenuation from the material.
let attenuation = adjust_attenuation(scatter_record.attenuation, ray.wavelength);
let wavelength = ray.wavelength;
let attenuation_factor = spectrum_xyz_to_p(wavelength, scatter_record.attenuation);
let attenuation = (scatter_record.attenuation * attenuation_factor).clamp();

// Check the material type and recurse accordingly:
match scatter_record.material_type {
Expand All @@ -73,6 +80,7 @@ pub fn colorize(
depth + 1,
max_depth,
rng,
sampler,
);
specular * attenuation
}
Expand Down Expand Up @@ -110,7 +118,7 @@ pub fn colorize(
};

// Recurse for the scattering ray
let recurse = colorize(&scatter_ray, scene, depth + 1, max_depth, rng);
let recurse = colorize(&scatter_ray, scene, depth + 1, max_depth, rng, sampler);
// Tint and weight it according to the PDF
let scattered = attenuation * scattering_pdf * recurse / pdf_val;
// Ensure positive color
Expand All @@ -120,14 +128,3 @@ pub fn colorize(
}
}
}

fn adjust_emitted(emitted: Xyz<E>, wavelength: Wavelength) -> Xyz<E> {
let tint: Xyz<E> = wavelength_into_xyz(wavelength);
tint * emitted
}

fn adjust_attenuation(attenuation: Xyz<E>, wavelength: Wavelength) -> Xyz<E> {
let attenuation_factor = spectrum_xyz_to_p(wavelength, attenuation);
let attenuation = attenuation * attenuation_factor;
attenuation.clamp()
}
109 changes: 61 additions & 48 deletions clovers-cli/src/draw_cpu.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use clovers::{
colorize::colorize, normals::normal_map, ray::Ray, scenes::Scene, Float, RenderOpts,
};
//! An opinionated method for drawing a scene using the CPU for rendering.
use clovers::wavelength::random_wavelength;
use clovers::Vec2;
use clovers::{ray::Ray, scenes::Scene, Float, RenderOpts};
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use palette::chromatic_adaptation::AdaptInto;
use palette::convert::IntoColorUnclamped;
Expand All @@ -10,23 +12,36 @@ use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use rayon::prelude::*;

use crate::colorize::colorize;
use crate::normals::normal_map;
use crate::sampler::blue::BlueSampler;
use crate::sampler::random::RandomSampler;
use crate::sampler::{Randomness, 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 +54,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 Randomness {
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, sampler);
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 +100,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
6 changes: 6 additions & 0 deletions clovers-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//! Runtime functions of the `clovers` renderer.
pub mod colorize;
pub mod draw_cpu;
pub mod normals;
pub mod sampler;
Loading

0 comments on commit 5f630ce

Please sign in to comment.