Continue(s)

Twitter:@dn0t_ GitHub:@ogrew

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

前回に引き続き、ComputeShaderを使っていろいろ試行錯誤していきます。

taiga.hatenadiary.com

前回は最終的にGPUまで登場させてUnity関係なく四則演算をちょっとやっただけだったので、今回はオブジェクトを1つ回転させて見るところから始めます。

まずはC#スクリプト側。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CubeComputeShader : MonoBehaviour
{
    public ComputeShader Shader;
    public Transform Target;

    private int _kernelIndex;
    private ComputeBuffer _buffer;

    void Start()
    {
        _kernelIndex = Shader.FindKernel("RotateTarget");
        _buffer = new ComputeBuffer(1, sizeof(float));
        Shader.SetBuffer(_kernelIndex, "floatBuffer", _buffer);
    }

    void Update()
    {
        Shader.Dispatch(_kernelIndex, 1, 1, 1);
        var data = new float[1];
        _buffer.GetData(data);
        // Unityで回転の代入は難しいです(よく理解していないという意味で)
        Target.eulerAngles = new Vector3(data[0], 0, 0);
    }

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

続いてシェーダ側。

#pragma kernel RotateTarget

RWStructuredBuffer<float> floatBuffer;

[numthreads(10, 1, 1)]
void RotateTarget(uint id : SV_DispatchThreadID)
{
    floatBuffer[id] += 1.0f;
}

実際に試してみましょう。

f:id:taiga006:20201026234808g:plain

1フレームごとに1度ずつキューブを回転させることが出来ました。(後続の実験のためにnumthreadsをあえて大きくしています。)

GPU「わざわざ仕事で呼び出されて、キューブをたった1つ回してお終いかい?」

では、たくさん回してもらいましよう。

まずはC#スクリプト側。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CubeComputeShader : MonoBehaviour
{
    public ComputeShader Shader;
    public int Count;

    private int _kernelIndex;
    private ComputeBuffer _buffer;
    private GameObject[] _cubes;

    private int _num = 10;

    void Start()
    {
        _cubes = new GameObject[Count];
        for(int i = 0; i < Count; i++) {
            var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
            cube.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f);
            cube.transform.localPosition = new Vector3(i%_num, (i/_num)%_num, i/(_num*_num));
            _cubes[i] = cube;
        }

        _kernelIndex = Shader.FindKernel("RotateTarget");
        _buffer = new ComputeBuffer(Count, sizeof(float));
        Shader.SetBuffer(_kernelIndex, "floatBuffer", _buffer);
    }

    void Update()
    {
        // 1000(= 10000 / 10)グループの各10スレッド(numthreads)で並列処理を実行
        int groupX = (Count / _num);
        Shader.Dispatch(_kernelIndex, groupX, 1, 1);
        var data = new float[Count];
        _buffer.GetData(data);
        
        for(int i = 0; i < Count; i++) {
            _cubes[i].transform.localEulerAngles = new Vector3(data[i], 0, 0);
        }
    }


    (~以下同じ~)
}

シェーダ側は最初の例と同じです。

では、実際に試してみましょう。

f:id:taiga006:20201027004443g:plain

本筋とは関係ないところですが、

cube.transform.localPosition = new Vector3(i%_num, (i/_num)%_num, i/(_num*_num));

ここが少しややこしいかもしれません。今回は大量のキューブということで10×10×10 = 10000個を等間隔で並べる都合でXYZ軸それぞれで計算しています。

少し凡例を出すとすぐに分かると思います。(たとえば250個目のキューブの座標は?)

実行結果は問題なさそうですが、果たしてこれで効率的なのかと言われると少し不安です。

        // 1000(= 10000 / 10)グループの各10スレッド(numthreads)で並列処理を実行
        int groupX = (Count / _num);
        Shader.Dispatch(_kernelIndex, groupX, 1, 1);

コメントにあるとおりなのですが、今回の例であればグループ10000でスレッド1でも結果は変わらないということになるのでしょうか?

グループ数よりはスレッド数を増やすので良い?ケースバイケース?ちょっとこのあたりはまだわかっていません。

GPU「なんだかたくさん回しているけどどれも条件が一緒で単調だな!?」

そうですね、せっかくですので最後に初期の回転をずらしてぐちゃぐちゃにしてみましょう。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CubeComputeShader : MonoBehaviour
{
    public ComputeShader Shader;
    public int Count;

    private int _kernelIndex;
    private ComputeBuffer _buffer;
    private GameObject[] _cubes;
    private float[] _angles;

    private int _num = 10;

    void Start()
    {
        _cubes = new GameObject[Count];
        _angles = new float[Count];
        for(int i = 0; i < Count; i++) {
            var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
            cube.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f);
            cube.transform.localPosition = new Vector3(i%_num, (i/_num)%_num, i/(_num*_num));
            _cubes[i] = cube;
            _angles[i] = Random.Range(-90, 90);
            _cubes[i].transform.localEulerAngles = new Vector3(_angles[i], 0, 0);
        }

        _kernelIndex = Shader.FindKernel("RotateTarget");
        _buffer = new ComputeBuffer(Count, sizeof(float));
        Shader.SetBuffer(_kernelIndex, "floatBuffer", _buffer);
        _buffer.SetData(_angles);
    }

    (~以下同じ~)

シェーダ側は引き続き一緒です。

実際に試してみましょう。

f:id:taiga006:20201027005926g:plain

できました。コードは、ほとんど変わっていません。

各キューブの角度を随時保存するための_anglesという配列を導入しました。それと、

_buffer.SetData(_angles);

で初期角度を代入しているところだけです。

実は最初戸惑ったのですが、この時点ですでに_bufferはシェーダ側のfloatBufferと紐付いているのでSetDataでそちらに初期角度を送ることがこれで出来ています。

まとめ

今回はUnityのコンピュートシェーダを使って大量のオブジェクトの回転を実験しました。

まだまだ手探りですが、前回と違ってUnityで実際に物が動いているのが見れたので、楽しかったです。

しかし、先の例で上げたスレッドとグループの効率化みたいな部分はよくわかっていません。その3ではそのあたりを学んでいきたいです。

それでは。

参考

www.wwwmaplesyrup-cs6.work

qiita.com

www.shibuya24.info

近況

10月から大きいプロジェクトに異動しました。相変わらずゲームを作っています。

PENTAX SPを買ったので昼休みや休日はひたすら散歩しています。

それと、寒くなってきて腰がまた痛みだしました。はー、早く引っ越ししたい。

f:id:taiga006:20201027011029j:plain PENTAX ME / Helios 44-2 / FUJIFILM 業務用100