@woosh/meep-engine 3.64.0 → 3.64.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 (52) hide show
  1. package/package.json +1 -1
  2. package/src/engine/graphics/texture/sampler/exr_to_sampler2d.d.ts +27 -0
  3. package/src/engine/graphics/texture/sampler/exr_to_sampler2d.d.ts.map +1 -0
  4. package/src/engine/graphics/texture/sampler/exr_to_sampler2d.js +91 -0
  5. package/src/engine/graphics/texture/sampler/sampler2d_to_exr.d.ts +23 -0
  6. package/src/engine/graphics/texture/sampler/sampler2d_to_exr.d.ts.map +1 -0
  7. package/src/engine/graphics/texture/sampler/sampler2d_to_exr.js +64 -0
  8. package/src/format/image/exr/ExrCompression.d.ts +22 -0
  9. package/src/format/image/exr/ExrCompression.d.ts.map +1 -0
  10. package/src/format/image/exr/ExrCompression.js +27 -0
  11. package/src/format/image/exr/ExrImage.d.ts +32 -0
  12. package/src/format/image/exr/ExrImage.d.ts.map +1 -0
  13. package/src/format/image/exr/ExrImage.js +32 -0
  14. package/src/format/image/exr/ExrPixelType.d.ts +13 -0
  15. package/src/format/image/exr/ExrPixelType.d.ts.map +1 -0
  16. package/src/format/image/exr/ExrPixelType.js +18 -0
  17. package/src/format/image/exr/exr_compare_names.d.ts +16 -0
  18. package/src/format/image/exr/exr_compare_names.d.ts.map +1 -0
  19. package/src/format/image/exr/exr_compare_names.js +25 -0
  20. package/src/format/image/exr/exr_constants.d.ts +49 -0
  21. package/src/format/image/exr/exr_constants.d.ts.map +1 -0
  22. package/src/format/image/exr/exr_constants.js +56 -0
  23. package/src/format/image/exr/exr_read.d.ts +32 -0
  24. package/src/format/image/exr/exr_read.d.ts.map +1 -0
  25. package/src/format/image/exr/exr_read.js +531 -0
  26. package/src/format/image/exr/exr_sample_bits.d.ts +16 -0
  27. package/src/format/image/exr/exr_sample_bits.d.ts.map +1 -0
  28. package/src/format/image/exr/exr_sample_bits.js +21 -0
  29. package/src/format/image/exr/exr_scanlines_per_chunk.d.ts +17 -0
  30. package/src/format/image/exr/exr_scanlines_per_chunk.d.ts.map +1 -0
  31. package/src/format/image/exr/exr_scanlines_per_chunk.js +31 -0
  32. package/src/format/image/exr/exr_write.d.ts +27 -0
  33. package/src/format/image/exr/exr_write.d.ts.map +1 -0
  34. package/src/format/image/exr/exr_write.js +378 -0
  35. package/src/shade/playground/probe_clipmap/index.html +5 -0
  36. package/src/shade/playground/probe_clipmap/main.d.ts.map +1 -1
  37. package/src/shade/playground/probe_clipmap/main.js +34 -1
  38. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.d.ts +3 -15
  39. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.d.ts.map +1 -1
  40. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.js +4 -17
  41. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.d.ts +3 -3
  42. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.d.ts.map +1 -1
  43. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.js +15 -13
  44. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.d.ts +7 -3
  45. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.d.ts.map +1 -1
  46. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.js +28 -17
  47. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_project.d.ts +18 -18
  48. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_project.d.ts.map +1 -1
  49. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_project.js +44 -48
  50. package/src/shade/renderer/shader/chunk/filter/chunk_multiscale_mean.d.ts +8 -0
  51. package/src/shade/renderer/shader/chunk/filter/chunk_multiscale_mean.d.ts.map +1 -1
  52. package/src/shade/renderer/shader/chunk/filter/chunk_multiscale_mean.js +21 -3
