fix: correct rig deformation — arms no longer cross body, feet no longer stretch to center
The model animated but deformed wrong: arms crossed the torso and foot heels stretched to the model's center-bottom. Two root causes, both fixed: 1. Pose mismatch. Sketchfab models are modelled in an A-pose (arms angled down) but the Quaternius library rests in a T-pose. Binding the A-pose mesh to T-pose arm bones misweighted the arms, and the library clips (which rotate arms ~70° down from the T rest) then shoved the already-drooped arms across the body. Fix: autorig.fit_arms_to_mesh() reorients each arm bone chain along the model's real arm direction; merge_animations retargets every clip from the library's T-pose rest onto the fitted rest via world-space constraint bake (Copy Rotation/Location + nla.bake), so world motion is reproduced regardless of the rest difference. 2. Feet-to-center stretch. The library's deform-flagged `root` bone runs up the body centre-line; nearest-bone weighting assigned inner-foot/heel verts to it, stretching them to center when animated. Fix: exclude root/control/mechanism bones from weighting (_NON_DEFORM_HINTS). Also upgraded nearest-bone weighting from rigid (1 bone/vert) to a smooth inverse-distance blend across the nearest 4 bones, so joints deform cleanly. Bone-heat still fails on Miku's joined hair/clothing mesh, so this is the primary path. Verified by rendering Idle/Walk/Run poses (Blender): natural stance, arms at sides swinging correctly, feet planted, proper running stride. Smoke test still 30/30. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
22cb0a58b7
commit
9939e7e524
+82
-14
@@ -230,8 +230,68 @@ def _add_clip_track(target_arm, action, clip_name):
|
||||
action.use_fake_user = True
|
||||
|
||||
|
||||
def _assign_action(obj, action):
|
||||
"""Assign an action to an object across Blender versions (slotted actions)."""
|
||||
if not obj.animation_data:
|
||||
obj.animation_data_create()
|
||||
obj.animation_data.action = action
|
||||
try:
|
||||
slots = action.slots
|
||||
if len(slots):
|
||||
obj.animation_data.action_slot = slots[0]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def retarget_action(target_arm, src_arm, src_action, clip_name):
|
||||
"""Retarget one source clip onto the character rig via world-space
|
||||
constraint baking.
|
||||
|
||||
The character rig's arms were fitted to the mesh's A-pose (autorig.py), so
|
||||
its rest differs from the library's T-pose. Copying each bone's WORLD
|
||||
orientation (not its local channel) reproduces the library's actual motion
|
||||
regardless of that rest difference, then baking captures it as a clean
|
||||
action on the character rig.
|
||||
"""
|
||||
_assign_action(src_arm, src_action)
|
||||
fr0 = int(src_action.frame_range[0])
|
||||
fr1 = int(src_action.frame_range[1])
|
||||
|
||||
for pb in target_arm.pose.bones:
|
||||
if pb.name not in src_arm.pose.bones:
|
||||
continue
|
||||
cr = pb.constraints.new("COPY_ROTATION")
|
||||
cr.target = src_arm
|
||||
cr.subtarget = pb.name
|
||||
cr.target_space = "WORLD"
|
||||
cr.owner_space = "WORLD"
|
||||
if pb.name.lower().endswith("hips"):
|
||||
cl = pb.constraints.new("COPY_LOCATION")
|
||||
cl.target = src_arm
|
||||
cl.subtarget = pb.name
|
||||
cl.target_space = "WORLD"
|
||||
cl.owner_space = "WORLD"
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
target_arm.select_set(True)
|
||||
bpy.context.view_layer.objects.active = target_arm
|
||||
if target_arm.animation_data:
|
||||
target_arm.animation_data.action = None
|
||||
|
||||
bpy.ops.nla.bake(frame_start=fr0, frame_end=fr1, step=1,
|
||||
only_selected=False, visual_keying=True,
|
||||
clear_constraints=True, clear_parents=False,
|
||||
use_current_action=True, bake_types={"POSE"})
|
||||
|
||||
baked = target_arm.animation_data.action
|
||||
if STRIP_ROOT_MOTION:
|
||||
strip_hips_horizontal(baked)
|
||||
return baked
|
||||
|
||||
|
||||
def merge_from_library(target_arm, target_hips, library_path):
|
||||
"""Merge clips from a single multi-clip library GLB (same skeleton)."""
|
||||
"""Retarget every mapped clip from a single multi-clip library GLB onto the
|
||||
character rig (whose arm rest was fitted to the mesh)."""
|
||||
print(f"Using multi-clip library: {library_path}")
|
||||
before = set(bpy.data.objects)
|
||||
before_actions = set(bpy.data.actions)
|
||||
@@ -239,31 +299,39 @@ def merge_from_library(target_arm, target_hips, library_path):
|
||||
new_objects = [o for o in bpy.data.objects if o not in before]
|
||||
new_actions = [a for a in bpy.data.actions if a not in before_actions]
|
||||
src_arm = find_armature(new_objects)
|
||||
if not src_arm:
|
||||
print("ERROR: no armature in animation library")
|
||||
sys.exit(1)
|
||||
|
||||
ratio = 1.0
|
||||
if src_arm:
|
||||
ratio = target_hips / max(hips_height(src_arm), 1e-6)
|
||||
print(f"Location scale ratio: {ratio:.3f}")
|
||||
# Match the library rig's height to the character so hips bob / foot planting
|
||||
# translate correctly under the world-space location copy.
|
||||
src_hips = hips_height(src_arm)
|
||||
if src_hips > 1e-4:
|
||||
s = target_hips / src_hips
|
||||
src_arm.scale = (s, s, s)
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
src_arm.select_set(True)
|
||||
bpy.context.view_layer.objects.active = src_arm
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
|
||||
merged = 0
|
||||
keep: list = []
|
||||
keep = []
|
||||
for action in new_actions:
|
||||
base = action.name.split(".")[0] # tolerate .001 suffixes
|
||||
clip_name = LIBRARY_CLIP_MAP.get(base)
|
||||
if not clip_name:
|
||||
continue
|
||||
print(f"--- {base} -> '{clip_name}'")
|
||||
scale_location_curves(action, ratio)
|
||||
if STRIP_ROOT_MOTION:
|
||||
strip_hips_horizontal(action)
|
||||
_add_clip_track(target_arm, action, clip_name)
|
||||
keep.append(action)
|
||||
print(f"--- {base} -> '{clip_name}' (retarget bake)")
|
||||
baked = retarget_action(target_arm, src_arm, action, clip_name)
|
||||
target_arm.animation_data.action = None
|
||||
_add_clip_track(target_arm, baked, clip_name)
|
||||
keep.append(baked)
|
||||
merged += 1
|
||||
|
||||
for o in new_objects:
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
for a in new_actions:
|
||||
if a not in keep:
|
||||
for a in list(bpy.data.actions):
|
||||
if a not in keep and a in new_actions:
|
||||
bpy.data.actions.remove(a)
|
||||
return merged
|
||||
|
||||
|
||||
Reference in New Issue
Block a user