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
|
||||
|
||||
|
||||
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,
|
||||
texture_size: int = 4096, decimation_target: int = 1_000_000,
|
||||
prefer_metal: bool = True, remesh: bool = False):
|
||||
|
||||
@ -73,6 +73,9 @@ def main():
|
||||
ap.add_argument("--texture", action="store_true",
|
||||
help="run the texture stage and bake PBR maps through o_voxel")
|
||||
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()
|
||||
|
||||
t = time.time()
|
||||
@ -94,23 +97,30 @@ def main():
|
||||
# makes it tractable, and the baker samples the attribute VOLUME at mesh
|
||||
# positions, so a decimated mesh still gets correct colours.
|
||||
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()
|
||||
pre, _ = clean(v.cpu().numpy(), f.cpu().numpy(),
|
||||
target_faces=a.target_faces or 500_000)
|
||||
print(f" pre-bake {len(pre.faces):,} faces {time.time() - t:6.1f}s")
|
||||
|
||||
import torch
|
||||
t = time.time()
|
||||
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 {len(mesh.faces):,} faces, {a.texture_size}px "
|
||||
f"{time.time() - t:6.1f}s")
|
||||
if a.baker == "vertex":
|
||||
mesh, bstat = bake_vertex_colors(pre, tex_voxels, info["output_resolution"])
|
||||
info["bake"] = bstat
|
||||
print(f" bake vertex colours, {bstat['hit_rate']:.1%} of vertices hit"
|
||||
f" {time.time() - t:6.1f}s")
|
||||
else:
|
||||
import torch
|
||||
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)
|
||||
info["faces"], info["vertices"] = len(mesh.faces), len(mesh.vertices)
|
||||
print(f"\nTOTAL {info['seconds']}s peak {info['peak_gb']} GB -> {a.output}")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user