| """Mesh simplification for the software rasterizer. |
| |
| The rasterizer costs roughly a fixed amount of Python work per triangle, so a |
| film-quality character mesh has to be thinned before it can animate in real |
| time. Vertex clustering is used rather than edge-collapse: it is a few lines, |
| runs once at load, and carries skinning weights and UVs along untouched, which |
| matters because these meshes are posed every frame. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import numpy as np |
|
|
| import gltf |
|
|
| |
| _UV_CELLS = 16 |
|
|
|
|
| def _weld(prim: gltf.Primitive, cells: int) -> gltf.Primitive: |
| """Snap vertices to a grid of `cells` per axis and drop collapsed triangles.""" |
| pos = prim.positions |
| lo = pos.min(axis=0) |
| span = np.maximum(pos.max(axis=0) - lo, 1e-9) |
| |
| grid = np.floor((pos - lo) / span * cells).astype(np.int64) |
| grid = np.clip(grid, 0, cells - 1) |
| keys = (grid[:, 0] * cells + grid[:, 1]) * cells + grid[:, 2] |
| |
| |
| dominant = prim.joints[np.arange(len(prim.joints)), np.argmax(prim.weights, axis=1)] |
| keys = keys * (int(dominant.max()) + 1) + dominant.astype(np.int64) |
| if prim.uv is not None: |
| |
| |
| |
| uv_cell = np.clip(np.floor(np.mod(prim.uv, 1.0) * _UV_CELLS), 0, _UV_CELLS - 1) |
| uv_key = uv_cell[:, 0].astype(np.int64) * _UV_CELLS + uv_cell[:, 1].astype(np.int64) |
| keys = keys * (_UV_CELLS * _UV_CELLS) + uv_key |
| _, first, inverse = np.unique(keys, return_index=True, return_inverse=True) |
|
|
| tris = inverse[prim.triangles] |
| |
| keep = (tris[:, 0] != tris[:, 1]) & (tris[:, 1] != tris[:, 2]) & (tris[:, 0] != tris[:, 2]) |
| tris = tris[keep] |
| if tris.shape[0] == 0: |
| return prim |
|
|
| |
| |
| merged_pos = pos[first] |
| merged_nrm = (prim.normals[first] if prim.normals is not None and prim.normals.size |
| else np.zeros((len(first), 3))) |
|
|
| |
| |
| |
| merged_uv = None if prim.uv is None else prim.uv[first] |
|
|
| return gltf.Primitive( |
| positions=merged_pos, |
| normals=merged_nrm, |
| |
| |
| joints=prim.joints[first], |
| weights=prim.weights[first], |
| triangles=tris, |
| color=prim.color, |
| uv=merged_uv, |
| texture=prim.texture, |
| ) |
|
|
|
|
| def to_budget(primitives: list[gltf.Primitive], budget: int) -> list[gltf.Primitive]: |
| """Thin primitives until their combined triangle count fits the budget.""" |
| total = sum(len(p.triangles) for p in primitives) |
| if total <= budget: |
| return primitives |
|
|
| out = list(primitives) |
| |
| |
| for cells in (128, 96, 72, 56, 44, 34, 26, 20, 16, 12): |
| candidate = [_weld(p, cells) for p in primitives] |
| if sum(len(p.triangles) for p in candidate) <= budget: |
| return candidate |
| out = candidate |
| return out |
|
|