assimp を使って PMX ファイルを読み込む

assimp を使って、自作のプログラムで PMX ファイルを読み込み描画をしてみたいと思います。PMX ファイルの仕様は公開されているので、全てを自力で読むのも可能ですが、assimp を使うと楽に読み込みが出来るのではないか、と期待しています。

PMX ファイルのロード

Assimp::Importer クラスを使って、シーンをロードします。
このとき、使用する描画 API の都合や処理内容にあわせて、フラグを設定します。

以下のコード例は、 VRM モデルでは有名な アリシア・ソリッド(http://3d.nicovideo.jp/alicia/) をロードしたときのものです。フラグには三角形化と、UVのフリップを設定しています。

model.importer = new Assimp::Importer();
uint32_t flags = 0;
flags |= aiProcess_Triangulate | aiProcess_FlipUVs;
model.scene = model.importer->ReadFile(fileName, flags);

Importer を経由してシーンをロードしたあとは、 aiScene クラスにアクセスして必要な情報を取得します。

ノード情報・メッシュ情報

aiScene の mRootNode メンバを参照して、シーンの根元ノードにアクセスできます。ここから、 aiNode クラスの変数を順番にアクセスして、親子構造を解析します。このとき、 aiNode::mName.C_Str() が返す文字列は UTF-8 エンコードされた文字列でした。おそらく PMX でのノード名が UTF-8 だからだろうと思いますが、この部分に注意が必要でしょう。デバッガや自作プログラムでの表示に都合の良いものに変換しておくとよさそうです。

aiNode のメンバには、 mMeshes, mNumMeshes のメンバがあり、そのノードに所属する(関連する) ポリゴンメッシュが参照できます。この mMeshes は int 型配列であり、メッシュ実体は aiScene が保持しているので、実際にアクセスしようとすると、以下のようなコードになります。

for(auto i=0; i< node->mNumMeshes; ++i) {
  auto meshIndex = node->mMeshes[i];
  const auto* mesh = scene->mMeshes[meshIndex];
  
  // mesh の中身にアクセス
}

aiNode には、aiMatrix4x4 mTransformation として、ノードの姿勢行列も保持されています。これはいわゆるローカル行列なので、実際に使う多くの場面では親ノードと乗算してワールド行列にして使うことになります。この行列の並びですが、 DirectX::XMMATRIX として使うには転置が必要でした。

頂点データの読み取り

aiMesh クラスの中に mNumVertices、 mNumFaces など情報があります。ここで mNumFaces はポリゴンの面数なので、先で三角形化している状況であれば、インデックス数は mNumFaces*3 です。

また頂点のデータについて、よく使用する各属性の情報は、それぞれ以下のメンバで保持されています。

  • mVertices 位置情報
  • mNormals 法線情報
  • mTextureCoords[uvIndex][] テクスチャのUV座標
    • UV セットとして複数を保持出来る構造のため、2重の配列です
  • mTangents タンジェント情報
    • HasTangentsAndBitangents() 有効時に値が入っている
  • mBitangents バイノーマル情報
    • HasTangentsAndBitangents() 有効時に値が入っている

スキニング用ボーン情報について

スキニング用のボーン情報は、メッシュの HasBone() が有効時にアクセスできます。メッシュの mBones メンバから aiBone* を取得でき、ボーンの名前やウェイト情報が格納されています。注意点として、ウェイト情報の中に参照する頂点のインデックスが入っています。CPU でスキニングを計算する場合にはそこまで問題になりませんが、 GPU でスキニングを計算する場合には、これらの頂点インデックスやウェイト値は、頂点属性(アトリビュート)としてパッキングしたいので、うまく配列構造に直していくことが必要です。

aiBone のメンバには、 mOffsetMatrix が存在します。こちらもスキニングの計算で使用します。

マテリアルインデックス

メッシュはマテリアルへの参照として、マテリアルインデックスを保持しています。

マテリアル情報

マテリアルは、 シーンの mMaterials に保持されています。このマテリアル(aiMaterial) には、使用するテクスチャの情報、色の情報、など一般的にマテリアルと呼ばれる部類のパラメータが詰まっています。

例えば、ディフューズ値を得るコードは以下の通りです。

aiColor3D diffuse{};
material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse);

例えば、シャイニネス(Shininess) の値を得るコードは以下の通りです。

float shininess = 0;
material->Get(AI_MATKEY_SHININESS, shininess);

このようにマテリアルのキー情報と、格納変数を引数にセットして情報を取得します。ここでキーの情報は、http://assimp.sourceforge.net/lib_html/materials.html のページを参考にしました。

描画

それぞれ情報を集めて、描画用に再構築して描画したものが以下の画像です。そのまま描画するとAポーズになってしまうので、一部のボーンの角度を変更して上手く関節変形ができているのか確認しています。

スキニング計算用のマトリックス生成について

GPU でスキニングを処理するための、マトリックスパレット生成において、各行列をどのように計算するのかという点で悩みがちなので、上記のプログラムで使用していた計算式を記載します。XMMatrixTranspose を使っている点は、C++ コード側と HLSL コード側で、行列のかけ算順序の表記を同一にするために適用しています。

// ボーン マトリックスパレットの準備.
std::vector<XMMATRIX> matrices;
for (auto bone : batch.boneList) {
    auto mtx = bone->offsetMatrix * bone->worldTransform * m_skinActor.invGlobalTransform;
    matrices.push_back(XMMatrixTranspose(mtx));
}

まとめ

assimp を使って簡単に PMX を表示できた、とは言い難いですが、assimp を用いることで他のデータ形式への対応もすぐに出来るようになった、というメリットはありました。スキニング部分を無効化すれば、すぐに obj ファイルをロードして描画への対応も出来ます。assimp を普段使っている人であれば、逆にすぐに PMX も読み込んで表示まで辿り着けるのではないでしょうか。

関連記事

コメントを残す

メールアドレスが公開されることはありません。

CAPTCHA


このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください