├── .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 | 
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 |
--------------------------------------------------------------------------------