topazcube 0.1.31 → 0.1.35

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.
Files changed (103) hide show
  1. package/LICENSE.txt +0 -0
  2. package/README.md +0 -0
  3. package/dist/Renderer.cjs +20844 -0
  4. package/dist/Renderer.cjs.map +1 -0
  5. package/dist/Renderer.js +20827 -0
  6. package/dist/Renderer.js.map +1 -0
  7. package/dist/client.cjs +91 -260
  8. package/dist/client.cjs.map +1 -1
  9. package/dist/client.js +68 -215
  10. package/dist/client.js.map +1 -1
  11. package/dist/server.cjs +165 -432
  12. package/dist/server.cjs.map +1 -1
  13. package/dist/server.js +117 -370
  14. package/dist/server.js.map +1 -1
  15. package/dist/terminal.cjs +113 -200
  16. package/dist/terminal.cjs.map +1 -1
  17. package/dist/terminal.js +50 -51
  18. package/dist/terminal.js.map +1 -1
  19. package/dist/utils-CRhi1BDa.cjs +259 -0
  20. package/dist/utils-CRhi1BDa.cjs.map +1 -0
  21. package/dist/utils-D7tXt6-2.js +260 -0
  22. package/dist/utils-D7tXt6-2.js.map +1 -0
  23. package/package.json +19 -15
  24. package/src/{client.ts → network/client.js} +170 -403
  25. package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
  26. package/src/{compress-node.ts → network/compress-node.js} +8 -14
  27. package/src/{server.ts → network/server.js} +229 -317
  28. package/src/{terminal.js → network/terminal.js} +0 -0
  29. package/src/{topazcube.ts → network/topazcube.js} +2 -2
  30. package/src/network/utils.js +375 -0
  31. package/src/renderer/Camera.js +191 -0
  32. package/src/renderer/DebugUI.js +703 -0
  33. package/src/renderer/Geometry.js +1049 -0
  34. package/src/renderer/Material.js +64 -0
  35. package/src/renderer/Mesh.js +211 -0
  36. package/src/renderer/Node.js +112 -0
  37. package/src/renderer/Pipeline.js +645 -0
  38. package/src/renderer/Renderer.js +1496 -0
  39. package/src/renderer/Skin.js +792 -0
  40. package/src/renderer/Texture.js +584 -0
  41. package/src/renderer/core/AssetManager.js +394 -0
  42. package/src/renderer/core/CullingSystem.js +308 -0
  43. package/src/renderer/core/EntityManager.js +541 -0
  44. package/src/renderer/core/InstanceManager.js +343 -0
  45. package/src/renderer/core/ParticleEmitter.js +358 -0
  46. package/src/renderer/core/ParticleSystem.js +564 -0
  47. package/src/renderer/core/SpriteSystem.js +349 -0
  48. package/src/renderer/gltf.js +563 -0
  49. package/src/renderer/math.js +161 -0
  50. package/src/renderer/rendering/HistoryBufferManager.js +333 -0
  51. package/src/renderer/rendering/ProbeCapture.js +1495 -0
  52. package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
  53. package/src/renderer/rendering/RenderGraph.js +2258 -0
  54. package/src/renderer/rendering/passes/AOPass.js +308 -0
  55. package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
  56. package/src/renderer/rendering/passes/BasePass.js +101 -0
  57. package/src/renderer/rendering/passes/BloomPass.js +420 -0
  58. package/src/renderer/rendering/passes/CRTPass.js +724 -0
  59. package/src/renderer/rendering/passes/FogPass.js +445 -0
  60. package/src/renderer/rendering/passes/GBufferPass.js +730 -0
  61. package/src/renderer/rendering/passes/HiZPass.js +744 -0
  62. package/src/renderer/rendering/passes/LightingPass.js +753 -0
  63. package/src/renderer/rendering/passes/ParticlePass.js +841 -0
  64. package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
  65. package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
  66. package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
  67. package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
  68. package/src/renderer/rendering/passes/SSGIPass.js +266 -0
  69. package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
  70. package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
  71. package/src/renderer/rendering/passes/TransparentPass.js +831 -0
  72. package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
  73. package/src/renderer/rendering/shaders/ao.wgsl +182 -0
  74. package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
  75. package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
  76. package/src/renderer/rendering/shaders/crt.wgsl +455 -0
  77. package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
  78. package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
  79. package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
  80. package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
  81. package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
  82. package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
  83. package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
  84. package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
  85. package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
  86. package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
  87. package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
  88. package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
  89. package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
  90. package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
  91. package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
  92. package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
  93. package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
  94. package/src/renderer/utils/BoundingSphere.js +439 -0
  95. package/src/renderer/utils/Frustum.js +281 -0
  96. package/src/renderer/utils/Raycaster.js +761 -0
  97. package/dist/client.d.cts +0 -211
  98. package/dist/client.d.ts +0 -211
  99. package/dist/server.d.cts +0 -120
  100. package/dist/server.d.ts +0 -120
  101. package/dist/terminal.d.cts +0 -64
  102. package/dist/terminal.d.ts +0 -64
  103. package/src/utils.ts +0 -403
