MMD のデーターを読み込み、アニメーションさせるライブラリを作りました。
Saba
OpenGL を使用した簡易的なビューワーと、 obj へ変換する簡単なサンプルがあります。
サポートしているファイルタイプは以下です。
- PMD
- PMX
- VMD
ソフトボディ等いくつか対応していないものはありますが、Bullet を使用した物理シミュレーションは実装しています。
また、MMD とは表示結果が違うこともあるかと思います。
環境としては、 Windows 、 Linux 、 Mac で動作することは一応確認しています。
ビルド方法
GitHub からソースをクローンします。
(submodule を更新しなくてもよくなりました)
git clone https://github.com/benikabocha/saba.git
cd saba
各環境のビルド方法は、 README.md を参照してください。
saba_viewer へモデル(PMD、PMX)をドラッグアンドドロップしたあと、アニメーション(VMD)をドラッグアンドドロップしてください。
動作させると以下のようになります。
© 2017 Pronama LLC
Windows でのビルド方法
Windows でビルドする方法がわからないとの指摘があったので、手順を詳しく書きます。
環境は以下のような感じです。
- Windows10
- Visual Studio 2017
- CMake 3.7
説明では d:/dev
以下で行うので、適宜読み替えてください。
また、操作のほとんどはコマンドプロンプトで行います。
コマンドプロンプトの起動方法は、コルタナに「コマンドプロンプト」と聞けば教えてくれます。
コマンドプロンプトが起動したら、まずビルドディレクトリに移動します。
cd /d d:\dev
ソースコードの用意
d:\dev
以下に、ソースコードを用意します。
git clone -b 2.86.1 https://github.com/bulletphysics/bullet3.git
git clone -b 3.2.1 https://github.com/glfw/glfw.git
git clone https://github.com/benikabocha/saba.git
するとこんな感じになります。
Bullet のビルド
Bullet をビルドし、 d:\dev\library
へインストールします。
まず、 d:\dev\bullet3
内にビルドディレクトリを用意します。
cd bullet3
mkdir build
cd build
cmake を以下のような設定で実行します。
cmake -G "Visual Studio 15 2017 Win64" ^
-D CMAKE_INSTALL_PREFIX="d:\dev\library\bullet3" ^
-D INSTALL_LIBS=ON ^
-D USE_MSVC_RUNTIME_LIBRARY_DLL=On ^
-D BUILD_CPU_DEMOS=Off ^
-D BUILD_OPENGL3_DEMOS=Off ^
-D BUILD_BULLET2_DEMOS=Off ^
-D BUILD_UNIT_TESTS=Off ^
..
次に、各ターゲットのビルドとインストールを行います。
cmake --build . --config Debug --target ALL_BUILD
cmake --build . --config Debug --target INSTALL
cmake --build . --config Release --target ALL_BUILD
cmake --build . --config Release --target INSTALL
終わったら、 d:\dev
に戻ります。
cd ..\..
glfw のビルド
glfw をビルドし、 d:\dev\library
へインストールします。
まず、 d:\dev\glfw
内にビルドディレクトリを用意します。
cd glfw
mkdir build
cd build
cmake を以下のような設定で実行します。
cmake -G "Visual Studio 15 2017 Win64" ^
-D CMAKE_INSTALL_PREFIX="d:\dev\library\glfw" ^
-D GLFW_BUILD_EXAMPLES=Off ^
-D GLFW_BUILD_TESTS=Off ^
-D GLFW_BUILD_DOCS=Off ^
-D GLFW_INSTALL=On ^
..
次に、各ターゲットのビルドとインストールを行います。
glfw は Release のみです。
cmake --build . --config Release --target ALL_BUILD
cmake --build . --config Release --target INSTALL
終わったら、 d:\dev
に戻ります。
cd ..\..
ライブラリの確認
Bullet と glfw のビルド、インストールが終了した後の d:\dev\library
は以下のようになっています。
saba のビルド
saba のソリューションファイルを作成し、 Visual Studio で開きます。
まず、 d:\dev\saba
内にビルドディレクトリを用意します。
cd saba
mkdir build
cd build
cmake を以下のような設定で実行します。
cmake -G "Visual Studio 15 2017 Win64" ^
-D SABA_BULLET_ROOT="d:\dev\library\bullet3" ^
-D SABA_GLFW_ROOT="d:\dev\library\glfw" ^
..
d:\dev\saba\build\saba.sln
を Visual Studio で開きます。
スタートアッププロジェクトの変更
saba_viewer
をスタートアッププロジェクトに設定します。
実行
F5
キーでデバッガーをアタッチして実行することができます。
ライブラリの使い方
ライブラリの使い方として mmd2obj を参考に解説します。
mmd2obj の使い方
mmd2obj <pmd/pmx file> [-vmd <vmd file>] [-t <animation time(sec)>]
モデルのパスは必須です。
-vmd
で VMD ファイルのパスを指定します。
VMDを複数指定した際は、アニメーションをマージします。
-t
で変換するアニメーションの時間を指定します。
単位は秒です。
文字コード
Saba では文字列を UTF-8 として扱うようにしています。
PMD ファイルでは Shift-JIS で作られていますが、読み込む際に UTF-8 へ変換しています。
ファイル名等も UTF-8 として扱うので注意してください。
(Windows 版では main ではなく wmain を使用しています)
これは、Mac 、 Linux の考慮や、日本語以外の環境を考慮してのことです。
Windows ではデバッグ時に文字列が読めなくなりますが、今のところは諦めています。
座標系
MMD の座標系は左手座標ですが、 Saba では右手座標に変換しています。
モデルのロード
モデルは、 saba::MMDModel
を使用します。
// モデルの読み込み
std::shared_ptr<saba::MMDModel> mmdModel;
std::string mmdDataPath = ""; // MMDデータ (標準Toonテクスチャ) のパスを指定する
std::string ext = saba::PathUtil::GetExt(modelPath);
if (ext == "pmd")
{
auto pmdModel = std::make_unique<saba::PMDModel>();
if (!pmdModel->Load(modelPath, mmdDataPath))
{
std::cout << "Load PMDModel Fail.\n";
return false;
}
mmdModel = std::move(pmdModel);
}
else if (ext == "pmx")
{
auto pmxModel = std::make_unique<saba::PMXModel>();
if (!pmxModel->Load(modelPath, mmdDataPath))
{
std::cout << "Load PMXModel Fail.\n";
return false;
}
mmdModel = std::move(pmxModel);
}
mmdDataPath
は今回は使用しないので空です。
Saba ではマテリアル読み込み時に、モデルのパスからテクスチャーパスを作成します。
MMD のToonテクスチャーは、モデルのパスから作成することはできないので、mmdDataPath
を指定してください。
アニメーションのロード
アニメーションには saba::VMDAnimation
を使用します。
// アニメーションの読み込み
auto vmdAnim = std::make_unique<saba::VMDAnimation>();
if (!vmdAnim->Create(mmdModel))
{
std::cout << "Create VMDAnimation Fail.\n";
return false;
}
for (const auto& vmdPath : vmdPaths)
{
saba::VMDFile vmdFile;
if (!saba::ReadVMDFile(&vmdFile, vmdPath.c_str()))
{
std::cout << "Read VMD File Fail.\n";
return false;
}
if (!vmdAnim->Add(vmdFile))
{
std::cout << "Add VMDAnimation Fail.\n";
return false;
}
}
saba::VMDAnimation
は複数の VMD ファイルをマージすることができます。
アニメーションの反映
// Initialize pose.
{
// Sync physics animation.
mmdModel->InitializeAnimation();
vmdAnim->SyncPhysics((float)animTime * 30.0f);
}
// Update animation(animation loop).
{
// Update bone animation.
mmdModel->BeginAnimation();
vmdAnim->Evaluate((float)animTime * 30.0f);
mmdModel->UpdateAnimation();
mmdModel->EndAnimation();
// Update physics animation.
mmdModel->UpdatePhysics(1.0f / 60.0f);
// Update vertex.
mmdModel->Update();
}
アニメーションを行う前に一度だけ、 mmdModel->InitializeAnimation()
と vmdAnim->SyncPhysics()
を実行します。
mmdModel->InitializeAnimation()
は、アニメーションの初期化を行います。
vmdAnim->SyncPhysics()
は、目的フレームのポーズを反映し、物理を目的のポーズへ徐々に同期させます。
これは、物理を目的のポーズへ一気に反映させると、コリジョンを突き抜ける等の不具合が発生するため、30フレームかけて徐々に反映します。
この処理は後のアニメーション更新時には、呼ばないようにしてください。
アニメーションの更新は以下の繰り返しです。
- VMDアニメーションを評価
vmdAnim->Evaluate()
- ノードの更新
- Physics の更新
- 頂点の更新 (アニメーションを反映)
頂点の取得
Saba では頂点の変形処理を CPU で行っています。
更新後の頂点は、以下のようにして取得します。
const glm::vec3* positions = mmdModel->GetUpdatePositions();
const glm::vec3* normals = mmdModel->GetUpdateNormals();
const glm::vec2* uvs = mmdModel->GetUpdateUVs();
面のインデックスはファイルの種類により、要素サイズが変わるため、事前にチェックしてアクセスするようにしてください。
OBJ ファイルの出力
OBJ の出力は、OpenGL 等で表示する際の参考になるかと思います。
// OBJ へ書き出し
std::ofstream objFile;
objFile.open("output.obj");
if (!objFile.is_open())
{
std::cout << "Open OBJ File Fail.\n";
return false;
}
objFile << "# mmmd2obj\n";
objFile << "mtllib output.mtl\n";
// 頂点を書き出し
size_t vtxCount = mmdModel->GetVertexCount();
const glm::vec3* positions = mmdModel->GetUpdatePositions();
for (size_t i = 0; i < vtxCount; i++)
{
objFile << "v " << positions[i].x << " " << positions[i].y << " " << positions[i].z << "\n";
}
const glm::vec3* normals = mmdModel->GetUpdateNormals();
for (size_t i = 0; i < vtxCount; i++)
{
objFile << "vn " << normals[i].x << " " << normals[i].y << " " << normals[i].z << "\n";
}
const glm::vec2* uvs = mmdModel->GetUpdateUVs();
for (size_t i = 0; i < vtxCount; i++)
{
objFile << "vt " << uvs[i].x << " " << uvs[i].y << "\n";
}
// 頂点インデックスをコピー
std::vector<size_t> indices(mmdModel->GetIndexCount());
if (mmdModel->GetIndexElementSize() == 1)
{
uint8_t* mmdIndices = (uint8_t*)mmdModel->GetIndices();
for (size_t i = 0; i < indices.size(); i++)
{
indices[i] = mmdIndices[i];
}
}
else if (mmdModel->GetIndexElementSize() == 2)
{
uint16_t* mmdIndices = (uint16_t*)mmdModel->GetIndices();
for (size_t i = 0; i < indices.size(); i++)
{
indices[i] = mmdIndices[i];
}
}
else if (mmdModel->GetIndexElementSize() == 4)
{
uint32_t* mmdIndices = (uint32_t*)mmdModel->GetIndices();
for (size_t i = 0; i < indices.size(); i++)
{
indices[i] = mmdIndices[i];
}
}
else
{
return false;
}
// 面を書き出し
size_t subMeshCount = mmdModel->GetSubMeshCount();
const saba::MMDSubMesh* subMeshes = mmdModel->GetSubMeshes();
for (size_t i = 0; i < subMeshCount; i++)
{
objFile << "\n";
objFile << "usemtl " << subMeshes[i].m_materialID << "\n";
for (size_t j = 0; j < subMeshes[i].m_vertexCount; j += 3)
{
auto vtxIdx = subMeshes[i].m_beginIndex + j;
auto vi0 = indices[vtxIdx + 0] + 1;
auto vi1 = indices[vtxIdx + 1] + 1;
auto vi2 = indices[vtxIdx + 2] + 1;
objFile << "f "
<< vi0 << "/" << vi0 << "/" << vi0 << " "
<< vi1 << "/" << vi1 << "/" << vi1 << " "
<< vi2 << "/" << vi2 << "/" << vi2 << "\n";
}
}
objFile.close();
結果
出来上がった OBJ ファイルを Toolbag2 に読み込ませてみました。