@woosh/meep-engine 3.34.0 → 3.35.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.
Files changed (90) hide show
  1. package/package.json +1 -1
  2. package/src/shade/device/timing/profile/GPUProfileSession.d.ts +25 -11
  3. package/src/shade/device/timing/profile/GPUProfileSession.d.ts.map +1 -1
  4. package/src/shade/device/timing/profile/GPUProfileSession.js +553 -474
  5. package/src/shade/playground/avboit/main.d.ts.map +1 -1
  6. package/src/shade/playground/avboit/main.js +888 -916
  7. package/src/shade/playground/particle_ecs/README.md +13 -7
  8. package/src/shade/playground/particle_ecs/bonfire_effects.d.ts +5 -5
  9. package/src/shade/playground/particle_ecs/bonfire_effects.js +5 -5
  10. package/src/shade/playground/particle_ecs/effects/smoke.json +1859 -1859
  11. package/src/shade/playground/particle_ecs/effects/soot.json +1605 -1605
  12. package/src/shade/playground/particle_system/README.md +1 -2
  13. package/src/shade/playground/profile_hotkey.d.ts.map +1 -1
  14. package/src/shade/playground/profile_hotkey.js +4 -40
  15. package/src/shade/playground/skinned_blas_refit/main.d.ts.map +1 -1
  16. package/src/shade/playground/skinned_blas_refit/main.js +4 -39
  17. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
  18. package/src/shade/playground/volumetrics_froxel/main.js +4 -39
  19. package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -1
  20. package/src/shade/playground/volumetrics_froxel/measure.js +2 -4
  21. package/src/shade/renderer/Renderer.d.ts +1 -1
  22. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  23. package/src/shade/renderer/Renderer.js +3 -1
  24. package/src/shade/renderer/light/GPULightCollection.d.ts +9 -1
  25. package/src/shade/renderer/light/GPULightCollection.d.ts.map +1 -1
  26. package/src/shade/renderer/light/GPULightCollection.js +41 -0
  27. package/src/shade/renderer/light/environment/shader_project_environment_sh.d.ts +37 -0
  28. package/src/shade/renderer/light/environment/shader_project_environment_sh.d.ts.map +1 -0
  29. package/src/shade/renderer/light/environment/shader_project_environment_sh.js +105 -0
  30. package/src/shade/renderer/particles/DESIGN.md +65 -8
  31. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +7 -1
  32. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  33. package/src/shade/renderer/particles/GPUParticleSystem.js +1247 -1244
  34. package/src/shade/renderer/particles/graph_particles_avboit.d.ts +7 -1
  35. package/src/shade/renderer/particles/graph_particles_avboit.d.ts.map +1 -1
  36. package/src/shade/renderer/particles/graph_particles_avboit.js +9 -0
  37. package/src/shade/renderer/particles/shaders/chunk_particle_billboard_vertex.d.ts.map +1 -1
  38. package/src/shade/renderer/particles/shaders/chunk_particle_billboard_vertex.js +4 -2
  39. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +15 -9
  40. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts.map +1 -1
  41. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +29 -11
  42. package/src/shade/renderer/particles/shaders/chunk_particle_lighting_model.d.ts +24 -0
  43. package/src/shade/renderer/particles/shaders/chunk_particle_lighting_model.d.ts.map +1 -0
  44. package/src/shade/renderer/particles/shaders/chunk_particle_lighting_model.js +45 -0
  45. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.d.ts +3 -0
  46. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.d.ts.map +1 -1
  47. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.js +38 -0
  48. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.d.ts.map +1 -1
  49. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.js +190 -185
  50. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts +8 -0
  51. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts.map +1 -1
  52. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.js +5 -0
  53. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts +8 -1
  54. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts.map +1 -1
  55. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +9 -0
  56. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts +13 -3
  57. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  58. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +9 -1
  59. package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.d.ts +39 -0
  60. package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.d.ts.map +1 -0
  61. package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.js +107 -0
  62. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.d.ts +15 -0
  63. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.d.ts.map +1 -0
  64. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.js +44 -0
  65. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.d.ts +19 -0
  66. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.d.ts.map +1 -0
  67. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.js +48 -0
  68. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.d.ts +29 -0
  69. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.d.ts.map +1 -0
  70. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.js +90 -0
  71. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_brick4.d.ts +10 -0
  72. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_brick4.d.ts.map +1 -0
  73. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_brick4.js +55 -0
  74. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_ibl.d.ts +10 -0
  75. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_ibl.d.ts.map +1 -0
  76. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_ibl.js +47 -0
  77. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_lpv.d.ts +10 -0
  78. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_lpv.d.ts.map +1 -0
  79. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_lpv.js +63 -0
  80. package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.d.ts +17 -0
  81. package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.d.ts.map +1 -0
  82. package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.js +26 -0
  83. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +8 -1
  84. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  85. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +50 -2
  86. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
  87. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +488 -436
  88. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts +1 -1
  89. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts.map +1 -1
  90. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +254 -63
@@ -134,6 +134,5 @@ and `camera_state()` / `apply_camera(...)` copy a view between sessions.
134
134
 
135
135
  ## What this page does not cover
136
136
 
137
- Lighting is the unlit hook — `chunk_particle_lighting_unlit`, not the shading-chunk-backed one a
138
- lit-emitter variant would swap in. No emitter opts into simulation culling, so the frustum test in
137
+ No emitter here opts into `EMITTER_FLAG.LIGHTING`; the bonfire in `../particle_ecs/` does. No emitter opts into simulation culling, so the frustum test in
139
138
  the tick pass is never taken here; the measured boxes are read by the occupancy pass all the same.
