Last active
October 3, 2025 22:01
-
-
Save avramovic/2c50405b32dece773882693b0d251ad1 to your computer and use it in GitHub Desktop.
Blender animation retargeting
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import bpy | |
| import sys | |
| import os | |
| # Parse arguments | |
| argv = sys.argv | |
| argv = argv[argv.index("--") + 1:] | |
| src_file = os.path.abspath(argv[0]) | |
| tgt_file = os.path.abspath(argv[1]) | |
| out_file = os.path.abspath(argv[2]) | |
| print("Source:", src_file) | |
| print("Target:", tgt_file) | |
| print("Output:", out_file) | |
| # Reset scene | |
| bpy.ops.wm.read_factory_settings(use_empty=True) | |
| # Import source | |
| print("Importing source...") | |
| bpy.ops.import_scene.gltf(filepath=src_file) | |
| # Find source armature | |
| arm_source = next((obj for obj in bpy.context.scene.objects if obj.type == "ARMATURE"), None) | |
| if not arm_source: | |
| raise Exception("No armature found in source file") | |
| print("Source armature:", arm_source.name) | |
| # Import target | |
| print("Importing target...") | |
| bpy.ops.import_scene.gltf(filepath=tgt_file) | |
| # Find target armature | |
| arm_target = next((obj for obj in bpy.context.scene.objects if obj.type == "ARMATURE" and obj != arm_source), None) | |
| if not arm_target: | |
| raise Exception("No armature found in target file") | |
| print("Target armature:", arm_target.name) | |
| # Bone mapping | |
| bone_map = { | |
| "DEF-hips": "Hips", "DEF-spine.001": "Spine", "DEF-spine.002": "Spine1", "DEF-spine.003": "Spine2", | |
| "DEF-neck": "Neck", "DEF-head": "Head", | |
| "DEF-shoulder.L": "LeftShoulder", "DEF-upper_arm.L": "LeftArm", "DEF-forearm.L": "LeftForeArm", "DEF-hand.L": "LeftHand", | |
| "DEF-thumb.01.L": "LeftHandThumb1", "DEF-thumb.02.L": "LeftHandThumb2", "DEF-thumb.03.L": "LeftHandThumb3", | |
| "DEF-f_index.01.L": "LeftHandIndex1", "DEF-f_index.02.L": "LeftHandIndex2", "DEF-f_index.03.L": "LeftHandIndex3", | |
| "DEF-f_middle.01.L": "LeftHandMiddle1", "DEF-f_middle.02.L": "LeftHandMiddle2", "DEF-f_middle.03.L": "LeftHandMiddle3", | |
| "DEF-f_ring.01.L": "LeftHandRing1", "DEF-f_ring.02.L": "LeftHandRing2", "DEF-f_ring.03.L": "LeftHandRing3", | |
| "DEF-f_pinky.01.L": "LeftHandPinky1", "DEF-f_pinky.02.L": "LeftHandPinky2", "DEF-f_pinky.03.L": "LeftHandPinky3", | |
| "DEF-shoulder.R": "RightShoulder", "DEF-upper_arm.R": "RightArm", "DEF-forearm.R": "RightForeArm", "DEF-hand.R": "RightHand", | |
| "DEF-thumb.01.R": "RightHandThumb1", "DEF-thumb.02.R": "RightHandThumb2", "DEF-thumb.03.R": "RightHandThumb3", | |
| "DEF-f_index.01.R": "RightHandIndex1", "DEF-f_index.02.R": "RightHandIndex2", "DEF-f_index.03.R": "RightHandIndex3", | |
| "DEF-f_middle.01.R": "RightHandMiddle1", "DEF-f_middle.02.R": "RightHandMiddle2", "DEF-f_middle.03.R": "RightHandMiddle3", | |
| "DEF-f_ring.01.R": "RightHandRing1", "DEF-f_ring.02.R": "RightHandRing2", "DEF-f_ring.03.R": "RightHandRing3", | |
| "DEF-f_pinky.01.R": "RightHandPinky1", "DEF-f_pinky.02.R": "RightHandPinky2", "DEF-f_pinky.03.R": "RightHandPinky3", | |
| "DEF-thigh.L": "LeftUpLeg", "DEF-shin.L": "LeftLeg", "DEF-foot.L": "LeftFoot", "DEF-toe.L": "LeftToeBase", | |
| "DEF-thigh.R": "RightUpLeg", "DEF-shin.R": "RightLeg", "DEF-foot.R": "RightFoot", "DEF-toe.R": "RightToeBase" | |
| } | |
| print("Bone mapping loaded.") | |
| # Rename original actions to [name]_old | |
| for action in list(bpy.data.actions): | |
| if not action.name.endswith("_old"): | |
| action.name = f"{action.name}_old" | |
| # Retarget animations | |
| for action in bpy.data.actions: | |
| if not action.name.endswith("_old"): | |
| continue | |
| print("Retargeting:", action.name) | |
| new_action = action.copy() | |
| new_action.name = action.name.replace("_old", "") | |
| for fcurve in new_action.fcurves: | |
| path = fcurve.data_path | |
| for src_bone, tgt_bone in bone_map.items(): | |
| if f'pose.bones["{src_bone}"]' in path: | |
| fcurve.data_path = path.replace(src_bone, tgt_bone) | |
| if not arm_target.animation_data: | |
| arm_target.animation_data_create() | |
| arm_target.animation_data.action = new_action | |
| # Remove old actions | |
| for action in list(bpy.data.actions): | |
| if action.name.endswith("_old"): | |
| bpy.data.actions.remove(action) | |
| # Rotate armature and children 180 degrees around Z axis | |
| print("Rotating target armature and children...") | |
| bpy.ops.object.select_all(action='DESELECT') | |
| arm_target.select_set(True) | |
| for child in arm_target.children: | |
| child.select_set(True) | |
| bpy.context.view_layer.objects.active = arm_target | |
| for obj in [arm_target] + list(arm_target.children): | |
| obj.rotation_euler[2] += 3.14159 # 180 degrees in radians | |
| bpy.context.view_layer.objects.active = obj | |
| bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) | |
| # Delete source model | |
| print("Deleting source...") | |
| bpy.ops.object.select_all(action='DESELECT') | |
| arm_source.select_set(True) | |
| for child in arm_source.children: | |
| child.select_set(True) | |
| bpy.ops.object.delete() | |
| # Export target | |
| print("Exporting target with animations...") | |
| bpy.ops.object.select_all(action='DESELECT') | |
| arm_target.select_set(True) | |
| for child in arm_target.children: | |
| child.select_set(True) | |
| bpy.context.view_layer.objects.active = arm_target | |
| bpy.ops.export_scene.gltf(filepath=out_file, export_format='GLB', use_selection=True) | |
| print("DONE. Exported to:", out_file) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment