@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
@@ -0,0 +1,145 @@
1
+ import { LIGHT_DATABASE_SPEC } from "../light/LIGHT_DATABASE_SPEC.js";
2
+ import { LIGHT_FLAGS } from "../light/model/io/LIGHT_FLAGS.js";
3
+ import {
4
+ chunk_uv_octahedral_unit_encode
5
+ } from "../shader/chunk/geometry/octahedron/chunk_uv_octahedral_unit_encode.js";
6
+ import { chunk_ndc_to_uv } from "../shader/chunk/texture/chunk_ndc_to_uv.js";
7
+ import { CodeChunk } from "../shader/compiler/CodeChunk.js";
8
+
9
+ const POINT_DESCRIPTOR = LIGHT_DATABASE_SPEC.get('light_point');
10
+ const SPOT_DESCRIPTOR = LIGHT_DATABASE_SPEC.get('light_spot');
11
+ const SHADOW_POINT_DESCRIPTOR = LIGHT_DATABASE_SPEC.get('shadow_point');
12
+ const SHADOW_SPOT_DESCRIPTOR = LIGHT_DATABASE_SPEC.get('shadow_spot');
13
+
14
+ /**
15
+ * Shadow visibility for clustered point and spot lights, sampled the way a *volume* wants rather
16
+ * than the way a surface does: one nearest texel out of the shadow atlas, no filter kernel, no
17
+ * receiver bias.
18
+ *
19
+ * This exists alongside {@link chunk_shadowmap_get_light_visibility} rather than replacing it. The
20
+ * deferred path needs its filtering — a hard shadow edge on a surface is a visible artefact — and
21
+ * its cost is paid once per pixel. Here the cost is paid once per light per froxel, with up to 255
22
+ * point and 255 spot lights in a cluster, and the playground scene alone carries 130 shadow-casting
23
+ * point lights. The two callers want genuinely different things out of the same atlas, so they get
24
+ * two functions over it; the atlas, its tile rects and its projections are untouched.
25
+ *
26
+ * Three reasons the cheap tap is not a compromise here:
27
+ *
28
+ * - The value being sampled is already an average over a froxel slab several pixels across, it is
29
+ * jittered in xy and in depth every frame, and it then goes through a 3x3x3 Blackman-Harris
30
+ * temporal resample at a 0.95 blend. A PCF kernel would be filtering something about to be
31
+ * filtered three more times. Frostbite takes exactly this route for its froxel pass and accepts
32
+ * the aliasing; Wronski's alternative (a separate pre-filtered low-resolution ESM) needs its own
33
+ * target and build pass, and cannot be a downsample of THIS atlas because blurring moments
34
+ * bleeds across tile borders.
35
+ *
36
+ * - There is no receiver bias because there is no receiver. Normal-offset and slope-scaled depth
37
+ * bias exist to stop a surface shadowing itself; a medium cannot. Note that the deferred helpers
38
+ * reach the same answer when a volumetric caller passes the light direction in place of a normal:
39
+ * `get_shadow_offsets(N, L)` with `N == L` gives `cos_alpha = 1`, hence `sqrt(1 - 1) = 0` for the
40
+ * normal scale and `0` for the depth scale, so both offsets vanish. Dropping them here therefore
41
+ * does not change the sampled position relative to those helpers — only the tap count does.
42
+ *
43
+ * - `textureLoad` rather than `textureGatherCompare`: the comparison is one instruction either way,
44
+ * and the raw load carries no uniformity requirement, so it is legal in the middle of a
45
+ * non-uniform light loop without the gather dance `shadowmap_sample_filtered` needs.
46
+ *
47
+ * Both functions early-out to 1.0 on a light with no `CASTS_SHADOW_BIT`, so a non-shadowing light in
48
+ * the cluster stays free.
49
+ *
50
+ * Requires `shadowmapAtlas` in the calling shader's scope.
51
+ *
52
+ * @type {CodeChunk}
53
+ */
54
+ export const chunk_volumetrics_local_light_shadow = CodeChunk.from(
55
+ //language=WGSL
56
+ `
57
+ /**
58
+ * Nearest texel of an atlas tile, given a uv over that tile. Clamped to the tile so a uv that lands
59
+ * exactly on the far edge (or drifts a hair past it) cannot read a neighbouring light's slot.
60
+ */
61
+ fn volumetrics_shadow_atlas_texel(uv: vec2f, atlas_aabb: vec4f) -> vec2u {
62
+ let local = clamp(floor(uv * atlas_aabb.zw), vec2f(0.0), atlas_aabb.zw - 1.0);
63
+
64
+ return vec2u(local) + vec2u(atlas_aabb.xy);
65
+ }
66
+
67
+ fn volumetrics_point_light_shadow(
68
+ database: ptr<storage, array<u32>>,
69
+ point_index: u32,
70
+ position_ws: vec3f,
71
+ ) -> f32 {
72
+ let light = ${POINT_DESCRIPTOR.marshalling_method_read}(database, point_index);
73
+
74
+ if ((light.flags & ${LIGHT_FLAGS.CASTS_SHADOW_BIT}u) == 0u) {
75
+ return 1.0;
76
+ }
77
+
78
+ let atlas_aabb = ${SHADOW_POINT_DESCRIPTOR.marshalling_method_read}(database, light.shadow_id);
79
+
80
+ // the cube faces are captured FROM the light, so the octahedral encode takes light -> sample
81
+ let to_sample = position_ws - light.position;
82
+ let distance_to_center = length(to_sample);
83
+
84
+ let direction_light_to_sample = to_sample / max(distance_to_center, 1e-6);
85
+
86
+ // reverse-Z radial, matching what the point shadow pass stores: at the light -> 1, at or beyond
87
+ // the light's range -> 0.
88
+ let reference_depth = 1.0 - saturate(distance_to_center / max(light.distance, 1e-6));
89
+
90
+ let texel = volumetrics_shadow_atlas_texel(
91
+ uv_octahedral_unit_encode(direction_light_to_sample),
92
+ atlas_aabb
93
+ );
94
+
95
+ let stored = textureLoad(shadowmapAtlas, texel, 0);
96
+
97
+ // A larger stored value is NEARER the light, so an occluder in front of this froxel reads
98
+ // greater than the froxel's own radial depth. Cleared atlas regions hold 0 and therefore never
99
+ // occlude, which is the right default for a light whose map was not rendered this frame.
100
+ return select(1.0, 0.0, stored > reference_depth);
101
+ }
102
+
103
+ fn volumetrics_spot_light_shadow(
104
+ database: ptr<storage, array<u32>>,
105
+ spot_index: u32,
106
+ position_ws: vec3f,
107
+ ) -> f32 {
108
+ let light = ${SPOT_DESCRIPTOR.marshalling_method_read}(database, spot_index);
109
+
110
+ if ((light.flags & ${LIGHT_FLAGS.CASTS_SHADOW_BIT}u) == 0u) {
111
+ return 1.0;
112
+ }
113
+
114
+ let view_meta = ${SHADOW_SPOT_DESCRIPTOR.marshalling_method_read}(database, light.shadow_id);
115
+
116
+ // Spot shadows are a single perspective view, so the divide is load-bearing (unlike the ortho
117
+ // cascades, where w is always 1). No bias offset is added before the transform — see the header.
118
+ let clip_light = view_meta.projection * vec4(position_ws, 1.0);
119
+
120
+ if (clip_light.w <= 0.0) {
121
+ // behind the light's apex: outside the map entirely, and the cone attenuation has already
122
+ // zeroed this light's contribution, so the value is moot. Unshadowed is the safe answer.
123
+ return 1.0;
124
+ }
125
+
126
+ let pos_light = clip_light.xyz / clip_light.w;
127
+
128
+ let texel = volumetrics_shadow_atlas_texel(ndc_to_uv(pos_light.xy), view_meta.atlas);
129
+
130
+ let stored = textureLoad(shadowmapAtlas, texel, 0);
131
+
132
+ // SHADOWMAP_COMPARE_SAMPLER_DESCRIPTOR compares "greater" — visible where the receiver's depth
133
+ // exceeds the stored one, which under reverse-Z means nearer to the light. Reproduced here by
134
+ // hand because the raw load skips the comparison sampler.
135
+ return select(0.0, 1.0, pos_light.z > stored);
136
+ }
137
+ `, [
138
+ POINT_DESCRIPTOR.chunk_read,
139
+ SPOT_DESCRIPTOR.chunk_read,
140
+ SHADOW_POINT_DESCRIPTOR.chunk_read,
141
+ SHADOW_SPOT_DESCRIPTOR.chunk_read,
142
+ chunk_ndc_to_uv,
143
+ chunk_uv_octahedral_unit_encode,
144
+ ]
145
+ );
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Extinction of the participating medium at a world position, or zero where the froxel grid does
3
+ * not reach.
4
+ *
5
+ * The grid is camera-shaped, so "does not reach" means outside the view frustum or past its far
6
+ * face — which is the medium's real extent as far as any consumer here is concerned, and the reason
7
+ * the sun's optical depth still stops at the frustum (Q-05) after this change as it did before it.
8
+ *
9
+ * The depth guard is not redundant with the bounds test. Behind the camera `view_depth` goes
10
+ * negative, the depth curve takes `log2` of a negative and yields NaN, and every comparison against
11
+ * NaN is false — so `any(abs(uvw - 0.5) >= 0.5)` reports the sample as *inside* and the fetch reads
12
+ * whatever the clamp lands on. The optical-depth march never had to care because it starts inside
13
+ * the frustum and breaks at the first sample outside it; a pass that sweeps a box does.
14
+ *
15
+ * @see shader_volumetrics_build_sun_shadow_volume
16
+ * @type {CodeChunk}
17
+ */
18
+ export const chunk_volumetrics_sample_froxel_extinction: CodeChunk;
19
+ import { CodeChunk } from "../shader/compiler/CodeChunk.js";
20
+ //# sourceMappingURL=chunk_volumetrics_sample_froxel_extinction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk_volumetrics_sample_froxel_extinction.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;GAgBG;AACH,yDAFU,SAAS,CA4BjB;0BA9CwB,iCAAiC"}
@@ -0,0 +1,48 @@
1
+ import { CAMERA_UNIFORM_STRUCT } from "../camera/CAMERA_UNIFORM_STRUCT.js";
2
+ import { CodeChunk } from "../shader/compiler/CodeChunk.js";
3
+ import { volumetrics_position_world_to_froxel_uvw } from "./volumetrics_position_world_to_froxel_uvw.js";
4
+
5
+ /**
6
+ * Extinction of the participating medium at a world position, or zero where the froxel grid does
7
+ * not reach.
8
+ *
9
+ * The grid is camera-shaped, so "does not reach" means outside the view frustum or past its far
10
+ * face — which is the medium's real extent as far as any consumer here is concerned, and the reason
11
+ * the sun's optical depth still stops at the frustum (Q-05) after this change as it did before it.
12
+ *
13
+ * The depth guard is not redundant with the bounds test. Behind the camera `view_depth` goes
14
+ * negative, the depth curve takes `log2` of a negative and yields NaN, and every comparison against
15
+ * NaN is false — so `any(abs(uvw - 0.5) >= 0.5)` reports the sample as *inside* and the fetch reads
16
+ * whatever the clamp lands on. The optical-depth march never had to care because it starts inside
17
+ * the frustum and breaks at the first sample outside it; a pass that sweeps a box does.
18
+ *
19
+ * @see shader_volumetrics_build_sun_shadow_volume
20
+ * @type {CodeChunk}
21
+ */
22
+ export const chunk_volumetrics_sample_froxel_extinction = CodeChunk.from(
23
+ //language=WGSL
24
+ `
25
+ fn volumetrics_sample_froxel_extinction(position_ws: vec3<f32>) -> vec3<f32> {
26
+
27
+ let position_vs = camera.view_matrix * vec4<f32>(position_ws, 1.0);
28
+
29
+ // view space looks down -Z
30
+ let view_depth = -position_vs.z;
31
+
32
+ if (view_depth <= 0.0) {
33
+ return vec3<f32>(0.0);
34
+ }
35
+
36
+ let uvw = volumetrics_position_world_to_froxel_uvw(position_ws);
37
+
38
+ if (any(abs(uvw - 0.5) >= vec3<f32>(0.5))) {
39
+ return vec3<f32>(0.0);
40
+ }
41
+
42
+ return textureSampleLevel(tParticipatingMedia1, sLinear, uvw, 0.0).rgb;
43
+ }
44
+ `, [
45
+ CAMERA_UNIFORM_STRUCT.declaration_chunk,
46
+ volumetrics_position_world_to_froxel_uvw,
47
+ ]
48
+ );
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Name of the texture holding cascade `index` of the sun's optical-depth volume.
3
+ *
4
+ * Generated rather than written out because the cascade count is a constant and everything that
5
+ * touches the structure — this chunk, the lighting pass's resource set, the frame graph — has to
6
+ * agree with it. A name spelled by hand in one of those places is the way a fourth cascade turns
7
+ * into a silent black frame.
8
+ *
9
+ * @param {number} index
10
+ * @returns {string}
11
+ */
12
+ export function volumetrics_sun_shadow_texture_name(index: number): string;
13
+ /**
14
+ * Optical depth of the participating medium between a world position and the sun, in one fetch.
15
+ *
16
+ * Same quantity `integrate_optical_depth` returns for a directional light, and deliberately the
17
+ * same signature shape, so the two are interchangeable at the call site. What it does not expose is
18
+ * how the answer was produced: the caller passes a world position and gets an integral back, and
19
+ * whether that came from a light-aligned prefix scan, a Fourier basis or a march is not its
20
+ * business. Swapping the representation should not touch `shader_volumetrics_build_lighting`.
21
+ *
22
+ * Cascades are selected on **view depth**, not on containment in each box. The two agree — every
23
+ * box is sized from the bounding sphere of the sub-frustum out to that depth, so a froxel at or
24
+ * inside it is inside the box — and only the first gives a scalar to blend along. It also makes the
25
+ * branch uniform across a workgroup: the lighting pass dispatches one froxel slice per workgroup in
26
+ * z, and a slice is a surface of constant depth.
27
+ *
28
+ * The `saturate` in each lookup is a statement about the domain rather than a clamp that fires.
29
+ * Selection has already established the position is inside the cascade it reads; outside, the
30
+ * sensible reading of a clamped fetch is still the right one, since the outermost box's faces sit
31
+ * in empty space and the value there is the column's total.
32
+ *
33
+ * @see shader_volumetrics_build_sun_shadow_volume, build_volumetrics_sun_shadow_transform
34
+ * @type {CodeChunk}
35
+ */
36
+ export const chunk_volumetrics_sun_optical_depth: CodeChunk;
37
+ import { CodeChunk } from "../shader/compiler/CodeChunk.js";
38
+ //# sourceMappingURL=chunk_volumetrics_sun_optical_depth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk_volumetrics_sun_optical_depth.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.js"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AACH,2DAHW,MAAM,GACJ,MAAM,CAIlB;AAkDD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,kDAFU,SAAS,CAkBjB;0BAzGwB,iCAAiC"}
@@ -0,0 +1,107 @@
1
+ import { CAMERA_UNIFORM_STRUCT } from "../camera/CAMERA_UNIFORM_STRUCT.js";
2
+ import { CodeChunk } from "../shader/compiler/CodeChunk.js";
3
+ import { VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT } from "./VOLUMETRICS_SUN_SHADOW_RESOLUTION.js";
4
+
5
+ /**
6
+ * Name of the texture holding cascade `index` of the sun's optical-depth volume.
7
+ *
8
+ * Generated rather than written out because the cascade count is a constant and everything that
9
+ * touches the structure — this chunk, the lighting pass's resource set, the frame graph — has to
10
+ * agree with it. A name spelled by hand in one of those places is the way a fourth cascade turns
11
+ * into a silent black frame.
12
+ *
13
+ * @param {number} index
14
+ * @returns {string}
15
+ */
16
+ export function volumetrics_sun_shadow_texture_name(index) {
17
+ return `tSunShadowCascade${index}`;
18
+ }
19
+
20
+ /**
21
+ * Fraction of a cascade's range at which it starts handing over to the next one out.
22
+ *
23
+ * The two cascades do not agree where they meet — that is the entire point of having two, one is
24
+ * more accurate than the other — so a hard switch is a visible edge at a fixed view depth, which is
25
+ * to say a ring around the screen that the 0.95-blend froxel TAA would smear rather than remove.
26
+ * Cascaded shadow maps blend across a band for the same reason.
27
+ *
28
+ * @type {number}
29
+ */
30
+ const CASCADE_BLEND_START = 0.9;
31
+
32
+ function cascade_lookup(index) {
33
+ return `
34
+ fn volumetrics_sun_optical_depth_cascade_${index}(position_ws: vec3<f32>) -> vec3<f32> {
35
+
36
+ let uvw = (volumetrics_metadata.sun_shadow_ws_to_uvw[${index}] * vec4<f32>(position_ws, 1.0)).xyz;
37
+
38
+ return textureSampleLevel(${volumetrics_sun_shadow_texture_name(index)}, sLinear, saturate(uvw), 0.0).rgb;
39
+ }`;
40
+ }
41
+
42
+ function cascade_selection() {
43
+ const lines = [];
44
+
45
+ for (let index = 0; index < VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT - 1; index++) {
46
+ lines.push(`
47
+ {
48
+ let far = volumetrics_metadata.sun_shadow_cascade_far[${index}];
49
+ let blend_start = far * ${CASCADE_BLEND_START};
50
+
51
+ if (view_depth < blend_start) {
52
+ return volumetrics_sun_optical_depth_cascade_${index}(position_ws);
53
+ }
54
+
55
+ if (view_depth < far) {
56
+ return mix(
57
+ volumetrics_sun_optical_depth_cascade_${index}(position_ws),
58
+ volumetrics_sun_optical_depth_cascade_${index + 1}(position_ws),
59
+ (view_depth - blend_start) / (far - blend_start)
60
+ );
61
+ }
62
+ }`);
63
+ }
64
+
65
+ return lines.join("\n");
66
+ }
67
+
68
+ /**
69
+ * Optical depth of the participating medium between a world position and the sun, in one fetch.
70
+ *
71
+ * Same quantity `integrate_optical_depth` returns for a directional light, and deliberately the
72
+ * same signature shape, so the two are interchangeable at the call site. What it does not expose is
73
+ * how the answer was produced: the caller passes a world position and gets an integral back, and
74
+ * whether that came from a light-aligned prefix scan, a Fourier basis or a march is not its
75
+ * business. Swapping the representation should not touch `shader_volumetrics_build_lighting`.
76
+ *
77
+ * Cascades are selected on **view depth**, not on containment in each box. The two agree — every
78
+ * box is sized from the bounding sphere of the sub-frustum out to that depth, so a froxel at or
79
+ * inside it is inside the box — and only the first gives a scalar to blend along. It also makes the
80
+ * branch uniform across a workgroup: the lighting pass dispatches one froxel slice per workgroup in
81
+ * z, and a slice is a surface of constant depth.
82
+ *
83
+ * The `saturate` in each lookup is a statement about the domain rather than a clamp that fires.
84
+ * Selection has already established the position is inside the cascade it reads; outside, the
85
+ * sensible reading of a clamped fetch is still the right one, since the outermost box's faces sit
86
+ * in empty space and the value there is the column's total.
87
+ *
88
+ * @see shader_volumetrics_build_sun_shadow_volume, build_volumetrics_sun_shadow_transform
89
+ * @type {CodeChunk}
90
+ */
91
+ export const chunk_volumetrics_sun_optical_depth = CodeChunk.from(
92
+ //language=WGSL
93
+ `
94
+ ${Array.from({ length: VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT }, (_, index) => cascade_lookup(index)).join("\n")}
95
+
96
+ fn volumetrics_sun_optical_depth(position_ws: vec3<f32>) -> vec3<f32> {
97
+
98
+ // view space looks down -Z
99
+ let view_depth = -(camera.view_matrix * vec4<f32>(position_ws, 1.0)).z;
100
+ ${cascade_selection()}
101
+
102
+ return volumetrics_sun_optical_depth_cascade_${VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT - 1}(position_ws);
103
+ }
104
+ `, [
105
+ CAMERA_UNIFORM_STRUCT.declaration_chunk,
106
+ ]
107
+ );
@@ -9,12 +9,15 @@
9
9
  * @param {number} light_cluster_parameters
