circle-loader
by
168/ 2

前書き

最近、Compute Shader技術は広く利用されています。この前の『Moonlight Blade Mobile(天涯明月刀M)』や『Naraka: Bladepoint (中国語: 永劫无间)』テームは技術共有をしている時、よくCompute Shaderに言及していました。

 

ComputeShaderに対し、Unity公式が次のように紹介しています。

https://docs.unity3d.com/Manual/class-ComputeShader.html

 

Compute Shaderは、他のShaderと同様にGPUで実行されますが、レンダリングパイプラインから独立しています。これを使用して、多数の並列GPGPUアルゴリズムを実装でき、ゲームを高速化させます。

 

Unityでは、プロジェクトで右クリックしてComputeShaderファイルを作成します。

生成されたファイルは一種のアセットファイルに属し、すべてのファイルは.computeをサフィックスとします。

 

この中のデフォルトのコンテンツを見てみましょう。

#pragma kernel CSMain

RWTexture2D<float4> Result;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);
}

この記事の主な目的は、私のような初心者が数行のコードの意味を理解できるようにすることであり、基本を学んだ後でないと、よりすばらしいコードを読むことができません。

 

言語

 

Unityは、DirectX 11のHLSL言語を使用します。これは、対応するプラットフォームに自動的にコンパイルされます。

 

kernel

次に、1行目を見てみましょう。

#pragma kernel CSMain

CSMainは実に一つの関数であり、コードの後に見られます。kernelはカーネルを意味します。この行は、CSMainという名前の関数をカーネルとして宣言しており、カーネル関数とも呼ばれています。このカーネル関数は、最終的にGPUで実行されます。

 

Compute Shaderは、少なくとも1つのカーネルを持たないと、呼び出すことができません。宣言方法は次のとおりです。

#pragma kernel functionName

また、これを使用して、Compute Shaderで複数のコアを宣言することもできます。また、次のように、このディレクティブの後にいくつかの前処理マクロコマンドを定義することもできます。

#pragma kernel KernelOne SOME_DEFINE DEFINE_WITH_VALUE = 1337

#pragma kernel KernelTwo OTHER_DEFINE

 

コマンドの後にコメントを書き込むことはできませんが、新しい行にコメントを書き込む必要があります。たとえば、次のように書き込むと、コンパイルエラーが発生します。

#pragma kernelfunctionName//コメント

 

RWTexture2D

 

次に、2行目を見てみましょう。

RWTexture2D<float4> Result;

テクスチャに関連する変数が宣言されているようです。これらのキーワードの意味を詳しく見てみましょう。

 

RWTexture2Dでは、RWは実際にはReadとWriteを意味します。Texture2Dは2次元のテクスチャであるため、ComputeShaderで読み取りと書き込みが可能な2次元のテクスチャを意味します。

 

読み取りのみを行い、書き込みを行わない場合は、Texture2Dタイプを使用できます。

 

テクスチャはピクセルで構成されており、各ピクセルには添え字があることがわかっているため、たとえば、Result [uint2(0,0)]のように、ピクセルの添え字を使用してそれらにアクセスできます。

 

同じ各ピクセルには、対応する値があります。これは、読み取りまたは書き込みを行う値です。この値のタイプは<>で記述されます。これは通常、RGBA値に対応するため、float4タイプです。通常、Compute Shaderでテクスチャを処理してから、FragmentShaderで処理されたテクスチャをサンプリングします。

 

このようにして、この行のコードの意味を大まかに理解し、各ピクセルの値がfloat4であるResultという名前の読み取りおよび書き込み可能な2次元テクスチャを宣言します。

 

Compute Shaderでは、RWTexture以外、読み取り可能および書き込み可能なタイプには、後で紹介するRWBufferおよびRWStructuredBufferが含まれます。

 

RWTexture2D – Win32 apps

 

numthreads

そして、次の一行(とても重要!!!!):

[numthreads(8,8,1)]

numであったり、threadであったり、きっとスレッドの数に関連しているでしょう。そうです、スレッドグループ(Thread Group)で実行できるスレッドの総数(Thread)を定義しています。形式は次のとおりです。

numthreads(tX、tY、tZ)

注:後続のグループのX、Y、Zと区別できるために、X、Y、Zの前にtを追加しました。

 

