サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Away3D: パーティクルのアニメーション

ID: FN1405001 Technique: HTML5 and JavaScript Library: Away3D TypeScript

オープンソースのリアルタイム3Dエンジン「Away3D」のJavaScriptへの移植が、「Away3D TypeScript」ライブラリとして進められています。すでにノート「Away3D: 立方体を回してみる」では、このAway3D TypeScriptを使ったごく簡単なJavaScriptコードで、3次元空間に立方体をつくって回しました。本稿は、さらにパーティクルのアニメーションで、燃える炎を表現してみます。


01 Away3D TypeScriptのライブラリとサンプル

Away3D TypeScript」サイトには、作例やソースファイルのリンクなどが掲げられています。ライブラリのダウンロードと基本的な使い方については、前出ノート「Away3D: 立方体を回してみる」をお読みください。今回つくるサンプルで参考にしたのは、ページの「Examples」の欄に掲げられた「Animating particles simulating fire」です(図001)。

図001■Away3D TypeScriptサイトの作例「Animating particles simulating fire」
図001

これから書くサンプルスクリプトは、この作例のソースコードをわかりやすく簡略化し、整理してまとめたものです。また、スクリプトの組立ては前出「Away3D: 立方体を回してみる」のJavaScriptコードと基本的に揃えてあります。

Away3Dのコードを試すときには、ライブラリのビルドの時期にお気をつけください。本稿では2014年8月18日にアップロードされたライブラリを使っています(図002)。2014年8月15日までは、ライブラリのJavaScript(JS)ファイルふたつの名前が異なります(「Away3D: 立方体を回してみる」注[*1]参照)。それより前のビルドでは、本稿のコードはエラーを起こして動きません。Away3D TypeScriptはまだアルファリリースですので、ビルドのたびに以前とは互換性がなくなる場合も少なくないでしょう。

図002■Away3Dで使う3つのライブラリのJSファイルとタイムスタンプ
図002


02 3次元空間を定める

パーティクルの炎を燃やすには、演出として火をともす床がほしいです。Away3Dの3次元空間に平面の床を敷きましょう。基本的な手順は、前出ノート「Away3D: 立方体を回してみる」で立方体をつくったのと変わりません。ただ、置く物体を平面にするだけです。

仕込みとしてHTMLドキュメントには、まずscript要素にAway3Dの3つのJSファイルawayjs-core.next.min.jsとstagegl-core.next.min.jsおよびstagegl-extensions.next.min.jsを読込みます。つぎに、JavaScriptコードには初めに呼出す関数(initialize())を設け、body要素のonload属性にその呼出しを加えます。

<script src="lib/awayjs-core.next.min.js"></script>
<script src="lib/stagegl-core.next.min.js"></script>
<script src="lib/stagegl-extensions.next.min.js"></script>
<script>
function initialize() {
  // 初期設定
}
</script>


<body onload="initialize();">

3次元空間に平面を置くまでのJavaScriptの記述は、後にコード001としてまとめました。以下に抜書きする行番号はコード001にもとづきます。前出ノート「Away3D: 立方体を回してみる」02「Viewクラスで3次元空間の表示領域を定める」でご説明したとおり、Away3Dの3次元空間はViewクラスで定めます。

Viewオブジェクトをつくって返す関数(createView())はつぎのように定め、以下の抜書きのように初期設定の関数(initialize())から呼出します(第7行目)。

createView(幅, 高さ)

Viewオブジェクトをつくる関数(createView())は、レンダラのDefaultRendererオブジェクト(defaultRenderer)をつくり、View()コンストラクタに引数として渡します(第17〜18行目)。そして、でき上がったViewオブジェクト(view)の幅と高さは関数の引数値(widthとheight)で定めて、そのオブジェクトを返します(第19〜21行目)。

  1. var view;
  1. function initialize() {
  2.   view = createView(400, 300);
  1. }
  2. function createView(width, height) {
  3.   var defaultRenderer = new away.render.DefaultRenderer();
  4.   var view = new away.containers.View(defaultRenderer);
  5.   view.width = width;
  6.   view.height = height;
  7.   return view;
  8. }

03 ライトを決める

3次元空間に光を与えます。前出ノート「Away3D: 立方体を回してみる」03「DirectionalLightクラスで平行光源を定める」と同じく、DirectionalLight()コンストラクタで平行光源のオブジェクトをつくります。そのための関数(createDirectionalLight())はつぎのように定め、オブジェクトのプロパティに与える5つの引数を渡すことにしましょう。

createDirectionalLight(環境光の強さ, 光源の色, 拡散反射の強さ, 鏡面反射の強さ, 環境光の色)

DirectionalLightクラスはLightBaseを継承します。そして、そのオブジェクトには次表001のようなプロパティが与えられます。directionプロパティがDirectionalLightクラス、あとのプロパティはスーパークラスのLightBaseに備わっています。

DirectionalLight → LightBase
表001■LightBaseおよびそのサブクラスの基本的なプロパティ
LightBaseとそのサブクラスのプロパティ プロパティの値
direction 光源の方向を示すVector3Dオブジェクト。デフォルト値は(0, -1, 1)。
ambient 環境光の強さを示す0から1までの数値。
color 光源の色を示すRGBカラー値。デフォルト値は白(0xFFFFFF)。
diffuse 拡散反射する光の強さを示す0以上の数値。デフォルト値は1。
specular 鏡面反射する光の強さを示す0以上の数値。デフォルト値は1。
ambientColor 環境光の色を示すRGBカラー値。デフォルト値は白(0xFFFFFF)。

DirectionalLightオブジェクトをつくる関数(createDirectionalLight())は、受取った5つの引数にもとづいて、光源の方向(direction)と環境光の強さ(ambient)のほか、拡散反射と鏡面反射それぞれの強さ(diffuseとspecular)、および環境光の色(ambientColor)をプロパティに定めます(第36〜45行目)。この関数は、初期設定の関数(initialize())から呼出します(第10行目)。

  1. var directionalLight;
  1. function initialize() {
  1.   directionalLight = createDirectionalLight(0.5, 0xEEDDDD, 0.5, 0, 0x808090);
  1. }
  1. function createDirectionalLight(ambient, color, diffuse, specular, ambientColor) {
  2.   var light = new away.entities.DirectionalLight();
  3.   light.direction = new away.geom.Vector3D(0, -1, 0);
  4.   light.ambient = ambient;
  5.   light.color = color;
  6.   light.diffuse = diffuse;
  7.   light.specular = specular;
  8.   light.ambientColor = ambientColor;
  9.   return light;
  10. }

04 平面のオブジェクトをつくって3次元空間に置く

3次元空間に置く物体は平面です。平面のひながた(プレハブ)は、PrimitivePlanePrefab()コンストラクタに、幅と高さを引数に渡してつくります。

new away.prefabs.PrimitivePlanePrefab(幅, 高さ)

ひながたのPrimitivePlanePrefabオブジェクトにgetNewObject()メソッドを呼出すと、平面のオブジェクトがつくられます。その後、物体に表面素材(マテリアル)やライトを加えて、3次元空間に置くまでの流れは、基本的に前出ノート「Away3D: 立方体を回してみる」04「PrimitiveCubePrefabクラスからつくった立方体のオブジェクトを水平および垂直の向きに回す」で述べた立方体の場合と同じです。

平面のオブジェクトをつくって3次元空間に加える関数は、つぎのように定めます。第3および第4引数のライトと鏡面反射は、素材のオブジェクトのプロパティに与えます(表002)。第6引数のシーンは、平面を加える3次元空間のView.sceneプロパティの参照です。

createPlane(幅, 高さ, ライト, 鏡面反射, 垂直座標, シーン)
表002■MaterialBaseおよびそのサブクラスの基本的なプロパティ
MaterialBaseとそのサブクラスのプロパティ プロパティの値
lightPicker MaterialBaseオブジェクトが用いるLightPickerBaseオブジェクト。照明をサポートすれば素材がそのライトで照らされる。
repeat 使われているテクスチャをタイル状に並べるかどうかのブール値。デフォルト値はfalseで、テクスチャの端のuv座標が0から1までの範囲を超えたサンプルはクランプされる。
blendMode レンダリングに使うブレンドモードを示す文字列。つぎの定数から定める。
  • BlendMode.NORMAL   標準(デフォルト)
  • BlendMode.LAYER   レイヤー
  • BlendMode.MULTIPLY   乗算
  • BlendMode.ADD   加算
  • BlendMode.ALPHA   アルファ
specular 鏡面反射の強さを示す数値。

今回素材のオブジェクトは、TriangleMethodMaterialクラスでつくります[*1]。このクラスはTriangleMaterialBaseとStageGLMaterialBaseを継承し、さらにそのスーパークラスがMaterialBaseです。前掲表002のプロパティのうちlightPickerrepeatおよびblendModeはMaterialBaseクラス、specularがTriangleMethodMaterialクラスに備わっています。MaterialBase.blendModeプロパティは後の項で使います。

TriangleMethodMaterial → TriangleMaterialBase → StageGLMaterialBase → MaterialBase

なお、MaterialBase.repeatプロパティは、素材オブジェクトがテクスチャより大きい場合の貼りつけ方(マッピング)を示します。trueにすると、テクスチャはタイル状に並べて貼られます。デフォルト値のfalseはクランプで、テクスチャの端のピクセルのカラーがそのまま続けて用いられます。(イメージは「クランプ テクスチャ アドレシング モード」をご参照ください)。

平面のオブジェクトをつくる関数(createPlane())は、以下のように初期設定の関数(initialize())から呼出します(第11行目)。でき上がったオブジェクトが関数から返されるので、変数(mesh)に納めています。

呼出された関数(createPlane())は、引数の幅(width)と高さ(height)でPrimitivePlanePrefabオブジェクトをつくり、PrefabBase.getNewObject()メソッドにより平面のオブジェクト(mesh)を得ます(第26行目)。そして、DefaultMaterialManager.getDefaultTexture()メソッドでつくったテクスチャをTriangleMethodMaterial()コンストラクタに渡して素材のオブジェクト(material)にします(第24〜25行目)。素材オブジェクトは、平面のオブジェクトのMesh.materialプロパティに与えます(第27行目)。

引数に受取ったライト(light)や鏡面反射(specular)は素材オブジェクト(material)、垂直座標(y)を平面のオブジェクト(mesh)に定めます(第28〜29および第31〜32行目)。また、MaterialBase.repeatプロパティはtrueとしました(第30行目)。そして、引数で得たシーン(scene)に平面のオブジェクトを加えたうえで、返します(第33〜34行目)。

  1. var mesh;
  1. var lightPicker;
  1. function initialize() {
  1.   var scene = view.scene;
  1.   mesh = createPlane(800, 800, directionalLight, 10, -20, scene);
  1. }
  1. function createPlane(width, height, light, specular, y, scene) {
  2.   var defaultTexture = away.materials.DefaultMaterialManager.getDefaultTexture();
  3.   var material = new away.materials.TriangleMethodMaterial(defaultTexture);
  4.   var mesh = new away.prefabs.PrimitivePlanePrefab(width, height).getNewObject();
  5.   mesh.material = material;
  6.   lightPicker = new away.materials.StaticLightPicker([light]);
  7.   material.lightPicker = lightPicker;
  8.   material.repeat = true;
  9.   material.specular = specular;
  10.   mesh.y = y;
  11.   scene.addChild(mesh);
  12.   return mesh;
  13. }

[*1] TriangleMethodMaterialは、2014年8月15日付ビルドでTriangleMaterialに替わって備わったクラスです(「Away3D: 立方体を回してみる」注[*4]参照)


05 カメラのコントローラを加える

カメラのコントローラを加えると、カメラワークを採入れたアニメーション表現ができます。HoverControllerクラスでは、カメラのパンとチルトができ、アニメーションにはトゥイーンが加わります。

コントローラを定める関数(setupCameraController())は、以下のように初期設定の関数(initialize())から呼出します(第12および第46〜54行目)。また、カメラワークをアニメーションさせるため、RequestAnimationFrame()メソッドにコールバック関数(render())を定めました(第13〜14行目および第55〜57行目)。これらの処理は、前出ノート「Away3D: 立方体を回してみる」05「映し出される立方体の角度をカメラの動きで変化させる」とほとんど同じです。中身については、このノートの解説をお読みください。

  1. var timer;
  1. var cameraController;
  2. function initialize() {
  1.   cameraController = setupCameraController(view.camera, 1000, 0, 90, 45, 20);
  2.   timer = new away.utils.RequestAnimationFrame(render);
  3.   timer.start();
  4. }
  1. function setupCameraController(camera, distance, minTiltAngle, maxTiltAngle, panAngle, tiltAngle) {
  2.   var cameraController = new away.controllers.HoverController(camera);
  3.   cameraController.distance = distance;
  4.   cameraController.minTiltAngle = minTiltAngle;
  5.   cameraController.maxTiltAngle = maxTiltAngle;
  6.   cameraController.panAngle = panAngle;
  7.   cameraController.tiltAngle = tiltAngle;
  8.   return cameraController;
  9. }
  10. function render(timeStamp) {
  11.   view.render();
  12. }

ここまで解説した関数やその呼出しなどをすべてまとめたのが、以下のコード001です。3次元空間に平面のオブジェクトが置かれ、カメラは予め定めたアングルまでトゥイーンアニメーションします(図003)。この平面を床として、その上に炎のパーティクルを加えてゆきます。なお、このコードはサンプル001として、jsdo.itにも掲げました。

図003■3次元空間に平面が置かれてカメラは予め定められたアングルにトゥイーンする
図003

コード001■3次元空間に平面のオブジェクトを置いてカメラは予め定めたアングルにトゥイーンさせる
  1. var view;
  2. var directionalLight;
  3. var mesh;
  4. var timer;
  5. var lightPicker;
  6. var cameraController;
  7. function initialize() {
  8.   view = createView(400, 300);
  9.   var scene = view.scene;
  10.   directionalLight = createDirectionalLight(0.5, 0xEEDDDD, 0.5, 0, 0x808090);
  11.   mesh = createPlane(800, 800, directionalLight, 10, -20, scene);
  12.   cameraController = setupCameraController(view.camera, 1000, 0, 90, 45, 20);
  13.   timer = new away.utils.RequestAnimationFrame(render);
  14.   timer.start();
  15. }
  16. function createView(width, height) {
  17.   var defaultRenderer = new away.render.DefaultRenderer();
  18.   var view = new away.containers.View(defaultRenderer);
  19.   view.width = width;
  20.   view.height = height;
  21.   return view;
  22. }
  23. function createPlane(width, height, light, specular, y, scene) {
  24.   var defaultTexture = away.materials.DefaultMaterialManager.getDefaultTexture();
  25.   var material = new away.materials.TriangleMethodMaterial(defaultTexture);
  26.   var mesh = new away.prefabs.PrimitivePlanePrefab(width, height).getNewObject();
  27.   mesh.material = material;
  28.   lightPicker = new away.materials.StaticLightPicker([light]);
  29.   material.lightPicker = lightPicker;
  30.   material.repeat = true;
  31.   material.specular = specular;
  32.   mesh.y = y;
  33.   scene.addChild(mesh);
  34.   return mesh;
  35. }
  36. function createDirectionalLight(ambient, color, diffuse, specular, ambientColor) {
  37.   var light = new away.entities.DirectionalLight();
  38.   light.direction = new away.geom.Vector3D(0, -1, 0);
  39.   light.ambient = ambient;
  40.   light.color = color;
  41.   light.diffuse = diffuse;
  42.   light.specular = specular;
  43.   light.ambientColor = ambientColor;
  44.   return light;
  45. }
  46. function setupCameraController(camera, distance, minTiltAngle, maxTiltAngle, panAngle, tiltAngle) {
  47.   var cameraController = new away.controllers.HoverController(camera);
  48.   cameraController.distance = distance;
  49.   cameraController.minTiltAngle = minTiltAngle;
  50.   cameraController.maxTiltAngle = maxTiltAngle;
  51.   cameraController.panAngle = panAngle;
  52.   cameraController.tiltAngle = tiltAngle;
  53.   return cameraController;
  54. }
  55. function render(timeStamp) {
  56.   view.render();
  57. }

サンプル001■Away3D 14/08/26 : PrimitivePlane and HoverController of the camera


06 パーティクルのオブジェクトを加える

3次元空間に置いた平面の上に炎のパーティクルを加えましょう。パーティクルのオブジェクトをつくる関数(createParticles())はつぎのように定めます。引数は、炎の数と炎から立ち上るパーティクル数、炎の平面状の位置を定める中心からの半径と垂直座標、および3次元空間のシーン(View.sceneプロパティ)です。そして、戻り値として、炎のオブジェクトが入った配列を返します。

createParticles(炎の数, パーティクル数, 炎の位置の半径, 炎の垂直座標, シーン)

炎をパーティクルのアニメーションとして加えたJavaScriptの記述は、後のコード003にまとめました。パーティクルをつくる関数(createParticles())は、以下の抜書きのように初期設定の関数(initialize())から呼出します(第15行目)。戻り値の炎のオブジェクトの配列は、変数(fireObjects)に納めました(第6行目)。

パーティクルをつくる関数(createParticles())は、そのもととなる小さな平面のひながたをPrimitivePlanePrefab()コンストラクタでつくります(第53行目)。渡した第3および第4引数は、x軸方向とyまたはz軸方向それぞれのセグメント数で、デフォルト値は1です。第5引数は、面の(法線ベクトルの)向きをy軸に合わせるかどうかのブール値で、falseはz軸に向けます。

