use std::{ cell::Cell, cmp, cmp::min, io::{self, Write}, sync::{Mutex, RwLock}, }; use crossbeam::sync::MsQueue; use scoped_threadpool::Pool; use glam::Vec4; use crate::{ accel::ACCEL_NODE_RAY_TESTS, color::{map_0_1_to_wavelength, SpectralSample, XYZ}, fp_utils::robust_ray_origin, hash::hash_u32, hilbert, image::Image, math::probit, mis::power_heuristic, ray::{Ray, RayBatch}, scene::{Scene, SceneLightSample}, surface, timer::Timer, tracer::Tracer, transform_stack::TransformStack, }; #[derive(Debug)] pub struct Renderer<'a> { pub output_file: String, pub resolution: (usize, usize), pub spp: usize, pub seed: u32, pub scene: Scene<'a>, } #[derive(Debug, Copy, Clone)] pub struct RenderStats { pub trace_time: f64, pub accel_node_visits: u64, pub ray_count: u64, pub initial_ray_generation_time: f64, pub ray_generation_time: f64, pub sample_writing_time: f64, pub total_time: f64, } impl RenderStats { fn new() -> RenderStats { RenderStats { trace_time: 0.0, accel_node_visits: 0, ray_count: 0, initial_ray_generation_time: 0.0, ray_generation_time: 0.0, sample_writing_time: 0.0, total_time: 0.0, } } fn collect(&mut self, other: RenderStats) { self.trace_time += other.trace_time; self.accel_node_visits += other.accel_node_visits; self.ray_count += other.ray_count; self.initial_ray_generation_time += other.initial_ray_generation_time; self.ray_generation_time += other.ray_generation_time; self.sample_writing_time += other.sample_writing_time; self.total_time += other.total_time; } } impl<'a> Renderer<'a> { pub fn render( &self, max_samples_per_bucket: u32, crop: Option<(u32, u32, u32, u32)>, thread_count: u32, do_blender_output: bool, ) -> (Image, RenderStats) { let mut tpool = Pool::new(thread_count); let image = Image::new(self.resolution.0, self.resolution.1); let (img_width, img_height) = (image.width(), image.height()); let all_jobs_queued = RwLock::new(false); let collective_stats = RwLock::new(RenderStats::new()); // Set up job queue let job_queue = MsQueue::new(); // For printing render progress let pixels_rendered = Mutex::new(Cell::new(0)); // Calculate dimensions and coordinates of what we're rendering. This // accounts for cropping. let (width, height, start_x, start_y) = if let Some((x1, y1, x2, y2)) = crop { let x1 = min(x1 as usize, img_width - 1); let y1 = min(y1 as usize, img_height - 1); let x2 = min(x2 as usize, img_width - 1); let y2 = min(y2 as usize, img_height - 1); (x2 - x1 + 1, y2 - y1 + 1, x1, y1) } else { (img_width, img_height, 0, 0) }; // Render tpool.scoped(|scope| { // Spawn worker tasks for _ in 0..thread_count { let jq = &job_queue; let ajq = &all_jobs_queued; let img = ℑ let pixrenref = &pixels_rendered; let cstats = &collective_stats; scope.execute(move || { self.render_job( jq, ajq, img, width * height, pixrenref, cstats, do_blender_output, ) }); } // Print initial 0.00% progress print!("0.00%"); let _ = io::stdout().flush(); // Determine bucket size based on the per-thread maximum number of samples to // calculate at a time. let (bucket_w, bucket_h) = { let target_pixels_per_bucket = max_samples_per_bucket as f64 / self.spp as f64; let target_bucket_dim = if target_pixels_per_bucket.sqrt() < 1.0 { 1usize } else { target_pixels_per_bucket.sqrt() as usize }; (target_bucket_dim, target_bucket_dim) }; // Populate job queue let bucket_n = { let bucket_count_x = ((width / bucket_w) + 1) as u32; let bucket_count_y = ((height / bucket_h) + 1) as u32; let larger = cmp::max(bucket_count_x, bucket_count_y); let pow2 = larger.next_power_of_two(); pow2 * pow2 }; for hilbert_d in 0..bucket_n { let (bx, by) = hilbert::d2xy(hilbert_d); let x = bx as usize * bucket_w; let y = by as usize * bucket_h; let w = if width >= x { min(bucket_w, width - x) } else { bucket_w }; let h = if height >= y { min(bucket_h, height - y) } else { bucket_h }; if x < width && y < height && w > 0 && h > 0 { job_queue.push(BucketJob { x: (start_x + x) as u32, y: (start_y + y) as u32, w: w as u32, h: h as u32, }); } } // Mark done queuing jobs *all_jobs_queued.write().unwrap() = true; }); // Clear percentage progress print print!("\r \r",); // Return the rendered image and stats return (image, *collective_stats.read().unwrap()); } /// Waits for buckets in the job queue to render and renders them when available. fn render_job( &self, job_queue: &MsQueue, all_jobs_queued: &RwLock, image: &Image, total_pixels: usize, pixels_rendered: &Mutex>, collected_stats: &RwLock, do_blender_output: bool, ) { let mut stats = RenderStats::new(); let mut timer = Timer::new(); let mut total_timer = Timer::new(); let mut paths = Vec::new(); let mut rays = RayBatch::new(); let mut tracer = Tracer::from_assembly(&self.scene.root); let mut xform_stack = TransformStack::new(); // Pre-calculate some useful values related to the image plane let cmpx = 1.0 / self.resolution.0 as f32; let cmpy = 1.0 / self.resolution.1 as f32; let min_x = -1.0; let max_x = 1.0; let min_y = -(self.resolution.1 as f32 / self.resolution.0 as f32); let max_y = self.resolution.1 as f32 / self.resolution.0 as f32; let x_extent = max_x - min_x; let y_extent = max_y - min_y; // Render 'render_loop: loop { paths.clear(); rays.clear(); // Get bucket, or exit if no more jobs left let bucket: BucketJob; loop { if let Some(b) = job_queue.try_pop() { bucket = b; break; } else if *all_jobs_queued.read().unwrap() { break 'render_loop; } } timer.tick(); // Generate light paths and initial rays for y in bucket.y..(bucket.y + bucket.h) { for x in bucket.x..(bucket.x + bucket.w) { for si in 0..self.spp { // Raw sample numbers. let (d0, d1, d2, d3) = get_sample_4d(si as u32, 0, (x, y), self.seed); let (d4, _, _, _) = get_sample_4d(si as u32, 1, (x, y), self.seed); // Calculate image plane x and y coordinates let (img_x, img_y) = { let filter_x = probit(d3, 2.0 / 6.0) + 0.5; let filter_y = probit(d4, 2.0 / 6.0) + 0.5; let samp_x = (filter_x + x as f32) * cmpx; let samp_y = (filter_y + y as f32) * cmpy; ((samp_x - 0.5) * x_extent, (0.5 - samp_y) * y_extent) }; // Create the light path and initial ray for this sample let (path, ray) = LightPath::new( &self.scene, self.seed, (x, y), (img_x, img_y), (d1, d2), d0, map_0_1_to_wavelength(golden_ratio_sample( si as u32, hash_u32((x << 16) ^ y, self.seed), )), si as u32, ); paths.push(path); rays.push(ray, false); } } } stats.initial_ray_generation_time += timer.tick() as f64; // Trace the paths! let mut pi = paths.len(); while pi > 0 { // Test rays against scene let isects = tracer.trace(&mut rays); stats.trace_time += timer.tick() as f64; // Determine next rays to shoot based on result let mut new_end = 0; for i in 0..pi { if paths[i].next(&mut xform_stack, &self.scene, &isects[i], &mut rays, i) { paths.swap(new_end, i); rays.swap(new_end, i); new_end += 1; } } rays.truncate(new_end); pi = new_end; stats.ray_generation_time += timer.tick() as f64; } { // Calculate color based on ray hits and save to image let min = (bucket.x, bucket.y); let max = (bucket.x + bucket.w, bucket.y + bucket.h); let mut img_bucket = image.get_bucket(min, max); for path in &paths { let path_col = SpectralSample::from_parts(path.color, path.wavelength); let mut col = img_bucket.get(path.pixel_co.0, path.pixel_co.1); col += XYZ::from_spectral_sample(&path_col) / self.spp as f32; img_bucket.set(path.pixel_co.0, path.pixel_co.1, col); } stats.sample_writing_time += timer.tick() as f64; // Pre-calculate base64 encoding if needed let base64_enc = if do_blender_output { use crate::color::xyz_to_rec709_e; Some(img_bucket.rgba_base64(xyz_to_rec709_e)) } else { None }; // Print render progress, and image data if doing blender output let guard = pixels_rendered.lock().unwrap(); let mut pr = (*guard).get(); let percentage_old = pr as f64 / total_pixels as f64 * 100.0; pr += bucket.w as usize * bucket.h as usize; (*guard).set(pr); let percentage_new = pr as f64 / total_pixels as f64 * 100.0; let old_string = format!("{:.2}%", percentage_old); let new_string = format!("{:.2}%", percentage_new); if let Some(bucket_data) = base64_enc { // If doing Blender output println!("DIV"); println!("{}", new_string); println!("{} {} {} {}", min.0, min.1, max.0, max.1); println!("{}", bucket_data); println!("BUCKET_END"); println!("DIV"); } else { // If doing console output if new_string != old_string { print!("\r{}", new_string); } } let _ = io::stdout().flush(); } } stats.total_time += total_timer.tick() as f64; stats.ray_count = tracer.rays_traced(); ACCEL_NODE_RAY_TESTS.with(|anv| { stats.accel_node_visits = anv.get(); anv.set(0); }); // Collect stats collected_stats.write().unwrap().collect(stats); } } #[derive(Debug)] enum LightPathEvent { CameraRay, BounceRay, ShadowRay, } #[derive(Debug)] pub struct LightPath { event: LightPathEvent, bounce_count: u32, sampling_seed: u32, pixel_co: (u32, u32), sample_number: u32, // Which sample in the LDS sequence this is. dim_offset: u32, time: f32, wavelength: f32, next_bounce_ray: Option, next_attenuation_fac: Vec4, closure_sample_pdf: f32, light_attenuation: Vec4, pending_color_addition: Vec4, color: Vec4, } #[allow(clippy::new_ret_no_self)] impl LightPath { fn new( scene: &Scene, sampling_seed: u32, pixel_co: (u32, u32), image_plane_co: (f32, f32), lens_uv: (f32, f32), time: f32, wavelength: f32, sample_number: u32, ) -> (LightPath, Ray) { ( LightPath { event: LightPathEvent::CameraRay, bounce_count: 0, sampling_seed: sampling_seed ^ 0x40d4682b, pixel_co: pixel_co, sample_number: sample_number, dim_offset: 0, time: time, wavelength: wavelength, next_bounce_ray: None, next_attenuation_fac: Vec4::splat(1.0), closure_sample_pdf: 1.0, light_attenuation: Vec4::splat(1.0), pending_color_addition: Vec4::splat(0.0), color: Vec4::splat(0.0), }, scene.camera.generate_ray( image_plane_co.0, image_plane_co.1, time, wavelength, lens_uv.0, lens_uv.1, ), ) } fn next_lds_sequence(&mut self) { self.dim_offset = 0; self.sampling_seed += 1; } fn next_lds_samp(&mut self) -> (f32, f32, f32, f32) { let dimension = self.dim_offset; self.dim_offset += 1; get_sample_4d( self.sample_number, dimension, self.pixel_co, self.sampling_seed, ) } fn next( &mut self, xform_stack: &mut TransformStack, scene: &Scene, isect: &surface::SurfaceIntersection, rays: &mut RayBatch, ray_idx: usize, ) -> bool { match self.event { //-------------------------------------------------------------------- // Result of Camera or bounce ray, prepare next bounce and light rays LightPathEvent::CameraRay | LightPathEvent::BounceRay => { if let surface::SurfaceIntersection::Hit { intersection_data: ref idata, ref closure, } = *isect { // Hit something! Do the stuff // If it's an emission closure, handle specially: // - Collect light from the emission. // - Terminate the path. use crate::shading::surface_closure::SurfaceClosure; if let SurfaceClosure::Emit(color) = *closure { let color = color.to_spectral_sample(self.wavelength).e; if let LightPathEvent::CameraRay = self.event { self.color += color; } else { let mis_pdf = power_heuristic(self.closure_sample_pdf, idata.sample_pdf); self.color += color * self.light_attenuation / mis_pdf; }; return false; } // Roll the previous closure pdf into the attenauation self.light_attenuation /= self.closure_sample_pdf; // Prepare light ray self.next_lds_sequence(); let (light_n, d2, d3, d4) = self.next_lds_samp(); let light_uvw = (d2, d3, d4); xform_stack.clear(); let light_info = scene.sample_lights( xform_stack, light_n, light_uvw, self.wavelength, self.time, isect, ); let found_light = if light_info.is_none() || light_info.pdf() <= 0.0 || light_info.selection_pdf() <= 0.0 { false } else { let light_pdf = light_info.pdf(); let light_sel_pdf = light_info.selection_pdf(); // Calculate the shadow ray and surface closure stuff let (attenuation, closure_pdf, shadow_ray) = match light_info { SceneLightSample::None => unreachable!(), // Distant light SceneLightSample::Distant { direction, .. } => { let (attenuation, closure_pdf) = closure.evaluate( rays.dir(ray_idx), direction, idata.nor, idata.nor_g, self.wavelength, ); let shadow_ray = { // Calculate the shadow ray for testing if the light is // in shadow or not. let offset_pos = robust_ray_origin( idata.pos, idata.pos_err, idata.nor_g.normalized(), direction, ); Ray { orig: offset_pos, dir: direction, time: self.time, wavelength: self.wavelength, max_t: std::f32::INFINITY, } }; (attenuation, closure_pdf, shadow_ray) } // Surface light SceneLightSample::Surface { sample_geo, .. } => { let dir = sample_geo.0 - idata.pos; let (attenuation, closure_pdf) = closure.evaluate( rays.dir(ray_idx), dir, idata.nor, idata.nor_g, self.wavelength, ); let shadow_ray = { // Calculate the shadow ray for testing if the light is // in shadow or not. let offset_pos = robust_ray_origin( idata.pos, idata.pos_err, idata.nor_g.normalized(), dir, ); let offset_end = robust_ray_origin( sample_geo.0, sample_geo.2, sample_geo.1.normalized(), -dir, ); Ray { orig: offset_pos, dir: offset_end - offset_pos, time: self.time, wavelength: self.wavelength, max_t: 1.0, } }; (attenuation, closure_pdf, shadow_ray) } }; // If there's any possible contribution, set up for a // light ray. if attenuation.e.max_element() <= 0.0 { false } else { // Calculate and store the light that will be contributed // to the film plane if the light is not in shadow. let light_mis_pdf = power_heuristic(light_pdf, closure_pdf); self.pending_color_addition = light_info.color().e * attenuation.e * self.light_attenuation / (light_mis_pdf * light_sel_pdf); rays.set_from_ray(&shadow_ray, true, ray_idx); true } }; // Prepare bounce ray let do_bounce = if self.bounce_count < 2 { self.bounce_count += 1; // Sample closure let (dir, filter, pdf) = { self.next_lds_sequence(); let (u, v, _, _) = self.next_lds_samp(); closure.sample( idata.incoming, idata.nor, idata.nor_g, (u, v), self.wavelength, ) }; // Check if pdf is zero, to avoid NaN's. if (pdf > 0.0) && (filter.e.max_element() > 0.0) { // Account for the additional light attenuation from // this bounce self.next_attenuation_fac = filter.e; self.closure_sample_pdf = pdf; // Calculate the ray for this bounce let offset_pos = robust_ray_origin( idata.pos, idata.pos_err, idata.nor_g.normalized(), dir, ); self.next_bounce_ray = Some(Ray { orig: offset_pos, dir: dir, time: self.time, wavelength: self.wavelength, max_t: std::f32::INFINITY, }); true } else { false } } else { self.next_bounce_ray = None; false }; // Book keeping for next event if found_light { self.event = LightPathEvent::ShadowRay; return true; } else if do_bounce { rays.set_from_ray(&self.next_bounce_ray.unwrap(), false, ray_idx); self.event = LightPathEvent::BounceRay; self.light_attenuation *= self.next_attenuation_fac; return true; } else { return false; } } else { // Didn't hit anything, so background color self.color += scene .world .background_color .to_spectral_sample(self.wavelength) .e * self.light_attenuation / self.closure_sample_pdf; return false; } } //-------------------------------------------------------------------- // Result of shadow ray from sampling a light LightPathEvent::ShadowRay => { // If the light was not in shadow, add it's light to the film // plane. if let surface::SurfaceIntersection::Miss = *isect { self.color += self.pending_color_addition; } // Set up for the next bounce, if any if let Some(ref nbr) = self.next_bounce_ray { rays.set_from_ray(nbr, false, ray_idx); self.light_attenuation *= self.next_attenuation_fac; self.event = LightPathEvent::BounceRay; return true; } else { return false; } } } } } /// Gets a sample, using LDS samples for lower dimensions, /// and switching to random samples at higher dimensions where /// LDS samples aren't available. #[inline(always)] fn get_sample_4d( i: u32, dimension_set: u32, pixel_co: (u32, u32), seed: u32, ) -> (f32, f32, f32, f32) { // A unique seed for every pixel coordinate up to a resolution of // 65536 x 65536. Also incorperating the seed. let seed = pixel_co.0 ^ (pixel_co.1 << 16) ^ seed.wrapping_mul(0x736caf6f); match dimension_set { ds if ds < sobol_burley::NUM_DIMENSION_SETS_4D as u32 => { // Sobol sampling. let n4 = sobol_burley::sample_4d(i, ds, seed); (n4[0], n4[1], n4[2], n4[3]) } ds => { // Random sampling. use crate::hash::hash_u32_to_f32; ( hash_u32_to_f32((ds * 4 + 0) ^ (i << 16), seed), hash_u32_to_f32((ds * 4 + 1) ^ (i << 16), seed), hash_u32_to_f32((ds * 4 + 2) ^ (i << 16), seed), hash_u32_to_f32((ds * 4 + 3) ^ (i << 16), seed), ) } } } /// Golden ratio sampler. fn golden_ratio_sample(i: u32, scramble: u32) -> f32 { // NOTE: use this for the wavelength dimension, because // due to the nature of hero wavelength sampling this ends up // being crazily more efficient than pretty much any other sampler, // and reduces variance by a huge amount. let n = i .wrapping_add(hash_u32(scramble, 0)) .wrapping_mul(2654435769); n as f32 * (1.0 / (1u64 << 32) as f32) } #[derive(Debug)] struct BucketJob { x: u32, y: u32, w: u32, h: u32, }