tXtYtZの値はスレッドの総数です。たとえば、numthreads(4、4、1)とnumthreads(16、1、1)はどちらも16スレッドを表します。では、なぜnumthreads(num)という形の定義を直接的に使用しなく、わざわざtX、tY、tZなどの3次元形式に分割するでしょう。後ろを見た後、この謎を自然に解けるでしょう。

 

各カーネル関数の前に、numthreadsを定義する必要があります。そうしないと、コンパイルでエラーが報告されます。

 

その中で、tX、tY、tZの3つの値をランダムに入力することはできません。たとえば、tX=99999で「会心」の一撃をするのは不可能です。異なるバージョンでは、次の制約があります。

Direct11では、ID3D11DeviceContext :: Dispatch(gX、gY、gZ)メソッドを使用してgXgYgZスレッドグループを作成できます。スレッドグループには複数のスレッドが含まれます(数はnumthreadsで定義されます)。

 

順序に注意してください。まず、numthreadsを使用して各カーネル関数(tXtYtZ)に対応するスレッドグループ内のスレッド数を定義し、次にDispatchを使用してこのカーネル関数の処理に使用されるスレッドグループ(gXgYgZ)の数を定義します。各スレッドグループのスレッドは並列であり、異なるスレッドグループのスレッドは同時に実行される場合と実行されない場合があります。一般に、GPUによって同時に実行されるスレッドの数は1000〜10000の間です。

 

次に、概略図を使用して、以下に示すように、スレッドとスレッドグループの構造を確認します。

上部はスレッドグループ構造を表し、下部はシングルスレッドグループのスレッド構造を表します。それらはすべて(X、Y、Z)で定義されているため、3次元配列のように、添え字は0から始まります。それらはテーブルとして考えられます。それぞれがX列とY行を持つZ個の同一のテーブルがあります。たとえば、スレッドグループの(2,1,0)は、最初のテーブルの2行目と3列目に対応するスレッドグループであり、下半分のスレッドについても同じです。

 

構造を明確にすることで、シングルスレッドに関連する次のパラメータの意味をよく理解できます。

ここで、グループであろうとスレッドであろうと、それらの順序は最初にX、次にY、最後にZであることに注意してください。テーブルを参照したら、最初に行(X)、次に列(Y)、次に次のテーブル(Z)という順に理解できます。たとえば、tX = 5、tY = 6だとしたら、1番目のスレッドではSV_GroupThreadID =(0,0,0)、2番目のスレッドではSV_GroupThreadID =(1,0,0)、6番目のスレッドの場合はSV_GroupThreadID =( 0,1,0)、30番目のSV_GroupThreadID =(4,5,0)、31番目のSV_GroupThreadID =(0,0,1)のようになりました。グループは同じですが、順序を理解すると、SV_GroupIndexの計算式がわかりやすくなります。

 

もう一つの例を挙げると、SV_GroupIDが(0,0,0)と(1,0,0)の2つのグループの場合、その中の最初のスレッドのSV_GroupThreadIDは両方とも(0,0,0)であり、SV_GroupIndexは0です。ただし、前者のSV_DispatchThreadID =(0,0,0)と後者のSV_DispatchThreadID =(tX、0,0)です。

 

それらはカーネル関数において非常に重要です。よく理解してください。

numthreads – Win32 apps

 

カーネル関数

void CSMain (uint3 id : SV_DispatchThreadID)
{
    Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);
}

最後は宣言したカーネル関数です。中には、パラメータSV_DispatchThreadIDの意味はこの前紹介しました。このパラメータを除い、前述のパラメータはすべてカーネル関数に渡すことができ、実際のニーズに応じて選択できます。完全なコードは次のように示します。

void KernelFunction(uint3 groupId : SV_GroupID,
    uint3 groupThreadId : SV_GroupThreadID,
    uint3 dispatchThreadId : SV_DispatchThreadID,
    uint groupIndex : SV_GroupIndex)
{

}

この関数で実行されるコードは、テクスチャで添え字がid.xyであるピクセルに色を割り当てることです。これは最適なところと言えます。

 

たとえば、以前は、x * yの解像度でテクスチャの各ピクセルに値を割り当てたいと考えていました。シングルスレッドの場合、コードは次のようになります。

