@woosh/meep-engine 3.18.0 → 3.19.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 (120) hide show
  1. package/build/bundle-worker-image-decoder.js +1 -1
  2. package/editor/view/particles/effect/ParticleCurveEditorView.d.ts.map +1 -1
  3. package/editor/view/particles/effect/ParticleCurveEditorView.js +303 -120
  4. package/editor/view/particles/effect/ParticleGradientEditorView.d.ts.map +1 -1
  5. package/editor/view/particles/effect/ParticleGradientEditorView.js +108 -63
  6. package/editor/view/particles/effect/ParticleGraphEditorView.js +1 -1
  7. package/editor/view/particles/effect/ParticleNodeParametersView.d.ts.map +1 -1
  8. package/editor/view/particles/effect/ParticleNodeParametersView.js +3 -1
  9. package/editor/view/particles/effect/particle-editor.css +60 -30
  10. package/package.json +1 -2
  11. package/samples/engine/README.md +1 -1
  12. package/src/core/binary/compression/decompress_bytes.d.ts +13 -0
  13. package/src/core/binary/compression/decompress_bytes.d.ts.map +1 -0
  14. package/src/core/binary/compression/decompress_bytes.js +28 -0
  15. package/src/core/model/node-graph/visual/layout/layout_assign_coordinates.js +67 -22
  16. package/src/engine/asset/loaders/image/ImageDecoderWorker.js +12 -27
  17. package/src/engine/asset/loaders/image/prototypePNG.js +8 -7
  18. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  19. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +4 -0
  20. package/src/engine/graphics/ecs/particles/ParticleEffect.d.ts +266 -0
  21. package/src/engine/graphics/ecs/particles/ParticleEffect.d.ts.map +1 -0
  22. package/src/engine/graphics/ecs/particles/ParticleEffect.js +455 -0
  23. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.d.ts +58 -0
  24. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.d.ts.map +1 -0
  25. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.js +219 -0
  26. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +178 -25
  27. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
  28. package/src/engine/graphics3/GPUParticleEmitterSystem.js +809 -310
  29. package/src/format/image/png/PNGReader.d.ts +7 -6
  30. package/src/format/image/png/PNGReader.d.ts.map +1 -1
  31. package/src/format/image/png/PNGReader.js +13 -12
  32. package/src/format/image/png/chunk/png_chunk_decode_iTXt.d.ts +3 -2
  33. package/src/format/image/png/chunk/png_chunk_decode_iTXt.d.ts.map +1 -1
  34. package/src/format/image/png/chunk/png_chunk_decode_iTXt.js +5 -4
  35. package/src/format/image/png/chunk/png_chunk_decode_zTXt.d.ts +3 -2
  36. package/src/format/image/png/chunk/png_chunk_decode_zTXt.d.ts.map +1 -1
  37. package/src/format/image/png/chunk/png_chunk_decode_zTXt.js +5 -4
  38. package/src/format/image/png/png_inflate.d.ts +3 -3
  39. package/src/format/image/png/png_inflate.d.ts.map +1 -1
  40. package/src/format/image/png/png_inflate.js +29 -39
  41. package/src/format/texture/ktx2/ktx2_read.d.ts +4 -4
  42. package/src/format/texture/ktx2/ktx2_read.d.ts.map +1 -1
  43. package/src/format/texture/ktx2/ktx2_read.js +18 -21
  44. package/src/shade/playground/particle_ecs/README.md +203 -0
  45. package/src/shade/playground/particle_ecs/bonfire_editor.d.ts +39 -0
  46. package/src/shade/playground/particle_ecs/bonfire_editor.d.ts.map +1 -0
  47. package/src/shade/playground/particle_ecs/bonfire_editor.js +315 -0
  48. package/src/shade/playground/particle_ecs/bonfire_effects.d.ts +145 -0
  49. package/src/shade/playground/particle_ecs/bonfire_effects.d.ts.map +1 -0
  50. package/src/shade/playground/particle_ecs/bonfire_effects.js +202 -0
  51. package/src/shade/playground/particle_ecs/bonfire_sprites.d.ts +23 -0
  52. package/src/shade/playground/particle_ecs/bonfire_sprites.d.ts.map +1 -0
  53. package/src/shade/playground/particle_ecs/bonfire_sprites.js +315 -0
  54. package/src/shade/playground/particle_ecs/bonfire_world.d.ts +86 -0
  55. package/src/shade/playground/particle_ecs/bonfire_world.d.ts.map +1 -0
  56. package/src/shade/playground/particle_ecs/bonfire_world.js +303 -0
  57. package/src/shade/playground/particle_ecs/effects/embers.json +1634 -0
  58. package/src/shade/playground/particle_ecs/effects/flame.json +1882 -0
  59. package/src/shade/playground/particle_ecs/effects/smoke.json +1860 -0
  60. package/src/shade/playground/particle_ecs/effects/soot.json +1606 -0
  61. package/src/shade/playground/particle_ecs/index.html +330 -0
  62. package/src/shade/playground/particle_ecs/main.d.ts +2 -0
  63. package/src/shade/playground/particle_ecs/main.d.ts.map +1 -0
  64. package/src/shade/playground/particle_ecs/main.js +566 -0
  65. package/src/shade/playground/particle_ecs/moonlit_environment.d.ts +16 -0
  66. package/src/shade/playground/particle_ecs/moonlit_environment.d.ts.map +1 -0
  67. package/src/shade/playground/particle_ecs/moonlit_environment.js +144 -0
  68. package/src/shade/playground/particle_editor/README.md +5 -4
  69. package/src/shade/playground/profile_hotkey.d.ts +58 -0
  70. package/src/shade/playground/profile_hotkey.d.ts.map +1 -0
  71. package/src/shade/playground/profile_hotkey.js +325 -0
  72. package/src/shade/playground/ssr_variance/README.md +105 -0
  73. package/src/shade/playground/ssr_variance/capture.d.ts +16 -0
  74. package/src/shade/playground/ssr_variance/capture.d.ts.map +1 -0
  75. package/src/shade/playground/ssr_variance/capture.js +96 -0
  76. package/src/shade/playground/ssr_variance/index.html +22 -0
  77. package/src/shade/playground/ssr_variance/main.d.ts +2 -0
  78. package/src/shade/playground/ssr_variance/main.d.ts.map +1 -0
  79. package/src/shade/playground/ssr_variance/main.js +246 -0
  80. package/src/shade/playground/ssr_variance/reference.d.ts +18 -0
  81. package/src/shade/playground/ssr_variance/reference.d.ts.map +1 -0
  82. package/src/shade/playground/ssr_variance/reference.js +78 -0
  83. package/src/shade/playground/ssr_variance/scene.d.ts +11 -0
  84. package/src/shade/playground/ssr_variance/scene.d.ts.map +1 -0
  85. package/src/shade/playground/ssr_variance/scene.js +61 -0
  86. package/src/shade/playground/ssr_variance/statistics.d.ts +44 -0
  87. package/src/shade/playground/ssr_variance/statistics.d.ts.map +1 -0
  88. package/src/shade/playground/ssr_variance/statistics.js +51 -0
  89. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts.map +1 -1
  90. package/src/shade/renderer/loader/gltf/tiny-gltf.js +9 -3
  91. package/src/shade/renderer/loader/usd/usd_decode_image.d.ts +4 -4
  92. package/src/shade/renderer/loader/usd/usd_decode_image.d.ts.map +1 -1
  93. package/src/shade/renderer/loader/usd/usd_decode_image.js +4 -4
  94. package/src/shade/renderer/particles/DESIGN.md +748 -703
  95. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts +11 -4
  96. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts.map +1 -1
  97. package/src/shade/renderer/particles/runtime/ParticleEmitter.js +431 -424
  98. package/src/shade/renderer/postprocess/ssr/SSR.d.ts.map +1 -1
  99. package/src/shade/renderer/postprocess/ssr/SSR.js +2 -1
  100. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_reprojection.d.ts.map +1 -1
  101. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_reprojection.js +0 -6
  102. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_sample_history.d.ts.map +1 -1
  103. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_sample_history.js +19 -5
  104. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_temporal_accumulate.d.ts +4 -0
  105. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_temporal_accumulate.d.ts.map +1 -0
  106. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_temporal_accumulate.js +133 -0
  107. package/src/shade/renderer/postprocess/ssr/ssr_reproject_shader.d.ts +0 -8
  108. package/src/shade/renderer/postprocess/ssr/ssr_reproject_shader.d.ts.map +1 -1
  109. package/src/shade/renderer/postprocess/ssr/ssr_reproject_shader.js +16 -113
  110. package/src/shade/renderer/texture/source/texel_data_from_ktx2.d.ts +2 -2
  111. package/src/shade/renderer/texture/source/texel_data_from_ktx2.d.ts.map +1 -1
  112. package/src/shade/renderer/texture/source/texel_data_from_ktx2.js +3 -3
  113. package/src/shade/renderer/particles/shaders/chunk_particle_emitter_warmup.d.ts +0 -20
  114. package/src/shade/renderer/particles/shaders/chunk_particle_emitter_warmup.d.ts.map +0 -1
  115. package/src/shade/renderer/particles/shaders/chunk_particle_emitter_world_sphere.d.ts +0 -14
  116. package/src/shade/renderer/particles/shaders/chunk_particle_emitter_world_sphere.d.ts.map +0 -1
  117. package/src/shade/renderer/particles/shaders/shader_particle_reclaim.d.ts +0 -50
  118. package/src/shade/renderer/particles/shaders/shader_particle_reclaim.d.ts.map +0 -1
  119. package/src/shade/renderer/postprocess/ssr/reproject/shader_ffx_denoiser_reflections_reproject.d.ts +0 -17
  120. package/src/shade/renderer/postprocess/ssr/reproject/shader_ffx_denoiser_reflections_reproject.d.ts.map +0 -1
