three.tdl.particle.system
Version:
GPU based particle system for three.js. Heavily based on tdl library (https://github.com/greggman/tdl)
354 lines (235 loc) • 12.4 kB
JavaScript
// source: https://github.com/greggman/tdl/blob/master/tdl/particles.js
// ported to three.js by fazeaction
import {
Mesh,
BufferAttribute,
BufferGeometry,
InterleavedBufferAttribute,
InstancedInterleavedBuffer,
InstancedBufferGeometry,
InterleavedBuffer,
NormalBlending,
Vector3,
Vector4,
ShaderMaterial,
DoubleSide,
FrontSide,
DynamicDrawUsage
} from 'three'
import * as Constants from './constants.js'
import {ParticleSpec} from './particle-spec.js'
import {OneShot} from './one-shot.js'
import billboardParticleInstancedVertexShader from './../shaders/particles-billboard-instanced_vs.glsl'
import orientedParticleInstancedVertexShader from './../shaders/particles-oriented-instanced_vs.glsl'
import particleFragmentShader from './../shaders/particles_fs.glsl'
class ParticleEmitter extends Mesh {
constructor( particleSystem, opt_texture, opt_clock ) {
super();
opt_clock = opt_clock || particleSystem.timeSource_;
//TODO make alternative to instanced buffer
//this.particleBuffer_ = new THREE.BufferGeometry();
//this.indexBuffer_ = [];
this.particleBuffer_ = new InstancedBufferGeometry();
this.interleavedBuffer = new InterleavedBuffer();
this.numParticles_ = 0;
this.rampTexture_ = particleSystem.defaultRampTexture;
this.colorTexture_ = opt_texture || particleSystem.defaultColorTexture;
this.particleSystem = particleSystem;
this.timeSource_ = opt_clock;
this.setState(NormalBlending);
}
setTranslation ( x, y, z ) {
this.position.x = x;
this.position.y = y;
this.position.z = z;
}
setState ( stateId ) {
this.blendFunc_ = stateId;
}
setColorRamp ( colorRamp ) {
var width = colorRamp.length / 4;
if (width % 1 != 0) {
throw 'colorRamp must have multiple of 4 entries';
}
if (this.rampTexture_ == this.particleSystem.defaultRampTexture) {
this.rampTexture_ = null;
}
this.rampTexture_ = this.particleSystem.createTextureFromFloats( width, 1, colorRamp, this.rampTexture_ );
}
validateParameters ( parameters ) {
var defaults = new ParticleSpec();
for ( var key in parameters ) {
if ( typeof defaults[ key ] === 'undefined' ) {
throw 'unknown particle parameter "' + key + '"';
}
}
for ( var key in defaults ) {
if ( typeof parameters[ key ] === 'undefined' ) {
parameters[ key ] = defaults[ key ];
}
}
}
createParticles_ ( firstParticleIndex, numParticles, parameters, opt_perParticleParamSetter ) {
var interleaveBufferData = this.interleavedBuffer.array;
this.billboard_ = parameters.billboard;
var random = this.particleSystem.randomFunction_;
var plusMinus = function ( range ) {
return ( random() - 0.5 ) * range * 2;
};
// TODO: change to not allocate.
var plusMinusVector = function ( range ) {
var v = [];
for (var ii = 0; ii < range.length; ++ ii) {
v.push( plusMinus( range[ ii ] ) );
}
return v;
};
for ( var ii = 0; ii < numParticles; ++ ii ) {
if ( opt_perParticleParamSetter ) {
opt_perParticleParamSetter( ii, parameters );
}
var pLifeTime = parameters.lifeTime;
var pStartTime = ( parameters.startTime === null ) ? ( ii * parameters.lifeTime / numParticles ) : parameters.startTime;
var pFrameStart = parameters.frameStart + plusMinus(parameters.frameStartRange);
var pPosition = new Vector3().addVectors( new Vector3().fromArray(parameters.position), new Vector3().fromArray(plusMinusVector(parameters.positionRange)));
var pVelocity = new Vector3().addVectors( new Vector3().fromArray(parameters.velocity), new Vector3().fromArray(plusMinusVector(parameters.velocityRange)));
var pAcceleration = new Vector3().addVectors( new Vector3().fromArray(parameters.acceleration), new Vector3().fromArray( plusMinusVector( parameters.accelerationRange )));
var pColorMult = new Vector4().addVectors( new Vector4().fromArray(parameters.colorMult), new Vector4().fromArray(plusMinusVector( parameters.colorMultRange )));
var pSpinStart = parameters.spinStart + plusMinus(parameters.spinStartRange);
var pSpinSpeed = parameters.spinSpeed + plusMinus(parameters.spinSpeedRange);
var pStartSize = parameters.startSize + plusMinus(parameters.startSizeRange);
var pEndSize = parameters.endSize + plusMinus(parameters.endSizeRange);
var pOrientation = new Vector4().fromArray(parameters.orientation);
for (var jj = 0; jj < 1; ++jj) {
var offset0 = Constants.LAST_IDX * jj + ( ii * Constants.LAST_IDX * 4 ) + ( firstParticleIndex * Constants.LAST_IDX * 4 );
var offset1 = offset0 + 1;
var offset2 = offset0 + 2;
var offset3 = offset0 + 3;
interleaveBufferData[Constants.POSITION_START_TIME_IDX + offset0] = pPosition.x;
interleaveBufferData[Constants.POSITION_START_TIME_IDX + offset1] = pPosition.y;
interleaveBufferData[Constants.POSITION_START_TIME_IDX + offset2] = pPosition.z;
interleaveBufferData[Constants.POSITION_START_TIME_IDX + offset3] = pStartTime;
interleaveBufferData[Constants.UV_LIFE_TIME_FRAME_START_IDX + offset0] = Constants.CORNERS_[jj][0];
interleaveBufferData[Constants.UV_LIFE_TIME_FRAME_START_IDX + offset1] = Constants.CORNERS_[jj][1];
interleaveBufferData[Constants.UV_LIFE_TIME_FRAME_START_IDX + offset2] = pLifeTime;
interleaveBufferData[Constants.UV_LIFE_TIME_FRAME_START_IDX + offset3] = pFrameStart;
interleaveBufferData[Constants.VELOCITY_START_SIZE_IDX + offset0] = pVelocity.x;
interleaveBufferData[Constants.VELOCITY_START_SIZE_IDX + offset1] = pVelocity.y;
interleaveBufferData[Constants.VELOCITY_START_SIZE_IDX + offset2] = pVelocity.z;
interleaveBufferData[Constants.VELOCITY_START_SIZE_IDX + offset3] = pStartSize;
interleaveBufferData[Constants.ACCELERATION_END_SIZE_IDX + offset0] = pAcceleration.x;
interleaveBufferData[Constants.ACCELERATION_END_SIZE_IDX + offset1] = pAcceleration.y;
interleaveBufferData[Constants.ACCELERATION_END_SIZE_IDX + offset2] = pAcceleration.z;
interleaveBufferData[Constants.ACCELERATION_END_SIZE_IDX + offset3] = pEndSize;
interleaveBufferData[Constants.SPIN_START_SPIN_SPEED_IDX + offset0] = pSpinStart;
interleaveBufferData[Constants.SPIN_START_SPIN_SPEED_IDX + offset1] = pSpinSpeed;
interleaveBufferData[Constants.SPIN_START_SPIN_SPEED_IDX + offset2] = 0;
interleaveBufferData[Constants.SPIN_START_SPIN_SPEED_IDX + offset3] = 0;
interleaveBufferData[Constants.ORIENTATION_IDX + offset0] = pOrientation.x;
interleaveBufferData[Constants.ORIENTATION_IDX + offset1] = pOrientation.y;
interleaveBufferData[Constants.ORIENTATION_IDX + offset2] = pOrientation.z;
interleaveBufferData[Constants.ORIENTATION_IDX + offset3] = pOrientation.w;
interleaveBufferData[Constants.COLOR_MULT_IDX + offset0] = pColorMult.x;
interleaveBufferData[Constants.COLOR_MULT_IDX + offset1] = pColorMult.y;
interleaveBufferData[Constants.COLOR_MULT_IDX + offset2] = pColorMult.z;
interleaveBufferData[Constants.COLOR_MULT_IDX + offset3] = pColorMult.w;
}
}
this.interleavedBuffer.needsUpdate = true;
this.material.uniforms.worldVelocity.value = new Vector3(parameters.worldVelocity[0], parameters.worldVelocity[1], parameters.worldVelocity[2]);
this.material.uniforms.worldAcceleration.value = new Vector3(parameters.worldAcceleration[0], parameters.worldAcceleration[1], parameters.worldAcceleration[2]);
this.material.uniforms.timeRange.value = parameters.timeRange;
this.material.uniforms.frameDuration.value = parameters.frameDuration;
this.material.uniforms.numFrames.value = parameters.numFrames;
this.material.uniforms.rampSampler.value = this.rampTexture_;
this.material.uniforms.colorSampler.value = this.colorTexture_;
this.material.blending = this.blendFunc_;
}
allocateParticles_ ( numParticles, parameters ) {
if ( this.numParticles_ != numParticles ) {
var numIndices = 6 * numParticles;
if (numIndices > 65536 && BufferGeometry.MaxIndex < 65536) {
throw "can't have more than 10922 particles per emitter";
}
var vertexBuffer = new InterleavedBuffer( new Float32Array([
// Front
0, 0, 0, 0, -0.5, -0.5, 0, 0,
0, 0, 0, 0, 0.5, -0.5, 0, 0,
0, 0, 0, 0, 0.5, 0.5, 0, 0,
0, 0, 0, 0, -0.5, 0.5, 0, 0
]), 8);
// Use vertexBuffer, starting at offset 0, 3 items in position attribute
var positions = new InterleavedBufferAttribute( vertexBuffer, 3, 0 );
this.particleBuffer_.setAttribute( 'position', positions );
// Use vertexBuffer, starting at offset 4, 2 items in uv attribute
var uvs = new InterleavedBufferAttribute( vertexBuffer, 2, 4 );
this.particleBuffer_.setAttribute( 'uv', uvs );
var indices = new Uint16Array([
0, 1, 2,
0, 2, 3
]);
this.particleBuffer_.setIndex( new BufferAttribute( indices, 1 ) );
this.numParticles_ = numParticles;
this.interleavedBuffer = new InstancedInterleavedBuffer( new Float32Array( numParticles * Constants.singleParticleArray_.byteLength ), Constants.LAST_IDX, 1 ).setUsage( DynamicDrawUsage );
this.particleBuffer_.setAttribute( 'position', new InterleavedBufferAttribute(this.interleavedBuffer, 3, Constants.POSITION_START_TIME_IDX));
this.particleBuffer_.setAttribute( 'startTime', new InterleavedBufferAttribute(this.interleavedBuffer, 1, 3));
this.particleBuffer_.setAttribute( 'uvLifeTimeFrameStart', new InterleavedBufferAttribute(this.interleavedBuffer, 4, Constants.UV_LIFE_TIME_FRAME_START_IDX));
this.particleBuffer_.setAttribute( 'velocityStartSize', new InterleavedBufferAttribute(this.interleavedBuffer, 4, Constants.VELOCITY_START_SIZE_IDX));
this.particleBuffer_.setAttribute( 'accelerationEndSize', new InterleavedBufferAttribute(this.interleavedBuffer, 4, Constants.ACCELERATION_END_SIZE_IDX));
this.particleBuffer_.setAttribute( 'spinStartSpinSpeed', new InterleavedBufferAttribute(this.interleavedBuffer, 4, Constants.SPIN_START_SPIN_SPEED_IDX));
this.particleBuffer_.setAttribute( 'orientation', new InterleavedBufferAttribute(this.interleavedBuffer, 4, Constants.ORIENTATION_IDX));
this.particleBuffer_.setAttribute( 'colorMult', new InterleavedBufferAttribute(this.interleavedBuffer, 4, Constants.COLOR_MULT_IDX));
this.particleBuffer_.computeBoundingSphere();
var uniforms = {
//world: { type: 'm4', value: this.matrixWorld },
viewInverse: { type: 'm4', value: this.particleSystem.camera.matrixWorld },
worldVelocity: { type: 'v3', value: null },
worldAcceleration: { type: 'v3', value: null },
timeRange: { type: 'f', value: null },
time: { type: 'f', value: null },
timeOffset: { type: 'f', value: null },
frameDuration: { type: 'f', value: null },
numFrames: { type: 'f', value: null },
rampSampler: { type: "t", value: this.rampTexture_ },
colorSampler: { type: "t", value: this.colorTexture_ }
};
var material = new ShaderMaterial({
uniforms: uniforms,
vertexShader: ( parameters.billboard ) ? billboardParticleInstancedVertexShader : orientedParticleInstancedVertexShader,
fragmentShader: particleFragmentShader,
side: (this.billboard_)? DoubleSide : FrontSide,
blending: this.blendFunc_,
depthTest: true,
depthWrite: false,
transparent: true
});
this.geometry = this.particleBuffer_;
this.material = material;
}
}
setParameters ( parameters, opt_perParticleParamSetter ) {
this.validateParameters ( parameters );
var numParticles = parameters.numParticles;
this.allocateParticles_ ( numParticles, parameters );
this.createParticles_ ( 0, numParticles, parameters, opt_perParticleParamSetter );
}
draw ( world, viewProjection, timeOffset ) {
var uniforms = this.material.uniforms;
uniforms.time.value = this.timeSource_();
uniforms.timeOffset.value = timeOffset;
}
createOneShot () {
return new OneShot( this, this.particleSystem.scene );
}
clone ( object ) {
if ( object === undefined ) object = this.particleSystem.createParticleEmitter( this.colorTexture_, this.timeSource_);
object.geometry = this.geometry;
object.material = this.material.clone();
object.material.uniforms.viewInverse.value = this.particleSystem.camera.matrixWorld;
object.material.uniforms.rampSampler.value = this.rampTexture_;
object.material.uniforms.colorSampler.value = this.colorTexture_;
super.copy( object );
return object;
}
}
export { ParticleEmitter }