C#
Unity
GPU
ComputeShader

[C#,Unity]Unityのコンピュートシェーダーでオブジェクトを動かしてみる

UnityのシェーダーにComputeShaderというものがあります
昔に触りにくそうという理由で諦めてしまったので、同じ道を辿る人が少しでも減るように超入門的なComputeShaderについての内容を書いていきます

環境
Unity 2017.4.1f1 64bit

オブジェクトを動かす

UnityのCubeを動かしてみましょう
NoName_2018-7-11_12-59_No-00.png
これをとりあえず右にComputeShaderを使って動かします

ComputeShaderを書いてみる

シェーダーファイルを作る

NoName_2018-7-11_13-1_No-00.png

最初の中身はこんな感じ

#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);
}

データの受け渡し準備をする(ComputeShader)

最初はTexture2Dを受け渡しするようになっていますが・・・

RWTexture2D<float4> Result;

今回は座標を受け渡ししたいので

RWStructuredBuffer<float> Result

にしましょう(横移動だけの予定なのでfloatだけ)
基本的にGPUのデータをCPU側に渡すために使います

次にCPU側からGPU側に現在の座標を渡せるようにするために
ComputeShaderに float positionX; を追加します

#pragma kernel CSMain
RWStructuredBuffer<float> Result;
float positionX;

[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);
}

ComputeShader側での受け渡し準備はこれで完了

移動処理を書く(ComputeShader)

現在の処理はデフォルトの

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

となっていますが、今回は座標を移動させたいので

Result[0] = positionX + 0.01f;

としましょう
これでComputeShader側のやるべきことは終わりです!

ComputeShaderのソース全体

MoveCompute.compute
#pragma kernel CSMain
RWStructuredBuffer<float> Result;
float positionX;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    Result[0] = positionX + 0.01f;
}

C#側でComputeShaderを使う

スクリプト生成

UnityのCreate->C# Scriptからスクリプトを作りましょう
その後適当なオブジェクトにAddComponentしてください

SerializeFieldの定義

今回は座標を移動させるためのComputeShaderと、現在の位置を取得するためのTransformを設定できるように変数を作ります

[SerializeField]
private ComputeShader m_ComputeShader;

[SerializeField]
private Transform m_Cube;

ComputeShaderにはProjectビューから、さきほど作ったComputeShaderを入れてください
Transformには移動させたいCubeのTransformを入れます

ComputeBufferの設定

ここからC#でComputeShaderを扱うためのメイン処理を書いていきます
先ほどComputeShaderで書いた RWStructuredBuffer<float> Result; を受け取るための変数を用意します

private ComputeBuffer m_Buffer;

そして開始時に初期化します

void Start ()
{
    m_Buffer = new ComputeBuffer(1, sizeof(float));
    m_ComputeShader.SetBuffer(0, "Result", m_Buffer);
}

詳しい説明

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

この行でComputeShaderの結果を受け取るためのバッファー(要素float1個分)を用意しています

m_ComputeShader.SetBuffer(0, "Result", m_Buffer);

そしてそのバッファーをComputeShaderに設定しています

これでCPUとGPUが繋がりました!

毎フレームデータを更新する

Update関数の中でデータを更新していきます
まずはCPU側のCubeの座標をGPUに送ります

m_ComputeShader.SetFloat("positionX", m_Cube.position.x);

処理の内容としてはComputeShaderにFloatをSetする関数を使って、positionXにm_Cubeのposition.Xを渡しているという内容です

これでGPU側がCPU側のCube座標について知ることができました!

GPU側に必要な情報は全て渡し終えたので計算処理をしてもらいます

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

ComputeShaderのDispatchを使うことで処理を開始できます
(ひとまず基礎的なことだけを知るためなので引数については省略)

Dispatchを呼んだことでGPU側でCubeを移動させる処理が完了しました!
しかしCPU側にそのデータは存在していないため、m_Cubeに適用することができません
なので取得をします

var data = new float[1];
m_Buffer.GetData(data);

詳しい説明

var data = new float[1];

受け取るデータはX軸の座標のみなのでfloatの要素数1の配列を用意します

m_Buffer.GetData(data);

用意した配列にm_Bufferに入っているデータを入れます
これでGPU側のデータをCPU側で使えるようになりました!

移動させる

Cubeを取得したデータを元に移動させます

float positionX = data[0];

var boxPosition = m_Cube.position;
boxPosition.x = positionX;
m_Cube.position = boxPosition;

再生すると右に移動していく
NoName_2018-7-11_12-59_No-00.png

無事GPUで計算した座標に移動させることができました!
おめでとうございます!

いらなくなったBufferを解放する

ちゃんと解放しましょう

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

C#側のソース全体

MoveCube.cs
using UnityEngine;

public class MoveCube: MonoBehaviour
{
    [SerializeField]
    private ComputeShader m_ComputeShader;

    [SerializeField]
    private Transform m_Cube;

    private ComputeBuffer m_Buffer;

    void Start ()
    {
        m_Buffer = new ComputeBuffer(1, sizeof(float));
        m_ComputeShader.SetBuffer(0, "Result", m_Buffer);
    }

    void Update ()
    {
        m_ComputeShader.SetFloat("positionX", m_Cube.position.x);

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

        var data = new float[1];
        m_Buffer.GetData(data);

        float positionX = data[0];

        var boxPosition = m_Cube.position;
        boxPosition.x = positionX;
        m_Cube.position = boxPosition;
    }

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