Source: core/SPE.Group.js

/**
 * An SPE.Group instance.
 * @typedef {Object} Group
 * @see SPE.Group
 */

/**
 * A map of options to configure an SPE.Group instance.
 * @typedef {Object} GroupOptions
 *
 * @property {Object} texture An object describing the texture used by the group.
 *
 * @property {Object} texture.value An instance of THREE.Texture.
 *
 * @property {Object=} texture.frames A THREE.Vector2 instance describing the number
 *                                    of frames on the x- and y-axis of the given texture.
 *                                    If not provided, the texture will NOT be treated as
 *                                    a sprite-sheet and as such will NOT be animated.
 *
 * @property {Number} [texture.frameCount=texture.frames.x * texture.frames.y] The total number of frames in the sprite-sheet.
 *                                                                   Allows for sprite-sheets that don't fill the entire
 *                                                                   texture.
 *
 * @property {Number} texture.loop The number of loops through the sprite-sheet that should
 *                                 be performed over the course of a single particle's lifetime.
 *
 * @property {Number} fixedTimeStep If no `dt` (or `deltaTime`) value is passed to this group's
 *                                  `tick()` function, this number will be used to move the particle
 *                                  simulation forward. Value in SECONDS.
 *
 * @property {Boolean} hasPerspective Whether the distance a particle is from the camera should affect
 *                                    the particle's size.
 *
 * @property {Boolean} colorize Whether the particles in this group should be rendered with color, or
 *                              whether the only color of particles will come from the provided texture.
 *
 * @property {Number} blending One of Three.js's blending modes to apply to this group's `ShaderMaterial`.
 *
 * @property {Boolean} transparent Whether these particle's should be rendered with transparency.
 *
 * @property {Number} alphaTest Sets the alpha value to be used when running an alpha test on the `texture.value` property. Value between 0 and 1.
 *
 * @property {Boolean} depthWrite Whether rendering the group has any effect on the depth buffer.
 *
 * @property {Boolean} depthTest Whether to have depth test enabled when rendering this group.
 *
 * @property {Boolean} fog Whether this group's particles should be affected by their scene's fog.
 *
 * @property {Number} scale The scale factor to apply to this group's particle sizes. Useful for
 *                          setting particle sizes to be relative to renderer size.
 */


/**
 * The SPE.Group class. Creates a new group, containing a material, geometry, and mesh.
 *
 * @constructor
 * @param {GroupOptions} options A map of options to configure the group instance.
 */
