Reaction-Diffusionのパラメータ空間を探索する — Gray-Scottモデルの4つの顔
このトピックを深掘りする
クリエイティブコーディング
ブラウザでの数学的シミュレーション・自然現象の可視化・生成アートの実装技法
全12本中 7 本目。
次に読む記事
砂山モデルと自己組織化臨界 — BTWモデルをブラウザで動かす
Bak-Tang-Wiesenfeld砂山モデルの実装を通じて、自己組織化臨界現象をブラウザで可視化する。べき乗則に従う雪崩分布の計測とフラクタルパターンの生成過程を解説。
二重振り子のカオスを数値で見る — RK4積分と初期値鋭敏性の実装
二重振り子シミュレーションの実装を通じて、ラグランジュ力学による運動方程式の導出、RK4積分の安定性、NaN対策、初期値鋭敏性の可視化までを解説する。
Fourier級数で音を作る — 倍音構成と波形合成の実装
矩形波・鋸歯状波・三角波をFourier級数で合成し、エピサイクルで可視化する。倍音の加算が音色をどう変えるか、Web Audio APIによる音合成の実装を通じて体感する。
シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →
Gray-Scottモデルとは
ヒョウの斑点、シマウマの縞、珊瑚の分岐構造。自然界には驚くほど規則的なパターンが至るところにある。これらのパターンは遺伝子に「ここに黒い点を描け」と書いてあるのだろうか。1952年、アラン・チューリングは「The Chemical Basis of Morphogenesis」という論文で、まったく異なる仮説を提示した。2つの化学物質の拡散と反応だけで、自発的にパターンが生まれる。
Gray-Scottモデルは、このチューリングの理論を2つの偏微分方程式で表現したものだ。登場人物は2つの化学物質だけ:
- u(活性化因子): 反応の「原料」。外部から一定速度で供給される
- v(抑制因子): 反応の「生成物」。uと反応して増殖し、一定速度で除去される
方程式はこうなる:
∂u/∂t = Du·∇²u - u·v² + f(1 - u)
∂v/∂t = Dv·∇²v + u·v² - (f + k)·v各項の意味を分解する:
- Du·∇²u / Dv·∇²v: 拡散項。uとvが空間的に広がる。Duは1.0、Dvは0.5で、uのほうが速く拡散する。この非対称性が鍵だ
- u·v²: 反応項。uとvが出会うと、uが消費されてvが生成される。v²なので、vが多い場所ほど反応が加速する(自己触媒)
- f(1 - u): 供給項。fは「feed rate」。uが少ない場所ほど強く補充される
- (f + k)·v: 除去項。kは「kill rate」。vは自然に分解される
たった4行の数式。しかしこの4行から、生命のようなパターンが自己組織的に生まれる。
4つのプリセットパターン
Gray-Scottモデルの魅力は、fとkというたった2つのパラメータを変えるだけで、まったく異なるパターンが出現することだ。ReactionDiffusion コンポーネントには4つのプリセットを用意した。
Spots (f=0.055, k=0.062) — 散在する斑点
fが比較的高く、kも高い。原料の供給は潤沢だが、生成物の除去も速い。結果として、vの濃度が局所的に高まった「島」がポツポツと形成される。島は一定サイズまで成長すると安定し、それ以上広がらない。
直感的に言えば、供給と除去のバランスが「点を維持するがそれ以上は許さない」均衡点にある。ヒョウの斑点やキリンの模様を想像してほしい。各斑点は独立して存在し、互いに一定の距離を保つ。これは拡散による物質の「なわばり」が形成されるためだ。
画面に現れるのは、濃い青の背景に散らばる緑〜黄色の点。初期の種(中央の円形パッチ)から波紋のようにパターンが広がり、やがてグリッド全体が斑点で覆われる。
Stripes (f=0.035, k=0.065) — 平行な縞模様
fを下げる。原料の供給が絞られると、vは「点」として孤立できなくなる。代わりに、隣接するvの領域がつながって線状構造を形成する。kが高いので線幅は細く保たれ、結果として縞模様が現れる。
この遷移は非直感的だ。fを0.02下げただけで、空間パターンの次元が0次元(点)から1次元(線)に跳躍する。縞は最初ランダムな方向を向いているが、時間とともに整列し、平行に近づいていく。シマウマの体表を思い浮かべてほしい。
実際に画面で見ると、Spotsプリセットから切り替えた瞬間、点が溶け出して互いにつながり始める。数百フレーム後、画面は波打つ縞で埋まる。縞の間隔はほぼ均一で、これは拡散係数の比率(Du/Dv = 2.0)によって決まる特性波長だ。
Coral (f=0.060, k=0.062) — 珊瑚のような分岐構造
fをさらに上げる。原料が豊富に供給されるため、vの領域は活発に成長する。しかしkがSpotsと同じ0.062なので、成長したvは端から徐々に分解される。結果として、中心から外側へ分岐しながら伸びる構造が生まれる。珊瑚、あるいは雪の結晶のフラクタル的な美しさだ。
SpotsとCoralのkは同じ0.062。違いはfだけ(0.055 vs 0.060)。0.005の差が「安定した点」と「成長し続ける枝」を分ける。これがパラメータ空間の相境界であり、Reaction-Diffusionの最も面白い領域だ。
画面では、種から発した波紋が指のように分岐し、迷路のような構造を描く。枝の先端では活発な反応が起きており、黄色〜赤に光る。基部は安定して青緑。この色の勾配が時間的な履歴を映し出している。
Mitosis (f=0.0367, k=0.0649) — 細胞分裂のような増殖
最も生物的なプリセット。fが低く、kも微妙に低い。この領域では、斑点が安定サイズに達すると不安定になり、2つに分裂する。分裂した2つの点はそれぞれ成長し、再び分裂する。細胞のミトーシス(有糸分裂)そのものだ。
なぜ分裂するのか。fが低いので成長速度は遅い。しかしkも低いのでvが長く残存する。点がある程度大きくなると、中心部のuが枯渇する(供給が追いつかない)。中心のv濃度が下がり、点が「くびれ」て2つに割れる。
画面の動きは催眠的だ。ゆっくりと膨らむ点が、ある瞬間パッと2つになる。2つになった点がまたゆっくり膨らみ、また分裂する。指数的に増えていき、最終的にグリッドは均一なサイズの点で充填される。
実装の詳細
ReactionDiffusion.tsx は296行。数式を忠実にコードに落とすと、どこにどれだけの行数が使われるかがよく分かる。
グリッドとダブルバッファリング
const GRID = 200
const DU = 1.0
const DV = 0.5
const DT = 1.0
function createGrid(): Float32Array {
return new Float32Array(GRID * GRID)
}200×200 = 40,000セル。uとv、それぞれのnext(計算結果書き込み用)で4つのFloat32Arrayを使う。ダブルバッファリングは「読み込み元」と「書き込み先」を分離するための定番テクニックだ。もし単一のバッファで計算すると、左上のセルの更新結果が右下のセルの計算に影響してしまい、更新順序に依存した歪みが生まれる。
各フレームで6ステップのシミュレーションを回す:
const STEPS_PER_FRAME = 6
for (let s = 0; s < STEPS_PER_FRAME; s++) {
simulate(u, v, nu, nv, f, k)
// Swap buffers
const tmpU = u
const tmpV = v
u = nu
v = nv
nu = tmpU
nv = tmpV
}STEPS_PER_FRAME = 6 はCFL条件(Courant-Friedrichs-Lewy条件)に基づく安定性の要請ではなく、見た目の速度調整だ。DT = 1.0、Du = 1.0の場合、2D拡散方程式のCFL条件は Δt ≤ Δx² / (4·Du) で、格子間隔Δx = 1とすると Δt ≤ 0.25 が安定限界になる。しかし実際にはDT = 1.0でも安定する。これはGray-Scottの反応項が拡散を打ち消す方向に働くためで、純粋な拡散方程式より安定限界が緩い。
5点ラプラシアンステンシル
離散ラプラシアン ∇²u の近似。中心セルと上下左右4つの隣接セルを使う:
const lapU = u[y * n + xm] + u[y * n + xp]
+ u[ym * n + x] + u[yp * n + x]
- 4 * uC9点ステンシル(対角セルも含む)を使えば精度は上がるが、計算量が2倍以上になる。200×200×6ステップ×60FPS = 毎秒1億4400万回の加減算。5点で十分な精度が得られるなら、余計な計算は省く。
周期的境界条件
const ym = y === 0 ? n - 1 : y - 1
const yp = y === n - 1 ? 0 : y + 1
const xm = x === 0 ? n - 1 : x - 1
const xp = x === n - 1 ? 0 : x + 1グリッドの上端と下端、左端と右端がつながっている。数学的にはトーラス(ドーナツの表面)上でシミュレーションしていることになる。これにより境界でのアーティファクトを避けつつ、パターンが途切れなく連続する。
カラーLUT
v濃度を色に変換するルックアップテーブルを事前計算する:
const COLOR_LUT = new Uint8Array(256 * 4)
// dark blue -> cyan -> green -> yellow -> red5つの色停止点(dark blue, cyan, green, yellow, red)の間を線形補間し、256段階×RGBAの1024バイトのテーブルを作る。毎フレーム40,000ピクセルの色を計算するので、三角関数やべき乗をリアルタイムで計算するのは論外だ。テーブル参照なら1回のメモリアクセスで済む。
v値を0〜255のインデックスに変換する際、512倍してからビット論理和で整数化している:
const ci = Math.min(255, Math.max(0, (vVal * 512) | 0))512倍なので、v = 0.5 でインデックスが256(clampで255)になる。実際のパターンではv濃度は0〜0.5程度の範囲に収まるので、色空間をフルに使い切れる。
クリックで種を蒔く
ReactionDiffusionコンポーネントの最も直感的な体験は、キャンバスをクリック(またはタッチ)してパターンの「種」を蒔くことだ。
const SEED_RADIUS = 5
function seedAt(u: Float32Array, v: Float32Array, gx: number, gy: number) {
for (let dy = -SEED_RADIUS; dy <= SEED_RADIUS; dy++) {
for (let dx = -SEED_RADIUS; dx <= SEED_RADIUS; dx++) {
if (dx * dx + dy * dy > SEED_RADIUS * SEED_RADIUS) continue
const x = gx + dx
const y = gy + dy
if (x >= 0 && x < GRID && y >= 0 && y < GRID) {
const idx = y * GRID + x
u[idx] = 0.5 + Math.random() * 0.02
v[idx] = 0.25 + Math.random() * 0.02
}
}
}
}ポインタ位置に半径5セルの円形パッチを注入する。u=0.5、v=0.25 に微小なランダム揺らぎを加えることで、対称性を破り、各注入点から異なるパターン成長が始まる。
この機能の本質は「パラメータ空間の"相図"の中を散歩する感覚」だ。プリセットでf/kを選び、クリックで新しい核を生成し、パターンが広がるのを眺める。Spotsを選んでクリックすれば点が広がり、Stripesなら縞が走る。同じ操作でまったく異なる結果が得られることに、Reaction-Diffusionの本質がある。
ポインタイベントはonPointerDown/Move/Upで処理し、キャンバスの座標からグリッド座標への変換はアスペクト比を維持したまま行う:
const dim = Math.min(rect.width, rect.height)
const ox = (rect.width - dim) / 2
const oy = (rect.height - dim) / 2
pointerPosRef.current = {
x: ((e.clientX - rect.left - ox) / dim) * GRID,
y: ((e.clientY - rect.top - oy) / dim) * GRID,
}正方形のシミュレーション領域をキャンバスの中央に配置し、余白を考慮した座標変換を行っている。
パラメータ感度
Reaction-Diffusionの最も驚くべき性質は、パラメータの微小な変化が質的に異なるパターンを生み出すことだ。
SpotsとCoralを比較する。kは同じ0.062。fだけが0.055と0.060で異なる。この0.005の差は、パラメータ空間上ではごく狭い距離だ。しかし結果は「安定した点」と「成長する枝」というまったく異なるトポロジーになる。
Stripesの f=0.035 をほんの少し上げて f=0.036 にすると、縞の一部がちぎれて点になり始める。f=0.037 では縞と点が混在する。f=0.038 では点が優勢になる。わずか0.001刻みで遷移が起きる。
この感度はカオスとは本質的に異なる。二重振り子のカオスは「初期条件の微小な差が時間とともに指数的に拡大する」現象だ。Reaction-Diffusionのパラメータ感度は「パラメータの微小な差が異なるアトラクタを選択する」現象だ。前者は予測不可能性、後者は分岐(bifurcation)だ。
重要なのは、同じパラメータで同じ初期条件から始めれば、毎回同じパターンが再現されること。決定論的で再現可能。しかしfを0.001動かすだけで、再現されるパターンが質的に変わる。この「再現可能だが敏感」という性質が、Reaction-Diffusionを数学的に面白くしている。
f/kの2次元パラメータ空間を地図に見立てると、そこには「Spots国」「Stripes国」「Coral国」「Mitosis国」が存在し、国境線は驚くほど細い。プリセットの4点は、それぞれの国の「首都」のような安定した代表点だ。国境付近のパラメータを選ぶと、複数のパターンが混在するカイメラ状態が現れることもある。
チューリングの先見性
1952年の論文「The Chemical Basis of Morphogenesis」で、チューリングは「動物の模様は、化学物質の拡散と反応によって自発的に形成される」と主張した。この主張は3つの理由で先見的だった。
第一に、当時はDNAの二重らせん構造すら発見されていなかった(ワトソンとクリックの論文は1953年)。遺伝子がどうやって形態を制御するのか、まったく分かっていない時代に、「化学反応だけで模様ができる」と言い切った。
第二に、チューリングの理論は数値的な検証を前提としていた。偏微分方程式の解を解析的に求めることは困難で、コンピュータによるシミュレーションが必要だった。しかし1952年のコンピュータ(Manchester Mark I)では、200×200グリッドのリアルタイムシミュレーションは夢物語だった。
第三に、チューリングの理論が実験的に確認されたのは、論文から半世紀以上経ってからだ。2006年にキョンの毛包細胞でチューリングパターンが確認され、2012年にはマウスの口蓋で反応拡散メカニズムによるしわ形成が実証された。
2026年の今、ブラウザの中で200×200グリッドを毎秒60フレームで回せる。Canvas API上のFloat32Arrayのメモリ確保から色変換まで、すべてがJavaScriptで完結する。チューリングが頭の中で想像するしかなかったパターンを、クリック一つで生成し、パラメータを変えながら散歩できる。この体験そのものが、74年の技術進歩の証だ。
まとめ:Reaction-Diffusionの教訓
Reaction-Diffusionを実装して得た最大の気づきは、複雑さは複雑なルールから生まれるとは限らないということだ。
数式は4行。パラメータは実質2つ(fとk)。実装は296行。数式と実装の差分は何か。境界条件の処理、ダブルバッファリング、カラーLUT、ポインタインタラクション、requestAnimationFrameによるキャンバスへのレンダリング。つまり「計算を人間に見せるための変換層」が全体の9割以上を占める。
この比率は示唆的だ。自然法則は簡潔だが、それを観測可能にするための装置は複雑になる。ブラウザでのシミュレーションも同じ構造を持っている。
もう一つの教訓。パラメータ空間の探索は、コードを書くこと以上に時間がかかった。f=0.055, k=0.062 という数値は、何十回もの試行錯誤から見つけた「美しいパターンが出る点」だ。数式を実装するのは1時間で終わる。美しいパラメータを見つけるのには半日かかった。数学とアートの境界は、パラメータの選択にある。
2つのパラメータだけで、生命のようなパターンが生まれる。この事実を画面で目撃する体験は、方程式を読むだけでは決して得られない。