ホーム < ゲームつくろー! < DirectX技術編 < アニメーションの根っこ:スキンメッシュを上から下まで

その28 アニメーションの根っこ:スキンメッシュを上から下まで


 前章まででスキンメッシュの仕組みをじんわりと説明してきました。この章ではこれまでの内容を踏まえて、スキンメッシュの読み込みから描画までどどっと全部やってみます。



@ まずは読み込み〜フレーム構造体の拡張

 スキンメッシュアニメーションで一番最初にする事はメッシュデータをXファイルから取得する事です。これはD3DXLoadMeshHierarchyFromX関数を用います。

D3DXLoadMeshHierarchyFromX関数
HRESULT D3DXLoadMeshHierarchyFromX(      
    LPCTSTR Filename,
    DWORD MeshOptions,
    LPDIRECT3DDEVICE9 pDevice,
    LPD3DXALLOCATEHIERARCHY pAlloc,
    LPD3DXLOADUSERDATA pUserDataLoader,
    LPD3DXFRAME* ppFrameHeirarchy,
    LPD3DXANIMATIONCONTROLLER* ppAnimController
);

この関数についての詳細はこちらをご覧下さい。一番大切なのは第4引数のpAllocです。ここにはID3DXAllocateHierarchyインターフェイスへのポインタを与えますが、この実体はユーザが独自に実装する必要があります。この実装の一例についてはDirectX技術編その25『アニメーションの根っこ:オブジェクトを読み込む』に詳しく記載しておりますのでご覧下さい。このインターフェイスのメンバ関数であるCreateFrameメソッド及びCreateMeshContainerメソッドに、スキンメッシュに必要な情報が全て渡されますので、関数内で余すところ無く保存します。「必要な情報」とは具体的に何か?それは大きく2つあります。1つは「D3DXFRAME構造体」に格納される情報です。これは3Dオブジェクトの「骨格」の繋がりと動きの情報で、D3DXFRAME構造体をツリー構造で繋ぐ事で表します。もう1つは「D3DXMESHCONTAINER構造体」。これはオブジェクトの「皮膚(スキン)」に関する情報を一まとめにした構造体です。ID3DXAllocateHierarchyインターフェイスでは、これらの情報を格納する実装を行います。

 D3DXFRAME構造体は骨格の動きの定義だと申しましたが、それがどういうことか次のイメージ図をご覧下さい:


 左側の座標がボーンの連結をイメージしています。右側が左のボーンの繋がりをツリー構造で表した図で、D3DXFRAME構造体は実際にこのような状態になっています。D3DXFRAME構造体には「ボーンが親ボーン座標に対してどういう位置と姿勢であるか」を表す行列が1つずつ格納されています。例えばBONE_Aを見ますと、親座標であるローカル座標に対して+30度くらい回転して、平行移動しています。それを表す変換行列がBONE_Aに該当するD3DXFRAME構造体に格納されます。もう1つ見てみましょう。右側のBONE_C1をご覧下さい。BONE_Cの親はBONE_B1です。黄色い矢印BONE_Cは親ボーンの座標に対して+60度くらい回転して、上方向(Z軸方向)に平行移動していますね(小さな図ですいません)。ですから、BONE_Cフレームには「60度回転+平行移動」を表した行列が格納されます。このように、全てのフレームは、それに該当するボーンのローカルな動きを格納しているわけです。ちなみに、フレームに格納される行列は「ボーン行列」と呼ばれます。1つのボーンを動かしたい時はそれに該当するフレームが持つボーン行列を変化させるだけです。ローカルな姿勢をツリー状に格納するD3DXFRAME構造体というのは、そう考えると良く出来ていると思います。

 ボーンの回りには「ポリゴンの頂点」が取り巻いています。ボーンを動かすとそれに合わせる様に頂点が動きます。結局、スキンメッシュはボーンをワールド空間に移動させる事で動きを実現している事に他なりません。ボーンの情報は全てフレームにありますから、D3DXFRAME構造体にワールド変換する行列があると管理しやすくなります。そこで、D3DXFRAME構造体内に各ボーンのワールド変換行列を格納する変数を追加します。
 ボーンに関連するもう1つの行列があります。それがボーンオフセット行列です。これは、ボーンの初期姿勢を表す「固定した」行列です。ボーンを取り巻く頂点を移動させるのに絶対に必要な行列で、1つのボーンに1つ定義されるものですから、これもD3DXFRAME構造体の拡張版に登録する事にしましょう。後、ちょっと蛇足かもしれませんが、「ボーンオフセット行列とボーン行列を掛け算した合成行列」というのも追加する事にします。この合成行列と頂点を掛け算する事で、頂点を一気にワールド空間に変換する事ができるようになります。あると便利な行列なんです。

