C++でMMDを読み込むライブラリを作った

MMD のデーターを読み込み、アニメーションさせるライブラリを作りました。
[Saba](https://github.com/benikabocha/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)をドラッグアンドドロップしてください。

動作させると以下のようになります。

![saba_viewer.png](https://qiita-image-store.s3.amazonaws.com/0/158895/c238002b-c101-7044-d435-c41f990c88d2.png)

© 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
```

するとこんな感じになります。

![dev_dir_01.png](https://qiita-image-store.s3.amazonaws.com/0/158895/d287deb9-dbed-7808-dc24-b973382cdb51.png)

### 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` は以下のようになっています。
![dev_dir_02.png](https://qiita-image-store.s3.amazonaws.com/0/158895/ab23ce06-779d-8add-188a-0e828c933504.png)


### 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 で開きます。

![dev_dir_03.png](https://qiita-image-store.s3.amazonaws.com/0/158895/f3371e68-e664-9275-dbaf-e0843b825750.png)

#### スタートアッププロジェクトの変更

`saba_viewer` をスタートアッププロジェクトに設定します。

![dev_vs_saba_01.png](https://qiita-image-store.s3.amazonaws.com/0/158895/01bec90a-2d57-d87e-ee58-1eca9d68ef82.png)

#### 実行

`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` を使用します。

``` cpp:mmd2obj.cpp
	// モデルの読み込み
	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` を使用します。

```cpp:mmd2obj.cpp
	// アニメーションの読み込み
	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 ファイルをマージすることができます。

## アニメーションの反映

```cpp:mmd2obj.cpp
	// 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フレームかけて徐々に反映します。
この処理は後のアニメーション更新時には、呼ばないようにしてください。

アニメーションの更新は以下の繰り返しです。

1. VMDアニメーションを評価 `vmdAnim->Evaluate()`
2. ノードの更新
3. Physics の更新
4. 頂点の更新 (アニメーションを反映)

## 頂点の取得

Saba では頂点の変形処理を CPU で行っています。
更新後の頂点は、以下のようにして取得します。

```cpp
const glm::vec3* positions = mmdModel->GetUpdatePositions();
const glm::vec3* normals = mmdModel->GetUpdateNormals();
const glm::vec2* uvs = mmdModel->GetUpdateUVs();
```

面のインデックスはファイルの種類により、要素サイズが変わるため、事前にチェックしてアクセスするようにしてください。

## OBJ ファイルの出力

OBJ の出力は、OpenGL 等で表示する際の参考になるかと思います。

```cpp:mmd2obj.cpp

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

![mmd2obj.png](https://qiita-image-store.s3.amazonaws.com/0/158895/fbcc42ea-8a00-ae2a-2821-1ffd63b21176.png)