完全ホワイトボックスなスキンメッシュアニメーションの解説スキンメッシュアニメーションは3Dのゲームの一つの大きな壁であり、一つの憧れ(?)でもあるかなと思います。マルペケでもDirectX技術編その23辺りからDirectXが用意しているスキンメッシュアニメーションの解説を行いました。当時は良く分からない状態からスタートしましたので、解説もまぁ...分かったようなわからないような内容になっております(^-^;。一通り実装して思ったのは「DirectX9が用意するスキンメッシュの仕組みを使うと本質がわからなくなる!」という事でした。D3DXLoadMeshHierarchyFromX関数を使ってXファイルを読み込むと、D3DXFRAME構造体とID3DXAnimationControllerが何故か出来ていて、アニメーションコントローラで時間を進めるとなぜかD3DXFRAMEの中身が更新されている。わかっているとありがたい仕組みですが、わからない時には恐ろしいまでのブラックボックスです。これでは何時まで経ってもスキンメッシュの理解に届きません。そこで本章では、XファイルどころかD3DXLoadMeshHierarchyFromX関数等も一切用いずにスキンメッシュアニメーションをするポリゴンを作ってみる事にしました。すべての処理が見える「完全ホワイトボックス」なコードです。これで、「スキンメッシュアニメーションとはなにか?」という根本が垣間見られれば良いなぁと思う次第です。本章はサンプルをまずは実行してみて下さい!本章は予め作ってあるサンプルを通して解説していきます。サンプルを動かしてみて下さい:こんな感じで三叉に分かれたポリゴンがぐねぐねと動きます。気持ち悪(^-^;いやはや。円錐はボーンのつもりです。このサンプルにはmain.cpp以外のファイルがありません。このサンプルの中にポリゴンデータ、ボーンの連結、各種姿勢の計算などスキンメッシュアニメーションの基本的な部分が全部入っています!コード透過度の高いサンプルとなっております(^-^)さ、それではじっくり行きますよ〜。ポリゴン設計図メッシュから始めましょう。今回は下のようなメッシュを作ることにしました:左図の各座標点は一所懸命に手計算。本来こういう頂点データはモデラーが出力してくれるためそれを読み込めば良いのですが、今回は完全ホワイトボックスなので全部用意です。上図の頂点数は15個。4)一番上が座標(X,Y)です(今回はZ=0.0で固定です)。真ん中は頂点に影響するボーンの影響力。下が影響するボーンの番号です。緑色の丸はボーンを表します。「あれ?ボーンって長いよね?」と思われるかもしれません。実際ボーンというのは関節と関節の間をつないだものに過ぎません。回ったり移動したりするのは関節なんです。Mayaではこの丸いのを「ジョイント」と呼んでいます。ジョイント(ボーン)は親子関係があり、上の場合0番がルートとなり三方に広がります。そのジョイントの向きですが、これは右図のようにしました。各ジョイントについている軸は赤いのがX軸、緑色のがY軸です。今回は初期姿勢でみんなX軸の方向に伸びるようにしました。いうジョイントごとのローカル姿勢(もしくは親ジョイントからの相対姿勢)も普通はモデラーが出力してくれます。このポリゴンを今回は「三角形リスト」でつなぎます。そのための頂点インデックスも必要になります。14)のようにちまちまと打ち込みます。matrixIndex[4];};頂点座標coordと頂点に影響を与えるボーンの重みweight、そして実際に影響を与えるボーンの番号であるmatrixIndexを宣言しています。頂点配列vtxからポリゴンを作るために頂点インデックスを作っています。今回はポリゴンが13枚あって三角形リストとしてつなげるのでインデックスは39個になります。最終的に配列vがモデルを構成するポリゴン情報となります。いう情報は全部モデラーが出力する頂点情報に含まれています。独自のフォーマットを作る場合少なくともこれらの情報をファイルに含める必要があるわけです。頂点宣言さて、上の頂点を描画デバイスに理解させるには「頂点宣言」が必要になります。これは「1頂点のどこに何の情報があるか」を表すオブジェクトで、IDirect3DVertexDeclaration9というインターフェイスで表現されます。0},D3DDECL_END()};この構造体1つで1頂点内の1要素の情報を表します。全部で6つのメンバがありますが、特に大切なのは2番目にある「Offset」、3番目の「Type」、5番目の「Usage」です。Offsetは先に作った1頂点を表すVertex構造体に含まれる項目が何バイト目にあるかを示します。24よってOffsetは右列のようになります。Typeには要素の型とサイズをD3DDECLTYPE列挙型で示します。Vertex::coordはD3DXVECTOR3で、これは実質float3つ分(12バイト)の情報です。その場合はD3DDECLTYPE_FLOAT3を設定します。他にも多数のフラグがありますのでリファレンスを参照して適切なフラグを設定して下さい。Usageはこの要素の使われ方、もう少し別の表現で言うなら「要素の意味・種類」を設定します。coordは「位置座標」です。その場合はD3DDECLUSAGE_POSITIONを設定します。このUsageはシェーダ内の「頂点シェーダの入力セマンティクス」と対応しています。);頂点宣言オブジェクトは、描画の直前に頂点情報と共に描画デバイスに渡すことになります。ここまででポリゴン情報が整いました(^-^)。ボーンの連結ここからはボーンのお話です。言うまでもなく、ボーンには明確な親子関係があります。冒頭の図では0番のボーンが親となり、1,3,5番がその子、さらにそれらの子に孫ボーンが連結しています。親のボーンが動いたら子もそれに合わせて動くことで「骨」が表現されるわけです。普通親には複数の子がいますが、子が複数の親を持つことはありません。この親子関係を表現する方法は色々とありますが、一般的にはいわゆる「ルートボーン」から樹状に骨を表現します:この連結(樹状構造)をプログラムで表現するには、2つの方法が考えられます。*combMatAry;};上3つに注目です。左側は自分の子ボーンを配列として持つタイプの構造体です。右側は自分の第1子ボーンそして次の兄弟ボーンへのリンクを持つタイプです。D3DXFRAME構造体は右側の方法を取っていますね。感覚的には、左の方が子ボーンの配列を持つだけなので上の図とも相性が良く簡単そうに感じますが、実は右側の方がメモリ管理の面で優位です。左の場合、children配列に連続した子ボーン配列を確保する、つまり配列newする必要が出てしまいます。右の方は第1子ボーンと次の兄弟ボーンへの単一ポインタを持てば良いだけで、Bone自体がnewする必要は無く、またポインタの先に幾つ子ボーンが並んでいるのかも心配する必要がありません。私も両方実装してみましたが、右側の方が何だかんだで扱いが良かったと感じました。何も考えずにボーンをバラバラにnewすると、どこか適当な場所に実体が確保されますが、それだと末端のボーンから情報を得るのにツリーを辿っていく必要が出てきます。ボーンには何かとサクサクアクセスしたいのですが、樹状構造だとそれがなかなかに厳しいものがあります。そこで、1モデルのボーンを線形配列としてガバっと確保してしまいます。その後連結作業を行います。モデル単位でこれを保持しておくと、ボーンをいっぺんに消すのも非常に簡単です。樹状構造は親子関係が必要になる計算の時だけ使う事にして、生死はモデルが管理する事にすれば、仕事がうまく分担されます。&bones[5];第1子供が4つに兄弟が2つ。上の図の通りに連結しているのがわかるかと思います。ボーンオフセット行列はボーンの初期姿勢から作れるのですよ!次です。冒頭の右図を再度御覧下さい:緑色の丸で示されているジョイント(ボーン)は「ローカル空間での姿勢」で定義されています。ここからまず「ボーンオフセット行列」を作ります。スキンメッシュアニメーションをする上で一番わかりにくいのはこのボーンオフセット行列かなと思います。これが何であって、どうして必要なのか、スキンメッシュの計算構造が理解できていないとさっぱりわからないんですよね。大丈夫です、ここでじっくり説明します。ボーンオフセット行列とは、ボーンの初期姿勢を「ローカル軸にぴったり戻してしまう」行列です。下の図のように戻してくれる行列です:ボーンオフセット行列はすべてのボーンに1つずつ対応した物があります。いう行列が必要なのか?下の図を御覧下さい:ゆっくり説明しますね。今、初期位置(2,1)で上図のような傾き(回転)を持ったジョイントがあったとします。このジョイントに影響される頂点(黄色丸)が付随しています。このローカルな頂点を先程ちまちまと打ち込んだわけです。この初期位置を基準として、あるフレームでジョイントを(0,4)に移動させるとしましょう。この時もちろん黄色丸で表した頂点も一緒に移動します。移動のさせ方として、直感的には点線矢印で示すように初期位置から移動したい場所までズバっ!と動かすのが良さそうに思います。ところが、こういう移動をさせるには「移動先と初期姿勢との差分」を毎回計算する必要が出て来てしまいます。上の場合ですと(0,4)-(2,1)=(-2,3)という移動量を計算する必要が生じます。実際は回転も入るでしょうから、差分を得るには何らかの逆行列を毎フレーム全ジョイントに関して再計算するという高負荷な手間が生じます。黒矢印で示すように、初期姿勢のジョイントをボーンオフセット行列を使って一端原点に戻し、それを(0,4)の位置に移動させるという、一見すると回りくどいと思われる方法を取るとどうでしょうか。初期姿勢のジョイントを原点に戻すのは毎回同じボーンオフセット行列をかければ良く、さらに移動先である(0,4)に移すための行列は普通モデラーが出力してくれます(もしくはキーフレーム補間で計算)。ジョイントの姿勢をすべて計算済みの「既知」の行列だけから更新する事ができるんです。こちらの方が遥かに低負荷なスキンメッシュアニメーションなんです。いうジョイント(ボーン)の姿勢更新を行うために、初期姿勢を原点に戻すボーンオフセット行列が必須というわけなんです。ボーンオフセット行列を作るのはとっても簡単です。初期姿勢行列はローカル座標の原点にあるジョイントを初期姿勢の位置に移動・回転させるものです(モデラーが出力してくれます)。その逆の動きをさせるのですから「初期姿勢行列の逆行列」を計算すれば良いのです。最終的に自分の親からの相対姿勢に直します。ボーンオフセット行列を計算しておきます。オフセット行列は各ボーンの「ローカル姿勢」の逆行列です。&bones[i].initMat);}各ジョイントの初期姿勢は冒頭のポリゴン設計図にある通りで、Z軸で指定の分だけ回転させています。各ジョイントの座標を行列の_41と_42(それぞれX成分、Y成分に対応)に直接書き込んでいます。して出来上がった初期姿勢行列の逆行列を計算することで、ボーンオフセット行列を計算しています。初期姿勢及びオフセット行列の計算は実際は普通必要ありません。モデラーが出力したファイルからそれらの行列を直接得る事ができます。少なくともボーンの姿勢は絶対にあるはずなので、もしボーンオフセット行列を直接得られなくとも計算してしまえます。独自のモデルファイルを作る時には双方ともファイルに埋め込んでしまいましょう。一つ、上のプログラムにあるcombMatは実は極めて重要です。これはボーンを動かす合成行列の配列で、後述するシェーダに渡すため配列にしています。Bone構造体はみんなこの配列の先頭ポインタを知っていて、姿勢を更新するときに勝手に書き込んでくれます。これについてはず〜っと後で出てくるので忘れないで下さい。初期姿勢を親空間ベースに変換ボーンオフセット行列があれば、あるフレームで各ジョイントのローカル姿勢があればもうスキンメッシュができてしまいます。普通FK(フォワードキネマティクス)をする場合、各フレームでのジョイントローカル姿勢はすぐに得られます。普段はこちらを使用して下さい。今回は完全ホワイトボックスということで、ジョイントローカル姿勢を毎フレーム計算で求めます。IK(インバースキネマティクス)の触りのようなもんです。自前でジョイントを動かそうと思う時、動かす軸を考える必要があります。一つは「自分の親の軸」なのですが、これが意外と扱いにくいんです。親の初期軸は結構色々な方向を向いていて、例えば同じ「Y軸回転」でもあらぬ方向に回転してしまう事があります。それよりも扱い易いのは「自分の初期姿勢の軸をベースに動かす」方法です:上のようなジョイントの更新を実現するには、初期姿勢を親空間ベースに変換する必要があります。[親のローカル姿勢]で求まるためです。プログラムでは現在初期姿勢は「ローカル空間ベース」です。これを親空間ベースに変換するには、子の初期姿勢に親のボーンオフセット行列を掛け算します。「なぜ!?」と思った方は次のように考えてみて下さい。