開発環境
- 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 7(Making a Scnene)のPut It Together(105)を取り組んでみる。
コード
Python 3
intersections_test.py
#!/usr/bin/env python3 from unittest import TestCase, main from tuples import Point, Vector from spheres import Sphere from rays import Ray from intersections import Intersection, Intersections 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) 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]) 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) if __name__ == '__main__': main()
intersections.py
from tuples import Point 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 = -normal_vector else: self.inside = False def __repr__(self): return f'Computations({self.t},{self.obj},{self.point},' +\ f'{self.eye_vector},{self.normal_vector})'
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 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) if __name__ == '__main__': main()
world.py
#!/usr/bin/env python3 from intersections import Intersections from tuples import Color 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) 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)
transformations_test.py
#!//usr/bin/env python3 from unittest import TestCase, main from tuples import Point, Vector from transformations import translation, scaling from transformations import rotation_x, rotation_y, rotation_z from transformations import view_transform from transformations import shearing from matrices import Matrix import math class TransformationsTest(TestCase): def setUp(self): pass def tearDown(self): pass def test_translation(self): transform = translation(5, -3, 2) inv = transform.inverse() p = Point(-3, 4, 5) self.assertEqual(transform * p, Point(2, 1, 7)) def test_translation_vector(self): transform = translation(5, -3, 2) v = Vector(-3, 4, 5) self.assertEqual(transform * v, v) def test_scaling_point(self): transform = scaling(2, 3, 4) p = Point(-4, 6, 8) self.assertEqual(transform * p, Point(-8, 18, 32)) def test_scaling_vector(self): transform = scaling(2, 3, 4) v = Vector(-4, 6, 8) self.assertEqual(transform * v, Vector(-8, 18, 32)) def test_scaling_vector_inv(self): transform = scaling(2, 3, 4) v = inv = transform.inverse() v = Vector(-4, 6, 8) self.assertEqual(inv * v, Vector(-2, 2, 2)) def test_scaling_negative(self): transform = scaling(-1, 1, 1) p = Point(2, 3, 4) self.assertEqual(transform * p, Point(-2, 3, 4)) def test_rotation_x(self): p = Point(0, 1, 0) half_quarter = rotation_x(math.pi / 4) full_quarter = rotation_x(math.pi / 2) self.assertEqual(half_quarter * p, Point(0, math.sqrt(2) / 2, math.sqrt(2) / 2)) self.assertEqual(full_quarter * p, Point(0, 0, 1)) def test_rotation_x_opposite(self): p = Point(0, 1, 0) half_quarter = rotation_x(math.pi / 4) inv = half_quarter.inverse() self.assertEqual(inv * p, Point(0, math.sqrt(2) / 2, -math.sqrt(2) / 2)) def test_rotation_y(self): p = Point(0, 0, 1) half_quarter = rotation_y(math.pi / 4) full_quarter = rotation_y(math.pi / 2) self.assertEqual(half_quarter * p, Point(math.sqrt(2) / 2, 0, math.sqrt(2) / 2)) self.assertEqual(full_quarter * p, Point(1, 0, 0)) def test_rotation_z(self): p = Point(0, 1, 0) half_quarter = rotation_z(math.pi / 4) full_quarter = rotation_z(math.pi / 2) self.assertEqual(half_quarter * p, Point(-math.sqrt(2) / 2, math.sqrt(2) / 2, 0)) self.assertEqual(full_quarter * p, Point(-1, 0, 0)) def test_shearing_x_to_y(self): transform = shearing(1, 0, 0, 0, 0, 0) p = Point(2, 3, 4) self.assertEqual(transform * p, Point(5, 3, 4)) def test_shearing_x_z(self): transform = shearing(0, 1, 0, 0, 0, 0) p = Point(2, 3, 4) self.assertEqual(transform * p, Point(6, 3, 4)) def test_shearing_y_x(self): transform = shearing(0, 0, 1, 0, 0, 0) p = Point(2, 3, 4) self.assertEqual(transform * p, Point(2, 5, 4)) def test_shearing_y_z(self): transform = shearing(0, 0, 0, 1, 0, 0) p = Point(2, 3, 4) self.assertEqual(transform * p, Point(2, 7, 4)) def test_shearing_z_x(self): transform = shearing(0, 0, 0, 0, 1, 0) p = Point(2, 3, 4) self.assertEqual(transform * p, Point(2, 3, 6)) def test_shearing_z_y(self): transform = shearing(0, 0, 0, 0, 0, 1) p = Point(2, 3, 4) self.assertEqual(transform * p, Point(2, 3, 7)) def test_transformation_sequence(self): p = Point(1, 0, 1) A = rotation_x(math.pi / 2) B = scaling(5, 5, 5) C = translation(10, 5, 7) p2 = A * p p3 = B * p2 p4 = C * p3 for a, b in zip([p2, p3, p4], [Point(1, -1, 0), Point(5, -5, 0), Point(15, 0, 7)]): self.assertEqual(a, b) def test_transformation_chain(self): p = Point(1, 0, 1) A = rotation_x(math.pi / 2) B = scaling(5, 5, 5) C = translation(10, 5, 7) T = C * B * A self.assertEqual(T * p, Point(15, 0, 7)) class ViewTransformTest(TestCase): def setUp(self): self.from_ = Point(0, 0, 0) self.up = Vector(0, 1, 0) def tearDown(self): pass def test_default_orientation(self): to = Point(0, 0, -1) self.assertEqual(view_transform(self.from_, to, self.up), Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])) def test_looking_positive_z_direction(self): to = Point(0, 0, 1) t = view_transform(self.from_, to, self.up) self.assertEqual(t, scaling(-1, 1, -1)) def test_movews_world(self): from_ = Point(0, 0, 8) to = Point(0, 0, 0) self.assertEqual(view_transform(from_, to, self.up), translation(0, 0, -8)) def test_arbitary(self): from_ = Point(1, 3, 2) to = Point(4, -2, 8) up = Vector(1, 1, 0) self.assertEqual(view_transform(from_, to, up), Matrix([[-0.50709, 0.50709, 0.67612, -2.36643], [0.76772, 0.60609, 0.12122, -2.82843], [-0.35857, 0.59761, -0.71714, 0], [0, 0, 0, 1]])) if __name__ == '__main__': main()
transformations.py
from matrices import Matrix import math def translation(x: float, y: float, z: float) -> Matrix: return Matrix([[1, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z], [0, 0, 0, 1]]) def scaling(x: float, y: float, z: float) -> Matrix: return Matrix([[x, 0, 0, 0], [0, y, 0, 0], [0, 0, z, 0], [0, 0, 0, 1]]) def rotation_x(r: float) -> Matrix: return Matrix(((1, 0, 0, 0), (0, math.cos(r), -math.sin(r), 0), (0, math.sin(r), math.cos(r), 0), (0, 0, 0, 1))) def rotation_y(r: float) -> Matrix: return Matrix(((math.cos(r), 0, math.sin(r), 0), (0, 1, 0, 0), (-math.sin(r), 0, math.cos(r), 0), (0, 0, 0, 1))) def rotation_z(r: float) -> Matrix: return Matrix(((math.cos(r), -math.sin(r), 0, 0), (math.sin(r), math.cos(r), 0, 0), (0, 0, 1, 0), (0, 0, 0, 1))) def shearing(xy: float, xz: float, yx: float, yz: float, zx: float, zy: float) -> Matrix: return Matrix(((1, xy, xz, 0), (yx, 1, yz, 0), (zx, zy, 1, 0), (0, 0, 0, 1))) def view_transform(from_, to, up): forward = (to - from_).normalize() left = forward.cross(up.normalize()) true_up = left.cross(forward) orientation = Matrix([[left.x, left.y, left.z, 0], [true_up.x, true_up.y, true_up.z, 0], [-forward.x, -forward.y, -forward.z, 0], [0, 0, 0, 1]]) return orientation * translation(-from_.x, -from_.y, -from_.z)
camera_test.py
#!//usr/bin/env python3 from unittest import TestCase, main from camera import Camera from matrices import Matrix from tuples import is_equal, Point, Vector, Color from transformations import rotation_y, translation, scaling from transformations import view_transform from lights import Light from spheres import Sphere from materials import Material from world import World from canvas import Canvas import math class CameraTest(TestCase): def setUp(self): self.c = Camera(201, 101, math.pi / 2) self.light = Light(Point(-10, 10, -10), Color(1, 1, 1)) self.s1 = Sphere() self.s1.material = Material( Color(0.8, 1.0, 0.6), diffuse=0.7, specular=0.2) self.s2 = Sphere() self.s2.transform = scaling(0.5, 0.5, 0.5) self.w = World([self.s1, self.s2], self.light) def tearDown(self): pass def test_camera(self): horizontal_size = 160 vertical_size = 120 field_of_view = math.pi / 2 c = Camera(horizontal_size, vertical_size, field_of_view) for a, b in [(c.horizontal_size, horizontal_size), (c.vertical_size, vertical_size), (c.field_of_view, math.pi / 2), (c.transform, Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]))]: self.assertEqual(a, b) def test_pixel_size_horizontal_canvas(self): c = Camera(200, 125, math.pi / 2) self.assertTrue(is_equal(c.pixel_size, 0.01)) def test_pixel_size_vertcial_canvas(self): c = Camera(125, 200, math.pi / 2) self.assertTrue(is_equal(c.pixel_size, 0.01)) def test_ray_for_pixel_center_canvas(self): r = self.c.ray_for_pixel(100, 50) for a, b in [(r.origin, Point(0, 0, 0)), (r.direction, Vector(0, 0, -1))]: self.assertEqual(a, b) def test_ray_for_pixel_corner_canvas(self): r = self.c.ray_for_pixel(0, 0) for a, b in [(r.origin, Point(0, 0, 0)), (r.direction, Vector(0.66519, 0.33259, -0.66851))]: self.assertEqual(a, b) def test_ray_for_pixel_camera_is_transformed(self): self.c.transform = rotation_y(math.pi / 4) * translation(0, -2, 5) r = self.c.ray_for_pixel(100, 50) for a, b in [(r.origin, Point(0, 2, -5)), (r.direction, Vector(1 / math.sqrt(2), 0, -1 / math.sqrt(2)))]: self.assertEqual(a, b) def test_rendering_world_camera(self): c = Camera(11, 11, math.pi / 2) from_ = Point(0, 0, -5) to = Point(0, 0, 0) up = Vector(0, 1, 0) c.transform = view_transform(from_, to, up) image = c.render(self.w) self.assertEqual(image.pixel_at(5, 5), Color(0.38066, 0.47583, 0.2855)) if __name__ == '__main__': main()
camera.py
from matrices import Matrix from tuples import Point from rays import Ray from canvas import Canvas import math class Camera: def __init__(self, horizontal_size, vertical_size, field_of_view, transform=Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])): self.horizontal_size = horizontal_size self.vertical_size = vertical_size self.field_of_view = field_of_view self.transform = transform half_view = math.tan(field_of_view / 2) aspect = horizontal_size / vertical_size if aspect >= 1: self.half_width = half_view self.half_height = half_view / aspect else: self.half_width = half_view * aspect self.half_height = half_view self.pixel_size = (self.half_width * 2) / horizontal_size def ray_for_pixel(self, x, y): x_offset = (x + 0.5) * self.pixel_size y_offset = (y + 0.5) * self.pixel_size world_x = self.half_width - x_offset world_y = self.half_height - y_offset pixel = self.transform.inverse() * Point(world_x, world_y, -1) origin = self.transform.inverse() * Point(0, 0, 0) direction = (pixel - origin).normalize() return Ray(origin, direction) def render(self, world): image = Canvas(self.horizontal_size, self.vertical_size) for y in range(self.vertical_size): for x in range(self.horizontal_size): ray = self.ray_for_pixel(x, y) color = world.color_at(ray) image.write_pixel(x, y, color) return image
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(100, 50, 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('sample.ppm', 'w') as f: canvas.to_ppm(f) seconds_to_ppm = time.time() - start print(f'sample.ppm, render time: {seconds_renader}, to_ppm time: ' + f'{seconds_to_ppm}') colors = [Color(1, 0, 0), Color(0, 1, 0), Color(0, 0, 1), Color(0.5, 0.5, 0)] for i, color in enumerate(colors, 1): camera = Camera(100, 50, math.pi / (1 + i), transform=view_transform(Point(0, 1.5, -5), Point(0, 1, 0), Vector(0, 1, 0))) other = Sphere(translation(0, 0.6, -0.5) * scaling(0.6, 0.6, 0.6), Material(color, 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'sample{i}.ppm', 'w') as f: canvas.to_ppm(f) seconds_to_ppm = time.time() - start print(f'sample{i}.ppm {color}, ' + f'render time: {seconds_renader}, to_ppm time: {seconds_to_ppm}') world.objs.pop()
入出力結果(cmd(コマンドプロンプト)、Terminal、Bash、Jupyter(IPython))
C:\Users\...>py intersections_test.py ......... ---------------------------------------------------------------------- Ran 9 tests in 0.004s OK C:\Users\...>py world_test.py ........ ---------------------------------------------------------------------- Ran 8 tests in 0.010s OK C:\Users\...>py transformations_test.py ...................... ---------------------------------------------------------------------- Ran 22 tests in 0.004s OK C:\Users\...>py camera_test.py ....... ---------------------------------------------------------------------- Ran 7 tests in 0.282s OK C:\Users\...>py sample.py sample.ppm, render time: 28.784709930419922, to_ppm time: 0.010057926177978516 sample1.ppm Color(1,0,0,0), render time: 31.43201994895935, to_ppm time: 0.01018214225769043 sample2.ppm Color(0,1,0,0), render time: 34.474550008773804, to_ppm time: 0.010061979293823242 sample3.ppm Color(0,0,1,0), render time: 32.88658618927002, to_ppm time: 0.010567903518676758 sample4.ppm Color(0.5,0.5,0,0), render time: 35.47628879547119, to_ppm time: 0.01112222671508789 C:\Users\...>
0 コメント:
コメントを投稿