@woosh/meep-engine 3.60.3 → 3.60.5

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 (514) hide show
  1. package/CHANGELOG.md +2602 -0
  2. package/CONTRIBUTING.md +7 -0
  3. package/MIGRATION_2_TO_3.md +136 -0
  4. package/SECURITY.md +4 -0
  5. package/editor/EditorCameraNavigation.d.ts +2 -1
  6. package/editor/EditorCameraNavigation.d.ts.map +1 -1
  7. package/editor/EditorCameraNavigation.js +51 -57
  8. package/editor/ecs/component/editors/ecs/Trail3DEditor.d.ts +3 -0
  9. package/editor/ecs/component/editors/ecs/Trail3DEditor.d.ts.map +1 -1
  10. package/editor/ecs/component/editors/ecs/Trail3DEditor.js +6 -0
  11. package/package.json +6 -2
  12. package/src/engine/EngineHarness.d.ts +3 -9
  13. package/src/engine/EngineHarness.d.ts.map +1 -1
  14. package/src/engine/EngineHarness.js +6 -8
  15. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts +9 -6
  16. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts.map +1 -1
  17. package/src/engine/graphics/camera/makeOrbitalCameraController.js +44 -71
  18. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts +64 -0
  19. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts.map +1 -0
  20. package/src/engine/graphics/camera/orbital/MomentumAxis.js +187 -0
  21. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts +86 -0
  22. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts.map +1 -0
  23. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.js +319 -0
  24. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts +34 -10
  25. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
  26. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +116 -30
  27. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +1 -7
  28. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  29. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +1 -37
  30. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometryFlags.js +1 -2
  31. package/src/engine/graphics/ecs/trail3d/TRAIL3D_FADE_LINEAR.d.ts +14 -0
  32. package/src/engine/graphics/ecs/trail3d/TRAIL3D_FADE_LINEAR.d.ts.map +1 -0
  33. package/src/engine/graphics/ecs/trail3d/TRAIL3D_FADE_LINEAR.js +22 -0
  34. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +19 -3
  35. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
  36. package/src/engine/graphics/ecs/trail3d/Trail3D.js +31 -4
  37. package/src/engine/graphics/ecs/trail3d/advance_trail.js +1 -1
  38. package/src/engine/graphics/ecs/water/Water.d.ts +8 -0
  39. package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
  40. package/src/engine/graphics/ecs/water/Water.js +8 -0
  41. package/src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.d.ts +5 -5
  42. package/src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.d.ts.map +1 -1
  43. package/src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.js +6 -6
  44. package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
  45. package/src/engine/graphics3/WaterSystem.js +731 -734
  46. package/src/engine/graphics3/decal/DecalTileShader.d.ts +61 -0
  47. package/src/engine/graphics3/decal/DecalTileShader.d.ts.map +1 -0
  48. package/src/engine/graphics3/decal/DecalTileShader.js +138 -0
  49. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts +10 -4
  50. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
  51. package/src/engine/graphics3/decal/GPUDecalRenderer.js +149 -44
  52. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts +25 -0
  53. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts.map +1 -0
  54. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.js +55 -0
  55. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts +22 -0
  56. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts.map +1 -0
  57. package/src/engine/graphics3/decal/graph_gather_decal_tiles.js +70 -0
  58. package/src/engine/graphics3/decal/shader_decal_composite.d.ts +7 -5
  59. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  60. package/src/engine/graphics3/decal/shader_decal_composite.js +48 -44
  61. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts +12 -0
  62. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts.map +1 -0
  63. package/src/engine/graphics3/decal/shader_decal_tile_copy.js +78 -0
  64. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts +11 -0
  65. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts.map +1 -0
  66. package/src/engine/graphics3/decal/shader_gather_decal_tiles.js +80 -0
  67. package/src/engine/graphics3/trail/trail_tube_write_geometry.d.ts +10 -0
  68. package/src/engine/graphics3/trail/trail_tube_write_geometry.d.ts.map +1 -1
  69. package/src/engine/graphics3/trail/trail_tube_write_geometry.js +13 -1
  70. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts +13 -0
  71. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts.map +1 -0
  72. package/src/engine/input/devices/mouse/MouseEventButtons.js +23 -0
  73. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts +4 -0
  74. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
  75. package/src/engine/input/ecs/controllers/KeyboardCameraController.js +9 -45
  76. package/src/engine/physics/cloth/ecs/cloth_geodesic.d.ts +2 -2
  77. package/src/engine/physics/cloth/ecs/cloth_geodesic.js +2 -2
  78. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  79. package/src/shade/playground/probe_clipmap/README.md +1 -0
  80. package/src/shade/playground/probe_clipmap/index.html +5 -0
  81. package/src/shade/playground/probe_clipmap/main.d.ts.map +1 -1
  82. package/src/shade/playground/probe_clipmap/main.js +16 -2
  83. package/src/shade/playground/vgeo_scene/README.md +1 -1
  84. package/src/shade/playground/water_refraction/README.md +5 -6
  85. package/src/shade/playground/water_refraction/index.html +1 -1
  86. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  87. package/src/shade/renderer/Renderer.js +7 -5
  88. package/src/shade/renderer/atmosphere/MULTISCATTER_LUT_CONFIG.d.ts +4 -6
  89. package/src/shade/renderer/atmosphere/MULTISCATTER_LUT_CONFIG.d.ts.map +1 -1
  90. package/src/shade/renderer/atmosphere/MULTISCATTER_LUT_CONFIG.js +34 -36
  91. package/src/shade/renderer/atmosphere/chunk_get_multiscatter_values.d.ts +1 -1
  92. package/src/shade/renderer/atmosphere/chunk_get_multiscatter_values.d.ts.map +1 -1
  93. package/src/shade/renderer/atmosphere/chunk_get_multiscatter_values.js +162 -163
  94. package/src/shade/renderer/atmosphere/compute_mie_absorption.d.ts +1 -1
  95. package/src/shade/renderer/atmosphere/compute_mie_absorption.js +1 -1
  96. package/src/shade/renderer/atmosphere/hillaire/ATMOSPHERE_EARTH.js +1 -1
  97. package/src/shade/renderer/atmosphere/hillaire/ATMOSPHERE_PARAMETERS_STRUCT.js +1 -1
  98. package/src/shade/renderer/atmosphere/shader_multiscatter_lut.js +164 -164
  99. package/src/shade/renderer/atmosphere/shader_sky_irradiance_lut.js +1 -1
  100. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
  101. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  102. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
  103. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  104. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  105. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  106. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  107. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  108. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +4 -4
  109. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +4 -4
  110. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +2 -1
  111. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  112. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +18 -1
  113. package/src/shade/renderer/geometry/sdf/cards/mesh_cards_raster_pass_descriptor.js +2 -2
  114. package/src/shade/renderer/geometry/sdf/cards/shader_mesh_cards_histogram.d.ts +1 -1
  115. package/src/shade/renderer/geometry/sdf/cards/shader_mesh_cards_histogram.js +1 -1
  116. package/src/shade/renderer/geometry/sdf/chunk_blas_query_band_sdf.d.ts +4 -5
  117. package/src/shade/renderer/geometry/sdf/chunk_blas_query_band_sdf.d.ts.map +1 -1
  118. package/src/shade/renderer/geometry/sdf/chunk_blas_query_band_sdf.js +6 -7
  119. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
  120. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
  121. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
  122. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
  123. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
  124. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
  125. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
  126. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
  127. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  128. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +2 -8
  129. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +5 -5
  130. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  131. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +10 -10
  132. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSettings.d.ts +12 -0
  133. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSettings.d.ts.map +1 -1
  134. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSettings.js +22 -0
  135. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +4 -4
  136. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  137. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +50 -13
  138. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.d.ts +6 -0
  139. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.d.ts.map +1 -1
  140. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.js +7 -0
  141. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP_RESAMPLING.d.ts +13 -0
  142. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP_RESAMPLING.d.ts.map +1 -0
  143. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP_RESAMPLING.js +13 -0
  144. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.d.ts +17 -0
  145. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.d.ts.map +1 -1
  146. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.js +20 -0
  147. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
  148. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
  149. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
  150. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_resampled.d.ts +18 -0
  151. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_resampled.d.ts.map +1 -0
  152. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_resampled.js +42 -0
  153. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_sample_radiance.d.ts.map +1 -1
  154. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_sample_radiance.js +5 -4
  155. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.d.ts +4 -0
  156. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.d.ts.map +1 -1
  157. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.js +13 -2
  158. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_resample.d.ts +52 -0
  159. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_resample.d.ts.map +1 -0
  160. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_resample.js +250 -0
  161. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  162. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
  163. package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
  164. package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
  165. package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
  166. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
  167. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
  168. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
  169. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
  170. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
  171. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
  172. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
  173. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  174. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
  175. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
  176. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
  177. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
  178. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
  179. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
  180. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
  181. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
  182. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
  183. package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
  184. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
  185. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  186. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
  187. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
  188. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
  189. package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
  190. package/src/shade/renderer/light/environment/environment_level_1_blur_plan.d.ts +39 -0
  191. package/src/shade/renderer/light/environment/environment_level_1_blur_plan.d.ts.map +1 -0
  192. package/src/shade/renderer/light/environment/environment_level_1_blur_plan.js +119 -0
  193. package/src/shade/renderer/light/environment/execute_filter_environment_map.d.ts +15 -7
  194. package/src/shade/renderer/light/environment/execute_filter_environment_map.d.ts.map +1 -1
  195. package/src/shade/renderer/light/environment/execute_filter_environment_map.js +66 -83
  196. package/src/shade/renderer/light/environment/graph_filter_environment_map.d.ts.map +1 -1
  197. package/src/shade/renderer/light/environment/graph_filter_environment_map.js +15 -6
  198. package/src/shade/renderer/light/environment/shader_blur_environment_map.d.ts +3 -0
  199. package/src/shade/renderer/light/environment/shader_blur_environment_map.d.ts.map +1 -0
  200. package/src/shade/renderer/light/environment/shader_blur_environment_map.js +111 -0
  201. package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_blurred.d.ts +3 -0
  202. package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_blurred.d.ts.map +1 -0
  203. package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_blurred.js +128 -0
  204. package/src/shade/renderer/light/environment/shader_downsample_environment_map.d.ts.map +1 -1
  205. package/src/shade/renderer/light/environment/shader_downsample_environment_map.js +0 -1
  206. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  207. package/src/shade/renderer/material/GPUMaterialManager.d.ts +7 -0
  208. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  209. package/src/shade/renderer/material/GPUMaterialManager.js +11 -0
  210. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
  211. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  212. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
  213. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -3
  214. package/src/shade/renderer/postprocess/nss/NSS.d.ts.map +1 -1
  215. package/src/shade/renderer/postprocess/nss/NSS.js +1 -3
  216. package/src/shade/renderer/postprocess/nss/NSS_NETWORK_SCHEMA.d.ts +5 -6
  217. package/src/shade/renderer/postprocess/nss/NSS_NETWORK_SCHEMA.d.ts.map +1 -1
  218. package/src/shade/renderer/postprocess/nss/NSS_NETWORK_SCHEMA.js +9 -10
  219. package/src/shade/renderer/postprocess/nss/shader_nss_extract_feedback.d.ts.map +1 -1
  220. package/src/shade/renderer/postprocess/nss/shader_nss_extract_feedback.js +48 -49
  221. package/src/shade/renderer/postprocess/nss/shader_nss_postprocess.d.ts.map +1 -1
  222. package/src/shade/renderer/postprocess/nss/shader_nss_postprocess.js +12 -16
  223. package/src/shade/renderer/postprocess/nss/shader_nss_preprocess.js +246 -246
  224. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.d.ts +1 -2
  225. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.d.ts.map +1 -1
  226. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.js +34 -35
  227. package/src/shade/renderer/shader/chunk/atmosphere/C_MIE_SCATTERING_DECLARATION.d.ts +2 -2
  228. package/src/shade/renderer/shader/chunk/atmosphere/C_MIE_SCATTERING_DECLARATION.js +2 -2
  229. package/src/shade/renderer/shader/chunk/atmosphere/chunk_compute_extinction.js +1 -1
  230. package/src/shade/renderer/shader/chunk/atmosphere/hillaire/DENSITY_PROFILE_LAYER_STRUCT.d.ts +1 -1
  231. package/src/shade/renderer/shader/chunk/atmosphere/hillaire/DENSITY_PROFILE_LAYER_STRUCT.js +1 -1
  232. package/src/shade/renderer/shader/chunk/brdf/fresnel/chunk_F_Schlick.d.ts +3 -3
  233. package/src/shade/renderer/shader/chunk/brdf/fresnel/chunk_F_Schlick.js +3 -3
  234. package/src/shade/renderer/shader/chunk/color/tonemap/aces/chunk_aces_ue_matrices.d.ts +9 -10
  235. package/src/shade/renderer/shader/chunk/color/tonemap/aces/chunk_aces_ue_matrices.d.ts.map +1 -1
  236. package/src/shade/renderer/shader/chunk/color/tonemap/aces/chunk_aces_ue_matrices.js +9 -10
  237. package/src/shade/renderer/shader/chunk/color/tonemap/aces/chunk_tonemap_ue_filmic.d.ts +4 -4
  238. package/src/shade/renderer/shader/chunk/color/tonemap/aces/chunk_tonemap_ue_filmic.js +5 -5
  239. package/src/shade/renderer/shader/chunk/math/chunk_f16_round_stochastic.d.ts +1 -2
  240. package/src/shade/renderer/shader/chunk/math/chunk_f16_round_stochastic.d.ts.map +1 -1
  241. package/src/shade/renderer/shader/chunk/math/chunk_f16_round_stochastic.js +1 -2
  242. package/src/shade/renderer/shader/chunk/noise/chunk_stbn_sample_vec2.d.ts +14 -1
  243. package/src/shade/renderer/shader/chunk/noise/chunk_stbn_sample_vec2.d.ts.map +1 -1
  244. package/src/shade/renderer/shader/chunk/noise/chunk_stbn_sample_vec2.js +19 -6
  245. package/src/shade/renderer/shader/chunk/noise/chunk_stbn_sample_vec3.d.ts +10 -1
  246. package/src/shade/renderer/shader/chunk/noise/chunk_stbn_sample_vec3.d.ts.map +1 -1
  247. package/src/shade/renderer/shader/chunk/noise/chunk_stbn_sample_vec3.js +14 -6
  248. package/src/shade/renderer/shader/chunk/random/murmur/chunk_murmur3_vec3u.d.ts +1 -1
  249. package/src/shade/renderer/shader/chunk/random/murmur/chunk_murmur3_vec3u.js +1 -1
  250. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  251. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
  252. package/src/shade/renderer/shadow/vsm/DEFAULT_VSM_MAX_DYNAMIC_LOD_BIAS.d.ts +2 -3
  253. package/src/shade/renderer/shadow/vsm/DEFAULT_VSM_MAX_DYNAMIC_LOD_BIAS.d.ts.map +1 -1
  254. package/src/shade/renderer/shadow/vsm/DEFAULT_VSM_MAX_DYNAMIC_LOD_BIAS.js +7 -8
  255. package/src/shade/renderer/shadow/vsm/DEFAULT_VSM_MAX_LOAD_FACTOR.d.ts +1 -1
  256. package/src/shade/renderer/shadow/vsm/DEFAULT_VSM_MAX_LOAD_FACTOR.js +12 -12
  257. package/src/shade/renderer/shadow/vsm/VSMPageManager.d.ts +2 -2
  258. package/src/shade/renderer/shadow/vsm/VSMPageManager.d.ts.map +1 -1
  259. package/src/shade/renderer/shadow/vsm/VSMPageManager.js +3 -4
  260. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
  261. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  262. package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
  263. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
  264. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
  265. package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
  266. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  267. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  268. package/src/shade/renderer/water/shader_water.js +22 -13
  269. package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
  270. package/src/shade/renderer/water/shader_water_avboit.js +7 -7
  271. package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
  272. package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
  273. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
  274. package/src/view/elements/progress/segmented/SegmentedResourceBarView.d.ts +0 -19
  275. package/src/view/elements/progress/segmented/SegmentedResourceBarView.d.ts.map +1 -1
  276. package/src/view/elements/progress/segmented/SegmentedResourceBarView.js +2 -52
  277. package/editor/REWORK_PLAN_2026_08.md +0 -692
  278. package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
  279. package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
  280. package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
  281. package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
  282. package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
  283. package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
  284. package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
  285. package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
  286. package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
  287. package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
  288. package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
  289. package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
  290. package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
  291. package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
  292. package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
  293. package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
  294. package/src/REVIEW_2026_08_06/adv-core.md +0 -511
  295. package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
  296. package/src/REVIEW_2026_08_06/adv-view.md +0 -446
  297. package/src/REVIEW_2026_08_06/api-core.md +0 -296
  298. package/src/REVIEW_2026_08_06/api-engine.md +0 -195
  299. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
  300. package/src/REVIEW_2026_08_06/core-binary.md +0 -348
  301. package/src/REVIEW_2026_08_06/core-collection.md +0 -376
  302. package/src/REVIEW_2026_08_06/core-math.md +0 -221
  303. package/src/REVIEW_2026_08_06/core-process.md +0 -266
  304. package/src/REVIEW_2026_08_06/core-science.md +0 -223
  305. package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
  306. package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
  307. package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
  308. package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
  309. package/src/REVIEW_2026_08_06/engine-input.md +0 -197
  310. package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
  311. package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
  312. package/src/REVIEW_2026_08_06/generation.md +0 -176
  313. package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
  314. package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
  315. package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
  316. package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
  317. package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
  318. package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
  319. package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
  320. package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
  321. package/src/REVIEW_2026_08_06/view.md +0 -667
  322. package/src/REVIEW_2026_08_06.md +0 -610
  323. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
  324. package/src/core/bvh2/bvh3/NOTES.md +0 -2
  325. package/src/core/bvh8/NOTES.md +0 -63
  326. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
  327. package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
  328. package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
  329. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
  330. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
  331. package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
  332. package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
  333. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
  334. package/src/core/geom/3d/gjk/NOTES.md +0 -8
  335. package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
  336. package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
  337. package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
  338. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
  339. package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
  340. package/src/engine/REVIEW_2026_07_03.md +0 -211
  341. package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
  342. package/src/engine/control/first-person/DESIGN.md +0 -658
  343. package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
  344. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
  345. package/src/engine/control/first-person/TODO.md +0 -160
  346. package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
  347. package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
  348. package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
  349. package/src/engine/ecs/ik/PLAN.md +0 -179
  350. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
  351. package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
  352. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
  353. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
  354. package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
  355. package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
  356. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
  357. package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
  358. package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
  359. package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
  360. package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
  361. package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
  362. package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
  363. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
  364. package/src/engine/network/CONGESTION_CONTROL.md +0 -87
  365. package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
  366. package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
  367. package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
  368. package/src/engine/physics/cloth/PLAN.md +0 -3056
  369. package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
  370. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
  371. package/src/engine/physics/fluid/solver/NOTES.md +0 -46
  372. package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
  373. package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
  374. package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
  375. package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
  376. package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
  377. package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
  378. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
  379. package/src/format/image/avif/DECISIONS.md +0 -545
  380. package/src/format/image/jpeg/NOTES.md +0 -3
  381. package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
  382. package/src/generation/COORDINATES.md +0 -54
  383. package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
  384. package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
  385. package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
  386. package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
  387. package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
  388. package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
  389. package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
  390. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
  391. package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
  392. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
  393. package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
  394. package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
  395. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
  396. package/src/shade/RENDERER_CONTRACT.md +0 -697
  397. package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
  398. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
  399. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
  400. package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
  401. package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
  402. package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
  403. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
  404. package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
  405. package/src/shade/renderer/NOTES.md +0 -78
  406. package/src/shade/renderer/PLAN.md +0 -8
  407. package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
  408. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
  409. package/src/shade/renderer/atmosphere/NOTES.md +0 -6
  410. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
  411. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
  412. package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
  413. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
  414. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
  415. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
  416. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
  417. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
  418. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
  419. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
  420. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
  421. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
  422. package/src/shade/renderer/camera/orbital/README.md +0 -81
  423. package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
  424. package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
  425. package/src/shade/renderer/camera/orbital/index.js +0 -9
  426. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
  427. package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
  428. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
  429. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
  430. package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
  431. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
  432. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
  433. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
  434. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
  435. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -483
  436. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
  437. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
  438. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
  439. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
  440. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
  441. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
  442. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
  443. package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -536
  444. package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
  445. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4225
  446. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
  447. package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
  448. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
  449. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
  450. package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
  451. package/src/shade/renderer/hiz/NOTES.md +0 -3
  452. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
  453. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
  454. package/src/shade/renderer/light/cluster/NOTES.md +0 -11
  455. package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
  456. package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
  457. package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_averages.d.ts +0 -13
  458. package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_averages.d.ts.map +0 -1
  459. package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_averages.js +0 -198
  460. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
  461. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
  462. package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
  463. package/src/shade/renderer/particles/DESIGN.md +0 -1011
  464. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -283
  465. package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
  466. package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
  467. package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
  468. package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
  469. package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
  470. package/src/shade/renderer/postprocess/nss/NOTES.md +0 -339
  471. package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
  472. package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
  473. package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
  474. package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
  475. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
  476. package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
  477. package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
  478. package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
  479. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
  480. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
  481. package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
  482. package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
  483. package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -394
  484. package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -160
  485. package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
  486. package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
  487. package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
  488. package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
  489. package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
  490. package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
  491. package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
  492. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
  493. package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
  494. package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -932
  495. package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
  496. package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
  497. package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
  498. package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
  499. package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
  500. package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
  501. package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
  502. package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
  503. package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
  504. package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
  505. package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -4
  506. package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
  507. package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
  508. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
  509. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
  510. package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
  511. package/src/shade/wgsl/NOTES.md +0 -4
  512. package/src/view/elements/progress/segmented/segment_frame_transform.d.ts +0 -15
  513. package/src/view/elements/progress/segmented/segment_frame_transform.d.ts.map +0 -1
  514. package/src/view/elements/progress/segmented/segment_frame_transform.js +0 -69
