開発環境
- macOS Mojave - Apple (OS)
- Emacs (Text Editor)
- Windows 10 Pro (OS)
- Visual Studio Code (Text Editor)
- Python 3.7 (プログラミング言語)
- GIMP (ビットマップ画像編集・加工ソフトウェア、PPM形式(portable pixmap)の画像用)
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 コメント:
コメントを投稿