@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
@@ -59,17 +59,23 @@ import { sceneCaptureTexels } from '../../sceneCapture.js';
59
59
  import { gpuCompressedFormats } from './compressedUpload.js';
60
60
  import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.js';
61
61
  import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel, skinProfileDistance, } from '../../surfaceModel.js';
62
+ import { OVERLAY_FLOATS, overlayLays, packSurfaceOverlay, } from '../../surfaceOverlay.js';
63
+ import { frameReflectionsRefused, resolveFrameReflections, } from '../../frameReflections.js';
64
+ import { ReflectionSurfacePass } from './reflectionSurfacePass.js';
62
65
  import { validateClothBinding } from '../../clothBindingData.js';
63
66
  import { createSdfTextBindGroup, createSdfTextBindGroupLayout, GpuSdfText, sdfTextPipeline, SDF_TEXT_FRAG_FIELDS, SDF_TEXT_FRAG_SIZE, SDF_TEXT_VERT_FIELDS, SDF_TEXT_VERT_SIZE, } from './sdfTextPass.js';
64
67
  import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG_FIELDS, FILM_FRAG_SIZE, FILM_VERT_FIELDS, FILM_VERT_SIZE, } from './filmPass.js';
65
68
  import { reflectionTargetSize } from '../../planarReflectionDraw.js';
66
69
  import { drawingBufferSize } from '../../drawingBuffer.js';
67
- import { MOTION_BLUR_MAX_UV, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV } from '../../vertexDefaults.js';
70
+ import { MOTION_BLUR_MAX_UV, OBJECT_BLUR_UNKEPT, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV, clampMotionShare, forwardTransformCode, } from '../../vertexDefaults.js';
71
+ import { FRINGE_FLOATS, resolveFringe } from '../../shaders/fringe.js';
72
+ import { displayHeadroom } from '../../displayRange.js';
73
+ import { DEFAULT_FILMIC_CURVE, FILMIC_CLAMPED, FILMIC_CONSTANTS, FILMIC_WITHOUT_COMPOSITE, resolveFilmicCurve, } from '../../filmicCurve.js';
68
74
  import { ExposurePass } from './exposurePass.js';
69
75
  import { AO_STORE } from '../../shaders/ambientOcclusion.js';
70
76
  import { clampAutoExposure } from '../../shaders/exposure.js';
71
77
  import { clampLocalExposure } from '../../shaders/localExposure.js';
