623 lines
18 KiB
JavaScript
623 lines
18 KiB
JavaScript
const NUM_PLANE_POSITIONS = 12;
|
|
|
|
const BLENDINGS = {
|
|
normal: THREE.NormalBlending,
|
|
additive: THREE.AdditiveBlending,
|
|
substractive: THREE.SubstractiveBlending,
|
|
multiply: THREE.MultiplyBlending
|
|
};
|
|
|
|
const SHADERS = {
|
|
flat: THREE.MeshBasicMaterial,
|
|
lambert: THREE.MeshLambertMaterial,
|
|
phong: THREE.MeshPhongMaterial,
|
|
standard: THREE.MeshStandardMaterial
|
|
};
|
|
|
|
const OFFSCREEN_VEC3 = new THREE.Vector3(-99999, -99999, -99999);
|
|
|
|
/**
|
|
* Particle Player component for A-Frame.
|
|
*/
|
|
AFRAME.registerComponent('particleplayer', {
|
|
schema: {
|
|
blending: {
|
|
default: 'additive',
|
|
oneOf: ['normal', 'additive', 'multiply', 'substractive']
|
|
},
|
|
color: {default: '#fff', type: 'color'},
|
|
count: {default: '100%'},
|
|
delay: {default: 0, type: 'int'},
|
|
dur: {default: 1000, type: 'int'},
|
|
img: {type: 'selector'},
|
|
interpolate: {default: false},
|
|
loop: {default: 'false'},
|
|
on: {default: 'init'},
|
|
poolSize: {default: 5, type: 'int'}, // number of simultaneous particle systems
|
|
protation: {type: 'vec3'},
|
|
pscale: {default: 1.0, type: 'float'},
|
|
scale: {default: 1.0, type: 'float'},
|
|
initialScale: {type: 'vec3'},
|
|
finalScale: {type: 'vec3'},
|
|
animateScale: {default: false},
|
|
shader: {
|
|
default: 'flat',
|
|
oneOf: ['flat', 'lambert', 'phong', 'standard']
|
|
},
|
|
src: {type: 'selector'}
|
|
},
|
|
|
|
multiple: true,
|
|
|
|
init: function () {
|
|
this.frame = 0;
|
|
this.framedata = null;
|
|
this.indexPool = null;
|
|
this.lastFrame = 0;
|
|
this.material = null;
|
|
this.msPerFrame = 0;
|
|
this.numFrames = 0;
|
|
this.numParticles = 0; // total number of particles per system
|
|
this.originalVertexPositions = [];
|
|
this.particleCount = 0; // actual number of particles to spawn per event (data.count)
|
|
this.particleSystems = [];
|
|
this.protation = false;
|
|
this.restPositions = []; // position at first frame each particle is alive
|
|
this.restRotations = [];
|
|
this.sprite_rotation = false;
|
|
this.systems = null;
|
|
this.useRotation = false;
|
|
this.scaleAnim = new THREE.Vector3();
|
|
},
|
|
|
|
update: function (oldData) {
|
|
const data = this.data;
|
|
|
|
if (!data.src) {
|
|
return;
|
|
}
|
|
|
|
if (oldData.on !== data.on) {
|
|
if (oldData.on) {
|
|
this.el.removeEventListener(oldData.on, this.start);
|
|
}
|
|
if (data.on !== 'play') {
|
|
this.el.addEventListener(data.on, this.start.bind(this));
|
|
}
|
|
}
|
|
|
|
this.loadParticlesJSON(data.src, data.scale);
|
|
|
|
this.numFrames = this.framedata.length;
|
|
this.numParticles = this.numFrames > 0 ? this.framedata[0].length : 0;
|
|
|
|
if (data.count[data.count.length - 1] === '%') {
|
|
this.particleCount = Math.floor(
|
|
(parseInt(data.count) * this.numParticles) / 100.0
|
|
);
|
|
} else {
|
|
this.particleCount = parseInt(data.count);
|
|
}
|
|
this.particleCount = Math.min(
|
|
this.numParticles,
|
|
Math.max(0, this.particleCount)
|
|
);
|
|
|
|
this.msPerFrame = data.dur / this.numFrames;
|
|
|
|
this.indexPool = new Array(this.numParticles);
|
|
|
|
const materialParams = {
|
|
color: new THREE.Color(data.color),
|
|
side: THREE.DoubleSide,
|
|
blending: BLENDINGS[data.blending],
|
|
map: data.img ? new THREE.TextureLoader().load(data.img.src) : null,
|
|
depthWrite: false,
|
|
opacity: data.opacity,
|
|
transparent: !!data.img || data.blending !== 'normal' || data.opacity < 1
|
|
};
|
|
if (SHADERS[data.shader] !== undefined) {
|
|
this.material = new SHADERS[data.shader](materialParams);
|
|
} else {
|
|
this.material = new SHADERS['flat'](materialParams);
|
|
}
|
|
|
|
this.createParticles(data.poolSize);
|
|
|
|
if (data.on === 'init') {
|
|
this.start();
|
|
}
|
|
},
|
|
|
|
loadParticlesJSON: function (json, scale) {
|
|
var alive;
|
|
|
|
this.restPositions.length = 0;
|
|
this.restRotations.length = 0;
|
|
|
|
const jsonData = JSON.parse(json.data);
|
|
const frames = jsonData.frames;
|
|
const precision = jsonData.precision;
|
|
|
|
this.useRotation = jsonData.rotation;
|
|
|
|
if (jsonData.sprite_rotation !== false) {
|
|
this.sprite_rotation = {
|
|
x: jsonData.sprite_rotation[0] / precision,
|
|
y: jsonData.sprite_rotation[1] / precision,
|
|
z: jsonData.sprite_rotation[2] / precision
|
|
};
|
|
} else {
|
|
this.sprite_rotation = false;
|
|
}
|
|
|
|
this.framedata = new Array(frames.length);
|
|
for (let frameIndex = 0; frameIndex < frames.length; frameIndex++) {
|
|
this.framedata[frameIndex] = new Array(frames[frameIndex].length);
|
|
for (
|
|
let particleIndex = 0;
|
|
particleIndex < frames[frameIndex].length;
|
|
particleIndex++
|
|
) {
|
|
let rawP = frames[frameIndex][particleIndex]; // data of particle i in frame f
|
|
alive = rawP !== 0; // 0 means not alive yet this frame.
|
|
|
|
let p = (this.framedata[frameIndex][particleIndex] = {
|
|
position: alive
|
|
? {
|
|
x: (rawP[0] / precision) * scale,
|
|
y: (rawP[1] / precision) * scale,
|
|
z: (rawP[2] / precision) * scale
|
|
}
|
|
: null,
|
|
alive: alive
|
|
});
|
|
|
|
if (jsonData.rotation) {
|
|
p.rotation = alive
|
|
? {
|
|
x: rawP[3] / precision,
|
|
y: rawP[4] / precision,
|
|
z: rawP[5] / precision
|
|
}
|
|
: null;
|
|
}
|
|
|
|
if (alive && frameIndex === 0) {
|
|
this.restPositions[particleIndex] = p.position
|
|
? {x: p.position.y, y: p.position.y, z: p.position.z}
|
|
: null;
|
|
this.restRotations[particleIndex] = p.rotation
|
|
? {x: p.rotation.y, y: p.rotation.y, z: p.rotation.z}
|
|
: null;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
createParticles: (function () {
|
|
const tempGeometries = [];
|
|
|
|
return function (numParticleSystems) {
|
|
const data = this.data;
|
|
var loop = parseInt(this.data.loop);
|
|
|
|
this.particleSystems.length = 0;
|
|
|
|
if (isNaN(loop)) {
|
|
loop = this.data.loop === 'true' ? Number.MAX_VALUE : 0;
|
|
}
|
|
|
|
for (let i = 0; i < numParticleSystems; i++) {
|
|
let particleSystem = {
|
|
active: false,
|
|
activeParticleIndices: new Array(this.particleCount),
|
|
loopCount: 0,
|
|
loopTotal: loop,
|
|
mesh: null,
|
|
time: 0,
|
|
pscale: data.animateScale ? new THREE.Vector3() : null
|
|
};
|
|
|
|
// Fill array of geometries to merge.
|
|
const ratio = data.img ? data.img.width / data.img.height : 1;
|
|
tempGeometries.length = 0;
|
|
for (let p = 0; p < this.numParticles; p++) {
|
|
let geometry = new THREE.PlaneBufferGeometry(
|
|
0.1 * ratio * data.pscale,
|
|
0.1 * data.pscale
|
|
);
|
|
if (this.sprite_rotation !== false) {
|
|
geometry.rotateX(this.sprite_rotation.x);
|
|
geometry.rotateY(this.sprite_rotation.y);
|
|
geometry.rotateZ(this.sprite_rotation.z);
|
|
} else {
|
|
geometry.rotateX((this.data.protation.x * Math.PI) / 180);
|
|
geometry.rotateY((this.data.protation.y * Math.PI) / 180);
|
|
geometry.rotateZ((this.data.protation.z * Math.PI) / 180);
|
|
}
|
|
tempGeometries.push(geometry);
|
|
}
|
|
|
|
// Create merged geometry for whole particle system.
|
|
let mergedBufferGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries(
|
|
tempGeometries
|
|
);
|
|
|
|
particleSystem.mesh = new THREE.Mesh(
|
|
mergedBufferGeometry,
|
|
this.material
|
|
);
|
|
particleSystem.mesh.visible = false;
|
|
this.el.setObject3D(`particleplayer${i}`, particleSystem.mesh);
|
|
copyArray(
|
|
this.originalVertexPositions,
|
|
mergedBufferGeometry.attributes.position.array
|
|
);
|
|
|
|
// Hide all particles by default.
|
|
for (
|
|
let i = 0;
|
|
i < mergedBufferGeometry.attributes.position.array.length;
|
|
i++
|
|
) {
|
|
mergedBufferGeometry.attributes.position.array[i] = -99999;
|
|
}
|
|
|
|
for (let i = 0; i < particleSystem.activeParticleIndices.length; i++) {
|
|
particleSystem.activeParticleIndices[i] = i;
|
|
}
|
|
|
|
this.particleSystems.push(particleSystem);
|
|
}
|
|
};
|
|
})(),
|
|
|
|
start: function (evt) {
|
|
if (this.data.delay > 0) {
|
|
setTimeout(() => this.startAfterDelay(evt), this.data.delay);
|
|
} else {
|
|
this.startAfterDelay(evt);
|
|
}
|
|
},
|
|
|
|
startAfterDelay: function (evt) {
|
|
// position, rotation
|
|
var found = -1;
|
|
var particleSystem;
|
|
var oldestTime = 0;
|
|
var position = evt ? evt.detail.position : null;
|
|
var rotation = evt ? evt.detail.rotation : null;
|
|
|
|
if (!(position instanceof THREE.Vector3)) {
|
|
position = new THREE.Vector3();
|
|
}
|
|
if (!(rotation instanceof THREE.Euler)) {
|
|
rotation = new THREE.Euler();
|
|
}
|
|
|
|
// find available (or oldest) particle system
|
|
for (var i = 0; i < this.particleSystems.length; i++) {
|
|
if (this.particleSystems[i].active === false) {
|
|
found = i;
|
|
break;
|
|
}
|
|
if (this.particleSystems[i].time > oldestTime) {
|
|
found = i;
|
|
oldestTime = this.particleSystems[i].time;
|
|
}
|
|
}
|
|
|
|
if (found === -1) { return; }
|
|
|
|
particleSystem = this.particleSystems[found];
|
|
particleSystem.active = true;
|
|
particleSystem.loopCount = 1;
|
|
particleSystem.mesh.visible = true;
|
|
particleSystem.mesh.position.copy(position);
|
|
particleSystem.mesh.rotation.copy(rotation);
|
|
particleSystem.time = 0;
|
|
if (this.data.animateScale) {
|
|
particleSystem.pscale.copy(this.data.initialScale);
|
|
}
|
|
|
|
this.resetParticles(particleSystem);
|
|
},
|
|
|
|
doLoop: function (particleSystem) {
|
|
particleSystem.loopCount++;
|
|
particleSystem.frame = -1;
|
|
particleSystem.time = 0;
|
|
if (this.data.animateScale) {
|
|
particleSystem.pscale.copy(data.initialScale);
|
|
}
|
|
this.resetParticles(particleSystem);
|
|
},
|
|
|
|
resetParticle: function (particleSystem, particleIndex) {
|
|
const geometry = particleSystem.mesh.geometry;
|
|
|
|
if (this.restPositions[particleIndex]) {
|
|
transformPlane(
|
|
particleIndex,
|
|
geometry,
|
|
this.originalVertexPositions,
|
|
this.restPositions[particleIndex],
|
|
this.useRotation && this.restRotations[particleIndex],
|
|
particleSystem.pscale
|
|
);
|
|
} else {
|
|
// Hide.
|
|
transformPlane(
|
|
particleIndex,
|
|
geometry,
|
|
this.originalVertexPositions,
|
|
OFFSCREEN_VEC3,
|
|
undefined,
|
|
null
|
|
);
|
|
}
|
|
|
|
// TODO: Can update transformPlane for lookAt.
|
|
// lookAt does not support rotated or translated parents! :_(
|
|
// part.lookAt(this.camera.position);
|
|
},
|
|
|
|
/**
|
|
* When starting or finishing (looping) animation, this resets particles
|
|
* to their initial position and, if user asked for replaying less than 100%
|
|
* of particles, randomly choose them.
|
|
*/
|
|
resetParticles: function (particleSystem) {
|
|
var i;
|
|
var rand;
|
|
|
|
// no picking, just hide and reset
|
|
if (this.particleCount === this.numParticles) {
|
|
for (i = 0; i < this.numParticles; i++) {
|
|
this.resetParticle(particleSystem, i);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// hide particles from last animation and initialize indexPool
|
|
const geometry = particleSystem.mesh.geometry;
|
|
for (i = 0; i < this.numParticles; i++) {
|
|
if (i < this.particleCount) {
|
|
transformPlane(
|
|
particleSystem.activeParticleIndices[i],
|
|
geometry,
|
|
this.originalVertexPositions,
|
|
OFFSCREEN_VEC3,
|
|
undefined,
|
|
null
|
|
);
|
|
}
|
|
this.indexPool[i] = i;
|
|
}
|
|
|
|
// scramble indexPool
|
|
for (i = 0; i < this.particleCount; i++) {
|
|
rand = i + Math.floor(Math.random() * (this.numParticles - i));
|
|
particleSystem.activeParticleIndices[i] = this.indexPool[rand];
|
|
this.indexPool[rand] = this.indexPool[i];
|
|
this.resetParticle(
|
|
particleSystem,
|
|
particleSystem.activeParticleIndices[i]
|
|
);
|
|
}
|
|
},
|
|
|
|
tick: (function () {
|
|
const helperPositionVec3 = new THREE.Vector3();
|
|
|
|
return function (time, delta) {
|
|
var frame; // current particle system frame
|
|
var fdata; // all particles data in current frame
|
|
var fdataNext; // next frame (for interpolation)
|
|
var useRotation = this.useRotation;
|
|
var frameTime; // time in current frame (for interpolation)
|
|
var relTime; // current particle system relative time (0-1)
|
|
var interpolate; // whether interpolate between frames or not
|
|
|
|
for (
|
|
let particleSystemIndex = 0;
|
|
particleSystemIndex < this.particleSystems.length;
|
|
particleSystemIndex++
|
|
) {
|
|
let particleSystem = this.particleSystems[particleSystemIndex];
|
|
if (!particleSystem.active) {
|
|
continue;
|
|
}
|
|
|
|
// if the duration is so short that there's no need to interpolate, don't do it
|
|
// even if user asked for it.
|
|
interpolate =
|
|
this.data.interpolate && this.data.dur / this.numFrames > delta;
|
|
relTime = particleSystem.time / this.data.dur;
|
|
frame = relTime * this.numFrames;
|
|
fdata = this.framedata[Math.floor(frame)];
|
|
if (interpolate) {
|
|
frameTime = frame - Math.floor(frame);
|
|
fdataNext =
|
|
frame < this.numFrames - 1
|
|
? this.framedata[Math.floor(frame) + 1]
|
|
: null;
|
|
}
|
|
|
|
if (this.data.animateScale) {
|
|
particleSystem.pscale.lerp(this.data.finalScale, relTime);
|
|
}
|
|
|
|
for (
|
|
let activeParticleIndex = 0;
|
|
activeParticleIndex < particleSystem.activeParticleIndices.length;
|
|
activeParticleIndex++
|
|
) {
|
|
let particleIndex =
|
|
particleSystem.activeParticleIndices[activeParticleIndex];
|
|
let rotation = useRotation && fdata[particleIndex].rotation;
|
|
|
|
// TODO: Add vertex position to original position to all vertices of plane...
|
|
if (!fdata[particleIndex].alive) {
|
|
// Hide plane off-screen when not alive.
|
|
transformPlane(
|
|
particleIndex,
|
|
particleSystem.mesh.geometry,
|
|
this.originalVertexPositions,
|
|
OFFSCREEN_VEC3,
|
|
undefined,
|
|
null
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if (interpolate && fdataNext && fdataNext[particleIndex].alive) {
|
|
helperPositionVec3.lerpVectors(
|
|
fdata[particleIndex].position,
|
|
fdataNext[particleIndex].position,
|
|
frameTime
|
|
);
|
|
transformPlane(
|
|
particleIndex,
|
|
particleSystem.mesh.geometry,
|
|
this.originalVertexPositions,
|
|
helperPositionVec3,
|
|
rotation,
|
|
particleSystem.pscale
|
|
);
|
|
} else {
|
|
transformPlane(
|
|
particleIndex,
|
|
particleSystem.mesh.geometry,
|
|
this.originalVertexPositions,
|
|
fdata[particleIndex].position,
|
|
rotation,
|
|
particleSystem.pscale
|
|
);
|
|
}
|
|
}
|
|
|
|
particleSystem.time += delta;
|
|
if (particleSystem.time >= this.data.dur) {
|
|
if (particleSystem.loopCount < particleSystem.loopTotal) {
|
|
this.el.emit('particleplayerloop', null, false);
|
|
this.doLoop(particleSystem);
|
|
} else {
|
|
this.el.emit('particleplayerfinished', null, false);
|
|
particleSystem.active = false;
|
|
particleSystem.mesh.visible = false;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
};
|
|
})(),
|
|
|
|
_transformPlane: transformPlane
|
|
});
|
|
|
|
// Use triangle geometry as a helper for rotating.
|
|
const tri = (function () {
|
|
const tri = new THREE.Geometry();
|
|
tri.vertices.push(new THREE.Vector3());
|
|
tri.vertices.push(new THREE.Vector3());
|
|
tri.vertices.push(new THREE.Vector3());
|
|
tri.faces.push(new THREE.Face3(0, 1, 2));
|
|
return tri;
|
|
})();
|
|
|
|
/**
|
|
* Faces of a plane are v0, v2, v1 and v2, v3, v1.
|
|
* Positions are 12 numbers: [v0, v1, v2, v3].
|
|
*/
|
|
function transformPlane (
|
|
particleIndex,
|
|
geometry,
|
|
originalArray,
|
|
position,
|
|
rotation,
|
|
scale
|
|
) {
|
|
const array = geometry.attributes.position.array;
|
|
const index = particleIndex * NUM_PLANE_POSITIONS;
|
|
|
|
// Calculate first face (0, 2, 1).
|
|
tri.vertices[0].set(
|
|
originalArray[index + 0],
|
|
originalArray[index + 1],
|
|
originalArray[index + 2]
|
|
);
|
|
tri.vertices[1].set(
|
|
originalArray[index + 3],
|
|
originalArray[index + 4],
|
|
originalArray[index + 5]
|
|
);
|
|
tri.vertices[2].set(
|
|
originalArray[index + 6],
|
|
originalArray[index + 7],
|
|
originalArray[index + 8]
|
|
);
|
|
if (scale !== null) {
|
|
tri.scale(scale.x, scale.y, scale.z);
|
|
}
|
|
if (rotation) {
|
|
tri.rotateX(rotation.x);
|
|
tri.rotateY(rotation.y);
|
|
tri.rotateZ(rotation.z);
|
|
}
|
|
tri.vertices[0].add(position);
|
|
tri.vertices[1].add(position);
|
|
tri.vertices[2].add(position);
|
|
array[index + 0] = tri.vertices[0].x;
|
|
array[index + 1] = tri.vertices[0].y;
|
|
array[index + 2] = tri.vertices[0].z;
|
|
array[index + 3] = tri.vertices[1].x;
|
|
array[index + 4] = tri.vertices[1].y;
|
|
array[index + 5] = tri.vertices[1].z;
|
|
array[index + 6] = tri.vertices[2].x;
|
|
array[index + 7] = tri.vertices[2].y;
|
|
array[index + 8] = tri.vertices[2].z;
|
|
|
|
// Calculate second face (2, 3, 1) just for the last vertex.
|
|
tri.vertices[0].set(
|
|
originalArray[index + 3],
|
|
originalArray[index + 4],
|
|
originalArray[index + 5]
|
|
);
|
|
tri.vertices[1].set(
|
|
originalArray[index + 6],
|
|
originalArray[index + 7],
|
|
originalArray[index + 8]
|
|
);
|
|
tri.vertices[2].set(
|
|
originalArray[index + 9],
|
|
originalArray[index + 10],
|
|
originalArray[index + 11]
|
|
);
|
|
if (scale !== null) {
|
|
tri.scale(scale.x, scale.y, scale.z);
|
|
}
|
|
if (rotation) {
|
|
tri.rotateX(rotation.x);
|
|
tri.rotateY(rotation.y);
|
|
tri.rotateZ(rotation.z);
|
|
}
|
|
tri.vertices[0].add(position);
|
|
tri.vertices[1].add(position);
|
|
tri.vertices[2].add(position);
|
|
array[index + 9] = tri.vertices[2].x;
|
|
array[index + 10] = tri.vertices[2].y;
|
|
array[index + 11] = tri.vertices[2].z;
|
|
|
|
geometry.attributes.position.needsUpdate = true;
|
|
}
|
|
module.exports.transformPlane = transformPlane;
|
|
|
|
function copyArray (dest, src) {
|
|
dest.length = 0;
|
|
for (let i = 0; i < src.length; i++) {
|
|
dest[i] = src[i];
|
|
}
|
|
}
|