Away3D TypeScriptではじめる3次元表現

第13回パーティクルを炎のように表現する

前回の第12回パーティクルのアニメーションを加えるでは、のろしが立ち上るような表現まではできた(再掲第12回図2⁠。今回は、パーティクルの動きや見た目を炎らしく整えよう。ランダムな動きやアニメーションノードを加え、テクスチャも与えたい。

第12回 図2 のろしのように立ち上るパーティクル(再掲)
第12回 図2 のろしのように立ち上るパーティクル(再掲)

パーティクルの動きにランダムな広がりを与える

始めに、⁠アニメーションさせる炎を定めたクラス」は、第12回コード1(再掲)のまま今回は手を加えない。第12回コード2「パーティクルを定めてアニメーションさせる」のスクリプトを、これから少しずつ書き替えてゆくことになる。

第12回 コード1 アニメーションさせる炎を定めたクラス(再掲)
function FireObject(mesh, animator) {
  this.strength = 0;
  this.mesh = mesh;
  this.animator = animator;
}
FireObject.prototype.startAnimation = function() {
  this.animator.start();
};

前回のコードのアニメーションがのろしのようにしか見えないのは、パーティクルの動きに広がりがないからだ。そこで、パーティクルの進む向きをランダムにしよう。パーティクルアニメーションの動きを定めるParticleVelocityNodeオブジェクトには、速度の3次元ベクトルをVector3Dオブジェクトで与える。速度は大きさと向きから導きたい。3次元空間の向きはふたつの角度により決まるので、つぎのような関数(getVector3D())に3つの引数を与え、Vector3Dオブジェクトを得ることにする。

getVector3D(距離, xz平面の角度, xy平面の角度)

2つの角度のうちのひとつは、原点(O)と求める座標(P)を結ぶ線分(OP)がxy平面となす角(θ)とする図1⁠。もうひとつは、線分のxy平面への投射(OQ)がxz平面(x軸)となす角(ω)である。このとき、もとの線分の長さ(r)にもとづいて、xyz座標(P)はつぎのように導かれる。なお、興味ある読者のために、数学における座標の求め方は本稿の結びで補う。

P(r cosθcosω, r cosθsinω, r sinθ)
図1 3次元座標を原点からの距離と2平面との角度から求める
図1 3次元座標を原点からの距離と2平面との角度から求める

距離と2つの角度からVector3Dオブジェクトを返す関数(getVector3D())はつぎのように定めた。パーティクルを初期化する関数(initParticle())から2つの角度をランダムに決めて呼び出し、得られたVector3DオブジェクトはParticleProperties.VelocityVector3Dプロパティ(定数ParticleVelocityNode.VELOCITY_VECTOR3Dは文字列"VelocityVector3D")に定めて、ひとつひとつのローカルなパーティクルの速度を変えた。

function initParticle(prop) {
  var PIx2 = Math.PI * 2;
  var radian0 = Math.random() * PIx2;
  var radian1 = Math.random() * PIx2;
  var velocityVector = getVector3D(15, radian0, radian1);

  prop[ParticleVelocityNode.VELOCITY_VECTOR3D] = velocityVector;
}
function getVector3D(radius, angleXY, angleXZ) {
  var sinXY = Math.sin(angleXY);
  var cosXY = Math.cos(angleXY);
  var sinXZ = Math.sin(angleXZ);
  var cosXZ = Math.cos(angleXZ);
  var vector = new Vector3D(radius * cosXY * cosXZ, radius * cosXY * sinXZ, radius * sinXY);
  return vector;
}

パーティクルにローカルなプロパティを定めるには、ParticleVelocityNodeオブジェクトをローカルなモードで加えなければならない。その場合、ParticleVelocityNode()コンストラクタの引数には、つぎのように定数ParticlePropertiesMode.LOCAL_STATICを与える表1参照⁠⁠。

function createParticles(numFires, numParticles, radius, y, scene) {
  var GLOBAL = ParticlePropertiesMode.GLOBAL;
  var animations = [
    new ParticleBillboardNode(),
    new ParticleVelocityNode(GLOBAL, new Vector3D(0, 80, 0)),
    new ParticleVelocityNode(ParticlePropertiesMode.LOCAL_STATIC)
  ];
}
表1 ParticlePropertiesModeクラスのモードを示す定数
ParticlePropertiesModeクラスの定数モード
GLOBALグローバルなプロパティ
LOCAL_STATICローカルな静的プロパティ
LOCAL_DYNAMICローカルな動的プロパティ

これで、パーティクルはそろって立ち上るだけでなく、それぞれにばらばらな向きへの動きが加わった。したがって、上るにつれて広がってゆく図2⁠。

図2 パーティクルに広がる動きが加わった
図2 パーティクルに広がる動きが加わった

さらに、パーティクルの大きさもまちまちにしよう。ParticleScaleNodeオブジェクトを加える。以下のように、ParticleScaleNode()に渡す第1引数のモードは定数ParticlePropertiesMode.GLOBALでグローバルにした。第2および第3引数はデフォルト値のfalseを与え、第4および第5引数でそれぞれ最小と最大の伸縮率を決めた。これで、パーティクルごとに大きさが変わる図3⁠。

new ParticleScaleNode(モード, cycleDurationの使用, cyclePhaseの使用, 最小伸縮率, 最大伸縮率)
var ParticleScaleNode = require("awayjs-renderergl/lib/animators/nodes/ParticleScaleNode");

function createParticles(numFires, numParticles, radius, y, scene) {
  var GLOBAL = ParticlePropertiesMode.GLOBAL;
  var animations = [
    new ParticleBillboardNode(),
    new ParticleScaleNode(GLOBAL, false, false, 2.5, 0.5),
    new ParticleVelocityNode(GLOBAL, new Vector3D(0, 80, 0)),
    new ParticleVelocityNode(ParticlePropertiesMode.LOCAL_STATIC)
  ];

}
図3 パーティクルの大きさがまちまちになった
図3 パーティクルの大きさがまちまちになった

パーティクルごとに異なる動きを加えて広がるようにし、大きさもまちまちにしたスクリプトはコード1にまとめた。アニメーションとしては、炎のふるまいに近づいたのではないか。

コード1 パーティクルごとの動きを広げて大きさもばらつかせる
var LoaderEvent = require("awayjs-core/lib/events/LoaderEvent");
var TimerEvent = require("awayjs-core/lib/events/TimerEvent");
var Vector3D = require("awayjs-core/lib/geom/Vector3D");
var AssetLibrary = require("awayjs-core/lib/library/AssetLibrary");
var URLRequest = require("awayjs-core/lib/net/URLRequest");
var RequestAnimationFrame = require("awayjs-core/lib/utils/RequestAnimationFrame");
var Timer = require("awayjs-core/lib/utils/Timer");
var View = require("awayjs-display/lib/containers/View");
var DirectionalLight = require("awayjs-display/lib/entities/DirectionalLight");
var Mesh = require("awayjs-display/lib/entities/Mesh");

var StaticLightPicker = require("awayjs-display/lib/materials/lightpickers/StaticLightPicker");
var PrimitivePlanePrefab = require("awayjs-display/lib/prefabs/PrimitivePlanePrefab");
var ParticleAnimationSet = require("awayjs-renderergl/lib/animators/ParticleAnimationSet");
var ParticleAnimator = require("awayjs-renderergl/lib/animators/ParticleAnimator");
var ParticlePropertiesMode = require("awayjs-renderergl/lib/animators/data/ParticlePropertiesMode");
var ParticleBillboardNode = require("awayjs-renderergl/lib/animators/nodes/ParticleBillboardNode");
var ParticleScaleNode = require("awayjs-renderergl/lib/animators/nodes/ParticleScaleNode");
var ParticleVelocityNode = require("awayjs-renderergl/lib/animators/nodes/ParticleVelocityNode");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var MethodMaterial = require("awayjs-methodmaterials/lib/MethodMaterial");
var MethodRendererPool = require("awayjs-methodmaterials/lib/pool/MethodRendererPool");
var HoverController = require("awayjs-display/lib/controllers/HoverController");
var ParticleGeometryHelper = require("awayjs-renderergl/lib/utils/ParticleGeometryHelper");
var view;
var plane;
var cameraController;
var lightPicker;
var timer;
var planeDiffuse = "assets/floor_diffuse.jpg";
var lastMouseX;
var lastMouseY;
var lastPanAngle;
var lastTiltAngle;
var fireObjects;

var particleMaterial;
function initialize() {
  var directionalLight = createDirectionalLight(0.25, 0xFFFFFF);
  view = createView(240, 180, 0x0);
  var scene = view.scene;
  lightPicker = new StaticLightPicker([directionalLight]);
  plane = createPlane(800, 800, lightPicker, -20);
  fireObjects = createParticles(3, 500, 300, 5, scene);
  scene.addChild(plane);
  cameraController = setupCameraController(view.camera, 1000, 0, 90, 45, 20);
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new URLRequest(planeDiffuse));
  document.onmousedown = startDrag;
  timer = new RequestAnimationFrame(render);
  timer.start();
  var fireTimer = new Timer(1000, fireObjects.length);
  fireTimer.addEventListener(TimerEvent.TIMER, startFire);
  fireTimer.start();
}
function createView(width, height, backgroundColor) {
  var defaultRenderer = new DefaultRenderer(MethodRendererPool);
  var view = new View(defaultRenderer);
  view.width = width;
  view.height = height;
  view.backgroundColor = backgroundColor;
  return view;
}
function createPlane(width, height, light, y) {
  var material = new MethodMaterial();
  var plane = new PrimitivePlanePrefab(width, height).getNewObject();
  plane.material = material;
  material.lightPicker = light;
  plane.y = y;
  return plane;
}
function createParticles(numFires, numParticles, radius, y, scene) {
  var GLOBAL = ParticlePropertiesMode.GLOBAL;
  var animations = [
    new ParticleBillboardNode(),
    new ParticleScaleNode(GLOBAL, false, false, 2.5, 0.5),
    new ParticleVelocityNode(GLOBAL, new Vector3D(0, 80, 0)),
    new ParticleVelocityNode(ParticlePropertiesMode.LOCAL_STATIC)
  ];
  var animationSet = getParticleAnimationSet(animations, initParticle);
  var primitive = new PrimitivePlanePrefab(10, 10, 1, 1, false);
  var geometry = primitive.geometry;
  var material = particleMaterial = new MethodMaterial();
  var geometrySet = [];
  for (var i = 0; i < numParticles; i++) {
    geometrySet[i] = geometry;
  }
  var fireObjects = getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene);
  return fireObjects;
}
function getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene) {
  var fireObjects = [];
  var particleGeometry = ParticleGeometryHelper.generateGeometry(geometrySet);
  var anglePerFire = Math.PI * 2 / numFires;
  for (var i = 0; i < numFires; i++) {
    var mesh = createAnimationParticle(particleGeometry, material, animationSet, fireObjects);
    var angle = i * anglePerFire;
    mesh.x = radius * Math.sin(angle);
    mesh.z = radius * Math.cos(angle);
    mesh.y = y;
    scene.addChild(mesh);
  }
  return fireObjects;
}
function getParticleAnimationSet(animations, initParticleFunc) {
  var animationSet = new ParticleAnimationSet(true, true);
  var count = animations.length;
  for (var i = 0; i < count; i++) {
    animationSet.addAnimation(animations[i]);
  }
  animationSet.initParticleFunc = initParticleFunc;
  return animationSet;
}
function createAnimationParticle(particleGeometry, material, animationSet, fireObjects) {
  var mesh = new Mesh(particleGeometry, material);
  var animator = new ParticleAnimator(animationSet);
  mesh.animator = animator;
  fireObjects.push(new FireObject(mesh, animator));
  return mesh;
}
function initParticle(prop) {
  var PIx2 = Math.PI * 2;
  var radian0 = Math.random() * PIx2;
  var radian1 = Math.random() * PIx2;
  var velocityVector = getVector3D(15, radian0, radian1);
  prop.startTime = Math.random() * 5;
  prop.duration = Math.random() * 4 + 0.1;
  prop[ParticleVelocityNode.VELOCITY_VECTOR3D] = velocityVector;
}
function getVector3D(radius, angleXY, angleXZ) {
  var sinXY = Math.sin(angleXY);
  var cosXY = Math.cos(angleXY);
  var sinXZ = Math.sin(angleXZ);
  var cosXZ = Math.cos(angleXZ);
  var vector = new Vector3D(radius * cosXY * cosXZ, radius * cosXY * sinXZ, radius * sinXY);
  return vector;
}
function startFire(eventObject) {
  var fireTimer = eventObject.target;
  var count = fireTimer.currentCount;
  var fireObject = fireObjects[count - 1];
  fireObject.startAnimation();
  if (count >= fireTimer.repeatCount) {
    fireTimer.removeEventListener(TimerEvent.TIMER, startFire);
  }
}
function createDirectionalLight(ambient, color) {
  var light = new DirectionalLight();
  light.ambient = ambient;
  light.color = color;
  return light;
}
function setupCameraController(camera, distance, minTiltAngle, maxTiltAngle, panAngle, tiltAngle) {
  var cameraController = new HoverController(camera);
  cameraController.distance = distance;
  cameraController.minTiltAngle = minTiltAngle;
  cameraController.maxTiltAngle = maxTiltAngle;
  cameraController.panAngle = panAngle;
  cameraController.tiltAngle = tiltAngle;
  return cameraController;
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var material;
  var count = assets.length;
  var url = eventObject.url;
  for (var i = 0; i < count; i++) {
    var asset = assets[i];
    switch (url) {
      case (planeDiffuse):
        material = plane.material;
        material.texture = asset;
        break;
    }
  }
}
function render(timeStamp) {
  view.render();
}
function startDrag(eventObject) {
  lastMouseX = eventObject.clientX;
  lastMouseY = eventObject.clientY;
  lastPanAngle = cameraController.panAngle;
  lastTiltAngle = cameraController.tiltAngle;
  document.onmousemove = drag;
  document.onmouseup = stopDrag;
}
function drag(eventObject) {
  cameraController.panAngle = 0.5 * (eventObject.clientX - lastMouseX) + lastPanAngle;
  cameraController.tiltAngle = 0.3 * (eventObject.clientY - lastMouseY) + lastTiltAngle;
}
function stopDrag(eventObject) {
  document.onmousemove = null;
  document.onmouseup = null;
}

パーティクルのカラーをアニメーションさせる

つぎに、パーティクルのカラーもアニメーションで変えてみたい。アニメーションノードには、 ParticleColorNodeオブジェクトを加える。そして、アニメーションの初めと終わりの色は、ParticleColorNode()コンストラクタの第6および第7引数にColorTransformオブジェクトで与える。コンストラクタの第2および第3引数の乗数とオフセットは、ColorTransformオブジェクトによる色の定め方とともに説明しよう。なお、表2にこれまでご紹介したアニメーションノードのコンストラクタをまとめた。

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

ColorTransformオブジェクトは、RGBAチャネルごとに定める乗数とオフセットによりカラーを変える仕組みだ。乗数は0から1の間の比率で、チャネルごとのカラー値に乗じる。オフセットは、±255の間の整数値を各カラー値に加える。その計算の結果、チャネルごとのピクセルカラー値がもとの色から変わるということだ。たとえば、Flash Professional CCには、この仕組みによりインスタンスの色を変える[カラー効果]の設定がある図4⁠。

もとの色×乗数 + オフセット
図4 Flash Professional CCの[カラー効果]
図4 Flash Professional CCの[カラー効果]

ColorTransform()コンストラクタに渡す引数はつぎのように、初めの4つが順にRGBAの乗数(デフォルト値1⁠⁠、後の4つが同じくオフセット(デフォルト値0)である。

new ColorTransform(乗数赤, 乗数緑, 乗数青, 乗数アルファ, オフセット赤, オフセット緑, オフセット青, オフセットアルファ)

もうひとつ、パーティクルを輝かせる工夫だ。DisplayObjectインスタンスにはDisplayObject.blendModeプロパティが備わっていて、背後のオブジェクトのイメージとピクセルをどう合成するかが決められる。プロパティに定数BlendMode.ADDを定めると、Fireworks CS6の[ブレンドモード]にある[加法]と同じで、オブジェクトの重なりはカラー値が加算されて明るくなる図5⁠。

図5 Fireworks CS6の[ブレンドモード][加法]を選ぶと重なりが明るくなる
図5 Fireworks CS6の[ブレンドモード]で[加法]を選ぶと重なりが明るくなる 図5 Fireworks CS6の[ブレンドモード]で[加法]を選ぶと重なりが明るくなる

アニメーションさせるカラーの初めと終わりを定めるColorTransformオブジェクトは、以下のようにどちらもRGBの乗数値を0にした。すると、もとの色が何であれ、カラーは黒(0x000000)になる。そのうえでRGBのオフセットを与えれば、そのカラーがアニメーションに定められることになる(つまり、0xFF3301から0x990000に変わる⁠⁠。

ParticleColorNode()コンストラクタの第1引数はParticlePropertiesMode.GLOBALとし、第2および第3引数は乗数とオフセットをともに使うためtrue第4および第5はデフォルト値のfalse第6と第7に上述のColorTransformオブジェクトを与えた。そして、DisplayObject.blendModeプロパティは定数BlendMode.ADDだ。

var BlendMode = require("awayjs-core/lib/data/BlendMode");

var ColorTransform = require("awayjs-core/lib/geom/ColorTransform");

var ParticleColorNode = require("awayjs-renderergl/lib/animators/nodes/ParticleColorNode");

function createParticles(numFires, numParticles, radius, y, scene) {
  var GLOBAL = ParticlePropertiesMode.GLOBAL;
  var startColor = new ColorTransform(0, 0, 0, 1, 0xFF, 0x33, 0x01);
  var endColor = new ColorTransform(0, 0, 0, 1, 0x99);
  var animations = [
    new ParticleBillboardNode(),
    new ParticleScaleNode(GLOBAL, false, false, 2.5, 0.5),
    new ParticleVelocityNode(GLOBAL, new Vector3D(0, 80, 0)),
    new ParticleColorNode(GLOBAL, true, true, false, false, startColor, endColor),
    new ParticleVelocityNode(ParticlePropertiesMode.LOCAL_STATIC)
  ];
  var material = particleMaterial = new MethodMaterial();
  material.blendMode = BlendMode.ADD;
}

パーティクルの初めの色と加算(加法)のブレンドモードにより、パーティクル同士がともに重なる始まりは黄色く、終わりに向けてばらけるにしたがって赤く変わる図6⁠。ただし、パーティクルのかたちは、よく見ると平面の矩形になっている図6下⁠。ここまでのスクリプトはコード2にまとめた。

図6 パーティクルは黄色から赤に移り変わる
図6 パーティクルは黄色から赤に移り変わる 図6 パーティクルは黄色から赤に移り変わる
コード2 パーティクルのカラーを炎のような明るい黄色から赤に移り変わらせる
var BlendMode = require("awayjs-core/lib/data/BlendMode");
var LoaderEvent = require("awayjs-core/lib/events/LoaderEvent");
var TimerEvent = require("awayjs-core/lib/events/TimerEvent");
var ColorTransform = require("awayjs-core/lib/geom/ColorTransform");
var Vector3D = require("awayjs-core/lib/geom/Vector3D");
var AssetLibrary = require("awayjs-core/lib/library/AssetLibrary");
var URLRequest = require("awayjs-core/lib/net/URLRequest");
var RequestAnimationFrame = require("awayjs-core/lib/utils/RequestAnimationFrame");
var Timer = require("awayjs-core/lib/utils/Timer");
var View = require("awayjs-display/lib/containers/View");
var DirectionalLight = require("awayjs-display/lib/entities/DirectionalLight");
var Mesh = require("awayjs-display/lib/entities/Mesh");

var StaticLightPicker = require("awayjs-display/lib/materials/lightpickers/StaticLightPicker");
var PrimitivePlanePrefab = require("awayjs-display/lib/prefabs/PrimitivePlanePrefab");
var ParticleAnimationSet = require("awayjs-renderergl/lib/animators/ParticleAnimationSet");
var ParticleAnimator = require("awayjs-renderergl/lib/animators/ParticleAnimator");
var ParticlePropertiesMode = require("awayjs-renderergl/lib/animators/data/ParticlePropertiesMode");
var ParticleBillboardNode = require("awayjs-renderergl/lib/animators/nodes/ParticleBillboardNode");
var ParticleScaleNode = require("awayjs-renderergl/lib/animators/nodes/ParticleScaleNode");
var ParticleVelocityNode = require("awayjs-renderergl/lib/animators/nodes/ParticleVelocityNode");
var ParticleColorNode = require("awayjs-renderergl/lib/animators/nodes/ParticleColorNode");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var MethodMaterial = require("awayjs-methodmaterials/lib/MethodMaterial");
var MethodRendererPool = require("awayjs-methodmaterials/lib/pool/MethodRendererPool");
var HoverController = require("awayjs-display/lib/controllers/HoverController");
var ParticleGeometryHelper = require("awayjs-renderergl/lib/utils/ParticleGeometryHelper");
var view;
var plane;
var cameraController;
var lightPicker;
var timer;
var planeDiffuse = "assets/floor_diffuse.jpg";
var lastMouseX;
var lastMouseY;
var lastPanAngle;
var lastTiltAngle;
var fireObjects;

var particleMaterial;
function initialize() {
  var directionalLight = createDirectionalLight(0.25, 0xFFFFFF);
  view = createView(240, 180, 0x0);
  var scene = view.scene;
  lightPicker = new StaticLightPicker([directionalLight]);
  plane = createPlane(800, 800, lightPicker, -20);
  fireObjects = createParticles(3, 500, 300, 5, scene);
  scene.addChild(plane);
  cameraController = setupCameraController(view.camera, 1000, 0, 90, 45, 20);
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new URLRequest(planeDiffuse));
  document.onmousedown = startDrag;
  timer = new RequestAnimationFrame(render);
  timer.start();
  var fireTimer = new Timer(1000, fireObjects.length);
  fireTimer.addEventListener(TimerEvent.TIMER, startFire);
  fireTimer.start();
}
function createView(width, height, backgroundColor) {
  var defaultRenderer = new DefaultRenderer(MethodRendererPool);
  var view = new View(defaultRenderer);
  view.width = width;
  view.height = height;
  view.backgroundColor = backgroundColor;
  return view;
}
function createPlane(width, height, light, y) {
  var material = new MethodMaterial();
  var plane = new PrimitivePlanePrefab(width, height).getNewObject();
  plane.material = material;
  material.lightPicker = light;
  plane.y = y;
  return plane;
}
function createParticles(numFires, numParticles, radius, y, scene) {
  var GLOBAL = ParticlePropertiesMode.GLOBAL;
  var startColor = new ColorTransform(0, 0, 0, 1, 0xFF, 0x33, 0x01);
  var endColor = new ColorTransform(0, 0, 0, 1, 0x99);
  var animations = [
    new ParticleBillboardNode(),
    new ParticleScaleNode(GLOBAL, false, false, 2.5, 0.5),
    new ParticleVelocityNode(GLOBAL, new Vector3D(0, 80, 0)),
    new ParticleColorNode(GLOBAL, true, true, false, false, startColor, endColor),
    new ParticleVelocityNode(ParticlePropertiesMode.LOCAL_STATIC)
  ];
  var animationSet = getParticleAnimationSet(animations, initParticle);
  var primitive = new PrimitivePlanePrefab(10, 10, 1, 1, false);
  var geometry = primitive.geometry;
  var material = particleMaterial = new MethodMaterial();
  var geometrySet = [];
  material.blendMode = BlendMode.ADD;
  for (var i = 0; i < numParticles; i++) {
    geometrySet[i] = geometry;
  }
  var fireObjects = getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene);
  return fireObjects;
}
function getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene) {
  var fireObjects = [];
  var particleGeometry = ParticleGeometryHelper.generateGeometry(geometrySet);
  var anglePerFire = Math.PI * 2 / numFires;
  for (var i = 0; i < numFires; i++) {
    var mesh = createAnimationParticle(particleGeometry, material, animationSet, fireObjects);
    var angle = i * anglePerFire;
    mesh.x = radius * Math.sin(angle);
    mesh.z = radius * Math.cos(angle);
    mesh.y = y;
    scene.addChild(mesh);
  }
  return fireObjects;
}
function getParticleAnimationSet(animations, initParticleFunc) {
  var animationSet = new ParticleAnimationSet(true, true);
  var count = animations.length;
  for (var i = 0; i < count; i++) {
    animationSet.addAnimation(animations[i]);
  }
  animationSet.initParticleFunc = initParticleFunc;
  return animationSet;
}
function createAnimationParticle(particleGeometry, material, animationSet, fireObjects) {
  var mesh = new Mesh(particleGeometry, material);
  var animator = new ParticleAnimator(animationSet);
  mesh.animator = animator;
  fireObjects.push(new FireObject(mesh, animator));
  return mesh;
}
function initParticle(prop) {
  var PIx2 = Math.PI * 2;
  var radian0 = Math.random() * PIx2;
  var radian1 = Math.random() * PIx2;
  var velocityVector = getVector3D(15, radian0, radian1);
  prop.startTime = Math.random() * 5;
  prop.duration = Math.random() * 4 + 0.1;
  prop[ParticleVelocityNode.VELOCITY_VECTOR3D] = velocityVector;
}
function getVector3D(radius, angleXY, angleXZ) {
  var sinXY = Math.sin(angleXY);
  var cosXY = Math.cos(angleXY);
  var sinXZ = Math.sin(angleXZ);
  var cosXZ = Math.cos(angleXZ);
  var vector = new Vector3D(radius * cosXY * cosXZ, radius * cosXY * sinXZ, radius * sinXY);
  return vector;
}
function startFire(eventObject) {
  var fireTimer = eventObject.target;
  var count = fireTimer.currentCount;
  var fireObject = fireObjects[count - 1];
  fireObject.startAnimation();
  if (count >= fireTimer.repeatCount) {
    fireTimer.removeEventListener(TimerEvent.TIMER, startFire);
  }
}
function createDirectionalLight(ambient, color) {
  var light = new DirectionalLight();
  light.ambient = ambient;
  light.color = color;
  return light;
}
function setupCameraController(camera, distance, minTiltAngle, maxTiltAngle, panAngle, tiltAngle) {
  var cameraController = new HoverController(camera);
  cameraController.distance = distance;
  cameraController.minTiltAngle = minTiltAngle;
  cameraController.maxTiltAngle = maxTiltAngle;
  cameraController.panAngle = panAngle;
  cameraController.tiltAngle = tiltAngle;
  return cameraController;
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var material;
  var count = assets.length;
  var url = eventObject.url;
  for (var i = 0; i < count; i++) {
    var asset = assets[i];
    switch (url) {
      case (planeDiffuse):
        material = plane.material;
        material.texture = asset;
        break;
    }
  }
}
function render(timeStamp) {
  view.render();
}
function startDrag(eventObject) {
  lastMouseX = eventObject.clientX;
  lastMouseY = eventObject.clientY;
  lastPanAngle = cameraController.panAngle;
  lastTiltAngle = cameraController.tiltAngle;
  document.onmousemove = drag;
  document.onmouseup = stopDrag;
}
function drag(eventObject) {
  cameraController.panAngle = 0.5 * (eventObject.clientX - lastMouseX) + lastPanAngle;
  cameraController.tiltAngle = 0.3 * (eventObject.clientY - lastMouseY) + lastTiltAngle;
}
function stopDrag(eventObject) {
  document.onmousemove = null;
  document.onmouseup = null;
}

