AI Dev Lab
StarTrail

StarTrail ができるまで — N 体重力を Canvas で動かす

重力シミュレーターの開発ログ。 万有引力を全ペアで直接計算し、 Velocity-Verlet 法で位置を進めて 太陽系・二重星・銀河衝突など 6 プリセットの軌道を描くまでと、 天体どうしが近づいたときに計算が発散しない工夫を記録しています。

StarTrail は、ニュートンの万有引力で動く N 体シミュレーターです。太陽系・二重星・銀河衝突・球状星団など 6 つのプリセットを、全ペア間の力を直接計算して動かします。粒子を増やすと計算量は二乗で効きますが、素直に総当たりする代わりに得られるものがありました。

#🎯 「重力だけ」でどこまで見えるか

ルールは 1 つだけです。

F = G · m₁ · m₂ / r²

これを全部のペアについて計算して、位置を進める。それだけで太陽系のような安定した軌道も、銀河の衝突も、球状星団のゆらぎも出てきます。局所的な規則から全体の形が生まれるタイプの題材で、作っていて一番気持ちが良い部類でした。

#⚙️ 全ペア総当たり(O(N²))で書く

高速化の定石は Barnes-Hut(四分木で遠くの天体をまとめる)ですが、あえて総当たりにしています。

ts
for (let i = 0; i < n; i++) {
  for (let j = i + 1; j < n; j++) {
    const dx = bj.x - bi.x
    const dy = bj.y - bi.y
    const r2 = dx * dx + dy * dy + eps2
    const r = Math.sqrt(r2)
    const f = G / (r2 * r)
    ax[i] += dx * f * bj.mass
    ay[i] += dy * f * bj.mass
    ax[j] -= dx * f * bi.mass   // 作用・反作用
    ay[j] -= dy * f * bi.mass
  }
}

理由は 3 つあります。

  1. 数百体なら総当たりで 60fps が出る。近似を入れる必要がない
  2. 近似を入れると「これは本当に重力なのか」が曖昧になる
  3. j = i + 1 から回して作用・反作用で 2 回ぶんを 1 回で済ませるので、実質半分

3 つ目は地味ですが効きます。i→j の力を計算したら、j→i は符号を反転するだけ。ループが半分になります。

f = G / (r² · r) としているのは、単位ベクトルへの正規化(÷r)を割り算にまとめるためです。dx/r × 1/r² を毎回書くより 1 回の除算で済みます。

#💥 ゼロ距離で発散させない

重力シミュレーションで必ず踏むのが、天体どうしが近づいたときの発散です。r が 0 に近づくと F が無限大になり、次のフレームで粒子が画面外へ吹き飛びます。

対策は softening(軟化) で、距離の二乗に小さな定数を足します。

ts
const eps2 = softening * softening
const r2 = dx * dx + dy * dy + eps2

r² + ε² にするだけで、r → 0 でも分母が ε² で下げ止まります。物理的には「点ではなく広がりを持った天体」として扱うことに相当し、天体シミュレーションでは標準的な手法です。

これを入れないと、球状星団のプリセットは数秒で崩壊します。ε をスライダーで触れるようにしてあるので、0 に近づけると実際に破綻するのが見られます。

#🌀 プリセットで「見どころ」を作る

重力は正しく実装できても、初期条件が悪いとただ散らばって終わります。6 つのプリセットは、見て面白い状態になる初期条件の詰め合わせです。

プリセット仕込み
太陽系風中心に重い天体 1 つ、円軌道になる速度を与える
二重星重い 2 体を共通重心の周りに配置
銀河衝突回転する 2 つの円盤を、ぶつかる向きの相対速度で置く
ランダム雲速度ゼロ。 重力だけで集まる過程が見える
球状星団中心密度を高くして、ゆらぎながら保つ
衛星系惑星の周りをさらに小さい天体が回る階層構造

円軌道にするには、その距離での円軌道速度を計算して接線方向に与えます。適当な速度だと楕円になったり飛んでいったりするので、ここは式から出しています。

回転する円盤(銀河)を作るときは、中心からの距離に応じて回転方向の速度を配ります。片方の円盤の回転方向を逆にすると、衝突したときの潮汐の腕の出方が変わります。

#🎨 軌跡で時間を見せる

各天体の過去の位置を残して、薄い線で繋いでいます。軌跡があると、

  • 楕円軌道なのか放物線なのかが一目で分かる
  • 衝突後にどう散ったかが残る
  • 静止画として保存したときに情報が残る

軌跡の長さはスライダーにしました。短くすると現在の状態が見やすく、長くすると軌道の形が見えます。

#😓 苦労したところ

質量の扱いを間違えていた。 加速度を求めるとき、力を自分の質量で割る必要があります。上のコードで ax[i] += dx * f * bj.mass相手の質量だけを掛けているのはそのためです(自分の質量は約分で消える)。最初ここを両方掛けていて、重い天体ほど動かないという挙動になっていました。軽い天体が重い天体に引かれるという当たり前の絵が出ないことで気づきました。

画面のスケール。 太陽系と球状星団では、必要な表示範囲が桁違いです。プリセットごとに初期ズームを持たせ、全体が入る倍率から始めるようにしました。

時間刻み(dt)の落としどころ。 大きくすると速い天体の軌道がずれ、小さくすると動きが遅くて退屈になります。プリセットごとに既定値を変え、スライダーでも触れるようにしています。

#🔭 今後の拡張

  • Barnes-Hut による高速化(数千体を動かす)
  • エネルギー保存の表示(二重振り子でやったのと同じ検算)
  • 天体の合体(一定距離以下で質量を統合)
  • 3 次元化

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

「速くする」より先に「正しく見える」を優先しました。 Barnes-Hut を入れれば粒子は増やせますが、数百体でも銀河衝突は十分に見えます。近似を足すのは、素直な実装で足りなくなってからで良いと思っています。

もう一つは softening のような「現実には無い定数」が、シミュレーションを成立させるということ。物理をそのまま書くと壊れる場面があり、そこで何を足すかに実装者の判断が出ます。ε をスライダーに出したのは、その判断を隠さないためです。

[ ./next_action ]

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

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

[ ./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 →