@woosh/meep-engine 3.18.0 → 3.20.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 (228) hide show
  1. package/build/bundle-worker-image-decoder.js +1 -1
  2. package/build/bundle-worker-terrain.js +1 -1
  3. package/editor/view/particles/effect/ParticleCurveEditorView.d.ts.map +1 -1
  4. package/editor/view/particles/effect/ParticleCurveEditorView.js +303 -120
  5. package/editor/view/particles/effect/ParticleGradientEditorView.d.ts.map +1 -1
  6. package/editor/view/particles/effect/ParticleGradientEditorView.js +108 -63
  7. package/editor/view/particles/effect/ParticleGraphEditorView.js +2 -2
  8. package/editor/view/particles/effect/ParticleNodeParametersView.d.ts.map +1 -1
  9. package/editor/view/particles/effect/ParticleNodeParametersView.js +7 -46
  10. package/editor/view/particles/effect/particle-editor.css +60 -30
  11. package/package.json +1 -2
  12. package/samples/engine/README.md +1 -1
  13. package/src/core/binary/compression/decompress_bytes.d.ts +13 -0
  14. package/src/core/binary/compression/decompress_bytes.d.ts.map +1 -0
  15. package/src/core/binary/compression/decompress_bytes.js +28 -0
  16. package/src/core/collection/array/array_group_by.d.ts.map +1 -1
  17. package/src/core/collection/array/array_group_by.js +2 -11
  18. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
  19. package/src/core/geom/3d/shape/HeightMapShape3D.js +4 -9
  20. package/src/core/model/node-graph/visual/layout/layout_assign_coordinates.js +67 -22
  21. package/src/core/model/object/optional_clone.d.ts +7 -0
  22. package/src/core/model/object/optional_clone.d.ts.map +1 -0
  23. package/src/core/model/object/optional_equality.d.ts +10 -0
  24. package/src/core/model/object/optional_equality.d.ts.map +1 -0
  25. package/src/{shade/descriptor/util → core/model/object}/optional_equality.js +2 -2
  26. package/src/core/model/object/optional_hash.d.ts +9 -0
  27. package/src/core/model/object/optional_hash.d.ts.map +1 -0
  28. package/src/{shade/descriptor/util → core/model/object}/optional_hash.js +3 -3
  29. package/src/engine/asset/Asset.d.ts +1 -1
  30. package/src/engine/asset/Asset.d.ts.map +1 -1
  31. package/src/engine/asset/Asset.js +3 -1
  32. package/src/engine/asset/loaders/image/ImageDecoderWorker.js +12 -27
  33. package/src/engine/asset/loaders/image/prototypePNG.js +8 -7
  34. package/src/engine/ecs/gui/hud/HeadsUpDisplaySystem.d.ts.map +1 -1
  35. package/src/engine/ecs/gui/hud/HeadsUpDisplaySystem.js +9 -10
  36. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  37. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +4 -0
  38. package/src/engine/graphics/ecs/particles/ParticleEffect.d.ts +266 -0
  39. package/src/engine/graphics/ecs/particles/ParticleEffect.d.ts.map +1 -0
  40. package/src/engine/graphics/ecs/particles/ParticleEffect.js +439 -0
  41. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.d.ts +58 -0
  42. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.d.ts.map +1 -0
  43. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.js +219 -0
  44. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +178 -25
  45. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
  46. package/src/engine/graphics3/GPUParticleEmitterSystem.js +809 -310
  47. package/src/format/image/png/PNGReader.d.ts +7 -6
  48. package/src/format/image/png/PNGReader.d.ts.map +1 -1
  49. package/src/format/image/png/PNGReader.js +13 -12
  50. package/src/format/image/png/chunk/png_chunk_decode_iTXt.d.ts +3 -2
  51. package/src/format/image/png/chunk/png_chunk_decode_iTXt.d.ts.map +1 -1
  52. package/src/format/image/png/chunk/png_chunk_decode_iTXt.js +5 -4
  53. package/src/format/image/png/chunk/png_chunk_decode_zTXt.d.ts +3 -2
  54. package/src/format/image/png/chunk/png_chunk_decode_zTXt.d.ts.map +1 -1
  55. package/src/format/image/png/chunk/png_chunk_decode_zTXt.js +5 -4
  56. package/src/format/image/png/png_inflate.d.ts +3 -3
  57. package/src/format/image/png/png_inflate.d.ts.map +1 -1
  58. package/src/format/image/png/png_inflate.js +29 -39
  59. package/src/format/texture/ktx2/ktx2_read.d.ts +4 -4
  60. package/src/format/texture/ktx2/ktx2_read.d.ts.map +1 -1
  61. package/src/format/texture/ktx2/ktx2_read.js +18 -21
  62. package/src/shade/descriptor/binding/BindGroupLayoutDescriptor.js +1 -1
  63. package/src/shade/descriptor/binding/BindGroupLayoutEntry.js +3 -3
  64. package/src/shade/descriptor/pipeline/PipelineDescriptorBase.js +1 -1
  65. package/src/shade/descriptor/pipeline/PipelineLayoutDescriptor.js +1 -1
  66. package/src/shade/descriptor/pipeline/render/ColorTargetState.js +3 -3
  67. package/src/shade/descriptor/pipeline/render/FragmentState.js +1 -1
  68. package/src/shade/descriptor/pipeline/render/RenderPipelineDescriptor.js +3 -3
  69. package/src/shade/playground/particle_ecs/README.md +203 -0
  70. package/src/shade/playground/particle_ecs/bonfire_editor.d.ts +39 -0
  71. package/src/shade/playground/particle_ecs/bonfire_editor.d.ts.map +1 -0
  72. package/src/shade/playground/particle_ecs/bonfire_editor.js +315 -0
  73. package/src/shade/playground/particle_ecs/bonfire_effects.d.ts +145 -0
  74. package/src/shade/playground/particle_ecs/bonfire_effects.d.ts.map +1 -0
  75. package/src/shade/playground/particle_ecs/bonfire_effects.js +202 -0
  76. package/src/shade/playground/particle_ecs/bonfire_sprites.d.ts +23 -0
  77. package/src/shade/playground/particle_ecs/bonfire_sprites.d.ts.map +1 -0
  78. package/src/shade/playground/particle_ecs/bonfire_sprites.js +315 -0
  79. package/src/shade/playground/particle_ecs/bonfire_world.d.ts +86 -0
  80. package/src/shade/playground/particle_ecs/bonfire_world.d.ts.map +1 -0
  81. package/src/shade/playground/particle_ecs/bonfire_world.js +303 -0
  82. package/src/shade/playground/particle_ecs/effects/embers.json +1634 -0
  83. package/src/shade/playground/particle_ecs/effects/flame.json +1882 -0
  84. package/src/shade/playground/particle_ecs/effects/smoke.json +1860 -0
  85. package/src/shade/playground/particle_ecs/effects/soot.json +1606 -0
  86. package/src/shade/playground/particle_ecs/index.html +330 -0
  87. package/src/shade/playground/particle_ecs/main.d.ts +2 -0
  88. package/src/shade/playground/particle_ecs/main.d.ts.map +1 -0
  89. package/src/shade/playground/particle_ecs/main.js +566 -0
  90. package/src/shade/playground/particle_ecs/moonlit_environment.d.ts +16 -0
  91. package/src/shade/playground/particle_ecs/moonlit_environment.d.ts.map +1 -0
  92. package/src/shade/playground/particle_ecs/moonlit_environment.js +144 -0
  93. package/src/shade/playground/particle_editor/README.md +5 -4
  94. package/src/shade/playground/particle_system/particle_prototype.d.ts +7 -9
  95. package/src/shade/playground/particle_system/particle_prototype.d.ts.map +1 -1
  96. package/src/shade/playground/particle_system/particle_prototype.js +7 -9
  97. package/src/shade/playground/profile_hotkey.d.ts +58 -0
  98. package/src/shade/playground/profile_hotkey.d.ts.map +1 -0
  99. package/src/shade/playground/profile_hotkey.js +325 -0
  100. package/src/shade/playground/ssr_motion/README.md +22 -0
  101. package/src/shade/playground/ssr_motion/index.html +18 -0
  102. package/src/shade/playground/ssr_motion/main.d.ts +2 -0
  103. package/src/shade/playground/ssr_motion/main.d.ts.map +1 -0
  104. package/src/shade/playground/ssr_motion/main.js +149 -0
  105. package/src/shade/playground/ssr_variance/README.md +105 -0
  106. package/src/shade/playground/ssr_variance/capture.d.ts +16 -0
  107. package/src/shade/playground/ssr_variance/capture.d.ts.map +1 -0
  108. package/src/shade/playground/ssr_variance/capture.js +96 -0
  109. package/src/shade/playground/ssr_variance/index.html +23 -0
  110. package/src/shade/playground/ssr_variance/main.d.ts +2 -0
  111. package/src/shade/playground/ssr_variance/main.d.ts.map +1 -0
  112. package/src/shade/playground/ssr_variance/main.js +246 -0
  113. package/src/shade/playground/ssr_variance/reference.d.ts +18 -0
  114. package/src/shade/playground/ssr_variance/reference.d.ts.map +1 -0
  115. package/src/shade/playground/ssr_variance/reference.js +78 -0
  116. package/src/shade/playground/ssr_variance/scene.d.ts +11 -0
  117. package/src/shade/playground/ssr_variance/scene.d.ts.map +1 -0
  118. package/src/shade/playground/ssr_variance/scene.js +61 -0
  119. package/src/shade/playground/ssr_variance/statistics.d.ts +44 -0
  120. package/src/shade/playground/ssr_variance/statistics.d.ts.map +1 -0
  121. package/src/shade/playground/ssr_variance/statistics.js +51 -0
  122. package/src/shade/renderer/geometry/Geometry.d.ts.map +1 -1
  123. package/src/shade/renderer/geometry/Geometry.js +4 -7
  124. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts.map +1 -1
  125. package/src/shade/renderer/loader/gltf/tiny-gltf.js +9 -3
  126. package/src/shade/renderer/loader/usd/usd_decode_image.d.ts +4 -4
  127. package/src/shade/renderer/loader/usd/usd_decode_image.d.ts.map +1 -1
  128. package/src/shade/renderer/loader/usd/usd_decode_image.js +4 -4
  129. package/src/shade/renderer/material/StandardShadeMaterial.js +2 -2
  130. package/src/shade/renderer/particles/DESIGN.md +745 -702
  131. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  132. package/src/shade/renderer/particles/GPUParticleSystem.js +3 -1
  133. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.d.ts +1 -1
  134. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.js +1 -1
  135. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
  136. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +9 -15
  137. package/src/shade/renderer/particles/graph/particle_group_library.d.ts.map +1 -1
  138. package/src/shade/renderer/particles/graph/particle_group_library.js +8 -3
  139. package/src/shade/renderer/particles/graph/particle_node_parameters.d.ts +0 -2
  140. package/src/shade/renderer/particles/graph/particle_node_parameters.d.ts.map +1 -1
  141. package/src/shade/renderer/particles/graph/particle_node_parameters.js +0 -9
  142. package/src/shade/renderer/particles/graph/particle_node_presentation.d.ts.map +1 -1
  143. package/src/shade/renderer/particles/graph/particle_node_presentation.js +0 -10
  144. package/src/shade/renderer/particles/graph/particle_ramp.d.ts +2 -8
  145. package/src/shade/renderer/particles/graph/particle_ramp.d.ts.map +1 -1
  146. package/src/shade/renderer/particles/graph/particle_ramp.js +2 -8
  147. package/src/shade/renderer/particles/graph/validate_particle_graph.js +0 -6
  148. package/src/shade/renderer/particles/graph_particles_avboit.d.ts.map +1 -1
  149. package/src/shade/renderer/particles/graph_particles_avboit.js +13 -4
  150. package/src/shade/renderer/particles/isa/InstructionStream.d.ts +1 -12
  151. package/src/shade/renderer/particles/isa/InstructionStream.d.ts.map +1 -1
  152. package/src/shade/renderer/particles/isa/InstructionStream.js +1 -12
  153. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts +54 -60
  154. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts.map +1 -1
  155. package/src/shade/renderer/particles/isa/ParticleVMISA.js +9 -17
  156. package/src/shade/renderer/particles/isa/particle_assembly.d.ts +0 -1
  157. package/src/shade/renderer/particles/isa/particle_assembly.d.ts.map +1 -1
  158. package/src/shade/renderer/particles/isa/particle_assembly.js +2 -16
  159. package/src/shade/renderer/particles/optimizer/dag/particle_vector_graph.d.ts +1 -1
  160. package/src/shade/renderer/particles/optimizer/dag/particle_vector_graph.js +1 -1
  161. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lift.js +0 -14
  162. package/src/shade/renderer/particles/optimizer/dag/particle_vector_pack.js +3 -17
  163. package/src/shade/renderer/particles/optimizer/particle_program_analysis.d.ts.map +1 -1
  164. package/src/shade/renderer/particles/optimizer/particle_program_analysis.js +15 -36
  165. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.d.ts.map +1 -1
  166. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.js +1 -5
  167. package/src/shade/renderer/particles/optimizer/particle_program_fuzz.d.ts.map +1 -1
  168. package/src/shade/renderer/particles/optimizer/particle_program_fuzz.js +1 -8
  169. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts +2 -11
  170. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts.map +1 -1
  171. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.js +2 -11
  172. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts +11 -4
  173. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts.map +1 -1
  174. package/src/shade/renderer/particles/runtime/ParticleEmitter.js +431 -424
  175. package/src/shade/renderer/particles/shaders/chunk_particle_avboit_contribution.d.ts.map +1 -1
  176. package/src/shade/renderer/particles/shaders/chunk_particle_avboit_contribution.js +13 -0
  177. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.d.ts +3 -6
  178. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.d.ts.map +1 -1
  179. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.js +49 -17
  180. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
  181. package/src/shade/renderer/particles/shaders/shader_particle_emit.js +0 -2
  182. package/src/shade/renderer/particles/shaders/shader_particle_emit_wide.d.ts.map +1 -1
  183. package/src/shade/renderer/particles/shaders/shader_particle_emit_wide.js +0 -2
  184. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
  185. package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +1 -5
  186. package/src/shade/renderer/particles/shaders/shader_particle_simulate_wide.d.ts.map +1 -1
  187. package/src/shade/renderer/particles/shaders/shader_particle_simulate_wide.js +0 -2
  188. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts +1 -3
  189. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts.map +1 -1
  190. package/src/shade/renderer/particles/vm/ParticleVMReference.js +4 -20
  191. package/src/shade/renderer/particles/vm/chunk_particle_vm.d.ts.map +1 -1
  192. package/src/shade/renderer/particles/vm/chunk_particle_vm.js +2 -5
  193. package/src/shade/renderer/particles/warmup/shader_particle_warmup_advance.d.ts.map +1 -1
  194. package/src/shade/renderer/particles/warmup/shader_particle_warmup_advance.js +0 -2
  195. package/src/shade/renderer/particles/warmup/shader_particle_warmup_advance_wide.d.ts.map +1 -1
  196. package/src/shade/renderer/particles/warmup/shader_particle_warmup_advance_wide.js +0 -2
  197. package/src/shade/renderer/postprocess/ssr/SSR.d.ts.map +1 -1
  198. package/src/shade/renderer/postprocess/ssr/SSR.js +2 -1
  199. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_reprojection.d.ts.map +1 -1
  200. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_reprojection.js +20 -5
  201. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_sample_history.d.ts.map +1 -1
  202. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_sample_history.js +19 -5
  203. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_temporal_accumulate.d.ts +4 -0
  204. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_temporal_accumulate.d.ts.map +1 -0
  205. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_temporal_accumulate.js +137 -0
  206. package/src/shade/renderer/postprocess/ssr/ssr_reproject_shader.d.ts +0 -8
  207. package/src/shade/renderer/postprocess/ssr/ssr_reproject_shader.d.ts.map +1 -1
  208. package/src/shade/renderer/postprocess/ssr/ssr_reproject_shader.js +18 -114
  209. package/src/shade/renderer/scene/Mesh.js +1 -1
  210. package/src/shade/renderer/texture/ShadeTexture.js +1 -1
  211. package/src/shade/renderer/texture/source/texel_data_from_ktx2.d.ts +2 -2
  212. package/src/shade/renderer/texture/source/texel_data_from_ktx2.d.ts.map +1 -1
  213. package/src/shade/renderer/texture/source/texel_data_from_ktx2.js +3 -3
  214. package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.d.ts +0 -19
  215. package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.d.ts.map +0 -1
  216. package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.js +0 -30
  217. package/src/shade/renderer/particles/shaders/chunk_particle_curve_disabled.d.ts +0 -11
  218. package/src/shade/renderer/particles/shaders/chunk_particle_curve_disabled.d.ts.map +0 -1
  219. package/src/shade/renderer/particles/shaders/chunk_particle_curve_disabled.js +0 -13
  220. package/src/shade/renderer/particles/shaders/chunk_particle_emitter_warmup.d.ts +0 -20
  221. package/src/shade/renderer/particles/shaders/chunk_particle_emitter_warmup.d.ts.map +0 -1
  222. package/src/shade/renderer/particles/shaders/chunk_particle_emitter_world_sphere.d.ts +0 -14
  223. package/src/shade/renderer/particles/shaders/chunk_particle_emitter_world_sphere.d.ts.map +0 -1
  224. package/src/shade/renderer/particles/shaders/shader_particle_reclaim.d.ts +0 -50
  225. package/src/shade/renderer/particles/shaders/shader_particle_reclaim.d.ts.map +0 -1
  226. package/src/shade/renderer/postprocess/ssr/reproject/shader_ffx_denoiser_reflections_reproject.d.ts +0 -17
  227. package/src/shade/renderer/postprocess/ssr/reproject/shader_ffx_denoiser_reflections_reproject.d.ts.map +0 -1
  228. /package/src/{shade/descriptor/util → core/model/object}/optional_clone.js +0 -0
