@woosh/meep-engine 3.10.0 → 3.11.1

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 (164) hide show
  1. package/README.md +168 -152
  2. package/package.json +1 -1
  3. package/src/REVIEW_2026_08_06.md +610 -610
  4. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
  5. package/src/core/geom/3d/shape/ConvexHullShape3D.js +28 -4
  6. package/src/engine/graphics/render/frame_graph/FrameGraph.d.ts +26 -0
  7. package/src/engine/graphics/render/frame_graph/FrameGraph.d.ts.map +1 -1
  8. package/src/engine/graphics/render/frame_graph/FrameGraph.js +47 -0
  9. package/src/engine/physics/ecs/PhysicsSystem.d.ts +0 -3
  10. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  11. package/src/engine/physics/ecs/PhysicsSystem.js +0 -3
  12. package/src/shade/device/ShadeGPUCommandContext.d.ts +21 -0
  13. package/src/shade/device/ShadeGPUCommandContext.d.ts.map +1 -1
  14. package/src/shade/device/ShadeGPUCommandContext.js +940 -749
  15. package/src/shade/device/timing/GPUTimerArray.d.ts +22 -4
  16. package/src/shade/device/timing/GPUTimerArray.d.ts.map +1 -1
  17. package/src/shade/device/timing/GPUTimerArray.js +88 -14
  18. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +349 -98
  19. package/src/shade/device/timing/profile/GPUFrameRecorder.d.ts +93 -0
  20. package/src/shade/device/timing/profile/GPUFrameRecorder.d.ts.map +1 -0
  21. package/src/shade/device/timing/profile/GPUFrameRecorder.js +297 -0
  22. package/src/shade/device/timing/profile/GPUProfileCapture.d.ts +62 -0
  23. package/src/shade/device/timing/profile/GPUProfileCapture.d.ts.map +1 -0
  24. package/src/shade/device/timing/profile/GPUProfileCapture.js +77 -0
  25. package/src/shade/device/timing/profile/GPUProfileFrame.d.ts +73 -0
  26. package/src/shade/device/timing/profile/GPUProfileFrame.d.ts.map +1 -0
  27. package/src/shade/device/timing/profile/GPUProfileFrame.js +88 -0
  28. package/src/shade/device/timing/profile/GPUProfileLevel.d.ts +20 -0
  29. package/src/shade/device/timing/profile/GPUProfileLevel.d.ts.map +1 -0
  30. package/src/shade/device/timing/profile/GPUProfileLevel.js +50 -0
  31. package/src/shade/device/timing/profile/GPUProfileMeta.d.ts +63 -0
  32. package/src/shade/device/timing/profile/GPUProfileMeta.d.ts.map +1 -0
  33. package/src/shade/device/timing/profile/GPUProfileMeta.js +73 -0
  34. package/src/shade/device/timing/profile/GPUProfileSession.d.ts +173 -0
  35. package/src/shade/device/timing/profile/GPUProfileSession.d.ts.map +1 -0
  36. package/src/shade/device/timing/profile/GPUProfileSession.js +470 -0
  37. package/src/shade/device/timing/profile/GPUProfileSpan.d.ts +60 -0
  38. package/src/shade/device/timing/profile/GPUProfileSpan.d.ts.map +1 -0
  39. package/src/shade/device/timing/profile/GPUProfileSpan.js +65 -0
  40. package/src/shade/device/timing/profile/GPUProfileSpanKind.d.ts +19 -0
  41. package/src/shade/device/timing/profile/GPUProfileSpanKind.d.ts.map +1 -0
  42. package/src/shade/device/timing/profile/GPUProfileSpanKind.js +25 -0
  43. package/src/shade/device/timing/profile/GPUProfileTopology.d.ts +159 -0
  44. package/src/shade/device/timing/profile/GPUProfileTopology.d.ts.map +1 -0
  45. package/src/shade/device/timing/profile/GPUProfileTopology.js +202 -0
  46. package/src/shade/device/timing/profile/GPUProfileWork.d.ts +90 -0
  47. package/src/shade/device/timing/profile/GPUProfileWork.d.ts.map +1 -0
  48. package/src/shade/device/timing/profile/GPUProfileWork.js +115 -0
  49. package/src/shade/device/timing/profile/SGPTDefect.d.ts +43 -0
  50. package/src/shade/device/timing/profile/SGPTDefect.d.ts.map +1 -0
  51. package/src/shade/device/timing/profile/SGPTDefect.js +65 -0
  52. package/src/shade/device/timing/profile/SGPTHeader.d.ts +38 -0
  53. package/src/shade/device/timing/profile/SGPTHeader.d.ts.map +1 -0
  54. package/src/shade/device/timing/profile/SGPTHeader.js +46 -0
  55. package/src/shade/device/timing/profile/SGPTRecordRef.d.ts +37 -0
  56. package/src/shade/device/timing/profile/SGPTRecordRef.d.ts.map +1 -0
  57. package/src/shade/device/timing/profile/SGPTRecordRef.js +45 -0
  58. package/src/shade/device/timing/profile/SGPTWriteStream.d.ts +103 -0
  59. package/src/shade/device/timing/profile/SGPTWriteStream.d.ts.map +1 -0
  60. package/src/shade/device/timing/profile/SGPTWriteStream.js +287 -0
  61. package/src/shade/device/timing/profile/SGPT_DEFECT.d.ts +19 -0
  62. package/src/shade/device/timing/profile/SGPT_DEFECT.d.ts.map +1 -0
  63. package/src/shade/device/timing/profile/SGPT_DEFECT.js +26 -0
  64. package/src/shade/device/timing/profile/SGPT_FLAG.d.ts +8 -0
  65. package/src/shade/device/timing/profile/SGPT_FLAG.d.ts.map +1 -0
  66. package/src/shade/device/timing/profile/SGPT_FLAG.js +15 -0
  67. package/src/shade/device/timing/profile/SGPT_FORMAT.md +345 -0
  68. package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.d.ts +10 -0
  69. package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.d.ts.map +1 -0
  70. package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.js +9 -0
  71. package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.d.ts +17 -0
  72. package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.d.ts.map +1 -0
  73. package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.js +16 -0
  74. package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.d.ts +16 -0
  75. package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.d.ts.map +1 -0
  76. package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.js +15 -0
  77. package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.d.ts +15 -0
  78. package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.d.ts.map +1 -0
  79. package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.js +29 -0
  80. package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.d.ts +7 -0
  81. package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.d.ts.map +1 -0
  82. package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.js +6 -0
  83. package/src/shade/device/timing/profile/SGPT_MAGIC.d.ts +7 -0
  84. package/src/shade/device/timing/profile/SGPT_MAGIC.d.ts.map +1 -0
  85. package/src/shade/device/timing/profile/SGPT_MAGIC.js +6 -0
  86. package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.d.ts +16 -0
  87. package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.d.ts.map +1 -0
  88. package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.js +15 -0
  89. package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.d.ts +7 -0
  90. package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.d.ts.map +1 -0
  91. package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.js +6 -0
  92. package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.d.ts +13 -0
  93. package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.d.ts.map +1 -0
  94. package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.js +23 -0
  95. package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.d.ts +12 -0
  96. package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.d.ts.map +1 -0
  97. package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.js +11 -0
  98. package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.d.ts +18 -0
  99. package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.d.ts.map +1 -0
  100. package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.js +46 -0
  101. package/src/shade/device/timing/profile/frame_graph_extract_topology.d.ts +20 -0
  102. package/src/shade/device/timing/profile/frame_graph_extract_topology.d.ts.map +1 -0
  103. package/src/shade/device/timing/profile/frame_graph_extract_topology.js +174 -0
  104. package/src/shade/device/timing/profile/make_profiling_pass_encoder.d.ts +21 -0
  105. package/src/shade/device/timing/profile/make_profiling_pass_encoder.d.ts.map +1 -0
  106. package/src/shade/device/timing/profile/make_profiling_pass_encoder.js +114 -0
  107. package/src/shade/device/timing/profile/parse_workgroup_size.d.ts +20 -0
  108. package/src/shade/device/timing/profile/parse_workgroup_size.d.ts.map +1 -0
  109. package/src/shade/device/timing/profile/parse_workgroup_size.js +43 -0
  110. package/src/shade/device/timing/profile/sgpt_fourcc.d.ts +18 -0
  111. package/src/shade/device/timing/profile/sgpt_fourcc.d.ts.map +1 -0
  112. package/src/shade/device/timing/profile/sgpt_fourcc.js +34 -0
  113. package/src/shade/device/timing/profile/sgpt_frame_codec.d.ts +42 -0
  114. package/src/shade/device/timing/profile/sgpt_frame_codec.d.ts.map +1 -0
  115. package/src/shade/device/timing/profile/sgpt_frame_codec.js +172 -0
  116. package/src/shade/device/timing/profile/sgpt_meta_codec.d.ts +20 -0
  117. package/src/shade/device/timing/profile/sgpt_meta_codec.d.ts.map +1 -0
  118. package/src/shade/device/timing/profile/sgpt_meta_codec.js +65 -0
  119. package/src/shade/device/timing/profile/sgpt_read_capture.d.ts +19 -0
  120. package/src/shade/device/timing/profile/sgpt_read_capture.d.ts.map +1 -0
  121. package/src/shade/device/timing/profile/sgpt_read_capture.js +134 -0
  122. package/src/shade/device/timing/profile/sgpt_read_header.d.ts +22 -0
  123. package/src/shade/device/timing/profile/sgpt_read_header.d.ts.map +1 -0
  124. package/src/shade/device/timing/profile/sgpt_read_header.js +94 -0
  125. package/src/shade/device/timing/profile/sgpt_read_records.d.ts +27 -0
  126. package/src/shade/device/timing/profile/sgpt_read_records.d.ts.map +1 -0
  127. package/src/shade/device/timing/profile/sgpt_read_records.js +153 -0
  128. package/src/shade/device/timing/profile/sgpt_topology_codec.d.ts +54 -0
  129. package/src/shade/device/timing/profile/sgpt_topology_codec.d.ts.map +1 -0
  130. package/src/shade/device/timing/profile/sgpt_topology_codec.js +329 -0
  131. package/src/shade/device/timing/profile/sgpt_write_header.d.ts +22 -0
  132. package/src/shade/device/timing/profile/sgpt_write_header.d.ts.map +1 -0
  133. package/src/shade/device/timing/profile/sgpt_write_header.js +47 -0
  134. package/src/shade/device/timing/profile/sgpt_write_record.d.ts +23 -0
  135. package/src/shade/device/timing/profile/sgpt_write_record.d.ts.map +1 -0
  136. package/src/shade/device/timing/profile/sgpt_write_record.js +62 -0
  137. package/src/shade/playground/volumetrics_froxel/README.md +88 -0
  138. package/src/shade/playground/volumetrics_froxel/index.html +251 -0
  139. package/src/shade/playground/volumetrics_froxel/main.d.ts +2 -0
  140. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -0
  141. package/src/shade/playground/volumetrics_froxel/main.js +619 -0
  142. package/src/shade/renderer/Renderer.d.ts +34 -0
  143. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  144. package/src/shade/renderer/Renderer.js +2536 -2455
  145. package/src/shade/renderer/volumetrics/NOTES.md +42 -1
  146. package/src/shade/renderer/volumetrics/chunk_camera_ray_planar_depth_scale.d.ts +21 -0
  147. package/src/shade/renderer/volumetrics/chunk_camera_ray_planar_depth_scale.d.ts.map +1 -0
  148. package/src/shade/renderer/volumetrics/chunk_camera_ray_planar_depth_scale.js +47 -0
  149. package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.d.ts.map +1 -1
  150. package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js +19 -8
  151. package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_content_offset.d.ts +37 -0
  152. package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_content_offset.d.ts.map +1 -0
  153. package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_content_offset.js +56 -0
  154. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  155. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +6 -0
  156. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
  157. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +367 -324
  158. package/src/shade/renderer/volumetrics/shader_volumetrics_build_scattering_lut.d.ts.map +1 -1
  159. package/src/shade/renderer/volumetrics/shader_volumetrics_build_scattering_lut.js +14 -3
  160. package/src/shade/renderer/volumetrics/volumetrics_position_world_to_froxel_uvw.d.ts.map +1 -1
  161. package/src/shade/renderer/volumetrics/volumetrics_position_world_to_froxel_uvw.js +48 -36
  162. package/src/engine/save/GameStateLoader.d.ts +0 -49
  163. package/src/engine/save/GameStateLoader.d.ts.map +0 -1
  164. package/src/engine/save/GameStateLoader.js +0 -168
