はじめに
大地が隆起し、洞窟が穿たれ、草木が芽吹き、そこに風が吹きました。 これまで構築してきた「Procedural World(プロシージャルな世界)」は、環境としてはすでに完成しつつあります。しかし、この空にはまだ「動く生き物」がいません。
Noise入門シリーズ第49回。今回は、この生きた環境に「自律的な生命」を誕生させます。 個々の個体が周囲と協調して動く「Boids(群れ)アルゴリズム」に、私たちがこれまで学んできた「風のベクトル場(Flow Field)」や Curl Noise を掛け合わせます。
ただランダムに空間を漂うのではなく、気流を読み、風に流されながらも群れをなして大空を舞う鳥たち。これらをGPGPUシミュレーションを用いて、Three.js上で実装する手法を解説します。環境と生命が交差する、プロシージャルワールドのさらなる深淵へ飛び込みましょう。
前回の記事:
[Noise 入門 #48] Wind & Flow — 風向ノイズで草木を揺らす
InstancedMeshで配置された大量の草木に対し、GLSLのVertex Shaderとノイズ関数を用いて自然な風の揺らぎ(Wind & Flow)を実装する手法。Three.jsのonBeforeCompileを活用したプロシージャルなアニメーションの …
https://humanxai.info/posts/noise-intro-48-wind-flow-vertex-shader/1. Boids(ボイド)アルゴリズムの基礎 — 生命の自律性を定義する
1986年にクレイグ・レイノルズ(Craig Reynolds)によって考案された「Boids(ボイド)」は、鳥の群れ(Flock)や魚の群れ(School)の複雑な動きを、驚くほどシンプルな3つのルールでシミュレートする画期的なアルゴリズムです。
Boidsの最大の特徴は、「群れ全体を制御するリーダーが存在しない」ということです。 各個体(ボイド)は、自身の「感知半径(視界)」に入る近隣の仲間の情報だけをもとに、自律的に次のフレームの「加速度(ベクトル)」を決定します。このような局所的な相互作用から、まるで一つの巨大な生命体のような複雑で美しい群れの動きが「創発(Emergence)」します。
群れを形成する3つの基本ルール(操舵力)
各個体は、周囲の仲間を観察し、以下の3つのルールに従って「操舵力(Steering Force)」を計算します。
① Separation(分離) — 「ぶつからないように避ける」
近づきすぎた仲間から離れる、衝突回避のルールです。 自身の位置から、近すぎる仲間の位置へ向かうベクトルを反転させます。距離が近いほど強く反発するように計算するのが一般的です。これが無いと、鳥たちは一箇所に密集して単一の点のようになってしまいます。
② Alignment(整列) — 「周りと同じ方向へ飛ぶ」
周囲の仲間と同じ方向、同じ速度で飛ぼうとするルールです。 感知半径内にいる仲間の「速度ベクトル(Velocity)」の平均を計算し、自分の現在の速度をその平均に近づけようとします。これにより、群れ全体が同じ方向へ流れるような美しい軌跡を描きます。
③ Cohesion(結合) — 「集団の中心に向かう」
周囲の仲間の「中心位置」に向かって飛ぼうとする、群れを形成するためのルールです。 感知半径内にいる仲間の「位置(Position)」の平均(重心)を計算し、そこへ向かうベクトルを算出します。これが無いと、鳥たちは群れを作らずにバラバラに飛び散ってしまいます。
ベクトルの合成とパラメータの「重み付け」
個々のボイドは、毎フレームこの3つの力を計算し、それらを合成して新しい加速度($\vec{A}$)と速度($\vec{V}$)を決定します。
ここで重要になるのが「重み付け(Weight)」です。3つのルールをそのまま足し合わせるのではなく、それぞれに係数を掛けて影響力を調整します。
- $W_{sep}$ を大きくすると、警戒心の強いバラバラの群れになります。
- $W_{ali}$ を大きくすると、一糸乱れぬ軍隊のような動きになります。
- $W_{coh}$ を大きくすると、密集した球状の群れになります。
この合成された力 $\vec{F}_{total}$ を加速度として扱い、現在の速度に足し合わせることで、最終的な新しい速度が決定されます。
このアルゴリズムだけでも美しい群れは形成されます。しかし、彼らが飛んでいるのは無重力・無風の「真空空間」に過ぎません。私たちが作ってきたプロシージャルな世界には、すでにノイズによる気流が存在します。
ここに、「ノイズの魔法(Flow Field)」を掛け合わせてみましょう。
2. Boids × Flow Field — ノイズの風に乗る
前回の [Noise 入門 #48]) では、Simplex Noise や Curl Noise を用いて、空間全体にうねる「風のベクトル場(Flow Field)」を構築し、大地の草木を揺らしました。
[Noise 入門 #48] Wind & Flow — 風向ノイズで草木を揺らす
InstancedMeshで配置された大量の草木に対し、GLSLのVertex Shaderとノイズ関数を用いて自然な風の揺らぎ(Wind & Flow)を実装する手法。Three.jsのonBeforeCompileを活用したプロシージャルなアニメーションの …
https://humanxai.info/posts/noise-intro-48-wind-flow-vertex-shader/真空空間を飛ぶ従来の Boids とは異なり、私たちが構築しているプロシージャルワールドには、すでにこの「大気の流れ」が存在します。鳥たちは自らの意思(Boidsの3ルール)で飛ぼうとしますが、同時に環境からの「物理的な影響」を絶えず受け続けることになります。
計算式に、環境からの外力となる4つ目の要素 Flow(気流) を追加しましょう。
Flow Fieldによる影響($\vec{F}_{flow}$)の算出
計算は非常にシンプルです。各ボイド(鳥)の現在座標 $\vec{P}$ を入力として Curl Noise 関数を呼び出し、その座標における「風のベクトル」を取得します。それを外力 $\vec{F}_{flow}$ として群れの加速度に加算するだけです。
しかし、このたった一つの外力が加わることで、単調だった群れの動きに「環境との相互作用」という劇的な変化が生まれます。
- 乱気流(Vortex)に巻き込まれる群れ: Curl Noise 特有の渦(Vortex)に群れが差し掛かると、整列(Alignment)しようとする鳥たちの意思が一時的に気流によって乱されます。結果として、竜巻に巻き込まれたように渦を巻きながら旋回する、非常にダイナミックな挙動が自然発生します。
- 風の通り道(Ridge)を利用する滑空: 特定の方向に強い気流が流れている場所に差し掛かると、鳥たちは自力で飛ぶよりも高速に移動できるようになります。自律的な群れのルールと気流のベクトルが重なることで、見えない「空の川」をなめらかに流れるような美しいラインが形成されます。
環境と生命のバランス(Weightの調整)
ここでも前項で触れた重み付けパラメータ($W_{flow}$)が重要になります。
気流の影響を強めすぎると、鳥たちは意志を持たない「ただのパーティクル(塵)」のように風に押し流されてしまいます。逆に弱すぎると、環境を無視した不自然な群れに戻ってしまいます。
「風に抗いながらも、最終的には風に乗って飛んでいく」
Boids の操舵力と、Curl Noise による外力の絶妙なバランスを見つけることで、単なる数式の計算結果が「環境を生きる生命」へと昇華されるのです。
3. GPGPUによる10万羽のシミュレーション(GLSL実装)
少数の群れ(数百羽程度)であれば、CPU(JavaScript)の配列計算でも十分に動かすことができます。しかし、私たちが構築しているのは「プロシージャルな広大な世界」です。空を埋め尽くすような数万〜10万羽のスケールを60FPSで描画するには、もはやCPUの力では太刀打ちできません。
ここで、[第29回] で学んだ GPGPU(General-Purpose computing on Graphics Processing Units) の技術が再び火を噴きます。すべての計算をGPUの Fragment Shader に委ねて並列処理させるのです。
[Noise 入門 #29] GPGPU × Noise — 100万の粒子が描く流体シミュレーション
CPUの限界を超え、GPUの真の力を解放するGPGPUの基礎。見えないテクスチャをデータ配列として扱い、Ping-Pongバッファを用いて100万の粒子の状態を毎フレーム更新する「流体シミュレーション」のGLSL×Three.js実装を直感的に解説します。
https://humanxai.info/posts/noise-intro-29-gpgpu-particles/テクスチャとしてのデータ保持(Ping-Pong Buffer)
GPGPUでは、見えないキャンバス(画像テクスチャ)をデータの保存領域として扱います。例えば10万羽の鳥のデータは、512 x 512(= 262,144ピクセル)のテクスチャの各ピクセルに格納されます。
計算には2種類のテクスチャを用意し、それぞれに RGBA の浮動小数点データ(Float)を持たせます。
- Position Texture(位置データ):
- R, G, B = 鳥の現在座標 (x, y, z)
- A = 固有のランダムシード値(個体ごとの羽ばたきの周期やオフセットに使用)
- Velocity Texture(速度データ):
- R, G, B = 鳥の現在の速度ベクトル (vx, vy, vz)
- A = アクティブフラグや寿命(Life)など
これらを「Ping-Pong Buffer(読み込み用と書き込み用のテクスチャを毎フレーム入れ替える手法)」を用いて更新していきます。
GPGPU Shader の処理フロー
群れの心臓部となる Velocity Shader(速度を更新するFragment Shader)の内部フローは以下のようになります。
- 自身の状態を取得: texture2D を使って、現在の自分の位置と速度を読み込む。
- 周囲の探索(サンプリング): 他のピクセル(他の鳥のデータ)を読み込み、自分との距離(Distance)を測る。
- 操舵力の計算: 一定の「感知半径(Radius)」内にいる鳥のデータから、前述した Separation, Alignment, Cohesion の3つのベクトルを計算する。
- Flow Fieldの取得: 自分の現在位置 (x, y, z) を引数として Curl Noise 関数を呼び出し、Flowベクトル(風の力)を取得する。
- ベクトルの合成と更新: 4つのベクトルにそれぞれ重み付け(Weight)をして合成し、最大速度(Max Speed)で制限(clamp)した上で、新しい速度としてピクセルに書き出す。
- 位置の更新: Position Shader 側で、この新しい速度を現在の座標に加算して位置を更新する。
最適化の壁:計算量 $O(N^2)$ 問題と解決策
ここで一つ、GPGPUならではの巨大な壁が立ちはだかります。 Boidsのルール通り「すべての鳥が、他のすべての鳥との距離を測る」というループ処理をShader内で組むと、10万羽の場合は 10万 × 10万 = 100億回 の計算が毎フレーム発生してしまいます。計算量 $O(N^2)$ となり、いかに強力なGPUでも完全にフリーズします。
これを解決するための実践的なアプローチが 「Stochastic Boids(確率的ボイド)」 による近似計算です。
全ピクセルを律儀に探索するのではなく、Shader内で乱数を用い、「ランダムにピックアップした数十羽のデータ」 だけをサンプリングして距離を測ります。 数万羽が飛び交う空間では、局所的な数十羽の傾向(位置や速度)をサンプリングするだけで、群れ全体としては十分に説得力のある Boids の挙動(創発性)が生まれます。これにより、計算量を $O(N)$ クラスに落とし込み、ブラウザ上でも10万羽の滑らかなシミュレーションが可能になるのです。
4. Three.js × InstancedMesh で大空へ描画する
GPGPUのPing-Pong Buffer内で10万羽の座標と速度が毎フレーム更新されるようになりました。しかし、これはまだGPUのメモリ上に存在する「見えない数値データの羅列(テクスチャ)」に過ぎません。
最後に、このデータをThree.jsの3D空間に「鳥の群れ」として可視化します。ここで用いるのが、[第42回])のボクセル地形生成でも大活躍した InstancedMesh です。
[Noise 入門 #42] Three.js × InstancedMesh — 10万のブロックで「最初の大地」を構築する
Three.jsのInstancedMeshを使ってMinecraftのようなボクセル地形を生成する方法を解説。FBMノイズを利用し、10万個のブロックを60FPSで描画するパフォーマンス最適化の実装を学びます。
https://humanxai.info/posts/noise-intro-42-instanced-mesh/10万個の鳥の3Dモデル(Geometry)を普通に配置すれば、ドローコール(CPUからGPUへの描画命令)がパンクしてしまいます。しかし InstancedMesh とカスタムShaderを組み合わせれば、ドローコールは「たった1回」のまま、10万羽を60FPSで描画することが可能です。
Vertex Shader でテクスチャを読み込む
Three.jsの ShaderMaterial(または onBeforeCompile)を用いて、InstancedMeshの Vertex Shader を拡張します。GPGPUが書き出した2枚のテクスチャ(PositionとVelocity)を uniform 変数として渡し、gl_InstanceID を元に各鳥のデータを抽出します。
// InstancedMesh用 Vertex Shader の要点
uniform sampler2D posTexture; // GPGPUから渡された位置データ
uniform sampler2D velTexture; // GPGPUから渡された速度データ
uniform float uTime; // 経過時間(羽ばたき用)
void main() {
// 1. インスタンスIDからUV座標を計算し、自分のデータを取得
vec2 uv = getUV(gl_InstanceID);
vec4 posData = texture2D(posTexture, uv);
vec4 velData = texture2D(velTexture, uv);
vec3 pos = posData.xyz;
vec3 vel = velData.xyz;
float seed = posData.w; // 個体固有のランダムシード(オフセット用)
// 2. 進行方向(Velocity)を向くための回転行列を計算
// 速度ベクトルを正規化し、upベクトルとの外積からLookAt行列を構築する
vec3 forward = normalize(vel);
vec3 right = normalize(cross(vec3(0.0, 1.0, 0.0), forward));
vec3 up = cross(forward, right);
mat3 rotationMatrix = mat3(right, up, forward);
// 3. ローカル頂点座標の取得
vec3 localPosition = position;
// --- ここにプロシージャルアニメーション(羽ばたき)を追加 ---
// 4. 回転と平行移動の適用
vec3 transformed = rotationMatrix * localPosition + pos;
// 5. 最終的な画面上の座標を計算
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
速度ベクトルから「進行方向」を向かせる
鳥が横を向いたままスライド移動しては不自然です。上記のコードの 2. にあるように、速度ベクトル($\vec{v}$)を正規化して進行方向(Forward)とし、外積(Cross Product)を用いて空間の基底ベクトル(Right, Up)を算出します。これらを並べることで、鳥のモデルを進行方向へ向かせる「回転行列(Rotation Matrix)」をShader内で動的に構築できます。
プロシージャルな「羽ばたき」アニメーション
さらに生命感を出すために、ボーン(骨格)アニメーションは使わず、頂点シェーダーの数式だけで羽ばたき(Flapping)を実装します。
鳥のローカル座標のX軸(左右の翼の広がり)を基準に、サイン波(sin)を用いてY軸(上下)を揺らします。このとき、GPGPUから渡された「速度(ベクトルの長さ)」をアニメーションのスピードに乗算します。
つまり、「風に乗って高速で滑空している時は羽ばたかず、遅い時は一生懸命羽ばたく」という挙動が、一切のキーフレームアニメーション無しに、純粋な数学的アプローチで実現できるのです。個体ごとのランダムシード値(seed)を時間のオフセットに加えれば、10万羽が完全にバラバラのタイミングで羽ばたくようになります。
まとめ — ノイズが生命を宿す瞬間
ノイズ関数によって削り出された無機質なベクトル場(Flow Field)と、他者との距離のみを測るシンプルな生命のルール(Boids)。
この2つがGPUの広大な並列計算空間で交差したとき、そこには単なる数式の羅列を超えた「生態系」の片鱗が生まれます。乱気流に煽られて群れが散り散りになり、再び風の尾根(Ridge)で合流して巨大なうねりとなる姿は、いつまで見ていても飽きない「創発(Emergence)」の美しさを持っています。
大地を隆起させ、洞窟を穿ち、海を満たし、森を育て、風を吹かせ、そしてついに大空に命を放ちました。私たちがゼロから数式で組み上げてきたプロシージャルワールドは、今、自律して呼吸を始めています。
次回予告:[Noise 入門 #50] 第5集完結 — 終わらない世界を歩く(Procedural Worldの統合)
長きにわたる旅路も、いよいよ次の一歩でひとつの巨大なマイルストーンを迎えます。
次回は第5集「Procedural World編」の完結、記念すべき第50回です。これまで個別に錬成してきた無限のチャンク生成、バイオーム、ボクセル地形、大気散乱、植生、そして今回放った生命の群れ。散り散りになっていたすべてのShaderとアルゴリズムを結合し、ひとつの「終わらない世界(Infinite Procedural World)」として統合します。
あなた自身の手で数式から構築した、無限に広がるノイズの箱庭を実際に歩いてみましょう。 集大成となる第50回、どうぞお楽しみに!
💬 コメント