@driftengine/core 4.3.0 → 4.4.0

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 (215) hide show
  1. package/CHANGELOG.json +261 -0
  2. package/README.md +1 -1
  3. package/dist/cinematic/cameraPath.d.ts +53 -0
  4. package/dist/cinematic/cameraPath.js +118 -0
  5. package/dist/environment/daylight.d.ts +77 -0
  6. package/dist/environment/daylight.js +77 -0
  7. package/dist/geometry/meshConcat.d.ts +9 -0
  8. package/dist/geometry/meshConcat.js +101 -0
  9. package/dist/index.d.ts +12 -2
  10. package/dist/index.js +14 -1
  11. package/dist/render/backend/createRenderer.d.ts +13 -1
  12. package/dist/render/backend/createRenderer.js +6 -5
  13. package/dist/render/backend/webgl2/renderer.d.ts +147 -1
  14. package/dist/render/backend/webgl2/renderer.js +402 -23
  15. package/dist/render/backend/webgpu/buffers.js +46 -8
  16. package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
  17. package/dist/render/backend/webgpu/depthPass.js +75 -14
  18. package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
  19. package/dist/render/backend/webgpu/exposurePass.js +190 -0
  20. package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
  21. package/dist/render/backend/webgpu/fieldCompose.js +13 -0
  22. package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
  23. package/dist/render/backend/webgpu/flatPass.js +12 -5
  24. package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
  25. package/dist/render/backend/webgpu/particlePass.js +1 -0
  26. package/dist/render/backend/webgpu/postPass.d.ts +12 -2
  27. package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
  28. package/dist/render/backend/webgpu/probeBake.js +97 -7
  29. package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
  30. package/dist/render/backend/webgpu/probeTargets.js +180 -0
  31. package/dist/render/backend/webgpu/renderer.d.ts +174 -11
  32. package/dist/render/backend/webgpu/renderer.js +675 -249
  33. package/dist/render/backend/webgpu/scatterPass.js +7 -7
  34. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  35. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  36. package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
  37. package/dist/render/backend/webgpu/skyPass.js +5 -1
  38. package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
  39. package/dist/render/backend/webgpu/uniformRing.js +53 -3
  40. package/dist/render/bloomChain.d.ts +6 -0
  41. package/dist/render/bloomChain.js +8 -0
  42. package/dist/render/clusteredLights.d.ts +19 -11
  43. package/dist/render/clusteredLights.js +96 -45
  44. package/dist/render/cutoutCaster.d.ts +23 -0
  45. package/dist/render/cutoutCaster.js +20 -0
  46. package/dist/render/driftLight/bake.d.ts +34 -0
  47. package/dist/render/driftLight/bake.js +95 -0
  48. package/dist/render/driftLight/falloff.d.ts +16 -0
  49. package/dist/render/driftLight/falloff.js +23 -0
  50. package/dist/render/driftLight/layout.d.ts +59 -0
  51. package/dist/render/driftLight/layout.js +138 -0
  52. package/dist/render/driftLight/lightField.d.ts +108 -0
  53. package/dist/render/driftLight/lightField.js +179 -0
  54. package/dist/render/driftLight/uniforms.d.ts +28 -0
  55. package/dist/render/driftLight/uniforms.js +41 -0
  56. package/dist/render/driftLight/visibility.d.ts +18 -0
  57. package/dist/render/driftLight/visibility.js +47 -0
  58. package/dist/render/emptyTexture.d.ts +6 -0
  59. package/dist/render/emptyTexture.js +19 -0
  60. package/dist/render/exposurePass.d.ts +48 -0
  61. package/dist/render/exposurePass.js +172 -0
  62. package/dist/render/flameLight.d.ts +71 -0
  63. package/dist/render/flameLight.js +63 -0
  64. package/dist/render/gi/bounceLights.d.ts +34 -0
  65. package/dist/render/gi/bounceLights.js +66 -0
  66. package/dist/render/gi/fieldReach.d.ts +12 -0
  67. package/dist/render/gi/fieldReach.js +62 -0
  68. package/dist/render/gi/globalField.d.ts +8 -0
  69. package/dist/render/gi/globalField.js +31 -0
  70. package/dist/render/gi/probeTrace.d.ts +3 -1
  71. package/dist/render/gi/probeTrace.js +3 -1
  72. package/dist/render/gpuCapability.js +19 -4
  73. package/dist/render/lightBudget.d.ts +11 -0
  74. package/dist/render/lightBudget.js +11 -0
  75. package/dist/render/pointLightSelection.d.ts +16 -0
  76. package/dist/render/pointLightSelection.js +178 -83
  77. package/dist/render/probeGrid.d.ts +11 -1
  78. package/dist/render/probeGrid.js +5 -2
  79. package/dist/render/probeSweeps.d.ts +45 -0
  80. package/dist/render/probeSweeps.js +77 -0
  81. package/dist/render/reflectionProbe.d.ts +43 -1
  82. package/dist/render/reflectionProbe.js +17 -3
  83. package/dist/render/renderQuality.d.ts +3 -1
  84. package/dist/render/rendererHarness.d.ts +9 -0
  85. package/dist/render/rendererHarness.js +18 -3
  86. package/dist/render/sceneTarget.d.ts +20 -0
  87. package/dist/render/sceneTarget.js +62 -1
  88. package/dist/render/shaders/agx.d.ts +31 -0
  89. package/dist/render/shaders/agx.js +81 -0
  90. package/dist/render/shaders/ambientOcclusion.d.ts +24 -5
  91. package/dist/render/shaders/ambientOcclusion.js +120 -63
  92. package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
  93. package/dist/render/shaders/cutoutCoverage.js +40 -0
  94. package/dist/render/shaders/depth.d.ts +25 -0
  95. package/dist/render/shaders/depth.js +69 -3
  96. package/dist/render/shaders/exposure.d.ts +52 -0
  97. package/dist/render/shaders/exposure.js +124 -0
  98. package/dist/render/shaders/filmLook.d.ts +47 -0
  99. package/dist/render/shaders/filmLook.js +98 -0
  100. package/dist/render/shaders/flat/directionalShadow.js +35 -4
  101. package/dist/render/shaders/flat/driftLight.d.ts +29 -0
  102. package/dist/render/shaders/flat/driftLight.js +75 -0
  103. package/dist/render/shaders/flat/index.js +5 -0
  104. package/dist/render/shaders/flat/lobes.js +13 -0
  105. package/dist/render/shaders/flat/main.d.ts +1 -1
  106. package/dist/render/shaders/flat/main.js +68 -9
  107. package/dist/render/shaders/flat/preamble.js +23 -5
  108. package/dist/render/shaders/flat/probeGrid.js +46 -3
  109. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +1 -1
  110. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +1 -1
  111. package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
  112. package/dist/render/shaders/generated/depth.wgsl.js +100 -0
  113. package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
  114. package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
  115. package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
  116. package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
  117. package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
  118. package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
  119. package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
  120. package/dist/render/shaders/generated/rush.wgsl.js +40 -10
  121. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  122. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  123. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
  124. package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
  125. package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
  126. package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
  127. package/dist/render/shaders/globalMedium.js +4 -0
  128. package/dist/render/shaders/localExposure.d.ts +35 -0
  129. package/dist/render/shaders/localExposure.js +138 -0
  130. package/dist/render/shaders/prefilter.d.ts +1 -1
  131. package/dist/render/shaders/prefilter.js +6 -0
  132. package/dist/render/shaders/rush.js +33 -5
  133. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  134. package/dist/render/shaders/tangentFrame.js +15 -3
  135. package/dist/render/shaders/temporalResolve.js +39 -1
  136. package/dist/render/shadowCasters.d.ts +5 -3
  137. package/dist/render/shadowMap.js +13 -0
  138. package/dist/render/surfaceTexture.d.ts +5 -0
  139. package/package.json +6 -6
  140. package/scripts/gpuCompute.mjs +24 -3
  141. package/scripts/png.mjs +23 -12
  142. package/src/cinematic/cameraPath.ts +178 -0
  143. package/src/environment/daylight.ts +152 -0
  144. package/src/geometry/meshConcat.ts +100 -0
  145. package/src/index.ts +32 -0
  146. package/src/render/backend/createRenderer.ts +22 -6
  147. package/src/render/backend/webgl2/renderer.ts +439 -20
  148. package/src/render/backend/webgpu/buffers.ts +51 -8
  149. package/src/render/backend/webgpu/depthPass.ts +78 -10
  150. package/src/render/backend/webgpu/exposurePass.ts +234 -0
  151. package/src/render/backend/webgpu/fieldCompose.ts +15 -0
  152. package/src/render/backend/webgpu/flatPass.ts +10 -2
  153. package/src/render/backend/webgpu/particlePass.ts +9 -0
  154. package/src/render/backend/webgpu/probeBake.ts +143 -9
  155. package/src/render/backend/webgpu/probeTargets.ts +211 -0
  156. package/src/render/backend/webgpu/renderer.ts +801 -270
  157. package/src/render/backend/webgpu/scatterPass.ts +7 -7
  158. package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
  159. package/src/render/backend/webgpu/skyPass.ts +5 -1
  160. package/src/render/backend/webgpu/uniformRing.ts +49 -3
  161. package/src/render/bloomChain.ts +9 -0
  162. package/src/render/clusteredLights.ts +108 -63
  163. package/src/render/cutoutCaster.ts +44 -0
  164. package/src/render/driftLight/bake.ts +125 -0
  165. package/src/render/driftLight/falloff.ts +26 -0
  166. package/src/render/driftLight/layout.ts +177 -0
  167. package/src/render/driftLight/lightField.ts +227 -0
  168. package/src/render/driftLight/uniforms.ts +61 -0
  169. package/src/render/driftLight/visibility.ts +58 -0
  170. package/src/render/emptyTexture.ts +41 -0
  171. package/src/render/exposurePass.ts +186 -0
  172. package/src/render/flameLight.ts +135 -0
  173. package/src/render/gi/bounceLights.ts +68 -0
  174. package/src/render/gi/fieldReach.ts +74 -0
  175. package/src/render/gi/globalField.ts +39 -0
  176. package/src/render/gi/probeTrace.ts +3 -1
  177. package/src/render/gpuCapability.ts +18 -5
  178. package/src/render/lightBudget.ts +12 -0
  179. package/src/render/pointLightSelection.ts +191 -79
  180. package/src/render/probeGrid.ts +13 -2
  181. package/src/render/probeSweeps.ts +80 -0
  182. package/src/render/reflectionProbe.ts +54 -2
  183. package/src/render/renderQuality.ts +3 -1
  184. package/src/render/rendererHarness.ts +25 -3
  185. package/src/render/sceneTarget.ts +75 -1
  186. package/src/render/shaders/ambientOcclusion.ts +125 -63
  187. package/src/render/shaders/cutoutCoverage.ts +43 -0
  188. package/src/render/shaders/depth.ts +80 -3
  189. package/src/render/shaders/exposure.ts +128 -0
  190. package/src/render/shaders/filmLook.ts +111 -0
  191. package/src/render/shaders/flat/directionalShadow.ts +35 -4
  192. package/src/render/shaders/flat/driftLight.ts +75 -0
  193. package/src/render/shaders/flat/index.ts +5 -0
  194. package/src/render/shaders/flat/lobes.ts +13 -0
  195. package/src/render/shaders/flat/main.ts +68 -9
  196. package/src/render/shaders/flat/preamble.ts +23 -5
  197. package/src/render/shaders/flat/probeGrid.ts +46 -3
  198. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +1 -1
  199. package/src/render/shaders/generated/depth.wgsl.ts +103 -0
  200. package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
  201. package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
  202. package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
  203. package/src/render/shaders/generated/rush.wgsl.ts +40 -10
  204. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  205. package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
  206. package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
  207. package/src/render/shaders/globalMedium.ts +4 -0
  208. package/src/render/shaders/localExposure.ts +154 -0
  209. package/src/render/shaders/prefilter.ts +6 -0
  210. package/src/render/shaders/rush.ts +33 -5
  211. package/src/render/shaders/tangentFrame.ts +15 -3
  212. package/src/render/shaders/temporalResolve.ts +39 -1
  213. package/src/render/shadowCasters.ts +5 -3
  214. package/src/render/shadowMap.ts +14 -0
  215. package/src/render/surfaceTexture.ts +5 -0
