Changed SurfaceLight API to return the sample point on the light.

More specifically: prior to this, SurfaceLights returned the
shadow ray direction vector to use.  That was fine, but it
kept the responsibility of generating proper offsets (to account
for floating point error) inside the lights.

Now the SurfaceLights return the world-space point on the light
to sample, along with its surface normal and error magnitude.
This allows the robust shadow ray generation code to be in one
place inside the renderer code.
This commit is contained in:
Nathan Vegdahl 2017-08-17 13:09:48 -07:00
parent b1bd419779
commit 072d366892
7 changed files with 228 additions and 101 deletions

View File

@ -5,7 +5,7 @@ mod sphere_light;
use std::fmt::Debug;
use color::SpectralSample;
use math::{Vector, Point, Matrix4x4};
use math::{Vector, Normal, Point, Matrix4x4};
use surface::Surface;
pub use self::distant_disk_light::DistantDiskLight;
@ -24,8 +24,12 @@ pub trait SurfaceLight: Surface {
/// - `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.
/// Returns:
/// - The light arriving at the point arr.
/// - A tuple with the sample point on the light, the surface normal at
/// that point, and the point's error magnitude. These are used
/// elsewhere to create a robust shadow ray.
/// - The pdf of the sample.
fn sample_from_point(
&self,
space: &Matrix4x4,
@ -34,7 +38,7 @@ pub trait SurfaceLight: Surface {
v: f32,
wavelength: f32,
time: f32,
) -> (SpectralSample, Vector, f32);
) -> (SpectralSample, (Point, Normal, f32), f32);
/// Returns whether the light has a delta distribution.

View File

@ -4,7 +4,7 @@ use bbox::BBox;
use boundable::Boundable;
use color::{XYZ, SpectralSample, Color};
use lerp::lerp_slice;
use math::{Vector, Point, Matrix4x4};
use math::{Vector, Normal, Point, Matrix4x4};
use ray::{Ray, AccelRay};
use sampling::{spherical_triangle_solid_angle, uniform_sample_spherical_triangle};
use shading::SurfaceShader;
@ -109,7 +109,7 @@ impl<'a> SurfaceLight for RectangleLight<'a> {
v: f32,
wavelength: f32,
time: f32,
) -> (SpectralSample, Vector, f32) {
) -> (SpectralSample, (Point, Normal, f32), f32) {
// Calculate time interpolated values
let dim = lerp_slice(self.dimensions, time);
let col = lerp_slice(self.colors, time);
@ -159,13 +159,18 @@ impl<'a> SurfaceLight for RectangleLight<'a> {
sample_point_local.set_z(0.0);
}
let sample_point = sample_point_local * space_inv;
let shadow_vec = sample_point - arr;
let normal = Normal::new(0.0, 0.0, 1.0) * space_inv;
let point_err = 0.0001; // TODO: this is a hack, do properly.
// Calculate pdf and light energy
let pdf = 1.0 / (area_1 + area_2); // PDF of the ray direction being sampled
let spectral_sample = (col * surface_area_inv as f32 * 0.5).to_spectral_sample(wavelength);
(spectral_sample, shadow_vec, pdf as f32)
(
spectral_sample,
(sample_point, normal, point_err),
pdf as f32,
)
}
fn is_delta(&self) -> bool {

View File

@ -6,7 +6,7 @@ use bbox::BBox;
use boundable::Boundable;
use color::{XYZ, SpectralSample, Color};
use lerp::lerp_slice;
use math::{Vector, Point, Matrix4x4, dot, coordinate_system_from_vector};
use math::{Vector, Normal, Point, Matrix4x4, dot, coordinate_system_from_vector};
use ray::{Ray, AccelRay};
use sampling::{uniform_sample_cone, uniform_sample_cone_pdf, uniform_sample_sphere};
use shading::surface_closure::{SurfaceClosureUnion, EmitClosure};
@ -17,8 +17,7 @@ use super::SurfaceLight;
// TODO: use proper error bounds for sample generation to avoid self-shadowing
// instead of these fudge factors.
const SAMPLE_RADIUS_EXPAND_FACTOR: f32 = 1.001;
const SAMPLE_RADIUS_SHRINK_FACTOR: f32 = 0.99;
const SAMPLE_POINT_FUDGE: f32 = 0.001;
// TODO: handle case where radius = 0.0.
@ -92,11 +91,14 @@ impl<'a> SurfaceLight for SphereLight<'a> {
v: f32,
wavelength: f32,
time: f32,
) -> (SpectralSample, Vector, f32) {
) -> (SpectralSample, (Point, Normal, f32), f32) {
// TODO: track fp error due to transforms
let arr = arr * *space;
let pos = Point::new(0.0, 0.0, 0.0);
// Precalculate local->world space transform matrix
let inv_space = space.inverse();
// Calculate time interpolated values
let radius: f64 = lerp_slice(self.radii, time) as f64;
let col = lerp_slice(self.colors, time);
@ -110,6 +112,14 @@ impl<'a> SurfaceLight for SphereLight<'a> {
let (z, x, y) = coordinate_system_from_vector(z);
let (x, y, z) = (x.normalized(), y.normalized(), z.normalized());
// Pre-calculate sample point error magnitude.
// TODO: do this properly. This is a total hack.
let sample_point_err = {
let v = Vector::new(radius as f32, radius as f32, radius as f32);
let v2 = v * inv_space;
v2.length() * SAMPLE_POINT_FUDGE
};
// If we're outside the sphere, sample the surface based on
// the angle it subtends from the point being lit.
if d > radius {
@ -145,26 +155,40 @@ impl<'a> SurfaceLight for SphereLight<'a> {
);
// Calculate the final values and return everything.
let shadow_vec = {
let (sample_point, normal) = {
let sample_vec = (x * sample.x()) + (y * sample.y()) + (z * sample.z());
let sample_point = (arr + sample_vec).into_vector().normalized() * radius as f32;
let adjusted_sample_point = sample_point * SAMPLE_RADIUS_EXPAND_FACTOR;
(adjusted_sample_point.into_point() - arr) * space.inverse()
let normal = (arr + sample_vec).into_vector().normalized();
let point = normal * radius as f32;
(
point.into_point() * inv_space,
normal.into_normal() * inv_space,
)
};
let pdf = uniform_sample_cone_pdf(cos_theta_max);
let spectral_sample = (col * surface_area_inv as f32).to_spectral_sample(wavelength);
return (spectral_sample, shadow_vec, pdf as f32);
return (
spectral_sample,
(sample_point, normal, sample_point_err),
pdf as f32,
);
} else {
// If we're inside the sphere, there's light from every direction.
let shadow_vec = {
let (sample_point, normal) = {
let sample_vec = uniform_sample_sphere(u, v);
let sample_point = (arr + sample_vec).into_vector().normalized() * radius as f32;
let adjusted_sample_point = sample_point * SAMPLE_RADIUS_SHRINK_FACTOR;
(adjusted_sample_point.into_point() - arr) * space.inverse()
let normal = (arr + sample_vec).into_vector().normalized();
let point = normal * radius as f32;
(
point.into_point() * inv_space,
normal.into_normal() * inv_space,
)
};
let pdf = 1.0 / (4.0 * PI_64);
let spectral_sample = (col * surface_area_inv as f32).to_spectral_sample(wavelength);
return (spectral_sample, shadow_vec, pdf as f32);
return (
spectral_sample,
(sample_point, normal, sample_point_err),
pdf as f32,
);
}
}

View File

@ -21,7 +21,7 @@ use image::Image;
use math::{fast_logit, upper_power_of_two};
use mis::power_heuristic;
use ray::Ray;
use scene::Scene;
use scene::{Scene, SceneLightSample};
use surface;
use timer::Timer;
use tracer::Tracer;
@ -463,7 +463,7 @@ impl LightPath {
return false;
}
// Roll the previous closure pdf into the attentuation
// Roll the previous closure pdf into the attenauation
self.light_attenuation /= self.closure_sample_pdf;
// Prepare light ray
@ -474,72 +474,104 @@ impl LightPath {
self.next_lds_samp(),
);
xform_stack.clear();
let found_light = 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,
)
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
{
// Check if pdf is zero, to avoid NaN's.
if light_pdf > 0.0 {
let material = closure.as_surface_closure();
let attenuation =
material.evaluate(ray.dir, shadow_vec, idata.nor, idata.nor_g);
if attenuation.e.h_max() > 0.0 {
// Calculate MIS
let closure_pdf = material.sample_pdf(
ray.dir,
shadow_vec,
idata.nor,
idata.nor_g,
);
let light_mis_pdf = power_heuristic(light_pdf, closure_pdf);
// Calculate and store the light that will be contributed
// to the film plane if the light is not in shadow.
self.pending_color_addition = light_color.e * attenuation.e *
self.light_attenuation /
(light_mis_pdf * light_sel_pdf);
// 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(),
shadow_vec,
);
*ray = Ray::new(
offset_pos,
shadow_vec,
self.time,
self.wavelength,
true,
);
// For distant lights
if is_infinite {
ray.max_t = std::f32::INFINITY;
}
true
} else {
false
}
} else {
false
}
} else {
false
} else {
let light_pdf = light_info.pdf();
let light_sel_pdf = light_info.selection_pdf();
let material = closure.as_surface_closure();
// 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 =
material.evaluate(ray.dir, direction, idata.nor, idata.nor_g);
let closure_pdf =
material.sample_pdf(ray.dir, direction, idata.nor, idata.nor_g);
let mut 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::new(
offset_pos,
direction,
self.time,
self.wavelength,
true,
)
};
shadow_ray.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 =
material.evaluate(ray.dir, dir, idata.nor, idata.nor_g);
let closure_pdf =
material.sample_pdf(ray.dir, dir, idata.nor, idata.nor_g);
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::new(
offset_pos,
offset_end - offset_pos,
self.time,
self.wavelength,
true,
)
};
(attenuation, closure_pdf, shadow_ray)
}
};
// If there's any possible contribution, set up for a
// light ray.
if attenuation.e.h_max() <= 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);
*ray = shadow_ray;
true
}
};
// Prepare bounce ray

