開発環境
- 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 コメント:
コメントを投稿