Download src/mesh3d.py from ayzeksalimli/hand-tracking-drawing: direct link, hf CLI and curl.
- Browser
- Download file 6.12 kB
-
https://huggingface.co/ayzeksalimli/hand-tracking-drawing/resolve/main/src/mesh3d.py
- Command line
-
hf download hf://ayzeksalimli/hand-tracking-drawing/src/mesh3d.py
-
curl -L -o mesh3d.py https://huggingface.co/ayzeksalimli/hand-tracking-drawing/resolve/main/src/mesh3d.py
6.12 kB
| """Triangle rasterizer for skinned character meshes. | |
| Every per-triangle quantity — projection, culling, lighting, colour — is | |
| computed for the whole mesh in one batched numpy pass, leaving the inner loop | |
| with nothing but a single OpenCV polygon fill per triangle. Shading a triangle | |
| pixel by pixel in Python instead costs about thirty times as much, which is the | |
| difference between a few frames per second and a usable one. | |
| Depth is resolved by drawing back to front rather than with a z-buffer: for a | |
| character mesh the sort handles the cases that matter, such as an arm crossing | |
| the chest, and it needs no per-pixel work. | |
| """ | |
| from __future__ import annotations | |
| import cv2 | |
| import numpy as np | |
| _LIGHT = np.array([0.4, -0.6, -0.9]) | |
| _LIGHT /= np.linalg.norm(_LIGHT) | |
| _FILL = np.array([-0.5, 0.2, -0.8]) | |
| _FILL /= np.linalg.norm(_FILL) | |
| _AMBIENT = 0.30 | |
| _FILL_GAIN = 0.22 | |
| _RIM_GAIN = 0.40 | |
| _SPEC_GAIN = 0.28 | |
| _SPEC_POWER = 24.0 | |
| _HALF = _LIGHT + np.array([0.0, 0.0, 1.0]) | |
| _HALF /= np.linalg.norm(_HALF) | |
| def _rotation(yaw: float, pitch: float, roll: float = 0.0) -> np.ndarray: | |
| """Build a yaw/pitch/roll rotation matrix.""" | |
| cy, sy = np.cos(yaw), np.sin(yaw) | |
| cp, sp = np.cos(pitch), np.sin(pitch) | |
| cr, sr = np.cos(roll), np.sin(roll) | |
| ry = np.array([[cy, 0, sy], [0, 1, 0], [-sy, 0, cy]]) | |
| rx = np.array([[1, 0, 0], [0, cp, -sp], [0, sp, cp]]) | |
| rz = np.array([[cr, -sr, 0], [sr, cr, 0], [0, 0, 1]]) | |
| return rz @ ry @ rx | |
| class Part: | |
| """One skinned, drawable mesh piece in world space.""" | |
| def __init__( | |
| self, | |
| positions: np.ndarray, | |
| normals: np.ndarray, | |
| triangles: np.ndarray, | |
| *, | |
| color: tuple[int, int, int] = (185, 180, 175), | |
| vertex_colors: np.ndarray | None = None, | |
| uv: np.ndarray | None = None, | |
| texture: np.ndarray | None = None, | |
| ) -> None: | |
| self.positions = positions | |
| self.normals = normals | |
| self.triangles = triangles | |
| self.color = color | |
| self.vertex_colors = vertex_colors | |
| self.uv = uv | |
| self.texture = texture | |
| class MeshRenderer: | |
| """Painter's-algorithm rasterizer with batched shading.""" | |
| def __init__(self, width: int, height: int) -> None: | |
| self.width = width | |
| self.height = height | |
| def render( | |
| self, | |
| background: np.ndarray, | |
| parts: list[Part], | |
| *, | |
| origin: tuple[float, float], | |
| focal: float, | |
| distance: float, | |
| yaw: float, | |
| pitch: float, | |
| roll: float = 0.0, | |
| ) -> np.ndarray: | |
| """Draw skinned parts over the background.""" | |
| out = background if background.flags.writeable else background.copy() | |
| h, w = out.shape[:2] | |
| rot = _rotation(yaw, pitch, roll) | |
| cx, cy = origin | |
| for part in parts: | |
| tris = part.triangles | |
| if part.positions.shape[0] == 0 or tris.shape[0] == 0: | |
| continue | |
| cam = part.positions @ rot.T | |
| z = cam[:, 2] + distance | |
| safe_z = np.maximum(z, 1e-3) | |
| screen = np.empty((len(cam), 2)) | |
| screen[:, 0] = focal * cam[:, 0] / safe_z + cx | |
| screen[:, 1] = -focal * cam[:, 1] / safe_z + cy | |
| pts = screen[tris] | |
| depth = z[tris].mean(axis=1) | |
| # Clockwise on screen means the face turns away from the camera. | |
| winding = ((pts[:, 1, 0] - pts[:, 0, 0]) * (pts[:, 2, 1] - pts[:, 0, 1]) | |
| - (pts[:, 2, 0] - pts[:, 0, 0]) * (pts[:, 1, 1] - pts[:, 0, 1])) | |
| lo = pts.min(axis=1) | |
| hi = pts.max(axis=1) | |
| visible = ( | |
| (winding < 0) & (depth > 0) & np.isfinite(depth) | |
| & (hi[:, 0] >= 0) & (lo[:, 0] < w) & (hi[:, 1] >= 0) & (lo[:, 1] < h) | |
| # Sub-pixel triangles cannot show anything the neighbours miss. | |
| & ((hi[:, 0] - lo[:, 0]) >= 0.7) & ((hi[:, 1] - lo[:, 1]) >= 0.7) | |
| ) | |
| keep = np.flatnonzero(visible) | |
| if keep.size == 0: | |
| continue | |
| # Sort first, then shade, so colours already come out in draw order. | |
| order = keep[np.argsort(-depth[keep])] | |
| poly = np.round(screen[tris[order]]).astype(np.int32) | |
| palette = self._colors(part, tris[order], cam, rot) | |
| for i in range(len(order)): | |
| cv2.fillConvexPoly(out, poly[i], palette[i], cv2.LINE_8) | |
| return out | |
| def _colors(part: Part, tris: np.ndarray, cam: np.ndarray, | |
| rot: np.ndarray) -> list[tuple[int, int, int]]: | |
| """Flat colour per triangle: unlit base times a light factor.""" | |
| if part.normals.size: | |
| n = (part.normals @ rot.T)[tris].mean(axis=1) | |
| else: | |
| v = cam[tris] | |
| n = np.cross(v[:, 1] - v[:, 0], v[:, 2] - v[:, 0]) | |
| n_len = np.linalg.norm(n, axis=1, keepdims=True) | |
| n = n / np.where(n_len > 1e-9, n_len, 1.0) | |
| n = np.where(n[:, 2:3] > 0, -n, n) | |
| shade = (_AMBIENT | |
| + (1.0 - _AMBIENT) * np.maximum(-(n @ _LIGHT), 0.0) | |
| + _FILL_GAIN * np.maximum(-(n @ _FILL), 0.0) | |
| + _RIM_GAIN * np.maximum(1.0 - np.abs(n[:, 2]), 0.0) ** 2.2 | |
| + _SPEC_GAIN * np.maximum(-(n @ _HALF), 0.0) ** _SPEC_POWER) | |
| if part.texture is not None and part.uv is not None: | |
| tex = part.texture | |
| th, tw = tex.shape[:2] | |
| uv = part.uv[tris].mean(axis=1) | |
| u = np.clip(np.mod(uv[:, 0], 1.0) * (tw - 1), 0, tw - 1).astype(np.int32) | |
| v = np.clip(np.mod(uv[:, 1], 1.0) * (th - 1), 0, th - 1).astype(np.int32) | |
| base = tex[v, u].astype(np.float64) | |
| elif part.vertex_colors is not None: | |
| base = part.vertex_colors[tris].mean(axis=1) | |
| else: | |
| base = np.broadcast_to(np.asarray(part.color, dtype=np.float64), (len(tris), 3)) | |
| lit = np.clip(base * shade[:, None], 0, 255).astype(np.uint8) | |
| # OpenCV wants plain ints; one tolist beats per-triangle conversion. | |
| return [tuple(c) for c in lit.tolist()] | |