#02 ビジュアルコーディング 物理シミュレーション

力と加速度——F=maをキャンバスで

前回は「速度があれば物体は動き続ける」という慣性を実装しました。しかし現実の世界では、物体は加速したり減速したりします。その原因が力(Force)です。

ニュートンの第二法則はたった1行の式で書けます。

F = m * a

力は質量と加速度の積に等しい。これをコードに落とし込むことで、重力・風・爆発・バネ——あらゆる力を扱えるようになります。


F = ma をコードに変換する

物理シミュレーションでは、この式を次のように変形して使います。

a = F / m        (加速度は力を質量で割ったもの)
v += a * dt      (速度は加速度を積分)
x += v * dt      (位置は速度を積分)

コードに直すと:

// 毎フレームの更新
var ax = Fx / mass;  // X方向の加速度
var ay = Fy / mass;  // Y方向の加速度

vx += ax * dt;       // 速度を更新
vy += ay * dt;

x += vx * dt;        // 位置を更新
y += vy * dt;

この「加速度 → 速度 → 位置」という3段階の積分がすべての物理シミュレーションの骨格です。


重力加速度の実装

地球上の重力加速度は約 9.8 m/s² ですが、ピクセル単位のシミュレーションでは単位を合わせる必要があります。スクリーン上では 200〜400 px/s² 程度を使うことが多いです。

重力は常に下向き(Y方向の正)に働く一定の加速度です。

var GRAVITY = 300; // px/s² (9.8 m/s² に相当するスケール)

// 毎フレーム
vy += GRAVITY * dt;  // Y速度が増え続ける(下に加速)

vx には何も足していないので、水平方向は等速のまま——これが放物運動です。

重力で落ちるボール
var x = W / 2, y = 30;
var vx = 0, vy = 0;
var GRAVITY = 350;
var r = 16;

function loop(ts) {
if (!loop.last) loop.last = ts;
var dt = Math.min((ts - loop.last) / 1000, 0.05);
loop.last = ts;

vy += GRAVITY * dt;
y += vy * dt;
x += vx * dt;

if (y + r > H) {
  y = H - r;
  vy = -vy * 0.75;
}

ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, W, H);

ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = '#ff6b6b';
ctx.fill();

ctx.fillStyle = '#333';
ctx.fillRect(0, H - 4, W, 4);

requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

重力があると速度が毎フレーム増加し、ボールが徐々に加速して落ちていきます。床に当たると速度を反転させ(vy = -vy * 0.75)、少し弱めて跳ね返ります。


放物運動(斜め上に投げる)

初速度に水平成分と垂直成分を両方持たせると、放物線を描く投擲運動になります。軌跡を描いて可視化してみましょう。

放物運動——斜め上に投げて軌跡を残す
var GRAVITY = 280;
var balls = [];
var timer = 0;

function spawnBall() {
var angle = -Math.PI / 4 - Math.random() * Math.PI / 4;
var speed = 300 + Math.random() * 150;
balls.push({
  x: 30,
  y: H - 20,
  vx: Math.cos(angle) * speed,
  vy: Math.sin(angle) * speed,
  hue: Math.random() * 360,
  trail: []
});
}
spawnBall();

function loop(ts) {
if (!loop.last) loop.last = ts;
var dt = Math.min((ts - loop.last) / 1000, 0.05);
loop.last = ts;
timer += dt;

if (timer > 1.5) {
  timer = 0;
  if (balls.length < 5) spawnBall();
}

ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, W, H);

ctx.fillStyle = '#1a2a1a';
ctx.fillRect(0, H - 20, W, 20);

balls = balls.filter(function(b) { return b.y < H + 50; });

balls.forEach(function(b) {
  b.vy += GRAVITY * dt;
  b.x += b.vx * dt;
  b.y += b.vy * dt;

  b.trail.push({x: b.x, y: b.y});
  if (b.trail.length > 40) b.trail.shift();

  for (var i = 1; i < b.trail.length; i++) {
    ctx.beginPath();
    ctx.moveTo(b.trail[i-1].x, b.trail[i-1].y);
    ctx.lineTo(b.trail[i].x, b.trail[i].y);
    ctx.strokeStyle = 'hsla(' + b.hue + ',80%,60%,' + (i / b.trail.length) + ')';
    ctx.lineWidth = 2;
    ctx.stroke();
  }

  ctx.beginPath();
  ctx.arc(b.x, b.y, 8, 0, Math.PI * 2);
  ctx.fillStyle = 'hsl(' + b.hue + ',80%,65%)';
  ctx.fill();
});

requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

軌跡が美しい放物線を描きます。水平速度は変わらず、垂直速度だけが重力によって増加していくのが分かります。


複数の力の合成

1つの物体に複数の力が働くときは、X・Y方向それぞれで足し合わせます。

// 複数の力を合成する
var Fx = 0, Fy = 0;

// 重力
Fy += mass * GRAVITY;

// 風(右向き)
Fx += WIND_FORCE;

// 推力(上向き)
Fy -= THRUST;

// 合成した加速度を計算
var ax = Fx / mass;
var ay = Fy / mass;

