【Blender】物理アニメーションをボーンアニメーションに変換する
先日Houdinistの方とお話しした際、崩壊アニメーションをUnity(VRChat)に持ってくるには?という知見を聞かせてもらった
要約するにボーンアニメーションに変換して、アニメーション付きFBXで持ってきているようだ
そうするとBlenderでもやってみたくなった
そのための知見をまとめていく
座標の取得
リジットボディで動いた座標を取得する
ボーンから取得?
特に難しいことをしなくても、リジッドボディにボーンを追加してその動きをキーフレームに挿入すればいいのでは?
結果は不可だった
アニメーションを再生しても、メッシュは動くがボーンは動かなかった
スクリプトで取得
ハンドラーを使用することで、フレームが変わるたびにスクリプトを実行できるようだ
import bpy
def print_rigid_body_world_location_rotation(scene):
obj = bpy.context.active_object
location = obj.matrix_world.translation
rotation = obj.matrix_world.to_euler()
print(f"World Location: {location}")
print(f"World Rotation: {rotation}")
# フレームごとにスクリプトを実行
bpy.app.handlers.frame_change_post.clear() # 既存のハンドラーをクリア(必要に応じて)
bpy.app.handlers.frame_change_post.append(print_rigid_body_world_location_rotation)
ハンドラー追加→全フレームを再生→ハンドラー解除
のような処理を書くことで全位置情報を取得できる
ボーン生成
import bpy
from mathutils import Vector
# 選択したオブジェクトを取得
obj = bpy.context.active_object
# アーマチュアを作成
bpy.ops.object.armature_add(enter_editmode=True, align='WORLD', location=obj.location)
armature = bpy.context.active_object
bone = armature.data.edit_bones[0]
bone.name = f"{obj.name}_Bone"
bone.head = obj.location
bone.tail = obj.location + Vector((0, 0, 0.5))
# エディットモードからオブジェクトモードに戻る
bpy.ops.object.mode_set(mode='OBJECT')
# オブジェクトをアーマチュアにペアレント
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
armature.select_set(True)
bpy.context.view_layer.objects.active = armature
bpy.ops.object.parent_set(type='ARMATURE_NAME', keep_transform=True)
# 新しい頂点グループを作成
vertex_group = obj.vertex_groups.get(bone.name) or obj.vertex_groups.new(name=bone.name)
# すべての頂点にウェイト1を設定
vertices = [v.index for v in obj.data.vertices]
vertex_group.add(vertices, 1.0, 'REPLACE')
オブジェクトに対して実行することで、ウェイトの塗られたボーンを挿入することできる
複数オブジェクト対応
import bpy
from mathutils import Vector
selected_objects = bpy.context.selected_objects
bpy.ops.object.mode_set(mode='OBJECT')
# アーマチュアを作成
bpy.ops.object.armature_add(enter_editmode=True, align='WORLD')
armature = bpy.context.active_object
# 選択されたオブジェクトごとにボーンを追加
for obj in selected_objects:
bone = armature.data.edit_bones.new(f"{obj.name}_Bone")
bone.head = obj.location
bone.tail = obj.location + Vector((0, 0, 0.5))
bpy.ops.object.mode_set(mode='OBJECT')
# 選択されたオブジェクトをアーマチュアにペアレント
bpy.ops.object.select_all(action='DESELECT')
for obj in selected_objects:
obj.select_set(True)
armature.select_set(True)
bpy.context.view_layer.objects.active = armature
bpy.ops.object.parent_set(type='ARMATURE_NAME', keep_transform=True)
# 各オブジェクトに新しい頂点グループを作成
for obj in selected_objects:
vertex_group = obj.vertex_groups.get(f"{obj.name}_Bone") or obj.vertex_groups.new(name=f"{obj.name}_Bone")
# すべての頂点にウェイト1を設定
vertices = [v.index for v in obj.data.vertices]
vertex_group.add(vertices, 1.0, 'REPLACE')
複数オブジェクトを選択して実行することで、各オブジェクトごとにボーンが設定される
キーフレーム挿入
ボーンのアニメーションを挿入するスクリプト
import bpy
# アーマチュアを取得
armature = bpy.data.objects.get("アーマチュア")
# アーマチュアが選択されているか確認
bpy.context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='POSE')
# 指定したボーンを取得
bone = armature.pose.bones.get("立方体_Bone")
# 位置と回転を設定
bone.location = (1.0, 0.0, 0.0)
bone.rotation_euler = (0.0, 0.0, 0.0)
# キーフレームを挿入
frame = 0
bone.keyframe_insert(data_path="location", frame=frame)
bone.keyframe_insert(data_path="rotation_euler", frame=frame)
0フレーム目にボーンアニメーションのキーフレームが挿入された
完成
これまでのスクリプトを踏襲してスクリプトを作成
まずアーマチュアを作成する
次に、各フレームのオブジェクト位置と回転をボーンに適応し、キーフレームに挿入する
最後にオブジェクトを統合してリジッドボディを削除する
import bpy
from mathutils import Vector
def CreateArmature(objs):
# アーマチュアを作成
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.armature_add(enter_editmode=True, align='WORLD', location=Vector((0, 0, 0)))
armature = bpy.context.active_object
# 選択されたオブジェクトごとにボーンを追加
for obj in objs:
bone = armature.data.edit_bones.new(f"{obj.name}_Bone")
bone.head = obj.location
bone.tail = obj.location + Vector((0, 1, 0))
# 選択されたオブジェクトをアーマチュアにペアレント
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
for obj in objs:
obj.select_set(True)
armature.select_set(True)
bpy.context.view_layer.objects.active = armature
bpy.ops.object.parent_set(type='ARMATURE_NAME', keep_transform=True)
# 各オブジェクトに新しい頂点グループを作成
for obj in selected_objects:
vertex_group = obj.vertex_groups.get(f"{obj.name}_Bone") or obj.vertex_groups.new(name=f"{obj.name}_Bone")
# すべての頂点にウェイト1を設定
vertices = [v.index for v in obj.data.vertices]
vertex_group.add(vertices, 1.0, 'REPLACE')
return armature
def ProtKeyframe(scene):
for obj in selected_objects:
location = obj.matrix_world.translation
rotation = obj.matrix_world.to_euler()
bpy.context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='POSE')
bone = armature.pose.bones.get(f"{obj.name}_Bone")
bone.rotation_mode = 'XYZ'
bone.location = location - obj.location
bone.rotation_euler = rotation
bone.keyframe_insert(data_path="location")
bone.keyframe_insert(data_path="rotation_euler")
def JoinObjs(objs):
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
for obj in objs:
obj.select_set(True)
bpy.context.view_layer.objects.active = objs[0]
bpy.ops.object.join()
return objs[0]
selected_objects = bpy.context.selected_objects
armature = CreateArmature(selected_objects)
# フレームごとにスクリプトを実行
bpy.app.handlers.frame_change_post.append(ProtKeyframe)
for frame in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end + 1):
bpy.context.scene.frame_set(frame)
bpy.app.handlers.frame_change_post.clear()
bpy.context.scene.frame_set(0)
obj = JoinObjs(selected_objects)
# objに適応されているリジッドボディを削除
bpy.context.view_layer.objects.active = obj
bpy.ops.rigidbody.object_remove()
注意点
原点の位置はオブジェクトの重心じゃないとダメ
オブジェクトの回転は0度じゃないとダメ
(回転させたいなら編集モードで頂点を回転させる)
アニメーションにベイクしてからボーンアニメーションに変換
Blenderではリジッドボディで計算した挙動をアニメーションのキーフレームに変換できる
作成したスクリプトの方法では、小分けにして変換する際リジットボディが再計算されてしまう
動きを確定させるために一度アニメーションに変換するとよい
その場合、スクリプトを改修する必要がある
import bpy
from mathutils import Vector
def CreateArmature(objs):
# アーマチュアを作成
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.armature_add(enter_editmode=True, align='WORLD', location=Vector((0, 0, 0)))
armature = bpy.context.active_object
# 選択されたオブジェクトごとにボーンを追加
for obj in objs:
bone = armature.data.edit_bones.new(f"{obj.name}_Bone")
bone.head = obj.location
bone.tail = obj.location + Vector((0, 1, 0))
# 選択されたオブジェクトをアーマチュアにペアレント
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
for obj in objs:
obj.select_set(True)
armature.select_set(True)
bpy.context.view_layer.objects.active = armature
bpy.ops.object.parent_set(type='ARMATURE_NAME', keep_transform=True)
return armature
def ProtKeyframe(scene):
for obj, origin_location in zip(selected_objects, origin_locations):
location = obj.matrix_world.translation
rotation = obj.matrix_world.to_euler()
bpy.context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='POSE')
bone = armature.pose.bones.get(f"{obj.name}_Bone")
bone.rotation_mode = 'XYZ'
bone.location = location - origin_location
bone.rotation_euler = rotation
bone.keyframe_insert(data_path="location")
bone.keyframe_insert(data_path="rotation_euler")
def AdaptWeight(objs):
# 各オブジェクトに新しい頂点グループを作成
for obj in objs:
vertex_group = obj.vertex_groups.get(f"{obj.name}_Bone") or obj.vertex_groups.new(name=f"{obj.name}_Bone")
# すべての頂点にウェイト1を設定
vertices = [v.index for v in obj.data.vertices]
vertex_group.add(vertices, 1.0, 'REPLACE')
def JoinObjs(objs):
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
for obj in objs:
obj.select_set(True)
bpy.context.view_layer.objects.active = objs[0]
bpy.ops.object.join()
return objs[0]
selected_objects = bpy.context.selected_objects
origin_locations = [obj.location.copy() for obj in selected_objects]
armature = CreateArmature(selected_objects)
# フレームごとにスクリプトを実行
bpy.app.handlers.frame_change_post.append(ProtKeyframe)
for frame in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end + 1):
bpy.context.scene.frame_set(frame)
bpy.app.handlers.frame_change_post.clear()
bpy.context.scene.frame_set(0)
for obj in selected_objects:
if obj.animation_data:
for fcurve in obj.animation_data.action.fcurves:
if fcurve.data_path in ['location', 'rotation_euler']:
obj.animation_data.action.fcurves.remove(fcurve)
AdaptWeight(selected_objects)
obj = JoinObjs(selected_objects)
変更点
オブジェクトの原点座標を計算で使用していたが、アニメーションすることで座標が更新されてしまうようになった
初めにコピーしてそれを使うようにした
ウェイトを塗ったままボーンを動かすと、アニメーションに影響が出てしまう
全部終わってからウェイトを塗るようにした
FBX出力
他記事でも書いていることだが、アニメーション付きFBXを出力する際は下記の設定をしておくとよい