72
- import { createDepthResolveLayout, createPostStageLayout, depthResolvePipeline, postPipeline, postPipelineTargets, sceneColorFormat, AO_BLUR_FIELDS, AO_BLUR_FRAG_WGSL, AO_BLUR_SIZE, AO_BLUR_TEXTURES, AO_BLUR_UNIFORMS, AO_FRAG_FIELDS, AO_FRAG_SIZE, AO_FRAG_WGSL, AO_TEXTURES, AO_UNIFORMS, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, TINTED_ADD_BLEND, BLOOM_UPSAMPLE_FRAG_WGSL, RESOLVED_DEPTH_FORMAT, RUSH_FRAG_FIELDS, RUSH_FRAG_SIZE, RUSH_FRAG_WGSL, RUSH_TEXTURES, RUSH_UNIFORMS, TAA_FRAG_FIELDS, TAA_FRAG_SIZE, TAA_TEXTURES, TAA_UNIFORMS, TEMPORAL_RESOLVE_FRAG_WGSL, MEDIUM_FRAG_FIELDS, MEDIUM_FRAG_SIZE, MEDIUM_FRAG_WGSL, MEDIUM_TEXTURES, MEDIUM_TRANSMIT_BLEND, MEDIUM_UNIFORMS, MEDIUM_UPSAMPLE_FIELDS, MEDIUM_UPSAMPLE_FRAG_WGSL, MEDIUM_UPSAMPLE_SIZE, MEDIUM_UPSAMPLE_TEXTURES, MEDIUM_UPSAMPLE_UNIFORMS, OIT_COMPOSITE_BLEND, OIT_RESOLVE_FRAG_WGSL, OIT_TEXTURES, DECAL_FRAG_FIELDS, DECAL_FRAG_SIZE, DECAL_MULTIPLY_BLEND, DECAL_PROJECT_FRAG_WGSL, DECAL_TEXTURES, DECAL_UNIFORMS, PREMULTIPLIED_OVER_BLEND, REFLECTION_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
78
+ import { createDepthResolveLayout, createPostStageLayout, depthResolvePipeline, postPipeline, postPipelineTargets, sceneColorFormat, AO_BLUR_FIELDS, AO_BLUR_FRAG_WGSL, AO_BLUR_SIZE, AO_BLUR_TEXTURES, AO_BLUR_UNIFORMS, AO_FRAG_FIELDS, AO_FRAG_SIZE, AO_FRAG_WGSL, AO_TEXTURES, AO_UNIFORMS, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, TINTED_ADD_BLEND, BLOOM_UPSAMPLE_FRAG_WGSL, RESOLVED_DEPTH_FORMAT, RUSH_FRAG_FIELDS, RUSH_FRAG_SIZE, RUSH_FRAG_WGSL, RUSH_TEXTURES, RUSH_UNIFORMS, TAA_FRAG_FIELDS, TAA_FRAG_SIZE, TAA_TEXTURES, TAA_UNIFORMS, TEMPORAL_RESOLVE_FRAG_WGSL, MEDIUM_FRAG_FIELDS, MEDIUM_FRAG_SIZE, MEDIUM_FRAG_WGSL, MEDIUM_TEXTURES, MEDIUM_TRANSMIT_BLEND, MEDIUM_UNIFORMS, MEDIUM_UPSAMPLE_FIELDS, MEDIUM_UPSAMPLE_FRAG_WGSL, MEDIUM_UPSAMPLE_SIZE, MEDIUM_UPSAMPLE_TEXTURES, MEDIUM_UPSAMPLE_UNIFORMS, OIT_COMPOSITE_BLEND, OIT_RESOLVE_FRAG_WGSL, OIT_TEXTURES, DECAL_FRAG_FIELDS, DECAL_FRAG_SIZE, DECAL_MULTIPLY_BLEND, DECAL_PROJECT_FRAG_WGSL, DECAL_TEXTURES, DECAL_UNIFORMS, PREMULTIPLIED_OVER_BLEND, FRAME_REFLECTION_BLEND, REFLECTION_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_MATERIAL_FIELDS, SSR_MATERIAL_RESOLVE_FRAG_WGSL, SSR_MATERIAL_SIZE, SSR_MATERIAL_TEXTURES, SSR_MATERIAL_UNIFORMS, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
73
79
  import { GRADE_PLACEHOLDER_SIZE, identityGradeLut, validateGradeLut, } from '../../colourGrade.js';
74
80
  import { FILM_LOOK_WITHOUT_COMPOSITE, clampGrain, clampVignette, grainSeed, } from '../../shaders/filmLook.js';
75
81
  import { createProbeMipBindGroupLayout, createProbePrefilterBindGroupLayout, PREFILTER_UNIFORM_OFFSETS, probeLevels, probeMipPipeline, probePrefilterPipeline, PROBE_FACES, PROBE_FACE_BASIS, PROBE_FAR_M, PROBE_FOV_DEG, PROBE_MIP_UNIFORM_SIZE, PROBE_NEAR_M, } from './probePass.js';
@@ -93,7 +99,7 @@ import { GpuGlassTint } from './glassTintPass.js';
93
99
  import { SUN_DYNAMIC_LAYER, SUN_GLASS_MOVING_LAYER, SUN_GLASS_STATIC_LAYER, SUN_PEELED_LAYER, SUN_STATIC_LAYER, } from '../../shadowMap.js';
94
100
  import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental } from './buffers.js';
95
101
  import { SkinPaletteRing } from './skinPaletteRing.js';
96
- import { DEPTH_FORMAT, FLAT_VERT_FIELDS, FLAT_VERT_SIZE, createFlatBindGroup, createFlatBindGroupLayout, flatFragmentBindings, flatVariant, flatVertexBindings, flatPipeline, flatPipelineAsync, } from './flatPass.js';
102
+ import { DEPTH_FORMAT, FLAT_VERT_FIELDS, FLAT_VERT_SIZE, createFlatBindGroup, createFlatBindGroupLayout, flatFragmentBindings, flatVariant, flatVertexBindings, flatPipeline, flatPipelineAsync, probeStageCeilings, } from './flatPass.js';
97
103
  import { DEPTH_FRAG_BINDING, DEPTH_FRAG_SIZE, DEPTH_FRAG_WGSL, DEPTH_PREVIOUS_BINDING, DEPTH_CUTOUT_VERT_FIELDS, DEPTH_CUTOUT_VERT_SIZE, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS, DEPTH_VERT_FIELDS, DEPTH_VERT_SIZE, SHADOW_FORMAT, createDepthBindGroup, createDepthBindGroupLayout, depthPipeline, } from './depthPass.js';
98
104
  import { PipelineCache } from './pipelineCache.js';
99
105
  import { SKIN_TARGET_FORMAT, SkinScatterPass } from './skinScatterPass.js';
@@ -215,34 +221,20 @@ const MAX_SKY_DRAWS = 64;
215
221
  /**
216
222
  * Whether the environment probe's permutation fits this device's per-stage binding limits.
217
223
  *
218
- * The widest flat variant declares one albedo, three directional shadow maps, twelve
219
- * point-shadow cubes and the probe: seventeen textures and seventeen samplers. WebGPU's default
220
- * is sixteen of each and an adapter is free to offer no more — this one offers 48 textures and
221
- * exactly 16 samplers, so the samplers are what decide it.
224
+ * **Two ceilings, counted separately**: sampled textures and samplers, since textures that share a
225
+ * sampler (`// wgsl:share` in `flat/preamble.ts`) count once against the second. The widest variant
226
+ * binds sixteen textures and twelve samplers, which is exactly WebGPU's default texture ceiling, and
227
+ * `select.ts` asks for the adapter's own — 48 here — so the probe does not depend on that request.
222
228
  */
223
229
  function probeFits(device) {
224
230
  /*
225
- * **Two ceilings, counted separately, and they are no longer the same number.**
226
- *
227
- * The widest permutation declares seventeen sampled textures — one albedo, three directional
228
- * shadow maps, the point-shadow array, a normal map and the probe's own cube — **seven**, where
229
- * it was seventeen while every point light's shadow was a `samplerCube` of its own. `select.ts` asks
230
- * for the adapter's ceiling, which is 48 on this machine against a device default of 16, so
231
- * six fits everywhere and the probe no longer depends on the request being granted.
232
- *
233
- * It used to declare seventeen *samplers* too, and that was the binding constraint: this
234
- * adapter offers exactly sixteen and no request can raise it, so the probe stood down and
235
- * reflective surfaces kept a gradient. The shadow bindings share one declaration — see
236
- * `// wgsl:share shadow` in `flat/preamble.ts`, which costs nothing because they were already
237
- * one object — so the count is four, and the fourth is the normal map's: it is a colour image
238
- * and wants filtering, which a non-filtering shadow sampler cannot give it.
239
- *
240
- * **Counted from the generated bindings rather than typed**: `FLAT_BINDINGS.flatFrag`'s
241
- * widest variant is `directionalShadows+environmentProbe+pointShadows`, seven textures and
242
- * four samplers. A stale number here reads as a device declining a feature it can run.
243
- */
244
- const TEXTURES = 7;
245
- const SAMPLERS = 4;
231
+ * **Counted from the generated bindings rather than typed** (`probeStageCeilings`). It was typed
232
+ * here as seven textures and four samplers while the widest variant grew to sixteen and twelve
233
+ * with the material maps, glass, the fixture atlas and DriftLight: a stale number reads as a
234
+ * device declining a feature it can run, or, the way this one had drifted, as one admitted to a
235
+ * permutation it cannot bind.
236
+ */
237
+ const { textures: TEXTURES, samplers: SAMPLERS } = probeStageCeilings();
246
238
  const fits = device.limits.maxSampledTexturesPerShaderStage >= TEXTURES &&
247
239
  device.limits.maxSamplersPerShaderStage >= SAMPLERS;
248
240
  if (!fits) {
@@ -279,6 +271,7 @@ const VERB_PASS = 3;
279
271
  /** What the depth snapshot copies from, and what it copies into. */
280
272
  const DEPTH_SOURCE = maskOf('sceneDepth');
281
273
  const DEPTH_SNAPSHOT = maskOf('depthSnapshot');
274
+ const COLOR_SNAPSHOT = maskOf('colorSnapshot');
282
275
  /** The mirror's own colour and depth. */
283
276
  const MIRROR_TARGET = maskOf('mirrorColor', 'mirrorDepth');
284
277
  /**
@@ -1280,6 +1273,17 @@ export class WebGPURenderer {
1280
1273
  */
1281
1274
  refractSnapshot = null;
1282
1275
  refractSnapshotView = null;
1276
+ /**
1277
+ * The frame's colour as a registered pass reads it (`reads: ['colorSnapshot']`): the scene's own
1278
+ * size and format, refilled at each such pass's draw. Allocated only once a pass declares the
1279
+ * read, beside the frame's targets, and never in the frame loop. See `takePassColour`.
1280
+ */
1281
+ passColour = null;
1282
+ passColourView = null;
1283
+ /** Registered passes that read the colour, queued this frame for after the upscale. */
1284
+ latePasses = [];
1285
+ /** A late pass draws unjittered, as every late draw does. */
1286
+ latePassJitter = new Float32Array(2);
1283
1287
  /** Whether this frame has taken it. One snapshot serves every refracting draw. */
1284
1288
  refractSnapshotTaken = false;
1285
1289
  /**
@@ -1782,8 +1786,18 @@ export class WebGPURenderer {
1782
1786
  reflectionQueue = new ReflectionQueue();
1783
1787
  ssrLayout;
1784
1788
  ssrResolveLayout;
1789
+ /** The frame's materials' reflections, resolved; null where off. See `frameReflections.ts`. */
1790
+ frameReflections;
1791
+ /** Their surface half, null where they are off or cannot be drawn: `reflectionSurfacePass.ts`. */
1792
+ reflectionSurface;
1793
+ /** The material resolve: its layout, its one block (the blur) and its group over the three maps. */
1794
+ ssrMaterialLayout;
1795
+ ssrMaterialUniforms;
1796
+ ssrMaterialFloats = new Float32Array(SSR_MATERIAL_SIZE / 4);
1797
+ ssrMaterialGroup = null;
1785
1798
  ssrUniforms;
1786
- ssrStaging = new ArrayBuffer(SSR_SLOT * MAX_REFLECTIVE_SURFACES);
1799
+ /* One slot a surface, and one more for the frame's materials' trace. */
1800
+ ssrStaging = new ArrayBuffer(SSR_SLOT * (MAX_REFLECTIVE_SURFACES + 1));
1787
1801
  ssrFloats = new Float32Array(this.ssrStaging);
1788
1802
  ssrReflection = null;
1789
1803
  ssrReflectionView = null;
@@ -2263,9 +2277,16 @@ export class WebGPURenderer {
2263
2277
  camera.updateMatrices(target.width / target.height);
2264
2278
  const { device } = this.surface;
2265
2279
  const { depth, colorMsaa } = target.attachments(this.samples, this.pipelines.format);
2266
- const savedPass = this.pass;
2280
+ /*
2281
+ * **Flushed, and only then set aside.** The flush replays what the frame has recorded, and
2282
+ * replaying opens the frame's pass where none is open yet; saved before it, the pass given back
2283
+ * afterwards was the null that stood there, while the one the flush opened was left open on the
2284
+ * frame's encoder and every later pass on it was refused — a black frame for any capture taken
2285
+ * after the frame had drawn something. `scripts/capture-check.mjs` takes one at three points.
2286
+ */
2267
2287
  if (this.quality.frameGraph)
2268
2288
  this.flushGraph();
2289
+ const savedPass = this.pass;
2269
2290
  const savedEncoder = this.encoder;
2270
2291
  this.markRings();
2271
2292
  const skinMark = this.skinPalettes.mark();
@@ -2371,6 +2392,8 @@ export class WebGPURenderer {
2371
2392
  this.emissiveMap = null;
2372
2393
  if (this.modelMap === texture)
2373
2394
  this.modelMap = null;
2395
+ if (this.overlayMaps === texture)
2396
+ this.overlayMaps = null;
2374
2397
  this.forgetBindingsOf(texture);
2375
2398
  texture.dispose();
2376
2399
  this.captureTargets.get(texture)?.dispose();
@@ -2388,6 +2411,7 @@ export class WebGPURenderer {
2388
2411
  /* Same reasoning, for whichever SDF labels were bound against this atlas. */
2389
2412
  this.sdfTextBindGroups.delete(texture);
2390
2413
  this.flatBindGroups.clear();
2414
+ this.overlayGroups.clear();
2391
2415
  /*
2392
2416
  * The blank one is built with the albedo held aside, because `buildFlatBindGroup` reads
2393
2417
  * `this.albedo` for that slot — leaving it in place would put a real colour map into the
@@ -2723,6 +2747,21 @@ export class WebGPURenderer {
2723
2747
  packAmbientSH(coefficients, this.ambientSH);
2724
2748
  this.material((f) => f.set(this.ambientSH, this.materialField('uAmbientSH')));
2725
2749
  }
2750
+ /**
2751
+ * The overlay the following draws wear, or null for none: pass state, put back to none by
2752
+ * `bindMeshPass`. See `renderer.ts` and `surfaceOverlay.ts`. The numbers go in the material block,
2753
+ * which every variant declares; the atlas goes in a twin of each flat group (`overlayGroup`).
2754
+ */
2755
+ setSurfaceOverlay(overlay) {
2756
+ if (this.surface.lost)
2757
+ return;
2758
+ packSurfaceOverlay(overlay, this.overlayFloats);
2759
+ /* The first overlay laid is when the lit pipelines start carrying one. */
2760
+ if (overlayLays(overlay))
2761
+ this.enableLit('SURFACE_OVERLAY');
2762
+ this.overlayMaps = (overlay?.maps ?? null);
2763
+ this.material((f) => f.set(this.overlayFloats, this.materialField('uOverlay')));
2764
+ }
2726
2765
  /** How reflective the surfaces drawn next are. Clamped, as `renderer.ts` clamps it. */
2727
2766
  setSurfaceReflectivity(amount) {
2728
2767
  this.material((f) => {
@@ -3393,7 +3432,7 @@ export class WebGPURenderer {
3393
3432
  */
3394
3433
  gradeCode() {
3395
3434
  return !this.hasComposite || !this.quality.hdrScene
3396
- ? (OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0)
3435
+ ? forwardTransformCode(this.quality.outputTransform)
3397
3436
  : 0;
3398
3437
  }
3399
3438
  gradeExposure() {
@@ -5313,6 +5352,10 @@ export class WebGPURenderer {
5313
5352
  this.skinPalettes = new SkinPaletteRing(quality.drawsPerFrame);
5314
5353
  this.maxDrawingBufferPixels = quality.maxDrawingBufferPixels;
5315
5354
  this.exposure = quality.outputExposure;
5355
+ resolveFilmicCurve(DEFAULT_FILMIC_CURVE, this.filmConstants);
5356
+ if (quality.outputTransform === 'filmic' && !(quality.screenEffects && quality.hdrScene)) {
5357
+ console.warn(FILMIC_WITHOUT_COMPOSITE);
5358
+ }
5316
5359
  this.flushSchedule =
5317
5360
  quality.frameGraph && quality.identifierGraph
5318
5361
  ? createFlushSchedule(MAX_DRAWS_PER_FRAME)
@@ -5382,6 +5425,25 @@ export class WebGPURenderer {
5382
5425
  console.warn("WebGPU: skinScattering 'screen-space' needs screenEffects to add into; skin is pre-integrated");
5383
5426
  }
5384
5427
  this.skinScatter = skinScreen ? new SkinScatterPass(device) : null;
5428
+ /*
5429
+ * The frame's materials' reflections, where the frame can carry them: scene light to swap the
5430
+ * probe's share in, and one sample, which the trace's depth copy and both maps are. Said once
5431
+ * where it cannot. See `frameReflections.ts`.
5432
+ */
5433
+ this.frameReflections = resolveFrameReflections(quality.screenSpaceReflections);
5434
+ const reflectionsRefusal = this.frameReflections === null
5435
+ ? null
5436
+ : !quality.screenEffects || !quality.hdrScene
5437
+ ? 'the frame keeps no scene light to swap the probe in (`screenEffects` and `hdrScene`)'
5438
+ : this.samples > 1
5439
+ ? 'a multisampled frame has no single depth or colour to march through'
5440
+ : null;
5441
+ if (reflectionsRefusal !== null)
5442
+ console.warn(`WebGPU: ${frameReflectionsRefused(reflectionsRefusal)}`);
5443
+ this.reflectionSurface =
5444
+ this.frameReflections !== null && reflectionsRefusal === null
5445
+ ? new ReflectionSurfacePass(device)
5446
+ : null;
5385
5447
  this.skinPipelines = skinScreen
5386
5448
  ? new PipelineCache(device, SKIN_TARGET_FORMAT, this.samples, glassShadows, quality.clusteredLights)
5387
5449
  : null;
@@ -5737,6 +5799,11 @@ export class WebGPURenderer {
5737
5799
  addressModeU: 'clamp-to-edge',
5738
5800
  addressModeV: 'clamp-to-edge',
5739
5801
  });
5802
+ /* Or a surface overlay's atlas, in the twin a draw wearing one binds: `overlayGroup`. */
5803
+ const maps = this.overlayBinding ? this.overlayMaps : null;
5804
+ if (maps !== null) {
5805
+ return { view: maps.layerView() ?? blankView, sampler: this.refractSampler };
5806
+ }
5740
5807
  return { view: this.refractSnapshotView ?? blankView, sampler: this.refractSampler };
5741
5808
  }
5742
5809
  /*
@@ -5867,6 +5934,8 @@ export class WebGPURenderer {
5867
5934
  { binding: RUSH_TEXTURES.uExposureHeld },
5868
5935
  /* Local exposure's grid, read filtered between tiles. See `localExposure.ts`. */
5869
5936
  { binding: RUSH_TEXTURES.uExposureLocal },
5937
+ /* Each mover's own motion: the reconstruction's target, or the scene where there is none. */
5938
+ { binding: RUSH_TEXTURES.uMotion },
5870
5939
  ]);
5871
5940
  this.rushUniforms = device.createBuffer({
5872
5941
  label: 'post.rushUniforms',
@@ -5884,11 +5953,24 @@ export class WebGPURenderer {
5884
5953
  this.ssrLayout = createPostStageLayout(device, SSR_UNIFORMS, SSR_FRAG_SIZE, [
5885
5954
  { binding: SSR_TEXTURES.uSsrDepth, filterable: false },
5886
5955
  { binding: SSR_TEXTURES.uSsrScene },
5956
+ /* The frame's materials, for the trace that reads them: `frameReflections.ts`. */
5957
+ { binding: SSR_TEXTURES.uSsrProbeMap },
5958
+ { binding: SSR_TEXTURES.uSsrTintMap },
5887
5959
  ]);
5888
5960
  this.ssrResolveLayout = createSsrResolveLayout(device);
5961
+ this.ssrMaterialLayout = createPostStageLayout(device, SSR_MATERIAL_UNIFORMS, SSR_MATERIAL_SIZE, [
5962
+ { binding: SSR_MATERIAL_TEXTURES.uSsrReflection },
5963
+ { binding: SSR_MATERIAL_TEXTURES.uSsrProbeMap },
5964
+ { binding: SSR_MATERIAL_TEXTURES.uSsrTintMap },
5965
+ ]);
5966
+ this.ssrMaterialUniforms = device.createBuffer({
5967
+ label: 'post.ssrMaterialUniforms',
5968
+ size: SSR_MATERIAL_SIZE,
5969
+ usage: USAGE_UNIFORM_DST,
5970
+ });
5889
5971
  this.ssrUniforms = device.createBuffer({
5890
5972
  label: 'post.ssrUniforms',
5891
- size: SSR_SLOT * MAX_REFLECTIVE_SURFACES,
5973
+ size: SSR_SLOT * (MAX_REFLECTIVE_SURFACES + 1),
5892
5974
  usage: USAGE_UNIFORM_DST,
5893
5975
  });
5894
5976
  this.decalUniforms = device.createBuffer({
@@ -6304,7 +6386,11 @@ export class WebGPURenderer {
6304
6386
  /* Last frame's snapshot is last frame's scene. */
6305
6387
  this.refractSnapshotTaken = false;
6306
6388
  this.skinScattered = false;
6389
+ /* One frame of the jitter's sequence however many times the frame binds its camera. */
6390
+ this.temporalHistory.nextFrame();
6391
+ this.latePasses.length = 0;
6307
6392
  this.skinScatter?.reset();
6393
+ this.reflectionSurface?.reset();
6308
6394
  this.plumeVerts.reset();
6309
6395
  this.plumeFrags.reset();
6310
6396
  this.waterVerts.reset();
@@ -6861,6 +6947,9 @@ export class WebGPURenderer {
6861
6947
  f[at('uWriteMode') + 1] = 0;
6862
6948
  /* 1, not 0: a multiplier's identity is one. See `setEnvironmentGain`. */
6863
6949
  f[at('uEnvironmentDials') + 2] = 1;
6950
+ /* No overlay until a draw sets one. See `setSurfaceOverlay`. */
6951
+ this.overlayFloats.fill(0);
6952
+ f.fill(0, at('uOverlay'), at('uOverlay') + OVERLAY_FLOATS);
6864
6953
  /* The frame's own ambient until a draw asks for its own. See `setAmbientSH`. */
6865
6954
  f.fill(0, at('uAmbientSH'), at('uAmbientSH') + AMBIENT_SH_FLOATS);
6866
6955
  /*
@@ -7229,6 +7318,8 @@ export class WebGPURenderer {
7229
7318
  this.emissiveMap = null;
7230
7319
  this.modelMap = null;
7231
7320
  this.bindGroup = this.blankAlbedoBindGroup;
7321
+ /* Nor an overlay; its numbers are zeroed with the block's other defaults. */
7322
+ this.overlayMaps = null;
7232
7323
  this.uvScale[0] = 1;
7233
7324
  this.uvScale[1] = 1;
7234
7325
  this.uvOffset[0] = 0;
@@ -7310,6 +7401,9 @@ export class WebGPURenderer {
7310
7401
  */
7311
7402
  if (this.tracksMotionNow())
7312
7403
  this.recordDrawMotion(mesh, model, previousModel);
7404
+ else if (previousModel !== null && this.quality.cameraMotionBlur * this.motionBlurScale > 0) {
7405
+ this.warnObjectBlur();
7406
+ }
7313
7407
  noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
7314
7408
  /* An opaque draw meets the medium as the dial says: `submitMesh` reads an absent `fog` as
7315
7409
  true, which is a translucent draw's default and would put every opaque draw back in the fog
@@ -7665,6 +7759,10 @@ export class WebGPURenderer {
7665
7759
  if (splits) {
7666
7760
  this.keepSkinDiffuse(mesh, key, base, skinned, morphed, deltas, layer, eight, clothed, slot, material);
7667
7761
  }
7762
+ /* And its surface half, for the frame's materials' reflections. See `keepReflectionSurface`. */
7763
+ if (!blend && lit && depthWrite && this.reflectionSurfaceKeeps()) {
7764
+ this.keepReflectionSurface(mesh, key, base, skinned, morphed, deltas, layer, eight, clothed, modelKind, slot, material, null, 1, null);
7765
+ }
7668
7766
  }
7669
7767
  /* Back to the defaults for whatever is drawn next, exactly as `renderer.ts` restores them —
7670
7768
  unconditionally, because the scratch state above is shared by every draw after this one
@@ -7794,6 +7892,7 @@ export class WebGPURenderer {
7794
7892
  const glassy = resolveGlass(options.glass, this.glassScratch);
7795
7893
  if ((refraction <= 0 && !glassy) || !this.takeRefractSnapshot())
7796
7894
  return false;
7895
+ this.seeThroughBound = true;
7797
7896
  const glass = this.glassScratch;
7798
7897
  /* Strength, path thickness, transmission, frost: one vector, for the budget the shader states. */
7799
7898
  const seeAt = this.materialField('uSeeThrough');
@@ -7814,6 +7913,7 @@ export class WebGPURenderer {
7814
7913
  }
7815
7914
  /** Put the scratch back after `bindSeeThrough`: refraction and glass off. */
7816
7915
  releaseSeeThrough() {
7916
+ this.seeThroughBound = false;
7817
7917
  const seeAt = this.materialField('uSeeThrough');
7818
7918
  this.perFrameFloats[seeAt] = 0;
7819
7919
  this.perFrameFloats[seeAt + 2] = 0;
@@ -7978,6 +8078,10 @@ export class WebGPURenderer {
7978
8078
  else
7979
8079
  pass.drawIndexedIndirect(cut.args, 0);
7980
8080
  }
8081
+ /* And its surface half, for the frame's materials' reflections, with the same survivors. */
8082
+ if (!blend && lit && depthWrite && this.reflectionSurfaceKeeps()) {
8083
+ this.keepReflectionSurface(mesh, key, base, false, false, null, layer, false, false, modelKind, slot, material, instanceBuffer, count, cut === null ? null : cut.args);
8084
+ }
7981
8085
  }
7982
8086
  /* Back to the defaults, unconditionally and for the reason `drawMesh` gives at length: this
7983
8087
  scratch block is cumulative state shared by every draw after this one, and an early return
@@ -8606,6 +8710,20 @@ export class WebGPURenderer {
8606
8710
  warnedNoPointShadowArray = false;
8607
8711
  /** Said once, for the retired ambient dial. */
8608
8712
  warnedEnvironmentAmbient = false;
8713
+ /** The overlay the draws wear, `uOverlay`: see `setSurfaceOverlay`. */
8714
+ overlayFloats = new Float32Array(OVERLAY_FLOATS);
8715
+ /** Its atlas, bound where the refraction copy goes in a twin of each flat group. */
8716
+ overlayMaps = null;
8717
+ /**
8718
+ * Each flat group's twin with the atlas in the refraction slot, by the group and the atlas, built
8719
+ * on the first draw that wears it and dropped wherever `flatBindGroups` is.
8720
+ */
8721
+ overlayGroups = new Map();
8722
+ /** True while a twin is built: what `flatTextures` hands the refraction slot then. */
8723
+ overlayBinding = false;
8724
+ /** True between `bindSeeThrough` and `releaseSeeThrough`: that draw reads the copy, not the atlas. */
8725
+ seeThroughBound = false;
8726
+ warnedOverlayRefraction = false;
8609
8727
  castingWorldIndices = new Int32Array(POINT_SHADOW_POOL).fill(-1);
8610
8728
  resolvedPointShadows = createResolvedPointShadows(MAX_POINT_LIGHTS);
8611
8729
  /** One face's view-projection, rebuilt per face rather than allocated. */
@@ -8956,6 +9074,41 @@ export class WebGPURenderer {
8956
9074
  buildFlatBindGroup() {
8957
9075
  return createFlatBindGroup(this.surface.device, this.bindGroupLayout, this.variant, this.perDraw.buffer, this.perFrame.buffer, this.flatTextures);
8958
9076
  }
9077
+ /**
9078
+ * The open material's flat group, or its twin with the surface overlay's atlas in the refraction
9079
+ * slot where the draw wears one and does not refract. A twin is built from the live material,
9080
+ * which `bindGroup` always is, and kept by the group and the atlas.
9081
+ *
9082
+ * **A draw that refracts, or is glass, keeps the frame's copy** and its overlay reads no image —
9083
+ * the shader asks the same of `uSeeThrough` (`overlayImagesBound`) — said once.
9084
+ */
9085
+ overlayGroup() {
9086
+ const maps = this.overlayMaps;
9087
+ if (maps === null)
9088
+ return this.bindGroup;
9089
+ if (this.seeThroughBound) {
9090
+ if (!this.warnedOverlayRefraction) {
9091
+ this.warnedOverlayRefraction = true;
9092
+ console.warn('WebGPU: a draw that refracts or is glass wears a surface overlay with maps. The frame ' +
9093
+ "copy it reads takes the overlay's slot, so its rim, dissolve and wrinkles read no image.");
9094
+ }
9095
+ return this.bindGroup;
9096
+ }
9097
+ let byMaps = this.overlayGroups.get(this.bindGroup);
9098
+ if (byMaps === undefined) {
9099
+ byMaps = new Map();
9100
+ this.overlayGroups.set(this.bindGroup, byMaps);
9101
+ }
9102
+ let twin = byMaps.get(maps);
9103
+ if (twin === undefined) {
9104
+ this.overlayBinding = true;
9105
+ twin = this.buildFlatBindGroup();
9106
+ this.overlayBinding = false;
9107
+ this.bindGroupBudget.ask();
9108
+ byMaps.set(maps, twin);
9109
+ }
9110
+ return twin;
9111
+ }
8959
9112
  /**
8960
9113
  * The bind group a draw uses, skinned or not.
8961
9114
  *
@@ -8968,8 +9121,9 @@ export class WebGPURenderer {
8968
9121
  * rebuild this every draw.
8969
9122
  */
8970
9123
  flatBindGroupFor(skinned, morphed, deltas) {
9124
+ const base = this.overlayGroup();
8971
9125
  if (!skinned && !morphed)
8972
- return this.bindGroup;
9126
+ return base;
8973
9127
  const palette = skinned ? this.skinPalettes.view(this.skinPaletteSlot) : null;
8974
9128
  /* Slot `k` caches at `k + 1`; index 0 is the morphed draw that is not skinned and has no
8975
9129
  slot of its own. See `skinnedGroups`. */
@@ -8977,13 +9131,16 @@ export class WebGPURenderer {
8977
9131
  /* A skinned group carries the cloth's views, or the stand-in's: see `clothViews`. */
8978
9132
  const cloth = skinned ? this.clothViews() : null;
8979
9133
  if ((this.skinnedGroups[at] ?? null) === null ||
8980
- this.skinnedGroupsBeside[at] !== this.bindGroup ||
9134
+ this.skinnedGroupsBeside[at] !== base ||
8981
9135
  this.skinnedGroupsPalette[at] !== palette ||
8982
9136
  this.skinnedGroupsDeltas[at] !== deltas ||
8983
9137
  this.skinnedGroupsCloth[at] !== (cloth?.particles ?? null) ||
8984
9138
  this.skinnedGroupsClothBinding[at] !== (cloth?.binding ?? null)) {
9139
+ /* Built from the live material, with the overlay's atlas where `base` is its twin. */
9140
+ this.overlayBinding = base !== this.bindGroup;
8985
9141
  this.skinnedGroups[at] = createFlatBindGroup(this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, this.perDraw.buffer, this.perFrame.buffer, this.flatTextures, palette, deltas, cloth);
8986
- this.skinnedGroupsBeside[at] = this.bindGroup;
9142
+ this.overlayBinding = false;
9143
+ this.skinnedGroupsBeside[at] = base;
8987
9144
  this.skinnedGroupsPalette[at] = palette;
8988
9145
  this.skinnedGroupsDeltas[at] = deltas;
8989
9146
  this.skinnedGroupsCloth[at] = cloth?.particles ?? null;
@@ -9050,6 +9207,7 @@ export class WebGPURenderer {
9050
9207
  */
9051
9208
  rebuildFlatGroupsForNewRings() {
9052
9209
  this.flatBindGroups.clear();
9210
+ this.overlayGroups.clear();
9053
9211
  this.flatEnvironment = this.flatTextures('uEnvironment').view;
9054
9212
  this.skinnedGroups.length = 0;
9055
9213
  const albedo = this.albedo;
@@ -9102,6 +9260,7 @@ export class WebGPURenderer {
9102
9260
  */
9103
9261
  rebuildFlatBindGroup() {
9104
9262
  this.flatBindGroups.clear();
9263
+ this.overlayGroups.clear();
9105
9264
  this.flatEnvironment = this.flatTextures('uEnvironment').view;
9106
9265
  if (this.albedo === null &&
9107
9266
  this.normalMap === null &&
@@ -9327,9 +9486,14 @@ export class WebGPURenderer {
9327
9486
  ? (OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0)
9328
9487
  : 0;
9329
9488
  f[at('uOutputExposure')] = graded ? this.exposure : 1;
9489
+ f.set(this.filmA, at('uFilmA'));
9490
+ f.set(this.filmB, at('uFilmB'));
9491
+ f[at('uDisplayHeadroom')] = this.headroom;
9330
9492
  f.set(this.reprojection, at('uReprojection'));
9331
9493
  f[at('uMotionStrength')] = motionStrength;
9332
- f[at('uMotionMax')] = MOTION_BLUR_MAX_UV;
9494
+ f[at('uMotionMax')] = this.motionBlurMax;
9495
+ /* Each mover's own motion, where the reconstruction drew its target this frame. */
9496
+ f[at('uObjectMotion')] = this.reconstructing && this.reconMotionView !== null ? 1 : 0;
9333
9497
  f[at('uAoStrength')] = this.frameProjection === null ? 0 : aoStrength;
9334
9498
  /*
9335
9499
  * The occlusion read where this frame's jitter put it (`uAoOffset`): measured from the jittered
@@ -9373,6 +9537,7 @@ export class WebGPURenderer {
9373
9537
  f[at('uGradeSize')] = this.gradeSize;
9374
9538
  /* The lens and the print. See `setVignette` and `setFilmGrain`. */
9375
9539
  f[at('uVignette')] = this.vignette;
9540
+ f.set(this.fringe, at('uFringe'));
9376
9541
  f[at('uAutoExposure')] = adapting ? this.autoExposure : 0;
9377
9542
  f[at('uLocalExposure')] = measuring && localising ? this.localExposure : 0;
9378
9543
  f[at('uGrain')] = this.grain;
@@ -9410,7 +9575,7 @@ export class WebGPURenderer {
9410
9575
  return;
9411
9576
  if (this.aoTargetView === null)
9412
9577
  return;
9413
- this.rushBindGroup = this.buildRushBindGroup('post.rushBindGroup', this.sceneColorView);
9578
+ this.rushBindGroup = this.buildRushBindGroup('post.rushBindGroup', this.sceneColorView, this.sceneColorView);
9414
9579
  /*
9415
9580
  * **And the reconstruction's twin, from the same three call sites.** A colour grade replaces
9416
9581
  * the lookup table and a resize replaces the depth and the occlusion target, and both groups
@@ -9418,11 +9583,13 @@ export class WebGPURenderer {
9418
9583
  * come to read a stale view on exactly the frames a consumer changed something.
9419
9584
  */
9420
9585
  if (this.reconShownView !== null) {
9421
- this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
9586
+ this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView, this.reconMotionView ?? this.reconShownView);
9422
9587
  }
9423
9588
  }
9424
9589
  /** The composite's group over one scene source. See `rebuildRushBindGroup`. */
9425
- buildRushBindGroup(label, sceneView) {
9590
+ buildRushBindGroup(label, sceneView,
9591
+ /** Each mover's own motion, or a stand-in the shader is told not to read. */
9592
+ motionView) {
9426
9593
  const { device } = this.surface;
9427
9594
  if (this.resolvedDepthView === null || this.aoTargetView === null)
9428
9595
  return null;
@@ -9451,6 +9618,7 @@ export class WebGPURenderer {
9451
9618
  and a declared binding still needs a complete texture, as the bloom fallback says. */
9452
9619
  ...entry(RUSH_TEXTURES.uExposureHeld, this.exposurePass?.heldView ?? sceneView),
9453
9620
  ...entry(RUSH_TEXTURES.uExposureLocal, this.exposurePass?.localView ?? sceneView),
9621
+ ...entry(RUSH_TEXTURES.uMotion, motionView),
9454
9622
  ],
9455
9623
  layout: this.rushLayout,
9456
9624
  });
@@ -9707,7 +9875,7 @@ export class WebGPURenderer {
9707
9875
  this.reconSharpenGroups[write] = build(`recon.sharpen${String(write)}`, target, this.reconShownView, records, nextRecords, swings, nextSwings);
9708
9876
  }
9709
9877
  /* The composite's own group, over the resolved picture instead of the scene target. */
9710
- this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
9878
+ this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView, this.reconMotionView ?? this.reconShownView);
9711
9879
  /* And the scene's, because `ensureComposite` built it before this one existed. */
9712
9880
  this.rebuildRushBindGroup();
9713
9881
  /* And the bloom's, which blooms the resolved picture: see `rebuildBloomGroups`. */
@@ -9796,7 +9964,7 @@ export class WebGPURenderer {
9796
9964
  if (!this.reconstructing || late === null || shown === null || depth === null)
9797
9965
  return;
9798
9966
  const oitSet = this.oitActive && this.translucentQueue.length > 0;
9799
- if (late.pending === 0 && !oitSet)
9967
+ if (late.pending === 0 && !oitSet && this.latePasses.length === 0)
9800
9968
  return;
9801
9969
  const width = Math.max(1, this.surface.canvas.width);
9802
9970
  const height = Math.max(1, this.surface.canvas.height);
@@ -9808,21 +9976,60 @@ export class WebGPURenderer {
9808
9976
  this.refractSnapshotTaken = true;
9809
9977
  }
9810
9978
  this.lateRefractWanted = false;
9811
- if (late.pending === 0)
9812
- return;
9813
- const pass = encoder.beginRenderPass({
9814
- label: 'recon.late',
9815
- timestampWrites: this.gpuTimer.writesFor(),
9816
- colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
9817
- depthStencilAttachment: {
9818
- view: late.depthView(width, height),
9819
- depthLoadOp: 'load',
9820
- /* Kept: the order-independent set replays against it after this pass. */
9821
- depthStoreOp: 'store',
9822
- },
9823
- });
9824
- late.replay(pass, this.issueBound);
9825
- pass.end();
9979
+ if (late.pending > 0) {
9980
+ const pass = encoder.beginRenderPass({
9981
+ label: 'recon.late',
9982
+ timestampWrites: this.gpuTimer.writesFor(),
9983
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
9984
+ depthStencilAttachment: {
9985
+ view: late.depthView(width, height),
9986
+ depthLoadOp: 'load',
9987
+ /* Kept: the order-independent set replays against it after this pass. */
9988
+ depthStoreOp: 'store',
9989
+ },
9990
+ });
9991
+ late.replay(pass, this.issueBound);
9992
+ pass.end();
9993
+ }
9994
+ this.runLatePasses(encoder, shown, late.depthView(width, height));
9995
+ }
9996
+ /**
9997
+ * The registered passes `drawPass` queued for after the upscale: each handed a fresh copy of the
9998
+ * reconstructed picture with every late draw in it, in the output-size texture the late
9999
+ * refraction keeps (its own reads are done by now: they were recorded before this copy), then
10000
+ * drawn over the picture in a pass of its own.
10001
+ */
10002
+ runLatePasses(encoder, shown, depth) {
10003
+ const copy = this.refractSnapshot;
10004
+ const source = this.reconShown;
10005
+ for (const handle of this.latePasses) {
10006
+ const definition = passAt(this.passes, handle);
10007
+ if (definition === undefined)
10008
+ continue;
10009
+ if (copy !== null && source !== null) {
10010
+ encoder.copyTextureToTexture({ texture: source }, { texture: copy }, [
10011
+ copy.width,
10012
+ copy.height,
10013
+ ]);
10014
+ }
10015
+ const pass = encoder.beginRenderPass({
10016
+ label: 'recon.latePass',
10017
+ timestampWrites: this.gpuTimer.writesFor(),
10018
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
10019
+ depthStencilAttachment: { view: depth, depthLoadOp: 'load', depthStoreOp: 'store' },
10020
+ });
10021
+ const context = this.passContext;
10022
+ context.pass = pass;
10023
+ context.outputTransform = this.gradeCode();
10024
+ context.outputExposure = this.gradeExposure();
10025
+ context.jitter = this.latePassJitter;
10026
+ context.format = RECON_HISTORY_FORMAT;
10027
+ context.depthFormat = DEPTH_FORMAT;
10028
+ context.samples = 1;
10029
+ definition.draw(context);
10030
+ context.jitter = this.passJitter;
10031
+ pass.end();
10032
+ }
9826
10033
  }
9827
10034
  /** Whether this draw lands in the reconstructed picture. See `lateRouting.ts`. */
9828
10035
  tracksMotionNow() {
@@ -10189,7 +10396,10 @@ export class WebGPURenderer {
10189
10396
  * other is the disagreement the parity rule exists to prevent. Said once rather than dropped.
10190
10397
  */
10191
10398
  runReflections(encoder) {
10192
- if (this.reflectionQueue.length === 0)
10399
+ const frameReflections = this.frameReflections;
10400
+ const surfaceHalf = this.reflectionSurface;
10401
+ const materials = frameReflections !== null && surfaceHalf !== null;
10402
+ if (this.reflectionQueue.length === 0 && !materials)
10193
10403
  return;
10194
10404
  const scene = this.sceneColorView;
10195
10405
  const reflection = this.ssrReflectionView;
@@ -10234,9 +10444,63 @@ export class WebGPURenderer {
10234
10444
  f[base + at('uSsrThickness')] = surface.thicknessM;
10235
10445
  f[base + at('uSsrSteps')] = surface.steps;
10236
10446
  f[base + at('uSsrEdgeFade')] = REFLECTION_EDGE_FADE;
10447
+ f[base + at('uSsrMaterial')] = 0;
10237
10448
  slot++;
10238
10449
  });
10450
+ const regions = slot;
10451
+ /* The frame's materials' trace takes the slot after the surfaces': the box, its axis and its
10452
+ strength unread, the march and the ceiling its own. See `frameReflections.ts`. */
10453
+ if (materials) {
10454
+ const base = (slot * SSR_SLOT) / 4;
10455
+ f.set(this.ssrDepthToWorld, base + at('uSsrDepthToWorld'));
10456
+ f.set(this.ssrViewProj, base + at('uSsrViewProj'));
10457
+ f.set(this.frameEye, base + at('uSsrEye'));
10458
+ f.fill(0, base + at('uSsrAxis'), base + at('uSsrAxis') + 3);
10459
+ f.fill(1, base + at('uSsrTint'), base + at('uSsrTint') + 3);
10460
+ f[base + at('uSsrStrength')] = 1;
10461
+ f[base + at('uSsrFresnel')] = 0;
10462
+ f[base + at('uSsrFacingCos')] = -1;
10463
+ f[base + at('uSsrReach')] = frameReflections.reachM;
10464
+ f[base + at('uSsrThickness')] = frameReflections.thicknessM;
10465
+ f[base + at('uSsrSteps')] = frameReflections.steps;
10466
+ f[base + at('uSsrEdgeFade')] = REFLECTION_EDGE_FADE;
10467
+ f[base + at('uSsrMaterial')] = 1;
10468
+ f[base + at('uSsrMaxRoughness')] = frameReflections.maxRoughness;
10469
+ slot++;
10470
+ }
10239
10471
  device.queue.writeBuffer(this.ssrUniforms, 0, this.ssrStaging, 0, slot * SSR_SLOT);
10472
+ const tracePipeline = postPipeline(this.pipelines, device, this.ssrLayout, 'post.ssrTrace', SSR_TRACE_FRAG_WGSL, this.pipelines.format, PREMULTIPLIED_OVER_BLEND);
10473
+ if (materials && this.depthView !== null && this.ssrMaterialGroup !== null) {
10474
+ /* The frame's opaque lit draws again, into what the trace and the resolve read. */
10475
+ surfaceHalf.draw(encoder, this.depthView, this.issueBound);
10476
+ const trace = encoder.beginRenderPass({
10477
+ label: 'post.ssr.materialTrace',
10478
+ colorAttachments: [
10479
+ { view: reflection, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
10480
+ ],
10481
+ });
10482
+ trace.setPipeline(tracePipeline);
10483
+ trace.setBindGroup(0, group, [regions * SSR_SLOT]);
10484
+ trace.draw(3);
10485
+ trace.end();
10486
+ /* The blur's texel and its radius at roughness 1, as a share of the frame's height. */
10487
+ const m = this.ssrMaterialFloats;
10488
+ const blurAt = this.postField(SSR_MATERIAL_FIELDS, 'uSsrBlur');
10489
+ m[blurAt] = 1 / Math.max(width, 1);
10490
+ m[blurAt + 1] = 1 / Math.max(height, 1);
10491
+ m[blurAt + 2] = frameReflections.blur * height;
10492
+ device.queue.writeBuffer(this.ssrMaterialUniforms, 0, m);
10493
+ const resolve = encoder.beginRenderPass({
10494
+ label: 'post.ssr.materialResolve',
10495
+ colorAttachments: [{ view: scene, loadOp: 'load', storeOp: 'store' }],
10496
+ });
10497
+ resolve.setPipeline(postPipeline(this.pipelines, device, this.ssrMaterialLayout, 'post.ssrMaterialResolve', SSR_MATERIAL_RESOLVE_FRAG_WGSL, this.pipelines.format, FRAME_REFLECTION_BLEND));
10498
+ resolve.setBindGroup(0, this.ssrMaterialGroup, [0]);
10499
+ resolve.draw(3);
10500
+ resolve.end();
10501
+ }
10502
+ if (regions === 0)
10503
+ return;
10240
10504
  /* Cleared whole and then scissored per surface: the resolve reads every pixel, and one left
10241
10505
  from the last frame would composite that reflection over this frame's scene. */
10242
10506
  const trace = encoder.beginRenderPass({
@@ -10245,7 +10509,7 @@ export class WebGPURenderer {
10245
10509
  { view: reflection, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
10246
10510
  ],
10247
10511
  });
10248
- trace.setPipeline(postPipeline(this.pipelines, device, this.ssrLayout, 'post.ssrTrace', SSR_TRACE_FRAG_WGSL, this.pipelines.format, PREMULTIPLIED_OVER_BLEND));
10512
+ trace.setPipeline(tracePipeline);
10249
10513
  slot = 0;
10250
10514
  this.reflectionQueue.replay((surface) => {
10251
10515
  const index = slot++;
@@ -10877,6 +11141,50 @@ export class WebGPURenderer {
10877
11141
  !this.overlayActive &&
10878
11142
  this.oitMode === 'none');
10879
11143
  }
11144
+ /** Whether a draw now is the frame's own, and so has a surface half to keep. */
11145
+ reflectionSurfaceKeeps() {
11146
+ return (this.reflectionSurface !== null &&
11147
+ !this.reflectionPassActive &&
11148
+ !this.probePassActive &&
11149
+ !this.capturePassActive &&
11150
+ !this.framePresented &&
11151
+ !this.overlayActive &&
11152
+ this.oitMode === 'none');
11153
+ }
11154
+ /**
11155
+ * An opaque lit draw's surface half, kept for the frame's materials' reflections: the draw again,
11156
+ * with the same slots, buffers and bind group, through the pipeline with `REFLECTION_SURFACE` on —
11157
+ * which writes what the pixel reflects of its environment into the two targets
11158
+ * `reflectionSurfacePass.ts` holds and tests for the depth its frame draw wrote. Replayed when the
11159
+ * frame's pass has ended, before the trace reads them. See `shaders/flat/reflectionSurface.ts`.
11160
+ */
11161
+ keepReflectionSurface(mesh, frameKey, base, skinned, morphed, deltas, layer, eight, clothed, modelKind, slot, material, instanceBuffer, instances, indirect) {
11162
+ const surface = this.reflectionSurface;
11163
+ const present = this.meshPresent.get(base);
11164
+ if (surface === null || present === undefined)
11165
+ return;
11166
+ const key = `${frameKey}|rs`;
11167
+ const pipeline = this.pipelines.peek(key) ??
11168
+ flatPipeline(this.pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, false, skinned, morphed, true, layer, instanceBuffer !== null, 'none', this.materialDoubleSided, false, eight, clothed, modelKind, 'whole', true);
11169
+ const command = surface.take();
11170
+ command.pipeline = pipeline;
11171
+ command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
11172
+ command.offsetA = slot;
11173
+ command.offsetB = material;
11174
+ command.offsetCount = 2;
11175
+ let index = 0;
11176
+ for (; index < mesh.vertexBuffers.length; index++) {
11177
+ command.vertexBuffers[index] = mesh.vertexBuffers[index];
11178
+ }
11179
+ if (instanceBuffer !== null)
11180
+ command.vertexBuffers[index++] = instanceBuffer;
11181
+ command.vertexCount = index;
11182
+ command.indexBuffer = mesh.indexBuffer;
11183
+ command.indexed = true;
11184
+ command.count = mesh.indexCount;
11185
+ command.instances = instances;
11186
+ command.indirect = indirect;
11187
+ }
10880
11188
  /**
10881
11189
  * A skin draw's diffuse and colour halves, kept for the spread: the frame's draw again, twice,
10882
11190
  * with the same slots, buffers and bind group, through the pipelines that write the diffuse's
@@ -10924,13 +11232,19 @@ export class WebGPURenderer {
10924
11232
  const skin = this.skinScatter;
10925
11233
  if (skin === null || this.skinScattered)
10926
11234
  return;
11235
+ /*
11236
+ * **Not the frame's picture, so not the frame's moment**: a blended draw in a mirror, a probe
11237
+ * bake or a scene capture lands over none of the frame's skin. Latching there switched the
11238
+ * spread off for the frame, so a capture with a glowing sign in it left every face drawn after
11239
+ * it flat. `scripts/capture-check.mjs`.
11240
+ */
11241
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive)
11242
+ return;
10927
11243
  /* Latched whether or not skin was kept: a skin drawn after this point is the whole surface. */
10928
11244
  this.skinScattered = true;
10929
11245
  if (!skin.pending)
10930
11246
  return;
10931
- if (this.reflectionPassActive || this.probePassActive || this.oitReplaying)
10932
- return;
10933
- if (this.capturePassActive)
11247
+ if (this.oitReplaying)
10934
11248
  return;
10935
11249
  const encoder = this.encoder;
10936
11250
  const depth = this.depthView;
@@ -11045,6 +11359,66 @@ export class WebGPURenderer {
11045
11359
  this.volumeDepthTaken = true;
11046
11360
  return true;
11047
11361
  }
11362
+ /** The colour copy a registered pass reads, at the scene's size and format. See `passColour`. */
11363
+ allocatePassColour() {
11364
+ const source = this.sceneColor;
11365
+ if (source === null || this.passColour !== null)
11366
+ return;
11367
+ this.passColour = this.surface.device.createTexture({
11368
+ label: 'pass.sceneColor',
11369
+ size: [source.width, source.height],
11370
+ format: this.pipelines.format,
11371
+ usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
11372
+ });
11373
+ this.passColourView = this.passColour.createView();
11374
+ }
11375
+ /**
11376
+ * The frame's colour so far, into `passColour`, for a registered pass that reads it: the
11377
+ * boundary `takeRefractSnapshot` describes — flush, end the pass, copy what its end resolved,
11378
+ * reopen with `load` — and refused where that one is, in a mirror, a probe bake or a capture,
11379
+ * whose pictures are not the frame's. **Not latched**, unlike refraction's: each reading pass
11380
+ * sees the passes drawn before it, at a full-frame copy each.
11381
+ */
11382
+ takePassColour() {
11383
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive)
11384
+ return;
11385
+ const copy = this.passColour;
11386
+ const source = this.sceneColor;
11387
+ const encoder = this.encoder;
11388
+ if (copy === null || source === null || encoder === null || this.pass === null)
11389
+ return;
11390
+ if (this.quality.frameGraph)
11391
+ this.flushGraph();
11392
+ this.pass.end();
11393
+ encoder.copyTextureToTexture({ texture: source }, { texture: copy }, [copy.width, copy.height]);
11394
+ this.pass = this.reopenFramePass('pass.colourTaken');
11395
+ }
11396
+ /**
11397
+ * The frame's pass again after a copy between passes, loading everything already drawn: the same
11398
+ * attachments the copy's caller ended, with nothing cleared.
11399
+ */
11400
+ reopenFramePass(label) {
11401
+ const encoder = this.encoder;
11402
+ const target = this.compositeTarget();
11403
+ if (encoder === null || target === null)
11404
+ return null;
11405
+ const multisampled = this.compositeMsaa() !== null;
11406
+ return encoder.beginRenderPass({
11407
+ label,
11408
+ timestampWrites: this.gpuTimer.writesFor(),
11409
+ colorAttachments: [
11410
+ {
11411
+ view: this.compositeMsaa() ?? target,
11412
+ resolveTarget: multisampled ? target : undefined,
11413
+ loadOp: 'load',
11414
+ storeOp: resolvedStoreOp(multisampled, false, this.quality.discardResolvedAttachments),
11415
+ },
11416
+ ],
11417
+ depthStencilAttachment: this.depthView === null
11418
+ ? undefined
11419
+ : { view: this.depthView, depthLoadOp: 'load', depthStoreOp: 'store' },
11420
+ });
11421
+ }
11048
11422
  /**
11049
11423
  * The pass to record into, reopening one on the canvas if the frame has already gone.
11050
11424
  *
@@ -11226,6 +11600,8 @@ export class WebGPURenderer {
11226
11600
  /* And a march reads the same copy, for the same reason and with the same failure if it is
11227
11601
  discarded: a depth of zero is the far plane reversed, so every ray finds nothing. */
11228
11602
  this.reflectionQueue.length > 0 ||
11603
+ /* And the frame's materials' reflections, whose surface half tests against the depth too. */
11604
+ this.reflectionSurface !== null ||
11229
11605
  /* And the medium's own march, which stops where the frame's depth says a surface is. Left
11230
11606
  out of this, every ray runs to `maxDistance` through the walls and the fog is drawn on
11231
11607
  top of the room rather than in it. */
@@ -12042,6 +12418,9 @@ export class WebGPURenderer {
12042
12418
  const reads = maskOf(...(definition.reads ?? []));
12043
12419
  this.passReads.set(handle, reads);
12044
12420
  this.passReadsUnion |= reads;
12421
+ /* The colour copy a reader needs, now rather than at its first draw: see `passColour`. */
12422
+ if ((reads & COLOR_SNAPSHOT) !== 0 && this.passColour === null)
12423
+ this.allocatePassColour();
12045
12424
  if (definition.prepare !== undefined)
12046
12425
  this.preparingPasses++;
12047
12426
  definition.init?.(this.passDevice());
@@ -12061,6 +12440,8 @@ export class WebGPURenderer {
12061
12440
  environment: null,
12062
12441
  distanceField: null,
12063
12442
  jitter: this.passJitter,
12443
+ sceneDepth: null,
12444
+ sceneColor: null,
12064
12445
  };
12065
12446
  /**
12066
12447
  * The object handed to a pass as `PrepareContext.environment`, rebuilt only when it changes.
@@ -12146,6 +12527,15 @@ export class WebGPURenderer {
12146
12527
  * than merely what happens.
12147
12528
  */
12148
12529
  this.prepareContext.distanceField = this.fieldComposer?.field ?? null;
12530
+ /* Settled for the frame: `ensureComposite` has run, so the view stays until the pass draws. */
12531
+ this.prepareContext.sceneDepth = this.resolvedDepthView;
12532
+ /*
12533
+ * Under a reconstruction a colour reader draws after the upscale (see `drawPass`), so it reads
12534
+ * the output-size copy the late refraction keeps rather than the render's.
12535
+ */
12536
+ this.prepareContext.sceneColor = this.reconstructionWanted
12537
+ ? this.refractSnapshotView
12538
+ : this.passColourView;
12149
12539
  for (const definition of this.passes.definitions) {
12150
12540
  definition?.prepare?.(this.prepareContext);
12151
12541
  }
@@ -12163,6 +12553,35 @@ export class WebGPURenderer {
12163
12553
  const definition = passAt(this.passes, handle);
12164
12554
  if (definition === undefined)
12165
12555
  return;
12556
+ const reads = this.passReads.get(handle) ?? 0;
12557
+ /*
12558
+ * **Under a reconstruction, a pass that reads the frame's colour draws after the blended draws
12559
+ * do.** Those land after the upscale, at the output's size, so a pass drawn among them at the
12560
+ * render's would read a frame without them and then be covered by them — a full-screen effect
12561
+ * with the hit sparks drawn sharp over it. Queued instead, and drawn by `runLate` over the
12562
+ * reconstructed picture, after every late draw, whatever order it was asked in.
12563
+ */
12564
+ if ((reads & COLOR_SNAPSHOT) !== 0 && this.drawsLate()) {
12565
+ this.latePasses.push(handle);
12566
+ return;
12567
+ }
12568
+ /*
12569
+ * A pass that reads the depth copy gets it with everything drawn so far: the copy is taken
12570
+ * here, once a frame, as a soft-edged sprite takes it. Before `openPass` below, because taking
12571
+ * it ends the frame's pass and opens another.
12572
+ */
12573
+ if ((reads & DEPTH_SNAPSHOT) !== 0 && !this.framePresented && this.openPass() !== null) {
12574
+ this.takeVolumeDepth();
12575
+ }
12576
+ /*
12577
+ * And one that reads the colour gets it afresh, with everything drawn before it — a full-screen
12578
+ * pass drawn last reads the whole frame and an effect drawn mid-frame reads the frame so far.
12579
+ * A copy a reading pass, where the depth above is one a frame: what a colour reader draws is
12580
+ * itself part of what the next one should see.
12581
+ */
12582
+ if ((reads & COLOR_SNAPSHOT) !== 0 && !this.framePresented && this.openPass() !== null) {
12583
+ this.takePassColour();
12584
+ }
12166
12585
  if (!this.quality.frameGraph) {
12167
12586
  const pass = this.openPass();
12168
12587
  if (pass === null)
@@ -12170,7 +12589,7 @@ export class WebGPURenderer {
12170
12589
  definition.draw(this.fillPassContext(pass));
12171
12590
  return;
12172
12591
  }
12173
- recordNode(this.arena, VERB_PASS, this.passReads.get(handle) ?? 0, this.currentTarget(), handle, 0);
12592
+ recordNode(this.arena, VERB_PASS, reads, this.currentTarget(), handle, 0);
12174
12593
  if (this.framePresented)
12175
12594
  this.queueOverlayFlush();
12176
12595
  }
@@ -12279,8 +12698,8 @@ export class WebGPURenderer {
12279
12698
  bindClusters(camera, env, f, i, at) {
12280
12699
  const binner = this.clusterBinner;
12281
12700
  i[at('uClustered')] = binner === null ? 0 : 1;
12282
- if (binner === null)
12283
- return;
12701
+ /* Written whether or not the lights are clustered: a surface overlay's rim finds its place on
12702
+ the screen through the same two (\`overlayScreen\`). */
12284
12703
  const p = camera.projection;
12285
12704
  const tanHalfFovY = 1 / (p[5] ?? 1);
12286
12705
  const aspect = (p[5] ?? 1) / (p[0] ?? 1);
@@ -12296,6 +12715,8 @@ export class WebGPURenderer {
12296
12715
  f[frustumAt + 2] = tanHalfFovY;
12297
12716
  f[frustumAt + 3] = aspect;
12298
12717
  f.set(camera.view, at('uView'));
12718
+ if (binner === null)
12719
+ return;
12299
12720
  /* The rectangles past the fixed arm's go into the table. See `ClusterLightSet.areas`. */
12300
12721
  const set = fillClusterLightSet(env, this.clusterLights, this.shadedAreaLights);
12301
12722
  /* A slot is read only under `POINT_SHADOWS`; see `lightHasFixture`. */
@@ -12596,6 +13017,43 @@ export class WebGPURenderer {
12596
13017
  }
12597
13018
  /** This frame's exposure into the tone curve. Starts at the profile's own. */
12598
13019
  exposure;
13020
+ /**
13021
+ * The `filmic` curve every later frame is graded through, until it is set again. See
13022
+ * `RendererApi.setFilmicCurve`; the constants are solved here once, not per frame or per pixel.
13023
+ */
13024
+ setFilmicCurve(curve) {
13025
+ this.filmCurve = curve;
13026
+ if (resolveFilmicCurve(curve, this.filmConstants, this.headroom) && !this.warnedFilmClamped) {
13027
+ this.warnedFilmClamped = true;
13028
+ console.warn(FILMIC_CLAMPED);
13029
+ }
13030
+ }
13031
+ warnedFilmClamped = false;
13032
+ /** The curve last set, so a new headroom can solve it again. */
13033
+ filmCurve = DEFAULT_FILMIC_CURVE;
13034
+ /** The display's peak over paper white; 1 on a standard range. See `setDisplayLuminance`. */
13035
+ headroom = 1;
13036
+ /** The range the frame goes out in. See `RendererApi.displayRange`. */
13037
+ get displayRange() {
13038
+ return this.surface.displayRange;
13039
+ }
13040
+ /** Why it is that range, in words. */
13041
+ get displayRangeReason() {
13042
+ return this.surface.displayRangeReason;
13043
+ }
13044
+ /**
13045
+ * How bright paper white and the display's peak are, in any one unit (nits, usually), held until
13046
+ * changed: their ratio is how far above white the highlights may run on a high range display,
13047
+ * and nothing changes on a standard one. See `RendererApi.setDisplayLuminance`.
13048
+ */
13049
+ setDisplayLuminance(paperWhite, peak) {
13050
+ this.headroom = displayHeadroom(this.surface.displayRange, paperWhite, peak);
13051
+ resolveFilmicCurve(this.filmCurve, this.filmConstants, this.headroom);
13052
+ }
13053
+ /** The curve's eight constants, and the two views the composite uploads them through. */
13054
+ filmConstants = new Float32Array(FILMIC_CONSTANTS);
13055
+ filmA = this.filmConstants.subarray(0, 4);
13056
+ filmB = this.filmConstants.subarray(4, 8);
12599
13057
  /*
12600
13058
  * -----------------------------------------------------------------------------------------
12601
13059
  * The three screen effects, which this backend cannot yet honour — and says so.
@@ -12639,6 +13097,8 @@ export class WebGPURenderer {
12639
13097
  veilColor = [0, 0, 0];
12640
13098
  veilAlpha = 0;
12641
13099
  vignette = 0;
13100
+ /** The lens's colour fringe: red's and green's pull and where it begins. See `fringe.ts`. */
13101
+ fringe = new Float32Array(FRINGE_FLOATS);
12642
13102
  grain = 0;
12643
13103
  grainSeed = 0;
12644
13104
  warnedLookWithoutComposite = false;
@@ -12660,6 +13120,20 @@ export class WebGPURenderer {
12660
13120
  }
12661
13121
  this.vignette = clampVignette(strength);
12662
13122
  }
13123
+ /**
13124
+ * The lens's colour fringe, held until changed: `intensity` is a percentage, 0 is none, and red
13125
+ * and green are pulled toward the centre by their wavelength's distance from blue, growing from
13126
+ * `start` — a share of the half-frame, 0 the centre — to the whole of it at the edge. Applied to
13127
+ * scene light at the composite's first read. Needs `screenEffects`, and says so once rather than
13128
+ * doing nothing. See `shaders/fringe.ts`.
13129
+ */
13130
+ setChromaticAberration(intensity, start = 0) {
13131
+ if (!this.hasComposite) {
13132
+ this.warnLookWithoutComposite(intensity);
13133
+ return;
13134
+ }
13135
+ resolveFringe(intensity, start, this.fringe);
13136
+ }
12663
13137
  /**
12664
13138
  * Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
12665
13139
  * grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
@@ -12793,9 +13267,26 @@ export class WebGPURenderer {
12793
13267
  setSpeedRush(strength) {
12794
13268
  this.rushStrength = Math.min(Math.max(strength, 0), 1);
12795
13269
  }
12796
- /** How much of the frame's camera motion blur to apply, 0 to 1. */
12797
- setCameraMotionBlur(scale) {
13270
+ /**
13271
+ * How much of the frame's motion blur to apply, 0 to 1, and the longest smear as a share of the
13272
+ * frame, both held until changed. See `RendererApi.setCameraMotionBlur`.
13273
+ */
13274
+ setCameraMotionBlur(scale, maxShare = MOTION_BLUR_MAX_UV) {
12798
13275
  this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
13276
+ this.motionBlurMax = clampMotionShare(maxShare);
13277
+ }
13278
+ /** The longest smear, as a share of the frame. See `setCameraMotionBlur`. */
13279
+ motionBlurMax = MOTION_BLUR_MAX_UV;
13280
+ warnedObjectBlur = false;
13281
+ /**
13282
+ * Said once: a draw named where it was last frame while the frame blurs, and this frame keeps no
13283
+ * motion target to blur it by — the camera's motion alone smears it. See `OBJECT_BLUR_UNKEPT`.
13284
+ */
13285
+ warnObjectBlur() {
13286
+ if (this.warnedObjectBlur)
13287
+ return;
13288
+ this.warnedObjectBlur = true;
13289
+ console.warn(OBJECT_BLUR_UNKEPT);
12799
13290
  }
12800
13291
  /**
12801
13292
  * The camera has cut: this frame is a new shot, not the next moment of the last one.
@@ -13342,18 +13833,19 @@ export class WebGPURenderer {
13342
13833
  * `bindMeshPass` and `drawMesh`, which record into whatever `this.pass` is. That is the same
13343
13834
  * arrangement `bakeFace` uses for a point light's cube, and it is why a bake must not be
13344
13835
  * opened inside a frame's pass by a caller who then expects the frame to continue.
13345
- */
13346
- const savedPass = this.pass;
13347
- /*
13836
+ *
13348
13837
  * **A bake is a boundary, and boundaries flush.** The mirror says why in its own words: what
13349
13838
  * has been recorded and not yet replayed belongs to the pass that is about to end, and leaving
13350
13839
  * it replays it into whichever pass opens next — a draw in the wrong target rather than a
13351
13840
  * missing one. This is the fourth such boundary in the backend and the only one that was never
13352
13841
  * given the treatment, which is what let a bake carry the frame's pending draws into a cube
13353
- * face.
13842
+ * face. **The pass is saved after the flush**, which opens it where none was: saved before,
13843
+ * the null given back left the flushed pass open and the frame's encoder locked. See
13844
+ * `captureScene`.
13354
13845
  */
13355
13846
  if (this.quality.frameGraph)
13356
13847
  this.flushGraph();
13848
+ const savedPass = this.pass;
13357
13849
  const savedEncoder = this.encoder;
13358
13850
  /*
13359
13851
  * **The rings are marked here and given back after the submit below**, because this bake is its
@@ -13938,6 +14430,7 @@ export class WebGPURenderer {
13938
14430
  this.sceneColor,
13939
14431
  this.sceneColorMsaa,
13940
14432
  this.refractSnapshot,
14433
+ this.passColour,
13941
14434
  this.resolvedDepth,
13942
14435
  this.aoTarget,
13943
14436
  this.aoScratch,
@@ -14003,6 +14496,10 @@ export class WebGPURenderer {
14003
14496
  usage: 0x4 | 0x2 | 0x10, // TEXTURE_BINDING | COPY_DST | RENDER_ATTACHMENT
14004
14497
  });
14005
14498
  this.refractSnapshotView = this.refractSnapshot.createView();
14499
+ this.passColour = null;
14500
+ this.passColourView = null;
14501
+ if ((this.passReadsUnion & COLOR_SNAPSHOT) !== 0)
14502
+ this.allocatePassColour();
14006
14503
  this.refractMips = new SnapshotMips(device, this.pipelines, this.refractSnapshot, reconstructed ? RECON_HISTORY_FORMAT : format);
14007
14504
  /*
14008
14505
  * **Every cached flat bind group is stale the moment this view changes**, and the failure is
@@ -14218,6 +14715,10 @@ export class WebGPURenderer {
14218
14715
  usage: USAGE,
14219
14716
  });
14220
14717
  this.ssrReflectionView = this.ssrReflection.createView();
14718
+ /* The frame's materials, at the scene's size; a stand-in where nothing traces them. */
14719
+ this.reflectionSurface?.size(width, height);
14720
+ const probeMap = this.reflectionSurface?.probeView ?? this.blankView;
14721
+ const tintMap = this.reflectionSurface?.tintView ?? this.blankView;
14221
14722
  this.ssrBindGroup =
14222
14723
  this.resolvedDepthView === null || this.sceneColorView === null
14223
14724
  ? null
@@ -14231,8 +14732,23 @@ export class WebGPURenderer {
14231
14732
  },
14232
14733
  ...entry(SSR_TEXTURES.uSsrDepth, this.resolvedDepthView, true),
14233
14734
  ...entry(SSR_TEXTURES.uSsrScene, this.sceneColorView),
14735
+ ...entry(SSR_TEXTURES.uSsrProbeMap, probeMap),
14736
+ ...entry(SSR_TEXTURES.uSsrTintMap, tintMap),
14234
14737
  ],
14235
14738
  });
14739
+ this.ssrMaterialGroup = device.createBindGroup({
14740
+ label: 'post.ssrMaterialGroup',
14741
+ layout: this.ssrMaterialLayout,
14742
+ entries: [
14743
+ {
14744
+ binding: SSR_MATERIAL_UNIFORMS,
14745
+ resource: { buffer: this.ssrMaterialUniforms, size: SSR_MATERIAL_SIZE },
14746
+ },
14747
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrReflection, this.ssrReflectionView),
14748
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrProbeMap, probeMap),
14749
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrTintMap, tintMap),
14750
+ ],
14751
+ });
14236
14752
  this.ssrResolveGroup = device.createBindGroup({
14237
14753
  label: 'post.ssrResolveGroup',
14238
14754
  layout: this.ssrResolveLayout,