頂点テクスチャでスキンアニメーション

2010/09/17 追記: XNA Game Studio 4.0用のサンプルをhttp://higeneko.net/hinikeni/sample/xna40/TexSkinningSample.zipにアップしました。詳細は「サンプルコードをXNA 4.0向けに更新」を見てください。

2009/06/25 追記: XNA GS 3.1用のサンプルを http://higeneko.net/hinikeni/sample/xna31/TexSkinningSample.zipにアップしました。

頂点テクスチャでスキンアニメーション、その2:頂点テクスチャでスキンアニメーション

今回は頂点テクスチャを使ったスキンアニメーションの実装方法を紹介します。

XNA Game Studio 3.0で動作するサンプルを用意しました。基本的にSkinned Modelサンプルと同じ使い方です。

http://higeneko.net/hinikeni/sample/TexSkinningSample.zip

今回のサンプルは前々回の「クォータニオンでスキンアニメーション」のサンプルプログラムに以下の変更を加えたものです。

  • AnimationPlayerの変更
  • 頂点テクスチャの生成
  • シェーダーの変更

頂点テクスチャフォーマットを決める

まずは、頂点テクスチャにどのようにボーンデータを格納するか決めます。ボーンの回転部分のクォータニオンはVector4と同じフォーマットなのでSurfaceFormat.Vector4が使えます。平行移動にはVector3を使っていますが、頂点テクスチャのフォーマットにはVector3が無いのでちょっともったいないですがSurfaceFormat.Vector4を使用します。

今回のサンプルでは、回転部分と平行移動部分を2つの頂点テクスチャに別々に格納しています。ひとつのテクセルにひとつのボーン情報を格納しているので、頂点テクスチャのサイズは横がボーン数、縦が1となっています。

tex-bone

頂点テクスチャのサンプラーは4つしかないので、2つの頂点テクスチャを使うのが厳しい場合は以下のように、2つのテクセルにひとつのボーン情報をまとめて格納するといいでしょう。この場合、頂点テクスチャのサイズは横がボーン数×2、縦が1となります。

tex-bone-02

頂点テクスチャが使うメモリサイズを節約するという観点では前者の回転部分と平行移動部分を別々の頂点テクスチャで持つほうが有利です。これはクォータニオンの4要素の値の範囲は-1~+1なので、SurfaceFormat.Vector4の代わりにSurfaceFormat.HalfVector4やSurfaceFormat.NormalizedShort4を使うことでメモリ使用量を半分にすることができるからです。また、通常のキャラクターアニメーションでは平行移動部分も大きな値を使用しないのでSurfaceFormat.HalfVector4を使って更にメモリ使用量を減らすこともできるでしょう。

AnimationPlayerの変更

機能的には前々回のサンプルとまったく同じなのですが、頂点テクスチャへの格納フォーマットがVector4に変わったので、それに合わせてSkinTranslationsの型もVector3[]からVector4[]に変更します。

頂点テクスチャの生成

まずは、使用する頂点テクスチャ(ボーン用の頂点テクスチャなので、ボーンテクスチャと呼びます)の宣言をします。フレーム毎にボーン情報をボーンテクスチャへ書き込むわけですが、「GPUはいつ描画するのか?」で解説したように、同じテクスチャに続けて書き込むとGPUの処理とバッティングしてしまうということに注意が必要です。

そこで、それぞれ複数の頂点テクスチャを生成して切り替えながら使う必要があります。この実装は非常に単純でTexture2Dの配列と、現在使用するテクスチャのインデックスを用意するだけでいいのですが、使用する頂点テクスチャが増えてくると余計な変数が増えてきて、コードの可読性が低くなり、ミスも起きやすくなってしまいます。そういった理由から、ここでは複数のテクスチャを切り替える機能をもったFlipTexture2Dというクラスを作ります。

FlipTexture2DクラスはTexture2Dと同様のコンストラクタを持ち、内部で複数のテクスチャを作り、Flipメソッドで使うテクスチャを切り替え、Textureプロパティで現在のテクスチャを返すようになっています。

     // ボーン情報を格納するテクスチャ
    FlipTexture2D rotationTexture;      // ボーンの回転部分を格納するテクスチャ
    FlipTexture2D translationTexture;   // ボーンの平行移動部分を格納するテクスチャ

ボーンテクスチャの生成は前述のように、横がボーン数、縦が1のサイズのテクスチャを作ります。ここでTextureUsageをTextureUsage.Linearを指定していることに注意してください。通常のテクスチャはピクセルシェーダー内でフェッチされることを前提としており、その用途に適しているTextureUsage.Tilingを使用するようになっています。TextureUsage.Noneを設定しても、テクスチャサイズがタイリングに適している場合は自動的にタイリングを使うようになっています。

