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 に読み込ませてみました。




