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