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>); /// Create an [Intersections] from a single [Intersection] to aid in tests. impl<'i> From> for Intersections<'i> { fn from(i: Intersection<'i>) -> Intersections<'i> { Intersections::new(vec![i]) } } impl<'i> Intersections<'i> { pub fn new(xs: Vec>) -> 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; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl<'i> Index 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> { 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> { // 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> { // 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> { // 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> { // 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> { // 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> { // 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> { 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> { 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> { 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(()) } } }