2019年5月15日水曜日

開発環境

The Ray Tracer Challenge: A Test-Driven Guide to Your First 3D Renderer (Jamis Buck(著)、Pragmatic Bookshelf)、Chapter 8(Shadows)のPut It Together(116)を取り組んでみる。

コード

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
import math


class MaterialTest(TestCase):
    def setUp(self):
        self.m = Material()
        self.position = Point(0, 0, 0)

    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(
            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(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(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(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(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(light, self.position, eye_vercotr,
                                 normal_vector, in_shadow)
        self.assertEqual(result, Color(0.1, 0.1, 0.1))


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):
        self.color = color
        self.ambient = ambient
        self.diffuse = diffuse
        self.specular = specular
        self.shininess = shininess

    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, light, point, eye_vector, normal_vector,
                 in_shadow=False) -> Color:
        effective_color = self.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

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, 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.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)

    def test_no_shadow_nothing_is_collinear_with_point_and_light(self):
        p = Point(0, 10, 0)
        self.assertFalse(self.w.is_shadowed(p))

    def test_shadow_obj_between_point_and_light(self):
        p = Point(10, -10, 10)
        self.assertTrue(self.w.is_shadowed(p))

    def test_no_shadow_obj_behind_light(self):
        p = Point(-20, 20, -20)
        self.assertFalse(self.w.is_shadowed(p))

    def test_no_shadow_obj_behind_point(self):
        p = Point(-2, 2, -2)
        self.assertFalse(self.w.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))


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):
        return comps.obj.material.lighting(
            self.light, comps.point, comps.eye_vector, comps.normal_vector,
            self.is_shadowed(comps.over_point))

    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)

    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

intrsections_test.py

#!/usr/bin/env python3
from unittest import TestCase, main
from intersections import Intersection, Intersections
from tuples import Point, Vector, EPSILON
from spheres import Sphere
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)


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
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 = -self.normal_vector
        else:
            self.inside = False
        self.over_point = point + self.normal_vector * EPSILON

    def __repr__(self):
        return f'Computations({self.t},{self.obj},{self.point},' +\
            f'{self.eye_vector},{self.normal_vector},{self.inside})'

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(250, 125, 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('sample1.ppm', 'w') as f:
    canvas.to_ppm(f)
seconds_to_ppm = time.time() - start
print('ファイル名, rendering time(秒)')
print(f'sample1.ppm,{seconds_renader}')

other = Sphere(translation(0, 0.7, -1) * scaling(0.7, 0.7, 0.7),
               Material(Color(1, 0, 0), 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'sample2.ppm', 'w') as f:
    canvas.to_ppm(f)
seconds_to_ppm = time.time() - start
print(f'sample2.ppm,{seconds_renader}')

入出力結果(cmd(コマンドプロンプト)、Terminal、Bash、Jupyter(IPython))

C:\Users\...>py materials_test.py
.......
----------------------------------------------------------------------
Ran 7 tests in 0.001s

OK

C:\Users\...>py world_test.py
.............
----------------------------------------------------------------------
Ran 13 tests in 0.022s

OK

C:\Users\...>py intersections_test.py
..........
----------------------------------------------------------------------
Ran 10 tests in 0.005s

OK

C:\Users\...>py sample.py
ファイル名, rendering time(秒)
sample1.ppm,304.4209039211273
sample2.ppm,341.31131410598755

C:\Users\...>

0 コメント:

コメントを投稿