読者です 読者をやめる 読者になる 読者になる

FBXファイルを読み込む(スキン情報の取得)

松浦さんから依頼を受けて、Autodesk Maya 等の .fbx 形式からMicrosoft DirectX の .x 形式に変換するスクリプトを書きました。
FBX形式の仕様は膨大で、予想していたよりもずっと大掛かりな作業となってしまいました。
これからFBX SDKを扱う人の助けになることを祈って、解説記事を書くことにします。
今回の作業に当たって、○×つくろーどっとコムさんのFBX修得編を参考にさせていただきました。
こちらは大変丁寧な記事ですので、FBX SDKを始める人はまずこちらを読むのがいいでしょう。
また、.x 形式についてはWindows Fortran入門さんのXファイルの形式に詳しい解説があります。

この記事では、まだFBX修得編でも解説されていないスキン情報の取得と、その DirectX 形式への変換について説明します。
私は3Dについては門外漢なので、もし不正確な表現がありましたらご指摘ください。修正させていただきます。

1. スキン情報オブジェクトを取り出す

FBXでは、スキン情報は KFbxDeformer クラスの子クラスであるKFbxSkin クラスによって管理されます。
KFbxDeformer オブジェクトは KFbxMesh オブジェクトから次のようにして取り出すことが出来ます。
PrintSkinweight() 関数の呼び出し
KFbxMesh *pMesh;
:  // set appropriate value to pMesh
// get number of deformer
int DeformerCount = pMesh->GetDeformerCount();
for(int i = 0 ; i < DeformerCount; ++i){
// output each deformer
PrintSkinweight(pMesh->GetDeformer(i));
}
1つのメッシュに対して複数の Deformer が定義されていることがあります。
よって、 GetDeformerCount() メソッドによって Deformer の個数を取得し、 GetDeformer(i) で i 番目の Deformer を取得します。

forループの中の PrintSkinWeight() 関数の中で、実際の出力処理を行います。
それでは、この関数の実装を説明しましょう。
PrintSkinweight() 関数の実装(1/7)
void PrintSkinweight(KFbxDeformer *pDeformer){
// open file to output
FILE *fp = fopen("output.x","w");
// Is deformer type eSKIN?
if(pDeformer->GetDeformerType() != KFbxDeformer::eSKIN){
fprintf(stderr, "Error : Deformer type is not skin.\n");
return;  // abort
}
KFbxSkin *pSkin = static_cast<KFbxSkin *>(pDeformer);
KFbxDeformer オブジェクトは、以下の4種類の DeformerType のうちどれか1つを持っています。
  • UNIDENTIFIED
  • eSKIN
  • eVERTEX_CACHE
  • eDEFORMER_COUNT
.x 形式の SkinWeight と同様の情報を持っているのは DeformerType が eSKIN であるオブジェクトで、これは KFbxSkin クラスにキャストすることが出来ます。
これで KFbxSkin オブジェクトを取り出すことが出来ました。

2. クラスタと3つのリンクモード

1つの KFbxSkin オブジェクトは、複数のクラスタから構成されます。
このクラスタひとつひとつが、 .x 形式のひとつの SkinWeight 情報に相当します。
つまり、クラスタはボーンの同義語のようです。

次のようにして、クラスタ情報を読み込みます。
PrintSkinweight() 関数の実装(2/7)
// get number of cluster
int ClusterCount = pSkin->GetClusterCount();
for(int i = 0; i < ClusterCount; ++i){
// get a cluster
KFbxCluster *pCluster = pSkin->GetCluster(i);
// set link mode to eTOTAL1
// pCluster->SetLinkMode(KFbxCluster::eTOTAL1);
// Sorry, let me discuss only clusters where link mode is eTOTAL1
if(pCluster->GetLinkMode() != KFbxCluter::eTOTAL1){
continue;
}
先ほど見た GetDeformerCount()GetDeformer() と同様にして、
クラスタの総数とそれぞれのクラスタを取得することが出来ます。

クラスタには、次の3種類のリンクモードのうちどれか1つが設定されています。
  • eNORMALIZE
  • eADDITIVE
  • eTOTAL1
本来ならばこの3種類のリンクモードに応じて3つの出力コードを書くべきなのですが、
これらの(特に eADDITIVE の)データ構造を理解してコードを書くのは非常に難しく大変な作業です。
あまり褒められた手段ではありませんが、ここでは SetLinkMode() メソッドを使ってリンクモードを eTOTAL1 に設定し、
eTOTAL1 用のコードだけを書くことにします。
eTOTAL1 を選んだのは、これが最も .x 形式に近いデータ構造だからです。
訂正:
SetLinkMode() メソッドはリンクモードのフラグを書き換えるだけで、実際のデータ構造には影響を与えないようです。したがって、やはり3種類のリンクモードに合わせて3つのコードを書かなければなりません。
ちなみに、このことについてもリファレンスに十分な記述はありません。

3. 影響を受ける頂点情報の取得

ここからは、実際にスキン情報を出力する部分に入ります。

まずは、 GetName() メソッドでクラスタの名前を取得します。
PrintSkinweight() 関数の実装(3/7)
fprintf(fp, "SkinWeights {\n");
fprintf(fp, "\"%s\";\n", pCluster->GetName());
次に、 GetControlPointIndicesCount() メソッド(長いですね)で、このクラスタによって移動する頂点の数を取得します。
PrintSkinweight() 関数の実装(4/7)
// get number of control point affected by this bone
int ControlPointIndicesCount = pCluster->GetControlPointIndicesCount();
// output number of control point
fprintf(fp, "%d;\n", ControlPointIndicesCount);
実際に移動する頂点のインデックスの配列は、 GetControlPointIndices()によって取得できます。
このメソッドは int * 型を返します。
PrintSkinweight() 関数の実装(5/7)
for(int j = 0; j < ControlPointIndicesCount; ++j){
fprintf(fp, "%d%c\n",
// get index of j-th control point that is deformed
(pCluster->GetControlPointIndices())[j],
// a comma is needed between two consecutive indices,
// a semicolon is needed after the last index.
(j+1==ControlPointIndicesCount ? ';' : ','));
}
そして、各頂点がボーンから受ける影響の重みの配列は、GetControlPointWeights() メソッドによって取得できます。
こちらは double * 型を返します。
PrintSkinweight() 関数の実装(6/7)
for(int j = 0; j < ControlPointIndicesCount; ++j){
fprintf(fp, "%.6lf%c\n",
(pCluster->GetControlPointWeights())[j],
(j+1==ControlPointIndicesCount ? ';' : ','));
}
簡単のために省略しましたが、実際には移動する頂点数が 0 だった場合も考慮して実装する必要があります。

3. トランスフォーム行列の取得

最後に、メッシュの頂点をボーンの空間へと変換する行列を取得します。
この変換を行う行列は、以下の3種類があります。
  • TransformMatrix
  • TransformLinkMatrix
  • TransformAssociateModelMatrix
まず、 TransformAssociateModelMatrix は無視してかまいません。
これはリンクモードが eADDITIVE のときだけ必要になるからです。
(先ほどリンクモードを eTOTAL1 に設定したことを思い出してください。)

残る2つの行列なのですが、どちらを使えばいいのかはよく分かりません。
とりあえず、 TransformMatrix を使うことにします。
PrintSkinweight() 関数の実装(7/7)
KFbxXMatrix Matrix, TransformMatrix, TransformLinkMatrix;
// get transform matrix and transform link matrix
pCluster->GetTransformMatrix(TransformMatrix);
pCluster->GetTransformLinkMatrix(TransformLinkMatrix);
// THESE ARE SUBJECT TO RETHINK
Matrix = TransformMatrix;
// Matrix = TransformLinkMatrix;
// Matrix = TransformLinkMatrix * TransformMatrix;
// Matrix = TransformMatrix * TransformLinkMatrix;
// output all elements of the matrix
for(int y = 0; y < 4; ++y)
for(int x = 0; x < 4; ++x)
fprintf(fp, "%.6lf%s", Matrix.Get(y, x), ((x==3 && y==3) ? ";;" : ","));
fprintf(fp, "\n}\n\n");
} // end of for(int i = 0; i < ControlPointIndicesCount; ++i){
return;
} // end of function
FBX SDKでは、行列は KFbxXMatrix クラスによって管理されます。
Get() メソッドによって要素へのアクセスを行います。


