俺にもできた Godot 4.x

Godot 4 でゲームを作る試み

【Blender 4.3.2 add-on】リターゲット用にアニメをコンバイン「あるぼんC(ArBoM 0.9.0)」誕生

キャラコンですがキャラをMixamo からVRoid にします。で、リターゲットするにはfbx の中のアニメライブラリが必要という事が判明。私の方法では一つひとつアニメを出すので各fbx には一つしかアニメが無い! fbx を全部読み直しリタゲ用に書き出さなければならないという事が分かり、これはメンドクサイ。アニメをBlender 上でCombine 出来るよう、あるぼん(ArBoM)をリニューアル、「あるぼんC」誕生って感じです。

Mixamo Root のすごさが分かったよ・・・

0. リターゲットにはライブラリが必要?!

Mixamo からfbx を持って来て、アニメを抜き出してアニメ部分だけGodot で使っていたので、もうfbx はプロジェクトの中には無いんだよ! 1つは残ってるんだけど、1つしかないんだよ。しかも、入ってるアニメは1つなんだよ。またfbx をimport してリタゲ用にアニメを一つひとつ抜き出すのはメンドクサイんだよ!!

って事でリタゲする時に楽出来る様、Blender のadd-on「あるぼん」を1つのfbx にRoot 付でアニメをまとめられるようにしました。それが「あるぼんC」です。「あるぼんC」の「C」はCombine のC です。バージョンとしては「ArBoM 0.9.0」です。

VRoid へのリタゲが捗るわい。Mixamo の全てのアニメに対応できるわけではありませんが、大体はイケると思います。自分で使っている範囲ではイケてますので・・・ですが、想定外の使い方に関してはエラーが出ると思います。 (当たり前)

1. あるぼんCのコード

コードを先に貼っておきます。これも暫定的な公開です。問題があれば消します。使う場合は以下の利用規約を守って下さい。

<< 利用規約 >>

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

import bpy
import os
from bpy_extras.io_utils import ImportHelper

# シーン内のすべてのオブジェクトとアクションを削除
def delete_all_obj_and_act():
    # アクティブオブジェクトがあれば、オブジェクトモードにし、全削除
    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)

# 小数点フレーム消去処理
def remove_fractional_frames(fcurve):
    for keyframe_point in fcurve.keyframe_points[1:]:
        frame = keyframe_point.co[0]
        # 小数点フレームはremove
        if frame % 1 != 0:
            fcurve.keyframe_points.remove(keyframe_point)

# パネルクラス(専用UI)
class FBXImportPanel(bpy.types.Panel):
    bl_idname = "VIEW3D_PT_fbx_importer"
    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

        col = layout.column(align=True)
        col.label(text="0. delete all OBJ and ACT")
        col.operator("fbx.delete_all", text="Delete ALL")
        layout.row().separator()

        col = layout.column(align=True)
        col.label(text="1. import ONE fbx WITH mesh")
        col.operator("fbx.with_mesh", text="Import FBX WITH Mesh")
        layout.row().separator()

        col = layout.column(align=True)
        col.label(text="2. import fbx(es) WITHOUT mesh")
        col.operator("fbx.without_mesh", text="Import FBX WITHOUT Mesh")
        layout.row().separator()
        
        col = layout.column(align=True)
        col.label(text="3. add ROOT to each action")
        col.operator("fbx.add_root", text="Add ROOT")

# オペレータークラス(全オブジェクトとアクションを削除)
class DeleteAllOperator(bpy.types.Operator):
    bl_idname = "fbx.delete_all"
    bl_label = "Delete All"
    bl_description = "Delete all objects and actions"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        delete_all_obj_and_act()
        return {"FINISHED"}

# オペレータークラス(メッシュを含むFBXを1ファイルのみインポート)
class FBXWithMeshOperator(bpy.types.Operator, ImportHelper):
    bl_idname = "fbx.with_mesh"
    bl_label = "Import FBX WITH Mesh"
    bl_description = "Import a single FBX file with mesh"
    bl_options = {"REGISTER", "UNDO"}

    filename_ext = ".fbx"
    filter_glob: bpy.props.StringProperty(default="*.fbx", options={"HIDDEN"})
    files: bpy.props.CollectionProperty(type=bpy.types.PropertyGroup)

    def execute(self, context):
        # 2ファイル以上選択したらキャンセル
        if len(self.files) > 1:
            self.report({"ERROR"}, "Multiple files are selected. Please select only one file.")
            return {"CANCELLED"}
        # 既存のアクションがあればキャンセル
        if len(bpy.data.actions) > 0:
            self.report({"ERROR"}, "Existing actions found. Please delete all before importing.")
            return {"CANCELLED"}

        try:
            # 1ファイル読み込み
            bpy.ops.import_scene.fbx(filepath=self.filepath)
            imported_objects = bpy.context.selected_objects

            # メッシュをチェック
            meshes = [obj for obj in imported_objects if obj.type == "MESH"]
            if meshes:
                # メッシュがあれば成功
                self.report({"INFO"}, "FBX file imported successfully with mesh(es).")
                # インポートされたアクションの名前をファイル名に変更
                imported_action = bpy.data.actions[-1]
                imported_action.name = os.path.splitext(os.path.basename(self.filepath))[0]
                return {"FINISHED"}
            else:
                # メッシュが無ければ失敗
                self.report({"ERROR"}, "No meshes found in the imported FBX file.")
                # 全消し
                delete_all_obj_and_act()
                return {"CANCELLED"}

        except Exception as e:
            self.report({"ERROR"}, f"FBX import failed: {e}")
            return {"CANCELLED"}

