開発環境
- macOS Mojave - Apple (OS)
- Emacs (Text Editor)
- Windows 10 Pro (OS)
- Visual Studio Code (Text Editor)
- Python 3.7 (プログラミング言語)
- GIMP (ビットマップ画像編集・加工ソフトウェア、PPM形式(portable pixmap)の画像用)
The Ray Tracer Challenge: A Test-Driven Guide to Your First 3D Renderer (Jamis Buck(著)、Pragmatic Bookshelf)、Chapter 11(Reflection and Refraction)のFresnel Effectを取り組んでみる。
コード
Python 3
intersections_test.py
#!/usr/bin/env python3 from unittest import TestCase, main import math from intersections import Intersection, Intersections from tuples import is_equal, Point, Vector, EPSILON from spheres import Sphere from planes import Plane from rays import Ray from transformations import translation, scaling from materials import Material class IntersectionTest(TestCase): def setUp(self): pass def tearDown(self): pass def test_intersection(self): s = Sphere() i = Intersection(3.5, s) self.assertEqual(i.t, 3.5) self.assertEqual(i.obj, s) def test_prepare_computations(self): r = Ray(Point(0, 0, -5), Vector(0, 0, 1)) shape = Sphere() i = Intersection(4, shape) comps = i.prepare_computations(r) for a, b in [(comps.t, i.t), (comps.obj, i.obj), (comps.point, Point(0, 0, -1)), (comps.eye_vector, Vector(0, 0, -1)), (comps.normal_vector, Vector(0, 0, -1))]: self.assertEqual(a, b) def test_hit_intersection_outside(self): r = Ray(Point(0, 0, -5), Vector(0, 0, 1)) shape = Sphere() i = Intersection(4, shape) comps = i.prepare_computations(r) self.assertFalse(comps.inside) def test_hit_intersection_inside(self): r = Ray(Point(0, 0, 0), Vector(0, 0, 1)) shape = Sphere() i = Intersection(1, shape) comps = i.prepare_computations(r) for a, b in [(comps.point, Point(0, 0, 1)), (comps.eye_vector, Vector(0, 0, -1)), (comps.normal_vector, Vector(0, 0, -1))]: self.assertEqual(a, b) self.assertTrue(comps.inside) def test_hit_shoud_offset_point(self): ray = Ray(Point(0, 0, -5), Vector(0, 0, 1)) shape = Sphere(transform=translation(0, 0, 1)) i = Intersection(5, shape) comps = i.prepare_computations(ray) self.assertLess(comps.over_point.z, -EPSILON / 2) self.assertGreater(comps.point.z, comps.over_point.z) def test_precomputing_reflection_vvector(self): shape = Plane() ray = Ray(Point(0, 1, -1), Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2))) intersection = Intersection(math.sqrt(2), shape) comps = intersection.prepare_computations(ray) self.assertEqual(comps.reflect_vector, Vector(0, 1 / math.sqrt(2), 1 / math.sqrt(2))) def test_n1_n2_various_intersections(self): a = glass_sphere() a.transform = scaling(2, 2, 2) a.material.refractive_index = 1.5 b = glass_sphere() b.transform = translation(0, 0, -0.25) b.material.refractive_index = 2 c = glass_sphere() c.transofrm = translation(0, 0, 0.25) c.material.refractive_index = 2.5 ray = Ray(Point(0, 0, -4), Vector(0, 0, 1)) ts = [(2, a), (2.75, b), (3.25, c), (4.75, b), (5.25, c), (6, a)] intersections = Intersections(*[Intersection(*t) for t in ts]) ns = [(1.0, 1.5), (1.5, 2.0), (2.0, 2.5), (2.5, 2.5), (2.5, 1.5), (1.5, 1.0)] for i, (n1, n2) in enumerate(ns): comps = intersections[i].prepare_computations(ray, intersections) self.assertEqual(comps.n1, n1) self.assertEqual(comps.n2, n2) def test_under_point_is_offset_below_surface(self): ray = Ray(Point(0, 0, -5), Vector(0, 0, 1)) shape = glass_sphere() shape.transform = translation(0, 0, 1) intersection = Intersection(5, shape) intersections = Intersections(intersection) comps = intersection.prepare_computations(ray, intersections) self.assertGreater(comps.under_point.z, EPSILON / 2) self.assertLess(comps.point.z, comps.under_point.z) def test_schlick_approximation_under_total_internal_reflection(self): shape = glass_sphere() ray = Ray(Point(0, 0, 1 / math.sqrt(2)), Vector(0, 1, 0)) intersections = Intersections( *[Intersection(t, shape) for t in [-1 / math.sqrt(2), 1 / math.sqrt(2)]]) computations = intersections[1].prepare_computations(ray, intersections) reflectance = computations.schlick() self.assertEqual(reflectance, 1.0) def test_schlick_approximation_with_perpendicular_viewing_angle(self): shape = glass_sphere() ray = Ray(Point(0, 0, 0), Vector(0, 1, 0)) intersections = Intersections( *[Intersection(t, shape) for t in [-1, 1]]) computations = intersections[1].prepare_computations( ray, intersections) reflectance = computations.schlick() self.assertTrue(is_equal(reflectance, 0.04)) def test_schlick_approximation_with_small_angle_and_n2_gt_n1(self): shape = glass_sphere() ray = Ray(Point(0, 0.99, -2), Vector(0, 0, 1)) intersections = Intersections(Intersection(1.8589, shape)) computations = intersections[0].prepare_computations(ray, intersections) reflectance = computations.schlick() self.assertTrue(is_equal(reflectance, 0.48873)) def glass_sphere(): return Sphere(material=Material(transparency=1.0, refractive_index=1.5)) class IntersectionsTest(TestCase): def setUp(self): pass def tearDown(self): pass def test_intersection(self): s = Sphere() i1 = Intersection(1, s) i2 = Intersection(2, s) xs = Intersections(i1, i2) for a, b in [(len(xs), 2), (xs[0].t, 1), (xs[1].t, 2)]: self.assertEqual(a, b) def test_hit(self): s = Sphere() i1 = Intersection(1, s) i2 = Intersection(2, s) xs = Intersections(i2, i1) self.assertEqual(xs.hit(), i1) def test_hit_positive_and_negative(self): s = Sphere() i1 = Intersection(-1, s) i2 = Intersection(1, s) xs = Intersections(i2, i1) self.assertEqual(xs.hit(), i2) def test_hit_none(self): s = Sphere() i1 = Intersection(-2, s) i2 = Intersection(-1, s) xs = Intersections(i2, i1) self.assertIsNone(xs.hit()) def test_hit_nonnegative(self): s = Sphere() intersections = [Intersection(t, s) for t in [5, 7, -3, 2]] xs = Intersections(*intersections) self.assertEqual(xs.hit(), intersections[-1]) if __name__ == '__main__': main()
intersections.py
import math from tuples import Point, EPSILON class Intersection: def __init__(self, t: float, obj): self.t = t self.obj = obj def __repr__(self): return f'Intersection({self.t},{self.obj})' def prepare_computations(self, ray, intersections=None): if intersections is None: intersections = [] point = ray.position(self.t) eye_vector = -ray.direction normal_vector = self.obj.normal_at(point) objs = [] n1 = 1 n2 = 1 for intersection in intersections: if intersection == self: if not objs: n1 = 1 else: n1 = objs[-1].material.refractive_index if intersection.obj in objs: objs.remove(intersection.obj) else: objs.append(intersection.obj) if intersection == self: if not objs: n2 = 1 else: n2 = objs[-1].material.refractive_index break return Computations(t=self.t, obj=self.obj, point=point, eye_vector=eye_vector, normal_vector=normal_vector, ray=ray, n1=n1, n2=n2) class Intersections: def __init__(self, *args): self.xs = list(args) self.xs.sort(key=lambda o: o.t) def __getitem__(self, i: int): return self.xs[i] def __len__(self): return len(self.xs) def __repr__(self): return f'Inersections({self.xs})' def hit(self): for i in self.xs: if i.t > 0: return i return None class Computations: def __init__(self, t, obj, point, eye_vector, normal_vector, ray, n1, n2): self.t = t self.obj = obj self.point = point self.eye_vector = eye_vector self.normal_vector = normal_vector if normal_vector.dot(eye_vector) < 0: self.inside = True self.normal_vector = -self.normal_vector else: self.inside = False self.over_point = point + self.normal_vector * EPSILON self.under_point = point - self.normal_vector * EPSILON self.reflect_vector = ray.direction.reflect(self.normal_vector) self.n1 = n1 self.n2 = n2 def __repr__(self): return f'Computations({self.t},{self.obj},{self.point},' +\ f'{self.eye_vector},{self.normal_vector},{self.inside},' + \ f'{self.over_point},{self.reflect_vector},{self.n1},{self.n2})' def schlick(self): cos = self.eye_vector.dot(self.normal_vector) if self.n1 > self.n2: n = self.n1 / self.n2 sin2t = n ** 2 * (1 - cos ** 2) if sin2t > 1: return 1 cos = math.sqrt(1 - sin2t) r = ((self.n1 - self.n2) / (self.n1 + self.n2)) ** 2 return r + (1 - r) * (1 - cos) ** 5
world_test.py
#!/usr/bin/env python3 from unittest import TestCase, main import math from tuples import Point, Vector, Color from lights import Light from spheres import Sphere from planes import Plane from materials import Material from patterns import Pattern from transformations import scaling, translation from rays import Ray from intersections import Intersection, Intersections from world import World class WorldTest(TestCase): def setUp(self): self.light = Light(Point(-10, 10, -10), Color(1, 1, 1)) self.sphere1 = Sphere(material=Material(color=Color(0.8, 1.0, 0.6), diffuse=0.7, specular=0.2)) self.sphere2 = Sphere(transform=scaling(0.5, 0.5, 0.5)) self.world = World(objs=[self.sphere1, self.sphere2], light=self.light) def tearDown(self): pass def test_world(self): w = World() self.assertEqual(len(w), 0) self.assertIsNone(w.light) def test_default_world(self): self.assertEqual(self.world.light, self.light) for s in [self.sphere1, self.sphere2]: self.assertIn(s, self.world) def test_intersect_ray(self): r = Ray(Point(0, 0, -5), Vector(0, 0, 1)) xs = self.world.intersect(r) self.assertEqual(len(xs), 4) for i, t in enumerate([4, 4.5, 5.5, 6]): self.assertEqual(xs[i].t, t) def test_shade_hit(self): r = Ray(Point(0, 0, -5), Vector(0, 0, 1)) shape = self.world[0] i = Intersection(4, shape) comps = i.prepare_computations(r) c = self.world.shade_hit(comps) self.assertEqual(c, Color(0.38066, 0.47583, 0.2855)) def test_shade_hit_from_inside(self): self.world.light = Light(Point(0, 0.25, 0), Color(1, 1, 1)) r = Ray(Point(0, 0, 0), Vector(0, 0, 1)) shape = self.world[1] i = Intersection(0.5, shape) comps = i.prepare_computations(r) c = self.world.shade_hit(comps) self.assertEqual(c, Color(0.90498, 0.90498, 0.90498)) def test_color_ray_misses(self): r = Ray(Point(0, 0, -5), Vector(0, 1, 0)) c = self.world.color_at(r) self.assertEqual(c, Color(0, 0, 0)) def test_color_ray_hits(self): r = Ray(Point(0, 0, -5), Vector(0, 0, 1)) c = self.world.color_at(r) self.assertEqual(c, Color(0.38066, 0.47583, 0.2855)) def test_color_intersection_behind_the_ray(self): outer = self.world[0] inner = self.world[1] outer.material.ambient = 1 inner.material.ambient = 1 r = Ray(Point(0, 0, 0.75), Vector(0, 0, -1)) c = self.world.color_at(r) self.assertNotEqual(c, outer.material.color) self.assertEqual(c, inner.material.color) def test_no_shadow_nothing_is_collinear_with_point_and_light(self): p = Point(0, 10, 0) self.assertFalse(self.world.is_shadowed(p)) def test_shadow_obj_between_point_and_light(self): p = Point(10, -10, 10) self.assertTrue(self.world.is_shadowed(p)) def test_no_shadow_obj_behind_light(self): p = Point(-20, 20, -20) self.assertFalse(self.world.is_shadowed(p)) def test_no_shadow_obj_behind_point(self): p = Point(-2, 2, -2) self.assertFalse(self.world.is_shadowed(p)) def test_shade_hit_given_intersection_in_shadow(self): light = Light(Point(0, 0, -10), Color(1, 1, 1)) s1 = Sphere() s2 = Sphere(translation(0, 0, 10)) w = World([s1, s2], light) ray = Ray(Point(0, 0, 5), Vector(0, 0, 1)) i = Intersection(4, s2) comps = i.prepare_computations(ray) c = w.shade_hit(comps) self.assertEqual(c, Color(0.1, 0.1, 0.1)) def test_reflected_color_for_nonreflecive_material(self): ray = Ray(Point(0, 0, 0), Vector(0, 0, 1)) shape = self.sphere2 shape.material.ambient = 1 intersection = Intersection(1, shape) comps = intersection.prepare_computations(ray) color = self.world.reflected_color(comps) self.assertEqual(color, Color(0, 0, 0)) def test_reflected_color_for_reflective_material(self): shape = Plane(material=Material(reflective=0.5), transform=translation(0, -1, 0)) self.world.objs.append(shape) ray = Ray(Point(0, 0, -3), Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2))) intersection = Intersection(math.sqrt(2), shape) comps = intersection.prepare_computations(ray) color = self.world.reflected_color(comps) self.assertEqual(color, Color(0.19033, 0.23791, 0.14274)) def test_shade_hit_with_reflective_material(self): shape = Plane(material=Material(reflective=0.5), transform=translation(0, -1, 0)) self.world.objs.append(shape) ray = Ray(Point(0, 0, -3), Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2))) intersection = Intersection(math.sqrt(2), shape) comps = intersection.prepare_computations(ray) color = self.world.shade_hit(comps) self.assertEqual(color, Color(0.87675, 0.92434, 0.82917)) def test_reflected_color_at_maximum_recursive_depth(self): shape = Plane(material=Material(reflective=0.5), transform=translation(0, -1, 0)) self.world.objs.append(shape) ray = Ray(Point(0, 0, -3), Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2))) intersection = Intersection(math.sqrt(2), shape) comps = intersection.prepare_computations(ray) color = self.world.reflected_color(comps, 0) self.assertEqual(color, Color(0, 0, 0)) def test_refracted_color_with_opaque_surface(self): shape = self.world[0] ray = Ray(Point(0, 0, -5), Vector(0, 0, 1)) intersections = Intersections( *[Intersection(t, shape) for t in [4, 6]]) computations = intersections[0].prepare_computations(ray, intersections) color = self.world.refracted_color(computations, 5) self.assertEqual(color, Color(0, 0, 0)) def test_refracted_color_at_maximum_recursive_depth(self): shape = self.world[0] shape.material.transparency = 1.0 shape.material.refractive_index = 1.5 ray = Ray(Point(0, 0, -5), Vector(0, 0, 1)) intersections = Intersections(*[Intersection(t, shape) for t in [4, 6]]) computations = intersections[0].prepare_computations(ray, intersections) color = self.world.refracted_color(computations, 0) self.assertEqual(color, Color(0, 0, 0)) def test_refracted_color_under_total_internal_reflection(self): shape = self.world[0] shape.material.transparency = 1.0 shape.material.refractive_index = 1.5 ray = Ray(Point(0, 0, 1 / math.sqrt(2)), Vector(0, 1, 0)) intersections = Intersections(*[Intersection(t, shape) for t in [-1 / math.sqrt(2), 1 / math.sqrt(2)]]) computations = intersections[1].prepare_computations(ray, intersections) color = self.world.refracted_color(computations, 5) self.assertEqual(color, Color(0, 0, 0)) def test_refracted_color_with_refracted_ray(self): a = self.world[0] a.material.ambient = 1.0 a.material.pattern = Pattern() b = self.world[1] b.material.transparency = 1 b.material.refractive_index = 1.5 ray = Ray(Point(0, 0, 0.1), Vector(0, 1, 0)) ts = [(-0.9899, a), (-0.4899, b), (0.4899, b), (0.9899, a)] intersections = Intersections( *[Intersection(t, shape) for t, shape in ts]) computations = intersections[2].prepare_computations(ray, intersections) color = self.world.refracted_color(computations, 5) self.assertEqual(color, Color(0, 0.99888, 0.04721)) def test_shade_hit_with_transpraent_material(self): floor = Plane(transform=translation(0, -1, 0), material=Material(transparency=0.5, refractive_index=1.5)) ball = Sphere(material=Material(color=Color(1, 0, 0), ambient=0.5), transform=translation(0, -3.5, -0.5)) self.world.objs += [floor, ball] ray = Ray(Point(0, 0, -3), Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2))) intersections = Intersections(Intersection(math.sqrt(2), floor)) computations = intersections[0].prepare_computations( ray, intersections) color = self.world.shade_hit(computations, 5) self.assertEqual(color, Color(0.93642, 0.68642, 0.68642)) def test_shade_hit_with_reflective_transparent_material(self): ray = Ray(Point(0, 0, -3), Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2))) floor = Plane(transform=translation(0, -1, 0), material=Material(reflective=0.5, transparency=0.5, refractive_index=1.5)) ball = Sphere(transform=translation(0, -3.5, -0.5), material=Material(Color(1, 0, 0), ambient=0.5)) self.world.objs.extend([floor, ball]) intersections = Intersections(Intersection(math.sqrt(2), floor)) computations = intersections[0].prepare_computations(ray, intersections) color = self.world.shade_hit(computations, 5) self.assertEqual(color, Color(0.93391, 0.69643, 0.69243)) if __name__ == '__main__': main()
world.py
#!/usr/bin/env python3 import math from intersections import Intersections from tuples import Color from rays import Ray class World: def __init__(self, objs=None, light=None): if objs is None: self.objs = [] else: self.objs = objs self.light = light def __getitem__(self, y): return self.objs[y] def __cointains__(self, key): return key in self.objs def __len__(self): return len(self.objs) def __repr__(self): return f'World({self.objs}, {self.light})' def intersect(self, ray): intersections = [] for obj in self.objs: intersections += obj.intersect(ray) return Intersections(*intersections) def shade_hit(self, computations, remaining=1): surface = computations.obj.material.lighting( computations.obj, self.light, computations.point, computations.eye_vector, computations.normal_vector, self.is_shadowed(computations.over_point)) reflected = self.reflected_color(computations, remaining) refracted = self.refracted_color(computations, remaining) material = computations.obj.material if material.reflective > 0 and material.transparency > 0: reflectance = computations.schlick() return surface + reflected * reflectance + \ refracted * (1 - reflectance) return surface + reflected + refracted def color_at(self, ray, remaining=1): intersections = self.intersect(ray) hit = intersections.hit() if hit is None: return Color(0, 0, 0) comps = hit.prepare_computations(ray) return self.shade_hit(comps, remaining) def is_shadowed(self, point) -> bool: vector = self.light.position - point distance = vector.magnitude() direction = vector.normalize() ray = Ray(point, direction) intersections = self.intersect(ray) hit = intersections.hit() return (hit is not None) and hit.t < distance def reflected_color(self, comps, remaining=5): if remaining <= 0: return Color(0, 0, 0) if comps.obj.material.reflective == 0: return Color(0, 0, 0) reflect_ray = Ray(comps.over_point, comps.reflect_vector) color = self.color_at(reflect_ray, remaining) return color * comps.obj.material.reflective def refracted_color(self, computations, remaining): if remaining == 0 or computations.obj.material.transparency == 0: return Color(0, 0, 0) n_ratio = computations.n1 / computations.n2 cos_i = computations.eye_vector.dot(computations.normal_vector) sin2t = n_ratio ** 2 * (1 - cos_i ** 2) if sin2t > 1: return Color(0, 0, 0) cos_t = math.sqrt(1 - sin2t) direction = computations.normal_vector * (n_ratio * cos_i - cos_t) - \ computations.eye_vector * n_ratio refract_ray = Ray(computations.under_point, direction) color = self.color_at(refract_ray, remaining-1) * \ computations.obj.material.transparency return color
sample3.py
#!/usr/bin/env python3 import math import time from tuples import Point, Vector, Color from planes import Plane from spheres import Sphere from materials import Material from patterns import Solid, Checkers from camera import Camera from lights import Light from world import World from transformations import translation, scaling, rotation_x, rotation_y from transformations import view_transform print('ファイル名, rendering time(秒)') width = 250 height = 125 light = Light(Point(-10, 10, -10), Color(1, 1, 1)) wall1 = Plane(transform=translation(0, 0, 10) * rotation_y(math.pi / 4) * rotation_x(math.pi / 2), material=Material(Color(1, 0, 0))) wall2 = Plane(transform=translation(0, 0, 10) * rotation_y(-math.pi / 4) * rotation_x(math.pi / 2), material=Material(Color(0, 1, 0))) floor = Plane(transform=translation(0, -1, 0), material=Material(reflective=0.5, transparency=0.5, refractive_index=1.5)) ball1 = Sphere(transform=translation(0, -3.5, 5), material=Material(Color(0, 0, 1), ambient=0.9)) ball2 = Sphere(transform=translation(0, -3.5, -1.5), material=Material(Color(0, 0, 1), ambient=0.9)) world = World([wall1, wall2, floor, ball1, ball2], light=light) camera = Camera(width, height, math.pi / 2, transform=view_transform( Point(0, 1.5, -10), Point(0, 0, 0), Vector(0, 1, 0))) start = time.time() canvas = camera.render(world) s = time.time() - start with open(f'sample4.ppm', 'w') as f: canvas.to_ppm(f) print(f'sample4.ppm,{s}')
入出力結果(Bash、cmd(コマンドプロンプト)、Terminal、Jupyter(IPython))
C:\Users\...>py intersections_test.py ................ ---------------------------------------------------------------------- Ran 16 tests in 0.017s OK C:\Users\...>py world_test.py ....................... ---------------------------------------------------------------------- Ran 23 tests in 0.069s OK C:\Users\...>py sample3.py ファイル名, rendering time(秒) sample4.ppm,418.6385991573334 C:\Users\...>
0 コメント:
コメントを投稿