めーぷるのおもちゃばこ

- アイドルになりたいエンジニア女子の制作日記 -

【Unity】Compute Shader入門①〜オブジェクトを実際に動かしてみて完全に理解〜

ComputeShaderに入門したので入門記事です!
コンピュートシェーダーってなに?って話と、実際にコンピュートシェーダーでオブジェクトを動かすをして完全に理解しましょう!🐼💫

   

💙💫コンピュートシェーダーってなに?


↑スライドにも書いてますが、コンピュートシェーダーは一言で言うとGPGPU(General-Purpose computing on Graphics Processing Units)のためのシェーダーです。
GPGPU(General-Purpose computing on Graphics Processing Units)とはGPUを画面描画以外の汎用的な計算に使うことをいいます。
フラグメントシェーダーやバーテックスシェーダーがGPUを描画に使うのに対してコンピュートシェーダーは描画ではなく演算に使います。
GPUを使用するので、CPUで実装すると重たい処理を軽く早くできるのが特徴です。

もともと描画用に設計されたGPUを扱うしくみをシェーダーと読んでいて、その描画部分がぬけてCompute(計算する) Shader(シェーダー) という名前になったのです。それが、いわゆるシェーダーと全然違うくせにシェーダーって単語が名前に入っている所以なのですね。

ちなみに描画には使用しないため、コンピュートシェーダはレンダリングパイプラインの外で利用され、GPU上で実行されます。

💙💫この記事でつくるもの

この記事は実際にコンピュートシェーダーをつかってオブジェクトを簡単に動かしてみてなんとなく使い方を理解していく記事です。

こんな感じで動かしてみようとおもいます。

f:id:maplesyrup-cs6:20200518193719g:plain
こんな感じで回転させてみる

あれ...?
この動き....どこかで......


....う、うわああああああ!!!!
  

f:id:maplesyrup-cs6:20200518193315g:plainf:id:maplesyrup-cs6:20200518193315g:plainf:id:maplesyrup-cs6:20200518193315g:plainf:id:maplesyrup-cs6:20200518193315g:plainf:id:maplesyrup-cs6:20200518193315g:plain
!!!!!!

  
やっていきましょう🐼


💙💫最初のコンピュートシェーダーのコードを書き換えてこう

🐼コンピュートシェーダーの作成

コンピュートシェーダーの作り方はプロジェクトビューで右クリック>Create>Shader>Compute Shaderから作成できます。
こんなアイコンのやつがコンピュートシェーダーです。なんやこいつは。

f:id:maplesyrup-cs6:20200518193942p:plain
コンピュートシェーダーのアイコン

 

🐼最初のコード

Compute Shaderを作ってみると最初のコードはこんな感じ。
なにをやってるのかをみながら、目的のコードに書き換えていきます!

// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain

// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWTexture2D<float4> Result;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    // TODO: insert actual code here!

    Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);
}

  

🐼カーネルとして扱う関数の設定

一つ一つみていきます。

#pragma kernel CSMain

ここはどの関数をカーネルとして扱うかです。kernelというのは詳しくは後述しますが、コンピュートシェーダーでのメイン関数だと考えて良いです。どの関数がカーネルに当たるかを定義します。ここでは、CSMain という名前の関数がカーネルだよという意味です。
このカーネルはいくつでも定義できます。

       

🐼CPUとGPU間でのデータのやり取りを表す変数

RWTexture2D<float4> Result;

これはCPUとGPU間でのデータのやり取りを表す変数です。
GPUとCPUとのやり取りは基本的にバッファを介して行う必要があり、CPUでデータを受け取るためにこの RWStructuredBufferを使います。
ちなみにこの RWはたぶん Read / WriteのRWです。
コンピュートシェーダーで実行した結果をバッファに保存してCPUからそれを取得します。
最初のこの場合はTexture2Dを入れるになっています。

  
今回はfloat2を使いたいので以下のようにしましょう。
名前もバッファだということがわかるようResultBufferとします。

RWStructuredBuffer<float2> ResultBuffer;

このようにintfloatを渡したい場合は RWStructuredBufferを使います。

   

🐼CPUから値の受け取り

CPUから値を渡すだけの時は、コンピュートシェーダー側では

int intVal;

のように普通に変数の宣言をします。
ここでは

float2 position;
float time;

としましょう。timeは文字通りtimeです。C#のほうからここに時間を渡してコンピュートシェーダーで使用します。
positionには、今回は回転させたいので回転の中心となるポジションをC#からここに渡します。それをコンピュートシェーダーで計算したのち、先ほどのResultBufferに入れて返します。

   

🐼メイン関数

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    // TODO: insert actual code here!

    Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);
}

ここがメインとなるコンピュートシェーダーの関数です。

[numthreads(8,8,1)]

これはスレッドの指定をします。スレッドについても後述します。
今回はCSMainの関数の中を以下のように書き換えます。

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    position.x += cos(time*10)*0.1;
    position.y += sin(time*10)*0.1;
    ResultBuffer[0] = float2(position.x, position.y);
}

回転の中心となるX軸とY軸の値をC#からpositionにうけとり、それをsinとcosで回転させています。
計算した値をResultBufferにいれます。
このバッファにC#からアクセスすることで、コンピュートシェーダーで計算した値をC#で使うことができます。
コンピュートシェーダーの全コードは以下の通りです。
  

🐼コンピュートシェーダー全コード

#pragma kernel CSMain

RWStructuredBuffer<float2> ResultBuffert;
float2 position;
float time;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    position.x += cos(time*10)*0.1;
    position.y += sin(time*10)*0.1;
    ResultBuffer[0] = float2(position.x, position.y);
}

   
次はこの計算を実際に実行するためにC#を書きます。

   

💙💫C#でコンピュートシェーダーを扱う

  
コンピュートシェーダーは計算のためのシェーダーなので、フラグメントシェーダーやバーテックスシェーダーのようにそれだけで完結することはできません。たぶん。
基本的にはC#から計算したい部分だけをコンピュートシェーダーにお願いするといった感じです。
  

🐼C#全コード

C#スクリプトをつくり、なにかしらゲームオブジェクトにつけます。
そしてまず全コードです↓↓これを一つずつ解説していきます。

using System.Runtime.InteropServices;   

public class ObjectMover : MonoBehaviour
{
    [SerializeField] ComputeShader _computeShader;
    [SerializeField] Transform _MovingObj;

    private ComputeBuffer _buffer;
    private Vector3 center = Vector3.zero;

    void Start()
    {
       _buffer = new ComputeBuffer(1, Marshal.SizeOf(typeof(Vector2))); 
       _computeShader.SetBuffer(_computeShader.FindKernel("CSMain"), "ResultBuffer", _buffer);
    }

    void Update()
    {
       _computeShader.SetFloats("position", center.x, center.y); 
       _computeShader.SetFloat("time", Time.time);
       _computeShader.Dispatch(0, 8, 8, 1);

       var data = new float[2];
       _buffer.GetData(data);

        Vector3 pos = _MovingObj.transform.localPosition;
        pos.x = data[0];
        pos.y = data[1];
        _MovingObj.transform.localPosition = pos;
    }

    private void OnDestroy() 
    {
        _buffer.Release();
    }
}

   

🐼変数の定義

[SerializeField] ComputeShader _computeShader;
[SerializeField] Transform _MovingObj;

private ComputeBuffer _buffer;
private Vector3 center = Vector3.zero;

[SerializeField] ComputeShader _computeShader;でコンピュートシェーダーと、
動かしたいオブジェクトをセットする変数[SerializeField] Transform _MovingObj;をつくります。これらはインスペクタからセットします。

private ComputeBuffer _buffer;はさきほどコンピュートシェーダーでつくったRWStructuredBuffer ResultBuffer;にセットするためのバッファです。

centerにはオブジェクトの回転軸となるポジションを渡します。今回は(0, 0, 0)にしています。

   

🐼初期化

void Start()
{
   _buffer = new ComputeBuffer(1, Marshal.SizeOf(typeof(Vector2)));    //①
   _computeShader.SetBuffer(_computeShader.FindKernel("CSMain"), "ResultBuffer", _buffer);    //②
}

バッファを初期化し、コンピュートシェーダーのバッファに設定します。
  
