Vertex-colour baker: 0.2s against the UV path's >20 minutes
The texture stage produced correct PBR voxels, but getting them ONTO a mesh through
o_voxel's UV path is not viable on this build. Measured, on an already welded,
floater-free, decimated 214k-face mesh:
o_voxel to_glb, remesh=True killed at 20min
o_voxel to_glb, remesh=False >20min CPU, killed
bake_vertex_colors 0.2s
xatlas scales badly and 214k is the decimation floor, so it cannot be fed a smaller
mesh either. The trellis-2 lane reached the same conclusion independently and ships
--baker vertex as its fast path; this now matches.
bake_vertex_colors samples the PBR attribute volume at each vertex and writes COLOR_0.
Positions map to voxel indices by the same linear aabb relation fdg_to_mesh uses, so
nothing is resampled; lookup is a sorted-key searchsorted, and misses keep neutral
grey rather than black.
Verified on the real pipeline output:
bake vertex colours, 96.1% of vertices hit 0.2s
result 90,093 verts / 214,322 faces
57,925 unique colours, mean RGB [105 100 87], std [40 35 38]
3.9% still default grey (matches the 4% miss rate)
TOTAL 266.2s end to end, peak 32.6GB
What this costs: no metallic/roughness maps, base colour only. That is the honest
trade and it is stated in the operator description rather than buried - remesh also
now defaults OFF in to_glb, since upstream's remesh=True assumes CUDA.
Operator gains a baker param (vertex default, uv opt-in and flagged offline-only).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b6f0d76619
commit
d338eca925
@ -91,6 +91,51 @@ def fdg_to_mesh(h, resolution: int, voxel_margin: float = 0.5) -> Tuple:
|
|||||||
return v, f
|
return v, f
|
||||||
|
|
||||||
|
|
||||||
|
def bake_vertex_colors(mesh, tex_voxels, resolution: int,
|
||||||
|
attr_layout: dict | None = None):
|
||||||
|
"""Sample the PBR attribute volume at each vertex -> COLOR_0. Seconds, not minutes.
|
||||||
|
|
||||||
|
The UV path (`to_glb`) runs o_voxel's unwrap+bake, which on this CPU/Metal build
|
||||||
|
burned >20 minutes of CPU on a 214k-face mesh even with remesh disabled — xatlas
|
||||||
|
scales badly and 214k is the decimation floor, so it cannot simply be fed less.
|
||||||
|
The trellis-2 lane reached the same conclusion and ships `--baker vertex` as its
|
||||||
|
fast path for exactly this reason.
|
||||||
|
|
||||||
|
Vertex colours lose the metallic/roughness maps — base colour only — but they are
|
||||||
|
correct, immediate, and enough to see the asset. Positions map to voxel indices by
|
||||||
|
the same linear aabb relation `fdg_to_mesh` used, so no resampling is involved.
|
||||||
|
"""
|
||||||
|
import trimesh
|
||||||
|
|
||||||
|
layout = attr_layout or PBR_ATTR_LAYOUT
|
||||||
|
coords = np.asarray(tex_voxels.coords[:, 1:])
|
||||||
|
attrs = np.asarray(tex_voxels.feats)
|
||||||
|
|
||||||
|
lo, hi = np.array(AABB[0]), np.array(AABB[1])
|
||||||
|
v = np.asarray(mesh.vertices)
|
||||||
|
idx = np.floor((v - lo) / (hi - lo) * resolution).astype(np.int64)
|
||||||
|
idx = np.clip(idx, 0, resolution - 1)
|
||||||
|
|
||||||
|
# hash voxel coords -> row, then look each vertex up; unmatched vertices keep grey
|
||||||
|
key = (coords[:, 0].astype(np.int64) * resolution + coords[:, 1]) * resolution + coords[:, 2]
|
||||||
|
order = np.argsort(key)
|
||||||
|
key_sorted = key[order]
|
||||||
|
q = (idx[:, 0] * resolution + idx[:, 1]) * resolution + idx[:, 2]
|
||||||
|
pos = np.searchsorted(key_sorted, q)
|
||||||
|
pos = np.clip(pos, 0, len(key_sorted) - 1)
|
||||||
|
hit = key_sorted[pos] == q
|
||||||
|
|
||||||
|
base = layout["base_color"]
|
||||||
|
rgb = np.full((len(v), 3), 0.5, np.float32)
|
||||||
|
rgb[hit] = attrs[order[pos[hit]], base]
|
||||||
|
colors = np.concatenate([np.clip(rgb, 0, 1), np.ones((len(v), 1), np.float32)], 1)
|
||||||
|
|
||||||
|
out = trimesh.Trimesh(mesh.vertices, mesh.faces, process=False)
|
||||||
|
out.visual = trimesh.visual.ColorVisuals(out, vertex_colors=(colors * 255).astype(np.uint8))
|
||||||
|
return out, {"vertices_coloured": int(hit.sum()), "vertices_total": len(v),
|
||||||
|
"hit_rate": round(float(hit.mean()), 4)}
|
||||||
|
|
||||||
|
|
||||||
def to_glb(vertices, faces, tex_voxels, attr_layout: dict, resolution: int,
|
def to_glb(vertices, faces, tex_voxels, attr_layout: dict, resolution: int,
|
||||||
texture_size: int = 4096, decimation_target: int = 1_000_000,
|
texture_size: int = 4096, decimation_target: int = 1_000_000,
|
||||||
prefer_metal: bool = True, remesh: bool = False):
|
prefer_metal: bool = True, remesh: bool = False):
|
||||||
|
|||||||
@ -73,6 +73,9 @@ def main():
|
|||||||
ap.add_argument("--texture", action="store_true",
|
ap.add_argument("--texture", action="store_true",
|
||||||
help="run the texture stage and bake PBR maps through o_voxel")
|
help="run the texture stage and bake PBR maps through o_voxel")
|
||||||
ap.add_argument("--texture-size", type=int, default=2048)
|
ap.add_argument("--texture-size", type=int, default=2048)
|
||||||
|
ap.add_argument("--baker", choices=("vertex", "uv"), default="vertex",
|
||||||
|
help="vertex = seconds, base colour only (default); "
|
||||||
|
"uv = o_voxel unwrap + full PBR maps, but >20min CPU here")
|
||||||
a = ap.parse_args()
|
a = ap.parse_args()
|
||||||
|
|
||||||
t = time.time()
|
t = time.time()
|
||||||
@ -94,23 +97,30 @@ def main():
|
|||||||
# makes it tractable, and the baker samples the attribute VOLUME at mesh
|
# makes it tractable, and the baker samples the attribute VOLUME at mesh
|
||||||
# positions, so a decimated mesh still gets correct colours.
|
# positions, so a decimated mesh still gets correct colours.
|
||||||
from pixal3d_mlx.cleanup import clean
|
from pixal3d_mlx.cleanup import clean
|
||||||
from pixal3d_mlx.mesh import to_glb
|
from pixal3d_mlx.mesh import bake_vertex_colors, to_glb
|
||||||
|
|
||||||
t = time.time()
|
t = time.time()
|
||||||
pre, _ = clean(v.cpu().numpy(), f.cpu().numpy(),
|
pre, _ = clean(v.cpu().numpy(), f.cpu().numpy(),
|
||||||
target_faces=a.target_faces or 500_000)
|
target_faces=a.target_faces or 500_000)
|
||||||
print(f" pre-bake {len(pre.faces):,} faces {time.time() - t:6.1f}s")
|
print(f" pre-bake {len(pre.faces):,} faces {time.time() - t:6.1f}s")
|
||||||
|
|
||||||
import torch
|
|
||||||
t = time.time()
|
t = time.time()
|
||||||
scene = to_glb(torch.from_numpy(np.asarray(pre.vertices, np.float32)),
|
if a.baker == "vertex":
|
||||||
torch.from_numpy(np.asarray(pre.faces, np.int32)),
|
mesh, bstat = bake_vertex_colors(pre, tex_voxels, info["output_resolution"])
|
||||||
tex_voxels, PBR_ATTR_LAYOUT, info["output_resolution"],
|
info["bake"] = bstat
|
||||||
texture_size=a.texture_size,
|
print(f" bake vertex colours, {bstat['hit_rate']:.1%} of vertices hit"
|
||||||
decimation_target=a.target_faces or 500_000)
|
f" {time.time() - t:6.1f}s")
|
||||||
mesh = scene if isinstance(scene, trimesh.Trimesh) else scene.dump(concatenate=True)
|
else:
|
||||||
print(f" bake {len(mesh.faces):,} faces, {a.texture_size}px "
|
import torch
|
||||||
f"{time.time() - t:6.1f}s")
|
scene = to_glb(torch.from_numpy(np.asarray(pre.vertices, np.float32)),
|
||||||
|
torch.from_numpy(np.asarray(pre.faces, np.int32)),
|
||||||
|
tex_voxels, PBR_ATTR_LAYOUT, info["output_resolution"],
|
||||||
|
texture_size=a.texture_size,
|
||||||
|
decimation_target=a.target_faces or 500_000)
|
||||||
|
mesh = scene if isinstance(scene, trimesh.Trimesh) else scene.dump(concatenate=True)
|
||||||
|
print(f" bake UV {len(mesh.faces):,} faces, {a.texture_size}px "
|
||||||
|
f"{time.time() - t:6.1f}s")
|
||||||
|
info["baker"] = a.baker
|
||||||
mesh.export(a.output)
|
mesh.export(a.output)
|
||||||
info["faces"], info["vertices"] = len(mesh.faces), len(mesh.vertices)
|
info["faces"], info["vertices"] = len(mesh.faces), len(mesh.vertices)
|
||||||
print(f"\nTOTAL {info['seconds']}s peak {info['peak_gb']} GB -> {a.output}")
|
print(f"\nTOTAL {info['seconds']}s peak {info['peak_gb']} GB -> {a.output}")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user