MYD3DXFRAME構造体
struct MYD3DXFRAME : public D3DXFRAME
{
   D3DXMATRIX WorldMatrix;     // ワールド変換行列
   D3DXMATRIX OffsetMatrix;    // ボーンオフセット行列
   D3DXMATRIX ConbinedMatrix;  // 合成行列
};

後々でこの行列を計算する事になります。



A 読み込み〜メッシュの最適化

 ID3DXAllocateHierarchyインターフェイスに渡されるもう1つの構造体であるD3DXMESHCONTAINER構造体からは、ID3DXMeshインターフェイスが取得できます。さらにXファイル内にスキン情報があった場合、それはすべてID3DXSkinInfoインターフェイスとしてまとめられます。スキン情報があった場合、実は一度読み込んだID3DXMeshメッシュを「最適化」する必要があります。そうしないとスキンメッシュアニメーションは途方も無く面倒になってしまうんです。最適化といってもユーザは大して面倒な事はしません。ID3DXSkinInfoインターフェイスには、読み込んだメッシュをスキンメッシュ用に最適化する仕組みが全部整っていますので、それを利用します。その最適化を担う関数がID3DXSkinInfo::ConvertToBlendedMeshメソッドです。

ID3DXSkinInfo::ConvertBlendedMeshメソッド
HRESULT ConvertToBlendedMesh(      
    LPD3DXMESH pMesh,
    DWORD Options,
    CONST LPDWORD pAdjacencyIn,
    LPDWORD pAdjacencyOut,
    DWORD *pFaceRemap,
    LPD3DXBUFFER *ppVertexRemap,
    DWORD *pMaxFaceInfl,
    DWORD *pNumBoneCombinations,
    LPD3DXBUFFER *ppBoneCombinationTable,
    LPD3DXMESH *ppMesh
);

引数が沢山ありますが、指し当たって重要なのはpMeshpMaxFaceInflpNumBoneConbinationsppBoneCombinationTableそしてppMeshです。
pMeshにはID3DAllocateHierarchyで読み込んだ最適化される前のメッシュを渡します。
pMaxFaceInfleは1つの頂点に影響するボーンの最大数が返ります。後々で使う数値です。
pNumBoneCombinationには「ボーンコンビネーションの数」が返ります。この値はあちこちで必要になるのですが、詳しくは後述します。
ppBoneCombinationTableには「ボーンコンビネーションテーブル」というボーンと頂点の対応表が配列としてドン!っと返ります。一番大切なのは実はこれです。これについては後述します。
ppMeshには最適化されたメッシュが返ります。
他の変数については補足的な物なので、ここでは説明しません(詳しくはマニュアルをご覧下さい)。

 上のpMesh以外の4つの変数はスキンメッシュをするために必ず必要になりますので、D3DXMESHCONTAINER構造体を拡張して、その保存変数を追加して置きます(メッシュの保存先はすでにあります)。

