@woosh/meep-engine 3.11.0 → 3.11.1

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 (26) hide show
  1. package/package.json +1 -1
  2. package/src/shade/playground/volumetrics_froxel/README.md +88 -0
  3. package/src/shade/playground/volumetrics_froxel/index.html +251 -0
  4. package/src/shade/playground/volumetrics_froxel/main.d.ts +2 -0
  5. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -0
  6. package/src/shade/playground/volumetrics_froxel/main.js +619 -0
  7. package/src/shade/renderer/Renderer.d.ts +13 -0
  8. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  9. package/src/shade/renderer/Renderer.js +23 -0
  10. package/src/shade/renderer/volumetrics/NOTES.md +42 -1
  11. package/src/shade/renderer/volumetrics/chunk_camera_ray_planar_depth_scale.d.ts +21 -0
  12. package/src/shade/renderer/volumetrics/chunk_camera_ray_planar_depth_scale.d.ts.map +1 -0
  13. package/src/shade/renderer/volumetrics/chunk_camera_ray_planar_depth_scale.js +47 -0
  14. package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.d.ts.map +1 -1
  15. package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js +19 -8
  16. package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_content_offset.d.ts +37 -0
  17. package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_content_offset.d.ts.map +1 -0
  18. package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_content_offset.js +56 -0
  19. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  20. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +6 -0
  21. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
  22. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +367 -324
  23. package/src/shade/renderer/volumetrics/shader_volumetrics_build_scattering_lut.d.ts.map +1 -1
  24. package/src/shade/renderer/volumetrics/shader_volumetrics_build_scattering_lut.js +14 -3
  25. package/src/shade/renderer/volumetrics/volumetrics_position_world_to_froxel_uvw.d.ts.map +1 -1
  26. package/src/shade/renderer/volumetrics/volumetrics_position_world_to_froxel_uvw.js +48 -36
@@ -6,4 +6,45 @@
6
6
 
7
7
 
8
8
  Check out "A Novel Sampling Algorithm for Fast and Stable Real-Time Volume Rendering" SIGGRAPH 2015
