開発環境
- 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)のReflectionを取り組んでみる。
コード
Python 3
materials_test.py
#!/usr/bin/env python3 from unittest import TestCase, main from materials import Material from tuples import Point, Vector, Color from lights import Light from patterns import Solid, Stripe from spheres import Sphere import math class MaterialTest(TestCase): def setUp(self): self.m = Material() self.position = Point(0, 0, 0) self.obj = Sphere() def tearDown(self): pass def test_marial(self): m = Material() tests = [(m.color, Color(1, 1, 1)), (m.ambient, 0.1), (m.diffuse, 0.9), (m.specular, 0.9), (m.shininess, 200)] for a, b in tests: self.assertEqual(a, b) def test_lighting_with_eye_between_light_surface(self): eye_vecotr = Vector(0, 0, -1) normal_vector = Vector(0, 0, -1) light = Light(Point(0, 0, -10), Color(1, 1, 1)) result = self.m.lighting( self.obj, light, self.position, eye_vecotr, normal_vector) self.assertEqual(result, Color(1.9, 1.9, 1.9)) def test_lighting_with_eye_between_light_surface_eye45(self): eye_vector = Vector(0, 1 / math.sqrt(2), -1 / math.sqrt(2)) normal_vector = Vector(0, 0, -1) light = Light(Point(0, 0, -10), Color(1, 1, 1)) self.assertEqual( self.m.lighting(self.obj, light, self.position, eye_vector, normal_vector), Color(1.0, 1.0, 1.0)) def test_lighting_with_eye_opposite_surface_light45(self): eye_vector = Vector(0, 0, -1) normal_vector = Vector(0, 0, -1) light = Light(Point(0, 10, -10), Color(1, 1, 1)) self.assertEqual( self.m.lighting(self.obj, light, self.position, eye_vector, normal_vector), Color(0.7364, 0.7364, 0.7364)) def test_lighting_eye_reflection(self): eye_vector = Vector(0, -1 / math.sqrt(2), -1 / math.sqrt(2)) normal_vector = Vector(0, 0, -1) light = Light(Point(0, 10, -10), Color(1, 1, 1)) self.assertEqual( self.m.lighting(self.obj, light, self.position, eye_vector, normal_vector), Color(1.6364, 1.6364, 1.6364)) def test_lighting_behind_surface(self): eye_vector = Vector(0, 0, -1) normal_vector = Vector(0, 0, -1) light = Light(Point(0, 0, 10), Color(1, 1, 1)) self.assertEqual( self.m.lighting(self.obj, light, self.position, eye_vector, normal_vector), Color(0.1, 0.1, 0.1)) def test_lighting_with_surface_in_shadow(self): eye_vercotr = Vector(0, 0, -1) normal_vector = Vector(0, 0, -1) light = Light(Point(0, 0, -10), Color(1, 1, 1)) in_shadow = True result = self.m.lighting(self.obj, light, self.position, eye_vercotr, normal_vector, in_shadow) self.assertEqual(result, Color(0.1, 0.1, 0.1)) def test_lighting_with_stripe_aplied(self): self.m.pattern = Stripe(Solid(Color(1, 1, 1)), Solid(Color(0, 0, 0))) self.m.ambient = 1 self.m.diffuse = 0 self.m.specular = 0 eye_vector = Vector(0, 0, -1) normal_vector = Vector(0, 0, -1) light = Light(Point(0, 0, -10), Color(1, 1, 1)) for xyz, color in [((0.9, 0, 0), Color(1, 1, 1)), ((1.1, 0, 0), Color(0, 0, 0))]: self.assertEqual( self.m.lighting(self.obj, light, Point( *xyz), eye_vector, normal_vector), color) def test_reflectivity(self): self.assertEqual(self.m.reflective, 0.0) if __name__ == '__main__': main()
materials.py
from tuples import Color, is_equal class Material: def __init__(self, color=Color(1, 1, 1), ambient=0.1, diffuse=0.9, specular=0.9, shininess=200, pattern=None, reflective=0): self.color = color self.ambient = ambient self.diffuse = diffuse self.specular = specular self.shininess = shininess self.pattern = pattern self.reflective = reflective def __repr__(self): return f'Material({self.color},{self.ambient},{self.diffuse},' +\ f'{self.specular},{self.shininess})' def __eq__(self, other): if self.color != other.color: return False tests = [(self.ambient, other.ambient), (self.diffuse, other.diffuse), (self.specular, other.specular), (self.shininess, other.shininess)] for a, b in tests: if not is_equal(a, b): return False return True def lighting(self, obj, light, point, eye_vector, normal_vector, in_shadow=False) -> Color: if self.pattern is None: color = self.color else: color = self.pattern.at_shape(obj, point) effective_color = color * light.intensity light_vector = (light.position - point).normalize() ambient = effective_color * self.ambient if in_shadow: return ambient light_dot_normal = light_vector.dot(normal_vector) if light_dot_normal < 0: diffuse = Color(0, 0, 0) specular = Color(0, 0, 0) else: diffuse = effective_color * self.diffuse * light_dot_normal reflect_vector = -light_vector.reflect(normal_vector) reflect_dot_eye = reflect_vector.dot(eye_vector) if reflect_dot_eye <= 0: specular = Color(0, 0, 0) else: factor = reflect_dot_eye ** self.shininess specular = light.intensity * self.specular * factor return ambient + diffuse + specular
intersections_test.py
#!/usr/bin/env python3 from unittest import TestCase, main import math from intersections import Intersection, Intersections from tuples import Point, Vector, EPSILON from spheres import Sphere from planes import Plane from rays import Ray from transformations import translation 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))) 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
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): point = ray.position(self.t) eye_vector = -ray.direction normal_vector = self.obj.normal_at(point) return Computations(t=self.t, obj=self.obj, point=point, eye_vector=eye_vector, normal_vector=normal_vector, ray=ray) 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): 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.reflect_vector = ray.direction.reflect(self.normal_vector) def __repr__(self): return f'Computations({self.t},{self.obj},{self.point},' +\ f'{self.eye_vector},{self.normal_vector},{self.inside})'
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 transformations import scaling, translation from rays import Ray from intersections import Intersection 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)) if __name__ == '__main__': main()
world.py
#!/usr/bin/env python3 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, comps, remaining=1): surface = comps.obj.material.lighting( comps.obj, self.light, comps.point, comps.eye_vector, comps.normal_vector, self.is_shadowed(comps.over_point)) reflected = self.reflected_color(comps, remaining) return surface + reflected 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
sample1.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, Stripe, Checkers from camera import Camera from lights import Light from world import World from transformations import translation, scaling, view_transform print('ファイル名, rendering time(秒)') width = 250 height = 125 checkers = Checkers(Solid(Color(0, 1, 0)), Solid(Color(1, 1, 1))) planes = [Plane(material=Material(pattern=checkers, reflective=1)), Plane(material=Material(pattern=checkers))] stripe = Stripe(Solid(Color(1, 0, 0)), Solid(Color(0, 0, 1)), transform=scaling(0.5, 0.5, 0.5)) spheres = [Sphere(material=Material(pattern=stripe), transform=translation(0, 1, 0)), Sphere(material=Material(pattern=stripe, reflective=1), transform=translation(0, 1, 0))] camera = Camera(width, height, math.pi / 2, transform=view_transform(Point(0, 1.5, -5), Point(0, 1, 0), Vector(0, 1, 0))) world = World([], Light(Point(-10, 10, -10), Color(1, 1, 1))) for i, objs in enumerate(zip(planes, spheres), 1): world.objs = objs start = time.time() canvas = camera.render(world) s = time.time() - start with open(f'sample{i}.ppm', 'w') as f: canvas.to_ppm(f) print(f'sample{i}.ppm,{s}')
入出力結果(Bash、cmd(コマンドプロンプト)、Terminal、Jupyter(IPython))
C:\Users\...>py materials_test.py ........ ---------------------------------------------------------------------- Ran 8 tests in 0.003s OK C:\Users\...>py patterns_test.py ................ ---------------------------------------------------------------------- Ran 16 tests in 0.007s OK C:\Users\...>py sample4.py ファイル名, rendering time(秒) sample6.ppm,132.31091618537903 sample7.ppm,143.2693841457367 C:\Users\...>
0 コメント:
コメントを投稿