LoginSignup
49
44

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-01-09

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)をドラッグアンドドロップしてください。

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

saba_viewer.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

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

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

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

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

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

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

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 ファイルをマージすることができます。

アニメーションの反映

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 で行っています。
更新後の頂点は、以下のようにして取得します。

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

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

OBJ ファイルの出力

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

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

49
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49
44