From 5debb16d106aa35df3a236e3e117711040433af5 Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Tue, 27 Jul 2021 21:51:26 -0700 Subject: [PATCH] intersections: move tests from doctest to unit. --- rtchallenge/src/intersections.rs | 433 ++++++++++++++++--------------- 1 file changed, 223 insertions(+), 210 deletions(-) diff --git a/rtchallenge/src/intersections.rs b/rtchallenge/src/intersections.rs index 6ea80e6..cf6b525 100644 --- a/rtchallenge/src/intersections.rs +++ b/rtchallenge/src/intersections.rs @@ -79,50 +79,6 @@ impl<'i> Intersections<'i> { self.0.len() } /// Finds nearest hit for this collection of intersections. - /// - /// # Examples - /// ``` - /// use rtchallenge::{ - /// intersections::{Intersection, Intersections}, - /// rays::Ray, - /// shapes::{intersect, Shape}, - /// tuples::Tuple, - /// }; - /// - /// // The hit, 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)); - /// - /// // The hit, 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)); - /// - /// // The hit, 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); - /// - /// // The hit is 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)); - /// ``` pub fn hit(&self) -> Option<&Intersection> { self.0.iter().filter(|i| i.t > 0.).min_by(|i1, i2| { i1.t.partial_cmp(&i2.t) @@ -163,102 +119,6 @@ pub struct PrecomputedData<'i> { } /// Precomputes data common to all intersections. -/// -/// # Examples -/// ``` -/// use rtchallenge::{ -/// intersections::{prepare_computations, Intersection, Intersections}, -/// rays::Ray, -/// materials::MaterialBuilder, -/// matrices::{Matrix4x4,scaling,translation}, -/// shapes::{intersect, Shape,glass_sphere}, -/// tuples::{point,vector,Tuple}, -/// EPSILON,Float -/// }; -/// -/// # fn main() -> Result<(), Box> { -/// -/// // Precomputing the state of an intersection. -/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::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, Tuple::point(0., 0., -1.)); -/// assert_eq!(comps.eyev, Tuple::vector(0., 0., -1.)); -/// assert_eq!(comps.normalv, Tuple::vector(0., 0., -1.)); -/// -/// // The hit, when an intersection occurs on the outside. -/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::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); -/// -/// // The hit, when an intersection occurs on the inside. -/// let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::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, Tuple::point(0., 0., 1.)); -/// assert_eq!(comps.eyev, Tuple::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, Tuple::vector(0., 0., -1.)); -/// -/// // The hit should offset the point. -/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.)); -/// let mut shape = Shape::sphere(); -/// shape .set_transform(Matrix4x4::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); -/// -/// // 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.)); -/// -/// // 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); -/// } -/// -/// // 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(()) -/// # } -/// ``` pub fn prepare_computations<'i>( hit: &'i Intersection, r: &Ray, @@ -345,83 +205,236 @@ pub fn schlick(comps: &PrecomputedData) -> Float { #[cfg(test)] mod tests { use crate::{ - float::consts::SQRT_2, - intersections::{prepare_computations, Intersection, Intersections}, + intersections::{prepare_computations, schlick, Intersection, Intersections}, materials::MaterialBuilder, - matrices::translation, + matrices::{scaling, translation}, rays::Ray, - shapes::{glass_sphere, plane, sphere}, + shapes::{glass_sphere, Shape}, tuples::{point, vector}, - world::World, Float, EPSILON, }; - use super::schlick; - #[test] - fn schlick_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(()) + 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)); + } } - #[test] - fn schlick_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(()) + + 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(()) + } } - #[test] - fn schlick_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(()) - } - #[test] - fn shade_hit_reflective_transparent_material() -> Result<(), Box> { - let mut w = World::test_world(); - let floor = plane() - .transform(translation(0., -1., 0.)) - .material( - MaterialBuilder::default() - .reflective(0.5) - .transparency(0.5) - .refractive_index(1.5) - .build()?, - ) - .build()?; - w.objects.push(floor.clone()); - let ball = sphere() - .transform(translation(0., -3.5, -0.5)) - .material( - MaterialBuilder::default() - .color([1., 0., 0.]) - .ambient(0.5) - .build()?, - ) - .build()?; - w.objects.push(ball); - let r = Ray::new(point(0., 0., -3.), vector(0., -SQRT_2 / 2., SQRT_2 / 2.)); - let xs = Intersections::new(vec![Intersection::new(SQRT_2, &floor)]); - let comps = prepare_computations(&xs[0], &r, &xs); - let color = w.shade_hit(&comps, 5); - assert_eq!(color, [0.93391, 0.69643, 0.69243].into()); - 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(()) + } } }