忍者ブログ

Memeplexes

プログラミング、3DCGとその他いろいろについて

XNA 複数のメッシュを一度のDrawで描画(シェーダー・インスタンシング)

前回はハードウェア・インスタンシングをやりましたが、これはGPUシェーダー・モデル3.0以上でなければ動きません。
どうやらまだ2.0までのものがよく使われているそうなので、これでは困る場合もあるでしょう。

実は、シェーダーモデルが2.0でも上手くいく方法があります。
グラフィックスカードのメモリを少々食うのですが、モデルそのものはたくさん複製して1つのVertexBufferに入れておいて、インスタンスの情報は配列にしてHLSLのグローバル変数としてセットしてしまうというものです。
これをシェーダー・インスタンシングといいます。

ハードウェア・インスタンシングではVertexBufferにインスタンスの情報を格納しましたが、こちらはHLSLのグローバル変数に格納します。
そして、本来のVertexBufferの中にあるそれぞれのモデルに、インデックスを振ります。
そのインデックスから対応するインスタンスの情報を特定して、頂点に適用するのです。(どうもわかりにくいですね・・・)

こうすることによって、1つのVertexBufferから、自由に動かせる複数のモデルを一度に描画することが出来ます。
モデルのデータを複製して1つのVertexBufferに入れるため、その分無駄なグラフィックス・カードのメモリを食うのですが、それでも一つ一つモデルを描画するよりもパフォーマンスが良くなる(CPUの負荷が減るので)そうです。

HLSLのコードは次のようになります:

ShaderInstancing.fx

float4x4 InstanceTransforms[10];

struct VertexPositionColor
{
	float4 Position : POSITION;
	float4 Color : COLOR;
};

VertexPositionColor VertexShader(
	VertexPositionColor input,
	float instanceIndex : TEXCOORD1
	)
{
	VertexPositionColor output;
	output.Position = mul(input.Position, InstanceTransforms[instanceIndex]);
	output.Color = input.Color;
	return output;
}

float4 PixelShader(float4 color : COLOR):COLOR
{
	return color;
}

technique ShaderInstancing
{
	pass ShaderInstancingPass
	{
		VertexShader = compile vs_2_0 VertexShader();
		PixelShader = compile ps_2_0 PixelShader();
	} 
}


これは、10個のインスタンスを同時に描画するためのエフェクトファイルです。

グローバル変数InstanceTransformsの中にはそれぞれのインスタンスの情報(マトリックス)が格納されています。
頂点シェーダの入力として入ってくるインデックスから、対応するマトリックスを割り出し、それをかけてモデルを変形しています。

C#側はこんなかんじです:
(HLSLのグローバル変数に値をセットし、インデックス付のVertexBufferを作るだけです)

MyGame.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;

struct VertexPositionColorIndex
{
    public Vector3 Position;
    public Color Color;
    public float Index;

    public static readonly int SizeInBytes 
        = System.Runtime.InteropServices.Marshal.SizeOf(
            typeof(VertexPositionColorIndex)
        );

    public static readonly VertexElement[] VertexElements
        = new VertexElement[]{
            new VertexElement(
                0,
                0,
                VertexElementFormat.Vector3,
                VertexElementMethod.Default,
                VertexElementUsage.Position,
                0
            ),
            new VertexElement(
                0,
                sizeof(float)*3,
                VertexElementFormat.Color, 
                VertexElementMethod.Default, 
                VertexElementUsage.Color, 
                0
            ),
            new VertexElement(
                0, 
                sizeof(float)*3 + 4, 
                VertexElementFormat.Single, 
                VertexElementMethod.Default, 
                VertexElementUsage.TextureCoordinate, 
                1
            )
        };
}

class MyGame : Game
{
    GraphicsDeviceManager graphics;
    ContentManager content;

    const int instanceCount = 10;
    VertexPositionColorIndex[] vertices 
        = new VertexPositionColorIndex[3 * instanceCount];
    Matrix[] instanceTransforms = new Matrix[instanceCount];

    Effect effect;
    VertexBuffer vertexBuffer;
   

    public MyGame()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);


        for (int i = 0; i < instanceCount; i++)
        {
            vertices[3 * i].Color = Color.Blue;
            vertices[3 * i].Position = new Vector3(-0.1f, 0.1f, 0);
            vertices[3 * i].Index = i;
            
            vertices[3 * i + 1].Color = Color.White;
            vertices[3 * i + 1].Position = new Vector3(0.1f, 0.1f, 0);
            vertices[3 * i + 1].Index = i;

            vertices[3 * i + 2].Color = Color.Red;
            vertices[3 * i + 2].Position = new Vector3(0.1f, -0.1f, 0);
            vertices[3 * i + 2].Index = i;
        }

        for (int i = 0; i < instanceTransforms.Length; i++)
        {
            instanceTransforms[i] = Matrix.CreateTranslation(0.1f * i, 0, 0);
        }
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("ShaderInstancing");

            vertexBuffer = new VertexBuffer(
                graphics.GraphicsDevice,
                vertices.Length * VertexPositionColorIndex.SizeInBytes,
                ResourceUsage.None
                );
            vertexBuffer.SetData<VertexPositionColorIndex>(vertices);

            graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration(
                graphics.GraphicsDevice,
                VertexPositionColorIndex.VertexElements
                );
        }
    }


    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

        graphics.GraphicsDevice.Vertices[0].SetSource(
            vertexBuffer,
            0,
            VertexPositionColorIndex.SizeInBytes
            );

        effect.Parameters["InstanceTransforms"].SetValue(instanceTransforms);

        effect.Begin();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Begin();

            graphics.GraphicsDevice.DrawPrimitives(
                PrimitiveType.TriangleList,
                0,
                vertices.Length / 3
                );

            pass.End();
        }

        effect.End();
        
    }
}


shaderInstancing.JPG

ここでは、三角形を10個描画しています。
複数のモデルを描画しているにもかかわらず、Drawコールは1回だけです。

VertexBufferにモデルのコピーが10個入っていて、それを一度に描画しているからです。

普通ならその方法ではそれぞれのモデルのインスタンスを別々に動かすことは難しいはずですが(少なくともVertexBufferを毎回アップデートしなければいけません)、シェーダー・インスタンシングは、動く部分のデータをVertexBufferから切り離すことによって、それを可能にしています。
動くデータはVertexBufferではなくて、HLSLのグローバル変数に入っているのです。
VertexBufferに入っているのは、そのグローバル変数から、対応する動くデータを取り出すためのキーです。
キーならば固定されていても問題ありません。

こういう方法によって、それぞれのモデルのインスタンスを自由に動かしつつ、全てを一度に描画することが出来るわけですね。

拍手[0回]

PR