モバイル&ワイヤレスブロードバンドでインターネットへ

gwaw.jp
 
GPGPU × TEXTURE · 技術ノート

計算が、生命の模様になる
— 反応拡散系をWebGPUで

ヒョウ柄、サンゴ、指紋、貝殻の模様。自然界の複雑な紋様は、たった2つの仮想物質が「反応」し「拡散」するだけの単純なルールから生まれる。チューリングが予言したこの形態形成を、WebGPUのテクスチャ計算で再現する。計算結果がそのまま画面全体の絵になる——粒子描画とは異なる、もう一つのGPGPUの姿を解説する。

WebGPU反応拡散系Gray-Scott storage textureダブルバッファチューリングパターン

01単純なルールが、複雑な模様を生む

ヒョウの斑点、シマウマの縞、熱帯魚の紋様、サンゴの形——自然界には複雑で美しい模様があふれている。これらはどうやって生まれるのか。設計図があるわけではない。驚くべきことに、ごく単純な局所ルールの繰り返しから、これらの模様は自発的に立ち現れる。

1952年、アラン・チューリングは「形態形成の化学的基礎」という論文で、2つの物質が反応しながら拡散すると、空間に安定した模様(チューリングパターン)が生まれることを数学的に示した。本稿で扱う反応拡散系は、その代表的なモデルだ。これまでの実験ノートで扱ってきた「計算」を、画面全体の生きた模様として描き出す。

02グレイ・スコットの式

反応拡散系の中でも有名なのがグレイ・スコットモデルだ。2つの仮想的な化学物質 U と V を考える。Uは「基質」、Vは「生成物」。この2つが、次のルールで変化する。

U + 2V → 3V (VがUを消費して、自分自身を増やす反応)
そして U と V は、それぞれ周囲へ拡散する

これを式にすると、各点での U, V の時間変化は次のように書ける。

∂U/∂t = Du·∇²U − U·V² + f·(1−U)
∂V/∂t = Dv·∇²V + U·V² − (f+k)·V

∇²(ラプラシアン)が拡散、U·V²が反応を表す。fは U の供給率、kは V の除去率。 このf と k の値を変えるだけで、生まれる模様が劇的に変わる——それが反応拡散系の最大の魅力だ。式は固定したまま、たった2つの数字で、迷路にも、斑点にも、うねる虫にもなる。

03状態をテクスチャに持つ — もう一つのGPGPU

ここからが実装だ。粒子のシミュレーションでは、各粒子の状態を「バッファ」(1次元の配列)に持った。だが反応拡散系は2次元の場——画面の各ピクセルが状態を持ち、隣のピクセルと影響し合う。この2次元の状態を保持するのに最適なのがテクスチャだ。

各ピクセルに U と V の2つの値を持たせる。rg32float形式のテクスチャを使えば、R チャンネルに U、G チャンネルに V を格納できる。コンピュートシェーダーは、このテクスチャを読んで計算し、結果を別のテクスチャに書く。

JavaScript — 状態を持つテクスチャ
const texDesc = { size:[SIZE, SIZE], format:'rg32float', // R=U, G=V usage: GPUTextureUsage.STORAGE_BINDING // compute が書く | GPUTextureUsage.TEXTURE_BINDING // compute/render が読む | GPUTextureUsage.COPY_DST, }; texA = device.createTexture(texDesc); texB = device.createTexture(texDesc); // 2枚使う(後述)

コンピュートシェーダーでは、各ピクセルが自分と上下左右・斜めの隣を読んでラプラシアン(拡散)を計算し、グレイ・スコットの式で次の U, V を求める。

WGSL — 反応拡散の1ステップ
@group(0) @binding(0) var src : texture_2d<f32>; // 読む @group(0) @binding(1) var dst : texture_storage_2d<rg32float, write>; // 書く @compute @workgroup_size(8,8) fn main(@builtin(global_invocation_id) gid: vec3<u32>) { let p = vec2<i32>(i32(gid.x), i32(gid.y)); let c = textureLoad(src, p, 0).xy; // 自分の (U,V) let lap = /* 隣接ピクセルからラプラシアンを計算 */; let U = c.x; let V = c.y; let uvv = U * V * V; // 反応項 let nU = U + (Du*lap.x - uvv + f*(1.0 - U)); let nV = V + (Dv*lap.y + uvv - (f+k)*V); textureStore(dst, p, vec4<f32>(nU, nV, 0.0, 1.0)); }
バッファとテクスチャ 粒子では1次元バッファを使った。テクスチャは2次元の隣接関係が自然に扱えるのが利点だ。textureLoad(src, p+offset)で隣のピクセルを直接読める。反応拡散のように「各点が周囲と影響し合う」場の計算には、テクスチャが向いている。

04ダブルバッファ — 読みと書きを分ける

ここで重要な制約がある。同じテクスチャを、同時に読みながら書くことはできない。もし1枚のテクスチャで計算すると、あるピクセルを更新した後、隣のピクセルがその「更新済みの値」を読んでしまい、計算が壊れる。全ピクセルは「同じ時刻の状態」を見る必要がある。

そこでテクスチャを2枚(A, B)用意し、交互に読み書きする。Aを読んでBに書く→次はBを読んでAに書く→またA…と切り替える。これがダブルバッファだ。

JavaScript — 2枚を交互に
// cur で「今どちらに書くか」を管理 cp.setBindGroup(0, cur===0 ? bindAtoB : bindBtoA); cp.dispatchWorkgroups(SIZE/8, SIZE/8); // ... cur = 1 - cur; // 次は逆向き(A→B と B→A を交互に)
なぜ2枚必要か セルオートマトンや格子計算に共通の原則だ。「現在の状態」を全セルが同時に参照して「次の状態」を作るには、現在と次を別の場所に持たねばならない。1枚で済ませようとすると、計算順序によって結果が変わる不正な状態になる。読む面と書く面を分ける——これが正しい並列更新の鍵だ。

05全画面に描く — テクスチャを絵にする

計算した状態テクスチャを、画面に表示する。やり方はシンプルだ。画面全体を覆う四角形(2つの三角形)を描き、そのピクセルごとにテクスチャから V の値をサンプリングして、色に変換する。

WGSL — 全画面の四角形にテクスチャを貼る
@vertex fn vs(@builtin(vertex_index) vi:u32) -> VOut { // 画面全体を覆う2三角形(6頂点)を直接生成 var p = array<vec2<f32>,6>( vec2(-1,-1), vec2(1,-1), vec2(1,1), vec2(-1,-1), vec2(1,1), vec2(-1,1)); /* ... uv を計算して fragment へ ... */ } @fragment fn fs(in: VOut) -> @location(0) vec4<f32> { let v = textureSample(tex, smp, in.uv).y; // V の濃度 // v を色にマップ(シアン/火炎/虹/モノクロ) return vec4<f32>(colorize(v), 1.0); }

頂点バッファすら要らない。頂点シェーダーがvertex_indexから画面隅の座標を直接生成し、フラグメントシェーダーが各ピクセルでテクスチャを読んで色を決める。計算結果のテクスチャが、そのまま画面の絵になる。V の濃度をどう色にマッピングするかで、同じ模様がシアンにも炎にも虹にもなる。

06f と k の魔法 — パラメータで模様が激変

反応拡散系の醍醐味は、式は同じまま、f と k の2つの数字だけで模様が一変することだ。これらをuniformでシェーダーに渡せば、実行中に切り替えられる。

迷路 (f=0.026, k=0.051) 虫 (f=0.022, k=0.051)
動く点 (f=0.014, k=0.054) 増殖 (f=0.030, k=0.057)

ただし注意がある。f, k の組み合わせには「模様が広がる領域」と「種が点のまま止まる領域」がある。同じグレイ・スコットでも、パラメータ次第で画面全体に広がることも、孤立した点で固まることもある。どの値が広がるかは、実際に計算して確かめるのが確実だ。本稿のデモのプリセットは、いずれも広がりを数値的に確認した値を採用している。

パラメータ空間の地図 f-k 平面は「模様の地図」になっている。少し動かすだけで、斑点→縞→迷路→カオスと、性質の違うパターンの領域を渡り歩く。この豊かさが、単純な式から生まれているのが驚きだ。次のデモで、その地図を旅してみてほしい。

07試してみる

下のデモは、ここまで解説した反応拡散系がそのまま動いている。パターンを切り替えて模様の激変を見たり、画面をタッチして種をまいたり、配色を変えたりできる。単純な2つの式から、生命的な模様が自発的に育っていく様子を体感してほしい。

WebGPU 判定中...
パターン:
配色:
✋ 画面をタッチ/なぞると、その場所から模様が育ちます
2つの仮想物質 U・V が反応し拡散します。まいた種から、グレイ・スコットの式に従って模様が自発的に育っていきます。「連続実行」で、珊瑚や斑点のような有機的なパターンが広がる様子を観察してください。単純なルールから複雑な模様が生まれる——チューリングが示した形態形成の原理です。

08結論 — 計算と生命のあいだ

反応拡散系が見せるのは、ただの綺麗な模様ではない。設計図のない自己組織化——中央の制御なしに、局所的なルールの繰り返しだけで、全体に秩序ある構造が生まれる現象だ。生物の模様、細胞の分化、さらには銀河の構造まで、自然界の形作りの根底には、こうした自己組織化がある。

技術的には、これは粒子描画とは異なるGPGPUの形を示している。粒子は1次元バッファに状態を持ち点として描いた。反応拡散は2次元テクスチャに状態を持ち、面として描く。compute がテクスチャを更新し、render がそれを画面に貼る——この「テクスチャを介したGPGPU」は、流体、煙、群衆、地形生成など、場のシミュレーション全般に通じる強力なパターンだ。

単純な計算が、生命の模様になる。手のひらの中のGPUで、チューリングが70年前に数式で予言した形態形成が、いま実際に立ち現れる。計算と生命のあいだには、思いのほか短い距離しかないのかもしれない。

『計算が、生命の模様になる — 反応拡散系をWebGPUで』を公開しました。