@driftengine/core 4.8.7 → 4.9.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 (163) hide show
  1. package/CHANGELOG.json +61 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +5 -0
  4. package/dist/index.js +7 -0
  5. package/dist/render/atmosphere.d.ts +13 -0
  6. package/dist/render/atmosphere.js +9 -1
  7. package/dist/render/backend/api.d.ts +9 -1
  8. package/dist/render/backend/createRenderer.d.ts +6 -0
  9. package/dist/render/backend/createRenderer.js +4 -1
  10. package/dist/render/backend/webgl2/renderer.d.ts +90 -2
  11. package/dist/render/backend/webgl2/renderer.js +297 -16
  12. package/dist/render/backend/webgpu/device.d.ts +7 -1
  13. package/dist/render/backend/webgpu/device.js +31 -3
  14. package/dist/render/backend/webgpu/flatPass.d.ts +16 -1
  15. package/dist/render/backend/webgpu/flatPass.js +58 -26
  16. package/dist/render/backend/webgpu/postPass.d.ts +51 -9
  17. package/dist/render/backend/webgpu/postPass.js +16 -2
  18. package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
  19. package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
  20. package/dist/render/backend/webgpu/renderer.d.ts +131 -2
  21. package/dist/render/backend/webgpu/renderer.js +586 -70
  22. package/dist/render/displayRange.d.ts +31 -0
  23. package/dist/render/displayRange.js +47 -0
  24. package/dist/render/filmicCurve.d.ts +74 -0
  25. package/dist/render/filmicCurve.js +203 -0
  26. package/dist/render/fog.d.ts +4 -1
  27. package/dist/render/fog.js +12 -2
  28. package/dist/render/frame/resources.d.ts +2 -2
  29. package/dist/render/frame/resources.js +6 -0
  30. package/dist/render/frameReflections.d.ts +40 -0
  31. package/dist/render/frameReflections.js +48 -0
  32. package/dist/render/pass.d.ts +49 -0
  33. package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
  34. package/dist/render/reflectionSurfaceTargets.js +99 -0
  35. package/dist/render/renderQuality.d.ts +39 -8
  36. package/dist/render/renderQuality.js +4 -0
  37. package/dist/render/sceneTarget.d.ts +5 -0
  38. package/dist/render/sceneTarget.js +24 -1
  39. package/dist/render/screenSpaceReflection.d.ts +5 -0
  40. package/dist/render/screenSpaceReflection.js +5 -0
  41. package/dist/render/shaders/arcane.d.ts +1 -1
  42. package/dist/render/shaders/bolt.d.ts +1 -1
  43. package/dist/render/shaders/caustics.d.ts +1 -1
  44. package/dist/render/shaders/film.d.ts +1 -1
  45. package/dist/render/shaders/filmLook.js +2 -1
  46. package/dist/render/shaders/fire.d.ts +1 -1
  47. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  48. package/dist/render/shaders/flat/glassTint.js +2 -0
  49. package/dist/render/shaders/flat/index.d.ts +10 -0
  50. package/dist/render/shaders/flat/index.js +6 -0
  51. package/dist/render/shaders/flat/main.d.ts +1 -1
  52. package/dist/render/shaders/flat/main.js +28 -0
  53. package/dist/render/shaders/flat/overlay.d.ts +16 -0
  54. package/dist/render/shaders/flat/overlay.js +117 -0
  55. package/dist/render/shaders/flat/pointShadow.js +1 -1
  56. package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
  57. package/dist/render/shaders/flat/reflectionSurface.js +61 -0
  58. package/dist/render/shaders/fog.d.ts +1 -1
  59. package/dist/render/shaders/fog.js +9 -2
  60. package/dist/render/shaders/fringe.d.ts +29 -0
  61. package/dist/render/shaders/fringe.js +55 -0
  62. package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
  63. package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
  64. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  65. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  66. package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
  67. package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
  68. package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
  69. package/dist/render/shaders/generated/film.wgsl.js +1 -1
  70. package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
  71. package/dist/render/shaders/generated/fire.wgsl.js +1 -1
  72. package/dist/render/shaders/generated/flat.wgsl.d.ts +176 -16
  73. package/dist/render/shaders/generated/flat.wgsl.js +214 -54
  74. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  75. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  76. package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
  77. package/dist/render/shaders/generated/particle.wgsl.js +4 -4
  78. package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
  79. package/dist/render/shaders/generated/rush.wgsl.js +56 -26
  80. package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
  81. package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
  82. package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
  83. package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
  84. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
  85. package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
  86. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +22 -2
  87. package/dist/render/shaders/generated/ssrTrace.wgsl.js +22 -2
  88. package/dist/render/shaders/generated/water.wgsl.d.ts +1 -1
  89. package/dist/render/shaders/generated/water.wgsl.js +1 -1
  90. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
  91. package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
  92. package/dist/render/shaders/line.d.ts +1 -1
  93. package/dist/render/shaders/particle.d.ts +3 -3
  94. package/dist/render/shaders/rush.js +54 -11
  95. package/dist/render/shaders/scatter.d.ts +1 -1
  96. package/dist/render/shaders/smoke.d.ts +1 -1
  97. package/dist/render/shaders/ssrResolve.d.ts +17 -0
  98. package/dist/render/shaders/ssrResolve.js +51 -0
  99. package/dist/render/shaders/ssrTrace.js +26 -0
  100. package/dist/render/shaders/water.d.ts +1 -1
  101. package/dist/render/ssrPass.d.ts +16 -0
  102. package/dist/render/ssrPass.js +106 -1
  103. package/dist/render/surfaceOverlay.d.ts +132 -0
  104. package/dist/render/surfaceOverlay.js +85 -0
  105. package/dist/render/surfaceTexture.d.ts +19 -0
  106. package/dist/render/surfaceTexture.js +69 -0
  107. package/dist/render/temporalAa.d.ts +14 -0
  108. package/dist/render/temporalAa.js +20 -0
  109. package/dist/render/vertexDefaults.d.ts +17 -0
  110. package/dist/render/vertexDefaults.js +24 -0
  111. package/package.json +6 -6
  112. package/src/index.ts +16 -0
  113. package/src/render/atmosphere.ts +22 -1
  114. package/src/render/backend/api.ts +9 -0
  115. package/src/render/backend/createRenderer.ts +18 -1
  116. package/src/render/backend/webgl2/renderer.ts +357 -13
  117. package/src/render/backend/webgpu/device.ts +42 -3
  118. package/src/render/backend/webgpu/flatPass.ts +63 -26
  119. package/src/render/backend/webgpu/postPass.ts +18 -0
  120. package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
  121. package/src/render/backend/webgpu/renderer.ts +735 -83
  122. package/src/render/displayRange.ts +61 -0
  123. package/src/render/filmicCurve.ts +229 -0
  124. package/src/render/fog.ts +19 -3
  125. package/src/render/frame/resources.ts +6 -0
  126. package/src/render/frameReflections.ts +72 -0
  127. package/src/render/pass.ts +53 -1
  128. package/src/render/reflectionSurfaceTargets.ts +105 -0
  129. package/src/render/renderQuality.ts +44 -8
  130. package/src/render/sceneTarget.ts +30 -2
  131. package/src/render/screenSpaceReflection.ts +5 -0
  132. package/src/render/shaders/filmLook.ts +2 -1
  133. package/src/render/shaders/flat/glassTint.ts +5 -1
  134. package/src/render/shaders/flat/index.ts +16 -0
  135. package/src/render/shaders/flat/main.ts +28 -0
  136. package/src/render/shaders/flat/overlay.ts +117 -0
  137. package/src/render/shaders/flat/pointShadow.ts +1 -1
  138. package/src/render/shaders/flat/reflectionSurface.ts +61 -0
  139. package/src/render/shaders/fog.ts +9 -2
  140. package/src/render/shaders/fringe.ts +59 -0
  141. package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
  142. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  143. package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
  144. package/src/render/shaders/generated/film.wgsl.ts +1 -1
  145. package/src/render/shaders/generated/fire.wgsl.ts +1 -1
  146. package/src/render/shaders/generated/flat.wgsl.ts +214 -54
  147. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  148. package/src/render/shaders/generated/particle.wgsl.ts +4 -4
  149. package/src/render/shaders/generated/rush.wgsl.ts +56 -26
  150. package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
  151. package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
  152. package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
  153. package/src/render/shaders/generated/ssrTrace.wgsl.ts +22 -2
  154. package/src/render/shaders/generated/water.wgsl.ts +1 -1
  155. package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
  156. package/src/render/shaders/rush.ts +54 -11
  157. package/src/render/shaders/ssrResolve.ts +52 -0
  158. package/src/render/shaders/ssrTrace.ts +26 -0
  159. package/src/render/ssrPass.ts +138 -1
  160. package/src/render/surfaceOverlay.ts +195 -0
  161. package/src/render/surfaceTexture.ts +67 -0
  162. package/src/render/temporalAa.ts +20 -0
  163. package/src/render/vertexDefaults.ts +28 -0
@@ -187,7 +187,19 @@ import {
187
187
  packModel,
188
188
  skinProfileDistance,
189
189
  } from '../../surfaceModel.ts';
190
- import type { SurfaceModel } from '../../surfaceModel.ts';
190
+ import type { SurfaceModel, SurfaceModelKind } from '../../surfaceModel.ts';
191
+ import {
192
+ OVERLAY_FLOATS,
193
+ overlayLays,
194
+ packSurfaceOverlay,
195
+ type SurfaceOverlay,
196
+ } from '../../surfaceOverlay.ts';
197
+ import {
198
+ frameReflectionsRefused,
199
+ resolveFrameReflections,
200
+ type ResolvedFrameReflections,
201
+ } from '../../frameReflections.ts';
202
+ import { ReflectionSurfacePass } from './reflectionSurfacePass.ts';
191
203
  import { validateClothBinding } from '../../clothBindingData.ts';
192
204
  import type { ClothBindingData } from '../../clothBindingData.ts';
193
205
  import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
@@ -216,7 +228,24 @@ import type { SurfaceMaterial, SurfaceTextureOptions } from '../../surfaceTextur
216
228
  import type { RenderBackend } from '../api.ts';
217
229
  import { reflectionTargetSize, type ReflectionSize } from '../../planarReflectionDraw.ts';
218
230
  import { drawingBufferSize } from '../../drawingBuffer.ts';
