zhang-ziang commited on
Commit
2d48693
1 Parent(s): 738bdfa

render engine

Browse files
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