ホーム < ゲームつくろー! < DirectX技術編 < スキンメッシュオブジェクトのコピーを考える

その38 スキンメッシュオブジェクトのコピーを考える


 ゲームには沢山のキャラクタが登場しますが、現実世界のように1つ1つが個性を持って別々に存在するわけにはいきません。大地に無数にある草木や家々を構成するレンガのすべてが完全に独立した情報を持っていては、それを格納するメモリがいくらあっても追いつかなくなります。このことから、ゲーム製作では昔から必ず何らかの「参照」が行わるわけです。昨今の3D全盛の時代においてもそれは変わりません。ただ、1つのキャラクタを表現するのに必要な情報の量と種類は今の方が莫大に多くなっているため、同じキャラクタを複製する時に何をコピーして何を参照するべきかをしっかり見極めなければなりません。

 中・大規模のゲームになると、殆どの場合スキンメッシュアニメーションをするキャラクタを複製する必要にかられます。巨大なデータと複雑な仕組みで動くスキンメッシュアニメーションでは、複製時に何をコピーして何を参照したら良いのでしょうか?これをしっかりと整理すると、スキンメッシュをサポートするゲームオブジェクトのクラス設計を適切に行う事ができるようになります。

 この章では、中・大規模なゲーム製作で絶対に必要になる「スキンメッシュアニメーションオブジェクトのコピー」について考えてみます。最初に申し上げおきます。この章、物凄いごっついです。これまで本サイトで紹介してきた技術がてんこ盛りで総動員です。また内容が複雑なため何度か加筆修正が行われる可能性がありますので、予めご了承下さい。



@ スキンメッシュアニメーションに必要な要素の参照とコピーの部類分け

 スキンメッシュアニメーションをするオブジェクトを複製する時に、必要な要素(メンバ変数)についてコピーするべきか参照で良いのかを検証してみましょう。

○ 頂点(ポリゴン) → 参照

 ゲームオブジェクトを表現するには、兎にも角にも「頂点(ポリゴン)」の情報が必要です。これが無いと、キャラクタは3D空間に目に見える形で存在できません。ID3DXMeshインターフェイスによって管理される頂点は、基本的に「固定物」です。ID3DXMeshとして扱う膨大な頂点の座標を変更することはあまりありません。「でも、スキンメッシュは頂点を動かす技術でしょ?」と思われるかもしれませんが、スキンメッシュアニメーションは元の頂点位置から移動先を毎回算出しているだけでして、元の頂点自体は動かしていないんです。このことから、1つのキャラクタを表すID3DXMeshは参照できることになります。ポリゴン頂点は情報量が膨大なだけに、これはありがたい事です。気をつけるのは、1つのキャラクタを表現するのにメッシュがいつも1つとは限らないという点です。むしろ複数あるものだと考えた実装をすべきでして、この取り扱いには注意を要します。

○ マテリアル情報 → コピー

 メッシュのサブセットごとの色や表面の質を表すマテリアル情報は基本的には固定情報なのですが、ディフューズカラーやアンビエントなどは個々によって異なる場合があります。特にディフューズカラーはアルファ値を変化させる事でオブジェクト自体を気軽に半透明にするのに活用できます。この事から、マテリアル情報は個々に持たせた方が良いかもしれません。別の方法として、デフォルトでは参照しておいて、マテリアルを個々で変化させたい時にだけ独自にマテリアル情報を作成して参照させるという方法が取れますが(FlyWeightデザインパターン)、今回はコピーする事にします。

○ ワールド変換行列 → コピー

 ID3SXMeshインターフェイスが保持する頂点は「ローカル座標空間」で定義されるので、そのままだと世界に配置できません。ゲームオブジェクトを世界に置くには「ワールド変換行列」を定義しておく必要があります。世界の空間で自分がいる位置は管理人が管理しても良いのですが、自分で持っていて悪い事はありません。よって、ワールド変換行列はコピーする事にします。

