vFetchでスキンアニメーション

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

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

vFetchでスキンアニメーション 、その3:vFetchでスキンアニメーション

スキンアニメーションで使えるボーン数を増やそうシリーズの記事も10回目となる今回で終わりです。今回はvFetchを使ったスキンアニメーションの実装例を紹介します。

XNA Game Studio 3.0で動作するサンプルを用意しました。基本的にSkinned Modelサンプルと同じ使い方ですが、今回のサンプルはXbox 360上でのみ動作することに注意してください。

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

  また、今回のサンプルはクォータニオンでスキンアニメーションサンプルに以下の変更を加えたものになっています。

  • BoneVertexの実装
  • AnimationPlayerの変更
  • モデルの頂点宣言の変更
  • ボーン格納用頂点バッファの生成
  • シェーダーの変更

BoneVertexの実装

ボーン情報を頂点バッファへ格納するためのBoneVertex構造体を作ります。この構造体には実際のデータの他に頂点要素宣言と、ストライド情報が含まれています。

この宣言の仕方は独自の頂点データを宣言する基本的な手法です。StructLayoutアトリビュートを使って構造体のメンバーがコンパイラーによって並び替えが起こらないようにし、静的なVertexElements配列と、構造体のバイトサイズを返すSizeInBytesを宣言します。

 /// <summary>
/// ボーン格納用の頂点構造体
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct BoneVertex
{
    /// <summary>
    /// 回転部分(クォータニオン) 
    /// </summary>
    public Quaternion Rotation;

    /// <summary>
    /// 平行移動部分
    /// </summary>
    public Vector3 Translation;

    public BoneVertex( Quaternion rotation, Vector3 translation )
    {
        Rotation = rotation;
        Translation = translation;
    }

    /// <summary>
    /// 頂点宣言
    /// </summary>
    public static VertexElement[] VertexElements = {
        // Rotation (16バイト)
        new VertexElement(1, 0, VertexElementFormat.Vector4,
                                VertexElementMethod.Default,
                                VertexElementUsage.TextureCoordinate, 1),
        // Traslation (12バイト)
        new VertexElement(1,16, VertexElementFormat.Vector3,
                                VertexElementMethod.Default,
                                VertexElementUsage.TextureCoordinate, 2),
    };

    /// <summary>
    /// ストライドの取得
    /// </summary>
    public static int SizeInBytes
    {
        get { return Marshal.SizeOf( typeof( BoneVertex ) ); }
    }
}

AnimationPlayerの変更

クォータニオンによるスキンアニメーションサンプルからの変更点としては、SkinRoataions、SkinTranslationsをSkinTransformsに変換、GetSkinTransformsメソッドはBoneVertex配列を返すように変更します。

モデルの頂点宣言の変更

ボーン情報を頂点ストリームに格納するので、頂点宣言を変更する必要があります。元となる頂点宣言に指定した頂点要素を追加するExtendVertexDeclarationメソッドを持つRenderHelperクラスを作ります。

ModelMeshPart.VertexDecralationは読み込み専用のプロパティなので、変更した頂点宣言はModelMeshPart.Tagプロパティに格納します。

 // モデルのMeshPartのVertexDeclarationにvFetch用の頂点宣言を
// 追加してMeshPart.Tagに格納する
foreach ( ModelMesh mesh in currentModel.Meshes )
{
    foreach ( ModelMeshPart meshPart in mesh.MeshParts )
    {
        meshPart.Tag = RenderHelper.ExtendVertexDeclaration(
                meshPart.VertexDeclaration, BoneVertex.VertexElements );
    }
}

ボーン格納用頂点バッファの生成

続いてボーン情報を格納するための頂点バッファを生成します。これも「頂点テクスチャでスキンアニメーション」の時と同じように、連続してデータを書き込むためのWritableVertexBufferというクラスを作って使用します。

 // ボーン情報を書き込むための頂点バッファの生成
boneVB = new WritableVertexBuffer( GraphicsDevice,
        animationPlayer.GetSkinTransforms().Length * BoneVertex.SizeInBytes, 2 );

