#09 ビジュアルコーディング パーティクル

炎エフェクト——揺らめく火を表現する

炎はビジュアルコーディングの定番テーマです。第2回で少し触れましたが、今回は本格的に炎エフェクトを実装します。パーティクルアプローチと、全く異なるピクセルシミュレーションアプローチの2種類を解説します。


炎の視覚的特徴を分析する

炎らしい表現を実装するには、まず実際の炎の特徴を言語化することが重要です。

特徴実装方法
下から上に向かうvy を上向き(マイナス)に設定
揺らぎがある毎フレーム vx にランダムな小さな値を加算
根元が明るく先端が暗い根元では速度が速く生まれ、短命で高いところで消える
色が変化する生まれたて(根元)は白黄色、消える直前(先端)は暗い赤
広がり方根元は細く、上に行くほど広がる

炎パーティクルの色変化

炎の色は下から上に向かって「白→黄→オレンジ→赤→暗い赤」と変化します。HSLで表現すると:

  • 色相: hsl(60, ...) = 黄色(根元)→ hsl(0, ...) = 赤(先端)
  • 明度: 根元は明るく(100%)→ 先端は暗く(30%)

ratio = life / maxLife を使った計算式:

// ratio=1(生まれたて・根元): hsl(60, 100%, 100%) = 白黄色
// ratio=0(消える直前・先端): hsl(0, 100%, 30%) = 暗い赤
var hue = ratio * 60;                         // 0→60
var lightness = 30 + ratio * 70;              // 30→100
var saturation = 100;
ctx.fillStyle = 'hsl(' + hue + ',' + saturation + '%,' + lightness + '%)';

炎パーティクルのデモ

炎パーティクルのデモ——色が変化しながら上昇する
var particles = [];

function loop() {
ctx.fillStyle = 'rgba(13,17,23,0.25)';
ctx.fillRect(0, 0, W, H);

for (var i = 0; i < 6; i++) {
  var angle = -Math.PI / 2 + (Math.random() - 0.5) * 0.8;
  var speed = Math.random() * 3 + 2;
  particles.push({
    x: W / 2 + (Math.random() - 0.5) * 50,
    y: H - 10,
    vx: Math.cos(angle) * speed,
    vy: Math.sin(angle) * speed,
    life: 50 + Math.floor(Math.random() * 30),
    maxLife: 80
  });
}

ctx.globalCompositeOperation = 'lighter';

particles.forEach(function(p) {
  p.x += p.vx;
  p.y += p.vy;
  p.life--;

  var ratio = p.life / p.maxLife;
  var hue = ratio * 60;
  var lightness = 30 + ratio * 70;
  var radius = (3 + ratio * 3) * ratio + 0.5;

  ctx.globalAlpha = ratio * 0.8;
  ctx.fillStyle = 'hsl(' + hue + ',100%,' + lightness + '%)';
  ctx.beginPath();
  ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
  ctx.fill();
  ctx.globalAlpha = 1;
});

ctx.globalCompositeOperation = 'source-over';
particles = particles.filter(function(p) { return p.life > 0; });
requestAnimationFrame(loop);
}
loop();

揺らぎを加えた炎

炎の最も重要な特徴は「揺らぎ」です。毎フレーム vx にランダムな微小な力を加えることで、不規則な揺れが生まれます。

p.vx += (Math.random() - 0.5) * 0.5;

この一行を更新ループに追加するだけで、炎が自然に揺らめきます。さらに上昇するにつれて広がる効果を付け加えます。

// 上に行くほど横方向の揺れを強くする
var ageRatio = 1 - p.life / p.maxLife;  // 0(生まれたて)→1(消える直前)
p.vx += (Math.random() - 0.5) * 0.4 * (1 + ageRatio * 2);
揺らぐ炎——ベースから放射・揺らぎあり
var particles = [];

function loop() {
ctx.fillStyle = 'rgba(13,17,23,0.22)';
ctx.fillRect(0, 0, W, H);

for (var i = 0; i < 8; i++) {
  var spreadX = (Math.random() - 0.5) * 80;
  var speed = Math.random() * 2.5 + 1.5;
  particles.push({
    x: W / 2 + spreadX,
    y: H - 15,
    vx: spreadX * 0.04 + (Math.random() - 0.5) * 0.5,
    vy: -(speed),
    life: 55 + Math.floor(Math.random() * 35),
    maxLife: 90
  });
}

ctx.globalCompositeOperation = 'lighter';

particles.forEach(function(p) {
  var ageRatio = 1 - p.life / p.maxLife;
  p.vx += (Math.random() - 0.5) * (0.3 + ageRatio * 0.8);
  p.vx *= 0.97;
  p.vy -= 0.03;
  p.x += p.vx;
  p.y += p.vy;
  p.life--;

  var ratio = p.life / p.maxLife;
  var hue = ratio * 55;
  var lightness = 25 + ratio * 65;
  var radius = (5 + ratio * 3) * ratio;

  ctx.globalAlpha = ratio * 0.75;
  ctx.fillStyle = 'hsl(' + hue + ',100%,' + lightness + '%)';
  ctx.beginPath();
  ctx.arc(p.x, p.y, Math.max(radius, 0.5), 0, Math.PI * 2);
  ctx.fill();
  ctx.globalAlpha = 1;
});

ctx.globalCompositeOperation = 'source-over';
particles = particles.filter(function(p) { return p.life > 0; });
requestAnimationFrame(loop);
}
loop();