パーティクルにテクスチャを与える

パーティクルにテクスチャを与えて、矩形でなく柔らかな円形にしよう。テクスチャの画像は、awayjs-examplesからblue.pngを用いた図7⁠。このファイルを素材用のフォルダ(assets)に納めておく。テクスチャファイルが床とパーティクルの2つになったので、AssetLibraryクラスを用いた読込みは以下のように新たに関数(loadAsset())として定めた。

図7 パーティクルの素材に与えるPNG画像
図7 パーティクルの素材に与えるPNG画像
var planeDiffuse = "assets/floor_diffuse.jpg";
var imageParticle = "assets/blue.png";

function initialize() {

  // AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  // AssetLibrary.load(new URLRequest(planeDiffuse));
  loadAsset(planeDiffuse);
  loadAsset(imageParticle);

}

function loadAsset(url) {
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new URLRequest(url));
}

そして、LoaderEvent.RESOURCE_COMPLETEイベントのリスナー関数(onResourceComplete())に読み込んだ素材(asset)がパーティクルの画像(imageParticle)だった場合のcase文を加え、パーティクルのMethodMaterialオブジェクト(particleMaterial)MaterialBase.textureプロパティに定めた。これで、パーティクルの平面は柔らかな円形になり、アニメーションの見た目も炎らしくなった図8⁠。スクリプトはコード3にまとめた。併せて、サンプル1をjsdo.itに掲げる。

