zhang-ziang
image post resize and light refine
864becb
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