use derive_builder::Builder; use crate::{ intersections::{prepare_computations, schlick, Intersections, PrecomputedData}, lights::PointLight, materials::{lighting, Material}, matrices::Matrix4x4, rays::Ray, shapes::{intersect, Shape}, tuples::{dot, Color, Tuple}, BLACK, WHITE, }; /// World holds all drawable objects and the light(s) that illuminate them. #[derive(Builder, Clone, Debug, Default)] #[builder(default)] pub struct World { pub lights: Vec, pub objects: Vec, } impl World { /// Creates a world suitable for use across multiple tests in from the book. pub fn test_world() -> World { let light = PointLight::new(Tuple::point(-10., 10., -10.), WHITE); let mut s1 = Shape::sphere(); s1.material = Material { color: [0.8, 1., 0.6].into(), diffuse: 0.7, specular: 0.2, ..Material::default() }; let mut s2 = Shape::sphere(); s2.set_transform(Matrix4x4::scaling(0.5, 0.5, 0.5)); World { lights: vec![light], objects: vec![s1, s2], } } /// Intersects the ray with this world. pub fn intersect(&self, r: &Ray) -> Intersections { let mut xs: Vec<_> = self .objects .iter() .map(|o| intersect(&o, &r)) .flatten() .collect(); xs.sort_by(|i1, i2| { i1.t.partial_cmp(&i2.t) .expect("an intersection has a t value that is NaN") }); Intersections::new(xs) } /// Compute shaded value for given precomputation. 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); let surface = lighting( &comps.object.material, &comps.object, light, comps.over_point, comps.eyev, comps.normalv, shadowed, ); acc + surface }); let reflected = self.reflected_color(comps, remaining); let refracted = self.refracted_color(comps, remaining); let material = &comps.object.material; if material.reflective > 0. && material.transparency > 0. { let reflectance = schlick(comps); surface + reflected * reflectance + refracted * (1. - reflectance) } else { surface + reflected + refracted } } /// Compute color for given ray fired at the world. pub fn color_at(&self, r: &Ray, remaining: usize) -> Color { let xs = self.intersect(r); match xs.hit() { Some(hit) => { let comps = prepare_computations(&hit, r, &xs); self.shade_hit(&comps, remaining) } None => BLACK, } } /// Determine if point in world is in a shadow. pub fn is_shadowed(&self, point: Tuple, light: &PointLight) -> bool { let v = light.position - point; let distance = v.magnitude(); let direction = v.normalize(); let r = Ray::new(point, direction); let intersections = self.intersect(&r); if let Some(h) = intersections.hit() { return h.t < distance; } false } /// Compute reflected color for the given precomputation. 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 } /// Compute refracted color for the given precomputation. pub fn refracted_color(&self, comps: &PrecomputedData, remaining: usize) -> Color { if remaining == 0 { return BLACK; } if comps.object.material.transparency == 0. { return BLACK; } // Find the ratio of the first index of refraction to the secon // (This is inverted from the definition of Snell's Law.) let n_ratio = comps.n1 / comps.n2; // cos(theta_i) is the same as the dot product of the two vectors // TODO(wathiede): is cosine faster than doc productings? let cos_i = dot(comps.eyev, comps.normalv); // Find the sin(theta_t)^2 via trigonometric identity. let sin2_t = n_ratio * n_ratio * (1. - cos_i * cos_i); if sin2_t > 1. { // Total internal reflection. return BLACK; } // Find cos(theta_t) via trigonometric identity. let cos_t = (1. - sin2_t).sqrt(); // Compute the direction of the refracted ray. let direction = comps.normalv * (n_ratio * cos_i - cos_t) - comps.eyev * n_ratio; // Create the refracted ray. let refract_ray = Ray::new(comps.under_point, direction); // Find he color of the refracted ray, making sure to multiply by the transparency value to // account for any opacity. self.color_at(&refract_ray, remaining - 1) * comps.object.material.transparency } } #[cfg(test)] mod tests { use crate::{ float::consts::SQRT_2, intersections::{prepare_computations, Intersection, Intersections}, lights::PointLight, materials::MaterialBuilder, matrices::translation, patterns::test_pattern, rays::Ray, shapes::{plane, sphere, Shape, ShapeBuilder}, tuples::{point, vector}, world::{World, WorldBuilder}, Float, BLACK, WHITE, }; mod intersect { use super::*; #[test] fn intersection_with_test_world() { let w = World::test_world(); let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.)); let xs = w.intersect(&r); assert_eq!(xs.len(), 4); assert_eq!(xs[0].t, 4.); assert_eq!(xs[1].t, 4.5); assert_eq!(xs[2].t, 5.5); assert_eq!(xs[3].t, 6.); } } mod world { use super::*; #[test] fn default_world() { let w = World::default(); assert!(w.objects.is_empty()); assert_eq!(w.lights.len(), 0); } #[test] fn test_world() { let w = World::test_world(); assert_eq!(w.objects.len(), 2); assert!(!w.lights.is_empty()); } } mod shade_hit { use super::*; #[test] fn shading_an_intersection() { // Shading an intersection. let w = World::test_world(); let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.)); let s = &w.objects[0]; let xs = Intersections::from(Intersection::new(4., &s)); let comps = prepare_computations(&xs[0], &r, &xs); let c = w.shade_hit(&comps, 1); assert_eq!(c, [0.38066, 0.47583, 0.2855].into()); } #[test] fn shading_an_intersection_from_inside() { // Shading an intersection from the inside. let mut w = World::test_world(); w.lights = vec![PointLight::new(point(0., 0.25, 0.), WHITE)]; let r = Ray::new(point(0., 0., 0.), vector(0., 0., 1.)); let s = &w.objects[1]; let xs = Intersections::from(Intersection::new(0.5, &s)); let comps = prepare_computations(&xs[0], &r, &xs); let c = w.shade_hit(&comps, 1); assert_eq!(c, [0.90498, 0.90498, 0.90498].into()); } #[test] fn shading_an_intersection_in_shadow() { // Shading with an intersection in shadow. let mut w = World::default(); w.lights = vec![PointLight::new(point(0., 0., -10.), WHITE)]; let s1 = Shape::sphere(); let mut s2 = Shape::sphere(); s2.set_transform(translation(0., 0., 10.)); w.objects = vec![s1, s2.clone()]; let r = Ray::new(point(0., 0., 5.), vector(0., 0., 1.)); let xs = Intersections::from(Intersection::new(4., &s2)); let comps = prepare_computations(&xs[0], &r, &xs); let c = w.shade_hit(&comps, 1); assert_eq!(c, [0.1, 0.1, 0.1].into()); } #[test] fn shading_with_a_reflective_material() -> Result<(), Box> { // Shading with a reflective material. 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 xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape)); let comps = prepare_computations(&xs[0], &r, &xs); let c = w.shade_hit(&comps, 1); assert_eq!(c, [0.87676, 0.92434, 0.82917].into()); Ok(()) } #[test] fn shading_with_a_transparent_material() -> Result<(), Box> { // shade_hit() with a transparent material. let mut w = World::test_world(); let floor = plane() .transform(translation(0., -1., 0.)) .material( MaterialBuilder::default() .transparency(0.5) .refractive_index(1.5) .build()?, ) .build()?; w.objects.push(floor.clone()); let ball = sphere() .material( MaterialBuilder::default() .color([1., 0., 0.]) .ambient(0.5) .build()?, ) .transform(translation(0., -3.5, -0.5)) .build()?; w.objects.push(ball); let r = Ray::new( point(0., 0., -3.), vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.), ); let xs = Intersections::new(vec![Intersection::new((2 as Float).sqrt(), &floor)]); let comps = prepare_computations(&xs[0], &r, &xs); let c = w.shade_hit(&comps, 5); assert_eq!(c, [0.93642, 0.68642, 0.68643].into()); Ok(()) } #[test] fn reflective_transparent_material() -> Result<(), Box> { let mut w = World::test_world(); let floor = plane() .transform(translation(0., -1., 0.)) .material( MaterialBuilder::default() .reflective(0.5) .transparency(0.5) .refractive_index(1.5) .build()?, ) .build()?; w.objects.push(floor.clone()); let ball = sphere() .transform(translation(0., -3.5, -0.5)) .material( MaterialBuilder::default() .color([1., 0., 0.]) .ambient(0.5) .build()?, ) .build()?; w.objects.push(ball); let r = Ray::new(point(0., 0., -3.), vector(0., -SQRT_2 / 2., SQRT_2 / 2.)); let xs = Intersections::new(vec![Intersection::new(SQRT_2, &floor)]); let comps = prepare_computations(&xs[0], &r, &xs); let color = w.shade_hit(&comps, 5); assert_eq!(color, [0.93391, 0.69643, 0.69243].into()); Ok(()) } } mod color_at { use super::*; #[test] fn color_when_ray_misses() { // The color when a ray misses. let w = World::test_world(); let r = Ray::new(point(0., 0., -5.), vector(0., 1., 0.)); let c = w.color_at(&r, 1); assert_eq!(c, BLACK); } #[test] fn color_when_ray_hits() { // The color when a ray hits. let w = World::test_world(); let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.)); let c = w.color_at(&r, 1); assert_eq!(c, [0.38066, 0.47583, 0.2855].into()); } #[test] fn color_with_an_intersection_behind_ray() { // The color with an intersection behind the ray. let w = { let mut w = World::test_world(); let mut outer = &mut w.objects[0]; outer.material.ambient = 1.; let inner = &mut w.objects[1]; inner.material.ambient = 1.; w }; let r = Ray::new(point(0., 0., 0.75), vector(0., 0., -1.)); let c = w.color_at(&r, 1); // inner.material.color is WHITE assert_eq!(c, WHITE); } #[test] fn ensure_mutually_reflective_surfaces_terminate() -> Result<(), Box> { // 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(()) } } mod is_shadowed { use super::*; #[test] fn no_shadow_when_nothing_collinear_with_point_and_light() { // There is no shadow when nothing is collinear with point and light. let w = World::test_world(); let light = &w.lights[0]; let p = point(0., 10., 0.); assert_eq!(w.is_shadowed(p, light), false); } #[test] fn shadow_when_object_between_point_and_light() { // The shadow when an object is between the point and the light. let w = World::test_world(); let light = &w.lights[0]; let p = point(10., -10., 10.); assert_eq!(w.is_shadowed(p, light), true); } #[test] fn no_shadow_when_object_behind_light() { // There is no shadow when an object is behind the light. let w = World::test_world(); let light = &w.lights[0]; let p = point(-20., 20., -20.); assert_eq!(w.is_shadowed(p, light), false); } #[test] fn no_shadow_when_object_behind_point() { // There is no shadow when an object is behind the point. let w = World::test_world(); let light = &w.lights[0]; let p = point(-2., 2., -2.); assert_eq!(w.is_shadowed(p, light), false); } } mod reflected_color { use super::*; #[test] fn reflected_color_for_nonreflective_material() { // The reflected color for a nonreflective material. let r = Ray::new(point(0., 0., 0.), vector(0., 0., 1.)); let mut w = World::test_world(); w.objects[1].material.ambient = 1.; let xs = Intersections::from(Intersection::new(1., &w.objects[1])); let comps = prepare_computations(&xs[0], &r, &xs); let c = w.reflected_color(&comps, 1); assert_eq!(c, BLACK); } #[test] fn reflected_color_for_reflective_material() -> Result<(), Box> { // 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(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 xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape)); let comps = prepare_computations(&xs[0], &r, &xs); let c = w.reflected_color(&comps, 1); assert_eq!(c, [0.19033, 0.23791, 0.14274].into()); Ok(()) } #[test] fn reflected_color_at_maximum_recursion_depth() -> Result<(), Box> { // 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 xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape)); let comps = prepare_computations(&xs[0], &r, &xs); let _ = w.reflected_color(&comps, 0); // Just needs to get here without infinite recursion. Ok(()) } } mod refracted_color { use super::*; #[test] fn refracted_color_with_opaque_surface() { // The refracted color with an opaque surface. let w = World::test_world(); let shape = &w.objects[0]; let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.)); let xs = Intersections::new(vec![ Intersection::new(4., &shape), Intersection::new(6., &shape), ]); let comps = prepare_computations(&xs[0], &r, &xs); let c = w.refracted_color(&comps, 5); assert_eq!(c, BLACK); } #[test] fn refracted_color_at_maximum_recursion_depth() { // The refracted color at the maximum recursive depth. let mut w = World::test_world(); w.objects[0].material.transparency = 1.0; w.objects[0].material.refractive_index = 1.5; let shape = &w.objects[0]; let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.)); let xs = Intersections::new(vec![ Intersection::new(4., &shape), Intersection::new(6., &shape), ]); let comps = prepare_computations(&xs[0], &r, &xs); let c = w.refracted_color(&comps, 0); assert_eq!(c, BLACK); } #[test] fn refracted_color_under_total_internal_reflection() { // The refracted color under total internal reflection. let mut w = World::test_world(); w.objects[0].material.transparency = 1.0; w.objects[0].material.refractive_index = 1.5; let shape = &w.objects[0]; let r = Ray::new(point(0., 0., (2 as Float).sqrt() / 2.), vector(0., 1., 0.)); let xs = Intersections::new(vec![ Intersection::new(-(2 as Float).sqrt() / 2., &shape), Intersection::new((2 as Float).sqrt() / 2., &shape), ]); let comps = prepare_computations(&xs[1], &r, &xs); let c = w.refracted_color(&comps, 5); assert_eq!(c, BLACK); } #[test] fn refracted_color_with_a_refracted_ray() -> Result<(), Box> { // The refracted color with a refracted ray. let mut w = World::test_world(); w.objects[0].material.ambient = 1.; w.objects[0].material.color = test_pattern().build()?; w.objects[1].material.transparency = 1.; w.objects[1].material.refractive_index = 1.5; let r = Ray::new(point(0., 0., 0.1), vector(0., 1., 0.)); let a = &w.objects[0]; let b = &w.objects[1]; let xs = Intersections::new(vec![ Intersection::new(-0.9899, &a), Intersection::new(-0.4899, &b), Intersection::new(0.4899, &b), Intersection::new(0.9899, &a), ]); let comps = prepare_computations(&xs[2], &r, &xs); let c = w.refracted_color(&comps, 5); assert_eq!(c, [0., 0.99887, 0.04721].into()); Ok(()) } } }