フリーで使用できる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で読み込むことが可能です。しかし、このサイトにあるように、そのまま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 + という名前で、新規BoneIndexを追加しました。
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だとわかります。
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になります。
// 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が途中で発生してしまいます。何かしらの改善が必要です。
float animTicks = mAnimation->GetAnimTicks(mAnimTime, mAnimIdx);
mAnimTicks = fmod(animTicks, mDuration - 1);
mAnimTicks++;