Spaces:
Running
Running
| import typing as t | |
| from functools import partial | |
| import numpy as np | |
| from copy import deepcopy | |
| from .canvas import Canvas | |
| from . import speedup | |
| # 2D part | |
| class Vec2d: | |
| __slots__ = "x", "y", "arr" | |
| def __init__(self, *args): | |
| if len(args) == 1 and isinstance(args[0], Vec3d): | |
| self.arr = Vec3d.narr | |
| else: | |
| assert len(args) == 2 | |
| self.arr = list(args) | |
| self.x, self.y = [d if isinstance(d, int) else int(d + 0.5) for d in self.arr] | |
| def __repr__(self): | |
| return f"Vec2d({self.x}, {self.y})" | |
| def __truediv__(self, other): | |
| return (self.y - other.y) / (self.x - other.x) | |
| def __eq__(self, other): | |
| return self.x == other.x and self.y == other.y | |
| def draw_line( | |
| v1: Vec2d, v2: Vec2d, canvas: Canvas, color: t.Union[tuple, str] = "white" | |
| ): | |
| """ | |
| Draw a line with a specified color | |
| https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm | |
| """ | |
| v1, v2 = deepcopy(v1), deepcopy(v2) | |
| if v1 == v2: | |
| canvas.draw((v1.x, v1.y), color=color) | |
| return | |
| steep = abs(v1.y - v2.y) > abs(v1.x - v2.x) | |
| if steep: | |
| v1.x, v1.y = v1.y, v1.x | |
| v2.x, v2.y = v2.y, v2.x | |
| v1, v2 = (v1, v2) if v1.x < v2.x else (v2, v1) | |
| slope = abs((v1.y - v2.y) / (v1.x - v2.x)) | |
| y = v1.y | |
| error: float = 0 | |
| incr = 1 if v1.y < v2.y else -1 | |
| dots = [] | |
| for x in range(int(v1.x), int(v2.x + 0.5)): | |
| dots.append((int(y), x) if steep else (x, int(y))) | |
| error += slope | |
| if abs(error) >= 0.5: | |
| y += incr | |
| error -= 1 | |
| canvas.draw(dots, color=color) | |
| def draw_triangle(v1, v2, v3, canvas, color, wireframe=False): | |
| """ | |
| Draw a triangle with 3 ordered vertices | |
| http://www.sunshine2k.de/coding/java/TriangleRasterization/TriangleRasterization.html | |
| """ | |
| _draw_line = partial(draw_line, canvas=canvas, color=color) | |
| if wireframe: | |
| _draw_line(v1, v2) | |
| _draw_line(v2, v3) | |
| _draw_line(v1, v3) | |
| return | |
| def sort_vertices_asc_by_y(vertices): | |
| return sorted(vertices, key=lambda v: v.y) | |
| def fill_bottom_flat_triangle(v1, v2, v3): | |
| invslope1 = (v2.x - v1.x) / (v2.y - v1.y) | |
| invslope2 = (v3.x - v1.x) / (v3.y - v1.y) | |
| x1 = x2 = v1.x | |
| y = v1.y | |
| while y <= v2.y: | |
| _draw_line(Vec2d(x1, y), Vec2d(x2, y)) | |
| x1 += invslope1 | |
| x2 += invslope2 | |
| y += 1 | |
| def fill_top_flat_triangle(v1, v2, v3): | |
| invslope1 = (v3.x - v1.x) / (v3.y - v1.y) | |
| invslope2 = (v3.x - v2.x) / (v3.y - v2.y) | |
| x1 = x2 = v3.x | |
| y = v3.y | |
| while y > v2.y: | |
| _draw_line(Vec2d(x1, y), Vec2d(x2, y)) | |
| x1 -= invslope1 | |
| x2 -= invslope2 | |
| y -= 1 | |
| v1, v2, v3 = sort_vertices_asc_by_y((v1, v2, v3)) | |
| # 填充 | |
| if v1.y == v2.y == v3.y: | |
| pass | |
| elif v2.y == v3.y: | |
| fill_bottom_flat_triangle(v1, v2, v3) | |
| elif v1.y == v2.y: | |
| fill_top_flat_triangle(v1, v2, v3) | |
| else: | |
| v4 = Vec2d(int(v1.x + (v2.y - v1.y) / (v3.y - v1.y) * (v3.x - v1.x)), v2.y) | |
| fill_bottom_flat_triangle(v1, v2, v4) | |
| fill_top_flat_triangle(v2, v4, v3) | |
| # 3D part | |
| class Vec3d: | |
| __slots__ = "x", "y", "z", "arr" | |
| def __init__(self, *args): | |
| # for Vec4d cast | |
| if len(args) == 1 and isinstance(args[0], Vec4d): | |
| vec4 = args[0] | |
| arr_value = (vec4.x, vec4.y, vec4.z) | |
| else: | |
| assert len(args) == 3 | |
| arr_value = args | |
| self.arr = np.array(arr_value, dtype=np.float64) | |
| self.x, self.y, self.z = self.arr | |
| def __repr__(self): | |
| return repr(f"Vec3d({','.join([repr(d) for d in self.arr])})") | |
| def __sub__(self, other): | |
| return self.__class__(*[ds - do for ds, do in zip(self.arr, other.arr)]) | |
| def __bool__(self): | |
| """ False for zero vector (0, 0, 0) | |
| """ | |
| return any(self.arr) | |
| class Mat4d: | |
| def __init__(self, narr=None, value=None): | |
| self.value = np.matrix(narr) if value is None else value | |
| def __repr__(self): | |
| return repr(self.value) | |
| def __mul__(self, other): | |
| return self.__class__(value=self.value * other.value) | |
| class Vec4d(Mat4d): | |
| def __init__(self, *narr, value=None): | |
| if value is not None: | |
| self.value = value | |
| elif len(narr) == 1 and isinstance(narr[0], Mat4d): | |
| self.value = narr[0].value | |
| else: | |
| assert len(narr) == 4 | |
| self.value = np.matrix([[d] for d in narr]) | |
| self.x, self.y, self.z, self.w = ( | |
| self.value[0, 0], | |
| self.value[1, 0], | |
| self.value[2, 0], | |
| self.value[3, 0], | |
| ) | |
| self.arr = self.value.reshape((1, 4)) | |
| # Math util | |
| def normalize(v: Vec3d): | |
| return Vec3d(*speedup.normalize(*v.arr)) | |
| def dot_product(a: Vec3d, b: Vec3d): | |
| return speedup.dot_product(*a.arr, *b.arr) | |
| def cross_product(a: Vec3d, b: Vec3d): | |
| return Vec3d(*speedup.cross_product(*a.arr, *b.arr)) | |
| BASE_LIGHT = 0.9 | |
| def get_light_intensity(face) -> float: | |
| # lights = [Vec3d(-2, 4, -10), Vec3d(10, 4, -2), Vec3d(8, 8, -8), Vec3d(0, 0, -8)] | |
| lights = [Vec3d(-2, 4, -10)] | |
| # lights = [] | |
| v1, v2, v3 = face | |
| up = normalize(cross_product(v2 - v1, v3 - v1)) | |
| intensity = BASE_LIGHT | |
| for light in lights: | |
| intensity += dot_product(up, normalize(light))*0.2 | |
| return intensity | |
| def look_at(eye: Vec3d, target: Vec3d, up: Vec3d = Vec3d(0, -1, 0)) -> Mat4d: | |
| """ | |
| http://www.songho.ca/opengl/gl_camera.html#lookat | |
| Args: | |
| eye: 摄像机的世界坐标位置 | |
| target: 观察点的位置 | |
| up: 就是你想让摄像机立在哪个方向 | |
| https://stackoverflow.com/questions/10635947/what-exactly-is-the-up-vector-in-opengls-lookat-function | |
| 这里默认使用了 0, -1, 0, 因为 blender 导出来的模型数据似乎有问题,导致y轴总是反的,于是把摄像机的up也翻一下得了。 | |
| """ | |
| f = normalize(eye - target) | |
| l = normalize(cross_product(up, f)) # noqa: E741 | |
| u = cross_product(f, l) | |
| rotate_matrix = Mat4d( | |
| [[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]] | |
| ) | |
| translate_matrix = Mat4d( | |
| [[1, 0, 0, -eye.x], [0, 1, 0, -eye.y], [0, 0, 1, -eye.z], [0, 0, 0, 1.0]] | |
| ) | |
| return Mat4d(value=(rotate_matrix * translate_matrix).value) | |
| def perspective_project(r, t, n, f, b=None, l=None): # noqa: E741 | |
| """ | |
| 目的: | |
| 把相机坐标转换成投影在视网膜的范围在(-1, 1)的笛卡尔坐标 | |
| 原理: | |
| 对于x,y坐标,相似三角形可以算出投影点的x,y | |
| 对于z坐标,是假设了near是-1,far是1,然后带进去算的 | |
| http://www.songho.ca/opengl/gl_projectionmatrix.html | |
| https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/opengl-perspective-projection-matrix | |
| 推导出来的矩阵: | |
| [ | |
| 2n/(r-l) 0 (r+l/r-l) 0 | |
| 0 2n/(t-b) (t+b)/(t-b) 0 | |
| 0 0 -(f+n)/f-n (-2*f*n)/(f-n) | |
| 0 0 -1 0 | |
| ] | |
| 实际上由于我们用的视网膜(near pane)是个关于远点对称的矩形,所以矩阵简化为: | |
| [ | |
| n/r 0 0 0 | |
| 0 n/t 0 0 | |
| 0 0 -(f+n)/f-n (-2*f*n)/(f-n) | |
| 0 0 -1 0 | |
| ] | |
| Args: | |
| r: right, t: top, n: near, f: far, b: bottom, l: left | |
| """ | |
| return Mat4d( | |
| [ | |
| [n / r, 0, 0, 0], | |
| [0, n / t, 0, 0], | |
| [0, 0, -(f + n) / (f - n), (-2 * f * n) / (f - n)], | |
| [0, 0, -1, 0], | |
| ] | |
| ) | |
| def draw(screen_vertices, world_vertices, model, canvas, wireframe=True): | |
| """standard algorithm | |
| """ | |
| for triangle_indices in model.indices: | |
| vertex_group = [screen_vertices[idx - 1] for idx in triangle_indices] | |
| face = [Vec3d(world_vertices[idx - 1]) for idx in triangle_indices] | |
| if wireframe: | |
| draw_triangle(*vertex_group, canvas=canvas, color="black", wireframe=True) | |
| else: | |
| intensity = get_light_intensity(face) | |
| if intensity > 0: | |
| draw_triangle( | |
| *vertex_group, canvas=canvas, color=(int(intensity * 255),) * 3 | |
| ) | |
| def draw_with_z_buffer(screen_vertices, world_vertices, model, canvas): | |
| """ z-buffer algorithm | |
| """ | |
| intensities = [] | |
| triangles = [] | |
| for i, triangle_indices in enumerate(model.indices): | |
| screen_triangle = [screen_vertices[idx - 1] for idx in triangle_indices] | |
| uv_triangle = [model.uv_vertices[idx - 1] for idx in model.uv_indices[i]] | |
| world_triangle = [Vec3d(world_vertices[idx - 1]) for idx in triangle_indices] | |
| intensities.append(abs(get_light_intensity(world_triangle))) | |
| # take off the class to let Cython work | |
| triangles.append( | |
| [np.append(screen_triangle[i].arr, uv_triangle[i]) for i in range(3)] | |
| ) | |
| faces = speedup.generate_faces( | |
| np.array(triangles, dtype=np.float64), model.texture_width, model.texture_height | |
| ) | |
| for face_dots in faces: | |
| for dot in face_dots: | |
| intensity = intensities[dot[0]] | |
| u, v = dot[3], dot[4] | |
| color = model.texture_array[u, v] | |
| canvas.draw((dot[1], dot[2]), tuple(int(c * intensity) for c in color[:3])) | |
| # TODO: add object rendering mode (no texture) | |
| # canvas.draw((dot[1], dot[2]), (int(255 * intensity),) * 3) | |
| def render(model, height, width, filename, cam_loc, wireframe=False): | |
| """ | |
| Args: | |
| model: the Model object | |
| height: cavas height | |
| width: cavas width | |
| picname: picture file name | |
| """ | |
| model_matrix = Mat4d([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) | |
| # TODO: camera configration | |
| view_matrix = look_at(Vec3d(cam_loc[0], cam_loc[1], cam_loc[2]), Vec3d(0, 0, 0)) | |
| projection_matrix = perspective_project(0.5, 0.5, 3, 1000) | |
| world_vertices = [] | |
| def mvp(v): | |
| world_vertex = model_matrix * v | |
| world_vertices.append(Vec4d(world_vertex)) | |
| return projection_matrix * view_matrix * world_vertex | |
| def ndc(v): | |
| """ | |
| 各个坐标同时除以 w,得到 NDC 坐标 | |
| """ | |
| v = v.value | |
| w = v[3, 0] | |
| x, y, z = v[0, 0] / w, v[1, 0] / w, v[2, 0] / w | |
| return Mat4d([[x], [y], [z], [1 / w]]) | |
| def viewport(v): | |
| x = y = 0 | |
| w, h = width, height | |
| n, f = 0.3, 1000 | |
| return Vec3d( | |
| w * 0.5 * v.value[0, 0] + x + w * 0.5, | |
| h * 0.5 * v.value[1, 0] + y + h * 0.5, | |
| 0.5 * (f - n) * v.value[2, 0] + 0.5 * (f + n), | |
| ) | |
| # the render pipeline | |
| screen_vertices = [viewport(ndc(mvp(v))) for v in model.vertices] | |
| with Canvas(filename, height, width) as canvas: | |
| if wireframe: | |
| draw(screen_vertices, world_vertices, model, canvas) | |
| else: | |
| draw_with_z_buffer(screen_vertices, world_vertices, model, canvas) | |
| render_img = canvas.add_white_border().copy() | |
| return render_img |