
「Add Root Bone to Mixamo」はMixamo Root のようにMixamo のアニメーションにルートモーション用のボーンを入れるBlender add-on です。「ArBoM」と略して「あるぼん」と発音します。ルートモーション用のボーンが無い状態からルートモーション用のボーンが在る状態になるという事で、ボーンが在るから「あるぼん」です。どお? ネーミングセンス、俺、小林製〇超えできた?
1. 「あるぼん」のコード
完成度70% なので、機能しない部分ががあります。暫定的な公開です。問題があれば消します。使う場合は、以下の利用規約を守って使ってください。
<< 利用規約 >>
- 何が起きても作者は責任を取りません。
- これ自体を販売しないで下さい。
- コピーライトは作者(俺)にあります。
- もっといいやり方があったら逆に(作者に)教えること。
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. 「あるぼん」の使い方
- コピペして「.py」のファイルを作る。
- Blender の[Edit] > [Preferences...] > [Add-ons] からインストールし、有効にする。
- UI パネル「ArBoM」があるのを確認。([Delete All] を押すか、キューブ、ライト、カメラを消しとく。)
- Mixamo からアニメをImport。([File] > [Import] > [FBX (.fbx)])
- ルートボーンを(0, 0, 0) に固定したい時は[Tpye] ドロップダウンメニュー:[zero move]。
- ルートボーンがヒップボーンを追いかけるように前に進ませたい時は[forward-backward move]。
- (ドロップダウンメニュー[left-right move] と[all move] は機能してません。)← その後のUPDATE で機能するようになりました。
- [Add Root Bone] ボタンを押すとルートボーンが挿入される。
- Export は[File] > [Export] > [FBX (.fbx)] で。
- Export の設定は、オブジェクトは[Armature] と[Mesh]、スケールは[FBX Custom Scale] 。
- 次のアニメに取り掛かりたい時は、[Delete All] で全消ししてから新しいアニメをImport。
注意:
- 検索したいヒップボーンの名前や、入れたいルートボーンの名前を変えたかったら、それぞれの項目を変えてください。
- [forward-backward move] にしたときに出てくる[Y fix] はヒップが「1」進んだときにどれだけ下げるかの修正値です。(前の記事を参考に。)
- 「.glb (glTF 2.0)」で出すとズレるはず。たぶんズレる。Godot のufbx importer を信じて「.fbx」でExportする事。
チュートリアル動画を作ってもいいかもね。時間があったら。