こうして生成した頂点バッファに以下のようにしてボーン情報を書き込み、ストリーム1として設定します。

 // ボーンの情報を頂点ストリームに書き込む
boneVB.Flip();
int offset = boneVB.SetData<BoneVertex>( animationPlayer.GetSkinTransforms() );

gd.Vertices[1].SetSource( boneVB.VertexBuffer, offset, BoneVertex.SizeInBytes );

実際の描画ですが、ModelMeshPart.Tagに格納した頂点データを使うので、単純にModelMesh.Drawメソッドを呼ばずに、個々のModelMeshPartを描画する必要があります。

 foreach ( ModelMesh mesh in currentModel.Meshes )
{
    foreach ( Effect effect in mesh.Effects )
    {
        effect.Parameters["World"].SetValue( world );
        effect.Parameters["View"].SetValue( view );
        effect.Parameters["Projection"].SetValue( projection );
    }

    // 
    gd.Indices = mesh.IndexBuffer;
    foreach ( ModelMeshPart meshPart in mesh.MeshParts )
    {
        // ボーン情報の頂点要素を追加した頂点宣言を使う
        gd.VertexDeclaration = meshPart.Tag as VertexDeclaration;

        gd.Vertices[0].SetSource( mesh.VertexBuffer,
                            meshPart.StreamOffset, meshPart.VertexStride );

        Effect effect = meshPart.Effect;
        effect.Begin();
        effect.CurrentTechnique.Passes[0].Begin();

        gd.DrawIndexedPrimitives( PrimitiveType.TriangleList, meshPart.BaseVertex,
                                0, meshPart.NumVertices, meshPart.StartIndex,
                                meshPart.PrimitiveCount );

        effect.CurrentTechnique.Passes[0].End();
        effect.End();
    }
}

シェーダーの変更

シェーダーの変更点は、vfetch命令を使って自前で頂点データをフェッチし、boneIndicesを使ってボーン情報をフェッチするだけです。後はクォータニオンを使ったスキンアニメーションと同じ処理をします。

 //-----------------------------------------------------------------------------
// 頂点シェーダー
//=============================================================================
VS_OUTPUT VertexShader(int index : INDEX)
{
    float4 position;
    float4 normal;
    float4 texCoord;
    float4 boneIndices;
    float4 boneWeights;

    // vfetchはアセンブリ命令なのでasmブロックを使う必要がある
    asm
    {
        // 頂点データのフェッチ
        vfetch position,        index, position0
        vfetch normal,            index, normal0
        vfetch texCoord,        index, texcoord0
        vfetch boneIndices,        index, blendindices
        vfetch boneWeights,        index, blendweight
    };
    
    // ボーン情報のフェッチ
    float4 q1, q2, q3, q4;
    float4 t1, t2, t3, t4;
    
    asm
    {
        vfetch q1,    boneIndices.x, texcoord1
        vfetch q2,    boneIndices.y, texcoord1
        vfetch q3,    boneIndices.z, texcoord1
        vfetch q4,    boneIndices.w, texcoord1
        
        vfetch t1,    boneIndices.x, texcoord2
        vfetch t2,    boneIndices.y, texcoord2
        vfetch t3,    boneIndices.z, texcoord2
        vfetch t4,    boneIndices.w, texcoord2
    };

vfetchを使った開発

vfetch命令はXbox 360上でしか使うことができないので、いきなりシェーダー全体をvfetchを使って書いてしまうと、うまく動作しなかったときにvfetchの仕方が悪いのか、シェーダー内の処理自体にバグがあるのかを特定するのが難しくなります。

ですから、vfetchを使ったシェーダーコードを書く場合、実際にvfetch命令を使ったシェーダーを書く前にWindows上で動作するシェーダーを書き、シェーダーが問題無く動作するのを確認してから、vfetchを使ったものに書き換えるようにすると良いでしょう。ここで問題���起きた場合はvfetch部分が原因だと特定できるので、時間の節約になります。