N体問題をブラウザで — 万有引力シミュレーションの実装
このトピックを深掘りする
クリエイティブコーディング
ブラウザでの数学的シミュレーション・自然現象の可視化・生成アートの実装技法
全12本中 6 本目。
次に読む記事
Reaction-Diffusionのパラメータ空間を探索する — Gray-Scottモデルの4つの顔
Gray-Scott Reaction-Diffusionモデルのfeed/kill率を変えると、斑点・縞模様・珊瑚・細胞分裂と全く異なるパターンが現れる。200×200グリッドでの実装と探索記録。
砂山モデルと自己組織化臨界 — BTWモデルをブラウザで動かす
Bak-Tang-Wiesenfeld砂山モデルの実装を通じて、自己組織化臨界現象をブラウザで可視化する。べき乗則に従う雪崩分布の計測とフラクタルパターンの生成過程を解説。
二重振り子のカオスを数値で見る — RK4積分と初期値鋭敏性の実装
二重振り子シミュレーションの実装を通じて、ラグランジュ力学による運動方程式の導出、RK4積分の安定性、NaN対策、初期値鋭敏性の可視化までを解説する。
シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →
万有引力の計算
ニュートンの万有引力は F = G * m1 * m2 / r² で表される。N個の天体の各ペアについてこの力を計算する:
type CelestialBody = {
x: number
y: number
vx: number
vy: number
mass: number
radius: number
color: string
trail: { x: number; y: number }[]
}
const G = 6.674 // スケーリング済みの重力定数
function computeGravity(bodies: CelestialBody[]) {
for (let i = 0; i < bodies.length; i++) {
for (let j = i + 1; j < bodies.length; j++) {
const a = bodies[i]
const b = bodies[j]
const dx = b.x - a.x
const dy = b.y - a.y
const distSq = dx * dx + dy * dy
const dist = Math.sqrt(distSq)
// ソフトニング: 距離が近すぎると力が発散する
const softening = 10
const force = (G * a.mass * b.mass) / (distSq + softening * softening)
const fx = force * dx / dist
const fy = force * dy / dist
a.vx += fx / a.mass
a.vy += fy / a.mass
b.vx -= fx / b.mass
b.vy -= fy / b.mass
}
}
}ソフトニング は数値的安全装置。2つの天体が極端に近づくと 1/r² が無限大に発散し、速度が爆発する。softening² を分母に加えることで、近距離での力を制限する。
積分法の選択
physics.tsのシンプレクティックオイラーでも動くが、天体軌道ではエネルギー保存性が重要。Velocity Verlet法を使う:
function integrateVerlet(bodies: CelestialBody[], dt: number) {
// 半ステップの速度更新(加速度は前ステップの値)
for (const b of bodies) {
b.vx += b.ax * dt * 0.5
b.vy += b.ay * dt * 0.5
}
// 位置更新
for (const b of bodies) {
b.x += b.vx * dt
b.y += b.vy * dt
}
// 新しい加速度を計算
computeGravity(bodies)
// 残り半ステップの速度更新
for (const b of bodies) {
b.vx += b.ax * dt * 0.5
b.vy += b.ay * dt * 0.5
}
}Velocity Verletはシンプレクティック(位相空間の体積を保存する)なので、長時間シミュレーションでも軌道がドリフトしにくい。SolarSystemでの惑星軌道が閉じるのはこの性質のおかげ。
SolarSystemの初期条件
太陽系のシミュレーションは初期条件が鍵。実際の惑星データをスケーリングして使う:
const SUN: CelestialBody = {
x: 400, y: 300,
vx: 0, vy: 0,
mass: 10000,
radius: 20,
color: '#FFD700',
trail: [],
}
// 円軌道の初速 v = sqrt(GM/r)
function orbitalVelocity(centralMass: number, distance: number): number {
return Math.sqrt(G * centralMass / distance)
}
const EARTH: CelestialBody = {
x: 400 + 150, y: 300,
vx: 0, vy: orbitalVelocity(10000, 150),
mass: 10,
radius: 6,
color: '#4169E1',
trail: [],
}orbitalVelocity で計算された初速を接線方向に与えると、天体は安定した楕円軌道を描く。ケプラーの法則がコードの中で再現される。
軌道の描画
Canvas APIで各天体の過去の位置をトレイルとして描画する:
function drawTrail(ctx: CanvasRenderingContext2D, trail: { x: number; y: number }[]) {
if (trail.length < 2) return
ctx.beginPath()
ctx.moveTo(trail[0].x, trail[0].y)
for (let i = 1; i < trail.length; i++) {
ctx.lineTo(trail[i].x, trail[i].y)
}
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'
ctx.lineWidth = 1
ctx.stroke()
}
function updateTrail(body: CelestialBody, maxLength: number) {
body.trail.push({ x: body.x, y: body.y })
if (body.trail.length > maxLength) {
body.trail.shift()
}
}トレイルの長さを制限することでメモリを節約しつつ、軌道の形状がわかる程度に残す。
GravityWellのインタラクション
GravityWellはユーザーのポインタ位置に重力源を置く。パーティクルがマウスに引き寄せられる様子を見せる:
function attractToPointer(
particles: CelestialBody[],
pointerX: number,
pointerY: number,
strength: number,
) {
for (const p of particles) {
const dx = pointerX - p.x
const dy = pointerY - p.y
const distSq = dx * dx + dy * dy
const dist = Math.sqrt(distSq)
if (dist < 1) continue
const force = strength / (distSq + 100)
p.vx += (force * dx) / dist
p.vy += (force * dy) / dist
}
}NBodyとGravityWellの違いは「天体同士が互いに引き合うか」。NBodyは全ペアの相互作用、GravityWellは1つの固定重力源への一方向の引力。
O(n²) の限界
N体問題の計算量は O(n²)。100個の天体で4,950回の力計算。これをさらにスケールするにはBarnes-Hut法(四分木で遠方の天体をまとめて計算)が必要だが、ブラウザのデモでは100個もあれば十分に美しいので、素朴な全ペア計算で割り切った。requestAnimationFrameで毎フレーム更新しても、この規模なら十分に滑らかだ。
必要十分な実装を選ぶのが、パフォーマンスとコードの複雑さのバランス。
まとめ:N体シミュレーションの道具たち
天体力学とベクトル演算の基礎は、道具棚の線形代数と物理シミュレーションの書籍でカバーできる。数式をコードに落とす練習として、N体問題は最適な題材。
よくある質問
- Q. N体問題のシミュレーションはなぜ不安定になりやすい?
- 重力は距離が近いほど急激に大きくなるため、時間刻みが粗いと発散しやすくなります。近距離での加速度の上限(ソフトニング)や、固定タイムステップ、半陰的な積分(例: symplectic Euler)などで安定性を上げられます。
- Q. O(n²) の重力計算はどこまで実用的?
- 粒子数 n が増えるとペア計算が急増します。ブラウザでは 60fps を維持するなら、まずは n を抑えつつベクトル演算を最適化し、必要なら Barnes–Hut などの近似(O(n log n))を検討するのが現実的です。
- Q. なぜ積分法が軌道の見た目に大きく影響する?
- 位置・速度の更新順序や数値誤差がエネルギーの増減として蓄積し、軌道が縮んだり膨らんだりします。簡易な Euler 法は誤差が増えやすいので、同じ計算量でもより安定な手法を選ぶと見た目が改善します。
- Q. マウス追従の重力源はどう実装する?
- ポインタ座標をワールド座標に変換し、各粒子に対して「重力源→粒子」の方向ベクトルを正規化して加速度に加えます。距離が 0 に近いと発散するので、最小距離を設けるかソフトニング項を足すのが安全です。