From 462977bd4dcf0817eb2ab64078f1d0c88d377e55 Mon Sep 17 00:00:00 2001 From: Nathan Vegdahl Date: Wed, 16 Aug 2017 18:17:50 -0700 Subject: [PATCH] WIP: multiple importance sampling. Reorganized light and surface traits so that light sources are surfaces as well, which will let them slide easily into intersection tests with the rest of the scene geometry. --- src/light/distant_disk_light.rs | 48 ++++++------ src/light/mod.rs | 118 +++++++---------------------- src/light/rectangle_light.rs | 128 +++++++++++++++++--------------- src/light/sphere_light.rs | 103 ++++++++++++------------- src/parse/psy_assembly.rs | 6 +- src/scene/assembly.rs | 22 ++++-- src/scene/scene.rs | 3 +- src/surface/mod.rs | 2 +- src/tracer.rs | 2 +- 9 files changed, 190 insertions(+), 242 deletions(-) diff --git a/src/light/distant_disk_light.rs b/src/light/distant_disk_light.rs index 7448df4..e42ff76 100644 --- a/src/light/distant_disk_light.rs +++ b/src/light/distant_disk_light.rs @@ -31,10 +31,36 @@ impl<'a> DistantDiskLight<'a> { colors: arena.copy_slice(&colors), } } + + // fn sample_pdf(&self, sample_dir: Vector, wavelength: f32, time: f32) -> f32 { + // // We're not using these, silence warnings + // let _ = (sample_dir, wavelength); + + // let radius: f64 = lerp_slice(self.radii, time) as f64; + + // uniform_sample_cone_pdf(radius.cos()) as f32 + // } + + // fn outgoing(&self, dir: Vector, wavelength: f32, time: f32) -> SpectralSample { + // // We're not using this, silence warning + // let _ = dir; + + // let radius = lerp_slice(self.radii, time) as f64; + // let col = lerp_slice(self.colors, time); + // let solid_angle = 2.0 * PI_64 * (1.0 - radius.cos()); + + // (col / solid_angle as f32).to_spectral_sample(wavelength) + // } } impl<'a> WorldLightSource for DistantDiskLight<'a> { - fn sample(&self, u: f32, v: f32, wavelength: f32, time: f32) -> (SpectralSample, Vector, f32) { + fn sample_from_point( + &self, + u: f32, + v: f32, + wavelength: f32, + time: f32, + ) -> (SpectralSample, Vector, f32) { // Calculate time interpolated values let radius: f64 = lerp_slice(self.radii, time) as f64; let direction = lerp_slice(self.directions, time); @@ -56,26 +82,6 @@ impl<'a> WorldLightSource for DistantDiskLight<'a> { (spectral_sample, shadow_vec, pdf as f32) } - fn sample_pdf(&self, sample_dir: Vector, wavelength: f32, time: f32) -> f32 { - // We're not using these, silence warnings - let _ = (sample_dir, wavelength); - - let radius: f64 = lerp_slice(self.radii, time) as f64; - - uniform_sample_cone_pdf(radius.cos()) as f32 - } - - fn outgoing(&self, dir: Vector, wavelength: f32, time: f32) -> SpectralSample { - // We're not using this, silence warning - let _ = dir; - - let radius = lerp_slice(self.radii, time) as f64; - let col = lerp_slice(self.colors, time); - let solid_angle = 2.0 * PI_64 * (1.0 - radius.cos()); - - (col / solid_angle as f32).to_spectral_sample(wavelength) - } - fn is_delta(&self) -> bool { false } diff --git a/src/light/mod.rs b/src/light/mod.rs index 1acf588..0ff1737 100644 --- a/src/light/mod.rs +++ b/src/light/mod.rs @@ -4,11 +4,9 @@ mod sphere_light; use std::fmt::Debug; -use boundable::Boundable; use color::SpectralSample; use math::{Vector, Point, Matrix4x4}; -use ray::{Ray, AccelRay}; -use surface::SurfaceIntersection; +use surface::Surface; pub use self::distant_disk_light::DistantDiskLight; pub use self::rectangle_light::RectangleLight; @@ -16,19 +14,19 @@ pub use self::sphere_light::SphereLight; /// A finite light source that can be bounded in space. -pub trait LightSource: Boundable + Debug + Sync { - /// Samples the light source for a given point to be illuminated. +pub trait SurfaceLight: Surface { + /// Samples the surface given a point to be illuminated. /// - /// - space: The world-to-object space transform of the light. - /// - arr: The point to be illuminated (in world space). - /// - u: Random parameter U. - /// - v: Random parameter V. - /// - wavelength: The wavelength of light to sample at. - /// - time: The time to sample at. + /// - `space`: The world-to-object space transform of the light. + /// - `arr`: The point to be illuminated (in world space). + /// - `u`: Random parameter U. + /// - `v`: Random parameter V. + /// - `wavelength`: The wavelength of light to sample at. + /// - `time`: The time to sample at. /// /// Returns: The light arriving at the point arr, the vector to use for /// shadow testing, and the pdf of the sample. - fn sample( + fn sample_from_point( &self, space: &Matrix4x4, arr: Point, @@ -39,53 +37,6 @@ pub trait LightSource: Boundable + Debug + Sync { ) -> (SpectralSample, Vector, f32); - /// Calculates the pdf of sampling the given - /// `sample_dir`/`sample_u`/`sample_v` from the given point `arr`. This is used - /// primarily to calculate probabilities for multiple importance sampling. - /// - /// NOTE: this function CAN assume that sample_dir, sample_u, and sample_v - /// are a valid sample for the light source (i.e. hits/lies on the light - /// source). No guarantees are made about the correctness of the return - /// value if they are not valid. - /// - /// TODO: this probably shouldn't be part of the public interface. In the - /// rest of the renderer, the PDF is always calculated by the `sample` and - /// and `intersect_rays` methods. - fn sample_pdf( - &self, - space: &Matrix4x4, - arr: Point, - sample_dir: Vector, - sample_u: f32, - sample_v: f32, - wavelength: f32, - time: f32, - ) -> f32; - - - /// Returns the color emitted in the given direction from the - /// given parameters on the light. - /// - /// - dir: The direction of the outgoing light. - /// - u: Random parameter U. - /// - v: Random parameter V. - /// - wavelength: The hero wavelength of light to sample at. - /// - time: The time to sample at. - /// - /// TODO: this probably shouldn't be part of the public interface. In the - /// rest of the renderer, this is handled by the `sample` and - /// `intersect_rays` methods. - fn outgoing( - &self, - space: &Matrix4x4, - dir: Vector, - u: f32, - v: f32, - wavelength: f32, - time: f32, - ) -> SpectralSample; - - /// Returns whether the light has a delta distribution. /// /// If a light has no chance of a ray hitting it through random process @@ -94,18 +45,12 @@ pub trait LightSource: Boundable + Debug + Sync { fn is_delta(&self) -> bool; - /// Returns an approximation of the total energy emitted by the light - /// source. Note that this does not need to be exact: it is used for - /// importance sampling. + /// Returns an approximation of the total energy emitted by the surface. + /// + /// Note: this does not need to be exact, but it does need to be non-zero + /// for any surface that does emit light. This is used for importance + /// sampling. fn approximate_energy(&self) -> f32; - - fn intersect_rays( - &self, - accel_rays: &mut [AccelRay], - wrays: &[Ray], - isects: &mut [SurfaceIntersection], - space: &[Matrix4x4], - ); } @@ -121,25 +66,13 @@ pub trait WorldLightSource: Debug + Sync { /// /// Returns: The light arriving from the shadow-testing direction, the /// vector to use for shadow testing, and the pdf of the sample. - fn sample(&self, u: f32, v: f32, wavelength: f32, time: f32) -> (SpectralSample, Vector, f32); - - - /// Calculates the pdf of sampling the given sample_dir. This is used - /// primarily to calculate probabilities for multiple importance sampling. - /// - /// NOTE: this function CAN assume that sample_dir is a valid sample for - /// the light source (i.e. hits/lies on the light source). No guarantees - /// are made about the correctness of the return value if it isn't valid. - fn sample_pdf(&self, sample_dir: Vector, wavelength: f32, time: f32) -> f32; - - - /// Returns the color emitted in the given direction from the - /// given parameters on the light. - /// - /// - dir: The direction of the outgoing light. - /// - wavelength: The hero wavelength of light to sample at. - /// - time: The time to sample at. - fn outgoing(&self, dir: Vector, wavelength: f32, time: f32) -> SpectralSample; + fn sample_from_point( + &self, + u: f32, + v: f32, + wavelength: f32, + time: f32, + ) -> (SpectralSample, Vector, f32); /// Returns whether the light has a delta distribution. @@ -151,7 +84,10 @@ pub trait WorldLightSource: Debug + Sync { /// Returns an approximation of the total energy emitted by the light - /// source. Note that this does not need to be exact: it is used for - /// importance sampling. + /// source. + /// + /// Note: this does not need to be exact, but it does need to be non-zero + /// for any light that emits any light. This is used for importance + /// sampling. fn approximate_energy(&self) -> f32; } diff --git a/src/light/rectangle_light.rs b/src/light/rectangle_light.rs index 53c6002..2c4d779 100644 --- a/src/light/rectangle_light.rs +++ b/src/light/rectangle_light.rs @@ -7,9 +7,10 @@ use lerp::lerp_slice; use math::{Vector, Point, Matrix4x4}; use ray::{Ray, AccelRay}; use sampling::{spherical_triangle_solid_angle, uniform_sample_spherical_triangle}; -use surface::SurfaceIntersection; +use shading::SurfaceShader; +use surface::{Surface, SurfaceIntersection}; -use super::LightSource; +use super::SurfaceLight; #[derive(Copy, Clone, Debug)] @@ -40,10 +41,67 @@ impl<'a> RectangleLight<'a> { bounds_: arena.copy_slice(&bbs), } } + + // fn sample_pdf( + // &self, + // space: &Matrix4x4, + // arr: Point, + // sample_dir: Vector, + // sample_u: f32, + // sample_v: f32, + // wavelength: f32, + // time: f32, + // ) -> f32 { + // // We're not using these, silence warnings + // let _ = (sample_dir, sample_u, sample_v, wavelength); + + // let dim = lerp_slice(self.dimensions, time); + + // // Get the four corners of the rectangle, transformed into world space + // let space_inv = space.inverse(); + // let p1 = Point::new(dim.0 * 0.5, dim.1 * 0.5, 0.0) * space_inv; + // let p2 = Point::new(dim.0 * -0.5, dim.1 * 0.5, 0.0) * space_inv; + // let p3 = Point::new(dim.0 * -0.5, dim.1 * -0.5, 0.0) * space_inv; + // let p4 = Point::new(dim.0 * 0.5, dim.1 * -0.5, 0.0) * space_inv; + + // // Get the four corners of the rectangle, projected on to the unit + // // sphere centered around arr. + // let sp1 = (p1 - arr).normalized(); + // let sp2 = (p2 - arr).normalized(); + // let sp3 = (p3 - arr).normalized(); + // let sp4 = (p4 - arr).normalized(); + + // // Get the solid angles of the rectangle split into two triangles + // let area_1 = spherical_triangle_solid_angle(sp2, sp1, sp3); + // let area_2 = spherical_triangle_solid_angle(sp4, sp1, sp3); + + // 1.0 / (area_1 + area_2) + // } + + // fn outgoing( + // &self, + // space: &Matrix4x4, + // dir: Vector, + // u: f32, + // v: f32, + // wavelength: f32, + // time: f32, + // ) -> SpectralSample { + // // We're not using these, silence warnings + // let _ = (space, dir, u, v); + + // let dim = lerp_slice(self.dimensions, time); + // let col = lerp_slice(self.colors, time); + + // // TODO: Is this right? Do we need to get the surface area post-transform? + // let surface_area_inv: f64 = 1.0 / (dim.0 as f64 * dim.1 as f64); + + // (col * surface_area_inv as f32 * 0.5).to_spectral_sample(wavelength) + // } } -impl<'a> LightSource for RectangleLight<'a> { - fn sample( +impl<'a> SurfaceLight for RectangleLight<'a> { + fn sample_from_point( &self, space: &Matrix4x4, arr: Point, @@ -110,63 +168,6 @@ impl<'a> LightSource for RectangleLight<'a> { (spectral_sample, shadow_vec, pdf as f32) } - fn sample_pdf( - &self, - space: &Matrix4x4, - arr: Point, - sample_dir: Vector, - sample_u: f32, - sample_v: f32, - wavelength: f32, - time: f32, - ) -> f32 { - // We're not using these, silence warnings - let _ = (sample_dir, sample_u, sample_v, wavelength); - - let dim = lerp_slice(self.dimensions, time); - - // Get the four corners of the rectangle, transformed into world space - let space_inv = space.inverse(); - let p1 = Point::new(dim.0 * 0.5, dim.1 * 0.5, 0.0) * space_inv; - let p2 = Point::new(dim.0 * -0.5, dim.1 * 0.5, 0.0) * space_inv; - let p3 = Point::new(dim.0 * -0.5, dim.1 * -0.5, 0.0) * space_inv; - let p4 = Point::new(dim.0 * 0.5, dim.1 * -0.5, 0.0) * space_inv; - - // Get the four corners of the rectangle, projected on to the unit - // sphere centered around arr. - let sp1 = (p1 - arr).normalized(); - let sp2 = (p2 - arr).normalized(); - let sp3 = (p3 - arr).normalized(); - let sp4 = (p4 - arr).normalized(); - - // Get the solid angles of the rectangle split into two triangles - let area_1 = spherical_triangle_solid_angle(sp2, sp1, sp3); - let area_2 = spherical_triangle_solid_angle(sp4, sp1, sp3); - - 1.0 / (area_1 + area_2) - } - - fn outgoing( - &self, - space: &Matrix4x4, - dir: Vector, - u: f32, - v: f32, - wavelength: f32, - time: f32, - ) -> SpectralSample { - // We're not using these, silence warnings - let _ = (space, dir, u, v); - - let dim = lerp_slice(self.dimensions, time); - let col = lerp_slice(self.colors, time); - - // TODO: Is this right? Do we need to get the surface area post-transform? - let surface_area_inv: f64 = 1.0 / (dim.0 as f64 * dim.1 as f64); - - (col * surface_area_inv as f32 * 0.5).to_spectral_sample(wavelength) - } - fn is_delta(&self) -> bool { false } @@ -178,14 +179,19 @@ impl<'a> LightSource for RectangleLight<'a> { ) / self.colors.len() as f32; color.y } +} + +impl<'a> Surface for RectangleLight<'a> { fn intersect_rays( &self, accel_rays: &mut [AccelRay], wrays: &[Ray], isects: &mut [SurfaceIntersection], + shader: &SurfaceShader, space: &[Matrix4x4], ) { + let _ = (accel_rays, wrays, isects, shader, space); unimplemented!() } } diff --git a/src/light/sphere_light.rs b/src/light/sphere_light.rs index 08f5fe8..3a94ddc 100644 --- a/src/light/sphere_light.rs +++ b/src/light/sphere_light.rs @@ -9,10 +9,11 @@ use lerp::lerp_slice; use math::{Vector, Point, Matrix4x4, dot, coordinate_system_from_vector}; use ray::{Ray, AccelRay}; use sampling::{uniform_sample_cone, uniform_sample_cone_pdf, uniform_sample_sphere}; -use surface::{SurfaceIntersection, SurfaceIntersectionData}; use shading::surface_closure::{SurfaceClosureUnion, EmitClosure}; +use shading::SurfaceShader; +use surface::{Surface, SurfaceIntersection, SurfaceIntersectionData}; -use super::LightSource; +use super::SurfaceLight; // TODO: handle case where radius = 0.0. @@ -40,10 +41,45 @@ impl<'a> SphereLight<'a> { bounds_: arena.copy_slice(&bbs), } } + + // TODO: this is only used from within `intersect_rays`, and could be done + // more efficiently by inlining it there. + fn sample_pdf( + &self, + space: &Matrix4x4, + arr: Point, + sample_dir: Vector, + sample_u: f32, + sample_v: f32, + wavelength: f32, + time: f32, + ) -> f32 { + // We're not using these, silence warnings + let _ = (sample_dir, sample_u, sample_v, wavelength); + + let arr = arr * *space; + let pos = Point::new(0.0, 0.0, 0.0); + let radius: f64 = lerp_slice(self.radii, time) as f64; + + let d2: f64 = (pos - arr).length2() as f64; // Distance from center of sphere squared + let d: f64 = d2.sqrt(); // Distance from center of sphere + + if d > radius { + // Calculate the portion of the sphere visible from the point + let sin_theta_max2: f64 = ((radius * radius) / d2).min(1.0); + let cos_theta_max2: f64 = 1.0 - sin_theta_max2; + let cos_theta_max: f64 = cos_theta_max2.sqrt(); + + uniform_sample_cone_pdf(cos_theta_max) as f32 + } else { + (1.0 / (4.0 * PI_64)) as f32 + } + } } -impl<'a> LightSource for SphereLight<'a> { - fn sample( + +impl<'a> SurfaceLight for SphereLight<'a> { + fn sample_from_point( &self, space: &Matrix4x4, arr: Point, @@ -61,7 +97,6 @@ impl<'a> LightSource for SphereLight<'a> { let col = lerp_slice(self.colors, time); let surface_area_inv: f64 = 1.0 / (4.0 * PI_64 * radius * radius); - // Create a coordinate system from the vector between the // point and the center of the light let z = pos - arr; @@ -119,57 +154,6 @@ impl<'a> LightSource for SphereLight<'a> { } } - fn sample_pdf( - &self, - space: &Matrix4x4, - arr: Point, - sample_dir: Vector, - sample_u: f32, - sample_v: f32, - wavelength: f32, - time: f32, - ) -> f32 { - // We're not using these, silence warnings - let _ = (sample_dir, sample_u, sample_v, wavelength); - - let arr = arr * *space; - let pos = Point::new(0.0, 0.0, 0.0); - let radius: f64 = lerp_slice(self.radii, time) as f64; - - let d2: f64 = (pos - arr).length2() as f64; // Distance from center of sphere squared - let d: f64 = d2.sqrt(); // Distance from center of sphere - - if d > radius { - // Calculate the portion of the sphere visible from the point - let sin_theta_max2: f64 = ((radius * radius) / d2).min(1.0); - let cos_theta_max2: f64 = 1.0 - sin_theta_max2; - let cos_theta_max: f64 = cos_theta_max2.sqrt(); - - uniform_sample_cone_pdf(cos_theta_max) as f32 - } else { - (1.0 / (4.0 * PI_64)) as f32 - } - } - - fn outgoing( - &self, - space: &Matrix4x4, - dir: Vector, - u: f32, - v: f32, - wavelength: f32, - time: f32, - ) -> SpectralSample { - // We're not using these, silence warnings - let _ = (space, dir, u, v); - - // TODO: use transform space correctly - let radius = lerp_slice(self.radii, time) as f64; - let col = lerp_slice(self.colors, time); - let surface_area = 4.0 * PI_64 * radius * radius; - (col / surface_area as f32).to_spectral_sample(wavelength) - } - fn is_delta(&self) -> bool { false } @@ -181,14 +165,20 @@ impl<'a> LightSource for SphereLight<'a> { ) / self.colors.len() as f32; color.y } +} + +impl<'a> Surface for SphereLight<'a> { fn intersect_rays( &self, accel_rays: &mut [AccelRay], wrays: &[Ray], isects: &mut [SurfaceIntersection], + shader: &SurfaceShader, space: &[Matrix4x4], ) { + let _ = shader; // Silence 'unused' warning + for r in accel_rays.iter_mut() { let wr = &wrays[r.id as usize]; @@ -316,6 +306,7 @@ impl<'a> LightSource for SphereLight<'a> { } } + impl<'a> Boundable for SphereLight<'a> { fn bounds(&self) -> &[BBox] { self.bounds_ diff --git a/src/parse/psy_assembly.rs b/src/parse/psy_assembly.rs index 8fea94c..86cb659 100644 --- a/src/parse/psy_assembly.rs +++ b/src/parse/psy_assembly.rs @@ -119,7 +119,7 @@ pub fn parse_assembly<'a>( if let DataTree::Internal { ident: Some(ident), .. } = *child { builder.add_object( ident, - Object::Light(arena.alloc(parse_sphere_light(arena, child)?)), + Object::SurfaceLight(arena.alloc(parse_sphere_light(arena, child)?)), ); } else { // No ident @@ -132,7 +132,9 @@ pub fn parse_assembly<'a>( if let DataTree::Internal { ident: Some(ident), .. } = *child { builder.add_object( ident, - Object::Light(arena.alloc(parse_rectangle_light(arena, child)?)), + Object::SurfaceLight( + arena.alloc(parse_rectangle_light(arena, child)?), + ), ); } else { // No ident diff --git a/src/scene/assembly.rs b/src/scene/assembly.rs index ba7ba4c..a522a5c 100644 --- a/src/scene/assembly.rs +++ b/src/scene/assembly.rs @@ -8,7 +8,7 @@ use bbox::{BBox, transform_bbox_slice_from}; use boundable::Boundable; use color::SpectralSample; use lerp::lerp_slice; -use light::LightSource; +use light::SurfaceLight; use math::{Matrix4x4, Vector}; use surface::{Surface, SurfaceIntersection}; use shading::SurfaceShader; @@ -75,7 +75,7 @@ impl<'a> Assembly<'a> { InstanceType::Object => { match self.objects[inst.data_index] { - Object::Light(light) => { + Object::SurfaceLight(light) => { // Get the world-to-object space transform of the light let xform = if let Some((a, b)) = inst.transform_indices { let pxforms = xform_stack.top(); @@ -95,8 +95,14 @@ impl<'a> Assembly<'a> { }; // Sample the light - let (color, shadow_vec, pdf) = - light.sample(&xform, idata.pos, uvw.0, uvw.1, wavelength, time); + let (color, shadow_vec, pdf) = light.sample_from_point( + &xform, + idata.pos, + uvw.0, + uvw.1, + wavelength, + time, + ); return Some((color, shadow_vec, pdf, sel_pdf)); } @@ -303,7 +309,7 @@ impl<'a> AssemblyBuilder<'a> { .iter() .filter(|inst| match inst.instance_type { InstanceType::Object => { - if let Object::Light(_) = self.objects[inst.data_index] { + if let Object::SurfaceLight(_) = self.objects[inst.data_index] { true } else { false @@ -324,7 +330,7 @@ impl<'a> AssemblyBuilder<'a> { let bounds = &bbs[bis[inst.id]..bis[inst.id + 1]]; let energy = match inst.instance_type { InstanceType::Object => { - if let Object::Light(light) = self.objects[inst.data_index] { + if let Object::SurfaceLight(light) = self.objects[inst.data_index] { light.approximate_energy() } else { 0.0 @@ -370,7 +376,7 @@ impl<'a> AssemblyBuilder<'a> { let obj = &self.objects[inst.data_index]; match *obj { Object::Surface(s) => bbs.extend(s.bounds()), - Object::Light(l) => bbs.extend(l.bounds()), + Object::SurfaceLight(l) => bbs.extend(l.bounds()), } } @@ -404,7 +410,7 @@ impl<'a> AssemblyBuilder<'a> { #[derive(Copy, Clone, Debug)] pub enum Object<'a> { Surface(&'a Surface), - Light(&'a LightSource), + SurfaceLight(&'a SurfaceLight), } diff --git a/src/scene/scene.rs b/src/scene/scene.rs index 7dd6db9..44a5cbb 100644 --- a/src/scene/scene.rs +++ b/src/scene/scene.rs @@ -60,7 +60,8 @@ impl<'a> Scene<'a> { // World lights let n = n / wl_prob; let (i, p) = weighted_choice(self.world.lights, n, |l| l.approximate_energy()); - let (ss, sv, pdf) = self.world.lights[i].sample(uvw.0, uvw.1, wavelength, time); + let (ss, sv, pdf) = + self.world.lights[i].sample_from_point(uvw.0, uvw.1, wavelength, time); return Some((ss, sv, pdf, p * wl_prob, true)); } else { // Local lights diff --git a/src/surface/mod.rs b/src/surface/mod.rs index a7931b0..076175d 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -8,8 +8,8 @@ use std::fmt::Debug; use boundable::Boundable; use math::{Point, Vector, Normal, Matrix4x4}; use ray::{Ray, AccelRay}; -use shading::SurfaceShader; use shading::surface_closure::SurfaceClosureUnion; +use shading::SurfaceShader; pub trait Surface: Boundable + Debug + Sync { diff --git a/src/tracer.rs b/src/tracer.rs index 4159b40..cade2d8 100644 --- a/src/tracer.rs +++ b/src/tracer.rs @@ -199,7 +199,7 @@ impl<'a> TracerInner<'a> { ); } - Object::Light(_) => { + Object::SurfaceLight(_) => { // TODO } }