@@ -1 +1 @@
1
- {"version":3,"file":"profile_hotkey.d.ts","sourceRoot":"","sources":["../../../../src/shade/playground/profile_hotkey.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AAEH;;;;;GAKG;AAEH;;;;;;;;;;;;;;;GAeG;AACH;IAZ8D,QAAQ,EAA3D,OAAO,yBAAyB,EAAE,QAAQ;IAE3B,IAAI,EAAnB,MAAM;IACoB,MAAM,GAAhC,WAAW,GAAC,IAAI;IAEc,QAAQ,SAA1B,MAAM,EAAE;IAEJ,KAAK,GAArB,MAAM;IACU,GAAG,GAAnB,MAAM;IACe,MAAM,GAA3B,WAAW;IACT,mBAAmB,CAwO/B;;wBA3PyB,OAAO;;;;kBACP,QAAQ,IAAI,CAAC;;;;mBACb,IAAI"}
1
+ {"version":3,"file":"profile_hotkey.d.ts","sourceRoot":"","sources":["../../../../src/shade/playground/profile_hotkey.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AAEH;;;;;GAKG;AAEH;;;;;;;;;;;;;;;GAeG;AACH;IAZ8D,QAAQ,EAA3D,OAAO,yBAAyB,EAAE,QAAQ;IAE3B,IAAI,EAAnB,MAAM;IACoB,MAAM,GAAhC,WAAW,GAAC,IAAI;IAEc,QAAQ,SAA1B,MAAM,EAAE;IAEJ,KAAK,GAArB,MAAM;IACU,GAAG,GAAnB,MAAM;IACe,MAAM,GAA3B,WAAW;IACT,mBAAmB,CAoM/B;;wBAvNyB,OAAO;;;;kBACP,QAAQ,IAAI,CAAC;;;;mBACb,IAAI"}
@@ -139,56 +139,20 @@ export function install_profile_hotkey({
139
139
  );
140
140
  }
141
141
 
142
- /**
143
- * Wait for frames already submitted to finish landing in the session.
144
- *
145
- * A frame is committed when its timestamp readback resolves — a `mapAsync` a frame or two behind
146
- * submit, see `ShadeGPUCommandContext.profiling_absorbed` — and `record_frame` silently ignores
147
- * anything arriving after `stop()`. Stopping the instant the key goes down therefore discards
148
- * the last few frames, which are the ones you were most likely looking at.
149
- *
150
- * Drains the queue, then waits for the frame count to stop moving. Capped, because a recording
151
- * taken while the page is paused never moves at all.
152
- *
153
- * @param {GPUProfileSession} recording
154
- * @returns {Promise<void>}
155
- */
156
- async function drain(recording) {
157
- const DEADLINE_MS = 1000;
158
- const QUIET_MS = 60;
159
-
160
- await renderer.device.queue.onSubmittedWorkDone();
161
-
162
- const deadline = performance.now() + DEADLINE_MS;
163
-
164
- let previous = recording.frames_recorded;
165
-
166
- while (performance.now() < deadline) {
167
- await new Promise(resolve => setTimeout(resolve, QUIET_MS));
168
-
169
- if (recording.frames_recorded === previous) {
170
- return;
171
- }
172
-
173
- previous = recording.frames_recorded;
174
- }
175
- }
176
-
177
142
  async function stop() {
178
143
  const recording = session;
179
144
 
180
145
  session = null;
181
146
 
182
- // Detached first, so nothing encoded during the drain below joins the recording. Frames
183
- // already encoded are unaffected: Renderer captures the session into a local before it
147
+ // Detached first, so nothing encoded while the frames in flight land joins the recording.
148
+ // Those frames are unaffected: Renderer captures the session into a local before it
184
149
  // subscribes to the readback.
185
150
  renderer.profile_session = null;
186
151
 
187
152
  set_status("stopping — draining in-flight frames…");
188
153
 
189
- await drain(recording);
190
-
191
- const bytes = recording.stop();
154
+ // resolves once the frames already submitted have landed, the last ones you were looking at
155
+ const bytes = await recording.stop();
192
156
  const filename = `${name}-${file_timestamp()}.sgpt`;
193
157
 
194
158
  download_bytes(bytes, filename);
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/skinned_blas_refit/main.js"],"names":[],"mappings":"AAwQA;;;;;GAKG;AACH,yBAFa,QAAQ,IAAI,CAAC,CAukBzB"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/skinned_blas_refit/main.js"],"names":[],"mappings":"AAwQA;;;;;GAKG;AACH,yBAFa,QAAQ,IAAI,CAAC,CAoiBzB"}
@@ -702,55 +702,20 @@ export async function start() {
702
702
  );
703
703
  }
704
704
 
