diff --git a/ray-tracer/src/types/intersection.rs b/ray-tracer/src/types/intersection.rs new file mode 100644 index 0000000..e69de29 diff --git a/ray-tracer/src/types/mod.rs b/ray-tracer/src/types/mod.rs index 56a7236..4b05a25 100644 --- a/ray-tracer/src/types/mod.rs +++ b/ray-tracer/src/types/mod.rs @@ -2,6 +2,8 @@ mod canvas; mod color; mod matrix; mod point; +mod ray; +mod sphere; mod tuple; mod vector; @@ -9,6 +11,8 @@ pub use canvas::Canvas; pub use color::Color; pub use matrix::Matrix; pub use point::Point; +pub use ray::Ray; +pub use sphere::Sphere; pub use tuple::Tuple; pub use vector::Vector; diff --git a/ray-tracer/src/types/ray.rs b/ray-tracer/src/types/ray.rs new file mode 100644 index 0000000..87f590c --- /dev/null +++ b/ray-tracer/src/types/ray.rs @@ -0,0 +1,176 @@ +use std::cmp::Ordering; + +use crate::types::{Point, Sphere, Vector}; + +#[derive(Clone, Debug, PartialEq)] +pub struct Intersection<'a> { + t: f64, + object: &'a Sphere, +} + +pub struct Intersections<'a>(Vec>); + +impl <'a> Intersections<'a> { + pub fn len(&'a self) -> usize { + self.0.len() + } + + pub fn hit(&'a self) -> Option<&Intersection<'a>> { + self.0.iter().find(|i| i.t >= 0.) + } +} + +impl <'a> std::ops::Index for Intersections<'a> { + type Output = Intersection<'a>; + fn index(&self, idx: usize) -> &Intersection<'a> { + &self.0[idx] + } +} + +impl <'a> From>> for Intersections<'a> { + fn from(mut v: Vec>) -> Self { + v.sort_by(|l, r| l.t.partial_cmp(&r.t).unwrap_or(Ordering::Equal)); + Self(v) + } +} + +pub struct Ray { + origin: Point, + direction: Vector, +} + +impl Ray { + pub fn new(origin: Point, direction: Vector) -> Self { + Self { origin, direction } + } + + pub fn position(&self, t: f64) -> Point { + self.origin + self.direction * t + } + + pub fn intersect<'a>(&self, s: &'a Sphere) -> Intersections<'a> { + let sphere_to_ray = self.origin - Point::new(0., 0., 0.); + let a = self.direction.dot(&self.direction); + let b = 2. * self.direction.dot(&sphere_to_ray); + let c = sphere_to_ray.dot(&sphere_to_ray) - 1.; + + let discriminant = b * b - 4. * a * c; + if discriminant < 0. { + return vec![].into(); + } + + let t1 = (-b - discriminant.sqrt()) / (2. * a); + let t2 = (-b + discriminant.sqrt()) / (2. * a); + + vec![ + Intersection { t: t1, object: &s }, + Intersection { t: t2, object: &s }, + ].into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn computing_point_from_distance() { + let r = Ray::new(Point::new(2., 3., 4.), Vector::new(1., 0., 0.)); + assert_eq!(r.position(0.), Point::new(2., 3., 4.)); + assert_eq!(r.position(1.), Point::new(3., 3., 4.)); + assert_eq!(r.position(-1.), Point::new(1., 3., 4.)); + assert_eq!(r.position(2.5), Point::new(4.5, 3., 4.)); + } + + #[test] + fn ray_intersects_sphere_at_two_points() { + let r = Ray::new(Point::new(0., 0., -5.), Vector::new(0., 0., 1.)); + let s = Sphere::new(); + let xs = r.intersect(&s); + assert_eq!(xs.len(), 2); + assert_eq!(xs[0].t, 4.); + assert_eq!(xs[1].t, 6.); + assert_eq!(*xs[0].object, s); + assert_eq!(*xs[1].object, s); + } + + #[test] + fn ray_tangents_sphere_at_one_point() { + let r = Ray::new(Point::new(0., 1., -5.), Vector::new(0., 0., 1.)); + let s = Sphere::new(); + let xs = r.intersect(&s); + assert_eq!(xs.len(), 2); + assert_eq!(xs[0].t, 5.); + assert_eq!(xs[1].t, 5.); + } + + #[test] + fn ray_misses_the_sphere() { + let r = Ray::new(Point::new(0., 2., -5.), Vector::new(0., 0., 1.)); + let s = Sphere::new(); + let xs = r.intersect(&s); + assert_eq!(xs.len(), 0); + } + + #[test] + fn ray_originates_inside_the_sphere() { + let r = Ray::new(Point::new(0., 0., 0.), Vector::new(0., 0., 1.)); + let s = Sphere::new(); + let xs = r.intersect(&s); + assert_eq!(xs.len(), 2); + assert_eq!(xs[0].t, -1.); + assert_eq!(xs[1].t, 1.); + } + + #[test] + fn sphere_is_behind_the_ray() { + let r = Ray::new(Point::new(0., 0., 5.), Vector::new(0., 0., 1.)); + let s = Sphere::new(); + let xs = r.intersect(&s); + assert_eq!(xs.len(), 2); + assert_eq!(xs[0].t, -6.); + assert_eq!(xs[1].t, -4.); + } + + #[test] + fn hit_all_intersections_are_positive() { + let s = Sphere::new(); + let i1 = Intersection{ t: 1., object: &s }; + let i2 = Intersection{ t: 2., object: &s }; + let xs = Intersections::from(vec![i1.clone(), i2]); + + assert_eq!(xs.hit(), Some(&i1)); + } + + #[test] + fn hit_some_intersections_are_negative() { + let s = Sphere::new(); + let i1 = Intersection{ t: -1., object: &s }; + let i2 = Intersection{ t: 1., object: &s }; + let xs = Intersections::from(vec![i1, i2.clone()]); + + assert_eq!(xs.hit(), Some(&i2)); + } + + #[test] + fn hit_all_intersections_are_negative() { + let s = Sphere::new(); + let i1 = Intersection{ t: -2., object: &s }; + let i2 = Intersection{ t: -1., object: &s }; + let xs = Intersections::from(vec![i1, i2]); + + assert_eq!(xs.hit(), None); + } + + #[test] + fn hit_is_always_lowest_nonnegative() { + let s = Sphere::new(); + let i1 = Intersection{ t: 5., object: &s }; + let i2 = Intersection{ t: 7., object: &s }; + let i3 = Intersection{ t: -3., object: &s }; + let i4 = Intersection{ t: 2., object: &s }; + let xs = Intersections::from(vec![i1, i2, i3, i4.clone()]); + + assert_eq!(xs.hit(), Some(&i4)); + } +} diff --git a/ray-tracer/src/types/sphere.rs b/ray-tracer/src/types/sphere.rs new file mode 100644 index 0000000..6b5eb00 --- /dev/null +++ b/ray-tracer/src/types/sphere.rs @@ -0,0 +1,13 @@ +use crate::types::Point; + +#[derive(Debug, PartialEq)] +pub struct Sphere { + origin: Point, +} + +impl Sphere { + pub fn new() -> Self { + Self{ origin: Point::new(0., 0., 0.) } + } +} +