autorig: gen-mesh mode — preclean (weld+degenerate+UV-preserving decimate to --tris budget) + manifold voxel-proxy bone-heat with weight transfer + smoothing passes
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 <noreply@anthropic.com>
This commit is contained in:
parent
65dc0061b5
commit
a5311fda76
@ -249,12 +249,87 @@ def skin(meshes, arm_obj):
|
|||||||
rescue_unweighted(m, 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():
|
def main():
|
||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
ap.add_argument("--in", dest="inp", required=True)
|
ap.add_argument("--in", dest="inp", required=True)
|
||||||
ap.add_argument("--out", required=True)
|
ap.add_argument("--out", required=True)
|
||||||
ap.add_argument("--markers", default=None)
|
ap.add_argument("--markers", default=None)
|
||||||
ap.add_argument("--name", default=None)
|
ap.add_argument("--name", default=None)
|
||||||
|
ap.add_argument("--tris", type=int, default=16000)
|
||||||
args = ap.parse_args(stage_args())
|
args = ap.parse_args(stage_args())
|
||||||
|
|
||||||
overrides = {}
|
overrides = {}
|
||||||
@ -270,13 +345,17 @@ def main():
|
|||||||
if not meshes:
|
if not meshes:
|
||||||
die("input contains no mesh")
|
die("input contains no mesh")
|
||||||
|
|
||||||
|
preclean(meshes, args.tris)
|
||||||
verts = gather_world_verts(meshes)
|
verts = gather_world_verts(meshes)
|
||||||
L = detect_landmarks(verts, overrides)
|
L = detect_landmarks(verts, overrides)
|
||||||
print(f"[mirpamo] mesh height {L['_height']:.3f}m, "
|
print(f"[mirpamo] mesh height {L['_height']:.3f}m, "
|
||||||
f"{len(verts)} verts, landmarks detected")
|
f"{len(verts)} verts, landmarks detected")
|
||||||
|
|
||||||
arm_obj = build_skeleton(L, name)
|
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
|
# sanity: every mesh must now have an armature modifier
|
||||||
for m in meshes:
|
for m in meshes:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user