FBX SDK(Python)でunityちゃんを解析し、WebGLでアニメーションする！(FBX解析編)

# FBX SDK(Python)でunityちゃんを解析し、WebGLでアニメーションする！(FBX解析編)

本記事は、[FBX SDK(Python)でunityちゃんを解析し、WebGLでアニメーションする！](https://qiita.com/kota017a/items/dd0fab59c06ca72dd3f6)の記事の、FBX SDKを用いた解析編の記事になります  

## FBX SDKのインストール

まずは、Python版のFBX SDKを取得して使えるようにしましょう
やり方は公式のHelpに書いています
[FBX SDK Help](https://help.autodesk.com/view/FBX/2019/ENU/?guid=FBX_Developer_Help_scripting_with_python_fbx_html)  

一応、簡単に手順を書いておきます
1. Python 2.7 をインストールする
1. [Autodeskのサイト](https://www.autodesk.com/products/fbx/overview?_ga=2.227847903.370728677.1589384616-1714689907.1531637071)から「FBX Python SDK」 をインストールする
1. FBX SDK のインストール先の「fbx.pyd」等のファイルを、Python 2.7のインストール先の「Lib\site-packages\」にコピーする

因みに、FBX SDK はPython 3.3版も提供されていますが、私の場合、Visual Studio CodeでのPython環境構築がうまく行かなったため、2.7版を使っています  

## unityちゃんのfbxを解析する

まず、unityちゃんのfbxを取得します  
これは、言うまでもないことかもしれませんが、[unityちゃんの公式サイト](https://unity-chan.com/)からダウンロードしてきて、Unityのプロジェクトにインポートすれば、fbxファイルを取得できます  

そして、FBXからの情報の取得ですが、実はほとんど、[「〇×(まるぺけ)つくろーどっとコム」さんの「FBX習得編」](http://marupeke296.com/FBX_main.html)のサイトを見ながらやればできます
Pythonを使っているとしても、使う関数などはほぼほぼ一緒なので当然ですね  

なので、詳細な手順などは説明しません。[「〇×(まるぺけ)つくろーどっとコム」さんの「FBX習得編」](http://marupeke296.com/FBX_main.html)のサイトを見てください
しかしながら、「SDKのバージョン違い」「Python版の使用」「unityちゃんの仕様」「WebGLの仕様」等によって発生する注意点があるので、そこだけ解説を加える形で説明していきます

サンプルコードを載せてしまいますので、コードを見たほうが理解しやすい人は確認していただければと思います　　

サンプルコードは、下記サイトも参考にしています
https://github.com/tody411/PyIntroduction/blob/master/fbx/load_file.md

<details><summary>unitychan.fbxのメッシュの情報を取得するソースコード</summary><div>

```python:getMeshInfo.py
from fbx import *

# 同一階層にunitychanのfbxがあることを想定
inputFolderName = "./" 
fbxFileName = "unitychan"

manager = FbxManager.Create()

scene = FbxScene.Create(manager, "Scene")

importer = FbxImporter.Create(manager, "myImporter")
if not importer.Initialize(inputFolderName + fbxFileName + ".fbx"):
    print("Fail: Create Importer ", inputFolderName + fbxFileName + ".fbx")
elif not importer.Import(scene):
    print("Fail: Import scene")

# 四角ポリゴンが混ざっているので、三角ポリゴンに変換する
geometryConverter = FbxGeometryConverter(manager)
geometryConverter.Triangulate(scene, True)

fbxJson = {}

fbxJson["meshInfo"] = {}

def dumpNodeInfo(node):
    attr = node.GetNodeAttribute()

    if(attr is not None):
        attrType = attr.GetAttributeType()
        if(attrType == FbxNodeAttribute.eMesh):
            meshJson = {}
            mesh = attr

            meshNode = mesh.GetNode()
            if(meshNode != 0):
                materialNum = meshNode.GetMaterialCount()
                if(materialNum != 0):
                    for materialIndex in range(materialNum):
                        material = meshNode.GetMaterial(materialIndex)
                        if (material != 0):
                            meshJson["materialName"] = material.GetName()
                            diffuseProperty = material.FindProperty(FbxSurfaceMaterial.sDiffuse)
                            # TODO python の FBX SDK だと、GetSrcObjectが使えないっぽいので、テクスチャのファイル名が取れない...
            boneInfoList = []

            skinCount = mesh.GetDeformerCount(FbxDeformer.eSkin)
            if skinCount == 0:
                # boneがない場合は、mesh自身の初期姿勢の情報を取る
                # 〇×さんのサイトで使われていたGetGlobalFromCurrentTake関数は無くなっているので注意！
                initMat = scene.GetAnimationEvaluator().GetNodeGlobalTransform(node, FbxTime(0), FbxNode.eSourcePivot, True, True)
                inverseInitMat = initMat.Inverse()
                # FBX SDKの行列は行オーダーなので、WebGLの列オーダーに合わせる
                inverseInitMatrix = [
                    inverseInitMat.Get(0, 0), inverseInitMat.Get(0, 1), inverseInitMat.Get(0, 2), inverseInitMat.Get(0, 3),
                    inverseInitMat.Get(1, 0), inverseInitMat.Get(1, 1), inverseInitMat.Get(1, 2), inverseInitMat.Get(1, 3),
                    inverseInitMat.Get(2, 0), inverseInitMat.Get(2, 1), inverseInitMat.Get(2, 2), inverseInitMat.Get(2, 3),
                    inverseInitMat.Get(3, 0), inverseInitMat.Get(3, 1), inverseInitMat.Get(3, 2), inverseInitMat.Get(3, 3),
                ]

                boneInfo = {}
                boneInfo["inverseInitMatrix"] = inverseInitMatrix
                boneInfo["linkNodeName"] = node.GetName()

                boneInfoList.append(boneInfo)
            else:
                skin = mesh.GetDeformer(0, FbxDeformer.eSkin)
                boneNum = skin.GetClusterCount()

                tempBoneWeightMatrix = {}
                for boneIndex in range(boneNum):
                    cluster = skin.GetCluster(boneIndex)

                    initMat = FbxAMatrix()
                    cluster.GetTransformLinkMatrix(initMat)
                    inverseInitMat = initMat.Inverse()
                    # FBX SDKの行列は行オーダーなので、WebGLの列オーダーに合わせる
                    inverseInitMatrix = [
                        inverseInitMat.Get(0, 0), inverseInitMat.Get(0, 1), inverseInitMat.Get(0, 2), inverseInitMat.Get(0, 3),
                        inverseInitMat.Get(1, 0), inverseInitMat.Get(1, 1), inverseInitMat.Get(1, 2), inverseInitMat.Get(1, 3),
                        inverseInitMat.Get(2, 0), inverseInitMat.Get(2, 1), inverseInitMat.Get(2, 2), inverseInitMat.Get(2, 3),
                        inverseInitMat.Get(3, 0), inverseInitMat.Get(3, 1), inverseInitMat.Get(3, 2), inverseInitMat.Get(3, 3),
                    ]

                    boneInfo = {}
                    boneInfo["inverseInitMatrix"] = inverseInitMatrix
                    # フレーム時姿勢は別のfbxファイルに格納されているので、ここではノード名だけを取得する
                    boneInfo["linkNodeName"] = cluster.GetLink().GetName()

                    boneInfoList.append(boneInfo)

                    pointNum = cluster.GetControlPointIndicesCount()
                    pointAry = cluster.GetControlPointIndices()
                    weightAry = cluster.GetControlPointWeights()

                    for pointIndex in range(pointNum):
                        index = pointAry[pointIndex]
                        weight = weightAry[pointIndex]

                        if not index in tempBoneWeightMatrix:
                            tempBoneWeightMatrix[index] = []

                        tempBoneWeightMatrix[index].append({"boneIndex": boneIndex, "weight": weight})


                # TODO weightの要素数を4以下にするために、フィルタリングをする(weight数に応じたシェーダが用意されているのが本来正しい？)
                maxBoneNum = 4
                for key in tempBoneWeightMatrix:
                    while(len(tempBoneWeightMatrix[key]) > maxBoneNum):
                        minValue = min(tempBoneWeightMatrix[key], key=lambda n: n["weight"])
                        tempBoneWeightMatrix[key] = filter(lambda n: n["weight"] > minValue["weight"], tempBoneWeightMatrix[key])

            meshJson["boneInfoList"] = boneInfoList

            layer0 = mesh.GetLayer(0)

            uv = layer0.GetUVs()
            src = mesh.GetControlPoints()
            polygonNum = mesh.GetPolygonCount()

            vertexTbl = []
            textureTbl = []

            boneIndexList = []
            weightList = []

            count = 0
            for polygonIndex in range(polygonNum):
                polygonVertexNum = mesh.GetPolygonSize(polygonIndex)
                for polygonVertexIndex in range(polygonVertexNum):
                    vertexIndex = mesh.GetPolygonVertex( polygonIndex, polygonVertexIndex)

                    vertexTbl.append(src[vertexIndex][0])
                    vertexTbl.append(src[vertexIndex][1])
                    vertexTbl.append(src[vertexIndex][2])

                    directIndex = uv.GetIndexArray().GetAt(count)
                    uvCoods = uv.GetDirectArray().GetAt(directIndex)
                    textureTbl.append(uvCoods[0])
                    # WebGlの座標系は「左下が原点」なので...
                    textureTbl.append(1.0 - uvCoods[1])

                    if skinCount > 0:
                        boneWeightList = tempBoneWeightMatrix[vertexIndex]
                        boneWeightList.sort(key=lambda n: n["boneIndex"])

                        maxBoneNum = 4
                        for boneWeightListIndex in range(maxBoneNum):
                            if boneWeightListIndex < len(boneWeightList):
                                boneWeightInfo = boneWeightList[boneWeightListIndex]
                                boneIndex = boneWeightInfo["boneIndex"]
                                weight = boneWeightInfo["weight"]

                                boneIndexList.append(boneIndex)
                                weightList.append(weight)
                            else:
                                # 使わない場合は、weightを0で初期化するので、boneIndexが0でも構わない
                                boneIndexList.append(0)
                                weightList.append(0)
                    else:
                        # boneがない場合は、0番目の w:1.0としてデータを構築する
                        # TODO ボーンのいらないメッシュは区別してシェーダ書いたりした方がいいかもしれない...

                        maxBoneNum = 4
                        for boneWeightListIndex in range(maxBoneNum):
                            if boneWeightListIndex == 0:
                                boneIndexList.append(0)
                                weightList.append(1.0)
                            else:
                                boneIndexList.append(0)
                                weightList.append(0)

                    count = count + 1

            meshJson["vertexTbl"] = vertexTbl
            meshJson["textureTbl"] = textureTbl
            meshJson["boneIndexList"] = boneIndexList
            meshJson["weightList"] = weightList

            fbxJson["meshInfo"][node.GetName()] = meshJson

    # 子nodeを探索し，再帰的にdumpする．
    num_childrens = node.GetChildCount()
    for ci in range(num_childrens):
        child_node = node.GetChild(ci)
        dumpNodeInfo(child_node)

# Root nodeの取得
root_node = scene.GetRootNode()

# 再帰的にnode情報を出力
dumpNodeInfo(root_node)

manager.Destroy()

```
</div></details>

<details><summary>unitychan_XXX.fbx等からアニメーションの情報を取得するソースコード</summary><div>

```python:getAnimationInfo.py
from fbx import *

# 同一階層にunitychan_DAMAGE00のfbxがあることを想定
inputFolderName = "./" 
fbxFileName = "unitychan_DAMAGE00"

manager = FbxManager.Create()

scene = FbxScene.Create(manager, "Scene")

importer = FbxImporter.Create(manager, "myImporter")
if not importer.Initialize(inputFolderName + fbxFileName):
    print("Fail: Create Importer ", inputFolderName + fbxFileName)
    continue
elif not importer.Import(scene):
    print("Fail: Import scene")
    continue

if importer.GetAnimStackCount() == 0:
    print "error :" + "no animation fbx"
    continue

# 〇×さんのサイトで使われていたGetGlobalTimeSettings関数は存在しないが,GetGlobalSettings関数から同様の情報は取れます
globalSettings = scene.GetGlobalSettings()
timeMode = globalSettings.GetTimeMode()
# 〇×さんのサイトとはTakeInfoの取り方が違いますが、これでとれます(GetAnimStackCountが1の前提)
currentTakeInfo = importer.GetTakeInfo(0)

start = currentTakeInfo.mLocalTimeSpan.GetStart()
stop = currentTakeInfo.mLocalTimeSpan.GetStop()
period = FbxTime()
period.SetTime( 0, 0, 0, 1, 0, timeMode)

frameNum = stop.Get() / period.Get()

# 四角ポリゴンが混ざっているので、三角ポリゴンに変換する
geometryConverter = FbxGeometryConverter(manager)
geometryConverter.Triangulate(scene, True)

fbxJson = {}

fbxJson["frameNum"] = frameNum
fbxJson["frameInfos"] = {}

def dumpNodeInfo(node):
    attr = node.GetNodeAttribute()

    if(attr is not None):
        attrType = attr.GetAttributeType()

        if(attrType == FbxNodeAttribute.eSkeleton or attrType == FbxNodeAttribute.eMesh):
            nodeName = node.GetName()

            frameInfo = []

            for frameIndex in range(frameNum):
                time = start + period * frameIndex
                # 〇×さんのサイトで使われていたGetGlobalFromCurrentTake関数は無くなっているので注意！
                mat = scene.GetAnimationEvaluator().GetNodeGlobalTransform(node, time, FbxNode.eSourcePivot, True, True)

                # FBX SDKの行列は行オーダーなので、WebGLの列オーダーに合わせる
                matrix = [
                    mat.Get(0, 0), mat.Get(0, 1), mat.Get(0, 2), mat.Get(0, 3),
                    mat.Get(1, 0), mat.Get(1, 1), mat.Get(1, 2), mat.Get(1, 3),
                    mat.Get(2, 0), mat.Get(2, 1), mat.Get(2, 2), mat.Get(2, 3),
                    mat.Get(3, 0), mat.Get(3, 1), mat.Get(3, 2), mat.Get(3, 3),
                ]

                frameInfo.append(matrix)

            fbxJson["frameInfos"][nodeName] = frameInfo
            
    # 子nodeを探索し，再帰的にdumpする．
    num_childrens = node.GetChildCount()
    for ci in range(num_childrens):
        child_node = node.GetChild(ci)
        dumpNodeInfo(child_node)

# Root nodeの取得
root_node = scene.GetRootNode()

# 再帰的にnode情報を出力
dumpNodeInfo(root_node)

manager.Destroy()

```
</div></details>


### 注意点1: unitychan.fbxには四角ポリゴンが混ざっている

unitychan.fbxには四角ポリゴンが混ざっています
WebGLでは、[四角形は描画できない](https://wgld.org/d/webgl/w006.html)ので、`FbxGeometryConverter.Triangulate`を用いて、三角ポリゴンに変換を掛けます
こちらの[サイト](https://codelabo.com/posts/20200228173514)で紹介されていました  

### 注意点2: materialのdiffusePropertyからテクスチャのファイルパスまでたどり着けない...([未解決](#未達成の内容について))

[未達成の内容について](https://qiita.com/kota017a/items/dd0fab59c06ca72dd3f6#%E6%9C%AA%E9%81%94%E6%88%90%E3%81%AE%E5%86%85%E5%AE%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)でも理由を記載していますが、解決していません...
マニュアルを読む限り、「PythonではC++のtemplate関数は提供しない」と記載されており、かつ、FBX SDK 2017において、templateを使わない版の関数が削除されたらしいです
つまり、マテリアルのディフーズプロパティに結びつけられたテクスチャを`GetSrcObject`関数で取得できません...  

[テンプレート関数はPythonで使えない](https://help.autodesk.com/view/FBX/2017/ENU/?guid=__files_GUID_363E55D8_6EDB_40F8_8F58_E42F8D525D3D_htm)
[テンプレート関数ではないClassId指定の関数が削除された](https://help.autodesk.com/view/FBX/2017/ENU/?guid=__files_GUID_2A50CD0F_062D_4F80_88C7_BBA0483FBE0F_htm)  

私のプログラムでは、以下のようなマテリアル名とテクスチャファイル名の対応表を手作業で用意して対応しました...  

```json
unityMeshMaterialTextureFileMap = {
    "body": "body_01",
    "eyeline": "eyeline_00",
    "skin1": "skin_01",
    "face": "face_00",
    "eye_R1": "eye_iris_R_00",
    "hair": "hair_01",
    "eye_L1": "eye_iris_L_00",
    "eyebase": "eyeline_00",
    "mat_cheek": "cheek_00",
}

```

どなたか解決策をご存じの方がいれば、是非教えてもらいたい...
(FBX SDKのフォーラムとかあるなら、問い合わせた方がいいかもしれない...)

### 注意点3: FBX SDK のGetGlobalFromCurrentTake関数が無くなっている

[「〇×(まるぺけ)つくろーどっとコム」さんの「FBX習得編」その8 位置の情報を取得する](http://marupeke296.com/FBX_No8_Position.html)で紹介されている`GetGlobalFromCurrentTake`関数ですが、FBX SDK 2017では削除されています  

[FBX SDKのヘルプページ](https://help.autodesk.com/view/FBX/2019/ENU/?guid=FBX_Developer_Help_welcome_to_the_fbx_sdk_what_new_fbx_sdk_2011_html)を確認すると、代わりに`FbxAnimEvaluator`を使うようにと書かれています  

[サンプルコード](https://help.autodesk.com/view/FBX/2019/ENU/?guid=FBX_Developer_Help_animation_evaluating_animation_in_scene_html)のページを見ると、`FbxNode.GetGlobalFromCurrentTake(KTime time)`と同等のものは、`FbxScene.GetAnimationEvaluator().GetNodeGlobalTransform(FbxNode node, KTime time)`で取得できるようです  

これで一安心...と思いきや、もう一つ落とし穴があります
実は、`GetNodeGlobalTransform`関数は、FbxNodeとKTimeを指定しただけの呼び出し方では、欲しい情報が得られません
正確な事を私も理解できていないのですが、正しく指定時間時のグローバル座標を得たい場合は、`pApplyTarget`とp`ForceEval`を`True`にする必要があります
(もしかしたら、どちらか片方だけでもいいかもしれない...)

`GetNodeGlobalTransform`関数の定義

```cpp
FbxAMatrix& GetNodeGlobalTransform	(	FbxNode * 	pNode,
const FbxTime & 	pTime = FbxTime((0x7fffffffffffffffLL)),
FbxNode::EPivotSet 	pPivotSet = FbxNode::eSourcePivot,
bool 	pApplyTarget = false,
bool 	pForceEval = false 
)
```

これで、`GetGlobalFromCurrentTake`関数が無くなっている問題は解決です  

### 注意点4: unitychan.fbxにはアニメーションの情報は含まれていない

[「〇×(まるぺけ)つくろーどっとコム」さんの「FBX習得編」その9 ボーンの情報を取得する](http://marupeke296.com/FBX_No9_Bone.html)の部分に関する注意点です  

unitychan.fbxには、アニメーションに関する情報が含まれていません
アニメーションに関する情報は、別のfbx(例：unitychan_DAMAGED00.fbx)に入っています  
逆にアニメーション用のfbxには、ボーンを持つメッシュの情報が含まれていません  

なので、unitychan.fbxからは、クラスタを保持しているノードの名前だけを取得しておき、アニメーション用のfbxからは、ボーンのフレーム時姿勢を取得する必要があります
ノードの名前を基に、ボーンのフレーム時姿勢との対応を取ることになります  

### 注意点5: unitychan.fbxには 4より多くのボーンの影響を受ける頂点が存在する

「スキンメッシュアニメーション」について調べると、1頂点に影響するボーンの最大数は「4」を想定されている解説が多いですが、unitychan.fbxでは、1頂点に影響するボーンの最大数は「6」個のメッシュが含まれていました...  

本来のゲーム開発ではこの時にどのように対処するのが適切なのかは分かりませんが、私の場合は、影響するボーンが4より多い場合は、影響度の小さいボーンを無視することで対応しました  

本来は、「メッシュ毎にボーン数の違う適切なシェーダを用意する」等の対応が必要なのかもしれません...(どうするのが正しいのでしょうか？)  

### 注意点6: unitychan.fbxにはボーン情報がないメッシュが存在する

unitychan.fbxの顔辺りのメッシュには、ボーンが設定されていません
アニメーション用のfbxの方では、ボーンが設定されていないメッシュに関しては、フレーム時姿勢が設定されています  

つまり、ボーンと同様に、「unitychan.fbxでのメッシュ自身の初期姿勢」「アニメーション用のfbxでのメッシュ自身のフレーム時姿勢」を取得すれば良いということです  

### 注意点7: FBX SDK の行列は行オーダー  

[FBX SDK の行列は「行オーダー」](https://help.autodesk.com/cloudhelp/2019/ENU/FBX-Developer-Help/cpp_ref/class_fbx_a_matrix.html)です
[WebGL(OpenGL)では行列は「列オーダー」](https://wgld.org/d/webgl/w013.html)です  

そのため、FBX SDK上のFBXAMatrixの内容を取得するときに、「列オーダー」に変換しておくと混乱が少なくて良いと思います  

### 注意点8: fbx上とWebGLでは、uv座標系が違う

uv座標をそのまま出力して、WebGLに持ってくると、テクスチャが正しく貼れませんでした  

[WebGL 開発支援サイト webgl.org テクスチャマッピング](https://wgld.org/d/webgl/w026.html)によると、「WebGL上では、座標系の上下が反転している」という記載があります
そこで、取得したuv座標のyを反転させてみた所、正しくテクスチャを張ることが出来ました  

### 注意点9: GetGlobalTimeSettings関数が存在しない

FBX SDKのhelpで検索しても出てこないので,
理由や経緯は分かりませんが、GetGlobalTimeSettings関数はFBX SDK 2017のバージョンには、存在しません  

ですが、GetGlobalSettings関数で同様のものが取れます  

### 注意点10: FbxSceneにGetTakeInfoが存在しない

[「〇×(まるぺけ)つくろーどっとコム」さんの「FBX習得編」その8 位置の情報を取得する](http://marupeke296.com/FBX_No8_Position.html)で紹介されている「テイク情報の取得」ですが、  FBX SDK 2017においては、その取得方法が異なる様です
具体的には、`FbxScene`クラスに、`GetTakeInfo`関数が存在しません  

FBX SDKのC++リファレンスで「GetTakeInfo」を検索すると、[FbxImporterクラスからTakeInfoを取得するコード](https://help.autodesk.com/cloudhelp/2019/ENU/FBX-Developer-Help/cpp_ref/_common_2_common_8cxx-example.html#a48)の例が見つかりました  

このコードのように、`FbxImporter.GetTakeInfo(int pIndex)`のように呼び出せば、`TakeInfo`が無事に取得できます  

## まとめ

以上、FBX SDK 2017(Python版)を使って、unitychan.fbxから、WebGLでスキンメッシュアニメーションを再生するために必要な情報の抽出方法を説明しました  

基本的には、[「〇×(まるぺけ)つくろーどっとコム」さんの「FBX習得編」](http://marupeke296.com/FBX_main.html)のサイト通りに進めれば目的は達成できるはずですが、本記事が対象とする環境において発生する幾つかの注意点を解説しています  

次は、抽出した情報を用いて、[WebGLでスキンメッシュアニメーション](https://qiita.com/kota017a/items/071c8b085c758c769758)させます

## 備考

本記事の内容を実際に私が作成した際のソースコードを一応GitHub公開していますので、必要であれば、ご確認ください  
[ソースコード](https://github.com/kota017a/WebGLTraining_FBXSkinMeshAnimationProject)