10
10
  * @param {number} shadowmap_atlas
11
11
  * @param {number[]} volumetric_resolution
12
+ * @param {number[]} sun_shadow_resolution voxel counts of one Q-04 sun optical-depth cascade;
13
+ * `[1, 1, 1]` when the froxels are marching instead, which keeps the passes in the graph without
14
+ * doing any work
12
15
  * @param {number} volumetrics_metadata
13
16
  * @param {number} [taa_history] imported previous-frame accumulated scattering volume
14
17
  * @param {number} [taa_output] imported destination for this frame's accumulated scattering volume
15
18
  * @param {GPUCameraContext} [camera_previous] previous-frame camera, for temporal reprojection
16
19
  */
17
- export function graph_build_volumetrics({ graph, camera, lights, view, light_cluster_data, light_cluster_lookup, light_cluster_parameters, shadowmap_atlas, volumetric_resolution, volumetrics_metadata, taa_history, taa_output, camera_previous, }: FrameGraph): {
20
+ export function graph_build_volumetrics({ graph, camera, lights, view, light_cluster_data, light_cluster_lookup, light_cluster_parameters, shadowmap_atlas, volumetric_resolution, sun_shadow_resolution, volumetrics_metadata, taa_history, taa_output, camera_previous, }: FrameGraph): {
18
21
  lut_transmittance: any;
19
22
  lut_multiple_scattering: any;
20
23
  lut_aerial: any;
@@ -1 +1 @@
1
- {"version":3,"file":"graph_build_volumetrics.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/graph_build_volumetrics.js"],"names":[],"mappings":"AAuSA;;;;;;;;;;;;;;;GAeG;AACH;;;;;EA0JC"}
1
+ {"version":3,"file":"graph_build_volumetrics.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/graph_build_volumetrics.js"],"names":[],"mappings":"AAyYA;;;;;;;;;;;;;;;;;;GAkBG;AACH;;;;;EAqKC"}