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 |