開発環境
- 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 9(Planes)のPut It Together(124)を取り組んでみる。
コード
Python 3
shapes_test.py
#!/usr/bin/env python3 from unittest import TestCase, main from shapes import Shape from transformations import translation from matrices import IDENTITY_MATRIX, Matrix from materials import Material from rays import Ray from tuples import Point, Vector from transformations import scaling class ShapeTest(TestCase): def setUp(self): self.shape = Shape(material=Material()) def tearDown(self): pass def test_default_transformation(self): self.assertEqual(self.shape.transform, IDENTITY_MATRIX) def test_material(self): self.assertEqual(self.shape.material.ambient, 0.1) self.assertEqual(self.shape.material, Material()) def test_transform(self): self.assertEqual(Shape().transform, IDENTITY_MATRIX) s = Shape() t = translation(2, 3, 4) s.transform = t self.assertEqual(s.transform, t) if __name__ == '__main__': main()
shapes.py
from matrices import Matrix, IDENTITY_MATRIX from materials import Material class Shape: def __init__(self, transform=None, material=None): if transform is None: self.transform = IDENTITY_MATRIX else: self.transform = transform if material is None: self.material = Material() else: self.material = material def __repr__(self): return f'{self.__class__.__name__}({self.transform},{self.material})' def intersect(self, ray): raise NotImplementedError() def normal_at(self, point): raise NotImplementedError()
spheres_test.py
#!/usr/bin/env python3 from unittest import TestCase, main from tuples import Point, Vector from matrices import Matrix from rays import Ray from spheres import Sphere from transformations import translation, scaling from materials import Material import math class SpheresTest(TestCase): def setUp(self): pass def tearDown(self): pass def test_sphere(self): r = Ray(Point(0, 0, -5), Vector(0, 0, 1)) s = Sphere() xs = s.intersect(r) for a, b in [(len(xs), 2), (xs[0].obj, s), (xs[1].obj, s)]: self.assertEqual(a, b) def test_intersect(self): r = Ray(Point(0, 0, -5), Vector(0, 0, 1)) s = Sphere() xs = s.intersect(r) self.assertEqual(len(xs), 2) for i, (a, b) in enumerate(zip(xs, [4, 6])): self.assertEqual(a.t, b) def test_intersect_target(self): r = Ray(Point(0, 1, -5), Vector(0, 0, 1)) s = Sphere() xs = s.intersect(r) self.assertEqual(len(xs), 2) for x in xs: self.assertEqual(x.t, 5) def test_intersect_misses(self): r = Ray(Point(0, 2, -5), Vector(0, 0, 1)) s = Sphere() xs = s.intersect(r) self.assertEqual(len(xs), 0) def test_intersect_inside(self): r = Ray(Point(0, 0, 0), Vector(0, 0, 1)) s = Sphere() xs = s.intersect(r) self.assertEqual(len(xs), 2) for a, b in zip(xs, [-1, 1]): self.assertEqual(a.t, b) def test_intersect_behind(self): r = Ray(Point(0, 0, 5), Vector(0, 0, 1)) s = Sphere() xs = s.intersect(r) for a, b in zip(xs, [-6.0, -4.0]): self.assertEqual(a.t, b) def test_intersect_scaled_with_ray(self): r = Ray(Point(0, 0, -5), Vector(0, 0, 1)) s = Sphere() s.transform = scaling(2, 2, 2) xs = s.intersect(r) for a, b in [(len(xs), 2), (xs[0].t, 3), (xs[1].t, 7)]: self.assertEqual(a, b) def test_normal_at(self): s = Sphere() tests = [((1, 0, 0), (1, 0, 0)), ((0, 1, 0), (0, 1, 0)), ((0, 0, 1), (0, 0, 1)), ((1 / math.sqrt(3), 1 / math.sqrt(3), 1 / math.sqrt(3)), (1 / math.sqrt(3), 1 / math.sqrt(3), 1 / math.sqrt(3)))] for a, b in tests: self.assertEqual(s.normal_at(Point(*a)), Vector(*b)) def test_normal_at(self): s = Sphere() n = s.normal_at(Point(1 / math.sqrt(3), 1 / math.sqrt(3), 1 / math.sqrt(3))) self.assertEqual(n, n.normalize()) def test_normal_at_translated(self): s = Sphere() s.transform = translation(0, 1, 0) n = s.normal_at(Point(0, 1.70711, -0.70711)) self.assertEqual(n, Vector(0, 0.70711, -0.70711)) if __name__ == '__main__': main()
spheres.py
from shapes import Shape from tuples import Point from rays import Ray from intersections import Intersection, Intersections from matrices import Matrix from materials import Material import math identity_matrix = Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) class Sphere(Shape): def intersect(self, ray: Ray) -> Intersections: ray = ray.transform(self.transform.inverse()) sphere_to_ray = ray.origin - Point(0, 0, 0) a = ray.direction.dot(ray.direction) b = 2 * ray.direction.dot(sphere_to_ray) c = sphere_to_ray.dot(sphere_to_ray) - 1 discriminant = b ** 2 - 4 * a * c if discriminant < 0: return Intersections() return Intersections( *[Intersection((-b + c * math.sqrt(discriminant)) / (2 * a), self) for c in [-1, 1]]) def normal_at(self, world_point: Point) -> Point: object_point = self.transform.inverse() * world_point object_normal = object_point - Point(0, 0, 0) world_normal = self.transform.inverse().transpose() * object_normal world_normal.w = 0 return world_normal.normalize()
planes_test.py
#!/usr/bin/env python3 from unittest import TestCase, main from planes import Plane from tuples import Point, Vector from rays import Ray class PlaneTest(TestCase): def setUp(self): pass def tearDown(self): pass def test_normal_at_constant_everywhere(self): plane = Plane() n1 = plane.normal_at(Point(0, 0, 0)) n2 = plane.normal_at(Point(10, 0, -10)) n3 = plane.normal_at(Point(-5, 0, 150)) for n in [n1, n2, n3]: self.assertEqual(n, Vector(0, 1, 0)) def test_intersect_ray_parallel_to_plane(self): plane = Plane() ray = Ray(Point(0, 10, 0), Vector(0, 0, 1)) xs = plane.intersect(ray) self.assertEqual(len(xs), 0) def test_intersect_from_above(self): plane = Plane() ray = Ray(Point(0, 1, 0), Vector(0, -1, 0)) xs = plane.intersect(ray) for a, b in [(len(xs), 1), (xs[0].t, 1), (xs[0].obj, plane)]: self.assertEqual(a, b) def test_intersect_from_below(self): plane = Plane() ray = Ray(Point(0, -1, 0), Vector(0, 1, 0)) xs = plane.intersect(ray) for a, b in [(len(xs), 1), (xs[0].t, 1), (xs[0].obj, plane)]: self.assertEqual(a, b) if __name__ == '__main__': main()
planes.py
from shapes import Shape from tuples import EPSILON, Vector from intersections import Intersection, Intersections class Plane(Shape): def normal_at(self, point): return self.transform.inverse() * Vector(0, 1, 0) def intersect(self, ray): r = ray.transform(self.transform.inverse()) if abs(r.direction.y) < EPSILON: return Intersections() t = -r.origin.y / r.direction.y i = Intersection(t, self) return Intersections(i)
sample.py
#!/usr/bin/env python3 import math import time from planes import Plane from spheres import Sphere from materials import Material from tuples import Point, Vector, Color from camera import Camera from transformations import view_transform, translation, scaling from transformations import rotation_x, rotation_y from world import World from lights import Light floor = Plane(material=Material(Color(1, 0.9, 0.9), specular=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)) other = Sphere(translation(0, 0.7, -1) * scaling(0.6, 0.6, 0.6), Material(Color(1, 0, 0), diffuse=0.7, specular=0.3)) backdrop = Plane(translation(0, 0, 5) * rotation_x(math.pi / 2), Material(Color(0, 0, 1), specular=0.3)) camera = Camera(250, 125, math.pi / 3, transform=view_transform(Point(0, 1.5, -5), Point(0, 1, 0), Vector(0, 1, 0))) world = World([floor, backdrop, left, middle, right, other], Light(Point(-10, 10, -10), Color(1, 1, 1))) print('ファイル名, rendering time(秒)') start = time.time() canvas = camera.render(world) seconds_renader = time.time() - start start = time.time() filename = 'sample1.ppm' with open(filename, 'w') as f: canvas.to_ppm(f) seconds_to_ppm = time.time() - start print(f'{filename},{seconds_renader}') # hexagonal-shaped room, ceiling, embedded sphere angle = 2 * math.pi / 6 colors = [Color(1, 0, 0), Color(0, 1, 0), Color(0, 0, 1), Color(0.5, 0.5, 0), Color(0.5, 0, 0.5), Color(0, 0.5, 0.5)] planes = [Plane(translation(5 * math.sin(angle * i), 0, 5 * math.cos(angle * i)) * rotation_y(i * angle) * rotation_x(math.pi / 2), Material(color)) for i, color in enumerate(colors)] planes.append(Plane(translation(0, 5, 0), Material(Color(1, 0.9, 0.9), specular=0))) sphere = Sphere(translation(0, 5, 0) * scaling(2, 2, 2), Material(Color(1, 0.8, 0.1), diffuse=0.7, specular=0.3)) camera = Camera(250, 125, math.pi / 3, view_transform(Point(-1, -5, -1), Point(0, 0, 0), Vector(0, 1, 0))) world = World(planes + [sphere], Light(Point(0, 0, 0), Color(1, 1, 1))) start = time.time() canvas = camera.render(world) seconds_renader = time.time() - start start = time.time() filename = 'sample2.ppm' with open(filename, 'w') as f: canvas.to_ppm(f) seconds_to_ppm = time.time() - start print(f'{filename},{seconds_renader}')
入出力結果(cmd(コマンドプロンプト)、Terminal、Bash、Jupyter(IPython))
C:\Users\...>py shapes_test.py ... ---------------------------------------------------------------------- Ran 3 tests in 0.000s OK C:\Users\...>py spheres_test.py ......... ---------------------------------------------------------------------- Ran 9 tests in 0.007s OK C:\Users\...>py planes_test.py .... ---------------------------------------------------------------------- Ran 4 tests in 0.004s OK C:\Users\...>py sample.py ファイル名, rendering time(秒) sample1.ppm,282.1003210544586 sample2.ppm,354.80750012397766 C:\Users\...>
0 コメント:
コメントを投稿