feature image

2022年5月31日 | ブログ記事

ライフゲームで盆栽

ライフゲーム

ライフゲームとは

知ってる人は下までスキップ推奨です。

ここに"世界"があります。

ライフゲームの"世界"

この世界は格子状のマス目で構成されており、各マスを"セル"(細胞)と呼びます。

各セルは2つの状態を持っています。"生"と"死"です。ここでは生きているセルを紫で、死んでいるセルを白で表します。

セルの2状態 生と死

この世界に時間の概念を持ち込みましょう。時間が1つ経過する(次の世代)と、各セルは一定のルールに従って状態が変化します。

状態遷移のルール
元の状態 隣接している生きたセルの数 次の世代での状態 アナロジー
2 / 3 生存
0 / 1 / 4 / 5 / 6 / 7 / 8 過疎/過密死
3 繁殖
0 / 1 / 2 / 4 / 5 / 6 / 7 / 8 過疎/過密死

ここで、「隣接している生きたセルの数」の"隣接"は、上下左右と斜め方向に隣り合ったセルを表しています。

ある生きたセルの周りに、2個または3個の生きたセルが隣接する場合、そのセルは次の世代でも生きたままです。
逆に、1個以下または4個以上の生きたセルが隣接する場合、そのセルは次の世代で死んでしまいます。
ご近所さんは少なすぎず・多すぎずがちょうどいい、という感じです。

ある死んだセルの周りに、ちょうど3個の生きたセルが隣接する場合、そのセルは次の世代で生きたセルに変化します。
適切な細胞の量により、細胞が増殖した、みたいなイメージです。

上記の状態遷移に従って、すべてのセルの状態を同時に更新することで、下図のようなアニメーションが得られます。

参考

以上の説明でよくわからなかった方はライフゲームの世界の動画シリーズやWikipediaをご覧ください。

ライフゲーム - Wikipedia

縦、横、過去

ここまで前提。ここから本題です。

上でも説明した通り、ライフゲームは状態変化の連続です。しかし、よく目にするライフゲームは、二次元上で各セルの現在の状態を示しており、状態変化の履歴、つまり過去の状態がわかりにくいものとなっています。

ですので次元を上げましょう。3Dです。

kako

本記事では、「過去の状態がわかりやすい、3Dでのライフゲーム表示」を目指します。

Blenderで表示する

今回はBlenderという無料3DCGソフトウェアを利用して目標を達成します。

Blenderとは

公式ページ:blender.org - Home of the Blender project - Free and Open 3D Creation Software

blender.org - Home of the Blender project - Free and Open 3D Creation Software
The Freedom to Create

Blenderは統合型の3DCG制作ソフトウェアです。

インストール方法はBlenderのインストール・日本語化 | traP 3DCG体験会で詳しく説明しています。

Blenderのインストール・日本語化 | traP 3DCG体験会
東京工業大学デジタル創作同好会traPが主催する、新入生向け3DCG体験会の資料ページです。座学編で3DCGについて俯瞰的に学び、実習編で簡単なアニメーションを作成します。

BlenderでのPythonの実行

Scripting — blender.org

BlenderではPythonによるスクリプティングが可能です。これにより、オリジナルの機能を持ったアドオンを作成したり、オブジェクトの動的な追加等を行うことができます。

python_in_blender

これを利用し、

  1. Pythonでライフゲームを実装する
  2. 生セルに対応したオブジェクトを追加する

をすることで本記事の目標が達成できそうです。

Pythonでのライフゲームの実装

まずはPythonでライフゲームを実装しましょう。

import time
from typing import Any

import numpy as np


