忍者ブログ

Memeplexes

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

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

この記事は古いです。
こちらと合わせてお読みください。



メッシュ・インスタンシング
ここのところXNA Creators Clubのサンプル、Mesh Instancingをいじっていたのですが、あまりに難解だったのでわかったことをメモしておきます。

このMesh Instancingというのが何をやっているかと言うと、複数のモデルを一度だけのDrawIndexedPrimitivesメソッドの呼び出しで描画してしまうと言うものです。(モデルのデータをC#で言う「クラス」にして、GPUの中でインスタンスをいっぱい作って増やすイメージです。普通はそれぞれのインスタンスを別々にGPUに送りますが(データ自体はGPUにあるので大したことありませんが)、Mesh Instancingではまず1つのモデルをGPUに送って、それを増やしているような感じです。)

何でそんなことをするのかと言うと、Draw****をたくさん呼ぶのははCPUに負荷がかかるからです。(GPUにパフォーマンスのボトルネックがあるのなら、これは意味がありません)
サンプルの解説によると、モデルの数が数千くらいになる(Draw****を数千回呼ぶ)とヤバいそうです。
Draw****のCPUへの負荷はモデルの複雑さにかかわらず一定なので、単純なモデルをたくさん個別に描画するのはCPUに負担がかかるわけです。(逆に、複雑なモデルを1回描画するだけならほとんどCPUに負担はないでしょう。GPUだけの問題になります)

そこで使われるのがこのMesh Instancingなるものです。
モデルをたくさん描画したい時、それぞれのモデルにつきDrawを呼ぶのではなく、全部まとめて1回のDrawにしてしまえば、CPUにはほとんど負荷がかからないというわけです。

これは、モデルが単純で、大量に描画したい時に使うCPUパフォーマンス改善テクニックです。複雑なモデルを少しだけ描画したい時にはほとんど意味が無いでしょう。

このMesh Instancingサンプルでは3つの方法をとっています。

Hardware Instancing Windows
(shader 3.0)
Shader Instancing Windows
(shader 2.0)
VFetch Xbox360

この3つ(に付け加え、Mesh Instancingテクニックを使わない方法2つ)が1つのソリューションにごっちゃになっているため、コードがスパゲッティの様相を呈しています。
別々にしてくれればまだわかりやすいんですけどね……。

Hardware Instancing
ここでは、一番上のHardware Instancingとかいうのをやってみたいと思います。

Hardware Instancingはどのようにモデルを増やしているかと言うと、VertexBufferを使います。

モデルのメッシュを表すVertexBufferとは別に、各モデルのインスタンス固有の情報(モデルのインスタンスの位置とか回転とかです・・・・・・C#で言うならちょうどメンバ変数のようなものでしょうか)を格納したVertexBufferを作るのです。
それを、2つともGraphicsDevice.Verticesにセットしてやります。

   解説
 GraphicsDevice.Vertices[0]  普通のVertexBufferと同じ。モデルを構成する頂点を持っています。ここでは、C#で言うなら「クラス」の役割を果たします。
 GraphicsDevice.Vertices[1]  頂点の代わりに、モデルの各インスタンス固有の情報をもっています。C#で言うなら「メンバ変数」がいっぱい詰まっている感じ。構造体の配列みたい。

さて、こうすると疑問が2つ出てきます。
まず、VertexDeclarationはどうなるんだと思われるかもしれません。
VertexBufferが2つあるのなら、どちらの要素からVertexDeclarationを作ればいいのでしょう?

答えは、両方です。
VertexDeclarationコンストラクタはVertexElement構造体の配列を引数に取りますが、その配列に「モデルの各インスタンス固有の情報」を表すVertexElementも入れてやればいいのです。(そのVertexElementのStreamプロパティの値は0ではなく1となります)

2つ目の疑問は、「ただGraphicsDevice.Vertices[1]に各インスタンス固有の情報とやらをセットするだけで上手くいくのかな?(上手くいくんだったら、残念だけど柔軟性に問題があるんじゃ・・・)」というものでしょう。

幸か不幸か、上手くいきません。(笑)
Vertices[0]にもVertices[1]にも周波数をセットして、頂点シェーダでうまく調和するようにしなければなりません。
Vertices[0]Vertices[1]の各要素それぞれに使われるわけですから、頂点シェーダで使われるリズム(?)が違うわけです。
2重ループの外と中みたいな感じでしょうか。
例を見ましょう:

HardwareInstancing.fx
struct VertexPositionColor
{
	float4 Position : POSITION0;
	float4 Color : COLOR;
};

VertexPositionColor VertexShader(
	VertexPositionColor input, float3 position:POSITION1
	)
{
	input.Position.xyz += position;
	return input;
}

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

technique HardwareInstancing
{
	pass Pass1
	{
		VertexShader = compile vs_3_0 VertexShader();
		PixelShader = compile ps_3_0 PixelShader();
	}
}

まず、頂点シェーダの引数positionVertices[1]から来た)が固定されていて、Vertices[0]から来たinputは目まぐるしく変わっていきます。
その次に、positionが次のものに代わって、inputは同じように目まぐるしく変わっていき・・・・・をくりかえしていきます。
この2つは、要素が使われるタイミングが違うわけです。

この2つの使われる周波数を設定するには、VertexStream.SetFrequencyOfIndexDataVertices[0]用)とVertexStream.SetFrequencyOfInstanceDataVertices[1]用)メソッドを使います。

public void SetFrequencyOfIndexData ( int frequency )

このfrequencyはモデルのインデックスの周波数です。実際に使う時には、インスタンスの数をセットします。どうやらIndexBufferと一緒に使わないといけないみたいです(IndexBufferを使わずにやってドつぼに嵌りました・・・)

public void SetFrequencyOfInstanceData ( int frequency)

こっちのfrequencyはインスタンス・データの周波数です。とりあえず1をセットします。


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



class MyGame : Game
{
    GraphicsDeviceManager graphics;
    ContentManager content;


    VertexPositionColor[] vertices = new VertexPositionColor[]{
        new VertexPositionColor(new Vector3(-0.1f, 0.1f, 0), Color.Blue),
        new VertexPositionColor(new Vector3(0.1f, 0.1f, 0), Color.White),
        new VertexPositionColor(new Vector3(0.1f, -0.1f, 0), Color.Red)
    };

    Vector3[] trianglePositions = new Vector3[] {
        new Vector3(),
        new Vector3(0.1f, 0.1f, 0)
    };


    //Graphics Device Objects
    Effect effect;
    VertexBuffer triangleVertexBuffer;
    IndexBuffer indexBuffer;
    VertexBuffer positionVertexBuffer;
   

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

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

            graphics.GraphicsDevice.VertexDeclaration 
                = createVertexDeclaration();


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


            indexBuffer = new IndexBuffer(
                graphics.GraphicsDevice,
                sizeof(int) * 3,
                ResourceUsage.None,
                IndexElementSize.ThirtyTwoBits
                );
            indexBuffer.SetData<int>(new int[] { 0, 1, 2 });


            positionVertexBuffer = new VertexBuffer(
                graphics.GraphicsDevice,
                sizeof(float) * 3 * trianglePositions.Length,
                ResourceUsage.None
                );
            positionVertexBuffer.SetData<Vector3>(trianglePositions);
        }
    }

    VertexDeclaration createVertexDeclaration()
    {
        System.Collections.Generic.List<VertexElement> elements 
            = new System.Collections.Generic.List<VertexElement>();

        elements.AddRange(VertexPositionColor.VertexElements);
        elements.Add(
            new VertexElement(
                1,
                0,
                VertexElementFormat.Vector3,
                VertexElementMethod.Default,
                VertexElementUsage.Position,
                1)
            );

        return new VertexDeclaration(
            graphics.GraphicsDevice,
            elements.ToArray()
            );
    }

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


        graphics.GraphicsDevice.Vertices[0].SetSource(
            triangleVertexBuffer,
            0,
            VertexPositionColor.SizeInBytes
            );
        graphics.GraphicsDevice.Vertices[0].SetFrequencyOfIndexData(
            trianglePositions.Length
            );

        graphics.GraphicsDevice.Vertices[1].SetSource(
            positionVertexBuffer,
            0,
            sizeof(float) * 3
            );
        graphics.GraphicsDevice.Vertices[1].SetFrequencyOfInstanceData(1);

        graphics.GraphicsDevice.Indices = indexBuffer;


        effect.Begin();

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

            graphics.GraphicsDevice.DrawIndexedPrimitives(
                PrimitiveType.TriangleList,
                0,  //baseVertex
                0,  //minVertexIndex
                vertices.Length,  //numVertices
                0,  //startIndex
                vertices.Length/3   //primitiveCount
                );

            pass.End();
        }

        effect.End();
        
    }
}


hardwareInstancing.JPG
ハードウェアインスタンシングで2つの三角形を描画しています。(Drawは一回だけ)

モデルは1つだけですが、その1つだけのモデルを使って位置{0, 0, 0}と{0.1f, 0.1f, 0}にずらして描画しています。

このようにモデルの数が少ない場合では効果は全く実感できません(シンプルにするのが目的だったんです。勘弁してください)が、これが数千個にもなるとかなり効いてくるのでしょうね。

また、ここでは単にインスタンス固有のデータとして位置(Vector3)を使っていますが、もっと柔軟にしたいのならいろんなデータの変換(平行移動も回転も拡大も)が出来るマトリックス(Matrix構造体)を使うべきでしょうね。じっさい、Xna Creators ClubのInstanced Model Sampleではそのようにマトリックスを使っています。(そして難読化しているのですが)
















拍手[2回]

PR