○ フレーム行列 → 参照

 オブジェクトの骨組みであるフレーム行列(D3DXFRAME構造体)は、個々のオブジェクトのある時点での姿勢をローカル座標空間で定義する極めて重要な行列群です。姿勢は個体情報であり個々が持つべきかなと感じるかもしれませんが、実は参照の方が都合が良いんです。参照にする一番の、インターフェイスの仕様上の理由としては、フレーム行列と関係するID3DXAnimationControllerインターフェイスとの兼ね合いがあります。ID3DXAnimationControllerインターフェイスは、自身のコピーを生成するCloneAnimationControllerメソッドを持つのですが、これによって作られるコピーインターフェイスはコピー元のフレーム行列を共有して参照してしまうんです。よって、フレーム行列をコピーしてしまうと、動きの再定義をする必要にかられます。これは出来なくはありませんが、結構面倒な作業を要します。フレーム行列を参照する前提で実装をすると、ID3DXAnimationControllerインターフェイスとの相性が良くなります。別の理由として、個々のオブジェクトの最終的な姿勢は、フレーム行列を途中で計算に入れながら「ボーン合成行列」として別枠で出力されます。計算の途中過程にしか使わないボーン行列を保持しておく意味はあまりないんです。以上から、フレーム行列は参照でも十分です。

○ ボーン合成行列配列 → コピー

 ボーンオフセット行列IDと1対1の関係にあり、1つ1つのボーンをワールド空間にダイレクトに変換するボーン合成行列の配列は、個々が持って良い情報になり得ます。ボーンの数だけ行列があるのでサイズとしては結構な大きさになってしまいますが、オブジェクトの姿勢の最終出力であり、衝突判定や描画時に使用できる情報なだけにどうしても必要になってしまいます。スキンメッシュを行わないキャラクタであれば、この行列は一般に非常に小さくなりますので、サイズの問題は差ほどでもなくなります。

○ アニメーションコントローラ → コピー

 アニメーションの全てを管理するアニメーションコントローラは、先ほども述べましたように自身をコピーするメソッドを持っています。これにより、複製キャラクタ個々に独立したアニメーションを実現できます(ただし、フレーム行列は共有されます)。このことから、アニメーションコントローラはコピーして使用するようにします。

○ スキン情報 → 参照

 ID3DXSkinInfoインターフェイス、動かしたボーンと頂点の対応を記述したD3DXBONECOMBINATION構造体配列は、いずれもスキンメッシュ特有の情報です。ID3DXSkinInfoインターフェイスはキャラクタの生成時に必要になりますが、通常はあまり使いません。一方D3DXBONECONBINATION構造体は描画時に必要になります。ただ、これらは1メッシュに対してほぼ完全に固定された情報ですから、参照で十分です。

○ テクスチャ → 参照

 オブジェクトに貼り付けられるテクスチャは、殆どの場合同じ物が貼り付けられます。テクスチャはIDirect3DTexture9インターフェイスとしてまとめられますので、参照で十分です。ただし、オブジェクトによっては別のテクスチャを貼りたい時もあります。よって、FlyWeightパターンのように参照先を取り替えられる仕組みを作っておくと便利です。


 以上のように、スキンメッシュアニメーションをするゲームオブジェクトには参照とコピーが入り乱れています。ただ、上のようにまとめておくと、ゲームオブジェクトクラスのメンバを参照(ポインタ)にするのか実体にするのか非常に明白になります。では、具体的なクラスのメンバ変数の宣言部分について考えてみる事にしましょう。



