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
|