2026年7月20日月曜日

How to transfer root bone pose animation to the animation of the entire armature 5.x

 import bpy

import math


from mathutils import Matrix

from bpy_extras import anim_utils



# ============================================================

# Settings

# ============================================================


ROOT_BONE_NAME = "Root"


# If True:

#   Bake a keyframe on every frame between the first and last

#   root bone keyframe.

#   This preserves interpolation more accurately, but creates

#   many additional keyframes.

#

# If False:

#   Process only frames that already contain root bone keys.

#   This behaves similarly to the original version of the script.

BAKE_EVERY_FRAME = False


# Delete the root bone after the transfer is complete.

DELETE_ROOT_BONE = True



# ============================================================

# Blender 5.x / Legacy Action Compatibility

# ============================================================


def get_action_fcurves(animated_id):

    """

    Return all F-Curves from the Action currently assigned

    to the specified animated datablock.


    Blender 4.x and earlier:

        action.fcurves


    Blender 5.x:

        Action Slot

            -> Channelbag

                -> F-Curves

    """


    animation_data = animated_id.animation_data


    if animation_data is None:

        return []


    action = animation_data.action


    if action is None:

        return []


    # Legacy Blender API.

    if hasattr(action, "fcurves"):

        return list(action.fcurves)


    # Blender 5.x layered Action API.

    action_slot = getattr(animation_data, "action_slot", None)


    # Normally animation_data.action_slot is available.

    # As a fallback, use the only slot if the Action has exactly one.

    if action_slot is None:

        slots = getattr(action, "slots", None)


        if slots is not None and len(slots) == 1:

            action_slot = slots[0]


    if action_slot is None:

        print(

            f"Warning: No Action slot was found for "

            f"'{animated_id.name}'."

        )

        return []


    channelbag = anim_utils.action_get_channelbag_for_slot(

        action,

        action_slot

    )


    if channelbag is None:

        return []


    return list(channelbag.fcurves)



def collect_keyframes(fcurves, data_path_prefix=None):

    """

    Collect all unique keyframe times from a list of F-Curves.


    If data_path_prefix is specified, only F-Curves whose

    data paths start with that prefix are included.

    """


    frames = set()


    for fcurve in fcurves:

        if (

            data_path_prefix is not None

            and not fcurve.data_path.startswith(data_path_prefix)

        ):

            continue


        for keyframe in fcurve.keyframe_points:

            frames.add(float(keyframe.co.x))


    return sorted(frames)



def set_scene_frame(scene, frame):

    """

    Set the current timeline position with subframe support,

    then update the dependency graph.

    """


    whole_frame = math.floor(frame)

    subframe = frame - whole_frame


    scene.frame_set(whole_frame, subframe=subframe)

    bpy.context.view_layer.update()



# ============================================================

# Main

# ============================================================


scene = bpy.context.scene


# Store the original timeline position so it can be restored later.

original_frame = scene.frame_current + scene.frame_subframe


# Use the currently active object as the source armature.

armature = bpy.context.active_object


if armature is None or armature.type != 'ARMATURE':

    raise ValueError("Please select an armature object.")


bpy.context.view_layer.objects.active = armature

armature.select_set(True)


# Switch to Object Mode before reading transforms.

if armature.mode != 'OBJECT':

    bpy.ops.object.mode_set(mode='OBJECT')



# ============================================================

# Validate Root Bone

# ============================================================


root_pose_bone = armature.pose.bones.get(ROOT_BONE_NAME)


if root_pose_bone is None:

    raise ValueError(

        f"Root bone '{ROOT_BONE_NAME}' was not found."

    )


root_data_bone = armature.data.bones.get(ROOT_BONE_NAME)


if root_data_bone is None:

    raise ValueError(

        f"Root data bone '{ROOT_BONE_NAME}' was not found."

    )



# ============================================================

# Store Root Bone Rest Transform

# ============================================================


armature_matrix_world = armature.matrix_world.copy()

root_bone_matrix_rest = root_data_bone.matrix_local.copy()


root_bone_matrix_world_rest = (

    armature_matrix_world

    @ root_bone_matrix_rest

)


(

    root_bone_default_loc,

    root_bone_default_rot,

    root_bone_default_scale

) = root_bone_matrix_world_rest.decompose()


inverse_root_bone_default_rot = (

    root_bone_default_rot.inverted()

)



# ============================================================

# Collect IK Targets

# ============================================================


ik_targets = []


for pose_bone in armature.pose.bones:

    for constraint in pose_bone.constraints:


        if constraint.type != 'IK':

            continue


        target = constraint.target


        if target is None:

            continue


        # Only process external IK target objects parented

        # to the selected armature.

        if target.parent != armature:

            continue


        # Prevent duplicate targets from being added.

        if target not in ik_targets:

            ik_targets.append(target)



# ============================================================

# Store Original IK Target World Positions

# ============================================================


ik_target_keyframes = {}


