
マイケル
みなさんこんにちは!
マイケルです!
マイケルです!

エレキベア
こんにちクマ〜〜

マイケル
今回は、UnityでのComputeShaderの使い方についてまとめていきます!

エレキベア
シェーダーなのクマか??

マイケル
名前はシェーダーとなっているけど、実際は 描画処理以外の汎用的な計算をGPUに行わせるもの になるんだ。
GPUは大量の簡単な計算を行うのが得意なため、そこを任せることによって効率化することができるというわけさ!
GPUは大量の簡単な計算を行うのが得意なため、そこを任せることによって効率化することができるというわけさ!

エレキベア
負荷が大きい処理をGPUに任せ流ことができるのクマね

マイケル
上手く使えば重い処理も効率よく行うことができるから、是非覚えてみよう!
なお、今回使用したUnityのバージョンは下記になります。
なお、今回使用したUnityのバージョンは下記になります。
Unity | 2021.3.1f1 |

エレキベア
楽しみクマ〜〜〜
ComputeShaderの概念

マイケル
改めてComputeShaderとは何なのかですが、
描画処理以外の汎用的な計算をGPUに行わせるもの になります。
こちらはGPGPUという概念と同じで、実現するための方法の一つです。
描画処理以外の汎用的な計算をGPUに行わせるもの になります。
こちらはGPGPUという概念と同じで、実現するための方法の一つです。

エレキベア
GPGPUはグラフィックス技術の書籍にも書いてあったクマね〜〜
↑書籍の紹介記事

マイケル
GPUを使うと汎用的な計算に使うと何がいいのかについては、まずCPUとGPUの違いを知る必要があります。
CPUとGPUではそれぞれ得意分野が異なっていて、CPUが高速でコアが少ないのに対してGPUは低速でコアが多いのが特徴です。
CPUとGPUではそれぞれ得意分野が異なっていて、CPUが高速でコアが少ないのに対してGPUは低速でコアが多いのが特徴です。

エレキベア
GPUのコアは低速なのクマね・・・
それなのにGPUに任せちゃっていいのクマ??
それなのにGPUに任せちゃっていいのクマ??

マイケル
よく例えられるのがCPUは1人のスーパーマンで、GPUは大量の凡人、といったものがあるよ。
数が多いから簡単な計算を何回も行う必要がある場合等に有効だね!
逆に数が少なかったり複雑な処理だと返って逆効果になる場合もあるからそこは注意が必要だ!
数が多いから簡単な計算を何回も行う必要がある場合等に有効だね!
逆に数が少なかったり複雑な処理だと返って逆効果になる場合もあるからそこは注意が必要だ!


エレキベア
アザラッシ・・・

マイケル
ComputeShaderはHLSLで記述できて、
下記のような単位で処理がまとめられています。
下記のような単位で処理がまとめられています。
用語 | 概念 |
---|---|
カーネル | GPUで実行される1つの処理 |
スレッド | カーネルを実行する単位 |
グループ | スレッドを実行する単位 |


エレキベア
これは使いながら覚えるしかなさそうクマね

マイケル
実際に触りながら覚えていこう!
GPUで簡単な計算を行う

マイケル
それでは早速簡単な計算を行ってみましょう!
今回は Shader > Compute Shader から HeavyProcess.compute という名前で作成します。
今回は Shader > Compute Shader から HeavyProcess.compute という名前で作成します。


マイケル
こちらに 簡単な計算処理を実装した例 が下記になります。
#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の数値が入ってくる形になります。
スレッドは(X,Y,Z)の形式で指定して参照することで使用します。
今回の場合はXに64を指定しているので、各スレッドで「groupThreadID.x」に0〜63の数値が入ってくる形になります。

マイケル
引数の「SV_GroupThreadID」にはスレッドのIDが渡されてきます。
「SV_GroupID」は、グループのIDが渡されますが、こちらの数は後述のC#側で指定します。
「SV_GroupID」は、グループのIDが渡されますが、こちらの数は後述のC#側で指定します。

エレキベア
いろいろ覚えることはありそうクマが、まあさっきの図の話クマね


マイケル
この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」で処理を実行
といった流れになります。
ざっくりと
・「ComputeShader.FindKernel」でカーネルを取得
・「ComputeShader.SetXXX」でパラメータを設定
・「ComputeShader.Dispatch」で処理を実行
といった流れになります。

エレキベア
Dispatchのタイミングで処理が実行されるクマね

マイケル
Dispatchの際に指定するのがグループ数で、例えば(2,1,1)を指定した場合は
(64,1,1) * (2,1,1) = (128,1,1) のスレッドが実行されます。
(64,1,1) * (2,1,1) = (128,1,1) のスレッドが実行されます。

エレキベア
ここでグループ数を指定するクマね

マイケル
これを実行すると、下記のように計算結果がログ出力されるはずです。


エレキベア
単純な計算結果クマね
演算結果をテクスチャに描画する

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

エレキベア
UnityGraphicsProgrammingSeriesにはお世話になってるクマ〜〜

マイケル
こちらのComputeShader側の実装は下記のようになります。
RWTexture2Dでテクスチャバッファを宣言して、float4型で描画色を設定しています。
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」になります。
計算式でいうと「SV_Group_ID * numthreads + GroupThreadID」になります。

エレキベア
座標位置が渡されるイメージクマかね

マイケル
テクスチャは2Dのため今回はnumthreadsは(8,8,1)で、XYそれぞれ8スレッドずつで指定しています。
これをC#側から呼び出す処理は下記の通り!
これを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;
}
}

マイケル
実行の流れはそこまで変わりませんが、カーネル実行の際に指定するグループ数は「幅/スレッド数」になります。
そうすることでスレッド実行ごとに描画する範囲を分担して処理することができます。
そうすることでスレッド実行ごとに描画する範囲を分担して処理することができます。

エレキベア
段々スレッド数とグループ数の関係が分かってきたクマね〜〜〜

マイケル
こちらを実行すると下記のように表示されるはずです!


エレキベア
綺麗クマ〜〜〜
おわりに

マイケル
というわけで今回はComputeShaderの使い方でした!
どうだったかな??
どうだったかな??

エレキベア
難しそうなイメージだったクマが、やってみると案外簡単だったクマね

マイケル
今回の例以外でも、下記のライフゲームの実装サンプルなんかも処理負荷の効率化が確認できて面白かったよ!
【Unity】Compute Shaderについて勉強してみた – 株式会社ロジカルビート


エレキベア
どんな処理をComputeShaderに移すかが中々悩ましいクマが、
これはやりやすくてよさそうクマね
これはやりやすくてよさそうクマね

マイケル
それでは今日はこの辺で!
アデューーー!!
アデューーー!!

エレキベア
クマ〜〜〜〜
【Unity】ComputeShaderの基本的な使い方についてまとめる〜完〜
コメント