topazcube 0.1.30 → 0.1.33

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 (96) hide show
  1. package/LICENSE.txt +0 -0
  2. package/README.md +0 -0
  3. package/dist/Renderer.cjs +18200 -0
  4. package/dist/Renderer.cjs.map +1 -0
  5. package/dist/Renderer.js +18183 -0
  6. package/dist/Renderer.js.map +1 -0
  7. package/dist/client.cjs +94 -260
  8. package/dist/client.cjs.map +1 -1
  9. package/dist/client.js +71 -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} +173 -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 +572 -0
  33. package/src/renderer/Geometry.js +1049 -0
  34. package/src/renderer/Material.js +61 -0
  35. package/src/renderer/Mesh.js +211 -0
  36. package/src/renderer/Node.js +112 -0
  37. package/src/renderer/Pipeline.js +643 -0
  38. package/src/renderer/Renderer.js +1324 -0
  39. package/src/renderer/Skin.js +792 -0
  40. package/src/renderer/Texture.js +584 -0
  41. package/src/renderer/core/AssetManager.js +359 -0
  42. package/src/renderer/core/CullingSystem.js +307 -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 +546 -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 +2064 -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 +417 -0
  58. package/src/renderer/rendering/passes/FogPass.js +419 -0
  59. package/src/renderer/rendering/passes/GBufferPass.js +706 -0
  60. package/src/renderer/rendering/passes/HiZPass.js +714 -0
  61. package/src/renderer/rendering/passes/LightingPass.js +739 -0
  62. package/src/renderer/rendering/passes/ParticlePass.js +835 -0
  63. package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
  64. package/src/renderer/rendering/passes/PostProcessPass.js +282 -0
  65. package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
  66. package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
  67. package/src/renderer/rendering/passes/SSGIPass.js +265 -0
  68. package/src/renderer/rendering/passes/SSGITilePass.js +296 -0
  69. package/src/renderer/rendering/passes/ShadowPass.js +1822 -0
  70. package/src/renderer/rendering/passes/TransparentPass.js +831 -0
  71. package/src/renderer/rendering/shaders/ao.wgsl +182 -0
  72. package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
  73. package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
  74. package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
  75. package/src/renderer/rendering/shaders/geometry.wgsl +550 -0
  76. package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
  77. package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
  78. package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
  79. package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
  80. package/src/renderer/rendering/shaders/particle_render.wgsl +525 -0
  81. package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
  82. package/src/renderer/rendering/shaders/postproc.wgsl +272 -0
  83. package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
  84. package/src/renderer/rendering/shaders/shadow.wgsl +76 -0
  85. package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
  86. package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
  87. package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
  88. package/src/renderer/utils/BoundingSphere.js +439 -0
  89. package/src/renderer/utils/Frustum.js +281 -0
  90. package/dist/client.d.cts +0 -211
  91. package/dist/client.d.ts +0 -211
  92. package/dist/server.d.cts +0 -120
  93. package/dist/server.d.ts +0 -120
  94. package/dist/terminal.d.cts +0 -64
  95. package/dist/terminal.d.ts +0 -64
  96. package/src/utils.ts +0 -403