パーティクルの平面は多くの数をまとめてつくりますので、PrefabBase.getNewObject()メソッドは用いません。そのとき求められる幾何学情報のGeometryオブジェクトを、PrimitivePrefabBase.geometryプロパティで得ます(第54行目)。また、素材オブジェクトには後でテクスチャを与えますので、TriangleMethodMaterial()コンストラクタに引数は渡しません(第55行目)。

パーティクルのGeometryオブジェクトは、その数(numParticles)だけ配列(geometrySet)に加えます(第56および第58〜60行目)。また、素材オブジェクトのブレンドモードをMaterialBase.blendModeプロパティで加算(定数BlendMode.ADD)にしました(第57行目)。炎のオブジェクトをつくる関数(getFireObjects())は別に定めて呼出し、でき上がったオブジェクトの配列(fireObjects)を受取って返します(第61〜62行目)。

  1. var fireObjects;
  1. var particleMaterial;
  1. function initialize() {
  1.   fireObjects = createParticles(3, 500, 300, 5, scene);
  1. }
  1. function createParticles(numFires, numParticles, radius, y, scene) {
  1.   var primitive = new away.prefabs.PrimitivePlanePrefab(10, 10, 1, 1, false);
  2.   var geometry = primitive.geometry;
  3.   var material = particleMaterial = new away.materials.TriangleMethodMaterial();
  4.   var geometrySet = [];
  5.   material.blendMode = away.base.BlendMode.ADD;
  6.   for (var i = 0; i < numParticles; i++) {
  7.     geometrySet[i] = geometry;
  8.   }
  9.   var fireObjects = getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene);
  10.   return fireObjects;
  11. }

炎のオブジェクトをつくって、配列に入れて返す関数(getFireObjects())はつぎのように定めます。第1引数にはGeometryオブジェクトを納めた配列、第3引数はパーティクルに用いる素材オブジェクトです。アニメーションの定め(第4引数)については、後で改めてご説明します。他の引数は、この関数を呼出したパーティクルをつくる関数(createParticles())が受取った値です。

getFireObjects(幾何情報の配列, 炎の数, 表面素材, アニメーションセット, 炎の位置の半径, 炎の垂直座標, シーン)

炎のオブジェクトの配列をつくる関数(getFireObjects())が受取ったGeometryオブジェクトの配列は、ParticleGeometryHelper.generateGeometry()メソッドに渡してParticleGeometryオブジェクトを得ます(第66行目)。ひとつひとつの炎のオブジェクトは別の関数(createAnimationParticle())でつくり、床の平面の中心から定められた半径(radius)の円周上に等間隔で並べたうえで、3次元空間のシーンに加えます(第67〜75行目)。

炎のオブジェクトをつくって返す関数(createAnimationParticle())は、Meshクラスのコンストラクタに幾何情報と素材オブジェクトを引数に渡して、まずMeshオブジェクトにします(第88行目)。そして、このMeshオブジェクト(mesh)と後述するアニメーションのオブジェクト(animator)を渡した炎のオブジェクトがつくられて、関数の引数に受取った配列(fireObjects)に納められます。戻り値はMeshオブジェクトです。なお、配列は参照が渡されますので、炎のオブジェクトは呼出し側の関数(getFireObjects())の変数(fireObjects)に加えられることになります(第65行目)。

  1. function getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene) {
  2.   var fireObjects = [];
  3.   var particleGeometry = away.tools.ParticleGeometryHelper.generateGeometry(geometrySet);
  4.   var anglePerFire = Math.PI * 2 / numFires;
  5.   for (var j = 0; j < numFires; j++) {
  6.     var mesh = createAnimationParticle(particleGeometry, material, animationSet, fireObjects);
  7.     var angle = j * anglePerFire;
  8.     mesh.x = radius * Math.sin(angle);
  9.     mesh.z = radius * Math.cos(angle);
  10.     mesh.y = y;
  11.     scene.addChild(mesh);
  12.   }
  13.   return fireObjects;
  14. }
  1. function createAnimationParticle(particleGeometry, material, animationSet, fireObjects) {
  2.   var mesh = new away.entities.Mesh(particleGeometry, material);
  1.   fireObjects.push(new FireObject(mesh, animator));
  2.   return mesh;
  3. }

アニメーションさせる炎のオブジェクトは、以下のクラス(FireObject)で定めることにしました(コード002)。引数は素材の(Mesh)オブジェクトとパーティクルのアニメーションを定めた(ParticleAnimator)オブジェクトです。クラスに加えたふたつのメソッド(startAnimation()とanimateLight())については、後でご説明します。なお、JavaScriptでクラスをどのように定義するかは「HTML5のCanvasでつくるダイナミックな表現―CreateJSを使う」第17回「簡単なクラスを定義する」をお読みください。

new FireObject(表面素材, アニメーションオブジェクト)
コード002■炎のオブジェクトを定めるクラス
  1. function FireObject(mesh, animator) {
  2.   this.strength = 0;
  3.   this.mesh = mesh;
  4.   this.animator = animator;
  5. }
  6. FireObject.prototype.startAnimation = function() {
  7.   this.animator.start();
  8. }
  9. FireObject.prototype.animateLight = function(fallOff, radius, addition) {
  10.   var light = this.light;
  11.   if (light) {
  12.     if (this.strength < 1) {
  13.       this.strength += 0.1;
  14.     }
  15.     light.fallOff = fallOff;
  16.     light.radius = radius;
  17.     light.diffuse = light.specular = this.strength + addition;
  18.   }
  19. };

07 パーティクルにアニメーションを定める

パーティクルのアニメーションは、アニメーションノードのオブジェクトで定めます。アニメーションノードをつくるクラスはParticleNodeBaseのサブクラスで、操作するプロパティによって使い分けます(表003)。それらのオブジェクトをまとめるのがParticleAnimationSetオブジェクトです。コンストラクタには、時間の定めを使うかどうかと、アニメーションをループさせるかどうかのふたつのブール値を渡します。

new away.animators.ParticleAnimationSet(時間設定, ループ)

ParticleAnimationSetオブジェクトには、ParticleAnimationSet.addAnimation()メソッドでアニメーションノードのオブジェクトを加えます。そして、ParticleAnimationSetオブジェクトからつくったParticleAnimatorオブジェクトを物体のMesh.animatorプロパティに与えればアニメーションが定まります。

ParticleAnimationSetオブジェクト.addAnimation(アニメーションノード)

new away.animators.ParticleAnimator(animationSetオブジェクト)

表003■パーティクルのアニメーションノードを定めるクラスのコンストラクタ
ParticleNodeBaseのサブクラスのコンストラクタ ノードの役割
ParticleBillboardNode() パーティクルの角度をつねにカメラに向ける。
ParticleScaleNode(モード, usesCycleの使用, usesPhaseの使用, 最小伸縮率, 最大伸縮率) パーティクルアニメーションが時間の中でどう伸縮するかを定める。
ParticleVelocityNode(モード, 速度ベクトル) パーティクルアニメーションが始まるときの速度を定める。
ParticleColorNode(モード, 乗数データの使用, オフセットデータの使用, usesCycleの使用, usesPhaseの使用, 初めの色, 終わりの色) パーティクルアニメーションの間に色がどう変わるかを定める。

アニメーションノードをつくるクラスのコンストラクタは、多くが第1引数にモードを受取ります。モードを示す定数はParticlePropertiesModeクラスに備わっています(表004)。そして、そのノードでつくったパーティクルアニメーションのプロパティへの働きがグローバルかローカルか、またローカルの場合に静的か動的かを決めます。

表004■ParticlePropertiesModeクラスのモードを示す定数
ParticlePropertiesModeクラスの定数 モード
GLOBAL グローバルなプロパティ
LOCAL_STATIC ローカルな静的プロパティ
LOCAL_DYNAMIC ローカルな動的プロパティ

パーティクルをつくる関数(createParticles())には、そのアニメーションを定める以下のJavaScriptコードを加えます。Away3Dのクラスのパス(完全修飾名)は長いので、何度か参照するクラスや定数は予めローカル変数にとりました(第44〜48行目)。ParticleColorNodeオブジェクトに与えるカラーは、ColorTransformオブジェクトで定めます(第49〜50行目)。コンストラクタに渡す引数は、初めの4つがRGBAの乗数(0〜1でデフォルト値1)、その後の4つがRGBAのオフセット値(±255の範囲でデフォルト値0)です。