ピクセル炎シミュレーション

パーティクルとは全く異なるアプローチ——ピクセルデータを直接操作する古典的な炎アルゴリズムです。

アルゴリズムの概要:

  1. 画面下端の行にランダムな高い値(熱量)を書き込む
  2. 毎フレーム、各ピクセルの値を「下の行の平均」に置き換えながら少しずつ減衰させる
  3. 値(熱量)を色にマッピングして描画する

このシンプルなルールだけで、現実の炎に近い動きが生まれます。

// 熱量を上方向に拡散・減衰
for (var y = 0; y < H - 1; y++) {
  for (var x = 0; x < W; x++) {
    var sum = heat[y + 1][x - 1 >= 0 ? x - 1 : 0]
            + heat[y + 1][x]
            + heat[y + 1][x + 1 < W ? x + 1 : W - 1]
            + heat[y + 1][x];
    heat[y][x] = Math.max(sum / 4 - decay, 0);
  }
}

色のマッピング:

  • 熱量 0 → 黒(rgb(0,0,0)
  • 熱量 128 → 赤(rgb(255,0,0)
  • 熱量 192 → オレンジ(rgb(255,128,0)
  • 熱量 255 → 白(rgb(255,255,255)
ピクセル炎シミュレーション——古典アルゴリズムによる炎
var scale = 4;
var fw = Math.floor(W / scale);
var fh = Math.floor(H / scale);
var heat = [];
for (var y = 0; y < fh; y++) {
heat[y] = new Float32Array(fw);
}

function heatToColor(v) {
var val = Math.min(v, 255);
var r, g, b;
if (val < 85) {
  r = Math.floor(val * 3); g = 0; b = 0;
} else if (val < 170) {
  r = 255; g = Math.floor((val - 85) * 3); b = 0;
} else {
  r = 255; g = 255; b = Math.floor((val - 170) * 3);
}
return 'rgb(' + r + ',' + g + ',' + b + ')';
}

function loop() {
for (var x = 0; x < fw; x++) {
  heat[fh - 1][x] = Math.random() < 0.6 ? Math.random() * 200 + 55 : 0;
}

for (var y = 0; y < fh - 1; y++) {
  for (var x2 = 0; x2 < fw; x2++) {
    var left = x2 > 0 ? heat[y + 1][x2 - 1] : heat[y + 1][x2];
    var mid = heat[y + 1][x2];
    var right = x2 < fw - 1 ? heat[y + 1][x2 + 1] : heat[y + 1][x2];
    heat[y][x2] = Math.max((left + mid + right + mid) / 4 - 1.5, 0);
  }
}

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

for (var iy = 0; iy < fh; iy++) {
  for (var ix = 0; ix < fw; ix++) {
    var v = heat[iy][ix];
    if (v > 5) {
      ctx.fillStyle = heatToColor(v);
      ctx.fillRect(ix * scale, iy * scale, scale, scale);
    }
  }
}

requestAnimationFrame(loop);
}
loop();

2つのアプローチの比較

観点パーティクル炎ピクセル炎
実装の考え方オブジェクト指向的データ駆動的
制御のしやすさ形状・色を直接制御物理的なルールで自然に生まれる
処理負荷パーティクル数に比例解像度の2乗に比例
カスタマイズ1粒の挙動を変える拡散ルールを変える
用途炎の噴射・たいまつ床全体の炎・背景効果

実際のゲームや演出では、両者を組み合わせることもあります。ピクセル炎を背景に使いつつ、爆発点にはパーティクル炎を重ねるといった使い方です。


炎の根元(ベース)を追加する

炎のリアリティを高めるには、根元に「光り輝く核」を加えます。

// 炎の根元に輝く点を追加
ctx.globalCompositeOperation = 'lighter';
var baseGrad = ctx.createRadialGradient(cx, baseY, 0, cx, baseY, 40);
baseGrad.addColorStop(0, 'rgba(255, 200, 80, 0.8)');
baseGrad.addColorStop(1, 'rgba(255, 100, 0, 0)');
ctx.fillStyle = baseGrad;
ctx.fillRect(cx - 40, baseY - 40, 80, 80);
ctx.globalCompositeOperation = 'source-over';

まとめ

この回でやったこと:

  • 炎を「上向き速度・揺らぎ・色変化(黄→赤)」の3要素で特徴づけた
  • hue = ratio * 60 / lightness = 30 + ratio * 70 で炎らしい色遷移を実装した
  • 毎フレーム vx += random * factor で揺らぎを加えた
  • globalCompositeOperation = 'lighter' で炎の輝きを表現した
  • 熱量配列を上方向に拡散・減衰させる古典ピクセル炎アルゴリズムを実装した

次回はシリーズの総まとめとして、インタラクティブパーティクルを作ります。クリック爆発・ドラッグ軌跡・ホバー反発をひとつのシステムに統合します。