## 2019年5月13日月曜日

### Python - Making a Scnene - World, View Transformation, Camera

The Ray Tracer Challenge: A Test-Driven Guide to Your First 3D Renderer (Jamis Buck(著)、Pragmatic Bookshelf)、Chapter 7(Making a Scnene)のPut It Together(105)を取り組んでみる。

コード

Python 3

intersections_test.py

```#!/usr/bin/env python3
from unittest import TestCase, main
from tuples import Point, Vector
from spheres import Sphere
from rays import Ray
from intersections import Intersection, Intersections

class IntersectionTest(TestCase):
def setUp(self):
pass

def tearDown(self):
pass

def test_intersection(self):
s = Sphere()
i = Intersection(3.5, s)
self.assertEqual(i.t, 3.5)
self.assertEqual(i.obj, s)

class IntersectionsTest(TestCase):
def setUp(self):
pass

def tearDown(self):
pass

def test_intersection(self):
s = Sphere()
i1 = Intersection(1, s)
i2 = Intersection(2, s)
xs = Intersections(i1, i2)
for a, b in [(len(xs), 2), (xs[0].t, 1), (xs[1].t, 2)]:
self.assertEqual(a, b)

def test_hit(self):
s = Sphere()
i1 = Intersection(1, s)
i2 = Intersection(2, s)
xs = Intersections(i2, i1)
self.assertEqual(xs.hit(), i1)

def test_hit_positive_and_negative(self):
s = Sphere()
i1 = Intersection(-1, s)
i2 = Intersection(1, s)
xs = Intersections(i2, i1)
self.assertEqual(xs.hit(), i2)

def test_hit_none(self):
s = Sphere()
i1 = Intersection(-2, s)
i2 = Intersection(-1, s)
xs = Intersections(i2, i1)
self.assertIsNone(xs.hit())

def test_hit_nonnegative(self):
s = Sphere()
intersections = [Intersection(t, s) for t in [5, 7, -3, 2]]
xs = Intersections(*intersections)
self.assertEqual(xs.hit(), intersections[-1])

def test_prepare_computations(self):
r = Ray(Point(0, 0, -5), Vector(0, 0, 1))
shape = Sphere()
i = Intersection(4, shape)
comps = i.prepare_computations(r)
for a, b in [(comps.t, i.t),
(comps.obj, i.obj),
(comps.point, Point(0, 0, -1)),
(comps.eye_vector, Vector(0, 0, -1)),
(comps.normal_vector, Vector(0, 0, -1))]:
self.assertEqual(a, b)

def test_hit_intersection_outside(self):
r = Ray(Point(0, 0, -5), Vector(0, 0, 1))
shape = Sphere()
i = Intersection(4, shape)
comps = i.prepare_computations(r)
self.assertFalse(comps.inside)

def test_hit_intersection_inside(self):
r = Ray(Point(0, 0, 0), Vector(0, 0, 1))
shape = Sphere()
i = Intersection(1, shape)
comps = i.prepare_computations(r)
for a, b in [(comps.point, Point(0, 0, 1)),
(comps.eye_vector, Vector(0, 0, -1)),
(comps.normal_vector, Vector(0, 0, -1))]:
self.assertEqual(a, b)
self.assertTrue(comps.inside)

if __name__ == '__main__':
main()
```

intersections.py

```from tuples import Point
from rays import Ray

class Intersection:
def __init__(self, t: float, obj):
self.t = t
self.obj = obj

def __repr__(self):
return f'Intersection({self.t},{self.obj})'

def prepare_computations(self, ray: Ray):
point = ray.position(self.t)
eye_vector = -ray.direction
normal_vector = self.obj.normal_at(point)
return Computations(t=self.t,
obj=self.obj,
point=point,
eye_vector=eye_vector,
normal_vector=normal_vector)

class Intersections:
def __init__(self, *args):
self.xs = list(args)
self.xs.sort(key=lambda o: o.t)

def __getitem__(self, i: int):
return self.xs[i]

def __len__(self):
return len(self.xs)

def __repr__(self):
return f'Inersections({self.xs})'

def hit(self):
for i in self.xs:
if i.t > 0:
return i
return None

class Computations:
def __init__(self, t, obj, point, eye_vector, normal_vector):
self.t = t
self.obj = obj
self.point = point
self.eye_vector = eye_vector
self.normal_vector = normal_vector
if normal_vector.dot(eye_vector) < 0:
self.inside = True
self.normal_vector = -normal_vector
else:
self.inside = False

def __repr__(self):
return f'Computations({self.t},{self.obj},{self.point},' +\
f'{self.eye_vector},{self.normal_vector})'
```

