From af11bf9d87e43b00e58d5dd0ab09b35770c68b0b Mon Sep 17 00:00:00 2001 From: Walther Date: Wed, 25 Dec 2024 19:55:10 +0100 Subject: [PATCH 1/4] refactor: create a separate write module --- clovers-cli/src/debug_visualizations.rs | 22 ++++--- clovers-cli/src/draw_cpu.rs | 35 ++++------- clovers-cli/src/lib.rs | 1 + clovers-cli/src/main.rs | 2 + clovers-cli/src/render.rs | 82 +++++++++---------------- clovers-cli/src/write.rs | 79 ++++++++++++++++++++++++ 6 files changed, 135 insertions(+), 86 deletions(-) create mode 100644 clovers-cli/src/write.rs diff --git a/clovers-cli/src/debug_visualizations.rs b/clovers-cli/src/debug_visualizations.rs index f9f6569e..6b117032 100644 --- a/clovers-cli/src/debug_visualizations.rs +++ b/clovers-cli/src/debug_visualizations.rs @@ -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 { let mut depth = 0; scene .bvh_root @@ -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 { + let color: LinSrgb = match depth { // under 256, grayscale 0..=255 => { let depth = depth as Float / 255.0; @@ -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 { let mut depth = 0; scene .bvh_root @@ -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 { + let color: LinSrgb = match depth { // under 256, grayscale 0..=255 => { let depth = depth as Float / 255.0; @@ -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() } diff --git a/clovers-cli/src/draw_cpu.rs b/clovers-cli/src/draw_cpu.rs index 5646bd1c..bdb1d1ef 100644 --- a/clovers-cli/src/draw_cpu.rs +++ b/clovers-cli/src/draw_cpu.rs @@ -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::*; @@ -27,7 +26,7 @@ pub fn draw( render_options: &RenderOptions, scene: &Scene, _sampler: Sampler, -) -> Vec> { +) -> Vec> { let GlobalOptions { debug: _, quiet } = *global_options; let RenderOptions { input: _, @@ -39,13 +38,14 @@ pub fn draw( mode, sampler, bvh: _, + format: _, } = *render_options; let bar = progress_bar(height, quiet); let height = height as usize; let width = width as usize; - let pixelbuffer: Vec> = (0..height) + let pixelbuffer: Vec> = (0..height) .into_par_iter() .map(|row_index| { let mut sampler_rng = SmallRng::from_entropy(); @@ -99,7 +99,7 @@ fn render_pixel( index: usize, rng: &mut SmallRng, sampler: &mut dyn SamplerTrait, -) -> Srgb { +) -> Xyz { let (x, y, width, height) = index_to_params(opts, index); let pixel_location = Vec2::new(x, y); let canvas_size = Vec2::new(width, height); @@ -125,10 +125,7 @@ fn render_pixel( pixel_color += sample_color; } } - pixel_color /= opts.samples as Float; - let color: Srgb = pixel_color.adapt_into(); - let color: Srgb = color.into_format(); - color + pixel_color / opts.samples as Float } // Render a single pixel in normalmap mode @@ -137,7 +134,7 @@ fn render_pixel_normalmap( opts: &RenderOptions, index: usize, rng: &mut SmallRng, -) -> Srgb { +) -> Xyz { let (x, y, width, height) = index_to_params(opts, index); let color: LinSrgb = { let pixel_location = Vec2::new(x / width, y / height); @@ -149,9 +146,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 = color.into_format(); - color + color.adapt_into() } // Render a single pixel in bvh test count visualization mode @@ -161,7 +156,7 @@ fn render_pixel_bvhtestcount( index: usize, rng: &mut SmallRng, _sampler: &mut dyn SamplerTrait, -) -> Srgb { +) -> Xyz { 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); @@ -171,10 +166,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 = color.into_format(); - color + bvh_testcount(&ray, scene, rng) } // Render a single pixel in primitive test count visualization mode @@ -184,7 +176,7 @@ fn render_pixel_primitivetestcount( index: usize, rng: &mut SmallRng, _sampler: &mut dyn SamplerTrait, -) -> Srgb { +) -> Xyz { 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); @@ -194,10 +186,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 = color.into_format(); - color + primitive_testcount(&ray, scene, rng) } fn index_to_params(opts: &RenderOptions, index: usize) -> (Float, Float, Float, Float) { diff --git a/clovers-cli/src/lib.rs b/clovers-cli/src/lib.rs index e7cf3918..70bb10bf 100644 --- a/clovers-cli/src/lib.rs +++ b/clovers-cli/src/lib.rs @@ -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 diff --git a/clovers-cli/src/main.rs b/clovers-cli/src/main.rs index 074e1993..50caab8f 100644 --- a/clovers-cli/src/main.rs +++ b/clovers-cli/src/main.rs @@ -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)] diff --git a/clovers-cli/src/render.rs b/clovers-cli/src/render.rs index 4874c4d7..e061c4ac 100644 --- a/clovers-cli/src/render.rs +++ b/clovers-cli/src/render.rs @@ -1,11 +1,8 @@ -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; @@ -13,9 +10,10 @@ 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()] @@ -44,6 +42,9 @@ pub struct RenderOptions { /// BVH construction algorithm. #[arg(long, default_value = "sah")] pub bvh: BvhAlgorithm, + /// File format selection for the output. + #[arg(short, long, default_value = "png")] + pub format: Format, } #[derive(Copy, Clone, Debug, PartialEq, ValueEnum)] @@ -66,6 +67,14 @@ pub enum BvhAlgorithm { Sah, } +#[derive(Copy, Clone, Debug, PartialEq, ValueEnum)] +pub enum Format { + /// Portable Network Graphics, lossless SDR + Png, + /// AV1 Image File Format, lossy HDR + Avif, +} + // CLI usage somehow not detected #[allow(dead_code)] pub(crate) fn render( @@ -83,6 +92,7 @@ pub(crate) fn render( mode, sampler, bvh, + format, } = render_options; if debug { @@ -123,8 +133,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() { @@ -138,53 +146,17 @@ 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 = 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 extension = match format { + Format::Png => "png", + Format::Avif => "avif", }; - 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); - } let target = match output { Some(filename) => filename.to_owned(), @@ -192,12 +164,14 @@ pub(crate) fn render( // Default to using a timestamp & `renders/` directory let timestamp = OffsetDateTime::now_utc().unix_timestamp(); fs::create_dir_all("renders")?; - format!("renders/{}.png", timestamp) + format!("renders/{timestamp}.{extension}") } }; - let output = File::create(&target)?; - png.encoder().write_to(output)?; + match format { + Format::Png => write::png(pixelbuffer, &target, duration, render_options.clone()), + Format::Avif => todo!(), + }?; info!("Image saved to {}", target); println!("Image saved to: {}", target); diff --git a/clovers-cli/src/write.rs b/clovers-cli/src/write.rs new file mode 100644 index 00000000..46dcb7b0 --- /dev/null +++ b/clovers-cli/src/write.rs @@ -0,0 +1,79 @@ +use std::fs::File; +use std::io::Cursor; + +use humantime::FormattedDuration; +use image::{ImageBuffer, ImageFormat, Rgb, RgbImage}; +use img_parts::png::{Png, PngChunk}; +use palette::{chromatic_adaptation::AdaptInto, white_point::E, Srgb, Xyz}; +use tracing::info; + +use crate::render::{RenderMode, RenderOptions}; + +pub fn png( + pixelbuffer: Vec>, + target: &String, + duration: FormattedDuration, + render_options: RenderOptions, +) -> Result<(), String> { + let RenderOptions { + input, + output: _, + width, + height, + samples, + max_depth, + mode, + sampler: _, + bvh: _, + format: _, + } = render_options; + 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; + let color: Srgb = pixelbuffer[index as usize].adapt_into(); + let color: Srgb = color.into_format(); + *pixel = Rgb([color.red, color.green, color.blue]); + }); + + // 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 + + info!("Writing an image file"); + + let mut bytes: Vec = Vec::new(); + img.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png) + .or(Err("Unable to write bytes"))?; + let mut png = Png::from_bytes(bytes.into()).or(Err("Unable to write bytes"))?; + + 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 threads = + std::thread::available_parallelism().or(Err("Unable to detect available parallelism"))?; + let stats = format!("Rendering finished in {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); + } + + let output = File::create(target).or(Err("Unable to create file"))?; + png.encoder() + .write_to(output) + .or(Err("Unable to write to file"))?; + + Ok(()) +} From 983c7cbcf9d39d4057d9aa3fb100b9e0305e4226 Mon Sep 17 00:00:00 2001 From: Walther Date: Thu, 26 Dec 2024 14:26:33 +0100 Subject: [PATCH 2/4] feature: add OpenEXR as an output format --- Justfile | 2 +- clovers-cli/Cargo.toml | 5 ++- clovers-cli/src/draw_cpu.rs | 7 ++-- clovers-cli/src/render.rs | 63 ++++++++++++++++++---------------- clovers-cli/src/write.rs | 68 ++++++++++++++++++++++++++----------- 5 files changed, 92 insertions(+), 53 deletions(-) diff --git a/Justfile b/Justfile index f43bb86d..37bbce80 100644 --- a/Justfile +++ b/Justfile @@ -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 diff --git a/clovers-cli/Cargo.toml b/clovers-cli/Cargo.toml index 39716c64..bb4364b7 100644 --- a/clovers-cli/Cargo.toml +++ b/clovers-cli/Cargo.toml @@ -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", diff --git a/clovers-cli/src/draw_cpu.rs b/clovers-cli/src/draw_cpu.rs index bdb1d1ef..8cb69054 100644 --- a/clovers-cli/src/draw_cpu.rs +++ b/clovers-cli/src/draw_cpu.rs @@ -38,14 +38,17 @@ pub fn draw( mode, sampler, bvh: _, - format: _, + formats: _, } = *render_options; let bar = progress_bar(height, quiet); let height = height as usize; let width = width as usize; - let pixelbuffer: Vec> = (0..height) + // TODO: fix the coordinate system; this flips up<->down + let rows: Vec = (0..height).rev().collect(); + + let pixelbuffer: Vec> = rows .into_par_iter() .map(|row_index| { let mut sampler_rng = SmallRng::from_entropy(); diff --git a/clovers-cli/src/render.rs b/clovers-cli/src/render.rs index e061c4ac..f7fdfda4 100644 --- a/clovers-cli/src/render.rs +++ b/clovers-cli/src/render.rs @@ -16,9 +16,9 @@ use crate::GlobalOptions; #[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, /// Width of the image in pixels. @@ -43,8 +43,9 @@ pub struct RenderOptions { #[arg(long, default_value = "sah")] pub bvh: BvhAlgorithm, /// File format selection for the output. - #[arg(short, long, default_value = "png")] - pub format: Format, + /// 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, } #[derive(Copy, Clone, Debug, PartialEq, ValueEnum)] @@ -69,10 +70,10 @@ pub enum BvhAlgorithm { #[derive(Copy, Clone, Debug, PartialEq, ValueEnum)] pub enum Format { - /// Portable Network Graphics, lossless SDR + /// Portable Network Graphics, lossless, standard dynamic range Png, - /// AV1 Image File Format, lossy HDR - Avif, + /// OpenEXR, high dynamic range + Exr, } // CLI usage somehow not detected @@ -92,7 +93,7 @@ pub(crate) fn render( mode, sampler, bvh, - format, + ref formats, } = render_options; if debug { @@ -153,28 +154,30 @@ pub(crate) fn render( println!("Finished render in {}", duration); } - let extension = match format { - Format::Png => "png", - Format::Avif => "avif", - }; - - 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/{timestamp}.{extension}") - } - }; - - match format { - Format::Png => write::png(pixelbuffer, &target, duration, render_options.clone()), - Format::Avif => todo!(), - }?; - - info!("Image saved to {}", target); - println!("Image saved to: {}", target); + 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); + } Ok(()) } diff --git a/clovers-cli/src/write.rs b/clovers-cli/src/write.rs index 46dcb7b0..b5a1919e 100644 --- a/clovers-cli/src/write.rs +++ b/clovers-cli/src/write.rs @@ -1,19 +1,20 @@ use std::fs::File; use std::io::Cursor; +use clovers::Float; use humantime::FormattedDuration; -use image::{ImageBuffer, ImageFormat, Rgb, RgbImage}; +use image::{ImageBuffer, ImageFormat, Rgb32FImage, RgbImage}; use img_parts::png::{Png, PngChunk}; -use palette::{chromatic_adaptation::AdaptInto, white_point::E, Srgb, Xyz}; +use palette::{chromatic_adaptation::AdaptInto, white_point::E, Xyz}; use tracing::info; use crate::render::{RenderMode, RenderOptions}; pub fn png( - pixelbuffer: Vec>, + pixelbuffer: &[Xyz], target: &String, - duration: FormattedDuration, - render_options: RenderOptions, + duration: &FormattedDuration, + render_options: &RenderOptions, ) -> Result<(), String> { let RenderOptions { input, @@ -25,28 +26,25 @@ pub fn png( mode, sampler: _, bvh: _, - format: _, + formats: _, } = render_options; + info!("Converting pixelbuffer to an image"); - let mut img: RgbImage = ImageBuffer::new(width, height); + let mut img: RgbImage = ImageBuffer::new(*width, *height); img.enumerate_pixels_mut().for_each(|(x, y, pixel)| { let index = y * width + x; - let color: Srgb = pixelbuffer[index as usize].adapt_into(); - let color: Srgb = color.into_format(); - *pixel = Rgb([color.red, color.green, color.blue]); + let color: palette::Srgb = pixelbuffer[index as usize].adapt_into(); + let color: palette::Srgb = color.into_format(); + *pixel = image::Rgb([color.red, color.green, color.blue]); }); - // 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 - info!("Writing an image file"); - let mut bytes: Vec = Vec::new(); - img.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png) - .or(Err("Unable to write bytes"))?; - let mut png = Png::from_bytes(bytes.into()).or(Err("Unable to write bytes"))?; + match img.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png) { + Ok(it) => it, + Err(err) => return Err(err.to_string()), + }; + let mut png = Png::from_bytes(bytes.into()).or(Err("Unable to read bytes"))?; 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" @@ -77,3 +75,35 @@ pub fn png( Ok(()) } + +pub fn exr( + pixelbuffer: &[Xyz], + target: &String, + _duration: &FormattedDuration, + render_options: &RenderOptions, +) -> Result<(), String> { + let RenderOptions { + input: _, + output: _, + width, + height, + samples: _, + max_depth: _, + mode: _, + sampler: _, + bvh: _, + formats: _, + } = render_options; + // TODO: metadata? + + info!("Converting pixelbuffer to an image"); + let mut img: Rgb32FImage = ImageBuffer::new(*width, *height); + img.enumerate_pixels_mut().for_each(|(x, y, pixel)| { + let index = y * width + x; + let color: palette::Srgb = pixelbuffer[index as usize].adapt_into(); + *pixel = image::Rgb([color.red, color.green, color.blue]); + }); + + img.save_with_format(target, ImageFormat::OpenExr) + .or(Err("Unable to write to file".to_owned())) +} From 8ae1e4b08e2d1594ae67b043b12b4953fab01b23 Mon Sep 17 00:00:00 2001 From: Walther Date: Thu, 26 Dec 2024 14:47:11 +0100 Subject: [PATCH 3/4] chore: add BSD-3-Clause to allowed licenses --- deny.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/deny.toml b/deny.toml index 62aaff03..cfb272ec 100644 --- a/deny.toml +++ b/deny.toml @@ -91,6 +91,7 @@ ignore = [ allow = [ "MIT", "Apache-2.0", + "BSD-3-Clause", #"Apache-2.0 WITH LLVM-exception", ] # The confidence threshold for detecting a license from license text. From 7bf31d7a73a5b0dce91424cafab4b06bab18018f Mon Sep 17 00:00:00 2001 From: Walther Date: Thu, 26 Dec 2024 23:12:14 +0100 Subject: [PATCH 4/4] fix: OpenEXR output color space --- clovers-cli/src/write.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clovers-cli/src/write.rs b/clovers-cli/src/write.rs index b5a1919e..52e640e6 100644 --- a/clovers-cli/src/write.rs +++ b/clovers-cli/src/write.rs @@ -100,7 +100,8 @@ pub fn exr( let mut img: Rgb32FImage = ImageBuffer::new(*width, *height); img.enumerate_pixels_mut().for_each(|(x, y, pixel)| { let index = y * width + x; - let color: palette::Srgb = pixelbuffer[index as usize].adapt_into(); + // NOTE: EXR format expects linear rgb + let color: palette::LinSrgb = pixelbuffer[index as usize].adapt_into(); *pixel = image::Rgb([color.red, color.green, color.blue]); });