@@ -1,1329 +0,0 @@
1
- * https://shaderbits.com/blog/creating-volumetric-ray-marcher
2
- * "Physically Based and Unified Volumetric Rendering in Frostbite" SIGGRAPH 2015
3
- * "Creating the Atmospheric World of Red Dead Redemption 2: A Complete and Integrated Solution" SIGGRAPH 2019
4
- * "Volumetric Fog in Enshrouded" GPC 2024 (https://www.youtube.com/watch?v=OR8HbFnQdlk)
5
-
6
-
7
-
8
- Check out "A Novel Sampling Algorithm for Fast and Stable Real-Time Volume Rendering" SIGGRAPH 2015
9
- * https://github.com/huwb/volsample/blob/master/src/shadertoy/clouds.shader
10
- ## Blue noise for the froxel column jitter — measured, 2026-08-29
11
-
12
- Considered as a fix for C-05 (adjacent columns carry independent jitter, so a trilinear XY fetch
13
- mixes slabs at different depths) and as groundwork for cutting `SAMPLE_COUNT`. Measured against the
14
- shipped `stbn_vec*.bin` volumes rather than argued. Three findings, one of which kills the original
15
- rationale.
16
-
17
- **Reusing `stbn_vec3` is not an option — it biases, and the bias does not converge away.**
18
- The aerial pass already reads `stbn_sample_vec3(vec3(voxel_coord.xy, view.frame_index))` and
19
- consumes all three channels (`.xy` for the tile UV, `.z` for the slice dither). A column jitter
20
- indexed the same way is *the same number* — correlation 1.0000, not merely "the same set". Two
21
- nuisance variables locked together stop exploring the unit square and only ever visit its diagonal,
22
- so the estimator converges to the wrong integral:
23
-
24
- | integrand over (j, d) | truth | naive `stbn_vec3.x` converges to | error |
25
- |---|---|---|---|
26
- | `j*d` | 0.250 | 0.334 | +34% |
27
- | `exp(-3(j+d))` | 0.100 | 0.167 | +67% |
28
- | `step(j > d)` | 0.500 | 0.000 | −100% |
29
-
30
- RMSE is flat from N=32 to N=128 in every case. That is bias, not noise — no TAA window fixes it.
31
-
32
- **An independent source is fine, and converges faster than the hash.** `stbn_vec1` is a separate
33
- scalar 128×128×64 volume, already loaded by `STATIC_GRAPHICS_ENGINE_ASSETS`, uniform to 0.75%, and
34
- decorrelated from all three `stbn_vec3` channels (|r| ≤ 0.0018). RMSE on `j*d`: 0.0478 vs the
35
- murmur hash's 0.0632 at N=8, 0.0149 vs 0.0219 at N=64 — roughly 1.3–1.5× better across the window
36
- the 0.95 TAA blend actually spans (~39 effective samples). Bias ≤ 0.0025.
37
-
38
- Caveat: STBN stops improving past N=64, the volume's temporal depth, while white noise keeps going —
39
- at N=128 the hash wins. The TAA never reaches that regime, but anything accumulating longer would.
40
-
41
- **It does not fix C-05, it makes it slightly worse.** Blue noise is built to make neighbours
42
- *maximally different*; C-05 wants them *similar*. Measured adjacent-column jitter difference:
43
- hash 0.3331 (neighbour r = 0.0005, i.e. independent), `stbn_vec1` 0.3656 (neighbour r = −0.112,
44
- anti-correlated). So it widens the cross-column depth mixing by ~10%.
45
-
46
- Conclusion: C-05 stays closed as a documented non-issue — it is zero-mean, the TAA integrates it
47
- away, and the obvious fix aggravates the stated mechanism. Genuinely removing it means dropping the
48
- XY jitter (the TAA's 3×3×3 Blackman–Harris resample already does spatial reconstruction), not
49
- changing the noise source. Adopt `stbn_vec1` when cutting `SAMPLE_COUNT` (P-01), where better
50
- per-pixel temporal distribution is the actual goal — and never `stbn_vec3`, for the reason above.
51
-
52
- ## Shadowing the clustered lights — measured, 2026-08-29
53
-
54
- Q-01. The point and spot loops in `shader_volumetrics_build_lighting` never applied a shadow map, so
55
- geometry did not occlude a local light in the volume at all; only the medium's own optical depth
56
- did. The directional loop always had. Fixed by `chunk_volumetrics_local_light_shadow`, one nearest
57
- atlas tap per light.
58
-
59
- Two things about it were expected to be problems and measured not to be. The numbers below came from
60
- a `local_light_shadow_mode` word on the metadata that switched between off, that tap, and the
61
- deferred path's filter kernels; it and the two playground checkboxes over it have since been removed,
62
- which is why nothing in the tree reproduces these rows any more. Restoring the comparison means
63
- re-adding that switch.
64
-
65
- **The cost is nearly nothing, even at 130 shadow-casting lights.** Whole-frame median over 18
66
- interleaved rounds in the playground scene at 3840x2160 (the froxel grid is 4x the 1080p one, so
67
- this magnifies any per-froxel cost by 4x):
68
-
69
- | sampling | frame (ms) | vs. off |
70
- |---|---|---|
71
- | off | 10.29 | — |
72
- | one atlas tap (what ships) | 10.42 | +0.13 (+1.2%) |
73
- | deferred kernels (8 cone taps / 9-gather PCF) | 10.71 | +0.42 (+4.0%) |
74
-
75
- Scaled back to 1080p that is +0.03 ms and +0.10 ms, both far below the run-to-run spread. Which
76
- means the thing P-02 exists to make affordable was already affordable — do not reach for a
77
- pre-filtered ESM/VSM target on Q-01's behalf. It also means the "skip the shadow test for lights
78
- whose contribution is negligible" idea has nothing left to buy and would only add a popping risk.
79
-
80
- **The cheap tap is not a quality compromise here — it is indistinguishable, and marginally
81
- quieter.** Under the measurement protocol in the playground README (auto exposure off, 45 frames of
82
- re-convergence, 32-frame average, noise floor from an identical no-op toggle):
83
-
84
- | comparison | signed mean | mean abs |
85
- |---|---|---|
86
- | noise floor (mode 1 vs itself) | -0.053 | 1.110 |
87
- | mode 1 vs off | **-0.852** | 1.826 |
88
- | mode 2 vs off | **-0.853** | 2.010 |
89
- | mode 1 vs mode 2 | +0.0004 | 1.439 |
90
-
91
- The two paths remove the same light to four decimal places — 0.0004 against a floor of 0.05 — and
92
- their per-pixel difference sits at the floor. Residual noise after the TAA (mean abs of two settled
93
- captures of the same setting) is 1.100 off, 1.343 for one tap, 1.381 for the kernels: the 8-tap cone
94
- is *noisier*, because its taps are drawn stochastically per frame. Repeated with a shadow-casting
95
- spot light added to the scene: mode 1 vs mode 2 signed mean -0.061, mean abs 1.092, against that
96
- run's floor of +0.016 / 1.124 — below the floor on both statistics.
97
-
98
- So the PCF kernel really was filtering something about to be filtered three more times, and the
99
- deferred path keeps it for surfaces, where it belongs.
100
-
101
- One caveat came out of this, recorded because it bit the spec, and since fixed — though not the way
102
- it first looked. `shadowmap_sample_point` rounds its taps stochastically from
103
- `uv * face_size - 0.5`, and an axis-aligned light-to-sample direction encodes to an octahedral uv of
104
- exactly 0 or 1, so some taps land at texel -1 or `face_size`, one texel outside the rect the record
105
- publishes. Latent in the deferred path (mode 2), not introduced here; mode 1 clamps and never had it.
106
- Not an exotic input either: the xy axes are the four edge midpoints of the square and the -Z pole is
107
- its corner, so a lamp above the floor beneath it overshoots on roughly half its taps.
108
-
109
- **That overshoot is provided for, and the first read of this was wrong.** `shadow_point` publishes
110
- the INNER octahedral surface — `make_octahedral_point_shadow_record` subtracts
111
- `SHADOWMAP_ATLAS_BORDER` (4) from every side of the slot — and the cube → octahedral remap fills the
112
- ring around it with `inner[texture_octahedral_wrap_texel_coordinates(local)]`, folding each border
113
- fragment through the wrap before decoding a direction from it. One texel of overshoot therefore
114
- reads this light's own wrapped data, four texels inside its slot. Nothing was ever reading a
115
- neighbouring light, and the `face_size` overshoot was never a defect at all.
116
-
117
- What *was* wrong is narrower: the sampler reached the skirt through
118
- `vec2<u32>(rounded_local_texel) + vec2<u32>(atlas_aabb.xy)`, and `vec2<u32>` of `-1.0` is a
119
- negative-to-`u32` conversion WGSL leaves indeterminate. Land on `0xFFFFFFFF` and the wrapping add
120
- gets to `inner - 1`, which is right; land on `0`, equally legal, and the tap silently reads the
121
- opposite edge of the inner surface. Fixed by doing the add in `i32` — exact for an integral `f32` in
122
- `[-1, size]`, and the inner origin is at least `SHADOWMAP_ATLAS_BORDER`, so the `u32` at the end is
123
- defined.
124
-
125
- `shadow/map/shader/shadowmap_sample_point_seam.spec.js` builds a synthetic atlas with the skirt the
126
- remap would have written, and drives the shader under *both* legal readings of that conversion,
127
- requiring the same answer. That is the assertion that failed before; a checkerboard inner surface
128
- makes the two readings disagree on every affected tap, because the skirt texel at local `(x, -1)`
129
- holds `inner[size - 1 - x, 0]`, of opposite parity to `inner[x, 0]`.
130
-
131
- Worth knowing if the border is ever revisited: `chunk_shadow2d_pcf_multiple_tap_pcf_contact_hardened`,
132
- the DPCF disk kernel `SHADOWMAP_ATLAS_BORDER`'s doc names as its reason to exist, is imported by no
133
- shader — `chunk_shadowmap_sample_point2` replaced it with cone sampling. So the skirt's only current
134
- reader is that one-texel overshoot, and 8 texels per slot (half of a 16-slot) are otherwise idle.
135
- Cutting the border is therefore possible but not free: the overshoot needs at least one texel of it.
136
-
137
- ## Amortizing the sun's optical depth into a light-aligned volume — measured, 2026-08-29
138
-
139
- > **Superseded in two places by the last section of this file.** What kept this switched off is not
140
- > the frustum-clipped medium; the field-of-view measurement that argued it was is confounded, because
141
- > widening the lens also coarsens the box. And the structure is cascaded now, not a single volume.
142
-
143
- Q-04. `integrate_optical_depth` marches 16 steps from every froxel toward every light. For two
144
- froxels on the same light ray the answers differ by one increment, and the pass recomputes the whole
145
- integral for each: a prefix sum evaluated as N independent sums.
146
- `shader_volumetrics_build_sun_shadow_volume` computes it once instead, into a voxel grid whose third
147
- axis is the sun, and `chunk_volumetrics_sun_optical_depth` reads it back in a single fetch.
148
- `__VOL_SUN_SHADOW` switches between them. **Off by default**, for the one reason recorded at the end.
149
-
150
- ### What the literature actually does, including a correction
151
-
152
- The report this came from credited both RDR2 and Enshrouded with amortizing the per-froxel march
153
- into a volumetric shadow representation. Read against the released material, one of those is right
154
- and the other is not.
155
-
156
- **Enshrouded (GPC 2024) is doing exactly this, cascaded.** From the talk: "for rendering shadows …
157
- we are using 3D volumes in a cascaded fashion and align their z-axis to the direction of the primary
158
- light source, so in a first pass we would just initialize them with participating media properties
159
- and in a second pass integrate from the light source down … and that gives us information about how
160
- much shadowing there is from the fog for any position inside those cascades." And on the shipping
161
- version: "we're initializing that one in one 3D dispatch and then do a 2D dispatch for each cascade
162
- in the direction of the light … each cascade as an initial shadow value reads the upper cascade, so
163
- in the end we can just sample the one cascade and with one sample we get the full shadow volume, and
164
- the outer cascade is initialized with approximate volumetric shadows for clouds." Same structure,
165
- same build shape — a 2D dispatch walking columns along the light — with cascades on top and the
166
- outermost seeded from a cloud approximation. Their fog lives in a world-space voxel grid at 1 m per
167
- voxel, which is why they do not have the caveat below.
168
-
169
- **RDR2 (SIGGRAPH 2019) is not.** It does have a *Shadow Volume* — R16F, one of three volumes in a
170
- 160x88x64 frustum grid, temporally filtered — but the slide and its speaker notes say what it holds:
171
- "the directional light shadow term sampled from the shadow map cascade as well as cloud and terrain
172
- shadow map". That is the **shadow map** lookup amortized, not the medium's own optical depth. The
173
- medium's self-shadowing there is a short secondary sample on cloud material ("we also sample
174
- extinction towards the sun or moon to generate high frequency shadowing") plus the cloud shadow map,
175
- which is a light-space ESM built by ray-marching the cloudscape from the light — a genuine
176
- light-space transmittance structure, but for clouds only. The talk's own Future Outlook slide lists
177
- **"Volumetric shadows"** as work not done. The review document is corrected accordingly.
178
-
179
- Costs from that talk, for calibration, on PS4: cloud ESM 0.156 ms, shadow volume 0.204 ms, material
180
- volume 0.33 ms, scattering volume 0.34 ms, far-field raymarch 1.31 ms.
181
-
182
- **Fourier Opacity Mapping is the famous answer and the wrong one here.** FOM (Jansen & Bavoil, I3D
183
- 2010) projects log-transmittance along each light-space texel onto a truncated Fourier basis. It
184
- exists because the medium usually arrives as *unordered rasterized particles*, where the only thing
185
- available is a fixed-size, order-independent, additively blendable summary — and it pays for that
186
- with ringing at sharp density transitions, which is why Adaptive Volumetric Shadow Maps (Salvi et
187
- al., EGSR 2010) answer the same problem with a variable-error compressed curve instead. This engine's
188
- medium is already a 3D texture with a defined order along any axis, so the scan is available, exact
189
- for the piecewise-constant field a voxel grid *is*, and has nothing to ring. In the shadow-map
190
- taxonomy what is implemented here is an Opacity Shadow Map (Kim & Neumann 2001) with enough slices
191
- and hardware trilinear reconstruction that the slice artefacts it is remembered for do not appear.
192
-
193
- ### The structure, and why it does not swim
194
-
195
- A cube in sun space, sized from the camera frustum's **bounding sphere** and snapped to whole voxels.
196
- Both halves matter and neither is decoration:
197
-
198
- - The sphere's radius is a function of `fov`, `aspect`, `near` and `far` only, so the extent does not
199
- change when the camera turns. A tight fit of the frustum corners in sun space — which is what this
200
- engine's CSM deliberately does, for reasons its own comment gives — would breathe under rotation,
201
- moving every voxel centre continuously. The CSM can afford that because it is two orders of
202
- magnitude denser and its consumer is a filtered depth comparison; this is neither.
203
- - With a fixed extent, snapping the origin makes the world-space voxel lattice *the same set of
204
- points* every frame. Only the indexing changes. `build_volumetrics_sun_shadow_transform.spec.js`
205
- asserts that directly: voxel centres generated for one camera pose land on voxel centres of the
206
- grid generated for another, under translation, rotation and both together.
207
-
208
- Confirmed on the device, with the volumetric TAA off so nothing could hide a discontinuity. Creeping
209
- the camera across three voxels of translation, and separately across 20 degrees of yaw, the volume
210
- path's mean absolute second difference of frame luminance is *lower* than the march's — 0.0140 vs
211
- 0.0165 translating, 0.0051 vs 0.0253 rotating. It does not snap.
212
-
213
- Six scripted motions (yaw, pitch, forward, backward, strafe, combined), 90 frames each, at two
214
- viewpoints, comparing the volume against the march on flicker while moving, on flicker and drift
215
- while settling, and on the converged image after the motion against the converged image from a
216
- standing start at the same pose. Every pair agrees to within a few percent; the largest gap in
217
- "flown vs still" is 0.06 mean luminance against a floor of 0.05. The structure adds no temporal
218
- instability of its own. `playground/volumetrics_froxel/measure.js` is that harness.
219
-
220
- ### Cost, from the profiler rather than from frame time
221
-
222
- Per pass, `WORKLOAD` capture decoded in the page, 50 frames, two runs each:
223
-
224
- | pass | 1920x1080, march -> volume | 3840x2160, march -> volume |
225
- |---|---|---|
226
- | build sun shadow volume | 0.003 -> **0.048** | 0.003 -> **0.053** |
227
- | build lighting | 0.675 -> **0.499** | 1.857 -> **1.274** |
228
- | net | **-0.13 ms** | **-0.53 ms** |
229
-
230
- The whole-frame delta at 2160p is -0.55 ms with disjoint spreads over seven interleaved rounds,
231
- matching the per-pass sum. At 1080p whole-frame timing could not resolve it at all: the run-to-run
232
- spread is about +-0.5 ms and three interleaved runs read +0.11, +0.31 and +0.37 — the *wrong sign*.
233
- The README's advice to attribute cost to passes rather than infer it from frame time earned itself
234
- here.
235
-
236
- The shape is the point. The build is fixed at ~0.05 ms, and the same at 64^3 as at 128^3 — it is not
237
- where the time goes — while what it removes scales with the froxel count. Double the resolution and
238
- the saving doubles while the cost does not move.
239
-
240
- ### Why it is off by default
241
-
242
- At seven of the nine playground viewpoints the two paths agree to within about four times the noise
243
- floor (|signed mean| <= 0.20 against a floor of 0.01-0.09). "Under the island" reads -0.73. **"Inside
244
- the fog" reads +1.92**, and carries a single **+3.8 pop** around 8 degrees into a yaw, which the
245
- 0.95-blend TAA passes straight through rather than absorbing.
246
-
247
- That is not resolution, and the measurement that settles it is the field of view. At that same
248
- viewpoint, widening to 110 degrees brings the disagreement to -0.05 against a floor of -0.06 — gone —
249
- while *raising* the volume's resolution does not converge at all (32^3 through 192^3 oscillates
250
- between +7.0 and -1.9). The pop moves when `__VOL_FROXEL_FAR` moves and does not move with the box's
251
- snapping: the transform is bit-identical either side of it.
252
-
253
- > **Read the next section before trusting the paragraph below.** It calls the march "the converged
254
- > one", on the strength of `STEP_COUNT = 256` moving it by only -0.24 at *that* viewpoint. That is
255
- > true there and false in general — sixteen steps step clean over a thin fog slab, and there are
256
- > viewpoints in this same scene where the march is 58 luminance from its own converged answer while
257
- > the volume is 29 from it. Which of the two is nearer the truth is a property of the geometry, not
258
- > of the method.
259
-
260
- All of which points at the medium rather than the structure. `tParticipatingMedia1` is hard clipped
261
- to the camera frustum (Q-05), so the field being scanned has an artificial discontinuity in it. The
262
- march resolves that edge at its own step size, which is anchored at the froxel and shrinks to nothing
263
- exactly where the exit is near; a world-space lattice resolves it at a fixed 2.3 units. Where the sun
264
- ray leaves the frustum within a voxel or two of the froxel — inside dense fog, looking toward a low
265
- sun — that edge *is* the integral, and the two disagree. Widening the FOV pushes it away and they
266
- agree again. For a reference: the march at `STEP_COUNT = 256` differs from the shipping 16 by only
267
- -0.24 mean luminance, so the march is the converged one and the volume is the one in error there.
268
-
269
- So the fix is not more voxels and not a smarter fit. It is to stop sampling a frustum-shaped medium —
270
- build the volume from the scene's participating-media volumes directly, which removes the
271
- discontinuity, closes Q-05, and is the arrangement Enshrouded has. That is one shader's worth of work
272
- now that the structure and the measurements exist, and flipping the default is a line.
273
-
274
- Cascades are the other half of what Enshrouded ships, and this is shaped to take them: an inner
275
- cascade's scan starts from the outer cascade's value at its entry face instead of from zero. Worth it
276
- when the near field needs resolution the uniform grid cannot give — which, on this evidence, is not
277
- the binding constraint yet.
278
-
279
- ## The sun volume strobed, and the march was not the reference — measured, 2026-08-29
280
-
281
- > **Read the last section of this file for what closed this.** The strobe diagnosis here is right
282
- > and the fix for it stands. The paragraph at the end proposing temporal accumulation is answered
283
- > there, and the answer is no; so is the claim that the box is thin in two axes.
284
-
285
- Reported from the playground: at a camera parked beside one of the small fog boxes, with the sun
286
- ahead, `__VOL_SUN_SHADOW = true` made the light pulse. Two poses, both reproducible, both with the
287
- camera *stationary* — which rules out everything the section above establishes about the box moving,
288
- since a still camera means a bit-identical transform every frame.
289
-
290
- Frame-to-frame mean luminance, 24 frames, camera static:
291
-
292
- | | march (16 steps) | volume, 1 sample per voxel |
293
- |---|---|---|
294
- | mean | 120.19 | 91.59 |
295
- | mean absolute frame-to-frame step | 0.0023 | 0.131 |
296
- | largest single-frame step | 0.004 | **0.623** |
297
-
298
- At the second pose the largest step was **1.81**. That is a visible pulse, and the temporal filter
299
- passes it through rather than absorbing it.
300
-
301
- ### The march was not the reference, and the earlier note leant on it as if it were
302
-
303
- Raising `STEP_COUNT` to 512 to get a converged answer at these poses:
304
-
305
- | | pose 1 | pose 2 |
306
- |---|---|---|
307
- | march, 16 steps (shipping) | 120.18 | 149.47 |
308
- | **march, 512 steps** | **62.19** | **79.03** |
309
- | volume, 128^3, 1 sample per voxel | 91.59 | 119.23 |
310
- | volume, 256^3 | 63.20 | — |
311
-
312
- So at these poses the shipping march is 58 luminance from its own converged answer and the volume is
313
- 29 from it: the amortized path is the *more* accurate of the two, and the resolution sweep that
314
- looked like divergence — 64^3 giving 120.3, 128^3 giving 91.8, 256^3 giving 63.2 — was convergence
315
- all along, toward a reference the earlier work never computed at this geometry. It read as
316
- divergence only because the shipping march was being used as ground truth, and here it is nowhere
317
- near it.
318
-
319
- Worth saying plainly, because it inverts the last paragraph of the section above: **the march at 16
320
- steps is converged when the medium is thick and spread out, and badly wrong when it is thin.** Its
321
- step is the distance to the frustum exit over sixteen, which at these poses is about 8 units, and
322
- the fog box it is meant to find is 3 units thick. It steps over it. A stable wrong answer does not
323
- strobe, which is the only reason the march looks better on the flicker column.
324
-
325
- ### Why it strobed, and the fix
326
-
327
- The fog box here is 10x3x20 and the camera sits about a third of a unit outside its face. The sun
328
- travels roughly along -X, so what the ray toward the sun has to cross is the 3-unit dimension — and
329
- the 128^3 volume's voxel is 2.28 units. Walking the column that passes through the camera and asking
330
- which lattice points fall inside any scene volume: **two of them.** The entire shadow rested on two
331
- samples, and the froxel grid's per-frame column jitter moves the medium under both.
332
-
333
- The fix is to stop conflating the sampling rate with the storage resolution. A voxel has to hold the
334
- medium's *integral across its slab*, and one point sample at its centre is not that. The build pass
335
- now takes four sub-steps per voxel along the sun axis and stores the running integral at the voxel
336
- centre as before — 0.57-unit sample spacing behind a 2.28-unit voxel, one loop rather than 64x the
337
- memory:
338
-
339
- | | 1 sample/voxel | 4 sub-steps/voxel |
340
- |---|---|---|
341
- | pose 1, largest frame step | 0.623 | **0.083** |
342
- | pose 2, largest frame step | 1.81 | **0.151** |
343
- | pose 1, mean abs step | 0.131 | 0.020 |
344
- | pose 2, mean abs step | 0.468 | 0.037 |
345
-
346
- Six to thirteen times quieter, and it improves the static disagreement at *Inside the fog* as well,
347
- +1.92 -> +1.30. The cost, per pass at `WORKLOAD`:
348
-
349
- | | 1920x1080 | 3840x2160 |
350
- |---|---|---|
351
- | build sun shadow volume | 0.008 -> 0.115 | 0.003 -> 0.133 |
352
- | build lighting | 0.427 -> 0.318 | 1.772 -> 1.068 |
353
- | net | **~0.00 ms** | **-0.57 ms** |
354
-
355
- So the strobe fix spends the 1080p win — it is now a wash there — and keeps the 2160p one. The build
356
- grew 2.7x rather than 4x, the rest being fixed overhead.
357
-
358
- ### The switch is gone, and what that costs
359
-
360
- Q-04 is approved, so the cascades are the sun's only path and the A/B that measured them has been
361
- removed: the `__VOL_SUN_SHADOW` global, the playground checkbox over it, and the `sun_shadow_mode`
362
- word in the metadata, along with the branch in `shader_volumetrics_build_lighting` that read it.
363
- Same treatment Q-01 got, and it has the same consequence — **nothing in the tree reproduces the
364
- tables above any more.** Restoring the comparison means re-adding that word and its branch.
365
-
366
- Restoring the rest of it means more than that, and it is worth writing down while it is fresh:
367
-
368
- * the converged reference needs `STEP_COUNT` in `chunk_integrate_optical_depth` raised to 512;
369
- * the single-box rows need `VOLUMETRICS_SUN_SHADOW_CASCADE_RATIO` set to 1, which makes the three
370
- cascades coincide and the structure exactly one box, built by the same code;
371
- * the resolution ladder needs `VOLUMETRICS_SUN_SHADOW_RESOLUTION` edited. There was a
372
- `__VOL_SUN_SHADOW_RESOLUTION` override for it; it went with the rest, because a global that only
373
- exists to A/B a constant is the same thing as the switch and it was inconsistent to keep one
374
- without the other — the ratio never had one.
375
-
376
- What replaces the mode word is one field rather than two. `sun_shadow_light_index` already said
377
- which directional light the cascades were built for, so that a second sun degrades to the march
378
- instead of being shadowed by the first one's boxes; it now also carries
379
- `VOLUMETRICS_SUN_SHADOW_LIGHT_NONE` for the two cases that can place no box at all — a scene with no
380
- directional light, and a camera with no field of view to derive a bounding sphere from. The sentinel
381
- matches no index, so those degrade every directional light the same way. "Not built this frame" is a
382
- property of the light, not a mode to select, and saying it that way removes the failure mode a mode
383
- word has: left at zero it selects the march, silently, everywhere.
384
-
385
- The build passes still run in those cases, collapsed to one voxel per cascade. Not for measurement
386
- symmetry any more — that argument went with the A/B — but because the lighting pass binds their
387
- outputs unconditionally and WebGPU has no optional bindings.
388
-
389
- ### What is left, and it is not more sub-steps
390
-
391
- Sub-stepping fixed the *temporal* aliasing and left most of the *magnitude* gap: pose 1 went 91.59 ->
392
- 92.97 against a reference of 62.19. That is because sub-stepping refines one axis and the fog box is
393
- thin in two of them — perpendicular to the sun its cross-section is the 3x20 face, so the 3-unit
394
- dimension is under-sampled laterally as well, and only 256^3, which refines all three, converges.
395
- Lateral sub-sampling would blur the shadow rather than sharpen it, so that is not the answer either.
396
-
397
- The answer is *not* what the first draft of this section said, which was to build the volume from
398
- the scene's media volumes and integrate each box analytically, "since the chord a sun ray cuts
399
- through a box has a closed form". That assumes the medium inside a box is uniform, and it is not.
400
-
401
- `compute_volume_fade_factor` ramps the density near every face:
402
- `smoothstep(0, fade_distance, d.x) * smoothstep(0, fade_distance, d.y) * smoothstep(0, fade_distance, d.z)`
403
- over the world-space distance to each face. So a box's interior is a separable cubic-Hermite ramp,
404
- and `chord_length * sigma` is wrong by whatever the fade profile does along that chord. The volumes
405
- overlap too, though that half is harmless on its own — optical depth is additive, so
406
- `tau = integral of sum(sigma_i) = sum of integral of sigma_i`, and each volume could in principle be
407
- integrated separately. It is the non-uniformity inside each one that kills it.
408
-
409
- And there is a worse problem underneath, which is that **the participating-media pass does not
410
- discretise a field — it evaluates an estimator**. `compute_volume_fade_factor` takes the *froxel's
411
- frustum*, intersects it with the box, and samples the fade at the midpoint of that intersection
412
- segment; `frustum_unit_box_coverage` multisamples the froxel/box overlap to get a coverage fraction.
413
- Both answers depend on the shape of the froxel, not only on where it is. There is therefore no
414
- `sigma(x)` that the PM grid is sampling and that something else could integrate exactly: an analytic
415
- sun-volume build would be computing a different quantity from the one the froxel grid holds, and the
416
- two would disagree by construction, everywhere, for a reason no measurement would attribute
417
- correctly.
418
-
419
- `ParticipatingMediaVolume` also carries `noise_scale`, `noise_amplitude` and `noise_evolution_speed`.
420
- They are not in `PARTICIPATING_MEDIA_VOLUME_STRUCT`, so nothing reaches the GPU and no noise is
421
- applied today — but the data model plainly intends it, and an analytic scheme would die the day it is
422
- wired.
423
-
424
- So: sample the medium, do not try to solve it. What makes that affordable is the one property this
425
- structure already has and the froxel grid does not — **the world-space voxel lattice does not move**,
426
- which the transform spec asserts and the device measurements confirm. A lattice that does not move
427
- can be accumulated in place: one *jittered* sample per voxel per frame, a ping-pong pair of volumes,
428
- and a blend. No reprojection, because there is nothing to reproject; the effective sample count grows
429
- with the frame count instead of with the per-frame cost, and it converges on the true cell average of
430
- whatever the medium turns out to be, fade profiles and noise included. The sub-steps this commit adds
431
- would then drop back toward one.
432
-
433
- It is not free of conditions, and they are the usual ones for anything temporal:
434
-
435
- * the lattice is invariant to *camera* motion, not to the sun turning (the basis changes) nor to
436
- `fov`/`near`/`far` changing (the extent changes) — both need a reset, and both are rare;
437
- * a volume that moves or animates needs the accumulation invalidated, which `SceneVolumetrics` can
438
- now signal since it gained `invalidate`;
439
- * it buys accuracy with lag, which is the trade the froxel TAA already makes one pass downstream.
440
-
441
- What none of this fixes is the *spatial* resolution of the stored shadow: a three-unit box behind a
442
- 2.3-unit voxel gives a blurry shadow no matter how well each voxel's content is estimated. That is
443
- what cascades are for, and it is a separate axis from this.
444
-
445
- ## The sun volume needed cascades, and nothing else would do — measured, 2026-08-29
446
-
447
- Q-04, third pass. The section above ends by proposing temporal accumulation, and blames the
448
- frustum-clipped medium (Q-05) for the disagreement that kept the structure switched off. Both are
449
- wrong, and the measurements that show it also show what the structure actually needed. It now ships
450
- **on by default**, as three cascades.
451
-
452
- Everything below is against a converged reference: `chunk_integrate_optical_depth` with `STEP_COUNT`
453
- raised to 512, rendered by the same code path as the shipping march so the two differ in nothing but
454
- the step count. 1280x720, auto exposure off, 45 frames of re-convergence and a 32-frame average,
455
- noise floor from a repeat of the reference inside the same run — the protocol in the playground
456
- README. Numbers are signed mean luminance against that reference.
457
-
458
- ### Where the three paths stand
459
-
460
- | viewpoint | reference | march, 16 steps | one box, 128^3 | **cascades, 3 x 96^3** |
461
- |---|---|---|---|---|
462
- | beside the fog bank, pose 1 | 62.39 | **+58.2** | +30.5 | **-1.13** |
463
- | beside the fog bank, pose 2 | 79.05 | **+70.5** | +37.6 | **-0.53** |
464
- | Inside the fog | 114.68 | +0.29 | +1.51 | +0.04 |
465
- | Fog bank, wide | 110.22 | +0.01 | 0.00 | -0.02 |
466
- | Under the island | 92.02 | +0.62 | -0.19 | -0.20 |
467
- | Dead overview | 100.88 | +0.12 | -0.16 | -0.08 |
468
-
469
- Floors 0.01 to 0.16. The amortized path is not a cheaper approximation of the march any more. It is
470
- the accurate one, by a factor of fifty at the two viewpoints where the two disagree at all, and it
471
- is also the cheaper one.
472
-
473
- ### The error was resolution, on one axis, and the axis is not the sun's
474
-
475
- Isolating axes at pose 1, one box, four sub-steps throughout so the quadrature is held fixed. The
476
- sun travels along -X at 12 degrees of elevation; the volume's basis puts `u` horizontal and `v`
477
- within 12 degrees of vertical, and the fog bank is a slab three units thick in world Y.
478
-
479
- | grid | lateral voxel (u, v) | sun voxel | signed |
480
- |---|---|---|---|
481
- | `[128,128,128]` | 2.28, 2.28 | 2.28 | +30.5 |
482
- | `[128,128,256]` — sun axis x2 | 2.28, 2.28 | 1.14 | +30.3 |
483
- | `[256,128,128]` — u x2 | 1.14, 2.28 | 2.28 | +30.9 |
484
- | **`[128,256,128]` — v x2** | 2.28, **1.14** | 2.28 | **+6.1** |
485
- | `[128,512,128]` — v x4 | 2.28, 0.57 | 2.28 | +4.8 |
486
- | `[256,256,128]` | 1.14, 1.14 | 2.28 | +3.5 |
487
-
488
- Doubling the sun axis buys nothing. Doubling the *one* lateral axis that runs across the slab's
489
- three-unit dimension removes 80% of the error. That is a Nyquist threshold and it sits where one
490
- would predict: 1.52 units (a `[192,192,*]` grid) still reads +21.9, and 1.14 units reads +6.1.
491
-
492
- So the previous section's account — "it refines one axis and the box is thin in two of them", and
493
- "only 256^3, which refines all three, converges" — is wrong on both counts. As the sun sees it the
494
- box is thin in *one* axis, and `[256,256,32]` converges at exactly the memory `[128,128,128]` was
495
- already spending.
496
-
497
- ### Prefiltering cannot fix it, and neither can temporal accumulation
498
-
499
- This is the measurement that closes the proposal the section above ends on. Replace the single
500
- lateral sample per sub-step with a 4x4 stratified average over the voxel's lateral footprint —
501
- sixteen taps, deterministic, and therefore the *converged limit* of any scheme that reaches the same
502
- cell average stochastically over time. Quadrature held at a constant 0.6 unit spacing.
503
-
504
- | | 1 lateral sample | 4x4 lateral |
505
- |---|---|---|
506
- | `[128,128,128]`, pose 1 | +30.5 | **+25.1** |
507
- | `[128,128,128]`, pose 2 | +37.5 | **+29.7** |
508
- | `[256,256,32]`, pose 1 | +0.35 | +4.09 |
509
- | `[256,256,32]`, pose 2 | +7.59 | +10.05 |
510
-
511
- Sixteen times the sampling recovers 18% of the error, and at a resolution that already resolves the
512
- medium it makes matters *worse*, because there it blurs a shadow that was resolved. That is what a
513
- prefilter is for and all it is for: it removes aliasing, not the resolution that was aliased away. A
514
- box filter preserves the integral of `tau` and lowers its peak, and the froxels that carry the image
515
- sit at the peak.
516
-
517
- **One jittered sample per voxel per frame, accumulated in place, converges on that same 4x4 column.**
518
- The world-space lattice really does not move — that half of the proposal is sound and its spec still
519
- asserts it — but what it does not move *toward* is the reference. Temporal accumulation would spend
520
- a settling window, a reset on every sun rotation and projection change, and an invalidation on every
521
- moving volume, to arrive at +25.1 instead of +30.5. It is the wrong instrument for this defect. It
522
- stays the right instrument for the residual per-frame noise, which is a different and much smaller
523
- problem — the stability table below puts that noise at 0.03 to 0.05.
524
-
525
- The literature says the same thing from the other end. Optical depth is *linear* in `sigma`, which is
526
- why every light-space structure in this family stores it: Fourier Opacity Mapping (Jansen & Bavoil,
527
- I3D 2010) projects absorption along the ray onto a Fourier basis and exponentiates on read, and its
528
- own framing is that it is for media whose spatial opacity variations are smooth. Deep Shadow Maps
529
- (Lokovic & Veach, SIGGRAPH 2000) prefilter in *transmittance* instead, because their medium is hair
530
- and the sub-texel variation is binary. And averaging the extinction of a heterogeneous medium to
531
- downsample it is known to be wrong in a specific direction — Zhao et al., "Downsampling Scattering
532
- Parameters for Rendering Anisotropic Media" (SIGGRAPH Asia 2016), put it as linear downsampling
533
- weakening the object's intrinsic shadowing structures, which is precisely the +25.1 above.
534
-
535
- ### And the frustum clip is not what kept it switched off
536
-
537
- The section above attributes the *Inside the fog* disagreement to Q-05, on the strength of widening
538
- the field of view to 110 degrees making it vanish while raising the resolution did not. That
539
- inference does not hold, because **widening the field of view changes the resolution**: the box is
540
- sized from the frustum's bounding sphere, whose radius runs 93 units at 40 degrees, 141 at 60, 245
541
- at 90 and 350 at 110. Opening the lens by a factor of 2.5 coarsens every voxel by the same factor.
542
- The disagreement went away at 110 degrees because the box happened to land on a size that suited
543
- that geometry, which the next table makes plain.
544
-
545
- ### Why no single box could ever have been right
546
-
547
- Pose 2, against a converged reference computed at each field of view. The cascade ratio is forced to
548
- 1 so the three cascades coincide and the structure is exactly a single box, built by the same code
549
- with the same sub-steps — the only variable is the shape.
550
-
551
- | field of view | reference | one box 96^3 | one box 128^3 | one box `[256,256,32]` | 3 x 64^3 | **3 x 96^3** | 3 x 128^3 |
552
- |---|---|---|---|---|---|---|---|
553
- | 40 deg | 84.96 | **+49.97** | +6.43 | +28.09 | -0.61 | **-1.35** | -1.77 |
554
- | 60 deg | 79.05 | +5.87 | **+37.68** | +2.47 | +1.01 | **-0.53** | -1.14 |
555
- | 90 deg | 76.21 | +5.13 | +1.43 | **+38.96** | +1.29 | **+0.32** | -0.41 |
556
- | 110 deg | 74.94 | **+39.46** | +2.07 | **+39.46** | +5.86 | **+1.77** | +0.13 |
557
-
558
- Floor ±0.12 throughout. **Every single-box configuration measured has a field of view at which it is
559
- 28 to 50 luminance from the truth, and which one that is moves with the resolution.** That is not
560
- tuning, it is arithmetic: the box has to contain the frustum under any rotation, so its side is
561
- proportional to `far` and to `tan(fov/2)`, while a three-unit fog slab is three units whatever the
562
- camera is doing. The ratio between what the structure can resolve and what it has to resolve is a
563
- free parameter of the *camera*, and no constant pins it.
564
-
565
- Buying the near field uniformly is not an option either. A 0.28 unit voxel over a 283 unit box is
566
- 1024 per axis: 268 MB.
567
-
568
- ### Cascades, which is what both shipped references do
569
-
570
- Three boxes, each sized from the bounding sphere of the sub-frustum out to `far / 3^(2-i)` — 13.3,
571
- 40 and 120 units at the playground's projection, so 0.34, 1.02 and 3.07 unit voxels at 96 per axis.
572
- 7.1 MB each, 21.2 MB in total, against the 16.8 MB the single 128^3 box was already spending.
573
-
574
- Every property the single box had transfers, because each cascade is placed by the same call with a
575
- different `far`: the extent is a function of the projection alone, so it cannot breathe under
576
- rotation, and the snapped origin makes each cascade's world-space voxel lattice the same set of
577
- points from frame to frame. `build_volumetrics_sun_shadow_transform.spec.js` now asserts that per
578
- cascade, along with the containment the seeding depends on.
579
-
580
- Enshrouded (GPC 2024) ships exactly this: "each cascade as an initial shadow value reads the upper
581
- cascade, so in the end we can just sample the one cascade and with one sample we get the full shadow
582
- volume". So does Frostbite (SIGGRAPH 2015) one level down the stack — its extinction volume is a
583
- three-level clip-map and its volumetric shadow maps are 32^3 3D textures storing *transmittance*,
584
- built at 0.04 ms for a spot light and 0.14 ms for a point light on PS4. Both cascade, and neither
585
- has this engine's problem of a medium that exists only inside the camera frustum.
586
-
587
- Three things about the construction are decisions rather than transcription:
588
-
589
- - **The seed is one fetch of the outer cascade at the world position where this column enters this
590
- box**, not at the box centre and not a re-march of the approach. Everything between the sun and
591
- that plane has already been integrated out there. It is what makes an inner cascade a *refinement*
592
- of the outer rather than a truncation of the medium — without it every near-field froxel would be
593
- lit as if the fog behind it did not exist, and the volume would still come out perfectly smooth,
594
- with the correct slope, and wrong.
595
- - **A texture per cascade, not one texture with the cascades stacked along w.** Stacking puts a
596
- pass's own output and its input in the same texture, which WebGPU rejects: a subresource cannot be
597
- bound as writable storage and as a sampled texture at once, and a 3D texture has no subresource
598
- finer than the whole thing to split it on.
599
- - **Selection is on view depth, with a blend band over the last 10% of each cascade's range.** The
600
- two cascades do not agree where they meet — one is more accurate, which is the reason there are
601
- two — so a hard switch is a ring at a fixed view depth, and the 0.95-blend froxel TAA smears such
602
- an edge rather than removing it. Depth also makes the branch uniform: the lighting pass dispatches
603
- one froxel slice per workgroup in z, and a slice is a surface of constant depth.
604
-
605
- The boxes are **cubic**, and that too is measured rather than assumed. `[256,256,32]` is tempting —
606
- at 60 degrees it beats `[128,128,128]` by an order of magnitude at identical memory, for the good
607
- reason that this scene's medium is eight times finer across the sun than along it. The table above
608
- is what that costs at other fields of view. Two axes, two Nyquist limits, and which one is starved
609
- is a property of the fog and the sun's elevation rather than of the method.
610
-
611
- The sub-step count is per cascade — 4, 2, 2 from the outermost inward — so that the *world-space
612
- sample spacing* is held constant across cascades rather than the count per voxel. It has to be even:
613
- the build stores its running integral at sub-step `count / 2`, which is the voxel's centre only when
614
- the halves are equal, and a count of one stores it at the near face, half a slab of missing shadow
615
- on whichever cascade drew the short straw.
616
-
617
- ### Cost
618
-
619
- Per pass, `WORKLOAD` capture decoded in the page — `measure.js` grew a `profile` for it, so this no
620
- longer needs a downloaded `.sgpt` — medians over interleaved rounds at pose 1.
621
-
622
- | 1920x1080 | build | build lighting | total |
623
- |---|---|---|---|
624
- | march, 16 steps | 0.010 | 0.521 | **0.531** |
625
- | cascades 3 x 64^3 | 0.120 | 0.282 | **0.422** |
626
- | cascades 3 x 96^3 | 0.187 | 0.190 | **0.370** |
627
-
628
- | 3840x2160 | build | build lighting | total |
629
- |---|---|---|---|
630
- | march, 16 steps | 0.011 | 1.503 | **1.515** |
631
- | cascades 3 x 64^3 | 0.131 | 0.866 | **0.997** |
632
- | cascades 3 x 96^3 | 0.188 | 0.617 | **0.805** |
633
-
634
- The single box with four sub-steps was break-even at 1080p; three cascades are 0.16 ms *ahead* of
635
- the march there and 0.71 ahead at 2160p. Not a paradox: the build is bound by the length of its
636
- serial dependency chain rather than by its sample count, so splitting one 128-slice column into
637
- three 96-slice ones with three times as many columns is a better shape for the machine even at 1.7x
638
- the samples. Measured directly on the single box, `[128,128,128]` and `[256,256,32]` carry the same
639
- 8.4M samples and cost 0.094 ms and 0.032 ms.
640
-
641
- One caveat in those tables. The lighting pass does the same work for 64^3 and 96^3 — one fetch
642
- either way — and reads 0.282 against 0.190. Every round put 64^3 immediately after the march, so
643
- read that gap as ordering within the round rather than as a property of the change.
644
-
645
- ### Temporal stability
646
-
647
- Static camera, 24 frames, largest single-frame step in mean luminance:
648
-
649
- | | pose 1 | pose 2 | Inside the fog |
650
- |---|---|---|---|
651
- | march (512 steps) | 0.012 | 0.011 | 0.004 |
652
- | cascades 3 x 96^3 | 0.051 | 0.033 | 0.003 |
653
- | cascades 3 x 64^3 | 0.023 | 0.049 | 0.004 |
654
- | *one box, one sample per voxel — the reported strobe* | *0.595* | *0.636* | — |
655
-
656
- An order of magnitude below the pulse that was reported as visible, and three to four times the
657
- march's own floor.
658
-
659
- Six scripted motions — yaw, pitch, forward, backward, strafe, and all three together — 90 frames of
660
- motion plus 40 of hold, at two viewpoints, comparing the cascades against the march on flicker while
661
- moving, on flicker and drift while settling, and on the converged image after the motion against the
662
- converged image from a standing start at the same pose. At pose 1:
663
-
664
- | motion | settling flicker, cascades / march | flown vs still, cascades / march |
665
- |---|---|---|
666
- | yaw | 0.011 / 0.008 | +0.056 / +0.004 |
667
- | pitch | 0.005 / 0.003 | -0.007 / +0.013 |
668
- | forward | 0.043 / 0.042 | +0.037 / +0.013 |
669
- | backward | 0.007 / 0.008 | -0.047 / -0.043 |
670
- | strafe | 0.022 / 0.022 | +0.024 / +0.016 |
671
- | combined | 0.012 / 0.013 | +0.009 / -0.016 |
672
-
673
- At *Inside the fog* the two agree to within 0.01 on every motion and every statistic. The forward
674
- and backward runs are the ones to look at: they sweep the cascade boundaries eight units through the
675
- fog, which is the one instability this change could have introduced, and they read identically to
676
- the march. The **+3.8 pop** the section above reports around 8 degrees into a yaw at that viewpoint
677
- is gone — the largest single-frame step across that motion is 0.233 for the cascades against 0.235
678
- for the march.
679
-
680
- ### What is left
681
-
682
- - **Q-05 still stands, and is now the largest defect here.** The medium exists only inside the camera
683
- frustum, so fog outside it casts no shadow. Both paths have that, so it appears nowhere in the
684
- tables above: every one of them compares two readings of the same clipped medium.
685
- - A systematic **-1.2 luminance** at the higher resolutions, converging rather than shrinking — 64^3
686
- reads -0.32 at pose 1, 96^3 -1.13, 112^3 -1.23. Small, and part of it is the reference's own bias
687
- (the march's first sample carries 0.9 of a step's weight), but it is not noise.
688
- - The cascade count is capped at 4 because the per-cascade scalars are packed into a `vec4f`. Past
689
- that the metadata wants a real array, whose uniform-space stride is 16 bytes per `f32`.
690
- - The build is latency-bound, not bandwidth-bound. If it ever has to be cheaper, the lever is the
691
- serial chain — fewer slices per cascade, or a cooperative scan — and not fewer samples.
692
-
693
- ## The lighting workgroup, and a premise that did not survive contact — measured, 2026-08-30
694
-
695
- S-01. The pass declared `@workgroup_size(16,16)`, dispatched one workgroup per froxel z slice. The
696
- study's argument for reshaping it: a froxel is `TILE_SIZE` = 8 pixels, so a 16x16 group covers
697
- 128x128 pixels — sixteen separate 32-pixel light clusters — and every lane of a wave can be walking
698
- a different light list, which is the worst case for the divergent light-record reads in the point
699
- and spot loops and for shadow atlas tile locality. 4x4 in xy is exactly one cluster tile.
700
-
701
- Mean over the build lighting span:
702
-
703
- | shape | invocations | clusters spanned | mean |
704
- |---|---|---|---|
705
- | 16x16x1 | 256 | 16 in xy | **247 us** |
706
- | 8x8x1 | 64 | 4 in xy | 368 us |
707
- | 4x4x4 | 64 | 1 in xy, 4 in z | 386 us |
708
-
709
- **The premise was false.** The shape spanning the most clusters is the fastest by 50%, so
710
- cross-cluster divergence is not what governs this pass's time — cutting the group's cluster
711
- footprint from sixteen tiles to one made it *slower*, and the xy alignment the whole finding rested
712
- on bought nothing. Whatever dominates here, it is group size, not light-list coherence.
713
-
714
- **Spanning z is separately wrong, and that part holds.** At equal occupancy, 4x4x4 is slower than
715
- 8x8x1. Lights are binned into 3D clusters and the largest distance between neighbouring clusters is
716
- along depth, so a group that spans z spans the most unrelated light lists — the axis the study
717
- picked to grow was the one axis to leave alone. The reshape is 2D for the same reason the pass was
718
- 2D to begin with.
719
-
720
- Shipping **8x8x1**: 64 invocations, one wave64 or two warps, flat in z. That is 121 us of measured
721
- cost against the original, paid deliberately, and the reason is **portability**.
722
-
723
- Both numbers above were taken on an RTX 4090. Its register file is unusually large, and a 256-invocation
724
- group winning there says more about that part than about the shape: the wider the group, the more
725
- registers it needs live at once, and a 4090 has the headroom to keep 256 of them resident where a
726
- smaller part would drop occupancy instead. AMD hardware in particular does better with smaller
727
- groups. So 16x16x1's 247 us is a local result, not a general one, and shipping the shape that wins
728
- on one machine would be tuning the engine to this desk.
729
-
730
- Do not read the table as an instruction to put 16x16x1 back. That would need measurements from more
731
- than one vendor, and it is a different question from the one measured here.
732
-
733
- The shape is now `VOLUMETRICS_LIGHTING_WORKGROUP_RESOLUTION`, read by both the `@workgroup_size` and
734
- the dispatch, following the sun shadow build. Dead `VOLUMETRICS_WORKGROUP_RESOLUTION` (`[8, 8, 4]`,
735
- zero importers, matching one pass's literal by coincidence) is deleted.
736
-
737
- ### Regrouping cannot change the output, and did not
738
-
739
- Every invocation computes from `global_invocation_id` alone — no shared memory, no subgroup op, no
740
- cross-invocation state — so the question reduces to which ids get dispatched. Driven through the
741
- emulator over both id sets, `tLighting` compared equal byte for byte at grids with a partial group
742
- on one axis, on all three, and on none, with no voxel left unwritten; coverage at 1080p's 240x135x64
743
- is exactly once each. That harness was throwaway — it compares against shapes that no longer exist —
744
- and what stayed is a test pinning `@workgroup_size` to the constant the dispatch divides by.
745
-
746
- Two things worth keeping from building its negative controls:
747
-
748
- - Under-dispatching fails loudly: a stale divisor drops froxels and the volume comes back holed.
749
- - **Over-dispatching is invisible.** A `group_count_z` left at the slice count under a 4-deep group
750
- still visits every voxel exactly once — `gz * 4 + lz` stays a bijection — so the volume is
751
- bit-identical while four times the groups launch to bounds-check and return. Only an assertion
752
- about the dispatch being tight catches it.
753
-
754
- ## Hoisting the local-light shadow test above the march — measured and NOT shipped, 2026-08-30
755
-
756
- F-01 and F-03, together, because separately neither is worth doing. Both local-light loops in
757
- `shader_volumetrics_build_lighting` ran march -> multiscatter -> shadow:
758
-
759
- ```
760
- let optical_depth = integrate_optical_depth(shadow_ray); // up to 16 filtered 3D fetches
761
- let ms = calculate_multiscattering(...); // 6 octaves, pow + exp per octave
762
- let shadow_term = volumetrics_point_light_shadow(...); // 1 textureLoad, returns 0 or 1
763
- inScatteringSum += shadow_term * ms * direct_light.color;
764
- ```
765
-
766
- The shadow term is binary by construction — `chunk_volumetrics_local_light_shadow` takes one nearest
767
- texel with no filter kernel and ends in a `select` between 0.0 and 1.0 — so a zero throws away
768
- everything above it. Testing it first and `continue`ing on zero skips that work (F-01). And
769
- `volumetrics_point_light_shadow(database, index, position)` re-read the record
770
- `get_point_light_info_by_index` had just unmarshalled, *before* its `CASTS_SHADOW_BIT` early-out
771
- could fire, so the reorder alone would have added a storage read for every non-shadowing light;
772
- passing the struct instead removes the duplicate and makes the flag test free (F-03).
773
-
774
- **The reorder is exact, and that was verified rather than argued.** Both orderings were driven
775
- through the WGSL emulator over thirteen fixtures — every exit of both loops, plus a case with real
776
- per-froxel extinction where the old order computed a genuine optical depth and multiscatter and then
777
- multiplied the pair by zero — and `tLighting` compared **byte for byte**, on the raw bits so that
778
- `+0`/`-0` and any NaN payload would count as a difference. It is identical everywhere. NaN is the one
779
- input for which the two are not equivalent (`0 * NaN` propagates, a skipped term does not), so the
780
- spec fails on any non-finite value in its own right instead of folding it into the comparison.
781
-
782
- **It is not faster.** Median of the `Volumetrics / build lighting` span over 18 interleaved rounds at
783
- 3840x2160, ABBA-ordered so neither arm systematically takes the warm half of a round, 48 recorded
784
- frames per capture after a 40-frame ramp:
785
-
786
- | arm | median (ms) |
787
- |---|---|
788
- | shadow test first (F-01/F-03) | 1.3573 |
789
- | march first (shipping) | 1.3471 |
790
- | **median paired difference** | **+0.0108** |
791
-
792
- and the same protocol run with the *same build on both arms*, which is what that number has to beat:
793
-
794
- | | median paired difference | median abs difference |
795
- |---|---|---|
796
- | A/B, 18 rounds | +0.0108 | 0.0998 |
797
- | **A/A floor, 10 rounds** | **+0.0092** | 0.0456 |
798
-
799
- The measured effect and the noise floor agree to 1.6 us. There is no signal, and the sign is the
800
- wrong one anyway.
801
-
802
- **Why there was nothing to win, which is the part worth keeping.** The local-light loops are not a
803
- measurable part of this pass in this scene at all. Removing all 130 shadow-casting point lights from
804
- the playground scene and re-measuring:
805
-
806
- | | median (ms) |
807
- |---|---|
808
- | 130 shadow-casting point lights | 1.2585 |
809
- | the same scene with all of them removed | 1.3343 |
810
-
811
- Deleting the entire workload the finding proposed to optimize does not make the pass faster — it
812
- reads a shade slower, which is to say the difference is noise. Whatever the 1.3 ms is spent on, it is
813
- the per-froxel work outside those loops: the STBN fetch, the two participating-media samples, the
814
- cluster lookup, and the directional loop's cascade fetch. That also retires the premise the finding
815
- rested on, the same way S-01's did: 130 shadow-casting lights in the scene does not mean 130 lights
816
- in the cluster any froxel actually reads.
817
-
818
- So this joins Q-01's "the cost is nearly nothing" as a cost claim the profiler refused. Not shipped;
819
- the tree keeps the march-first order.
820
-
821
- What would have to change before it is worth revisiting: a scene where the froxel volume genuinely
822
- overlaps many clustered lights, shown by that removal test moving the number. Reach for the removal
823
- test *first* next time — it is three captures and it would have settled this before any code was
824
- written.
825
-
826
- Two things did survive, both about the shipping code rather than the change:
827
-
828
- - `shader_volumetrics_build_lighting_local_shadow.spec.js` now covers the exits it did not: a light
829
- over an atlas region that was never rendered (`stored == 0`, must not occlude), a froxel behind a
830
- spot's apex (`clip_light.w <= 0`), that the optical-depth march still runs and still attenuates
831
- when the shadow test passes, and that no NaN or infinity reaches `tLighting` in any fixture. The
832
- apex case needs a deliberately inconsistent fixture and says so: in the shipping arrangement a
833
- froxel behind the apex is also outside the cone, so the range cull `continue`s before the shadow
834
- function is reached, and the branch has to be built rather than found.
835
-
836
- - The dev server no longer hot-reloads (`server.hmr: false`). A reload drops the WebGPU device and
837
- resets the temporal accumulation, and an edit anywhere in the module graph — including one made by
838
- someone else working in the tree at the same time, which is exactly what happened here — destroys
839
- an interleaved A/B mid-run.
840
-
841
- Measurement conditions, because one of them is unusual: RTX 4090, canvas 1280x720 at pixel ratio 3.
842
- The pass was confirmed resolution-bound first (0.4618 ms at 1280x720 rising to 1.878 ms at 3840x2160)
843
- rather than presentation-throttled. Another session was building and running GPU work on the same
844
- machine throughout, which put occasional 2x outliers in both arms; medians absorb them, and the A/A
845
- floor was taken under the same conditions so it prices them in.
846
-
847
- ## The participating-media coverage skip — counted and NOT shipped, 2026-08-30
848
-
849
- `shader_volumetrics_build_participating_media` carried a TODO: if a froxel is entirely inside a
850
- volume, coverage is 1.0 and `frustum_unit_box_coverage` need not run. The TODO proposed getting there
851
- by replacing `aabb3_intersects_frustum` with something that returns an intersection *degree*.
852
-
853
- ### The degree answers the other question
854
-
855
- `aabb3_intersects_frustum(unit_bounds, local_frustum)` classifies the **volume box** against the
856
- **froxel's planes**, and `aabb3_intersects_frustum_degree` already returns 0/1/2 for exactly that.
857
- Its `2` means the box is inside the froxel — the arrangement where the froxel is *larger* than the
858
- volume, where coverage is a small fraction. Reading it as "coverage is 1.0" would flood the far field
859
- with fog, which `volumetrics_froxel_containment.spec.js` pins as a test.
860
-
861
- What coverage of 1.0 needs is the dual: **froxel ⊆ box**. Both bodies are convex, so it holds exactly
862
- when the froxel's 8 vertices are inside the box, and `frustum_to_corners` produces the same points
863
- `frustum_sample_point` reaches at `uvw ∈ {0,1}³` — the sign flips in `plane_convex_intersection`
864
- cancel, so a plane and its negation give the same corner. The test is exact, not conservative, and
865
- the same bound gives a second exact skip: if the froxel clears `fade_distance` from every face in
866
- world units, all three of `compute_volume_fade_factor`'s smoothsteps saturate and the fade is 1.0.
867
-
868
- ### It is exact and it still loses
869
-
870
- Corners cost 8 `plane_convex_intersection` — 3 crosses, a dot and a divide each — which is what
871
- `frustum_unit_box_coverage` spends on its 4 rays. Deriving them per volume pays for precisely the
872
- work it skips, so the only arrangement that can win builds them once per froxel in world space and
873
- maps them into each volume with a matrix multiply.
874
-
875
- Hit rates over the playground's six volumes at 240x135x64 (every 4th column in X and Y, every slice
876
- in Z, froxel extents kept at the true 1/240 and 1/135):
877
-
878
- | pose | froxels touching a volume | survivors per froxel | contained | fade saturated |
879
- |---|---|---|---|---|
880
- | overview (default) | 100.0% | 1.223 | 7.0% | 3.3% |
881
- | fog bank, wide | 91.2% | 0.912 | 8.1% | 4.1% |
882
- | inside the fog | 97.6% | 1.565 | 33.2% | 27.2% |
883
-
884
- Per froxel, in three-plane intersections:
885
-
886
- | pose | paid | saved | |
887
- |---|---|---|---|
888
- | overview | 8.00 | 0.77 | **10.4x loss** |
889
- | fog bank | 8.00 | 0.67 | **12.0x loss** |
890
- | inside the fog | 8.00 | 5.00 | **1.6x loss** |
891
-
892
- Two refinements do not rescue it. Building the corners **lazily**, on the first volume that survives
893
- the plane test, saves nothing: the 400x20x400 cloud deck is a huge thin slab that defeats the
894
- conservative frustum/AABB test almost everywhere, so 91-100% of froxels touch a volume anyway.
895
- Gating on a **1-intersection necessary condition** — the froxel's own centre must be inside the box,
896
- or containment is impossible — is very tight (centre-inside is 8.7% against contained's 7.0%) and
897
- still loses: 2.7x, 2.5x and 1.3x on the three poses.
898
-
899
- So the geometry is right and the arithmetic is against it, at every pose measured, including the one
900
- where a third of froxels qualify. Not shipped. `volumetrics_froxel_containment.spec.js` keeps the
901
- proof — containment implies coverage of exactly 1.0, and the fade core implies a fade of exactly
902
- 1.0 — alongside `chunk_aabb3_contains_aabb3` and `chunk_aabb3_from_transformed_corners`, so if the
903
- grid, the volume mix or the estimator's cost moves, restoring the early-out is a few lines rather
904
- than a rediscovery.
905
-
906
- This one was settled by counting rather than by the profiler, which is unusual here. The hit rate is
907
- a property of the scene's geometry and not of the device, and the cost ratio it implies is 2.5x to
908
- 12x on the two poses that matter — far outside anything a capture would overturn. A GPU A/B is the
909
- confirmation, not the decision.
910
-
911
- ### What did ship
912
-
913
- The line above the TODO was doing real redundant work. `frustum_transform_m4(froxel_frustum,
914
- volume.transform_inverse)` derives its plane matrix as `transpose(mat4_inverse(transform))`, so
915
- handing it `transform_inverse` made it compute `transpose(mat4_inverse(transform_inverse))` — which
916
- is `transpose(volume.transform)`, a matrix already resident in the struct. Every froxel/volume pair
917
- was running the 85-line generic cofactor `mat4_inverse` to recover a matrix it had been given: at
918
- 1080p with six volumes, 12.4M of them per frame.
919
-
920
- `chunk_frustum_transform_plane_matrix` is that function with the plane matrix passed in, and
921
- `frustum_transform_m4` is now the wrapper that derives one. The shader calls the former with
922
- `transpose(volume.transform)`. Strictly less work, and marginally more accurate for dropping an
923
- inverse round-trip. It is a full 4x4, not a 3x3 normal basis: the last row carries the translation,
924
- which is what supplies each transformed plane's distance term, and
925
- `chunk_frustum_transform_plane_matrix.spec.js` asserts both that and the equality with the old route.
926
-
927
- `transpose` was missing from the WGSL emulator's language core, which is why `frustum_transform_m4`
928
- had never been exercised there; it is implemented now for the square matrix sizes, on the same
929
- footing as `determinant`.
930
-
931
- ## Volumetric TAA review — 2026-09-02
932
-
933
- `taa/shader_volumetrics_taa.js`, reviewed for correctness. Each finding is its own commit, pinned
934
- by a spec that was red against the old code except where noted. The reprojection and the resolve
935
- were lifted into pure chunks first so the emulator could execute them.
936
-
937
- | item | finding | pin |
938
- |---|---|---|
939
- | history fetch | the 3x3x3 Blackman-Harris resample weighted the ±1 neighbours 8.4% each even at a zero offset, so every frame blurred the accumulation and the 0.95 feedback compounded it to ~1.8 froxels of sigma per axis, Z included. Now trilinear through the same sampler the lighting pass uses. | none, by request; validity gate |
940
- | reprojection | went through the previous camera's jittered VP while the reconstruction used the un-jittered CPU frustum, so a static camera missed its own texel by last frame's jitter. Now the projection diagonal on view-space xy. | a jittered previous camera must round-trip a static froxel exactly |
941
- | validity | a froxel behind the previous camera has negative w, the divide mirrors NDC back into frame, and the uv check passed. Now depth > 0 as well. | camera backing up past a near froxel |
942
- | box | the variance band excluded the centre sample whenever gamma < 1, breaking the clip's precondition; both failure paths output the raw sample. Now unioned with the centre. | 26 dark neighbours, a bright centre, the motion-end gamma |
943
- | resolve | damped the HISTORY to zero on clip, Karis's rule backwards: every clipped voxel output the raw 1-spp sample. Now a fixed 0.95 feedback with the incoming weight damped toward a quarter as the clamp distance reaches one. | impulse moves the resolve by ≤ W_CURRENT·Δ; clipped history kept; wall pull ≤ wide pull |
944
- | host | camera near/far changes rebuilt the froxel curve without a history reset. Now any change to the built curve resets. | software device, host-written `history_reset` across frames |
945
-
946
- Observed, not fixed:
947
-
948
- * The last slice (63) does not round-trip to its own texel centre. Its centre depth on the curve
949
- exceeds the logical far, the reconstruction clamps it to far, and far maps back to slice 63.0
950
- rather than 63.5, so trilinear reads a 50/50 of slices 62 and 63 even when static. The lighting
951
- pass truncates that slab at far as well, so its stored content is not centred at 63.5 either.
952
- The reprojection spec excludes the last slice from the exact round trip.
953
- * Box collapse. When all 27 neighbourhood samples miss a thin feature the spatial variance is
954
- exactly zero, the box has zero width, and the clip erases everything the history accumulated;
955
- it then climbs at 5% a frame and never reaches the mean — about a third of it at 10% coverage.
956
- The damping does not touch this either way. It needs a box that knows temporal variance or a
957
- width floor, and there is no drop-in for that with high confidence.
958
-
959
- ## The fog runs only with volumes, and particle lighting left it — measured, 2026-10-09
960
-
961
- The whole chain used to run every frame, whatever the scene held. With no volume, it spent this
962
- integrating a medium that is zero everywhere (volumetrics playground with all six volumes removed,
963
- 1378x746, RTX 4090, one 180-frame `WORKLOAD` capture):
964
-
965
- | pass | us |
966
- |---|---|
967
- | build sun shadow volume (outermost) | 259 |
968
- | build sun shadow volume (seeded, two cascades) | 91 |
969
- | build lighting | 36 |
970
- | sky transmittance + multi-scatter LUTs | 31 |
971
- | build scattering lut (aerial) | 29 |
972
- | build participating media | 23 |
973
- | volumetric TAA | 19 |
974
- | GI IBL | 4 |
975
- | **total** | **~490 of 2,415** |
976
-
977
- The sky LUT row was already gone by the time this landed: `3f28049c5` reads the scene sky's two LUTs
978
- instead of rebuilding them every frame.
979
-
980
- Two things held all of it in place, and the frame graph's culling would otherwise have dropped it:
981
-
982
- - **The volumetric TAA writes the imported history textures.** That makes it a side-effect pass,
983
- which never culls, and it reads everything upstream.
984
- - **Particle lighting was a by-product of the fog's lighting pass.** The pass also wrote the froxel
985
- irradiance volume lit particles read, with every light dimmed by `exp(-optical_depth)` through the
986
- medium, so particle lighting needed the media, the sun cascades and the sky LUTs.
987
-
988
- So the irradiance has its own pass now (`shader_volumetrics_build_irradiance`): the froxel GI plus
989
- every light with its shadow, at the same jittered point the fog's lighting pass samples, and no
990
- medium. A lit particle is lit as the surfaces beside it are, which the deferred and forward paths never dimmed by the medium either, and
991
- the fog is composited over both. `GPUViewVolumetrics` records the fog only when the scene holds a
992
- volume; otherwise `resolved` is a 1x1x1 constant, `(0, 0, 0, 1)`, which is what the aerial pass
993
- integrates to through no medium, and the TAA history is marked stale for when a volume arrives. The
994
- frame graph culls the irradiance when nothing reads it.
995
-
996
- After, from the same kind of capture on the new build. Per-pass times swing by 3-5x between captures
997
- on this machine (the media build read 46 us and 232 us in two captures of the same scene), so the
998
- result is which passes run, not a frame-time delta:
999
-
1000
- - volumetrics playground, volumes removed: **no volumetrics pass at all**;
1001
- - bonfire playground (lit smoke, no volumes): 324 us of volumetrics passes before (16 us of them the
1002
- sky LUTs), **GI 4 us + irradiance 28 us** after;
1003
- - volumetrics playground with its volumes: every fog pass still runs, and GI does not (nothing there
1004
- reads the irradiance). Screenshots agree with the old build to a mean |d| of 0.73/255; the same view
1005
- with and without its fog differs by 11.8.
1006
-
1007
- One behaviour change, deliberate: lit particles inside fog are no longer dimmed by the medium on the
1008
- light's way in. Without fog, particle lighting is what it was: in the bonfire with one 3.5 m lit
1009
- particle behind the fire and a point light 0.25 m in front of it, stepped by hand so both builds see
1010
- the same frames, the new build matches the old to a max |d| of 0 per channel on the raw frame.
1011
-
1012
- **The point stays jittered.** Evaluating the lights at each froxel's centre instead
1013
- (`volumetrics_froxel_centre_ws`, where the GI stage asks) was tried and is wrong: the texels then sit
1014
- on a lattice that never moves, and that same giant particle showed it as a dark hole beside the light
1015
- with a hard edge along a froxel row (mean |d| 2.1/255 against the old build, max 190). The jitter
1016
- moves the lattice every frame so the main TAA can resolve it; `shader_volumetrics_build_irradiance`
1017
- takes its point exactly as the fog's lighting pass does, and a spec pins that a froxel straddling a
1018
- shadow edge is lit in some frames and shadowed in others.
1019
-
1020
- Observed in that same set-up, and not changed here: with the light 0.25 m from the particle, the
1021
- jittered irradiance shows as blotchy 16-pixel blocks around the light even after 420 frames of the
1022
- main TAA, identically on the old build. Nothing filters the irradiance volume in time, and a whole
1023
- froxel flickering together is coherent over more pixels than the TAA's neighbourhood clamp looks at.
1024
-
1025
- ## Noise on the volumes — built and measured, 2026-10-15
1026
-
1027
- `VOLUMETRIC_NOISE_PLAN_2026_10_15.md` is the design and the record of how it got there (Smolder,
1028
- Enshrouded 2025 and AC4 read; three revisions; Alex's answers). What landed: a general 3-D tile
1029
- atlas (`texture/atlas/GPUTileAtlas3D`), the volumetrics instance of it on `GPUSceneVolumetrics`
1030
- (`r8unorm`, 128³ content + a one-texel skirt, growing by a layer along z), slot 0 baked on the
1031
- device from three octaves of `psrdnoise3` the first time the scene updates, and a per-volume
1032
- `[0, 1]` multiplier on density — two trilinear taps at most (base, and a warp tap that displaces the
1033
- base lookup), read at a per-froxel STBN-jittered point at a third decorrelation offset `(53, 97)`,
1034
- scrolled by phases the host folds and wraps in f64. The path tracer reads the same function.
1035
-
1036
- **The tile is on the device.** Read back off the atlas through `download_region` in the froxel
1037
- playground (RTX 4090, Chrome, headless): a z slice through the middle of slot 0 spans 0–255 with a
1038
- mean of 125.5 and texels in all eight histogram bins; every skirt texel checked equals the content
1039
- texel it wraps; a row reads 127, 167, 200, 220, …, 253, …, 14 — a continuous field, not a block.
1040
-
1041
- **The picture changes by the noise and nothing else.** `measure.js`'s image `ab` from the "Inside
1042
- the fog" viewpoint at 1080p, the fog bank with `noise_strength` 0 against 1 (16-frame averages after
1043
- 45 frames of settling, the floor from a repeat of each side):
1044
-
1045
- | | mean |d| luminance |
1046
- |---|---|
1047
- | floor, off vs off / on vs on | 0.44 / 0.24 |
1048
- | signal, off vs on, twice | 17.0 / 16.7 |
1049
-
1050
- Signed mean +14.9: the fog thins where the tile is below its mean, so more of the lit scene shows.
1051
-
1052
- **What it costs: the taps are below the density pass's timestamp quantum unless the camera is in
1053
- the noisy volume.** Per-pass GPU medians (`profile`, 60 recorded frames an arm, six rounds, the
1054
- two arms interleaved and their order alternated each round, 3796×1852):
1055
-
1056
- | view | `build participating media`, uniform | with noise | frame |
1057
- |---|---|---|---|
1058
- | Dead overview, 2 noisy volumes covering few froxels | 0.2355 (6/6 identical) | 0.2355 (6/6) | 5.062 vs 5.068 |
1059
- | Inside the graveyard fog | 0.2427–0.2437 | 0.2468 (6/6) | 4.619 vs 4.624 |
1060
- | Inside the fog bank, 3 noisy volumes | 0.2642 (6/6) | 0.2703–0.2714 | 5.456 vs 5.466 |
1061
-
1062
- So +0.003 to +0.007 ms on the pass at 2160p with every froxel in front of the camera paying two
1063
- taps, spreads disjoint; the frame totals sit inside their own round-to-round spread (±0.01 ms).
1064
- Lighting, TAA and the LUT read identically in both arms. The whole-frame readings are the usual
1065
- reminder that the pass capture is the number to believe (the README's Q-04 story).
1066
-
1067
- **The uniform path is untouched by construction**, not by measurement: `noise_strength > 0` is a
1068
- per-volume uniform branch around the taps. A zero-volume scene was not re-timed.
1069
-
1070
- Not done on the device: the froxel-versus-tracer parity check for a noisy volume (both paths read
1071
- one function and `chunk_participating_media_noise.spec.js` pins it on the emulator), and the
1072
- giant-particle seam check with a noisy volume (the point is jittered on all three axes exactly as
1073
- the lighting pass's is). Driver and probes: `probe/` in session 2fe0ac92's scratchpad, a one-off.
1074
-
1075
- ## The flow map is linked only where the wind field has cascades — measured, 2026-10-16
1076
-
1077
- The noise's flow map (the GPU fluid's volumetrics, `e54461d36`) cost the froxel build 16.4 → 18.3 µs
1078
- in uniform air, a scene with no fluid, where it never runs: a reach of exactly zero takes the one
1079
- tap. The GPU fluid session's breakdown (RTX 4090, 1280×720, camera inside the fog) put it on the code
1080
- being linked, not on running it: without the flow map and the wind sample the shader read 0.1–0.2 µs
1081
- off the old one, and an exact-compare skip, dropping the two taps, or dropping the wind sample, each
1082
- on its own, still cost about 1.8 µs.
1083
-
1084
- So the field's shape is a shader switch, `WIND_FIELD_UNIFORM` (a bool; `false`, the default, is the
1085
- field as it is), decided in two places from one value: `chunk_wind_field_sample` (uniform: return
1086
- `far_velocity`, which the cascaded read returns at every point of a field with no cascades) and
1087
- `chunk_participating_media_noise` (uniform: the one tap at the scrolled lookup, the flow map's value
1088
- at a zero departure). The froxel build picks its variant from the field it binds,
1089
- `scene.wind.cascade_count === 0`, the frame it binds it; the other readers are left at the default.
1090
- `ShaderSwitchSet.with` now remembers the sets it hands out, so deriving the build's set every frame
1091
- allocates nothing (it built two arrays, a key and an assert message a call: 595 B).
1092
-
1093
- **On the device**, the three froxel builds interleaved by blocks of four frames in one capture: the
1094
- pass as built (`new`, the variant it picked), the shader before the flow map (`old`, `ec5f0c093`), and
1095
- master's (`head`, the flow map always linked). Means of ~175 frames an arm a round, µs, three rounds
1096
- each:
1097
-
1098
- | scene | order | new | old | head |
1099
- |---|---|---|---|---|
1100
- | uniform air | new, old, head | 15.89 / 15.71 / 16.28 | 15.80 / 15.65 / 16.27 | 17.32 / 17.17 / 17.80 |
1101
- | uniform air | head, old, new | 14.24 / 14.68 / 14.97 | 14.23 / 14.75 / 15.09 | 15.66 / 16.02 / 16.46 |
1102
- | a domain, a wake, a vortex | head, old, new | 20.13 / 17.05 / 17.05 | 16.76 / 13.98 / 13.87 | 20.18 / 17.17 / 16.79 |
1103
-
1104
- Uniform air is the old cost to within 0.1 µs; disturbed air (the `new` arm on the full variant)
1105
- is master's to within ±0.3 µs either way. TAA, which no arm touches, read alike in every row. The
1106
- labels in the capture confirm which variant the pass picked in each scene.
1107
-
1108
- Every touched shader compiles through Tint on the device and builds a pipeline: both variants of the
1109
- build, the water cull, the fluid query, the accumulating tracer, the Brick4 bake, the LPV trace, both
1110
- lightmap viz bakes. Linked for the default, the seven readers of the wind alone (five particle
1111
- passes, the water cull, the fluid query) are byte-identical to master's WGSL; the six readers of the
1112
- noise differ only by the scroll-and-warp lookup now being one function both alternatives call.
1113
-
1114
- Specs: on the emulator, linked for a uniform field, the multiplier, the multiplier in a wind, the
1115
- density and the wind read what the full variant reads in a field with no cascades, bit for bit (96
1116
- random volumes, three far fields; red against a scrolled alternative that drops the warp and a
1117
- uniform wind of zero). On the software device, the froxel build dispatches the uniform variant in a
1118
- scene with no fluid and the full one when the field has a cascade, judged by the module it was
1119
- handed (red against a pass that never and one that always picks it). Driver: `wfu_*` in session
1120
- f2a32857's scratchpad, a one-off, adapted from the GPU fluid session's `s3g_*`.
1121
-
1122
- ## The media build walks its workgroup's volumes, pre-culled by a bin — measured on the iGPU, 2026-10-17
1123
-
1124
- `shader_volumetrics_build_participating_media` walked every volume of the scene from every froxel.
1125
- Each volume cost a plane transform and a six-plane box test even where it was nowhere near the
1126
- froxel, and on a modest GPU that was the pass. On the box's AMD RDNA2 iGPU (2 CUs, headless
1127
- Chrome, `--use-adapter-luid`) at 1898×982, small volumes placed beyond the far plane, where no
1128
- froxel can reach them, cost the build **0.78 ms each**:
1129
-
1130
- | volumes beyond the far plane | 0 | 16 | 64 | 256 |
1131
- |---|---|---|---|---|
1132
- | build participating media (ms) | 7.78 | 20.18 | 57.53 | 207.05 |
1133
-
1134
- Lighting and TAA did not move in any arm (4.0–4.4 and 5.73–5.77 ms).
1135
-
1136
- **Now a bin runs first.** `shader_volumetrics_bin_participating_media` dispatches one 64-lane
1137
- workgroup per workgroup of the build (16×16 froxels × 1 slice). Its lanes take the volumes in
1138
- turn, test each against the group's slab with the build's own rejection
1139
- (`volumetrics_volume_in_slab`), and set the volume's bit in that group's list. The build then walks
1140
- its group's list in row order. There is a list a group, a bit a volume, in a FrameGraph transient
1141
- sized from the scene's volume count and cleared by the graph. A lane of the bin tests ⌈volumes /
1142
- 64⌉ volumes, whatever the resolution and however large a volume is.
1143
-
1144
- **The output does not change.** A volume adds to a froxel only where its coverage is above zero,
1145
- and coverage samples points inside the froxel. The group's slab holds every froxel of the group,
1146
- widened a quarter froxel in xy and, in z, by the column jitter's half slice plus a quarter. So a
1147
- volume that adds anything is in the list. The walk is in row order, so the sums add in the order
1148
- they did. `shader_volumetrics_bin_participating_media.spec.js` runs both passes under the emulator
1149
- and compares the build's bits through the bin's lists against lists holding every volume. It is red
1150
- with the z widening cut to the jitter-free quarter slice (1,536 and 24 differing words), and green
1151
- with it.
1152
-
1153
- **The walk has a budget**, `VOLUMETRICS_MEDIA_GROUP_VOLUME_BUDGET` = 32, chosen. Past it a froxel
1154
- drops its group's highest-indexed volumes. Before the bin the loop had no budget at all.
1155
-
1156
- **After**, same driver, same scene, before/after/before/after in separate processes:
1157
-
1158
- | volumes beyond the far plane | 0 | 16 | 64 | 256 | 1,024 |
1159
- |---|---|---|---|---|---|
1160
- | build participating media (ms) | 4.48 | 4.48 | 4.58 | 4.95 | 6.99 |
1161
- | bin participating media (ms) | 0.37 | 0.37 | 0.56 | 1.15 | 3.54 |
1162
- | sum, against before | 4.85 / 7.78 | 4.85 / 20.18 | 5.14 / 57.53 | 6.10 / 207.05 | 10.53 / — |
1163
-
1164
- The two runs of each side agree to 0.01 ms. The scene's own six volumes, with nothing added, go
1165
- from 7.78 to 4.85 ms. The old build was not run at 1,024; at 0.78 ms a volume it would be about
1166
- 0.8 s. At 1,024 the build's remaining growth is the list itself: 33 words a froxel to load.
1167
-
1168
- The picture, by the README's protocol (45 frames settled, 16 averaged, block means of 32×32
1169
- pixels): before against after reads a mean |d| of 0.117–0.156 with signed means within ±0.004,
1170
- inside the floor between two processes running the *same* build (0.121 and 0.151).
1171
-
1172
- **A defect this turned up, fixed below.** The build's Y flip negates as well as swapping
1173
- (`planes[2] = -planes[3]; planes[3] = -t`), so its froxel's two Y planes face *out*. The corners
1174
- `frustum_sample_point` builds are unchanged, being intersections, but `aabb3_intersects_frustum`
1175
- on those planes keeps a box only when it spans the froxel's whole row. Measured on the emulator: a
1176
- 1×1×1 box at depth 10 covers rows 8.8–11.2 of a 20-row grid and fogs rows 9 and 10 only. The fog
1177
- loses up to a row at every volume's top and bottom edge, and a volume thinner than a row is never
1178
- drawn. The bin slices its slab with the planes facing in (`volumetrics_froxel_planes`: a swap, no
1179
- negation). Inverted planes across a group's 16 rows rejected nearly every volume, and that is how
1180
- it showed. The build keeps its arithmetic, so the output above stays byte-identical. Fixing it
1181
- changes the fog.
1182
-
1183
- **Not covered:** the aerial pass's far field, `far_sample_volumes` in
1184
- `shader_volumetrics_build_scattering_lut`, still walks every volume at every march sample past
1185
- `froxel_far`. With `froxel_far` at its default, the camera's far, that is only each column's last
1186
- step, whose dithered depth can land past it: about one far-field sample a column a frame. The
1187
- playground's `froxel_far` slider below 120 hands the whole tail to the far field, many samples a
1188
- column. Measured in the same runs, at the default, the pass reads 0.40–0.47 ms with the scene's own
1189
- volumes and 2.27 ms with 256 added. In the 1,024 run it reads 9.56 ms.
1190
-
1191
- ### The split ahead of the bin: a near field and a far field — measured on the iGPU, 2026-10-17
1192
-
1193
- `shader_volumetrics_split_participating_media` runs before the bin, with one thread per volume.
1194
- It drops a volume outside any of the grid's four side planes or its near plane. Those are the bin's
1195
- planes, widened the same quarter froxel. The plane at `froxel_far` then sorts the rest:
1196
-
1197
- - any part of a volume nearer than `froxel_far` puts it in the near field;
1198
- - any part beyond it puts it in the far field;
1199
- - a volume across it goes in both.
1200
-
1201
- Each field is a list, a count and rows in no particular order, which the split appends to from a
1202
- count of zero. The bin now tests only the near field, and the aerial pass's far field walks the
1203
- far field (see "The aerial pass's far field walks the far-field list" below).
1204
-
1205
- **The near field holds every volume the bin could keep, at the default `froxel_far`.** That
1206
- default is the camera's far, where the bin's last groups end. The emulator spec runs split, bin
1207
- and build together against lists holding every volume, byte for byte. It is red with the
1208
- discriminator at half of `froxel_far`, and its own split cases are red with the far test's
1209
- negation dropped. With `froxel_far` pulled nearer, the grid's last slice still reaches the camera's
1210
- far, because its jittered slab clamps there. A volume wholly between the two now belongs to the
1211
- far field alone and no longer reaches that slice. It had been counted by both until now.
1212
-
1213
- **After**, against master (bin only), master/split/master/split in separate processes, with a third
1214
- master run. Same driver, scene and 1898×982 as the section above, figures in ms:
1215
-
1216
- | volumes beyond the far plane | 0 | 64 | 256 | 1,024 |
1217
- |---|---|---|---|---|
1218
- | split | 0.012–0.014 | 0.020–0.022 | 0.009–0.011 | 0.016–0.018 |
1219
- | bin, master → split | 0.36–0.37 → 0.35 | 0.55–0.56 → 0.35 | 1.14–1.15 → 0.35 | 3.54 → 0.35 |
1220
- | build, master → split | 4.48–4.50 → 4.17–4.18 | 4.57–4.61 → 4.29–4.31 | 4.94–4.98 → 4.69–4.71 | 7.03–7.11 → 6.87–6.95 |
1221
-
1222
- The bin no longer grows with volumes the camera cannot see. The build's code is unchanged, yet it
1223
- reads 0.3 ms less at every count. Its lists are now built from the near field only, which fits
1224
- the split dropping volumes that a group's plane-by-plane test let through. That account is not
1225
- verified.
1226
-
1227
- The picture reads the same. Master against split gives a mean |d| of 0.057–0.164, with signed means
1228
- within ±0.003. Two runs of the same build differ by 0.177 (master) and 0.284 (split). One master
1229
- run read 1.54 against every other run, its own build's included, and is left out.
1230
-
1231
- ## The media build's Y planes face in — measured on the emulator and the iGPU, 2026-10-17
1232
-
1233
- The build turned the metadata frustum's Y pair to the grid's rows by swapping and negating. That
1234
- left both of a froxel's Y planes facing out (the defect above). It now takes the pair from
1235
- `volumetrics_froxel_planes`, the same swap the bin slices its slabs with, and both planes face in.
1236
- A plane and its negation meet the other planes at the same points, so the froxel's corners do not
1237
- move. `frustum_sample_point`, the coverage estimate and the fade therefore read exactly what they
1238
- did. Only the box test changes. It now rejects a volume only where the froxel lies wholly outside
1239
- it. Before, it kept a volume only in the rows the volume spanned from top to bottom.
1240
-
1241
- **On the emulator** (`shader_volumetrics_build_participating_media.spec.js`, a 40×20×8 grid, the
1242
- bin spec's camera). Take a box 1 tall and 1 deep, across the whole view at depth 10. It projects
1243
- onto rows 8.73–11.27. With its fade off, a froxel's fog is its coverage alone: the box fogged rows 9
1244
- and 10, and now fogs 8 to 11, the rows it reaches. A box half a row tall across the middle of row 9,
1245
- with the volume's default fade, fogged nothing and now fogs row 9. Both tests were red against the
1246
- old flip (`[9, 10]` and `[]`) and are green. The bin's spec stays green against the corrected
1247
- build: through the bin's lists the build still writes, bit for bit, what it writes with every volume
1248
- listed.
1249
-
1250
- **The fade hides half of what the box test dropped.** A froxel whose centre ray misses the box
1251
- takes the fallback in `compute_volume_fade_factor`, the fade at the froxel's centre. Outside the
1252
- box that is 0. So an edge froxel gets its fog back only when its centre ray reaches into the
1253
- volume, which means only rows the volume covers past their middle. The 1×1×1 box at depth 10 with a
1254
- fade of 0.25 still fogs rows 9 and 10 only: it covers rows 8 and 11 to about a quarter of their
1255
- height, short of their middles. The fallback's comment calls this case "rare if coverage > 0", but
1256
- at a volume's edges it is every froxel whose centre ray misses. Not changed here.
1257
-
1258
- **In the playground, on the iGPU** (headless Chrome, `--use-adapter-luid`, 1898×982, so 119×62
1259
- froxels of 16 px). Both flips ran in one page behind a temporary switch, on the bin as committed at
1260
- `7f09e80b4`, before the split above; the split's spec holds the build's output byte for byte at the
1261
- default `froxel_far`. By the README's protocol at each of the 11 viewpoints: old, new, old, new, each
1262
- after a view change, a 45-frame settle and a 16-frame average. Each floor compares a flip with its own repeat. Mean luminance in 8-bit codes, per
1263
- pixel:
1264
-
1265
- | viewpoint | signed mean, old → new | floor, old / new | mean \|d\|, old → new | floor, old / new |
1266
- |---|---|---|---|---|
1267
- | Dead overview | +0.011, −0.013 | −0.000 / −0.025 | 0.162, 0.160 | 0.213 / 0.214 |
1268
- | Fog bank, wide | +0.001, +0.000 | +0.001 / +0.000 | 0.303, 0.236 | 0.320 / 0.301 |
1269
- | Inside the fog | +0.001, +0.002 | +0.002 / +0.003 | 0.138, 0.137 | 0.163 / 0.163 |
1270
- | Island broadside | −0.007, −0.004 | +0.001 / +0.004 | 0.114, 0.124 | 0.153 / 0.168 |
1271
- | Island from above | −0.008, −0.003 | −0.003 / +0.002 | 0.100, 0.093 | 0.134 / 0.124 |
1272
- | Inside the cloud deck | −0.003, −0.002 | −0.003 / −0.002 | 0.081, 0.081 | 0.093 / 0.093 |
1273
- | Volume edge | −0.007, −0.002 | −0.009 / −0.004 | 0.129, 0.141 | 0.185 / 0.199 |
1274
- | Volume edge, above | −0.003, +0.001 | +0.006 / +0.010 | 0.130, 0.133 | 0.179 / 0.176 |
1275
- | Beside the fog box | −0.003, −0.022 | +0.031 / +0.012 | 0.129, 0.131 | 0.159 / 0.158 |
1276
- | Beside the fog box, deeper | +0.023, +0.023 | −0.013 / −0.013 | 0.126, 0.127 | 0.155 / 0.153 |
1277
- | Under the island | +0.001, +0.002 | +0.011 / +0.013 | 0.129, 0.126 | 0.165 / 0.166 |
1278
-
1279
- On 32×32 block means one block, in *Fog bank, wide*, moved the same way in both pairs by more
1280
- than the largest block move between repeats of one flip: +0.09 and +0.07 against 0.06. No other
1281
- block at any viewpoint did. The one viewpoint whose signed means agree and clear their floors,
1282
- *Beside the fog box, deeper*, moves by 0.023 of a code. **The change is inside the picture's noise
1283
- at every viewpoint.** The six volumes are large on screen and fade over 0.5–7 m, so a row at their
1284
- edge carries little of their density. What the defect hid was fog in volumes that are thin or small
1285
- on screen: one under 16 px tall here never drew at all. The floors span two captures and the
1286
- signals one, so a slow drift (the noise scrolling with the wind) counts double in a floor. That is
1287
- why the signals' mean |d| sits below the floors'.
1288
-
1289
- **Cost: unchanged.** Old against new, alternating which goes first, two rounds a viewpoint: the
1290
- media build reads 3.19–5.62 ms across the viewpoints, and new/old has a median of 0.998 (range
1291
- 0.982–1.026). More froxels at a volume's edge now run the coverage estimate, but too few to show.
1292
-
1293
- Driver: `bench/job_flip.js` and `summarize_flip.mjs` in session b5223342's scratchpad, run by the
1294
- bin session's `drive.mjs`.
1295
-
1296
- ## The aerial pass's far field walks the far-field list — measured on the iGPU, 2026-10-17
1297
-
1298
- `far_sample_volumes` in `shader_volumetrics_build_scattering_lut` used to walk every volume of the
1299
- scene at each march sample past `froxel_far`. It now walks the split's far field. The list holds
1300
- every volume with some part beyond `froxel_far` and inside the grid's widened side planes. A sample
1301
- on a column's view ray past `froxel_far` lies inside both, so a volume left off the list cannot
1302
- contain it.
1303
-
1304
- The list is in no particular order, so the sums round differently from frame to frame. Alex
1305
- accepted that: the volumetric TAA and the final image's TAA absorb it.
1306
- `shader_volumetrics_build_scattering_lut.far_field.spec.js` pins that the far field adds the
1307
- volumes the list names and no others. It is red against the shader that walked every volume.
1308
-
1309
- Master (the split, the far field unread) against this, in separate processes, at 1898×982, ms:
1310
-
1311
- | aerial pass (`build scattering lut`) | 0 volumes added | 256 | 1,024 |
1312
- |---|---|---|---|
1313
- | `froxel_far` 40 m (camera far 120), master | 0.68–0.75 | 12.87–12.90 | 55.75–56.27 |
1314
- | `froxel_far` 40 m, far-field list | 0.62–0.68 | 1.60–1.63 | 5.86–5.88 |
1315
- | `froxel_far` at its default, master | 0.54 | 2.39 | 10.72 |
1316
- | `froxel_far` at its default, far-field list | 0.63 | 0.69 | 1.13 |
1317
-
1318
- The added volumes are the ones the earlier sections place beyond the far plane in random
1319
- directions. The far field keeps only those inside the view's side planes. At the default the far
1320
- field is reached only by each column's dithered last step, and the pass grew a tenth as fast to
1321
- begin with. One run each at the default: its 0.09 ms at no added volumes is inside the 0.54–0.75
1322
- the pass reads across all runs, and is not shown to be a cost. The media build reads the same in
1323
- every arm.
1324
-
1325
- The picture reads the same. At the default, master against list gives a mean |d| of 0.083 against
1326
- floors of 0.075. At 40 m the pairs give 0.15 and 0.41, against 0.55 between the two runs of the
1327
- list's own build. The first run of the batch read 1.58 against every other run, its own build's
1328
- included, as one master run did in the section above. Both times it was the first run of a batch,
1329
- so it is left out and taken as a property of the driver, not of either build.