もちゅろぐ

iOSやSwift、モバイル設計だったりRailsについてまとめていく

DXSDK - MultiAnimation サンプルの解析メモ

DirectX9のサンプルのMultiAnimation をちょっと解析してみた。

人それぞれサンプルの解析の仕方があると思う。

自分の場合は、

  1. ざっと目を通す
  2. 重要そうなファイルを選択する
  3. ソースファイルの整理
  4. ソースのコメント
  5. 不要部分の削除

これが全部終わる頃には、だいたい解析できる。

時間がない場合や規模が小さい場合は、2番で終わってソース読む。

まあ文句いいながら書いてるけど、サンプルには直接関係ないです。

重要そうなファイルを選択する

ファイル一覧
  • AllocHierarchy.cpp
  • AnimationInstance.cpp
  • MultiAnimation.cpp
  • MultiAnimation.h
  • MultiAnimation.fx
  • MultiAnimationLib.cpp
  • Tiny.cpp
  • Tiny.h
クラス一覧
  • CMultiAnimAllocateHierarchy
  • CAnimInstance
  • MultiAnimMC
  • MultiAnimFrame
  • CMultiAnim
  • CBHandlerTiny
  • CTiny


汚い。


 クラスの宣言と定義がばらついている。
出来るだけ1ファイル/1クラスにして欲しい。
じゃないと、ファイル探す時に感覚的に探せない。
 処理を1ソースをまとめるという点から見ても、
ソースを眺める時に関係のないソースが混じって可読性が低下する。
処理をまとめるなら一段階上のフォルダ分けで解決して欲しい。
 いくつかファイル名とクラス名が一致しているのがあるけど
クラス名の先頭には「C」がついているけど、ファイル名にはついてない。
これものちのち響く。

なお略字を普通に使うので、要脳内変換。

つーわけで、1ファイル/1クラスに整理。

ソースファイルの整理整頓完了。
ちょっとクラス眺めて予測でクラス概要書いてみる。

ファイル/クラス一覧
  • MultiAnimation.cpp
WinMain
このサンプルのメイン処理
  • MultiAnimAllocateHierarchy.h/cpp
#0099FF;">class MultiAnimAllocateHierarchy:ID3DXAllocateHierarchyの実装クラス
このクラスが階層メッシュの読み込みの必要処理を行ういわばビルダー的役割を持っている。そのためずっと保持する必要はない。
各ヘルパー関数を使う時だけ宣言すれば十分。
  • MultiAnimFrame.h/cpp
#0099FF;">struct MultiAnimFrame:D3DXFRAMEの派生クラス。継承しているだけで、特に処理はしていない。
  • MultiAnimMC.h/cpp
#0099FF;">class MultiAnimMC:D3DXMESHCONTAINERの派生クラス。MCは(M)esh(C)ontainerの略
  • AnimationInstance.h/cpp
#0099FF;">class AnimationInstance:アニメのインスタンスカプセル化したもの。1つのアニメコントローラを持っている。
  • MultiAnim.h/cpp
#0099FF;">class MultiAnim:アニメを制御するための上4つを管理するクラス。ここでメッシュ読み込み関数が呼ばれている。
  • Tiny.h/cpp
#0099FF;">class CBHandlerTiny:ID3DXAnimationCallbackHandlerの派生クラス。足音をコールバックされたタイミングで再生している。
#0099FF;">class Tiny:みんな大好きティニーちゃんのキャラクラス。ティニーちゃんが嫌いな人はPlayerInsなり変えてください。

一個ずつ解析

コンテナやフレーム部分は、サンプルの SkinnedMesh でも見てください。

って言いたい所だけど、昔学生の頃 http://www.t-pot.com/ のサイトのxファイルの表示サンプルに
かなり助けられたことがあるので、自分もそんな頑張る学生の手助けをしようと思う。

WinMain(簡単に)

 流れを簡単に説明。

  1. コールバックの登録
  2. UIやダイアログ、カメラ設定、デバイス作成時の情報設定
  3. DX周りの初期化
  4. ウィンドウ作成
  5. DirectSoundの初期化
  6. DirectDeviceの作成
  7. メインループ
  8. 解放処理

MultiAnimFrameの前に

 このページを観覧している人の大半が、DXでスキンメッシュができない人だと思うので、
スキンメッシュの概要を自分の頭だけで、書いてみる。

画像のボーンと言われる部分が、人間で言う骨であり、DXプログラムで言うフレームと呼ばれるもの。
画像のウェイトと言われる部分が、人間で言う筋肉であり、DXプログラムで言うメッシュコンテナと呼ばれるもの。


 骨(フレーム)を動かせば、筋肉(メッシュコンテナ)が動く。

