Closed7

【Blender】物理アニメーションをボーンアニメーションに変換する

nekoconekoco

先日Houdinistの方とお話しした際、崩壊アニメーションをUnity(VRChat)に持ってくるには?という知見を聞かせてもらった
要約するにボーンアニメーションに変換して、アニメーション付きFBXで持ってきているようだ

そうするとBlenderでもやってみたくなった
そのための知見をまとめていく

nekoconekoco

座標の取得

リジットボディで動いた座標を取得する

ボーンから取得?

特に難しいことをしなくても、リジッドボディにボーンを追加してその動きをキーフレームに挿入すればいいのでは?

結果は不可だった
アニメーションを再生しても、メッシュは動くがボーンは動かなかった

スクリプトで取得

ハンドラーを使用することで、フレームが変わるたびにスクリプトを実行できるようだ

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)

ハンドラー追加→全フレームを再生→ハンドラー解除
のような処理を書くことで全位置情報を取得できる

nekoconekoco

ボーン生成

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

複数オブジェクトを選択して実行することで、各オブジェクトごとにボーンが設定される

nekoconekoco

キーフレーム挿入

ボーンのアニメーションを挿入するスクリプト

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フレーム目にボーンアニメーションのキーフレームが挿入された

nekoconekoco

完成

これまでのスクリプトを踏襲してスクリプトを作成

まずアーマチュアを作成する
次に、各フレームのオブジェクト位置と回転をボーンに適応し、キーフレームに挿入する
最後にオブジェクトを統合してリジッドボディを削除する

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

https://x.com/nekoco_vrc/status/1830992897627570424

注意点

原点の位置はオブジェクトの重心じゃないとダメ
オブジェクトの回転は0度じゃないとダメ
(回転させたいなら編集モードで頂点を回転させる)

nekoconekoco

アニメーションにベイクしてからボーンアニメーションに変換

Blenderではリジッドボディで計算した挙動をアニメーションのキーフレームに変換できる
https://zenn.dev/nekoco/scraps/959dc11e83aac7

作成したスクリプトの方法では、小分けにして変換する際リジットボディが再計算されてしまう
動きを確定させるために一度アニメーションに変換するとよい

その場合、スクリプトを改修する必要がある

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)

https://x.com/nekoco_vrc/status/1831241635445030920

変更点

オブジェクトの原点座標を計算で使用していたが、アニメーションすることで座標が更新されてしまうようになった
初めにコピーしてそれを使うようにした

ウェイトを塗ったままボーンを動かすと、アニメーションに影響が出てしまう
全部終わってからウェイトを塗るようにした

nekoconekoco

FBX出力

他記事でも書いていることだが、アニメーション付きFBXを出力する際は下記の設定をしておくとよい

このスクラップは2ヶ月前にクローズされました