@driftengine/core 4.2.1 → 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 +272 -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 +166 -1
  14. package/dist/render/backend/webgl2/renderer.js +424 -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 +176 -11
  32. package/dist/render/backend/webgpu/renderer.js +680 -250
  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 +462 -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 +807 -271
  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
@@ -42,16 +42,28 @@
42
42
  */
43
43
 
44
44
  import { PROBE_DIRECTIONS } from '../../gi/probeTrace.ts';
45
+ import { BOUNCE_LIGHT_FLOATS } from '../../gi/bounceLights.ts';
45
46
  import { probeUpdateSchedule } from '../../gi/probeVolume.ts';
47
+ import { probesTheFieldReaches } from '../../gi/fieldReach.ts';
46
48
  import { irradianceLevelFor } from '../../prefilterEnvMap.ts';
47
49
  import {
48
50
  BAKE_COUNTS,
51
+ BAKE_DRIFT_LIGHT,
52
+ BAKE_DRIFT_ORIGIN,
49
53
  BAKE_EDGE,
54
+ BAKE_FALLOFF,
55
+ BAKE_LIGHT_COUNT,
56
+ BOUNCE_LIGHT_ROWS,
50
57
  BAKE_FRAME,
51
58
  BAKE_ORIGIN,
52
59
  BAKE_SCHEDULED,
53
60
  BAKE_SPACING,
61
+ BAKE_HISTORY,
54
62
  BAKE_SKY_COLOUR,
63
+ BAKE_SKY_DEEP,
64
+ BAKE_SKY_DRAWN,
65
+ BAKE_SKY_SUN,
66
+ BAKE_SKY_TOP,
55
67
  BAKE_SUN_COLOUR,
56
68
  BAKE_SUN_DIR,
57
69
  PROBE_BAKE_FLOATS,
@@ -97,6 +109,20 @@ const VISIBILITY_COMPUTE = 0x4;
97
109
  */
98
110
  export const PROBES_PER_FRAME = 5;
99
111
 
112
+ /**
113
+ * How much of what a probe held each refresh keeps, once the grid has been traced whole.
114
+ *
115
+ * Four fifths, so a probe is about the mean of its last five refreshes. The directions no longer
116
+ * turn, so a still scene gives the same estimate every time and this has nothing to average there;
117
+ * what it smooths is a light that moves, whose sun crosses from one direction of the set to the next
118
+ * in steps. What it gives up is how fast the bounce follows: a fifth of the way each time a probe
119
+ * comes round, which with 64
120
+ * probes at five a frame is a time constant of about half a second at 120 frames and a second at
121
+ * 60. It is counted in refreshes rather than seconds, because the renderer is given no clock; what
122
+ * would make it wrong is a scene whose light jumps and has to be followed at once.
123
+ */
124
+ export const PROBE_HISTORY = 0.8;
125
+
100
126
  export interface ProbeBakeTargets {
101
127
  /** The array the shading samples, whose irradiance level this writes. */
102
128
  readonly array: GPUTexture;
@@ -108,6 +134,27 @@ export interface ProbeBakeTargets {
108
134
  readonly format: GPUTextureFormat;
109
135
  }
110
136
 
137
+ /**
138
+ * A DriftLight field as the bake reads it: its two volumes and its two uniform rows, resolved for a
139
+ * probe by `resolveDriftLight` (radius zero, every fixed light through the volume).
140
+ */
141
+ /**
142
+ * The frame's exact lights as the bake lights a hit with them: `resolveBounceLights`'s packing,
143
+ * how many it packed, and the frame's falloff code.
144
+ */
145
+ export interface ProbeBakeLamps {
146
+ readonly data: Float32Array;
147
+ readonly count: number;
148
+ readonly falloff: number;
149
+ }
150
+
151
+ export interface ProbeBakeLightField {
152
+ readonly index: GPUTextureView;
153
+ readonly atlas: GPUTextureView;
154
+ readonly light: ArrayLike<number>;
155
+ readonly origin: ArrayLike<number>;
156
+ }
157
+
111
158
  export class ProbeBaker {
112
159
  private readonly device: GPUDevice;
113
160
  private readonly targets: ProbeBakeTargets;
@@ -124,15 +171,22 @@ export class ProbeBaker {
124
171
  private readonly marchBuffer: GPUBuffer;
125
172
  private readonly bakeBuffer: GPUBuffer;
126
173
  private readonly volumeBuffer: GPUBuffer;
174
+ private readonly lampBuffer: GPUBuffer;
127
175
  private readonly scheduleBuffer: GPUBuffer;
128
176
  private readonly rayBuffer: GPUBuffer;
129
177
  private readonly irradianceBuffer: GPUBuffer;
130
178
  private readonly blitParams: GPUBuffer;
131
179
 
132
180
  private readonly blitGroup: GPUBindGroup;
133
- /** Rebuilt only when the field it was built against is a different object. */
181
+ /** Rebuilt only when the field or the light field's volumes it was built against change. */
134
182
  private computeGroup: GPUBindGroup | null = null;
135
183
  private boundTo: PassDistanceField | null = null;
184
+ private boundAtlas: GPUTextureView | null = null;
185
+ /** DriftLight's volumes while a scene has none whole: one texel each, never read while off. */
186
+ private readonly emptyIndex: GPUTexture;
187
+ private readonly emptyAtlas: GPUTexture;
188
+ private readonly emptyIndexView: GPUTextureView;
189
+ private readonly emptyAtlasView: GPUTextureView;
136
190
 
137
191
  private readonly marchScratch = new Float32Array(PROBE_MARCH_FLOATS);
138
192
  private readonly marchInts = new Uint32Array(this.marchScratch.buffer);
@@ -200,8 +254,26 @@ export class ProbeBaker {
200
254
  },
201
255
  { binding: 9, visibility: VISIBILITY_COMPUTE, sampler: { type: 'filtering' } },
202
256
  { binding: 10, visibility: VISIBILITY_COMPUTE, buffer: storageRead },
257
+ {
258
+ binding: 11,
259
+ visibility: VISIBILITY_COMPUTE,
260
+ texture: { sampleType: 'uint', viewDimension: '3d' },
261
+ },
262
+ {
263
+ binding: 12,
264
+ visibility: VISIBILITY_COMPUTE,
265
+ texture: { sampleType: 'float', viewDimension: '3d' },
266
+ },
267
+ { binding: 13, visibility: VISIBILITY_COMPUTE, sampler: { type: 'filtering' } },
268
+ { binding: 14, visibility: VISIBILITY_COMPUTE, buffer: { type: 'uniform' } },
203
269
  ],
204
270
  });
271
+ const emptyVolume = (label: string, format: GPUTextureFormat): GPUTexture =>
272
+ device.createTexture({ label, size: [1, 1, 1], dimension: '3d', format, usage: 0x4 });
273
+ this.emptyIndex = emptyVolume('probe bake.emptyDriftLightIndex', 'r32uint');
274
+ this.emptyAtlas = emptyVolume('probe bake.emptyDriftLightAtlas', 'rgba16float');
275
+ this.emptyIndexView = this.emptyIndex.createView({ dimension: '3d' });
276
+ this.emptyAtlasView = this.emptyAtlas.createView({ dimension: '3d' });
205
277
  const computeLayout = device.createPipelineLayout({
206
278
  bindGroupLayouts: [this.computeLayout],
207
279
  });
@@ -258,6 +330,7 @@ export class ProbeBaker {
258
330
  this.marchBuffer = this.uniform('march', PROBE_MARCH_FLOATS * 4);
259
331
  this.bakeBuffer = this.uniform('bake', PROBE_BAKE_FLOATS * 4);
260
332
  this.volumeBuffer = this.uniform('volume', PROBE_VOLUME_FLOATS * 4);
333
+ this.lampBuffer = this.uniform('lamps', BOUNCE_LIGHT_ROWS * 16);
261
334
  this.scheduleBuffer = this.storage('schedule', PROBES_PER_FRAME * 4);
262
335
  this.rayBuffer = this.storage(
263
336
  'rays',
@@ -419,6 +492,18 @@ export class ProbeBaker {
419
492
  return true;
420
493
  }
421
494
 
495
+ /**
496
+ * Whether the trace has written `layer`'s irradiance, after which that level is the trace's alone.
497
+ *
498
+ * A rasterised bake of a traced layer refreshes only its roughness chain. Two writers of one
499
+ * level is a layer that jumps to the raster's answer and is pulled back over the next refreshes,
500
+ * which on screen is light sweeping across a scene a probe at a time. What it gives up is the
501
+ * raster's own diffuse answer for that layer, which the trace replaces within a few frames anyway.
502
+ */
503
+ traced(layer: number): boolean {
504
+ return this.filled[layer] === 1;
505
+ }
506
+
422
507
  /**
423
508
  * Refresh this frame's share of the grid.
424
509
  *
@@ -429,18 +514,29 @@ export class ProbeBaker {
429
514
  field: PassDistanceField,
430
515
  grid: ProbeGrid,
431
516
  seeded: boolean,
517
+ lightField: ProbeBakeLightField | null,
518
+ lamps: ProbeBakeLamps,
432
519
  sun: {
433
520
  readonly direction: ArrayLike<number>;
434
521
  readonly colour: ArrayLike<number>;
435
- readonly sky: ArrayLike<number>;
522
+ /** Outside, flat, for a frame that drew no sky: the environment's ambient. */
523
+ readonly ambient: ArrayLike<number>;
524
+ /** The sky the frame drew, which an escaping ray reads when there is one. */
525
+ readonly sky: {
526
+ readonly top: ArrayLike<number>;
527
+ readonly horizon: ArrayLike<number>;
528
+ readonly deep: ArrayLike<number>;
529
+ readonly sunDir: ArrayLike<number>;
530
+ } | null;
436
531
  },
437
532
  ): number {
438
533
  if (this.layers !== grid.layers) this.setGrid(grid.layers);
439
- const scheduled = probeUpdateSchedule(
440
- grid.layers,
441
- PROBES_PER_FRAME,
442
- this.frameIndex,
534
+ /* A probe the field does not reach keeps what it has. See `gi/fieldReach.ts`. */
535
+ const scheduled = probesTheFieldReaches(
536
+ grid,
443
537
  this.scheduleScratch,
538
+ probeUpdateSchedule(grid.layers, PROBES_PER_FRAME, this.frameIndex, this.scheduleScratch),
539
+ field.outerBounds,
444
540
  );
445
541
  if (scheduled === 0) return 0;
446
542
 
@@ -476,8 +572,19 @@ export class ProbeBaker {
476
572
  this.volumeScratch[VOLUME_COUNTS + axis] = grid.counts[axis] as number;
477
573
  }
478
574
  this.bakeScratch[BAKE_EDGE] = this.levelEdge;
479
- this.bakeScratch[BAKE_FRAME] = this.frameIndex;
575
+ /*
576
+ * **The same directions at every refresh.** The set used to turn with the frame so a probe
577
+ * would converge, but only a running mean converges and a blended history never stops moving:
578
+ * every refresh was a new estimate a few per cent from the last, and a courtyard held still
579
+ * flickered, 77% of its pixels moving by more than 3 levels over a dozen frames against 2.5%
580
+ * rasterised. Held, it is 4.8%. What it gives up is the quadrature error of one set of 256
581
+ * directions, which stays where it is rather than averaging away: the held frame read 2.5 levels
582
+ * brighter than the turning one's mean. What would make it wrong is a small bright source that
583
+ * falls between the directions of every probe that sees it.
584
+ */
585
+ this.bakeScratch[BAKE_FRAME] = 0;
480
586
  this.bakeScratch[BAKE_SCHEDULED] = scheduled;
587
+ this.bakeScratch[BAKE_HISTORY] = this.complete ? PROBE_HISTORY : 0;
481
588
  /*
482
589
  * **The sun, because the trace shades what it hits rather than reading a capture.** That is
483
590
  * the whole of what a traced grid buys over a rasterised one: a light that moves changes the
@@ -486,7 +593,24 @@ export class ProbeBaker {
486
593
  for (let axis = 0; axis < 3; axis += 1) {
487
594
  this.bakeScratch[BAKE_SUN_DIR + axis] = sun.direction[axis] ?? 0;
488
595
  this.bakeScratch[BAKE_SUN_COLOUR + axis] = sun.colour[axis] ?? 0;
489
- this.bakeScratch[BAKE_SKY_COLOUR + axis] = sun.sky[axis] ?? 0;
596
+ const sky = sun.sky;
597
+ this.bakeScratch[BAKE_SKY_COLOUR + axis] =
598
+ sky === null ? (sun.ambient[axis] ?? 0) : (sky.horizon[axis] ?? 0);
599
+ this.bakeScratch[BAKE_SKY_TOP + axis] = sky === null ? 0 : (sky.top[axis] ?? 0);
600
+ this.bakeScratch[BAKE_SKY_DEEP + axis] = sky === null ? 0 : (sky.deep[axis] ?? 0);
601
+ this.bakeScratch[BAKE_SKY_SUN + axis] = sky === null ? 0 : (sky.sunDir[axis] ?? 0);
602
+ }
603
+ this.bakeScratch[BAKE_SKY_DRAWN] = sun.sky === null ? 0 : 1;
604
+ /* The summed light of the scene's fixed lights, or zeros that switch it off. */
605
+ for (let lane = 0; lane < 4; lane += 1) {
606
+ this.bakeScratch[BAKE_DRIFT_LIGHT + lane] = lightField?.light[lane] ?? 0;
607
+ this.bakeScratch[BAKE_DRIFT_ORIGIN + lane] = lightField?.origin[lane] ?? 0;
608
+ }
609
+ /* And the frame's exact lights, which light what a ray strikes as they light the frame. */
610
+ this.bakeScratch[BAKE_LIGHT_COUNT] = lamps.count;
611
+ this.bakeScratch[BAKE_FALLOFF] = lamps.falloff;
612
+ if (lamps.count > 0) {
613
+ queue.writeBuffer(this.lampBuffer, 0, lamps.data, 0, lamps.count * BOUNCE_LIGHT_FLOATS);
490
614
  }
491
615
  queue.writeBuffer(this.bakeBuffer, 0, this.bakeScratch);
492
616
 
@@ -501,7 +625,8 @@ export class ProbeBaker {
501
625
  this.volumeScratch[VOLUME_RADIANCE_LEVEL] = 0;
502
626
  queue.writeBuffer(this.volumeBuffer, 0, this.volumeScratch);
503
627
 
504
- if (this.boundTo !== field || this.computeGroup === null) {
628
+ const atlas = lightField?.atlas ?? this.emptyAtlasView;
629
+ if (this.boundTo !== field || this.boundAtlas !== atlas || this.computeGroup === null) {
505
630
  this.computeGroup = this.device.createBindGroup({
506
631
  label: 'probe bake',
507
632
  layout: this.computeLayout,
@@ -517,9 +642,14 @@ export class ProbeBaker {
517
642
  { binding: 8, resource: this.targets.view },
518
643
  { binding: 9, resource: this.targets.sampler },
519
644
  { binding: 10, resource: { buffer: field.albedo } },
645
+ { binding: 11, resource: lightField?.index ?? this.emptyIndexView },
646
+ { binding: 12, resource: atlas },
647
+ { binding: 13, resource: this.targets.sampler },
648
+ { binding: 14, resource: { buffer: this.lampBuffer } },
520
649
  ],
521
650
  });
522
651
  this.boundTo = field;
652
+ this.boundAtlas = atlas;
523
653
  }
524
654
 
525
655
  const pass = encoder.beginComputePass({
@@ -579,9 +709,13 @@ export class ProbeBaker {
579
709
  this.rayBuffer.destroy();
580
710
  this.irradianceBuffer.destroy();
581
711
  this.blitParams.destroy();
712
+ this.lampBuffer.destroy();
713
+ this.emptyIndex.destroy();
714
+ this.emptyAtlas.destroy();
582
715
  this.views = [];
583
716
  this.computeGroup = null;
584
717
  this.boundTo = null;
718
+ this.boundAtlas = null;
585
719
  }
586
720
  }
587
721
 
@@ -0,0 +1,211 @@
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
+
18
+ import { DEPTH_FORMAT } from '../../depthConvention.ts';
19
+ import {
20
+ PREFILTER_SAMPLER_BINDING,
21
+ PREFILTER_TEXTURE_BINDING,
22
+ PREFILTER_UNIFORM_BINDING,
23
+ PROBE_FACE_BASIS,
24
+ PROBE_MIP_UNIFORM_SIZE,
25
+ probeLevels,
26
+ } from './probePass.ts';
27
+
28
+ export class ProbeTargets {
29
+ private readonly device: GPUDevice;
30
+ private readonly probe: GPUTexture;
31
+ private readonly size: number;
32
+ /** Level 0 of each face, which the bake's passes resolve into. */
33
+ readonly faceViews: readonly GPUTextureView[];
34
+ /** The whole chain as a cube, which the convolution samples. */
35
+ readonly wholeCube: GPUTextureView;
36
+ /** Level `l - 1` as a cube, which the blur into level `l` samples; index 0 unused. */
37
+ private readonly chainSources: GPUTextureView[] = [];
38
+ /** Level `l` of face `f`, at `l * faces + f`, which the blur writes. */
39
+ private readonly chainTargets: GPUTextureView[] = [];
40
+ private depth: GPUTexture | null = null;
41
+ private depthView: GPUTextureView | null = null;
42
+ private colorMsaa: GPUTexture | null = null;
43
+ private colorMsaaView: GPUTextureView | null = null;
44
+ private attachmentKey = '';
45
+ /* The array's level-and-layer views and the groups over this ring's buffer, each made once. */
46
+ private readonly arrayViews = new Map<number, GPUTextureView>();
47
+ private chainGroups: GPUBindGroup[] = [];
48
+ private prefilterGroup: GPUBindGroup | null = null;
49
+ private groupsBuffer: GPUBuffer | null = null;
50
+
51
+ constructor(device: GPUDevice, probe: GPUTexture, size: number) {
52
+ this.device = device;
53
+ this.probe = probe;
54
+ this.size = size;
55
+ const faces: GPUTextureView[] = [];
56
+ for (let face = 0; face < PROBE_FACE_BASIS.length; face++) {
57
+ faces.push(this.faceLevel(face, 0));
58
+ }
59
+ this.faceViews = faces;
60
+ this.wholeCube = probe.createView({ dimension: 'cube' });
61
+ const levels = probeLevels(size);
62
+ for (let level = 1; level < levels; level++) {
63
+ this.chainSources[level] = probe.createView({
64
+ dimension: 'cube',
65
+ baseMipLevel: level - 1,
66
+ mipLevelCount: 1,
67
+ });
68
+ for (let face = 0; face < PROBE_FACE_BASIS.length; face++) {
69
+ this.chainTargets[level * PROBE_FACE_BASIS.length + face] = this.faceLevel(face, level);
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * The depth and, above one sample, the multisampled colour a face is drawn into: made on the first
76
+ * bake and again only if the sample count or the format has changed since.
77
+ */
78
+ attachments(
79
+ samples: number,
80
+ format: GPUTextureFormat,
81
+ ): { depth: GPUTextureView; colorMsaa: GPUTextureView | null } {
82
+ const key = `${samples}:${format}`;
83
+ if (this.depthView === null || key !== this.attachmentKey) {
84
+ this.destroyAttachments();
85
+ this.attachmentKey = key;
86
+ this.depth = this.device.createTexture({
87
+ label: 'probe.depth',
88
+ size: [this.size, this.size],
89
+ format: DEPTH_FORMAT,
90
+ /* The world's sample count, because a bake draws the world with the world's pipelines. */
91
+ sampleCount: samples,
92
+ usage: 0x10, // RENDER_ATTACHMENT
93
+ });
94
+ this.depthView = this.depth.createView();
95
+ this.colorMsaa =
96
+ samples > 1
97
+ ? this.device.createTexture({
98
+ label: 'probe.colorMsaa',
99
+ size: [this.size, this.size],
100
+ format,
101
+ sampleCount: samples,
102
+ usage: 0x10, // RENDER_ATTACHMENT
103
+ })
104
+ : null;
105
+ this.colorMsaaView = this.colorMsaa?.createView() ?? null;
106
+ }
107
+ return { depth: this.depthView as GPUTextureView, colorMsaa: this.colorMsaaView };
108
+ }
109
+
110
+ /** The blur's target for `level` of `face`. */
111
+ chainTarget(level: number, face: number): GPUTextureView {
112
+ return this.chainTargets[level * PROBE_FACE_BASIS.length + face] as GPUTextureView;
113
+ }
114
+
115
+ /**
116
+ * The blur's group for `level`, over `buffer`: the uniforms, the level above as a cube, the
117
+ * sampler. Remade only if the ring's buffer has been replaced.
118
+ */
119
+ chainGroup(
120
+ layout: GPUBindGroupLayout,
121
+ buffer: GPUBuffer,
122
+ sampler: GPUSampler,
123
+ level: number,
124
+ ): GPUBindGroup {
125
+ this.forBuffer(buffer);
126
+ let group = this.chainGroups[level];
127
+ if (group === undefined) {
128
+ group = this.device.createBindGroup({
129
+ layout,
130
+ entries: [
131
+ { binding: 0, resource: { buffer, size: PROBE_MIP_UNIFORM_SIZE } },
132
+ { binding: 1, resource: this.chainSources[level] as GPUTextureView },
133
+ { binding: 2, resource: sampler },
134
+ ],
135
+ });
136
+ this.chainGroups[level] = group;
137
+ }
138
+ return group;
139
+ }
140
+
141
+ /** The convolution's group over `buffer`: the uniforms, the whole chain, the sampler. */
142
+ prefilterGroupFor(
143
+ layout: GPUBindGroupLayout,
144
+ buffer: GPUBuffer,
145
+ sampler: GPUSampler,
146
+ ): GPUBindGroup {
147
+ this.forBuffer(buffer);
148
+ this.prefilterGroup ??= this.device.createBindGroup({
149
+ layout,
150
+ entries: [
151
+ { binding: PREFILTER_UNIFORM_BINDING, resource: { buffer, size: PROBE_MIP_UNIFORM_SIZE } },
152
+ { binding: PREFILTER_TEXTURE_BINDING, resource: this.wholeCube },
153
+ { binding: PREFILTER_SAMPLER_BINDING, resource: sampler },
154
+ ],
155
+ });
156
+ return this.prefilterGroup;
157
+ }
158
+
159
+ /** `level` of `layer` of the grid's array, which the convolution writes. */
160
+ arrayView(array: GPUTexture, layer: number, level: number): GPUTextureView {
161
+ const key = layer * 32 + level;
162
+ let view = this.arrayViews.get(key);
163
+ if (view === undefined) {
164
+ view = array.createView({
165
+ dimension: '2d',
166
+ baseArrayLayer: layer,
167
+ arrayLayerCount: 1,
168
+ baseMipLevel: level,
169
+ mipLevelCount: 1,
170
+ });
171
+ this.arrayViews.set(key, view);
172
+ }
173
+ return view;
174
+ }
175
+
176
+ /** The grid's array has been replaced: nothing made against the old one may be handed out. */
177
+ forgetArray(): void {
178
+ this.arrayViews.clear();
179
+ }
180
+
181
+ dispose(): void {
182
+ this.destroyAttachments();
183
+ }
184
+
185
+ private faceLevel(face: number, level: number): GPUTextureView {
186
+ return this.probe.createView({
187
+ dimension: '2d',
188
+ baseArrayLayer: face,
189
+ arrayLayerCount: 1,
190
+ baseMipLevel: level,
191
+ mipLevelCount: 1,
192
+ });
193
+ }
194
+
195
+ /** Groups hold the ring's buffer, so a replaced buffer makes every one of them stale. */
196
+ private forBuffer(buffer: GPUBuffer): void {
197
+ if (buffer === this.groupsBuffer) return;
198
+ this.groupsBuffer = buffer;
199
+ this.chainGroups = [];
200
+ this.prefilterGroup = null;
201
+ }
202
+
203
+ private destroyAttachments(): void {
204
+ this.depth?.destroy();
205
+ this.colorMsaa?.destroy();
206
+ this.depth = null;
207
+ this.depthView = null;
208
+ this.colorMsaa = null;
209
+ this.colorMsaaView = null;
210
+ }
211
+ }