2019年6月7日金曜日

開発環境

The Ray Tracer Challenge: A Test-Driven Guide to Your First 3D Renderer (Jamis Buck(著)、Pragmatic Bookshelf)、Chapter 14(Groups)のFinding the Normal on a Child Object、Test #9(Find the Normal on an Object in a Group)を取り組んでみる。

コード

shapes_test.py

#!/usr/bin/env python3
import math
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, rotation_y
from groups import Group
from spheres import Sphere


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)

    def test_parent_attribute(self):
        s = Shape()
        self.assertIsNone(s.parent)

    def test_converting_point_from_world_to_object_space(self):
        group1 = Group(transform=rotation_y(math.pi / 2))
        group2 = Group(transform=scaling(2, 2, 2))
        group1.add_child(group2)
        sphere = Sphere(translation(5, 0, 0))
        group2.add_child(sphere)
        point = sphere.world_to_obj(Point(-2, 0, -10))
        self.assertEqual(point, Point(0, 0, -1))

    def test_converting_normal_from_obj_to_world_space(self):
        group1 = Group(transform=rotation_y(math.pi / 2))
        group2 = Group(transform=scaling(1, 2, 3))
        group1.add_child(group2)
        sphere = Sphere(translation(5, 0, 0))
        group2.add_child(sphere)
        normal = sphere.normal_to_world(
            Vector(-1 / math.sqrt(3), 1 / math.sqrt(3), 1 / math.sqrt(3)))
        self.assertEqual(normal, Vector(0.28571, 0.42857, 0.85714))

    def test_finding_normal_on_child_obj(self):
        group1 = Group(transform=rotation_y(math.pi / 2))
        group2 = Group(transform=scaling(1, 2, 3))
        group1.add_child(group2)
        sphere = Sphere(translation(5, 0, 0))
        group2.add_child(sphere)
        normal = sphere.normal_at(Point(1.7321, 1.1547, -5.55774))
        self.assertEqual(normal, Vector(0.29299, 0.43948, -0.84911))


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, parent=None):
        if transform is None:
            self.transform = IDENTITY_MATRIX
        else:
            self.transform = transform
        if material is None:
            self.material = Material()
        else:
            self.material = material
        self.parent = None

    def __repr__(self):
        return f'{self.__class__.__name__}({self.transform},{self.material})'

    def intersect(self, ray):
        raise NotImplementedError()

    def normal_at(self, world_point):
        point = self.world_to_obj(world_point)
        normal = self.local_normal_at(point)
        return self.normal_to_world(normal)

    def world_to_obj(self, point):
        if self.parent is not None:
            point = self.parent.world_to_obj(point)
        return self.transform.inverse() * point

    def normal_to_world(self, normal):
        normal = self.transform.inverse().transpose() * normal
        normal.w = 0
        normal = normal.normalize()
        if self.parent is not None:
            normal = self.parent.normal_to_world(normal)
        return normal

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 local_normal_at(self, 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()
        normal = point - Point(0, 0, 0)
        normal.w = 0
        return normal.normalize()

入出力結果(Bash、cmd.exe(コマンドプロンプト)、Terminal、Jupyter(IPython))

C:\Users\...>py shapes_test.py
.......
----------------------------------------------------------------------
Ran 7 tests in 0.008s

OK

C:\Users\...>py spheres_test.py
.........
----------------------------------------------------------------------
Ran 9 tests in 0.007s

OK

C:\Users\...>

0 コメント:

コメントを投稿

関連コンテンツ