Skip to content

Commit

Permalink
Merge pull request #226 from Walther/hdr
Browse files Browse the repository at this point in the history
feature: add high dynamic range file output
  • Loading branch information
Walther authored Dec 27, 2024
2 parents 4706503 + 7bf31d7 commit fa951a0
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 103 deletions.
2 changes: 1 addition & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ all-scenes *ARGS:
DATE=$(date -u +%s); \
mkdir -p renders/$DATE; \
for scene in $(ls scenes/ |grep json); \
do just cli render --output renders/$DATE/${scene%.json}.png {{ARGS}} scenes/$scene; \
do just cli render --output renders/$DATE/${scene%.json} {{ARGS}} -i scenes/$scene; \
done;

# Profiling helper
Expand Down
5 changes: 4 additions & 1 deletion clovers-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ blue-noise-sampler = "0.1.0"
clap = { version = "4.5.23", features = ["std", "derive"] }
human_format = "1.1.0"
humantime = "2.1.0"
image = { version = "0.25.5", features = ["png"], default-features = false }
image = { version = "0.25.5", features = [
"png",
"exr",
], default-features = false }
img-parts = "0.3.1"
indicatif = { version = "0.17.9", features = [
"rayon",
Expand Down
22 changes: 13 additions & 9 deletions clovers-cli/src/debug_visualizations.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
//! Alternative rendering methods for debug visualization purposes.
use clovers::{ray::Ray, scenes::Scene, Float, EPSILON_SHADOW_ACNE};
use palette::LinSrgb;
use palette::{chromatic_adaptation::AdaptInto, white_point::E, LinSrgb, Xyz};
use rand::rngs::SmallRng;

/// Visualizes the BVH traversal count - how many BVH nodes needed to be tested for intersection?
#[must_use]
pub fn bvh_testcount(ray: &Ray, scene: &Scene, rng: &mut SmallRng) -> LinSrgb {
pub fn bvh_testcount(ray: &Ray, scene: &Scene, rng: &mut SmallRng) -> Xyz<E> {
let mut depth = 0;
scene
.bvh_root
Expand All @@ -16,8 +16,8 @@ pub fn bvh_testcount(ray: &Ray, scene: &Scene, rng: &mut SmallRng) -> LinSrgb {
}

#[must_use]
pub fn bvh_testcount_to_color(depth: usize) -> LinSrgb {
match depth {
pub fn bvh_testcount_to_color(depth: usize) -> Xyz<E> {
let color: LinSrgb = match depth {
// under 256, grayscale
0..=255 => {
let depth = depth as Float / 255.0;
Expand All @@ -29,12 +29,14 @@ pub fn bvh_testcount_to_color(depth: usize) -> LinSrgb {
512..=1023 => LinSrgb::new(1.0, 0.5, 0.0),
// more than 1024, red
1024.. => LinSrgb::new(1.0, 0.0, 0.0),
}
};

color.adapt_into()
}

/// Visualizes the primitive traversal count - how many primitives needed to be tested for intersection?
#[must_use]
pub fn primitive_testcount(ray: &Ray, scene: &Scene, rng: &mut SmallRng) -> LinSrgb {
pub fn primitive_testcount(ray: &Ray, scene: &Scene, rng: &mut SmallRng) -> Xyz<E> {
let mut depth = 0;
scene
.bvh_root
Expand All @@ -44,8 +46,8 @@ pub fn primitive_testcount(ray: &Ray, scene: &Scene, rng: &mut SmallRng) -> LinS
}

#[must_use]
pub fn primitive_testcount_to_color(depth: usize) -> LinSrgb {
match depth {
pub fn primitive_testcount_to_color(depth: usize) -> Xyz<E> {
let color: LinSrgb = match depth {
// under 256, grayscale
0..=255 => {
let depth = depth as Float / 255.0;
Expand All @@ -57,5 +59,7 @@ pub fn primitive_testcount_to_color(depth: usize) -> LinSrgb {
512..=1023 => LinSrgb::new(1.0, 0.5, 0.0),
// more than 1024, red
1024.. => LinSrgb::new(1.0, 0.0, 0.0),
}
};

color.adapt_into()
}
38 changes: 15 additions & 23 deletions clovers-cli/src/draw_cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ use clovers::Vec2;
use clovers::{ray::Ray, scenes::Scene, Float};
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use palette::chromatic_adaptation::AdaptInto;
use palette::convert::IntoColorUnclamped;
use palette::white_point::E;
use palette::{LinSrgb, Srgb, Xyz};
use palette::{LinSrgb, Xyz};
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use rayon::prelude::*;
Expand All @@ -27,7 +26,7 @@ pub fn draw(
render_options: &RenderOptions,
scene: &Scene,
_sampler: Sampler,
) -> Vec<Srgb<u8>> {
) -> Vec<Xyz<E>> {
let GlobalOptions { debug: _, quiet } = *global_options;
let RenderOptions {
input: _,
Expand All @@ -39,13 +38,17 @@ pub fn draw(
mode,
sampler,
bvh: _,
formats: _,
} = *render_options;
let bar = progress_bar(height, quiet);

let height = height as usize;
let width = width as usize;

let pixelbuffer: Vec<Srgb<u8>> = (0..height)
// TODO: fix the coordinate system; this flips up<->down
let rows: Vec<usize> = (0..height).rev().collect();

let pixelbuffer: Vec<Xyz<E>> = rows
.into_par_iter()
.map(|row_index| {
let mut sampler_rng = SmallRng::from_entropy();
Expand Down Expand Up @@ -99,7 +102,7 @@ fn render_pixel(
index: usize,
rng: &mut SmallRng,
sampler: &mut dyn SamplerTrait,
) -> Srgb<u8> {
) -> Xyz<E> {
let (x, y, width, height) = index_to_params(opts, index);
let pixel_location = Vec2::new(x, y);
let canvas_size = Vec2::new(width, height);
Expand All @@ -125,10 +128,7 @@ fn render_pixel(
pixel_color += sample_color;
}
}
pixel_color /= opts.samples as Float;
let color: Srgb = pixel_color.adapt_into();
let color: Srgb<u8> = color.into_format();
color
pixel_color / opts.samples as Float
}

// Render a single pixel in normalmap mode
Expand All @@ -137,7 +137,7 @@ fn render_pixel_normalmap(
opts: &RenderOptions,
index: usize,
rng: &mut SmallRng,
) -> Srgb<u8> {
) -> Xyz<E> {
let (x, y, width, height) = index_to_params(opts, index);
let color: LinSrgb = {
let pixel_location = Vec2::new(x / width, y / height);
Expand All @@ -149,9 +149,7 @@ fn render_pixel_normalmap(
.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
color.adapt_into()
}

// Render a single pixel in bvh test count visualization mode
Expand All @@ -161,7 +159,7 @@ fn render_pixel_bvhtestcount(
index: usize,
rng: &mut SmallRng,
_sampler: &mut dyn SamplerTrait,
) -> Srgb<u8> {
) -> Xyz<E> {
let (x, y, width, height) = index_to_params(render_options, index);
let pixel_location = Vec2::new(x / width, y / height);
let lens_offset = Vec2::new(0.0, 0.0);
Expand All @@ -171,10 +169,7 @@ fn render_pixel_bvhtestcount(
.camera
.get_ray(pixel_location, lens_offset, time, wavelength);

let color: LinSrgb = { bvh_testcount(&ray, scene, rng) };
let color: Srgb = color.into_color_unclamped();
let color: Srgb<u8> = color.into_format();
color
bvh_testcount(&ray, scene, rng)
}

// Render a single pixel in primitive test count visualization mode
Expand All @@ -184,7 +179,7 @@ fn render_pixel_primitivetestcount(
index: usize,
rng: &mut SmallRng,
_sampler: &mut dyn SamplerTrait,
) -> Srgb<u8> {
) -> Xyz<E> {
let (x, y, width, height) = index_to_params(render_options, index);
let pixel_location = Vec2::new(x / width, y / height);
let lens_offset = Vec2::new(0.0, 0.0);
Expand All @@ -194,10 +189,7 @@ fn render_pixel_primitivetestcount(
.camera
.get_ray(pixel_location, lens_offset, time, wavelength);

let color: LinSrgb = { primitive_testcount(&ray, scene, rng) };
let color: Srgb = color.into_color_unclamped();
let color: Srgb<u8> = color.into_format();
color
primitive_testcount(&ray, scene, rng)
}

fn index_to_params(opts: &RenderOptions, index: usize) -> (Float, Float, Float, Float) {
Expand Down
1 change: 1 addition & 0 deletions clovers-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod normals;
pub mod render;
pub mod sampler;
pub mod scenefile;
pub mod write;

// TODO: move this into a better place - but keep rustc happy with the imports
/// Global options
Expand Down
2 changes: 2 additions & 0 deletions clovers-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ mod validate;
use validate::{validate, ValidateParams};
#[doc(hidden)]
pub mod scenefile;
#[doc(hidden)]
mod write;

/// clovers 🍀 path tracing renderer
#[derive(Parser)]
Expand Down
115 changes: 46 additions & 69 deletions clovers-cli/src/render.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
use clap::{Args, ValueEnum};
use humantime::format_duration;
use image::{ImageBuffer, ImageFormat, Rgb, RgbImage};
use img_parts::png::{Png, PngChunk};
use std::fs::File;
use std::io::Cursor;
use std::path::Path;
use std::{error::Error, fs, time::Instant};

use clap::{Args, ValueEnum};
use humantime::format_duration;
use time::OffsetDateTime;
use tracing::{debug, info, Level};
use tracing_subscriber::fmt::time::UtcTime;

use crate::draw_cpu;
use crate::json_scene::initialize;
use crate::sampler::Sampler;
use crate::write;
use crate::GlobalOptions;

#[derive(Args, Debug)]
#[derive(Args, Clone, Debug)]
pub struct RenderOptions {
/// Input filename / location
#[arg()]
#[arg(short, long)]
pub input: String,
/// Output filename / location. Defaults to ./renders/unix_timestamp.png
/// Output file path, without extension. Defaults to `./renders/unix_timestamp`.
#[arg(short, long)]
pub output: Option<String>,
/// Width of the image in pixels.
Expand All @@ -44,6 +42,10 @@ pub struct RenderOptions {
/// BVH construction algorithm.
#[arg(long, default_value = "sah")]
pub bvh: BvhAlgorithm,
/// File format selection for the output.
/// Multiple formats can be provided to save the same image in multiple formats.
#[arg(short, long, default_value = "png", num_args = 1..)]
pub formats: Vec<Format>,
}

#[derive(Copy, Clone, Debug, PartialEq, ValueEnum)]
Expand All @@ -66,6 +68,14 @@ pub enum BvhAlgorithm {
Sah,
}

#[derive(Copy, Clone, Debug, PartialEq, ValueEnum)]
pub enum Format {
/// Portable Network Graphics, lossless, standard dynamic range
Png,
/// OpenEXR, high dynamic range
Exr,
}

// CLI usage somehow not detected
#[allow(dead_code)]
pub(crate) fn render(
Expand All @@ -83,6 +93,7 @@ pub(crate) fn render(
mode,
sampler,
bvh,
ref formats,
} = render_options;

if debug {
Expand Down Expand Up @@ -123,8 +134,6 @@ pub(crate) fn render(
BvhAlgorithm::Sah => clovers::bvh::BvhAlgorithm::Sah,
};

let threads = std::thread::available_parallelism()?;

info!("Reading the scene file");
let path = Path::new(&input);
let scene = match path.extension() {
Expand All @@ -138,69 +147,37 @@ pub(crate) fn render(
info!("Calling draw()");
let start = Instant::now();
let pixelbuffer = draw_cpu::draw(&global_options, &render_options, &scene, sampler);
info!("Drawing a pixelbuffer finished");

info!("Converting pixelbuffer to an image");
let mut img: RgbImage = ImageBuffer::new(width, height);
img.enumerate_pixels_mut().for_each(|(x, y, pixel)| {
let index = y * width + x;
*pixel = Rgb(pixelbuffer[index as usize].into());
});

// Graphics assume origin at bottom left corner of the screen
// Our buffer writes pixels from top left corner. Simple fix, just flip it!
image::imageops::flip_vertical_in_place(&mut img);
// TODO: fix the coordinate system

let duration = Instant::now() - start;
let formatted_duration = format_duration(duration);
info!("Finished render in {}", formatted_duration);

let duration = format_duration(duration);
info!("Finished render in {}", duration);
if !quiet {
println!("Finished render in {}", formatted_duration);
println!("Finished render in {}", duration);
}

info!("Writing an image file");

let mut bytes: Vec<u8> = Vec::new();
img.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png)?;
let mut png = Png::from_bytes(bytes.into())?;

let common = format!(
"Comment\0Rendered with the clovers path tracing engine. Scene file {input} rendered using the {mode:?} rendering mode at {width}x{height} resolution"
);
let details = match mode {
RenderMode::PathTracing => {
format!(", {samples} samples per pixel, {max_depth} max ray bounce depth.")
}
_ => ".".to_owned(),
};
let stats = format!("Rendering finished in {formatted_duration}, using {threads} threads.");
let comment = format!("{common}{details} {stats}");

let software = "Software\0https://github.com/walther/clovers".to_string();

for metadata in [comment, software] {
let bytes = metadata.as_bytes().to_owned();
let chunk = PngChunk::new([b't', b'E', b'X', b't'], bytes.into());
png.chunks_mut().push(chunk);
for format in formats {
let extension = match format {
Format::Png => "png",
Format::Exr => "exr",
};

let target = match output {
Some(filename) => format!("{filename}.{extension}"),
None => {
// Default to using a timestamp & `renders/` directory
let timestamp = OffsetDateTime::now_utc().unix_timestamp();
fs::create_dir_all("renders")?;
format!("renders/{timestamp}.{extension}")
}
};

match format {
Format::Png => write::png(&pixelbuffer, &target, &duration, &render_options),
Format::Exr => write::exr(&pixelbuffer, &target, &duration, &render_options),
}?;

info!("Image saved to {}", target);
println!("Image saved to: {}", target);
}

let target = match output {
Some(filename) => filename.to_owned(),
None => {
// Default to using a timestamp & `renders/` directory
let timestamp = OffsetDateTime::now_utc().unix_timestamp();
fs::create_dir_all("renders")?;
format!("renders/{}.png", timestamp)
}
};

let output = File::create(&target)?;
png.encoder().write_to(output)?;

info!("Image saved to {}", target);
println!("Image saved to: {}", target);

Ok(())
}
Loading

0 comments on commit fa951a0

Please sign in to comment.