View File

@ -9,7 +9,7 @@ use boundable::Boundable;
use color::SpectralSample;
use lerp::lerp_slice;
use light::SurfaceLight;
use math::{Matrix4x4, Vector};
use math::{Matrix4x4, Normal, Point};
use surface::{Surface, SurfaceIntersection};
use shading::SurfaceShader;
use transform_stack::TransformStack;
@ -39,7 +39,7 @@ pub struct Assembly<'a> {
}
impl<'a> Assembly<'a> {
// Returns (light_color, shadow_vector, pdf, selection_pdf)
// Returns (light_color, (sample_point, normal, point_err), pdf, selection_pdf)
pub fn sample_lights(
&self,
xform_stack: &mut TransformStack,
@ -48,7 +48,7 @@ impl<'a> Assembly<'a> {
wavelength: f32,
time: f32,
intr: &SurfaceIntersection,
) -> Option<(SpectralSample, Vector, f32, f32)> {
) -> Option<(SpectralSample, (Point, Normal, f32), f32, f32)> {
if let SurfaceIntersection::Hit {
intersection_data: idata,
closure,
@ -95,7 +95,7 @@ impl<'a> Assembly<'a> {
};
// Sample the light
let (color, shadow_vec, pdf) = light.sample_from_point(
let (color, sample_geo, pdf) = light.sample_from_point(
&xform,
idata.pos,
uvw.0,
@ -103,7 +103,7 @@ impl<'a> Assembly<'a> {
wavelength,
time,
);
return Some((color, shadow_vec, pdf, sel_pdf));
return Some((color, sample_geo, pdf, sel_pdf));
}
_ => unimplemented!(),

View File

@ -3,5 +3,5 @@ mod scene;
mod world;
pub use self::assembly::{Assembly, AssemblyBuilder, Object, InstanceType};
pub use self::scene::Scene;
pub use self::scene::{Scene, SceneLightSample};
pub use self::world::World;

View File

@ -2,7 +2,7 @@ use accel::LightAccel;
use algorithm::weighted_choice;
use camera::Camera;
use color::SpectralSample;
use math::Vector;
use math::{Vector, Normal, Point};
use surface::SurfaceIntersection;
use transform_stack::TransformStack;
@ -27,7 +27,7 @@ impl<'a> Scene<'a> {
wavelength: f32,
time: f32,
intr: &SurfaceIntersection,
) -> Option<(SpectralSample, Vector, f32, f32, bool)> {
) -> SceneLightSample {
// 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
@ -52,7 +52,7 @@ impl<'a> Scene<'a> {
// Decide either world or local lights, and select and sample a light.
if tot_energy <= 0.0 {
return None;
return SceneLightSample::None;
} else {
let wl_prob = wl_energy / tot_energy;
@ -62,12 +62,17 @@ impl<'a> Scene<'a> {
let (i, p) = weighted_choice(self.world.lights, n, |l| l.approximate_energy());
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));
return SceneLightSample::Distant {
color: ss,
direction: sv,
pdf: pdf,
selection_pdf: p * wl_prob,
};
} else {
// Local lights
let n = (n - wl_prob) / (1.0 - wl_prob);
if let Some((ss, sv, pdf, spdf)) =
if let Some((ss, sgeo, pdf, spdf)) =
self.root.sample_lights(
xform_stack,
n,
@ -77,11 +82,68 @@ impl<'a> Scene<'a> {
intr,
)
{
return Some((ss, sv, pdf, spdf * (1.0 - wl_prob), false));
return SceneLightSample::Surface {
color: ss,
sample_geo: sgeo,
pdf: pdf,
selection_pdf: spdf * (1.0 - wl_prob),
};
} else {
return None;
return SceneLightSample::None;
}
}
}
}
}
#[derive(Debug, Copy, Clone)]
pub enum SceneLightSample {
None,
Distant {
color: SpectralSample,
direction: Vector,
pdf: f32,
selection_pdf: f32,
},
Surface {
color: SpectralSample,
sample_geo: (Point, Normal, f32),
pdf: f32,
selection_pdf: f32,
},
}
impl SceneLightSample {
pub fn is_none(&self) -> bool {
if let SceneLightSample::None = *self {
true
} else {
false
}
}
pub fn color(&self) -> SpectralSample {
match *self {
SceneLightSample::None => panic!(),
SceneLightSample::Distant { color, .. } => color,
SceneLightSample::Surface { color, .. } => color,
}
}
pub fn pdf(&self) -> f32 {
match *self {
SceneLightSample::None => panic!(),
SceneLightSample::Distant { pdf, .. } => pdf,
SceneLightSample::Surface { pdf, .. } => pdf,
}
}
pub fn selection_pdf(&self) -> f32 {
match *self {
SceneLightSample::None => panic!(),
SceneLightSample::Distant { selection_pdf, .. } => selection_pdf,
SceneLightSample::Surface { selection_pdf, .. } => selection_pdf,
}
}
}