これで .x 形式での出力が完了しました。
ここまでお付き合い下さいましてありがとうございます。

FBX SDKの問題点

FBX SDK は最近バージョンアップがあり、大幅な機能の強化が施されました。
しかし、ユーティリティに関しては十分でも、ユーザビリティに関してはお世辞にも満足とは言えません。

まず、この記事で触れた部分の中で私が不満を持った点を挙げます。

1. KFbxSkin が見付けにくい

KFbxSkin オブジェクトは、 KFbxDeformer オブジェクトをキャストして得られると説明しました。
しかしリファレンスでは、この2つのクラスの関連性についてほとんど表記されていません。(*KFbxDeformer, KFbxSkin)
クラス図によって継承関係は示されていますが、文章による説明は全くありません。
これは、リファレンスをトップダウンに見ながらスキン情報を探すユーザにとって大きな障害です。
もしスキン情報を見つけたいと思ったなら、 KFbxDeformer クラスの解説の中で
ただ一度しか触れられていない KFbxSkin という単語を見つけ、
それがスキン情報だという洞察を発揮した上で(もしボーンという単語だけを
探していたなら、この単語は見つけられないでしょう)クリックする必要があります。

始めに言ったように私は門外漢ですので、まず KFbxDeformer クラスを見付け、
検索フォームに思いつく限りの単語を打ち込んで KFbxSkin クラスを見付け、
これらのクラスの説明を何度か見比べてやっと理解しました。

2. どちらの行列を使うべきか

頂点をトランスフォームする行列の解説について、恥ずかしながら
どちらを使えばいいのかよく分からないと逃げてしまいました。
この点について、リファレンスの該当箇所を見てみましょう。
  • Transform refers to the global initial position of the node containing the link
  • TransformLink refers to global initial position of the link node
これでは、お世辞にも分かりやすいとは言えません。
2つのモノの違いを説明するのに、なぜ全く同じ単語を使うのでしょうか?

何が問題か

これらの問題は、「ドキュメントが不十分である」と言い換えられます。
十分な量の説明を与えていれば、私は情報を求めてリファレンスを何度もめくったり、
不正確なコードを書くことも無かったのです。

「十分な知識があればこのドキュメントからでもすぐに情報を
見付けられるのだから、ドキュメントが悪いのではない。
不勉強なお前が悪いのだ」という反論もあるかもしれません。
確かに私が悪いのですが、しかしそれは「ドキュメントをより充実させなくても良い」
という理由にはなりえません。
なぜなら、FBX SDKは多くの人に使われることを目的としている(はずだ)からです。

もしより詳細なドキュメントのあるSDKが使えるならば、たとえ機能が劣っていても
私はそちらを使います。そちらのほうが効率的に製作できるからです。
「ドキュメントは貧弱だがウチのSDKの方が高性能だ。だから必死に勉強して
ウチのSDKを使え」と言われても、耳を貸すつもりはありません。

もしAutodesk社がFBX SDKを多くの人に使ってもらいたいと思っているのなら、
詳細なドキュメントを書く以外に採るべき手段はありません。
担当:(後半は成田さんの霊が乗り移った)田山