@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.
- package/CHANGELOG.md +2602 -0
- package/CONTRIBUTING.md +7 -0
- package/MIGRATION_2_TO_3.md +136 -0
- package/SECURITY.md +4 -0
- package/editor/EditorCameraNavigation.d.ts +2 -1
- package/editor/EditorCameraNavigation.d.ts.map +1 -1
- package/editor/EditorCameraNavigation.js +51 -57
- package/editor/ecs/component/editors/ecs/Trail3DEditor.d.ts +3 -0
- package/editor/ecs/component/editors/ecs/Trail3DEditor.d.ts.map +1 -1
- package/editor/ecs/component/editors/ecs/Trail3DEditor.js +6 -0
- package/package.json +6 -2
- package/src/engine/EngineHarness.d.ts +3 -9
- package/src/engine/EngineHarness.d.ts.map +1 -1
- package/src/engine/EngineHarness.js +6 -8
- package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts +9 -6
- package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts.map +1 -1
- package/src/engine/graphics/camera/makeOrbitalCameraController.js +44 -71
- package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts +64 -0
- package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts.map +1 -0
- package/src/engine/graphics/camera/orbital/MomentumAxis.js +187 -0
- package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts +86 -0
- package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts.map +1 -0
- package/src/engine/graphics/camera/orbital/OrbitalCameraRig.js +319 -0
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts +34 -10
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +116 -30
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +1 -7
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +1 -37
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometryFlags.js +1 -2
- package/src/engine/graphics/ecs/trail3d/TRAIL3D_FADE_LINEAR.d.ts +14 -0
- package/src/engine/graphics/ecs/trail3d/TRAIL3D_FADE_LINEAR.d.ts.map +1 -0
- package/src/engine/graphics/ecs/trail3d/TRAIL3D_FADE_LINEAR.js +22 -0
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +19 -3
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.js +31 -4
- package/src/engine/graphics/ecs/trail3d/advance_trail.js +1 -1
- package/src/engine/graphics/ecs/water/Water.d.ts +8 -0
- package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
- package/src/engine/graphics/ecs/water/Water.js +8 -0
- package/src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.d.ts +5 -5
- package/src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.d.ts.map +1 -1
- package/src/engine/graphics/trail/tube/simulator/TubeXFixedPhysicsSimulator.js +6 -6
- package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
- package/src/engine/graphics3/WaterSystem.js +731 -734
- package/src/engine/graphics3/decal/DecalTileShader.d.ts +61 -0
- package/src/engine/graphics3/decal/DecalTileShader.d.ts.map +1 -0
- package/src/engine/graphics3/decal/DecalTileShader.js +138 -0
- package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts +10 -4
- package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
- package/src/engine/graphics3/decal/GPUDecalRenderer.js +149 -44
- package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts +25 -0
- package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts.map +1 -0
- package/src/engine/graphics3/decal/chunk_decal_tile_vertex.js +55 -0
- package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts +22 -0
- package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts.map +1 -0
- package/src/engine/graphics3/decal/graph_gather_decal_tiles.js +70 -0
- package/src/engine/graphics3/decal/shader_decal_composite.d.ts +7 -5
- package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
- package/src/engine/graphics3/decal/shader_decal_composite.js +48 -44
- package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts +12 -0
- package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts.map +1 -0
- package/src/engine/graphics3/decal/shader_decal_tile_copy.js +78 -0
- package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts +11 -0
- package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts.map +1 -0
- package/src/engine/graphics3/decal/shader_gather_decal_tiles.js +80 -0
- package/src/engine/graphics3/trail/trail_tube_write_geometry.d.ts +10 -0
- package/src/engine/graphics3/trail/trail_tube_write_geometry.d.ts.map +1 -1
- package/src/engine/graphics3/trail/trail_tube_write_geometry.js +13 -1
- package/src/engine/input/devices/mouse/MouseEventButtons.d.ts +13 -0
- package/src/engine/input/devices/mouse/MouseEventButtons.d.ts.map +1 -0
- package/src/engine/input/devices/mouse/MouseEventButtons.js +23 -0
- package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts +4 -0
- package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
- package/src/engine/input/ecs/controllers/KeyboardCameraController.js +9 -45
- package/src/engine/physics/cloth/ecs/cloth_geodesic.d.ts +2 -2
- package/src/engine/physics/cloth/ecs/cloth_geodesic.js +2 -2
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/shade/playground/probe_clipmap/README.md +1 -0
- package/src/shade/playground/probe_clipmap/index.html +5 -0
- package/src/shade/playground/probe_clipmap/main.d.ts.map +1 -1
- package/src/shade/playground/probe_clipmap/main.js +16 -2
- package/src/shade/playground/vgeo_scene/README.md +1 -1
- package/src/shade/playground/water_refraction/README.md +5 -6
- package/src/shade/playground/water_refraction/index.html +1 -1
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +7 -5
- package/src/shade/renderer/atmosphere/MULTISCATTER_LUT_CONFIG.d.ts +4 -6
- package/src/shade/renderer/atmosphere/MULTISCATTER_LUT_CONFIG.d.ts.map +1 -1
- package/src/shade/renderer/atmosphere/MULTISCATTER_LUT_CONFIG.js +34 -36
- package/src/shade/renderer/atmosphere/chunk_get_multiscatter_values.d.ts +1 -1
- package/src/shade/renderer/atmosphere/chunk_get_multiscatter_values.d.ts.map +1 -1
- package/src/shade/renderer/atmosphere/chunk_get_multiscatter_values.js +162 -163
- package/src/shade/renderer/atmosphere/compute_mie_absorption.d.ts +1 -1
- package/src/shade/renderer/atmosphere/compute_mie_absorption.js +1 -1
- package/src/shade/renderer/atmosphere/hillaire/ATMOSPHERE_EARTH.js +1 -1
- package/src/shade/renderer/atmosphere/hillaire/ATMOSPHERE_PARAMETERS_STRUCT.js +1 -1
- package/src/shade/renderer/atmosphere/shader_multiscatter_lut.js +164 -164
- package/src/shade/renderer/atmosphere/shader_sky_irradiance_lut.js +1 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
- package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
- package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
- package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
- package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
- package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
- package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
- package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +4 -4
- package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +4 -4
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +2 -1
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +18 -1
- package/src/shade/renderer/geometry/sdf/cards/mesh_cards_raster_pass_descriptor.js +2 -2
- package/src/shade/renderer/geometry/sdf/cards/shader_mesh_cards_histogram.d.ts +1 -1
- package/src/shade/renderer/geometry/sdf/cards/shader_mesh_cards_histogram.js +1 -1
- package/src/shade/renderer/geometry/sdf/chunk_blas_query_band_sdf.d.ts +4 -5
- package/src/shade/renderer/geometry/sdf/chunk_blas_query_band_sdf.d.ts.map +1 -1
- package/src/shade/renderer/geometry/sdf/chunk_blas_query_band_sdf.js +6 -7
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
- package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +2 -8
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +5 -5
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +10 -10
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSettings.d.ts +12 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSettings.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSettings.js +22 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +4 -4
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +50 -13
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.d.ts +6 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.js +7 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP_RESAMPLING.d.ts +13 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP_RESAMPLING.d.ts.map +1 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP_RESAMPLING.js +13 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.d.ts +17 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.js +20 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_resampled.d.ts +18 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_resampled.d.ts.map +1 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_resampled.js +42 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_sample_radiance.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_sample_radiance.js +5 -4
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.d.ts +4 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.js +13 -2
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_resample.d.ts +52 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_resample.d.ts.map +1 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_resample.js +250 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
- package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
- package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
- package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
- package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
- package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
- package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
- package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
- package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
- package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
- package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
- package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
- package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
- package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
- package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
- package/src/shade/renderer/light/environment/environment_level_1_blur_plan.d.ts +39 -0
- package/src/shade/renderer/light/environment/environment_level_1_blur_plan.d.ts.map +1 -0
- package/src/shade/renderer/light/environment/environment_level_1_blur_plan.js +119 -0
- package/src/shade/renderer/light/environment/execute_filter_environment_map.d.ts +15 -7
- package/src/shade/renderer/light/environment/execute_filter_environment_map.d.ts.map +1 -1
- package/src/shade/renderer/light/environment/execute_filter_environment_map.js +66 -83
- package/src/shade/renderer/light/environment/graph_filter_environment_map.d.ts.map +1 -1
- package/src/shade/renderer/light/environment/graph_filter_environment_map.js +15 -6
- package/src/shade/renderer/light/environment/shader_blur_environment_map.d.ts +3 -0
- package/src/shade/renderer/light/environment/shader_blur_environment_map.d.ts.map +1 -0
- package/src/shade/renderer/light/environment/shader_blur_environment_map.js +111 -0
- package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_blurred.d.ts +3 -0
- package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_blurred.d.ts.map +1 -0
- package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_blurred.js +128 -0
- package/src/shade/renderer/light/environment/shader_downsample_environment_map.d.ts.map +1 -1
- package/src/shade/renderer/light/environment/shader_downsample_environment_map.js +0 -1
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
- package/src/shade/renderer/material/GPUMaterialManager.d.ts +7 -0
- package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
- package/src/shade/renderer/material/GPUMaterialManager.js +11 -0
- package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
- package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
- package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
- package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -3
- package/src/shade/renderer/postprocess/nss/NSS.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/nss/NSS.js +1 -3
- package/src/shade/renderer/postprocess/nss/NSS_NETWORK_SCHEMA.d.ts +5 -6
- package/src/shade/renderer/postprocess/nss/NSS_NETWORK_SCHEMA.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/nss/NSS_NETWORK_SCHEMA.js +9 -10
- package/src/shade/renderer/postprocess/nss/shader_nss_extract_feedback.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/nss/shader_nss_extract_feedback.js +48 -49
- package/src/shade/renderer/postprocess/nss/shader_nss_postprocess.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/nss/shader_nss_postprocess.js +12 -16
- package/src/shade/renderer/postprocess/nss/shader_nss_preprocess.js +246 -246
- package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.d.ts +1 -2
- package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.js +34 -35
- package/src/shade/renderer/shader/chunk/atmosphere/C_MIE_SCATTERING_DECLARATION.d.ts +2 -2
- package/src/shade/renderer/shader/chunk/atmosphere/C_MIE_SCATTERING_DECLARATION.js +2 -2
- package/src/shade/renderer/shader/chunk/atmosphere/chunk_compute_extinction.js +1 -1
- package/src/shade/renderer/shader/chunk/atmosphere/hillaire/DENSITY_PROFILE_LAYER_STRUCT.d.ts +1 -1
- package/src/shade/renderer/shader/chunk/atmosphere/hillaire/DENSITY_PROFILE_LAYER_STRUCT.js +1 -1
- package/src/shade/renderer/shader/chunk/brdf/fresnel/chunk_F_Schlick.d.ts +3 -3
- package/src/shade/renderer/shader/chunk/brdf/fresnel/chunk_F_Schlick.js +3 -3
- package/src/shade/renderer/shader/chunk/color/tonemap/aces/chunk_aces_ue_matrices.d.ts +9 -10
- package/src/shade/renderer/shader/chunk/color/tonemap/aces/chunk_aces_ue_matrices.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/color/tonemap/aces/chunk_aces_ue_matrices.js +9 -10
- package/src/shade/renderer/shader/chunk/color/tonemap/aces/chunk_tonemap_ue_filmic.d.ts +4 -4
- package/src/shade/renderer/shader/chunk/color/tonemap/aces/chunk_tonemap_ue_filmic.js +5 -5
- package/src/shade/renderer/shader/chunk/math/chunk_f16_round_stochastic.d.ts +1 -2
- package/src/shade/renderer/shader/chunk/math/chunk_f16_round_stochastic.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/math/chunk_f16_round_stochastic.js +1 -2
- package/src/shade/renderer/shader/chunk/noise/chunk_stbn_sample_vec2.d.ts +14 -1
- package/src/shade/renderer/shader/chunk/noise/chunk_stbn_sample_vec2.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/noise/chunk_stbn_sample_vec2.js +19 -6
- package/src/shade/renderer/shader/chunk/noise/chunk_stbn_sample_vec3.d.ts +10 -1
- package/src/shade/renderer/shader/chunk/noise/chunk_stbn_sample_vec3.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/noise/chunk_stbn_sample_vec3.js +14 -6
- package/src/shade/renderer/shader/chunk/random/murmur/chunk_murmur3_vec3u.d.ts +1 -1
- package/src/shade/renderer/shader/chunk/random/murmur/chunk_murmur3_vec3u.js +1 -1
- package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
- package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
- package/src/shade/renderer/shadow/vsm/DEFAULT_VSM_MAX_DYNAMIC_LOD_BIAS.d.ts +2 -3
- package/src/shade/renderer/shadow/vsm/DEFAULT_VSM_MAX_DYNAMIC_LOD_BIAS.d.ts.map +1 -1
- package/src/shade/renderer/shadow/vsm/DEFAULT_VSM_MAX_DYNAMIC_LOD_BIAS.js +7 -8
- package/src/shade/renderer/shadow/vsm/DEFAULT_VSM_MAX_LOAD_FACTOR.d.ts +1 -1
- package/src/shade/renderer/shadow/vsm/DEFAULT_VSM_MAX_LOAD_FACTOR.js +12 -12
- package/src/shade/renderer/shadow/vsm/VSMPageManager.d.ts +2 -2
- package/src/shade/renderer/shadow/vsm/VSMPageManager.d.ts.map +1 -1
- package/src/shade/renderer/shadow/vsm/VSMPageManager.js +3 -4
- package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
- package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
- package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
- package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
- package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
- package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
- package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
- package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
- package/src/shade/renderer/water/shader_water.js +22 -13
- package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
- package/src/shade/renderer/water/shader_water_avboit.js +7 -7
- package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
- package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
- package/src/view/elements/progress/segmented/SegmentedResourceBarView.d.ts +0 -19
- package/src/view/elements/progress/segmented/SegmentedResourceBarView.d.ts.map +1 -1
- package/src/view/elements/progress/segmented/SegmentedResourceBarView.js +2 -52
- package/editor/REWORK_PLAN_2026_08.md +0 -692
- package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
- package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
- package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
- package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
- package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
- package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
- package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
- package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
- package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
- package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
- package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
- package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
- package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
- package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
- package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
- package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
- package/src/REVIEW_2026_08_06/adv-core.md +0 -511
- package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
- package/src/REVIEW_2026_08_06/adv-view.md +0 -446
- package/src/REVIEW_2026_08_06/api-core.md +0 -296
- package/src/REVIEW_2026_08_06/api-engine.md +0 -195
- package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
- package/src/REVIEW_2026_08_06/core-binary.md +0 -348
- package/src/REVIEW_2026_08_06/core-collection.md +0 -376
- package/src/REVIEW_2026_08_06/core-math.md +0 -221
- package/src/REVIEW_2026_08_06/core-process.md +0 -266
- package/src/REVIEW_2026_08_06/core-science.md +0 -223
- package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
- package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
- package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
- package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
- package/src/REVIEW_2026_08_06/engine-input.md +0 -197
- package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
- package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
- package/src/REVIEW_2026_08_06/generation.md +0 -176
- package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
- package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
- package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
- package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
- package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
- package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
- package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
- package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
- package/src/REVIEW_2026_08_06/view.md +0 -667
- package/src/REVIEW_2026_08_06.md +0 -610
- package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
- package/src/core/bvh2/bvh3/NOTES.md +0 -2
- package/src/core/bvh8/NOTES.md +0 -63
- package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
- package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
- package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
- package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
- package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
- package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
- package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
- package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
- package/src/core/geom/3d/gjk/NOTES.md +0 -8
- package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
- package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
- package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
- package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
- package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
- package/src/engine/REVIEW_2026_07_03.md +0 -211
- package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
- package/src/engine/control/first-person/DESIGN.md +0 -658
- package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
- package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
- package/src/engine/control/first-person/TODO.md +0 -160
- package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
- package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
- package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
- package/src/engine/ecs/ik/PLAN.md +0 -179
- package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
- package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
- package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
- package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
- package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
- package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
- package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
- package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
- package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
- package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
- package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
- package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
- package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
- package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
- package/src/engine/network/CONGESTION_CONTROL.md +0 -87
- package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
- package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
- package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
- package/src/engine/physics/cloth/PLAN.md +0 -3056
- package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
- package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
- package/src/engine/physics/fluid/solver/NOTES.md +0 -46
- package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
- package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
- package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
- package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
- package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
- package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
- package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
- package/src/format/image/avif/DECISIONS.md +0 -545
- package/src/format/image/jpeg/NOTES.md +0 -3
- package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
- package/src/generation/COORDINATES.md +0 -54
- package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
- package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
- package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
- package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
- package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
- package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
- package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
- package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
- package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
- package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
- package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
- package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
- package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
- package/src/shade/RENDERER_CONTRACT.md +0 -697
- package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
- package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
- package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
- package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
- package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
- package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
- package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
- package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
- package/src/shade/renderer/NOTES.md +0 -78
- package/src/shade/renderer/PLAN.md +0 -8
- package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
- package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
- package/src/shade/renderer/atmosphere/NOTES.md +0 -6
- package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
- package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
- package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
- package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
- package/src/shade/renderer/camera/orbital/README.md +0 -81
- package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
- package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/index.js +0 -9
- package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
- package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
- package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
- package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
- package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
- package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
- package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
- package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
- package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
- package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -483
- package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
- package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
- package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
- package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
- package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
- package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
- package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
- package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -536
- package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
- package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4225
- package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
- package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
- package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
- package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
- package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
- package/src/shade/renderer/hiz/NOTES.md +0 -3
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
- package/src/shade/renderer/light/cluster/NOTES.md +0 -11
- package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
- package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
- package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_averages.d.ts +0 -13
- package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_averages.d.ts.map +0 -1
- package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_averages.js +0 -198
- package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
- package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
- package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
- package/src/shade/renderer/particles/DESIGN.md +0 -1011
- package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -283
- package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
- package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
- package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
- package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
- package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
- package/src/shade/renderer/postprocess/nss/NOTES.md +0 -339
- package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
- package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
- package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
- package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
- package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
- package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
- package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
- package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
- package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
- package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
- package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
- package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
- package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -394
- package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -160
- package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
- package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
- package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
- package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
- package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
- package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
- package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
- package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
- package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
- package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -932
- package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
- package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
- package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
- package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
- package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
- package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
- package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
- package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
- package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
- package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
- package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -4
- package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
- package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
- package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
- package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
- package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
- package/src/shade/wgsl/NOTES.md +0 -4
- package/src/view/elements/progress/segmented/segment_frame_transform.d.ts +0 -15
- package/src/view/elements/progress/segmented/segment_frame_transform.d.ts.map +0 -1
- 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.
|