## 2019年5月6日月曜日

### Python - Drawing on a Canvas - Color, Canvas(PPM Format)、trajecotry, projectile

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)

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)

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)

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='')
```

```\$ ./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
\$
```