@woosh/meep-engine 3.11.1 → 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 (73) 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 +104 -1
  15. package/src/shade/playground/volumetrics_froxel/index.html +20 -1
  16. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
  17. package/src/shade/playground/volumetrics_froxel/main.js +298 -0
  18. package/src/shade/playground/volumetrics_froxel/measure.d.ts +119 -0
  19. package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -0
  20. package/src/shade/playground/volumetrics_froxel/measure.js +559 -0
  21. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  22. package/src/shade/renderer/Renderer.js +22 -18
  23. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  24. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts +14 -13
  25. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts.map +1 -1
  26. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.js +133 -129
  27. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  28. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +480 -315
  29. package/src/shade/renderer/volumetrics/NOTES.md +649 -8
  30. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts +5 -0
  31. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts.map +1 -1
  32. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.js +21 -2
  33. package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts +26 -0
  34. package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts.map +1 -1
  35. package/src/shade/renderer/volumetrics/SceneVolumetrics.js +64 -21
  36. package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.d.ts.map +1 -1
  37. package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.js +84 -28
  38. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts +98 -0
  39. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts.map +1 -0
  40. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.js +111 -0
  41. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts +84 -0
  42. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts.map +1 -0
  43. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.js +252 -0
  44. package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js +75 -75
  45. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts +43 -0
  46. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts.map +1 -0
  47. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.js +145 -0
  48. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts +20 -0
  49. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts.map +1 -0
  50. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.js +48 -0
  51. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts +38 -0
  52. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts.map +1 -0
  53. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.js +107 -0
  54. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +4 -1
  55. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  56. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +577 -471
  57. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
  58. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +64 -13
  59. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts +8 -0
  60. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts.map +1 -0
  61. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.js +174 -0
  62. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts +25 -0
  63. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts.map +1 -1
  64. package/src/shade/wgsl/emulator/CPUBitmapData.js +33 -0
  65. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2221 -2215
  66. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  67. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +1297 -1117
  68. package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts +69 -0
  69. package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts.map +1 -0
  70. package/src/shade/wgsl/emulator/cpu_texture_gather.js +148 -0
  71. package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts +16 -0
  72. package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts.map +1 -1
  73. package/src/shade/wgsl/emulator/cpu_texture_sample.js +176 -172
@@ -39,6 +39,11 @@ export class VolumetricsParticleSpec {
39
39
  radius: any;
40
40
  g?: number;
41
41
  }): void;
42
+ /**
43
+ * @param {VolumetricsParticleSpec} other
44
+ * @returns {boolean}
45
+ */
46
+ equals(other: VolumetricsParticleSpec): boolean;
42
47
  }
43
48
  export const VOLUMETRIC_PARTICLE_SPEC_FOG: Readonly<VolumetricsParticleSpec>;
44
49
  export class ParticipatingMediaVolume {
@@ -1 +1 @@
1
- {"version":3,"file":"ParticipatingMediaVolume.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/ParticipatingMediaVolume.js"],"names":[],"mappings":"AAIA;IA2DI;;;;OAIG;IACH,+BAHW;QAAC,wBAAwB,CAAC;QAAC,wBAAwB,CAAC;QAAC,MAAM,CAAC;QAAC,CAAC,CAAA;KAAC,GAC9D,uBAAuB,CAalC;IAED,oDAIC;IAhFD;;;OAGG;IACH,YAFU,MAAM,EAAE,CAEK;IAEvB;;;OAGG;IACH,YAFU,MAAM,EAAE,CAEK;IAEvB;;;OAGG;IACH,QAFU,MAAM,CAEL;IAEX,UAAQ;IAER,iCAIC;IAED;;;OAGG;IACH,YAFW,uBAAuB,QAOjC;IAED;;;;;aAmBC;CAyBJ;AAED,6EAKI;AAIJ;IAII,iBAEC;IAED;;;OAGG;IACH,aAFU,MAAM,EAAE,CAEM;IACxB,0BAA4B;IAC5B,uBAAyB;IAEzB;;;OAGG;IACH,gBAFU,MAAM,EAAE,CAES;IAE3B;;;OAGG;IACH,uBAFU,MAAM,EAAE,CAEgB;IAElC;;;OAGG;IACH,MAFU,MAAM,CAEP;IAET;;;OAGG;IACH,gBAAc;IAEd;;;;;;;;;OASG;IACH,mCAEC;IAED,uCAA+E;IAE/E;;;;;;OAMG;IACH,eAFU,MAAM,CAEI;IAEpB;;;;OAIG;IACH,WAFU,SAAS,CAES;;CAE/B;0BAvKyB,4CAA4C"}
1
+ {"version":3,"file":"ParticipatingMediaVolume.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/ParticipatingMediaVolume.js"],"names":[],"mappings":"AAKA;IA6EI;;;;OAIG;IACH,+BAHW;QAAC,wBAAwB,CAAC;QAAC,wBAAwB,CAAC;QAAC,MAAM,CAAC;QAAC,CAAC,CAAA;KAAC,GAC9D,uBAAuB,CAalC;IAED,oDAIC;IAlGD;;;OAGG;IACH,YAFU,MAAM,EAAE,CAEK;IAEvB;;;OAGG;IACH,YAFU,MAAM,EAAE,CAEK;IAEvB;;;OAGG;IACH,QAFU,MAAM,CAEL;IAEX,UAAQ;IAER,iCAIC;IAED;;;OAGG;IACH,YAFW,uBAAuB,QAOjC;IAED;;;;;aA0BC;IAED;;;OAGG;IACH,cAHW,uBAAuB,GACrB,OAAO,CAOnB;CAyBJ;AAED,6EAKI;AAIJ;IAII,iBAEC;IAED;;;OAGG;IACH,aAFU,MAAM,EAAE,CAEM;IACxB,0BAA4B;IAC5B,uBAAyB;IAEzB;;;OAGG;IACH,gBAFU,MAAM,EAAE,CAES;IAE3B;;;OAGG;IACH,uBAFU,MAAM,EAAE,CAEgB;IAElC;;;OAGG;IACH,MAFU,MAAM,CAEP;IAET;;;OAGG;IACH,gBAAc;IAEd;;;;;;;;;OASG;IACH,mCAEC;IAED,uCAA+E;IAE/E;;;;;;OAMG;IACH,eAFU,MAAM,CAEI;IAEpB;;;;OAIG;IACH,WAFU,SAAS,CAES;;CAE/B;0BAzLyB,4CAA4C"}
@@ -1,4 +1,5 @@
1
1
  import { assert } from "../../../core/assert.js";