world_test.py

```#!/usr/bin/env python3
from unittest import TestCase, main
from tuples import Point, Vector, Color
from lights import Light
from spheres import Sphere
from materials import Material
from transformations import scaling
from rays import Ray
from intersections import Intersection
from world import World

class WorldTest(TestCase):
def setUp(self):
self.light = Light(Point(-10, 10, -10), Color(1, 1, 1))
self.s1 = Sphere(material=Material(color=Color(0.8, 1.0, 0.6),
diffuse=0.7,
specular=0.2))
self.s2 = Sphere(transform=scaling(0.5, 0.5, 0.5))
self.w = World(objs=[self.s1, self.s2], light=self.light)

def tearDown(self):
pass

def test_world(self):
w = World()
self.assertEqual(len(w), 0)
self.assertIsNone(w.light)

def test_default_world(self):
self.assertEqual(self.w.light, self.light)
for s in [self.s1, self.s2]:
self.assertIn(s, self.w)

def test_intersect_ray(self):
r = Ray(Point(0, 0, -5), Vector(0, 0, 1))
xs = self.w.intersect(r)
self.assertEqual(len(xs), 4)
for i, t in enumerate([4, 4.5, 5.5, 6]):
self.assertEqual(xs[i].t, t)

r = Ray(Point(0, 0, -5), Vector(0, 0, 1))
shape = self.w[0]
i = Intersection(4, shape)
comps = i.prepare_computations(r)
self.assertEqual(c, Color(0.38066, 0.47583, 0.2855))

self.w.light = Light(Point(0, 0.25, 0), Color(1, 1, 1))
r = Ray(Point(0, 0, 0), Vector(0, 0, 1))
shape = self.w[1]
i = Intersection(0.5, shape)
comps = i.prepare_computations(r)
self.assertEqual(c, Color(0.90498, 0.90498, 0.90498))

def test_color_ray_misses(self):
r = Ray(Point(0, 0, -5), Vector(0, 1, 0))
c = self.w.color_at(r)
self.assertEqual(c, Color(0, 0, 0))

def test_color_ray_hits(self):
r = Ray(Point(0, 0, -5), Vector(0, 0, 1))
c = self.w.color_at(r)
self.assertEqual(c, Color(0.38066, 0.47583, 0.2855))

def test_color_intersection_behind_the_ray(self):
outer = self.w[0]
inner = self.w[1]
outer.material.ambient = 1
inner.material.ambient = 1
r = Ray(Point(0, 0, 0.75), Vector(0, 0, -1))
c = self.w.color_at(r)
# self.assertNotEqual(c, outer.material.color)
# self.assertEqual(c, inner.material.color)

if __name__ == '__main__':
main()
```

world.py

```#!/usr/bin/env python3
from intersections import Intersections
from tuples import Color

class World:
def __init__(self, objs=None, light=None):
if objs is None:
self.objs = []
else:
self.objs = objs
self.light = light

def __getitem__(self, y):
return self.objs[y]

def __cointains__(self, key):
return key in self.objs

def __len__(self):
return len(self.objs)

def __repr__(self):
return f'World({self.objs}, {self.light})'

def intersect(self, ray):
intersections = []
for obj in self.objs:
intersections += obj.intersect(ray)
return Intersections(*intersections)

return comps.obj.material.lighting(
self.light, comps.point, comps.eye_vector, comps.normal_vector)

def color_at(self, r):
intersections = self.intersect(r)
hit = intersections.hit()
if hit is None:
return Color(0, 0, 0)
comps = hit.prepare_computations(r)
```

