開発環境
- 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 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)
def test_shade_hit(self):
r = Ray(Point(0, 0, -5), Vector(0, 0, 1))
shape = self.w[0]
i = Intersection(4, shape)
comps = i.prepare_computations(r)
c = self.w.shade_hit(comps)
self.assertEqual(c, Color(0.38066, 0.47583, 0.2855))
def test_shade_hit_from_inside(self):
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)
c = self.w.shade_hit(comps)
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)
def shade_hit(self, comps):
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)
return self.shade_hit(comps)
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)
seconds_renader = time.time() - start
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)
seconds_renader = time.time() - start
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()
入出力結果(cmd(コマンドプロンプト)、Terminal、Bash、Jupyter(IPython))
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\...>





0 コメント:
コメントを投稿