今週の進み具合 #12 - パーティクルシステムことはじめ

進み具合 (2014年5月12日 - 5月27日)

前回までスケルタルアニメーションをやってきました。スキニングの実装途中でしたが(実装するのが少々面倒くさくなり)スキニングはひとまず隅に置いて、ブランチをきって新しくパーティクルシステムを実装しました。

(動画) 実装したパーティクルシステムで炎のエフェクトを再生している例

実装したパーティクルシステムは CPU でシミュレーションを行っています。描画はスプライトバッチ(ハードウェアインスタンシング)を利用しています。

描画に使っているパーティクル素材は図1の通りです。上記の動画では 1 秒間に 256 個生成し、最大 2048 個のパーティクルを一回のドローコールでバッチレンダリングをしています。わかりやすいようにパーティクルスプライトをワイアーフレーム表示したものが図2です。

(図 1) パーティクル素材
(図 1) パーティクル素材

(図 2) 左は1秒間に96個のパーティクルを放出した様子。右はそれをワイアーフレーム表示したもの
(図 2) 左は1秒間に96個のパーティクルを放出した様子。右はそれをワイアーフレーム表示したもの

パーティクル効果

グラフィカルなビデオゲームには、視覚効果(VFX, または視覚エフェクトと呼ばれています)が山のように登場します。その1つがパーティクル効果 (パーティクルエフェクト, particle effect) です。

パーティクル効果を利用する例として、爆発や炎、煙、雪、雨、花火などを表現するときが挙げられます。お好きなファンタジー作品を何か1つ思い浮かべてみてください。魔法使いが呪文を唱えると、杖の先から大量の星屑や光の粒が放出され、空気中を舞っているでしょう(図 3 を参照)。古来より魔法使いは演出としてパーティクル効果を利用していたのです。

(図 3) 魔法使いが呪文を唱えると、どこからともなくパーティクルが発生する
(図 3) 魔法使いが杖を振ると、どこからともなくパーティクルが発生する

現代の魔法使いも、C++ や HLSL, GLSL といった闇の呪文を使ってパーティクル効果を実現しています。最近では WebGL で実装されたものもあります。わけもわからず "おまじない" を唱えると後で痛い目を見るのは魔法の世界ではお約束ですので、今回はパーティクル効果についてざっくり見ていきたいと思います。

パーティクルシステム

パーティクル効果を実現するためにビデオゲームではパーティクルシステムを利用します。このパーティクルシステムは大きく分けて次の 2 つの要素から構成されます。

  • パーティクル(particle, 粒子)
  • エミッタ(emitter, 放出器)

パーティクルは小さな粒のことで、砂粒のようなものから、空気中の微細なほこり、粉塵、あるいは光子(可視光の粒)を表現します。このパーティクルを大量にスクリーンに表示することで砂埃や煙、炎やレーザー光線などを表現します。 エミッタはこれら大量のパーティクルの発生源となる放出器のことを指します。

パーティクル

冬の寒い日に外で息をすると白い煙のような吐息が出ます。 ここだけの話、あれもパーティクル効果です。白く見えるものの正体は空気中の微細な水滴です。やがて数秒も経たないうちに、水滴は水蒸気になり見えなくなります。 これを再現するため、パーティクルには寿命または生存期間 (Lifetime または Time to live) が設定されています。寿命をむかえたパーティクルはゲームワールドから姿を消し、見えなくなってしまいます。
また、パーティクルは時間の経過とともに徐々に位置が変化します。実装によっては、色、サイズ、回転量または速度ベクトルなども時間経過に応じて変化します。これにより個々のパーティクルが異なる動きを見せ、ダイナミックで複雑な動きをシミュレートすることができます。例えば、2次元空間でのパーティクルは次のような構造体で表現できます。

struct Particle {
    Vector2 Position;
    Color Color;
    float Rotation;
    float Size;
    float TimeToLive;
};

エミッタ

パーティクルの初期位置や初速はどのようにして決めるのか気になったかと思います。パーティクルの初期状態を決めるのがエミッタの役割です。シンプルなパーティクルシステムを考えてみましょう。パーティクルの初期位置を決めるときに、エミッタの現在位置を利用します。そうすれば、エミッタの位置からパーティクルが放出される様子が想像できるでしょう。エミッタのコード例を示します。

struct ParticleEmitter {
    Vector2 StartPosition;
    Color StartColor;
    float StartRotation;
    float StartSize;
    float StartLifetime;
};

パーティクルの初期状態を決定する役目の他に、1秒間に何個のパーティクルを放出するのか、つまりパーティクルの放出率 (発生頻度)を指定するのもエミッタの役割です。また最大でどれくらいの量のパーティクルが発生するのかを指定するのもエミッタの仕事です。このことをふまえたエミッタのコードは次のようになります。

struct ParticleEmitter {
    Vector2 StartPosition;
    Color StartColor;
    float StartRotation;
    float StartSize;
    float StartLifetime;

    std::uint32_t EmissionRate;
    std::uint32_t MaxParticles;
};

パーティクルの生成とシミュレーション

パーティクルシステムには大きく分けて 2 つのフェーズがあります。

  1. パーティクルの生成・発生
  2. パーティクルのシミュレーション

まずパーティクルの生成フェーズでは、エミッタの発生頻度に合わせてパーティクルを生成します。

static std::vector<Particle> particles;
static float emissionTimer = 0;

