開発環境
- 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 11(Reflection and Refraction)のReflectionを取り組んでみる。
コード
Python 3
materials_test.py
#!/usr/bin/env python3
from unittest import TestCase, main
from materials import Material
from tuples import Point, Vector, Color
from lights import Light
from patterns import Solid, Stripe
from spheres import Sphere
import math
class MaterialTest(TestCase):
def setUp(self):
self.m = Material()
self.position = Point(0, 0, 0)
self.obj = Sphere()
def tearDown(self):
pass
def test_marial(self):
m = Material()
tests = [(m.color, Color(1, 1, 1)),
(m.ambient, 0.1),
(m.diffuse, 0.9),
(m.specular, 0.9),
(m.shininess, 200)]
for a, b in tests:
self.assertEqual(a, b)
def test_lighting_with_eye_between_light_surface(self):
eye_vecotr = Vector(0, 0, -1)
normal_vector = Vector(0, 0, -1)
light = Light(Point(0, 0, -10), Color(1, 1, 1))
result = self.m.lighting(
self.obj, light, self.position, eye_vecotr, normal_vector)
self.assertEqual(result, Color(1.9, 1.9, 1.9))
def test_lighting_with_eye_between_light_surface_eye45(self):
eye_vector = Vector(0, 1 / math.sqrt(2), -1 / math.sqrt(2))
normal_vector = Vector(0, 0, -1)
light = Light(Point(0, 0, -10), Color(1, 1, 1))
self.assertEqual(
self.m.lighting(self.obj, light, self.position,
eye_vector, normal_vector),
Color(1.0, 1.0, 1.0))
def test_lighting_with_eye_opposite_surface_light45(self):
eye_vector = Vector(0, 0, -1)
normal_vector = Vector(0, 0, -1)
light = Light(Point(0, 10, -10), Color(1, 1, 1))
self.assertEqual(
self.m.lighting(self.obj, light, self.position,
eye_vector, normal_vector),
Color(0.7364, 0.7364, 0.7364))
def test_lighting_eye_reflection(self):
eye_vector = Vector(0, -1 / math.sqrt(2), -1 / math.sqrt(2))
normal_vector = Vector(0, 0, -1)
light = Light(Point(0, 10, -10), Color(1, 1, 1))
self.assertEqual(
self.m.lighting(self.obj, light, self.position,
eye_vector, normal_vector),
Color(1.6364, 1.6364, 1.6364))
def test_lighting_behind_surface(self):
eye_vector = Vector(0, 0, -1)
normal_vector = Vector(0, 0, -1)
light = Light(Point(0, 0, 10), Color(1, 1, 1))
self.assertEqual(
self.m.lighting(self.obj, light, self.position,
eye_vector, normal_vector),
Color(0.1, 0.1, 0.1))
def test_lighting_with_surface_in_shadow(self):
eye_vercotr = Vector(0, 0, -1)
normal_vector = Vector(0, 0, -1)
light = Light(Point(0, 0, -10), Color(1, 1, 1))
in_shadow = True
result = self.m.lighting(self.obj, light, self.position, eye_vercotr,
normal_vector, in_shadow)
self.assertEqual(result, Color(0.1, 0.1, 0.1))
def test_lighting_with_stripe_aplied(self):
self.m.pattern = Stripe(Solid(Color(1, 1, 1)), Solid(Color(0, 0, 0)))
self.m.ambient = 1
self.m.diffuse = 0
self.m.specular = 0
eye_vector = Vector(0, 0, -1)
normal_vector = Vector(0, 0, -1)
light = Light(Point(0, 0, -10), Color(1, 1, 1))
for xyz, color in [((0.9, 0, 0), Color(1, 1, 1)),
((1.1, 0, 0), Color(0, 0, 0))]:
self.assertEqual(
self.m.lighting(self.obj, light, Point(
*xyz), eye_vector, normal_vector),
color)
def test_reflectivity(self):
self.assertEqual(self.m.reflective, 0.0)
if __name__ == '__main__':
main()
materials.py
from tuples import Color, is_equal
class Material:
def __init__(self, color=Color(1, 1, 1), ambient=0.1, diffuse=0.9,
specular=0.9, shininess=200, pattern=None, reflective=0):
self.color = color
self.ambient = ambient
self.diffuse = diffuse
self.specular = specular
self.shininess = shininess
self.pattern = pattern
self.reflective = reflective
def __repr__(self):
return f'Material({self.color},{self.ambient},{self.diffuse},' +\
f'{self.specular},{self.shininess})'
def __eq__(self, other):
if self.color != other.color:
return False
tests = [(self.ambient, other.ambient),
(self.diffuse, other.diffuse),
(self.specular, other.specular),
(self.shininess, other.shininess)]
for a, b in tests:
if not is_equal(a, b):
return False
return True
def lighting(self, obj, light, point, eye_vector, normal_vector,
in_shadow=False) -> Color:
if self.pattern is None:
color = self.color
else:
color = self.pattern.at_shape(obj, point)
effective_color = color * light.intensity
light_vector = (light.position - point).normalize()
ambient = effective_color * self.ambient
if in_shadow:
return ambient
light_dot_normal = light_vector.dot(normal_vector)
if light_dot_normal < 0:
diffuse = Color(0, 0, 0)
specular = Color(0, 0, 0)
else:
diffuse = effective_color * self.diffuse * light_dot_normal
reflect_vector = -light_vector.reflect(normal_vector)
reflect_dot_eye = reflect_vector.dot(eye_vector)
if reflect_dot_eye <= 0:
specular = Color(0, 0, 0)
else:
factor = reflect_dot_eye ** self.shininess
specular = light.intensity * self.specular * factor
return ambient + diffuse + specular
intersections_test.py
#!/usr/bin/env python3
from unittest import TestCase, main
import math
from intersections import Intersection, Intersections
from tuples import Point, Vector, EPSILON
from spheres import Sphere
from planes import Plane
from rays import Ray
from transformations import translation
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)
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)
def test_hit_shoud_offset_point(self):
ray = Ray(Point(0, 0, -5), Vector(0, 0, 1))
shape = Sphere(transform=translation(0, 0, 1))
i = Intersection(5, shape)
comps = i.prepare_computations(ray)
self.assertLess(comps.over_point.z, -EPSILON / 2)
self.assertGreater(comps.point.z, comps.over_point.z)
def test_precomputing_reflection_vvector(self):
shape = Plane()
ray = Ray(Point(0, 1, -1),
Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2)))
intersection = Intersection(math.sqrt(2), shape)
comps = intersection.prepare_computations(ray)
self.assertEqual(comps.reflect_vector,
Vector(0, 1 / math.sqrt(2), 1 / math.sqrt(2)))
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])
if __name__ == '__main__':
main()
intersections.py
from tuples import Point, EPSILON
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):
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,
ray=ray)
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, ray):
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 = -self.normal_vector
else:
self.inside = False
self.over_point = point + self.normal_vector * EPSILON
self.reflect_vector = ray.direction.reflect(self.normal_vector)
def __repr__(self):
return f'Computations({self.t},{self.obj},{self.point},' +\
f'{self.eye_vector},{self.normal_vector},{self.inside})'
world_test.py
#!/usr/bin/env python3
from unittest import TestCase, main
import math
from tuples import Point, Vector, Color
from lights import Light
from spheres import Sphere
from planes import Plane
from materials import Material
from transformations import scaling, translation
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.sphere1 = Sphere(material=Material(color=Color(0.8, 1.0, 0.6),
diffuse=0.7,
specular=0.2))
self.sphere2 = Sphere(transform=scaling(0.5, 0.5, 0.5))
self.world = World(objs=[self.sphere1, self.sphere2], 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.world.light, self.light)
for s in [self.sphere1, self.sphere2]:
self.assertIn(s, self.world)
def test_intersect_ray(self):
r = Ray(Point(0, 0, -5), Vector(0, 0, 1))
xs = self.world.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.world[0]
i = Intersection(4, shape)
comps = i.prepare_computations(r)
c = self.world.shade_hit(comps)
self.assertEqual(c, Color(0.38066, 0.47583, 0.2855))
def test_shade_hit_from_inside(self):
self.world.light = Light(Point(0, 0.25, 0), Color(1, 1, 1))
r = Ray(Point(0, 0, 0), Vector(0, 0, 1))
shape = self.world[1]
i = Intersection(0.5, shape)
comps = i.prepare_computations(r)
c = self.world.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.world.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.world.color_at(r)
self.assertEqual(c, Color(0.38066, 0.47583, 0.2855))
def test_color_intersection_behind_the_ray(self):
outer = self.world[0]
inner = self.world[1]
outer.material.ambient = 1
inner.material.ambient = 1
r = Ray(Point(0, 0, 0.75), Vector(0, 0, -1))
c = self.world.color_at(r)
self.assertNotEqual(c, outer.material.color)
self.assertEqual(c, inner.material.color)
def test_no_shadow_nothing_is_collinear_with_point_and_light(self):
p = Point(0, 10, 0)
self.assertFalse(self.world.is_shadowed(p))
def test_shadow_obj_between_point_and_light(self):
p = Point(10, -10, 10)
self.assertTrue(self.world.is_shadowed(p))
def test_no_shadow_obj_behind_light(self):
p = Point(-20, 20, -20)
self.assertFalse(self.world.is_shadowed(p))
def test_no_shadow_obj_behind_point(self):
p = Point(-2, 2, -2)
self.assertFalse(self.world.is_shadowed(p))
def test_shade_hit_given_intersection_in_shadow(self):
light = Light(Point(0, 0, -10), Color(1, 1, 1))
s1 = Sphere()
s2 = Sphere(translation(0, 0, 10))
w = World([s1, s2], light)
ray = Ray(Point(0, 0, 5), Vector(0, 0, 1))
i = Intersection(4, s2)
comps = i.prepare_computations(ray)
c = w.shade_hit(comps)
self.assertEqual(c, Color(0.1, 0.1, 0.1))
def test_reflected_color_for_nonreflecive_material(self):
ray = Ray(Point(0, 0, 0), Vector(0, 0, 1))
shape = self.sphere2
shape.material.ambient = 1
intersection = Intersection(1, shape)
comps = intersection.prepare_computations(ray)
color = self.world.reflected_color(comps)
self.assertEqual(color, Color(0, 0, 0))
def test_reflected_color_for_reflective_material(self):
shape = Plane(material=Material(reflective=0.5),
transform=translation(0, -1, 0))
self.world.objs.append(shape)
ray = Ray(Point(0, 0, -3),
Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2)))
intersection = Intersection(math.sqrt(2), shape)
comps = intersection.prepare_computations(ray)
color = self.world.reflected_color(comps)
self.assertEqual(color, Color(0.19033, 0.23791, 0.14274))
def test_shade_hit_with_reflective_material(self):
shape = Plane(material=Material(reflective=0.5),
transform=translation(0, -1, 0))
self.world.objs.append(shape)
ray = Ray(Point(0, 0, -3),
Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2)))
intersection = Intersection(math.sqrt(2), shape)
comps = intersection.prepare_computations(ray)
color = self.world.shade_hit(comps)
self.assertEqual(color, Color(0.87675, 0.92434, 0.82917))
def test_reflected_color_at_maximum_recursive_depth(self):
shape = Plane(material=Material(reflective=0.5),
transform=translation(0, -1, 0))
self.world.objs.append(shape)
ray = Ray(Point(0, 0, -3),
Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2)))
intersection = Intersection(math.sqrt(2), shape)
comps = intersection.prepare_computations(ray)
color = self.world.reflected_color(comps, 0)
self.assertEqual(color, Color(0, 0, 0))
if __name__ == '__main__':
main()
world.py
#!/usr/bin/env python3
from intersections import Intersections
from tuples import Color
from rays import Ray
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, remaining=1):
surface = comps.obj.material.lighting(
comps.obj, self.light, comps.point, comps.eye_vector,
comps.normal_vector, self.is_shadowed(comps.over_point))
reflected = self.reflected_color(comps, remaining)
return surface + reflected
def color_at(self, ray, remaining=1):
intersections = self.intersect(ray)
hit = intersections.hit()
if hit is None:
return Color(0, 0, 0)
comps = hit.prepare_computations(ray)
return self.shade_hit(comps, remaining)
def is_shadowed(self, point) -> bool:
vector = self.light.position - point
distance = vector.magnitude()
direction = vector.normalize()
ray = Ray(point, direction)
intersections = self.intersect(ray)
hit = intersections.hit()
return (hit is not None) and hit.t < distance
def reflected_color(self, comps, remaining=5):
if remaining <= 0:
return Color(0, 0, 0)
if comps.obj.material.reflective == 0:
return Color(0, 0, 0)
reflect_ray = Ray(comps.over_point, comps.reflect_vector)
color = self.color_at(reflect_ray, remaining)
return color * comps.obj.material.reflective
sample1.py
#!/usr/bin/env python3
import math
import time
from tuples import Point, Vector, Color
from planes import Plane
from spheres import Sphere
from materials import Material
from patterns import Solid, Stripe, Checkers
from camera import Camera
from lights import Light
from world import World
from transformations import translation, scaling, view_transform
print('ファイル名, rendering time(秒)')
width = 250
height = 125
checkers = Checkers(Solid(Color(0, 1, 0)), Solid(Color(1, 1, 1)))
planes = [Plane(material=Material(pattern=checkers, reflective=1)),
Plane(material=Material(pattern=checkers))]
stripe = Stripe(Solid(Color(1, 0, 0)), Solid(Color(0, 0, 1)),
transform=scaling(0.5, 0.5, 0.5))
spheres = [Sphere(material=Material(pattern=stripe),
transform=translation(0, 1, 0)),
Sphere(material=Material(pattern=stripe, reflective=1),
transform=translation(0, 1, 0))]
camera = Camera(width, height, math.pi / 2,
transform=view_transform(Point(0, 1.5, -5), Point(0, 1, 0),
Vector(0, 1, 0)))
world = World([], Light(Point(-10, 10, -10), Color(1, 1, 1)))
for i, objs in enumerate(zip(planes, spheres), 1):
world.objs = objs
start = time.time()
canvas = camera.render(world)
s = time.time() - start
with open(f'sample{i}.ppm', 'w') as f:
canvas.to_ppm(f)
print(f'sample{i}.ppm,{s}')
入出力結果(Bash、cmd(コマンドプロンプト)、Terminal、Jupyter(IPython))
C:\Users\...>py materials_test.py ........ ---------------------------------------------------------------------- Ran 8 tests in 0.003s OK C:\Users\...>py patterns_test.py ................ ---------------------------------------------------------------------- Ran 16 tests in 0.007s OK C:\Users\...>py sample4.py ファイル名, rendering time(秒) sample6.ppm,132.31091618537903 sample7.ppm,143.2693841457367 C:\Users\...>
0 コメント:
コメントを投稿