2
+ import { isArrayEqualStrict } from "../../../core/collection/array/isArrayEqualStrict.js";
2
3
  import { rgb_to_luminance } from "../../../core/color/rgb_to_luminance.js";
3
4
  import { Transform } from "../../../engine/ecs/transform/Transform.js";
4
5
 
@@ -55,12 +56,30 @@ export class VolumetricsParticleSpec {
55
56
  assert.isNumber(g, 'g');
56
57
  assert.isFinite(g, 'g must be finite');
57
58
 
58
- this.extinction = extinction;
59
- this.scattering = scattering;
59
+ /*
60
+ Copied rather than referenced: the sources these arrive from are shared constants —
61
+ `VOLUMETRIC_PARTICLE_SPEC_FOG` and the generated `MIE_PARTICLES_STANDARD_PRECOMPUTED`
62
+ table — and `Object.freeze` on the object around them does not freeze the arrays inside.
63
+ Holding onto them would make every spec built from the same entry the same spec, so
64
+ tuning one volume would retune every other volume sharing its particle.
65
+ */
66
+ this.extinction = extinction.slice();
67
+ this.scattering = scattering.slice();
60
68
  this.radius = radius;
61
69
  this.g = g;
62
70
  }
63
71
 
72
+ /**
73
+ * @param {VolumetricsParticleSpec} other
74
+ * @returns {boolean}
75
+ */
76
+ equals(other) {
77
+ return this.radius === other.radius
78
+ && this.g === other.g
79
+ && isArrayEqualStrict(this.extinction, other.extinction)
80
+ && isArrayEqualStrict(this.scattering, other.scattering);
81
+ }
82
+
64
83
  /**
65
84
  *
66
85
  * @param {{cross_section_extinction, cross_section_scattering, radius, g}} particle_spec
@@ -1,3 +1,15 @@
1
+ /**
2
+ * Every participating medium in a scene, and a counter saying whether the GPU's copy is stale.
3
+ *
4
+ * **The version is the whole synchronisation protocol.** {@link GPUSceneVolumetrics} repacks the
5
+ * entire table whenever the number moves and does nothing at all when it does not, so a volume
6
+ * changed without moving the counter renders with the values it was last uploaded with — the CPU
7
+ * object holds the new density, reads back correctly, and only the picture is stale. Anything that
8
+ * writes into a volume in place therefore has to say so; see {@link invalidate}.
9
+ *
10
+ * **Volumes are packed by array index**, so removing one renumbers the rest. That is free here
11
+ * because the upload is all-or-nothing, and it is the reason no volume carries a slot number.
12
+ */
1
13
  export class SceneVolumetrics {
2
14
  /**
3
15
  *
@@ -10,5 +22,19 @@ export class SceneVolumetrics {
10
22
  * @param {ParticipatingMediaVolume} volume
11
23
  */
12
24
  add(volume: ParticipatingMediaVolume): void;
25
+ /**
26
+ * Take a volume back out.
27
+ *
28
+ * @param {ParticipatingMediaVolume} volume
29
+ * @returns {boolean} true if it was there to be removed
30
+ */
31
+ remove(volume: ParticipatingMediaVolume): boolean;
32
+ /**
33
+ * Say that a volume already in the scene has been written into.
34
+ *
35
+ * Adding and removing move the version by themselves; changing a volume in place is invisible
36
+ * from here, and this is how such a change reaches the GPU.
37
+ */
38
+ invalidate(): void;
13
39
  }
