use std::sync::{Arc, Mutex}; use derive_builder::Builder; use crate::{ intersections::Intersections, materials::{Material, MaterialBuilder}, matrices::Matrix4x4, rays::Ray, tuples::Tuple, }; #[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, /// AABB cube at origin from -1,1 in each direction. Cube, } impl Default for Geometry { fn default() -> Geometry { Geometry::Sphere } } 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, (Cube, Cube) => 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(Builder, Debug, Clone, PartialEq)] #[builder(default, pattern = "owned")] pub struct Shape { transform: Matrix4x4, #[builder(private, default = "self.default_inverse_transform()?")] inverse_transform: Matrix4x4, pub material: Material, geometry: Geometry, } /// Short hand for creating a ShapeBuilder with a plane geometry. pub fn plane() -> ShapeBuilder { ShapeBuilder::plane() } /// Short hand for creating a ShapeBuilder with a sphere geometry. pub fn sphere() -> ShapeBuilder { ShapeBuilder::sphere() } /// Short hand for creating a ShapeBuilder with a test shape geometry. pub fn test_shape() -> ShapeBuilder { ShapeBuilder::test_shape() } /// Short hand for creating a ShapeBuilder with a cube geometry. pub fn cube() -> ShapeBuilder { ShapeBuilder::cube() } /// Helper for producing a sphere with a glassy material. pub fn glass_sphere() -> ShapeBuilder { ShapeBuilder::sphere().material( MaterialBuilder::default() .transparency(1.) .refractive_index(1.5) .build() .unwrap(), ) } impl ShapeBuilder { /// Short hand for creating a ShapeBuilder with a plane geometry. pub fn plane() -> ShapeBuilder { ShapeBuilder::default().geometry(Geometry::Plane) } /// Short hand for creating a ShapeBuilder with a sphere geometry. pub fn sphere() -> ShapeBuilder { ShapeBuilder::default().geometry(Geometry::Sphere) } /// Short hand for creating a ShapeBuilder with a test shape geometry. pub fn test_shape() -> ShapeBuilder { ShapeBuilder::default().geometry(Geometry::TestShape(Arc::new(Mutex::new( TestData::default(), )))) } /// Short hand for creating a ShapeBuilder with a cube geometry. pub fn cube() -> ShapeBuilder { ShapeBuilder::default().geometry(Geometry::Cube) } fn default_inverse_transform(&self) -> Result { Ok(self.transform.unwrap_or(Matrix4x4::identity()).inverse()) } } impl Default for Shape { fn default() -> Shape { Shape { transform: Matrix4x4::identity(), inverse_transform: Matrix4x4::identity(), material: Material::default(), geometry: Geometry::default(), } } } impl Shape { /// Create a test shape useful for debugging. 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()))), } } pub fn sphere() -> Shape { Shape { transform: Matrix4x4::identity(), inverse_transform: Matrix4x4::identity(), material: Material::default(), geometry: Geometry::Sphere, } } pub fn plane() -> Shape { Shape { transform: Matrix4x4::identity(), inverse_transform: Matrix4x4::identity(), material: Material::default(), geometry: Geometry::Plane, } } pub fn cube() -> Shape { Shape { transform: Matrix4x4::identity(), inverse_transform: Matrix4x4::identity(), material: Material::default(), geometry: Geometry::Cube, } } /// Find the normal at the point on the sphere. pub fn normal_at(&self, world_point: Tuple) -> Tuple { let object_point = self.inverse_transform * world_point; 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, Geometry::Cube => cube::local_normal_at(object_point), }; let mut world_normal = self.inverse_transform.transpose() * object_normal; world_normal.w = 0.; world_normal.normalize() } pub fn transform(&self) -> Matrix4x4 { self.transform } pub fn inverse_transform(&self) -> Matrix4x4 { self.inverse_transform } pub fn set_transform(&mut self, t: Matrix4x4) { self.transform = t; self.inverse_transform = t.inverse(); } pub fn geometry(&self) -> &Geometry { &self.geometry } } /// Intersect a ray with a shapes. pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> { let local_ray = ray.transform(shape.inverse_transform); 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), Geometry::Cube => cube::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() } } mod sphere { use crate::{ intersections::{Intersection, Intersections}, rays::Ray, shapes::Shape, tuples::{dot, Tuple}, }; pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> { let sphere_to_ray = ray.origin - Tuple::point(0., 0., 0.); let a = dot(ray.direction, ray.direction); let b = 2. * dot(ray.direction, sphere_to_ray); let c = dot(sphere_to_ray, sphere_to_ray) - 1.; let discriminant = b * b - 4. * a * c; if discriminant < 0. { return Intersections::default(); } Intersections::new(vec![ Intersection::new((-b - discriminant.sqrt()) / (2. * a), &shape), Intersection::new((-b + discriminant.sqrt()) / (2. * a), &shape), ]) } } mod plane { use crate::{ intersections::{Intersection, Intersections}, rays::Ray, shapes::Shape, EPSILON, }; pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> { if (ray.direction.y).abs() < EPSILON { return Intersections::default(); } Intersections::new(vec![Intersection::new( -ray.origin.y / ray.direction.y, &shape, )]) } } mod cube { use crate::{ intersections::{Intersection, Intersections}, rays::Ray, shapes::Shape, tuples::{vector, Tuple}, Float, EPSILON, }; fn check_axis(origin: Float, direction: Float) -> (Float, Float) { let tmin_numerator = -1. - origin; let tmax_numerator = 1. - origin; let (tmin, tmax) = if direction.abs() >= EPSILON { (tmin_numerator / direction, tmax_numerator / direction) } else { ( tmin_numerator * Float::INFINITY, tmax_numerator * Float::INFINITY, ) }; if tmin > tmax { return (tmax, tmin); } (tmin, tmax) } pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> { let (xtmin, xtmax) = check_axis(ray.origin.x, ray.direction.x); let (ytmin, ytmax) = check_axis(ray.origin.y, ray.direction.y); let (ztmin, ztmax) = check_axis(ray.origin.z, ray.direction.z); let tmin = xtmin.max(ytmin).max(ztmin); let tmax = xtmax.min(ytmax).min(ztmax); if tmin > tmax { return Intersections::default(); } Intersections::new(vec![ Intersection::new(tmin, &shape), Intersection::new(tmax, &shape), ]) } pub fn local_normal_at(point: Tuple) -> Tuple { let x = point.x.abs(); let y = point.y.abs(); let z = point.z.abs(); let maxc = x.max(y).max(z); if maxc == x { return vector(point.x, 0., 0.); } if maxc == y { return vector(0., point.y, 0.); } vector(0., 0., point.z) } #[cfg(test)] mod tests { use crate::{ rays::Ray, shapes::Shape, tuples::{point, vector}, EPSILON, }; use super::{intersect, local_normal_at}; #[test] fn ray_intersects_cube() { let c = Shape::cube(); for (name, o, d, t1, t2) in [ ("+x", point(5., 0.5, 0.), vector(-1., 0., 0.), 4., 6.), ("-x", point(-5., 0.5, 0.), vector(1., 0., 0.), 4., 6.), ("+y", point(0.5, 5., 0.), vector(0., -1., 0.), 4., 6.), ("-y", point(0.5, -5., 0.), vector(0., 1., 0.), 4., 6.), ("+z", point(0.5, 0., 5.), vector(0., 0., -1.), 4., 6.), ("-z", point(0.5, 0., -5.), vector(0., 0., 1.), 4., 6.), ("inside", point(0., 0.5, 0.), vector(0., 0., 1.), -1., 1.), ] { let r = Ray::new(o, d); let xs = intersect(&c, &r); assert_eq!(xs.len(), 2, "{}", name); assert!((xs[0].t - t1).abs() < EPSILON, "{} t1 {}", name, xs[0].t); assert!((xs[1].t - t2).abs() < EPSILON, "{} t2 {}", name, xs[1].t); } } #[test] fn ray_misses_cube() { let c = Shape::cube(); for (o, d) in [ (point(-2., 0., 0.), vector(0.2673, 0.5345, 0.8018)), (point(0., -2., 0.), vector(0.8018, 0.2673, 0.5345)), (point(0., 0., -2.), vector(0.5345, 0.8018, 0.2673)), (point(2., 0., 2.), vector(0., 0., -1.)), (point(0., 2., 2.), vector(0., -1., 0.)), (point(2., 2., -5.), vector(-1., 0., 0.)), ] { let r = Ray::new(o, d); let xs = intersect(&c, &r); assert_eq!(xs.len(), 0, "({:?}, {:?})", o, d); } } #[test] fn normal_cube_surface() { for (p, n) in [ (point(1., 0.8, -0.8), vector(1., 0., 0.)), (point(-1., -0.2, 0.9), vector(-1., 0., 0.)), (point(-0.4, 1., -0.1), vector(0., 1., 0.)), (point(0.3, -1., -0.7), vector(0., -1., 0.)), (point(-0.6, 0.3, 1.), vector(0., 0., 1.)), (point(0.4, 0.4, -1.), vector(0., 0., -1.)), (point(1., 1., 1.), vector(1., 0., 0.)), (point(-1., -1., -1.), vector(-1., 0., 0.)), ] { let normal = local_normal_at(p); assert_eq!(n, normal); } } } } #[cfg(test)] mod tests { mod shape_builder { use std::error::Error; use crate::shapes::{plane, sphere, test_shape, Shape}; #[test] fn plane_builder() -> Result<(), Box> { assert_eq!(plane().build()?, Shape::plane()); Ok(()) } #[test] fn sphere_builder() -> Result<(), Box> { assert_eq!(sphere().build()?, Shape::sphere()); Ok(()) } #[test] fn test_shape_builder() -> Result<(), Box> { assert_eq!(test_shape().build()?, Shape::test_shape()); Ok(()) } } mod shape { use crate::{ materials::Material, matrices::{identity, translation}, shapes::Shape, }; #[test] fn test_shape() { let mut s = Shape::test_shape(); // The default transform. assert_eq!(s.transform(), identity()); // The default material. assert_eq!(s.material, Material::default()); // Assigning a material. let m = Material { ambient: 1., ..Material::default() }; s.material = m.clone(); assert_eq!(s.material, m); } #[test] fn sphere() { // A sphere's default transform is the identity matrix. let s = Shape::sphere(); assert_eq!(s.transform(), identity()); // It can be changed by directly setting the transform member. let mut s = Shape::sphere(); let t = translation(2., 3., 4.); s.set_transform(t.clone()); assert_eq!(s.transform(), t); // Default Sphere has the default material. assert_eq!(s.material, Material::default()); // It can be overridden. let mut s = Shape::sphere(); let mut m = Material::default(); m.ambient = 1.; s.material = m.clone(); assert_eq!(s.material, m); } } mod normal_at { use crate::{float::consts::PI, matrices::Matrix4x4, shapes::Shape, tuples::Tuple, Float}; #[test] fn compute_normal_on_translated_shape() { // 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)); } #[test] fn compute_normal_on_scaled_shape() { // Computing the normal on a scaled 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)); } #[test] fn sphere_normal_on_x_axis() { // Normal on X-axis let s = Shape::sphere(); let n = s.normal_at(Tuple::point(1., 0., 0.)); assert_eq!(n, Tuple::vector(1., 0., 0.)); } #[test] fn sphere_normal_on_y_axis() { // Normal on Y-axis let s = Shape::sphere(); let n = s.normal_at(Tuple::point(0., 1., 0.)); assert_eq!(n, Tuple::vector(0., 1., 0.)); } #[test] fn sphere_normal_on_z_axis() { // Normal on Z-axis let s = Shape::sphere(); let n = s.normal_at(Tuple::point(0., 0., 1.)); assert_eq!(n, Tuple::vector(0., 0., 1.)); } #[test] fn sphere_normal_non_axial() { // Normal on a sphere at a nonaxial point. let s = Shape::sphere(); let n = s.normal_at(Tuple::point( (3. as Float).sqrt() / 3., (3. as Float).sqrt() / 3., (3. as Float).sqrt() / 3., )); assert_eq!( n, Tuple::vector( (3. as Float).sqrt() / 3., (3. as Float).sqrt() / 3., (3. as Float).sqrt() / 3., ) ); } #[test] fn normals_are_normalized() { // Normals returned are normalized. let s = Shape::sphere(); let n = s.normal_at(Tuple::point( (3. as Float).sqrt() / 3., (3. as Float).sqrt() / 3., (3. as Float).sqrt() / 3., )); assert_eq!(n, n.normalize()); } #[test] fn compute_normal_on_translated_sphere() { // Compute the normal on a translated sphere. let mut s = Shape::sphere(); 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)); } #[test] fn compute_normal_on_scaled_sphere() { // Compute the normal on a transformed sphere. let mut s = Shape::sphere(); 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)); } #[test] fn nomal_of_plane_constant() { // Normal of a plane is constant everywhere. let p = Shape::plane(); assert_eq!( p.normal_at(Tuple::point(0., 0., 0.)), Tuple::vector(0., 1., 0.) ); assert_eq!( p.normal_at(Tuple::point(10., 0., -10.)), Tuple::vector(0., 1., 0.) ); assert_eq!( p.normal_at(Tuple::point(-5., 0., 150.)), Tuple::vector(0., 1., 0.) ); } } mod intersect { use crate::{ intersections::{Intersection, Intersections}, matrices::Matrix4x4, rays::Ray, shapes::{intersect, Geometry, Shape}, tuples::Tuple, }; #[test] fn scaled_shape_with_ray() { // 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") }; } #[test] fn translated_shape_with_ray() { // 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") }; } #[test] fn ray_intersects_sphere_two_points() { // 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(); let xs = intersect(&s, &r); assert_eq!( xs, Intersections::new(vec![Intersection::new(4., &s), Intersection::new(6., &s)]) ); } #[test] fn ray_intersects_at_tangent() { // A ray intersects a sphere at a tangent. let r = Ray::new(Tuple::point(0., 2., -5.), Tuple::vector(0., 0., 1.)); let s = Shape::sphere(); let xs = intersect(&s, &r); assert_eq!(xs, Intersections::default()); } #[test] fn ray_originates_inside_sphere() { // A ray originates inside a sphere. let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::vector(0., 0., 1.)); let s = Shape::sphere(); let xs = intersect(&s, &r); assert_eq!( xs, Intersections::new(vec![Intersection::new(-1., &s), Intersection::new(1., &s)]) ); } #[test] fn sphere_behind_ray() { // A sphere is behind a ray. let r = Ray::new(Tuple::point(0., 0., 5.), Tuple::vector(0., 0., 1.)); let s = Shape::sphere(); let xs = intersect(&s, &r); assert_eq!( xs, Intersections::new(vec![Intersection::new(-6., &s), Intersection::new(-4., &s)]) ); } #[test] fn ray_intersects_scaled_sphere() { // Intersect a scaled sphere with a ray. let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.)); let mut s = Shape::sphere(); s.set_transform(Matrix4x4::scaling(2., 2., 2.)); let xs = intersect(&s, &r); assert_eq!(xs.len(), 2, "xs {:?}", xs); assert_eq!(xs[0].t, 3., "xs {:?}", xs); assert_eq!(xs[1].t, 7., "xs {:?}", xs); } #[test] fn ray_intersects_translated_sphere() { // Intersect a translated sphere with a ray. let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.)); let mut s = Shape::sphere(); s.set_transform(Matrix4x4::translation(5., 0., 0.)); let xs = intersect(&s, &r); assert_eq!(xs.len(), 0); } #[test] fn ray_parallel_to_plane() { // Intersect with a ray parallel to the plane. let p = Shape::plane(); let r = Ray::new(Tuple::point(0., 10., 0.), Tuple::vector(0., 0., 1.)); let xs = intersect(&p, &r); assert_eq!(xs.len(), 0); } #[test] fn ray_coplanar_to_plane() { // Intersect with a coplanar. let p = Shape::plane(); let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::vector(0., 0., 1.)); let xs = intersect(&p, &r); assert_eq!(xs.len(), 0); } #[test] fn ray_intersects_plane_from_above() { // A ray intersecting a plane from above. let p = Shape::plane(); let r = Ray::new(Tuple::point(0., 1., 0.), Tuple::vector(0., -1., 0.)); let xs = intersect(&p, &r); assert_eq!(xs.len(), 1); assert_eq!(xs[0].t, 1.); assert_eq!(xs[0].object, &p); } #[test] fn ray_intersects_plane_from_below() { // A ray intersecting a plane from below. let p = Shape::plane(); let r = Ray::new(Tuple::point(0., -1., 0.), Tuple::vector(0., 1., 0.)); let xs = intersect(&p, &r); assert_eq!(xs.len(), 1); assert_eq!(xs[0].t, 1.); assert_eq!(xs[0].object, &p); } } }