前回の第12回「パーティクルのアニメーションを加える 」では、のろしが立ち上るような表現まではできた(再掲第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平面との角度から求める
距離と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 パーティクルに広がる動きが加わった
さらに、パーティクルの大きさもまちまちにしよう。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 パーティクルの大きさがまちまちになった
パーティクルごとに異なる動きを加えて広がるようにし、大きさもまちまちにしたスクリプトはコード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の[カラー効果]
ColorTransform() コンストラクタに渡す引数はつぎのように、初めの4つが順にRGBAの乗数(デフォルト値1) 、後の4つが同じくオフセット(デフォルト値0)である。
new ColorTransform(乗数赤, 乗数緑, 乗数青, 乗数アルファ, オフセット赤, オフセット緑, オフセット青, オフセットアルファ)
もうひとつ、パーティクルを輝かせる工夫だ。DisplayObjectインスタンスにはDisplayObject.blendMode プロパティが備わっていて、背後のオブジェクトのイメージとピクセルをどう合成するかが決められる。プロパティに定数BlendMode.ADD を定めると、Fireworks CS6の[ブレンドモード]にある[加法]と同じで、オブジェクトの重なりはカラー値が加算されて明るくなる(図5 ) 。
図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 パーティクルは黄色から赤に移り変わる
コード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画像
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 パーティクルに柔らかな円形のテクスチャが与えられた
コード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平面との角度から三角関数で導く