慣性モーメントと回転——剛体を回転させる
これまでは物体を「点」として扱ってきました。現実の物体は大きさを持ち、回転もします。宇宙空間を漂う宇宙船、床を転がるサイコロ——回転は物理シミュレーションの重要な要素です。
今回は回転の物理を学びます。直線運動の「速度・加速度・質量」と対応する「角速度・角加速度・慣性モーメント」という概念を理解します。
角度・角速度・角加速度
回転の状態は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)。重心からずれた位置に力を加えるとトルクが発生します。
// 同じ質量・同じ初期角速度、でも形が違う → 回転速度の変化が違う
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)を解説します。