俺にもできた Godot 4.x

Godot 4 でゲームを作る試み

【Blender 4.3.2 add-on】Mixamo アニメにルートボーンを入れる自作Blender add-on、「Add Root Bone to Mixamo」暫定公開中(完成度70%)

「Add Root Bone to Mixamo」はMixamo Root のようにMixamo のアニメーションにルートモーション用のボーンを入れるBlender add-on です。「ArBoM」と略して「あるぼん」と発音します。ルートモーション用のボーンが無い状態からルートモーション用のボーンが在る状態になるという事で、ボーンが在るから「あるぼん」です。どお? ネーミングセンス、俺、小林製〇超えできた?

1. 「あるぼん」のコード

完成度70% なので、機能しない部分ががあります。暫定的な公開です。問題があれば消します。使う場合は、以下の利用規約を守って使ってください。

<< 利用規約 >>

  1. 何が起きても作者は責任を取りません。
  2. これ自体を販売しないで下さい。
  3. コピーライトは作者(俺)にあります。
  4. もっといいやり方があったら逆に(作者に)教えること。
bl_info = {
    "name": "Add Root Bone to Mixamo Animation",
    "author": "honto2kansei",
    "version": (0, 6, 9),
    "blender": (4, 3, 2),
    "category": "Object",
}

import bpy

# ワークスペースをAnimationへ変更
def change_to_animation_workspace():
    for screen in bpy.data.screens:
        if screen.name == "Animation":
            bpy.context.window.workspace = bpy.data.workspaces[screen.name]

# シーン内のすべてのオブジェクトとアクションを削除
def delete_all_objects_and_actions():
    # オブジェクトモードにし、全オブジェクトの削除
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.select_all(action='SELECT')
    bpy.ops.object.delete()

    # アクションの削除
    for action in bpy.data.actions:
        bpy.data.actions.remove(action)

# ルートボーンを入れ1フレにキーを打つ
def add_root_bone(ope):
    # アーマチュアを取得。無ければreturn
    armature = None
    for obj in bpy.data.objects:
        if obj.type == "ARMATURE":
            armature = obj
            break
    if not armature:
        ope.report({"ERROR"}, "No armature found in the scene.")
        return False, None, None, None, None

    # アクションが無ければreturn
    if not bpy.data.actions:
        ope.report({"ERROR"}, "No action found in the scene.")
        return False, None, None, None, None
    action = bpy.data.actions[0]

    # ルートボーン名とヒップボーン名をUIから取得
    root_bone_name = bpy.context.scene.root_bone_name
    hips_bone_name = bpy.context.scene.hips_bone_name

    # アーマチュアを選択しエディットモードに入る
    bpy.context.view_layer.objects.active = armature
    bpy.ops.object.mode_set(mode="EDIT")

    #UIから取得したルートボーン名のボーンがアーマチュア内にすでに有ればreturn
    if root_bone_name in armature.data.edit_bones:
        ope.report({"ERROR"}, f"Bone '{hips_bone_name}' already exists in the armature.")
        return False, None, None, None, None

    #UIから取得したヒップボーン名のボーンがアーマチュア内に無ければreturn
    if hips_bone_name not in armature.data.edit_bones:
        ope.report({"ERROR"}, f"Bone '{hips_bone_name}' not found in the armature.")
        return False, None, None, None, None

    # アーマチュアにルートボーンを追加
    root_bone = armature.data.edit_bones.new(root_bone_name)
    root_bone.head = (0, 0, 0)
    root_bone.tail = (0, 30, 0)

    # ルートボーンをヒップボーンの親にする
    hips_bone = armature.data.edit_bones[hips_bone_name]
    hips_bone.parent = root_bone

    # アクションにグループを追加
    if root_bone_name not in action.groups:
        new_group = action.groups.new(name=root_bone_name)

    # ポーズモードにし、ルートボーンの位置を(0,0,0)に指定
    bpy.ops.object.mode_set(mode="POSE")
    armature.pose.bones[root_bone_name].location = (0, 0, 0)

    # アクションを指定
    armature.animation_data.action = action

    # 位置情報をキーフレームとして1フレーム目に追加
    armature.pose.bones[root_bone_name].keyframe_insert(data_path="location", frame=1, group=new_group.name)

    return True, armature, action, root_bone_name, hips_bone_name