@@ -1 +1 @@
1
- {"version":3,"file":"shader_probe_clipmap_accumulate.d.ts","sourceRoot":"","sources":["../../../../../../../src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.js"],"names":[],"mappings":"AA+CA;;GAEG;AACH,wCAFU,YAAY,CAmBQ;AAsB9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,8CAFU,aAAa,CA2TpB;6BAvb0B,sCAAsC;8BAJrC,kCAAkC"}
1
+ {"version":3,"file":"shader_probe_clipmap_accumulate.d.ts","sourceRoot":"","sources":["../../../../../../../src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.js"],"names":[],"mappings":"AA+CA;;GAEG;AACH,wCAFU,YAAY,CAmBQ;AAwB9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,8CAFU,aAAa,CAgUpB;6BAlc0B,sCAAsC;8BAJrC,kCAAkC"}
@@ -29,6 +29,7 @@ import {
29
29
  PROBE_RAY_STRUCT,
30
30
  PROBE_RAY_SUMS_READ_RESOURCE,
31
31
  PROBE_RAYS_READ_RESOURCE,
32
+ PROBE_REQUESTS_READ_RESOURCE,
32
33
  PROBE_RESAMPLES_READ_RESOURCE,
33
34
  PROBE_STATES_READ_RESOURCE,
34
35
  PROBE_TRACE_READ_RESOURCE
@@ -36,7 +37,6 @@ import {
36
37
  import { chunk_probe_clipmap_resampled } from "./chunk_probe_clipmap_resampled.js";
37
38
  import {
38
39
  PROBE_BACK_FACE_DEPTH,
39
- PROBE_CHANGE_SIGNIFICANCE,
40
40
  PROBE_DEPTH_LIMIT,
41
41
  PROBE_DEPTH_TEXEL_RAYS,
42
42
  PROBE_RADIANCE_SAMPLES_LIMIT,
@@ -82,6 +82,8 @@ resources.createGroup()
82
82
  .add(PROBE_RADIANCE_HISTORY_RESOURCE)
83
83
  .add(PROBE_DEPTH_HISTORY_RESOURCE)
84
84
  .add(PROBE_ESTIMATORS_RESOURCE)
85
+ // the probes' L0 blends, as their last projection left them
86
+ .add(PROBE_REQUESTS_READ_RESOURCE)
85
87
  // read only where the frame re-samples (chunk_probe_clipmap_resampled)
86
88
  .add(PROBE_RESAMPLES_READ_RESOURCE)
87
89
  // the factor the display multiplies radiance by ahead of its tone curve, as the last frame was shown
@@ -94,9 +96,9 @@ resources.createGroup()
94
96
  * `probe_maps` (`probe_clipmap_maps_words`): two words a radiance texel, rgba as half floats; then a
95
97
  * word a radiance texel, its rays this update, resolved in the low half and back faces in the high,
96
98
  * which classification sums; then a word a depth texel, its moments as half floats; then a word a
97
- * radiance texel, how far its sample fell from its mean in its own deviation, luminance, held to
98
- * `PROBE_CHANGE_SIGNIFICANCE`, and one for its blending one, as half floats, which the probe's ray request
99
- * sums (zero where no sample was blended).
99
+ * radiance texel, its estimator after the update as the probe's L0 estimator sums it
100
+ * (`shader_probe_clipmap_project`): its short mean less its mean and its deviation (the estimator's own,
101
+ * floored at the display's exposure), luminance, as half floats; nothing for a texel no ray has reached.
100
102
  *
101
103
  * - **A texel's rays**: on a uniform update (`PROBE_UNIFORM_PERIOD`), `probe_clipmap_texel_ray`'s. On a
102
104
  * guided one, the run that begins at its walk position in the probe's guide, at most
@@ -114,6 +116,10 @@ resources.createGroup()
114
116
  * - The mean blends by at least 1/(a+1) until the count reaches its limit: a young texel is its running
115
117
  * mean, which the estimator's blend reduction, nothing at its seed, would otherwise hold back. Past it
116
118
  * the estimator alone, down to 1/8192 an update on a noisy texel.
119
+ * - And by at least the probe's L0 blend (`PROBE_REQUEST_STRUCT`'s `blend`), its L0 estimator's catch-up
120
+ * after its last update: a change in the probe's whole light, which the texels' own tests, a one-ray
121
+ * sample's noise apiece, do not see. The texel's own catch-up stays, for light that moves without
122
+ * changing L0.
117
123
  * - Samples are clipped once the variance's window has passed, when 1/(a+1) is no longer its blend:
118
124
  * before that it holds no spread to clip to.
119
125
  * - A texel's first sample seeds both means. A texel no ray reached keeps its history.
@@ -268,34 +274,29 @@ fn main(
268
274
  let tile = index_to_grid2d(slot, settings.radiance_tiles_per_row) * (n + 2u * settings.radiance_skirt) + settings.radiance_skirt;
269
275
  let history = select(textureLoad(probe_radiance, tile + vec2<u32>(i % n, i / n), 0), vec4<f32>(0.0), updates == 0u);
270
276
 
277
+ let estimator = slot * texels + i;
278
+
271
279
  var texel = history;
272
- // how far the sample fell from the texel's mean in its own deviation, luminance, and that it blended
273
- // one: the probe's ray request reads their sums. A texel's part is held to PROBE_CHANGE_SIGNIFICANCE,
274
- // so that one bright sample is one texel's at most
275
- var change = vec2<f32>(0.0);
280
+ // the texel's estimator, its history's until a sample is blended
281
+ var e = probe_accumulate_estimator(history.rgb, probe_estimators[estimator]);
276
282
 
277
283
  if (count > 0u) {
278
284
  // the rays, and the neighbours' re-samples where the frame took them
279
285
  let pooled = probe_clipmap_resampled(k * texels + i, sum, count);
280
286
  let sample = pooled.rgb / pooled.w;
281
- let estimator = slot * texels + i;
287
+ let before = e;
282
288
 
283
- var e = multiscale_mean_seed(sample);
289
+ e = multiscale_mean_seed(sample);
284
290
 
285
291
  if (history.a > 0.0) {
286
292
  let ramp = 1.0 / (history.a + 1.0);
287
- let before = probe_accumulate_estimator(history.rgb, probe_estimators[estimator]);
288
- // the estimator's own floor on the deviation, at the display's exposure
289
- let deviation = rgb_to_luminance(max(multiscale_mean_deviation_floor(before.short_mean, probe_exposure.value), sqrt(max(vec3<f32>(0.0), before.variance))));
290
-
291
- change = vec2<f32>(clamp(rgb_to_luminance(sample - history.rgb) / deviation, -${PROBE_CHANGE_SIGNIFICANCE}.0, ${PROBE_CHANGE_SIGNIFICANCE}.0), 1.0);
292
293
 
293
294
  e = multiscale_mean_add(
294
295
  before,
295
296
  sample,
296
297
  max(ramp, ${PROBE_SHORT_BLEND}),
297
298
  max(ramp, ${PROBE_VARIANCE_BLEND}),
298
- select(0.0, ramp, history.a < ${PROBE_RADIANCE_SAMPLES_LIMIT}.0),
299
+ max(select(0.0, ramp, history.a < ${PROBE_RADIANCE_SAMPLES_LIMIT}.0), probe_requests[slot].blend),
299
300
  ramp <= ${PROBE_VARIANCE_BLEND},
300
301
  probe_exposure.value,
301
302
  );
@@ -306,13 +307,23 @@ fn main(
306
307
  probe_estimators[estimator] = probe_accumulate_estimator_words(e, slot, per_probe + 2u * i, updates);
307
308
  }
308
309
 
310
+ // the estimator as the probe's L0 estimator sums it, its deviation floored at the display's exposure as
311
+ // the estimator's own; nothing for a texel no ray has reached
312
+ var state = vec2<f32>(0.0);
313
+
314
+ if (texel.a > 0.0) {
315
+ let deviation = max(multiscale_mean_deviation_floor(e.short_mean, probe_exposure.value), sqrt(max(vec3<f32>(0.0), e.variance)));
316
+
317
+ state = vec2<f32>(rgb_to_luminance(e.short_mean - e.mean), rgb_to_luminance(deviation));
318
+ }
319
+
309
320
  // the count is whole, which any noise keeps
310
321
  let stored = f16_round_stochastic(texel, vec4<f32>(probe_accumulate_store_noise(slot, i, updates), 0.0));
311
322
 
312
323
  probe_maps[maps + 2u * i] = pack2x16float(stored.rg);
313
324
  probe_maps[maps + 2u * i + 1u] = pack2x16float(stored.ba);
314
325
  probe_maps[maps + 2u * texels + i] = count | (back_faces << 16u);
315
- probe_maps[maps + 3u * texels + m * m + i] = pack2x16float(change);
326
+ probe_maps[maps + 3u * texels + m * m + i] = pack2x16float(state);
316
327
 
317
328
  return;
318
329
  }
@@ -6,9 +6,10 @@ export const PROBE_PROJECT_SETTINGS: WebGPUStruct;
6
6
  * Projection, rows (PROBE_CLIPMAP_PROPOSAL_2026_10_27.md §8.8): a thread a row of each listed probe's
7
7
  * radiance map, dispatched through `PROBE_DISPATCH_ROWS`. It sums the row's texels, new and kept, as
8
8
  * RGB radiance SH3, `L·Y(ω)·dω` over each texel's solid angle, so that `sh3_color_get_irradiance_at` gives
9
- * E; the row's rays this update, resolved and back faces; and how far the update's
10
- * samples fell from their means, with that sum's variance. One row of the probe's projection each, which
11
- * the probe's pass sums.
9
+ * E; the row's rays this update, resolved and back faces; and the row's part of L0 as the texels'
10
+ * estimators hold it, each texel by its solid angle: their short means less their means, their deviations
11
+ * squared (by the solid angle squared) and their short means, luminance. One row of the probe's projection
12
+ * each, which the probe's pass sums.
12
13
  *
13
14
  * @type {ComputeShader}
14
15
  */
@@ -25,21 +26,20 @@ export const shader_probe_clipmap_project_rows: ComputeShader;
25
26
  * radiance is nothing, so their share would read a probe inside a solid as alive. Such an update keeps
26
27
  * the probe's kind and its back-face share.
27
28
  *
28
- * **The probe's ray request** (§8.6): the rays its next guided update asks for, in `probe_requests`.
29
- * - After a probe's first update, the most: Enshrouded's new probe that asks for all it can have.
30
- * - When its light has moved, the most again. The update's change score sums its blended texels' falls
31
- * from their means, luminance, each in its own deviation and held to `PROBE_CHANGE_SIGNIFICANCE` by
32
- * accumulation, over the root of their count. A test texel by texel sees no change under a one-ray
33
- * texel's own noise (measured: the estimator's inconsistency barely moves on a doubling); a sum of the
34
- * falls unbounded is one bright sample's.
35
- * - The score is held to the probe's own: moved means past `PROBE_CHANGE_SIGNIFICANCE` of its running
36
- * deviation, at least one, from its running mean, both over the estimator's variance window,
37
- * `PROBE_VARIANCE_BLEND` an update however long since the last. A probe whose means sit off its samples
38
- * (the estimator's own bias on texels lit by few rays) scores steadily off zero.
39
- * - Measured, Sponza on the 4090, the camera and the light still, 3,600 frames settled: of the probes
40
- * past 64 updates, 4.2% asked for the most against a sum of the raw falls held to zero, 3.2% with each
41
- * texel's fall bounded, and 1.1–1.3% held to their own statistics.
42
- * - Otherwise half what it asked for last, down to the fewest: a converged probe sips.
29
+ * **The probe's L0 estimator** (§8.6, §8.8): a multi-scale mean estimator (`chunk_multiscale_mean`) over the
30
+ * probe's whole light, L0, the solid-angle-weighted sum of its radiance texels. Its short and long means are
31
+ * L0 of the texels' short and long means, and its deviation the root of their deviations' sum, each by its
32
+ * solid angle squared, as the rows sum them; its inconsistency and blend reduction are the probe's own, in
33
+ * `probe_requests`. Its catch-up, the published estimator's, decides what the probe's next update takes:
34
+ * - **Blend**: every texel's long mean blends by at least the catch-up (`shader_probe_clipmap_accumulate`).
35
+ * A one-ray texel's own test sees no change under its own noise; L0 pools every texel's evidence, so a
36
+ * change in the probe's whole light, brighter or darker, is found where no texel can find it.
37
+ * - **Rays**: the next guided update asks for half what the last asked for, raised toward the most by the
38
+ * catch-up, and never under the fewest: a probe whose light moved asks for more, a steady one sips.
39
+ * - After a probe's first update the most rays, a blend of nothing and the estimator seeded as a texel's is,
40
+ * an inconsistency of one and no blend reduction: its slot's last owner's state is not its own.
41
+ * - An update whose texels hold no spread, L0's deviation nothing, leaves the estimator as it was and halves
42
+ * the rays.
43
43
  *
44
44
  * @type {ComputeShader}
45
45
  */
@@ -1 +1 @@
1
- {"version":3,"file":"shader_probe_clipmap_project.d.ts","sourceRoot":"","sources":["../../../../../../../src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_project.js"],"names":[],"mappings":"AAmCA;;GAEG;AACH,qCAFU,YAAY,CAmBK;AAW3B;;;;;;;;;GASG;AACH,gDAFU,aAAa,CAsDpB;AAcH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,2CAFU,aAAa,CA8FpB;6BA3P0B,sCAAsC;8BAHrC,kCAAkC"}
1
+ {"version":3,"file":"shader_probe_clipmap_project.d.ts","sourceRoot":"","sources":["../../../../../../../src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_project.js"],"names":[],"mappings":"AAgCA;;GAEG;AACH,qCAFU,YAAY,CAmBK;AAW3B;;;;;;;;;;GAUG;AACH,gDAFU,aAAa,CAyDpB;AAcH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,2CAFU,aAAa,CA0FpB;6BArP0B,sCAAsC;8BAHrC,kCAAkC"}
@@ -1,3 +1,5 @@
1
+ import { chunk_rgb_to_luminance } from "../../../shader/chunk/color/chunk_rgb_to_luminance.js";
2
+ import { chunk_multiscale_mean } from "../../../shader/chunk/filter/chunk_multiscale_mean.js";
1
3
  import {
2
4
  chunk_octahedral_solid_angle
3
5
  } from "../../../shader/chunk/geometry/octahedron/chunk_octahedral_solid_angle.js";
@@ -26,12 +28,7 @@ import {
26
28
  PROBE_STATES_STORE_RESOURCE,
27
29
  PROBE_TRACE_READ_RESOURCE
28
30
  } from "./chunk_probe_clipmap.js";
29
- import {
30
- PROBE_CHANGE_SIGNIFICANCE,
31
- PROBE_RADIANCE_SAMPLES_LIMIT,
32
- PROBE_THREAD_WORKGROUP_SIZE,
33
- PROBE_VARIANCE_BLEND
34
- } from "./PROBE_CLIPMAP.js";
31
+ import { PROBE_SHORT_BLEND, PROBE_THREAD_WORKGROUP_SIZE } from "./PROBE_CLIPMAP.js";
35
32
 
36
33
  /**
37
34
  * @type {WebGPUStruct}
@@ -68,9 +65,10 @@ row_resources.createGroup()
68
65
  * Projection, rows (PROBE_CLIPMAP_PROPOSAL_2026_10_27.md §8.8): a thread a row of each listed probe's
69
66
  * radiance map, dispatched through `PROBE_DISPATCH_ROWS`. It sums the row's texels, new and kept, as
70
67
  * RGB radiance SH3, `L·Y(ω)·dω` over each texel's solid angle, so that `sh3_color_get_irradiance_at` gives
71
- * E; the row's rays this update, resolved and back faces; and how far the update's
72
- * samples fell from their means, with that sum's variance. One row of the probe's projection each, which
73
- * the probe's pass sums.
68
+ * E; the row's rays this update, resolved and back faces; and the row's part of L0 as the texels'
69
+ * estimators hold it, each texel by its solid angle: their short means less their means, their deviations
70
+ * squared (by the solid angle squared) and their short means, luminance. One row of the probe's projection
71
+ * each, which the probe's pass sums.
74
72
  *
75
73
  * @type {ComputeShader}
76
74
  */
@@ -106,13 +104,15 @@ fn main(
106
104
  let uv = (vec2<f32>(f32(x), f32(row)) + 0.5) * inverse;
107
105
  let solid_angle = octahedral_solid_angle(uv, inverse);
108
106
  let rays = probe_maps[maps + 2u * texels + i];
109
- let change = unpack2x16float(probe_maps[maps + 3u * texels + settings.depth_resolution * settings.depth_resolution + i]);
107
+ // the texel's estimator: its short mean less its mean, and its deviation, luminance
108
+ let state = unpack2x16float(probe_maps[maps + 3u * texels + settings.depth_resolution * settings.depth_resolution + i]);
110
109
 
111
110
  sh3_color_accumulate(&row_sum.sh, light * solid_angle, sh3_basis_at(uv_octahedral_unit_decode(uv)));
112
111
  row_sum.resolved += rays & 0xFFFFu;
113
112
  row_sum.back_faces += rays >> 16u;
114
- row_sum.change += change.x;
115
- row_sum.blended += change.y;
113
+ row_sum.gap += state.x * solid_angle;
114
+ row_sum.variance += state.y * state.y * solid_angle * solid_angle;
115
+ row_sum.level += (rgb_to_luminance(light) + state.x) * solid_angle;
116
116
  }
117
117
 
118
118
  probe_projection[thread] = row_sum;
@@ -124,6 +124,7 @@ fn main(
124
124
  chunk_uv_octahedral_unit_decode,
125
125
  chunk_sh3_basis_at,
126
126
  chunk_sh3_color_accumulate,
127
+ chunk_rgb_to_luminance,
127
128
  PROBE_PROJECTION_ROW_STRUCT.declaration_chunk,
128
129
  ]),
129
130
  });
@@ -152,21 +153,20 @@ probe_resources.createGroup()
152
153
  * radiance is nothing, so their share would read a probe inside a solid as alive. Such an update keeps
153
154
  * the probe's kind and its back-face share.
154
155
  *
155
- * **The probe's ray request** (§8.6): the rays its next guided update asks for, in `probe_requests`.
156
- * - After a probe's first update, the most: Enshrouded's new probe that asks for all it can have.
157
- * - When its light has moved, the most again. The update's change score sums its blended texels' falls
158
- * from their means, luminance, each in its own deviation and held to `PROBE_CHANGE_SIGNIFICANCE` by
159
- * accumulation, over the root of their count. A test texel by texel sees no change under a one-ray
160
- * texel's own noise (measured: the estimator's inconsistency barely moves on a doubling); a sum of the
161
- * falls unbounded is one bright sample's.
162
- * - The score is held to the probe's own: moved means past `PROBE_CHANGE_SIGNIFICANCE` of its running
163
- * deviation, at least one, from its running mean, both over the estimator's variance window,
164
- * `PROBE_VARIANCE_BLEND` an update however long since the last. A probe whose means sit off its samples
165
- * (the estimator's own bias on texels lit by few rays) scores steadily off zero.
166
- * - Measured, Sponza on the 4090, the camera and the light still, 3,600 frames settled: of the probes
167
- * past 64 updates, 4.2% asked for the most against a sum of the raw falls held to zero, 3.2% with each
168
- * texel's fall bounded, and 1.1–1.3% held to their own statistics.
169
- * - Otherwise half what it asked for last, down to the fewest: a converged probe sips.
156
+ * **The probe's L0 estimator** (§8.6, §8.8): a multi-scale mean estimator (`chunk_multiscale_mean`) over the
157
+ * probe's whole light, L0, the solid-angle-weighted sum of its radiance texels. Its short and long means are
158
+ * L0 of the texels' short and long means, and its deviation the root of their deviations' sum, each by its
159
+ * solid angle squared, as the rows sum them; its inconsistency and blend reduction are the probe's own, in
160
+ * `probe_requests`. Its catch-up, the published estimator's, decides what the probe's next update takes:
161
+ * - **Blend**: every texel's long mean blends by at least the catch-up (`shader_probe_clipmap_accumulate`).
162
+ * A one-ray texel's own test sees no change under its own noise; L0 pools every texel's evidence, so a
163
+ * change in the probe's whole light, brighter or darker, is found where no texel can find it.
164
+ * - **Rays**: the next guided update asks for half what the last asked for, raised toward the most by the
165
+ * catch-up, and never under the fewest: a probe whose light moved asks for more, a steady one sips.
166
+ * - After a probe's first update the most rays, a blend of nothing and the estimator seeded as a texel's is,
167
+ * an inconsistency of one and no blend reduction: its slot's last owner's state is not its own.
168
+ * - An update whose texels hold no spread, L0's deviation nothing, leaves the estimator as it was and halves
169
+ * the rays.
170
170
  *
171
171
  * @type {ComputeShader}
172
172
  */
@@ -201,8 +201,9 @@ fn main(
201
201
 
202
202
  sum.resolved += part.resolved;
203
203
  sum.back_faces += part.back_faces;
204
- sum.change += part.change;
205
- sum.blended += part.blended;
204
+ sum.gap += part.gap;
205
+ sum.variance += part.variance;
206
+ sum.level += part.level;
206
207
  }
207
208
 
208
209
  let state = probe_states[slot].x;
@@ -227,28 +228,22 @@ fn main(
227
228
  probe_records[slot].sh = probe_record_sh_encode(sum.sh);
228
229
  probe_records[slot].traced_frame = settings.frame;
229
230
 
230
- // the next guided update's rays: the most after the first update and whenever the light moved, else half
231
+ // the L0 estimator: the next update's blend and rays
231
232
  var request = probe_requests[slot];
232
233
 
233
234
  if (updates == 0u) {
234
- // its slot's last owner's statistics are not its own
235
- request = ${PROBE_REQUEST_STRUCT.make({ rays: "settings.rays_max", change_mean: "0.0", change_variance: "0.0", changes: "0u" })};
236
- } else if (sum.blended > 0.0) {
237
- let score = sum.change / sqrt(sum.blended);
238
- let reach = ${PROBE_CHANGE_SIGNIFICANCE} * max(sqrt(request.change_variance), 1.0);
239
- let moved = abs(score - request.change_mean) > reach;
240
-
241
- // the score's running statistics, a running mean while young, then over the estimator's variance
242
- // window; a score past their reach counts at it, so a change in the light does not become the probe's
243
- // own spread, while a lasting shift of its own still does within a few windows
244
- let held = clamp(score, request.change_mean - reach, request.change_mean + reach);
245
- let blend = max(1.0 / f32(request.changes + 1u), ${PROBE_VARIANCE_BLEND});
246
- let mean = mix(request.change_mean, held, blend);
247
-
248
- request.change_variance = mix(request.change_variance, (held - request.change_mean) * (held - mean), blend);
249
- request.change_mean = mean;
250
- request.changes = min(request.changes + 1u, ${PROBE_RADIANCE_SAMPLES_LIMIT}u);
251
- request.rays = select(max(settings.rays_min, request.rays >> 1u), settings.rays_max, moved);
235
+ // seeded: its slot's last owner's state is not its own
236
+ request = ${PROBE_REQUEST_STRUCT.make({ rays: "settings.rays_max", blend: "0.0", inconsistency: "1.0", reduction: "0.0" })};
237
+ } else if (sum.variance > 0.0) {
238
+ let deviation = sqrt(sum.variance);
239
+ let relative = abs(sum.gap) / deviation;
240
+ let inconsistency = mix(request.inconsistency, relative, ${PROBE_SHORT_BLEND});
241
+ let blend = saturate(multiscale_mean_catch_up(relative, inconsistency, request.reduction));
242
+
243
+ request.reduction = mix(request.reduction, multiscale_mean_blend_reduction(sum.level / deviation), MULTISCALE_MEAN_REDUCTION_BLEND);
244
+ request.inconsistency = inconsistency;
245
+ request.blend = blend;
246
+ request.rays = max(settings.rays_min, u32(round(mix(f32(request.rays >> 1u), f32(settings.rays_max), blend))));
252
247
  } else {
253
248
  request.rays = max(settings.rays_min, request.rays >> 1u);
254
249
  }
@@ -259,6 +254,7 @@ fn main(
259
254
  chunk_probe_clipmap,
260
255
  chunk_probe_clipmap_guide,
261
256
  chunk_folded_workgroup_index,
257
+ chunk_multiscale_mean,
262
258
  PROBE_PROJECTION_ROW_STRUCT.declaration_chunk,
263
259
  PROBE_REQUEST_STRUCT.declaration_chunk,
264
260
  ]),
@@ -8,8 +8,16 @@
8
8
  * fn multiscale_mean_seed(sample: vec3f) -> MultiscaleMean
9
9
  * fn multiscale_mean_add(e: MultiscaleMean, sample: vec3f, short_blend: f32, variance_blend: f32, mean_blend_min: f32, clip: bool, exposure: f32) -> MultiscaleMean
10
10
  * fn multiscale_mean_deviation_floor(short_mean: vec3f, exposure: f32) -> vec3f
11
+ * fn multiscale_mean_catch_up(relative: f32, inconsistency: f32, reduction: f32) -> f32
12
+ * fn multiscale_mean_blend_reduction(level: f32) -> f32
13
+ * const MULTISCALE_MEAN_REDUCTION_BLEND: f32
11
14
  * ```
12
15
  *
16
+ * `multiscale_mean_catch_up` and `multiscale_mean_blend_reduction` are the long mean's decision alone, for an
17
+ * estimator over a quantity the caller keeps the means of itself: `relative` the means' gap over the deviation,
18
+ * `inconsistency` its smoothed history, `reduction` the blend reduction as it stood before this sample, and `level`
19
+ * the short mean over the deviation. The reduction follows `level`'s by `MULTISCALE_MEAN_REDUCTION_BLEND` a sample.
20
+ *
13
21
  * `MultiscaleMean` is {@link MULTISCALE_MEAN_STRUCT}.
14
22
  *
15
23
  * - **Exposure**: the factor the display multiplies the samples by ahead of its tone curve, Unreal Engine's
@@ -1 +1 @@
1
- {"version":3,"file":"chunk_multiscale_mean.d.ts","sourceRoot":"","sources":["../../../../../../../src/shade/renderer/shader/chunk/filter/chunk_multiscale_mean.js"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,oCAFU,SAAS,CAuDZ;0BAhHmB,6BAA6B"}
1
+ {"version":3,"file":"chunk_multiscale_mean.d.ts","sourceRoot":"","sources":["../../../../../../../src/shade/renderer/shader/chunk/filter/chunk_multiscale_mean.js"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,oCAFU,SAAS,CAiEZ;0BAlImB,6BAA6B"}
@@ -16,8 +16,16 @@ const EXPOSURE_MAX = '1.70141183e38';
16
16
  * fn multiscale_mean_seed(sample: vec3f) -> MultiscaleMean
17
17
  * fn multiscale_mean_add(e: MultiscaleMean, sample: vec3f, short_blend: f32, variance_blend: f32, mean_blend_min: f32, clip: bool, exposure: f32) -> MultiscaleMean
18
18
  * fn multiscale_mean_deviation_floor(short_mean: vec3f, exposure: f32) -> vec3f
19
+ * fn multiscale_mean_catch_up(relative: f32, inconsistency: f32, reduction: f32) -> f32
20
+ * fn multiscale_mean_blend_reduction(level: f32) -> f32
21
+ * const MULTISCALE_MEAN_REDUCTION_BLEND: f32
19
22
  * ```
20
23
  *
24
+ * `multiscale_mean_catch_up` and `multiscale_mean_blend_reduction` are the long mean's decision alone, for an
25
+ * estimator over a quantity the caller keeps the means of itself: `relative` the means' gap over the deviation,
26
+ * `inconsistency` its smoothed history, `reduction` the blend reduction as it stood before this sample, and `level`
27
+ * the short mean over the deviation. The reduction follows `level`'s by `MULTISCALE_MEAN_REDUCTION_BLEND` a sample.
28
+ *
21
29
  * `MultiscaleMean` is {@link MULTISCALE_MEAN_STRUCT}.
22
30
  *
23
31
  * - **Exposure**: the factor the display multiplies the samples by ahead of its tone curve, Unreal Engine's
@@ -79,6 +87,16 @@ fn multiscale_mean_deviation_floor(short_mean: vec3f, exposure: f32) -> vec3f {
79
87
  return max(vec3f(sqrt(1e-5) / multiscale_mean_exposure(exposure)), max(abs(short_mean) * exp2(-10.0), vec3f(exp2(-24.0))));
80
88
  }
81
89
 
90
+ const MULTISCALE_MEAN_REDUCTION_BLEND: f32 = 0.1;
91
+
92
+ fn multiscale_mean_catch_up(relative: f32, inconsistency: f32, reduction: f32) -> f32 {
93
+ return clamp(smoothstep(0.0, 1.0, relative * max(0.02, inconsistency - 0.2)), 1.0 / 256.0, 1.0) * reduction;
94
+ }
95
+
96
+ fn multiscale_mean_blend_reduction(level: f32) -> f32 {
97
+ return clamp(0.5 * level, 1.0 / 32.0, 1.0);
98
+ }
99
+
82
100
  fn multiscale_mean_add(e: ${MULTISCALE_MEAN_STRUCT.wgsl_ref}, sample: vec3f, short_blend: f32, variance_blend: f32, mean_blend_min: f32, clip: bool, exposure: f32) -> ${MULTISCALE_MEAN_STRUCT.wgsl_ref} {
83
101
  var y = sample;
84
102
 
@@ -95,15 +113,15 @@ fn multiscale_mean_add(e: ${MULTISCALE_MEAN_STRUCT.wgsl_ref}, sample: vec3f, sho
95
113
 
96
114
  let relative = rgb_to_luminance(abs(e.mean - short_mean) / deviation);
97
115
  let inconsistency = mix(e.inconsistency, relative, short_blend);
98
- let reduction = clamp(rgb_to_luminance(0.5 * short_mean / deviation), 1.0 / 32.0, 1.0);
116
+ let reduction = multiscale_mean_blend_reduction(rgb_to_luminance(short_mean / deviation));
99
117
 
100
- let catch_up = clamp(smoothstep(0.0, 1.0, relative * max(0.02, inconsistency - 0.2)), 1.0 / 256.0, 1.0) * e.reduction;
118
+ let catch_up = multiscale_mean_catch_up(relative, inconsistency, e.reduction);
101
119
 
102
120
  return ${MULTISCALE_MEAN_STRUCT.make({
103
121
  mean: 'mix(e.mean, y, max(saturate(catch_up), mean_blend_min))',
104
122
  short_mean: 'short_mean',
105
123
  variance: 'variance',
106
- reduction: 'mix(e.reduction, reduction, 0.1)',
124
+ reduction: 'mix(e.reduction, reduction, MULTISCALE_MEAN_REDUCTION_BLEND)',
107
125
  inconsistency: 'inconsistency',
108
126
  })};
109
127
  }