SPE.Group = function( options ) {
    'use strict';

    var utils = SPE.utils,
        types = utils.types;

    // Ensure we have a map of options to play with
    options = utils.ensureTypedArg( options, types.OBJECT, {} );
    options.texture = utils.ensureTypedArg( options.texture, types.OBJECT, {} );

    // Assign a UUID to this instance
    this.uuid = THREE.Math.generateUUID();

    // If no `deltaTime` value is passed to the `SPE.Group.tick` function,
    // the value of this property will be used to advance the simulation.
    this.fixedTimeStep = utils.ensureTypedArg( options.fixedTimeStep, types.NUMBER, 0.016 );

    // Set properties used in the uniforms map, starting with the
    // texture stuff.
    this.texture = utils.ensureInstanceOf( options.texture.value, THREE.Texture, null );
    this.textureFrames = utils.ensureInstanceOf( options.texture.frames, THREE.Vector2, new THREE.Vector2( 1, 1 ) );
    this.textureFrameCount = utils.ensureTypedArg( options.texture.frameCount, types.NUMBER, this.textureFrames.x * this.textureFrames.y );
    this.textureLoop = utils.ensureTypedArg( options.texture.loop, types.NUMBER, 1 );
    this.textureFrames.max( new THREE.Vector2( 1, 1 ) );

    this.hasPerspective = utils.ensureTypedArg( options.hasPerspective, types.BOOLEAN, true );
    this.colorize = utils.ensureTypedArg( options.colorize, types.BOOLEAN, true );

    this.maxParticleCount = utils.ensureTypedArg( options.maxParticleCount, types.NUMBER, null );


    // Set properties used to define the ShaderMaterial's appearance.
    this.blending = utils.ensureTypedArg( options.blending, types.NUMBER, THREE.AdditiveBlending );
    this.transparent = utils.ensureTypedArg( options.transparent, types.BOOLEAN, true );
    this.alphaTest = parseFloat( utils.ensureTypedArg( options.alphaTest, types.NUMBER, 0.0 ) );
    this.depthWrite = utils.ensureTypedArg( options.depthWrite, types.BOOLEAN, false );
    this.depthTest = utils.ensureTypedArg( options.depthTest, types.BOOLEAN, true );
    this.fog = utils.ensureTypedArg( options.fog, types.BOOLEAN, true );
    this.scale = utils.ensureTypedArg( options.scale, types.NUMBER, 300 );

    // Where emitter's go to curl up in a warm blanket and live
    // out their days.
    this.emitters = [];
    this.emitterIDs = [];

    // Create properties for use by the emitter pooling functions.
    this._pool = [];
    this._poolCreationSettings = null;
    this._createNewWhenPoolEmpty = 0;

    // Whether all attributes should be forced to updated
    // their entire buffer contents on the next tick.
    //
    // Used when an emitter is removed.
    this._attributesNeedRefresh = false;
    this._attributesNeedDynamicReset = false;

    this.particleCount = 0;


    // Map of uniforms to be applied to the ShaderMaterial instance.
    this.uniforms = {
        texture: {
            type: 't',
            value: this.texture
        },
        textureAnimation: {
            type: 'v4',
            value: new THREE.Vector4(
                this.textureFrames.x,
                this.textureFrames.y,
                this.textureFrameCount,
                Math.max( Math.abs( this.textureLoop ), 1.0 )
            )
        },
        fogColor: {
            type: 'c',
            value: null
        },
        fogNear: {
            type: 'f',
            value: 10
        },
        fogFar: {
            type: 'f',
            value: 200
        },
        fogDensity: {
            type: 'f',
            value: 0.5
        },
        deltaTime: {
            type: 'f',
            value: 0
        },
        runTime: {
            type: 'f',
            value: 0
        },
        scale: {
            type: 'f',
            value: this.scale
        }
    };

    // Add some defines into the mix...
    this.defines = {
        HAS_PERSPECTIVE: this.hasPerspective,
        COLORIZE: this.colorize,
        VALUE_OVER_LIFETIME_LENGTH: SPE.valueOverLifetimeLength,

        SHOULD_ROTATE_TEXTURE: false,
        SHOULD_ROTATE_PARTICLES: false,
        SHOULD_WIGGLE_PARTICLES: false,

        SHOULD_CALCULATE_SPRITE: this.textureFrames.x > 1 || this.textureFrames.y > 1
    };

    // Map of all attributes to be applied to the particles.
    //
    // See SPE.ShaderAttribute for a bit more info on this bit.
    this.attributes = {
        position: new SPE.ShaderAttribute( 'v3', true ),
        acceleration: new SPE.ShaderAttribute( 'v4', true ), // w component is drag
        velocity: new SPE.ShaderAttribute( 'v3', true ),
        rotation: new SPE.ShaderAttribute( 'v4', true ),
        rotationCenter: new SPE.ShaderAttribute( 'v3', true ),
        params: new SPE.ShaderAttribute( 'v4', true ), // Holds (alive, age, delay, wiggle)
        size: new SPE.ShaderAttribute( 'v4', true ),
        angle: new SPE.ShaderAttribute( 'v4', true ),
        color: new SPE.ShaderAttribute( 'v4', true ),
        opacity: new SPE.ShaderAttribute( 'v4', true )
    };

    this.attributeKeys = Object.keys( this.attributes );
    this.attributeCount = this.attributeKeys.length;

    // Create the ShaderMaterial instance that'll help render the
    // particles.
    this.material = new THREE.ShaderMaterial( {
        uniforms: this.uniforms,
        vertexShader: SPE.shaders.vertex,
        fragmentShader: SPE.shaders.fragment,
        blending: this.blending,
        transparent: this.transparent,
        alphaTest: this.alphaTest,
        depthWrite: this.depthWrite,
        depthTest: this.depthTest,
        defines: this.defines,
        fog: this.fog
    } );

    // Create the BufferGeometry and Points instances, ensuring
    // the geometry and material are given to the latter.
    this.geometry = new THREE.BufferGeometry();
    this.mesh = new THREE.Points( this.geometry, this.material );

    if ( this.maxParticleCount === null ) {
        console.warn( 'SPE.Group: No maxParticleCount specified. Adding emitters after rendering will probably cause errors.' );
    }
};

