TouchDesignerでライフゲーム

TouchDesignerのFeedback CHOPを使ってライフゲームを実装してみます。
![study_lifegame.7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/5e496c79-3d60-22ac-68df-ebf2cf876b24.gif)
ネットワーク全体は以下のようになります。
![network_all.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/8cf3a0fc-f22e-24c2-5245-167d55b069e9.png)
コンポーネントの色ごとにおおよそ次の処理を行っています。

- 青: ライフゲームの初期状態の設定
- 橙: ライフゲームの次状態の計算
- 黄: 各セルの位置と色の計算
- 青: インスタンシングを用いた描画

ここからは各処理ごとに詳細を見ていきます。

# ライフゲームの初期状態の設定

![network_init.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/70b038a1-886d-d6e0-8257-f6954f003f7b.png)
セルを格子上に並べるため、Grid CHOPを使用して各セルの位置を求めています。縦横それぞれのセルの数はライフゲームの次状態の計算でも必要なのでConstant CHOPで管理しています。
![grid1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/d88817ff-15c5-d1a9-b271-2377b9a325e7.png)
各セルの初期状態を保持するChannelをNoise CHOPとExpression Chopで作成します。Noise CHOPでセル数と同じ大きさのChannelを作成して、Expression Chopで値が0・1の二値になるようにしています。今回の実装ではは0が「死」、1が「生」の状態を表しています。
![noise1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/9a524709-27c8-d93e-7efa-99e5f8504ef5.png)


# ライフゲームの次状態の計算

![network_calc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/49e7c6f8-a832-eb67-f9df-10515ec539a1.png)
Feedback CHOPで各セルの状態をフレームをまたいで管理するようにし、Script CHOPで次状態の計算を行っています。Feedback CHOPの第１入力にScript CHOPの出力を繋ぎ、第２入力には先ほどに作成した初期状態の出力を繋いでいます。Feedback CHOPのResetパラメータの「Pulse」を押すと、第２入力の状態をもとにシミュレーションが実行されます。

Script CHOPには以下の次状態を計算するPythonスクリプトを記述しています。`onCook`関数の中で実際の計算が行われています。また、縦横のセルの数を設定するために`onSetupParameters`関数内でパラメータを追加しています。

```python
# me - this DAT
# scriptOp - the OP which is cooking

# press 'Setup Parameters' in the OP to call this function to re-create the parameters.
def onSetupParameters(scriptOp):
	page = scriptOp.appendCustomPage('Custom')
	p = page.appendInt('Width', label='width')
	p = page.appendInt('Height', label='height')
	return

# called whenever custom pulse parameter is pushed
def onPulse(par):
	return

def onCook(scriptOp):
	scriptOp.clear()

	input = scriptOp.inputs[0]
	prev_chan = input.chan(0)

	chan = scriptOp.appendChan(prev_chan.name)
	width = scriptOp.par.Width.eval()
	height = scriptOp.par.Height.eval()
	for w in range(width):
		for h in range(height):
			left = w - 1 if w != 0 else width - 1
			right = w + 1 if w != width - 1 else 0
			down = h - 1 if h != 0 else height - 1
			up = h + 1 if h != height - 1 else 0

			alives \
				= prev_chan[left + up * width] \
				+ prev_chan[left + h * width] \
				+ prev_chan[left + down * width] \
				+ prev_chan[w + up * width] \
				+ prev_chan[w + down * width] \
				+ prev_chan[right + up * width] \
				+ prev_chan[right + h * width] \
				+ prev_chan[right + down * width]

			idx = w + h * width
			state = prev_chan[idx]

			chan[idx] = 1 if \
				((state == 0 and alives == 3) or\
				(state == 1 and (alives == 2 or alives == 3))) else 0

	return
```

追加したパラメータには縦横それぞれのセル数を入れるようにします。
![script.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/2180deee-694c-7140-b54e-1e821ce1523b.png)


# 各セルの位置と色の計算

![network_cell.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/46c34884-4f06-8bf5-fc81-cb81fb87cc77.png)
各セルの位置と色を計算しています。位置はSOP to CHOPで作成したものをそのまま用いています。色はLookup CHOPを用いて0のときは黒、1のときは緑が割り当てられるようにしています。


# インスタンシングを用いた描画

![network_render.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/d86f88f6-e623-c3c4-7cb0-3c24cecb7ea2.png)
インスタンシングを有効にして、先に作成した各Channelを位置と色をパラメータに割り当てます。ここ以降は通常のインスタンシングを用いた描画と同じなので説明を割愛します。
![instancing.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/a793e2ea-57d4-6577-94d1-33d1f4caf4cc.png)

# 終わりに

Feedback CHOPを使ってライフゲームを実装してみました。Feedback CHOPとScript CHOPを使う方法は他のシミュレーションでも有効そうです。

ただし、Operator Snippetsの「GLSL TOP」のサンプルとしてGLSL TOPとFeedback TOPを使ったライフゲームの実装があるので、実用的にはそちらを参考にしたほうがいいと思います、GPUで計算したほうが効率がいいはずなので。