for (int i = 0; i < x; i++)
    for (int j = 0; j < y; j++)
        Result[uint2(x, y)] = float4(a, b, c, d);

2つのループ、ピクセルは1つずつゆっくりと割り当てられます。したがって、フレームごとに多数の2048 * 2048の画像を与えるのは、スタックする見当が付きます。

 

マルチスレッドを使用する場合、異なるスレッドが同じピクセルを操作することを避けるために、次のようにセグメント化された操作の方法を使用することがよくあります。以下のように、4つのスレッドに対し、処理を行います。

void Thread1()
{
    for (int i = 0; i < x/4; i++)
        for (int j = 0; j < y/4; j++)
            Result[uint2(x, y)] = float4(a, b, c, d);
}

void Thread2()
{
    for (int i = x/4; i < x/2; i++)
        for (int j = y/4; j < y/2; j++)
            Result[uint2(x, y)] = float4(a, b, c, d);
}

void Thread3()
{
    for (int i = x/2; i < x/4*3; i++)
        for (int j = x/2; j < y/4*3; j++)
            Result[uint2(x, y)] = float4(a, b, c, d);
}

void Thread4()
{
    for (int i = x/4*3; i < x; i++)
        for (int j = y/4*3; j < y; j++)
            Result[uint2(x, y)] = float4(a, b, c, d);
}

このように書くのはばかげているのではないでしょうか。より多くのスレッドがあったら、より多くのセグメントに分割されたら、重複的なコードになったのではないか。しかし、各スレッドの開始と終了の添え字を知ることができれば、次のようにこれらのコードを統合することはできるでしょう。

void Thread(int start, int end)
{
    for (int i = start; i < end; i++)
        for (int j = start; j < end; j++)
            Result[uint2(x, y)] = float4(a, b, c, d);
}

では、たくさんのスレッドを開くことができたら、1つのスレッドで1つのピクセルを処理できるでしょう?

void Thread(int x, int y)
{
    Result[uint2(x, y)] = float4(a, b, c, d);
}

CPUではこれを行うことはできませんが、GPUではComputeShaderで行うことができます。実際、上記のデフォルトのCompute Shaderのコードでは、カーネル関数の内容は確かにそうです。

 

次に、Compute Shaderの凄いところを見てみましょう。id.xyの値を確認したら、idのタイプはSV_DispatchThreadIDです。最初にSV_DispatchThreadIDの計算式を思い出してみましょう。

スレッドのSV_GroupID=(a、b、c)、SV_GroupThreadID =(i、j、k)だとしたら、SV_DispatchThreadID =(atX + i、btY + j、c * tZ + k)のようになりました。

 

まず、この前[numthreads(8,8,1)]を使用しました。つまりtX = 8、tY = 8、tZ = 1を使用し、しかもiとjの値の範囲は0〜7、k=0です。したがって、スレッドグループ(0,0,0)内のすべてのスレッドのSV_DispatchThreadID.xyの値の範囲、つまりid.xyの値の範囲は(0,0)から(7,7)であり、スレッドグループ(1,0,0)でその値の範囲は(8,0)から(15、7)、…であり、スレッド(0,1,0)の値の範囲は(0,8)から(7,15)、..となります、スレッドグループ(a、b、0)での値の範囲は、(a8、b8、0)から(a8 + 7、b8 + 7,0)です。

 

次の画像の各メッシュに64ピクセルが含まれていると仮定して、概略図で見てみましょう。

つまり、各スレッドグループには64スレッドが64ピクセルを同期的に処理し、異なるスレッドグループのスレッドは同じピクセルを繰り返し処理しません。1024* 1024の解像度の画像を処理するには、( 1024 / 8、1024 / 8、1)個のスレッドグループをdispatchするだけで済みます。

 

このようにして、数百または数千のスレッドが同時に同じピクセルを処理することはできるようになります。これは、CPU方式では不可能です。素晴らしいですね。

 

そして、numthreadsに設定された値は、推敲する価値があると気付きます。たとえば、処理する4 * 4マトリックスがあり、numthreads(4,4,1)を設定すると、各スレッドのSV_GroupThreadID.xyの値はマトリックスの各項目の添え字に対応しているのではないでしょうか?

 