SPE.Group.constructor = SPE.Group;


SPE.Group.prototype._updateDefines = function() {
    'use strict';

    var emitters = this.emitters,
        i = emitters.length - 1,
        emitter,
        defines = this.defines;

    for ( i; i >= 0; --i ) {
        emitter = emitters[ i ];

        // Only do angle calculation if there's no spritesheet defined.
        //
        // Saves calculations being done and then overwritten in the shaders.
        if ( !defines.SHOULD_CALCULATE_SPRITE ) {
            defines.SHOULD_ROTATE_TEXTURE = defines.SHOULD_ROTATE_TEXTURE || !!Math.max(
                Math.max.apply( null, emitter.angle.value ),
                Math.max.apply( null, emitter.angle.spread )
            );
        }

        defines.SHOULD_ROTATE_PARTICLES = defines.SHOULD_ROTATE_PARTICLES || !!Math.max(
            emitter.rotation.angle,
            emitter.rotation.angleSpread
        );

        defines.SHOULD_WIGGLE_PARTICLES = defines.SHOULD_WIGGLE_PARTICLES || !!Math.max(
            emitter.wiggle.value,
            emitter.wiggle.spread
        );
    }

    this.material.needsUpdate = true;
};

SPE.Group.prototype._applyAttributesToGeometry = function() {
    'use strict';

    var attributes = this.attributes,
        geometry = this.geometry,
        geometryAttributes = geometry.attributes,
        attribute,
        geometryAttribute;

    // Loop through all the shader attributes and assign (or re-assign)
    // typed array buffers to each one.
    for ( var attr in attributes ) {
        if ( attributes.hasOwnProperty( attr ) ) {
            attribute = attributes[ attr ];
            geometryAttribute = geometryAttributes[ attr ];

            // Update the array if this attribute exists on the geometry.
            //
            // This needs to be done because the attribute's typed array might have
            // been resized and reinstantiated, and might now be looking at a
            // different ArrayBuffer, so reference needs updating.
            if ( geometryAttribute ) {
                geometryAttribute.array = attribute.typedArray.array;
            }

            // // Add the attribute to the geometry if it doesn't already exist.
            else {
                geometry.addAttribute( attr, attribute.bufferAttribute );
            }

            // Mark the attribute as needing an update the next time a frame is rendered.
            attribute.bufferAttribute.needsUpdate = true;
        }
    }

    // Mark the draw range on the geometry. This will ensure
    // only the values in the attribute buffers that are
    // associated with a particle will be used in THREE's
    // render cycle.
    this.geometry.setDrawRange( 0, this.particleCount );
};

/**
 * Adds an SPE.Emitter instance to this group, creating particle values and
 * assigning them to this group's shader attributes.
 *
 * @param {Emitter} emitter The emitter to add to this group.
 */
