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)
|
||||
|
||||
|
||||
|
||||
|
||||
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,12 +345,16 @@ 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)
|
||||
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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user