transformations_test.py

```#!//usr/bin/env python3
from unittest import TestCase, main
from tuples import Point, Vector
from transformations import translation, scaling
from transformations import rotation_x, rotation_y, rotation_z
from transformations import view_transform
from transformations import shearing
from matrices import Matrix
import math

class TransformationsTest(TestCase):
def setUp(self):
pass

def tearDown(self):
pass

def test_translation(self):
transform = translation(5, -3, 2)
inv = transform.inverse()
p = Point(-3, 4, 5)
self.assertEqual(transform * p, Point(2, 1, 7))

def test_translation_vector(self):
transform = translation(5, -3, 2)
v = Vector(-3, 4, 5)
self.assertEqual(transform * v, v)

def test_scaling_point(self):
transform = scaling(2, 3, 4)
p = Point(-4, 6, 8)
self.assertEqual(transform * p, Point(-8, 18, 32))

def test_scaling_vector(self):
transform = scaling(2, 3, 4)
v = Vector(-4, 6, 8)
self.assertEqual(transform * v, Vector(-8, 18, 32))

def test_scaling_vector_inv(self):
transform = scaling(2, 3, 4)
v = inv = transform.inverse()
v = Vector(-4, 6, 8)
self.assertEqual(inv * v, Vector(-2, 2, 2))

def test_scaling_negative(self):
transform = scaling(-1, 1, 1)
p = Point(2, 3, 4)
self.assertEqual(transform * p, Point(-2, 3, 4))

def test_rotation_x(self):
p = Point(0, 1, 0)
half_quarter = rotation_x(math.pi / 4)
full_quarter = rotation_x(math.pi / 2)
self.assertEqual(half_quarter * p,
Point(0, math.sqrt(2) / 2, math.sqrt(2) / 2))
self.assertEqual(full_quarter * p,
Point(0, 0, 1))

def test_rotation_x_opposite(self):
p = Point(0, 1, 0)
half_quarter = rotation_x(math.pi / 4)
inv = half_quarter.inverse()
self.assertEqual(inv * p,
Point(0, math.sqrt(2) / 2, -math.sqrt(2) / 2))

def test_rotation_y(self):
p = Point(0, 0, 1)
half_quarter = rotation_y(math.pi / 4)
full_quarter = rotation_y(math.pi / 2)
self.assertEqual(half_quarter * p,
Point(math.sqrt(2) / 2, 0, math.sqrt(2) / 2))
self.assertEqual(full_quarter * p,
Point(1, 0, 0))

def test_rotation_z(self):
p = Point(0, 1, 0)
half_quarter = rotation_z(math.pi / 4)
full_quarter = rotation_z(math.pi / 2)
self.assertEqual(half_quarter * p,
Point(-math.sqrt(2) / 2, math.sqrt(2) / 2, 0))
self.assertEqual(full_quarter * p,
Point(-1, 0, 0))

def test_shearing_x_to_y(self):
transform = shearing(1, 0, 0, 0, 0, 0)
p = Point(2, 3, 4)
self.assertEqual(transform * p, Point(5, 3, 4))

def test_shearing_x_z(self):
transform = shearing(0, 1, 0, 0, 0, 0)
p = Point(2, 3, 4)
self.assertEqual(transform * p, Point(6, 3, 4))

def test_shearing_y_x(self):
transform = shearing(0, 0, 1, 0, 0, 0)
p = Point(2, 3, 4)
self.assertEqual(transform * p, Point(2, 5, 4))

def test_shearing_y_z(self):
transform = shearing(0, 0, 0, 1, 0, 0)
p = Point(2, 3, 4)
self.assertEqual(transform * p, Point(2, 7, 4))

def test_shearing_z_x(self):
transform = shearing(0, 0, 0, 0, 1, 0)
p = Point(2, 3, 4)
self.assertEqual(transform * p, Point(2, 3, 6))

def test_shearing_z_y(self):
transform = shearing(0, 0, 0, 0, 0, 1)
p = Point(2, 3, 4)
self.assertEqual(transform * p, Point(2, 3, 7))

def test_transformation_sequence(self):
p = Point(1, 0, 1)
A = rotation_x(math.pi / 2)
B = scaling(5, 5, 5)
C = translation(10, 5, 7)
p2 = A * p
p3 = B * p2
p4 = C * p3
for a, b in zip([p2, p3, p4], [Point(1, -1, 0),
Point(5, -5, 0),
Point(15, 0, 7)]):
self.assertEqual(a, b)

def test_transformation_chain(self):
p = Point(1, 0, 1)
A = rotation_x(math.pi / 2)
B = scaling(5, 5, 5)
C = translation(10, 5, 7)
T = C * B * A
self.assertEqual(T * p, Point(15, 0, 7))

class ViewTransformTest(TestCase):
def setUp(self):
self.from_ = Point(0, 0, 0)
self.up = Vector(0, 1, 0)

def tearDown(self):
pass

def test_default_orientation(self):
to = Point(0, 0, -1)
self.assertEqual(view_transform(self.from_, to, self.up),
Matrix([[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]]))

def test_looking_positive_z_direction(self):
to = Point(0, 0, 1)
t = view_transform(self.from_, to, self.up)
self.assertEqual(t, scaling(-1, 1, -1))

def test_movews_world(self):
from_ = Point(0, 0, 8)
to = Point(0, 0, 0)
self.assertEqual(view_transform(from_, to, self.up),
translation(0, 0, -8))

def test_arbitary(self):
from_ = Point(1, 3, 2)
to = Point(4, -2, 8)
up = Vector(1, 1, 0)
self.assertEqual(view_transform(from_, to, up),
Matrix([[-0.50709, 0.50709, 0.67612, -2.36643],
[0.76772, 0.60609, 0.12122, -2.82843],
[-0.35857, 0.59761, -0.71714, 0],
[0, 0, 0, 1]]))

if __name__ == '__main__':
main()
```