アニメーションノードのオブジェクトは配列(animations)に納めます(第51行目)。そして、別に定める関数(getParticleAnimationSet())に渡して、アニメーションのParticleAnimationSetオブジェクト(animationSet)を得ています(第52および第78〜86行目)。そして、そのオブジェクトを炎のオブジェクト作成の関数(getFireObjects())からパーティクルアニメーション設定の関数(createAnimationParticle())に渡して(第61および第69行目)、Mesh.animatorプロパティに定めました(第89〜90行目)。

  1. function createParticles(numFires, numParticles, radius, y, scene) {
  2.   var ParticlePropertiesMode = away.animators.ParticlePropertiesMode;
  3.   var ParticleVelocityNode = away.animators.ParticleVelocityNode;
  4.   var ParticleColorNode = away.animators.ParticleColorNode;
  5.   var ColorTransform = away.geom.ColorTransform;
  6.   var GLOBAL = ParticlePropertiesMode.GLOBAL;
  7.   var startColor = new ColorTransform(0, 0, 0, 1, 0xFF, 0x33, 0x01);
  8.   var endColor = new ColorTransform(0, 0, 0, 1, 0x99);
  9.   var animations = [
        new away.animators.ParticleBillboardNode(),
        new away.animators.ParticleScaleNode(GLOBAL, false, false, 2.5, 0.5),
        new ParticleVelocityNode(GLOBAL, new away.geom.Vector3D(0, 80, 0)),
        new away.animators.ParticleColorNode(GLOBAL, true, true, false, false, startColor, endColor),
        new ParticleVelocityNode(ParticlePropertiesMode.LOCAL_STATIC)
      ];
  10.   var animationSet = getParticleAnimationSet(animations, initParticle);
  1.   var fireObjects = getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene);
  1. }
  2. function getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene) {
  1.   for (var j = 0; j < numFires; j++) {
  2.     var mesh = createAnimationParticle(particleGeometry, material, animationSet, fireObjects);
  1.   }
  1. }
  2. function getParticleAnimationSet(animations, initParticleFunc) {
  3.   var animationSet = new away.animators.ParticleAnimationSet(true, true);
  4.   var count = animations.length;
  5.   for (var i = 0; i < count; i++) {
  6.     animationSet.addAnimation(animations[i])
  7.   }
  1.   return animationSet;
  2. }
  3. function createAnimationParticle(particleGeometry, material, animationSet, fireObjects) {
  1.   var animator = new away.animators.ParticleAnimator(animationSet);
  2.   mesh.animator = animator;
  1. }

パーティクルのアニメーションノードには、ParticleVelocityNode()コンストラクタでローカルな静的プロパティのモード(定数ParticlePropertiesMode.LOCAL_STATIC)を加えました(第51行目)。このモードで各パーティクルが用いる静的プロパティとその初期値は、ParticleAnimationSet.initParticleFuncハンドラにコールバック関数として定めます。

コールバック関数は、以下のようにアニメーションのParticleAnimationSetオブジェクトをつくる関数(getParticleAnimationSet())が引数(initParticleFunc)として受取り、ParticleAnimationSet.initParticleFuncハンドラに与えました(第84行目)。

パーティクルを初期化するためにコールバック関数が呼出されるとき、引数(prop)にParticlePropertiesオブジェクトを受取ります(第94行目)。パーティクルごとに用いる静的プロパティとその値は、このオブジェクトのプロパティとして与えます。デフォルトでは、アニメーション開始時のstartTimeプロパティを定めます。また、アニメーションの長さであるdurationプロパティも加えられます。これらの値はランダムに求めました(第104〜105行目)。

そして、前述のとおりParticleVelocityNodeオブジェクトでローカルな静的プロパティのモードを定めたので、そこで用いる速度のプロパティParticleVelocityNode.VELOCITY_VECTOR3Dに長さ(radius)の決まったランダムな方向のVector3Dオブジェクト(velocityVector)を与えました(第95〜103および第106行目)[*2]

  1. function getParticleAnimationSet(animations, initParticleFunc) {
  1.   animationSet.initParticleFunc = initParticleFunc;
  1. }
  1. function initParticle(prop) {
  2.   var PIx2 = Math.PI * 2;
  3.   var degree1 = Math.random() * PIx2;
  4.   var degree2 = Math.random() * PIx2;
  5.   var radius = 15;
  6.   var sin_1 = Math.sin(degree1);
  7.   var cos_1 = Math.cos(degree1);
  8.   var sin_2 = Math.sin(degree2);
  9.   var cos_2 = Math.cos(degree2);
  10.   var velocityVector = new away.geom.Vector3D(radius * cos_1 * cos_2, radius * sin_1 * cos_2, radius * sin_2);
  11.   prop.startTime = Math.random() * 5;
  12.   prop.duration = Math.random() * 4 + 0.1;
  13.   prop[away.animators.ParticleVelocityNode.VELOCITY_VECTOR3D] = velocityVector;
  14. }

[*2] まず、xz平面でx軸の正方向となす角がθのxz座標は、つぎのように求められます。

xz平面のx座標 = 半径×cosθ
xz平面のz座標 = 半径×sinθ

つぎに、このxz座標をxy平面で、x軸の正方向となす角ωで回すと、xy座標はつぎの式で導けます。

xy平面のx座標 = xz平面のx座標×cosω
xy平面のx座標 = xz平面のx座標×sinω

したがって、3次元空間のxyz座標値はつぎの式で示されます。

x座標 = 半径×cosθ×cosω
y座標 = 半径×cosθ×sinω
z座標 = 半径×sinθ

08 パーティクルアニメーションを動かす

パーティクルをアニメーションとして動かすには、パーティクルのMeshオブジェクトのMesh.animatorプロパティに与えたParticleAnimatorオブジェクトに対してAnimatorBase.start()メソッドを呼出さなければなりません。この呼出しは、前掲コード002で炎のオブジェクトの定めたクラス(FireObject)に、以下のようにメソッドとして備えました。

クラスのコンストラクタ(FireObject)は、ParticleAnimatorオブジェクトを引数に受取って、プロパティとして定めます(第1および第4行目)。そして、アニメーションを開始するメソッド(startAnimation())で、そのプロパティのAnimatorBase.start()メソッドが呼出せます(第6〜8行目)。

  1. function FireObject(mesh, animator) {
  1.   this.animator = animator;
  2. }
  3. FireObject.prototype.startAnimation = function() {
  4.   this.animator.start();
  5. }

そして、後掲コード003の初期化の関数(initialize())には以下のようにTimerオブジェクト(fireTimer)を加えて、炎のパーティクルアニメーションをひとつずつ一定の間隔で動かしました(第19〜21行目)。リスナー関数(startFire())は、炎のオブジェクトが入った配列(fireObjects)からひとつずつ順に取出して、アニメーション開始のメソッド(startAnimation())を呼出しています(第109および第112行目)。

  1. var fireTimer;
  1. function initialize() {
  1.   fireTimer = new away.utils.Timer(1000, fireObjects.length);
  2.   fireTimer.addEventListener(away.events.TimerEvent.TIMER, startFire);
  3.   fireTimer.start();
  4. }
  1. function startFire(eventObject) {
  2.   var fireObject = fireObjects[fireTimer.currentCount - 1];
  1.   fireObject.startAnimation();
  1. }

これで、炎のパーティクルのオブジェクトが床の上に順に置かれ、舞い上がる炎のアニメーションが動き出します(図004)。もっとも、これだけでは床の存在感が薄いです。床の素材のTriangleMethodMaterialオブジェクトは、SinglePassMaterialBase.specularプロパティで鏡面反射も強めました。そこで、炎の映り込みをPointLightオブジェクトで加えましょう(「Away3D: 立方体を回してみる」03「DirectionalLightクラスで平行光源を定める」図003「3次元表現で使われる光源の種類」参照)。

図004■炎のパーティクルが順に床に置かれてアニメーションする
図004

後掲コード003のアニメーションを開始する関数(startFire())には、以下のようなライトの定めが加わっています。PointLight()コンストラクタでライトのオブジェクト(light)をつくって、色や拡散・鏡面反射、位置を決めたら、炎のオブジェクト(fireObject)のプロパティ(light)に与えます(第111および第113〜117行目)。なお、位置はPointLightもMeshもDisplayObjectのサブクラスですので、プロパティDisplayObject.transformTransform.positionから3次元空間座標を示すVector3Dオブジェクトとして得られます。

ライトのオブジェクトはStaticLightPickerオブジェクトにも加えなければなりません。StaticLightPicker.lightsプロパティからライトオブジェクトの配列が得られますので、新たなPointLightオブジェクトを加えたうえで、StaticLightPicker.lightsプロパティに戻します(第110および第118〜119行目)。

  1. function startFire(eventObject) {
  1.   var lights = lightPicker.lights;
  2.   var light = new away.entities.PointLight();
  1.   light.color = 0xFF3301;
  2.   light.diffuse = 0;
  3.   light.specular = 0;
  4.   light.transform.position = fireObject.mesh.transform.position;
  5.   fireObject.light = light;
  6.   lights.push(light);
  7.   lightPicker.lights = lights;
  8. }

さらに、ライトにも揺らぐアニメーションを加えます。そのために、前掲コード002の炎のオブジェクトを定めるクラス(FireObject)には、以下のようにライトのアニメーションがメソッド(animateLight())として備わっています。

