2024年5月28日火曜日

How to transfer root bone pose animation to the animation of the entire armature

#RootPoseToArmature

import bpy

from mathutils import Matrix, Vector


# Note: This script will automatically set the armature's rotation mode to QUATERNION.

# The root bone will be deleted after the script runs.

# Make sure to back up your project before running this script.


# Set the name of the root bone

root_bone_name = "Root"  # Set the name of the root bone


# Get the currently selected object

armature = bpy.context.active_object

if not armature or armature.type != 'ARMATURE':

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


bpy.context.view_layer.objects.active = armature


# Switch to Object Mode to get the default pose

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


# Get the default global location and rotation of the root bone

root_bone = armature.pose.bones.get(root_bone_name)

if not root_bone:

    raise ValueError(f"Root bone '{root_bone_name}' not found.")


# Get the armature's rest pose

armature_matrix_world = armature.matrix_world.copy()

root_bone_matrix_rest = armature.data.bones[root_bone_name].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_matrix_world_rest.decompose()

inverse_root_bone_default_rot = root_bone_default_rot.inverted()


# Get the IK targets

ik_targets = []

for bone in armature.pose.bones:

    for constraint in bone.constraints:

        if constraint.type == 'IK' and constraint.target:

            target = constraint.target

            if target.parent == armature:

                ik_targets.append(target)


# Record the global positions of the IK targets for each keyframe

ik_target_keyframes = {}

for target in ik_targets:

    ik_target_keyframes[target.name] = {}

    if target.animation_data and target.animation_data.action:

        for fcurve in target.animation_data.action.fcurves:

            for keyframe in fcurve.keyframe_points:

                frame = int(keyframe.co.x)

                bpy.context.scene.frame_set(frame)

                ik_target_keyframes[target.name][frame] = target.matrix_world.translation.copy()


# Get the animation data

animation_data = armature.animation_data

if not animation_data:

    raise ValueError("Animation data not found.")


action = animation_data.action

if not action:

    raise ValueError("Action not found in animation data.")


# Process the animation data and apply the root bone transform to the armature

transform_data = {'location': [], 'rotation_quaternion': [], 'scale': []}

for fcurve in action.fcurves:

    if fcurve.data_path.startswith(f"pose.bones[\"{root_bone_name}\"]"):

        for keyframe in fcurve.keyframe_points:

            frame = int(keyframe.co.x)


            # Move to the frame to set the keyframe

            bpy.context.scene.frame_set(frame)


            # Get the pose position of the root bone

            root_bone_matrix = root_bone.matrix


            # Calculate the global transform of the root bone

            global_matrix = armature.matrix_world @ root_bone_matrix


            # Decompose the matrix into location, rotation, and scale

            global_loc, global_rot, global_scale = global_matrix.decompose()


            # Save the transform data in the dictionary

            transform_data['location'].append((frame, global_loc))

            transform_data['rotation_quaternion'].append((frame, global_rot))

            transform_data['scale'].append((frame, global_scale))


# Switch to Object Mode to apply transformations

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


# Automatically set the armature's rotation mode to QUATERNION

armature.rotation_mode = 'QUATERNION'


# Apply the transforms to the armature object and insert keyframes

for (frame, loc), (_, rot) in zip(transform_data['location'], transform_data['rotation_quaternion']):

    # Correct the rotation for the root bone's default rotation

    corrected_rot = (rot @ inverse_root_bone_default_rot)

    

    # Create the translation matrices for the default and current locations

    translation_matrix_to_default = Matrix.Translation(-root_bone_default_loc)

    translation_matrix_back = Matrix.Translation(root_bone_default_loc)

    translation_matrix_current = Matrix.Translation(loc)

    

    # Create the rotation matrix for the corrected rotation

    rotation_matrix = corrected_rot.to_matrix().to_4x4()

    

    # Combine the matrices: 

    # 1. Translate to default position

    # 2. Apply rotation

    # 3. Translate to the current location

    final_matrix = translation_matrix_current @ rotation_matrix @ translation_matrix_to_default

    

    # Decompose the final matrix into location and rotation

    final_loc, final_rot, _ = final_matrix.decompose()

    

    # Apply the final location and rotation to the armature

    armature.location = final_loc

    armature.rotation_quaternion = final_rot

    armature.keyframe_insert(data_path="location", frame=frame)

    armature.keyframe_insert(data_path="rotation_quaternion", frame=frame)