@@ -179,7 +179,9 @@ export function createFlatBindGroupLayout(device, variant, skinned = false, morp
179
179
  * other kind against a `uint` texture, and the shader never samples it — every read is a
180
180
  * `texelFetch`, which is why the declaration shares a sampler rather than bringing its own.
181
181
  */
182
- const integer = texture.type === 'usampler2D';
182
+ const integer = texture.type === 'usampler2D' || texture.type === 'usampler3D';
183
+ /* DriftLight's brick index and atlas, the lit pass's only volumes. See `driftLight.ts`. */
184
+ const volume = texture.type === 'sampler3D' || texture.type === 'usampler3D';
183
185
  const isShadow = integer || (name !== 'uEnvironment' && (name.endsWith('ShadowMap') || cube || array));
184
186
  entries.push({
185
187
  binding: texture.texture,
@@ -188,6 +190,7 @@ export function createFlatBindGroupLayout(device, variant, skinned = false, morp
188
190
  sampleType: integer ? 'uint' : isShadow ? 'unfilterable-float' : 'float',
189
191
  ...(cube ? { viewDimension: 'cube' } : {}),
190
192
  ...(array ? { viewDimension: '2d-array' } : {}),
193
+ ...(volume ? { viewDimension: '3d' } : {}),
191
194
  },
192
195
  });
193
196
  /*
@@ -347,8 +350,10 @@ depthWrite = true, depthLayer = 0,
347
350
  /** Whether placement and tint arrive per instance. See `FlatVertexOptions`. */
348
351
  instanced = false,
349
352
  /** Which order-independent buffer this pipeline writes, if any. The key must carry it. */
350
- oit = 'none') {
351
- return cache.get(key, () => flatDescriptor(cache, device, layout, variant, key, present, translucent, skinned, morphed, depthWrite, depthLayer, instanced, oit));
353
+ oit = 'none',
354
+ /** Culls nothing, for a surface seen from both faces. The key must carry it: `|2s`. */
355
+ doubleSided = false) {
356
+ return cache.get(key, () => flatDescriptor(cache, device, layout, variant, key, present, translucent, skinned, morphed, depthWrite, depthLayer, instanced, oit, doubleSided));
352
357
  }
