├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── aframe-particle-system-component.js ├── aframe-particle-system-component.js.map ├── aframe-particle-system-component.min.js └── images │ ├── cloud.png │ ├── cloudSml.png │ ├── raindrop.png │ ├── shockwave.png │ ├── smokeparticle.png │ ├── star.png │ └── star2.png ├── examples ├── colors │ └── index.html ├── dust │ └── index.html ├── rain │ └── index.html ├── snow │ └── index.html └── stars │ └── index.html ├── index.html ├── index.js ├── lib └── SPE.js ├── package.json └── webpack.config.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: vincentfretin 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .sw[ponm] 2 | node_modules/ 3 | npm-debug.log 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 IdeaSpace 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aframe-particle-system-component 2 | 3 | Particle system component for [A-Frame](https://aframe.io). 4 | 5 | ![Particle System](https://cloud.githubusercontent.com/assets/674727/24214966/6d43ef14-0ef4-11e7-973f-c561b81d175f.gif) 6 | 7 | ### Examples 8 | 9 | - [Stars](https://c-frame.github.io/aframe-particle-system-component/examples/stars/) 10 | - [Dust](https://c-frame.github.io/aframe-particle-system-component/examples/dust/) 11 | - [Rain](https://c-frame.github.io/aframe-particle-system-component/examples/rain/) 12 | - [Snow](https://c-frame.github.io/aframe-particle-system-component/examples/snow/) 13 | - [Colors](https://c-frame.github.io/aframe-particle-system-component/examples/colors/) 14 | 15 | ### Properties 16 | 17 | This component exposes only a subset of the [ShaderParticleEngine API](http://squarefeet.github.io/ShaderParticleEngine/docs/api/). 18 | 19 | | Property | Description | Default Value | 20 | |--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------| 21 | | preset | Preset configuration. Possible values are: `default`, `dust`, `snow`, `rain`. | `default` | 22 | | maxAge | The particle's maximum age in seconds. | `6` | 23 | | positionSpread | Describes this emitter's position variance on a per-particle basis. | `0 0 0` | 24 | | type | The default distribution this emitter should use to control its particle's spawn position and force behaviour. Possible values are `1` (box), `2` (sphere), `3` (disc) | 1 (box) | 25 | | rotationAxis | Describes this emitter's axis of rotation. Possible values are `x`, `y` and `z`. | `x` | 26 | | rotationAngle | The angle of rotation, given in radians. `Dust` preset is `3.14`. | `0` | 27 | | rotationAngleSpread | The amount of variance in the angle of rotation per-particle, given in radians. | `0` | 28 | | accelerationValue | Describes this emitter's base acceleration. | `0, -10, 0` | 29 | | accelerationSpread | Describes this emitter's acceleration variance on a per-particle basis. | `10 0 10` | 30 | | velocityValue | Describes this emitter's base velocity. | `0 25 0` | 31 | | velocitySpread | Describes this emitter's acceleration variance on a per-particle basis. | `10 7.5 10` | 32 | | dragValue | Number between 0 and 1 describing drag applied to all particles. | `0` | 33 | | dragSpread | Number describing drag variance on a per-particle basis. | `0` | 34 | | dragRandomise | WHen a particle is re-spawned, whether it's drag should be re-randomised or not. Can incur a performance hit. | `false` | 35 | | color | Describes a particle's color. This property is a "value-over-lifetime" property, meaning an array of values can be given to describe specific value changes over a particle's lifetime. | `#0000FF,#FF0000` | 36 | | size | Describes a particle's size. Either a single number, or an array of values can be given to describe specific value changes over a particle's lifetime. | `1` | 37 | | sizeSpread | Per-particle variation in size. Either a single number, or an array of values can be given to describe specific value changes over a particle's lifetime. | 0 | 38 | | direction | The direction of the emitter. If value is `1`, emitter will start at beginning of particle's lifecycle. If value is `-1`, emitter will start at end of particle's lifecycle and work it's way backwards. | `1` | 39 | | duration | The duration in seconds that this emitter should live for. If not specified, the emitter will emit particles indefinitely. | `null` | 40 | | enabled | When `true` the emitter will emit particles, when `false` it will not. This value can be changed dynamically during a scene. While particles are emitting, they will disappear immediately when set to `false`. | `true` | 41 | | particleCount | The total number of particles this emitter will hold. NOTE: this is not the number of particles emitted in a second, or anything like that. The number of particles emitted per-second is calculated by particleCount / maxAge (approximately!) | `1000` | 42 | | texture | The texture used by this emitter. | `./images/star2.png` | 43 | | randomise | When a particle is re-spawned, whether it's position should be re-randomised or not. Can incur a performance hit. | `false` | 44 | | opacity | Either a single number to describe the opacity of a particle, or an array of values can be given to describe specific value changes over a particle's lifetime. | `1` | 45 | | opacitySpread | Per-particle variation in opacity. Either a single number, or an array of values can be given to describe specific value changes over a particle's lifetime. | 0 | 46 | | blending | The blending mode of the particles. Possible values are `0` (no blending), `1` (normal), `2` (additive), `3` (subtractive), `4` (multiply) | `2` | 47 | | maxParticleCount | | `250000` | 48 | 49 | ### Usage 50 | 51 | ```html 52 | 53 | ``` 54 | 55 | ```html 56 | 57 | ``` 58 | 59 | ```html 60 | 61 | ``` 62 | 63 | ```html 64 | 65 | ``` 66 | 67 | ### Functions 68 | 69 | #### startParticles 70 | Enables the emitters. Useful to start the animations when `enabled` is set to `false`. 71 | 72 | #### stopParticles 73 | Disables the emitters. 74 | 75 | ### Usage 76 | ```javascript 77 | this.el.components['particle-system'].startParticles(); 78 | this.el.components['particle-system'].stopParticles(); 79 | ``` 80 | 81 | ### Browser Installation 82 | 83 | Install and use by directly including the [browser files](dist). 84 | 85 | ```html 86 | 87 | 88 | 89 | 90 | A-Frame Particle System Component Example 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | ``` 110 | 111 | ### npm 112 | 113 | https://www.npmjs.com/package/aframe-particle-system-component 114 | 115 | ``` 116 | npm install aframe-particle-system-component 117 | ``` 118 | 119 | ### Local Development 120 | 121 | ``` 122 | npm install 123 | npm run dev # Changes are only in the served version, not dist/aframe-particle-system-component.js 124 | ``` 125 | 126 | To rebuild: 127 | 128 | ``` 129 | npm run dist 130 | ``` 131 | 132 | ### Credits 133 | 134 | Based on the [ShaderParticleEngine by Squarefeet](https://github.com/squarefeet/ShaderParticleEngine). 135 | -------------------------------------------------------------------------------- /dist/aframe-particle-system-component.min.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={479:(e,t,r)=>{var i,a,s={distributions:{BOX:1,SPHERE:2,DISC:3,LINE:4},valueOverLifetimeLength:4};void 0===(a="function"==typeof(i=s)?i.call(t,r,t,e):i)||(e.exports=a),s.TypedArrayHelper=function(e,t,r,i){"use strict";this.componentSize=r||1,this.size=t||1,this.TypedArrayConstructor=e||Float32Array,this.array=new e(t*this.componentSize),this.indexOffset=i||0},s.TypedArrayHelper.constructor=s.TypedArrayHelper,s.TypedArrayHelper.prototype.setSize=function(e,t){"use strict";var r=this.array.length;return t||(e*=this.componentSize),er?this.grow(e):void console.info("TypedArray is already of size:",e+".","Will not resize.")},s.TypedArrayHelper.prototype.shrink=function(e){"use strict";return this.array=this.array.subarray(0,e),this.size=e,this},s.TypedArrayHelper.prototype.grow=function(e){"use strict";var t=this.array,r=new this.TypedArrayConstructor(e);return r.set(t),this.array=r,this.size=e,this},s.TypedArrayHelper.prototype.splice=function(e,t){"use strict";e*=this.componentSize,t*=this.componentSize;for(var r=[],i=this.array,a=i.length,s=0;s=t)&&r.push(i[s]);return this.setFromArray(0,r),this},s.TypedArrayHelper.prototype.setFromArray=function(e,t){"use strict";var r=e+t.length;return r>this.array.length?this.grow(r):r=81&&(this.bufferAttribute.count=this.bufferAttribute.array.length/this.bufferAttribute.itemSize),void(this.bufferAttribute.needsUpdate=!0);this.bufferAttribute=new THREE.BufferAttribute(this.typedArray.array,this.componentSize),this.bufferAttribute.usage=this.dynamicBuffer?THREE.DynamicDrawUsage:THREE.StaticDrawUsage},s.ShaderAttribute.prototype.getLength=function(){"use strict";return null===this.typedArray?0:this.typedArray.array.length},s.shaderChunks={defines:["#define PACKED_COLOR_SIZE 256.0","#define PACKED_COLOR_DIVISOR 255.0"].join("\n"),uniforms:["uniform float deltaTime;","uniform float runTime;","uniform sampler2D tex;","uniform vec4 textureAnimation;","uniform float scale;"].join("\n"),attributes:["attribute vec4 acceleration;","attribute vec3 velocity;","attribute vec4 rotation;","attribute vec3 rotationCenter;","attribute vec4 params;","attribute vec4 size;","attribute vec4 angle;","attribute vec4 color;","attribute vec4 opacity;"].join("\n"),varyings:["varying vec4 vColor;","#ifdef SHOULD_ROTATE_TEXTURE"," varying float vAngle;","#endif","#ifdef SHOULD_CALCULATE_SPRITE"," varying vec4 vSpriteSheet;","#endif"].join("\n"),branchAvoidanceFunctions:["float when_gt(float x, float y) {"," return max(sign(x - y), 0.0);","}","float when_lt(float x, float y) {"," return min( max(1.0 - sign(x - y), 0.0), 1.0 );","}","float when_eq( float x, float y ) {"," return 1.0 - abs( sign( x - y ) );","}","float when_ge(float x, float y) {"," return 1.0 - when_lt(x, y);","}","float when_le(float x, float y) {"," return 1.0 - when_gt(x, y);","}","float and(float a, float b) {"," return a * b;","}","float or(float a, float b) {"," return min(a + b, 1.0);","}"].join("\n"),unpackColor:["vec3 unpackColor( in float hex ) {"," vec3 c = vec3( 0.0 );"," float r = mod( (hex / PACKED_COLOR_SIZE / PACKED_COLOR_SIZE), PACKED_COLOR_SIZE );"," float g = mod( (hex / PACKED_COLOR_SIZE), PACKED_COLOR_SIZE );"," float b = mod( hex, PACKED_COLOR_SIZE );"," c.r = r / PACKED_COLOR_DIVISOR;"," c.g = g / PACKED_COLOR_DIVISOR;"," c.b = b / PACKED_COLOR_DIVISOR;"," return c;","}"].join("\n"),unpackRotationAxis:["vec3 unpackRotationAxis( in float hex ) {"," vec3 c = vec3( 0.0 );"," float r = mod( (hex / PACKED_COLOR_SIZE / PACKED_COLOR_SIZE), PACKED_COLOR_SIZE );"," float g = mod( (hex / PACKED_COLOR_SIZE), PACKED_COLOR_SIZE );"," float b = mod( hex, PACKED_COLOR_SIZE );"," c.r = r / PACKED_COLOR_DIVISOR;"," c.g = g / PACKED_COLOR_DIVISOR;"," c.b = b / PACKED_COLOR_DIVISOR;"," c *= vec3( 2.0 );"," c -= vec3( 1.0 );"," return c;","}"].join("\n"),floatOverLifetime:["float getFloatOverLifetime( in float positionInTime, in vec4 attr ) {"," highp float value = 0.0;"," float deltaAge = positionInTime * float( VALUE_OVER_LIFETIME_LENGTH - 1 );"," float fIndex = 0.0;"," float shouldApplyValue = 0.0;"," value += attr[ 0 ] * when_eq( deltaAge, 0.0 );",""," for( int i = 0; i < VALUE_OVER_LIFETIME_LENGTH - 1; ++i ) {"," fIndex = float( i );"," shouldApplyValue = and( when_gt( deltaAge, fIndex ), when_le( deltaAge, fIndex + 1.0 ) );"," value += shouldApplyValue * mix( attr[ i ], attr[ i + 1 ], deltaAge - fIndex );"," }",""," return value;","}"].join("\n"),colorOverLifetime:["vec3 getColorOverLifetime( in float positionInTime, in vec3 color1, in vec3 color2, in vec3 color3, in vec3 color4 ) {"," vec3 value = vec3( 0.0 );"," value.x = getFloatOverLifetime( positionInTime, vec4( color1.x, color2.x, color3.x, color4.x ) );"," value.y = getFloatOverLifetime( positionInTime, vec4( color1.y, color2.y, color3.y, color4.y ) );"," value.z = getFloatOverLifetime( positionInTime, vec4( color1.z, color2.z, color3.z, color4.z ) );"," return value;","}"].join("\n"),paramFetchingFunctions:["float getAlive() {"," return params.x;","}","float getAge() {"," return params.y;","}","float getMaxAge() {"," return params.z;","}","float getWiggle() {"," return params.w;","}"].join("\n"),forceFetchingFunctions:["vec4 getPosition( in float age ) {"," return modelViewMatrix * vec4( position, 1.0 );","}","vec3 getVelocity( in float age ) {"," return velocity * age;","}","vec3 getAcceleration( in float age ) {"," return acceleration.xyz * age;","}"].join("\n"),rotationFunctions:["#ifdef SHOULD_ROTATE_PARTICLES"," mat4 getRotationMatrix( in vec3 axis, in float angle) {"," axis = normalize(axis);"," float s = sin(angle);"," float c = cos(angle);"," float oc = 1.0 - c;",""," return mat4(oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0,"," oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0,"," oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0,"," 0.0, 0.0, 0.0, 1.0);"," }",""," vec3 getRotation( in vec3 pos, in float positionInTime ) {"," if( rotation.y == 0.0 ) {"," return pos;"," }",""," vec3 axis = unpackRotationAxis( rotation.x );"," vec3 center = rotationCenter;"," vec3 translated;"," mat4 rotationMatrix;"," float angle = 0.0;"," angle += when_eq( rotation.z, 0.0 ) * rotation.y;"," angle += when_gt( rotation.z, 0.0 ) * mix( 0.0, rotation.y, positionInTime );"," translated = rotationCenter - pos;"," rotationMatrix = getRotationMatrix( axis, angle );"," return center - vec3( rotationMatrix * vec4( translated, 0.0 ) );"," }","#endif"].join("\n"),rotateTexture:[" vec2 vUv = vec2( gl_PointCoord.x, 1.0 - gl_PointCoord.y );",""," #ifdef SHOULD_ROTATE_TEXTURE"," float x = gl_PointCoord.x - 0.5;"," float y = 1.0 - gl_PointCoord.y - 0.5;"," float c = cos( -vAngle );"," float s = sin( -vAngle );"," vUv = vec2( c * x + s * y + 0.5, c * y - s * x + 0.5 );"," #endif",""," #ifdef SHOULD_CALCULATE_SPRITE"," float framesX = vSpriteSheet.x;"," float framesY = vSpriteSheet.y;"," float columnNorm = vSpriteSheet.z;"," float rowNorm = vSpriteSheet.w;"," vUv.x = gl_PointCoord.x * framesX + columnNorm;"," vUv.y = 1.0 - (gl_PointCoord.y * framesY + rowNorm);"," #endif",""," vec4 rotatedTexture = texture2D( tex, vUv );"].join("\n")},s.shaders={vertex:[s.shaderChunks.defines,s.shaderChunks.uniforms,s.shaderChunks.attributes,s.shaderChunks.varyings,THREE.ShaderChunk.common,THREE.ShaderChunk.logdepthbuf_pars_vertex,THREE.ShaderChunk.fog_pars_vertex,s.shaderChunks.branchAvoidanceFunctions,s.shaderChunks.unpackColor,s.shaderChunks.unpackRotationAxis,s.shaderChunks.floatOverLifetime,s.shaderChunks.colorOverLifetime,s.shaderChunks.paramFetchingFunctions,s.shaderChunks.forceFetchingFunctions,s.shaderChunks.rotationFunctions,"void main() {"," highp float age = getAge();"," highp float alive = getAlive();"," highp float maxAge = getMaxAge();"," highp float positionInTime = (age / maxAge);"," highp float isAlive = when_gt( alive, 0.0 );"," #ifdef SHOULD_WIGGLE_PARTICLES"," float wiggleAmount = positionInTime * getWiggle();"," float wiggleSin = isAlive * sin( wiggleAmount );"," float wiggleCos = isAlive * cos( wiggleAmount );"," #endif"," vec3 vel = getVelocity( age );"," vec3 accel = getAcceleration( age );"," vec3 force = vec3( 0.0 );"," vec3 pos = vec3( position );"," float drag = 1.0 - (positionInTime * 0.5) * acceleration.w;"," force += vel;"," force *= drag;"," force += accel * age;"," pos += force;"," #ifdef SHOULD_WIGGLE_PARTICLES"," pos.x += wiggleSin;"," pos.y += wiggleCos;"," pos.z += wiggleSin;"," #endif"," #ifdef SHOULD_ROTATE_PARTICLES"," pos = getRotation( pos, positionInTime );"," #endif"," vec4 mvPosition = modelViewMatrix * vec4( pos, 1.0 );"," highp float pointSize = getFloatOverLifetime( positionInTime, size ) * isAlive;"," #ifdef HAS_PERSPECTIVE"," float perspective = scale / length( mvPosition.xyz );"," #else"," float perspective = 1.0;"," #endif"," float pointSizePerspective = pointSize * perspective;"," #ifdef COLORIZE"," vec3 c = isAlive * getColorOverLifetime("," positionInTime,"," unpackColor( color.x ),"," unpackColor( color.y ),"," unpackColor( color.z ),"," unpackColor( color.w )"," );"," #else"," vec3 c = vec3(1.0);"," #endif"," float o = isAlive * getFloatOverLifetime( positionInTime, opacity );"," vColor = vec4( c, o );"," #ifdef SHOULD_ROTATE_TEXTURE"," vAngle = isAlive * getFloatOverLifetime( positionInTime, angle );"," #endif"," #ifdef SHOULD_CALCULATE_SPRITE"," float framesX = textureAnimation.x;"," float framesY = textureAnimation.y;"," float loopCount = textureAnimation.w;"," float totalFrames = textureAnimation.z;"," float frameNumber = mod( (positionInTime * loopCount) * totalFrames, totalFrames );"," float column = floor(mod( frameNumber, framesX ));"," float row = floor( (frameNumber - column) / framesX );"," float columnNorm = column / framesX;"," float rowNorm = row / framesY;"," vSpriteSheet.x = 1.0 / framesX;"," vSpriteSheet.y = 1.0 / framesY;"," vSpriteSheet.z = columnNorm;"," vSpriteSheet.w = rowNorm;"," #endif"," gl_PointSize = pointSizePerspective;"," gl_Position = projectionMatrix * mvPosition;",THREE.ShaderChunk.logdepthbuf_vertex,THREE.ShaderChunk.fog_vertex,"}"].join("\n"),fragment:[s.shaderChunks.uniforms,THREE.ShaderChunk.common,THREE.ShaderChunk.fog_pars_fragment,THREE.ShaderChunk.logdepthbuf_pars_fragment,s.shaderChunks.varyings,s.shaderChunks.branchAvoidanceFunctions,"void main() {"," vec3 outgoingLight = vColor.xyz;"," "," #ifdef ALPHATEST"," if ( vColor.w < float(ALPHATEST) ) discard;"," #endif",s.shaderChunks.rotateTexture,THREE.ShaderChunk.logdepthbuf_fragment," outgoingLight = vColor.xyz * rotatedTexture.xyz;"," gl_FragColor = vec4( outgoingLight.xyz, rotatedTexture.w * vColor.w );",THREE.ShaderChunk.fog_fragment,"}"].join("\n")},s.utils={types:{BOOLEAN:"boolean",STRING:"string",NUMBER:"number",OBJECT:"object"},ensureTypedArg:function(e,t,r){"use strict";return typeof e===t?e:r},ensureArrayTypedArg:function(e,t,r){"use strict";if(Array.isArray(e)){for(var i=e.length-1;i>=0;--i)if(typeof e[i]!==t)return r;return e}return this.ensureTypedArg(e,t,r)},ensureInstanceOf:function(e,t,r){"use strict";return void 0!==t&&e instanceof t?e:r},ensureArrayInstanceOf:function(e,t,r){"use strict";if(Array.isArray(e)){for(var i=e.length-1;i>=0;--i)if(void 0!==t&&e[i]instanceof t==0)return r;return e}return this.ensureInstanceOf(e,t,r)},ensureValueOverLifetimeCompliance:function(e,t,r){"use strict";t=t||3,r=r||3,!1===Array.isArray(e._value)&&(e._value=[e._value]),!1===Array.isArray(e._spread)&&(e._spread=[e._spread]);var i=this.clamp(e._value.length,t,r),a=this.clamp(e._spread.length,t,r),s=Math.max(i,a);e._value.length!==s&&(e._value=this.interpolateArray(e._value,s)),e._spread.length!==s&&(e._spread=this.interpolateArray(e._spread,s))},interpolateArray:function(e,t){"use strict";for(var r=e.length,i=["function"==typeof e[0]?.clone?e[0].clone():e[0]],a=(r-1)/(t-1),s=1;s-1e-5&&(i=-i),i},lerpTypeAgnostic:function(e,t,r){"use strict";var i,a=this.types;return typeof e===a.NUMBER&&typeof t===a.NUMBER?e+(t-e)*r:e instanceof THREE.Vector2&&t instanceof THREE.Vector2?((i=e.clone()).x=this.lerp(e.x,t.x,r),i.y=this.lerp(e.y,t.y,r),i):e instanceof THREE.Vector3&&t instanceof THREE.Vector3?((i=e.clone()).x=this.lerp(e.x,t.x,r),i.y=this.lerp(e.y,t.y,r),i.z=this.lerp(e.z,t.z,r),i):e instanceof THREE.Vector4&&t instanceof THREE.Vector4?((i=e.clone()).x=this.lerp(e.x,t.x,r),i.y=this.lerp(e.y,t.y,r),i.z=this.lerp(e.z,t.z,r),i.w=this.lerp(e.w,t.w,r),i):e instanceof THREE.Color&&t instanceof THREE.Color?((i=e.clone()).r=this.lerp(e.r,t.r,r),i.g=this.lerp(e.g,t.g,r),i.b=this.lerp(e.b,t.b,r),i):void console.warn("Invalid argument types, or argument types do not match:",e,t)},lerp:function(e,t,r){"use strict";return e+(t-e)*r},roundToNearestMultiple:function(e,t){"use strict";var r;return 0===t||0==(r=Math.abs(e)%t)?e:e<0?-(Math.abs(e)-r):e+t-r},arrayValuesAreEqual:function(e){"use strict";for(var t=0;t1||this.textureFrames.y>1},this.attributes={position:new s.ShaderAttribute("v3",!0),acceleration:new s.ShaderAttribute("v4",!0),velocity:new s.ShaderAttribute("v3",!0),rotation:new s.ShaderAttribute("v4",!0),rotationCenter:new s.ShaderAttribute("v3",!0),params:new s.ShaderAttribute("v4",!0),size:new s.ShaderAttribute("v4",!0),angle:new s.ShaderAttribute("v4",!0),color:new s.ShaderAttribute("v4",!0),opacity:new s.ShaderAttribute("v4",!0)},this.attributeKeys=Object.keys(this.attributes),this.attributeCount=this.attributeKeys.length,this.material=new THREE.ShaderMaterial({uniforms:this.uniforms,vertexShader:s.shaders.vertex,fragmentShader:s.shaders.fragment,blending:this.blending,transparent:this.transparent,alphaTest:this.alphaTest,depthWrite:this.depthWrite,depthTest:this.depthTest,defines:this.defines,fog:this.fog}),this.geometry=new THREE.BufferGeometry,this.mesh=new THREE.Points(this.geometry,this.material),null===this.maxParticleCount&&console.warn("SPE.Group: No maxParticleCount specified. Adding emitters after rendering will probably cause errors.")},s.Group.constructor=s.Group,s.Group.prototype._updateDefines=function(){"use strict";for(var e,t=this.emitters,r=t.length-1,i=this.defines;r>=0;--r)e=t[r],i.SHOULD_CALCULATE_SPRITE||(i.SHOULD_ROTATE_TEXTURE=i.SHOULD_ROTATE_TEXTURE||!!Math.max(Math.max.apply(null,e.angle.value),Math.max.apply(null,e.angle.spread))),i.SHOULD_ROTATE_PARTICLES=i.SHOULD_ROTATE_PARTICLES||!!Math.max(e.rotation.angle,e.rotation.angleSpread),i.SHOULD_WIGGLE_PARTICLES=i.SHOULD_WIGGLE_PARTICLES||!!Math.max(e.wiggle.value,e.wiggle.spread);this.material.needsUpdate=!0},s.Group.prototype._applyAttributesToGeometry=function(){"use strict";var e,t,r=this.attributes,i=this.geometry,a=i.attributes;for(var s in r)r.hasOwnProperty(s)&&(e=r[s],(t=a[s])?t.array=e.typedArray.array:i.setAttribute(s,e.bufferAttribute),e.bufferAttribute.needsUpdate=!0);this.geometry.setDrawRange(0,this.particleCount)},s.Group.prototype.addEmitter=function(e){"use strict";if(e instanceof s.Emitter!=0)if(this.emitterIDs.indexOf(e.uuid)>-1)console.error("Emitter already exists in this group. Will not add again.");else{if(null===e.group){var t=this.attributes,r=this.particleCount,i=r+e.particleCount;for(var a in this.particleCount=i,null!==this.maxParticleCount&&this.particleCount>this.maxParticleCount&&console.warn("SPE.Group: maxParticleCount exceeded. Requesting",this.particleCount,"particles, can support only",this.maxParticleCount),e._calculatePPSValue(e.maxAge._value+e.maxAge._spread),e._setBufferUpdateRanges(this.attributeKeys),e._setAttributeOffset(r),e.group=this,e.attributes=this.attributes,t)t.hasOwnProperty(a)&&t[a]._createBufferAttribute(null!==this.maxParticleCount?this.maxParticleCount:this.particleCount);for(var o=r;o1)for(var r=0;r=0;--t)r[e[t]].resetUpdateRange()},s.Group.prototype._updateBuffers=function(e){"use strict";for(var t,r,i,a=this.attributeKeys,s=this.attributeCount-1,o=this.attributes,n=e.bufferUpdateRanges;s>=0;--s)r=n[t=a[s]],(i=o[t]).setUpdateRange(r.min,r.max),i.flagUpdate()},s.Group.prototype.tick=function(e){"use strict";var t=this.emitters,r=t.length,i=e||this.fixedTimeStep,a=this.attributeKeys,s=this.attributes;if(this._updateUniforms(i),this._resetBufferRanges(),0!==r||!1!==this._attributesNeedRefresh||!1!==this._attributesNeedDynamicReset){for(var o,n=0;n=0;--n)s[a[n]].resetDynamic();this._attributesNeedDynamicReset=!1}if(!0===this._attributesNeedRefresh){for(n=this.attributeCount-1;n>=0;--n)s[a[n]].forceUpdateAll();this._attributesNeedRefresh=!1,this._attributesNeedDynamicReset=!0}}},s.Group.prototype.dispose=function(){"use strict";return this.geometry.dispose(),this.material.dispose(),this},s.Emitter=function(e){"use strict";var t=s.utils,r=t.types,i=s.valueOverLifetimeLength;for(var a in(e=t.ensureTypedArg(e,r.OBJECT,{})).position=t.ensureTypedArg(e.position,r.OBJECT,{}),e.velocity=t.ensureTypedArg(e.velocity,r.OBJECT,{}),e.acceleration=t.ensureTypedArg(e.acceleration,r.OBJECT,{}),e.radius=t.ensureTypedArg(e.radius,r.OBJECT,{}),e.drag=t.ensureTypedArg(e.drag,r.OBJECT,{}),e.rotation=t.ensureTypedArg(e.rotation,r.OBJECT,{}),e.color=t.ensureTypedArg(e.color,r.OBJECT,{}),e.opacity=t.ensureTypedArg(e.opacity,r.OBJECT,{}),e.size=t.ensureTypedArg(e.size,r.OBJECT,{}),e.angle=t.ensureTypedArg(e.angle,r.OBJECT,{}),e.wiggle=t.ensureTypedArg(e.wiggle,r.OBJECT,{}),e.maxAge=t.ensureTypedArg(e.maxAge,r.OBJECT,{}),e.onParticleSpawn&&console.warn("onParticleSpawn has been removed. Please set properties directly to alter values at runtime."),this.uuid=THREE.MathUtils.generateUUID(),this.type=t.ensureTypedArg(e.type,r.NUMBER,s.distributions.BOX),this.position={_value:t.ensureInstanceOf(e.position.value,THREE.Vector3,new THREE.Vector3),_spread:t.ensureInstanceOf(e.position.spread,THREE.Vector3,new THREE.Vector3),_spreadClamp:t.ensureInstanceOf(e.position.spreadClamp,THREE.Vector3,new THREE.Vector3),_distribution:t.ensureTypedArg(e.position.distribution,r.NUMBER,this.type),_randomise:t.ensureTypedArg(e.position.randomise,r.BOOLEAN,!1),_radius:t.ensureTypedArg(e.position.radius,r.NUMBER,10),_radiusScale:t.ensureInstanceOf(e.position.radiusScale,THREE.Vector3,new THREE.Vector3(1,1,1)),_distributionClamp:t.ensureTypedArg(e.position.distributionClamp,r.NUMBER,0)},this.velocity={_value:t.ensureInstanceOf(e.velocity.value,THREE.Vector3,new THREE.Vector3),_spread:t.ensureInstanceOf(e.velocity.spread,THREE.Vector3,new THREE.Vector3),_distribution:t.ensureTypedArg(e.velocity.distribution,r.NUMBER,this.type),_randomise:t.ensureTypedArg(e.position.randomise,r.BOOLEAN,!1)},this.acceleration={_value:t.ensureInstanceOf(e.acceleration.value,THREE.Vector3,new THREE.Vector3),_spread:t.ensureInstanceOf(e.acceleration.spread,THREE.Vector3,new THREE.Vector3),_distribution:t.ensureTypedArg(e.acceleration.distribution,r.NUMBER,this.type),_randomise:t.ensureTypedArg(e.position.randomise,r.BOOLEAN,!1)},this.drag={_value:t.ensureTypedArg(e.drag.value,r.NUMBER,0),_spread:t.ensureTypedArg(e.drag.spread,r.NUMBER,0),_randomise:t.ensureTypedArg(e.position.randomise,r.BOOLEAN,!1)},this.wiggle={_value:t.ensureTypedArg(e.wiggle.value,r.NUMBER,0),_spread:t.ensureTypedArg(e.wiggle.spread,r.NUMBER,0)},this.rotation={_axis:t.ensureInstanceOf(e.rotation.axis,THREE.Vector3,new THREE.Vector3(0,1,0)),_axisSpread:t.ensureInstanceOf(e.rotation.axisSpread,THREE.Vector3,new THREE.Vector3),_angle:t.ensureTypedArg(e.rotation.angle,r.NUMBER,0),_angleSpread:t.ensureTypedArg(e.rotation.angleSpread,r.NUMBER,0),_static:t.ensureTypedArg(e.rotation.static,r.BOOLEAN,!1),_center:t.ensureInstanceOf(e.rotation.center,THREE.Vector3,this.position._value.clone()),_randomise:t.ensureTypedArg(e.position.randomise,r.BOOLEAN,!1)},this.maxAge={_value:t.ensureTypedArg(e.maxAge.value,r.NUMBER,2),_spread:t.ensureTypedArg(e.maxAge.spread,r.NUMBER,0)},this.color={_value:t.ensureArrayInstanceOf(e.color.value,THREE.Color,new THREE.Color),_spread:t.ensureArrayInstanceOf(e.color.spread,THREE.Vector3,new THREE.Vector3),_randomise:t.ensureTypedArg(e.position.randomise,r.BOOLEAN,!1)},this.opacity={_value:t.ensureArrayTypedArg(e.opacity.value,r.NUMBER,1),_spread:t.ensureArrayTypedArg(e.opacity.spread,r.NUMBER,0),_randomise:t.ensureTypedArg(e.position.randomise,r.BOOLEAN,!1)},this.size={_value:t.ensureArrayTypedArg(e.size.value,r.NUMBER,1),_spread:t.ensureArrayTypedArg(e.size.spread,r.NUMBER,0),_randomise:t.ensureTypedArg(e.position.randomise,r.BOOLEAN,!1)},this.angle={_value:t.ensureArrayTypedArg(e.angle.value,r.NUMBER,0),_spread:t.ensureArrayTypedArg(e.angle.spread,r.NUMBER,0),_randomise:t.ensureTypedArg(e.position.randomise,r.BOOLEAN,!1)},this.particleCount=t.ensureTypedArg(e.particleCount,r.NUMBER,100),this.duration=t.ensureTypedArg(e.duration,r.NUMBER,null),this.isStatic=t.ensureTypedArg(e.isStatic,r.BOOLEAN,!1),this.activeMultiplier=t.ensureTypedArg(e.activeMultiplier,r.NUMBER,1),this.direction=t.ensureTypedArg(e.direction,r.NUMBER,1),this.alive=t.ensureTypedArg(e.alive,r.BOOLEAN,!0),this.particlesPerSecond=0,this.activationIndex=0,this.attributeOffset=0,this.attributeEnd=0,this.age=0,this.activeParticleCount=0,this.group=null,this.attributes=null,this.paramsArray=null,this.resetFlags={position:t.ensureTypedArg(e.position.randomise,r.BOOLEAN,!1)||t.ensureTypedArg(e.radius.randomise,r.BOOLEAN,!1),velocity:t.ensureTypedArg(e.velocity.randomise,r.BOOLEAN,!1),acceleration:t.ensureTypedArg(e.acceleration.randomise,r.BOOLEAN,!1)||t.ensureTypedArg(e.drag.randomise,r.BOOLEAN,!1),rotation:t.ensureTypedArg(e.rotation.randomise,r.BOOLEAN,!1),rotationCenter:t.ensureTypedArg(e.rotation.randomise,r.BOOLEAN,!1),size:t.ensureTypedArg(e.size.randomise,r.BOOLEAN,!1),color:t.ensureTypedArg(e.color.randomise,r.BOOLEAN,!1),opacity:t.ensureTypedArg(e.opacity.randomise,r.BOOLEAN,!1),angle:t.ensureTypedArg(e.angle.randomise,r.BOOLEAN,!1)},this.updateFlags={},this.updateCounts={},this.updateMap={maxAge:"params",position:"position",velocity:"velocity",acceleration:"acceleration",drag:"acceleration",wiggle:"params",rotation:"rotation",size:"size",color:"color",opacity:"opacity",angle:"angle"},this.updateMap)this.updateMap.hasOwnProperty(a)&&(this.updateCounts[this.updateMap[a]]=0,this.updateFlags[this.updateMap[a]]=!1,this._createGetterSetters(this[a],a));this.bufferUpdateRanges={},this.attributeKeys=null,this.attributeCount=0,t.ensureValueOverLifetimeCompliance(this.color,i,i),t.ensureValueOverLifetimeCompliance(this.opacity,i,i),t.ensureValueOverLifetimeCompliance(this.size,i,i),t.ensureValueOverLifetimeCompliance(this.angle,i,i)},s.Emitter.constructor=s.Emitter,s.Emitter.prototype._createGetterSetters=function(e,t){"use strict";var r=this;for(var i in e)if(e.hasOwnProperty(i)){var a=i.replace("_","");Object.defineProperty(e,a,{get:function(e){return function(){return this[e]}}(i),set:function(e){return function(i){var a=r.updateMap[t],o=this[e],n=s.valueOverLifetimeLength;"_rotationCenter"===e?(r.updateFlags.rotationCenter=!0,r.updateCounts.rotationCenter=0):"_randomise"===e?r.resetFlags[a]=i:(r.updateFlags[a]=!0,r.updateCounts[a]=0),r.group._updateDefines(),this[e]=i,Array.isArray(o)&&s.utils.ensureValueOverLifetimeCompliance(r[t],n,n)}}(i)})}},s.Emitter.prototype._setBufferUpdateRanges=function(e){"use strict";this.attributeKeys=e,this.attributeCount=e.length;for(var t=this.attributeCount-1;t>=0;--t)this.bufferUpdateRanges[e[t]]={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY}},s.Emitter.prototype._calculatePPSValue=function(e){"use strict";var t=this.particleCount;this.duration?this.particlesPerSecond=t/(e=0;--n)r=a[t=o[n]],!0!==i[t]&&!0!==r||(this._assignValue(t,e),this._updateAttributeUpdateRange(t,e),!0===r&&s[t]===this.particleCount?(a[t]=!1,s[t]=0):!0===r&&++s[t])},s.Emitter.prototype._updateAttributeUpdateRange=function(e,t){"use strict";var r=this.bufferUpdateRanges[e];r.min=Math.min(t,r.min),r.max=Math.max(t,r.max)},s.Emitter.prototype._resetBufferRanges=function(){"use strict";for(var e,t=this.bufferUpdateRanges,r=this.bufferUpdateKeys,i=this.bufferUpdateCount-1;i>=0;--i)t[e=r[i]].min=Number.POSITIVE_INFINITY,t[e].max=Number.NEGATIVE_INFINITY},s.Emitter.prototype._onRemove=function(){"use strict";this.particlesPerSecond=0,this.attributeOffset=0,this.activationIndex=0,this.activeParticleCount=0,this.group=null,this.attributes=null,this.paramsArray=null,this.age=0},s.Emitter.prototype._decrementParticleCount=function(){"use strict";--this.activeParticleCount},s.Emitter.prototype._incrementParticleCount=function(){"use strict";++this.activeParticleCount},s.Emitter.prototype._checkParticleAges=function(e,t,r,i){"use strict";for(var a,s,o,n,u=t-1;u>=e;--u)0!==(n=r[a=4*u])&&(o=r[a+1],s=r[a+2],1===this.direction?(o+=i)>=s&&(o=0,n=0,this._decrementParticleCount()):(o-=i)<=0&&(o=s,n=0,this._decrementParticleCount()),r[a]=n,r[a+1]=o,this._updateAttributeUpdateRange("params",u))},s.Emitter.prototype._activateParticles=function(e,t,r,i){"use strict";for(var a,s,o=this.direction,n=e;nthis.duration)return this.alive=!1,void(this.age=0);var o=1===this.particleCount?s:0|s,n=Math.min(o+a,this.activationEnd),u=n-this.activationIndex|0,l=u>0?e/u:0;this._activateParticles(o,n,i,l),this.activationIndex+=a,this.activationIndex>r&&(this.activationIndex=t),this.age+=e}else this.age=0}},s.Emitter.prototype.reset=function(e){"use strict";if(this.age=0,this.alive=!1,!0===e){for(var t,r=this.attributeOffset,i=r+this.particleCount,a=this.paramsArray,s=this.attributes.params.bufferAttribute,o=i-1;o>=r;--o)a[t=4*o]=0,a[t+1]=0;s.clearUpdateRanges(),s.needsUpdate=!0}return this},s.Emitter.prototype.enable=function(){"use strict";return this.alive=!0,this},s.Emitter.prototype.disable=function(){"use strict";return this.alive=!1,this},s.Emitter.prototype.remove=function(){"use strict";return null!==this.group?this.group.removeEmitter(this):console.error("Emitter does not belong to a group, cannot remove."),this}}},t={},r=function r(i){var a=t[i];if(void 0!==a)return a.exports;var s=t[i]={exports:{}};return e[i](s,s.exports,r),s.exports}(479);if("undefined"==typeof AFRAME)throw new Error("Component attempted to register before AFRAME was available.");AFRAME.registerComponent("particle-system",{schema:{preset:{type:"string",default:"",oneOf:["default","dust","snow","rain"]},maxAge:{type:"number",default:6},positionSpread:{type:"vec3",default:{x:0,y:0,z:0}},type:{type:"number",default:r.distributions.BOX},rotationAxis:{type:"string",default:"x"},rotationAngle:{type:"number",default:0},rotationAngleSpread:{type:"number",default:0},accelerationValue:{type:"vec3",default:{x:0,y:-10,z:0}},accelerationSpread:{type:"vec3",default:{x:10,y:0,z:10}},velocityValue:{type:"vec3",default:{x:0,y:25,z:0}},velocitySpread:{type:"vec3",default:{x:10,y:7.5,z:10}},dragValue:{type:"number",default:0},dragSpread:{type:"number",default:0},dragRandomise:{type:"boolean",default:!1},color:{type:"array",default:["#0000FF","#FF0000"]},size:{type:"array",default:["1"]},sizeSpread:{type:"array",default:["0"]},direction:{type:"number",default:1},duration:{type:"number",default:1/0},particleCount:{type:"number",default:1e3},texture:{type:"asset",default:"https://cdn.rawgit.com/c-frame/aframe-particle-system-component/master/dist/images/star2.png"},randomise:{type:"boolean",default:!1},opacity:{type:"array",default:["1"]},opacitySpread:{type:"array",default:["0"]},maxParticleCount:{type:"number",default:25e4},blending:{type:"number",default:THREE.AdditiveBlending,oneOf:[THREE.NoBlending,THREE.NormalBlending,THREE.AdditiveBlending,THREE.SubtractiveBlending,THREE.MultiplyBlending]},enabled:{type:"boolean",default:!0}},init:function(){this.presets={},this.presets.dust={maxAge:20,positionSpread:{x:100,y:100,z:100},rotationAngle:3.14,accelerationValue:{x:0,y:0,z:0},accelerationSpread:{x:0,y:0,z:0},velocityValue:{x:1,y:.3,z:1},velocitySpread:{x:.5,y:1,z:.5},color:["#FFFFFF"],particleCount:100,texture:"https://cdn.rawgit.com/c-frame/aframe-particle-system-component/master/dist/images/smokeparticle.png"},this.presets.snow={maxAge:20,positionSpread:{x:100,y:100,z:100},rotationAngle:3.14,accelerationValue:{x:0,y:0,z:0},accelerationSpread:{x:.2,y:0,z:.2},velocityValue:{x:0,y:8,z:0},velocitySpread:{x:2,y:0,z:2},color:["#FFFFFF"],particleCount:200,texture:"https://cdn.rawgit.com/c-frame/aframe-particle-system-component/master/dist/images/smokeparticle.png"},this.presets.rain={maxAge:1,positionSpread:{x:100,y:100,z:100},rotationAngle:3.14,accelerationValue:{x:0,y:3,z:0},accelerationSpread:{x:2,y:1,z:2},velocityValue:{x:0,y:75,z:0},velocitySpread:{x:10,y:50,z:10},color:["#FFFFFF"],size:[.4],texture:"https://cdn.rawgit.com/c-frame/aframe-particle-system-component/master/dist/images/raindrop.png"}},update:function(e){for(var t in this.particleGroup&&this.el.removeObject3D("particle-system"),this.preset=this.presets[this.data.preset]||{},this.data)this.data[t]=this.applyPreset(t);this.initParticleSystem(this.data),!0===this.data.enabled?this.startParticles():this.stopParticles()},applyPreset:function(e){return!this.attrValue[e]&&this.preset[e]?this.preset[e]:this.data[e]},tick:function(e,t){this.particleGroup.tick(t/1e3)},remove:function(){this.particleGroup&&this.el.removeObject3D("particle-system")},startParticles:function(){this.particleGroup.emitters.forEach((function(e){e.enable()}))},stopParticles:function(){this.particleGroup.emitters.forEach((function(e){e.disable()}))},initParticleSystem:function(e){var t=(new THREE.TextureLoader).load(e.texture,(function(e){return e}),(function(e){console.log(e.loaded/e.total*100+"% loaded")}),(function(e){console.log("An error occurred")}));this.particleGroup=new r.Group({texture:{value:t},maxParticleCount:e.maxParticleCount,blending:e.blending});var i=new r.Emitter({maxAge:{value:e.maxAge},type:{value:e.type},position:{spread:new THREE.Vector3(e.positionSpread.x,e.positionSpread.y,e.positionSpread.z),randomise:e.randomise},rotation:{axis:"x"==e.rotationAxis?new THREE.Vector3(1,0,0):"y"==e.rotationAxis?new THREE.Vector3(0,1,0):"z"==e.rotationAxis?new THREE.Vector3(0,0,1):new THREE.Vector3(0,1,0),angle:e.rotationAngle,angleSpread:e.rotationAngleSpread,static:!0},acceleration:{value:new THREE.Vector3(e.accelerationValue.x,e.accelerationValue.y,e.accelerationValue.z),spread:new THREE.Vector3(e.accelerationSpread.x,e.accelerationSpread.y,e.accelerationSpread.z)},velocity:{value:new THREE.Vector3(e.velocityValue.x,e.velocityValue.y,e.velocityValue.z),spread:new THREE.Vector3(e.velocitySpread.x,e.velocitySpread.y,e.velocitySpread.z)},drag:{value:new THREE.Vector3(e.dragValue.x,e.dragValue.y,e.dragValue.z),spread:new THREE.Vector3(e.dragSpread.x,e.dragSpread.y,e.dragSpread.z),randomise:e.dragRandomise},color:{value:e.color?.map((function(e){return new THREE.Color(e)}))},size:{value:e.size?.map((function(e){return parseFloat(e)})),spread:e.sizeSpread?.map((function(e){return parseFloat(e)}))},direction:{value:e.direction},duration:e.duration,opacity:{value:e.opacity?.map((function(e){return parseFloat(e)})),spread:e.opacitySpread?.map((function(e){return parseFloat(e)}))},particleCount:e.particleCount});this.particleGroup.addEmitter(i),this.particleGroup.mesh.frustumCulled=!1,this.el.setObject3D("particle-system",this.particleGroup.mesh)}})})(); -------------------------------------------------------------------------------- /dist/images/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-particle-system-component/dc633448da3461704fcfe79167caf8f787e1f1d2/dist/images/cloud.png -------------------------------------------------------------------------------- /dist/images/cloudSml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-particle-system-component/dc633448da3461704fcfe79167caf8f787e1f1d2/dist/images/cloudSml.png -------------------------------------------------------------------------------- /dist/images/raindrop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-particle-system-component/dc633448da3461704fcfe79167caf8f787e1f1d2/dist/images/raindrop.png -------------------------------------------------------------------------------- /dist/images/shockwave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-particle-system-component/dc633448da3461704fcfe79167caf8f787e1f1d2/dist/images/shockwave.png -------------------------------------------------------------------------------- /dist/images/smokeparticle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-particle-system-component/dc633448da3461704fcfe79167caf8f787e1f1d2/dist/images/smokeparticle.png -------------------------------------------------------------------------------- /dist/images/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-particle-system-component/dc633448da3461704fcfe79167caf8f787e1f1d2/dist/images/star.png -------------------------------------------------------------------------------- /dist/images/star2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-particle-system-component/dc633448da3461704fcfe79167caf8f787e1f1d2/dist/images/star2.png -------------------------------------------------------------------------------- /examples/colors/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Colors Example — A-Frame Particle System Component 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/dust/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dust Example — A-Frame Particle System Component 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/rain/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rain Example — A-Frame Particle System Component 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/snow/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Snow Example — A-Frame Particle System Component 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/stars/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Stars Example — A-Frame Particle System Component 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A-Frame Particle System Component 4 | 5 | 46 | 47 | 48 |

A-Frame Particle System Component

49 | 50 |
    51 |
  • 52 | 53 |

    Stars

    54 |

    Default preset.

    55 |
  • 56 | 57 |
  • 58 | 59 |

    Dust

    60 |

    61 |
  • 62 | 63 |
  • 64 | 65 |

    Rain

    66 |

    67 |
  • 68 | 69 |
  • 70 | 71 |

    Snow

    72 |

    73 |
  • 74 | 75 |
  • 76 | 77 |

    Colors

    78 |

    79 |
  • 80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Particles component for A-Frame. 3 | * 4 | * ShaderParticleEngine by Squarefeet (https://github.com/squarefeet). 5 | */ 6 | 7 | var SPE = require('./lib/SPE.js'); 8 | 9 | if (typeof AFRAME === 'undefined') { 10 | throw new Error('Component attempted to register before AFRAME was available.'); 11 | } 12 | 13 | AFRAME.registerComponent('particle-system', { 14 | 15 | schema: { 16 | preset: { 17 | type: 'string', 18 | default: '', 19 | oneOf: ['default', 'dust', 'snow', 'rain'] 20 | }, 21 | maxAge: { 22 | type: 'number', 23 | default: 6 24 | }, 25 | positionSpread: { 26 | type: 'vec3', 27 | default: { x: 0, y: 0, z: 0 } 28 | }, 29 | type: { 30 | type: 'number', 31 | default: SPE.distributions.BOX 32 | }, 33 | rotationAxis: { 34 | type: 'string', 35 | default: 'x' 36 | }, 37 | rotationAngle: { 38 | type: 'number', 39 | default: 0 40 | }, 41 | rotationAngleSpread: { 42 | type: 'number', 43 | default: 0 44 | }, 45 | accelerationValue: { 46 | type: 'vec3', 47 | default: { x: 0, y: -10, z: 0 } 48 | }, 49 | accelerationSpread: { 50 | type: 'vec3', 51 | default: { x: 10, y: 0, z: 10 } 52 | }, 53 | velocityValue: { 54 | type: 'vec3', 55 | default: { x: 0, y: 25, z: 0 } 56 | }, 57 | velocitySpread: { 58 | type: 'vec3', 59 | default: { x: 10, y: 7.5, z: 10 } 60 | }, 61 | dragValue: { 62 | type: 'number', 63 | default: 0 64 | }, 65 | dragSpread: { 66 | type: 'number', 67 | default: 0 68 | }, 69 | dragRandomise: { 70 | type: 'boolean', 71 | default: false 72 | }, 73 | color: { 74 | type: 'array', 75 | default: [ '#0000FF', '#FF0000' ] 76 | }, 77 | size: { 78 | type: 'array', 79 | default: [ '1' ] 80 | }, 81 | sizeSpread: { 82 | type: 'array', 83 | default: [ '0' ] 84 | }, 85 | direction: { 86 | type: 'number', 87 | default: 1 88 | }, 89 | duration: { 90 | type: 'number', 91 | default: Infinity 92 | }, 93 | particleCount: { 94 | type: 'number', 95 | default: 1000 96 | }, 97 | texture: { 98 | type: 'asset', 99 | default: 'https://cdn.rawgit.com/c-frame/aframe-particle-system-component/master/dist/images/star2.png' 100 | }, 101 | randomise: { 102 | type: 'boolean', 103 | default: false 104 | }, 105 | opacity: { 106 | type: 'array', 107 | default: [ '1' ] 108 | }, 109 | opacitySpread: { 110 | type: 'array', 111 | default: [ '0' ] 112 | }, 113 | maxParticleCount: { 114 | type: 'number', 115 | default: 250000 116 | }, 117 | blending: { 118 | type: 'number', 119 | default: THREE.AdditiveBlending, 120 | oneOf: [THREE.NoBlending,THREE.NormalBlending,THREE.AdditiveBlending,THREE.SubtractiveBlending,THREE.MultiplyBlending] 121 | }, 122 | enabled: { 123 | type:'boolean', 124 | default:true 125 | } 126 | }, 127 | 128 | 129 | init: function() { 130 | 131 | this.presets = {}; 132 | 133 | /* preset settings can be overwritten */ 134 | 135 | this.presets['dust'] = { 136 | maxAge: 20, 137 | positionSpread: {x:100,y:100,z:100}, 138 | rotationAngle: 3.14, 139 | accelerationValue: {x: 0, y: 0, z: 0}, 140 | accelerationSpread: {x: 0, y: 0, z: 0}, 141 | velocityValue: {x: 1, y: 0.3, z: 1}, 142 | velocitySpread: {x: 0.5, y: 1, z: 0.5}, 143 | color: ['#FFFFFF'], 144 | particleCount: 100, 145 | texture: 'https://cdn.rawgit.com/c-frame/aframe-particle-system-component/master/dist/images/smokeparticle.png' 146 | }; 147 | 148 | 149 | this.presets['snow'] = { 150 | maxAge: 20, 151 | positionSpread: {x:100,y:100,z:100}, 152 | rotationAngle: 3.14, 153 | accelerationValue: {x: 0, y: 0, z: 0}, 154 | accelerationSpread: {x: 0.2, y: 0, z: 0.2}, 155 | velocityValue: {x: 0, y: 8, z: 0}, 156 | velocitySpread: {x: 2, y: 0, z: 2}, 157 | color: ['#FFFFFF'], 158 | particleCount: 200, 159 | texture: 'https://cdn.rawgit.com/c-frame/aframe-particle-system-component/master/dist/images/smokeparticle.png' 160 | }; 161 | 162 | 163 | this.presets['rain'] = { 164 | maxAge: 1, 165 | positionSpread: {x:100,y:100,z:100}, 166 | rotationAngle: 3.14, 167 | accelerationValue: {x: 0, y: 3, z: 0}, 168 | accelerationSpread: {x: 2, y: 1, z: 2}, 169 | velocityValue: {x: 0, y: 75, z: 0}, 170 | velocitySpread: {x: 10, y: 50, z: 10}, 171 | color: ['#FFFFFF'], 172 | size: [0.4], 173 | texture: 'https://cdn.rawgit.com/c-frame/aframe-particle-system-component/master/dist/images/raindrop.png' 174 | }; 175 | 176 | 177 | }, 178 | 179 | 180 | update: function (oldData) { 181 | 182 | // Remove old particle group. 183 | if (this.particleGroup) { 184 | this.el.removeObject3D('particle-system'); 185 | } 186 | 187 | // Set the selected preset, if any, or use an empty object to keep schema defaults 188 | this.preset = this.presets[this.data.preset] || {}; 189 | 190 | // Get custom, preset, or default data for each property defined in the schema 191 | for (var key in this.data) { 192 | this.data[key] = this.applyPreset(key); 193 | } 194 | 195 | this.initParticleSystem(this.data); 196 | 197 | if(this.data.enabled === true) { 198 | this.startParticles() 199 | } else { 200 | this.stopParticles() 201 | } 202 | }, 203 | 204 | 205 | applyPreset: function (key) { 206 | // !this.attrValue[key] = the user did not set a custom value 207 | // this.preset[key] = there exists a value for this key in the selected preset 208 | if (!this.attrValue[key] && this.preset[key]) { 209 | return this.preset[key]; 210 | } else { 211 | // Otherwise stick to the user or schema default value 212 | return this.data[key]; 213 | } 214 | }, 215 | 216 | 217 | tick: function(time, dt) { 218 | 219 | this.particleGroup.tick(dt / 1000); 220 | }, 221 | 222 | 223 | remove: function() { 224 | 225 | // Remove particle system. 226 | if (!this.particleGroup) { return; } 227 | this.el.removeObject3D('particle-system'); 228 | }, 229 | 230 | startParticles: function() { 231 | this.particleGroup.emitters.forEach(function(em) { em.enable() }); 232 | }, 233 | 234 | stopParticles: function() { 235 | this.particleGroup.emitters.forEach(function(em) { em.disable() }); 236 | }, 237 | 238 | 239 | initParticleSystem: function(settings) { 240 | 241 | var loader = new THREE.TextureLoader(); 242 | var particle_texture = loader.load( 243 | settings.texture, 244 | function (texture) { 245 | return texture; 246 | }, 247 | function (xhr) { 248 | console.log((xhr.loaded / xhr.total * 100) + '% loaded'); 249 | }, 250 | function (xhr) { 251 | console.log('An error occurred'); 252 | } 253 | ); 254 | 255 | this.particleGroup = new SPE.Group({ 256 | texture: { 257 | value: particle_texture 258 | }, 259 | maxParticleCount: settings.maxParticleCount, 260 | blending: settings.blending 261 | }); 262 | 263 | var emitter = new SPE.Emitter({ 264 | maxAge: { 265 | value: settings.maxAge 266 | }, 267 | type: { 268 | value: settings.type 269 | }, 270 | position: { 271 | spread: new THREE.Vector3(settings.positionSpread.x, settings.positionSpread.y, settings.positionSpread.z), 272 | randomise: settings.randomise 273 | //spreadClamp: new THREE.Vector3( 2, 2, 2 ), 274 | //radius: 4 275 | }, 276 | rotation: { 277 | axis: (settings.rotationAxis=='x'?new THREE.Vector3(1, 0, 0):(settings.rotationAxis=='y'?new THREE.Vector3(0, 1, 0):(settings.rotationAxis=='z'?new THREE.Vector3(0, 0, 1):new THREE.Vector3(0, 1, 0)))), 278 | angle: settings.rotationAngle, 279 | angleSpread: settings.rotationAngleSpread, 280 | static: true 281 | }, 282 | acceleration: { 283 | value: new THREE.Vector3(settings.accelerationValue.x, settings.accelerationValue.y, settings.accelerationValue.z), 284 | spread: new THREE.Vector3(settings.accelerationSpread.x, settings.accelerationSpread.y, settings.accelerationSpread.z) 285 | }, 286 | velocity: { 287 | value: new THREE.Vector3(settings.velocityValue.x, settings.velocityValue.y, settings.velocityValue.z), 288 | spread: new THREE.Vector3(settings.velocitySpread.x, settings.velocitySpread.y, settings.velocitySpread.z) 289 | }, 290 | drag: { 291 | value: new THREE.Vector3(settings.dragValue.x, settings.dragValue.y, settings.dragValue.z), 292 | spread: new THREE.Vector3(settings.dragSpread.x, settings.dragSpread.y, settings.dragSpread.z), 293 | randomise: settings.dragRandomise 294 | }, 295 | color: { 296 | value: settings.color?.map(function(c) { return new THREE.Color(c); }) 297 | }, 298 | size: { value: settings.size?.map(function (s) { return parseFloat(s); }), 299 | spread: settings.sizeSpread?.map(function (s) { return parseFloat(s); }) }, 300 | 301 | /*wiggle: { value: 4, spread: 2 }, //settings.wiggle,*/ 302 | /*drag: { 303 | value: settings.drag 304 | },*/ 305 | direction: { 306 | value: settings.direction 307 | }, 308 | duration: settings.duration, 309 | opacity: { value: settings.opacity?.map(function (o) { return parseFloat(o); }), 310 | spread: settings.opacitySpread?.map(function (o) { return parseFloat(o); }) }, 311 | particleCount: settings.particleCount 312 | }); 313 | 314 | this.particleGroup.addEmitter(emitter); 315 | this.particleGroup.mesh.frustumCulled = false; 316 | this.el.setObject3D('particle-system', this.particleGroup.mesh); 317 | } 318 | }); 319 | -------------------------------------------------------------------------------- /lib/SPE.js: -------------------------------------------------------------------------------- 1 | /* shader-particle-engine 1.0.6 2 | * 3 | * (c) 2015 Luke Moody (http://www.github.com/squarefeet) 4 | * Originally based on Lee Stemkoski's original work (https://github.com/stemkoski/stemkoski.github.com/blob/master/Three.js/js/ParticleEngine.js). 5 | * 6 | * shader-particle-engine may be freely distributed under the MIT license (See LICENSE at root of this repository.) 7 | */ 8 | /** 9 | * @typedef {Number} distribution 10 | * @property {Number} SPE.distributions.BOX Values will be distributed within a box. 11 | * @property {Number} SPE.distributions.SPHERE Values will be distributed within a sphere. 12 | * @property {Number} SPE.distributions.DISC Values will be distributed within a 2D disc. 13 | */ 14 | 15 | /** 16 | * Namespace for Shader Particle Engine. 17 | * 18 | * All SPE-related code sits under this namespace. 19 | * 20 | * @type {Object} 21 | * @namespace 22 | */ 23 | var SPE = { 24 | 25 | /** 26 | * A map of supported distribution types used 27 | * by SPE.Emitter instances. 28 | * 29 | * These distribution types can be applied to 30 | * an emitter globally, which will affect the 31 | * `position`, `velocity`, and `acceleration` 32 | * value calculations for an emitter, or they 33 | * can be applied on a per-property basis. 34 | * 35 | * @enum {Number} 36 | */ 37 | distributions: { 38 | /** 39 | * Values will be distributed within a box. 40 | * @type {Number} 41 | */ 42 | BOX: 1, 43 | 44 | /** 45 | * Values will be distributed on a sphere. 46 | * @type {Number} 47 | */ 48 | SPHERE: 2, 49 | 50 | /** 51 | * Values will be distributed on a 2d-disc shape. 52 | * @type {Number} 53 | */ 54 | DISC: 3, 55 | 56 | /** 57 | * Values will be distributed along a line. 58 | * @type {Number} 59 | */ 60 | LINE: 4 61 | }, 62 | 63 | 64 | /** 65 | * Set this value to however many 'steps' you 66 | * want value-over-lifetime properties to have. 67 | * 68 | * It's adjustable to fix an interpolation problem: 69 | * 70 | * Assuming you specify an opacity value as [0, 1, 0] 71 | * and the `valueOverLifetimeLength` is 4, then the 72 | * opacity value array will be reinterpolated to 73 | * be [0, 0.66, 0.66, 0]. 74 | * This isn't ideal, as particles would never reach 75 | * full opacity. 76 | * 77 | * NOTE: 78 | * This property affects the length of ALL 79 | * value-over-lifetime properties for ALL 80 | * emitters and ALL groups. 81 | * 82 | * Only values >= 3 && <= 4 are allowed. 83 | * 84 | * @type {Number} 85 | */ 86 | valueOverLifetimeLength: 4 87 | }; 88 | 89 | // Module loader support: 90 | if ( typeof define === 'function' && define.amd ) { 91 | define( 'spe', SPE ); 92 | } 93 | else if ( typeof exports !== 'undefined' && typeof module !== 'undefined' ) { 94 | module.exports = SPE; 95 | } 96 | 97 | 98 | /** 99 | * A helper class for TypedArrays. 100 | * 101 | * Allows for easy resizing, assignment of various component-based 102 | * types (Vector2s, Vector3s, Vector4s, Mat3s, Mat4s), 103 | * as well as Colors (where components are `r`, `g`, `b`), 104 | * Numbers, and setting from other TypedArrays. 105 | * 106 | * @author Luke Moody 107 | * @constructor 108 | * @param {Function} TypedArrayConstructor The constructor to use (Float32Array, Uint8Array, etc.) 109 | * @param {Number} size The size of the array to create 110 | * @param {Number} componentSize The number of components per-value (ie. 3 for a vec3, 9 for a Mat3, etc.) 111 | * @param {Number} indexOffset The index in the array from which to start assigning values. Default `0` if none provided 112 | */ 113 | SPE.TypedArrayHelper = function( TypedArrayConstructor, size, componentSize, indexOffset ) { 114 | 'use strict'; 115 | 116 | this.componentSize = componentSize || 1; 117 | this.size = ( size || 1 ); 118 | this.TypedArrayConstructor = TypedArrayConstructor || Float32Array; 119 | this.array = new TypedArrayConstructor( size * this.componentSize ); 120 | this.indexOffset = indexOffset || 0; 121 | }; 122 | 123 | SPE.TypedArrayHelper.constructor = SPE.TypedArrayHelper; 124 | 125 | /** 126 | * Sets the size of the internal array. 127 | * 128 | * Delegates to `this.shrink` or `this.grow` depending on size 129 | * argument's relation to the current size of the internal array. 130 | * 131 | * Note that if the array is to be shrunk, data will be lost. 132 | * 133 | * @param {Number} size The new size of the array. 134 | */ 135 | SPE.TypedArrayHelper.prototype.setSize = function( size, noComponentMultiply ) { 136 | 'use strict'; 137 | 138 | var currentArraySize = this.array.length; 139 | 140 | if ( !noComponentMultiply ) { 141 | size = size * this.componentSize; 142 | } 143 | 144 | if ( size < currentArraySize ) { 145 | return this.shrink( size ); 146 | } 147 | else if ( size > currentArraySize ) { 148 | return this.grow( size ); 149 | } 150 | else { 151 | console.info( 'TypedArray is already of size:', size + '.', 'Will not resize.' ); 152 | } 153 | }; 154 | 155 | /** 156 | * Shrinks the internal array. 157 | * 158 | * @param {Number} size The new size of the typed array. Must be smaller than `this.array.length`. 159 | * @return {SPE.TypedArrayHelper} Instance of this class. 160 | */ 161 | SPE.TypedArrayHelper.prototype.shrink = function( size ) { 162 | 'use strict'; 163 | 164 | this.array = this.array.subarray( 0, size ); 165 | this.size = size; 166 | return this; 167 | }; 168 | 169 | /** 170 | * Grows the internal array. 171 | * @param {Number} size The new size of the typed array. Must be larger than `this.array.length`. 172 | * @return {SPE.TypedArrayHelper} Instance of this class. 173 | */ 174 | SPE.TypedArrayHelper.prototype.grow = function( size ) { 175 | 'use strict'; 176 | 177 | var existingArray = this.array, 178 | newArray = new this.TypedArrayConstructor( size ); 179 | 180 | newArray.set( existingArray ); 181 | this.array = newArray; 182 | this.size = size; 183 | 184 | return this; 185 | }; 186 | 187 | 188 | /** 189 | * Perform a splice operation on this array's buffer. 190 | * @param {Number} start The start index of the splice. Will be multiplied by the number of components for this attribute. 191 | * @param {Number} end The end index of the splice. Will be multiplied by the number of components for this attribute. 192 | * @returns {Object} The SPE.TypedArrayHelper instance. 193 | */ 194 | SPE.TypedArrayHelper.prototype.splice = function( start, end ) { 195 | 'use strict'; 196 | start *= this.componentSize; 197 | end *= this.componentSize; 198 | 199 | var data = [], 200 | array = this.array, 201 | size = array.length; 202 | 203 | for ( var i = 0; i < size; ++i ) { 204 | if ( i < start || i >= end ) { 205 | data.push( array[ i ] ); 206 | } 207 | // array[ i ] = 0; 208 | } 209 | 210 | this.setFromArray( 0, data ); 211 | 212 | return this; 213 | }; 214 | 215 | 216 | /** 217 | * Copies from the given TypedArray into this one, using the index argument 218 | * as the start position. Alias for `TypedArray.set`. Will automatically resize 219 | * if the given source array is of a larger size than the internal array. 220 | * 221 | * @param {Number} index The start position from which to copy into this array. 222 | * @param {TypedArray} array The array from which to copy; the source array. 223 | * @return {SPE.TypedArrayHelper} Instance of this class. 224 | */ 225 | SPE.TypedArrayHelper.prototype.setFromArray = function( index, array ) { 226 | 'use strict'; 227 | 228 | var sourceArraySize = array.length, 229 | newSize = index + sourceArraySize; 230 | 231 | if ( newSize > this.array.length ) { 232 | this.grow( newSize ); 233 | } 234 | else if ( newSize < this.array.length ) { 235 | this.shrink( newSize ); 236 | } 237 | 238 | this.array.set( array, this.indexOffset + index ); 239 | 240 | return this; 241 | }; 242 | 243 | /** 244 | * Set a Vector2 value at `index`. 245 | * 246 | * @param {Number} index The index at which to set the vec2 values from. 247 | * @param {Vector2} vec2 Any object that has `x` and `y` properties. 248 | * @return {SPE.TypedArrayHelper} Instance of this class. 249 | */ 250 | SPE.TypedArrayHelper.prototype.setVec2 = function( index, vec2 ) { 251 | 'use strict'; 252 | 253 | return this.setVec2Components( index, vec2.x, vec2.y ); 254 | }; 255 | 256 | /** 257 | * Set a Vector2 value using raw components. 258 | * 259 | * @param {Number} index The index at which to set the vec2 values from. 260 | * @param {Number} x The Vec2's `x` component. 261 | * @param {Number} y The Vec2's `y` component. 262 | * @return {SPE.TypedArrayHelper} Instance of this class. 263 | */ 264 | SPE.TypedArrayHelper.prototype.setVec2Components = function( index, x, y ) { 265 | 'use strict'; 266 | 267 | var array = this.array, 268 | i = this.indexOffset + ( index * this.componentSize ); 269 | 270 | array[ i ] = x; 271 | array[ i + 1 ] = y; 272 | return this; 273 | }; 274 | 275 | /** 276 | * Set a Vector3 value at `index`. 277 | * 278 | * @param {Number} index The index at which to set the vec3 values from. 279 | * @param {Vector3} vec2 Any object that has `x`, `y`, and `z` properties. 280 | * @return {SPE.TypedArrayHelper} Instance of this class. 281 | */ 282 | SPE.TypedArrayHelper.prototype.setVec3 = function( index, vec3 ) { 283 | 'use strict'; 284 | 285 | return this.setVec3Components( index, vec3.x, vec3.y, vec3.z ); 286 | }; 287 | 288 | /** 289 | * Set a Vector3 value using raw components. 290 | * 291 | * @param {Number} index The index at which to set the vec3 values from. 292 | * @param {Number} x The Vec3's `x` component. 293 | * @param {Number} y The Vec3's `y` component. 294 | * @param {Number} z The Vec3's `z` component. 295 | * @return {SPE.TypedArrayHelper} Instance of this class. 296 | */ 297 | SPE.TypedArrayHelper.prototype.setVec3Components = function( index, x, y, z ) { 298 | 'use strict'; 299 | 300 | var array = this.array, 301 | i = this.indexOffset + ( index * this.componentSize ); 302 | 303 | array[ i ] = x; 304 | array[ i + 1 ] = y; 305 | array[ i + 2 ] = z; 306 | return this; 307 | }; 308 | 309 | /** 310 | * Set a Vector4 value at `index`. 311 | * 312 | * @param {Number} index The index at which to set the vec4 values from. 313 | * @param {Vector4} vec2 Any object that has `x`, `y`, `z`, and `w` properties. 314 | * @return {SPE.TypedArrayHelper} Instance of this class. 315 | */ 316 | SPE.TypedArrayHelper.prototype.setVec4 = function( index, vec4 ) { 317 | 'use strict'; 318 | 319 | return this.setVec4Components( index, vec4.x, vec4.y, vec4.z, vec4.w ); 320 | }; 321 | 322 | /** 323 | * Set a Vector4 value using raw components. 324 | * 325 | * @param {Number} index The index at which to set the vec4 values from. 326 | * @param {Number} x The Vec4's `x` component. 327 | * @param {Number} y The Vec4's `y` component. 328 | * @param {Number} z The Vec4's `z` component. 329 | * @param {Number} w The Vec4's `w` component. 330 | * @return {SPE.TypedArrayHelper} Instance of this class. 331 | */ 332 | SPE.TypedArrayHelper.prototype.setVec4Components = function( index, x, y, z, w ) { 333 | 'use strict'; 334 | 335 | var array = this.array, 336 | i = this.indexOffset + ( index * this.componentSize ); 337 | 338 | array[ i ] = x; 339 | array[ i + 1 ] = y; 340 | array[ i + 2 ] = z; 341 | array[ i + 3 ] = w; 342 | return this; 343 | }; 344 | 345 | /** 346 | * Set a Matrix3 value at `index`. 347 | * 348 | * @param {Number} index The index at which to set the matrix values from. 349 | * @param {Matrix3} mat3 The 3x3 matrix to set from. Must have a TypedArray property named `elements` to copy from. 350 | * @return {SPE.TypedArrayHelper} Instance of this class. 351 | */ 352 | SPE.TypedArrayHelper.prototype.setMat3 = function( index, mat3 ) { 353 | 'use strict'; 354 | 355 | return this.setFromArray( this.indexOffset + ( index * this.componentSize ), mat3.elements ); 356 | }; 357 | 358 | /** 359 | * Set a Matrix4 value at `index`. 360 | * 361 | * @param {Number} index The index at which to set the matrix values from. 362 | * @param {Matrix4} mat3 The 4x4 matrix to set from. Must have a TypedArray property named `elements` to copy from. 363 | * @return {SPE.TypedArrayHelper} Instance of this class. 364 | */ 365 | SPE.TypedArrayHelper.prototype.setMat4 = function( index, mat4 ) { 366 | 'use strict'; 367 | 368 | return this.setFromArray( this.indexOffset + ( index * this.componentSize ), mat4.elements ); 369 | }; 370 | 371 | /** 372 | * Set a Color value at `index`. 373 | * 374 | * @param {Number} index The index at which to set the vec3 values from. 375 | * @param {Color} color Any object that has `r`, `g`, and `b` properties. 376 | * @return {SPE.TypedArrayHelper} Instance of this class. 377 | */ 378 | SPE.TypedArrayHelper.prototype.setColor = function( index, color ) { 379 | 'use strict'; 380 | 381 | return this.setVec3Components( index, color.r, color.g, color.b ); 382 | }; 383 | 384 | /** 385 | * Set a Number value at `index`. 386 | * 387 | * @param {Number} index The index at which to set the vec3 values from. 388 | * @param {Number} numericValue The number to assign to this index in the array. 389 | * @return {SPE.TypedArrayHelper} Instance of this class. 390 | */ 391 | SPE.TypedArrayHelper.prototype.setNumber = function( index, numericValue ) { 392 | 'use strict'; 393 | 394 | this.array[ this.indexOffset + ( index * this.componentSize ) ] = numericValue; 395 | return this; 396 | }; 397 | 398 | /** 399 | * Returns the value of the array at the given index, taking into account 400 | * the `indexOffset` property of this class. 401 | * 402 | * Note that this function ignores the component size and will just return a 403 | * single value. 404 | * 405 | * @param {Number} index The index in the array to fetch. 406 | * @return {Number} The value at the given index. 407 | */ 408 | SPE.TypedArrayHelper.prototype.getValueAtIndex = function( index ) { 409 | 'use strict'; 410 | 411 | return this.array[ this.indexOffset + index ]; 412 | }; 413 | 414 | /** 415 | * Returns the component value of the array at the given index, taking into account 416 | * the `indexOffset` property of this class. 417 | * 418 | * If the componentSize is set to 3, then it will return a new TypedArray 419 | * of length 3. 420 | * 421 | * @param {Number} index The index in the array to fetch. 422 | * @return {TypedArray} The component value at the given index. 423 | */ 424 | SPE.TypedArrayHelper.prototype.getComponentValueAtIndex = function( index ) { 425 | 'use strict'; 426 | 427 | return this.array.subarray( this.indexOffset + ( index * this.componentSize ) ); 428 | }; 429 | 430 | /** 431 | * A helper to handle creating and updating a THREE.BufferAttribute instance. 432 | * 433 | * @author Luke Moody 434 | * @constructor 435 | * @param {String} type The buffer attribute type. See SPE.ShaderAttribute.typeSizeMap for valid values. 436 | * @param {Boolean=} dynamicBuffer Whether this buffer attribute should be marked as dynamic or not. 437 | * @param {Function=} arrayType A reference to a TypedArray constructor. Defaults to Float32Array if none provided. 438 | */ 439 | SPE.ShaderAttribute = function( type, dynamicBuffer, arrayType ) { 440 | 'use strict'; 441 | 442 | var typeMap = SPE.ShaderAttribute.typeSizeMap; 443 | 444 | this.type = typeof type === 'string' && typeMap.hasOwnProperty( type ) ? type : 'f'; 445 | this.componentSize = typeMap[ this.type ]; 446 | this.arrayType = arrayType || Float32Array; 447 | this.typedArray = null; 448 | this.bufferAttribute = null; 449 | this.dynamicBuffer = !!dynamicBuffer; 450 | 451 | this.updateMin = 0; 452 | this.updateMax = 0; 453 | }; 454 | 455 | SPE.ShaderAttribute.constructor = SPE.ShaderAttribute; 456 | 457 | /** 458 | * A map of uniform types to their component size. 459 | * @enum {Number} 460 | */ 461 | SPE.ShaderAttribute.typeSizeMap = { 462 | /** 463 | * Float 464 | * @type {Number} 465 | */ 466 | f: 1, 467 | 468 | /** 469 | * Vec2 470 | * @type {Number} 471 | */ 472 | v2: 2, 473 | 474 | /** 475 | * Vec3 476 | * @type {Number} 477 | */ 478 | v3: 3, 479 | 480 | /** 481 | * Vec4 482 | * @type {Number} 483 | */ 484 | v4: 4, 485 | 486 | /** 487 | * Color 488 | * @type {Number} 489 | */ 490 | c: 3, 491 | 492 | /** 493 | * Mat3 494 | * @type {Number} 495 | */ 496 | m3: 9, 497 | 498 | /** 499 | * Mat4 500 | * @type {Number} 501 | */ 502 | m4: 16 503 | }; 504 | 505 | /** 506 | * Calculate the minimum and maximum update range for this buffer attribute using 507 | * component size independant min and max values. 508 | * 509 | * @param {Number} min The start of the range to mark as needing an update. 510 | * @param {Number} max The end of the range to mark as needing an update. 511 | */ 512 | SPE.ShaderAttribute.prototype.setUpdateRange = function( min, max ) { 513 | 'use strict'; 514 | this.updateMin = Math.min( min * this.componentSize, this.updateMin * this.componentSize ); 515 | this.updateMax = Math.max( max * this.componentSize, this.updateMax * this.componentSize ); 516 | }; 517 | 518 | /** 519 | * Calculate the number of indices that this attribute should mark as needing 520 | * updating. Also marks the attribute as needing an update. 521 | */ 522 | SPE.ShaderAttribute.prototype.flagUpdate = function() { 523 | 'use strict'; 524 | 525 | var attr = this.bufferAttribute; 526 | 527 | var count = Math.min( ( this.updateMax - this.updateMin ) + this.componentSize, this.typedArray.array.length ); 528 | attr.clearUpdateRanges(); 529 | attr.addUpdateRange(this.updateMin, count); 530 | // console.log( range.offset, range.count, this.typedArray.array.length ); 531 | // console.log( 'flagUpdate:', range.offset, range.count ); 532 | attr.needsUpdate = true; 533 | }; 534 | 535 | 536 | 537 | /** 538 | * Reset the index update counts for this attribute 539 | */ 540 | SPE.ShaderAttribute.prototype.resetUpdateRange = function() { 541 | 'use strict'; 542 | this.updateMin = 0; 543 | this.updateMax = 0; 544 | }; 545 | 546 | SPE.ShaderAttribute.prototype.resetDynamic = function() { 547 | 'use strict'; 548 | this.bufferAttribute.usage = this.dynamicBuffer ? 549 | THREE.DynamicDrawUsage : 550 | THREE.StaticDrawUsage; 551 | }; 552 | 553 | /** 554 | * Perform a splice operation on this attribute's buffer. 555 | * @param {Number} start The start index of the splice. Will be multiplied by the number of components for this attribute. 556 | * @param {Number} end The end index of the splice. Will be multiplied by the number of components for this attribute. 557 | */ 558 | SPE.ShaderAttribute.prototype.splice = function( start, end ) { 559 | 'use strict'; 560 | 561 | this.typedArray.splice( start, end ); 562 | 563 | // Reset the reference to the attribute's typed array 564 | // since it has probably changed. 565 | this.forceUpdateAll(); 566 | }; 567 | 568 | SPE.ShaderAttribute.prototype.forceUpdateAll = function() { 569 | 'use strict'; 570 | 571 | this.bufferAttribute.array = this.typedArray.array; 572 | this.bufferAttribute.clearUpdateRanges(); 573 | // this.bufferAttribute.dynamic = false; 574 | // this.bufferAttribute.usage = this.dynamicBuffer ? 575 | // THREE.DynamicDrawUsage : 576 | // THREE.StaticDrawUsage; 577 | 578 | this.bufferAttribute.usage = THREE.StaticDrawUsage; 579 | this.bufferAttribute.needsUpdate = true; 580 | }; 581 | 582 | /** 583 | * Make sure this attribute has a typed array associated with it. 584 | * 585 | * If it does, then it will ensure the typed array is of the correct size. 586 | * 587 | * If not, a new SPE.TypedArrayHelper instance will be created. 588 | * 589 | * @param {Number} size The size of the typed array to create or update to. 590 | */ 591 | SPE.ShaderAttribute.prototype._ensureTypedArray = function( size ) { 592 | 'use strict'; 593 | 594 | // Condition that's most likely to be true at the top: no change. 595 | if ( this.typedArray !== null && this.typedArray.size === size * this.componentSize ) { 596 | return; 597 | } 598 | 599 | // Resize the array if we need to, telling the TypedArrayHelper to 600 | // ignore it's component size when evaluating size. 601 | else if ( this.typedArray !== null && this.typedArray.size !== size ) { 602 | this.typedArray.setSize( size ); 603 | } 604 | 605 | // This condition should only occur once in an attribute's lifecycle. 606 | else if ( this.typedArray === null ) { 607 | this.typedArray = new SPE.TypedArrayHelper( this.arrayType, size, this.componentSize ); 608 | } 609 | }; 610 | 611 | 612 | /** 613 | * Creates a THREE.BufferAttribute instance if one doesn't exist already. 614 | * 615 | * Ensures a typed array is present by calling _ensureTypedArray() first. 616 | * 617 | * If a buffer attribute exists already, then it will be marked as needing an update. 618 | * 619 | * @param {Number} size The size of the typed array to create if one doesn't exist, or resize existing array to. 620 | */ 621 | SPE.ShaderAttribute.prototype._createBufferAttribute = function( size ) { 622 | 'use strict'; 623 | 624 | // Make sure the typedArray is present and correct. 625 | this._ensureTypedArray( size ); 626 | 627 | // Don't create it if it already exists, but do 628 | // flag that it needs updating on the next render 629 | // cycle. 630 | if ( this.bufferAttribute !== null ) { 631 | this.bufferAttribute.array = this.typedArray.array; 632 | 633 | // Since THREE.js version 81, dynamic count calculation was removed 634 | // so I need to do it manually here. 635 | // 636 | // In the next minor release, I may well remove this check and force 637 | // dependency on THREE r81+. 638 | if ( parseFloat( THREE.REVISION ) >= 81 ) { 639 | this.bufferAttribute.count = this.bufferAttribute.array.length / this.bufferAttribute.itemSize; 640 | } 641 | 642 | this.bufferAttribute.needsUpdate = true; 643 | return; 644 | } 645 | 646 | this.bufferAttribute = new THREE.BufferAttribute( this.typedArray.array, this.componentSize ); 647 | // this.bufferAttribute.dynamic = this.dynamicBuffer; 648 | this.bufferAttribute.usage = this.dynamicBuffer ? 649 | THREE.DynamicDrawUsage : 650 | THREE.StaticDrawUsage; 651 | }; 652 | 653 | /** 654 | * Returns the length of the typed array associated with this attribute. 655 | * @return {Number} The length of the typed array. Will be 0 if no typed array has been created yet. 656 | */ 657 | SPE.ShaderAttribute.prototype.getLength = function() { 658 | 'use strict'; 659 | 660 | if ( this.typedArray === null ) { 661 | return 0; 662 | } 663 | 664 | return this.typedArray.array.length; 665 | }; 666 | 667 | 668 | SPE.shaderChunks = { 669 | // Register color-packing define statements. 670 | defines: [ 671 | '#define PACKED_COLOR_SIZE 256.0', 672 | '#define PACKED_COLOR_DIVISOR 255.0' 673 | ].join( '\n' ), 674 | 675 | // All uniforms used by vertex / fragment shaders 676 | uniforms: [ 677 | 'uniform float deltaTime;', 678 | 'uniform float runTime;', 679 | 'uniform sampler2D tex;', 680 | 'uniform vec4 textureAnimation;', 681 | 'uniform float scale;', 682 | ].join( '\n' ), 683 | 684 | // All attributes used by the vertex shader. 685 | // 686 | // Note that some attributes are squashed into other ones: 687 | // 688 | // * Drag is acceleration.w 689 | attributes: [ 690 | 'attribute vec4 acceleration;', 691 | 'attribute vec3 velocity;', 692 | 'attribute vec4 rotation;', 693 | 'attribute vec3 rotationCenter;', 694 | 'attribute vec4 params;', 695 | 'attribute vec4 size;', 696 | 'attribute vec4 angle;', 697 | 'attribute vec4 color;', 698 | 'attribute vec4 opacity;' 699 | ].join( '\n' ), 700 | 701 | // 702 | varyings: [ 703 | 'varying vec4 vColor;', 704 | '#ifdef SHOULD_ROTATE_TEXTURE', 705 | ' varying float vAngle;', 706 | '#endif', 707 | 708 | '#ifdef SHOULD_CALCULATE_SPRITE', 709 | ' varying vec4 vSpriteSheet;', 710 | '#endif' 711 | ].join( '\n' ), 712 | 713 | 714 | // Branch-avoiding comparison fns 715 | // - http://theorangeduck.com/page/avoiding-shader-conditionals 716 | branchAvoidanceFunctions: [ 717 | 'float when_gt(float x, float y) {', 718 | ' return max(sign(x - y), 0.0);', 719 | '}', 720 | 721 | 'float when_lt(float x, float y) {', 722 | ' return min( max(1.0 - sign(x - y), 0.0), 1.0 );', 723 | '}', 724 | 725 | 'float when_eq( float x, float y ) {', 726 | ' return 1.0 - abs( sign( x - y ) );', 727 | '}', 728 | 729 | 'float when_ge(float x, float y) {', 730 | ' return 1.0 - when_lt(x, y);', 731 | '}', 732 | 733 | 'float when_le(float x, float y) {', 734 | ' return 1.0 - when_gt(x, y);', 735 | '}', 736 | 737 | // Branch-avoiding logical operators 738 | // (to be used with above comparison fns) 739 | 'float and(float a, float b) {', 740 | ' return a * b;', 741 | '}', 742 | 743 | 'float or(float a, float b) {', 744 | ' return min(a + b, 1.0);', 745 | '}', 746 | ].join( '\n' ), 747 | 748 | 749 | // From: 750 | // - http://stackoverflow.com/a/12553149 751 | // - https://stackoverflow.com/questions/22895237/hexadecimal-to-rgb-values-in-webgl-shader 752 | unpackColor: [ 753 | 'vec3 unpackColor( in float hex ) {', 754 | ' vec3 c = vec3( 0.0 );', 755 | 756 | ' float r = mod( (hex / PACKED_COLOR_SIZE / PACKED_COLOR_SIZE), PACKED_COLOR_SIZE );', 757 | ' float g = mod( (hex / PACKED_COLOR_SIZE), PACKED_COLOR_SIZE );', 758 | ' float b = mod( hex, PACKED_COLOR_SIZE );', 759 | 760 | ' c.r = r / PACKED_COLOR_DIVISOR;', 761 | ' c.g = g / PACKED_COLOR_DIVISOR;', 762 | ' c.b = b / PACKED_COLOR_DIVISOR;', 763 | 764 | ' return c;', 765 | '}', 766 | ].join( '\n' ), 767 | 768 | unpackRotationAxis: [ 769 | 'vec3 unpackRotationAxis( in float hex ) {', 770 | ' vec3 c = vec3( 0.0 );', 771 | 772 | ' float r = mod( (hex / PACKED_COLOR_SIZE / PACKED_COLOR_SIZE), PACKED_COLOR_SIZE );', 773 | ' float g = mod( (hex / PACKED_COLOR_SIZE), PACKED_COLOR_SIZE );', 774 | ' float b = mod( hex, PACKED_COLOR_SIZE );', 775 | 776 | ' c.r = r / PACKED_COLOR_DIVISOR;', 777 | ' c.g = g / PACKED_COLOR_DIVISOR;', 778 | ' c.b = b / PACKED_COLOR_DIVISOR;', 779 | 780 | ' c *= vec3( 2.0 );', 781 | ' c -= vec3( 1.0 );', 782 | 783 | ' return c;', 784 | '}', 785 | ].join( '\n' ), 786 | 787 | floatOverLifetime: [ 788 | 'float getFloatOverLifetime( in float positionInTime, in vec4 attr ) {', 789 | ' highp float value = 0.0;', 790 | ' float deltaAge = positionInTime * float( VALUE_OVER_LIFETIME_LENGTH - 1 );', 791 | ' float fIndex = 0.0;', 792 | ' float shouldApplyValue = 0.0;', 793 | 794 | // This might look a little odd, but it's faster in the testing I've done than using branches. 795 | // Uses basic maths to avoid branching. 796 | // 797 | // Take a look at the branch-avoidance functions defined above, 798 | // and be sure to check out The Orange Duck site where I got this 799 | // from (link above). 800 | 801 | // Fix for static emitters (age is always zero). 802 | ' value += attr[ 0 ] * when_eq( deltaAge, 0.0 );', 803 | '', 804 | ' for( int i = 0; i < VALUE_OVER_LIFETIME_LENGTH - 1; ++i ) {', 805 | ' fIndex = float( i );', 806 | ' shouldApplyValue = and( when_gt( deltaAge, fIndex ), when_le( deltaAge, fIndex + 1.0 ) );', 807 | ' value += shouldApplyValue * mix( attr[ i ], attr[ i + 1 ], deltaAge - fIndex );', 808 | ' }', 809 | '', 810 | ' return value;', 811 | '}', 812 | ].join( '\n' ), 813 | 814 | colorOverLifetime: [ 815 | 'vec3 getColorOverLifetime( in float positionInTime, in vec3 color1, in vec3 color2, in vec3 color3, in vec3 color4 ) {', 816 | ' vec3 value = vec3( 0.0 );', 817 | ' value.x = getFloatOverLifetime( positionInTime, vec4( color1.x, color2.x, color3.x, color4.x ) );', 818 | ' value.y = getFloatOverLifetime( positionInTime, vec4( color1.y, color2.y, color3.y, color4.y ) );', 819 | ' value.z = getFloatOverLifetime( positionInTime, vec4( color1.z, color2.z, color3.z, color4.z ) );', 820 | ' return value;', 821 | '}', 822 | ].join( '\n' ), 823 | 824 | paramFetchingFunctions: [ 825 | 'float getAlive() {', 826 | ' return params.x;', 827 | '}', 828 | 829 | 'float getAge() {', 830 | ' return params.y;', 831 | '}', 832 | 833 | 'float getMaxAge() {', 834 | ' return params.z;', 835 | '}', 836 | 837 | 'float getWiggle() {', 838 | ' return params.w;', 839 | '}', 840 | ].join( '\n' ), 841 | 842 | forceFetchingFunctions: [ 843 | 'vec4 getPosition( in float age ) {', 844 | ' return modelViewMatrix * vec4( position, 1.0 );', 845 | '}', 846 | 847 | 'vec3 getVelocity( in float age ) {', 848 | ' return velocity * age;', 849 | '}', 850 | 851 | 'vec3 getAcceleration( in float age ) {', 852 | ' return acceleration.xyz * age;', 853 | '}', 854 | ].join( '\n' ), 855 | 856 | 857 | rotationFunctions: [ 858 | // Huge thanks to: 859 | // - http://www.neilmendoza.com/glsl-rotation-about-an-arbitrary-axis/ 860 | '#ifdef SHOULD_ROTATE_PARTICLES', 861 | ' mat4 getRotationMatrix( in vec3 axis, in float angle) {', 862 | ' axis = normalize(axis);', 863 | ' float s = sin(angle);', 864 | ' float c = cos(angle);', 865 | ' float oc = 1.0 - c;', 866 | '', 867 | ' return mat4(oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0,', 868 | ' oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0,', 869 | ' oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0,', 870 | ' 0.0, 0.0, 0.0, 1.0);', 871 | ' }', 872 | '', 873 | ' vec3 getRotation( in vec3 pos, in float positionInTime ) {', 874 | ' if( rotation.y == 0.0 ) {', 875 | ' return pos;', 876 | ' }', 877 | '', 878 | ' vec3 axis = unpackRotationAxis( rotation.x );', 879 | ' vec3 center = rotationCenter;', 880 | ' vec3 translated;', 881 | ' mat4 rotationMatrix;', 882 | 883 | ' float angle = 0.0;', 884 | ' angle += when_eq( rotation.z, 0.0 ) * rotation.y;', 885 | ' angle += when_gt( rotation.z, 0.0 ) * mix( 0.0, rotation.y, positionInTime );', 886 | ' translated = rotationCenter - pos;', 887 | ' rotationMatrix = getRotationMatrix( axis, angle );', 888 | ' return center - vec3( rotationMatrix * vec4( translated, 0.0 ) );', 889 | ' }', 890 | '#endif' 891 | ].join( '\n' ), 892 | 893 | 894 | // Fragment chunks 895 | rotateTexture: [ 896 | ' vec2 vUv = vec2( gl_PointCoord.x, 1.0 - gl_PointCoord.y );', 897 | '', 898 | ' #ifdef SHOULD_ROTATE_TEXTURE', 899 | ' float x = gl_PointCoord.x - 0.5;', 900 | ' float y = 1.0 - gl_PointCoord.y - 0.5;', 901 | ' float c = cos( -vAngle );', 902 | ' float s = sin( -vAngle );', 903 | 904 | ' vUv = vec2( c * x + s * y + 0.5, c * y - s * x + 0.5 );', 905 | ' #endif', 906 | '', 907 | 908 | // Spritesheets overwrite angle calculations. 909 | ' #ifdef SHOULD_CALCULATE_SPRITE', 910 | ' float framesX = vSpriteSheet.x;', 911 | ' float framesY = vSpriteSheet.y;', 912 | ' float columnNorm = vSpriteSheet.z;', 913 | ' float rowNorm = vSpriteSheet.w;', 914 | 915 | ' vUv.x = gl_PointCoord.x * framesX + columnNorm;', 916 | ' vUv.y = 1.0 - (gl_PointCoord.y * framesY + rowNorm);', 917 | ' #endif', 918 | 919 | '', 920 | ' vec4 rotatedTexture = texture2D( tex, vUv );', 921 | ].join( '\n' ) 922 | }; 923 | 924 | SPE.shaders = { 925 | vertex: [ 926 | SPE.shaderChunks.defines, 927 | SPE.shaderChunks.uniforms, 928 | SPE.shaderChunks.attributes, 929 | SPE.shaderChunks.varyings, 930 | 931 | THREE.ShaderChunk.common, 932 | THREE.ShaderChunk.logdepthbuf_pars_vertex, 933 | THREE.ShaderChunk.fog_pars_vertex, 934 | 935 | SPE.shaderChunks.branchAvoidanceFunctions, 936 | SPE.shaderChunks.unpackColor, 937 | SPE.shaderChunks.unpackRotationAxis, 938 | SPE.shaderChunks.floatOverLifetime, 939 | SPE.shaderChunks.colorOverLifetime, 940 | SPE.shaderChunks.paramFetchingFunctions, 941 | SPE.shaderChunks.forceFetchingFunctions, 942 | SPE.shaderChunks.rotationFunctions, 943 | 944 | 945 | 'void main() {', 946 | 947 | 948 | // 949 | // Setup... 950 | // 951 | ' highp float age = getAge();', 952 | ' highp float alive = getAlive();', 953 | ' highp float maxAge = getMaxAge();', 954 | ' highp float positionInTime = (age / maxAge);', 955 | ' highp float isAlive = when_gt( alive, 0.0 );', 956 | 957 | ' #ifdef SHOULD_WIGGLE_PARTICLES', 958 | ' float wiggleAmount = positionInTime * getWiggle();', 959 | ' float wiggleSin = isAlive * sin( wiggleAmount );', 960 | ' float wiggleCos = isAlive * cos( wiggleAmount );', 961 | ' #endif', 962 | 963 | // 964 | // Forces 965 | // 966 | 967 | // Get forces & position 968 | ' vec3 vel = getVelocity( age );', 969 | ' vec3 accel = getAcceleration( age );', 970 | ' vec3 force = vec3( 0.0 );', 971 | ' vec3 pos = vec3( position );', 972 | 973 | // Calculate the required drag to apply to the forces. 974 | ' float drag = 1.0 - (positionInTime * 0.5) * acceleration.w;', 975 | 976 | // Integrate forces... 977 | ' force += vel;', 978 | ' force *= drag;', 979 | ' force += accel * age;', 980 | ' pos += force;', 981 | 982 | 983 | // Wiggly wiggly wiggle! 984 | ' #ifdef SHOULD_WIGGLE_PARTICLES', 985 | ' pos.x += wiggleSin;', 986 | ' pos.y += wiggleCos;', 987 | ' pos.z += wiggleSin;', 988 | ' #endif', 989 | 990 | 991 | // Rotate the emitter around it's central point 992 | ' #ifdef SHOULD_ROTATE_PARTICLES', 993 | ' pos = getRotation( pos, positionInTime );', 994 | ' #endif', 995 | 996 | // Convert pos to a world-space value 997 | ' vec4 mvPosition = modelViewMatrix * vec4( pos, 1.0 );', 998 | 999 | // Determine point size. 1000 | ' highp float pointSize = getFloatOverLifetime( positionInTime, size ) * isAlive;', 1001 | 1002 | // Determine perspective 1003 | ' #ifdef HAS_PERSPECTIVE', 1004 | ' float perspective = scale / length( mvPosition.xyz );', 1005 | ' #else', 1006 | ' float perspective = 1.0;', 1007 | ' #endif', 1008 | 1009 | // Apply perpective to pointSize value 1010 | ' float pointSizePerspective = pointSize * perspective;', 1011 | 1012 | 1013 | // 1014 | // Appearance 1015 | // 1016 | 1017 | // Determine color and opacity for this particle 1018 | ' #ifdef COLORIZE', 1019 | ' vec3 c = isAlive * getColorOverLifetime(', 1020 | ' positionInTime,', 1021 | ' unpackColor( color.x ),', 1022 | ' unpackColor( color.y ),', 1023 | ' unpackColor( color.z ),', 1024 | ' unpackColor( color.w )', 1025 | ' );', 1026 | ' #else', 1027 | ' vec3 c = vec3(1.0);', 1028 | ' #endif', 1029 | 1030 | ' float o = isAlive * getFloatOverLifetime( positionInTime, opacity );', 1031 | 1032 | // Assign color to vColor varying. 1033 | ' vColor = vec4( c, o );', 1034 | 1035 | // Determine angle 1036 | ' #ifdef SHOULD_ROTATE_TEXTURE', 1037 | ' vAngle = isAlive * getFloatOverLifetime( positionInTime, angle );', 1038 | ' #endif', 1039 | 1040 | // If this particle is using a sprite-sheet as a texture, we'll have to figure out 1041 | // what frame of the texture the particle is using at it's current position in time. 1042 | ' #ifdef SHOULD_CALCULATE_SPRITE', 1043 | ' float framesX = textureAnimation.x;', 1044 | ' float framesY = textureAnimation.y;', 1045 | ' float loopCount = textureAnimation.w;', 1046 | ' float totalFrames = textureAnimation.z;', 1047 | ' float frameNumber = mod( (positionInTime * loopCount) * totalFrames, totalFrames );', 1048 | 1049 | ' float column = floor(mod( frameNumber, framesX ));', 1050 | ' float row = floor( (frameNumber - column) / framesX );', 1051 | 1052 | ' float columnNorm = column / framesX;', 1053 | ' float rowNorm = row / framesY;', 1054 | 1055 | ' vSpriteSheet.x = 1.0 / framesX;', 1056 | ' vSpriteSheet.y = 1.0 / framesY;', 1057 | ' vSpriteSheet.z = columnNorm;', 1058 | ' vSpriteSheet.w = rowNorm;', 1059 | ' #endif', 1060 | 1061 | // 1062 | // Write values 1063 | // 1064 | 1065 | // Set PointSize according to size at current point in time. 1066 | ' gl_PointSize = pointSizePerspective;', 1067 | ' gl_Position = projectionMatrix * mvPosition;', 1068 | 1069 | THREE.ShaderChunk.logdepthbuf_vertex, 1070 | THREE.ShaderChunk.fog_vertex, 1071 | 1072 | '}' 1073 | ].join( '\n' ), 1074 | 1075 | fragment: [ 1076 | SPE.shaderChunks.uniforms, 1077 | 1078 | THREE.ShaderChunk.common, 1079 | THREE.ShaderChunk.fog_pars_fragment, 1080 | THREE.ShaderChunk.logdepthbuf_pars_fragment, 1081 | 1082 | SPE.shaderChunks.varyings, 1083 | 1084 | SPE.shaderChunks.branchAvoidanceFunctions, 1085 | 1086 | 'void main() {', 1087 | ' vec3 outgoingLight = vColor.xyz;', 1088 | ' ', 1089 | ' #ifdef ALPHATEST', 1090 | ' if ( vColor.w < float(ALPHATEST) ) discard;', 1091 | ' #endif', 1092 | 1093 | SPE.shaderChunks.rotateTexture, 1094 | 1095 | THREE.ShaderChunk.logdepthbuf_fragment, 1096 | 1097 | ' outgoingLight = vColor.xyz * rotatedTexture.xyz;', 1098 | ' gl_FragColor = vec4( outgoingLight.xyz, rotatedTexture.w * vColor.w );', 1099 | 1100 | THREE.ShaderChunk.fog_fragment, 1101 | 1102 | '}' 1103 | ].join( '\n' ) 1104 | }; 1105 | 1106 | 1107 | /** 1108 | * A bunch of utility functions used throughout the library. 1109 | * @namespace 1110 | * @type {Object} 1111 | */ 1112 | SPE.utils = { 1113 | /** 1114 | * A map of types used by `SPE.utils.ensureTypedArg` and 1115 | * `SPE.utils.ensureArrayTypedArg` to compare types against. 1116 | * 1117 | * @enum {String} 1118 | */ 1119 | types: { 1120 | /** 1121 | * Boolean type. 1122 | * @type {String} 1123 | */ 1124 | BOOLEAN: 'boolean', 1125 | 1126 | /** 1127 | * String type. 1128 | * @type {String} 1129 | */ 1130 | STRING: 'string', 1131 | 1132 | /** 1133 | * Number type. 1134 | * @type {String} 1135 | */ 1136 | NUMBER: 'number', 1137 | 1138 | /** 1139 | * Object type. 1140 | * @type {String} 1141 | */ 1142 | OBJECT: 'object' 1143 | }, 1144 | 1145 | /** 1146 | * Given a value, a type, and a default value to fallback to, 1147 | * ensure the given argument adheres to the type requesting, 1148 | * returning the default value if type check is false. 1149 | * 1150 | * @param {(boolean|string|number|object)} arg The value to perform a type-check on. 1151 | * @param {String} type The type the `arg` argument should adhere to. 1152 | * @param {(boolean|string|number|object)} defaultValue A default value to fallback on if the type check fails. 1153 | * @return {(boolean|string|number|object)} The given value if type check passes, or the default value if it fails. 1154 | */ 1155 | ensureTypedArg: function( arg, type, defaultValue ) { 1156 | 'use strict'; 1157 | 1158 | if ( typeof arg === type ) { 1159 | return arg; 1160 | } 1161 | else { 1162 | return defaultValue; 1163 | } 1164 | }, 1165 | 1166 | /** 1167 | * Given an array of values, a type, and a default value, 1168 | * ensure the given array's contents ALL adhere to the provided type, 1169 | * returning the default value if type check fails. 1170 | * 1171 | * If the given value to check isn't an Array, delegates to SPE.utils.ensureTypedArg. 1172 | * 1173 | * @param {Array|boolean|string|number|object} arg The array of values to check type of. 1174 | * @param {String} type The type that should be adhered to. 1175 | * @param {(boolean|string|number|object)} defaultValue A default fallback value. 1176 | * @return {(boolean|string|number|object)} The given value if type check passes, or the default value if it fails. 1177 | */ 1178 | ensureArrayTypedArg: function( arg, type, defaultValue ) { 1179 | 'use strict'; 1180 | 1181 | // If the argument being checked is an array, loop through 1182 | // it and ensure all the values are of the correct type, 1183 | // falling back to the defaultValue if any aren't. 1184 | if ( Array.isArray( arg ) ) { 1185 | for ( var i = arg.length - 1; i >= 0; --i ) { 1186 | if ( typeof arg[ i ] !== type ) { 1187 | return defaultValue; 1188 | } 1189 | } 1190 | 1191 | return arg; 1192 | } 1193 | 1194 | // If the arg isn't an array then just fallback to 1195 | // checking the type. 1196 | return this.ensureTypedArg( arg, type, defaultValue ); 1197 | }, 1198 | 1199 | /** 1200 | * Ensures the given value is an instance of a constructor function. 1201 | * 1202 | * @param {Object} arg The value to check instance of. 1203 | * @param {Function} instance The constructor of the instance to check against. 1204 | * @param {Object} defaultValue A default fallback value if instance check fails 1205 | * @return {Object} The given value if type check passes, or the default value if it fails. 1206 | */ 1207 | ensureInstanceOf: function( arg, instance, defaultValue ) { 1208 | 'use strict'; 1209 | 1210 | if ( instance !== undefined && arg instanceof instance ) { 1211 | return arg; 1212 | } 1213 | else { 1214 | return defaultValue; 1215 | } 1216 | }, 1217 | 1218 | /** 1219 | * Given an array of values, ensure the instances of all items in the array 1220 | * matches the given instance constructor falling back to a default value if 1221 | * the check fails. 1222 | * 1223 | * If given value isn't an Array, delegates to `SPE.utils.ensureInstanceOf`. 1224 | * 1225 | * @param {Array|Object} arg The value to perform the instanceof check on. 1226 | * @param {Function} instance The constructor of the instance to check against. 1227 | * @param {Object} defaultValue A default fallback value if instance check fails 1228 | * @return {Object} The given value if type check passes, or the default value if it fails. 1229 | */ 1230 | ensureArrayInstanceOf: function( arg, instance, defaultValue ) { 1231 | 'use strict'; 1232 | 1233 | // If the argument being checked is an array, loop through 1234 | // it and ensure all the values are of the correct type, 1235 | // falling back to the defaultValue if any aren't. 1236 | if ( Array.isArray( arg ) ) { 1237 | for ( var i = arg.length - 1; i >= 0; --i ) { 1238 | if ( instance !== undefined && arg[ i ] instanceof instance === false ) { 1239 | return defaultValue; 1240 | } 1241 | } 1242 | 1243 | return arg; 1244 | } 1245 | 1246 | // If the arg isn't an array then just fallback to 1247 | // checking the type. 1248 | return this.ensureInstanceOf( arg, instance, defaultValue ); 1249 | }, 1250 | 1251 | /** 1252 | * Ensures that any "value-over-lifetime" properties of an emitter are 1253 | * of the correct length (as dictated by `SPE.valueOverLifetimeLength`). 1254 | * 1255 | * Delegates to `SPE.utils.interpolateArray` for array resizing. 1256 | * 1257 | * If properties aren't arrays, then property values are put into one. 1258 | * 1259 | * @param {Object} property The property of an SPE.Emitter instance to check compliance of. 1260 | * @param {Number} minLength The minimum length of the array to create. 1261 | * @param {Number} maxLength The maximum length of the array to create. 1262 | */ 1263 | ensureValueOverLifetimeCompliance: function( property, minLength, maxLength ) { 1264 | 'use strict'; 1265 | 1266 | minLength = minLength || 3; 1267 | maxLength = maxLength || 3; 1268 | 1269 | // First, ensure both properties are arrays. 1270 | if ( Array.isArray( property._value ) === false ) { 1271 | property._value = [ property._value ]; 1272 | } 1273 | 1274 | if ( Array.isArray( property._spread ) === false ) { 1275 | property._spread = [ property._spread ]; 1276 | } 1277 | 1278 | var valueLength = this.clamp( property._value.length, minLength, maxLength ), 1279 | spreadLength = this.clamp( property._spread.length, minLength, maxLength ), 1280 | desiredLength = Math.max( valueLength, spreadLength ); 1281 | 1282 | if ( property._value.length !== desiredLength ) { 1283 | property._value = this.interpolateArray( property._value, desiredLength ); 1284 | } 1285 | 1286 | if ( property._spread.length !== desiredLength ) { 1287 | property._spread = this.interpolateArray( property._spread, desiredLength ); 1288 | } 1289 | }, 1290 | 1291 | /** 1292 | * Performs linear interpolation (lerp) on an array. 1293 | * 1294 | * For example, lerping [1, 10], with a `newLength` of 10 will produce [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]. 1295 | * 1296 | * Delegates to `SPE.utils.lerpTypeAgnostic` to perform the actual 1297 | * interpolation. 1298 | * 1299 | * @param {Array} srcArray The array to lerp. 1300 | * @param {Number} newLength The length the array should be interpolated to. 1301 | * @return {Array} The interpolated array. 1302 | */ 1303 | interpolateArray: function( srcArray, newLength ) { 1304 | 'use strict'; 1305 | 1306 | var sourceLength = srcArray.length, 1307 | newArray = [ typeof srcArray[ 0 ]?.clone === 'function' ? srcArray[ 0 ].clone() : srcArray[ 0 ] ], 1308 | factor = ( sourceLength - 1 ) / ( newLength - 1 ); 1309 | 1310 | 1311 | for ( var i = 1; i < newLength - 1; ++i ) { 1312 | var f = i * factor, 1313 | before = Math.floor( f ), 1314 | after = Math.ceil( f ), 1315 | delta = f - before; 1316 | 1317 | newArray[ i ] = this.lerpTypeAgnostic( srcArray[ before ], srcArray[ after ], delta ); 1318 | } 1319 | 1320 | newArray.push( 1321 | typeof srcArray[ sourceLength - 1 ]?.clone === 'function' ? 1322 | srcArray[ sourceLength - 1 ].clone() : 1323 | srcArray[ sourceLength - 1 ] 1324 | ); 1325 | 1326 | return newArray; 1327 | }, 1328 | 1329 | /** 1330 | * Clamp a number to between the given min and max values. 1331 | * @param {Number} value The number to clamp. 1332 | * @param {Number} min The minimum value. 1333 | * @param {Number} max The maximum value. 1334 | * @return {Number} The clamped number. 1335 | */ 1336 | clamp: function( value, min, max ) { 1337 | 'use strict'; 1338 | 1339 | return Math.max( min, Math.min( value, max ) ); 1340 | }, 1341 | 1342 | /** 1343 | * If the given value is less than the epsilon value, then return 1344 | * a randomised epsilon value if specified, or just the epsilon value if not. 1345 | * Works for negative numbers as well as positive. 1346 | * 1347 | * @param {Number} value The value to perform the operation on. 1348 | * @param {Boolean} randomise Whether the value should be randomised. 1349 | * @return {Number} The result of the operation. 1350 | */ 1351 | zeroToEpsilon: function( value, randomise ) { 1352 | 'use strict'; 1353 | 1354 | var epsilon = 0.00001, 1355 | result = value; 1356 | 1357 | result = randomise ? Math.random() * epsilon * 10 : epsilon; 1358 | 1359 | if ( value < 0 && value > -epsilon ) { 1360 | result = -result; 1361 | } 1362 | 1363 | // if ( value === 0 ) { 1364 | // result = randomise ? Math.random() * epsilon * 10 : epsilon; 1365 | // } 1366 | // else if ( value > 0 && value < epsilon ) { 1367 | // result = randomise ? Math.random() * epsilon * 10 : epsilon; 1368 | // } 1369 | // else if ( value < 0 && value > -epsilon ) { 1370 | // result = -( randomise ? Math.random() * epsilon * 10 : epsilon ); 1371 | // } 1372 | 1373 | return result; 1374 | }, 1375 | 1376 | /** 1377 | * Linearly interpolates two values of various types. The given values 1378 | * must be of the same type for the interpolation to work. 1379 | * @param {(number|Object)} start The start value of the lerp. 1380 | * @param {(number|object)} end The end value of the lerp. 1381 | * @param {Number} delta The delta posiiton of the lerp operation. Ideally between 0 and 1 (inclusive). 1382 | * @return {(number|object|undefined)} The result of the operation. Result will be undefined if 1383 | * the start and end arguments aren't a supported type, or 1384 | * if their types do not match. 1385 | */ 1386 | lerpTypeAgnostic: function( start, end, delta ) { 1387 | 'use strict'; 1388 | 1389 | var types = this.types, 1390 | out; 1391 | 1392 | if ( typeof start === types.NUMBER && typeof end === types.NUMBER ) { 1393 | return start + ( ( end - start ) * delta ); 1394 | } 1395 | else if ( start instanceof THREE.Vector2 && end instanceof THREE.Vector2 ) { 1396 | out = start.clone(); 1397 | out.x = this.lerp( start.x, end.x, delta ); 1398 | out.y = this.lerp( start.y, end.y, delta ); 1399 | return out; 1400 | } 1401 | else if ( start instanceof THREE.Vector3 && end instanceof THREE.Vector3 ) { 1402 | out = start.clone(); 1403 | out.x = this.lerp( start.x, end.x, delta ); 1404 | out.y = this.lerp( start.y, end.y, delta ); 1405 | out.z = this.lerp( start.z, end.z, delta ); 1406 | return out; 1407 | } 1408 | else if ( start instanceof THREE.Vector4 && end instanceof THREE.Vector4 ) { 1409 | out = start.clone(); 1410 | out.x = this.lerp( start.x, end.x, delta ); 1411 | out.y = this.lerp( start.y, end.y, delta ); 1412 | out.z = this.lerp( start.z, end.z, delta ); 1413 | out.w = this.lerp( start.w, end.w, delta ); 1414 | return out; 1415 | } 1416 | else if ( start instanceof THREE.Color && end instanceof THREE.Color ) { 1417 | out = start.clone(); 1418 | out.r = this.lerp( start.r, end.r, delta ); 1419 | out.g = this.lerp( start.g, end.g, delta ); 1420 | out.b = this.lerp( start.b, end.b, delta ); 1421 | return out; 1422 | } 1423 | else { 1424 | console.warn( 'Invalid argument types, or argument types do not match:', start, end ); 1425 | } 1426 | }, 1427 | 1428 | /** 1429 | * Perform a linear interpolation operation on two numbers. 1430 | * @param {Number} start The start value. 1431 | * @param {Number} end The end value. 1432 | * @param {Number} delta The position to interpolate to. 1433 | * @return {Number} The result of the lerp operation. 1434 | */ 1435 | lerp: function( start, end, delta ) { 1436 | 'use strict'; 1437 | return start + ( ( end - start ) * delta ); 1438 | }, 1439 | 1440 | /** 1441 | * Rounds a number to a nearest multiple. 1442 | * 1443 | * @param {Number} n The number to round. 1444 | * @param {Number} multiple The multiple to round to. 1445 | * @return {Number} The result of the round operation. 1446 | */ 1447 | roundToNearestMultiple: function( n, multiple ) { 1448 | 'use strict'; 1449 | 1450 | var remainder = 0; 1451 | 1452 | if ( multiple === 0 ) { 1453 | return n; 1454 | } 1455 | 1456 | remainder = Math.abs( n ) % multiple; 1457 | 1458 | if ( remainder === 0 ) { 1459 | return n; 1460 | } 1461 | 1462 | if ( n < 0 ) { 1463 | return -( Math.abs( n ) - remainder ); 1464 | } 1465 | 1466 | return n + multiple - remainder; 1467 | }, 1468 | 1469 | /** 1470 | * Check if all items in an array are equal. Uses strict equality. 1471 | * 1472 | * @param {Array} array The array of values to check equality of. 1473 | * @return {Boolean} Whether the array's values are all equal or not. 1474 | */ 1475 | arrayValuesAreEqual: function( array ) { 1476 | 'use strict'; 1477 | 1478 | for ( var i = 0; i < array.length - 1; ++i ) { 1479 | if ( array[ i ] !== array[ i + 1 ] ) { 1480 | return false; 1481 | } 1482 | } 1483 | 1484 | return true; 1485 | }, 1486 | 1487 | // colorsAreEqual: function() { 1488 | // var colors = Array.prototype.slice.call( arguments ), 1489 | // numColors = colors.length; 1490 | 1491 | // for ( var i = 0, color1, color2; i < numColors - 1; ++i ) { 1492 | // color1 = colors[ i ]; 1493 | // color2 = colors[ i + 1 ]; 1494 | 1495 | // if ( 1496 | // color1.r !== color2.r || 1497 | // color1.g !== color2.g || 1498 | // color1.b !== color2.b 1499 | // ) { 1500 | // return false 1501 | // } 1502 | // } 1503 | 1504 | // return true; 1505 | // }, 1506 | 1507 | 1508 | /** 1509 | * Given a start value and a spread value, create and return a random 1510 | * number. 1511 | * @param {Number} base The start value. 1512 | * @param {Number} spread The size of the random variance to apply. 1513 | * @return {Number} A randomised number. 1514 | */ 1515 | randomFloat: function( base, spread ) { 1516 | 'use strict'; 1517 | return base + spread * ( Math.random() - 0.5 ); 1518 | }, 1519 | 1520 | 1521 | 1522 | /** 1523 | * Given an SPE.ShaderAttribute instance, and various other settings, 1524 | * assign values to the attribute's array in a `vec3` format. 1525 | * 1526 | * @param {Object} attribute The instance of SPE.ShaderAttribute to save the result to. 1527 | * @param {Number} index The offset in the attribute's TypedArray to save the result from. 1528 | * @param {Object} base THREE.Vector3 instance describing the start value. 1529 | * @param {Object} spread THREE.Vector3 instance describing the random variance to apply to the start value. 1530 | * @param {Object} spreadClamp THREE.Vector3 instance describing the multiples to clamp the randomness to. 1531 | */ 1532 | randomVector3: function( attribute, index, base, spread, spreadClamp ) { 1533 | 'use strict'; 1534 | 1535 | var x = base.x + ( Math.random() * spread.x - ( spread.x * 0.5 ) ), 1536 | y = base.y + ( Math.random() * spread.y - ( spread.y * 0.5 ) ), 1537 | z = base.z + ( Math.random() * spread.z - ( spread.z * 0.5 ) ); 1538 | 1539 | // var x = this.randomFloat( base.x, spread.x ), 1540 | // y = this.randomFloat( base.y, spread.y ), 1541 | // z = this.randomFloat( base.z, spread.z ); 1542 | 1543 | if ( spreadClamp ) { 1544 | x = -spreadClamp.x * 0.5 + this.roundToNearestMultiple( x, spreadClamp.x ); 1545 | y = -spreadClamp.y * 0.5 + this.roundToNearestMultiple( y, spreadClamp.y ); 1546 | z = -spreadClamp.z * 0.5 + this.roundToNearestMultiple( z, spreadClamp.z ); 1547 | } 1548 | 1549 | attribute.typedArray.setVec3Components( index, x, y, z ); 1550 | }, 1551 | 1552 | /** 1553 | * Given an SPE.Shader attribute instance, and various other settings, 1554 | * assign Color values to the attribute. 1555 | * @param {Object} attribute The instance of SPE.ShaderAttribute to save the result to. 1556 | * @param {Number} index The offset in the attribute's TypedArray to save the result from. 1557 | * @param {Object} base THREE.Color instance describing the start color. 1558 | * @param {Object} spread THREE.Vector3 instance describing the random variance to apply to the start color. 1559 | */ 1560 | randomColor: function( attribute, index, base, spread ) { 1561 | 'use strict'; 1562 | 1563 | var r = base.r + ( Math.random() * spread.x ), 1564 | g = base.g + ( Math.random() * spread.y ), 1565 | b = base.b + ( Math.random() * spread.z ); 1566 | 1567 | r = this.clamp( r, 0, 1 ); 1568 | g = this.clamp( g, 0, 1 ); 1569 | b = this.clamp( b, 0, 1 ); 1570 | 1571 | 1572 | attribute.typedArray.setVec3Components( index, r, g, b ); 1573 | }, 1574 | 1575 | 1576 | randomColorAsHex: ( function() { 1577 | 'use strict'; 1578 | 1579 | var workingColor = new THREE.Color(); 1580 | 1581 | /** 1582 | * Assigns a random color value, encoded as a hex value in decimal 1583 | * format, to a SPE.ShaderAttribute instance. 1584 | * @param {Object} attribute The instance of SPE.ShaderAttribute to save the result to. 1585 | * @param {Number} index The offset in the attribute's TypedArray to save the result from. 1586 | * @param {Object} base THREE.Color instance describing the start color. 1587 | * @param {Object} spread THREE.Vector3 instance describing the random variance to apply to the start color. 1588 | */ 1589 | return function( attribute, index, base, spread ) { 1590 | var numItems = base.length, 1591 | colors = []; 1592 | 1593 | for ( var i = 0; i < numItems; ++i ) { 1594 | var spreadVector = spread[ i ]; 1595 | 1596 | workingColor.copy( base[ i ] ); 1597 | 1598 | workingColor.r += ( Math.random() * spreadVector.x ) - ( spreadVector.x * 0.5 ); 1599 | workingColor.g += ( Math.random() * spreadVector.y ) - ( spreadVector.y * 0.5 ); 1600 | workingColor.b += ( Math.random() * spreadVector.z ) - ( spreadVector.z * 0.5 ); 1601 | 1602 | workingColor.r = this.clamp( workingColor.r, 0, 1 ); 1603 | workingColor.g = this.clamp( workingColor.g, 0, 1 ); 1604 | workingColor.b = this.clamp( workingColor.b, 0, 1 ); 1605 | 1606 | colors.push( workingColor.getHex() ); 1607 | } 1608 | 1609 | attribute.typedArray.setVec4Components( index, colors[ 0 ], colors[ 1 ], colors[ 2 ], colors[ 3 ] ); 1610 | }; 1611 | }() ), 1612 | 1613 | /** 1614 | * Given an SPE.ShaderAttribute instance, and various other settings, 1615 | * assign values to the attribute's array in a `vec3` format. 1616 | * 1617 | * @param {Object} attribute The instance of SPE.ShaderAttribute to save the result to. 1618 | * @param {Number} index The offset in the attribute's TypedArray to save the result from. 1619 | * @param {Object} start THREE.Vector3 instance describing the start line position. 1620 | * @param {Object} end THREE.Vector3 instance describing the end line position. 1621 | */ 1622 | randomVector3OnLine: function( attribute, index, start, end ) { 1623 | 'use strict'; 1624 | var pos = start.clone(); 1625 | 1626 | pos.lerp( end, Math.random() ); 1627 | 1628 | attribute.typedArray.setVec3Components( index, pos.x, pos.y, pos.z ); 1629 | }, 1630 | 1631 | /** 1632 | * Given an SPE.Shader attribute instance, and various other settings, 1633 | * assign Color values to the attribute. 1634 | * @param {Object} attribute The instance of SPE.ShaderAttribute to save the result to. 1635 | * @param {Number} index The offset in the attribute's TypedArray to save the result from. 1636 | * @param {Object} base THREE.Color instance describing the start color. 1637 | * @param {Object} spread THREE.Vector3 instance describing the random variance to apply to the start color. 1638 | */ 1639 | 1640 | /** 1641 | * Assigns a random vector 3 value to an SPE.ShaderAttribute instance, projecting the 1642 | * given values onto a sphere. 1643 | * 1644 | * @param {Object} attribute The instance of SPE.ShaderAttribute to save the result to. 1645 | * @param {Number} index The offset in the attribute's TypedArray to save the result from. 1646 | * @param {Object} base THREE.Vector3 instance describing the origin of the transform. 1647 | * @param {Number} radius The radius of the sphere to project onto. 1648 | * @param {Number} radiusSpread The amount of randomness to apply to the projection result 1649 | * @param {Object} radiusScale THREE.Vector3 instance describing the scale of each axis of the sphere. 1650 | * @param {Number} radiusSpreadClamp What numeric multiple the projected value should be clamped to. 1651 | */ 1652 | randomVector3OnSphere: function( 1653 | attribute, index, base, radius, radiusSpread, radiusScale, radiusSpreadClamp, distributionClamp 1654 | ) { 1655 | 'use strict'; 1656 | 1657 | var depth = 2 * Math.random() - 1, 1658 | t = 6.2832 * Math.random(), 1659 | r = Math.sqrt( 1 - depth * depth ), 1660 | rand = this.randomFloat( radius, radiusSpread ), 1661 | x = 0, 1662 | y = 0, 1663 | z = 0; 1664 | 1665 | 1666 | if ( radiusSpreadClamp ) { 1667 | rand = Math.round( rand / radiusSpreadClamp ) * radiusSpreadClamp; 1668 | } 1669 | 1670 | 1671 | 1672 | // Set position on sphere 1673 | x = r * Math.cos( t ) * rand; 1674 | y = r * Math.sin( t ) * rand; 1675 | z = depth * rand; 1676 | 1677 | // Apply radius scale to this position 1678 | x *= radiusScale.x; 1679 | y *= radiusScale.y; 1680 | z *= radiusScale.z; 1681 | 1682 | // Translate to the base position. 1683 | x += base.x; 1684 | y += base.y; 1685 | z += base.z; 1686 | 1687 | // Set the values in the typed array. 1688 | attribute.typedArray.setVec3Components( index, x, y, z ); 1689 | }, 1690 | 1691 | seededRandom: function( seed ) { 1692 | var x = Math.sin( seed ) * 10000; 1693 | return x - ( x | 0 ); 1694 | }, 1695 | 1696 | 1697 | 1698 | /** 1699 | * Assigns a random vector 3 value to an SPE.ShaderAttribute instance, projecting the 1700 | * given values onto a 2d-disc. 1701 | * 1702 | * @param {Object} attribute The instance of SPE.ShaderAttribute to save the result to. 1703 | * @param {Number} index The offset in the attribute's TypedArray to save the result from. 1704 | * @param {Object} base THREE.Vector3 instance describing the origin of the transform. 1705 | * @param {Number} radius The radius of the sphere to project onto. 1706 | * @param {Number} radiusSpread The amount of randomness to apply to the projection result 1707 | * @param {Object} radiusScale THREE.Vector3 instance describing the scale of each axis of the disc. The z-component is ignored. 1708 | * @param {Number} radiusSpreadClamp What numeric multiple the projected value should be clamped to. 1709 | */ 1710 | randomVector3OnDisc: function( attribute, index, base, radius, radiusSpread, radiusScale, radiusSpreadClamp ) { 1711 | 'use strict'; 1712 | 1713 | var t = 6.2832 * Math.random(), 1714 | rand = Math.abs( this.randomFloat( radius, radiusSpread ) ), 1715 | x = 0, 1716 | y = 0, 1717 | z = 0; 1718 | 1719 | if ( radiusSpreadClamp ) { 1720 | rand = Math.round( rand / radiusSpreadClamp ) * radiusSpreadClamp; 1721 | } 1722 | 1723 | // Set position on sphere 1724 | x = Math.cos( t ) * rand; 1725 | y = Math.sin( t ) * rand; 1726 | 1727 | // Apply radius scale to this position 1728 | x *= radiusScale.x; 1729 | y *= radiusScale.y; 1730 | 1731 | // Translate to the base position. 1732 | x += base.x; 1733 | y += base.y; 1734 | z += base.z; 1735 | 1736 | // Set the values in the typed array. 1737 | attribute.typedArray.setVec3Components( index, x, y, z ); 1738 | }, 1739 | 1740 | randomDirectionVector3OnSphere: ( function() { 1741 | 'use strict'; 1742 | 1743 | var v = new THREE.Vector3(); 1744 | 1745 | /** 1746 | * Given an SPE.ShaderAttribute instance, create a direction vector from the given 1747 | * position, using `speed` as the magnitude. Values are saved to the attribute. 1748 | * 1749 | * @param {Object} attribute The instance of SPE.ShaderAttribute to save the result to. 1750 | * @param {Number} index The offset in the attribute's TypedArray to save the result from. 1751 | * @param {Number} posX The particle's x coordinate. 1752 | * @param {Number} posY The particle's y coordinate. 1753 | * @param {Number} posZ The particle's z coordinate. 1754 | * @param {Object} emitterPosition THREE.Vector3 instance describing the emitter's base position. 1755 | * @param {Number} speed The magnitude to apply to the vector. 1756 | * @param {Number} speedSpread The amount of randomness to apply to the magnitude. 1757 | */ 1758 | return function( attribute, index, posX, posY, posZ, emitterPosition, speed, speedSpread ) { 1759 | v.copy( emitterPosition ); 1760 | 1761 | v.x -= posX; 1762 | v.y -= posY; 1763 | v.z -= posZ; 1764 | 1765 | v.normalize().multiplyScalar( -this.randomFloat( speed, speedSpread ) ); 1766 | 1767 | attribute.typedArray.setVec3Components( index, v.x, v.y, v.z ); 1768 | }; 1769 | }() ), 1770 | 1771 | 1772 | randomDirectionVector3OnDisc: ( function() { 1773 | 'use strict'; 1774 | 1775 | var v = new THREE.Vector3(); 1776 | 1777 | /** 1778 | * Given an SPE.ShaderAttribute instance, create a direction vector from the given 1779 | * position, using `speed` as the magnitude. Values are saved to the attribute. 1780 | * 1781 | * @param {Object} attribute The instance of SPE.ShaderAttribute to save the result to. 1782 | * @param {Number} index The offset in the attribute's TypedArray to save the result from. 1783 | * @param {Number} posX The particle's x coordinate. 1784 | * @param {Number} posY The particle's y coordinate. 1785 | * @param {Number} posZ The particle's z coordinate. 1786 | * @param {Object} emitterPosition THREE.Vector3 instance describing the emitter's base position. 1787 | * @param {Number} speed The magnitude to apply to the vector. 1788 | * @param {Number} speedSpread The amount of randomness to apply to the magnitude. 1789 | */ 1790 | return function( attribute, index, posX, posY, posZ, emitterPosition, speed, speedSpread ) { 1791 | v.copy( emitterPosition ); 1792 | 1793 | v.x -= posX; 1794 | v.y -= posY; 1795 | v.z -= posZ; 1796 | 1797 | v.normalize().multiplyScalar( -this.randomFloat( speed, speedSpread ) ); 1798 | 1799 | attribute.typedArray.setVec3Components( index, v.x, v.y, 0 ); 1800 | }; 1801 | }() ), 1802 | 1803 | getPackedRotationAxis: ( function() { 1804 | 'use strict'; 1805 | 1806 | var v = new THREE.Vector3(), 1807 | vSpread = new THREE.Vector3(), 1808 | c = new THREE.Color(), 1809 | addOne = new THREE.Vector3( 1, 1, 1 ); 1810 | 1811 | /** 1812 | * Given a rotation axis, and a rotation axis spread vector, 1813 | * calculate a randomised rotation axis, and pack it into 1814 | * a hexadecimal value represented in decimal form. 1815 | * @param {Object} axis THREE.Vector3 instance describing the rotation axis. 1816 | * @param {Object} axisSpread THREE.Vector3 instance describing the amount of randomness to apply to the rotation axis. 1817 | * @return {Number} The packed rotation axis, with randomness. 1818 | */ 1819 | return function( axis, axisSpread ) { 1820 | v.copy( axis ).normalize(); 1821 | vSpread.copy( axisSpread ).normalize(); 1822 | 1823 | v.x += ( -axisSpread.x * 0.5 ) + ( Math.random() * axisSpread.x ); 1824 | v.y += ( -axisSpread.y * 0.5 ) + ( Math.random() * axisSpread.y ); 1825 | v.z += ( -axisSpread.z * 0.5 ) + ( Math.random() * axisSpread.z ); 1826 | 1827 | // v.x = Math.abs( v.x ); 1828 | // v.y = Math.abs( v.y ); 1829 | // v.z = Math.abs( v.z ); 1830 | 1831 | v.normalize().add( addOne ).multiplyScalar( 0.5 ); 1832 | 1833 | c.setRGB( v.x, v.y, v.z ); 1834 | 1835 | return c.getHex(); 1836 | }; 1837 | }() ) 1838 | }; 1839 | 1840 | 1841 | /** 1842 | * An SPE.Group instance. 1843 | * @typedef {Object} Group 1844 | * @see SPE.Group 1845 | */ 1846 | 1847 | /** 1848 | * A map of options to configure an SPE.Group instance. 1849 | * @typedef {Object} GroupOptions 1850 | * 1851 | * @property {Object} texture An object describing the texture used by the group. 1852 | * 1853 | * @property {Object} texture.value An instance of THREE.Texture. 1854 | * 1855 | * @property {Object=} texture.frames A THREE.Vector2 instance describing the number 1856 | * of frames on the x- and y-axis of the given texture. 1857 | * If not provided, the texture will NOT be treated as 1858 | * a sprite-sheet and as such will NOT be animated. 1859 | * 1860 | * @property {Number} [texture.frameCount=texture.frames.x * texture.frames.y] The total number of frames in the sprite-sheet. 1861 | * Allows for sprite-sheets that don't fill the entire 1862 | * texture. 1863 | * 1864 | * @property {Number} texture.loop The number of loops through the sprite-sheet that should 1865 | * be performed over the course of a single particle's lifetime. 1866 | * 1867 | * @property {Number} fixedTimeStep If no `dt` (or `deltaTime`) value is passed to this group's 1868 | * `tick()` function, this number will be used to move the particle 1869 | * simulation forward. Value in SECONDS. 1870 | * 1871 | * @property {Boolean} hasPerspective Whether the distance a particle is from the camera should affect 1872 | * the particle's size. 1873 | * 1874 | * @property {Boolean} colorize Whether the particles in this group should be rendered with color, or 1875 | * whether the only color of particles will come from the provided texture. 1876 | * 1877 | * @property {Number} blending One of Three.js's blending modes to apply to this group's `ShaderMaterial`. 1878 | * 1879 | * @property {Boolean} transparent Whether these particle's should be rendered with transparency. 1880 | * 1881 | * @property {Number} alphaTest Sets the alpha value to be used when running an alpha test on the `texture.value` property. Value between 0 and 1. 1882 | * 1883 | * @property {Boolean} depthWrite Whether rendering the group has any effect on the depth buffer. 1884 | * 1885 | * @property {Boolean} depthTest Whether to have depth test enabled when rendering this group. 1886 | * 1887 | * @property {Boolean} fog Whether this group's particles should be affected by their scene's fog. 1888 | * 1889 | * @property {Number} scale The scale factor to apply to this group's particle sizes. Useful for 1890 | * setting particle sizes to be relative to renderer size. 1891 | */ 1892 | 1893 | 1894 | /** 1895 | * The SPE.Group class. Creates a new group, containing a material, geometry, and mesh. 1896 | * 1897 | * @constructor 1898 | * @param {GroupOptions} options A map of options to configure the group instance. 1899 | */ 1900 | SPE.Group = function( options ) { 1901 | 'use strict'; 1902 | 1903 | var utils = SPE.utils, 1904 | types = utils.types; 1905 | 1906 | // Ensure we have a map of options to play with 1907 | options = utils.ensureTypedArg( options, types.OBJECT, {} ); 1908 | options.texture = utils.ensureTypedArg( options.texture, types.OBJECT, {} ); 1909 | 1910 | // Assign a UUID to this instance 1911 | this.uuid = THREE.MathUtils.generateUUID(); 1912 | 1913 | // If no `deltaTime` value is passed to the `SPE.Group.tick` function, 1914 | // the value of this property will be used to advance the simulation. 1915 | this.fixedTimeStep = utils.ensureTypedArg( options.fixedTimeStep, types.NUMBER, 0.016 ); 1916 | 1917 | // Set properties used in the uniforms map, starting with the 1918 | // texture stuff. 1919 | this.texture = utils.ensureInstanceOf( options.texture.value, THREE.Texture, null ); 1920 | this.textureFrames = utils.ensureInstanceOf( options.texture.frames, THREE.Vector2, new THREE.Vector2( 1, 1 ) ); 1921 | this.textureFrameCount = utils.ensureTypedArg( options.texture.frameCount, types.NUMBER, this.textureFrames.x * this.textureFrames.y ); 1922 | this.textureLoop = utils.ensureTypedArg( options.texture.loop, types.NUMBER, 1 ); 1923 | this.textureFrames.max( new THREE.Vector2( 1, 1 ) ); 1924 | 1925 | this.hasPerspective = utils.ensureTypedArg( options.hasPerspective, types.BOOLEAN, true ); 1926 | this.colorize = utils.ensureTypedArg( options.colorize, types.BOOLEAN, true ); 1927 | 1928 | this.maxParticleCount = utils.ensureTypedArg( options.maxParticleCount, types.NUMBER, null ); 1929 | 1930 | 1931 | // Set properties used to define the ShaderMaterial's appearance. 1932 | this.blending = utils.ensureTypedArg( options.blending, types.NUMBER, THREE.AdditiveBlending ); 1933 | this.transparent = utils.ensureTypedArg( options.transparent, types.BOOLEAN, true ); 1934 | this.alphaTest = parseFloat( utils.ensureTypedArg( options.alphaTest, types.NUMBER, 0.0 ) ); 1935 | this.depthWrite = utils.ensureTypedArg( options.depthWrite, types.BOOLEAN, false ); 1936 | this.depthTest = utils.ensureTypedArg( options.depthTest, types.BOOLEAN, true ); 1937 | this.fog = utils.ensureTypedArg( options.fog, types.BOOLEAN, true ); 1938 | this.scale = utils.ensureTypedArg( options.scale, types.NUMBER, 300 ); 1939 | 1940 | // Where emitter's go to curl up in a warm blanket and live 1941 | // out their days. 1942 | this.emitters = []; 1943 | this.emitterIDs = []; 1944 | 1945 | // Create properties for use by the emitter pooling functions. 1946 | this._pool = []; 1947 | this._poolCreationSettings = null; 1948 | this._createNewWhenPoolEmpty = 0; 1949 | 1950 | // Whether all attributes should be forced to updated 1951 | // their entire buffer contents on the next tick. 1952 | // 1953 | // Used when an emitter is removed. 1954 | this._attributesNeedRefresh = false; 1955 | this._attributesNeedDynamicReset = false; 1956 | 1957 | this.particleCount = 0; 1958 | 1959 | 1960 | // Map of uniforms to be applied to the ShaderMaterial instance. 1961 | this.uniforms = { 1962 | tex: { 1963 | type: 't', 1964 | value: this.texture 1965 | }, 1966 | textureAnimation: { 1967 | type: 'v4', 1968 | value: new THREE.Vector4( 1969 | this.textureFrames.x, 1970 | this.textureFrames.y, 1971 | this.textureFrameCount, 1972 | Math.max( Math.abs( this.textureLoop ), 1.0 ) 1973 | ) 1974 | }, 1975 | fogColor: { 1976 | type: 'c', 1977 | value: this.fog ? new THREE.Color() : null 1978 | }, 1979 | fogNear: { 1980 | type: 'f', 1981 | value: 10 1982 | }, 1983 | fogFar: { 1984 | type: 'f', 1985 | value: 200 1986 | }, 1987 | fogDensity: { 1988 | type: 'f', 1989 | value: 0.5 1990 | }, 1991 | deltaTime: { 1992 | type: 'f', 1993 | value: 0 1994 | }, 1995 | runTime: { 1996 | type: 'f', 1997 | value: 0 1998 | }, 1999 | scale: { 2000 | type: 'f', 2001 | value: this.scale 2002 | } 2003 | }; 2004 | 2005 | // Add some defines into the mix... 2006 | this.defines = { 2007 | HAS_PERSPECTIVE: this.hasPerspective, 2008 | COLORIZE: this.colorize, 2009 | VALUE_OVER_LIFETIME_LENGTH: SPE.valueOverLifetimeLength, 2010 | 2011 | SHOULD_ROTATE_TEXTURE: false, 2012 | SHOULD_ROTATE_PARTICLES: false, 2013 | SHOULD_WIGGLE_PARTICLES: false, 2014 | 2015 | SHOULD_CALCULATE_SPRITE: this.textureFrames.x > 1 || this.textureFrames.y > 1 2016 | }; 2017 | 2018 | // Map of all attributes to be applied to the particles. 2019 | // 2020 | // See SPE.ShaderAttribute for a bit more info on this bit. 2021 | this.attributes = { 2022 | position: new SPE.ShaderAttribute( 'v3', true ), 2023 | acceleration: new SPE.ShaderAttribute( 'v4', true ), // w component is drag 2024 | velocity: new SPE.ShaderAttribute( 'v3', true ), 2025 | rotation: new SPE.ShaderAttribute( 'v4', true ), 2026 | rotationCenter: new SPE.ShaderAttribute( 'v3', true ), 2027 | params: new SPE.ShaderAttribute( 'v4', true ), // Holds (alive, age, delay, wiggle) 2028 | size: new SPE.ShaderAttribute( 'v4', true ), 2029 | angle: new SPE.ShaderAttribute( 'v4', true ), 2030 | color: new SPE.ShaderAttribute( 'v4', true ), 2031 | opacity: new SPE.ShaderAttribute( 'v4', true ) 2032 | }; 2033 | 2034 | this.attributeKeys = Object.keys( this.attributes ); 2035 | this.attributeCount = this.attributeKeys.length; 2036 | 2037 | // Create the ShaderMaterial instance that'll help render the 2038 | // particles. 2039 | this.material = new THREE.ShaderMaterial( { 2040 | uniforms: this.uniforms, 2041 | vertexShader: SPE.shaders.vertex, 2042 | fragmentShader: SPE.shaders.fragment, 2043 | blending: this.blending, 2044 | transparent: this.transparent, 2045 | alphaTest: this.alphaTest, 2046 | depthWrite: this.depthWrite, 2047 | depthTest: this.depthTest, 2048 | defines: this.defines, 2049 | fog: this.fog 2050 | } ); 2051 | 2052 | // Create the BufferGeometry and Points instances, ensuring 2053 | // the geometry and material are given to the latter. 2054 | this.geometry = new THREE.BufferGeometry(); 2055 | this.mesh = new THREE.Points( this.geometry, this.material ); 2056 | 2057 | if ( this.maxParticleCount === null ) { 2058 | console.warn( 'SPE.Group: No maxParticleCount specified. Adding emitters after rendering will probably cause errors.' ); 2059 | } 2060 | }; 2061 | 2062 | SPE.Group.constructor = SPE.Group; 2063 | 2064 | 2065 | SPE.Group.prototype._updateDefines = function() { 2066 | 'use strict'; 2067 | 2068 | var emitters = this.emitters, 2069 | i = emitters.length - 1, 2070 | emitter, 2071 | defines = this.defines; 2072 | 2073 | for ( i; i >= 0; --i ) { 2074 | emitter = emitters[ i ]; 2075 | 2076 | // Only do angle calculation if there's no spritesheet defined. 2077 | // 2078 | // Saves calculations being done and then overwritten in the shaders. 2079 | if ( !defines.SHOULD_CALCULATE_SPRITE ) { 2080 | defines.SHOULD_ROTATE_TEXTURE = defines.SHOULD_ROTATE_TEXTURE || !!Math.max( 2081 | Math.max.apply( null, emitter.angle.value ), 2082 | Math.max.apply( null, emitter.angle.spread ) 2083 | ); 2084 | } 2085 | 2086 | defines.SHOULD_ROTATE_PARTICLES = defines.SHOULD_ROTATE_PARTICLES || !!Math.max( 2087 | emitter.rotation.angle, 2088 | emitter.rotation.angleSpread 2089 | ); 2090 | 2091 | defines.SHOULD_WIGGLE_PARTICLES = defines.SHOULD_WIGGLE_PARTICLES || !!Math.max( 2092 | emitter.wiggle.value, 2093 | emitter.wiggle.spread 2094 | ); 2095 | } 2096 | 2097 | this.material.needsUpdate = true; 2098 | }; 2099 | 2100 | SPE.Group.prototype._applyAttributesToGeometry = function() { 2101 | 'use strict'; 2102 | 2103 | var attributes = this.attributes, 2104 | geometry = this.geometry, 2105 | geometryAttributes = geometry.attributes, 2106 | attribute, 2107 | geometryAttribute; 2108 | 2109 | // Loop through all the shader attributes and assign (or re-assign) 2110 | // typed array buffers to each one. 2111 | for ( var attr in attributes ) { 2112 | if ( attributes.hasOwnProperty( attr ) ) { 2113 | attribute = attributes[ attr ]; 2114 | geometryAttribute = geometryAttributes[ attr ]; 2115 | 2116 | // Update the array if this attribute exists on the geometry. 2117 | // 2118 | // This needs to be done because the attribute's typed array might have 2119 | // been resized and reinstantiated, and might now be looking at a 2120 | // different ArrayBuffer, so reference needs updating. 2121 | if ( geometryAttribute ) { 2122 | geometryAttribute.array = attribute.typedArray.array; 2123 | } 2124 | 2125 | // // Add the attribute to the geometry if it doesn't already exist. 2126 | else { 2127 | geometry.setAttribute( attr, attribute.bufferAttribute ); 2128 | } 2129 | 2130 | // Mark the attribute as needing an update the next time a frame is rendered. 2131 | attribute.bufferAttribute.needsUpdate = true; 2132 | } 2133 | } 2134 | 2135 | // Mark the draw range on the geometry. This will ensure 2136 | // only the values in the attribute buffers that are 2137 | // associated with a particle will be used in THREE's 2138 | // render cycle. 2139 | this.geometry.setDrawRange( 0, this.particleCount ); 2140 | }; 2141 | 2142 | /** 2143 | * Adds an SPE.Emitter instance to this group, creating particle values and 2144 | * assigning them to this group's shader attributes. 2145 | * 2146 | * @param {Emitter} emitter The emitter to add to this group. 2147 | */ 2148 | SPE.Group.prototype.addEmitter = function( emitter ) { 2149 | 'use strict'; 2150 | 2151 | // Ensure an actual emitter instance is passed here. 2152 | // 2153 | // Decided not to throw here, just in case a scene's 2154 | // rendering would be paused. Logging an error instead 2155 | // of stopping execution if exceptions aren't caught. 2156 | if ( emitter instanceof SPE.Emitter === false ) { 2157 | console.error( '`emitter` argument must be instance of SPE.Emitter. Was provided with:', emitter ); 2158 | return; 2159 | } 2160 | 2161 | // If the emitter already exists as a member of this group, then 2162 | // stop here, we don't want to add it again. 2163 | else if ( this.emitterIDs.indexOf( emitter.uuid ) > -1 ) { 2164 | console.error( 'Emitter already exists in this group. Will not add again.' ); 2165 | return; 2166 | } 2167 | 2168 | // And finally, if the emitter is a member of another group, 2169 | // don't add it to this group. 2170 | else if ( emitter.group !== null ) { 2171 | console.error( 'Emitter already belongs to another group. Will not add to requested group.' ); 2172 | return; 2173 | } 2174 | 2175 | var attributes = this.attributes, 2176 | start = this.particleCount, 2177 | end = start + emitter.particleCount; 2178 | 2179 | // Update this group's particle count. 2180 | this.particleCount = end; 2181 | 2182 | // Emit a warning if the emitter being added will exceed the buffer sizes specified. 2183 | if ( this.maxParticleCount !== null && this.particleCount > this.maxParticleCount ) { 2184 | console.warn( 'SPE.Group: maxParticleCount exceeded. Requesting', this.particleCount, 'particles, can support only', this.maxParticleCount ); 2185 | } 2186 | 2187 | 2188 | // Set the `particlesPerSecond` value (PPS) on the emitter. 2189 | // It's used to determine how many particles to release 2190 | // on a per-frame basis. 2191 | emitter._calculatePPSValue( emitter.maxAge._value + emitter.maxAge._spread ); 2192 | emitter._setBufferUpdateRanges( this.attributeKeys ); 2193 | 2194 | // Store the offset value in the TypedArray attributes for this emitter. 2195 | emitter._setAttributeOffset( start ); 2196 | 2197 | // Save a reference to this group on the emitter so it knows 2198 | // where it belongs. 2199 | emitter.group = this; 2200 | 2201 | // Store reference to the attributes on the emitter for 2202 | // easier access during the emitter's tick function. 2203 | emitter.attributes = this.attributes; 2204 | 2205 | 2206 | 2207 | // Ensure the attributes and their BufferAttributes exist, and their 2208 | // TypedArrays are of the correct size. 2209 | for ( var attr in attributes ) { 2210 | if ( attributes.hasOwnProperty( attr ) ) { 2211 | // When creating a buffer, pass through the maxParticle count 2212 | // if one is specified. 2213 | attributes[ attr ]._createBufferAttribute( 2214 | this.maxParticleCount !== null ? 2215 | this.maxParticleCount : 2216 | this.particleCount 2217 | ); 2218 | } 2219 | } 2220 | 2221 | // Loop through each particle this emitter wants to have, and create the attributes values, 2222 | // storing them in the TypedArrays that each attribute holds. 2223 | for ( var i = start; i < end; ++i ) { 2224 | emitter._assignPositionValue( i ); 2225 | emitter._assignForceValue( i, 'velocity' ); 2226 | emitter._assignForceValue( i, 'acceleration' ); 2227 | emitter._assignAbsLifetimeValue( i, 'opacity' ); 2228 | emitter._assignAbsLifetimeValue( i, 'size' ); 2229 | emitter._assignAngleValue( i ); 2230 | emitter._assignRotationValue( i ); 2231 | emitter._assignParamsValue( i ); 2232 | emitter._assignColorValue( i ); 2233 | } 2234 | 2235 | // Update the geometry and make sure the attributes are referencing 2236 | // the typed arrays properly. 2237 | this._applyAttributesToGeometry(); 2238 | 2239 | // Store this emitter in this group's emitter's store. 2240 | this.emitters.push( emitter ); 2241 | this.emitterIDs.push( emitter.uuid ); 2242 | 2243 | // Update certain flags to enable shader calculations only if they're necessary. 2244 | this._updateDefines( emitter ); 2245 | 2246 | // Update the material since defines might have changed 2247 | this.material.needsUpdate = true; 2248 | this.geometry.needsUpdate = true; 2249 | this._attributesNeedRefresh = true; 2250 | 2251 | // Return the group to enable chaining. 2252 | return this; 2253 | }; 2254 | 2255 | /** 2256 | * Removes an SPE.Emitter instance from this group. When called, 2257 | * all particle's belonging to the given emitter will be instantly 2258 | * removed from the scene. 2259 | * 2260 | * @param {Emitter} emitter The emitter to add to this group. 2261 | */ 2262 | SPE.Group.prototype.removeEmitter = function( emitter ) { 2263 | 'use strict'; 2264 | 2265 | var emitterIndex = this.emitterIDs.indexOf( emitter.uuid ); 2266 | 2267 | // Ensure an actual emitter instance is passed here. 2268 | // 2269 | // Decided not to throw here, just in case a scene's 2270 | // rendering would be paused. Logging an error instead 2271 | // of stopping execution if exceptions aren't caught. 2272 | if ( emitter instanceof SPE.Emitter === false ) { 2273 | console.error( '`emitter` argument must be instance of SPE.Emitter. Was provided with:', emitter ); 2274 | return; 2275 | } 2276 | 2277 | // Issue an error if the emitter isn't a member of this group. 2278 | else if ( emitterIndex === -1 ) { 2279 | console.error( 'Emitter does not exist in this group. Will not remove.' ); 2280 | return; 2281 | } 2282 | 2283 | // Kill all particles by marking them as dead 2284 | // and their age as 0. 2285 | var start = emitter.attributeOffset, 2286 | end = start + emitter.particleCount, 2287 | params = this.attributes.params.typedArray; 2288 | 2289 | // Set alive and age to zero. 2290 | for ( var i = start; i < end; ++i ) { 2291 | params.array[ i * 4 ] = 0.0; 2292 | params.array[ i * 4 + 1 ] = 0.0; 2293 | } 2294 | 2295 | // Remove the emitter from this group's "store". 2296 | this.emitters.splice( emitterIndex, 1 ); 2297 | this.emitterIDs.splice( emitterIndex, 1 ); 2298 | 2299 | // Remove this emitter's attribute values from all shader attributes. 2300 | // The `.splice()` call here also marks each attribute's buffer 2301 | // as needing to update it's entire contents. 2302 | for ( var attr in this.attributes ) { 2303 | if ( this.attributes.hasOwnProperty( attr ) ) { 2304 | this.attributes[ attr ].splice( start, end ); 2305 | } 2306 | } 2307 | 2308 | // Ensure this group's particle count is correct. 2309 | this.particleCount -= emitter.particleCount; 2310 | 2311 | // Call the emitter's remove method. 2312 | emitter._onRemove(); 2313 | 2314 | // Set a flag to indicate that the attribute buffers should 2315 | // be updated in their entirety on the next frame. 2316 | this._attributesNeedRefresh = true; 2317 | }; 2318 | 2319 | 2320 | /** 2321 | * Fetch a single emitter instance from the pool. 2322 | * If there are no objects in the pool, a new emitter will be 2323 | * created if specified. 2324 | * 2325 | * @return {Emitter|null} 2326 | */ 2327 | SPE.Group.prototype.getFromPool = function() { 2328 | 'use strict'; 2329 | 2330 | var pool = this._pool, 2331 | createNew = this._createNewWhenPoolEmpty; 2332 | 2333 | if ( pool.length ) { 2334 | return pool.pop(); 2335 | } 2336 | else if ( createNew ) { 2337 | var emitter = new SPE.Emitter( this._poolCreationSettings ); 2338 | 2339 | this.addEmitter( emitter ); 2340 | 2341 | return emitter; 2342 | } 2343 | 2344 | return null; 2345 | }; 2346 | 2347 | 2348 | /** 2349 | * Release an emitter into the pool. 2350 | * 2351 | * @param {ShaderParticleEmitter} emitter 2352 | * @return {Group} This group instance. 2353 | */ 2354 | SPE.Group.prototype.releaseIntoPool = function( emitter ) { 2355 | 'use strict'; 2356 | 2357 | if ( emitter instanceof SPE.Emitter === false ) { 2358 | console.error( 'Argument is not instanceof SPE.Emitter:', emitter ); 2359 | return; 2360 | } 2361 | 2362 | emitter.reset(); 2363 | this._pool.unshift( emitter ); 2364 | 2365 | return this; 2366 | }; 2367 | 2368 | 2369 | /** 2370 | * Get the pool array 2371 | * 2372 | * @return {Array} 2373 | */ 2374 | SPE.Group.prototype.getPool = function() { 2375 | 'use strict'; 2376 | return this._pool; 2377 | }; 2378 | 2379 | 2380 | /** 2381 | * Add a pool of emitters to this particle group 2382 | * 2383 | * @param {Number} numEmitters The number of emitters to add to the pool. 2384 | * @param {EmitterOptions|Array} emitterOptions An object, or array of objects, describing the options to pass to each emitter. 2385 | * @param {Boolean} createNew Should a new emitter be created if the pool runs out? 2386 | * @return {Group} This group instance. 2387 | */ 2388 | SPE.Group.prototype.addPool = function( numEmitters, emitterOptions, createNew ) { 2389 | 'use strict'; 2390 | 2391 | var emitter; 2392 | 2393 | // Save relevant settings and flags. 2394 | this._poolCreationSettings = emitterOptions; 2395 | this._createNewWhenPoolEmpty = !!createNew; 2396 | 2397 | // Create the emitters, add them to this group and the pool. 2398 | for ( var i = 0; i < numEmitters; ++i ) { 2399 | if ( Array.isArray( emitterOptions ) ) { 2400 | emitter = new SPE.Emitter( emitterOptions[ i ] ); 2401 | } 2402 | else { 2403 | emitter = new SPE.Emitter( emitterOptions ); 2404 | } 2405 | this.addEmitter( emitter ); 2406 | this.releaseIntoPool( emitter ); 2407 | } 2408 | 2409 | return this; 2410 | }; 2411 | 2412 | 2413 | 2414 | SPE.Group.prototype._triggerSingleEmitter = function( pos ) { 2415 | 'use strict'; 2416 | 2417 | var emitter = this.getFromPool(), 2418 | self = this; 2419 | 2420 | if ( emitter === null ) { 2421 | console.log( 'SPE.Group pool ran out.' ); 2422 | return; 2423 | } 2424 | 2425 | // TODO: 2426 | // - Make sure buffers are update with thus new position. 2427 | if ( pos instanceof THREE.Vector3 ) { 2428 | emitter.position.value.copy( pos ); 2429 | 2430 | // Trigger the setter for this property to force an 2431 | // update to the emitter's position attribute. 2432 | emitter.position.value = emitter.position.value; 2433 | } 2434 | 2435 | emitter.enable(); 2436 | 2437 | setTimeout( function() { 2438 | emitter.disable(); 2439 | self.releaseIntoPool( emitter ); 2440 | }, ( Math.max( emitter.duration, ( emitter.maxAge.value + emitter.maxAge.spread ) ) ) * 1000 ); 2441 | 2442 | return this; 2443 | }; 2444 | 2445 | 2446 | /** 2447 | * Set a given number of emitters as alive, with an optional position 2448 | * vector3 to move them to. 2449 | * 2450 | * @param {Number} numEmitters The number of emitters to activate 2451 | * @param {Object} [position=undefined] A THREE.Vector3 instance describing the position to activate the emitter(s) at. 2452 | * @return {Group} This group instance. 2453 | */ 2454 | SPE.Group.prototype.triggerPoolEmitter = function( numEmitters, position ) { 2455 | 'use strict'; 2456 | 2457 | if ( typeof numEmitters === 'number' && numEmitters > 1 ) { 2458 | for ( var i = 0; i < numEmitters; ++i ) { 2459 | this._triggerSingleEmitter( position ); 2460 | } 2461 | } 2462 | else { 2463 | this._triggerSingleEmitter( position ); 2464 | } 2465 | 2466 | return this; 2467 | }; 2468 | 2469 | 2470 | 2471 | SPE.Group.prototype._updateUniforms = function( dt ) { 2472 | 'use strict'; 2473 | 2474 | this.uniforms.runTime.value += dt; 2475 | this.uniforms.deltaTime.value = dt; 2476 | }; 2477 | 2478 | SPE.Group.prototype._resetBufferRanges = function() { 2479 | 'use strict'; 2480 | 2481 | var keys = this.attributeKeys, 2482 | i = this.attributeCount - 1, 2483 | attrs = this.attributes; 2484 | 2485 | for ( i; i >= 0; --i ) { 2486 | attrs[ keys[ i ] ].resetUpdateRange(); 2487 | } 2488 | }; 2489 | 2490 | 2491 | SPE.Group.prototype._updateBuffers = function( emitter ) { 2492 | 'use strict'; 2493 | 2494 | var keys = this.attributeKeys, 2495 | i = this.attributeCount - 1, 2496 | attrs = this.attributes, 2497 | emitterRanges = emitter.bufferUpdateRanges, 2498 | key, 2499 | emitterAttr, 2500 | attr; 2501 | 2502 | for ( i; i >= 0; --i ) { 2503 | key = keys[ i ]; 2504 | emitterAttr = emitterRanges[ key ]; 2505 | attr = attrs[ key ]; 2506 | attr.setUpdateRange( emitterAttr.min, emitterAttr.max ); 2507 | attr.flagUpdate(); 2508 | } 2509 | }; 2510 | 2511 | 2512 | /** 2513 | * Simulate all the emitter's belonging to this group, updating 2514 | * attribute values along the way. 2515 | * @param {Number} [dt=Group's `fixedTimeStep` value] The number of seconds to simulate the group's emitters for (deltaTime) 2516 | */ 2517 | SPE.Group.prototype.tick = function( dt ) { 2518 | 'use strict'; 2519 | 2520 | var emitters = this.emitters, 2521 | numEmitters = emitters.length, 2522 | deltaTime = dt || this.fixedTimeStep, 2523 | keys = this.attributeKeys, 2524 | i, 2525 | attrs = this.attributes; 2526 | 2527 | // Update uniform values. 2528 | this._updateUniforms( deltaTime ); 2529 | 2530 | // Reset buffer update ranges on the shader attributes. 2531 | this._resetBufferRanges(); 2532 | 2533 | 2534 | // If nothing needs updating, then stop here. 2535 | if ( 2536 | numEmitters === 0 && 2537 | this._attributesNeedRefresh === false && 2538 | this._attributesNeedDynamicReset === false 2539 | ) { 2540 | return; 2541 | } 2542 | 2543 | // Loop through each emitter in this group and 2544 | // simulate it, then update the shader attribute 2545 | // buffers. 2546 | for ( var i = 0, emitter; i < numEmitters; ++i ) { 2547 | emitter = emitters[ i ]; 2548 | emitter.tick( deltaTime ); 2549 | this._updateBuffers( emitter ); 2550 | } 2551 | 2552 | // If the shader attributes have been refreshed, 2553 | // then the dynamic properties of each buffer 2554 | // attribute will need to be reset back to 2555 | // what they should be. 2556 | if ( this._attributesNeedDynamicReset === true ) { 2557 | i = this.attributeCount - 1; 2558 | 2559 | for ( i; i >= 0; --i ) { 2560 | attrs[ keys[ i ] ].resetDynamic(); 2561 | } 2562 | 2563 | this._attributesNeedDynamicReset = false; 2564 | } 2565 | 2566 | // If this group's shader attributes need a full refresh 2567 | // then mark each attribute's buffer attribute as 2568 | // needing so. 2569 | if ( this._attributesNeedRefresh === true ) { 2570 | i = this.attributeCount - 1; 2571 | 2572 | for ( i; i >= 0; --i ) { 2573 | attrs[ keys[ i ] ].forceUpdateAll(); 2574 | } 2575 | 2576 | this._attributesNeedRefresh = false; 2577 | this._attributesNeedDynamicReset = true; 2578 | } 2579 | }; 2580 | 2581 | 2582 | /** 2583 | * Dipose the geometry and material for the group. 2584 | * 2585 | * @return {Group} Group instance. 2586 | */ 2587 | SPE.Group.prototype.dispose = function() { 2588 | 'use strict'; 2589 | this.geometry.dispose(); 2590 | this.material.dispose(); 2591 | return this; 2592 | }; 2593 | 2594 | 2595 | /** 2596 | * An SPE.Emitter instance. 2597 | * @typedef {Object} Emitter 2598 | * @see SPE.Emitter 2599 | */ 2600 | 2601 | /** 2602 | * A map of options to configure an SPE.Emitter instance. 2603 | * 2604 | * @typedef {Object} EmitterOptions 2605 | * 2606 | * @property {distribution} [type=BOX] The default distribution this emitter should use to control 2607 | * its particle's spawn position and force behaviour. 2608 | * Must be an SPE.distributions.* value. 2609 | * 2610 | * 2611 | * @property {Number} [particleCount=100] The total number of particles this emitter will hold. NOTE: this is not the number 2612 | * of particles emitted in a second, or anything like that. The number of particles 2613 | * emitted per-second is calculated by particleCount / maxAge (approximately!) 2614 | * 2615 | * @property {Number|null} [duration=null] The duration in seconds that this emitter should live for. If not specified, the emitter 2616 | * will emit particles indefinitely. 2617 | * NOTE: When an emitter is older than a specified duration, the emitter is NOT removed from 2618 | * it's group, but rather is just marked as dead, allowing it to be reanimated at a later time 2619 | * using `SPE.Emitter.prototype.enable()`. 2620 | * 2621 | * @property {Boolean} [isStatic=false] Whether this emitter should be not be simulated (true). 2622 | * @property {Boolean} [activeMultiplier=1] A value between 0 and 1 describing what percentage of this emitter's particlesPerSecond should be 2623 | * emitted, where 0 is 0%, and 1 is 100%. 2624 | * For example, having an emitter with 100 particles, a maxAge of 2, yields a particlesPerSecond 2625 | * value of 50. Setting `activeMultiplier` to 0.5, then, will only emit 25 particles per second (0.5 = 50%). 2626 | * Values greater than 1 will emulate a burst of particles, causing the emitter to run out of particles 2627 | * before it's next activation cycle. 2628 | * 2629 | * @property {Boolean} [direction=1] The direction of the emitter. If value is `1`, emitter will start at beginning of particle's lifecycle. 2630 | * If value is `-1`, emitter will start at end of particle's lifecycle and work it's way backwards. 2631 | * 2632 | * @property {Object} [maxAge={}] An object describing the particle's maximum age in seconds. 2633 | * @property {Number} [maxAge.value=2] A number between 0 and 1 describing the amount of maxAge to apply to all particles. 2634 | * @property {Number} [maxAge.spread=0] A number describing the maxAge variance on a per-particle basis. 2635 | * 2636 | * 2637 | * @property {Object} [position={}] An object describing this emitter's position. 2638 | * @property {Object} [position.value=new THREE.Vector3()] A THREE.Vector3 instance describing this emitter's base position. 2639 | * @property {Object} [position.spread=new THREE.Vector3()] A THREE.Vector3 instance describing this emitter's position variance on a per-particle basis. 2640 | * Note that when using a SPHERE or DISC distribution, only the x-component 2641 | * of this vector is used. 2642 | * When using a LINE distribution, this value is the endpoint of the LINE. 2643 | * @property {Object} [position.spreadClamp=new THREE.Vector3()] A THREE.Vector3 instance describing the numeric multiples the particle's should 2644 | * be spread out over. 2645 | * Note that when using a SPHERE or DISC distribution, only the x-component 2646 | * of this vector is used. 2647 | * When using a LINE distribution, this property is ignored. 2648 | * @property {Number} [position.radius=10] This emitter's base radius. 2649 | * @property {Object} [position.radiusScale=new THREE.Vector3()] A THREE.Vector3 instance describing the radius's scale in all three axes. Allows a SPHERE or DISC to be squashed or stretched. 2650 | * @property {distribution} [position.distribution=value of the `type` option.] A specific distribution to use when radiusing particles. Overrides the `type` option. 2651 | * @property {Boolean} [position.randomise=false] When a particle is re-spawned, whether it's position should be re-randomised or not. Can incur a performance hit. 2652 | * 2653 | * 2654 | * @property {Object} [velocity={}] An object describing this particle velocity. 2655 | * @property {Object} [velocity.value=new THREE.Vector3()] A THREE.Vector3 instance describing this emitter's base velocity. 2656 | * @property {Object} [velocity.spread=new THREE.Vector3()] A THREE.Vector3 instance describing this emitter's velocity variance on a per-particle basis. 2657 | * Note that when using a SPHERE or DISC distribution, only the x-component 2658 | * of this vector is used. 2659 | * @property {distribution} [velocity.distribution=value of the `type` option.] A specific distribution to use when calculating a particle's velocity. Overrides the `type` option. 2660 | * @property {Boolean} [velocity.randomise=false] When a particle is re-spawned, whether it's velocity should be re-randomised or not. Can incur a performance hit. 2661 | * 2662 | * 2663 | * @property {Object} [acceleration={}] An object describing this particle's acceleration. 2664 | * @property {Object} [acceleration.value=new THREE.Vector3()] A THREE.Vector3 instance describing this emitter's base acceleration. 2665 | * @property {Object} [acceleration.spread=new THREE.Vector3()] A THREE.Vector3 instance describing this emitter's acceleration variance on a per-particle basis. 2666 | * Note that when using a SPHERE or DISC distribution, only the x-component 2667 | * of this vector is used. 2668 | * @property {distribution} [acceleration.distribution=value of the `type` option.] A specific distribution to use when calculating a particle's acceleration. Overrides the `type` option. 2669 | * @property {Boolean} [acceleration.randomise=false] When a particle is re-spawned, whether it's acceleration should be re-randomised or not. Can incur a performance hit. 2670 | * 2671 | * 2672 | * @property {Object} [drag={}] An object describing this particle drag. Drag is applied to both velocity and acceleration values. 2673 | * @property {Number} [drag.value=0] A number between 0 and 1 describing the amount of drag to apply to all particles. 2674 | * @property {Number} [drag.spread=0] A number describing the drag variance on a per-particle basis. 2675 | * @property {Boolean} [drag.randomise=false] When a particle is re-spawned, whether it's drag should be re-randomised or not. Can incur a performance hit. 2676 | * 2677 | * 2678 | * @property {Object} [wiggle={}] This is quite a fun one! The values of this object will determine whether a particle will wiggle, or jiggle, or wave, 2679 | * or shimmy, or waggle, or... Well you get the idea. The wiggle is calculated over-time, meaning that a particle will 2680 | * start off with no wiggle, and end up wiggling about with the distance of the `value` specified by the time it dies. 2681 | * It's quite handy to simulate fire embers, or similar effects where the particle's position should slightly change over 2682 | * time, and such change isn't easily controlled by rotation, velocity, or acceleration. The wiggle is a combination of sin and cos calculations, so is circular in nature. 2683 | * @property {Number} [wiggle.value=0] A number describing the amount of wiggle to apply to all particles. It's measured in distance. 2684 | * @property {Number} [wiggle.spread=0] A number describing the wiggle variance on a per-particle basis. 2685 | * 2686 | * 2687 | * @property {Object} [rotation={}] An object describing this emitter's rotation. It can either be static, or set to rotate from 0radians to the value of `rotation.value` 2688 | * over a particle's lifetime. Rotation values affect both a particle's position and the forces applied to it. 2689 | * @property {Object} [rotation.axis=new THREE.Vector3(0, 1, 0)] A THREE.Vector3 instance describing this emitter's axis of rotation. 2690 | * @property {Object} [rotation.axisSpread=new THREE.Vector3()] A THREE.Vector3 instance describing the amount of variance to apply to the axis of rotation on 2691 | * a per-particle basis. 2692 | * @property {Number} [rotation.angle=0] The angle of rotation, given in radians. If `rotation.static` is true, the emitter will start off rotated at this angle, and stay as such. 2693 | * Otherwise, the particles will rotate from 0radians to this value over their lifetimes. 2694 | * @property {Number} [rotation.angleSpread=0] The amount of variance in each particle's rotation angle. 2695 | * @property {Boolean} [rotation.static=false] Whether the rotation should be static or not. 2696 | * @property {Object} [rotation.center=The value of `position.value`] A THREE.Vector3 instance describing the center point of rotation. 2697 | * @property {Boolean} [rotation.randomise=false] When a particle is re-spawned, whether it's rotation should be re-randomised or not. Can incur a performance hit. 2698 | * 2699 | * 2700 | * @property {Object} [color={}] An object describing a particle's color. This property is a "value-over-lifetime" property, meaning an array of values and spreads can be 2701 | * given to describe specific value changes over a particle's lifetime. 2702 | * Depending on the value of SPE.valueOverLifetimeLength, if arrays of THREE.Color instances are given, then the array will be interpolated to 2703 | * have a length matching the value of SPE.valueOverLifetimeLength. 2704 | * @property {Object} [color.value=new THREE.Color()] Either a single THREE.Color instance, or an array of THREE.Color instances to describe the color of a particle over it's lifetime. 2705 | * @property {Object} [color.spread=new THREE.Vector3()] Either a single THREE.Vector3 instance, or an array of THREE.Vector3 instances to describe the color variance of a particle over it's lifetime. 2706 | * @property {Boolean} [color.randomise=false] When a particle is re-spawned, whether it's color should be re-randomised or not. Can incur a performance hit. 2707 | * 2708 | * 2709 | * @property {Object} [opacity={}] An object describing a particle's opacity. This property is a "value-over-lifetime" property, meaning an array of values and spreads can be 2710 | * given to describe specific value changes over a particle's lifetime. 2711 | * Depending on the value of SPE.valueOverLifetimeLength, if arrays of numbers are given, then the array will be interpolated to 2712 | * have a length matching the value of SPE.valueOverLifetimeLength. 2713 | * @property {Number} [opacity.value=1] Either a single number, or an array of numbers to describe the opacity of a particle over it's lifetime. 2714 | * @property {Number} [opacity.spread=0] Either a single number, or an array of numbers to describe the opacity variance of a particle over it's lifetime. 2715 | * @property {Boolean} [opacity.randomise=false] When a particle is re-spawned, whether it's opacity should be re-randomised or not. Can incur a performance hit. 2716 | * 2717 | * 2718 | * @property {Object} [size={}] An object describing a particle's size. This property is a "value-over-lifetime" property, meaning an array of values and spreads can be 2719 | * given to describe specific value changes over a particle's lifetime. 2720 | * Depending on the value of SPE.valueOverLifetimeLength, if arrays of numbers are given, then the array will be interpolated to 2721 | * have a length matching the value of SPE.valueOverLifetimeLength. 2722 | * @property {Number} [size.value=1] Either a single number, or an array of numbers to describe the size of a particle over it's lifetime. 2723 | * @property {Number} [size.spread=0] Either a single number, or an array of numbers to describe the size variance of a particle over it's lifetime. 2724 | * @property {Boolean} [size.randomise=false] When a particle is re-spawned, whether it's size should be re-randomised or not. Can incur a performance hit. 2725 | * 2726 | * 2727 | * @property {Object} [angle={}] An object describing a particle's angle. The angle is a 2d-rotation, measured in radians, applied to the particle's texture. 2728 | * NOTE: if a particle's texture is a sprite-sheet, this value IS IGNORED. 2729 | * This property is a "value-over-lifetime" property, meaning an array of values and spreads can be 2730 | * given to describe specific value changes over a particle's lifetime. 2731 | * Depending on the value of SPE.valueOverLifetimeLength, if arrays of numbers are given, then the array will be interpolated to 2732 | * have a length matching the value of SPE.valueOverLifetimeLength. 2733 | * @property {Number} [angle.value=0] Either a single number, or an array of numbers to describe the angle of a particle over it's lifetime. 2734 | * @property {Number} [angle.spread=0] Either a single number, or an array of numbers to describe the angle variance of a particle over it's lifetime. 2735 | * @property {Boolean} [angle.randomise=false] When a particle is re-spawned, whether it's angle should be re-randomised or not. Can incur a performance hit. 2736 | * 2737 | */ 2738 | 2739 | /** 2740 | * The SPE.Emitter class. 2741 | * 2742 | * @constructor 2743 | * 2744 | * @param {EmitterOptions} options A map of options to configure the emitter. 2745 | */ 2746 | SPE.Emitter = function( options ) { 2747 | 'use strict'; 2748 | 2749 | var utils = SPE.utils, 2750 | types = utils.types, 2751 | lifetimeLength = SPE.valueOverLifetimeLength; 2752 | 2753 | // Ensure we have a map of options to play with, 2754 | // and that each option is in the correct format. 2755 | options = utils.ensureTypedArg( options, types.OBJECT, {} ); 2756 | options.position = utils.ensureTypedArg( options.position, types.OBJECT, {} ); 2757 | options.velocity = utils.ensureTypedArg( options.velocity, types.OBJECT, {} ); 2758 | options.acceleration = utils.ensureTypedArg( options.acceleration, types.OBJECT, {} ); 2759 | options.radius = utils.ensureTypedArg( options.radius, types.OBJECT, {} ); 2760 | options.drag = utils.ensureTypedArg( options.drag, types.OBJECT, {} ); 2761 | options.rotation = utils.ensureTypedArg( options.rotation, types.OBJECT, {} ); 2762 | options.color = utils.ensureTypedArg( options.color, types.OBJECT, {} ); 2763 | options.opacity = utils.ensureTypedArg( options.opacity, types.OBJECT, {} ); 2764 | options.size = utils.ensureTypedArg( options.size, types.OBJECT, {} ); 2765 | options.angle = utils.ensureTypedArg( options.angle, types.OBJECT, {} ); 2766 | options.wiggle = utils.ensureTypedArg( options.wiggle, types.OBJECT, {} ); 2767 | options.maxAge = utils.ensureTypedArg( options.maxAge, types.OBJECT, {} ); 2768 | 2769 | if ( options.onParticleSpawn ) { 2770 | console.warn( 'onParticleSpawn has been removed. Please set properties directly to alter values at runtime.' ); 2771 | } 2772 | 2773 | this.uuid = THREE.MathUtils.generateUUID(); 2774 | 2775 | this.type = utils.ensureTypedArg( options.type, types.NUMBER, SPE.distributions.BOX ); 2776 | 2777 | // Start assigning properties...kicking it off with props that DON'T support values over 2778 | // lifetimes. 2779 | // 2780 | // Btw, values over lifetimes are just the new way of referring to *Start, *Middle, and *End. 2781 | this.position = { 2782 | _value: utils.ensureInstanceOf( options.position.value, THREE.Vector3, new THREE.Vector3() ), 2783 | _spread: utils.ensureInstanceOf( options.position.spread, THREE.Vector3, new THREE.Vector3() ), 2784 | _spreadClamp: utils.ensureInstanceOf( options.position.spreadClamp, THREE.Vector3, new THREE.Vector3() ), 2785 | _distribution: utils.ensureTypedArg( options.position.distribution, types.NUMBER, this.type ), 2786 | _randomise: utils.ensureTypedArg( options.position.randomise, types.BOOLEAN, false ), 2787 | _radius: utils.ensureTypedArg( options.position.radius, types.NUMBER, 10 ), 2788 | _radiusScale: utils.ensureInstanceOf( options.position.radiusScale, THREE.Vector3, new THREE.Vector3( 1, 1, 1 ) ), 2789 | _distributionClamp: utils.ensureTypedArg( options.position.distributionClamp, types.NUMBER, 0 ), 2790 | }; 2791 | 2792 | this.velocity = { 2793 | _value: utils.ensureInstanceOf( options.velocity.value, THREE.Vector3, new THREE.Vector3() ), 2794 | _spread: utils.ensureInstanceOf( options.velocity.spread, THREE.Vector3, new THREE.Vector3() ), 2795 | _distribution: utils.ensureTypedArg( options.velocity.distribution, types.NUMBER, this.type ), 2796 | _randomise: utils.ensureTypedArg( options.position.randomise, types.BOOLEAN, false ) 2797 | }; 2798 | 2799 | this.acceleration = { 2800 | _value: utils.ensureInstanceOf( options.acceleration.value, THREE.Vector3, new THREE.Vector3() ), 2801 | _spread: utils.ensureInstanceOf( options.acceleration.spread, THREE.Vector3, new THREE.Vector3() ), 2802 | _distribution: utils.ensureTypedArg( options.acceleration.distribution, types.NUMBER, this.type ), 2803 | _randomise: utils.ensureTypedArg( options.position.randomise, types.BOOLEAN, false ) 2804 | }; 2805 | 2806 | this.drag = { 2807 | _value: utils.ensureTypedArg( options.drag.value, types.NUMBER, 0 ), 2808 | _spread: utils.ensureTypedArg( options.drag.spread, types.NUMBER, 0 ), 2809 | _randomise: utils.ensureTypedArg( options.position.randomise, types.BOOLEAN, false ) 2810 | }; 2811 | 2812 | this.wiggle = { 2813 | _value: utils.ensureTypedArg( options.wiggle.value, types.NUMBER, 0 ), 2814 | _spread: utils.ensureTypedArg( options.wiggle.spread, types.NUMBER, 0 ) 2815 | }; 2816 | 2817 | this.rotation = { 2818 | _axis: utils.ensureInstanceOf( options.rotation.axis, THREE.Vector3, new THREE.Vector3( 0.0, 1.0, 0.0 ) ), 2819 | _axisSpread: utils.ensureInstanceOf( options.rotation.axisSpread, THREE.Vector3, new THREE.Vector3() ), 2820 | _angle: utils.ensureTypedArg( options.rotation.angle, types.NUMBER, 0 ), 2821 | _angleSpread: utils.ensureTypedArg( options.rotation.angleSpread, types.NUMBER, 0 ), 2822 | _static: utils.ensureTypedArg( options.rotation.static, types.BOOLEAN, false ), 2823 | _center: utils.ensureInstanceOf( options.rotation.center, THREE.Vector3, this.position._value.clone() ), 2824 | _randomise: utils.ensureTypedArg( options.position.randomise, types.BOOLEAN, false ) 2825 | }; 2826 | 2827 | 2828 | this.maxAge = { 2829 | _value: utils.ensureTypedArg( options.maxAge.value, types.NUMBER, 2 ), 2830 | _spread: utils.ensureTypedArg( options.maxAge.spread, types.NUMBER, 0 ) 2831 | }; 2832 | 2833 | 2834 | 2835 | // The following properties can support either single values, or an array of values that change 2836 | // the property over a particle's lifetime (value over lifetime). 2837 | this.color = { 2838 | _value: utils.ensureArrayInstanceOf( options.color.value, THREE.Color, new THREE.Color() ), 2839 | _spread: utils.ensureArrayInstanceOf( options.color.spread, THREE.Vector3, new THREE.Vector3() ), 2840 | _randomise: utils.ensureTypedArg( options.position.randomise, types.BOOLEAN, false ) 2841 | }; 2842 | 2843 | this.opacity = { 2844 | _value: utils.ensureArrayTypedArg( options.opacity.value, types.NUMBER, 1 ), 2845 | _spread: utils.ensureArrayTypedArg( options.opacity.spread, types.NUMBER, 0 ), 2846 | _randomise: utils.ensureTypedArg( options.position.randomise, types.BOOLEAN, false ) 2847 | }; 2848 | 2849 | this.size = { 2850 | _value: utils.ensureArrayTypedArg( options.size.value, types.NUMBER, 1 ), 2851 | _spread: utils.ensureArrayTypedArg( options.size.spread, types.NUMBER, 0 ), 2852 | _randomise: utils.ensureTypedArg( options.position.randomise, types.BOOLEAN, false ) 2853 | }; 2854 | 2855 | this.angle = { 2856 | _value: utils.ensureArrayTypedArg( options.angle.value, types.NUMBER, 0 ), 2857 | _spread: utils.ensureArrayTypedArg( options.angle.spread, types.NUMBER, 0 ), 2858 | _randomise: utils.ensureTypedArg( options.position.randomise, types.BOOLEAN, false ) 2859 | }; 2860 | 2861 | 2862 | // Assign renaining option values. 2863 | this.particleCount = utils.ensureTypedArg( options.particleCount, types.NUMBER, 100 ); 2864 | this.duration = utils.ensureTypedArg( options.duration, types.NUMBER, null ); 2865 | this.isStatic = utils.ensureTypedArg( options.isStatic, types.BOOLEAN, false ); 2866 | this.activeMultiplier = utils.ensureTypedArg( options.activeMultiplier, types.NUMBER, 1 ); 2867 | this.direction = utils.ensureTypedArg( options.direction, types.NUMBER, 1 ); 2868 | 2869 | // Whether this emitter is alive or not. 2870 | this.alive = utils.ensureTypedArg( options.alive, types.BOOLEAN, true ); 2871 | 2872 | 2873 | // The following properties are set internally and are not 2874 | // user-controllable. 2875 | this.particlesPerSecond = 0; 2876 | 2877 | // The current particle index for which particles should 2878 | // be marked as active on the next update cycle. 2879 | this.activationIndex = 0; 2880 | 2881 | // The offset in the typed arrays this emitter's 2882 | // particle's values will start at 2883 | this.attributeOffset = 0; 2884 | 2885 | // The end of the range in the attribute buffers 2886 | this.attributeEnd = 0; 2887 | 2888 | 2889 | 2890 | // Holds the time the emitter has been alive for. 2891 | this.age = 0.0; 2892 | 2893 | // Holds the number of currently-alive particles 2894 | this.activeParticleCount = 0.0; 2895 | 2896 | // Holds a reference to this emitter's group once 2897 | // it's added to one. 2898 | this.group = null; 2899 | 2900 | // Holds a reference to this emitter's group's attributes object 2901 | // for easier access. 2902 | this.attributes = null; 2903 | 2904 | // Holds a reference to the params attribute's typed array 2905 | // for quicker access. 2906 | this.paramsArray = null; 2907 | 2908 | // A set of flags to determine whether particular properties 2909 | // should be re-randomised when a particle is reset. 2910 | // 2911 | // If a `randomise` property is given, this is preferred. 2912 | // Otherwise, it looks at whether a spread value has been 2913 | // given. 2914 | // 2915 | // It allows randomization to be turned off as desired. If 2916 | // all randomization is turned off, then I'd expect a performance 2917 | // boost as no attribute buffers (excluding the `params`) 2918 | // would have to be re-passed to the GPU each frame (since nothing 2919 | // except the `params` attribute would have changed). 2920 | this.resetFlags = { 2921 | // params: utils.ensureTypedArg( options.maxAge.randomise, types.BOOLEAN, !!options.maxAge.spread ) || 2922 | // utils.ensureTypedArg( options.wiggle.randomise, types.BOOLEAN, !!options.wiggle.spread ), 2923 | position: utils.ensureTypedArg( options.position.randomise, types.BOOLEAN, false ) || 2924 | utils.ensureTypedArg( options.radius.randomise, types.BOOLEAN, false ), 2925 | velocity: utils.ensureTypedArg( options.velocity.randomise, types.BOOLEAN, false ), 2926 | acceleration: utils.ensureTypedArg( options.acceleration.randomise, types.BOOLEAN, false ) || 2927 | utils.ensureTypedArg( options.drag.randomise, types.BOOLEAN, false ), 2928 | rotation: utils.ensureTypedArg( options.rotation.randomise, types.BOOLEAN, false ), 2929 | rotationCenter: utils.ensureTypedArg( options.rotation.randomise, types.BOOLEAN, false ), 2930 | size: utils.ensureTypedArg( options.size.randomise, types.BOOLEAN, false ), 2931 | color: utils.ensureTypedArg( options.color.randomise, types.BOOLEAN, false ), 2932 | opacity: utils.ensureTypedArg( options.opacity.randomise, types.BOOLEAN, false ), 2933 | angle: utils.ensureTypedArg( options.angle.randomise, types.BOOLEAN, false ) 2934 | }; 2935 | 2936 | this.updateFlags = {}; 2937 | this.updateCounts = {}; 2938 | 2939 | // A map to indicate which emitter parameters should update 2940 | // which attribute. 2941 | this.updateMap = { 2942 | maxAge: 'params', 2943 | position: 'position', 2944 | velocity: 'velocity', 2945 | acceleration: 'acceleration', 2946 | drag: 'acceleration', 2947 | wiggle: 'params', 2948 | rotation: 'rotation', 2949 | size: 'size', 2950 | color: 'color', 2951 | opacity: 'opacity', 2952 | angle: 'angle' 2953 | }; 2954 | 2955 | for ( var i in this.updateMap ) { 2956 | if ( this.updateMap.hasOwnProperty( i ) ) { 2957 | this.updateCounts[ this.updateMap[ i ] ] = 0.0; 2958 | this.updateFlags[ this.updateMap[ i ] ] = false; 2959 | this._createGetterSetters( this[ i ], i ); 2960 | } 2961 | } 2962 | 2963 | this.bufferUpdateRanges = {}; 2964 | this.attributeKeys = null; 2965 | this.attributeCount = 0; 2966 | 2967 | 2968 | // Ensure that the value-over-lifetime property objects above 2969 | // have value and spread properties that are of the same length. 2970 | // 2971 | // Also, for now, make sure they have a length of 3 (min/max arguments here). 2972 | utils.ensureValueOverLifetimeCompliance( this.color, lifetimeLength, lifetimeLength ); 2973 | utils.ensureValueOverLifetimeCompliance( this.opacity, lifetimeLength, lifetimeLength ); 2974 | utils.ensureValueOverLifetimeCompliance( this.size, lifetimeLength, lifetimeLength ); 2975 | utils.ensureValueOverLifetimeCompliance( this.angle, lifetimeLength, lifetimeLength ); 2976 | }; 2977 | 2978 | SPE.Emitter.constructor = SPE.Emitter; 2979 | 2980 | SPE.Emitter.prototype._createGetterSetters = function( propObj, propName ) { 2981 | 'use strict'; 2982 | 2983 | var self = this; 2984 | 2985 | for ( var i in propObj ) { 2986 | if ( propObj.hasOwnProperty( i ) ) { 2987 | 2988 | var name = i.replace( '_', '' ); 2989 | 2990 | Object.defineProperty( propObj, name, { 2991 | get: ( function( prop ) { 2992 | return function() { 2993 | return this[ prop ]; 2994 | }; 2995 | }( i ) ), 2996 | 2997 | set: ( function( prop ) { 2998 | return function( value ) { 2999 | var mapName = self.updateMap[ propName ], 3000 | prevValue = this[ prop ], 3001 | length = SPE.valueOverLifetimeLength; 3002 | 3003 | if ( prop === '_rotationCenter' ) { 3004 | self.updateFlags.rotationCenter = true; 3005 | self.updateCounts.rotationCenter = 0.0; 3006 | } 3007 | else if ( prop === '_randomise' ) { 3008 | self.resetFlags[ mapName ] = value; 3009 | } 3010 | else { 3011 | self.updateFlags[ mapName ] = true; 3012 | self.updateCounts[ mapName ] = 0.0; 3013 | } 3014 | 3015 | self.group._updateDefines(); 3016 | 3017 | this[ prop ] = value; 3018 | 3019 | // If the previous value was an array, then make 3020 | // sure the provided value is interpolated correctly. 3021 | if ( Array.isArray( prevValue ) ) { 3022 | SPE.utils.ensureValueOverLifetimeCompliance( self[ propName ], length, length ); 3023 | } 3024 | }; 3025 | }( i ) ) 3026 | } ); 3027 | } 3028 | } 3029 | }; 3030 | 3031 | SPE.Emitter.prototype._setBufferUpdateRanges = function( keys ) { 3032 | 'use strict'; 3033 | 3034 | this.attributeKeys = keys; 3035 | this.attributeCount = keys.length; 3036 | 3037 | for ( var i = this.attributeCount - 1; i >= 0; --i ) { 3038 | this.bufferUpdateRanges[ keys[ i ] ] = { 3039 | min: Number.POSITIVE_INFINITY, 3040 | max: Number.NEGATIVE_INFINITY 3041 | }; 3042 | } 3043 | }; 3044 | 3045 | SPE.Emitter.prototype._calculatePPSValue = function( groupMaxAge ) { 3046 | 'use strict'; 3047 | 3048 | var particleCount = this.particleCount; 3049 | 3050 | 3051 | // Calculate the `particlesPerSecond` value for this emitter. It's used 3052 | // when determining which particles should die and which should live to 3053 | // see another day. Or be born, for that matter. The "God" property. 3054 | if ( this.duration ) { 3055 | this.particlesPerSecond = particleCount / ( groupMaxAge < this.duration ? groupMaxAge : this.duration ); 3056 | } 3057 | else { 3058 | this.particlesPerSecond = particleCount / groupMaxAge; 3059 | } 3060 | }; 3061 | 3062 | SPE.Emitter.prototype._setAttributeOffset = function( startIndex ) { 3063 | this.attributeOffset = startIndex; 3064 | this.activationIndex = startIndex; 3065 | this.activationEnd = startIndex + this.particleCount; 3066 | }; 3067 | 3068 | 3069 | SPE.Emitter.prototype._assignValue = function( prop, index ) { 3070 | 'use strict'; 3071 | 3072 | switch ( prop ) { 3073 | case 'position': 3074 | this._assignPositionValue( index ); 3075 | break; 3076 | 3077 | case 'velocity': 3078 | case 'acceleration': 3079 | this._assignForceValue( index, prop ); 3080 | break; 3081 | 3082 | case 'size': 3083 | case 'opacity': 3084 | this._assignAbsLifetimeValue( index, prop ); 3085 | break; 3086 | 3087 | case 'angle': 3088 | this._assignAngleValue( index ); 3089 | break; 3090 | 3091 | case 'params': 3092 | this._assignParamsValue( index ); 3093 | break; 3094 | 3095 | case 'rotation': 3096 | this._assignRotationValue( index ); 3097 | break; 3098 | 3099 | case 'color': 3100 | this._assignColorValue( index ); 3101 | break; 3102 | } 3103 | }; 3104 | 3105 | SPE.Emitter.prototype._assignPositionValue = function( index ) { 3106 | 'use strict'; 3107 | 3108 | var distributions = SPE.distributions, 3109 | utils = SPE.utils, 3110 | prop = this.position, 3111 | attr = this.attributes.position, 3112 | value = prop._value, 3113 | spread = prop._spread, 3114 | distribution = prop._distribution; 3115 | 3116 | switch ( distribution ) { 3117 | case distributions.BOX: 3118 | utils.randomVector3( attr, index, value, spread, prop._spreadClamp ); 3119 | break; 3120 | 3121 | case distributions.SPHERE: 3122 | utils.randomVector3OnSphere( attr, index, value, prop._radius, prop._spread.x, prop._radiusScale, prop._spreadClamp.x, prop._distributionClamp || this.particleCount ); 3123 | break; 3124 | 3125 | case distributions.DISC: 3126 | utils.randomVector3OnDisc( attr, index, value, prop._radius, prop._spread.x, prop._radiusScale, prop._spreadClamp.x ); 3127 | break; 3128 | 3129 | case distributions.LINE: 3130 | utils.randomVector3OnLine( attr, index, value, spread ); 3131 | break; 3132 | } 3133 | }; 3134 | 3135 | SPE.Emitter.prototype._assignForceValue = function( index, attrName ) { 3136 | 'use strict'; 3137 | 3138 | var distributions = SPE.distributions, 3139 | utils = SPE.utils, 3140 | prop = this[ attrName ], 3141 | value = prop._value, 3142 | spread = prop._spread, 3143 | distribution = prop._distribution, 3144 | pos, 3145 | positionX, 3146 | positionY, 3147 | positionZ, 3148 | i; 3149 | 3150 | switch ( distribution ) { 3151 | case distributions.BOX: 3152 | utils.randomVector3( this.attributes[ attrName ], index, value, spread ); 3153 | break; 3154 | 3155 | case distributions.SPHERE: 3156 | pos = this.attributes.position.typedArray.array; 3157 | i = index * 3; 3158 | 3159 | // Ensure position values aren't zero, otherwise no force will be 3160 | // applied. 3161 | // positionX = utils.zeroToEpsilon( pos[ i ], true ); 3162 | // positionY = utils.zeroToEpsilon( pos[ i + 1 ], true ); 3163 | // positionZ = utils.zeroToEpsilon( pos[ i + 2 ], true ); 3164 | positionX = pos[ i ]; 3165 | positionY = pos[ i + 1 ]; 3166 | positionZ = pos[ i + 2 ]; 3167 | 3168 | utils.randomDirectionVector3OnSphere( 3169 | this.attributes[ attrName ], index, 3170 | positionX, positionY, positionZ, 3171 | this.position._value, 3172 | prop._value.x, 3173 | prop._spread.x 3174 | ); 3175 | break; 3176 | 3177 | case distributions.DISC: 3178 | pos = this.attributes.position.typedArray.array; 3179 | i = index * 3; 3180 | 3181 | // Ensure position values aren't zero, otherwise no force will be 3182 | // applied. 3183 | // positionX = utils.zeroToEpsilon( pos[ i ], true ); 3184 | // positionY = utils.zeroToEpsilon( pos[ i + 1 ], true ); 3185 | // positionZ = utils.zeroToEpsilon( pos[ i + 2 ], true ); 3186 | positionX = pos[ i ]; 3187 | positionY = pos[ i + 1 ]; 3188 | positionZ = pos[ i + 2 ]; 3189 | 3190 | utils.randomDirectionVector3OnDisc( 3191 | this.attributes[ attrName ], index, 3192 | positionX, positionY, positionZ, 3193 | this.position._value, 3194 | prop._value.x, 3195 | prop._spread.x 3196 | ); 3197 | break; 3198 | 3199 | case distributions.LINE: 3200 | utils.randomVector3OnLine( this.attributes[ attrName ], index, value, spread ); 3201 | break; 3202 | } 3203 | 3204 | if ( attrName === 'acceleration' ) { 3205 | var drag = utils.clamp( utils.randomFloat( this.drag._value, this.drag._spread ), 0, 1 ); 3206 | this.attributes.acceleration.typedArray.array[ index * 4 + 3 ] = drag; 3207 | } 3208 | }; 3209 | 3210 | SPE.Emitter.prototype._assignAbsLifetimeValue = function( index, propName ) { 3211 | 'use strict'; 3212 | 3213 | var array = this.attributes[ propName ].typedArray, 3214 | prop = this[ propName ], 3215 | utils = SPE.utils, 3216 | value; 3217 | 3218 | if ( utils.arrayValuesAreEqual( prop._value ) && utils.arrayValuesAreEqual( prop._spread ) ) { 3219 | value = Math.abs( utils.randomFloat( prop._value[ 0 ], prop._spread[ 0 ] ) ); 3220 | array.setVec4Components( index, value, value, value, value ); 3221 | } 3222 | else { 3223 | array.setVec4Components( index, 3224 | Math.abs( utils.randomFloat( prop._value[ 0 ], prop._spread[ 0 ] ) ), 3225 | Math.abs( utils.randomFloat( prop._value[ 1 ], prop._spread[ 1 ] ) ), 3226 | Math.abs( utils.randomFloat( prop._value[ 2 ], prop._spread[ 2 ] ) ), 3227 | Math.abs( utils.randomFloat( prop._value[ 3 ], prop._spread[ 3 ] ) ) 3228 | ); 3229 | } 3230 | }; 3231 | 3232 | SPE.Emitter.prototype._assignAngleValue = function( index ) { 3233 | 'use strict'; 3234 | 3235 | var array = this.attributes.angle.typedArray, 3236 | prop = this.angle, 3237 | utils = SPE.utils, 3238 | value; 3239 | 3240 | if ( utils.arrayValuesAreEqual( prop._value ) && utils.arrayValuesAreEqual( prop._spread ) ) { 3241 | value = utils.randomFloat( prop._value[ 0 ], prop._spread[ 0 ] ); 3242 | array.setVec4Components( index, value, value, value, value ); 3243 | } 3244 | else { 3245 | array.setVec4Components( index, 3246 | utils.randomFloat( prop._value[ 0 ], prop._spread[ 0 ] ), 3247 | utils.randomFloat( prop._value[ 1 ], prop._spread[ 1 ] ), 3248 | utils.randomFloat( prop._value[ 2 ], prop._spread[ 2 ] ), 3249 | utils.randomFloat( prop._value[ 3 ], prop._spread[ 3 ] ) 3250 | ); 3251 | } 3252 | }; 3253 | 3254 | SPE.Emitter.prototype._assignParamsValue = function( index ) { 3255 | 'use strict'; 3256 | 3257 | this.attributes.params.typedArray.setVec4Components( index, 3258 | this.isStatic ? 1 : 0, 3259 | 0.0, 3260 | Math.abs( SPE.utils.randomFloat( this.maxAge._value, this.maxAge._spread ) ), 3261 | SPE.utils.randomFloat( this.wiggle._value, this.wiggle._spread ) 3262 | ); 3263 | }; 3264 | 3265 | SPE.Emitter.prototype._assignRotationValue = function( index ) { 3266 | 'use strict'; 3267 | 3268 | this.attributes.rotation.typedArray.setVec3Components( index, 3269 | SPE.utils.getPackedRotationAxis( this.rotation._axis, this.rotation._axisSpread ), 3270 | SPE.utils.randomFloat( this.rotation._angle, this.rotation._angleSpread ), 3271 | this.rotation._static ? 0 : 1 3272 | ); 3273 | 3274 | this.attributes.rotationCenter.typedArray.setVec3( index, this.rotation._center ); 3275 | }; 3276 | 3277 | SPE.Emitter.prototype._assignColorValue = function( index ) { 3278 | 'use strict'; 3279 | SPE.utils.randomColorAsHex( this.attributes.color, index, this.color._value, this.color._spread ); 3280 | }; 3281 | 3282 | SPE.Emitter.prototype._resetParticle = function( index ) { 3283 | 'use strict'; 3284 | 3285 | var resetFlags = this.resetFlags, 3286 | updateFlags = this.updateFlags, 3287 | updateCounts = this.updateCounts, 3288 | keys = this.attributeKeys, 3289 | key, 3290 | updateFlag; 3291 | 3292 | for ( var i = this.attributeCount - 1; i >= 0; --i ) { 3293 | key = keys[ i ]; 3294 | updateFlag = updateFlags[ key ]; 3295 | 3296 | if ( resetFlags[ key ] === true || updateFlag === true ) { 3297 | this._assignValue( key, index ); 3298 | this._updateAttributeUpdateRange( key, index ); 3299 | 3300 | if ( updateFlag === true && updateCounts[ key ] === this.particleCount ) { 3301 | updateFlags[ key ] = false; 3302 | updateCounts[ key ] = 0.0; 3303 | } 3304 | else if ( updateFlag === true ) { 3305 | ++updateCounts[ key ]; 3306 | } 3307 | } 3308 | } 3309 | }; 3310 | 3311 | SPE.Emitter.prototype._updateAttributeUpdateRange = function( attr, i ) { 3312 | 'use strict'; 3313 | 3314 | var ranges = this.bufferUpdateRanges[ attr ]; 3315 | 3316 | ranges.min = Math.min( i, ranges.min ); 3317 | ranges.max = Math.max( i, ranges.max ); 3318 | }; 3319 | 3320 | SPE.Emitter.prototype._resetBufferRanges = function() { 3321 | 'use strict'; 3322 | 3323 | var ranges = this.bufferUpdateRanges, 3324 | keys = this.bufferUpdateKeys, 3325 | i = this.bufferUpdateCount - 1, 3326 | key; 3327 | 3328 | for ( i; i >= 0; --i ) { 3329 | key = keys[ i ]; 3330 | ranges[ key ].min = Number.POSITIVE_INFINITY; 3331 | ranges[ key ].max = Number.NEGATIVE_INFINITY; 3332 | } 3333 | }; 3334 | 3335 | SPE.Emitter.prototype._onRemove = function() { 3336 | 'use strict'; 3337 | // Reset any properties of the emitter that were set by 3338 | // a group when it was added. 3339 | this.particlesPerSecond = 0; 3340 | this.attributeOffset = 0; 3341 | this.activationIndex = 0; 3342 | this.activeParticleCount = 0; 3343 | this.group = null; 3344 | this.attributes = null; 3345 | this.paramsArray = null; 3346 | this.age = 0.0; 3347 | }; 3348 | 3349 | SPE.Emitter.prototype._decrementParticleCount = function() { 3350 | 'use strict'; 3351 | --this.activeParticleCount; 3352 | 3353 | // TODO: 3354 | // - Trigger event if count === 0. 3355 | }; 3356 | 3357 | SPE.Emitter.prototype._incrementParticleCount = function() { 3358 | 'use strict'; 3359 | ++this.activeParticleCount; 3360 | 3361 | // TODO: 3362 | // - Trigger event if count === this.particleCount. 3363 | }; 3364 | 3365 | SPE.Emitter.prototype._checkParticleAges = function( start, end, params, dt ) { 3366 | 'use strict'; 3367 | for ( var i = end - 1, index, maxAge, age, alive; i >= start; --i ) { 3368 | index = i * 4; 3369 | 3370 | alive = params[ index ]; 3371 | 3372 | if ( alive === 0.0 ) { 3373 | continue; 3374 | } 3375 | 3376 | // Increment age 3377 | age = params[ index + 1 ]; 3378 | maxAge = params[ index + 2 ]; 3379 | 3380 | if ( this.direction === 1 ) { 3381 | age += dt; 3382 | 3383 | if ( age >= maxAge ) { 3384 | age = 0.0; 3385 | alive = 0.0; 3386 | this._decrementParticleCount(); 3387 | } 3388 | } 3389 | else { 3390 | age -= dt; 3391 | 3392 | if ( age <= 0.0 ) { 3393 | age = maxAge; 3394 | alive = 0.0; 3395 | this._decrementParticleCount(); 3396 | } 3397 | } 3398 | 3399 | params[ index ] = alive; 3400 | params[ index + 1 ] = age; 3401 | 3402 | this._updateAttributeUpdateRange( 'params', i ); 3403 | } 3404 | }; 3405 | 3406 | SPE.Emitter.prototype._activateParticles = function( activationStart, activationEnd, params, dtPerParticle ) { 3407 | 'use strict'; 3408 | var direction = this.direction; 3409 | 3410 | for ( var i = activationStart, index, dtValue; i < activationEnd; ++i ) { 3411 | index = i * 4; 3412 | 3413 | // Don't re-activate particles that aren't dead yet. 3414 | // if ( params[ index ] !== 0.0 && ( this.particleCount !== 1 || this.activeMultiplier !== 1 ) ) { 3415 | // continue; 3416 | // } 3417 | 3418 | if ( params[ index ] != 0.0 && this.particleCount !== 1 ) { 3419 | continue; 3420 | } 3421 | 3422 | // Increment the active particle count. 3423 | this._incrementParticleCount(); 3424 | 3425 | // Mark the particle as alive. 3426 | params[ index ] = 1.0; 3427 | 3428 | // Reset the particle 3429 | this._resetParticle( i ); 3430 | 3431 | // Move each particle being activated to 3432 | // it's actual position in time. 3433 | // 3434 | // This stops particles being 'clumped' together 3435 | // when frame rates are on the lower side of 60fps 3436 | // or not constant (a very real possibility!) 3437 | dtValue = dtPerParticle * ( i - activationStart ) 3438 | params[ index + 1 ] = direction === -1 ? params[ index + 2 ] - dtValue : dtValue; 3439 | 3440 | this._updateAttributeUpdateRange( 'params', i ); 3441 | } 3442 | }; 3443 | 3444 | /** 3445 | * Simulates one frame's worth of particles, updating particles 3446 | * that are already alive, and marking ones that are currently dead 3447 | * but should be alive as alive. 3448 | * 3449 | * If the emitter is marked as static, then this function will do nothing. 3450 | * 3451 | * @param {Number} dt The number of seconds to simulate (deltaTime) 3452 | */ 3453 | SPE.Emitter.prototype.tick = function( dt ) { 3454 | 'use strict'; 3455 | 3456 | if ( this.isStatic ) { 3457 | return; 3458 | } 3459 | 3460 | if ( this.paramsArray === null ) { 3461 | this.paramsArray = this.attributes.params.typedArray.array; 3462 | } 3463 | 3464 | var start = this.attributeOffset, 3465 | end = start + this.particleCount, 3466 | params = this.paramsArray, // vec3( alive, age, maxAge, wiggle ) 3467 | ppsDt = this.particlesPerSecond * this.activeMultiplier * dt, 3468 | activationIndex = this.activationIndex; 3469 | 3470 | // Reset the buffer update indices. 3471 | this._resetBufferRanges(); 3472 | 3473 | // Increment age for those particles that are alive, 3474 | // and kill off any particles whose age is over the limit. 3475 | this._checkParticleAges( start, end, params, dt ); 3476 | 3477 | // If the emitter is dead, reset the age of the emitter to zero, 3478 | // ready to go again if required 3479 | if ( this.alive === false ) { 3480 | this.age = 0.0; 3481 | return; 3482 | } 3483 | 3484 | // If the emitter has a specified lifetime and we've exceeded it, 3485 | // mark the emitter as dead. 3486 | if ( this.duration !== null && this.age > this.duration ) { 3487 | this.alive = false; 3488 | this.age = 0.0; 3489 | return; 3490 | } 3491 | 3492 | 3493 | var activationStart = this.particleCount === 1 ? activationIndex : ( activationIndex | 0 ), 3494 | activationEnd = Math.min( activationStart + ppsDt, this.activationEnd ), 3495 | activationCount = activationEnd - this.activationIndex | 0, 3496 | dtPerParticle = activationCount > 0 ? dt / activationCount : 0; 3497 | 3498 | this._activateParticles( activationStart, activationEnd, params, dtPerParticle ); 3499 | 3500 | // Move the activation window forward, soldier. 3501 | this.activationIndex += ppsDt; 3502 | 3503 | if ( this.activationIndex > end ) { 3504 | this.activationIndex = start; 3505 | } 3506 | 3507 | 3508 | // Increment the age of the emitter. 3509 | this.age += dt; 3510 | }; 3511 | 3512 | /** 3513 | * Resets all the emitter's particles to their start positions 3514 | * and marks the particles as dead if the `force` argument is 3515 | * true. 3516 | * 3517 | * @param {Boolean} [force=undefined] If true, all particles will be marked as dead instantly. 3518 | * @return {Emitter} This emitter instance. 3519 | */ 3520 | SPE.Emitter.prototype.reset = function( force ) { 3521 | 'use strict'; 3522 | 3523 | this.age = 0.0; 3524 | this.alive = false; 3525 | 3526 | if ( force === true ) { 3527 | var start = this.attributeOffset, 3528 | end = start + this.particleCount, 3529 | array = this.paramsArray, 3530 | attr = this.attributes.params.bufferAttribute; 3531 | 3532 | for ( var i = end - 1, index; i >= start; --i ) { 3533 | index = i * 4; 3534 | 3535 | array[ index ] = 0.0; 3536 | array[ index + 1 ] = 0.0; 3537 | } 3538 | 3539 | attr.clearUpdateRanges(); 3540 | attr.needsUpdate = true; 3541 | } 3542 | 3543 | return this; 3544 | }; 3545 | 3546 | /** 3547 | * Enables the emitter. If not already enabled, the emitter 3548 | * will start emitting particles. 3549 | * 3550 | * @return {Emitter} This emitter instance. 3551 | */ 3552 | SPE.Emitter.prototype.enable = function() { 3553 | 'use strict'; 3554 | this.alive = true; 3555 | return this; 3556 | }; 3557 | 3558 | /** 3559 | * Disables th emitter, but does not instantly remove it's 3560 | * particles fromt the scene. When called, the emitter will be 3561 | * 'switched off' and just stop emitting. Any particle's alive will 3562 | * be allowed to finish their lifecycle. 3563 | * 3564 | * @return {Emitter} This emitter instance. 3565 | */ 3566 | SPE.Emitter.prototype.disable = function() { 3567 | 'use strict'; 3568 | 3569 | this.alive = false; 3570 | return this; 3571 | }; 3572 | 3573 | /** 3574 | * Remove this emitter from it's parent group (if it has been added to one). 3575 | * Delgates to SPE.group.prototype.removeEmitter(). 3576 | * 3577 | * When called, all particle's belonging to this emitter will be instantly 3578 | * removed from the scene. 3579 | * 3580 | * @return {Emitter} This emitter instance. 3581 | * 3582 | * @see SPE.Group.prototype.removeEmitter 3583 | */ 3584 | SPE.Emitter.prototype.remove = function() { 3585 | 'use strict'; 3586 | if ( this.group !== null ) { 3587 | this.group.removeEmitter( this ); 3588 | } 3589 | else { 3590 | console.error( 'Emitter does not belong to a group, cannot remove.' ); 3591 | } 3592 | 3593 | return this; 3594 | }; 3595 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@c-frame/aframe-particle-system-component", 3 | "version": "1.2.0", 4 | "description": "Particle systems for A-Frame.", 5 | "main": "./index.js", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "devDependencies": { 10 | "semistandard": "^17.0.0", 11 | "snazzy": "^9.0.0", 12 | "webpack": "^5.93.0", 13 | "webpack-cli": "^5.1.4", 14 | "webpack-dev-server": "^5.0.4" 15 | }, 16 | "engines": { 17 | "node": ">= v18.12.0" 18 | }, 19 | "scripts": { 20 | "dev": "webpack serve", 21 | "dist": "webpack && NODE_ENV=production webpack", 22 | "lint": "semistandard -v | snazzy", 23 | "prepublish": "npm run dist", 24 | "start": "npm run dev" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/c-frame/aframe-particle-system-component.git" 29 | }, 30 | "keywords": [ 31 | "ideaspace", 32 | "c-frame", 33 | "webvr", 34 | "aframe-vr", 35 | "aframe-component", 36 | "aframe", 37 | "mozvr", 38 | "vr" 39 | ], 40 | "author": "IdeaSpace (https://www.IdeaSpace.org)", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/c-frame/aframe-particle-system-component/issues" 44 | }, 45 | "homepage": "https://github.com/c-frame/aframe-particle-system-component#readme" 46 | } 47 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // Generated using webpack-cli https://github.com/webpack/webpack-cli 2 | 3 | const path = require('path'); 4 | 5 | const isProduction = process.env.NODE_ENV == 'production'; 6 | 7 | 8 | const config = { 9 | context: __dirname, 10 | entry: './index.js', 11 | externals: { 12 | // Stubs out `import ... from 'three'` so it returns `import ... from window.THREE` effectively using THREE global variable that is defined by AFRAME. 13 | three: 'THREE' 14 | }, 15 | output: { 16 | path: path.resolve(__dirname, 'dist'), 17 | filename: isProduction ? "aframe-particle-system-component.min.js" : "aframe-particle-system-component.js" 18 | }, 19 | plugins: [ 20 | // Add your plugins here 21 | // Learn more about plugins from https://webpack.js.org/configuration/plugins/ 22 | ], 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i, 27 | type: 'asset', 28 | }, 29 | 30 | // Add your rules for custom modules here 31 | // Learn more about loaders from https://webpack.js.org/loaders/ 32 | ], 33 | }, 34 | devtool: 'cheap-module-source-map', 35 | devServer: { 36 | static: { directory: path.join(__dirname), }, 37 | compress: true, 38 | port: process.env.PORT || 8000, 39 | devMiddleware: { publicPath: '/dist' }, 40 | watchFiles: ['index.html', 'lib/SPE.js', 'examples/**/index.html'], 41 | }, 42 | }; 43 | 44 | module.exports = () => { 45 | if (isProduction) { 46 | config.mode = 'production'; 47 | 48 | 49 | } else { 50 | config.mode = 'development'; 51 | } 52 | return config; 53 | }; 54 | --------------------------------------------------------------------------------