Unityでライフゲームの実装いろいろ

Unityで大量のメッシュをレンダリングする勉強として、ライフゲームをいろいろな方法で実装してみました。
以下のように、Boxメッシュを一つのセルとして平面状に並べて色で状態を表します。

![ouput.gif](https://qiita-image-store.s3.amazonaws.com/0/61569/4a70ce8e-756b-29fb-5363-e3f8b4e81312.gif)

基本的にはすべてのサンプルで同じものが表示されますが、メッシュのレンダリング方法と計算方法が違います。

# GameObject.CreatePrimitiveを利用する方法

`GameObject.CreatePrimitive`を利用して、すべてのセルをGameObjectにするという単純な方法です。
`Start()`でGameObjectおよび状態を管理する配列を作成し、`Update()`でセルの次状態の計算をしています。

```csharp:LifeGameByCreatePrimitive.cs
using UnityEngine;

struct Cell
{
    public GameObject gameObject;
    public int state;
};

public class LifeGameByCreatePrimitive : MonoBehaviour
{
    [Range(10, 1000)] public int width = 120;
    [Range(10, 1000)] public int height = 80;
    [Range(1, 10)] public float spacing = 2f;
    public Material material;
    private Cell[,] cells;

    void Awake()
    {
        cells = new Cell[width, height];
        float halfW = width * spacing * 0.5f;
        float halfH = height * spacing * 0.5f;
        for (int h = 0; h < height; h++)
        {
            for (int w = 0; w < width; w++)
            {
                GameObject gameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
                Renderer renderer = gameObject.GetComponent<Renderer>();
                gameObject.GetComponent<Renderer>().material = material;
                gameObject.transform.parent = this.transform;
                gameObject.transform.position = new Vector3(w * spacing - halfW, h * spacing - halfH, 0);
                renderer.receiveShadows = false;
                renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
                cells[w, h].gameObject = gameObject;
                cells[w, h].state = Random.value < 0.5 ? 1 : 0;
            }
        }
    }
	
	void Update () {
        Cell[,] nextCells = new Cell[width, height];

        for (int h = 0; h < height; h++)
        {
            for (int w = 0; w < width; w++)
            {
                if (cells[w, h].state == 1)
                {
                    cells[w,h].gameObject.GetComponent<Renderer>().material.color = Color.green;
                }
                else
                {
                    cells[w, h].gameObject.GetComponent<Renderer>().material.color = Color.black;
                }
                int left = w != 0 ? w - 1 : width - 1;
                int right = w != width - 1 ? w + 1 : 0;
                int up = h != 0 ? h - 1 : height - 1;
                int down = h != height - 1 ? h + 1 : 0;

                int neighbors = 0;
                neighbors += cells[left, up].state;
                neighbors += cells[w, up].state;
                neighbors += cells[right, up].state;
                neighbors += cells[left, h].state;
                neighbors += cells[right, h].state;
                neighbors += cells[left, down].state;
                neighbors += cells[w, down].state;
                neighbors += cells[right, down].state;

                int prevState = cells[w, h].state;
                int nextState = (prevState == 1 && (neighbors == 2 || neighbors == 3)) || (prevState == 0 && neighbors == 3) ? 1 : 0;
                nextCells[w, h].gameObject = cells[w, h].gameObject;
                nextCells[w, h].state = nextState;
            }
        }
        cells = nextCells;
	}
}
```

# Graphics.DrawMeshを利用する方法

この方法ではGameObjectは作成せずに、`Graphics.DrawMesh`を各セルごとに呼ぶことでメッシュのレンダリングを行います。Materialをすべてのセルで使いまわせるように、`MaterialPropertyBlock`を利用しています。
メッシュの次状態の計算は`GameObject.CreatePrimitive`と同じです。

```csharp:LifeGameByDrawMesh.cs
using UnityEngine;

public class LifeGameByDrawMesh : MonoBehaviour
{
    [Range(10, 1000)] public int width = 120;
    [Range(10, 1000)] public int height = 80;
    [Range(1, 10)] public float spacing = 2f;
    public Mesh mesh;
    public Material material;
    private int[,] states;
    private int _width, _height;
    private MaterialPropertyBlock materialPropertyBlock;

	void Start ()
    {
        _width = width;
        _height = height;
        states = new int[_width, _height];
        for (int h = 0; h < _height; h++)
        {
            for (int w = 0; w < _width; w++)
            {
                states[w, h] = Random.value < 0.5 ? 1 : 0;
            }
        }
        materialPropertyBlock = new MaterialPropertyBlock();
	}
	
	void Update ()
    {
        int[,] nextStates = new int[_width, _height];

        float halfW = _width * spacing * 0.5f;
        float halfH = _height * spacing * 0.5f;
        for (int h = 0; h < _height; h++)
        {
            for (int w = 0; w < _width; w++)
            {
                int state = states[w, h];

                materialPropertyBlock.Clear();
                if (state == 1) {
                    materialPropertyBlock.SetColor("_Color", Color.green);
                } else {
                    materialPropertyBlock.SetColor("_Color", Color.black);
                }
                Vector3 position = new Vector3(w * spacing - halfW, h * spacing - halfH, 0);
                Graphics.DrawMesh(mesh, position, Quaternion.identity, material, 0, null, 0, materialPropertyBlock, false, false);

                int left = w != 0 ? w - 1 : _width - 1;
                int right = w != _width - 1 ? w + 1 : 0;
                int up = h != 0 ? h - 1 : _height - 1;
                int down = h != _height - 1 ? h + 1 : 0;

                int neighbors = 0;
                neighbors += states[left, up];
                neighbors += states[w, up];
                neighbors += states[right, up];
                neighbors += states[left, h];
                neighbors += states[right, h];
                neighbors += states[left, down];
                neighbors += states[w, down];
                neighbors += states[right, down];

                int nextState = (state == 1 && (neighbors == 2 || neighbors == 3)) || (state == 0 && neighbors == 3) ? 1 : 0;
                nextStates[w, h] = nextState;
            }
        }

        states = nextStates;
	}
}
```

# Graphics.DrawMeshInstancedを利用する方法

GPUインスタンシングを利用して、メッシュをまとめてレンダリングする方法です。
描画位置を表した`Matrix4x4`のリストを引数にして、二重ループの内側のループを抜けるたびに`Graphics.DrawMeshInstanced`を呼んでいます。これはGPUインスタンシングでは一度に1024個しかレンダリングできないためです（詳しいことはわかりませんが...）。
使用するMaterialは「Enable GPU Instancing」をチェックしておく必要があります。

この方法はDrawCallが少なくなるので、先に述べた2つの方法と比べて大量にセルを表示してもパフォーマンスが落ちにくいです。

```csharp:LifeGameByDrawMeshInstanced.cs
using System.Collections.Generic;
using UnityEngine;

public class LifeGameByDrawMeshInstanced : MonoBehaviour
{

    [Range(10, 1000)] public int width = 120;
    [Range(10, 1000)] public int height = 80;
    [Range(1, 10)] public float spacing = 2f;
    public Mesh mesh;
    public Material material;
    private int[,] states;
    private int _width, _height;
    private MaterialPropertyBlock materialPropertyBlock;
    
	void Start ()
    {
        _width = width;
        _height = height;

        states = new int[_width, _height];
        for (int h = 0; h < _height; h++)
        {
            for (int w = 0; w < _width; w++)
            {
                states[w, h] = Random.value < 0.5 ? 1 : 0;
            }
        }

        materialPropertyBlock = new MaterialPropertyBlock();
    }
	
	void Update ()
    {
        int[,] nextStates = new int[_width, _height];
        List<Matrix4x4> aliveList = new List<Matrix4x4>();
        List<Matrix4x4> deadList = new List<Matrix4x4>();

        float halfW = _width * spacing * 0.5f;
        float halfH = _height * spacing * 0.5f;
        for (int h = 0; h < _height; h++)
        {
            for (int w = 0; w < _width; w++)
            {
                int state = states[w, h];
                Vector3 position = new Vector3(w * spacing - halfW, h * spacing - halfH, 0);
                Matrix4x4 mat = new Matrix4x4();
                mat.SetTRS(position, Quaternion.identity, Vector3.one);
                if (state == 1)
                {
                    aliveList.Add(mat);
                }
                else
                {
                    deadList.Add(mat);
                }

                int left = w != 0 ? w - 1 : _width - 1;
                int right = w != _width - 1 ? w + 1 : 0;
                int up = h != 0 ? h - 1 : _height - 1;
                int down = h != _height - 1 ? h + 1 : 0;

                int neighbors = 0;
                neighbors += states[left, up];
                neighbors += states[w, up];
                neighbors += states[right, up];
                neighbors += states[left, h];
                neighbors += states[right, h];
                neighbors += states[left, down];
                neighbors += states[w, down];
                neighbors += states[right, down];

                int nextState = (state == 1 && (neighbors == 2 || neighbors == 3)) || (state == 0 && neighbors == 3) ? 1 : 0;
                nextStates[w, h] = nextState;
            }

            materialPropertyBlock.Clear();
            materialPropertyBlock.SetColor("_Color", Color.green);
            Graphics.DrawMeshInstanced(mesh, 0, material, aliveList.ToArray(), aliveList.Count, materialPropertyBlock, UnityEngine.Rendering.ShadowCastingMode.Off, false);

            materialPropertyBlock.Clear();
            materialPropertyBlock.SetColor("_Color", Color.black);
            Graphics.DrawMeshInstanced(mesh, 0, material, deadList.ToArray(), deadList.Count, materialPropertyBlock, UnityEngine.Rendering.ShadowCastingMode.Off, false);

            aliveList.Clear();
            deadList.Clear();
        }
        states = nextStates;
    }
}
```

# Graphics.DrawMeshInstancedIndirect + Compute Shaderを利用する方法

次状態の計算をCompute Shaderで行い、状態を管理するBufferをそのままMaterialのシェーダーに渡してGPUインスタンシングでレンダリングする方法です。ちゃんと測ったわけではありませんが、これが最も大量のセルを表示することができます。

```csharp:ByDrawMeshInstancedIndirect.cs
using UnityEngine;

public class LifeGameByDrawMeshInstancedIndirect : MonoBehaviour
{

    const int BLOCK_SIZE = 256;
    [Range(10, 1000)] public int width = 120;
    [Range(10, 1000)] public int height = 80;
    [Range(1, 10)] public float spacing = 2f;
    public Mesh mesh;
    public Material material;
    public ComputeShader computeShader;
    private int _width, _height;
    private Bounds bounds;
    private ComputeBuffer stateBuffer;
    private ComputeBuffer argsBuffer;
    private uint[] args = new uint[5];

    void Start()
    {
        _width = width;
        _height = height;

        stateBuffer = new ComputeBuffer(width * height, sizeof(int));
        int[] states = new int[_width * _height];
        for (int h = 0; h < _height; h++)
        {
            for (int w = 0; w < _width; w++)
            {
                states[w + h * _width] = Random.value < 0.5 ? 1 : 0;
            }
        }
        stateBuffer.SetData(states);
        bounds = new Bounds(Vector3.zero, new Vector3(_width * spacing, _height * spacing, 2));

        argsBuffer = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
        args[0] = mesh.GetIndexCount(0);
        args[1] = (uint) (width * height);
        args[2] = mesh.GetIndexStart(0);
        args[3] = mesh.GetBaseVertex(0);
        args[4] = 0;
        argsBuffer.SetData(args);

        computeShader.SetInt("_Width", _width);
        computeShader.SetInt("_Heihgt", _height);
        material.SetInt("_Width", _width);
        material.SetInt("_Height", _height);
    }
	
	void Update ()
    {
        int kernelId = computeShader.FindKernel("LifeGame");
        computeShader.SetBuffer(kernelId, "StateBuffer", stateBuffer);
        int groupSize = Mathf.CeilToInt(_width * _height / BLOCK_SIZE);
        computeShader.Dispatch(kernelId, groupSize, 1, 1);

        material.SetBuffer("stateBuffer", stateBuffer);
        material.SetFloat("_Spacing", spacing);
        Graphics.DrawMeshInstancedIndirect(mesh, 0, material, bounds, argsBuffer);
	}

    void OnDestroy()
    {
        stateBuffer.Release();
        argsBuffer.Release();
    }
}
```

```ByDrawMeshInstancedIndirect.compute
#pragma kernel LifeGame

RWStructuredBuffer<int> StateBuffer;

#define BLOCK_SIZE 256

uint _Width;
uint _Height;

uint2 FromIdToCoord(uint id) {
	uint x = id % _Width;
	uint y = id / _Width;
	return uint2(x, y);
}

int FromCoordToId(uint2 coord) {
	return coord.y * _Width + coord.x;
}

[numthreads(BLOCK_SIZE,1,1)]
void LifeGame (uint3 id : SV_DispatchThreadID) {
	uint2 center = FromIdToCoord(id.x);
	uint left = center.x != 0 ? center.x - 1 : _Width - 1;
	uint right = center.x != _Width - 1 ? center.x + 1 : 0;
	uint up = center.y != 0 ? center.y - 1 : _Height - 1;
	uint down = center.y != _Height - 1 ? center.y + 1 : 0;

	int state = StateBuffer[id.x];
	int neighbors = 0;
	neighbors += StateBuffer[FromCoordToId(uint2(left, up))];
	neighbors += StateBuffer[FromCoordToId(uint2(center.x, up))];
	neighbors += StateBuffer[FromCoordToId(uint2(right, up))];
	neighbors += StateBuffer[FromCoordToId(uint2(left, center.y))];
	neighbors += StateBuffer[FromCoordToId(uint2(right, center.y))];
	neighbors += StateBuffer[FromCoordToId(uint2(left, down))];
	neighbors += StateBuffer[FromCoordToId(uint2(center.x, down))];
	neighbors += StateBuffer[FromCoordToId(uint2(right, down))];

	StateBuffer[id.x] = (state ==1 && (neighbors == 2 || neighbors == 3)) || (state == 0 && neighbors == 3) ? 1 : 0;
}
```

```ByDrawMeshInstancedIndirect.shader
Shader "LifeGame/ByDrawMeshInstancedIndirect" {
	Properties {
		_Color ("Color", Color) = (1,1,1,1)
		_Glossiness ("Smoothness", Range(0,1)) = 0.5
		_Metallic ("Metallic", Range(0,1)) = 0.0
		_Width ("Width", Range(10, 1000)) = 120
		_Heihgt ("Height", Range(10, 1000)) = 80
		_Spacing ("Spacing", Range(1, 10)) = 2
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200

		CGPROGRAM
		#pragma surface surf Standard fullforwardshadows
		#pragma multi_compile_instancing
		#pragma instancing_options procedural:setup

		struct Input {
			float2 uv_MainTex;
		};

		#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
		StructuredBuffer<int> stateBuffer;
		#endif

		int _Width;
		int _Height;
		float _Spacing;

		void setup() {
			#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
			int x = (unity_InstanceID % _Width - _Width * 0.5) * _Spacing;
			int y = (unity_InstanceID / _Width - _Height * 0.5) * _Spacing;
			unity_ObjectToWorld._14_24_34_44 = float4(x, y, 0, 1);
			#endif
		}

		half _Glossiness;
		half _Metallic;
		fixed4 _Color;

		void surf (Input IN, inout SurfaceOutputStandard o) {
			#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
			if (stateBuffer[unity_InstanceID]  == 1) {
				o.Albedo = fixed3(0, 1, 0);
			} else {
				o.Albedo = fixed3(0, 0, 0);
			}
			#endif
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = 1;
		}
		ENDCG
	}
	FallBack "Diffuse"
}
```