Fixed sampling of very small rectangle lights.

The sampling method used before is numerically unstable for very
small lights.  That sampling method is still used for large/close
lights, since it works very well for that.  But for small/distant
lights a simpler and numerically stable method is used.
This commit is contained in:
Nathan Vegdahl 2017-10-26 08:42:09 -07:00
parent 3de276cbaa
commit a797ff012d
3 changed files with 131 additions and 49 deletions

View File

@ -4,9 +4,10 @@ use bbox::BBox;
use boundable::Boundable; use boundable::Boundable;
use color::{XYZ, SpectralSample, Color}; use color::{XYZ, SpectralSample, Color};
use lerp::lerp_slice; use lerp::lerp_slice;
use math::{Vector, Normal, Point, Matrix4x4, cross}; use math::{Vector, Normal, Point, Matrix4x4, cross, dot};
use ray::{Ray, AccelRay}; use ray::{Ray, AccelRay};
use sampling::{spherical_triangle_solid_angle, uniform_sample_spherical_triangle}; use sampling::{spherical_triangle_solid_angle, uniform_sample_spherical_triangle,
triangle_surface_area, uniform_sample_triangle};
use shading::surface_closure::{SurfaceClosureUnion, EmitClosure}; use shading::surface_closure::{SurfaceClosureUnion, EmitClosure};
use shading::SurfaceShader; use shading::SurfaceShader;
use surface::{Surface, SurfaceIntersection, SurfaceIntersectionData, triangle}; use surface::{Surface, SurfaceIntersection, SurfaceIntersectionData, triangle};
@ -14,6 +15,9 @@ use surface::{Surface, SurfaceIntersection, SurfaceIntersectionData, triangle};
use super::SurfaceLight; use super::SurfaceLight;
const SIMPLE_SAMPLING_THRESHOLD: f32 = 0.01;
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct RectangleLight<'a> { pub struct RectangleLight<'a> {
dimensions: &'a [(f32, f32)], dimensions: &'a [(f32, f32)],
@ -50,13 +54,12 @@ impl<'a> RectangleLight<'a> {
space: &Matrix4x4, space: &Matrix4x4,
arr: Point, arr: Point,
sample_dir: Vector, sample_dir: Vector,
sample_u: f32, hit_point: Point,
sample_v: f32,
wavelength: f32, wavelength: f32,
time: f32, time: f32,
) -> f32 { ) -> f32 {
// We're not using these, silence warnings // We're not using these, silence warnings
let _ = (sample_dir, sample_u, sample_v, wavelength); let _ = wavelength;
let dim = lerp_slice(self.dimensions, time); let dim = lerp_slice(self.dimensions, time);
@ -78,7 +81,18 @@ impl<'a> RectangleLight<'a> {
let area_1 = spherical_triangle_solid_angle(sp2, sp1, sp3); let area_1 = spherical_triangle_solid_angle(sp2, sp1, sp3);
let area_2 = spherical_triangle_solid_angle(sp4, sp1, sp3); let area_2 = spherical_triangle_solid_angle(sp4, sp1, sp3);
1.0 / (area_1 + area_2) // World-space surface normal
let normal = Normal::new(0.0, 0.0, 1.0) * space_inv;
// PDF
if (area_1 + area_2) < SIMPLE_SAMPLING_THRESHOLD {
let area = triangle_surface_area(p2, p1, p3) + triangle_surface_area(p4, p1, p3);
(hit_point - arr).length2() /
dot(sample_dir.normalized(), normal.into_vector().normalized()).abs() /
area
} else {
1.0 / (area_1 + area_2)
}
} }
// fn outgoing( // fn outgoing(
@ -117,7 +131,8 @@ impl<'a> SurfaceLight for RectangleLight<'a> {
let dim = lerp_slice(self.dimensions, time); let dim = lerp_slice(self.dimensions, time);
let col = lerp_slice(self.colors, time); let col = lerp_slice(self.colors, time);
let surface_area_inv: f64 = 1.0 / (dim.0 as f64 * dim.1 as f64); let surface_area: f64 = dim.0 as f64 * dim.1 as f64;
let surface_area_inv: f64 = 1.0 / surface_area;
// Get the four corners of the rectangle, transformed into world space // Get the four corners of the rectangle, transformed into world space
let space_inv = space.inverse(); let space_inv = space.inverse();
@ -126,53 +141,104 @@ impl<'a> SurfaceLight for RectangleLight<'a> {
let p3 = 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; 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 // Get the four corners of the rectangle relative to arr.
// sphere centered around arr. let lp1 = p1 - arr;
let sp1 = (p1 - arr).normalized(); let lp2 = p2 - arr;
let sp2 = (p2 - arr).normalized(); let lp3 = p3 - arr;
let sp3 = (p3 - arr).normalized(); let lp4 = p4 - arr;
let sp4 = (p4 - arr).normalized();
// Four corners projected on to the unit sphere.
let sp1 = lp1.normalized();
let sp2 = lp2.normalized();
let sp3 = lp3.normalized();
let sp4 = lp4.normalized();
// Get the solid angles of the rectangle split into two triangles // Get the solid angles of the rectangle split into two triangles
let area_1 = spherical_triangle_solid_angle(sp2, sp1, sp3); let area_1 = spherical_triangle_solid_angle(sp2, sp1, sp3);
let area_2 = spherical_triangle_solid_angle(sp4, sp1, sp3); let area_2 = spherical_triangle_solid_angle(sp4, sp1, sp3);
// Normalize the solid angles for selection purposes // Calculate world-space surface normal
let prob_1 = area_1 / (area_1 + area_2);
let prob_2 = 1.0 - prob_1;
// Select one of the triangles and sample it
let shadow_vec = if u < prob_1 {
uniform_sample_spherical_triangle(sp2, sp1, sp3, v, u / prob_1)
} else {
uniform_sample_spherical_triangle(sp4, sp1, sp3, v, 1.0 - ((u - prob_1) / prob_2))
};
// Project shadow_vec back onto the light's surface
let arr_local = arr * *space;
let shadow_vec_local = shadow_vec * *space;
let shadow_vec_local = shadow_vec_local * (-arr_local.z() / shadow_vec_local.z());
let mut sample_point_local = arr_local + shadow_vec_local;
{
let x = sample_point_local.x().max(dim.0 * -0.5).min(dim.0 * 0.5);
let y = sample_point_local.y().max(dim.1 * -0.5).min(dim.1 * 0.5);
sample_point_local.set_x(x);
sample_point_local.set_y(y);
sample_point_local.set_z(0.0);
}
let sample_point = sample_point_local * space_inv;
let normal = Normal::new(0.0, 0.0, 1.0) * space_inv; 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 if (area_1 + area_2) < SIMPLE_SAMPLING_THRESHOLD {
let pdf = 1.0 / (area_1 + area_2); // PDF of the ray direction being sampled // Simple sampling for more distant lights
let spectral_sample = (col * surface_area_inv as f32 * 0.5).to_spectral_sample(wavelength); let surface_area_1 = triangle_surface_area(p2, p1, p3);
let surface_area_2 = triangle_surface_area(p4, p1, p3);
let sample_point = {
// Select which triangle to sample
let threshhold = surface_area_1 / (surface_area_1 + surface_area_2);
if u < threshhold {
uniform_sample_triangle(
p2.into_vector(),
p1.into_vector(),
p3.into_vector(),
v,
u / threshhold,
)
} else {
uniform_sample_triangle(
p4.into_vector(),
p1.into_vector(),
p3.into_vector(),
v,
(u - threshhold) / (1.0 - threshhold),
)
}
}.into_point();
let shadow_vec = sample_point - arr;
let spectral_sample =
(col * surface_area_inv as f32 * 0.5).to_spectral_sample(wavelength);
let pdf = (sample_point - arr).length2() /
dot(shadow_vec.normalized(), normal.into_vector().normalized()).abs() /
(surface_area_1 + surface_area_2);
let point_err = 0.0001; // TODO: this is a hack, do properly.
(spectral_sample, (sample_point, normal, point_err), pdf)
} else {
// Sophisticated sampling for close lights.
( // Normalize the solid angles for selection purposes
spectral_sample, let prob_1 = if area_1.is_infinite() {
(sample_point, normal, point_err), 1.0
pdf as f32, } else if area_2.is_infinite() {
) 0.0
} else {
area_1 / (area_1 + area_2)
};
let prob_2 = 1.0 - prob_1;
// Select one of the triangles and sample it
let shadow_vec = if u < prob_1 {
uniform_sample_spherical_triangle(sp2, sp1, sp3, v, u / prob_1)
} else {
uniform_sample_spherical_triangle(sp4, sp1, sp3, v, 1.0 - ((u - prob_1) / prob_2))
};
// Project shadow_vec back onto the light's surface
let arr_local = arr * *space;
let shadow_vec_local = shadow_vec * *space;
let shadow_vec_local = shadow_vec_local * (-arr_local.z() / shadow_vec_local.z());
let mut sample_point_local = arr_local + shadow_vec_local;
{
let x = sample_point_local.x().max(dim.0 * -0.5).min(dim.0 * 0.5);
let y = sample_point_local.y().max(dim.1 * -0.5).min(dim.1 * 0.5);
sample_point_local.set_x(x);
sample_point_local.set_y(y);
sample_point_local.set_z(0.0);
}
let sample_point = sample_point_local * 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,
(sample_point, normal, point_err),
pdf as f32,
)
}
} }
fn is_delta(&self) -> bool { fn is_delta(&self) -> bool {
@ -239,8 +305,7 @@ impl<'a> Surface for RectangleLight<'a> {
&xform, &xform,
wr.orig, wr.orig,
wr.dir, wr.dir,
0.0, pos,
0.0,
wr.wavelength, wr.wavelength,
r.time, r.time,
), ),

View File

@ -2,4 +2,5 @@ mod monte_carlo;
pub use self::monte_carlo::{square_to_circle, cosine_sample_hemisphere, uniform_sample_hemisphere, pub use self::monte_carlo::{square_to_circle, cosine_sample_hemisphere, uniform_sample_hemisphere,
uniform_sample_sphere, uniform_sample_cone, uniform_sample_cone_pdf, uniform_sample_sphere, uniform_sample_cone, uniform_sample_cone_pdf,
uniform_sample_triangle, triangle_surface_area,
spherical_triangle_solid_angle, uniform_sample_spherical_triangle}; spherical_triangle_solid_angle, uniform_sample_spherical_triangle};

View File

@ -4,7 +4,7 @@ use std::f32::consts::FRAC_PI_4 as QPI_32;
use std::f32::consts::PI as PI_32; use std::f32::consts::PI as PI_32;
use std::f64::consts::PI as PI_64; use std::f64::consts::PI as PI_64;
use math::{Vector, dot}; use math::{Vector, Point, cross, dot};
/// Maps the unit square to the unit circle. /// Maps the unit square to the unit circle.
@ -80,6 +80,22 @@ pub fn uniform_sample_cone_pdf(cos_theta_max: f64) -> f64 {
1.0 / (2.0 * PI_64 * (1.0 - cos_theta_max)) 1.0 / (2.0 * PI_64 * (1.0 - cos_theta_max))
} }
/// Generates a uniform sample on a triangle given two uniform random
/// variables i and j in [0, 1].
pub fn uniform_sample_triangle(va: Vector, vb: Vector, vc: Vector, i: f32, j: f32) -> Vector {
let isqrt = i.sqrt();
let a = 1.0 - isqrt;
let b = isqrt * (1.0 - j);
let c = j * isqrt;
(va * a) + (vb * b) + (vc * c)
}
/// Calculates the surface area of a triangle.
pub fn triangle_surface_area(p0: Point, p1: Point, p2: Point) -> f32 {
0.5 * cross(p1 - p0, p2 - p0).length()
}
/// Calculates the projected solid angle of a spherical triangle. /// Calculates the projected solid angle of a spherical triangle.
/// ///
/// A, B, and C are the points of the triangle on a unit sphere. /// A, B, and C are the points of the triangle on a unit sphere.