///@INFO: UNCOMMON
/**
* The base class used by the ParticlesEmissor
* @class Particle
* @namespace LS
* @constructor
* @param {Object} object to configure from
*/
function Particle()
{
this.id = 0;
this._pos = vec3.fromValues(0,0,0);
this._vel = vec3.fromValues(0,0,0);
this.life = 1;
this.angle = 0;
this.size = 1;
this.rot = 0;
}
Object.defineProperty( Particle.prototype, 'pos', {
get: function() { return this._pos; },
set: function(v) { this._pos.set(v); },
enumerable: true
});
Object.defineProperty( Particle.prototype, 'vel', {
get: function() { return this._vel; },
set: function(v) { this._vel.set(v); },
enumerable: true
});
/**
* ParticlesEmissor allow to render a particle system, meant to render things like smoke or fire
* @class ParticlesEmissor
* @namespace LS.Components
* @constructor
* @param {Object} object to configure from
*/
function ParticleEmissor(o)
{
this.enabled = true;
this.max_particles = 1024;
this.warm_up_time = 0;
this.point_particles = false;
this.emissor_type = ParticleEmissor.BOX_EMISSOR;
this.emissor_rate = 5; //particles per second
this.emissor_size = vec3.fromValues(10,10,10);
this.emissor_mesh = null;
this.particle_life = 5;
this.particle_speed = 10;
this.particle_size = 5;
this.particle_rotation = 0;
this.particle_size_curve = [[1,1]];
this._particle_start_color = vec3.fromValues(1,1,1);
this._particle_end_color = vec3.fromValues(1,1,1);
this.particle_opacity_curve = [[0.5,1]];
this.texture_grid_size = 1;
//physics
this.physics_gravity = [0,0,0];
this.physics_friction = 0;
//material
this.opacity = 1;
this.additive_blending = false;
this.texture = null;
this.animation_fps = 1;
this.soft_particles = false;
this.use_node_material = false;
this.animated_texture = false; //change frames
this.loop_animation = false;
this.independent_color = false;
this.premultiplied_alpha = false;
this.align_with_camera = true;
this.align_always = false; //align with all cameras
this.follow_emitter = false;
this.sort_in_z = true; //slower
this.stop_update = false; //do not move particles
this.ignore_lights = false;
this.onCreateParticle = null;
this.onUpdateParticle = null;
if(o)
this.configure(o);
//LEGACY!!! sizes where just a number before
if(typeof(this.emissor_size) == "number")
this.emissor_size = [this.emissor_size,this.emissor_size,this.emissor_size];
this._emissor_pos = vec3.create();
this._particles = [];
this._remining_dt = 0;
this._visible_particles = 0;
this._min_particle_size = 0.001;
this._last_id = 0;
if(global.gl)
this.createMesh();
/* demo particles
for(var i = 0; i < this.max_particles; i++)
{
var p = this.createParticle();
this._particles.push(p);
}
*/
}
ParticleEmissor.BOX_EMISSOR = 1;
ParticleEmissor.SPHERE_EMISSOR = 2;
ParticleEmissor.MESH_EMISSOR = 3;
ParticleEmissor.CUSTOM_EMISSOR = 10;
ParticleEmissor["@emissor_type"] = { type:"enum", values:{ "Box":ParticleEmissor.BOX_EMISSOR, "Sphere":ParticleEmissor.SPHERE_EMISSOR, "Mesh":ParticleEmissor.MESH_EMISSOR, "Custom": ParticleEmissor.CUSTOM_EMISSOR }};
ParticleEmissor.icon = "mini-icon-particles.png";
Object.defineProperty( ParticleEmissor.prototype, 'particle_start_color', {
get: function() { return this._particle_start_color; },
set: function(v) {
if(v)
this._particle_start_color.set(v);
},
enumerable: true
});
Object.defineProperty( ParticleEmissor.prototype, 'particle_end_color', {
get: function() { return this._particle_end_color; },
set: function(v) {
if(v)
this._particle_end_color.set(v);
},
enumerable: true
});
ParticleEmissor.prototype.onAddedToScene = function(scene)
{
LEvent.bind( scene, "update",this.onUpdate,this);
LEvent.bind( scene, "start",this.onStart,this);
LEvent.bind( scene, "collectRenderInstances", this.onCollectInstances, this);
LEvent.bind( scene, "afterCameraEnabled",this.onAfterCamera, this);
}
ParticleEmissor.prototype.onRemovedFromScene = function(scene)
{
LEvent.unbindAll( scene, this );
}
ParticleEmissor.prototype.getResources = function(res)
{
if(this.emissor_mesh)
res[ this.emissor_mesh ] = Mesh;
if(this.texture)
res[ this.texture ] = Texture;
}
ParticleEmissor.prototype.onResourceRenamed = function (old_name, new_name, resource)
{
if(this.emissor_mesh == old_name)
this.emissor_mesh = new_name;
if(this.texture == old_name)
this.texture = new_name;
}
ParticleEmissor.prototype.onAfterCamera = function(e,camera)
{
if(!this.enabled)
return;
if(this.align_always)
this.updateMesh( camera );
}
ParticleEmissor.prototype.createParticle = function(p)
{
p = p || new Particle();
switch(this.emissor_type)
{
case ParticleEmissor.BOX_EMISSOR: p._pos.set( [this.emissor_size[0] * ( Math.random() - 0.5), this.emissor_size[1] * ( Math.random() - 0.5 ), this.emissor_size[2] * (Math.random() - 0.5) ]); break;
case ParticleEmissor.SPHERE_EMISSOR:
var gamma = 2 * Math.PI * Math.random();
var theta = Math.acos(2 * Math.random() - 1);
p._pos.set( [Math.sin(theta) * Math.cos(gamma), Math.sin(theta) * Math.sin(gamma), Math.cos(theta) ]);
vec3.multiply( p.pos, p.pos, this.emissor_size);
break;
//p.pos = vec3.multiply( vec3.normalize( vec3.create( [(Math.random() - 0.5), ( Math.random() - 0.5 ), (Math.random() - 0.5)])), this.emissor_size); break;
case ParticleEmissor.MESH_EMISSOR:
var mesh = this.emissor_mesh;
if(mesh && mesh.constructor === String)
mesh = LS.ResourcesManager.getMesh(this.emissor_mesh);
if(mesh && mesh.getBuffer("vertices") )
{
var vertices = mesh.getBuffer("vertices").data;
var v = Math.floor(Math.random() * vertices.length / 3)*3;
p._pos.set( [vertices[v] + Math.random() * 0.001, vertices[v+1] + Math.random() * 0.001, vertices[v+2] + Math.random() * 0.001] );
}
else
p._pos.set([0,0,0]);
break;
case ParticleEmissor.CUSTOM_EMISSOR: //done after the rest
default:
}
p._vel.set( [Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5 ] );
p.life = this.particle_life;
p.id = this._last_id;
p.angle = 0;
p.size = 1;
p.rot = this.particle_rotation + 0.25 * this.particle_rotation * Math.random();
this._last_id += 1;
if(this.independent_color)
p.c = vec3.clone( this.particle_start_color );
vec3.scale(p._vel, p._vel, this.particle_speed);
//after everything so the user can edit whatever he wants
if(this.emissor_type == ParticleEmissor.CUSTOM_EMISSOR && this.onCreateParticle)
this.onCreateParticle( p, this );
//this._root.transform.transformPoint(p.pos, p.pos);
if(!this.follow_emitter) //the transform will be applyed in the matrix
vec3.add(p._pos, p._pos, this._emissor_pos);
return p;
}
ParticleEmissor.prototype.onStart = function(e)
{
if(!this.enabled)
return;
if(this.warm_up_time <= 0)
return;
var delta = 1/30;
for(var i = 0; i < this.warm_up_time; i+= delta)
this.onUpdate( null, delta, true);
}
ParticleEmissor.prototype.onUpdate = function(e, dt, do_not_updatemesh )
{
if(!this.enabled)
return;
if(!this._root.scene)
throw("update without scene? impossible");
if(this._root.transform)
this._root.transform.getGlobalPosition(this._emissor_pos);
if(this.emissor_rate < 0) this.emissor_rate = 0;
if(!this.stop_update)
{
//update particles
var gravity = vec3.clone(this.physics_gravity);
var friction = this.physics_friction;
var particles = [];
var vel = vec3.create();
var rot = this.particle_rotation * dt;
for(var i = 0, l = this._particles.length; i < l; ++i)
{
var p = this._particles[i];
vec3.copy(vel, p._vel);
vec3.add(vel, gravity, vel);
vec3.scale(vel, vel, dt);
if(friction)
{
vel[0] -= vel[0] * friction;
vel[1] -= vel[1] * friction;
vel[2] -= vel[2] * friction;
}
vec3.add( p._pos, vel, p._pos);
p.angle += p.rot * dt;
p.life -= dt;
if(this.onUpdateParticle)
this.onUpdateParticle(p,dt,this);
if(p.life > 0) //keep alive
particles.push(p);
}
//emit new
if(this.emissor_rate != 0)
{
var new_particles = (dt + this._remining_dt) * this.emissor_rate;
this._remining_dt = (new_particles % 1) / this.emissor_rate;
new_particles = new_particles<<0;
if(new_particles > this.max_particles)
new_particles = this.max_particles;
for(var i = 0; i < new_particles; i++)
{
var p = this.createParticle();
if(particles.length < this.max_particles)
particles.push(p);
}
}
//replace old container with new one
this._particles = particles;
}
//compute mesh
if(!this.align_always && !do_not_updatemesh)
{
this.updateMesh( LS.Renderer._current_camera );
this._root.scene.requestFrame();
}
//send change
LEvent.trigger( this._root.scene , "change"); //??
}
ParticleEmissor.prototype.createMesh = function ()
{
if( this._mesh_maxparticles == this.max_particles)
return;
this._vertices = new Float32Array(this.max_particles * 6 * 3); //6 vertex per particle x 3 floats per vertex
this._coords = new Float32Array(this.max_particles * 6 * 2);
this._colors = new Float32Array(this.max_particles * 6 * 4);
this._extra2 = new Float32Array(this.max_particles * 2);
var default_coords = [1,1, 0,1, 1,0, 0,1, 0,0, 1,0];
var default_color = [1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1];
for(var i = 0; i < this.max_particles; i++)
{
this._coords.set( default_coords , i*6*2);
this._colors.set( default_color , i*6*4);
this._extra2[i*2] = 1;
this._extra2[i*2+1] = i;
}
this._computed_grid_size = 1;
//this._mesh = Mesh.load({ vertices:this._vertices, coords: this._coords, colors: this._colors, stream_type: gl.STREAM_DRAW });
this._mesh = new GL.Mesh();
this._mesh.addBuffers({ vertices:this._vertices, coords: this._coords, colors: this._colors, extra2: this._extra2 }, null, gl.STREAM_DRAW);
this._mesh_maxparticles = this.max_particles;
}
ParticleEmissor._tmp_quat = quat.create();
ParticleEmissor.prototype.updateMesh = function (camera)
{
if(!camera) //no main camera specified (happens at early updates)
return;
if( this._mesh_maxparticles != this.max_particles)
this.createMesh();
var center = camera.getEye();
var MIN_SIZE = this._min_particle_size;
/*
if(this.follow_emitter)
{
var iM = this._root.transform.getMatrix();
mat4.multiplyVec3(iM, center);
}
*/
var front = camera.getLocalVector([0,0,1]);
var right = camera.getLocalVector([1,0,0]);
var top = camera.getLocalVector([0,1,0]);
var temp = vec3.create();
var size = this.particle_size;
var topleft = vec3.fromValues(-1,0,-1);
var topright = vec3.fromValues(1,0,-1);
var bottomleft = vec3.fromValues(-1,0,1);
var bottomright = vec3.fromValues(1,0,1);
if(this.align_with_camera)
{
vec3.subtract(topleft, top,right);
vec3.add(topright, top,right);
vec3.scale(bottomleft,topright,-1);
vec3.scale(bottomright,topleft,-1);
}
//scaled versions
var s_topleft = vec3.create()
var s_topright = vec3.create()
var s_bottomleft = vec3.create()
var s_bottomright = vec3.create()
var particles = this._particles;
if(this.sort_in_z)
{
particles = this._particles.concat(); //copy
var plane = geo.createPlane(center, front); //compute camera plane
var den = 1 / Math.sqrt(plane[0]*plane[0] + plane[1]*plane[1] + plane[2]*plane[2]); //delta
for(var i = 0; i < particles.length; ++i)
particles[i]._dist = Math.abs(vec3.dot(particles[i]._pos,plane) + plane[3]) * den;
//particles[i]._dist = vec3.dist( center, particles[i].pos );
particles.sort(function(a,b) { return a._dist < b._dist ? 1 : (a._dist > b._dist ? -1 : 0); });
this._particles = particles;
}
//avoid errors
if(this.particle_life == 0)
this.particle_life = 0.0001;
var color = new Float32Array([1,1,1,1]);
var particle_start_color = this._particle_start_color;
var particle_end_color = this._particle_end_color;
//used for grid based textures
var recompute_coords = false;
if((this._computed_grid_size != this.texture_grid_size || this.texture_grid_size > 1) || this.point_particles)
{
recompute_coords = true;
this._computed_grid_size = this.texture_grid_size;
}
var texture_grid_size = this.texture_grid_size;
var d_uvs = 1 / this.texture_grid_size;
var offset_u = 0, offset_v = 0;
var grid_frames = this.texture_grid_size<<2;
var animated_texture = this.animated_texture;
var loop_animation = this.loop_animation;
var time = this._root.scene.getTime() * this.animation_fps;
//used for precompute curves to speed up (sampled at 60 frames per second)
var recompute_colors = true;
var opacity_curve = new Float32Array((this.particle_life * 60)<<0);
var size_curve = new Float32Array((this.particle_life * 60)<<0);
var particle_size = this.particle_size;
var dI = 1 / (this.particle_life * 60);
for(var i = 0; i < opacity_curve.length; i += 1)
{
opacity_curve[i] = LS.getCurveValueAt(this.particle_opacity_curve,0,1,0, i * dI );
size_curve[i] = LS.getCurveValueAt(this.particle_size_curve,0,1,0, i * dI );
}
//references
var points = this.point_particles;
var max_vertices = this._vertices.length;
var vertices = this._vertices;
var colors = this._colors;
var extra2 = this._extra2;
var coords = this._coords;
//used for rotations
var rot = ParticleEmissor._tmp_quat;
//generate quads
var i = 0, f = 0;
for( var iParticle = 0, l = particles.length; iParticle < l; ++iParticle )
{
var p = particles[iParticle];
if(p.life <= 0)
continue;
f = 1.0 - p.life / this.particle_life;
if(recompute_colors) //compute color and opacity
{
var a = opacity_curve[(f*opacity_curve.length)<<0]; //getCurveValueAt(this.particle_opacity_curve,0,1,0,f);
if(this.independent_color && p.c)
vec3.clone(color,p.c);
else
vec3.lerp(color, particle_start_color, particle_end_color, f);
if(this.premultiplied_alpha)
{
vec3.scale(color,color,a);
color[3] = 1.0;
}
else
color[3] = a;
if(a < 0.001)
continue;
}
var s = p.size * size_curve[(f*size_curve.length)<<0]; //getCurveValueAt(this.particle_size_curve,0,1,0,f);
if(Math.abs(s * particle_size) < MIN_SIZE)
continue; //ignore almost transparent particles
if(points)
{
vertices.set(p._pos, i*3);
colors.set(color, i*4);
if(recompute_coords)
{
var iG = (animated_texture ? ((loop_animation?time:f)*grid_frames)<<0 : p.id) % grid_frames;
offset_u = iG * d_uvs;
offset_v = 1 - (offset_u<<0) * d_uvs - d_uvs;
offset_u = offset_u%1;
coords[i*2] = offset_u;
coords[i*2+1] = offset_v;
}
extra2[i*2] = s;
extra2[i*2+1] = i;
++i;
if(i*3 >= max_vertices)
break; //too many particles
continue;
}
s *= particle_size;
vec3.scale(s_bottomleft, bottomleft, s)
vec3.scale(s_topright, topright, s);
vec3.scale(s_topleft, topleft, s);
vec3.scale(s_bottomright, bottomright, s);
if(p.angle != 0)
{
quat.setAxisAngle( rot , front, p.angle * DEG2RAD);
vec3.transformQuat(s_bottomleft, s_bottomleft, rot);
vec3.transformQuat(s_topright, s_topright, rot);
vec3.transformQuat(s_topleft, s_topleft, rot);
vec3.transformQuat(s_bottomright, s_bottomright, rot);
}
vec3.add(temp, p._pos, s_topright);
vertices.set(temp, i*6*3);
vec3.add(temp, p._pos, s_topleft);
vertices.set(temp, i*6*3 + 3);
vec3.add(temp, p._pos, s_bottomright);
vertices.set(temp, i*6*3 + 3*2);
vec3.add(temp, p._pos, s_topleft);
vertices.set(temp, i*6*3 + 3*3);
vec3.add(temp, p._pos, s_bottomleft);
vertices.set(temp, i*6*3 + 3*4);
vec3.add(temp, p._pos, s_bottomright);
vertices.set(temp, i*6*3 + 3*5);
if(recompute_colors)
{
colors.set(color, i*6*4);
colors.set(color, i*6*4 + 4);
colors.set(color, i*6*4 + 4*2);
colors.set(color, i*6*4 + 4*3);
colors.set(color, i*6*4 + 4*4);
colors.set(color, i*6*4 + 4*5);
}
if(recompute_coords)
{
var iG = (animated_texture ? ((loop_animation?time:f)*grid_frames)<<0 : p.id) % grid_frames;
offset_u = iG * d_uvs;
offset_v = 1 - (offset_u<<0) * d_uvs - d_uvs;
offset_u = offset_u%1;
coords.set([offset_u+d_uvs,offset_v+d_uvs, offset_u,offset_v+d_uvs, offset_u+d_uvs,offset_v, offset_u,offset_v+d_uvs, offset_u,offset_v, offset_u+d_uvs,offset_v], i*6*2);
}
++i;
if(i*6*3 >= max_vertices)
break; //too many particles
}
this._visible_particles = i;
//upload geometry
this._mesh.vertexBuffers["vertices"].data = this._vertices;
this._mesh.vertexBuffers["vertices"].upload(gl.STREAM_DRAW);
this._mesh.vertexBuffers["colors"].data = this._colors;
this._mesh.vertexBuffers["colors"].upload(gl.STREAM_DRAW);
this._mesh.vertexBuffers["extra2"].data = this._extra2;
this._mesh.vertexBuffers["extra2"].upload(gl.STREAM_DRAW);
if(recompute_coords)
{
this._mesh.vertexBuffers["coords"].data = this._coords;
this._mesh.vertexBuffers["coords"].upload(gl.STREAM_DRAW);
}
}
ParticleEmissor._identity = mat4.create();
//ParticleEmissor.prototype.getRenderInstance = function(options,camera)
ParticleEmissor.prototype.onCollectInstances = function(e, instances, options)
{
if(!this._root || !this.enabled)
return;
var camera = LS.Renderer._current_camera;
if(!this._material)
this._material = new LS.StandardMaterial({ shader_name:"lowglobal" });
this._material.opacity = this.opacity - 0.01; //try to keep it under 1
this._material.setTexture( "color", this.texture );
this._material.blend_mode = this.additive_blending ? LS.Blend.ADD : LS.Blend.ALPHA;
this._material.constant_diffuse = true;
this._material.uvs_matrix[0] = this._material.uvs_matrix[4] = 1 / this.texture_grid_size;
this._material.flags.depth_write = false;
this._material.flags.ignore_lights = this.ignore_lights;
if(!this._mesh)
return null;
var RI = this._render_instance;
if(!RI)
this._render_instance = RI = new LS.RenderInstance(this._root, this);
if(this.follow_emitter)
mat4.translate( RI.matrix, ParticleEmissor._identity, this._root.transform._position );
else
{
mat4.copy( RI.matrix, ParticleEmissor._identity );
if(this._root.transform)
this._root.transform.getGlobalPosition( RI.center );
}
var material = (this._root.material && this.use_node_material) ? this._root.getMaterial() : this._material;
mat4.multiplyVec3(RI.center, RI.matrix, vec3.create());
RI.setMaterial( material );
if(this.point_particles)
{
RI.setMesh( this._mesh, gl.POINTS );
RI.uniforms.u_point_size = this.particle_size;
RI.setRange(0, this._visible_particles);
}
else
{
RI.setMesh( this._mesh, gl.TRIANGLES );
RI.setRange(0, this._visible_particles * 6); //6 vertex per particle
delete RI.uniforms["u_point_size"];
}
RI.use_bounding = false; //bounding is not valid
instances.push( RI );
}
LS.Particle = Particle;
LS.registerComponent(ParticleEmissor);
//shader
// - apply light per vertex before expanding
// - inflate with camera vectors