From e041fd1f6a365947ea3d2d796e1cd64d6fddde2f Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Wed, 21 Jul 2021 19:41:40 -0700 Subject: [PATCH] shapes: implement TestShape. --- rtchallenge/src/shapes.rs | 128 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 5 deletions(-) diff --git a/rtchallenge/src/shapes.rs b/rtchallenge/src/shapes.rs index 1f64a98..b526164 100644 --- a/rtchallenge/src/shapes.rs +++ b/rtchallenge/src/shapes.rs @@ -1,20 +1,41 @@ +use std::sync::{Arc, Mutex}; + use crate::{ intersections::Intersections, materials::Material, matrices::Matrix4x4, rays::Ray, tuples::Tuple, }; -#[derive(Debug, PartialEq, Clone)] -enum Geometry { +#[derive(Default, PartialEq, Debug, Clone)] +pub struct TestData { + pub saved_ray: Option, +} + +#[derive(Debug, Clone)] +pub enum Geometry { + /// Shape with predictable normals useful for debugging. + TestShape(Arc>), /// Sphere represents the unit-sphere (radius of unit 1.) at the origin 0., 0., 0. Sphere, /// Flat surface that extends infinitely in the XZ axes. Plane, } +impl PartialEq for Geometry { + fn eq(&self, rhs: &Geometry) -> bool { + use Geometry::*; + match (self, rhs) { + (TestShape(l), TestShape(r)) => *l.lock().unwrap() == *r.lock().unwrap(), + (Sphere, Sphere) => true, + (Plane, Plane) => true, + _ => false, + } + } +} + /// Shape represents visible objects. A signal instance of Shape can generically represent one of /// many different shapes based on the value of it's geometry field. Users chose the shape by /// calling the appropriate constructor, i.e. [Shape::sphere]. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Shape { transform: Matrix4x4, inverse_transform: Matrix4x4, @@ -23,6 +44,33 @@ pub struct Shape { } impl Shape { + /// Create a test shape useful for debugging. + /// + /// # Examples + /// ``` + /// use rtchallenge::{materials::Material, matrices::Matrix4x4, shapes::Shape}; + /// + /// let mut s = Shape::test_shape(); + /// // The default transform. + /// assert_eq!(s.transform(), Matrix4x4::identity()); + /// // The default material. + /// assert_eq!(s.material, Material::default()); + /// // Assigning a material. + /// let mut m = Material { + /// ambient: 1., + /// ..Material::default() + /// }; + /// s.material = m.clone(); + /// assert_eq!(s.material, m); + /// ``` + pub fn test_shape() -> Shape { + Shape { + transform: Matrix4x4::identity(), + inverse_transform: Matrix4x4::identity(), + material: Material::default(), + geometry: Geometry::TestShape(Arc::new(Mutex::new(TestData::default()))), + } + } /// # Examples /// ``` /// use rtchallenge::{materials::Material, matrices::Matrix4x4, shapes::Shape}; @@ -71,6 +119,22 @@ impl Shape { /// Float, /// }; /// + /// // Computing the normal on a translated shape. + /// let mut s = Shape::test_shape(); + /// s.set_transform(Matrix4x4::translation(0., 1., 0.)); + /// let n = s.normal_at(Tuple::point(0., 1.70711, -0.70711)); + /// assert_eq!(n, Tuple::vector(0., 0.70711, -0.70711)); + /// + /// // Computing the normal on a transform shape. + /// let mut s = Shape::test_shape(); + /// s.set_transform(Matrix4x4::scaling(1., 0.5, 1.) * Matrix4x4::rotation_z(PI / 5.)); + /// let n = s.normal_at(Tuple::point( + /// 0., + /// (2. as Float).sqrt() / 2., + /// -(2. as Float).sqrt() / 2., + /// )); + /// assert_eq!(n, Tuple::vector(0., 0.97014, -0.24254)); + /// /// // Normal on X-axis /// let s = Shape::sphere(); /// let n = s.normal_at(Tuple::point(1., 0., 0.)); @@ -147,6 +211,7 @@ impl Shape { let object_normal = match self.geometry { Geometry::Sphere => object_point - Tuple::point(0., 0., 0.), Geometry::Plane => Tuple::vector(0., 1., 0.), + Geometry::TestShape(_) => object_point, }; let mut world_normal = self.inverse_transform.transpose() * object_normal; world_normal.w = 0.; @@ -158,6 +223,7 @@ impl Shape { let object_normal = match self.geometry { Geometry::Sphere => object_point - Tuple::point(0., 0., 0.), Geometry::Plane => Tuple::vector(0., 1., 0.), + Geometry::TestShape(_) => todo!("test shape normal"), }; let mut world_normal = self.transform.inverse().transpose() * object_normal; world_normal.w = 0.; @@ -172,9 +238,12 @@ impl Shape { self.transform = t; self.inverse_transform = t.inverse(); } + pub fn geometry(&self) -> &Geometry { + &self.geometry + } } -/// Intersect a ray with a sphere. +/// Intersect a ray with a shapes. /// /// # Examples /// ``` @@ -182,10 +251,42 @@ impl Shape { /// intersections::{Intersection, Intersections}, /// matrices::Matrix4x4, /// rays::Ray, -/// shapes::{intersect, Shape}, +/// shapes::{intersect, Geometry, Shape}, /// tuples::Tuple, /// }; /// +/// // Intersecting a scaled shape with a ray. +/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.)); +/// let mut s = Shape::test_shape(); +/// s.set_transform(Matrix4x4::scaling(2., 2., 2.)); +/// let xs = intersect(&s, &r); +/// if let Geometry::TestShape(data) = s.geometry() { +/// if let Some(ray) = &data.lock().unwrap().saved_ray { +/// assert_eq!(ray.origin, Tuple::point(0., 0., -2.5)); +/// assert_eq!(ray.direction, Tuple::vector(0., 0., 0.5)); +/// } else { +/// panic!("ray wasn't set"); +/// }; +/// } else { +/// panic!("test_shape returned a non-TestShape geometry") +/// }; +/// +/// // Intersecting a translated shape with a ray. +/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.)); +/// let mut s = Shape::test_shape(); +/// s.set_transform(Matrix4x4::translation(5., 0., 0.)); +/// let xs = intersect(&s, &r); +/// if let Geometry::TestShape(data) = s.geometry() { +/// if let Some(ray) = &data.lock().unwrap().saved_ray { +/// assert_eq!(ray.origin, Tuple::point(-5., 0., -5.)); +/// assert_eq!(ray.direction, Tuple::vector(0., 0., 1.)); +/// } else { +/// panic!("ray wasn't set"); +/// }; +/// } else { +/// panic!("test_shape returned a non-TestShape geometry") +/// }; +/// /// // A ray intersects a sphere in two points. /// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.)); /// let s = Shape::sphere(); @@ -265,6 +366,23 @@ pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> { match shape.geometry { Geometry::Sphere => sphere::intersect(shape, &local_ray), Geometry::Plane => plane::intersect(shape, &local_ray), + Geometry::TestShape(_) => test_shape::intersect(shape, &local_ray), + } +} + +mod test_shape { + use crate::{ + intersections::Intersections, + rays::Ray, + shapes::{Geometry, Shape}, + }; + pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> { + if let Geometry::TestShape(s) = &shape.geometry { + s.lock() + .expect("couldn't grab mutex for TestData") + .saved_ray = Some(ray.clone()); + } + Intersections::default() } }