for frame, scale in transform_data['scale']:

    armature.scale = scale

    armature.keyframe_insert(data_path="scale", frame=frame)


# Update the IK target positions for each frame

for target in ik_targets:

    for frame, initial_position in ik_target_keyframes[target.name].items():

        bpy.context.scene.frame_set(frame)

        # Convert the initial position to the armature's local space at the current frame

        local_pos = armature.matrix_world.inverted() @ initial_position

        target.location = local_pos

        target.keyframe_insert(data_path="location", frame=frame)


# Delete the root bone

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

bones = armature.data.edit_bones

root_bone = bones.get(root_bone_name)

if root_bone:

    bones.remove(root_bone)


# Restore the current frame

bpy.context.scene.frame_set(bpy.context.scene.frame_current)


2024年4月30日火曜日

UnityでつけたポーズをBlenderに取り込む方法(もっと簡単な方法あるかも)

Unityのエディタでシーンビュー内でつけたポーズをBlenderにもっていく方法
1. Unity側のスクリプトをポーズを付けたFBXにアタッチしてレストポーズにしたFBXを
referenceModelとして設定する。
ポーズを付けたFBXのrootBoneと出力先のパスを設定する。
2. インスペクター上で右クリックでExportBoneData
3. Blender側のスクリプトでアーマチュアの名前と読み込むポーズのファイルのパスを設定して RunScript


↓Unity側

using System.Collections.Generic;

using UnityEngine;

using System.IO;


[System.Serializable]

public class BoneData

{

    public string name;

    public Vector3 position;

    public Vector3 scale;

    public Quaternion rotation; // This will be Q_relative

}


[System.Serializable]

public class BoneDataList

{

    public List<BoneData> bones = new List<BoneData>();

}


public class BoneDataExporter : MonoBehaviour

{

    public Transform rootBone;

    public GameObject referenceModel; // Add a field for the reference model

    public string boneDataPath = "Assets/boneData.json";



    [ContextMenu("Export Bone Data")]

    void ExportBoneData()

    {

        BoneDataList boneDataList = new BoneDataList();

        CollectBoneData(rootBone, boneDataList.bones, referenceModel.transform);

        string json = JsonUtility.ToJson(boneDataList, true);

        File.WriteAllText(boneDataPath, json);

        Debug.Log("Bone data exported!");

    }


    void CollectBoneData(Transform bone, List<BoneData> boneData, Transform reference)

    {


        // Find the same bone in the reference model by name

        Transform referenceBone = reference.FindChildRecursive(bone.name);

        Quaternion Q0 = referenceBone != null ? referenceBone.localRotation : Quaternion.identity;

        Quaternion Q = bone.localRotation;

        Quaternion Q0_inv= Quaternion.Inverse(Q0);

        Quaternion Q_relative =Q0_inv*Q;

        Quaternion Q_final = new Quaternion(Q_relative.x, -Q_relative.y, -Q_relative.z, Q_relative.w);


        boneData.Add(new BoneData

        {

            name = bone.name,

            position = bone.localPosition,

            scale = bone.localScale,

            rotation = Q_final // Store the relative rotation

        });


        foreach (Transform child in bone)

        {

            CollectBoneData(child, boneData, reference);

        }

    }

}


↓Blender側

import bpy

import json


def apply_pose(armature_name, json_path):

    with open(json_path, 'r') as f:

        data = json.load(f)


    armature = bpy.data.objects[armature_name]

    bpy.context.view_layer.objects.active = armature

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


    for bone_data in data['bones']:

        bone = armature.pose.bones.get(bone_data['name'])

        if bone:

            bone.location = (bone_data['position']['x'], bone_data['position']['y'], bone_data['position']['z'])

            bone.scale = (bone_data['scale']['x'], bone_data['scale']['y'], bone_data['scale']['z'])

            # 注意:四元数は (w, x, y, z) の順で設定

            bone.rotation_quaternion = (bone_data['rotation']['w'], bone_data['rotation']['x'], bone_data['rotation']['y'], bone_data['rotation']['z'])


    # Save the pose

    bpy.ops.object.posemode_toggle()

    bpy.ops.wm.save_mainfile()


# ファイルパスは適宜設定してください


apply_pose('RobinArmature', 'C:boneData.json')

    





2024年4月22日月曜日

Efficient Use of Blender Assets

-How to bake actions in Blender