プロパティPointLight.fallOffPointLight.radiusは、光の届く距離のそれぞれ最大値と最小値を示します。炎のオブジェクトにライトのプロパティ(light)が与えられたことを確かめたうえで、メソッドの引数値(fallOffとradius)をふたつのプロパティに与えます(第9および第11、第15〜16行目)。ライトのプロパティLightBase.diffuseLightBase.specularには、炎のオブジェクトのプロパティ値(strength)を毎回加算したうえで、さらに引数値(addition)を加えて定めます(第12〜14および第17行目)。

  1. FireObject.prototype.animateLight = function(fallOff, radius, addition) {
  2.   var light = this.light;
  3.   if (light) {
  4.     if (this.strength < 1) {
  5.       this.strength += 0.1;
  6.     }
  7.     light.fallOff = fallOff;
  8.     light.radius = radius;
  9.     light.diffuse = light.specular = this.strength + addition;
  10.   }
  11. };

上述した炎のオブジェクトのクラス(FireObject)に定めたライトをアニメーションさせるメソッド(animateLight())は、以下のように描画のためのリスナー関数(render())から呼出します。炎のオブジェクトが納められた配列(fireObjects)からインスタンスを順に取出し、ランダムな値を引数に渡しました(後述コード003第140〜147行目)。これで床の平面に映り込んだ炎がゆらゆらと揺らぎます(図005左)。

  1. function render(timeStamp) {
  2.   var count = fireObjects.length;
  3.   for (var i = 0; i < count; i++) {
  4.     var fireObject = fireObjects[i];
  5.     fireObject.animateLight(380 + Math.random() * 20, 200 + Math.random() * 30, Math.random() * 0.2);
  6.   }
  1. }
図005■床に映り込む炎がゆらゆらと揺らぐ
図005左 図005右

炎のパーティクルは小さなPrimitivePlanePrefabオブジェクトからつくりましたので、よく見ると矩形になっていて炎のイメージではありません(前掲図005右)。これは、素材オブジェクトにテクスチャを与えて改善します。ひとまず今までのJavaScriptをつぎのコード003にまとめましょう。なお、炎のオブジェクトを定めるクラス(FireObject)は、前掲コード002のままです。jsdo.itにもサンプル002としてコードを掲げました。

コード003■炎のパーティクルと床に映り込むライトが揺らぐアニメーション
  1. var view;
  2. var directionalLight;
  3. var mesh;
  4. var cameraController;
  5. var timer;
  6. var fireObjects;
  7. var fireTimer;
  8. var lightPicker;
  9. var particleMaterial;
  10. function initialize() {
  11.   view = createView(400, 300);
  12.   var scene = view.scene;
  13.   directionalLight = createDirectionalLight(0.5, 0xEEDDDD, 0.5, 0, 0x808090);
  14.   mesh = createPlane(800, 800, directionalLight, 10, -20, scene);
  15.   fireObjects = createParticles(3, 500, 300, 5, scene);
  16.   cameraController = setupCameraController(view.camera, 1000, 0, 90, 45, 20);
  17.   timer = new away.utils.RequestAnimationFrame(render);
  18.   timer.start();
  19.   fireTimer = new away.utils.Timer(1000, fireObjects.length);
  20.   fireTimer.addEventListener(away.events.TimerEvent.TIMER, startFire);
  21.   fireTimer.start();
  22. }
  23. function createView(width, height) {
  24.   var defaultRenderer = new away.render.DefaultRenderer();
  25.   var view = new away.containers.View(defaultRenderer);
  26.   view.width = width;
  27.   view.height = height;
  28.   return view;
  29. }
  30. function createPlane(width, height, light, specular, y, scene) {
  31.   var defaultTexture = away.materials.DefaultMaterialManager.getDefaultTexture();
  32.   var material = new away.materials.TriangleMethodMaterial(defaultTexture);
  33.   var mesh = new away.prefabs.PrimitivePlanePrefab(width, height).getNewObject();
  34.   mesh.material = material;
  35.   lightPicker = new away.materials.StaticLightPicker([light]);
  36.   material.lightPicker = lightPicker;
  37.   material.repeat = true;
  38.   material.specular = specular;
  39.   mesh.y = y;
  40.   scene.addChild(mesh);
  41.   return mesh;
  42. }
  43. function createParticles(numFires, numParticles, radius, y, scene) {
  44.   var ParticlePropertiesMode = away.animators.ParticlePropertiesMode;
  45.   var ParticleVelocityNode = away.animators.ParticleVelocityNode;
  46.   var ParticleColorNode = away.animators.ParticleColorNode;
  47.   var ColorTransform = away.geom.ColorTransform;
  48.   var GLOBAL = ParticlePropertiesMode.GLOBAL;
  49.   var startColor = new ColorTransform(0, 0, 0, 1, 0xFF, 0x33, 0x01);
  50.   var endColor = new ColorTransform(0, 0, 0, 1, 0x99);
  51.   var animations = [
        new away.animators.ParticleBillboardNode(),
        new away.animators.ParticleScaleNode(GLOBAL, false, false, 2.5, 0.5),
        new ParticleVelocityNode(GLOBAL, new away.geom.Vector3D(0, 80, 0)),
        new away.animators.ParticleColorNode(GLOBAL, true, true, false, false, startColor, endColor),
        new ParticleVelocityNode(ParticlePropertiesMode.LOCAL_STATIC)
      ];
  52.   var animationSet = getParticleAnimationSet(animations, initParticle);
  53.   var primitive = new away.prefabs.PrimitivePlanePrefab(10, 10, 1, 1, false);
  54.   var geometry = primitive.geometry;
  55.   var material = particleMaterial = new away.materials.TriangleMethodMaterial();
  56.   var geometrySet = [];
  57.   material.blendMode = away.base.BlendMode.ADD;
  58.   for (var i = 0; i < numParticles; i++) {
  59.     geometrySet[i] = geometry;
  60.   }
  61.   var fireObjects = getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene);
  62.   return fireObjects;
  63. }
  64. function getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene) {
  65.   var fireObjects = [];
  66.   var particleGeometry = away.tools.ParticleGeometryHelper.generateGeometry(geometrySet);
  67.   var anglePerFire = Math.PI * 2 / numFires;
  68.   for (var j = 0; j < numFires; j++) {
  69.     var mesh = createAnimationParticle(particleGeometry, material, animationSet, fireObjects);
  70.     var angle = j * anglePerFire;
  71.     mesh.x = radius * Math.sin(angle);
  72.     mesh.z = radius * Math.cos(angle);
  73.     mesh.y = y;
  74.     scene.addChild(mesh);
  75.   }
  76.   return fireObjects;
  77. }
  78. function getParticleAnimationSet(animations, initParticleFunc) {
  79.   var animationSet = new away.animators.ParticleAnimationSet(true, true);
  80.   var count = animations.length;
  81.   for (var i = 0; i < count; i++) {
  82.     animationSet.addAnimation(animations[i]);
  83.   }
  84.   animationSet.initParticleFunc = initParticleFunc;
  85.   return animationSet;
  86. }
  87. function createAnimationParticle(particleGeometry, material, animationSet, fireObjects) {
  88.   var mesh = new away.entities.Mesh(particleGeometry, material);
  89.   var animator = new away.animators.ParticleAnimator(animationSet);
  90.   mesh.animator = animator;
  91.   fireObjects.push(new FireObject(mesh, animator));
  92.   return mesh;
  93. }
  94. function initParticle(prop) {
  95.   var PIx2 = Math.PI * 2;
  96.   var degree1 = Math.random() * PIx2;
  97.   var degree2 = Math.random() * PIx2;
  98.   var radius = 15;
  99.   var sin_1 = Math.sin(degree1);
  100.   var cos_1 = Math.cos(degree1);
  101.   var sin_2 = Math.sin(degree2);
  102.   var cos_2 = Math.cos(degree2);
  103.   var velocityVector = new away.geom.Vector3D(radius * cos_1 * cos_2, radius * sin_1 * cos_2, radius * sin_2);
  104.   prop.startTime = Math.random() * 5;
  105.   prop.duration = Math.random() * 4 + 0.1;
  106.   prop[away.animators.ParticleVelocityNode.VELOCITY_VECTOR3D] = velocityVector;
  107. }
  108. function startFire(eventObject) {
  109.   var fireObject = fireObjects[fireTimer.currentCount - 1];
  110.   var lights = lightPicker.lights;
  111.   var light = new away.entities.PointLight();
  112.   fireObject.startAnimation();
  113.   light.color = 0xFF3301;
  114.   light.diffuse = 0;
  115.   light.specular = 0;
  116.   light.transform.position = fireObject.mesh.transform.position;
  117.   fireObject.light = light;
  118.   lights.push(light);
  119.   lightPicker.lights = lights;
  120. }
  121. function createDirectionalLight(ambient, color, diffuse, specular, ambientColor) {
  122.   var light = new away.entities.DirectionalLight();
  123.   light.direction = new away.geom.Vector3D(0, -1, 0);
  124.   light.ambient = ambient;
  125.   light.color = color;
  126.   light.diffuse = diffuse;
  127.   light.specular = specular;
  128.   light.ambientColor = ambientColor;
  129.   return light;
  130. }
  131. function setupCameraController(camera, distance, minTiltAngle, maxTiltAngle, panAngle, tiltAngle) {
  132.   var cameraController = new away.controllers.HoverController(camera);
  133.   cameraController.distance = distance;
  134.   cameraController.minTiltAngle = minTiltAngle;
  135.   cameraController.maxTiltAngle = maxTiltAngle;
  136.   cameraController.panAngle = panAngle;
  137.   cameraController.tiltAngle = tiltAngle;
  138.   return cameraController;
  139. }
  140. function render(timeStamp) {
  141.   var count = fireObjects.length;
  142.   for (var i = 0; i < count; i++) {
  143.     var fireObject = fireObjects[i];
  144.     fireObject.animateLight(380 + Math.random() * 20, 200 + Math.random() * 30, Math.random() * 0.2);
  145.   }
  146.   view.render();
  147. }