A コピーを考えたゲームオブジェクトクラスの構想

 ここからは、コピーを考えたゲームオブジェクトクラスを構想していきます。まずは@で挙げた様々な要素の繋がりを表した関連図をご覧下さい:

 非常に巨大で複雑な図ですいません。これでも相当に整理したつもりなんです(涙)。ウィンドウをぐいっと大きくしてご覧下さい。

 この図をどう見るかこれから説明致します。この図は大きく2つに分かれています。1つは「ゲームオブジェクト」と名付けられている黄色い枠内です。これはゲームオブジェクトクラスを表しています。もう1つは破線で囲まれている部分で、これはゲームオブジェクトを生成する「ゲームオブジェクトファクトリクラス」です。ゲームオブジェクトを新設したい場合、ユーザはゲームオブジェクトファクトリに対して生成要求を出します。ファクトリはユーザが指定したXファイル情報に基づいて破線内の全ての構造体・オブジェクトを生成し、またそれを関連付けてゲームオブジェクトとして出力します。ゲームオブジェクトファクトリ内にある「テクスチャストッカー」というオブジェクト以外は、ファクトリが動的にメモリを確保して、ゲームオブジェクトに渡してしまいます。ファクトリ自身はそれらを保持しません。

 ゲームオブジェクトが持つべきメンバ変数の実体と参照は「ゲームオブジェクト」内に記述してあります。緑四角で表されたメンバ変数は実体(コピー)を持つ事を表しています。一方丸で表されたメンバ変数は「ポインタ変数」を表します。この実体は当然ですがポインタの先にありますので、オブジェクト自体は保持しません。青い枠の羅列がありますが、これは配列を意味しています。ゲームオブジェクトクラスには、図に示すように沢山の変数が含まれる事になります。ただ頂点・テクスチャ・フレーム行列など極めて大きな情報はポインタなので、実はゲームオブジェクト自体は結構軽いデータなんです。

 下の点線枠で囲まれたゲームオブジェクトファクトリクラスの仕事は極めて多くて複雑ですが、それについてはDirectX技術編その25〜28辺りで詳しく説明しております。

 ゲームオブジェクト内にある各メンバ変数について、以下で補足説明を列挙致します。

○ サブメッシュオブジェクト

 これはメッシュへのポインタ、マテリアル配列そしてテクスチャへのポインタを格納した構造体(もしくはクラス)です。ID3DXSkinInfoインターフェイスなどのスキン情報、もしくはもっと大胆にD3DXMESHCONTAINER構造体へのポインタを含めても良いかもしれません。図ではこれを配列として保持するようにしています。これは1つのキャラクタを表現するのに複数のメッシュが使用されることがあるためです。各々のメッシュには親子兄弟関係があるのですが、それはフレーム行列がうまく表現してくれていますので、ここでは独立として扱って問題ありません。ただ、以後のポインタやインターフェイスにも言えることですが、それらの「参照形式」な変数については、スマートポインタやCOMポインタなどでしっかりと包む必要があります(もうこのレベルになるとスマートポインタCOMポインタの知識は必須です)。それをしないと、原型が無くなった時に、複製の参照は全部メモリ保護違反となってしまいます。

○ フレームオブジェクト

 フレーム行列はD3DXFRAME構造体のツリー構造で表現されます。ツリーを保持するにはルートのポインタを持てば十分なのですが、実はフレーム行列についてはそれほど単純な話ではないんです。沢山の問題点を含んでいますので、少しずつ整理していきます。まず、ツリー構造をなすD3DXFRAME構造体は全て動的に作成され、ポインタで共有されます。この生成責任はID3DXAllocateHierarchyインターフェイスにあります。そして、この消去責任(消去方法を知っているの)もこのインターフェイスにあります。つまり、フレームツリーとID3DXAllocateHierarchyインターフェイスは切り離せないんです。両者を一緒にするにはコンポジションとして2つを持つか、一方を他方が吸収するかしかありません。今回はID3DXAllocateHierarchyインターフェイスの派生型である「フレームクラス」がD3DXFRAME構造体を持つ形にしました。この派生クラスのデストラクタでフレームツリーは綺麗に消去される事になります。

 ゲームオブジェクトクラスが持つフレームオブジェクトへのポインタはもちろんCOMポインタです。そうしておけば、フレームツリーを誰も使わなくなった時点で、自動的に消去が実行されます。こうすることによって、ユーザは複雑な参照カウンタの操作や消去責任の管理・ダングリングポインタの恐怖から解放されます。COMポインタを使ってみればわかるのですが、この恩恵は本当に計り知れないんです!DirectXでゲームを作ろうと思っていてCOMポインタを知らない方は是非使用してください!

