2019年5月11日土曜日

開発環境

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

コード

Python 3

tuples_test.py

#!/usr/bin/env python3
from unittest import TestCase, main
from tuples import Tuple, Point, Vector, Color
import math


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

    def tearDown(self):
        pass

    def test_is_point(self):
        a = Point(4.3, -4.2, 3.1)
        self.assertEqual(a.x, 4.3)
        self.assertEqual(a.y, -4.2)
        self.assertEqual(a.z, 3.1)
        self.assertEqual(type(a), Point)
        self.assertNotEqual(type(a), Vector)

    def test_add(self):
        a1 = Tuple(3, -2, 5, 1)
        a2 = Tuple(-2, 3, 1, 0)
        self.assertEqual(a1 + a2, Tuple(1, 1, 6, 1))

    def test_sub(self):
        p1 = Point(3, 2, 1)
        p2 = Point(5, 6, 7)
        self.assertEqual(p1 - p2, Vector(-2, -4, -6))
        self.assertEqual(type(p1 - p2), Vector)

    def test_sub_vector_from_point(self):
        p = Point(3, 2, 1)
        v = Vector(5, 6, 7)
        self.assertEqual(p - v, Point(-2, -4, -6))

    def test_sub_vector(self):
        v1 = Vector(3, 2, 1)
        v2 = Vector(5, 6, 7)
        self.assertEqual(v1 - v2, Vector(-2, -4, -6))

    def test_sub_vect_from_zero_vect(self):
        zero = Vector(0, 0, 0)
        v = Vector(1, -2, 3)
        self.assertEqual(zero - v, Vector(-1, 2, -3))

    def test_neg(self):
        a = Tuple(1, -2, 3, -4)
        self.assertEqual(-a, Tuple(-1, 2, -3, 4))

    def test_scalar_mul(self):
        a = Tuple(1, -2, 3, -4)
        self.assertEqual(a * 3.5, Tuple(3.5, -7, 10.5, -14))
        self.assertEqual(a * 0.5, Tuple(0.5, -1, 1.5, -2))

    def test_div(self):
        a = Tuple(1, -2, 3, -4)
        self.assertEqual(a / 2, Tuple(0.5, -1, 1.5, -2))

    def test_mag_vector(self):
        vectors = [Vector(1, 0, 0),
                   Vector(0, 1, 0),
                   Vector(0, 0, 1),
                   Vector(1, 2, 3),
                   Vector(-1, -2, -3)]
        mags = [1, 1, 1, math.sqrt(14), math.sqrt(14)]
        for vector, mag in zip(vectors, mags):
            self.assertEqual(vector.magnitude(), mag)

    def test_normalizing_vector(self):
        v = Vector(4, 0, 0)
        self.assertEqual(v.normalize(), Vector(1, 0, 0))
        v = Vector(1, 2, 3)
        self.assertEqual(v.normalize(),
                         Vector(1 / math.sqrt(14),
                                2 / math.sqrt(14),
                                3 / math.sqrt(14)))
        norm = v.normalize()
        self.assertEqual(norm.magnitude(), 1)

    def test_dot_product(self):
        a = Vector(1, 2, 3)
        b = Vector(2, 3, 4)
        self.assertEqual(a.dot(b), 20)

    def test_cross_product(self):
        a = Vector(1, 2, 3)
        b = Vector(2, 3, 4)
        self.assertEqual(a.cross(b), Vector(-1, 2, - 1))
        self.assertEqual(b.cross(a), Vector(1, -2, 1))

    def test_color(self):
        c = Color(-0.5, 0.4, 1.7)
        tests = {c.red: -0.5, c.green: 0.4, c.blue: 1.7}
        for k, v in tests.items():
            self.assertEqual(k, v)

    def test_colors_add(self):
        c1 = Color(0.9, 0.6, 0.75)
        c2 = Color(0.7, 0.1, 0.25)
        self.assertEqual(c1 + c2, Color(1.6, 0.7, 1.0))

    def test_colors_sub(self):
        c1 = Color(0.9, 0.6, 0.75)
        c2 = Color(0.7, 0.1, 0.25)
        self.assertEqual(c1 - c2, Color(0.2, 0.5, 0.5))

    def test_colors_mul_by_scalar(self):
        c = Color(0.2, 0.3, 0.4)
        self.assertEqual(c * 2, Color(0.4, 0.6, 0.8))

    def test_colors_mul(self):
        c1 = Color(1, 0.2, 0.4)
        c2 = Color(0.9, 1, 0.1)
        self.assertEqual(c1 * c2, Color(0.9, 0.2, 0.04))


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

    def tearDown(self):
        pass

    def test_reflect(self):
        v = Vector(1, -1, 0)
        n = Vector(0, 1, 0)
        self.assertEqual(v.reflect(n), Vector(1, 1, 0))

    def test_reflect_slanted_surface(self):
        v = Vector(0, -1, 0)
        n = Vector(1 / math.sqrt(2), 1 / math.sqrt(2), 0)
        self.assertEqual(v.reflect(n), Vector(1, 0, 0))


