/*! * 07_making_scene.cpp * * Copyright (c) 2015-2024, NADAL Jean-Baptiste. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA * * @Author: NADAL Jean-Baptiste * @Date: 14/02/2024 * */ /*---------------------------------------------------------------------------*/ #include #include #include "raytracing.h" using namespace Raytracer; /* ------------------------------------------------------------------------- */ SCENARIO("Creating a world", "[features/world.feature]") { GIVEN("w <- world()") { World w; THEN("w contains no objects") { REQUIRE(w.objects_count() == 0); } AND_THEN("w has no light source") { REQUIRE(w.has_light() == false); } } } /* ------------------------------------------------------------------------- */ SCENARIO("The default world", "[features/world.feature]") { GIVEN("light <- point_light(point(-10, 10, -10), color(1, 1, 1))") { PointLight light = PointLight(Tuple::Point(-10, 10, -10), Color(1, 1, 1)); AND_GIVEN("s1 <- sphere") // with: // | material.color | (0.8, 1.0, 0.6) | // | material.diffuse | 0.7 | // | material.specular | 0.2 | { Sphere s1; s1.material().set_color(Color(0.8, 1.0, 0.6)); s1.material().set_diffuse(0.7); s1.material().set_specular(0.2); AND_GIVEN("s2 <- sphere") // with: // | transform | scaling(0.5, 0.5, 0.5) | { Sphere s2; s2.set_transform(Matrix::scaling(0.5, 0.5, 0.5)); WHEN("w <- default_world") { World w = World::default_world(); THEN("w.light = light") { REQUIRE(w.light() == light); } AND_THEN("w contains s1") { REQUIRE(w.contains(s1) == true); } AND_THEN("w contains s2") { REQUIRE(w.contains(s2) == true); } } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("Intersect a world with a ray", "[features/world.feature]") { GIVEN("w <- default_world()") { World w = World::default_world(); AND_GIVEN("r <- ray(point(0, 0, -5), vector(0, 0, 1))") { Ray r(Tuple::Point(0, 0, -5), Tuple::Vector(0, 0, 1)); WHEN("xs <- intersect_world(w,r)") { Intersections xs = w.intersect_world(r); THEN("xs.count = 4") { REQUIRE(xs.count() == 4); } AND_THEN("xs[0].t = 4") { REQUIRE(xs[0].distance_t() == 4); } AND_THEN("xs[1].t = 4.5") { REQUIRE(xs[1].distance_t() == 4.5); } AND_THEN("xs[2].t = 5.5") { REQUIRE(xs[2].distance_t() == 5.5); } AND_THEN("xs[3].t = 6") { REQUIRE(xs[3].distance_t() == 6); } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("Precompute the state of an intersection", "[features/intersections.feature]") { GIVEN("r <- ray(point(0, 0, -5), vector(0, 0, 1))") { Ray r(Tuple::Point(0, 0, -5), Tuple::Vector(0, 0, 1)); AND_GIVEN("shape <- sphere()") { Sphere shape; AND_GIVEN("i <- intersection(4, shape)") { Intersection i(4, &shape); WHEN("comps <- prepare_computations(i,r)") { IntersectionData comps = i.prepare_computations(r); THEN("comps.t = i.t") { REQUIRE(comps.distance_t() == i.distance_t()); } AND_THEN("comps.object = i.object") { REQUIRE(comps.object() == i.object()); } AND_THEN("comps.point = point(0, 0, -1)") { REQUIRE(comps.point() == Tuple::Point(0, 0, -1)); } AND_THEN("comps.eyev = vector(0, 0, -1)") { REQUIRE(comps.eyev() == Tuple::Vector(0, 0, -1)); } AND_THEN("comps.normalv = vector(0, 0, -1)") { REQUIRE(comps.normalv() == Tuple::Vector(0, 0, -1)); } } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("The hit, when an intersection occurs on the outside", "[features/intersections.feature]") { GIVEN("r <- ray(point(0, 0, -5), vector(0, 0, 1))") { Ray r(Tuple::Point(0, 0, -5), Tuple::Vector(0, 0, 1)); AND_GIVEN("shape <- sphere()") { Sphere shape; AND_GIVEN("i <- intersection(4, shape)") { Intersection i(4, &shape); WHEN("comps <- prepare_computations(i,r)") { IntersectionData comps = i.prepare_computations(r); THEN("comps.inside = false") { REQUIRE(comps.is_inside() == false); } } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("The hit, when an intersection occurs on the inside", "[features/intersections.feature]") { GIVEN("r <- ray(point(0, 0, 0), vector(0, 0, 1))") { Ray r(Tuple::Point(0, 0, 0), Tuple::Vector(0, 0, 1)); AND_GIVEN("shape <- sphere()") { Sphere shape; AND_GIVEN("i <- intersection(1, shape)") { Intersection i(1, &shape); WHEN("comps <- prepare_computations(i,r)") { IntersectionData comps = i.prepare_computations(r); THEN("comps.point = point(0, 0, 1)") { REQUIRE(comps.point() == Tuple::Point(0, 0, 1)); } AND_THEN("comps.eyev = vector(0, 0, -1)") { REQUIRE(comps.eyev() == Tuple::Vector(0, 0, -1)); } AND_THEN("comps.inside = false") { REQUIRE(comps.is_inside() == true); } AND_THEN("comps.normalv = vector(0, 0, -1)") { REQUIRE(comps.normalv() == Tuple::Vector(0, 0, -1)); } } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("Shading an intersection", "[features/world.feature]") { GIVEN("w <- default_world()") { World w = World::default_world(); AND_GIVEN("r <- ray(point(0, 0, -5), vector(0, 0, 1))") { Ray r(Tuple::Point(0, 0, -5), Tuple::Vector(0, 0, 1)); AND_GIVEN("shape <- first object of w") { Shape *shape = w.objects(0); AND_GIVEN("i <- intersection(4, shape)") { Intersection i(4, shape); WHEN("comps <- prepare_computations(i,r)") { IntersectionData comps = i.prepare_computations(r); AND_WHEN("c <- shade_hit(w, comps)") { Color c = w.shade_hit(comps); THEN("c = color(0.38066, 0.47583, 0.2855)") { REQUIRE(c == Color(0.38066, 0.47583, 0.2855)); } } } } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("Shading an intersection from the inside", "[features/world.feature]") { GIVEN("w <- default_world()") { World w = World::default_world(); AND_GIVEN("w.light <- point_light(point(0, 0.25, 0))") { w.set_light(PointLight(Tuple::Point(0, 0.25, 0), Color(1, 1, 1))); AND_GIVEN("r <- ray(point(0, 0, 0), vector(0, 0, 1))") { Ray r(Tuple::Point(0, 0, 0), Tuple::Vector(0, 0, 1)); AND_GIVEN("shape <- second object of w") { Shape *shape = w.objects(1); AND_GIVEN("i <- intersection(0.5, shape)") { Intersection i(0.5, shape); WHEN("comps <- prepare_computations(i,r)") { IntersectionData comps = i.prepare_computations(r); AND_WHEN("c <- shade_hit(w, comps)") { Color c = w.shade_hit(comps); THEN("c = color(0.90498, 0.90498, 0.90498)") { REQUIRE(c == Color(0.90498, 0.90498, 0.90498)); } } } } } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("The color when a ray misses", "[features/world.feature]") { GIVEN("w <- default_world()") { World w = World::default_world(); AND_GIVEN("r <- ray(point(0, 0, -5), vector(0, 1, 0))") { Ray r(Tuple::Point(0, 0, -5), Tuple::Vector(0, 1, 0)); WHEN("c <- color_at(w, r)") { Color c = w.color_at(r); THEN("c = color(0, 0, 0)") { REQUIRE(c == Color(0, 0, 0)); } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("The color when a ray hits", "[features/world.feature]") { GIVEN("w <- default_world()") { World w = World::default_world(); AND_GIVEN("r <- ray(point(0, 0, -5), vector(0, 0, 1))") { Ray r(Tuple::Point(0, 0, -5), Tuple::Vector(0, 0, 1)); WHEN("c <- color_at(w, r)") { Color c = w.color_at(r); THEN("c = color(0.38066, 0.47583, 0.2855)") { REQUIRE(c == Color(0.38066, 0.47583, 0.2855)); } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("The color with an intersection behind the ray", "[features/world.feature]") { GIVEN("w <- default_world()") { World w = World::default_world(); AND_GIVEN("outer <- the first object in w") { Shape *outer = w.objects(0); AND_GIVEN("outer.material.ambient <- 1") { outer->material().set_ambient(1); AND_GIVEN("inner <- the second object in w") { Shape *inner = w.objects(1); AND_GIVEN("inner.material.ambient <- 1") { inner->material().set_ambient(1); AND_GIVEN("r <- ray(point(0, 0, 0.75), vector(0, 0, -1))") { Ray r(Tuple::Point(0, 0, 0.75), Tuple::Vector(0, 0, -1)); WHEN("c <- color_at(w, r)") { Color c = w.color_at(r); THEN("c = inner.material.color") { REQUIRE(c == inner->material().color()); } } } } } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("The transformation matrix for the default orientation", "[features/transformations.feature]") { GIVEN("from <- point(0, 0, 0)") { Tuple from = Tuple::Point(0, 0, 0); AND_GIVEN("to <- point(0, 0, -1)") { Tuple to = Tuple::Point(0, 0, -1); AND_GIVEN("up <- vector(0, 1, 0)") { Tuple up = Tuple::Vector(0, 1, 0); WHEN("t <- view_transform(from, to, up)") { Matrix t = Matrix::view_transform(from, to, up); THEN("t = identify_matrix") { REQUIRE(t == Matrix::identity()); } } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("A view transformation matrix looking in positive z direction", "[features/transformations.feature]") { GIVEN("from <- point(0, 0, 0)") { Tuple from = Tuple::Point(0, 0, 0); AND_GIVEN("to <- point(0, 0, 1)") { Tuple to = Tuple::Point(0, 0, 1); AND_GIVEN("up <- vector(0, 1, 0)") { Tuple up = Tuple::Vector(0, 1, 0); WHEN("t <- view_transform(from, to, up)") { Matrix t = Matrix::view_transform(from, to, up); THEN("t = scaling(-1, 1, -1)") { REQUIRE(t == Matrix::scaling(-1, 1, -1)); } } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("A view transformation moves the world", "[features/transformations.feature]") { GIVEN("from <- point(0, 0, 8)") { Tuple from = Tuple::Point(0, 0, 8); AND_GIVEN("to <- point(0, 0, 0)") { Tuple to = Tuple::Point(0, 0, 0); AND_GIVEN("up <- vector(0, 1, 0)") { Tuple up = Tuple::Vector(0, 1, 0); WHEN("t <- view_transform(from, to, up)") { Matrix t = Matrix::view_transform(from, to, up); THEN("t = translation(0, 0, -8)") { REQUIRE(t == Matrix::translation(0, 0, -8)); } } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("An arbitrary view transformation", "[features/transformations.feature]") { GIVEN("from <- point(1, 3, 2)") { Tuple from = Tuple::Point(1, 3, 2); AND_GIVEN("to <- point(4, -2, 8)") { Tuple to = Tuple::Point(4, -2, 8); AND_GIVEN("up <- vector(1, 1, 0)") { Tuple up = Tuple::Vector(1, 1, 0); WHEN("t <- view_transform(from, to, up)") { Matrix t = Matrix::view_transform(from, to, up); THEN("t iq the following 4x4 matrix") { // | -0.50709 | 0.50709 | 0.67612 | -2.36643 | // | 0.76772 | 0.60609 | 0.12122 | -2.82843 | // | -0.35857 | 0.59761 | -0.71714 | 0.00000 | // | 0.00000 | 0.00000 | 0.00000 | 1.00000 | REQUIRE(t == Matrix({ {-0.50709, 0.50709, 0.67612, -2.36643}, { 0.76772, 0.60609, 0.12122, -2.82843}, {-0.35857, 0.59761, -0.71714, 0.00000}, { 0.00000, 0.00000, 0.00000, 1.00000} })); } } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("Constructing a camera", "[features/camera.feature]") { GIVEN("hsize <- 160") { uint16_t hsize = 160; AND_GIVEN("vsize <- 120") { uint16_t vsize = 120; AND_GIVEN("field_of_view <- pi / 2") { double field_of_view = std::numbers::pi / 2; WHEN("c <- camera(hsize, vsize,field_of_view)") { Camera c(hsize, vsize, field_of_view); THEN("c.hsize = 160") { REQUIRE(c.hsize() == 160); } AND_THEN("c.vsize = 120") { REQUIRE(c.vsize() == 120); } AND_THEN("c.field_of_view = pi / 2") { REQUIRE(c.field_of_view() == std::numbers::pi / 2); } AND_THEN("c.transform = identity_matrix") { REQUIRE(c.transform() == Matrix::identity()); } } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("The pixel size for a horizontal canvas", "[features/camera.feature]") { GIVEN("c <- camera(200, 125, pi/2)") { Camera c(200, 125, std::numbers::pi / 2); THEN("c.pixel_size = 0.01") { REQUIRE(c.pixel_size() == 0.01); } } } /* ------------------------------------------------------------------------- */ SCENARIO("The pixel size for a vertical canvas", "[features/camera.feature]") { GIVEN("c <- camera(125, 200, pi/2)") { Camera c(125, 200, std::numbers::pi / 2); THEN("c.pixel_size = 0.01") { REQUIRE(c.pixel_size() == 0.01); } } } /* ------------------------------------------------------------------------- */ SCENARIO("Constructing a ray through the center of the canvas", "[features/camera.feature]") { GIVEN("c <- camera(201, 101, pi/2)") { Camera c(201, 101, std::numbers::pi / 2); WHEN("r <- ray_for_pixel(c, 100, 50)") { Ray r = c.ray_for_pixel(100, 50); THEN("r.origin = point(0, 0, 0)") { REQUIRE(r.origin() == Tuple::Point(0, 0, 0)); } } } } /* ------------------------------------------------------------------------- */ SCENARIO("Constructing a ray through a corner of the canvas", "[features/camera.feature]") { GIVEN("c <- camera(201, 101, pi/2)") { Camera c(201, 101, std::numbers::pi / 2); WHEN("r <- ray_for_pixel(c, 0, 0)") { Ray r = c.ray_for_pixel(0, 0); THEN("r.origin = point(0, 0, 0)") { REQUIRE(r.origin() == Tuple::Point(0, 0, 0)); } AND_THEN("r.direction = vector(0.66519, 0.33259, -0.66851)") { REQUIRE(r.direction() == Tuple::Vector(0.66519, 0.33259, -0.66851)); } } } } /* ------------------------------------------------------------------------- */ SCENARIO("Constructing a ray when the camera is transformed", "[features/camera.feature]") { GIVEN("c <- camera(201, 101, pi/2)") { Camera c(201, 101, std::numbers::pi / 2); WHEN("c.transform <- rotation_y(pi/4) * translation(0, -2, 5)") { c.set_transform(Matrix::rotation_y(std::numbers::pi / 4) * Matrix::translation(0, -2, 5)); AND_WHEN("r <- ray_for_pixel(c, 100, 50)") { Ray r = c.ray_for_pixel(100, 50); THEN("r.origin = point(0, 2, -5)") { REQUIRE(r.origin() == Tuple::Point(0, 2, -5)); } AND_THEN("r.direction = vector(sqrt(2) / 2, 0, -sqrt(2) / 2)") { REQUIRE(r.direction() == Tuple::Vector(std::sqrt(2) / 2, 0, -std::sqrt(2) / 2)); } } } } } /* ------------------------------------------------------------------------- */ SCENARIO("Rendering a world with the camera", "[features/camera.feature]") { GIVEN("w <- default_world()") { World w = World::default_world(); AND_GIVEN("c <- camera(11, 11, pi/2)") { Camera c(11, 11, std::numbers::pi / 2); AND_GIVEN("from <- point(0, 0, -5)") { Tuple from = Tuple::Point(0, 0, -5); AND_GIVEN("to <- point(0, 0, 0)") { Tuple to = Tuple::Point(0, 0, 0); AND_GIVEN("up <- vector(0, 1, 0)") { Tuple up = Tuple::Vector(0, 1, 0); AND_GIVEN("c.transform <- view_transform(from, to, up)") { c.set_transform(Matrix::view_transform(from, to, up)); WHEN("image <- c.render(w)") { Canvas image = c.render(w); THEN("pixel_at(image, 5, 5) = color(0.38066, 0.47583, 0.2855)") { REQUIRE(image.pixel_at(5, 5) == Color(0.38066, 0.47583, 0.2855)); } } } } } } } } }