サンプル002■Away3D 14/08/26: Animating particle fires and their reflection


09 パーティクルの素材にテクスチャを与える

パーティクルの素材に与えるテクスチャには、256×256ピクセルのアルファチャネルつきPNGファイルを用いました(図006)。テクスチャにするイメージは幅と高さをいずれも2の累乗に定めます。本稿が参考にしたサンプルコード「Animating particles simulating fire」の炎に使われている画像です。「Away3D TypeScript」サイトからダウンロードできるサンプルファイル「stagegl-examples-ts」の中の「blue.png」です。HTMLドキュメントと同じ階層の「assets」というフォルダに納めます。

図006■パーティクルの素材に与えるPNG画像
図006

画像ファイルは、静的なAssetLibrary.load()メソッドで読込みます。引数はファイルのURLを与えたURLRequestオブジェクトです。そして、読込み終えたときの扱いは、これも静的メソッドAssetLibrary.addEventListener()によりAssetLibraryBundle.resourceCompleteイベント(定数LoaderEvent.RESOURCE_COMPLETE)のリスナーに定めます。

away.library.AssetLibrary.addEventListener(away.events.LoaderEvent.RESOURCE_COMPLETE, リスナー)
away.library.AssetLibrary.load(new away.net.URLRequest(url))

テクスチャの読込みと素材オブジェクトへの設定を加えたJavaScriptは、後にコード004としてまとめました。以下はその抜書きです。画像ファイルを読込む関数(loadAsset())は別に定め、初期化の関数から呼出します(第17行目)。引数はファイルのURLです。

ファイルを読込む関数(loadAsset())は、まずAssetLibraryクラスの参照を扱いやすいようにローカル変数にとりました(第148行目)。そのうえで、別に定める関数(onResourceComplete())をAssetLibraryBundle.resourceCompleteイベントのリスナーに加え、引数(url)に受取ったURLのファイルをAssetLibrary.load()メソッドにより読込みます(第149〜150行目)。

AssetLibraryBundle.resourceCompleteイベントのリスナー関数(onResourceComplete())が引数に受取るLoaderEventオブジェクトは、LoaderEvent.assetsというプロパティに、素材のオブジェクトが納められた配列をもちます(第153行目)。その配列(assets)が空でないことを確かめたうえで[*3]、今回は素材はひとつしか読込んでいませんので、初めのエレメントをTriangleMethodMaterial.textureプロパティにテクスチャとして与えます(第154〜155行目)。

  1. function initialize() {
  1.   loadAsset("assets/blue.png");
  1. }
  1. function loadAsset(url) {
  2.   var AssetLibrary = away.library.AssetLibrary;
  3.   AssetLibrary.addEventListener(away.events.LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  4.   AssetLibrary.load(new away.net.URLRequest(url));
  5. }
  6. function onResourceComplete(eventObject) {
  7.   var assets = eventObject.assets;
  8.   if (assets.length > 0) {
  9.     particleMaterial.texture = assets[0];
  10.   }
  11. }

これで、アニメーションする炎のパーティクルに、アルファつき画像のテクスチャが与えられました(図007)。パーティクルが単なる矩形でなく、輪郭のぼけた丸いかたちに変わります。以下のコード004に、スクリプトをまとめました(ただし、クラス定義はコード002のままなので省いています)。

図007■アニメーションする炎のパーティクルにテクスチャが与えられた
図007

コード004■テクスチャを与えた炎のパーティクルが揺らぐアニメーション
  1. var view;
  2. var directionalLight;
  3. var mesh;
  4. var cameraController;
  5. var timer;
  6. var fireObjects;
  7. var fireTimer;
  8. var lightPicker;
  9. var particleMaterial;
  10. function initialize() {
  11.   view = createView(400, 300);
  12.   var scene = view.scene;
  13.   directionalLight = createDirectionalLight(0.5, 0xEEDDDD, 0.5, 0, 0x808090);
  14.   mesh = createPlane(800, 800, directionalLight, 10, -20, scene);
  15.   fireObjects = createParticles(3, 500, 300, 5, scene);
  16.   cameraController = setupCameraController(view.camera, 1000, 0, 90, 45, 20);
  17.   loadAsset("assets/blue.png");
  18.   timer = new away.utils.RequestAnimationFrame(render);
  19.   timer.start();
  20.   fireTimer = new away.utils.Timer(1000, fireObjects.length);
  21.   fireTimer.addEventListener(away.events.TimerEvent.TIMER, startFire);
  22.   fireTimer.start();
  23. }
  24. function createView(width, height) {
  25.   var defaultRenderer = new away.render.DefaultRenderer();
  26.   var view = new away.containers.View(defaultRenderer);
  27.   view.width = width;
  28.   view.height = height;
  29.   return view;
  30. }
  31. function createPlane(width, height, light, specular, y, scene) {
  32.   var defaultTexture = away.materials.DefaultMaterialManager.getDefaultTexture();
  33.   var material = new away.materials.TriangleMethodMaterial(defaultTexture);
  34.   var mesh = new away.prefabs.PrimitivePlanePrefab(width, height).getNewObject();
  35.   mesh.material = material;
  36.   lightPicker = new away.materials.StaticLightPicker([light]);
  37.   material.lightPicker = lightPicker;
  38.   material.repeat = true;
  39.   material.specular = specular;
  40.   mesh.y = y;
  41.   scene.addChild(mesh);
  42.   return mesh;
  43. }
  44. function createParticles(numFires, numParticles, radius, y, scene) {
  45.   var ParticlePropertiesMode = away.animators.ParticlePropertiesMode;
  46.   var ParticleVelocityNode = away.animators.ParticleVelocityNode;
  47.   var ParticleColorNode = away.animators.ParticleColorNode;
  48.   var ColorTransform = away.geom.ColorTransform;
  49.   var GLOBAL = ParticlePropertiesMode.GLOBAL;
  50.   var startColor = new ColorTransform(0, 0, 0, 1, 0xFF, 0x33, 0x01);
  51.   var endColor = new ColorTransform(0, 0, 0, 1, 0x99);
  52.   var animations = [
  53.     new away.animators.ParticleBillboardNode(),
  54.     new away.animators.ParticleScaleNode(GLOBAL, false, false, 2.5, 0.5),
  55.     new ParticleVelocityNode(GLOBAL, new away.geom.Vector3D(0, 80, 0)),
  56.     new away.animators.ParticleColorNode(GLOBAL, true, true, false, false, startColor, endColor),
  57.     new ParticleVelocityNode(ParticlePropertiesMode.LOCAL_STATIC)
  58.   ];
  59.   var animationSet = getParticleAnimationSet(animations, initParticle);
  60.   var primitive = new away.prefabs.PrimitivePlanePrefab(10, 10, 1, 1, false);
  61.   var geometry = primitive.geometry;
  62.   var material = particleMaterial = new away.materials.TriangleMethodMaterial();
  63.   var geometrySet = [];
  64.   material.blendMode = away.base.BlendMode.ADD;
  65.   for (var i = 0; i < numParticles; i++) {
  66.     geometrySet[i] = geometry;
  67.   }
  68.   var fireObjects = getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene);
  69.   return fireObjects;
  70. }
  71. function getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene) {
  72.   var fireObjects = [];
  73.   var particleGeometry = away.tools.ParticleGeometryHelper.generateGeometry(geometrySet);
  74.   var anglePerFire = Math.PI * 2 / numFires;
  75.   for (var j = 0; j < numFires; j++) {
  76.     var mesh = createAnimationParticle(particleGeometry, material, animationSet, fireObjects);
  77.     var angle = j * anglePerFire;
  78.     mesh.x = radius * Math.sin(angle);
  79.     mesh.z = radius * Math.cos(angle);
  80.     mesh.y = y;
  81.     scene.addChild(mesh);
  82.   }
  83.   return fireObjects;
  84. }
  85. function getParticleAnimationSet(animations, initParticleFunc) {
  86.   var animationSet = new away.animators.ParticleAnimationSet(true, true);
  87.   var count = animations.length;
  88.   for (var i = 0; i < count; i++) {
  89.     animationSet.addAnimation(animations[i]);
  90.   }
  91.   animationSet.initParticleFunc = initParticleFunc;
  92.   return animationSet;
  93. }
  94. function createAnimationParticle(particleGeometry, material, animationSet, fireObjects) {
  95.   var mesh = new away.entities.Mesh(particleGeometry, material);
  96.   var animator = new away.animators.ParticleAnimator(animationSet);
  97.   mesh.animator = animator;
  98.   fireObjects.push(new FireObject(mesh, animator));
  99.   return mesh;
  100. }
  101. function initParticle(prop) {
  102.   var PIx2 = Math.PI * 2;
  103.   var degree1 = Math.random() * PIx2;
  104.   var degree2 = Math.random() * PIx2;
  105.   var radius = 15;
  106.   var sin_1 = Math.sin(degree1);
  107.   var cos_1 = Math.cos(degree1);
  108.   var sin_2 = Math.sin(degree2);
  109.   var cos_2 = Math.cos(degree2);
  110.   var velocityVector = new away.geom.Vector3D(radius * cos_1 * cos_2, radius * sin_1 * cos_2, radius * sin_2);
  111.   prop.startTime = Math.random() * 5;
  112.   prop.duration = Math.random() * 4 + 0.1;
  113.   prop[away.animators.ParticleVelocityNode.VELOCITY_VECTOR3D] = velocityVector;
  114. }
  115. function startFire(eventObject) {
  116.   var fireObject = fireObjects[fireTimer.currentCount - 1];
  117.   var lights = lightPicker.lights;
  118.   var light = new away.entities.PointLight();
  119.   fireObject.startAnimation();
  120.   light.color = 0xFF3301;
  121.   light.diffuse = 0;
  122.   light.specular = 0;
  123.   light.transform.position = fireObject.mesh.transform.position;
  124.   fireObject.light = light;
  125.   lights.push(light);
  126.   lightPicker.lights = lights;
  127. }
  128. function createDirectionalLight(ambient, color, diffuse, specular, ambientColor) {
  129.   var light = new away.entities.DirectionalLight();
  130.   light.direction = new away.geom.Vector3D(0, -1, 0);
  131.   light.ambient = ambient;
  132.   light.color = color;
  133.   light.diffuse = diffuse;
  134.   light.specular = specular;
  135.   light.ambientColor = ambientColor;
  136.   return light;
  137. }
  138. function setupCameraController(camera, distance, minTiltAngle, maxTiltAngle, panAngle, tiltAngle) {
  139.   var cameraController = new away.controllers.HoverController(camera);
  140.   cameraController.distance = distance;
  141.   cameraController.minTiltAngle = minTiltAngle;
  142.   cameraController.maxTiltAngle = maxTiltAngle;
  143.   cameraController.panAngle = panAngle;
  144.   cameraController.tiltAngle = tiltAngle;
  145.   return cameraController;
  146. }
  147. function loadAsset(url) {
  148.   var AssetLibrary = away.library.AssetLibrary;
  149.   AssetLibrary.addEventListener(away.events.LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  150.   AssetLibrary.load(new away.net.URLRequest(url));
  151. }
  152. function onResourceComplete(eventObject) {
  153.   var assets = eventObject.assets;
  154.   if (assets.length > 0) {
  155.     particleMaterial.texture = assets[0];
  156.   }
  157. }
  158. function render(timeStamp) {
  159.   var count = fireObjects.length;
  160.   for (var i = 0; i < count; i++) {
  161.     var fireObject = fireObjects[i];
  162.     fireObject.animateLight(380 + Math.random() * 20, 200 + Math.random() * 30, Math.random() * 0.2);
  163.   }
  164.   view.render();
  165. }

サンプル003■Away3D 14/08/26: Animating particle fires with a texture


[*3] AssetLibraryBundle.resourceCompleteは、素材データがすべて読込まれたときに生じるイベントです。したがって、リスナー関数が受取ったオブジェクトのLoaderEvent.assetsプロパティで得た配列に素材のエレメントがないのはおかしいでしょう。けれど実際には、配列がエレメントを含まないことがあり、そのままではもうAssetLibraryBundle.resourceCompleteイベントは起こりません。

そこで、この問題を解決するため、前掲サンプル003のコードのリスナー関数(onResourceComplete())では、つぎのように配列が空のとき改めて素材ファイル読込みの関数(loadAsset())を呼出しています。なお、LoaderEvent.urlプロパティで読込みを試みたファイルのurlが得られます。

function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  if (assets.length > 0) {
    particleMaterial.texture = assets[0];
  } else {   // 配列が空のときは再読込み
    loadAsset(eventObject.url);
  }
}


