@newkrok/three-particles 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,23 +1,21 @@
1
1
  # THREE Particles
2
- Particle sytem for ThreeJS
2
+
3
+ Particle system for ThreeJS
3
4
 
4
5
  # THREE Particles Editor
6
+
5
7
  You can create your own particle effects with it's editor https://github.com/NewKrok/three-particles-editor
6
8
 
7
9
  # Live demo
10
+
8
11
  https://newkrok.com/three-particles-editor/index.html
9
12
 
10
13
  # Install
14
+
11
15
  npm package https://www.npmjs.com/package/@newkrok/three-particles
12
16
 
13
17
  Install with npm
14
18
  `npm i @newkrok/three-particles`
15
19
 
16
20
  Add as a package.json dependency
17
- `
18
- "dependencies": {
19
- ...
20
- "@newkrok/three-particles": "0.0.1"
21
- ...
22
- },
23
- `
21
+ `"dependencies": { ... "@newkrok/three-particles": "0.2.2" ... }, `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newkrok/three-particles",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Particle system for ThreeJS",
5
5
  "main": "src/js/three-particles.js",
6
6
  "bin": {
@@ -22,6 +22,10 @@
22
22
  "url": "https://github.com/NewKrok/three-particles/issues"
23
23
  },
24
24
  "homepage": "https://github.com/NewKrok/three-particles#readme",
25
+ "dependencies": {
26
+ "three": "0.135.0",
27
+ "easing-functions": "1.0.1"
28
+ },
25
29
  "scripts": {
26
30
  "test": "echo \"Error: no test specified\" && exit 1"
27
31
  }
@@ -2,20 +2,30 @@ const ParticleSystemFragmentShader = `
2
2
  uniform sampler2D map;
3
3
  uniform float elapsed;
4
4
  uniform float fps;
5
+ uniform bool useFPSForFrameIndex;
5
6
  uniform vec2 tiles;
6
7
 
7
8
  varying vec4 vColor;
8
- varying float vLifeTime;
9
+ varying float vLifetime;
10
+ varying float vStartLifetime;
9
11
  varying float vRotation;
12
+ varying float vStartFrame;
10
13
 
11
14
  void main()
12
15
  {
13
16
  gl_FragColor = vColor;
14
17
  float mid = 0.5;
15
18
 
16
- float frameIndex = max(min(vLifeTime / 1000.0 * fps, tiles.x * tiles.y - 1.0), 0.0);
19
+ float frameIndex = round(vStartFrame) + (
20
+ useFPSForFrameIndex == true
21
+ ? fps == 0.0
22
+ ? 0.0
23
+ : max((vLifetime / 1000.0) * fps, 0.0)
24
+ : max(min(floor(min(vLifetime / vStartLifetime, 1.0) * (tiles.x * tiles.y)), tiles.x * tiles.y - 1.0), 0.0)
25
+ );
26
+
17
27
  float spriteXIndex = floor(mod(frameIndex, tiles.x));
18
- float spriteYIndex = floor(mod(frameIndex / tiles.y, tiles.y));
28
+ float spriteYIndex = floor(mod(frameIndex / tiles.x, tiles.y));
19
29
 
20
30
  vec2 frameUV = vec2(
21
31
  gl_PointCoord.x / tiles.x + spriteXIndex / tiles.x,
@@ -1,27 +1,33 @@
1
1
  const ParticleSystemVertexShader = `
2
- attribute float startSize;
2
+ attribute float size;
3
3
  attribute float colorR;
4
4
  attribute float colorG;
5
5
  attribute float colorB;
6
6
  attribute float colorA;
7
- attribute float lifeTime;
7
+ attribute float lifetime;
8
+ attribute float startLifetime;
8
9
  attribute float rotation;
10
+ attribute float startFrame;
9
11
 
10
12
  varying mat4 vPosition;
11
13
  varying vec4 vColor;
12
- varying float vLifeTime;
14
+ varying float vLifetime;
15
+ varying float vStartLifetime;
13
16
  varying float vRotation;
17
+ varying float vStartFrame;
14
18
 
15
19
  void main()
16
20
  {
17
21
  vColor = vec4(colorR, colorG, colorB, colorA);
18
- vLifeTime = lifeTime;
22
+ vLifetime = lifetime;
23
+ vStartLifetime = startLifetime;
19
24
  vRotation = rotation;
25
+ vStartFrame = startFrame;
20
26
 
21
27
  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
22
- gl_PointSize = startSize * (100.0 / length(mvPosition.xyz));
28
+ gl_PointSize = size * (100.0 / length(mvPosition.xyz));
23
29
  gl_Position = projectionMatrix * mvPosition;
24
30
  }
25
- `
31
+ `;
26
32
 