MYD3DXMESHCONTAINER構造体
struct MYD3DXMESHCONTAINER : public D3DXMESHCONTAINER
{
   DWORD dwMaxInfleNum;             // ボーン最大影響数
   DWORD dwBoneCombNum;             // ボーンコンビネーション数
   D3DXBONECOMBINATION *pBoneComb;  // ボーンコンビネーション構造体配列へのポインタ
};

 ここで何度も出てきている「最適化」というのは何の事を言っているのか?これは簡単に言うと「同じボーンの影響を受ける頂点のインデックスを連番に直す」という最適化の事を言っています。とても大切な最適化なのですが、イメージがさっぱりだと思いますので(^-^;、下の図をご覧下さい。


 左側がXファイルから読み込んだ直後の、最適化される前の頂点の並びです。この順序を決めているのは3Dオブジェクトを生成するソフトです。この頂点はスキンメッシュとして描画するに都合が悪すぎます。例えば青いボーンの影響を受ける頂点を頂点0と5だとしますと、これらの番号は離れていますから、2回レンダリングしなければなりません(サブセットとしてレンダリングするには連続したインデックスが必要)。緑のボーンについても同様です。黄色はたまたまですが連番になっていますね。つまり、最適化前の頂点インデックスの並びですと、合計5回もレンダリングしなければなりません。一方、頂点インデックスを最適化して並びを整えると右のようになります。各ボーンに影響される頂点の番号を連番にするのがポイントです。これによりレンダリング回数は3回に減ります。たった6点でもずいぶん回数が減りますが、これが数千という頂点数になりますと、この最適化の威力は絶大となります。ConvertBlendedMeshメソッドはこの最適化されたメッシュを自動的に作ってくれるわけです。何ともありがたいこってです(^-^)

 この最適化をすると、読み込み時のメッシュ(pMeshとして与えたメッシュ)は必要なくなります。これはID3DXAllomateHierarchy::CreateMeshContainerメソッド内で「pMeshのAddRef関数を呼ばなければ良い」だけです。こうすると、D3DXLoadMeshHierarchyFormX関数を抜けた時に自動的にメッシュは削除されます。

 ConvertBlendedMeshメソッドが返すボーンコンビーションテーブル(ppBoneCombinationTable:D3DXBONECOMBINATION構造体の配列)というのは、上図のようなボーンとそれに影響されるインデックスの並びをまとめたものです。これは次のような表でイメージできます:

サブセット番号(配列要素番号) ボーン番号(ボーングループ) 影響する連番頂点インデックス
0 0,4,6,7 0 〜 125
1 0,1 126 〜 139
2 2,3,8,9 140 〜 197
3 4,5,6 198 〜 247
...

表の1行が1つのD3DXBONECOMBINATION構造体の情報で、全部でpNumBoneCombinationだけあります。あるボーングループによって影響される頂点インデックスが連番でまとめられています。例えば、サブセット番号0番は、ボーン番号0,4,6,7が頂点インデックス0〜125番までに影響する事を表しています。ここでボーン番号というのがありますが、これはID3DXSkinInfo::GetBoneOffsetMatrixメソッドで獲得できるボーンオフセット行列配列の要素番号に対応します。この対応が極めて重要です。次のBでのハッシュテーブルの作成や、描画時に必ず必要になります。各行のサブセット番号は、レンダリング時にm_pMesh->DrawSubset(3);などのように番号を指定して描画する事になります。この表やD3DXBONECOMBINATION構造体の中身を理解する事が、スキンメッシュをするにはどうしても必要になります。



B ボーンオフセット行列番号とフレームとの対応ハッシュ作成

 MYD3DXFRAME構造体のツリー構造、そしてMYD3DXMESHCONTAINER構造体を適切に初期化すると、次の作業に移る事ができます。次にやる事は、MYD3DXFRAME構造体とボーンオフセット行列とを結びつけるハッシュテーブルを作成する事です。どうしてそんなハッシュテーブルが必要なのか、まずはそれを説明します。

 ボーンオフセット行列は頂点を動かすために必要な行列で、次のように常にボーン行列と一緒に使用します:

 下段右辺にあるPbefore.Localというのがローカル座標で表された頂点の位置座標、BOfsMtというのがボーンオフセット行列です。MA〜MCは各フレームに保存されているボーン行列です。右側の括弧の計算結果はボーンCの「ワールド変換行列」となります。上の式の結果出力されるPafter,Localというのが、変換後の頂点の位置座標となります。この計算についてはDirectX技術編その27『アニメーションの根っこ:『スキンメッシュアニメーション(ボーン操作)』で詳し〜く説明してありますので、混乱されている方は一読下さい。

 さて、MYD3DXFRAME構造体において、1つのボーン(=フレーム)には、1つのボーンオフセット行列と1つのワールド変換行列を格納する事にしたのでした。一方、つい先ほど出てきたボーンコンビネーション構造体には「ボーン番号」が格納されています。描画をする時には、「0,4,6,7のボーンのワールド変換行列とオフセット行列を取り出す」という作業が必ず必要になるのですが、フレーム構造体はツリーであるため「要素番号」を頼りにそれらを参照できません。


 そこで、ボーンオフセット行列番号とフレームポインタのハッシュテーブルを作成してしまえば、共通する要素番号から両方の行列を最高に効率良く引っ張り出せます。

 ハッシュテーブルの作り方です。ボーン番号とフレームを結びつけるポイントは「ボーンの名前」です。ボーンには固有名が付いています。これはID3DXSkinInfo::GetBoneNameメソッドにボーンの番号を与えると取得することができます。取得した名前と同じ名前を持つフレームを階層を辿って探します。フレームが見つかったら、そのボーン番号とフレームへのポインタをひとまとめにします。この作業を全てのボーンについて行えば、ハッシュテーブルの完成です。この結合作業は1回限りですから、パフォーマンスの負担には殆どなりません。

 ハッシュテーブル作成の実装は、例えば次のようになります。

ボーンオフセット行列とフレームを結ぶハッシュを作成
// ハッシュテーブル作成関数
bool CreateFrameHashTable(
   vector< MYD3DXFRAME* > &VctHash,   // ハッシュテーブル
   MYD3DXFRAME* pRootFrame,          // ルートフレーム
   ID3DXSkinInfo *pSkinInfo          // ID3DXSkinInfoインターフェイスポインタ
)
{
   DWORD dwNumBone = pSkinInfo->GetNumBones();  // ボーンの数を取得
   DWORD i;
   // ボーンの数だけ対応フレームを検索
   for( i=0; i<dwNumBone; i++)
   {
      MYD3DXFRAME *pFrame = NULL;   // 一致したフレームポインタ
      // ボーン名を取得
      string BoneName = pSkinInfo->GetBoneName(i);
      // 名前検索
      pFrame = SearchFrameName( pRootFrame, BoneName );
      // フレームを配列に格納
      VctHash.push_back( pFrame );
   }

   return true;
}


// 一致フレーム検索関数
MYD3DXFRAME* SearchFrameName(
   MYD3DXFRAME* pFrame,    // 名前を調べるフレーム
   string &BoneName        // ボーンの名前
)
{
   // フレームの名前をstringに格納
   string FrameName = pFrame->Name;
   // 比較
   if( BoneName == FrameName )
   {
      // 一致したのでポインタを返す
      return pFrame;
   }

   MYD3DXFRAME *pTmp = NULL;
   // 子フレームを検索
   if( pFrame->pFrameFirstChild){
      pTmp = SearchFrameName( (MYD3DXFRAME*)pFrame->pFrameFirstChild, BoneName );
      if(pTmp)
         return pTmp;
   }

   // 兄弟フレームを検索
   if( pFrame->pFrameSibling){
      pTmp = SearchFrameName( (MYD3DXFRAME*)pFrame->pFrameSibling, BoneName );
      if(pTmp)
         return pTmp;
   }

   // このフレーム以下の全フレームは該当しない
   return NULL;
}

 単なるツリー検索をしているだけですから、ソースの詳細は省略致します。ハッシュテーブルの作成は、フレームが確定しなければできませんので、D3DXLoadMeshHierarchyFromX関数で全ての情報を取得した後に行います。ハッシュテーブルの保存先はどこでも良いのですが、D3DXMESHCONTAINER構造体に格納するのも手です。

MYD3DXFRAME構造体
struct MYD3DXMESHCONTAINER : public D3DXMESHCONTAINER
{
   DWORD dwMaxInfleNum;             // ボーン最大影響数
   DWORD dwBoneCombNum;             // ボーンコンビネーション数
   D3DXBONECOMBINATION *pBoneComb;  // ボーンコンビネーション構造体配列へのポインタ
  vector<MYD3DXFRAME*> FrameHashTable;   // ボーンオフセット行列番号とフレームのハッシュテーブル
   IDirect3DTexture9 Texture;             // 使用するテクスチャ
};

 ついでにメッシュに必要なテクスチャも保存してしまう事にしましょう。これらの準備が整いましたら、次のステップに進めます。



C アニメーションコントローラでボーン行列を間接操作

 D3DXLoadMeshHierarchyFromX関数からは、フレーム情報と共にアニメーションコントローラ(ID3DXAnimationControllerインターフェイス)も取得できています。このアニメーションコントローラを操作することで、ボーンを動かす事が出来ます。「ボーンを動かすというのは、フレーム内のボーン行列を操作することじゃなかったっけ?」と思われるかもしれません。実は、アニメーションコントローラは裏でこっそりとフレーム階層と繋がっていまして、アニメーションコントローラでの操作が即座にフレーム階層のボーン行列に反映される仕組みになっています。つまり、アニメーションコントローラというのは、アニメーションをするための補助インターフェイスというわけなんです。

 アニメーションコントローラには「アニメーションセット」というある一連の動きを定義するインターフェイスが複数格納されています。Xファイルにいくつのアニメーションセットがあり、どれだけ格納されたかは、ID3DXAnimationController::GetNumAnimationSetsメソッドで知る事が出来ます。

 アニメーションコントローラの基本的な使い方は、

 ・ GetAnimationSetメソッドで格納されているアニメーションセットへのポインタを取得
 ・ SetTrackAnimationSetメソッドで動作させたいアニメーションをトラックにセット
 ・ AdvanceTimeメソッドに経過時間を設定しボーン行列を更新

となります。読み込んだ時点で一番最初のアニメーションセットがトラック0番に格納されていますが、別の動きにスイッチしたい場合は、トラックにアニメーションセットを登録し直すわけです。複数のアニメーションセットの使い分けについてはこの章の範疇を超えますので、ここでの説明は省きます。改めて別の章を設ける予定です。



D ボーンのワールド変換行列を更新

 アニメーションコントローラの持つAdvanceTimeメソッドで時間を更新した段階で、フレーム階層に保存されている全てのボーン行列が自動更新されます。更新されるのはボーン行列のみで、ボーンのワールド変換行列はそのままです。そこで次に、更新後のボーン行列から各フレームのワールド変換行列を算出します。

 この意味及び計算方法の詳細はDirectX技術編その26『アニメーションの根っこ:ワールド変換行列スタック』にあますとこなく記載しております。ルートフレームから定まった計算式に従ってワールド変換行列を更新するだけです。その時ついでに合成行列(オフセット行列×ボーンワールド行列)も算出してしまうと楽です。最終的に、全てのフレームにワールド変換行列と合成行列が格納されます。ここまで来れば、後は描画をするのみです。



E サブセットごとの描画

 この段階まで情報を揃えれば、3Dオブジェクトを描画することができます。スキンメッシュの描画(固定頂点ブレンディングの場合)のポイントは2つあります。1つは「サブセットごとに描画する」こと、そしてもう1つは「デバイスに複数のボーン合成行列を設定する事」です。基本的には通常のメッシュ描画と殆ど変わりませんが、スキンメッシュ特有の追加事項がいくつかあります。

 描画すべきスキンメッシュのサブセットは、Aで説明しましたD3DXBONECOMBINATION構造体の配列分だけあります。1つのD3DXBONECOMBINATION構造体には頂点に影響を与える複数のボーン(ボーン行列番号)が固定長の配列として格納されています。

 以下1つのサブセットに注目します。サブセット番号sに該当するD3DXBONECOMBINATION構造体をBoneComb[s]としておきます(配列です)。BoneComb[s]に登録されている複数のボーン番号はBoneComb[s].BoneId[b]で取得できます。ここでdは配列の要素番号です。ではBoneComb[s]に格納されているボーン配列の様子を表でご覧下さい:

サブセット番号(s) ボーン配列
b=0 b=1 b=2 b=3
0 0 3 UINT_MAX UINT_MAX
1 1 4 2 5
2 2 3 6 UNIT_MAX
...

 これは最大ボーン数が4だった場合です(1つのBoneComb[s]に登録されている最大ボーン数は、大分前にMYD3DXMESHCONTAINER::dwMaxInfleNumメンバ変数として取得しています)。1つの頂点に影響するボーンが常に4つであるわけではありません。例えばBoneComb[0]は2つのボーンしか登録されていません。これは、2つしか必要ないためです。ただ、1つのD3DXBONECOMBINATION構造体に確保されるボーン番号の配列は「固定長」なため、要素番号b=2およびb=3の部分は「UINT_MAX」というマクロ定数で埋められます。
 これをうまく使えば、1つのBoneComb[s]に登録されているボーンの数は、次のようなプログラムで計算できます。

影響ボーン数の取得
DWORD dwNumInflBone = 0;
DWORD b;
for(b=0; b<dwNumMaxInflBone; b++)
{
   if(BoneComb[s].BoneId[b] != UINT_MAX)
      dwNumInfBone++;
}

この影響ボーン数は、すぐ後で使います。

 次にすべきはボーンの持つ合成行列をデバイスに設定する事です。ここではBで作成したフレームハッシュテーブルが大活躍してくれます。ここのプログラムはお陰でとってもシンプルですが、1つ重要な箇所があります。。

ワールド変換行列を作成
for(b=0; b<dwNumInfleBone; b++){
   DWORD BoneNumber = BoneComb[s].BoneId[b];
   pDev->SetTransform( D3DTS_WORLDMATRIX(b), &(FrameHashTable[BoneNumber]->ConbinedMatrix) );
}

SetTransformメソッドは通常の描画でもおなじみの、頂点変換を行う行列を登録する関数です。実はこの関数の第1引数にD3DTS_WORLDMATRIX(d)という引数付きマクロを設定すると、複数の合成行列から頂点ブレンディングをする事が出来るようになります。嬉しい機能です。ただ、デフォルトではD3DTS_WORLDMATRIX(0)に設定した行列し亜k扱ってくれません。これを変更するにはレンダリングステートに対して頂点ブレンディングを有効にするよう設定します。

頂点ブレンドをするようにレンダリングステートを設定
pDev->SetRenderState( D3DRS_VERTEXBLEND, dwNumInflBone-1 );

第1引数にD3DRS_VERTEXBLENDを、そして第2引数には設定した合成行列の数-1を指定します。マイナス1する点に注意です。本当は「いくつの行列を合成するか」というのを表すマクロ定数がちゃんとあるのですが、面倒なのでこうしています。

 ここまで設定したら、1つのサブセットについて、いつもと同じようにマテリアルとテクスチャを設定して描画します。

サブセットの描画
DWORD AttribID = BoneComb[s].AttribId;   // 属性番号を取得
pDev->SetMaterial( pMaterials[AttribID].MatD3D );
pDev->SetTexture( 0, pTexture[AttribID] );
pMesh->DrawSubset(s);

 マテリアルとテクスチャはもちろんサブセットごとに設定するのですが、これは「属性番号(Attribution)」として別管理されています。どういうことか、下の表をご覧下さい。

サブセット番号 属性番号 マテリアル テクスチャ名
0 0 Mat[0] Tex_01.bmp
1 0 Mat[0] Tex_01.bmp
2 0 Mat[0] Tex_01.bmp
3 1 Mat[1] Tex_02.bmp
4 1 Mat[1] Tex_02.bmp

この例では、サブセット0〜2は同じマテリアルとテクスチャを使用しています。これを属性番号0番としているわけです。サブセットごとに何番の属性番号のマテリアルとテクスチャを用いるかは、D3DXBONECOMBINATION::AttribIdメンバ変数にしっかりと格納されています。

これで1つのサブセットに関連する頂点が内部で自動的にブレンドされて描画されます。後はこのステップを全てのサブセットに関して繰り返すと、アニメーションされたスキンメッシュが描画されます!!



F スキンメッシュは深く深く…

 スキンメッシュの描画の上から下までどどーっと見てきました。Direct3DXやIDirect3DDevice9が持つ固定頂点ブレンディングの機能により、プログラマの作業量は相当に減ってはいるのですが、それでもなかなかにして大変な作業です。作業もそうですが、スキンメッシュは「理屈を理解する」のも大変だと私は思います。スキンメッシュを扱っている書籍はいくつかありますが、ページを割いてじっくり説明してくれている本に私は出会っていません。この章に至るまでの「根っこシリーズ」は、そういう閉塞的な状況を打破するために立ち上げましたが、これでようやくひと段落とあいなりました。

 まだまだ細かい部分で説明しきれない所が山ほどあるのですが、何はともあれスキンメッシュができるというのは、Direct3Dを扱う中で格別に嬉しいものです。ようやく3Dの世界の本質に近づけたかなぁという実感が沸くんですよね。しかしボーンを動かす技術であるスキンメッシュは、物理現象や衝突判定、インバースキネマティクスなど、まだまだ奥深いのです。これらを取り入れるには、独自にこの段階まで実装しておかないと厳しいものがあります。DirectXに付属しているスキンメッシュサンプルの関数をただうわべで使っていちゃだめなんです(^-^)。

 昨今のゲーム業界では、スキンメッシュは出来て当然当たり前の技術になっています。それに追いつくためにも、その取っ掛かりとして自身で扱いやすい形にスキンメッシュをクラス化するなど、さらに深くスキンメッシュを追求してみて下さい。