10 3次元空間のカメラをマウス操作に合わせてにパンおよびチルトさせる

サンプルの仕上げとして、インタラクティブなカメラワークを加えます。マウスのドラッグにより、3次元空間のカメラをパンおよびチルトします。でき上がりのJavaScriptは、後にコード005としてまとめました(クラス定義はコード002のままなので省きます)。書き加えたのはつぎに抜書きしたコードで、これは前出「Away3D: 立方体を回してみる」05「映し出される立方体の角度をカメラの動きで変化させる」と同じです。したがって、中身についてはリンクした解説をお読みください

  1. var lastMouseX;
  2. var lastMouseY;
  3. var lastPanAngle;
  4. var lastTiltAngle;
  5. function initialize() {
  1.   document.onmousedown = startDrag;
    }
  1. function startDrag(eventObject) {
  2.   lastMouseX = eventObject.clientX;
  3.   lastMouseY = eventObject.clientY;
  4.   lastPanAngle = cameraController.panAngle;
  5.   lastTiltAngle = cameraController.tiltAngle;
  6.   document.onmousemove = drag;
  7.   document.onmouseup = stopDrag;
  8. }
  9. function drag(eventObject) {
  10.   cameraController.panAngle = 0.5 * (eventObject.clientX - lastMouseX) + lastPanAngle;
  11.   cameraController.tiltAngle = 0.3 * (eventObject.clientY - lastMouseY) + lastTiltAngle;
  12. }
  13. function stopDrag(eventObject) {
  14.   document.onmousemove = null;
  15.   document.onmouseup = null;
  16. }

これで、マウスをドラッグすることにより、3次元空間を映すカメラがパンあるいはチルトできるようになりました(図008)。以下のコード005は、サンプル004としてjsdo.itにも掲げてあります。

図008■マウスドラッグにより3次元空間を映すカメラがパンやチルトできる
図008