# ルートボーンとヒップボーンを移動(前後移動用)
def change_root_hip_z_pos(armature, action, root_bone_name, hips_bone_name, ope):
    # ヒップボーンのアクショングループ確認
    if hips_bone_name not in action.groups:
        ope.report({"ERROR"}, f"Group '{hips_bone_name}' not found in the action.")
        return False

    # タイムラインのプレイヘッドを1フレーム目に移動
    bpy.context.scene.frame_set(1)

    # 1フレーム目のヒップボーンのZ位置を取得
    initial_hips_z_pos = armature.pose.bones[hips_bone_name].location.z
    # print(f"Frame: 1, Hips Z Position: {initial_hips_z_pos}")

    # アクションのフレームを取得し「2」フレーム目から「最終」フレームまでまわす(+1は必須!)
    frame_range = range(2, int(action.frame_range[1]) + 1)
    for frame in frame_range:
        # タイムラインのプレイヘッドを移動
        bpy.context.scene.frame_set(frame)
        # 指定フレームのヒップボーンの位置を取得
        original_hips_pos = armature.pose.bones[hips_bone_name].location

        # ルートボーンの位置情報キーフレームを打つ(zのみ,xyは0のまま)
        root_bone = armature.pose.bones[root_bone_name]
        root_bone.location.z = original_hips_pos.z - initial_hips_z_pos
        root_bone.keyframe_insert(data_path="location", frame=frame, group=action.groups[root_bone_name].name)

        # ヒップボーンの位置情報キーフレームを打つ(zをinitial_hips_z_pos、xはオリジナル情報を残す、yはfix)
        hips_bone = armature.pose.bones[hips_bone_name]
        fix_y = original_hips_pos.z * float(bpy.context.scene.y_fix_value)
        hips_bone.location = (original_hips_pos.x, original_hips_pos.y - fix_y, initial_hips_z_pos)
        hips_bone.keyframe_insert(data_path="location", frame=frame, group=action.groups[hips_bone_name].name)

        # print(f"Frame: {frame}, Hips Position: {original_hips_pos}")

    return True

# ドロップダウンメニュー「zero move」の処理
# add_root_bone()
# change_to_animation_workspace()
def add_root_bone_zero(ope):
    result, armature, action, root_bone_name, hips_bone_name = add_root_bone(ope)
    if result == False:
        return
    change_to_animation_workspace()

# ドロップダウンメニュー「forward-backward move」の処理
# add_root_bone()
# change_root_hip_z_pos()
# change_to_animation_workspace()
def add_root_bone_fb(ope):
    result, armature, action, root_bone_name, hips_bone_name = add_root_bone(ope)
    if result == False:
        return
    result = change_root_hip_z_pos(armature, action, root_bone_name, hips_bone_name, ope)
    if result == False:
        return
    change_to_animation_workspace()

# ドロップダウンメニュー「left-right move」の処理
def add_root_bone_lr(ope):
    # ここに新しい関数のロジックを追加します
    change_to_animation_workspace()

# ドロップダウンメニュー「all move」の処理
def add_root_bone_all(ope):
    # ここに新しい関数のロジックを追加します
    change_to_animation_workspace()

# オペレータークラス(Add Root Bone)
class AddRootBoneOperator(bpy.types.Operator):
    bl_idname = "object.add_root_bone"
    bl_label = "Add Root Bone"
    bl_description = "Add root bone to action"

    def execute(self, context):
        match context.scene.bone_animation_enum:
            case "ZERO":
                add_root_bone_zero(self)
            case "FB":
                add_root_bone_fb(self)
            case "LR":
                add_root_bone_lr(self)
            case "ALL":
                add_root_bone_all(self)
            case _:
                self.report({"ERROR"}, "Unknown animation type selected.")
        return {"FINISHED"}

