開発環境
- 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 13(Cylinders)のIntersecting a Ray with a Cylinder、Truncating Cylindersを取り組んでみる。
コード
cylinders_test.py
#!/usr/bin/env python3 from unittest import TestCase, main from cylinders import Cylinder from tuples import is_equal, Point, Vector from rays import Ray class CylinderTest(TestCase): def setUp(self): pass def tearDown(self): pass def test_ray_misses(self): cylinder = Cylinder() origins = [(1, 0, 0), (0, 0, 0), (0, 0, -5)] directions = [(0, 1, 0), (0, 1, 0), (1, 1, 1)] for origin, direction in zip(origins, directions): origin = Point(*origin) direction = Vector(*direction).normalize() ray = Ray(origin, direction) intersections = cylinder.intersect(ray) self.assertEqual(len(intersections), 0) def test_ray_strikes(self): cylinder = Cylinder() origins = [(1, 0, -5), (0, 0, -5), (0.5, 0, -5)] directions = [(0, 0, 1), (0, 0, 1), (0.1, 1, 1)] ts0 = [5, 4, 6.80798] ts1 = [5, 6, 7.08872] for origin, direction, t0, t1 in zip(origins, directions, ts0, ts1): origin = Point(*origin) direction = Vector(*direction).normalize() ray = Ray(origin, direction) intersections = cylinder.intersect(ray) self.assertEqual(len(intersections), 2) self.assertTrue(is_equal(intersections[0].t, t0)) self.assertTrue(is_equal(intersections[1].t, t1)) def test_nomral_vector(self): cylinder = Cylinder() points = [(1, 0, 0), (0, 5, -1), (0, -2, 1), (-1, 1, 0)] normals = [(1, 0, 0), (0, 0, -1), (0, 0, 1), (-1, 0, 0)] for point, normal in zip(points, normals): point = Point(*point) normal = Vector(*normal) n = cylinder.normal_at(point) self.assertEqual(n, normal) def test_intersecting_constrained(self): cylinder = Cylinder(minimum=1, maximum=2) points = [(0, 1.5, 0), (0, 3, -5), (0, 0, -5), (0, 2, -5), (0, 1, -5), (0, 1.5, -2)] directions = [(0.1, 1, 0), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] counts = [0, 0, 0, 0, 0, 2] for direction, point, count in zip(directions, points, counts): direction = Vector(*direction) direction = direction.normalize() point = Point(*point) ray = Ray(point, direction) intersections = cylinder.intersect(ray) self.assertEqual(len(intersections), count) if __name__ == '__main__': main()
cylinders.py
import math from shapes import Shape from intersections import Intersection, Intersections from tuples import is_equal, Vector class Cylinder(Shape): def __init__(self, transform=None, material=None, minimum=-math.inf, maximum=math.inf): super().__init__(transform=transform, material=material) self.minimum = minimum self.maximum = maximum def intersect(self, ray): ray = ray.transform(self.transform.inverse()) a = ray.direction.x ** 2 + ray.direction.z ** 2 if is_equal(a, 0): return Intersections() b = 2 * ray.origin.x * ray.direction.x + \ 2 * ray.origin.z * ray.direction.z c = ray.origin.x ** 2 + ray.origin.z ** 2 - 1 disc = b ** 2 - 4 * a * c if disc < 0: return Intersections() t0 = (-b - math.sqrt(disc)) / (2 * a) t1 = (-b + math.sqrt(disc)) / (2 * a) intersections = [Intersection(t, self) for t in [t0, t1] if self.minimum < ray.origin.y + t * ray.direction.y < self.maximum] return Intersections(*intersections) def normal_at(self, point): point = self.transform.inverse() * point return Vector(point.x, 0, point.z)
sample2.py
#!/usr/bin/env python3 import math import time from tuples import Point, Vector, Color from planes import Plane from cylinders import Cylinder from materials import Material from camera import Camera from lights import Light from world import World from transformations import translation, view_transform from transformations import rotation_x, rotation_y, rotation_z print('ファイル名, rendering time(秒)') width = 250 height = 125 wall1 = Plane(material=Material(color=Color(0, 0, 1)), transform=translation(0, 0, 7) * rotation_y(-math.pi / 4) * rotation_x(math.pi / 2)) wall2 = Plane(material=Material(color=Color(1, 0, 0)), transform=translation(0, 0, 7) * rotation_y(math.pi / 4) * rotation_x(math.pi / 2)) floor = Plane(material=Material(Color(0, 1, 0)), transform=translation(0, -1, 0)) cylinder = Cylinder(minimum=0, maximum=2, material=Material(color=Color(1, 1, 0))) cylinder1 = Cylinder(minimum=0, maximum=2, transform=rotation_x(math.pi / 2), material=Material(color=Color(1, 1, 0))) cylinder2 = Cylinder(minimum=0, maximum=2, transform=rotation_z(math.pi / 2), material=Material(color=Color(1, 1, 0))) cylinders = [cylinder, cylinder1, cylinder2] 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([floor, wall1, wall2], Light(Point(-10, 10, -10), Color(1, 1, 1))) for i, cyl in enumerate(cylinders, 4): world.objs.append(cyl) 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}') world.objs.remove(cyl)
入出力結果(Bash、cmd.exe(コマンドプロンプト)、Terminal、Jupyter(IPython))
C:\Users\...>py cyliners_test.py .... ---------------------------------------------------------------------- Ran 4 tests in 0.010s OK C:\Users\...>py sample2.py ファイル名, rendering time(秒) sample4.ppm,206.15631413459778 sample5.ppm,199.89930176734924 sample6.ppm,200.73605513572693 C:\Users\...>
0 コメント:
コメントを投稿