vx += ax * dt;
vy += ay * dt;

このように「力を全部足してから加速度に変換する」のが正しい手順です。


空気抵抗の実装

空気抵抗は速度に比例した、速度と逆方向の力です。

F_drag = -k * v

k は抵抗係数、v は速度です。速いほど大きな抵抗がかかります。

var k = 0.5; // 空気抵抗係数

// 毎フレーム
var drag_x = -k * vx;
var drag_y = -k * vy;

// 重力と空気抵抗を合成
var ax = drag_x / mass;
var ay = (mass * GRAVITY + drag_y) / mass;

vx += ax * dt;
vy += ay * dt;
空気抵抗あり vs なしの比較
var GRAVITY = 300;
var balls = [
{x: W*0.3, y: 30, vx: 200, vy: -50, k: 0, hue: 0,   label: '抵抗なし'},
{x: W*0.3, y: 30, vx: 200, vy: -50, k: 1.5, hue: 120, label: '抵抗あり(k=1.5)'},
];

function loop(ts) {
if (!loop.last) loop.last = ts;
var dt = Math.min((ts - loop.last) / 1000, 0.05);
loop.last = ts;

ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, W, H);

ctx.fillStyle = '#1a1a2a';
ctx.fillRect(0, H - 16, W, 16);

balls.forEach(function(b) {
  if (b.y < H - 16) {
    var dragX = -b.k * b.vx;
    var dragY = -b.k * b.vy;
    b.vx += (dragX) * dt;
    b.vy += (GRAVITY + dragY) * dt;
    b.x += b.vx * dt;
    b.y += b.vy * dt;
  }

  ctx.beginPath();
  ctx.arc(b.x, b.y, 12, 0, Math.PI * 2);
  ctx.fillStyle = 'hsl(' + b.hue + ',80%,60%)';
  ctx.fill();

  ctx.fillStyle = 'hsl(' + b.hue + ',80%,75%)';
  ctx.font = '12px monospace';
  ctx.fillText(b.label, 8, b.hue === 0 ? 20 : 40);
});

requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

赤(抵抗なし)は水平方向に速度を保ったまま遠くまで飛びます。緑(抵抗あり)は速度に比例した抵抗で徐々に減速し、短い距離で落下します。


質量の違いによる運動の差

F = ma から a = F / m を考えると、同じ力でも質量が大きいほど加速しにくいことが分かります。

重力は F = m * g なので、加速度 a = F/m = m*g/m = g となり、質量によらず同じ加速度になります(これはガリレオが発見した「重力加速度は質量によらず同じ」という事実と一致しています)。

しかし、空気抵抗は速度に比例するため、同じ速度でも軽いものの方が大きな影響を受けます。

// 重力加速度:質量に関係なく同じ
ay_gravity = GRAVITY;

// 空気抵抗の加速度:質量が大きいほど影響が小さい
ay_drag = -k * vy / mass;
質量の違い——重力は同じでも空気抵抗の影響が異なる
var GRAVITY = 300;
var K = 0.8;
var masses = [0.5, 1, 2, 4];

var balls = masses.map(function(m, i) {
return {
  x: (i + 1) * W / (masses.length + 1),
  y: 30,
  vx: 0,
  vy: 0,
  mass: m,
  hue: i * 90
};
});

function loop(ts) {
if (!loop.last) loop.last = ts;
var dt = Math.min((ts - loop.last) / 1000, 0.05);
loop.last = ts;

ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, W, H);

ctx.fillStyle = '#1a1a2a';
ctx.fillRect(0, H - 16, W, 16);

balls.forEach(function(b, i) {
  if (b.y < H - 16 - b.mass * 8) {
    var dragY = -K * b.vy / b.mass;
    b.vy += (GRAVITY + dragY) * dt;
    b.y += b.vy * dt;
  } else {
    b.vy = 0;
    b.y = H - 16 - b.mass * 8;
  }

  var r = Math.max(6, b.mass * 8);
  ctx.beginPath();
  ctx.arc(b.x, b.y, r, 0, Math.PI * 2);
  ctx.fillStyle = 'hsl(' + b.hue + ',80%,60%)';
  ctx.fill();

  ctx.fillStyle = '#aaa';
  ctx.font = '11px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('m=' + b.mass, b.x, H - 2);
  ctx.textAlign = 'left';
});

requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

軽いボールは空気抵抗の影響が大きく、ゆっくり落ちます。重いボールは抵抗の影響が小さく、速く落ちます。


まとめ

この回でやったこと:

  • F = maa = F/mv += a*dtx += v*dt という連鎖を実装した
  • 重力加速度(vy += GRAVITY * dt)を実装し、放物運動を作った
  • 複数の力を合成する手順(X・Y方向で加算)を学んだ
  • 空気抵抗 F = -k * v の実装と、抵抗なしとの比較を確認した
  • 質量によって空気抵抗の影響が変わることを確認した

次回はこの「更新則」の数値解法——オイラー法を詳しく掘り下げます。dtを大きくすると誤差が蓄積される仕組みを可視化し、より精度の高い中点法との比較を行います。