@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,144 @@
1
+ import Vector3 from "../../../core/geom/Vector3.js";
2
+ import { Sampler2D } from "../../../engine/graphics/texture/sampler/Sampler2D.js";
3
+ import { sampler2d_to_f16 } from "../../../engine/graphics/texture/sampler/sampler2d_to_f16.js";
4
+ import { octahedral_uv_to_direction } from "../../renderer/light/environment/octahedral_uv_to_direction.js";
5
+ import { ColorSpace } from "../../renderer/texture/ColorSpace.js";
6
+ import { ShadeImage } from "../../renderer/texture/source/ShadeImage.js";
7
+ import { ShadeTexture } from "../../renderer/texture/ShadeTexture.js";
8
+
9
+ /**
10
+ * A moonlit night sky, generated: what the bonfire is lit by when it is not lighting itself.
11
+ *
12
+ * Built rather than loaded for the same reason the sprites are — `test_assets/` is gitignored, so a
13
+ * fresh checkout has no environment map, and a page that needs one to look right would look wrong
14
+ * for everybody who has not fetched the asset repository. This is small, it is in linear radiance,
15
+ * and it is the same octahedral projection `scene.lights.environment` samples.
16
+ *
17
+ * **The sky is the reason the fire reads as a fire.** Shade assumes global illumination, so a scene
18
+ * with no environment renders unlit; give it a bright day and the flame washes out to a pale smear,
19
+ * because the fire's own light is then a small fraction of what is already there. A night sky about
20
+ * three orders of magnitude below daylight is what leaves the fire room to be the brightest thing
21
+ * in the frame, and what makes the smoke read as blue where the firelight has stopped reaching it.
22
+ */
23
+
24
+ /**
25
+ * Resolution per octahedral face grid.
26
+ *
27
+ * Small on purpose: this is an ambient source, convolved into irradiance and a handful of roughness
28
+ * mips before anything samples it. What it has to be is smooth and correctly oriented.
29
+ *
30
+ * @type {number}
31
+ */
32
+ const RESOLUTION = 128;
33
+
34
+ /**
35
+ * Where the moon is. Unit, +Y up — the direction light arrives *from*, so the shadows fall the
36
+ * other way.
37
+ *
38
+ * @type {Vector3}
39
+ */
40
+ export const MOON_DIRECTION = new Vector3(-0.42, 0.62, -0.66).normalize();
41
+
42
+ /**
43
+ * Angular radius of the moon's disc, in radians. Far larger than the real one (about 0.0045): the
44
+ * map is 128 texels across a hemisphere, so a true disc would be a fraction of a texel and would
45
+ * disappear into the first mip. This is a moon to look at, not an ephemeris.
46
+ *
47
+ * @type {number}
48
+ */
49
+ const MOON_RADIUS = 0.075;
50
+
51
+ /**
52
+ * @type {Vector3}
53
+ */
54
+ const SCRATCH_DIRECTION = new Vector3();
55
+
56
+ /**
57
+ * Radiance arriving from a direction, in linear space.
58
+ *
59
+ * @param {number[]} out rgb
60
+ * @param {number} x
61
+ * @param {number} y +Y is up
62
+ * @param {number} z
63
+ */
64
+ function moonlit_radiance(out, x, y, z) {
65
+ // How close this direction is to the moon, as the cosine of the angle between them.
66
+ const alignment = x * MOON_DIRECTION.x + y * MOON_DIRECTION.y + z * MOON_DIRECTION.z;
67
+ const angle = Math.acos(Math.min(1, Math.max(-1, alignment)));
68
+
69
+ if (y >= 0) {
70
+ // Night sky: deepest at the zenith, lifting toward the horizon where the atmosphere is
71
+ // thicker and there is more of it to scatter what little light there is.
72
+ const horizon = Math.pow(1 - y, 4);
73
+
74
+ out[0] = 0.0035 + horizon * 0.011;
75
+ out[1] = 0.0052 + horizon * 0.014;
76
+ out[2] = 0.0115 + horizon * 0.020;
77
+ } else {
78
+ // Below the horizon: ground bounce, dimmer and less blue than the sky. This is what lights
79
+ // the underside of the smoke, so it is not zero however dark the night is.
80
+ const t = Math.pow(1 + y, 2);
81
+
82
+ out[0] = 0.0016 + t * 0.0042;
83
+ out[1] = 0.0017 + t * 0.0044;
84
+ out[2] = 0.0022 + t * 0.0055;
85
+ }
86
+
87
+ // A broad halo, then the disc itself. The halo is what makes the sky around the moon read as
88
+ // sky with a moon in it rather than as a sticker on a gradient.
89
+ const halo = Math.exp(-angle * 5.5) * 0.05;
90
+
91
+ out[0] += halo * 0.75;
92
+ out[1] += halo * 0.82;
93
+ out[2] += halo;
94
+
95
+ if (angle < MOON_RADIUS) {
96
+ // Soft-edged, so the disc survives the convolution into the roughness mips with a shape
97
+ // rather than as ringing.
98
+ const disc = 1 - Math.pow(angle / MOON_RADIUS, 3);
99
+
100
+ out[0] += disc * 5.2;
101
+ out[1] += disc * 5.6;
102
+ out[2] += disc * 6.4;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * The environment map. Assign it to `scene.lights.environment`.
108
+ *
109
+ * @returns {ShadeTexture}
110
+ */
111
+ export function moonlit_environment() {
112
+ const sampler = Sampler2D.float32(4, RESOLUTION, RESOLUTION);
113
+
114
+ const data = sampler.data;
115
+ const rgb = [0, 0, 0];
116
+
117
+ for (let v = 0; v < RESOLUTION; v++) {
118
+ for (let u = 0; u < RESOLUTION; u++) {
119
+ // texel centres, so the mapping is symmetric across the octahedron's folds
120
+ octahedral_uv_to_direction(
121
+ SCRATCH_DIRECTION,
122
+ (u + 0.5) / RESOLUTION,
123
+ (v + 0.5) / RESOLUTION
124
+ );
125
+
126
+ moonlit_radiance(rgb, SCRATCH_DIRECTION.x, SCRATCH_DIRECTION.y, SCRATCH_DIRECTION.z);
127
+
128
+ const offset = (v * RESOLUTION + u) * 4;
129
+
130
+ data[offset] = rgb[0];
131
+ data[offset + 1] = rgb[1];
132
+ data[offset + 2] = rgb[2];
133
+ data[offset + 3] = 1;
134
+ }
135
+ }
136
+
137
+ const image = ShadeImage.fromSampler2D(sampler2d_to_f16(sampler));
138
+
139
+ // radiance, not colour: the values above are light arriving, and treating them as sRGB would
140
+ // put a transfer curve through numbers that never had one
141
+ image.color_space = ColorSpace.LinearSRGB;
142
+
143
+ return ShadeTexture.from(image);
144
+ }
@@ -78,10 +78,11 @@ edits it at both altitudes.
78
78
  - Two of them are **ramps**, edited as a picture. An **RGBA Gradient** (and an RGB one) is a strip of
79
79
  the colour it produces with its stops underneath: drag a stop, click the lane to add one — in the
80
80
  colour the gradient already is there, so adding never changes anything — and middle-click to remove.
81
- A **Curve** is the unit square with its keys in it, and the picker under it decides the selected
82
- key's tangents. Neither is sampled from a table at runtime: both are written out into the program as
83
- arithmetic, which is what the **Code** tab shows. `Curve Table`, which *is* the table, is the other
84
- node — it takes a row handle rather than a curve.
81
+ A **Curve** plots normalized time against unrestricted values, with a Y axis that fits the whole
82
+ curve, including peaks between keys. Its fields edit the selected key's time, value and tangents.
83
+ The scale stays fixed during a drag and refits on release. Neither ramp is sampled from a table
84
+ at runtime: both are written out into the program as arithmetic, which is what the **Code** tab
85
+ shows.
85
86
  - The strip along the bottom lists every problem with the effect, from **both** phases, loudest
86
87
  first. Click one to jump to the phase it is in and select the node; the node is outlined in the
87
88
  graph too.
@@ -1,31 +1,29 @@
1
1
  /**
2
2
  * Worked examples of authoring particle effects with the node graph + placing them in a scene for a
3
3
  * {@link GPUParticleSystem} to run. Demonstrates: distinct per-particle lifecycle (no built-in age — the
4
- * effect declares it), AnimationCurve sampling over life, flipbook frames, random emission,
4
+ * effect declares it), authored curves over life, flipbook frames, random emission,
5
5
  * additive vs alpha blending, projection modes, soft depth and lighting toggles.
6
6
  *
7
7
  * These are examples, not engine code, which is why they sit here rather than under
8
8
  * `renderer/particles/`: the only thing that opens them is this playground's page, and keeping them
9
9
  * on this side of the line means the dependency runs playground -> renderer and never back.
10
10
  *
11
- * `curves` is a `GPUAnimationManager` (or anything with `register_curve(AnimationCurve) -> handle`);
12
- * `size_curve` / `alpha_curve` are meep `AnimationCurve`s. Both are optional — passing `undefined`
13
- * yields effects that use constant size/alpha instead (curve handle 0, evaluated as 0 by the
14
- * disabled provider), which is handy for the headless tests.
11
+ * `size_curve` / `alpha_curve` are authored curve parameters compiled into arithmetic. Both are
12
+ * optional; omitted curves leave the initial size/colour unchanged.
15
13
  */
16
14
  /**
17
15
  * A fire effect: buoyant, turbulent, additive-blended camera-facing sprites whose size and opacity
18
16
  * follow curves over their lifetime and whose flipbook advances with age.
19
17
  *
20
18
  * @param {object} [options]
21
- * @param {number} [options.size_curve] curve handle for size(normalizedAge)
22
- * @param {number} [options.alpha_curve] curve handle for alpha(normalizedAge)
19
+ * @param {import("../../renderer/particles/graph/particle_ramp.js").ParticleCurveParameter} [options.size_curve] size(normalizedAge)
20
+ * @param {import("../../renderer/particles/graph/particle_ramp.js").ParticleCurveParameter} [options.alpha_curve] alpha(normalizedAge)
23
21
  * @param {number} [options.flipbook_frames]
24
22
  * @returns {{layout: ParticleLayout, effect: object, emitter: object}}
25
23
  */
26
24
  export function fire_effect({ size_curve, alpha_curve, flipbook_frames }?: {
27
- size_curve?: number;
28
- alpha_curve?: number;
25
+ size_curve?: import("../../renderer/particles/graph/particle_ramp.js").ParticleCurveParameter;
26
+ alpha_curve?: import("../../renderer/particles/graph/particle_ramp.js").ParticleCurveParameter;
29
27
  flipbook_frames?: number;
30
28
  }): {
31
29
  layout: ParticleLayout;
@@ -1 +1 @@
1
- {"version":3,"file":"particle_prototype.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/particle_system/particle_prototype.js"],"names":[],"mappings":"AAYA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;;GASG;AACH;IAL4B,UAAU,GAA3B,MAAM;IACW,WAAW,GAA5B,MAAM;IACW,eAAe,GAAhC,MAAM;;YACK,cAAc;YAAU,MAAM;aAAW,MAAM;EA+GpE;AAED;;;;GAIG;AACH;YAFsB,cAAc;YAAU,MAAM;aAAW,MAAM;EAYpE;AAED;;;;;;;;;;;;;GAaG;AACH,yCANW,OAAO,+BAA+B,EAAE,KAAK;IAEa,IAAI,GAA9D,OAAO,gCAAgC,EAAE,MAAM;;UAEtC,eAAe;WAAS,eAAe;EAe1D;+BAzL8B,mDAAmD;gCAElD,qDAAqD"}
1
+ {"version":3,"file":"particle_prototype.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/particle_system/particle_prototype.js"],"names":[],"mappings":"AAYA;;;;;;;;;;;;GAYG;AAEH;;;;;;;;;GASG;AACH;IALsG,UAAU,GAArG,OAAO,iDAAiD,EAAE,sBAAsB;IACW,WAAW,GAAtG,OAAO,iDAAiD,EAAE,sBAAsB;IAC/D,eAAe,GAAhC,MAAM;;YACK,cAAc;YAAU,MAAM;aAAW,MAAM;EA+GpE;AAED;;;;GAIG;AACH;YAFsB,cAAc;YAAU,MAAM;aAAW,MAAM;EAYpE;AAED;;;;;;;;;;;;;GAaG;AACH,yCANW,OAAO,+BAA+B,EAAE,KAAK;IAEa,IAAI,GAA9D,OAAO,gCAAgC,EAAE,MAAM;;UAEtC,eAAe;WAAS,eAAe;EAe1D;+BAvL8B,mDAAmD;gCAElD,qDAAqD"}
@@ -13,17 +13,15 @@ import { ParticleEmitter } from "../../renderer/particles/runtime/ParticleEmitte
13
13
  /**
14
14
  * Worked examples of authoring particle effects with the node graph + placing them in a scene for a
15
15
  * {@link GPUParticleSystem} to run. Demonstrates: distinct per-particle lifecycle (no built-in age — the
16
- * effect declares it), AnimationCurve sampling over life, flipbook frames, random emission,
16
+ * effect declares it), authored curves over life, flipbook frames, random emission,
17
17
  * additive vs alpha blending, projection modes, soft depth and lighting toggles.
18
18
  *
19
19
  * These are examples, not engine code, which is why they sit here rather than under
20
20
  * `renderer/particles/`: the only thing that opens them is this playground's page, and keeping them
21
21
  * on this side of the line means the dependency runs playground -> renderer and never back.
22
22
  *
23
- * `curves` is a `GPUAnimationManager` (or anything with `register_curve(AnimationCurve) -> handle`);
24
- * `size_curve` / `alpha_curve` are meep `AnimationCurve`s. Both are optional — passing `undefined`
25
- * yields effects that use constant size/alpha instead (curve handle 0, evaluated as 0 by the
26
- * disabled provider), which is handy for the headless tests.
23
+ * `size_curve` / `alpha_curve` are authored curve parameters compiled into arithmetic. Both are
24
+ * optional; omitted curves leave the initial size/colour unchanged.
27
25
  */
28
26
 
29
27
  /**
@@ -31,8 +29,8 @@ import { ParticleEmitter } from "../../renderer/particles/runtime/ParticleEmitte
31
29
  * follow curves over their lifetime and whose flipbook advances with age.
32
30
  *
33
31
  * @param {object} [options]
34
- * @param {number} [options.size_curve] curve handle for size(normalizedAge)
35
- * @param {number} [options.alpha_curve] curve handle for alpha(normalizedAge)
32
+ * @param {import("../../renderer/particles/graph/particle_ramp.js").ParticleCurveParameter} [options.size_curve] size(normalizedAge)
33
+ * @param {import("../../renderer/particles/graph/particle_ramp.js").ParticleCurveParameter} [options.alpha_curve] alpha(normalizedAge)
36
34
  * @param {number} [options.flipbook_frames]
37
35
  * @returns {{layout: ParticleLayout, effect: object, emitter: object}}
38
36
  */
@@ -111,12 +109,12 @@ export function fire_effect({ size_curve, alpha_curve, flipbook_frames = 16 } =
111
109
 
112
110
  // size + alpha from curves over life (or constants if no curve provided)
113
111
  if (size_curve !== undefined) {
114
- const sc = particle_node(update, "curve", { handle: size_curve }); particle_wire(update, sc, "t", nAge);
112
+ const sc = particle_node(update, "curveLiteral", { curve: size_curve }); particle_wire(update, sc, "t", nAge);
115
113
  particle_wire(update, particle_node(update, "setAttribute", { name: "size" }), "value", sc);
116
114
  }
117
115
  const color = particle_node(update, "attribute", { name: "color" });
118
116
  if (alpha_curve !== undefined) {
119
- const ac = particle_node(update, "curve", { handle: alpha_curve }); particle_wire(update, ac, "t", nAge);
117
+ const ac = particle_node(update, "curveLiteral", { curve: alpha_curve }); particle_wire(update, ac, "t", nAge);
120
118
  // color.rgb unchanged, color.a = ac : rebuild via select on a swizzle mask is overkill;
121
119
  // simplest: multiply color by (1,1,1,ac_normalized) — approximate by scaling alpha only.
122
120
  const alphaVec = particle_node(update, "mul");
@@ -0,0 +1,58 @@
1
+ /**
2
+ * One key that records a GPU profile capture and downloads it.
3
+ *
4
+ * `T` starts a recording, `T` again stops it and saves an `.sgpt` next to the browser's other
5
+ * downloads. That is the whole interaction, and it is what every playground page wants: a capture is
6
+ * something you take *while looking at* the thing you are suspicious of, so reaching for a panel is
7
+ * already too slow.
8
+ *
9
+ * **The engine never constructs a `GPUProfileSession`.** `Renderer.profile_session` is a nullable
10
+ * field and the null checks around it are the entire integration, which is what keeps the profiler
11
+ * out of a bundle that does not ask for it. Importing this module is the opt-in.
12
+ *
13
+ * `volumetrics_froxel/main.js` has its own copy of this, written first; it is the page this was
14
+ * lifted from and it can adopt this when somebody has the assets to verify it against.
15
+ */
16
+ /**
17
+ * @typedef {object} ProfileHotkeyHandle
18
+ * @property {function(): boolean} is_recording
19
+ * @property {function(): Promise<void>} toggle start a recording, or stop and save the one running
20
+ * @property {function(): void} dispose remove the key listener
21
+ */
22
+ /**
23
+ * Wire the key up.
24
+ *
25
+ * @param {object} params
26
+ * @param {import("../renderer/Renderer.js").Renderer} params.renderer initialized — this reads its
27
+ * device for the capture's metadata
28
+ * @param {string} params.name goes in the filename and in the capture's note
29
+ * @param {HTMLElement|null} [params.status] written with what the recording is doing; the element
30
+ * gets the class `recording` while one is running, and nothing otherwise
31
+ * @param {function(): string[]} [params.settings] what the page was set to when the capture was
32
+ * taken. A pass timing says nothing until you know that
33
+ * @param {number} [params.level] see {@link GPUProfileLevel}; `WORKLOAD` is the most detailed
34
+ * @param {string} [params.key] the letter to bind, lowercase
35
+ * @param {EventTarget} [params.target] where the listener goes
36
+ * @returns {ProfileHotkeyHandle}
37
+ */
38
+ export function install_profile_hotkey({ renderer, name, status, settings, level, key, target, }: {
39
+ renderer: import("../renderer/Renderer.js").Renderer;
40
+ name: string;
41
+ status?: HTMLElement | null;
42
+ settings?: () => string[];
43
+ level?: number;
44
+ key?: string;
45
+ target?: EventTarget;
46
+ }): ProfileHotkeyHandle;
47
+ export type ProfileHotkeyHandle = {
48
+ is_recording: () => boolean;
49
+ /**
50
+ * start a recording, or stop and save the one running
51
+ */
52
+ toggle: () => Promise<void>;
53
+ /**
54
+ * remove the key listener
55
+ */
56
+ dispose: () => void;
57
+ };
58
+ //# sourceMappingURL=profile_hotkey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile_hotkey.d.ts","sourceRoot":"","sources":["../../../../src/shade/playground/profile_hotkey.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AAEH;;;;;GAKG;AAEH;;;;;;;;;;;;;;;GAeG;AACH;IAZ8D,QAAQ,EAA3D,OAAO,yBAAyB,EAAE,QAAQ;IAE3B,IAAI,EAAnB,MAAM;IACoB,MAAM,GAAhC,WAAW,GAAC,IAAI;IAEc,QAAQ,SAA1B,MAAM,EAAE;IAEJ,KAAK,GAArB,MAAM;IACU,GAAG,GAAnB,MAAM;IACe,MAAM,GAA3B,WAAW;IACT,mBAAmB,CAwO/B;;wBA3PyB,OAAO;;;;kBACP,QAAQ,IAAI,CAAC;;;;mBACb,IAAI"}
@@ -0,0 +1,325 @@
1
+ import package_json from "../../../package.json";
2
+ import { GPUProfileLevel } from "../device/timing/profile/GPUProfileLevel.js";
3
+ import { GPUProfileSession } from "../device/timing/profile/GPUProfileSession.js";
4
+
5
+ /**
6
+ * One key that records a GPU profile capture and downloads it.
7
+ *
8
+ * `T` starts a recording, `T` again stops it and saves an `.sgpt` next to the browser's other
9
+ * downloads. That is the whole interaction, and it is what every playground page wants: a capture is
10
+ * something you take *while looking at* the thing you are suspicious of, so reaching for a panel is
11
+ * already too slow.
12
+ *
13
+ * **The engine never constructs a `GPUProfileSession`.** `Renderer.profile_session` is a nullable
14
+ * field and the null checks around it are the entire integration, which is what keeps the profiler
15
+ * out of a bundle that does not ask for it. Importing this module is the opt-in.
16
+ *
17
+ * `volumetrics_froxel/main.js` has its own copy of this, written first; it is the page this was
18
+ * lifted from and it can adopt this when somebody has the assets to verify it against.
19
+ */
20
+
21
+ /**
22
+ * @typedef {object} ProfileHotkeyHandle
23
+ * @property {function(): boolean} is_recording
24
+ * @property {function(): Promise<void>} toggle start a recording, or stop and save the one running
25
+ * @property {function(): void} dispose remove the key listener
26
+ */
27
+
28
+ /**
29
+ * Wire the key up.
30
+ *
31
+ * @param {object} params
32
+ * @param {import("../renderer/Renderer.js").Renderer} params.renderer initialized — this reads its
33
+ * device for the capture's metadata
34
+ * @param {string} params.name goes in the filename and in the capture's note
35
+ * @param {HTMLElement|null} [params.status] written with what the recording is doing; the element
36
+ * gets the class `recording` while one is running, and nothing otherwise
37
+ * @param {function(): string[]} [params.settings] what the page was set to when the capture was
38
+ * taken. A pass timing says nothing until you know that
39
+ * @param {number} [params.level] see {@link GPUProfileLevel}; `WORKLOAD` is the most detailed
40
+ * @param {string} [params.key] the letter to bind, lowercase
41
+ * @param {EventTarget} [params.target] where the listener goes
42
+ * @returns {ProfileHotkeyHandle}
43
+ */
44
+ export function install_profile_hotkey({
45
+ renderer,
46
+ name,
47
+ status = null,
48
+ settings = () => [],
49
+ level = GPUProfileLevel.WORKLOAD,
50
+ key = "t",
51
+ target = window,
52
+ }) {
53
+ /**
54
+ * The recording in progress, or null.
55
+ * @type {GPUProfileSession|null}
56
+ */
57
+ let session = null;
58
+
59
+ /** Guards against the key being leaned on while a stop is still draining. */
60
+ let busy = false;
61
+
62
+ /**
63
+ * @param {string} text
64
+ * @param {boolean} [recording]
65
+ */
66
+ function set_status(text, recording = false) {
67
+ if (status === null) {
68
+ return;
69
+ }
70
+
71
+ status.textContent = text;
72
+ status.className = recording ? "recording" : "";
73
+ }
74
+
75
+ function start() {
76
+ const device = renderer.device;
77
+
78
+ const recording = new GPUProfileSession({
79
+ level,
80
+ note: `${name} — ${[...settings(), `${renderer.pixel_ratio.toFixed(2)}x`].join(" · ")}`,
81
+ });
82
+
83
+ /*
84
+ Populated before start(): the meta record is written into the stream there and not read
85
+ again. Without it the capture is unreadable in the sense that matters — a pass taking 0.4 ms
86
+ says nothing until you know which GPU and at what resolution.
87
+ */
88
+ const meta = recording.meta;
89
+
90
+ // `GPUDevice.adapterInfo` is recent enough that a browser running this page may not have it,
91
+ // and several browsers withhold the fields even when they do. Empty is what the format
92
+ // expects in that case.
93
+ const info = device.adapterInfo;
94
+
95
+ if (info !== undefined && info !== null) {
96
+ meta.adapter_vendor = info.vendor ?? "";
97
+ meta.adapter_architecture = info.architecture ?? "";
98
+ meta.adapter_device = info.device ?? "";
99
+ meta.adapter_description = info.description ?? "";
100
+ }
101
+
102
+ meta.features = [...device.features];
103
+ meta.engine_version = package_json.version;
104
+
105
+ recording.onBytesWritten.add(bytes => {
106
+ /*
107
+ Only while this is still the live recording. Frames keep landing all through the drain
108
+ in `stop`, which is the whole point of draining — and without this check each one would
109
+ overwrite "stopping…" with "● recording · T to stop", so a capture that is on its way to
110
+ disk reads as one that is still running.
111
+ */
112
+ if (session !== recording) {
113
+ return;
114
+ }
115
+
116
+ set_status(
117
+ `● recording — ${recording.frames_recorded} frames, ${format_bytes(bytes)}`
118
+ + ` · ${key.toUpperCase()} to stop`,
119
+ true
120
+ );
121
+ });
122
+
123
+ renderer.profile_session = recording;
124
+ recording.start();
125
+
126
+ session = recording;
127
+
128
+ // The timers go quiet without this rather than failing, so the capture still carries the
129
+ // frame graph and the workload — it just has no durations in it. Worth knowing before you
130
+ // record for a minute and then look.
131
+ const timed = device.features.has("timestamp-query");
132
+
133
+ set_status(
134
+ timed
135
+ ? `● recording — 0 frames · ${key.toUpperCase()} to stop`
136
+ : "● recording without timestamp-query — structure only, no durations"
137
+ + ` · ${key.toUpperCase()} to stop`,
138
+ true
139
+ );
140
+ }
141
+
142
+ /**
143
+ * Wait for frames already submitted to finish landing in the session.
144
+ *
145
+ * A frame is committed when its timestamp readback resolves — a `mapAsync` a frame or two behind
146
+ * submit, see `ShadeGPUCommandContext.profiling_absorbed` — and `record_frame` silently ignores
147
+ * anything arriving after `stop()`. Stopping the instant the key goes down therefore discards
148
+ * the last few frames, which are the ones you were most likely looking at.
149
+ *
150
+ * Drains the queue, then waits for the frame count to stop moving. Capped, because a recording
151
+ * taken while the page is paused never moves at all.
152
+ *
153
+ * @param {GPUProfileSession} recording
154
+ * @returns {Promise<void>}
155
+ */
156
+ async function drain(recording) {
157
+ const DEADLINE_MS = 1000;
158
+ const QUIET_MS = 60;
159
+
160
+ await renderer.device.queue.onSubmittedWorkDone();
161
+
162
+ const deadline = performance.now() + DEADLINE_MS;
163
+
164
+ let previous = recording.frames_recorded;
165
+
166
+ while (performance.now() < deadline) {
167
+ await new Promise(resolve => setTimeout(resolve, QUIET_MS));
168
+
169
+ if (recording.frames_recorded === previous) {
170
+ return;
171
+ }
172
+
173
+ previous = recording.frames_recorded;
174
+ }
175
+ }
176
+
177
+ async function stop() {
178
+ const recording = session;
179
+
180
+ session = null;
181
+
182
+ // Detached first, so nothing encoded during the drain below joins the recording. Frames
183
+ // already encoded are unaffected: Renderer captures the session into a local before it
184
+ // subscribes to the readback.
185
+ renderer.profile_session = null;
186
+
187
+ set_status("stopping — draining in-flight frames…");
188
+
189
+ await drain(recording);
190
+
191
+ const bytes = recording.stop();
192
+ const filename = `${name}-${file_timestamp()}.sgpt`;
193
+
194
+ download_bytes(bytes, filename);
195
+
196
+ const topologies = `${recording.topology_count} topolog${recording.topology_count === 1 ? "y" : "ies"}`;
197
+
198
+ // Also to the console, because the status line is the only other place the name appears and
199
+ // a browser that declines the save says nothing about which capture it declined.
200
+ console.log(
201
+ `profile: ${recording.frames_recorded} frames, ${format_bytes(bytes.byteLength)}, `
202
+ + `${topologies} -> ${filename}`
203
+ );
204
+
205
+ set_status(
206
+ `${recording.frames_recorded} frames, ${format_bytes(bytes.byteLength)}, ${topologies}`
207
+ + ` · ${filename}`
208
+ );
209
+ }
210
+
211
+ /**
212
+ * @returns {Promise<void>}
213
+ */
214
+ function toggle() {
215
+ if (busy) {
216
+ return Promise.resolve();
217
+ }
218
+
219
+ busy = true;
220
+
221
+ const done = session === null ? Promise.resolve(start()) : stop();
222
+
223
+ return done.catch(error => {
224
+ set_status(`profiling failed: ${error?.message ?? error}`);
225
+ console.error(error);
226
+
227
+ session = null;
228
+ renderer.profile_session = null;
229
+ }).finally(() => {
230
+ busy = false;
231
+ });
232
+ }
233
+
234
+ /**
235
+ * @param {KeyboardEvent} event
236
+ */
237
+ function on_keydown(event) {
238
+ if (event.ctrlKey || event.metaKey || event.altKey || event.repeat) {
239
+ return;
240
+ }
241
+
242
+ /*
243
+ `key` first, because it is the letter actually printed on the key the user pressed — `code`
244
+ is physical position, so on AZERTY it names the key where T sits on QWERTY. `code` is the
245
+ fallback for layouts whose `key` is not Latin at all (Cyrillic gives "е" here).
246
+ */
247
+ if (event.key.toLowerCase() !== key && event.code !== `Key${key.toUpperCase()}`) {
248
+ return;
249
+ }
250
+
251
+ // a panel's fields and buttons are focusable; do not steal a key from one
252
+ const focused = event.target;
253
+
254
+ if (
255
+ focused instanceof HTMLInputElement
256
+ || focused instanceof HTMLTextAreaElement
257
+ || focused instanceof HTMLSelectElement
258
+ ) {
259
+ return;
260
+ }
261
+
262
+ event.preventDefault();
263
+
264
+ toggle();
265
+ }
266
+
267
+ target.addEventListener("keydown", on_keydown);
268
+
269
+ return {
270
+ is_recording: () => session !== null,
271
+ toggle,
272
+ dispose: () => target.removeEventListener("keydown", on_keydown),
273
+ };
274
+ }
275
+
276
+ /**
277
+ * @param {Uint8Array} bytes
278
+ * @param {string} filename
279
+ */
280
+ function download_bytes(bytes, filename) {
281
+ const url = URL.createObjectURL(new Blob([bytes], { type: "application/octet-stream" }));
282
+
283
+ const anchor = document.createElement("a");
284
+
285
+ anchor.href = url;
286
+ anchor.download = filename;
287
+
288
+ document.body.appendChild(anchor);
289
+ anchor.click();
290
+ anchor.remove();
291
+
292
+ // Revoking in the same task cancels the download in some browsers — the click only schedules it.
293
+ setTimeout(() => URL.revokeObjectURL(url), 60000);
294
+ }
295
+
296
+ /**
297
+ * `YYYYMMDD-HHMMSS` in local time, matching the naming the other `.sgpt` captures already use — same
298
+ * sort order, and it reads as the clock you took it by rather than as UTC.
299
+ *
300
+ * @returns {string}
301
+ */
302
+ function file_timestamp() {
303
+ const now = new Date();
304
+
305
+ const pad = value => String(value).padStart(2, "0");
306
+
307
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
308
+ + `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
309
+ }
310
+
311
+ /**
312
+ * @param {number} bytes
313
+ * @returns {string}
314
+ */
315
+ function format_bytes(bytes) {
316
+ if (bytes < 1024) {
317
+ return `${bytes} B`;
318
+ }
319
+
320
+ if (bytes < 1024 * 1024) {
321
+ return `${(bytes / 1024).toFixed(1)} KiB`;
322
+ }
323
+
324
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
325
+ }
@@ -0,0 +1,22 @@
1
+ # SSR cross camera playground
2
+
3
+ Open `/src/shade/playground/ssr_motion/` on the engine's Vite development server.
4
+ It shares the exact scene, camera lens, resolution and fixed exposure of
5
+ `ssr_variance`: one metallic floor and an emissive cross card with two colored
6
+ markers. Final-image TAA and raster jitter default off independently.
7
+
8
+ Drag to orbit, right-drag to pan, wheel to zoom, or focus the canvas and use WASD.
9
+ Floor roughness changes live from 0.01 to 1.00. Material changes clear history;
10
+ ordinary camera movement does not. The lateral path uses the same motion-speed
11
+ scale as `ssr_reprojection` (x = 2 sin(phase), phase rate = 0.7 × speed).
12
+ Choose speed 2.0 to reproduce the faster lateral movement.
13
+
14
+ Pause motion continues sampling at the current pose. Pause rendering freezes the
15
+ image. Step frame submits one 1/60-second frame. SSR parallax, surface-only and
16
+ no-history modes, spatial denoising, final TAA and jitter can be compared separately.
17
+
18
+ Query controls: `?motion=lateral&speed=2&roughness=0.3`, `taa=on`, `jitter=on`,
19
+ `spatial=off`, and `temporal=0|1|2`. `manual` starts at frame zero with rendering
20
+ paused. `await ssr_motion_ready` exposes the renderer, camera, scene, `pose`,
21
+ `frame`, and `step(n, advance=true)` for controlled experiments. `advance=false`
22
+ keeps the current pose while sampling. Exact stepping requires rendering paused.