void Emit(float frameDuration, ParticleEmitter const& emitter)
{
    emissionTimer += frameDuration;

    // 1 個発生させるのにかかる時間(秒/個)
    auto emissionInterval = 1.0f / emitter.EmissionRate;

    while (emissionTimer >= emissionInterval)
    {
        if (particles.size() < emitter.MaxParticles) {
            break;
        }

        emissionTimer -= emissionInterval;

        Particle particle;
        particle.TimeToLive = emitter.StartLifetime;
        particle.Position = emitter.StartPosition;
        particles.push_back(std::move(particle));
    }
}

パーティクルのシミュレーションフェーズでは、現在発生しているパーティクルの状態(位置や速度など)を生存期間に応じて変化させます。また、生存期間が過ぎたパーティクルオブジェクトをゲームワールドから削除するのもこのフェーズです。

static std::vector<Particle> particles;

void Simulate(float frameDuration)
{
    for (auto & particle: particles)
    {
        Vector2 velocity {3.0f, 4.0f};
        particle.TimeToLive -= frameDuration;
        particle.Position += (velocity * frameDuration);
    }

    particles.erase(std::remove_if(std::begin(particles), std::end(particles),
        [](Particle const& p){ return p.TimeToLive <= 0; }), std::end(particles));
}

この 2 つのフェーズを毎フレーム行うことで大量のパーティクルをシミュレートします。

void Update(float frameDuration, ParticleEmitter const& emitter)
{
    Emit(frameDuration, emitter):
    Simulate(frameDuration);
}

パーティクルシステムの課題

パーティクルシステムをビデオゲームで実装する上で、以下の問題がつきまといます。

  • 大量の粒子を描画する必要があること (A)
  • 大量の粒子の情報を毎フレーム更新する必要があること (B)

(A) はスプライトバッチのようなバッチレンダリングを利用することで解決できます。もう少し言えば、ハードウェアインスタンシング(OpenGL だとジオメトリインスタンシング)を用います。1回のドローコールで大量のパーティクルを描画することができればボトルネックになることはありません。

(B) は発生させるパーティクルの最大数にもよります。現在の計算機であれば 2000 個までだったら、ソフトウェア上で計算して問題ないでしょう。大量の粒子をシミュレートしようとするときは厄介です。例えば 10 万個以上のパーティクルの動きを毎フレーム CPU で計算しようとすれば、おそらくフレームレートが急激に落ちます。そこで大量の粒子をシミュレートするときは GPU を用いて計算するのが昨今の有力なアプローチです。頂点シェーダ・ピクセルシェーダを用いて実装することはもちろん、最近ではコンピュートシェーダを利用するケースが多いようです。 Unreal Engine 4 では GPUSprite として GPU で計算を行うパーティクルシステムを提供しています。GPU を用いると 10 万個以上の大量のパーティクルを表現することができます。また、CPU で実装するとボトルネックになりがちだった複雑なシミュレーションも得意としています。例えば、外部の環境に影響するパーティクルを実装することが出来ます。風に影響を受けたり、重力に影響を受けたり、あるいは物体に衝突したり、とゲームワールドの影響を受けることでインタラクティブな動きを表現することができます。

Note: 現実のパーティクル
砂粒の大きさは 2 mm から 60 μm、花粉の大きさは 10 μm、タバコの煙に含まれる粒子の大きさは 0.01 μm から 1 μm ほどだそうです。本来の粒子(パーティクル)はあまりにも小さく、煙の塊として肉眼で認識するためには莫大な数のパーティクルを発生させる必要があります。 リアルタイムのビデオゲームで、莫大な数のパーティクルをシミュレーションするのはハードウェアの制約上とても難しいです。そこで現在目にするパーティクルシステムの多くは、パーティクル 1 個の大きさが大きかったり、いくつもの微細な粒子の集合を 1 個のパーティクルとしてシミュレーションしています。

柔軟なパーティクルシステム

放出されるパーティクルがすべて、同じ位置から、同じ速度で、同じ向きで放出されたらどうでしょうか。きっと煙には見えないはずです。初期の状態(位置、速度ベクトル、回転角、サイズあるいは色など)を乱数によって決めたり、あるいは時間経過に応じて初期状態を変化させると見た目の面白いパーティクルシステムになります。そこで多くのゲームエンジンでは次のようなエミッタの設定を行えるようになっています。

  • 乱数とその範囲(distribution, variance)を使ったパラメータ制御
  • カーブやコントロールポイントによるパラメータ制御

特に Unity 4 に搭載されているパーティクルシステム Shuriken では Curve Editor と呼ばれるエディタを使って、パーティクルの各状態の変化量を、生存期間や速度に応じて細かく制御できるようになっています。

また、パーティクルシステムで、煙や花火、炎、雪、雨など多くの表現を行うためには、多くの設定をカスタマイズでき、それをランタイムで再生することが重要になってきます。 そのため次のような機能を提供しているゲームエンジンもあります。

  • エミッタの形状(パーティクルの放出角、速度、位置に影響する)
  • 重力または加速度
  • コリジョン
  • サブエミッタ

この中で面白いのがサブエミッタです。パーティクルが寿命を迎えゲームワールドから消えるとともに、そのパーティクルがエミッタに変化します。それがサブエミッタです。打ち上げ花火を表現するときに適した機能です。

今週の予定

  • 雷エフェクト
  • ラインレンダリング

スキニングはすっかり忘れて、雷や電撃を表現するエフェクトを実装します。できればラインレンダリングもやりたいところ。

(はたして来月 6 月に "今週の進み具合 #13" が更新されるのでしょうか。7, 8 月になったらごめんなさい。)

Leave a Reply