function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var material;
  var count = assets.length;
  var url = eventObject.url;
  for (var i = 0; i < count; i++) {
    var asset = assets[i];
    switch (url) {
      case (planeDiffuse):
        material = plane.material;
        material.texture = asset;
        break;
      case (imageParticle):
        particleMaterial.texture = asset;
        break;
    }
  }
}
図8 パーティクルに柔らかな円形のテクスチャが与えられた
図8 パーティクルに柔らかな円形のテクスチャが与えられた
コード3 アニメーションするパーティクルに柔らかな円形のテクスチャを与える
var BlendMode = require("awayjs-core/lib/data/BlendMode");
var LoaderEvent = require("awayjs-core/lib/events/LoaderEvent");
var TimerEvent = require("awayjs-core/lib/events/TimerEvent");
var ColorTransform = require("awayjs-core/lib/geom/ColorTransform");
var Vector3D = require("awayjs-core/lib/geom/Vector3D");
var AssetLibrary = require("awayjs-core/lib/library/AssetLibrary");
var URLRequest = require("awayjs-core/lib/net/URLRequest");
var RequestAnimationFrame = require("awayjs-core/lib/utils/RequestAnimationFrame");
var Timer = require("awayjs-core/lib/utils/Timer");
var View = require("awayjs-display/lib/containers/View");
var DirectionalLight = require("awayjs-display/lib/entities/DirectionalLight");
var Mesh = require("awayjs-display/lib/entities/Mesh");

