内容
モーションをより高いフレームレートにしたいとき、フレームごとのデータを編集する必要がありますが、そのためのコードと説明です。
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)
という手順で更新します
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()で現在のシーンを更新します。毎フレーム毎ボーンでシーンを更新する必要があるので、その分処理速度は遅くなります。