-
Notifications
You must be signed in to change notification settings - Fork 2
/
renderer.py
163 lines (130 loc) · 4.9 KB
/
renderer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import numpy as np
from PIL import Image
class Object3D:
def __init__(self, points, triangles, colors):
self.points = points
self.triangles = triangles
self.colors = colors
class Renderer:
def __init__(
self,
objects,
viewport,
resolution,
camera_angle=0.0,
camera_pitch=0.0,
):
self._objects = objects
self._viewport = viewport
self._resolution = resolution
self._camera_angle = camera_angle
self._camera_pitch = camera_pitch
resolution_x, resolution_y = self._resolution
self._screen = np.ones((resolution_y, resolution_x, 3), "uint8") * 120
self._z_buffer = np.ones((resolution_y, resolution_x)) * -np.inf
x_min, y_min, x_max, y_max = self._viewport
self._range_x = np.linspace(x_min, x_max, resolution_x)
self._range_y = np.linspace(y_max, y_min, resolution_y)
self._camera_dir = np.array([0, 0, -1])
self._light_dir = _normalize_vector(np.array([1, -1, -1]))
def render_scene(self, output_path):
for object_3d in self._objects:
self._render_object(object_3d)
im = Image.fromarray(self._screen)
im.save(output_path)
def _render_object(self, object_3d):
projected_points = self._get_object_projected_points(object_3d)
projected_triangles = projected_points[object_3d.triangles]
visible_mask = self._get_screen_dot_products(projected_triangles) < 0
if not any(visible_mask):
return
for triangle_points, color in zip(
projected_triangles[visible_mask], object_3d.colors[visible_mask]
):
self._render_triangle(triangle_points, color)
def _get_screen_dot_products(self, triangles):
normals = np.cross(
(triangles[:, 0] - triangles[:, 1]),
(triangles[:, 2] - triangles[:, 1]),
)
return (self._camera_dir * normals).sum(axis=1)
def _get_object_projected_points(self, object_3d):
return (
object_3d.points
@ _get_z_rotation_matrix(self._camera_angle)
@ _get_x_rotation_matrix(self._camera_pitch)
)
def _render_triangle(self, points, color):
bounding_box = _get_bounding_box(points)
try:
delta_z = _calculate_delta_z(points)
except np.linalg.LinAlgError:
return
shaded_color = self._calculate_color(points, color)
for screen_x, scene_x in enumerate(self._range_x):
if scene_x < bounding_box[0, 0] or scene_x > bounding_box[1, 0]:
continue
for screen_y, scene_y in enumerate(self._range_y):
if scene_y < bounding_box[0, 1] or scene_y > bounding_box[1, 1]:
continue
if not _point_in_triangle(np.array([scene_x, scene_y, 0]), points):
continue
depth = (
points[0][2]
+ (np.array([scene_x, scene_y]) - points[0][:2]) @ delta_z
)
if depth <= self._z_buffer[screen_y, screen_x]:
continue
self._screen[screen_y, screen_x, :] = shaded_color
self._z_buffer[screen_y, screen_x] = depth
def _calculate_color(self, points, color):
v_ba = points[0] - points[1]
v_bc = points[2] - points[1]
surface_unit_vector = _normalize_vector(np.cross(v_ba, v_bc))
factor = 1 - (np.dot(self._light_dir, surface_unit_vector) + 1) / 2
r, g, b = color
r = int(factor * r)
g = int(factor * g)
b = int(factor * b)
return np.array([r, g, b], "uint8")
def _sign(p1, p2, p3):
return (p1[0] - p3[0]) * (p2[1] - (p3[1])) - (p2[0] - p3[0]) * (p1[1] - p3[1])
def _point_in_triangle(p, triangle):
a, b, c = triangle
d1 = _sign(p, a, b)
d2 = _sign(p, b, c)
d3 = _sign(p, c, a)
has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0)
has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0)
return not (has_neg and has_pos)
def _get_bounding_box(points):
return np.array(
[
[np.min(points[:, 0]), np.min(points[:, 1])],
[np.max(points[:, 0]), np.max(points[:, 1])],
]
)
def _get_x_rotation_matrix(angle):
return np.array(
[
[1, 0, 0],
[0, np.cos(angle), -np.sin(angle)],
[0, np.sin(angle), np.cos(angle)],
]
)
def _get_z_rotation_matrix(angle):
return np.array(
[
[np.cos(angle), -np.sin(angle), 0],
[np.sin(angle), np.cos(angle), 0],
[0, 0, 1],
]
)
def _calculate_delta_z(points):
v_ab = points[1] - points[0]
v_ac = points[2] - points[0]
slope = np.array([v_ab[:2], v_ac[:2]])
zs = np.array([v_ab[2], v_ac[2]])
return np.linalg.solve(slope, zs)
def _normalize_vector(p):
return 1 / np.sqrt((p**2).sum()) * p