通常のレンダリング時には極力タイリングを使うべきですが、SetDataを呼び出したときに通常のフォーマットからタイリングフォーマットへの変換がCPUによって行われます。

今回はボーンテクスチャとして使用するので、タイリングを使う必要がないこと、SetData時にタイリングフォーマット変換に掛かる時間を節約したいという二点の理由から、TextureUsage.Linearを指定します。

 // 頂点テクスチャの生成
int width = animationPlayer.GetSkinRotations().Length;
int height = 1;

rotationTexture = new FlipTexture2D( GraphicsDevice, width, height, 1,
                        TextureUsage.Linear, SurfaceFormat.Vector4 );

translationTexture = new FlipTexture2D( GraphicsDevice, width, height, 1,
                        TextureUsage.Linear, SurfaceFormat.Vector4 );

こうして作ったボーンテクスチャにanimationPlayerで生成したSkinRotationsとSkinTranslationsをSetData<T>を使って書き込みます。ここではFlipTexture2D.Flipメソッドを呼び出すことで書き込むテクスチャを切り替えてから書き出します。

 // ボーンのクォータニオンと平行移動部分を取得し頂点テクスチャに書き込み
rotationTexture.Flip();
translationTexture.Flip();

rotationTexture.Texture.SetData<Quaternion>( animationPlayer.GetSkinRotations() );
translationTexture.Texture.SetData<Vector4>( animationPlayer.GetSkinTraslations() );

頂点シェーダー内で頂点テクスチャをフェッチする場合に指定するのはテクスチャ座標なので、ボーン番号からテクスチャ座標に変換するために必要なtextureSizeを設定します。

実際の描画コードは以下のようになっています、通常の描画コードにrotationTexture、traslationTexture、そしてtextureSizeを設定するコードが追加しただけです。

 Vector2 textureSize = new Vector2( rotationTexture.Texture.Width,
                                    rotationTexture.Texture.Height );

foreach ( ModelMesh mesh in currentModel.Meshes )
{
    foreach ( Effect effect in mesh.Effects )
    {
        effect.Parameters["BoneRotationTexture"].SetValue(
                                            rotationTexture.Texture );
        effect.Parameters["BoneTranslationTexture"].SetValue(
                                            translationTexture.Texture );

        effect.Parameters["BoneTextureSize"].SetValue( textureSize );
        effect.Parameters["World"].SetValue( world );
        effect.Parameters["View"].SetValue( view );
        effect.Parameters["Projection"].SetValue( projection );
    }

    mesh.Draw();
}

シェーダーの変更

シェーダー内での頂点テクスチャの宣言は通常のテクスチャと殆ど変わりありませんが、殆どのGPUでは浮動小数点テクスチャのバイリニアフィルタリングが利かないのと、正しいフィルター設定しないとフェッチができないものが多いので、ここではポイントサンプリング、ミップマップなし、テクスチャのアドレッシングをクランプに設定しています。

また、このコードではregister(vs, s0) というレジスタ宣言をして明示的に頂点テクスチャを任意のサンプラーに割り当てています。この宣言は必ずしも必要はありませんが、複数のエフェクトで同じ定数を共有したい場合にsharedを指定すると、コンパイラはサンプラーが通常のテクスチャなのか頂点テクスチャなのかを判断することができなってしまうので、registerを使って指定する必要があります。

 //-----------------------------------------------------------------------------
// 頂点テクスチャ用の定数レジスタ宣言
//=============================================================================
float2 BoneTextureSize;    // ボーン用頂点テクスチャのサイズ

// ボーン用頂点テクスチャサンプラー宣言
texture BoneRotationTexture;

sampler BoneRotationSampler : register(vs,s0) = sampler_state
{
    Texture = (BoneRotationTexture);
    // 殆どのGPUでは以下のようなステート設定にしないと
    // 頂点テクスチャのフェッチがうまくいかない
    MinFilter = Point;
    MagFilter = Point;
    MipFilter = None;
    AddressU = Clamp;
    AddressV = Clamp;
};

texture BoneTranslationTexture;

sampler BoneTranslationSampler : register(vs,s1) = sampler_state
{
    Texture = (BoneTranslationTexture);
    // 殆どのGPUでは以下のようなステート設定にしないと
    // 頂点テクスチャのフェッチがうまくいかない
    MinFilter = Point;
    MagFilter = Point;
    MipFilter = None;
    AddressU = Clamp;
    AddressV = Clamp;
};

