Files
junisaber/src/components/particleplayer.js
Kevin Ngo e94ab73212 lint
2018-10-13 17:19:36 -07:00

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];
}
}