まずこの   

_buffer = new ComputeBuffer(1, Marshal.SizeOf(typeof(Vector2))); 

はコンピュートシェーダーのバッファにセットするためのバッファを用意しています。
一つ目の引数はバッファの要素数二つ目の数はバッファ1つの要素サイズです。

バッファ一つの要素サイズがfloatであれば、

_buffer = new ComputeBuffer(1, sizeof(float));

   
と書けばいけるのですがそもそもこのsizeof演算子とは、

sizeof演算子は、指定された型の変数が占有しているバイト数を返します。 sizeof 演算子への引数は、アンマネージド型の名前、またはアンマネージド型に制限される型パラメーターである必要があります。
引用元:sizeof 演算子 - C# リファレンス | Microsoft Docs

というものらしく、

整数型など型のサイズが定義されている構造体の場合、Marshal.SizeOfメソッドを使うことにより型のサイズ(バイト数)を取得することができます。
引用元:構造体のサイズ (Marshal.SizeOf, sizeof) - Programming/.NET Framework/構造体 - 総武ソフトウェア推進所

     
とのことで、Vector2やVector3やその他構造体を設定したい時は

Marshal.SizeOf(typeof(Vector2));

とかくとよいみたいです。
このMarshal.SizeOf()関数を使用するために一番最初のusing System.Runtime.InteropServices;が必要になります。
   
そして次に

_computeShader.SetBuffer(_computeShader.FindKernel("CSMain"), "ResultBuffer", _buffer);

でそのバッファーをコンピュートシェーダーのバッファに設定しています。
引数は以下の通りです。

_computeShader.SetBuffer(カーネルのインデックス, 対象のバッファの名前, 設定するバッファ);

  
1つ目の引数のカーネルのインデックスとは、コンピュートシェーダーで最初に定義している

#pragma kernel CSMain

の部分のことです。例えばカーネルとして定義されてる関数が3つあったとき

#pragma kernel CSMain1
#pragma kernel CSMain2
#pragma kernel CSMain3

この定義のうえから順に0、1、2とカーネルのインデックスがつけられます。
なのでこの1つ目の引数には直接0とか1とかの数字をいれることもできますが、それだと書き換えたときに事故ったりするので
_computeShader.FindKernel("CSMain")と書くことで指定した名前のカーネルのインデックスを引数に与えてくれます。
  

※なぜバッファをセットするかというと、GPUは単体では駆動できず、CPUからの命令を受けて初めて動くことができます。
CPUがシェーダーをコンパイルしてそれを命令として受け取り、CPUが用意してくれたデータを使用して計算を行います。
なので、コンピュートシェーダーでバッファを定義しましたが、そのバッファの確保をするのはCPUの仕事なのです。CPUにバッファを作ってもらってセットしてもらわないとGPU単体ではバッファを確保できません。

  

🐼コンピュートシェーダに値を渡す

つづいてUpdateの中をみていきます。

_computeShader.SetFloats("position", center.x, center.y); 
_computeShader.SetFloat("time", Time.time);

コンピュートシェーダーに値をセットしています。ここはCPUからGPUに渡す値です。
floatの場合はSetFloat()を使い、Vector3などで複数値を渡すばあいはSetFloats()を使用します。
引数は共に(コンピュートシェーダーでの変数名、渡す値)です。

   

🐼コンピュートシェーダの実行

こうしてやっとこさコンピュートシェーダーを実行できます。コンピュートシェーダーの実行は

_computeShader.Dispatch(0, 8, 8, 1);

と書きます。
これでコンピュートシェーダーのカーネルに書いた計算を実行してくれます。
引数は(カーネルのインデックス、グループのx、グループのy、グループのz)です。
カーネルとグループについては後述するので今は気にしなくていいです。

🐼コンピュートシェーダで計算した値の取得

実行したら今度はコンピュートシェーダーで計算した結果の値をうけとります。

 var data = new float[2];
 _buffer.GetData(data);

_buffer.GetData()でコンピュートシェーダーのResultBufferに入った値を取得することができます。
引数にはその取得した値を入れる変数が必要なので、その変数を先に定義しています。

  

🐼取得した値を使用する

