@woosh/meep-engine 3.11.0 → 3.11.2

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 (86) hide show
  1. package/package.json +1 -1
  2. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  3. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +294 -290
  4. package/src/engine/graphics3/ParticipatingMedia.d.ts +111 -0
  5. package/src/engine/graphics3/ParticipatingMedia.d.ts.map +1 -0
  6. package/src/engine/graphics3/ParticipatingMedia.js +203 -0
  7. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts +34 -0
  8. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts.map +1 -0
  9. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.js +73 -0
  10. package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts +87 -0
  11. package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts.map +1 -0
  12. package/src/engine/graphics3/ParticipatingMediaSystem3.js +184 -0
  13. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  14. package/src/shade/playground/volumetrics_froxel/README.md +191 -0
  15. package/src/shade/playground/volumetrics_froxel/index.html +270 -0
  16. package/src/shade/playground/volumetrics_froxel/main.d.ts +2 -0
  17. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -0
  18. package/src/shade/playground/volumetrics_froxel/main.js +917 -0
  19. package/src/shade/playground/volumetrics_froxel/measure.d.ts +119 -0
  20. package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -0
  21. package/src/shade/playground/volumetrics_froxel/measure.js +559 -0
  22. package/src/shade/renderer/Renderer.d.ts +13 -0
  23. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  24. package/src/shade/renderer/Renderer.js +45 -18
  25. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  26. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts +14 -13
  27. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts.map +1 -1
  28. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.js +133 -129
  29. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  30. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +480 -315
  31. package/src/shade/renderer/volumetrics/NOTES.md +691 -9
  32. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts +5 -0
  33. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts.map +1 -1
  34. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.js +21 -2
  35. package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts +26 -0
  36. package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts.map +1 -1
  37. package/src/shade/renderer/volumetrics/SceneVolumetrics.js +64 -21
  38. package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.d.ts.map +1 -1
  39. package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.js +84 -28
  40. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts +98 -0
  41. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts.map +1 -0
  42. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.js +111 -0
  43. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts +84 -0
  44. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts.map +1 -0
  45. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.js +252 -0
  46. package/src/shade/renderer/volumetrics/chunk_camera_ray_planar_depth_scale.d.ts +21 -0
  47. package/src/shade/renderer/volumetrics/chunk_camera_ray_planar_depth_scale.d.ts.map +1 -0
  48. package/src/shade/renderer/volumetrics/chunk_camera_ray_planar_depth_scale.js +47 -0
  49. package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.d.ts.map +1 -1
  50. package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js +75 -64
  51. package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_content_offset.d.ts +37 -0
  52. package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_content_offset.d.ts.map +1 -0
  53. package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_content_offset.js +56 -0
  54. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts +43 -0
  55. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts.map +1 -0
  56. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.js +145 -0
  57. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts +20 -0
  58. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts.map +1 -0
  59. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.js +48 -0
  60. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts +38 -0
  61. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts.map +1 -0
  62. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.js +107 -0
  63. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +4 -1
  64. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  65. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +577 -465
  66. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
  67. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +418 -324
  68. package/src/shade/renderer/volumetrics/shader_volumetrics_build_scattering_lut.d.ts.map +1 -1
  69. package/src/shade/renderer/volumetrics/shader_volumetrics_build_scattering_lut.js +14 -3
  70. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts +8 -0
  71. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts.map +1 -0
  72. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.js +174 -0
  73. package/src/shade/renderer/volumetrics/volumetrics_position_world_to_froxel_uvw.d.ts.map +1 -1
  74. package/src/shade/renderer/volumetrics/volumetrics_position_world_to_froxel_uvw.js +48 -36
  75. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts +25 -0
  76. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts.map +1 -1
  77. package/src/shade/wgsl/emulator/CPUBitmapData.js +33 -0
  78. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2221 -2215
  79. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  80. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +1297 -1117
  81. package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts +69 -0
  82. package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts.map +1 -0
  83. package/src/shade/wgsl/emulator/cpu_texture_gather.js +148 -0
  84. package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts +16 -0
  85. package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts.map +1 -1
  86. package/src/shade/wgsl/emulator/cpu_texture_sample.js +176 -172
