Assimpを使ってUnityChanを描画する

フリーで使用できるFBXモデルで代表的なものに、UnityChanがあります。これをC++で読み込んで、OpenGLで描画するプログラムを作成しました。


# UnityChanをAssimpで読み込む
## Assimpで読み込めない問題
UnityChanをそのままAssimpのImporterで読み込ませると、下記のようなエラーが発生し、読み込むことができません。
Error parsing './unitychan.fbx': 'FBX-Parser (TOK_DATA, offset 0x192452) failed to parse ID, unexpected data type, expected L(ong) 
(binary)'

これの対策として、いくつか方法が考えられます。
例えば、Blenerにインポートして、エクスポートすれば、Assimpで読み込むことが可能です。しかし、[このサイト](https://bluebirdofoz.hatenablog.com/entry/2019/04/25/091709)にあるように、そのままUnityChanを読み込むと、Boneの配置が崩れたり、顔の位置がずれたりなど、Blender内での調整が必要となります。

ここでは、FBX SDKのConverterを活用しました。

## UnityChanをAssimpで読めるようにする
FBX SDKをインストールすると、Sampleプログラムの中に、Sceneを変更して再出力するものがあります。
http://docs.autodesk.com/FBX/2014/ENU/FBX-SDK-Documentation/index.html?url=cpp_ref/_convert_scene_2main_8cxx-example.html,topicNumber=cpp_ref__convert_scene_2main_8cxx_example_htmlbd068b85-cbd6-4fa5-b5b7-274ab3b3ff24
このプログラムにunitychan.fbxを入力し、コンバートします。Assimpでは、「_fbx7binary.fbx」がファイルの末尾についたメッシュデータを読み込むことができます。




# UnityChanを描画する
## 顔にBoneを割り当てる
メッシュデータは、OGL DEVさんのソースコードで描画できます。しかし、顔にBoneが割り当てられていないため、Bone情報を反映させると、顔が描画されません(恐怖)。
そのため、新たにBoneを追加します。私の実装では、名前とBoneを関連付ける配列mBoneNameToIndexMapにmeshIdx + <meshIdx>という名前で、新規BoneIndexを追加しました。
```cpp:Skeleton.cpp
if (mesh->mNumBones == 0) {  // meshはconst aiMesh*型
        // Boneが割り当てられていないので、新たに作成
        int BoneIndex = (int)mBoneNameToIndexMap.size();
        std::string newBoneName = "meshIdx" + std::to_string(meshIdx);
        mBoneNameToIndexMap[newBoneName] = BoneIndex;

        for (int vertIdx = 0; vertIdx < mesh->mNumVertices; vertIdx++) {
            // 追加したBoneに関連づいた頂点Index
            unsigned int GlobalVertexID = baseVertex + vertIdx;
            mBones[GlobalVertexID].AddBoneData(BoneIndex, 1.f); // weight = 1.0
        }

        if (BoneIndex == mOffsetMatrices.size()) {
            mOffsetMatrices.push_back(glm::mat4(1.f));
        }

        printf("warn: this mesh does not assigned bone: %s, meshIdx: %d\n", mesh->mName.C_Str(), meshIdx);

        return true;
    }
```
この処理は元からすべてのMeshのBoneが割り当てられていたら発生しない処理です。なので、Blenderで正しく読み込めたなら、すべてのポリゴンにBoneを割り当てれば実装はもっと楽になります。

## 顔の位置も考慮したSkeletal Animationの再生
Assimpを用いたSkeletal Animationの再生では、Nodeを再帰的に走ることで読み出します。UnityChanをダウンロードすると、付属で走るアニメーションなどが付属されています。これは、Boenが割り当てられていない顔のMeshだけ、一緒に入っているので、Nodeを読んだときにMeshがあるNodeが、新規に追加したBoneだとわかります。
```cpp:Animation.cpp
    std::string NodeName(pNode->mName.data);
    for (int i = 0; i < pNode->mNumMeshes; i++) { // meshがある = 顔
        unsigned int meshIdx = pNode->mMeshes[i];
        std::string meshName = m_pScene->mMeshes[meshIdx]->mName.C_Str();
        std::string boneName = "meshIdx" + std::to_string(meshIdx);
        NodeName = boneName;
    }
```
BoneのTransformは基本的にNodeのTransformを使えばOKですので、OGL DEVさんのコードがそのまま活用できます。顔については、FBX SDKでコンバートするときに、データが壊れるのかわかりませんが、一部の顔のパーツのTransformが壊れるため、ハードコーディングで下記のような顔のTransformを与えています。ただし、まつ毛のEL_DEF Meshについては、顔に関連づいているため、単位行列のTransformになります。
```cpp
    // UnityChanの顔のBoneのTransform
    glm::mat4 meshMat = {
        {0.0157026369f, 0.0416036099f, 0.999011219f, 0.f},
        {0.990548074f, 0.136814535f, 0.00987200066f, 0.f},
        {-0.136268482f, 0.989723265f, -0.0433587097f, 0.f},
        {-128.945908f, -16.3280373f, -1.34681416f, 1.f}
    };
```
UniyChanのアニメーションでは、Bind poseも一緒に入っているので、それが描画されないようにする必要があります。Assimpで読み込んだ場合、Animation内の時刻が0から1までの期間がBind poseの状態を取ります。なので、以下のように書いて、0から1までの期間を除外しています。しかし、このように書いても、アニメーション間を移行するときになぜかBind poseが途中で発生してしまいます。何かしらの改善が必要です。
```cpp
        float animTicks = mAnimation->GetAnimTicks(mAnimTime, mAnimIdx);
        mAnimTicks = fmod(animTicks, mDuration - 1);
        mAnimTicks++;
```