SPE.Group.prototype.addEmitter = function( emitter ) {
    'use strict';

    // Ensure an actual emitter instance is passed here.
    //
    // Decided not to throw here, just in case a scene's
    // rendering would be paused. Logging an error instead
    // of stopping execution if exceptions aren't caught.
    if ( emitter instanceof SPE.Emitter === false ) {
        console.error( '`emitter` argument must be instance of SPE.Emitter. Was provided with:', emitter );
        return;
    }

    // If the emitter already exists as a member of this group, then
    // stop here, we don't want to add it again.
    else if ( this.emitterIDs.indexOf( emitter.uuid ) > -1 ) {
        console.error( 'Emitter already exists in this group. Will not add again.' );
        return;
    }

    // And finally, if the emitter is a member of another group,
    // don't add it to this group.
    else if ( emitter.group !== null ) {
        console.error( 'Emitter already belongs to another group. Will not add to requested group.' );
        return;
    }

    var attributes = this.attributes,
        start = this.particleCount,
        end = start + emitter.particleCount;

    // Update this group's particle count.
    this.particleCount = end;

    // Emit a warning if the emitter being added will exceed the buffer sizes specified.
    if ( this.maxParticleCount !== null && this.particleCount > this.maxParticleCount ) {
        console.warn( 'SPE.Group: maxParticleCount exceeded. Requesting', this.particleCount, 'particles, can support only', this.maxParticleCount );
    }


    // Set the `particlesPerSecond` value (PPS) on the emitter.
    // It's used to determine how many particles to release
    // on a per-frame basis.
    emitter._calculatePPSValue( emitter.maxAge._value + emitter.maxAge._spread );
    emitter._setBufferUpdateRanges( this.attributeKeys );

    // Store the offset value in the TypedArray attributes for this emitter.
    emitter._setAttributeOffset( start );

    // Save a reference to this group on the emitter so it knows
    // where it belongs.
    emitter.group = this;

    // Store reference to the attributes on the emitter for
    // easier access during the emitter's tick function.
    emitter.attributes = this.attributes;



    // Ensure the attributes and their BufferAttributes exist, and their
    // TypedArrays are of the correct size.
    for ( var attr in attributes ) {
        if ( attributes.hasOwnProperty( attr ) ) {
            // When creating a buffer, pass through the maxParticle count
            // if one is specified.
            attributes[ attr ]._createBufferAttribute(
                this.maxParticleCount !== null ?
                this.maxParticleCount :
                this.particleCount
            );
        }
    }

    // Loop through each particle this emitter wants to have, and create the attributes values,
    // storing them in the TypedArrays that each attribute holds.
    for ( var i = start; i < end; ++i ) {
        emitter._assignPositionValue( i );
        emitter._assignForceValue( i, 'velocity' );
        emitter._assignForceValue( i, 'acceleration' );
        emitter._assignAbsLifetimeValue( i, 'opacity' );
        emitter._assignAbsLifetimeValue( i, 'size' );
        emitter._assignAngleValue( i );
        emitter._assignRotationValue( i );
        emitter._assignParamsValue( i );
        emitter._assignColorValue( i );
    }

    // Update the geometry and make sure the attributes are referencing
    // the typed arrays properly.
    this._applyAttributesToGeometry();

    // Store this emitter in this group's emitter's store.
    this.emitters.push( emitter );
    this.emitterIDs.push( emitter.uuid );

    // Update certain flags to enable shader calculations only if they're necessary.
    this._updateDefines( emitter );

    // Update the material since defines might have changed
    this.material.needsUpdate = true;
    this.geometry.needsUpdate = true;
    this._attributesNeedRefresh = true;

    // Return the group to enable chaining.
    return this;
};

/**
 * Removes an SPE.Emitter instance from this group. When called,
 * all particle's belonging to the given emitter will be instantly
 * removed from the scene.
 *
 * @param {Emitter} emitter The emitter to add to this group.
 */
