2019年5月16日木曜日

開発環境

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 コメント:

コメントを投稿