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

慣性モーメントと回転——剛体を回転させる

これまでは物体を「点」として扱ってきました。現実の物体は大きさを持ち、回転もします。宇宙空間を漂う宇宙船、床を転がるサイコロ——回転は物理シミュレーションの重要な要素です。

今回は回転の物理を学びます。直線運動の「速度・加速度・質量」と対応する「角速度・角加速度・慣性モーメント」という概念を理解します。


角度・角速度・角加速度

回転の状態は3つの変数で表します。直線運動との対応がきれいです。

直線運動回転運動
位置 x角度 θ(ラジアン)
速度 v角速度 ω(rad/s)
加速度 a角加速度 α(rad/s²)
質量 m慣性モーメント I
Fトルク τ

更新則も直線運動と全く同じ構造です:

// 直線運動
vx += ax * dt;
x  += vx * dt;

// 回転運動
omega += alpha * dt;   // 角速度 += 角加速度 * dt
theta += omega * dt;   // 角度 += 角速度 * dt

慣性モーメント(I)

慣性モーメントは「回転しにくさ」を表す量で、質量の回転版です。形状によって計算式が異なります。

円(半径 r):         I = 0.5 * m * r²
中空円筒(外径 R):   I = m * R²
矩形(幅 w、高さ h): I = m * (w² + h²) / 12
薄い棒(長さ L):     I = m * L² / 12

矩形の慣性モーメントは以下のように計算できます:

var mass = 1.0;
var w = 80, h = 50;
var I = mass * (w * w + h * h) / 12;

大きく・重い物体ほど I が大きく、同じトルクを加えても回転しにくいです。


トルク: τ = r × F

トルク(回転力)は、力のモーメントです。重心からある距離の点に力を加えると回転が生まれます。

τ = r × F  (クロス積)

2次元では:

// 重心 (cx, cy) から力の作用点 (px, py) へのベクトル
var rx = px - cx;
var ry = py - cy;

// 力 (Fx, Fy)
// 2Dクロス積(スカラー値)
var torque = rx * Fy - ry * Fx;

結果は正なら反時計回りの回転、負なら時計回りの回転です。

// 角加速度 = トルク / 慣性モーメント
var alpha = torque / I;

回転しながら飛ぶ矩形

重心に初期角速度を与えた矩形が、宇宙空間を回転しながら飛ぶデモです。

ctx.save() / ctx.restore()ctx.translate() / ctx.rotate() を組み合わせて、各フレームで矩形を角度に合わせて描画します。

function drawRect(rect) {
  ctx.save();
  ctx.translate(rect.x, rect.y);  // 重心に移動
  ctx.rotate(rect.angle);          // 回転
  ctx.fillRect(-rect.w/2, -rect.h/2, rect.w, rect.h);  // 中心基準で描く
  ctx.restore();
}
回転アニメーション——宇宙空間を漂う矩形
var rects = [];
for (var i = 0; i < 6; i++) {
var angle = Math.random() * Math.PI * 2;
var speed = 60 + Math.random() * 100;
rects.push({
  x: 60 + Math.random() * (W - 120),
  y: 40 + Math.random() * (H - 80),
  vx: Math.cos(angle) * speed,
  vy: Math.sin(angle) * speed,
  w: 30 + Math.random() * 50,
  h: 20 + Math.random() * 30,
  angle: Math.random() * Math.PI * 2,
  omega: (Math.random() - 0.5) * 4,
  hue: i * 60
});
}

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

ctx.fillStyle = 'rgba(5,8,15,0.25)';
ctx.fillRect(0, 0, W, H);

rects.forEach(function(r) {
  r.x += r.vx * dt;
  r.y += r.vy * dt;
  r.angle += r.omega * dt;

  if (r.x < -60) r.x = W + 60;
  if (r.x > W + 60) r.x = -60;
  if (r.y < -60) r.y = H + 60;
  if (r.y > H + 60) r.y = -60;

  ctx.save();
  ctx.translate(r.x, r.y);
  ctx.rotate(r.angle);

  ctx.fillStyle = 'hsla(' + r.hue + ',75%,55%,0.85)';
  ctx.fillRect(-r.w/2, -r.h/2, r.w, r.h);
  ctx.strokeStyle = 'hsl(' + r.hue + ',80%,75%)';
  ctx.lineWidth = 1.5;
  ctx.strokeRect(-r.w/2, -r.h/2, r.w, r.h);

  ctx.fillStyle = 'rgba(255,255,255,0.6)';
  ctx.beginPath();
  ctx.arc(0, 0, 3, 0, Math.PI * 2);
  ctx.fill();

  ctx.restore();
});

requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

各矩形が独立した角速度で回転しています。重心(白い点)を中心に回転しているのが分かります。


重心に力を加えると回転する

重心に力を加えても回転は生まれません(トルク = 0)。重心からずれた位置に力を加えるとトルクが発生します。

