janusweb
Version:
Web client for JanusVR worlds
719 lines (651 loc) • 25.1 kB
JavaScript
elation.require(['janusweb.janusbase'], function() {
let shaderdef = {
vertex: `
attribute float size;
attribute vec4 customColor;
varying vec4 vColor;
void main() {
vColor = customColor;
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_PointSize = size * ( 300.0 / -mvPosition.z );
gl_Position = projectionMatrix * mvPosition;
}
`,
fragment: `
uniform vec3 color;
uniform sampler2D sprite;
varying vec4 vColor;
void main() {
gl_FragColor = vec4( color * vColor.rgb, vColor.a );
#ifdef USE_MAP
gl_FragColor = gl_FragColor * texture2D( sprite, gl_PointCoord );
#endif
}
`
};
elation.component.add('engine.things.janusparticle', function() {
this.postinit = function() {
elation.engine.things.janusparticle.extendclass.postinit.call(this);
this.defineProperties({
emitter_id: { type: 'string', set: this.updateGeometry },
emitter_scale: { type: 'vector3', default: [1, 1, 1], set: this.updateGeometry},
emitter_pos: { type: 'vector3', default: [0, 0, 0] },
image_id: { type: 'string', set: this.updateMaterial },
rate: { type: 'float', default: 1 },
count: { type: 'int', default: 100, set: this.updateParticles },
duration: { type: 'float', default: 1.0 },
opacity: { type: 'float', default: 1.0, set: this.updateMaterial },
fade_in: { type: 'float', default: 1.0 },
fade_out: { type: 'float', default: 1.0 },
particle_scale: { type: 'vector3', default: [1, 1, 1], set: this.updateMaterial},
particle_vel: { type: 'vector3', default: [0, 0, 0]},
particle_accel: { type: 'vector3', default: [0, 0, 0]},
rand_pos: { type: 'vector3', default: [0, 0, 0]},
rand_vel: { type: 'vector3', default: [0, 0, 0]},
rand_accel: { type: 'vector3', default: [0, 0, 0]},
rand_col: { type: 'vector3', default: [0, 0, 0]},
rand_scale: { type: 'vector3', default: [0, 0, 0]},
speed_min: { type: 'float', default: null },
speed_max: { type: 'float', default: null },
loop: { type: 'bool', default: true, set: this.updateParticles },
refreshrate: { type: 'int', default: 30 },
blend_src: { type: 'string', default: 'src_alpha', set: this.updateMaterial },
blend_dest: { type: 'string', default: 'one_minus_src_alpha', set: this.updateMaterial },
depth_write: { type: 'bool', default: false },
depth_test: { type: 'bool', default: true },
});
this.particles = [];
this.emitted = 0;
this.emitmesh = false;
this.emitpoints = false;
this.currentpoint = 0;
this.lasttime = 0;
this.loaded = false;
this.started = false;
this.pickable = false;
this.collidable = false;
this.boundingRadiusSq = 0;
this.boundingSphereWorld = new THREE.Sphere();
this.furthestPoint = new THREE.Vector3();
this.lastrefresh = 0;
this.updateParticles = elation.bind(this, this.updateParticles); // FIXME - hack, this should happen at the lower level of all components
}
this.createObject3D = function() {
var geo = this.geometry = new THREE.BufferGeometry();
var texture = null,
textureasset = null;
if (this.image_id) {
textureasset = this.getAsset('image', this.image_id);
if (textureasset) {
elation.events.add(textureasset, 'asset_load', elation.bind(this, this.updateMaterial));
elation.events.add(textureasset, 'update', elation.bind(this, this.refresh));
this.imageasset = textureasset;
texture = textureasset.getInstance();
}
}
if (this.emitter_id) {
var asset = this.getAsset('model', this.emitter_id);
if (asset) {
this.emitmesh = asset.getInstance();
if (asset.loaded) {
this.loaded = true;
} else {
elation.events.add(asset, 'asset_load_complete', elation.bind(this, function() { this.loaded = true; this.createParticles(false); this.start(); }));
}
}
} else {
this.loaded = true;
}
this.createParticles();
if (this.loaded) {
this.start();
}
/*
var mat = new THREE.PointsMaterial({
//color: this.color,
map: texture,
size: this.particle_scale.x + this.rand_scale.x,
transparent: true,
opacity: this.opacity,
alphaTest: 0.001,
//blending: THREE.AdditiveBlending,
vertexColors: THREE.VertexColors
});
*/
let shaderdefines = {};
if (texture) {
shaderdefines.USE_MAP = 1;
}
var mat = new THREE.ShaderMaterial( {
uniforms: {
color: { value: new THREE.Color( 0xffffff ) },
sprite: { value: texture },
},
defines: shaderdefines,
vertexShader: shaderdef.vertex,
fragmentShader: shaderdef.fragment,
//blending: THREE.AdditiveBlending,
transparent: true,
alphaTest: .001,
depthWrite: this.depth_write,
depthTest: this.depth_test,
} );
this.material = mat;
this.updateMaterial();
this.scale.set(1,1,1);
var obj = new THREE.Points(geo, mat);
if (this.renderorder) obj.renderOrder = this.renderorder;
return obj;
}
this.updateGeometry = function() {
if (this.emitter_id) {
var asset = this.getAsset('model', this.emitter_id);
if (asset) {
this.emitmesh = asset.getInstance();
if (asset.loaded) {
this.emitpoints = null;
this.loaded = true;
this.createParticles();
} else {
elation.events.add(asset, 'asset_load_complete', elation.bind(this, function() {
this.emitpoints = null;
this.loaded = true;
this.createParticles();
this.start();
}));
}
}
}
}
this.updateMaterial = function() {
if (this.material) {
this.material.opacity = this.opacity;
this.material.transparent = (this.imageasset && this.imageasset.hasalpha) || (this.opacity < 1);
this.material.color = this.color;
this.material.size = this.particle_scale.x + this.rand_scale.x;
let blend_src = false,
blend_dest = false,
srcfactors = {
'zero': THREE.ZeroFactor,
'one': THREE.OneFactor,
'src_color': THREE.SrcColorFactor,
'dst_color': THREE.DstColorFactor,
'src_alpha': THREE.SrcAlphaFactor,
'dst_alpha': THREE.DstAlphaFactor,
'one_minus_src_color': THREE.OneMinusSrcColorFactor,
'one_minus_src_alpha': THREE.OneMinusSrcAlphaFactor,
'one_minus_dst_color': THREE.OneMinusDstColorFactor,
'one_minus_dst_alpha': THREE.OneMinusDstAlphaFactor,
};
if (srcfactors[this.blend_src]) {
blend_src = srcfactors[this.blend_src];
}
if (srcfactors[this.blend_dest]) {
blend_dest = srcfactors[this.blend_dest];
}
if (blend_src || blend_dest) {
let m = this.material;
if (blend_src) m.blendSrc = blend_src;
if (blend_dest) m.blendDst = blend_dest;
m.blending = THREE.CustomBlending;
m.blendSrcAlpha = THREE.SrcAlphaFactor;
m.blendDstAlpha = THREE.OneFactor;
if (!(this.blend_src == 'src_alpha' && this.blend_dest == 'one_minus_src_alpha')) {
m.transparent = true;
}
} else {
m.blending = THREE.NormalBlending;
}
}
}
this.createForces = function() {
elation.engine.things.janusparticle.extendclass.createForces.call(this);
this.properties.particle_vel.copy(this.properties.velocity);
this.properties.velocity.set(0,0,0); // FIXME - hack to override "vel" property mapping
if ((this.collidable || this.pickable) && !this.collider) {
this.collider = new THREE.Points(this.geometry, new THREE.PointsMaterial({color: 0xffff00, opacity: .2, transparent: true, size: this.particle_scale.x + this.rand_scale.x, }));
this.colliders.add(this.collider);
}
}
this.createParticles = function(reset=true) {
if (reset) {
this.particles = [];
}
if (this.emitmesh && !this.emitpoints) {
this.extractEmitPoints(this.emitmesh);
}
var geo = this.geometry;
var count = this.count;
var position = (geo.attributes.position && geo.attributes.position.count == count ? geo.attributes.position.array : new Float32Array(count * 3));
var color = (geo.attributes.customColor && geo.attributes.customColor.count == count ? geo.attributes.customColor.array : new Float32Array(count * 4));
var size = (geo.attributes.size && geo.attributes.size.count == count ? geo.attributes.size.array : new Float32Array(count));
for (var i = 0; i < count; i++) {
var point = this.particles[i];
if (reset || !point) {
point = this.createPoint();
this.resetPoint(point);
this.particles[i] = point;
}
//this.geometry.vertices[i] = point.pos;
position[i*3] = point.pos.x;
position[i*3+1] = point.pos.y;
position[i*3+2] = point.pos.z;
color[i*4] = point.color.r;
color[i*4+1] = point.color.g;
color[i*4+2] = point.color.b;
color[i*4+3] = point.opacity;
size[i] = point.size;
}
geo.setAttribute('position', new THREE.BufferAttribute(position, 3));
geo.setAttribute('customColor', new THREE.BufferAttribute(color, 4));
geo.setAttribute('size', new THREE.BufferAttribute(size, 1));
this.created = true;
this.spawncount = 0;
this.currentpoint = 0;
this.lasttime = performance.now();
//this.updateBoundingSphere();
}
this.resetParticles = function() {
if (!this.created) return;
var geo = this.geometry;
var count = this.count;
var position = geo.attributes.position;
var color = geo.attributes.customColor;
for (var i = 0; i < count; i++) {
var point = this.particles[i];
this.resetPoint(point);
position[i*3] = point.pos.x;
position[i*3+1] = point.pos.y;
position[i*3+2] = point.pos.z;
color[i*4] = point.color.r;
color[i*4+1] = point.color.g;
color[i*4+2] = point.color.b;
color[i*4+3] = point.opacity;
}
this.emitted = 0;
}
this.updateParticles = function(ev) {
if (!this.loaded || !this.started || !this.parent || !this.visible) return;
var now = performance.now(),
elapsed = now - this.lasttime,
endtime = now + this.duration * 1000,
emitted = 0,
startpoint = this.currentpoint,
spawncount = this.spawncount + this.rate * elapsed / 1000,
count = this.count,
loop = this.loop;
// If we have no particles to render, there's nothing to do!
if (count <= 0) return;
if (count != this.particles.length) {
this.createParticles(false);
}
for (var i = 0; i < count; i++) {
var idx = (i + startpoint) % count;
var p = this.particles[idx];
if (p.active == 1) {
this.updatePoint(p, idx, elapsed/1000);
if (now > p.endtime) {
p.active = (loop ? 0 : 2);
}
} else if (p.active == 0) {
if (spawncount > emitted) {
this.resetPoint(p, idx);
p.starttime = now;
p.endtime = (this.duration > 0 ? endtime : Infinity);
p.active = 1;
emitted++;
spawncount--;
this.currentpoint = (this.currentpoint + 1) % count;
}
}
}
this.spawncount = spawncount;
this.lasttime = now;
// Notify the renderer of our changes, but only if we're visible to the player
// We also rate limit here, so if nothing else in the scene is changing, we
// render at a lower fps
this.localToWorld(this.boundingSphereWorld.center.set(0,0,0));
let geo = this.objects['3d'].geometry;
if (!geo.boundingSphere) {
geo.computeBoundingSphere();
} else {
let currentBoundingRadius = geo.boundingSphere.radius;
if (this.boundingRadiusSq > currentBoundingRadius * currentBoundingRadius) {
geo.boundingSphere.radius = Math.sqrt(this.boundingRadiusSq) + .1;
}
}
this.boundingSphereWorld.radius = geo.boundingSphere.radius;
if (player.viewfrustum.intersectsSphere(this.boundingSphereWorld) && now - this.lastrefresh > (1000 / this.refreshrate)) {
this.refresh();
this.lastrefresh = now;
}
this.geometry.attributes.position.needsUpdate = true;
this.geometry.attributes.customColor.needsUpdate = true;
}
this.updateBoundingSphere = function(vec) {
if (this.objects['3d']) {
var lengthSq = vec.lengthSq();
if (lengthSq > this.boundingRadiusSq) {
this.boundingRadiusSq = lengthSq;
this.furthestPoint.copy(vec);
/*
var geo = this.objects['3d'].geometry;
if (!geo.boundingSphere) {
geo.boundingSphere = new THREE.Sphere();
}
geo.boundingSphere.radius = Math.sqrt(lengthSq);
*/
}
}
}
this.createPoint = function() {
return {
pos: new THREE.Vector3(0, 0, 0),
vel: new THREE.Vector3(0, 0, 0),
accel: new THREE.Vector3(0, 0, 0),
scale: new THREE.Vector3(1,1,1),
color: new THREE.Color(255, 255, 255),
rand_col: new THREE.Color(0, 0, 0),
size: this.particle_scale.x,
opacity: 1,
active: 0,
start: 0,
end: 0
};
}
this.updatePoint = (function() {
var tmpvec = new THREE.Vector3();
return function(point, idx, delta) {
tmpvec.copy(point.accel).multiplyScalar(delta);
point.vel.add(tmpvec);
tmpvec.copy(point.vel).multiplyScalar(delta);
point.pos.add(tmpvec);
var pos = this.geometry.attributes.position.array,
color = this.geometry.attributes.customColor.array,
size = this.geometry.attributes.size.array,
offset = idx * 3;
pos[offset] = point.pos.x;
pos[offset+1] = point.pos.y;
pos[offset+2] = point.pos.z;
color[idx*4] = point.color.r;
color[idx*4+1] = point.color.g;
color[idx*4+2] = point.color.b;
color[idx*4+3] = point.opacity;
size[idx] = point.size;
this.updateBoundingSphere(point.pos);
}
})();
this.resetPoint = function(point, idx) {
var rand_pos = this.properties.rand_pos;
var randomInRange = function(range) {
//return (Math.random() - .5) * range;
return Math.random() * range;
};
let pointpos = point.pos;
pointpos.set(randomInRange(rand_pos.x), randomInRange(rand_pos.y), randomInRange(rand_pos.z));
if (this.emitpoints) {
var rand_id = Math.floor(Math.random() * this.emitpoints.length);
pointpos.add(this.emitpoints[rand_id]);
}
pointpos.add(this.emitter_pos);
point.opacity = this.opacity;
point.active = 0;
var vel = point.vel,
accel = point.accel,
col = point.color,
rand_vel = this.properties.rand_vel,
rand_accel = this.properties.rand_accel,
rand_col = this.properties.rand_col,
rand_scale = this.properties.rand_scale;
vel.copy(this.properties.particle_vel);
accel.copy(this.properties.particle_accel);
if (rand_vel.lengthSq() > 0) {
vel.x += randomInRange(rand_vel.x);
vel.y += randomInRange(rand_vel.y);
vel.z += randomInRange(rand_vel.z);
}
let speed = vel.length();
if (this.speed_min !== null && speed < this.speed_min) {
vel.normalize().multiplyScalar(this.speed_min);
} else if (this.speed_max !== null && speed > this.speed_max) {
vel.normalize().multiplyScalar(this.speed_max);
}
if (rand_accel.lengthSq() > 0) {
accel.x += randomInRange(rand_accel.x);
accel.y += randomInRange(rand_accel.y);
accel.z += randomInRange(rand_accel.z);
}
col.copy(this.properties.color);
if (rand_col.lengthSq() > 0) {
var rand = Math.random();
col.r += rand * rand_col.x;
col.g += rand * rand_col.y;
col.b += rand * rand_col.z;
}
point.size = this.particle_scale.x + randomInRange(rand_scale.x);
if (this.geometry.attributes.position) {
var pos = this.geometry.attributes.position.array,
color = this.geometry.attributes.customColor.array,
size = this.geometry.attributes.size.array;
pos[idx*3] = pointpos.x;
pos[idx*3+1] = pointpos.y;
pos[idx*3+2] = pointpos.z;
color[idx*4] = point.color.r;
color[idx*4+1] = point.color.g;
color[idx*4+2] = point.color.b;
color[idx*4+3] = point.opacity;
size[idx] = point.size;
this.updateBoundingSphere(pointpos);
}
}
this.extractEmitPoints = function(mesh) {
if (mesh) {
var vertices = [];
var scale = this.properties.emitter_scale;
mesh.traverse(function(n) {
if (n && n.geometry) {
var geo = n.geometry;
if (geo instanceof THREE.BufferGeometry) {
var positions = geo.attributes.position.array;
for (var i = 0; i < positions.length; i += 3) {
vertices.push(new THREE.Vector3(positions[i] * scale.x, positions[i+1] * scale.y, positions[i+2] * scale.z));
}
}
}
});
if (vertices.length > 0) {
this.emitpoints = vertices;
}
}
}
this.start = function() {
if (!this.created || (this.count > this.particles.length)) {
this.createParticles();
} else {
/*
for (var i = 0; i < this.particles.length; i++) {
this.particles[i].active = 0;
}
this.resetParticles();
*/
}
this.currentpoint = 0;
if (this.started) {
this.stop();
}
this.started = true;
if (this.duration > 0) {
elation.events.add(this.engine, 'engine_frame', this.updateParticles);
}
}
this.stop = function() {
this.started = false;
if (this.duration > 0) {
elation.events.remove(this.engine, 'engine_frame', this.updateParticles);
}
}
this.getProxyObject = function(classdef) {
if (!this._proxyobject) {
this._proxyobject = elation.engine.things.janusparticle.extendclass.getProxyObject.call(this, classdef);
this._proxyobject._proxydefs = {
loop: [ 'property', 'loop'],
vel: [ 'property', 'particle_vel'],
accel: [ 'property', 'particle_accel'],
rand_pos: [ 'property', 'rand_pos'],
rand_vel: [ 'property', 'rand_vel'],
rand_accel: [ 'property', 'rand_accel'],
rand_col: [ 'property', 'rand_col'],
particle_vel: [ 'property', 'particle_vel'],
image_id: [ 'property', 'image_id'],
emitter_id: [ 'property', 'emitter_id'],
emitter_scale: [ 'property', 'emitter_scale'],
emitter_pos: [ 'property', 'emitter_pos'],
rate: [ 'property', 'rate'],
count: [ 'property', 'count'],
duration: [ 'property', 'duration'],
opacity: [ 'property', 'opacity'],
depth_write: [ 'property', 'depth_write'],
depth_test: [ 'property', 'depth_test'],
play: [ 'function', 'start'],
};
}
return this._proxyobject;
}
this.setPoint = function(pointnum, newpos, newvel, newaccel, newcol, newsize, newopacity=1) {
var offset = pointnum * 3;
var point = this.particles[pointnum];
if (!point) {
point = this.createPoint();
point.active = 1;
this.particles[pointnum] = point;
}
point.active = 1;
var geo = this.geometry,
pos = geo.attributes.position.array,
color = geo.attributes.customColor.array,
size = geo.attributes.size.array;
if (newpos) {
point.pos.copy(newpos);
pos[offset ] = newpos.x;
pos[offset + 1] = newpos.y;
pos[offset + 2] = newpos.z;
geo.attributes.position.needsUpdate = true;
//this.updateBoundingSphere(newpos);
if (!this.updateBoundingSphereTimer) {
this.updateBoundingSphereTimer = setTimeout(() => {
this.updateBoundingSphere(newpos);
this.updateBoundingSphereTimer = false;
}, 0);
}
}
if (newvel) {
point.vel.copy(newvel);
}
if (newaccel) {
point.accel.copy(newaccel);
}
if (newcol) {
point.color.setRGB(newcol.x, newcol.y, newcol.z);
color[pointnum*4 ] = newcol.x;
color[pointnum*4 + 1] = newcol.y;
color[pointnum*4 + 2] = newcol.z;
color[pointnum*4 + 3] = elation.utils.any(point.opacity, this.properties.opacity, 1);
geo.attributes.customColor.needsUpdate = true;
}
if (newsize) {
point.size = newsize;
size[pointnum] = newsize;
geo.attributes.size.needsUpdate = true;
}
point.opacity = newopacity;
if (pointnum >= this.properties.count) {
if (!this.updateParticleTimer) {
this.updateParticleTimer = setTimeout(() => {
this.count = this.particles.length;
this.createParticles(false);
this.updateParticleTimer = false;
}, 0);
}
}
}
this.setPoints = (function() {
let furthest = new THREE.Vector3();
return function(points) {
var geo = this.geometry,
pos = geo.attributes.position.array,
color = geo.attributes.customColor.array,
size = geo.attributes.size.array;
furthestDistSq = 0,
particles = this.particles;
for (let i = 0; i < points.length; i++) {
let point = particles[i],
newpoint = points[i],
offset = i * 3;
if (!point) {
point = this.createPoint();
point.active = 1;
particles[i] = point;
}
point.active = 1;
if ('pos' in newpoint) {
//point.pos.copy(newpoint.pos);
let p1 = point.pos,
p2 = newpoint.pos;
pos[offset ] = p1.x = p2.x;
pos[offset + 1] = p1.y = p2.y;
pos[offset + 2] = p1.z = p2.z;
/*
point.pos.copy(newpoint.pos);
pos[offset ] = point.pos.x;
pos[offset + 1] = point.pos.y;
pos[offset + 2] = point.pos.z;
*/
//this.updateBoundingSphere(newpos);
let distSq = p2.x * p2.x + p2.y * p2.y + p2.z * p2.z;
if (distSq > furthestDistSq) {
furthest.x = newpoint.pos.x;
furthest.y = newpoint.pos.y;
furthest.z = newpoint.pos.z;
furthestDistSq = distSq;
}
}
if ('vel' in newpoint) {
//point.vel.copy(newpoint.vel);
point.vel.x = newpoint.vel.x;
point.vel.y = newpoint.vel.y;
point.vel.z = newpoint.vel.z;
}
if ('accel' in newpoint) {
//point.accel.copy(newpoint.accel);
point.accel.x = newpoint.accel.x;
point.accel.y = newpoint.accel.y;
point.accel.z = newpoint.accel.z;
}
if ('opacity' in newpoint) {
point.opacity = newpoint.opacity;
}
if ('col' in newpoint) {
point.color.setRGB(newpoint.col.x, newpoint.col.y, newpoint.col.z);
color[i*4 ] = newpoint.col.x;
color[i*4 + 1] = newpoint.col.y;
color[i*4 + 2] = newpoint.col.z;
color[i*4 + 3] = elation.utils.any(point.opacity, this.properties.opacity, 1);
geo.attributes.customColor.needsUpdate = true;
}
if ('size' in newpoint) {
point.size = newpoint.size;
size[i] = newpoint.size;
geo.attributes.size.needsUpdate = true;
}
}
geo.attributes.position.needsUpdate = true;
this.updateBoundingSphere(furthest);
if (points.length >= this.properties.count) {
this.count = particles.length;
this.createParticles(false);
this.updateParticleTimer = false;
}
}
})();
}, elation.engine.things.janusbase);
});