そのためフレームの中にメッシュコンテナがあったほうが都合が良い。
骨(フレーム)はモデルによってまちまちで、少ない場合もあれば、多い場合もある。
そして骨にも名前があるように、フレームにも名前が存在する。
右骨、左骨があるようにフレームにも兄弟が存在する。
肩→肘→手首→指があるようにフレームにも息子と呼ぶ配下が存在する。
人間でいうアダムとイブが、ルートと呼ばれる一番上の親フレームになる。
 

上記のことをイメージで捉える程度の知識で問題ない。

MultiAnimFrame

 骨だ。D3DXFRAME の派生構造体になる。
D3DXFRAME の中身は、次のようになっている

typedef struct _D3DXFRAME
{
    LPSTR                   Name;// 骨の名前だ
    D3DXMATRIX              TransformationMatrix;// 骨の位置だ

    LPD3DXMESHCONTAINER     pMeshContainer;// 筋肉だ

    struct _D3DXFRAME       *pFrameSibling;// 兄弟だ
    struct _D3DXFRAME       *pFrameFirstChild;// 息子だ
} D3DXFRAME, *LPD3DXFRAME;

このフレームの一番上、いわゆるルートを使って、描画や姿勢更新がされる。

MultiAnimMC

 筋肉だ。D3DXMESHCONTAINER の派生構造体になる
D3DXMESHCONTAINER の中身は、次のようになっている

typedef struct _D3DXMESHCONTAINER
{
    LPSTR                   Name;//筋肉の名前だ

    D3DXMESHDATA            MeshData;//筋肉そのものだ

    LPD3DXMATERIAL          pMaterials;//マテリアル情報
    LPD3DXEFFECTINSTANCE    pEffects;//エフェクトのインスタンス情報
    DWORD                   NumMaterials;//マテリアル数
    DWORD                  *pAdjacency;//隣接情報

    LPD3DXSKININFO          pSkinInfo;//スキンインフォ

    struct _D3DXMESHCONTAINER *pNextMeshContainer;
} D3DXMESHCONTAINER, *LPD3DXMESHCONTAINER;

派生している理由は、なんとこの筋肉は、テクスチャ情報を持っていないのだ。

続いて派生した MultiAnimMC の中身は次のようになっている

struct MultiAnimMC : public D3DXMESHCONTAINER
{
    LPDIRECT3DTEXTURE9 *m_apTextures;// テクスチャ
    LPD3DXMESH          m_pWorkingMesh;// 作業用メッシュ
    D3DXMATRIX *        m_amxBoneOffsets;  // 各ボーンのオフセット行列
    D3DXMATRIX **       m_apmxBonePointers;  // 各ボーンの行列 アクセス速度を向上させるためポインタを持たせている

    DWORD               m_dwNumPaletteEntries;//影響するパレット数
    DWORD               m_dwMaxNumFaceInfls;//影響する最大頂点数
    DWORD               m_dwNumAttrGroups;//属性グループの数
    LPD3DXBUFFER        m_pBufBoneCombos;//ルート行列からこのメッシュまでの合成行列

    HRESULT SetupBonePtrs( D3DXFRAME * pFrameRoot );
};

テクスチャ以外のものは、アクセス速度の便宜上であったり、
ブレンディングメッシュを作成するために必要なパラメタになる。
ブレンディングメッシュは、MultiAnimAllocateHierarchy::CreateMeshContainerで作成する

MultiAnimAllocateHierarchy

 DirectXには、アニメーションをサポートするためのインターフェイスが、用意されています。
そのインターフェイスは、様々なケースに対応できるように
インターフェイスの重要部分に関しては実装をユーザに任せています。


ID3DXAllocateHierarchy クラスもその一つです。
ちなみに Hierarchy:階層 です。発音は、ホントか分からないけど、昔ウェブで「ヒエラルキー」って書いてた。


あまり慣れていない人が、このクラスを見たらわけが分からないはず。
STDMETHOD ってコードがメソッドらしき宣言に大量にくくられている。
VC使っている人は落ち着いて STDMETHOD にカーソルを合わせて F12 押してください。
VC使っていない人は聞いてください。単なるマクロです。

宣言しているメソッド

全部 仮想関数です。
こちらで直接呼ぶ必要はなく、特定のタイミングになったら自動で呼ばれます。

CreateFrame
フレームを作るタイミングで呼ばれます。
CreateMeshContainer
メッシュコンテナを作るタイミングで呼ばれます。
DestroyFrame
フレームを破棄するタイミングで呼ばれます。
DestroyMeshContainer
メッシュコンテナを破棄するタイミングで呼ばれます。
SetMA
MultiAnimクラスのセットするだけです。このメソッドはクラスの独自のメソッドです。
CreateFrame

 このメソッドは、階層メッシュの読み込み時に呼ばれます。
目的は、フレームの容量確保と、フレームの名前領域の容量確保です。

CreateMeshContainer

 このメソッドは、階層メッシュの読み込み時に呼ばれます。
目的は、

  • メッシュコンテナの容量確保
  • メッシュコンテナの名前領域の容量確保
  • メッシュコンテナのメンバ変数の初期化


 メッシュコンテナのメンバ変数の初期化は

  • メッシュ上書き
  • 隣接情報の上書き
  • マテリアルのコピー
  • テクスチャの作成
  • スキン情報のコピー
  • スキン情報を元にボーンの姿勢行列の領域確保
  • 通常メッシュからブレンドメッシュの作成
  • 姿勢行列の作業領域の確保
  • メッシュのFVFの対応
  • GeForce3対応処理

処理は長いけど、1つ1つは単純。
姿勢行列の作業領域の確保 の理由は、HLSLでまとめて行列を渡す必要があるため。

DestroyFrame

 CreateFrameで確保した領域を開放する

DestroyMeshContainer

 CreateMeshContainerで確保した領域を開放する。

AnimationInstance

 アニメーションコントローラ(LPD3DXANIMATIONCONTROLLER)を保持したクラス
このクラスで、描画や姿勢更新を行っており、クラス1個で1キャラのアニメを制御している。

UpdateFrames( MultiAnimFrame * pFrame, D3DXMATRIX * pmxBase )

 階層メッシュの姿勢行列を更新するメソッド。
第一引数にはルートフレームを入れる、第二引数には、モデルの基本行列を入れる
このメソッドの中で、第二引数の行列をベースに第一引数のルートフレームを自分→兄弟→息子と、順番に姿勢行列を求めていく。

DrawFrames( MultiAnimFrame * pFrame )

 階層メッシュの描画をするメソッド
第一引数には、更新メソッドと同じでルートフレームを入れる。
そして自分→兄弟→息子の順に描画を行う。
このメソッドはフレーム間の移動をして描画メソッドを呼んでいるだけで、
直接メッシュの描画を行っているのは、次の DrawMeshFrame になる

DrawMeshFrame( MultiAnimFrame * pFrame )

 メッシュコンテナを描画するメソッド。
メソッドの処理自体はさほど難しくない。

  1. 渡されたフレームのメッシュコンテナを取得
  2. メッシュコンテナの中に入っているボーン行列バッファを LPD3DXBONECOMBINATION へ変換して取得する
    この変換した LPD3DXBONECOMBINATION は配列としてアクセスできる。その要素数は、メッシュコンテナの m_dwNumAttrGroups の数になる。
  3. 属性グループの数だけ繰り返す
    1. 各行列を合成
    2. その結果をエフェクトへ送信する
      エフェクト自体は MultiAnim クラスが保持している
    3. テクスチャの送信
    4. 現在のボーン数を送信
    5. テクニックの設定
      テクニック自体は MultiAnim クラスが保持している
    6. メッシュの描画

MultiAnim

 上記のクラスをまとめるクラスになる。

  • ルートフレーム(MultiAnimFrame)を保持
    • フレームの中には、MultiAnimMCがある
  • アニメインスタンス(AnimationInstance)を保持

ルートフレームを保持し、かつルートフレームのアニメコントローラのインスタンスを生成する。
このアニメコントローラのインスタンスは、後で説明する Tiny クラスが受け取る。

やっかいなアニメーションコントローラ

 メッシュの場合だと、行列さえ変えれば普通に描画できたのだが、
アニメコントローラは、内部的問題で使い回しができない。
2体のモデルを描画する場合は、2個のアニメコントーラが必要になる。
そのためアニメコントローラをどこかで、コピーする必要がある。
 サンプルの場合は、MultiAnim で行っている


 一応コアとも言える部分なので、主要メソッドを各個説明する。

CreateInstance( AnimationInstance ** ppAnimInstance )

 アニメインスタンスを作成する。
このメソッドの中で一個上であげた、アニメコントローラのコピーを行っている。
そのコピーが終わったら後は、アニメインスタンスのクラスを動的生成して、初期化を行っているだけ

SetupBonePtrs( MultiAnimFrame * pFrame )

 各フレームのメッシュコンテナのボーン行列の配列を初期化している。
ボーン行列の個数領域分確保を行い、中身には、各ボーンの初期行列を入れている。
 

Setup( LPDIRECT3DDEVICE9 pDevice, WCHAR sXFile, WCHAR sFxFile, MultiAnimAllocateHierarchy *pAH, LPD3DXLOADUSERDATA pLUD )

 俗に言う ロード処理 になる。
処理を順次辿ってみる

  1. エフェクトの作成
  2. 階層メッシュの読み込み
  3. ボーンの初期化
    上の SetupBonePtrs になる
  4. 階層メッシュのスフィアの取得
  5. アニメインスタンスの更新

重要な部分といえば、2.3.5.だろうか。
2.で階層メッシュを読み込む関数を呼び出している。
3.では、読み込み終わったフレームの初期化を行っている
5.は、このメソッドが呼ばれる前にアニメインスタンスがあった場合は、アニメコントローラを作成しなおしている。
理由は、オリジナルのアニメコントローラを作り直しているため。

Cleanup( MultiAnimAllocateHierarchy * pAH )

 解放処理。以上

CreateNewInstance( DWORD * pdwNewIdx )

 アニメインスタンスの生成
前の解説した CreateInstance をここで呼んでいる。
インスタンスの生成自体は CreateInstance で終了していて、ここでの処理は、
管理用配列に新しく作成したアニメインスタンスを追加しているだけになる。
 引数には、配列のIdxを返している。
これを行う事によって、容易にアニメインスタンスにアクセスできるため。

Draw()

 描画。
管理用配列に存在するアニメインスタンスの描画を一斉に呼び出している。
だけど、この描画はサンプル用に用意したように思えるくらいにシンプル。

Tiny

俗に行くキャラクターインスタンス
サンプルでは管理を MultiAnimation.cpp の vector 配列で済ましている。
ここでは、二つのクラスを定義している。

CBHandlerTiny

 ID3DXAnimationCallbackHandler クラスの派生クラスになる。
これは、アニメコントローラの AdvanceTime の第二引数用のコールバッククラスになる。
このコールバックが呼ばれるタイミングの設定場所や、設定方法は後述する。
このコールバックをうまく使えば、「アニメーションの特定タイミングで、手の位置から炎を生成させる」
などといった、インタラクティブな制御が可能になる。

Setup( MultiAnim *pMA, vector< Tiny* > *pv_pChars, CSoundManager *pSM, double dTimeCurrent )

 アニメーションのための準備と、クラスの初期化

  • MultiAnimを使ってアニメインスタンスの作成
  • アニメインスタンス作成時に取得したIdxを使って、アニメインスタンスを取得する。
  • 位置初期化
  • 各アニメのidxを取得
  • コールバックデータ用の初期化
  • 足音の初期化
  • 姿勢の初期化
  • Tiny配列の追加
Animate( double dTimeDelta )

 アニメーションを指定した時間だけ更新。

  1. 移動更新
  2. アニメーションのスムース処理
  3. 行列処理
AdvanceTime( double dTimeDelta, D3DXVECTOR3 *pvEye )

 コールバック処理と、アニメインスタンスの時間進行を行う。

  1. コールバックデータの更新
  2. ローカル時間の進行
  3. アニメインスタンスの時間進行
AddCallbackKeysAndCompress( LPD3DXANIMATIONCONTROLLER pAC, LPD3DXKEYFRAMEDANIMATIONSET pAS, DWORD dwNumCallbackKeys, D3DXKEY_CALLBACK aKeys[], DWORD dwCompressionFlags, FLOAT fCompression )

 アニメにコールバックを追加します。

  • 圧縮用バッファの作成。これはアニメーションセットデータの作業領域。
  • D3DXCreateCompressedAnimationSet で、圧縮されたアニメセットの作成。
  • さっき作った作業領域を開放
  • 圧縮されたアニメセットの前のアニメセットを破棄。
  • 圧縮されたアニメセットをアニメコントローラに登録
SetupCallbacksAndCompression()

 圧縮されたキーデータとコールバックの初期化
圧縮されたキーデータの初期化
上のメソッドを使ってコールバックの追加を行う。

SetMoveKey()

 アニメの切り替わりの制御
トラックからトラックの切り替わりを行ってアニメの切り替えを制御している。

一通り解析完了

 後半の解析になればなるほど、「これ、解説必要なのかな・・・」ってものまで、
とりあえず解説しているので、もしかしたらかえって見づらいかもしれない。


 全体的に3つに分かれてた。

でもスキニングメッシュの実装部分とアニメコントローラの実装部分が醜かった。

フレンドしまくりで、あれじゃクラスでカプセル化してる意味がない。
修正したくなったけど、解析が目的なのでやめた。

残念ながら

 本当なら最後に今回解析につかったMultiAnimサンプルをプロジェクトごとzipでアップして置こうと
考えていたのに、どーやらはてなでは、データのアップができないみたい。

 もし間違いを見つけたら指摘してくれると助かります。