705
- /**
706
- * Wait for frames already submitted to finish landing in the session.
707
- *
708
- * A frame is committed when its timestamp readback resolves — a `mapAsync` a frame or two behind
709
- * submit, see `ShadeGPUCommandContext.profiling_absorbed` — and `record_frame` silently ignores
710
- * anything arriving after `stop()`. Stopping the instant the key goes down therefore discards
711
- * the last few frames, which are the ones you were most likely looking at.
712
- *
713
- * Drains the queue, then waits for the frame count to stop moving. Capped, because a recording
714
- * taken while the page is paused never moves at all.
715
- *
716
- * @param {GPUProfileSession} session
717
- */
718
- async function drain_profile(session) {
719
- const DEADLINE_MS = 1000;
720
- const QUIET_MS = 60;
721
-
722
- await renderer.device.queue.onSubmittedWorkDone();
723
-
724
- const deadline = performance.now() + DEADLINE_MS;
725
-
726
- let previous = session.frames_recorded;
727
-
728
- while (performance.now() < deadline) {
729
- await new Promise(resolve => setTimeout(resolve, QUIET_MS));
730
-
731
- if (session.frames_recorded === previous) {
732
- return;
733
- }
734
-
735
- previous = session.frames_recorded;
736
- }
737
- }
738
-
739
705
  async function stop_profile() {
740
706
  const session = profile_session;
741
707
 
742
708
  profile_session = null;
743
709
 
744
- // Detach first, so nothing encoded during the drain below joins the recording. Frames
745
- // already encoded are unaffected: Renderer captures the session into a local before it
710
+ // Detached first, so nothing encoded while the frames in flight land joins the recording.
711
+ // Those frames are unaffected: Renderer captures the session into a local before it
746
712
  // subscribes to the readback.
747
713
  renderer.profile_session = null;
748
714
 
749
715
  set_profile_status('stopping — draining in-flight frames…');
750
716
 
751
- await drain_profile(session);
752
-
753
- const bytes = session.stop();
717
+ // resolves once the frames already submitted have landed, the last ones you were looking at
718
+ const bytes = await session.stop();
754
719
 
755
720
  const filename = `skinned_blas_refit-${file_timestamp()}.sgpt`;
756
721
 
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/volumetrics_froxel/main.js"],"names":[],"mappings":"AA2aA,uCAigBC"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/volumetrics_froxel/main.js"],"names":[],"mappings":"AA2aA,uCA8dC"}
@@ -766,55 +766,20 @@ export async function start() {
766
766
  );
767
767
  }
768
768
 