http://eizouasobi.blogspot.com/2024/04/how-to-bake-actions-in-blender.html


-How to split actions in Blender

http://eizouasobi.blogspot.com/2024/04/how-to-split-actions-in-blender.html


-How to use in game engine

https://eizouasobi.blogspot.com/2019/01/how-to-use-in-game-engine.html


Formatting Animation Name Files for Use with ActionMaking.py

If the animation number files attached to your assets are not in the correct format for use with the ActionMaking.py script, please use the FormatAnimationNamesAnimColonNum.py script to adjust them.


This script performs the following tasks:

-Removes empty lines and trailing colons: This step cleans up unnecessary formatting that may interfere with processing.

-Replaces spaces with colons: This adjustment is made for lines where the frame range and animation name are separated by spaces instead of colons, ensuring consistency in data format.

-Checks and adjusts the position of name and frame data: This part verifies whether the line format is 'frames:name' or 'name:frames' and adjusts accordingly.

-Processes frame data: Depending on the number of listed frame ranges, the script selects the appropriate frames (the middle ones if there are four frames, the first two if there are three).

For example, a file with lines like:

50-80-110-140:WalkForward:

IdleToTrot:3850-3880-3910

will be changed to:

WalkForward:80-110

IdleToTrot:3850-3880

If there are any extraneous comments or notes that cannot be adjusted by this script, please remove them before running the script.

2024年4月20日土曜日

How to split actions in Blender

One of the ways to further utilize animation assets in Blender is to split actions. Here is a step-by-step guide on how to split actions.


Step 1: Baking the Animation

First, bake the animation in Blender. This involves solidifying the animation data and making any necessary adjustments such as removing root motion. For detailed instructions, please refer to this link.


Step 2: Preparing a Text File for Action Splitting

Prepare a text file in the following format to split actions:

WalkForward:10-40

WalkRight:50-80

If the format of the animation names in the text file differs (e.g., "10-40 WalkForward"), please adjust the format using the script provided at this link.


Step 3: Using the ActionMaking.py Script

Open the ActionMaking.py script from Blender’s text editor. Set the file_path to the path of the text file prepared in Step 2, and set the original_action_name to the name of the action you want to split.


Step 4: Running the Script

Select the target armature and run the Run Script. This will appropriately split the specified action based on the text file, making each action available as a separate item.


ActionMaking.py


import bpy


# Function to load animation names and frame ranges from a text file

def load_animation_data(file_path):

    with open(file_path, 'r') as file:

        animation_data = [line.strip().split(':') for line in file if line.strip()]

    return [(data[0], tuple(map(int, data[1].split('-')))) for data in animation_data]


# Function to duplicate the specified action and set up each copy as an independent animation clip

def duplicate_and_setup_actions(obj, original_action_name, animation_data):

    original_action = bpy.data.actions.get(original_action_name)

    if not original_action:

        print("Original action not found:", original_action_name)

        return


    for anim_name, (start_frame, end_frame) in animation_data:

        # Duplicate the action and assign a new name

        new_action = original_action.copy()

        new_action.name = anim_name


        # Trim the action's keyframes to the specified frame range

        for fcurve in new_action.fcurves:

            keyframe_points = [kp for kp in fcurve.keyframe_points if start_frame <= kp.co.x <= end_frame]

            fcurve.keyframe_points.clear()

            for kp in keyframe_points:

                fcurve.keyframe_points.insert(frame=kp.co.x - start_frame, value=kp.co.y)


        # Optionally set the new action to the object (for demonstration, not always necessary)

        obj.animation_data.action = new_action


# Main execution block

if __name__ == "__main__":

    file_path = "C:\BlenderAsset\AnimationNames1.txt"  # Set the file path appropriately

    obj = bpy.context.object  # Get the current object

    original_action_name = "Action"  # Name of the original action to duplicate


    # Load animation data from the file

    animation_data = load_animation_data(file_path)

    # Duplicate and set up actions

    duplicate_and_setup_actions(obj, original_action_name, animation_data)


How to bake actions in Blender

1.Select all bones in Pose Mode.

2.Go to the menu in the 3D Viewport and select Pose -> Animation -> Bake Action.

3.Ensure these five options are set before clicking OK:

-Set the Start Frame.

-Set the End Frame.

-Check the box for Visual Keying.

-Check the box for Clear Constraints.

-For Bake Data, select Pose.

This should consolidate everything into a single action.