Vector3 pos = _MovingObj.transform.localPosition;
pos.x = data[0];
pos.y = data[1];
_MovingObj.transform.localPosition = pos;

そして取得した値を_MovingObjのlocalPositionに入れることで、オブジェクトを動かすことができました!

  

🐼バッファの開放

OnDestroyの際に必要のなくなったバッファは開放してあげましょう。

private void OnDestroy() 
{
    _buffer.Release();
}


わーい!!!🐼
無事オブジェクトを動かせました!簡単な内容でつかってみることで、なんとなく使い方を理解できたとおもいます。できていたら嬉しいです!

    

💙💫カーネル、スレッド、グループについて

最後に、何度かカーネルという単語がでてきて「後述します」とだけ書いていたので、その説明を含めた、「カーネル(Kernel)、スレッド(Thread)、グループ(Group)という概念」についてお話ししたいとおもいます。
コンピュートシェーダーを理解するためには欠かせない内容(らしい)なのですが小難しいこと先に話してもUターンしたくなってしまうので、最後にかきました!
とはいえ私も細かい使いかたなどは理解しきれてないのですが... 概念の説明をしておきます。
   

🐼カーネル(Kernel)

まず、何度かでてきたカーネル(Kernel)とは、GPUで実行される一つの処理を差し、コード上では1つの関数として扱われます。コンピュートシェーダーの最初に定義したこれは

#pragma kernel CSMain

CSMainという名前のやつがカーネルだよということですね。
このカーネルは同じコンピュートシェーダーファイル内にいくつもかくことができます。
ここでこの宣言をしていない関数はC#からFindKernelできないし、Dispatch()の実行もできないです。
  
また、カーネルに引数があったのですが↓↓

void CSMain (uint3 id : SV_DispatchThreadID)

今回は使用しなかったのでまた使用するブログを書くときに説明しようとおもいます。

    

🐼スレッド(Thread)

スレッド(Thread)とは、カーネルを実行する単位です。1スレッドが1カーネルを実行します。コンピュートシェーダーではカーネルを複数のスレッドで並行して同時に実行することができます。スレッドの指定は(x, y, z)の三次元です。
コンピュートシェーダーのCSMainのカーネルの上にあった[numthreads(8,8,1)]がスレッドの指定です。

[numthreads(8,8,1)]    //これ
void CSMain (uint3 id : SV_DispatchThreadID)
{
.....

この場合 (8, 8, 1)なので 8 * 8 * 1 = 64 Threadが同時に実行されます。
正直ここの数の決め方はよくわかっていません(すみません)。デフォルトが(8, 8, 1)だったのでそのまま使いました。
多次元的な結果や多次元てきな演算が必要な場合にこの多次元スレッドが有効なようです。今回はxのスレッドしか使わない状態になっています。
理解したらまたその辺を説明するブログを書きます。

   

🐼グループ(Group)

つづいてグループ(Group)です。Groupはスレッドを実行する単位です。Groupが持つThreadのことをGroup Threadといいます。
Groupも同じように三次元 (x, y, z) で指定します。
C#からコンピュートシェーダーを実行したときのこれ↓

_computeShader.Dispatch(0, 8, 8, 1);

この引数が(カーネルのインデックス、グループのx、グループのy、グループのz) なので、後ろ三つの引数がGroupの指定になっています。
Threadが(8, 8, 1)、Groupが (8, 8, 1) の指定なので、
(8 * 8 * 1) * (8 * 8 * 1) = 4096 のスレッドが実行されます。

え、なんかすごい数... ここの数の決め方はちゃんと理解しておいた方がよさそう。
たとえばテクスチャのピクセルなどを計算するときとかに、そのピクセル数に合わせてスレッドやグループの数を指定するみたいです。


なんだかややこしい気もしますが図にするとこんな感じです。(2次元の図ですが実際は三次元です。)
   

f:id:maplesyrup-cs6:20200519173927p:plain
イメージ図


   
以上で入門編ブログおしまいです!
自分も入門したばっかなので理解が追いついてない部分もありましたが、入門できましたでしょうか!
続きのコンピュートシェーダー入門ブログ書いたらまたここ↓↓に貼りますね🐼💫