FBXファイルを読み込む(スキン情報の取得)
松浦さんから依頼を受けて、Autodesk Maya 等の .fbx 形式からMicrosoft DirectX の .x 形式に変換するスクリプトを書きました。
FBX形式の仕様は膨大で、予想していたよりもずっと大掛かりな作業となってしまいました。
これからFBX SDKを扱う人の助けになることを祈って、解説記事を書くことにします。
FBX形式の仕様は膨大で、予想していたよりもずっと大掛かりな作業となってしまいました。
これからFBX SDKを扱う人の助けになることを祈って、解説記事を書くことにします。
今回の作業に当たって、○×つくろーどっとコムさんのFBX修得編を参考にさせていただきました。
こちらは大変丁寧な記事ですので、FBX SDKを始める人はまずこちらを読むのがいいでしょう。
また、.x 形式についてはWindows Fortran入門さんのXファイルの形式に詳しい解説があります。
この記事では、まだFBX修得編でも解説されていないスキン情報の取得と、その DirectX 形式への変換について説明します。
私は3Dについては門外漢なので、もし不正確な表現がありましたらご指摘ください。修正させていただきます。
こちらは大変丁寧な記事ですので、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 が定義されていることがあります。
よって、
forループの中の
それでは、この関数の実装を説明しましょう。
よって、
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
KFbxSkin
クラスにキャストすることが出来ます。これで
KFbxSkin
オブジェクトを取り出すことが出来ました。2. クラスタと3つのリンクモード
1つの
このクラスタひとつひとつが、 .x 形式のひとつの SkinWeight 情報に相当します。
つまり、クラスタはボーンの同義語のようです。
次のようにして、クラスタ情報を読み込みます。
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); // |
先ほど見た
クラスタの総数とそれぞれのクラスタを取得することが出来ます。
各クラスタには、次の3種類のリンクモードのうちどれか1つが設定されています。
本来ならばこの3種類のリンクモードに応じて3つの出力コードを書くべきなのですが、
これらの(特に
あまり褒められた手段ではありませんが、ここでは
訂正:
SetLinkMode() メソッドはリンクモードのフラグを書き換えるだけで、実際のデータ構造には影響を与えないようです。したがって、やはり3種類のリンクモードに合わせて3つのコードを書かなければなりません。
ちなみに、このことについてもリファレンスに十分な記述はありません。
GetDeformerCount()
や GetDeformer()
と同様にして、クラスタの総数とそれぞれのクラスタを取得することが出来ます。
各クラスタには、次の3種類のリンクモードのうちどれか1つが設定されています。
- eNORMALIZE
- eADDITIVE
- eTOTAL1
これらの(特に
eADDITIVE
の)データ構造を理解してコードを書くのは非常に難しく大変な作業です。あまり褒められた手段ではありませんが、ここでは
SetLinkMode()
メソッドを使ってリンクモードを eTOTAL1
に設定し、eTOTAL1
用のコードだけを書くことにします。eTOTAL1
を選んだのは、これが最も .x 形式に近いデータ構造だからです。SetLinkMode() メソッドはリンクモードのフラグを書き換えるだけで、実際のデータ構造には影響を与えないようです。したがって、やはり3種類のリンクモードに合わせて3つのコードを書かなければなりません。
ちなみに、このことについてもリファレンスに十分な記述はありません。
3. 影響を受ける頂点情報の取得
ここからは、実際にスキン情報を出力する部分に入ります。
まずは、
次に、
実際に移動する頂点のインデックスの配列は、
このメソッドは int * 型を返します。
そして、各頂点がボーンから受ける影響の重みの配列は、
こちらは double * 型を返します。
簡単のために省略しましたが、実際には移動する頂点数が 0 だった場合も考慮して実装する必要があります。
まずは、
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 ? ';' : ',')); } |
3. トランスフォーム行列の取得
最後に、メッシュの頂点をボーンの空間へと変換する行列を取得します。
この変換を行う行列は、以下の3種類があります。
これはリンクモードが
(先ほどリンクモードを
残る2つの行列なのですが、どちらを使えばいいのかはよく分かりません。
とりあえず、
FBX SDKでは、行列は
これで .x 形式での出力が完了しました。
ここまでお付き合い下さいましてありがとうございます。
この変換を行う行列は、以下の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 |
KFbxXMatrix
クラスによって管理されます。Get()
メソッドによって要素へのアクセスを行います。これで .x 形式での出力が完了しました。
ここまでお付き合い下さいましてありがとうございます。
FBX SDKの問題点
FBX SDK は最近バージョンアップがあり、大幅な機能の強化が施されました。
しかし、ユーティリティに関しては十分でも、ユーザビリティに関してはお世辞にも満足とは言えません。
まず、この記事で触れた部分の中で私が不満を持った点を挙げます。
しかし、ユーティリティに関しては十分でも、ユーザビリティに関してはお世辞にも満足とは言えません。
まず、この記事で触れた部分の中で私が不満を持った点を挙げます。
1. KFbxSkin
が見付けにくい
KFbxSkin
オブジェクトは、 KFbxDeformer
オブジェクトをキャストして得られると説明しました。しかしリファレンスでは、この2つのクラスの関連性についてほとんど表記されていません。(*KFbxDeformer, KFbxSkin)
クラス図によって継承関係は示されていますが、文章による説明は全くありません。
これは、リファレンスをトップダウンに見ながらスキン情報を探すユーザにとって大きな障害です。
もしスキン情報を見つけたいと思ったなら、
KFbxDeformer
クラスの解説の中でただ一度しか触れられていない
KFbxSkin
という単語を見つけ、それがスキン情報だという洞察を発揮した上で(もしボーンという単語だけを
探していたなら、この単語は見つけられないでしょう)クリックする必要があります。
始めに言ったように私は門外漢ですので、まず
KFbxDeformer
クラスを見付け、検索フォームに思いつく限りの単語を打ち込んで
KFbxSkin
クラスを見付け、これらのクラスの説明を何度か見比べてやっと理解しました。
2. どちらの行列を使うべきか
頂点をトランスフォームする行列の解説について、恥ずかしながら
どちらを使えばいいのかよく分からないと逃げてしまいました。
この点について、リファレンスの該当箇所を見てみましょう。
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を多くの人に使ってもらいたいと思っているのなら、
詳細なドキュメントを書く以外に採るべき手段はありません。
十分な量の説明を与えていれば、私は情報を求めてリファレンスを何度もめくったり、
不正確なコードを書くことも無かったのです。
「十分な知識があればこのドキュメントからでもすぐに情報を
見付けられるのだから、ドキュメントが悪いのではない。
不勉強なお前が悪いのだ」という反論もあるかもしれません。
確かに私が悪いのですが、しかしそれは「ドキュメントをより充実させなくても良い」
という理由にはなりえません。
なぜなら、FBX SDKは多くの人に使われることを目的としている(はずだ)からです。
もしより詳細なドキュメントのあるSDKが使えるならば、たとえ機能が劣っていても
私はそちらを使います。そちらのほうが効率的に製作できるからです。
「ドキュメントは貧弱だがウチのSDKの方が高性能だ。だから必死に勉強して
ウチのSDKを使え」と言われても、耳を貸すつもりはありません。
もしAutodesk社がFBX SDKを多くの人に使ってもらいたいと思っているのなら、
詳細なドキュメントを書く以外に採るべき手段はありません。
担当:(後半は成田さんの霊が乗り移った)田山