b6f0d76619
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b6f0d76619 |
Texture stage: tex SLAT + PBR decode, and the bake order that makes it tractable
The texture flow is imgshape2tex - it denoises 32 PBR channels while SEEING the shape latent, so in_channels is 64 against out_channels 32. Upstream feeds the shape latent as concat_cond and the model does sparse_cat([x, concat_cond], dim=-1); both share coords, so it reduces to a channel concat. Added to slat_flow and carried on the sampler (it is fixed for the whole trajectory and must reach BOTH CFG branches). Verified running on the real checkpoints: 3,988,052 PBR voxels x 6 channels in 65.8s (base_color 0:3, metallic 3:4, roughness 4:5, alpha 5:6). Two things here fail SILENTLY rather than loudly, so both are asserted in comments: 1. shape_slat arrives DENORMALISED - the shape stage un-standardises it for the decoder - but the texture flow was trained against the standardised form. It is re-normalised before use as concat_cond. Skipping that gives a plausible mesh with wrong colours, not an error. 2. tex_dec has pred_subdiv=False: it cannot invent subdivisions and must be handed the shape decoder's subs as guides, so texture voxels land on the geometry that was actually built. The decoder's output is mapped * 0.5 + 0.5 into [0,1], the range o_voxel expects. BAKE ORDER. Handing o_voxel the raw ~8M-face mesh hangs - the same wall the standalone remesh test hit (killed at 20min), and the trellis2 lane's own operator note says the uncapped bake peaks at 75GB. So the mesh is welded, stripped of floaters and decimated BEFORE baking; the baker samples the attribute VOLUME at mesh positions, so a decimated mesh still gets correct colours. Measured on the way through: welded 3,988,052 -> 3,983,672 verts floaters 12 components -> 1 kept, 6,332 faces dropped decimated 7,996,876 -> 214,322 faces pre-bake 34.6s That floater count is worth noting: 12 components, not the 52,855 the first health pass reported. Welding first is what makes the difference. remesh now defaults OFF in to_glb, unlike upstream. Upstream runs on CUDA; this is the CPU/Metal build and its remesher took >20 minutes on a 214k-face mesh. It is also handed an already-clean mesh, so there is far less for it to fix. Operator gains texture + texture_size params; geometry-only stays the default because it is ~3min against the textured path's extra flow and bake. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
516e3e4457 |
Mesh cleanup: weld, strip floaters, iterative decimation
The raw cascade output is ~4M verts / 8M faces and is not usable as-is. cleanup.py is ordinary mesh hygiene, kept out of the model code, and the ORDER is the whole point: weld -> strip floaters -> decimate -> strip again -> fix normals CORRECTION TO THE FLOATER COUNT. The health pass reported 52,855 components with 52,838 fragments under 100 faces, and I took those for stray shells. They were mostly NOT: the decoder emits per-voxel vertices, so coincident corners are duplicated and the same continuous surface reads as tens of thousands of islands. Welding FIRST collapses it to a single component, and only 6,332 faces are genuinely stray. Ordering the pass the other way round removes 250k faces of real geometry and calls it cleaning. Two performance fixes, both because an operator runs this every job: - Component labelling is a scipy union-find over the VERTEX graph, not trimesh.face_adjacency. Same answer, ~240s -> ~1s on this mesh. - fix_normals runs LAST, on the decimated mesh. It walks face adjacency, so on the raw 7.99M-face mesh it costs minutes and the result is then thrown away by decimation. Cleanup went ~243s -> ~20s. Decimation is iterative. A single fast_simplification call will not reduce past roughly 4.4% of its input whatever target_reduction (or agg) is asked for: from 7.99M faces, targets of 200k, 50k and 20k ALL returned 351,535. Repeated smaller passes get further because each re-evaluates quadrics on the collapsed mesh. MEASURED, on the sample: target 500,000 -> 499,984 faces silhouette IoU 0.965 19.3s target 200,000 -> 214,322 faces silhouette IoU 0.823 34.8s target 100,000 -> 214,322 faces silhouette IoU 0.823 target 20,000 -> 214,322 faces silhouette IoU 0.823 HONEST LIMITATION: ~214k is a hard floor, and reaching it costs real fidelity (0.965 -> 0.823). The cause is the ~180,000 BOUNDARY edges the Flexible Dual Grid produces for open surfaces - quadric decimation will not collapse those, and no aggressiveness setting changes it. Below ~214k needs a REMESH, not a decimator. 500k is effectively lossless and is the setting to use; anything under 214k is not currently reachable and the loop stops rather than spinning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
165de26db5 |
The shipped cascade: image -> GLB at silhouette IoU 0.969
image_to_mesh() now runs the real cascade, not the single-stage shortcut: structure 3048 voxels @32^3 (64^3 occupancy, MAX-POOLED DOWN) LR SLAT 3048 x 32 shape_512 extractor refine 13147 coords @64^3 four decoder stages -> coords -> quantise HR SLAT 13147 x 32 shape_1024 extractor mesh 3988052 verts, 7996876 faces @1024^3 TOTAL 258.4s, peak 27.9GB with every model resident silhouette IoU 0.969 Three things the cascade needed: 1. occupied_coords_at() - ss_dec always decodes 64^3 but the cascade STARTS at 32^3. Upstream max-pools the boolean grid down by the ratio (a voxel survives if ANY of its eight children was occupied). I had been feeding the raw 64^3 set to the HR flow. 2. decoder.upsample() - pushes the LR latent four stages in and returns COORDS, not features. The predicted subdivisions grow the occupied set; those coords quantise onto the HR flow's grid. Stops BEFORE stage `upsample_times`, as upstream does; one stage further doubles the resolution and misplaces every voxel. 3. grid_resolution override on ProjConditioner - upstream backs the HR grid off in 128-unit steps while the token count exceeds max_num_tokens, so a dense object degrades instead of exploding. refine_coords() implements that loop. I WAS WRONG ABOUT THE HALO. The previous commit blamed the single-stage shortcut for a 0.639 silhouette IoU and predicted the cascade would fix it. The cascade measured 0.640 - no change. The real fault was in my VERIFICATION, not the pipeline: o_voxel returns vertices in the voxel-grid frame, while ProjGrid rotates its lattice by _BLENDER_ROT before projecting. Rotating the mesh the same way scores 0.969 on the same geometry the earlier commit had already produced. Added mesh.to_camera_frame() so the trap is named where it bites; the earlier mesh was correct all along. The cascade is still the right thing - it is the shipped path, and staged loading halves peak memory (12.8GB vs 22.6GB) when models are released between stages. Also adds models.load_all(), so a server builds all five models plus both conditioners ONCE. Warmup is ~71s against ~17s of compute, so an operator must never fork per job. Holding everything resident costs 27.9GB peak - nothing on a 256GB box. scripts/image_to_mesh.py exits non-zero if IoU < 0.85: a run that completes with a bad reconstruction has failed even though nothing raised. 27/27 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |