AI Dev Lab
FlockDrift

FlockDrift ができるまで — Boids の 3 規則で群れを作る

群れのシミュレーターの開発ログ。 分離・整列・結合の 3 規則だけで鳥や魚の群れが生まれる Boids を実装し、 捕食者からの逃避を足して ムクドリ・魚群・蛍・蚊柱 の 5 プリセットに仕上げるまでを記録しています。

FlockDrift は、鳥や魚の群れを再現するシミュレーターです。中心となる指令役はいません。個体が近くの仲間だけを見て動く 3 つの規則から、全体のうねりが勝手に生まれます。Craig Reynolds が 1986 年に発表した Boids の実装です。

#🎯 群れには「群れを作る意思」がない

ムクドリの大群がうねる映像を見ると、誰かが全体を指揮しているように見えます。でも実際には、鳥は近くの数羽しか見ていません。

Boids はそれを 3 つの規則で表します。

  1. 分離(Separation) — 近すぎる仲間から離れる
  2. 整列(Alignment) — 近くの仲間と同じ方向に揃える
  3. 結合(Cohesion) — 近くの仲間の中心へ寄る

全体を見る処理はどこにもありません。 それでも渦を巻き、分裂し、また合流します。作っていて一番驚いたのはここで、規則を書き終えて動かした瞬間にもう群れになっていました。

#🏗️ 3 規則を 1 ループで書く

各個体について、視界内の仲間を集めながら 3 つの合計を同時に作ります。

ts
for (let j = 0; j < boids.length; j++) {
  if (i === j) continue
  const dx = o.x - b.x, dy = o.y - b.y
  const d2 = dx * dx + dy * dy
  if (d2 > perc2 || d2 === 0) continue
  n++
  aliX += o.vx; aliY += o.vy      // 整列用: 速度の合計
  cohX += o.x;  cohY += o.y       // 結合用: 位置の合計
  if (d2 < sep2) {
    sepX -= dx / d2               // 分離用: 近いほど強く反発
    sepY -= dy / d2
  }
}

3 つの規則で別々にループを回さないのがポイントです。視界判定は距離計算が必要で、そこが一番重い。1 回の走査で 3 つぶんの材料を集めれば、計算量が 1/3 になります。

距離は二乗のまま比較しています。Math.sqrt は視界内と分かった相手にしか使いません。個体数 N に対して N² 回まわるループなので、平方根を省くだけで体感が変わります。

#⚖️ 分離だけ「距離で割る」

3 規則のうち、分離だけ扱いが違います。

ts
sepX -= dx / d2

距離の二乗で割っているので、近いほど反発が急激に強くなります。整列と結合は視界内なら一律に平均を取るのに対し、分離だけは至近距離で強く効く。

これが無いと個体が同じ点に潰れます。逆に分離を線形にすると、今度は群れがほどけて散ります。衝突回避だけは非線形、というのが Boids の効きどころでした。

#🎚️ 係数の大きさが 3 桁違う

3 つの規則に掛ける係数を見ると、桁がばらばらです。

ts
b.vx += ((aliX / n) - b.vx) * 0.05   * p.alignment   // 整列
b.vx += ((cohX / n) - b.x)  * 0.0008 * p.cohesion    // 結合
b.vx += sepX                * 0.9    * p.separation  // 分離

理由は掛かる値の単位が違うからです。

  • 整列は 速度の差(数 px/frame)
  • 結合は 位置の差(数百 px)
  • 分離は 1/距離 の合計(小さい値)

そのままだと結合だけが極端に強くなります。だから結合には 0.0008、分離には 0.9 と、値域を揃えるための係数が入ります。ユーザーに見せるスライダー(0〜1)とは別に、内部の正規化係数を持つ構造にしました。

この分離があると、スライダーをどう動かしても群れが破綻しません。つまみを触って壊れるシミュレーターは、この正規化をサボっている場合が多いです。

#🦅 捕食者を入れると群れが「割れる」

マウスに追従する捕食者を足しました。個体は捕食者から一定距離内に入ると、反対方向へ強く加速します。

ts
const flee2 = (p.perception * 2.2) * (p.perception * 2.2)
if (d2 < flee2 && d2 > 0) {
  const d = Math.sqrt(d2)
  b.vx += (dx / d) * 0.9
  b.vy += (dy / d) * 0.9
}

逃避の視界を通常の2.2 倍にしているのがポイントで、仲間より捕食者を先に見つけないと逃げ遅れます。

これを入れると、群れが避けて割れ、通過後にまた合流する ── 実際の鳥の群れで見る動きが出ます。規則を 1 つ足しただけで、行動のレパートリーが一段増えるのが面白いところでした。

#🐟 プリセットで「何の群れか」を変える

ムクドリ・魚群・蛍・蚊柱の 4 つは、パラメータの組み合わせだけで作り分けています。

プリセット特徴
ムクドリ視界が広く、整列が強い。 大きなうねり
魚群分離が強く、密度が高い。 急な方向転換
速度が遅く、ゆらぎが大きい。 ばらけて漂う
蚊柱結合が非常に強く、狭い範囲に密集

同じ 3 規則でも、係数の比率だけで生き物が変わります。

#😓 苦労したところ

画面端の扱い。 端で反転させると不自然に跳ね返り、ラップ(反対側から出す)にすると群れが分断されます。最終的に、端から一定距離(margin 40px)に入ったら中央へ向かう力を弱く足す方式にしました。壁ではなく「戻りたくなる領域」を作る発想です。

個体数と速度の両立。 O(N²) なので数を増やすと落ちます。視界内かどうかの判定を最初に置いて早期 continue し、平方根を避け、1 ループにまとめた ── この 3 つで数百体まで 60fps を保てました。空間分割は入れていません。

「群れているか」の判定基準がない。 物理シミュレーションと違って、正解の数値がありません。エネルギー保存のような検算ができないので、見て判断するしかない題材でした。だからこそプリセットを用意して、「この設定ならこう見える」という基準点を置いています。

#🔭 今後の拡張

  • 空間分割(グリッドや四分木)で数千体
  • 障害物を置いて避けさせる
  • 3 次元化
  • 個体ごとに視野角を持たせる(現在は全方位。 実際の鳥は後ろが見えない)

#💡 このサービスから言えること

創発を扱うプログラムは、規則の少なさそのものが価値です。 全体を制御するコードを 1 行も書かずに群れが出る、という事実が面白い。制御を足したくなったら、それは Boids ではない別のものになります。

実装面では、単位の違う量を足し合わせるときの正規化が全部でした。3 規則の係数が 0.0008 から 0.9 まで散らばっているのは行儀が悪く見えますが、そこを揃えたからスライダーで壊れない挙動になっています。

[ ./next_action ]

読んだら、 FlockDrift を実際に動かす。

この開発ログは FlockDrift をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ToonCast

ToonCast ができるまで — AnimeGANv2 をブラウザで動かす

AnimeGANv2 の小さな ONNX (約9MB) を onnxruntime-web (単一スレッド WASM=COOP/COEP不要、 color-revive で承認済みライブラリの再利用) で実行。 512x512・[-1,1] 正規化で推論し、 結果を元解像度に戻して表示する設計記録。 写真は端末内処理。

read log →
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、 輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。 写真は端末内処理。

read log →
PhotoTwin

PhotoTwin ができるまで — CLIP画像埋め込みで似た写真を見つける

CLIP (Xenova/clip-vit-base-patch32) の image-feature-extraction を transformers.js の CDN ESM で side-load し、 各写真を正規化ベクトル化。 cosine 類似度で重複・似た写真をブラウザ内で検出する設計記録 (新ライブラリ追加なし=what-cam と同じ CLIP の再利用)。

read log →
AkinFind

AkinFind ができるまで — ブラウザ内で完結する embeddings 意味検索

多言語の文章埋め込みモデル (Xenova/multilingual-e5-small) を transformers.js の CDN ESM で side-load し、 各文を正規化ベクトル化。 cosine 類似度で意味検索と似ている文ペア検出を全て端末内で行う設計記録。

read log →
WhatCam

WhatCam ができるまで — CLIP のゼロショット画像分類をブラウザで動かす

CLIP (Xenova/clip-vit-base-patch32) を transformers.js の CDN ESM で side-load し、 写真と候補ラベルの近さをブラウザ内で計算。 日本語ラベルを英語プロンプトに変換し、 図鑑と自由入力の両モードで「これ何?」を判定する設計記録。

read log →
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

read log →