for target in ik_targets:

    target_fcurves = get_action_fcurves(target)

    target_frames = collect_keyframes(target_fcurves)


    ik_target_keyframes[target.name] = {}


    for frame in target_frames:

        set_scene_frame(scene, frame)


        ik_target_keyframes[target.name][frame] = (

            target.matrix_world.translation.copy()

        )



# ============================================================

# Get Armature Action

# ============================================================


animation_data = armature.animation_data


if animation_data is None:

    raise ValueError("Animation data was not found.")


action = animation_data.action


if action is None:

    raise ValueError("No Action is assigned to the armature.")


armature_fcurves = get_action_fcurves(armature)


if not armature_fcurves:

    raise ValueError(

        "No F-Curves were found in the armature Action."

    )



# ============================================================

# Collect Root Bone Keyframes

# ============================================================


root_data_path_prefix = (

    f'pose.bones["{ROOT_BONE_NAME}"]'

)


root_keyframes = collect_keyframes(

    armature_fcurves,

    root_data_path_prefix

)


if not root_keyframes:

    raise ValueError(

        f"No animation keys were found for "

        f"root bone '{ROOT_BONE_NAME}'."

    )


# Optionally bake every integer frame between

# the first and last root bone keyframe.

if BAKE_EVERY_FRAME:

    first_frame = math.floor(min(root_keyframes))

    last_frame = math.ceil(max(root_keyframes))


    sample_frames = [

        float(frame)

        for frame in range(first_frame, last_frame + 1)

    ]


else:

    sample_frames = root_keyframes



# ============================================================

# Sample Root Bone Transforms

#

# At this stage, transforms are only recorded.

# No keys are written to the armature object yet.

#

# This prevents newly inserted armature object keyframes

# from affecting transform evaluation on later frames.

# ============================================================


transform_data = {}


for frame in sample_frames:

    set_scene_frame(scene, frame)


    # PoseBone.matrix is expressed in armature object space.

    root_bone_matrix = root_pose_bone.matrix.copy()


    # Convert the root bone transform to world space.

    global_matrix = (

        armature.matrix_world

        @ root_bone_matrix

    )


    global_loc, global_rot, global_scale = (

        global_matrix.decompose()

    )


    transform_data[frame] = {

        "location": global_loc.copy(),

        "rotation": global_rot.copy(),

        "scale": global_scale.copy(),

    }



# ============================================================

# Bake the Root Bone Transform to the Armature Object

# ============================================================


bpy.ops.object.mode_set(mode='OBJECT')


# Quaternion rotation avoids Euler angle discontinuities

# during the transfer.

armature.rotation_mode = 'QUATERNION'


translation_matrix_to_default = Matrix.Translation(

    -root_bone_default_loc

)


for frame in sample_frames:

    data = transform_data[frame]


    loc = data["location"]

    rot = data["rotation"]

    scale = data["scale"]


    # Remove the root bone's rest rotation.

    corrected_rot = (

        rot

        @ inverse_root_bone_default_rot

    )


    translation_matrix_current = Matrix.Translation(loc)

    rotation_matrix = corrected_rot.to_matrix().to_4x4()


    # Apply the same transform order as the original script:

    #   1. Translate relative to the root bone's rest location.

    #   2. Apply the corrected root bone rotation.

    #   3. Move to the current root bone location.

    final_matrix = (

        translation_matrix_current

        @ rotation_matrix

        @ translation_matrix_to_default

    )


    final_loc, final_rot, _ = final_matrix.decompose()


    armature.location = final_loc

    armature.rotation_quaternion = final_rot

    armature.scale = scale


    armature.keyframe_insert(

        data_path="location",

        frame=frame

    )


    armature.keyframe_insert(

        data_path="rotation_quaternion",

        frame=frame

    )


    armature.keyframe_insert(

        data_path="scale",

        frame=frame

    )



# ============================================================

# Preserve IK Target World Positions

# ============================================================


for target in ik_targets:

    saved_frames = ik_target_keyframes.get(target.name, {})


    for frame, initial_world_position in saved_frames.items():

        set_scene_frame(scene, frame)


        # Convert the stored world-space position back into

        # the target's armature-relative local space using

        # the armature transform at the current frame.

        local_position = (

            armature.matrix_world.inverted()

            @ initial_world_position

        )


        target.location = local_position


        target.keyframe_insert(

            data_path="location",

            frame=frame

        )



# ============================================================

# Delete Root Bone

# ============================================================


if DELETE_ROOT_BONE:

    bpy.context.view_layer.objects.active = armature

    armature.select_set(True)


    bpy.ops.object.mode_set(mode='EDIT')


    edit_root_bone = armature.data.edit_bones.get(

        ROOT_BONE_NAME

    )


    if edit_root_bone is not None:

        armature.data.edit_bones.remove(edit_root_bone)


    bpy.ops.object.mode_set(mode='OBJECT')



# ============================================================

# Restore Original Timeline Position

# ============================================================


set_scene_frame(scene, original_frame)


print(

    f"Finished. Root bone animation has been transferred "

    f"to the armature object '{armature.name}'."

)


0 件のコメント:

コメントを投稿