var StaticLightPicker = require("awayjs-display/lib/materials/lightpickers/StaticLightPicker");
var PrimitivePlanePrefab = require("awayjs-display/lib/prefabs/PrimitivePlanePrefab");
var ParticleAnimationSet = require("awayjs-renderergl/lib/animators/ParticleAnimationSet");
var ParticleAnimator = require("awayjs-renderergl/lib/animators/ParticleAnimator");
var ParticlePropertiesMode = require("awayjs-renderergl/lib/animators/data/ParticlePropertiesMode");
var ParticleBillboardNode = require("awayjs-renderergl/lib/animators/nodes/ParticleBillboardNode");
var ParticleScaleNode = require("awayjs-renderergl/lib/animators/nodes/ParticleScaleNode");
var ParticleVelocityNode = require("awayjs-renderergl/lib/animators/nodes/ParticleVelocityNode");
var ParticleColorNode = require("awayjs-renderergl/lib/animators/nodes/ParticleColorNode");
var DefaultRenderer = require("awayjs-renderergl/lib/DefaultRenderer");
var MethodMaterial = require("awayjs-methodmaterials/lib/MethodMaterial");
var MethodRendererPool = require("awayjs-methodmaterials/lib/pool/MethodRendererPool");
var HoverController = require("awayjs-display/lib/controllers/HoverController");
var ParticleGeometryHelper = require("awayjs-renderergl/lib/utils/ParticleGeometryHelper");
var view;
var plane;
var cameraController;
var lightPicker;
var timer;
var planeDiffuse = "assets/floor_diffuse.jpg";
var imageParticle = "assets/blue.png";
var lastMouseX;
var lastMouseY;
var lastPanAngle;
var lastTiltAngle;
var fireObjects;

