@woosh/meep-engine 3.58.0 → 3.59.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 (609) hide show
  1. package/editor/particles/effect/ParticleEffectDocument.d.ts +162 -43
  2. package/editor/particles/effect/ParticleEffectDocument.d.ts.map +1 -1
  3. package/editor/particles/effect/ParticleEffectDocument.js +450 -43
  4. package/editor/particles/effect/ParticleReferenceSimulation.d.ts +12 -3
  5. package/editor/particles/effect/ParticleReferenceSimulation.d.ts.map +1 -1
  6. package/editor/particles/effect/ParticleReferenceSimulation.js +78 -23
  7. package/editor/particles/effect/make_starter_particle_effect.js +17 -17
  8. package/editor/view/particles/effect/ParticleEmitterInspectorView.d.ts +3 -3
  9. package/editor/view/particles/effect/ParticleEmitterInspectorView.d.ts.map +1 -1
  10. package/editor/view/particles/effect/ParticleEmitterInspectorView.js +224 -67
  11. package/editor/view/particles/effect/ParticleGraphEditorView.d.ts.map +1 -1
  12. package/editor/view/particles/effect/ParticleGraphEditorView.js +44 -11
  13. package/editor/view/particles/effect/ParticleNodeParametersView.d.ts +2 -1
  14. package/editor/view/particles/effect/ParticleNodeParametersView.d.ts.map +1 -1
  15. package/editor/view/particles/effect/ParticleNodeParametersView.js +46 -16
  16. package/editor/view/particles/effect/ParticlePreviewView.js +2 -2
  17. package/editor/view/particles/effect/particle-editor.css +77 -68
  18. package/package.json +1 -1
  19. package/src/engine/Engine.d.ts.map +1 -1
  20. package/src/engine/Engine.js +2 -2
  21. package/src/engine/ecs/EntityManager.d.ts +29 -5
  22. package/src/engine/ecs/EntityManager.d.ts.map +1 -1
  23. package/src/engine/ecs/EntityManager.js +37 -1
  24. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  25. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +4 -0
  26. package/src/engine/graphics/ecs/particles/ParticleEffect.d.ts +74 -17
  27. package/src/engine/graphics/ecs/particles/ParticleEffect.d.ts.map +1 -1
  28. package/src/engine/graphics/ecs/particles/ParticleEffect.js +156 -25
  29. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.d.ts +13 -3
  30. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.d.ts.map +1 -1
  31. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.js +59 -13
  32. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationUpgrader_1_2.d.ts +3 -3
  33. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationUpgrader_1_2.js +9 -9
  34. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationUpgrader_2_3.d.ts +15 -0
  35. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationUpgrader_2_3.d.ts.map +1 -0
  36. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationUpgrader_2_3.js +78 -0
  37. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +2 -0
  38. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
  39. package/src/engine/graphics/ecs/trail3d/Trail3D.js +2 -0
  40. package/src/engine/graphics/ecs/trail3d/seed_trail_stroke.js +1 -1
  41. package/src/engine/graphics/ecs/trail3d/seed_trail_tube.js +1 -1
  42. package/src/engine/graphics/trail/tube/TubeX.d.ts +5 -3
  43. package/src/engine/graphics/trail/tube/TubeX.d.ts.map +1 -1
  44. package/src/engine/graphics/trail/tube/TubeX.js +5 -3
  45. package/src/engine/graphics/trail/tube/tube_attributes_spec.d.ts +3 -2
  46. package/src/engine/graphics/trail/tube/tube_attributes_spec.d.ts.map +1 -1
  47. package/src/engine/graphics/trail/tube/tube_attributes_spec.js +9 -8
  48. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +65 -4
  49. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
  50. package/src/engine/graphics3/GPUParticleEmitterSystem.js +1351 -904
  51. package/src/engine/graphics3/trail/trail_tube_write_geometry.d.ts +1 -1
  52. package/src/engine/graphics3/trail/trail_tube_write_geometry.js +13 -13
  53. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  54. package/src/generation/foliage/FOLIAGE_DESIGN.md +26 -5
  55. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +35 -0
  56. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +2 -1
  57. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +552 -0
  58. package/src/generation/foliage/analysis/tree_crown_opacity.d.ts.map +1 -1
  59. package/src/generation/foliage/analysis/tree_crown_opacity.js +2 -1
  60. package/src/generation/foliage/analysis/tree_render_cost.d.ts +5 -3
  61. package/src/generation/foliage/analysis/tree_render_cost.d.ts.map +1 -1
  62. package/src/generation/foliage/analysis/tree_render_cost.js +8 -7
  63. package/src/generation/foliage/asset/TreeAsset.d.ts +17 -0
  64. package/src/generation/foliage/asset/TreeAsset.d.ts.map +1 -1
  65. package/src/generation/foliage/asset/TreeAsset.js +20 -0
  66. package/src/generation/foliage/asset/TreeSpeciesCache.d.ts +69 -0
  67. package/src/generation/foliage/asset/TreeSpeciesCache.d.ts.map +1 -0
  68. package/src/generation/foliage/asset/TreeSpeciesCache.js +147 -0
  69. package/src/generation/foliage/asset/generate_tree.d.ts +10 -0
  70. package/src/generation/foliage/asset/generate_tree.d.ts.map +1 -1
  71. package/src/generation/foliage/asset/generate_tree.js +57 -29
  72. package/src/generation/foliage/asset/place_tree.d.ts +2 -1
  73. package/src/generation/foliage/asset/place_tree.d.ts.map +1 -1
  74. package/src/generation/foliage/asset/place_tree.js +2 -1
  75. package/src/generation/foliage/asset/tree_lod_plan.d.ts +4 -2
  76. package/src/generation/foliage/asset/tree_lod_plan.d.ts.map +1 -1
  77. package/src/generation/foliage/asset/tree_lod_plan.js +12 -14
  78. package/src/generation/foliage/asset/tree_shoot_instances.d.ts +119 -0
  79. package/src/generation/foliage/asset/tree_shoot_instances.d.ts.map +1 -0
  80. package/src/generation/foliage/asset/tree_shoot_instances.js +410 -0
  81. package/src/generation/foliage/asset/tree_shoot_prototypes.d.ts +155 -0
  82. package/src/generation/foliage/asset/tree_shoot_prototypes.d.ts.map +1 -0
  83. package/src/generation/foliage/asset/tree_shoot_prototypes.js +251 -0
  84. package/src/generation/foliage/config/TreeGrowth.d.ts +10 -0
  85. package/src/generation/foliage/config/TreeGrowth.d.ts.map +1 -0
  86. package/src/generation/foliage/config/TreeGrowth.js +18 -0
  87. package/src/generation/foliage/config/TreeLODConfig.d.ts +6 -0
  88. package/src/generation/foliage/config/TreeLODConfig.d.ts.map +1 -1
  89. package/src/generation/foliage/config/TreeLODConfig.js +7 -0
  90. package/src/generation/foliage/index.d.ts +3 -0
  91. package/src/generation/foliage/index.js +3 -0
  92. package/src/generation/foliage/material/foliage_tile.d.ts +6 -6
  93. package/src/generation/foliage/material/foliage_tile.js +3 -3
  94. package/src/generation/foliage/material/tree_cluster_atlas.d.ts +44 -0
  95. package/src/generation/foliage/material/tree_cluster_atlas.d.ts.map +1 -0
  96. package/src/generation/foliage/material/tree_cluster_atlas.js +37 -0
  97. package/src/generation/foliage/mesh/foliage_collect_attachments.js +2 -2
  98. package/src/generation/foliage/mesh/foliage_scale_spread.d.ts +14 -0
  99. package/src/generation/foliage/mesh/foliage_scale_spread.d.ts.map +1 -0
  100. package/src/generation/foliage/mesh/foliage_scale_spread.js +15 -0
  101. package/src/generation/foliage/mesh/tree_cluster_representatives.d.ts +8 -7
  102. package/src/generation/foliage/mesh/tree_cluster_representatives.d.ts.map +1 -1
  103. package/src/generation/foliage/mesh/tree_cluster_representatives.js +85 -40
  104. package/src/generation/foliage/mesh/tree_clusters.d.ts +26 -5
  105. package/src/generation/foliage/mesh/tree_clusters.d.ts.map +1 -1
  106. package/src/generation/foliage/mesh/tree_clusters.js +52 -39
  107. package/src/generation/foliage/mesh/tree_mesh_build_bark.d.ts.map +1 -1
  108. package/src/generation/foliage/mesh/tree_mesh_build_bark.js +32 -54
  109. package/src/generation/foliage/presets/make_tree_species_preset.d.ts +6 -1
  110. package/src/generation/foliage/presets/make_tree_species_preset.d.ts.map +1 -1
  111. package/src/generation/foliage/presets/make_tree_species_preset.js +157 -3
  112. package/src/generation/foliage/skeleton/TreeSkeleton.d.ts +19 -0
  113. package/src/generation/foliage/skeleton/TreeSkeleton.d.ts.map +1 -1
  114. package/src/generation/foliage/skeleton/TreeSkeleton.js +41 -0
  115. package/src/generation/foliage/skeleton/tree_skeleton_compute_joint_mechanics.d.ts +5 -0
  116. package/src/generation/foliage/skeleton/tree_skeleton_compute_joint_mechanics.d.ts.map +1 -1
  117. package/src/generation/foliage/skeleton/tree_skeleton_compute_joint_mechanics.js +8 -1
  118. package/src/generation/foliage/skeleton/tree_skeleton_copy_branches.d.ts +70 -0
  119. package/src/generation/foliage/skeleton/tree_skeleton_copy_branches.d.ts.map +1 -0
  120. package/src/generation/foliage/skeleton/tree_skeleton_copy_branches.js +171 -0
  121. package/src/generation/foliage/skeleton/tree_skeleton_extend_branch.d.ts +17 -0
  122. package/src/generation/foliage/skeleton/tree_skeleton_extend_branch.d.ts.map +1 -0
  123. package/src/generation/foliage/skeleton/tree_skeleton_extend_branch.js +79 -0
  124. package/src/generation/foliage/skeleton/tree_skeleton_fit_crown_frame.d.ts +12 -0
  125. package/src/generation/foliage/skeleton/tree_skeleton_fit_crown_frame.d.ts.map +1 -0
  126. package/src/generation/foliage/skeleton/tree_skeleton_fit_crown_frame.js +56 -0
  127. package/src/generation/foliage/skeleton/tree_skeleton_floor_girth.d.ts +11 -0
  128. package/src/generation/foliage/skeleton/tree_skeleton_floor_girth.d.ts.map +1 -0
  129. package/src/generation/foliage/skeleton/tree_skeleton_floor_girth.js +38 -0
  130. package/src/generation/foliage/skeleton/tree_skeleton_grow.d.ts +4 -11
  131. package/src/generation/foliage/skeleton/tree_skeleton_grow.d.ts.map +1 -1
  132. package/src/generation/foliage/skeleton/tree_skeleton_grow.js +8 -2034
  133. package/src/generation/foliage/skeleton/tree_skeleton_grow_framework.d.ts +22 -0
  134. package/src/generation/foliage/skeleton/tree_skeleton_grow_framework.d.ts.map +1 -0
  135. package/src/generation/foliage/skeleton/tree_skeleton_grow_framework.js +2050 -0
  136. package/src/generation/foliage/skeleton/tree_skeleton_grow_shoots.d.ts +10 -0
  137. package/src/generation/foliage/skeleton/tree_skeleton_grow_shoots.d.ts.map +1 -1
  138. package/src/generation/foliage/skeleton/tree_skeleton_grow_shoots.js +27 -56
  139. package/src/shade/descriptor/pipeline/render/DepthStencilState.d.ts +4 -0
  140. package/src/shade/descriptor/pipeline/render/DepthStencilState.d.ts.map +1 -1
  141. package/src/shade/descriptor/pipeline/render/DepthStencilState.js +170 -144
  142. package/src/shade/descriptor/pipeline/render/MultisampleState.d.ts +4 -0
  143. package/src/shade/descriptor/pipeline/render/MultisampleState.d.ts.map +1 -1
  144. package/src/shade/descriptor/pipeline/render/MultisampleState.js +78 -65
  145. package/src/shade/descriptor/pipeline/render/RenderPipelineDescriptor.d.ts.map +1 -1
  146. package/src/shade/descriptor/pipeline/render/RenderPipelineDescriptor.js +176 -172
  147. package/src/shade/descriptor/pipeline/render/StencilFaceState.d.ts +4 -0
  148. package/src/shade/descriptor/pipeline/render/StencilFaceState.d.ts.map +1 -1
  149. package/src/shade/descriptor/pipeline/render/StencilFaceState.js +107 -88
  150. package/src/shade/playground/particle_ecs/README.md +9 -8
  151. package/src/shade/playground/particle_ecs/bonfire_editor.js +3 -3
  152. package/src/shade/playground/particle_ecs/bonfire_effects.d.ts +6 -6
  153. package/src/shade/playground/particle_ecs/bonfire_effects.js +6 -6
  154. package/src/shade/playground/particle_editor/README.md +14 -11
  155. package/src/shade/playground/particle_system/particle_prototype.js +18 -18
  156. package/src/shade/playground/particle_system/particle_scene.js +13 -13
  157. package/src/shade/playground/probe_clipmap/README.md +42 -0
  158. package/src/shade/playground/probe_clipmap/index.html +178 -0
  159. package/src/shade/playground/probe_clipmap/main.d.ts +8 -0
  160. package/src/shade/playground/probe_clipmap/main.d.ts.map +1 -0
  161. package/src/shade/playground/probe_clipmap/main.js +360 -0
  162. package/src/shade/renderer/Renderer.d.ts +3 -1
  163. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  164. package/src/shade/renderer/Renderer.js +94 -38
  165. package/src/shade/renderer/extension/ViewTextures.d.ts +2 -1
  166. package/src/shade/renderer/extension/ViewTextures.d.ts.map +1 -1
  167. package/src/shade/renderer/extension/ViewTextures.js +2 -1
  168. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  169. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  170. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  171. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  172. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  173. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts +34 -12
  174. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -1
  175. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +56 -15
  176. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts +2 -2
  177. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts.map +1 -1
  178. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.js +13 -26
  179. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.d.ts +25 -2
  180. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.d.ts.map +1 -1
  181. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.js +54 -32
  182. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_VERDICT.d.ts +14 -0
  183. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_VERDICT.d.ts.map +1 -0
  184. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_VERDICT.js +37 -0
  185. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.d.ts.map +1 -1
  186. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.js +16 -0
  187. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.d.ts.map +1 -1
  188. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.js +2 -6
  189. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_descent.d.ts +131 -0
  190. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_descent.d.ts.map +1 -0
  191. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_descent.js +467 -0
  192. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_instance.d.ts +24 -0
  193. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_instance.d.ts.map +1 -0
  194. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_instance.js +111 -0
  195. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_rule.d.ts +42 -0
  196. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_rule.d.ts.map +1 -0
  197. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_rule.js +404 -0
  198. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_stat.d.ts +16 -0
  199. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_stat.d.ts.map +1 -0
  200. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_stat.js +23 -0
  201. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_want.d.ts +17 -0
  202. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_want.d.ts.map +1 -0
  203. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_want.js +65 -0
  204. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts.map +1 -1
  205. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.js +14 -70
  206. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -1
  207. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +13 -773
  208. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_cut_descent.d.ts +55 -0
  209. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_cut_descent.d.ts.map +1 -0
  210. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_cut_descent.js +248 -0
  211. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts.map +1 -1
  212. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.js +15 -174
  213. package/src/shade/renderer/global_illumination/brick4/gpu/draw/chunk_brick4_probe_meta_pick2.d.ts.map +1 -1
  214. package/src/shade/renderer/global_illumination/brick4/gpu/draw/chunk_brick4_probe_meta_pick2.js +8 -24
  215. package/src/shade/renderer/global_illumination/brick4/gpu/draw/chunk_brick4_sample_forward_indirect.d.ts.map +1 -1
  216. package/src/shade/renderer/global_illumination/brick4/gpu/draw/chunk_brick4_sample_forward_indirect.js +1 -25
  217. package/src/shade/renderer/global_illumination/lpv/deprecated/chunk_lpv_get_visibility_mask.d.ts.map +1 -1
  218. package/src/shade/renderer/global_illumination/lpv/deprecated/chunk_lpv_get_visibility_mask.js +5 -23
  219. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts +25 -4
  220. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  221. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +126 -8
  222. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +832 -4
  223. package/src/shade/renderer/global_illumination/probe_clipmap/brick/BRICK_CLIPMAP.d.ts +7 -0
  224. package/src/shade/renderer/global_illumination/probe_clipmap/brick/BRICK_CLIPMAP.d.ts.map +1 -1
  225. package/src/shade/renderer/global_illumination/probe_clipmap/brick/BRICK_CLIPMAP.js +8 -0
  226. package/src/shade/renderer/global_illumination/probe_clipmap/brick/GPUBrickClipmap.d.ts +16 -0
  227. package/src/shade/renderer/global_illumination/probe_clipmap/brick/GPUBrickClipmap.d.ts.map +1 -1
  228. package/src/shade/renderer/global_illumination/probe_clipmap/brick/GPUBrickClipmap.js +9 -0
  229. package/src/shade/renderer/global_illumination/probe_clipmap/brick/{mesh/chunk_mesh_brick_object.d.ts → chunk_brick_object.d.ts} +8 -8
  230. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_object.d.ts.map +1 -0
  231. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_object.js +42 -0
  232. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_provider.d.ts +45 -0
  233. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_provider.d.ts.map +1 -0
  234. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_provider.js +93 -0
  235. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/MESH_BRICK_INSTANCE_STRUCT.d.ts +0 -7
  236. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/MESH_BRICK_INSTANCE_STRUCT.d.ts.map +1 -1
  237. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/MESH_BRICK_INSTANCE_STRUCT.js +0 -8
  238. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.d.ts +2 -4
  239. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.d.ts.map +1 -1
  240. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.js +5 -8
  241. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_provider.d.ts.map +1 -1
  242. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_provider.js +13 -22
  243. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/VGEO_BRICK.d.ts +99 -0
  244. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/VGEO_BRICK.d.ts.map +1 -0
  245. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/VGEO_BRICK.js +139 -0
  246. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/VGeoBrickProvider.d.ts +65 -0
  247. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/VGeoBrickProvider.d.ts.map +1 -0
  248. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/VGeoBrickProvider.js +393 -0
  249. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/chunk_vgeo_brick_box.d.ts +17 -0
  250. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/chunk_vgeo_brick_box.d.ts.map +1 -0
  251. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/chunk_vgeo_brick_box.js +36 -0
  252. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/chunk_vgeo_brick_pair.d.ts +29 -0
  253. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/chunk_vgeo_brick_pair.d.ts.map +1 -0
  254. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/chunk_vgeo_brick_pair.js +108 -0
  255. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/chunk_vgeo_brick_texel.d.ts +31 -0
  256. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/chunk_vgeo_brick_texel.d.ts.map +1 -0
  257. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/chunk_vgeo_brick_texel.js +113 -0
  258. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/chunk_vgeo_brick_triangle.d.ts +36 -0
  259. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/chunk_vgeo_brick_triangle.d.ts.map +1 -0
  260. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/chunk_vgeo_brick_triangle.js +85 -0
  261. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/shader_vgeo_brick_distance.d.ts +23 -0
  262. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/shader_vgeo_brick_distance.d.ts.map +1 -0
  263. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/shader_vgeo_brick_distance.js +273 -0
  264. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/shader_vgeo_brick_gather.d.ts +13 -0
  265. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/shader_vgeo_brick_gather.d.ts.map +1 -0
  266. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/shader_vgeo_brick_gather.js +151 -0
  267. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/shader_vgeo_brick_traverse.d.ts +13 -0
  268. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/shader_vgeo_brick_traverse.d.ts.map +1 -0
  269. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/shader_vgeo_brick_traverse.js +133 -0
  270. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/shader_vgeo_brick_wait.d.ts +22 -0
  271. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/shader_vgeo_brick_wait.d.ts.map +1 -0
  272. package/src/shade/renderer/global_illumination/probe_clipmap/brick/vgeo/shader_vgeo_brick_wait.js +135 -0
  273. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +20 -6
  274. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  275. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +152 -10
  276. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.d.ts +7 -0
  277. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.d.ts.map +1 -1
  278. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.js +8 -0
  279. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.d.ts +14 -2
  280. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.d.ts.map +1 -1
  281. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.js +50 -5
  282. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_cage.d.ts +19 -0
  283. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_cage.d.ts.map +1 -0
  284. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_cage.js +40 -0
  285. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_pick.d.ts +72 -0
  286. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_pick.d.ts.map +1 -0
  287. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_pick.js +343 -0
  288. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_sample_radiance.d.ts +29 -0
  289. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_sample_radiance.d.ts.map +1 -0
  290. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_sample_radiance.js +188 -0
  291. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_want.d.ts +26 -0
  292. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_want.d.ts.map +1 -0
  293. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_want.js +132 -0
  294. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.d.ts +4 -0
  295. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.d.ts.map +1 -1
  296. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.js +26 -3
  297. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_admit.d.ts +3 -0
  298. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_admit.d.ts.map +1 -1
  299. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_admit.js +8 -1
  300. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_diffuse.d.ts +12 -0
  301. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_diffuse.d.ts.map +1 -0
  302. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_diffuse.js +101 -0
  303. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_due.d.ts +1 -1
  304. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_due.js +1 -1
  305. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_indirect_lighting.d.ts +15 -0
  306. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_indirect_lighting.d.ts.map +1 -0
  307. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_indirect_lighting.js +175 -0
  308. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_light.d.ts +26 -0
  309. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_light.d.ts.map +1 -0
  310. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_light.js +210 -0
  311. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_retire.d.ts +3 -3
  312. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_retire.js +3 -3
  313. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts +2 -1
  314. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  315. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +5 -1
  316. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_trace.d.ts.map +1 -1
  317. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_trace.js +3 -10
  318. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_want.d.ts +5 -9
  319. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_want.d.ts.map +1 -1
  320. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_want.js +9 -110
  321. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_want_hits.d.ts +27 -0
  322. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_want_hits.d.ts.map +1 -0
  323. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_want_hits.js +110 -0
  324. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.d.ts +13 -0
  325. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.d.ts.map +1 -1
  326. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.js +40 -0
  327. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.d.ts +11 -6
  328. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.d.ts.map +1 -1
  329. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.js +15 -8
  330. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.d.ts +9 -2
  331. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.d.ts.map +1 -1
  332. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.js +25 -11
  333. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  334. package/src/shade/renderer/particles/DESIGN.md +151 -42
  335. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +283 -0
  336. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +10 -2
  337. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  338. package/src/shade/renderer/particles/GPUParticleSystem.js +46 -8
  339. package/src/shade/renderer/particles/ParticleConstants.d.ts +23 -6
  340. package/src/shade/renderer/particles/ParticleConstants.d.ts.map +1 -1
  341. package/src/shade/renderer/particles/ParticleConstants.js +25 -6
  342. package/src/shade/renderer/particles/bounds/chunk_particle_bounds.js +5 -5
  343. package/src/shade/renderer/particles/bounds/shader_particle_bounds.js +187 -187
  344. package/src/shade/renderer/particles/data/PARTICLE_DATABASE_SPEC.d.ts +20 -9
  345. package/src/shade/renderer/particles/data/PARTICLE_DATABASE_SPEC.d.ts.map +1 -1
  346. package/src/shade/renderer/particles/data/PARTICLE_DATABASE_SPEC.js +36 -10
  347. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STRUCT.d.ts +8 -7
  348. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STRUCT.d.ts.map +1 -1
  349. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STRUCT.js +25 -16
  350. package/src/shade/renderer/particles/data/PARTICLE_FIELD_BINDING.d.ts +51 -0
  351. package/src/shade/renderer/particles/data/PARTICLE_FIELD_BINDING.d.ts.map +1 -0
  352. package/src/shade/renderer/particles/data/PARTICLE_FIELD_BINDING.js +56 -0
  353. package/src/shade/renderer/particles/data/particle_emitter_record.d.ts.map +1 -1
  354. package/src/shade/renderer/particles/data/particle_emitter_record.js +2 -0
  355. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.d.ts +1 -1
  356. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.js +1 -1
  357. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +22 -11
  358. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -1
  359. package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +14 -11
  360. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +4 -4
  361. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
  362. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +59 -24
  363. package/src/shade/renderer/particles/graph/ParticleValueType.d.ts +3 -3
  364. package/src/shade/renderer/particles/graph/ParticleValueType.js +287 -287
  365. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +13 -6
  366. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
  367. package/src/shade/renderer/particles/graph/compile_particle_graph.js +46 -27
  368. package/src/shade/renderer/particles/graph/infer_particle_graph_types.d.ts +8 -2
  369. package/src/shade/renderer/particles/graph/infer_particle_graph_types.d.ts.map +1 -1
  370. package/src/shade/renderer/particles/graph/infer_particle_graph_types.js +240 -234
  371. package/src/shade/renderer/particles/graph/make_particle_group.d.ts +1 -1
  372. package/src/shade/renderer/particles/graph/make_particle_group.js +1 -1
  373. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts +3 -2
  374. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts.map +1 -1
  375. package/src/shade/renderer/particles/graph/particle_graph_authoring.js +3 -2
  376. package/src/shade/renderer/particles/graph/particle_graph_distributions.js +170 -170
  377. package/src/shade/renderer/particles/graph/particle_graph_json.d.ts +25 -16
  378. package/src/shade/renderer/particles/graph/particle_graph_json.d.ts.map +1 -1
  379. package/src/shade/renderer/particles/graph/particle_graph_json.js +16 -2
  380. package/src/shade/renderer/particles/graph/particle_node_parameters.d.ts +29 -15
  381. package/src/shade/renderer/particles/graph/particle_node_parameters.d.ts.map +1 -1
  382. package/src/shade/renderer/particles/graph/particle_node_parameters.js +54 -22
  383. package/src/shade/renderer/particles/graph/particle_node_presentation.d.ts.map +1 -1
  384. package/src/shade/renderer/particles/graph/particle_node_presentation.js +18 -8
  385. package/src/shade/renderer/particles/graph/validate_particle_graph.d.ts +9 -2
  386. package/src/shade/renderer/particles/graph/validate_particle_graph.d.ts.map +1 -1
  387. package/src/shade/renderer/particles/graph/validate_particle_graph.js +70 -32
  388. package/src/shade/renderer/particles/graph_particles.d.ts +9 -4
  389. package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
  390. package/src/shade/renderer/particles/graph_particles.js +27 -4
  391. package/src/shade/renderer/particles/isa/InstructionStream.d.ts +7 -6
  392. package/src/shade/renderer/particles/isa/InstructionStream.d.ts.map +1 -1
  393. package/src/shade/renderer/particles/isa/InstructionStream.js +17 -16
  394. package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts +17 -0
  395. package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts.map +1 -1
  396. package/src/shade/renderer/particles/isa/ParticleAssembler.js +35 -0
  397. package/src/shade/renderer/particles/isa/ParticleProgram.d.ts +26 -6
  398. package/src/shade/renderer/particles/isa/ParticleProgram.d.ts.map +1 -1
  399. package/src/shade/renderer/particles/isa/ParticleProgram.js +61 -9
  400. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts +55 -4
  401. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts.map +1 -1
  402. package/src/shade/renderer/particles/isa/ParticleVMISA.js +79 -10
  403. package/src/shade/renderer/particles/isa/particle_assembly.d.ts +20 -6
  404. package/src/shade/renderer/particles/isa/particle_assembly.d.ts.map +1 -1
  405. package/src/shade/renderer/particles/isa/particle_assembly.js +136 -25
  406. package/src/shade/renderer/particles/isa/particle_link_global_fields.d.ts +20 -0
  407. package/src/shade/renderer/particles/isa/particle_link_global_fields.d.ts.map +1 -0
  408. package/src/shade/renderer/particles/isa/particle_link_global_fields.js +46 -0
  409. package/src/shade/renderer/particles/isa/validate_particle_program.js +8 -8
  410. package/src/shade/renderer/particles/layout/ParticleLayout.d.ts +62 -28
  411. package/src/shade/renderer/particles/layout/ParticleLayout.d.ts.map +1 -1
  412. package/src/shade/renderer/particles/layout/ParticleLayout.js +102 -62
  413. package/src/shade/renderer/particles/optimizer/dag/optimize_particle_program_vector.d.ts.map +1 -1
  414. package/src/shade/renderer/particles/optimizer/dag/optimize_particle_program_vector.js +2 -0
  415. package/src/shade/renderer/particles/optimizer/dag/particle_vector_groups.js +1 -1
  416. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lift.js +4 -3
  417. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lower.js +2 -2
  418. package/src/shade/renderer/particles/optimizer/dag/particle_vector_pack.d.ts +3 -3
  419. package/src/shade/renderer/particles/optimizer/dag/particle_vector_pack.js +11 -11
  420. package/src/shade/renderer/particles/optimizer/optimize_particle_program.d.ts +4 -4
  421. package/src/shade/renderer/particles/optimizer/optimize_particle_program.d.ts.map +1 -1
  422. package/src/shade/renderer/particles/optimizer/optimize_particle_program.js +5 -4
  423. package/src/shade/renderer/particles/optimizer/particle_integer_bounds.d.ts +1 -1
  424. package/src/shade/renderer/particles/optimizer/particle_integer_bounds.js +1 -1
  425. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.d.ts +4 -4
  426. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.d.ts.map +1 -1
  427. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.js +13 -11
  428. package/src/shade/renderer/particles/optimizer/particle_program_fuzz.js +7 -7
  429. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts +3 -2
  430. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts.map +1 -1
  431. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.js +5 -4
  432. package/src/shade/renderer/particles/optimizer/pass_allocate_registers.d.ts.map +1 -1
  433. package/src/shade/renderer/particles/optimizer/pass_dedup_constants.d.ts.map +1 -1
  434. package/src/shade/renderer/particles/optimizer/pass_eliminate_dead_code.d.ts.map +1 -1
  435. package/src/shade/renderer/particles/optimizer/pass_eliminate_dead_code.js +51 -51
  436. package/src/shade/renderer/particles/optimizer/pass_fold_constants.d.ts.map +1 -1
  437. package/src/shade/renderer/particles/optimizer/pass_fold_constants.js +5 -5
  438. package/src/shade/renderer/particles/optimizer/pass_inline_constants.d.ts.map +1 -1
  439. package/src/shade/renderer/particles/optimizer/pass_narrow_widths.d.ts.map +1 -1
  440. package/src/shade/renderer/particles/optimizer/pass_pack_registers.d.ts.map +1 -1
  441. package/src/shade/renderer/particles/optimizer/pass_peephole.d.ts.map +1 -1
  442. package/src/shade/renderer/particles/optimizer/pass_propagate_copies.d.ts.map +1 -1
  443. package/src/shade/renderer/particles/optimizer/pass_schedule.d.ts.map +1 -1
  444. package/src/shade/renderer/particles/optimizer/pass_schedule.js +2 -2
  445. package/src/shade/renderer/particles/optimizer/pass_share_subexpressions.d.ts.map +1 -1
  446. package/src/shade/renderer/particles/optimizer/pass_share_subexpressions.js +5 -4
  447. package/src/shade/renderer/particles/runtime/EmitterRegistry.d.ts +93 -22
  448. package/src/shade/renderer/particles/runtime/EmitterRegistry.d.ts.map +1 -1
  449. package/src/shade/renderer/particles/runtime/EmitterRegistry.js +336 -29
  450. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.d.ts +43 -3
  451. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.d.ts.map +1 -1
  452. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.js +43 -3
  453. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts +56 -26
  454. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts.map +1 -1
  455. package/src/shade/renderer/particles/runtime/ParticleEmitter.js +89 -33
  456. package/src/shade/renderer/particles/runtime/ParticleGlobalFields.d.ts +85 -0
  457. package/src/shade/renderer/particles/runtime/ParticleGlobalFields.d.ts.map +1 -0
  458. package/src/shade/renderer/particles/runtime/ParticleGlobalFields.js +221 -0
  459. package/src/shade/renderer/particles/runtime/ProgramHeap.d.ts +143 -18
  460. package/src/shade/renderer/particles/runtime/ProgramHeap.d.ts.map +1 -1
  461. package/src/shade/renderer/particles/runtime/ProgramHeap.js +378 -28
  462. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +19 -7
  463. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
  464. package/src/shade/renderer/particles/runtime/create_particle_effect.js +22 -9
  465. package/src/shade/renderer/particles/shaders/chunk_particle_billboard_vertex.js +8 -8
  466. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.js +1 -1
  467. package/src/shade/renderer/particles/shaders/chunk_particle_setup_context.d.ts +3 -0
  468. package/src/shade/renderer/particles/shaders/chunk_particle_setup_context.d.ts.map +1 -1
  469. package/src/shade/renderer/particles/shaders/chunk_particle_setup_context.js +4 -0
  470. package/src/shade/renderer/particles/shaders/shader_particle_field_bind.d.ts +30 -0
  471. package/src/shade/renderer/particles/shaders/shader_particle_field_bind.d.ts.map +1 -0
  472. package/src/shade/renderer/particles/shaders/shader_particle_field_bind.js +121 -0
  473. package/src/shade/renderer/particles/vm/PARTICLE_VM_CONTEXT_STRUCT.d.ts.map +1 -1
  474. package/src/shade/renderer/particles/vm/PARTICLE_VM_CONTEXT_STRUCT.js +3 -1
  475. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts +12 -4
  476. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts.map +1 -1
  477. package/src/shade/renderer/particles/vm/ParticleVMReference.js +48 -11
  478. package/src/shade/renderer/particles/vm/chunk_particle_vm.d.ts.map +1 -1
  479. package/src/shade/renderer/particles/vm/chunk_particle_vm.js +36 -14
  480. package/src/shade/renderer/postprocess/PostProcess.d.ts.map +1 -1
  481. package/src/shade/renderer/postprocess/PostProcess.js +2 -1
  482. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts +31 -19
  483. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts.map +1 -1
  484. package/src/shade/renderer/postprocess/gtao/GTAO.js +44 -30
  485. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts.map +1 -1
  486. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.js +16 -2
  487. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  488. package/src/shade/renderer/postprocess/nss/NSS.d.ts.map +1 -1
  489. package/src/shade/renderer/postprocess/nss/NSS.js +4 -3
  490. package/src/shade/renderer/postprocess/ssr/SSR.d.ts +7 -1
  491. package/src/shade/renderer/postprocess/ssr/SSR.d.ts.map +1 -1
  492. package/src/shade/renderer/postprocess/ssr/SSR.js +16 -0
  493. package/src/shade/renderer/postprocess/ssr/resolve/chunk_ssr_add_hit_irradiance.d.ts +20 -2
  494. package/src/shade/renderer/postprocess/ssr/resolve/chunk_ssr_add_hit_irradiance.d.ts.map +1 -1
  495. package/src/shade/renderer/postprocess/ssr/resolve/chunk_ssr_add_hit_irradiance.js +53 -3
  496. package/src/shade/renderer/postprocess/ssr/resolve/chunk_ssr_sample_radiance.d.ts +10 -2
  497. package/src/shade/renderer/postprocess/ssr/resolve/chunk_ssr_sample_radiance.d.ts.map +1 -1
  498. package/src/shade/renderer/postprocess/ssr/resolve/chunk_ssr_sample_radiance.js +31 -3
  499. package/src/shade/renderer/postprocess/ssr/resolve/ssr_resolve_shader.d.ts.map +1 -1
  500. package/src/shade/renderer/postprocess/ssr/resolve/ssr_resolve_shader.js +17 -11
  501. package/src/shade/renderer/postprocess/taa/TAA.d.ts +4 -2
  502. package/src/shade/renderer/postprocess/taa/TAA.d.ts.map +1 -1
  503. package/src/shade/renderer/postprocess/taa/TAA.js +6 -1
  504. package/src/shade/renderer/postprocess/taa/chunk_fsr2_compute_depth_clip.d.ts.map +1 -1
  505. package/src/shade/renderer/postprocess/taa/chunk_fsr2_compute_depth_clip.js +10 -5
  506. package/src/shade/renderer/postprocess/taa/shader_taa.d.ts.map +1 -1
  507. package/src/shade/renderer/postprocess/taa/shader_taa.js +15 -12
  508. package/src/shade/renderer/postprocess/velocity/graph_add_per_object_velocity.d.ts.map +1 -1
  509. package/src/shade/renderer/postprocess/velocity/graph_add_per_object_velocity.js +26 -17
  510. package/src/shade/renderer/rasterize/forward/chunk_forward_sample_indirect.d.ts +4 -3
  511. package/src/shade/renderer/rasterize/forward/chunk_forward_sample_indirect.d.ts.map +1 -1
  512. package/src/shade/renderer/rasterize/forward/chunk_forward_sample_indirect.js +6 -4
  513. package/src/shade/renderer/rasterize/forward/chunk_forward_sample_indirect_probe_clipmap.d.ts +12 -0
  514. package/src/shade/renderer/rasterize/forward/chunk_forward_sample_indirect_probe_clipmap.d.ts.map +1 -0
  515. package/src/shade/renderer/rasterize/forward/chunk_forward_sample_indirect_probe_clipmap.js +57 -0
  516. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts +4 -2
  517. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts.map +1 -1
  518. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.js +4 -2
  519. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts +8 -2
  520. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts.map +1 -1
  521. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +28 -15
  522. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw.d.ts +2 -1
  523. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw.d.ts.map +1 -1
  524. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw.js +19 -3
  525. package/src/shade/renderer/rasterize/native/graph_rasterize_meshes_alpha_tested.d.ts.map +1 -1
  526. package/src/shade/renderer/rasterize/native/graph_rasterize_meshes_alpha_tested.js +4 -2
  527. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.d.ts +3 -1
  528. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.d.ts.map +1 -1
  529. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.js +27 -14
  530. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments.d.ts +2 -1
  531. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments.d.ts.map +1 -1
  532. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments.js +19 -3
  533. package/src/shade/renderer/shader/chunk/geometry/meshlet/chunk_read_meshlet_triangle_bounds.d.ts +2 -8
  534. package/src/shade/renderer/shader/chunk/geometry/meshlet/chunk_read_meshlet_triangle_bounds.d.ts.map +1 -1
  535. package/src/shade/renderer/shader/chunk/geometry/meshlet/chunk_read_meshlet_triangle_bounds.js +25 -52
  536. package/src/shade/renderer/shader/chunk/geometry/meshlet/chunk_read_meshlet_triangle_positions.d.ts +15 -0
  537. package/src/shade/renderer/shader/chunk/geometry/meshlet/chunk_read_meshlet_triangle_positions.d.ts.map +1 -0
  538. package/src/shade/renderer/shader/chunk/geometry/meshlet/chunk_read_meshlet_triangle_positions.js +52 -0
  539. package/src/shade/renderer/shader/chunk/geometry/triangle/TRIANGLE_POSITIONS_STRUCT.d.ts +7 -0
  540. package/src/shade/renderer/shader/chunk/geometry/triangle/TRIANGLE_POSITIONS_STRUCT.d.ts.map +1 -0
  541. package/src/shade/renderer/shader/chunk/geometry/triangle/TRIANGLE_POSITIONS_STRUCT.js +12 -0
  542. package/src/shade/renderer/shader/chunk/grid/chunk_index_to_grid2d_reciprocal.d.ts +22 -0
  543. package/src/shade/renderer/shader/chunk/grid/chunk_index_to_grid2d_reciprocal.d.ts.map +1 -0
  544. package/src/shade/renderer/shader/chunk/grid/chunk_index_to_grid2d_reciprocal.js +37 -0
  545. package/src/shade/renderer/shader/chunk/light/lpv/depth/chunk_lpv_mask_weights_by_visibility.d.ts.map +1 -1
  546. package/src/shade/renderer/shader/chunk/light/lpv/depth/chunk_lpv_mask_weights_by_visibility.js +26 -60
  547. package/src/shade/renderer/shader/chunk/light/probe/chunk_probe_visibility_weight.d.ts +23 -0
  548. package/src/shade/renderer/shader/chunk/light/probe/chunk_probe_visibility_weight.d.ts.map +1 -0
  549. package/src/shade/renderer/shader/chunk/light/probe/chunk_probe_visibility_weight.js +54 -0
  550. package/src/shade/renderer/shader/chunk/math/chunk_f16_round_stochastic.d.ts +25 -0
  551. package/src/shade/renderer/shader/chunk/math/chunk_f16_round_stochastic.d.ts.map +1 -0
  552. package/src/shade/renderer/shader/chunk/math/chunk_f16_round_stochastic.js +43 -0
  553. package/src/shade/renderer/shader/chunk/random/chunk_jitter_from_vectors.d.ts +14 -0
  554. package/src/shade/renderer/shader/chunk/random/chunk_jitter_from_vectors.d.ts.map +1 -0
  555. package/src/shade/renderer/shader/chunk/random/chunk_jitter_from_vectors.js +37 -0
  556. package/src/shade/renderer/shader/chunk/random/reservoir/chunk_pick2_wrs_mat2x4.d.ts +27 -0
  557. package/src/shade/renderer/shader/chunk/random/reservoir/chunk_pick2_wrs_mat2x4.d.ts.map +1 -0
  558. package/src/shade/renderer/shader/chunk/random/reservoir/chunk_pick2_wrs_mat2x4.js +61 -0
  559. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/chunk_resolve_irradiance_sh_to_specular.d.ts +21 -0
  560. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/chunk_resolve_irradiance_sh_to_specular.d.ts.map +1 -0
  561. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/chunk_resolve_irradiance_sh_to_specular.js +41 -0
  562. package/src/shade/renderer/texture/util/texture_fill_rectangle.d.ts.map +1 -1
  563. package/src/shade/renderer/texture/util/texture_fill_rectangle.js +282 -267
  564. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts +8 -4
  565. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  566. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +36 -36
  567. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.d.ts +6 -1
  568. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.d.ts.map +1 -1
  569. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.js +498 -484
  570. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.d.ts +15 -2
  571. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.d.ts.map +1 -1
  572. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.js +57 -3
  573. package/src/shade/renderer/volumetrics/irradiance_clipmap/shader_irradiance_clipmap_build.d.ts.map +1 -1
  574. package/src/shade/renderer/volumetrics/irradiance_clipmap/shader_irradiance_clipmap_build.js +197 -191
  575. package/src/shade/renderer/volumetrics/taa/shader_volumetrics_taa.js +4 -4
  576. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +2 -1
  577. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  578. package/src/shade/renderer/water/GPUWaterRenderer.js +5 -1
  579. package/src/shade/renderer/water/chunk_water_gi.d.ts +13 -3
  580. package/src/shade/renderer/water/chunk_water_gi.d.ts.map +1 -1
  581. package/src/shade/renderer/water/chunk_water_gi.js +47 -5
  582. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  583. package/src/shade/renderer/water/shader_water.d.ts +3 -2
  584. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  585. package/src/shade/renderer/water/shader_water.js +9 -3
  586. package/src/shade/renderer/geometry/sdf/SDF_BUILD_SETTINGS_STRUCT.d.ts +0 -3
  587. package/src/shade/renderer/geometry/sdf/SDF_BUILD_SETTINGS_STRUCT.d.ts.map +0 -1
  588. package/src/shade/renderer/geometry/sdf/SFD_BUILD_WORKGROUP_SIZE.d.ts +0 -2
  589. package/src/shade/renderer/geometry/sdf/SFD_BUILD_WORKGROUP_SIZE.d.ts.map +0 -1
  590. package/src/shade/renderer/geometry/sdf/chunk_blas_query_sdf.d.ts +0 -8
  591. package/src/shade/renderer/geometry/sdf/chunk_blas_query_sdf.d.ts.map +0 -1
  592. package/src/shade/renderer/geometry/sdf/chunk_blas_query_volume_sign.d.ts +0 -3
  593. package/src/shade/renderer/geometry/sdf/chunk_blas_query_volume_sign.d.ts.map +0 -1
  594. package/src/shade/renderer/geometry/sdf/shader_build_sdf.d.ts +0 -3
  595. package/src/shade/renderer/geometry/sdf/shader_build_sdf.d.ts.map +0 -1
  596. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_object.d.ts.map +0 -1
  597. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_object.js +0 -42
  598. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.d.ts +0 -31
  599. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.d.ts.map +0 -1
  600. package/src/shade/renderer/shadow/vsm/VSMMoverTracker.d.ts +0 -42
  601. package/src/shade/renderer/shadow/vsm/VSMMoverTracker.d.ts.map +0 -1
  602. package/src/shade/renderer/shadow/vsm/VSM_MOVER_LAYOUT.d.ts +0 -54
  603. package/src/shade/renderer/shadow/vsm/VSM_MOVER_LAYOUT.d.ts.map +0 -1
  604. package/src/shade/renderer/shadow/vsm/shader/chunk_vsm_append_mover.d.ts +0 -14
  605. package/src/shade/renderer/shadow/vsm/shader/chunk_vsm_append_mover.d.ts.map +0 -1
  606. package/src/shade/renderer/shadow/vsm/shader/shader_vsm_movers.d.ts +0 -30
  607. package/src/shade/renderer/shadow/vsm/shader/shader_vsm_movers.d.ts.map +0 -1
  608. package/src/shade/renderer/shadow/vsm/shader/shader_vsm_skinned_movers.d.ts +0 -23
  609. package/src/shade/renderer/shadow/vsm/shader/shader_vsm_skinned_movers.d.ts.map +0 -1
