@newkrok/three-particles 0.3.1 → 0.4.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
@@ -18,4 +18,4 @@ Install with npm
18
18
  `npm i @newkrok/three-particles`
19
19
 
20
20
  Add as a package.json dependency
21
- `"dependencies": { ... "@newkrok/three-particles": "0.2.2" ... }, `
21
+ `"dependencies": { ... "@newkrok/three-particles": "0.4.0" ... }, `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newkrok/three-particles",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Particle system for ThreeJS",
5
5
  "main": "src/js/three-particles.js",
6
6
  "bin": {
@@ -23,8 +23,9 @@
23
23
  },
24
24
  "homepage": "https://github.com/NewKrok/three-particles#readme",
25
25
  "dependencies": {
26
- "three": "0.135.0",
27
- "easing-functions": "1.0.1"
26
+ "easing-functions": "1.0.1",
27
+ "three": "0.136.0",
28
+ "three-noise": "^1.1.1"
28
29
  },
29
30
  "scripts": {
30
31
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -1,6 +1,11 @@
1
+ import * as THREE from "three/build/three.module.js";
2
+
1
3
  import { getCurveFunction } from "./three-particles-curves.js";
4
+ import { size } from "lodash";
5
+
6
+ const noiseInput = new THREE.Vector3(0, 0, 0);
2
7
 
3
- const modifiers = [
8
+ const curveModifiers = [
4
9
  // {key:"colorOverLifetime", attributeKeys:["colorR", "colorG", "colorB"]},
5
10
  {
6
11
  key: "opacityOverLifetime",
@@ -15,17 +20,20 @@ const modifiers = [
15
20
  ];
16
21
 
17
22
  export const applyModifiers = ({
23
+ delta,
24
+ noise,
18
25
  startValues,
26
+ lifetimeValues,
19
27
  normalizedConfig,
20
28
  attributes,
21
29
  particleLifetimePercentage,
22
30
  particleIndex,
23
31
  forceUpdate = false,
24
32
  }) => {
25
- modifiers.forEach(({ key, attributeKeys, startValueKeys }) => {
26
- const modifier = normalizedConfig[key];
27
- if (modifier.isActive) {
28
- const multiplier = getCurveFunction(modifier.curveFunction)(
33
+ curveModifiers.forEach(({ key, attributeKeys, startValueKeys }) => {
34
+ const curveModifier = normalizedConfig[key];
35
+ if (curveModifier.isActive) {
36
+ const multiplier = getCurveFunction(curveModifier.curveFunction)(
29
37
  particleLifetimePercentage
30
38
  );
31
39
  attributeKeys.forEach((attributeKey, index) => {
@@ -41,4 +49,58 @@ export const applyModifiers = ({
41
49
  });
42
50
  }
43
51
  });
52
+
53
+ if (lifetimeValues.rotationOverLifetime) {
54
+ attributes.rotation.array[particleIndex] +=
55
+ lifetimeValues.rotationOverLifetime[particleIndex] * delta * 0.02;
56
+ attributes.rotation.needsUpdate = true;
57
+ }
58
+
59
+ if (noise.isActive) {
60
+ const {
61
+ sampler,
62
+ strength,
63
+ offsets,
64
+ positionAmount,
65
+ rotationAmount,
66
+ sizeAmount,
67
+ } = noise;
68
+ const positionIndex = particleIndex * 3;
69
+ const positionArr = attributes.position.array;
70
+ let noiseOnPosition;
71
+
72
+ const noisePosition =
73
+ (particleLifetimePercentage + (offsets ? offsets[particleIndex] : 0)) *
74
+ 10 *
75
+ strength;
76
+ const noisePower = 0.15 * strength;
77
+
78
+ noiseInput.set(noisePosition, 0, 0);
79
+ noiseOnPosition = sampler.get3(noiseInput);
80
+ positionArr[positionIndex] += noiseOnPosition * noisePower * positionAmount;
81
+
82
+ if (rotationAmount !== 0) {
83
+ attributes.rotation.array[particleIndex] +=
84
+ noiseOnPosition * noisePower * rotationAmount;
85
+ attributes.rotation.needsUpdate = true;
86
+ }
87
+
88
+ if (sizeAmount !== 0) {
89
+ attributes.size.array[particleIndex] +=
90
+ noiseOnPosition * noisePower * sizeAmount;
91
+ attributes.size.needsUpdate = true;
92
+ }
93
+
94
+ noiseInput.set(noisePosition, noisePosition, 0);
95
+ noiseOnPosition = sampler.get3(noiseInput);
96
+ positionArr[positionIndex + 1] +=
97
+ noiseOnPosition * noisePower * positionAmount;
98
+
99
+ noiseInput.set(noisePosition, noisePosition, noisePosition);
100
+ noiseOnPosition = sampler.get3(noiseInput);
101
+ positionArr[positionIndex + 2] +=
102
+ noiseOnPosition * noisePower * positionAmount;
103
+
104
+ attributes.position.needsUpdate = true;
105
+ }
44
106
  };
@@ -11,7 +11,12 @@ export const patchObject = (
11
11
  if (typeof objectA[key] === "object" && objectA[key] && objectB[key]) {
12
12
  result[key] = patchObject(objectA[key], objectB[key], config);
13
13
  } else {
14
- result[key] = objectB[key] === 0 ? 0 : objectB[key] || objectA[key];
14
+ result[key] =
15
+ objectB[key] === 0
16
+ ? 0
17
+ : objectB[key] === false
18
+ ? false
19
+ : objectB[key] || objectA[key];
15
20
  if (config.applyToFirstObject) objectA[key] = result[key];
16
21
  }
17
22
  }
@@ -9,13 +9,11 @@ import {
9
9
  } from "./three-particles/three-particles-utils.js";
10
10
 
11
11
  import { CurveFunction } from "./three-particles/three-particles-curves.js";
12
+ import { FBM } from "three-noise/build/three-noise.module.js";
12
13
  import ParticleSystemFragmentShader from "./three-particles/shaders/particle-system-fragment-shader.glsl.js";
13
14
  import ParticleSystemVertexShader from "./three-particles/shaders/particle-system-vertex-shader.glsl.js";
14
15
  import { applyModifiers } from "./three-particles/three-particles-modifiers.js";
15
16
 
16
- // Float32Array is not enough accurate when we are storing timestamp in it so we just remove unnecessary time
17
- const float32Helper = 1638200000000;
18
-
19
17
  let createdParticleSystems = [];
20
18
 
21
19
  export const SimulationSpace = {
@@ -36,6 +34,14 @@ export const TimeMode = {
36
34
  FPS: "FPS",
37
35
  };
38
36
 
37
+ export const blendingMap = {
38
+ "THREE.NoBlending": THREE.NoBlending,
39
+ "THREE.NormalBlending": THREE.NormalBlending,
40
+ "THREE.AdditiveBlending": THREE.AdditiveBlending,
41
+ "THREE.SubtractiveBlending": THREE.SubtractiveBlending,
42
+ "THREE.MultiplyBlending": THREE.MultiplyBlending,
43
+ };
44
+
39
45
  export const getDefaultParticleSystemConfig = () =>
40
46
  JSON.parse(JSON.stringify(DEFAULT_PARTICLE_SYSTEM_CONFIG));
41
47
 
@@ -88,6 +94,25 @@ const DEFAULT_PARTICLE_SYSTEM_CONFIG = {
88
94
  },
89
95
  },
90
96
  map: null,
97
+ renderer: {
98
+ blending: THREE.THREE.NormalBlending,
99
+ transparent: true,
100
+ depthTest: true,
101
+ depthWrite: false,
102
+ },
103
+ velocityOverLifetime: {
104
+ isActive: false,
105
+ linear: {
106
+ x: { min: 0, max: 0 },
107
+ y: { min: 0, max: 0 },
108
+ z: { min: 0, max: 0 },
109
+ },
110
+ orbital: {
111
+ x: { min: 0, max: 0 },
112
+ y: { min: 0, max: 0 },
113
+ z: { min: 0, max: 0 },
114
+ },
115
+ },
91
116
  sizeOverLifetime: {
92
117
  isActive: false,
93
118
  curveFunction: CurveFunction.LINEAR,
@@ -100,6 +125,21 @@ const DEFAULT_PARTICLE_SYSTEM_CONFIG = {
100
125
  isActive: false,
101
126
  curveFunction: CurveFunction.LINEAR,
102
127
  },
128
+ rotationOverLifetime: {
129
+ isActive: false,
130
+ min: 0.0,
131
+ max: 0.0,
132
+ },
133
+ noise: {
134
+ isActive: false,
135
+ useRandomOffset: false,
136
+ strength: 1.0,
137
+ frequency: 0.5,
138
+ octaves: 1,
139
+ positionAmount: 1.0,
140
+ rotationAmount: 0.0,
141
+ sizeAmount: 0.0,
142
+ },
103
143
  textureSheetAnimation: {
104
144
  tiles: new THREE.Vector2(1.0, 1.0),
105
145
  timeMode: TimeMode.LIFETIME,
@@ -132,7 +172,8 @@ const calculatePositionAndVelocity = (
132
172
  { shape, sphere, cone, circle, rectangle },
133
173
  startSpeed,
134
174
  position,
135
- velocity
175
+ velocity,
176
+ velocityOverLifetime
136
177
  ) => {
137
178
  switch (shape) {
138
179
  case Shape.SPHERE:
@@ -171,6 +212,21 @@ const calculatePositionAndVelocity = (
171
212
  );
172
213
  break;
173
214
  }
215
+
216
+ if (velocityOverLifetime.isActive) {
217
+ velocity.x += THREE.MathUtils.randFloat(
218
+ velocityOverLifetime.linear.x.min,
219
+ velocityOverLifetime.linear.x.max
220
+ );
221
+ velocity.y += THREE.MathUtils.randFloat(
222
+ velocityOverLifetime.linear.y.min,
223
+ velocityOverLifetime.linear.y.max
224
+ );
225
+ velocity.z += THREE.MathUtils.randFloat(
226
+ velocityOverLifetime.linear.z.min,
227
+ velocityOverLifetime.linear.z.max
228
+ );
229
+ }
174
230
  };
175
231
 
176
232
  export const createParticleSystem = (
@@ -187,6 +243,9 @@ export const createParticleSystem = (
187
243
  worldEuler: new THREE.Euler(),
188
244
  gravityVelocity: new THREE.Vector3(0, 0, 0),
189
245
  startValues: {},
246
+ lifetimeValues: {},
247
+ creationTimes: [],
248
+ noise: null,
190
249
  };
191
250
 
192
251
  const normalizedConfig = patchObject(DEFAULT_PARTICLE_SYSTEM_CONFIG, config);
@@ -207,11 +266,17 @@ export const createParticleSystem = (
207
266
  emission,
208
267
  shape,
209
268
  map,
269
+ renderer,
270
+ noise,
271
+ velocityOverLifetime,
210
272
  onUpdate,
211
273
  onComplete,
212
274
  textureSheetAnimation,
213
275
  } = normalizedConfig;
214
276
 
277
+ if (typeof renderer.blending === "string")
278
+ renderer.blending = blendingMap[renderer.blending];
279
+
215
280
  const startPositions = Array.from(
216
281
  { length: maxParticles },
217
282
  () => new THREE.Vector3()
@@ -221,6 +286,8 @@ export const createParticleSystem = (
221
286
  () => new THREE.Vector3()
222
287
  );
223
288
 
289
+ generalData.creationTimes = Array.from({ length: maxParticles }, () => 0);
290
+
224
291
  const startValueKeys = ["startSize", "startOpacity"];
225
292
  startValueKeys.forEach((key) => {
226
293
  generalData.startValues[key] = Array.from({ length: maxParticles }, () =>
@@ -231,6 +298,37 @@ export const createParticleSystem = (
231
298
  );
232
299
  });
233
300
 
301
+ const lifetimeValueKeys = ["rotationOverLifetime"];
302
+ lifetimeValueKeys.forEach((key) => {
303
+ if (normalizedConfig[key].isActive)
304
+ generalData.lifetimeValues[key] = Array.from(
305
+ { length: maxParticles },
306
+ () =>
307
+ THREE.MathUtils.randFloat(
308
+ normalizedConfig[key].min,
309
+ normalizedConfig[key].max
310
+ )
311
+ );
312
+ });
313
+
314
+ generalData.noise = {
315
+ isActive: noise.isActive,
316
+ strength: noise.strength,
317
+ positionAmount: noise.positionAmount,
318
+ rotationAmount: noise.rotationAmount,
319
+ sizeAmount: noise.sizeAmount,
320
+ sampler: noise.isActive
321
+ ? new FBM({
322
+ seed: Math.random(),
323
+ scale: noise.frequency,
324
+ octaves: noise.octaves,
325
+ })
326
+ : null,
327
+ offsets: noise.useRandomOffset
328
+ ? Array.from({ length: maxParticles }, () => Math.random() * 100)
329
+ : null,
330
+ };
331
+
234
332
  const material = new THREE.ShaderMaterial({
235
333
  uniforms: {
236
334
  elapsed: {
@@ -251,10 +349,10 @@ export const createParticleSystem = (
251
349
  },
252
350
  vertexShader: ParticleSystemVertexShader,
253
351
  fragmentShader: ParticleSystemFragmentShader,
254
- transparent: true,
255
- blending: THREE.AdditiveBlending,
256
- depthTest: true,
257
- depthWrite: false,
352
+ transparent: renderer.transparent,
353
+ blending: renderer.blending,
354
+ depthTest: renderer.depthTest,
355
+ depthWrite: renderer.depthWrite,
258
356
  });
259
357
 
260
358
  const geometry = new THREE.BufferGeometry();
@@ -264,7 +362,8 @@ export const createParticleSystem = (
264
362
  shape,
265
363
  startSpeed,
266
364
  startPositions[i],
267
- velocities[i]
365
+ velocities[i],
366
+ velocityOverLifetime
268
367
  );
269
368
 
270
369
  geometry.setFromPoints(
@@ -283,7 +382,6 @@ export const createParticleSystem = (
283
382
  };
284
383
 
285
384
  createFloat32AttributesRequest("isActive", false);
286
- createFloat32AttributesRequest("creationTime", 0);
287
385
  createFloat32AttributesRequest("lifetime", 0);
288
386
  createFloat32AttributesRequest("startLifetime", () =>
289
387
  THREE.MathUtils.randFloat(startLifetime.min, startLifetime.max)
@@ -339,8 +437,10 @@ export const createParticleSystem = (
339
437
 
340
438
  const activateParticle = ({ particleIndex, activationTime }) => {
341
439
  geometry.attributes.isActive.array[particleIndex] = true;
342
- geometry.attributes.creationTime.array[particleIndex] =
343
- activationTime - float32Helper;
440
+ generalData.creationTimes[particleIndex] = activationTime;
441
+
442
+ if (generalData.noise.offsets)
443
+ generalData.noise.offsets[particleIndex] = Math.random() * 100;
344
444
 
345
445
  const colorRandomRatio = Math.random();
346
446
 
@@ -379,6 +479,13 @@ export const createParticleSystem = (
379
479
  THREE.MathUtils.randFloat(startRotation.min, startRotation.max)
380
480
  );
381
481
 
482
+ if (normalizedConfig.rotationOverLifetime.isActive)
483
+ generalData.lifetimeValues.rotationOverLifetime[particleIndex] =
484
+ THREE.MathUtils.randFloat(
485
+ normalizedConfig.rotationOverLifetime.min,
486
+ normalizedConfig.rotationOverLifetime.max
487
+ );
488
+
382
489
  geometry.attributes.rotation.needsUpdate = true;
383
490
  geometry.attributes.colorB.needsUpdate = true;
384
491
 
@@ -386,7 +493,8 @@ export const createParticleSystem = (
386
493
  shape,
387
494
  startSpeed,
388
495
  startPositions[particleIndex],
389
- velocities[particleIndex]
496
+ velocities[particleIndex],
497
+ velocityOverLifetime
390
498
  );
391
499
  const positionIndex = Math.floor(particleIndex * 3);
392
500
  geometry.attributes.position.array[positionIndex] =
@@ -395,13 +503,16 @@ export const createParticleSystem = (
395
503
  startPositions[particleIndex].y;
396
504
  geometry.attributes.position.array[positionIndex + 2] =
397
505
  startPositions[particleIndex].z;
398
- particleSystem.geometry.attributes.position.needsUpdate = true;
399
506
 
400
507
  geometry.attributes.lifetime.array[particleIndex] = 0;
401
508
  geometry.attributes.lifetime.needsUpdate = true;
402
509
 
403
510
  applyModifiers({
511
+ delta: 0,
512
+ elapsed: 0,
513
+ noise: generalData.noise,
404
514
  startValues: generalData.startValues,
515
+ lifetimeValues: generalData.lifetimeValues,
405
516
  normalizedConfig,
406
517
  attributes: particleSystem.geometry.attributes,
407
518
  particleLifetimePercentage: 0,
@@ -519,59 +630,61 @@ export const updateParticleSystems = ({ now, delta, elapsed }) => {
519
630
  particleSystem.updateMatrixWorld();
520
631
  }
521
632
 
522
- particleSystem.geometry.attributes.creationTime.array.forEach(
523
- (entry, index) => {
524
- if (particleSystem.geometry.attributes.isActive.array[index]) {
525
- const particleLifetime = now - float32Helper - entry;
633
+ generalData.creationTimes.forEach((entry, index) => {
634
+ if (particleSystem.geometry.attributes.isActive.array[index]) {
635
+ const particleLifetime = now - entry;
636
+ if (
637
+ particleLifetime >
638
+ particleSystem.geometry.attributes.startLifetime.array[index]
639
+ )
640
+ deactivateParticle(index);
641
+ else {
642
+ const velocity = velocities[index];
643
+ velocity.x -= gravityVelocity.x;
644
+ velocity.y -= gravityVelocity.y;
645
+ velocity.z -= gravityVelocity.z;
646
+
526
647
  if (
527
- particleLifetime >
528
- particleSystem.geometry.attributes.startLifetime.array[index]
529
- )
530
- deactivateParticle(index);
531
- else {
532
- const velocity = velocities[index];
533
- velocity.x -= gravityVelocity.x;
534
- velocity.y -= gravityVelocity.y;
535
- velocity.z -= gravityVelocity.z;
536
-
537
- if (
538
- gravity !== 0 ||
539
- velocity.x !== 0 ||
540
- velocity.y !== 0 ||
541
- velocity.z !== 0
542
- ) {
543
- const positionIndex = index * 3;
544
- const positionArr =
545
- particleSystem.geometry.attributes.position.array;
546
- if (simulationSpace === SimulationSpace.WORLD) {
547
- positionArr[positionIndex] -= worldPositionChange.x;
548
- positionArr[positionIndex + 1] -= worldPositionChange.y;
549
- positionArr[positionIndex + 2] -= worldPositionChange.z;
550
- }
551
- positionArr[positionIndex] += velocity.x * delta;
552
- positionArr[positionIndex + 1] += velocity.y * delta;
553
- positionArr[positionIndex + 2] += velocity.z * delta;
554
- particleSystem.geometry.attributes.position.needsUpdate = true;
648
+ gravity !== 0 ||
649
+ velocity.x !== 0 ||
650
+ velocity.y !== 0 ||
651
+ velocity.z !== 0
652
+ ) {
653
+ const positionIndex = index * 3;
654
+ const positionArr =
655
+ particleSystem.geometry.attributes.position.array;
656
+ if (simulationSpace === SimulationSpace.WORLD) {
657
+ positionArr[positionIndex] -= worldPositionChange.x;
658
+ positionArr[positionIndex + 1] -= worldPositionChange.y;
659
+ positionArr[positionIndex + 2] -= worldPositionChange.z;
555
660
  }
556
-
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
- });
661
+ positionArr[positionIndex] += velocity.x * delta;
662
+ positionArr[positionIndex + 1] += velocity.y * delta;
663
+ positionArr[positionIndex + 2] += velocity.z * delta;
664
+ particleSystem.geometry.attributes.position.needsUpdate = true;
571
665
  }
666
+
667
+ particleSystem.geometry.attributes.lifetime.array[index] =
668
+ particleLifetime;
669
+ particleSystem.geometry.attributes.lifetime.needsUpdate = true;
670
+
671
+ const particleLifetimePercentage =
672
+ particleLifetime /
673
+ particleSystem.geometry.attributes.startLifetime.array[index];
674
+ applyModifiers({
675
+ delta,
676
+ elapsed,
677
+ noise: generalData.noise,
678
+ startValues: generalData.startValues,
679
+ lifetimeValues: generalData.lifetimeValues,
680
+ normalizedConfig,
681
+ attributes: particleSystem.geometry.attributes,
682
+ particleLifetimePercentage,
683
+ particleIndex: index,
684
+ });
572
685
  }
573
686
  }
574
- );
687
+ });
575
688
 
576
689
  if (looping || lifetime < duration * 1000) {
577
690
  const emissionDelta = now - lastEmissionTime;