@woosh/meep-engine 3.34.0 → 3.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/package.json +1 -1
  2. package/src/engine/Engine.d.ts +2 -0
  3. package/src/engine/Engine.d.ts.map +1 -1
  4. package/src/engine/Engine.js +5 -1
  5. package/src/engine/input/devices/XRInputDevice.d.ts +63 -0
  6. package/src/engine/input/devices/XRInputDevice.d.ts.map +1 -0
  7. package/src/engine/input/devices/XRInputDevice.js +386 -0
  8. package/src/engine/input/devices/xr/XRInputAction.d.ts +37 -0
  9. package/src/engine/input/devices/xr/XRInputAction.d.ts.map +1 -0
  10. package/src/engine/input/devices/xr/XRInputAction.js +85 -0
  11. package/src/engine/input/devices/xr/XRInputButton.d.ts +28 -0
  12. package/src/engine/input/devices/xr/XRInputButton.d.ts.map +1 -0
  13. package/src/engine/input/devices/xr/XRInputButton.js +43 -0
  14. package/src/engine/input/devices/xr/XRInputCoordinates.d.ts +15 -0
  15. package/src/engine/input/devices/xr/XRInputCoordinates.d.ts.map +1 -0
  16. package/src/engine/input/devices/xr/XRInputCoordinates.js +21 -0
  17. package/src/engine/input/devices/xr/XRInputHandle.d.ts +63 -0
  18. package/src/engine/input/devices/xr/XRInputHandle.d.ts.map +1 -0
  19. package/src/engine/input/devices/xr/XRInputHandle.js +171 -0
  20. package/src/engine/input/devices/xr/XRInputPose.d.ts +23 -0
  21. package/src/engine/input/devices/xr/XRInputPose.d.ts.map +1 -0
  22. package/src/engine/input/devices/xr/XRInputPose.js +36 -0
  23. package/src/engine/input/devices/xr/XRInputTypes.d.ts +76 -0
  24. package/src/engine/input/devices/xr/XRInputTypes.d.ts.map +1 -0
  25. package/src/engine/input/devices/xr/XRInputTypes.js +31 -0
  26. package/src/engine/input/devices/xr/XRInputValueSource.d.ts +11 -0
  27. package/src/engine/input/devices/xr/XRInputValueSource.d.ts.map +1 -0
  28. package/src/engine/input/devices/xr/XRInputValueSource.js +11 -0
  29. package/src/engine/input/ecs/ism/coordinate/InputCoordinateChannel.d.ts +15 -1
  30. package/src/engine/input/ecs/ism/coordinate/InputCoordinateChannel.d.ts.map +1 -1
  31. package/src/engine/input/ecs/ism/coordinate/InputCoordinateChannel.js +9 -0
  32. package/src/engine/input/ecs/ism/coordinate/InputCoordinateDevice.d.ts +15 -4
  33. package/src/engine/input/ecs/ism/coordinate/InputCoordinateDevice.d.ts.map +1 -1
  34. package/src/engine/input/ecs/ism/coordinate/InputCoordinateDevice.js +22 -0
  35. package/src/engine/input/ecs/ism/coordinate/InputCoordinateSwitches.d.ts +17 -2
  36. package/src/engine/input/ecs/ism/coordinate/InputCoordinateSwitches.d.ts.map +1 -1
  37. package/src/engine/input/ecs/ism/device/GamepadInputDeviceAdapter.d.ts +5 -0
  38. package/src/engine/input/ecs/ism/device/GamepadInputDeviceAdapter.d.ts.map +1 -1
  39. package/src/engine/input/ecs/ism/device/InputDeviceAdapter.d.ts +2 -0
  40. package/src/engine/input/ecs/ism/device/InputDeviceAdapter.d.ts.map +1 -1
  41. package/src/engine/input/ecs/ism/device/InputDeviceAdapter.js +2 -0
  42. package/src/engine/input/ecs/ism/device/KeyboardInputDeviceAdapter.d.ts +7 -0
  43. package/src/engine/input/ecs/ism/device/KeyboardInputDeviceAdapter.d.ts.map +1 -1
  44. package/src/engine/input/ecs/ism/device/ManualInputDeviceAdapter.d.ts +5 -0
  45. package/src/engine/input/ecs/ism/device/ManualInputDeviceAdapter.d.ts.map +1 -1
  46. package/src/engine/input/ecs/ism/device/PointerInputDeviceAdapter.d.ts +5 -0
  47. package/src/engine/input/ecs/ism/device/PointerInputDeviceAdapter.d.ts.map +1 -1
  48. package/src/engine/input/ecs/ism/device/XRInputDeviceAdapter.d.ts +16 -0
  49. package/src/engine/input/ecs/ism/device/XRInputDeviceAdapter.d.ts.map +1 -0
  50. package/src/engine/input/ecs/ism/device/XRInputDeviceAdapter.js +54 -0
  51. package/src/engine/input/ecs/ism/device/XR_INPUT_DEVICE_IDS.d.ts +9 -0
  52. package/src/engine/input/ecs/ism/device/XR_INPUT_DEVICE_IDS.d.ts.map +1 -0
  53. package/src/engine/input/ecs/ism/device/XR_INPUT_DEVICE_IDS.js +12 -0
  54. package/src/engine/input/ecs/ism/trigger/InputTrigger.d.ts +11 -1
  55. package/src/engine/input/ecs/ism/trigger/InputTrigger.d.ts.map +1 -1
  56. package/src/engine/input/ecs/ism/trigger/InputTrigger.js +7 -0
  57. package/src/engine/input/ecs/ism/trigger/InputTriggerChord.d.ts +16 -2
  58. package/src/engine/input/ecs/ism/trigger/InputTriggerChord.d.ts.map +1 -1
  59. package/src/engine/input/ecs/ism/trigger/InputTriggerHold.d.ts +22 -2
  60. package/src/engine/input/ecs/ism/trigger/InputTriggerHold.d.ts.map +1 -1
  61. package/src/engine/input/ecs/ism/trigger/InputTriggerRelease.d.ts +16 -2
  62. package/src/engine/input/ecs/ism/trigger/InputTriggerRelease.d.ts.map +1 -1
  63. package/src/engine/input/ecs/ism/trigger/InputTriggerSequence.d.ts +24 -4
  64. package/src/engine/input/ecs/ism/trigger/InputTriggerSequence.d.ts.map +1 -1
  65. package/src/engine/input/ecs/ism/trigger/InputTriggerSwitch.d.ts +6 -8
  66. package/src/engine/input/ecs/ism/trigger/InputTriggerSwitch.d.ts.map +1 -1
  67. package/src/engine/input/ecs/ism/trigger/InputTriggerSwitch.js +5 -0
  68. package/src/engine/input/ecs/ism/trigger/InputTriggerXRButton.d.ts +7 -0
  69. package/src/engine/input/ecs/ism/trigger/InputTriggerXRButton.d.ts.map +1 -0
  70. package/src/engine/input/ecs/ism/trigger/InputTriggerXRButton.js +15 -0
  71. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  72. package/src/shade/device/timing/profile/GPUProfileSession.d.ts +25 -11
  73. package/src/shade/device/timing/profile/GPUProfileSession.d.ts.map +1 -1
  74. package/src/shade/device/timing/profile/GPUProfileSession.js +553 -474
  75. package/src/shade/playground/avboit/main.d.ts.map +1 -1
  76. package/src/shade/playground/avboit/main.js +888 -916
  77. package/src/shade/playground/particle_ecs/README.md +13 -7
  78. package/src/shade/playground/particle_ecs/bonfire_effects.d.ts +5 -5
  79. package/src/shade/playground/particle_ecs/bonfire_effects.js +5 -5
  80. package/src/shade/playground/particle_ecs/effects/smoke.json +1859 -1859
  81. package/src/shade/playground/particle_ecs/effects/soot.json +1605 -1605
  82. package/src/shade/playground/particle_system/README.md +1 -2
  83. package/src/shade/playground/profile_hotkey.d.ts.map +1 -1
  84. package/src/shade/playground/profile_hotkey.js +4 -40
  85. package/src/shade/playground/skinned_blas_refit/main.d.ts.map +1 -1
  86. package/src/shade/playground/skinned_blas_refit/main.js +4 -39
  87. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
  88. package/src/shade/playground/volumetrics_froxel/main.js +4 -39
  89. package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -1
  90. package/src/shade/playground/volumetrics_froxel/measure.js +2 -4
  91. package/src/shade/renderer/Renderer.d.ts +1 -1
  92. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  93. package/src/shade/renderer/Renderer.js +3 -1
  94. package/src/shade/renderer/light/GPULightCollection.d.ts +9 -1
  95. package/src/shade/renderer/light/GPULightCollection.d.ts.map +1 -1
  96. package/src/shade/renderer/light/GPULightCollection.js +41 -0
  97. package/src/shade/renderer/light/environment/shader_project_environment_sh.d.ts +37 -0
  98. package/src/shade/renderer/light/environment/shader_project_environment_sh.d.ts.map +1 -0
  99. package/src/shade/renderer/light/environment/shader_project_environment_sh.js +105 -0
  100. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  101. package/src/shade/renderer/particles/DESIGN.md +65 -8
  102. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +7 -1
  103. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  104. package/src/shade/renderer/particles/GPUParticleSystem.js +1247 -1244
  105. package/src/shade/renderer/particles/graph_particles_avboit.d.ts +7 -1
  106. package/src/shade/renderer/particles/graph_particles_avboit.d.ts.map +1 -1
  107. package/src/shade/renderer/particles/graph_particles_avboit.js +9 -0
  108. package/src/shade/renderer/particles/shaders/chunk_particle_billboard_vertex.d.ts.map +1 -1
  109. package/src/shade/renderer/particles/shaders/chunk_particle_billboard_vertex.js +4 -2
  110. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +15 -9
  111. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts.map +1 -1
  112. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +29 -11
  113. package/src/shade/renderer/particles/shaders/chunk_particle_lighting_model.d.ts +24 -0
  114. package/src/shade/renderer/particles/shaders/chunk_particle_lighting_model.d.ts.map +1 -0
  115. package/src/shade/renderer/particles/shaders/chunk_particle_lighting_model.js +45 -0
  116. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.d.ts +3 -0
  117. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.d.ts.map +1 -1
  118. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.js +38 -0
  119. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.d.ts.map +1 -1
  120. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.js +190 -185
  121. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts +8 -0
  122. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts.map +1 -1
  123. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.js +5 -0
  124. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts +8 -1
  125. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts.map +1 -1
  126. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +9 -0
  127. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts +13 -3
  128. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  129. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +9 -1
  130. package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.d.ts +39 -0
  131. package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.d.ts.map +1 -0
  132. package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.js +107 -0
  133. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.d.ts +15 -0
  134. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.d.ts.map +1 -0
  135. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.js +44 -0
  136. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.d.ts +19 -0
  137. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.d.ts.map +1 -0
  138. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.js +48 -0
  139. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.d.ts +29 -0
  140. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.d.ts.map +1 -0
  141. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.js +90 -0
  142. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_brick4.d.ts +10 -0
  143. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_brick4.d.ts.map +1 -0
  144. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_brick4.js +55 -0
  145. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_ibl.d.ts +10 -0
  146. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_ibl.d.ts.map +1 -0
  147. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_ibl.js +47 -0
  148. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_lpv.d.ts +10 -0
  149. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_lpv.d.ts.map +1 -0
  150. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_lpv.js +63 -0
  151. package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.d.ts +17 -0
  152. package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.d.ts.map +1 -0
  153. package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.js +26 -0
  154. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +8 -1
  155. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  156. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +50 -2
  157. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
  158. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +488 -436
  159. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts +1 -1
  160. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts.map +1 -1
  161. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +254 -63
