502 lines
17 KiB
JavaScript
502 lines
17 KiB
JavaScript
/**
|
|
* ThreeJS-粒子引擎库
|
|
*/
|
|
|
|
// attribute: data that may be different for each particle (such as size and color);
|
|
// can only be used in vertex shader
|
|
// varying: used to communicate data from vertex shader to fragment shader
|
|
// uniform: data that is the same for each particle (such as texture)
|
|
|
|
particleVertexShader =
|
|
[
|
|
"attribute vec3 customColor;",
|
|
"attribute float customOpacity;",
|
|
"attribute float customSize;",
|
|
"attribute float customAngle;",
|
|
"attribute float customVisible;", // float used as boolean (0 = false, 1 = true)
|
|
"varying vec4 vColor;",
|
|
"varying float vAngle;",
|
|
"void main()",
|
|
"{",
|
|
"if ( customVisible > 0.5 )", // true
|
|
"vColor = vec4( customColor, customOpacity );", // set color associated to vertex; use later in fragment shader.
|
|
"else", // false
|
|
"vColor = vec4(0.0, 0.0, 0.0, 0.0);", // make particle invisible.
|
|
|
|
"vAngle = customAngle;",
|
|
|
|
"vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );",
|
|
"gl_PointSize = customSize * ( 300.0 / length( mvPosition.xyz ) );", // scale particles as objects in 3D space
|
|
"gl_Position = projectionMatrix * mvPosition;",
|
|
"}"
|
|
].join("\n");
|
|
|
|
particleFragmentShader =
|
|
[
|
|
"uniform sampler2D texture;",
|
|
"varying vec4 vColor;",
|
|
"varying float vAngle;",
|
|
"void main()",
|
|
"{",
|
|
"gl_FragColor = vColor;",
|
|
|
|
"float c = cos(vAngle);",
|
|
"float s = sin(vAngle);",
|
|
"vec2 rotatedUV = vec2(c * (gl_PointCoord.x - 0.5) + s * (gl_PointCoord.y - 0.5) + 0.5,",
|
|
"c * (gl_PointCoord.y - 0.5) - s * (gl_PointCoord.x - 0.5) + 0.5);", // rotate UV coordinates to rotate texture
|
|
"vec4 rotatedTexture = texture2D( texture, rotatedUV );",
|
|
"gl_FragColor = gl_FragColor * rotatedTexture;", // sets an otherwise white particle texture to desired color
|
|
"}"
|
|
].join("\n");
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
/////////////////
|
|
// TWEEN CLASS //
|
|
/////////////////
|
|
|
|
function Tween(timeArray, valueArray) {
|
|
this.times = timeArray || [];
|
|
this.values = valueArray || [];
|
|
}
|
|
|
|
Tween.prototype.lerp = function (t) {
|
|
var i = 0;
|
|
var n = this.times.length;
|
|
while (i < n && t > this.times[i])
|
|
i++;
|
|
if (i == 0) return this.values[0];
|
|
if (i == n) return this.values[n - 1];
|
|
var p = (t - this.times[i - 1]) / (this.times[i] - this.times[i - 1]);
|
|
if (this.values[0] instanceof THREE.Vector3)
|
|
return this.values[i - 1].clone().lerp(this.values[i], p);
|
|
else // its a float
|
|
return this.values[i - 1] + p * (this.values[i] - this.values[i - 1]);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
////////////////////
|
|
// PARTICLE CLASS //
|
|
////////////////////
|
|
|
|
function Particle() {
|
|
// 粒子位置
|
|
this.position = new THREE.Vector3();
|
|
// 速度
|
|
this.velocity = new THREE.Vector3(); // units per second
|
|
// 加速度
|
|
this.acceleration = new THREE.Vector3();
|
|
|
|
// 角度
|
|
this.angle = 0;
|
|
// 角速度
|
|
this.angleVelocity = 0; // degrees per second
|
|
// 角加速度
|
|
this.angleAcceleration = 0; // degrees per second, per second
|
|
|
|
// 大小
|
|
this.size = 16.0;
|
|
// 颜色
|
|
this.color = new THREE.Color();
|
|
// 透明度
|
|
this.opacity = 1.0;
|
|
// 生命周期
|
|
this.age = 0;
|
|
// 控制生死
|
|
this.alive = 0; // use float instead of boolean for shader purposes
|
|
}
|
|
|
|
Particle.prototype.update = function (dt) {
|
|
this.position.add(this.velocity.clone().multiplyScalar(dt));
|
|
this.velocity.add(this.acceleration.clone().multiplyScalar(dt));
|
|
|
|
// convert from degrees to radians: 0.01745329251 = Math.PI/180
|
|
this.angle += this.angleVelocity * 0.01745329251 * dt;
|
|
this.angleVelocity += this.angleAcceleration * 0.01745329251 * dt;
|
|
|
|
this.age += dt;
|
|
|
|
// if the tween for a given attribute is nonempty,
|
|
// then use it to update the attribute's value
|
|
|
|
if (this.sizeTween.times.length > 0)
|
|
this.size = this.sizeTween.lerp(this.age);
|
|
|
|
if (this.colorTween.times.length > 0) {
|
|
var colorHSL = this.colorTween.lerp(this.age);
|
|
this.color = new THREE.Color().setHSL(colorHSL.x, colorHSL.y, colorHSL.z);
|
|
}
|
|
|
|
if (this.opacityTween.times.length > 0)
|
|
this.opacity = this.opacityTween.lerp(this.age);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
///////////////////////////
|
|
// PARTICLE ENGINE CLASS //
|
|
///////////////////////////
|
|
|
|
var Type = Object.freeze({ "CUBE": 1, "SPHERE": 2 });
|
|
|
|
/**
|
|
* 粒子引擎
|
|
*/
|
|
function ParticleEngine(c, m) {
|
|
/////////////////////////
|
|
// PARTICLE PROPERTIES //
|
|
/////////////////////////
|
|
|
|
this.positionStyle = Type.CUBE;
|
|
this.positionBase = new THREE.Vector3();
|
|
// cube shape data
|
|
this.positionSpread = new THREE.Vector3();
|
|
// sphere shape data
|
|
this.positionRadius = 0; // distance from base at which particles start
|
|
|
|
this.velocityStyle = Type.CUBE;
|
|
// cube movement data
|
|
this.velocityBase = new THREE.Vector3();
|
|
this.velocitySpread = new THREE.Vector3();
|
|
// sphere movement data
|
|
// direction vector calculated using initial position
|
|
this.speedBase = 0;
|
|
this.speedSpread = 0;
|
|
|
|
this.accelerationBase = new THREE.Vector3();
|
|
this.accelerationSpread = new THREE.Vector3();
|
|
|
|
this.angleBase = 0;
|
|
this.angleSpread = 0;
|
|
this.angleVelocityBase = 0;
|
|
this.angleVelocitySpread = 0;
|
|
this.angleAccelerationBase = 0;
|
|
this.angleAccelerationSpread = 0;
|
|
|
|
this.sizeBase = 0.0;
|
|
this.sizeSpread = 0.0;
|
|
this.sizeTween = new Tween();
|
|
|
|
// store colors in HSL format in a THREE.Vector3 object
|
|
// http://en.wikipedia.org/wiki/HSL_and_HSV
|
|
this.colorBase = new THREE.Vector3(0.0, 1.0, 0.5);
|
|
this.colorSpread = new THREE.Vector3(0.0, 0.0, 0.0);
|
|
this.colorTween = new Tween();
|
|
|
|
this.opacityBase = 1.0;
|
|
this.opacitySpread = 0.0;
|
|
this.opacityTween = new Tween();
|
|
|
|
this.blendStyle = THREE.NormalBlending; // false;
|
|
|
|
this.particleArray = [];
|
|
this.particlesPerSecond = 100;
|
|
this.particleDeathAge = 1.0;
|
|
|
|
///////////////////////////////////
|
|
// EMITTER PROPERTIES 发射器属性 //
|
|
///////////////////////////////////
|
|
|
|
this.emitterAge = 0.0;
|
|
this.emitterAlive = true;
|
|
this.emitterDeathAge = 60; // time (seconds) at which to stop creating particles.
|
|
|
|
// How many particles could be active at any time?
|
|
this.particleCount = this.particlesPerSecond * Math.min(this.particleDeathAge, this.emitterDeathAge);
|
|
|
|
//////////////
|
|
// THREE.JS //
|
|
//////////////
|
|
|
|
// 粒子几何体
|
|
this.particleGeometry = new THREE.BufferGeometry();
|
|
this.particleGeometry.addAttribute('position', new THREE.Float32BufferAttribute([], 3));
|
|
this.particleGeometry.addAttribute('customVisible', new THREE.Float32BufferAttribute([], 1));
|
|
this.particleGeometry.addAttribute('customAngle', new THREE.Float32BufferAttribute([], 1));
|
|
this.particleGeometry.addAttribute('customSize', new THREE.Float32BufferAttribute([], 1));
|
|
this.particleGeometry.addAttribute('customColor', new THREE.Float32BufferAttribute([], 3));
|
|
this.particleGeometry.addAttribute('customOpacity', new THREE.Float32BufferAttribute([], 1));
|
|
// 粒子纹理
|
|
this.particleTexture = null;
|
|
// 粒子材质
|
|
this.particleMaterial = new THREE.ShaderMaterial(
|
|
{
|
|
uniforms:
|
|
{
|
|
texture: { type: "t", value: this.particleTexture },
|
|
},
|
|
vertexShader: particleVertexShader,
|
|
fragmentShader: particleFragmentShader,
|
|
transparent: true, // alphaTest: 0.5, // if having transparency issues, try including: alphaTest: 0.5,
|
|
blending: THREE.NormalBlending,
|
|
depthTest: true,
|
|
|
|
});
|
|
// 粒子-弃用THREE.ParticleSystem改用THREE.Points
|
|
this.particleMesh = null;
|
|
|
|
// 变换矩阵
|
|
this.trsfMatrix = m;
|
|
// 粒子位置
|
|
this.center = c;
|
|
|
|
this.scene;
|
|
}
|
|
|
|
/**
|
|
* 设置粒子属性
|
|
*/
|
|
ParticleEngine.prototype.setValues = function (parameters) {
|
|
if (parameters === undefined) return;
|
|
|
|
// clear any previous tweens that might exist
|
|
this.sizeTween = new Tween();
|
|
this.colorTween = new Tween();
|
|
this.opacityTween = new Tween();
|
|
|
|
for (var key in parameters)
|
|
this[key] = parameters[key];
|
|
|
|
// attach tweens to particles
|
|
Particle.prototype.sizeTween = this.sizeTween;
|
|
Particle.prototype.colorTween = this.colorTween;
|
|
Particle.prototype.opacityTween = this.opacityTween;
|
|
|
|
// calculate/set derived particle engine values
|
|
this.particleArray = [];
|
|
this.emitterAge = 0.0;
|
|
this.emitterAlive = true;
|
|
this.particleCount = this.particlesPerSecond * Math.min(this.particleDeathAge, this.emitterDeathAge);
|
|
|
|
this.particleGeometry = new THREE.BufferGeometry();
|
|
this.particleGeometry.addAttribute('position', new THREE.Float32BufferAttribute([], 3));
|
|
this.particleGeometry.addAttribute('customVisible', new THREE.Float32BufferAttribute([], 1));
|
|
this.particleGeometry.addAttribute('customAngle', new THREE.Float32BufferAttribute([], 1));
|
|
this.particleGeometry.addAttribute('customSize', new THREE.Float32BufferAttribute([], 1));
|
|
this.particleGeometry.addAttribute('customColor', new THREE.Float32BufferAttribute([], 3));
|
|
this.particleGeometry.addAttribute('customOpacity', new THREE.Float32BufferAttribute([], 1));
|
|
this.particleMaterial = new THREE.ShaderMaterial(
|
|
{
|
|
uniforms:
|
|
{
|
|
texture: { type: "t", value: this.particleTexture },
|
|
},
|
|
vertexShader: particleVertexShader,
|
|
fragmentShader: particleFragmentShader,
|
|
transparent: true, alphaTest: 0.5, // if having transparency issues, try including: alphaTest: 0.5,
|
|
blending: THREE.NormalBlending,
|
|
depthTest: true
|
|
});
|
|
// this.particleMesh = new THREE.ParticleSystem();
|
|
this.particleMesh = new THREE.Points(this.particleGeometry, this.particleMaterial);
|
|
}
|
|
|
|
// helper functions for randomization
|
|
ParticleEngine.prototype.randomValue = function (base, spread) {
|
|
|
|
return base + spread * (Math.random() - 0.5);
|
|
|
|
}
|
|
ParticleEngine.prototype.randomVector3 = function (base, spread) {
|
|
|
|
var rand3 = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5);
|
|
|
|
return new THREE.Vector3().addVectors(base, new THREE.Vector3().multiplyVectors(spread, rand3));
|
|
|
|
}
|
|
|
|
/**
|
|
* 创建粒子
|
|
*/
|
|
ParticleEngine.prototype.createParticle = function () {
|
|
|
|
var particle = new Particle();
|
|
|
|
if (this.positionStyle == Type.CUBE) {
|
|
|
|
particle.position = this.randomVector3(this.positionBase, this.positionSpread);
|
|
|
|
}
|
|
|
|
if (this.positionStyle == Type.SPHERE) {
|
|
|
|
var z = 2 * Math.random() - 1;
|
|
var t = 6.2832 * Math.random();
|
|
var r = Math.sqrt(1 - z * z);
|
|
var vec3 = new THREE.Vector3(r * Math.cos(t), r * Math.sin(t), z);
|
|
particle.position = new THREE.Vector3().addVectors(this.positionBase, vec3.multiplyScalar(this.positionRadius));
|
|
|
|
}
|
|
|
|
if (this.velocityStyle == Type.CUBE) {
|
|
|
|
particle.velocity = this.randomVector3(this.velocityBase, this.velocitySpread);
|
|
|
|
}
|
|
|
|
if (this.velocityStyle == Type.SPHERE) {
|
|
|
|
var direction = new THREE.Vector3().subVectors(particle.position, this.positionBase);
|
|
var speed = this.randomValue(this.speedBase, this.speedSpread);
|
|
particle.velocity = direction.normalize().multiplyScalar(speed);
|
|
|
|
}
|
|
|
|
particle.acceleration = this.randomVector3(this.accelerationBase, this.accelerationSpread);
|
|
|
|
particle.angle = this.randomValue(this.angleBase, this.angleSpread);
|
|
particle.angleVelocity = this.randomValue(this.angleVelocityBase, this.angleVelocitySpread);
|
|
particle.angleAcceleration = this.randomValue(this.angleAccelerationBase, this.angleAccelerationSpread);
|
|
|
|
particle.size = this.randomValue(this.sizeBase, this.sizeSpread);
|
|
|
|
var color = this.randomVector3(this.colorBase, this.colorSpread);
|
|
particle.color = new THREE.Color().setHSL(color.x, color.y, color.z);
|
|
|
|
particle.opacity = this.randomValue(this.opacityBase, this.opacitySpread);
|
|
|
|
particle.age = 0;
|
|
particle.alive = 0; // particles initialize as inactive
|
|
|
|
return particle;
|
|
}
|
|
|
|
/**
|
|
* 初始化
|
|
*/
|
|
ParticleEngine.prototype.initialize = function (scene) {
|
|
|
|
let p = [];
|
|
let alive = [];
|
|
let color = [];
|
|
let opacity = [];
|
|
let size = [];
|
|
let angle = [];
|
|
// link particle data with geometry/material data
|
|
for (var i = 0; i < this.particleCount; i++) {
|
|
|
|
// remove duplicate code somehow, here and in update function below.
|
|
this.particleArray[i] = this.createParticle();
|
|
p.push(this.particleArray[i].position.x);
|
|
p.push(this.particleArray[i].position.y);
|
|
p.push(this.particleArray[i].position.z);
|
|
alive.push(this.particleArray[i].alive);
|
|
color.push(this.particleArray[i].color.r);
|
|
color.push(this.particleArray[i].color.g);
|
|
color.push(this.particleArray[i].color.b);
|
|
opacity.push(this.particleArray[i].opacity);
|
|
size.push(this.particleArray[i].size);
|
|
angle.push(this.particleArray[i].angle);
|
|
|
|
}
|
|
|
|
this.particleGeometry = new THREE.BufferGeometry();
|
|
this.particleGeometry.addAttribute('position', new THREE.Float32BufferAttribute(p, 3));
|
|
this.particleGeometry.addAttribute('customVisible', new THREE.Float32BufferAttribute(alive, 1));
|
|
this.particleGeometry.addAttribute('customColor', new THREE.Float32BufferAttribute(color, 3));
|
|
this.particleGeometry.addAttribute('customOpacity', new THREE.Float32BufferAttribute(opacity, 1));
|
|
this.particleGeometry.addAttribute('customSize', new THREE.Float32BufferAttribute(size, 1));
|
|
this.particleGeometry.addAttribute('customAngle', new THREE.Float32BufferAttribute(angle, 1));
|
|
|
|
this.particleMaterial.blending = this.blendStyle;
|
|
|
|
if (this.blendStyle != THREE.NormalBlending)
|
|
this.particleMaterial.depthTest = false;
|
|
|
|
this.particleMesh = new THREE.Points(this.particleGeometry, this.particleMaterial);
|
|
this.particleMesh.dynamic = true;
|
|
this.particleMesh.sortParticles = true;
|
|
|
|
this.particleMesh.layers.set(0);
|
|
|
|
this.scene = scene;
|
|
|
|
this.scene.add(this.particleMesh);
|
|
|
|
|
|
}
|
|
|
|
ParticleEngine.prototype.update = function (dt) {
|
|
|
|
// 回收粒子索引数组
|
|
var recycleIndices = [];
|
|
|
|
// update particle data
|
|
for (var i = 0; i < this.particleCount; i++) {
|
|
if (this.particleArray[i].alive) {
|
|
this.particleArray[i].update(dt);
|
|
|
|
// check if particle should expire
|
|
// could also use: death by size<0 or alpha<0.
|
|
if (this.particleArray[i].age > this.particleDeathAge) {
|
|
this.particleArray[i].alive = 0.0;
|
|
recycleIndices.push(i);
|
|
}
|
|
|
|
this.particleGeometry.attributes.customVisible.setX(i, this.particleArray[i].alive);
|
|
this.particleGeometry.attributes.customColor.setXYZ(i, this.particleArray[i].color.r, this.particleArray[i].color.g, this.particleArray[i].color.b);
|
|
this.particleGeometry.attributes.customOpacity.setX(i, this.particleArray[i].opacity);
|
|
this.particleGeometry.attributes.customSize.setX(i, this.particleArray[i].size);
|
|
this.particleGeometry.attributes.customAngle.setX(i, this.particleArray[i].angle);
|
|
|
|
this.particleGeometry.attributes.customVisible.needsUpdate = true;
|
|
this.particleGeometry.attributes.customColor.needsUpdate = true;
|
|
this.particleGeometry.attributes.customOpacity.needsUpdate = true;
|
|
this.particleGeometry.attributes.customSize.needsUpdate = true;
|
|
this.particleGeometry.attributes.customAngle.needsUpdate = true;
|
|
|
|
// 坐标系转换
|
|
let tempPos = this.particleArray[i].position.clone();
|
|
let pT = new THREE.Vector3(tempPos.x - this.center.x, tempPos.y - this.center.y, tempPos.z - this.center.z);
|
|
pT.applyAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI/2);
|
|
let p = pT.applyMatrix4(this.trsfMatrix);
|
|
|
|
this.particleGeometry.attributes.position.setXYZ(i, p.x, p.y, p.z);
|
|
this.particleGeometry.attributes.position.needsUpdate = true;
|
|
}
|
|
}
|
|
|
|
// check if particle emitter is still running
|
|
if (!this.emitterAlive) return;
|
|
|
|
// if no particles have died yet, then there are still particles to activate
|
|
if (this.emitterAge < this.particleDeathAge) {
|
|
// determine indices of particles to activate
|
|
var startIndex = Math.round(this.particlesPerSecond * (this.emitterAge + 0));
|
|
var endIndex = Math.round(this.particlesPerSecond * (this.emitterAge + dt));
|
|
if (endIndex > this.particleCount)
|
|
endIndex = this.particleCount;
|
|
|
|
for (var i = startIndex; i < endIndex; i++)
|
|
this.particleArray[i].alive = 1.0;
|
|
}
|
|
|
|
// if any particles have died while the emitter is still running, we imediately recycle them
|
|
for (var j = 0; j < recycleIndices.length; j++) {
|
|
|
|
var i = recycleIndices[j];
|
|
this.particleArray[i] = this.createParticle();
|
|
this.particleArray[i].alive = 1.0; // activate right away
|
|
|
|
// 坐标系转换
|
|
let tempPos = this.particleArray[i].position.clone();
|
|
let pT = new THREE.Vector3(tempPos.x - this.center.x, tempPos.y - this.center.y, tempPos.z - this.center.z);
|
|
pT.applyAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI/2);
|
|
let p = pT.applyMatrix4(this.trsfMatrix);
|
|
|
|
this.particleGeometry.attributes.position.setXYZ(i, p.x, p.y, p.z);
|
|
this.particleGeometry.attributes.position.needsUpdate = true;
|
|
|
|
}
|
|
|
|
// stop emitter?
|
|
this.emitterAge += dt;
|
|
if (this.emitterAge > this.emitterDeathAge) this.emitterAlive = false;
|
|
}
|
|
|
|
ParticleEngine.prototype.destroy = function () {
|
|
this.scene.remove(this.particleMesh);
|
|
}
|
|
///////////////////////////////////////////////////////////////////////////////
|