2019年5月17日金曜日

開発環境

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

コード

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 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(Color(1, 1, 1), 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)


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

    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

patterns_test.py

#!//usr/bin/env python3
from unittest import TestCase, main
from patterns import Pattern, Stripe, Gradient, Ring, Checkers
from tuples import Point, Color
from spheres import Sphere
from transformations import scaling, translation
from matrices import IDENTITY_MATRIX


class PatternTest(TestCase):
    def setUp(self):
        self.pattern = Pattern()

    def tearDown(self):
        pass

    def test_transformation(self):
        self.assertEqual(self.pattern.transform, IDENTITY_MATRIX)

    def test_assign_transformation(self):
        self.pattern.transform = translation(1, 2, 3)
        self.assertEqual(self.pattern.transform,  translation(1, 2, 3))

    def test_with_obj_transformation(self):
        shape = Sphere(scaling(2, 2, 2))
        c = self.pattern.at_shape(shape, Point(2, 3, 4))
        self.assertEqual(c, Color(1, 1.5, 2))

    def test_with_pattern_transformation(self):
        shape = Sphere()
        self.pattern.transform = scaling(2, 2, 2)
        c = self.pattern.at_shape(shape, Point(2, 3, 4))
        self.assertEqual(c, Color(1, 1.5, 2))


class StripeTest(TestCase):
    def setUp(self):
        self.black = Color(0, 0, 0)
        self.white = Color(1, 1, 1)

    def tearDown(self):
        pass

    def test_stripe(self):
        pattern = Stripe(self.white, self.black)
        for a, b in [(pattern.a, self.white),
                     (pattern.b, self.black)]:
            self.assertEqual(a, b)

    def test_constant_y(self):
        pattern = Stripe(self.white, self.black)
        for point in [Point(0, 0, 0),
                      Point(0, 1, 0),
                      Point(0, 2, 0)]:
            self.assertEqual(pattern.at(point), self.white)

    def test_constant_z(self):
        pattern = Stripe(self.white, self.black)
        for point in [Point(0, 0, 0),
                      Point(0, 0, 1),
                      Point(0, 0, 2)]:
            self.assertEqual(pattern.at(point), self.white)

    def test_alternates_x(self):
        pattern = Stripe(self.white, self.black)
        for xyz, color in [((0, 0, 0), self.white),
                           ((0.9, 0, 0), self.white),
                           ((1, 0, 0), self.black),
                           ((-0.1, 0, 0), self.black),
                           ((-1, 0, 0), self.black),
                           ((-1.1, 0, 0), self.white)]:
            self.assertEqual(pattern.at(Point(*xyz)), color)

    def test_with_obj_transformation(self):
        obj = Sphere(transform=scaling(2, 2, 2))
        pattern = Stripe(self.white, self.black)
        self.assertEqual(pattern.at_shape(obj, Point(1.5, 0, 0)),
                         self.white)

    def test_with_pattern_transformation(self):
        obj = Sphere()
        pattern = Stripe(self.white, self.black, transform=scaling(2, 2, 2))
        self.assertEqual(pattern.at_shape(obj, Point(1.5, 0, 0)),
                         self.white)

    def test_with_both_obj_pattern_transformation(self):
        obj = Sphere(scaling(2, 2, 2))
        pattern = Stripe(self.white, self.black, translation(0.5, 0, 0))
        self.assertEqual(pattern.at_shape(obj, Point(2.5, 0, 0)),
                         self.white)


class GradientTest(TestCase):
    def setUp(self):
        pass

    def tearDown(self):
        pass

    def test_linearly_interpolates_between_colors(self):
        pattern = Gradient(Color(1, 1, 1), Color(0, 0, 0))
        for point_xyz, color_xyz in [((0, 0, 0), (1, 1, 1)),
                                     ((0.25, 0, 0), (0.75, 0.75, 0.75)),
                                     ((0.5, 0, 0), (0.5, 0.5, 0.5)),
                                     ((0.75, 0, 0), (0.25, 0.25, 0.25))]:
            self.assertEqual(pattern.at(Point(*point_xyz)),
                             Color(*color_xyz))


class RingTest(TestCase):
    def setUp(self):
        pass

    def tearDown(self):
        pass

    def test(self):
        ring = Ring(Color(1, 1, 1), Color(0, 0, 0))
        for point, color in [((0, 0, 0), (1, 1, 1)),
                             ((1, 0, 0), (0, 0, 0)),
                             ((0, 0, 1), (0, 0, 0)),
                             ((0.708, 0, 0.708), (0, 0, 0))]:
            self.assertEqual(ring.at(Point(*point)), Color(*color))