SPE.Group.prototype.removeEmitter = function( emitter ) {
    'use strict';

    var emitterIndex = this.emitterIDs.indexOf( emitter.uuid );

    // Ensure an actual emitter instance is passed here.
    //
    // Decided not to throw here, just in case a scene's
    // rendering would be paused. Logging an error instead
    // of stopping execution if exceptions aren't caught.
    if ( emitter instanceof SPE.Emitter === false ) {
        console.error( '`emitter` argument must be instance of SPE.Emitter. Was provided with:', emitter );
        return;
    }

    // Issue an error if the emitter isn't a member of this group.
    else if ( emitterIndex === -1 ) {
        console.error( 'Emitter does not exist in this group. Will not remove.' );
        return;
    }

    // Kill all particles by marking them as dead
    // and their age as 0.
    var start = emitter.attributeOffset,
        end = start + emitter.particleCount,
        params = this.attributes.params.typedArray;

    // Set alive and age to zero.
    for ( var i = start; i < end; ++i ) {
        params.array[ i * 4 ] = 0.0;
        params.array[ i * 4 + 1 ] = 0.0;
    }

    // Remove the emitter from this group's "store".
    this.emitters.splice( emitterIndex, 1 );
    this.emitterIDs.splice( emitterIndex, 1 );

    // Remove this emitter's attribute values from all shader attributes.
    // The `.splice()` call here also marks each attribute's buffer
    // as needing to update it's entire contents.
    for ( var attr in this.attributes ) {
        if ( this.attributes.hasOwnProperty( attr ) ) {
            this.attributes[ attr ].splice( start, end );
        }
    }

    // Ensure this group's particle count is correct.
    this.particleCount -= emitter.particleCount;

    // Call the emitter's remove method.
    emitter._onRemove();

    // Set a flag to indicate that the attribute buffers should
    // be updated in their entirety on the next frame.
    this._attributesNeedRefresh = true;
};


/**
 * 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 {Emitter|null}
 */
SPE.Group.prototype.getFromPool = function() {
    'use strict';

    var pool = this._pool,
        createNew = this._createNewWhenPoolEmpty;

    if ( pool.length ) {
        return pool.pop();
    }
    else if ( createNew ) {
        return new SPE.Emitter( this._poolCreationSettings );
    }

    return null;
};


/**
 * Release an emitter into the pool.
 *
 * @param  {ShaderParticleEmitter} emitter
 * @return {Group} This group instance.
 */
SPE.Group.prototype.releaseIntoPool = function( emitter ) {
    'use strict';

    if ( emitter instanceof SPE.Emitter === false ) {
        console.error( 'Argument is not instanceof SPE.Emitter:', emitter );
        return;
    }

    emitter.reset();
    this._pool.unshift( emitter );

    return this;
};


/**
 * Get the pool array
 *
 * @return {Array}
 */