var particleMaterial;
function initialize() {
  var directionalLight = createDirectionalLight(0.25, 0xFFFFFF);
  view = createView(240, 180, 0x0);
  var scene = view.scene;
  lightPicker = new StaticLightPicker([directionalLight]);
  plane = createPlane(800, 800, lightPicker, -20);
  fireObjects = createParticles(3, 500, 300, 5, scene);
  scene.addChild(plane);
  cameraController = setupCameraController(view.camera, 1000, 0, 90, 45, 20);
  loadAsset(planeDiffuse);
  loadAsset(imageParticle);
  document.onmousedown = startDrag;
  timer = new RequestAnimationFrame(render);
  timer.start();
  var fireTimer = new Timer(1000, fireObjects.length);
  fireTimer.addEventListener(TimerEvent.TIMER, startFire);
  fireTimer.start();
}
function createView(width, height, backgroundColor) {
  var defaultRenderer = new DefaultRenderer(MethodRendererPool);
  var view = new View(defaultRenderer);
  view.width = width;
  view.height = height;
  view.backgroundColor = backgroundColor;
  return view;
}
function createPlane(width, height, light, y) {
  var material = new MethodMaterial();
  var plane = new PrimitivePlanePrefab(width, height).getNewObject();
  plane.material = material;
  material.lightPicker = light;
  plane.y = y;
  return plane;
}
function createParticles(numFires, numParticles, radius, y, scene) {
  var GLOBAL = ParticlePropertiesMode.GLOBAL;
  var startColor = new ColorTransform(0, 0, 0, 1, 0xFF, 0x33, 0x01);
  var endColor = new ColorTransform(0, 0, 0, 1, 0x99);
  var animations = [
    new ParticleBillboardNode(),
    new ParticleScaleNode(GLOBAL, false, false, 2.5, 0.5),
    new ParticleVelocityNode(GLOBAL, new Vector3D(0, 80, 0)),
    new ParticleColorNode(GLOBAL, true, true, false, false, startColor, endColor),
    new ParticleVelocityNode(ParticlePropertiesMode.LOCAL_STATIC)
  ];
  var animationSet = getParticleAnimationSet(animations, initParticle);
  var primitive = new PrimitivePlanePrefab(10, 10, 1, 1, false);
  var geometry = primitive.geometry;
  var material = particleMaterial = new MethodMaterial();
  var geometrySet = [];
  material.blendMode = BlendMode.ADD;
  for (var i = 0; i < numParticles; i++) {
    geometrySet[i] = geometry;
  }
  var fireObjects = getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene);
  return fireObjects;
}
function getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene) {
  var fireObjects = [];
  var particleGeometry = ParticleGeometryHelper.generateGeometry(geometrySet);
  var anglePerFire = Math.PI * 2 / numFires;
  for (var i = 0; i < numFires; i++) {
    var mesh = createAnimationParticle(particleGeometry, material, animationSet, fireObjects);
    var angle = i * anglePerFire;
    mesh.x = radius * Math.sin(angle);
    mesh.z = radius * Math.cos(angle);
    mesh.y = y;
    scene.addChild(mesh);
  }
  return fireObjects;
}
function getParticleAnimationSet(animations, initParticleFunc) {
  var animationSet = new ParticleAnimationSet(true, true);
  var count = animations.length;
  for (var i = 0; i < count; i++) {
    animationSet.addAnimation(animations[i]);
  }
  animationSet.initParticleFunc = initParticleFunc;
  return animationSet;
}
function createAnimationParticle(particleGeometry, material, animationSet, fireObjects) {
  var mesh = new Mesh(particleGeometry, material);
  var animator = new ParticleAnimator(animationSet);
  mesh.animator = animator;
  fireObjects.push(new FireObject(mesh, animator));
  return mesh;
}
function initParticle(prop) {
  var PIx2 = Math.PI * 2;
  var radian0 = Math.random() * PIx2;
  var radian1 = Math.random() * PIx2;
  var velocityVector = getVector3D(15, radian0, radian1);
  prop.startTime = Math.random() * 5;
  prop.duration = Math.random() * 4 + 0.1;
  prop[ParticleVelocityNode.VELOCITY_VECTOR3D] = velocityVector;
}
function getVector3D(radius, angleXY, angleXZ) {
  var sinXY = Math.sin(angleXY);
  var cosXY = Math.cos(angleXY);
  var sinXZ = Math.sin(angleXZ);
  var cosXZ = Math.cos(angleXZ);
  var vector = new Vector3D(radius * cosXY * cosXZ, radius * cosXY * sinXZ, radius * sinXY);
  return vector;
}
function startFire(eventObject) {
  var fireTimer = eventObject.target;
  var count = fireTimer.currentCount;
  var fireObject = fireObjects[count - 1];
  fireObject.startAnimation();
  if (count >= fireTimer.repeatCount) {
    fireTimer.removeEventListener(TimerEvent.TIMER, startFire);
  }
}
function createDirectionalLight(ambient, color) {
  var light = new DirectionalLight();
  light.ambient = ambient;
  light.color = color;
  return light;
}
function setupCameraController(camera, distance, minTiltAngle, maxTiltAngle, panAngle, tiltAngle) {
  var cameraController = new HoverController(camera);
  cameraController.distance = distance;
  cameraController.minTiltAngle = minTiltAngle;
  cameraController.maxTiltAngle = maxTiltAngle;
  cameraController.panAngle = panAngle;
  cameraController.tiltAngle = tiltAngle;
  return cameraController;
}
function loadAsset(url) {
  AssetLibrary.addEventListener(LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  AssetLibrary.load(new URLRequest(url));
}
function onResourceComplete(eventObject) {
  var assets = eventObject.assets;
  var material;
  var count = assets.length;
  var url = eventObject.url;
  for (var i = 0; i < count; i++) {
    var asset = assets[i];
    switch (url) {
      case (planeDiffuse):
        material = plane.material;
        material.texture = asset;
        break;
      case (imageParticle):
        particleMaterial.texture = asset;
        break;
    }
  }
}
function render(timeStamp) {
  view.render();
}
function startDrag(eventObject) {
  lastMouseX = eventObject.clientX;
  lastMouseY = eventObject.clientY;
  lastPanAngle = cameraController.panAngle;
  lastTiltAngle = cameraController.tiltAngle;
  document.onmousemove = drag;
  document.onmouseup = stopDrag;
}
function drag(eventObject) {
  cameraController.panAngle = 0.5 * (eventObject.clientX - lastMouseX) + lastPanAngle;
  cameraController.tiltAngle = 0.3 * (eventObject.clientY - lastMouseY) + lastTiltAngle;
}
function stopDrag(eventObject) {
  document.onmousemove = null;
  document.onmouseup = null;
}
サンプル1 Away3D 15/03/13: Mapping a texture onto particles with animation

さて次回は、床に映る炎の光をアニメーションさせて、さらに表現力を高めたい。

距離と2つの角度から3次元空間の座標を求める

数学に興味のある読者向けに、距離とふたつの角度から3次元空間の座標を求める計算について補おう。第7回「回り込むカメラの真ん中にオブジェクトを捉える」立方体を中心に三角関数でカメラを回すの項で解説したとおり、2次元平面で原点からの距離がr、原点と結んだ線分がx軸正方向となす角がθの座標Pは、つぎの式で表される(なお、sinとcosは何する関数?参照⁠⁠。

P(r cosθ, r sinθ)

すると、この式を2つの角度にもとづいて2回使えば、3次元空間の座標もつぎのように導ける図9⁠。

OPz = QP = r sinθ
OQ = r cosθ
OPx = OQ cosω = r cosθcosω
OPy = OQ sinω = r cosθsinω

したがって、P(r cosθcosω, r cosθsinω, r sinθ)となる。

図9 3次元座標を原点からの距離と2平面との角度から三角関数で導く
図9 3次元座標を原点からの距離と2平面との角度から三角関数で導く

おすすめ記事

記事・ニュース一覧