diff --git a/rtchallenge/src/lib.rs b/rtchallenge/src/lib.rs index 9c3a0a0..563870a 100644 --- a/rtchallenge/src/lib.rs +++ b/rtchallenge/src/lib.rs @@ -46,7 +46,7 @@ pub mod prelude { materials::{Material, MaterialBuilder}, matrices::{identity, rotation_x, rotation_y, rotation_z, scaling, shearing, translation}, patterns::{checkers_pattern, gradient_pattern, ring_pattern, stripe_pattern}, - shapes::{plane, sphere, test_shape}, + shapes::{cube, plane, sphere, test_shape}, transformations::view_transform, tuples::{point, vector, Color}, world::{World, WorldBuilder}, diff --git a/rtchallenge/src/shapes.rs b/rtchallenge/src/shapes.rs index 662e9d2..22aed99 100644 --- a/rtchallenge/src/shapes.rs +++ b/rtchallenge/src/shapes.rs @@ -23,6 +23,8 @@ pub enum Geometry { 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 { @@ -38,6 +40,7 @@ impl PartialEq for Geometry { (TestShape(l), TestShape(r)) => *l.lock().unwrap() == *r.lock().unwrap(), (Sphere, Sphere) => true, (Plane, Plane) => true, + (Cube, Cube) => true, _ => false, } } @@ -70,6 +73,11 @@ 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( @@ -95,6 +103,11 @@ impl ShapeBuilder { 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()) } @@ -121,7 +134,6 @@ impl Shape { geometry: Geometry::TestShape(Arc::new(Mutex::new(TestData::default()))), } } - /// # Examples pub fn sphere() -> Shape { Shape { transform: Matrix4x4::identity(), @@ -138,6 +150,14 @@ impl Shape { 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; @@ -145,6 +165,7 @@ impl Shape { 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.; @@ -175,6 +196,7 @@ pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> { 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), } } @@ -235,6 +257,129 @@ mod plane { } } +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 {