Unityでどのようにカーネル関数を呼び出すか、またどのようにスレッドグループをディスパッチするか、また使用されているRWTextureはどこからのか。これについては、C#の部分に戻ります。

 

C#

 

従来のvertex&fragment shaderはMaterialに関連付けられていましたが、Compute Shaderはそれと異なり、C#によって駆動されます。

 

最初に新しいmonobehaviourスクリプトを作成します。Unityは、この前に生成された.computeファイルを参照するためのComputeShaderタイプを提供します。

public ComputeShader computeShader;

Inspectorインターフェイスで.computeファイルを関連付ける

他に、もう一つのマテリアルを関連付ける必要があります。これは、Compute Shaderによって処理されたテクスチャは、依然としてFragment Shaderによってサンプリングされてから表示できるからです。

このマテリアルでは、Unlit Shaderを使用しており、かつテクスチャを設定する必要はありません。次のように示します。

次に、それをスクリプトに関連付けします。同時にこのマテリアルに関連付けられているキューブをも作成します。

 

次に、UnityのRenderTextureをCompute ShaderのRWTexture2Dに割り当てることができますが、複数のスレッドでピクセルを処理したため、しかもこの処理プロセスが順序がないため、RenderTextureのenableRandomWriteプロパティをtrueに設定する必要があることに注意してください。コードは次のとおりです。

RenderTexture mRenderTexture = new RenderTexture(256, 256, 16);
mRenderTexture.enableRandomWrite = true;
mRenderTexture.Create();

解像度が256 * 256のRenderTextureを作成しました。まず、Cubeに表示されることができるように、これをMaterialに割り当てる必要があります。そして、それをComputeShaderのResult変数に割り当てます。コードは次のとおりです。

material.mainTexture = mRenderTexture;
computeShader.SetTexture(kernelIndex, "Result", mRenderTexture);

ここにはカーネル関数の添え字であるkernelIndex変数があります。FindKernelを使用して、宣言したカーネル関数の添え字を見つけることができます。

int kernelIndex = computeShader.FindKernel("CSMain");

このように、FragmentShaderサンプリングすると、サンプリングされたのはComputeShaderによって処理されたテクスチャです。

fixed4 frag (v2f i) : SV_Target
{
    // _MainTex は処理された後のRenderTexture
    fixed4 col = tex2D(_MainTex, i.uv);
    return col;
}

最後は、スレッドグループを開き、カーネル関数を呼び出すことです。ComputeShaderでは、Dispatchメソッドが1つのステップで実行されます。

computeShader.Dispatch(kernelIndex, 256 / 8, 256 / 8, 1);

なぜ256/8であるかはこの前説明しました。効果を見ましょう。

上の図は、デフォルトでUnityによって生成されたCompute Shaderコードの効果を示しています。これを使用して、2048*2048テクスチャを処理することもできます。これも非常に高速です。

 

次に、パーティクル効果の例を見てみましょう。

 

まず、パーティクルには通常、色と位置の2つの属性があり、Compute Shaderでこれら2つの属性を処理する必要があります。次に、ComputeShaderでstructを作成して格納するのができます。

struct ParticleData {
float3 pos;
float4 color;
};

次に、このパーティクルはきっと多数であるが、それらを格納するためのリストのようなものが必要となります。RWStructuredBufferタイプがComputeShaderで提供されます。

 

RWStructuredBuffer

 

これは読み取りと書き込みが可能なバッファであり、バッファ内のデータ型をカスタムのstructとして指定できます。intやfloatなどの基本型に限定されなくなりました。

 

したがって、パーティクルデータを次のように定義できます。

RWStructuredBuffer<ParticleData> ParticleBuffer;

RWStructuredBuffer – Win32 apps

 

アニメーションを作成するため、別の時間関連値を追加し、時間に応じてパーティクルの位置と色を変更できます。

float Time;

次は、カーネル関数でパーティクル情報を変更することになります。パーティクルを変更するには、バッファ内のパーティクルの添え字を知っている必要があります。さらにこの添え字を別のスレッドで繰り返すことはできません。そうしないと、複数のスレッドが同じ粒子を変更するようになってしまったかもしれません。

 