9
- * https://github.com/huwb/volsample/blob/master/src/shadertoy/clouds.shader
9
+ * https://github.com/huwb/volsample/blob/master/src/shadertoy/clouds.shader
10
+ ## Blue noise for the froxel column jitter — measured, 2026-08-29
11
+
12
+ Considered as a fix for C-05 (adjacent columns carry independent jitter, so a trilinear XY fetch
13
+ mixes slabs at different depths) and as groundwork for cutting `SAMPLE_COUNT`. Measured against the
14
+ shipped `stbn_vec*.bin` volumes rather than argued. Three findings, one of which kills the original
15
+ rationale.
16
+
17
+ **Reusing `stbn_vec3` is not an option — it biases, and the bias does not converge away.**
18
+ The aerial pass already reads `stbn_sample_vec3(vec3(voxel_coord.xy, view.frame_index))` and
19
+ consumes all three channels (`.xy` for the tile UV, `.z` for the slice dither). A column jitter
20
+ indexed the same way is *the same number* — correlation 1.0000, not merely "the same set". Two
21
+ nuisance variables locked together stop exploring the unit square and only ever visit its diagonal,
22
+ so the estimator converges to the wrong integral:
23
+
24
+ | integrand over (j, d) | truth | naive `stbn_vec3.x` converges to | error |
25
+ |---|---|---|---|
26
+ | `j*d` | 0.250 | 0.334 | +34% |
27
+ | `exp(-3(j+d))` | 0.100 | 0.167 | +67% |
28
+ | `step(j > d)` | 0.500 | 0.000 | −100% |
29
+
30
+ RMSE is flat from N=32 to N=128 in every case. That is bias, not noise — no TAA window fixes it.
31
+
32
+ **An independent source is fine, and converges faster than the hash.** `stbn_vec1` is a separate
33
+ scalar 128×128×64 volume, already loaded by `STATIC_GRAPHICS_ENGINE_ASSETS`, uniform to 0.75%, and
34
+ decorrelated from all three `stbn_vec3` channels (|r| ≤ 0.0018). RMSE on `j*d`: 0.0478 vs the
35
+ murmur hash's 0.0632 at N=8, 0.0149 vs 0.0219 at N=64 — roughly 1.3–1.5× better across the window
36
+ the 0.95 TAA blend actually spans (~39 effective samples). Bias ≤ 0.0025.
37
+
38
+ Caveat: STBN stops improving past N=64, the volume's temporal depth, while white noise keeps going —
39
+ at N=128 the hash wins. The TAA never reaches that regime, but anything accumulating longer would.
40
+
41
+ **It does not fix C-05, it makes it slightly worse.** Blue noise is built to make neighbours
42
+ *maximally different*; C-05 wants them *similar*. Measured adjacent-column jitter difference:
43
+ hash 0.3331 (neighbour r = 0.0005, i.e. independent), `stbn_vec1` 0.3656 (neighbour r = −0.112,
44
+ anti-correlated). So it widens the cross-column depth mixing by ~10%.
45
+
46
+ Conclusion: C-05 stays closed as a documented non-issue — it is zero-mean, the TAA integrates it
47
+ away, and the obvious fix aggravates the stated mechanism. Genuinely removing it means dropping the
48
+ XY jitter (the TAA's 3×3×3 Blackman–Harris resample already does spatial reconstruction), not
49
+ changing the noise source. Adopt `stbn_vec1` when cutting `SAMPLE_COUNT` (P-01), where better
50
+ per-pixel temporal distribution is the actual goal — and never `stbn_vec3`, for the reason above.
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Converts a planar view-space depth into a distance along a unit-length view ray.
3
+ *
4
+ * The froxel depth curve is planar. `cluster_depth_from_z_slice` and its inverse are built in
5
+ * `-position_vs.z` — that is what `build_light_cluster_parameters` is derived from, what
6
+ * `volumetrics_position_world_to_froxel_uvw` reads back, and what the deferred composite feeds in
7
+ * (`get_view_space_depth` is a function of the depth buffer alone, with no screen-position term, so
8
+ * it cannot be anything else). `camera_ray_from_uv` returns a *unit* direction. A ray therefore
9
+ * reaches the depth plane at `depth` only after travelling `depth / cos(theta)`, where theta is the
10
+ * angle off the camera forward axis.
11
+ *
12
+ * Multiplying a planar depth by a unit direction stops on the arc of constant distance instead —
13
+ * exact on the centre axis, and short by cos(theta) everywhere else, which reads as a radial error
14
+ * rather than an offset and is why it survives casual inspection.
15
+ *
16
+ * @see shader_volumetrics_build_lighting, shader_volumetrics_build_scattering_lut
17
+ * @type {CodeChunk}
18
+ */
19
+ export const chunk_camera_ray_planar_depth_scale: CodeChunk;
20
+ import { CodeChunk } from "../shader/compiler/CodeChunk.js";
21
+ //# sourceMappingURL=chunk_camera_ray_planar_depth_scale.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk_camera_ray_planar_depth_scale.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/chunk_camera_ray_planar_depth_scale.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;GAiBG;AACH,kDAFU,SAAS,CA2BjB;0BA7CwB,iCAAiC"}
@@ -0,0 +1,47 @@
1
+ import { CAMERA_UNIFORM_STRUCT } from "../camera/CAMERA_UNIFORM_STRUCT.js";
2
+ import { CodeChunk } from "../shader/compiler/CodeChunk.js";
3
+
4
+ /**
5
+ * Converts a planar view-space depth into a distance along a unit-length view ray.
6
+ *
7
+ * The froxel depth curve is planar. `cluster_depth_from_z_slice` and its inverse are built in
8
+ * `-position_vs.z` — that is what `build_light_cluster_parameters` is derived from, what
9
+ * `volumetrics_position_world_to_froxel_uvw` reads back, and what the deferred composite feeds in
10
+ * (`get_view_space_depth` is a function of the depth buffer alone, with no screen-position term, so
11
+ * it cannot be anything else). `camera_ray_from_uv` returns a *unit* direction. A ray therefore
12
+ * reaches the depth plane at `depth` only after travelling `depth / cos(theta)`, where theta is the
13
+ * angle off the camera forward axis.
14
+ *
15
+ * Multiplying a planar depth by a unit direction stops on the arc of constant distance instead —
16
+ * exact on the centre axis, and short by cos(theta) everywhere else, which reads as a radial error
17
+ * rather than an offset and is why it survives casual inspection.
18
+ *
19
+ * @see shader_volumetrics_build_lighting, shader_volumetrics_build_scattering_lut
20
+ * @type {CodeChunk}
21
+ */
22
+ export const chunk_camera_ray_planar_depth_scale = CodeChunk.from(
23
+ //language=WGSL
24
+ `
25
+ fn camera_ray_planar_depth_scale(
26
+ direction_ws: vec3<f32>,
27
+ camera: ${CAMERA_UNIFORM_STRUCT.wgsl_ref}
28
+ ) -> f32 {
29
+
30
+ // The z row of the view matrix dotted with the direction — i.e. (V * vec4(dir, 0)).z without
31
+ // the rest of the multiply. WGSL indexes a matrix by column, so m[c][2] walks that row.
32
+ // View space looks down -Z, so negate to get a positive cosine for a forward-going ray.
33
+ let cos_theta = -(
34
+ camera.view_matrix[0][2] * direction_ws.x
35
+ + camera.view_matrix[1][2] * direction_ws.y
36
+ + camera.view_matrix[2][2] * direction_ws.z
37
+ );
38
+
39
+ // A ray at or past 90 degrees off-axis never reaches the plane at all. Clamping rather than
40
+ // dividing by ~0 keeps the caller finite; no froxel ray is anywhere near this, since every one
41
+ // of them lies inside the frustum.
42
+ return 1.0 / max(cos_theta, 1e-4);
43
+ }
44
+ `, [
45
+ CAMERA_UNIFORM_STRUCT.declaration_chunk,
46
+ ]
47
+ );
@@ -1 +1 @@
1
- {"version":3,"file":"chunk_integrate_optical_depth.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js"],"names":[],"mappings":"AAKA,sDA2DC;0BA9DyB,iCAAiC"}
1
+ {"version":3,"file":"chunk_integrate_optical_depth.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js"],"names":[],"mappings":"AAKA,sDAsEC;0BAzEyB,iCAAiC"}
@@ -6,14 +6,25 @@ import { volumetrics_position_world_to_froxel_uvw } from "./volumetrics_position
6
6
  export const chunk_integrate_optical_depth = CodeChunk.from(
7
7
  //language=WGSL
8
8
  `
9
- fn integrate_optical_depth(ray_ws: ${RAY_STRUCT.wgsl_ref}) -> vec3<f32>{
10
-
11
- var frustum_tmax = min(ray_ws.tmax, frustum_clip_ray(
12
- ray_ws.origin,
13
- ray_ws.direction,
14
- camera.frustum
15
- ));
16
-
9
+ fn integrate_optical_depth(ray_ws: ${RAY_STRUCT.wgsl_ref}) -> vec3<f32>{
10
+
11
+ // volumetrics_metadata.frustum, NOT camera.frustum. The camera's frustum has no usable far
12
+ // plane: an infinite reverse-Z projection puts (0,0,0,near) in row 2 of the VP, a normal that
13
+ // cannot be normalized, so PerspectiveCamera.update_frustum zeroes the entry outright.
14
+ // frustum_clip_ray then discards it — correctly — which leaves the frustum OPEN at the far end,
15
+ // and a ray whose direction lies inside the view cone crosses no plane outward at all. t_exit
16
+ // stays at its 1e20 sentinel; capped by a directional light's tmax of 1.496e11 that gives a
17
+ // step of ~9.35e9, so the first sample lands far outside the grid, the loop below breaks on
18
+ // iteration one, and this returns zero — no media self-shadowing at all whenever the sun is in
19
+ // front of the camera, which is exactly when shafts are wanted.
20
+ //
21
+ // The metadata frustum is the same one rebuilt with a FINITE far plane
22
+ // (update_frustum_far_plane), which the participating-media pass already slices against.
23
+ var frustum_tmax = min(
24
+ ray_ws.tmax,
25
+ frustum_clip_ray(ray_ws.origin, ray_ws.direction, volumetrics_metadata.frustum)
26
+ );
27
+
17
28
  if(frustum_tmax <= 0.0){
18
29
  return vec3<f32>(0.0);
19
30
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Where a froxel's stored value actually sits inside its slab, in slices from the slab's near face.
3
+ *
4
+ * The froxel grid is *cell-averaged*: texel `k` holds an average over the slab spanning slice
5
+ * coordinates `[k + j, k + 1 + j]`, where `j` is the shared column jitter. Reading it back by depth
6
+ * therefore needs the inverse of that, and the centroid is not the midpoint — the lighting pass
7
+ * importance-samples uniformly in `z^3` (a froxel's volume grows as `z^2`), and the depth curve is
8
+ * exponential, so the mean lands slightly past halfway.
9
+ *
10
+ * With `r` the per-slice depth ratio — constant on a log curve, which is what makes the answer the
11
+ * same for every slice and every jitter:
12
+ *
13
+ * z(u) = z0 * (1 + u (r^3 - 1))^(1/3), u ~ U(0,1)
14
+ * offset(u) = ln(1 + u (r^3 - 1)) / (3 ln r)
15
+ * E[offset] = r^3 / (r^3 - 1) - 1 / (3 ln r)
16
+ *
17
+ * That runs ~0.525 to ~0.543 across the depth ranges this engine uses, and tends to exactly 0.5 as
18
+ * the curve flattens toward linear. Treating it as 0.5 — or, as the shipping code did, addressing
19
+ * texel `k` as if it held slice `k` — leaves a systematic bias of about half a froxel, which on a
20
+ * 64-slice log grid is a ~7% depth error at every distance.
21
+ *
22
+ * The jitter `j` is deliberately NOT corrected for here. It is zero-mean, so the temporal filter
23
+ * integrates it away, and a sample lands between two columns carrying *different* jitters anyway
24
+ * (see C-05) — so there is no single `j` to subtract. What is left after this is the part that does
25
+ * not average out.
26
+ *
27
+ * Contrast the aerial LUT, which is *boundary-sampled*: it stores at texel `k` the integral
28
+ * accumulated up to `from_z_slice(k)`, so texel `k` really is slice `k` and the plain `+0.5`
29
+ * texel-centre convention is correct there. Two grids, two conventions; the bug was applying the
30
+ * boundary one to the cell-averaged textures.
31
+ *
32
+ * @see volumetrics_position_world_to_froxel_uvw, shader_volumetrics_build_lighting
33
+ * @type {CodeChunk}
34
+ */
35
+ export const chunk_volumetrics_froxel_content_offset: CodeChunk;
36
+ import { CodeChunk } from "../shader/compiler/CodeChunk.js";
37
+ //# sourceMappingURL=chunk_volumetrics_froxel_content_offset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk_volumetrics_froxel_content_offset.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/chunk_volumetrics_froxel_content_offset.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,sDAFU,SAAS,CAqBjB;0BAvDwB,iCAAiC"}
@@ -0,0 +1,56 @@
1
+ import { CodeChunk } from "../shader/compiler/CodeChunk.js";
2
+
3
+ /**
4
+ * Where a froxel's stored value actually sits inside its slab, in slices from the slab's near face.
5
+ *
6
+ * The froxel grid is *cell-averaged*: texel `k` holds an average over the slab spanning slice
7
+ * coordinates `[k + j, k + 1 + j]`, where `j` is the shared column jitter. Reading it back by depth
8
+ * therefore needs the inverse of that, and the centroid is not the midpoint — the lighting pass
9
+ * importance-samples uniformly in `z^3` (a froxel's volume grows as `z^2`), and the depth curve is
10
+ * exponential, so the mean lands slightly past halfway.
11
+ *
12
+ * With `r` the per-slice depth ratio — constant on a log curve, which is what makes the answer the
13
+ * same for every slice and every jitter:
14
+ *
15
+ * z(u) = z0 * (1 + u (r^3 - 1))^(1/3), u ~ U(0,1)
16
+ * offset(u) = ln(1 + u (r^3 - 1)) / (3 ln r)
17
+ * E[offset] = r^3 / (r^3 - 1) - 1 / (3 ln r)
18
+ *
19
+ * That runs ~0.525 to ~0.543 across the depth ranges this engine uses, and tends to exactly 0.5 as
20
+ * the curve flattens toward linear. Treating it as 0.5 — or, as the shipping code did, addressing
21
+ * texel `k` as if it held slice `k` — leaves a systematic bias of about half a froxel, which on a
22
+ * 64-slice log grid is a ~7% depth error at every distance.
23
+ *
24
+ * The jitter `j` is deliberately NOT corrected for here. It is zero-mean, so the temporal filter
25
+ * integrates it away, and a sample lands between two columns carrying *different* jitters anyway
26
+ * (see C-05) — so there is no single `j` to subtract. What is left after this is the part that does
27
+ * not average out.
28
+ *
29
+ * Contrast the aerial LUT, which is *boundary-sampled*: it stores at texel `k` the integral
30
+ * accumulated up to `from_z_slice(k)`, so texel `k` really is slice `k` and the plain `+0.5`
31
+ * texel-centre convention is correct there. Two grids, two conventions; the bug was applying the
32
+ * boundary one to the cell-averaged textures.
33
+ *
34
+ * @see volumetrics_position_world_to_froxel_uvw, shader_volumetrics_build_lighting
35
+ * @type {CodeChunk}
36
+ */
37
+ export const chunk_volumetrics_froxel_content_offset = CodeChunk.from(
38
+ //language=WGSL
39
+ `
40
+ fn volumetrics_froxel_content_offset(cluster_scale: f32) -> f32 {
41
+
42
+ // r = 2^(1/scale) is the depth ratio across one slice, so ln(r) = ln(2) / scale
43
+ let ln_r = 0.69314718 / max(cluster_scale, 1e-6);
44
+
45
+ // As the curve flattens both terms below diverge while their difference tends to 1/2. Bail out
46
+ // before the subtraction loses its significant digits rather than after.
47
+ if (ln_r < 1e-3) {
48
+ return 0.5;
49
+ }
50
+
51
+ let r3 = exp(3.0 * ln_r);
52
+
53
+ return r3 / (r3 - 1.0) - 1.0 / (3.0 * ln_r);
54
+ }
55
+ `
56
+ );
@@ -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":"AA6SA;;;;;;;;;;;;;;;GAeG;AACH;;;;;EA0JC"}
@@ -239,6 +239,11 @@ function graph_integrate_light_scattering({
239
239
  magFilter: "linear",
240
240
  }));
241
241
 
242
+ // Spatiotemporal blue noise for the per-froxel sample offset. The pass takes one sample and
243
+ // leans on the volumetric TAA to integrate, which only works if each froxel's sequence over
244
+ // frames is well distributed — the property a per-froxel hash does not have.
245
+ const tSTBN_vec3 = context.graphics.textures.obtain(STATIC_GRAPHICS_ENGINE_ASSETS.stbn_vec3);
246
+
242
247
  const encoder = context.encoder;
243
248
 
244
249
  shader_volumetrics_build_lighting.dispatch({
@@ -253,6 +258,7 @@ function graph_integrate_light_scattering({
253
258
  sLinear,
254
259
  tSunTransmittance: sun_transmittance_lut.obtainView(),
255
260
  tMultiScattering: sun_multiple_scattering_lut.obtainView(),
261
+ tSTBN_vec3: tSTBN_vec3.obtainView(),
256
262
  tParticipatingMedia0: participating_media0.obtainView(),
257
263
  tParticipatingMedia1: participating_media1.obtainView(),
258
264
  tLighting: output.obtainView(DEFAULT_RENDER_TARGET_VIEW_DESCRIPTOR),
@@ -1 +1 @@
1
- {"version":3,"file":"shader_volumetrics_build_lighting.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js"],"names":[],"mappings":"AA+TA,8DAIE;8BA9S4B,4BAA4B"}
1
+ {"version":3,"file":"shader_volumetrics_build_lighting.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js"],"names":[],"mappings":"AA0WA,8DAIE;8BA1V4B,4BAA4B"}