重力と風——物理が加わると世界になる
前回まで、パーティクルは等速直線運動をするだけでした。ここに重力・風力・空気抵抗という「力」を加えると、まったく違う世界が生まれます。重力だけで噴水になり、バウンスを加えれば地面が生まれます。
物理シミュレーションは難しそうに聞こえますが、パーティクルの場合はとてもシンプルです。速度に毎フレーム少しだけ加算するだけで表現できます。
物理の基本——加速度と速度の関係
物理の世界では「力は速度の変化(加速度)を引き起こす」という関係があります。これをコードに落とし込むと:
加速度 → 速度を変化させる
速度 → 位置を変化させる
// 毎フレームの更新
p.vy += gravity; // 重力によって、下向きの速度が増える(加速)
p.y += p.vy; // 速度によって位置が変化する
gravity は小さな定数(0.1〜0.3 程度)で、毎フレームわずかに vy を増やし続けます。最初は上向きだった vy がだんだん下向きになり、放物線を描きます。
重力の追加——噴水を作る
上向きに強い初速を与えてから重力を加えると、放物線を描く噴水になります。
// 生成時: 上向きの速度を与える
vy: -(Math.random() * 5 + 3) // マイナスで上向き
// 毎フレームの更新
p.vy += 0.15; // 重力(下向きに加速)
p.x += p.vx;
p.y += p.vy;
var particles = [];
function loop() {
ctx.fillStyle = 'rgba(13,17,23,0.2)';
ctx.fillRect(0, 0, W, H);
for (var i = 0; i < 4; i++) {
var spread = (Math.random() - 0.5) * 2;
particles.push({
x: W / 2,
y: H - 10,
vx: spread,
vy: -(Math.random() * 6 + 3),
life: 120, maxLife: 120
});
}
particles.forEach(function(p) {
p.vy += 0.15;
p.x += p.vx;
p.y += p.vy;
p.life--;
var ratio = p.life / p.maxLife;
ctx.globalAlpha = ratio;
ctx.fillStyle = 'hsl(200, 80%, 70%)';
ctx.beginPath();
ctx.arc(p.x, p.y, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
});
particles = particles.filter(function(p) {
return p.life > 0 && p.y < H + 10;
});
requestAnimationFrame(loop);
}
loop(); 風力——横方向に一定の力を加える
重力と同じ仕組みで、横方向(vx)に力を加えると風になります。
p.vx += wind; // wind = 0.05 などの小さな定数
重力(下向きの一定加速)と風(横向きの一定加速)を組み合わせると、斜めに流れていくパーティクルが作れます。
var particles = [];
function loop() {
ctx.fillStyle = 'rgba(13,17,23,0.15)';
ctx.fillRect(0, 0, W, H);
for (var i = 0; i < 5; i++) {
var angle = Math.random() * Math.PI * 2;
var speed = Math.random() * 2 + 1;
particles.push({
x: W / 2,
y: H / 2,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 90, maxLife: 90
});
}
particles.forEach(function(p) {
p.vx += 0.04; // 右方向の風
p.vy += 0.08; // 弱い重力
p.x += p.vx;
p.y += p.vy;
p.life--;
var ratio = p.life / p.maxLife;
ctx.globalAlpha = ratio;
ctx.fillStyle = 'hsl(140, 70%, 65%)';
ctx.beginPath();
ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
});
particles = particles.filter(function(p) { return p.life > 0; });
requestAnimationFrame(loop);
}
loop(); 空気抵抗——フレームごとに速度を減衰させる
現実世界では、空気抵抗によって物体は徐々に減速します。これをコードで再現するには、速度に 1.0 未満の係数をかけ続けます。
p.vx *= 0.99; // 毎フレーム速度が1%減る
p.vy *= 0.99;
0.99 のような値でも、フレームが積み重なると大きな差が生まれます。60フレーム後には 0.99^60 ≈ 0.547、つまり速度が半分以下になります。
空気抵抗の強さの目安:
0.999— ほとんど感じない、宇宙空間的0.99— 緩やかな減衰、水中っぽい0.95— 強い減衰、重い物体的0.9— かなり強い、爆発の残滓的
var psA = [], psB = [];
function spawn(arr, ox) {
for (var i = 0; i < 3; i++) {
var angle = Math.random() * Math.PI * 2;
var speed = Math.random() * 5 + 2;
arr.push({
x: ox, y: H / 2,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 120, maxLife: 120
});
}
}
function loop() {
ctx.fillStyle = 'rgba(13,17,23,0.2)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = 'rgba(255,255,255,0.08)';
ctx.fillRect(W / 2, 0, 1, H);
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.font = '13px monospace';
ctx.fillText('空気抵抗あり', 10, 20);
ctx.fillText('空気抵抗なし', W / 2 + 10, 20);
spawn(psA, W / 4);
spawn(psB, W * 3 / 4);
[psA, psB].forEach(function(ps, idx) {
ps.forEach(function(p) {
if (idx === 0) { p.vx *= 0.96; p.vy *= 0.96; }
p.x += p.vx; p.y += p.vy; p.life--;
var ratio = p.life / p.maxLife;
ctx.globalAlpha = ratio;
ctx.fillStyle = idx === 0 ? 'hsl(40,100%,70%)' : 'hsl(200,80%,70%)';
ctx.beginPath();
ctx.arc(p.x, p.y, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
});
if (idx === 0) psA = ps.filter(function(p) { return p.life > 0; });
else psB = ps.filter(function(p) { return p.life > 0; });
});
requestAnimationFrame(loop);
}
loop(); 地面バウンス——衝突と反発を実装する
パーティクルが地面(画面下端)に当たったときに反発させると、弾む動きが生まれます。
if (p.y > H - 5) {
p.y = H - 5; // 地面にめり込まないよう位置を修正
p.vy = -p.vy * 0.6; // Y方向の速度を反転&減衰
}
0.6 は反発係数です。1.0 なら完全弾性衝突(エネルギー損失なし)、0.0 なら完全非弾性衝突(バウンドしない)です。0.4〜0.7 が自然に見えます。
地面の横方向の摩擦も加えると、より自然です。
if (p.y > H - 5) {
p.vy = -p.vy * 0.6;
p.vx *= 0.85; // 地面との摩擦
}
var particles = [];
function loop() {
ctx.fillStyle = 'rgba(13,17,23,0.3)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(0, H - 4, W, 4);
if (particles.length < 200) {
for (var i = 0; i < 4; i++) {
var angle = -Math.PI / 2 + (Math.random() - 0.5) * 1.5;
var speed = Math.random() * 6 + 3;
particles.push({
x: W / 2 + (Math.random() - 0.5) * 20,
y: H - 10,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 180, maxLife: 180,
hue: Math.random() * 60 + 180
});
}
}
particles.forEach(function(p) {
p.vy += 0.2;
p.x += p.vx;
p.y += p.vy;
if (p.y > H - 5) {
p.y = H - 5;
p.vy = -p.vy * 0.55;
p.vx *= 0.82;
}
p.life--;
var ratio = p.life / p.maxLife;
ctx.globalAlpha = ratio;
ctx.fillStyle = 'hsl(' + p.hue + ', 70%, 65%)';
ctx.beginPath();
ctx.arc(p.x, p.y, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
});
particles = particles.filter(function(p) {
return p.life > 0 && Math.abs(p.vy) > 0.1;
});
requestAnimationFrame(loop);
}
loop(); 壁への反発
側面の壁(左端・右端)でも同様に反発させられます。
if (p.x < 0) {
p.x = 0;
p.vx = -p.vx * 0.7; // 左壁で反発
}
if (p.x > W) {
p.x = W;
p.vx = -p.vx * 0.7; // 右壁で反発
}
地面・天井・左右の壁すべてに反発を実装すると、箱の中で跳ね回るパーティクルが作れます。
まとめ
この回でやったこと:
p.vy += gravityでフレームごとに下向き加速度を与えたp.vx += windで横方向の風力を実装したp.vx *= 0.99で空気抵抗による減速を実装したp.y > Hの判定で地面バウンスを実装し、反発係数で跳ね返りを調整した
物理の力はすべて「速度への毎フレームの加算・乗算」で表現できます。複数の力を組み合わせると、それだけで自然界の現象に近い動きが生まれます。
次回は引力場・渦場・フローフィールドを使った、より複雑な力の場を実装します。パーティクルが特定のポイントに引き寄せられたり、渦を巻いたりするエフェクトです。