if __name__ == '__main__':
    main()

tuples.py

#!/usr/bin/env python3
import math

EPSILON = 0.00001


def is_equal(a: float, b: float):
    return abs(a - b) < EPSILON


class Tuple:
    def __init__(self, x: float, y: float, z: float, w: float):
        self.x = x
        self.y = y
        self.z = z
        self.w = w

    def __eq__(self, other):
        return is_equal(self.x, other.x) and is_equal(self.y, other.y) and \
            is_equal(self.z, other.z) and is_equal(self.w, other.w)

    def __add__(self, other):
        return self.__class__(self.x + other.x, self.y + other.y,
                              self.z + other.z, self.w + other.w)

    def __sub__(self, other):
        return self.__class__(self.x - other.x, self.y - other.y,
                              self.z - other.z, self.w - other.w)

    def __neg__(self):
        return self.__class__(-self.x, -self.y, -self.z, -self.w)

    def __mul__(self, other: float):
        return self.__class__(self.x * other, self.y * other,
                              self.z * other, self.w * other)

    def __truediv__(self, other):
        return self * (1 / other)

    def magnitude(self):
        return math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2 + self.w ** 2)

    def normalize(self):
        mag = self.magnitude()
        return self.__class__(self.x, self.y, self.z, self.w) / mag

    def dot(self, other):
        return sum([a * b
                    for a, b in zip([self.x, self.y, self.z, self.w],
                                    [other.x, other.y, other.z, other.w])])

    def __repr__(self):
        return f'{self.__class__.__name__}({self.x},{self.y},{self.z},{self.w})'


class Point(Tuple):
    def __init__(self, x: float, y: float, z: float, w: float = 1):
        super().__init__(x, y, z, w)

    def __sub__(self, other):
        t = super().__sub__(other)
        if type(other) == self.__class__:
            return Vector(t.x, t.y, t.z)
        elif type(other) == Vector:
            return t
        raise TypeError(
            "unsupported operand type(s) for -: "
            f"'{type(self)}' and '{type(other)}'")


class Vector(Tuple):
    def __init__(self, x: float, y: float, z: float, w: float = 0):
        super().__init__(x, y, z, w)

    def cross(self, other):
        return self.__class__(self.y * other.z - self.z * other.y,
                              self.z * other.x - self.x * other.z,
                              self.x * other.y - self.y * other.x)

    def reflect(self, normal):
        return self - normal * 2 * self.dot(normal)


class Color(Tuple):
    def __init__(self, red: float, green: float, blue: float, w: float = 0):
        super().__init__(red, green, blue, 0)
        self.red = red
        self.green = green
        self.blue = blue

    def __mul__(self, other):
        if type(other) == self.__class__:
            return self.__class__(
                self.x * other.x, self.y * other.y, self.z * other.z)
        return super().__mul__(other)

lights_test.py

#!/usr/bin/env python3
from unittest import TestCase, main
from tuples import Point, Color
from lights import Light


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

    def tearDown(self):
        pass

    def test_light(self):
        intensity = Color(1, 1, 1)
        position = Point(0, 0, 0)
        light = Light(position, intensity)
        for a, b in [(light.position, position),
                     (light.intensity, intensity)]:
            self.assertEqual(a, b)


if __name__ == '__main__':
    main()

lights.py

class Light:
    def __init__(self, position, intensity):
        self.position = position
        self.intensity = intensity

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_lightning_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.lightning(
            light, self.position, eye_vecotr, normal_vector)
        self.assertEqual(result, Color(1.9, 1.9, 1.9))

    def test_lightning_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.lightning(light, self.position, eye_vector, normal_vector),
            Color(1.0, 1.0, 1.0))

    def test_lightning_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.lightning(light, self.position, eye_vector, normal_vector),
            Color(0.7364, 0.7364, 0.7364))

    def test_lightning_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.lightning(light, self.position, eye_vector, normal_vector),
            Color(1.6364, 1.6364, 1.6364))

    def test_lightning_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.lightning(light, self.position, eye_vector, normal_vector),
            Color(0.1, 0.1, 0.1))


