コンテンツにスキップ

参考4-1: スクリプトによるモーションブレンド

スクリプトによるモーション合成

モーション合成用のプログラムが仕込まれたテンプレートファイルをダウンロードしてください.

※ NLA Editorの方がインタラクティブに動作し,直観的なので基本はこちらを使えば良いです.
※ スクリプトでもキーフレーム情報を読み取って同様のことができることを確認する検証実験です.

初めにモーションを読み込む

初めに大元となるモーションを読み込んでください.

duplicate_armature.pyの実行

前回のプログラムと同じです.

duplicate_armature.py

import bpy
from mathutils import Vector
import numpy as np

def duplicate_armature(src_armature_name="Armature", dst_armature_name="DstArmature", 
                       with_mesh=False, translation=Vector((1.0, 0.0, 0.0))):
    armature = bpy.data.objects[src_armature_name]
    armature.select_set(True)

    if with_mesh:
        for child in armature.children:
            child.select_set(True)

    bpy.ops.object.duplicate()

    selected_objects = bpy.context.selected_objects

    for object in selected_objects:
        if object.type == "ARMATURE":
            object.name = dst_armature_name
            object.location += translation

src_armature_name = "Armature"
dst_armature_name = "DstArmature"
with_mesh = False
translation = Vector((1.0, 0.0, 0.0))

duplicate_armature(src_armature_name=src_armature_name, dst_armature_name=dst_armature_name, 
                   with_mesh=with_mesh, translation=translation)

DstArmatureが出来上がったら,Armatureを分かりやすい名前にリネームしましょう.

  • 例) Armature -> Walk

各種合成用のモーションの読み込み

モーションデータを読み込むと,Armatureとして読み込まれますので適宜リネームします.

  • 例) Armature -> Run

Rename

合成用のキーフレーム位置合わせ

  1. Object Modeで対象のモデルを選択
  2. Pose Modeに変更
  3. Aキーを押して関節を全選択
  4. Summaryモードにしてキーフレーム編集する

二つのモーション合成は,プログラム側で実行するので, ここでは横に移動してタイミングを調整するのが主な編集になります.

Rename

linear_blend.pyの実行

スクリプトを linear_blend.py に切り替えて編集します.

import bpy
from mathutils import Vector

def get_pose(bones, frame):
    bpy.context.scene.frame_set(frame)
    bpy.context.view_layer.update()
    pose = []
    for i, bone in enumerate(bones):
        location = bone.location.copy()
        scale = bone.scale.copy()
        rotation_quaternion = bone.rotation_quaternion.copy()
        pose.append([location, scale, rotation_quaternion])
    return pose

def get_offset_pose(A_poses_switch, B_poses_switch, B_bose):
    A_loc = A_poses_switch[0][0] 

    B_loc_offset = B_bose[0][0] - B_poses_switch[0][0]
    B_loc = A_loc + B_loc_offset
    B_bose[0][0] = B_loc

def set_pose(bones, pose, frame):
    bpy.context.scene.frame_set(frame)
    for i, bone in enumerate(bones):
        location, scale, rotation_quaternion = pose[i]
        if i == 0:
            print(f"Set: {frame}: {location}")
        bone.location = location
        bone.scale = scale
        bone.rotation_quaternion = rotation_quaternion

        bone.keyframe_insert(data_path="location", frame=frame, group=bone.name)
        bone.keyframe_insert(data_path="scale", frame=frame)
        bone.keyframe_insert(data_path="rotation_quaternion", frame=frame)

    bpy.context.view_layer.update()


def blend_pose(A_pose, B_pose, t):
    pose = []

    for i, (A_pose_i, B_pose_i) in enumerate(zip(A_pose, B_pose)):
        location = A_pose_i[0].lerp(B_pose_i[0], t)
        scale =  A_pose_i[1].lerp(B_pose_i[1], t)
        rotation_quaternion = A_pose_i[2].slerp(B_pose_i[2], t)

        pose.append([location, scale, rotation_quaternion])

    return pose


def linear_blend(A_armature_name, B_armature_name, dst_armature_name, 
                 A_start, A_end, B_start, B_end,
                 offset_mode=False):
    A_armature = bpy.data.objects[A_armature_name]
    B_armature = bpy.data.objects[B_armature_name]
    dst_armature = bpy.data.objects[dst_armature_name]
    A_bones = A_armature.pose.bones
    B_bones = B_armature.pose.bones
    dst_bones = dst_armature.pose.bones

    for frame in range(A_start, B_start):
        A_pose = get_pose(A_bones, frame)
        set_pose(dst_bones, A_pose, frame)

    A_pose_switch = get_pose(A_bones, B_start)
    B_pose_switch = get_pose(B_bones, B_start)

    for frame in range(B_start, A_end):
        t = (frame - B_start) / (A_end - B_start)

        print(f"{frame}: {t}")
        A_pose = get_pose(A_bones, frame)
        B_pose = get_pose(B_bones, frame)
        if offset_mode:
            get_offset_pose(A_pose_switch, B_pose_switch, B_pose)
        pose = blend_pose(A_pose, B_pose, t)
        set_pose(dst_bones, pose, frame)


    for frame in range(A_end, B_end+1):
        B_pose = get_pose(B_bones, frame)
        if offset_mode:
            get_offset_pose(A_pose_switch, B_pose_switch, B_pose)
        set_pose(dst_bones, B_pose, frame)


def step_blend(A_armature_name, B_armature_name, dst_armature_name, 
               A_start, B_start, B_end):
    linear_blend(A_armature_name, B_armature_name, dst_armature_name, 
                 A_start, B_start, B_start, B_end) 

## Work Area

linear_blend("Idle", "Walk", "DstArmature", A_start=1, A_end=20, B_start=10, B_end=40)
step_blend("Idle", "Walk", "DstArmature", A_start=1, B_start=20, B_end=40)

ここで,いじるのは,linear_blendとstep_blendの中身です.

linear_blend(A_armature_name, B_armature_name, dst_armature_name, 
                 A_start, A_end, B_start, B_end)

A_armature_nameとB_armature_nameのモーションを合成して dst_armature_nameにセットします.

例えば,

linear_blend("Idle", "Walk", "DstArmature", A_start=1, A_end=20, B_start=10, B_end=40)

の場合,以下の図のように,A_start, A_end, B_start, B_endでモーションをオーバーラップさせて合成します.

Linear Blend Setting

合成を簡単にするため, AとBのポーズはタイムライン上で表示されているものをそのまま合成します.

合成を調整したい場合は,合成用のAとBの各モーションのキーフレームのタイミングを事前に合わせておく必要があります.

Linear Blend

step_blend

step_blendでは,モーションを補間合成せずに,B_startの時点で急激に切り替えます.

step_blend("Idle", "Walk", "DstArmature", A_start=1, B_start=20, B_end=40)