← Blog
PhysicsSimulationCanvas APIMath

N体問題をブラウザで — 万有引力シミュレーションの実装

7 分で読める

このトピックを深掘りする

クリエイティブコーディング

ブラウザでの数学的シミュレーション・自然現象の可視化・生成アートの実装技法

全12本中 6 本目。

このトピックの記事一覧

シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →

万有引力の計算

ニュートンの万有引力は 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 に近いと発散するので、最小距離を設けるかソフトニング項を足すのが安全です。