769
- /**
770
- * Wait for frames already submitted to finish landing in the session.
771
- *
772
- * A frame is committed when its timestamp readback resolves — a `mapAsync` a frame or two behind
773
- * submit, see `ShadeGPUCommandContext.profiling_absorbed` — and `record_frame` silently ignores
774
- * anything arriving after `stop()`. Stopping the instant the key goes down therefore discards
775
- * the last few frames, which are the ones you were most likely looking at.
776
- *
777
- * Drains the queue, then waits for the frame count to stop moving. Capped, because a recording
778
- * taken while the page is paused never moves at all.
779
- *
780
- * @param {GPUProfileSession} session
781
- */
782
- async function drain_profile(session) {
783
- const DEADLINE_MS = 1000;
784
- const QUIET_MS = 60;
785
-
786
- await renderer.device.queue.onSubmittedWorkDone();
787
-
788
- const deadline = performance.now() + DEADLINE_MS;
789
-
790
- let previous = session.frames_recorded;
791
-
792
- while (performance.now() < deadline) {
793
- await new Promise(resolve => setTimeout(resolve, QUIET_MS));
794
-
795
- if (session.frames_recorded === previous) {
796
- return;
797
- }
798
-
799
- previous = session.frames_recorded;
800
- }
801
- }
802
-
803
769
  async function stop_profile() {
804
770
  const session = profile_session;
805
771
 
806
772
  profile_session = null;
807
773
 
808
- // Detach first, so nothing encoded during the drain below joins the recording. Frames
809
- // already encoded are unaffected: Renderer captures the session into a local before it
774
+ // Detached first, so nothing encoded while the frames in flight land joins the recording.
775
+ // Those frames are unaffected: Renderer captures the session into a local before it
810
776
  // subscribes to the readback.
811
777
  renderer.profile_session = null;
812
778
 
813
779
  set_profile_status('stopping — draining in-flight frames…');
814
780
 
815
- await drain_profile(session);
816
-
817
- const bytes = session.stop();
781
+ // resolves once the frames already submitted have landed, the last ones you were looking at
782
+ const bytes = await session.stop();
818
783
 
819
784
  const filename = `volumetrics_froxel-${file_timestamp()}.sgpt`;
820
785
 
@@ -1 +1 @@
1
- {"version":3,"file":"measure.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/volumetrics_froxel/measure.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH;;;;;;GAMG;AACH;IAL8B,QAAQ;IACX,KAAK;IACO,MAAM;IACA,UAAU;;;;;;;;;;UAuLtC,QAAQ,YAAY,CAAC;;;;;gCA5DvB,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA4MV,QAAQ,MAAM,EAAE,CAAC;;;;;;;;;;;;;QAsGF,MAAM,GAAvB,MAAM;QACW,MAAM,GAAvB,MAAM;UACJ;YAAe,MAAM,GAAE,MAAM;MAAE;;GAsF/C"}
1
+ {"version":3,"file":"measure.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/volumetrics_froxel/measure.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH;;;;;;GAMG;AACH;IAL8B,QAAQ;IACX,KAAK;IACO,MAAM;IACA,UAAU;;;;;;;;;;UAuLtC,QAAQ,YAAY,CAAC;;;;;gCA5DvB,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA4MV,QAAQ,MAAM,EAAE,CAAC;;;;;;;;;;;;;QAsGF,MAAM,GAAvB,MAAM;QACW,MAAM,GAAvB,MAAM;UACJ;YAAe,MAAM,GAAE,MAAM;MAAE;;GAoF/C"}
@@ -495,10 +495,8 @@ export async function attach({
495
495
 
496
496
  renderer.profile_session = null;
497
497
 
498
- await device.queue.onSubmittedWorkDone();
499
- await new Promise(resolve => setTimeout(resolve, 200));
500
-
501
- const bytes = session.stop();
498
+ // resolves once the frames already submitted have landed
499
+ const bytes = await session.stop();
502
500
 
503
501
  const buffer = bytes instanceof ArrayBuffer
504
502
  ? bytes
@@ -277,7 +277,7 @@ export class Renderer {
277
277
  * renderer.profile_session = session;
278
278
  * session.start();
279
279
  * // ... frames ...
280
- * const bytes = session.stop();
280
+ * const bytes = await session.stop();
281
281
  * renderer.profile_session = null;
282
282
  * ```
283
283
  *
@@ -1 +1 @@
1
- {"version":3,"file":"Renderer.d.ts","sourceRoot":"","sources":["../../../../src/shade/renderer/Renderer.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Renderer.d.ts","sourceRoot":"","sources":["../../../../src/shade/renderer/Renderer.js"],"names":[],"mappings":"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"}
@@ -668,7 +668,7 @@ export class Renderer {
668
668
  * renderer.profile_session = session;
669
669
  * session.start();
670
670
  * // ... frames ...
671
- * const bytes = session.stop();
671
+ * const bytes = await session.stop();
672
672
  * renderer.profile_session = null;
673
673
  * ```
674
674
  *
@@ -2433,6 +2433,7 @@ export class Renderer {
2433
2433
  light_cluster_lookup: light_clusters.lookup,
2434
2434
  light_cluster_parameters: light_clusters.parameters,
2435
2435
  shadowmap_atlas: gr_shadowmap_atlas,
2436
+ indirect_lighting_mode: this.indirect_lighting_mode,
2436
2437
  });
2437
2438
 
2438
2439
  /**
@@ -2687,6 +2688,7 @@ export class Renderer {
2687
2688
 
2688
2689
  volumetrics_lut: volumetrics.resolved,
2689
2690
  volumetrics_metadata: volumetrics.metadata,
2691
+ volumetrics_irradiance: volumetrics.irradiance,
2690
2692
 
2691
2693
  indirect_lighting_mode: this.indirect_lighting_mode,
2692
2694
  avboit: this.avboit,
@@ -42,7 +42,15 @@ export class GPULightCollection {
42
42
  * @returns {GPUTextureContext}
43
43
  */
44
44
  get environment(): GPUTextureContext;
45
- get gpu_memory_usage(): number;
45
+ /**
46
+ * The environment's radiance as RGB SH2 (`ENVIRONMENT_SH_TYPE`), the form the renderer's probe-based
47
+ * indirect lighting hands over too: what the froxel GI stage starts the froxel irradiance volume
48
+ * with under IBL. Zero while there is no environment, as {@link environment} is black then.
49
+ *
50
+ * @returns {GPUBuffer}
51
+ */
52
+ get environment_sh(): GPUBuffer;
53
+ get gpu_memory_usage(): any;
46
54
  /**
47
55
  *
48
56
  * @param {GraphicsContext} ctx
@@ -1 +1 @@
1
- {"version":3,"file":"GPULightCollection.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/light/GPULightCollection.js"],"names":[],"mappings":"AA0CA;IAgII;;;;;;OAMG;IACH,sBALW,eAAe,aACf,eAAe,EA8BzB;IA1JD;;;OAGG;IACH,6DAEC;IAeD;;OAEG;IACH,4BAEC;IAED;;;;;;OAMG;IACH,6BAEC;IAED;;;;;;;OAOG;IACH,qFAEC;IAWD;;OAEG;IACH,+CAEC;IAyCD;;;OAGG;IACH,qCAEC;IAGD,+BAIC;IA4GD;;;OAGG;IACH,YAFW,eAAe,QA8BzB;IAED;;;;;;;;;OASG;IACH,gBAFW,OAAO,+BAA+B,EAAE,YAAY,QAkC9D;IAED;;;;;;OAMG;IACH,uBAHW,sBAAsB,mCAyBhC;IAwED,gBAQC;;CACJ;8BA3bY,OAAO,uBAAuB,EAAE,eAAe;8BAC/C,OAAO,6BAA6B,EAAE,eAAe;8BACrD,OAAO,sBAAsB,EAAE,eAAe;4BApB/B,gCAAgC;yCACnB,2CAA2C;kCAClD,iCAAiC;uCAH5B,wCAAwC"}
1
+ {"version":3,"file":"GPULightCollection.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/light/GPULightCollection.js"],"names":[],"mappings":"AA2CA;IAmJI;;;;;;OAMG;IACH,sBALW,eAAe,aACf,eAAe,EAqCzB;IApLD;;;OAGG;IACH,6DAEC;IAeD;;OAEG;IACH,4BAEC;IAED;;;;;;OAMG;IACH,6BAEC;IAED;;;;;;;OAOG;IACH,qFAEC;IAWD;;OAEG;IACH,+CAEC;IAgDD;;;OAGG;IACH,qCAEC;IAED;;;;;;OAMG;IACH,gCAEC;IAGD,4BAKC;IAgID;;;OAGG;IACH,YAFW,eAAe,QA8BzB;IAED;;;;;;;;;OASG;IACH,gBAFW,OAAO,+BAA+B,EAAE,YAAY,QAkC9D;IAED;;;;;;OAMG;IACH,uBAHW,sBAAsB,mCAyBhC;IAwED,gBASC;;CACJ;8BAneY,OAAO,uBAAuB,EAAE,eAAe;8BAC/C,OAAO,6BAA6B,EAAE,eAAe;8BACrD,OAAO,sBAAsB,EAAE,eAAe;4BArB/B,gCAAgC;yCACnB,2CAA2C;kCAClD,iCAAiC;uCAH5B,wCAAwC"}
@@ -7,6 +7,7 @@ import { GPUTextureContext } from "../texture/GPUTextureContext.js";
7
7
  import { texture_mip_level_count } from "../../util/texture_mip_extent.js";
8
8
  import { texture_write_to_gpu } from "../texture/texture_write_to_gpu.js";
9
9
  import { execute_filter_environment_map } from "./environment/execute_filter_environment_map.js";
10
+ import { ENVIRONMENT_SH_TYPE, shader_project_environment_sh } from "./environment/shader_project_environment_sh.js";
10
11
  import { LIGHT_DATABASE_SPEC } from "./LIGHT_DATABASE_SPEC.js";
11
12
  import { light_record_directional } from "./model/io/light_record_directional.js";
12
13
  import { light_record_point } from "./model/io/light_record_point.js";
@@ -152,6 +153,13 @@ export class GPULightCollection {
152
153
  */
153
154
  #environment_source;
154
155
 
156
+ /**
157
+ * The environment's radiance as RGB SH2, projected from {@link #environment} whenever it changes;
158
+ * see {@link shader_project_environment_sh}.
159
+ * @type {GPUBuffer}
160
+ */
161
+ #environment_sh;
162
+
155
163
 
156
164
  /**
157
165
  *
@@ -161,9 +169,21 @@ export class GPULightCollection {
161
169
  return this.#environment;
162
170
  }
163
171
 
172
+ /**
173
+ * The environment's radiance as RGB SH2 (`ENVIRONMENT_SH_TYPE`), the form the renderer's probe-based
174
+ * indirect lighting hands over too: what the froxel GI stage starts the froxel irradiance volume
175
+ * with under IBL. Zero while there is no environment, as {@link environment} is black then.
176
+ *
177
+ * @returns {GPUBuffer}
178
+ */
179
+ get environment_sh() {
180
+ return this.#environment_sh;
181
+ }
182
+
164
183
 
165
184
  get gpu_memory_usage() {
166
185
  return this.#environment.gpu_memory_usage
186
+ + this.#environment_sh.size
167
187
  + this.#database.gpu_memory_usage
168
188
  ;
169
189
  }
@@ -194,6 +214,13 @@ export class GPULightCollection {
194
214
  | GPUTextureUsage.STORAGE_BINDING // so we can convolve it in compute shader
195
215
  ;
196
216
 
217
+ // zeroed on creation, which is what an absent environment projects to
218
+ this.#environment_sh = device.createBuffer({
219
+ label: "environment SH",
220
+ size: ENVIRONMENT_SH_TYPE.size,
221
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
222
+ });
223
+
197
224
  this.#database = new GPUDatabase({
198
225
  device,
199
226
  label: 'Light Collection / Database',
@@ -236,6 +263,9 @@ export class GPULightCollection {
236
263
 
237
264
  gpu_ctx.allocate();
238
265
 
266
+ // what a black environment projects to; nothing to run the projection over
267
+ this.#device.queue.writeBuffer(this.#environment_sh, 0, new Float32Array(this.#environment_sh.size / 4));
268
+
239
269
  return;
240
270
  }
241
271
 
@@ -271,6 +301,16 @@ export class GPULightCollection {
271
301
 
272
302
  execute_filter_environment_map(gpu_ctx, cmd);
273
303
 
304
+ // after the prefilter, whose roughest levels are what it reads
305
+ shader_project_environment_sh.dispatch({
306
+ encoder: cmd,
307
+ group_count_x: 1,
308
+ bindings: {
309
+ tEnvironment: gpu_ctx.obtainView(),
310
+ output: this.#environment_sh,
311
+ },
312
+ });
313
+
274
314
  cmd.finish();
275
315
  }
276
316
 
@@ -461,5 +501,6 @@ export class GPULightCollection {
461
501
  this.#shadow_context.destroy();
462
502
  this.#database.destroy();
463
503
  this.#environment.destroy();
504
+ this.#environment_sh.destroy();
464
505
  }
465
506
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The layout {@link shader_project_environment_sh} writes: the environment's radiance as RGB SH2,
3
+ * coefficient `k` in `.xyz` of element `k`, ordered `[DC, y, z, x]` in the project's all-positive
4
+ * basis (`sh2_basis_at`); `.w` is unused. Radiance, not cosine-convolved — the convention the probes
5
+ * of Brick4 and of the light probe volume are stored in — so `sh2_color_get_irradiance_at` turns it
6
+ * into the irradiance of a surface facing the direction evaluated at.
7
+ *
8
+ * @type {WebGPUArray}
9
+ */
10
+ export const ENVIRONMENT_SH_TYPE: WebGPUArray;
11
+ /**
12
+ * Directions the projection samples, spread evenly over the sphere (`sphere_fibonacci_point`).
13
+ * The field is the environment at the roughness diffuse reads it at, so this is far more than its
14
+ * two bands can hold, and it costs nothing that recurs: the pass runs when the environment changes.
15
+ *
16
+ * @type {number}
17
+ */
18
+ export const ENVIRONMENT_SH_SAMPLE_COUNT: number;
19
+ /**
20
+ * The environment as RGB SH2 of radiance ({@link ENVIRONMENT_SH_TYPE}): the form the renderer's other
21
+ * indirect lighting — Brick4, the light probe volume — already hands over, so the froxel GI stage
22
+ * (`volumetrics/gi/`) takes the environment as one more source of it. What the froxel irradiance
23
+ * volume is started with under IBL, and what a lit particle's ambient is there; reading the
24
+ * environment itself per pixel, as the forward mesh path does, cost 880 us of lighting's 1,090 us on
25
+ * the bonfire's overdraw at 1898x926 (RTX 4090).
26
+ *
27
+ * The field projected is the one the forward path reads (`get_ibl_irradiance`: the prefiltered map at
28
+ * roughness 1, times pi), and the radiance stored is what that field is the cosine convolution of, so
29
+ * convolving it back gives the forward path's diffuse up to what two bands drop. Run by
30
+ * `GPULightCollection` when the environment changes, after the prefilter it reads.
31
+ *
32
+ * @type {ComputeShader}
33
+ */
34
+ export const shader_project_environment_sh: ComputeShader;
35
+ import { WebGPUArray } from "../../shader/type/WebGPUArray.js";
36
+ import { ComputeShader } from "../../shader/ComputeShader.js";
37
+ //# sourceMappingURL=shader_project_environment_sh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shader_project_environment_sh.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/light/environment/shader_project_environment_sh.js"],"names":[],"mappings":"AAUA;;;;;;;;GAQG;AACH,kCAFU,WAAW,CAEyC;AAE9D;;;;;;GAMG;AACH,0CAFU,MAAM,CAEgC;AAyDhD;;;;;;;;;;;;;;GAcG;AACH,4CAFU,aAAa,CAMpB;4BAjGyB,kCAAkC;8BAFhC,+BAA+B"}
@@ -0,0 +1,105 @@
1
+ import { chunk_get_ibl_irradiance } from "../../deferred/indirect/chunk_get_ibl_irradiance.js";
2
+ import { PI_DECLARATION } from "../../shader/chunk/math/PI_DECLARATION.js";
3
+ import { chunk_sphere_fibonacci_point } from "../../shader/chunk/geometry/sphere/chunk_sphere_fibonacci_point.js";
4
+ import { chunk_sh2_basis_at } from "../../shader/chunk/spherical_harmonics/chunk_sh2_basis_at.js";
5
+ import { CodeChunk } from "../../shader/compiler/CodeChunk.js";
6
+ import { ComputeShader } from "../../shader/ComputeShader.js";
7
+ import { ShaderResourceSetDescriptor } from "../../shader/resource/ShaderResourceSetDescriptor.js";
8
+ import { WebGPUArray } from "../../shader/type/WebGPUArray.js";
9
+ import { vec4f } from "../../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
10
+
11
+ /**
12
+ * The layout {@link shader_project_environment_sh} writes: the environment's radiance as RGB SH2,
13
+ * coefficient `k` in `.xyz` of element `k`, ordered `[DC, y, z, x]` in the project's all-positive
14
+ * basis (`sh2_basis_at`); `.w` is unused. Radiance, not cosine-convolved — the convention the probes
15
+ * of Brick4 and of the light probe volume are stored in — so `sh2_color_get_irradiance_at` turns it
16
+ * into the irradiance of a surface facing the direction evaluated at.
17
+ *
18
+ * @type {WebGPUArray}
19
+ */
20
+ export const ENVIRONMENT_SH_TYPE = WebGPUArray.from(vec4f, 4);
21
+
22
+ /**
23
+ * Directions the projection samples, spread evenly over the sphere (`sphere_fibonacci_point`).
24
+ * The field is the environment at the roughness diffuse reads it at, so this is far more than its
25
+ * two bands can hold, and it costs nothing that recurs: the pass runs when the environment changes.
26
+ *
27
+ * @type {number}
28
+ */
29
+ export const ENVIRONMENT_SH_SAMPLE_COUNT = 1024;
30
+
31
+ const resources = new ShaderResourceSetDescriptor();
32
+
33
+ resources.createGroup()
34
+ // read texel by texel (texture_octahedral_sample_bilinear), as the forward path reads it
35
+ .addTexture("tEnvironment")
36
+ .addStorageBuffer("output", ENVIRONMENT_SH_TYPE, true);
37
+
38
+ const body = CodeChunk.from(
39
+ //language=WGSL
40
+ `
41
+ const SAMPLE_COUNT : u32 = ${ENVIRONMENT_SH_SAMPLE_COUNT}u;
42
+
43
+ // One invocation: the projection is 1024 reads that happen when the environment changes and never
44
+ // otherwise, and a single lane needs no reduction and runs whole on the emulator.
45
+ @compute @workgroup_size(1)
46
+ fn main() {
47
+ var sh_0 = vec3<f32>(0.0);
48
+ var sh_1 = vec3<f32>(0.0);
49
+ var sh_2 = vec3<f32>(0.0);
50
+ var sh_3 = vec3<f32>(0.0);
51
+
52
+ for (var i = 0u; i < SAMPLE_COUNT; i++) {
53
+ let direction = sphere_fibonacci_point(f32(i), f32(SAMPLE_COUNT));
54
+
55
+ // exactly what the forward mesh path takes as a surface's diffuse irradiance under IBL
56
+ let irradiance = get_ibl_irradiance(direction);
57
+
58
+ let basis = sh2_basis_at(direction);
59
+
60
+ sh_0 += irradiance * basis[0];
61
+ sh_1 += irradiance * basis[1];
62
+ sh_2 += irradiance * basis[2];
63
+ sh_3 += irradiance * basis[3];
64
+ }
65
+
66
+ // each sample stands for an equal share of the sphere
67
+ let weight = 4.0 * PI / f32(SAMPLE_COUNT);
68
+
69
+ // What was projected is irradiance; what is stored is the radiance it is the cosine convolution
70
+ // of, band by band: the clamped cosine's zonal weights are pi for band 0 and 2 pi / 3 for band 1.
71
+ let band_0 = weight / PI;
72
+ let band_1 = weight * 3.0 / (2.0 * PI);
73
+
74
+ output[0] = vec4<f32>(sh_0 * band_0, 0.0);
75
+ output[1] = vec4<f32>(sh_1 * band_1, 0.0);
76
+ output[2] = vec4<f32>(sh_2 * band_1, 0.0);
77
+ output[3] = vec4<f32>(sh_3 * band_1, 0.0);
78
+ }
79
+ `, [
80
+ chunk_sphere_fibonacci_point,
81
+ chunk_get_ibl_irradiance,
82
+ chunk_sh2_basis_at,
83
+ PI_DECLARATION,
84
+ ]);
85
+
86
+ /**
87
+ * The environment as RGB SH2 of radiance ({@link ENVIRONMENT_SH_TYPE}): the form the renderer's other
88
+ * indirect lighting — Brick4, the light probe volume — already hands over, so the froxel GI stage
89
+ * (`volumetrics/gi/`) takes the environment as one more source of it. What the froxel irradiance
90
+ * volume is started with under IBL, and what a lit particle's ambient is there; reading the
91
+ * environment itself per pixel, as the forward mesh path does, cost 880 us of lighting's 1,090 us on
92
+ * the bonfire's overdraw at 1898x926 (RTX 4090).
93
+ *
94
+ * The field projected is the one the forward path reads (`get_ibl_irradiance`: the prefiltered map at
95
+ * roughness 1, times pi), and the radiance stored is what that field is the cosine convolution of, so
96
+ * convolving it back gives the forward path's diffuse up to what two bands drop. Run by
97
+ * `GPULightCollection` when the environment changes, after the prefilter it reads.
98
+ *
99
+ * @type {ComputeShader}
100
+ */
101
+ export const shader_project_environment_sh = ComputeShader.from({
102
+ label: "Environment / project SH",
103
+ resources,
104
+ body,
105
+ });
@@ -21,7 +21,8 @@ rows, and the decision to spawn is the GPU's.
21
21
  - Scene / bone attachment: an emitter stands at an entity with a row; its world matrix is that row
22
22
  of the scene's `transforms` table, composed by the GPU hierarchy pass.
23
23
  - Distinct per-particle lifecycle: **no** built-in age/position — the schema + program define state.
24
- - Billboard drawing reuses engine shader chunks incl. lighting (forward-shaded, optional per emitter).
24
+ - Lighting per emitter (`EMITTER_FLAG.LIGHTING`): the scene's lights from the froxel irradiance
25
+ volume the fog pass writes, plus the environment map. See "Lighting" below.
25
26
  - Blend modes (alpha + additive) via a **single** premultiplied blend setup.
26
27
  - Authored curves and RGB/RGBA gradients compiled into arithmetic for simulation.
27
28
  - Spawning decided on the GPU, per emitter, with the CPU able to ask for bursts through the same path.
@@ -516,10 +517,9 @@ draw's.
516
517
  ### Rendering
517
518
 
518
519
  A 6-vertex quad is expanded per instance (billboard/velocity-stretch/axis-lock/world modes) by
519
- `chunk_particle_billboard_vertex`. Fragment: sample atlas (region + flipbook frame), optional forward
520
- lighting via `chunk_shade_standard_material_direct` + IBL, optional soft-depth fade. Soft-depth
521
- fading scales both RGB and alpha for premultiplied inputs; straight-alpha and additive inputs have
522
- alpha scaled before premultiplication.
520
+ `chunk_particle_billboard_vertex`. Fragment: sample atlas (region + flipbook frame), optional
521
+ soft-depth fade, optional lighting (below). Soft-depth fading scales both RGB and alpha for
522
+ premultiplied inputs; straight-alpha and additive inputs have alpha scaled before premultiplication.
523
523
 
524
524
  Additive vs. alpha is never a pipeline switch: the shader emits premultiplied RGB with alpha 0
525
525
  (additive) or alpha a (normal), so one setup covers both and the choice stays per particle and
@@ -778,6 +778,63 @@ was seeded.
778
778
 
779
779
  Done: `Renderer.feature_particles_enabled` (off by default), `Renderer.particles(scene_ctx)` creating
780
780
  one system per scene context on first use, `simulate` recorded before the transparency pass and the
781
- system passed to the AVBOIT orchestrator as its side channel. Remaining:
782
- 1. For lit emitters, compile a render variant that swaps the disabled lighting hook for a
783
- shading-chunk-backed lighting hook (+ the `SHADING_LIGHT_RESOURCE_GROUP`).
781
+ system passed to the AVBOIT orchestrator as its side channel.
782
+
783
+ ### Lighting
784
+
785
+ An emitter flagged `EMITTER_FLAG.LIGHTING` treats its colour as a diffuse albedo; an emitter without
786
+ it is its own colour, which is what additive (emissive) emitters want and stay. The flag is read per
787
+ fragment from the emitter record, so lit and unlit particles share one pipeline and one draw. All of
788
+ it is evaluated per pixel (`chunk_particle_lighting`, the model in `chunk_particle_lighting_model`):
789
+ a vertex need not be on screen, and one quad can span the frustum.
790
+
791
+ - **Direct light comes from the froxel irradiance volume** (`volumetrics/chunk_volumetrics_irradiance_sh`),
792
+ which `shader_volumetrics_build_lighting` writes in the loop that lights the fog: every
793
+ directional, point and spot light at the froxel, shadowed, attenuated by the fog toward it, stored
794
+ as luma SH2 + constant chroma (Metro's L1 YCoCg, 6 coefficients in two `rgba16float` volumes, since
795
+ `rg16float` is not a storage format without `texture-formats-tier1`). The volume rides the
796
+ orchestrator to side channels as `volumetrics_irradiance`. Resolution is the fog's — 16 px tiles
797
+ and 64 slices — so light and shadow edges inside a particle are soft. idTech 8 builds the same
798
+ volume in the same pass (Sousa, SIGGRAPH 2025, p.29); Ghost of Tsushima lights particles from its
799
+ haze grid the same way. Chosen over per-pixel forward shading (UE's most expensive mode, and a
800
+ flat card lit that way goes black from behind), per-vertex/per-particle lighting (the light
801
+ clusters are screen-space: a point off screen has no light list, the artefact that moved idTech 8
802
+ off it) and a per-particle lighting atlas (DOOM 2016, CoD IW: best-looking, but it needs a
803
+ world-space light lookup this engine does not have).
804
+ - **The SH is evaluated at the normal and the irradiance clamped at zero.** One light gives
805
+ `E max(0, 1/4 + cos/2)`. SH2 cannot keep two lights apart: a light's lobe is `-E/4` facing away
806
+ from it, so a light behind a particle subtracts from the ones in front before the clamp sees a
807
+ total. Measured on the bonfire before transmission, the moon behind the smoke brought the fire-lit
808
+ side down to about half (display red 10 against 21).
809
+ - **Chroma has no direction**: two lights of different colours from opposite sides tint both sides
810
+ their mix. Measured on the bonfire, the blue moon moves the fire-lit underside of the smoke from
811
+ (22.9, 5.3, 1.5) to (15.9, 4.7, 2.6) in display values; a same-coloured moon only brightens it.
812
+ - **Indirect light is in the same volume**, whatever the renderer's `indirect_lighting_mode`. The
813
+ froxel GI stage (`volumetrics/gi/`) starts the volume with one concrete module per mode — IBL,
814
+ Brick4, LPV — each asking its source what radiance arrives at the froxel's centre, as RGB SH2,
815
+ and `shader_volumetrics_build_lighting` adds the lights to that. The particle knows no mode: a new
816
+ GI system is one more module. IBL's source is `GPULightCollection.environment_sh`, the
817
+ environment projected to radiance SH2 when it changes (`shader_project_environment_sh`); Brick4
818
+ interpolates its probes (SH3, truncated) with a zero normal, which weighs every probe of the
819
+ brick alike; LPV interpolates the tetrahedron the froxel falls in, and gives nothing outside the
820
+ mesh, as the deferred LPV pass does. Measured at 1898x926 (RTX 4090): the IBL module 5-6 us, the
821
+ Brick4 module 31 us over an empty map. Reading the environment per pixel instead, as the forward
822
+ mesh path does, had been 880 us of lighting's 1,090 us — the particle draw is all overdraw.
823
+ - **Back-face transmission** (Persson, "Practical Particle Lighting", GDC 2012, pp.34-37): the same
824
+ light evaluated at `-normal`, passed on in proportion to `1 - alpha`. The dense core of a puff
825
+ holds light from behind back and its thin edges let it through — smoke backlit by the moon has an
826
+ edge instead of being black. It costs nothing measurable: the draw is bound by its texture reads.
827
+ - **The normal** is the quad's own plane per projection mode, turned to the side the eye is on and
828
+ bent out toward the corners (`particle_billboard_normal`), so a sprite shades as a puff.
829
+ - **The directional light skips the atmosphere's transmittance** the fog applies: meshes are lit by
830
+ `sun.color` as it stands, and a lit particle has to agree with the surface beside it.
831
+
832
+ Cost, bonfire (~1,300 live, smoke and soot lit), RTX 4090, lit minus unlit p10 in one page: the
833
+ draw +90..275 us at 1898x926 (of ~450) across runs, +0.8..1.2 ms at 3818x2006 (of ~2.1 ms). The fog pass does
834
+ not get slower for the two extra stores — paired against the tree before it, it measured 77 vs 89 us
835
+ and 280 vs 320 us, reason not established. The volume has no temporal filter of its own: with the
836
+ simulation frozen, lit and unlit smoke change equally from frame to frame even with the screen TAA
837
+ off (0.09 vs 0.09 mean display units). That scene has no shadow edge through the smoke, which is
838
+ where a one-sample-per-froxel volume would show it first.
839
+
840
+ Open: a particle does not shade the fog or other particles (the volume holds no particle density).