JavaScriptで取り組むクリエイティブコーディング
パーティクル表現入門

69
50
49

HTML Canvas要素とJavaScriptを使うと、手軽にクリエイティブコーディングをはじめられます。

前回の記事「パーリンノイズを使いこなせ」に引き続き、先月7月25日に開催されたイベント「Frontend de KANPAI! #4」の登壇内容を記事として紹介します。

本記事ではHTML CanvasとJavaScriptの理解につながることを目標に、次のパーティクル表現の作成テクニックを解説します。サンプルのソースコードはすべてGitHubにて公開していますので、あわせて参照ください。

▲ 完成版サンプル。実装する上で重要な表現のエッセンスだけを絞って解説します

ステップ① パーティクル表現

シンプルなグラフィックから手順を理解していきましょう。パーティクルを定義するのに、座標と速度、寿命を用意します。オブジェクトにプロパティを定義するだけです。

const particles = [];
// パーティクルを発生させます
function emit() {
  // オブジェクトの作成
  const particle = {
    x: stageW * 0.5, // パーティクルの発生場所(X)
    y: (stageH * 4) / 5, // パーティクルの発生場所(Y)
    vy: 0, // 速度
    life: MAX_LIFE // 寿命
  };

  // 配列に保存
  particles.push(particle);
}

アニメーション用の関数requestAnimationFrame()によって、時間経過でupdate()関数を呼び出します。関数のなかでは、パーティクルに対して重力を加えたり、摩擦を計算します。いつか消えるように寿命も減らしておくことも必要です。

// パーティクルを更新します
function update() {
  // パーティクルの計算を行う
  for (let i = 0; i < particles.length; i++) {
    // オブジェクトの作成
    const particle = particles[i];
    // 重力
    particle.vy -= 1;
    // 摩擦
    particle.vy *= 0.92;
    // 速度を位置に適用
    particle.y += particle.vy;

    // 寿命を減らす
    particle.life -= 1;
    // 寿命の判定
    if (particle.life <= 0) {
      // 配列からも削除
      particles.splice(i, 1);
      i -= 1;
    }
  }
}

描画は次のようにHTML Canvasに描きます。配列particlesに格納したパーティクルの情報を使います。

// 描画します
function draw(time) {
  // 画面をリセット
  context.clearRect(0, 0, stageW, stageH);

  particles.forEach(particle => {
    context.beginPath();
    // 白い色を設定
    context.fillStyle = `white`;
    // 円を描く
    context.arc(
      particle.x,
      particle.y,
      100,
      0,
      Math.PI * 2,
      false
    );
    // 形状に沿って塗る
    context.fill();
    context.closePath();
  });
}

ステップ② 寿命でスケール変化

パーティクルの寿命を使って変化を持たせます。発生時のパーティクルは大きいサイズとし、寿命を迎えて死滅するときには小さいサイズにします。

寿命に対する生存期間の割合は計算によって求まるので、その値をスケールに割り当てます。

// パーティクルのサイズを寿命に比例にする
const scale = particle.life / MAX_LIFE;
particle.scale = scale;

particle.life -= 1; // 寿命を減らす
if (particle.life <= 0) {
  // 寿命の判定
  // 削除
}

ステップ③ 数を増やす

時間経過でパーティクルがたくさん発生するようにします。発生位置に乱数を持たせて、横幅いっぱいにパーティクルを発生するようにしておきましょう。

// オブジェクトの作成
const particle = {
  x: stageW * Math.random(), // パーティクルの発生場所(X)
  y: (stageH * 3) / 4, // パーティクルの発生場所(Y)
  vy: 30 * (Math.random() - 0.5), // 速度
  life: MAX_LIFE // 寿命
};

ステップ④ 形状を増やし点滅させる

形状を増やすために、パーティクル発生時に形状の種類を示すtypeプロパティを設定します。

// オブジェクトの作成
const particle = {
  // 種類を定義
  type = Math.floor(Math.random() * 2).toString();
  // 他いろいろ・・・
};

