2019年5月6日月曜日

開発環境

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

コード

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)


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)


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.dot(other)
        return super().__mul__(other)

canvas_test.py

#!/usr/bin/env python3
from unittest import TestCase, main
from canvas import Canvas
from tuples import Color


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

    def tearDown(self):
        pass

    def test_canvas(self):
        c = Canvas(10, 20)
        self.assertEqual(c.width, 10)
        self.assertEqual(c.height, 20)
        for row in c.pixel:
            for col in row:
                self.assertEqual(col, Color(0, 0, 0))

    def test_write_pixel(self):
        c = Canvas(10, 20)
        red = Color(1, 0, 0)
        c.write_pixel(2, 3, red)
        self.assertEqual(c.pixel_at(2, 3), red)

#     def test_canvas_to_ppm(self):
#         c = Canvas(5, 3)
#         c1 = Color(1.5, 0, 0)
#         c2 = Color(0, 0.5, 0)
#         c3 = Color(-0.5, 0, 1)
#         c.write_pixel(0, 0, c1)
#         c.write_pixel(2, 1, c2)
#         c.write_pixel(4, 2, c3)
#         ppm = c.to_ppm()
#         self.assertEqual(ppm, '''P3
# 5 3
# 255
# 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0
# 0 0 0 0 0 0 0 128 0 0 0 0 0 0 0
# 0 0 0 0 0 0 0 0 0 0 0 0 0 0 255
# ''')
    def test_canvas_to_ppm(self):
        c = Canvas(10, 2)
        color = Color(1, 0.8, 0.6)
        for x in range(c.width):
            for y in range(c.height):
                c.write_pixel(x, y, color)
        ppm = c.to_ppm()
        self.assertEqual(ppm, '''P3
10 2
255
255 204 153 255 204 153 255 204 153 255 204 153 255 204 153 255 204 
153 255 204 153 255 204 153 255 204 153 255 204 153 
255 204 153 255 204 153 255 204 153 255 204 153 255 204 153 255 204 
153 255 204 153 255 204 153 255 204 153 255 204 153 
''')
        self.assertEqual(ppm[-1], '\n')


if __name__ == '__main__':
    main()

canvas.py

from tuples import Color
import math


class Canvas:
    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height
        self.pixel = [[Color(0, 0, 0) for _ in range(width)]
                      for _ in range(height)]

    def write_pixel(self, x: int, y: int, color: Color):
        if 0 <= x < self.width and 0 <= y < self.height:
            self.pixel[y][x] = color

    def pixel_at(self, x: int, y: int):
        return self.pixel[y][x]

    # def to_ppm(self):
    #     ppm = f'P3\n{self.width} {self.height}\n255\n'
    #     for row in self.pixel:
    #         line = ''
    #         for color in row:
    #             for c in [color.red, color.green, color.blue]:
    #                 c = 255 * c
    #                 if c > 255:
    #                     c = 255
    #                 elif c < 0:
    #                     c = 0
    #                 else:
    #                     c = math.ceil(c)
    #                 line += f'{c} '
    #         ppm += line[:-1] + '\n'
    #     return ppm
    def to_ppm(self):
        ppm = f'P3\n{self.width} {self.height}\n255\n'
        for row in self.pixel:
            line = ''
            i = 0
            for color in row:
                for c in [color.red, color.green, color.blue]:
                    c = 255 * c
                    if c > 255:
                        c = 255
                    elif c < 0:
                        c = 0
                    else:
                        c = math.ceil(c)
                    line += f'{c} '
                    i += 1
                    if i == 17:
                        line += '\n'
                        i = 0
            ppm += line + '\n'
        return ppm

sample1.py

#!/usr/bin/env python3
from tuples import Point, Vector, Color
from canvas import Canvas


class Projectile:
    def __init__(self, position: Point, velocity: Vector):
        self.position = position
        self.velocity = velocity


class Environment:
    def __init__(self, gravity: Vector, wind: Vector):
        self.gravity = gravity
        self.wind = wind


def tick(env: Environment, proj: Projectile) -> Projectile:
    position = proj.position + proj.velocity
    velocity = proj.velocity + env.gravity + env.wind
    return Projectile(position, velocity)


if __name__ == '__main__':
    start = Point(0, 1, 0)
    velocity = Vector(1, 1.8, 0).normalize() * 11.25
    proj = Projectile(start, velocity)
    gravity = Vector(0, -0.1, 0)
    wind = Vector(-0.01, 0, 0)
    env = Environment(gravity, wind)
    width = 900
    height = 500
    canvas = Canvas(width, height)
    color = Color(1, 0, 0)
    while proj.position.y > 0:
        x = int(proj.position.x)
        y = int(proj.position.y)
        canvas.write_pixel(x, height - y, color)
        proj = tick(env, proj)

    with open('sample1.ppm', 'w') as f:
        print(canvas.to_ppm(), file=f, end='')

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

$ ./tuples_test.py
..................
----------------------------------------------------------------------
Ran 18 tests in 0.001s

OK
$ ./canvas_test.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.002s

OK
$
$ ./sample1.py
$ convert sample1.ppm sample1.png
$ 

0 コメント:

コメントを投稿