@codexo/exojs-particles 0.15.2 → 0.16.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 +21 -43
- package/dist/esm/ParticleStorage.d.ts +188 -0
- package/dist/esm/ParticleStorage.d.ts.map +1 -0
- package/dist/esm/ParticleStorage.js +208 -0
- package/dist/esm/ParticleStorage.js.map +1 -0
- package/dist/esm/ParticleSystem.d.ts +189 -60
- package/dist/esm/ParticleSystem.d.ts.map +1 -0
- package/dist/esm/ParticleSystem.js +660 -560
- package/dist/esm/ParticleSystem.js.map +1 -1
- package/dist/esm/distributions/BoxArea.d.ts +1 -0
- package/dist/esm/distributions/BoxArea.d.ts.map +1 -0
- package/dist/esm/distributions/BoxArea.js +38 -44
- package/dist/esm/distributions/BoxArea.js.map +1 -1
- package/dist/esm/distributions/CircleArea.d.ts +1 -0
- package/dist/esm/distributions/CircleArea.d.ts.map +1 -0
- package/dist/esm/distributions/CircleArea.js +30 -28
- package/dist/esm/distributions/CircleArea.js.map +1 -1
- package/dist/esm/distributions/ColorGradient.d.ts +3 -2
- package/dist/esm/distributions/ColorGradient.d.ts.map +1 -0
- package/dist/esm/distributions/ColorGradient.js +70 -75
- package/dist/esm/distributions/ColorGradient.js.map +1 -1
- package/dist/esm/distributions/ConeDirection.d.ts +2 -1
- package/dist/esm/distributions/ConeDirection.d.ts.map +1 -0
- package/dist/esm/distributions/ConeDirection.js +41 -39
- package/dist/esm/distributions/ConeDirection.js.map +1 -1
- package/dist/esm/distributions/Constant.d.ts +1 -0
- package/dist/esm/distributions/Constant.d.ts.map +1 -0
- package/dist/esm/distributions/Constant.js +32 -32
- package/dist/esm/distributions/Constant.js.map +1 -1
- package/dist/esm/distributions/Curve.d.ts +2 -1
- package/dist/esm/distributions/Curve.d.ts.map +1 -0
- package/dist/esm/distributions/Curve.js +46 -55
- package/dist/esm/distributions/Curve.js.map +1 -1
- package/dist/esm/distributions/Distribution.d.ts +4 -3
- package/dist/esm/distributions/Distribution.d.ts.map +1 -0
- package/dist/esm/distributions/LineSegment.d.ts +1 -0
- package/dist/esm/distributions/LineSegment.d.ts.map +1 -0
- package/dist/esm/distributions/LineSegment.js +25 -23
- package/dist/esm/distributions/LineSegment.js.map +1 -1
- package/dist/esm/distributions/Range.d.ts +1 -0
- package/dist/esm/distributions/Range.d.ts.map +1 -0
- package/dist/esm/distributions/Range.js +18 -16
- package/dist/esm/distributions/Range.js.map +1 -1
- package/dist/esm/distributions/VectorRange.d.ts +1 -0
- package/dist/esm/distributions/VectorRange.d.ts.map +1 -0
- package/dist/esm/distributions/VectorRange.js +29 -27
- package/dist/esm/distributions/VectorRange.js.map +1 -1
- package/dist/esm/distributions/index.d.ts +1 -0
- package/dist/esm/distributions/index.d.ts.map +1 -0
- package/dist/esm/gpu/ParticleGpuState.d.ts +143 -18
- package/dist/esm/gpu/ParticleGpuState.d.ts.map +1 -0
- package/dist/esm/gpu/ParticleGpuState.js +655 -497
- package/dist/esm/gpu/ParticleGpuState.js.map +1 -1
- package/dist/esm/gpu/shaders/particle-simulate.wgsl.js +6 -0
- package/dist/esm/gpu/shaders/particle-simulate.wgsl.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +39 -32
- package/dist/esm/modules/AlphaFadeOverLifetime.d.ts +4 -3
- package/dist/esm/modules/AlphaFadeOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/AlphaFadeOverLifetime.js +68 -55
- package/dist/esm/modules/AlphaFadeOverLifetime.js.map +1 -1
- package/dist/esm/modules/ApplyForce.d.ts +4 -3
- package/dist/esm/modules/ApplyForce.d.ts.map +1 -0
- package/dist/esm/modules/ApplyForce.js +49 -43
- package/dist/esm/modules/ApplyForce.js.map +1 -1
- package/dist/esm/modules/AttractToPoint.d.ts +4 -3
- package/dist/esm/modules/AttractToPoint.d.ts.map +1 -0
- package/dist/esm/modules/AttractToPoint.js +73 -62
- package/dist/esm/modules/AttractToPoint.js.map +1 -1
- package/dist/esm/modules/BurstSpawn.d.ts +6 -18
- package/dist/esm/modules/BurstSpawn.d.ts.map +1 -0
- package/dist/esm/modules/BurstSpawn.js +56 -91
- package/dist/esm/modules/BurstSpawn.js.map +1 -1
- package/dist/esm/modules/ColorOverLifetime.d.ts +4 -3
- package/dist/esm/modules/ColorOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/ColorOverLifetime.js +65 -53
- package/dist/esm/modules/ColorOverLifetime.js.map +1 -1
- package/dist/esm/modules/ColorOverSpeed.d.ts +5 -4
- package/dist/esm/modules/ColorOverSpeed.d.ts.map +1 -0
- package/dist/esm/modules/ColorOverSpeed.js +90 -75
- package/dist/esm/modules/ColorOverSpeed.js.map +1 -1
- package/dist/esm/modules/DeathModule.d.ts +26 -10
- package/dist/esm/modules/DeathModule.d.ts.map +1 -0
- package/dist/esm/modules/DeathModule.js +38 -22
- package/dist/esm/modules/DeathModule.js.map +1 -1
- package/dist/esm/modules/Drag.d.ts +3 -2
- package/dist/esm/modules/Drag.d.ts.map +1 -0
- package/dist/esm/modules/Drag.js +43 -37
- package/dist/esm/modules/Drag.js.map +1 -1
- package/dist/esm/modules/OrbitalForce.d.ts +4 -3
- package/dist/esm/modules/OrbitalForce.d.ts.map +1 -0
- package/dist/esm/modules/OrbitalForce.js +70 -58
- package/dist/esm/modules/OrbitalForce.js.map +1 -1
- package/dist/esm/modules/RateSpawn.d.ts +7 -22
- package/dist/esm/modules/RateSpawn.d.ts.map +1 -0
- package/dist/esm/modules/RateSpawn.js +39 -71
- package/dist/esm/modules/RateSpawn.js.map +1 -1
- package/dist/esm/modules/RepelFromPoint.d.ts +4 -3
- package/dist/esm/modules/RepelFromPoint.d.ts.map +1 -0
- package/dist/esm/modules/RepelFromPoint.js +72 -62
- package/dist/esm/modules/RepelFromPoint.js.map +1 -1
- package/dist/esm/modules/RotateOverLifetime.d.ts +3 -2
- package/dist/esm/modules/RotateOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/RotateOverLifetime.js +40 -36
- package/dist/esm/modules/RotateOverLifetime.js.map +1 -1
- package/dist/esm/modules/ScaleOverLifetime.d.ts +5 -4
- package/dist/esm/modules/ScaleOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/ScaleOverLifetime.js +60 -50
- package/dist/esm/modules/ScaleOverLifetime.js.map +1 -1
- package/dist/esm/modules/SpawnModule.d.ts +15 -12
- package/dist/esm/modules/SpawnModule.d.ts.map +1 -0
- package/dist/esm/modules/SpawnModule.js +32 -28
- package/dist/esm/modules/SpawnModule.js.map +1 -1
- package/dist/esm/modules/SpawnOnDeath.d.ts +13 -9
- package/dist/esm/modules/SpawnOnDeath.d.ts.map +1 -0
- package/dist/esm/modules/SpawnOnDeath.js +41 -43
- package/dist/esm/modules/SpawnOnDeath.js.map +1 -1
- package/dist/esm/modules/Turbulence.d.ts +4 -3
- package/dist/esm/modules/Turbulence.d.ts.map +1 -0
- package/dist/esm/modules/Turbulence.js +101 -89
- package/dist/esm/modules/Turbulence.js.map +1 -1
- package/dist/esm/modules/UpdateModule.d.ts +21 -13
- package/dist/esm/modules/UpdateModule.d.ts.map +1 -0
- package/dist/esm/modules/UpdateModule.js +65 -63
- package/dist/esm/modules/UpdateModule.js.map +1 -1
- package/dist/esm/modules/VelocityOverLifetime.d.ts +7 -6
- package/dist/esm/modules/VelocityOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/VelocityOverLifetime.js +75 -73
- package/dist/esm/modules/VelocityOverLifetime.js.map +1 -1
- package/dist/esm/modules/WgslContribution.d.ts +16 -15
- package/dist/esm/modules/WgslContribution.d.ts.map +1 -0
- package/dist/esm/modules/WgslContribution.js +37 -29
- package/dist/esm/modules/WgslContribution.js.map +1 -1
- package/dist/esm/modules/index.d.ts +3 -1
- package/dist/esm/modules/index.d.ts.map +1 -0
- package/dist/esm/modules/spawnFields.d.ts +32 -0
- package/dist/esm/modules/spawnFields.d.ts.map +1 -0
- package/dist/esm/modules/spawnFields.js +31 -0
- package/dist/esm/modules/spawnFields.js.map +1 -0
- package/dist/esm/particlesBuildInfo.d.ts +1 -0
- package/dist/esm/particlesBuildInfo.d.ts.map +1 -0
- package/dist/esm/particlesBuildInfo.js +6 -4
- package/dist/esm/particlesBuildInfo.js.map +1 -1
- package/dist/esm/particlesExtension.d.ts +13 -5
- package/dist/esm/particlesExtension.d.ts.map +1 -0
- package/dist/esm/particlesExtension.js +38 -42
- package/dist/esm/particlesExtension.js.map +1 -1
- package/dist/esm/public.d.ts +11 -0
- package/dist/esm/public.d.ts.map +1 -0
- package/dist/esm/renderModes/MeshParticles.d.ts +128 -0
- package/dist/esm/renderModes/MeshParticles.d.ts.map +1 -0
- package/dist/esm/renderModes/MeshParticles.js +263 -0
- package/dist/esm/renderModes/MeshParticles.js.map +1 -0
- package/dist/esm/renderModes/ParticleBufferLayout.d.ts +59 -0
- package/dist/esm/renderModes/ParticleBufferLayout.d.ts.map +1 -0
- package/dist/esm/renderModes/ParticleBufferLayout.js +99 -0
- package/dist/esm/renderModes/ParticleBufferLayout.js.map +1 -0
- package/dist/esm/renderModes/ParticleInstanceWriter.d.ts +58 -0
- package/dist/esm/renderModes/ParticleInstanceWriter.d.ts.map +1 -0
- package/dist/esm/renderModes/ParticleInstanceWriter.js +173 -0
- package/dist/esm/renderModes/ParticleInstanceWriter.js.map +1 -0
- package/dist/esm/renderModes/ParticleMaterial.d.ts +18 -0
- package/dist/esm/renderModes/ParticleMaterial.d.ts.map +1 -0
- package/dist/esm/renderModes/ParticleMaterial.js +24 -0
- package/dist/esm/renderModes/ParticleMaterial.js.map +1 -0
- package/dist/esm/renderModes/ParticleRenderMode.d.ts +114 -0
- package/dist/esm/renderModes/ParticleRenderMode.d.ts.map +1 -0
- package/dist/esm/renderModes/ParticleRenderMode.js +99 -0
- package/dist/esm/renderModes/ParticleRenderMode.js.map +1 -0
- package/dist/esm/renderModes/QuadParticles.d.ts +55 -0
- package/dist/esm/renderModes/QuadParticles.d.ts.map +1 -0
- package/dist/esm/renderModes/QuadParticles.js +95 -0
- package/dist/esm/renderModes/QuadParticles.js.map +1 -0
- package/dist/esm/renderModes/RibbonParticles.d.ts +96 -0
- package/dist/esm/renderModes/RibbonParticles.d.ts.map +1 -0
- package/dist/esm/renderModes/RibbonParticles.js +202 -0
- package/dist/esm/renderModes/RibbonParticles.js.map +1 -0
- package/dist/esm/renderModes/shaders/mesh-particles.wgsl.js +6 -0
- package/dist/esm/renderModes/shaders/mesh-particles.wgsl.js.map +1 -0
- package/dist/esm/renderModes/shaders/mesh.vert.js +6 -0
- package/dist/esm/renderModes/shaders/mesh.vert.js.map +1 -0
- package/dist/esm/renderModes/shaders/quad-particles.wgsl.js +6 -0
- package/dist/esm/renderModes/shaders/quad-particles.wgsl.js.map +1 -0
- package/dist/esm/renderModes/shaders/ribbon-particles.wgsl.js +6 -0
- package/dist/esm/renderModes/shaders/ribbon-particles.wgsl.js.map +1 -0
- package/dist/esm/renderModes/shaders/ribbon.frag.js +6 -0
- package/dist/esm/renderModes/shaders/ribbon.frag.js.map +1 -0
- package/dist/esm/renderModes/shaders/ribbon.vert.js +6 -0
- package/dist/esm/renderModes/shaders/ribbon.vert.js.map +1 -0
- package/dist/esm/renderers/WebGl2ParticleRenderer.d.ts +48 -20
- package/dist/esm/renderers/WebGl2ParticleRenderer.d.ts.map +1 -0
- package/dist/esm/renderers/WebGl2ParticleRenderer.js +317 -321
- package/dist/esm/renderers/WebGl2ParticleRenderer.js.map +1 -1
- package/dist/esm/renderers/WebGpuParticleRenderer.d.ts +99 -18
- package/dist/esm/renderers/WebGpuParticleRenderer.d.ts.map +1 -0
- package/dist/esm/renderers/WebGpuParticleRenderer.js +542 -551
- package/dist/esm/renderers/WebGpuParticleRenderer.js.map +1 -1
- package/dist/esm/renderers/shaders/particle.frag.js +6 -0
- package/dist/esm/renderers/shaders/particle.frag.js.map +1 -0
- package/dist/esm/renderers/shaders/particle.vert.js +6 -0
- package/dist/esm/renderers/shaders/particle.vert.js.map +1 -0
- package/package.json +7 -14
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/register.d.ts +0 -1
- package/dist/esm/register.js +0 -43
- package/dist/esm/register.js.map +0 -1
- package/dist/esm/renderers/glsl/particle.frag.js +0 -4
- package/dist/esm/renderers/glsl/particle.frag.js.map +0 -1
- package/dist/esm/renderers/glsl/particle.vert.js +0 -4
- package/dist/esm/renderers/glsl/particle.vert.js.map +0 -1
|
@@ -1,569 +1,669 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ParticleGpuState } from "./gpu/ParticleGpuState.js";
|
|
2
|
+
import { ParticleSlotWriter, ParticleStorage } from "./ParticleStorage.js";
|
|
3
|
+
import { QuadParticles } from "./renderModes/QuadParticles.js";
|
|
4
|
+
import { Drawable, Rectangle, Spritesheet, Texture } from "@codexo/exojs";
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
//#region src/ParticleSystem.ts
|
|
5
7
|
const defaultCapacity = 4096;
|
|
6
8
|
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
* Lazily-initialised 1×1 opaque-white texture used as the default sprite
|
|
10
|
+
* when a {@link ParticleSystem} is constructed without one. Particles
|
|
11
|
+
* render as solid color quads (the per-particle `color` channel times
|
|
12
|
+
* white-with-alpha-1). Shared across systems to avoid wasted texture
|
|
13
|
+
* allocations.
|
|
14
|
+
*/
|
|
13
15
|
let defaultWhiteTexture = null;
|
|
14
16
|
const getDefaultWhiteTexture = () => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
if (defaultWhiteTexture === null) {
|
|
18
|
+
const canvas = document.createElement("canvas");
|
|
19
|
+
canvas.width = 1;
|
|
20
|
+
canvas.height = 1;
|
|
21
|
+
const ctx = canvas.getContext("2d");
|
|
22
|
+
if (ctx !== null) {
|
|
23
|
+
ctx.fillStyle = "#ffffff";
|
|
24
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
25
|
+
}
|
|
26
|
+
defaultWhiteTexture = new Texture(canvas);
|
|
27
|
+
}
|
|
28
|
+
return defaultWhiteTexture;
|
|
27
29
|
};
|
|
28
30
|
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
31
|
+
* The process-wide default render mode, created on first use.
|
|
32
|
+
*
|
|
33
|
+
* Every system constructed without `ParticleSystemOptions.render` draws with
|
|
34
|
+
* this one instance. The backends key their GPU resources on the mode's
|
|
35
|
+
* material, so sharing it means N systems cost one compiled program / pipeline
|
|
36
|
+
* set, one vertex array object and one vertex buffer between them - what a
|
|
37
|
+
* single system costs.
|
|
38
|
+
*
|
|
39
|
+
* Sharing the mode also shares its scratch buffer, which is safe because that
|
|
40
|
+
* buffer is only live between a `build()` and the upload that immediately
|
|
41
|
+
* follows it: the WebGL2 renderer flushes the pending system at the top of
|
|
42
|
+
* every `render()` before building the next one, and the WebGPU renderer
|
|
43
|
+
* copies the built bytes out through `queue.writeBuffer` before it moves on to
|
|
44
|
+
* the next drawcall. That is exactly how the renderers behaved when each of
|
|
45
|
+
* them owned one shared pack buffer for all systems.
|
|
46
|
+
*
|
|
47
|
+
* Never destroyed. It outlives every individual system (see
|
|
48
|
+
* {@link ParticleSystem.destroy}), so a scene that destroys one system leaves
|
|
49
|
+
* the others drawing and a system constructed afterwards reuses the already
|
|
50
|
+
* compiled program instead of paying for a fresh one. The GPU resources behind
|
|
51
|
+
* it are released by the backend when the renderer disconnects.
|
|
52
|
+
*/
|
|
53
|
+
let defaultRenderMode = null;
|
|
54
|
+
const getDefaultRenderMode = () => {
|
|
55
|
+
defaultRenderMode ??= new QuadParticles();
|
|
56
|
+
return defaultRenderMode;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The central coordinator of the particle pipeline. `ParticleSystem` is a
|
|
60
|
+
* {@link Drawable} that owns:
|
|
61
|
+
*
|
|
62
|
+
* - **Particle storage** - one channel per attribute (position, velocity,
|
|
63
|
+
* scale, rotation, color, timing, ...), sized to a fixed capacity at
|
|
64
|
+
* construction. Modules and render modes address it by name through a
|
|
65
|
+
* {@link ParticleBatch}; user code brings particles into existence with
|
|
66
|
+
* {@link emit}.
|
|
67
|
+
* - **Spawn modules** - fill freshly emitted particles.
|
|
68
|
+
* - **Update modules** - mutate the live range each frame (forces, color
|
|
69
|
+
* blends, scale curves, drag, ...). Built-in modules ship both CPU and
|
|
70
|
+
* WGSL implementations; custom modules can opt into GPU acceleration by
|
|
71
|
+
* implementing `wgsl()`.
|
|
72
|
+
* - **Death modules** - fire once per dying particle, before its slot is
|
|
73
|
+
* recycled (sub-emitters, event hooks).
|
|
74
|
+
*
|
|
75
|
+
* **Auto-routing CPU vs GPU:** at first {@link update}, the system checks:
|
|
76
|
+
* if a `WebGpuBackend` was supplied AND every registered update module has
|
|
77
|
+
* `wgsl()` AND the render mode is GPU-eligible, the GPU path engages - a
|
|
78
|
+
* composite compute pipeline runs
|
|
79
|
+
* integration plus all module bodies in one dispatch and writes directly
|
|
80
|
+
* into the renderer's instance buffer (no CPU readback). Otherwise the CPU
|
|
81
|
+
* path runs the existing per-module `apply()` loops.
|
|
82
|
+
*
|
|
83
|
+
* **Per-frame order in {@link update} (CPU mode):**
|
|
84
|
+
* 1. Run every spawn module.
|
|
85
|
+
* 2. Integrate position from velocity, rotation from rotationSpeed, advance `elapsed`.
|
|
86
|
+
* 3. Run every update module on the live range.
|
|
87
|
+
* 4. Compact: scan `[0, liveCount)` forward, fire death modules on expired
|
|
88
|
+
* slots, copy survivors down. `liveCount` shrinks to the survivor count.
|
|
89
|
+
*
|
|
90
|
+
* **Per-frame order in {@link update} (GPU mode):**
|
|
91
|
+
* 1. Run every spawn module (CPU writes initial values into the spawn slot).
|
|
92
|
+
* 2. Detect expiries on CPU (via `elapsed >= lifetime`); fire death modules;
|
|
93
|
+
* set `lifetime[slot] = -1` sentinel + clear `alive[slot]` so the GPU
|
|
94
|
+
* shader skips them. **No compaction** - slots are recycled on next spawn.
|
|
95
|
+
* 3. Dispatch the composite compute pipeline. Integration + update modules
|
|
96
|
+
* + pack-instances run in one pass; the instance buffer is written
|
|
97
|
+
* directly. CPU SoA stays as-is for spawn writes.
|
|
98
|
+
*
|
|
99
|
+
* **Coordinate space:** particle positions are LOCAL to the system. The
|
|
100
|
+
* system's `getGlobalTransform()` is applied on top during rendering - both
|
|
101
|
+
* the WebGL2 and WebGPU shaders multiply `projection * translation * rotated`.
|
|
102
|
+
* Setting world-space positions on individual particles double-translates.
|
|
103
|
+
* Position the system itself via `system.setPosition(...)` and emit relative
|
|
104
|
+
* to `(0, 0)`.
|
|
105
|
+
*
|
|
106
|
+
* **Pixel snapping:** {@link Drawable.pixelSnapMode} is intentionally ignored
|
|
107
|
+
* for particle systems. Particle instances bake their own per-particle
|
|
108
|
+
* transforms in the emitter/compute path rather than reading the shared
|
|
109
|
+
* pixel-snap transform row, so a snap mode set on the system has no effect on
|
|
110
|
+
* rendered output - snapping thousands of independently-moving sub-pixel
|
|
111
|
+
* particles to the device grid is neither meaningful nor desirable.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* // Backend-agnostic - runs CPU on WebGL2, GPU on WebGPU automatically.
|
|
115
|
+
* const system = new ParticleSystem(loader.get('spark.png'), {
|
|
116
|
+
* capacity: 8192,
|
|
117
|
+
* });
|
|
118
|
+
*
|
|
119
|
+
* system.addSpawnModule(new RateSpawn({ rate: new Constant(60), ... }));
|
|
120
|
+
* system.addUpdateModule(new ApplyForce(0, 980)); // gravity, GPU-eligible
|
|
121
|
+
* system.addUpdateModule(new ColorOverLifetime(fireGradient));
|
|
122
|
+
* scene.addChild(system);
|
|
123
|
+
*/
|
|
124
|
+
var ParticleSystem = class extends Drawable {
|
|
125
|
+
/** Maximum particle count this system will store. Fixed at construction. */
|
|
126
|
+
capacity;
|
|
127
|
+
/**
|
|
128
|
+
* The simulation's channel storage. Handed to update modules and render modes
|
|
129
|
+
* as a {@link ParticleBatch}; never exposed as a property of the system,
|
|
130
|
+
* because outside those two callbacks its integrated values are not
|
|
131
|
+
* backend-true.
|
|
132
|
+
* @internal
|
|
133
|
+
*/
|
|
134
|
+
_storage;
|
|
135
|
+
_writer;
|
|
136
|
+
_spawnModules = [];
|
|
137
|
+
_updateModules = [];
|
|
138
|
+
_deathModules = [];
|
|
139
|
+
_backend = null;
|
|
140
|
+
_device = null;
|
|
141
|
+
_gpuState = null;
|
|
142
|
+
_gpuMode = false;
|
|
143
|
+
_compiled = false;
|
|
144
|
+
_spawnHint = 0;
|
|
145
|
+
/**
|
|
146
|
+
* In GPU mode, slots whose CPU SoA values need re-uploading to the GPU
|
|
147
|
+
* (newly spawned, or just-expired with lifetime sentinel). Cleared
|
|
148
|
+
* after each compute dispatch. CPU never overwrites integrated GPU
|
|
149
|
+
* state - only dirty slots flow CPU → GPU.
|
|
150
|
+
*/
|
|
151
|
+
_gpuDirtySlots = /* @__PURE__ */ new Set();
|
|
152
|
+
/**
|
|
153
|
+
* Spawn lifetimes, in death order per slot, of the particles that expired
|
|
154
|
+
* since the last batch the device staged for readback. The device's own
|
|
155
|
+
* record cannot carry them: by the time the shader sees the particle, the CPU
|
|
156
|
+
* has already overwritten its lifetime with the expiry sentinel. A slot can
|
|
157
|
+
* appear more than once when it is recycled and dies again before the batch
|
|
158
|
+
* is staged, so the lifetimes queue up per slot.
|
|
159
|
+
*/
|
|
160
|
+
_pendingDeathLifetimes = /* @__PURE__ */ new Map();
|
|
161
|
+
/** Total entries across `_pendingDeathLifetimes`, which is what the dispatch reports. */
|
|
162
|
+
_pendingDeathCount = 0;
|
|
163
|
+
/** Whether this system has already reported that its death backlog overflowed. */
|
|
164
|
+
_deathOverflowReported = false;
|
|
165
|
+
/**
|
|
166
|
+
* Slots handed out by `spawn()` while a recording window is open, or `null`
|
|
167
|
+
* when nothing is recording. Lets callers identify freshly spawned particles
|
|
168
|
+
* without diffing `liveCount`, which cannot see a recycled GPU slot.
|
|
169
|
+
*/
|
|
170
|
+
_spawnRecord = null;
|
|
171
|
+
_renderMode;
|
|
172
|
+
/** Whether {@link destroy} may destroy {@link _renderMode} - false for the shared default. */
|
|
173
|
+
_ownsRenderMode;
|
|
174
|
+
_texture;
|
|
175
|
+
_frames = [];
|
|
176
|
+
_textureFrame = new Rectangle();
|
|
177
|
+
_vertices = /* @__PURE__ */ new Float32Array(4);
|
|
178
|
+
_texCoords = /* @__PURE__ */ new Uint32Array(4);
|
|
179
|
+
_updateTexCoords = true;
|
|
180
|
+
_updateVertices = true;
|
|
181
|
+
constructor(sourceOrOptions, framesOrOptions, finalOptions) {
|
|
182
|
+
super();
|
|
183
|
+
let texture = null;
|
|
184
|
+
let frames = null;
|
|
185
|
+
let options;
|
|
186
|
+
if (sourceOrOptions instanceof Texture) {
|
|
187
|
+
texture = sourceOrOptions;
|
|
188
|
+
if (Array.isArray(framesOrOptions)) {
|
|
189
|
+
frames = framesOrOptions;
|
|
190
|
+
options = finalOptions ?? {};
|
|
191
|
+
} else options = framesOrOptions ?? {};
|
|
192
|
+
} else if (sourceOrOptions instanceof Spritesheet) {
|
|
193
|
+
texture = sourceOrOptions.texture;
|
|
194
|
+
frames = [...sourceOrOptions.frames.values()];
|
|
195
|
+
options = framesOrOptions ?? {};
|
|
196
|
+
} else options = sourceOrOptions ?? {};
|
|
197
|
+
const capacity = options.capacity ?? defaultCapacity;
|
|
198
|
+
if (capacity <= 0 || !Number.isInteger(capacity)) throw new Error(`ParticleSystem capacity must be a positive integer (got ${capacity}).`);
|
|
199
|
+
this.capacity = capacity;
|
|
200
|
+
this._storage = new ParticleStorage(capacity);
|
|
201
|
+
this._writer = new ParticleSlotWriter(this._storage);
|
|
202
|
+
this._device = options.device ?? null;
|
|
203
|
+
this._ownsRenderMode = options.render !== void 0;
|
|
204
|
+
this._renderMode = options.render ?? getDefaultRenderMode();
|
|
205
|
+
this._texture = texture ?? getDefaultWhiteTexture();
|
|
206
|
+
if (frames !== null) for (const frame of frames) this._frames.push(frame.clone());
|
|
207
|
+
this.resetTextureFrame();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The render mode this system's particles are drawn with. Fixed at
|
|
211
|
+
* construction via `ParticleSystemOptions.render`; the backend renderers
|
|
212
|
+
* read it every draw to learn the vertex layout, shader and draw model.
|
|
213
|
+
*
|
|
214
|
+
* Without that option this is the shared default mode - the same instance
|
|
215
|
+
* every other defaulted system draws with, so do not destroy it or mutate
|
|
216
|
+
* its material.
|
|
217
|
+
*/
|
|
218
|
+
get renderMode() {
|
|
219
|
+
return this._renderMode;
|
|
220
|
+
}
|
|
221
|
+
get texture() {
|
|
222
|
+
return this._texture;
|
|
223
|
+
}
|
|
224
|
+
set texture(texture) {
|
|
225
|
+
this.setTexture(texture);
|
|
226
|
+
}
|
|
227
|
+
get textureFrame() {
|
|
228
|
+
return this._textureFrame;
|
|
229
|
+
}
|
|
230
|
+
set textureFrame(frame) {
|
|
231
|
+
this.setTextureFrame(frame);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Atlas frames declared on this system, or empty when the texture is
|
|
235
|
+
* used as a single frame. Each particle's {@link textureIndex} selects
|
|
236
|
+
* an entry from this list; anything out of range shows frame 0.
|
|
237
|
+
*/
|
|
238
|
+
get frames() {
|
|
239
|
+
return this._frames;
|
|
240
|
+
}
|
|
241
|
+
/** `true` when the system declares more than one atlas frame. */
|
|
242
|
+
get hasAtlas() {
|
|
243
|
+
return this._frames.length > 1;
|
|
244
|
+
}
|
|
245
|
+
get vertices() {
|
|
246
|
+
if (this._updateVertices) {
|
|
247
|
+
const { x, y, width, height } = this._textureFrame;
|
|
248
|
+
const offsetX = width / 2;
|
|
249
|
+
const offsetY = height / 2;
|
|
250
|
+
this._vertices[0] = x - offsetX;
|
|
251
|
+
this._vertices[1] = y - offsetY;
|
|
252
|
+
this._vertices[2] = width - offsetX;
|
|
253
|
+
this._vertices[3] = height - offsetY;
|
|
254
|
+
this._updateVertices = false;
|
|
255
|
+
}
|
|
256
|
+
return this._vertices;
|
|
257
|
+
}
|
|
258
|
+
get texCoords() {
|
|
259
|
+
if (this._updateTexCoords) {
|
|
260
|
+
const { width, height } = this._texture;
|
|
261
|
+
const { left, top, right, bottom } = this._textureFrame;
|
|
262
|
+
const minX = left / width * 65535 & 65535;
|
|
263
|
+
const minY = (top / height * 65535 & 65535) << 16;
|
|
264
|
+
const maxX = right / width * 65535 & 65535;
|
|
265
|
+
const maxY = (bottom / height * 65535 & 65535) << 16;
|
|
266
|
+
if (this._texture.flipY) {
|
|
267
|
+
this._texCoords[0] = maxY | minX;
|
|
268
|
+
this._texCoords[1] = maxY | maxX;
|
|
269
|
+
this._texCoords[2] = minY | maxX;
|
|
270
|
+
this._texCoords[3] = minY | minX;
|
|
271
|
+
} else {
|
|
272
|
+
this._texCoords[0] = minY | minX;
|
|
273
|
+
this._texCoords[1] = minY | maxX;
|
|
274
|
+
this._texCoords[2] = maxY | maxX;
|
|
275
|
+
this._texCoords[3] = maxY | minX;
|
|
276
|
+
}
|
|
277
|
+
this._updateTexCoords = false;
|
|
278
|
+
}
|
|
279
|
+
return this._texCoords;
|
|
280
|
+
}
|
|
281
|
+
/** `true` when the system is running on the GPU compute pipeline. */
|
|
282
|
+
get gpuMode() {
|
|
283
|
+
return this._gpuMode;
|
|
284
|
+
}
|
|
285
|
+
/** GPU-side state, or `null` in CPU mode. */
|
|
286
|
+
get gpuState() {
|
|
287
|
+
return this._gpuState;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Upper bound of the slot range that can hold live particles.
|
|
291
|
+
*
|
|
292
|
+
* Exact on the CPU path: after each `update()` slots `[0, liveCount)` are all
|
|
293
|
+
* alive. On the GPU path it is a high-water mark whose range can contain dead
|
|
294
|
+
* holes that future emissions fill; {@link aliveCount} counts the live ones.
|
|
295
|
+
*/
|
|
296
|
+
get liveCount() {
|
|
297
|
+
return this._storage.count;
|
|
298
|
+
}
|
|
299
|
+
/** Actual count of live particles. May be below {@link liveCount} on the GPU path. */
|
|
300
|
+
get aliveCount() {
|
|
301
|
+
const { alive, count } = this._storage;
|
|
302
|
+
let live = 0;
|
|
303
|
+
for (let i = 0; i < count; i++) if (alive[i] === 1) live++;
|
|
304
|
+
return live;
|
|
305
|
+
}
|
|
306
|
+
get spawnModules() {
|
|
307
|
+
return this._spawnModules;
|
|
308
|
+
}
|
|
309
|
+
get updateModules() {
|
|
310
|
+
return this._updateModules;
|
|
311
|
+
}
|
|
312
|
+
get deathModules() {
|
|
313
|
+
return this._deathModules;
|
|
314
|
+
}
|
|
315
|
+
setTexture(texture) {
|
|
316
|
+
if (this._texture !== texture) {
|
|
317
|
+
this._texture = texture;
|
|
318
|
+
this.resetTextureFrame();
|
|
319
|
+
}
|
|
320
|
+
return this;
|
|
321
|
+
}
|
|
322
|
+
setTextureFrame(frame) {
|
|
323
|
+
this._textureFrame.copy(frame);
|
|
324
|
+
this._updateTexCoords = true;
|
|
325
|
+
this._updateVertices = true;
|
|
326
|
+
this._setLocalBounds(0, 0, frame.width, frame.height);
|
|
327
|
+
return this;
|
|
328
|
+
}
|
|
329
|
+
resetTextureFrame() {
|
|
330
|
+
return this.setTextureFrame(Rectangle.temp.set(0, 0, this._texture.width, this._texture.height));
|
|
331
|
+
}
|
|
332
|
+
addSpawnModule(mod) {
|
|
333
|
+
this._spawnModules.push(mod);
|
|
334
|
+
return this;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Registers an update module. Modules run in registration order, each seeing
|
|
338
|
+
* what the previous ones did.
|
|
339
|
+
*
|
|
340
|
+
* Modules may be added and removed at any time, including mid-flight: the
|
|
341
|
+
* next update rebuilds whatever the change invalidated. On the GPU path that
|
|
342
|
+
* is the compute program alone - live particles keep the state the device has
|
|
343
|
+
* been integrating. Adding a module without a `wgsl()` implementation to a
|
|
344
|
+
* running GPU system is the one change that cannot preserve them: the
|
|
345
|
+
* simulation moves to the CPU, which has no copy of the integrated state, so
|
|
346
|
+
* the system clears its live particles rather than continuing from stale
|
|
347
|
+
* values (see {@link clearParticles}).
|
|
348
|
+
*/
|
|
349
|
+
addUpdateModule(mod) {
|
|
350
|
+
this._updateModules.push(mod);
|
|
351
|
+
this._invalidateProgram();
|
|
352
|
+
return this;
|
|
353
|
+
}
|
|
354
|
+
addDeathModule(mod) {
|
|
355
|
+
const hadNone = this._deathModules.length === 0;
|
|
356
|
+
this._deathModules.push(mod);
|
|
357
|
+
if (hadNone) this._invalidateProgram();
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
clearSpawnModules() {
|
|
361
|
+
for (const mod of this._spawnModules) mod.destroy();
|
|
362
|
+
this._spawnModules.length = 0;
|
|
363
|
+
return this;
|
|
364
|
+
}
|
|
365
|
+
clearUpdateModules() {
|
|
366
|
+
for (const mod of this._updateModules) mod.destroy();
|
|
367
|
+
this._updateModules.length = 0;
|
|
368
|
+
this._invalidateProgram();
|
|
369
|
+
return this;
|
|
370
|
+
}
|
|
371
|
+
clearDeathModules() {
|
|
372
|
+
for (const mod of this._deathModules) mod.destroy();
|
|
373
|
+
this._deathModules.length = 0;
|
|
374
|
+
this._invalidateProgram();
|
|
375
|
+
return this;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Brings one particle into existence and returns a writer for its initial
|
|
379
|
+
* values, or `null` when the system is at {@link capacity}.
|
|
380
|
+
*
|
|
381
|
+
* The particle starts at the spawn defaults - origin, no velocity, unit
|
|
382
|
+
* scale, no rotation, opaque white, frame 0, one second of life - so a caller
|
|
383
|
+
* writes only what it varies. The returned writer is a cursor that the next
|
|
384
|
+
* `emit()` rebinds, so it must not be stored.
|
|
385
|
+
*
|
|
386
|
+
* Emission is the only per-particle write that is true on every backend:
|
|
387
|
+
* spawn values originate on the CPU and are uploaded from there, while
|
|
388
|
+
* everything the simulation integrates afterwards lives wherever the
|
|
389
|
+
* simulation runs.
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* ```ts
|
|
393
|
+
* const particle = system.emit();
|
|
394
|
+
*
|
|
395
|
+
* if (particle) {
|
|
396
|
+
* particle.position.set(120, 40);
|
|
397
|
+
* particle.velocity.set(0, -80);
|
|
398
|
+
* particle.lifetime = 2;
|
|
399
|
+
* }
|
|
400
|
+
* ```
|
|
401
|
+
*/
|
|
402
|
+
emit() {
|
|
403
|
+
const slot = this._spawnSlot();
|
|
404
|
+
if (slot < 0) return null;
|
|
405
|
+
return this._writer.bind(slot);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Allocates one slot at the spawn defaults and returns its index, or `-1` at
|
|
409
|
+
* capacity. Backs {@link emit}; the simulation's own machinery addresses
|
|
410
|
+
* particles by slot, the public surface does not.
|
|
411
|
+
* @internal
|
|
412
|
+
*/
|
|
413
|
+
_spawnSlot() {
|
|
414
|
+
const slot = this._gpuMode ? this._spawnGpu() : this._spawnCpu();
|
|
415
|
+
if (slot >= 0) this._storage.reset(slot);
|
|
416
|
+
return slot;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Begin recording the slots handed out by {@link spawn}.
|
|
420
|
+
*
|
|
421
|
+
* Callers that post-process freshly spawned particles cannot infer which
|
|
422
|
+
* slots those are from {@link liveCount}: in GPU mode a spawn may recycle a
|
|
423
|
+
* dead slot below the high-water mark, leaving the count unchanged, and the
|
|
424
|
+
* reused slots are scattered rather than contiguous. Pass the returned token
|
|
425
|
+
* back to {@link _endSpawnRecording}.
|
|
426
|
+
*
|
|
427
|
+
* @internal
|
|
428
|
+
*/
|
|
429
|
+
_beginSpawnRecording() {
|
|
430
|
+
const previous = this._spawnRecord;
|
|
431
|
+
this._spawnRecord = [];
|
|
432
|
+
return previous;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* End the recording started by {@link _beginSpawnRecording} and return the
|
|
436
|
+
* slots allocated during it. Slots are also handed to an enclosing recording
|
|
437
|
+
* so a nested spawn stays visible to the outer window.
|
|
438
|
+
*
|
|
439
|
+
* @internal
|
|
440
|
+
*/
|
|
441
|
+
_endSpawnRecording(previous) {
|
|
442
|
+
const recorded = this._spawnRecord ?? [];
|
|
443
|
+
this._spawnRecord = previous;
|
|
444
|
+
if (previous !== null) for (const slot of recorded) previous.push(slot);
|
|
445
|
+
return recorded;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Shifts the particles a sub-emitter just produced by `(x, y)`, so a child
|
|
449
|
+
* spawner's distributions read as offsets from the death position.
|
|
450
|
+
* @internal
|
|
451
|
+
*/
|
|
452
|
+
_offsetSpawned(slots, x, y) {
|
|
453
|
+
const { posX, posY } = this._storage;
|
|
454
|
+
for (const slot of slots) {
|
|
455
|
+
posX[slot] = posX[slot] + x;
|
|
456
|
+
posY[slot] = posY[slot] + y;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/** Resets the system to zero live particles without destroying it. */
|
|
460
|
+
clearParticles() {
|
|
461
|
+
const storage = this._storage;
|
|
462
|
+
storage.count = 0;
|
|
463
|
+
this._spawnHint = 0;
|
|
464
|
+
storage.alive.fill(0);
|
|
465
|
+
storage.lifetime.fill(0);
|
|
466
|
+
storage.elapsed.fill(0);
|
|
467
|
+
return this;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* @internal
|
|
471
|
+
*
|
|
472
|
+
* Collect-hook: captures the active backend before this node is emitted so
|
|
473
|
+
* the next `update()` can compile a GPU pipeline if the backend turned out
|
|
474
|
+
* to be `WebGpuBackend`. Re-captures and rebuilds when the backend
|
|
475
|
+
* reference changes (e.g. after device-loss recovery).
|
|
476
|
+
*/
|
|
477
|
+
/** @internal */
|
|
478
|
+
_collect(builder, seq) {
|
|
479
|
+
const backend = builder.backend;
|
|
480
|
+
if (this._backend !== backend) {
|
|
481
|
+
this._backend = backend;
|
|
482
|
+
if (this._gpuState !== null) {
|
|
483
|
+
this._gpuState.destroy();
|
|
484
|
+
this._gpuState = null;
|
|
485
|
+
}
|
|
486
|
+
this._gpuMode = false;
|
|
487
|
+
this._compiled = false;
|
|
488
|
+
}
|
|
489
|
+
super._collect(builder, seq);
|
|
490
|
+
}
|
|
491
|
+
/** Per-frame entry point. Routes to CPU or GPU pipeline based on auto-detection at first call. */
|
|
492
|
+
update(delta) {
|
|
493
|
+
if (!this._compiled) this._compile();
|
|
494
|
+
const dt = delta;
|
|
495
|
+
for (let i = 0; i < this._spawnModules.length; i++) this._spawnModules[i].apply(this, dt);
|
|
496
|
+
if (this._gpuMode) this._updateGpu(dt);
|
|
497
|
+
else this._updateCpu(dt);
|
|
498
|
+
return this;
|
|
499
|
+
}
|
|
500
|
+
destroy() {
|
|
501
|
+
super.destroy();
|
|
502
|
+
this.clearSpawnModules();
|
|
503
|
+
this.clearUpdateModules();
|
|
504
|
+
this.clearDeathModules();
|
|
505
|
+
if (this._ownsRenderMode) this._renderMode.destroy();
|
|
506
|
+
if (this._gpuState !== null) {
|
|
507
|
+
this._gpuState.destroy();
|
|
508
|
+
this._gpuState = null;
|
|
509
|
+
}
|
|
510
|
+
for (const frame of this._frames) frame.destroy();
|
|
511
|
+
this._frames.length = 0;
|
|
512
|
+
this._gpuMode = false;
|
|
513
|
+
this._compiled = false;
|
|
514
|
+
this._storage.count = 0;
|
|
515
|
+
this._storage.alive.fill(0);
|
|
516
|
+
this._textureFrame.destroy();
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Marks the compiled program stale. The next update decides what that costs:
|
|
520
|
+
* a GPU system that stays GPU-eligible rebuilds only its program, everything
|
|
521
|
+
* else follows the transition rules in {@link _compile}.
|
|
522
|
+
*/
|
|
523
|
+
_invalidateProgram() {
|
|
524
|
+
this._compiled = false;
|
|
525
|
+
}
|
|
526
|
+
_compile() {
|
|
527
|
+
this._compiled = true;
|
|
528
|
+
const eligible = this._updateModules.every((m) => typeof m.wgsl === "function") && this._renderMode.gpuEligible;
|
|
529
|
+
if (this._gpuState !== null) {
|
|
530
|
+
if (eligible) {
|
|
531
|
+
this._gpuState.setProgram(this._updateModules, this._deathModules.length > 0);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
this._gpuState.destroy();
|
|
535
|
+
this._gpuState = null;
|
|
536
|
+
this._gpuMode = false;
|
|
537
|
+
this._gpuDirtySlots.clear();
|
|
538
|
+
this.clearParticles();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const backendDevice = this._backend?.device ?? null;
|
|
542
|
+
const device = this._device ?? backendDevice;
|
|
543
|
+
if (device === null) return;
|
|
544
|
+
if (!eligible) return;
|
|
545
|
+
this._gpuState = new ParticleGpuState(device, this.capacity, this._updateModules, this._frames, this._texture, this._deathModules.length > 0);
|
|
546
|
+
this._gpuMode = true;
|
|
547
|
+
const { alive, count } = this._storage;
|
|
548
|
+
for (let i = 0; i < count; i++) if (alive[i] === 1) this._gpuDirtySlots.add(i);
|
|
549
|
+
}
|
|
550
|
+
_spawnCpu() {
|
|
551
|
+
const storage = this._storage;
|
|
552
|
+
if (storage.count >= this.capacity) return -1;
|
|
553
|
+
const slot = storage.count++;
|
|
554
|
+
storage.alive[slot] = 1;
|
|
555
|
+
this._spawnRecord?.push(slot);
|
|
556
|
+
return slot;
|
|
557
|
+
}
|
|
558
|
+
_spawnGpu() {
|
|
559
|
+
const capacity = this.capacity;
|
|
560
|
+
const alive = this._storage.alive;
|
|
561
|
+
const start = this._spawnHint;
|
|
562
|
+
for (let i = start; i < capacity; i++) if (alive[i] === 0) return this._claimGpuSlot(i, i + 1 === capacity ? 0 : i + 1);
|
|
563
|
+
for (let i = 0; i < start; i++) if (alive[i] === 0) return this._claimGpuSlot(i, i + 1);
|
|
564
|
+
return -1;
|
|
565
|
+
}
|
|
566
|
+
_claimGpuSlot(slot, nextHint) {
|
|
567
|
+
const storage = this._storage;
|
|
568
|
+
storage.alive[slot] = 1;
|
|
569
|
+
this._spawnHint = nextHint;
|
|
570
|
+
if (slot >= storage.count) storage.count = slot + 1;
|
|
571
|
+
this._gpuDirtySlots.add(slot);
|
|
572
|
+
this._spawnRecord?.push(slot);
|
|
573
|
+
return slot;
|
|
574
|
+
}
|
|
575
|
+
_updateCpu(dt) {
|
|
576
|
+
const storage = this._storage;
|
|
577
|
+
const { posX, posY, velX, velY, rotations, rotationSpeeds, elapsed, lifetime, alive } = storage;
|
|
578
|
+
const liveCount = storage.count;
|
|
579
|
+
for (let i = 0; i < liveCount; i++) {
|
|
580
|
+
posX[i] = posX[i] + velX[i] * dt;
|
|
581
|
+
posY[i] = posY[i] + velY[i] * dt;
|
|
582
|
+
rotations[i] = rotations[i] + rotationSpeeds[i] * dt;
|
|
583
|
+
elapsed[i] = elapsed[i] + dt;
|
|
584
|
+
}
|
|
585
|
+
for (let i = 0; i < this._updateModules.length; i++) this._updateModules[i].apply(storage, dt);
|
|
586
|
+
const deathModules = this._deathModules;
|
|
587
|
+
let writeIndex = 0;
|
|
588
|
+
for (let readIndex = 0; readIndex < storage.count; readIndex++) {
|
|
589
|
+
if (elapsed[readIndex] >= lifetime[readIndex]) {
|
|
590
|
+
if (deathModules.length > 0) this._reportDeath(storage.snapshot(readIndex));
|
|
591
|
+
alive[readIndex] = 0;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (writeIndex !== readIndex) {
|
|
595
|
+
storage.copySlot(readIndex, writeIndex);
|
|
596
|
+
alive[writeIndex] = 1;
|
|
597
|
+
}
|
|
598
|
+
writeIndex++;
|
|
599
|
+
}
|
|
600
|
+
for (let i = writeIndex; i < storage.count; i++) alive[i] = 0;
|
|
601
|
+
storage.count = writeIndex;
|
|
602
|
+
}
|
|
603
|
+
/** Hands one death snapshot to every registered death module, in registration order. */
|
|
604
|
+
_reportDeath(death) {
|
|
605
|
+
const deathModules = this._deathModules;
|
|
606
|
+
for (let m = 0; m < deathModules.length; m++) deathModules[m].onDeath(this, death);
|
|
607
|
+
}
|
|
608
|
+
_updateGpu(dt) {
|
|
609
|
+
const storage = this._storage;
|
|
610
|
+
const { elapsed, lifetime, alive } = storage;
|
|
611
|
+
const liveCount = storage.count;
|
|
612
|
+
const reportsDeaths = this._deathModules.length > 0;
|
|
613
|
+
for (let i = 0; i < liveCount; i++) {
|
|
614
|
+
if (alive[i] === 0) continue;
|
|
615
|
+
elapsed[i] = elapsed[i] + dt;
|
|
616
|
+
if (elapsed[i] >= lifetime[i]) {
|
|
617
|
+
if (reportsDeaths) {
|
|
618
|
+
const queued = this._pendingDeathLifetimes.get(i);
|
|
619
|
+
if (queued === void 0) this._pendingDeathLifetimes.set(i, [lifetime[i]]);
|
|
620
|
+
else queued.push(lifetime[i]);
|
|
621
|
+
this._pendingDeathCount++;
|
|
622
|
+
}
|
|
623
|
+
alive[i] = 0;
|
|
624
|
+
lifetime[i] = -1;
|
|
625
|
+
this._gpuDirtySlots.delete(i);
|
|
626
|
+
this._gpuState.uploadExpiry(i);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (this._gpuDirtySlots.size > 0) {
|
|
630
|
+
this._gpuState.uploadDirty(this, this._gpuDirtySlots);
|
|
631
|
+
this._gpuDirtySlots.clear();
|
|
632
|
+
}
|
|
633
|
+
const staged = this._gpuState.dispatch(dt, liveCount, this._pendingDeathCount);
|
|
634
|
+
let newLiveCount = storage.count;
|
|
635
|
+
while (newLiveCount > 0 && alive[newLiveCount - 1] === 0) newLiveCount--;
|
|
636
|
+
storage.count = newLiveCount;
|
|
637
|
+
if (staged) {
|
|
638
|
+
const pending = this._pendingDeathLifetimes;
|
|
639
|
+
this._pendingDeathLifetimes = /* @__PURE__ */ new Map();
|
|
640
|
+
this._pendingDeathCount = 0;
|
|
641
|
+
this._drainDeaths(pending);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Delivers a staged batch of deaths once its readback has landed. Each record
|
|
646
|
+
* carries what the device integrated; the lifetime comes from the CPU, which
|
|
647
|
+
* is where it was written at spawn.
|
|
648
|
+
*/
|
|
649
|
+
async _drainDeaths(pending) {
|
|
650
|
+
await this._gpuState?.readDeaths((records) => {
|
|
651
|
+
for (const record of records) this._reportDeath({
|
|
652
|
+
x: record.x,
|
|
653
|
+
y: record.y,
|
|
654
|
+
velocityX: record.velocityX,
|
|
655
|
+
velocityY: record.velocityY,
|
|
656
|
+
rotation: record.rotation,
|
|
657
|
+
scaleX: record.scaleX,
|
|
658
|
+
scaleY: record.scaleY,
|
|
659
|
+
color: record.color,
|
|
660
|
+
elapsed: record.elapsed,
|
|
661
|
+
lifetime: pending.get(record.slot)?.shift() ?? 0
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
};
|
|
567
666
|
|
|
667
|
+
//#endregion
|
|
568
668
|
export { ParticleSystem };
|
|
569
|
-
//# sourceMappingURL=ParticleSystem.js.map
|
|
669
|
+
//# sourceMappingURL=ParticleSystem.js.map
|