# オペレータークラス(メッシュを持たないFBXをインポート(もしあったら削除してインポート))
class FBXWithoutMeshOperator(bpy.types.Operator, ImportHelper):
    bl_idname = "fbx.without_mesh"
    bl_label = "Import FBX WITHOUT Mesh"
    bl_description = "Import multiple FBX files without meshes"
    bl_options = {"REGISTER", "UNDO"}

    filename_ext = ".fbx"
    filter_glob: bpy.props.StringProperty(default="*.fbx", options={"HIDDEN"})
    files: bpy.props.CollectionProperty(type=bpy.types.PropertyGroup)

    def execute(self, context):
        # データ上にメッシュが無ければキャンセル
        if not any(obj.type == "MESH" for obj in bpy.data.objects):
            self.report({"ERROR"}, "No mesh found. Import FBX with mesh before.")
            return {"CANCELLED"}

        try:
            # ファイル読み込み(複数ファイルの読み込みに対応)
            directory = os.path.dirname(self.filepath)
            actions_before_import = set(bpy.data.actions.keys())  # インポート前のアクションを記録
            for file in self.files:
                filepath = os.path.join(directory, file.name)
                bpy.ops.import_scene.fbx(filepath=filepath)
                imported_objects = bpy.context.selected_objects

                # 新しく追加されたアクションを特定して名前を変更
                actions_after_import = set(bpy.data.actions.keys())  # インポート後のアクションを記録
                new_action_name = (actions_after_import - actions_before_import).pop()  # pop()で名前を取得
                new_action = bpy.data.actions.get(new_action_name)  # 名前からアクションを取得
                if new_action:
                    new_action.name = os.path.splitext(os.path.basename(filepath))[0]
                    actions_before_import.add(new_action.name)  # 名前を変更したアクションをリストに追加

                # 余計に入って来たものは削除
                for obj in imported_objects:
                    if obj:
                        bpy.data.objects.remove(obj, do_unlink=True)
            return {"FINISHED"}

        except Exception as e:
            self.report({"ERROR"}, f"FBX import failed: {e}")
            return {"CANCELLED"}

# オペレータークラス(全てのアクションにRootを追加する)
class AddRootOperator(bpy.types.Operator):
    bl_idname = "fbx.add_root"
    bl_label = "Add Root to Each Action"
    bl_description = "Add a root bone to each action"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        # ルートボーン名、ヒップボーン名、修正値を入れる
        root_bone_name = "Root"
        hips_bone_name = "mixamorig:Hips"
        y_fix_value = 0.0129317

        # Blender内の唯一のアーマチュアオブジェクトを取得(必ず在る!)
        for obj in bpy.data.objects:
            if obj.type == "ARMATURE":
                armature_obj = obj
                break

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

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

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

        # ポーズモードに切り替え
        bpy.ops.object.mode_set(mode="POSE")

        # すべてのアクションでやる(フェイクも含む)
        for action in bpy.data.actions:
            # アクションにヒップボーン名でグループを追加
            action.groups.new(name=root_bone_name)

            # アクションをアクティブにしてポーズを設定
            bpy.context.view_layer.objects.active = armature_obj
            armature_obj.animation_data.action = action

            # ルートボーンの位置を(0, 0, 0)に設定し、キーフレームを挿入
            pose_bone = armature_obj.pose.bones[root_bone_name]
            pose_bone.location = (0, 0, 0)
            pose_bone.keyframe_insert(data_path="location", frame=1, group=root_bone_name)

            # タイムラインのプレイヘッドを1フレーム目に移動
            bpy.context.scene.frame_set(1)
            # 1フレーム目のヒップボーンのX,Z位置を取得
            initial_hips_x_pos = armature_obj.pose.bones[hips_bone_name].location.x
            initial_hips_z_pos = armature_obj.pose.bones[hips_bone_name].location.z

            # アクション>グループ・ヒップボーン>locationチャンネルのfカーブを調べる
            channel = f'pose.bones["{hips_bone_name}"].location'
            fcurve_x = action.fcurves.find(channel, index=0)  # X座標のためindex=0
            fcurve_y = action.fcurves.find(channel, index=1)  # Y座標のためindex=1
            fcurve_z = action.fcurves.find(channel, index=2)  # Z座標のためindex=2

            # キーが打たれてたら修正(代表としてX座標deチェック)
            for keyframe_point in fcurve_x.keyframe_points[1:]:
                frame = keyframe_point.co[0]
                # 小数点フレームは処理をスキップ
                if frame % 1 != 0:
                    continue
                bpy.context.scene.frame_set(int(frame))
                # 指定フレームのヒップボーンの位置を取得
                original_hips_pos = armature_obj.pose.bones[hips_bone_name].location
                # ルートボーンの位置情報キーフレームを打つ(x,zを変える、yは0のまま)
                root_bone = armature_obj.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=root_bone_name)
                # ヒップボーンの位置情報キーフレームを打つ(x,zをinitial_~、yはfix)
                hips_bone = armature_obj.pose.bones[hips_bone_name]
                fix_y = original_hips_pos.z * 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=hips_bone_name)

            # 小数点フレーム消去処理
            remove_fractional_frames(fcurve_x)
            remove_fractional_frames(fcurve_y)
            remove_fractional_frames(fcurve_z)

        bpy.ops.pose.select_all(action="SELECT")
        return {"FINISHED"}

