├── .gitignore ├── Gruntfile.js ├── LICENSE.txt ├── README.md ├── build ├── ShaderParticles.js └── ShaderParticles.min.js ├── examples ├── basic.html ├── clouds.html ├── css │ └── style.css ├── img │ ├── cloud.png │ ├── cloudSml.png │ ├── smokeparticle.png │ └── star.png ├── js │ ├── Pool.js │ ├── Stats.min.js │ └── THREE-r60.min.js ├── mouseFollow.html ├── multipleEmitters.html ├── pool.html ├── sandbox.html ├── sphere.html ├── spherePulsing.html ├── starfield.html └── static.html ├── package.json └── src ├── ShaderParticleEmitter.js └── ShaderParticleGroup.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function( grunt ) { 2 | var packageJSON = grunt.file.readJSON('package.json'); 3 | 4 | var licenseBanner = '/* ' + packageJSON.name + ' ' + packageJSON.version + '\n' + 5 | ' * ' + '\n' + 6 | ' * (c) 2013 Luke Moody (http://www.github.com/squarefeet) & Lee Stemkoski (http://www.adelphi.edu/~stemkoski/)' + '\n' + 7 | ' * Based on Lee Stemkoski\'s original work (https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/js/ParticleEngine.js).' + '\n' + 8 | ' *' + '\n' + 9 | ' * ' + packageJSON.name + ' may be freely distributed under the MIT license (See LICENSE.txt at root of this repository.)' + '\n */\n'; 10 | 11 | 12 | // Specify input files and output paths 13 | var files = [ 14 | 'src/ShaderParticleGroup.js', 15 | 'src/ShaderParticleEmitter.js' 16 | ], 17 | outputPath = 'build/ShaderParticles.js', 18 | outputPathMin = outputPath.replace( '.js', '.min.js' ); 19 | 20 | 21 | var uglifySettings = { 22 | min: { 23 | options: { 24 | mangle: true, 25 | compress: { 26 | dead_code: true, 27 | }, 28 | banner: licenseBanner 29 | }, 30 | files: {} 31 | } 32 | }; 33 | 34 | // Set the path for where the minified files should be saved 35 | uglifySettings.min.files[ outputPathMin ] = [ outputPath ]; 36 | 37 | 38 | grunt.initConfig({ 39 | uglify: uglifySettings, 40 | 41 | concat: { 42 | options: { 43 | separator: ';\n\n' 44 | }, 45 | dist: { 46 | src: files, 47 | dest: outputPath, 48 | }, 49 | } 50 | }); 51 | 52 | grunt.loadNpmTasks( 'grunt-contrib-concat' ); 53 | grunt.loadNpmTasks( 'grunt-contrib-uglify' ); 54 | 55 | grunt.registerTask( 'default', ['concat', 'uglify'] ); 56 | }; -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Luke Moody & Lee Stemkoski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ShaderParticleEngine 2 | ==================== 3 | A GLSL-heavy particle engine for THREE.js. Based on [Stemkoski's great particle engine](https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/js/ParticleEngine.js). 4 | 5 | 6 | Pull requests and issue reports welcome. 7 | 8 | 9 | Version 0.5.0 10 | ============= 11 | Currently not at 1.0.0, so the API is due to change. Please be aware of this when using this library. 12 | That said, it ain't gonna be long until it's at 1.0.0. 13 | 14 | 15 | Changelog 16 | ========= 17 | **Version 0.5.0** 18 | * The latest update sees the addition of the ```ShaderParticleGroup.addPool()``` method. This allows for much easier control of emitter pools. See [the pool example](http://squarefeet.github.io/ShaderParticleEngine/examples/pool.html) for an example. 19 | * There are also quite a few bug fixes courtesy of [Stemkoski](https://github.com/stemkoski/). 20 | * I've also added quite a few more comments to the source-code, so it should be easier to get your head around should you want/need to dig into the internals. 21 | 22 | 23 | 24 | About 25 | ===== 26 | After experimenting with Stemkoski's particle engine, I was having trouble getting high numbers of particles to render at ~60fps. After digging into the code and doing some benchmarks, it was clear that the bottleneck was coming from applying each particle's movement parameters (```velocity += acceleration```, and ```position += velocity```). After moving these calculations to the shaders the performance was drastically increased. 27 | 28 | Another optimisation I wanted was to be able to 'group' lots of emitters into one ```THREE.ParticleSystem```, so that if I had (for example) 20 particle emitters sharing the same texture, I could send all 20 of those emitters to the GPU at the same time via sharing the same geometry. This is where the basis for the ```ShaderParticleGroup``` comes from. 29 | 30 | This project requires THREE.js revision 58/59/60. 31 | 32 | 33 | 34 | Usage 35 | ===== 36 | See the ```./examples/``` folder (or [here](http://squarefeet.github.io/ShaderParticleEngine/)) for some simple demos. 37 | 38 | Assuming you have a basic scene set up using THREE.js and have added the JS to your page, adding a particle emitter is as simple as the following code: 39 | 40 | ```javascript 41 | // Create a particle group to add the emitter to. 42 | var particleGroup = new ShaderParticleGroup({ 43 | // Give the particles in this group a texture 44 | texture: THREE.ImageUtils.loadTexture('path/to/your/texture.file'), 45 | 46 | // How long should the particles live for? Measured in seconds. 47 | maxAge: 5 48 | }); 49 | 50 | // Create a single emitter 51 | var particleEmitter = new ShaderParticleEmitter({ 52 | type: 'cube', 53 | position: new THREE.Vector3(0, 0, 0), 54 | acceleration: new THREE.Vector3(0, 10, 0), 55 | velocity: new THREE.Vector3(0, 15, 0), 56 | particlesPerSecond: 100, 57 | size: 10, 58 | sizeEnd: 0, 59 | opacityStart: 1, 60 | opacityEnd: 0, 61 | colorStart: new THREE.Color('blue'), 62 | colorEnd: new THREE.Color('white') 63 | }); 64 | 65 | // Add the emitter to the group. 66 | particleGroup.addEmitter( particleEmitter ); 67 | 68 | // Add the particle group to the scene so it can be drawn. 69 | scene.add( particleGroup.mesh ); // Where `scene` is an instance of `THREE.Scene`. 70 | 71 | // ... 72 | 73 | // In your frame render function: 74 | // Where dt is the time delta 75 | // (the time it took to render the last frame.) 76 | particleGroup.tick( dt ); 77 | 78 | ``` 79 | 80 | 81 | API 82 | === 83 | 84 | ####```ShaderParticleGroup``` settings:#### 85 | 86 | ```javascript 87 | // All possible parameters for the ShaderParticleGroup constructor. 88 | // - Default values for each key are as given below if the key is [OPTIONAL]. 89 | var particleGroup = new ShaderParticleGroup({ 90 | 91 | // [REQUIRED] Give the particles in this group a texture. 92 | texture: THREE.ImageUtils.loadTexture('path/to/your/texture.file'), 93 | 94 | // [OPTIONAL] How long should the particles live for? Measured in seconds. 95 | maxAge: 3, 96 | 97 | // [OPTIONAL] Should the particles have perspective applied when drawn? 98 | // Use 0 for false and 1 for true. 99 | hasPerspective: 1, 100 | 101 | // [OPTIONAL] Should the particles in this group have a color applied? 102 | // Use 0 for false and 1 for true 103 | colorize: 1, 104 | 105 | // [OPTIONAL] What blending style should be used? 106 | // THREE.NoBlending 107 | // THREE.NormalBlending 108 | // THREE.AdditiveBlending 109 | // THREE.SubtractiveBlending 110 | // THREE.MultiplyBlending 111 | blending: THREE.AdditiveBlending, 112 | 113 | // [OPTIONAL] Should transparency be applied? 114 | transparent: true, 115 | 116 | // [OPTIONAL] What threshold should be used to test the alpha channel? 117 | alphaTest: 0.5, 118 | 119 | // [OPTIONAL] Should this particle group be written to the depth buffer? 120 | depthWrite: false, 121 | 122 | // [OPTIONAL] Should a depth test be performed on this group? 123 | depthTest: true, 124 | 125 | // [OPTIONAL] Specify a fixed time-step value if you're more bothered 126 | // about smooth performance. Only use this if necessary. Measured in seconds. 127 | fixedTimeStep: 0.016 128 | }); 129 | ``` 130 | 131 | 132 | ####```ShaderParticleEmitter``` settings: 133 | 134 | ```javascript 135 | // All possible parameters for the ShaderParticleEmitter constructor 136 | // - Default values for each key are as given below if the key is [OPTIONAL] 137 | var particleEmitter = new ShaderParticleEmitter({ 138 | 139 | // [OPTIONAL] Emitter shape. 140 | // 'cube' or 'sphere'. 141 | // When using 'sphere' shape, use `radius` and `speed` parameters. 142 | // When using 'cube' shape, use `acceleration` and `velocity` parameters. 143 | type: 'cube', 144 | 145 | 146 | // [OPTIONAL] Base position for the emitter. Can be changed over time. 147 | position: new THREE.Vector3(0, 0, 0), 148 | 149 | // [OPTIONAL] Particle start position variance. 150 | positionSpread: new THREE.Vector3(0, 0, 0), 151 | 152 | 153 | // [OPTIONAL] Acceleration base vector. 154 | acceleration: new THREE.Vector3(0, 0, 0), 155 | 156 | // [OPTIONAL] Acceleration variance. 157 | accelerationSpread: new THREE.Vector3(0, 0, 0), 158 | 159 | 160 | // [OPTIONAL] Velocity base vector. 161 | velocity: new THREE.Vector3(0, 0, 0), 162 | 163 | // [OPTIONAL] Velocity variance. 164 | velocitySpread: new THREE.Vector3(0, 0, 0), 165 | 166 | 167 | // [OPTIONAL - Sphere type] Starting position radius. 168 | radius: 10, 169 | 170 | // [OPTIONAL - Sphere type] Starting position radius scale. 171 | radiusScale: new THREE.Vector3(1, 1, 1), 172 | 173 | // [OPTIONAL - Sphere type] Particle speed. 174 | speed: 0, 175 | 176 | // [OPTIONAL - Sphere type] Particle speed variance. 177 | speedSpread: 0, 178 | 179 | 180 | // [OPTIONAL] Particle start size. 181 | size: 10, 182 | 183 | // [OPTIONAL] Particle start size variance. 184 | sizeSpread: 0, 185 | 186 | // [OPTIONAL] Particle end size. 187 | sizeEnd: 10, 188 | 189 | 190 | // [OPTIONAL] Particle start colour. 191 | colorStart: new THREE.Color( 'white' ), 192 | 193 | // [OPTIONAL] Particle start colour variance. 194 | colorSpread: new THREE.Vector3(0, 0, 0), 195 | 196 | // [OPTIONAL] Particle end colour. 197 | colorEnd: new THREE.Color( 'blue' ), 198 | 199 | 200 | // [OPTIONAL] Particle start opacity. 201 | opacityStart: 1, 202 | 203 | // [OPTIONAL] New in v0.4.0. Particle middle opacity. 204 | // The opacity value at half a particle's lifecycle. 205 | // If not specified, it will be set to halfway between the 206 | // `opacityStart` and `opacityEnd` values. 207 | opacityMiddle: 0.5 208 | 209 | // [OPTIONAL] Particle end opacity. 210 | opacityEnd: 0, 211 | 212 | 213 | // [OPTIONAL] The number of particles emitted per second. 214 | particlesPerSecond: 100, 215 | 216 | // [OPTIONAL] Emitter duration. Measured in seconds. 217 | // A null value indicates an infinite duration. 218 | emitterDuration: null, 219 | 220 | // [OPTIONAL] Should this emitter be alive (i.e. should it be emitting)? 221 | // 0 for false, 1 for true 222 | alive: 1, 223 | 224 | // [OPTIONAL] New in v0.4.0. If you want a huge amount of particles, and 225 | // they aren't going to be moving, then set this property to `1`. This will 226 | // take the start values for color, opacity, and size (with spreads applied), 227 | // not add the emitter from its group's tick function, and so will be static. 228 | // See the static.html file in the examples directory for more. 229 | static: 0 230 | }); 231 | ``` 232 | 233 | ####"Public" Methods for ```ShaderParticleGroup```:#### 234 | 235 | **- ```.addEmitter( emitter )```** 236 | Adds an instance of ```ShaderParticleEmitter``` to the particle group. 237 | 238 | **- ```.tick( dt )```** 239 | Call this function once per frame. If no ```dt``` argument is given, the ```ShaderParticleGroup``` instance will use its ```.fixedTimeStep``` value as ```dt```. 240 | 241 | **- ```.addPool( numEmitters, emitterSettings, createNewEmitterIfPoolRunsOut )```** 242 | Automatically create a pool of emitters for easy triggering in the future. 243 | 244 | **- ```.triggerPoolEmitter( numEmittersToActivate, positionVector )```** 245 | Turn on a given number of emitters that live in a pool created using the method above. You can also pass a ```THREE.Vector3``` instance to dictate where this emitter will sit. 246 | 247 | 248 | Known Bugs 249 | ========== 250 | See the [issues page](https://github.com/squarefeet/ShaderParticleEngine/issues) for any known bugs. Please open an issue if you find anything not behaving properly. 251 | 252 | 253 | 254 | Thanks 255 | ====== 256 | Huge thanks are extended to [Stemkoski](http://stemkoski.github.io/Three.js/) for his initial particle engine, and to [Mr Doob, AlteredQualia, et. al](https://github.com/mrdoob/three.js/graphs/contributors) for their awesome work on [THREE.js](http://threejs.org/). -------------------------------------------------------------------------------- /build/ShaderParticles.js: -------------------------------------------------------------------------------- 1 | // ShaderParticleGroup 0.5.0 2 | // 3 | // (c) 2013 Luke Moody (http://www.github.com/squarefeet) & Lee Stemkoski (http://www.adelphi.edu/~stemkoski/) 4 | // Based on Lee Stemkoski's original work (https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/js/ParticleEngine.js). 5 | // 6 | // ShaderParticleGroup may be freely distributed under the MIT license (See LICENSE.txt) 7 | 8 | 9 | function ShaderParticleGroup( options ) { 10 | var that = this; 11 | 12 | that.fixedTimeStep = parseFloat( options.fixedTimeStep || 0.016, 10 ); 13 | 14 | // Uniform properties ( applied to all particles ) 15 | that.maxAge = parseFloat( options.maxAge || 3 ); 16 | that.texture = options.texture || null; 17 | that.hasPerspective = parseInt( typeof options.hasPerspective === 'number' ? options.hasPerspective : 1 ); 18 | that.colorize = parseInt( options.colorize || 1 ); 19 | 20 | // Material properties 21 | that.blending = typeof options.blending === 'number' ? options.blending : THREE.AdditiveBlending; 22 | that.transparent = options.transparent || true; 23 | that.alphaTest = options.alphaTest || 0.5; 24 | that.depthWrite = options.depthWrite || false; 25 | that.depthTest = options.depthTest || true; 26 | 27 | // Create uniforms 28 | that.uniforms = { 29 | duration: { type: 'f', value: that.maxAge }, 30 | texture: { type: 't', value: that.texture }, 31 | hasPerspective: { type: 'i', value: that.hasPerspective }, 32 | colorize: { type: 'i', value: that.colorize } 33 | }; 34 | 35 | // Create a map of attributes that will hold values for each particle in this group. 36 | that.attributes = { 37 | acceleration: { type: 'v3', value: [] }, 38 | velocity: { type: 'v3', value: [] }, 39 | alive: { type: 'f', value: [] }, 40 | age: { type: 'f', value: [] }, 41 | size: { type: 'f', value: [] }, 42 | sizeEnd: { type: 'f', value: [] }, 43 | 44 | customColor: { type: 'c', value: [] }, 45 | customColorEnd: { type: 'c', value: [] }, 46 | 47 | opacity: { type: 'f', value: [] }, 48 | opacityMiddle: { type: 'f', value: [] }, 49 | opacityEnd: { type: 'f', value: [] } 50 | }; 51 | 52 | // Emitters (that aren't static) will be added to this array for 53 | // processing during the `tick()` function. 54 | that.emitters = []; 55 | 56 | // Create properties for use by the emitter pooling functions. 57 | that._pool = []; 58 | that._poolCreationSettings = null; 59 | that._createNewWhenPoolEmpty = 0; 60 | that.maxAgeMilliseconds = that.maxAge * 1000; 61 | 62 | // Create an empty geometry to hold the particles. 63 | // Each particle is a vertex pushed into this geometry's 64 | // vertices array. 65 | that.geometry = new THREE.Geometry(); 66 | 67 | // Create the shader material using the properties we set above. 68 | that.material = new THREE.ShaderMaterial({ 69 | uniforms: that.uniforms, 70 | attributes: that.attributes, 71 | vertexShader: ShaderParticleGroup.shaders.vertex, 72 | fragmentShader: ShaderParticleGroup.shaders.fragment, 73 | blending: that.blending, 74 | transparent: that.transparent, 75 | alphaTest: that.alphaTest, 76 | depthWrite: that.depthWrite, 77 | depthTest: that.depthTest, 78 | }); 79 | 80 | // And finally create the ParticleSystem. It's got its `dynamic` property 81 | // set so that THREE.js knows to update it on each frame. 82 | that.mesh = new THREE.ParticleSystem( that.geometry, that.material ); 83 | that.mesh.dynamic = true; 84 | } 85 | 86 | ShaderParticleGroup.prototype = { 87 | 88 | /** 89 | * Given a base vector and a spread range vector, create 90 | * a new THREE.Vector3 instance with randomised values. 91 | * 92 | * @private 93 | * 94 | * @param {THREE.Vector3} base 95 | * @param {THREE.Vector3} spread 96 | * @return {THREE.Vector3} 97 | */ 98 | _randomVector3: function( base, spread ) { 99 | var v = new THREE.Vector3(); 100 | 101 | v.copy( base ); 102 | 103 | v.x += Math.random() * spread.x - (spread.x/2); 104 | v.y += Math.random() * spread.y - (spread.y/2); 105 | v.z += Math.random() * spread.z - (spread.z/2); 106 | 107 | return v; 108 | }, 109 | 110 | /** 111 | * Create a new THREE.Color instance and given a base vector and 112 | * spread range vector, assign random values. 113 | * 114 | * Note that THREE.Color RGB values are in the range of 0 - 1, not 0 - 255. 115 | * 116 | * @private 117 | * 118 | * @param {THREE.Vector3} base 119 | * @param {THREE.Vector3} spread 120 | * @return {THREE.Color} 121 | */ 122 | _randomColor: function( base, spread ) { 123 | var v = new THREE.Color(); 124 | 125 | v.copy( base ); 126 | 127 | v.r += (Math.random() * spread.x) - (spread.x/2); 128 | v.g += (Math.random() * spread.y) - (spread.y/2); 129 | v.b += (Math.random() * spread.z) - (spread.z/2); 130 | 131 | v.r = Math.max( 0, Math.min( v.r, 1 ) ); 132 | v.g = Math.max( 0, Math.min( v.g, 1 ) ); 133 | v.b = Math.max( 0, Math.min( v.b, 1 ) ); 134 | 135 | return v; 136 | }, 137 | 138 | 139 | /** 140 | * Create a random Number value based on an initial value and 141 | * a spread range 142 | * 143 | * @private 144 | * 145 | * @param {Number} base 146 | * @param {Number} spread 147 | * @return {Number} 148 | */ 149 | _randomFloat: function( base, spread ) { 150 | return base + spread * (Math.random() - 0.5); 151 | }, 152 | 153 | 154 | /** 155 | * Create a new THREE.Vector3 instance and project it onto a random point 156 | * on a sphere with radius `radius`. 157 | * 158 | * @param {THREE.Vector3} base 159 | * @param {Number} radius 160 | * @param {THREE.Vector3} scale 161 | * 162 | * @private 163 | * 164 | * @return {THREE.Vector3} 165 | */ 166 | _randomVector3OnSphere: function( base, radius, scale ) { 167 | var z = 2 * Math.random() - 1; 168 | var t = 6.2832 * Math.random(); 169 | var r = Math.sqrt( 1 - z*z ); 170 | var vec = new THREE.Vector3( r * Math.cos(t), r * Math.sin(t), z ); 171 | 172 | vec.multiplyScalar( radius ); 173 | 174 | if( scale ) { 175 | vec.multiply( scale ); 176 | } 177 | 178 | vec.add( base ); 179 | 180 | return vec; 181 | }, 182 | 183 | 184 | /** 185 | * Create a new THREE.Vector3 instance, and given a base position, and various 186 | * other values, project it onto a random point on a sphere with radius `radius`. 187 | * 188 | * @param {THREE.Vector3} base 189 | * @param {THREE.Vector3} position 190 | * @param {Number} speed 191 | * @param {Number} speedSpread 192 | * @param {THREE.Vector3} scale 193 | * @param {Number} radius 194 | * 195 | * @private 196 | * 197 | * @return {THREE.Vector3} 198 | */ 199 | _randomVelocityVector3OnSphere: function( base, position, speed, speedSpread, scale, radius ) { 200 | var direction = new THREE.Vector3().subVectors( base, position ); 201 | 202 | direction.normalize().multiplyScalar( this._randomFloat( speed, speedSpread ) ); 203 | 204 | if( scale ) { 205 | direction.multiply( scale ); 206 | } 207 | 208 | return direction; 209 | }, 210 | 211 | 212 | /** 213 | * Given a base vector and a spread vector, randomise the given vector 214 | * accordingly. 215 | * 216 | * @param {THREE.Vector3} vector 217 | * @param {THREE.Vector3} base 218 | * @param {THREE.Vector3} spread 219 | * 220 | * @private 221 | * 222 | * @return {[type]} 223 | */ 224 | _randomizeExistingVector3: function( vector, base, spread ) { 225 | vector.set( 226 | Math.random() * base.x - spread.x, 227 | Math.random() * base.y - spread.y, 228 | Math.random() * base.z - spread.z 229 | ); 230 | }, 231 | 232 | 233 | /** 234 | * Tells the age and alive attributes (and the geometry vertices) 235 | * that they need updating by THREE.js's internal tick functions. 236 | * 237 | * @private 238 | * 239 | * @return {this} 240 | */ 241 | _flagUpdate: function() { 242 | var that = this; 243 | 244 | // Set flags to update (causes less garbage than 245 | // ```ParticleSystem.sortParticles = true``` in THREE.r58 at least) 246 | that.attributes.age.needsUpdate = true; 247 | that.attributes.alive.needsUpdate = true; 248 | that.geometry.verticesNeedUpdate = true; 249 | 250 | return that; 251 | }, 252 | 253 | 254 | /** 255 | * Add an emitter to this particle group. Once added, an emitter will be automatically 256 | * updated when ShaderParticleGroup#tick() is called. 257 | * 258 | * @param {ShaderParticleEmitter} emitter 259 | * @return {this} 260 | */ 261 | addEmitter: function( emitter ) { 262 | var that = this; 263 | 264 | if( emitter.duration ) { 265 | emitter.numParticles = emitter.particlesPerSecond * (that.maxAge < emitter.emitterDuration ? that.maxAge : emitter.emitterDuration) | 0; 266 | } 267 | else { 268 | emitter.numParticles = emitter.particlesPerSecond * that.maxAge | 0; 269 | } 270 | 271 | emitter.numParticles = Math.ceil(emitter.numParticles); 272 | 273 | var vertices = that.geometry.vertices, 274 | start = vertices.length, 275 | end = emitter.numParticles + start, 276 | a = that.attributes, 277 | acceleration = a.acceleration.value, 278 | velocity = a.velocity.value, 279 | alive = a.alive.value, 280 | age = a.age.value, 281 | size = a.size.value, 282 | sizeEnd = a.sizeEnd.value, 283 | customColor = a.customColor.value, 284 | customColorEnd = a.customColorEnd.value, 285 | opacity = a.opacity.value, 286 | opacityMiddle = a.opacityMiddle.value; 287 | opacityEnd = a.opacityEnd.value; 288 | 289 | emitter.particleIndex = parseFloat( start, 10 ); 290 | 291 | // Create the values 292 | for( var i = start; i < end; ++i ) { 293 | 294 | if( emitter.type === 'sphere' ) { 295 | vertices[i] = that._randomVector3OnSphere( emitter.position, emitter.radius, emitter.radiusScale ); 296 | velocity[i] = that._randomVelocityVector3OnSphere( vertices[i], emitter.position, emitter.speed, emitter.speedSpread, emitter.radiusScale, emitter.radius ); 297 | } 298 | else { 299 | vertices[i] = that._randomVector3( emitter.position, emitter.positionSpread ); 300 | velocity[i] = that._randomVector3( emitter.velocity, emitter.velocitySpread ); 301 | } 302 | 303 | 304 | acceleration[i] = that._randomVector3( emitter.acceleration, emitter.accelerationSpread ); 305 | 306 | // Fix for bug #1 (https://github.com/squarefeet/ShaderParticleEngine/issues/1) 307 | // For some stupid reason I was limiting the size value to a minimum of 0.1. Derp. 308 | size[i] = that._randomFloat( emitter.size, emitter.sizeSpread ); 309 | sizeEnd[i] = emitter.sizeEnd; 310 | age[i] = 0.0; 311 | alive[i] = emitter.static ? 1.0 : 0.0; 312 | 313 | 314 | customColor[i] = that._randomColor( emitter.colorStart, emitter.colorSpread ); 315 | customColorEnd[i] = emitter.colorEnd; 316 | opacity[i] = emitter.opacityStart; 317 | opacityMiddle[i] = emitter.opacityMiddle; 318 | opacityEnd[i] = emitter.opacityEnd; 319 | } 320 | 321 | // Cache properties on the emitter so we can access 322 | // them from its tick function. 323 | emitter.verticesIndex = parseFloat( start ); 324 | emitter.attributes = that.attributes; 325 | emitter.vertices = that.geometry.vertices; 326 | emitter.maxAge = that.maxAge; 327 | 328 | // Save this emitter in an array for processing during this.tick() 329 | if( !emitter.static ) { 330 | that.emitters.push( emitter ); 331 | } 332 | 333 | return that; 334 | }, 335 | 336 | 337 | /** 338 | * The main particle group update function. Call this once per frame. 339 | * 340 | * @param {Number} dt 341 | * @return {this} 342 | */ 343 | tick: function( dt ) { 344 | var that = this, 345 | emitters = that.emitters, 346 | numEmitters = emitters.length; 347 | 348 | dt = dt || that.fixedTimeStep; 349 | 350 | if( numEmitters === 0 ) return; 351 | 352 | for( var i = 0; i < numEmitters; ++i ) { 353 | emitters[i].tick( dt ); 354 | } 355 | 356 | that._flagUpdate(); 357 | return that; 358 | }, 359 | 360 | 361 | /** 362 | * Fetch a single emitter instance from the pool. 363 | * If there are no objects in the pool, a new emitter will be 364 | * created if specified. 365 | * 366 | * @return {ShaderParticleEmitter | null} 367 | */ 368 | getFromPool: function() { 369 | var that = this, 370 | pool = that._pool, 371 | createNew = that._createNewWhenPoolEmpty; 372 | 373 | if( pool.length ) { 374 | return pool.pop(); 375 | } 376 | else if( createNew ) { 377 | return new ShaderParticleEmitter( that._poolCreationSettings ); 378 | } 379 | 380 | return null; 381 | }, 382 | 383 | 384 | /** 385 | * Release an emitter into the pool. 386 | * 387 | * @param {ShaderParticleEmitter} emitter 388 | * @return {this} 389 | */ 390 | releaseIntoPool: function( emitter ) { 391 | if( !(emitter instanceof ShaderParticleEmitter) ) { 392 | console.error( 'Will not add non-emitter to particle group pool:', emitter ); 393 | return; 394 | } 395 | 396 | emitter.reset(); 397 | this._pool.unshift( emitter ); 398 | 399 | return this; 400 | }, 401 | 402 | 403 | /** 404 | * Get the pool array 405 | * 406 | * @return {Array} 407 | */ 408 | getPool: function() { 409 | return this._pool; 410 | }, 411 | 412 | 413 | /** 414 | * Add a pool of emitters to this particle group 415 | * 416 | * @param {Number} numEmitters The number of emitters to add to the pool. 417 | * @param {Object} emitterSettings An object describing the settings to pass to each emitter. 418 | * @param {Boolean} createNew Should a new emitter be created if the pool runs out? 419 | * @return {this} 420 | */ 421 | addPool: function( numEmitters, emitterSettings, createNew ) { 422 | var that = this, 423 | pool = that._pool, 424 | emitter; 425 | 426 | // Save relevant settings and flags. 427 | that._poolCreationSettings = emitterSettings; 428 | that._createNewWhenPoolEmpty = !!createNew; 429 | 430 | // Create the emitters, add them to this group and the pool. 431 | for( var i = 0; i < numEmitters; ++i ) { 432 | emitter = new ShaderParticleEmitter( emitterSettings ); 433 | that.addEmitter( emitter ); 434 | that.releaseIntoPool( emitter ); 435 | } 436 | 437 | return that; 438 | }, 439 | 440 | 441 | /** 442 | * Internal method. Sets a single emitter to be alive 443 | * 444 | * @private 445 | * 446 | * @param {THREE.Vector3} pos 447 | * @return {this} 448 | */ 449 | _triggerSingleEmitter: function( pos ) { 450 | var that = this, 451 | emitter = that.getFromPool(); 452 | 453 | if( emitter === null ) { 454 | console.log('ShaderParticleGroup pool ran out.'); 455 | return; 456 | } 457 | 458 | // TODO: Should an instanceof check happen here? Or maybe at least a typeof? 459 | if( pos ) { 460 | emitter.position.copy( pos ); 461 | } 462 | 463 | emitter.enable(); 464 | 465 | setTimeout( function() { 466 | emitter.disable(); 467 | that.releaseIntoPool( emitter ); 468 | }, that.maxAgeMilliseconds ); 469 | 470 | return that; 471 | }, 472 | 473 | 474 | /** 475 | * Set a given number of emitters as alive, with an optional position 476 | * vector3 to move them to. 477 | * 478 | * @param {Number} numEmitters 479 | * @param {THREE.Vector3} position 480 | * @return {this} 481 | */ 482 | triggerPoolEmitter: function( numEmitters, position ) { 483 | var that = this; 484 | 485 | if( typeof numEmitters === 'number' && numEmitters > 1) { 486 | for( var i = 0; i < numEmitters; ++i ) { 487 | that._triggerSingleEmitter( position ); 488 | } 489 | } 490 | else { 491 | that._triggerSingleEmitter( position ); 492 | } 493 | 494 | return that; 495 | } 496 | }; 497 | 498 | 499 | 500 | // The all-important shaders 501 | ShaderParticleGroup.shaders = { 502 | vertex: [ 503 | 'uniform float duration;', 504 | 'uniform int hasPerspective;', 505 | 506 | 'attribute vec3 customColor;', 507 | 'attribute vec3 customColorEnd;', 508 | 'attribute float opacity;', 509 | 'attribute float opacityMiddle;', 510 | 'attribute float opacityEnd;', 511 | 512 | 'attribute vec3 acceleration;', 513 | 'attribute vec3 velocity;', 514 | 'attribute float alive;', 515 | 'attribute float age;', 516 | 'attribute float size;', 517 | 'attribute float sizeEnd;', 518 | 519 | 'varying vec4 vColor;', 520 | 521 | // Linearly lerp a float 522 | 'float Lerp( float start, float end, float amount ) {', 523 | 'return (start + ((end - start) * amount));', 524 | '}', 525 | 526 | // Linearly lerp a vector3 527 | 'vec3 Lerp( vec3 start, vec3 end, float amount ) {', 528 | 'return (start + ((end - start) * amount));', 529 | '}', 530 | 531 | // Integrate acceleration into velocity and apply it to the particle's position 532 | 'vec4 GetPos() {', 533 | 'vec3 newPos = vec3( position );', 534 | 535 | // Move acceleration & velocity vectors to the value they 536 | // should be at the current age 537 | 'vec3 a = acceleration * age;', 538 | 'vec3 v = velocity * age;', 539 | 540 | // Move velocity vector to correct values at this age 541 | 'v = v + (a * age);', 542 | 543 | // Add velocity vector to the newPos vector 544 | 'newPos = newPos + v;', 545 | 546 | // Convert the newPos vector into world-space 547 | 'vec4 mvPosition = modelViewMatrix * vec4( newPos, 1.0 );', 548 | 549 | 'return mvPosition;', 550 | '}', 551 | 552 | 553 | 'void main() {', 554 | 555 | 'float positionInTime = (age / duration);', 556 | 'float halfDuration = (duration / 2.0);', 557 | 558 | 'if( alive > 0.5 ) {', 559 | // Integrate color "tween" 560 | 'vec3 color = vec3( customColor );', 561 | 'if( customColor != customColorEnd ) {', 562 | 'color = Lerp( customColor, customColorEnd, positionInTime );', 563 | '}', 564 | 565 | // Store the color of this particle in the varying vColor, 566 | // so frag shader can access it. 567 | 'if( opacity == opacityMiddle && opacityMiddle == opacityEnd ) {', 568 | 'vColor = vec4( color, opacity );', 569 | '}', 570 | 571 | 'else if( positionInTime < 0.5 ) {', 572 | 'vColor = vec4( color, Lerp( opacity, opacityMiddle, age / halfDuration ) );', 573 | '}', 574 | 575 | 'else if( positionInTime > 0.5 ) {', 576 | 'vColor = vec4( color, Lerp( opacityMiddle, opacityEnd, (age - halfDuration) / halfDuration ) );', 577 | '}', 578 | 579 | 'else {', 580 | 'vColor = vec4( color, opacityMiddle );', 581 | '}', 582 | 583 | // Get the position of this particle so we can use it 584 | // when we calculate any perspective that might be required. 585 | 'vec4 pos = GetPos();', 586 | 587 | // Determine point size . 588 | 'float pointSize = Lerp( size, sizeEnd, positionInTime );', 589 | 590 | 'if( hasPerspective == 1 ) {', 591 | 'pointSize = pointSize * ( 300.0 / length( pos.xyz ) );', 592 | '}', 593 | 594 | // Set particle size and position 595 | 'gl_PointSize = pointSize;', 596 | 'gl_Position = projectionMatrix * pos;', 597 | '}', 598 | 599 | 'else {', 600 | // Hide particle and set its position to the (maybe) glsl 601 | // equivalent of Number.POSITIVE_INFINITY 602 | 'vColor = vec4( customColor, 0.0 );', 603 | 'gl_Position = vec4(1e20, 1e20, 1e20, 0);', 604 | '}', 605 | '}', 606 | ].join('\n'), 607 | 608 | fragment: [ 609 | 'uniform sampler2D texture;', 610 | 'uniform int colorize;', 611 | 612 | 'varying vec4 vColor;', 613 | 614 | 'void main() {', 615 | 'float c = cos(0.0);', 616 | 'float s = sin(0.0);', 617 | 618 | 'vec2 rotatedUV = vec2(c * (gl_PointCoord.x - 0.5) + s * (gl_PointCoord.y - 0.5) + 0.5,', 619 | 'c * (gl_PointCoord.y - 0.5) - s * (gl_PointCoord.x - 0.5) + 0.5);', 620 | 621 | 'vec4 rotatedTexture = texture2D( texture, rotatedUV );', 622 | 623 | 'if( colorize == 1 ) {', 624 | 'gl_FragColor = vColor * rotatedTexture;', 625 | '}', 626 | 'else {', 627 | 'gl_FragColor = rotatedTexture;', 628 | '}', 629 | '}' 630 | ].join('\n') 631 | }; 632 | ; 633 | 634 | // ShaderParticleEmitter 0.5.0 635 | // 636 | // (c) 2013 Luke Moody (http://www.github.com/squarefeet) & Lee Stemkoski (http://www.adelphi.edu/~stemkoski/) 637 | // Based on Lee Stemkoski's original work (https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/js/ParticleEngine.js). 638 | // 639 | // ShaderParticleEmitter may be freely distributed under the MIT license (See LICENSE.txt) 640 | 641 | function ShaderParticleEmitter( options ) { 642 | // If no options are provided, fallback to an empty object. 643 | options = options || {}; 644 | 645 | // Helps with minification. Not as easy to read the following code, 646 | // but should still be readable enough! 647 | var that = this; 648 | 649 | 650 | that.particlesPerSecond = typeof options.particlesPerSecond === 'number' ? options.particlesPerSecond : 100; 651 | that.type = (options.type === 'cube' || options.type === 'sphere') ? options.type : 'cube'; 652 | 653 | that.position = options.position instanceof THREE.Vector3 ? options.position : new THREE.Vector3(); 654 | that.positionSpread = options.positionSpread instanceof THREE.Vector3 ? options.positionSpread : new THREE.Vector3(); 655 | 656 | // These two properties are only used when this.type === 'sphere' 657 | that.radius = typeof options.radius === 'number' ? options.radius : 10; 658 | that.radiusScale = options.radiusScale instanceof THREE.Vector3 ? options.radiusScale : new THREE.Vector3(1, 1, 1); 659 | 660 | that.acceleration = options.acceleration instanceof THREE.Vector3 ? options.acceleration : new THREE.Vector3(); 661 | that.accelerationSpread = options.accelerationSpread instanceof THREE.Vector3 ? options.accelerationSpread : new THREE.Vector3(); 662 | 663 | that.velocity = options.velocity instanceof THREE.Vector3 ? options.velocity : new THREE.Vector3(); 664 | that.velocitySpread = options.velocitySpread instanceof THREE.Vector3 ? options.velocitySpread : new THREE.Vector3(); 665 | 666 | // And again here; only used when this.type === 'sphere' 667 | that.speed = parseFloat( typeof options.speed === 'number' ? options.speed : 0, 10 ); 668 | that.speedSpread = parseFloat( typeof options.speedSpread === 'number' ? options.speedSpread : 0, 10 ); 669 | 670 | that.size = parseFloat( typeof options.size === 'number' ? options.size : 10.0, 10 ); 671 | that.sizeSpread = parseFloat( typeof options.sizeSpread === 'number' ? options.sizeSpread : 0, 10 ); 672 | that.sizeEnd = parseFloat( typeof options.sizeEnd === 'number' ? options.sizeEnd : 10.0, 10 ); 673 | 674 | that.colorStart = options.colorStart instanceof THREE.Color ? options.colorStart : new THREE.Color( 'white' ); 675 | that.colorEnd = options.colorEnd instanceof THREE.Color ? options.colorEnd : new THREE.Color( 'blue' ); 676 | that.colorSpread = options.colorSpread instanceof THREE.Vector3 ? options.colorSpread : new THREE.Vector3(); 677 | 678 | that.opacityStart = parseFloat( typeof options.opacityStart !== 'undefined' ? options.opacityStart : 1, 10 ); 679 | that.opacityEnd = parseFloat( typeof options.opacityEnd === 'number' ? options.opacityEnd : 0, 10 ); 680 | that.opacityMiddle = parseFloat( 681 | typeof options.opacityMiddle !== 'undefined' ? 682 | options.opacityMiddle : 683 | Math.abs(that.opacityEnd + that.opacityStart) / 2, 684 | 10 ); 685 | 686 | that.emitterDuration = typeof options.emitterDuration === 'number' ? options.emitterDuration : null; 687 | that.alive = parseInt( typeof options.alive === 'number' ? options.alive : 1, 10); 688 | 689 | that.static = typeof options.static === 'number' ? options.static : 0; 690 | 691 | // The following properties are used internally, and mostly set when this emitter 692 | // is added to a particle group. 693 | that.numParticles = 0; 694 | that.attributes = null; 695 | that.vertices = null; 696 | that.verticesIndex = 0; 697 | that.age = 0.0; 698 | that.maxAge = 0.0; 699 | 700 | that.particleIndex = 0.0; 701 | 702 | that.userData = {}; 703 | } 704 | 705 | 706 | ShaderParticleEmitter.prototype = { 707 | 708 | /** 709 | * Reset a particle's position. Accounts for emitter type and spreads. 710 | * 711 | * @private 712 | * 713 | * @param {THREE.Vector3} p 714 | */ 715 | _resetParticle: function( p ) { 716 | var that = this; 717 | spread = that.positionSpread, 718 | type = that.type; 719 | 720 | // Optimise for no position spread or radius 721 | if( 722 | ( type === 'cube' && spread.x === 0 && spread.y === 0 && spread.z === 0 ) || 723 | ( type === 'sphere' && that.radius === 0 ) 724 | ) { 725 | p.copy( that.position ); 726 | } 727 | 728 | // If there is a position spread, then get a new position based on this spread. 729 | else if( type === 'cube' ) { 730 | that._randomizeExistingVector3( p, that.position, spread ); 731 | } 732 | 733 | else if( type === 'sphere') { 734 | that._randomizeExistingVector3OnSphere( p, that.position, that.radius ); 735 | } 736 | }, 737 | 738 | 739 | /** 740 | * Given an existing particle vector, randomise it based on base and spread vectors 741 | * 742 | * @private 743 | * 744 | * @param {THREE.Vector3} v 745 | * @param {THREE.Vector3} base 746 | * @param {THREE.Vector3} spread 747 | */ 748 | _randomizeExistingVector3: function( v, base, spread ) { 749 | var r = Math.random; 750 | 751 | v.copy( base ); 752 | 753 | v.x += r() * spread.x - (spread.x/2); 754 | v.y += r() * spread.y - (spread.y/2); 755 | v.z += r() * spread.z - (spread.z/2); 756 | }, 757 | 758 | 759 | /** 760 | * Given an existing particle vector, project it onto a random point on a 761 | * sphere with radius `radius` and position `base`. 762 | * 763 | * @private 764 | * 765 | * @param {THREE.Vector3} v 766 | * @param {THREE.Vector3} base 767 | * @param {Number} radius 768 | */ 769 | _randomizeExistingVector3OnSphere: function( v, base, radius ) { 770 | var rand = Math.random; 771 | 772 | var z = 2 * rand() - 1; 773 | var t = 6.2832 * rand(); 774 | var r = Math.sqrt( 1 - z*z ); 775 | 776 | var x = ((r * Math.cos(t)) * radius); 777 | var y = ((r * Math.sin(t)) * radius); 778 | var z = (z * radius); 779 | 780 | v.set(x, y, z).multiply( this.radiusScale ); 781 | 782 | v.add( base ); 783 | }, 784 | 785 | 786 | // This function is called by the instance of `ShaderParticleEmitter` that 787 | // this emitter has been added to. 788 | /** 789 | * Update this emitter's particle's positions. Called by the ShaderParticleGroup 790 | * that this emitter belongs to. 791 | * 792 | * @param {Number} dt 793 | */ 794 | tick: function( dt ) { 795 | 796 | if( this.static ) { 797 | return; 798 | } 799 | 800 | // Cache some values for quicker access in loops. 801 | var that = this, 802 | a = that.attributes, 803 | alive = a.alive.value, 804 | age = a.age.value, 805 | start = that.verticesIndex, 806 | numParticles = that.numParticles, 807 | end = start + numParticles, 808 | pps = that.particlesPerSecond, 809 | ppsdt = pps * dt, 810 | m = that.maxAge, 811 | emitterAge = that.age, 812 | duration = that.emitterDuration, 813 | pIndex = that.particleIndex; 814 | 815 | // Loop through all the particles in this emitter and 816 | // determine whether they're still alive and need advancing 817 | // or if they should be dead and therefore marked as such 818 | // and pushed into the recycled vertices array for reuse. 819 | for( var i = start; i < end; ++i ) { 820 | if( alive[ i ] === 1.0 ) { 821 | age[ i ] += dt; 822 | } 823 | 824 | if( age[ i ] >= m ) { 825 | age[ i ] = 0.0; 826 | alive[ i ] = 0.0; 827 | } 828 | } 829 | 830 | // If the emitter is dead, reset any particles that are in 831 | // the recycled vertices array and reset the age of the 832 | // emitter to zero ready to go again if required, then 833 | // exit this function. 834 | if( that.alive === 0 ) { 835 | that.age = 0.0; 836 | return; 837 | } 838 | 839 | // If the emitter has a specified lifetime and we've exceeded it, 840 | // mark the emitter as dead and exit this function. 841 | if( typeof duration === 'number' && emitterAge > duration ) { 842 | that.alive = 0; 843 | that.age = 0.0; 844 | return; 845 | } 846 | 847 | var n = Math.min( end, pIndex + ppsdt ); 848 | 849 | for( i = pIndex | 0; i < n; ++i ) { 850 | if( alive[ i ] !== 1.0 ) { 851 | alive[ i ] = 1.0; 852 | that._resetParticle( that.vertices[ i ] ); 853 | } 854 | } 855 | 856 | that.particleIndex += ppsdt; 857 | 858 | if( pIndex >= start + that.numParticles ) { 859 | that.particleIndex = parseFloat( start, 10 ); 860 | } 861 | 862 | // Add the delta time value to the age of the emitter. 863 | that.age += dt; 864 | }, 865 | 866 | /** 867 | * Reset this emitter back to its starting position. 868 | * If `force` is truthy, then reset all particles in this 869 | * emitter as well, even if they're currently alive. 870 | * 871 | * @param {Boolean} force 872 | * @return {this} 873 | */ 874 | reset: function( force ) { 875 | var that = this; 876 | 877 | that.age = 0.0; 878 | that.alive = 0; 879 | 880 | if( force ) { 881 | var start = that.verticesIndex, 882 | end = that.verticesIndex + that.numParticles, 883 | a = that.attributes, 884 | alive = a.alive.value, 885 | age = a.age.value; 886 | 887 | for( var i = start; i < end; ++i ) { 888 | alive[ i ] = 0.0; 889 | age[ i ] = 0.0; 890 | } 891 | } 892 | 893 | return that; 894 | }, 895 | 896 | 897 | /** 898 | * Enable this emitter. 899 | */ 900 | enable: function() { 901 | this.alive = 1; 902 | }, 903 | 904 | /** 905 | * Disable this emitter. 906 | */ 907 | disable: function() { 908 | this.alive = 0; 909 | } 910 | }; 911 | -------------------------------------------------------------------------------- /build/ShaderParticles.min.js: -------------------------------------------------------------------------------- 1 | /* Shader-Particles 0.5.0 2 | * 3 | * (c) 2013 Luke Moody (http://www.github.com/squarefeet) & Lee Stemkoski (http://www.adelphi.edu/~stemkoski/) 4 | * Based on Lee Stemkoski's original work (https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/js/ParticleEngine.js). 5 | * 6 | * Shader-Particles may be freely distributed under the MIT license (See LICENSE.txt at root of this repository.) 7 | */ 8 | function ShaderParticleGroup(a){var b=this;b.fixedTimeStep=parseFloat(a.fixedTimeStep||.016,10),b.maxAge=parseFloat(a.maxAge||3),b.texture=a.texture||null,b.hasPerspective=parseInt("number"==typeof a.hasPerspective?a.hasPerspective:1),b.colorize=parseInt(a.colorize||1),b.blending="number"==typeof a.blending?a.blending:THREE.AdditiveBlending,b.transparent=a.transparent||!0,b.alphaTest=a.alphaTest||.5,b.depthWrite=a.depthWrite||!1,b.depthTest=a.depthTest||!0,b.uniforms={duration:{type:"f",value:b.maxAge},texture:{type:"t",value:b.texture},hasPerspective:{type:"i",value:b.hasPerspective},colorize:{type:"i",value:b.colorize}},b.attributes={acceleration:{type:"v3",value:[]},velocity:{type:"v3",value:[]},alive:{type:"f",value:[]},age:{type:"f",value:[]},size:{type:"f",value:[]},sizeEnd:{type:"f",value:[]},customColor:{type:"c",value:[]},customColorEnd:{type:"c",value:[]},opacity:{type:"f",value:[]},opacityMiddle:{type:"f",value:[]},opacityEnd:{type:"f",value:[]}},b.emitters=[],b._pool=[],b._poolCreationSettings=null,b._createNewWhenPoolEmpty=0,b.maxAgeMilliseconds=1e3*b.maxAge,b.geometry=new THREE.Geometry,b.material=new THREE.ShaderMaterial({uniforms:b.uniforms,attributes:b.attributes,vertexShader:ShaderParticleGroup.shaders.vertex,fragmentShader:ShaderParticleGroup.shaders.fragment,blending:b.blending,transparent:b.transparent,alphaTest:b.alphaTest,depthWrite:b.depthWrite,depthTest:b.depthTest}),b.mesh=new THREE.ParticleSystem(b.geometry,b.material),b.mesh.dynamic=!0}function ShaderParticleEmitter(a){a=a||{};var b=this;b.particlesPerSecond="number"==typeof a.particlesPerSecond?a.particlesPerSecond:100,b.type="cube"===a.type||"sphere"===a.type?a.type:"cube",b.position=a.position instanceof THREE.Vector3?a.position:new THREE.Vector3,b.positionSpread=a.positionSpread instanceof THREE.Vector3?a.positionSpread:new THREE.Vector3,b.radius="number"==typeof a.radius?a.radius:10,b.radiusScale=a.radiusScale instanceof THREE.Vector3?a.radiusScale:new THREE.Vector3(1,1,1),b.acceleration=a.acceleration instanceof THREE.Vector3?a.acceleration:new THREE.Vector3,b.accelerationSpread=a.accelerationSpread instanceof THREE.Vector3?a.accelerationSpread:new THREE.Vector3,b.velocity=a.velocity instanceof THREE.Vector3?a.velocity:new THREE.Vector3,b.velocitySpread=a.velocitySpread instanceof THREE.Vector3?a.velocitySpread:new THREE.Vector3,b.speed=parseFloat("number"==typeof a.speed?a.speed:0,10),b.speedSpread=parseFloat("number"==typeof a.speedSpread?a.speedSpread:0,10),b.size=parseFloat("number"==typeof a.size?a.size:10,10),b.sizeSpread=parseFloat("number"==typeof a.sizeSpread?a.sizeSpread:0,10),b.sizeEnd=parseFloat("number"==typeof a.sizeEnd?a.sizeEnd:10,10),b.colorStart=a.colorStart instanceof THREE.Color?a.colorStart:new THREE.Color("white"),b.colorEnd=a.colorEnd instanceof THREE.Color?a.colorEnd:new THREE.Color("blue"),b.colorSpread=a.colorSpread instanceof THREE.Vector3?a.colorSpread:new THREE.Vector3,b.opacityStart=parseFloat("undefined"!=typeof a.opacityStart?a.opacityStart:1,10),b.opacityEnd=parseFloat("number"==typeof a.opacityEnd?a.opacityEnd:0,10),b.opacityMiddle=parseFloat("undefined"!=typeof a.opacityMiddle?a.opacityMiddle:Math.abs(b.opacityEnd+b.opacityStart)/2,10),b.emitterDuration="number"==typeof a.emitterDuration?a.emitterDuration:null,b.alive=parseInt("number"==typeof a.alive?a.alive:1,10),b.static="number"==typeof a.static?a.static:0,b.numParticles=0,b.attributes=null,b.vertices=null,b.verticesIndex=0,b.age=0,b.maxAge=0,b.particleIndex=0,b.userData={}}ShaderParticleGroup.prototype={_randomVector3:function(a,b){var c=new THREE.Vector3;return c.copy(a),c.x+=Math.random()*b.x-b.x/2,c.y+=Math.random()*b.y-b.y/2,c.z+=Math.random()*b.z-b.z/2,c},_randomColor:function(a,b){var c=new THREE.Color;return c.copy(a),c.r+=Math.random()*b.x-b.x/2,c.g+=Math.random()*b.y-b.y/2,c.b+=Math.random()*b.z-b.z/2,c.r=Math.max(0,Math.min(c.r,1)),c.g=Math.max(0,Math.min(c.g,1)),c.b=Math.max(0,Math.min(c.b,1)),c},_randomFloat:function(a,b){return a+b*(Math.random()-.5)},_randomVector3OnSphere:function(a,b,c){var d=2*Math.random()-1,e=6.2832*Math.random(),f=Math.sqrt(1-d*d),g=new THREE.Vector3(f*Math.cos(e),f*Math.sin(e),d);return g.multiplyScalar(b),c&&g.multiply(c),g.add(a),g},_randomVelocityVector3OnSphere:function(a,b,c,d,e){var f=(new THREE.Vector3).subVectors(a,b);return f.normalize().multiplyScalar(this._randomFloat(c,d)),e&&f.multiply(e),f},_randomizeExistingVector3:function(a,b,c){a.set(Math.random()*b.x-c.x,Math.random()*b.y-c.y,Math.random()*b.z-c.z)},_flagUpdate:function(){var a=this;return a.attributes.age.needsUpdate=!0,a.attributes.alive.needsUpdate=!0,a.geometry.verticesNeedUpdate=!0,a},addEmitter:function(a){var b=this;a.numParticles=a.duration?0|a.particlesPerSecond*(b.maxAgeq;++q)"sphere"===a.type?(c[q]=b._randomVector3OnSphere(a.position,a.radius,a.radiusScale),h[q]=b._randomVelocityVector3OnSphere(c[q],a.position,a.speed,a.speedSpread,a.radiusScale,a.radius)):(c[q]=b._randomVector3(a.position,a.positionSpread),h[q]=b._randomVector3(a.velocity,a.velocitySpread)),g[q]=b._randomVector3(a.acceleration,a.accelerationSpread),k[q]=b._randomFloat(a.size,a.sizeSpread),l[q]=a.sizeEnd,j[q]=0,i[q]=a.static?1:0,m[q]=b._randomColor(a.colorStart,a.colorSpread),n[q]=a.colorEnd,o[q]=a.opacityStart,p[q]=a.opacityMiddle,opacityEnd[q]=a.opacityEnd;return a.verticesIndex=parseFloat(d),a.attributes=b.attributes,a.vertices=b.geometry.vertices,a.maxAge=b.maxAge,a.static||b.emitters.push(a),b},tick:function(a){var b=this,c=b.emitters,d=c.length;if(a=a||b.fixedTimeStep,0!==d){for(var e=0;d>e;++e)c[e].tick(a);return b._flagUpdate(),b}},getFromPool:function(){var a=this,b=a._pool,c=a._createNewWhenPoolEmpty;return b.length?b.pop():c?new ShaderParticleEmitter(a._poolCreationSettings):null},releaseIntoPool:function(a){return a instanceof ShaderParticleEmitter?(a.reset(),this._pool.unshift(a),this):(console.error("Will not add non-emitter to particle group pool:",a),void 0)},getPool:function(){return this._pool},addPool:function(a,b,c){var d,e=this;e._pool,e._poolCreationSettings=b,e._createNewWhenPoolEmpty=!!c;for(var f=0;a>f;++f)d=new ShaderParticleEmitter(b),e.addEmitter(d),e.releaseIntoPool(d);return e},_triggerSingleEmitter:function(a){var b=this,c=b.getFromPool();return null===c?(console.log("ShaderParticleGroup pool ran out."),void 0):(a&&c.position.copy(a),c.enable(),setTimeout(function(){c.disable(),b.releaseIntoPool(c)},b.maxAgeMilliseconds),b)},triggerPoolEmitter:function(a,b){var c=this;if("number"==typeof a&&a>1)for(var d=0;a>d;++d)c._triggerSingleEmitter(b);else c._triggerSingleEmitter(b);return c}},ShaderParticleGroup.shaders={vertex:["uniform float duration;","uniform int hasPerspective;","attribute vec3 customColor;","attribute vec3 customColorEnd;","attribute float opacity;","attribute float opacityMiddle;","attribute float opacityEnd;","attribute vec3 acceleration;","attribute vec3 velocity;","attribute float alive;","attribute float age;","attribute float size;","attribute float sizeEnd;","varying vec4 vColor;","float Lerp( float start, float end, float amount ) {","return (start + ((end - start) * amount));","}","vec3 Lerp( vec3 start, vec3 end, float amount ) {","return (start + ((end - start) * amount));","}","vec4 GetPos() {","vec3 newPos = vec3( position );","vec3 a = acceleration * age;","vec3 v = velocity * age;","v = v + (a * age);","newPos = newPos + v;","vec4 mvPosition = modelViewMatrix * vec4( newPos, 1.0 );","return mvPosition;","}","void main() {","float positionInTime = (age / duration);","float halfDuration = (duration / 2.0);","if( alive > 0.5 ) {","vec3 color = vec3( customColor );","if( customColor != customColorEnd ) {","color = Lerp( customColor, customColorEnd, positionInTime );","}","if( opacity == opacityMiddle && opacityMiddle == opacityEnd ) {","vColor = vec4( color, opacity );","}","else if( positionInTime < 0.5 ) {","vColor = vec4( color, Lerp( opacity, opacityMiddle, age / halfDuration ) );","}","else if( positionInTime > 0.5 ) {","vColor = vec4( color, Lerp( opacityMiddle, opacityEnd, (age - halfDuration) / halfDuration ) );","}","else {","vColor = vec4( color, opacityMiddle );","}","vec4 pos = GetPos();","float pointSize = Lerp( size, sizeEnd, positionInTime );","if( hasPerspective == 1 ) {","pointSize = pointSize * ( 300.0 / length( pos.xyz ) );","}","gl_PointSize = pointSize;","gl_Position = projectionMatrix * pos;","}","else {","vColor = vec4( customColor, 0.0 );","gl_Position = vec4(1e20, 1e20, 1e20, 0);","}","}"].join("\n"),fragment:["uniform sampler2D texture;","uniform int colorize;","varying vec4 vColor;","void main() {","float c = cos(0.0);","float s = sin(0.0);","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);","vec4 rotatedTexture = texture2D( texture, rotatedUV );","if( colorize == 1 ) {","gl_FragColor = vColor * rotatedTexture;","}","else {","gl_FragColor = rotatedTexture;","}","}"].join("\n")},ShaderParticleEmitter.prototype={_resetParticle:function(a){var b=this;spread=b.positionSpread,type=b.type,"cube"===type&&0===spread.x&&0===spread.y&&0===spread.z||"sphere"===type&&0===b.radius?a.copy(b.position):"cube"===type?b._randomizeExistingVector3(a,b.position,spread):"sphere"===type&&b._randomizeExistingVector3OnSphere(a,b.position,b.radius)},_randomizeExistingVector3:function(a,b,c){var d=Math.random;a.copy(b),a.x+=d()*c.x-c.x/2,a.y+=d()*c.y-c.y/2,a.z+=d()*c.z-c.z/2},_randomizeExistingVector3OnSphere:function(a,b,c){var d=Math.random,e=2*d()-1,f=6.2832*d(),g=Math.sqrt(1-e*e),h=g*Math.cos(f)*c,i=g*Math.sin(f)*c,e=e*c;a.set(h,i,e).multiply(this.radiusScale),a.add(b)},tick:function(a){if(!this.static){for(var b=this,c=b.attributes,d=c.alive.value,e=c.age.value,f=b.verticesIndex,g=b.numParticles,h=f+g,i=b.particlesPerSecond,j=i*a,k=b.maxAge,l=b.age,m=b.emitterDuration,n=b.particleIndex,o=f;h>o;++o)1===d[o]&&(e[o]+=a),e[o]>=k&&(e[o]=0,d[o]=0);if(0===b.alive)return b.age=0,void 0;if("number"==typeof m&&l>m)return b.alive=0,b.age=0,void 0;var p=Math.min(h,n+j);for(o=0|n;p>o;++o)1!==d[o]&&(d[o]=1,b._resetParticle(b.vertices[o]));b.particleIndex+=j,n>=f+b.numParticles&&(b.particleIndex=parseFloat(f,10)),b.age+=a}},reset:function(a){var b=this;if(b.age=0,b.alive=0,a)for(var c=b.verticesIndex,d=b.verticesIndex+b.numParticles,e=b.attributes,f=e.alive.value,g=e.age.value,h=c;d>h;++h)f[h]=0,g[h]=0;return b},enable:function(){this.alive=1},disable:function(){this.alive=0}}; -------------------------------------------------------------------------------- /examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Basic Shader Particles 4 | 5 | 6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /examples/clouds.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Basic Shader Particles 4 | 5 | 6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /examples/css/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | .numParticles { 7 | position: absolute; 8 | bottom: 10px; 9 | left: 10px; 10 | color: white; 11 | text-shadow: 0 0 3px rgba(0, 0, 0, 0.8); 12 | } -------------------------------------------------------------------------------- /examples/img/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stemkoski/ShaderParticleEngine/1073898d06a1abdb67298b1df4c2138278aaac90/examples/img/cloud.png -------------------------------------------------------------------------------- /examples/img/cloudSml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stemkoski/ShaderParticleEngine/1073898d06a1abdb67298b1df4c2138278aaac90/examples/img/cloudSml.png -------------------------------------------------------------------------------- /examples/img/smokeparticle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stemkoski/ShaderParticleEngine/1073898d06a1abdb67298b1df4c2138278aaac90/examples/img/smokeparticle.png -------------------------------------------------------------------------------- /examples/img/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stemkoski/ShaderParticleEngine/1073898d06a1abdb67298b1df4c2138278aaac90/examples/img/star.png -------------------------------------------------------------------------------- /examples/js/Pool.js: -------------------------------------------------------------------------------- 1 | function Pool( numItems, object, creationArgument ) { 2 | var store = []; 3 | 4 | this.get = function() { 5 | if(store.length) { 6 | return store.pop(); 7 | } 8 | else { 9 | return new object( creationArgument ); 10 | } 11 | }; 12 | 13 | this.release = function( o ) { 14 | this.reset( o ); 15 | store.unshift( o ); 16 | }; 17 | 18 | this.reset = function() {}; 19 | 20 | this.getLength = function() { 21 | return store.length; 22 | }; 23 | 24 | this.getStore = function() { 25 | return store; 26 | }; 27 | 28 | for( var i = 0, o; i < numItems; ++i ) { 29 | o = new object( creationArgument ); 30 | store.push( o ); 31 | } 32 | } -------------------------------------------------------------------------------- /examples/js/Stats.min.js: -------------------------------------------------------------------------------- 1 | // stats.js - http://github.com/mrdoob/stats.js 2 | var Stats=function(){var l=Date.now(),m=l,g=0,n=Infinity,o=0,h=0,p=Infinity,q=0,r=0,s=0,f=document.createElement("div");f.id="stats";f.addEventListener("mousedown",function(b){b.preventDefault();t(++s%2)},!1);f.style.cssText="width:80px;opacity:0.9;cursor:pointer";var a=document.createElement("div");a.id="fps";a.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#002";f.appendChild(a);var i=document.createElement("div");i.id="fpsText";i.style.cssText="color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px"; 3 | i.innerHTML="FPS";a.appendChild(i);var c=document.createElement("div");c.id="fpsGraph";c.style.cssText="position:relative;width:74px;height:30px;background-color:#0ff";for(a.appendChild(c);74>c.children.length;){var j=document.createElement("span");j.style.cssText="width:1px;height:30px;float:left;background-color:#113";c.appendChild(j)}var d=document.createElement("div");d.id="ms";d.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#020;display:none";f.appendChild(d);var k=document.createElement("div"); 4 | k.id="msText";k.style.cssText="color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px";k.innerHTML="MS";d.appendChild(k);var e=document.createElement("div");e.id="msGraph";e.style.cssText="position:relative;width:74px;height:30px;background-color:#0f0";for(d.appendChild(e);74>e.children.length;)j=document.createElement("span"),j.style.cssText="width:1px;height:30px;float:left;background-color:#131",e.appendChild(j);var t=function(b){s=b;switch(s){case 0:a.style.display= 5 | "block";d.style.display="none";break;case 1:a.style.display="none",d.style.display="block"}};return{REVISION:11,domElement:f,setMode:t,begin:function(){l=Date.now()},end:function(){var b=Date.now();g=b-l;n=Math.min(n,g);o=Math.max(o,g);k.textContent=g+" MS ("+n+"-"+o+")";var a=Math.min(30,30-30*(g/200));e.appendChild(e.firstChild).style.height=a+"px";r++;b>m+1E3&&(h=Math.round(1E3*r/(b-m)),p=Math.min(p,h),q=Math.max(q,h),i.textContent=h+" FPS ("+p+"-"+q+")",a=Math.min(30,30-30*(h/100)),c.appendChild(c.firstChild).style.height= 6 | a+"px",m=b,r=0);return b},update:function(){l=this.end()}}}; 7 | -------------------------------------------------------------------------------- /examples/mouseFollow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Basic Shader Particles 4 | 5 | 6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /examples/multipleEmitters.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Basic Shader Particles 4 | 5 | 6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /examples/pool.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic Shader Particles 5 | 6 | 7 | 8 |

Click mouse or press any key to trigger an explosion.

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /examples/sandbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic Shader Particles 5 | 6 | 7 | 8 |

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /examples/sphere.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Basic Shader Particles 4 | 5 | 6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /examples/spherePulsing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Basic Shader Particles 4 | 5 | 6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /examples/starfield.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Basic Shader Particles 4 | 5 | 6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /examples/static.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Basic Shader Particles 4 | 5 | 6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Shader-Particles", 3 | "version": "0.5.0", 4 | "devDependencies": { 5 | "grunt": "~0.4.1", 6 | "grunt-contrib-concat": "~0.3.0", 7 | "grunt-contrib-uglify": "~0.2.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ShaderParticleEmitter.js: -------------------------------------------------------------------------------- 1 | // ShaderParticleEmitter 0.5.0 2 | // 3 | // (c) 2013 Luke Moody (http://www.github.com/squarefeet) & Lee Stemkoski (http://www.adelphi.edu/~stemkoski/) 4 | // Based on Lee Stemkoski's original work (https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/js/ParticleEngine.js). 5 | // 6 | // ShaderParticleEmitter may be freely distributed under the MIT license (See LICENSE.txt) 7 | 8 | function ShaderParticleEmitter( options ) { 9 | // If no options are provided, fallback to an empty object. 10 | options = options || {}; 11 | 12 | // Helps with minification. Not as easy to read the following code, 13 | // but should still be readable enough! 14 | var that = this; 15 | 16 | 17 | that.particlesPerSecond = typeof options.particlesPerSecond === 'number' ? options.particlesPerSecond : 100; 18 | that.type = (options.type === 'cube' || options.type === 'sphere') ? options.type : 'cube'; 19 | 20 | that.position = options.position instanceof THREE.Vector3 ? options.position : new THREE.Vector3(); 21 | that.positionSpread = options.positionSpread instanceof THREE.Vector3 ? options.positionSpread : new THREE.Vector3(); 22 | 23 | // These two properties are only used when this.type === 'sphere' 24 | that.radius = typeof options.radius === 'number' ? options.radius : 10; 25 | that.radiusScale = options.radiusScale instanceof THREE.Vector3 ? options.radiusScale : new THREE.Vector3(1, 1, 1); 26 | 27 | that.acceleration = options.acceleration instanceof THREE.Vector3 ? options.acceleration : new THREE.Vector3(); 28 | that.accelerationSpread = options.accelerationSpread instanceof THREE.Vector3 ? options.accelerationSpread : new THREE.Vector3(); 29 | 30 | that.velocity = options.velocity instanceof THREE.Vector3 ? options.velocity : new THREE.Vector3(); 31 | that.velocitySpread = options.velocitySpread instanceof THREE.Vector3 ? options.velocitySpread : new THREE.Vector3(); 32 | 33 | // And again here; only used when this.type === 'sphere' 34 | that.speed = parseFloat( typeof options.speed === 'number' ? options.speed : 0, 10 ); 35 | that.speedSpread = parseFloat( typeof options.speedSpread === 'number' ? options.speedSpread : 0, 10 ); 36 | 37 | that.size = parseFloat( typeof options.size === 'number' ? options.size : 10.0, 10 ); 38 | that.sizeSpread = parseFloat( typeof options.sizeSpread === 'number' ? options.sizeSpread : 0, 10 ); 39 | that.sizeEnd = parseFloat( typeof options.sizeEnd === 'number' ? options.sizeEnd : 10.0, 10 ); 40 | 41 | that.colorStart = options.colorStart instanceof THREE.Color ? options.colorStart : new THREE.Color( 'white' ); 42 | that.colorEnd = options.colorEnd instanceof THREE.Color ? options.colorEnd : new THREE.Color( 'blue' ); 43 | that.colorSpread = options.colorSpread instanceof THREE.Vector3 ? options.colorSpread : new THREE.Vector3(); 44 | 45 | that.opacityStart = parseFloat( typeof options.opacityStart !== 'undefined' ? options.opacityStart : 1, 10 ); 46 | that.opacityEnd = parseFloat( typeof options.opacityEnd === 'number' ? options.opacityEnd : 0, 10 ); 47 | that.opacityMiddle = parseFloat( 48 | typeof options.opacityMiddle !== 'undefined' ? 49 | options.opacityMiddle : 50 | Math.abs(that.opacityEnd + that.opacityStart) / 2, 51 | 10 ); 52 | 53 | that.emitterDuration = typeof options.emitterDuration === 'number' ? options.emitterDuration : null; 54 | that.alive = parseInt( typeof options.alive === 'number' ? options.alive : 1, 10); 55 | 56 | that.static = typeof options.static === 'number' ? options.static : 0; 57 | 58 | // The following properties are used internally, and mostly set when this emitter 59 | // is added to a particle group. 60 | that.numParticles = 0; 61 | that.attributes = null; 62 | that.vertices = null; 63 | that.verticesIndex = 0; 64 | that.age = 0.0; 65 | that.maxAge = 0.0; 66 | 67 | that.particleIndex = 0.0; 68 | 69 | that.userData = {}; 70 | } 71 | 72 | 73 | ShaderParticleEmitter.prototype = { 74 | 75 | /** 76 | * Reset a particle's position. Accounts for emitter type and spreads. 77 | * 78 | * @private 79 | * 80 | * @param {THREE.Vector3} p 81 | */ 82 | _resetParticle: function( p ) { 83 | var that = this; 84 | spread = that.positionSpread, 85 | type = that.type; 86 | 87 | // Optimise for no position spread or radius 88 | if( 89 | ( type === 'cube' && spread.x === 0 && spread.y === 0 && spread.z === 0 ) || 90 | ( type === 'sphere' && that.radius === 0 ) 91 | ) { 92 | p.copy( that.position ); 93 | } 94 | 95 | // If there is a position spread, then get a new position based on this spread. 96 | else if( type === 'cube' ) { 97 | that._randomizeExistingVector3( p, that.position, spread ); 98 | } 99 | 100 | else if( type === 'sphere') { 101 | that._randomizeExistingVector3OnSphere( p, that.position, that.radius ); 102 | } 103 | }, 104 | 105 | 106 | /** 107 | * Given an existing particle vector, randomise it based on base and spread vectors 108 | * 109 | * @private 110 | * 111 | * @param {THREE.Vector3} v 112 | * @param {THREE.Vector3} base 113 | * @param {THREE.Vector3} spread 114 | */ 115 | _randomizeExistingVector3: function( v, base, spread ) { 116 | var r = Math.random; 117 | 118 | v.copy( base ); 119 | 120 | v.x += r() * spread.x - (spread.x/2); 121 | v.y += r() * spread.y - (spread.y/2); 122 | v.z += r() * spread.z - (spread.z/2); 123 | }, 124 | 125 | 126 | /** 127 | * Given an existing particle vector, project it onto a random point on a 128 | * sphere with radius `radius` and position `base`. 129 | * 130 | * @private 131 | * 132 | * @param {THREE.Vector3} v 133 | * @param {THREE.Vector3} base 134 | * @param {Number} radius 135 | */ 136 | _randomizeExistingVector3OnSphere: function( v, base, radius ) { 137 | var rand = Math.random; 138 | 139 | var z = 2 * rand() - 1; 140 | var t = 6.2832 * rand(); 141 | var r = Math.sqrt( 1 - z*z ); 142 | 143 | var x = ((r * Math.cos(t)) * radius); 144 | var y = ((r * Math.sin(t)) * radius); 145 | var z = (z * radius); 146 | 147 | v.set(x, y, z).multiply( this.radiusScale ); 148 | 149 | v.add( base ); 150 | }, 151 | 152 | 153 | // This function is called by the instance of `ShaderParticleEmitter` that 154 | // this emitter has been added to. 155 | /** 156 | * Update this emitter's particle's positions. Called by the ShaderParticleGroup 157 | * that this emitter belongs to. 158 | * 159 | * @param {Number} dt 160 | */ 161 | tick: function( dt ) { 162 | 163 | if( this.static ) { 164 | return; 165 | } 166 | 167 | // Cache some values for quicker access in loops. 168 | var that = this, 169 | a = that.attributes, 170 | alive = a.alive.value, 171 | age = a.age.value, 172 | start = that.verticesIndex, 173 | numParticles = that.numParticles, 174 | end = start + numParticles, 175 | pps = that.particlesPerSecond, 176 | ppsdt = pps * dt, 177 | m = that.maxAge, 178 | emitterAge = that.age, 179 | duration = that.emitterDuration, 180 | pIndex = that.particleIndex; 181 | 182 | // Loop through all the particles in this emitter and 183 | // determine whether they're still alive and need advancing 184 | // or if they should be dead and therefore marked as such 185 | // and pushed into the recycled vertices array for reuse. 186 | for( var i = start; i < end; ++i ) { 187 | if( alive[ i ] === 1.0 ) { 188 | age[ i ] += dt; 189 | } 190 | 191 | if( age[ i ] >= m ) { 192 | age[ i ] = 0.0; 193 | alive[ i ] = 0.0; 194 | } 195 | } 196 | 197 | // If the emitter is dead, reset any particles that are in 198 | // the recycled vertices array and reset the age of the 199 | // emitter to zero ready to go again if required, then 200 | // exit this function. 201 | if( that.alive === 0 ) { 202 | that.age = 0.0; 203 | return; 204 | } 205 | 206 | // If the emitter has a specified lifetime and we've exceeded it, 207 | // mark the emitter as dead and exit this function. 208 | if( typeof duration === 'number' && emitterAge > duration ) { 209 | that.alive = 0; 210 | that.age = 0.0; 211 | return; 212 | } 213 | 214 | var n = Math.min( end, pIndex + ppsdt ); 215 | 216 | for( i = pIndex | 0; i < n; ++i ) { 217 | if( alive[ i ] !== 1.0 ) { 218 | alive[ i ] = 1.0; 219 | that._resetParticle( that.vertices[ i ] ); 220 | } 221 | } 222 | 223 | that.particleIndex += ppsdt; 224 | 225 | if( pIndex >= start + that.numParticles ) { 226 | that.particleIndex = parseFloat( start, 10 ); 227 | } 228 | 229 | // Add the delta time value to the age of the emitter. 230 | that.age += dt; 231 | }, 232 | 233 | /** 234 | * Reset this emitter back to its starting position. 235 | * If `force` is truthy, then reset all particles in this 236 | * emitter as well, even if they're currently alive. 237 | * 238 | * @param {Boolean} force 239 | * @return {this} 240 | */ 241 | reset: function( force ) { 242 | var that = this; 243 | 244 | that.age = 0.0; 245 | that.alive = 0; 246 | 247 | if( force ) { 248 | var start = that.verticesIndex, 249 | end = that.verticesIndex + that.numParticles, 250 | a = that.attributes, 251 | alive = a.alive.value, 252 | age = a.age.value; 253 | 254 | for( var i = start; i < end; ++i ) { 255 | alive[ i ] = 0.0; 256 | age[ i ] = 0.0; 257 | } 258 | } 259 | 260 | return that; 261 | }, 262 | 263 | 264 | /** 265 | * Enable this emitter. 266 | */ 267 | enable: function() { 268 | this.alive = 1; 269 | }, 270 | 271 | /** 272 | * Disable this emitter. 273 | */ 274 | disable: function() { 275 | this.alive = 0; 276 | } 277 | }; 278 | -------------------------------------------------------------------------------- /src/ShaderParticleGroup.js: -------------------------------------------------------------------------------- 1 | // ShaderParticleGroup 0.5.0 2 | // 3 | // (c) 2013 Luke Moody (http://www.github.com/squarefeet) & Lee Stemkoski (http://www.adelphi.edu/~stemkoski/) 4 | // Based on Lee Stemkoski's original work (https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/js/ParticleEngine.js). 5 | // 6 | // ShaderParticleGroup may be freely distributed under the MIT license (See LICENSE.txt) 7 | 8 | 9 | function ShaderParticleGroup( options ) { 10 | var that = this; 11 | 12 | that.fixedTimeStep = parseFloat( options.fixedTimeStep || 0.016, 10 ); 13 | 14 | // Uniform properties ( applied to all particles ) 15 | that.maxAge = parseFloat( options.maxAge || 3 ); 16 | that.texture = options.texture || null; 17 | that.hasPerspective = parseInt( typeof options.hasPerspective === 'number' ? options.hasPerspective : 1 ); 18 | that.colorize = parseInt( options.colorize || 1 ); 19 | 20 | // Material properties 21 | that.blending = typeof options.blending === 'number' ? options.blending : THREE.AdditiveBlending; 22 | that.transparent = options.transparent || true; 23 | that.alphaTest = options.alphaTest || 0.5; 24 | that.depthWrite = options.depthWrite || false; 25 | that.depthTest = options.depthTest || true; 26 | 27 | // Create uniforms 28 | that.uniforms = { 29 | duration: { type: 'f', value: that.maxAge }, 30 | texture: { type: 't', value: that.texture }, 31 | hasPerspective: { type: 'i', value: that.hasPerspective }, 32 | colorize: { type: 'i', value: that.colorize } 33 | }; 34 | 35 | // Create a map of attributes that will hold values for each particle in this group. 36 | that.attributes = { 37 | acceleration: { type: 'v3', value: [] }, 38 | velocity: { type: 'v3', value: [] }, 39 | alive: { type: 'f', value: [] }, 40 | age: { type: 'f', value: [] }, 41 | size: { type: 'f', value: [] }, 42 | sizeEnd: { type: 'f', value: [] }, 43 | 44 | customColor: { type: 'c', value: [] }, 45 | customColorEnd: { type: 'c', value: [] }, 46 | 47 | opacity: { type: 'f', value: [] }, 48 | opacityMiddle: { type: 'f', value: [] }, 49 | opacityEnd: { type: 'f', value: [] } 50 | }; 51 | 52 | // Emitters (that aren't static) will be added to this array for 53 | // processing during the `tick()` function. 54 | that.emitters = []; 55 | 56 | // Create properties for use by the emitter pooling functions. 57 | that._pool = []; 58 | that._poolCreationSettings = null; 59 | that._createNewWhenPoolEmpty = 0; 60 | that.maxAgeMilliseconds = that.maxAge * 1000; 61 | 62 | // Create an empty geometry to hold the particles. 63 | // Each particle is a vertex pushed into this geometry's 64 | // vertices array. 65 | that.geometry = new THREE.Geometry(); 66 | 67 | // Create the shader material using the properties we set above. 68 | that.material = new THREE.ShaderMaterial({ 69 | uniforms: that.uniforms, 70 | attributes: that.attributes, 71 | vertexShader: ShaderParticleGroup.shaders.vertex, 72 | fragmentShader: ShaderParticleGroup.shaders.fragment, 73 | blending: that.blending, 74 | transparent: that.transparent, 75 | alphaTest: that.alphaTest, 76 | depthWrite: that.depthWrite, 77 | depthTest: that.depthTest, 78 | }); 79 | 80 | // And finally create the ParticleSystem. It's got its `dynamic` property 81 | // set so that THREE.js knows to update it on each frame. 82 | that.mesh = new THREE.ParticleSystem( that.geometry, that.material ); 83 | that.mesh.dynamic = true; 84 | } 85 | 86 | ShaderParticleGroup.prototype = { 87 | 88 | /** 89 | * Given a base vector and a spread range vector, create 90 | * a new THREE.Vector3 instance with randomised values. 91 | * 92 | * @private 93 | * 94 | * @param {THREE.Vector3} base 95 | * @param {THREE.Vector3} spread 96 | * @return {THREE.Vector3} 97 | */ 98 | _randomVector3: function( base, spread ) { 99 | var v = new THREE.Vector3(); 100 | 101 | v.copy( base ); 102 | 103 | v.x += Math.random() * spread.x - (spread.x/2); 104 | v.y += Math.random() * spread.y - (spread.y/2); 105 | v.z += Math.random() * spread.z - (spread.z/2); 106 | 107 | return v; 108 | }, 109 | 110 | /** 111 | * Create a new THREE.Color instance and given a base vector and 112 | * spread range vector, assign random values. 113 | * 114 | * Note that THREE.Color RGB values are in the range of 0 - 1, not 0 - 255. 115 | * 116 | * @private 117 | * 118 | * @param {THREE.Vector3} base 119 | * @param {THREE.Vector3} spread 120 | * @return {THREE.Color} 121 | */ 122 | _randomColor: function( base, spread ) { 123 | var v = new THREE.Color(); 124 | 125 | v.copy( base ); 126 | 127 | v.r += (Math.random() * spread.x) - (spread.x/2); 128 | v.g += (Math.random() * spread.y) - (spread.y/2); 129 | v.b += (Math.random() * spread.z) - (spread.z/2); 130 | 131 | v.r = Math.max( 0, Math.min( v.r, 1 ) ); 132 | v.g = Math.max( 0, Math.min( v.g, 1 ) ); 133 | v.b = Math.max( 0, Math.min( v.b, 1 ) ); 134 | 135 | return v; 136 | }, 137 | 138 | 139 | /** 140 | * Create a random Number value based on an initial value and 141 | * a spread range 142 | * 143 | * @private 144 | * 145 | * @param {Number} base 146 | * @param {Number} spread 147 | * @return {Number} 148 | */ 149 | _randomFloat: function( base, spread ) { 150 | return base + spread * (Math.random() - 0.5); 151 | }, 152 | 153 | 154 | /** 155 | * Create a new THREE.Vector3 instance and project it onto a random point 156 | * on a sphere with radius `radius`. 157 | * 158 | * @param {THREE.Vector3} base 159 | * @param {Number} radius 160 | * @param {THREE.Vector3} scale 161 | * 162 | * @private 163 | * 164 | * @return {THREE.Vector3} 165 | */ 166 | _randomVector3OnSphere: function( base, radius, scale ) { 167 | var z = 2 * Math.random() - 1; 168 | var t = 6.2832 * Math.random(); 169 | var r = Math.sqrt( 1 - z*z ); 170 | var vec = new THREE.Vector3( r * Math.cos(t), r * Math.sin(t), z ); 171 | 172 | vec.multiplyScalar( radius ); 173 | 174 | if( scale ) { 175 | vec.multiply( scale ); 176 | } 177 | 178 | vec.add( base ); 179 | 180 | return vec; 181 | }, 182 | 183 | 184 | /** 185 | * Create a new THREE.Vector3 instance, and given a base position, and various 186 | * other values, project it onto a random point on a sphere with radius `radius`. 187 | * 188 | * @param {THREE.Vector3} base 189 | * @param {THREE.Vector3} position 190 | * @param {Number} speed 191 | * @param {Number} speedSpread 192 | * @param {THREE.Vector3} scale 193 | * @param {Number} radius 194 | * 195 | * @private 196 | * 197 | * @return {THREE.Vector3} 198 | */ 199 | _randomVelocityVector3OnSphere: function( base, position, speed, speedSpread, scale, radius ) { 200 | var direction = new THREE.Vector3().subVectors( base, position ); 201 | 202 | direction.normalize().multiplyScalar( this._randomFloat( speed, speedSpread ) ); 203 | 204 | if( scale ) { 205 | direction.multiply( scale ); 206 | } 207 | 208 | return direction; 209 | }, 210 | 211 | 212 | /** 213 | * Given a base vector and a spread vector, randomise the given vector 214 | * accordingly. 215 | * 216 | * @param {THREE.Vector3} vector 217 | * @param {THREE.Vector3} base 218 | * @param {THREE.Vector3} spread 219 | * 220 | * @private 221 | * 222 | * @return {[type]} 223 | */ 224 | _randomizeExistingVector3: function( vector, base, spread ) { 225 | vector.set( 226 | Math.random() * base.x - spread.x, 227 | Math.random() * base.y - spread.y, 228 | Math.random() * base.z - spread.z 229 | ); 230 | }, 231 | 232 | 233 | /** 234 | * Tells the age and alive attributes (and the geometry vertices) 235 | * that they need updating by THREE.js's internal tick functions. 236 | * 237 | * @private 238 | * 239 | * @return {this} 240 | */ 241 | _flagUpdate: function() { 242 | var that = this; 243 | 244 | // Set flags to update (causes less garbage than 245 | // ```ParticleSystem.sortParticles = true``` in THREE.r58 at least) 246 | that.attributes.age.needsUpdate = true; 247 | that.attributes.alive.needsUpdate = true; 248 | that.geometry.verticesNeedUpdate = true; 249 | 250 | return that; 251 | }, 252 | 253 | 254 | /** 255 | * Add an emitter to this particle group. Once added, an emitter will be automatically 256 | * updated when ShaderParticleGroup#tick() is called. 257 | * 258 | * @param {ShaderParticleEmitter} emitter 259 | * @return {this} 260 | */ 261 | addEmitter: function( emitter ) { 262 | var that = this; 263 | 264 | if( emitter.duration ) { 265 | emitter.numParticles = emitter.particlesPerSecond * (that.maxAge < emitter.emitterDuration ? that.maxAge : emitter.emitterDuration) | 0; 266 | } 267 | else { 268 | emitter.numParticles = emitter.particlesPerSecond * that.maxAge | 0; 269 | } 270 | 271 | emitter.numParticles = Math.ceil(emitter.numParticles); 272 | 273 | var vertices = that.geometry.vertices, 274 | start = vertices.length, 275 | end = emitter.numParticles + start, 276 | a = that.attributes, 277 | acceleration = a.acceleration.value, 278 | velocity = a.velocity.value, 279 | alive = a.alive.value, 280 | age = a.age.value, 281 | size = a.size.value, 282 | sizeEnd = a.sizeEnd.value, 283 | customColor = a.customColor.value, 284 | customColorEnd = a.customColorEnd.value, 285 | opacity = a.opacity.value, 286 | opacityMiddle = a.opacityMiddle.value; 287 | opacityEnd = a.opacityEnd.value; 288 | 289 | emitter.particleIndex = parseFloat( start, 10 ); 290 | 291 | // Create the values 292 | for( var i = start; i < end; ++i ) { 293 | 294 | if( emitter.type === 'sphere' ) { 295 | vertices[i] = that._randomVector3OnSphere( emitter.position, emitter.radius, emitter.radiusScale ); 296 | velocity[i] = that._randomVelocityVector3OnSphere( vertices[i], emitter.position, emitter.speed, emitter.speedSpread, emitter.radiusScale, emitter.radius ); 297 | } 298 | else { 299 | vertices[i] = that._randomVector3( emitter.position, emitter.positionSpread ); 300 | velocity[i] = that._randomVector3( emitter.velocity, emitter.velocitySpread ); 301 | } 302 | 303 | 304 | acceleration[i] = that._randomVector3( emitter.acceleration, emitter.accelerationSpread ); 305 | 306 | // Fix for bug #1 (https://github.com/squarefeet/ShaderParticleEngine/issues/1) 307 | // For some stupid reason I was limiting the size value to a minimum of 0.1. Derp. 308 | size[i] = that._randomFloat( emitter.size, emitter.sizeSpread ); 309 | sizeEnd[i] = emitter.sizeEnd; 310 | age[i] = 0.0; 311 | alive[i] = emitter.static ? 1.0 : 0.0; 312 | 313 | 314 | customColor[i] = that._randomColor( emitter.colorStart, emitter.colorSpread ); 315 | customColorEnd[i] = emitter.colorEnd; 316 | opacity[i] = emitter.opacityStart; 317 | opacityMiddle[i] = emitter.opacityMiddle; 318 | opacityEnd[i] = emitter.opacityEnd; 319 | } 320 | 321 | // Cache properties on the emitter so we can access 322 | // them from its tick function. 323 | emitter.verticesIndex = parseFloat( start ); 324 | emitter.attributes = that.attributes; 325 | emitter.vertices = that.geometry.vertices; 326 | emitter.maxAge = that.maxAge; 327 | 328 | // Save this emitter in an array for processing during this.tick() 329 | if( !emitter.static ) { 330 | that.emitters.push( emitter ); 331 | } 332 | 333 | return that; 334 | }, 335 | 336 | 337 | /** 338 | * The main particle group update function. Call this once per frame. 339 | * 340 | * @param {Number} dt 341 | * @return {this} 342 | */ 343 | tick: function( dt ) { 344 | var that = this, 345 | emitters = that.emitters, 346 | numEmitters = emitters.length; 347 | 348 | dt = dt || that.fixedTimeStep; 349 | 350 | if( numEmitters === 0 ) return; 351 | 352 | for( var i = 0; i < numEmitters; ++i ) { 353 | emitters[i].tick( dt ); 354 | } 355 | 356 | that._flagUpdate(); 357 | return that; 358 | }, 359 | 360 | 361 | /** 362 | * Fetch a single emitter instance from the pool. 363 | * If there are no objects in the pool, a new emitter will be 364 | * created if specified. 365 | * 366 | * @return {ShaderParticleEmitter | null} 367 | */ 368 | getFromPool: function() { 369 | var that = this, 370 | pool = that._pool, 371 | createNew = that._createNewWhenPoolEmpty; 372 | 373 | if( pool.length ) { 374 | return pool.pop(); 375 | } 376 | else if( createNew ) { 377 | return new ShaderParticleEmitter( that._poolCreationSettings ); 378 | } 379 | 380 | return null; 381 | }, 382 | 383 | 384 | /** 385 | * Release an emitter into the pool. 386 | * 387 | * @param {ShaderParticleEmitter} emitter 388 | * @return {this} 389 | */ 390 | releaseIntoPool: function( emitter ) { 391 | if( !(emitter instanceof ShaderParticleEmitter) ) { 392 | console.error( 'Will not add non-emitter to particle group pool:', emitter ); 393 | return; 394 | } 395 | 396 | emitter.reset(); 397 | this._pool.unshift( emitter ); 398 | 399 | return this; 400 | }, 401 | 402 | 403 | /** 404 | * Get the pool array 405 | * 406 | * @return {Array} 407 | */ 408 | getPool: function() { 409 | return this._pool; 410 | }, 411 | 412 | 413 | /** 414 | * Add a pool of emitters to this particle group 415 | * 416 | * @param {Number} numEmitters The number of emitters to add to the pool. 417 | * @param {Object} emitterSettings An object describing the settings to pass to each emitter. 418 | * @param {Boolean} createNew Should a new emitter be created if the pool runs out? 419 | * @return {this} 420 | */ 421 | addPool: function( numEmitters, emitterSettings, createNew ) { 422 | var that = this, 423 | pool = that._pool, 424 | emitter; 425 | 426 | // Save relevant settings and flags. 427 | that._poolCreationSettings = emitterSettings; 428 | that._createNewWhenPoolEmpty = !!createNew; 429 | 430 | // Create the emitters, add them to this group and the pool. 431 | for( var i = 0; i < numEmitters; ++i ) { 432 | emitter = new ShaderParticleEmitter( emitterSettings ); 433 | that.addEmitter( emitter ); 434 | that.releaseIntoPool( emitter ); 435 | } 436 | 437 | return that; 438 | }, 439 | 440 | 441 | /** 442 | * Internal method. Sets a single emitter to be alive 443 | * 444 | * @private 445 | * 446 | * @param {THREE.Vector3} pos 447 | * @return {this} 448 | */ 449 | _triggerSingleEmitter: function( pos ) { 450 | var that = this, 451 | emitter = that.getFromPool(); 452 | 453 | if( emitter === null ) { 454 | console.log('ShaderParticleGroup pool ran out.'); 455 | return; 456 | } 457 | 458 | // TODO: Should an instanceof check happen here? Or maybe at least a typeof? 459 | if( pos ) { 460 | emitter.position.copy( pos ); 461 | } 462 | 463 | emitter.enable(); 464 | 465 | setTimeout( function() { 466 | emitter.disable(); 467 | that.releaseIntoPool( emitter ); 468 | }, that.maxAgeMilliseconds ); 469 | 470 | return that; 471 | }, 472 | 473 | 474 | /** 475 | * Set a given number of emitters as alive, with an optional position 476 | * vector3 to move them to. 477 | * 478 | * @param {Number} numEmitters 479 | * @param {THREE.Vector3} position 480 | * @return {this} 481 | */ 482 | triggerPoolEmitter: function( numEmitters, position ) { 483 | var that = this; 484 | 485 | if( typeof numEmitters === 'number' && numEmitters > 1) { 486 | for( var i = 0; i < numEmitters; ++i ) { 487 | that._triggerSingleEmitter( position ); 488 | } 489 | } 490 | else { 491 | that._triggerSingleEmitter( position ); 492 | } 493 | 494 | return that; 495 | } 496 | }; 497 | 498 | 499 | 500 | // The all-important shaders 501 | ShaderParticleGroup.shaders = { 502 | vertex: [ 503 | 'uniform float duration;', 504 | 'uniform int hasPerspective;', 505 | 506 | 'attribute vec3 customColor;', 507 | 'attribute vec3 customColorEnd;', 508 | 'attribute float opacity;', 509 | 'attribute float opacityMiddle;', 510 | 'attribute float opacityEnd;', 511 | 512 | 'attribute vec3 acceleration;', 513 | 'attribute vec3 velocity;', 514 | 'attribute float alive;', 515 | 'attribute float age;', 516 | 'attribute float size;', 517 | 'attribute float sizeEnd;', 518 | 519 | 'varying vec4 vColor;', 520 | 521 | // Linearly lerp a float 522 | 'float Lerp( float start, float end, float amount ) {', 523 | 'return (start + ((end - start) * amount));', 524 | '}', 525 | 526 | // Linearly lerp a vector3 527 | 'vec3 Lerp( vec3 start, vec3 end, float amount ) {', 528 | 'return (start + ((end - start) * amount));', 529 | '}', 530 | 531 | // Integrate acceleration into velocity and apply it to the particle's position 532 | 'vec4 GetPos() {', 533 | 'vec3 newPos = vec3( position );', 534 | 535 | // Move acceleration & velocity vectors to the value they 536 | // should be at the current age 537 | 'vec3 a = acceleration * age;', 538 | 'vec3 v = velocity * age;', 539 | 540 | // Move velocity vector to correct values at this age 541 | 'v = v + (a * age);', 542 | 543 | // Add velocity vector to the newPos vector 544 | 'newPos = newPos + v;', 545 | 546 | // Convert the newPos vector into world-space 547 | 'vec4 mvPosition = modelViewMatrix * vec4( newPos, 1.0 );', 548 | 549 | 'return mvPosition;', 550 | '}', 551 | 552 | 553 | 'void main() {', 554 | 555 | 'float positionInTime = (age / duration);', 556 | 'float halfDuration = (duration / 2.0);', 557 | 558 | 'if( alive > 0.5 ) {', 559 | // Integrate color "tween" 560 | 'vec3 color = vec3( customColor );', 561 | 'if( customColor != customColorEnd ) {', 562 | 'color = Lerp( customColor, customColorEnd, positionInTime );', 563 | '}', 564 | 565 | // Store the color of this particle in the varying vColor, 566 | // so frag shader can access it. 567 | 'if( opacity == opacityMiddle && opacityMiddle == opacityEnd ) {', 568 | 'vColor = vec4( color, opacity );', 569 | '}', 570 | 571 | 'else if( positionInTime < 0.5 ) {', 572 | 'vColor = vec4( color, Lerp( opacity, opacityMiddle, age / halfDuration ) );', 573 | '}', 574 | 575 | 'else if( positionInTime > 0.5 ) {', 576 | 'vColor = vec4( color, Lerp( opacityMiddle, opacityEnd, (age - halfDuration) / halfDuration ) );', 577 | '}', 578 | 579 | 'else {', 580 | 'vColor = vec4( color, opacityMiddle );', 581 | '}', 582 | 583 | // Get the position of this particle so we can use it 584 | // when we calculate any perspective that might be required. 585 | 'vec4 pos = GetPos();', 586 | 587 | // Determine point size . 588 | 'float pointSize = Lerp( size, sizeEnd, positionInTime );', 589 | 590 | 'if( hasPerspective == 1 ) {', 591 | 'pointSize = pointSize * ( 300.0 / length( pos.xyz ) );', 592 | '}', 593 | 594 | // Set particle size and position 595 | 'gl_PointSize = pointSize;', 596 | 'gl_Position = projectionMatrix * pos;', 597 | '}', 598 | 599 | 'else {', 600 | // Hide particle and set its position to the (maybe) glsl 601 | // equivalent of Number.POSITIVE_INFINITY 602 | 'vColor = vec4( customColor, 0.0 );', 603 | 'gl_Position = vec4(1e20, 1e20, 1e20, 0);', 604 | '}', 605 | '}', 606 | ].join('\n'), 607 | 608 | fragment: [ 609 | 'uniform sampler2D texture;', 610 | 'uniform int colorize;', 611 | 612 | 'varying vec4 vColor;', 613 | 614 | 'void main() {', 615 | 'float c = cos(0.0);', 616 | 'float s = sin(0.0);', 617 | 618 | 'vec2 rotatedUV = vec2(c * (gl_PointCoord.x - 0.5) + s * (gl_PointCoord.y - 0.5) + 0.5,', 619 | 'c * (gl_PointCoord.y - 0.5) - s * (gl_PointCoord.x - 0.5) + 0.5);', 620 | 621 | 'vec4 rotatedTexture = texture2D( texture, rotatedUV );', 622 | 623 | 'if( colorize == 1 ) {', 624 | 'gl_FragColor = vColor * rotatedTexture;', 625 | '}', 626 | 'else {', 627 | 'gl_FragColor = rotatedTexture;', 628 | '}', 629 | '}' 630 | ].join('\n') 631 | }; 632 | --------------------------------------------------------------------------------