353
358
  /**
354
359
  * The same pipeline, compiled off the main thread.
@@ -418,7 +423,9 @@ instanced = false,
418
423
  * ordinary translucent path uses, weighted by a uniform. That is what keeps this off
419
424
  * `flatFrag`'s permutation axis, which `scripts/wgsl.ts` measures at about 247 KB gzipped.
420
425
  */
421
- oit = 'none') {
426
+ oit = 'none',
427
+ /** Culls nothing: a surface seen from both faces. */
428
+ doubleSided = false) {
422
429
  const offset = depthOffsetForLayer(depthLayer);
423
430
  return {
424
431
  label: key,
@@ -459,7 +466,7 @@ oit = 'none') {
459
466
  : { format: cache.format }),
460
467
  ],
461
468
  },
462
- primitive: { topology: 'triangle-list', cullMode: 'back' },
469
+ primitive: { topology: 'triangle-list', cullMode: doubleSided ? 'none' : 'back' },
463
470
  multisample: { count: cache.sampleCount },
464
471
  /*
465
472
  * `less`, matching the WebGL2 path's `LESS` rather than `LEQUAL`, and for the reason
@@ -50,6 +50,14 @@ export interface GpuParticles {
50
50
  readonly fragInts: Int32Array;
51
51
  readonly fields: UniformFields;
52
52
  readonly bindGroup: GPUBindGroup;
53
+ /**
54
+ * The encoder this batch was last drawn into, by the renderer's count, or -1.
55
+ *
56
+ * **A second draw into the same encoder is refused**, because this batch's buffers are written
57
+ * at each draw and every write lands before the encoder runs: both draws would show the second.
58
+ * WebGL2 has no such limit, and a limit one backend has is said, not left to a wrong picture.
59
+ */
60
+ drawnInEncoder: number;
53
61
  dispose(): void;
54
62
  }
55
63
  /**
@@ -145,6 +145,7 @@ export function createGpuParticles(device, capacity, material, blend, stretchSec
145
145
  const vertStaging = new ArrayBuffer(PARTICLE_VERT_SIZE);
146
146
  const fragStaging = new ArrayBuffer(fragSize);
147
147
  return {
148
+ drawnInEncoder: -1,
148
149
  corners: cornerBuffer,
149
150
  instances,
150
151
  indexBuffer,
@@ -26,7 +26,7 @@ import type { PipelineCache } from './pipelineCache.ts';
26
26
  * float target the generated shaders can sample as an ordinary texture.
27
27
  */
28
28
  export declare const RUSH_FRAG_FIELDS: UniformFields;
29
- export declare const RUSH_FRAG_SIZE: 160;
29
+ export declare const RUSH_FRAG_SIZE: 192;
30
30
  declare const RUSH_UNIFORMS: 1;