27
- export default ParticleSystemVertexShader
33
+ export default ParticleSystemVertexShader;
@@ -0,0 +1,74 @@
1
+ import Easing from "easing-functions";
2
+
3
+ export const CurveFunction = {
4
+ LINEAR: "LINEAR",
5
+ QUADRATIC_IN: "QUADRATIC_IN",
6
+ QUADRATIC_OUT: "QUADRATIC_OUT",
7
+ QUADRATIC_IN_OUT: "QUADRATIC_IN_OUT",
8
+ CUBIC_IN: "CUBIC_IN",
9
+ CUBIC_OUT: "CUBIC_OUT",
10
+ CUBIC_IN_OUT: "CUBIC_IN_OUT",
11
+ QUARTIC_IN: "QUARTIC_IN",
12
+ QUARTIC_OUT: "QUARTIC_OUT",
13
+ QUARTIC_IN_OUT: "QUARTIC_IN_OUT",
14
+ QUINTIC_IN: "QUINTIC_IN",
15
+ QUINTIC_OUT: "QUINTIC_OUT",
16
+ QUINTIC_IN_OUT: "QUINTIC_IN_OUT",
17
+ SINUSOIDAL_IN: "SINUSOIDAL_IN",
18
+ SINUSOIDAL_OUT: "SINUSOIDAL_OUT",
19
+ SINUSOIDAL_IN_OUT: "SINUSOIDAL_IN_OUT",
20
+ EXPONENTIAL_IN: "EXPONENTIAL_IN",
21
+ EXPONENTIAL_OUT: "EXPONENTIAL_OUT",
22
+ EXPONENTIAL_IN_OUT: "EXPONENTIAL_IN_OUT",
23
+ CIRCULAR_IN: "CIRCULAR_IN",
24
+ CIRCULAR_OUT: "CIRCULAR_OUT",
25
+ CIRCULAR_IN_OUT: "CIRCULAR_IN_OUT",
26
+ ELASTIC_IN: "ELASTIC_IN",
27
+ ELASTIC_OUT: "ELASTIC_OUT",
28
+ ELASTIC_IN_OUT: "ELASTIC_IN_OUT",
29
+ BACK_IN: "BACK_IN",
30
+ BACK_OUT: "BACK_OUT",
31
+ BACK_IN_OUT: "BACK_IN_OUT",
32
+ BOUNCE_IN: "BOUNCE_IN",
33
+ BOUNCE_OUT: "BOUNCE_OUT",
34
+ BOUNCE_IN_OUT: "BOUNCE_IN_OUT",
35
+ };
36
+
37
+ const CurveFunctionMap = {
38
+ [CurveFunction.LINEAR]: Easing.Linear.None,
39
+ [CurveFunction.QUADRATIC_IN]: Easing.Quadratic.In,
40
+ [CurveFunction.QUADRATIC_OUT]: Easing.Quadratic.Out,
41
+ [CurveFunction.QUADRATIC_IN_OUT]: Easing.Quadratic.InOut,
42
+ [CurveFunction.CUBIC_IN]: Easing.Cubic.In,
43
+ [CurveFunction.CUBIC_OUT]: Easing.Cubic.Out,
44
+ [CurveFunction.CUBIC_IN_OUT]: Easing.Cubic.InOut,
45
+ [CurveFunction.QUARTIC_IN]: Easing.Quartic.In,
46
+ [CurveFunction.QUARTIC_OUT]: Easing.Quartic.Out,
47
+ [CurveFunction.QUARTIC_IN_OUT]: Easing.Quartic.InOut,
48
+ [CurveFunction.QUINTIC_IN]: Easing.Quintic.In,
49
+ [CurveFunction.QUINTIC_OUT]: Easing.Quintic.Out,
50
+ [CurveFunction.QUINTIC_IN_OUT]: Easing.Quintic.InOut,
51
+ [CurveFunction.SINUSOIDAL_IN]: Easing.Sinusoidal.In,
52
+ [CurveFunction.SINUSOIDAL_OUT]: Easing.Sinusoidal.Out,
53
+ [CurveFunction.SINUSOIDAL_IN_OUT]: Easing.Sinusoidal.InOut,
54
+ [CurveFunction.EXPONENTIAL_IN]: Easing.Exponential.In,
55
+ [CurveFunction.EXPONENTIAL_OUT]: Easing.Exponential.Out,
56
+ [CurveFunction.EXPONENTIAL_IN_OUT]: Easing.Exponential.InOut,
57
+ [CurveFunction.CIRCULAR_IN]: Easing.Circular.In,
58
+ [CurveFunction.CIRCULAR_OUT]: Easing.Circular.Out,
59
+ [CurveFunction.CIRCULAR_IN_OUT]: Easing.Circular.InOut,
60
+ [CurveFunction.ELASTIC_IN]: Easing.Elastic.In,
61
+ [CurveFunction.ELASTIC_OUT]: Easing.Elastic.Out,
62
+ [CurveFunction.ELASTIC_IN_OUT]: Easing.Elastic.InOut,
63
+ [CurveFunction.BACK_IN]: Easing.Back.In,
64
+ [CurveFunction.BACK_OUT]: Easing.Back.Out,
65
+ [CurveFunction.BACK_IN_OUT]: Easing.Back.InOut,
66
+ [CurveFunction.BOUNCE_IN]: Easing.Bounce.In,
67
+ [CurveFunction.BOUNCE_OUT]: Easing.Bounce.Out,
68
+ [CurveFunction.BOUNCE_IN_OUT]: Easing.Bounce.InOut,
69
+ };
70
+
71
+ export const getCurveFunction = (curveFunction) =>
72
+ typeof curveFunction === "function"
73
+ ? curveFunction
74
+ : CurveFunctionMap[curveFunction];
@@ -0,0 +1,44 @@
1
+ import { getCurveFunction } from "./three-particles-curves.js";
2
+
3
+ const modifiers = [
4
+ // {key:"colorOverLifetime", attributeKeys:["colorR", "colorG", "colorB"]},
5
+ {
6
+ key: "opacityOverLifetime",
7
+ attributeKeys: ["colorA"],
8
+ startValueKeys: ["startOpacity"],
9
+ },
10
+ {
11
+ key: "sizeOverLifetime",
12
+ attributeKeys: ["size"],
13
+ startValueKeys: ["startSize"],
14
+ },
15
+ ];
16
+
17
+ export const applyModifiers = ({
18
+ startValues,
19
+ normalizedConfig,
20
+ attributes,
21
+ particleLifetimePercentage,
22
+ particleIndex,
23
+ forceUpdate = false,
24
+ }) => {
25
+ modifiers.forEach(({ key, attributeKeys, startValueKeys }) => {
26
+ const modifier = normalizedConfig[key];
27
+ if (modifier.isActive) {
28
+ const multiplier = getCurveFunction(modifier.curveFunction)(
29
+ particleLifetimePercentage
30
+ );
31
+ attributeKeys.forEach((attributeKey, index) => {
32
+ attributes[attributeKey].array[particleIndex] =
33
+ startValues[startValueKeys[index]][particleIndex] * multiplier;
34
+ attributes[attributeKey].needsUpdate = true;
35
+ });
36
+ } else if (forceUpdate) {
37
+ attributeKeys.forEach((attributeKey, index) => {
38
+ attributes[attributeKey].array[particleIndex] =
39
+ startValues[startValueKeys[index]][particleIndex];
40
+ attributes[attributeKey].needsUpdate = true;
41
+ });
42
+ }
43
+ });
44
+ };
@@ -1,5 +1,24 @@
1
1
  import * as THREE from "three/build/three.module.js";
