ホーム < ゲームつくろー! < FBX習得編

その9 ボーンの情報を取得する


 前章までで、FBXファイルから3Dモデルを表現するに必要な情報を一揃え抽出できました。固定的なモデルであればそれらの情報からDirectXなどでモデルを描画する事ができるはずです。

 この章ではスキンメッシュアニメーションの基本であるボーンの情報を取得してみたいと思います。



@ 最終的に欲しい情報をまとめます

 スキンメッシュアニメーションに必要な情報はたくさんあり、また複雑です。まず必要な情報を整理します。

 スキンメッシュアニメーションとは、モデルに埋め込まれた「ボーン(Bone)」を動かすことによってその周りの頂点を動かす手法です。1本のボーンが影響を与える頂点は予め決まっています。また、1本のボーンがある1つの頂点に与える影響力を「ウェイト(Weight)」と言います。一般に1つの頂点には複数のボーンが少しずつ影響を与え、それにより柔らかなスキンメッシュが表現されています。

 1つの頂点に対して複数のボーンが対応し、そのボーンはその頂点に与えるウェイトを持っている。この関係をまとめると、例えば次のような表ができあがります:

表1:
頂点インデックス ボーン 対応ボーン ウェイト
B0 B1 B2 B3 B4 BI0 BI1 BI2 BI3 W0 W1 W2 W3
0         B0 B1     0.4 0.6 0 0
1       B1 B2 B4   0.3 0.3 0.4 0
2         B1 B3     0.5 0.5 0 0
3       B0 B2 B4   0.1 0.3 0.6 0
4         B2 B3     0.3 0.7 0 0
5     B0 B2 B3 B4 0.1 0.3 0.2 0.4

 一番左側に頂点インデックスがあります。モデルに埋め込んだボーンはBで表しています。上表で、例えば頂点3番にはボーンB0、B2そしてB4が影響を与えています。すべての頂点に対してそういう「対応ボーン」が対応しています(通常4本くらいが最大数です)。各対応ボーンがどれだけ影響を与えるかはウェイトで表されます。すべての対応ボーンのウェイトを足すと1.0になるのが普通です。

 続いて各ボーンの「姿勢・動き」に注目した情報についてまとめます。各ボーンには「初期姿勢」と「フレーム時姿勢」があります。初期姿勢はボーンの初期位置で、ポリゴンモデルの骨格を形成しています。モデリングツールで言うならば、オブジェクトにボーンをアタッチした瞬間の各ボーンの位置がそれに該当します。
 一方フレーム時姿勢はある時刻が経過した時の各ボーンの位置を表します。時刻が進むと連続的にフレーム時姿勢は変化していきます。これによりアニメーションが実現されるわけです。気をつけたいのが、フレーム0時点の姿勢≠初期姿勢である事です。フレーム0でポーズを付ける事は至って普通ですが、それは初期姿勢からボーンを動かすのであって、初期姿勢をそのポーズにはしません。よって、初期姿勢とフレーム時姿勢は別の情報として個別に取り扱います。 

 これらの姿勢は、言うまでも無く「行列」によって表現されます。表1にある各ボーンBには初期姿勢行列が1つだけ定義されます。一方フレーム時姿勢行列はフレームの数だけ定義されます。表にまとめると次のようになります:

表2-1
ボーン 初期姿勢行列
B0 IM0 (Initial Matirx )
B1 IM1
B2 IM2
表2-2
ボーン フレーム数 フレーム時姿勢行列
B0 1 B0_FM1 (Frame Matirx )
2 B0_FM2
3 B0_FM3
B1 1 B1_FM1
2 B1_FM2
3 B1_FM3


 表1、表2-1そして表2-2の情報があるとスキンメッシュアニメーションを実現できます。それら情報をFBX SDKで取得してみます。



A 頂点インデックス固有の情報(表1)を取得する

 まずは表1の情報をFBXから取得してみます。表1にある情報は「クラスタ(KFbxCluster)」というクラスが取りまとめています。FBXからクラスタを取得するには次のような手順を経ます:

クラスタを取得
KFbxMesh* mesh;  // メッシュオブジェクト(取得して下さい)

// スキンの数を取得
int skinCount  = mesh->GetDeformerCount( KFbxDeformer::eSKIN );

