これは Python Advent Calendar 2019 の7日目の記事です。
昨日は @ko-he-8 さんによる pythonのユニットテスト用ライブラリNoseのオプション紹介-19種類- でした。
Pythonのコードだけで3Dアニメーションを作りたい
Blenderのような3Dアニメーションソフトを使えば、便利なGUIで3Dアニメーションファイルを作ることができます!
そう!クリエイター・アニメーターならBlenderを使いましょう!!(え、Mayaがいい?それならMayaでどうぞ)
しかし!Qiitaの読者のほとんどは、プログラマーだと思います!
プログラマーなんだったら、プログラミングで3Dアニメーションを作りたいですよね!
いや、プログラミングで作るべきだ!
そんなあなたのために、 FBX SDK Python というものがあります!
これさえあれば! 大好きなPythonで3DアニメーションファイルFBXを生成することができるんです!
え?Pythonなんて別に好きじゃない?UnityならC#でFBXが作れる?
・・・
さぁ始めましょう!
今回の目標
以下のように、立方体が円運動するようなアニメーションを作ることが今回の目標です!
事前準備:FBX SDK Pythonをインストールする
まずはFBX SDK Pythonをインストールしましょう!
以前、いくつか記事を作成しましたので、ご自身の環境に合わせてご用意ください。
とりあえずサンプルコードを動かしてみる
まずは、サンプルコードを実際に動かしてみましょう!
以下にサンプルコードを置きましたので、ぜひダウンロードしてみてください。
GitHub / segurvita / fbx_sdk_python_sample
ダウンロードができたら、以下のコマンドをたたいてみましょう!
python generate_fbx/circle_anim.py resources/cube_ascii.fbx resources/moving_circle_cube_ascii.fbx
resources
フォルダーに moving_circle_cube_ascii.fbx
というファイルが生成されたと思います。
これが今回の成果物です!
Autodesk FBX Review というソフトで開けば、先ほどの動画のように、立方体が円運動している様子を眺めることができます!ぜひお試しください!
コマンド解説
さて、まだPythonについて一切解説していませんねw
まずは、以下のコマンドから解説をします。
python generate_fbx/circle_anim.py resources/cube_ascii.fbx resources/moving_circle_cube_ascii.fbx
generate_fbx/circle_anim.py
というのが、今回使ったPythonのソースコードです。その次に2つの引数があります。これはそれぞれ
-
入力ファイル:
resources/cube_ascii.fbx
-
出力ファイル:
resources/moving_circle_cube_ascii.fbx
となっています。
Python解説
次に generate_fbx/circle_anim.py
についてみていきましょう!
全体像はこんな感じですね!
import sys
import math
from fbx import *
fps = 30.0
rps = 0.5
def generate_anim_stack(scene):
# 関数
def generate_anim_layer(scene, anim_stack, node):
# 関数
def prot_circle(degree, time, curve_t_x, curve_t_y, curve_r_z):
# 関数
def move_circle(node, anim_base_layer):
# 関数
def get_ascii_format_id(manager):
# 関数
def main(obj_path, fbx_path):
# 関数
if __name__ == '__main__':
# get argument
args = sys.argv
if len(args) < 2:
print('Arguments are too short')
else:
main(args[1], args[2])
関数がいっぱいありますので、今回は、3Dアニメーションに関わる部分だけを解説していこうと思います。
main関数を見てみる
まずはmain関数ですね。アニメーションに関わる部分はこんな感じです。
def main(obj_path, fbx_path):
# 各種インスタンスを生成する
manager = FbxManager.Create()
scene = FbxScene.Create(manager, "fbxScene")
importer = FbxImporter.Create(manager, "")
exporter = FbxExporter.Create(manager, "")
# ファイルをシーンに読み込む
importer.Initialize(obj_path, -1)
importer.Import(scene)
# シーン内にアニメーションスタックを作る
anim_stack = generate_anim_stack(scene)
# ルートノード(シーン内で最上位のノード)を取得する
root_node = scene.GetRootNode()
# ルートノードがあれば
if (root_node):
# ルートの直下の子ノードの数だけループする
for i in range(root_node.GetChildCount()):
# 子ノードを取得する
node = root_node.GetChild(i)
# アニメーションレイヤーを作る
anim_base_layer = generate_anim_layer(scene, anim_stack, node)
# アニメーションカーブを作る
move_circle(node, anim_base_layer)
# シーンをファイルに書き込む
exporter.Initialize(fbx_path, get_ascii_format_id(manager))
exporter.Export(scene)
# インスタンスを破棄する
exporter.Destroy()
importer.Destroy()
scene.Destroy()
manager.Destroy()
聞きなれない用語がいくつかでてきましたね。
Scene : シーン
シーンとは、FBXファイルがもっている巨大な空間のようなものです。
このシーンの中に、アニメーションやメッシュといったさまざまなデータが格納されます。
(BlenderやUnityにもシーンという概念がありますが、それらとほとんど同じイメージでよいです!)
こんなコードで生成できます。
# シーンを作る
scene = FbxScene.Create(manager, "fbxScene")
Node : ノード
シーン内にはさまざまなデータがあります。その1つ1つがノードです。
ノードには親子関係があります。
たとえば、みなさん、自分の肩を回してみてください。
肩を回すと、肘の位置が変わりますよね?
つまり、 肘ノードは肩ノードの子 ってことです。子ノードは親ノードの影響を受けます。
反対に、肘だけを回しも、肩の位置は変わりませんよね?
親ノードは子ノードの影響は受けないんです。
FBXの場合、肘や肩といった関節もノードですし、アニメーションやメッシュ等もすべてノードになります。
Root Node : ルートノード
そんなノードの親をたどっていくと最終的に行き着くのが、ルートノードです。
シーンで最上位のノードですね!
こんなコードで取得できます。
# ルートノード(シーン内で最上位のノード)を取得する
root_node = scene.GetRootNode()
以下のように書けば、子ノードを順番に取得できます。
# ルートの直下の子ノードの数だけループする
for i in range(root_node.GetChildCount()):
# 子ノードを取得する
node = root_node.GetChild(i)
アニメーションスタックを作る
だんだん難しくなってきました・・・
アニメーションスタックというのは、アニメーションに関するデータをとりまとめるノードです。
これがないとアニメーションできません!
とりあえず、1つ作りましょう。
サンプルコードでは generate_anim_stack
という関数でつくっています。
def generate_anim_stack(scene):
# アニメーションスタックを作る
anim_stack = FbxAnimStack.Create(scene, "stack")
return anim_stack
これで作れます!
アニメーションレイヤーを作ろう
またまた謎の用語が・・・説明します。
3Dアニメーションといっても、いろんな動きがありますよね?
走る・歩く・ジャンプする・・・
そういった動き1つ1つをFBX用語では アニメーションレイヤー って呼ぶんです。(間違ってたらすみません)
今回は円運動をさせるだけなので、とりあえず1つ作りましょう!
def generate_anim_layer(scene, anim_stack, node):
# ノード(立方体)の名前を取得
node_name = node.GetName()
# ノード(立方体)の位置と回転を0にする
node.LclTranslation.Set(FbxDouble3(0.0, 0.0, 0.0))
node.LclRotation.Set(FbxDouble3(0.0, 0.0, 0.0))
# アニメーションレイヤーを作る(ノードの名前をつけて区別できるようにする)
anim_base_layer = FbxAnimLayer.Create(scene, "layer_" + node_name)
# アニメーションレイヤーをアニメーションスタック配下に加える
anim_stack.AddMember(anim_base_layer)
# アニメーションカーブノードを作る
anim_curve_node = node.LclTranslation.GetCurveNode(
anim_base_layer, True
)
# アニメーションレイヤーを返却する
return anim_base_layer
これでアニメーションレイヤーが作れます!
・・・
なにやら途中でアニメーションカーブノードという謎の用語が登場しましたね・・・
これは、ノード(ここでは立方体)の位置や回転という情報をアニメーションで変化させるために必要なデータです。とりあえず必要なので作りましょう。(私もよくわかってません。すみません。)
アニメーションカーブを作ろう
あぁ!また謎の用語が!
実はノード(立方体)をアニメーションさせると、ひと口に言っても、
- 位置を動かす
- 回転させる
- 大きさを伸縮させる
- 等など
さまざまなアニメーションがありますよね?
たとえば、今回のように円運動をさせたいのであれば、
- ノードの位置のx座標を動かす
- ノードの位置のy座標を動かす
- ノードの回転方向のz座標を動かす
みたいに3つの座標を動かすことになります。
この1つ1つが アニメーションカーブ です!
(BlenderやUnityのアニメーションカーブと概念はほとんど同じです)
今回は、3つのアニメーションカーブがあるんですね!
というわけで、以下がサンプルコードです。
def move_circle(node, anim_base_layer):
# ノードの位置のx座標のアニメーションカーブを作る
curve_t_x = node.LclTranslation.GetCurve(
anim_base_layer, "X", True
)
# ノードの位置のy座標のアニメーションカーブを作る
curve_t_y = node.LclTranslation.GetCurve(
anim_base_layer, "Y", True
)
# ノードの回転方向のz座標のアニメーションカーブを作る
curve_r_z = node.LclRotation.GetCurve(
anim_base_layer, "Z", True
)
# 時間記録用の変数を用意する
time = FbxTime()
# アニメーションカーブの記録を開始する
curve_t_x.KeyModifyBegin()
curve_t_y.KeyModifyBegin()
curve_r_z.KeyModifyBegin()
# fps = 30 (1秒で30フレーム)
# rps = 0.5 (1秒で円運動を半周)
# fps/rps = 60 (60フレームで円運動を1周)
# 60フレームを1フレーム毎に処理する。
for frame in range(int(fps / rps)):
# 経過時間を計算する。フレーム数/fps
sec = float(frame) / fps
# 経過時間を記録する
time.SetSecondDouble(sec)
# 回転角度を計算する。rps×経過時間×360°
degree = rps * sec * 360.0
# 回転角度に応じた値をアニメーションカーブにプロットする
prot_circle(degree, time, curve_t_x, curve_t_y, curve_r_z)
# アニメーションカーブの記録を終了する
curve_t_x.KeyModifyEnd()
curve_t_y.KeyModifyEnd()
curve_r_z.KeyModifyEnd()
細かい計算式が色々でてきましたが、1フレーム毎に経過時間を記録して、円運動の回転角度を計算していることがわかると思います。
実際にアニメーションカーブへ値を記録するところは、 prot_circle
という関数に記載しましたので、見ていきましょう。
アニメーションカーブにキーを追加する
アニメーションカーブに値を記録していくことを、 キーを追加する といったります。
以下が、キー追加処理のコードです。
def prot_circle(degree, time, curve_t_x, curve_t_y, curve_r_z):
# 回転角度をラジアンに変換する
radian = math.radians(degree)
# 位置のx座標は cos(radian) で計算して、キー追加する
key_index, key_last = curve_t_x.KeyAdd(time)
curve_t_x.KeySet(
key_index, time, math.cos(radian), FbxAnimCurveDef.eInterpolationLinear
)
# 位置のy座標は sin(radian) で計算して、キー追加する
key_index, key_last = curve_t_y.KeyAdd(time)
curve_t_y.KeySet(
key_index, time, math.sin(radian), FbxAnimCurveDef.eInterpolationLinear
)
# 回転方向のz座標はラジアンではなく度数のまま、キー追加する
key_index, key_last = curve_r_z.KeyAdd(time)
curve_r_z.KeySet(
key_index, time, degree, FbxAnimCurveDef.eInterpolationLinear
)
この関数が1フレーム毎に呼ばれることで、3つのアニメーションカーブに1フレーム毎、キーが追加されていきます。
キーを追加しているのはこの部分です。
key_index, key_last = curve_t_x.KeyAdd(time)
これでキーが追加されます。
Pythonに慣れていない人のために、念のため補足すると、 KeyAdd
という関数の戻り値が2つありまして、 key_index
と key_last
にそれぞれ代入さているというコードになります。
このように2つの変数を並べて書くことをタプルと読んだりします。
今回は、 key_index
に、キーの番号が代入されます。 key_last
は使いませんので無視しましょう。
次のコードで、この key_index
に対し、実際の値をセットしています。
curve_t_x.KeySet(
key_index, time, math.cos(radian), FbxAnimCurveDef.eInterpolationLinear
)
以上で、Pythonの解説はおしまいです!おつかれさまでした!
さいごに
ここまでお読みいただき、ありがとうございました。
これで、Blenderを使わずに、Pythonだけで3Dアニメーションを作ることができましたね!!
本記事作成にあたり、以下のページを参考にさせていただきました。