transformations.py

```from matrices import Matrix
import math

def translation(x: float, y: float, z: float) -> Matrix:
return Matrix([[1, 0, 0, x],
[0, 1, 0, y],
[0, 0, 1, z],
[0, 0, 0, 1]])

def scaling(x: float, y: float, z: float) -> Matrix:
return Matrix([[x, 0, 0, 0],
[0, y, 0, 0],
[0, 0, z, 0],
[0, 0, 0, 1]])

def rotation_x(r: float) -> Matrix:
return Matrix(((1, 0, 0, 0),
(0, math.cos(r), -math.sin(r), 0),
(0, math.sin(r), math.cos(r), 0),
(0, 0, 0, 1)))

def rotation_y(r: float) -> Matrix:
return Matrix(((math.cos(r), 0, math.sin(r), 0),
(0, 1, 0, 0),
(-math.sin(r), 0, math.cos(r), 0),
(0, 0, 0, 1)))

def rotation_z(r: float) -> Matrix:
return Matrix(((math.cos(r), -math.sin(r), 0, 0),
(math.sin(r), math.cos(r), 0, 0),
(0, 0, 1, 0),
(0, 0, 0, 1)))

def shearing(xy: float, xz: float,
yx: float, yz: float,
zx: float, zy: float) -> Matrix:
return Matrix(((1, xy, xz, 0),
(yx, 1, yz, 0),
(zx, zy, 1, 0),
(0, 0, 0, 1)))

def view_transform(from_, to, up):
forward = (to - from_).normalize()
left = forward.cross(up.normalize())
true_up = left.cross(forward)
orientation = Matrix([[left.x, left.y, left.z, 0],
[true_up.x, true_up.y, true_up.z, 0],
[-forward.x, -forward.y, -forward.z, 0],
[0, 0, 0, 1]])
return orientation * translation(-from_.x, -from_.y, -from_.z)
```

