From a5311fda76bb2964c5059494cc769e93327389ed Mon Sep 17 00:00:00 2001 From: m3ultra Date: Fri, 17 Jul 2026 15:58:50 +1000 Subject: [PATCH] =?UTF-8?q?autorig:=20gen-mesh=20mode=20=E2=80=94=20precle?= =?UTF-8?q?an=20(weld+degenerate+UV-preserving=20decimate=20to=20--tris=20?= =?UTF-8?q?budget)=20+=20manifold=20voxel-proxy=20bone-heat=20with=20weigh?= =?UTF-8?q?t=20transfer=20+=20smoothing=20passes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TRELLIS meshes never bone-heat directly (non-manifold even after weld); the watertight proxy solves, POLYINTERP_NEAREST transfers (ACTIVE->SELECTED — reversed is a silent no-op), 4x smooth knocks down shell-bleed spikes. Proven on the tradie: 194k->14k tris, 22 real groups, clean walk at game scale. --tris 0 = old direct path for authored meshes. Co-Authored-By: Claude Fable 5 --- bpy/autorig.py | 81 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/bpy/autorig.py b/bpy/autorig.py index ac5b9f0..8e62d37 100644 --- a/bpy/autorig.py +++ b/bpy/autorig.py @@ -249,12 +249,87 @@ def skin(meshes, arm_obj): rescue_unweighted(m, arm_obj) + + +def proxy_skin(meshes, arm_obj): + """Bone-heat on a VOXEL-REMESHED manifold proxy, weights transferred back to the real textured + mesh (POLYINTERP_NEAREST). Gen meshes stay non-manifold even after preclean ("Mesh not valid"), + so direct bone heat can never solve them — the watertight proxy can, and the transfer keeps + UVs/materials untouched. Falls back to the old rescue if the proxy path itself fails.""" + for m in meshes: + select_only([m], active=m) + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + proxy = m.copy(); proxy.data = m.data.copy() + bpy.context.collection.objects.link(proxy) + try: + select_only([proxy], active=proxy) + rm = proxy.modifiers.new("mirpamo_vox", "REMESH") + rm.mode = "VOXEL" + dims = max(proxy.dimensions) or 1.0 + rm.voxel_size = max(0.008, dims / 60.0) + bpy.ops.object.modifier_apply(modifier=rm.name) + select_only([proxy, arm_obj], active=arm_obj) + bpy.ops.object.parent_set(type="ARMATURE_AUTO") + groups = [g for g in proxy.vertex_groups] + if not groups: + raise RuntimeError("proxy solve produced no vertex groups") + # data_transfer flows ACTIVE -> SELECTED (bit us 2026-07-17: reversed = silent no-op) + select_only([m, proxy], active=proxy) + bpy.ops.object.data_transfer(use_create=True, data_type="VGROUP_WEIGHTS", + vert_mapping="POLYINTERP_NEAREST", + layers_select_src="ALL", layers_select_dst="NAME") + am = m.modifiers.new("Armature", "ARMATURE") + am.object = arm_obj + m.parent = arm_obj + rescue_unweighted(m, arm_obj) # belt-and-braces for verts the transfer missed + # smooth the transferred weights — POLYINTERP bleed between close shells (vest/torso) + # shows as silhouette spikes; a few smoothing passes knock them down + select_only([m], active=m) + bpy.ops.object.mode_set(mode="WEIGHT_PAINT") + bpy.ops.object.vertex_group_smooth(group_select_mode="ALL", factor=0.5, repeat=4, expand=0.08) + bpy.ops.object.mode_set(mode="OBJECT") + print(f"[mirpamo] proxy-skin OK for {m.name} ({len(m.vertex_groups)} groups on the real mesh)") + except Exception as e: + print(f"[mirpamo] proxy-skin failed for {m.name} ({e}) — falling back to direct bone heat") + select_only([m, arm_obj], active=arm_obj) + bpy.ops.object.parent_set(type="ARMATURE_AUTO") + rescue_unweighted(m, arm_obj) + finally: + if proxy.name in bpy.data.objects: + bpy.data.objects.remove(proxy, do_unlink=True) + + +def preclean(meshes, target_tris): + """Gen-mesh hygiene BEFORE landmarks/weights. TRELLIS/photogrammetry meshes arrive dense and + non-manifold: bone heat fails and the proximity fallback candy-wraps the joints (the tradie's + vest, 2026-07-17). Weld + degenerate-dissolve helps manifoldness; a UV-preserving collapse + decimate to the tri budget lightens the solve AND is the game's ped tri diet in the same pass. + target_tris=0 skips (authored meshes come in clean).""" + if not target_tris: + return + import bmesh + for m in meshes: + tris0 = sum(max(0, len(pg.vertices) - 2) for pg in m.data.polygons) + bm = bmesh.new(); bm.from_mesh(m.data) + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0005) + bmesh.ops.dissolve_degenerate(bm, edges=bm.edges, dist=0.0005) + bm.to_mesh(m.data); bm.free() + if tris0 > target_tris * 1.3: + mod = m.modifiers.new("mirpamo_decimate", "DECIMATE") + mod.ratio = max(0.02, target_tris / max(tris0, 1)) + bpy.context.view_layer.objects.active = m + bpy.ops.object.modifier_apply(modifier=mod.name) + tris1 = sum(max(0, len(pg.vertices) - 2) for pg in m.data.polygons) + print(f"[mirpamo] preclean {m.name}: {tris0} -> {tris1} tris") + + def main(): ap = argparse.ArgumentParser() ap.add_argument("--in", dest="inp", required=True) ap.add_argument("--out", required=True) ap.add_argument("--markers", default=None) ap.add_argument("--name", default=None) + ap.add_argument("--tris", type=int, default=16000) args = ap.parse_args(stage_args()) overrides = {} @@ -270,13 +345,17 @@ def main(): if not meshes: die("input contains no mesh") + preclean(meshes, args.tris) verts = gather_world_verts(meshes) L = detect_landmarks(verts, overrides) print(f"[mirpamo] mesh height {L['_height']:.3f}m, " f"{len(verts)} verts, landmarks detected") arm_obj = build_skeleton(L, name) - skin(meshes, arm_obj) + if args.tris: + proxy_skin(meshes, arm_obj) # gen-mesh mode: manifold proxy solve + weight transfer + else: + skin(meshes, arm_obj) # sanity: every mesh must now have an armature modifier for m in meshes: