From 7f36aecf5e8f92eab7740c051a8f21c1082039a1 Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Sun, 25 Jul 2021 16:28:34 -0700 Subject: [PATCH] world: add reflection to ray tracer. --- rtchallenge/src/camera.rs | 6 +- rtchallenge/src/intersections.rs | 16 ++- rtchallenge/src/materials.rs | 3 + rtchallenge/src/world.rs | 180 +++++++++++++++++++++++++------ 4 files changed, 169 insertions(+), 36 deletions(-) diff --git a/rtchallenge/src/camera.rs b/rtchallenge/src/camera.rs index 12e1378..e94a25c 100644 --- a/rtchallenge/src/camera.rs +++ b/rtchallenge/src/camera.rs @@ -22,6 +22,8 @@ use crate::{ Float, BLACK, }; +const MAX_DEPTH_RECURSION: usize = 5; + #[derive(Copy, Clone, StructOpt, Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum RenderStrategy { @@ -395,12 +397,12 @@ impl Camera { let color = self .supersample_rays_for_pixel(x, y, self.samples_per_pixel) .iter() - .map(|ray| w.color_at(&ray)) + .map(|ray| w.color_at(&ray, MAX_DEPTH_RECURSION)) .fold(BLACK, |acc, c| acc + c); color / self.samples_per_pixel as Float } else { let ray = self.ray_for_pixel(x, y); - w.color_at(&ray) + w.color_at(&ray, MAX_DEPTH_RECURSION) } } } diff --git a/rtchallenge/src/intersections.rs b/rtchallenge/src/intersections.rs index d0b16d9..9c47e35 100644 --- a/rtchallenge/src/intersections.rs +++ b/rtchallenge/src/intersections.rs @@ -3,7 +3,7 @@ use std::ops::Index; use crate::{ rays::Ray, shapes::Shape, - tuples::{dot, Tuple}, + tuples::{dot, reflect, Tuple}, Float, EPSILON, }; @@ -148,6 +148,7 @@ pub struct PrecomputedData<'i> { pub over_point: Tuple, pub eyev: Tuple, pub normalv: Tuple, + pub reflectv: Tuple, pub inside: bool, } @@ -160,8 +161,8 @@ pub struct PrecomputedData<'i> { /// rays::Ray, /// matrices::Matrix4x4, /// shapes::{intersect, Shape}, -/// tuples::Tuple, -/// EPSILON +/// tuples::{point,vector,Tuple}, +/// EPSILON,Float /// }; /// /// // Precomputing the state of an intersection. @@ -201,6 +202,13 @@ pub struct PrecomputedData<'i> { /// let comps = prepare_computations(&i, &r); /// 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 i = Intersection::new((2 as Float).sqrt(), &shape); +/// let comps = prepare_computations(&i, &r); +/// assert_eq!(comps.reflectv, vector(0.,(2 as Float).sqrt()/2., (2 as Float).sqrt()/2.)); /// ``` pub fn prepare_computations<'i>(i: &'i Intersection, r: &Ray) -> PrecomputedData<'i> { let point = r.position(i.t); @@ -211,6 +219,7 @@ pub fn prepare_computations<'i>(i: &'i Intersection, r: &Ray) -> PrecomputedData } else { (false, normalv) }; + let reflectv = reflect(r.direction, normalv); let over_point = point + normalv * EPSILON; PrecomputedData { @@ -219,6 +228,7 @@ pub fn prepare_computations<'i>(i: &'i Intersection, r: &Ray) -> PrecomputedData point, over_point, normalv, + reflectv, inside, eyev, } diff --git a/rtchallenge/src/materials.rs b/rtchallenge/src/materials.rs index cfec864..8446795 100644 --- a/rtchallenge/src/materials.rs +++ b/rtchallenge/src/materials.rs @@ -17,6 +17,7 @@ pub struct Material { pub diffuse: Float, pub specular: Float, pub shininess: Float, + pub reflective: Float, } impl Default for Material { @@ -35,6 +36,7 @@ impl Default for Material { /// diffuse: 0.9, /// specular: 0.9, /// shininess: 200., + /// reflective: 0.0, /// } /// ); /// ``` @@ -45,6 +47,7 @@ impl Default for Material { diffuse: 0.9, specular: 0.9, shininess: 200., + reflective: 0.0, } } } diff --git a/rtchallenge/src/world.rs b/rtchallenge/src/world.rs index 22d9cb5..6a6868d 100644 --- a/rtchallenge/src/world.rs +++ b/rtchallenge/src/world.rs @@ -93,21 +93,24 @@ impl World { /// use rtchallenge::{ /// intersections::{prepare_computations, Intersection}, /// lights::PointLight, + /// materials::MaterialBuilder, /// matrices::Matrix4x4, /// rays::Ray, - /// shapes::Shape, + /// shapes::{Shape, ShapeBuilder}, /// tuples::{Color, Tuple}, /// world::World, - /// WHITE, + /// Float, WHITE, /// }; /// + /// # fn main() -> Result<(), Box> { + /// /// // Shading an intersection. /// let w = World::test_world(); /// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.)); /// let s = &w.objects[0]; /// let i = Intersection::new(4., &s); /// let comps = prepare_computations(&i, &r); - /// let c = w.shade_hit(&comps); + /// let c = w.shade_hit(&comps, 1); /// assert_eq!(c, Color::new(0.38066, 0.47583, 0.2855)); /// /// // Shading an intersection from the inside. @@ -117,7 +120,7 @@ impl World { /// let s = &w.objects[1]; /// let i = Intersection::new(0.5, &s); /// let comps = prepare_computations(&i, &r); - /// let c = w.shade_hit(&comps); + /// let c = w.shade_hit(&comps, 1); /// assert_eq!(c, Color::new(0.90498, 0.90498, 0.90498)); /// /// // Shading with an intersection in shadow. @@ -130,16 +133,35 @@ impl World { /// let r = Ray::new(Tuple::point(0., 0., 5.), Tuple::vector(0., 0., 1.)); /// let i = Intersection::new(4., &s2); /// let comps = prepare_computations(&i, &r); - /// let c = w.shade_hit(&comps); + /// let c = w.shade_hit(&comps, 1); /// assert_eq!(c, Color::new(0.1, 0.1, 0.1)); + /// + /// // Shading with a reflective material. + /// let mut w = World::test_world(); + /// let shape = ShapeBuilder::plane() + /// .material(MaterialBuilder::default().reflective(0.5).build()?) + /// .transform(Matrix4x4::translation(0., -1., 0.)) + /// .build()?; + /// w.objects.push(shape.clone()); + /// let r = Ray::new( + /// Tuple::point(0., 0., -3.), + /// Tuple::vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.), + /// ); + /// let i = Intersection::new((2 as Float).sqrt(), &shape); + /// let comps = prepare_computations(&i, &r); + /// let c = w.shade_hit(&comps, 1); + /// assert_eq!(c, [0.87676, 0.92434, 0.82917].into()); + /// + /// # Ok(()) + /// # } /// ``` - pub fn shade_hit(&self, comps: &PrecomputedData) -> Color { - let c = self + pub fn shade_hit(&self, comps: &PrecomputedData, remaining: usize) -> Color { + let surface = self .lights .iter() .fold(Color::new(0., 0., 0.), |acc, light| { let shadowed = self.is_shadowed(comps.over_point, light); - acc + lighting( + let surface = lighting( &comps.object.material, &comps.object, light, @@ -147,9 +169,11 @@ impl World { comps.eyev, comps.normalv, shadowed, - ) + ); + acc + surface }); - c + let reflected = self.reflected_color(comps, remaining); + surface + reflected } /// Compute color for given ray fired at the world. /// @@ -158,23 +182,27 @@ impl World { /// use rtchallenge::{ /// intersections::{prepare_computations, Intersection}, /// lights::PointLight, + /// materials::MaterialBuilder, + /// matrices::translation, /// rays::Ray, - /// shapes::Shape, - /// tuples::{Color, Tuple}, - /// world::World, + /// shapes::{Shape, ShapeBuilder}, + /// tuples::{point, vector, Color, Tuple}, + /// world::{World, WorldBuilder}, /// BLACK, WHITE, /// }; /// + /// # fn main() -> Result<(), Box> { + /// /// // The color when a ray misses. /// let w = World::test_world(); /// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 1., 0.)); - /// let c = w.color_at(&r); + /// let c = w.color_at(&r, 1); /// assert_eq!(c, BLACK); /// /// // The color when a ray hits. /// let w = World::test_world(); /// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.)); - /// let c = w.color_at(&r); + /// let c = w.color_at(&r, 1); /// assert_eq!(c, Color::new(0.38066, 0.47583, 0.2855)); /// /// // The color with an intersection behind the ray. @@ -188,15 +216,34 @@ impl World { /// }; /// let inner = &w.objects[1]; /// let r = Ray::new(Tuple::point(0., 0., 0.75), Tuple::vector(0., 0., -1.)); - /// let c = w.color_at(&r); + /// let c = w.color_at(&r, 1); /// // inner.material.color is WHITE /// assert_eq!(c, WHITE); + /// + /// // Ensure mutually reflective surfaces don't infinitely recurse. + /// let lower = ShapeBuilder::plane() + /// .material(MaterialBuilder::default().reflective(1.).build()?) + /// .transform(translation(0., -1., 0.)) + /// .build()?; + /// let upper = ShapeBuilder::plane() + /// .material(MaterialBuilder::default().reflective(1.).build()?) + /// .transform(translation(0., 1., 0.)) + /// .build()?; + /// let w = WorldBuilder::default() + /// .lights(vec![PointLight::new(point(0., 0., 0.), WHITE)]) + /// .objects(vec![lower, upper]) + /// .build()?; + /// let r = Ray::new(point(0., 0., 0.), vector(0., 1., 0.)); + /// // This should complete without stack overflow. + /// w.color_at(&r, 1); + /// # Ok(()) + /// # } /// ``` - pub fn color_at(&self, r: &Ray) -> Color { + pub fn color_at(&self, r: &Ray, remaining: usize) -> Color { match self.intersect(r).hit() { Some(hit) => { let comps = prepare_computations(&hit, r); - self.shade_hit(&comps) + self.shade_hit(&comps, remaining) } None => BLACK, } @@ -206,30 +253,27 @@ impl World { /// /// # Examples /// ``` - /// use rtchallenge::{ - /// tuples::{ Tuple}, - /// shapes::Shape, - /// world::World, - /// }; + /// use rtchallenge::{shapes::Shape, tuples::Tuple, world::World}; /// /// let w = World::test_world(); /// let light = &w.lights[0]; /// /// // There is no shadow when nothing is collinear with point and light. - /// let p = Tuple::point(0.,10.,0.); - /// assert_eq!(w.is_shadowed(p,light), false); + /// let p = Tuple::point(0., 10., 0.); + /// assert_eq!(w.is_shadowed(p, light), false); /// /// // There shadow when an object is between the point and the light. - /// let p = Tuple::point(10.,-10.,10.); - /// assert_eq!(w.is_shadowed(p,light), true); + /// let p = Tuple::point(10., -10., 10.); + /// assert_eq!(w.is_shadowed(p, light), true); /// /// // There is no shadow when an object is behind the light. - /// let p = Tuple::point(-20.,20.,-20.); - /// assert_eq!(w.is_shadowed(p,light), false); + /// let p = Tuple::point(-20., 20., -20.); + /// assert_eq!(w.is_shadowed(p, light), false); /// /// // There is no shadow when an object is behind the point. - /// let p = Tuple::point(-2.,2.,-2.); - /// assert_eq!(w.is_shadowed(p,light), false); + /// let p = Tuple::point(-2., 2., -2.); + /// assert_eq!(w.is_shadowed(p, light), false); + /// ``` pub fn is_shadowed(&self, point: Tuple, light: &PointLight) -> bool { let v = light.position - point; let distance = v.magnitude(); @@ -242,4 +286,78 @@ impl World { } false } + + /// Compute reflected color for the given precomputation. + /// + /// # Examples + /// ``` + /// use rtchallenge::{ + /// intersections::{prepare_computations, Intersection}, + /// lights::PointLight, + /// materials::MaterialBuilder, + /// matrices::{translation, Matrix4x4}, + /// rays::Ray, + /// shapes::{Shape, ShapeBuilder}, + /// tuples::{point, vector, Color, Tuple}, + /// world::World, + /// Float, BLACK, + /// }; + /// + /// # fn main() -> Result<(), Box> { + /// + /// // The reflected color for a nonreflective material. + /// let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::vector(0., 0., 1.)); + /// let mut w = World::test_world(); + /// w.objects[1].material.ambient = 1.; + /// let i = Intersection::new(1., &w.objects[1]); + /// let comps = prepare_computations(&i, &r); + /// let c = w.reflected_color(&comps, 1); + /// assert_eq!(c, BLACK); + /// + /// // The reflected color for a reflective material. + /// let mut w = World::test_world(); + /// let shape = ShapeBuilder::plane() + /// .material(MaterialBuilder::default().reflective(0.5).build()?) + /// .transform(Matrix4x4::translation(0., -1., 0.)) + /// .build()?; + /// w.objects.push(shape.clone()); + /// let r = Ray::new( + /// Tuple::point(0., 0., -3.), + /// Tuple::vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.), + /// ); + /// let i = Intersection::new((2 as Float).sqrt(), &shape); + /// let comps = prepare_computations(&i, &r); + /// let c = w.reflected_color(&comps, 1); + /// assert_eq!(c, [0.19033, 0.23791, 0.14274].into()); + /// + /// // The reflected color at the maximum recursion depth. + /// let mut w = World::test_world(); + /// let shape = ShapeBuilder::plane() + /// .material(MaterialBuilder::default().reflective(0.5).build()?) + /// .transform(translation(0., -1., 0.)) + /// .build()?; + /// w.objects.push(shape.clone()); + /// let r = Ray::new( + /// point(0., 0., -3.), + /// vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.), + /// ); + /// let i = Intersection::new((2 as Float).sqrt(), &shape); + /// let comps = prepare_computations(&i, &r); + /// let c = w.reflected_color(&comps, 0); + /// + /// # Ok(()) + /// # } + /// ``` + pub fn reflected_color(&self, comps: &PrecomputedData, remaining: usize) -> Color { + if remaining == 0 { + return BLACK; + } + if comps.object.material.reflective == 0. { + return BLACK; + } + // Fire reflected ray to figure out color. + let reflect_ray = Ray::new(comps.over_point, comps.reflectv); + let color = self.color_at(&reflect_ray, remaining - 1); + color * comps.object.material.reflective + } }