コード005■3次元空間のカメラをマウス操作に合わせてパンおよびチルトさせる
  1. var view;
  2. var directionalLight;
  3. var mesh;
  4. var cameraController;
  5. var timer;
  6. var fireObjects;
  7. var fireTimer;
  8. var lightPicker;
  9. var particleMaterial;
  10. var lastMouseX;
  11. var lastMouseY;
  12. var lastPanAngle;
  13. var lastTiltAngle;
  14. function initialize() {
  15.   view = createView(400, 300);
  16.   var scene = view.scene;
  17.   directionalLight = createDirectionalLight(0.5, 0xEEDDDD, 0.5, 0, 0x808090);
  18.   mesh = createPlane(800, 800, directionalLight, 10, -20, scene);
  19.   fireObjects = createParticles(3, 500, 300, 5, scene);
  20.   cameraController = setupCameraController(view.camera, 1000, 0, 90, 45, 20);
  21.   loadAsset("assets/blue.png");
  22.   document.onmousedown = startDrag;
  23.   timer = new away.utils.RequestAnimationFrame(render);
  24.   timer.start();
  25.   fireTimer = new away.utils.Timer(1000, fireObjects.length);
  26.   fireTimer.addEventListener(away.events.TimerEvent.TIMER, startFire);
  27.   fireTimer.start();
  28. }
  29. function createView(width, height) {
  30.   var defaultRenderer = new away.render.DefaultRenderer();
  31.   var view = new away.containers.View(defaultRenderer);
  32.   view.width = width;
  33.   view.height = height;
  34.   return view;
  35. }
  36. function createPlane(width, height, light, specular, y, scene) {
  37.   var defaultTexture = away.materials.DefaultMaterialManager.getDefaultTexture();
  38.   var material = new away.materials.TriangleMethodMaterial(defaultTexture);
  39.   var mesh = new away.prefabs.PrimitivePlanePrefab(width, height).getNewObject();
  40.   mesh.material = material;
  41.   lightPicker = new away.materials.StaticLightPicker([light]);
  42.   material.lightPicker = lightPicker;
  43.   material.repeat = true;
  44.   material.specular = specular;
  45.   mesh.y = y;
  46.   scene.addChild(mesh);
  47.   return mesh;
  48. }
  49. function createParticles(numFires, numParticles, radius, y, scene) {
  50.   var ParticlePropertiesMode = away.animators.ParticlePropertiesMode;
  51.   var ParticleVelocityNode = away.animators.ParticleVelocityNode;
  52.   var ParticleColorNode = away.animators.ParticleColorNode;
  53.   var ColorTransform = away.geom.ColorTransform;
  54.   var GLOBAL = ParticlePropertiesMode.GLOBAL;
  55.   var startColor = new ColorTransform(0, 0, 0, 1, 0xFF, 0x33, 0x01);
  56.   var endColor = new ColorTransform(0, 0, 0, 1, 0x99);
  57.   var animations = [
  58.     new away.animators.ParticleBillboardNode(),
  59.     new away.animators.ParticleScaleNode(GLOBAL, false, false, 2.5, 0.5),
  60.     new ParticleVelocityNode(GLOBAL, new away.geom.Vector3D(0, 80, 0)),
  61.     new away.animators.ParticleColorNode(GLOBAL, true, true, false, false, startColor, endColor),
  62.     new ParticleVelocityNode(ParticlePropertiesMode.LOCAL_STATIC)
  63.   ];
  64.   var animationSet = getParticleAnimationSet(animations, initParticle);
  65.   var primitive = new away.prefabs.PrimitivePlanePrefab(10, 10, 1, 1, false);
  66.   var geometry = primitive.geometry;
  67.   var material = particleMaterial = new away.materials.TriangleMethodMaterial();
  68.   var geometrySet = [];
  69.   material.blendMode = away.base.BlendMode.ADD;
  70.   for (var i = 0; i < numParticles; i++) {
  71.     geometrySet[i] = geometry;
  72.   }
  73.   var fireObjects = getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene);
  74.   return fireObjects;
  75. }
  76. function getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene) {
  77.   var fireObjects = [];
  78.   var particleGeometry = away.tools.ParticleGeometryHelper.generateGeometry(geometrySet);
  79.   var anglePerFire = Math.PI * 2 / numFires;
  80.   for (var j = 0; j < numFires; j++) {
  81.     var mesh = createAnimationParticle(particleGeometry, material, animationSet, fireObjects);
  82.     var angle = j * anglePerFire;
  83.     mesh.x = radius * Math.sin(angle);
  84.     mesh.z = radius * Math.cos(angle);
  85.     mesh.y = y;
  86.     scene.addChild(mesh);
  87.   }
  88.   return fireObjects;
  89. }
  90. function getParticleAnimationSet(animations, initParticleFunc) {
  91.   var animationSet = new away.animators.ParticleAnimationSet(true, true);
  92.   var count = animations.length;
  93.   for (var i = 0; i < count; i++) {
  94.     animationSet.addAnimation(animations[i]);
  95.   }
  96.   animationSet.initParticleFunc = initParticleFunc;
  97.   return animationSet;
  98. }
  99. function createAnimationParticle(particleGeometry, material, animationSet, fireObjects) {
  100.   var mesh = new away.entities.Mesh(particleGeometry, material);
  101.   var animator = new away.animators.ParticleAnimator(animationSet);
  102.   mesh.animator = animator;
  103.   fireObjects.push(new FireObject(mesh, animator));
  104.   return mesh;
  105. }
  106. function initParticle(prop) {
  107.   var PIx2 = Math.PI * 2;
  108.   var degree1 = Math.random() * PIx2;
  109.   var degree2 = Math.random() * PIx2;
  110.   var radius = 15;
  111.   var sin_1 = Math.sin(degree1);
  112.   var cos_1 = Math.cos(degree1);
  113.   var sin_2 = Math.sin(degree2);
  114.   var cos_2 = Math.cos(degree2);
  115.   var velocityVector = new away.geom.Vector3D(radius * cos_1 * cos_2, radius * sin_1 * cos_2, radius * sin_2);
  116.   prop.startTime = Math.random() * 5;
  117.   prop.duration = Math.random() * 4 + 0.1;
  118.   prop[away.animators.ParticleVelocityNode.VELOCITY_VECTOR3D] = velocityVector;
  119. }
  120. function startFire(eventObject) {
  121.   var fireObject = fireObjects[fireTimer.currentCount - 1];
  122.   var lights = lightPicker.lights;
  123.   var light = new away.entities.PointLight();
  124.   fireObject.startAnimation();
  125.   light.color = 0xFF3301;
  126.   light.diffuse = 0;
  127.   light.specular = 0;
  128.   light.transform.position = fireObject.mesh.transform.position;
  129.   fireObject.light = light;
  130.   lights.push(light);
  131.   lightPicker.lights = lights;
  132. }
  133. function createDirectionalLight(ambient, color, diffuse, specular, ambientColor) {
  134.   var light = new away.entities.DirectionalLight();
  135.   light.direction = new away.geom.Vector3D(0, -1, 0);
  136.   light.ambient = ambient;
  137.   light.color = color;
  138.   light.diffuse = diffuse;
  139.   light.specular = specular;
  140.   light.ambientColor = ambientColor;
  141.   return light;
  142. }
  143. function setupCameraController(camera, distance, minTiltAngle, maxTiltAngle, panAngle, tiltAngle) {
  144.   var cameraController = new away.controllers.HoverController(camera);
  145.   cameraController.distance = distance;
  146.   cameraController.minTiltAngle = minTiltAngle;
  147.   cameraController.maxTiltAngle = maxTiltAngle;
  148.   cameraController.panAngle = panAngle;
  149.   cameraController.tiltAngle = tiltAngle;
  150.   return cameraController;
  151. }
  152. function loadAsset(url) {
  153.   var AssetLibrary = away.library.AssetLibrary;
  154.   AssetLibrary.addEventListener(away.events.LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  155.   AssetLibrary.load(new away.net.URLRequest(url));
  156. }
  157. function onResourceComplete(eventObject) {
  158.   var assets = eventObject.assets;
  159.   if (assets.length > 0) {
  160.     particleMaterial.texture = assets[0];
  161.   }
  162. }
  163. function render(timeStamp) {
  164.   var count = fireObjects.length;
  165.   for (var i = 0; i < count; i++) {
  166.     var fireObject = fireObjects[i];
  167.     fireObject.animateLight(380 + Math.random() * 20, 200 + Math.random() * 30, Math.random() * 0.2);
  168.   }
  169.   view.render();
  170. }
  171. function startDrag(eventObject) {
  172.   lastMouseX = eventObject.clientX;
  173.   lastMouseY = eventObject.clientY;
  174.   lastPanAngle = cameraController.panAngle;
  175.   lastTiltAngle = cameraController.tiltAngle;
  176.   document.onmousemove = drag;
  177.   document.onmouseup = stopDrag;
  178. }
  179. function drag(eventObject) {
  180.   cameraController.panAngle = 0.5 * (eventObject.clientX - lastMouseX) + lastPanAngle;
  181.   cameraController.tiltAngle = 0.3 * (eventObject.clientY - lastMouseY) + lastTiltAngle;
  182. }
  183. function stopDrag(eventObject) {
  184.   document.onmousemove = null;
  185.   document.onmouseup = null;
  186. }

サンプル004■Away3D 14/08/26: Animating particle fires shot by the interactive camera

この後さらに、参考にしたAway3D TypeScriptサイトのサンプル「Animating particles simulating fire」と同じように、床のテクスチャに質感を加え、炎の数を増やすやり方については稿を改めて解説します(図009)。「Away3D: テクスチャの凹凸と反射 ー 法線マップとスペキュラマップ」をお読みください。

図009■床のテクスチャに質感を加えて炎の数も増やす
図009



作成者: 野中文雄
更新日: 2014年9月10日 2014年8月26日付ビルドの更新にもとづき、細かい表記と画像の一部を変更。
更新日: 2014年8月21日 2014年8月18日付ビルドの更新にもとづき、ふたつのJavaScriptライブラリ名を変更し、クラスTriangleMaterialをTriangleMethodMaterialに差替えた(注[*1])。
更新日: 2014年6月29日 2014年6月24日付ビルドの更新にもとづき、クラスPointLightとDirectionalLightのパッケージをaway.lightsからaway.entitiesに書替えた。
更新日: 2014年6月17日 2014年6月13日付ビルドの更新にもとづき、クラスTextureMaterialをTriangleMaterialに差替えた。
更新日: 2014年5月22日 5月13日付のビルド更新にもとづき、動作を確かめたうえで日付の記載を変更。
作成日: 2014年5月21日 5月18日付の暫定版から完成。


Copyright © 2001-2014 Fumio Nonaka.  All rights reserved.