typeプロパティに値によって描画の形状を切り替えます。このとき、透明度は80%〜100%の値になるように乱数を使って設定します。時間経過で乱数によって透明度が変化するため、点滅しているような表現になります。

context.beginPath();
context.arc(particle.x, particle.y, ・・・);

// 点滅ロジック
const alpha = Math.random() * 0.2 + 0.8;

switch (particle.type) {
  case '0':
    context.fillStyle = `hsla(0, 0%, 50%, ${alpha})`;
    context.fill();
    break;
  case '1':
    context.strokeStyle = `hsla(0, 0%, 50%, ${alpha})`;
    context.lineWidth = 5;
    context.stroke();
    break;
}
context.closePath();

ステップ⑤ 乱数を使いこなせ

乱数の制御テクニックを紹介します。乱数を使ってパーティクルの発生位置を調整すると、表現にバリエーションを増やせます。JavaScriptにはMath.random()という一様分布の乱数生成命令があります。この乱数を加減乗除するといろんなバリエーションが得られます。

中央に偏らせる

const x = (Math.random() + Math.random()) / 2;

中央に強く偏らせる

const valueA = (Math.random() + Math.random()) / 2;
const valueB = (Math.random() + Math.random()) / 2;
const x = (valueA + valueB) / 2;

端に偏らせる

const base = Math.random() * Math.random() * Math.random();
const inverse = 1.0 - base;
const x = Math.random() < 0.5 ? base : inverse;

ここで紹介したランダムの計算方法は一部です。詳しくは記事「JavaScript開発に役立つ重要なランダムの数式まとめ」にまとめているので、参照ください。

ステップ⑥ 最適化手法としてのオブジェクトプール

パーティクル表現は、大量のオブジェクトの生成・破棄を繰り返します。JavaScriptにはメモリ管理の機構としてガベージコレクション(略してGCといいます)が存在します。どこからも参照されておらず不要になったゴミオブジェクトを自動的に回収することで、メモリ使用量を抑えます。

一見便利そうなガベージコレクションは、実は重たい処理です。ガベージコレクションが発生すると、数ミリ秒の負荷が発生します。数ミリ秒というのは、表現の世界ではシビアな時間です。ウェブコンテンツは一般的に60fpsであり、1フレームあたり16ミリ秒しか時間がありません。16ミリ秒で1フレームの描画更新処理を終わらせなければならないのに、数ミリ秒もガベージコレクションに時間が取られてしまうのは、厳しい状況と言わざるを得ません。

もし描画更新処理が16ミリ秒を超えてしまえば、一瞬止まったようなプチフリーズが発生します。そこで対策となるのがオブジェクトプールです。

参照が外れると、ガベージコレクションの対象となります。メモリ使用量が多いと、強烈なGCを引き起こす可能性があります。

オブジェクトプールはガベージコレクションの対象にならないように、あえて参照を保持する方法です。インスタンスはプールと呼ばれる配列に保持されるので、ガベージコレクションの対象にはなりません。

次のコードはオブジェクトプールの簡易的な実装例です。

const objectPool = []; // オブジェクトプール

function toPool(particle) {
  // 使用済み粒子はプールに移動
  objectPool.unshift(particle);
}

function fromPool() {
  if (objectPool.length === 0) {
    // プールが空なら新規生成
    return new Particle();
  } else {
    // プールにストックがあれば取り出す
    return objectPool.pop();
  }
}

詳しくは次の記事「ゲームにおけるガベージコレクションとの付き合い方 - Qiita」が参考になるでしょう。内容はFlashの話題ですが、JavaScriptでも同じことがいえます。いまもFlash時代の資産から学べることが多いですね。

まとめ

今回の記事はパーティクル作り方をかいつまんで説明しました。基本的には、簡単な物理演算とJavaScriptの配列管理がキモです。記事「CreateJS でパーティクルシステムの開発に挑戦しよう」ではもっと初心的なことから解説しているので、あわせて参照ください。

池田 泰延

ICS代表。筑波大学 非常勤講師。ICS MEDIA編集長。個人実験サイト「ClockMaker Labs」のようなビジュアルプログラミングとUIデザインが得意分野です。

この担当の記事一覧