○ ボーン合成行列配列

 1つ1つのボーンをワールド空間にダイレクトに置くボーン合成行列配列は、姿勢の更新時にフレームツリーを辿りながら更新されていきます。図にあるD3DXFRAME構造体には「ボーンオフセット行列」と「オフセット行列ID」を追加してあります。このオフセットIDとボーン合成行列配列の要素番号は完全に一致させます。これにより、ツリーを辿りながら正しい要素番号の合成行列を更新していけます。また描画時にはD3DXBONECOMBINATION構造体が持つサブメッシュごとのボーングループ番号から、デバイスに正しいワールド変換行列を渡せるようになります。詳しくはDirectX技術編その28をご覧下さい。この配列はゲームオブジェクトを新規作成するときにゲームオブジェクトファクトリクラスが責任を持って指定することになります。ゲームオブジェクトクラスはそれを設定するメソッドを公開します。

○ テクスチャストッカー

 テクスチャストッカーというのは、リソースであるテクスチャの生成責任と保持責任のあるクラスです。これはファクトリクラスに登録されます。このクラスの役目は同じテクスチャを無駄に複製しないように、要求されたテクスチャがすでに生成されていればそのポインタを、生成されていなければ生成してそのポインタを渡します。これはFlyWeightデザインパターンの典型です。大量ビデオメモリの時代になたっとは言え、強烈に消費されるビデオメモリを節約する事は大切なんです。ゲームオブジェクトファクトリクラスは、テクスチャの生成をこのクラスに委譲して、ゲームオブジェクトにそれを渡します。ゲームオブジェクトがテクスチャを新たに要求できるように、このクラスのオブジェクトへのスマートポインタを保持しても良いかもしれません。


 以上から、ゲームオブジェクトクラスを始めとした各クラスのメンバ変数部分は次のようになりそうです:

ゲームオブジェクトクラス群
// 型表記変換
typedef vector< D3DMATERIAL9 >        VctD3DMAT9;
typedef Com_ptr< IDirect3DTexture9 >  ComTexture;
typedef vector< ComTexture >          VctComTex;
typedef sp< MYD3DXFRAME >             SPMYFRAME;      // フレーム構造体のスマートポインタ
typedef sp< CFrameObject >            SPFrameObject;  // フレームオブジェクトのスマートポインタ
typedef Com_ptr< IGameObject >        ComGameObject;  // ゲームオブジェクトインターフェイスのCOMポインタ


/////////////////////////
// 拡張フレーム構造体
/////
struct MYD3DXFRAME : public D3DXFRAME
{
public:
   D3DXMATRIX m_Offset;   // ボーンオフセット行列
   DWORD m_dwID;          // ボーンオフセット行列ID
};


///////////////////////////////
// メッシュオブジェクトクラス
/////
class CMeshObject : public IMeshObject
{
protected:
   Com_ptr< ID3DXMEsh > m_cpMesh;   // メッシュ(COMポインタ)
   VctD3DMAT9 m_vctMaterial;        // マテリアル配列
   VctComTex m_vctTex;              // テクスチャ配列(COMポインタ)

public:
   // メンバ変数(省略します)
};


////////////////////////////////
// フレームオブジェクトクラス
/////
class CFrameObject : public ID3DXAllocateHierarchy
{
protected:
   SPMYFRAME m_spRootFrame;   // ルートフレーム(スマートポインタ)
};