for ( int i = 0; i < skinCount; ++i ) {
   // i番目のスキンを取得
   KFbxSkin* skin = mesh->GetDeformer( i, KFbxDeformer::eSKIN );

   // クラスターの数を取得
   int clusterNum = skin->GetClusterCount();

   for ( j = 0; j < clusterNum; ++j ) {
      // j番目のクラスタを取得
      KFbxCluster* cluster = skin->GetCluster( j );
   }
}

 まずFBXからKFbxMeshオブジェクトを取得します。次にメッシュに含まれているスキンの数をチェックします。これが無い場合、そのモデルにはボーンが埋め込まれていません。スキンがあった場合はメッシュからそれを取り出します。1つのスキンオブジェクト(KFbxSkin)はKFbxMesh::GetDeformerメソッドで取得できます。取り出したスキンには複数の「クラスタ」が含まれています。クラスタはスキンに作用する1本のボーンの情報を管理しています。クラスタを取得するにはKFbxSkin::GetClusterメソッドを使います。

 メッシュ、スキンそしてクラス他の関係は次のような感じです:

 KFbxSkin::GetClusterメソッドの引数に渡すのはクラスタIDで、0から連番で振られています。この番号はそのままボーンの番号となります。

 1つのクラスタ(ボーン)からはそれが影響を与える頂点インデックスとウェイトの情報を取得できます:

ボーンが影響を与える頂点の情報を取得する
int pointNum = cluster->GetControlPointIndicesCount();
int* pointAry = cluster->GetControlPointIndices();
double* weightAry = cluster->GetControlPointWeights();

for ( int i = 0; i < pointNum; ++i ) {
   // 頂点インデックスとウェイトを取得
   int   index  = pointAry[ i ];
   float weight = (float)weightAry[ i ];
}

 こうすることで、「1つのボーンに対する頂点インデックスとウェイト」が次の表のようにまとめられます:

ボーン0
頂点インデックス ウェイト
0 0.3
2 0.2
3 0.6
10 0.5
11 0.1
15 0.2
... ...

 影響を与えるインデックス番号はもちろん連番にはなりません。

 各ボーンについて上のような表を作り横に並べるとこうなります:

ウェイト
頂点インデックス ボーン0 ボーン1 ボーン2
0 0.3 0.7
1 1.0
2 0.2 0.8
3 0.6 0.4
4 0.3 0.7
5 0.5 0.5
6 1.0
... ... ... ...

 各頂点インデックスと対応するボーン、そしてそのボーンが持つウェイトを一塊にしてまとめれば、DirectXの頂点シェーダと親和性の良いデータになります(表1がそういう状態です)。



B ボーン行列(表2)を取得する

 続いてクラスタからボーンの姿勢や動きを表す行列を取得します。先に説明したように、これには「初期姿勢」と「フレーム時姿勢」があります。 

 まず初期姿勢を抽出してみましょう。クラスタからボーンの初期姿勢を取得するにはKFbxCluster::GetTransformLinkMatrixメソッドを用います:

ボーンの初期姿勢を取得
KFbxXMatrix initMat;
cluster->GetTransformLinkMatrix( initMat );

このメソッドで取得できるのは4x4の姿勢行列で、すべてのクラスタについてこの姿勢行列を取得すれば、モデルに組み込まれている骨組みがわかります:

 ここで重要な注意です。KFbxCluster::GetTransformLinkMatrixメソッドで取得した行列は「ローカル座標空間でのボーンの絶対位置」です。上の図を見て頂くとわかるのですが、各ボーンの位置は親のボーンからの相対位置ではなくて、あくまでも座標上の絶対位置になっています。この考え方がDirectXと違いますので注意して下さい。

 続いて各フレームごとのボーンの姿勢であるフレーム時姿勢を取得します。この情報は実はクラスタではなく、クラスタを保持しているノード(KFbxNode)オブジェクトが持っています。前章でモデルのアニメーション位置を取得するのに用いたKFbxNode::GetGlobalFromCurrentTakeメソッドで各フレームのボーンの姿勢を取得できます:

ボーンのフレーム時姿勢を取得
int frameNum = 24;  // フレーム数(ここでは適当です)
KTime start, period;  // スタート時間と単位時間