@@ -0,0 +1,145 @@
1
+ /**
2
+ * @returns {NodeDescriptionVisualRegistry}
3
+ */
4
+ export function particle_node_visuals(): NodeDescriptionVisualRegistry;
5
+ /**
6
+ * Open one of the saved effects as a document.
7
+ *
8
+ * @param {string} name see {@link BONFIRE_EFFECT_NAMES}
9
+ * @param {object} [json] an effect to open instead of the saved one — what the page hands over when
10
+ * somebody imports a file they downloaded earlier
11
+ * @returns {ParticleEffectDocument}
12
+ */
13
+ export function bonfire_effect_document(name: string, json?: object): ParticleEffectDocument;
14
+ /**
15
+ * All four, as documents.
16
+ *
17
+ * @returns {Object<string, ParticleEffectDocument>}
18
+ */
19
+ export function bonfire_effect_documents(): {
20
+ [x: string]: ParticleEffectDocument;
21
+ };
22
+ /**
23
+ * The ECS component a document describes, compiled.
24
+ *
25
+ * `null` when the effect does not compile. A graph an author has half-rewired is the ordinary case
26
+ * for that, so it is the caller's business to say so rather than this one's to throw — which is why
27
+ * the page can leave the previous effect burning while the editor shows the error.
28
+ *
29
+ * @param {ParticleEffectDocument} document
30
+ * @param {string|null} [texture] image URL to draw with, used when the document names none of its
31
+ * own. The page's sprites are generated at runtime, so they cannot be in the file
32
+ * @returns {ParticleEffect|null}
33
+ */
34
+ export function bonfire_effect_component(document: ParticleEffectDocument, texture?: string | null): ParticleEffect | null;
35
+ /**
36
+ * The four effects, ready to go on entities.
37
+ *
38
+ * @param {object} [textures] image URLs to draw with, `null` for an untextured (flat quad) effect
39
+ * @param {string|null} [textures.flame]
40
+ * @param {string|null} [textures.smoke]
41
+ * @param {string|null} [textures.ember]
42
+ * @param {string|null} [textures.soot]
43
+ * @returns {{flame: ParticleEffect, smoke: ParticleEffect, embers: ParticleEffect, soot: ParticleEffect}}
44
+ */
45
+ export function bonfire_effects(textures?: {
46
+ flame?: string | null;
47
+ smoke?: string | null;
48
+ ember?: string | null;
49
+ soot?: string | null;
50
+ }): {
51
+ flame: ParticleEffect;
52
+ smoke: ParticleEffect;
53
+ embers: ParticleEffect;
54
+ soot: ParticleEffect;
55
+ };
56
+ /**
57
+ * The sprite URLs keyed the way the effects are, so the page and the editor agree on which image
58
+ * belongs to which effect. `bonfire_sprites()` names the ember one in the singular.
59
+ *
60
+ * @param {object} [textures]
61
+ * @returns {Object<string, string|null>}
62
+ */
63
+ export function bonfire_sprite_urls(textures?: object): {
64
+ [x: string]: string | null;
65
+ };
66
+ /**
67
+ * A bonfire, as four saved particle effects.
68
+ *
69
+ * Flame, smoke, embers and soot: four effects that read as one fire because they were authored
70
+ * against one another — the smoke starts where the flame stops being bright, the embers leave from
71
+ * inside the flame core and outlive it, and the soot rides the same plume as the smoke and then
72
+ * falls out of it.
73
+ *
74
+ * ## They are documents, not code
75
+ *
76
+ * Each is a {@link ParticleEffectDocument} saved as JSON in `effects/`: the per-particle attribute
77
+ * declarations, the INIT and UPDATE node graphs with the positions they were arranged at, and the
78
+ * emitter settings. That is the whole effect, and it is what the editor opens — press `E` on the
79
+ * page, change something, download the result, drop it over the file, reload. There is no porting
80
+ * step and no authoring code to keep in step with what was edited, because the file the editor
81
+ * writes **is** the effect.
82
+ *
83
+ * Which is the same claim `ParticleEffectSerializationAdapter` makes about a level: the VM's
84
+ * instruction set is a durable format, so an effect is content. These documents keep the *graph*
85
+ * rather than the bytecode because they are the authoring form — a level saves the compiled program,
86
+ * an author's file saves what it was compiled from.
87
+ *
88
+ * ## How each effect is built
89
+ *
90
+ * Every one of them has the same skeleton, which is the skeleton of nearly every particle effect:
91
+ *
92
+ * - **INIT** places the particle, gives it a velocity, and rolls its lifetime, its base size and a
93
+ * personal `seed`. There is no built-in age or lifetime — the effect declares what it needs, which
94
+ * is why `age` and `life` are ordinary attributes.
95
+ * - **UPDATE** advances the age, kills the particle when it runs out, integrates the forces, and
96
+ * drives the drawn size and colour from `age / life` through a ramp — a curve for the size and a
97
+ * gradient for the colour, both written into the program as arithmetic rather than sampled from a
98
+ * table the host has to keep in step.
99
+ *
100
+ * Two things are worth knowing before editing one:
101
+ *
102
+ * - **One store per attribute per phase.** Every store runs and nothing orders them, so a second
103
+ * store does not refine the first, it replaces it — which is how the smoke's drag silently ate its
104
+ * own velocity integration until the editor's validator was pointed at it. The graph editor calls
105
+ * that an error; nothing at runtime does.
106
+ * - **The register budget is shared.** The VM keeps its file in shader registers, and one program
107
+ * needing more than `PARTICLE_VM_FAST_REGISTER_SLOTS` swaps emit and simulate for storage-backed
108
+ * variants *for the whole system* — so an effect that grows past it slows down every effect in the
109
+ * scene rather than itself. `bonfire.spec.js` pins it.
110
+ *
111
+ * The turbulence in all four is three decorrelated `simplex3` samples read as a vector, at a point
112
+ * that drifts with time so the field scrolls through the rising column. Deliberately not the
113
+ * library's `curl3`, which is twelve samples for a divergence-free field: over a metre of flame
114
+ * nobody can tell, and the budget above is shared.
115
+ *
116
+ * ## Lighting
117
+ *
118
+ * None of these opt into `EMITTER_FLAG.LIGHTING`, and that is not an oversight: the shipped lighting
119
+ * hook is `chunk_particle_lighting_unlit`, a pass-through, so the flag would cost a bit in a record
120
+ * and change no pixel. The smoke and the soot are *painted* lit instead — their gradients carry the
121
+ * fire's warmth low down where the fire would light them and the moon's blue higher up, over the
122
+ * particle's own age. When the lit hook lands the gradients are what to reconsider.
123
+ */
124
+ /**
125
+ * The four saved documents, by the key the page knows them by.
126
+ *
127
+ * Exported as the raw JSON rather than as documents, because a document is mutable and every caller
128
+ * that opens one wants its own.
129
+ *
130
+ * @type {Object<string, object>}
131
+ */
132
+ export const BONFIRE_EFFECT_JSON: {
133
+ [x: string]: object;
134
+ };
135
+ /**
136
+ * The order the page offers them in, which is also least to most incidental: the flame is the thing,
137
+ * and the soot is the detail that makes it read.
138
+ *
139
+ * @type {string[]}
140
+ */
141
+ export const BONFIRE_EFFECT_NAMES: string[];
142
+ import { NodeDescriptionVisualRegistry } from "../../../core/model/node-graph/visual/NodeDescriptionVisualRegistry.js";
143
+ import { ParticleEffectDocument } from "../../../../editor/particles/effect/ParticleEffectDocument.js";
144
+ import { ParticleEffect } from "../../../engine/graphics/ecs/particles/ParticleEffect.js";
145
+ //# sourceMappingURL=bonfire_effects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bonfire_effects.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/particle_ecs/bonfire_effects.js"],"names":[],"mappings":"AAyGA;;GAEG;AACH,yCAFa,6BAA6B,CAUzC;AAED;;;;;;;GAOG;AACH,8CALW,MAAM,SACN,MAAM,GAEJ,sBAAsB,CAIlC;AAED;;;;GAIG;AACH;QAFoB,MAAM,GAAE,sBAAsB;EAIjD;AAED;;;;;;;;;;;GAWG;AACH,mDALW,sBAAsB,YACtB,MAAM,GAAC,IAAI,GAET,cAAc,GAAC,IAAI,CAY/B;AAED;;;;;;;;;GASG;AACH;IANkC,KAAK,GAA5B,MAAM,GAAC,IAAI;IACY,KAAK,GAA5B,MAAM,GAAC,IAAI;IACY,KAAK,GAA5B,MAAM,GAAC,IAAI;IACY,IAAI,GAA3B,MAAM,GAAC,IAAI;IACT;IAAC,KAAK,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,cAAc,CAAA;CAAC,CAcxG;AAED;;;;;;GAMG;AACH,+CAHW,MAAM;QACG,MAAM,GAAE,MAAM,GAAC,IAAI;EAStC;AA7LD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAEH;;;;;;;GAOG;AACH;QAFiB,MAAM,GAAE,MAAM;EAO5B;AAEH;;;;;GAKG;AACH,mCAFU,MAAM,EAAE,CAEsE;8CA1FjF,wEAAwE;uCAExC,+DAA+D;+BADvE,0DAA0D"}
@@ -0,0 +1,202 @@
1
+ import {
2
+ NodeDescriptionVisualRegistry
3
+ } from "../../../core/model/node-graph/visual/NodeDescriptionVisualRegistry.js";
4
+ import { ParticleEffect } from "../../../engine/graphics/ecs/particles/ParticleEffect.js";
5
+ import { ParticleEffectDocument } from "../../../../editor/particles/effect/ParticleEffectDocument.js";
6
+ import { PARTICLE_NODE_REGISTRY } from "../../renderer/particles/graph/ParticleNodeRegistry.js";
7
+ import "../../renderer/particles/graph/particle_node_library.js";
8
+ import flame_json from "./effects/flame.json";
9
+ import smoke_json from "./effects/smoke.json";
10
+ import embers_json from "./effects/embers.json";
11
+ import soot_json from "./effects/soot.json";
12
+
13
+ /**
14
+ * A bonfire, as four saved particle effects.
15
+ *
16
+ * Flame, smoke, embers and soot: four effects that read as one fire because they were authored
17
+ * against one another — the smoke starts where the flame stops being bright, the embers leave from
18
+ * inside the flame core and outlive it, and the soot rides the same plume as the smoke and then
19
+ * falls out of it.
20
+ *
21
+ * ## They are documents, not code
22
+ *
23
+ * Each is a {@link ParticleEffectDocument} saved as JSON in `effects/`: the per-particle attribute
24
+ * declarations, the INIT and UPDATE node graphs with the positions they were arranged at, and the
25
+ * emitter settings. That is the whole effect, and it is what the editor opens — press `E` on the
26
+ * page, change something, download the result, drop it over the file, reload. There is no porting
27
+ * step and no authoring code to keep in step with what was edited, because the file the editor
28
+ * writes **is** the effect.
29
+ *
30
+ * Which is the same claim `ParticleEffectSerializationAdapter` makes about a level: the VM's
31
+ * instruction set is a durable format, so an effect is content. These documents keep the *graph*
32
+ * rather than the bytecode because they are the authoring form — a level saves the compiled program,
33
+ * an author's file saves what it was compiled from.
34
+ *
35
+ * ## How each effect is built
36
+ *
37
+ * Every one of them has the same skeleton, which is the skeleton of nearly every particle effect:
38
+ *
39
+ * - **INIT** places the particle, gives it a velocity, and rolls its lifetime, its base size and a
40
+ * personal `seed`. There is no built-in age or lifetime — the effect declares what it needs, which
41
+ * is why `age` and `life` are ordinary attributes.
42
+ * - **UPDATE** advances the age, kills the particle when it runs out, integrates the forces, and
43
+ * drives the drawn size and colour from `age / life` through a ramp — a curve for the size and a
44
+ * gradient for the colour, both written into the program as arithmetic rather than sampled from a
45
+ * table the host has to keep in step.
46
+ *
47
+ * Two things are worth knowing before editing one:
48
+ *
49
+ * - **One store per attribute per phase.** Every store runs and nothing orders them, so a second
50
+ * store does not refine the first, it replaces it — which is how the smoke's drag silently ate its
51
+ * own velocity integration until the editor's validator was pointed at it. The graph editor calls
52
+ * that an error; nothing at runtime does.
53
+ * - **The register budget is shared.** The VM keeps its file in shader registers, and one program
54
+ * needing more than `PARTICLE_VM_FAST_REGISTER_SLOTS` swaps emit and simulate for storage-backed
55
+ * variants *for the whole system* — so an effect that grows past it slows down every effect in the
56
+ * scene rather than itself. `bonfire.spec.js` pins it.
57
+ *
58
+ * The turbulence in all four is three decorrelated `simplex3` samples read as a vector, at a point
59
+ * that drifts with time so the field scrolls through the rising column. Deliberately not the
60
+ * library's `curl3`, which is twelve samples for a divergence-free field: over a metre of flame
61
+ * nobody can tell, and the budget above is shared.
62
+ *
63
+ * ## Lighting
64
+ *
65
+ * None of these opt into `EMITTER_FLAG.LIGHTING`, and that is not an oversight: the shipped lighting
66
+ * hook is `chunk_particle_lighting_unlit`, a pass-through, so the flag would cost a bit in a record
67
+ * and change no pixel. The smoke and the soot are *painted* lit instead — their gradients carry the
68
+ * fire's warmth low down where the fire would light them and the moon's blue higher up, over the
69
+ * particle's own age. When the lit hook lands the gradients are what to reconsider.
70
+ */
71
+
72
+ /**
73
+ * The four saved documents, by the key the page knows them by.
74
+ *
75
+ * Exported as the raw JSON rather than as documents, because a document is mutable and every caller
76
+ * that opens one wants its own.
77
+ *
78
+ * @type {Object<string, object>}
79
+ */
80
+ export const BONFIRE_EFFECT_JSON = Object.freeze({
81
+ flame: flame_json,
82
+ smoke: smoke_json,
83
+ embers: embers_json,
84
+ soot: soot_json,
85
+ });
86
+
87
+ /**
88
+ * The order the page offers them in, which is also least to most incidental: the flame is the thing,
89
+ * and the soot is the detail that makes it read.
90
+ *
91
+ * @type {string[]}
92
+ */
93
+ export const BONFIRE_EFFECT_NAMES = Object.freeze(["flame", "smoke", "embers", "soot"]);
94
+
95
+ /**
96
+ * A visual registry covering every particle node type, which a document needs in order to size the
97
+ * nodes it loads.
98
+ *
99
+ * One, shared and built on demand: it is a function of the node library rather than of any document,
100
+ * and building it per document would generate a visual description per node type per effect.
101
+ *
102
+ * @type {NodeDescriptionVisualRegistry|null}
103
+ */
104
+ let node_visuals = null;
105
+
106
+ /**
107
+ * @returns {NodeDescriptionVisualRegistry}
108
+ */
109
+ export function particle_node_visuals() {
110
+ if (node_visuals === null) {
111
+ node_visuals = new NodeDescriptionVisualRegistry();
112
+
113
+ PARTICLE_NODE_REGISTRY.getNodesAsArray().forEach(description => node_visuals.generate(description));
114
+ }
115
+
116
+ return node_visuals;
117
+ }
118
+
119
+ /**
120
+ * Open one of the saved effects as a document.
121
+ *
122
+ * @param {string} name see {@link BONFIRE_EFFECT_NAMES}
123
+ * @param {object} [json] an effect to open instead of the saved one — what the page hands over when
124
+ * somebody imports a file they downloaded earlier
125
+ * @returns {ParticleEffectDocument}
126
+ */
127
+ export function bonfire_effect_document(name, json = BONFIRE_EFFECT_JSON[name]) {
128
+ return new ParticleEffectDocument().fromJSON(json, particle_node_visuals());
129
+ }
130
+
131
+ /**
132
+ * All four, as documents.
133
+ *
134
+ * @returns {Object<string, ParticleEffectDocument>}
135
+ */
136
+ export function bonfire_effect_documents() {
137
+ return Object.fromEntries(BONFIRE_EFFECT_NAMES.map(name => [name, bonfire_effect_document(name)]));
138
+ }
139
+
140
+ /**
141
+ * The ECS component a document describes, compiled.
142
+ *
143
+ * `null` when the effect does not compile. A graph an author has half-rewired is the ordinary case
144
+ * for that, so it is the caller's business to say so rather than this one's to throw — which is why
145
+ * the page can leave the previous effect burning while the editor shows the error.
146
+ *
147
+ * @param {ParticleEffectDocument} document
148
+ * @param {string|null} [texture] image URL to draw with, used when the document names none of its
149
+ * own. The page's sprites are generated at runtime, so they cannot be in the file
150
+ * @returns {ParticleEffect|null}
151
+ */
152
+ export function bonfire_effect_component(document, texture = null) {
153
+ const compilation = document.compile();
154
+
155
+ if (compilation.program === null || compilation.layout === null) {
156
+ return null;
157
+ }
158
+
159
+ const config = document.emitterConfig(compilation);
160
+
161
+ return ParticleEffect.from({ ...config, texture: config.texture ?? texture });
162
+ }
163
+
164
+ /**
165
+ * The four effects, ready to go on entities.
166
+ *
167
+ * @param {object} [textures] image URLs to draw with, `null` for an untextured (flat quad) effect
168
+ * @param {string|null} [textures.flame]
169
+ * @param {string|null} [textures.smoke]
170
+ * @param {string|null} [textures.ember]
171
+ * @param {string|null} [textures.soot]
172
+ * @returns {{flame: ParticleEffect, smoke: ParticleEffect, embers: ParticleEffect, soot: ParticleEffect}}
173
+ */
174
+ export function bonfire_effects(textures = {}) {
175
+ const sprites = bonfire_sprite_urls(textures);
176
+
177
+ return Object.fromEntries(BONFIRE_EFFECT_NAMES.map(name => {
178
+ const component = bonfire_effect_component(bonfire_effect_document(name), sprites[name]);
179
+
180
+ if (component === null) {
181
+ throw new Error(`the saved '${name}' effect does not compile — see effects/${name}.json`);
182
+ }
183
+
184
+ return [name, component];
185
+ }));
186
+ }
187
+
188
+ /**
189
+ * The sprite URLs keyed the way the effects are, so the page and the editor agree on which image
190
+ * belongs to which effect. `bonfire_sprites()` names the ember one in the singular.
191
+ *
192
+ * @param {object} [textures]
193
+ * @returns {Object<string, string|null>}
194
+ */
195
+ export function bonfire_sprite_urls(textures = {}) {
196
+ return {
197
+ flame: textures.flame ?? null,
198
+ smoke: textures.smoke ?? null,
199
+ embers: textures.ember ?? null,
200
+ soot: textures.soot ?? null,
201
+ };
202
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @typedef {object} BonfireSprites
3
+ * @property {string} flame
4
+ * @property {string} smoke
5
+ * @property {string} ember
6
+ * @property {string} soot
7
+ */
8
+ /**
9
+ * Draw the four sprites.
10
+ *
11
+ * Browser only — it draws through a `<canvas>`. The effects take their URLs as arguments precisely
12
+ * so that everything else about them can be built and tested without one.
13
+ *
14
+ * @returns {BonfireSprites}
15
+ */
16
+ export function bonfire_sprites(): BonfireSprites;
17
+ export type BonfireSprites = {
18
+ flame: string;
19
+ smoke: string;
20
+ ember: string;
21
+ soot: string;
22
+ };
23
+ //# sourceMappingURL=bonfire_sprites.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bonfire_sprites.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/particle_ecs/bonfire_sprites.js"],"names":[],"mappings":"AAmBA;;;;;;GAMG;AAEH;;;;;;;GAOG;AACH,mCAFa,cAAc,CAS1B;;WArBa,MAAM;WACN,MAAM;WACN,MAAM;UACN,MAAM"}
@@ -0,0 +1,315 @@
1
+ /**
2
+ * The four sprites the bonfire draws with, generated rather than shipped.
3
+ *
4
+ * They are PNG `data:` URLs, which is the point of doing it this way: the effects name a URL like
5
+ * any other effect would, `AssetManager` fetches and decodes it like any other image, and
6
+ * `GPUParticleEmitterSystem` packs it into the scene's sprite atlas like any other sprite. Nothing
7
+ * about the path is special-cased for a generated image, so the page exercises the real one — and
8
+ * it does it without a binary asset in the repository, which is what a playground under
9
+ * `test_assets/`-free source control needs.
10
+ *
11
+ * Generation is deterministic: one seeded generator, so the same page always draws the same fire.
12
+ */
13
+
14
+ /** Size of the flame and smoke sprites, in texels. Big enough to be blurred, small enough to pack. */
15
+ const LARGE = 128;
16
+
17
+ /** Size of the ember and soot sprites. They are drawn a few pixels across; detail here is wasted. */
18
+ const SMALL = 32;
19
+
20
+ /**
21
+ * @typedef {object} BonfireSprites
22
+ * @property {string} flame
23
+ * @property {string} smoke
24
+ * @property {string} ember
25
+ * @property {string} soot
26
+ */
27
+
28
+ /**
29
+ * Draw the four sprites.
30
+ *
31
+ * Browser only — it draws through a `<canvas>`. The effects take their URLs as arguments precisely
32
+ * so that everything else about them can be built and tested without one.
33
+ *
34
+ * @returns {BonfireSprites}
35
+ */
36
+ export function bonfire_sprites() {
37
+ return {
38
+ flame: flame_sprite(),
39
+ smoke: smoke_sprite(),
40
+ ember: ember_sprite(),
41
+ soot: soot_sprite(),
42
+ };
43
+ }
44
+
45
+ /**
46
+ * A soft round puff with a bright core: what a lick of flame is made of, additively.
47
+ *
48
+ * The falloff is `(1 - r)^2` rather than linear, so a hundred overlapping puffs sum into a body with
49
+ * a hot centre instead of a uniform blob — the shape of the falloff is what the fire's silhouette
50
+ * comes out of, far more than the shape of any one particle.
51
+ *
52
+ * @returns {string} PNG data URL
53
+ */
54
+ function flame_sprite() {
55
+ return draw(LARGE, (x, y) => {
56
+ const r = Math.hypot(x, y);
57
+
58
+ if (r >= 1) {
59
+ return 0;
60
+ }
61
+
62
+ const falloff = (1 - r) * (1 - r);
63
+
64
+ return falloff * (0.55 + 0.45 * (1 - r));
65
+ });
66
+ }
67
+
68
+ /**
69
+ * A cloudy puff: a radial mask with several octaves of value noise cut into it, so a smoke particle
70
+ * has structure of its own and a column of them does not read as a stack of discs.
71
+ *
72
+ * @returns {string} PNG data URL
73
+ */
74
+ function smoke_sprite() {
75
+ const noise = value_noise_field(LARGE, 0x51ce);
76
+
77
+ return draw(LARGE, (x, y, ix, iy) => {
78
+ const r = Math.hypot(x, y);
79
+
80
+ if (r >= 1) {
81
+ return 0;
82
+ }
83
+
84
+ // A long shoulder rather than a plateau, and noise deep enough to break what is left of
85
+ // the circle: a smoke particle whose own outline is visible is a smoke particle you can
86
+ // count, and a column of countable puffs is the thing that reads as "particles" rather
87
+ // than as smoke.
88
+ const mask = smoothstep(1, 0.05, r);
89
+ const detail = 0.18 + 0.82 * noise[iy * LARGE + ix];
90
+
91
+ return mask * mask * detail;
92
+ });
93
+ }
94
+
95
+ /**
96
+ * A tight bright dot with a faint halo — a cinder, and the glow around it.
97
+ *
98
+ * @returns {string} PNG data URL
99
+ */
100
+ function ember_sprite() {
101
+ return draw(SMALL, (x, y) => {
102
+ const r = Math.hypot(x, y);
103
+
104
+ if (r >= 1) {
105
+ return 0;
106
+ }
107
+
108
+ const core = smoothstep(0.45, 0.0, r);
109
+ const halo = (1 - r) * (1 - r) * 0.35;
110
+
111
+ return Math.min(1, core + halo);
112
+ });
113
+ }
114
+
115
+ /**
116
+ * An irregular flake with soft edges: soot is not round, and a flake that tumbles has to look
117
+ * different from one moment to the next for the tumble to be visible at all.
118
+ *
119
+ * @returns {string} PNG data URL
120
+ */
121
+ function soot_sprite() {
122
+ const random = seeded_random(0x50075);
123
+ const lobes = Array.from({ length: 5 }, () => 0.55 + random() * 0.45);
124
+
125
+ return draw(SMALL, (x, y) => {
126
+ const r = Math.hypot(x, y);
127
+ const angle = Math.atan2(y, x);
128
+
129
+ // A radius that varies with the angle, interpolated around the five lobes.
130
+ const t = (angle / (Math.PI * 2) + 1) % 1 * lobes.length;
131
+ const i = Math.floor(t);
132
+ const edge = lerp(lobes[i], lobes[(i + 1) % lobes.length], t - i);
133
+
134
+ return smoothstep(edge, edge * 0.55, r);
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Rasterize an alpha function into a white PNG.
140
+ *
141
+ * White because the colour is the particle's — every one of these effects drives `color` from a
142
+ * gradient over the particle's age, and the sprite's job is the shape it comes out in. `x` and `y`
143
+ * are in `[-1, 1]` at texel centres; `ix` and `iy` are the texel.
144
+ *
145
+ * @param {number} size
146
+ * @param {function(number, number, number, number): number} alpha
147
+ * @returns {string} PNG data URL
148
+ */
149
+ function draw(size, alpha) {
150
+ const canvas = document.createElement("canvas");
151
+
152
+ canvas.width = size;
153
+ canvas.height = size;
154
+
155
+ const context = canvas.getContext("2d");
156
+ const image = context.createImageData(size, size);
157
+ const data = image.data;
158
+
159
+ for (let iy = 0; iy < size; iy++) {
160
+ for (let ix = 0; ix < size; ix++) {
161
+ const x = (ix + 0.5) / size * 2 - 1;
162
+ const y = (iy + 0.5) / size * 2 - 1;
163
+ const offset = (iy * size + ix) * 4;
164
+
165
+ data[offset] = 255;
166
+ data[offset + 1] = 255;
167
+ data[offset + 2] = 255;
168
+ data[offset + 3] = Math.round(clamp(alpha(x, y, ix, iy), 0, 1) * 255);
169
+ }
170
+ }
171
+
172
+ context.putImageData(image, 0, 0);
173
+
174
+ return canvas.toDataURL("image/png");
175
+ }
176
+
177
+ /**
178
+ * Three octaves of tiling value noise in `[0, 1]`, one value per texel.
179
+ *
180
+ * Tiling, because the field is sampled over the whole sprite and a seam across the middle of a
181
+ * smoke puff is the one artefact that would be visible from any distance.
182
+ *
183
+ * @param {number} size
184
+ * @param {number} seed
185
+ * @returns {Float32Array}
186
+ */
187
+ function value_noise_field(size, seed) {
188
+ const field = new Float32Array(size * size);
189
+
190
+ let amplitude = 1;
191
+ let total = 0;
192
+
193
+ for (const period of [4, 8, 16]) {
194
+ const lattice = lattice_values(period, seed + period);
195
+
196
+ for (let y = 0; y < size; y++) {
197
+ for (let x = 0; x < size; x++) {
198
+ field[y * size + x] += amplitude * sample_lattice(
199
+ lattice, period, x / size * period, y / size * period
200
+ );
201
+ }
202
+ }
203
+
204
+ total += amplitude;
205
+ amplitude *= 0.55;
206
+ }
207
+
208
+ for (let i = 0; i < field.length; i++) {
209
+ field[i] /= total;
210
+ }
211
+
212
+ return field;
213
+ }
214
+
215
+ /**
216
+ * @param {number} period
217
+ * @param {number} seed
218
+ * @returns {Float32Array} `period * period` values in `[0, 1]`
219
+ */
220
+ function lattice_values(period, seed) {
221
+ const random = seeded_random(seed);
222
+ const values = new Float32Array(period * period);
223
+
224
+ for (let i = 0; i < values.length; i++) {
225
+ values[i] = random();
226
+ }
227
+
228
+ return values;
229
+ }
230
+
231
+ /**
232
+ * Smooth bilinear lookup, wrapping at the period so the field tiles.
233
+ *
234
+ * @param {Float32Array} lattice
235
+ * @param {number} period
236
+ * @param {number} x
237
+ * @param {number} y
238
+ * @returns {number}
239
+ */
240
+ function sample_lattice(lattice, period, x, y) {
241
+ const x0 = Math.floor(x);
242
+ const y0 = Math.floor(y);
243
+ const fx = smootherstep(x - x0);
244
+ const fy = smootherstep(y - y0);
245
+
246
+ const at = (ix, iy) => lattice[((iy % period) + period) % period * period + ((ix % period) + period) % period];
247
+
248
+ return lerp(
249
+ lerp(at(x0, y0), at(x0 + 1, y0), fx),
250
+ lerp(at(x0, y0 + 1), at(x0 + 1, y0 + 1), fx),
251
+ fy
252
+ );
253
+ }
254
+
255
+ /**
256
+ * mulberry32 — small, fast, and good enough for texture noise. Named so that "deterministic" above
257
+ * is a claim about a specific generator rather than about `Math.random`.
258
+ *
259
+ * @param {number} seed
260
+ * @returns {function(): number} uniform in `[0, 1)`
261
+ */
262
+ function seeded_random(seed) {
263
+ let state = seed >>> 0;
264
+
265
+ return () => {
266
+ state = (state + 0x6d2b79f5) >>> 0;
267
+
268
+ let t = state;
269
+
270
+ t = Math.imul(t ^ (t >>> 15), t | 1);
271
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
272
+
273
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
274
+ };
275
+ }
276
+
277
+ /**
278
+ * @param {number} edge0
279
+ * @param {number} edge1
280
+ * @param {number} x
281
+ * @returns {number}
282
+ */
283
+ function smoothstep(edge0, edge1, x) {
284
+ const t = clamp((x - edge0) / (edge1 - edge0), 0, 1);
285
+
286
+ return t * t * (3 - 2 * t);
287
+ }
288
+
289
+ /**
290
+ * @param {number} t
291
+ * @returns {number}
292
+ */
293
+ function smootherstep(t) {
294
+ return t * t * t * (t * (t * 6 - 15) + 10);
295
+ }
296
+
297
+ /**
298
+ * @param {number} a
299
+ * @param {number} b
300
+ * @param {number} t
301
+ * @returns {number}
302
+ */
303
+ function lerp(a, b, t) {
304
+ return a + (b - a) * t;
305
+ }
306
+
307
+ /**
308
+ * @param {number} value
309
+ * @param {number} low
310
+ * @param {number} high
311
+ * @returns {number}
312
+ */
313
+ function clamp(value, low, high) {
314
+ return value < low ? low : (value > high ? high : value);
315
+ }