ParticleEngine.js实现烟雾效果
参考网址:http://stemkoski.github.io/Three.js/Particle-Engine.html
ParticleEngine.js源码依赖的three.js版本是60,而我使用的three.js的版本是112,新版本不支持为ShaderMaterial设置attributes,所以修改了ParticleEngine.js源码。
原始版本ParticleEngine.js代码:
/** * @author Lee Stemkoski http://www.adelphi.edu/~stemkoski/ */ /////////////////////////////////////////////////////////////////////////////// ///////////// // SHADERS // ///////////// // 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() { ///////////////////////// // 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.Geometry(); this.particleTexture = null; this.particleMaterial = new THREE.ShaderMaterial( { uniforms: { texture: { type: "t", value: this.particleTexture }, }, attributes: { customVisible: { type: 'f', value: [] }, customAngle: { type: 'f', value: [] }, customSize: { type: 'f', value: [] }, customColor: { type: 'c', value: [] }, customOpacity: { type: 'f', value: [] } }, 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.Mesh(); } 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.Geometry(); this.particleMaterial = new THREE.ShaderMaterial( { uniforms: { texture: { type: "t", value: this.particleTexture }, }, attributes: { customVisible: { type: 'f', value: [] }, customAngle: { type: 'f', value: [] }, customSize: { type: 'f', value: [] }, customColor: { type: 'c', value: [] }, customOpacity: { type: 'f', value: [] } }, 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(); } // 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() { // 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(); this.particleGeometry.vertices[i] = this.particleArray[i].position; this.particleMaterial.attributes.customVisible.value[i] = this.particleArray[i].alive; this.particleMaterial.attributes.customColor.value[i] = this.particleArray[i].color; this.particleMaterial.attributes.customOpacity.value[i] = this.particleArray[i].opacity; this.particleMaterial.attributes.customSize.value[i] = this.particleArray[i].size; this.particleMaterial.attributes.customAngle.value[i] = this.particleArray[i].angle; } this.particleMaterial.blending = this.blendStyle; if ( this.blendStyle != THREE.NormalBlending) this.particleMaterial.depthTest = false; this.particleMesh = new THREE.ParticleSystem( this.particleGeometry, this.particleMaterial ); this.particleMesh.dynamic = true; this.particleMesh.sortParticles = true; 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); } // update particle properties in shader this.particleMaterial.attributes.customVisible.value[i] = this.particleArray[i].alive; this.particleMaterial.attributes.customColor.value[i] = this.particleArray[i].color; this.particleMaterial.attributes.customOpacity.value[i] = this.particleArray[i].opacity; this.particleMaterial.attributes.customSize.value[i] = this.particleArray[i].size; this.particleMaterial.attributes.customAngle.value[i] = this.particleArray[i].angle; } } // 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 this.particleGeometry.vertices[i] = this.particleArray[i].position; } // stop emitter? this.emitterAge += dt; if ( this.emitterAge > this.emitterDeathAge ) this.emitterAlive = false; } ParticleEngine.prototype.destroy = function() { scene.remove( this.particleMesh ); } ///////////////////////////////////////////////////////////////////////////////
修改后的ParticleEngine.js代码:
/** * @author Lee Stemkoski http://www.adelphi.edu/~stemkoski/ */ /////////////////////////////////////////////////////////////////////////////// ///////////// // SHADERS // ///////////// // 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) import * as THREE from '../../build/three.module.js'; let 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"); let 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 ParticleTween(timeArray, valueArray) { this.times = timeArray || []; this.values = valueArray || []; } ParticleTween.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 // /////////////////////////// let Type = Object.freeze({ "CUBE": 1, "SPHERE": 2 }); let scene; function ParticleEngine(scene_) { ///////////////////////// // PARTICLE PROPERTIES // ///////////////////////// scene = scene_; 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 ParticleTween(); // 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 ParticleTween(); this.opacityBase = 1.0; this.opacitySpread = 0.0; this.opacityTween = new ParticleTween(); 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.particleTexture = null; this.particleMaterial = new THREE.ShaderMaterial( { uniforms: { texture: { type: "t", value: this.particleTexture }, }, vertexShader: particleVertexShader, fragmentShader: particleFragmentShader, blending: THREE.NormalBlending, depthTest: false, side: THREE.DoubleSide, transparent: true, // alphaTest: 0.5, // if having transparency issues, try including: alphaTest: 0.5, opacity: 1.0 }); this.particleMesh = new THREE.Mesh(); } ParticleEngine.prototype.setValues = function (parameters) { if (parameters === undefined) return; // clear any previous tweens that might exist this.sizeTween = new ParticleTween(); this.colorTween = new ParticleTween(); this.opacityTween = new ParticleTween(); 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.Geometry(); this.particleMaterial = new THREE.ShaderMaterial( { uniforms: { texture: { type: "t", value: this.particleTexture }, }, vertexShader: particleVertexShader, fragmentShader: particleFragmentShader, blending: THREE.NormalBlending, depthTest: false, side: THREE.DoubleSide, transparent: true, // alphaTest: 0.5, // if having transparency issues, try including: alphaTest: 0.5, opacity: 1.0 }); this.particleMesh = new THREE.Points(); } // 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 () { let customVisible = []; let customColor = []; let customOpacity = []; let customSize = []; let customAngle = []; // 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(); this.particleGeometry.vertices[i] = this.particleArray[i].position; customVisible.push(this.particleArray[i].alive); customColor.push(this.particleArray[i].color); customOpacity.push(this.particleArray[i].opacity); customSize.push(this.particleArray[i].size); customAngle.push(this.particleArray[i].angle); } // 原ParticleEngine.js依赖的是旧版本的three.js,新版本的three.js的ShaderMaterial不支持attributes // 此处使用BufferGeometry作了修改 for (var i = 0; i < this.particleCount - 2; i += 3) { let face = new THREE.Face3(i, i + 1, i + 2); this.particleGeometry.faces.push(face); } this.bufferGeometry = new THREE.BufferGeometry(); this.bufferGeometry.fromGeometry(this.particleGeometry); this.bufferGeometry.setAttribute('customVisible', new THREE.BufferAttribute(new Float32Array(customVisible), 1)); this.bufferGeometry.setAttribute('customColor', new THREE.BufferAttribute(new Float32Array(customColor.length * 3), 3).copyColorsArray(customColor)); this.bufferGeometry.setAttribute('customOpacity', new THREE.BufferAttribute(new Float32Array(customOpacity), 1)); this.bufferGeometry.setAttribute('customSize', new THREE.BufferAttribute(new Float32Array(customSize), 1)); this.bufferGeometry.setAttribute('customAngle', new THREE.BufferAttribute(new Float32Array(customAngle), 1)); this.particleMaterial.blending = this.blendStyle; if (this.blendStyle != THREE.NormalBlending) this.particleMaterial.depthTest = false; this.particleMesh = new THREE.Points(this.bufferGeometry, this.particleMaterial); this.particleMesh.dynamic = true; this.particleMesh.sortParticles = true; scene.add(this.particleMesh); this.isShow = true; } ParticleEngine.prototype.update = function (dt) { var recycleIndices = []; let customVisible = []; let customColor = []; let customOpacity = []; let customSize = []; let customAngle = []; // update particle data for (var i = 0; i < this.particleCount; i++) { if (this.particleArray[i].alive) { this.particleArray[i].update(dt); this.particleGeometry.vertices[i] = this.particleArray[i].position; // 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); } // update particle properties in shader customVisible.push(this.particleArray[i].alive); customColor.push(this.particleArray[i].color); customOpacity.push(this.particleArray[i].opacity); customSize.push(this.particleArray[i].size); customAngle.push(this.particleArray[i].angle); } } // bufferGeometry需要更新 this.bufferGeometry.fromGeometry(this.particleGeometry); this.bufferGeometry.getAttribute('customVisible').copyArray(customVisible); this.bufferGeometry.getAttribute('customColor').copyColorsArray(customColor); this.bufferGeometry.getAttribute('customOpacity').copyArray(customOpacity); this.bufferGeometry.getAttribute('customSize').copyArray(customSize); this.bufferGeometry.getAttribute('customAngle').copyArray(customAngle); this.bufferGeometry.getAttribute('customVisible').needsUpdate = true; this.bufferGeometry.getAttribute('customColor').needsUpdate = true; this.bufferGeometry.getAttribute('customOpacity').needsUpdate = true; this.bufferGeometry.getAttribute('customSize').needsUpdate = true; this.bufferGeometry.getAttribute('customAngle').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 this.particleGeometry.vertices[i] = this.particleArray[i].position; } // stop emitter? this.emitterAge += dt; if (this.emitterAge > this.emitterDeathAge) this.emitterAlive = false; } ParticleEngine.prototype.destroy = function () { scene.remove(this.particleMesh); this.isShow = false; } ParticleEngine.prototype.show = function () { scene.add(this.particleMesh); this.isShow = true; } ParticleEngine.prototype.hide = function () { scene.remove(this.particleMesh); this.isShow = false; } /////////////////////////////////////////////////////////////////////////////// export { ParticleEngine, Type, ParticleTween }
MySmoke.js创建烟雾代码:
//消防烟雾效果 import * as THREE from '../build/three.module.js'; import { ParticleEngine, Type as ParticleType, ParticleTween } from '../js/particle-engine/ParticleEngine.js' let particleEngine; let clockForParticleEngine = new THREE.Clock(); let scene; let position; /** 创建烟雾 */ function createSmoke(scene_, position_) { scene = scene_; position = position_; if (particleEngine) { particleEngine.destroy(); } particleEngine = new ParticleEngine(scene); particleEngine.setValues({ positionStyle: ParticleType.CUBE, positionBase: new THREE.Vector3(position.x, position.y + 0, position.z), positionSpread: new THREE.Vector3(10, 0, 10), velocityStyle: ParticleType.CUBE, velocityBase: new THREE.Vector3(0, 200, 0), velocitySpread: new THREE.Vector3(200, 150, 200), accelerationBase: new THREE.Vector3(0, -100, 0), particleTexture: new THREE.TextureLoader().load('images/particle-engine/smokeparticle.png'), angleBase: 0, angleSpread: 720, angleVelocityBase: 0, angleVelocitySpread: 720, sizeTween: new ParticleTween([0, 1], [64, 256]), opacityTween: new ParticleTween([0.8, 2], [0.5, 0]), colorTween: new ParticleTween([0.4, 1], [new THREE.Vector3(0, 0, 0.2), new THREE.Vector3(0, 0, 0.5)]), particlesPerSecond: 200, particleDeathAge: 2.0, emitterDeathAge: 60 }); particleEngine.initialize(); } /** 更新烟雾 */ function updateParticle() { let delta = clockForParticleEngine.getDelta(); if (delta < 0.2) { // 1秒 / 5帧 = 0.2 秒/帧 particleEngine && particleEngine.update(delta * 0.5); } else { if (scene && position) { let isShow = false; if (particleEngine && particleEngine.isShow) { isShow = true; } createSmoke(scene, position); if (!isShow) { particleEngine && particleEngine.hide(); } } } } export { createSmoke, updateParticle, particleEngine }
火焰效果MyFire3.js代码:
/** * 火焰 */ import * as THREE from '../build/three.module.js'; let MyFire3 = function () { let fireVertexShader = ` attribute vec4 orientation; attribute vec3 offset; attribute vec2 scale; attribute float life; attribute float random; varying vec2 vUv; varying float vRandom; varying float vAlpha; float range(float oldValue, float oldMin, float oldMax, float newMin, float newMax) { float oldRange = oldMax - oldMin; float newRange = newMax - newMin; return (((oldValue - oldMin) * newRange) / oldRange) + newMin; } // From Inigo Quilez http://www.iquilezles.org/www/articles/functions/functions.htm float pcurve(float x, float a, float b) { float k = pow(a + b, a + b) / (pow(a, a) * pow(b, b)); return k * pow(x, a) * pow(1.0 - x, b); } void main() { vUv = uv; vRandom = random; vAlpha = pcurve(life, 1.0, 2.0); vec3 pos = position; pos.xy *= scale * vec2(range(pow(life, 1.5), 0.0, 1.0, 1.0, 0.6), range(pow(life, 1.5), 0.0, 1.0, 0.6, 1.2)); vec4 or = orientation; vec3 vcV = cross(or.xyz, pos); pos = vcV * (2.0 * or.w) + (cross(or.xyz, vcV) * 2.0 + pos); gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); } `; let fireFragmentShader = ` uniform sampler2D uMap; uniform vec3 uColor1; uniform vec3 uColor2; uniform float uTime; varying vec2 vUv; varying float vAlpha; varying float vRandom; void main() { vec2 uv = vUv; float spriteLength = 10.0; uv.x /= spriteLength; float spriteIndex = mod(uTime * 0.1 + vRandom * 2.0, 1.0); uv.x += floor(spriteIndex * spriteLength) / spriteLength; vec4 map = texture2D(uMap, uv); gl_FragColor.rgb = mix(uColor2, uColor1, map.r); gl_FragColor.a = vAlpha * map.a; } `; let embersVertexShader = ` attribute float size; attribute float life; attribute vec3 offset; varying float vAlpha; // From Inigo Quilez http://www.iquilezles.org/www/articles/functions/functions.htm float impulse(float k, float x) { float h = k * x; return h * exp(1.0 - h); } void main() { vAlpha = impulse(6.28, life); vec3 pos = position; pos += offset * vec3(life * 0.7 + 0.3, life * 0.9 + 0.1, life * 0.7 + 0.3); vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); gl_PointSize = size * (80.0 / length(mvPosition.xyz)); gl_Position = projectionMatrix * mvPosition; } `; let embersFragmentShader = ` uniform sampler2D uMap; uniform vec3 uColor; varying float vAlpha; void main() { vec2 uv = vec2(gl_PointCoord.x, 1.0 - gl_PointCoord.y); vec4 mask = texture2D(uMap, uv); gl_FragColor.rgb = uColor; gl_FragColor.a = mask.a * vAlpha * 0.8; } `; let hazeVertexShader = ` attribute vec3 base; attribute vec3 offset; attribute vec4 orientation; attribute vec2 scale; attribute float life; varying float vAlpha; varying vec2 vUv; // From Inigo Quilez http://www.iquilezles.org/www/articles/functions/functions.htm float impulse(float k, float x) { float h = k * x; return h * exp(1.0 - h); } float pcurve(float x, float a, float b) { float k = pow(a + b, a + b) / (pow(a, a) * pow(b, b)); return k * pow(x, a) * pow(1.0 - x, b); } void main() { vUv = uv; vAlpha = pcurve(life, 1.0, 2.0); vec3 pos = position; pos.xy *= scale * (life * 0.7 + 0.3); vec4 or = orientation; vec3 vcV = cross(or.xyz, pos); pos = vcV * (2.0 * or.w) + (cross(or.xyz, vcV) * 2.0 + pos); pos += base; pos += offset * vec3(life * 0.7 + 0.3, life * 0.9 + 0.1, life * 0.7 + 0.3); gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);; } `; let hazeFragmentShader = ` uniform sampler2D uMap; uniform sampler2D uMask; uniform vec2 uResolution; varying float vAlpha; varying vec2 vUv; void main() { vec2 uv = gl_FragCoord.xy / uResolution; vec2 mask = texture2D(uMask, vUv).ra - vec2(0.5); uv -= mask * 0.1; vec4 tex = texture2D(uMap, uv); gl_FragColor.rgb = tex.rgb; gl_FragColor.a = vAlpha * 0.5; } `; function random(min, max, precision) { var p = Math.pow(10, precision); return Math.round((min + Math.random() * (max - min)) * p) / p; } let _scene; let _renderer; let _camera; let _controls; let _rtt; let _fire; let _width; let _height; this.objs = []; let _self = this; this._isShow = false; let _pos_x; let _pos_y; let _pos_z; this.config = function (scene_, renderer_, camera_, controls_) { _width = 1920; _height = 1040; _renderer = renderer_; _scene = scene_; _camera = camera_; _controls = controls_; var _parameters = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat, stencilBuffer: false }; _rtt = new THREE.WebGLRenderTarget(_width * 0.5, _height * 0.5, _parameters); } this.setPosition = function (x, y, z) { _pos_x = x; _pos_y = y; _pos_z = z; } this.showFire = function () { initFire(); initEmbers(); initHaze(); this._isShow = true; } this.refresh = function () { _self.loop(); _self.loop2(); _self.loop3(); } this.isShow = function () { return this._isShow; } this.hide = function () { _self.objs.map(obj => { _scene.remove(obj); }); this._isShow = false; } this.show = function () { _self.objs.map(obj => { _scene.add(obj); }); this._isShow = true; } //=====// Fire //========================================// function initFire() { var _geometry, _shader, _mesh, _group; var _num = 50; var _x = new THREE.Vector3(1, 0, 0); var _y = new THREE.Vector3(0, 1, 0); var _z = new THREE.Vector3(0, 0, 1); var _tipTarget = new THREE.Vector3(); var _tip = new THREE.Vector3(); var _diff = new THREE.Vector3(); var _quat = new THREE.Quaternion(); var _quat2 = new THREE.Quaternion(); (function () { initGeometry(); initInstances(); initShader(); initMesh(); })(); function initGeometry() { _geometry = new THREE.InstancedBufferGeometry(); _geometry.maxInstancedCount = _num; var shape = new THREE.PlaneBufferGeometry(200, 200); shape.translate(0, 0.4, 0); var data = shape.attributes; _geometry.addAttribute('position', new THREE.BufferAttribute(new Float32Array(data.position.array), 3)); _geometry.addAttribute('uv', new THREE.BufferAttribute(new Float32Array(data.uv.array), 2)); _geometry.addAttribute('normal', new THREE.BufferAttribute(new Float32Array(data.normal.array), 3)); _geometry.setIndex(new THREE.BufferAttribute(new Uint16Array(shape.index.array), 1)); shape.dispose(); } function initInstances() { var orientation = new THREE.InstancedBufferAttribute(new Float32Array(_num * 4), 4); var randoms = new THREE.InstancedBufferAttribute(new Float32Array(_num), 1); var scale = new THREE.InstancedBufferAttribute(new Float32Array(_num * 2), 2); var life = new THREE.InstancedBufferAttribute(new Float32Array(_num), 1); for (let i = 0; i < _num; i++) { orientation.setXYZW(i, 0, 0, 0, 1); life.setX(i, i / _num + 1); } _geometry.addAttribute('orientation', orientation); _geometry.addAttribute('scale', scale); _geometry.addAttribute('life', life); _geometry.addAttribute('random', randoms); } function initShader() { var uniforms = { uMap: { type: 't', value: null }, uColor1: { type: 'c', value: new THREE.Color(0x961800) }, // red uColor2: { type: 'c', value: new THREE.Color(0x4b5828) }, // yellow uTime: { type: 'f', value: 0 }, }; _shader = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: fireVertexShader, fragmentShader: fireFragmentShader, blending: THREE.AdditiveBlending, transparent: true, depthTest: false, side: THREE.DoubleSide, }); var textureLoader = new THREE.TextureLoader(); textureLoader.load('images/myFire3/flame.png', t => _shader.uniforms.uMap.value = t); } function initMesh() { _group = new THREE.Group(); _mesh = new THREE.Mesh(_geometry, _shader); _mesh.frustumCulled = false; _group.add(_mesh); _scene.add(_group); _self.objs.push(_group); _fire = _group; } _self.loop = function () { let e = 100; _shader.uniforms.uTime.value = e * 0.001; var life = _geometry.attributes.life; var orientation = _geometry.attributes.orientation; var scale = _geometry.attributes.scale; var randoms = _geometry.attributes.random; for (let i = 0; i < _num; i++) { var value = life.array[i]; value += 0.04; if (value > 1) { value -= 1; _quat.setFromAxisAngle(_y, random(0, 3.14, 3)); _quat2.setFromAxisAngle(_x, random(-1, 1, 2) * 0.1); _quat.multiply(_quat2); _quat2.setFromAxisAngle(_z, random(-1, 1, 2) * 0.3); _quat.multiply(_quat2); orientation.setXYZW(i, _quat.x, _quat.y, _quat.z, _quat.w); scale.setXY(i, random(0.8, 1.2, 3), random(0.8, 1.2, 3)); randoms.setX(i, random(0, 1, 3)); } life.setX(i, value); } life.needsUpdate = true; orientation.needsUpdate = true; scale.needsUpdate = true; randoms.needsUpdate = true; _group.position.x = _pos_x; //Math.sin(e * 0.002) * 1.4; _group.position.y = _pos_y + 50; //Math.cos(e * 0.0014) * 0.2; _group.position.z = _pos_z; //Math.cos(e * 0.0014) * 0.5; let tipOffset = 0.4; _tipTarget.copy(_group.position); _tipTarget.y += tipOffset; _tip.lerp(_tipTarget, 0.1); _diff.copy(_tip); _diff.sub(_group.position); let length = _diff.length(); //_group.scale.y = (length / tipOffset - 1) * 0.4 + 1; _group.quaternion.setFromUnitVectors(_y, _diff.normalize()); } } //=====// Embers //========================================// function initEmbers() { var _geometry, _shader, _points; var _num = 8; (function () { initGeometry(); initShader(); initMesh(); })(); function initGeometry() { _geometry = new THREE.BufferGeometry(); _geometry.addAttribute('position', new THREE.BufferAttribute(new Float32Array(_num * 3), 3)); _geometry.addAttribute('offset', new THREE.BufferAttribute(new Float32Array(_num * 3), 3)); _geometry.addAttribute('size', new THREE.BufferAttribute(new Float32Array(_num), 1)); _geometry.addAttribute('life', new THREE.BufferAttribute(new Float32Array(_num), 1)); var scale = new THREE.InstancedBufferAttribute(new Float32Array(_num * 2), 2); _geometry.addAttribute('scale', scale); for (var i = 0; i < _num; i++) { _geometry.attributes.life.setX(i, random(0, 1, 3) + 1); } } function initShader() { var uniforms = { uMap: { type: 't', value: null }, uColor: { type: 'c', value: new THREE.Color(0xffe61e) }, }; _shader = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: embersVertexShader, fragmentShader: embersFragmentShader, blending: THREE.AdditiveBlending, transparent: true, depthTest: false, }); var textureLoader = new THREE.TextureLoader(); textureLoader.load('images/myFire3/ember.png', t => _shader.uniforms.uMap.value = t); } function initMesh() { _points = new THREE.Points(_geometry, _shader); _points.frustumCulled = false; _scene.add(_points); _self.objs.push(_points); } _self.loop2 = function () { var life = _geometry.attributes.life; var position = _geometry.attributes.position; var size = _geometry.attributes.size; var offset = _geometry.attributes.offset; var scale = _geometry.attributes.scale; for (let i = 0; i < _num; i++) { var value = life.array[i]; value += 0.02; if (value > 1) { value -= 1; position.setXYZ(i, _pos_x, _pos_y + 0.1, _pos_z); offset.setXYZ(i, random(-150, 150, 3), random(100, 300, 3), random(-100, 100, 3) ); size.setX(i, random(20, 100, 3)); scale.setXY(i, 50, 50); } life.setX(i, value); } life.needsUpdate = true; position.needsUpdate = true; size.needsUpdate = true; offset.needsUpdate = true; } } //=====// Haze //========================================// function initHaze() { var _geometry, _shader, _mesh; var _num = 4; var _z = new THREE.Vector3(0, 0, 1); var _quat = new THREE.Quaternion(); var _quat2 = new THREE.Quaternion(); (function () { initGeometry(); initInstances(); initShader(); initMesh(); window.addEventListener('resize', resizeHaze, false); resizeHaze(); })(); function initGeometry() { _geometry = new THREE.InstancedBufferGeometry(); _geometry.maxInstancedCount = _num; var shape = new THREE.PlaneBufferGeometry(0.1, 0.1); var data = shape.attributes; _geometry.addAttribute('position', new THREE.BufferAttribute(new Float32Array(data.position.array), 3)); _geometry.addAttribute('uv', new THREE.BufferAttribute(new Float32Array(data.uv.array), 2)); _geometry.addAttribute('normal', new THREE.BufferAttribute(new Float32Array(data.normal.array), 3)); _geometry.setIndex(new THREE.BufferAttribute(new Uint16Array(shape.index.array), 1)); shape.dispose(); } function initInstances() { var base = new THREE.InstancedBufferAttribute(new Float32Array(_num * 3), 3); var offset = new THREE.InstancedBufferAttribute(new Float32Array(_num * 3), 3); var orientation = new THREE.InstancedBufferAttribute(new Float32Array(_num * 4), 4); var scale = new THREE.InstancedBufferAttribute(new Float32Array(_num * 2), 2); var rotation = new THREE.InstancedBufferAttribute(new Float32Array(_num), 1); var life = new THREE.InstancedBufferAttribute(new Float32Array(_num), 1); for (let i = 0; i < _num; i++) { orientation.setXYZW(i, 0, 0, 0, 1); life.setX(i, i / _num + 1); } _geometry.addAttribute('base', base); _geometry.addAttribute('offset', offset); _geometry.addAttribute('orientation', orientation); _geometry.addAttribute('scale', scale); _geometry.addAttribute('rotation', rotation); _geometry.addAttribute('life', life); } function initShader() { let dpr = _renderer.getPixelRatio(); var uniforms = { uMap: { type: 't', value: null }, uMask: { type: 't', value: null }, uResolution: { type: 'v2', value: new THREE.Vector2(_width * dpr, _height * dpr) }, }; _shader = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: hazeVertexShader, fragmentShader: hazeFragmentShader, transparent: true, depthTest: false, }); var textureLoader = new THREE.TextureLoader(); textureLoader.load('images/myFire3/haze.png', t => _shader.uniforms.uMask.value = t); } function initMesh() { _mesh = new THREE.Mesh(_geometry, _shader); _mesh.frustumCulled = false; _scene.add(_mesh); _self.objs.push(_mesh); } function resizeHaze() { let dpr = _renderer.getPixelRatio(); _shader.uniforms.uMap.value = _rtt.texture; _shader.uniforms.uResolution.value.set(_width * dpr, _height * dpr); } _self.loop3 = function () { let e = 100; _mesh.visible = false; //_renderer.render(_scene, _camera, _rtt); _mesh.visible = true; var life = _geometry.attributes.life; var base = _geometry.attributes.base; var offset = _geometry.attributes.offset; var scale = _geometry.attributes.scale; var orientation = _geometry.attributes.orientation; var rotation = _geometry.attributes.rotation; for (let i = 0; i < _num; i++) { var value = life.array[i]; value += 0.008; if (value > 1) { value -= 1; rotation.setX(i, random(0, 3.14, 3)); base.setXYZ(i, _pos_x, _pos_y + 0.1, _pos_z); offset.setXYZ(i, random(-150, 150, 3), random(100, 300, 3), 0 ); //scale.setXY(i, random(0.6, 1.2, 3), random(0.6, 1.2, 3)); scale.setXY(i, 50, 50); } _quat.copy(_camera.quaternion); _quat2.setFromAxisAngle(_z, rotation.array[i]); _quat.multiply(_quat2); orientation.setXYZW(i, _quat.x, _quat.y, _quat.z, _quat.w); life.setX(i, value); } life.needsUpdate = true; base.needsUpdate = true; scale.needsUpdate = true; offset.needsUpdate = true; orientation.needsUpdate = true; } } } MyFire3.prototype.constructor = MyFire3; export { MyFire3 }
在主文件中添加如下代码实现火焰烟雾效果:
import { createSmoke, updateParticle, particleEngine } from '../js.my/MySmoke.js' import { MyFire3 } from '../js.my/MyFire3.js'; let showFire = function () { myFire3 = new MyFire3(); myFire3.config(scene, renderer, camera, controls); myFire3.setPosition(417, 0, 1134); if (planSelect.getPosition()) { let pos = planSelect.getPosition(); myFire3.setPosition(pos.x, pos.y, pos.z); createSmoke(scene, pos); } myFire3.showFire(); planTypeSelect.setFire(myFire3); planTypeSelect.setSmoke(particleEngine); } if (!myFire3) { showFire(); } else { if (myFire3.isShow()) { myFire3.hide(); particleEngine.hide(); } else { showFire(); particleEngine.show(); } }
说明:planSelect.getPosition()选择应急预案获取事件位置信息,planTypeSelect选择预案类型,当预案类型变更时隐藏火焰和烟雾,不是消防类预案时不显示火焰和烟雾,所以需要把相关变量传给planTypeSelect。
效果图: