前回の第11回「Away3D TypeScriptが2015年3月13日付でビルドを改めた」では、最新のライブラリを使って床の平面に石畳のテクスチャを貼った(再掲第11回図2)。カメラはマウスドラッグで周囲を回る。今回から、お題にした「Animating particles simulating fire」の本論となるパーティクルをつくり始める。パーティクルは仕込みが多く、目に見える動きがなかなか確かめられない。今回、アニメーションを試すのは本稿の終わりのほうになる。しばしご辛抱のうえ、おつき合いいただきたい。
パーティクルのオブジェクトを加える
炎のパーティクルは、床の中心から描く円周上に置こう。パーティクルのオブジェクトをつくる関数(createParticles())はつぎのように定める。引数は、炎の数と炎から立ち上るパーティクル数、炎の平面上の位置を定める中心からの半径と垂直座標、および3次元空間のシーン(View.sceneプロパティ)だ。そして戻り値として、炎のオブジェクトが入った配列を返す。
このパーティクルをつくる関数(createParticles())は、以下の抜書きのように初期設定の関数(initialize())から呼び出す。そして、戻り値の炎のオブジェクトの配列は変数(fireObjects)に納めた。また、少しだけコードを手直しする。StaticLightPickerオブジェクトは初期設定の関数でつくって変数(lightPicker)に納め、床をつくる関数(createPlane())にはそのオブジェクトを引数として渡すことにした。そして、最後の引数である垂直位置は、今回のお題に合うよう少し引上げている(-300から-20へ)。
なお、MethodMaterial()コンストラクタには、とくに引数は渡さなくてよい[1]。したがって、平面をつくる関数(createPlane())でもコンストラクタから引数を省き、require()関数によるDefaultMaterialManagerクラスの読込みも除いた。
パーティクルをつくる関数(createParticles())の中身は以下のとおりだ。もととなる小さな平面のひながたは、PrimitivePlanePrefab()コンストラクタでつくる。渡す第3および第4引数は、x軸方向とyまたはz軸方向それぞれのセグメント数で、デフォルト値は1だ。第5引数は、面の(法線ベクトルの)向きをy軸に合わせるかどうかのブール値で、デフォルト値がtrue、falseはz軸に向ける。
パーティクルは平面で、同じものをたくさんつくる。そこで、いちいちPrefabBase.getNewObject()メソッドは用いず、幾何学情報のGeometryオブジェクト(geometry)をPrimitivePrefabBase.geometryプロパティで得る。これとMethodMaterialオブジェクト(material)を後述のMesh()コンストラクタに渡せば、パーティクルの平面オブジェクトはつくれる。
パーティクルに用いるGeometryオブジェクトは、パーティクルと同じ数(numParticles)だけ配列(geometrySet)に加えた。炎のオブジェクトをつくる関数(getFireObjects())は別に定めて呼び出し、でき上がったオブジェクトの配列(fireObjects)を受け取って返す。
炎のオブジェクトをつくって、配列に入れて返す関数(getFireObjects())はつぎのように定める。第1引数はGeometryオブジェクトを納めた配列、第3引数にはパーティクルに用いる素材オブジェクトを渡す。第4引数のアニメーションの定めについては、後で改めて説明する。他の引数は、この関数を呼び出す前掲のパーティクルをつくる関数(createParticles())が受け取った値だ。
炎のオブジェクトの配列をつくる関数(getFireObjects())が受け取ったGeometryオブジェクトの配列(geometrySet)は、ParticleGeometryHelper.generateGeometry()メソッドに渡してParticleGeometryオブジェクト(particleGeometry)を得る。ひとつひとつの炎のオブジェクト(mesh)は別の関数(createAnimationParticle())でつくり、床の平面の中心から定められた半径(radius)の円周上に等間隔(anglePerFire)で並べたうえで、3次元空間のシーン(scene)に加えた。
炎のオブジェクトをつくって返す関数(createAnimationParticle())は、Meshクラスのコンストラクタに幾何情報(particleGeometry)と素材オブジェクト(material)を引数に渡して、まずMeshオブジェクトにする。そして、このMeshオブジェクト(mesh)と後述するアニメーションのオブジェクト(animator)を渡した炎のオブジェクトがつくられて、関数の引数に受け取った配列(fireObjects)に納められる。関数の戻り値はMeshオブジェクトだ。なお、配列は参照が渡されるので、炎のオブジェクトは前掲呼出し側の関数(getFireObjects())の変数(fireObjects)に加えられることになる。
アニメーションさせる炎のオブジェクトは、以下のクラス(FireObject)で定めた(コード1)。コンストラクタの引数は、つぎのようにMeshオブジェクト(mesh)とパーティクルのアニメーションを定めたParticleAnimatorオブジェクト(animator)だ(ParticleAnimatorオブジェクトは後述)。クラスに加えたメソッド(startAnimation())については後ほど説明する。なお、JavaScriptでクラスをどのように定義するかは、「HTML5のCanvasでつくるダイナミックな表現―CreateJSを使う」第17回「簡単なクラスを定義する」を参照してほしい。
パーティクルにアニメーションを定める
パーティクルのアニメーションは、アニメーションノードのオブジェクトで定める。アニメーションノードをつくるクラスはParticleNodeBaseのサブクラスで、操作するプロパティによって使い分ける。それらのオブジェクトをまとめるのがParticleAnimationSetオブジェクトだ。コンストラクタには、時間の定めを使うかどうかと、アニメーションをループさせるかどうかの2つのブール値を渡す。
そして、ParticleAnimationSetオブジェクトを引数としてParticleAnimator()コンストラクタからつくったオブジェクトを物体のMesh.animatorプロパティに与えればアニメーションが定まる。
ParticleAnimationSetオブジェクトをつくる関数(getParticleAnimationSet())と炎のオブジェクトをつくる関数(getFireObjects())は、つぎのようにともにパーティクルをつくる関数(createParticles())から呼び出される。前者はParticleAnimationSet()コンストラクタが生成したオブジェクトを返す。後者はそのオブジェクトからつくられたParticleAnimatorインスタンスをMesh.animatorプロパティに与えている。
アニメーションノードとして、とりあえず2つのオブジェクトをつくることにする。ひとつは、ParticleBillboardNodeオブジェクトで、パーティクルの角度をつねにカメラに向ける。もうひとつは、パーティクルアニメーションが始まるときの速度を定めるParticleVelocityNodeだ。第1引数のモードに定数ParticlePropertiesMode.GLOBALを与えると、アニメーションのプロパティへの働きがグローバルになる。
そして、アニメーションノードのオブジェクトは、ParticleAnimationSet.addAnimation()メソッドでParticleAnimationSetオブジェクトに加える。
アニメーションノードのオブジェクトは、つぎのようにパーティクルをつくる関数(createParticles())の中で配列(animations)に納めた。その配列をParticleAnimationSetオブジェクトをつくる関数(getParticleAnimationSet())に渡すと、オブジェクトにParticleAnimationSet.addAnimation()メソッドでアニメーションノードすべてが加えられる。
パーティクルのアニメーションを動かす
ようやくパーティクルをアニメーションとして動かせる。炎のMeshオブジェクトとそのMesh.animatorプロパティに与えたParticleAnimatorオブジェクトは、前掲コード1のクラス(FireObject)のコンストラクタに引数として与えてあった。アニメーションを動かすには、それぞれのParticleAnimatorオブジェクトに対してParticleAnimator.start()メソッドを呼び出す。その呼出しは、すでにクラスのメソッド(startAnimation())としてつぎのように定めた。
床はトゥイーンアニメーションしながら表れるので、パーティクルのアニメーションも始まりに待ちを加えよう。時間待ちはTimerクラスで行う。コンストラクタに待ち時間と繰返し回数の2つの引数を与え、TimerEvent.TIMERイベントのリスナー関数に待ち時間が済んだ後の処理を定める。Timer.start()メソッドを呼び出さないと、待ち時間が始まらないことに注意してほしい。
Timerクラスの時間待ちは、つぎのように初期設定の関数(initialize())で定めた。待ち時間が過ぎるごとに、炎のパーティクルをひとつずつアニメーションさせている。プロパティTimer.currentCountは何回目の待ち時間が過ぎたかを示し、Timer()コンストラクタに定めた繰返し回数はTimer.repeatCountで得られる。その回数を終えたら、イベントリスナー(startFire())は除いた。なお、リスナー関数が引数に受取ったイベントオブジェクト(eventObject)のEvent.targetプロパティで、イベントの起こったオブジェクト(ここではTimerインスタンス)が得られる。
パーティクルのアニメーションを動かすにはもうひとつだけ、ParticleAnimationSetオブジェクトのParticleAnimationSet.initParticleFuncプロパティに関数を定めなければならない。ParticleAnimationSetオブジェクトをつくる関数(getParticleAnimationSet())で、つぎのように取りあえず本体が空の関数(initParticle())を書き加えて与えよう。
試してみると、炎の数(3個)だけ小さなパーティクルが立ち上って、画面の上端に消える(図1)。つぎのように引数を定めた前述のパーティクルをつくる関数には、炎の数3個、パーティクルは500個が与えてあったはずだ(createParticles(3, 500, 300, 5, scene))。炎は床を中心とした円周上に等間隔で位置決めした。だが、パーティクルについてはとくに何も定めていないため、すべて重なってしまったのだ。
実は、パーティクルにはParticleAnimationSet.initParticleFuncプロパティに定めた関数で初期値が与えられる。プロパティParticleProperties.startTimeとParticleProperties.durationで、それぞれアニメーションを始める時間と長さが定まる。そこで、パーティクルを初期化する関数(initParticle())で、つぎのようにランダムな時間を与えた。受取る引数(prop)がパーティクルのParticlePropertiesオブジェクトだ。これで、パーティクルのアニメーションの時間がばらつく。
なお、ParticleAnimationSetオブジェクトをつくるコンストラクタの2つの引数には、前述のとおりともにtrueが渡してあった。したがって、パーティクルのアニメーションはつぎの構文のとおりに、定められた時間ずっと繰り返される。改めて試すと、炎の位置からパーティクルがのろしのように立ち上り続ける(図2)。
これまで書いたスクリプトをコード2にまとめた。クラスの定めはコード1のまま変えていない。あわせて、サンプル1をjsdo.itに掲げた。今回はここまでで区切ろう。次回はパーティクルの見た目を炎らしく整える。