LoginSignup
0
0

More than 1 year has passed since last update.

Blenderでモーションを線形補間して120fpsデータに変換するスクリプト

Posted at

内容

モーションをより高いフレームレートにしたいとき、フレームごとのデータを編集する必要がありますが、そのためのコードと説明です。
Blenderではアニメーションデータをキーフレームから編集する方法が一般的らしいですが、内部のデータを扱うのは面倒なので、ボーンデータを編集して線形補間後のモーションを作成します。

Blenderのボーンの種類

Bone

Objectモードのボーン。Read Onlyなので、名前の変更やボーンの追加、削除ができません。

EditBone

Editモードのボーン。名前の変更やボーンの追加、削除ができますが、フレームごとのデータ編集はできません。

PoseBone

Poseモードのボーン。フレームごとのデータ編集ができます。




今回はフレームデータを編集するので、PoseBoneを使用します。

ボーン内のデータの種類

平行移動

  • xyzの3次元ベクトルでそれぞれの軸の平行移動を表します
  • 補間方法は線形補間
  • PoseBone.locationでアクセス可能

回転

  • 表現方法が複数あり、それぞれデータの次元が異なります
  • 補間方法は球面線形補間
  • アクセス方法は後述

スケール

  • xyzの3次元ベクトルでそれぞれの軸のスケールを表します
  • 補間方法は線形補間
  • PoseBone.scaleでアクセス可能

Blenderの回転の種類

Blenderの数学モジュールであるmathutilsでは、回転を表現するために以下の4つのデータ型が用意されていますが、補間に適するものはQuaternionとMatrixの2つです。

Euler

  • オイラー角、BVHファイルに保存されている3次元の回転データ表現
  • 補間方法が用意されていません
  • PoseBone.rotation_eulerでアクセス可能

Vector

  • これ自体は普通のベクトルですが、回転角と回転軸(3次元ベクトル)の4次元ベクトルで回転を表現できます(angle axis representation)
  • 通常の線形補間、球面線形補間の関数(lerp, slerp)がありますが、直交座標と極座標用の補間なので、今回の補間としては不適切です
  • PoseBone.rotation_angle_axisでアクセス可能

Quaternion

  • (w,x,y,z)の形式で保存されるクオータニオン(四元数)
  • 球面線形補間の関数(slerp)があります
  • PoseBone.rotation_quaternionでアクセス可能

Matrix

  • トランスフォーム行列(同次座標系の4次行列)で、データの性質上、平行移動・回転・スケールの情報が入っており、decompose()でそれぞれに分解できます
  • 補間の関数(lerp)があり、平行移動、回転、スケールの全てのデータを適切に線形補間できます
  • PoseBone.matrixでアクセス可能



長々書きましたが、今回は回転だけでなく平行移動も補間したいので、PoseBone.matrixを使用します。

コード例

  • 入力されたBVHファイルのモーションを線形補間して、120fpsに変換します
    (120fps以外の変換でも動作します)

  • 処理の流れとしては、毎フレームずつ更新すると元データが消えて参照できなくなるので、

    • 全フレームデータ取得(get_motion_data)
    • 補間
    • 全フレームデータ更新(set_motion_data)

    という手順で更新します

interp.py
import bpy
import os
import copy
import argparse


def init_scene():
    if bpy.context:
        scene = bpy.context.scene
        for object_ in scene.objects:
            bpy.data.objects.remove(object_, do_unlink=True)
    
    for action in bpy.data.actions:
        if action.users == 0:
            bpy.data.actions.remove(action)


def get_motion_data():
    matrices = []
    for f in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end+1):
        matrices_frame = []
        bpy.context.scene.frame_set(f)
        for posebone in bpy.context.object.pose.bones:
            matrices_frame.append(copy.deepcopy(posebone.matrix))
        matrices.append(matrices_frame)
    return matrices
    

def set_motion_data(matrices):
    for f, matrices_frame in enumerate(matrices):
        bpy.context.scene.frame_set(f+1)
        for i, posebone in enumerate(bpy.context.object.pose.bones):
            posebone.matrix = matrices_frame[i]
            posebone.keyframe_insert(data_path='location')
            posebone.keyframe_insert(data_path='rotation_euler')
            bpy.context.view_layer.update()


def interp_bvh(src_path, tgt_path, tgt_fps=120):        
    bpy.ops.import_anim.bvh(filepath=src_path, update_scene_fps=True)
    
    ## データ取得
    src_mats = get_motion_data()

    ## 補間
    src_fps = bpy.context.scene.render.fps
    src_time_step = 1.0 / float(src_fps) # 元データの1フレームにかかる時間(秒)
    tgt_time_step = 1.0 / float(tgt_fps) # 出力したいFPSでの1フレームにかかる時間(秒)
     
    frame_range = bpy.context.object.animation_data.action.frame_range
    total_time = float(frame_range[1] - frame_range[0]) / float(src_fps) # モーションの合計時間(補間前後で変化しない)
    
    tgt_mats = [] # 更新後のデータを入れるリスト
    current_time = 0.0 # ループ内で更新される現在時間
    while current_time <= total_time:
        prev_frame = int(current_time // src_time_step) # 現在時間の前のフレーム
        next_frame = prev_frame + 1 # 現在時間の後のフレーム
        ratio = current_time % src_time_step * src_fps # 補間の割合
        
        tgt_mats_frame = []
        for prev_matrix, next_matrix in zip(src_mats[prev_frame], src_mats[next_frame]):
            tgt_mats_frame.append(prev_matrix.lerp(next_matrix, ratio)) # 各フレームの全関節について補間
            
        tgt_mats.append(tgt_mats_frame)
        current_time += tgt_time_step # 出力用のタイムステップで現在時間を更新
        
    ## データ更新
    set_motion_data(tgt_mats)
    
    bpy.context.scene.render.fps = tgt_fps
    bpy.ops.export_anim.bvh(filepath=tgt_path)


if __name__ ==  '__main__':
    input_path = "入力するBVHファイルのパス"
    output_path = "出力するBVHファイルのパス"
    interp_bvh(input_path, output_path, 120)
    # コマンドライン実行したい場合 (blender -b -P interp.py -i "input.bvh" -o "output.bvh" -f 120) 
    # parser = argparse.ArgumentParser()
    # parser.add_argument('-i', '--input_path')
    # parser.add_argument('-o', '--output_path')
    # parser.add_argument('-f' '--fps', default=120)
    # args = parser.parse_args()
    # interp_bvh(args.input_path, args.output_path, args.fps)

補足説明

  • import時にupdate_scene_fps=Trueとすることで、シーンが入力ファイルのフレームレートに更新され、適切な補間の割合を計算できます。
  • データ取得時の注意点としては、PoseBone.matrixには行列の参照が入っており、配列にそのまま格納した後に参照するフレームを変更すると、先ほど格納した配列内のデータも変更されてしまうため、copy.deepcopy()を使用してこれを回避します。
  • データ更新時の注意点としては、PoseBoneに直接値を代入するだけではアニメーションデータを更新できないので、keyframe_insert()でアニメーションにPoseBoneのデータを挿入する必要があります。また、ボーンを更新するごとに、bpy.context.view_layer.update()で現在のシーンを更新します。毎フレーム毎ボーンでシーンを更新する必要があるので、その分処理速度は遅くなります。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0