31
31
  declare const RUSH_TEXTURES: {
32
32
  readonly uScene: {
@@ -49,9 +49,19 @@ declare const RUSH_TEXTURES: {
49
49
  readonly sampler: 39;
50
50
  readonly type: "sampler2D";
51
51
  };
52
- readonly uGradeLut: {
52
+ readonly uExposureHeld: {
53
53
  readonly texture: 40;
54
54
  readonly sampler: 41;
55
+ readonly type: "sampler2D";
56
+ };
57
+ readonly uExposureLocal: {
58
+ readonly texture: 42;
59
+ readonly sampler: 43;
60
+ readonly type: "sampler2D";
61
+ };
62
+ readonly uGradeLut: {
63
+ readonly texture: 44;
64
+ readonly sampler: 45;
55
65
  readonly type: "sampler3D";
56
66
  };
57
67
  };
@@ -51,6 +51,19 @@ import type { PassDistanceField } from '../../pass.ts';
51
51
  * thirteen frames, which at sixty a second is a fifth of a second for a light that moved.
52
52
  */
53
53
  export declare const PROBES_PER_FRAME = 5;
54
+ /**
55
+ * How much of what a probe held each refresh keeps, once the grid has been traced whole.
56
+ *
57
+ * Four fifths, so a probe is about the mean of its last five refreshes. The directions no longer
58
+ * turn, so a still scene gives the same estimate every time and this has nothing to average there;
59
+ * what it smooths is a light that moves, whose sun crosses from one direction of the set to the next
60
+ * in steps. What it gives up is how fast the bounce follows: a fifth of the way each time a probe
61
+ * comes round, which with 64
62
+ * probes at five a frame is a time constant of about half a second at 120 frames and a second at
63
+ * 60. It is counted in refreshes rather than seconds, because the renderer is given no clock; what
64
+ * would make it wrong is a scene whose light jumps and has to be followed at once.
65
+ */
66
+ export declare const PROBE_HISTORY = 0.8;
54
67
  export interface ProbeBakeTargets {
55
68
  /** The array the shading samples, whose irradiance level this writes. */
56
69
  readonly array: GPUTexture;
@@ -61,6 +74,25 @@ export interface ProbeBakeTargets {
61
74
  readonly edge: number;
62
75
  readonly format: GPUTextureFormat;
63
76
  }
77
+ /**
78
+ * A DriftLight field as the bake reads it: its two volumes and its two uniform rows, resolved for a
79
+ * probe by `resolveDriftLight` (radius zero, every fixed light through the volume).
80
+ */
81
+ /**
82
+ * The frame's exact lights as the bake lights a hit with them: `resolveBounceLights`'s packing,
83
+ * how many it packed, and the frame's falloff code.
84
+ */
85
+ export interface ProbeBakeLamps {
86
+ readonly data: Float32Array;
87
+ readonly count: number;
88
+ readonly falloff: number;
89
+ }
90
+ export interface ProbeBakeLightField {
91
+ readonly index: GPUTextureView;
92
+ readonly atlas: GPUTextureView;
93
+ readonly light: ArrayLike<number>;
94
+ readonly origin: ArrayLike<number>;
95
+ }
64
96
  export declare class ProbeBaker {
65
97
  private readonly device;
66
98
  private readonly targets;
@@ -75,14 +107,21 @@ export declare class ProbeBaker {
75
107
  private readonly marchBuffer;
76
108
  private readonly bakeBuffer;
77
109
  private readonly volumeBuffer;
110
+ private readonly lampBuffer;
78
111
  private readonly scheduleBuffer;
79
112
  private readonly rayBuffer;
80
113
  private readonly irradianceBuffer;
81
114
  private readonly blitParams;
82
115
  private readonly blitGroup;
83
- /** Rebuilt only when the field it was built against is a different object. */
116
+ /** Rebuilt only when the field or the light field's volumes it was built against change. */
84
117
  private computeGroup;
85
118
  private boundTo;
119
+ private boundAtlas;
120
+ /** DriftLight's volumes while a scene has none whole: one texel each, never read while off. */
121
+ private readonly emptyIndex;
122
+ private readonly emptyAtlas;
123
+ private readonly emptyIndexView;
124
+ private readonly emptyAtlasView;
86
125
  private readonly marchScratch;
87
126
  private readonly marchInts;
88
127
  private readonly bakeScratch;
@@ -134,15 +173,32 @@ export declare class ProbeBaker {
134
173
  setGrid(layers: number): void;
135
174
  /** Whether every layer has had its chain written, which is what makes the array safe to sample. */
136
175
  get complete(): boolean;
176
+ /**
177
+ * Whether the trace has written `layer`'s irradiance, after which that level is the trace's alone.
178
+ *
179
+ * A rasterised bake of a traced layer refreshes only its roughness chain. Two writers of one
180
+ * level is a layer that jumps to the raster's answer and is pulled back over the next refreshes,
181
+ * which on screen is light sweeping across a scene a probe at a time. What it gives up is the
182
+ * raster's own diffuse answer for that layer, which the trace replaces within a few frames anyway.
183
+ */
184
+ traced(layer: number): boolean;
137
185
  /**
138
186
  * Refresh this frame's share of the grid.
139
187
  *
140
188
  * Returns how many probes were baked, which is zero when there is nothing to trace against.
141
189
  */
142
- bake(encoder: GPUCommandEncoder, field: PassDistanceField, grid: ProbeGrid, seeded: boolean, sun: {
190
+ bake(encoder: GPUCommandEncoder, field: PassDistanceField, grid: ProbeGrid, seeded: boolean, lightField: ProbeBakeLightField | null, lamps: ProbeBakeLamps, sun: {
143
191
  readonly direction: ArrayLike<number>;
144
192
  readonly colour: ArrayLike<number>;
145
- readonly sky: ArrayLike<number>;
193
+ /** Outside, flat, for a frame that drew no sky: the environment's ambient. */
194
+ readonly ambient: ArrayLike<number>;
195
+ /** The sky the frame drew, which an escaping ray reads when there is one. */
196
+ readonly sky: {
197
+ readonly top: ArrayLike<number>;
198
+ readonly horizon: ArrayLike<number>;
199
+ readonly deep: ArrayLike<number>;
200
+ readonly sunDir: ArrayLike<number>;
201
+ } | null;
146
202
  }): number;
147
203
  dispose(): void;
148
204
  }
@@ -41,9 +41,11 @@
41
41
  * irradiance level is rewritten, which is the level the diffuse term reads.
42
42
  */
43
43
  import { PROBE_DIRECTIONS } from '../../gi/probeTrace.js';
44
+ import { BOUNCE_LIGHT_FLOATS } from '../../gi/bounceLights.js';
44
45
  import { probeUpdateSchedule } from '../../gi/probeVolume.js';
46
+ import { probesTheFieldReaches } from '../../gi/fieldReach.js';
45
47
  import { irradianceLevelFor } from '../../prefilterEnvMap.js';
46
- import { BAKE_COUNTS, BAKE_EDGE, BAKE_FRAME, BAKE_ORIGIN, BAKE_SCHEDULED, BAKE_SPACING, BAKE_SKY_COLOUR, BAKE_SUN_COLOUR, BAKE_SUN_DIR, PROBE_BAKE_FLOATS, PROBE_BAKE_WORKGROUP, PROBE_BLIT_FLOATS, PROBE_BLIT_STRIDE, PROBE_BLIT_WGSL, PROBE_RAY_FLOATS, PROBE_TEXEL_FLOATS, probeBakeWgsl, } from '../../shaders/gi/probeBake.wgsl.js';
48
+ import { BAKE_COUNTS, BAKE_DRIFT_LIGHT, BAKE_DRIFT_ORIGIN, BAKE_EDGE, BAKE_FALLOFF, BAKE_LIGHT_COUNT, BOUNCE_LIGHT_ROWS, BAKE_FRAME, BAKE_ORIGIN, BAKE_SCHEDULED, BAKE_SPACING, BAKE_HISTORY, BAKE_SKY_COLOUR, BAKE_SKY_DEEP, BAKE_SKY_DRAWN, BAKE_SKY_SUN, BAKE_SKY_TOP, BAKE_SUN_COLOUR, BAKE_SUN_DIR, PROBE_BAKE_FLOATS, PROBE_BAKE_WORKGROUP, PROBE_BLIT_FLOATS, PROBE_BLIT_STRIDE, PROBE_BLIT_WGSL, PROBE_RAY_FLOATS, PROBE_TEXEL_FLOATS, probeBakeWgsl, } from '../../shaders/gi/probeBake.wgsl.js';
47
49
  import { PROBE_MARCH_DEFAULTS, PROBE_MARCH_FLOATS } from '../../shaders/gi/probeTrace.wgsl.js';
48
50
  import { PROBE_VOLUME_FLOATS, VOLUME_COUNTS, VOLUME_EDGE, VOLUME_INV_SPACING, VOLUME_LEVEL, VOLUME_ORIGIN, VOLUME_RADIANCE_EDGE, VOLUME_RADIANCE_LEVEL, } from '../../shaders/gi/probeVolume.wgsl.js';
49
51
  const COPY_DST = 0x0008;
@@ -63,6 +65,19 @@ const VISIBILITY_COMPUTE = 0x4;
63
65
  * thirteen frames, which at sixty a second is a fifth of a second for a light that moved.
64
66
  */
65
67
  export const PROBES_PER_FRAME = 5;
68
+ /**
69
+ * How much of what a probe held each refresh keeps, once the grid has been traced whole.
70
+ *
71
+ * Four fifths, so a probe is about the mean of its last five refreshes. The directions no longer
72
+ * turn, so a still scene gives the same estimate every time and this has nothing to average there;
73
+ * what it smooths is a light that moves, whose sun crosses from one direction of the set to the next
74
+ * in steps. What it gives up is how fast the bounce follows: a fifth of the way each time a probe
75
+ * comes round, which with 64
76
+ * probes at five a frame is a time constant of about half a second at 120 frames and a second at
77
+ * 60. It is counted in refreshes rather than seconds, because the renderer is given no clock; what
78
+ * would make it wrong is a scene whose light jumps and has to be followed at once.
79
+ */
80
+ export const PROBE_HISTORY = 0.8;
66
81
  export class ProbeBaker {
67
82
  device;
68
83
  targets;
@@ -77,14 +92,21 @@ export class ProbeBaker {
77
92
  marchBuffer;
78
93
  bakeBuffer;
79
94
  volumeBuffer;
95
+ lampBuffer;
80
96
  scheduleBuffer;
81
97
  rayBuffer;
82
98
  irradianceBuffer;
83
99
  blitParams;
84
100
  blitGroup;
85
- /** Rebuilt only when the field it was built against is a different object. */
101
+ /** Rebuilt only when the field or the light field's volumes it was built against change. */
86
102
  computeGroup = null;
87
103
  boundTo = null;
104
+ boundAtlas = null;
105
+ /** DriftLight's volumes while a scene has none whole: one texel each, never read while off. */
106
+ emptyIndex;
107
+ emptyAtlas;
108
+ emptyIndexView;
109
+ emptyAtlasView;
88
110
  marchScratch = new Float32Array(PROBE_MARCH_FLOATS);
89
111
  marchInts = new Uint32Array(this.marchScratch.buffer);
90
112
  bakeScratch = new Float32Array(PROBE_BAKE_FLOATS);
@@ -146,8 +168,25 @@ export class ProbeBaker {
146
168
  },
147
169
  { binding: 9, visibility: VISIBILITY_COMPUTE, sampler: { type: 'filtering' } },
148
170
  { binding: 10, visibility: VISIBILITY_COMPUTE, buffer: storageRead },
171
+ {
172
+ binding: 11,
173
+ visibility: VISIBILITY_COMPUTE,
174
+ texture: { sampleType: 'uint', viewDimension: '3d' },
175
+ },
176
+ {
177
+ binding: 12,
178
+ visibility: VISIBILITY_COMPUTE,
179
+ texture: { sampleType: 'float', viewDimension: '3d' },
180
+ },
181
+ { binding: 13, visibility: VISIBILITY_COMPUTE, sampler: { type: 'filtering' } },
182
+ { binding: 14, visibility: VISIBILITY_COMPUTE, buffer: { type: 'uniform' } },
149
183
  ],
150
184
  });
185
+ const emptyVolume = (label, format) => device.createTexture({ label, size: [1, 1, 1], dimension: '3d', format, usage: 0x4 });
186
+ this.emptyIndex = emptyVolume('probe bake.emptyDriftLightIndex', 'r32uint');
187
+ this.emptyAtlas = emptyVolume('probe bake.emptyDriftLightAtlas', 'rgba16float');
188
+ this.emptyIndexView = this.emptyIndex.createView({ dimension: '3d' });
189
+ this.emptyAtlasView = this.emptyAtlas.createView({ dimension: '3d' });
151
190
  const computeLayout = device.createPipelineLayout({
152
191
  bindGroupLayouts: [this.computeLayout],
153
192
  });
@@ -202,6 +241,7 @@ export class ProbeBaker {
202
241
  this.marchBuffer = this.uniform('march', PROBE_MARCH_FLOATS * 4);
203
242
  this.bakeBuffer = this.uniform('bake', PROBE_BAKE_FLOATS * 4);
204
243
  this.volumeBuffer = this.uniform('volume', PROBE_VOLUME_FLOATS * 4);
244
+ this.lampBuffer = this.uniform('lamps', BOUNCE_LIGHT_ROWS * 16);
205
245
  this.scheduleBuffer = this.storage('schedule', PROBES_PER_FRAME * 4);
206
246
  this.rayBuffer = this.storage('rays', PROBES_PER_FRAME * PROBE_DIRECTIONS * PROBE_RAY_FLOATS * 4);
207
247
  this.irradianceBuffer = this.storage('irradiance', PROBES_PER_FRAME * this.texels * PROBE_TEXEL_FLOATS * 4);
@@ -348,15 +388,27 @@ export class ProbeBaker {
348
388
  }
349
389
  return true;
350
390
  }
391
+ /**
392
+ * Whether the trace has written `layer`'s irradiance, after which that level is the trace's alone.
393
+ *
394
+ * A rasterised bake of a traced layer refreshes only its roughness chain. Two writers of one
395
+ * level is a layer that jumps to the raster's answer and is pulled back over the next refreshes,
396
+ * which on screen is light sweeping across a scene a probe at a time. What it gives up is the
397
+ * raster's own diffuse answer for that layer, which the trace replaces within a few frames anyway.
398
+ */
399
+ traced(layer) {
400
+ return this.filled[layer] === 1;
401
+ }
351
402
  /**
352
403
  * Refresh this frame's share of the grid.
353
404
  *
354
405
  * Returns how many probes were baked, which is zero when there is nothing to trace against.
355
406
  */
356
- bake(encoder, field, grid, seeded, sun) {
407
+ bake(encoder, field, grid, seeded, lightField, lamps, sun) {
357
408
  if (this.layers !== grid.layers)
358
409
  this.setGrid(grid.layers);
359
- const scheduled = probeUpdateSchedule(grid.layers, PROBES_PER_FRAME, this.frameIndex, this.scheduleScratch);
410
+ /* A probe the field does not reach keeps what it has. See `gi/fieldReach.ts`. */
411
+ const scheduled = probesTheFieldReaches(grid, this.scheduleScratch, probeUpdateSchedule(grid.layers, PROBES_PER_FRAME, this.frameIndex, this.scheduleScratch), field.outerBounds);
360
412
  if (scheduled === 0)
361
413
  return 0;
362
414
  const queue = this.device.queue;
@@ -389,8 +441,19 @@ export class ProbeBaker {
389
441
  this.volumeScratch[VOLUME_COUNTS + axis] = grid.counts[axis];
390
442
  }
391
443
  this.bakeScratch[BAKE_EDGE] = this.levelEdge;
392
- this.bakeScratch[BAKE_FRAME] = this.frameIndex;
444
+ /*
445
+ * **The same directions at every refresh.** The set used to turn with the frame so a probe
446
+ * would converge, but only a running mean converges and a blended history never stops moving:
447
+ * every refresh was a new estimate a few per cent from the last, and a courtyard held still
448
+ * flickered, 77% of its pixels moving by more than 3 levels over a dozen frames against 2.5%
449
+ * rasterised. Held, it is 4.8%. What it gives up is the quadrature error of one set of 256
450
+ * directions, which stays where it is rather than averaging away: the held frame read 2.5 levels
451
+ * brighter than the turning one's mean. What would make it wrong is a small bright source that
452
+ * falls between the directions of every probe that sees it.
453
+ */
454
+ this.bakeScratch[BAKE_FRAME] = 0;
393
455
  this.bakeScratch[BAKE_SCHEDULED] = scheduled;
456
+ this.bakeScratch[BAKE_HISTORY] = this.complete ? PROBE_HISTORY : 0;
394
457
  /*
395
458
  * **The sun, because the trace shades what it hits rather than reading a capture.** That is
396
459
  * the whole of what a traced grid buys over a rasterised one: a light that moves changes the
@@ -399,7 +462,24 @@ export class ProbeBaker {
399
462
  for (let axis = 0; axis < 3; axis += 1) {
400
463
  this.bakeScratch[BAKE_SUN_DIR + axis] = sun.direction[axis] ?? 0;
401
464
  this.bakeScratch[BAKE_SUN_COLOUR + axis] = sun.colour[axis] ?? 0;
402
- this.bakeScratch[BAKE_SKY_COLOUR + axis] = sun.sky[axis] ?? 0;
465
+ const sky = sun.sky;
466
+ this.bakeScratch[BAKE_SKY_COLOUR + axis] =
467
+ sky === null ? (sun.ambient[axis] ?? 0) : (sky.horizon[axis] ?? 0);
468
+ this.bakeScratch[BAKE_SKY_TOP + axis] = sky === null ? 0 : (sky.top[axis] ?? 0);
469
+ this.bakeScratch[BAKE_SKY_DEEP + axis] = sky === null ? 0 : (sky.deep[axis] ?? 0);
470
+ this.bakeScratch[BAKE_SKY_SUN + axis] = sky === null ? 0 : (sky.sunDir[axis] ?? 0);
471
+ }
472
+ this.bakeScratch[BAKE_SKY_DRAWN] = sun.sky === null ? 0 : 1;
473
+ /* The summed light of the scene's fixed lights, or zeros that switch it off. */
474
+ for (let lane = 0; lane < 4; lane += 1) {
475
+ this.bakeScratch[BAKE_DRIFT_LIGHT + lane] = lightField?.light[lane] ?? 0;
476
+ this.bakeScratch[BAKE_DRIFT_ORIGIN + lane] = lightField?.origin[lane] ?? 0;
477
+ }
478
+ /* And the frame's exact lights, which light what a ray strikes as they light the frame. */
479
+ this.bakeScratch[BAKE_LIGHT_COUNT] = lamps.count;
480
+ this.bakeScratch[BAKE_FALLOFF] = lamps.falloff;
481
+ if (lamps.count > 0) {
482
+ queue.writeBuffer(this.lampBuffer, 0, lamps.data, 0, lamps.count * BOUNCE_LIGHT_FLOATS);
403
483
  }
404
484
  queue.writeBuffer(this.bakeBuffer, 0, this.bakeScratch);
405
485
  this.volumeScratch[VOLUME_EDGE] = this.levelEdge;
@@ -412,7 +492,8 @@ export class ProbeBaker {
412
492
  this.volumeScratch[VOLUME_RADIANCE_EDGE] = this.targets.edge;
413
493
  this.volumeScratch[VOLUME_RADIANCE_LEVEL] = 0;
414
494
  queue.writeBuffer(this.volumeBuffer, 0, this.volumeScratch);
415
- if (this.boundTo !== field || this.computeGroup === null) {
495
+ const atlas = lightField?.atlas ?? this.emptyAtlasView;
496
+ if (this.boundTo !== field || this.boundAtlas !== atlas || this.computeGroup === null) {
416
497
  this.computeGroup = this.device.createBindGroup({
417
498
  label: 'probe bake',
418
499
  layout: this.computeLayout,
@@ -428,9 +509,14 @@ export class ProbeBaker {
428
509
  { binding: 8, resource: this.targets.view },
429
510
  { binding: 9, resource: this.targets.sampler },
430
511
  { binding: 10, resource: { buffer: field.albedo } },
512
+ { binding: 11, resource: lightField?.index ?? this.emptyIndexView },
513
+ { binding: 12, resource: atlas },
514
+ { binding: 13, resource: this.targets.sampler },
515
+ { binding: 14, resource: { buffer: this.lampBuffer } },
431
516
  ],
432
517
  });
433
518
  this.boundTo = field;
519
+ this.boundAtlas = atlas;
434
520
  }
435
521
  const pass = encoder.beginComputePass({
436
522
  label: 'probe bake',
@@ -485,9 +571,13 @@ export class ProbeBaker {
485
571
  this.rayBuffer.destroy();
486
572
  this.irradianceBuffer.destroy();
487
573
  this.blitParams.destroy();
574
+ this.lampBuffer.destroy();
575
+ this.emptyIndex.destroy();
576
+ this.emptyAtlas.destroy();
488
577
  this.views = [];
489
578
  this.computeGroup = null;
490
579
  this.boundTo = null;
580
+ this.boundAtlas = null;
491
581
  }
492
582
  }
493
583
  const BLACK = { r: 0, g: 0, b: 0, a: 1 };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * What a probe bake draws into and reads from, made once and kept rather than made per bake.
3
+ *
4
+ * **A bake is a per-frame path now, and it was written as a one-off.** It created a depth and a
5
+ * multisampled colour texture per call and destroyed them after, a view per face, and for each
6
+ * probe it completed a view and a bind group per level and face of the blur and the convolution.
7
+ * That was a load-time cost while a scene baked its probes once. Since a bake can be spread a few
8
+ * faces a frame (`ProbeBakeOptions.faces`) and a crossfading grid re-bakes as the light moves, it
9
+ * is every frame, and on the native host, where each object costs most, a courtyard with the sun
10
+ * moving spent a third of its CPU in bakes. The capture cube lives as long as the renderer, so
11
+ * every view of it can too; the array is replaced only when the grid is, and `forgetArray` drops
12
+ * what was made against the old one.
13
+ *
14
+ * What it gives up is holding the attachments between bakes: a depth and a colour texture the size
15
+ * of one face, at the scene's sample count, kept for as long as the renderer is.
16
+ */
17
+ export declare class ProbeTargets {
18
+ private readonly device;
19
+ private readonly probe;
20
+ private readonly size;
21
+ /** Level 0 of each face, which the bake's passes resolve into. */
22
+ readonly faceViews: readonly GPUTextureView[];
23
+ /** The whole chain as a cube, which the convolution samples. */
24
+ readonly wholeCube: GPUTextureView;
25
+ /** Level `l - 1` as a cube, which the blur into level `l` samples; index 0 unused. */
26
+ private readonly chainSources;
27
+ /** Level `l` of face `f`, at `l * faces + f`, which the blur writes. */
28
+ private readonly chainTargets;
29
+ private depth;
30
+ private depthView;
31
+ private colorMsaa;
32
+ private colorMsaaView;
33
+ private attachmentKey;
34
+ private readonly arrayViews;
35
+ private chainGroups;
36
+ private prefilterGroup;
37
+ private groupsBuffer;
38
+ constructor(device: GPUDevice, probe: GPUTexture, size: number);
39
+ /**
40
+ * The depth and, above one sample, the multisampled colour a face is drawn into: made on the first
41
+ * bake and again only if the sample count or the format has changed since.
42
+ */
43
+ attachments(samples: number, format: GPUTextureFormat): {
44
+ depth: GPUTextureView;
45
+ colorMsaa: GPUTextureView | null;
46
+ };
47
+ /** The blur's target for `level` of `face`. */
48
+ chainTarget(level: number, face: number): GPUTextureView;
49
+ /**
50
+ * The blur's group for `level`, over `buffer`: the uniforms, the level above as a cube, the
51
+ * sampler. Remade only if the ring's buffer has been replaced.
52
+ */
53
+ chainGroup(layout: GPUBindGroupLayout, buffer: GPUBuffer, sampler: GPUSampler, level: number): GPUBindGroup;
54
+ /** The convolution's group over `buffer`: the uniforms, the whole chain, the sampler. */
55
+ prefilterGroupFor(layout: GPUBindGroupLayout, buffer: GPUBuffer, sampler: GPUSampler): GPUBindGroup;
56
+ /** `level` of `layer` of the grid's array, which the convolution writes. */
57
+ arrayView(array: GPUTexture, layer: number, level: number): GPUTextureView;
58
+ /** The grid's array has been replaced: nothing made against the old one may be handed out. */
59
+ forgetArray(): void;
60
+ dispose(): void;
61
+ private faceLevel;
62
+ /** Groups hold the ring's buffer, so a replaced buffer makes every one of them stale. */
63
+ private forBuffer;
64
+ private destroyAttachments;
65
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * What a probe bake draws into and reads from, made once and kept rather than made per bake.
3
+ *
4
+ * **A bake is a per-frame path now, and it was written as a one-off.** It created a depth and a
5
+ * multisampled colour texture per call and destroyed them after, a view per face, and for each
6
+ * probe it completed a view and a bind group per level and face of the blur and the convolution.
7
+ * That was a load-time cost while a scene baked its probes once. Since a bake can be spread a few
8
+ * faces a frame (`ProbeBakeOptions.faces`) and a crossfading grid re-bakes as the light moves, it
9
+ * is every frame, and on the native host, where each object costs most, a courtyard with the sun
10
+ * moving spent a third of its CPU in bakes. The capture cube lives as long as the renderer, so
11
+ * every view of it can too; the array is replaced only when the grid is, and `forgetArray` drops
12
+ * what was made against the old one.
13
+ *
14
+ * What it gives up is holding the attachments between bakes: a depth and a colour texture the size
15
+ * of one face, at the scene's sample count, kept for as long as the renderer is.
16
+ */
17
+ import { DEPTH_FORMAT } from '../../depthConvention.js';
18
+ import { PREFILTER_SAMPLER_BINDING, PREFILTER_TEXTURE_BINDING, PREFILTER_UNIFORM_BINDING, PROBE_FACE_BASIS, PROBE_MIP_UNIFORM_SIZE, probeLevels, } from './probePass.js';
19
+ export class ProbeTargets {
20
+ device;
21
+ probe;
22
+ size;
23
+ /** Level 0 of each face, which the bake's passes resolve into. */
24
+ faceViews;
25
+ /** The whole chain as a cube, which the convolution samples. */
26
+ wholeCube;
27
+ /** Level `l - 1` as a cube, which the blur into level `l` samples; index 0 unused. */
28
+ chainSources = [];
29
+ /** Level `l` of face `f`, at `l * faces + f`, which the blur writes. */
30
+ chainTargets = [];
31
+ depth = null;
32
+ depthView = null;
33
+ colorMsaa = null;
34
+ colorMsaaView = null;
35
+ attachmentKey = '';
36
+ /* The array's level-and-layer views and the groups over this ring's buffer, each made once. */
37
+ arrayViews = new Map();
38
+ chainGroups = [];
39
+ prefilterGroup = null;
40
+ groupsBuffer = null;
41
+ constructor(device, probe, size) {
42
+ this.device = device;
43
+ this.probe = probe;
44
+ this.size = size;
45
+ const faces = [];
46
+ for (let face = 0; face < PROBE_FACE_BASIS.length; face++) {
47
+ faces.push(this.faceLevel(face, 0));
48
+ }
49
+ this.faceViews = faces;
50
+ this.wholeCube = probe.createView({ dimension: 'cube' });
51
+ const levels = probeLevels(size);
52
+ for (let level = 1; level < levels; level++) {
53
+ this.chainSources[level] = probe.createView({
54
+ dimension: 'cube',
55
+ baseMipLevel: level - 1,
56
+ mipLevelCount: 1,
57
+ });
58
+ for (let face = 0; face < PROBE_FACE_BASIS.length; face++) {
59
+ this.chainTargets[level * PROBE_FACE_BASIS.length + face] = this.faceLevel(face, level);
60
+ }
61
+ }
62
+ }
63
+ /**
64
+ * The depth and, above one sample, the multisampled colour a face is drawn into: made on the first
65
+ * bake and again only if the sample count or the format has changed since.
66
+ */
67
+ attachments(samples, format) {
68
+ const key = `${samples}:${format}`;
69
+ if (this.depthView === null || key !== this.attachmentKey) {
70
+ this.destroyAttachments();
71
+ this.attachmentKey = key;
72
+ this.depth = this.device.createTexture({
73
+ label: 'probe.depth',
74
+ size: [this.size, this.size],
75
+ format: DEPTH_FORMAT,
76
+ /* The world's sample count, because a bake draws the world with the world's pipelines. */
77
+ sampleCount: samples,
78
+ usage: 0x10, // RENDER_ATTACHMENT
79
+ });
80
+ this.depthView = this.depth.createView();
81
+ this.colorMsaa =
82
+ samples > 1
83
+ ? this.device.createTexture({
84
+ label: 'probe.colorMsaa',
85
+ size: [this.size, this.size],
86
+ format,
87
+ sampleCount: samples,
88
+ usage: 0x10, // RENDER_ATTACHMENT
89
+ })
90
+ : null;
91
+ this.colorMsaaView = this.colorMsaa?.createView() ?? null;
92
+ }
93
+ return { depth: this.depthView, colorMsaa: this.colorMsaaView };
94
+ }
95
+ /** The blur's target for `level` of `face`. */
96
+ chainTarget(level, face) {
97
+ return this.chainTargets[level * PROBE_FACE_BASIS.length + face];
98
+ }
99
+ /**
100
+ * The blur's group for `level`, over `buffer`: the uniforms, the level above as a cube, the
101
+ * sampler. Remade only if the ring's buffer has been replaced.
102
+ */
103
+ chainGroup(layout, buffer, sampler, level) {
104
+ this.forBuffer(buffer);
105
+ let group = this.chainGroups[level];
106
+ if (group === undefined) {
107
+ group = this.device.createBindGroup({
108
+ layout,
109
+ entries: [
110
+ { binding: 0, resource: { buffer, size: PROBE_MIP_UNIFORM_SIZE } },
111
+ { binding: 1, resource: this.chainSources[level] },
112
+ { binding: 2, resource: sampler },
113
+ ],
114
+ });
115
+ this.chainGroups[level] = group;
116
+ }
117
+ return group;
118
+ }
119
+ /** The convolution's group over `buffer`: the uniforms, the whole chain, the sampler. */
120
+ prefilterGroupFor(layout, buffer, sampler) {
121
+ this.forBuffer(buffer);
122
+ this.prefilterGroup ??= this.device.createBindGroup({
123
+ layout,
124
+ entries: [
125
+ { binding: PREFILTER_UNIFORM_BINDING, resource: { buffer, size: PROBE_MIP_UNIFORM_SIZE } },
126
+ { binding: PREFILTER_TEXTURE_BINDING, resource: this.wholeCube },
127
+ { binding: PREFILTER_SAMPLER_BINDING, resource: sampler },
128
+ ],
129
+ });
130
+ return this.prefilterGroup;
131
+ }
132
+ /** `level` of `layer` of the grid's array, which the convolution writes. */
133
+ arrayView(array, layer, level) {
134
+ const key = layer * 32 + level;
135
+ let view = this.arrayViews.get(key);
136
+ if (view === undefined) {
137
+ view = array.createView({
138
+ dimension: '2d',
139
+ baseArrayLayer: layer,
140
+ arrayLayerCount: 1,
141
+ baseMipLevel: level,
142
+ mipLevelCount: 1,
143
+ });
144
+ this.arrayViews.set(key, view);
145
+ }
146
+ return view;
147
+ }
148
+ /** The grid's array has been replaced: nothing made against the old one may be handed out. */
149
+ forgetArray() {
150
+ this.arrayViews.clear();
151
+ }
152
+ dispose() {
153
+ this.destroyAttachments();
154
+ }
155
+ faceLevel(face, level) {
156
+ return this.probe.createView({
157
+ dimension: '2d',
158
+ baseArrayLayer: face,
159
+ arrayLayerCount: 1,
160
+ baseMipLevel: level,
161
+ mipLevelCount: 1,
162
+ });
163
+ }
164
+ /** Groups hold the ring's buffer, so a replaced buffer makes every one of them stale. */
165
+ forBuffer(buffer) {
166
+ if (buffer === this.groupsBuffer)
167
+ return;
168
+ this.groupsBuffer = buffer;
169
+ this.chainGroups = [];
170
+ this.prefilterGroup = null;
171
+ }
172
+ destroyAttachments() {
173
+ this.depth?.destroy();
174
+ this.colorMsaa?.destroy();
175
+ this.depth = null;
176
+ this.depthView = null;
177
+ this.colorMsaa = null;
178
+ this.colorMsaaView = null;
179
+ }
180
+ }