particle/ParticleRenderable.js

import Renderable from '../Renderable';

import Geometry from '../Geometry';
import Material from '../Material';
import Shader from '../Shader';

import particleEssl from './particle.glsl.js';
Shader['import'](particleEssl);

var particleShader = new Shader(Shader.source('clay.particle.vertex'), Shader.source('clay.particle.fragment'));

/**
 * @constructor clay.particle.ParticleRenderable
 * @extends clay.Renderable
 *
 * @example
 *     var particleRenderable = new clay.particle.ParticleRenderable({
 *         spriteAnimationTileX: 4,
 *         spriteAnimationTileY: 4,
 *         spriteAnimationRepeat: 1
 *     });
 *     scene.add(particleRenderable);
 *     // Enable uv animation in the shader
 *     particleRenderable.material.define('both', 'UV_ANIMATION');
 *     var Emitter = clay.particle.Emitter;
 *     var Vector3 = clay.Vector3;
 *     var emitter = new Emitter({
 *         max: 2000,
 *         amount: 100,
 *         life: Emitter.random1D(10, 20),
 *         position: Emitter.vector(new Vector3()),
 *         velocity: Emitter.random3D(new Vector3(-10, 0, -10), new Vector3(10, 0, 10));
 *     });
 *     particleRenderable.addEmitter(emitter);
 *     var gravityField = new clay.particle.ForceField();
 *     gravityField.force.y = -10;
 *     particleRenderable.addField(gravityField);
 *     ...
 *     animation.on('frame', function(frameTime) {
 *         particleRenderable.updateParticles(frameTime);
 *         renderer.render(scene, camera);
 *     });
 */