前の紹介によると、SV_GroupIndexはスレッドグループ内で唯一であることがわかっていますが、異なるスレッドグループ内ではそうではありません。たとえば、各スレッドグループに1000のスレッドがある場合、SV_GroupIDは0〜999です。 SV_GroupIDに従って重ね合わせることができます。たとえば、SV_GroupID =(0,0,0)は0〜999、SV_GroupID =(1,0,0)は1000〜1999などです。便宜上、スレッドグループは( X、1,1)形式。次に、時間とインデックスによってパーティクルを任意に配置できます。ComputeShaderの完全なコードは次のとおりです。

#pragma kernel UpdateParticle

struct ParticleData {
float3 pos;
float4 color;
};

RWStructuredBuffer<ParticleData> ParticleBuffer;

float Time;

[numthreads(10, 10, 10)]
void UpdateParticle(uint3 gid : SV_GroupID, uint index : SV_GroupIndex)
{
int pindex = gid.x * 1000 + index;

float x = sin(index);
float y = sin(index * 1.2f);
float3 forward = float3(x, y, -sqrt(1 - x * x - y * y));
ParticleBuffer[pindex].color = float4(forward.x, forward.y, cos(index) * 0.5f + 0.5, 1);
if (Time > gid.x)
ParticleBuffer[pindex].pos += forward * 0.005f;
}

次に、C#でパーティクルを初期化し、それをComputeShaderに渡す必要があります。パーティクルデータを渡すのは、以前のRWStructuredBufferに値を割り当てる必要があります。Unityは、RWStructuredBufferまたはStructuredBufferに対応するComputeBufferクラスを提供します。

ComputeBuffer

 

Compute Shaderでは、カスタムStructデータの一部をメモリバッファーに読み書きする必要があることがよくあります。ComputeBufferはこの状況のために生まれました。これをC#で作成してパデング(padding)してから、ComputeShaderまたは他のShaderに渡して使用できます。

 

通常、次の方法で作成します。

ComputeBuffer buffer = new ComputeBuffer(int count, int stride)

ここで、countはバッファ内の要素の数を表し、strideは各要素が占めるスペース(バイト)を表します。たとえば、10個のfloatタイプを渡すと、count = 10、stride=4になります。 ComputeBufferのストライドサイズは、RWStructuredBufferの各要素のサイズと同じである必要があることに注意してください。

 

宣言が完了したら、SetDataメソッドを使用してパデングできます。パラメーターは、カスタムのstruct配列です。

buffer.SetData(T[]);

最後に、Compute ShaderクラスのSetBufferメソッドを使用して、ComputeShaderに渡すことができます。

public void SetBuffer(int kernelIndex, string name, ComputeBuffer buffer)

使用後は必ずRelease()してください。

https://docs.unity3d.com/ScriptReference/ComputeBuffer.html

 

C#では同じサイズのstructを定義します。このようにすると、ComputeShaderでのと同じサイズを確保することができます。

public struct ParticleData
{
    public Vector3 pos;//float3と同様
    public Color color;//float4と同様
}

次に、StartメソッドでComputeBufferを宣言し、カーネル関数を見つけます。

void Start()
{
    //structには合計7個のfloatがあり、size=28
    mParticleDataBuffer = new ComputeBuffer(mParticleCount, 28);
    ParticleData[] particleDatas = new ParticleData[mParticleCount];
    mParticleDataBuffer.SetData(particleDatas);
    kernelId = computeShader.FindKernel("UpdateParticle");
}

パーティクルを動かしたいので、つまり、フレームごとにパーティクルの情報を変更します。したがって、UpdateメソッドでBufferとDispatchを渡します。

void Update()
{
    computeShader.SetBuffer(kernelId, "ParticleBuffer", mParticleDataBuffer);
    computeShader.SetFloat("Time", Time.time);
    computeShader.Dispatch(kernelId,mParticleCount/1000,1,1);
}

ここまでパーティクルの位置と色の操作は完了しましたが、これらのデータをUnityで表示することはできません。Vertex&FragmentShaderの助けも必要です。新しいUnlitShaderを作成し、内部のコードを次のように変更します。

