@newkrok/three-particles 0.4.1 → 0.6.2
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 +5 -1
- package/package.json +5 -2
- package/src/js/effects/three-particles/three-particles-bezier.js +36 -0
- package/src/js/effects/three-particles/three-particles-curves.js +1 -0
- package/src/js/effects/three-particles/three-particles-modifiers.js +1 -2
- package/src/js/effects/three-particles/three-particles-utils.js +57 -2
- package/src/js/effects/three-particles.js +99 -22
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@ Particle system for ThreeJS
|
|
|
6
6
|
|
|
7
7
|
You can create your own particle effects with it's editor https://github.com/NewKrok/three-particles-editor
|
|
8
8
|
|
|
9
|
+
# Video
|
|
10
|
+
- Projectiles: https://youtu.be/Q352JuxON04
|
|
11
|
+
- First preview: https://youtu.be/dtN_bndvoGU
|
|
12
|
+
|
|
9
13
|
# Live demo
|
|
10
14
|
|
|
11
15
|
https://newkrok.com/three-particles-editor/index.html
|
|
@@ -18,4 +22,4 @@ Install with npm
|
|
|
18
22
|
`npm i @newkrok/three-particles`
|
|
19
23
|
|
|
20
24
|
Add as a package.json dependency
|
|
21
|
-
`"dependencies": { ... "@newkrok/three-particles": "0.
|
|
25
|
+
`"dependencies": { ... "@newkrok/three-particles": "0.6.2" ... }, `
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@newkrok/three-particles",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Particle system for ThreeJS",
|
|
5
5
|
"main": "src/js/three-particles.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"threejs",
|
|
15
|
+
"particle",
|
|
15
16
|
"particles",
|
|
17
|
+
"particle engine",
|
|
18
|
+
"effects",
|
|
16
19
|
"3d",
|
|
17
20
|
"lib"
|
|
18
21
|
],
|
|
@@ -24,7 +27,7 @@
|
|
|
24
27
|
"homepage": "https://github.com/NewKrok/three-particles#readme",
|
|
25
28
|
"dependencies": {
|
|
26
29
|
"easing-functions": "1.0.1",
|
|
27
|
-
"three": "0.
|
|
30
|
+
"three": "0.137.0",
|
|
28
31
|
"three-noise": "^1.1.1"
|
|
29
32
|
},
|
|
30
33
|
"scripts": {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const nCr = (n, k) => {
|
|
2
|
+
let z = 1;
|
|
3
|
+
for (let i = 1; i <= k; i++) z *= (n + 1 - i) / i;
|
|
4
|
+
return z;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const createBezierCurveFunction = (bezierPoints) => (percentage) => {
|
|
8
|
+
if (percentage < 0) return bezierPoints[0];
|
|
9
|
+
if (percentage > 1) return bezierPoints[bezierPoints.length - 1];
|
|
10
|
+
|
|
11
|
+
let start = 0;
|
|
12
|
+
let stop = bezierPoints.length - 1;
|
|
13
|
+
|
|
14
|
+
bezierPoints.find((point, index) => {
|
|
15
|
+
const result = percentage < point.percentage;
|
|
16
|
+
if (result) stop = index;
|
|
17
|
+
else if (point.percentage !== undefined) start = index;
|
|
18
|
+
return result;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const n = stop - start;
|
|
22
|
+
const calculatedPercentage =
|
|
23
|
+
(percentage - bezierPoints[start].percentage) /
|
|
24
|
+
(bezierPoints[stop].percentage - bezierPoints[start].percentage);
|
|
25
|
+
|
|
26
|
+
let value = 0;
|
|
27
|
+
for (let i = 0; i <= n; i++) {
|
|
28
|
+
const p = bezierPoints[start + i];
|
|
29
|
+
const c =
|
|
30
|
+
nCr(n, i) *
|
|
31
|
+
Math.pow(1 - calculatedPercentage, n - i) *
|
|
32
|
+
Math.pow(calculatedPercentage, i);
|
|
33
|
+
value += c * p.y;
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import * as THREE from "three
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
import { EmitFrom } from "../three-particles.js";
|
|
2
4
|
|
|
3
5
|
export const patchObject = (
|
|
4
6
|
objectA,
|
|
@@ -8,7 +10,12 @@ export const patchObject = (
|
|
|
8
10
|
const result = {};
|
|
9
11
|
Object.keys(objectA).forEach((key) => {
|
|
10
12
|
if (!config.skippedProperties || !config.skippedProperties.includes(key)) {
|
|
11
|
-
if (
|
|
13
|
+
if (
|
|
14
|
+
typeof objectA[key] === "object" &&
|
|
15
|
+
objectA[key] &&
|
|
16
|
+
objectB[key] &&
|
|
17
|
+
!Array.isArray(objectA[key])
|
|
18
|
+
) {
|
|
12
19
|
result[key] = patchObject(objectA[key], objectB[key], config);
|
|
13
20
|
} else {
|
|
14
21
|
result[key] =
|
|
@@ -109,6 +116,54 @@ export const calculateRandomPositionAndVelocityOnCone = (
|
|
|
109
116
|
);
|
|
110
117
|
};
|
|
111
118
|
|
|
119
|
+
export const calculateRandomPositionAndVelocityOnBox = (
|
|
120
|
+
position,
|
|
121
|
+
velocity,
|
|
122
|
+
startSpeed,
|
|
123
|
+
{ scale, emitFrom }
|
|
124
|
+
) => {
|
|
125
|
+
switch (emitFrom) {
|
|
126
|
+
case EmitFrom.VOLUME:
|
|
127
|
+
position.x = Math.random() * scale.x - scale.x / 2;
|
|
128
|
+
position.y = Math.random() * scale.y - scale.y / 2;
|
|
129
|
+
position.z = Math.random() * scale.z - scale.z / 2;
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case EmitFrom.SHELL:
|
|
133
|
+
const side = Math.floor(Math.random() * 6);
|
|
134
|
+
const perpendicularAxis = side % 3;
|
|
135
|
+
const shellResult = [];
|
|
136
|
+
shellResult[perpendicularAxis] = side > 2 ? 1 : 0;
|
|
137
|
+
shellResult[(perpendicularAxis + 1) % 3] = Math.random();
|
|
138
|
+
shellResult[(perpendicularAxis + 2) % 3] = Math.random();
|
|
139
|
+
position.x = shellResult[0] * scale.x - scale.x / 2;
|
|
140
|
+
position.y = shellResult[1] * scale.y - scale.y / 2;
|
|
141
|
+
position.z = shellResult[2] * scale.z - scale.z / 2;
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case EmitFrom.EDGE:
|
|
145
|
+
const side2 = Math.floor(Math.random() * 6);
|
|
146
|
+
const perpendicularAxis2 = side2 % 3;
|
|
147
|
+
const edge = Math.floor(Math.random() * 4);
|
|
148
|
+
const edgeResult = [];
|
|
149
|
+
edgeResult[perpendicularAxis2] = side2 > 2 ? 1 : 0;
|
|
150
|
+
edgeResult[(perpendicularAxis2 + 1) % 3] =
|
|
151
|
+
edge < 2 ? Math.random() : edge - 2;
|
|
152
|
+
edgeResult[(perpendicularAxis2 + 2) % 3] =
|
|
153
|
+
edge < 2 ? edge : Math.random();
|
|
154
|
+
position.x = edgeResult[0] * scale.x - scale.x / 2;
|
|
155
|
+
position.y = edgeResult[1] * scale.y - scale.y / 2;
|
|
156
|
+
position.z = edgeResult[2] * scale.z - scale.z / 2;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const randomizedSpeed = THREE.MathUtils.randFloat(
|
|
161
|
+
startSpeed.min,
|
|
162
|
+
startSpeed.max
|
|
163
|
+
);
|
|
164
|
+
velocity.set(0, 0, randomizedSpeed);
|
|
165
|
+
};
|
|
166
|
+
|
|
112
167
|
export const calculateRandomPositionAndVelocityOnCircle = (
|
|
113
168
|
position,
|
|
114
169
|
velocity,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import * as THREE from "three
|
|
1
|
+
import * as THREE from "three";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
calculateRandomPositionAndVelocityOnBox,
|
|
4
5
|
calculateRandomPositionAndVelocityOnCircle,
|
|
5
6
|
calculateRandomPositionAndVelocityOnCone,
|
|
6
7
|
calculateRandomPositionAndVelocityOnRectangle,
|
|
@@ -13,6 +14,7 @@ import { FBM } from "three-noise/build/three-noise.module.js";
|
|
|
13
14
|
import ParticleSystemFragmentShader from "./three-particles/shaders/particle-system-fragment-shader.glsl.js";
|
|
14
15
|
import ParticleSystemVertexShader from "./three-particles/shaders/particle-system-vertex-shader.glsl.js";
|
|
15
16
|
import { applyModifiers } from "./three-particles/three-particles-modifiers.js";
|
|
17
|
+
import { createBezierCurveFunction } from "./three-particles/three-particles-bezier";
|
|
16
18
|
|
|
17
19
|
let createdParticleSystems = [];
|
|
18
20
|
|
|
@@ -29,6 +31,12 @@ export const Shape = {
|
|
|
29
31
|
RECTANGLE: "RECTANGLE",
|
|
30
32
|
};
|
|
31
33
|
|
|
34
|
+
export const EmitFrom = {
|
|
35
|
+
VOLUME: "VOLUME",
|
|
36
|
+
SHELL: "SHELL",
|
|
37
|
+
EDGE: "EDGE",
|
|
38
|
+
};
|
|
39
|
+
|
|
32
40
|
export const TimeMode = {
|
|
33
41
|
LIFETIME: "LIFETIME",
|
|
34
42
|
FPS: "FPS",
|
|
@@ -92,6 +100,10 @@ const DEFAULT_PARTICLE_SYSTEM_CONFIG = {
|
|
|
92
100
|
rotation: { x: 0.0, y: 0.0 }, // TODO: add z rotation
|
|
93
101
|
scale: { x: 1.0, y: 1.0 },
|
|
94
102
|
},
|
|
103
|
+
box: {
|
|
104
|
+
scale: { x: 1.0, y: 1.0, z: 1.0 },
|
|
105
|
+
emitFrom: EmitFrom.VOLUME,
|
|
106
|
+
},
|
|
95
107
|
},
|
|
96
108
|
map: null,
|
|
97
109
|
renderer: {
|
|
@@ -115,7 +127,11 @@ const DEFAULT_PARTICLE_SYSTEM_CONFIG = {
|
|
|
115
127
|
},
|
|
116
128
|
sizeOverLifetime: {
|
|
117
129
|
isActive: false,
|
|
118
|
-
curveFunction: CurveFunction.
|
|
130
|
+
curveFunction: CurveFunction.BEZIER,
|
|
131
|
+
bezierPoints: [
|
|
132
|
+
{ x: 0, y: 0, percentage: 0 },
|
|
133
|
+
{ x: 1, y: 1, percentage: 1 },
|
|
134
|
+
],
|
|
119
135
|
},
|
|
120
136
|
/* colorOverLifetime: {
|
|
121
137
|
isActive: false,
|
|
@@ -123,7 +139,11 @@ const DEFAULT_PARTICLE_SYSTEM_CONFIG = {
|
|
|
123
139
|
}, */
|
|
124
140
|
opacityOverLifetime: {
|
|
125
141
|
isActive: false,
|
|
126
|
-
curveFunction: CurveFunction.
|
|
142
|
+
curveFunction: CurveFunction.BEZIER,
|
|
143
|
+
bezierPoints: [
|
|
144
|
+
{ x: 0, y: 0, percentage: 0 },
|
|
145
|
+
{ x: 1, y: 1, percentage: 1 },
|
|
146
|
+
],
|
|
127
147
|
},
|
|
128
148
|
rotationOverLifetime: {
|
|
129
149
|
isActive: false,
|
|
@@ -169,7 +189,7 @@ const createFloat32Attributes = ({
|
|
|
169
189
|
};
|
|
170
190
|
|
|
171
191
|
const calculatePositionAndVelocity = (
|
|
172
|
-
{ shape, sphere, cone, circle, rectangle },
|
|
192
|
+
{ shape, sphere, cone, circle, rectangle, box },
|
|
173
193
|
startSpeed,
|
|
174
194
|
position,
|
|
175
195
|
velocity,
|
|
@@ -211,6 +231,15 @@ const calculatePositionAndVelocity = (
|
|
|
211
231
|
rectangle
|
|
212
232
|
);
|
|
213
233
|
break;
|
|
234
|
+
|
|
235
|
+
case Shape.BOX:
|
|
236
|
+
calculateRandomPositionAndVelocityOnBox(
|
|
237
|
+
position,
|
|
238
|
+
velocity,
|
|
239
|
+
startSpeed,
|
|
240
|
+
box
|
|
241
|
+
);
|
|
242
|
+
break;
|
|
214
243
|
}
|
|
215
244
|
|
|
216
245
|
if (velocityOverLifetime.isActive) {
|
|
@@ -249,6 +278,22 @@ export const createParticleSystem = (
|
|
|
249
278
|
};
|
|
250
279
|
|
|
251
280
|
const normalizedConfig = patchObject(DEFAULT_PARTICLE_SYSTEM_CONFIG, config);
|
|
281
|
+
|
|
282
|
+
const bezierCompatibleProperties = [
|
|
283
|
+
"sizeOverLifetime",
|
|
284
|
+
"opacityOverLifetime",
|
|
285
|
+
];
|
|
286
|
+
bezierCompatibleProperties.forEach((key) => {
|
|
287
|
+
if (
|
|
288
|
+
normalizedConfig[key].isActive &&
|
|
289
|
+
normalizedConfig[key].curveFunction === CurveFunction.BEZIER &&
|
|
290
|
+
normalizedConfig[key].bezierPoints
|
|
291
|
+
)
|
|
292
|
+
normalizedConfig[key].curveFunction = createBezierCurveFunction(
|
|
293
|
+
normalizedConfig[key].bezierPoints
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
252
297
|
const {
|
|
253
298
|
transform,
|
|
254
299
|
duration,
|
|
@@ -435,7 +480,7 @@ export const createParticleSystem = (
|
|
|
435
480
|
geometry.attributes.colorA.needsUpdate = true;
|
|
436
481
|
};
|
|
437
482
|
|
|
438
|
-
const activateParticle = ({ particleIndex, activationTime }) => {
|
|
483
|
+
const activateParticle = ({ particleIndex, activationTime, position }) => {
|
|
439
484
|
geometry.attributes.isActive.array[particleIndex] = true;
|
|
440
485
|
generalData.creationTimes[particleIndex] = activationTime;
|
|
441
486
|
|
|
@@ -498,11 +543,12 @@ export const createParticleSystem = (
|
|
|
498
543
|
);
|
|
499
544
|
const positionIndex = Math.floor(particleIndex * 3);
|
|
500
545
|
geometry.attributes.position.array[positionIndex] =
|
|
501
|
-
startPositions[particleIndex].x;
|
|
546
|
+
(position ? position.x : 0) + startPositions[particleIndex].x;
|
|
502
547
|
geometry.attributes.position.array[positionIndex + 1] =
|
|
503
|
-
startPositions[particleIndex].y;
|
|
548
|
+
(position ? position.y : 0) + startPositions[particleIndex].y;
|
|
504
549
|
geometry.attributes.position.array[positionIndex + 2] =
|
|
505
|
-
startPositions[particleIndex].z;
|
|
550
|
+
(position ? position.z : 0) + startPositions[particleIndex].z;
|
|
551
|
+
geometry.attributes.position.needsUpdate = true;
|
|
506
552
|
|
|
507
553
|
geometry.attributes.lifetime.array[particleIndex] = 0;
|
|
508
554
|
geometry.attributes.lifetime.needsUpdate = true;
|
|
@@ -596,19 +642,22 @@ export const updateParticleSystems = ({ now, delta, elapsed }) => {
|
|
|
596
642
|
gravityVelocity,
|
|
597
643
|
} = generalData;
|
|
598
644
|
|
|
645
|
+
const lastWorldPositionSnapshot = { ...lastWorldPosition };
|
|
646
|
+
|
|
599
647
|
const lifetime = now - creationTime;
|
|
600
648
|
particleSystem.material.uniforms.elapsed.value = elapsed;
|
|
601
649
|
|
|
602
650
|
particleSystem.getWorldPosition(currentWorldPosition);
|
|
603
|
-
if (lastWorldPosition.x !== -99999)
|
|
651
|
+
if (lastWorldPosition.x !== -99999) {
|
|
604
652
|
worldPositionChange.set(
|
|
605
653
|
currentWorldPosition.x - lastWorldPosition.x,
|
|
606
654
|
currentWorldPosition.y - lastWorldPosition.y,
|
|
607
655
|
currentWorldPosition.z - lastWorldPosition.z
|
|
608
656
|
);
|
|
657
|
+
worldPositionChange.applyQuaternion(worldQuaternion.invert());
|
|
658
|
+
}
|
|
609
659
|
generalData.distanceFromLastEmitByDistance += worldPositionChange.length();
|
|
610
660
|
particleSystem.getWorldPosition(lastWorldPosition);
|
|
611
|
-
|
|
612
661
|
particleSystem.getWorldQuaternion(worldQuaternion);
|
|
613
662
|
if (
|
|
614
663
|
lastWorldQuaternion.x === -99999 ||
|
|
@@ -618,16 +667,12 @@ export const updateParticleSystems = ({ now, delta, elapsed }) => {
|
|
|
618
667
|
) {
|
|
619
668
|
worldEuler.setFromQuaternion(worldQuaternion);
|
|
620
669
|
lastWorldQuaternion.copy(worldQuaternion);
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
particleSystem.position.set(0, 0, 0);
|
|
627
|
-
particleSystem.updateMatrixWorld();
|
|
670
|
+
gravityVelocity.set(
|
|
671
|
+
lastWorldPosition.x,
|
|
672
|
+
lastWorldPosition.y + gravity,
|
|
673
|
+
lastWorldPosition.z
|
|
674
|
+
);
|
|
628
675
|
particleSystem.worldToLocal(gravityVelocity);
|
|
629
|
-
particleSystem.position.set(tempPosX, tempPosY, tempPosZ);
|
|
630
|
-
particleSystem.updateMatrixWorld();
|
|
631
676
|
}
|
|
632
677
|
|
|
633
678
|
generalData.creationTimes.forEach((entry, index) => {
|
|
@@ -648,11 +693,15 @@ export const updateParticleSystems = ({ now, delta, elapsed }) => {
|
|
|
648
693
|
gravity !== 0 ||
|
|
649
694
|
velocity.x !== 0 ||
|
|
650
695
|
velocity.y !== 0 ||
|
|
651
|
-
velocity.z !== 0
|
|
696
|
+
velocity.z !== 0 ||
|
|
697
|
+
worldPositionChange.x !== 0 ||
|
|
698
|
+
worldPositionChange.y !== 0 ||
|
|
699
|
+
worldPositionChange.z !== 0
|
|
652
700
|
) {
|
|
653
701
|
const positionIndex = index * 3;
|
|
654
702
|
const positionArr =
|
|
655
703
|
particleSystem.geometry.attributes.position.array;
|
|
704
|
+
|
|
656
705
|
if (simulationSpace === SimulationSpace.WORLD) {
|
|
657
706
|
positionArr[positionIndex] -= worldPositionChange.x;
|
|
658
707
|
positionArr[positionIndex + 1] -= worldPositionChange.y;
|
|
@@ -699,12 +748,28 @@ export const updateParticleSystems = ({ now, delta, elapsed }) => {
|
|
|
699
748
|
(1 / emission.rateOverDistance)
|
|
700
749
|
)
|
|
701
750
|
: 0;
|
|
751
|
+
const distanceStep =
|
|
752
|
+
neededParticlesByDistance > 0
|
|
753
|
+
? {
|
|
754
|
+
x:
|
|
755
|
+
(currentWorldPosition.x - lastWorldPositionSnapshot.x) /
|
|
756
|
+
neededParticlesByDistance,
|
|
757
|
+
y:
|
|
758
|
+
(currentWorldPosition.y - lastWorldPositionSnapshot.y) /
|
|
759
|
+
neededParticlesByDistance,
|
|
760
|
+
z:
|
|
761
|
+
(currentWorldPosition.z - lastWorldPositionSnapshot.z) /
|
|
762
|
+
neededParticlesByDistance,
|
|
763
|
+
}
|
|
764
|
+
: null;
|
|
702
765
|
const neededParticles = neededParticlesByTime + neededParticlesByDistance;
|
|
703
766
|
|
|
704
|
-
if (emission.rateOverDistance > 0 && neededParticlesByDistance >= 1)
|
|
767
|
+
if (emission.rateOverDistance > 0 && neededParticlesByDistance >= 1) {
|
|
705
768
|
generalData.distanceFromLastEmitByDistance = 0;
|
|
769
|
+
}
|
|
706
770
|
|
|
707
771
|
if (neededParticles > 0) {
|
|
772
|
+
let generatedParticlesByDistanceNeeds = 0;
|
|
708
773
|
for (let i = 0; i < neededParticles; i++) {
|
|
709
774
|
let particleIndex = -1;
|
|
710
775
|
particleSystem.geometry.attributes.isActive.array.find(
|
|
@@ -723,7 +788,19 @@ export const updateParticleSystems = ({ now, delta, elapsed }) => {
|
|
|
723
788
|
particleIndex <
|
|
724
789
|
particleSystem.geometry.attributes.isActive.array.length
|
|
725
790
|
) {
|
|
726
|
-
|
|
791
|
+
let position;
|
|
792
|
+
if (generatedParticlesByDistanceNeeds < neededParticlesByDistance) {
|
|
793
|
+
position =
|
|
794
|
+
generatedParticlesByDistanceNeeds < neededParticlesByDistance
|
|
795
|
+
? {
|
|
796
|
+
x: distanceStep.x * generatedParticlesByDistanceNeeds,
|
|
797
|
+
y: distanceStep.y * generatedParticlesByDistanceNeeds,
|
|
798
|
+
z: distanceStep.z * generatedParticlesByDistanceNeeds,
|
|
799
|
+
}
|
|
800
|
+
: null;
|
|
801
|
+
generatedParticlesByDistanceNeeds++;
|
|
802
|
+
}
|
|
803
|
+
activateParticle({ particleIndex, activationTime: now, position });
|
|
727
804
|
props.lastEmissionTime = now;
|
|
728
805
|
}
|
|
729
806
|
}
|