# オペレータークラス(Delete All)
class DeleteAllOperator(bpy.types.Operator):
    bl_idname = "object.delete_all"
    bl_label = "Delete All"
    bl_description = "Delete all object and action"

    def execute(self, context):
        delete_all_objects_and_actions()
        return {'FINISHED'}

# パネルクラス
class AddRootBonePanel(bpy.types.Panel):
    bl_idname = "VIEW3D_PT_add_root_bone"
    bl_label = "Add Root Bone to Mixamo"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "ArBoM"

    def draw(self, context):
        layout = self.layout
        row = layout.row()
        row.prop(context.scene, "hips_bone_name")
        row = layout.row()
        row.prop(context.scene, "root_bone_name")
        row = layout.row()
        row.prop(context.scene, "bone_animation_enum")
        if context.scene.bone_animation_enum == "FB":
            row = layout.row()
            row.prop(context.scene, "y_fix_value")
        layout.operator(AddRootBoneOperator.bl_idname)
        layout.operator(DeleteAllOperator.bl_idname)

def register():
    bpy.utils.register_class(AddRootBoneOperator)
    bpy.utils.register_class(AddRootBonePanel)
    bpy.utils.register_class(DeleteAllOperator)
    bpy.types.Scene.hips_bone_name = bpy.props.StringProperty(name="Hips", default="mixamorig:Hips")
    bpy.types.Scene.root_bone_name = bpy.props.StringProperty(name="Root", default="Root")
    bpy.types.Scene.y_fix_value = bpy.props.StringProperty(name="Y fix", default="0.0129317")
    bpy.types.Scene.bone_animation_enum = bpy.props.EnumProperty(
        name="Type",
        items=[
            ("ZERO", "zero move", ""),
            ("FB", "forward-backward move", ""),
            ("LR", "left-right move", ""),
            ("ALL", "all move", "")
        ],
        default="ZERO"
    )

def unregister():
    bpy.utils.unregister_class(AddRootBoneOperator)
    bpy.utils.unregister_class(AddRootBonePanel)
    bpy.utils.unregister_class(DeleteAllOperator)
    del bpy.types.Scene.hips_bone_name
    del bpy.types.Scene.root_bone_name
    del bpy.types.Scene.y_fix_value
    del bpy.types.Scene.bone_animation_enum

if __name__ == "__main__":
    register()

2. 「あるぼん」の使い方

  1. コピペして「.py」のファイルを作る。
  2. Blender の[Edit] > [Preferences...] > [Add-ons] からインストールし、有効にする。
  3. UI パネル「ArBoM」があるのを確認。([Delete All] を押すか、キューブ、ライト、カメラを消しとく。)
  4. Mixamo からアニメをImport。([File] > [Import] > [FBX (.fbx)])
  5. ルートボーンを(0, 0, 0) に固定したい時は[Tpye] ドロップダウンメニュー:[zero move]。
  6. ルートボーンがヒップボーンを追いかけるように前に進ませたい時は[forward-backward move]。
  7. (ドロップダウンメニュー[left-right move] と[all move] は機能してません。)← その後のUPDATE で機能するようになりました。
  8. [Add Root Bone] ボタンを押すとルートボーンが挿入される。
  9. Export は[File] > [Export] > [FBX (.fbx)] で。
  10. Export の設定は、オブジェクトは[Armature] と[Mesh]、スケールは[FBX Custom Scale] 。
  11. 次のアニメに取り掛かりたい時は、[Delete All] で全消ししてから新しいアニメをImport。

注意:

  1. 検索したいヒップボーンの名前や、入れたいルートボーンの名前を変えたかったら、それぞれの項目を変えてください。
  2. [forward-backward move] にしたときに出てくる[Y fix] はヒップが「1」進んだときにどれだけ下げるかの修正値です。(前の記事を参考に。)
  3. 「.glb (glTF 2.0)」で出すとズレるはず。たぶんズレる。Godot のufbx importer を信じて「.fbx」でExportする事。

チュートリアル動画を作ってもいいかもね。時間があったら。