@@ -1,436 +1,488 @@
1
- import { vec3f } from "../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
2
- import { WebGPUArray } from "../shader/type/WebGPUArray.js";
3
- import { CAMERA_UNIFORM_STRUCT } from "../camera/CAMERA_UNIFORM_STRUCT.js";
4
- import { chunk_rgb_to_luminance } from "../shader/chunk/color/chunk_rgb_to_luminance.js";
5
- import { chunk_stbn_sample_vec3 } from "../shader/chunk/noise/chunk_stbn_sample_vec3.js";
6
- import { chunk_camera_ray_from_uv } from "../shader/chunk/geometry/ray/chunk_camera_ray_from_uv.js";
7
- import { RAY_STRUCT } from "../shader/chunk/geometry/ray/RAY_STRUCT.js";
8
- import {
9
- chunk_directional_lights_iteration_mask
10
- } from "../shader/chunk/light/io/chunk_directional_lights_iteration_mask.js";
11
- import {
12
- chunk_get_directional_light_info_by_index
13
- } from "../shader/chunk/light/io/chunk_get_directional_light_info_by_index.js";
14
- import {
15
- chunk_get_point_light_info_by_index
16
- } from "../shader/chunk/light/io/chunk_get_point_light_info_by_index.js";
17
- import { chunk_get_spot_light_info_by_index } from "../shader/chunk/light/io/chunk_get_spot_light_info_by_index.js";
18
- import { chunk_texel_coordinate_to_uv } from "../shader/chunk/texture/chunk_texel_coordinate_to_uv.js";
19
- import { chunk_uv_to_texel_coordinate } from "../shader/chunk/texture/chunk_uv_to_texel_coordinate.js";
20
- import { CodeChunk } from "../shader/compiler/CodeChunk.js";
21
- import { ComputeShader } from "../shader/ComputeShader.js";
22
- import { ShaderResourceSetDescriptor } from "../shader/resource/ShaderResourceSetDescriptor.js";
23
- import { chunk_shadowmap_get_light_visibility } from "../shadow/map/shader/chunk_shadowmap_get_light_visibility.js";
24
- import { SHADOWMAP_COMPARE_SAMPLER_DESCRIPTOR } from "../shadow/map/SHADOWMAP_COMPARE_SAMPLER_DESCRIPTOR.js";
25
- import { SAMPLER_LINEAR_UPSCALE } from "../texture/SAMPLER_LINEAR_UPSCALE.js";
26
- import { VIEW_UNIFORM_STRUCT } from "../view/VIEW_UNIFORM_STRUCT.js";
27
- import { chunk_cluster_depth_from_z_slice } from "../light/cluster/chunk_cluster_depth_from_z_slice.js";
28
- import { chunk_cluster_depth_to_z_slice } from "../light/cluster/chunk_cluster_depth_to_z_slice.js";
29
- import { chunk_light_cluster_counts } from "../light/cluster/chunk_light_cluster_counts.js";
30
- import { chunk_light_cluster_metadata_by_position } from "../light/cluster/chunk_light_cluster_metadata_by_position.js";
31
- import { LIGHT_CLUSTER_LOOKUP_STRUCT } from "../light/cluster/shader_cluster_assign_lights.js";
32
- import { chunk_sky_lut_compute_uv } from "../atmosphere/chunk_sky_lut_compute_uv.js";
33
- import { chunk_calculate_multiscattering } from "./chunk_calculate_multiscattering.js";
34
- import { chunk_camera_ray_planar_depth_scale } from "./chunk_camera_ray_planar_depth_scale.js";
35
- import { chunk_volumetrics_froxel_content_offset } from "./chunk_volumetrics_froxel_content_offset.js";
36
- import { chunk_integrate_optical_depth } from "./chunk_integrate_optical_depth.js";
37
- import {
38
- chunk_volumetrics_sun_optical_depth,
39
- volumetrics_sun_shadow_texture_name
40
- } from "./chunk_volumetrics_sun_optical_depth.js";
41
- import { VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT } from "./VOLUMETRICS_SUN_SHADOW_RESOLUTION.js";
42
- import { chunk_volumetrics_column_jitter } from "./chunk_volumetrics_column_jitter.js";
43
- import { chunk_volumetrics_local_light_shadow } from "./chunk_volumetrics_local_light_shadow.js";
44
- import { VOLUMETRICS_METADATA_STRUCT } from "./VOLUMETRICS_METADATA_STRUCT.js";
45
-
46
- /**
47
- * Workgroup shape of this pass, `[x, y, z]` in froxels, stated to the `@workgroup_size` below.
48
- * That attribute is where the dispatch gets the shape from in turn: `graph_build_volumetrics` asks
49
- * for a thread count and `ComputeShader` divides by the size it read back out of the WGSL.
50
- *
51
- * Flat in z. Lights are binned into 3D clusters and the largest distance between neighbouring
52
- * clusters is along depth, so a group spanning z is the one spanning the most unrelated light
53
- * lists.
54
- *
55
- * 64 invocations — one wave64, two warps — is a deliberate trade: 16x16x1 measures faster on the
56
- * RTX 4090 this was benchmarked on, and is kept off because that part's register file is unusually
57
- * large and the result is not expected to generalize (AMD favours smaller groups). NOTES.md carries
58
- * the numbers and the reasoning.
59
- *
60
- * @type {number[]}
61
- */
62
- export const VOLUMETRICS_LIGHTING_WORKGROUP_RESOLUTION = [8, 8, 1];
63
-
64
- const resources = new ShaderResourceSetDescriptor();
65
-
66
- const primary_group = resources.createGroup()
67
- .addUniform("volumetrics_metadata", VOLUMETRICS_METADATA_STRUCT)
68
- .addUniform("camera", CAMERA_UNIFORM_STRUCT)
69
- .addUniform("view", VIEW_UNIFORM_STRUCT)
70
- .addTexture("tSunTransmittance", "float", "2d")
71
- .addTexture("tMultiScattering", "float", "2d")
72
- .addSampler("sLinear", "filtering", SAMPLER_LINEAR_UPSCALE)
73
- .addTexture("tSTBN_vec3", "unfilterable-float", "3d")
74
- .addTexture("tParticipatingMedia0", "float", "3d")
75
- .addTexture("tParticipatingMedia1", "float", "3d");
76
-
77
- // Q-04: the sun's optical depth, pre-integrated along the light axis by
78
- // shader_volumetrics_build_sun_shadow_volume, one cascade per texture and innermost first.
79
- // Sampled, not marched.
80
- for (let cascade = 0; cascade < VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT; cascade++) {
81
- primary_group.addTexture(volumetrics_sun_shadow_texture_name(cascade), "float", "3d");
82
- }
83
-
84
- primary_group.addStorageTexture("tLighting", "rgba16float", "3d", "write-only")
85
-
86
- // Lighting information
87
- resources.createGroup()
88
-
89
- .addStorageBuffer("light_data", WebGPUArray.u32)
90
- // clusters
91
- .addUniform("cluster_parameters", vec3f)
92
- .addStorageBuffer("cluster_lookup", LIGHT_CLUSTER_LOOKUP_STRUCT)
93
- .addStorageBuffer("cluster_data", WebGPUArray.u32)
94
- // shadows
95
- .addTexture("shadowmapAtlas", "depth")
96
- .addSampler("sCompareLinear", "comparison", SHADOWMAP_COMPARE_SAMPLER_DESCRIPTOR)
97
-
98
- const body = CodeChunk.from(
99
- //language=WGSL
100
- `
101
- @compute @workgroup_size(${VOLUMETRICS_LIGHTING_WORKGROUP_RESOLUTION.join(",")})
102
- fn main(
103
- @builtin(global_invocation_id) voxel_coord : vec3<u32>
104
- ){
105
- let resolution = textureDimensions(tParticipatingMedia0);
106
-
107
- if(any(voxel_coord >= resolution)){
108
- // overflow
109
- return;
110
- }
111
-
112
- // Logical near/far, NOT the projection-derived range: the projection is infinite-far
113
- // reverse-Z, so its far resolves to ~3.4e38 (FLOAT32_MAX). That value would leave the last
114
- // froxel slice's z1 (and z1*z1*z1 below) at +Inf. clip_near_far is the bounded logical range,
115
- // matching the participating-media build shader.
116
- let near_far = camera.clip_near_far;
117
-
118
- // same jitter valud for all voxels in Z column
119
- // must be the same for PM and Lights
120
- let z_jitter = volumetrics_column_jitter(voxel_coord.xy, view.frame_index);
121
-
122
- let z_slice_0 = f32(voxel_coord.z ) + z_jitter;
123
- let z_slice_1 = f32(voxel_coord.z + 1) + z_jitter;
124
-
125
- let z0 = max(near_far.x, cluster_depth_from_z_slice( z_slice_0, volumetrics_metadata.froxel_cluster_parameters, f32(resolution.z) ));
126
- let z1 = min(near_far.y, cluster_depth_from_z_slice( z_slice_1, volumetrics_metadata.froxel_cluster_parameters, f32(resolution.z) ));
127
-
128
- // 2. Cubic mix gives uniform distribution in World Space Volume
129
- let z3_min = z0 * z0 * z0;
130
- let z3_max = z1 * z1 * z1;
131
-
132
- var inScatteringSum = vec3<f32>(0.0);
133
- var extinctionSum = 0.0;
134
-
135
-
136
- // One sample per froxel. The 0.95-blend TAA downstream is a ~39-effective-sample temporal
137
- // integrator; supersampling here as well spends N times this pass removing variance that filter
138
- // removes anyway — and spends it at the Monte Carlo rate, because the samples were independent
139
- // white noise and converged as 1/sqrt(N). Raising this is only worth it with the temporal
140
- // filter off.
141
- const SAMPLE_COUNT = 1u;
142
-
143
- for(var i = 0u; i < SAMPLE_COUNT; i++){
144
-
145
- // Spatiotemporal blue noise rather than a per-froxel hash. What lets 1 spp survive a
146
- // temporal filter is each froxel seeing a well-distributed sequence over frames, which is
147
- // STBN's defining property and exactly what white noise lacks — measured against the murmur
148
- // hash it reaches the same error in roughly half the frames, across the whole window the
149
- // 0.95 blend spans.
150
- //
151
- // The xy offset is load-bearing, not decoration. The aerial pass reads this same volume at
152
- // (voxel_coord.xy, frame_index) and consumes all three channels, so sampling it there would
153
- // not merely correlate the two passes' jitters — it would make them the same number. Two
154
- // nuisance variables locked together stop covering the sample domain and the estimator
155
- // converges to the wrong value, a bias that does NOT shrink with more frames, so no TAA
156
- // window recovers it. Measured worst case |r| against any aerial channel: 1.0000 at zero
157
- // offset, 0.0027 here.
158
- //
159
- // froxel z rides the temporal axis so froxels down a column do not all share one value —
160
- // the fetch is indexed by column, and STBN's z axis is the one built to be well distributed.
161
- const STBN_DECORRELATION_OFFSET = vec2<u32>(37u, 71u);
162
-
163
- let noise = stbn_sample_vec3(vec3(
164
- voxel_coord.xy + STBN_DECORRELATION_OFFSET,
165
- (view.frame_index + voxel_coord.z) * SAMPLE_COUNT + i
166
- ));
167
-
168
- // [0..1] — offset (jitter - 0.5) must be centered on the froxel for every axis. The depth
169
- // axis is stratified so that raising SAMPLE_COUNT converges faster than 1/sqrt(N); a no-op
170
- // while it is 1.
171
- let jitter = vec3(noise.xy, (f32(i) + noise.z) / f32(SAMPLE_COUNT));
172
-
173
-
174
- // Interpolate the cubes, then take the cube root
175
- let z3_sample = mix(z3_min, z3_max, jitter.z);
176
- let view_depth = pow(z3_sample, 0.333333);
177
-
178
- let sample_z_index:f32 = cluster_depth_to_z_slice(
179
- view_depth,
180
- volumetrics_metadata.froxel_cluster_parameters,
181
- f32(resolution.z)
182
- );
183
-
184
- // 'saturate' prevents accidental out-of-bounds samples
185
- let tile_uv = saturate(
186
- texel_coordinate_to_uv(vec2f(voxel_coord.xy) + (jitter.xy - 0.5), resolution.xy)
187
- );
188
- let view_ray = camera_ray_from_uv(tile_uv, camera);
189
-
190
- // The PM volumes are CELL-AVERAGED: texel k holds the slab [k+j, k+1+j], so its content
191
- // sits at slice k + j + delta, not at slice k. Addressing them with the plain texel-centre
192
- // convention is what the boundary-sampled aerial LUT wants, and reads the medium about half
193
- // a froxel too far out. delta is a property of the depth curve — see the chunk.
194
- let content_offset = volumetrics_froxel_content_offset(volumetrics_metadata.froxel_cluster_parameters.z);
195
-
196
- let uvw = vec3<f32>(tile_uv, (sample_z_index + 0.5 - content_offset) / f32(resolution.z));
197
-
198
- let participating_media0 = textureSampleLevel(tParticipatingMedia0, sLinear, uvw, 0.0);
199
- let participating_media1 = textureSampleLevel(tParticipatingMedia1, sLinear, uvw, 0.0);
200
-
201
- let participating_media_scattering = participating_media0.rgb;
202
- let participating_media_extinction = participating_media1.rgb;
203
- let participating_media_phase_g = participating_media1.a;
204
-
205
- // Accumulate the (linear) mono extinction coefficient. Carried in tLighting.a below so the
206
- // 3D TAA accumulates sigma_t alongside the in-scatter (Hillaire: temporally integrate the
207
- // LINEAR extinction, never the non-linear transmittance).
208
- extinctionSum += rgb_to_luminance(participating_media_extinction);
209
-
210
- let view_pixel_coord = uv_to_texel_coordinate(tile_uv, vec2(view.width, view.height));
211
-
212
- // view_depth is a PLANAR view-space depth and view_ray.direction is unit length, so the
213
- // depth has to be converted to a distance along the ray before stepping. Multiplying them
214
- // directly stops on the arc of constant distance instead of the plane of constant depth,
215
- // short by cos(theta) — exact on the centre axis, ~72% of the intended depth in the corners
216
- // of a 50 degree vertical FOV. Radial, which is why it reads as "the fog looks a bit off"
217
- // rather than as an offset.
218
- //
219
- // Everything downstream assumes the planar reading: the light cluster is looked up at the
220
- // planar depth, the shadow lookup happens at this position, and the TAA reconstructs froxel
221
- // centres as planar slabs.
222
- let position_ws = view_ray.origin
223
- + view_ray.direction * (view_depth * camera_ray_planar_depth_scale(view_ray.direction, camera));
224
-
225
- // Global lights (directional) — iterate over the first 32 slots
226
- // of the directional light table (max enforced by GPULightCollection).
227
- var directional_bits = directional_lights_iteration_mask(&light_data);
228
- while (directional_bits != 0u) {
229
- let directional_index = countTrailingZeros(directional_bits);
230
- directional_bits &= ~(1u << directional_index);
231
-
232
- let sun = get_directional_light_info_by_index(&light_data, directional_index);
233
-
234
- let sun_lut_uv = sky_lut_compute_uv(position_ws, sun.direction, vec2<f32>(textureDimensions(tSunTransmittance)));
235
-
236
- let shadow_term = shadowmap_get_directional_light_visibility(
237
- &light_data,
238
- directional_index,
239
- position_ws,
240
- // toward-camera, matching the deferred cascade-blend convention. The consumer
241
- // (shadowmap_csm_compute_cascade_blended) negates this internally; the deferred
242
- // caller passes the surface->camera vector, whereas view_ray.direction is
243
- // camera->scene, so it must be negated here to agree.
244
- -view_ray.direction,
245
- sun.direction
246
- );
247
-
248
- let u = dot(sun.direction, view_ray.direction);
249
-
250
- // sample atmospheric transmittance
251
- let transmittance_atmospheric = textureSampleLevel(
252
- tSunTransmittance,
253
- sLinear,
254
- sun_lut_uv, 0.0
255
- ).rgb;
256
-
257
- // Q-04. Every froxel on this sun ray is asking for the same integral, one increment
258
- // apart, and the march recomputes all of it from scratch for each: O(froxels * steps)
259
- // to produce O(froxels) of information. The sun is where that is worst, because its
260
- // tmax is the distance to the sun clipped to the frustum, so the march spans the whole
261
- // grid rather than the few metres a lamp's does.
262
- //
263
- // So read the answer out of the cascades built once this frame with their axis on the
264
- // sun, where the increment relation IS the storage — see
265
- // shader_volumetrics_build_sun_shadow_volume. Same quantity, same units, one fetch.
266
- //
267
- // The march below is not an alternative to it, it is what is left for the lights the
268
- // cascades do not cover: a second directional light, since there is one ladder and it
269
- // is aligned to one direction, and the two cases that cannot place a box at all, which
270
- // sun_shadow_light_index reports by holding a value no light index can equal.
271
- var optical_depth: vec3<f32>;
272
-
273
- if (directional_index == volumetrics_metadata.sun_shadow_light_index) {
274
-
275
- optical_depth = volumetrics_sun_optical_depth(position_ws);
276
-
277
- } else {
278
-
279
- // .direction points TOWARD the light; march the optical-depth ray toward the light (+direction).
280
- let shadow_ray = ${RAY_STRUCT.make({
281
- origin: "position_ws",
282
- direction: "sun.direction",
283
- tmax: "sun.distance",
284
- })};
285
-
286
- optical_depth = integrate_optical_depth(shadow_ray);
287
- }
288
-
289
- let ms = calculate_multiscattering(
290
- optical_depth,
291
- participating_media_scattering,
292
- participating_media_extinction,
293
- u,
294
- participating_media_phase_g,
295
- volumetrics_metadata.ms_octave_count,
296
- volumetrics_metadata.ms_extinction_decay
297
- );
298
-
299
- inScatteringSum += shadow_term * ms * transmittance_atmospheric * sun.color;
300
- }
301
-
302
- // Local lights
303
- let cluster_metadata = light_cluster_metadata_by_position(
304
- view_pixel_coord,
305
- view_depth,
306
- vec2( view.width, view.height )
307
- );
308
-
309
- let cluster_offset = cluster_metadata.offset;
310
- let point_count = light_cluster_count_point(cluster_metadata.counts);
311
- let spot_count = light_cluster_count_spot(cluster_metadata.counts);
312
-
313
- for (var j = 0u; j < point_count; j++) {
314
- let light_index = cluster_data[cluster_offset + j];
315
- var direct_light = get_point_light_info_by_index(&light_data, light_index, position_ws);
316
-
317
- if (all(direct_light.color == vec3(0.0))) {
318
- continue;
319
- }
320
-
321
- let u = dot(direct_light.direction, view_ray.direction);
322
-
323
- // .direction points TOWARD the light; march the optical-depth ray toward the light (+direction).
324
- let shadow_ray = ${RAY_STRUCT.make({
325
- origin: "position_ws",
326
- direction: "direct_light.direction",
327
- tmax: "direct_light.distance",
328
- })};
329
-
330
- let optical_depth = integrate_optical_depth(shadow_ray);
331
-
332
- let ms = calculate_multiscattering(
333
- optical_depth,
334
- participating_media_scattering,
335
- participating_media_extinction,
336
- u,
337
- participating_media_phase_g,
338
- volumetrics_metadata.ms_octave_count,
339
- volumetrics_metadata.ms_extinction_decay
340
- );
341
-
342
- // Before this, integrate_optical_depth was the ONLY thing occluding a local light:
343
- // the medium shadowed itself and geometry did not shadow it at all, so a torch behind a
344
- // wall lit the fog through the wall and a light through a window cast no shaft. The
345
- // directional loop above has always multiplied its shadow map in; this is the same
346
- // operation for the clustered lights, which is what Frostbite and UE both do in the
347
- // froxel pass. One nearest atlas tap rather than the deferred path's filter kernel —
348
- // see the chunk for why that is not a compromise here.
349
- let shadow_term = volumetrics_point_light_shadow(&light_data, light_index, position_ws);
350
-
351
- inScatteringSum += shadow_term * ms * direct_light.color;
352
- }
353
-
354
- for (var k = 0u; k < spot_count; k++) {
355
- let light_index = cluster_data[cluster_offset + point_count + k];
356
- var direct_light = get_spot_light_info_by_index(&light_data, light_index, position_ws);
357
-
358
- if (all(direct_light.color == vec3(0.0))) {
359
- continue;
360
- }
361
-
362
- let u = dot(direct_light.direction, view_ray.direction);
363
-
364
- // .direction points TOWARD the light; march the optical-depth ray toward the light (+direction).
365
- let shadow_ray = ${RAY_STRUCT.make({
366
- origin: "position_ws",
367
- direction: "direct_light.direction",
368
- tmax: "direct_light.distance",
369
- })};
370
-
371
- let optical_depth = integrate_optical_depth(shadow_ray);
372
-
373
- let ms = calculate_multiscattering(
374
- optical_depth,
375
- participating_media_scattering,
376
- participating_media_extinction,
377
- u,
378
- participating_media_phase_g,
379
- volumetrics_metadata.ms_octave_count,
380
- volumetrics_metadata.ms_extinction_decay
381
- );
382
-
383
- // see the point loop above
384
- let shadow_term = volumetrics_spot_light_shadow(&light_data, light_index, position_ws);
385
-
386
- inScatteringSum += shadow_term * ms * direct_light.color;
387
- }
388
- }
389
-
390
-
391
- var inScattering = inScatteringSum / f32(SAMPLE_COUNT);
392
-
393
- // .a carries the mono extinction coefficient so the aerial pass can read a temporally-stable
394
- // sigma_t (after the 3D TAA) instead of re-sampling the raw per-frame-jittered PM extinction.
395
- // The composited fog transmittance is already mono (rgb_to_luminance of the integrated
396
- // throughput), so collapsing extinction to luminance only drops the subtle chromatic
397
- // depth-attenuation of the in-scatter.
398
- let extinction = extinctionSum / f32(SAMPLE_COUNT);
399
-
400
- textureStore(tLighting, voxel_coord, vec4<f32>(inScattering, extinction));
401
- }
402
- `, [
403
- chunk_cluster_depth_from_z_slice,
404
- chunk_cluster_depth_to_z_slice,
405
- chunk_texel_coordinate_to_uv,
406
- chunk_camera_ray_from_uv,
407
- chunk_camera_ray_planar_depth_scale,
408
- chunk_volumetrics_froxel_content_offset,
409
- chunk_uv_to_texel_coordinate,
410
-
411
- chunk_stbn_sample_vec3,
412
-
413
- chunk_light_cluster_metadata_by_position,
414
- chunk_light_cluster_counts,
415
- chunk_directional_lights_iteration_mask,
416
- chunk_get_directional_light_info_by_index,
417
- chunk_get_point_light_info_by_index,
418
- chunk_get_spot_light_info_by_index,
419
- chunk_shadowmap_get_light_visibility,
420
- chunk_volumetrics_local_light_shadow,
421
- chunk_sky_lut_compute_uv,
422
-
423
- RAY_STRUCT.declaration_chunk,
424
- chunk_integrate_optical_depth,
425
- chunk_volumetrics_sun_optical_depth,
426
- chunk_calculate_multiscattering,
427
- chunk_rgb_to_luminance,
428
- chunk_volumetrics_column_jitter,
429
- ]
430
- )
431
-
432
- export const shader_volumetrics_build_lighting = ComputeShader.from({
433
- label: "Volumetrics / build lighting",
434
- resources,
435
- body
436
- })
1
+ import { vec3f } from "../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
2
+ import { WebGPUArray } from "../shader/type/WebGPUArray.js";
3
+ import { CAMERA_UNIFORM_STRUCT } from "../camera/CAMERA_UNIFORM_STRUCT.js";
4
+ import { chunk_rgb_to_luminance } from "../shader/chunk/color/chunk_rgb_to_luminance.js";
5
+ import { chunk_stbn_sample_vec3 } from "../shader/chunk/noise/chunk_stbn_sample_vec3.js";
6
+ import { chunk_camera_ray_from_uv } from "../shader/chunk/geometry/ray/chunk_camera_ray_from_uv.js";
7
+ import { RAY_STRUCT } from "../shader/chunk/geometry/ray/RAY_STRUCT.js";
8
+ import {
9
+ chunk_directional_lights_iteration_mask
10
+ } from "../shader/chunk/light/io/chunk_directional_lights_iteration_mask.js";
11
+ import {
12
+ chunk_get_directional_light_info_by_index
13
+ } from "../shader/chunk/light/io/chunk_get_directional_light_info_by_index.js";
14
+ import {
15
+ chunk_get_point_light_info_by_index
16
+ } from "../shader/chunk/light/io/chunk_get_point_light_info_by_index.js";
17
+ import { chunk_get_spot_light_info_by_index } from "../shader/chunk/light/io/chunk_get_spot_light_info_by_index.js";
18
+ import { chunk_texel_coordinate_to_uv } from "../shader/chunk/texture/chunk_texel_coordinate_to_uv.js";
19
+ import { chunk_uv_to_texel_coordinate } from "../shader/chunk/texture/chunk_uv_to_texel_coordinate.js";
20
+ import { CodeChunk } from "../shader/compiler/CodeChunk.js";
21
+ import { ComputeShader } from "../shader/ComputeShader.js";
22
+ import { ShaderResourceSetDescriptor } from "../shader/resource/ShaderResourceSetDescriptor.js";
23
+ import { chunk_shadowmap_get_light_visibility } from "../shadow/map/shader/chunk_shadowmap_get_light_visibility.js";
24
+ import { SHADOWMAP_COMPARE_SAMPLER_DESCRIPTOR } from "../shadow/map/SHADOWMAP_COMPARE_SAMPLER_DESCRIPTOR.js";
25
+ import { SAMPLER_LINEAR_UPSCALE } from "../texture/SAMPLER_LINEAR_UPSCALE.js";
26
+ import { VIEW_UNIFORM_STRUCT } from "../view/VIEW_UNIFORM_STRUCT.js";
27
+ import { chunk_cluster_depth_from_z_slice } from "../light/cluster/chunk_cluster_depth_from_z_slice.js";
28
+ import { chunk_cluster_depth_to_z_slice } from "../light/cluster/chunk_cluster_depth_to_z_slice.js";
29
+ import { chunk_light_cluster_counts } from "../light/cluster/chunk_light_cluster_counts.js";
30
+ import { chunk_light_cluster_metadata_by_position } from "../light/cluster/chunk_light_cluster_metadata_by_position.js";
31
+ import { LIGHT_CLUSTER_LOOKUP_STRUCT } from "../light/cluster/shader_cluster_assign_lights.js";
32
+ import { chunk_sky_lut_compute_uv } from "../atmosphere/chunk_sky_lut_compute_uv.js";
33
+ import { chunk_calculate_multiscattering } from "./chunk_calculate_multiscattering.js";
34
+ import { chunk_camera_ray_planar_depth_scale } from "./chunk_camera_ray_planar_depth_scale.js";
35
+ import { chunk_volumetrics_froxel_content_offset } from "./chunk_volumetrics_froxel_content_offset.js";
36
+ import { chunk_integrate_optical_depth } from "./chunk_integrate_optical_depth.js";
37
+ import {
38
+ chunk_volumetrics_sun_optical_depth,
39
+ volumetrics_sun_shadow_texture_name
40
+ } from "./chunk_volumetrics_sun_optical_depth.js";
41
+ import { VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT } from "./VOLUMETRICS_SUN_SHADOW_RESOLUTION.js";
42
+ import { chunk_volumetrics_column_jitter } from "./chunk_volumetrics_column_jitter.js";
43
+ import { chunk_volumetrics_local_light_shadow } from "./chunk_volumetrics_local_light_shadow.js";
44
+ import { chunk_volumetrics_irradiance_sh } from "./chunk_volumetrics_irradiance_sh.js";
45
+ import {
46
+ METRO_L1_IRRADIANCE_STRUCT
47
+ } from "../shader/chunk/spherical_harmonics/l1i/chunk_convert_to_irradiance_sh.js";
48
+ import { VOLUMETRICS_METADATA_STRUCT } from "./VOLUMETRICS_METADATA_STRUCT.js";
49
+
50
+ /**
51
+ * Workgroup shape of this pass, `[x, y, z]` in froxels, stated to the `@workgroup_size` below.
52
+ * That attribute is where the dispatch gets the shape from in turn: `graph_build_volumetrics` asks
53
+ * for a thread count and `ComputeShader` divides by the size it read back out of the WGSL.
54
+ *
55
+ * Flat in z. Lights are binned into 3D clusters and the largest distance between neighbouring
56
+ * clusters is along depth, so a group spanning z is the one spanning the most unrelated light
57
+ * lists.
58
+ *
59
+ * 64 invocations — one wave64, two warps — is a deliberate trade: 16x16x1 measures faster on the
60
+ * RTX 4090 this was benchmarked on, and is kept off because that part's register file is unusually
61
+ * large and the result is not expected to generalize (AMD favours smaller groups). NOTES.md carries
62
+ * the numbers and the reasoning.
63
+ *
64
+ * @type {number[]}
65
+ */
66
+ export const VOLUMETRICS_LIGHTING_WORKGROUP_RESOLUTION = [8, 8, 1];
67
+
68
+ const resources = new ShaderResourceSetDescriptor();
69
+
70
+ const primary_group = resources.createGroup()
71
+ .addUniform("volumetrics_metadata", VOLUMETRICS_METADATA_STRUCT)
72
+ .addUniform("camera", CAMERA_UNIFORM_STRUCT)
73
+ .addUniform("view", VIEW_UNIFORM_STRUCT)
74
+ .addTexture("tSunTransmittance", "float", "2d")
75
+ .addTexture("tMultiScattering", "float", "2d")
76
+ .addSampler("sLinear", "filtering", SAMPLER_LINEAR_UPSCALE)
77
+ .addTexture("tSTBN_vec3", "unfilterable-float", "3d")
78
+ .addTexture("tParticipatingMedia0", "float", "3d")
79
+ .addTexture("tParticipatingMedia1", "float", "3d");
80
+
81
+ // Q-04: the sun's optical depth, pre-integrated along the light axis by
82
+ // shader_volumetrics_build_sun_shadow_volume, one cascade per texture and innermost first.
83
+ // Sampled, not marched.
84
+ for (let cascade = 0; cascade < VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT; cascade++) {
85
+ primary_group.addTexture(volumetrics_sun_shadow_texture_name(cascade), "float", "3d");
86
+ }
87
+
88
+ primary_group.addStorageTexture("tLighting", "rgba16float", "3d", "write-only")
89
+
90
+ // The froxel irradiance volume (chunk_volumetrics_irradiance_sh): luma SH2 in one, chroma in the
91
+ // .rg of the other. rgba16float for both, because rg16float is not a storage format without
92
+ // texture-formats-tier1. It is what the froxel GI stage (gi/) wrote to tGIY / tGICoCg, plus the lights.
93
+ primary_group.addTexture("tGIY", "float", "3d")
94
+ primary_group.addTexture("tGICoCg", "float", "3d")
95
+ primary_group.addStorageTexture("tIrradianceY", "rgba16float", "3d", "write-only")
96
+ primary_group.addStorageTexture("tIrradianceCoCg", "rgba16float", "3d", "write-only")
97
+
98
+ // Lighting information
99
+ resources.createGroup()
100
+
101
+ .addStorageBuffer("light_data", WebGPUArray.u32)
102
+ // clusters
103
+ .addUniform("cluster_parameters", vec3f)
104
+ .addStorageBuffer("cluster_lookup", LIGHT_CLUSTER_LOOKUP_STRUCT)
105
+ .addStorageBuffer("cluster_data", WebGPUArray.u32)
106
+ // shadows
107
+ .addTexture("shadowmapAtlas", "depth")
108
+ .addSampler("sCompareLinear", "comparison", SHADOWMAP_COMPARE_SAMPLER_DESCRIPTOR)
109
+
110
+ const body = CodeChunk.from(
111
+ //language=WGSL
112
+ `
113
+ @compute @workgroup_size(${VOLUMETRICS_LIGHTING_WORKGROUP_RESOLUTION.join(",")})
114
+ fn main(
115
+ @builtin(global_invocation_id) voxel_coord : vec3<u32>
116
+ ){
117
+ let resolution = textureDimensions(tParticipatingMedia0);
118
+
119
+ if(any(voxel_coord >= resolution)){
120
+ // overflow
121
+ return;
122
+ }
123
+
124
+ // Logical near/far, NOT the projection-derived range: the projection is infinite-far
125
+ // reverse-Z, so its far resolves to ~3.4e38 (FLOAT32_MAX). That value would leave the last
126
+ // froxel slice's z1 (and z1*z1*z1 below) at +Inf. clip_near_far is the bounded logical range,
127
+ // matching the participating-media build shader.
128
+ let near_far = camera.clip_near_far;
129
+
130
+ // same jitter valud for all voxels in Z column
131
+ // must be the same for PM and Lights
132
+ let z_jitter = volumetrics_column_jitter(voxel_coord.xy, view.frame_index);
133
+
134
+ let z_slice_0 = f32(voxel_coord.z ) + z_jitter;
135
+ let z_slice_1 = f32(voxel_coord.z + 1) + z_jitter;
136
+
137
+ let z0 = max(near_far.x, cluster_depth_from_z_slice( z_slice_0, volumetrics_metadata.froxel_cluster_parameters, f32(resolution.z) ));
138
+ let z1 = min(near_far.y, cluster_depth_from_z_slice( z_slice_1, volumetrics_metadata.froxel_cluster_parameters, f32(resolution.z) ));
139
+
140
+ // 2. Cubic mix gives uniform distribution in World Space Volume
141
+ let z3_min = z0 * z0 * z0;
142
+ let z3_max = z1 * z1 * z1;
143
+
144
+ var inScatteringSum = vec3<f32>(0.0);
145
+ var extinctionSum = 0.0;
146
+
147
+ // What a transparent surface at the sample receives from the same lights: their shadowed
148
+ // colour, attenuated by the medium toward each light, before any scattering by it.
149
+ var irradianceSum: ${METRO_L1_IRRADIANCE_STRUCT.wgsl_ref};
150
+
151
+
152
+ // One sample per froxel. The 0.95-blend TAA downstream is a ~39-effective-sample temporal
153
+ // integrator; supersampling here as well spends N times this pass removing variance that filter
154
+ // removes anyway — and spends it at the Monte Carlo rate, because the samples were independent
155
+ // white noise and converged as 1/sqrt(N). Raising this is only worth it with the temporal
156
+ // filter off.
157
+ const SAMPLE_COUNT = 1u;
158
+
159
+ for(var i = 0u; i < SAMPLE_COUNT; i++){
160
+
161
+ // Spatiotemporal blue noise rather than a per-froxel hash. What lets 1 spp survive a
162
+ // temporal filter is each froxel seeing a well-distributed sequence over frames, which is
163
+ // STBN's defining property and exactly what white noise lacks — measured against the murmur
164
+ // hash it reaches the same error in roughly half the frames, across the whole window the
165
+ // 0.95 blend spans.
166
+ //
167
+ // The xy offset is load-bearing, not decoration. The aerial pass reads this same volume at
168
+ // (voxel_coord.xy, frame_index) and consumes all three channels, so sampling it there would
169
+ // not merely correlate the two passes' jitters — it would make them the same number. Two
170
+ // nuisance variables locked together stop covering the sample domain and the estimator
171
+ // converges to the wrong value, a bias that does NOT shrink with more frames, so no TAA
172
+ // window recovers it. Measured worst case |r| against any aerial channel: 1.0000 at zero
173
+ // offset, 0.0027 here.
174
+ //
175
+ // froxel z rides the temporal axis so froxels down a column do not all share one value —
176
+ // the fetch is indexed by column, and STBN's z axis is the one built to be well distributed.
177
+ const STBN_DECORRELATION_OFFSET = vec2<u32>(37u, 71u);
178
+
179
+ let noise = stbn_sample_vec3(vec3(
180
+ voxel_coord.xy + STBN_DECORRELATION_OFFSET,
181
+ (view.frame_index + voxel_coord.z) * SAMPLE_COUNT + i
182
+ ));
183
+
184
+ // [0..1] — offset (jitter - 0.5) must be centered on the froxel for every axis. The depth
185
+ // axis is stratified so that raising SAMPLE_COUNT converges faster than 1/sqrt(N); a no-op
186
+ // while it is 1.
187
+ let jitter = vec3(noise.xy, (f32(i) + noise.z) / f32(SAMPLE_COUNT));
188
+
189
+
190
+ // Interpolate the cubes, then take the cube root
191
+ let z3_sample = mix(z3_min, z3_max, jitter.z);
192
+ let view_depth = pow(z3_sample, 0.333333);
193
+
194
+ let sample_z_index:f32 = cluster_depth_to_z_slice(
195
+ view_depth,
196
+ volumetrics_metadata.froxel_cluster_parameters,
197
+ f32(resolution.z)
198
+ );
199
+
200
+ // 'saturate' prevents accidental out-of-bounds samples
201
+ let tile_uv = saturate(
202
+ texel_coordinate_to_uv(vec2f(voxel_coord.xy) + (jitter.xy - 0.5), resolution.xy)
203
+ );
204
+ let view_ray = camera_ray_from_uv(tile_uv, camera);
205
+
206
+ // The PM volumes are CELL-AVERAGED: texel k holds the slab [k+j, k+1+j], so its content
207
+ // sits at slice k + j + delta, not at slice k. Addressing them with the plain texel-centre
208
+ // convention is what the boundary-sampled aerial LUT wants, and reads the medium about half
209
+ // a froxel too far out. delta is a property of the depth curve — see the chunk.
210
+ let content_offset = volumetrics_froxel_content_offset(volumetrics_metadata.froxel_cluster_parameters.z);
211
+
212
+ let uvw = vec3<f32>(tile_uv, (sample_z_index + 0.5 - content_offset) / f32(resolution.z));
213
+
214
+ let participating_media0 = textureSampleLevel(tParticipatingMedia0, sLinear, uvw, 0.0);
215
+ let participating_media1 = textureSampleLevel(tParticipatingMedia1, sLinear, uvw, 0.0);
216
+
217
+ let participating_media_scattering = participating_media0.rgb;
218
+ let participating_media_extinction = participating_media1.rgb;
219
+ let participating_media_phase_g = participating_media1.a;
220
+
221
+ // Accumulate the (linear) mono extinction coefficient. Carried in tLighting.a below so the
222
+ // 3D TAA accumulates sigma_t alongside the in-scatter (Hillaire: temporally integrate the
223
+ // LINEAR extinction, never the non-linear transmittance).
224
+ extinctionSum += rgb_to_luminance(participating_media_extinction);
225
+
226
+ let view_pixel_coord = uv_to_texel_coordinate(tile_uv, vec2(view.width, view.height));
227
+
228
+ // view_depth is a PLANAR view-space depth and view_ray.direction is unit length, so the
229
+ // depth has to be converted to a distance along the ray before stepping. Multiplying them
230
+ // directly stops on the arc of constant distance instead of the plane of constant depth,
231
+ // short by cos(theta) — exact on the centre axis, ~72% of the intended depth in the corners
232
+ // of a 50 degree vertical FOV. Radial, which is why it reads as "the fog looks a bit off"
233
+ // rather than as an offset.
234
+ //
235
+ // Everything downstream assumes the planar reading: the light cluster is looked up at the
236
+ // planar depth, the shadow lookup happens at this position, and the TAA reconstructs froxel
237
+ // centres as planar slabs.
238
+ let position_ws = view_ray.origin
239
+ + view_ray.direction * (view_depth * camera_ray_planar_depth_scale(view_ray.direction, camera));
240
+
241
+ // Global lights (directional) — iterate over the first 32 slots
242
+ // of the directional light table (max enforced by GPULightCollection).
243
+ var directional_bits = directional_lights_iteration_mask(&light_data);
244
+ while (directional_bits != 0u) {
245
+ let directional_index = countTrailingZeros(directional_bits);
246
+ directional_bits &= ~(1u << directional_index);
247
+
248
+ let sun = get_directional_light_info_by_index(&light_data, directional_index);
249
+
250
+ let sun_lut_uv = sky_lut_compute_uv(position_ws, sun.direction, vec2<f32>(textureDimensions(tSunTransmittance)));
251
+
252
+ let shadow_term = shadowmap_get_directional_light_visibility(
253
+ &light_data,
254
+ directional_index,
255
+ position_ws,
256
+ // toward-camera, matching the deferred cascade-blend convention. The consumer
257
+ // (shadowmap_csm_compute_cascade_blended) negates this internally; the deferred
258
+ // caller passes the surface->camera vector, whereas view_ray.direction is
259
+ // camera->scene, so it must be negated here to agree.
260
+ -view_ray.direction,
261
+ sun.direction
262
+ );
263
+
264
+ let u = dot(sun.direction, view_ray.direction);
265
+
266
+ // sample atmospheric transmittance
267
+ let transmittance_atmospheric = textureSampleLevel(
268
+ tSunTransmittance,
269
+ sLinear,
270
+ sun_lut_uv, 0.0
271
+ ).rgb;
272
+
273
+ // Q-04. Every froxel on this sun ray is asking for the same integral, one increment
274
+ // apart, and the march recomputes all of it from scratch for each: O(froxels * steps)
275
+ // to produce O(froxels) of information. The sun is where that is worst, because its
276
+ // tmax is the distance to the sun clipped to the frustum, so the march spans the whole
277
+ // grid rather than the few metres a lamp's does.
278
+ //
279
+ // So read the answer out of the cascades built once this frame with their axis on the
280
+ // sun, where the increment relation IS the storage — see
281
+ // shader_volumetrics_build_sun_shadow_volume. Same quantity, same units, one fetch.
282
+ //
283
+ // The march below is not an alternative to it, it is what is left for the lights the
284
+ // cascades do not cover: a second directional light, since there is one ladder and it
285
+ // is aligned to one direction, and the two cases that cannot place a box at all, which
286
+ // sun_shadow_light_index reports by holding a value no light index can equal.
287
+ var optical_depth: vec3<f32>;
288
+
289
+ if (directional_index == volumetrics_metadata.sun_shadow_light_index) {
290
+
291
+ optical_depth = volumetrics_sun_optical_depth(position_ws);
292
+
293
+ } else {
294
+
295
+ // .direction points TOWARD the light; march the optical-depth ray toward the light (+direction).
296
+ let shadow_ray = ${RAY_STRUCT.make({
297
+ origin: "position_ws",
298
+ direction: "sun.direction",
299
+ tmax: "sun.distance",
300
+ })};
301
+
302
+ optical_depth = integrate_optical_depth(shadow_ray);
303
+ }
304
+
305
+ let ms = calculate_multiscattering(
306
+ optical_depth,
307
+ participating_media_scattering,
308
+ participating_media_extinction,
309
+ u,
310
+ participating_media_phase_g,
311
+ volumetrics_metadata.ms_octave_count,
312
+ volumetrics_metadata.ms_extinction_decay
313
+ );
314
+
315
+ inScatteringSum += shadow_term * ms * transmittance_atmospheric * sun.color;
316
+
317
+ // No atmospheric transmittance: that is the air's, and a surface in the volume is lit
318
+ // the way the surfaces around it are — the deferred and forward paths light meshes by
319
+ // sun.color as it stands, and a lit particle beside a lit log has to agree with it.
320
+ irradianceSum = volumetrics_irradiance_sh_add(
321
+ irradianceSum,
322
+ shadow_term * exp(-optical_depth) * sun.color,
323
+ sun.direction
324
+ );
325
+ }
326
+
327
+ // Local lights
328
+ let cluster_metadata = light_cluster_metadata_by_position(
329
+ view_pixel_coord,
330
+ view_depth,
331
+ vec2( view.width, view.height )
332
+ );
333
+
334
+ let cluster_offset = cluster_metadata.offset;
335
+ let point_count = light_cluster_count_point(cluster_metadata.counts);
336
+ let spot_count = light_cluster_count_spot(cluster_metadata.counts);
337
+
338
+ for (var j = 0u; j < point_count; j++) {
339
+ let light_index = cluster_data[cluster_offset + j];
340
+ var direct_light = get_point_light_info_by_index(&light_data, light_index, position_ws);
341
+
342
+ if (all(direct_light.color == vec3(0.0))) {
343
+ continue;
344
+ }
345
+
346
+ let u = dot(direct_light.direction, view_ray.direction);
347
+
348
+ // .direction points TOWARD the light; march the optical-depth ray toward the light (+direction).
349
+ let shadow_ray = ${RAY_STRUCT.make({
350
+ origin: "position_ws",
351
+ direction: "direct_light.direction",
352
+ tmax: "direct_light.distance",
353
+ })};
354
+
355
+ let optical_depth = integrate_optical_depth(shadow_ray);
356
+
357
+ let ms = calculate_multiscattering(
358
+ optical_depth,
359
+ participating_media_scattering,
360
+ participating_media_extinction,
361
+ u,
362
+ participating_media_phase_g,
363
+ volumetrics_metadata.ms_octave_count,
364
+ volumetrics_metadata.ms_extinction_decay
365
+ );
366
+
367
+ // Before this, integrate_optical_depth was the ONLY thing occluding a local light:
368
+ // the medium shadowed itself and geometry did not shadow it at all, so a torch behind a
369
+ // wall lit the fog through the wall and a light through a window cast no shaft. The
370
+ // directional loop above has always multiplied its shadow map in; this is the same
371
+ // operation for the clustered lights, which is what Frostbite and UE both do in the
372
+ // froxel pass. One nearest atlas tap rather than the deferred path's filter kernel —
373
+ // see the chunk for why that is not a compromise here.
374
+ let shadow_term = volumetrics_point_light_shadow(&light_data, light_index, position_ws);
375
+
376
+ inScatteringSum += shadow_term * ms * direct_light.color;
377
+
378
+ irradianceSum = volumetrics_irradiance_sh_add(
379
+ irradianceSum,
380
+ shadow_term * exp(-optical_depth) * direct_light.color,
381
+ direct_light.direction
382
+ );
383
+ }
384
+
385
+ for (var k = 0u; k < spot_count; k++) {
386
+ let light_index = cluster_data[cluster_offset + point_count + k];
387
+ var direct_light = get_spot_light_info_by_index(&light_data, light_index, position_ws);
388
+
389
+ if (all(direct_light.color == vec3(0.0))) {
390
+ continue;
391
+ }
392
+
393
+ let u = dot(direct_light.direction, view_ray.direction);
394
+
395
+ // .direction points TOWARD the light; march the optical-depth ray toward the light (+direction).
396
+ let shadow_ray = ${RAY_STRUCT.make({
397
+ origin: "position_ws",
398
+ direction: "direct_light.direction",
399
+ tmax: "direct_light.distance",
400
+ })};
401
+
402
+ let optical_depth = integrate_optical_depth(shadow_ray);
403
+
404
+ let ms = calculate_multiscattering(
405
+ optical_depth,
406
+ participating_media_scattering,
407
+ participating_media_extinction,
408
+ u,
409
+ participating_media_phase_g,
410
+ volumetrics_metadata.ms_octave_count,
411
+ volumetrics_metadata.ms_extinction_decay
412
+ );
413
+
414
+ // see the point loop above
415
+ let shadow_term = volumetrics_spot_light_shadow(&light_data, light_index, position_ws);
416
+
417
+ inScatteringSum += shadow_term * ms * direct_light.color;
418
+
419
+ irradianceSum = volumetrics_irradiance_sh_add(
420
+ irradianceSum,
421
+ shadow_term * exp(-optical_depth) * direct_light.color,
422
+ direct_light.direction
423
+ );
424
+ }
425
+ }
426
+
427
+
428
+ var inScattering = inScatteringSum / f32(SAMPLE_COUNT);
429
+
430
+ // .a carries the mono extinction coefficient so the aerial pass can read a temporally-stable
431
+ // sigma_t (after the 3D TAA) instead of re-sampling the raw per-frame-jittered PM extinction.
432
+ // The composited fog transmittance is already mono (rgb_to_luminance of the integrated
433
+ // throughput), so collapsing extinction to luminance only drops the subtle chromatic
434
+ // depth-attenuation of the in-scatter.
435
+ let extinction = extinctionSum / f32(SAMPLE_COUNT);
436
+
437
+ textureStore(tLighting, voxel_coord, vec4<f32>(inScattering, extinction));
438
+
439
+ irradianceSum.Y /= f32(SAMPLE_COUNT);
440
+ irradianceSum.CoCg /= f32(SAMPLE_COUNT);
441
+
442
+ // What the renderer's indirect lighting gives this froxel, in the same encoding; the lights add
443
+ // to it. Written by the froxel GI module the mode selects, which is all this pass knows of GI.
444
+ irradianceSum.Y += textureLoad(tGIY, voxel_coord, 0);
445
+ irradianceSum.CoCg += textureLoad(tGICoCg, voxel_coord, 0).xy;
446
+
447
+ // nothing TAA-filters this volume, so nothing downstream would stop an Inf or a NaN here
448
+ let irradiance = volumetrics_irradiance_sh_storable(irradianceSum);
449
+
450
+ textureStore(tIrradianceY, voxel_coord, irradiance.Y);
451
+ textureStore(tIrradianceCoCg, voxel_coord, vec4<f32>(irradiance.CoCg, 0.0, 0.0));
452
+ }
453
+ `, [
454
+ chunk_cluster_depth_from_z_slice,
455
+ chunk_cluster_depth_to_z_slice,
456
+ chunk_texel_coordinate_to_uv,
457
+ chunk_camera_ray_from_uv,
458
+ chunk_camera_ray_planar_depth_scale,
459
+ chunk_volumetrics_froxel_content_offset,
460
+ chunk_uv_to_texel_coordinate,
461
+
462
+ chunk_stbn_sample_vec3,
463
+
464
+ chunk_light_cluster_metadata_by_position,
465
+ chunk_light_cluster_counts,
466
+ chunk_directional_lights_iteration_mask,
467
+ chunk_get_directional_light_info_by_index,
468
+ chunk_get_point_light_info_by_index,
469
+ chunk_get_spot_light_info_by_index,
470
+ chunk_shadowmap_get_light_visibility,
471
+ chunk_volumetrics_local_light_shadow,
472
+ chunk_volumetrics_irradiance_sh,
473
+ chunk_sky_lut_compute_uv,
474
+
475
+ RAY_STRUCT.declaration_chunk,
476
+ chunk_integrate_optical_depth,
477
+ chunk_volumetrics_sun_optical_depth,
478
+ chunk_calculate_multiscattering,
479
+ chunk_rgb_to_luminance,
480
+ chunk_volumetrics_column_jitter,
481
+ ]
482
+ )
483
+
484
+ export const shader_volumetrics_build_lighting = ComputeShader.from({
485
+ label: "Volumetrics / build lighting",
486
+ resources,
487
+ body
488
+ })