【Unity】Compute Shaderでライフゲーム

UnityのCompute Shaderでライフゲームの実装です。

![lifegame.gif](https://qiita-image-store.s3.amazonaws.com/0/61569/7bfb3b37-ebf1-6e95-ef6d-d5ca6b36dd2d.gif)


```cs:LifeGame.cs
using UnityEngine;

public class LifeGame : MonoBehaviour
{
    [SerializeField] GameObject plane;
    [SerializeField] ComputeShader computeShader;
    private RenderTexture stateTexture, drawTexture;

    private int kernelInitialize, kernelUpdate, kernelDraw;
    private ThreadSize threadSizeInitialize, threadSizeUpdate, threadSizeDraw;

    struct ThreadSize
    {
        public int x;
        public int y;
        public int z;

        public ThreadSize(uint x, uint y, uint z)
        {
            this.x = (int)x;
            this.y = (int)y;
            this.z = (int)z;
        }
    }

    private void Start()
    {
        // カーネルIdの取得
        kernelInitialize = computeShader.FindKernel("Initialize");
        kernelUpdate = computeShader.FindKernel("Update");
        kernelDraw = computeShader.FindKernel("Draw");

        // 状態を格納するテクスチャの作成
        stateTexture = new RenderTexture(512, 512, 0, RenderTextureFormat.RInt);
        stateTexture.wrapMode = TextureWrapMode.Repeat;
        stateTexture.enableRandomWrite = true;
        stateTexture.Create();
        // レンダリング用のテクスチャの取得
        drawTexture = new RenderTexture(512, 512, 0, RenderTextureFormat.ARGB32);
        drawTexture.filterMode = FilterMode.Point;
        drawTexture.enableRandomWrite = true;
        drawTexture.Create();

        // スレッド数の取得
        uint threadSizeX, threadSizeY, threadSizeZ;
        computeShader.GetKernelThreadGroupSizes(kernelInitialize, out threadSizeX, out threadSizeY, out threadSizeZ);
        threadSizeInitialize = new ThreadSize(threadSizeX, threadSizeY, threadSizeZ);
        computeShader.GetKernelThreadGroupSizes(kernelUpdate, out threadSizeX, out threadSizeY, out threadSizeZ);
        threadSizeUpdate = new ThreadSize(threadSizeX, threadSizeY, threadSizeZ);
        computeShader.GetKernelThreadGroupSizes(kernelDraw, out threadSizeX, out threadSizeY, out threadSizeZ);
        threadSizeDraw = new ThreadSize(threadSizeX, threadSizeY, threadSizeZ);

        // LifeGameの状態の初期化
        computeShader.SetTexture(kernelInitialize, "stateTexture", stateTexture);
        computeShader.Dispatch(kernelInitialize, Mathf.CeilToInt(stateTexture.width / threadSizeInitialize.x), Mathf.CeilToInt(stateTexture.height / threadSizeInitialize.y), 1);
    }

    private void Update()
    {
        // LifeGameの状態の更新
        computeShader.SetTexture(kernelUpdate, "stateTexture", stateTexture);
        computeShader.Dispatch(kernelUpdate, Mathf.CeilToInt(stateTexture.width / threadSizeUpdate.x), Mathf.CeilToInt(stateTexture.height / threadSizeUpdate.y), 1);

        // LifeGameの状態をもとにレンダリング用のテクスチャを作成
        computeShader.SetTexture(kernelDraw, "stateTexture", stateTexture);
        computeShader.SetTexture(kernelDraw, "drawTexture", drawTexture);
        computeShader.Dispatch(kernelDraw, Mathf.CeilToInt(stateTexture.width / threadSizeDraw.x), Mathf.CeilToInt(stateTexture.height / threadSizeDraw.y), 1);
        plane.GetComponent<Renderer>().material.mainTexture = drawTexture;
    }
}
```

```LifeGame.compute
#pragma kernel Initialize
#pragma kernel Update
#pragma kernel Draw

RWTexture2D<int>  stateTexture;
RWTexture2D<float4> drawTexture;

float random(float2 x)
{
    return frac(sin(dot(x.xy, float2(12.9898, 78.233))) * 43758.5453);
}

[numthreads(8, 8, 1)]
void Initialize(uint3 dispatchThreadId : SV_DispatchThreadID)
{
	stateTexture[dispatchThreadId.xy] = random(dispatchThreadId.xy) < 0.5 ? 0 : 1;
}

[numthreads(8, 8, 1)]
void Update(uint3 dispatchThreadId : SV_DispatchThreadID)
{

	int state = stateTexture[dispatchThreadId.xy];

	int neighbors = 0;
	neighbors += stateTexture[dispatchThreadId.xy + uint2(-1, -1)];
	neighbors += stateTexture[dispatchThreadId.xy + uint2(-1,  0)];
	neighbors += stateTexture[dispatchThreadId.xy + uint2(-1,  1)];
	neighbors += stateTexture[dispatchThreadId.xy + uint2( 0, -1)];
	neighbors += stateTexture[dispatchThreadId.xy + uint2( 0,  1)];
	neighbors += stateTexture[dispatchThreadId.xy + uint2( 1, -1)];
	neighbors += stateTexture[dispatchThreadId.xy + uint2( 1,  0)];
	neighbors += stateTexture[dispatchThreadId.xy + uint2( 1,  1)];

	if ((state == 0 && neighbors == 3) || (state == 1 && (neighbors == 2 || neighbors == 3))) {
		stateTexture[dispatchThreadId.xy] = 1;
	} else {
		stateTexture[dispatchThreadId.xy] = 0;
	}
}

[numthreads(8, 8, 1)]
void Draw(uint3 dispatchThreadId : SV_DispatchThreadID)
{
	drawTexture[dispatchThreadId.xy] = stateTexture[dispatchThreadId.xy] == 1 ? float4(0, 1, 0, 1) : float4(0, 0, 0, 1);
}
```

`LifeGame.cs`内の`Initialize`メソッドでは、まず`RenderingTexture`の作成を行っています。今回はライフゲームの状態を格納する`stateTexture`とマテリアルにテクスチャとして渡す`drawTexture`を作成しています。ライフゲームの状態は0と1の2値で管理するので`stateTexture`には`RenderTextureFormat.RInt`を使用しています。
また、`Initialize`メソッドではライフゲームの状態の初期化をComputeShaderで行っています。`Initialize`カーネルを用いてランダムに0と1を設定しています。

`Update`メソッド内では、ライフゲームの状態の更新を`Update`カーネルで、状態をもとにしたレンダリング用のテクスチャへの描画を`Draw`メソッド内で行っています。