camera_test.py

```#!//usr/bin/env python3
from unittest import TestCase, main
from camera import Camera
from matrices import Matrix
from tuples import is_equal, Point, Vector, Color
from transformations import rotation_y, translation, scaling
from transformations import view_transform
from lights import Light
from spheres import Sphere
from materials import Material
from world import World
from canvas import Canvas
import math

class CameraTest(TestCase):
def setUp(self):
self.c = Camera(201, 101, math.pi / 2)
self.light = Light(Point(-10, 10, -10), Color(1, 1, 1))
self.s1 = Sphere()
self.s1.material = Material(
Color(0.8, 1.0, 0.6), diffuse=0.7, specular=0.2)
self.s2 = Sphere()
self.s2.transform = scaling(0.5, 0.5, 0.5)
self.w = World([self.s1, self.s2], self.light)

def tearDown(self):
pass

def test_camera(self):
horizontal_size = 160
vertical_size = 120
field_of_view = math.pi / 2
c = Camera(horizontal_size, vertical_size, field_of_view)
for a, b in [(c.horizontal_size, horizontal_size),
(c.vertical_size, vertical_size),
(c.field_of_view, math.pi / 2),
(c.transform, Matrix([[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]]))]:
self.assertEqual(a, b)

def test_pixel_size_horizontal_canvas(self):
c = Camera(200, 125, math.pi / 2)
self.assertTrue(is_equal(c.pixel_size, 0.01))

def test_pixel_size_vertcial_canvas(self):
c = Camera(125, 200, math.pi / 2)
self.assertTrue(is_equal(c.pixel_size, 0.01))

def test_ray_for_pixel_center_canvas(self):
r = self.c.ray_for_pixel(100, 50)
for a, b in [(r.origin, Point(0, 0, 0)),
(r.direction, Vector(0, 0, -1))]:
self.assertEqual(a, b)

def test_ray_for_pixel_corner_canvas(self):
r = self.c.ray_for_pixel(0, 0)
for a, b in [(r.origin, Point(0, 0, 0)),
(r.direction, Vector(0.66519, 0.33259, -0.66851))]:
self.assertEqual(a, b)

def test_ray_for_pixel_camera_is_transformed(self):
self.c.transform = rotation_y(math.pi / 4) * translation(0, -2, 5)
r = self.c.ray_for_pixel(100, 50)
for a, b in [(r.origin, Point(0, 2, -5)),
(r.direction,
Vector(1 / math.sqrt(2), 0, -1 / math.sqrt(2)))]:
self.assertEqual(a, b)

def test_rendering_world_camera(self):
c = Camera(11, 11, math.pi / 2)
from_ = Point(0, 0, -5)
to = Point(0, 0, 0)
up = Vector(0, 1, 0)
c.transform = view_transform(from_, to, up)
image = c.render(self.w)
self.assertEqual(image.pixel_at(5, 5),
Color(0.38066, 0.47583, 0.2855))

if __name__ == '__main__':
main()
```

camera.py

```from matrices import Matrix
from tuples import Point
from rays import Ray
from canvas import Canvas
import math

class Camera:
def __init__(self, horizontal_size, vertical_size, field_of_view,
transform=Matrix([[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]])):
self.horizontal_size = horizontal_size
self.vertical_size = vertical_size
self.field_of_view = field_of_view
self.transform = transform
half_view = math.tan(field_of_view / 2)
aspect = horizontal_size / vertical_size
if aspect >= 1:
self.half_width = half_view
self.half_height = half_view / aspect
else:
self.half_width = half_view * aspect
self.half_height = half_view
self.pixel_size = (self.half_width * 2) / horizontal_size

def ray_for_pixel(self, x, y):
x_offset = (x + 0.5) * self.pixel_size
y_offset = (y + 0.5) * self.pixel_size

world_x = self.half_width - x_offset
world_y = self.half_height - y_offset
pixel = self.transform.inverse() * Point(world_x, world_y, -1)
origin = self.transform.inverse() * Point(0, 0, 0)
direction = (pixel - origin).normalize()
return Ray(origin, direction)

def render(self, world):
image = Canvas(self.horizontal_size, self.vertical_size)
for y in range(self.vertical_size):
for x in range(self.horizontal_size):
ray = self.ray_for_pixel(x, y)
color = world.color_at(ray)
image.write_pixel(x, y, color)
return image
```

