## 2019年6月7日金曜日

### Python - Groups - Finding the Normal on a Child Object(Find the Normal on an Object in a Group)

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))
sphere = Sphere(translation(5, 0, 0))
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))
sphere = Sphere(translation(5, 0, 0))
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))
sphere = Sphere(translation(5, 0, 0))
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()
```

```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\...>
```