【Unity】ComputeShaderの基本的な使い方についてまとめる

Unity
マイケル
マイケル
みなさんこんにちは!
マイケルです!
エレキベア
エレキベア
こんにちクマ〜〜
マイケル
マイケル
今回は、UnityでのComputeShaderの使い方についてまとめていきます!
エレキベア
エレキベア
シェーダーなのクマか??
マイケル
マイケル
名前はシェーダーとなっているけど、実際は 描画処理以外の汎用的な計算をGPUに行わせるもの になるんだ。
GPUは大量の簡単な計算を行うのが得意なため、そこを任せることによって効率化することができるというわけさ!
エレキベア
エレキベア
負荷が大きい処理をGPUに任せ流ことができるのクマね
マイケル
マイケル
上手く使えば重い処理も効率よく行うことができるから、是非覚えてみよう!
なお、今回使用したUnityのバージョンは下記になります。
Unity2021.3.1f1
エレキベア
エレキベア
楽しみクマ〜〜〜
スポンサーリンク

ComputeShaderの概念

マイケル
マイケル
改めてComputeShaderとは何なのかですが、
描画処理以外の汎用的な計算をGPUに行わせるもの になります。
こちらはGPGPUという概念と同じで、実現するための方法の一つです。
エレキベア
エレキベア
GPGPUはグラフィックス技術の書籍にも書いてあったクマね〜〜

↑書籍の紹介記事

マイケル
マイケル
GPUを使うと汎用的な計算に使うと何がいいのかについては、まずCPUとGPUの違いを知る必要があります。
CPUとGPUではそれぞれ得意分野が異なっていて、CPUが高速でコアが少ないのに対してGPUは低速でコアが多いのが特徴です。
エレキベア
エレキベア
GPUのコアは低速なのクマね・・・
それなのにGPUに任せちゃっていいのクマ??
マイケル
マイケル
よく例えられるのがCPUは1人のスーパーマンで、GPUは大量の凡人、といったものがあるよ。
数が多いから簡単な計算を何回も行う必要がある場合等に有効だね!
逆に数が少なかったり複雑な処理だと返って逆効果になる場合もあるからそこは注意が必要だ!
ScreenShot 2023 01 06 12 49 07
エレキベア
エレキベア
アザラッシ・・・
マイケル
マイケル
ComputeShaderはHLSLで記述できて、
下記のような単位で処理がまとめられています。
用語概念
カーネルGPUで実行される1つの処理
スレッドカーネルを実行する単位
グループスレッドを実行する単位
ScreenShot 2023 01 06 2 08 30
エレキベア
エレキベア
これは使いながら覚えるしかなさそうクマね
マイケル
マイケル
実際に触りながら覚えていこう!

GPUで簡単な計算を行う

マイケル
マイケル
それでは早速簡単な計算を行ってみましょう!
今回は Shader > Compute Shader から HeavyProcess.compute という名前で作成します。
ScreenShot 2023 01 07 0 25 45
マイケル
マイケル
こちらに 簡単な計算処理を実装した例 が下記になります。
#pragma kernel HeavyProcess

RWStructuredBuffer<int> intBuffer;
int intValue;

[numthreads(64,1,1)]
void HeavyProcess(uint3 groupID : SV_GroupID, uint3 groupThreadID : SV_GroupThreadID)
{
    intBuffer[groupThreadID.x] = 0;
    for (int i = 0; i < 100; i++)
    {
        intBuffer[groupThreadID.x] += intValue;
    }
}
エレキベア
エレキベア
これは確かHLSLで記述されているのだったクマね
マイケル
マイケル
「#pragma kernel」でカーネルを指定して、「numthreads」でスレッド数を指定します。
スレッドは(X,Y,Z)の形式で指定して参照することで使用します。
今回の場合はXに64を指定しているので、各スレッドで「groupThreadID.x」に0〜63の数値が入ってくる形になります。
マイケル
マイケル
引数の「SV_GroupThreadID」にはスレッドのIDが渡されてきます。
「SV_GroupID」は、グループのIDが渡されますが、こちらの数は後述のC#側で指定します。
エレキベア
エレキベア
いろいろ覚えることはありそうクマが、まあさっきの図の話クマね
ScreenShot 2023 01 06 2 08 30
マイケル
マイケル
このComputeShaderをC#から呼び出す方法は下記になります。
using UnityEngine;

public class HeavyProcess : MonoBehaviour
{
    [SerializeField] private ComputeShader computeShader;
    private int _kernelHeavyProcess;
    private ComputeBuffer _intComputeBuffer;

    private void Start()
    {
        // カーネル取得
        _kernelHeavyProcess = computeShader.FindKernel("HeavyProcess");

        // Buffer領域を確保
        _intComputeBuffer = new ComputeBuffer(64, sizeof(int));

        // パラメータの設定
        computeShader.SetInt("intValue", 1);
        computeShader.SetBuffer(_kernelHeavyProcess, "intBuffer", _intComputeBuffer);
    }

    private void Update()
    {
        // GroupIDを指定して数回実行
        // (64,1,1)スレッド * (1,1,1)グループ
        computeShader.Dispatch(_kernelHeavyProcess, 1, 1, 1);

        // 実行結果
        var result = new int[64];
        _intComputeBuffer.GetData(result);
        Debug.Log("RESULT: HeavyProcess");
        for (var i = 0; i < 64; i++)
        {
            Debug.Log(i + ": " + result[i]);
        }
    }