Shader "Unlit/ParticleShader"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct v2f
            {
                float4 col : COLOR0;
                float4 vertex : SV_POSITION;
            };

            struct particleData
            {
float3 pos;
float4 color;
            };

            StructuredBuffer<particleData> _particleDataBuffer;

            v2f vert (uint id : SV_VertexID)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(float4(_particleDataBuffer[id].pos, 0));
                o.col = _particleDataBuffer[id].color;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return i.col;
            }
            ENDCG
        }
    }
}

この前、ComputeBufferも通常のシェーダーに渡されると述べたので、シェーダーで構造が同じStructを作成し、そしてStructuredBufferを使用してそれを受け取ります。

 

SV_VertexID:VertexShaderで渡されるパラメーターとして使用され、頂点の添え字を表します。頂点の数はパーティクルの数と同じです。頂点データは、ComputeShaderで処理したバッファーを使用します。

 

最後に、C#で上記のシェーダーが付けられたマテルアルに関連を付け、パーティクルデータを渡して、最後に描画します。完全なコードは次のとおりです。

public class ParticleEffect : MonoBehaviour
{
    public ComputeShader computeShader;
    public Material material;

    ComputeBuffer mParticleDataBuffer;
    const int mParticleCount = 20000;
    int kernelId;

    struct ParticleData
    {
        public Vector3 pos;
        public Color color;
    }

    void Start()
    {
        //structには合計7個のfloatがあり、size=28
        mParticleDataBuffer = new ComputeBuffer(mParticleCount, 28);
        ParticleData[] particleDatas = new ParticleData[mParticleCount];
        mParticleDataBuffer.SetData(particleDatas);
        kernelId = computeShader.FindKernel("UpdateParticle");
    }

    void Update()
    {
        computeShader.SetBuffer(kernelId, "ParticleBuffer", mParticleDataBuffer);
        computeShader.SetFloat("Time", Time.time);
        computeShader.Dispatch(kernelId,mParticleCount/1000,1,1);
        material.SetBuffer("_particleDataBuffer", mParticleDataBuffer);
    }

    void OnRenderObject()
    {
        material.SetPass(0);
        Graphics.DrawProceduralNow(MeshTopology.Points, mParticleCount);
    }

    void OnDestroy()
    {
        mParticleDataBuffer.Release();
        mParticleDataBuffer = null;
    }
}

Material.SetBuffer:ComputeBufferをシェーダーに渡します。

 

OnRenderObject:このメソッドでは、描画ジオメトリをカスタマイズできます。

 

DrawProceduralNow:このメソッドを使用してジオメトリを描画できます。最初のパラメーターはトポロジで、2番目のパラメーターは頂点の数です。

 

https://docs.unity3d.com/ScriptReference/Graphics.DrawProceduralNow.html

 

最終結果は次のとおりです。

 

デモリンクは次のとおりです。

https://github.com/luckyWjr/ComputeShaderDemo/tree/master/Assets/Particle

 

ComputeBufferType

 

この例では、新しいComputeBufferをnewした時に、ComputeBufferTypeのパラメーターは使用されず、ComputeBufferType.Defaultがデフォルトで使用されます。実際、ComputeBufferは、さまざまなシナリオで使用されるHLSLのさまざまなバッファーに対応するさまざまなタイプを持つことができます。合計で次のタイプがあります。

たとえば、GPUカリングを行う場合、Append Bufferがよく使用されます(たとえば、後で説明するCompute Shaderを使用してビュー錐台カリングを実装します)。C#での宣言は次のとおりです。

var buffer = new ComputeBuffer(count, sizeof(float), ComputeBufferType.Append);

注:Default,Append,Counter,Structuredに対応するのはバッファーの各要素のサイズです。つまり、strideの値は4の倍数で2048未満である必要があります。

 

上記のComputeBufferは、Compute ShaderのAppendStructuredBufferに対応できます。次に、Compute ShaderのAppendメソッドを使用して、次のように要素をバッファに追加できます。

AppendStructuredBuffer<float> result;

[numthreads(640, 1, 1)]
void ViewPortCulling(uint3 id : SV_DispatchThreadID)
{
    if(いくつかのカスタム条件が満たされている)
        result.Append(value);
}

では、バッファにはいくつの要素がありますか?カウンターは、この結果を得るのに役立ちます。

 

