psychopath/src/light/rectangle_light.rs
Nathan Vegdahl 462977bd4d 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.
2017-08-16 18:17:50 -07:00

204 lines
6.7 KiB
Rust

use mem_arena::MemArena;
use bbox::BBox;
use boundable::Boundable;
use color::{XYZ, SpectralSample, Color};
use lerp::lerp_slice;
use math::{Vector, Point, Matrix4x4};
use ray::{Ray, AccelRay};
use sampling::{spherical_triangle_solid_angle, uniform_sample_spherical_triangle};
use shading::SurfaceShader;
use surface::{Surface, SurfaceIntersection};
use super::SurfaceLight;
#[derive(Copy, Clone, Debug)]
pub struct RectangleLight<'a> {
dimensions: &'a [(f32, f32)],
colors: &'a [XYZ],
bounds_: &'a [BBox],
}
impl<'a> RectangleLight<'a> {
pub fn new<'b>(
arena: &'b MemArena,
dimensions: Vec<(f32, f32)>,
colors: Vec<XYZ>,
) -> RectangleLight<'b> {
let bbs: Vec<_> = dimensions
.iter()
.map(|d| {
BBox {
min: Point::new(d.0 * -0.5, d.1 * -0.5, 0.0),
max: Point::new(d.0 * 0.5, d.1 * 0.5, 0.0),
}
})
.collect();
RectangleLight {
dimensions: arena.copy_slice(&dimensions),
colors: arena.copy_slice(&colors),
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> SurfaceLight for RectangleLight<'a> {
fn sample_from_point(
&self,
space: &Matrix4x4,
arr: Point,
u: f32,
v: f32,
wavelength: f32,
time: f32,
) -> (SpectralSample, Vector, f32) {
// Calculate time interpolated values
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);
// 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);
// Normalize the solid angles for selection purposes
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 shadow_vec = sample_point - arr;
// 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)
}
fn is_delta(&self) -> bool {
false
}
fn approximate_energy(&self) -> f32 {
let color: XYZ = self.colors.iter().fold(
XYZ::new(0.0, 0.0, 0.0),
|a, &b| a + b,
) / 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!()
}
}
impl<'a> Boundable for RectangleLight<'a> {
fn bounds(&self) -> &[BBox] {
self.bounds_
}
}