for ( int i = 0; i < frameNum; ++i ) {
   KFbxXMatrix mat;
   KTime time = start + period * i;
   mat = cluster->GetLink()->GetGlobalFromCurrentTake( time );
}

 いくつかポイントがあります。クラスタを保持しているKFbxNodeオブジェクトはKFbxCluster::GetLinkメソッドで取得できます。KFbxNode::GetglobalFromCurrentTakeメソッドにはFBXの時刻オブジェクトであるKTimeを与える必要があります。フレーム単位で取得したいので、上ではtimeの右辺のような式で時刻を算出しています。このstartとperiodの取り方は前章に詳しく説明してありますのでご確認下さい。

 ここから得られるフレーム時姿勢も初期姿勢と同様にそのフレームでのボーンの絶対位置になります。これをすべてのボーンのすべてのフレームについて取得すれば、ボーンのアニメーション情報がすべて手に入ります(表2)。



C 絶対座標ボーンの扱い方

 ここまででスキンメッシュに必要な情報が揃いました。しかし、DirectXでスキンメッシュアニメーションを実装された経験がある方は「え!」と思うのではないでしょうか?A、Bのどちらでも「ボーンの親子関係の情報」を扱っていないんです。「親子関係の情報が無くてスキンメッシュてできるの?」と私も最初戸惑ったのですが、実はできるんです。しかも、物凄く簡単に!

 DirectXの特にD3DX系で用意してくれているスキンメッシュアニメーションはボーンの親子関係を用いてボーンの動きを制御します。マルペケでもそのように説明してきました(DirectX技術編その23〜28を参照下さい)。この方法の良い点は各ボーンが自分の親の姿勢(座標空間)に対して相対的に動くため、親の動きと自分の動きが独立しています。しかし難点としては、描画時に自分の親の絶対座標を毎回求める手間が生じます。ボーンは樹状に連なっているため、この計算は普通「行列スタック」を用いてトラバースしていく必要があります。また頂点を動かすために一度ボーンの空間に頂点を戻し(ボーンオフセット行列)、さらに親の絶対姿勢行列(ボーン行列)を掛けて世界に戻すという複雑なプロセスを経なければなりません。さらにID3DXMeshの最適化処理が重なって、結果として実装が酷く複雑になってしまいます。

 FBX SDKではボーンの初期姿勢やフレーム時姿勢を「絶対座標」で扱います。1つ1つのボーンの位置がローカル座標空間の原点を中心に設定されるためボーンの親子関係を考えません。これがどういう事なのかというと、各フレームでの絶対姿勢行列であるボーン行列が予め計算されている状態です。つまり、行列スタックを用いた複雑な計算部分がごっそりと省けるんです。そのため、スキンメッシュの計算が驚くほど簡単になります。

 具体的にどう計算するかを説明します。

 あるボーンBに注目します。このボーンの初期姿勢をBI、フレーム時姿勢をBFと表すことにします。初期姿勢時のボーンとローカル座標定義の頂点の関係は次の図の通りです:

 この初期姿勢からあるフレーム時にボーンが動くとこうなります:

 今は簡単のためにBは頂点Pに対して100%影響するとします。よって、求めたい変換行列は図の青い矢印のような位置変換を実現してくれる行列となります。ただ、このままだとどうして良いかちょっと判断できません。そこで、この青い矢印を次のような動きに置き換えます:

 初期姿勢のボーンを一度原点に戻し、改めてフレーム時の姿勢に持っていく。こうすると、点Pは先の図の位置にちゃんと動きます。この動きは青い矢印を分解したに過ぎません。注目は「初期姿勢のボーンを原点に戻す行列」というのは初期姿勢の逆行列に他ならないという点です。上の図ではそれをInvBIと表しています。つまり、絶対座標で表現されたボーンを用いると、ローカル頂点を動かすボーン行列は次のように計算することができます:

 描画時にすべてのボーンについてフレーム時姿勢行列を使って再計算する事で、簡単にボーン行列を得ることができます。この計算に行列スタックやボーンツリーのトラバースは一切入りません!また上の式から、FBXから抽出する行列は「初期姿勢の逆行列」と「フレーム時姿勢行列」で良いことがわかります。



 これで、スキンメッシュアニメーションを実現する情報もFBXから取得することができました。この段階でアニメーション付きのモデルをDirectXで動かす情報が一揃いした事になります。必要に応じてFBXからさらに情報を抽出しますが、ここから先は「抽出した情報をどのようにDirectXに持っていくか?」に注目したいと思います。いよいよ「脱Xファイル」が見えてきました。