Blenderを使わずにPythonだけで3Dアニメーションを作ってみよう！【FBX SDK Python】



![20191201_moving_circle_cube.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/417f7112-1767-104b-4291-3efb86bdd630.gif)


これは [Python Advent Calendar 2019](https://qiita.com/advent-calendar/2019/python) の7日目の記事です。
昨日は @ko-he-8 さんによる [pythonのユニットテスト用ライブラリNoseのオプション紹介-19種類-](https://qiita.com/ko-he-8/items/92f95fe3ee696b334713) でした。



# Pythonのコードだけで3Dアニメーションを作りたい

Blenderのような3Dアニメーションソフトを使えば、便利なGUIで3Dアニメーションファイルを作ることができます！

そう！クリエイター・アニメーターならBlenderを使いましょう！！（え、Mayaがいい？それならMayaでどうぞ）

しかし！Qiitaの読者のほとんどは、**プログラマー**だと思います！

**プログラマーなんだったら、プログラミングで3Dアニメーションを作りたいですよね！**

いや、プログラミングで作るべきだ！

そんなあなたのために、 **FBX SDK Python** というものがあります！

これさえあれば！ **大好きなPythonで3DアニメーションファイルFBXを生成することができるんです！**

え？Pythonなんて別に好きじゃない？UnityならC#でFBXが作れる？

・・・

**さぁ始めましょう！**



# 今回の目標

以下のように、立方体が円運動するようなアニメーションを作ることが今回の目標です！



![20191201_moving_circle_cube.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/417f7112-1767-104b-4291-3efb86bdd630.gif)



# 事前準備：FBX SDK Pythonをインストールする

まずはFBX SDK Pythonをインストールしましょう！

以前、いくつか記事を作成しましたので、ご自身の環境に合わせてご用意ください。

- [Windowsにインストールする](https://qiita.com/segur/items/5d0dff790e784760a547)
- [Macにインストールする](https://qiita.com/segur/items/e09d464b5e2ff0c8725e)
- [Google Colaboratoryにインストールする](https://qiita.com/segur/items/e04d887d960c8ece31d6)



# とりあえずサンプルコードを動かしてみる

まずは、サンプルコードを実際に動かしてみましょう！

以下にサンプルコードを置きましたので、ぜひダウンロードしてみてください。

[GitHub / segurvita / fbx_sdk_python_sample](https://github.com/segurvita/fbx_sdk_python_sample)



ダウンロードができたら、以下のコマンドをたたいてみましょう！

```bash
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](https://www.autodesk.com/products/fbx/fbx-review) というソフトで開けば、先ほどの動画のように、立方体が円運動している様子を眺めることができます！ぜひお試しください！



# コマンド解説

さて、まだPythonについて一切解説していませんねｗ

まずは、以下のコマンドから解説をします。

```bash
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`  についてみていきましょう！

全体像はこんな感じですね！

```python: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関数ですね。アニメーションに関わる部分はこんな感じです。

```python
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にもシーンという概念がありますが、それらとほとんど同じイメージでよいです！）

こんなコードで生成できます。

```python
# シーンを作る
scene = FbxScene.Create(manager, "fbxScene")
```



### Node : ノード

シーン内にはさまざまなデータがあります。その1つ1つがノードです。

ノードには親子関係があります。

たとえば、みなさん、自分の肩を回してみてください。

肩を回すと、肘の位置が変わりますよね？

つまり、 **肘ノードは肩ノードの子** ってことです。子ノードは親ノードの影響を受けます。

反対に、肘だけを回しも、肩の位置は変わりませんよね？

親ノードは子ノードの影響は受けないんです。

FBXの場合、肘や肩といった関節もノードですし、アニメーションやメッシュ等もすべてノードになります。



### Root Node : ルートノード

そんなノードの親をたどっていくと最終的に行き着くのが、ルートノードです。

シーンで最上位のノードですね！

こんなコードで取得できます。

```python
# ルートノード（シーン内で最上位のノード）を取得する
root_node = scene.GetRootNode()
```

以下のように書けば、子ノードを順番に取得できます。

```python
# ルートの直下の子ノードの数だけループする
for i in range(root_node.GetChildCount()):
    # 子ノードを取得する
    node = root_node.GetChild(i)
```



## アニメーションスタックを作る

だんだん難しくなってきました・・・

アニメーションスタックというのは、アニメーションに関するデータをとりまとめるノードです。

これがないとアニメーションできません！

とりあえず、1つ作りましょう。

サンプルコードでは `generate_anim_stack` という関数でつくっています。

```python
def generate_anim_stack(scene):
    # アニメーションスタックを作る
    anim_stack = FbxAnimStack.Create(scene, "stack")

    return anim_stack
```

これで作れます！



## アニメーションレイヤーを作ろう

またまた謎の用語が・・・説明します。

3Dアニメーションといっても、いろんな動きがありますよね？

走る・歩く・ジャンプする・・・

そういった動き1つ1つをFBX用語では **アニメーションレイヤー** って呼ぶんです。（間違ってたらすみません）

今回は円運動をさせるだけなので、とりあえず1つ作りましょう！

```python
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つのアニメーションカーブがあるんですね！

というわけで、以下がサンプルコードです。

```python
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 （１秒で30フレーム）
    # rps = 0.5 （１秒で円運動を半周）
    # fps/rps = 60 （60フレームで円運動を１周）
    # 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` という関数に記載しましたので、見ていきましょう。



## アニメーションカーブにキーを追加する

アニメーションカーブに値を記録していくことを、 **キーを追加する** といったります。

以下が、キー追加処理のコードです。

```python
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フレーム毎、キーが追加されていきます。

キーを追加しているのはこの部分です。

```python
key_index, key_last = curve_t_x.KeyAdd(time)
```

これでキーが追加されます。

Pythonに慣れていない人のために、念のため補足すると、 `KeyAdd` という関数の戻り値が2つありまして、 `key_index` と `key_last` にそれぞれ代入さているというコードになります。

このように2つの変数を並べて書くことをタプルと読んだりします。

今回は、 `key_index` に、キーの番号が代入されます。 `key_last` は使いませんので無視しましょう。

次のコードで、この `key_index` に対し、実際の値をセットしています。

```python
curve_t_x.KeySet(
    key_index, time, math.cos(radian), FbxAnimCurveDef.eInterpolationLinear
)
```

以上で、Pythonの解説はおしまいです！おつかれさまでした！



# さいごに

ここまでお読みいただき、ありがとうございました。

これで、Blenderを使わずに、Pythonだけで3Dアニメーションを作ることができましたね！！



本記事作成にあたり、以下のページを参考にさせていただきました。

- [Autodesk FBX SDK](http://help.autodesk.com/view/FBX/2019/ENU/)