@@ -0,0 +1,62 @@
1
+ import { assert } from "../../../../core/assert.js";
2
+ import { crc32 } from "../../../../core/binary/hash/crc32.js";
3
+ import { SGPT_RECORD_HEADER_SIZE } from "./SGPT_RECORD_HEADER_SIZE.js";
4
+ import { SGPT_RECORD_SYNC } from "./SGPT_RECORD_SYNC.js";
5
+
6
+ /**
7
+ * Append one framed record, encoding its payload through `write_payload`.
8
+ *
9
+ * The payload is written first, into the same buffer, and the framing is filled in behind it once
10
+ * its length and checksum are known. That ordering is what keeps a record a single pass: the
11
+ * alternative — encode to a scratch buffer, measure, copy — costs an allocation and a copy per
12
+ * frame, every frame, for nothing.
13
+ *
14
+ * A record is complete or it is absent. If `write_payload` throws, `position` is restored to where
15
+ * the record would have begun, so a failed encode leaves no half-record for a reader to trip over.
16
+ *
17
+ * @template T
18
+ * @param {BinaryBuffer} buffer positioned where the record should start
19
+ * @param {number} type one of {@link SGPT_RECORD_TYPE}
20
+ * @param {T} data handed to `write_payload`
21
+ * @param {function(BinaryBuffer, T): void} write_payload
22
+ * @returns {number} bytes written, framing included
23
+ *
24
+ * @author Alex Goldring
25
+ * @copyright Company Named Limited (c) 2026
26
+ */
27
+ export function sgpt_write_record(buffer, type, data, write_payload) {
28
+ assert.defined(buffer, 'buffer');
29
+ assert.isInteger(type, 'type');
30
+ assert.isFunction(write_payload, 'write_payload');
31
+
32
+ const record_start = buffer.position;
33
+ const payload_start = record_start + SGPT_RECORD_HEADER_SIZE;
34
+
35
+ buffer.position = payload_start;
36
+
37
+ try {
38
+ write_payload(buffer, data);
39
+ } catch (e) {
40
+ buffer.position = record_start;
41
+
42
+ throw e;
43
+ }
44
+
45
+ const payload_end = buffer.position;
46
+ const payload_byte_length = payload_end - payload_start;
47
+
48
+ const checksum = crc32(buffer.raw_bytes, payload_start, payload_byte_length);
49
+
50
+ buffer.position = record_start;
51
+
52
+ buffer.writeUint32(SGPT_RECORD_SYNC);
53
+ buffer.writeUint32(type);
54
+ buffer.writeUint32(payload_byte_length);
55
+ buffer.writeUint32(checksum);
56
+
57
+ assert.equal(buffer.position, payload_start, 'record framing size mismatch');
58
+
59
+ buffer.position = payload_end;
60
+
61
+ return payload_end - record_start;
62
+ }
@@ -0,0 +1,88 @@
1
+ # volumetrics_froxel
2
+
3
+ Real participating media, a movable sun and a wide-FOV camera in front of the froxel volumetrics
4
+ pass.
5
+
6
+ ```bash
7
+ npm run dev --workspace @woosh/meep-engine
8
+ ```
9
+
10
+ then open <http://localhost:5173/src/shade/playground/volumetrics_froxel/index.html>.
11
+
12
+ ## Why it exists
13
+
14
+ `shader_volumetrics_build_lighting` and the passes around it were reviewed against current practice.
15
+ Five correctness findings came out of it; four were fixed and one was closed as a documented
16
+ non-issue. All five were geometric, and geometry is the class of defect a unit test can prove but
17
+ not show — an emulator can assert that a sample lands at the wrong depth and still not tell you
18
+ whether that reads as a smear, a seam, or nothing at all. This page was where that got decided.
19
+
20
+ | finding | what it was | where the fix lives |
21
+ |---|---|---|
22
+ | C-01a | Planar view depth walked as a distance along a unit ray, so the froxel sample fell short by cos θ | `chunk_camera_ray_planar_depth_scale`, from the lighting pass |
23
+ | C-01b | The aerial pass integrated Beer's law over a planar depth difference rather than arc length | same chunk, from the aerial pass — this was the visible half |
24
+ | C-02 | Cell-averaged grids read with the boundary-sampled convention, a ~0.53-froxel bias | `chunk_volumetrics_froxel_content_offset` |
25
+ | C-03 | Shadow ray clipped against a frustum with no far plane, so a sun anywhere in the view cone got zero optical depth | `chunk_integrate_optical_depth` takes the metadata frustum |
26
+ | C-04 | The shadow march addressed the froxel grid with the aerial LUT's depth curve | `volumetrics_position_world_to_froxel_uvw` |
27
+ | C-05 | Adjacent columns carry independent jitter, so an XY fetch mixes depths | closed — zero-mean and TAA-absorbable; `volumetrics/NOTES.md` has the measurements and why the obvious fix makes it worse |
28
+
29
+ Those fixes are in the permanent path now and there is nothing left to A/B. What the page is for is
30
+ the *next* change here — the performance findings especially, since every one of them trades image
31
+ quality for time and none can be judged from a screenshot.
32
+
33
+ ## Measuring, rather than eyeballing
34
+
35
+ This pass is stochastic and temporally filtered, so most changes to it move the image by less than
36
+ the frame-to-frame noise. A screenshot pair is not evidence. Four traps, each of which produced a
37
+ confident wrong answer during the work above:
38
+
39
+ - **Auto exposure.** It keeps adapting for seconds after any change, so two captures of the
40
+ *identical* setting differed in mean luminance by more than the change being judged. Off by
41
+ default here; `renderer.exposure_compensation` sets the fixed value.
42
+ - **Per-frame jitter.** The column jitter and the sample RNG both key on `view.frame_index`, so
43
+ consecutive frames genuinely differ. Average 18+ frames per capture.
44
+ - **No noise floor, no result.** Capture the *same* setting twice and difference those first. If the
45
+ change's delta is not clearly above that, nothing has been shown.
46
+ - **The noise floor has to pay the same costs the signal does.** Anything that calls
47
+ `indicate_view_change()` resets temporal accumulation, so a capture taken right after one carries
48
+ a re-convergence transient that a back-to-back capture does not. Measuring the floor without that
49
+ in the middle inflates every result — it made C-04 fail a test it should have passed. Capture as
50
+ *change → settle ~45 frames → average*, and take the floor by making an identical no-op change so
51
+ both paths cost the same.
52
+
53
+ Prefer the **signed mean** when the change has a direction; it discriminates far better than the
54
+ mean-absolute, which is dominated by noise. C-01b read −0.148 at 60° FOV against a floor of ±0.05,
55
+ and −1.071 at 110°: same fix, and that 7× is the `1/cos θ` scaling it predicts.
56
+
57
+ Also: mutating `light.intensity` does not mark the light buffer dirty, so a sweep of it renders
58
+ identically. Remove and re-add the light instead.
59
+
60
+ ## Scene
61
+
62
+ `flying_world_-_battle_of_the_trash_god/v2`, six participating-media volumes, a low sun, and the
63
+ Venice sunset environment. Both the volumes and most of the camera placements are carried over from
64
+ the sibling repo's playground, where they were found by hand against this exact model — keeping the
65
+ numbers means a before/after pair is taken from the same place to the pixel.
66
+
67
+ Assets live under `test_assets/`, which is gitignored; `test_assets/README.md` has how they get
68
+ there. This page needs:
69
+
70
+ ```bash
71
+ mkdir -p "test_assets/flying_world_-_battle_of_the_trash_god/v2"
72
+ cp "H:/git/meep-renderer-deferred-algo/public/flying_world_-_battle_of_the_trash_god/v2/scene.gltf" "H:/git/meep-renderer-deferred-algo/public/flying_world_-_battle_of_the_trash_god/v2/scene.bin" "test_assets/flying_world_-_battle_of_the_trash_god/v2/"
73
+ cp "H:/git/meep-renderer-deferred-algo/public/environments/octahedral/venice_sunset_2k.hdr" test_assets/environments/octahedral/
74
+ ```
75
+
76
+ Only `scene.gltf` and `scene.bin` are needed from that directory — about 26 MB. glTF rather than the
77
+ `.shade` sitting beside it: that export's header reads format version 1 and `deserialize_scene`
78
+ accepts version 2 only, deliberately and with no backward compatibility.
79
+
80
+ ## Notes
81
+
82
+ Two viewpoints from the sibling repo (`v_volumetrics_light_extinction_0` and `_1`) sit inside
83
+ geometry here and were replaced with placements verified against this build. *Log camera* writes the
84
+ current placement to the console in the form the `VIEWS` table takes, which is how to add more.
85
+
86
+ Frame time settles around 28 ms at 1920×1080 on a 4070-class part. That is the whole frame, not the
87
+ volumetrics pass — separating it out needs a timestamp query. It is the baseline the performance
88
+ findings (P-01 … P-06) would move.
@@ -0,0 +1,251 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>meep — froxel volumetrics</title>
6
+ <style>
7
+ :root {
8
+ color-scheme: dark;
9
+ --panel: rgba(20, 22, 26, 0.88);
10
+ --line: #2b3038;
11
+ --text: #d6dae0;
12
+ --muted: #8b939f;
13
+ --pass: #55c46a;
14
+ --fail: #ff6b6b;
15
+ --warn: #e5b54a;
16
+ --tag: #c98b3f;
17
+ }
18
+
19
+ * { box-sizing: border-box; }
20
+
21
+ html, body { margin: 0; height: 100%; overflow: hidden; background: #0d0f12; }
22
+
23
+ /* body carries tabindex so the camera controller's keyboard device has something focusable
24
+ to listen on; the focus ring around the whole viewport is not wanted */
25
+ body:focus { outline: none; }
26
+
27
+ #view { display: block; width: 100vw; height: 100vh; }
28
+
29
+ #panel {
30
+ position: fixed;
31
+ top: 12px;
32
+ left: 12px;
33
+ width: 330px;
34
+ max-height: calc(100vh - 24px);
35
+ overflow-y: auto;
36
+ padding: 12px 14px;
37
+ background: var(--panel);
38
+ border: 1px solid var(--line);
39
+ border-radius: 8px;
40
+ backdrop-filter: blur(6px);
41
+ color: var(--text);
42
+ font: 13px/1.5 system-ui, -apple-system, Segoe UI, sans-serif;
43
+ }
44
+
45
+ #panel h1 { font-size: 13px; font-weight: 600; margin: 0 0 8px; }
46
+
47
+ h2 {
48
+ font-size: 11px;
49
+ font-weight: 600;
50
+ text-transform: uppercase;
51
+ letter-spacing: 0.06em;
52
+ color: var(--muted);
53
+ margin: 0 0 6px;
54
+ }
55
+
56
+ label { display: flex; align-items: center; gap: 8px; padding: 3px 0; cursor: pointer; }
57
+ label input[type="checkbox"] { margin: 0; }
58
+ label .hint { color: var(--muted); font-size: 11.5px; }
59
+
60
+ hr { border: 0; border-top: 1px solid var(--line); margin: 10px 0; }
61
+
62
+ button {
63
+ font: inherit;
64
+ padding: 5px 11px;
65
+ border-radius: 6px;
66
+ border: 1px solid var(--line);
67
+ background: #1c1f25;
68
+ color: var(--text);
69
+ cursor: pointer;
70
+ }
71
+
72
+ button:hover { border-color: #3d4552; }
73
+ button:focus-visible { outline: 2px solid var(--tag); outline-offset: 2px; }
74
+
75
+ #views { display: flex; flex-direction: column; gap: 4px; }
76
+
77
+ button.view {
78
+ text-align: left;
79
+ display: flex;
80
+ align-items: baseline;
81
+ gap: 8px;
82
+ padding: 4px 9px;
83
+ font-size: 12.5px;
84
+ }
85
+
86
+ .tag {
87
+ font: 10px ui-monospace, SFMono-Regular, Consolas, monospace;
88
+ color: var(--tag);
89
+ border: 1px solid currentColor;
90
+ border-radius: 3px;
91
+ padding: 0 4px;
92
+ flex: 0 0 auto;
93
+ }
94
+
95
+ .slider {
96
+ display: grid;
97
+ grid-template-columns: 1fr auto;
98
+ align-items: center;
99
+ gap: 2px 8px;
100
+ padding: 4px 0;
101
+ }
102
+
103
+ .slider .name { font-size: 12px; }
104
+ .slider .value {
105
+ font: 12px ui-monospace, SFMono-Regular, Consolas, monospace;
106
+ color: var(--muted);
107
+ text-align: right;
108
+ min-width: 44px;
109
+ }
110
+ .slider input[type="range"] { grid-column: 1 / -1; width: 100%; margin: 0; }
111
+
112
+ .row { display: flex; gap: 6px; align-items: center; }
113
+ .row button { flex: 1 1 auto; }
114
+
115
+ #status { font-size: 12px; color: var(--muted); word-break: break-word; margin-top: 6px; }
116
+ #status.pass { color: var(--pass); }
117
+ #status.fail { color: var(--fail); }
118
+ #status.warn { color: var(--warn); }
119
+
120
+ #stats {
121
+ margin-top: 4px;
122
+ font: 12px ui-monospace, SFMono-Regular, Consolas, monospace;
123
+ color: var(--text);
124
+ }
125
+
126
+ p.note { margin: 6px 0 0; font-size: 11.5px; color: var(--muted); }
127
+ </style>
128
+ </head>
129
+ <body tabindex="0">
130
+
131
+ <canvas id="view"></canvas>
132
+
133
+ <div id="panel">
134
+ <h1>Froxel volumetrics</h1>
135
+
136
+ <h2>Viewpoints</h2>
137
+ <div id="views"></div>
138
+
139
+ <hr>
140
+
141
+ <h2>Sun</h2>
142
+
143
+ <div class="row">
144
+ <button id="sun_into_view">Sun into view</button>
145
+ <button id="sun_behind">Sun behind</button>
146
+ </div>
147
+ <p class="note">
148
+ Places the sun on the camera's forward axis, or directly opposite it. Media self-shadowing
149
+ depends on the shadow ray's path through the grid, so swinging the sun through the frustum
150
+ is the quickest way to exercise it.
151
+ </p>
152
+
153
+ <div class="slider">
154
+ <span class="name">Azimuth</span>
155
+ <span class="value" id="sun_azimuth_value">200°</span>
156
+ <input type="range" id="sun_azimuth" min="0" max="359" step="1" value="200">
157
+ </div>
158
+
159
+ <div class="slider">
160
+ <span class="name">Elevation</span>
161
+ <span class="value" id="sun_elevation_value">12°</span>
162
+ <input type="range" id="sun_elevation" min="1" max="80" step="1" value="12">
163
+ </div>
164
+
165
+ <hr>
166
+
167
+ <h2>Camera</h2>
168
+
169
+ <div class="slider">
170
+ <span class="name">Field of view</span>
171
+ <span class="value" id="fov_value">60°</span>
172
+ <input type="range" id="fov" min="30" max="110" step="1" value="60">
173
+ </div>
174
+ <p class="note">
175
+ Froxel geometry is angle-dependent — the planar depth curve and the ray it is walked along
176
+ diverge as cos&nbsp;θ. A wide FOV is where anything that confuses the two shows up, so open
177
+ this to 110° when judging a change to the grid.
178
+ </p>
179
+
180
+ <hr>
181
+
182
+ <h2>Volumetrics</h2>
183
+
184
+ <label>
185
+ <input type="checkbox" id="vol_taa" checked>
186
+ <span>Volumetric TAA <span class="hint">— off exposes the raw per-frame noise</span></span>
187
+ </label>
188
+
189
+ <label>
190
+ <input type="checkbox" id="taa" checked>
191
+ <span>Renderer TAA</span>
192
+ </label>
193
+
194
+ <label>
195
+ <input type="checkbox" id="auto_exposure">
196
+ <span>Auto exposure <span class="hint">— off: A/B is meaningless with it on</span></span>
197
+ </label>
198
+ <p class="note">
199
+ Eye adaptation drifts for seconds after any change, so two captures of the same setting can
200
+ differ by more than the change being judged. It starts off here for that reason; turn it on
201
+ only to see how something reads in a normally-exposed frame.
202
+ </p>
203
+
204
+ <div class="slider">
205
+ <span class="name">Froxel far</span>
206
+ <span class="value" id="froxel_far_value">off</span>
207
+ <input type="range" id="froxel_far" min="20" max="120" step="5" value="120">
208
+ </div>
209
+ <p class="note">
210
+ Packs the froxel grid into a shorter range than <code>camera.far</code>, so the near field
211
+ gets more slices and the aerial pass ray-marches the volumes beyond it. At <em>off</em> the
212
+ grid spans the whole range and the two depth curves coincide.
213
+ </p>
214
+
215
+ <div class="slider">
216
+ <span class="name">Multiscatter octaves</span>
217
+ <span class="value" id="ms_octaves_value">6</span>
218
+ <input type="range" id="ms_octaves" min="1" max="8" step="1" value="6">
219
+ </div>
220
+
221
+ <hr>
222
+
223
+ <div class="row">
224
+ <button id="pause">Pause</button>
225
+ <button id="log_camera">Log camera</button>
226
+ </div>
227
+
228
+ <div id="status"></div>
229
+ <div id="stats"></div>
230
+
231
+ <p class="note">
232
+ Drag to orbit, wheel to zoom, WASD to move. <em>Log camera</em> writes the current placement
233
+ to the console in the form the <code>VIEWS</code> table takes.
234
+ </p>
235
+ </div>
236
+
237
+ <script type="module">
238
+ import { start } from './main.js';
239
+
240
+ start().catch(error => {
241
+ const status = document.getElementById('status');
242
+
243
+ status.className = 'fail';
244
+ status.textContent = String(error?.message ?? error);
245
+
246
+ console.error(error);
247
+ });
248
+ </script>
249
+
250
+ </body>
251
+ </html>
@@ -0,0 +1,2 @@
1
+ export function start(): Promise<void>;
2
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/volumetrics_froxel/main.js"],"names":[],"mappings":"AA+TA,uCA2SC"}