MMDのモデルデータをざっくり読み込む


はじめに

この記事は、NCC Advent Calendar 2018の参加記事です。

DirectX11 SDKを使用しているため、Windows上でしか動作もコンパイルもできません。ご了承ください。

また、今回の記事内容をざっくりとGitHubに上げておきました。興味があればご覧ください。
https://github.com/Kuyuri-Iroha/Draw-PMX

MMDのモデルデータについて

MMDのモデルデータの読み込みの前に、今回使用するMMDモデルのファイル形式について少し説明します。

今回使用するのはPMXファイルという形式のモデルファイルです。これは、MMDモデルのファイル形式であるPMDファイルの上位互換的なものです。詳しくはVPVP wikiさんのMikuMikuDance/PMXの概要をご覧ください。

また、今回はPMXファイルのデータの一部のみを読み込み・使用します。具体的には、以下のデータです。

  • 頂点座標
  • UV座標
  • 頂点インデックス
  • テクスチャ
  • 拡散色
  • 反射色

1. DirectX11のスケルトンプログラム

今回はDirectX11とC++の組み合わせで描画処理を行うので、まずはスケルトンプログラムを組みます。まずはWindows APIを使用してWindowを作成し...などと1から説明するとC++ & DirectX11のスケルトンプログラムは固定機能パイプラインが存在しないこともあって無駄に長くなってしまうので、ざっくりと手順をリストアップするに留めておきます。

  1. Windows APIを初期化
    1. エントリーポイントを記述
    2. Windowを作成する
  2. DirectX11を初期化
    1. デバイス系を初期化
    2. スワップチェインと呼ばれるダブルバッファリング機構を初期化
    3. レンダリングパイプラインを初期化
      1. Blend Stateの設定
      2. Rasterizer Stateの設定
      3. Depth Stencil Stateの設定
      4. Input Assemblerの設定
      5. Output Mergerの設定

これだけあって初めてこんな画面を表示できます。

はい。真っ白です。
色はここらへんのコードでよしなに変えられます。

constexpr float CLEAR_COLOR[]{1.0f, 1.0f, 1.0f, 1.0f};
pContext->ClearRenderTargetView(pRenderTargetView, CLEAR_COLOR);

pContextID3D11DeviceContext*型、pRenderTargetViewID3D11RenderTargetView*型のポインタです。

これでスケルトンプログラムは出来上がったので、PMXファイルのデータを読み込んでいきます。

2. 頂点を読み込む

いよいよ頂点を読み込みますが、その前にバイナリ形式であるPMXファイルのデータ構造を把握しなければなりません。

PMXのデータ構造

PMXファイルのデータ構造は極北PさんのPmxEditor内のLib/PMX仕様/PMX仕様.txtに全てまとめられています。

私は、上に掲載しているコード内ではstd::ifstreamをバイナリモードで開いてbyte単位で取得したchar*型データを各データ型にキャストすることで取得しています。ですが、おそらくvoid*型とかでもできるんじゃないかなあとも思います。つまりは基本的にメモリに直接データを書き込むことでデータを取得しているので、PMXファイルと違うビッグエンディアンの処理系で動作させるとデータがうまく読み取れないかもしれません。例としてコードの一部を掲載しておきます。

pmxFile.read(reinterpret_cast<char*>(&data.vertices[i].position), 12);
pmxFile.read(reinterpret_cast<char*>(&data.vertices[i].normal), 12);
pmxFile.read(reinterpret_cast<char*>(&data.vertices[i].uv), 8);

これは、上から頂点座標、法線ベクトル、UV(テクスチャ)座標を取得するコードですが、reinterpret_cast<>()で強制キャストをし、char*型が1byteであることを利用して12byte (float[3]) のデータを読み出しています。
また、PMXのテキストエンコードは、PMX仕様.txt に書かれている通り UTF16LE か UTF8 となっています。PMX仕様.txt を見る限りではC#では簡単に取り出せるようですが、C++ではかなり厄介なことになっているので、私は以下のようなコードを書いて、対応を UTF16LE に絞ることで対処しています。以下がその部分を担う関数です。

bool getPMXStringUTF16(std::ifstream& _file, std::wstring& output)
{
    std::array<wchar_t, 512> wBuffer{};
    int textSize;

    _file.read(reinterpret_cast<char*>(&textSize), 4);

    _file.read(reinterpret_cast<char*>(&wBuffer), textSize);
    output = std::wstring(&wBuffer[0], &wBuffer[0] + textSize / 2);

    return true;
}

取得した頂点をGPUに渡す

さて、それでは取得した頂点情報を描画に使っていきます。

ここで使用するのは、頂点座標 (float[3])と頂点インデックス (int)です。この時点では動作確認も兼ねてシェーダーを組むので、ピクセルシェーダーでは任意の単色ベタ塗りで動作させています。

ここでは説明を省きますが、DirectX11ではInput Assemblerで頂点座標と頂点インデックスを頂点シェーダーに渡すことで描画します。

そんな感じに動作させると、このような画面が描画されます。3Dモデルはコロン/koron氏の初音ミクver.2.1を使用させていただいております。

これで頂点が正しく読み込まれたことが確認できました。

マテリアルを読み込む

次にマテリアルを読み込みます。
といっても、読み込み方法は先程と同じでマテリアル部分を読み込むだけなのでほぼ省略してしまいますが、今回の私のコードでは、テクスチャをID3D11ShaderResourceView*で管理しています。また、PMXではテクスチャが割り当てられていないメッシュがあるので、メッシュごとにシェーダーを切り替えるようにしています。

今回はテクスチャを、UV座標を頂点バッファに追加することで貼り付けているだけですが、ここで取得したデータがあればシェーディングなどもできるデータが揃っているのでかなりいろいろ楽しめます。

マテリアルに関してはほぼ説明していないも同義な内容ですが、マテリアルを適用した動作結果はこんな感じです。

PMXファイルはMMDでの仕様が想定された3Dモデルなのでテクスチャを描画するだけでもすごく綺麗です。

おわりに

今回の内容はNCC Advent Calendar 2018への参加と、高校時代の私のコードのリファクタリングを兼ねてDirectX11を触りましたが、最近WebGL、Processing、openFrameworksを触っていたので、レンダリングパイプラインの大部分の初期化を自分でしなければいけないことへの作業量を完全になめていました。
結果大遅刻です。(悲しいね)
それにしても、DirectX11はレンダリングパイプラインの勉強にはすごくいいなあと思いました。(苦行ですが)

そして、今日(12/16)も書きます。がんばります。明日も書きます。流石に明日の内容は変更して簡単にしてしまうと思います。