Skip to content

Instantly share code, notes, and snippets.

@avramovic
Last active October 3, 2025 22:01
Show Gist options
  • Select an option

  • Save avramovic/2c50405b32dece773882693b0d251ad1 to your computer and use it in GitHub Desktop.

Select an option

Save avramovic/2c50405b32dece773882693b0d251ad1 to your computer and use it in GitHub Desktop.

Revisions

  1. avramovic revised this gist Oct 3, 2025. 1 changed file with 59 additions and 95 deletions.
    154 changes: 59 additions & 95 deletions retarget.py
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,8 @@
    import bpy, sys, os
    import bpy
    import sys
    import os

    # Parse arguments
    argv = sys.argv
    argv = argv[argv.index("--") + 1:]
    src_file = os.path.abspath(argv[0])
    @@ -18,131 +21,93 @@
    bpy.ops.import_scene.gltf(filepath=src_file)

    # Find source armature
    arm_source = None
    for obj in bpy.context.scene.objects:
    if obj.type == "ARMATURE":
    arm_source = obj
    break
    arm_source = next((obj for obj in bpy.context.scene.objects if obj.type == "ARMATURE"), None)
    if not arm_source:
    raise Exception("No armature in source file")

    raise Exception("No armature found in source file")
    print("Source armature:", arm_source.name)

    # Import target (mesh for retarget)
    # Import target
    print("Importing target...")
    bpy.ops.import_scene.gltf(filepath=tgt_file)

    arm_target = None
    for obj in bpy.context.scene.objects:
    if obj.type == "ARMATURE" and obj != arm_source:
    arm_target = obj
    break
    # 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 in target file")

    raise Exception("No armature found in target file")
    print("Target armature:", arm_target.name)

    # Bone map
    # Bone mapping
    bone_map = {
    # Root / Spine
    "DEF-hips": "Hips",
    "DEF-spine.001": "Spine",
    "DEF-spine.002": "Spine1",
    "DEF-spine.003": "Spine2",
    "DEF-neck": "Neck",
    "DEF-head": "Head",

    # Left arm
    "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",

    # Right arm
    "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",

    # Legs
    "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",
    "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.")

    print("Bone mapping:", bone_map)
    # 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"

    # Copy all animations
    # Retarget animations
    for action in bpy.data.actions:
    print("Copying action:", action.name)
    if not action.name.endswith("_old"):
    continue

    print("Retargeting:", action.name)
    new_action = action.copy()
    new_action.name = f"{action.name}_retarget"
    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 src_bone in path:
    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 source model
    # 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()

    # Exportuj only target
    # Export target
    print("Exporting target with animations...")
    bpy.ops.object.select_all(action='DESELECT')
    arm_target.select_set(True)
    @@ -151,5 +116,4 @@
    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)
  2. avramovic created this gist Oct 3, 2025.
    155 changes: 155 additions & 0 deletions retarget.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,155 @@
    import bpy, sys, os

    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 = None
    for obj in bpy.context.scene.objects:
    if obj.type == "ARMATURE":
    arm_source = obj
    break
    if not arm_source:
    raise Exception("No armature in source file")

    print("Source armature:", arm_source.name)

    # Import target (mesh for retarget)
    print("Importing target...")
    bpy.ops.import_scene.gltf(filepath=tgt_file)

    arm_target = None
    for obj in bpy.context.scene.objects:
    if obj.type == "ARMATURE" and obj != arm_source:
    arm_target = obj
    break
    if not arm_target:
    raise Exception("No armature in target file")

    print("Target armature:", arm_target.name)

    # Bone map
    bone_map = {
    # Root / Spine
    "DEF-hips": "Hips",
    "DEF-spine.001": "Spine",
    "DEF-spine.002": "Spine1",
    "DEF-spine.003": "Spine2",
    "DEF-neck": "Neck",
    "DEF-head": "Head",

    # Left arm
    "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",

    # Right arm
    "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",

    # Legs
    "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:", bone_map)

    # Copy all animations
    for action in bpy.data.actions:
    print("Copying action:", action.name)
    new_action = action.copy()
    new_action.name = f"{action.name}_retarget"

    for fcurve in new_action.fcurves:
    path = fcurve.data_path
    for src_bone, tgt_bone in bone_map.items():
    if 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 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()

    # Exportuj only 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)