スキニング


 1.はじめに…
メッシュを読み込みやったついでに,スキニングもやってみました。




 2.スキニングの概要
たまには,少し説明してみます。
まず,アニメーションについてですが…基本はパラパラ漫画です。
時間経過とともに表示されているものが変わることでアニメーションが表現できます。

次。
腕や手,足といったものを動かすにはどうすればいいでしょうか?
これは,パラパラ漫画の各ページに対応するような,各時間ごとの頂点座標を用意すればよいでしょう。60フレームのアニメーションを実現したいのであれば,モデルデータに60フレーム分のデータを持たせればできそうです。…が,コンピュータのメモリ量は決まっているので,できればそんなにデータは持たせたくないです。
そこで,60フレーム分すべてではなく,各アニメーションのキーとなるフレームのデータだけを用意し,各キー間のフレームデータは計算により求めてしまおうというのが,いわゆるキーフレームアニメーションです。
さて,キーフレームにより少しデータが削減できました。でも,キーフレームにしても各フレームごとの全頂点データを持つのは,よろしいとは言えません。各キーフレームの全頂点データが表すのは,アニメーションさせた時の頂点データであり,結局頂点がどこにあるか=どのように移動したかという結果を示すものにすぎません。どのように移動したかをデータとして保存するならば,別に全頂点データを保存せずとも,行列なり四元数なりで表現すればよいです。そこで出てくるのがボーン(骨)です。頂点とボーンを関連付けし,ボーンの動きを行列なり四元数なりで表現して,ボーンを動かせば関連する頂点も同じく移動します。このようにしてアニメーションを行うのがいわゆるボーンアニメーションです。ボーンアニメーションも何も考えずにやってしまうと,関節部分が固いパーツでつないだロボットのような動きになってしまいます。これを解決するのがスキニングです。ボーンの動きにあわせてスキンを変形させます。関節部分など複数のボーンの影響を受けやすい箇所などにおいて頂点座標をボーンの重みづけによってブレンディングし,皮膚を滑らかに接続させます。


 3.スキニング処理
