Blenderのコンポジットノードでライフゲームする

> この記事は [Blender Advent Calender 2017](https://adventar.org/calendars/2391) 12/17 分の記事です。


# Blenderのコンポジットノードでライフゲームする

## はじめに

[Blenderコンポジットの小ネタ集 > 直前のフレームのコンポジット出力を利用する](https://qiita.com/nacasora/items/cdd504a7fa3c6962481e#%E7%9B%B4%E5%89%8D%E3%81%AE%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%9D%E3%82%B8%E3%83%83%E3%83%88%E5%87%BA%E5%8A%9B%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B) にあるように、Blenderのコンポジットノードは現在よりも前のフレームの出力結果に応じて次の出力結果を変化させることが可能です。

前のフレーム(時間)における画像(セル)から次のフレームの画像を決定できるというのは[セル・オートマトン](https://ja.wikipedia.org/wiki/%E3%82%BB%E3%83%AB%E3%83%BB%E3%82%AA%E3%83%BC%E3%83%88%E3%83%9E%E3%83%88%E3%83%B3)に通じるものがあります。セル・オートマトンといったら[ライフゲーム](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)が代表的です。ひょっとしたら、このテクニックを利用すれば一切コードを書かずともBlenderのコンポジットノード上で動作するライフゲームが実装できるのでは？


## 結果

できました

![1.gif](https://qiita-image-store.s3.amazonaws.com/0/152179/d7125e4f-08aa-2ee3-898c-683b4d4ec2e6.gif)

入力画像から次のフレームの画像を計算し出力、その結果からさらに次のフレームを計算…の繰り返しをコンポジットノードだけで完結できています。プログラムのコードは一切書いていません。

ノードの全体図はこちら

![entire.png](https://qiita-image-store.s3.amazonaws.com/0/152179/90230bf5-6e9d-9f20-5733-3fb5ee4cd8e3.png)

## 実装

### ライフゲームのルール

まず初めにライフゲームのルールを確認しておきます。
[Wikipediaのライフゲームの項](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)を見ると、大まかに次のようなルールになっています：

- グリッド状にセルが並んでいる
- 各セルは生きているか死んでいるかの2つの状態を持つ
- 各セルは、自身の生死の状態と、自身の周囲8マスの生きているセルの個数($=n$)で次の生死が決定される
  - **誕生** : セルが死んでいる＆ $n=3$ ならば、次の世代では生きているセルになる
  - **生存** : セルが生きている＆ $2 \leq n \leq 3$  ならば、次の世代でも生きたまま
  - **過疎** : セルが生きている＆ $ n \leq 1 $ ならば、次の世代ではセルは死ぬ
  - **過密** : セルが生きている＆ $ 4 \leq n $ ならば、次の世代ではセルは死ぬ

この条件を満たすようにノードを組んでいきます。

### 入力画像

入力画像を用意します。画像は白黒の2値画像で、1ドットがライフゲーム上の1つのセルを表します。黒が死んでいるセルで白が生きているセルです。
今回は64x64のサイズの画像にします。原寸は↓の通り

![2.png](https://qiita-image-store.s3.amazonaws.com/0/152179/f22786dc-55b3-a7c9-0d2b-15e9da0a43e6.png)

またレンダリング解像度を画像サイズに合わせておきます。

### ノードを組む

#### 現在のセルを表す画像を取得

[Blenderコンポジットの小ネタ集 > 直前のフレームのコンポジット出力を利用する](https://qiita.com/nacasora/items/cdd504a7fa3c6962481e#%E7%9B%B4%E5%89%8D%E3%81%AE%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%9D%E3%82%B8%E3%83%83%E3%83%88%E5%87%BA%E5%8A%9B%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B)にある通りにImage SequenceノードとFile Outputノードをセットアップします。また入力画像(`input image`)もImageノードとして読み込み、Image Sequenceの方のImageノード(`prev result`)と次のように組み合わせておきます：

<img src="https://qiita-image-store.s3.amazonaws.com/0/152179/2cd9cdb8-8fad-8819-40da-cab2f1087909.png" height="200">

`input image`の上に`prev result`をAlpha Overノードで重ねています。`prev result`は一番最初のフレームでは内容が空、2フレーム以降ではノンアルファな画像を出力するようになるので、`current cell`の画像は1フレーム目に限って`input image`を指し、2フレーム目以降では`prev result`の画像を指すようになります。

---

#### 周囲のセルを参照する

`current cell`は入力画像中のある1ピクセルを指しています。この周囲のピクセルの情報を得るには`Translate`ノードを使います。

![tr1.jpg](https://qiita-image-store.s3.amazonaws.com/0/152179/9a70d146-a782-0801-08be-b21643f1ba29.jpeg)

![tr2.gif](https://qiita-image-store.s3.amazonaws.com/0/152179/0fd5faf8-3d5a-cb44-3e03-64e7f7998878.gif)


Translateノードは画像を指定したピクセル分だけスライドしてくれるので、例えば`(X,Y)=(1,1)`だけスライドすれば対岸の`(-1,-1)`にある箇所が現在のピクセルに重なることになります。上の画像では`X=-1, 0, 1` `Y=-1, 0, 1` の組み合わせの9通りについてTranslateノードを用意しています。ただ`(X,Y)=(0,0)`は移動しない無意味な処理なのでノードを通す必要は無いのですが、目印がてらミュートだけしています。

またTranslateノードの`Wrapping`を`Both Axes`にしておけば、ライフゲームに都合が良いことに上下左右でループしたスライドを行ってくれます。

ライフゲームでは現在のセルの生死の状態と、周囲8マスの生きているセルの総数のみが必要な情報になるので、それを用意します。現在のセルの状態はそのまま`current cell`の値が対応しています。周囲8マスの生きているセルの総数は、上で用意した8つのTranslateノードの結果を全て足し合わせたものがそれに対応します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/152179/d2bf74a6-ff19-cc8d-10ba-0cd1e387c090.png" height="200">


#### 条件を計算

ライフゲームのルールで挙げられているそれぞれの条件に従って計算を行います。セルが条件を満たすなら値は1(：真)に、満たさないなら0(：偽)になります。ただ今回は4つあるルールのうち **誕生** と **生存** の場合だけ計算すれば十分です。(後述)

<img src="https://qiita-image-store.s3.amazonaws.com/0/152179/fca9d0b0-12a1-4040-09ce-63e8bf59ffca.png" height="200">

周囲の生きているセルの総数 $n$ について $n=3$ と $2 \leq n \leq 3$ を計算しています。Mathノードの`Greater Than`もしくは`Less Than`は上ソケットに繋がれている値 $n$ 下ソケットの値 $x$ に対し $ n > x $ もしくは $ n < x $ を満たす場合に`1`を、そうでないなら`0`を返します。Greater/Less両方の結果をMultiplyでかけ合わせれば 「 $ n > x $ 且つ $ n < y $ 」 が表現できます。

今回は $n = 3$ を表すために`3`よりも少し値を増減させて $2.9 < n < 3.1$ の結果で代用しています。また $2 \leq n \leq 3$ の代用として $1.9 < n < 3.1$ の結果を使用しています。

さて、**誕生**のルールは 「『セルが死んでいる』 且つ $n=3$ 」 でした。`is live cell`の値はセルが生きている場合に
`1`を返していたため、逆に「セルが死んでいる」場合に1を返すようにするにはInvertノードで値を0←→1で反転させれば目的の結果が得られます。Invertしたことで「セルが死んでいる」という条件が得られたので、これを「$n=3$」の条件とMultiplyノードで掛け合わせることで**誕生**のルールを満たす場合に1を返すセルが取得できます。

**生存**のルールも同様で、「セルが生きている」と「 $2 \leq n \leq 3$ 」とを掛け合わせることで表現できます。


#### 次フレームのセルを出力

**誕生**と**生存**で得られたセルを足し合わせることで次フレームで生きているセルを取得できます。

今回は次フレームのセルを用意するにあたって、デフォルトで値が0である(死んでいる)セルの上に生きているセルを追加していく形になっていたこと、またライフゲームの4つのルールはどの条件も排反であったことから、**過疎**と**過密**のルールについては計算する必要はありませんでした。

ともかくこれで1フレーム間のセルの更新は完了です。

<img src="https://qiita-image-store.s3.amazonaws.com/0/152179/132fd273-95d7-f8cd-f39c-1b026b066abd.png" height="200">

最終結果を以前用意したFile Outputノードに繋ぎます。
色でなく値をそのままFile Outputに繋ぐと、出力するファイル形式によってはアルファ値にその値が設定された画像が出力されてしまうので、ノンアルファな2値画像で出力するためにMixノードを通してから最終的な出力に繋いでいます。

#### 以降繰り返し

Render > `Animation`ボタンで1フレーム目から順に上の計算が行われていき、1フレームごとに更新されたセルが出力されていきます。

あらためてノードの全体像です。ViewerとCompositeノードは`current cell`に繋いでおきます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/152179/90230bf5-6e9d-9f20-5733-3fb5ee4cd8e3.png" height="200">

これでコンポジットノードだけでライフゲームが実現できました。


## まとめ

- 3DCG？それBlenderでできるよ
- コンポジット？それBlenderでできるよ
- 動画編集？それBlenderでできるよ
- [画像処理？それBlenderでできるよ](https://qiita.com/nacasora/items/cf0e27d38b09654cf701)
- ライフゲーム？それBlenderでできるよ ←<font color="Red">New!!</font>



---

この記事は [Blender Advent Calender 2017](https://adventar.org/calendars/2391) 12/17 日目の記事でした。
次回はMcLion0さんによる「『BlenderAutotrackerAddon』を使った感想とか解説とか。（仮）」です。