@@ -0,0 +1,566 @@
1
+ import ConcurrentExecutor from "../../../core/process/executor/ConcurrentExecutor.js";
2
+ import Vector3 from "../../../core/geom/Vector3.js";
3
+ import { DEG_TO_RAD } from "../../../core/math/DEG_TO_RAD.js";
4
+ import { AssetManager } from "../../../engine/asset/AssetManager.js";
5
+ import { GameAssetType } from "../../../engine/asset/GameAssetType.js";
6
+ import { ImageRGBADataLoader } from "../../../engine/asset/loaders/image/ImageRGBADataLoader.js";
7
+ import { EntityComponentDataset } from "../../../engine/ecs/EntityComponentDataset.js";
8
+ import { EntityManager } from "../../../engine/ecs/EntityManager.js";
9
+ import { GPUParticleEmitterSystem } from "../../../engine/graphics3/GPUParticleEmitterSystem.js";
10
+ import { GraphicsEngine } from "../../../engine/graphics3/GraphicsEngine.js";
11
+ import { LightSystem } from "../../../engine/graphics3/LightSystem.js";
12
+ import { ShadedGeometrySystem } from "../../../engine/graphics3/ShadedGeometrySystem.js";
13
+ import { OrbitalCameraController } from "../../renderer/camera/OrbitalCameraController.js";
14
+ import { create_frame_loop } from "../../renderer/create_frame_loop.js";
15
+ import { install_profile_hotkey } from "../profile_hotkey.js";
16
+ import { PARTICLE_COUNTER_COUNT } from "../../renderer/particles/data/PARTICLE_COUNTERS.js";
17
+ import { Scene } from "../../renderer/scene/Scene.js";
18
+ import { ShadeTransparencyMode } from "../../renderer/ShadeTransparencyMode.js";
19
+ import { animate_bonfire, build_bonfire_world } from "./bonfire_world.js";
20
+ import { BONFIRE_EFFECT_NAMES, bonfire_sprite_urls } from "./bonfire_effects.js";
21
+ import { install_effect_editor } from "./bonfire_editor.js";
22
+ import { bonfire_sprites } from "./bonfire_sprites.js";
23
+ import { moonlit_environment } from "./moonlit_environment.js";
24
+
25
+ /**
26
+ * The page: a meep `EntityManager` with four systems on it, an entity per thing in the set, and a
27
+ * frame loop that ticks the one and draws the other.
28
+ *
29
+ * There is no particle code here at all. The fire is four entities, each carrying a `Transform64`
30
+ * and a `ParticleEffect`, and `GPUParticleEmitterSystem` is what makes them burn — the same way
31
+ * `ShadedGeometrySystem` makes the logs visible and `LightSystem` makes the moon shine. That
32
+ * symmetry is what the page exists to show, and it is the whole of the API being demonstrated:
33
+ *
34
+ * ```js
35
+ * new Entity()
36
+ * .add(new Transform64())
37
+ * .add(ParticleEffect.from({ ...effect, spawn_rate: 900 }))
38
+ * .build(dataset);
39
+ * ```
40
+ *
41
+ * Serve the workspace with `npm run dev --workspace @woosh/meep-engine` and open
42
+ * `/src/shade/playground/particle_ecs/index.html`. See README.md for what to look at and for the
43
+ * `?debug` query flag that swaps the frame loop for manual stepping.
44
+ */
45
+
46
+ /** Particles one press of "Stoke the fire" asks the ember emitter for. */
47
+ const STOKE_EMBERS = 900;
48
+
49
+ /** …and the flame, so the fire visibly flares rather than only throwing sparks. */
50
+ const STOKE_FLAME = 400;
51
+
52
+ /** How often the counters are copied back for the overlay. Display only — nothing sizes a pass. */
53
+ const STATS_INTERVAL_FRAMES = 20;
54
+
55
+ /** Bytes {@link capture_stats} copies: the particle counters, then the indirect draw args. */
56
+ const STATS_BYTES = 64;
57
+ const STATS_OFFSET_DRAW_ARGS = 48;
58
+
59
+ const CAMERA = {
60
+ position: new Vector3(2.9, 1.35, 3.7),
61
+ target: new Vector3(0, 1.0, 0),
62
+ fov_degrees: 48,
63
+ near: 0.05,
64
+ far: 300,
65
+ };
66
+
67
+ const element = id => document.getElementById(id);
68
+
69
+ const view_size = () => [Math.max(1, window.innerWidth), Math.max(1, window.innerHeight)];
70
+
71
+ async function main() {
72
+ // ---- the world, before any device -------------------------------------------------------
73
+ // Entities and components are plain data, so all of this is built and correct whether or not a
74
+ // WebGPU adapter ever turns up. The systems below are what need one.
75
+
76
+ const graphics = new GraphicsEngine();
77
+ const scene = new Scene();
78
+
79
+ // Before any system starts: `GraphicsEngine.set_scene` fills an environment in for a scene that
80
+ // has none, and its default is a daylight sky — which would leave the fire as a pale smudge.
81
+ scene.lights.environment = moonlit_environment();
82
+
83
+ element("viewport").appendChild(graphics.viewport.el);
84
+
85
+ const [initial_width, initial_height] = view_size();
86
+
87
+ graphics.viewport.size.set(initial_width, initial_height);
88
+
89
+ const assets = new AssetManager({ context: {}, executor: new ConcurrentExecutor() });
90
+
91
+ await assets.registerLoader(GameAssetType.Image, new ImageRGBADataLoader());
92
+
93
+ assets.startup();
94
+
95
+ const dataset = new EntityComponentDataset();
96
+ const entities = new EntityManager();
97
+
98
+ const particles = new GPUParticleEmitterSystem(graphics, scene, assets);
99
+
100
+ entities.addSystem(new ShadedGeometrySystem(graphics, scene));
101
+ entities.addSystem(new LightSystem(graphics, scene));
102
+ entities.addSystem(particles);
103
+ entities.attachDataset(dataset);
104
+
105
+ await new Promise((resolve, reject) => entities.startup(resolve, reject));
106
+
107
+ const sprites = bonfire_sprites();
108
+ const world = await build_bonfire_world(dataset, { sprites });
109
+
110
+ // ---- camera and controls ------------------------------------------------------------------
111
+ // Wired before the device on purpose: a browser without an adapter still gets a page that can be
112
+ // driven, and the failure it reports is the device's rather than a cascade of undefined handlers.
113
+
114
+ const camera = graphics.camera.camera;
115
+
116
+ camera.near = CAMERA.near;
117
+ camera.far = CAMERA.far;
118
+ camera.fov = CAMERA.fov_degrees * DEG_TO_RAD;
119
+
120
+ // document.body rather than the canvas: the controller starts a keyboard device, and only a
121
+ // focusable element receives key events
122
+ const controller = new OrbitalCameraController(camera, document.body);
123
+
124
+ controller.look(CAMERA.position, CAMERA.target);
125
+ controller.movement_speed_scale = 2;
126
+
127
+ document.body.focus();
128
+
129
+ /*
130
+ Which checkbox drives which effect, by index rather than by component: a live edit in the
131
+ particle editor REPLACES the component on the entity, so a handler that had captured one would
132
+ be writing to an object that left the dataset several edits ago.
133
+ */
134
+ for (let i = 0; i < BONFIRE_EFFECT_NAMES.length; i++) {
135
+ const id = BONFIRE_EFFECT_NAMES[i];
136
+
137
+ element(id).addEventListener("change", () => {
138
+ /*
139
+ The whole of turning an effect off: write the component. No system call, no node, no
140
+ remembering what the rate was — `emitting` holds the authored `spawn_rate` and the next
141
+ frame's comparison finds the change. Particles already alive burn out on their own,
142
+ which is what putting a fire out looks like.
143
+ */
144
+ world.effects[i].emitting = element(id).checked;
145
+ });
146
+ }
147
+
148
+ element("stoke").addEventListener("click", () => {
149
+ /*
150
+ And the whole of a one-shot: ask the entity. The request is queued and handed to the GPU
151
+ after the next frame's membership sweep, so it works before the first frame has run and it
152
+ goes through the same spawn path the continuous rate does.
153
+ */
154
+ particles.burst(world.embers, STOKE_EMBERS);
155
+ particles.burst(world.flame, STOKE_FLAME);
156
+ });
157
+
158
+ // ---- the particle editor --------------------------------------------------------------------
159
+ // E opens it over the fire. An edit recompiles the document and swaps the component on its
160
+ // entity, so what is on screen is the effect being edited rather than a preview of it.
161
+
162
+ const editor = install_effect_editor({
163
+ dataset,
164
+ world,
165
+ sprites: bonfire_sprite_urls(sprites),
166
+ host: element("effect_editor"),
167
+ selector: element("editor_effect"),
168
+ status: element("editor_status"),
169
+ mount: element("editor_mount"),
170
+ });
171
+
172
+ element("editor_close").addEventListener("click", close_editor);
173
+
174
+ element("editor_download").addEventListener("click", () => {
175
+ const source = editor.current();
176
+
177
+ download_json(`${source.name}.json`, source.toJSON());
178
+ });
179
+
180
+ element("editor_import").addEventListener("click", () => element("editor_file").click());
181
+
182
+ element("editor_file").addEventListener("change", async event => {
183
+ const file = event.target.files?.[0];
184
+
185
+ if (file === undefined) {
186
+ return;
187
+ }
188
+
189
+ try {
190
+ editor.import_json(JSON.parse(await file.text()));
191
+ } catch (e) {
192
+ console.error(e);
193
+ element("editor_status").textContent = `could not open ${file.name}: ${e.message}`;
194
+ element("editor_status").className = "failed";
195
+ }
196
+
197
+ // cleared, so opening the same file twice in a row fires again
198
+ event.target.value = "";
199
+ });
200
+
201
+ /**
202
+ * Leave the editor and hand the keyboard back to the scene.
203
+ *
204
+ * The focus matters: the camera controller listens on `body`, so coming back with the caret
205
+ * still in a spawn-rate field would leave WASD typing numbers into it.
206
+ */
207
+ function close_editor() {
208
+ if (!editor.is_open()) {
209
+ return;
210
+ }
211
+
212
+ editor.toggle();
213
+ document.body.focus();
214
+ }
215
+
216
+ /*
217
+ Bound in the CAPTURE phase, which is the difference between a way out and a trap: the editor is
218
+ a tree of views that stop propagation on the keys they own, and a listener on `window` in the
219
+ bubble phase is the last thing an event reaches — if it reaches it at all.
220
+ */
221
+ window.addEventListener("keydown", event => {
222
+ if (event.ctrlKey || event.metaKey || event.altKey || event.repeat) {
223
+ return;
224
+ }
225
+
226
+ const escape = event.key === "Escape" || event.code === "Escape";
227
+ const toggle = event.key.toLowerCase() === "e" || event.code === "KeyE";
228
+
229
+ if (!escape && !toggle) {
230
+ return;
231
+ }
232
+
233
+ /*
234
+ Escape leaves, from anywhere, including from inside a field. It types nothing, so there is
235
+ nothing for it to conflict with — and using the editor means clicking into its fields, after
236
+ which a rule that says "not while something is focused" means there is no key that gets you
237
+ out at all. That is the bug this replaces: E opened the editor and then never closed it.
238
+ */
239
+ if (escape) {
240
+ if (!editor.is_open()) {
241
+ return;
242
+ }
243
+
244
+ event.preventDefault();
245
+ close_editor();
246
+
247
+ return;
248
+ }
249
+
250
+ // E is a letter, so it belongs to whatever is being typed into — a text field, or a <select>,
251
+ // where letters jump to the option that starts with one. Escape is the way out of those.
252
+ const focused = event.target;
253
+
254
+ if (
255
+ focused instanceof HTMLInputElement
256
+ || focused instanceof HTMLTextAreaElement
257
+ || focused instanceof HTMLSelectElement
258
+ || (focused instanceof HTMLElement && focused.isContentEditable)
259
+ ) {
260
+ return;
261
+ }
262
+
263
+ event.preventDefault();
264
+
265
+ if (editor.is_open()) {
266
+ close_editor();
267
+ } else {
268
+ editor.toggle();
269
+ }
270
+ }, true);
271
+
272
+ /*
273
+ `particle_editor` rather than `editor`: an element with an id is already a global of that name,
274
+ so `globalThis.editor` reads as the <section> until this line runs and as the handle afterwards.
275
+ One name, one meaning.
276
+ */
277
+ Object.assign(globalThis, {
278
+ graphics, scene, dataset, entities, particles, world, camera, controller,
279
+ particle_editor: editor,
280
+ });
281
+
282
+ /**
283
+ * Where the camera is and what it orbits, as plain numbers: enough to put another instance of
284
+ * this page in exactly the same view.
285
+ *
286
+ * @returns {{position: number[], target: number[], fov_degrees: number}}
287
+ */
288
+ globalThis.camera_state = () => {
289
+ const p = camera.transform;
290
+ const t = controller.target;
291
+ const round = v => Math.round(v * 1000) / 1000;
292
+
293
+ return {
294
+ position: [round(p.translation_x), round(p.translation_y), round(p.translation_z)],
295
+ target: [round(t.x), round(t.y), round(t.z)],
296
+ fov_degrees: round(camera.fov / DEG_TO_RAD),
297
+ };
298
+ };
299
+
300
+ /**
301
+ * Put the camera where a {@link camera_state} says.
302
+ *
303
+ * @param {{position: number[], target: number[], fov_degrees?: number}|string} state
304
+ */
305
+ globalThis.apply_camera = state => {
306
+ const parsed = typeof state === "string" ? JSON.parse(state) : state;
307
+
308
+ if (parsed.fov_degrees !== undefined) {
309
+ camera.fov = parsed.fov_degrees * DEG_TO_RAD;
310
+ }
311
+
312
+ controller.look(
313
+ new Vector3(parsed.position[0], parsed.position[1], parsed.position[2]),
314
+ new Vector3(parsed.target[0], parsed.target[1], parsed.target[2])
315
+ );
316
+ };
317
+
318
+ // ---- the device, last ---------------------------------------------------------------------
319
+
320
+ graphics.on.contextFailed.add(failure => show_message(
321
+ "There will be no rendering.\n\n" + failure.message
322
+ + "\n\nThe page needs WebGPU with subgroups and immediate data, over http://localhost — "
323
+ + "a secure context, and not file://."
324
+ ));
325
+
326
+ try {
327
+ await graphics.start();
328
+ } catch (e) {
329
+ console.error(e);
330
+
331
+ return;
332
+ }
333
+
334
+ const renderer = graphics.renderer;
335
+
336
+ globalThis.renderer = renderer;
337
+
338
+ // The particles are drawn inside the AVBOIT transparency pipeline as a side channel of the
339
+ // transparent meshes. MBOIT is the legacy path and has no particle channel.
340
+ renderer.transparency_mode = ShadeTransparencyMode.AVBOIT;
341
+
342
+ // Eye adaptation drifts for seconds after any change, which makes a fire impossible to judge —
343
+ // it is the brightest thing in a dark frame, so the adaptation chases it as it flickers.
344
+ renderer.feature_automatic_exposure_enabled = false;
345
+ renderer.exposure_compensation = 0;
346
+
347
+ const device = renderer.graphics.device;
348
+
349
+ /** @type {(() => void)|null} */
350
+ let stop_loop = null;
351
+
352
+ device.lost?.then(info => {
353
+ stop_loop?.();
354
+ show_message("The GPU device was lost.\n\n" + (info?.message ?? ""));
355
+ });
356
+
357
+ // ---- profiling ------------------------------------------------------------------------------
358
+ // T records a GPU capture and T again saves it. Worth having on this page in particular: four
359
+ // distinct VM programs means the simulate pass runs four coherence buckets, and what that costs
360
+ // relative to the emit, the bounds and the three AVBOIT passes is not a thing anybody can guess.
361
+
362
+ install_profile_hotkey({
363
+ renderer,
364
+ name: "particle_ecs",
365
+ status: element("profile_status"),
366
+ settings: () => [
367
+ `${particles.emitters.length} emitters`,
368
+ BONFIRE_EFFECT_NAMES.filter((_, i) => world.effects[i].emitting).join("+") || "none emitting",
369
+ `${globalThis.__stats?.alive ?? "?"} alive`,
370
+ ],
371
+ });
372
+
373
+ // ---- frame loop -----------------------------------------------------------------------------
374
+
375
+ const stats_buffer = device.createBuffer({
376
+ label: "bonfire/stats",
377
+ size: STATS_BYTES,
378
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
379
+ });
380
+
381
+ let stats_pending = false;
382
+ let frame_index = 0;
383
+ let time = 0;
384
+ let fps = 0;
385
+
386
+ /**
387
+ * @param {number} dt seconds
388
+ */
389
+ function frame(dt) {
390
+ time += dt;
391
+ fps = dt > 0 ? fps * 0.9 + 0.1 / dt : fps;
392
+
393
+ const [width, height] = view_size();
394
+
395
+ if (width !== graphics.viewport.size.x || height !== graphics.viewport.size.y) {
396
+ graphics.viewport.size.set(width, height);
397
+ }
398
+
399
+ animate_bonfire(world, time);
400
+ controller.update();
401
+
402
+ // The simulation tick: every system's `update`, in the order the manager resolved. The
403
+ // particle system's is where the four components are compared against their emitters.
404
+ entities.update(dt);
405
+
406
+ /*
407
+ Nothing is drawn while the editor is up. It covers the window, so a frame underneath it is
408
+ a whole scene — the transparency pipeline, the particle passes, the resolve — composited
409
+ behind an opaque panel and then thrown away. Measured at 1075x1216, the trickle this
410
+ replaces ran 9.8 of them a second, each costing 3.5 ms of CPU to encode and about 21 ms
411
+ from submission to completion — so a fifth of every second went to pixels nobody could
412
+ see. The machine goes to the graph editor instead, which is the thing you are looking at.
413
+
414
+ An edit still reaches the GPU, it just waits for the way back: GPUParticleEmitterSystem
415
+ publishes the emitter table from `prepare` at FrameStart rather than from the tick above,
416
+ and that is idempotent by design, because a frame must not depend on a simulation tick
417
+ having preceded it. So the first frame after Escape carries every edit made while the
418
+ editor was open.
419
+
420
+ What this gives up is that the fire no longer advances behind the editor — the simulation
421
+ is GPU work, so you come back to the instant you left rather than a few seconds on. An edit
422
+ kills and restarts the effect anyway, which is where that would otherwise have shown.
423
+ */
424
+ if (!editor.is_open()) {
425
+ graphics.render(dt);
426
+ }
427
+
428
+ frame_index++;
429
+
430
+ /*
431
+ The counters only move on a drawn frame and the panel showing them is behind the editor, so
432
+ while it is open this would be a copy and a map round trip for numbers nobody can see that
433
+ have not changed since the last one.
434
+ */
435
+ if (!editor.is_open() && !stats_pending && frame_index % STATS_INTERVAL_FRAMES === 0) {
436
+ capture_stats();
437
+ }
438
+ }
439
+
440
+ /** Read the counters back to display them. Submitted on its own, after the frame's own work. */
441
+ function capture_stats() {
442
+ const system = renderer.particles(renderer.scenes.obtain(scene));
443
+ const buffers = system.buffers;
444
+ const encoder = device.createCommandEncoder({ label: "bonfire/stats" });
445
+
446
+ encoder.copyBufferToBuffer(buffers.counters, 0, stats_buffer, 0, PARTICLE_COUNTER_COUNT * 4);
447
+ encoder.copyBufferToBuffer(buffers.draw_args, 0, stats_buffer, STATS_OFFSET_DRAW_ARGS, 4 * 4);
448
+
449
+ device.queue.submit([encoder.finish()]);
450
+
451
+ stats_pending = true;
452
+
453
+ stats_buffer.mapAsync(GPUMapMode.READ).then(() => {
454
+ const words = new Uint32Array(stats_buffer.getMappedRange().slice(0));
455
+
456
+ stats_buffer.unmap();
457
+ stats_pending = false;
458
+
459
+ globalThis.__stats = {
460
+ frame: frame_index,
461
+ fps,
462
+ alive: words[0],
463
+ dead: words[1],
464
+ draw: words[STATS_OFFSET_DRAW_ARGS / 4 + 1],
465
+ emitters: particles.emitters.length,
466
+ };
467
+
468
+ write_hud(globalThis.__stats);
469
+ }).catch(() => {
470
+ stats_pending = false;
471
+ });
472
+ }
473
+
474
+ /**
475
+ * One frame, with the loop stopped on the first failure.
476
+ *
477
+ * A lost device makes `render` throw, and the frame loop re-arms itself before it calls back —
478
+ * so without this the page would throw once per frame for ever, with the first and most
479
+ * informative failure scrolled far out of reach.
480
+ *
481
+ * @param {number} dt
482
+ */
483
+ function guarded_frame(dt) {
484
+ try {
485
+ frame(dt);
486
+ } catch (e) {
487
+ stop_loop?.();
488
+ console.error(e);
489
+ show_message("The frame failed and the loop has stopped.\n\n" + e.message);
490
+ }
491
+ }
492
+
493
+ if (new URLSearchParams(location.search).has("debug")) {
494
+ // Drive frames by hand when the tab is hidden (rAF suspended) — headless verification.
495
+ // Nothing is scheduled: a stray rAF frame interleaved with __step would make the count the
496
+ // caller asked for and the count that ran two different numbers.
497
+ globalThis.__step = (n = 1, dt = 1 / 60) => {
498
+ for (let i = 0; i < n; i++) {
499
+ guarded_frame(dt);
500
+ }
501
+
502
+ return frame_index;
503
+ };
504
+
505
+ return;
506
+ }
507
+
508
+ stop_loop = create_frame_loop(dt => guarded_frame(Math.min(dt, 0.05)));
509
+ }
510
+
511
+ /**
512
+ * @param {{fps: number, alive: number, dead: number, draw: number, emitters: number}} stats
513
+ */
514
+ function write_hud({ fps, alive, dead, draw, emitters }) {
515
+ const put = (id, value) => {
516
+ const node = element(id);
517
+
518
+ if (node !== null) {
519
+ node.textContent = value;
520
+ }
521
+ };
522
+
523
+ put("stat-fps", fps.toFixed(0));
524
+ put("stat-alive", alive.toLocaleString());
525
+ put("stat-dead", dead.toLocaleString());
526
+ put("stat-draw", draw.toLocaleString());
527
+ put("stat-emitters", String(emitters));
528
+ }
529
+
530
+ /**
531
+ * Hand the browser a file. The editor's Download button is the only thing here that produces one,
532
+ * and it is the whole of the round trip: what comes out is what `effects/` holds.
533
+ *
534
+ * @param {string} filename
535
+ * @param {object} json
536
+ */
537
+ function download_json(filename, json) {
538
+ const url = URL.createObjectURL(
539
+ new Blob([JSON.stringify(json, null, 2)], { type: "application/json" })
540
+ );
541
+
542
+ const anchor = document.createElement("a");
543
+
544
+ anchor.href = url;
545
+ anchor.download = filename;
546
+
547
+ document.body.appendChild(anchor);
548
+ anchor.click();
549
+ anchor.remove();
550
+
551
+ // Revoking in the same task cancels the download in some browsers — the click only schedules it.
552
+ setTimeout(() => URL.revokeObjectURL(url), 60000);
553
+ }
554
+
555
+ function show_message(text) {
556
+ const node = element("message");
557
+
558
+ if (node !== null) {
559
+ node.textContent = text;
560
+ }
561
+ }
562
+
563
+ main().catch(e => {
564
+ console.error(e);
565
+ show_message("The bonfire playground failed to start — see the console.\n\n" + e.message);
566
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The environment map. Assign it to `scene.lights.environment`.
3
+ *
4
+ * @returns {ShadeTexture}
5
+ */
6
+ export function moonlit_environment(): ShadeTexture;
7
+ /**
8
+ * Where the moon is. Unit, +Y up — the direction light arrives *from*, so the shadows fall the
9
+ * other way.
10
+ *
11
+ * @type {Vector3}
12
+ */
13
+ export const MOON_DIRECTION: Vector3;
14
+ import { ShadeTexture } from "../../renderer/texture/ShadeTexture.js";
15
+ import Vector3 from "../../../core/geom/Vector3.js";
16
+ //# sourceMappingURL=moonlit_environment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moonlit_environment.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/particle_ecs/moonlit_environment.js"],"names":[],"mappings":"AAyGA;;;;GAIG;AACH,uCAFa,YAAY,CAmCxB;AA9GD;;;;;GAKG;AACH,6BAFU,OAAO,CAEyD;6BAjC7C,wCAAwC;oBANjD,+BAA+B"}