//////////////////////////////
// ゲームオブジェクトクラス
/////
class CGameObject : public IGameObject
{
protected:
   SPFrameObject m_spFrameObj;           // フレームオブジェクト(スマートポインタ)
   vector< CMeshObject > m_VctMeshObj;   // メッシュオブジェクト配列
   D3DXMATRIX m_WorldMat;                // ワールド変換行列
   vector< D3DXMATRIX > m_VctCombMat;    // 合成行列配列
   Com_ptr< ID3DXAnimationController > m_cpAnimCont;   // アニメーションコントローラのCOMポインタ

public:
   virtual bool CloneObject( IGameObject **ppObject );   // コピーメソッド
};


///////////////////////////////
// テクスチャストッカークラス
/////
class CTextureStocker : public ITextureStocker
{
protected:
   map< string, ComTexture > m_TextureMap;   // テクスチャマップ
};


////////////////////////////////////////
// ゲームオブジェクトファクトリクラス
/////
class CGameObjectFactory : public IGameObjectFactory
{
protected:
   sp< CTextureStocker > m_TexStocker;   // テクスチャストッカー(スマートポインタ)

public:
   virtual bool CreateGameObjectFromX( string filename, ComGameObject &obj );   // ゲームオブジェクト生成
  virtual bool CreateGameObjectFromXInMemory( void* memory, ComGameObject &obj );   // メモリからゲームオブジェクト生成
};

 あっはっは、ごっつい(笑)。自分で書いていて笑いが出てきました。

 スキンメッシュアニメーションはどう簡潔にしてもやっぱりこのくらいのレベルの実装になってしまいます。これにメソッドの実装が加わるのですから、げんなりですよね。でもこの設計でスキンメッシュアニメーションをするオブジェクトの複製が非常に楽に、混乱する事無しに行えるのですから、実装する価値は十二分にあります。

 上記クラス群のメンバメソッドについてのお話しは、正直もうへとへとなので(^-^;、ここでの検討はやめておきます。この章の目的は、ここまでで果たせているものと思います。



B 後書き

 普段はあまり後書きを付記しないのですが、今回はさすがに書きたくなってしまいました。

 1つのオブジェクトを表示させてアニメーションするだけでも大変な作業ですが、そのコピーもサポートしようとなると本当に大変です。スキンメッシュアニメーションの全容の把握が必要ですし、要素の分割や削除、ダングリングポインタの防止のためのスマートポインタやCOMポインタなど、これまで本サイトで説明してきた知識と技術のフル活用になります。

 実は、この章を書こうと思い立った動機は、現在秘密裏に製作しているSTGで「弾を大量に複製する」必要にかられたためでした。STGの弾ほどめちゃくちゃに複製されるオブジェクトはそうありません。個々の弾が独立に、色々な方向を向いて、あまつさえ簡単なアニメーションをしているとしたら、これはもうりっぱな「アニメーションオブジェクト」です。実装するにあたり、パフォーマンスとリソースの消費からメッシュを複製するわけには行かない事はすぐにわかりましたが、その他の位置やアニメーションなどの情報についてはどうして良いやらさっぱりだったんです。光明が見えたのがスキンメッシュアニメーションの構造を理解した事、そして複製したアニメーションコントローラが複製元のフレーム行列を参照している事を確認した事でして、これにより前章(その37)でのアニメーションコントローラの記事、そして本章のクラス構想へと繋がりました。スキンメッシュアニメーションの実装や複製の問題は、かなり前から考えて試行錯誤設計していたのですが、ここに来てようやく解決が見えた気がしています。何よりも、それをドキュメントとして残せた事にほっとしています。こんなの触れていないと3日ですっきり忘れますからね(笑)。

 それにしても、本当に世の中のゲーム製作事情は恐ろしいもんです。調べれば調べるほど苦笑いが出ます。現在のゲーム製作は悲しいかな(?)これが必要最低限でして、これにさらに描画のための技術やゲームシステムが加わります。いやホント、苦笑いです(^-^;。プロのゲームプログラマ以外で、今回の記事を一度でぱっと理解できる方は本当に稀有だと思いますが、ゲームプログラマを目指す方には是非とも理解して欲しい所でもあります。

 兎にも角にも、ここまでお読み下さったすべての皆さん、お疲れ様でした。