@@ -0,0 +1,564 @@
1
+ import { ParticleEmitter } from "./ParticleEmitter.js"
2
+ import { Texture } from "../Texture.js"
3
+
4
+ /**
5
+ * ParticleSystem - Main system managing particle emitters and GPU resources
6
+ *
7
+ * Handles:
8
+ * - Emitter registry and lifecycle
9
+ * - GPU buffer allocation with global budget
10
+ * - Texture caching for particle sprites
11
+ * - Compute shader dispatch for particle simulation
12
+ * - Data preparation for particle rendering
13
+ */
14
+
15
+ // Particle struct size in bytes (must match WGSL)
16
+ // position (vec3f) + lifetime (f32) + velocity (vec3f) + maxLifetime (f32) +
17
+ // color (vec4f) + size (vec2f) + rotation (f32) + flags (u32) +
18
+ // lighting (vec3f) + lightingPad (f32) = 80 bytes
19
+ const PARTICLE_STRIDE = 80
20
+
21
+ // Spawn request struct size: position (vec3f) + velocity (vec3f) + lifetime (f32) +
22
+ // maxLifetime (f32) + color (vec4f) + startSize (f32) + endSize (f32) +
23
+ // seed (f32) + flags (u32) = 64 bytes
24
+ const SPAWN_REQUEST_STRIDE = 64
25
+
26
+ class ParticleSystem {
27
+ constructor(engine) {
28
+ this.engine = engine
29
+ this.device = engine.device
30
+
31
+ // Global particle budget
32
+ this.globalMaxParticles = 50000
33
+ this.globalAliveCount = 0
34
+
35
+ // Emitter registry: uid -> ParticleEmitter
36
+ this._emitters = new Map()
37
+
38
+ // Active emitter list (for iteration)
39
+ this._activeEmitters = []
40
+
41
+ // Texture cache: url -> Texture
42
+ this._textureCache = new Map()
43
+
44
+ // Default particle texture (white circle)
45
+ this._defaultTexture = null
46
+
47
+ // GPU resources (created on first use)
48
+ this._particleBuffer = null // Storage buffer for all particles
49
+ this._spawnBuffer = null // Buffer for spawn requests
50
+ this._emitterBuffer = null // Buffer for emitter uniforms
51
+ this._counterBuffer = null // Atomic counters
52
+ this._readbackBuffer = null // For reading counter values
53
+
54
+ // Compute pipeline (created by ParticlePass)
55
+ this.simulatePipeline = null
56
+ this.spawnPipeline = null
57
+
58
+ // Time tracking
59
+ this._time = 0
60
+ this._lastSpawnTime = 0
61
+
62
+ // Spawn queue: accumulated spawn requests to send to GPU
63
+ this._spawnQueue = []
64
+ this._maxSpawnPerFrame = 1000 // Limit spawns per frame
65
+
66
+ // Emitter uniforms buffer data
67
+ this._emitterData = new Float32Array(32) // Per-emitter uniforms
68
+
69
+ this._initialized = false
70
+ }
71
+
72
+ /**
73
+ * Initialize GPU resources
74
+ */
75
+ async init() {
76
+ if (this._initialized) return
77
+
78
+ // Create particle storage buffer
79
+ this._particleBuffer = this.device.createBuffer({
80
+ size: this.globalMaxParticles * PARTICLE_STRIDE,
81
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
82
+ label: 'ParticleSystem particles'
83
+ })
84
+
85
+ // Create spawn request buffer (sized for max spawns per frame)
86
+ this._spawnBuffer = this.device.createBuffer({
87
+ size: this._maxSpawnPerFrame * SPAWN_REQUEST_STRIDE,
88
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
89
+ label: 'ParticleSystem spawn requests'
90
+ })
91
+
92
+ // Create counter buffer: [aliveCount, nextFreeIndex, spawnCount, frameCount]
93
+ this._counterBuffer = this.device.createBuffer({
94
+ size: 16,
95
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
96
+ label: 'ParticleSystem counters'
97
+ })
98
+
99
+ // Readback buffer for counter values
100
+ this._readbackBuffer = this.device.createBuffer({
101
+ size: 16,
102
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
103
+ label: 'ParticleSystem counter readback'
104
+ })
105
+
106
+ // Create emitter uniforms buffer
107
+ this._emitterBuffer = this.device.createBuffer({
108
+ size: 256, // Enough for emitter parameters
109
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
110
+ label: 'ParticleSystem emitter uniforms'
111
+ })
112
+
113
+ // Initialize counters
114
+ const counterData = new Uint32Array([0, 0, 0, 0]) // [aliveCount, nextFreeIndex, spawnCount, frameCount]
115
+ this.device.queue.writeBuffer(this._counterBuffer, 0, counterData)
116
+
117
+ // Create default texture (white circle with soft edges)
118
+ this._defaultTexture = await this._createDefaultTexture()
119
+
120
+ this._initialized = true
121
+ }
122
+
123
+ /**
124
+ * Create a default particle texture (soft white circle)
125
+ */
126
+ async _createDefaultTexture() {
127
+ const size = 64
128
+ const data = new Uint8Array(size * size * 4)
129
+
130
+ const center = size / 2
131
+ const maxRadius = size / 2
132
+
133
+ for (let y = 0; y < size; y++) {
134
+ for (let x = 0; x < size; x++) {
135
+ const dx = x - center + 0.5
136
+ const dy = y - center + 0.5
137
+ const dist = Math.sqrt(dx * dx + dy * dy)
138
+
139
+ // Soft circular falloff
140
+ const alpha = Math.max(0, 1 - dist / maxRadius)
141
+ const softAlpha = alpha * alpha * (3 - 2 * alpha) // Smoothstep
142
+
143
+ const i = (y * size + x) * 4
144
+ data[i + 0] = 255 // R
145
+ data[i + 1] = 255 // G
146
+ data[i + 2] = 255 // B
147
+ data[i + 3] = Math.floor(softAlpha * 255) // A
148
+ }
149
+ }
150
+
151
+ return Texture.fromRawData(this.engine, data, size, size, {
152
+ srgb: true,
153
+ generateMips: true
154
+ })
155
+ }
156
+
157
+ /**
158
+ * Load or get cached particle texture
159
+ * @param {string} url - Texture URL
160
+ * @returns {Promise<Texture>}
161
+ */
162
+ async loadTexture(url) {
163
+ if (!url) return this._defaultTexture
164
+
165
+ if (this._textureCache.has(url)) {
166
+ return this._textureCache.get(url)
167
+ }
168
+
169
+ const texture = await Texture.fromImage(this.engine, url, {
170
+ srgb: true,
171
+ generateMips: true,
172
+ flipY: true
173
+ })
174
+
175
+ this._textureCache.set(url, texture)
176
+ return texture
177
+ }
178
+
179
+ /**
180
+ * Register a new particle emitter
181
+ * @param {ParticleEmitter|Object} config - Emitter or configuration
182
+ * @returns {ParticleEmitter}
183
+ */
184
+ addEmitter(config) {
185
+ const emitter = config instanceof ParticleEmitter
186
+ ? config
187
+ : new ParticleEmitter(config)
188
+
189
+ this._emitters.set(emitter.uid, emitter)
190
+ this._activeEmitters.push(emitter)
191
+
192
+ // Queue initial burst if specified
193
+ if (emitter.spawnBurst > 0) {
194
+ this._queueSpawn(emitter, emitter.spawnBurst)
195
+ }
196
+
197
+ return emitter
198
+ }
199
+
200
+ /**
201
+ * Remove an emitter
202
+ * @param {number|ParticleEmitter} emitterOrId - Emitter or UID
203
+ */
204
+ removeEmitter(emitterOrId) {
205
+ const uid = typeof emitterOrId === 'number' ? emitterOrId : emitterOrId.uid
206
+ const emitter = this._emitters.get(uid)
207
+ if (!emitter) return
208
+
209
+ this._emitters.delete(uid)
210
+ const idx = this._activeEmitters.indexOf(emitter)
211
+ if (idx >= 0) this._activeEmitters.splice(idx, 1)
212
+ }
213
+
214
+ /**
215
+ * Get emitter by UID
216
+ * @param {number} uid - Emitter UID
217
+ * @returns {ParticleEmitter|null}
218
+ */
219
+ getEmitter(uid) {
220
+ return this._emitters.get(uid) || null
221
+ }
222
+
223
+ /**
224
+ * Queue particles for spawning
225
+ * @param {ParticleEmitter} emitter - Emitter to spawn from
226
+ * @param {number} count - Number of particles to spawn
227
+ */
228
+ _queueSpawn(emitter, count) {
229
+ if (!emitter.enabled) return
230
+
231
+ // Respect global budget
232
+ const available = this.globalMaxParticles - this.globalAliveCount
233
+ const toSpawn = Math.min(count, available, emitter.maxParticles - emitter.aliveCount)
234
+
235
+ if (toSpawn <= 0) return
236
+
237
+ for (let i = 0; i < toSpawn; i++) {
238
+ // Generate random seeds
239
+ const seed1 = Math.random()
240
+ const seed2 = Math.random()
241
+ const seed3 = Math.random()
242
+ const seed4 = Math.random()
243
+ const seed5 = Math.random()
244
+
245
+ // Calculate spawn position and velocity
246
+ const position = emitter.getSpawnPosition([seed1, seed2, seed3])
247
+ const direction = emitter.getEmissionDirection([seed4, seed5])
248
+ const speed = ParticleEmitter.randomInRange(emitter.speed, Math.random())
249
+ const velocity = [
250
+ direction[0] * speed,
251
+ direction[1] * speed,
252
+ direction[2] * speed
253
+ ]
254
+
255
+ // Calculate lifetime
256
+ const lifetime = ParticleEmitter.randomInRange(emitter.lifetime, Math.random())
257
+
258
+ // Random rotation: -PI to +PI (sign determines spin direction)
259
+ const rotation = (Math.random() - 0.5) * Math.PI * 2
260
+
261
+ // Flags: bit 0 = alive, bit 1 = additive, bits 8-15 = emitter index
262
+ const emitterIndex = this._activeEmitters.indexOf(emitter)
263
+ const flags = 1 | (emitter.blendMode === 'additive' ? 2 : 0) | ((emitterIndex & 0xFF) << 8)
264
+
265
+ this._spawnQueue.push({
266
+ emitter,
267
+ position,
268
+ velocity,
269
+ lifetime,
270
+ maxLifetime: lifetime,
271
+ color: [...emitter.color],
272
+ startSize: emitter.size[0],
273
+ endSize: emitter.size[1],
274
+ rotation,
275
+ flags
276
+ })
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Update particle system
282
+ * @param {number} dt - Delta time in seconds
283
+ */
284
+ update(dt) {
285
+ this._time += dt
286
+
287
+ // Estimate particle deaths based on average lifetime
288
+ // This prevents globalAliveCount from growing forever
289
+ this._estimateDeaths(dt)
290
+
291
+ // Process spawn rates for each emitter
292
+ for (const emitter of this._activeEmitters) {
293
+ if (!emitter.enabled || emitter.spawnRate <= 0) continue
294
+
295
+ // Accumulate spawn time
296
+ emitter.spawnAccumulator += dt * emitter.spawnRate
297
+ const toSpawn = Math.floor(emitter.spawnAccumulator)
298
+
299
+ if (toSpawn > 0) {
300
+ emitter.spawnAccumulator -= toSpawn
301
+ this._queueSpawn(emitter, toSpawn)
302
+ }
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Estimate particle deaths based on spawn history and average lifetime
308
+ * @param {number} dt - Delta time
309
+ */
310
+ _estimateDeaths(dt) {
311
+ // Simple estimation: assume deaths occur at roughly the spawn rate
312
+ // after accounting for average particle lifetime
313
+ // This is approximate but prevents the counter from growing forever
314
+
315
+ let totalSpawnRate = 0
316
+ let totalAvgLifetime = 0
317
+ let activeCount = 0
318
+
319
+ for (const emitter of this._activeEmitters) {
320
+ if (emitter.enabled && emitter.spawnRate > 0) {
321
+ totalSpawnRate += emitter.spawnRate
322
+ const avgLifetime = (emitter.lifetime[0] + emitter.lifetime[1]) / 2
323
+ totalAvgLifetime += avgLifetime
324
+ activeCount++
325
+ }
326
+ }
327
+
328
+ if (activeCount > 0) {
329
+ const avgLifetime = totalAvgLifetime / activeCount
330
+ // At steady state: deaths per second ≈ spawn rate
331
+ // Apply deaths proportional to dt
332
+ const estimatedDeaths = totalSpawnRate * dt
333
+ this.globalAliveCount = Math.max(0, this.globalAliveCount - estimatedDeaths)
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Write spawn requests to GPU and execute spawn compute pass
339
+ * @param {GPUCommandEncoder} commandEncoder
340
+ */
341
+ executeSpawn(commandEncoder) {
342
+ if (this._spawnQueue.length === 0) return
343
+
344
+ // Limit spawns per frame
345
+ const toProcess = Math.min(this._spawnQueue.length, this._maxSpawnPerFrame)
346
+ const spawnData = new Float32Array(toProcess * (SPAWN_REQUEST_STRIDE / 4))
347
+
348
+ for (let i = 0; i < toProcess; i++) {
349
+ const spawn = this._spawnQueue[i]
350
+ const offset = i * 16 // 16 floats per spawn
351
+
352
+ // position (vec3f) + lifetime (f32)
353
+ spawnData[offset + 0] = spawn.position[0]
354
+ spawnData[offset + 1] = spawn.position[1]
355
+ spawnData[offset + 2] = spawn.position[2]
356
+ spawnData[offset + 3] = spawn.lifetime
357
+
358
+ // velocity (vec3f) + maxLifetime (f32)
359
+ spawnData[offset + 4] = spawn.velocity[0]
360
+ spawnData[offset + 5] = spawn.velocity[1]
361
+ spawnData[offset + 6] = spawn.velocity[2]
362
+ spawnData[offset + 7] = spawn.maxLifetime
363
+
364
+ // color (vec4f)
365
+ spawnData[offset + 8] = spawn.color[0]
366
+ spawnData[offset + 9] = spawn.color[1]
367
+ spawnData[offset + 10] = spawn.color[2]
368
+ spawnData[offset + 11] = spawn.color[3]
369
+
370
+ // startSize + endSize + rotation + flags
371
+ spawnData[offset + 12] = spawn.startSize
372
+ spawnData[offset + 13] = spawn.endSize
373
+ spawnData[offset + 14] = spawn.rotation
374
+ // flags needs to be written as uint32
375
+ }
376
+
377
+ // Write flags separately as uint32
378
+ const flagsView = new Uint32Array(spawnData.buffer)
379
+ for (let i = 0; i < toProcess; i++) {
380
+ flagsView[i * 16 + 15] = this._spawnQueue[i].flags
381
+ }
382
+
383
+ // Upload spawn data
384
+ this.device.queue.writeBuffer(this._spawnBuffer, 0, spawnData)
385
+
386
+ // Update spawn count in counter buffer
387
+ const counterUpdate = new Uint32Array([0, 0, toProcess, 0])
388
+ this.device.queue.writeBuffer(this._counterBuffer, 8, counterUpdate.subarray(2, 3))
389
+
390
+ // Clear processed spawns
391
+ this._spawnQueue.splice(0, toProcess)
392
+
393
+ // Update alive count estimate
394
+ this.globalAliveCount += toProcess
395
+ for (const emitter of this._activeEmitters) {
396
+ emitter.totalSpawned += toProcess
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Prepare emitter uniforms for compute/render
402
+ * @param {ParticleEmitter} emitter
403
+ * @returns {Float32Array}
404
+ */
405
+ getEmitterUniforms(emitter) {
406
+ const data = this._emitterData
407
+
408
+ // Gravity (vec3f) + dt (f32)
409
+ data[0] = emitter.gravity[0]
410
+ data[1] = emitter.gravity[1]
411
+ data[2] = emitter.gravity[2]
412
+ data[3] = 0 // dt will be set by pass
413
+
414
+ // drag + turbulence + fadeIn + fadeOut
415
+ data[4] = emitter.drag
416
+ data[5] = emitter.turbulence
417
+ data[6] = emitter.fadeIn
418
+ data[7] = emitter.fadeOut
419
+
420
+ // startSize + endSize + time + maxParticles
421
+ data[8] = emitter.size[0]
422
+ data[9] = emitter.size[1]
423
+ data[10] = this._time
424
+ data[11] = emitter.maxParticles
425
+
426
+ // Color (vec4f)
427
+ data[12] = emitter.color[0]
428
+ data[13] = emitter.color[1]
429
+ data[14] = emitter.color[2]
430
+ data[15] = emitter.color[3]
431
+
432
+ // Softness + zOffset + blendMode (as float) + lit
433
+ data[16] = emitter.softness
434
+ data[17] = emitter.zOffset
435
+ data[18] = emitter.blendMode === 'additive' ? 1.0 : 0.0
436
+ data[19] = emitter.lit ? 1.0 : 0.0
437
+
438
+ return data
439
+ }
440
+
441
+ /**
442
+ * Get particle buffer for rendering
443
+ * @returns {GPUBuffer}
444
+ */
445
+ getParticleBuffer() {
446
+ return this._particleBuffer
447
+ }
448
+
449
+ /**
450
+ * Get counter buffer
451
+ * @returns {GPUBuffer}
452
+ */
453
+ getCounterBuffer() {
454
+ return this._counterBuffer
455
+ }
456
+
457
+ /**
458
+ * Get spawn buffer
459
+ * @returns {GPUBuffer}
460
+ */
461
+ getSpawnBuffer() {
462
+ return this._spawnBuffer
463
+ }
464
+
465
+ /**
466
+ * Get all active emitters
467
+ * @returns {ParticleEmitter[]}
468
+ */
469
+ getActiveEmitters() {
470
+ return this._activeEmitters.filter(e => e.enabled)
471
+ }
472
+
473
+ /**
474
+ * Get total particle count across all emitters
475
+ * @returns {number}
476
+ */
477
+ getTotalParticleCount() {
478
+ return this.globalAliveCount
479
+ }
480
+
481
+ /**
482
+ * Get default particle texture
483
+ * @returns {Texture}
484
+ */
485
+ getDefaultTexture() {
486
+ return this._defaultTexture
487
+ }
488
+
489
+ /**
490
+ * Reset all particles (clear all emitters)
491
+ */
492
+ reset() {
493
+ // Clear counters
494
+ const counterData = new Uint32Array([0, 0, 0, 0])
495
+ this.device.queue.writeBuffer(this._counterBuffer, 0, counterData)
496
+
497
+ this.globalAliveCount = 0
498
+ this._spawnQueue = []
499
+
500
+ for (const emitter of this._activeEmitters) {
501
+ emitter.aliveCount = 0
502
+ emitter.spawnAccumulator = 0
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Spawn a burst of particles at a specific position
508
+ * @param {Object} config - Burst configuration
509
+ * @param {number[]} config.position - [x, y, z] world position
510
+ * @param {number} config.count - Number of particles
511
+ * @param {Object} [config.overrides] - Emitter property overrides
512
+ * @returns {ParticleEmitter} Temporary emitter used for the burst
513
+ */
514
+ burst(config) {
515
+ const { position, count, overrides = {} } = config
516
+
517
+ // Create temporary emitter for this burst
518
+ const emitter = new ParticleEmitter({
519
+ position,
520
+ spawnRate: 0, // No continuous spawning
521
+ spawnBurst: count,
522
+ ...overrides
523
+ })
524
+
525
+ // Add emitter (burst will be queued automatically)
526
+ this.addEmitter(emitter)
527
+
528
+ return emitter
529
+ }
530
+
531
+ /**
532
+ * Destroy and clean up resources
533
+ */
534
+ destroy() {
535
+ if (this._particleBuffer) {
536
+ this._particleBuffer.destroy()
537
+ this._particleBuffer = null
538
+ }
539
+ if (this._spawnBuffer) {
540
+ this._spawnBuffer.destroy()
541
+ this._spawnBuffer = null
542
+ }
543
+ if (this._counterBuffer) {
544
+ this._counterBuffer.destroy()
545
+ this._counterBuffer = null
546
+ }
547
+ if (this._readbackBuffer) {
548
+ this._readbackBuffer.destroy()
549
+ this._readbackBuffer = null
550
+ }
551
+ if (this._emitterBuffer) {
552
+ this._emitterBuffer.destroy()
553
+ this._emitterBuffer = null
554
+ }
555
+
556
+ this._textureCache.clear()
557
+ this._emitters.clear()
558
+ this._activeEmitters = []
559
+ this._spawnQueue = []
560
+ this._initialized = false
561
+ }
562
+ }
563
+
564
+ export { ParticleSystem, PARTICLE_STRIDE, SPAWN_REQUEST_STRIDE }