@@ -0,0 +1,835 @@
1
+ import { BasePass } from "./BasePass.js"
2
+ import { Texture } from "../../Texture.js"
3
+
4
+ import particleSimulateWGSL from "../shaders/particle_simulate.wgsl"
5
+ import particleRenderWGSL from "../shaders/particle_render.wgsl"
6
+
7
+ /**
8
+ * ParticlePass - GPU particle simulation and rendering
9
+ *
10
+ * Handles:
11
+ * - Compute shader dispatch for particle spawning and simulation
12
+ * - Forward rendering with additive or alpha blending
13
+ * - Soft particle depth fade
14
+ * - Billboard rendering using camera vectors
15
+ */
16
+ class ParticlePass extends BasePass {
17
+ constructor(engine = null) {
18
+ super('Particles', engine)
19
+
20
+ // Compute pipelines
21
+ this.spawnPipeline = null
22
+ this.simulatePipeline = null
23
+ this.resetPipeline = null
24
+
25
+ // Render pipelines (by blend mode)
26
+ this.renderPipelineAdditive = null
27
+ this.renderPipelineAlpha = null
28
+
29
+ // Bind groups
30
+ this.computeBindGroupLayout = null
31
+ this.renderBindGroupLayout = null
32
+
33
+ // Uniform buffers
34
+ this.simulationUniformBuffer = null
35
+ this.renderUniformBuffer = null
36
+ this.emitterSettingsBuffer = null
37
+ this.emitterRenderSettingsBuffer = null // For lit + emissive per emitter
38
+
39
+ // References
40
+ this.particleSystem = null
41
+ this.gbuffer = null
42
+ this.outputTexture = null
43
+ this.shadowPass = null // For shadow maps
44
+ this.environmentMap = null // For IBL
45
+ this.environmentEncoding = 0 // 0=equirect, 1=octahedral
46
+ this.lightingPass = null // For point/spot lights buffer
47
+
48
+ // Per-emitter texture cache
49
+ this._textureCache = new Map() // textureUrl -> {texture, bindGroup}
50
+
51
+ // Placeholder resources
52
+ this._placeholderTexture = null
53
+ this._placeholderSampler = null
54
+
55
+ // Frame timing
56
+ this._lastTime = 0
57
+ this._deltaTime = 0
58
+ }
59
+
60
+ /**
61
+ * Set particle system reference
62
+ */
63
+ setParticleSystem(particleSystem) {
64
+ this.particleSystem = particleSystem
65
+ }
66
+
67
+ /**
68
+ * Set GBuffer for depth testing
69
+ */
70
+ setGBuffer(gbuffer) {
71
+ this.gbuffer = gbuffer
72
+ }
73
+
74
+ /**
75
+ * Set output texture to render onto
76
+ */
77
+ setOutputTexture(texture) {
78
+ this.outputTexture = texture
79
+ }
80
+
81
+ /**
82
+ * Set shadow pass for accessing shadow maps
83
+ */
84
+ setShadowPass(shadowPass) {
85
+ this.shadowPass = shadowPass
86
+ }
87
+
88
+ /**
89
+ * Set environment map for IBL lighting
90
+ * @param {Texture} envMap - Environment map texture
91
+ * @param {number} encoding - 0=equirectangular, 1=octahedral
92
+ */
93
+ setEnvironmentMap(envMap, encoding = 0) {
94
+ this.environmentMap = envMap
95
+ this.environmentEncoding = encoding
96
+ }
97
+
98
+ /**
99
+ * Set lighting pass for accessing point/spot lights buffer
100
+ * @param {LightingPass} lightingPass
101
+ */
102
+ setLightingPass(lightingPass) {
103
+ this.lightingPass = lightingPass
104
+ }
105
+
106
+ async _init() {
107
+ const { device } = this.engine
108
+
109
+ // Create uniform buffers
110
+ // SimulationUniforms: dt, time, maxParticles, emitterCount + lighting params
111
+ // dt(f32) + time(f32) + maxParticles(u32) + emitterCount(u32) = 16 bytes
112
+ // cameraPosition(vec3f) + shadowBias(f32) = 16 bytes
113
+ // lightDir(vec3f) + shadowStrength(f32) = 16 bytes
114
+ // lightColor(vec4f) = 16 bytes
115
+ // ambientColor(vec4f) = 16 bytes
116
+ // cascadeSizes(vec4f) = 16 bytes
117
+ // lightCount(u32) + pad(3xu32) = 16 bytes
118
+ // Total: 112 bytes
119
+ this.simulationUniformBuffer = device.createBuffer({
120
+ size: 112,
121
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
122
+ label: 'Particle Simulation Uniforms'
123
+ })
124
+
125
+ this.renderUniformBuffer = device.createBuffer({
126
+ size: 368, // ParticleUniforms struct (increased for lighting + IBL + light count)
127
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
128
+ label: 'Particle Render Uniforms'
129
+ })
130
+
131
+ // Emitter settings buffer: 16 emitters * 48 bytes each = 768 bytes
132
+ this.emitterSettingsBuffer = device.createBuffer({
133
+ size: 16 * 48,
134
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
135
+ label: 'Particle Emitter Settings'
136
+ })
137
+
138
+ // Emitter render settings buffer: 16 emitters * 16 bytes each = 256 bytes
139
+ // Contains: lit, emissive, softness, zOffset per emitter
140
+ this.emitterRenderSettingsBuffer = device.createBuffer({
141
+ size: 16 * 16,
142
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
143
+ label: 'Particle Emitter Render Settings'
144
+ })
145
+
146
+ // Create placeholder resources
147
+ this._placeholderTexture = await Texture.fromRGBA(this.engine, 1, 1, 1, 1)
148
+ this._placeholderSampler = device.createSampler({
149
+ magFilter: 'linear',
150
+ minFilter: 'linear',
151
+ mipmapFilter: 'linear'
152
+ })
153
+
154
+ // Placeholder comparison sampler for shadow sampling
155
+ this._placeholderComparisonSampler = device.createSampler({
156
+ compare: 'less',
157
+ magFilter: 'linear',
158
+ minFilter: 'linear'
159
+ })
160
+
161
+ // Placeholder depth texture array for shadow maps (when shadows disabled)
162
+ this._placeholderDepthTexture = device.createTexture({
163
+ size: [1, 1, 1],
164
+ format: 'depth32float',
165
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
166
+ })
167
+ this._placeholderDepthView = this._placeholderDepthTexture.createView({
168
+ dimension: '2d-array',
169
+ arrayLayerCount: 1
170
+ })
171
+
172
+ // Placeholder buffer for cascade matrices (3 mat4 = 192 bytes)
173
+ this._placeholderMatricesBuffer = device.createBuffer({
174
+ size: 192,
175
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
176
+ label: 'Placeholder Cascade Matrices'
177
+ })
178
+
179
+ // Placeholder lights buffer (64 lights * 64 bytes each = 4096 bytes)
180
+ // Light struct: enabled(u32) + position(vec3f) + color(vec4f) + direction(vec3f) + geom(vec4f) + shadowIndex(i32)
181
+ this._placeholderLightsBuffer = device.createBuffer({
182
+ size: 64 * 64,
183
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
184
+ label: 'Placeholder Lights Buffer'
185
+ })
186
+
187
+ // Placeholder spot shadow atlas (1x1 depth texture)
188
+ this._placeholderSpotShadowTexture = device.createTexture({
189
+ size: [1, 1],
190
+ format: 'depth32float',
191
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
192
+ })
193
+ this._placeholderSpotShadowView = this._placeholderSpotShadowTexture.createView()
194
+
195
+ // Placeholder spot shadow matrices buffer (8 mat4 = 512 bytes)
196
+ this._placeholderSpotMatricesBuffer = device.createBuffer({
197
+ size: 8 * 64,
198
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
199
+ label: 'Placeholder Spot Shadow Matrices'
200
+ })
201
+
202
+ // Create compute pipelines
203
+ await this._createComputePipelines()
204
+
205
+ // Create render pipelines
206
+ await this._createRenderPipelines()
207
+ }
208
+
209
+ async _createComputePipelines() {
210
+ const { device } = this.engine
211
+
212
+ // Compute shader module
213
+ const computeModule = device.createShaderModule({
214
+ label: 'Particle Compute Shader',
215
+ code: particleSimulateWGSL
216
+ })
217
+
218
+ // Bind group layout for compute (includes lighting bindings for shadow sampling)
219
+ this.computeBindGroupLayout = device.createBindGroupLayout({
220
+ label: 'Particle Compute BindGroup Layout',
221
+ entries: [
222
+ // Core bindings
223
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
224
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
225
+ { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
226
+ { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
227
+ { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
228
+ // Lighting bindings
229
+ { binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // emitterRenderSettings
230
+ { binding: 6, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: 'depth', viewDimension: '2d-array' } }, // shadowMapArray
231
+ { binding: 7, visibility: GPUShaderStage.COMPUTE, sampler: { type: 'comparison' } }, // shadowSampler
232
+ { binding: 8, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // cascadeMatrices
233
+ { binding: 9, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // lights
234
+ { binding: 10, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: 'depth' } }, // spotShadowAtlas
235
+ { binding: 11, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // spotMatrices
236
+ ]
237
+ })
238
+
239
+ const computePipelineLayout = device.createPipelineLayout({
240
+ bindGroupLayouts: [this.computeBindGroupLayout]
241
+ })
242
+
243
+ // Spawn pipeline
244
+ this.spawnPipeline = await device.createComputePipelineAsync({
245
+ label: 'Particle Spawn Pipeline',
246
+ layout: computePipelineLayout,
247
+ compute: {
248
+ module: computeModule,
249
+ entryPoint: 'spawn'
250
+ }
251
+ })
252
+
253
+ // Simulate pipeline
254
+ this.simulatePipeline = await device.createComputePipelineAsync({
255
+ label: 'Particle Simulate Pipeline',
256
+ layout: computePipelineLayout,
257
+ compute: {
258
+ module: computeModule,
259
+ entryPoint: 'simulate'
260
+ }
261
+ })
262
+
263
+ // Reset counters pipeline
264
+ this.resetPipeline = await device.createComputePipelineAsync({
265
+ label: 'Particle Reset Pipeline',
266
+ layout: computePipelineLayout,
267
+ compute: {
268
+ module: computeModule,
269
+ entryPoint: 'resetCounters'
270
+ }
271
+ })
272
+ }
273
+
274
+ async _createRenderPipelines() {
275
+ const { device } = this.engine
276
+
277
+ // Render shader module
278
+ const renderModule = device.createShaderModule({
279
+ label: 'Particle Render Shader',
280
+ code: particleRenderWGSL
281
+ })
282
+
283
+ // Bind group layout for rendering
284
+ this.renderBindGroupLayout = device.createBindGroupLayout({
285
+ label: 'Particle Render BindGroup Layout',
286
+ entries: [
287
+ // Uniforms
288
+ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
289
+ // Particle storage buffer (read only in render)
290
+ { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
291
+ // Particle texture
292
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
293
+ { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
294
+ // Depth texture for soft particles (GBuffer depth32float)
295
+ { binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
296
+ // Shadow map array (cascade shadows)
297
+ { binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth', viewDimension: '2d-array' } },
298
+ // Shadow comparison sampler
299
+ { binding: 6, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
300
+ // Cascade matrices (storage buffer)
301
+ { binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } },
302
+ // Emitter render settings (lit, emissive, softness, zOffset)
303
+ { binding: 8, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } },
304
+ // Environment map for IBL
305
+ { binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
306
+ { binding: 10, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
307
+ // Point/spot lights buffer
308
+ { binding: 11, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } },
309
+ // Spot shadow atlas
310
+ { binding: 12, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
311
+ { binding: 13, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
312
+ // Spot shadow matrices
313
+ { binding: 14, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } },
314
+ ]
315
+ })
316
+
317
+ const renderPipelineLayout = device.createPipelineLayout({
318
+ bindGroupLayouts: [this.renderBindGroupLayout]
319
+ })
320
+
321
+ // Additive blend pipeline
322
+ this.renderPipelineAdditive = await device.createRenderPipelineAsync({
323
+ label: 'Particle Render Pipeline (Additive)',
324
+ layout: renderPipelineLayout,
325
+ vertex: {
326
+ module: renderModule,
327
+ entryPoint: 'vertexMain',
328
+ buffers: [] // All data from storage buffer
329
+ },
330
+ fragment: {
331
+ module: renderModule,
332
+ entryPoint: 'fragmentMainAdditive',
333
+ targets: [{
334
+ format: 'rgba16float',
335
+ blend: {
336
+ color: {
337
+ srcFactor: 'one', // Additive: src + dst
338
+ dstFactor: 'one',
339
+ operation: 'add'
340
+ },
341
+ alpha: {
342
+ srcFactor: 'one',
343
+ dstFactor: 'one',
344
+ operation: 'add'
345
+ }
346
+ }
347
+ }]
348
+ },
349
+ depthStencil: {
350
+ format: 'depth32float',
351
+ depthWriteEnabled: false, // No depth write for particles
352
+ depthCompare: 'less-equal', // Standard depth test with linear depth
353
+ },
354
+ primitive: {
355
+ topology: 'triangle-list',
356
+ cullMode: 'none',
357
+ }
358
+ })
359
+
360
+ // Alpha blend pipeline
361
+ this.renderPipelineAlpha = await device.createRenderPipelineAsync({
362
+ label: 'Particle Render Pipeline (Alpha)',
363
+ layout: renderPipelineLayout,
364
+ vertex: {
365
+ module: renderModule,
366
+ entryPoint: 'vertexMain',
367
+ buffers: []
368
+ },
369
+ fragment: {
370
+ module: renderModule,
371
+ entryPoint: 'fragmentMainAlpha',
372
+ targets: [{
373
+ format: 'rgba16float',
374
+ blend: {
375
+ color: {
376
+ srcFactor: 'src-alpha',
377
+ dstFactor: 'one-minus-src-alpha',
378
+ operation: 'add'
379
+ },
380
+ alpha: {
381
+ srcFactor: 'one',
382
+ dstFactor: 'one-minus-src-alpha',
383
+ operation: 'add'
384
+ }
385
+ }
386
+ }]
387
+ },
388
+ depthStencil: {
389
+ format: 'depth32float',
390
+ depthWriteEnabled: false,
391
+ depthCompare: 'less-equal', // Standard depth test with linear depth
392
+ },
393
+ primitive: {
394
+ topology: 'triangle-list',
395
+ cullMode: 'none',
396
+ }
397
+ })
398
+ }
399
+
400
+ async _execute(context) {
401
+ const { device, canvas, stats } = this.engine
402
+ const { camera, time, mainLight } = context
403
+
404
+ if (!this.particleSystem || !this.outputTexture || !this.gbuffer) {
405
+ return
406
+ }
407
+
408
+ // Initialize particle system if needed
409
+ await this.particleSystem.init()
410
+
411
+ // Calculate delta time
412
+ const currentTime = time ?? (performance.now() / 1000)
413
+ this._deltaTime = this._lastTime > 0 ? currentTime - this._lastTime : 0.016
414
+ this._lastTime = currentTime
415
+
416
+ // Clamp delta time to prevent huge jumps
417
+ this._deltaTime = Math.min(this._deltaTime, 0.1)
418
+
419
+ // Update particle system (CPU side - spawn rate accumulation)
420
+ this.particleSystem.update(this._deltaTime)
421
+
422
+ const emitters = this.particleSystem.getActiveEmitters()
423
+ if (emitters.length === 0) {
424
+ return
425
+ }
426
+
427
+ // Create command encoder
428
+ const commandEncoder = device.createCommandEncoder({ label: 'Particle Pass' })
429
+
430
+ // === COMPUTE PHASE (includes lighting calculation) ===
431
+ await this._executeCompute(commandEncoder, emitters, camera, mainLight)
432
+
433
+ // === RENDER PHASE ===
434
+ await this._executeRender(commandEncoder, emitters, camera, mainLight)
435
+
436
+ // Submit
437
+ device.queue.submit([commandEncoder.finish()])
438
+
439
+ // Update stats
440
+ if (stats) {
441
+ stats.particleEmitters = emitters.length
442
+ stats.particleCount = this.particleSystem.getTotalParticleCount()
443
+ }
444
+ }
445
+
446
+ async _executeCompute(commandEncoder, emitters, camera, mainLight) {
447
+ const { device } = this.engine
448
+
449
+ // Get spawn count BEFORE executing (executeSpawn clears the queue)
450
+ const spawnCount = Math.min(this.particleSystem._spawnQueue?.length || 0, 1000)
451
+
452
+ // Execute spawns - writes data to GPU buffers
453
+ this.particleSystem.executeSpawn(commandEncoder)
454
+
455
+ // Get lighting settings
456
+ const mainLightEnabled = mainLight?.enabled !== false
457
+ const mainLightIntensity = mainLight?.intensity ?? 1.0
458
+ const mainLightColor = mainLight?.color || [1.0, 0.95, 0.9]
459
+ const lightDir = mainLight?.direction || [-1, 1, -0.5]
460
+
461
+ // Normalize light direction
462
+ const lightDirLen = Math.sqrt(lightDir[0] * lightDir[0] + lightDir[1] * lightDir[1] + lightDir[2] * lightDir[2])
463
+ const normalizedLightDir = [
464
+ lightDir[0] / lightDirLen,
465
+ lightDir[1] / lightDirLen,
466
+ lightDir[2] / lightDirLen
467
+ ]
468
+
469
+ // Get ambient and shadow settings
470
+ const ambientColor = this.settings?.ambient?.color || [0.1, 0.12, 0.15]
471
+ const ambientIntensity = this.settings?.ambient?.intensity ?? 0.3
472
+ const shadowConfig = this.settings?.shadow || {}
473
+ const shadowBias = shadowConfig.bias ?? 0.0005
474
+ const shadowStrength = shadowConfig.strength ?? 0.8
475
+ const cascadeSizes = shadowConfig.cascadeSizes || [10, 25, 100]
476
+ const lightCount = this.lightingPass?.lights?.length ?? 0
477
+
478
+ // Update simulation uniforms (28 floats = 112 bytes)
479
+ const uniformData = new Float32Array(28)
480
+ uniformData[0] = this._deltaTime // dt
481
+ uniformData[1] = this._lastTime // time
482
+ const uniformIntView = new Uint32Array(uniformData.buffer)
483
+ uniformIntView[2] = this.particleSystem.globalMaxParticles // maxParticles
484
+ uniformIntView[3] = emitters.length // emitterCount
485
+ // cameraPosition (vec3f) + shadowBias (f32) - floats 4-7
486
+ uniformData[4] = camera.position[0]
487
+ uniformData[5] = camera.position[1]
488
+ uniformData[6] = camera.position[2]
489
+ uniformData[7] = shadowBias
490
+ // lightDir (vec3f) + shadowStrength (f32) - floats 8-11
491
+ uniformData[8] = normalizedLightDir[0]
492
+ uniformData[9] = normalizedLightDir[1]
493
+ uniformData[10] = normalizedLightDir[2]
494
+ uniformData[11] = shadowStrength
495
+ // lightColor (vec4f) - floats 12-15
496
+ uniformData[12] = mainLightColor[0]
497
+ uniformData[13] = mainLightColor[1]
498
+ uniformData[14] = mainLightColor[2]
499
+ uniformData[15] = mainLightEnabled ? mainLightIntensity * 12.0 : 0.0
500
+ // ambientColor (vec4f) - floats 16-19
501
+ uniformData[16] = ambientColor[0]
502
+ uniformData[17] = ambientColor[1]
503
+ uniformData[18] = ambientColor[2]
504
+ uniformData[19] = ambientIntensity
505
+ // cascadeSizes (vec4f) - floats 20-23
506
+ uniformData[20] = cascadeSizes[0]
507
+ uniformData[21] = cascadeSizes[1]
508
+ uniformData[22] = cascadeSizes[2]
509
+ uniformData[23] = 0.0 // padding
510
+ // lightCount (u32) + padding - floats 24-27
511
+ uniformIntView[24] = lightCount
512
+ uniformIntView[25] = 0
513
+ uniformIntView[26] = 0
514
+ uniformIntView[27] = 0
515
+
516
+ device.queue.writeBuffer(this.simulationUniformBuffer, 0, uniformData)
517
+
518
+ // Update per-emitter settings buffer
519
+ // EmitterSettings: gravity(vec3f), drag, turbulence, fadeIn, fadeOut, rotationSpeed, startSize, endSize, baseAlpha, padding
520
+ // = 12 floats = 48 bytes per emitter
521
+ const emitterData = new Float32Array(16 * 12) // 16 emitters max
522
+ for (let i = 0; i < Math.min(emitters.length, 16); i++) {
523
+ const e = emitters[i]
524
+ const offset = i * 12
525
+ // gravity (vec3f) + drag (f32)
526
+ emitterData[offset + 0] = e.gravity[0]
527
+ emitterData[offset + 1] = e.gravity[1]
528
+ emitterData[offset + 2] = e.gravity[2]
529
+ emitterData[offset + 3] = e.drag
530
+ // turbulence + fadeIn + fadeOut + rotationSpeed
531
+ emitterData[offset + 4] = e.turbulence
532
+ emitterData[offset + 5] = e.fadeIn
533
+ emitterData[offset + 6] = e.fadeOut
534
+ emitterData[offset + 7] = e.rotationSpeed ?? 0.5
535
+ // startSize + endSize + baseAlpha + padding
536
+ emitterData[offset + 8] = e.size[0]
537
+ emitterData[offset + 9] = e.size[1]
538
+ emitterData[offset + 10] = e.color[3] // baseAlpha
539
+ emitterData[offset + 11] = 0 // padding
540
+ }
541
+
542
+ device.queue.writeBuffer(this.emitterSettingsBuffer, 0, emitterData)
543
+
544
+ // Create compute bind group with lighting resources
545
+ const computeBindGroup = device.createBindGroup({
546
+ layout: this.computeBindGroupLayout,
547
+ entries: [
548
+ // Core bindings
549
+ { binding: 0, resource: { buffer: this.simulationUniformBuffer } },
550
+ { binding: 1, resource: { buffer: this.particleSystem.getParticleBuffer() } },
551
+ { binding: 2, resource: { buffer: this.particleSystem.getCounterBuffer() } },
552
+ { binding: 3, resource: { buffer: this.particleSystem.getSpawnBuffer() } },
553
+ { binding: 4, resource: { buffer: this.emitterSettingsBuffer } },
554
+ // Lighting bindings
555
+ { binding: 5, resource: { buffer: this.emitterRenderSettingsBuffer } },
556
+ { binding: 6, resource: this.shadowPass?.getShadowMapView() || this._placeholderDepthView },
557
+ { binding: 7, resource: this.shadowPass?.getShadowSampler() || this._placeholderComparisonSampler },
558
+ { binding: 8, resource: { buffer: this.shadowPass?.getCascadeMatricesBuffer() || this._placeholderMatricesBuffer } },
559
+ { binding: 9, resource: { buffer: this.lightingPass?.lightBuffer || this._placeholderLightsBuffer } },
560
+ { binding: 10, resource: this.shadowPass?.getSpotShadowAtlasView?.() || this._placeholderSpotShadowView },
561
+ { binding: 11, resource: { buffer: this.shadowPass?.getSpotMatricesBuffer?.() || this._placeholderSpotMatricesBuffer } },
562
+ ]
563
+ })
564
+
565
+ // Dispatch spawn compute (using spawnCount captured before executeSpawn cleared the queue)
566
+ if (spawnCount > 0) {
567
+ const spawnPass = commandEncoder.beginComputePass({ label: 'Particle Spawn' })
568
+ spawnPass.setPipeline(this.spawnPipeline)
569
+ spawnPass.setBindGroup(0, computeBindGroup)
570
+ spawnPass.dispatchWorkgroups(Math.ceil(spawnCount / 64))
571
+ spawnPass.end()
572
+ }
573
+
574
+ // Dispatch simulate compute
575
+ const maxParticles = this.particleSystem.globalMaxParticles
576
+ const simulatePass = commandEncoder.beginComputePass({ label: 'Particle Simulate' })
577
+ simulatePass.setPipeline(this.simulatePipeline)
578
+ simulatePass.setBindGroup(0, computeBindGroup)
579
+ simulatePass.dispatchWorkgroups(Math.ceil(maxParticles / 64))
580
+ simulatePass.end()
581
+ }
582
+
583
+ async _executeRender(commandEncoder, emitters, camera, mainLight) {
584
+ const { device, canvas } = this.engine
585
+
586
+ // Extract camera right/up from view matrix if not provided
587
+ // View matrix rows are: right, up, forward (transposed world axes)
588
+ let cameraRight = camera.right
589
+ let cameraUp = camera.up
590
+
591
+ if (!cameraRight && camera.view) {
592
+ // First row of view matrix is camera right vector
593
+ cameraRight = [camera.view[0], camera.view[4], camera.view[8]]
594
+ }
595
+ if (!cameraUp && camera.view) {
596
+ // Second row of view matrix is camera up vector
597
+ cameraUp = [camera.view[1], camera.view[5], camera.view[9]]
598
+ }
599
+
600
+ // Fallback defaults
601
+ cameraRight = cameraRight || [1, 0, 0]
602
+ cameraUp = cameraUp || [0, 1, 0]
603
+
604
+ // Get lighting settings
605
+ const mainLightEnabled = mainLight?.enabled !== false
606
+ const mainLightIntensity = mainLight?.intensity ?? 1.0
607
+ const mainLightColor = mainLight?.color || [1.0, 0.95, 0.9]
608
+ const lightDir = mainLight?.direction || [-1, 1, -0.5]
609
+
610
+ // Normalize light direction
611
+ const lightDirLen = Math.sqrt(lightDir[0] * lightDir[0] + lightDir[1] * lightDir[1] + lightDir[2] * lightDir[2])
612
+ const normalizedLightDir = [
613
+ lightDir[0] / lightDirLen,
614
+ lightDir[1] / lightDirLen,
615
+ lightDir[2] / lightDirLen
616
+ ]
617
+
618
+ // Get ambient color from settings
619
+ const ambientColor = this.settings?.ambient?.color || [0.1, 0.12, 0.15]
620
+ const ambientIntensity = this.settings?.ambient?.intensity ?? 0.3
621
+
622
+ // Get shadow settings
623
+ const shadowConfig = this.settings?.shadow || {}
624
+ const shadowBias = shadowConfig.bias ?? 0.0005
625
+ const shadowStrength = shadowConfig.strength ?? 0.8
626
+ const shadowMapSize = shadowConfig.mapSize ?? 2048
627
+ const cascadeSizes = shadowConfig.cascadeSizes || [10, 25, 100]
628
+
629
+ // Get IBL settings - use actual mip count from environment map
630
+ const envSettings = this.settings?.environment || {}
631
+ const envDiffuseLevel = envSettings.diffuseLevel ?? 0.5
632
+ const envMipCount = this.environmentMap?.mipCount ?? envSettings.envMipCount ?? 8
633
+ const envExposure = envSettings.exposure ?? 1.0
634
+
635
+ // Get light count from lighting pass
636
+ const lightCount = this.lightingPass?.lights?.length ?? 0
637
+
638
+ // Update render uniforms (92 floats = 368 bytes)
639
+ const uniformData = new Float32Array(92)
640
+ // viewMatrix (mat4x4f) - floats 0-15
641
+ uniformData.set(camera.view, 0)
642
+ // projectionMatrix (mat4x4f) - floats 16-31
643
+ uniformData.set(camera.proj, 16)
644
+ // cameraPosition (vec3f) + time (f32) - floats 32-35
645
+ uniformData.set(camera.position, 32)
646
+ uniformData[35] = this._lastTime
647
+ // cameraRight (vec3f) + softness (f32) - floats 36-39
648
+ uniformData.set(cameraRight, 36)
649
+ uniformData[39] = emitters[0]?.softness ?? 0.25
650
+ // cameraUp (vec3f) + zOffset (f32) - floats 40-43
651
+ uniformData.set(cameraUp, 40)
652
+ uniformData[43] = emitters[0]?.zOffset ?? 0.01
653
+ // screenSize (vec2f) + near + far - floats 44-47
654
+ uniformData[44] = canvas.width
655
+ uniformData[45] = canvas.height
656
+ uniformData[46] = camera.near ?? 0.1
657
+ uniformData[47] = camera.far ?? 1000
658
+ // blendMode + lit + shadowBias + shadowStrength - floats 48-51
659
+ // blendMode set per-pass below
660
+ uniformData[49] = emitters[0]?.lit ? 1.0 : 0.0
661
+ uniformData[50] = shadowBias
662
+ uniformData[51] = shadowStrength
663
+ // lightDir (vec3f) + shadowMapSize (f32) - floats 52-55
664
+ uniformData.set(normalizedLightDir, 52)
665
+ uniformData[55] = shadowMapSize
666
+ // lightColor (vec4f) - floats 56-59
667
+ // Multiply intensity by 12.0 to match LightingPass convention
668
+ uniformData[56] = mainLightColor[0]
669
+ uniformData[57] = mainLightColor[1]
670
+ uniformData[58] = mainLightColor[2]
671
+ uniformData[59] = mainLightEnabled ? mainLightIntensity * 12.0 : 0.0
672
+ // ambientColor (vec4f) - floats 60-63
673
+ uniformData[60] = ambientColor[0]
674
+ uniformData[61] = ambientColor[1]
675
+ uniformData[62] = ambientColor[2]
676
+ uniformData[63] = ambientIntensity
677
+ // cascadeSizes (vec4f) - floats 64-67
678
+ uniformData[64] = cascadeSizes[0]
679
+ uniformData[65] = cascadeSizes[1]
680
+ uniformData[66] = cascadeSizes[2]
681
+ uniformData[67] = 0.0 // padding
682
+ // envParams (vec4f) - floats 68-71
683
+ // x = diffuse level, y = mip count, z = encoding (0=equirect, 1=octahedral), w = exposure
684
+ uniformData[68] = this.environmentMap ? envDiffuseLevel : 0.0
685
+ uniformData[69] = envMipCount
686
+ uniformData[70] = this.environmentEncoding === 'octahedral' || this.environmentEncoding === 1 ? 1.0 : 0.0
687
+ uniformData[71] = envExposure
688
+
689
+ // lightParams (vec4u) - floats 72-75 (stored as u32)
690
+ // x = light count, y = unused, z = unused, w = unused
691
+ const uniformIntView = new Uint32Array(uniformData.buffer)
692
+ uniformIntView[72] = lightCount
693
+ uniformIntView[73] = 0
694
+ uniformIntView[74] = 0
695
+ uniformIntView[75] = 0
696
+
697
+ // Fog uniforms - floats 76-91
698
+ const fogSettings = this.settings?.environment?.fog || {}
699
+ const fogEnabled = fogSettings.enabled ?? false
700
+ const fogColor = fogSettings.color ?? [0.8, 0.85, 0.9]
701
+ const fogDistances = fogSettings.distances ?? [0, 50, 200]
702
+ const fogAlphas = fogSettings.alpha ?? [0.0, 0.3, 0.8]
703
+ const fogHeightFade = fogSettings.heightFade ?? [-10, 100]
704
+ const fogBrightResist = fogSettings.brightResist ?? 0.8
705
+
706
+ // fogColor (vec3f) + fogEnabled (f32) - floats 76-79
707
+ uniformData[76] = fogColor[0]
708
+ uniformData[77] = fogColor[1]
709
+ uniformData[78] = fogColor[2]
710
+ uniformData[79] = fogEnabled ? 1.0 : 0.0
711
+ // fogDistances (vec3f) + fogBrightResist (f32) - floats 80-83
712
+ uniformData[80] = fogDistances[0]
713
+ uniformData[81] = fogDistances[1]
714
+ uniformData[82] = fogDistances[2]
715
+ uniformData[83] = fogBrightResist
716
+ // fogAlphas (vec3f) + fogPad1 (f32) - floats 84-87
717
+ uniformData[84] = fogAlphas[0]
718
+ uniformData[85] = fogAlphas[1]
719
+ uniformData[86] = fogAlphas[2]
720
+ uniformData[87] = 0.0 // padding
721
+ // fogHeightFade (vec2f) + fogPad2 (vec2f) - floats 88-91
722
+ uniformData[88] = fogHeightFade[0] // bottomY
723
+ uniformData[89] = fogHeightFade[1] // topY
724
+ uniformData[90] = 0.0 // padding
725
+ uniformData[91] = 0.0 // padding
726
+
727
+ // Update emitter render settings buffer (lit, emissive, softness, zOffset per emitter)
728
+ const emitterRenderData = new Float32Array(16 * 4) // 16 emitters * 4 floats
729
+ for (let i = 0; i < Math.min(emitters.length, 16); i++) {
730
+ const e = emitters[i]
731
+ const offset = i * 4
732
+ emitterRenderData[offset + 0] = e.lit ? 1.0 : 0.0
733
+ emitterRenderData[offset + 1] = e.emissive ?? 1.0
734
+ emitterRenderData[offset + 2] = e.softness ?? 0.25
735
+ emitterRenderData[offset + 3] = e.zOffset ?? 0.01
736
+ }
737
+ device.queue.writeBuffer(this.emitterRenderSettingsBuffer, 0, emitterRenderData)
738
+
739
+ // Get or load particle texture
740
+ const textureUrl = emitters[0]?.texture
741
+ const particleTexture = textureUrl
742
+ ? await this.particleSystem.loadTexture(textureUrl)
743
+ : this.particleSystem.getDefaultTexture()
744
+
745
+ // Create render bind group - use GBuffer depth directly (depthReadOnly allows this)
746
+ const renderBindGroup = device.createBindGroup({
747
+ layout: this.renderBindGroupLayout,
748
+ entries: [
749
+ { binding: 0, resource: { buffer: this.renderUniformBuffer } },
750
+ { binding: 1, resource: { buffer: this.particleSystem.getParticleBuffer() } },
751
+ { binding: 2, resource: particleTexture?.view || this._placeholderTexture.view },
752
+ { binding: 3, resource: particleTexture?.sampler || this._placeholderSampler },
753
+ { binding: 4, resource: this.gbuffer.depth.view },
754
+ { binding: 5, resource: this.shadowPass?.getShadowMapView() || this._placeholderDepthView },
755
+ { binding: 6, resource: this.shadowPass?.getShadowSampler() || this._placeholderComparisonSampler },
756
+ { binding: 7, resource: { buffer: this.shadowPass?.getCascadeMatricesBuffer() || this._placeholderMatricesBuffer } },
757
+ { binding: 8, resource: { buffer: this.emitterRenderSettingsBuffer } },
758
+ { binding: 9, resource: this.environmentMap?.view || this._placeholderTexture.view },
759
+ { binding: 10, resource: this.environmentMap?.sampler || this._placeholderSampler },
760
+ // Point/spot lights buffer from lighting pass
761
+ { binding: 11, resource: { buffer: this.lightingPass?.lightBuffer || this._placeholderLightsBuffer } },
762
+ // Spot shadow atlas (uses same sampler as cascade shadows)
763
+ { binding: 12, resource: this.shadowPass?.getSpotShadowAtlasView?.() || this._placeholderSpotShadowView },
764
+ { binding: 13, resource: this.shadowPass?.getShadowSampler?.() || this._placeholderComparisonSampler },
765
+ // Spot shadow matrices
766
+ { binding: 14, resource: { buffer: this.shadowPass?.getSpotMatricesBuffer?.() || this._placeholderSpotMatricesBuffer } },
767
+ ]
768
+ })
769
+
770
+ const maxParticles = this.particleSystem.globalMaxParticles
771
+
772
+ // Check which blend modes are in use
773
+ const hasAlpha = emitters.some(e => e.blendMode !== 'additive')
774
+ const hasAdditive = emitters.some(e => e.blendMode === 'additive')
775
+
776
+ // Draw alpha-blended particles first (back-to-front would be ideal, but we draw all)
777
+ if (hasAlpha) {
778
+ uniformData[48] = 0.0 // blendMode = alpha
779
+ device.queue.writeBuffer(this.renderUniformBuffer, 0, uniformData)
780
+
781
+ const renderPass = commandEncoder.beginRenderPass({
782
+ colorAttachments: [{
783
+ view: this.outputTexture.view,
784
+ loadOp: 'load',
785
+ storeOp: 'store',
786
+ }],
787
+ depthStencilAttachment: {
788
+ view: this.gbuffer.depth.view,
789
+ depthReadOnly: true,
790
+ },
791
+ label: 'Particle Render (Alpha)'
792
+ })
793
+
794
+ renderPass.setPipeline(this.renderPipelineAlpha)
795
+ renderPass.setBindGroup(0, renderBindGroup)
796
+ renderPass.draw(6, maxParticles, 0, 0)
797
+ renderPass.end()
798
+ }
799
+
800
+ // Draw additive particles (order doesn't matter for additive)
801
+ if (hasAdditive) {
802
+ uniformData[48] = 1.0 // blendMode = additive
803
+ device.queue.writeBuffer(this.renderUniformBuffer, 0, uniformData)
804
+
805
+ const renderPass = commandEncoder.beginRenderPass({
806
+ colorAttachments: [{
807
+ view: this.outputTexture.view,
808
+ loadOp: 'load',
809
+ storeOp: 'store',
810
+ }],
811
+ depthStencilAttachment: {
812
+ view: this.gbuffer.depth.view,
813
+ depthReadOnly: true,
814
+ },
815
+ label: 'Particle Render (Additive)'
816
+ })
817
+
818
+ renderPass.setPipeline(this.renderPipelineAdditive)
819
+ renderPass.setBindGroup(0, renderBindGroup)
820
+ renderPass.draw(6, maxParticles, 0, 0)
821
+ renderPass.end()
822
+ }
823
+ }
824
+
825
+ async _resize(width, height) {
826
+ // Nothing to resize - uses shared resources
827
+ }
828
+
829
+ _destroy() {
830
+ this._textureCache.clear()
831
+ this.particleSystem = null
832
+ }
833
+ }
834
+
835
+ export { ParticlePass }