C#では、最初にComputeBuffer.SetCounterValueメソッドを使用して、カウンターの値を初期化できます。次に例を示します。

buffer.SetCounterValue(0);//カウンター値が0である

AppendStructuredBuffer.Appendメソッドを使用すると、カウンターの値は自動的に++になります。 Compute Shaderの処理が完了すると、次のようにComputeBuffer.CopyCountメソッドを使用してカウンターの値を取得できます。

public static void CopyCount(ComputeBuffer src, ComputeBuffer dst, int dstOffsetBytes);

Append、Consume、またはCounterのバッファは、バッファ内の要素の数を格納するためのカウンタを維持します。このメソッドは、srcのカウンタの値をdstにコピーでき、dstOffsetBytesはdstのオフセットです。 DX11プラットフォームでは、dstのタイプはRawまたはIndirectArgumentsである必要がありますが、他のプラットフォームでは任意のタイプにすることができます。

 

したがって、バッファ内の要素数を取得するコードは次のとおりです。

uint[] countBufferData = new uint[1] { 0 };
var countBuffer = new ComputeBuffer(1, sizeof(uint), ComputeBufferType.IndirectArguments);
ComputeBuffer.CopyCount(buffer, countBuffer, 0);
countBuffer.GetData(countBufferData);
//bufferにおける要素の数は:countBufferData[0]

上記の2つの最も基本的な例から、Compute ShaderのデータはC#から渡されることがわかります。つまり、データはCPUからGPUに渡される必要があります。そして、Compute Shader処理が終了すると、GPUからCPUに送り返されます。これは少し遅れる可能性があり、それらの間の転送速度もボトルネックになります。

 

ただし、コンピューティングのニーズが多い場合は、パフォーマンスを大幅に向上させることができるComputeShaderを使用することを躊躇しないでください。

 

UAV(Unordered Access view)

 

Unorderedは順序なしを意味し、Accessはアクセスを意味し、viewは「data in the required format」を表します。これはデータに必要な形式として理解されるべきです。

 

どういう意味ですか? Compute Shaderはマルチスレッドで並列であるため、データは順序なしのアクセスをサポートできる必要があります。たとえば、テクスチャに(0,0)、(1,0)、(2,0)、…でのみアクセスできる場合、バッファには[0]、[1]、[2]、…でのみアクセスできます。では、マルチスレッドでそれらを変更することは明らかに不可能であるため、新しい概念__UAV(すなわち順序なしでアクセスできるデータの形式)が提案されています。

 

RWTextureとRWStructuredBufferはすべてUAVデータ型であり、読み取り中の書き込みをサポートしていることは前述しました。これらは、FragmentShaderおよびComputeShaderでのみ使用(バインド)できます。

 

RenderTextureがenableRandomWriteを設定しない場合、またはテクスチャをRWTextureに渡す場合、ランタイムはエラーを報告します。

the texture wasn’t created with the UAV usage flag set!

Texure2Dなど、読み取りと書き込みができないデータ型をSRV(Shader Resource View)と呼びます。)。

 

Direct3D12用語集-Win32アプリ

 

Wrap / WaveFront

先ほど、numthreadsを使用すると各スレッドグループのスレッド数を定義できると述べましたが、numthreads(1,1,1)を使用して、スレッドグループごとに本当にスレッドを1つだけ持つのでしょうか。いいえ!

 

この問題はハードウェアから始まります。GPUのモードはSIMT(single-instruction multiple-thread、単一命令マルチスレッド)です。 NVIDIAグラフィックカードでは、1つのSM(Streaming Multiprocessor)で複数のラップをスケジュールでき、各ラップには32のスレッドがあります。命令が少なくとも32の並列スレッドをスケジュールすることを簡単に理解できます。 AMDのグラフィックカードでは、この数は64となり、Wavefrontと呼ばれます。

 

つまり、NVIDIAグラフィックカードの場合、numthreads(1,1,1)を使用すると、スレッドグループには32のスレッドが残りますが、余分な31のスレッドは完全に使用されないため、無駄になります。したがって、numthreadsを使用する場合は、両方のグラフィックカードを考慮できるように、スレッドグループの数を64の倍数として定義するのが最適です。

 

https://www.cvg.ethz.ch/teaching/2011spring/gpgpu/GPU-Optimization.pdf

 

