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