psychopath/src/light/rectangle_light.rs

340 lines
13 KiB
Rust

use mem_arena::MemArena;
use bbox::BBox;
use boundable::Boundable;
use color::{Color, SpectralSample, XYZ};
use lerp::lerp_slice;
use math::{cross, dot, Matrix4x4, Normal, Point, Vector};
use ray::{AccelRay, Ray};
use sampling::{spherical_triangle_solid_angle, triangle_surface_area,
uniform_sample_spherical_triangle, uniform_sample_triangle};
use shading::surface_closure::{EmitClosure, SurfaceClosureUnion};
use shading::SurfaceShader;
use surface::{triangle, Surface, SurfaceIntersection, SurfaceIntersectionData};
use super::SurfaceLight;
const SIMPLE_SAMPLING_THRESHOLD: f32 = 0.01;
#[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),
}
}
// 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,
hit_point: Point,
wavelength: f32,
time: f32,
) -> f32 {
// We're not using these, silence warnings
let _ = 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);
// 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(
// &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, (Point, Normal, f32), f32) {
// Calculate time interpolated values
let dim = lerp_slice(self.dimensions, time);
let col = lerp_slice(self.colors, time);
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
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 relative to arr.
let lp1 = p1 - arr;
let lp2 = p2 - arr;
let lp3 = p3 - arr;
let lp4 = p4 - arr;
// 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
let area_1 = spherical_triangle_solid_angle(sp2, sp1, sp3);
let area_2 = spherical_triangle_solid_angle(sp4, sp1, sp3);
// Calculate world-space surface normal
let normal = Normal::new(0.0, 0.0, 1.0) * space_inv;
if (area_1 + area_2) < SIMPLE_SAMPLING_THRESHOLD {
// Simple sampling for more distant lights
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
let prob_1 = if area_1.is_infinite() {
1.0
} 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 {
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 _ = shader; // Silence 'unused' warning
for r in accel_rays.iter_mut() {
let wr = &wrays[r.id as usize];
// Calculate time interpolated values
let dim = lerp_slice(self.dimensions, r.time);
let xform = lerp_slice(space, r.time);
let space_inv = xform.inverse();
// Get the four corners of the rectangle, transformed into world space
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;
// Test against two triangles that make up the light
for tri in &[(p1, p2, p3), (p3, p4, p1)] {
if let Some((t, b0, b1, b2)) = triangle::intersect_ray(wr, *tri) {
if t < r.max_t {
if r.is_occlusion() {
isects[r.id as usize] = SurfaceIntersection::Occlude;
r.mark_done();
} else {
let (pos, pos_err) = triangle::surface_point(*tri, (b0, b1, b2));
let normal = cross(tri.0 - tri.1, tri.0 - tri.2).into_normal();
let intersection_data = SurfaceIntersectionData {
incoming: wr.dir,
t: t,
pos: pos,
pos_err: pos_err,
nor: normal,
nor_g: normal,
uv: (0.0, 0.0), // TODO
local_space: xform,
sample_pdf: self.sample_pdf(
&xform,
wr.orig,
wr.dir,
pos,
wr.wavelength,
r.time,
),
};
let closure = {
let inv_surface_area = (1.0 / (dim.0 as f64 * dim.1 as f64)) as f32;
let color = lerp_slice(self.colors, r.time)
.to_spectral_sample(wr.wavelength)
* inv_surface_area;
SurfaceClosureUnion::EmitClosure(EmitClosure::new(color))
};
// Fill in intersection
isects[r.id as usize] = SurfaceIntersection::Hit {
intersection_data: intersection_data,
closure: closure,
};
// Set ray's max t
r.max_t = t;
}
break;
}
}
}
}
}
}
impl<'a> Boundable for RectangleLight<'a> {
fn bounds(&self) -> &[BBox] {
self.bounds_
}
}