var ParticleRenderable = Renderable.extend(/** @lends clay.particle.ParticleRenderable# */ {
    /**
     * @type {boolean}
     */
    loop: true,
    /**
     * @type {boolean}
     */
    oneshot: false,
    /**
     * Duration of particle system in milliseconds
     * @type {number}
     */
    duration: 1,

    // UV Animation
    /**
     * @type {number}
     */
    spriteAnimationTileX: 1,
    /**
     * @type {number}
     */
    spriteAnimationTileY: 1,
    /**
     * @type {number}
     */
    spriteAnimationRepeat: 0,

    mode: Renderable.POINTS,

    ignorePicking: true,

    _elapsedTime: 0,

    _emitting: true

}, function(){

    this.geometry = new Geometry({
        dynamic: true
    });

    if (!this.material) {
        this.material = new Material({
            shader: particleShader,
            transparent: true,
            depthMask: false
        });

        this.material.enableTexture('sprite');
    }

    this._particles = [];
    this._fields = [];
    this._emitters = [];
},
/** @lends clay.particle.ParticleRenderable.prototype */
{

    culling: false,

    frustumCulling: false,

    castShadow: false,
    receiveShadow: false,

    /**
     * Add emitter
     * @param {clay.particle.Emitter} emitter
     */
    addEmitter: function(emitter) {
        this._emitters.push(emitter);
    },

    /**
     * Remove emitter
     * @param {clay.particle.Emitter} emitter
     */
    removeEmitter: function(emitter) {
        this._emitters.splice(this._emitters.indexOf(emitter), 1);
    },

    /**
     * Add field
     * @param {clay.particle.Field} field
     */
    addField: function(field) {
        this._fields.push(field);
    },

    /**
     * Remove field
     * @param {clay.particle.Field} field
     */
    removeField: function(field) {
        this._fields.splice(this._fields.indexOf(field), 1);
    },

    /**
     * Reset the particle system.
     */
    reset: function() {
        // Put all the particles back
        for (var i = 0; i < this._particles.length; i++) {
            var p = this._particles[i];
            p.emitter.kill(p);
        }
        this._particles.length = 0;
        this._elapsedTime = 0;
        this._emitting = true;
    },

    /**
     * @param  {number} deltaTime
     */
    updateParticles: function(deltaTime) {

        // MS => Seconds
        deltaTime /= 1000;
        this._elapsedTime += deltaTime;

        var particles = this._particles;

        if (this._emitting) {
            for (var i = 0; i < this._emitters.length; i++) {
                this._emitters[i].emit(particles);
            }
            if (this.oneshot) {
                this._emitting = false;
            }
        }

        // Aging
        var len = particles.length;
        for (var i = 0; i < len;) {
            var p = particles[i];
            p.age += deltaTime;
            if (p.age >= p.life) {
                p.emitter.kill(p);
                particles[i] = particles[len-1];
                particles.pop();
                len--;
            } else {
                i++;
            }
        }

        for (var i = 0; i < len; i++) {
            // Update
            var p = particles[i];
            if (this._fields.length > 0) {
                for (var j = 0; j < this._fields.length; j++) {
                    this._fields[j].applyTo(p.velocity, p.position, p.weight, deltaTime);
                }
            }
            p.update(deltaTime);
        }

        this._updateVertices();
    },

    _updateVertices: function() {
        var geometry = this.geometry;
        // If has uv animation
        var animTileX = this.spriteAnimationTileX;
        var animTileY = this.spriteAnimationTileY;
        var animRepeat = this.spriteAnimationRepeat;
        var nUvAnimFrame = animTileY * animTileX * animRepeat;
        var hasUvAnimation = nUvAnimFrame > 1;
        var positions = geometry.attributes.position.value;
        // Put particle status in normal
        var normals = geometry.attributes.normal.value;
        var uvs = geometry.attributes.texcoord0.value;
        var uvs2 = geometry.attributes.texcoord1.value;

        var len = this._particles.length;
        if (!positions || positions.length !== len * 3) {
            // TODO Optimize
            positions = geometry.attributes.position.value = new Float32Array(len * 3);
            normals = geometry.attributes.normal.value = new Float32Array(len * 3);
            if (hasUvAnimation) {
                uvs = geometry.attributes.texcoord0.value = new Float32Array(len * 2);
                uvs2 = geometry.attributes.texcoord1.value = new Float32Array(len * 2);
            }
        }

        var invAnimTileX = 1 / animTileX;
        for (var i = 0; i < len; i++) {
            var particle = this._particles[i];
            var offset = i * 3;
            for (var j = 0; j < 3; j++) {
                positions[offset + j] = particle.position.array[j];
                normals[offset] = particle.age / particle.life;
                // normals[offset + 1] = particle.rotation;
                normals[offset + 1] = 0;
                normals[offset + 2] = particle.spriteSize;
            }
            var offset2 = i * 2;
            if (hasUvAnimation) {
                // TODO
                var p = particle.age / particle.life;
                var stage = Math.round(p * (nUvAnimFrame - 1)) * animRepeat;
                var v = Math.floor(stage * invAnimTileX);
                var u = stage - v * animTileX;
                uvs[offset2] = u / animTileX;
                uvs[offset2 + 1] = 1 - v / animTileY;
                uvs2[offset2] = (u + 1) / animTileX;
                uvs2[offset2 + 1] = 1 - (v + 1) / animTileY;
            }
        }

        geometry.dirty();
    },

    /**
     * @return {boolean}
     */
    isFinished: function() {
        return this._elapsedTime > this.duration && !this.loop;
    },

    /**
     * @param  {clay.Renderer} renderer
     */
    dispose: function(renderer) {
        // Put all the particles back
        for (var i = 0; i < this._particles.length; i++) {
            var p = this._particles[i];
            p.emitter.kill(p);
        }
        this.geometry.dispose(renderer);
        // TODO Dispose texture ?
    },

    /**
     * @return {clay.particle.ParticleRenderable}
     */
    clone: function() {
        var particleRenderable = new ParticleRenderable({
            material: this.material
        });
        particleRenderable.loop = this.loop;
        particleRenderable.duration = this.duration;
        particleRenderable.oneshot = this.oneshot;
        particleRenderable.spriteAnimationRepeat = this.spriteAnimationRepeat;
        particleRenderable.spriteAnimationTileY = this.spriteAnimationTileY;
        particleRenderable.spriteAnimationTileX = this.spriteAnimationTileX;

        particleRenderable.position.copy(this.position);
        particleRenderable.rotation.copy(this.rotation);
        particleRenderable.scale.copy(this.scale);

        for (var i = 0; i < this._children.length; i++) {
            particleRenderable.add(this._children[i].clone());
        }
        return particleRenderable;
    }
});

export default ParticleRenderable;