def test_refraction__standard(self): """Test that we return the correct color under normal refraction""" world = copy.deepcopy(self.default_scene) # s1 = A s1 = world.objects[1] s1.material.ambient = 1.0 s1.material.pattern = patterns.TestPattern() # s2 = B s2 = world.objects[0] s2.material.transparency = 1.0 s2.material.refractive_index = 1.5 r = rays.Ray(points.Point(0, 0, 0.1), vectors.Vector(0, 1, 0)) xs = intersections.Intersections( intersections.Intersection(s1, -0.9899), intersections.Intersection(s2, -0.4899), intersections.Intersection(s2, 0.4899), intersections.Intersection(s1, 0.9899)) # Intersections[2] because the first two are behind the camera comps = xs.intersections[2].precompute(r, all_intersections=xs) result, _ = world.refracted_color(comps, 5) # Remember the test pattern returns the x, y, z coordinates of the # input point as the color so we can inspect positions self.assertEqual(result, colors.Color(0, 0.99888, 0.0475))
def test_reflection__reflective(self): """Test the reflection color of a reflective material is not black""" p = shapes.Plane(material=materials.Material(reflective=0.5)) p.set_transform(transforms.Translate(0, -1, 0)) world = copy.deepcopy(self.default_scene) world.objects.append(p) r = rays.Ray(points.Point(0, 0, -3), vectors.Vector(0, -math.sqrt(2) / 2, math.sqrt(2) / 2)) i = intersections.Intersection(p, math.sqrt(2)) comps = i.precompute(r) # Test reflected color alone result, _ = world.reflected_color(comps) self.assertEqual(result, colors.Color(0.19032, 0.2379, 0.14274)) # Now test the reflected color is added to the surface color result, _ = world.shade_hit(comps) self.assertEqual(result, colors.Color(0.87677, 0.92436, 0.82918))
def test_reflective_transparent_material(self): """Test shade_hit with a reflective, transparent material""" world = copy.deepcopy(self.default_scene) floor = shapes.Plane(material=materials.Material( reflective=0.5, transparency=0.5, refractive_index=1.5)) floor.set_transform(transforms.Translate(0, -1, 0)) ball = shapes.Sphere(material=materials.Material( color=colors.Color(1, 0, 0), ambient=0.5)) ball.set_transform(transforms.Translate(0, -3.5, -0.5)) r = rays.Ray(points.Point(0, 0, -3), vectors.Vector(0, -math.sqrt(2) / 2, math.sqrt(2) / 2)) xs = intersections.Intersections( intersections.Intersection(floor, math.sqrt(2))) world.add_object(floor) world.add_object(ball) comps = xs.intersections[0].precompute(r, all_intersections=xs) color, _ = world.shade_hit(comps, remaining=5) self.assertEqual(color, colors.Color(0.93391, 0.69643, 0.69243))
def test_intersection_miss(self): """Test we handle the case where the ray misses the sphere""" s = shapes.Sphere() r = rays.Ray(points.Point(0, 2, -5), vectors.Vector(0, 0, 1)) self.assertEqual(s.intersect(r).intersections, [])
def test_shadows__full_scene(self): """Test that we identify a shadow in a full scene""" # First sphere is at z=10 s1 = shapes.Sphere() s1.set_transform(transforms.Translate(0, 0, 10)) # Second sphere is at the origin s2 = shapes.Sphere() s2.material = materials.Material() # Light is at z=-10 l1 = lights.Light(position=points.Point(0, 0, -10), intensity=colors.Color(1, 1, 1)) scene = scenes.Scene(objects=[s1, s2], lights=[l1]) # The ray is at z=5 (i.e. between the spheres), pointing at the further # out sphere ray = rays.Ray(points.Point(0, 0, 5), vectors.Vector(0, 0, 1)) isection = intersections.Intersection(s2, 4) comps = isection.precompute(ray) result, _ = scene.shade_hit(comps) self.assertEqual(result, colors.Color(0.1, 0.1, 0.1))
def test_refraction__total_internal_reflection(self): """Test that we return black if we hit total internal reflection""" world = copy.deepcopy(self.default_scene) s = world.objects[1] s.material.transparency = 1.0 s.material.refractive_index = 1.5 r = rays.Ray(points.Point(0, 0, math.sqrt(2) / 2), vectors.Vector(0, 1, 0)) xs = intersections.Intersections( intersections.Intersection(s, -math.sqrt(2) / 2), intersections.Intersection(s, math.sqrt(2) / 2)) # n.b. the camera is inside the inner-sphere so use the second # intersection comps = xs.intersections[1].precompute(r, all_intersections=xs) result, _ = world.refracted_color(comps, 5) self.assertEqual(result, colors.Color(0, 0, 0))
def test_local_intersect(self): """Test we can calculate where a ray intersects with a plane""" p = shapes.Plane() # Rays parallel with the plane r = rays.Ray(points.Point(0, 10, 0), vectors.Vector(0, 0, 1)) xs = p.local_intersect(r) self.assertEqual(len(xs.intersections), 0) r = rays.Ray(points.Point(0, 0, 0), vectors.Vector(0, 0, 1)) xs = p.local_intersect(r) self.assertEqual(len(xs.intersections), 0) # Rays that do intersect the plane r = rays.Ray(points.Point(0, 1, 0), vectors.Vector(0, -1, 0)) xs = p.local_intersect(r) self.assertEqual(xs.intersections[0].t, 1)
def test_intersect_scene(self): """Test that we can intersect an entire scene""" r = rays.Ray(points.Point(0, 0, -5), vectors.Vector(0, 0, 1)) result = self.default_scene.intersect(r) ts = [x.t for x in result.intersections] self.assertEqual(ts, [4, 4.5, 5.5, 6])
def test_refractive_index_intersections(self): """Test we can calculate the refractive indices between intersections""" # Set up a scene with three glass spheres. One at the origin with size # 2 then inside of that 2 that are offset along z by different amounts A = shapes.Sphere(material=materials.Material(refractive_index=1.5, transparency=1.0)) B = shapes.Sphere(material=materials.Material(refractive_index=2.0, transparency=1.0)) C = shapes.Sphere(material=materials.Material(refractive_index=2.5, transparency=1.0)) A.set_transform(transforms.Scale(2, 2, 2)) B.set_transform(transforms.Translate(0, 0, -0.25)) C.set_transform(transforms.Translate(0, 0, 0.25)) r = rays.Ray(points.Point(0, 0, -4), vectors.Vector(0, 0, 1)) xs = intersections.Intersections(intersections.Intersection(A, 2), intersections.Intersection(B, 2.75), intersections.Intersection(C, 3.25), intersections.Intersection(B, 4.75), intersections.Intersection(C, 5.25), intersections.Intersection(A, 6)) expected_results = [ { "n1": 1.0, "n2": 1.5 }, { "n1": 1.5, "n2": 2.0 }, { "n1": 2.0, "n2": 2.5 }, { "n1": 2.5, "n2": 2.5 }, { "n1": 2.5, "n2": 1.5 }, { "n1": 1.5, "n2": 1.0 }, ] for index, expected in enumerate(expected_results): comps = xs.intersections[index].precompute(r, all_intersections=xs) self.assertDictEqual(expected, {"n1": comps.n1, "n2": comps.n2})
def test_position(self): origin = points.Point(2, 3, 4) direction = vectors.Vector(1, 0, 0) r = rays.Ray(origin, direction) self.assertEqual(r.position(0), origin) self.assertEqual(r.position(1), points.Point(3, 3, 4)) self.assertEqual(r.position(-1), points.Point(1, 3, 4)) self.assertEqual(r.position(2.5), points.Point(4.5, 3, 4))
def test_intersection_standard(self): """Test we can identify what points a ray intersects with a sphere""" s = shapes.Sphere() r = rays.Ray(points.Point(0, 0, -5), vectors.Vector(0, 0, 1)) result = s.intersect(r) self.assertEqual(result.intersections[0].t, 4) self.assertEqual(result.intersections[1].t, 6) self.assertEqual(result.intersections[0].shape, s) self.assertEqual(result.intersections[1].shape, s)
def test_intersection_behind(self): """Test we handle the case where the ray starts inside the sphere""" s = shapes.Sphere() r = rays.Ray(points.Point(0, 0, 5), vectors.Vector(0, 0, 1)) result = s.intersect(r) self.assertEqual(result.intersections[0].t, -6) self.assertEqual(result.intersections[1].t, -4) self.assertEqual(result.intersections[0].shape, s) self.assertEqual(result.intersections[1].shape, s)
def test_intersections_with_transformed_ray__translation(self): """Test we get the correct intersections after adding a translation to a shape """ s = shapes.Sphere() s.set_transform(transforms.Translate(5, 0, 0)) r = rays.Ray(points.Point(0, 0, -5), vectors.Vector(0, 0, 1)) result = s.intersect(r) self.assertTrue(len(result.intersections) == 0)
def test_schlick__small_angle(self): """Test that we calculate the reflectance at a small angle """ r = rays.Ray(points.Point(0, 0.99, -2), vectors.Vector(0, 0, 1)) xs = intersections.Intersections( intersections.Intersection(self.glass_sphere, 1.8589), ) comps = xs.intersections[0].precompute(r, all_intersections=xs) self.assertAlmostEqual(comps.schlick, 0.48873081)
def test_color_at(self): """Tests on the color_at function""" # The ray points away from the object r = rays.Ray(points.Point(0, 0, -5), vectors.Vector(0, 1, 0)) self.assertEqual( self.default_scene.color_at(r)[0], colors.Color(0, 0, 0)) # The ray points at the outer sphere r = rays.Ray(points.Point(0, 0, -5), vectors.Vector(0, 0, 1)) self.assertEqual( self.default_scene.color_at(r)[0], colors.Color(0.38066, 0.47583, 0.2855)) # The ray is outside the inner sphere, but inside the outer sphere, # pointing in. It should return the color of the inner sphere scene = copy.deepcopy(self.default_scene) scene.objects[0].material.ambient = 1 scene.objects[1].material.ambient = 1 r = rays.Ray(points.Point(0, 0, 0.75), vectors.Vector(0, 0, -1)) self.assertEqual(scene.color_at(r)[0], scene.objects[0].material.color)
def test_schlick__perpendicular(self): """Test that we calculate the reflectance for a perpendicular ray """ r = rays.Ray(points.Point(0, 0, 0), vectors.Vector(0, 1, 0)) xs = intersections.Intersections( intersections.Intersection(self.glass_sphere, -1), intersections.Intersection(self.glass_sphere, 1)) comps = xs.intersections[1].precompute(r, all_intersections=xs) self.assertAlmostEqual(comps.schlick, 0.04)
def test_shade_hit__outside(self): """Test that we shade an individual hit the correct color when outside an object""" r = rays.Ray(points.Point(0, 0, -5), vectors.Vector(0, 0, 1)) shape = self.default_scene.objects[1] i = intersections.Intersection(shape, 4) computations = i.precompute(r) color, _ = self.default_scene.shade_hit(computations) self.assertEqual(color, colors.Color(0.38066, 0.47583, 0.2855))
def test_ray_transforms(self): """Test that we can transform a ray""" origin = points.Point(1, 2, 3) direction = vectors.Vector(0, 1, 0) r = rays.Ray(origin, direction) r2 = r.transform(transforms.Translate(3, 4, 5)) self.assertEqual(r2.origin, points.Point(4, 6, 8)) self.assertEqual(r2.direction, vectors.Vector(0, 1, 0)) r3 = r.transform(transforms.Scale(2, 3, 4)) self.assertEqual(r3.origin, points.Point(2, 6, 12)) self.assertEqual(r3.direction, vectors.Vector(0, 3, 0))
def test_precompute__inside(self): """Test that we can precompute vectors for an intersection and ray when inside of a shape""" r = rays.Ray(points.Point(0, 0, 0), vectors.Vector(0, 0, 1)) s = shapes.Sphere() i = intersections.Intersection(s, 1) computations = i.precompute(r) self.assertEqual(computations.t, 1) self.assertEqual(computations.point, points.Point(0, 0, 1)) self.assertEqual(computations.eyev, vectors.Vector(0, 0, -1)) self.assertEqual(computations.normalv, vectors.Vector(0, 0, -1)) self.assertTrue(computations.inside)
def test_precompute_reflectv(self): """Test that we can calculate the reflection vector""" p = shapes.Plane() r = rays.Ray(points.Point(0, 1, -1), vectors.Vector(0, -math.sqrt(2) / 2, math.sqrt(2) / 2)) i = intersections.Intersection(p, math.sqrt(2)) comps = i.precompute(r) self.assertEqual(comps.reflectv, vectors.Vector(0, math.sqrt(2) / 2, math.sqrt(2) / 2))
def test_precompute__over_vector(self): """Test that we calculate a vector just inside of the surface of a shape """ r = rays.Ray(points.Point(0, 0, -5), vectors.Vector(0, 0, 1)) s = shapes.Sphere() s.set_transform(transforms.Translate(0, 0, 1)) i = intersections.Intersection(s, 5) computations = i.precompute(r) self.assertTrue(computations.under_point.z > computations.point.z) self.assertTrue(computations.under_point.z > intersections.EPSILON / 2)
def test_intersections_with_transformed_ray__scaling(self): """Test we get the correct intersections after adding a scaling to a shape """ s = shapes.Sphere() s.set_transform(transforms.Scale(2, 2, 2)) r = rays.Ray(points.Point(0, 0, -5), vectors.Vector(0, 0, 1)) result = s.intersect(r) self.assertEqual(result.intersections[0].t, 3) self.assertEqual(result.intersections[1].t, 7) self.assertEqual(result.intersections[0].shape, s) self.assertEqual(result.intersections[1].shape, s)
def is_shadowed(self, point: points.Point, light: lights.Light) -> bool: """Returns True if the point is shadowed from the light""" v = light.position - point distance = v.magnitude() direction = v.normalize() ray = rays.Ray(point, direction) intersections = self.intersect(ray) hit = intersections.hit() if hit is not None and hit.t < distance: return True return False
def test_schlick__total_internal_reflection(self): """Test that we calculate the reflectance under total internal reflection """ r = rays.Ray(points.Point(0, 0, math.sqrt(2) / 2), vectors.Vector(0, 1, 0)) xs = intersections.Intersections( intersections.Intersection(self.glass_sphere, -math.sqrt(2) / 2), intersections.Intersection(self.glass_sphere, math.sqrt(2) / 2)) comps = xs.intersections[1].precompute(r, all_intersections=xs) self.assertEqual(comps.schlick, 1)
def ray_for_pixel(self, pixel_x: int, pixel_y: int) -> rays.Ray: """Given the x and y indices of a pixel, get the ray that is fired""" xoffset = (pixel_x + 0.5) * self.pixel_size yoffset = (pixel_y + 0.5) * self.pixel_size world_x = self.half_width - xoffset world_y = self.half_height - yoffset # Using the camera's transform, change the canvas point and origin. # remember the canvas is at z=-1 pixel = self.transform.inverse() * points.Point(world_x, world_y, -1) origin = self.transform.inverse() * points.Point(0, 0, 0) direction = (pixel - origin).normalize() return rays.Ray(origin, direction)
def test_refraction__opaque(self): """Test the refraction color of an opaque material is black""" world = copy.deepcopy(self.default_scene) s = world.objects[1] r = rays.Ray(points.Point(0, 0, -5), vectors.Vector(0, 0, 1)) xs = intersections.Intersections(intersections.Intersection(s, 4), intersections.Intersection(s, 6)) comps = xs.intersections[0].precompute(r, all_intersections=xs) result, _ = world.refracted_color(comps, 5) self.assertEqual(result, colors.Color(0, 0, 0))
def test_shade_hit__inside(self): """Test that we shade an individual hit the correct color when inside an object""" r = rays.Ray(points.Point(0, 0, 0), vectors.Vector(0, 0, 1)) self.default_scene.lights = [ lights.PointLight(points.Point(0, 0.25, 0), colors.Color(1, 1, 1)) ] shape = self.default_scene.objects[0] i = intersections.Intersection(shape, 0.5) computations = i.precompute(r) color, _ = self.default_scene.shade_hit(computations) self.assertEqual(color, colors.Color(0.90498, 0.90498, 0.90498))
def test_reflection__non_reflective(self): """Test the reflection color of a nonreflective material is not black""" world = copy.deepcopy(self.default_scene) s = world.objects[0] s.material.ambient = 1 # The ray is inside the inner sphere of the default scene r = rays.Ray(points.Point(0, 0, 0), vectors.Vector(0, 0, 1)) i = intersections.Intersection(s, 1) comps = i.precompute(r) result, _ = world.reflected_color(comps) self.assertEqual(result, colors.Color(0, 0, 0))
def test_refraction__recursion_limit(self): """Test that we return black if we hit the refraction limit""" world = copy.deepcopy(self.default_scene) s = world.objects[1] s.material.transparency = 1.0 s.material.refractive_index = 1.5 r = rays.Ray(points.Point(0, 0, -5), vectors.Vector(0, 0, 1)) xs = intersections.Intersections(intersections.Intersection(s, 4), intersections.Intersection(s, 6)) comps = xs.intersections[0].precompute(r, all_intersections=xs) result, _ = world.refracted_color(comps, 0) self.assertEqual(result, colors.Color(0, 0, 0))
def test_reflection__infinite_recursion(self): """Test that we don't break if there is infinite recursion""" # Two parallel planes s1 = shapes.Plane(material=materials.Material(reflective=1)) s1.set_transform(transforms.Translate(0, -1, 0)) # Second sphere is at the origin s2 = shapes.Plane(material=materials.Material(reflective=1)) s2.set_transform(transforms.Translate(0, 1, 0)) # Light is at z=-10 l1 = lights.Light(position=points.Point(0, 0, 0), intensity=colors.Color(1, 1, 1)) scene = scenes.Scene(objects=[s1, s2], lights=[l1]) r = rays.Ray(points.Point(0, 0, 0), vectors.Vector(0, 1, 0)) # If this is working the following will NOT cause a stack trace scene.color_at(r)