モバイル端末のサポート問題

 

実行時にSystemInfo.supportsComputeShadersを呼び出して、現在のモデルがComputeShaderをサポートしているかどうかを判断できます。OpenGL ESはバージョン3.1から、Compute Shaderをサポートし始まったが、Vulkanを使用するAndroidプラットフォームとMetalを使用するIOSプラットフォームの両方がComputeShaderをサポートしています。

 

ただし、一部のAndroidフォンがCompute Shaderをサポートしても、RWStructuredBufferのサポートはフレンドリーではありません。たとえば、一部のOpenGL ES 3.1携帯電話では、フラグメントシェーダーのStructuredBufferへのアクセスのみがサポートされています。

 

通常のシェーダーでComputeShaderをサポートするには、シェーダーモデルの最小要件は4.5です。つまり、次のようになります。

#pragma target 4.5

ComputeShaderを使用してビュー錐台カリングを実装する

「UnityでComputeShaderを使用してビュー錐台カリングを実装する」(中国語注意)

 

ComputeShaderを使用したHi-zオクルージョンカリングを実装する

「UnityでComputeShaderを使用してHi-zオクルージョンカリングを実装する」(中国語注意)

 

Shader.PropertyToID

 

Compute Shaderで定義された変数は相変わらずShader.PropertyToID( “name”)によって唯一のIDを取得できます。このように、ComputeShader.SetBufferを頻繁に使用して同じ変数のいくつかに値を割り当てる場合、GCの発生を回避するために、これらのIDを事前にキャッシュできます。

int grassMatrixBufferId;
void Start() {
    grassMatrixBufferId = Shader.PropertyToID("grassMatrixBuffer");
}
void Update() {
    compute.SetBuffer(kernel, grassMatrixBufferId, grassMatrixBuffer);

    // dont use it
    //compute.SetBuffer(kernel, "grassMatrixBuffer", grassMatrixBuffer);
}

グローバル変数または定数?

 

一つの要件を実装し、ComputeShaderで頂点が固定サイズのバウンディングボックスにあるかどうかを判断するには、前のC#書き込み方法に従って、バウンディングボックスのサイズを次のように定義できます。

#pragma kernel CSMain

float3 boxSize1 = float3(1.0f, 1.0f, 1.0f); // 方法1
const float3 boxSize2 = float3(2.0f, 2.0f, 2.0f); // 方法2
static float3 boxSize3 = float3(3.0f, 3.0f, 3.0f); // 方法3

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    // 判断を実行する
}

テスト後、メソッド1とメソッド2の定義、CSMainで読み取られる値は両方ともfloat3(0.0f、0.0f、0.0f)であり、メソッド3のみが最初に定義された値です。

 

Shader variants and keywords

ComputeShaderはShaderバリアントもサポートしており、その使用法は基本的に通常のShaderバリアントと同様です。例は次のとおりです。

#pragma kernel CSMain
#pragma multi_compile __ COLOR_WHITE COLOR_BLACK

RWTexture2D<float4> Result;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
#if defined(COLOR_WHITE)
Result[id.xy] = float4(1.0, 1.0, 1.0, 1.0);
#elif defined(COLOR_BLACK)
Result[id.xy] = float4(0.0, 0.0, 0.0, 1.0);
#else
Result[id.xy] = float4(id.x & id.y, (id.x & 15) / 15.0, (id.y & 15) / 15.0, 0.0);
#endif
}

次に、C#側でバリアントを有効または無効にできます。

  • #pragma multi_compileによって宣言されたグローバルバリアントは、Shader.EnableKeyword/Shader.DisableKeywordまたはComputeShader.EnableKeyword/ComputeShader.DisableKeywordを使用できます。
  • #pragma multi_compile_local宣言のローカルバリアントは、ComputeShader.EnableKeyword/ComputeShader.DisableKeywordを使用できます。

例は次のとおりです。

public class DrawParticle : MonoBehaviour
{
    public ComputeShader computeShader;

    void Start() {
        ......
        computeShader.EnableKeyword("COLOR_WHITE");
    }
}

UWA公式サイト:https://jp.uwa4d.com

UWA公式ブログ:https://blog.jp.uwa4d.com

UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com