class LifeGame():

    __kernel = np.array([[1, 1, 1],
                         [1, 0, 1],
                         [1, 1, 1]])

    def __init__(self, size: tuple[int, int], rule: tuple[list[int], list[int]] = ([2, 3], [3]), loop: bool = True) -> None:
        self.__size = size[::-1]
        self.__cells = np.zeros(self.__size, dtype=np.int8)
        self.__rule = rule
        self.__loop = loop

    def __str__(self) -> str:
        return "\n".join(["".join(["*" if cell else " " for cell in row]) for row in self.cells])

    @property
    def cells(self):
        return self.__cells

    @cells.setter
    def cells(self, map: np.ndarray[Any, np.dtype[np.int8]]):
        if (map.shape != self.__cells.shape):
            raise ValueError("map size is not match")
        if (map.dtype != self.__cells.dtype):
            raise ValueError("map type is not match")
        self.__cells = map

    def random(self) -> None:
        self.cells = np.random.randint(0, 2, size=self.__size, dtype=np.int8)

    def update(self) -> None:
        neighbours = self.__convolve2d(self.__cells, self.__kernel)
        survives = self.cells & (np.logical_or.reduce(
            [neighbours == survive_rule for survive_rule in self.__rule[0]]
        ))
        born = ~self.cells & (np.logical_or.reduce(
            [neighbours == born_rule for born_rule in self.__rule[1]]
        ))
        next = survives | born
        self.cells = next

    # see https://qiita.com/secang0/items/f3a3ff629988dc660d87
    def __convolve2d(self, img, kernel):
        mode = "wrap" if self.__loop else "constant"
        pad = np.pad(img, 1, mode=mode)

        # 部分行列の大きさを計算
        sub_shape = tuple(np.subtract(pad.shape, kernel.shape) + 1)

        # 部分行列の行列を作成
        submatrices = np.lib.stride_tricks.as_strided(
            pad, kernel.shape + sub_shape, pad.strides * 2
        )

        # 部分行列とカーネルのアインシュタイン和を計算
        convolved_matrix = np.einsum('ij,ijkl->kl', kernel, submatrices)

        return convolved_matrix


if __name__ == "__main__":
    game = LifeGame((24, 24))
    game.random()
    for i in range(50):
        print("-" * 24)
        print(game)
        game.update()
        time.sleep(0.2)

しました。これを実行することで、(Blender上でも)コンソールでライフゲームが始まると思います。

この記事の主題は「ライフゲームの状態遷移を3Dで表示したらイイ感じになるんじゃね?」なので細かいスクリプト解説は飛ばします。要望があれば追記します...

bpyによるオブジェクト追加

次はPythonからBlenderを操作して、オブジェクトの追加をしてみましょう。BlenderをPythonから制御するためのAPI(bpy)が用意されているのでこれを利用します。

公式ドキュメント:Blender 3.1 Python API Documentation — Blender Python API

import bpy

verts = [(-1, -1, -1), (1, -1, -1), (1, 1, -1), (-1, 1, -1),
         (-1, -1, 1), (1, -1, 1), (1, 1, 1), (-1, 1, 1)]
faces = [(0, 1, 2, 3), (4, 5, 6, 7), (0, 1, 5, 4),
         (2, 3, 7, 6), (0, 3, 7, 4), (1, 2, 6, 5)]


# create mesh
cube_mesh = bpy.data.meshes.new(name="cube")
cube_mesh.from_pydata(verts, [], faces)

# create object
cube_obj = bpy.data.objects.new(name="cube_from_python", object_data=cube_mesh)
cube_obj.data = cube_mesh

# link object to scene
scene = bpy.context.scene
scene.collection.objects.link(cube_obj)

例えば上のようなスクリプトを実行することで、シーンに立方体を追加することができます。ここでは頂点座標の配列と、面を構成する頂点のインデックスの組の配列からメッシュを構成しています。

これを応用して、ライフゲームにおける"生きたセル"を、Blenderにおける"頂点(vertex)"に対応させてみましょう。

import time
from typing import Any

import bpy # <- 追加
import numpy as np


class LifeGame():

    # 省略

        return convolved_matrix

# ここから追加
def map_to_verts(map: np.ndarray[Any, np.dtype[np.int8]], z: int) -> list[tuple[int, int, int]]:
    return list(zip(*np.where(map == 1), [z for _ in range(map.shape[0]*map.shape[1])]))


if __name__ == "__main__":
    game = LifeGame((24, 24))
    game.random()
    max_z = 24

    verts = []

    for z in range(max_z):
        verts += map_to_verts(game.cells, z)
        game.update()

    # create mesh
    cells_mesh = bpy.data.meshes.new(name="cells")
    cells_mesh.from_pydata(verts, [], [])

    # create object
    cells_obj = bpy.data.objects.new(name="cells", object_data=cells_mesh)
    cells_obj.data = cells_mesh

    # link object to scene
    bpy.context.scene.collection.objects.link(cells_obj)

対応させました。これを実行することで、シーンに頂点だけで構成されたオブジェクトが追加されます。

cells_in_3d

X軸とY軸(画像内の赤/緑線)が各世代の"世界"の横軸と縦軸を、
Z軸が世代数を表しています。上に行くほど世代が進んでいるイメージです。

...が、これではまだ分かりづらいですね🤔

Geometry Nodesによる装飾

頂点による表示だけでは分かりづらいので、各頂点位置に、わかりやすい色/形のオブジェクトを設置してみましょう。これまでのようにPythonによる追加もできますが、ここではより簡単に扱うことのできるGeometry Nodesを利用してみます。

Geometry Nodesは、オブジェクトのジオメトリ(頂点や辺、面など)を、ノードベースの操作により変更するシステムです。

公式ドキュメント:Geometry Nodes — Blender Manual

例えば上図左のようなノードを組むことで、元のオブジェクトの各頂点位置に、任意のオブジェクトを設置することができます。画像の例では立方体の各頂点に"Suzanne"というサルのモデルを設置しています。

これと同様に、ライフゲームでの生セルに対応した頂点に、オブジェクトを設置してみます。
まず、各生セルを表す簡単なオブジェクトを作成しました。つやつやな立方体です。

cell

先ほど例示したGeometry Nodesを使用して、この立方体を頂点位置に配置してみましょう。

replace_vertexes

Z軸、つまり時間経過に従って、色相が変化するようマテリアルを設定しています。

最後に、世代更新時にセルが生まれるようなアニメーションを作成してみました。

Geometry Nodes内で、アニメーションの経過時間と各セルのZ座標(=世代)からサイズを算出する処理を行って実現しています。ライフゲームの状態遷移がわかりやすくなりましたね。

これで目標達成です。ライフゲームの核である"状態遷移"を、PythonとBlenderによる3DCG作成によりうまく表現できました🎉

盆栽

上の動画を眺めていると、ライフゲームにおけるセルの状態遷移が、植物の成長のように見えてきました。ということで、もっと植物っぽくしてみましょう。

盆栽っぽくする

植木鉢を用意します。

pod

種を植えます。

seed

世代を進めます。

育ちました。

かわいいですね。

別の初期状態で少しだけ生やしてみましょう。

かっこいいですね。

いかがでしたか

BlenderとPythonを組み合わせることで、3DなGenerative Artを比較的簡単に作成できることがお分かりいただけたかと思います。皆さんもぜひ色々試してみてください。

d_etteiu8383 icon
この記事を書いた人
d_etteiu8383

グラフィック班とゲーム班とSysAd班所属 いろいろ活動しています

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2023年11月21日
School Breakin' Tag -新感覚おにごっこ-
s9 icon s9
2023年4月17日
ポケモンを飼いたい夢を叶える
tqk icon tqk
2023年3月20日
traPグラフィック班の活動紹介(Ver.2023)
NABE icon NABE
2022年4月7日
traPグラフィック班の活動紹介
annin icon annin
2021年3月19日
traPグラフィック班の活動紹介
NABE icon NABE
2024年3月22日
traPグラフィック班の活動紹介2024
haru10 icon haru10
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記