SPE.Group.prototype.getPool = function() {
    'use strict';
    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 {EmitterOptions|Array} emitterOptions  An object, or array of objects, describing the options to pass to each emitter.
 * @param {Boolean} createNew       Should a new emitter be created if the pool runs out?
 * @return {Group} This group instance.
 */
SPE.Group.prototype.addPool = function( numEmitters, emitterOptions, createNew ) {
    'use strict';

    var emitter;

    // Save relevant settings and flags.
    this._poolCreationSettings = emitterOptions;
    this._createNewWhenPoolEmpty = !!createNew;

    // Create the emitters, add them to this group and the pool.
    for ( var i = 0; i < numEmitters; ++i ) {
        if ( Array.isArray( emitterOptions ) ) {
            emitter = new SPE.Emitter( emitterOptions[ i ] );
        }
        else {
            emitter = new SPE.Emitter( emitterOptions );
        }
        this.addEmitter( emitter );
        this.releaseIntoPool( emitter );
    }

    return this;
};



SPE.Group.prototype._triggerSingleEmitter = function( pos ) {
    'use strict';

    var emitter = this.getFromPool(),
        self = this;

    if ( emitter === null ) {
        console.log( 'SPE.Group pool ran out.' );
        return;
    }

    // TODO:
    // - Make sure buffers are update with thus new position.
    if ( pos instanceof THREE.Vector3 ) {
        emitter.position.value.copy( pos );

        // Trigger the setter for this property to force an
        // update to the emitter's position attribute.
        emitter.position.value = emitter.position.value;
    }

    emitter.enable();

    setTimeout( function() {
        emitter.disable();
        self.releaseIntoPool( emitter );
    }, ( emitter.maxAge.value + emitter.maxAge.spread ) * 1000 );

    return this;
};


/**
 * Set a given number of emitters as alive, with an optional position
 * vector3 to move them to.
 *
 * @param  {Number} numEmitters The number of emitters to activate
 * @param  {Object} [position=undefined] A THREE.Vector3 instance describing the position to activate the emitter(s) at.
 * @return {Group} This group instance.
 */
SPE.Group.prototype.triggerPoolEmitter = function( numEmitters, position ) {
    'use strict';

    if ( typeof numEmitters === 'number' && numEmitters > 1 ) {
        for ( var i = 0; i < numEmitters; ++i ) {
            this._triggerSingleEmitter( position );
        }
    }
    else {
        this._triggerSingleEmitter( position );
    }

    return this;
};



SPE.Group.prototype._updateUniforms = function( dt ) {
    'use strict';

    this.uniforms.runTime.value += dt;
    this.uniforms.deltaTime.value = dt;
};

SPE.Group.prototype._resetBufferRanges = function() {
    'use strict';

    var keys = this.attributeKeys,
        i = this.attributeCount - 1,
        attrs = this.attributes;

    for ( i; i >= 0; --i ) {
        attrs[ keys[ i ] ].resetUpdateRange();
    }
};


SPE.Group.prototype._updateBuffers = function( emitter ) {
    'use strict';

    var keys = this.attributeKeys,
        i = this.attributeCount - 1,
        attrs = this.attributes,
        emitterRanges = emitter.bufferUpdateRanges,
        key,
        emitterAttr,
        attr;

    for ( i; i >= 0; --i ) {
        key = keys[ i ];
        emitterAttr = emitterRanges[ key ];
        attr = attrs[ key ];
        attr.setUpdateRange( emitterAttr.min, emitterAttr.max );
        attr.flagUpdate();
    }
};


/**
 * Simulate all the emitter's belonging to this group, updating
 * attribute values along the way.
 * @param  {Number} [dt=Group's `fixedTimeStep` value] The number of seconds to simulate the group's emitters for (deltaTime)
 */
SPE.Group.prototype.tick = function( dt ) {
    'use strict';

    var emitters = this.emitters,
        numEmitters = emitters.length,
        deltaTime = dt || this.fixedTimeStep,
        keys = this.attributeKeys,
        i,
        attrs = this.attributes;

    // Update uniform values.
    this._updateUniforms( deltaTime );

    // Reset buffer update ranges on the shader attributes.
    this._resetBufferRanges();


    // If nothing needs updating, then stop here.
    if (
        numEmitters === 0 &&
        this._attributesNeedRefresh === false &&
        this._attributesNeedDynamicReset === false
    ) {
        return;
    }

    // Loop through each emitter in this group and
    // simulate it, then update the shader attribute
    // buffers.
    for ( var i = 0, emitter; i < numEmitters; ++i ) {
        emitter = emitters[ i ];
        emitter.tick( deltaTime );
        this._updateBuffers( emitter );
    }

    // If the shader attributes have been refreshed,
    // then the dynamic properties of each buffer
    // attribute will need to be reset back to
    // what they should be.
    if ( this._attributesNeedDynamicReset === true ) {
        i = this.attributeCount - 1;

        for ( i; i >= 0; --i ) {
            attrs[ keys[ i ] ].resetDynamic();
        }

        this._attributesNeedDynamicReset = false;
    }

    // If this group's shader attributes need a full refresh
    // then mark each attribute's buffer attribute as
    // needing so.
    if ( this._attributesNeedRefresh === true ) {
        i = this.attributeCount - 1;

        for ( i; i >= 0; --i ) {
            attrs[ keys[ i ] ].forceUpdateAll();
        }

        this._attributesNeedRefresh = false;
        this._attributesNeedDynamicReset = true;
    }
};


/**
 * Dipose the geometry and material for the group.
 *
 * @return {Group} Group instance.
 */
SPE.Group.prototype.dispose = function() {
    'use strict';
    this.geometry.dispose();
    this.material.dispose();
    return this;
};