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.
- package/LICENSE.txt +0 -0
- package/README.md +0 -0
- package/dist/Renderer.cjs +20844 -0
- package/dist/Renderer.cjs.map +1 -0
- package/dist/Renderer.js +20827 -0
- package/dist/Renderer.js.map +1 -0
- package/dist/client.cjs +91 -260
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +68 -215
- package/dist/client.js.map +1 -1
- package/dist/server.cjs +165 -432
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +117 -370
- package/dist/server.js.map +1 -1
- package/dist/terminal.cjs +113 -200
- package/dist/terminal.cjs.map +1 -1
- package/dist/terminal.js +50 -51
- package/dist/terminal.js.map +1 -1
- package/dist/utils-CRhi1BDa.cjs +259 -0
- package/dist/utils-CRhi1BDa.cjs.map +1 -0
- package/dist/utils-D7tXt6-2.js +260 -0
- package/dist/utils-D7tXt6-2.js.map +1 -0
- package/package.json +19 -15
- package/src/{client.ts → network/client.js} +170 -403
- package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
- package/src/{compress-node.ts → network/compress-node.js} +8 -14
- package/src/{server.ts → network/server.js} +229 -317
- package/src/{terminal.js → network/terminal.js} +0 -0
- package/src/{topazcube.ts → network/topazcube.js} +2 -2
- package/src/network/utils.js +375 -0
- package/src/renderer/Camera.js +191 -0
- package/src/renderer/DebugUI.js +703 -0
- package/src/renderer/Geometry.js +1049 -0
- package/src/renderer/Material.js +64 -0
- package/src/renderer/Mesh.js +211 -0
- package/src/renderer/Node.js +112 -0
- package/src/renderer/Pipeline.js +645 -0
- package/src/renderer/Renderer.js +1496 -0
- package/src/renderer/Skin.js +792 -0
- package/src/renderer/Texture.js +584 -0
- package/src/renderer/core/AssetManager.js +394 -0
- package/src/renderer/core/CullingSystem.js +308 -0
- package/src/renderer/core/EntityManager.js +541 -0
- package/src/renderer/core/InstanceManager.js +343 -0
- package/src/renderer/core/ParticleEmitter.js +358 -0
- package/src/renderer/core/ParticleSystem.js +564 -0
- package/src/renderer/core/SpriteSystem.js +349 -0
- package/src/renderer/gltf.js +563 -0
- package/src/renderer/math.js +161 -0
- package/src/renderer/rendering/HistoryBufferManager.js +333 -0
- package/src/renderer/rendering/ProbeCapture.js +1495 -0
- package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
- package/src/renderer/rendering/RenderGraph.js +2258 -0
- package/src/renderer/rendering/passes/AOPass.js +308 -0
- package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
- package/src/renderer/rendering/passes/BasePass.js +101 -0
- package/src/renderer/rendering/passes/BloomPass.js +420 -0
- package/src/renderer/rendering/passes/CRTPass.js +724 -0
- package/src/renderer/rendering/passes/FogPass.js +445 -0
- package/src/renderer/rendering/passes/GBufferPass.js +730 -0
- package/src/renderer/rendering/passes/HiZPass.js +744 -0
- package/src/renderer/rendering/passes/LightingPass.js +753 -0
- package/src/renderer/rendering/passes/ParticlePass.js +841 -0
- package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
- package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
- package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
- package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
- package/src/renderer/rendering/passes/SSGIPass.js +266 -0
- package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
- package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
- package/src/renderer/rendering/passes/TransparentPass.js +831 -0
- package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
- package/src/renderer/rendering/shaders/ao.wgsl +182 -0
- package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
- package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/crt.wgsl +455 -0
- package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
- package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
- package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
- package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
- package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
- package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
- package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
- package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
- package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
- package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
- package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
- package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
- package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
- package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
- package/src/renderer/utils/BoundingSphere.js +439 -0
- package/src/renderer/utils/Frustum.js +281 -0
- package/src/renderer/utils/Raycaster.js +761 -0
- package/dist/client.d.cts +0 -211
- package/dist/client.d.ts +0 -211
- package/dist/server.d.cts +0 -120
- package/dist/server.d.ts +0 -120
- package/dist/terminal.d.cts +0 -64
- package/dist/terminal.d.ts +0 -64
- 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 }
|