慣性モーメントの違いを感じる——同じトルクで3つの形が回る
// 同じ質量・同じ初期角速度、でも形が違う → 回転速度の変化が違う
var mass = 1.0;
var TORQUE = 0; // トルクなし。初期omegaを与えて自由に減衰させる

var shapes = [
// コンパクトな正方形: I = m*(50²+50²)/12 ≈ 208
{x:W*0.18, y:H/2, w:50, h:50, angle:0, omega:6.0, mass:mass, color:'#ff6b6b', label:'正方形'},
// 中くらいの矩形: I = m*(100²+30²)/12 ≈ 908
{x:W*0.50, y:H/2, w:100, h:30, angle:0, omega:6.0, mass:mass, color:'#4ecdc4', label:'横長矩形'},
// 長い棒: I = m*(160²+10²)/12 ≈ 2142
{x:W*0.82, y:H/2, w:160, h:10, angle:0, omega:6.0, mass:mass, color:'#a78bfa', label:'長い棒'},
];

shapes.forEach(function(s) {
s.I = s.mass * (s.w*s.w + s.h*s.h) / 12;
s.initialOmega = s.omega;
});

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

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

shapes.forEach(function(s) {
  // 減衰: I が大きいほど減衰しにくい(同じ抵抗トルクで角速度の変化が小さい)
  var drag = -2.5 * s.omega;           // 抵抗トルク(空気抵抗に相当)
  s.omega += (drag / s.I) * dt;        // α = τ/I
  s.angle += s.omega * dt;

  // 描画
  ctx.save();
  ctx.translate(s.x, s.y);
  ctx.rotate(s.angle);
  ctx.fillStyle = s.color;
  ctx.fillRect(-s.w/2, -s.h/2, s.w, s.h);
  ctx.strokeStyle = s.color.replace(')', ',0.5)').replace('hsl','hsla');
  ctx.lineWidth = 2;
  ctx.strokeRect(-s.w/2, -s.h/2, s.w, s.h);
  // 重心マーク
  ctx.beginPath(); ctx.arc(0,0,4,0,Math.PI*2);
  ctx.fillStyle='#fff'; ctx.fill();
  ctx.restore();

  // ラベルと角速度表示
  ctx.fillStyle = s.color;
  ctx.font = 'bold 11px monospace';
  ctx.textAlign = 'center';
  ctx.fillText(s.label, s.x, s.y + s.h/2 + 22);
  ctx.font = '11px monospace';
  ctx.fillText('ω=' + s.omega.toFixed(2) + ' I=' + (s.I|0), s.x, s.y + s.h/2 + 38);
  ctx.textAlign = 'left';
});

ctx.fillStyle = '#555';
ctx.font = '11px monospace';
ctx.fillText('同じ抵抗トルクでも I が大きいほど回転が長く続く', 8, H-10);

requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

3つは同じ質量・同じ初期角速度から始まります。コンパクトな正方形(I小)は早く止まり、長い棒(I大)は長く回り続けます——これが慣性モーメントの「回転しにくさ・止まりにくさ」です。


角速度に減衰を加える

角速度にも線形の減衰を加えることで、自然な摩擦感が生まれます。

// 角速度の減衰(エネルギーが徐々に失われる)
omega += (-damping * omega / I) * dt;

// または単純に係数を掛ける
omega *= (1 - 0.02);  // 毎フレーム2%の減衰

回転する物体の完全な状態

ここまでの知識を統合すると、回転する剛体の完全な状態と更新ループは以下のようになります。

var body = {
  // 位置(重心)
  x: 300, y: 200,
  // 速度
  vx: 100, vy: 0,
  // 質量
  mass: 2.0,
  // 角度(ラジアン)
  angle: 0.5,
  // 角速度(rad/s)
  omega: 1.2,
  // 慣性モーメント
  I: mass * (w*w + h*h) / 12,
  // 形状
  w: 60, h: 40
};

// 毎フレームの更新
function update(dt) {
  // 力の合成(重力など)
  var Fx = 0;
  var Fy = body.mass * GRAVITY;
  var torque = 0;  // 今回は外部トルクなし

  // 直線運動の積分(シンプレクティックオイラー)
  body.vx += (Fx / body.mass) * dt;
  body.vy += (Fy / body.mass) * dt;
  body.x += body.vx * dt;
  body.y += body.vy * dt;

  // 回転の積分
  body.omega += (torque / body.I) * dt;
  body.angle += body.omega * dt;
}

まとめ

この回でやったこと:

  • 角度 θ・角速度 ω・角加速度 α の対応関係を理解した
  • 慣性モーメント(矩形: I = m*(w²+h²)/12)の計算を学んだ
  • トルク(τ = rx * Fy - ry * Fx)の2Dクロス積を実装した
  • ctx.translate() + ctx.rotate() で重心周りの回転描画を実装した
  • クリックで力を加え、トルクによる回転が発生するインタラクティブデモを作った

次回は矩形同士の衝突——AABBと分離軸定理(SAT)を解説します。