441 lines
14 KiB
Rust
441 lines
14 KiB
Rust
use std::ops::Index;
|
|
|
|
use crate::{
|
|
rays::Ray,
|
|
shapes::Shape,
|
|
tuples::{dot, reflect, Tuple},
|
|
Float, EPSILON,
|
|
};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Intersection<'i> {
|
|
pub t: Float,
|
|
pub object: &'i Shape,
|
|
}
|
|
impl<'i> PartialEq for Intersection<'i> {
|
|
fn eq(&self, rhs: &Intersection) -> bool {
|
|
((self.t - rhs.t).abs() < EPSILON) && (self.object == rhs.object)
|
|
}
|
|
}
|
|
|
|
impl<'i> Intersection<'i> {
|
|
/// Create new `Intersection` at the given `t` that hits the given `object`.
|
|
///
|
|
/// # Examples
|
|
/// ```
|
|
/// use rtchallenge::{intersections::Intersection, shapes::Shape};
|
|
///
|
|
/// // An intersection ecapsulates t and object.
|
|
/// let s = Shape::sphere();
|
|
/// let i = Intersection::new(3.5, &s);
|
|
/// assert_eq!(i.t, 3.5);
|
|
/// assert_eq!(i.object, &s);
|
|
/// ```
|
|
pub fn new(t: Float, object: &Shape) -> Intersection {
|
|
Intersection { t, object }
|
|
}
|
|
}
|
|
|
|
/// Aggregates `Intersection`s.
|
|
///
|
|
/// # Examples
|
|
/// ```
|
|
/// use rtchallenge::{
|
|
/// intersections::{Intersection, Intersections},
|
|
/// rays::Ray,
|
|
/// shapes::{intersect, Shape},
|
|
/// tuples::Tuple,
|
|
/// };
|
|
///
|
|
/// let s = Shape::sphere();
|
|
/// let i1 = Intersection::new(1., &s);
|
|
/// let i2 = Intersection::new(2., &s);
|
|
/// let xs = Intersections::new(vec![i1, i2]);
|
|
/// assert_eq!(xs.len(), 2);
|
|
/// assert_eq!(xs[0].t, 1.);
|
|
/// assert_eq!(xs[1].t, 2.);
|
|
///
|
|
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
|
|
/// let xs = intersect(&s, &r);
|
|
/// assert_eq!(xs.len(), 2);
|
|
/// assert_eq!(xs[0].object, &s);
|
|
/// assert_eq!(xs[1].object, &s);
|
|
/// ```
|
|
#[derive(Debug, Default, PartialEq)]
|
|
pub struct Intersections<'i>(Vec<Intersection<'i>>);
|
|
|
|
/// Create an [Intersections] from a single [Intersection] to aid in tests.
|
|
impl<'i> From<Intersection<'i>> for Intersections<'i> {
|
|
fn from(i: Intersection<'i>) -> Intersections<'i> {
|
|
Intersections::new(vec![i])
|
|
}
|
|
}
|
|
|
|
impl<'i> Intersections<'i> {
|
|
pub fn new(xs: Vec<Intersection<'i>>) -> Intersections {
|
|
Intersections(xs)
|
|
}
|
|
pub fn len(&self) -> usize {
|
|
self.0.len()
|
|
}
|
|
/// Finds nearest hit for this collection of intersections.
|
|
pub fn hit(&self) -> Option<&Intersection> {
|
|
self.0.iter().filter(|i| i.t > 0.).min_by(|i1, i2| {
|
|
i1.t.partial_cmp(&i2.t)
|
|
.expect("an intersection has a t value that is NaN")
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<'i> IntoIterator for Intersections<'i> {
|
|
type Item = Intersection<'i>;
|
|
type IntoIter = std::vec::IntoIter<Self::Item>;
|
|
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
self.0.into_iter()
|
|
}
|
|
}
|
|
|
|
impl<'i> Index<usize> for Intersections<'i> {
|
|
type Output = Intersection<'i>;
|
|
fn index(&self, idx: usize) -> &Self::Output {
|
|
&self.0[idx]
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct PrecomputedData<'i> {
|
|
pub t: Float,
|
|
pub object: &'i Shape,
|
|
pub point: Tuple,
|
|
pub under_point: Tuple,
|
|
pub over_point: Tuple,
|
|
pub eyev: Tuple,
|
|
pub normalv: Tuple,
|
|
pub reflectv: Tuple,
|
|
pub inside: bool,
|
|
pub n1: Float,
|
|
pub n2: Float,
|
|
}
|
|
|
|
/// Precomputes data common to all intersections.
|
|
pub fn prepare_computations<'i>(
|
|
hit: &'i Intersection,
|
|
r: &Ray,
|
|
xs: &Intersections,
|
|
) -> PrecomputedData<'i> {
|
|
let point = r.position(hit.t);
|
|
let normalv = hit.object.normal_at(point);
|
|
let eyev = -r.direction;
|
|
let (inside, normalv) = if dot(normalv, eyev) < 0. {
|
|
(true, -normalv)
|
|
} else {
|
|
(false, normalv)
|
|
};
|
|
let reflectv = reflect(r.direction, normalv);
|
|
|
|
let mut n1 = -1.;
|
|
let mut n2 = -1.;
|
|
let mut containers: Vec<&Shape> = Vec::new();
|
|
for i in xs.0.iter() {
|
|
if hit == i {
|
|
if containers.is_empty() {
|
|
n1 = 1.;
|
|
} else {
|
|
n1 = containers.last().unwrap().material.refractive_index;
|
|
}
|
|
}
|
|
|
|
match containers.iter().position(|o| o == &i.object) {
|
|
Some(idx) => {
|
|
containers.remove(idx);
|
|
}
|
|
None => containers.push(i.object),
|
|
}
|
|
|
|
if hit == i {
|
|
if containers.is_empty() {
|
|
n2 = 1.;
|
|
} else {
|
|
n2 = containers.last().unwrap().material.refractive_index;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
let over_point = point + normalv * EPSILON;
|
|
let under_point = point - normalv * EPSILON;
|
|
PrecomputedData {
|
|
t: hit.t,
|
|
object: hit.object,
|
|
point,
|
|
over_point,
|
|
under_point,
|
|
normalv,
|
|
reflectv,
|
|
inside,
|
|
eyev,
|
|
n1,
|
|
n2,
|
|
}
|
|
}
|
|
|
|
/// Compute Schlick approximation to Fresnel's equations.
|
|
pub fn schlick(comps: &PrecomputedData) -> Float {
|
|
// Find the cosine of the angle between the eye and normal vectors.
|
|
let mut cos = dot(comps.eyev, comps.normalv);
|
|
// Total internal reflection can only occur if n1 > n2.
|
|
if comps.n1 > comps.n2 {
|
|
let n = comps.n1 / comps.n2;
|
|
let sin2_t = n * n * (1. - cos * cos);
|
|
if sin2_t > 1. {
|
|
return 1.;
|
|
}
|
|
// Compute cosine of theta_t using trig identity.
|
|
let cos_t = (1. - sin2_t).sqrt();
|
|
// When n1 > n2 use cos(theta_t) instead.
|
|
cos = cos_t;
|
|
}
|
|
|
|
let r0 = (comps.n1 - comps.n2) / (comps.n1 + comps.n2);
|
|
let r0 = r0 * r0;
|
|
r0 + (1. - r0) * (1. - cos) * (1. - cos) * (1. - cos) * (1. - cos) * (1. - cos)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::{
|
|
intersections::{prepare_computations, schlick, Intersection, Intersections},
|
|
materials::MaterialBuilder,
|
|
matrices::{scaling, translation},
|
|
rays::Ray,
|
|
shapes::{glass_sphere, Shape},
|
|
tuples::{point, vector},
|
|
Float, EPSILON,
|
|
};
|
|
|
|
mod hit {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn when_all_intersections_have_positive_t() {
|
|
let s = Shape::sphere();
|
|
let i1 = Intersection::new(1., &s);
|
|
let i2 = Intersection::new(2., &s);
|
|
let xs = Intersections::new(vec![i2, i1.clone()]);
|
|
let i = xs.hit();
|
|
assert_eq!(i, Some(&i1));
|
|
}
|
|
#[test]
|
|
fn when_some_intersections_have_negative_t() {
|
|
let s = Shape::sphere();
|
|
let i1 = Intersection::new(-1., &s);
|
|
let i2 = Intersection::new(1., &s);
|
|
let xs = Intersections::new(vec![i2.clone(), i1]);
|
|
let i = xs.hit();
|
|
assert_eq!(i, Some(&i2));
|
|
}
|
|
#[test]
|
|
fn when_all_intersections_have_negative_t() {
|
|
let s = Shape::sphere();
|
|
let i1 = Intersection::new(-2., &s);
|
|
let i2 = Intersection::new(-1., &s);
|
|
let xs = Intersections::new(vec![i2, i1]);
|
|
let i = xs.hit();
|
|
assert_eq!(i, None);
|
|
}
|
|
#[test]
|
|
fn always_the_lowest_nonnegative_intersection() {
|
|
let s = Shape::sphere();
|
|
let i1 = Intersection::new(5., &s);
|
|
let i2 = Intersection::new(7., &s);
|
|
let i3 = Intersection::new(-3., &s);
|
|
let i4 = Intersection::new(2., &s);
|
|
let xs = Intersections::new(vec![i1, i2, i3, i4.clone()]);
|
|
let i = xs.hit();
|
|
assert_eq!(i, Some(&i4));
|
|
}
|
|
}
|
|
|
|
mod prepare_computations {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn precomputing_the_state_of_intersection() -> Result<(), Box<dyn std::error::Error>> {
|
|
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
|
let shape = Shape::sphere();
|
|
let xs = Intersections::from(Intersection::new(4., &shape));
|
|
let comps = prepare_computations(&xs[0], &r, &xs);
|
|
assert_eq!(comps.t, xs[0].t);
|
|
assert_eq!(comps.object, xs[0].object);
|
|
assert_eq!(comps.point, point(0., 0., -1.));
|
|
assert_eq!(comps.eyev, vector(0., 0., -1.));
|
|
assert_eq!(comps.normalv, vector(0., 0., -1.));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn hit_when_intersection_occurs_outside() -> Result<(), Box<dyn std::error::Error>> {
|
|
// The hit, when an intersection occurs on the outside.
|
|
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
|
let shape = Shape::sphere();
|
|
let xs = Intersections::from(Intersection::new(4., &shape));
|
|
let comps = prepare_computations(&xs[0], &r, &xs);
|
|
assert_eq!(comps.inside, false);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn hit_when_intersection_occurs_inside() -> Result<(), Box<dyn std::error::Error>> {
|
|
// The hit, when an intersection occurs on the inside.
|
|
let r = Ray::new(point(0., 0., 0.), vector(0., 0., 1.));
|
|
let shape = Shape::sphere();
|
|
let xs = Intersections::from(Intersection::new(1., &shape));
|
|
let comps = prepare_computations(&xs[0], &r, &xs);
|
|
assert_eq!(comps.point, point(0., 0., 1.));
|
|
assert_eq!(comps.eyev, vector(0., 0., -1.));
|
|
assert_eq!(comps.inside, true);
|
|
// Normal would have been (0, 0, 1), but is inverted when inside.
|
|
assert_eq!(comps.normalv, vector(0., 0., -1.));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn hit_should_offset_the_point() -> Result<(), Box<dyn std::error::Error>> {
|
|
// The hit should offset the point.
|
|
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
|
let mut shape = Shape::sphere();
|
|
shape.set_transform(translation(0., 0., 1.));
|
|
let xs = Intersections::from(Intersection::new(5., &shape));
|
|
let comps = prepare_computations(&xs[0], &r, &xs);
|
|
assert!(comps.over_point.z < -EPSILON / 2.);
|
|
assert!(comps.point.z > comps.over_point.z);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn precomputing_the_reflection_vector() -> Result<(), Box<dyn std::error::Error>> {
|
|
// Precomputing the reflection vector.
|
|
let shape = Shape::plane();
|
|
let r = Ray::new(
|
|
point(0., 1., -1.),
|
|
vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
|
|
);
|
|
let xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape));
|
|
let comps = prepare_computations(&xs[0], &r, &xs);
|
|
assert_eq!(
|
|
comps.reflectv,
|
|
vector(0., (2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.)
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn finding_n1_and_n2() -> Result<(), Box<dyn std::error::Error>> {
|
|
// Finding n1 and n2 at various intersections.
|
|
let a = glass_sphere()
|
|
.transform(scaling(2., 2., 2.))
|
|
.material(
|
|
MaterialBuilder::default()
|
|
.transparency(1.)
|
|
.refractive_index(1.5)
|
|
.build()?,
|
|
)
|
|
.build()?;
|
|
let b = glass_sphere()
|
|
.transform(translation(0., 0., -0.25))
|
|
.material(
|
|
MaterialBuilder::default()
|
|
.transparency(1.)
|
|
.refractive_index(2.)
|
|
.build()?,
|
|
)
|
|
.build()?;
|
|
let c = glass_sphere()
|
|
.transform(translation(0., 0., 0.25))
|
|
.material(
|
|
MaterialBuilder::default()
|
|
.transparency(1.)
|
|
.refractive_index(2.5)
|
|
.build()?,
|
|
)
|
|
.build()?;
|
|
let r = Ray::new(point(0., 0., -4.), vector(0., 0., 1.));
|
|
let xs = Intersections::new(vec![
|
|
Intersection::new(2., &a),
|
|
Intersection::new(2.75, &b),
|
|
Intersection::new(3.25, &c),
|
|
Intersection::new(4.75, &b),
|
|
Intersection::new(5.25, &c),
|
|
Intersection::new(6., &a),
|
|
]);
|
|
for (index, n1, n2) in &[
|
|
(0, 1.0, 1.5),
|
|
(1, 1.5, 2.0),
|
|
(2, 2.0, 2.5),
|
|
(3, 2.5, 2.5),
|
|
(4, 2.5, 1.5),
|
|
(5, 1.5, 1.0),
|
|
] {
|
|
let comps = prepare_computations(&xs[*index], &r, &xs);
|
|
assert_eq!(comps.n1, *n1);
|
|
assert_eq!(comps.n2, *n2);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn under_point_offset_below_surface() -> Result<(), Box<dyn std::error::Error>> {
|
|
// The under point is offset below the surface.
|
|
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
|
let shape = glass_sphere().transform(translation(0., 0., 1.)).build()?;
|
|
let xs = Intersections::from(Intersection::new(5., &shape));
|
|
let comps = prepare_computations(&xs[0], &r, &xs);
|
|
assert!(comps.under_point.z > EPSILON / 2.);
|
|
assert!(comps.point.z < comps.under_point.z);
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
mod schlick {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn under_total_reflection() -> Result<(), Box<dyn std::error::Error>> {
|
|
let shape = glass_sphere().build()?;
|
|
let r = Ray::new(point(0., 0., (2 as Float).sqrt() / 2.), vector(0., 1., 0.));
|
|
let xs = Intersections::new(vec![
|
|
Intersection::new(-(2 as Float).sqrt() / 2., &shape),
|
|
Intersection::new((2 as Float).sqrt() / 2., &shape),
|
|
]);
|
|
let comps = prepare_computations(&xs[1], &r, &xs);
|
|
let reflectance = schlick(&comps);
|
|
assert_eq!(reflectance, 1.);
|
|
Ok(())
|
|
}
|
|
#[test]
|
|
fn perpendicular_viewing_angle() -> Result<(), Box<dyn std::error::Error>> {
|
|
let shape = glass_sphere().build()?;
|
|
let r = Ray::new(point(0., 0., 0.), vector(0., 1., 0.));
|
|
let xs = Intersections::new(vec![
|
|
Intersection::new(-1., &shape),
|
|
Intersection::new(1., &shape),
|
|
]);
|
|
let comps = prepare_computations(&xs[1], &r, &xs);
|
|
let reflectance = schlick(&comps);
|
|
assert!((reflectance - 0.04).abs() < EPSILON);
|
|
Ok(())
|
|
}
|
|
#[test]
|
|
fn small_angle_n2_greater_n1() -> Result<(), Box<dyn std::error::Error>> {
|
|
let shape = glass_sphere().build()?;
|
|
let r = Ray::new(point(0., 0.99, -2.), vector(0., 0., 1.));
|
|
let xs = Intersections::new(vec![Intersection::new(1.8589, &shape)]);
|
|
let comps = prepare_computations(&xs[0], &r, &xs);
|
|
let reflectance = schlick(&comps);
|
|
assert!((reflectance - 0.48873).abs() < EPSILON);
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|