Spaces:
Running
Running
zhang-ziang
commited on
Commit
•
2d48693
1
Parent(s):
738bdfa
render engine
Browse files- render/__init__.py +3 -0
- render/__pycache__/__init__.cpython-312.pyc +0 -0
- render/__pycache__/canvas.cpython-312.pyc +0 -0
- render/__pycache__/core.cpython-312.pyc +0 -0
- render/__pycache__/model.cpython-312.pyc +0 -0
- render/__pycache__/speedup.cpython-312.pyc +0 -0
- render/canvas.py +49 -0
- render/core.py +366 -0
- render/model.py +31 -0
- render/speedup.py +101 -0
render/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# flake8: noqa
|
2 |
+
from .core import render
|
3 |
+
from .model import Model
|
render/__pycache__/__init__.cpython-312.pyc
ADDED
Binary file (207 Bytes). View file
|
|
render/__pycache__/canvas.cpython-312.pyc
ADDED
Binary file (2.9 kB). View file
|
|
render/__pycache__/core.cpython-312.pyc
ADDED
Binary file (20.6 kB). View file
|
|
render/__pycache__/model.cpython-312.pyc
ADDED
Binary file (2.7 kB). View file
|
|
render/__pycache__/speedup.cpython-312.pyc
ADDED
Binary file (4.6 kB). View file
|
|
render/canvas.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import typing as t
|
2 |
+
|
3 |
+
from PIL import Image, ImageColor, ImageOps, ImageChops, ImageFilter
|
4 |
+
import numpy as np
|
5 |
+
|
6 |
+
class Canvas:
|
7 |
+
def __init__(self, filename=None, height=500, width=500):
|
8 |
+
self.filename = filename
|
9 |
+
self.height, self.width = height, width
|
10 |
+
self.img = Image.new("RGBA", (self.height, self.width), (0, 0, 0, 0))
|
11 |
+
|
12 |
+
def draw(self, dots, color: t.Union[tuple, str]):
|
13 |
+
if isinstance(color, str):
|
14 |
+
color = ImageColor.getrgb(color)
|
15 |
+
if isinstance(dots, tuple):
|
16 |
+
dots = [dots]
|
17 |
+
for dot in dots:
|
18 |
+
if dot[0]>=self.height or dot[1]>=self.width or dot[0]<0 or dot[1]<0:
|
19 |
+
# print(dot)
|
20 |
+
continue
|
21 |
+
self.img.putpixel(dot, color + (255,))
|
22 |
+
|
23 |
+
def add_white_border(self, border_size=5):
|
24 |
+
# 确保输入图像是 RGBA 模式
|
25 |
+
if self.img.mode != "RGBA":
|
26 |
+
self.img = self.img.convert("RGBA")
|
27 |
+
|
28 |
+
# 提取 alpha 通道
|
29 |
+
alpha = self.img.getchannel("A")
|
30 |
+
# print(alpha.size)
|
31 |
+
dilated_alpha = alpha.filter(ImageFilter.MaxFilter(size=5))
|
32 |
+
# # print(dilated_alpha.size)
|
33 |
+
white_area = Image.new("RGBA", self.img.size, (255, 255, 255, 255))
|
34 |
+
white_area.putalpha(dilated_alpha)
|
35 |
+
|
36 |
+
# 合并膨胀后的白色区域与原图像
|
37 |
+
result = Image.alpha_composite(white_area, self.img)
|
38 |
+
# expanded_alpha = ImageOps.expand(alpha, border=border_size, fill=255)
|
39 |
+
# white_border = Image.new("RGBA", image.size, (255, 255, 255, 255))
|
40 |
+
# white_border.putalpha(alpha)
|
41 |
+
return result
|
42 |
+
|
43 |
+
def __enter__(self):
|
44 |
+
return self
|
45 |
+
|
46 |
+
def __exit__(self, type, value, traceback):
|
47 |
+
# self.img = add_white_border(self.img)
|
48 |
+
self.img.save(self.filename)
|
49 |
+
pass
|
render/core.py
ADDED
@@ -0,0 +1,366 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import typing as t
|
2 |
+
from functools import partial
|
3 |
+
|
4 |
+
import numpy as np
|
5 |
+
from copy import deepcopy
|
6 |
+
from .canvas import Canvas
|
7 |
+
|
8 |
+
from . import speedup
|
9 |
+
|
10 |
+
|
11 |
+
# 2D part
|
12 |
+
|
13 |
+
|
14 |
+
class Vec2d:
|
15 |
+
__slots__ = "x", "y", "arr"
|
16 |
+
|
17 |
+
def __init__(self, *args):
|
18 |
+
if len(args) == 1 and isinstance(args[0], Vec3d):
|
19 |
+
self.arr = Vec3d.narr
|
20 |
+
else:
|
21 |
+
assert len(args) == 2
|
22 |
+
self.arr = list(args)
|
23 |
+
|
24 |
+
self.x, self.y = [d if isinstance(d, int) else int(d + 0.5) for d in self.arr]
|
25 |
+
|
26 |
+
def __repr__(self):
|
27 |
+
return f"Vec2d({self.x}, {self.y})"
|
28 |
+
|
29 |
+
def __truediv__(self, other):
|
30 |
+
return (self.y - other.y) / (self.x - other.x)
|
31 |
+
|
32 |
+
def __eq__(self, other):
|
33 |
+
return self.x == other.x and self.y == other.y
|
34 |
+
|
35 |
+
|
36 |
+
def draw_line(
|
37 |
+
v1: Vec2d, v2: Vec2d, canvas: Canvas, color: t.Union[tuple, str] = "white"
|
38 |
+
):
|
39 |
+
"""
|
40 |
+
Draw a line with a specified color
|
41 |
+
|
42 |
+
https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
|
43 |
+
"""
|
44 |
+
v1, v2 = deepcopy(v1), deepcopy(v2)
|
45 |
+
if v1 == v2:
|
46 |
+
canvas.draw((v1.x, v1.y), color=color)
|
47 |
+
return
|
48 |
+
|
49 |
+
steep = abs(v1.y - v2.y) > abs(v1.x - v2.x)
|
50 |
+
if steep:
|
51 |
+
v1.x, v1.y = v1.y, v1.x
|
52 |
+
v2.x, v2.y = v2.y, v2.x
|
53 |
+
v1, v2 = (v1, v2) if v1.x < v2.x else (v2, v1)
|
54 |
+
slope = abs((v1.y - v2.y) / (v1.x - v2.x))
|
55 |
+
y = v1.y
|
56 |
+
error: float = 0
|
57 |
+
incr = 1 if v1.y < v2.y else -1
|
58 |
+
dots = []
|
59 |
+
for x in range(int(v1.x), int(v2.x + 0.5)):
|
60 |
+
dots.append((int(y), x) if steep else (x, int(y)))
|
61 |
+
error += slope
|
62 |
+
if abs(error) >= 0.5:
|
63 |
+
y += incr
|
64 |
+
error -= 1
|
65 |
+
|
66 |
+
canvas.draw(dots, color=color)
|
67 |
+
|
68 |
+
|
69 |
+
def draw_triangle(v1, v2, v3, canvas, color, wireframe=False):
|
70 |
+
"""
|
71 |
+
Draw a triangle with 3 ordered vertices
|
72 |
+
|
73 |
+
http://www.sunshine2k.de/coding/java/TriangleRasterization/TriangleRasterization.html
|
74 |
+
"""
|
75 |
+
_draw_line = partial(draw_line, canvas=canvas, color=color)
|
76 |
+
|
77 |
+
if wireframe:
|
78 |
+
_draw_line(v1, v2)
|
79 |
+
_draw_line(v2, v3)
|
80 |
+
_draw_line(v1, v3)
|
81 |
+
return
|
82 |
+
|
83 |
+
def sort_vertices_asc_by_y(vertices):
|
84 |
+
return sorted(vertices, key=lambda v: v.y)
|
85 |
+
|
86 |
+
def fill_bottom_flat_triangle(v1, v2, v3):
|
87 |
+
invslope1 = (v2.x - v1.x) / (v2.y - v1.y)
|
88 |
+
invslope2 = (v3.x - v1.x) / (v3.y - v1.y)
|
89 |
+
|
90 |
+
x1 = x2 = v1.x
|
91 |
+
y = v1.y
|
92 |
+
|
93 |
+
while y <= v2.y:
|
94 |
+
_draw_line(Vec2d(x1, y), Vec2d(x2, y))
|
95 |
+
x1 += invslope1
|
96 |
+
x2 += invslope2
|
97 |
+
y += 1
|
98 |
+
|
99 |
+
def fill_top_flat_triangle(v1, v2, v3):
|
100 |
+
invslope1 = (v3.x - v1.x) / (v3.y - v1.y)
|
101 |
+
invslope2 = (v3.x - v2.x) / (v3.y - v2.y)
|
102 |
+
|
103 |
+
x1 = x2 = v3.x
|
104 |
+
y = v3.y
|
105 |
+
|
106 |
+
while y > v2.y:
|
107 |
+
_draw_line(Vec2d(x1, y), Vec2d(x2, y))
|
108 |
+
x1 -= invslope1
|
109 |
+
x2 -= invslope2
|
110 |
+
y -= 1
|
111 |
+
|
112 |
+
v1, v2, v3 = sort_vertices_asc_by_y((v1, v2, v3))
|
113 |
+
|
114 |
+
# 填充
|
115 |
+
if v1.y == v2.y == v3.y:
|
116 |
+
pass
|
117 |
+
elif v2.y == v3.y:
|
118 |
+
fill_bottom_flat_triangle(v1, v2, v3)
|
119 |
+
elif v1.y == v2.y:
|
120 |
+
fill_top_flat_triangle(v1, v2, v3)
|
121 |
+
else:
|
122 |
+
v4 = Vec2d(int(v1.x + (v2.y - v1.y) / (v3.y - v1.y) * (v3.x - v1.x)), v2.y)
|
123 |
+
fill_bottom_flat_triangle(v1, v2, v4)
|
124 |
+
fill_top_flat_triangle(v2, v4, v3)
|
125 |
+
|
126 |
+
|
127 |
+
# 3D part
|
128 |
+
|
129 |
+
|
130 |
+
class Vec3d:
|
131 |
+
__slots__ = "x", "y", "z", "arr"
|
132 |
+
|
133 |
+
def __init__(self, *args):
|
134 |
+
# for Vec4d cast
|
135 |
+
if len(args) == 1 and isinstance(args[0], Vec4d):
|
136 |
+
vec4 = args[0]
|
137 |
+
arr_value = (vec4.x, vec4.y, vec4.z)
|
138 |
+
else:
|
139 |
+
assert len(args) == 3
|
140 |
+
arr_value = args
|
141 |
+
self.arr = np.array(arr_value, dtype=np.float64)
|
142 |
+
self.x, self.y, self.z = self.arr
|
143 |
+
|
144 |
+
def __repr__(self):
|
145 |
+
return repr(f"Vec3d({','.join([repr(d) for d in self.arr])})")
|
146 |
+
|
147 |
+
def __sub__(self, other):
|
148 |
+
return self.__class__(*[ds - do for ds, do in zip(self.arr, other.arr)])
|
149 |
+
|
150 |
+
def __bool__(self):
|
151 |
+
""" False for zero vector (0, 0, 0)
|
152 |
+
"""
|
153 |
+
return any(self.arr)
|
154 |
+
|
155 |
+
|
156 |
+
class Mat4d:
|
157 |
+
def __init__(self, narr=None, value=None):
|
158 |
+
self.value = np.matrix(narr) if value is None else value
|
159 |
+
|
160 |
+
def __repr__(self):
|
161 |
+
return repr(self.value)
|
162 |
+
|
163 |
+
def __mul__(self, other):
|
164 |
+
return self.__class__(value=self.value * other.value)
|
165 |
+
|
166 |
+
|
167 |
+
class Vec4d(Mat4d):
|
168 |
+
def __init__(self, *narr, value=None):
|
169 |
+
if value is not None:
|
170 |
+
self.value = value
|
171 |
+
elif len(narr) == 1 and isinstance(narr[0], Mat4d):
|
172 |
+
self.value = narr[0].value
|
173 |
+
else:
|
174 |
+
assert len(narr) == 4
|
175 |
+
self.value = np.matrix([[d] for d in narr])
|
176 |
+
|
177 |
+
self.x, self.y, self.z, self.w = (
|
178 |
+
self.value[0, 0],
|
179 |
+
self.value[1, 0],
|
180 |
+
self.value[2, 0],
|
181 |
+
self.value[3, 0],
|
182 |
+
)
|
183 |
+
self.arr = self.value.reshape((1, 4))
|
184 |
+
|
185 |
+
|
186 |
+
# Math util
|
187 |
+
def normalize(v: Vec3d):
|
188 |
+
return Vec3d(*speedup.normalize(*v.arr))
|
189 |
+
|
190 |
+
|
191 |
+
def dot_product(a: Vec3d, b: Vec3d):
|
192 |
+
return speedup.dot_product(*a.arr, *b.arr)
|
193 |
+
|
194 |
+
|
195 |
+
def cross_product(a: Vec3d, b: Vec3d):
|
196 |
+
return Vec3d(*speedup.cross_product(*a.arr, *b.arr))
|
197 |
+
|
198 |
+
BASE_LIGHT = 0.3
|
199 |
+
def get_light_intensity(face) -> float:
|
200 |
+
light0 = Vec3d(-2, 4, -10)
|
201 |
+
|
202 |
+
light1 = Vec3d(10, 4, -2)
|
203 |
+
v1, v2, v3 = face
|
204 |
+
up = normalize(cross_product(v2 - v1, v3 - v1))
|
205 |
+
return dot_product(up, normalize(light0))*0.6 + dot_product(up, normalize(light1))*0.6 + BASE_LIGHT
|
206 |
+
|
207 |
+
|
208 |
+
def look_at(eye: Vec3d, target: Vec3d, up: Vec3d = Vec3d(0, -1, 0)) -> Mat4d:
|
209 |
+
"""
|
210 |
+
http://www.songho.ca/opengl/gl_camera.html#lookat
|
211 |
+
|
212 |
+
Args:
|
213 |
+
eye: 摄像机的世界坐标位置
|
214 |
+
target: 观察点的位置
|
215 |
+
up: 就是你想让摄像机立在哪个方向
|
216 |
+
https://stackoverflow.com/questions/10635947/what-exactly-is-the-up-vector-in-opengls-lookat-function
|
217 |
+
这里默认使用了 0, -1, 0, 因为 blender 导出来的模型数据似乎有问题,导致y轴总是反的,于是把摄像机的up也翻一下得了。
|
218 |
+
"""
|
219 |
+
f = normalize(eye - target)
|
220 |
+
l = normalize(cross_product(up, f)) # noqa: E741
|
221 |
+
u = cross_product(f, l)
|
222 |
+
|
223 |
+
rotate_matrix = Mat4d(
|
224 |
+
[[l.x, l.y, l.z, 0], [u.x, u.y, u.z, 0], [f.x, f.y, f.z, 0], [0, 0, 0, 1.0]]
|
225 |
+
)
|
226 |
+
translate_matrix = Mat4d(
|
227 |
+
[[1, 0, 0, -eye.x], [0, 1, 0, -eye.y], [0, 0, 1, -eye.z], [0, 0, 0, 1.0]]
|
228 |
+
)
|
229 |
+
|
230 |
+
return Mat4d(value=(rotate_matrix * translate_matrix).value)
|
231 |
+
|
232 |
+
|
233 |
+
def perspective_project(r, t, n, f, b=None, l=None): # noqa: E741
|
234 |
+
"""
|
235 |
+
目的:
|
236 |
+
把相机坐标转换成投影在视网膜的范围在(-1, 1)的笛卡尔坐标
|
237 |
+
|
238 |
+
原理:
|
239 |
+
对于x,y坐标,相似三角形可以算出投影点的x,y
|
240 |
+
对于z坐标,是假设了near是-1,far是1,然后带进去算的
|
241 |
+
http://www.songho.ca/opengl/gl_projectionmatrix.html
|
242 |
+
https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/opengl-perspective-projection-matrix
|
243 |
+
|
244 |
+
推导出来的矩阵:
|
245 |
+
[
|
246 |
+
2n/(r-l) 0 (r+l/r-l) 0
|
247 |
+
0 2n/(t-b) (t+b)/(t-b) 0
|
248 |
+
0 0 -(f+n)/f-n (-2*f*n)/(f-n)
|
249 |
+
0 0 -1 0
|
250 |
+
]
|
251 |
+
|
252 |
+
实际上由于我们用的视网膜(near pane)是个关于远点对称的矩形,所以矩阵简化为:
|
253 |
+
[
|
254 |
+
n/r 0 0 0
|
255 |
+
0 n/t 0 0
|
256 |
+
0 0 -(f+n)/f-n (-2*f*n)/(f-n)
|
257 |
+
0 0 -1 0
|
258 |
+
]
|
259 |
+
|
260 |
+
Args:
|
261 |
+
r: right, t: top, n: near, f: far, b: bottom, l: left
|
262 |
+
"""
|
263 |
+
return Mat4d(
|
264 |
+
[
|
265 |
+
[n / r, 0, 0, 0],
|
266 |
+
[0, n / t, 0, 0],
|
267 |
+
[0, 0, -(f + n) / (f - n), (-2 * f * n) / (f - n)],
|
268 |
+
[0, 0, -1, 0],
|
269 |
+
]
|
270 |
+
)
|
271 |
+
|
272 |
+
|
273 |
+
def draw(screen_vertices, world_vertices, model, canvas, wireframe=True):
|
274 |
+
"""standard algorithm
|
275 |
+
"""
|
276 |
+
for triangle_indices in model.indices:
|
277 |
+
vertex_group = [screen_vertices[idx - 1] for idx in triangle_indices]
|
278 |
+
face = [Vec3d(world_vertices[idx - 1]) for idx in triangle_indices]
|
279 |
+
if wireframe:
|
280 |
+
draw_triangle(*vertex_group, canvas=canvas, color="black", wireframe=True)
|
281 |
+
else:
|
282 |
+
intensity = get_light_intensity(face)
|
283 |
+
if intensity > 0:
|
284 |
+
draw_triangle(
|
285 |
+
*vertex_group, canvas=canvas, color=(int(intensity * 255),) * 3
|
286 |
+
)
|
287 |
+
|
288 |
+
|
289 |
+
def draw_with_z_buffer(screen_vertices, world_vertices, model, canvas):
|
290 |
+
""" z-buffer algorithm
|
291 |
+
"""
|
292 |
+
intensities = []
|
293 |
+
triangles = []
|
294 |
+
for i, triangle_indices in enumerate(model.indices):
|
295 |
+
screen_triangle = [screen_vertices[idx - 1] for idx in triangle_indices]
|
296 |
+
uv_triangle = [model.uv_vertices[idx - 1] for idx in model.uv_indices[i]]
|
297 |
+
world_triangle = [Vec3d(world_vertices[idx - 1]) for idx in triangle_indices]
|
298 |
+
intensities.append(abs(get_light_intensity(world_triangle)))
|
299 |
+
# take off the class to let Cython work
|
300 |
+
triangles.append(
|
301 |
+
[np.append(screen_triangle[i].arr, uv_triangle[i]) for i in range(3)]
|
302 |
+
)
|
303 |
+
|
304 |
+
faces = speedup.generate_faces(
|
305 |
+
np.array(triangles, dtype=np.float64), model.texture_width, model.texture_height
|
306 |
+
)
|
307 |
+
for face_dots in faces:
|
308 |
+
for dot in face_dots:
|
309 |
+
intensity = intensities[dot[0]]
|
310 |
+
u, v = dot[3], dot[4]
|
311 |
+
color = model.texture_array[u, v]
|
312 |
+
canvas.draw((dot[1], dot[2]), tuple(int(c * intensity) for c in color[:3]))
|
313 |
+
# TODO: add object rendering mode (no texture)
|
314 |
+
# canvas.draw((dot[1], dot[2]), (int(255 * intensity),) * 3)
|
315 |
+
|
316 |
+
|
317 |
+
def render(model, height, width, filename, cam_loc, wireframe=False):
|
318 |
+
"""
|
319 |
+
Args:
|
320 |
+
model: the Model object
|
321 |
+
height: cavas height
|
322 |
+
width: cavas width
|
323 |
+
picname: picture file name
|
324 |
+
"""
|
325 |
+
model_matrix = Mat4d([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
|
326 |
+
# TODO: camera configration
|
327 |
+
view_matrix = look_at(Vec3d(cam_loc[0], cam_loc[1], cam_loc[2]), Vec3d(0, 0, 0))
|
328 |
+
projection_matrix = perspective_project(0.5, 0.5, 3, 1000)
|
329 |
+
|
330 |
+
world_vertices = []
|
331 |
+
|
332 |
+
def mvp(v):
|
333 |
+
world_vertex = model_matrix * v
|
334 |
+
world_vertices.append(Vec4d(world_vertex))
|
335 |
+
return projection_matrix * view_matrix * world_vertex
|
336 |
+
|
337 |
+
def ndc(v):
|
338 |
+
"""
|
339 |
+
各个坐标同时除以 w,得到 NDC 坐标
|
340 |
+
"""
|
341 |
+
v = v.value
|
342 |
+
w = v[3, 0]
|
343 |
+
x, y, z = v[0, 0] / w, v[1, 0] / w, v[2, 0] / w
|
344 |
+
return Mat4d([[x], [y], [z], [1 / w]])
|
345 |
+
|
346 |
+
def viewport(v):
|
347 |
+
x = y = 0
|
348 |
+
w, h = width, height
|
349 |
+
n, f = 0.3, 1000
|
350 |
+
return Vec3d(
|
351 |
+
w * 0.5 * v.value[0, 0] + x + w * 0.5,
|
352 |
+
h * 0.5 * v.value[1, 0] + y + h * 0.5,
|
353 |
+
0.5 * (f - n) * v.value[2, 0] + 0.5 * (f + n),
|
354 |
+
)
|
355 |
+
|
356 |
+
# the render pipeline
|
357 |
+
screen_vertices = [viewport(ndc(mvp(v))) for v in model.vertices]
|
358 |
+
|
359 |
+
with Canvas(filename, height, width) as canvas:
|
360 |
+
if wireframe:
|
361 |
+
draw(screen_vertices, world_vertices, model, canvas)
|
362 |
+
else:
|
363 |
+
draw_with_z_buffer(screen_vertices, world_vertices, model, canvas)
|
364 |
+
|
365 |
+
render_img = canvas.add_white_border().copy()
|
366 |
+
return render_img
|
render/model.py
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy
|
2 |
+
from PIL import Image
|
3 |
+
from .core import Vec4d
|
4 |
+
|
5 |
+
|
6 |
+
class Model:
|
7 |
+
def __init__(self, filename, texture_filename):
|
8 |
+
"""
|
9 |
+
https://en.wikipedia.org/wiki/Wavefront_.obj_file#Vertex_normal_indices
|
10 |
+
"""
|
11 |
+
self.vertices = []
|
12 |
+
self.uv_vertices = []
|
13 |
+
self.uv_indices = []
|
14 |
+
self.indices = []
|
15 |
+
|
16 |
+
texture = Image.open(texture_filename)
|
17 |
+
self.texture_array = numpy.array(texture)
|
18 |
+
self.texture_width, self.texture_height = texture.size
|
19 |
+
|
20 |
+
with open(filename) as f:
|
21 |
+
for line in f:
|
22 |
+
if line.startswith("v "):
|
23 |
+
x, y, z = [float(d) for d in line.strip("v").strip().split(" ")]
|
24 |
+
self.vertices.append(Vec4d(x, y, z, 1))
|
25 |
+
elif line.startswith("vt "):
|
26 |
+
u, v = [float(d) for d in line.strip("vt").strip().split(" ")]
|
27 |
+
self.uv_vertices.append([u, v])
|
28 |
+
elif line.startswith("f "):
|
29 |
+
facet = [d.split("/") for d in line.strip("f").strip().split(" ")]
|
30 |
+
self.indices.append([int(d[0]) for d in facet])
|
31 |
+
self.uv_indices.append([int(d[1]) for d in facet])
|
render/speedup.py
ADDED
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# import cython
|
2 |
+
import numpy as np
|
3 |
+
from math import sqrt
|
4 |
+
|
5 |
+
|
6 |
+
def normalize(x, y, z):
|
7 |
+
unit = sqrt(x * x + y * y + z * z)
|
8 |
+
if unit == 0:
|
9 |
+
return 0, 0, 0
|
10 |
+
return x / unit, y / unit, z / unit
|
11 |
+
|
12 |
+
|
13 |
+
def get_min_max(a, b, c):
|
14 |
+
min = a
|
15 |
+
max = a
|
16 |
+
if min > b:
|
17 |
+
min = b
|
18 |
+
if min > c:
|
19 |
+
min = c
|
20 |
+
if max < b:
|
21 |
+
max = b
|
22 |
+
if max < c:
|
23 |
+
max = c
|
24 |
+
return int(min), int(max)
|
25 |
+
|
26 |
+
def dot_product(a0, a1, a2, b0, b1, b2):
|
27 |
+
r = a0 * b0 + a1 * b1 + a2 * b2
|
28 |
+
return r
|
29 |
+
|
30 |
+
|
31 |
+
def cross_product(a0, a1, a2, b0, b1, b2):
|
32 |
+
x = a1 * b2 - a2 * b1
|
33 |
+
y = a2 * b0 - a0 * b2
|
34 |
+
z = a0 * b1 - a1 * b0
|
35 |
+
return x,y,z
|
36 |
+
|
37 |
+
|
38 |
+
# @cython.boundscheck(False)
|
39 |
+
def generate_faces(triangles, width, height):
|
40 |
+
""" draw the triangle faces with z buffer
|
41 |
+
|
42 |
+
Args:
|
43 |
+
triangles: groups of vertices
|
44 |
+
|
45 |
+
FYI:
|
46 |
+
* zbuffer, https://github.com/ssloy/tinyrenderer/wiki/Lesson-3:-Hidden-faces-removal-(z-buffer)
|
47 |
+
* uv mapping and perspective correction
|
48 |
+
"""
|
49 |
+
i, j, k, length = 0, 0, 0, 0
|
50 |
+
bcy, bcz, x, y, z = 0.,0.,0.,0.,0.
|
51 |
+
a, b, c = [0.,0.,0.],[0.,0.,0.],[0.,0.,0.]
|
52 |
+
m, bc = [0.,0.,0.],[0.,0.,0.]
|
53 |
+
uva, uvb, uvc = [0.,0.],[0.,0.],[0.,0.]
|
54 |
+
minx, maxx, miny, maxy = 0,0,0,0
|
55 |
+
length = triangles.shape[0]
|
56 |
+
zbuffer = {}
|
57 |
+
faces = []
|
58 |
+
|
59 |
+
for i in range(length):
|
60 |
+
a = triangles[i, 0, 0], triangles[i, 0, 1], triangles[i, 0, 2]
|
61 |
+
b = triangles[i, 1, 0], triangles[i, 1, 1], triangles[i, 1, 2]
|
62 |
+
c = triangles[i, 2, 0], triangles[i, 2, 1], triangles[i, 2, 2]
|
63 |
+
uva = triangles[i, 0, 3], triangles[i, 0, 4]
|
64 |
+
uvb = triangles[i, 1, 3], triangles[i, 1, 4]
|
65 |
+
uvc = triangles[i, 2, 3], triangles[i, 2, 4]
|
66 |
+
minx, maxx = get_min_max(a[0], b[0], c[0])
|
67 |
+
miny, maxy = get_min_max(a[1], b[1], c[1])
|
68 |
+
pixels = []
|
69 |
+
for j in range(minx, maxx + 2):
|
70 |
+
for k in range(miny - 1, maxy + 2):
|
71 |
+
# 必须显式转换成 double 参与底下的运算,不然结果是错的
|
72 |
+
x = j
|
73 |
+
y = k
|
74 |
+
|
75 |
+
m[0], m[1], m[2] = cross_product(c[0] - a[0], b[0] - a[0], a[0] - x, c[1] - a[1], b[1] - a[1], a[1] - y)
|
76 |
+
if abs(m[2]) > 0:
|
77 |
+
bcy = m[1] / m[2]
|
78 |
+
bcz = m[0] / m[2]
|
79 |
+
bc = (1 - bcy - bcz, bcy, bcz)
|
80 |
+
else:
|
81 |
+
continue
|
82 |
+
|
83 |
+
# here, -0.00001 because of the precision lose
|
84 |
+
if bc[0] < -0.00001 or bc[1] < -0.00001 or bc[2] < -0.00001:
|
85 |
+
continue
|
86 |
+
|
87 |
+
z = 1 / (bc[0] / a[2] + bc[1] / b[2] + bc[2] / c[2])
|
88 |
+
|
89 |
+
# Blender 导出来的 uv 数据,跟之前的顶点数据有一样的问题,Y轴是个反的,
|
90 |
+
# 所以这里的纹理图片要旋转一下才能 work
|
91 |
+
v = (uva[0] * bc[0] / a[2] + uvb[0] * bc[1] / b[2] + uvc[0] * bc[2] / c[2]) * z * width
|
92 |
+
u = height - (uva[1] * bc[0] / a[2] + uvb[1] * bc[1] / b[2] + uvc[1] * bc[2] / c[2]) * z * height
|
93 |
+
|
94 |
+
# https://en.wikipedia.org/wiki/Pairing_function
|
95 |
+
idx = ((x + y) * (x + y + 1) + y) / 2
|
96 |
+
if zbuffer.get(idx) is None or zbuffer[idx] < z:
|
97 |
+
zbuffer[idx] = z
|
98 |
+
pixels.append((i, j, k, int(u) - 1, int(v) - 1))
|
99 |
+
|
100 |
+
faces.append(pixels)
|
101 |
+
return faces
|