mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-17 19:12:33 -05:00
1187 lines
40 KiB
JavaScript
1187 lines
40 KiB
JavaScript
// ShaderParticleUtils 0.7.9
|
|
//
|
|
// (c) 2014 Luke Moody (http://www.github.com/squarefeet)
|
|
// & Lee Stemkoski (http://www.adelphi.edu/~stemkoski/)
|
|
//
|
|
// Based on Lee Stemkoski's original work:
|
|
// (https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/js/ParticleEngine.js).
|
|
//
|
|
// ShaderParticleGroup may be freely distributed under the MIT license (See LICENSE.txt)
|
|
|
|
var SPE = SPE || {};
|
|
|
|
SPE.utils = {
|
|
|
|
/**
|
|
* Given a base vector and a spread range vector, create
|
|
* a new THREE.Vector3 instance with randomised values.
|
|
*
|
|
* @private
|
|
*
|
|
* @param {THREE.Vector3} base
|
|
* @param {THREE.Vector3} spread
|
|
* @return {THREE.Vector3}
|
|
*/
|
|
randomVector3: function( base, spread ) {
|
|
var v = new THREE.Vector3();
|
|
|
|
v.copy( base );
|
|
|
|
v.x += Math.random() * spread.x - (spread.x/2);
|
|
v.y += Math.random() * spread.y - (spread.y/2);
|
|
v.z += Math.random() * spread.z - (spread.z/2);
|
|
|
|
return v;
|
|
},
|
|
|
|
/**
|
|
* Create a new THREE.Color instance and given a base vector and
|
|
* spread range vector, assign random values.
|
|
*
|
|
* Note that THREE.Color RGB values are in the range of 0 - 1, not 0 - 255.
|
|
*
|
|
* @private
|
|
*
|
|
* @param {THREE.Vector3} base
|
|
* @param {THREE.Vector3} spread
|
|
* @return {THREE.Color}
|
|
*/
|
|
randomColor: function( base, spread ) {
|
|
var v = new THREE.Color();
|
|
|
|
v.copy( base );
|
|
|
|
v.r += (Math.random() * spread.x) - (spread.x/2);
|
|
v.g += (Math.random() * spread.y) - (spread.y/2);
|
|
v.b += (Math.random() * spread.z) - (spread.z/2);
|
|
|
|
v.r = Math.max( 0, Math.min( v.r, 1 ) );
|
|
v.g = Math.max( 0, Math.min( v.g, 1 ) );
|
|
v.b = Math.max( 0, Math.min( v.b, 1 ) );
|
|
|
|
return v;
|
|
},
|
|
|
|
/**
|
|
* Create a random Number value based on an initial value and
|
|
* a spread range
|
|
*
|
|
* @private
|
|
*
|
|
* @param {Number} base
|
|
* @param {Number} spread
|
|
* @return {Number}
|
|
*/
|
|
randomFloat: function( base, spread ) {
|
|
return base + spread * (Math.random() - 0.5);
|
|
},
|
|
|
|
/**
|
|
* Create a new THREE.Vector3 instance and project it onto a random point
|
|
* on a sphere with randomized radius.
|
|
*
|
|
* @param {THREE.Vector3} base
|
|
* @param {Number} radius
|
|
* @param {THREE.Vector3} radiusSpread
|
|
* @param {THREE.Vector3} radiusScale
|
|
*
|
|
* @private
|
|
*
|
|
* @return {THREE.Vector3}
|
|
*/
|
|
randomVector3OnSphere: function( base, radius, radiusSpread, radiusScale, radiusSpreadClamp ) {
|
|
var z = 2 * Math.random() - 1;
|
|
var t = 6.2832 * Math.random();
|
|
var r = Math.sqrt( 1 - z*z );
|
|
var vec = new THREE.Vector3( r * Math.cos(t), r * Math.sin(t), z );
|
|
|
|
var rand = this._randomFloat( radius, radiusSpread );
|
|
|
|
if( radiusSpreadClamp ) {
|
|
rand = Math.round( rand / radiusSpreadClamp ) * radiusSpreadClamp;
|
|
}
|
|
|
|
vec.multiplyScalar( rand );
|
|
|
|
if( radiusScale ) {
|
|
vec.multiply( radiusScale );
|
|
}
|
|
|
|
vec.add( base );
|
|
|
|
return vec;
|
|
},
|
|
|
|
/**
|
|
* Create a new THREE.Vector3 instance and project it onto a random point
|
|
* on a disk (in the XY-plane) centered at `base` and with randomized radius.
|
|
*
|
|
* @param {THREE.Vector3} base
|
|
* @param {Number} radius
|
|
* @param {THREE.Vector3} radiusSpread
|
|
* @param {THREE.Vector3} radiusScale
|
|
*
|
|
* @private
|
|
*
|
|
* @return {THREE.Vector3}
|
|
*/
|
|
randomVector3OnDisk: function( base, radius, radiusSpread, radiusScale, radiusSpreadClamp ) {
|
|
var t = 6.2832 * Math.random();
|
|
var rand = this._randomFloat( radius, radiusSpread );
|
|
|
|
if( radiusSpreadClamp ) {
|
|
rand = Math.round( rand / radiusSpreadClamp ) * radiusSpreadClamp;
|
|
}
|
|
|
|
var vec = new THREE.Vector3( Math.cos(t), Math.sin(t), 0 ).multiplyScalar( rand );
|
|
|
|
if ( radiusScale ) {
|
|
vec.multiply( radiusScale );
|
|
}
|
|
|
|
vec.add( base );
|
|
|
|
return vec ;
|
|
},
|
|
|
|
|
|
/**
|
|
* Create a new THREE.Vector3 instance, and given a sphere with center `base` and
|
|
* point `position` on sphere, set direction away from sphere center with random magnitude.
|
|
*
|
|
* @param {THREE.Vector3} base
|
|
* @param {THREE.Vector3} position
|
|
* @param {Number} speed
|
|
* @param {Number} speedSpread
|
|
* @param {THREE.Vector3} scale
|
|
*
|
|
* @private
|
|
*
|
|
* @return {THREE.Vector3}
|
|
*/
|
|
randomVelocityVector3OnSphere: function( base, position, speed, speedSpread, scale ) {
|
|
var direction = new THREE.Vector3().subVectors( base, position );
|
|
|
|
direction.normalize().multiplyScalar( Math.abs( this._randomFloat( speed, speedSpread ) ) );
|
|
|
|
if( scale ) {
|
|
direction.multiply( scale );
|
|
}
|
|
|
|
return direction;
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
* Given a base vector and a spread vector, randomise the given vector
|
|
* accordingly.
|
|
*
|
|
* @param {THREE.Vector3} vector
|
|
* @param {THREE.Vector3} base
|
|
* @param {THREE.Vector3} spread
|
|
*
|
|
* @private
|
|
*
|
|
* @return {[type]}
|
|
*/
|
|
randomizeExistingVector3: function( v, base, spread ) {
|
|
v.copy( base );
|
|
|
|
v.x += Math.random() * spread.x - (spread.x/2);
|
|
v.y += Math.random() * spread.y - (spread.y/2);
|
|
v.z += Math.random() * spread.z - (spread.z/2);
|
|
},
|
|
|
|
|
|
/**
|
|
* Randomize a THREE.Color instance and given a base vector and
|
|
* spread range vector, assign random values.
|
|
*
|
|
* Note that THREE.Color RGB values are in the range of 0 - 1, not 0 - 255.
|
|
*
|
|
* @private
|
|
*
|
|
* @param {THREE.Vector3} base
|
|
* @param {THREE.Vector3} spread
|
|
* @return {THREE.Color}
|
|
*/
|
|
randomizeExistingColor: function( v, base, spread ) {
|
|
v.copy( base );
|
|
|
|
v.r += (Math.random() * spread.x) - (spread.x/2);
|
|
v.g += (Math.random() * spread.y) - (spread.y/2);
|
|
v.b += (Math.random() * spread.z) - (spread.z/2);
|
|
|
|
v.r = Math.max( 0, Math.min( v.r, 1 ) );
|
|
v.g = Math.max( 0, Math.min( v.g, 1 ) );
|
|
v.b = Math.max( 0, Math.min( v.b, 1 ) );
|
|
},
|
|
|
|
/**
|
|
* Given an existing particle vector, project it onto a random point on a
|
|
* sphere with radius `radius` and position `base`.
|
|
*
|
|
* @private
|
|
*
|
|
* @param {THREE.Vector3} v
|
|
* @param {THREE.Vector3} base
|
|
* @param {Number} radius
|
|
*/
|
|
randomizeExistingVector3OnSphere: function( v, base, radius, radiusSpread, radiusScale, radiusSpreadClamp ) {
|
|
var z = 2 * Math.random() - 1,
|
|
t = 6.2832 * Math.random(),
|
|
r = Math.sqrt( 1 - z*z ),
|
|
rand = this._randomFloat( radius, radiusSpread );
|
|
|
|
if( radiusSpreadClamp ) {
|
|
rand = Math.round( rand / radiusSpreadClamp ) * radiusSpreadClamp;
|
|
}
|
|
|
|
v.set(
|
|
(r * Math.cos(t)) * rand,
|
|
(r * Math.sin(t)) * rand,
|
|
z * rand
|
|
).multiply( radiusScale );
|
|
|
|
v.add( base );
|
|
},
|
|
|
|
|
|
/**
|
|
* Given an existing particle vector, project it onto a random point
|
|
* on a disk (in the XY-plane) centered at `base` and with radius `radius`.
|
|
*
|
|
* @private
|
|
*
|
|
* @param {THREE.Vector3} v
|
|
* @param {THREE.Vector3} base
|
|
* @param {Number} radius
|
|
*/
|
|
randomizeExistingVector3OnDisk: function( v, base, radius, radiusSpread, radiusScale, radiusSpreadClamp ) {
|
|
var t = 6.2832 * Math.random(),
|
|
rand = Math.abs( this._randomFloat( radius, radiusSpread ) );
|
|
|
|
if( radiusSpreadClamp ) {
|
|
rand = Math.round( rand / radiusSpreadClamp ) * radiusSpreadClamp;
|
|
}
|
|
|
|
v.set(
|
|
Math.cos( t ),
|
|
Math.sin( t ),
|
|
0
|
|
).multiplyScalar( rand );
|
|
|
|
if ( radiusScale ) {
|
|
v.multiply( radiusScale );
|
|
}
|
|
|
|
v.add( base );
|
|
},
|
|
|
|
randomizeExistingVelocityVector3OnSphere: function( v, base, position, speed, speedSpread ) {
|
|
v.copy(position)
|
|
.sub(base)
|
|
.normalize()
|
|
.multiplyScalar( Math.abs( this._randomFloat( speed, speedSpread ) ) );
|
|
},
|
|
|
|
generateID: function() {
|
|
var str = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
|
|
|
|
str = str.replace(/[xy]/g, function(c) {
|
|
var rand = Math.random();
|
|
var r = rand*16|0%16, v = c === 'x' ? r : (r&0x3|0x8);
|
|
|
|
return v.toString(16);
|
|
});
|
|
|
|
return str;
|
|
}
|
|
};;
|
|
|
|
// ShaderParticleGroup 0.7.9
|
|
//
|
|
// (c) 2014 Luke Moody (http://www.github.com/squarefeet)
|
|
// & Lee Stemkoski (http://www.adelphi.edu/~stemkoski/)
|
|
//
|
|
// Based on Lee Stemkoski's original work:
|
|
// (https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/js/ParticleEngine.js).
|
|
//
|
|
// ShaderParticleGroup may be freely distributed under the MIT license (See LICENSE.txt)
|
|
|
|
var SPE = SPE || {};
|
|
|
|
SPE.Group = function( options ) {
|
|
var that = this;
|
|
|
|
that.fixedTimeStep = parseFloat( typeof options.fixedTimeStep === 'number' ? options.fixedTimeStep : 0.016 );
|
|
|
|
// Uniform properties ( applied to all particles )
|
|
that.maxAge = parseFloat( options.maxAge || 3 );
|
|
that.texture = options.texture || null;
|
|
that.hasPerspective = parseInt( typeof options.hasPerspective === 'number' ? options.hasPerspective : 1, 10 );
|
|
that.colorize = parseInt( typeof options.colorize === 'number' ? options.colorize : 1, 10 );
|
|
|
|
// Material properties
|
|
that.blending = typeof options.blending === 'number' ? options.blending : THREE.AdditiveBlending;
|
|
that.transparent = typeof options.transparent === 'boolean' ? options.transparent : true;
|
|
that.alphaTest = typeof options.alphaTest === 'number' ? options.alphaTest : 0.5;
|
|
that.depthWrite = typeof options.depthWrite === 'boolean' ? options.depthWrite : false;
|
|
that.depthTest = typeof options.depthTest === 'boolean' ? options.depthTest : true;
|
|
|
|
// Create uniforms
|
|
that.uniforms = {
|
|
duration: { type: 'f', value: that.maxAge },
|
|
texture: { type: 't', value: that.texture },
|
|
hasPerspective: { type: 'i', value: that.hasPerspective },
|
|
colorize: { type: 'i', value: that.colorize }
|
|
};
|
|
|
|
// Create a map of attributes that will hold values for each particle in this group.
|
|
that.attributes = {
|
|
acceleration: { type: 'v3', value: [] },
|
|
velocity: { type: 'v3', value: [] },
|
|
|
|
alive: { type: 'f', value: [] },
|
|
age: { type: 'f', value: [] },
|
|
|
|
size: { type: 'v3', value: [] },
|
|
angle: { type: 'v4', value: [] },
|
|
|
|
colorStart: { type: 'c', value: [] },
|
|
colorMiddle: { type: 'c', value: [] },
|
|
colorEnd: { type: 'c', value: [] },
|
|
|
|
opacity: { type: 'v3', value: [] }
|
|
};
|
|
|
|
// Emitters (that aren't static) will be added to this array for
|
|
// processing during the `tick()` function.
|
|
that.emitters = [];
|
|
|
|
// Create properties for use by the emitter pooling functions.
|
|
that._pool = [];
|
|
that._poolCreationSettings = null;
|
|
that._createNewWhenPoolEmpty = 0;
|
|
that.maxAgeMilliseconds = that.maxAge * 1000;
|
|
|
|
// Create an empty geometry to hold the particles.
|
|
// Each particle is a vertex pushed into this geometry's
|
|
// vertices array.
|
|
that.geometry = new THREE.Geometry();
|
|
|
|
// Create the shader material using the properties we set above.
|
|
that.material = new THREE.ShaderMaterial({
|
|
uniforms: that.uniforms,
|
|
attributes: that.attributes,
|
|
vertexShader: SPE.shaders.vertex,
|
|
fragmentShader: SPE.shaders.fragment,
|
|
blending: that.blending,
|
|
transparent: that.transparent,
|
|
alphaTest: that.alphaTest,
|
|
depthWrite: that.depthWrite,
|
|
depthTest: that.depthTest
|
|
});
|
|
|
|
// And finally create the ParticleSystem. It's got its `dynamic` property
|
|
// set so that THREE.js knows to update it on each frame.
|
|
that.mesh = new THREE.PointCloud( that.geometry, that.material );
|
|
that.mesh.dynamic = true;
|
|
};
|
|
|
|
SPE.Group.prototype = {
|
|
|
|
/**
|
|
* Tells the age and alive attributes (and the geometry vertices)
|
|
* that they need updating by THREE.js's internal tick functions.
|
|
*
|
|
* @private
|
|
*
|
|
* @return {this}
|
|
*/
|
|
_flagUpdate: function() {
|
|
var that = this;
|
|
|
|
// Set flags to update (causes less garbage than
|
|
// ```ParticleSystem.sortParticles = true``` in THREE.r58 at least)
|
|
that.attributes.age.needsUpdate = true;
|
|
that.attributes.alive.needsUpdate = true;
|
|
that.attributes.angle.needsUpdate = true;
|
|
// that.attributes.angleAlignVelocity.needsUpdate = true;
|
|
that.attributes.velocity.needsUpdate = true;
|
|
that.attributes.acceleration.needsUpdate = true;
|
|
that.geometry.verticesNeedUpdate = true;
|
|
|
|
return that;
|
|
},
|
|
|
|
/**
|
|
* Add an emitter to this particle group. Once added, an emitter will be automatically
|
|
* updated when SPE.Group#tick() is called.
|
|
*
|
|
* @param {SPE.Emitter} emitter
|
|
* @return {this}
|
|
*/
|
|
addEmitter: function( emitter ) {
|
|
var that = this;
|
|
|
|
if( emitter.duration ) {
|
|
emitter.particlesPerSecond = emitter.particleCount / (that.maxAge < emitter.duration ? that.maxAge : emitter.duration) | 0;
|
|
}
|
|
else {
|
|
emitter.particlesPerSecond = emitter.particleCount / that.maxAge | 0
|
|
}
|
|
|
|
var vertices = that.geometry.vertices,
|
|
start = vertices.length,
|
|
end = emitter.particleCount + start,
|
|
a = that.attributes,
|
|
acceleration = a.acceleration.value,
|
|
velocity = a.velocity.value,
|
|
alive = a.alive.value,
|
|
age = a.age.value,
|
|
size = a.size.value,
|
|
angle = a.angle.value,
|
|
colorStart = a.colorStart.value,
|
|
colorMiddle = a.colorMiddle.value,
|
|
colorEnd = a.colorEnd.value,
|
|
opacity = a.opacity.value;
|
|
|
|
emitter.particleIndex = parseFloat( start );
|
|
|
|
// Create the values
|
|
for( var i = start; i < end; ++i ) {
|
|
|
|
if( emitter.type === 'sphere' ) {
|
|
vertices[i] = that._randomVector3OnSphere( emitter.position, emitter.radius, emitter.radiusSpread, emitter.radiusScale, emitter.radiusSpreadClamp );
|
|
velocity[i] = that._randomVelocityVector3OnSphere( vertices[i], emitter.position, emitter.speed, emitter.speedSpread );
|
|
}
|
|
else if( emitter.type === 'disk' ) {
|
|
vertices[i] = that._randomVector3OnDisk( emitter.position, emitter.radius, emitter.radiusSpread, emitter.radiusScale, emitter.radiusSpreadClamp );
|
|
velocity[i] = that._randomVelocityVector3OnSphere( vertices[i], emitter.position, emitter.speed, emitter.speedSpread );
|
|
}
|
|
else {
|
|
vertices[i] = that._randomVector3( emitter.position, emitter.positionSpread );
|
|
velocity[i] = that._randomVector3( emitter.velocity, emitter.velocitySpread );
|
|
}
|
|
|
|
acceleration[i] = that._randomVector3( emitter.acceleration, emitter.accelerationSpread );
|
|
|
|
size[i] = new THREE.Vector3(
|
|
Math.abs( that._randomFloat( emitter.sizeStart, emitter.sizeStartSpread ) ),
|
|
Math.abs( that._randomFloat( emitter.sizeMiddle, emitter.sizeMiddleSpread ) ),
|
|
Math.abs( that._randomFloat( emitter.sizeEnd, emitter.sizeEndSpread ) )
|
|
);
|
|
|
|
angle[i] = new THREE.Vector4(
|
|
that._randomFloat( emitter.angleStart, emitter.angleStartSpread ),
|
|
that._randomFloat( emitter.angleMiddle, emitter.angleMiddleSpread ),
|
|
that._randomFloat( emitter.angleEnd, emitter.angleEndSpread ),
|
|
emitter.angleAlignVelocity ? 1.0 : 0.0
|
|
);
|
|
|
|
age[i] = 0.0;
|
|
alive[i] = emitter.isStatic ? 1.0 : 0.0;
|
|
|
|
colorStart[i] = that._randomColor( emitter.colorStart, emitter.colorStartSpread );
|
|
colorMiddle[i] = that._randomColor( emitter.colorMiddle, emitter.colorMiddleSpread );
|
|
colorEnd[i] = that._randomColor( emitter.colorEnd, emitter.colorEndSpread );
|
|
|
|
opacity[i] = new THREE.Vector3(
|
|
Math.abs( that._randomFloat( emitter.opacityStart, emitter.opacityStartSpread ) ),
|
|
Math.abs( that._randomFloat( emitter.opacityMiddle, emitter.opacityMiddleSpread ) ),
|
|
Math.abs( that._randomFloat( emitter.opacityEnd, emitter.opacityEndSpread ) )
|
|
);
|
|
}
|
|
|
|
// Cache properties on the emitter so we can access
|
|
// them from its tick function.
|
|
emitter.verticesIndex = parseFloat( start );
|
|
emitter.attributes = a;
|
|
emitter.vertices = that.geometry.vertices;
|
|
emitter.maxAge = that.maxAge;
|
|
|
|
// Assign a unique ID to this emitter
|
|
emitter.__id = that._generateID();
|
|
|
|
// Save this emitter in an array for processing during this.tick()
|
|
if( !emitter.isStatic ) {
|
|
that.emitters.push( emitter );
|
|
}
|
|
|
|
return that;
|
|
},
|
|
|
|
|
|
removeEmitter: function( emitter ) {
|
|
var id,
|
|
emitters = this.emitters;
|
|
|
|
if( emitter instanceof SPE.Emitter ) {
|
|
id = emitter.__id;
|
|
}
|
|
else if( typeof emitter === 'string' ) {
|
|
id = emitter;
|
|
}
|
|
else {
|
|
console.warn('Invalid emitter or emitter ID passed to SPE.Group#removeEmitter.' );
|
|
return;
|
|
}
|
|
|
|
for( var i = 0, il = emitters.length; i < il; ++i ) {
|
|
if( emitters[i].__id === id ) {
|
|
emitters.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* The main particle group update function. Call this once per frame.
|
|
*
|
|
* @param {Number} dt
|
|
* @return {this}
|
|
*/
|
|
tick: function( dt ) {
|
|
var that = this,
|
|
emitters = that.emitters,
|
|
numEmitters = emitters.length;
|
|
|
|
dt = dt || that.fixedTimeStep;
|
|
|
|
if( numEmitters === 0 ) {
|
|
return;
|
|
}
|
|
|
|
for( var i = 0; i < numEmitters; ++i ) {
|
|
emitters[i].tick( dt );
|
|
}
|
|
|
|
that._flagUpdate();
|
|
return that;
|
|
},
|
|
|
|
|
|
/**
|
|
* Fetch a single emitter instance from the pool.
|
|
* If there are no objects in the pool, a new emitter will be
|
|
* created if specified.
|
|
*
|
|
* @return {ShaderParticleEmitter | null}
|
|
*/
|
|
getFromPool: function() {
|
|
var that = this,
|
|
pool = that._pool,
|
|
createNew = that._createNewWhenPoolEmpty;
|
|
|
|
if( pool.length ) {
|
|
return pool.pop();
|
|
}
|
|
else if( createNew ) {
|
|
return new SPE.Emitter( that._poolCreationSettings );
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
|
|
/**
|
|
* Release an emitter into the pool.
|
|
*
|
|
* @param {ShaderParticleEmitter} emitter
|
|
* @return {this}
|
|
*/
|
|
releaseIntoPool: function( emitter ) {
|
|
if( !(emitter instanceof SPE.Emitter) ) {
|
|
console.error( 'Will not add non-emitter to particle group pool:', emitter );
|
|
return;
|
|
}
|
|
|
|
emitter.reset();
|
|
this._pool.unshift( emitter );
|
|
|
|
return this;
|
|
},
|
|
|
|
|
|
/**
|
|
* Get the pool array
|
|
*
|
|
* @return {Array}
|
|
*/
|
|
getPool: function() {
|
|
return this._pool;
|
|
},
|
|
|
|
|
|
/**
|
|
* Add a pool of emitters to this particle group
|
|
*
|
|
* @param {Number} numEmitters The number of emitters to add to the pool.
|
|
* @param {Object} emitterSettings An object describing the settings to pass to each emitter.
|
|
* @param {Boolean} createNew Should a new emitter be created if the pool runs out?
|
|
* @return {this}
|
|
*/
|
|
addPool: function( numEmitters, emitterSettings, createNew ) {
|
|
var that = this,
|
|
emitter;
|
|
|
|
// Save relevant settings and flags.
|
|
that._poolCreationSettings = emitterSettings;
|
|
that._createNewWhenPoolEmpty = !!createNew;
|
|
|
|
// Create the emitters, add them to this group and the pool.
|
|
for( var i = 0; i < numEmitters; ++i ) {
|
|
emitter = new SPE.Emitter( emitterSettings );
|
|
that.addEmitter( emitter );
|
|
that.releaseIntoPool( emitter );
|
|
}
|
|
|
|
return that;
|
|
},
|
|
|
|
|
|
/**
|
|
* Internal method. Sets a single emitter to be alive
|
|
*
|
|
* @private
|
|
*
|
|
* @param {THREE.Vector3} pos
|
|
* @return {this}
|
|
*/
|
|
_triggerSingleEmitter: function( pos ) {
|
|
var that = this,
|
|
emitter = that.getFromPool();
|
|
|
|
if( emitter === null ) {
|
|
console.log('SPE.Group pool ran out.');
|
|
return;
|
|
}
|
|
|
|
// TODO: Should an instanceof check happen here? Or maybe at least a typeof?
|
|
if( pos ) {
|
|
emitter.position.copy( pos );
|
|
}
|
|
|
|
emitter.enable();
|
|
|
|
setTimeout( function() {
|
|
emitter.disable();
|
|
that.releaseIntoPool( emitter );
|
|
}, that.maxAgeMilliseconds );
|
|
|
|
return that;
|
|
},
|
|
|
|
|
|
/**
|
|
* Set a given number of emitters as alive, with an optional position
|
|
* vector3 to move them to.
|
|
*
|
|
* @param {Number} numEmitters
|
|
* @param {THREE.Vector3} position
|
|
* @return {this}
|
|
*/
|
|
triggerPoolEmitter: function( numEmitters, position ) {
|
|
var that = this;
|
|
|
|
if( typeof numEmitters === 'number' && numEmitters > 1) {
|
|
for( var i = 0; i < numEmitters; ++i ) {
|
|
that._triggerSingleEmitter( position );
|
|
}
|
|
}
|
|
else {
|
|
that._triggerSingleEmitter( position );
|
|
}
|
|
|
|
return that;
|
|
}
|
|
};
|
|
|
|
|
|
// Extend ShaderParticleGroup's prototype with functions from utils object.
|
|
for( var i in SPE.utils ) {
|
|
SPE.Group.prototype[ '_' + i ] = SPE.utils[i];
|
|
}
|
|
|
|
|
|
// The all-important shaders
|
|
SPE.shaders = {
|
|
vertex: [
|
|
'uniform float duration;',
|
|
'uniform int hasPerspective;',
|
|
|
|
'attribute vec3 colorStart;',
|
|
'attribute vec3 colorMiddle;',
|
|
'attribute vec3 colorEnd;',
|
|
'attribute vec3 opacity;',
|
|
|
|
'attribute vec3 acceleration;',
|
|
'attribute vec3 velocity;',
|
|
'attribute float alive;',
|
|
'attribute float age;',
|
|
|
|
'attribute vec3 size;',
|
|
'attribute vec4 angle;',
|
|
|
|
// values to be passed to the fragment shader
|
|
'varying vec4 vColor;',
|
|
'varying float vAngle;',
|
|
|
|
|
|
// Integrate acceleration into velocity and apply it to the particle's position
|
|
'vec4 GetPos() {',
|
|
'vec3 newPos = vec3( position );',
|
|
|
|
// Move acceleration & velocity vectors to the value they
|
|
// should be at the current age
|
|
'vec3 a = acceleration * age;',
|
|
'vec3 v = velocity * age;',
|
|
|
|
// Move velocity vector to correct values at this age
|
|
'v = v + (a * age);',
|
|
|
|
// Add velocity vector to the newPos vector
|
|
'newPos = newPos + v;',
|
|
|
|
// Convert the newPos vector into world-space
|
|
'vec4 mvPosition = modelViewMatrix * vec4( newPos, 1.0 );',
|
|
|
|
'return mvPosition;',
|
|
'}',
|
|
|
|
|
|
'void main() {',
|
|
|
|
'float positionInTime = (age / duration);',
|
|
|
|
'float lerpAmount1 = (age / (0.5 * duration));', // percentage during first half
|
|
'float lerpAmount2 = ((age - 0.5 * duration) / (0.5 * duration));', // percentage during second half
|
|
'float halfDuration = duration / 2.0;',
|
|
'float pointSize = 0.0;',
|
|
|
|
'vAngle = 0.0;',
|
|
|
|
'if( alive > 0.5 ) {',
|
|
|
|
// lerp the color and opacity
|
|
'if( positionInTime < 0.5 ) {',
|
|
'vColor = vec4( mix(colorStart, colorMiddle, lerpAmount1), mix(opacity.x, opacity.y, lerpAmount1) );',
|
|
'}',
|
|
'else {',
|
|
'vColor = vec4( mix(colorMiddle, colorEnd, lerpAmount2), mix(opacity.y, opacity.z, lerpAmount2) );',
|
|
'}',
|
|
|
|
|
|
// Get the position of this particle so we can use it
|
|
// when we calculate any perspective that might be required.
|
|
'vec4 pos = GetPos();',
|
|
|
|
|
|
// Determine the angle we should use for this particle.
|
|
'if( angle[3] == 1.0 ) {',
|
|
'vAngle = -atan(pos.y, pos.x);',
|
|
'}',
|
|
'else if( positionInTime < 0.5 ) {',
|
|
'vAngle = mix( angle.x, angle.y, lerpAmount1 );',
|
|
'}',
|
|
'else {',
|
|
'vAngle = mix( angle.y, angle.z, lerpAmount2 );',
|
|
'}',
|
|
|
|
// Determine point size.
|
|
'if( positionInTime < 0.5) {',
|
|
'pointSize = mix( size.x, size.y, lerpAmount1 );',
|
|
'}',
|
|
'else {',
|
|
'pointSize = mix( size.y, size.z, lerpAmount2 );',
|
|
'}',
|
|
|
|
|
|
'if( hasPerspective == 1 ) {',
|
|
'pointSize = pointSize * ( 300.0 / length( pos.xyz ) );',
|
|
'}',
|
|
|
|
// Set particle size and position
|
|
'gl_PointSize = pointSize;',
|
|
'gl_Position = projectionMatrix * pos;',
|
|
'}',
|
|
|
|
'else {',
|
|
// Hide particle and set its position to the (maybe) glsl
|
|
// equivalent of Number.POSITIVE_INFINITY
|
|
'vColor = vec4( 0.0, 0.0, 0.0, 0.0 );',
|
|
'gl_Position = vec4(1000000000.0, 1000000000.0, 1000000000.0, 0.0);',
|
|
'}',
|
|
'}',
|
|
].join('\n'),
|
|
|
|
fragment: [
|
|
'uniform sampler2D texture;',
|
|
'uniform int colorize;',
|
|
|
|
'varying vec4 vColor;',
|
|
'varying float vAngle;',
|
|
|
|
'void main() {',
|
|
'float c = cos(vAngle);',
|
|
'float s = sin(vAngle);',
|
|
|
|
'vec2 rotatedUV = vec2(c * (gl_PointCoord.x - 0.5) + s * (gl_PointCoord.y - 0.5) + 0.5,',
|
|
'c * (gl_PointCoord.y - 0.5) - s * (gl_PointCoord.x - 0.5) + 0.5);',
|
|
|
|
'vec4 rotatedTexture = texture2D( texture, rotatedUV );',
|
|
|
|
'if( colorize == 1 ) {',
|
|
'gl_FragColor = vColor * rotatedTexture;',
|
|
'}',
|
|
'else {',
|
|
'gl_FragColor = rotatedTexture;',
|
|
'}',
|
|
'}'
|
|
].join('\n')
|
|
};
|
|
;
|
|
|
|
// ShaderParticleEmitter 0.7.9
|
|
//
|
|
// (c) 2014 Luke Moody (http://www.github.com/squarefeet)
|
|
// & Lee Stemkoski (http://www.adelphi.edu/~stemkoski/)
|
|
//
|
|
// Based on Lee Stemkoski's original work:
|
|
// (https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/js/ParticleEngine.js).
|
|
//
|
|
// ShaderParticleEmitter may be freely distributed under the MIT license (See LICENSE.txt)
|
|
|
|
var SPE = SPE || {};
|
|
|
|
SPE.Emitter = function( options ) {
|
|
// If no options are provided, fallback to an empty object.
|
|
options = options || {};
|
|
|
|
// Helps with minification. Not as easy to read the following code,
|
|
// but should still be readable enough!
|
|
var that = this;
|
|
|
|
|
|
that.particleCount = typeof options.particleCount === 'number' ? options.particleCount : 100;
|
|
that.type = (options.type === 'cube' || options.type === 'sphere' || options.type === 'disk') ? options.type : 'cube';
|
|
|
|
that.position = options.position instanceof THREE.Vector3 ? options.position : new THREE.Vector3();
|
|
that.positionSpread = options.positionSpread instanceof THREE.Vector3 ? options.positionSpread : new THREE.Vector3();
|
|
|
|
// These two properties are only used when this.type === 'sphere' or 'disk'
|
|
that.radius = typeof options.radius === 'number' ? options.radius : 10;
|
|
that.radiusSpread = typeof options.radiusSpread === 'number' ? options.radiusSpread : 0;
|
|
that.radiusScale = options.radiusScale instanceof THREE.Vector3 ? options.radiusScale : new THREE.Vector3(1, 1, 1);
|
|
that.radiusSpreadClamp = typeof options.radiusSpreadClamp === 'number' ? options.radiusSpreadClamp : 0;
|
|
|
|
that.acceleration = options.acceleration instanceof THREE.Vector3 ? options.acceleration : new THREE.Vector3();
|
|
that.accelerationSpread = options.accelerationSpread instanceof THREE.Vector3 ? options.accelerationSpread : new THREE.Vector3();
|
|
|
|
that.velocity = options.velocity instanceof THREE.Vector3 ? options.velocity : new THREE.Vector3();
|
|
that.velocitySpread = options.velocitySpread instanceof THREE.Vector3 ? options.velocitySpread : new THREE.Vector3();
|
|
|
|
|
|
// And again here; only used when this.type === 'sphere' or 'disk'
|
|
that.speed = parseFloat( typeof options.speed === 'number' ? options.speed : 0.0 );
|
|
that.speedSpread = parseFloat( typeof options.speedSpread === 'number' ? options.speedSpread : 0.0 );
|
|
|
|
|
|
// Sizes
|
|
that.sizeStart = parseFloat( typeof options.sizeStart === 'number' ? options.sizeStart : 1.0 );
|
|
that.sizeStartSpread = parseFloat( typeof options.sizeStartSpread === 'number' ? options.sizeStartSpread : 0.0 );
|
|
|
|
that.sizeEnd = parseFloat( typeof options.sizeEnd === 'number' ? options.sizeEnd : that.sizeStart );
|
|
that.sizeEndSpread = parseFloat( typeof options.sizeEndSpread === 'number' ? options.sizeEndSpread : 0.0 );
|
|
|
|
that.sizeMiddle = parseFloat(
|
|
typeof options.sizeMiddle !== 'undefined' ?
|
|
options.sizeMiddle :
|
|
Math.abs(that.sizeEnd + that.sizeStart) / 2
|
|
);
|
|
that.sizeMiddleSpread = parseFloat( typeof options.sizeMiddleSpread === 'number' ? options.sizeMiddleSpread : 0 );
|
|
|
|
|
|
// Angles
|
|
that.angleStart = parseFloat( typeof options.angleStart === 'number' ? options.angleStart : 0 );
|
|
that.angleStartSpread = parseFloat( typeof options.angleStartSpread === 'number' ? options.angleStartSpread : 0 );
|
|
|
|
that.angleEnd = parseFloat( typeof options.angleEnd === 'number' ? options.angleEnd : 0 );
|
|
that.angleEndSpread = parseFloat( typeof options.angleEndSpread === 'number' ? options.angleEndSpread : 0 );
|
|
|
|
that.angleMiddle = parseFloat(
|
|
typeof options.angleMiddle !== 'undefined' ?
|
|
options.angleMiddle :
|
|
Math.abs(that.angleEnd + that.angleStart) / 2
|
|
);
|
|
that.angleMiddleSpread = parseFloat( typeof options.angleMiddleSpread === 'number' ? options.angleMiddleSpread : 0 );
|
|
|
|
that.angleAlignVelocity = options.angleAlignVelocity || false;
|
|
|
|
|
|
// Colors
|
|
that.colorStart = options.colorStart instanceof THREE.Color ? options.colorStart : new THREE.Color( 'white' );
|
|
that.colorStartSpread = options.colorStartSpread instanceof THREE.Vector3 ? options.colorStartSpread : new THREE.Vector3();
|
|
|
|
that.colorEnd = options.colorEnd instanceof THREE.Color ? options.colorEnd : that.colorStart.clone();
|
|
that.colorEndSpread = options.colorEndSpread instanceof THREE.Vector3 ? options.colorEndSpread : new THREE.Vector3();
|
|
|
|
that.colorMiddle =
|
|
options.colorMiddle instanceof THREE.Color ?
|
|
options.colorMiddle :
|
|
new THREE.Color().addColors( that.colorStart, that.colorEnd ).multiplyScalar( 0.5 );
|
|
that.colorMiddleSpread = options.colorMiddleSpread instanceof THREE.Vector3 ? options.colorMiddleSpread : new THREE.Vector3();
|
|
|
|
|
|
|
|
// Opacities
|
|
that.opacityStart = parseFloat( typeof options.opacityStart !== 'undefined' ? options.opacityStart : 1 );
|
|
that.opacityStartSpread = parseFloat( typeof options.opacityStartSpread !== 'undefined' ? options.opacityStartSpread : 0 );
|
|
|
|
that.opacityEnd = parseFloat( typeof options.opacityEnd === 'number' ? options.opacityEnd : 0 );
|
|
that.opacityEndSpread = parseFloat( typeof options.opacityEndSpread !== 'undefined' ? options.opacityEndSpread : 0 );
|
|
|
|
that.opacityMiddle = parseFloat(
|
|
typeof options.opacityMiddle !== 'undefined' ?
|
|
options.opacityMiddle :
|
|
Math.abs(that.opacityEnd + that.opacityStart) / 2
|
|
);
|
|
that.opacityMiddleSpread = parseFloat( typeof options.opacityMiddleSpread === 'number' ? options.opacityMiddleSpread : 0 );
|
|
|
|
|
|
// Generic
|
|
that.duration = typeof options.duration === 'number' ? options.duration : null;
|
|
that.alive = parseFloat( typeof options.alive === 'number' ? options.alive : 1.0 );
|
|
that.isStatic = typeof options.isStatic === 'number' ? options.isStatic : 0;
|
|
|
|
// Particle spawn callback function.
|
|
that.onParticleSpawn = typeof options.onParticleSpawn === 'function' ? options.onParticleSpawn : null;
|
|
|
|
|
|
// The following properties are used internally, and mostly set when this emitter
|
|
// is added to a particle group.
|
|
that.particlesPerSecond = 0;
|
|
that.attributes = null;
|
|
that.vertices = null;
|
|
that.verticesIndex = 0;
|
|
that.age = 0.0;
|
|
that.maxAge = 0.0;
|
|
|
|
that.particleIndex = 0.0;
|
|
|
|
that.__id = null;
|
|
|
|
that.userData = {};
|
|
};
|
|
|
|
SPE.Emitter.prototype = {
|
|
|
|
/**
|
|
* Reset a particle's position. Accounts for emitter type and spreads.
|
|
*
|
|
* @private
|
|
*
|
|
* @param {THREE.Vector3} p
|
|
*/
|
|
_resetParticle: function( i ) {
|
|
var that = this,
|
|
type = that.type,
|
|
spread = that.positionSpread,
|
|
particlePosition = that.vertices[i],
|
|
a = that.attributes,
|
|
particleVelocity = a.velocity.value[i],
|
|
|
|
vSpread = that.velocitySpread,
|
|
aSpread = that.accelerationSpread;
|
|
|
|
// Optimise for no position spread or radius
|
|
if(
|
|
( type === 'cube' && spread.x === 0 && spread.y === 0 && spread.z === 0 ) ||
|
|
( type === 'sphere' && that.radius === 0 ) ||
|
|
( type === 'disk' && that.radius === 0 )
|
|
) {
|
|
particlePosition.copy( that.position );
|
|
that._randomizeExistingVector3( particleVelocity, that.velocity, vSpread );
|
|
|
|
if( type === 'cube' ) {
|
|
that._randomizeExistingVector3( that.attributes.acceleration.value[i], that.acceleration, aSpread );
|
|
}
|
|
}
|
|
|
|
// If there is a position spread, then get a new position based on this spread.
|
|
else if( type === 'cube' ) {
|
|
that._randomizeExistingVector3( particlePosition, that.position, spread );
|
|
that._randomizeExistingVector3( particleVelocity, that.velocity, vSpread );
|
|
that._randomizeExistingVector3( that.attributes.acceleration.value[i], that.acceleration, aSpread );
|
|
}
|
|
|
|
else if( type === 'sphere') {
|
|
that._randomizeExistingVector3OnSphere( particlePosition, that.position, that.radius, that.radiusSpread, that.radiusScale, that.radiusSpreadClamp );
|
|
that._randomizeExistingVelocityVector3OnSphere( particleVelocity, that.position, particlePosition, that.speed, that.speedSpread );
|
|
}
|
|
|
|
else if( type === 'disk') {
|
|
that._randomizeExistingVector3OnDisk( particlePosition, that.position, that.radius, that.radiusSpread, that.radiusScale, that.radiusSpreadClamp );
|
|
that._randomizeExistingVelocityVector3OnSphere( particleVelocity, that.position, particlePosition, that.speed, that.speedSpread );
|
|
}
|
|
|
|
if( typeof that.onParticleSpawn === 'function' ) {
|
|
that.onParticleSpawn( a, i );
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update this emitter's particle's positions. Called by the SPE.Group
|
|
* that this emitter belongs to.
|
|
*
|
|
* @param {Number} dt
|
|
*/
|
|
tick: function( dt ) {
|
|
|
|
if( this.isStatic ) {
|
|
return;
|
|
}
|
|
|
|
// Cache some values for quicker access in loops.
|
|
var that = this,
|
|
a = that.attributes,
|
|
alive = a.alive.value,
|
|
age = a.age.value,
|
|
start = that.verticesIndex,
|
|
particleCount = that.particleCount,
|
|
end = start + particleCount,
|
|
pps = that.particlesPerSecond * that.alive,
|
|
ppsdt = pps * dt,
|
|
m = that.maxAge,
|
|
emitterAge = that.age,
|
|
duration = that.duration,
|
|
pIndex = that.particleIndex;
|
|
|
|
// Loop through all the particles in this emitter and
|
|
// determine whether they're still alive and need advancing
|
|
// or if they should be dead and therefore marked as such.
|
|
for( var i = start; i < end; ++i ) {
|
|
if( alive[ i ] === 1.0 ) {
|
|
age[ i ] += dt;
|
|
}
|
|
|
|
if( age[ i ] >= m ) {
|
|
age[ i ] = 0.0;
|
|
alive[ i ] = 0.0;
|
|
}
|
|
}
|
|
|
|
// If the emitter is dead, reset any particles that are in
|
|
// the recycled vertices array and reset the age of the
|
|
// emitter to zero ready to go again if required, then
|
|
// exit this function.
|
|
if( that.alive === 0.0 ) {
|
|
that.age = 0.0;
|
|
return;
|
|
}
|
|
|
|
// If the emitter has a specified lifetime and we've exceeded it,
|
|
// mark the emitter as dead and exit this function.
|
|
if( typeof duration === 'number' && emitterAge > duration ) {
|
|
that.alive = 0.0;
|
|
that.age = 0.0;
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
var n = Math.max( Math.min( end, pIndex + ppsdt ), 0),
|
|
count = 0,
|
|
index = 0,
|
|
pIndexFloor = pIndex | 0,
|
|
dtInc;
|
|
|
|
for( i = pIndexFloor; i < n; ++i ) {
|
|
if( alive[ i ] !== 1.0 ) {
|
|
++count;
|
|
}
|
|
}
|
|
|
|
if( count !== 0 ) {
|
|
dtInc = dt / count;
|
|
|
|
for( i = pIndexFloor; i < n; ++i, ++index ) {
|
|
if( alive[ i ] !== 1.0 ) {
|
|
alive[ i ] = 1.0;
|
|
age[ i ] = dtInc * index;
|
|
that._resetParticle( i );
|
|
}
|
|
}
|
|
}
|
|
|
|
that.particleIndex += ppsdt;
|
|
|
|
if( that.particleIndex < 0.0 ) {
|
|
that.particleIndex = 0.0;
|
|
}
|
|
|
|
if( pIndex >= start + particleCount ) {
|
|
that.particleIndex = parseFloat( start );
|
|
}
|
|
|
|
// Add the delta time value to the age of the emitter.
|
|
that.age += dt;
|
|
|
|
if( that.age < 0.0 ) {
|
|
that.age = 0.0;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Reset this emitter back to its starting position.
|
|
* If `force` is truthy, then reset all particles in this
|
|
* emitter as well, even if they're currently alive.
|
|
*
|
|
* @param {Boolean} force
|
|
* @return {this}
|
|
*/
|
|
reset: function( force ) {
|
|
var that = this;
|
|
|
|
that.age = 0.0;
|
|
that.alive = 0;
|
|
|
|
if( force ) {
|
|
var start = that.verticesIndex,
|
|
end = that.verticesIndex + that.particleCount,
|
|
a = that.attributes,
|
|
alive = a.alive.value,
|
|
age = a.age.value;
|
|
|
|
for( var i = start; i < end; ++i ) {
|
|
alive[ i ] = 0.0;
|
|
age[ i ] = 0.0;
|
|
}
|
|
}
|
|
|
|
return that;
|
|
},
|
|
|
|
|
|
/**
|
|
* Enable this emitter.
|
|
*/
|
|
enable: function() {
|
|
this.alive = 1;
|
|
},
|
|
|
|
/**
|
|
* Disable this emitter.
|
|
*/
|
|
disable: function() {
|
|
this.alive = 0;
|
|
}
|
|
};
|
|
|
|
// Extend SPE.Emitter's prototype with functions from utils object.
|
|
for( var i in SPE.utils ) {
|
|
SPE.Emitter.prototype[ '_' + i ] = SPE.utils[i];
|
|
}
|