From d504ca5e6a61c09caa0ad8016cb6bd22f7fc6a2e Mon Sep 17 00:00:00 2001 From: Nathan Vegdahl Date: Mon, 13 Feb 2017 00:03:34 -0800 Subject: [PATCH] Got DistantDiscLights working! Yay! This involved fixing a bug in PsychoBlend. It was including translation in how it was transforming the sun lights' direction vector. --- psychoblend/psy_export.py | 2 +- src/algorithm.rs | 25 +++++++++++++ src/light/distant_disk_light.rs | 10 ++--- src/math/mod.rs | 4 ++ src/renderer.rs | 21 +++++++---- src/sampling.rs | 6 +++ src/scene.rs | 65 +++++++++++++++++++++++++++++++++ 7 files changed, 118 insertions(+), 15 deletions(-) diff --git a/psychoblend/psy_export.py b/psychoblend/psy_export.py index 90ad3fe..20d73ac 100644 --- a/psychoblend/psy_export.py +++ b/psychoblend/psy_export.py @@ -447,7 +447,7 @@ class PsychoExporter: time_rad = [] for i in range(self.time_samples): self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i)) - time_dir += [tuple(ob.matrix_world * Vector((0, 0, -1)))] + time_dir += [tuple(ob.matrix_world.to_3x3() * Vector((0, 0, -1)))] time_col += [ob.data.color * ob.data.energy] time_rad += [ob.data.shadow_soft_size] diff --git a/src/algorithm.rs b/src/algorithm.rs index 6347ddd..b4b6231 100644 --- a/src/algorithm.rs +++ b/src/algorithm.rs @@ -8,6 +8,31 @@ use hash::hash_u64; use lerp::{Lerp, lerp_slice}; +/// Selects an item from a slice based on a weighting function and a +/// number (n) between 0.0 and 1.0. Returns the index of the selected +/// item and the probability that it would have been selected with a +/// random n. +pub fn weighted_choice(slc: &[T], n: f32, weight: F) -> (usize, f32) + where F: Fn(&T) -> f32 +{ + assert!(slc.len() > 0); + + let total_weight = slc.iter().fold(0.0, |sum, v| sum + weight(v)); + let n = n * total_weight; + + let mut x = 0.0; + for (i, v) in slc.iter().enumerate() { + let w = weight(v); + x += w; + if x > n || i == slc.len() { + return (i, w / total_weight); + } + } + + unreachable!() +} + + /// Partitions a slice in-place with the given unary predicate, returning /// the index of the first element for which the predicate evaluates /// false. diff --git a/src/light/distant_disk_light.rs b/src/light/distant_disk_light.rs index 7108a72..ad2983a 100644 --- a/src/light/distant_disk_light.rs +++ b/src/light/distant_disk_light.rs @@ -36,19 +36,17 @@ impl WorldLightSource for DistantDiskLight { // Create a coordinate system from the vector pointing at the center of // of the light. - let (z, x, y) = coordinate_system_from_vector(-direction); - let (x, y, z) = (x.normalized(), y.normalized(), z.normalized()); - - // Calculate the radius in terms of cosine - let cos_theta_max: f64 = radius.cos(); + let (z, x, y) = coordinate_system_from_vector(-direction.normalized()); // Sample the cone subtended by the light. + let cos_theta_max: f64 = radius.cos(); let sample = uniform_sample_cone(u, v, cos_theta_max).normalized(); // Calculate the final values and return everything. + let spectral_sample = (col * solid_angle_inv as f32).to_spectral_sample(wavelength); let shadow_vec = (x * sample.x()) + (y * sample.y()) + (z * sample.z()); let pdf = uniform_sample_cone_pdf(cos_theta_max); - let spectral_sample = (col * solid_angle_inv as f32).to_spectral_sample(wavelength); + return (spectral_sample, shadow_vec, pdf as f32); } diff --git a/src/math/mod.rs b/src/math/mod.rs index ebdebcc..9fa5751 100644 --- a/src/math/mod.rs +++ b/src/math/mod.rs @@ -114,6 +114,10 @@ pub fn log2_64(value: u64) -> u64 { /// Creates a coordinate system from a single vector. +/// +/// The input vector, v, becomes the first vector of the +/// returned tuple, with the other two vectors in the returned +/// tuple defining the orthoganal axes. pub fn coordinate_system_from_vector(v: Vector) -> (Vector, Vector, Vector) { let v2 = if v.x().abs() > v.y().abs() { let invlen = 1.0 / ((v.x() * v.x()) + (v.z() * v.z())).sqrt(); diff --git a/src/renderer.rs b/src/renderer.rs index 639ad93..380f6aa 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,3 +1,4 @@ +use std; use std::cell::Cell; use std::cmp; use std::cmp::min; @@ -298,14 +299,13 @@ impl LightPath { let light_n = self.next_lds_samp(); let light_uvw = (self.next_lds_samp(), self.next_lds_samp(), self.next_lds_samp()); xform_stack.clear(); - if let Some((light_color, shadow_vec, light_pdf, light_sel_pdf)) = - scene.root - .sample_lights(xform_stack, - light_n, - light_uvw, - self.wavelength, - self.time, - isect) { + if let Some((light_color, shadow_vec, light_pdf, light_sel_pdf, is_infinite)) = + scene.sample_lights(xform_stack, + light_n, + light_uvw, + self.wavelength, + self.time, + isect) { // Calculate and store the light that will be contributed // to the film plane if the light is not in shadow. self.pending_color_addition = { @@ -323,6 +323,11 @@ impl LightPath { self.time, true); + // For distant lights + if is_infinite { + ray.max_t = std::f32::INFINITY; + } + return true; } else { return false; diff --git a/src/sampling.rs b/src/sampling.rs index 2124a83..7257563 100644 --- a/src/sampling.rs +++ b/src/sampling.rs @@ -58,6 +58,12 @@ pub fn uniform_sample_sphere(u: f32, v: f32) -> Vector { Vector::new(x, y, z) } +/// Samples a solid angle defined by a cone originating from (0,0,0) +/// and pointing down the positive z-axis. +/// +/// u, v: sampling variables, should each be in the interval [0,1] +/// cos_theta_max: cosine of the max angle from the z-axis, defining +/// the outer extent of the cone. pub fn uniform_sample_cone(u: f32, v: f32, cos_theta_max: f64) -> Vector { let cos_theta = (1.0 - u as f64) + (u as f64 * cos_theta_max); let sin_theta = (1.0 - (cos_theta * cos_theta)).sqrt(); diff --git a/src/scene.rs b/src/scene.rs index 9acc906..0280ab5 100644 --- a/src/scene.rs +++ b/src/scene.rs @@ -1,6 +1,12 @@ use assembly::Assembly; use camera::Camera; use world::World; +use algorithm::weighted_choice; +use transform_stack::TransformStack; +use color::SpectralSample; +use surface::SurfaceIntersection; +use math::Vector; +use light_accel::LightAccel; @@ -11,3 +17,62 @@ pub struct Scene { pub world: World, pub root: Assembly, } + +impl Scene { + pub fn sample_lights(&self, + xform_stack: &mut TransformStack, + n: f32, + uvw: (f32, f32, f32), + wavelength: f32, + time: f32, + intr: &SurfaceIntersection) + -> Option<(SpectralSample, Vector, f32, f32, bool)> { + // TODO: this just selects between world lights and local lights + // with a 50/50 chance. We should do something more sophisticated + // than this, accounting for the estimated impact of the lights + // on the point being lit. + + // Calculate relative probabilities of traversing into world lights + // or local lights. + let wl_energy = if self.world + .lights + .iter() + .fold(0.0, |energy, light| energy + light.approximate_energy()) <= + 0.0 { + 0.0 + } else { + 1.0 + }; + let ll_energy = if self.root.light_accel.approximate_energy() <= 0.0 { + 0.0 + } else { + 1.0 + }; + let tot_energy = wl_energy + ll_energy; + + // Decide either world or local lights, and select and sample a light. + if tot_energy <= 0.0 { + return None; + } else { + let wl_prob = wl_energy / tot_energy; + + if n < wl_prob { + // 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); + return Some((ss, sv, pdf, p * wl_prob, true)); + } else { + // Local lights + let n = (n - wl_prob) / (1.0 - wl_prob); + + if let Some((ss, sv, pdf, spdf)) = + self.root.sample_lights(xform_stack, n, uvw, wavelength, time, intr) { + return Some((ss, sv, pdf, spdf * (1.0 - wl_prob), false)); + } else { + return None; + } + } + } + } +}