class CheckersTest(TestCase):
    def setUp(self):
        self.white = Color(1, 1, 1)
        self.black = Color(0, 0,  0)

    def tearDown(self):
        pass

    def test_repeate_x(self):
        checkers = Checkers(self.white, self.black)
        for point, color in [((0, 0, 0), self.white),
                             ((0.99, 0, 0), self.white),
                             ((1.01, 0, 0), self.black)]:
            self.assertEqual(checkers.at(Point(*point)), color)

    def test_repeate_y(self):
        checkers = Checkers(self.white, self.black)
        for point, color in [((0, 0, 0), self.white),
                             ((0, 0.99, 0), self.white),
                             ((0, 1.01, 0), self.black)]:
            self.assertEqual(checkers.at(Point(*point)), color)

    def test_repeate_z(self):
        checkers = Checkers(self.white, self.black)
        for point, color in [((0, 0, 0), self.white),
                             ((0, 0, 0.99), self.white),
                             ((0, 0, 1.01), self.black)]:
            self.assertEqual(checkers.at(Point(*point)), color)


if __name__ == '__main__':
    main()

patterns.py

import math
from matrices import IDENTITY_MATRIX
from tuples import Color


class Pattern:
    def __init__(self, transform=IDENTITY_MATRIX):
        self.transform = transform

    def at(self, point):
        return Color(point.x, point.y, point.z)

    def at_shape(self, shape, world_point):
        obj_point = shape.transform.inverse() * world_point
        pattern_point = self.transform.inverse() * obj_point
        return self.at(pattern_point)


class Stripe(Pattern):
    def __init__(self, color1, color2, transform=IDENTITY_MATRIX):
        super().__init__(transform)
        self.a = color1
        self.b = color2

    def at(self, point):
        if math.floor(point.x) % 2 == 0:
            return self.a
        return self.b


class Gradient(Pattern):
    def __init__(self, color1, color2, transform=IDENTITY_MATRIX):
        super().__init__(transform)
        self.a = color1
        self.b = color2

    def at(self, point):
        distance = self.b - self.a
        fraction = point.x - math.floor(point.x)
        return self.a + distance * fraction


class Ring(Pattern):
    def __init__(self, color1, color2, transform=IDENTITY_MATRIX):
        super().__init__(transform)
        self.a = color1
        self.b = color2

    def at(self, point):
        if math.floor(math.sqrt(point.x ** 2 + point.z ** 2)) % 2 == 0:
            return self.a
        return self.b


class Checkers(Pattern):
    def __init__(self, color1, color2, transform=IDENTITY_MATRIX):
        super().__init__(transform)
        self.a = color1
        self.b = color2

    def at(self, point):
        if sum([math.floor(p) for p in [point.x, point.y, point.z]]) % 2 == 0:
            return self.a
        return self.b


class RadialGradient(Pattern):
    def __init__(self, color1, color2, transform=IDENTITY_MATRIX):
        super().__init__(transform)
        self.a = color1
        self.b = color2

    def at(self, point):
        color_distance = self.b - self.a
        distance = math.sqrt(point.x ** 2 + point.z ** 2)
        fraction = distance - math.floor(distance)
        return self.a + color_distance * fraction

sample1.py

#!/usr/bin/env python3
import math
import time
from tuples import Point, Vector, Color
from planes import Plane
from materials import Material
from patterns import Ring, Gradient, RadialGradient
from camera import Camera
from lights import Light
from world import World
from transformations import scaling, view_transform, translation, rotation_x

print('ファイル名, rendering time(秒)')

width = 250
height = 125
patterns = [Ring(Color(1, 0, 0), Color(0, 1, 0)),
            Gradient(Color(1, 0, 0), Color(0, 1, 0)),
            RadialGradient(Color(1, 0, 0), Color(0, 1, 0))]

material = Material(specular=0)
# material = Material(specular=0, pattern=pattern)
shape = Plane(material=material, transform=rotation_x(math.pi / 2))

camera = Camera(width, height, math.pi / 3,
                transform=view_transform(Point(0, 0, -5),
                                         Point(0, 0, 0),
                                         Vector(0, 1, 0)))
world = World([shape], Light(Point(-10, 10, -10), Color(1, 1, 1)))
for i, pattern in enumerate(patterns, 1):
    material.pattern = pattern
    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}')

入出力結果(cmd(コマンドプロンプト)、Terminal、Bash、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 sample1.py
ファイル名, rendering time(秒)
sample1.ppm,130.32527017593384
sample2.ppm,131.36756920814514
sample3.ppm,139.62577509880066

C:\Users\...>

0 コメント:

コメントを投稿