"""Export a reusable Mixamo rest pose, or fit it to a humanoid mesh. Blender: blender -b --python Tools/model_tools/bl_fit_mixamo.py -- template blender -b --python Tools/model_tools/bl_fit_mixamo.py -- fit [palm|full] blender -b --python Tools/model_tools/bl_fit_mixamo.py -- remap """ import json import sys from pathlib import Path import bpy import numpy as np from math import pi from mathutils import Matrix, Vector BODY = [ "mixamorig:Hips", "mixamorig:Spine", "mixamorig:Spine1", "mixamorig:Spine2", "mixamorig:Neck", "mixamorig:Head", "mixamorig:HeadTop_End", "mixamorig:LeftShoulder", "mixamorig:LeftArm", "mixamorig:LeftForeArm", "mixamorig:LeftHand", "mixamorig:RightShoulder", "mixamorig:RightArm", "mixamorig:RightForeArm", "mixamorig:RightHand", "mixamorig:LeftUpLeg", "mixamorig:LeftLeg", "mixamorig:LeftFoot", "mixamorig:LeftToeBase", "mixamorig:LeftToe_End", "mixamorig:RightUpLeg", "mixamorig:RightLeg", "mixamorig:RightFoot", "mixamorig:RightToeBase", "mixamorig:RightToe_End", ] def argv(): return sys.argv[sys.argv.index("--") + 1:] def mesh_objects(): return [o for o in bpy.context.scene.objects if o.type == "MESH"] def armature(): arms = [o for o in bpy.context.scene.objects if o.type == "ARMATURE"] if len(arms) != 1: raise SystemExit(f"expected one armature, found {len(arms)}") return arms[0] def apply_transform(obj): bpy.ops.object.select_all(action="DESELECT") obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) def bone_heads(arm): return {b.name: arm.matrix_world @ b.head_local for b in arm.data.bones} def points_of(obj): if obj.type == "MESH": return np.array([obj.matrix_world @ v.co for v in obj.data.vertices], dtype=np.float64) return np.array([obj.matrix_world @ b.head_local for b in obj.data.bones], dtype=np.float64) def rotate_axis_to_y(obj, axis): if axis == 1: return rotation = Matrix.Rotation(-pi / 2, 4, "X") if axis == 2 else Matrix.Rotation(pi / 2, 4, "Z") obj.matrix_world = rotation @ obj.matrix_world apply_transform(obj) def put_height_on_y(obj): if obj.type == "ARMATURE": heads = bone_heads(obj) delta = heads["mixamorig:Head"] - heads["mixamorig:Hips"] axis = int(np.argmax(np.abs([delta.x, delta.y, delta.z]))) else: span = points_of(obj).max(0) - points_of(obj).min(0) order = np.argsort(span) axis = int(order[-1] if span[order[-1]] > span[order[-2]] * 1.25 else order[-2]) rotate_axis_to_y(obj, axis) def face_negative_z(arm): heads = bone_heads(arm) forward = heads["mixamorig:LeftToeBase"] - heads["mixamorig:LeftFoot"] if abs(forward.x) > abs(forward.z): arm.matrix_world = Matrix.Rotation(pi / 2 if forward.x > 0 else -pi / 2, 4, "Y") @ arm.matrix_world apply_transform(arm) heads = bone_heads(arm) forward = heads["mixamorig:LeftToeBase"] - heads["mixamorig:LeftFoot"] if forward.z > 0: arm.matrix_world = Matrix.Rotation(pi, 4, "Y") @ arm.matrix_world apply_transform(arm) def clear_motion(arm): if arm.animation_data: arm.animation_data_clear() arm.data.pose_position = "REST" def export_fbx(arm, path): bpy.ops.object.select_all(action="DESELECT") arm.select_set(True) bpy.context.view_layer.objects.active = arm bpy.ops.export_scene.fbx( filepath=str(path), use_selection=True, object_types={"ARMATURE"}, add_leaf_bones=False, bake_anim=False, axis_forward="-Z", axis_up="Y", bake_space_transform=False, ) def template(src, dst): bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.fbx(filepath=str(src)) for obj in list(mesh_objects()): bpy.data.objects.remove(obj, do_unlink=True) arm = armature() clear_motion(arm) put_height_on_y(arm) face_negative_z(arm) heads = bone_heads(arm) feet = [heads[n] for n in heads if n.endswith("Foot") or n.endswith("ToeBase") or n.endswith("Toe_End")] min_y = min(p.y for p in feet) center_x = (heads["mixamorig:LeftUpLeg"].x + heads["mixamorig:RightUpLeg"].x) * 0.5 center_z = heads["mixamorig:Hips"].z arm.location = Vector((-center_x, -min_y, -center_z)) apply_transform(arm) dst = Path(dst) dst.parent.mkdir(parents=True, exist_ok=True) export_fbx(arm, dst) heads = bone_heads(arm) top = max(p.y for p in heads.values()) info = { "bones": len(arm.data.bones), "names": [b.name for b in arm.data.bones], "height": round(top, 4), "up": "Y", "front": "-Z", "left": "-X", "source": str(src), } dst.with_suffix(".json").write_text(json.dumps(info, ensure_ascii=False, indent=2), encoding="utf-8") print("TEMPLATE", json.dumps({k: info[k] for k in ("bones", "height", "up", "front")})) def slice_centers(pts, y0, y1): sl = pts[(pts[:, 1] >= y0) & (pts[:, 1] < y1)] return sl def landmarks(pts): lo, hi = pts.min(0), pts.max(0) height = hi[1] - lo[1] bands = np.linspace(lo[1], hi[1], 36) spans = [] for a, b in zip(bands[:-1], bands[1:]): sl = slice_centers(pts, a, b) spans.append(0 if len(sl) < 8 else sl[:, 0].max() - sl[:, 0].min()) arm_i = int(np.argmax(spans)) arm_y = (bands[arm_i] + bands[arm_i + 1]) * 0.5 arm = slice_centers(pts, arm_y - height * 0.04, arm_y + height * 0.04) reach = max(abs(arm[:, 0].min()), abs(arm[:, 0].max())) left_hand = arm[arm[:, 0] < -reach * 0.55].mean(0) right_hand = arm[arm[:, 0] > reach * 0.55].mean(0) torso = arm[np.abs(arm[:, 0]) < reach * 0.38] if len(torso) < 8: torso = arm shoulders = np.array([torso[:, 0].min(), arm_y, torso[:, 2].mean()]) feet = pts[pts[:, 1] < lo[1] + height * 0.12] left_foot = feet[feet[:, 0] < 0].mean(0) right_foot = feet[feet[:, 0] > 0].mean(0) crotch_y = arm_y for a, b in zip(bands[:arm_i][::-1], bands[1:arm_i + 1][::-1]): sl = slice_centers(pts, a, b) if len(sl) < 8: continue xs = np.sort(sl[:, 0]) if np.diff(xs).max() > reach * 0.18: crotch_y = (a + b) * 0.5 break hip_y = min(arm_y - height * 0.05, crotch_y + height * 0.06) head = pts[pts[:, 1] > lo[1] + height * 0.78] return { "height": height, "arm_y": arm_y, "hip_y": hip_y, "head": head.mean(0), "top": hi[1], "left_hand": left_hand, "right_hand": right_hand, "left_shoulder_x": float(torso[:, 0].min()), "right_shoulder_x": float(torso[:, 0].max()), "left_foot": left_foot, "right_foot": right_foot, "depth": head[:, 2].mean() if False else float(head[:, 2].mean()), } def chain_fraction(arm, names): ys = [(arm.matrix_world @ arm.data.bones[n].head_local).y for n in names if n in arm.data.bones] span = ys[-1] - ys[0] if abs(span) < 1e-6: return [0.0 for _ in ys] return [(y - ys[0]) / span for y in ys] def move_bones(arm, marks): bpy.ops.object.select_all(action="DESELECT") arm.select_set(True) bpy.context.view_layer.objects.active = arm torso_names = [n for n in ("mixamorig:Hips", "mixamorig:Spine", "mixamorig:Spine1", "mixamorig:Spine2", "mixamorig:Neck", "mixamorig:Head", "mixamorig:HeadTop_End") if n in arm.data.bones] torso_fr = chain_fraction(arm, torso_names) side_fr = {} for side in ("Left", "Right"): arm_names = [f"mixamorig:{side}{p}" for p in ("Shoulder", "Arm", "ForeArm", "Hand") if f"mixamorig:{side}{p}" in arm.data.bones] leg_names = [n for n in (f"mixamorig:{side}UpLeg", f"mixamorig:{side}Leg", f"mixamorig:{side}Foot") if n in arm.data.bones] side_fr[side] = (arm_names, chain_fraction(arm, arm_names), leg_names, chain_fraction(arm, leg_names)) bpy.ops.object.mode_set(mode="EDIT") bones = arm.data.edit_bones rolls = {b.name: b.roll for b in bones} present = {b.name: b for b in bones} def put(name, head, tail): bone = present[name] bone.use_connect = False bone.head = Vector(head) bone.tail = Vector(tail) bone.roll = rolls[name] def between(a, b, t): return a * (1 - t) + b * t fr = torso_fr hip = np.array([0.0, marks["hip_y"], marks["depth"]]) crown = np.array([marks["head"][0] * 0.2, marks["top"], marks["head"][2]]) placed = {} for name, t in zip(torso_names, fr): placed[name] = between(hip, crown, t) for i, name in enumerate(torso_names): nxt = placed[torso_names[i + 1]] if i + 1 < len(torso_names) else placed[name] + np.array([0, marks["height"] * 0.03, 0]) put(name, placed[name], nxt) for side, sign, hand, foot, shoulder_x in ( ("Left", -1, marks["left_hand"], marks["left_foot"], marks["left_shoulder_x"]), ("Right", 1, marks["right_hand"], marks["right_foot"], marks["right_shoulder_x"]), ): shoulder = np.array([shoulder_x, marks["arm_y"], marks["depth"]]) hand = np.array(hand) arm_names, afr, leg_core, lfr = side_fr[side] joints = [between(shoulder, hand, t) for t in afr] for i, name in enumerate(arm_names): nxt = joints[i + 1] if i + 1 < len(joints) else joints[i] + (hand - shoulder) * 0.08 put(name, joints[i], nxt) hip_j = np.array([foot[0] * 0.72, marks["hip_y"], marks["depth"]]) foot_j = np.array([foot[0], max(foot[1], 0.0), foot[2]]) leg_names = leg_core + [n for n in (f"mixamorig:{side}ToeBase", f"mixamorig:{side}Toe_End") if n in present] leg = [between(hip_j, foot_j, t) for t in lfr] toe = foot_j + np.array([0.0, 0.0, -abs(marks["height"]) * 0.06]) toe_end = toe + np.array([0.0, 0.0, -abs(marks["height"]) * 0.035]) extra = {f"mixamorig:{side}ToeBase": toe, f"mixamorig:{side}Toe_End": toe_end} seq = leg + [extra[n] for n in leg_names[3:]] for i, name in enumerate(leg_names): nxt = seq[i + 1] if i + 1 < len(seq) else seq[i] + np.array([0, 0, -0.02]) put(name, seq[i], nxt) bpy.ops.object.mode_set(mode="OBJECT") def drop_fingers(arm): bpy.ops.object.mode_set(mode="EDIT") keys = ("Thumb", "Index", "Middle", "Ring", "Pinky") guard = 0 while guard < 8: guard += 1 bones = list(arm.data.edit_bones) leaves = [b for b in bones if any(k in b.name for k in keys) and not any(c.parent == b for c in bones)] if not leaves: break for bone in leaves: arm.data.edit_bones.remove(bone) bpy.ops.object.mode_set(mode="OBJECT") def fit(template_path, mesh_path, out_path, fingers): bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.fbx(filepath=str(mesh_path)) mesh = mesh_objects()[0] put_height_on_y(mesh) before = set(bpy.data.objects) bpy.ops.import_scene.fbx(filepath=str(template_path)) arm = next(o for o in bpy.context.scene.objects if o not in before and o.type == "ARMATURE") clear_motion(arm) put_height_on_y(arm) face_negative_z(arm) if fingers == "palm": drop_fingers(arm) marks = landmarks(points_of(mesh)) move_bones(arm, marks) heads = bone_heads(arm) print("MARKS", json.dumps({k: (round(float(v), 3) if np.ndim(v) == 0 else [round(float(x), 3) for x in v]) for k, v in marks.items() if k != "head"})) for name in ("mixamorig:Hips", "mixamorig:Head", "mixamorig:LeftHand", "mixamorig:LeftFoot", "mixamorig:Spine2"): p = heads[name] print(name, round(p.x, 3), round(p.y, 3), round(p.z, 3)) mesh.parent = arm arm.matrix_world = Matrix.Rotation(pi / 2, 4, "X") @ arm.matrix_world apply_transform(arm) out = Path(out_path) out.parent.mkdir(parents=True, exist_ok=True) bpy.ops.export_scene.gltf(filepath=str(out), export_format="GLB", export_yup=True, export_skins=True) names = [b.name for b in arm.data.bones] print("FIT", json.dumps({"bones": len(names), "fingers": fingers, "out": str(out)})) def remap_names(source_glb, rigged_glb, out_glb): """TokenRig --use_skeleton renames joints to bone_* and can reverse a limb chain. Match each output joint to the fitted Mixamo bone by head position, then copy that bone's parent, head, tail and roll. Vertex groups follow the new names. """ bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.gltf(filepath=str(source_glb)) src = armature() bpy.context.view_layer.objects.active = src bpy.ops.object.mode_set(mode="EDIT") src_pose = {} for bone in src.data.edit_bones: src_pose[bone.name] = { "parent": bone.parent.name if bone.parent else None, "head": src.matrix_world @ bone.head, "tail": src.matrix_world @ bone.tail, "roll": bone.roll, } bpy.ops.object.mode_set(mode="OBJECT") for obj in list(bpy.data.objects): bpy.data.objects.remove(obj, do_unlink=True) bpy.ops.import_scene.gltf(filepath=str(rigged_glb)) arm = armature() mesh = mesh_objects()[0] used = set() mapping = {} for bone in arm.data.bones: head = arm.matrix_world @ bone.head_local name, dist = min( ((n, (head - pose["head"]).length) for n, pose in src_pose.items() if n not in used), key=lambda item: item[1], ) used.add(name) mapping[bone.name] = (name, dist) bpy.context.view_layer.objects.active = arm bpy.ops.object.mode_set(mode="EDIT") for old, (new, _dist) in mapping.items(): arm.data.edit_bones[old].name = new for bone in arm.data.edit_bones: bone.use_connect = False bone.parent = None for bone in arm.data.edit_bones: pose = src_pose[bone.name] bone.head = arm.matrix_world.inverted() @ pose["head"] bone.tail = arm.matrix_world.inverted() @ pose["tail"] bone.roll = pose["roll"] for bone in arm.data.edit_bones: parent = src_pose[bone.name]["parent"] bone.parent = arm.data.edit_bones[parent] if parent else None bpy.ops.object.mode_set(mode="OBJECT") for group in mesh.vertex_groups: if group.name in mapping: group.name = mapping[group.name][0] worst = max(dist for _, dist in mapping.values()) if worst > 0.05: raise SystemExit(f"bone heads moved, worst={worst:.4f}") out = Path(out_glb) out.parent.mkdir(parents=True, exist_ok=True) bpy.ops.export_scene.gltf(filepath=str(out), export_format="GLB", export_yup=True, export_skins=True) print("REMAP", json.dumps({"bones": len(mapping), "worst": round(worst, 4), "out": str(out)})) def main(): args = argv() if args[0] == "template": template(args[1], args[2]) elif args[0] == "fit": fit(args[1], args[2], args[3], args[4] if len(args) > 4 else "palm") elif args[0] == "remap": remap_names(args[1], args[2], args[3]) else: raise SystemExit("use template, fit, or remap") if __name__ == "__main__": main()