sample.py

```#!/usr/bin/env python3
import math
from tuples import Point, Vector, Color
from spheres import Sphere
from transformations import scaling, translation, rotation_x, rotation_y
from transformations import view_transform
from materials import Material
from camera import Camera
from world import World
from lights import Light
import time

floor = Sphere()
floor.transform = scaling(10, 0.01, 10)
floor.maerial = Material(color=Color(1, 0.9, 0.9), specular=0)

left_wall = Sphere(translation(0, 0, 5) *
rotation_y(-math.pi / 4) *
rotation_x(math.pi / 2) *
scaling(10, 0.01, 10),
floor.material)
right_wall = Sphere(translation(0, 0, 5) *
rotation_y(math.pi / 4) *
rotation_x(math.pi / 2) *
scaling(10, 0.01, 10),
floor.material)
camera = Camera(100, 50, math.pi / 3,
transform=view_transform(Point(0, 1.5, -5),
Point(0, 1, 0),
Vector(0, 1, 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))

world = World([floor, left_wall, right_wall, middle, right, left],
Light(Point(-10, 10, -10), Color(1, 1, 1)))

start = time.time()
canvas = camera.render(world)
start = time.time()
with open('sample.ppm', 'w') as f:
canvas.to_ppm(f)
seconds_to_ppm = time.time() - start
print(f'sample.ppm, render time: {seconds_renader}, to_ppm time: ' +
f'{seconds_to_ppm}')

colors = [Color(1, 0, 0), Color(0, 1, 0), Color(0, 0, 1), Color(0.5, 0.5, 0)]
for i, color in enumerate(colors, 1):
camera = Camera(100, 50, math.pi / (1 + i),
transform=view_transform(Point(0, 1.5, -5),
Point(0, 1, 0),
Vector(0, 1, 0)))
other = Sphere(translation(0, 0.6, -0.5) * scaling(0.6, 0.6, 0.6),
Material(color, diffuse=0.7, specular=0.3))
world.objs.append(other)
start = time.time()
canvas = camera.render(world)
start = time.time()
with open(f'sample{i}.ppm', 'w') as f:
canvas.to_ppm(f)
seconds_to_ppm = time.time() - start
print(f'sample{i}.ppm {color}, ' +
f'render time: {seconds_renader}, to_ppm time: {seconds_to_ppm}')
world.objs.pop()
```

```C:\Users\...>py intersections_test.py
.........
----------------------------------------------------------------------
Ran 9 tests in 0.004s

OK

C:\Users\...>py world_test.py
........
----------------------------------------------------------------------
Ran 8 tests in 0.010s

OK

C:\Users\...>py transformations_test.py
......................
----------------------------------------------------------------------
Ran 22 tests in 0.004s

OK

C:\Users\...>py camera_test.py
.......
----------------------------------------------------------------------
Ran 7 tests in 0.282s

OK

C:\Users\...>py sample.py
sample.ppm, render time: 28.784709930419922, to_ppm time: 0.010057926177978516
sample1.ppm Color(1,0,0,0), render time: 31.43201994895935, to_ppm time: 0.01018214225769043
sample2.ppm Color(0,1,0,0), render time: 34.474550008773804, to_ppm time: 0.010061979293823242
sample3.ppm Color(0,0,1,0), render time: 32.88658618927002, to_ppm time: 0.010567903518676758
sample4.ppm Color(0.5,0.5,0,0), render time: 35.47628879547119, to_ppm time: 0.01112222671508789

C:\Users\...>
```