@@ -1,324 +1,418 @@
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_camera_ray_from_uv } from "../shader/chunk/geometry/ray/chunk_camera_ray_from_uv.js";
6
- import { RAY_STRUCT } from "../shader/chunk/geometry/ray/RAY_STRUCT.js";
7
- import {
8
- chunk_directional_lights_iteration_mask
9
- } from "../shader/chunk/light/io/chunk_directional_lights_iteration_mask.js";
10
- import {
11
- chunk_get_directional_light_info_by_index
12
- } from "../shader/chunk/light/io/chunk_get_directional_light_info_by_index.js";
13
- import {
14
- chunk_get_point_light_info_by_index
15
- } from "../shader/chunk/light/io/chunk_get_point_light_info_by_index.js";
16
- import { chunk_get_spot_light_info_by_index } from "../shader/chunk/light/io/chunk_get_spot_light_info_by_index.js";
17
- import { chunk_random } from "../shader/chunk/random/chunk_random.js";
18
- import { chunk_random_initialize } from "../shader/chunk/random/chunk_random_initialize.js";
19
- import { chunk_texel_coordinate_to_uv } from "../shader/chunk/texture/chunk_texel_coordinate_to_uv.js";
20
- import { chunk_uv_to_texel_coordinate } from "../shader/chunk/texture/chunk_uv_to_texel_coordinate.js";
21
- import { CodeChunk } from "../shader/compiler/CodeChunk.js";
22
- import { ComputeShader } from "../shader/ComputeShader.js";
23
- import { ShaderResourceSetDescriptor } from "../shader/resource/ShaderResourceSetDescriptor.js";
24
- import { chunk_shadowmap_get_light_visibility } from "../shadow/map/shader/chunk_shadowmap_get_light_visibility.js";
25
- import { SHADOWMAP_COMPARE_SAMPLER_DESCRIPTOR } from "../shadow/map/SHADOWMAP_COMPARE_SAMPLER_DESCRIPTOR.js";
26
- import { SAMPLER_LINEAR_UPSCALE } from "../texture/SAMPLER_LINEAR_UPSCALE.js";
27
- import { VIEW_UNIFORM_STRUCT } from "../view/VIEW_UNIFORM_STRUCT.js";
28
- import { chunk_cluster_depth_from_z_slice } from "../light/cluster/chunk_cluster_depth_from_z_slice.js";
29
- import { chunk_cluster_depth_to_z_slice } from "../light/cluster/chunk_cluster_depth_to_z_slice.js";
30
- import { chunk_light_cluster_counts } from "../light/cluster/chunk_light_cluster_counts.js";
31
- import { chunk_light_cluster_metadata_by_position } from "../light/cluster/chunk_light_cluster_metadata_by_position.js";
32
- import { LIGHT_CLUSTER_LOOKUP_STRUCT } from "../light/cluster/shader_cluster_assign_lights.js";
33
- import { chunk_sky_lut_compute_uv } from "../atmosphere/chunk_sky_lut_compute_uv.js";
34
- import { chunk_calculate_multiscattering } from "./chunk_calculate_multiscattering.js";
35
- import { chunk_integrate_optical_depth } from "./chunk_integrate_optical_depth.js";
36
- import { chunk_volumetrics_column_jitter } from "./chunk_volumetrics_column_jitter.js";
37
- import { VOLUMETRICS_METADATA_STRUCT } from "./VOLUMETRICS_METADATA_STRUCT.js";
38
-
39
- const resources = new ShaderResourceSetDescriptor();
40
-
41
- resources.createGroup()
42
- .addUniform("volumetrics_metadata", VOLUMETRICS_METADATA_STRUCT)
43
- .addUniform("camera", CAMERA_UNIFORM_STRUCT)
44
- .addUniform("view", VIEW_UNIFORM_STRUCT)
45
- .addTexture("tSunTransmittance", "float", "2d")
46
- .addTexture("tMultiScattering", "float", "2d")
47
- .addSampler("sLinear", "filtering", SAMPLER_LINEAR_UPSCALE)
48
- .addTexture("tParticipatingMedia0", "float", "3d")
49
- .addTexture("tParticipatingMedia1", "float", "3d")
50
- .addStorageTexture("tLighting", "rgba16float", "3d", "write-only")
51
-
52
- // Lighting information
53
- resources.createGroup()
54
-
55
- .addStorageBuffer("light_data", WebGPUArray.u32)
56
- // clusters
57
- .addUniform("cluster_parameters", vec3f)
58
- .addStorageBuffer("cluster_lookup", LIGHT_CLUSTER_LOOKUP_STRUCT)
59
- .addStorageBuffer("cluster_data", WebGPUArray.u32)
60
- // shadows
61
- .addTexture("shadowmapAtlas", "depth")
62
- .addSampler("sCompareLinear", "comparison", SHADOWMAP_COMPARE_SAMPLER_DESCRIPTOR)
63
-
64
- const body = CodeChunk.from(
65
- //language=WGSL
66
- `
67
- @compute @workgroup_size(16,16)
68
- fn main(
69
- @builtin(global_invocation_id) voxel_coord : vec3<u32>
70
- ){
71
- let resolution = textureDimensions(tParticipatingMedia0);
72
-
73
- if(any(voxel_coord >= resolution)){
74
- // overflow
75
- return;
76
- }
77
-
78
- random_initialize(voxel_coord, vec3(view.frame_index, 0x9E3779B9, 0xBABECAFE));
79
-
80
- // Logical near/far, NOT the projection-derived range: the projection is infinite-far
81
- // reverse-Z, so its far resolves to ~3.4e38 (FLOAT32_MAX). That value would leave the last
82
- // froxel slice's z1 (and z1*z1*z1 below) at +Inf. clip_near_far is the bounded logical range,
83
- // matching the participating-media build shader.
84
- let near_far = camera.clip_near_far;
85
-
86
- // same jitter valud for all voxels in Z column
87
- // must be the same for PM and Lights
88
- let z_jitter = volumetrics_column_jitter(voxel_coord.xy, view.frame_index);
89
-
90
- let z_slice_0 = f32(voxel_coord.z ) + z_jitter;
91
- let z_slice_1 = f32(voxel_coord.z + 1) + z_jitter;
92
-
93
- let z0 = max(near_far.x, cluster_depth_from_z_slice( z_slice_0, volumetrics_metadata.froxel_cluster_parameters, f32(resolution.z) ));
94
- let z1 = min(near_far.y, cluster_depth_from_z_slice( z_slice_1, volumetrics_metadata.froxel_cluster_parameters, f32(resolution.z) ));
95
-
96
- // 2. Cubic mix gives uniform distribution in World Space Volume
97
- let z3_min = z0 * z0 * z0;
98
- let z3_max = z1 * z1 * z1;
99
-
100
- var inScatteringSum = vec3<f32>(0.0);
101
- var extinctionSum = 0.0;
102
-
103
-
104
- const SAMPLE_COUNT = 6;
105
-
106
- // integrate multiple samples inside the voxel, to reduce noise.
107
-
108
- for(var i = 0; i < SAMPLE_COUNT; i++){
109
-
110
- let jitter = vec3(
111
- random(),
112
- random(),
113
- random(),
114
- ); // [0..1] — offset (jitter - 0.5) must be centered on the froxel for every axis
115
-
116
-
117
- // Interpolate the cubes, then take the cube root
118
- let z3_sample = mix(z3_min, z3_max, jitter.z);
119
- let view_depth = pow(z3_sample, 0.333333);
120
-
121
- let sample_z_index:f32 = cluster_depth_to_z_slice(
122
- view_depth,
123
- volumetrics_metadata.froxel_cluster_parameters,
124
- f32(resolution.z)
125
- );
126
-
127
- // 'saturate' prevents accidental out-of-bounds samples
128
- let tile_uv = saturate(
129
- texel_coordinate_to_uv(vec2f(voxel_coord.xy) + (jitter.xy - 0.5), resolution.xy)
130
- );
131
- let view_ray = camera_ray_from_uv(tile_uv, camera);
132
-
133
- let uvw = vec3<f32>(tile_uv, (sample_z_index + 0.5) / f32(resolution.z));
134
-
135
- let participating_media0 = textureSampleLevel(tParticipatingMedia0, sLinear, uvw, 0.0);
136
- let participating_media1 = textureSampleLevel(tParticipatingMedia1, sLinear, uvw, 0.0);
137
-
138
- let participating_media_scattering = participating_media0.rgb;
139
- let participating_media_extinction = participating_media1.rgb;
140
- let participating_media_phase_g = participating_media1.a;
141
-
142
- // Accumulate the (linear) mono extinction coefficient. Carried in tLighting.a below so the
143
- // 3D TAA accumulates sigma_t alongside the in-scatter (Hillaire: temporally integrate the
144
- // LINEAR extinction, never the non-linear transmittance).
145
- extinctionSum += rgb_to_luminance(participating_media_extinction);
146
-
147
- let view_pixel_coord = uv_to_texel_coordinate(tile_uv, vec2(view.width, view.height));
148
-
149
- let position_ws = view_ray.origin + view_ray.direction * view_depth;
150
-
151
- // Global lights (directional) iterate over the first 32 slots
152
- // of the directional light table (max enforced by GPULightCollection).
153
- var directional_bits = directional_lights_iteration_mask(&light_data);
154
- while (directional_bits != 0u) {
155
- let directional_index = countTrailingZeros(directional_bits);
156
- directional_bits &= ~(1u << directional_index);
157
-
158
- let sun = get_directional_light_info_by_index(&light_data, directional_index);
159
-
160
- let sun_lut_uv = sky_lut_compute_uv(position_ws, sun.direction);
161
-
162
- let shadow_term = shadowmap_get_directional_light_visibility(
163
- &light_data,
164
- directional_index,
165
- position_ws,
166
- // toward-camera, matching the deferred cascade-blend convention. The consumer
167
- // (shadowmap_csm_compute_cascade_blended) negates this internally; the deferred
168
- // caller passes the surface->camera vector, whereas view_ray.direction is
169
- // camera->scene, so it must be negated here to agree.
170
- -view_ray.direction,
171
- sun.direction
172
- );
173
-
174
- let u = dot(sun.direction, view_ray.direction);
175
-
176
- // sample atmospheric transmittance
177
- let transmittance_atmospheric = textureSampleLevel(
178
- tSunTransmittance,
179
- sLinear,
180
- sun_lut_uv, 0.0
181
- ).rgb;
182
-
183
- // .direction points TOWARD the light; march the optical-depth ray toward the light (+direction).
184
- let shadow_ray = ${RAY_STRUCT.make({
185
- origin: "position_ws",
186
- direction: "sun.direction",
187
- tmax: "sun.distance",
188
- })};
189
-
190
- let optical_depth = integrate_optical_depth(shadow_ray);
191
-
192
- let ms = calculate_multiscattering(
193
- optical_depth,
194
- participating_media_scattering,
195
- participating_media_extinction,
196
- u,
197
- participating_media_phase_g,
198
- volumetrics_metadata.ms_octave_count,
199
- volumetrics_metadata.ms_extinction_decay
200
- );
201
-
202
- inScatteringSum += shadow_term * ms * transmittance_atmospheric * sun.color;
203
- }
204
-
205
- // Local lights
206
- let cluster_metadata = light_cluster_metadata_by_position(
207
- view_pixel_coord,
208
- view_depth,
209
- vec2( view.width, view.height )
210
- );
211
-
212
- let cluster_offset = cluster_metadata.offset;
213
- let point_count = light_cluster_count_point(cluster_metadata.counts);
214
- let spot_count = light_cluster_count_spot(cluster_metadata.counts);
215
-
216
- for (var j = 0u; j < point_count; j++) {
217
- let light_index = cluster_data[cluster_offset + j];
218
- var direct_light = get_point_light_info_by_index(&light_data, light_index, position_ws);
219
-
220
- if (all(direct_light.color == vec3(0.0))) {
221
- continue;
222
- }
223
-
224
- let u = dot(direct_light.direction, view_ray.direction);
225
-
226
- // .direction points TOWARD the light; march the optical-depth ray toward the light (+direction).
227
- let shadow_ray = ${RAY_STRUCT.make({
228
- origin: "position_ws",
229
- direction: "direct_light.direction",
230
- tmax: "direct_light.distance",
231
- })};
232
-
233
- let optical_depth = integrate_optical_depth(shadow_ray);
234
-
235
- let ms = calculate_multiscattering(
236
- optical_depth,
237
- participating_media_scattering,
238
- participating_media_extinction,
239
- u,
240
- participating_media_phase_g,
241
- volumetrics_metadata.ms_octave_count,
242
- volumetrics_metadata.ms_extinction_decay
243
- );
244
-
245
- inScatteringSum += ms * direct_light.color;
246
- }
247
-
248
- for (var k = 0u; k < spot_count; k++) {
249
- let light_index = cluster_data[cluster_offset + point_count + k];
250
- var direct_light = get_spot_light_info_by_index(&light_data, light_index, position_ws);
251
-
252
- if (all(direct_light.color == vec3(0.0))) {
253
- continue;
254
- }
255
-
256
- let u = dot(direct_light.direction, view_ray.direction);
257
-
258
- // .direction points TOWARD the light; march the optical-depth ray toward the light (+direction).
259
- let shadow_ray = ${RAY_STRUCT.make({
260
- origin: "position_ws",
261
- direction: "direct_light.direction",
262
- tmax: "direct_light.distance",
263
- })};
264
-
265
- let optical_depth = integrate_optical_depth(shadow_ray);
266
-
267
- let ms = calculate_multiscattering(
268
- optical_depth,
269
- participating_media_scattering,
270
- participating_media_extinction,
271
- u,
272
- participating_media_phase_g,
273
- volumetrics_metadata.ms_octave_count,
274
- volumetrics_metadata.ms_extinction_decay
275
- );
276
-
277
- inScatteringSum += ms * direct_light.color;
278
- }
279
- }
280
-
281
-
282
- var inScattering = inScatteringSum / SAMPLE_COUNT;
283
-
284
- // .a carries the mono extinction coefficient so the aerial pass can read a temporally-stable
285
- // sigma_t (after the 3D TAA) instead of re-sampling the raw per-frame-jittered PM extinction.
286
- // The composited fog transmittance is already mono (rgb_to_luminance of the integrated
287
- // throughput), so collapsing extinction to luminance only drops the subtle chromatic
288
- // depth-attenuation of the in-scatter.
289
- let extinction = extinctionSum / SAMPLE_COUNT;
290
-
291
- textureStore(tLighting, voxel_coord, vec4<f32>(inScattering, extinction));
292
- }
293
- `, [
294
- chunk_cluster_depth_from_z_slice,
295
- chunk_cluster_depth_to_z_slice,
296
- chunk_texel_coordinate_to_uv,
297
- chunk_camera_ray_from_uv,
298
- chunk_uv_to_texel_coordinate,
299
-
300
- chunk_random_initialize,
301
- chunk_random,
302
-
303
- chunk_light_cluster_metadata_by_position,
304
- chunk_light_cluster_counts,
305
- chunk_directional_lights_iteration_mask,
306
- chunk_get_directional_light_info_by_index,
307
- chunk_get_point_light_info_by_index,
308
- chunk_get_spot_light_info_by_index,
309
- chunk_shadowmap_get_light_visibility,
310
- chunk_sky_lut_compute_uv,
311
-
312
- RAY_STRUCT.declaration_chunk,
313
- chunk_integrate_optical_depth,
314
- chunk_calculate_multiscattering,
315
- chunk_rgb_to_luminance,
316
- chunk_volumetrics_column_jitter,
317
- ]
318
- )
319
-
320
- export const shader_volumetrics_build_lighting = ComputeShader.from({
321
- label: "Volumetrics / build lighting",
322
- resources,
323
- body
324
- })
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
+ const resources = new ShaderResourceSetDescriptor();
47
+
48
+ const primary_group = resources.createGroup()
49
+ .addUniform("volumetrics_metadata", VOLUMETRICS_METADATA_STRUCT)
50
+ .addUniform("camera", CAMERA_UNIFORM_STRUCT)
51
+ .addUniform("view", VIEW_UNIFORM_STRUCT)
52
+ .addTexture("tSunTransmittance", "float", "2d")
53
+ .addTexture("tMultiScattering", "float", "2d")
54
+ .addSampler("sLinear", "filtering", SAMPLER_LINEAR_UPSCALE)
55
+ .addTexture("tSTBN_vec3", "unfilterable-float", "3d")
56
+ .addTexture("tParticipatingMedia0", "float", "3d")
57
+ .addTexture("tParticipatingMedia1", "float", "3d");
58
+
59
+ // Q-04: the sun's optical depth, pre-integrated along the light axis by
60
+ // shader_volumetrics_build_sun_shadow_volume, one cascade per texture and innermost first.
61
+ // Sampled, not marched.
62
+ for (let cascade = 0; cascade < VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT; cascade++) {
63
+ primary_group.addTexture(volumetrics_sun_shadow_texture_name(cascade), "float", "3d");
64
+ }
65
+
66
+ primary_group.addStorageTexture("tLighting", "rgba16float", "3d", "write-only")
67
+
68
+ // Lighting information
69
+ resources.createGroup()
70
+
71
+ .addStorageBuffer("light_data", WebGPUArray.u32)
72
+ // clusters
73
+ .addUniform("cluster_parameters", vec3f)
74
+ .addStorageBuffer("cluster_lookup", LIGHT_CLUSTER_LOOKUP_STRUCT)
75
+ .addStorageBuffer("cluster_data", WebGPUArray.u32)
76
+ // shadows
77
+ .addTexture("shadowmapAtlas", "depth")
78
+ .addSampler("sCompareLinear", "comparison", SHADOWMAP_COMPARE_SAMPLER_DESCRIPTOR)
79
+
80
+ const body = CodeChunk.from(
81
+ //language=WGSL
82
+ `
83
+ @compute @workgroup_size(16,16)
84
+ fn main(
85
+ @builtin(global_invocation_id) voxel_coord : vec3<u32>
86
+ ){
87
+ let resolution = textureDimensions(tParticipatingMedia0);
88
+
89
+ if(any(voxel_coord >= resolution)){
90
+ // overflow
91
+ return;
92
+ }
93
+
94
+ // Logical near/far, NOT the projection-derived range: the projection is infinite-far
95
+ // reverse-Z, so its far resolves to ~3.4e38 (FLOAT32_MAX). That value would leave the last
96
+ // froxel slice's z1 (and z1*z1*z1 below) at +Inf. clip_near_far is the bounded logical range,
97
+ // matching the participating-media build shader.
98
+ let near_far = camera.clip_near_far;
99
+
100
+ // same jitter valud for all voxels in Z column
101
+ // must be the same for PM and Lights
102
+ let z_jitter = volumetrics_column_jitter(voxel_coord.xy, view.frame_index);
103
+
104
+ let z_slice_0 = f32(voxel_coord.z ) + z_jitter;
105
+ let z_slice_1 = f32(voxel_coord.z + 1) + z_jitter;
106
+
107
+ let z0 = max(near_far.x, cluster_depth_from_z_slice( z_slice_0, volumetrics_metadata.froxel_cluster_parameters, f32(resolution.z) ));
108
+ let z1 = min(near_far.y, cluster_depth_from_z_slice( z_slice_1, volumetrics_metadata.froxel_cluster_parameters, f32(resolution.z) ));
109
+
110
+ // 2. Cubic mix gives uniform distribution in World Space Volume
111
+ let z3_min = z0 * z0 * z0;
112
+ let z3_max = z1 * z1 * z1;
113
+
114
+ var inScatteringSum = vec3<f32>(0.0);
115
+ var extinctionSum = 0.0;
116
+
117
+
118
+ // One sample per froxel. The 0.95-blend TAA downstream is a ~39-effective-sample temporal
119
+ // integrator; supersampling here as well spends N times this pass removing variance that filter
120
+ // removes anyway — and spends it at the Monte Carlo rate, because the samples were independent
121
+ // white noise and converged as 1/sqrt(N). Raising this is only worth it with the temporal
122
+ // filter off.
123
+ const SAMPLE_COUNT = 1u;
124
+
125
+ for(var i = 0u; i < SAMPLE_COUNT; i++){
126
+
127
+ // Spatiotemporal blue noise rather than a per-froxel hash. What lets 1 spp survive a
128
+ // temporal filter is each froxel seeing a well-distributed sequence over frames, which is
129
+ // STBN's defining property and exactly what white noise lacks — measured against the murmur
130
+ // hash it reaches the same error in roughly half the frames, across the whole window the
131
+ // 0.95 blend spans.
132
+ //
133
+ // The xy offset is load-bearing, not decoration. The aerial pass reads this same volume at
134
+ // (voxel_coord.xy, frame_index) and consumes all three channels, so sampling it there would
135
+ // not merely correlate the two passes' jitters — it would make them the same number. Two
136
+ // nuisance variables locked together stop covering the sample domain and the estimator
137
+ // converges to the wrong value, a bias that does NOT shrink with more frames, so no TAA
138
+ // window recovers it. Measured worst case |r| against any aerial channel: 1.0000 at zero
139
+ // offset, 0.0027 here.
140
+ //
141
+ // froxel z rides the temporal axis so froxels down a column do not all share one value —
142
+ // the fetch is indexed by column, and STBN's z axis is the one built to be well distributed.
143
+ const STBN_DECORRELATION_OFFSET = vec2<u32>(37u, 71u);
144
+
145
+ let noise = stbn_sample_vec3(vec3(
146
+ voxel_coord.xy + STBN_DECORRELATION_OFFSET,
147
+ (view.frame_index + voxel_coord.z) * SAMPLE_COUNT + i
148
+ ));
149
+
150
+ // [0..1] — offset (jitter - 0.5) must be centered on the froxel for every axis. The depth
151
+ // axis is stratified so that raising SAMPLE_COUNT converges faster than 1/sqrt(N); a no-op
152
+ // while it is 1.
153
+ let jitter = vec3(noise.xy, (f32(i) + noise.z) / f32(SAMPLE_COUNT));
154
+
155
+
156
+ // Interpolate the cubes, then take the cube root
157
+ let z3_sample = mix(z3_min, z3_max, jitter.z);
158
+ let view_depth = pow(z3_sample, 0.333333);
159
+
160
+ let sample_z_index:f32 = cluster_depth_to_z_slice(
161
+ view_depth,
162
+ volumetrics_metadata.froxel_cluster_parameters,
163
+ f32(resolution.z)
164
+ );
165
+
166
+ // 'saturate' prevents accidental out-of-bounds samples
167
+ let tile_uv = saturate(
168
+ texel_coordinate_to_uv(vec2f(voxel_coord.xy) + (jitter.xy - 0.5), resolution.xy)
169
+ );
170
+ let view_ray = camera_ray_from_uv(tile_uv, camera);
171
+
172
+ // The PM volumes are CELL-AVERAGED: texel k holds the slab [k+j, k+1+j], so its content
173
+ // sits at slice k + j + delta, not at slice k. Addressing them with the plain texel-centre
174
+ // convention is what the boundary-sampled aerial LUT wants, and reads the medium about half
175
+ // a froxel too far out. delta is a property of the depth curve — see the chunk.
176
+ let content_offset = volumetrics_froxel_content_offset(volumetrics_metadata.froxel_cluster_parameters.z);
177
+
178
+ let uvw = vec3<f32>(tile_uv, (sample_z_index + 0.5 - content_offset) / f32(resolution.z));
179
+
180
+ let participating_media0 = textureSampleLevel(tParticipatingMedia0, sLinear, uvw, 0.0);
181
+ let participating_media1 = textureSampleLevel(tParticipatingMedia1, sLinear, uvw, 0.0);
182
+
183
+ let participating_media_scattering = participating_media0.rgb;
184
+ let participating_media_extinction = participating_media1.rgb;
185
+ let participating_media_phase_g = participating_media1.a;
186
+
187
+ // Accumulate the (linear) mono extinction coefficient. Carried in tLighting.a below so the
188
+ // 3D TAA accumulates sigma_t alongside the in-scatter (Hillaire: temporally integrate the
189
+ // LINEAR extinction, never the non-linear transmittance).
190
+ extinctionSum += rgb_to_luminance(participating_media_extinction);
191
+
192
+ let view_pixel_coord = uv_to_texel_coordinate(tile_uv, vec2(view.width, view.height));
193
+
194
+ // view_depth is a PLANAR view-space depth and view_ray.direction is unit length, so the
195
+ // depth has to be converted to a distance along the ray before stepping. Multiplying them
196
+ // directly stops on the arc of constant distance instead of the plane of constant depth,
197
+ // short by cos(theta) — exact on the centre axis, ~72% of the intended depth in the corners
198
+ // of a 50 degree vertical FOV. Radial, which is why it reads as "the fog looks a bit off"
199
+ // rather than as an offset.
200
+ //
201
+ // Everything downstream assumes the planar reading: the light cluster is looked up at the
202
+ // planar depth, the shadow lookup happens at this position, and the TAA reconstructs froxel
203
+ // centres as planar slabs.
204
+ let position_ws = view_ray.origin
205
+ + view_ray.direction * (view_depth * camera_ray_planar_depth_scale(view_ray.direction, camera));
206
+
207
+ // Global lights (directional) — iterate over the first 32 slots
208
+ // of the directional light table (max enforced by GPULightCollection).
209
+ var directional_bits = directional_lights_iteration_mask(&light_data);
210
+ while (directional_bits != 0u) {
211
+ let directional_index = countTrailingZeros(directional_bits);
212
+ directional_bits &= ~(1u << directional_index);
213
+
214
+ let sun = get_directional_light_info_by_index(&light_data, directional_index);
215
+
216
+ let sun_lut_uv = sky_lut_compute_uv(position_ws, sun.direction);
217
+
218
+ let shadow_term = shadowmap_get_directional_light_visibility(
219
+ &light_data,
220
+ directional_index,
221
+ position_ws,
222
+ // toward-camera, matching the deferred cascade-blend convention. The consumer
223
+ // (shadowmap_csm_compute_cascade_blended) negates this internally; the deferred
224
+ // caller passes the surface->camera vector, whereas view_ray.direction is
225
+ // camera->scene, so it must be negated here to agree.
226
+ -view_ray.direction,
227
+ sun.direction
228
+ );
229
+
230
+ let u = dot(sun.direction, view_ray.direction);
231
+
232
+ // sample atmospheric transmittance
233
+ let transmittance_atmospheric = textureSampleLevel(
234
+ tSunTransmittance,
235
+ sLinear,
236
+ sun_lut_uv, 0.0
237
+ ).rgb;
238
+
239
+ // Q-04. Every froxel on this sun ray is asking for the same integral, one increment
240
+ // apart, and the march recomputes all of it from scratch for each: O(froxels * steps)
241
+ // to produce O(froxels) of information. The sun is where that is worst, because its
242
+ // tmax is the distance to the sun clipped to the frustum, so the march spans the whole
243
+ // grid rather than the few metres a lamp's does.
244
+ //
245
+ // So read the answer out of the cascades built once this frame with their axis on the
246
+ // sun, where the increment relation IS the storage — see
247
+ // shader_volumetrics_build_sun_shadow_volume. Same quantity, same units, one fetch.
248
+ //
249
+ // The march below is not an alternative to it, it is what is left for the lights the
250
+ // cascades do not cover: a second directional light, since there is one ladder and it
251
+ // is aligned to one direction, and the two cases that cannot place a box at all, which
252
+ // sun_shadow_light_index reports by holding a value no light index can equal.
253
+ var optical_depth: vec3<f32>;
254
+
255
+ if (directional_index == volumetrics_metadata.sun_shadow_light_index) {
256
+
257
+ optical_depth = volumetrics_sun_optical_depth(position_ws);
258
+
259
+ } else {
260
+
261
+ // .direction points TOWARD the light; march the optical-depth ray toward the light (+direction).
262
+ let shadow_ray = ${RAY_STRUCT.make({
263
+ origin: "position_ws",
264
+ direction: "sun.direction",
265
+ tmax: "sun.distance",
266
+ })};
267
+
268
+ optical_depth = integrate_optical_depth(shadow_ray);
269
+ }
270
+
271
+ let ms = calculate_multiscattering(
272
+ optical_depth,
273
+ participating_media_scattering,
274
+ participating_media_extinction,
275
+ u,
276
+ participating_media_phase_g,
277
+ volumetrics_metadata.ms_octave_count,
278
+ volumetrics_metadata.ms_extinction_decay
279
+ );
280
+
281
+ inScatteringSum += shadow_term * ms * transmittance_atmospheric * sun.color;
282
+ }
283
+
284
+ // Local lights
285
+ let cluster_metadata = light_cluster_metadata_by_position(
286
+ view_pixel_coord,
287
+ view_depth,
288
+ vec2( view.width, view.height )
289
+ );
290
+
291
+ let cluster_offset = cluster_metadata.offset;
292
+ let point_count = light_cluster_count_point(cluster_metadata.counts);
293
+ let spot_count = light_cluster_count_spot(cluster_metadata.counts);
294
+
295
+ for (var j = 0u; j < point_count; j++) {
296
+ let light_index = cluster_data[cluster_offset + j];
297
+ var direct_light = get_point_light_info_by_index(&light_data, light_index, position_ws);
298
+
299
+ if (all(direct_light.color == vec3(0.0))) {
300
+ continue;
301
+ }
302
+
303
+ let u = dot(direct_light.direction, view_ray.direction);
304
+
305
+ // .direction points TOWARD the light; march the optical-depth ray toward the light (+direction).
306
+ let shadow_ray = ${RAY_STRUCT.make({
307
+ origin: "position_ws",
308
+ direction: "direct_light.direction",
309
+ tmax: "direct_light.distance",
310
+ })};
311
+
312
+ let optical_depth = integrate_optical_depth(shadow_ray);
313
+
314
+ let ms = calculate_multiscattering(
315
+ optical_depth,
316
+ participating_media_scattering,
317
+ participating_media_extinction,
318
+ u,
319
+ participating_media_phase_g,
320
+ volumetrics_metadata.ms_octave_count,
321
+ volumetrics_metadata.ms_extinction_decay
322
+ );
323
+
324
+ // Before this, integrate_optical_depth was the ONLY thing occluding a local light:
325
+ // the medium shadowed itself and geometry did not shadow it at all, so a torch behind a
326
+ // wall lit the fog through the wall and a light through a window cast no shaft. The
327
+ // directional loop above has always multiplied its shadow map in; this is the same
328
+ // operation for the clustered lights, which is what Frostbite and UE both do in the
329
+ // froxel pass. One nearest atlas tap rather than the deferred path's filter kernel —
330
+ // see the chunk for why that is not a compromise here.
331
+ let shadow_term = volumetrics_point_light_shadow(&light_data, light_index, position_ws);
332
+
333
+ inScatteringSum += shadow_term * ms * direct_light.color;
334
+ }
335
+
336
+ for (var k = 0u; k < spot_count; k++) {
337
+ let light_index = cluster_data[cluster_offset + point_count + k];
338
+ var direct_light = get_spot_light_info_by_index(&light_data, light_index, position_ws);
339
+
340
+ if (all(direct_light.color == vec3(0.0))) {
341
+ continue;
342
+ }
343
+
344
+ let u = dot(direct_light.direction, view_ray.direction);
345
+
346
+ // .direction points TOWARD the light; march the optical-depth ray toward the light (+direction).
347
+ let shadow_ray = ${RAY_STRUCT.make({
348
+ origin: "position_ws",
349
+ direction: "direct_light.direction",
350
+ tmax: "direct_light.distance",
351
+ })};
352
+
353
+ let optical_depth = integrate_optical_depth(shadow_ray);
354
+
355
+ let ms = calculate_multiscattering(
356
+ optical_depth,
357
+ participating_media_scattering,
358
+ participating_media_extinction,
359
+ u,
360
+ participating_media_phase_g,
361
+ volumetrics_metadata.ms_octave_count,
362
+ volumetrics_metadata.ms_extinction_decay
363
+ );
364
+
365
+ // see the point loop above
366
+ let shadow_term = volumetrics_spot_light_shadow(&light_data, light_index, position_ws);
367
+
368
+ inScatteringSum += shadow_term * ms * direct_light.color;
369
+ }
370
+ }
371
+
372
+
373
+ var inScattering = inScatteringSum / f32(SAMPLE_COUNT);
374
+
375
+ // .a carries the mono extinction coefficient so the aerial pass can read a temporally-stable
376
+ // sigma_t (after the 3D TAA) instead of re-sampling the raw per-frame-jittered PM extinction.
377
+ // The composited fog transmittance is already mono (rgb_to_luminance of the integrated
378
+ // throughput), so collapsing extinction to luminance only drops the subtle chromatic
379
+ // depth-attenuation of the in-scatter.
380
+ let extinction = extinctionSum / f32(SAMPLE_COUNT);
381
+
382
+ textureStore(tLighting, voxel_coord, vec4<f32>(inScattering, extinction));
383
+ }
384
+ `, [
385
+ chunk_cluster_depth_from_z_slice,
386
+ chunk_cluster_depth_to_z_slice,
387
+ chunk_texel_coordinate_to_uv,
388
+ chunk_camera_ray_from_uv,
389
+ chunk_camera_ray_planar_depth_scale,
390
+ chunk_volumetrics_froxel_content_offset,
391
+ chunk_uv_to_texel_coordinate,
392
+
393
+ chunk_stbn_sample_vec3,
394
+
395
+ chunk_light_cluster_metadata_by_position,
396
+ chunk_light_cluster_counts,
397
+ chunk_directional_lights_iteration_mask,
398
+ chunk_get_directional_light_info_by_index,
399
+ chunk_get_point_light_info_by_index,
400
+ chunk_get_spot_light_info_by_index,
401
+ chunk_shadowmap_get_light_visibility,
402
+ chunk_volumetrics_local_light_shadow,
403
+ chunk_sky_lut_compute_uv,
404
+
405
+ RAY_STRUCT.declaration_chunk,
406
+ chunk_integrate_optical_depth,
407
+ chunk_volumetrics_sun_optical_depth,
408
+ chunk_calculate_multiscattering,
409
+ chunk_rgb_to_luminance,
410
+ chunk_volumetrics_column_jitter,
411
+ ]
412
+ )
413
+
414
+ export const shader_volumetrics_build_lighting = ComputeShader.from({
415
+ label: "Volumetrics / build lighting",
416
+ resources,
417
+ body
418
+ })