From 5d9e180817887ba69b4c6d67d71db4ff12707f68 Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Wed, 16 Oct 2019 20:21:16 -0700 Subject: [PATCH] First version of adaptive subsampling. Add debugging images, and move rendering to output module. --- rtiow/Cargo.lock | 19 ++-- rtiow/Cargo.toml | 1 + rtiow/src/lib.rs | 1 + rtiow/src/output.rs | 139 +++++++++++++++++++++++++++++ rtiow/src/renderer.rs | 168 +++++++++++++++++++++++++++-------- rtiow/src/scenes/book.rs | 2 +- rtiow/src/scenes/tutorial.rs | 6 +- 7 files changed, 286 insertions(+), 50 deletions(-) create mode 100644 rtiow/src/output.rs diff --git a/rtiow/Cargo.lock b/rtiow/Cargo.lock index 9df414c..1e799f4 100644 --- a/rtiow/Cargo.lock +++ b/rtiow/Cargo.lock @@ -96,7 +96,7 @@ dependencies = [ "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", "serde_urlencoded 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", "sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -428,7 +428,7 @@ dependencies = [ "rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", "tinytemplate 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.2.7 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1566,7 +1566,7 @@ dependencies = [ "mime_guess 2.0.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)", "native-tls 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", "serde_urlencoded 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1614,6 +1614,7 @@ dependencies = [ "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", "stderrlog 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1641,7 +1642,7 @@ dependencies = [ [[package]] name = "ryu" -version = "0.2.7" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1732,11 +1733,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.38" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1926,7 +1927,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2593,7 +2594,7 @@ dependencies = [ "checksum rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "adacaae16d02b6ec37fdc7acfcddf365978de76d1983d3ee22afc260e1ca9619" "checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -"checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7" +"checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" "checksum safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dca453248a96cb0749e36ccdfe2b0b4e54a61bfef89fb97ec621eb8e0a93dd9" "checksum same-file 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8f20c4be53a8a1ff4c1f1b2bd14570d2f634628709752f0702ecdd2b3f9a5267" "checksum schannel 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "0e1a231dc10abf6749cfa5d7767f25888d484201accbd919b66ab5413c502d56" @@ -2606,7 +2607,7 @@ dependencies = [ "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" "checksum serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)" = "9f301d728f2b94c9a7691c90f07b0b4e8a4517181d9461be94c04bddeb4bd850" "checksum serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)" = "beed18e6f5175aef3ba670e57c60ef3b1b74d250d962a26604bff4c80e970dd4" -"checksum serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "27dce848e7467aa0e2fcaf0a413641499c0b745452aaca1194d24dedde9e13c9" +"checksum serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)" = "2f72eb2a68a7dc3f9a691bfda9305a1c017a6215e5a4545c258500d2099a37c2" "checksum serde_urlencoded 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d48f9f99cd749a2de71d29da5f948de7f2764cc5a9d7f3c97e3514d4ee6eabf2" "checksum sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" "checksum signal-hook 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1f272d1b7586bec132ed427f532dd418d8beca1ca7f2caf7df35569b1415a4b4" diff --git a/rtiow/Cargo.toml b/rtiow/Cargo.toml index 1a8c06c..62e5af3 100644 --- a/rtiow/Cargo.toml +++ b/rtiow/Cargo.toml @@ -20,6 +20,7 @@ num_cpus = "1.8.0" rand = "0.5.5" serde = "1.0.79" serde_derive = "1.0.79" +serde_json = "1.0.41" stderrlog = "0.4.1" structopt = "0.2.10" diff --git a/rtiow/src/lib.rs b/rtiow/src/lib.rs index 7b3eb8b..5607118 100644 --- a/rtiow/src/lib.rs +++ b/rtiow/src/lib.rs @@ -11,6 +11,7 @@ pub mod kdtree; pub mod material; pub mod moving_sphere; pub mod noise; +pub mod output; pub mod ray; pub mod rect; pub mod renderer; diff --git a/rtiow/src/output.rs b/rtiow/src/output.rs new file mode 100644 index 0000000..9994be3 --- /dev/null +++ b/rtiow/src/output.rs @@ -0,0 +1,139 @@ +use std::collections::HashMap; +use std::fs::File; +use std::path::Path; +use std::sync::Arc; +use std::sync::Mutex; + +use chrono::Local; +use image::RgbImage; +use lazy_static::lazy_static; +use log::info; + +use crate::vec3::Vec3; + +// Main RGB image output from rendering the scene. +pub const MAIN_IMAGE: &str = "@final"; +// Debug image for adaptive pixels subsampling. +// Red indicates recursion hit maximum depth splitting the pixel. +// Green indicates no subdivision necessary +pub const ADAPTIVE_DEPTH: &str = "adaptive_depth"; + +lazy_static! { + static ref DEBUGGER: Arc> = Arc::new(Mutex::new(Debugger::new())); +} + +struct Debugger { + images: HashMap, +} + +impl Debugger { + fn new() -> Debugger { + Debugger { + images: HashMap::new(), + } + } +} + +pub fn register_image(name: String, dimensions: (u32, u32)) { + let mut debugger = DEBUGGER.lock().unwrap(); + debugger + .images + .insert(name, RgbImage::new(dimensions.0, dimensions.1)); +} + +pub fn set_pixel(name: &str, x: usize, y: usize, pixel: Vec3) { + let mut debugger = DEBUGGER.lock().unwrap(); + let img = debugger + .images + .get_mut(name) + .expect(&format!("couldn't find image named '{}'", name)); + let y_inv = img.height() - y as u32 - 1; + img.put_pixel( + x as u32, + y_inv as u32, + image::Rgb([ + (pixel[0] * 255.).min(255.) as u8, + (pixel[1] * 255.).min(255.) as u8, + (pixel[2] * 255.).min(255.) as u8, + ]), + ); +} + +pub fn write_images>(output_dir: P) -> std::io::Result<()> { + let output_dir: &Path = output_dir.as_ref(); + let debugger = DEBUGGER.lock().unwrap(); + let now = Local::now(); + /* + let style = r#" + + "#; + writeln!(f, "{}", style)?; + writeln!( + f, + "

Run @ {} ({})

", + now.to_rfc2822(), + now.timestamp() + )?; + */ + // Write out images in consistent order. + let mut names = debugger.images.keys().collect::>(); + names.sort(); + let mut ratio = 1.; + let mut size = (0, 0); + for name in &names { + let img = debugger.images.get(*name).unwrap(); + if *name == MAIN_IMAGE { + let (w, h) = img.dimensions(); + ratio = w as f32 / h as f32; + size = img.dimensions(); + } + let filename = format!("{}.png", name); + /* + writeln!( + f, + r#"
{1} ({2})
"#, + filename, name, now.timestamp())?; + */ + let path = output_dir.join(filename); + info!("Saving {}", path.to_string_lossy()); + img.save(path)?; + } + let f = File::create(output_dir.join("data.json"))?; + #[derive(Serialize)] + struct Data { + timestamp: i64, + ratio: f32, + size: (u32, u32), + names: Vec, + images: Vec, + } + serde_json::ser::to_writer( + f, + &Data { + timestamp: now.timestamp(), + ratio, + size, + names: names.iter().map(|s| s.to_string()).collect(), + images: names.iter().map(|s| format!("{}.png", s)).collect(), + }, + )?; + Ok(()) +} diff --git a/rtiow/src/renderer.rs b/rtiow/src/renderer.rs index d11e480..478f820 100644 --- a/rtiow/src/renderer.rs +++ b/rtiow/src/renderer.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::ops::Range; use std::path::Path; use std::path::PathBuf; use std::str; @@ -13,8 +14,6 @@ use std::sync::Mutex; use std::thread; use std::time; -use image; -use image::RgbImage; #[cfg(feature = "prom")] use lazy_static::lazy_static; use num_cpus; @@ -25,6 +24,7 @@ use crate::camera::Camera; use crate::hitable::Hit; use crate::human; use crate::material::Lambertian; +use crate::output; use crate::ray::Ray; use crate::scenes; use crate::sphere::Sphere; @@ -150,9 +150,6 @@ pub struct Opt { /// Sub-samples per pixel #[structopt(short = "s", long = "subsample", default_value = "8")] pub subsamples: usize, - /// Use adaptive subsampling - #[structopt(long = "adaptive")] - pub adaptive_subsampling: bool, /// Select scene to render, one of: "bench", "book", "tutorial", "bvh", "test", "cornell_box", /// "cornell_smoke", "perlin_debug", "final" #[structopt(long = "model", default_value = "book")] @@ -188,7 +185,7 @@ pub struct Scene { pub camera: Camera, pub subsamples: usize, /// overrides subsamples setting. - pub adaptive_subsampling: bool, + pub adaptive_subsampling: Option, pub num_threads: Option, pub width: usize, pub height: usize, @@ -223,7 +220,7 @@ impl Default for Scene { )), camera, subsamples: 0, - adaptive_subsampling: false, + adaptive_subsampling: None, num_threads: None, width: 0, height: 0, @@ -278,7 +275,103 @@ fn color( Vec3::new(0., 0., 0.) } -fn trace_pixel(x: usize, y: usize, scene: &Scene) -> Vec3 { +const MAX_ADAPTIVE_DEPTH: usize = 10; +fn trace_pixel_adaptive( + depth: usize, + threshold: f32, + x: usize, + y: usize, + x_range: Range, + y_range: Range, + scene: &Scene, +) -> Vec3 { + let w = scene.width as f32; + let h = scene.height as f32; + let x_mid = x_range.start + ((x_range.end - x_range.start) / 2.); + let y_mid = y_range.start + ((y_range.end - y_range.start) / 2.); + let mc = ((x_mid + x as f32) / w, (y_mid + y as f32) / h); + let center = color( + scene.camera.get_ray(mc.0, mc.1), + scene.world.as_ref(), + 0, + scene.global_illumination, + &scene.env_map, + ); + if depth == 0 { + output::set_pixel(output::ADAPTIVE_DEPTH, x, y, [1., 0., 0.].into()); + return center; + } + // t = top + // m = middle + // b = bottom + // l = left + // c = center + // r = right + let tl = ( + (x_range.start + x as f32) / w, + (y_range.start + y as f32) / h, + ); + let tr = ((x_range.end + x as f32) / w, (y_range.start + y as f32) / h); + let bl = ((x_range.start + x as f32) / w, (y_range.end + y as f32) / h); + let br = ((x_range.end + x as f32) / w, (y_range.end + y as f32) / h); + + let corners = [tl, tr, mc, bl, br] + .iter() + .map(|(u, v)| { + color( + scene.camera.get_ray(*u, *v), + scene.world.as_ref(), + 0, + scene.global_illumination, + &scene.env_map, + ) + }) + .fold([0., 0., 0.].into(), |a: Vec3, b: Vec3| a + b) + / 5.; + if (corners - center).length() > threshold { + let pixel = (trace_pixel_adaptive( + depth - 1, + threshold, + x, + y, + x_range.start..x_mid, + y_range.start..y_mid, + scene, + ) + trace_pixel_adaptive( + depth - 1, + threshold, + x, + y, + x_mid..x_range.end, + y_range.start..y_mid, + scene, + ) + trace_pixel_adaptive( + depth - 1, + threshold, + x, + y, + x_range.start..x_mid, + y_mid..y_range.end, + scene, + ) + trace_pixel_adaptive( + depth - 1, + threshold, + x, + y, + x_mid..x_range.end, + y_mid..y_range.end, + scene, + )) / 4.; + pixel + } else { + if depth == MAX_ADAPTIVE_DEPTH { + output::set_pixel(output::ADAPTIVE_DEPTH, x, y, [0., 1., 0.].into()); + } + corners + } +} + +fn trace_pixel_random(x: usize, y: usize, scene: &Scene) -> Vec3 { let mut rng = rand::thread_rng(); let u = (rng.gen_range::(0., 1.) + x as f32) / scene.width as f32; let v = (rng.gen_range::(0., 1.) + y as f32) / scene.height as f32; @@ -306,17 +399,25 @@ static PIXEL_COUNT: AtomicUsize = AtomicUsize::new(0); fn render_pixel(scene: &Scene, x: usize, y: usize) -> Vec3 { let mut pixel: Vec3 = Default::default(); - let pixel = if scene.adaptive_subsampling { - Default::default() + let pixel = if let Some(threshold) = scene.adaptive_subsampling { + trace_pixel_adaptive( + MAX_ADAPTIVE_DEPTH, + threshold, + x, + y, + 0.0..1.0, + 0.0..1.0, + scene, + ) } else { for _ in 0..scene.subsamples { - pixel = pixel + trace_pixel(x, y, scene); + pixel = pixel + trace_pixel_random(x, y, scene); } pixel / scene.subsamples as f32 }; + PIXEL_COUNT.fetch_add(1, Ordering::SeqCst); // Gamma correct, use gamma 2 correction, which is 1/gamma where gamma=2 which is 1/2 or // sqrt. - PIXEL_COUNT.fetch_add(1, Ordering::SeqCst); Vec3::new(pixel[0].sqrt(), pixel[1].sqrt(), pixel[2].sqrt()) } @@ -361,7 +462,7 @@ pub fn render(scene: Scene, output_dir: &Path) -> std::result::Result<(), std::i let scene = Arc::new(scene); let pixel_req_rx = Arc::new(Mutex::new(pixel_req_rx)); info!("Creating {} render threads", num_threads); - info!("Adaptive subsampling: {}", scene.adaptive_subsampling); + info!("Adaptive subsampling: {:?}", scene.adaptive_subsampling); for i in 0..num_threads { let s = sync::Arc::clone(&scene); let pixel_req_rx = pixel_req_rx.clone(); @@ -394,6 +495,18 @@ pub fn render(scene: Scene, output_dir: &Path) -> std::result::Result<(), std::i drop(pixel_req_tx); }); + info!("Rendering with {} subsamples", scene.subsamples); + output::register_image( + output::MAIN_IMAGE.to_string(), + (scene.width as u32, scene.height as u32), + ); + if scene.adaptive_subsampling.is_some() { + output::register_image( + output::ADAPTIVE_DEPTH.to_string(), + (scene.width as u32, scene.height as u32), + ); + } + let pixel_total = scene.width * scene.height; thread::spawn(move || { let mut last_time = time::Instant::now(); @@ -423,40 +536,17 @@ pub fn render(scene: Scene, output_dir: &Path) -> std::result::Result<(), std::i } }); - info!("Rendering with {} subsamples", scene.subsamples); - let mut img = RgbImage::new(scene.width as u32, scene.height as u32); for resp in pixel_resp_rx { match resp { Response::Pixel { x, y, pixel } => { - let y_inv = scene.height - y - 1; - img.put_pixel( - x as u32, - y_inv as u32, - image::Rgb([ - (pixel[0] * 255.).min(255.) as u8, - (pixel[1] * 255.).min(255.) as u8, - (pixel[2] * 255.).min(255.) as u8, - ]), - ); + output::set_pixel(output::MAIN_IMAGE, x, y, pixel); } Response::Line { y, pixels } => { for (x, pixel) in pixels.iter().enumerate() { - let y_inv = scene.height - y - 1; - img.put_pixel( - x as u32, - y_inv as u32, - image::Rgb([ - (pixel[0] * 255.).min(255.) as u8, - (pixel[1] * 255.).min(255.) as u8, - (pixel[2] * 255.).min(255.) as u8, - ]), - ); + output::set_pixel(output::MAIN_IMAGE, x, y, *pixel); } } } } - let path = output_dir.join("final.png"); - // Write the contents of this image to the Writer in PNG format. - trace!(target: "renderer", "Saving {}", path.display()); - img.save(path) + output::write_images(output_dir) } diff --git a/rtiow/src/scenes/book.rs b/rtiow/src/scenes/book.rs index 649bcad..258d99b 100644 --- a/rtiow/src/scenes/book.rs +++ b/rtiow/src/scenes/book.rs @@ -51,7 +51,7 @@ pub fn new(opt: &Opt) -> Scene { camera, world, subsamples: opt.subsamples, - adaptive_subsampling: opt.adaptive_subsampling, + adaptive_subsampling: Some(0.5), num_threads: opt.num_threads, width: opt.width, height: opt.height, diff --git a/rtiow/src/scenes/tutorial.rs b/rtiow/src/scenes/tutorial.rs index 80e6021..e2b15f2 100644 --- a/rtiow/src/scenes/tutorial.rs +++ b/rtiow/src/scenes/tutorial.rs @@ -2,6 +2,7 @@ use crate::camera::Camera; use crate::hitable::Hit; use crate::hitable_list::HitableList; use crate::kdtree::KDTree; +use crate::material::Dielectric; use crate::material::Lambertian; use crate::material::Metal; use crate::moving_sphere::MovingSphere; @@ -12,7 +13,7 @@ use crate::texture::ConstantTexture; use crate::vec3::Vec3; pub fn new(opt: &Opt) -> Scene { - let lookfrom = Vec3::new(3., 3., 2.); + let lookfrom = Vec3::new(3., 2., 2.); let lookat = Vec3::new(0., 0., -1.); let dist_to_focus = (lookfrom - lookat).length(); let aperture = 0.1; @@ -37,6 +38,7 @@ pub fn new(opt: &Opt) -> Scene { let objects: Vec> = vec![ //let world: Box = Box::new(HitableList::new(vec![ + Box::new(Sphere::new([1., 0., 0.], 0.5, Dielectric::new(1.5))), Box::new(Sphere::new( Vec3::new(0., 0., -1.), 0.5, @@ -70,6 +72,8 @@ pub fn new(opt: &Opt) -> Scene { camera, world, subsamples: opt.subsamples, + // adaptive_subsampling: None, + adaptive_subsampling: Some(0.5), num_threads: opt.num_threads, width: opt.width, height: opt.height,