    private void OnDestroy()
    {
        // バッファの解放
        _intComputeBuffer.Release();
    }
}
マイケル
マイケル
ComputeShaderをアタッチしておき、各処理を呼び出します。
ざっくりと
・「ComputeShader.FindKernel」でカーネルを取得
・「ComputeShader.SetXXX」でパラメータを設定
・「ComputeShader.Dispatch」で処理を実行
といった流れになります。
エレキベア
エレキベア
Dispatchのタイミングで処理が実行されるクマね
マイケル
マイケル
Dispatchの際に指定するのがグループ数で、例えば(2,1,1)を指定した場合は
(64,1,1) * (2,1,1) = (128,1,1) のスレッドが実行されます。
エレキベア
エレキベア
ここでグループ数を指定するクマね
マイケル
マイケル
これを実行すると、下記のように計算結果がログ出力されるはずです。
ScreenShot 2023 01 07 1 37 18
エレキベア
エレキベア
単純な計算結果クマね

演算結果をテクスチャに描画する

マイケル
マイケル
次は演算結果をテクスチャに描画する方法についてです!
こちらは下記の UnityGraphicsProgrammingSeries を参考にさせていただきました。

GitHub – UnityGraphicsProgrammingSeries

エレキベア
エレキベア
UnityGraphicsProgrammingSeriesにはお世話になってるクマ〜〜
マイケル
マイケル
こちらのComputeShader側の実装は下記のようになります。
RWTexture2Dでテクスチャバッファを宣言して、float4型で描画色を設定しています。
#pragma kernel DrawTexture

RWTexture2D<float4> textureBuffer;

// SV_DispatchThreadID: あるカーネルを実行するスレッドが全てのスレッドのどこに位置するか
// SV_Group_ID * numthreads + GroupThreadID
[numthreads(8,8,1)]
void DrawTexture(uint3 dispatchThreadID : SV_DispatchThreadID)
{
    // X方向になるほど濃くなるようにする
    float width, height;
    textureBuffer.GetDimensions(width, height);
    textureBuffer[dispatchThreadID.xy] = float4(
        dispatchThreadID.x / width,
        dispatchThreadID.x / width,
        dispatchThreadID.x / width,
        1);
}
マイケル
マイケル
引数で指定している「SV_DispatchThreadID」が少しややこしいのですが、「実行するスレッドが全てのスレッドのどこに位置するか(X,Y,Z)」を示しています。
計算式でいうと「SV_Group_ID * numthreads + GroupThreadID」になります。
エレキベア
エレキベア
座標位置が渡されるイメージクマかね
マイケル
マイケル
テクスチャは2Dのため今回はnumthreadsは(8,8,1)で、XYそれぞれ8スレッドずつで指定しています。
これをC#側から呼び出す処理は下記の通り!
using UnityEngine;

public class DrawTexture : MonoBehaviour
{
    [SerializeField] private ComputeShader computeShader;
    [SerializeField] private Renderer planeRenderer;

    private int _kernelDrawTexture;
    private RenderTexture _renderTexture;

    private struct ThreadSize
    {
        public readonly int X;
        public readonly int Y;
        public readonly int Z;
        public ThreadSize(uint x, uint y, uint z)
        {
            X = (int) x;
            Y = (int) y;
            Z = (int) z;
        }
    }
    private ThreadSize _kernelThreadSize;

    private void Start()
    {
        // RenderTextureの生成
        _renderTexture = new RenderTexture(512, 512, 0, RenderTextureFormat.ARGB32);
        _renderTexture.enableRandomWrite = true;
        _renderTexture.Create();

        // カーネル取得
        _kernelDrawTexture = computeShader.FindKernel("DrawTexture");

        // スレッドサイズの取得
        computeShader.GetKernelThreadGroupSizes(_kernelDrawTexture,
            out var threadSizeX,
            out var threadSizeY,
            out var threadSizeZ);
        _kernelThreadSize = new ThreadSize(threadSizeX, threadSizeY, threadSizeZ);

        // テクスチャの設定
        computeShader.SetTexture(_kernelDrawTexture, "textureBuffer", _renderTexture);

        // カーネルの実行
        // 水平方向のグループ数: 512 / 8 = 64
        // それぞれのスレッドで設定する範囲を分担する
        computeShader.Dispatch(
            _kernelDrawTexture,
            _renderTexture.width / _kernelThreadSize.X,
            _renderTexture.height / _kernelThreadSize.Y,
            _kernelThreadSize.Z);

        // テクスチャの設定
        planeRenderer.material.mainTexture = _renderTexture;
    }
}
マイケル
マイケル
実行の流れはそこまで変わりませんが、カーネル実行の際に指定するグループ数「幅/スレッド数」になります。
そうすることでスレッド実行ごとに描画する範囲を分担して処理することができます。
エレキベア
エレキベア
段々スレッド数とグループ数の関係が分かってきたクマね〜〜〜
マイケル
マイケル
こちらを実行すると下記のように表示されるはずです!
ScreenShot 2023 01 07 2 00 58↑右方向へのグラデーションが描画される
エレキベア
エレキベア
綺麗クマ〜〜〜

おわりに

マイケル
マイケル
というわけで今回はComputeShaderの使い方でした!
どうだったかな??
エレキベア
エレキベア
難しそうなイメージだったクマが、やってみると案外簡単だったクマね
マイケル
マイケル
今回の例以外でも、下記のライフゲームの実装サンプルなんかも処理負荷の効率化が確認できて面白かったよ!

【Unity】Compute Shaderについて勉強してみた – 株式会社ロジカルビート

ScreenShot 2023 01 07 2 10 10
エレキベア
エレキベア
どんな処理をComputeShaderに移すかが中々悩ましいクマが、
これはやりやすくてよさそうクマね
マイケル
マイケル
それでは今日はこの辺で!
アデューーー!!
エレキベア
エレキベア
クマ〜〜〜〜

【Unity】ComputeShaderの基本的な使い方についてまとめる〜完〜

コメント