Continue(s)

Twitter:@dn0t_ GitHub:@ogrew

UnityのCompute Shaderに入門する。その1

はじめに

なにはなくともCompute Shaderに挑戦してみよう!の回です。

いくつか文献をあたって学んだこと、そして実践したことなどをまとめています。

複数回にわたる予定です。その1回です。

(いきなり余談ですが、Compute Shaderに関する書籍やサイトってまだまだ少ないですね...)

Compute Shader あるいは GPGPU

Compute Shader というのは、数あるシェーダの中でGPU数値計算をさせることを目的としたシェーダの1種です。特にGPUはもともとが描画目的に作られた仕組みなので単純な計算を複数回(それも膨大な数)繰り返すのに適していることが大きな特徴です。

www.youtube.com (この手の話題になるとみんなが紹介する動画です。)

Compute Shaderを上手く使うことでGPUとCPUのいいとこ取りをしていくことができます。

本来の描画目的ではない用途でGPUに計算処理をさせることを俗にGPGPU(General Purpose computing ON GPU)と呼びます。

カーネル、スレッド、グループ

Compute Shaderを使っていく上で避けて通れない用語があるので簡単に説明をします。

カーネルとは「GPUでの1つの計算処理」のことを指し、スレッドは、その「カーネルを実行する単位」を意味します。グループは、さらにそれら「スレッドを実行する単位」のことです。

カーネルには様々なセマンティクスが用意されており、それを使って例えばグループのID(SV_GroupID)、現在実行中のスレッド内の一意のID(SV_DispatchThreadID)だったりを参照するすることが出来ます。

f:id:taiga006:20201023020028p:plain ( Unity : ComputeShader のシンプルなサンプル(1) - NEAREAL より画像転載 )

スクリプト

まずは仕組みを理解するために、Compute Shaderを使って地味な数値計算をさせてみます。

最初にシェーダ側の実装です。足し算と引き算をするだけの2つの関数(=カーネル)を用意しています。

// カーネルの定義
#pragma kernel CPlus
#pragma kernel CMinus

// 実行結果を保持するバッファ
RWStructuredBuffer<int> intBuffer;

// カーネルを実行するスレッド数の指定
[numthreads(3, 1, 1)]
void CPlus (uint3 id : SV_GroupThreadID)
{
    intBuffer[id.x] += 1;
}

[numthreads(3, 1, 1)]
void CMinus (uint3 id : SV_GroupThreadID)
{
    intBuffer[id.x] -= 1;
}

次にこれらをCPU側から操作するためのC#スクリプトです。

using UnityEngine;

public class TryComputeShader : MonoBehaviour
{
    public ComputeShader Shader;
    public int num;
    private int _kernelIndex_plus;
    private int _kernelIndex_minus;
    private ComputeBuffer _Buffer;

    private int[] _resultArr;

    private void Start()
    {
        // カーネルには識別するため固有のインデックスが与えられる
        _kernelIndex_plus = Shader.FindKernel("CPlus");
        _kernelIndex_minus = Shader.FindKernel("CMinus");
        // int 型でsize:3 のバッファを用意する
        _Buffer = new ComputeBuffer(num, sizeof(int));
        // 結果を取得するための配列を用意する
        _resultArr = new int[num];
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.UpArrow)) {
            // シェーダ側の実行結果を保持するバッファを紐付ける
            Shader.SetBuffer(_kernelIndex_plus, "intBuffer", _Buffer);
            // グループ数 1*1*1 でカーネル "CPlus" を実行する
            Shader.Dispatch(_kernelIndex_plus, 1, 1, 1);
            // 実行結果をバッファから取得する
            _Buffer.GetData(_resultArr);

            Debug.Log("--- PLUS  ---");
            for (int i = 0; i < _resultArr.Length; i++) {
                Debug.Log(_resultArr[i]);
            }
        } else if (Input.GetKeyDown(KeyCode.DownArrow)) {
            Shader.SetBuffer(_kernelIndex_minus, "intBuffer", _Buffer);
            Shader.Dispatch(_kernelIndex_minus, 1, 1, 1);
            _Buffer.GetData(_resultArr);

            Debug.Log("--- MINUS ---");
            for (int i = 0; i < _resultArr.Length; i++) {
                Debug.Log(_resultArr[i]);
            }
        }
    }

    private void OnDisable()
    {
        _Buffer.Release();
    }
}

上下の矢印ボタンで計算します。

実行結果

f:id:taiga006:20201023015057p:plain

よくわからんこと

OnDisable を使って使用しているバッファの開放はしているつもりなんですが、どういうロジックかたまにバッファがクリアされていないような挙動を示します。 上のコードで何処か間違っているのでしょうか?ちょっと謎。

まとめ

ひとまず大枠となる仕組みは理解できましたが、やはりCompute Shaderといえば複雑な画像処理やシミュレーションなどが醍醐味だと思うので、次回はそのあたり挑戦してみたいと思います。

参考

docs.unity3d.com

edom18.hateblo.jp

blog.yucchiy.com

github.com