開発環境
- 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 8(Shadows)のPut It Together(116)を取り組んでみる。
コード
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 import math class MaterialTest(TestCase): def setUp(self): self.m = Material() self.position = Point(0, 0, 0) 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( 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(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(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(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(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(light, self.position, eye_vercotr, normal_vector, in_shadow) self.assertEqual(result, Color(0.1, 0.1, 0.1)) 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): self.color = color self.ambient = ambient self.diffuse = diffuse self.specular = specular self.shininess = shininess 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, light, point, eye_vector, normal_vector, in_shadow=False) -> Color: effective_color = self.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
world_test.py
#!/usr/bin/env python3 from unittest import TestCase, main from tuples import Point, Vector, Color from lights import Light from spheres import Sphere 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.s1 = Sphere(material=Material(color=Color(0.8, 1.0, 0.6), diffuse=0.7, specular=0.2)) self.s2 = Sphere(transform=scaling(0.5, 0.5, 0.5)) self.w = World(objs=[self.s1, self.s2], 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.w.light, self.light) for s in [self.s1, self.s2]: self.assertIn(s, self.w) def test_intersect_ray(self): r = Ray(Point(0, 0, -5), Vector(0, 0, 1)) xs = self.w.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.w[0] i = Intersection(4, shape) comps = i.prepare_computations(r) c = self.w.shade_hit(comps) self.assertEqual(c, Color(0.38066, 0.47583, 0.2855)) def test_shade_hit_from_inside(self): self.w.light = Light(Point(0, 0.25, 0), Color(1, 1, 1)) r = Ray(Point(0, 0, 0), Vector(0, 0, 1)) shape = self.w[1] i = Intersection(0.5, shape) comps = i.prepare_computations(r) c = self.w.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.w.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.w.color_at(r) self.assertEqual(c, Color(0.38066, 0.47583, 0.2855)) def test_color_intersection_behind_the_ray(self): outer = self.w[0] inner = self.w[1] outer.material.ambient = 1 inner.material.ambient = 1 r = Ray(Point(0, 0, 0.75), Vector(0, 0, -1)) c = self.w.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.w.is_shadowed(p)) def test_shadow_obj_between_point_and_light(self): p = Point(10, -10, 10) self.assertTrue(self.w.is_shadowed(p)) def test_no_shadow_obj_behind_light(self): p = Point(-20, 20, -20) self.assertFalse(self.w.is_shadowed(p)) def test_no_shadow_obj_behind_point(self): p = Point(-2, 2, -2) self.assertFalse(self.w.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)) 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): return comps.obj.material.lighting( self.light, comps.point, comps.eye_vector, comps.normal_vector, self.is_shadowed(comps.over_point)) def color_at(self, r): intersections = self.intersect(r) hit = intersections.hit() if hit is None: return Color(0, 0, 0) comps = hit.prepare_computations(r) return self.shade_hit(comps) 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
intrsections_test.py
#!/usr/bin/env python3 from unittest import TestCase, main from intersections import Intersection, Intersections from tuples import Point, Vector, EPSILON from spheres import Sphere 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) 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 from rays import Ray 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: 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) 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): 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 def __repr__(self): return f'Computations({self.t},{self.obj},{self.point},' +\ f'{self.eye_vector},{self.normal_vector},{self.inside})'
sample.py
#!/usr/bin/env python3 import math from tuples import Point, Vector, Color from spheres import Sphere from transformations import scaling, translation, rotation_x, rotation_y from transformations import view_transform from materials import Material from camera import Camera from world import World from lights import Light import time floor = Sphere() floor.transform = scaling(10, 0.01, 10) floor.maerial = Material(color=Color(1, 0.9, 0.9), specular=0) left_wall = Sphere(translation(0, 0, 5) * rotation_y(-math.pi / 4) * rotation_x(math.pi / 2) * scaling(10, 0.01, 10), floor.material) right_wall = Sphere(translation(0, 0, 5) * rotation_y(math.pi / 4) * rotation_x(math.pi / 2) * scaling(10, 0.01, 10), floor.material) camera = Camera(250, 125, math.pi / 3, transform=view_transform(Point(0, 1.5, -5), Point(0, 1, 0), Vector(0, 1, 0))) middle = Sphere(translation(-0.5, 1, 0.5), Material(Color(0.1, 1, 0.5), diffuse=0.7, specular=0.3)) right = Sphere(translation(1.5, 0.5, -0.5) * scaling(0.5, 0.5, 0.5), Material(Color(0.5, 1, 0.1), diffuse=0.7, specular=0.3)) left = Sphere(translation(-1.5, 0.33, -0.75) * scaling(0.33, 0.33, 0.33), Material(Color(1, 0.8, 0.1), diffuse=0.7, specular=0.3)) world = World([floor, left_wall, right_wall, middle, right, left], Light(Point(-10, 10, -10), Color(1, 1, 1))) start = time.time() canvas = camera.render(world) seconds_renader = time.time() - start start = time.time() with open('sample1.ppm', 'w') as f: canvas.to_ppm(f) seconds_to_ppm = time.time() - start print('ファイル名, rendering time(秒)') print(f'sample1.ppm,{seconds_renader}') other = Sphere(translation(0, 0.7, -1) * scaling(0.7, 0.7, 0.7), Material(Color(1, 0, 0), diffuse=0.7, specular=0.3)) world.objs.append(other) start = time.time() canvas = camera.render(world) seconds_renader = time.time() - start start = time.time() with open(f'sample2.ppm', 'w') as f: canvas.to_ppm(f) seconds_to_ppm = time.time() - start print(f'sample2.ppm,{seconds_renader}')
入出力結果(cmd(コマンドプロンプト)、Terminal、Bash、Jupyter(IPython))
C:\Users\...>py materials_test.py ....... ---------------------------------------------------------------------- Ran 7 tests in 0.001s OK C:\Users\...>py world_test.py ............. ---------------------------------------------------------------------- Ran 13 tests in 0.022s OK C:\Users\...>py intersections_test.py .......... ---------------------------------------------------------------------- Ran 10 tests in 0.005s OK C:\Users\...>py sample.py ファイル名, rendering time(秒) sample1.ppm,304.4209039211273 sample2.ppm,341.31131410598755 C:\Users\...>
0 コメント:
コメントを投稿