@@ -1,904 +1,1351 @@
1
- import { assert } from "../../core/assert.js";
2
- import { warn_limited } from "../../shade/util/warn_limited.js";
3
- import { FramePhase } from "../../shade/renderer/extension/FramePhase.js";
4
- import { RenderExtension } from "../../shade/renderer/extension/RenderExtension.js";
5
- import { ParticleEmitter } from "../../shade/renderer/particles/runtime/ParticleEmitter.js";
6
- import { GPUTextureContext } from "../../shade/renderer/texture/GPUTextureContext.js";
7
- import { System } from "../ecs/System.js";
8
- import Name from "../ecs/name/Name.js";
9
- import { Transform64 } from "../ecs/transform/Transform64.js";
10
- import { TRANSFORM64_EVENT_CHANGE } from "../ecs/transform/TRANSFORM64_EVENT_CHANGE.js";
11
- import { row_of_entity } from "../../shade/renderer/scene/rows/GPUSceneRows.js";
12
- import { GameAssetType } from "../asset/GameAssetType.js";
13
- import { ImageRGBADataLoader } from "../asset/loaders/image/ImageRGBADataLoader.js";
14
- import { ParticleEffect } from "../graphics/ecs/particles/ParticleEffect.js";
15
- import { ManagedAtlas } from "../graphics/texture/atlas/ManagedTextureAtlas.js";
16
- import { SpriteAtlas } from "../graphics/texture/atlas/SpriteAtlas.js";
17
- import { SpriteAtlasFill } from "../graphics/texture/atlas/SpriteAtlasFill.js";
18
- import { TextureFilterType } from "../../shade/renderer/texture/TextureFilterType.js";
19
- import { Sampler2D } from "../graphics/texture/sampler/Sampler2D.js";
20
-
21
- /**
22
- * Bursts {@link GPUParticleEmitterSystem#burst} will hold before it starts dropping them. Reached
23
- * only when frames have stopped arriving — see the call site.
24
- *
25
- * @type {number}
26
- */
27
- const BURST_QUEUE_LIMIT = 256;
28
-
29
- /**
30
- * @typedef {object} ParticleEmitterEntry
31
- * @property {number} entity
32
- * @property {ParticleEffect} effect the entity's component — what to emit
33
- * @property {Transform64} transform the entity's transform — where to emit it
34
- * @property {ParticleEmitter|null} emitter the emitter, once the effect has a program to run
35
- * @property {import("../../shade/renderer/scene/GPUSceneContext.js").GPUSceneContext|null} context
36
- * the scene context the emitter is registered with, at the entity's row; null until a frame has
37
- * registered it, or after a device restart took that context away
38
- * @property {function():void} on_transform_changed
39
- * @property {string|null} url last observed {@link ParticleEffect#texture}
40
- * @property {import("../graphics/texture/atlas/ManagedTextureAtlas.js").ManagedAtlasReference|null} texture shared image reference, or null for an untextured effect
41
- */
42
-
43
- /**
44
- * Draws the game's GPU particle effects: one emitter per entity carrying
45
- * {@link ../graphics/ecs/particles/ParticleEffect.js ParticleEffect} and
46
- * {@link ../ecs/transform/Transform64.js Transform64}.
47
- *
48
- * ```js
49
- * entityManager.addSystem(new GPUParticleEmitterSystem(graphics, scene, assetManager));
50
- *
51
- * new Entity()
52
- * .add(new Transform64())
53
- * .add(ParticleEffect.from({ ...create_particle_effect({ layout, init, update }), spawn_rate: 400 }))
54
- * .build(dataset);
55
- * ```
56
- *
57
- * **The entity is the emitter.** The component says what it emits and the transform says where, the
58
- * same division every other placed thing in this engine uses — a `Decal`, an `SGMesh`, a `Light`.
59
- * Which means an emitter is spawned, moved, parented, attached to a bone, saved and destroyed by
60
- * exactly the machinery that does those things for everything else, and a particle effect stops
61
- * being a special case that needs its own scene-graph call.
62
- *
63
- * ## What this system does with the pair
64
- *
65
- * It builds a {@link ParticleEmitter} per entity and owns it: the emitter is registered with the
66
- * scene's particle system at the entity's row when the component has a compiled effect, so it
67
- * stands where the entity stands, carries whatever the component currently says, and is retired
68
- * when the entity goes. The emitter is not hidden — {@link emitter_of} hands it out — but it is
69
- * not the interface either: write to the component and the next frame carries it.
70
- *
71
- * Changes are found by **comparison** rather than announced. Once a frame each entity's
72
- * fields are compared against what was last written to its emitter, and the emitter is marked dirty only
73
- * where they differ; `EmitterRegistry` then restages exactly the rows that moved. A component nobody
74
- * touched costs a run of integer comparisons and no upload, so there is no change signal to fire and
75
- * none to forget. What that buys is that gameplay can write `effect.spawn_rate = 0` anywhere,
76
- * including from code that has never heard of this system.
77
- *
78
- * ## It owns the scene's sprite atlas
79
- *
80
- * Every {@link ParticleEffect#texture} is loaded through the supplied `AssetManager` and packed into
81
- * one {@link SpriteAtlas} shared by the scene, one patch per distinct URL however many entities
82
- * name it, reference-counted through {@link ManagedAtlas} so unreferenced sprites remain in the
83
- * atlas graveyard and are only reaped when incoming sprites need the space. A `FrameStart`
84
- * extension publishes the pixels and every current patch region before the renderer records the
85
- * particle passes — every repack moves UVs that nothing else changed, so the regions are rewritten
86
- * whole rather than when a URL changes. Pending and failed images sample a transparent texel;
87
- * effects with no image sample a white one.
88
- *
89
- * An emitter in this scene that this system did not build — one registered with the particle
90
- * system directly — gets the white texel. The atlas is the scene's and that emitter has nothing in
91
- * it, so the alternative is not "its own texture" but a UV rectangle from somebody else's image.
92
- *
93
- * ## What it is not
94
- *
95
- * It does not simulate anything and does not record a particle pass. `GPUParticleSystem` does both,
96
- * inside the renderer, driven from the emitter table these rows are in — see
97
- * `shade/renderer/particles/DESIGN.md`. This system's whole contribution is membership, placement,
98
- * the fields, and the sprite sheet.
99
- */
100
- export class GPUParticleEmitterSystem extends System {
101
- dependencies = [ParticleEffect, Transform64];
102
-
103
- /**
104
- * CPU atlas, exposed for inspection. Owned by this system.
105
- * @type {SpriteAtlas}
106
- */
107
- atlas = new SpriteAtlas(SpriteAtlasFill.EdgeExtend, 64);
108
-
109
- /** @type {import("./GraphicsEngine.js").GraphicsEngine} */
110
- #graphics;
111
- /** @type {import("../../shade/renderer/scene/Scene.js").Scene} */
112
- #scene;
113
- /** @type {import("../asset/AssetManager.js").AssetManager} */
114
- #assets;
115
- /** @type {Map<number, ParticleEmitterEntry>} */
116
- #entries = new Map();
117
- /**
118
- * The same entries by the emitter they own, so that the emitters this system did not build can
119
- * be told apart from the ones it did without a linear search per emitter per frame.
120
- * @type {Map<ParticleEmitter, ParticleEmitterEntry>}
121
- */
122
- #by_emitter = new Map();
123
- /** @type {ManagedAtlas} */
124
- #managed_atlas;
125
- /**
126
- * Bursts asked for through {@link burst} and not yet handed to the particle system. Flat pairs
127
- * of `entity, count`, drained every frame.
128
- * @type {number[]}
129
- */
130
- #bursts = [];
131
- /** @type {import("../graphics/texture/atlas/AtlasPatch.js").AtlasPatch} */
132
- #white;
133
- /** @type {import("../graphics/texture/atlas/AtlasPatch.js").AtlasPatch} */
134
- #transparent;
135
- /** @type {GPUParticleAtlasExtension|null} */
136
- #extension = null;
137
- /** @type {GPUTextureContext|null} */
138
- #texture = null;
139
- /** @type {GPUTextureView|null} */
140
- #view = null;
141
- /** @type {GPUDevice|null} */
142
- #device = null;
143
- /** @type {number} */
144
- #uploaded_version = -1;
145
- /** @type {import("../../shade/renderer/particles/GPUParticleSystem.js").GPUParticleSystem|null} */
146
- #particles = null;
147
- /** @type {GPUTextureView|null} */
148
- #previous_atlas = null;
149
- /** @type {Float32Array} */
150
- #region = new Float32Array(4);
151
-
152
- /**
153
- * @param {import("./GraphicsEngine.js").GraphicsEngine} graphics
154
- * @param {import("../../shade/renderer/scene/Scene.js").Scene} scene scene the emitters are put
155
- * in, and whose particle atlas this system owns
156
- * @param {import("../asset/AssetManager.js").AssetManager} assets
157
- */
158
- constructor(graphics, scene, assets) {
159
- super();
160
-
161
- assert.defined(graphics, "graphics");
162
- assert.equal(scene.isScene, true, "scene.isScene !== true");
163
- assert.defined(assets, "assets");
164
-
165
- this.#graphics = graphics;
166
- this.#scene = scene;
167
- this.#assets = assets;
168
- this.#managed_atlas = new ManagedAtlas({
169
- assetManager: this.#assets,
170
- atlas: this.atlas
171
- });
172
-
173
- this.#create_defaults();
174
- }
175
-
176
- /**
177
- * The scene the emitters are put in. Received rather than created: meshes and lights come from
178
- * systems of their own, and all of them have to be writing into one scene for any of it to be
179
- * visible together.
180
- *
181
- * @returns {import("../../shade/renderer/scene/Scene.js").Scene}
182
- */
183
- get scene() {
184
- return this.#scene;
185
- }
186
-
187
- /**
188
- * The emitters this system currently holds, in no particular order. Read-only — they follow
189
- * the entities.
190
- *
191
- * @returns {ParticleEmitter[]}
192
- */
193
- get emitters() {
194
- return Array.from(this.#by_emitter.keys());
195
- }
196
-
197
- /**
198
- * The emitter built for an entity.
199
- *
200
- * Handed out for what an entity reference cannot express — asking the particle system about
201
- * it — and for nothing routine: the component is how an effect is changed, and an emitter
202
- * written to behind the component's back is overwritten by the next comparison.
203
- *
204
- * @param {number} entity
205
- * @returns {ParticleEmitter|null} `null` for an entity this system has not linked, or one whose
206
- * component has no compiled effect yet
207
- */
208
- emitter_of(entity) {
209
- const entry = this.#entries.get(entity);
210
-
211
- if (entry === undefined) {
212
- return null;
213
- }
214
-
215
- return entry.emitter;
216
- }
217
-
218
- /**
219
- * Ask an entity's emitter for `count` particles on top of its rate, on the next frame.
220
- *
221
- * The one-shot half of the API, and the only thing here the CPU asks for rather than describes:
222
- * an impact, a footfall, a muzzle flash. It goes through the same GPU path as continuous
223
- * emission — the tick pass folds the burst into the emitter's accumulator, and the pool budget
224
- * and the culling apply — so it is a request rather than a guarantee, and an entity whose
225
- * emitter is not live by the time the frame starts is silently skipped.
226
- *
227
- * @param {number} entity
228
- * @param {number} count
229
- */
230
- burst(entity, count) {
231
- assert.isNonNegativeInteger(count, "count");
232
-
233
- if (count === 0) {
234
- return;
235
- }
236
-
237
- if (this.#bursts.length >= BURST_QUEUE_LIMIT * 2) {
238
- /*
239
- The queue is drained by a frame, so it only grows without bound when frames have stopped
240
- arriving — a scene nobody draws, or a device that never came up — and gameplay carries on
241
- asking. Dropping is right either way: nothing is going to spawn these, and a burst is a
242
- request about the moment it was made.
243
-
244
- The text is the key `warn_limited` counts on, so it carries no entity: one message per
245
- limit, not one per caller that happens to be unlucky.
246
- */
247
- warn_limited(
248
- `GPUParticleEmitterSystem: ${BURST_QUEUE_LIMIT} bursts are queued and no frame has `
249
- + "drained them; dropping the rest"
250
- );
251
-
252
- return;
253
- }
254
-
255
- this.#bursts.push(entity, count);
256
- }
257
-
258
- /**
259
- * @param {import("../ecs/EntityManager.js").EntityManager} entityManager
260
- * @returns {Promise<void>}
261
- */
262
- async startup(entityManager) {
263
- this.entityManager = entityManager;
264
-
265
- // the scene's rows are this dataset's entities; the first system to place one says so
266
- const dataset = entityManager.dataset;
267
- const scene = this.#scene;
268
-
269
- assert.ok(
270
- scene.dataset === null || scene.dataset === undefined || scene.dataset === dataset,
271
- 'the scene is bound to another dataset'
272
- );
273
-
274
- scene.dataset = dataset;
275
-
276
- if (!this.#assets.hasLoaderForType(GameAssetType.Image)) {
277
- await this.#assets.registerLoader(GameAssetType.Image, new ImageRGBADataLoader());
278
- }
279
-
280
- this.#extension = new GPUParticleAtlasExtension(this);
281
-
282
- this.#graphics.add_extension(this.#extension);
283
- }
284
-
285
- /** @returns {Promise<void>} */
286
- async shutdown() {
287
- if (this.#extension !== null) {
288
- this.#graphics.remove_extension(this.#extension);
289
- this.#extension = null;
290
- }
291
-
292
- for (const entity of Array.from(this.#entries.keys())) {
293
- this.#drop(entity);
294
- }
295
-
296
- this.#detach();
297
-
298
- this.#texture?.destroy();
299
- this.#texture = null;
300
- this.#view = null;
301
- this.#device = null;
302
- this.#uploaded_version = -1;
303
- this.#bursts.length = 0;
304
-
305
- this.#managed_atlas.reset();
306
- this.#create_defaults();
307
- }
308
-
309
- /**
310
- * @param {ParticleEffect} effect
311
- * @param {Transform64} transform
312
- * @param {number} entity
313
- */
314
- link(effect, transform, entity) {
315
- /** @type {ParticleEmitterEntry} */
316
- const entry = {
317
- entity,
318
- effect,
319
- transform,
320
- emitter: null,
321
- context: null,
322
- on_transform_changed: null,
323
- url: null,
324
- texture: null
325
- };
326
-
327
- this.#entries.set(entity, entry);
328
-
329
- /*
330
- The emitter is placed the moment its transform moves rather than on a poll. A Transform64
331
- has no signals of its own, so the announcement comes from the dataset: whoever wrote the
332
- transform sends TRANSFORM64_EVENT_CHANGE and this wakes on it. Coarser than watching
333
- position, rotation and scale separately — a scale write wakes a placement that reads the
334
- whole transform anyway — and that is the trade the type makes everywhere.
335
- */
336
- entry.on_transform_changed = () => this.#place(entry);
337
-
338
- this.entityManager.dataset.addEntityEventListener(
339
- entity, TRANSFORM64_EVENT_CHANGE, entry.on_transform_changed
340
- );
341
-
342
- // Everything else — building the emitter, acquiring the texture — is what a frame does
343
- // anyway, and doing it here as well would be a second copy of it that has to stay in step.
344
- this.#sync(entry);
345
-
346
- // Registered now when there is a context to register with, so an entity spawned mid-level
347
- // stands where it should from its first frame.
348
- this.#register_all();
349
- }
350
-
351
- /**
352
- * @param {ParticleEffect} effect
353
- * @param {Transform64} transform
354
- * @param {number} entity
355
- */
356
- unlink(effect, transform, entity) {
357
- this.#drop(entity);
358
- }
359
-
360
- /**
361
- * Bring every emitter in line with its entity: build emitters for effects that have arrived,
362
- * retire emitters for effects that have gone, copy changed fields onto the ones that stay, and
363
- * follow the texture URLs.
364
- *
365
- * @param {number} [time_delta_seconds]
366
- * @returns {void}
367
- */
368
- update(time_delta_seconds) {
369
- for (const entry of this.#entries.values()) {
370
- this.#sync(entry);
371
- }
372
-
373
- /*
374
- Registered here as well as at FrameStart, so an emitter whose effect arrived during a
375
- simulation tick has its row written by the next frame's build rather than the one after:
376
- a frame builds the scene context before FrameStart runs, so a row acquired at FrameStart
377
- is not written until the frame after.
378
- */
379
- this.#register_all();
380
-
381
- this.#managed_atlas.update();
382
- }
383
-
384
- /**
385
- * The particle system a scene context's frames run, or null while there is no renderer.
386
- *
387
- * @param {import("../../shade/renderer/scene/GPUSceneContext.js").GPUSceneContext} context
388
- * @returns {import("../../shade/renderer/particles/GPUParticleSystem.js").GPUParticleSystem|null}
389
- */
390
- #particles_of(context) {
391
- const renderer = this.#graphics.renderer;
392
-
393
- if (renderer === null || renderer === undefined) {
394
- return null;
395
- }
396
-
397
- return renderer.particles(context);
398
- }
399
-
400
- /**
401
- * {@link #register} against whatever context and particle system the scene has right now, if
402
- * it has any.
403
- */
404
- #register_all() {
405
- const context = this.#graphics.scene_context(this.#scene);
406
-
407
- if (context === null) {
408
- return;
409
- }
410
-
411
- const particles = this.#particles_of(context);
412
-
413
- if (particles !== null) {
414
- this.#register(particles, context);
415
- }
416
- }
417
-
418
- /**
419
- * Publish the atlas at FrameStart — after the scene context has established transform rows, and
420
- * before the renderer records the particle passes that read the emitter table.
421
- *
422
- * {@link update} is called from here as well as by the entity manager, because a frame must not
423
- * depend on a simulation tick having preceded it: an editor draws on demand, and a paused game
424
- * still renders. It is idempotent and costs a comparison per emitter when nothing has changed,
425
- * which is what makes running it twice a frame the cheap way to be right rather than a saving
426
- * worth chasing.
427
- *
428
- * @param {import("../../shade/renderer/extension/FrameContext.js").FrameContext} frame
429
- * @returns {void}
430
- */
431
- prepare(frame) {
432
- if (frame.view.scene.scene !== this.#scene) {
433
- return;
434
- }
435
-
436
- this.update();
437
-
438
- const renderer = this.#graphics.renderer;
439
-
440
- /*
441
- The renderer's GPU particle feature is this system's to turn on, and it is turned on every
442
- frame rather than once, because a device restart builds a renderer that has never heard of
443
- it. The consequence is worth stating: while this system is in the profile there is no way to
444
- switch GPU particles off through the renderer, because this would switch them back on at the
445
- next FrameStart. Switching them off means taking the system out — or, for one effect,
446
- `ParticleEffect#emitting`.
447
- */
448
- renderer.feature_particles_enabled = true;
449
-
450
- const particles = renderer.particles(frame.view.scene);
451
-
452
- if (particles !== this.#particles) {
453
- this.#detach();
454
- this.#particles = particles;
455
- this.#previous_atlas = particles.atlas;
456
- }
457
-
458
- this.#register(particles, frame.view.scene);
459
-
460
- this.#upload(renderer.graphics);
461
-
462
- particles.atlas = this.#view;
463
-
464
- this.#publish_regions(particles);
465
- this.#drain_bursts(particles);
466
- }
467
-
468
- /**
469
- * Register every emitter not yet registered with this context, at its entity's row.
470
- *
471
- * From {@link update} and {@link prepare} rather than at link, because a row needs a scene
472
- * context and the context needs a device, and neither is a system's to wait for: a level loads
473
- * before the device is up, and a device restart replaces the context under every entry. An
474
- * entry registered with a context that is no longer this one is registered again with the one
475
- * that is. Idempotent, and a comparison per entry when nothing has changed.
476
- *
477
- * @param {import("../../shade/renderer/particles/GPUParticleSystem.js").GPUParticleSystem} particles
478
- * @param {import("../../shade/renderer/scene/GPUSceneContext.js").GPUSceneContext} scene_context
479
- */
480
- #register(particles, scene_context) {
481
- const rows = scene_context.rows;
482
-
483
- for (const entry of this.#entries.values()) {
484
- if (entry.emitter === null || entry.context === scene_context) {
485
- continue;
486
- }
487
-
488
- rows.acquire(entry.entity);
489
-
490
- particles.add_emitter(entry.emitter, row_of_entity(entry.entity));
491
-
492
- entry.context = scene_context;
493
- }
494
- }
495
-
496
- /**
497
- * Point every emitter of this scene at its patch in the atlas.
498
- *
499
- * Every region, every frame: growth also moves the UVs of images that did not change, and there
500
- * is no signal for that which an emitter could subscribe to.
501
- *
502
- * @param {import("../../shade/renderer/particles/GPUParticleSystem.js").GPUParticleSystem} particles
503
- */
504
- #publish_regions(particles) {
505
- const registry = particles.registry;
506
- const emitters = registry.emitters;
507
- const region = this.#region;
508
-
509
- for (let i = 0; i < emitters.length; i++) {
510
- const emitter = emitters[i];
511
- const entry = this.#by_emitter.get(emitter);
512
-
513
- // Not ours: it has nothing in this atlas, so it gets the one patch that is true for
514
- // everybody — a white texel, which draws it as a flat quad of its own colour.
515
- const patch = entry === undefined || entry.texture === null
516
- ? this.#white
517
- : entry.texture.getValue() ?? this.#transparent;
518
-
519
- const uv = patch.uv;
520
-
521
- if (patch === this.#white || patch === this.#transparent) {
522
- // Sample the texel centre everywhere, avoiding interpolation into the gutter.
523
- region[0] = uv.position.x + uv.size.x * 0.5;
524
- region[1] = uv.position.y + uv.size.y * 0.5;
525
- region[2] = 0;
526
- region[3] = 0;
527
- } else {
528
- region[0] = uv.position.x;
529
- region[1] = uv.position.y;
530
- region[2] = uv.size.x;
531
- region[3] = uv.size.y;
532
- }
533
-
534
- registry.set_atlas_region(emitter, region);
535
- }
536
- }
537
-
538
- /**
539
- * Hand this frame's bursts to the particle system, now that membership has been swept and every
540
- * emitter that can have a row has one.
541
- *
542
- * The queue is emptied whether or not each burst could be delivered. A burst is a request about
543
- * *this* moment — a footfall, an impact — and holding one for an emitter that is not live yet
544
- * would deliver it seconds late, at whatever unrelated moment the emitter finally arrived.
545
- *
546
- * @param {import("../../shade/renderer/particles/GPUParticleSystem.js").GPUParticleSystem} particles
547
- */
548
- #drain_bursts(particles) {
549
- const bursts = this.#bursts;
550
-
551
- for (let i = 0; i < bursts.length; i += 2) {
552
- const entry = this.#entries.get(bursts[i]);
553
-
554
- if (entry === undefined || entry.emitter === null) {
555
- continue;
556
- }
557
-
558
- if (particles.registry.context(entry.emitter) === undefined) {
559
- continue;
560
- }
561
-
562
- particles.spawn(entry.emitter, bursts[i + 1]);
563
- }
564
-
565
- bursts.length = 0;
566
- }
567
-
568
- /**
569
- * One entity's emitter, brought in line with its component.
570
- *
571
- * @param {ParticleEmitterEntry} entry
572
- */
573
- #sync(entry) {
574
- const effect = entry.effect;
575
-
576
- /*
577
- Ahead of everything the effect gates, and deliberately: an image takes a fetch and a decode
578
- to arrive, and an entity that names one has said what it wants to draw with whether or not
579
- its program has finished compiling. Waiting would mean a first frame of transparent
580
- particles for every effect that arrives with its texture.
581
- */
582
- if (entry.url !== effect.texture) {
583
- entry.texture?.release();
584
-
585
- entry.url = effect.texture;
586
- entry.texture = entry.url === null ? null : this.#managed_atlas.acquire(entry.url);
587
- }
588
-
589
- if (effect.layout === null || effect.program === null) {
590
- // No effect (yet, or any more). An entity whose effect is still compiling is ordinary,
591
- // and so is one whose effect was cleared — both mean "emits nothing", and the emitter
592
- // is retired rather than keeping the last program it happened to have.
593
- this.#detach_emitter(entry);
594
-
595
- return;
596
- }
597
-
598
- if (entry.emitter === null) {
599
- const emitter = new ParticleEmitter();
600
-
601
- /*
602
- The emitter takes the entity's name when it has one, exactly as `ShadedGeometrySystem`
603
- does for a primitive. Nothing draws differently for it: what it buys is that a walk over
604
- the registry is readable in a debugger.
605
- */
606
- const name = this.entityManager.dataset.getComponent(entry.entity, Name);
607
-
608
- emitter.name = name === undefined || name === null
609
- ? `particles/entity ${entry.entity}`
610
- : name.getValue();
611
-
612
- entry.emitter = emitter;
613
-
614
- apply_particle_effect(emitter, effect);
615
-
616
- // registered at the entity's row by the next frame — see #register
617
- this.#by_emitter.set(emitter, entry);
618
- } else if (apply_particle_effect(entry.emitter, effect)) {
619
- // The emitter's own change counter, which is what EmitterRegistry restages a row from.
620
- entry.emitter.needsUpdate = true;
621
- }
622
- }
623
-
624
- /**
625
- * The entity moved: its row is rewritten from its transform by the context that holds it.
626
- *
627
- * @param {ParticleEmitterEntry} entry
628
- */
629
- #place(entry) {
630
- const context = entry.context;
631
-
632
- if (context === null || context !== this.#graphics.scene_context(this.#scene)) {
633
- return;
634
- }
635
-
636
- context.rows.mark_world(entry.entity);
637
- }
638
-
639
- /**
640
- * Retire an entity's emitter and let go of its row, leaving the entry linked.
641
- *
642
- * @param {ParticleEmitterEntry} entry
643
- */
644
- #detach_emitter(entry) {
645
- const emitter = entry.emitter;
646
-
647
- if (emitter === null) {
648
- return;
649
- }
650
-
651
- const context = entry.context;
652
-
653
- // A context that has been replaced took its rows and its particle system with it; only the
654
- // one that is running is handed anything back.
655
- if (context !== null && context === this.#graphics.scene_context(this.#scene)) {
656
- const particles = this.#particles_of(context);
657
-
658
- if (particles !== null && particles.registry.context(emitter) !== undefined) {
659
- particles.remove_emitter(emitter);
660
- }
661
-
662
- context.rows.release(entry.entity);
663
- }
664
-
665
- entry.context = null;
666
-
667
- this.#by_emitter.delete(emitter);
668
-
669
- entry.emitter = null;
670
- }
671
-
672
- /**
673
- * @param {number} entity
674
- */
675
- #drop(entity) {
676
- const entry = this.#entries.get(entity);
677
-
678
- if (entry === undefined) {
679
- return;
680
- }
681
-
682
- if (entry.on_transform_changed !== null) {
683
- this.entityManager.dataset.removeEntityEventListener(
684
- entity, TRANSFORM64_EVENT_CHANGE, entry.on_transform_changed
685
- );
686
-
687
- entry.on_transform_changed = null;
688
- }
689
-
690
- this.#detach_emitter(entry);
691
- entry.texture?.release();
692
-
693
- entry.url = null;
694
- entry.texture = null;
695
-
696
- this.#entries.delete(entity);
697
- }
698
-
699
- /** @returns {void} */
700
- #create_defaults() {
701
- const white = Sampler2D.uint8(4, 1, 1);
702
-
703
- white.data.fill(255);
704
-
705
- this.#white = this.atlas.add(white);
706
- this.#transparent = this.atlas.add(Sampler2D.uint8(4, 1, 1));
707
- }
708
-
709
- /**
710
- * @param {import("../../shade/renderer/GPUDeviceContext.js").GPUDeviceContext} graphics
711
- * @returns {void}
712
- */
713
- #upload(graphics) {
714
- const device = graphics.device;
715
- const source = this.atlas.sampler;
716
-
717
- if (this.#device !== device || this.#texture === null) {
718
- this.#texture?.destroy();
719
-
720
- this.#device = device;
721
- this.#texture = new GPUTextureContext(device);
722
-
723
- const descriptor = this.#texture.descriptor;
724
-
725
- descriptor.label = "particles/sprite atlas";
726
- descriptor.format = "rgba8unorm";
727
- descriptor.usage = GPUTextureUsage.TEXTURE_BINDING
728
- | GPUTextureUsage.COPY_DST
729
- | GPUTextureUsage.RENDER_ATTACHMENT;
730
-
731
- this.#uploaded_version = -1;
732
- }
733
-
734
- const texture = this.#texture;
735
-
736
- if (texture.width !== source.width || texture.height !== source.height) {
737
- texture.descriptor.mipLevelCount = 1 + Math.floor(Math.log2(Math.max(source.width, source.height)));
738
- texture.resize(source.width, source.height);
739
-
740
- this.#uploaded_version = -1;
741
- }
742
-
743
- if (this.#uploaded_version !== source.version) {
744
- device.queue.writeTexture({ texture: texture.gpu_texture }, source.data,
745
- { bytesPerRow: source.width * 4 }, [source.width, source.height]);
746
-
747
- if (texture.descriptor.mipLevelCount > 1) {
748
- graphics.textures.mipmaps.generateMipmap(
749
- texture.gpu_texture,
750
- texture.descriptor,
751
- TextureFilterType.Linear
752
- );
753
- }
754
-
755
- texture.incrementVersion();
756
-
757
- this.#uploaded_version = source.version;
758
- }
759
-
760
- this.#view = texture.obtainView();
761
- }
762
-
763
- /** @returns {void} */
764
- #detach() {
765
- if (this.#particles !== null && this.#particles.atlas === this.#view) {
766
- this.#particles.atlas = this.#previous_atlas;
767
-
768
- for (const emitter of this.#particles.emitters) {
769
- this.#particles.registry.set_atlas_region(emitter, [0, 0, 1, 1]);
770
- }
771
- }
772
-
773
- this.#particles = null;
774
- this.#previous_atlas = null;
775
- }
776
- }
777
-
778
- /**
779
- * Copy a component onto the emitter built for it, reporting whether anything actually moved.
780
- *
781
- * Comparing while assigning rather than in a pass of its own is what makes change detection cost
782
- * nothing extra: the fields have to be read to be written, and the only addition is an `||` per
783
- * field. What it answers is whether the emitter's GPU row has to be restaged, and restaging one that
784
- * has not changed is an upload per emitter per frame.
785
- *
786
- * `spawn_rate` comes from {@link ParticleEffect#effective_spawn_rate}, so an effect switched off
787
- * with `emitting = false` reaches the GPU as a rate of zero and keeps its authored rate to come back
788
- * to. Layout and program are shared by reference, never copied — two entities of one effect belong
789
- * on one program id in the heap.
790
- *
791
- * Every field is compared by name, and that is load-bearing rather than style. Through a shared
792
- * `assign(target, field, value)` the one `target[field]` access saw all thirteen names, went
793
- * megamorphic, and read numbers back boxed, while the call boxed the value it was handed: 228 B and
794
- * ~120 ns per emitter per call, against nothing and ~9 ns written out like this
795
- * (`GPUParticleEmitterSystem.allocation.spec.js`). This runs for every live emitter twice a frame.
796
- *
797
- * @param {ParticleEmitter} emitter
798
- * @param {ParticleEffect} effect
799
- * @returns {boolean} whether the emitter differs from what it held before
800
- */
801
- export function apply_particle_effect(emitter, effect) {
802
- let changed = false;
803
-
804
- if (emitter.layout !== effect.layout) {
805
- emitter.layout = effect.layout;
806
- changed = true;
807
- }
808
-
809
- if (emitter.program !== effect.program) {
810
- emitter.program = effect.program;
811
- changed = true;
812
- }
813
-
814
- if (emitter.texture !== effect.texture) {
815
- emitter.texture = effect.texture;
816
- changed = true;
817
- }
818
-
819
- if (emitter.seed !== effect.seed) {
820
- emitter.seed = effect.seed;
821
- changed = true;
822
- }
823
-
824
- const spawn_rate = effect.effective_spawn_rate;
825
-
826
- if (emitter.spawn_rate !== spawn_rate) {
827
- emitter.spawn_rate = spawn_rate;
828
- changed = true;
829
- }
830
-
831
- if (emitter.prewarm !== effect.prewarm) {
832
- emitter.prewarm = effect.prewarm;
833
- changed = true;
834
- }
835
-
836
- if (emitter.flags !== effect.flags) {
837
- emitter.flags = effect.flags;
838
- changed = true;
839
- }
840
-
841
- if (emitter.render_position !== effect.render_position) {
842
- emitter.render_position = effect.render_position;
843
- changed = true;
844
- }
845
-
846
- if (emitter.render_size !== effect.render_size) {
847
- emitter.render_size = effect.render_size;
848
- changed = true;
849
- }
850
-
851
- if (emitter.render_color !== effect.render_color) {
852
- emitter.render_color = effect.render_color;
853
- changed = true;
854
- }
855
-
856
- if (emitter.render_orientation !== effect.render_orientation) {
857
- emitter.render_orientation = effect.render_orientation;
858
- changed = true;
859
- }
860
-
861
- if (emitter.render_frame !== effect.render_frame) {
862
- emitter.render_frame = effect.render_frame;
863
- changed = true;
864
- }
865
-
866
- if (emitter.render_velocity !== effect.render_velocity) {
867
- emitter.render_velocity = effect.render_velocity;
868
- changed = true;
869
- }
870
-
871
- const flipbook = emitter.flipbook;
872
-
873
- if (flipbook[0] !== effect.flipbook[0] || flipbook[1] !== effect.flipbook[1]) {
874
- flipbook.set(effect.flipbook);
875
-
876
- changed = true;
877
- }
878
-
879
- return changed;
880
- }
881
-
882
- class GPUParticleAtlasExtension extends RenderExtension {
883
- /** @type {string} */
884
- name = "gpu particle atlas";
885
- /** @type {FramePhase} */
886
- phase = FramePhase.FrameStart;
887
- /** @type {GPUParticleEmitterSystem} */
888
- #system;
889
-
890
- /** @param {GPUParticleEmitterSystem} system */
891
- constructor(system) {
892
- super();
893
-
894
- this.#system = system;
895
- }
896
-
897
- /**
898
- * @param {import("../../shade/renderer/extension/FrameContext.js").FrameContext} frame
899
- * @returns {void}
900
- */
901
- record(frame) {
902
- this.#system.prepare(frame);
903
- }
904
- }
1
+ import { assert } from "../../core/assert.js";
2
+ import { warn_limited } from "../../shade/util/warn_limited.js";
3
+ import { FramePhase } from "../../shade/renderer/extension/FramePhase.js";
4
+ import { RenderExtension } from "../../shade/renderer/extension/RenderExtension.js";
5
+ import { ParticleEmitter } from "../../shade/renderer/particles/runtime/ParticleEmitter.js";
6
+ import { GPUTextureContext } from "../../shade/renderer/texture/GPUTextureContext.js";
7
+ import { System } from "../ecs/System.js";
8
+ import Name from "../ecs/name/Name.js";
9
+ import { Transform64 } from "../ecs/transform/Transform64.js";
10
+ import { EventType } from "../ecs/EventType.js";
11
+ import { TRANSFORM64_EVENT_CHANGE } from "../ecs/transform/TRANSFORM64_EVENT_CHANGE.js";
12
+ import { row_of_entity } from "../../shade/renderer/scene/rows/GPUSceneRows.js";
13
+ import { GameAssetType } from "../asset/GameAssetType.js";
14
+ import { ImageRGBADataLoader } from "../asset/loaders/image/ImageRGBADataLoader.js";
15
+ import { ParticleEffect } from "../graphics/ecs/particles/ParticleEffect.js";
16
+ import { ManagedAtlas } from "../graphics/texture/atlas/ManagedTextureAtlas.js";
17
+ import { SpriteAtlas } from "../graphics/texture/atlas/SpriteAtlas.js";
18
+ import { SpriteAtlasFill } from "../graphics/texture/atlas/SpriteAtlasFill.js";
19
+ import { TextureFilterType } from "../../shade/renderer/texture/TextureFilterType.js";
20
+ import { Sampler2D } from "../graphics/texture/sampler/Sampler2D.js";
21
+
22
+ /**
23
+ * Bursts {@link GPUParticleEmitterSystem#burst} will hold before it starts dropping them. Reached
24
+ * only when frames have stopped arriving — see the call site.
25
+ *
26
+ * @type {number}
27
+ */
28
+ const BURST_QUEUE_LIMIT = 256;
29
+
30
+ /**
31
+ * @typedef {object} ParticleEmitterEntry
32
+ * @property {number} entity
33
+ * @property {ParticleEffect} effect the entity's component — what to emit
34
+ * @property {Transform64} transform the entity's transform — where to emit it
35
+ * @property {ParticleEmitter|null} emitter the emitter, once the effect has a program to run
36
+ * @property {import("../../shade/renderer/scene/GPUSceneContext.js").GPUSceneContext|null} context
37
+ * the scene context the emitter is registered with, at the entity's row; null until a frame has
38
+ * registered it, or after a device restart took that context away
39
+ * @property {function():void} on_transform_changed
40
+ * @property {string|null} url last observed {@link ParticleEffect#texture}
41
+ * @property {import("../graphics/texture/atlas/ManagedTextureAtlas.js").ManagedAtlasReference|null} texture shared image reference, or null for an untextured effect
42
+ * @property {Map<string, {entity: number, channel: number, live: boolean, on_removed: function|null, layout: import("../../shade/renderer/particles/layout/ParticleLayout.js").ParticleLayout|null}>} bound
43
+ * the transform bindings asked of the emitter, by field: `live` ones hold their entity's row in
44
+ * {@link context}; the others were refused or lost their entity, and wait for the request or the
45
+ * registry's {@link ../../shade/renderer/particles/runtime/EmitterRegistry.js EmitterRegistry#binding_layout}
46
+ * (`layout`, the one they were made under) to change
47
+ * @property {import("../../shade/renderer/particles/isa/ParticleProgram.js").ParticleProgram|null} refused_program
48
+ * a program the particle system refused, not offered again until the effect's program changes
49
+ */
50
+
51
+ /**
52
+ * Draws the game's GPU particle effects: one emitter per entity carrying
53
+ * {@link ../graphics/ecs/particles/ParticleEffect.js ParticleEffect} and
54
+ * {@link ../ecs/transform/Transform64.js Transform64}.
55
+ *
56
+ * ```js
57
+ * entityManager.addSystem(new GPUParticleEmitterSystem(graphics, scene, assetManager));
58
+ *
59
+ * new Entity()
60
+ * .add(new Transform64())
61
+ * .add(ParticleEffect.from({ ...create_particle_effect({ layout, init, update }), spawn_rate: 400 }))
62
+ * .build(dataset);
63
+ * ```
64
+ *
65
+ * **The entity is the emitter.** The component says what it emits and the transform says where, the
66
+ * same division every other placed thing in this engine uses — a `Decal`, an `SGMesh`, a `Light`.
67
+ * Which means an emitter is spawned, moved, parented, attached to a bone, saved and destroyed by
68
+ * exactly the machinery that does those things for everything else, and a particle effect stops
69
+ * being a special case that needs its own scene-graph call.
70
+ *
71
+ * ## What this system does with the pair
72
+ *
73
+ * It builds a {@link ParticleEmitter} per entity and owns it: the emitter is registered with the
74
+ * scene's particle system at the entity's row when the component has a compiled effect, so it
75
+ * stands where the entity stands, carries whatever the component currently says, and is retired
76
+ * when the entity goes. The emitter is not hidden — {@link emitter_of} hands it out — but it is
77
+ * not the interface either: write to the component and the next frame carries it.
78
+ *
79
+ * Changes are found by **comparison** rather than announced. Once a frame each entity's
80
+ * fields are compared against what was last written to its emitter, and the emitter is marked dirty only
81
+ * where they differ; `EmitterRegistry` then restages exactly the rows that moved. A component nobody
82
+ * touched costs a run of integer comparisons and no upload, so there is no change signal to fire and
83
+ * none to forget. What that buys is that gameplay can write `effect.spawn_rate = 0` anywhere,
84
+ * including from code that has never heard of this system.
85
+ *
86
+ * ## It owns the scene's sprite atlas
87
+ *
88
+ * Every {@link ParticleEffect#texture} is loaded through the supplied `AssetManager` and packed into
89
+ * one {@link SpriteAtlas} shared by the scene, one patch per distinct URL however many entities
90
+ * name it, reference-counted through {@link ManagedAtlas} so unreferenced sprites remain in the
91
+ * atlas graveyard and are only reaped when incoming sprites need the space. A `FrameStart`
92
+ * extension publishes the pixels and every current patch region before the renderer records the
93
+ * particle passes — every repack moves UVs that nothing else changed, so the regions are rewritten
94
+ * whole rather than when a URL changes. Pending and failed images sample a transparent texel;
95
+ * effects with no image sample a white one.
96
+ *
97
+ * An emitter in this scene that this system did not build — one registered with the particle
98
+ * system directly — gets the white texel. The atlas is the scene's and that emitter has nothing in
99
+ * it, so the alternative is not "its own texture" but a UV rectangle from somebody else's image.
100
+ *
101
+ * ## What it is not
102
+ *
103
+ * It does not simulate anything and does not record a particle pass. `GPUParticleSystem` does both,
104
+ * inside the renderer, driven from the emitter table these rows are in — see
105
+ * `shade/renderer/particles/DESIGN.md`. This system's whole contribution is membership, placement,
106
+ * the component's fields, the global fields, the transform bindings, and the sprite sheet.
107
+ */
108
+ export class GPUParticleEmitterSystem extends System {
109
+ dependencies = [ParticleEffect, Transform64];
110
+
111
+ /**
112
+ * CPU atlas, exposed for inspection. Owned by this system.
113
+ * @type {SpriteAtlas}
114
+ */
115
+ atlas = new SpriteAtlas(SpriteAtlasFill.EdgeExtend, 64);
116
+
117
+ /** @type {import("./GraphicsEngine.js").GraphicsEngine} */
118
+ #graphics;
119
+ /** @type {import("../../shade/renderer/scene/Scene.js").Scene} */
120
+ #scene;
121
+ /** @type {import("../asset/AssetManager.js").AssetManager} */
122
+ #assets;
123
+ /** @type {Map<number, ParticleEmitterEntry>} */
124
+ #entries = new Map();
125
+ /**
126
+ * The same entries by the emitter they own, so that the emitters this system did not build can
127
+ * be told apart from the ones it did without a linear search per emitter per frame.
128
+ * @type {Map<ParticleEmitter, ParticleEmitterEntry>}
129
+ */
130
+ #by_emitter = new Map();
131
+ /** @type {ManagedAtlas} */
132
+ #managed_atlas;
133
+ /**
134
+ * Bursts asked for through {@link burst} and not yet handed to the particle system. Flat pairs
135
+ * of `entity, count`, drained every frame.
136
+ * @type {number[]}
137
+ */
138
+ #bursts = [];
139
+ /**
140
+ * Global field values gameplay has set, by name, held here so they reach a particle system that
141
+ * does not exist yet and a replacement one after a device restart.
142
+ * @type {Map<string, Float32Array>}
143
+ */
144
+ #global_fields = new Map();
145
+ /**
146
+ * Names in {@link #global_fields} not yet handed to the particle system.
147
+ * @type {Set<string>}
148
+ */
149
+ #global_fields_dirty = new Set();
150
+ /**
151
+ * Global field transform bindings gameplay asked for, by name.
152
+ * @type {Map<string, {entity: number, channel: number}>}
153
+ */
154
+ #global_bindings = new Map();
155
+ /**
156
+ * The global bindings applied to {@link #particles}, by name: `live` ones hold their entity's row
157
+ * in {@link #global_bindings_context}; the others were refused or lost their entity, and wait for
158
+ * the request to change.
159
+ * @type {Map<string, {entity: number, channel: number, live: boolean, on_removed: function|null}>}
160
+ */
161
+ #global_bound = new Map();
162
+ /**
163
+ * The scene context {@link #global_bound}'s rows are held in.
164
+ * @type {import("../../shade/renderer/scene/GPUSceneContext.js").GPUSceneContext|null}
165
+ */
166
+ #global_bindings_context = null;
167
+ /** @type {import("../graphics/texture/atlas/AtlasPatch.js").AtlasPatch} */
168
+ #white;
169
+ /** @type {import("../graphics/texture/atlas/AtlasPatch.js").AtlasPatch} */
170
+ #transparent;
171
+ /** @type {GPUParticleAtlasExtension|null} */
172
+ #extension = null;
173
+ /** @type {GPUTextureContext|null} */
174
+ #texture = null;
175
+ /** @type {GPUTextureView|null} */
176
+ #view = null;
177
+ /** @type {GPUDevice|null} */
178
+ #device = null;
179
+ /** @type {number} */
180
+ #uploaded_version = -1;
181
+ /** @type {import("../../shade/renderer/particles/GPUParticleSystem.js").GPUParticleSystem|null} */
182
+ #particles = null;
183
+ /** @type {GPUTextureView|null} */
184
+ #previous_atlas = null;
185
+ /** @type {Float32Array} */
186
+ #region = new Float32Array(4);
187
+
188
+ /**
189
+ * @param {import("./GraphicsEngine.js").GraphicsEngine} graphics
190
+ * @param {import("../../shade/renderer/scene/Scene.js").Scene} scene scene the emitters are put
191
+ * in, and whose particle atlas this system owns
192
+ * @param {import("../asset/AssetManager.js").AssetManager} assets
193
+ */
194
+ constructor(graphics, scene, assets) {
195
+ super();
196
+
197
+ assert.defined(graphics, "graphics");
198
+ assert.equal(scene.isScene, true, "scene.isScene !== true");
199
+ assert.defined(assets, "assets");
200
+
201
+ this.#graphics = graphics;
202
+ this.#scene = scene;
203
+ this.#assets = assets;
204
+ this.#managed_atlas = new ManagedAtlas({
205
+ assetManager: this.#assets,
206
+ atlas: this.atlas
207
+ });
208
+
209
+ this.#create_defaults();
210
+ }
211
+
212
+ /**
213
+ * The scene the emitters are put in. Received rather than created: meshes and lights come from
214
+ * systems of their own, and all of them have to be writing into one scene for any of it to be
215
+ * visible together.
216
+ *
217
+ * @returns {import("../../shade/renderer/scene/Scene.js").Scene}
218
+ */
219
+ get scene() {
220
+ return this.#scene;
221
+ }
222
+
223
+ /**
224
+ * The emitters this system currently holds, in no particular order. Read-only — they follow
225
+ * the entities.
226
+ *
227
+ * @returns {ParticleEmitter[]}
228
+ */
229
+ get emitters() {
230
+ return Array.from(this.#by_emitter.keys());
231
+ }
232
+
233
+ /**
234
+ * The emitter built for an entity.
235
+ *
236
+ * Handed out for what an entity reference cannot express — asking the particle system about
237
+ * it — and for nothing routine: the component is how an effect is changed, and an emitter
238
+ * written to behind the component's back is overwritten by the next comparison.
239
+ *
240
+ * @param {number} entity
241
+ * @returns {ParticleEmitter|null} `null` for an entity this system has not linked, or one whose
242
+ * component has no compiled effect yet
243
+ */
244
+ emitter_of(entity) {
245
+ const entry = this.#entries.get(entity);
246
+
247
+ if (entry === undefined) {
248
+ return null;
249
+ }
250
+
251
+ return entry.emitter;
252
+ }
253
+
254
+ /**
255
+ * Ask an entity's emitter for `count` particles on top of its rate, on the next frame.
256
+ *
257
+ * The one-shot half of the API, and the only thing here the CPU asks for rather than describes:
258
+ * an impact, a footfall, a muzzle flash. It goes through the same GPU path as continuous
259
+ * emission — the tick pass folds the burst into the emitter's accumulator, and the pool budget
260
+ * and the culling apply — so it is a request rather than a guarantee, and an entity whose
261
+ * emitter is not live by the time the frame starts is silently skipped.
262
+ *
263
+ * @param {number} entity
264
+ * @param {number} count
265
+ */
266
+ burst(entity, count) {
267
+ assert.isNonNegativeInteger(count, "count");
268
+
269
+ if (count === 0) {
270
+ return;
271
+ }
272
+
273
+ if (this.#bursts.length >= BURST_QUEUE_LIMIT * 2) {
274
+ /*
275
+ The queue is drained by a frame, so it only grows without bound when frames have stopped
276
+ arriving — a scene nobody draws, or a device that never came up — and gameplay carries on
277
+ asking. Dropping is right either way: nothing is going to spawn these, and a burst is a
278
+ request about the moment it was made.
279
+
280
+ The text is the key `warn_limited` counts on, so it carries no entity: one message per
281
+ limit, not one per caller that happens to be unlucky.
282
+ */
283
+ warn_limited(
284
+ `GPUParticleEmitterSystem: ${BURST_QUEUE_LIMIT} bursts are queued and no frame has `
285
+ + "drained them; dropping the rest"
286
+ );
287
+
288
+ return;
289
+ }
290
+
291
+ this.#bursts.push(entity, count);
292
+ }
293
+
294
+ /**
295
+ * Set one of the scene's global fields — a member every effect may read by name, such as a gust
296
+ * strength — declaring it at this width if nothing has yet. The value is copied and reaches the
297
+ * particle system on the next frame, and again after a device restart; setting the same value
298
+ * again costs a comparison.
299
+ *
300
+ * Fewer values than the field is declared with set its leading components. More are refused
301
+ * with a warning on that frame. While the field is bound ({@link bind_global_field}) the binding
302
+ * overwrites it every frame, and this value is what it reads once unbound.
303
+ *
304
+ * @param {string} name
305
+ * @param {ArrayLike<number>} values one per component, 1..4
306
+ */
307
+ set_global_field(name, values) {
308
+ assert.isString(name, "name");
309
+ assert.greaterThanOrEqual(values.length, 1, "values.length");
310
+ assert.lessThanOrEqual(values.length, 4, "values.length");
311
+
312
+ let stored = this.#global_fields.get(name);
313
+
314
+ if (stored === undefined || stored.length !== values.length) {
315
+ stored = new Float32Array(values.length);
316
+ this.#global_fields.set(name, stored);
317
+ } else {
318
+ let same = true;
319
+
320
+ for (let i = 0; i < values.length; i++) {
321
+ if (!Object.is(stored[i], Math.fround(values[i]))) {
322
+ same = false;
323
+ break;
324
+ }
325
+ }
326
+
327
+ if (same) {
328
+ return;
329
+ }
330
+ }
331
+
332
+ stored.set(values);
333
+
334
+ this.#global_fields_dirty.add(name);
335
+ }
336
+
337
+ /**
338
+ * Write an entity's composed transform into a global field every frame, on the GPU — the
339
+ * player's position, a light's direction. Applied on the next frame, declaring the field at the
340
+ * channel's width if nothing has yet. Binding a bound name replaces its binding.
341
+ *
342
+ * Refused with a warning when the entity has no Transform64 or the field is declared at another
343
+ * width than the channel writes; a refused binding is not retried until the request changes.
344
+ * The binding holds until {@link unbind_global_field}, the entity's removal, or
345
+ * {@link shutdown}. It is runtime state and is not saved.
346
+ *
347
+ * @param {string} name
348
+ * @param {number} entity an entity with a Transform64
349
+ * @param {number} channel a {@link ../../shade/renderer/particles/data/PARTICLE_FIELD_BINDING.js PARTICLE_FIELD_BINDING_CHANNEL}
350
+ */
351
+ bind_global_field(name, entity, channel) {
352
+ assert.isString(name, "name");
353
+ assert.isNonNegativeInteger(entity, "entity");
354
+
355
+ this.#global_bindings.set(name, { entity, channel });
356
+ }
357
+
358
+ /**
359
+ * Stop writing a transform into a global field, from the next frame. It reads the value last
360
+ * set with {@link set_global_field} again, or zero.
361
+ *
362
+ * @param {string} name
363
+ */
364
+ unbind_global_field(name) {
365
+ this.#global_bindings.delete(name);
366
+ }
367
+
368
+ /**
369
+ * Bring the emitters' and the global fields' transform bindings in line with what was asked for,
370
+ * holding each bound entity's row for as long as it is bound.
371
+ *
372
+ * What was asked for and what holds are compared field by field: a binding is let go when it is
373
+ * no longer asked for, or when the registry no longer has it (an emitter layout that lost the
374
+ * field dropped it there), and asked for again on the same pass. A request that was refused —
375
+ * an entity that is gone or has no transform, a field the effect does not have at that width —
376
+ * is remembered, unheld, and not tried again until the request changes or, for an emitter
377
+ * field, the layout the registry binds it against does. Where one field is asked for twice, the first request is the
378
+ * one that holds.
379
+ *
380
+ * @param {import("../../shade/renderer/particles/GPUParticleSystem.js").GPUParticleSystem} particles
381
+ * @param {import("../../shade/renderer/scene/GPUSceneContext.js").GPUSceneContext} scene_context
382
+ */
383
+ #bind(particles, scene_context) {
384
+ const registry = particles.registry;
385
+
386
+ for (const entry of this.#entries.values()) {
387
+ if (entry.emitter === null || entry.context !== scene_context) {
388
+ continue;
389
+ }
390
+
391
+ const emitter = entry.emitter;
392
+ const wanted = entry.effect.bindings;
393
+ const bound = entry.bound;
394
+ const held = registry.context(emitter).bindings;
395
+ const layout = registry.binding_layout(emitter);
396
+
397
+ if (bound.size > 0) {
398
+ for (const field of bound.keys()) {
399
+ const current = bound.get(field);
400
+
401
+ // A live binding holds while the registry still has it; a refused one stays
402
+ // refused while the emitter layout it was refused under is the one there is.
403
+ const settled = current.live ? held.has(field) : current.layout === layout;
404
+
405
+ if (is_requested(wanted, field, current) && settled) {
406
+ continue;
407
+ }
408
+
409
+ if (current.live && held.has(field)) {
410
+ registry.unbind_emitter_field(emitter, field);
411
+ }
412
+
413
+ this.#let_go(scene_context, current);
414
+ bound.delete(field);
415
+ }
416
+ }
417
+
418
+ for (let i = 0; i < wanted.length; i++) {
419
+ const { field, entity, channel } = wanted[i];
420
+
421
+ if (bound.has(field) || first_request(wanted, field) !== i) {
422
+ continue;
423
+ }
424
+
425
+ const binding = { entity, channel, live: false, on_removed: null, layout };
426
+
427
+ if (this.#hold(scene_context, binding, () => registry.bind_emitter_field(emitter, field, row_of_entity(entity), channel))) {
428
+ binding.on_removed = () => {
429
+ // the entity is going: let go of it while it still exists
430
+ if (binding.live && registry.context(emitter)?.bindings.has(field) === true) {
431
+ registry.unbind_emitter_field(emitter, field);
432
+ }
433
+
434
+ this.#let_go(scene_context, binding, false);
435
+ };
436
+
437
+ this.entityManager.dataset.addEntityEventListener(entity, EventType.EntityRemoved, binding.on_removed);
438
+ }
439
+
440
+ bound.set(field, binding);
441
+ }
442
+ }
443
+
444
+ if (this.#global_bindings_context !== scene_context) {
445
+ this.#forget_global_bindings();
446
+ this.#global_bindings_context = scene_context;
447
+ }
448
+
449
+ const requests = this.#global_bindings;
450
+ const global_bound = this.#global_bound;
451
+
452
+ if (global_bound.size > 0) {
453
+ for (const name of global_bound.keys()) {
454
+ const current = global_bound.get(name);
455
+ const request = requests.get(name);
456
+
457
+ if (request !== undefined && request.entity === current.entity && request.channel === current.channel) {
458
+ continue;
459
+ }
460
+
461
+ if (current.live) {
462
+ registry.unbind_global_field(name);
463
+ }
464
+
465
+ this.#let_go(scene_context, current);
466
+ global_bound.delete(name);
467
+ }
468
+ }
469
+
470
+ if (requests.size > global_bound.size) {
471
+ for (const name of requests.keys()) {
472
+ if (global_bound.has(name)) {
473
+ continue;
474
+ }
475
+
476
+ const { entity, channel } = requests.get(name);
477
+ const binding = { entity, channel, live: false, on_removed: null };
478
+
479
+ if (this.#hold(scene_context, binding, () => registry.bind_global_field(name, row_of_entity(entity), channel))) {
480
+ binding.on_removed = () => {
481
+ if (binding.live) {
482
+ registry.unbind_global_field(name);
483
+ }
484
+
485
+ this.#let_go(scene_context, binding, false);
486
+ };
487
+
488
+ this.entityManager.dataset.addEntityEventListener(entity, EventType.EntityRemoved, binding.on_removed);
489
+ }
490
+
491
+ global_bound.set(name, binding);
492
+ }
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Hold a binding's entity's row and apply it, or record why not.
498
+ *
499
+ * @param {import("../../shade/renderer/scene/GPUSceneContext.js").GPUSceneContext} scene_context
500
+ * @param {{entity: number, channel: number, live: boolean}} binding becomes live on success
501
+ * @param {function():void} apply binds it in the registry; a throw is a refusal
502
+ * @returns {boolean} whether it is live; a refusal is reported with a warning
503
+ */
504
+ #hold(scene_context, binding, apply) {
505
+ const dataset = this.entityManager.dataset;
506
+ const entity = binding.entity;
507
+
508
+ if (!dataset.entityExists(entity) || dataset.getComponent(entity, Transform64) === undefined) {
509
+ warn_limited("GPUParticleEmitterSystem: a transform binding names an entity with no Transform64; it is not bound");
510
+ return false;
511
+ }
512
+
513
+ scene_context.rows.acquire(entity);
514
+
515
+ try {
516
+ apply();
517
+ } catch (e) {
518
+ scene_context.rows.release(entity);
519
+ warn_limited(`GPUParticleEmitterSystem: a transform binding was refused: ${e.message}`);
520
+ return false;
521
+ }
522
+
523
+ binding.live = true;
524
+
525
+ return true;
526
+ }
527
+
528
+ /**
529
+ * Let go of a binding's hold on its entity: the row, and the removal listener.
530
+ *
531
+ * @param {import("../../shade/renderer/scene/GPUSceneContext.js").GPUSceneContext} scene_context
532
+ * @param {{entity: number, live: boolean, on_removed: function|null}} binding
533
+ * @param {boolean} [unlisten] false when called from the removal itself, whose listeners the
534
+ * dataset drops on its own
535
+ */
536
+ #let_go(scene_context, binding, unlisten = true) {
537
+ if (!binding.live) {
538
+ return;
539
+ }
540
+
541
+ if (unlisten && binding.on_removed !== null) {
542
+ this.entityManager.dataset.removeEntityEventListener(binding.entity, EventType.EntityRemoved, binding.on_removed);
543
+ }
544
+
545
+ scene_context.rows.release(binding.entity);
546
+
547
+ binding.live = false;
548
+ binding.on_removed = null;
549
+ }
550
+
551
+ /**
552
+ * Forget an entry's bindings without touching any rows — they were let go already, or belonged
553
+ * to a context that is gone — taking down the removal listeners still standing.
554
+ *
555
+ * @param {ParticleEmitterEntry} entry
556
+ */
557
+ #forget_bindings(entry) {
558
+ for (const binding of entry.bound.values()) {
559
+ if (binding.live && binding.on_removed !== null) {
560
+ this.entityManager.dataset.removeEntityEventListener(binding.entity, EventType.EntityRemoved, binding.on_removed);
561
+ }
562
+ }
563
+
564
+ entry.bound.clear();
565
+ }
566
+
567
+ /**
568
+ * Forget the global bindings applied to a context that is gone, which took its rows and its
569
+ * particle system with it: only the removal listeners are left to take down.
570
+ */
571
+ #forget_global_bindings() {
572
+ for (const binding of this.#global_bound.values()) {
573
+ if (binding.live && binding.on_removed !== null) {
574
+ this.entityManager.dataset.removeEntityEventListener(binding.entity, EventType.EntityRemoved, binding.on_removed);
575
+ }
576
+ }
577
+
578
+ this.#global_bound.clear();
579
+ }
580
+
581
+ /**
582
+ * Hand the global field values set since the last frame to the particle system.
583
+ *
584
+ * @param {import("../../shade/renderer/particles/GPUParticleSystem.js").GPUParticleSystem} particles
585
+ */
586
+ #publish_global_fields(particles) {
587
+ if (this.#global_fields_dirty.size === 0) {
588
+ return;
589
+ }
590
+
591
+ for (const name of this.#global_fields_dirty) {
592
+ try {
593
+ particles.global_fields.set(name, this.#global_fields.get(name));
594
+ } catch (e) {
595
+ // wider than the field is declared, or no room left for it: refused, and not tried
596
+ // again until set again
597
+ warn_limited(`GPUParticleEmitterSystem: a global field value was refused: ${e.message}`);
598
+ }
599
+ }
600
+
601
+ this.#global_fields_dirty.clear();
602
+ }
603
+
604
+ /**
605
+ * @param {import("../ecs/EntityManager.js").EntityManager} entityManager
606
+ * @returns {Promise<void>}
607
+ */
608
+ async startup(entityManager) {
609
+ this.entityManager = entityManager;
610
+
611
+ // the scene's rows are this dataset's entities; the first system to place one says so
612
+ const dataset = entityManager.dataset;
613
+ const scene = this.#scene;
614
+
615
+ assert.ok(
616
+ scene.dataset === null || scene.dataset === undefined || scene.dataset === dataset,
617
+ 'the scene is bound to another dataset'
618
+ );
619
+
620
+ scene.dataset = dataset;
621
+
622
+ if (!this.#assets.hasLoaderForType(GameAssetType.Image)) {
623
+ await this.#assets.registerLoader(GameAssetType.Image, new ImageRGBADataLoader());
624
+ }
625
+
626
+ this.#extension = new GPUParticleAtlasExtension(this);
627
+
628
+ this.#graphics.add_extension(this.#extension);
629
+ }
630
+
631
+ /** @returns {Promise<void>} */
632
+ async shutdown() {
633
+ if (this.#extension !== null) {
634
+ this.#graphics.remove_extension(this.#extension);
635
+ this.#extension = null;
636
+ }
637
+
638
+ for (const entity of Array.from(this.#entries.keys())) {
639
+ this.#drop(entity);
640
+ }
641
+
642
+ const context = this.#global_bindings_context;
643
+
644
+ if (context !== null && context === this.#graphics.scene_context(this.#scene)) {
645
+ const particles = this.#particles_of(context);
646
+
647
+ for (const name of this.#global_bound.keys()) {
648
+ const binding = this.#global_bound.get(name);
649
+
650
+ if (binding.live && particles !== null) {
651
+ particles.registry.unbind_global_field(name);
652
+ }
653
+
654
+ this.#let_go(context, binding);
655
+ }
656
+ }
657
+
658
+ this.#forget_global_bindings();
659
+ this.#global_bindings_context = null;
660
+ this.#global_bindings.clear();
661
+
662
+ this.#detach();
663
+
664
+ this.#texture?.destroy();
665
+ this.#texture = null;
666
+ this.#view = null;
667
+ this.#device = null;
668
+ this.#uploaded_version = -1;
669
+ this.#bursts.length = 0;
670
+
671
+ this.#managed_atlas.reset();
672
+ this.#create_defaults();
673
+ }
674
+
675
+ /**
676
+ * @param {ParticleEffect} effect
677
+ * @param {Transform64} transform
678
+ * @param {number} entity
679
+ */
680
+ link(effect, transform, entity) {
681
+ /** @type {ParticleEmitterEntry} */
682
+ const entry = {
683
+ entity,
684
+ effect,
685
+ transform,
686
+ emitter: null,
687
+ context: null,
688
+ on_transform_changed: null,
689
+ url: null,
690
+ texture: null,
691
+ bound: new Map(),
692
+ refused_program: null,
693
+ };
694
+
695
+ this.#entries.set(entity, entry);
696
+
697
+ /*
698
+ The emitter is placed the moment its transform moves rather than on a poll. A Transform64
699
+ has no signals of its own, so the announcement comes from the dataset: whoever wrote the
700
+ transform sends TRANSFORM64_EVENT_CHANGE and this wakes on it. Coarser than watching
701
+ position, rotation and scale separately — a scale write wakes a placement that reads the
702
+ whole transform anyway — and that is the trade the type makes everywhere.
703
+ */
704
+ entry.on_transform_changed = () => this.#place(entry);
705
+
706
+ this.entityManager.dataset.addEntityEventListener(
707
+ entity, TRANSFORM64_EVENT_CHANGE, entry.on_transform_changed
708
+ );
709
+
710
+ // Everything else — building the emitter, acquiring the texture — is what a frame does
711
+ // anyway, and doing it here as well would be a second copy of it that has to stay in step.
712
+ this.#sync(entry);
713
+
714
+ // Registered now when there is a context to register with, so an entity spawned mid-level
715
+ // stands where it should from its first frame.
716
+ this.#register_all();
717
+ }
718
+
719
+ /**
720
+ * @param {ParticleEffect} effect
721
+ * @param {Transform64} transform
722
+ * @param {number} entity
723
+ */
724
+ unlink(effect, transform, entity) {
725
+ this.#drop(entity);
726
+ }
727
+
728
+ /**
729
+ * Bring every emitter in line with its entity: build emitters for effects that have arrived,
730
+ * retire emitters for effects that have gone, copy changed fields onto the ones that stay, and
731
+ * follow the texture URLs.
732
+ *
733
+ * @param {number} [time_delta_seconds]
734
+ * @returns {void}
735
+ */
736
+ update(time_delta_seconds) {
737
+ for (const entry of this.#entries.values()) {
738
+ this.#sync(entry);
739
+ }
740
+
741
+ /*
742
+ Registered here as well as at FrameStart, so an emitter whose effect arrived during a
743
+ simulation tick has its row written by the next frame's build rather than the one after:
744
+ a frame builds the scene context before FrameStart runs, so a row acquired at FrameStart
745
+ is not written until the frame after.
746
+ */
747
+ this.#register_all();
748
+
749
+ this.#managed_atlas.update();
750
+ }
751
+
752
+ /**
753
+ * The particle system a scene context's frames run, or null while there is no renderer.
754
+ *
755
+ * @param {import("../../shade/renderer/scene/GPUSceneContext.js").GPUSceneContext} context
756
+ * @returns {import("../../shade/renderer/particles/GPUParticleSystem.js").GPUParticleSystem|null}
757
+ */
758
+ #particles_of(context) {
759
+ const renderer = this.#graphics.renderer;
760
+
761
+ if (renderer === null || renderer === undefined) {
762
+ return null;
763
+ }
764
+
765
+ return renderer.particles(context);
766
+ }
767
+
768
+ /**
769
+ * {@link #register} against whatever context and particle system the scene has right now, if
770
+ * it has any.
771
+ */
772
+ #register_all() {
773
+ const context = this.#graphics.scene_context(this.#scene);
774
+
775
+ if (context === null) {
776
+ return;
777
+ }
778
+
779
+ const particles = this.#particles_of(context);
780
+
781
+ if (particles !== null) {
782
+ this.#register(particles, context);
783
+ }
784
+ }
785
+
786
+ /**
787
+ * Publish the atlas at FrameStart — after the scene context has established transform rows, and
788
+ * before the renderer records the particle passes that read the emitter table.
789
+ *
790
+ * {@link update} is called from here as well as by the entity manager, because a frame must not
791
+ * depend on a simulation tick having preceded it: an editor draws on demand, and a paused game
792
+ * still renders. It is idempotent and costs a comparison per emitter when nothing has changed,
793
+ * which is what makes running it twice a frame the cheap way to be right rather than a saving
794
+ * worth chasing.
795
+ *
796
+ * @param {import("../../shade/renderer/extension/FrameContext.js").FrameContext} frame
797
+ * @returns {void}
798
+ */
799
+ prepare(frame) {
800
+ if (frame.view.scene.scene !== this.#scene) {
801
+ return;
802
+ }
803
+
804
+ this.update();
805
+
806
+ const renderer = this.#graphics.renderer;
807
+
808
+ /*
809
+ The renderer's GPU particle feature is this system's to turn on, and it is turned on every
810
+ frame rather than once, because a device restart builds a renderer that has never heard of
811
+ it. The consequence is worth stating: while this system is in the profile there is no way to
812
+ switch GPU particles off through the renderer, because this would switch them back on at the
813
+ next FrameStart. Switching them off means taking the system out — or, for one effect,
814
+ `ParticleEffect#emitting`.
815
+ */
816
+ renderer.feature_particles_enabled = true;
817
+
818
+ const particles = renderer.particles(frame.view.scene);
819
+
820
+ if (particles !== this.#particles) {
821
+ this.#detach();
822
+ this.#particles = particles;
823
+ this.#previous_atlas = particles.atlas;
824
+
825
+ // a particle system this one has not fed yet: everything set so far is news to it
826
+ for (const name of this.#global_fields.keys()) {
827
+ this.#global_fields_dirty.add(name);
828
+ }
829
+ }
830
+
831
+ this.#register(particles, frame.view.scene);
832
+ this.#bind(particles, frame.view.scene);
833
+
834
+ this.#upload(renderer.graphics);
835
+
836
+ particles.atlas = this.#view;
837
+
838
+ this.#publish_regions(particles);
839
+ this.#publish_global_fields(particles);
840
+ this.#drain_bursts(particles);
841
+ }
842
+
843
+ /**
844
+ * Register every emitter not yet registered with this context, at its entity's row.
845
+ *
846
+ * From {@link update} and {@link prepare} rather than at link, because a row needs a scene
847
+ * context and the context needs a device, and neither is a system's to wait for: a level loads
848
+ * before the device is up, and a device restart replaces the context under every entry. An
849
+ * entry registered with a context that is no longer this one is registered again with the one
850
+ * that is. Idempotent, and a comparison per entry when nothing has changed.
851
+ *
852
+ * @param {import("../../shade/renderer/particles/GPUParticleSystem.js").GPUParticleSystem} particles
853
+ * @param {import("../../shade/renderer/scene/GPUSceneContext.js").GPUSceneContext} scene_context
854
+ */
855
+ #register(particles, scene_context) {
856
+ const rows = scene_context.rows;
857
+
858
+ for (const entry of this.#entries.values()) {
859
+ if (entry.emitter === null || entry.context === scene_context || entry.emitter.program === entry.refused_program) {
860
+ continue;
861
+ }
862
+
863
+ rows.acquire(entry.entity);
864
+
865
+ try {
866
+ particles.add_emitter(entry.emitter, row_of_entity(entry.entity));
867
+ } catch (e) {
868
+ // The program's global imports do not link against the scene's global fields. One
869
+ // effect must not stop every other one: it is reported, and not tried again until
870
+ // its program changes.
871
+ rows.release(entry.entity);
872
+ entry.refused_program = entry.emitter.program;
873
+ warn_limited(`GPUParticleEmitterSystem: an effect was refused: ${e.message}`);
874
+ continue;
875
+ }
876
+
877
+ entry.refused_program = null;
878
+ entry.context = scene_context;
879
+
880
+ // whatever was bound was bound in a context that is gone, holds and all
881
+ this.#forget_bindings(entry);
882
+ }
883
+ }
884
+
885
+ /**
886
+ * Point every emitter of this scene at its patch in the atlas.
887
+ *
888
+ * Every region, every frame: growth also moves the UVs of images that did not change, and there
889
+ * is no signal for that which an emitter could subscribe to.
890
+ *
891
+ * @param {import("../../shade/renderer/particles/GPUParticleSystem.js").GPUParticleSystem} particles
892
+ */
893
+ #publish_regions(particles) {
894
+ const registry = particles.registry;
895
+ const emitters = registry.emitters;
896
+ const region = this.#region;
897
+
898
+ for (let i = 0; i < emitters.length; i++) {
899
+ const emitter = emitters[i];
900
+ const entry = this.#by_emitter.get(emitter);
901
+
902
+ // Not ours: it has nothing in this atlas, so it gets the one patch that is true for
903
+ // everybody — a white texel, which draws it as a flat quad of its own colour.
904
+ const patch = entry === undefined || entry.texture === null
905
+ ? this.#white
906
+ : entry.texture.getValue() ?? this.#transparent;
907
+
908
+ const uv = patch.uv;
909
+
910
+ if (patch === this.#white || patch === this.#transparent) {
911
+ // Sample the texel centre everywhere, avoiding interpolation into the gutter.
912
+ region[0] = uv.position.x + uv.size.x * 0.5;
913
+ region[1] = uv.position.y + uv.size.y * 0.5;
914
+ region[2] = 0;
915
+ region[3] = 0;
916
+ } else {
917
+ region[0] = uv.position.x;
918
+ region[1] = uv.position.y;
919
+ region[2] = uv.size.x;
920
+ region[3] = uv.size.y;
921
+ }
922
+
923
+ registry.set_atlas_region(emitter, region);
924
+ }
925
+ }
926
+
927
+ /**
928
+ * Hand this frame's bursts to the particle system, now that membership has been swept and every
929
+ * emitter that can have a row has one.
930
+ *
931
+ * The queue is emptied whether or not each burst could be delivered. A burst is a request about
932
+ * *this* moment — a footfall, an impact — and holding one for an emitter that is not live yet
933
+ * would deliver it seconds late, at whatever unrelated moment the emitter finally arrived.
934
+ *
935
+ * @param {import("../../shade/renderer/particles/GPUParticleSystem.js").GPUParticleSystem} particles
936
+ */
937
+ #drain_bursts(particles) {
938
+ const bursts = this.#bursts;
939
+
940
+ for (let i = 0; i < bursts.length; i += 2) {
941
+ const entry = this.#entries.get(bursts[i]);
942
+
943
+ if (entry === undefined || entry.emitter === null) {
944
+ continue;
945
+ }
946
+
947
+ if (particles.registry.context(entry.emitter) === undefined) {
948
+ continue;
949
+ }
950
+
951
+ particles.spawn(entry.emitter, bursts[i + 1]);
952
+ }
953
+
954
+ bursts.length = 0;
955
+ }
956
+
957
+ /**
958
+ * One entity's emitter, brought in line with its component.
959
+ *
960
+ * @param {ParticleEmitterEntry} entry
961
+ */
962
+ #sync(entry) {
963
+ const effect = entry.effect;
964
+
965
+ /*
966
+ Ahead of everything the effect gates, and deliberately: an image takes a fetch and a decode
967
+ to arrive, and an entity that names one has said what it wants to draw with whether or not
968
+ its program has finished compiling. Waiting would mean a first frame of transparent
969
+ particles for every effect that arrives with its texture.
970
+ */
971
+ if (entry.url !== effect.texture) {
972
+ entry.texture?.release();
973
+
974
+ entry.url = effect.texture;
975
+ entry.texture = entry.url === null ? null : this.#managed_atlas.acquire(entry.url);
976
+ }
977
+
978
+ if (effect.layout === null || effect.program === null) {
979
+ // No effect (yet, or any more). An entity whose effect is still compiling is ordinary,
980
+ // and so is one whose effect was cleared — both mean "emits nothing", and the emitter
981
+ // is retired rather than keeping the last program it happened to have.
982
+ this.#detach_emitter(entry);
983
+
984
+ return;
985
+ }
986
+
987
+ if (entry.emitter === null) {
988
+ const emitter = new ParticleEmitter();
989
+
990
+ /*
991
+ The emitter takes the entity's name when it has one, exactly as `ShadedGeometrySystem`
992
+ does for a primitive. Nothing draws differently for it: what it buys is that a walk over
993
+ the registry is readable in a debugger.
994
+ */
995
+ const name = this.entityManager.dataset.getComponent(entry.entity, Name);
996
+
997
+ emitter.name = name === undefined || name === null
998
+ ? `particles/entity ${entry.entity}`
999
+ : name.getValue();
1000
+
1001
+ entry.emitter = emitter;
1002
+
1003
+ apply_particle_effect(emitter, effect);
1004
+
1005
+ // registered at the entity's row by the next frame — see #register
1006
+ this.#by_emitter.set(emitter, entry);
1007
+ } else if (apply_particle_effect(entry.emitter, effect)) {
1008
+ // The emitter's own change counter, which is what EmitterRegistry restages a row from.
1009
+ entry.emitter.needsUpdate = true;
1010
+ }
1011
+ }
1012
+
1013
+ /**
1014
+ * The entity moved: its row is rewritten from its transform by the context that holds it.
1015
+ *
1016
+ * @param {ParticleEmitterEntry} entry
1017
+ */
1018
+ #place(entry) {
1019
+ const context = entry.context;
1020
+
1021
+ if (context === null || context !== this.#graphics.scene_context(this.#scene)) {
1022
+ return;
1023
+ }
1024
+
1025
+ context.rows.mark_world(entry.entity);
1026
+ }
1027
+
1028
+ /**
1029
+ * Retire an entity's emitter and let go of its row, leaving the entry linked.
1030
+ *
1031
+ * @param {ParticleEmitterEntry} entry
1032
+ */
1033
+ #detach_emitter(entry) {
1034
+ const emitter = entry.emitter;
1035
+
1036
+ if (emitter === null) {
1037
+ return;
1038
+ }
1039
+
1040
+ const context = entry.context;
1041
+
1042
+ // A context that has been replaced took its rows and its particle system with it; only the
1043
+ // one that is running is handed anything back.
1044
+ if (context !== null && context === this.#graphics.scene_context(this.#scene)) {
1045
+ const particles = this.#particles_of(context);
1046
+
1047
+ if (particles !== null && particles.registry.context(emitter) !== undefined) {
1048
+ particles.remove_emitter(emitter);
1049
+ }
1050
+
1051
+ context.rows.release(entry.entity);
1052
+
1053
+ // the registry dropped the bindings with the emitter; the rows they held go too
1054
+ for (const binding of entry.bound.values()) {
1055
+ this.#let_go(context, binding);
1056
+ }
1057
+ }
1058
+
1059
+ entry.context = null;
1060
+ this.#forget_bindings(entry);
1061
+
1062
+ this.#by_emitter.delete(emitter);
1063
+
1064
+ entry.emitter = null;
1065
+ }
1066
+
1067
+ /**
1068
+ * @param {number} entity
1069
+ */
1070
+ #drop(entity) {
1071
+ const entry = this.#entries.get(entity);
1072
+
1073
+ if (entry === undefined) {
1074
+ return;
1075
+ }
1076
+
1077
+ if (entry.on_transform_changed !== null) {
1078
+ this.entityManager.dataset.removeEntityEventListener(
1079
+ entity, TRANSFORM64_EVENT_CHANGE, entry.on_transform_changed
1080
+ );
1081
+
1082
+ entry.on_transform_changed = null;
1083
+ }
1084
+
1085
+ this.#detach_emitter(entry);
1086
+ entry.texture?.release();
1087
+
1088
+ entry.url = null;
1089
+ entry.texture = null;
1090
+
1091
+ this.#entries.delete(entity);
1092
+ }
1093
+
1094
+ /** @returns {void} */
1095
+ #create_defaults() {
1096
+ const white = Sampler2D.uint8(4, 1, 1);
1097
+
1098
+ white.data.fill(255);
1099
+
1100
+ this.#white = this.atlas.add(white);
1101
+ this.#transparent = this.atlas.add(Sampler2D.uint8(4, 1, 1));
1102
+ }
1103
+
1104
+ /**
1105
+ * @param {import("../../shade/renderer/GPUDeviceContext.js").GPUDeviceContext} graphics
1106
+ * @returns {void}
1107
+ */
1108
+ #upload(graphics) {
1109
+ const device = graphics.device;
1110
+ const source = this.atlas.sampler;
1111
+
1112
+ if (this.#device !== device || this.#texture === null) {
1113
+ this.#texture?.destroy();
1114
+
1115
+ this.#device = device;
1116
+ this.#texture = new GPUTextureContext(device);
1117
+
1118
+ const descriptor = this.#texture.descriptor;
1119
+
1120
+ descriptor.label = "particles/sprite atlas";
1121
+ descriptor.format = "rgba8unorm";
1122
+ descriptor.usage = GPUTextureUsage.TEXTURE_BINDING
1123
+ | GPUTextureUsage.COPY_DST
1124
+ | GPUTextureUsage.RENDER_ATTACHMENT;
1125
+
1126
+ this.#uploaded_version = -1;
1127
+ }
1128
+
1129
+ const texture = this.#texture;
1130
+
1131
+ if (texture.width !== source.width || texture.height !== source.height) {
1132
+ texture.descriptor.mipLevelCount = 1 + Math.floor(Math.log2(Math.max(source.width, source.height)));
1133
+ texture.resize(source.width, source.height);
1134
+
1135
+ this.#uploaded_version = -1;
1136
+ }
1137
+
1138
+ if (this.#uploaded_version !== source.version) {
1139
+ device.queue.writeTexture({ texture: texture.gpu_texture }, source.data,
1140
+ { bytesPerRow: source.width * 4 }, [source.width, source.height]);
1141
+
1142
+ if (texture.descriptor.mipLevelCount > 1) {
1143
+ graphics.textures.mipmaps.generateMipmap(
1144
+ texture.gpu_texture,
1145
+ texture.descriptor,
1146
+ TextureFilterType.Linear
1147
+ );
1148
+ }
1149
+
1150
+ texture.incrementVersion();
1151
+
1152
+ this.#uploaded_version = source.version;
1153
+ }
1154
+
1155
+ this.#view = texture.obtainView();
1156
+ }
1157
+
1158
+ /** @returns {void} */
1159
+ #detach() {
1160
+ if (this.#particles !== null && this.#particles.atlas === this.#view) {
1161
+ this.#particles.atlas = this.#previous_atlas;
1162
+
1163
+ for (const emitter of this.#particles.emitters) {
1164
+ this.#particles.registry.set_atlas_region(emitter, [0, 0, 1, 1]);
1165
+ }
1166
+ }
1167
+
1168
+ this.#particles = null;
1169
+ this.#previous_atlas = null;
1170
+ }
1171
+ }
1172
+
1173
+ /**
1174
+ * Copy a component onto the emitter built for it, reporting whether anything actually moved.
1175
+ *
1176
+ * Comparing while assigning rather than in a pass of its own is what makes change detection cost
1177
+ * nothing extra: the fields have to be read to be written, and the only addition is an `||` per
1178
+ * field. What it answers is whether the emitter's GPU row has to be restaged, and restaging one that
1179
+ * has not changed is an upload per emitter per frame.
1180
+ *
1181
+ * `spawn_rate` comes from {@link ParticleEffect#effective_spawn_rate}, so an effect switched off
1182
+ * with `emitting = false` reaches the GPU as a rate of zero and keeps its authored rate to come back
1183
+ * to. Layout, program and emitter layout are shared by reference, never copied — two entities of one
1184
+ * effect belong on one program id in the heap. Transform bindings are not copied here; the system
1185
+ * applies them on its own pass.
1186
+ *
1187
+ * Every field is compared by name, and that is load-bearing rather than style. Through a shared
1188
+ * `assign(target, field, value)` the one `target[field]` access saw every name, went
1189
+ * megamorphic, and read numbers back boxed, while the call boxed the value it was handed: 228 B and
1190
+ * ~120 ns per emitter per call, against nothing and ~9 ns written out like this
1191
+ * (`GPUParticleEmitterSystem.allocation.spec.js`). This runs for every live emitter twice a frame.
1192
+ *
1193
+ * @param {ParticleEmitter} emitter
1194
+ * @param {ParticleEffect} effect
1195
+ * @returns {boolean} whether the emitter differs from what it held before
1196
+ */
1197
+ export function apply_particle_effect(emitter, effect) {
1198
+ let changed = false;
1199
+
1200
+ if (emitter.layout !== effect.layout) {
1201
+ emitter.layout = effect.layout;
1202
+ changed = true;
1203
+ }
1204
+
1205
+ if (emitter.program !== effect.program) {
1206
+ emitter.program = effect.program;
1207
+ changed = true;
1208
+ }
1209
+
1210
+ if (emitter.texture !== effect.texture) {
1211
+ emitter.texture = effect.texture;
1212
+ changed = true;
1213
+ }
1214
+
1215
+ if (emitter.seed !== effect.seed) {
1216
+ emitter.seed = effect.seed;
1217
+ changed = true;
1218
+ }
1219
+
1220
+ const spawn_rate = effect.effective_spawn_rate;
1221
+
1222
+ if (emitter.spawn_rate !== spawn_rate) {
1223
+ emitter.spawn_rate = spawn_rate;
1224
+ changed = true;
1225
+ }
1226
+
1227
+ if (emitter.prewarm !== effect.prewarm) {
1228
+ emitter.prewarm = effect.prewarm;
1229
+ changed = true;
1230
+ }
1231
+
1232
+ if (emitter.flags !== effect.flags) {
1233
+ emitter.flags = effect.flags;
1234
+ changed = true;
1235
+ }
1236
+
1237
+ if (emitter.render_position !== effect.render_position) {
1238
+ emitter.render_position = effect.render_position;
1239
+ changed = true;
1240
+ }
1241
+
1242
+ if (emitter.render_size !== effect.render_size) {
1243
+ emitter.render_size = effect.render_size;
1244
+ changed = true;
1245
+ }
1246
+
1247
+ if (emitter.render_color !== effect.render_color) {
1248
+ emitter.render_color = effect.render_color;
1249
+ changed = true;
1250
+ }
1251
+
1252
+ if (emitter.render_orientation !== effect.render_orientation) {
1253
+ emitter.render_orientation = effect.render_orientation;
1254
+ changed = true;
1255
+ }
1256
+
1257
+ if (emitter.render_frame !== effect.render_frame) {
1258
+ emitter.render_frame = effect.render_frame;
1259
+ changed = true;
1260
+ }
1261
+
1262
+ if (emitter.render_velocity !== effect.render_velocity) {
1263
+ emitter.render_velocity = effect.render_velocity;
1264
+ changed = true;
1265
+ }
1266
+
1267
+ const flipbook = emitter.flipbook;
1268
+
1269
+ if (flipbook[0] !== effect.flipbook[0] || flipbook[1] !== effect.flipbook[1]) {
1270
+ flipbook.set(effect.flipbook);
1271
+
1272
+ changed = true;
1273
+ }
1274
+
1275
+ if (emitter.emitter_layout !== effect.emitter_layout) {
1276
+ emitter.emitter_layout = effect.emitter_layout;
1277
+ changed = true;
1278
+ }
1279
+
1280
+ // The values are copied, never shared: gameplay writes the component's array in place, so an
1281
+ // emitter holding the same array would always compare equal to it and never see an edit.
1282
+ const source = effect.emitter_fields;
1283
+ let fields = emitter.emitter_fields;
1284
+
1285
+ if (fields.length !== source.length) {
1286
+ fields = emitter.emitter_fields = new Uint32Array(source.length);
1287
+ fields.set(source);
1288
+ changed = true;
1289
+ } else {
1290
+ for (let i = 0; i < source.length; i++) {
1291
+ if (fields[i] !== source[i]) {
1292
+ fields.set(source);
1293
+ changed = true;
1294
+ break;
1295
+ }
1296
+ }
1297
+ }
1298
+
1299
+ return changed;
1300
+ }
1301
+
1302
+ /**
1303
+ * @param {Array<{field: string, entity: number, channel: number}>} wanted
1304
+ * @param {string} field
1305
+ * @returns {number} the index of the first request for `field`, or -1
1306
+ */
1307
+ function first_request(wanted, field) {
1308
+ for (let i = 0; i < wanted.length; i++) {
1309
+ if (wanted[i].field === field) {
1310
+ return i;
1311
+ }
1312
+ }
1313
+
1314
+ return -1;
1315
+ }
1316
+
1317
+ /**
1318
+ * @param {Array<{field: string, entity: number, channel: number}>} wanted
1319
+ * @param {string} field
1320
+ * @param {{entity: number, channel: number}} current
1321
+ * @returns {boolean} whether the request that holds for `field` is `current`'s
1322
+ */
1323
+ function is_requested(wanted, field, current) {
1324
+ const i = first_request(wanted, field);
1325
+
1326
+ return i !== -1 && wanted[i].entity === current.entity && wanted[i].channel === current.channel;
1327
+ }
1328
+
1329
+ class GPUParticleAtlasExtension extends RenderExtension {
1330
+ /** @type {string} */
1331
+ name = "gpu particle atlas";
1332
+ /** @type {FramePhase} */
1333
+ phase = FramePhase.FrameStart;
1334
+ /** @type {GPUParticleEmitterSystem} */
1335
+ #system;
1336
+
1337
+ /** @param {GPUParticleEmitterSystem} system */
1338
+ constructor(system) {
1339
+ super();
1340
+
1341
+ this.#system = system;
1342
+ }
1343
+
1344
+ /**
1345
+ * @param {import("../../shade/renderer/extension/FrameContext.js").FrameContext} frame
1346
+ * @returns {void}
1347
+ */
1348
+ record(frame) {
1349
+ this.#system.prepare(frame);
1350
+ }
1351
+ }