2
2
 
3
+ export const patchObject = (
4
+ objectA,
5
+ objectB,
6
+ config = { skippedProperties: [], applyToFirstObject: false }
7
+ ) => {
8
+ const result = {};
9
+ Object.keys(objectA).forEach((key) => {
10
+ if (!config.skippedProperties || !config.skippedProperties.includes(key)) {
11
+ if (typeof objectA[key] === "object" && objectA[key] && objectB[key]) {
12
+ result[key] = patchObject(objectA[key], objectB[key], config);
13
+ } else {
14
+ result[key] = objectB[key] === 0 ? 0 : objectB[key] || objectA[key];
15
+ if (config.applyToFirstObject) objectA[key] = result[key];
16
+ }
17
+ }
18
+ });
19
+ return result;
20
+ };
21
+
3
22
  export const calculateRandomPositionAndVelocityOnSphere = (
4
23
  position,
5
24
  velocity,
@@ -81,7 +100,7 @@ export const calculateRandomPositionAndVelocityOnCone = (
81
100
  sinNormalizedAngle *
82
101
  speedMultiplierByPosition *
83
102
  randomizedSpeed,
84
- -Math.cos(normalizedAngle) * randomizedSpeed
103
+ Math.cos(normalizedAngle) * randomizedSpeed
85
104
  );
86
105
  };
87
106
 
@@ -106,12 +125,12 @@ export const calculateRandomPositionAndVelocityOnCircle = (
106
125
  radius * radiusThickness * randomizedDistanceRatio * yDirection;
107
126
  position.z = 0;
108
127
 
109
- const positionLength = position.length();
110
-
111
128
  const randomizedSpeed = THREE.MathUtils.randFloat(
112
129
  startSpeed.min,
113
130
  startSpeed.max
114
131
  );
132
+
133
+ const positionLength = position.length();
115
134
  const speedMultiplierByPosition = 1 / positionLength;
116
135
  velocity.set(
117
136
  position.x * speedMultiplierByPosition * randomizedSpeed,
@@ -119,3 +138,24 @@ export const calculateRandomPositionAndVelocityOnCircle = (
119
138
  0
120
139
  );
121
140
  };
141
+
142
+ export const calculateRandomPositionAndVelocityOnRectangle = (
143
+ position,
144
+ velocity,
145
+ startSpeed,
146
+ { rotation, scale }
147
+ ) => {
148
+ const xOffset = Math.random() * scale.x - scale.x / 2;
149
+ const yOffset = Math.random() * scale.y - scale.y / 2;
150
+ const rotationX = THREE.Math.degToRad(rotation.x);
151
+ const rotationY = THREE.Math.degToRad(rotation.y);
152
+ position.x = xOffset * Math.cos(rotationY);
153
+ position.y = yOffset * Math.cos(rotationX);
154
+ position.z = xOffset * Math.sin(rotationY) - yOffset * Math.sin(rotationX);
155
+
156
+ const randomizedSpeed = THREE.MathUtils.randFloat(
157
+ startSpeed.min,
158
+ startSpeed.max
159
+ );
160
+ velocity.set(0, 0, randomizedSpeed);
161
+ };
@@ -3,11 +3,15 @@ import * as THREE from "three/build/three.module.js";
3
3
  import {
4
4
  calculateRandomPositionAndVelocityOnCircle,
5
5
  calculateRandomPositionAndVelocityOnCone,
6
+ calculateRandomPositionAndVelocityOnRectangle,
6
7
  calculateRandomPositionAndVelocityOnSphere,
7
- } from "./three-particles-utils.js";
8
+ patchObject,
9
+ } from "./three-particles/three-particles-utils.js";
8
10
 
9
- import ParticleSystemFragmentShader from "./shaders/particle-system-fragment-shader.glsl.js";
10
- import ParticleSystemVertexShader from "./shaders/particle-system-vertex-shader.glsl.js";
11
+ import { CurveFunction } from "./three-particles/three-particles-curves.js";
12
+ import ParticleSystemFragmentShader from "./three-particles/shaders/particle-system-fragment-shader.glsl.js";
13
+ import ParticleSystemVertexShader from "./three-particles/shaders/particle-system-vertex-shader.glsl.js";
14
+ import { applyModifiers } from "./three-particles/three-particles-modifiers.js";
11
15
 
