したかみ ぶろぐ

Unity成分多め

ComputeShaderとDrawMeshInstancedIndirectでGraphicsBufferを使う

始めに

去年にこの記事を拝見し、GraphicsBufferなるものの存在を知りました。

zenn.dev



また、この記事内にて遠い将来ComputeBufferが無くなり、GraphicsBufferに切り替わる予定とのことでした。 (GraphicsBuffer, Mesh vertices and Compute shaders - Unity Forum より)

なので、一度簡単にGraphicsBufferについて触ってみようと思います。



GraphicsBuffer

簡単にGraphicsBufferについてまとめます。

コンストラク

次のように定義されています。

public GraphicsBuffer (GraphicsBuffer.Target target, int count, int stride);


第一引数のtargetではGraphicsBufferの用途を指定します。

例えば、Graphics.DrawMeshInstancedIndirectのargumentsBufferとして使用する場合はGraphicsBuffer.Target.IndirectArguments、ComputeShaderのStructuredBufferとして使用する場合はGraphicsBuffer.Target.Structuredを指定します。


また、このGraphicsBuffer.Targetは次のようbitで定義されています。 (Unity公式リポジトリより

なので、OR演算などで組み合わせられるようですが、このあたりは詳しくは分かりません。

        public enum Target
        {
            Vertex            = 1 << 0,
            Index             = 1 << 1,
            CopySource        = 1 << 2,
            CopyDestination   = 1 << 3,
            Structured        = 1 << 4,
            Raw               = 1 << 5,
            Append            = 1 << 6,
            Counter           = 1 << 7,
            IndirectArguments = 1 << 8,
            Constant          = 1 << 9,
        }


第二引数、第三引数についてはComputeBufferと同じく、データの長さとデータサイズを表しています。



ComputeBufferからGraphicsBufferに書き換える

既存のComputeBufferはGraphicesBufferに置き換えられます。

StructuredBuffer, AppendStructuredBuffer, ConsumeStructuredBuffer

GraphicsBufferはComputeShaderで使われるStructuredBufferとして扱うことができます。

ComputeBufferとの違いとしては、ComputeBufferではComputeBufferType.Structuredを指定する必要はありませんでしたが、GraphicsBufferでは明示的に指定しなければいけません。

// ComputeBuffer
m_particleBuffer = new ComputeBuffer(m_instanceCount, Marshal.SizeOf(typeof(Particle)));
m_particleBuffer.SetData(particles);
m_particleCalclator.SetBuffer(m_calcParticlePositionKernel, "_Particle", m_particleBuffer );

// GraphicsBuffer
m_graphicsBuffer_Particle = new GraphicsBuffer(GraphicsBuffer.Target.Structured, m_instanceCount, Marshal.SizeOf(typeof(Particle)));
m_graphicsBuffer_Particle.SetData(particles);
m_particleCalclator.SetBuffer(m_calcParticlePositionKernel, "_Particle", m_graphicsBuffer_Particle);



また、AppendStructuredBuffer, ConsumeStructuredBufferの場合はGraphicsBuffer.Target.Appendを指定することで扱えます。

その他、AppendStructuredBufferで使われるComputeBuffer.CopyCountGraphicsBuffer.CopyCountに置き換えられます。

m_particlePoolBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Append, m_instanceCount, Marshal.SizeOf(typeof(int)));
m_particlePoolBuffer.SetCounterValue(0);

GraphicsBuffer.CopyCount (m_particlePoolBuffer, m_particlePoolCountBuffer, 0);



Graphic.DrawMeshInstancedIndirect

インスタンシングで使われるDrawMeshInstancedIndirectでもGraphicsBufferを使えます。

この場合はGraphicsBuffer.Target.IndirectArgumentsを指定します。

// ComputeBuffer
m_argsBuffer = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
m_argsBuffer.SetData(args);

// GraphicsBuffer
m_graphicsBuffer_Args = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, 1, args.Length * sizeof(uint));
m_graphicsBuffer_Args.SetData(args);

Graphics.DrawMeshInstancedIndirect(
    m_mesh,
    0,
    m_instanceMaterial,
    m_bounds,                                                                 
    m_graphicsBuffer_Args,
    0,
    null,
    m_shadowCastingMode,
    m_receiveShadows
);


このほかにGraphics.DrawProcedural系のメソッドでも使えるそうです。



Material.SetBuffer

Graphic.DrawMeshInstancedIndirectを使う場合はMaterial.SetBufferでデータを渡していましたが、こちらもGraphicsBufferを使えます。

m_graphicsBuffer_Particle = new GraphicsBuffer(GraphicsBuffer.Target.Structured, m_instanceCount, Marshal.SizeOf(typeof(Particle)));
m_graphicsBuffer_Particle.SetData(particles);
m_instanceMaterial.SetBuffer("_ParticleBuffer", m_graphicsBuffer_Particle);


総括

おおよそ触ってみて、これまでの私が書いてきたプログラムはすべてGraphicsBufferに置き換えられることがわかりました。



GraphicsBufferの利点

ComputeBufferからGraphicsBufferに置き換えられることは分かりましたが、それらを置き換えることで処理が高速になるかと言われればそうではないようです。

しかし、GraphicsBufferではMeshの頂点情報やインデックス情報を扱えるようになったようです。

私自身Meshの頂点情報やインデックス情報を扱った機会はほとんどありませんが、Mesh.GetVertexBufferで直接`GraphicsBufferに頂点情報を受け取れるようになります。 (私の環境 Unity2020.3.7f1ではまだ用意されていなかった)

そうなると、過去記事で頂点情報から力場を求めるプログラムで無駄なメモリを使わずに頂点情報を扱うことができます。

shitakami.hatenablog.com


// 前はm_verticesに一度移さないといけない手間があった
m_skinnedMeshRenderer.BakeMesh(m_bakeMesh);
m_bakeMesh.GetVertices(m_vertices);
m_positionsBuffer.SetData(m_vertices);

// 頂点情報をそのままGraphicsBufferに渡せる
m_skinnedMeshRenderer.BakeMesh(m_bakeMesh);
m_positionBuffer = m_bakeMesh.GetVertexBuffer();


また、Graphics.DrawProcedural系でも頂点情報などをGraphicsBufferでそのまま渡せるらしいので、この辺りは少し調べてみたいです。



まとめ

簡単に調べてみて、ComputeBufferの使い方とほぼ同じなので抵抗なく使えることがわかりました。

最初はGraphicsBufferに変わることで得られる恩恵とは無縁かと思っておりましたが、過去のプログラムの面倒くさい部分が解消されることが分かったので良かったです。

最近はGPU系のことを全くやっていないので、また何かやってみようと思います。



参考

始めにも載せましたが、きっかけはこちらの記事でした。また、リンクの内容もとても良かったので詳しく知りたい方は参考にしてください。

zenn.dev



また、UnityStationでもGraphicsBufferについて詳しくお話しされていたのでお時間があるときに見てみると良いと思います。

learning.unity3d.jp