XNA Frameworkブログ
 

May, 2009

  • ひにけにXNA

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

    • 1 Comments

    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部分が原因だと特定できるので、時間の節約になります。

  • ひにけにXNA

    vFetchの使い方

    • 1 Comments

    vFetchでスキンアニメーション、その2:vFetchの使い方

    Fetchを使ったスキンアニメーションの実装例を紹介する前にvfetchの基本的な使い方を紹介します。

    例えば、以下のシェーダーコードのように頂点位置、色を使用する頂点シェーダーがあるとします。

    VS_OUTPUT VertexShader(float4 position: POSITION, float4 color : COLOR )
    {
    }

    上のコードと同じ動作をするコードはvFetchを使って以下のように書くことができます。

    // INDEXセマンティクスを使ってインデックス値を取得する
    VS_OUTPUT VertexShader(int index : INDEX)
    {
        float4 pos;    // フェッチしたデータを格納するための変数、float4のみ指定できる
        float4 col;
    
        // vfetchはアセンブリ命令なのでasmブロックを使う必要がある
        asm
        {
            vfetch pos, index, position        // positionのフェッチ
             vfetch col, index, color        // colorのフェッチ
        };
        

    最初にインデックスバッファから読み込んだインデックス値を取得するためにINDEXセマンティクスを使います。次にフェッチしたデータを格納する為の変数を宣言します。この変数はfloat4型でないといけません。それ以外の型を指定するとコンパイルエラーになります。

    次にvfetch命令を使って頂点データをフェッチします。vfetchはアセンブリ命令なのでasmブロック内に書く必要があります。vfetchの書式は以下の様になっています。今までは読みやすいようにvFetchと書いてきましたが、実際の命令は全て小文字のvfetchになります。

    vfetch 取得したデータを格納する変数名, 頂点インデックス, セマンティクス

    ここではインデックスバッファから取得した頂点インデックスを使用していますが、任意のインデックスを指定することができます。このことを利用して、GameFest Japan 2009のデモでは、index内に元の頂点インデックスとインスタンスのインデックスの2つの情報を算術符号化圧縮を使って格納し、シェーダー内で展開、頂点データとインスタンス用のデータをvFetchを使って取得しています。

    vfetchに指定するセマンティクスはシェーダーで指定する入力セマンティクスの小文字となっています。大文字が混ざっているとコンパイルエラーになります。また、texcoord0, texcoord1のようにセマンティクスの後に数字を指定することもできます。下の表は入力セマンティクスとvFetchで指定するセマンティクスの対応表です。

    入力セマンティクス

    vFetchセマンティクス

    説明

    BINORMAL binormal バイノーマル(従法線)
    BLENDINDICES blendindices ブレンドインデックス
    BLENDWIEHGTS blendweight ブレンドの重み
    COLOR color カラー
    NORMAL normal 法線
    POSITION position 頂点位置
    PSIZE psize ポイントサイズ
    TANGENT tangent タンジェント(接線)
    TEXCOORD texcoord テクスチャ座標

    まとめると

    • 頂点インデックスはINDEXセマンティクスを使って取得する
    • フェッチしたデータを格納する変数の型はfloat4にする
    • vfetchはasmブロック内に記述する
    • vfetchのセマンティクスは入力セマンティクスの小文字になっている

    と、なります。

    次回は、このvFetchを使ったスキンアニメーションの実装例を紹介します。

  • ひにけにXNA

    vFetchってなに?

    • 1 Comments

    vFetchでスキンアニメーション、その1:vFetchってなに?

    Xbox 360のGPUはDirect X 9.0とDirect X 10の中間であると言われることがありますが、vFetchはその特徴を示すひとつの機能です。vFetchはシェーダー内で使えるアセンブリ命令で、Vertex Fetchの略、つまり頂点データをフェッチをするための命令です。

    vFetchを使うことで頂点数の増減こそできませんが、Direct X 10で追加されたジオメトリシェーダーを使ったファーシェーダーのように隣接する頂点データをフェッチしてフィンポリゴンをリアルタイムに生成したりすることもできます。

    自由に頂点をフェッチできるというのが最大の利点ですが、弱点もあります。

    • Windows上では使えない
    • アセンブリ命令なので、頂点フェッチの最適化などを自前でする必要がある

    vFetchはWindows上では使うことができないので、Windows/Xbox 360両対応のゲームを製作する場合にはそれぞれのプラットフォーム別に違う手法を実装する必要があります。

    vFetch命令はメモリアクセス命令でもあるので、使用する場所を間違えると大きなパフォーマンスロスになってしまいます。通常はシェーダーコンパイラーが最適化してくれるのですが、明示的にvFetchを使うときには注意が必要です。vFetchを最適な状態で使用していないと40%程のパフォーマンスロスになってしまった、という経験が私自身ありました。

    GPUはどうやって頂点データを読んでいるのか?

    vFetchの使い方を知るには、GPUがどのように頂点データを読み込んでいるのかを知っておくと理解しやすいと思います。

    頂点シェーダー内で頂点データを読み込むには以下の三つの情報が不可欠です。

    • シェーダー内の入力セマンティクス
    • 頂点宣言(VertexDeclaration)
    • 頂点ストリーム

    シェーダー内の入力セマンティクス指定は以下のコードように「変数名 : セマンティクス名」となります。この宣言によって、以下の例では、position変数にはPOSITIONセマンティクス、normal変数にはNORMALセマンティクスをといった感じに、任意の変数に指定したセマンティクスを割り当てています。

    VS_OUTPUT VertexShader(float4 position : POSITION, float3 normal : NORMAL)
    {
        // ...
    
    

    次に、頂点宣言では複数の頂点要素(VertexElement)を指定することで、セマンティクスとメモリレイアウトとの関連付けをします。セマンティクスはVertexElementUsageで表されます。頂点宣言によって、任意のセマンティクスがどのストリームのどのメモリアドレスからどんなフォーマットで読み込むのかを指定します。

    下の頂点要素の宣言は「POSITIONはストリーム番号0番、頂点データの12バイト目からVector3フォーマットで格納されている」という意味になります。

    new VertexElement(0, 12, VertexElementFormat.Vector3,
                            VertexElementMethod.Default,
                            VertexElementUsage.Position, 0),

    そして、最後に頂点ストリームの設定です。頂点ストリームの設定はVertexStream.SetSourceメソッドで行います。以下のコードの意味は「ストリーム0番はメッシュの頂点バッファの100バイト目から始まり、ストライドサイズは64バイト」という意味です。通常、ストライドサイズはひとつの頂点データと同じサイズですが、別のサイズでも問題ありません。例えば頂点サイズは20バイトだけど、他にも頂点データ以外のデータが入っているのでストライドサイズは48バイトなんていう指定の仕方もできます。

    GraphicsDevice.Vertices[0].SetSource( mesh.VertexBuffer, 100, 64 );

    ストリームと頂点宣言の関係をまとめると以下の図のようになります。

    stream

    頂点バッファには頂点という名前がついているので、GPUが読み込める頂点データ形式だけを格納するべきものだと思われがちですが、GPUからすると頂点バッファは単なるメモリの塊に過ぎないので上図のように、GPUで読み込むべきPosition, Normal, Color以外にGPUが呼ぶ必要がないSpeedがあっても全く問題ありません。GPUが気にするのはストリーム情報と頂点宣言だけです。

    GameFest Japan 2008のデモでは、このことを利用してゲームオブジェクトのリストをそのまんま頂点バッファに設定して使用することでCPU側の無駄なコピー処理を省くDirect Mapping(私が勝手に命名)は、この仕組みを利用しています。

    ただし、ストライドの最大サイズは256バイトなので、大きなデータ構造を指定できないことに注意してください。

    GPUはシェーダー内のセマンティクス、頂点宣言、そしてストリームの情報を元に以下のように頂点データを読み込んでいます。ここではDrawIndexedPrimitiveを使い、頂点シェーダー内でposition変数の値を取得するまでの過程を説明します。

    1. インデックスバッファから頂点インデックスを取得
    2. POSITIONセマンティクスに該当する頂点要素の情報を見つける
    3. 頂点アドレスの計算
      • 頂点アドレス = 頂点バッファのアドレス + ストリームオフセット + ストライド × 頂点インデックス
    4. 頂点要素のメモリアドレスの計算
      • 頂点要素アドレス = 頂点アドレス + 頂点要素のオフセット
    5. 頂点要素のメモリアドレスから指定されたフォーマット形式でデータをposition変数に読み込む

    以上のステップは普段はGPUで自動的に行われているのですが、この過程の1と2の部分をシェーダー内でやってしまおうというのがvFetch命令です。

    次回に続く

  • ひにけにXNA

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

    • 2 Comments

    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を使った手法を紹介したいと思います。

  • ひにけにXNA

    頂点テクスチャってなに?

    • 1 Comments

    頂点テクスチャでスキンアニメーション、その1:頂点テクスチャってなに?

    前回紹介したクォータニオンを使ったスキンアニメーションの実装方法はシェーダーモデル2.0では最も多くのボーン数を使うことができる手法です。この手法を使うことでオリジナルのスキンアニメーションサンプルでは59個だった最大ボーン数が、二倍近い117個になりました。

     

    もっと多くのボーン数を使える手法はないのでしょうか?

     

    残念ながら、シェーダーモデル2.0ではここが限界ですが、シェーダーモデル3.0から追加された機能を使うことで更に多くのボーンを使うことができます。

    使う機能とは以下の二つです。

    • 頂点テクスチャ
    • 浮動小数点テクスチャ

    頂点テクスチャは、その名のとおり頂点シェーダー内でテクスチャデータをフェッチ(読み込み)できる機能です。発表当時(2004年)のデモでは波のシミュレーションをピクセルシェーダーで行い、その結果を頂点テクスチャとして使うことで、立体的な波を表現していました。

    XNA Field

    また、XNA GSE 1.0のXNA Fieldデモではマルチスレッドでリアルタイムにフラクタル生成した結果を頂点テクスチャとして使用して地形モデルを表示していました。

    実際のゲームでも、バーチャファイター5では水面と雪原、フォグなどの表現で頂点テクスチャは使われています。

    頂点テクスチャの特徴としては

    • 頂点シェーダー内で最大4種類のテクスチャをフェッチできる
    • 任意のテクスチャ座標から複数回の読み込みができる
    • 浮動小数点テクスチャと組み合わせることでさまざまなデータを扱うことができる
    • 定数レジスタに比べて大量のデータを扱うことができる

    が、あります。

    普段、テクスチャというと色情報が入った画像を使いますが、0-255の範囲の数値として扱うことで色以外の情報を格納するのにも使えます。これに加えてシェーダーモデル3.0から採用された浮動小数点テクスチャを使うことで、色以外の情報を格納するのが更に容易になりました。

    また、頂点テクスチャの読み込みはピクセルシェーダー内でのテクスチャの読み込みと同様に自由にテクスチャ座標を指定することができ、複数回読み込むことができます。

    これらの特徴を利用して、画像情報以外のデータを入れたテクスチャから自由にデータを読み込むことで頂点テクスチャを定数レジスタの変わりとして使うことができます。定数レジスタ数が256だったのに対して、頂点テクスチャでは大量のデータを格納することができます。例えば2,048x2,048のテクスチャは定数レジスタ数に換算すると400万という膨大な数のデータを格納することができます。

    このように頂点テクスチャは非常に魅力的な機能ですが、弱点もあります。

    • 対応しているGPUはまだ少ない、対応していても動作が遅いものが少なくない
    • 浮動小数点テクスチャではフィルタリングが使えないものが殆ど
    • テクスチャに格納できる要素数が1,2,4と限定されている

    頂点テクスチャは最近のGPUでは当たり前のようについている機能ですが、まだ対応していないGPUの数も多く、ピクセルシェーダー内でテクスチャフェッチをするのに比べて速度的に劣る場合が多いです。

    また、頂点テクスチャで使用されるテクスチャはその性質上、浮動小数点テクスチャを使うことが多いのですが、Direct X 9.0世代の殆どのGPUでは浮動小数点テクスチャに対してのバイリニアなどのフィルタリングができないものが殆どです。しかも、できないだけなら良いのですが、テクスチャフィルタを設定した状態でフェッチするとでたらめな数値を返してきたりするので、正しいフィルタリング設定をせずに頂点テクスチャを使ってしまい、自分のプログラムが正しく動作しているかどうかを時間を掛けて調べたあげく、見つけた原因が単純にフィルタリング設定のミスだったなんてことがあります。

    頂点バッファで使われる代表的な型といえばVector3ですが、残念ながら頂点テクスチャではVector3は使えません。浮動小数点型で使えるのはSingle、Vector2、そしてVector4だけです。これはテクスチャのテクセルサイズには16ビット、32ビット、64ビット、そして128ビットの組み合わせしか指定できないのが原因です。

    頂点テクスチャを使おう

    以上のように、頂点テクスチャは魅力的な機能なのですが、使えるGPUの少なさから今まではおまけ的な機能として使われることが殆どでした。ですが、最近では使えるGPUの数も増えてきたので有効活用するケースも増えてきました。特にXbox 360やPS3はどちらも頂点テクスチャをサポートしていて、使っていると思われるタイトルも見かけるようになりました。

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

  • ひにけにXNA

    クォータニオンでスキンアニメーション

    • 5 Comments

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

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

    クォータニオンでボーン処理、その4:その実装

    クォータニオンを使ってのボーン処理の記事も4回目になりました。今回はいよいよい実際にクォータニオンを使ったスキニングアニメーションの実装方法を紹介します。

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

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

    変更部分はもちろん、オリジナルのコードのコメントも翻訳しているので、理解する手助けになると思います。

    クォータニオンでボーン処理するためにはオリジナルサンプルに以下の変更を加えます。

    • QuatTransformの実装
    • AnimationPlayerの変更
    • シェーダーの変更

    QuatTransformの実装

    まずはMatrix構造体の代わりに回転部分をクォータニオン、平行移動部分をVector3で表したQuatTransform構造体を作ります。この構造体は回転と平行移動を結合した行列と同じ振る舞いをします。これで使用するメモリがMatrixの半分以下になります。

    public struct QuatTransform
    {
        public Quaternion   Rotation;       // 回転
        public Vector3      Translation;    // 平行移動
    }

    次に、行列からQuatTransformへの変換コードを実装します。これは単純にMatrix.Decomposeを使って回転部分と平行移動部分に分割した結果をそのまま使うだけです。サンプルコードではこれ以外にもスケール部分に1以外の値が使われていないかのチェックをしてありますが、ここでは割愛します。

    public static QuatTransform CreateFromMatrix( Matrix matrix )
    {
        // 行列の分解
        Quaternion rotation;
        Vector3 translation, scale;
        matrix.Decompose( out scale, out rotation, out translation );
        return new QuatTransform( rotation, translation );
    }

    最後に、QuatTransformの結合の実装をします。ここでは演算オーバーロードを使っていますが、好みに合わせてメソッド化するといいでしょう。QuatTransform内の回転と平行移動の評価順は回転した後に平行移動となっているので、以下のコードのようにして結合します。

    public static QuatTransform operator *(QuatTransform value1, QuatTransform value2)
    {
        // 平行移動の算出
        Vector3 newTranslation;
        Vector3.Transform(ref value1.Translation, ref value2.Rotation,
                            out newTranslation);
    
        newTranslation.X += value2.Translation.X;
        newTranslation.Y += value2.Translation.Y;
        newTranslation.Z += value2.Translation.Z;
    
        // 回転部分の結合
        QuatTransform result;
        Quaternion.Concatenate(ref value1.Rotation, ref value2.Rotation,
                                    out result.Rotation);
    
        result.Translation = newTranslation;
    
        return result;
    }

    これでCPU側でスキンアニメーションに必要な最低限の機能が実装できました。後は、コンテントプロセッサ内やアニメーションプレイヤーでMatrixを指定している場所をQuatTransformに置き換えていきます。

    AnimationPlayerの変更

    オリジナルのAnimationPlayerのGetSkinTransformメソッドはMatrixの配列を返していました。それに倣ってQuatTransformの配列を返すようにしたいのですが、QuatTransform配列そのままでは定数レジスタに設定することができません。

    そこで、定数レジスタに設定しやすいように、回転部分をQuaternion配列で返すGetSkinRotations, 平行移動部分をVector3配列で返すGetSkinTranslationsに置き換えます。

    それぞれの配列にはUpdateSkinTransformsメソッド内で回転部分、平行移動部分の情報を分割して格納しています。

    public void UpdateSkinTransforms()
    {
        for (int bone = 0; bone < skinRotations.Length; bone++)
        {
            QuatTransform xform =
                skinningDataValue.InverseBindPose[bone] * worldTransforms[bone];
    
            skinRotations[bone] = xform.Rotation;
            skinTranslations[bone] = xform.Translation;
        }
    }

    QuatTransformが格納できる情報は回転と平行移動だけです。オリジナルのサンプルではMatrixを指定できるようになっていたので、スケールが入ったMatrixをワールド行列として指定することができました。ですが、QuatTransformではスケールが入った行列を扱うことができません。

    また、実際のゲームの場合、同じアニメーションを複数回、違ったワールド座標を指定して描画することが良くあります。例えば、複数のキャラクターが街中を歩き回っている場合、キャラクターごとにアニメーションを更新するのは計算コストが掛かるので、複数のキャラクターで同じアニメーションを共有して、ワールド座標だけを変えて描画することで計算コストを抑えることができます。

    以上の理由から、このサンプルではワールド座標とのAnimationPlayer内ではなく、シェーダー内で行うようにしました。

    シェーダーの変更

    シェーダーの変更点ですが、まず今まではfloat4x4の配列だったものを、回転部分と平行移動部分の二つに分けた配列を用意します。これで、定数レジスタの使用量はオリジナルのサンプルの半分になるので、最大117ボーンが使えます。以前の記事で118と書きましたが、ワールド座標情報を追加したことでひとつ減って117になりました。

    // 最大ボーン数
    // この数値を変えた時にはSkinnedModelProcessorの
    // MaxBonesも変えることを忘れないように
    #define MaxBones 117
    
    float4 BoneRotations[MaxBones];        // ボーンの回転部分
    float3 BoneTranslations[MaxBones];    // ボーンの平行移動部分
    
    

    後は行列と同じようにBoneRotations、BoneTranslationsをそれぞれそブレンディングして、行列に変換すれば動きそうに思えますが、実はそのままでは動きません。前述のように、QuatTransformは言い換えれば、回転行列と移動行列をつなぎ合わせたものだということに気をつけないといけません。

    通常の行列のブレンディングは以下の式で表されます。Lerpは線形補間関数、Mは行列、tはブレンド率を表します。

    formulat01

    これをクォータニオンを使ったブレンディングの場合、行列Mは回転部分のRと平行移動部分のTで表されるので、以下の式になります。

    formulat02もし、回転部分と平行移動部分を別々にブレンディングすると以下のような式になります。

    formulat03 この式の意味は「二つの回転を補間した回転の後に二つの平行移動を補間した分移動する」という、まったく違う意味になってしまいます。

    そこでシェーダー内でQuatTransformを行列に変換してからブレンディングする必要があります。QuatTransformから行列への変換コードは以下のようになります。

    // クォータニオンと平行移動から行列に変換する
    // スキニングに使用する場合、単純にこのメソッドを4回呼ぶのが理想的だが
    // SM2.0だと一時レジスタ(12個)を超えてしまうので、そのままでは使えない
    float4x4 CreateTransformFromQuaternionTransform( float4 quaternion, float3 translation )
    {
        float4 q = quaternion;
        float ww = q.w * q.w - 0.5f;
        float3 v00 = float3( ww       , q.x * q.y, q.x * q.z );
        float3 v01 = float3( q.x * q.x, q.w * q.z,-q.w * q.y );
        float3 v10 = float3( q.x * q.y, ww,        q.y * q.z );
        float3 v11 = float3( q.w * q.z,-q.y * q.y, q.w * q.x );
        float3 v20 = float3( q.x * q.z, q.y * q.z, ww        );
        float3 v21 = float3( q.w * q.y,-q.w * q.x, q.z * q.z );
        
        return float4x4(
            2.0f * ( v00 + v01 ), 0,
            2.0f * ( v10 + v11 ), 0, 
            2.0f * ( v20 + v21 ), 0,
            translation, 1
        );
    }

    このメソッドはひとつのQuatTransformを変換するのには便利ですが、最大4つのボーンブレンディングが必要になるスキンアニメーションで使うと、SM2.0では一時レジスタの数が足りないというコンパイルエラーがでてしまいます。これは、シェーダーコンパイラーの最適化は複雑なスカラー計算をシェーダーが得意なベクトル計算へ���変換できないことが’原因です。

    そこで、ひとつひとつのボーンを行列に変換してブレンディングするのではなく、4つのボーンをまとめて行列に変化する形式に書き直すことで、ベクトル計算の利点を活用するようにします。

    // 4つクォータニオンと平行移動から行列に変換する
    // SM2.0の一時レジスタ(12個)数制限を回避するために、一時レジスタの使用量を抑えるように
    // 書き換えたもの
    float4x4 CreateTransformFromQuaternionTransforms(
            float4 q1, float3 t1,
            float4 q2, float3 t2,
            float4 q3, float3 t3,
            float4 q4, float3 t4,
            float4 weights )
    {
        float ww = q1.w * q1.w - 0.5f;
        float3 row10 = float3( ww         , q1.x * q1.y, q1.x * q1.z ) +
                       float3( q1.x * q1.x, q1.w * q1.z,-q1.w * q1.y );
        float3 row11 = float3( q1.x * q1.y, ww,          q1.y * q1.z ) +
                       float3(-q1.w * q1.z, q1.y * q1.y, q1.w * q1.x );
        float3 row12 = float3( q1.x * q1.z, q1.y * q1.z, ww          ) +
                       float3( q1.w * q1.y,-q1.w * q1.x, q1.z * q1.z );
        
        ww = q2.w * q2.w - 0.5f;
        float3 row20 = float3( ww,          q2.x * q2.y, q2.x * q2.z ) +
                       float3( q2.x * q2.x, q2.w * q2.z,-q2.w * q2.y );
        float3 row21 = float3( q2.x * q2.y, ww,          q2.y * q2.z ) +
                       float3(-q2.w * q2.z, q2.y * q2.y, q2.w * q2.x );
        float3 row22 = float3( q2.x * q2.z, q2.y * q2.z, ww          ) +
                       float3( q2.w * q2.y,-q2.w * q2.x, q2.z * q2.z );
        
        ww = q3.w * q3.w - 0.5f;
        float3 row30 = float3( ww,          q3.x * q3.y, q3.x * q3.z ) +
                       float3( q3.x * q3.x, q3.w * q3.z,-q3.w * q3.y );
        float3 row31 = float3( q3.x * q3.y, ww,          q3.y * q3.z ) +
                       float3(-q3.w * q3.z, q3.y * q3.y, q3.w * q3.x );
        float3 row32 = float3( q3.x * q3.z, q3.y * q3.z, ww          ) +
                       float3( q3.w * q3.y,-q3.w * q3.x, q3.z * q3.z );
        
        ww = q4.w * q4.w - 0.5f;
        float3 row40 = float3( ww,          q4.x * q4.y, q4.x * q4.z ) +
                       float3( q4.x * q4.x, q4.w * q4.z,-q4.w * q4.y );
        float3 row41 = float3( q4.x * q4.y, ww,          q4.y * q4.z ) +
                       float3(-q4.w * q4.z, q4.y * q4.y, q4.w * q4.x );
        float3 row42 = float3( q4.x * q4.z, q4.y * q4.z, ww          ) +
                       float3( q4.w * q4.y,-q4.w * q4.x, q4.z * q4.z );
                       
        float4 w2 = 2.0f * weights;
        
        return float4x4(
            row10 * w2.x + row20 * w2.y + row30 * w2.z + row40 * w2.w, 0,
            row11 * w2.x + row21 * w2.y + row31 * w2.z + row41 * w2.w, 0,
            row12 * w2.x + row22 * w2.y + row32 * w2.z + row42 * w2.w, 0, 
            t1 * weights.x + t2 * weights.y + t3 * weights.z + t4 * weights.w, 1
        );
        
    }

    コード量は多くなりますが、単純に前述のメソッドを4回コピーしたものになっています。また、処理速度的にもオイラー角を使った場合だとSin、Cosといった三角関数を使う必要がありますが、クォータニオンの場合は単純な積和演算で済むので速度的にも優位です。

    これで、オリジナルのシェーダーコードのskinTransformが計算できました。最後に、ワールド座標変換行列と結合することで、頂点変換、ライティング計算用の法線変換にも使えるskinTransformになります。

        
        // スキン変換行列の取得
        float4x4 skinTransform = CreateTransformFromQuaternionTransforms(
                BoneRotations[input.BoneIndices.x], BoneTranslations[input.BoneIndices.x],
                BoneRotations[input.BoneIndices.y], BoneTranslations[input.BoneIndices.y],
                BoneRotations[input.BoneIndices.z], BoneTranslations[input.BoneIndices.z],
                BoneRotations[input.BoneIndices.w], BoneTranslations[input.BoneIndices.w],
                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));
        
        // 他の処理
        
    

     定数レジスタ使用数が半分になった

    オリジナルのサンプルではひとつのボーンに4つの定数レジスタが必要でしたが、クォータニオンと平行移動の組み合わせにすることで、使用する定数レジスタの数は2つになり、最大ボーン数が117個になりました。また、ボーン数を117個以下にして余った定数レジスタを他の用途に使うこともできるようになりました。

    CPU側の使用するメモリが半分以下になるのはもちろん、メモリアクセス数が半減することで速度的にも有利になります。

    ただし、以下の点に気をつける必要があります。

    • スケールを持つポーンアニメーションには対応していない

    通常、スキンアニメーションにはスケール情報は使わないので殆どの場合は問題はありませんが、どうしてもスケール情報を追加したい場合、そのスケール値の条件によって2つのオプションがあります。

    ひとつは、追加するスケール情報が一意、つまりスケールのx,y,zの値が同じ場合のみのスケールに対応するのであれば、QuatTransformにfloat形のスケールを追加し、QuatTransformの結合部分、シェーダー内の行列変換部分を書き換えるだけで比較的容易に実現できます。

    追加するスケール値が一意でない場合、安直にQuatTransformにVector3のスケールを追加してしまうと、必要になる定数レジスタが3つになってしまい、QuatTransformのメモリ消費量の少なさという利点を失ってしまいます。ですから、こういったスケール値を追加したい場合は、QuatTransformよりも単なる行列を使用した方が良いでしょう。

    クォータニオンを使おう

    今回のサンプルの目的のひとつは使えるボーン数を増やすことですが、もうひとつの目的はクォータニオンの有効利用法のひとつを紹介することでした。

    アニメーションデータが日増しに多くなっている昨今、殆どのゲームではクォータニオンを使ってアニメーションデータの圧縮をしています。また、今まで紹介してきたように自由な回転を扱うケースでもクォータニオンは使われています。

    これを機にクォータニオンをあなたのゲームでも使ってみてはどうでしょうか?

    もっと骨が欲しい

    今回のサンプルではボーン数が最大117まで使えるようになりましたが、次回からは使えるボーン数が256個になる手法を紹介します。

  • ひにけにXNA

    クォータニオンの使いどころ

    • 1 Comments

    クォータニオンでボーン処理、その3:クォータニオンの使いどころ

    前回はXNAフレームワーク内での基本的なクォータニオンの使い方、特にクォータニオンは回転行列の代わりに使えるといったことを紹介しました。ただ、それだけではクォータニオンを使いたいと思った人は少ないと思います。そこで今回はクォータニオン特有の利点と、ゲームでの実際の使用方法を紹介します。

    今回紹介するクォータニオンの特徴は以下の五つです

    1. メモリ使用量が3x3の回転行列の半分以下になる
    2. 回転の結合が容易にできる
    3. 回転の補間が容易にできる
    4. 正規化ができる
    5. 行列への変換がオイラー角を使うのに比べて高速

     

    メモリ使用量を減らす

    メモリの使用量については特に大量の容量を必要とするアニメーションデータを減らす目的でクォータニオンが使われることが多いです、Brute Forceではメインキャラクター4体のアニメーションだけで約2時間近いデータがありました。このデータを単純に4x4の行列で持つと14MB以上になりますが、クォータニオンと移動の組み合わせにすると5.6MBと、半分以下のサイズになり、更にクォータニオン内のx,y,z,wの値の範囲は-1~1までなので、それを量子化してそれぞれの要素を16ビットと半分のサイズにすることで、4.5MBと元の3分の1以下のサイズに減らしていました。

    これでもXbox 1のメインメモリの64MBの7%近くを使用していたので、いつもアニメーターの人とプログラマーの間で「もっとアニメーションを増やしたい」「ダメ、メモリ足りない」というやり取りがしばしばありました。

     

    回転の結合が容易にできる

    回転の結合についてですが、例えばフライトシミュレーター系のゲームを作っている場合、現在の飛行機の姿勢を保持しておいて、コントローラーによってフレーム毎のヨー、ピッチ、ロールの差分を現在の姿勢と結合することで飛行機の姿勢を変更します。

    // コントローラーによるヨー、ピッチ、ロールの取得
    float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
    float factor = MathHelper.ToRadians(180.0f) * dt;
    float yaw = (padState.Triggers.Left - padState.Triggers.Right) * factor;
    float pitch = padState.ThumbSticks.Left.Y * factor;
    float roll = padState.ThumbSticks.Left.X * factor;
    
    // 行列を使った姿勢変更
    Matrix deltaMtx = Matrix.CreateFromYawPitchRoll(yaw, pitch, roll);
    rotationMatrix = rotationMatrix * deltaMtx;
    
    // クォータニオンを使った姿勢変更
    Quaternion deltaQuat = Quaternion.CreateFromYawPitchRoll(yaw, pitch, roll);
    rotationQuaternion = Quaternion.Concatenate(rotationQuaternion, deltaQuat);
    
    // クォータニオンの正規化
    rotationQuaternion.Normalize();

    行列でも回転の結合ができるのですが、行列の場合、単精度浮動小数点(float)の誤差が計算ごとに溜まっていき、最終的には正常な回転行列ではなくなってしまうという問題があります。そこで、正規化を使うことで計算誤差でおかしくなった回転状態を簡単に正常に戻すことのできるクォータニオンです。

    と、書きたかったのですが、よくよく考えるとXNAフレームワークを使っている場合、計算自体はXbox 360、Windows上のどちらでも倍精度(double)で行われるので、あんまり問題ありませんでした。ちなみに、単精度の計算すると60fpsのゲームで3分後くらいにモデルの形状がおかしくなってしまうという問題がありました。

    そこで、プログラムのアップデート内で実際の速度の3,000倍の速さで更新するプログラムを作って試してみました。下はプログラムを実行し始めてから三時間経過した時の様子です。左側は行列で回転の結合をしただけのもの、右側はクォータニオンを使って結合、正規化をしたものです。左側の機体が右側の機体よりも大きくなっているのが分かると思います。これは、回転行列の結合を続けることで浮動小数点の誤差が積み重なって、正しい回転行列ではなくなってしまったという証拠です。

     itoxe1-17 ただし、前述のようにこのプログラムは本来の3000倍の速度で動作しているので、実際の時間に換算して1年になるので、XNAフレームワークをWindows, Xbox 360上で使っている限りは問題ありませんが、XNAフレームワーク以外で単精度の浮動小数点を使っている場合は精度の問題が発生するので、クォータニオンを使って回転の結合をした後に正規化をすることによって、この誤差を修正することができます。

     

    回転の補間が容易にできる

    次にクォータニオンを使う醍醐味と言っていいほどの特徴が回転の補間です。オイラー角を使った場合、ひとつの軸のに対しての回転であれば回転の補間はできますが、自由な回転同士の補間は不可能です。それに対して、クォータニオンを使った場合はどんな回転の状態であれ、簡単に補間することができます。

    また、スキンアニメーションなどで複数の行列をブレンドしている、つまり補間していますが、これは線形補間でしかないので回転が含まれている行列同士の補間では厳密には正確な補間ができないのですが、スキンアニメーションのボーンブレンディングの場合は誤差が無視できる範囲でブレンディングが行われるので殆どの場合は問題ないように見えているというのが現状です。

    クォータニオンの補間にはLerp(ラープ)、Slerp(スラープ)を使って簡単に正確な回転補間ができます。ちなみにLerpはLinear Interporationの略で日本語では線形補間、SlerpはSphercal Linear Interpolationの略で、日本語では球面線形補間となります。

    下は0度回転の状態と90度回転したものを補間するコードです。ここではSlerpを使っていますが、この場合では線形補間であるLerpを使っても問題ありません。線形とは言ってもクォータニオンの世界のなかの線形であって、3次元空間の線形とは違います。クォータニオンのLerpとSlerpでは前者の方が計算が軽いですが、誤差が発生してしまいます。Brute Forceでは計算速度を稼ぐために回転角度が大きく違う時以外はLerpを使い、誤差を修正するために時々正規化していました。

    // 0度と90度回転の行列とクォータニオンの生成
    Matrix mtx0 = Matrix.CreateRotationZ(MathHelper.ToRadians(0));
    Matrix mtx1 = Matrix.CreateRotationZ(MathHelper.ToRadians(90));
    
    Quaternion q0 = Quaternion.CreateFromAxisAngle( Vector3.UnitZ, MathHelper.ToRadians(0));
    Quaternion q1 = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathHelper.ToRadians(90));
    
    // 補間
    Matrix m = Matrix.Slerp(mtx0, mtx1, t);
    Quaternion q = Quaternion.Lerp(q0, q1, t);

    コードだけでは、実際にどのように補間されているの分からないので、テストプログラムを使って動画にしてみました。赤い軸が0度の地点、緑の軸が90度回転した位置を表しています。青い点は行列の補間の結果で、白い点はクォータニオンの補間の結果になります。

    クォータニオンの補間では白い点が綺麗に円弧を描いているのに対して、行列の変換では青い点が直線的に動いているのが分かると思います。

    blend-sphere-01

    また、拡大してみると判りますが、クォータニオンでの補間だと問題ありませんが、行列の補間の場合だと形がゆがんでいます。動画を見ると、このゆがみは補完する中間地点に近づくほどに大きくなり、始点と終点付近で元に戻っている様子がわかると思います。

    blend-sphere-02

    このように、行列の線形補間は回転の補間が苦手ということがわかったと思います。アニメーションデータの容量を少なくするためにキーフレーム間の補間をするときがありますが、すばやい回転アニメーション、例えば野球選手の投球モーションの腕の部分のように短い時間で大きな回転動作をする場合に行列の線形補間を使うと、回転モーションが正しく保管されずに縮こまった投球モーションになってしまいます。そこでクォータニオンを使えば少ないアニメーションデータでも綺麗な投球モーションになります。

     

     

    正規化ができる

    これは既に上で何度か説明していますが、浮動小数点による計算誤差や、Lerpを使った時などに回転状態が徐々に崩れていってしまうという問題があるのですが、クォータニオンの場合は単純にQuaternion.Normalizeを呼び出すだけで簡単に正常な回転状態に戻すことができます。

    この正常な状態に戻せるというのは重要なことで、実際にゲームを作っていると最初プレイしているうちは滑らかに動いていたのに、時間が経つとガクガクしてくるという問題に直面することがあり、その原因が精度の問題だった場合、原因を突き止めるのが非常に困難になるので、こういった問題が発生する前から誤差があっても動作するコードが書けるというのは時間の節約にもなります。

     

     

    行列への変換がオイラー角を使うのに比べて高速

    例えば爆破などのエフェクトで回転しながら飛び散る破片があるとします。この時に、X,Y,Zのオイラー角を使って実装した場合、フレーム毎に描画する前に行列に変換しないといけませんが、その変換過程で計算に時間の掛かるSin,Cosの三角関数が使用されます。それに対して、クォータニオンの場合は最初に回転速度を表すクォータニオンを生成しておけば、フレーム毎の更新では現在の姿勢を示すクォータニオンと結合するだけですみます。この結合とクォータニオンから行列への変換は計算時間の掛からない単純な積和演算だけで済むのでむので大量のオブジェクトを処理するときに有用です。

     

     

    クォータニオンを使う目安

    三回に渡ってクォータニオンの特徴を説明してきましたが、最後にクォータニオンをいつ使うべきかを判断するポイントを紹介します。

    クォータニオンを使うと便利

    • アニメーションデータが多すぎるので圧縮したい
      • キーフレーム間の回転補間
    • 自由に回転状態を変化させたい
      • 岩などのオブジェクトが地面の起伏の状態によって回転状態が変化する場合
      • 飛行機などの姿勢制御
    • 回転状態を変化させる必要はないけど対象となるオブジェクトが大量にある
      • 爆発などで大量の破片がランダムに回転しながら飛び散るエフェクト

    オイラー角で十分

    • 制限された回転
      • FPS、TPSなどのカメラコントロール
    • 回転状態を変化する必要がない
      • 爆発などで少数の破片がランダムに回転しながら飛び散るエフェクト
    • 誤差があっても大丈夫
      • 回転の結合が短時間だけ必要な場合、上記のエフェクトなどカットシーン的な場面で使われる回転

    次は、いよいよ(やっと?)クォータニオンを使ったボーン数節約の実装例を紹介します。

Page 1 of 1 (7 items)