開発環境
- 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 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 コメント:
コメントを投稿