俺にもできた Godot 4.x

Godot 4 でゲームを作る試み

【Blender 4.3.2 add-on】あるぼん(ArBoM)アップデート(完成だけど完成度は95% !?)

あるぼん(ArBoM)アップデートです。一応完成としますが、完成度は95% と言ったところです。俺にもやっと全方向対応のRoot Bone を付ける事ができた感じです。結構完成まで早くて良かったね。あとはジャンプ・エンドのアニメの腰あたりとか、アイドル時の足滑りとか直したいですが、まー時間があったら対応しようと思います。

1. 「all move」がデフォです

Root Bone を付けるのに「zero move」・「forward-backward move」・「left-right move」・「all move」の4つのモードがありますが、「all move」をデフォルトにしました。基本これで全部イケます。妙な横揺れや前後移動が入って見た目が気に入らない時に、他のモードを試す感じにして下さい。(妙な動きが入ってると酔うよね。)

2. あるぼん(ArBoM)コード

バージョン 0.7.2 公開。一応完成版。暫定的な公開です。問題があれば消します。使う場合は以下の利用規約を守って下さい。

<< 利用規約 >>

  1. 何が起きても作者は責任を取りません。
  2. これ自体を販売しないで下さい。
  3. コピーライトは作者(俺)にあります。
  4. もっといいやり方があったら逆に(作者に)教えること。
bl_info = {
    "name": "Add Root Bone to Mixamo",
    "author": "honto2kansei",
    "version": (0, 7, 2),
    "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():
    # アクティブオブジェクトがあれば、オブジェクトモードにし、全削除
    if bpy.context.active_object:
        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

    # アクションのフレームを取得し「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)

    return True

# ルートボーンとヒップボーンを移動(左右移動用)
def change_root_hip_x_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フレーム目のヒップボーンのx位置を取得
    initial_hips_x_pos = armature.pose.bones[hips_bone_name].location.x

    # アクションのフレームを取得し「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

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

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

    return True

# ルートボーンとヒップボーンを移動(前後左右移動用)
def change_root_hip_xz_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フレーム目のヒップボーンのx,z位置を取得
    initial_hips_x_pos = armature.pose.bones[hips_bone_name].location.x
    initial_hips_z_pos = armature.pose.bones[hips_bone_name].location.z

    # アクションのフレームを取得し「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

        # ルートボーンの位置情報キーフレームを打つ(x,zを変える、yは0のまま)
        root_bone = armature.pose.bones[root_bone_name]
        root_bone.location.x = original_hips_pos.x - initial_hips_x_pos
        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)

        # ヒップボーンの位置情報キーフレームを打つ(x,zをinitial_~、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 = (initial_hips_x_pos, 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)

    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」の処理
# add_root_bone()
# change_root_hip_x_pos()
# change_to_animation_workspace()
def add_root_bone_lr(ope):
    result, armature, action, root_bone_name, hips_bone_name = add_root_bone(ope)
    if result == False:
        return
    result = change_root_hip_x_pos(armature, action, root_bone_name, hips_bone_name, ope)
    if result == False:
        return
    change_to_animation_workspace()

# ドロップダウンメニュー「all move」の処理
# add_root_bone()
# change_root_hip_xz_pos()
# change_to_animation_workspace()
def add_root_bone_all(ope):
    result, armature, action, root_bone_name, hips_bone_name = add_root_bone(ope)
    if result == False:
        return
    result = change_root_hip_xz_pos(armature, action, root_bone_name, hips_bone_name, ope)
    if result == False:
        return
    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" or context.scene.bone_animation_enum == "ALL":
            row = layout.row()
            row.prop(context.scene, "y_fix_value")
        layout.operator(AddRootBoneOperator.bl_idname)
        layout.separator()
        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="ALL"
    )

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()

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

使用方法は変わっていません。以前の記事を参考に。