12
16
  // Float32Array is not enough accurate when we are storing timestamp in it so we just remove unnecessary time
13
17
  const float32Helper = 1638200000000;
@@ -27,9 +31,81 @@ export const Shape = {
27
31
  RECTANGLE: "RECTANGLE",
28
32
  };
29
33
 
30
- const defaultTextureSheetAnimation = {
31
- tiles: new THREE.Vector2(1.0, 1.0),
32
- fps: 30.0,
34
+ export const TimeMode = {
35
+ LIFETIME: "LIFETIME",
36
+ FPS: "FPS",
37
+ };
38
+
39
+ export const getDefaultParticleSystemConfig = () =>
40
+ JSON.parse(JSON.stringify(DEFAULT_PARTICLE_SYSTEM_CONFIG));
41
+
42
+ const DEFAULT_PARTICLE_SYSTEM_CONFIG = {
43
+ transform: {
44
+ position: { x: 0, y: 0, z: 0 },
45
+ rotation: { x: 0, y: 0, z: 0 },
46
+ scale: { x: 1, y: 1, z: 1 },
47
+ },
48
+ duration: 5.0,
49
+ looping: true,
50
+ startDelay: { min: 0.0, max: 0.0 },
51
+ startLifetime: { min: 2.0, max: 2.0 },
52
+ startSpeed: { min: 1.0, max: 1.0 },
53
+ startSize: { min: 1.0, max: 1.0 },
54
+ startRotation: { min: 0.0, max: 0.0 },
55
+ startColor: {
56
+ min: { r: 1.0, g: 1.0, b: 1.0 },
57
+ max: { r: 1.0, g: 1.0, b: 1.0 },
58
+ },
59
+ startOpacity: { min: 1.0, max: 1.0 },
60
+ gravity: 0.0,
61
+ simulationSpace: SimulationSpace.LOCAL,
62
+ maxParticles: 100.0,
63
+ emission: {
64
+ rateOverTime: 10.0,
65
+ rateOverDistance: 0.0,
66
+ },
67
+ shape: {
68
+ shape: Shape.SPHERE,
69
+ sphere: {
70
+ radius: 1.0,
71
+ radiusThickness: 1.0,
72
+ arc: 360.0,
73
+ },
74
+ cone: {
75
+ angle: 25.0,
76
+ radius: 1.0,
77
+ radiusThickness: 1.0,
78
+ arc: 360.0,
79
+ },
80
+ circle: {
81
+ radius: 1.0,
82
+ radiusThickness: 1.0,
83
+ arc: 360.0,
84
+ },
85
+ rectangle: {
86
+ rotation: { x: 0.0, y: 0.0 }, // TODO: add z rotation
87
+ scale: { x: 1.0, y: 1.0 },
88
+ },
89
+ },
90
+ map: null,
91
+ sizeOverLifetime: {
92
+ isActive: false,
93
+ curveFunction: CurveFunction.LINEAR,
94
+ },
95
+ /* colorOverLifetime: {
96
+ isActive: false,
97
+ curveFunction: CurveFunction.LINEAR,
98
+ }, */
99
+ opacityOverLifetime: {
100
+ isActive: false,
101
+ curveFunction: CurveFunction.LINEAR,
102
+ },
103
+ textureSheetAnimation: {
104
+ tiles: new THREE.Vector2(1.0, 1.0),
105
+ timeMode: TimeMode.LIFETIME,
106
+ fps: 10.0,
107
+ startFrame: { min: 0.0, max: 0.0 },
108
+ },
33
109
  };
34
110
 
35
111
  const createFloat32Attributes = ({
@@ -53,7 +129,7 @@ const createFloat32Attributes = ({
53
129
  };
54
130
 
55
131
  const calculatePositionAndVelocity = (
56
- { shape, sphere, cone, circle },
132
+ { shape, sphere, cone, circle, rectangle },
57
133
  startSpeed,
58
134
  position,
59
135
  velocity
@@ -85,79 +161,57 @@ const calculatePositionAndVelocity = (
85
161
  circle
86
162
  );
87
163
  break;
164
+
165
+ case Shape.RECTANGLE:
166
+ calculateRandomPositionAndVelocityOnRectangle(
167
+ position,
168
+ velocity,
169
+ startSpeed,
170
+ rectangle
171
+ );
172
+ break;
88
173
  }
89
174
  };
90
175
 
91
- export const createParticleSystem = ({
92
- duration = 5.0,
93
- looping = true,
94
- startDelay = { min: 0.0, max: 0.0 },
95
- startLifeTime = { min: 5.0, max: 5.0 },
96
- startSpeed = { min: 5.0, max: 5.0 },
97
- startSize = { min: 1.0, max: 1.0 },
98
- startRotation = { min: 0.0, max: 0.0 },
99
- startColor = {
100
- min: { r: 1.0, g: 1.0, b: 1.0 },
101
- max: { r: 1.0, g: 1.0, b: 1.0 },
102
- },
103
- startOpacity = { min: 1.0, max: 1.0 },
104
- gravity = 0.0,
105
- simulationSpace = SimulationSpace.LOCAL,
106
- maxParticles = 100,
107
- emission = {
108
- rateOverTime: 10.0,
109
- rateOverDistance: 0.0,
110
- },
111
- shape,
112
- map,
113
- onUpdate = null,
114
- onComplete = null,
115
- textureSheetAnimation = defaultTextureSheetAnimation,
116
- }) => {
176
+ export const createParticleSystem = (
177
+ config = DEFAULT_PARTICLE_SYSTEM_CONFIG
178
+ ) => {
117
179
  const now = Date.now();
118
- const lastWorldPosition = new THREE.Vector3(-99999, -99999, -99999);
119
- const worldPositionChange = new THREE.Vector3();
120
- const generalData = { distanceFromLastEmitByDistance: 0 };
121
-
122
- const normalizedStartDelay = { min: 0.0, max: 0.0, ...startDelay };
123
- const normalizedStartLifeTime = { min: 5.0, max: 5.0, ...startLifeTime };
124
- const normalizedStartSpeed = { min: 5.0, max: 5.0, ...startSpeed };
125
- const normalizedStartSize = { min: 1.0, max: 1.0, ...startSize };
126
- const normalizedStartRotation = { min: 0.0, max: 0.0, ...startRotation };
127
- const normalizedStartColor = {
128
- min: { r: 1.0, g: 1.0, b: 1.0, ...startColor.min },
129
- max: { r: 1.0, g: 1.0, b: 1.0, ...startColor.max },
130
- };
131
- const normalizedStartOpacity = { min: 0.0, max: 0.0, ...startOpacity };
132
- const normalizedEmission = {
133
- rateOverTime: 10.0,
134
- rateOverDistance: 0.0,
135
- ...emission,
136
- };
137
- const normalizedShape = {
138
- shape: Shape.SPHERE,
139
- sphere: {
140
- radius: 1.0,
141
- radiusThickness: 1.0,
142
- arc: 360.0,
143
- ...shape.sphere,
144
- },
145
- cone: {
146
- angle: 25,
147
- radius: 1.0,
148
- radiusThickness: 1.0,
149
- arc: 360.0,
150
- ...shape.cone,
151
- },
152
- circle: {
153
- radius: 1.0,
154
- radiusThickness: 1.0,
155
- arc: 360.0,
156
- ...shape.circle,
157
- },
158
- ...shape,
180
+ const generalData = {
181
+ distanceFromLastEmitByDistance: 0,
182
+ lastWorldPosition: new THREE.Vector3(-99999),
183
+ currentWorldPosition: new THREE.Vector3(-99999),
184
+ worldPositionChange: new THREE.Vector3(),
185
+ worldQuaternion: new THREE.Quaternion(),
186
+ lastWorldQuaternion: new THREE.Quaternion(-99999),
187
+ worldEuler: new THREE.Euler(),
188
+ gravityVelocity: new THREE.Vector3(0, 0, 0),
189
+ startValues: {},
159
190
  };
160
191
 
192
+ const normalizedConfig = patchObject(DEFAULT_PARTICLE_SYSTEM_CONFIG, config);
193
+ const {
194
+ transform,
195
+ duration,
196
+ looping,
197
+ startDelay,
198
+ startLifetime,
199
+ startSpeed,
200
+ startSize,
201
+ startRotation,
202
+ startColor,
203
+ startOpacity,
204
+ gravity,
205
+ simulationSpace,
206
+ maxParticles,
207
+ emission,
208
+ shape,
209
+ map,
210
+ onUpdate,
211
+ onComplete,
212
+ textureSheetAnimation,
213
+ } = normalizedConfig;
214
+
161
215
  const startPositions = Array.from(
162
216
  { length: maxParticles },
163
217
  () => new THREE.Vector3()
@@ -167,21 +221,15 @@ export const createParticleSystem = ({
167
221
  () => new THREE.Vector3()
168
222
  );
169
223
 
170
- const rawUniforms = {
171
- ...defaultTextureSheetAnimation,
172
- ...textureSheetAnimation,
173
- };
174
- rawUniforms.tiles = rawUniforms.tiles
175
- ? rawUniforms.tiles
176
- : defaultTextureSheetAnimation.tiles;
177
-
178
- const uniforms = Object.keys(rawUniforms).reduce(
179
- (prev, key) => ({
180
- ...prev,
181
- [key]: { value: rawUniforms[key] },
182
- }),
183
- {}
184
- );
224
+ const startValueKeys = ["startSize", "startOpacity"];
225
+ startValueKeys.forEach((key) => {
226
+ generalData.startValues[key] = Array.from({ length: maxParticles }, () =>
227
+ THREE.MathUtils.randFloat(
228
+ normalizedConfig[key].min,
229
+ normalizedConfig[key].max
230
+ )
231
+ );
232
+ });
185
233
 
186
234
  const material = new THREE.ShaderMaterial({
187
235
  uniforms: {
@@ -191,7 +239,15 @@ export const createParticleSystem = ({
191
239
  map: {
192
240
  value: map,
193
241
  },
194
- ...uniforms,
242
+ tiles: {
243
+ value: textureSheetAnimation.tiles,
244
+ },
245
+ fps: {
246
+ value: textureSheetAnimation.fps,
247
+ },
248
+ useFPSForFrameIndex: {
249
+ value: textureSheetAnimation.timeMode === TimeMode.FPS,
250
+ },
195
251
  },
196
252
  vertexShader: ParticleSystemVertexShader,
197
253
  fragmentShader: ParticleSystemFragmentShader,
@@ -205,8 +261,8 @@ export const createParticleSystem = ({
205
261
 
206
262
  for (let i = 0; i < maxParticles; i++)
207
263
  calculatePositionAndVelocity(
208
- normalizedShape,
209
- normalizedStartSpeed,
264
+ shape,
265
+ startSpeed,
210
266
  startPositions[i],
211
267
  velocities[i]
212
268
  );
@@ -228,39 +284,29 @@ export const createParticleSystem = ({
228
284
 
229
285
  createFloat32AttributesRequest("isActive", false);
230
286
  createFloat32AttributesRequest("creationTime", 0);
231
- createFloat32AttributesRequest("lifeTime", 0);
232
- createFloat32AttributesRequest("startLifeTime", () =>
233
- THREE.MathUtils.randFloat(
234
- normalizedStartLifeTime.min,
235
- normalizedStartLifeTime.max
287
+ createFloat32AttributesRequest("lifetime", 0);
288
+ createFloat32AttributesRequest("startLifetime", () =>
289
+ THREE.MathUtils.randFloat(startLifetime.min, startLifetime.max)
290
+ );
291
+ createFloat32AttributesRequest("startFrame", () =>
292
+ THREE.MathUtils.randInt(
293
+ textureSheetAnimation.startFrame.min,
294
+ textureSheetAnimation.startFrame.max
236
295
  )
237
296
  );
238
297
 
239
298
  createFloat32AttributesRequest("opacity", 0);
240
299
 
241
- createFloat32Attributes({
242
- geometry,
243
- propertyName: "rotation",
244
- maxParticles,
245
- factory: () =>
246
- THREE.Math.degToRad(
247
- THREE.MathUtils.randFloat(
248
- normalizedStartRotation.min,
249
- normalizedStartRotation.max
250
- )
251
- ),
252
- });
300
+ createFloat32AttributesRequest("rotation", () =>
301
+ THREE.Math.degToRad(
302
+ THREE.MathUtils.randFloat(startRotation.min, startRotation.max)
303
+ )
304
+ );
253
305
 
254
- createFloat32Attributes({
255
- geometry,
256
- propertyName: "startSize",
257
- maxParticles,
258
- factory: () =>
259
- THREE.MathUtils.randFloat(
260
- normalizedStartSize.min,
261
- normalizedStartSize.max
262
- ),
263
- });
306
+ createFloat32AttributesRequest(
307
+ "size",
308
+ (_, index) => generalData.startValues.startSize[index]
309
+ );
264
310
 
265
311
  createFloat32AttributesRequest("rotation", 0);
266
312
 
@@ -268,30 +314,25 @@ export const createParticleSystem = ({
268
314
  createFloat32AttributesRequest(
269
315
  "colorR",
270
316
  () =>
271
- normalizedStartColor.min.r +
272
- colorRandomRatio *
273
- (normalizedStartColor.max.r - normalizedStartColor.min.r)
317
+ startColor.min.r +
318
+ colorRandomRatio * (startColor.max.r - startColor.min.r)
274
319
  );
275
320
  createFloat32AttributesRequest(
276
321
  "colorG",
277
322
  () =>
278
- normalizedStartColor.min.g +
279
- colorRandomRatio *
280
- (normalizedStartColor.max.g - normalizedStartColor.min.g)
323
+ startColor.min.g +
324
+ colorRandomRatio * (startColor.max.g - startColor.min.g)
281
325
  );
282
326
  createFloat32AttributesRequest(
283
327
  "colorB",
284
328
  () =>
285
- normalizedStartColor.min.b +
286
- colorRandomRatio *
287
- (normalizedStartColor.max.b - normalizedStartColor.min.b)
329
+ startColor.min.b +
330
+ colorRandomRatio * (startColor.max.b - startColor.min.b)
288
331
  );
289
332
  createFloat32AttributesRequest("colorA", 0);
290
333
 
291
334
  const deactivateParticle = (particleIndex) => {
292
335
  geometry.attributes.isActive.array[particleIndex] = false;
293
- geometry.attributes.lifeTime.array[particleIndex] = 0;
294
- geometry.attributes.lifeTime.needsUpdate = true;
295
336
  geometry.attributes.colorA.array[particleIndex] = 0;
296
337
  geometry.attributes.colorA.needsUpdate = true;
297
338
  };
@@ -304,54 +345,46 @@ export const createParticleSystem = ({
304
345
  const colorRandomRatio = Math.random();
305
346
 
306
347
  geometry.attributes.colorR.array[particleIndex] =
307
- normalizedStartColor.min.r +
308
- colorRandomRatio *
309
- (normalizedStartColor.max.r - normalizedStartColor.min.r);
348
+ startColor.min.r +
349
+ colorRandomRatio * (startColor.max.r - startColor.min.r);
310
350
  geometry.attributes.colorR.needsUpdate = true;
311
351
 
312
352
  geometry.attributes.colorG.array[particleIndex] =
313
- normalizedStartColor.min.g +
314
- colorRandomRatio *
315
- (normalizedStartColor.max.g - normalizedStartColor.min.g);
353
+ startColor.min.g +
354
+ colorRandomRatio * (startColor.max.g - startColor.min.g);
316
355
  geometry.attributes.colorG.needsUpdate = true;
317
356
 
318
357
  geometry.attributes.colorB.array[particleIndex] =
319
- normalizedStartColor.min.b +
320
- colorRandomRatio *
321
- (normalizedStartColor.max.b - normalizedStartColor.min.b);
358
+ startColor.min.b +
359
+ colorRandomRatio * (startColor.max.b - startColor.min.b);
322
360
  geometry.attributes.colorB.needsUpdate = true;
323
361
 
324
- geometry.attributes.colorA.array[particleIndex] = THREE.MathUtils.randFloat(
325
- normalizedStartOpacity.min,
326
- normalizedStartOpacity.max
327
- );
328
- geometry.attributes.colorA.needsUpdate = true;
362
+ geometry.attributes.startFrame.array[particleIndex] =
363
+ THREE.MathUtils.randInt(
364
+ textureSheetAnimation.startFrame.min,
365
+ textureSheetAnimation.startFrame.max
366
+ );
367
+ geometry.attributes.startFrame.needsUpdate = true;
329
368
 
330
- geometry.attributes.startLifeTime.array[particleIndex] =
331
- THREE.MathUtils.randFloat(
332
- normalizedStartLifeTime.min,
333
- normalizedStartLifeTime.max
334
- ) * 1000;
335
- geometry.attributes.startLifeTime.needsUpdate = true;
369
+ geometry.attributes.startLifetime.array[particleIndex] =
370
+ THREE.MathUtils.randFloat(startLifetime.min, startLifetime.max) * 1000;
371
+ geometry.attributes.startLifetime.needsUpdate = true;
336
372
 
337
- geometry.attributes.startSize.array[particleIndex] =
338
- THREE.MathUtils.randFloat(
339
- normalizedStartSize.min,
340
- normalizedStartSize.max
341
- );
342
- geometry.attributes.startSize.needsUpdate = true;
373
+ generalData.startValues.startSize[particleIndex] =
374
+ THREE.MathUtils.randFloat(startSize.min, startSize.max);
375
+ generalData.startValues.startOpacity[particleIndex] =
376
+ THREE.MathUtils.randFloat(startOpacity.min, startOpacity.max);
343
377
 
344
378
  geometry.attributes.rotation.array[particleIndex] = THREE.Math.degToRad(
345
- THREE.MathUtils.randFloat(
346
- normalizedStartRotation.min,
347
- normalizedStartRotation.max
348
- )
379
+ THREE.MathUtils.randFloat(startRotation.min, startRotation.max)
349
380
  );
381
+
350
382
  geometry.attributes.rotation.needsUpdate = true;
383
+ geometry.attributes.colorB.needsUpdate = true;
351
384
 
352
385
  calculatePositionAndVelocity(
353
- normalizedShape,
354
- normalizedStartSpeed,
386
+ shape,
387
+ startSpeed,
355
388
  startPositions[particleIndex],
356
389
  velocities[particleIndex]
357
390
  );
@@ -364,26 +397,34 @@ export const createParticleSystem = ({
364
397
  startPositions[particleIndex].z;
365
398
  particleSystem.geometry.attributes.position.needsUpdate = true;
366
399
 
367
- geometry.attributes.lifeTime.array[particleIndex] = 0;
368
- geometry.attributes.lifeTime.needsUpdate = true;
400
+ geometry.attributes.lifetime.array[particleIndex] = 0;
401
+ geometry.attributes.lifetime.needsUpdate = true;
402
+
403
+ applyModifiers({
404
+ startValues: generalData.startValues,
405
+ normalizedConfig,
406
+ attributes: particleSystem.geometry.attributes,
407
+ particleLifetimePercentage: 0,
408
+ particleIndex,
409
+ forceUpdate: true,
410
+ });
369
411
  };
370
412
 
371
413
  const particleSystem = new THREE.Points(geometry, material);
372
414
  particleSystem.sortParticles = true;
373
415
 
416
+ particleSystem.position.copy(transform.position);
417
+ particleSystem.rotation.x = THREE.Math.degToRad(transform.rotation.x);
418
+ particleSystem.rotation.y = THREE.Math.degToRad(transform.rotation.y);
419
+ particleSystem.rotation.z = THREE.Math.degToRad(transform.rotation.z);
420
+ particleSystem.scale.copy(transform.scale);
421
+
374
422
  const calculatedCreationTime =
375
- now +
376
- THREE.MathUtils.randFloat(
377
- normalizedStartDelay.min,
378
- normalizedStartDelay.max
379
- ) *
380
- 1000;
423
+ now + THREE.MathUtils.randFloat(startDelay.min, startDelay.max) * 1000;
381
424
 
382
425
  createdParticleSystems.push({
383
426
  particleSystem,
384
427
  generalData,
385
- lastWorldPosition,
386
- worldPositionChange,
387
428
  onUpdate,
388
429
  onComplete,
389
430
  creationTime: calculatedCreationTime,
@@ -392,7 +433,8 @@ export const createParticleSystem = ({
392
433
  looping,
393
434
  simulationSpace,
394
435
  gravity,
395
- emission: normalizedEmission,
436
+ emission,
437
+ normalizedConfig,
396
438
  iterationCount: 0,
397
439
  velocities,
398
440
  deactivateParticle,
@@ -412,14 +454,11 @@ export const destroyParticleSystem = (particleSystem) => {
412
454
  particleSystem.parent.remove(particleSystem);
413
455
  };
414
456
 
415
- export const updateParticleSystems = ({ delta, elapsed }) => {
416
- const now = Date.now();
457
+ export const updateParticleSystems = ({ now, delta, elapsed }) => {
417
458
  createdParticleSystems.forEach((props) => {
418
459
  const {
419
460
  onUpdate,
420
461
  generalData,
421
- lastWorldPosition,
422
- worldPositionChange,
423
462
  onComplete,
424
463
  particleSystem,
425
464
  creationTime,
@@ -427,6 +466,7 @@ export const updateParticleSystems = ({ delta, elapsed }) => {
427
466
  duration,
428
467
  looping,
429
468
  emission,
469
+ normalizedConfig,
430
470
  iterationCount,
431
471
  velocities,
432
472
  deactivateParticle,
@@ -434,34 +474,65 @@ export const updateParticleSystems = ({ delta, elapsed }) => {
434
474
  simulationSpace,
435
475
  gravity,
436
476
  } = props;
437
- const lifeTime = now - creationTime;
477
+
478
+ const {
479
+ lastWorldPosition,
480
+ currentWorldPosition,
481
+ worldPositionChange,
482
+ lastWorldQuaternion,
483
+ worldQuaternion,
484
+ worldEuler,
485
+ gravityVelocity,
486
+ } = generalData;
487
+
488
+ const lifetime = now - creationTime;
438
489
  particleSystem.material.uniforms.elapsed.value = elapsed;
439
490
 
440
- if (
441
- lastWorldPosition.x !== -99999 &&
442
- lastWorldPosition.y !== -99999 &&
443
- lastWorldPosition.z !== -99999
444
- )
491
+ particleSystem.getWorldPosition(currentWorldPosition);
492
+ if (lastWorldPosition.x !== -99999)
445
493
  worldPositionChange.set(
446
- particleSystem.position.x - lastWorldPosition.x,
447
- particleSystem.position.y - lastWorldPosition.y,
448
- particleSystem.position.z - lastWorldPosition.z
494
+ currentWorldPosition.x - lastWorldPosition.x,
495
+ currentWorldPosition.y - lastWorldPosition.y,
496
+ currentWorldPosition.z - lastWorldPosition.z
449
497
  );
450
498
  generalData.distanceFromLastEmitByDistance += worldPositionChange.length();
451
- lastWorldPosition.copy(particleSystem.position);
499
+ particleSystem.getWorldPosition(lastWorldPosition);
500
+
501
+ particleSystem.getWorldQuaternion(worldQuaternion);
502
+ if (
503
+ lastWorldQuaternion.x === -99999 ||
504
+ lastWorldQuaternion.x != worldQuaternion.x ||
505
+ lastWorldQuaternion.y != worldQuaternion.y ||
506
+ lastWorldQuaternion.z != worldQuaternion.z
507
+ ) {
508
+ worldEuler.setFromQuaternion(worldQuaternion);
509
+ lastWorldQuaternion.copy(worldQuaternion);
510
+
511
+ const tempPosX = particleSystem.position.x;
512
+ const tempPosY = particleSystem.position.y;
513
+ const tempPosZ = particleSystem.position.z;
514
+ gravityVelocity.set(0, gravity, 0);
515
+ particleSystem.position.set(0, 0, 0);
516
+ particleSystem.updateMatrixWorld();
517
+ particleSystem.worldToLocal(gravityVelocity);
518
+ particleSystem.position.set(tempPosX, tempPosY, tempPosZ);
519
+ particleSystem.updateMatrixWorld();
520
+ }
452
521
 
453
522
  particleSystem.geometry.attributes.creationTime.array.forEach(
454
523
  (entry, index) => {
455
524
  if (particleSystem.geometry.attributes.isActive.array[index]) {
456
- const particleLifeTime = now - float32Helper - entry;
525
+ const particleLifetime = now - float32Helper - entry;
457
526
  if (
458
- particleLifeTime >
459
- particleSystem.geometry.attributes.startLifeTime.array[index]
527
+ particleLifetime >
528
+ particleSystem.geometry.attributes.startLifetime.array[index]
460
529
  )
461
530
  deactivateParticle(index);
462
531
  else {
463
532
  const velocity = velocities[index];
464
- velocity.y -= gravity;
533
+ velocity.x -= gravityVelocity.x;
534
+ velocity.y -= gravityVelocity.y;
535
+ velocity.z -= gravityVelocity.z;
465
536
 
466
537
  if (
467
538
  gravity !== 0 ||
@@ -483,22 +554,26 @@ export const updateParticleSystems = ({ delta, elapsed }) => {
483
554
  particleSystem.geometry.attributes.position.needsUpdate = true;
484
555
  }
485
556
 
486
- particleSystem.geometry.attributes.lifeTime.array[index] =
487
- particleLifeTime;
488
- particleSystem.geometry.attributes.lifeTime.needsUpdate = true;
489
-
490
- // TEMP
491
- particleSystem.geometry.attributes.colorA.array[index] =
492
- 1 -
493
- particleLifeTime /
494
- particleSystem.geometry.attributes.startLifeTime.array[index];
495
- particleSystem.geometry.attributes.colorA.needsUpdate = true;
557
+ particleSystem.geometry.attributes.lifetime.array[index] =
558
+ particleLifetime;
559
+ particleSystem.geometry.attributes.lifetime.needsUpdate = true;
560
+
561
+ const particleLifetimePercentage =
562
+ particleLifetime /
563
+ particleSystem.geometry.attributes.startLifetime.array[index];
564
+ applyModifiers({
565
+ startValues: generalData.startValues,
566
+ normalizedConfig,
567
+ attributes: particleSystem.geometry.attributes,
568
+ particleLifetimePercentage,
569
+ particleIndex: index,
570
+ });
496
571
  }
497
572
  }
498
573
  }
499
574
  );
500
575
 
501
- if (looping || lifeTime < duration * 1000) {
576
+ if (looping || lifetime < duration * 1000) {
502
577
  const emissionDelta = now - lastEmissionTime;
503
578
  const neededParticlesByTime = Math.floor(
504
579
  emission.rateOverTime * (emissionDelta / 1000)
@@ -546,7 +621,7 @@ export const updateParticleSystems = ({ delta, elapsed }) => {
546
621
  particleSystem,
547
622
  delta,
548
623
  elapsed,
549
- lifeTime,
624
+ lifetime,
550
625
  iterationCount: iterationCount + 1,
551
626
  });
552
627
  } else if (onComplete)