続いてボーン情報をボーンテクスチャからフェッチします。頂点テクスチャのフェッチにはtex2Dlodを使います。tex2Dlodにはfloat4値を渡し、x,y,zにはテクスチャ座標のu,v,wを、wにはミップマップレベルを指定します。ピクセルシェーダー内でおなじみのtex2Dが頂点シェーダー内で使えない理由はピクセルシェーダー内ではハードウェアが自動的にミップマップレベルを計算してくれますが、頂点シェーダー内ではミップマップレベルを自動的に計算することができないからです。ですから、tex2Dlodを使用してミップマップレベルを指定する必要があります。

4つのボーンインデックスから計算したテクスチャ座標を使ってそれぞれのボーン情報をフェッチして、前々回のサンプルで作ったCreateTransformFromQuaternionTransformsメソッドを使ってskinTransformを計算します。

 //-----------------------------------------------------------------------------
// 頂点テクスチャからボーン情報のフェッチ
//=============================================================================
float4x4 CreateTransformFromBoneTexture( float4 boneIndices, float4 boneWeights )
{
    float2 uv = 1.0f / BoneTextureSize;
    uv.y *= 0.5f;
    float4 texCoord0 = float4( ( 0.5f + boneIndices.x ) * uv.x, uv.y, 0, 1 );
    float4 texCoord1 = float4( ( 0.5f + boneIndices.y ) * uv.x, uv.y, 0, 1 );
    float4 texCoord2 = float4( ( 0.5f + boneIndices.z ) * uv.x, uv.y, 0, 1 );
    float4 texCoord3 = float4( ( 0.5f + boneIndices.w ) * uv.x, uv.y, 0, 1 );

    // 回転部分のフェッチ
    float4 q1 = tex2Dlod( BoneRotationSampler, texCoord0 );
    float4 q2 = tex2Dlod( BoneRotationSampler, texCoord1 );
    float4 q3 = tex2Dlod( BoneRotationSampler, texCoord2 );
    float4 q4 = tex2Dlod( BoneRotationSampler, texCoord3 );

    // 平行移動部分のフェッチ
    float4 t1 = tex2Dlod( BoneTranslationSampler, texCoord0 );
    float4 t2 = tex2Dlod( BoneTranslationSampler, texCoord1 );
    float4 t3 = tex2Dlod( BoneTranslationSampler, texCoord2 );
    float4 t4 = tex2Dlod( BoneTranslationSampler, texCoord3 );
    
    return CreateTransformFromQuaternionTransforms(
                    q1, t1,
                    q2, t2,
                    q3, t3,
                    q4, t4,
                    boneWeights );
}

頂点シェーダー本体のコードは以下のようになります。前々回のサンプルではCreateTransformFromQuaternionTransformsを呼んでいた部分がCreateTransformFromBoneTextureに変わっただけです。

 //-----------------------------------------------------------------------------
// 頂点シェーダー
//=============================================================================
VS_OUTPUT VertexShader(VS_INPUT input)
{
    VS_OUTPUT output;
    
    // スキン変換行列の取得
    float4x4 skinTransform =
                CreateTransformFromBoneTexture( input.BoneIndices, input.BoneWeights );
            
    skinTransform = mul( skinTransform, World );
  
    // 頂点変換
    float4 position = mul(input.Position, skinTransform);
    output.Position = mul(mul(position, View), Projection);

    // 法線変換
    float3 normal = normalize( mul( input.Normal, skinTransform));
    
    float3 light1 = max(dot(normal, Light1Direction), 0) * Light1Color;
    float3 light2 = max(dot(normal, Light2Direction), 0) * Light2Color;

    output.Lighting = light1 + light2 + AmbientColor;

    output.TexCoord = input.TexCoord;
    
    return output;
}

定数レジスタがいらなくなった

ボーン情報を頂点テクスチャから読み込んでいるので、定数レジスタの数制限からくるボーン数制限がなくなりました。ただし、XNAフレームワークのコンテント・パイプラインで変換されるボーンインデックスは最大256個までとなっているので、最大ボーン数は256個になります。もちろん、コンテント・パイプラインを拡張して出力するボーンインデックスのフォーマットを変更すれば更に多くのボーンを使うこともできますが、256個以上のボーンが必要になるケースというのはあるのでしょうか?

これでオリジナルのサンプルでは59個から4倍以上のボーン数を使えるようになりました。定数レジスタを必要としないので、余った定数レジスタは他の用途に使うことができます。

PC上ではシェーダーモデル3.0以上のビデオカードが必要になりますが、Xbox 360上では問題なく動くし、この手法を活用できる場面(近日中の記事で紹介予定)も多いので、魅力的な手法ではないでしょうか?

今回の手法で使えるボーン数が上限にまで達しましたが、次回はXbox 360専用の機能であるvFetchを使った手法を紹介したいと思います。