def register():
    bpy.utils.register_class(FBXImportPanel)
    bpy.utils.register_class(DeleteAllOperator)
    bpy.utils.register_class(FBXWithMeshOperator)
    bpy.utils.register_class(FBXWithoutMeshOperator)
    bpy.utils.register_class(AddRootOperator)

def unregister():
    bpy.utils.unregister_class(FBXImportPanel)
    bpy.utils.unregister_class(DeleteAllOperator)
    bpy.utils.unregister_class(FBXWithMeshOperator)
    bpy.utils.unregister_class(FBXWithoutMeshOperator)
    bpy.utils.unregister_class(AddRootOperator)

if __name__ == "__main__":
    register()

2. あるぼんCの使い方

★1. Blender のadd-on ですので、Blender が必要です。最新の4.3.2 を使ってください。

★2. アニメはあらかじめMixamo からDL しておいてください。基本となるアニメをひとつ「With Mesh」で、残りは「Without Mesh」でOK です。

  1. テキストファイルにコードをコピーして適切な名前を付けて(例えば、"ArBoM090.py"とか)保存。
  2. Blender のadd-on としてinstall。有効化。(← これが出来ない人はここで終了)
  3. [3D Viewport]で「ArBoM」という独立したタブをクリックしてUI を表示させる。(Nキーで右にタブ出るっしょ)
  4. まずは、[Delete All] ボタンでシーンにある余計なオブジェを削除。(Blender 長い人は初動で例の3つ のオブジェクトを消すのがデフォですかね。)
  5. 次に、[Import FBX WITH mesh] ボタンでメッシュ付のfbx を1つだけ選択してインポート。これがアニメのまとめ元になります。どんなアニメでもいいです。fbx はデスクトップにフォルダを作ってそこに移動させておくのがイイでしょう。深い階層まで探しに行くのは手間ですから。インポーターの窓はデフォでドキュメントフォルダなので、そこでもいいけど。
  6. 次は、[Import FBX WITHOUT mesh] ボタンでメッシュ無しのfbx を複数選択してインポート。当たり前ですがフォルダはまたげないです。こちらもデスクトップにフォルダを作ってそこにまとめて置いておくのがイイと思います。(実は内緒ですが、メッシュが付いててもちゃんとインポート出来ます。)
  7. 最後に、[Add Root] ボタンで全てのアクション(アニメ)にRoot を付けます。
  8. 確認の意味で[Timeline] を[Dope Sheet] の[Action Editor] にしてアクション名とか変えてもいいですね。ファイル名になってます。それとRoot がちゃんと付いてキーも打ってあるか確認出来るよう、[Pose Mode] 全選択で処理をぬけてますので気になったら見てください。
  9. 後は普通にBlender の[File] > [Export] > [FBX (.fbx)] でExport して完了です。Export 前にプレイヘッドを「1フレーム」の所に戻してください。
  10. なお、Export 設定は各人の使用方法によります。ちなみに私は「 [Armature], [Mesh] のみ選択 + [FBX Custom Scale] 」で出してます。

近いうちに、Tube にリタゲまで含めた使い方動画を出そうと思ってますが今回はこれで。

おっと、Root Bone, Hips Bone の名称が変だと思ったら、コードの以下の部分を探して変えてください。ストリングなので。

        # ルートボーン名、ヒップボーン名、修正値を入れる
        root_bone_name = "Root"
        hips_bone_name = "mixamorig:Hips"