スキニング処理ですが,行列パレットが求まっていれば大したことはありません。「行列パレット」とは、変換のためのローカル行列を配列として格納したもので,実際にブレンドに使用する行列を「行列パレット」から引っ張ってきます。絵を書くときにパレットから原色を取り出して色を作るように行列を混ぜ合わせて、実際に用いる行列を作るためそのように呼ばれるそうです。
GPU側で行う処理は,下記の様になります。
00095:  //-----------------------------------------------------------------------
00096:  //! @brief スキニング処理
00097:  //-----------------------------------------------------------------------
00098:  VSOutput VSFunc( VSInput input )
00099:  {
00100:     VSOutput output = (VSOutput)0;
00101:  
00102:     float4 localPos = float4( input.Position, 1.0f );
00103:  
00104:     float4x4 skinTransform = float4x4( 1.0f, 0.0f, 0.0f, 0.0f,
00105:                                        0.0f, 1.0f, 0.0f, 0.0f,
00106:                                        0.0f, 0.0f, 1.0f, 0.0f,
00107:                                        0.0f, 0.0f, 0.0f, 1.0f );
00108:  
00109:     // スキニング処理
00110:     skinTransform += Bones[ input.SkinIndex.x ] * input.SkinWeight.x;
00111:     skinTransform += Bones[ input.SkinIndex.y ] * input.SkinWeight.y;
00112:     skinTransform += Bones[ input.SkinIndex.z ] * input.SkinWeight.z;
00113:     skinTransform += Bones[ input.SkinIndex.w ] * input.SkinWeight.w;
00114:  
00115:     // 位置座標を変換
00116:     float4 transPos = mul( skinTransform, localPos );
00117:     float4 worldPos = mul( World, transPos );
00118:     float4 viewPos  = mul( View, worldPos );
00119:     float4 projPos  = mul( Proj, viewPos );
00120:  
00121:     // 法線ベクトルを変換
00122:     float3 transNormal = mul( (float3x3)skinTransform, input.Normal );
00123:     float3 worldNormal = mul( (float3x3)World, transNormal );
00124:  
00125:     // 出力値設定
00126:     output.Position = projPos;
00127:     output.TexCoord = input.TexCoord;
00128:     output.Normal   = worldNormal;
00129:  
00130:     return output;
00131:  }
00132:  
上記コードのBonesと名のついたものが,行列パレットにあたります。ボーンの重み付けに当たる部分がSkinWeightに対応しています。
さてスキニング処理で大事な所はこの行列パレットを求めるところです。実際に行う処理は下記のようになります。
01962:  ///---------------------------------------------------------------------
01963:  ///<summary>
01964:  ///ボーン変換行列を更新する
01965:  ///</summary>
01966:  ///<param name="time"></param>
01967:  ///<param name="relativeToCurTime"></param>
01968:  ///---------------------------------------------------------------------
01969:  void AnimationPlayer::UpdateBoneTransform( float time, bool relativeToCurTime )
01970:  {
01971:      if ( mpClip == null )
01972:      {
01973:          ELOG( "Error : AnimationPlayer::UpdateBoneTransform() mpClip == null" );
01974:          assert( false );
01975:      }
01976:  
01977:      if ( relativeToCurTime )
01978:      {
01979:          // 時間を進める
01980:          time += mCurTime;
01981:  
01982:          // 継続時間よりも大きくなったら,初めに戻す
01983:          while( time >= mpClip->GetDuration() )
01984:          {
01985:              time -= mpClip->GetDuration();
01986:          }
01987:      }
01988:      // 時間の範囲チェック
01989:      if ( (time < 0.0f) || (time >= mpClip->GetDuration()) )
01990:      {
01991:          DLOG( "Error : AnimationPlayer::UpdateBoneTransform() time value range is invalid." );
01992:          assert( false );
01993:      }
01994:  
01995:      // 現在時間よりも小さいなら,初期化
01996:      if ( time < mCurTime )
01997:      {
01998:          for( size_t i=0; i<mBones.size(); i++ )
01999:          {
02000:              mBoneTransforms[i] = mBones[i]->GetPoseMatrix();
02001:          }
02002:      }
02003:  
02004:      // 現在時間を更新
02005:      mCurTime = time;
02006:  
02007:      uint32_t num_frames = mpClip->GetNumFrames();
02008:      for( uint32_t i=0; i<num_frames; ++i )
02009:      {
02010:          // アニメーションデータ取得
02011:          IAnimation* animation = mpClip->GetFrame( i );
02012:  
02013:          uint32_t index1 = 0;
02014:          uint32_t index2 = 0;
02015:          // 現在時間に近いフレームを検索
02016:          for( uint32_t j=0; j<animation->GetNumKeys(); ++j )
02017:          {
02018:              if ( animation->GetKey( j )->GetTime() > mCurTime )
02019:              {
02020:                  index1 = j;
02021:                  index2 = ( j > 0 ) ? ( j - 1 ) : j;
02022:                  break;
02023:              }
02024:          }
02025:  
02026:         // 骨のインデックスを取得する
02027:          uint32_t bone_index = animation->GetBoneIndex();
02028:          assert( bone_index >= 0 );
02029:  
02030:         // 骨のインデックスからボーンアニメーションに用いる変換行列を取得する
02031:         Matrix mat1 = animation->GetKey( index1 )->GetTransform();
02032:         Matrix mat2 = animation->GetKey( index2 )->GetTransform();
02033:  
02034:         // 骨の変換行列を求める
02035:         mBoneTransforms[ bone_index ] = mat1 + mat2;
02036:      }
02037:  }
02038:  
02039:  ///---------------------------------------------------------------------
02040:  ///<summary>
02041:  ///ワールド行列を更新する
02042:  ///</summary>
02043:  ///<param name="rootMatrix">ルートノードのワールド行列</param>
02044:  ///---------------------------------------------------------------------
02045:  void AnimationPlayer::UpdateWorldTransform( const Matrix &rootMatrix )
02046:  {
02047:      // 親の変換行列を求める
02048:      mWorldTransforms[0] = mBoneTransforms[0] * rootMatrix;
02049:  
02050:      // 子供の変換行列を求める
02051:      for( size_t i=1; i<mBones.size(); i++ )
02052:      {
02053:          int parent = mBones[i]->GetParentIndex();
02054:          assert( parent >= 0 );
02055:  
02056:          mWorldTransforms[i] = mBoneTransforms[i] * mWorldTransforms[parent];
02057:      }
02058:  }
02059:  
02060:  ///---------------------------------------------------------------------
02061:  ///<summary>
02062:  ///スキニング行列を更新する
02063:  ///</summary>
02064:  ///---------------------------------------------------------------------
02065:  void AnimationPlayer::UpdateSkinTransform()
02066:  {
02067:      for( size_t i=0; i<mBones.size(); i++ )
02068:      {
02069:           // スキニング行列を求める
02070:           mSkinTransforms[i] = mBones[i]->GetBindMatrix() * mWorldTransforms[i];
02071:      }
02072:  }
02073:  
02074:  ///---------------------------------------------------------------------
02075:  ///<summary>
02076:  ///アニメーション更新処理
02077:  ///</summary>
02078:  ///---------------------------------------------------------------------
02079:  void AnimationPlayer::Update( float time, bool relativeToCurTime, const Matrix &rootTransform )
02080:  {
02081:      // 骨を動かす
02082:      UpdateBoneTransform( time, relativeToCurTime );
02083:  
02084:      // ワールド変換行列を更新
02085:      UpdateWorldTransform( rootTransform );
02086:  
02087:      // スキニング行列を更新
02088:      UpdateSkinTransform();
02089:  }
02090:  
上記のコードでやっていることは,まずアニメーションを行うにあたり,現在の時間にキーフレームとデータを持ってきます。その持ってきたキーフレームのひとつ前のキーフレームのデータを用意して,骨を動かす変換行列を算出します。次に,親の変換を行列を求め,そこから再帰的に子供の変換行列を求めていきます。そして求めた骨を動かす変換行列と骨のオフセット行列を元に行列パレットを算出しています。オフセット行列とは頂点座標を骨の座標系に変換するための行列です。つまり骨を原点とした座標系に変換するための行列になります。
あとは,このようにして求めた行列パレットをシェーダに転送してやればOKです。


 4.終わりに
スキニング処理をちょっとだけ説明してみました。詳しい説明が欲しい方は,ゲームエンジンアーキテクチャあたりを読むとよいかと思います。
スキニング処理の一番嫌な所は,実は行列を求めるところよりも,モデルファイルから処理に必要な基本データを引っ張ってくるところなんですよね。その辺の処理をしたい人は,サンプルコードの中身の方を追ってみてください。


 Download
本ソースコードおよびプログラムを使用したことによる如何なる損害も製作者は責任を負いません。
本ソースコードおよびプログラムは自己責任でご使用ください。
プログラムの作成にはMicrosoft Visual Studio 2008 SP1 Professional, Microsoft DirectX SDK (June 2010)を用いています。







Flashを利用するためにはPluginが必要です。 <!-- <div style="text-align: center;"><div style="display: inline-block; position: relative; z-index: 9999;"><script type="text/javascript" charset="utf-8" src="//asumi.shinobi.jp/fire?f=434"></script></div></div></body> -->