use std::fmt; use std::ops::AddAssign; use std::ops::Range; use std::path::Path; use std::path::PathBuf; use std::str; use std::sync; use std::sync::mpsc::sync_channel; use std::sync::mpsc::Receiver; use std::sync::mpsc::SyncSender; use std::sync::Arc; use std::sync::Mutex; use std::thread; use std::time; use core_affinity; use log::info; use log::trace; use num_cpus; use rand; use rand::Rng; use serde_derive::Serialize; use structopt::StructOpt; 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; use crate::texture::ConstantTexture; use crate::texture::EnvMap; use crate::vec3::Vec3; #[derive(Debug)] pub enum Model { BVH, Bench, Book, CornellBox, CornellSmoke, Final, Mandelbrot, PerlinDebug, Spheramid, Test, Tutorial, } impl Model { pub fn scene(&self, opt: &Opt) -> Scene { match self { Model::BVH => scenes::bvh::new(&opt), Model::Bench => scenes::bench::new(&opt), Model::Book => scenes::book::new(&opt), Model::CornellBox => scenes::cornell_box::new(&opt), Model::CornellSmoke => scenes::cornell_smoke::new(&opt), Model::Final => scenes::final_scene::new(&opt), Model::Mandelbrot => scenes::mandelbrot::new(&opt), Model::PerlinDebug => scenes::perlin_debug::new(&opt), Model::Spheramid => scenes::spheramid::new(&opt), Model::Test => scenes::test::new(&opt), Model::Tutorial => scenes::tutorial::new(&opt), } } } #[derive(Debug)] pub struct ModelParseError(String); impl fmt::Display for ModelParseError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "unknown model enum type '{}'", self.0) } } impl str::FromStr for Model { type Err = ModelParseError; fn from_str(s: &str) -> std::result::Result { match s { "bench" => Ok(Model::Bench), "book" => Ok(Model::Book), "bvh" => Ok(Model::BVH), "cornell_box" => Ok(Model::CornellBox), "cornell_smoke" => Ok(Model::CornellSmoke), "final" => Ok(Model::Final), "mandelbrot" => Ok(Model::Mandelbrot), "perlin_debug" => Ok(Model::PerlinDebug), "spheramid" => Ok(Model::Spheramid), "test" => Ok(Model::Test), "tutorial" => Ok(Model::Tutorial), _ => Err(ModelParseError(s.to_owned())), } } } impl std::string::ToString for Model { fn to_string(&self) -> String { match self { Model::BVH => "bvh".to_string(), Model::Bench => "bench".to_string(), Model::Book => "book".to_string(), Model::CornellBox => "cornell_box".to_string(), Model::CornellSmoke => "cornell_smoke".to_string(), Model::Final => "final".to_string(), Model::Mandelbrot => "mandelbrot".to_string(), Model::PerlinDebug => "perlin_debug".to_string(), Model::Spheramid => "spheramid".to_string(), Model::Test => "test".to_string(), Model::Tutorial => "tutorial".to_string(), } } } #[derive(Debug, StructOpt)] #[structopt(name = "tracer", about = "An experimental ray tracer.")] pub struct Opt { /// Image width #[structopt(short = "w", long = "width", default_value = "512")] pub width: usize, /// Image height #[structopt(short = "h", long = "height", default_value = "512")] pub height: usize, /// Number of threads #[structopt(short = "t", long = "num_threads")] pub num_threads: Option, /// Sub-samples per pixel #[structopt(short = "s", long = "subsample", default_value = "8")] pub subsamples: usize, /// Select scene to render, one of: "bench", "book", "tutorial", "bvh", "test", "cornell_box", /// "cornell_smoke", "perlin_debug", "final" #[structopt(long = "model", default_value = "book")] pub model: Model, /// Path to store pprof profile data, i.e. /tmp/cpuprofile.pprof #[structopt(long = "pprof", parse(from_os_str))] pub pprof: Option, /// Use acceleration data structure, may be BVH or kd-tree depending on scene. #[structopt(long = "use_accel")] pub use_accel: bool, /// Output directory #[structopt(parse(from_os_str), default_value = "/tmp/tracer")] pub output: PathBuf, } pub fn opt_hash(opt: &Opt) -> String { // TODO(wathiede): add threads. format!( "w:{}-h:{}-s:{}-pprof:{}-model:{}-use_accel:{}-{}", opt.width, opt.height, opt.subsamples, opt.pprof.is_some(), opt.model.to_string(), opt.use_accel, opt.output.display().to_string().replace("/", "_") ) } // TODO(wathiede): implement the skips and then the renderer could use json as an input file type. #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct Scene { #[serde(skip)] pub world: Box, #[serde(skip)] pub camera: Camera, pub subsamples: usize, /// overrides subsamples setting. pub adaptive_subsampling: Option, pub num_threads: Option, pub width: usize, pub height: usize, pub global_illumination: bool, #[serde(skip)] pub env_map: Option, } impl Default for Scene { fn default() -> Scene { let lookfrom = Vec3::new(20., 20., 20.); let lookat = Vec3::new(0., 0., 0.); let dist_to_focus = (lookfrom - lookat).length(); let aperture = 0.1; let time_min = 0.; let time_max = 1.; let camera = Camera::new( lookfrom, lookat, Vec3::new(0., 1., 0.), 70., 1., aperture, dist_to_focus, time_min, time_max, ); Scene { world: Box::new(Sphere::new( Vec3::new(0., 0., 0.), 1.0, Lambertian::new(ConstantTexture::new([0., 1., 0.])), )), camera, subsamples: 0, adaptive_subsampling: None, num_threads: None, width: 0, height: 0, global_illumination: false, env_map: None, } } } // color will trace ray up to 50 bounces deep accumulating color as it goes. If // global_illumination is true, a default light background color is assumed and will light the // world. If false, it is expected the scene has emissive light sources. fn color( r: Ray, world: &dyn Hit, depth: usize, global_illumination: bool, env_map: &Option, ) -> (Vec3, usize) { if let Some(rec) = world.hit(r, 0.001, std::f32::MAX) { let (u, v) = rec.uv; let emitted = rec.material.emitted(u, v, rec.p); let scatter_response = rec.material.scatter(&r, &rec); if depth < 50 && scatter_response.reflected { let (c, rays) = color( scatter_response.scattered, world, depth + 1, global_illumination, env_map, ); return (emitted + scatter_response.attenutation * c, rays + 1); } else { return (emitted, 1); } } if global_illumination { return match env_map { Some(env_map) => (env_map.color(r.direction.unit_vector()), 1), None => { let unit_direction = r.direction.unit_vector(); // No hit, choose color from background. let t = 0.5 * (unit_direction.y + 1.); ( Vec3::new(1., 1., 1.) * (1. - t) + Vec3::new(0.5, 0.7, 1.) * t, 1, ) } }; } // No global illumination, so background is black. (Vec3::new(0., 0., 0.), 1) } 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, usize) { 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, rays) = 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, rays); } // 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, rays) = [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(), 0), |(p1, r1): (Vec3, usize), (p2, r2): (Vec3, usize)| ((p1 + p2), (r1 + r2)), ); let corners = corners / 5.; if (corners - center).length() > threshold { let tl = trace_pixel_adaptive( depth - 1, threshold, x, y, x_range.start..x_mid, y_range.start..y_mid, scene, ); let tr = trace_pixel_adaptive( depth - 1, threshold, x, y, x_mid..x_range.end, y_range.start..y_mid, scene, ); let bl = trace_pixel_adaptive( depth - 1, threshold, x, y, x_range.start..x_mid, y_mid..y_range.end, scene, ); let br = trace_pixel_adaptive( depth - 1, threshold, x, y, x_mid..x_range.end, y_mid..y_range.end, scene, ); let pixel = (tl.0 + tr.0 + bl.0 + br.0) / 4.; let rays = tl.1 + tr.1 + bl.1 + br.1; (pixel, rays) } else { if depth == MAX_ADAPTIVE_DEPTH { output::set_pixel(output::ADAPTIVE_DEPTH, x, y, [0., 1., 0.].into()); } (corners, rays) } } fn trace_pixel_random(x: usize, y: usize, scene: &Scene) -> (Vec3, usize) { 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; let ray = scene.camera.get_ray(u, v); color( ray, scene.world.as_ref(), 0, scene.global_illumination, &scene.env_map, ) } #[derive(Clone, Copy)] struct RenderStats { rays: usize, pixels: usize, } impl AddAssign for RenderStats { fn add_assign(&mut self, other: Self) { *self = Self { rays: self.rays + other.rays, pixels: self.pixels + other.pixels, } } } fn progress( last_stat: &RenderStats, current_stat: &RenderStats, time_diff: time::Duration, pixel_total: usize, ) -> String { let human = human::Formatter::new(); let pixel_diff = current_stat.pixels - last_stat.pixels; let ray_diff = current_stat.rays - last_stat.rays; format!( "{:7} / {:7}pixels ({:2}%) {:7}pixels/s {:7}rays/s", human.format(current_stat.pixels as f64), human.format(pixel_total as f64), 100 * current_stat.pixels / pixel_total, human.format(pixel_diff as f64 / time_diff.as_secs_f64()), human.format(ray_diff as f64 / time_diff.as_secs_f64()) ) } impl Default for RenderStats { fn default() -> Self { RenderStats { rays: 0, pixels: 0 } } } enum Request { Pixel { x: usize, y: usize }, Line { width: usize, y: usize }, } enum Response { Pixel { x: usize, y: usize, pixel: Vec3, rs: RenderStats, }, Line { y: usize, pixels: Vec, rs: RenderStats, }, } fn render_pixel(scene: &Scene, x: usize, y: usize) -> (Vec3, usize) { let (pixel, rays) = 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 { let (pixel, rays) = (0..scene.subsamples) .map(|_| trace_pixel_random(x, y, scene)) .fold( ([0., 0., 0.].into(), 0), |(p1, r1): (Vec3, usize), (p2, r2): (Vec3, usize)| ((p1 + p2), (r1 + r2)), ); output::set_pixel_grey(output::RAYS_PER_PIXEL, x, y, rays as f32); (pixel / scene.subsamples as f32, rays) }; // Gamma correct, use gamma 2 correction, which is 1/gamma where gamma=2 which is 1/2 or // sqrt. ( Vec3::new(pixel[0].sqrt(), pixel[1].sqrt(), pixel[2].sqrt()), rays, ) } fn render_worker( tid: usize, scene: &Scene, input_chan: Arc>>, output_chan: &SyncSender, ) { loop { let job = { input_chan.lock().unwrap().recv() }; match job { Err(err) => { trace!("Shutting down render_worker {}: {}", tid, err); return; } Ok(req) => match req { Request::Line { width, y } => { trace!("tid {} width {} y {}", tid, width, y); let batch = false; if batch { let (pixels, rays): (Vec, Vec) = (0..width) .map(|x| render_pixel(scene, x, y)) .collect::>() .into_iter() .unzip(); let rays = rays.iter().sum(); output_chan .send(Response::Line { y, pixels, rs: RenderStats { rays, pixels: width, }, }) .expect("failed to send pixel response"); } else { (0..width).for_each(|x| { let (pixel, rays) = render_pixel(scene, x, y); output_chan .send(Response::Pixel { x, y, pixel, rs: RenderStats { rays, pixels: 1 }, }) .expect("failed to send pixel response"); }); } } Request::Pixel { x, y } => { trace!("tid {} x {} y {}", tid, x, y); let (pixel, rays) = render_pixel(scene, x, y); output_chan .send(Response::Pixel { x, y, pixel, rs: RenderStats { rays, pixels: 1 }, }) .expect("failed to send line response"); } }, } } } pub fn render(scene: Scene, output_dir: &Path) -> std::result::Result<(), std::io::Error> { let num_threads = scene.num_threads.unwrap_or_else(num_cpus::get); let (pixel_req_tx, pixel_req_rx) = sync_channel(2 * num_threads); let (pixel_resp_tx, pixel_resp_rx) = sync_channel(2 * num_threads); let scene = Arc::new(scene); let pixel_req_rx = Arc::new(Mutex::new(pixel_req_rx)); info!("Adaptive subsampling: {:?}", scene.adaptive_subsampling); // Retrieve the IDs of all active CPU cores. let core_ids = core_affinity::get_core_ids().unwrap(); let core_ids = if core_ids.len() > num_threads { core_ids[..num_threads].to_vec() } else { core_ids }; info!("Creating {} render threads", core_ids.len()); output::register_image( output::MAIN_IMAGE.to_string(), (scene.width, scene.height), output::ImageType::RGB01, ); if scene.adaptive_subsampling.is_some() { output::register_image( output::ADAPTIVE_DEPTH.to_string(), (scene.width, scene.height), output::ImageType::RGB01, ); } output::register_image( output::RAYS_PER_PIXEL.to_string(), (scene.width, scene.height), output::ImageType::GreyNormalized, ); // Create a thread for each active CPU core. let mut handles = core_ids .into_iter() .enumerate() .filter(|(i, _id)| *i < num_threads) .map(|(i, id)| { let s = sync::Arc::clone(&scene); let pixel_req_rx = pixel_req_rx.clone(); let pixel_resp_tx = pixel_resp_tx.clone(); thread::spawn(move || { core_affinity::set_for_current(id); render_worker(i, &s, pixel_req_rx, &pixel_resp_tx); }) }) .collect::>(); drop(pixel_req_rx); drop(pixel_resp_tx); let start_time = time::Instant::now(); let (w, h) = (scene.width, scene.height); handles.push(thread::spawn(move || { let batch_line_requests = true; if batch_line_requests { for y in 0..h { pixel_req_tx .send(Request::Line { width: w, y }) .expect("failed to send line request"); } } else { for y in 0..h { for x in 0..w { pixel_req_tx .send(Request::Pixel { x, y }) .expect("failed to send pixel request"); } } } drop(pixel_req_tx); })); info!("Rendering with {} subsamples", scene.subsamples); let pixel_total = scene.width * scene.height; let mut last_time = time::Instant::now(); let mut last_stat: RenderStats = Default::default(); let mut current_stat: RenderStats = Default::default(); for resp in pixel_resp_rx { match resp { Response::Pixel { x, y, pixel, rs } => { current_stat += rs; output::set_pixel(output::MAIN_IMAGE, x, y, pixel); } Response::Line { y, pixels, rs } => { current_stat += rs; for (x, pixel) in pixels.iter().enumerate() { output::set_pixel(output::MAIN_IMAGE, x, y, *pixel); } } } let now = time::Instant::now(); let time_diff = now - last_time; if time_diff > time::Duration::from_secs(1) { info!( "{}", progress(&last_stat, ¤t_stat, time_diff, pixel_total) ); last_stat = current_stat; last_time = now; } } for thr in handles { thr.join().expect("thread join"); } let time_diff = time::Instant::now() - start_time; info!( "Runtime {} seconds {}", time_diff.as_secs_f32(), progress(&Default::default(), ¤t_stat, time_diff, pixel_total) ); output::write_images(&scene, time_diff, output_dir) }