p5.jsでParticle Systemを実装する
はじめに
前回はGenerative Artの一環としてL-systemを実装しましたが、今回はParticle System(パーティクルシステム)を実装しようと思います。 Particle SystemはL-systemと同じく自然現象を描画する手法の一つですが、 集団の動きを制御したり、炎や煙などの境界があいまい(fuzzy)なものを表現することが可能な手法になっています。
fuzzyなものは粒径が小さい多数の粒子の集合体を構成し、それぞれの粒子の位置や動きを制御することで表現されます。 また粒径を大きくし、粒子の運動や粒子同士の相互作用に着目すれば、 物理法則などを自然法則のモデルとして動きを制御することで、 ボールなどの運動の制御、流水、天体、鳥の群れ(Boids)などの自然現象のシミュレーションを行うことができます。
Particle System(パーティクルシステム)の構成
Particle Systemの例としては以下のような構成となります。
- Particle Class
Particle Classは単一粒子の性質を定義します。粒子の大きさや色、寿命、運動(速度/加速度)など各種パラメータを定義するclassになります。
- ParticleSystem Class
ParticleSystem ClassはParticle Classオブジェクト群を制御するためのclassで、複数のParticle Classオブジェクトを保持、生成、削除を行ったり、粒子群としての運動を制御するためのclassになります。
鳥の群れを例にとれば、Partcle Classは鳥1羽1羽、ParticleSystem Classは鳥の群れに相当します。 それぞれのclassに対して、粒子・粒子群の生成、運動のシミュレーション、描画の実装が最低限必要になります。 本実装例では生成はコンストラクタ、シミュレーションはupdateメソッド、描画はdisplayメソッドとして実装しています。
Particle Systemの実装例
ここではParticle Systemの例として、疑似的な花火の描画を行います。 完成すれば以下のように描画されます。
このような描画を行うには、花火一つ一つが以下のように制御されていることが必要です。
- きれいに円状に広がること(粒子速度の制御)
- 粒子の色や大きさに規則があること(粒子の特徴の設定)
- フェードしながらあるきまった時間に消えること(寿命の設定)
Particle Class
class Particle { constructor(location, velocity, acceleration, decay, color, radius) { this.location = location; this.velocity = velocity; this.acceleration = acceleration; this.life = 255.0; this.decay = decay; this.color = color; this.radius = radius; } run() { this.update(); this.display(); } update() { this.acceleration = p5.Vector.mult(this.velocity, -0.01); this.velocity.add(this.acceleration); this.location.add(this.velocity); this.life -= this.decay; } display() { stroke(...this.color, this.life/255); fill(...this.color, this.life/255); strokeWeight(this.radius); line(this.location.x, this.location.y, this.location.x - this.velocity.x, this.location.y - this.velocity.y); } isdead() { return (this.life < 0.0); } }
コンストラクタから各種メンバ変数を設定して、Particleオブジェクトを生成します。 location(位置), velocity(速度), acceleration(加速度)は二次元ベクトル変数になります。 lifeで寿命、decayで寿命が減る速度を決めます。 また描画のための粒子の色(color)と半径(radius)も決めておきます。
updateメソッドとdisplayメソッドはrunメソッドを通じてフレーム毎に呼び出されます。 updateメソッドにより加速度に応じた速度の変更と位置の変更を行い、 lifeがdecay分だけ減らします。lifeが0以下になればParticleオブジェクトを削除します。
displayメソッドでは実際に描画処理を行いますが、今回は粒子形状の円を描画するのではなく太線を描画するようにしています。 これはスムーズに残像を残すための工夫です。 ここで円を描画してしまうと粒子速度が大きい時に飛び飛びに描画されてしまいます。 またalpha値をlife値に依存させることでよりゆっくりと粒子がフェードアウトするような描写となります。
ParticleSystem Class
本実装ではParticleSystem ClassとしてRoundParticlesと名前を付けています。
class RoundParticles { constructor(n, colors, center, speed, angle, avelocity, decayspeed) { this.particles = Array(n); this.center = center; this.avelocity = avelocity; this.angle = angle; let l = colors.length; let radius = random(2,10); for(let i = 0;i < n;++i) { let velocity = createVector(speed * cos(i * TWO_PI/ n), speed * sin(i * TWO_PI/ n)); this.particles[i] = new Particle( createVector(0,0), velocity, createVector(0,0), decayspeed, colors[i%l], radius ); } } run() { this.update(); this.display(); } update() { this.angle = (this.angle + this.avelocity) % TWO_PI; for(let i = this.particles.length-1;i >= 0;--i) { if(this.particles[i].isdead()) { this.particles.splice(i,1); } } } display() { push(); translate(this.center); rotate(this.angle); for(let i = this.particles.length-1;i >= 0;--i) { this.particles[i].run(); } pop(); } }
花火は火球が円状に広がりながら移動するので、コンストラクタは火球の数(n)、広がるスピード(speed)、花火が破裂する位置(center)を設定します。 また花火の輪っかをゆっくりと回転させるために角速度(avelocity)と初期角度(angle)を決めておきます。 これらの変数を用いて長さnのParticleオブジェクトの配列を生成します。
updateメソッドではループを逆から回して配列のparticleオブジェクトを更新し、もしlifeがなくなったらそのオブジェクトを削除します。 また、回転のための角度の変更も行います。
displayメソッドでは花火の輪っかの中心にtranslateで移動しrotateで回転させた後、それぞれの粒子の更新と描画を行います。 こうすることで花火の輪っかの中心とそれぞれの粒子の位置を相対位置として計算できるので、実装しやすくなります。
仕上げ
最後にParticle systemオブジェクトを生成する関数を作成します。 フレーム毎に確率で新たにparticle systemオブジェクトを生成して、何度も花火を生成するようにしています。 なお最初のセットアップ時にカラーモードをHSBに変更しています。
function draw() { background('rgba(0, 0, 0, 0.3)'); for(let i = PSArray.length-1; i >= 0; --i) { PSArray[i].run(); if(PSArray[i].particles.length == 0) { PSArray.splice(i,1); } } if(random(0,1) < 0.06) { generate_roundpatricles(); } } function generate_roundpatricles () { let num = int(random(3,8)); let pos = createVector(random(30, W-30), random(30, H-30)); for(let i = 0; i < num; ++i) { let n_color = Math.pow(2, int(random(1,3))); let color_array = []; for(let co = 0; co < n_color; ++co) { color_array.push([random(0,360), random(50,100), 100]) } PSArray.push( new RoundParticles( Math.pow(2,int(random(2,5))), color_array, pos, random(1,5), random(0, TWO_PI), random(0.001, 0.02), random(1,3) ) ); } }
本実装では行っていませんが、他にも重力や風の影響を考慮したり、火球を途中で破裂させる工夫などもできると思います。 力学的な作用を適用する場合は、粒子に質量を導入して加速度を考慮すると良いかと思います。
まとめ
Particle Systemの実装例として疑似的な花火の描画を行いました。 粒子を定義するParticle Classと粒子群の管理と運動の制御を行うParticleSystem Classの二つを設計することでシンプルに実装することができました。
ただ規模が大きくなってくるとグローバルモードではなく、インスタンスモードで使用したいですね。
Reference
- 書籍 The Nature of Code
- https://en.wikipedia.org/wiki/Particle_system