fix(audit): converge on the 2026-07-23 animation standard — tool sync, QuatSkin INBOX, registry corrections
From the conformance audit (ariki-game .agents/audits/animation-standard-assessment-2026-07-23.md): - tools/: cc_retarget + mixamo_retarget re-synced from ariki-game (were stale forks missing --despike/--fingers/--map ual); kevin_retarget path now repo-relative; mirror rule documented in exchange/GUIDE.md - INBOX/lena: add canonical Ariki_Female_QuatSkin.glb; README marks MixamoSkin superseded - REGISTRY: tide_dance_01 flagged (no DanceType enum entry); rain_dance note corrected (RainDance IS in the enum); boat table notes live canoedismount1/run_to_dive; QC split (loop_qc pre-commit vs anim_qc post-ingest) documented - docs/iclone-bridge.md: stub created (GUIDE referenced it but it never existed) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,22 @@ def detect_prefix(arm):
|
||||
raise RuntimeError("no *Hips bone found — not a Mixamo rig?")
|
||||
|
||||
|
||||
# ── UAL/Quaternius-source mode (2026-07-21): retarget stock-skeleton clips (UAL,
|
||||
# Kevin) onto a DIFFERENT-convention skeleton (e.g. the renamed Mixamo rig — its
|
||||
# bone local frames differ, so raw clip playback tips her face-down). Identity
|
||||
# bone map (names already match), source frame bones are the Quaternius names.
|
||||
QUAT_BONES = ["pelvis", "spine_01", "spine_02", "spine_03", "neck_01", "Head"]
|
||||
for _T in ("l", "r"):
|
||||
QUAT_BONES += [f"clavicle_{_T}", f"upperarm_{_T}", f"lowerarm_{_T}", f"hand_{_T}",
|
||||
f"thigh_{_T}", f"calf_{_T}", f"foot_{_T}", f"ball_{_T}"]
|
||||
for _i in (1, 2, 3):
|
||||
QUAT_BONES += [f"index_0{_i}_{_T}", f"middle_0{_i}_{_T}", f"ring_0{_i}_{_T}",
|
||||
f"pinky_0{_i}_{_T}", f"thumb_0{_i}_{_T}"]
|
||||
UAL_MAP = {n: n for n in QUAT_BONES}
|
||||
# Per-mode source-frame bones (see retarget_one): mixamo | ual
|
||||
SRC_FRAME_BONES = ("Hips", "Head", "LeftArm")
|
||||
|
||||
|
||||
def apply_object_transform(obj):
|
||||
"""Bake importer object-level rotation/scale into the armature data (meters, Z-up)."""
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
@@ -109,11 +125,12 @@ def retarget_one(src_arm, tgt_arm, action, name, scn, prefix, unit):
|
||||
tgt_rest_q = {n: m.to_quaternion().normalized() for n, m in tgt_rest.items()}
|
||||
|
||||
C = (char_frame(tgt_rest, "pelvis", "Head", "upperarm_l")
|
||||
@ char_frame(src_rest, prefix + "Hips", prefix + "Head", prefix + "LeftArm").inverted()).normalized()
|
||||
@ char_frame(src_rest, prefix + SRC_FRAME_BONES[0], prefix + SRC_FRAME_BONES[1],
|
||||
prefix + SRC_FRAME_BONES[2]).inverted()).normalized()
|
||||
Cinv = C.inverted()
|
||||
|
||||
# Uniform size ratio from hips→head world distance (unit/scale agnostic).
|
||||
h_src = (src_rest[prefix + "Head"].translation - src_rest[prefix + "Hips"].translation).length
|
||||
h_src = (src_rest[prefix + SRC_FRAME_BONES[1]].translation - src_rest[prefix + SRC_FRAME_BONES[0]].translation).length
|
||||
h_tgt = (tgt_rest["Head"].translation - tgt_rest["pelvis"].translation).length
|
||||
tscale = h_tgt / h_src if h_src > 1e-5 else 1.0
|
||||
|
||||
@@ -127,7 +144,7 @@ def retarget_one(src_arm, tgt_arm, action, name, scn, prefix, unit):
|
||||
for pb in tgt_arm.pose.bones:
|
||||
pb.rotation_mode = "QUATERNION"
|
||||
|
||||
src_hips_rest_t = src_rest[prefix + "Hips"].translation
|
||||
src_hips_rest_t = src_rest[prefix + SRC_FRAME_BONES[0]].translation
|
||||
|
||||
for f in range(f0, f1 + 1):
|
||||
scn.frame_set(f)
|
||||
@@ -181,18 +198,23 @@ def retarget_one(src_arm, tgt_arm, action, name, scn, prefix, unit):
|
||||
|
||||
|
||||
def main():
|
||||
global BONE_MAP, SRC_FRAME_BONES
|
||||
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||
args = dict(zip(argv[::2], argv[1::2]))
|
||||
srcs, out = args["--src"].split(","), args["--out"]
|
||||
limit = int(args.get("--limit", 0))
|
||||
target = args.get("--target", DEFAULT_TARGET_GLTF)
|
||||
clips_filter = [c.strip().lower() for c in args.get("--clips", "").split(",") if c.strip()]
|
||||
if args.get("--map") == "ual":
|
||||
BONE_MAP = UAL_MAP # identity: source bones already quat-named
|
||||
SRC_FRAME_BONES = ("pelvis", "Head", "upperarm_l")
|
||||
|
||||
fbx_files = []
|
||||
for s in srcs:
|
||||
s = s.strip()
|
||||
if os.path.isdir(s):
|
||||
for root, _, files in os.walk(s):
|
||||
fbx_files += [os.path.join(root, x) for x in sorted(files) if x.lower().endswith(".fbx")]
|
||||
fbx_files += [os.path.join(root, x) for x in sorted(files) if x.lower().endswith((".fbx", ".glb", ".gltf"))]
|
||||
else:
|
||||
fbx_files.append(s)
|
||||
if limit:
|
||||
@@ -218,22 +240,31 @@ def main():
|
||||
name = clip_name(f)
|
||||
pre = set(bpy.data.objects)
|
||||
pre_actions = set(bpy.data.actions)
|
||||
bpy.ops.import_scene.fbx(filepath=f)
|
||||
if f.lower().endswith((".glb", ".gltf")):
|
||||
bpy.ops.import_scene.gltf(filepath=f)
|
||||
else:
|
||||
bpy.ops.import_scene.fbx(filepath=f)
|
||||
new_objs = [o for o in bpy.data.objects if o not in pre]
|
||||
src_arm = next((o for o in new_objs if o.type == "ARMATURE"), None)
|
||||
new_acts = [a for a in bpy.data.actions if a not in pre_actions]
|
||||
if clips_filter:
|
||||
new_acts = [a for a in new_acts if any(c in a.name.lower() for c in clips_filter)]
|
||||
if src_arm is None or not new_acts:
|
||||
print(f"[mixamo] SKIP (no armature/action): {f}")
|
||||
else:
|
||||
bpy.context.view_layer.update()
|
||||
prefix = detect_prefix(src_arm)
|
||||
prefix = "" if args.get("--map") == "ual" else detect_prefix(src_arm)
|
||||
# Importer unit scale (cm→m 0.01) — capture BEFORE transform_apply wipes it;
|
||||
# the hips translation math needs it (fcurves stay in source units).
|
||||
unit = src_arm.scale.x
|
||||
apply_object_transform(src_arm)
|
||||
act = retarget_one(src_arm, tgt_arm, new_acts[0], name, scn, prefix, unit)
|
||||
done.append(act)
|
||||
print(f"[mixamo] {i + 1}/{len(fbx_files)} {name} ({int(act.frame_range[1])}f, prefix='{prefix}')")
|
||||
for act_src in new_acts:
|
||||
out_name = act_src.name if args.get("--map") == "ual" else name
|
||||
if args.get("--map") == "ual":
|
||||
act_src.name += "__src" # free the clean name — Blender dedupes the output to .001 otherwise
|
||||
act = retarget_one(src_arm, tgt_arm, act_src, out_name, scn, prefix, unit)
|
||||
done.append(act)
|
||||
print(f"[mixamo] {i + 1}/{len(fbx_files)} {out_name} ({int(act.frame_range[1])}f, prefix='{prefix}')")
|
||||
for o in new_objs:
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
for a in new_acts:
|
||||
|
||||
Reference in New Issue
Block a user