セルオートマトンの世界 — ライフゲームから波動関数崩壊まで
このトピックを深掘りする
クリエイティブコーディング
ブラウザでの数学的シミュレーション・自然現象の可視化・生成アートの実装技法
全12本中 1 本目。
次に読む記事
ブラウザで流体シミュレーション — 100×60グリッドで実現するリアルタイム演算
Navier-Stokes方程式を簡略化し、100×60のグリッドでリアルタイムに流体を動かす。拡散・移流・圧力ソルバーの実装詳細。
フラクタルの数学をブラウザで描く — マンデルブロ集合からバーンズリーのシダまで
複素数反復、エスケープタイムアルゴリズム、IFS(反復関数系)、再帰描画を使い、ブラウザでフラクタルを描画する実装解説。
Perlinノイズで自然な地形を生成する — ランダムなのに美しい理由
勾配ノイズの数学的な仕組みからオクターブ合成、Canvas上での3Dメッシュ描画まで、Perlinノイズによる自然な地形生成をステップごとに実装・解説する。
シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →
セルオートマトンの共通構造
GameOfLife、SandFall、WaveFunctionCollapseは見た目がまったく違うが、Canvas API上で実装した共通の設計パターンがある:
- グリッド: 2D配列に各セルの状態を格納
- 近傍ルール: 隣接セルの状態を参照して次の状態を決定
- 一括更新: 全セルの次状態を計算してから、一括で書き換える
GameOfLife — 生命のルール
コンウェイのライフゲームはたった4つのルールで驚くほど複雑な振る舞いを見せる:
const W = 200
const H = 150
let grid = new Uint8Array(W * H)
let nextGrid = new Uint8Array(W * H)
function stepLife() {
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
let neighbors = 0
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0) continue
const nx = (x + dx + W) % W
const ny = (y + dy + H) % H
neighbors += grid[ny * W + nx]
}
}
const idx = y * W + x
const alive = grid[idx]
if (alive) {
nextGrid[idx] = neighbors === 2 || neighbors === 3 ? 1 : 0
} else {
nextGrid[idx] = neighbors === 3 ? 1 : 0
}
}
}
// バッファスワップ(コピーなし)
const temp = grid
grid = nextGrid
nextGrid = temp
}Uint8Arrayを使う理由は2つ: メモリ効率(1セルあたり1バイト)と、JITコンパイラが整数演算に最適化しやすいこと。(x + dx + W) % W はトーラス境界(右端が左端に繋がる)。
バッファスワップの重要性
grid を直接上書きすると、更新済みのセルが未更新セルの近傍計算に影響する。ダブルバッファリングで解決する。上のコードではコピーではなく参照を入れ替えているので、コスト0。
SandFall — 落下シミュレーション
SandFallは重力に従って粒子が落下するオートマトン。ルールはシンプル:
type Material = 0 | 1 | 2 // 0: 空気, 1: 砂, 2: 水
function stepSand(grid: Uint8Array, w: number, h: number) {
// 下から上にスキャン(重力方向の処理順が重要)
for (let y = h - 2; y >= 0; y--) {
for (let x = 0; x < w; x++) {
const idx = y * w + x
if (grid[idx] !== 1) continue
const below = (y + 1) * w + x
const belowLeft = (y + 1) * w + (x - 1)
const belowRight = (y + 1) * w + (x + 1)
if (grid[below] === 0) {
// 真下が空なら落下
grid[below] = 1
grid[idx] = 0
} else if (x > 0 && grid[belowLeft] === 0) {
// 左下が空なら斜め落下
grid[belowLeft] = 1
grid[idx] = 0
} else if (x < w - 1 && grid[belowRight] === 0) {
// 右下が空なら斜め落下
grid[belowRight] = 1
grid[idx] = 0
}
}
}
}下から上にスキャンするのが重要。上からスキャンすると、1回のステップで粒子が複数セル落下してしまう。
水の場合は左右にも流れるルールを追加する。たった数行の追加で「液体」と「固体」の挙動の違いが表現できる。
WaveFunctionCollapse — 制約伝播
WFC(波動関数崩壊)はプロシージャル生成アルゴリズム。各セルが「取りうる状態の集合」を持ち、観測と制約伝播を繰り返す:
type Cell = {
collapsed: boolean
options: Set<number>
}
function findLowestEntropy(cells: Cell[][]): [number, number] | null {
let minEntropy = Infinity
let candidates: [number, number][] = []
for (let y = 0; y < cells.length; y++) {
for (let x = 0; x < cells[y].length; x++) {
if (cells[y][x].collapsed) continue
const entropy = cells[y][x].options.size
if (entropy < minEntropy) {
minEntropy = entropy
candidates = [[x, y]]
} else if (entropy === minEntropy) {
candidates.push([x, y])
}
}
}
if (candidates.length === 0) return null
return candidates[Math.floor(Math.random() * candidates.length)]
}
function collapse(cells: Cell[][], x: number, y: number) {
const options = Array.from(cells[y][x].options)
const chosen = options[Math.floor(Math.random() * options.length)]
cells[y][x].options = new Set([chosen])
cells[y][x].collapsed = true
}
function propagate(cells: Cell[][], rules: Map<number, Set<number>[]>) {
const stack: [number, number][] = []
// ... 制約を近傍に伝播
// 隣接セルの選択肢を互換性ルールで削減
}WFCのポイントは最小エントロピーセルを先に確定すること。選択肢が少ないセルを先に決めることで、矛盾の発生を抑える。
ImageDataによる高速描画
30,000セル(200x150)を毎フレーム描画するには、fillRect ではなく ImageDataを直接操作する:
function renderGrid(ctx: CanvasRenderingContext2D, grid: Uint8Array, w: number, h: number) {
const imageData = ctx.createImageData(w, h)
const data = imageData.data
for (let i = 0; i < w * h; i++) {
const px = i * 4
const cell = grid[i]
if (cell === 1) { // 砂
data[px] = 194
data[px + 1] = 178
data[px + 2] = 128
} else if (cell === 2) { // 水
data[px] = 64
data[px + 1] = 164
data[px + 2] = 223
}
data[px + 3] = 255
}
ctx.putImageData(imageData, 0, 0)
}putImageData は1回のGPU転送で全ピクセルを更新する。セルごとに fillRect を呼ぶのと比べて桁違いに速い。
まとめ:セルオートマトン実装の道具たち
セルオートマトンと複雑系の関係を深く理解するなら、道具棚の書籍が最適。単純なルールから複雑な振る舞いが生まれるメカニズムは、プログラミング全般の設計思想に通じる。