14
40
  //# sourceMappingURL=SceneVolumetrics.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SceneVolumetrics.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/SceneVolumetrics.js"],"names":[],"mappings":"AAAA;IACI;;;OAGG;IACH,SAFU,0BAA0B,CAEvB;IAEb,gBAAY;IAEZ;;;OAGG;IACH,4CAMC;CACJ"}
1
+ {"version":3,"file":"SceneVolumetrics.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/SceneVolumetrics.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH;IACI;;;OAGG;IACH,SAFU,0BAA0B,CAEvB;IAEb,gBAAY;IAEZ;;;OAGG;IACH,4CAMC;IAED;;;;;OAKG;IACH,0CAFa,OAAO,CAenB;IAED;;;;;OAKG;IACH,mBAEC;CACJ"}
@@ -1,21 +1,64 @@
1
- export class SceneVolumetrics {
2
- /**
3
- *
4
- * @type {ParticipatingMediaVolume[]}
5
- */
6
- volumes = [];
7
-
8
- version = 0;
9
-
10
- /**
11
- *
12
- * @param {ParticipatingMediaVolume} volume
13
- */
14
- add(volume) {
15
-
16
- this.volumes.push(volume);
17
-
18
- this.version++;
19
-
20
- }
21
- }
1
+ /**
2
+ * Every participating medium in a scene, and a counter saying whether the GPU's copy is stale.
3
+ *
4
+ * **The version is the whole synchronisation protocol.** {@link GPUSceneVolumetrics} repacks the
5
+ * entire table whenever the number moves and does nothing at all when it does not, so a volume
6
+ * changed without moving the counter renders with the values it was last uploaded with — the CPU
7
+ * object holds the new density, reads back correctly, and only the picture is stale. Anything that
8
+ * writes into a volume in place therefore has to say so; see {@link invalidate}.
9
+ *
10
+ * **Volumes are packed by array index**, so removing one renumbers the rest. That is free here
11
+ * because the upload is all-or-nothing, and it is the reason no volume carries a slot number.
12
+ */
13
+ export class SceneVolumetrics {
14
+ /**
15
+ *
16
+ * @type {ParticipatingMediaVolume[]}
17
+ */
18
+ volumes = [];
19
+
20
+ version = 0;
21
+
22
+ /**
23
+ *
24
+ * @param {ParticipatingMediaVolume} volume
25
+ */
26
+ add(volume) {
27
+
28
+ this.volumes.push(volume);
29
+
30
+ this.version++;
31
+
32
+ }
33
+
34
+ /**
35
+ * Take a volume back out.
36
+ *
37
+ * @param {ParticipatingMediaVolume} volume
38
+ * @returns {boolean} true if it was there to be removed
39
+ */
40
+ remove(volume) {
41
+
42
+ const index = this.volumes.indexOf(volume);
43
+
44
+ if (index === -1) {
45
+ return false;
46
+ }
47
+
48
+ this.volumes.splice(index, 1);
49
+
50
+ this.version++;
51
+
52
+ return true;
53
+ }
54
+
55
+ /**
56
+ * Say that a volume already in the scene has been written into.
57
+ *
58
+ * Adding and removing move the version by themselves; changing a volume in place is invisible
59
+ * from here, and this is how such a change reaches the GPU.
60
+ */
61
+ invalidate() {
62
+ this.version++;
63
+ }
64
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"VOLUMETRICS_METADATA_STRUCT.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.js"],"names":[],"mappings":"AAIA,uDAwBG;6BA1B0B,gCAAgC"}
1
+ {"version":3,"file":"VOLUMETRICS_METADATA_STRUCT.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.js"],"names":[],"mappings":"AAKA,uDA+EG;6BAlF0B,gCAAgC"}
@@ -1,29 +1,85 @@
1
- import { f32, u32, vec3f, vec3u, vec4f } from "../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
2
- import { WebGPUArray } from "../shader/type/WebGPUArray.js";
3
- import { WebGPUStruct } from "../shader/type/WebGPUStruct.js";
4
-
5
- export const VOLUMETRICS_METADATA_STRUCT = WebGPUStruct.from({
6
- resolution: vec3u,
7
- // LUT / composite depth curve — covers the full logical range up to camera.far.
8
- cluster_parameters: vec3f,
9
- // Froxel-grid depth curve — covers [near, froxel_far]. Equals cluster_parameters when the grid
10
- // spans the whole range (the no-op default). PM/lighting/TAA build the grid on THIS curve; the
11
- // aerial pass uses it to map a LUT depth back to the grid slice it should sample.
12
- froxel_cluster_parameters: vec3f,
13
- volume_count: u32,
14
- // World-space frustum planes with a FINITE far plane. The camera's own projection is
15
- // infinite-far reverse-Z (its frustum far plane is zeroed), which frustum_slice can't slice;
16
- // this is rebuilt at the logical far for the participating-media coverage test.
17
- frustum: WebGPUArray.from(vec4f, 6),
18
- // 1 when temporal-reprojection history is invalid (first frame, resolution change, camera
19
- // teleport): the volumetric TAA then outputs the fresh sample instead of blending stale history.
20
- history_reset: u32,
21
- // View-space depth at which the froxel grid ends. The aerial pass samples the grid where
22
- // depth <= froxel_far and ray-marches the scene volumes directly beyond it (RDR2-style far
23
- // field, growing log steps). Defaults to camera.far -> the whole range is grid-sampled (no-op).
24
- froxel_far: f32,
25
- // Multiscatter tuning (calculate_multiscattering). Fewer octaves / higher decay -> less
26
- // shadow fill -> stronger lit-rim-vs-core contrast (silver lining). Engine defaults: 6 / 0.5.
27
- ms_octave_count: u32,
28
- ms_extinction_decay: f32,
1
+ import { f32, mat4x4f, u32, vec3f, vec3u, vec4f } from "../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
2
+ import { WebGPUArray } from "../shader/type/WebGPUArray.js";
3
+ import { WebGPUStruct } from "../shader/type/WebGPUStruct.js";
4
+ import { VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT } from "./VOLUMETRICS_SUN_SHADOW_RESOLUTION.js";
5
+
6
+ export const VOLUMETRICS_METADATA_STRUCT = WebGPUStruct.from({
7
+ resolution: vec3u,
8
+ // LUT / composite depth curve — covers the full logical range up to camera.far.
9
+ cluster_parameters: vec3f,
10
+ // Froxel-grid depth curve covers [near, froxel_far]. Equals cluster_parameters when the grid
11
+ // spans the whole range (the no-op default). PM/lighting/TAA build the grid on THIS curve; the
12
+ // aerial pass uses it to map a LUT depth back to the grid slice it should sample.
13
+ froxel_cluster_parameters: vec3f,
14
+ volume_count: u32,
15
+ // World-space frustum planes with a FINITE far plane. The camera's own projection is
16
+ // infinite-far reverse-Z (its frustum far plane is zeroed), which frustum_slice can't slice;
17
+ // this is rebuilt at the logical far for the participating-media coverage test.
18
+ frustum: WebGPUArray.from(vec4f, 6),
19
+ // 1 when temporal-reprojection history is invalid (first frame, resolution change, camera
20
+ // teleport): the volumetric TAA then outputs the fresh sample instead of blending stale history.
21
+ history_reset: u32,
22
+ // View-space depth at which the froxel grid ends. The aerial pass samples the grid where
23
+ // depth <= froxel_far and ray-marches the scene volumes directly beyond it (RDR2-style far
24
+ // field, growing log steps). Defaults to camera.far -> the whole range is grid-sampled (no-op).
25
+ froxel_far: f32,
26
+ // Multiscatter tuning (calculate_multiscattering). Fewer octaves / higher decay -> less
27
+ // shadow fill -> stronger lit-rim-vs-core contrast (silver lining). Engine defaults: 6 / 0.5.
28
+ ms_octave_count: u32,
29
+ ms_extinction_decay: f32,
30
+
31
+ /**
32
+ * Q-04. World position -> [0,1]^3 inside one cascade of the sun's optical-depth volume, and its
33
+ * inverse, innermost cascade first.
34
+ *
35
+ * Each volume's third axis is the direction light travels, so a column of voxels is one sun ray
36
+ * and the value stored along it is the running integral of extinction — read back in a single
37
+ * fetch by `chunk_volumetrics_sun_optical_depth`, where the shipping path marches 16 steps per
38
+ * froxel. `build_volumetrics_sun_shadow_transform` places each box and explains why it is
39
+ * placed that way; the inverse is what `shader_volumetrics_build_sun_shadow_volume` walks.
40
+ */
41
+ sun_shadow_ws_to_uvw: WebGPUArray.from(mat4x4f, VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT),
42
+ sun_shadow_uvw_to_ws: WebGPUArray.from(mat4x4f, VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT),
43
+
44
+ /**
45
+ * World-space length of one voxel along the sun axis, per cascade — the scan's `dt`. Constant
46
+ * for the frame, because each box's extent comes from the projection rather than from where the
47
+ * camera happens to be pointing.
48
+ *
49
+ * A `vec4f` rather than an array because WGSL's uniform address space rounds an `array<f32, N>`
50
+ * element up to 16 bytes, so the packed vector is the same four values in a quarter of the
51
+ * space and with no stride to get wrong. It is also the reason the cascade count is capped at
52
+ * four.
53
+ */
54
+ sun_shadow_step_ws: vec4f,
55
+
56
+ /**
57
+ * View depth each cascade's box covers out to, innermost first — what the read side selects on.
58
+ *
59
+ * Selection is by depth rather than by testing containment in each box because the two agree
60
+ * (every box contains the sub-frustum out to this depth, by construction) and only the first
61
+ * gives a scalar to blend across. Unused lanes hold the outermost cascade's far, so a read that
62
+ * runs off the end of the ladder still lands on a real cascade.
63
+ */
64
+ sun_shadow_cascade_far: vec4f,
65
+
66
+ /**
67
+ * Q-04. Which slot of the `light_directional` table the cascades were built for, or
68
+ * `VOLUMETRICS_SUN_SHADOW_LIGHT_NONE` when they were not built at all.
69
+ *
70
+ * The lighting pass tests its loop index against this rather than assuming zero, so a second
71
+ * directional light degrades to the march instead of being shadowed by the first one's
72
+ * cascades — there is one ladder and it is aligned to one direction. The sentinel matches no
73
+ * index, so it degrades *every* directional light the same way, which is what the two cases
74
+ * that cannot place a box need: a scene with no directional light, and a camera with no field
75
+ * of view to derive a bounding sphere from.
76
+ *
77
+ * This carried a `sun_shadow_mode` word beside it while Q-04 was an A/B. That word is gone: the
78
+ * cascades are the only path for the sun now, and "not built this frame" is a property of the
79
+ * light rather than a mode to select.
80
+ *
81
+ * Point and spot lights always march. Their `tmax` is the distance to the light, so those rays
82
+ * are short and local and there is far less to amortize.
83
+ */
84
+ sun_shadow_light_index: u32,
29
85
  });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Sub-steps the build pass takes across one voxel of cascade `index`, along the sun.
3
+ *
4
+ * A voxel has to hold the medium's integral across its slab, and the rate that integral is sampled
5
+ * at is a separate knob from the rate it is stored at — only the first has to beat the medium's
6
+ * features. The outermost cascade has the largest voxel and needs the most; each cascade in from it
7
+ * has a voxel `RATIO` times smaller, so the same world-space sample spacing costs `RATIO` times
8
+ * fewer sub-steps. Holding the spacing constant across cascades rather than the count is what keeps
9
+ * the innermost cascade from spending its budget resolving a millimetre.
10
+ *
11
+ * **Always even, and never fewer than two.** The build stores its running integral when it reaches
12
+ * sub-step `count / 2`, which is the voxel's centre only when the halves are equal; an odd count
13
+ * stores the integral to `floor(count/2)/count` of the slab, and one sub-step stores it to the near
14
+ * face — a half-slab bias in the direction that makes the medium shadow too little, on the cascade
15
+ * that matters most.
16
+ *
17
+ * @param {number} index
18
+ * @returns {number}
19
+ */
20
+ export function volumetrics_sun_shadow_sub_steps(index: number): number;
21
+ /**
22
+ * Voxel counts of one cascade of the sun optical-depth volume, `[x, y, z]`, with `z` along the sun.
23
+ *
24
+ * Cubic, and that is a measured decision rather than a default. It is tempting to make the grid
25
+ * lateral-heavy: at the playground's 60 degree field of view the medium there is a horizontal slab
26
+ * three units thick crossed by a low sun, so its structure perpendicular to the sun is far finer
27
+ * than its structure along it, and as a single box `[256, 256, 32]` beats `[128, 128, 128]` by an
28
+ * order of magnitude at identical memory. That advantage belongs to *that* fog and *that* sun
29
+ * elevation. Against a converged reference at one viewpoint, as a single box:
30
+ *
31
+ * | field of view | `[128,128,128]` | `[256,256,32]` |
32
+ * |---|---|---|
33
+ * | 40 deg | +6.4 | +28.1 |
34
+ * | 60 deg | +37.7 | +2.5 |
35
+ * | 90 deg | +1.4 | +39.0 |
36
+ * | 110 deg | +2.1 | +39.5 |
37
+ *
38
+ * Two axes, two Nyquist limits, and which one is starved depends on the camera. Cubic is the shape
39
+ * that does not assume. 96 puts one cascade at 7.1 MB, three at 21.2 MB.
40
+ *
41
+ * @type {number[]}
42
+ */
43
+ export const VOLUMETRICS_SUN_SHADOW_RESOLUTION: number[];
44
+ /**
45
+ * How many cascades the structure carries.
46
+ *
47
+ * A single world-uniform box cannot be sized correctly, and that is arithmetic rather than tuning.
48
+ * The box has to contain the camera frustum under any rotation, so its side is proportional to
49
+ * `far` and to `tan(fov/2)`; the medium's features — a three-unit fog slab — are absolute. So the
50
+ * ratio between what the structure can resolve and what it has to resolve is a free parameter of
51
+ * the *camera*, and the table above is what that looks like: every single-box configuration
52
+ * measured has a field of view at which it is 28 to 50 mean luminance from the truth, and which
53
+ * one moves with the resolution. Three cascades of 96 are within 1.8 across the same range.
54
+ *
55
+ * Buying the near field's resolution uniformly instead is not an option: 0.28 unit voxels over a
56
+ * 283 unit box is 1024 per axis, i.e. 268 MB. Cascades buy it where the froxels are, which on a
57
+ * log-distributed grid over [0.1, 120] is two thirds of the slices inside 15 units — the innermost
58
+ * box here is 33 units across, so its voxel is 0.34 units for 7.1 MB.
59
+ *
60
+ * Three is what Frostbite ships for its extinction clipmap and what Enshrouded ships for this exact
61
+ * structure. Four is a constant away; the read side, the metadata and the build shaders are all
62
+ * generated from this.
63
+ *
64
+ * @type {number}
65
+ */
66
+ export const VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT: number;
67
+ /**
68
+ * Ratio between one cascade's far distance and the next one in.
69
+ *
70
+ * `far_i = far / RATIO^(count - 1 - i)`, the logarithmic split, so every cascade has the same
71
+ * voxel-size ratio to its neighbour and the *worst* case within a cascade — its near end — is a
72
+ * fixed factor. Three cascades at ratio 3 cover a 120 unit far plane as 13.3 / 40 / 120, giving
73
+ * 0.34 / 1.02 / 3.07 unit voxels at 96 per axis.
74
+ *
75
+ * Bounded by the metadata: the per-cascade scalars are packed into `vec4f`, so no more than four.
76
+ *
77
+ * @type {number}
78
+ */
79
+ export const VOLUMETRICS_SUN_SHADOW_CASCADE_RATIO: number;
80
+ /**
81
+ * `sun_shadow_light_index` when no cascades were placed this frame, so no directional light matches
82
+ * it and every one of them falls back to the per-froxel march.
83
+ *
84
+ * Two situations reach it and neither is a switch: a scene with no directional light, and a camera
85
+ * with no field of view for {@link frustum_bounding_sphere} to work from. The build passes still
86
+ * run — collapsed to one voxel — because the lighting pass binds their outputs unconditionally and
87
+ * WebGPU has no optional bindings.
88
+ *
89
+ * @type {number}
90
+ */
91
+ export const VOLUMETRICS_SUN_SHADOW_LIGHT_NONE: number;
92
+ /**
93
+ * Workgroup shape of the build pass. One invocation owns a whole column along the sun axis, so
94
+ * this is 2D and covers the volume's x/y.
95
+ * @type {number[]}
96
+ */
97
+ export const VOLUMETRICS_SUN_SHADOW_WORKGROUP_RESOLUTION: number[];
98
+ //# sourceMappingURL=VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.js"],"names":[],"mappings":"AA8DA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wDAHW,MAAM,GACJ,MAAM,CAWlB;AA1FD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,gDAFU,MAAM,EAAE,CAE4C;AAE9D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,mDAFU,MAAM,CAEsC;AAEtD;;;;;;;;;;;GAWG;AACH,mDAFU,MAAM,CAEsC;AAgCtD;;;;;;;;;;GAUG;AACH,gDAFU,MAAM,CAE4C;AAE5D;;;;GAIG;AACH,0DAFU,MAAM,EAAE,CAEgD"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Voxel counts of one cascade of the sun optical-depth volume, `[x, y, z]`, with `z` along the sun.
3
+ *
4
+ * Cubic, and that is a measured decision rather than a default. It is tempting to make the grid
5
+ * lateral-heavy: at the playground's 60 degree field of view the medium there is a horizontal slab
6
+ * three units thick crossed by a low sun, so its structure perpendicular to the sun is far finer
7
+ * than its structure along it, and as a single box `[256, 256, 32]` beats `[128, 128, 128]` by an
8
+ * order of magnitude at identical memory. That advantage belongs to *that* fog and *that* sun
9
+ * elevation. Against a converged reference at one viewpoint, as a single box:
10
+ *
11
+ * | field of view | `[128,128,128]` | `[256,256,32]` |
12
+ * |---|---|---|
13
+ * | 40 deg | +6.4 | +28.1 |
14
+ * | 60 deg | +37.7 | +2.5 |
15
+ * | 90 deg | +1.4 | +39.0 |
16
+ * | 110 deg | +2.1 | +39.5 |
17
+ *
18
+ * Two axes, two Nyquist limits, and which one is starved depends on the camera. Cubic is the shape
19
+ * that does not assume. 96 puts one cascade at 7.1 MB, three at 21.2 MB.
20
+ *
21
+ * @type {number[]}
22
+ */
23
+ export const VOLUMETRICS_SUN_SHADOW_RESOLUTION = [96, 96, 96];
24
+
25
+ /**
26
+ * How many cascades the structure carries.
27
+ *
28
+ * A single world-uniform box cannot be sized correctly, and that is arithmetic rather than tuning.
29
+ * The box has to contain the camera frustum under any rotation, so its side is proportional to
30
+ * `far` and to `tan(fov/2)`; the medium's features — a three-unit fog slab — are absolute. So the
31
+ * ratio between what the structure can resolve and what it has to resolve is a free parameter of
32
+ * the *camera*, and the table above is what that looks like: every single-box configuration
33
+ * measured has a field of view at which it is 28 to 50 mean luminance from the truth, and which
34
+ * one moves with the resolution. Three cascades of 96 are within 1.8 across the same range.
35
+ *
36
+ * Buying the near field's resolution uniformly instead is not an option: 0.28 unit voxels over a
37
+ * 283 unit box is 1024 per axis, i.e. 268 MB. Cascades buy it where the froxels are, which on a
38
+ * log-distributed grid over [0.1, 120] is two thirds of the slices inside 15 units — the innermost
39
+ * box here is 33 units across, so its voxel is 0.34 units for 7.1 MB.
40
+ *
41
+ * Three is what Frostbite ships for its extinction clipmap and what Enshrouded ships for this exact
42
+ * structure. Four is a constant away; the read side, the metadata and the build shaders are all
43
+ * generated from this.
44
+ *
45
+ * @type {number}
46
+ */
47
+ export const VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT = 3;
48
+
49
+ /**
50
+ * Ratio between one cascade's far distance and the next one in.
51
+ *
52
+ * `far_i = far / RATIO^(count - 1 - i)`, the logarithmic split, so every cascade has the same
53
+ * voxel-size ratio to its neighbour and the *worst* case within a cascade — its near end — is a
54
+ * fixed factor. Three cascades at ratio 3 cover a 120 unit far plane as 13.3 / 40 / 120, giving
55
+ * 0.34 / 1.02 / 3.07 unit voxels at 96 per axis.
56
+ *
57
+ * Bounded by the metadata: the per-cascade scalars are packed into `vec4f`, so no more than four.
58
+ *
59
+ * @type {number}
60
+ */
61
+ export const VOLUMETRICS_SUN_SHADOW_CASCADE_RATIO = 3;
62
+
63
+ /**
64
+ * Sub-steps the build pass takes across one voxel of cascade `index`, along the sun.
65
+ *
66
+ * A voxel has to hold the medium's integral across its slab, and the rate that integral is sampled
67
+ * at is a separate knob from the rate it is stored at — only the first has to beat the medium's
68
+ * features. The outermost cascade has the largest voxel and needs the most; each cascade in from it
69
+ * has a voxel `RATIO` times smaller, so the same world-space sample spacing costs `RATIO` times
70
+ * fewer sub-steps. Holding the spacing constant across cascades rather than the count is what keeps
71
+ * the innermost cascade from spending its budget resolving a millimetre.
72
+ *
73
+ * **Always even, and never fewer than two.** The build stores its running integral when it reaches
74
+ * sub-step `count / 2`, which is the voxel's centre only when the halves are equal; an odd count
75
+ * stores the integral to `floor(count/2)/count` of the slab, and one sub-step stores it to the near
76
+ * face — a half-slab bias in the direction that makes the medium shadow too little, on the cascade
77
+ * that matters most.
78
+ *
79
+ * @param {number} index
80
+ * @returns {number}
81
+ */
82
+ export function volumetrics_sun_shadow_sub_steps(index) {
83
+ const outermost = 4;
84
+
85
+ const halves = Math.max(
86
+ 1,
87
+ Math.ceil(outermost / (2 * Math.pow(VOLUMETRICS_SUN_SHADOW_CASCADE_RATIO, VOLUMETRICS_SUN_SHADOW_CASCADE_COUNT - 1 - index)))
88
+ );
89
+
90
+ return 2 * halves;
91
+ }
92
+
93
+ /**
94
+ * `sun_shadow_light_index` when no cascades were placed this frame, so no directional light matches
95
+ * it and every one of them falls back to the per-froxel march.
96
+ *
97
+ * Two situations reach it and neither is a switch: a scene with no directional light, and a camera
98
+ * with no field of view for {@link frustum_bounding_sphere} to work from. The build passes still
99
+ * run — collapsed to one voxel — because the lighting pass binds their outputs unconditionally and
100
+ * WebGPU has no optional bindings.
101
+ *
102
+ * @type {number}
103
+ */
104
+ export const VOLUMETRICS_SUN_SHADOW_LIGHT_NONE = 0xFFFFFFFF;
105
+
106
+ /**
107
+ * Workgroup shape of the build pass. One invocation owns a whole column along the sun axis, so
108
+ * this is 2D and covers the volume's x/y.
109
+ * @type {number[]}
110
+ */
111
+ export const VOLUMETRICS_SUN_SHADOW_WORKGROUP_RESOLUTION = [8, 8];
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Far distance of cascade `index`, i.e. the view depth its box has to cover.
3
+ *
4
+ * A logarithmic split, `far / ratio^(count - 1 - index)`. The choice is forced by what the cascades
5
+ * are for: every box is sized from the bounding sphere of the sub-frustum it covers, so its voxel is
6
+ * proportional to its far distance, and a constant ratio between far distances is a constant ratio
7
+ * between voxel sizes. Any other split leaves one cascade carrying a resolution jump the others do
8
+ * not.
9
+ *
10
+ * The innermost cascade still starts at the camera rather than at the previous cascade's far plane —
11
+ * cascades here nest rather than tile, because the box is sized from a *frustum's* bounding sphere
12
+ * and a shell would need a sphere around the shell, which is the outer sphere again.
13
+ *
14
+ * @param {number} far the full range the structure must cover
15
+ * @param {number} index
16
+ * @param {number} count
17
+ * @param {number} ratio
18
+ * @returns {number}
19
+ */
20
+ export function volumetrics_sun_shadow_cascade_far(far: number, index: number, count: number, ratio: number): number;
21
+ /**
22
+ * Radius of the smallest sphere containing a symmetric perspective frustum, and where its centre
23
+ * sits along the view axis.
24
+ *
25
+ * This is the quantity that makes the structure temporally stable, and the reason it is a sphere
26
+ * rather than a tight fit: it depends only on `fov`, `aspect`, `near` and `far`, so it does not
27
+ * change when the camera turns. A box sized from it therefore has a fixed extent, hence a fixed
28
+ * voxel size, hence a world-space voxel lattice that only ever translates.
29
+ *
30
+ * Both frustum corner rings are equidistant from the centre when
31
+ * `z = (far + near)(k^2 + 1) / 2` with `k^2 = tan^2(theta_x) + tan^2(theta_y)`. Past `z = far` that
32
+ * solution leaves the sphere behind the far plane, and the far ring alone bounds it.
33
+ *
34
+ * @param {number[]} out `[center_view_z, radius]`
35
+ * @param {number} fov_y vertical field of view, radians
36
+ * @param {number} aspect width / height
37
+ * @param {number} near
38
+ * @param {number} far
39
+ * @returns {number[]} out
40
+ */
41
+ export function frustum_bounding_sphere(out: number[], fov_y: number, aspect: number, near: number, far: number): number[];
42
+ /**
43
+ * Place the sun's optical-depth volume for this frame.
44
+ *
45
+ * The volume is a box aligned to the sun: its third axis is the direction light travels, so a
46
+ * column of voxels along that axis is a light ray, and the optical depth of every voxel in the
47
+ * column is one running sum away from its neighbour's. That is what
48
+ * `shader_volumetrics_build_sun_shadow_volume` computes and what
49
+ * `chunk_volumetrics_sun_optical_depth` reads back in a single fetch.
50
+ *
51
+ * Two properties matter and both are about time rather than space, because the froxel lighting this
52
+ * feeds goes through a 0.95-blend temporal filter that turns any instability into either ghosting
53
+ * or flicker:
54
+ *
55
+ * - **The extent never changes with camera orientation.** It comes from the frustum's bounding
56
+ * sphere, which is a function of the projection only. A tight fit of the frustum corners in sun
57
+ * space — the obvious thing, and what this engine's CSM deliberately does for its cascades —
58
+ * would breathe as the camera turned, moving every voxel centre continuously and making the
59
+ * whole structure swim. The CSM can afford the tight fit because it is two orders of magnitude
60
+ * denser and its consumer is a filtered depth comparison; this is neither.
61
+ *
62
+ * - **The origin is snapped to a whole voxel.** With a fixed extent that makes the world-space
63
+ * voxel lattice identical from frame to frame no matter how the camera moves: voxel centres land
64
+ * on the same world positions, so the sampled medium is the same and the reconstruction is the
65
+ * same function of world position. Only the indexing changes. The box shifts by whole voxels,
66
+ * and the slab it gains or loses lies outside the frustum where the medium is zero, so even the
67
+ * scan's starting value is unaffected.
68
+ *
69
+ * @param {Float32Array} ws_to_uvw 16 floats, column-major. World position -> [0,1]^3 volume coords.
70
+ * @param {Float32Array} uvw_to_ws 16 floats, column-major. Its inverse, for the build pass.
71
+ * @param {ArrayLike<number>} light_direction unit vector along which light TRAVELS (the
72
+ * `light_directional` table's `direction`, i.e. NOT the direction toward the light).
73
+ * @param {ArrayLike<number>} camera_position world space
74
+ * @param {ArrayLike<number>} camera_forward unit, world space
75
+ * @param {number} fov_y vertical field of view, radians
76
+ * @param {number} aspect width / height
77
+ * @param {number} near
78
+ * @param {number} far the depth the medium actually extends to — `min(froxel_far, camera.far)`,
79
+ * since the froxel grid is where the extinction is read from and it ends there.
80
+ * @param {ArrayLike<number>} resolution `[x, y, z]` voxel counts
81
+ * @returns {number} world-space length of one voxel along the sun axis, i.e. the scan's `dz`
82
+ */
83
+ export function build_volumetrics_sun_shadow_transform({ ws_to_uvw, uvw_to_ws, light_direction, camera_position, camera_forward, fov_y, aspect, near, far, resolution, }: Float32Array): number;
84
+ //# sourceMappingURL=build_volumetrics_sun_shadow_transform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build_volumetrics_sun_shadow_transform.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.js"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wDANW,MAAM,SACN,MAAM,SACN,MAAM,SACN,MAAM,GACJ,MAAM,CAOlB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,6CAPW,MAAM,EAAE,SACR,MAAM,UACN,MAAM,QACN,MAAM,OACN,MAAM,GACJ,MAAM,EAAE,CAoBpB;AAyDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,0KAdW,YAAY,GAYV,MAAM,CA4ElB"}