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;