219
- import { MOTION_BLUR_MAX_UV, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV } from '../../vertexDefaults.ts';
231
+ import {
232
+ MOTION_BLUR_MAX_UV,
233
+ OBJECT_BLUR_UNKEPT,
234
+ OUTPUT_TRANSFORM_CODE,
235
+ RUSH_REACH_UV,
236
+ clampMotionShare,
237
+ forwardTransformCode,
238
+ } from '../../vertexDefaults.ts';
239
+ import { FRINGE_FLOATS, resolveFringe } from '../../shaders/fringe.ts';
240
+ import { displayHeadroom, type DisplayRange } from '../../displayRange.ts';
241
+ import {
242
+ DEFAULT_FILMIC_CURVE,
243
+ FILMIC_CLAMPED,
244
+ FILMIC_CONSTANTS,
245
+ FILMIC_WITHOUT_COMPOSITE,
246
+ resolveFilmicCurve,
247
+ type FilmicCurve,
248
+ } from '../../filmicCurve.ts';
220
249
  import { ExposurePass } from './exposurePass.ts';
221
250
  import { AO_STORE } from '../../shaders/ambientOcclusion.ts';
222
251
  import { clampAutoExposure } from '../../shaders/exposure.ts';
@@ -281,9 +310,15 @@ import {
281
310
  DECAL_TEXTURES,
282
311
  DECAL_UNIFORMS,
283
312
  PREMULTIPLIED_OVER_BLEND,
313
+ FRAME_REFLECTION_BLEND,
284
314
  REFLECTION_OVER_BLEND,
285
315
  SSR_FRAG_FIELDS,
286
316
  SSR_FRAG_SIZE,
317
+ SSR_MATERIAL_FIELDS,
318
+ SSR_MATERIAL_RESOLVE_FRAG_WGSL,
319
+ SSR_MATERIAL_SIZE,
320
+ SSR_MATERIAL_TEXTURES,
321
+ SSR_MATERIAL_UNIFORMS,
287
322
  SSR_RESOLVE_FRAG_WGSL,
288
323
  SSR_RESOLVE_TEXTURES,
289
324
  SSR_TEXTURES,
@@ -392,6 +427,7 @@ import {
392
427
  type FlatVariant,
393
428
  flatPipeline,
394
429
  flatPipelineAsync,
430
+ probeStageCeilings,
395
431
  } from './flatPass.ts';
396
432
  import type { OitTarget } from './flatPass.ts';
397
433
  import {
@@ -694,34 +730,20 @@ const MAX_SKY_DRAWS = 64;
694
730
  /**
695
731
  * Whether the environment probe's permutation fits this device's per-stage binding limits.
696
732
  *
697
- * The widest flat variant declares one albedo, three directional shadow maps, twelve
698
- * point-shadow cubes and the probe: seventeen textures and seventeen samplers. WebGPU's default
699
- * is sixteen of each and an adapter is free to offer no more — this one offers 48 textures and
700
- * exactly 16 samplers, so the samplers are what decide it.
733
+ * **Two ceilings, counted separately**: sampled textures and samplers, since textures that share a
734
+ * sampler (`// wgsl:share` in `flat/preamble.ts`) count once against the second. The widest variant
735
+ * binds sixteen textures and twelve samplers, which is exactly WebGPU's default texture ceiling, and
736
+ * `select.ts` asks for the adapter's own — 48 here — so the probe does not depend on that request.
701
737
  */
702
738
  function probeFits(device: GPUDevice): boolean {
703
739
  /*
704
- * **Two ceilings, counted separately, and they are no longer the same number.**
705
- *
706
- * The widest permutation declares seventeen sampled textures — one albedo, three directional
707
- * shadow maps, the point-shadow array, a normal map and the probe's own cube — **seven**, where
708
- * it was seventeen while every point light's shadow was a `samplerCube` of its own. `select.ts` asks
709
- * for the adapter's ceiling, which is 48 on this machine against a device default of 16, so
710
- * six fits everywhere and the probe no longer depends on the request being granted.
711
- *
712
- * It used to declare seventeen *samplers* too, and that was the binding constraint: this
713
- * adapter offers exactly sixteen and no request can raise it, so the probe stood down and
714
- * reflective surfaces kept a gradient. The shadow bindings share one declaration — see
715
- * `// wgsl:share shadow` in `flat/preamble.ts`, which costs nothing because they were already
716
- * one object — so the count is four, and the fourth is the normal map's: it is a colour image
717
- * and wants filtering, which a non-filtering shadow sampler cannot give it.
718
- *
719
- * **Counted from the generated bindings rather than typed**: `FLAT_BINDINGS.flatFrag`'s
720
- * widest variant is `directionalShadows+environmentProbe+pointShadows`, seven textures and
721
- * four samplers. A stale number here reads as a device declining a feature it can run.
722
- */
723
- const TEXTURES = 7;
724
- const SAMPLERS = 4;
740
+ * **Counted from the generated bindings rather than typed** (`probeStageCeilings`). It was typed
741
+ * here as seven textures and four samplers while the widest variant grew to sixteen and twelve
742
+ * with the material maps, glass, the fixture atlas and DriftLight: a stale number reads as a
743
+ * device declining a feature it can run, or, the way this one had drifted, as one admitted to a
744
+ * permutation it cannot bind.
745
+ */
746
+ const { textures: TEXTURES, samplers: SAMPLERS } = probeStageCeilings();
725
747
  const fits =
726
748
  device.limits.maxSampledTexturesPerShaderStage >= TEXTURES &&
727
749
  device.limits.maxSamplersPerShaderStage >= SAMPLERS;
@@ -789,6 +811,7 @@ const VERB_PASS = 3;
789
811
  /** What the depth snapshot copies from, and what it copies into. */
790
812
  const DEPTH_SOURCE = maskOf('sceneDepth');
791
813
  const DEPTH_SNAPSHOT = maskOf('depthSnapshot');
814
+ const COLOR_SNAPSHOT = maskOf('colorSnapshot');
792
815
 
793
816
  /** The mirror's own colour and depth. */
794
817
  const MIRROR_TARGET = maskOf('mirrorColor', 'mirrorDepth');
@@ -1919,6 +1942,17 @@ export class WebGPURenderer implements RendererApi {
1919
1942
  */
1920
1943
  private refractSnapshot: GPUTexture | null = null;
1921
1944
  private refractSnapshotView: GPUTextureView | null = null;
1945
+ /**
1946
+ * The frame's colour as a registered pass reads it (`reads: ['colorSnapshot']`): the scene's own
1947
+ * size and format, refilled at each such pass's draw. Allocated only once a pass declares the
1948
+ * read, beside the frame's targets, and never in the frame loop. See `takePassColour`.
1949
+ */
1950
+ private passColour: GPUTexture | null = null;
1951
+ private passColourView: GPUTextureView | null = null;
1952
+ /** Registered passes that read the colour, queued this frame for after the upscale. */
1953
+ private readonly latePasses: PassHandle[] = [];
1954
+ /** A late pass draws unjittered, as every late draw does. */
1955
+ private readonly latePassJitter = new Float32Array(2);
1922
1956
  /** Whether this frame has taken it. One snapshot serves every refracting draw. */
1923
1957
  private refractSnapshotTaken = false;
1924
1958
  /**
@@ -2429,8 +2463,18 @@ export class WebGPURenderer implements RendererApi {
2429
2463
  private readonly reflectionQueue = new ReflectionQueue();
2430
2464
  private ssrLayout!: GPUBindGroupLayout;
2431
2465
  private ssrResolveLayout!: GPUBindGroupLayout;
2466
+ /** The frame's materials' reflections, resolved; null where off. See `frameReflections.ts`. */
2467
+ private readonly frameReflections: ResolvedFrameReflections | null;
2468
+ /** Their surface half, null where they are off or cannot be drawn: `reflectionSurfacePass.ts`. */
2469
+ private readonly reflectionSurface: ReflectionSurfacePass | null;
2470
+ /** The material resolve: its layout, its one block (the blur) and its group over the three maps. */
2471
+ private ssrMaterialLayout!: GPUBindGroupLayout;
2472
+ private ssrMaterialUniforms!: GPUBuffer;
2473
+ private readonly ssrMaterialFloats = new Float32Array(SSR_MATERIAL_SIZE / 4);
2474
+ private ssrMaterialGroup: GPUBindGroup | null = null;
2432
2475
  private ssrUniforms!: GPUBuffer;
2433
- private readonly ssrStaging = new ArrayBuffer(SSR_SLOT * MAX_REFLECTIVE_SURFACES);
2476
+ /* One slot a surface, and one more for the frame's materials' trace. */
2477
+ private readonly ssrStaging = new ArrayBuffer(SSR_SLOT * (MAX_REFLECTIVE_SURFACES + 1));
2434
2478
  private readonly ssrFloats = new Float32Array(this.ssrStaging);
2435
2479
  private ssrReflection: GPUTexture | null = null;
2436
2480
  private ssrReflectionView: GPUTextureView | null = null;
@@ -3000,8 +3044,15 @@ export class WebGPURenderer implements RendererApi {
3000
3044
  const { device } = this.surface;
3001
3045
  const { depth, colorMsaa } = target.attachments(this.samples, this.pipelines.format);
3002
3046
 
3003
- const savedPass = this.pass;
3047
+ /*
3048
+ * **Flushed, and only then set aside.** The flush replays what the frame has recorded, and
3049
+ * replaying opens the frame's pass where none is open yet; saved before it, the pass given back
3050
+ * afterwards was the null that stood there, while the one the flush opened was left open on the
3051
+ * frame's encoder and every later pass on it was refused — a black frame for any capture taken
3052
+ * after the frame had drawn something. `scripts/capture-check.mjs` takes one at three points.
3053
+ */
3004
3054
  if (this.quality.frameGraph) this.flushGraph();
3055
+ const savedPass = this.pass;
3005
3056
  const savedEncoder = this.encoder;
3006
3057
  this.markRings();
3007
3058
  const skinMark = this.skinPalettes.mark();
@@ -3105,6 +3156,7 @@ export class WebGPURenderer implements RendererApi {
3105
3156
  if (this.ormMap === texture) this.ormMap = null;
3106
3157
  if (this.emissiveMap === texture) this.emissiveMap = null;
3107
3158
  if (this.modelMap === texture) this.modelMap = null;
3159
+ if (this.overlayMaps === texture) this.overlayMaps = null;
3108
3160
  this.forgetBindingsOf(texture);
3109
3161
  texture.dispose();
3110
3162
  this.captureTargets.get(texture)?.dispose();
@@ -3123,6 +3175,7 @@ export class WebGPURenderer implements RendererApi {
3123
3175
  /* Same reasoning, for whichever SDF labels were bound against this atlas. */
3124
3176
  this.sdfTextBindGroups.delete(texture);
3125
3177
  this.flatBindGroups.clear();
3178
+ this.overlayGroups.clear();
3126
3179
  /*
3127
3180
  * The blank one is built with the albedo held aside, because `buildFlatBindGroup` reads
3128
3181
  * `this.albedo` for that slot — leaving it in place would put a real colour map into the
@@ -3516,6 +3569,20 @@ export class WebGPURenderer implements RendererApi {
3516
3569
  this.material((f) => f.set(this.ambientSH, this.materialField('uAmbientSH')));
3517
3570
  }
3518
3571
 
3572
+ /**
3573
+ * The overlay the following draws wear, or null for none: pass state, put back to none by
3574
+ * `bindMeshPass`. See `renderer.ts` and `surfaceOverlay.ts`. The numbers go in the material block,
3575
+ * which every variant declares; the atlas goes in a twin of each flat group (`overlayGroup`).
3576
+ */
3577
+ setSurfaceOverlay(overlay: SurfaceOverlay | null): void {
3578
+ if (this.surface.lost) return;
3579
+ packSurfaceOverlay(overlay, this.overlayFloats);
3580
+ /* The first overlay laid is when the lit pipelines start carrying one. */
3581
+ if (overlayLays(overlay)) this.enableLit('SURFACE_OVERLAY');
3582
+ this.overlayMaps = (overlay?.maps ?? null) as GpuSurfaceTexture | null;
3583
+ this.material((f) => f.set(this.overlayFloats, this.materialField('uOverlay')));
3584
+ }
3585
+
3519
3586
  /** How reflective the surfaces drawn next are. Clamped, as `renderer.ts` clamps it. */
3520
3587
  setSurfaceReflectivity(amount: number): void {
3521
3588
  this.material((f) => {
@@ -4260,7 +4327,7 @@ export class WebGPURenderer implements RendererApi {
4260
4327
  */
4261
4328
  private gradeCode(): number {
4262
4329
  return !this.hasComposite || !this.quality.hdrScene
4263
- ? (OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0)
4330
+ ? forwardTransformCode(this.quality.outputTransform)
4264
4331
  : 0;
4265
4332
  }
4266
4333
 
@@ -6579,6 +6646,10 @@ export class WebGPURenderer implements RendererApi {
6579
6646
  this.skinPalettes = new SkinPaletteRing(quality.drawsPerFrame);
6580
6647
  this.maxDrawingBufferPixels = quality.maxDrawingBufferPixels;
6581
6648
  this.exposure = quality.outputExposure;
6649
+ resolveFilmicCurve(DEFAULT_FILMIC_CURVE, this.filmConstants);
6650
+ if (quality.outputTransform === 'filmic' && !(quality.screenEffects && quality.hdrScene)) {
6651
+ console.warn(FILMIC_WITHOUT_COMPOSITE);
6652
+ }
6582
6653
  this.flushSchedule =
6583
6654
  quality.frameGraph && quality.identifierGraph
6584
6655
  ? createFlushSchedule(MAX_DRAWS_PER_FRAME)
@@ -6656,6 +6727,26 @@ export class WebGPURenderer implements RendererApi {
6656
6727
  );
6657
6728
  }
6658
6729
  this.skinScatter = skinScreen ? new SkinScatterPass(device) : null;
6730
+ /*
6731
+ * The frame's materials' reflections, where the frame can carry them: scene light to swap the
6732
+ * probe's share in, and one sample, which the trace's depth copy and both maps are. Said once
6733
+ * where it cannot. See `frameReflections.ts`.
6734
+ */
6735
+ this.frameReflections = resolveFrameReflections(quality.screenSpaceReflections);
6736
+ const reflectionsRefusal =
6737
+ this.frameReflections === null
6738
+ ? null
6739
+ : !quality.screenEffects || !quality.hdrScene
6740
+ ? 'the frame keeps no scene light to swap the probe in (`screenEffects` and `hdrScene`)'
6741
+ : this.samples > 1
6742
+ ? 'a multisampled frame has no single depth or colour to march through'
6743
+ : null;
6744
+ if (reflectionsRefusal !== null)
6745
+ console.warn(`WebGPU: ${frameReflectionsRefused(reflectionsRefusal)}`);
6746
+ this.reflectionSurface =
6747
+ this.frameReflections !== null && reflectionsRefusal === null
6748
+ ? new ReflectionSurfacePass(device)
6749
+ : null;
6659
6750
  this.skinPipelines = skinScreen
6660
6751
  ? new PipelineCache(
6661
6752
  device,
@@ -7046,6 +7137,11 @@ export class WebGPURenderer implements RendererApi {
7046
7137
  addressModeU: 'clamp-to-edge',
7047
7138
  addressModeV: 'clamp-to-edge',
7048
7139
  });
7140
+ /* Or a surface overlay's atlas, in the twin a draw wearing one binds: `overlayGroup`. */
7141
+ const maps = this.overlayBinding ? this.overlayMaps : null;
7142
+ if (maps !== null) {
7143
+ return { view: maps.layerView() ?? blankView, sampler: this.refractSampler };
7144
+ }
7049
7145
  return { view: this.refractSnapshotView ?? blankView, sampler: this.refractSampler };
7050
7146
  }
7051
7147
  /*
@@ -7222,6 +7318,8 @@ export class WebGPURenderer implements RendererApi {
7222
7318
  { binding: RUSH_TEXTURES.uExposureHeld },
7223
7319
  /* Local exposure's grid, read filtered between tiles. See `localExposure.ts`. */
7224
7320
  { binding: RUSH_TEXTURES.uExposureLocal },
7321
+ /* Each mover's own motion: the reconstruction's target, or the scene where there is none. */
7322
+ { binding: RUSH_TEXTURES.uMotion },
7225
7323
  ]);
7226
7324
  this.rushUniforms = device.createBuffer({
7227
7325
  label: 'post.rushUniforms',
@@ -7239,11 +7337,29 @@ export class WebGPURenderer implements RendererApi {
7239
7337
  this.ssrLayout = createPostStageLayout(device, SSR_UNIFORMS, SSR_FRAG_SIZE, [
7240
7338
  { binding: SSR_TEXTURES.uSsrDepth, filterable: false },
7241
7339
  { binding: SSR_TEXTURES.uSsrScene },
7340
+ /* The frame's materials, for the trace that reads them: `frameReflections.ts`. */
7341
+ { binding: SSR_TEXTURES.uSsrProbeMap },
7342
+ { binding: SSR_TEXTURES.uSsrTintMap },
7242
7343
  ]);
7243
7344
  this.ssrResolveLayout = createSsrResolveLayout(device);
7345
+ this.ssrMaterialLayout = createPostStageLayout(
7346
+ device,
7347
+ SSR_MATERIAL_UNIFORMS,
7348
+ SSR_MATERIAL_SIZE,
7349
+ [
7350
+ { binding: SSR_MATERIAL_TEXTURES.uSsrReflection },
7351
+ { binding: SSR_MATERIAL_TEXTURES.uSsrProbeMap },
7352
+ { binding: SSR_MATERIAL_TEXTURES.uSsrTintMap },
7353
+ ],
7354
+ );
7355
+ this.ssrMaterialUniforms = device.createBuffer({
7356
+ label: 'post.ssrMaterialUniforms',
7357
+ size: SSR_MATERIAL_SIZE,
7358
+ usage: USAGE_UNIFORM_DST,
7359
+ });
7244
7360
  this.ssrUniforms = device.createBuffer({
7245
7361
  label: 'post.ssrUniforms',
7246
- size: SSR_SLOT * MAX_REFLECTIVE_SURFACES,
7362
+ size: SSR_SLOT * (MAX_REFLECTIVE_SURFACES + 1),
7247
7363
  usage: USAGE_UNIFORM_DST,
7248
7364
  });
7249
7365
  this.decalUniforms = device.createBuffer({
@@ -7947,7 +8063,11 @@ export class WebGPURenderer implements RendererApi {
7947
8063
  /* Last frame's snapshot is last frame's scene. */
7948
8064
  this.refractSnapshotTaken = false;
7949
8065
  this.skinScattered = false;
8066
+ /* One frame of the jitter's sequence however many times the frame binds its camera. */
8067
+ this.temporalHistory.nextFrame();
8068
+ this.latePasses.length = 0;
7950
8069
  this.skinScatter?.reset();
8070
+ this.reflectionSurface?.reset();
7951
8071
  this.plumeVerts.reset();
7952
8072
  this.plumeFrags.reset();
7953
8073
  this.waterVerts.reset();
@@ -8557,6 +8677,9 @@ export class WebGPURenderer implements RendererApi {
8557
8677
  f[at('uWriteMode') + 1] = 0;
8558
8678
  /* 1, not 0: a multiplier's identity is one. See `setEnvironmentGain`. */
8559
8679
  f[at('uEnvironmentDials') + 2] = 1;
8680
+ /* No overlay until a draw sets one. See `setSurfaceOverlay`. */
8681
+ this.overlayFloats.fill(0);
8682
+ f.fill(0, at('uOverlay'), at('uOverlay') + OVERLAY_FLOATS);
8560
8683
  /* The frame's own ambient until a draw asks for its own. See `setAmbientSH`. */
8561
8684
  f.fill(0, at('uAmbientSH'), at('uAmbientSH') + AMBIENT_SH_FLOATS);
8562
8685
  /*
@@ -8949,6 +9072,8 @@ export class WebGPURenderer implements RendererApi {
8949
9072
  this.emissiveMap = null;
8950
9073
  this.modelMap = null;
8951
9074
  this.bindGroup = this.blankAlbedoBindGroup;
9075
+ /* Nor an overlay; its numbers are zeroed with the block's other defaults. */
9076
+ this.overlayMaps = null;
8952
9077
  this.uvScale[0] = 1;
8953
9078
  this.uvScale[1] = 1;
8954
9079
  this.uvOffset[0] = 0;
@@ -9036,6 +9161,9 @@ export class WebGPURenderer implements RendererApi {
9036
9161
  * where it happened to pass, write motion for a surface no pixel of the frame belongs to.
9037
9162
  */
9038
9163
  if (this.tracksMotionNow()) this.recordDrawMotion(mesh, model, previousModel);
9164
+ else if (previousModel !== null && this.quality.cameraMotionBlur * this.motionBlurScale > 0) {
9165
+ this.warnObjectBlur();
9166
+ }
9039
9167
  noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
9040
9168
  /* An opaque draw meets the medium as the dial says: `submitMesh` reads an absent `fog` as
9041
9169
  true, which is a translucent draw's default and would put every opaque draw back in the fog
@@ -9455,6 +9583,26 @@ export class WebGPURenderer implements RendererApi {
9455
9583
  material,
9456
9584
  );
9457
9585
  }
9586
+ /* And its surface half, for the frame's materials' reflections. See `keepReflectionSurface`. */
9587
+ if (!blend && lit && depthWrite && this.reflectionSurfaceKeeps()) {
9588
+ this.keepReflectionSurface(
9589
+ mesh,
9590
+ key,
9591
+ base,
9592
+ skinned,
9593
+ morphed,
9594
+ deltas,
9595
+ layer,
9596
+ eight,
9597
+ clothed,
9598
+ modelKind,
9599
+ slot,
9600
+ material,
9601
+ null,
9602
+ 1,
9603
+ null,
9604
+ );
9605
+ }
9458
9606
  }
9459
9607
  /* Back to the defaults for whatever is drawn next, exactly as `renderer.ts` restores them —
9460
9608
  unconditionally, because the scratch state above is shared by every draw after this one
@@ -9606,6 +9754,7 @@ export class WebGPURenderer implements RendererApi {
9606
9754
  const refraction = options.refraction ?? 0;
9607
9755
  const glassy = resolveGlass(options.glass, this.glassScratch);
9608
9756
  if ((refraction <= 0 && !glassy) || !this.takeRefractSnapshot()) return false;
9757
+ this.seeThroughBound = true;
9609
9758
  const glass = this.glassScratch;
9610
9759
  /* Strength, path thickness, transmission, frost: one vector, for the budget the shader states. */
9611
9760
  const seeAt = this.materialField('uSeeThrough');
@@ -9627,6 +9776,7 @@ export class WebGPURenderer implements RendererApi {
9627
9776
 
9628
9777
  /** Put the scratch back after `bindSeeThrough`: refraction and glass off. */
9629
9778
  private releaseSeeThrough(): void {
9779
+ this.seeThroughBound = false;
9630
9780
  const seeAt = this.materialField('uSeeThrough');
9631
9781
  this.perFrameFloats[seeAt] = 0;
9632
9782
  this.perFrameFloats[seeAt + 2] = 0;
@@ -9827,6 +9977,26 @@ export class WebGPURenderer implements RendererApi {
9827
9977
  if (cut === null) pass.drawIndexed(mesh.indexCount, count);
9828
9978
  else pass.drawIndexedIndirect(cut.args, 0);
9829
9979
  }
9980
+ /* And its surface half, for the frame's materials' reflections, with the same survivors. */
9981
+ if (!blend && lit && depthWrite && this.reflectionSurfaceKeeps()) {
9982
+ this.keepReflectionSurface(
9983
+ mesh,
9984
+ key,
9985
+ base,
9986
+ false,
9987
+ false,
9988
+ null,
9989
+ layer,
9990
+ false,
9991
+ false,
9992
+ modelKind,
9993
+ slot,
9994
+ material,
9995
+ instanceBuffer,
9996
+ count,
9997
+ cut === null ? null : cut.args,
9998
+ );
9999
+ }
9830
10000
  }
9831
10001
  /* Back to the defaults, unconditionally and for the reason `drawMesh` gives at length: this
9832
10002
  scratch block is cumulative state shared by every draw after this one, and an early return
@@ -10534,6 +10704,20 @@ export class WebGPURenderer implements RendererApi {
10534
10704
  private warnedNoPointShadowArray = false;
10535
10705
  /** Said once, for the retired ambient dial. */
10536
10706
  private warnedEnvironmentAmbient = false;
10707
+ /** The overlay the draws wear, `uOverlay`: see `setSurfaceOverlay`. */
10708
+ private readonly overlayFloats = new Float32Array(OVERLAY_FLOATS);
10709
+ /** Its atlas, bound where the refraction copy goes in a twin of each flat group. */
10710
+ private overlayMaps: GpuSurfaceTexture | null = null;
10711
+ /**
10712
+ * Each flat group's twin with the atlas in the refraction slot, by the group and the atlas, built
10713
+ * on the first draw that wears it and dropped wherever `flatBindGroups` is.
10714
+ */
10715
+ private readonly overlayGroups = new Map<GPUBindGroup, Map<GpuSurfaceTexture, GPUBindGroup>>();
10716
+ /** True while a twin is built: what `flatTextures` hands the refraction slot then. */
10717
+ private overlayBinding = false;
10718
+ /** True between `bindSeeThrough` and `releaseSeeThrough`: that draw reads the copy, not the atlas. */
10719
+ private seeThroughBound = false;
10720
+ private warnedOverlayRefraction = false;
10537
10721
  private readonly castingWorldIndices = new Int32Array(POINT_SHADOW_POOL).fill(-1);
10538
10722
  private readonly resolvedPointShadows = createResolvedPointShadows(MAX_POINT_LIGHTS);
10539
10723
  /** One face's view-projection, rebuilt per face rather than allocated. */
@@ -10982,6 +11166,43 @@ export class WebGPURenderer implements RendererApi {
10982
11166
  );
10983
11167
  }
10984
11168
 
11169
+ /**
11170
+ * The open material's flat group, or its twin with the surface overlay's atlas in the refraction
11171
+ * slot where the draw wears one and does not refract. A twin is built from the live material,
11172
+ * which `bindGroup` always is, and kept by the group and the atlas.
11173
+ *
11174
+ * **A draw that refracts, or is glass, keeps the frame's copy** and its overlay reads no image —
11175
+ * the shader asks the same of `uSeeThrough` (`overlayImagesBound`) — said once.
11176
+ */
11177
+ private overlayGroup(): GPUBindGroup {
11178
+ const maps = this.overlayMaps;
11179
+ if (maps === null) return this.bindGroup;
11180
+ if (this.seeThroughBound) {
11181
+ if (!this.warnedOverlayRefraction) {
11182
+ this.warnedOverlayRefraction = true;
11183
+ console.warn(
11184
+ 'WebGPU: a draw that refracts or is glass wears a surface overlay with maps. The frame ' +
11185
+ "copy it reads takes the overlay's slot, so its rim, dissolve and wrinkles read no image.",
11186
+ );
11187
+ }
11188
+ return this.bindGroup;
11189
+ }
11190
+ let byMaps = this.overlayGroups.get(this.bindGroup);
11191
+ if (byMaps === undefined) {
11192
+ byMaps = new Map();
11193
+ this.overlayGroups.set(this.bindGroup, byMaps);
11194
+ }
11195
+ let twin = byMaps.get(maps);
11196
+ if (twin === undefined) {
11197
+ this.overlayBinding = true;
11198
+ twin = this.buildFlatBindGroup();
11199
+ this.overlayBinding = false;
11200
+ this.bindGroupBudget.ask();
11201
+ byMaps.set(maps, twin);
11202
+ }
11203
+ return twin;
11204
+ }
11205
+
10985
11206
  /**
10986
11207
  * The bind group a draw uses, skinned or not.
10987
11208
  *
@@ -10998,7 +11219,8 @@ export class WebGPURenderer implements RendererApi {
10998
11219
  morphed: boolean,
10999
11220
  deltas: GPUTextureView | null,
11000
11221
  ): GPUBindGroup {
11001
- if (!skinned && !morphed) return this.bindGroup;
11222
+ const base = this.overlayGroup();
11223
+ if (!skinned && !morphed) return base;
11002
11224
  const palette = skinned ? this.skinPalettes.view(this.skinPaletteSlot) : null;
11003
11225
  /* Slot `k` caches at `k + 1`; index 0 is the morphed draw that is not skinned and has no
11004
11226
  slot of its own. See `skinnedGroups`. */
@@ -11007,12 +11229,14 @@ export class WebGPURenderer implements RendererApi {
11007
11229
  const cloth = skinned ? this.clothViews() : null;
11008
11230
  if (
11009
11231
  (this.skinnedGroups[at] ?? null) === null ||
11010
- this.skinnedGroupsBeside[at] !== this.bindGroup ||
11232
+ this.skinnedGroupsBeside[at] !== base ||
11011
11233
  this.skinnedGroupsPalette[at] !== palette ||
11012
11234
  this.skinnedGroupsDeltas[at] !== deltas ||
11013
11235
  this.skinnedGroupsCloth[at] !== (cloth?.particles ?? null) ||
11014
11236
  this.skinnedGroupsClothBinding[at] !== (cloth?.binding ?? null)
11015
11237
  ) {
11238
+ /* Built from the live material, with the overlay's atlas where `base` is its twin. */
11239
+ this.overlayBinding = base !== this.bindGroup;
11016
11240
  this.skinnedGroups[at] = createFlatBindGroup(
11017
11241
  this.surface.device,
11018
11242
  this.flatLayoutFor(skinned, morphed),
@@ -11024,7 +11248,8 @@ export class WebGPURenderer implements RendererApi {
11024
11248
  deltas,
11025
11249
  cloth,
11026
11250
  );
11027
- this.skinnedGroupsBeside[at] = this.bindGroup;
11251
+ this.overlayBinding = false;
11252
+ this.skinnedGroupsBeside[at] = base;
11028
11253
  this.skinnedGroupsPalette[at] = palette;
11029
11254
  this.skinnedGroupsDeltas[at] = deltas;
11030
11255
  this.skinnedGroupsCloth[at] = cloth?.particles ?? null;
@@ -11090,6 +11315,7 @@ export class WebGPURenderer implements RendererApi {
11090
11315
  */
11091
11316
  private rebuildFlatGroupsForNewRings(): void {
11092
11317
  this.flatBindGroups.clear();
11318
+ this.overlayGroups.clear();
11093
11319
  this.flatEnvironment = this.flatTextures('uEnvironment').view;
11094
11320
  this.skinnedGroups.length = 0;
11095
11321
 
@@ -11153,6 +11379,7 @@ export class WebGPURenderer implements RendererApi {
11153
11379
  */
11154
11380
  private rebuildFlatBindGroup(): void {
11155
11381
  this.flatBindGroups.clear();
11382
+ this.overlayGroups.clear();
11156
11383
  this.flatEnvironment = this.flatTextures('uEnvironment').view;
11157
11384
  if (
11158
11385
  this.albedo === null &&
@@ -11394,9 +11621,14 @@ export class WebGPURenderer implements RendererApi {
11394
11621
  ? (OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0)
11395
11622
  : 0;
11396
11623
  f[at('uOutputExposure')] = graded ? this.exposure : 1;
11624
+ f.set(this.filmA, at('uFilmA'));
11625
+ f.set(this.filmB, at('uFilmB'));
11626
+ f[at('uDisplayHeadroom')] = this.headroom;
11397
11627
  f.set(this.reprojection, at('uReprojection'));
11398
11628
  f[at('uMotionStrength')] = motionStrength;
11399
- f[at('uMotionMax')] = MOTION_BLUR_MAX_UV;
11629
+ f[at('uMotionMax')] = this.motionBlurMax;
11630
+ /* Each mover's own motion, where the reconstruction drew its target this frame. */
11631
+ f[at('uObjectMotion')] = this.reconstructing && this.reconMotionView !== null ? 1 : 0;
11400
11632
  f[at('uAoStrength')] = this.frameProjection === null ? 0 : aoStrength;
11401
11633
  /*
11402
11634
  * The occlusion read where this frame's jitter put it (`uAoOffset`): measured from the jittered
@@ -11440,6 +11672,7 @@ export class WebGPURenderer implements RendererApi {
11440
11672
  f[at('uGradeSize')] = this.gradeSize;
11441
11673
  /* The lens and the print. See `setVignette` and `setFilmGrain`. */
11442
11674
  f[at('uVignette')] = this.vignette;
11675
+ f.set(this.fringe, at('uFringe'));
11443
11676
  f[at('uAutoExposure')] = adapting ? this.autoExposure : 0;
11444
11677
  f[at('uLocalExposure')] = measuring && localising ? this.localExposure : 0;
11445
11678
  f[at('uGrain')] = this.grain;
@@ -11485,7 +11718,11 @@ export class WebGPURenderer implements RendererApi {
11485
11718
  private rebuildRushBindGroup(): void {
11486
11719
  if (this.sceneColorView === null || this.resolvedDepthView === null) return;
11487
11720
  if (this.aoTargetView === null) return;
11488
- this.rushBindGroup = this.buildRushBindGroup('post.rushBindGroup', this.sceneColorView);
11721
+ this.rushBindGroup = this.buildRushBindGroup(
11722
+ 'post.rushBindGroup',
11723
+ this.sceneColorView,
11724
+ this.sceneColorView,
11725
+ );
11489
11726
  /*
11490
11727
  * **And the reconstruction's twin, from the same three call sites.** A colour grade replaces
11491
11728
  * the lookup table and a resize replaces the depth and the occlusion target, and both groups
@@ -11493,12 +11730,21 @@ export class WebGPURenderer implements RendererApi {
11493
11730
  * come to read a stale view on exactly the frames a consumer changed something.
11494
11731
  */
11495
11732
  if (this.reconShownView !== null) {
11496
- this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
11733
+ this.reconRushGroup = this.buildRushBindGroup(
11734
+ 'recon.rushBindGroup',
11735
+ this.reconShownView,
11736
+ this.reconMotionView ?? this.reconShownView,
11737
+ );
11497
11738
  }
11498
11739
  }
11499
11740
 
11500
11741
  /** The composite's group over one scene source. See `rebuildRushBindGroup`. */
11501
- private buildRushBindGroup(label: string, sceneView: GPUTextureView): GPUBindGroup | null {
11742
+ private buildRushBindGroup(
11743
+ label: string,
11744
+ sceneView: GPUTextureView,
11745
+ /** Each mover's own motion, or a stand-in the shader is told not to read. */
11746
+ motionView: GPUTextureView,
11747
+ ): GPUBindGroup | null {
11502
11748
  const { device } = this.surface;
11503
11749
  if (this.resolvedDepthView === null || this.aoTargetView === null) return null;
11504
11750
  const entry = (
@@ -11530,6 +11776,7 @@ export class WebGPURenderer implements RendererApi {
11530
11776
  and a declared binding still needs a complete texture, as the bloom fallback says. */
11531
11777
  ...entry(RUSH_TEXTURES.uExposureHeld, this.exposurePass?.heldView ?? sceneView),
11532
11778
  ...entry(RUSH_TEXTURES.uExposureLocal, this.exposurePass?.localView ?? sceneView),
11779
+ ...entry(RUSH_TEXTURES.uMotion, motionView),
11533
11780
  ],
11534
11781
  layout: this.rushLayout,
11535
11782
  });
@@ -11813,7 +12060,11 @@ export class WebGPURenderer implements RendererApi {
11813
12060
  );
11814
12061
  }
11815
12062
  /* The composite's own group, over the resolved picture instead of the scene target. */
11816
- this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
12063
+ this.reconRushGroup = this.buildRushBindGroup(
12064
+ 'recon.rushBindGroup',
12065
+ this.reconShownView,
12066
+ this.reconMotionView ?? this.reconShownView,
12067
+ );
11817
12068
  /* And the scene's, because `ensureComposite` built it before this one existed. */
11818
12069
  this.rebuildRushBindGroup();
11819
12070
  /* And the bloom's, which blooms the resolved picture: see `rebuildBloomGroups`. */
@@ -11927,7 +12178,7 @@ export class WebGPURenderer implements RendererApi {
11927
12178
  const depth = this.depthView;
11928
12179
  if (!this.reconstructing || late === null || shown === null || depth === null) return;
11929
12180
  const oitSet = this.oitActive && this.translucentQueue.length > 0;
11930
- if (late.pending === 0 && !oitSet) return;
12181
+ if (late.pending === 0 && !oitSet && this.latePasses.length === 0) return;
11931
12182
  const width = Math.max(1, this.surface.canvas.width);
11932
12183
  const height = Math.max(1, this.surface.canvas.height);
11933
12184
  /* The order-independent set tests against this depth too, so it is made for either. */
@@ -11951,20 +12202,64 @@ export class WebGPURenderer implements RendererApi {
11951
12202
  this.refractSnapshotTaken = true;
11952
12203
  }
11953
12204
  this.lateRefractWanted = false;
11954
- if (late.pending === 0) return;
11955
- const pass = encoder.beginRenderPass({
11956
- label: 'recon.late',
11957
- timestampWrites: this.gpuTimer.writesFor(),
11958
- colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
11959
- depthStencilAttachment: {
11960
- view: late.depthView(width, height),
11961
- depthLoadOp: 'load',
11962
- /* Kept: the order-independent set replays against it after this pass. */
11963
- depthStoreOp: 'store',
11964
- },
11965
- });
11966
- late.replay(pass, this.issueBound);
11967
- pass.end();
12205
+ if (late.pending > 0) {
12206
+ const pass = encoder.beginRenderPass({
12207
+ label: 'recon.late',
12208
+ timestampWrites: this.gpuTimer.writesFor(),
12209
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
12210
+ depthStencilAttachment: {
12211
+ view: late.depthView(width, height),
12212
+ depthLoadOp: 'load',
12213
+ /* Kept: the order-independent set replays against it after this pass. */
12214
+ depthStoreOp: 'store',
12215
+ },
12216
+ });
12217
+ late.replay(pass, this.issueBound);
12218
+ pass.end();
12219
+ }
12220
+ this.runLatePasses(encoder, shown, late.depthView(width, height));
12221
+ }
12222
+
12223
+ /**
12224
+ * The registered passes `drawPass` queued for after the upscale: each handed a fresh copy of the
12225
+ * reconstructed picture with every late draw in it, in the output-size texture the late
12226
+ * refraction keeps (its own reads are done by now: they were recorded before this copy), then
12227
+ * drawn over the picture in a pass of its own.
12228
+ */
12229
+ private runLatePasses(
12230
+ encoder: GPUCommandEncoder,
12231
+ shown: GPUTextureView,
12232
+ depth: GPUTextureView,
12233
+ ): void {
12234
+ const copy = this.refractSnapshot;
12235
+ const source = this.reconShown;
12236
+ for (const handle of this.latePasses) {
12237
+ const definition = passAt(this.passes, handle);
12238
+ if (definition === undefined) continue;
12239
+ if (copy !== null && source !== null) {
12240
+ encoder.copyTextureToTexture({ texture: source }, { texture: copy }, [
12241
+ copy.width,
12242
+ copy.height,
12243
+ ]);
12244
+ }
12245
+ const pass = encoder.beginRenderPass({
12246
+ label: 'recon.latePass',
12247
+ timestampWrites: this.gpuTimer.writesFor(),
12248
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
12249
+ depthStencilAttachment: { view: depth, depthLoadOp: 'load', depthStoreOp: 'store' },
12250
+ });
12251
+ const context = this.passContext;
12252
+ context.pass = pass;
12253
+ context.outputTransform = this.gradeCode();
12254
+ context.outputExposure = this.gradeExposure();
12255
+ context.jitter = this.latePassJitter;
12256
+ context.format = RECON_HISTORY_FORMAT;
12257
+ context.depthFormat = DEPTH_FORMAT;
12258
+ context.samples = 1;
12259
+ definition.draw(context as PassContext);
12260
+ context.jitter = this.passJitter;
12261
+ pass.end();
12262
+ }
11968
12263
  }
11969
12264
 
11970
12265
  /** Whether this draw lands in the reconstructed picture. See `lateRouting.ts`. */
@@ -12391,7 +12686,10 @@ export class WebGPURenderer implements RendererApi {
12391
12686
  * other is the disagreement the parity rule exists to prevent. Said once rather than dropped.
12392
12687
  */
12393
12688
  private runReflections(encoder: GPUCommandEncoder): void {
12394
- if (this.reflectionQueue.length === 0) return;
12689
+ const frameReflections = this.frameReflections;
12690
+ const surfaceHalf = this.reflectionSurface;
12691
+ const materials = frameReflections !== null && surfaceHalf !== null;
12692
+ if (this.reflectionQueue.length === 0 && !materials) return;
12395
12693
  const scene = this.sceneColorView;
12396
12694
  const reflection = this.ssrReflectionView;
12397
12695
  const group = this.ssrBindGroup;
@@ -12436,9 +12734,81 @@ export class WebGPURenderer implements RendererApi {
12436
12734
  f[base + at('uSsrThickness')] = surface.thicknessM;
12437
12735
  f[base + at('uSsrSteps')] = surface.steps;
12438
12736
  f[base + at('uSsrEdgeFade')] = REFLECTION_EDGE_FADE;
12737
+ f[base + at('uSsrMaterial')] = 0;
12439
12738
  slot++;
12440
12739
  });
12740
+ const regions = slot;
12741
+ /* The frame's materials' trace takes the slot after the surfaces': the box, its axis and its
12742
+ strength unread, the march and the ceiling its own. See `frameReflections.ts`. */
12743
+ if (materials) {
12744
+ const base = (slot * SSR_SLOT) / 4;
12745
+ f.set(this.ssrDepthToWorld, base + at('uSsrDepthToWorld'));
12746
+ f.set(this.ssrViewProj, base + at('uSsrViewProj'));
12747
+ f.set(this.frameEye, base + at('uSsrEye'));
12748
+ f.fill(0, base + at('uSsrAxis'), base + at('uSsrAxis') + 3);
12749
+ f.fill(1, base + at('uSsrTint'), base + at('uSsrTint') + 3);
12750
+ f[base + at('uSsrStrength')] = 1;
12751
+ f[base + at('uSsrFresnel')] = 0;
12752
+ f[base + at('uSsrFacingCos')] = -1;
12753
+ f[base + at('uSsrReach')] = frameReflections.reachM;
12754
+ f[base + at('uSsrThickness')] = frameReflections.thicknessM;
12755
+ f[base + at('uSsrSteps')] = frameReflections.steps;
12756
+ f[base + at('uSsrEdgeFade')] = REFLECTION_EDGE_FADE;
12757
+ f[base + at('uSsrMaterial')] = 1;
12758
+ f[base + at('uSsrMaxRoughness')] = frameReflections.maxRoughness;
12759
+ slot++;
12760
+ }
12441
12761
  device.queue.writeBuffer(this.ssrUniforms, 0, this.ssrStaging, 0, slot * SSR_SLOT);
12762
+ const tracePipeline = postPipeline(
12763
+ this.pipelines,
12764
+ device,
12765
+ this.ssrLayout,
12766
+ 'post.ssrTrace',
12767
+ SSR_TRACE_FRAG_WGSL,
12768
+ this.pipelines.format,
12769
+ PREMULTIPLIED_OVER_BLEND,
12770
+ );
12771
+
12772
+ if (materials && this.depthView !== null && this.ssrMaterialGroup !== null) {
12773
+ /* The frame's opaque lit draws again, into what the trace and the resolve read. */
12774
+ surfaceHalf.draw(encoder, this.depthView, this.issueBound);
12775
+ const trace = encoder.beginRenderPass({
12776
+ label: 'post.ssr.materialTrace',
12777
+ colorAttachments: [
12778
+ { view: reflection, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
12779
+ ],
12780
+ });
12781
+ trace.setPipeline(tracePipeline);
12782
+ trace.setBindGroup(0, group, [regions * SSR_SLOT]);
12783
+ trace.draw(3);
12784
+ trace.end();
12785
+ /* The blur's texel and its radius at roughness 1, as a share of the frame's height. */
12786
+ const m = this.ssrMaterialFloats;
12787
+ const blurAt = this.postField(SSR_MATERIAL_FIELDS, 'uSsrBlur');
12788
+ m[blurAt] = 1 / Math.max(width, 1);
12789
+ m[blurAt + 1] = 1 / Math.max(height, 1);
12790
+ m[blurAt + 2] = frameReflections.blur * height;
12791
+ device.queue.writeBuffer(this.ssrMaterialUniforms, 0, m);
12792
+ const resolve = encoder.beginRenderPass({
12793
+ label: 'post.ssr.materialResolve',
12794
+ colorAttachments: [{ view: scene, loadOp: 'load', storeOp: 'store' }],
12795
+ });
12796
+ resolve.setPipeline(
12797
+ postPipeline(
12798
+ this.pipelines,
12799
+ device,
12800
+ this.ssrMaterialLayout,
12801
+ 'post.ssrMaterialResolve',
12802
+ SSR_MATERIAL_RESOLVE_FRAG_WGSL,
12803
+ this.pipelines.format,
12804
+ FRAME_REFLECTION_BLEND,
12805
+ ),
12806
+ );
12807
+ resolve.setBindGroup(0, this.ssrMaterialGroup, [0]);
12808
+ resolve.draw(3);
12809
+ resolve.end();
12810
+ }
12811
+ if (regions === 0) return;
12442
12812
 
12443
12813
  /* Cleared whole and then scissored per surface: the resolve reads every pixel, and one left
12444
12814
  from the last frame would composite that reflection over this frame's scene. */
@@ -12448,17 +12818,7 @@ export class WebGPURenderer implements RendererApi {
12448
12818
  { view: reflection, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
12449
12819
  ],
12450
12820
  });
12451
- trace.setPipeline(
12452
- postPipeline(
12453
- this.pipelines,
12454
- device,
12455
- this.ssrLayout,
12456
- 'post.ssrTrace',
12457
- SSR_TRACE_FRAG_WGSL,
12458
- this.pipelines.format,
12459
- PREMULTIPLIED_OVER_BLEND,
12460
- ),
12461
- );
12821
+ trace.setPipeline(tracePipeline);
12462
12822
  slot = 0;
12463
12823
  this.reflectionQueue.replay((surface) => {
12464
12824
  const index = slot++;
@@ -13222,6 +13582,90 @@ export class WebGPURenderer implements RendererApi {
13222
13582
  );
13223
13583
  }
13224
13584
 
13585
+ /** Whether a draw now is the frame's own, and so has a surface half to keep. */
13586
+ private reflectionSurfaceKeeps(): boolean {
13587
+ return (
13588
+ this.reflectionSurface !== null &&
13589
+ !this.reflectionPassActive &&
13590
+ !this.probePassActive &&
13591
+ !this.capturePassActive &&
13592
+ !this.framePresented &&
13593
+ !this.overlayActive &&
13594
+ this.oitMode === 'none'
13595
+ );
13596
+ }
13597
+
13598
+ /**
13599
+ * An opaque lit draw's surface half, kept for the frame's materials' reflections: the draw again,
13600
+ * with the same slots, buffers and bind group, through the pipeline with `REFLECTION_SURFACE` on —
13601
+ * which writes what the pixel reflects of its environment into the two targets
13602
+ * `reflectionSurfacePass.ts` holds and tests for the depth its frame draw wrote. Replayed when the
13603
+ * frame's pass has ended, before the trace reads them. See `shaders/flat/reflectionSurface.ts`.
13604
+ */
13605
+ private keepReflectionSurface(
13606
+ mesh: GpuMesh,
13607
+ frameKey: string,
13608
+ base: string,
13609
+ skinned: boolean,
13610
+ morphed: boolean,
13611
+ deltas: GPUTextureView | null,
13612
+ layer: number,
13613
+ eight: boolean,
13614
+ clothed: boolean,
13615
+ modelKind: SurfaceModelKind | null,
13616
+ slot: number,
13617
+ material: number,
13618
+ instanceBuffer: GPUBuffer | null,
13619
+ instances: number,
13620
+ indirect: GPUBuffer | null,
13621
+ ): void {
13622
+ const surface = this.reflectionSurface;
13623
+ const present = this.meshPresent.get(base);
13624
+ if (surface === null || present === undefined) return;
13625
+ const key = `${frameKey}|rs`;
13626
+ const pipeline =
13627
+ this.pipelines.peek(key) ??
13628
+ flatPipeline(
13629
+ this.pipelines,
13630
+ this.surface.device,
13631
+ this.flatLayoutFor(skinned, morphed),
13632
+ this.variant,
13633
+ key,
13634
+ present,
13635
+ false,
13636
+ skinned,
13637
+ morphed,
13638
+ true,
13639
+ layer,
13640
+ instanceBuffer !== null,
13641
+ 'none',
13642
+ this.materialDoubleSided,
13643
+ false,
13644
+ eight,
13645
+ clothed,
13646
+ modelKind,
13647
+ 'whole',
13648
+ true,
13649
+ );
13650
+ const command = surface.take();
13651
+ command.pipeline = pipeline;
13652
+ command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
13653
+ command.offsetA = slot;
13654
+ command.offsetB = material;
13655
+ command.offsetCount = 2;
13656
+ let index = 0;
13657
+ for (; index < mesh.vertexBuffers.length; index++) {
13658
+ command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
13659
+ }
13660
+ if (instanceBuffer !== null) command.vertexBuffers[index++] = instanceBuffer;
13661
+ command.vertexCount = index;
13662
+ command.indexBuffer = mesh.indexBuffer;
13663
+ command.indexed = true;
13664
+ command.count = mesh.indexCount;
13665
+ command.instances = instances;
13666
+ command.indirect = indirect;
13667
+ }
13668
+
13225
13669
  /**
13226
13670
  * A skin draw's diffuse and colour halves, kept for the spread: the frame's draw again, twice,
13227
13671
  * with the same slots, buffers and bind group, through the pipelines that write the diffuse's
@@ -13301,11 +13745,17 @@ export class WebGPURenderer implements RendererApi {
13301
13745
  private runSkinScatter(): void {
13302
13746
  const skin = this.skinScatter;
13303
13747
  if (skin === null || this.skinScattered) return;
13748
+ /*
13749
+ * **Not the frame's picture, so not the frame's moment**: a blended draw in a mirror, a probe
13750
+ * bake or a scene capture lands over none of the frame's skin. Latching there switched the
13751
+ * spread off for the frame, so a capture with a glowing sign in it left every face drawn after
13752
+ * it flat. `scripts/capture-check.mjs`.
13753
+ */
13754
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive) return;
13304
13755
  /* Latched whether or not skin was kept: a skin drawn after this point is the whole surface. */
13305
13756
  this.skinScattered = true;
13306
13757
  if (!skin.pending) return;
13307
- if (this.reflectionPassActive || this.probePassActive || this.oitReplaying) return;
13308
- if (this.capturePassActive) return;
13758
+ if (this.oitReplaying) return;
13309
13759
  const encoder = this.encoder;
13310
13760
  const depth = this.depthView;
13311
13761
  const resolved = this.resolvedDepthView;
@@ -13430,6 +13880,65 @@ export class WebGPURenderer implements RendererApi {
13430
13880
  return true;
13431
13881
  }
13432
13882
 
13883
+ /** The colour copy a registered pass reads, at the scene's size and format. See `passColour`. */
13884
+ private allocatePassColour(): void {
13885
+ const source = this.sceneColor;
13886
+ if (source === null || this.passColour !== null) return;
13887
+ this.passColour = this.surface.device.createTexture({
13888
+ label: 'pass.sceneColor',
13889
+ size: [source.width, source.height],
13890
+ format: this.pipelines.format,
13891
+ usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
13892
+ });
13893
+ this.passColourView = this.passColour.createView();
13894
+ }
13895
+
13896
+ /**
13897
+ * The frame's colour so far, into `passColour`, for a registered pass that reads it: the
13898
+ * boundary `takeRefractSnapshot` describes — flush, end the pass, copy what its end resolved,
13899
+ * reopen with `load` — and refused where that one is, in a mirror, a probe bake or a capture,
13900
+ * whose pictures are not the frame's. **Not latched**, unlike refraction's: each reading pass
13901
+ * sees the passes drawn before it, at a full-frame copy each.
13902
+ */
13903
+ private takePassColour(): void {
13904
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive) return;
13905
+ const copy = this.passColour;
13906
+ const source = this.sceneColor;
13907
+ const encoder = this.encoder;
13908
+ if (copy === null || source === null || encoder === null || this.pass === null) return;
13909
+ if (this.quality.frameGraph) this.flushGraph();
13910
+ this.pass.end();
13911
+ encoder.copyTextureToTexture({ texture: source }, { texture: copy }, [copy.width, copy.height]);
13912
+ this.pass = this.reopenFramePass('pass.colourTaken');
13913
+ }
13914
+
13915
+ /**
13916
+ * The frame's pass again after a copy between passes, loading everything already drawn: the same
13917
+ * attachments the copy's caller ended, with nothing cleared.
13918
+ */
13919
+ private reopenFramePass(label: string): GPURenderPassEncoder | null {
13920
+ const encoder = this.encoder;
13921
+ const target = this.compositeTarget();
13922
+ if (encoder === null || target === null) return null;
13923
+ const multisampled = this.compositeMsaa() !== null;
13924
+ return encoder.beginRenderPass({
13925
+ label,
13926
+ timestampWrites: this.gpuTimer.writesFor(),
13927
+ colorAttachments: [
13928
+ {
13929
+ view: this.compositeMsaa() ?? target,
13930
+ resolveTarget: multisampled ? target : undefined,
13931
+ loadOp: 'load',
13932
+ storeOp: resolvedStoreOp(multisampled, false, this.quality.discardResolvedAttachments),
13933
+ },
13934
+ ],
13935
+ depthStencilAttachment:
13936
+ this.depthView === null
13937
+ ? undefined
13938
+ : { view: this.depthView, depthLoadOp: 'load', depthStoreOp: 'store' },
13939
+ });
13940
+ }
13941
+
13433
13942
  /**
13434
13943
  * The pass to record into, reopening one on the canvas if the frame has already gone.
13435
13944
  *
@@ -13621,6 +14130,8 @@ export class WebGPURenderer implements RendererApi {
13621
14130
  /* And a march reads the same copy, for the same reason and with the same failure if it is
13622
14131
  discarded: a depth of zero is the far plane reversed, so every ray finds nothing. */
13623
14132
  this.reflectionQueue.length > 0 ||
14133
+ /* And the frame's materials' reflections, whose surface half tests against the depth too. */
14134
+ this.reflectionSurface !== null ||
13624
14135
  /* And the medium's own march, which stops where the frame's depth says a surface is. Left
13625
14136
  out of this, every ray runs to `maxDistance` through the walls and the fog is drawn on
13626
14137
  top of the room rather than in it. */
@@ -14485,6 +14996,8 @@ export class WebGPURenderer implements RendererApi {
14485
14996
  const reads = maskOf(...(definition.reads ?? []));
14486
14997
  this.passReads.set(handle, reads);
14487
14998
  this.passReadsUnion |= reads;
14999
+ /* The colour copy a reader needs, now rather than at its first draw: see `passColour`. */
15000
+ if ((reads & COLOR_SNAPSHOT) !== 0 && this.passColour === null) this.allocatePassColour();
14488
15001
  if (definition.prepare !== undefined) this.preparingPasses++;
14489
15002
  definition.init?.(this.passDevice());
14490
15003
  return handle;
@@ -14504,12 +15017,16 @@ export class WebGPURenderer implements RendererApi {
14504
15017
  environment: PassEnvironment | null;
14505
15018
  distanceField: ComposedField | null;
14506
15019
  jitter: Float32Array;
15020
+ sceneDepth: GPUTextureView | null;
15021
+ sceneColor: GPUTextureView | null;
14507
15022
  } = {
14508
15023
  backend: 'webgpu',
14509
15024
  encoder: null,
14510
15025
  environment: null,
14511
15026
  distanceField: null,
14512
15027
  jitter: this.passJitter,
15028
+ sceneDepth: null,
15029
+ sceneColor: null,
14513
15030
  };
14514
15031
  /**
14515
15032
  * The object handed to a pass as `PrepareContext.environment`, rebuilt only when it changes.
@@ -14607,6 +15124,15 @@ export class WebGPURenderer implements RendererApi {
14607
15124
  * than merely what happens.
14608
15125
  */
14609
15126
  this.prepareContext.distanceField = this.fieldComposer?.field ?? null;
15127
+ /* Settled for the frame: `ensureComposite` has run, so the view stays until the pass draws. */
15128
+ this.prepareContext.sceneDepth = this.resolvedDepthView;
15129
+ /*
15130
+ * Under a reconstruction a colour reader draws after the upscale (see `drawPass`), so it reads
15131
+ * the output-size copy the late refraction keeps rather than the render's.
15132
+ */
15133
+ this.prepareContext.sceneColor = this.reconstructionWanted
15134
+ ? this.refractSnapshotView
15135
+ : this.passColourView;
14610
15136
  for (const definition of this.passes.definitions) {
14611
15137
  definition?.prepare?.(this.prepareContext as PrepareContext);
14612
15138
  }
@@ -14623,20 +15149,42 @@ export class WebGPURenderer implements RendererApi {
14623
15149
  if (!this.canDraw()) return;
14624
15150
  const definition = passAt(this.passes, handle);
14625
15151
  if (definition === undefined) return;
15152
+ const reads = this.passReads.get(handle) ?? 0;
15153
+ /*
15154
+ * **Under a reconstruction, a pass that reads the frame's colour draws after the blended draws
15155
+ * do.** Those land after the upscale, at the output's size, so a pass drawn among them at the
15156
+ * render's would read a frame without them and then be covered by them — a full-screen effect
15157
+ * with the hit sparks drawn sharp over it. Queued instead, and drawn by `runLate` over the
15158
+ * reconstructed picture, after every late draw, whatever order it was asked in.
15159
+ */
15160
+ if ((reads & COLOR_SNAPSHOT) !== 0 && this.drawsLate()) {
15161
+ this.latePasses.push(handle);
15162
+ return;
15163
+ }
15164
+ /*
15165
+ * A pass that reads the depth copy gets it with everything drawn so far: the copy is taken
15166
+ * here, once a frame, as a soft-edged sprite takes it. Before `openPass` below, because taking
15167
+ * it ends the frame's pass and opens another.
15168
+ */
15169
+ if ((reads & DEPTH_SNAPSHOT) !== 0 && !this.framePresented && this.openPass() !== null) {
15170
+ this.takeVolumeDepth();
15171
+ }
15172
+ /*
15173
+ * And one that reads the colour gets it afresh, with everything drawn before it — a full-screen
15174
+ * pass drawn last reads the whole frame and an effect drawn mid-frame reads the frame so far.
15175
+ * A copy a reading pass, where the depth above is one a frame: what a colour reader draws is
15176
+ * itself part of what the next one should see.
15177
+ */
15178
+ if ((reads & COLOR_SNAPSHOT) !== 0 && !this.framePresented && this.openPass() !== null) {
15179
+ this.takePassColour();
15180
+ }
14626
15181
  if (!this.quality.frameGraph) {
14627
15182
  const pass = this.openPass();
14628
15183
  if (pass === null) return;
14629
15184
  definition.draw(this.fillPassContext(pass));
14630
15185
  return;
14631
15186
  }
14632
- recordNode(
14633
- this.arena,
14634
- VERB_PASS,
14635
- this.passReads.get(handle) ?? 0,
14636
- this.currentTarget(),
14637
- handle,
14638
- 0,
14639
- );
15187
+ recordNode(this.arena, VERB_PASS, reads, this.currentTarget(), handle, 0);
14640
15188
  if (this.framePresented) this.queueOverlayFlush();
14641
15189
  }
14642
15190
 
@@ -14757,8 +15305,9 @@ export class WebGPURenderer implements RendererApi {
14757
15305
  ): void {
14758
15306
  const binner = this.clusterBinner;
14759
15307
  i[at('uClustered')] = binner === null ? 0 : 1;
14760
- if (binner === null) return;
14761
15308
 
15309
+ /* Written whether or not the lights are clustered: a surface overlay's rim finds its place on
15310
+ the screen through the same two (\`overlayScreen\`). */
14762
15311
  const p = camera.projection;
14763
15312
  const tanHalfFovY = 1 / (p[5] ?? 1);
14764
15313
  const aspect = (p[5] ?? 1) / (p[0] ?? 1);
@@ -14774,6 +15323,7 @@ export class WebGPURenderer implements RendererApi {
14774
15323
  f[frustumAt + 2] = tanHalfFovY;
14775
15324
  f[frustumAt + 3] = aspect;
14776
15325
  f.set(camera.view as Float32Array, at('uView'));
15326
+ if (binner === null) return;
14777
15327
 
14778
15328
  /* The rectangles past the fixed arm's go into the table. See `ClusterLightSet.areas`. */
14779
15329
  const set = fillClusterLightSet(env, this.clusterLights, this.shadedAreaLights);
@@ -15098,6 +15648,49 @@ export class WebGPURenderer implements RendererApi {
15098
15648
  /** This frame's exposure into the tone curve. Starts at the profile's own. */
15099
15649
  private exposure: number;
15100
15650
 
15651
+ /**
15652
+ * The `filmic` curve every later frame is graded through, until it is set again. See
15653
+ * `RendererApi.setFilmicCurve`; the constants are solved here once, not per frame or per pixel.
15654
+ */
15655
+ setFilmicCurve(curve: FilmicCurve): void {
15656
+ this.filmCurve = curve;
15657
+ if (resolveFilmicCurve(curve, this.filmConstants, this.headroom) && !this.warnedFilmClamped) {
15658
+ this.warnedFilmClamped = true;
15659
+ console.warn(FILMIC_CLAMPED);
15660
+ }
15661
+ }
15662
+
15663
+ private warnedFilmClamped = false;
15664
+ /** The curve last set, so a new headroom can solve it again. */
15665
+ private filmCurve: FilmicCurve = DEFAULT_FILMIC_CURVE;
15666
+ /** The display's peak over paper white; 1 on a standard range. See `setDisplayLuminance`. */
15667
+ private headroom = 1;
15668
+
15669
+ /** The range the frame goes out in. See `RendererApi.displayRange`. */
15670
+ get displayRange(): DisplayRange {
15671
+ return this.surface.displayRange;
15672
+ }
15673
+
15674
+ /** Why it is that range, in words. */
15675
+ get displayRangeReason(): string {
15676
+ return this.surface.displayRangeReason;
15677
+ }
15678
+
15679
+ /**
15680
+ * How bright paper white and the display's peak are, in any one unit (nits, usually), held until
15681
+ * changed: their ratio is how far above white the highlights may run on a high range display,
15682
+ * and nothing changes on a standard one. See `RendererApi.setDisplayLuminance`.
15683
+ */
15684
+ setDisplayLuminance(paperWhite: number, peak: number): void {
15685
+ this.headroom = displayHeadroom(this.surface.displayRange, paperWhite, peak);
15686
+ resolveFilmicCurve(this.filmCurve, this.filmConstants, this.headroom);
15687
+ }
15688
+
15689
+ /** The curve's eight constants, and the two views the composite uploads them through. */
15690
+ private readonly filmConstants = new Float32Array(FILMIC_CONSTANTS);
15691
+ private readonly filmA = this.filmConstants.subarray(0, 4);
15692
+ private readonly filmB = this.filmConstants.subarray(4, 8);
15693
+
15101
15694
  /*
15102
15695
  * -----------------------------------------------------------------------------------------
15103
15696
  * The three screen effects, which this backend cannot yet honour — and says so.
@@ -15142,6 +15735,8 @@ export class WebGPURenderer implements RendererApi {
15142
15735
  private readonly veilColor: Vec3 = [0, 0, 0];
15143
15736
  private veilAlpha = 0;
15144
15737
  private vignette = 0;
15738
+ /** The lens's colour fringe: red's and green's pull and where it begins. See `fringe.ts`. */
15739
+ private readonly fringe = new Float32Array(FRINGE_FLOATS);
15145
15740
  private grain = 0;
15146
15741
  private grainSeed = 0;
15147
15742
  private warnedLookWithoutComposite = false;
@@ -15165,6 +15760,21 @@ export class WebGPURenderer implements RendererApi {
15165
15760
  this.vignette = clampVignette(strength);
15166
15761
  }
15167
15762
 
15763
+ /**
15764
+ * The lens's colour fringe, held until changed: `intensity` is a percentage, 0 is none, and red
15765
+ * and green are pulled toward the centre by their wavelength's distance from blue, growing from
15766
+ * `start` — a share of the half-frame, 0 the centre — to the whole of it at the edge. Applied to
15767
+ * scene light at the composite's first read. Needs `screenEffects`, and says so once rather than
15768
+ * doing nothing. See `shaders/fringe.ts`.
15769
+ */
15770
+ setChromaticAberration(intensity: number, start = 0): void {
15771
+ if (!this.hasComposite) {
15772
+ this.warnLookWithoutComposite(intensity);
15773
+ return;
15774
+ }
15775
+ resolveFringe(intensity, start, this.fringe);
15776
+ }
15777
+
15168
15778
  /**
15169
15779
  * Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
15170
15780
  * grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
@@ -15316,9 +15926,27 @@ export class WebGPURenderer implements RendererApi {
15316
15926
  this.rushStrength = Math.min(Math.max(strength, 0), 1);
15317
15927
  }
15318
15928
 
15319
- /** How much of the frame's camera motion blur to apply, 0 to 1. */
15320
- setCameraMotionBlur(scale: number): void {
15929
+ /**
15930
+ * How much of the frame's motion blur to apply, 0 to 1, and the longest smear as a share of the
15931
+ * frame, both held until changed. See `RendererApi.setCameraMotionBlur`.
15932
+ */
15933
+ setCameraMotionBlur(scale: number, maxShare = MOTION_BLUR_MAX_UV): void {
15321
15934
  this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
15935
+ this.motionBlurMax = clampMotionShare(maxShare);
15936
+ }
15937
+
15938
+ /** The longest smear, as a share of the frame. See `setCameraMotionBlur`. */
15939
+ private motionBlurMax = MOTION_BLUR_MAX_UV;
15940
+ private warnedObjectBlur = false;
15941
+
15942
+ /**
15943
+ * Said once: a draw named where it was last frame while the frame blurs, and this frame keeps no
15944
+ * motion target to blur it by — the camera's motion alone smears it. See `OBJECT_BLUR_UNKEPT`.
15945
+ */
15946
+ private warnObjectBlur(): void {
15947
+ if (this.warnedObjectBlur) return;
15948
+ this.warnedObjectBlur = true;
15949
+ console.warn(OBJECT_BLUR_UNKEPT);
15322
15950
  }
15323
15951
  /**
15324
15952
  * The camera has cut: this frame is a new shot, not the next moment of the last one.
@@ -15927,17 +16555,18 @@ export class WebGPURenderer implements RendererApi {
15927
16555
  * `bindMeshPass` and `drawMesh`, which record into whatever `this.pass` is. That is the same
15928
16556
  * arrangement `bakeFace` uses for a point light's cube, and it is why a bake must not be
15929
16557
  * opened inside a frame's pass by a caller who then expects the frame to continue.
15930
- */
15931
- const savedPass = this.pass;
15932
- /*
16558
+ *
15933
16559
  * **A bake is a boundary, and boundaries flush.** The mirror says why in its own words: what
15934
16560
  * has been recorded and not yet replayed belongs to the pass that is about to end, and leaving
15935
16561
  * it replays it into whichever pass opens next — a draw in the wrong target rather than a
15936
16562
  * missing one. This is the fourth such boundary in the backend and the only one that was never
15937
16563
  * given the treatment, which is what let a bake carry the frame's pending draws into a cube
15938
- * face.
16564
+ * face. **The pass is saved after the flush**, which opens it where none was: saved before,
16565
+ * the null given back left the flushed pass open and the frame's encoder locked. See
16566
+ * `captureScene`.
15939
16567
  */
15940
16568
  if (this.quality.frameGraph) this.flushGraph();
16569
+ const savedPass = this.pass;
15941
16570
  const savedEncoder = this.encoder;
15942
16571
  /*
15943
16572
  * **The rings are marked here and given back after the submit below**, because this bake is its
@@ -16629,6 +17258,7 @@ export class WebGPURenderer implements RendererApi {
16629
17258
  this.sceneColor,
16630
17259
  this.sceneColorMsaa,
16631
17260
  this.refractSnapshot,
17261
+ this.passColour,
16632
17262
  this.resolvedDepth,
16633
17263
  this.aoTarget,
16634
17264
  this.aoScratch,
@@ -16698,6 +17328,9 @@ export class WebGPURenderer implements RendererApi {
16698
17328
  usage: 0x4 | 0x2 | 0x10, // TEXTURE_BINDING | COPY_DST | RENDER_ATTACHMENT
16699
17329
  });
16700
17330
  this.refractSnapshotView = this.refractSnapshot.createView();
17331
+ this.passColour = null;
17332
+ this.passColourView = null;
17333
+ if ((this.passReadsUnion & COLOR_SNAPSHOT) !== 0) this.allocatePassColour();
16701
17334
  this.refractMips = new SnapshotMips(
16702
17335
  device,
16703
17336
  this.pipelines,
@@ -16926,6 +17559,10 @@ export class WebGPURenderer implements RendererApi {
16926
17559
  usage: USAGE,
16927
17560
  });
16928
17561
  this.ssrReflectionView = this.ssrReflection.createView();
17562
+ /* The frame's materials, at the scene's size; a stand-in where nothing traces them. */
17563
+ this.reflectionSurface?.size(width, height);
17564
+ const probeMap = this.reflectionSurface?.probeView ?? this.blankView;
17565
+ const tintMap = this.reflectionSurface?.tintView ?? this.blankView;
16929
17566
  this.ssrBindGroup =
16930
17567
  this.resolvedDepthView === null || this.sceneColorView === null
16931
17568
  ? null
@@ -16939,8 +17576,23 @@ export class WebGPURenderer implements RendererApi {
16939
17576
  },
16940
17577
  ...entry(SSR_TEXTURES.uSsrDepth, this.resolvedDepthView, true),
16941
17578
  ...entry(SSR_TEXTURES.uSsrScene, this.sceneColorView),
17579
+ ...entry(SSR_TEXTURES.uSsrProbeMap, probeMap),
17580
+ ...entry(SSR_TEXTURES.uSsrTintMap, tintMap),
16942
17581
  ],
16943
17582
  });
17583
+ this.ssrMaterialGroup = device.createBindGroup({
17584
+ label: 'post.ssrMaterialGroup',
17585
+ layout: this.ssrMaterialLayout,
17586
+ entries: [
17587
+ {
17588
+ binding: SSR_MATERIAL_UNIFORMS,
17589
+ resource: { buffer: this.ssrMaterialUniforms, size: SSR_MATERIAL_SIZE },
17590
+ },
17591
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrReflection, this.ssrReflectionView),
17592
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrProbeMap, probeMap),
17593
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrTintMap, tintMap),
17594
+ ],
17595
+ });
16944
17596
  this.ssrResolveGroup = device.createBindGroup({
16945
17597
  label: 'post.ssrResolveGroup',
16946
17598
  layout: this.ssrResolveLayout,