if __name__ == '__main__':
    main()

materials.py

from tuples import Color, is_equal


class Material:
    def __init__(self):
        self.color = Color(1, 1, 1)
        self.ambient = 0.1
        self.diffuse = 0.9
        self.specular = 0.9
        self.shininess = 200

    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 lightning(self, light, point, eye_vector, normal_vector) -> Color:
        effective_color = self.color * light.intensity
        light_vector = (light.position - point).normalize()
        ambient = effective_color * self.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

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_transform(self):
        self.assertEqual(Sphere().transform,
                         Matrix([[1, 0, 0, 0],
                                 [0, 1, 0, 0],
                                 [0, 0, 1, 0],
                                 [0, 0, 0, 1]]))
        s = Sphere()
        t = translation(2, 3, 4)
        s.transform = t
        self.assertEqual(s.transform, t)

    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_normalzed_vector(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_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))

    def test_material(self):
        s = Sphere()
        m = s.material
        self.assertEqual(m, Material())

    def test_material_assigned(self):
        s = Sphere()
        m = Material()
        m.ambient = 1
        s.material = m
        self.assertEqual(s.material, m)


if __name__ == '__main__':
    main()

spheres.py

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():
    def __init__(self):
        self.transform = identity_matrix
        self.material = Material()

    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 normal_at(self, world_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()

casting_rays_sphere.py

#!/usr/bin/env python3
from canvas import Canvas
from tuples import Point, Color
from rays import Ray
from spheres import Sphere
from transformations import scaling, rotation_z, shearing
from materials import Material
from lights import Light
import math

ray_origin = Point(0, 0, -5)
wall_z = 10
wall_size = 7.0
canvas_pixels = 100
pixel_size = wall_size / canvas_pixels
half = wall_size / 2
canvas = Canvas(canvas_pixels, canvas_pixels)
sphere = Sphere()
sphere.material = Material()
sphere.material.color = Color(1, 0.2, 1)
light_position = Point(-10, 10, -10)
light_color = Color(1, 1, 1)
light = Light(light_position, light_color)

for y in range(canvas_pixels):
    world_y = half - pixel_size * y
    for x in range(canvas_pixels):
        world_x = -half + pixel_size * x
        position = Point(world_x, world_y, wall_z)
        ray = Ray(ray_origin, (position - ray_origin).normalize())
        xs = sphere.intersect(ray)
        hit = xs.hit()
        if hit is not None:
            point = ray.position(hit.t)
            normal = hit.obj.normal_at(point)
            eye = -ray.direction
            color = hit.obj.material.lightning(light, point, eye, normal)
            canvas.write_pixel(int(x), int(y), color)

with open('casting_rays_sphere.ppm', 'w') as f:
    canvas.to_ppm(f)

transforms = [scaling(1, 0.5, 1),
              scaling(0.5, 1, 1),
              rotation_z(math.pi / 4) * scaling(0.5, 1, 1),
              shearing(1, 0, 0, 0, 0, 0) * scaling(0.5, 1, 1)]

colors = [Color(1, 0, 0), Color(0, 1, 0), Color(0, 0, 1), Color(0.5, 0.5, 0)]

for i, (transform, color) in enumerate(zip(transforms, colors), 1):
    canvas = Canvas(canvas_pixels, canvas_pixels)
    sphere.transform = transform
    sphere.material.color = color
    for y in range(canvas_pixels):
        world_y = half - pixel_size * y
        for x in range(canvas_pixels):
            world_x = -half + pixel_size * x
            position = Point(world_x, world_y, wall_z)
            ray = Ray(ray_origin, (position - ray_origin).normalize())
            hit = xs.hit()
            xs = sphere.intersect(ray)
            if hit is not None:
                point = ray.position(hit.t)
                normal = hit.obj.normal_at(point)
                eye = -ray.direction
                color = hit.obj.material.lightning(light, point, eye, normal)
                canvas.write_pixel(int(x), int(y), color)
    with open(f'casting_rays_sphere{i}.ppm', 'w') as f:
        canvas.to_ppm(f)

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

C:\Users\...>py tuples_test.py
....
....................
----------------------------------------------------------------------
Ran 20 tests in 0.001s

OK

C:\Users\...>py lights_test.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

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

OK

C:\Users\...>py spheres_test.py
.............
----------------------------------------------------------------------
Ran 13 tests in 0.012s

OK

C:\Users\...>py casting_rays_sphere.py

C:\Users\...>

0 コメント:

コメントを投稿