@driftengine/core 4.4.2 → 4.5.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 (193) hide show
  1. package/CHANGELOG.json +46 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +5 -1
  4. package/dist/index.js +7 -0
  5. package/dist/render/backend/api.d.ts +17 -11
  6. package/dist/render/backend/webgl2/glassTintPrograms.d.ts +13 -0
  7. package/dist/render/backend/webgl2/glassTintPrograms.js +38 -0
  8. package/dist/render/backend/webgl2/renderer.d.ts +79 -8
  9. package/dist/render/backend/webgl2/renderer.js +360 -105
  10. package/dist/render/backend/webgpu/buffers.d.ts +19 -1
  11. package/dist/render/backend/webgpu/buffers.js +27 -1
  12. package/dist/render/backend/webgpu/depthPass.d.ts +1 -1
  13. package/dist/render/backend/webgpu/depthPass.js +2 -1
  14. package/dist/render/backend/webgpu/flatPass.d.ts +2 -0
  15. package/dist/render/backend/webgpu/flatPass.js +20 -1
  16. package/dist/render/backend/webgpu/glassTintPass.d.ts +28 -0
  17. package/dist/render/backend/webgpu/glassTintPass.js +205 -0
  18. package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +60 -1
  19. package/dist/render/backend/webgpu/gpuDrivenPass.js +305 -5
  20. package/dist/render/backend/webgpu/instanced.d.ts +12 -1
  21. package/dist/render/backend/webgpu/instanced.js +20 -1
  22. package/dist/render/backend/webgpu/latePass.d.ts +26 -0
  23. package/dist/render/backend/webgpu/latePass.js +133 -0
  24. package/dist/render/backend/webgpu/lightVolumePass.js +5 -1
  25. package/dist/render/backend/webgpu/motionPass.d.ts +95 -0
  26. package/dist/render/backend/webgpu/motionPass.js +491 -0
  27. package/dist/render/backend/webgpu/pipelineCache.d.ts +10 -1
  28. package/dist/render/backend/webgpu/pipelineCache.js +11 -1
  29. package/dist/render/backend/webgpu/pointGlassTintPass.d.ts +32 -0
  30. package/dist/render/backend/webgpu/pointGlassTintPass.js +193 -0
  31. package/dist/render/backend/webgpu/pointShadowPass.d.ts +35 -6
  32. package/dist/render/backend/webgpu/pointShadowPass.js +105 -20
  33. package/dist/render/backend/webgpu/postPass.d.ts +2 -12
  34. package/dist/render/backend/webgpu/renderer.d.ts +171 -54
  35. package/dist/render/backend/webgpu/renderer.js +944 -390
  36. package/dist/render/backend/webgpu/scatterPass.js +2 -1
  37. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  38. package/dist/render/backend/webgpu/skinPaletteRing.js +4 -0
  39. package/dist/render/backend/webgpu/snapshotMips.d.ts +29 -0
  40. package/dist/render/backend/webgpu/snapshotMips.js +52 -0
  41. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +5 -0
  42. package/dist/render/backend/webgpu/surfaceTexturePass.js +1 -1
  43. package/dist/render/glass.d.ts +45 -0
  44. package/dist/render/glass.js +39 -0
  45. package/dist/render/glassCasters.d.ts +40 -0
  46. package/dist/render/glassCasters.js +84 -0
  47. package/dist/render/glassShadow.d.ts +78 -0
  48. package/dist/render/glassShadow.js +125 -0
  49. package/dist/render/globalMediumPass.d.ts +2 -3
  50. package/dist/render/globalMediumPass.js +2 -8
  51. package/dist/render/gpudriven/lit.d.ts +12 -0
  52. package/dist/render/gpudriven/lit.js +23 -0
  53. package/dist/render/gpudriven/materialTable.d.ts +15 -5
  54. package/dist/render/gpudriven/materialTable.js +26 -6
  55. package/dist/render/gpudriven/pipeline.d.ts +1 -1
  56. package/dist/render/gpudriven/pipeline.js +4 -3
  57. package/dist/render/gpudriven/sunGlass.d.ts +35 -0
  58. package/dist/render/gpudriven/sunGlass.js +89 -0
  59. package/dist/render/lightBudget.d.ts +17 -5
  60. package/dist/render/lightBudget.js +17 -5
  61. package/dist/render/pointGlassTint.d.ts +59 -0
  62. package/dist/render/pointGlassTint.js +198 -0
  63. package/dist/render/pointShadowArray.d.ts +56 -3
  64. package/dist/render/pointShadowArray.js +127 -16
  65. package/dist/render/pointShadowMap.d.ts +23 -1
  66. package/dist/render/pointShadowMap.js +27 -1
  67. package/dist/render/recon/changeFrames.d.ts +15 -0
  68. package/dist/render/recon/changeFrames.js +21 -0
  69. package/dist/render/recon/lateRouting.d.ts +14 -0
  70. package/dist/render/recon/lateRouting.js +18 -0
  71. package/dist/render/recon/mover.d.ts +57 -0
  72. package/dist/render/recon/mover.js +86 -0
  73. package/dist/render/renderQuality.d.ts +35 -0
  74. package/dist/render/renderQuality.js +7 -0
  75. package/dist/render/rendererHarness.js +12 -0
  76. package/dist/render/sceneTarget.d.ts +7 -1
  77. package/dist/render/sceneTarget.js +58 -12
  78. package/dist/render/shaders/depth.d.ts +16 -1
  79. package/dist/render/shaders/depth.js +85 -9
  80. package/dist/render/shaders/flat/directionalShadow.js +91 -6
  81. package/dist/render/shaders/flat/glassTint.d.ts +6 -0
  82. package/dist/render/shaders/flat/glassTint.js +25 -0
  83. package/dist/render/shaders/flat/index.d.ts +13 -0
  84. package/dist/render/shaders/flat/index.js +4 -0
  85. package/dist/render/shaders/flat/lampGlass.d.ts +1 -0
  86. package/dist/render/shaders/flat/lampGlass.js +183 -0
  87. package/dist/render/shaders/flat/main.d.ts +1 -1
  88. package/dist/render/shaders/flat/main.js +115 -15
  89. package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
  90. package/dist/render/shaders/flat/pointShadow.js +2 -0
  91. package/dist/render/shaders/flat/preamble.js +21 -6
  92. package/dist/render/shaders/generated/depth.wgsl.d.ts +227 -4
  93. package/dist/render/shaders/generated/depth.wgsl.js +227 -4
  94. package/dist/render/shaders/generated/flat.wgsl.d.ts +360 -312
  95. package/dist/render/shaders/generated/flat.wgsl.js +376 -328
  96. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +3 -13
  97. package/dist/render/shaders/generated/globalMedium.wgsl.js +3 -13
  98. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +3 -13
  99. package/dist/render/shaders/generated/lightVolume.wgsl.js +4 -14
  100. package/dist/render/shaders/generated/octahedralResolve.wgsl.d.ts +29 -0
  101. package/dist/render/shaders/generated/octahedralResolve.wgsl.js +29 -0
  102. package/dist/render/shaders/globalMedium.js +6 -6
  103. package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +7 -0
  104. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +86 -1
  105. package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +1 -1
  106. package/dist/render/shaders/gpudriven/cull.wgsl.js +11 -2
  107. package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +1 -1
  108. package/dist/render/shaders/gpudriven/materialTable.wgsl.js +3 -1
  109. package/dist/render/shaders/gpudriven/shade.wgsl.js +50 -1
  110. package/dist/render/shaders/gpudriven/sunGlass.wgsl.d.ts +9 -0
  111. package/dist/render/shaders/gpudriven/sunGlass.wgsl.js +133 -0
  112. package/dist/render/shaders/lightVolume.js +6 -6
  113. package/dist/render/shaders/octahedralResolve.d.ts +8 -0
  114. package/dist/render/shaders/octahedralResolve.js +48 -17
  115. package/dist/render/shaders/recon/lateDepth.wgsl.d.ts +1 -0
  116. package/dist/render/shaders/recon/lateDepth.wgsl.js +44 -0
  117. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  118. package/dist/render/shaders/recon/motion.wgsl.js +84 -0
  119. package/dist/render/shadowCasters.d.ts +20 -1
  120. package/dist/render/shadowCasters.js +15 -1
  121. package/dist/render/shadowMap.d.ts +54 -5
  122. package/dist/render/shadowMap.js +190 -36
  123. package/dist/render/sunGlassTint.d.ts +25 -0
  124. package/dist/render/sunGlassTint.js +79 -0
  125. package/dist/render/translucentQueue.d.ts +6 -0
  126. package/dist/render/translucentQueue.js +15 -1
  127. package/package.json +6 -6
  128. package/src/index.ts +10 -0
  129. package/src/render/backend/api.ts +15 -9
  130. package/src/render/backend/webgl2/glassTintPrograms.ts +58 -0
  131. package/src/render/backend/webgl2/renderer.ts +401 -108
  132. package/src/render/backend/webgpu/buffers.ts +50 -2
  133. package/src/render/backend/webgpu/depthPass.ts +2 -1
  134. package/src/render/backend/webgpu/flatPass.ts +22 -1
  135. package/src/render/backend/webgpu/glassTintPass.ts +244 -0
  136. package/src/render/backend/webgpu/gpuDrivenPass.ts +349 -5
  137. package/src/render/backend/webgpu/instanced.ts +21 -1
  138. package/src/render/backend/webgpu/latePass.ts +152 -0
  139. package/src/render/backend/webgpu/lightVolumePass.ts +5 -1
  140. package/src/render/backend/webgpu/motionPass.ts +535 -0
  141. package/src/render/backend/webgpu/pipelineCache.ts +12 -1
  142. package/src/render/backend/webgpu/pointGlassTintPass.ts +233 -0
  143. package/src/render/backend/webgpu/pointShadowPass.ts +158 -26
  144. package/src/render/backend/webgpu/renderer.ts +1108 -421
  145. package/src/render/backend/webgpu/scatterPass.ts +2 -1
  146. package/src/render/backend/webgpu/skinPaletteRing.ts +4 -0
  147. package/src/render/backend/webgpu/snapshotMips.ts +80 -0
  148. package/src/render/backend/webgpu/surfaceTexturePass.ts +1 -1
  149. package/src/render/glass.ts +74 -0
  150. package/src/render/glassCasters.ts +164 -0
  151. package/src/render/glassShadow.ts +153 -0
  152. package/src/render/globalMediumPass.ts +4 -11
  153. package/src/render/gpudriven/lit.ts +31 -0
  154. package/src/render/gpudriven/materialTable.ts +28 -6
  155. package/src/render/gpudriven/pipeline.ts +4 -3
  156. package/src/render/gpudriven/sunGlass.ts +117 -0
  157. package/src/render/lightBudget.ts +19 -5
  158. package/src/render/pointGlassTint.ts +250 -0
  159. package/src/render/pointShadowArray.ts +188 -22
  160. package/src/render/pointShadowMap.ts +41 -0
  161. package/src/render/recon/changeFrames.ts +24 -0
  162. package/src/render/recon/lateRouting.ts +29 -0
  163. package/src/render/recon/mover.ts +118 -0
  164. package/src/render/renderQuality.ts +47 -0
  165. package/src/render/rendererHarness.ts +12 -0
  166. package/src/render/sceneTarget.ts +58 -11
  167. package/src/render/shaders/depth.ts +109 -9
  168. package/src/render/shaders/flat/directionalShadow.ts +97 -6
  169. package/src/render/shaders/flat/glassTint.ts +25 -0
  170. package/src/render/shaders/flat/index.ts +17 -0
  171. package/src/render/shaders/flat/lampGlass.ts +185 -0
  172. package/src/render/shaders/flat/main.ts +115 -15
  173. package/src/render/shaders/flat/pointShadow.ts +2 -0
  174. package/src/render/shaders/flat/preamble.ts +21 -6
  175. package/src/render/shaders/generated/depth.wgsl.ts +234 -4
  176. package/src/render/shaders/generated/flat.wgsl.ts +376 -328
  177. package/src/render/shaders/generated/globalMedium.wgsl.ts +3 -13
  178. package/src/render/shaders/generated/lightVolume.wgsl.ts +4 -14
  179. package/src/render/shaders/generated/octahedralResolve.wgsl.ts +30 -0
  180. package/src/render/shaders/globalMedium.ts +6 -6
  181. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +88 -1
  182. package/src/render/shaders/gpudriven/cull.wgsl.ts +11 -2
  183. package/src/render/shaders/gpudriven/materialTable.wgsl.ts +3 -1
  184. package/src/render/shaders/gpudriven/shade.wgsl.ts +50 -1
  185. package/src/render/shaders/gpudriven/sunGlass.wgsl.ts +139 -0
  186. package/src/render/shaders/lightVolume.ts +6 -6
  187. package/src/render/shaders/octahedralResolve.ts +50 -17
  188. package/src/render/shaders/recon/lateDepth.wgsl.ts +47 -0
  189. package/src/render/shaders/recon/motion.wgsl.ts +84 -0
  190. package/src/render/shadowCasters.ts +28 -1
  191. package/src/render/shadowMap.ts +231 -37
  192. package/src/render/sunGlassTint.ts +86 -0
  193. package/src/render/translucentQueue.ts +20 -1
@@ -9,12 +9,7 @@ import {
9
9
  } from '../../temporalAa.ts';
10
10
  import { OIT_MULTISAMPLE_REFUSAL } from '../../orderIndependent.ts';
11
11
  import { TranslucentQueue } from '../../translucentQueue.ts';
12
- import {
13
- DEPTH_CLEAR,
14
- DEPTH_COMPARE_EQUAL,
15
- MAX_DEPTH_LAYER,
16
- REVERSED_DEPTH,
17
- } from '../../depthConvention.ts';
12
+ import { DEPTH_CLEAR, MAX_DEPTH_LAYER, REVERSED_DEPTH } from '../../depthConvention.ts';
18
13
  import { RECONSTRUCTION_MULTISAMPLE_REFUSAL, reconRenderSize } from '../../recon/frameSizes.ts';
19
14
  import { jitterOffset as jitterOffsetFor, reconJitterPhases } from '../../recon/jitter.ts';
20
15
  import {
@@ -23,16 +18,25 @@ import {
23
18
  RECON_WORKGROUP,
24
19
  reconResolveWgsl,
25
20
  } from '../../shaders/recon/resolve.wgsl.ts';
26
- import {
27
- MOTION_DRAW_FLOATS,
28
- MOTION_DRAW_STRIDE,
29
- MOTION_FRAME_FLOATS,
30
- MOTION_TARGET_FORMAT,
31
- RECON_MOTION_WGSL,
32
- } from '../../shaders/recon/motion.wgsl.ts';
21
+ import { MotionPass } from './motionPass.ts';
22
+ import { LatePass } from './latePass.ts';
23
+ import { SnapshotMips } from './snapshotMips.ts';
24
+ import { resolveGlass } from '../../glass.ts';
25
+ import type { GlassOptions, ResolvedGlass } from '../../glass.ts';
26
+ import { routesLate, tracksMotion } from '../../recon/lateRouting.ts';
27
+ import { frameOfChange, keepsPrevious, pairedInstances } from '../../recon/changeFrames.ts';
28
+ import { INSTANCE_STRIDE } from '../../instances.ts';
33
29
  import { DEFAULT_DISOCCLUSION } from '../../recon/disocclusion.ts';
34
30
  import { DEFAULT_RECON_QUALITY } from '../../recon/resolve.ts';
35
31
  import type { ReadonlyMat4 } from 'gl-matrix';
32
+ import {
33
+ MOVER_MOTION,
34
+ MOVER_TWICE,
35
+ advanceMover,
36
+ claimRendererId,
37
+ isMover,
38
+ } from '../../recon/mover.ts';
39
+ import type { Mover } from '../../recon/mover.ts';
36
40
 
37
41
  import type { Vec3 } from '../../../math/color.ts';
38
42
  import { clamp } from '../../../math/scalar.ts';
@@ -128,6 +132,7 @@ import {
128
132
  type ShadowLight,
129
133
  } from '../../pointShadowSystem.ts';
130
134
  import { GpuPointShadowArray, GpuPointShadowMap } from './pointShadowPass.ts';
135
+ import { OCTAHEDRAL_EDGE, strideLayers } from '../../pointShadowArray.ts';
131
136
  import {
132
137
  createPanelBindGroup,
133
138
  createPanelBindGroupLayout,
@@ -326,6 +331,16 @@ import {
326
331
  } from '../../globalMedium.ts';
327
332
  import type { GlobalMediumOptions } from '../../globalMedium.ts';
328
333
  import type { MeshData } from '../../mesh.ts';
334
+ import { castsDepth, glassOf } from '../../shadowCasters.ts';
335
+ import { GlassCasterList, type GlassReplaySink } from '../../glassCasters.ts';
336
+ import { GpuGlassTint } from './glassTintPass.ts';
337
+ import {
338
+ SUN_DYNAMIC_LAYER,
339
+ SUN_GLASS_MOVING_LAYER,
340
+ SUN_GLASS_STATIC_LAYER,
341
+ SUN_PEELED_LAYER,
342
+ SUN_STATIC_LAYER,
343
+ } from '../../shadowMap.ts';
329
344
  import type { SceneCasterMaterial, ShadowCasterSink, ShadowCasters } from '../../shadowCasters.ts';
330
345
  import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental, type GpuMesh } from './buffers.ts';
331
346
  import type { GpuSurface } from './device.ts';
@@ -1308,6 +1323,49 @@ function padded(direction: ArrayLike<number>): Float32Array {
1308
1323
  return SCRATCH_VEC4;
1309
1324
  }
1310
1325
 
1326
+ /** The sun texture's usage: attached, sampled, and copied both ways when glass grows it. */
1327
+ const SUN_USAGE = 0x10 | 0x4 | 0x1 | 0x2; // RENDER_ATTACHMENT | TEXTURE_BINDING | COPY_SRC | COPY_DST
1328
+
1329
+ /** Every view of the sun's texture a pass reads or attaches, for `maps` of its layers. */
1330
+ function sunViewsOf(
1331
+ texture: GPUTexture,
1332
+ maps: number,
1333
+ layers: number,
1334
+ ): {
1335
+ array: GPUTextureView;
1336
+ staticArray: GPUTextureView;
1337
+ static: GPUTextureView;
1338
+ dynamic: GPUTextureView | null;
1339
+ peel: GPUTextureView | null;
1340
+ glass: readonly [GPUTextureView, GPUTextureView] | null;
1341
+ } {
1342
+ const layer = (index: number): GPUTextureView =>
1343
+ texture.createView({ dimension: '2d', baseArrayLayer: index, arrayLayerCount: 1 });
1344
+ return {
1345
+ array: texture.createView({ dimension: '2d-array' }),
1346
+ staticArray: texture.createView({
1347
+ dimension: '2d-array',
1348
+ baseArrayLayer: SUN_STATIC_LAYER,
1349
+ arrayLayerCount: 1,
1350
+ }),
1351
+ static: layer(SUN_STATIC_LAYER),
1352
+ dynamic: maps > SUN_DYNAMIC_LAYER ? layer(SUN_DYNAMIC_LAYER) : null,
1353
+ peel: maps > SUN_PEELED_LAYER ? layer(SUN_PEELED_LAYER) : null,
1354
+ glass: layers > maps ? [layer(SUN_GLASS_STATIC_LAYER), layer(SUN_GLASS_MOVING_LAYER)] : null,
1355
+ };
1356
+ }
1357
+
1358
+ /**
1359
+ * What the sun keeps through glass: two `rgba8unorm` layers, attached one at a time at level 0,
1360
+ * sampled whole with their mip chains, which are rebuilt after each layer is drawn.
1361
+ */
1362
+ interface SunTintTarget {
1363
+ readonly texture: GPUTexture;
1364
+ readonly layers: readonly [GPUTextureView, GPUTextureView];
1365
+ readonly array: GPUTextureView;
1366
+ readonly mips: readonly [SnapshotMips, SnapshotMips];
1367
+ }
1368
+
1311
1369
  export class WebGPURenderer implements RendererApi {
1312
1370
  /**
1313
1371
  * Real timestamp queries, where the adapter offers them.
@@ -1417,6 +1475,8 @@ export class WebGPURenderer implements RendererApi {
1417
1475
  * caller who passed null gets — rather than reading a slot this frame has already re-let.
1418
1476
  */
1419
1477
  private skinPaletteSlot = -1;
1478
+ /** The palette `skinPaletteSlot` was uploaded from, for a skinned mover to copy. See `Mover`. */
1479
+ private skinPaletteData: Float32Array | null = null;
1420
1480
 
1421
1481
  private readonly surface: GpuSurface;
1422
1482
  /** The encoder for the frame in flight, or null between frames. */
@@ -1426,6 +1486,27 @@ export class WebGPURenderer implements RendererApi {
1426
1486
  private pass: GPURenderPassEncoder | null = null;
1427
1487
 
1428
1488
  private readonly pipelines: PipelineCache;
1489
+ /**
1490
+ * The pipelines a reconstructing frame's blended draws are built for: the reconstructed picture's
1491
+ * format at one sample, which is where `LatePass` draws them. Aliased to `pipelines` when the
1492
+ * world already lands on exactly that, as `overlayPipelines` is.
1493
+ */
1494
+ private readonly latePipelines: PipelineCache;
1495
+ /** The blended draws of a reconstructing frame, drawn after the upscale. See `LatePass`. */
1496
+ private latePass: LatePass | null = null;
1497
+ /** A late draw asked to refract: `runLate` copies the reconstructed picture before replaying. */
1498
+ private lateRefractWanted = false;
1499
+ /** The refraction copy's mip chain, recorded after each copy: what frosted glass reads. */
1500
+ private refractMips: SnapshotMips | null = null;
1501
+ /**
1502
+ * The refraction copy's sampler: filtering, and **between levels as well**, so a frosted pane's
1503
+ * level falls smoothly between two blurs rather than stepping from one to the next.
1504
+ */
1505
+ private refractSampler: GPUSampler | null = null;
1506
+ /** One glass resolution reused by every draw, so a pane allocates nothing. See `glass.ts`. */
1507
+ private readonly glassScratch: ResolvedGlass = { transmission: 0, frost: 0, tint: [1, 1, 1] };
1508
+ /** `issueCommand` bound once, so the late replay allocates no closure per frame. */
1509
+ private readonly issueBound: (pass: GPURenderPassEncoder, command: DrawCommand) => void;
1429
1510
  /**
1430
1511
  * The same pipelines again, built for the canvas, for anything drawn after `endFrame`.
1431
1512
  *
@@ -1971,10 +2052,10 @@ export class WebGPURenderer implements RendererApi {
1971
2052
  * the *unsharpened* result — sharpening into the history would sharpen a sharpened picture every
1972
2053
  * frame, which is the halo the reference's own clamp exists to prevent, compounded.
1973
2054
  *
1974
- * The motion target is render size and carries a drawn object's own motion where something wrote
1975
- * one. **Nothing writes it yet**, so it is cleared every frame and every pixel takes the camera's
1976
- * motion, which the resolve derives from the depth and the reprojection. Static geometry is
1977
- * exact that way and a moving object ghosts until the motion pass lands.
2055
+ * The motion target is render size and carries a drawn object's own motion where the motion pass
2056
+ * wrote one — a draw that named its mover, or passed last frame's matrix. Every other texel is
2057
+ * cleared, and the resolve takes the camera's motion there, derived from the depth and the
2058
+ * reprojection: exact for anything that did not move. See `MotionPass`.
1978
2059
  */
1979
2060
  private reconHistories: (GPUTexture | null)[] = [null, null];
1980
2061
  private reconHistoryViews: (GPUTextureView | null)[] = [null, null];
@@ -1996,34 +2077,15 @@ export class WebGPURenderer implements RendererApi {
1996
2077
  private reconGroups: (GPUBindGroup | null)[] = [null, null];
1997
2078
  private reconSharpenGroups: (GPUBindGroup | null)[] = [null, null];
1998
2079
  private reconRushGroup: GPUBindGroup | null = null;
1999
- /**
2000
- * What the motion pass will draw, recorded by `drawMesh` and spent once a frame.
2001
- *
2002
- * A parallel array of meshes and one flat matrix buffer rather than an array of objects, because
2003
- * this is written on the frame's hot path and the house rule about allocating there binds it: the
2004
- * matrices are copied into a buffer that only ever grows, and the count is what resets.
2005
- */
2006
- private readonly motionMeshes: GpuMesh[] = [];
2007
- private motionMatrices = new Float32Array(0);
2008
- private motionCount = 0;
2009
- private motionLayout: GPUBindGroupLayout | null = null;
2010
- /**
2011
- * One pipeline per vertex stride, because a stride is a property of the *mesh*.
2012
- *
2013
- * Which optional attributes a mesh supplied decides how far apart its positions are, and this
2014
- * pass reads nothing but positions — so two meshes drawn by the same shader still need two
2015
- * vertex layouts. A map rather than a cache key on the pipeline cache: there is one shader here
2016
- * and a handful of strides in any scene.
2017
- */
2018
- private readonly motionPipelines = new Map<number, GPURenderPipeline>();
2019
- private motionModule: GPUShaderModule | null = null;
2020
- private motionPipelineLayout: GPUPipelineLayout | null = null;
2021
- private motionFrameBuffer: GPUBuffer | null = null;
2022
- private motionDrawBuffer: GPUBuffer | null = null;
2023
- private motionDrawCapacity = 0;
2024
- private motionGroup: GPUBindGroup | null = null;
2025
- private readonly motionFrameStaging = new Float32Array(MOTION_FRAME_FLOATS);
2026
- private motionDrawStaging = new Uint8Array(0);
2080
+ /** The motion pass, made with the reconstruction's targets. See `MotionPass`. */
2081
+ private motionPass: MotionPass | null = null;
2082
+ /** This renderer's identity to a `Mover`. See `claimRendererId`. */
2083
+ private readonly moverRenderer = claimRendererId();
2084
+ /** Frames begun, so a mover can tell last frame from any other. Advanced in `beginFrame`. */
2085
+ private moverFrame = 0;
2086
+ /** Between `beginFrame` and `endFrame`. A change outside it is the next frame's. */
2087
+ private inFrame = false;
2088
+ private warnedMoverTwice = false;
2027
2089
  /** The scene's own clip transform with the generated shaders' y negation folded in. */
2028
2090
  private readonly motionRaster = mat4.create();
2029
2091
  private reconSizeKey = '';
@@ -2276,6 +2338,7 @@ export class WebGPURenderer implements RendererApi {
2276
2338
  refraction: number;
2277
2339
  refractTint: Vec3 | undefined;
2278
2340
  thicknessM: number;
2341
+ glass: GlassOptions | undefined;
2279
2342
  } = {
2280
2343
  lit: true,
2281
2344
  fog: true,
@@ -2286,6 +2349,7 @@ export class WebGPURenderer implements RendererApi {
2286
2349
  refraction: 0,
2287
2350
  refractTint: undefined,
2288
2351
  thicknessM: 0,
2352
+ glass: undefined,
2289
2353
  };
2290
2354
 
2291
2355
  private reflectionPassActive = false;
@@ -2591,7 +2655,7 @@ export class WebGPURenderer implements RendererApi {
2591
2655
  this.filmFrags.writeBlock(fragmentSlot, this.filmFragInts);
2592
2656
 
2593
2657
  const filmPipe = filmPipeline(
2594
- this.pipelines,
2658
+ this.blendedPipelines(),
2595
2659
  this.surface.device,
2596
2660
  this.filmLayout,
2597
2661
  `film|${geometry.key ?? ''}`,
@@ -2605,10 +2669,7 @@ export class WebGPURenderer implements RendererApi {
2605
2669
  * holds the mirror's colour whatever `uReflectionEnabled` says, and WebGPU validates the
2606
2670
  * binding rather than the read — the argument `filmBindGroupBlank` exists for.
2607
2671
  */
2608
- const filmCommand = this.recordDraw(
2609
- filmGroup === this.filmBindGroup ? MIRROR_COLOR : 0,
2610
- this.currentTarget(),
2611
- );
2672
+ const filmCommand = this.recordBlended(filmGroup === this.filmBindGroup ? MIRROR_COLOR : 0);
2612
2673
  if (filmCommand !== null) {
2613
2674
  filmCommand.pipeline = filmPipe;
2614
2675
  filmCommand.bindGroup = filmGroup;
@@ -2799,6 +2860,7 @@ export class WebGPURenderer implements RendererApi {
2799
2860
  */
2800
2861
  setSkinPalette(palette: Float32Array | null): void {
2801
2862
  if (this.surface.lost) return;
2863
+ this.skinPaletteData = palette;
2802
2864
  if (palette === null) {
2803
2865
  this.skinPaletteSlot = -1;
2804
2866
  return;
@@ -3167,7 +3229,8 @@ export class WebGPURenderer implements RendererApi {
3167
3229
  * no-op before the first `bindMeshPass` of a session, same as every other pass that reads it.
3168
3230
  */
3169
3231
  drawSdfText(text: GpuSdfText, model: Float32Array, color: Vec3, opacity: number): void {
3170
- const viewProj = this.viewProj;
3232
+ /* Unjittered when it lands after the upscale, like every late draw. See `drawsLate`. */
3233
+ const viewProj = this.drawsLate() ? this.correctedViewProj : this.viewProj;
3171
3234
  if (!this.canDraw() || viewProj === null) return;
3172
3235
  const font = text.font;
3173
3236
  const atlas = text.atlas;
@@ -3201,12 +3264,12 @@ export class WebGPURenderer implements RendererApi {
3201
3264
  this.sdfTextFrags.writeFloat(fragmentSlot, fragAt('uOutputExposure'), this.gradeExposure());
3202
3265
 
3203
3266
  const sdfPipe = sdfTextPipeline(
3204
- this.targetPipelines(),
3267
+ this.blendedPipelines(),
3205
3268
  this.surface.device,
3206
3269
  this.sdfTextLayout,
3207
3270
  );
3208
3271
  const sdfGroup = this.sdfTextBindGroupFor(atlas);
3209
- const sdfCommand = this.recordDraw(0, this.currentTarget());
3272
+ const sdfCommand = this.recordBlended(0);
3210
3273
  if (sdfCommand !== null) {
3211
3274
  sdfCommand.pipeline = sdfPipe;
3212
3275
  sdfCommand.bindGroup = sdfGroup;
@@ -3322,8 +3385,8 @@ export class WebGPURenderer implements RendererApi {
3322
3385
 
3323
3386
  const pass = this.openTextPass();
3324
3387
  if (pass === null) return;
3325
- const textPipe = textPipeline(this.targetPipelines(), this.surface.device, this.textLayout);
3326
- const textCommand = this.recordDraw(0, this.currentTarget());
3388
+ const textPipe = textPipeline(this.blendedPipelines(), this.surface.device, this.textLayout);
3389
+ const textCommand = this.recordBlended(0);
3327
3390
  if (textCommand !== null) {
3328
3391
  textCommand.pipeline = textPipe;
3329
3392
  textCommand.bindGroup = this.textBindGroup;
@@ -3773,13 +3836,13 @@ export class WebGPURenderer implements RendererApi {
3773
3836
 
3774
3837
  const layout = this.particleLayoutFor(batch.material).layout;
3775
3838
  const particlePipe = particlePipeline(
3776
- this.pipelines,
3839
+ this.blendedPipelines(),
3777
3840
  device,
3778
3841
  layout,
3779
3842
  batch.material,
3780
3843
  batch.blend,
3781
3844
  );
3782
- const particleCommand = this.recordDraw(0, this.currentTarget());
3845
+ const particleCommand = this.recordBlended(0);
3783
3846
  if (particleCommand !== null) {
3784
3847
  particleCommand.pipeline = particlePipe;
3785
3848
  particleCommand.bindGroup = batch.bindGroup;
@@ -3924,13 +3987,13 @@ export class WebGPURenderer implements RendererApi {
3924
3987
  frags.writeFloats(fragmentSlot, atF('uTint'), plumes.tint);
3925
3988
 
3926
3989
  const plumePipe = plumePipeline(
3927
- this.pipelines,
3990
+ this.blendedPipelines(),
3928
3991
  this.surface.device,
3929
3992
  this.plumeLayout,
3930
3993
  plumes.material,
3931
3994
  plumes.blend,
3932
3995
  );
3933
- const plumeCommand = this.recordDraw(0, this.currentTarget());
3996
+ const plumeCommand = this.recordBlended(0);
3934
3997
  if (plumeCommand !== null) {
3935
3998
  plumeCommand.pipeline = plumePipe;
3936
3999
  plumeCommand.bindGroup = this.plumeBindGroup;
@@ -4485,9 +4548,13 @@ export class WebGPURenderer implements RendererApi {
4485
4548
  this.windStreakVerts.writeBlock(vertexSlot, this.windStreakVertBlock);
4486
4549
  this.windStreakFrags.writeBlock(fragmentSlot, this.windStreakFragBlock);
4487
4550
 
4488
- const windPipe = windStreakPipeline(this.pipelines, this.surface.device, this.windStreakLayout);
4551
+ const windPipe = windStreakPipeline(
4552
+ this.blendedPipelines(),
4553
+ this.surface.device,
4554
+ this.windStreakLayout,
4555
+ );
4489
4556
  /* Two uniform buffers and two vertex buffers. Nothing is sampled, so nothing is read. */
4490
- const windCommand = this.recordDraw(0, this.currentTarget());
4557
+ const windCommand = this.recordBlended(0);
4491
4558
  if (windCommand !== null) {
4492
4559
  windCommand.pipeline = windPipe;
4493
4560
  windCommand.bindGroup = this.windStreakBindGroup;
@@ -4715,8 +4782,8 @@ export class WebGPURenderer implements RendererApi {
4715
4782
  this.boltVerts.writeBlock(vertexSlot, this.boltVertBlock);
4716
4783
  this.boltFrags.writeBlock(fragmentSlot, this.boltFragInts);
4717
4784
 
4718
- const boltPipe = boltPipeline(this.pipelines, this.surface.device, this.boltLayout);
4719
- const boltCommand = this.recordDraw(0, this.currentTarget());
4785
+ const boltPipe = boltPipeline(this.blendedPipelines(), this.surface.device, this.boltLayout);
4786
+ const boltCommand = this.recordBlended(0);
4720
4787
  if (boltCommand !== null) {
4721
4788
  boltCommand.pipeline = boltPipe;
4722
4789
  boltCommand.bindGroup = this.boltBindGroup;
@@ -4853,9 +4920,14 @@ export class WebGPURenderer implements RendererApi {
4853
4920
  this.lineFrags.writeFloat(fragmentSlot, atF('uFogNear'), medium.fogNear);
4854
4921
  this.lineFrags.writeFloat(fragmentSlot, atF('uFogFar'), medium.fogFar);
4855
4922
 
4856
- const linePipe = linePipeline(this.pipelines, this.surface.device, this.lineLayout, additive);
4923
+ const linePipe = linePipeline(
4924
+ this.blendedPipelines(),
4925
+ this.surface.device,
4926
+ this.lineLayout,
4927
+ additive,
4928
+ );
4857
4929
  /* Two uniform blocks and three vertex buffers, and no attachment among them. */
4858
- const lineCommand = this.recordDraw(0, this.currentTarget());
4930
+ const lineCommand = this.recordBlended(0);
4859
4931
  if (lineCommand !== null) {
4860
4932
  lineCommand.pipeline = linePipe;
4861
4933
  lineCommand.bindGroup = this.lineBindGroup;
@@ -5078,8 +5150,39 @@ export class WebGPURenderer implements RendererApi {
5078
5150
 
5079
5151
  /* -- The shadow pass -------------------------------------------------------------- */
5080
5152
 
5081
- private readonly shadowMap: GPUTexture;
5082
- private readonly shadowView: GPUTextureView;
5153
+ /**
5154
+ * The sun's static, moving and peeled maps, as layers of one texture (`shadowMap.ts`). The three
5155
+ * views below are single layers, what each pass attaches; the two here are what is sampled.
5156
+ */
5157
+ private sunShadowTexture: GPUTexture;
5158
+ /** Every layer, for the lit pass, the light volumes and the medium. */
5159
+ private sunArrayView: GPUTextureView;
5160
+ /**
5161
+ * The static layer alone, as an array, for the peel: it draws into another layer of the same
5162
+ * texture, and a view covering that layer too would put it in two usages in one pass.
5163
+ */
5164
+ private sunStaticArrayView: GPUTextureView;
5165
+ /** How many layers the sun's texture holds: its maps, and the two glass layers once glass casts. */
5166
+ private sunLayerCount: number;
5167
+ /** The glass depth layers, static then moving, once glass has cast. See `growSunForGlass`. */
5168
+ private sunGlassViews: readonly [GPUTextureView, GPUTextureView] | null = null;
5169
+ /** The texture a growth replaced, destroyed once the encoder that copied out of it is submitted. */
5170
+ private retiredSunTexture: GPUTexture | null = null;
5171
+ /** What the sun's light keeps through glass: two `rgba8unorm` layers, made when glass first casts. */
5172
+ private sunTint: SunTintTarget | null = null;
5173
+ /** The tint pass's layouts, per-draw block, groups and pipelines, made when glass first casts. */
5174
+ private glassTint: GpuGlassTint | null = null;
5175
+ /** The tint pass open while the glass list is replayed into it. */
5176
+ private tintPass: GPURenderPassEncoder | null = null;
5177
+ /** True while the glass list is replayed into a glass depth layer through `casterSink`. */
5178
+ private drawingGlassDepth = false;
5179
+ /** Whether the open sun pass is the movers' layer, whose glass goes to the moving glass layer. */
5180
+ private shadowLayerIsDynamic = false;
5181
+ /** The glass tints' sampler: filtering, and between levels, for frost's coarser reads. */
5182
+ private readonly tintSampler: GPUSampler;
5183
+ /** The light a tint pass is drawn for: xyz a direction (w 0) or a position (w 1). */
5184
+ private readonly glassLight = new Float32Array(4);
5185
+ private shadowView: GPUTextureView;
5083
5186
  private readonly shadowLayout: GPUBindGroupLayout;
5084
5187
  /** The same layout plus the joint palette. See `createDepthBindGroupLayout`. */
5085
5188
  private readonly shadowSkinnedLayout: GPUBindGroupLayout;
@@ -5105,8 +5208,7 @@ export class WebGPURenderer implements RendererApi {
5105
5208
  * read twice — so a profile that switches peeling off gets no map and no pass on either
5106
5209
  * backend rather than a map nothing writes.
5107
5210
  */
5108
- private readonly peelMap: GPUTexture | null;
5109
- private readonly peelView: GPUTextureView | null;
5211
+ private peelView: GPUTextureView | null;
5110
5212
  /**
5111
5213
  * The movers' own layer: the character, the rope it swings on, and anything else that walks.
5112
5214
  *
@@ -5127,10 +5229,9 @@ export class WebGPURenderer implements RendererApi {
5127
5229
  * and therefore as lit — so nothing looked broken, only empty, which is why it survived the
5128
5230
  * port and every gate in it.
5129
5231
  */
5130
- private readonly dynamicMap: GPUTexture | null;
5131
- private readonly dynamicView: GPUTextureView | null;
5232
+ private dynamicView: GPUTextureView | null;
5132
5233
  /** The peel's own bind groups: `uPeelShadowLayer` at 1, and the static map to peel against. */
5133
- private readonly shadowPeelBindGroup: GPUBindGroup | null;
5234
+ private shadowPeelBindGroup: GPUBindGroup | null;
5134
5235
  /**
5135
5236
  * The cutout casters' layout, and a bind group per cutout map for each layer, built the first
5136
5237
  * time a map casts and kept: a `createBindGroup` per draw is the allocation the house rules forbid.
@@ -5142,7 +5243,7 @@ export class WebGPURenderer implements RendererApi {
5142
5243
  private readonly cutoutPeelGroups = new Map<GpuSurfaceTexture, GPUBindGroup>();
5143
5244
  private shadowPeelPassUniforms: GPUBuffer | null = null;
5144
5245
  private shadowPeelSampler: GPUSampler | null = null;
5145
- private readonly scatterDepthPeelBindGroup: GPUBindGroup | null;
5246
+ private scatterDepthPeelBindGroup: GPUBindGroup | null;
5146
5247
  /** Which layer the open pass is writing, so the sink picks the bindings that match it. */
5147
5248
  private shadowLayerIsPeel = false;
5148
5249
  /**
@@ -5201,7 +5302,7 @@ export class WebGPURenderer implements RendererApi {
5201
5302
  const held = groups.get(texture);
5202
5303
  if (held !== undefined) return held;
5203
5304
  const passUniforms = peel ? this.shadowPeelPassUniforms : this.shadowPassUniforms;
5204
- const previousView = peel ? this.shadowView : this.shadowBlankView;
5305
+ const previousView = peel ? this.sunStaticArrayView : this.shadowBlankView;
5205
5306
  const previousSampler = peel ? this.shadowPeelSampler : this.shadowBlankSampler;
5206
5307
  if (passUniforms === null || previousView === null || previousSampler === null) return null;
5207
5308
  const group = createDepthBindGroup(
@@ -5279,8 +5380,23 @@ export class WebGPURenderer implements RendererApi {
5279
5380
  * Built once and held, rather than made per call: it closes over `this` and a fresh one
5280
5381
  * every frame would be an allocation in a per-frame path.
5281
5382
  */
5383
+ /** The glass the open shadow pass was offered, kept for its own passes: `glassCasters.ts`. */
5384
+ private readonly glassCasters = new GlassCasterList();
5385
+ /** One resolution every glass caster is read through, so recording one allocates nothing. */
5386
+ private readonly glassCasterScratch: ResolvedGlass = {
5387
+ transmission: 0,
5388
+ frost: 0,
5389
+ tint: [1, 1, 1],
5390
+ };
5282
5391
  private readonly casterSink: ShadowCasterSink = {
5283
5392
  mesh: (mesh, model, material) => {
5393
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
5394
+ /* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
5395
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
5396
+ this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
5397
+ }
5398
+ return;
5399
+ }
5284
5400
  const pass = this.shadowPass;
5285
5401
  if (pass === null) return;
5286
5402
  /* A cutout caster's group is looked up before a slot is spent, so a miss costs nothing. */
@@ -5330,6 +5446,12 @@ export class WebGPURenderer implements RendererApi {
5330
5446
  * uses — one draw, however many instances, exactly as the colour pass does it.
5331
5447
  */
5332
5448
  instanced: (batch, data, material) => {
5449
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
5450
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
5451
+ this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
5452
+ }
5453
+ return;
5454
+ }
5333
5455
  const pass = this.shadowPass;
5334
5456
  if (pass === null) return;
5335
5457
  const gpuBatch = batch as GpuInstancedBatch;
@@ -5378,7 +5500,13 @@ export class WebGPURenderer implements RendererApi {
5378
5500
  pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
5379
5501
  pass.drawIndexed(geometry.indexCount, count);
5380
5502
  },
5381
- skinnedMesh: (mesh, model, palette) => {
5503
+ skinnedMesh: (mesh, model, palette, material) => {
5504
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
5505
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
5506
+ this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
5507
+ }
5508
+ return;
5509
+ }
5382
5510
  const pass = this.shadowPass;
5383
5511
  if (pass === null) return;
5384
5512
  /* Its own slot, so two characters casting into one bake do not both take the second one's
@@ -5762,10 +5890,13 @@ export class WebGPURenderer implements RendererApi {
5762
5890
  * nothing at all under the default profile, which is on. The cache carries it for the same
5763
5891
  * reason it carries the sample count: no pass can then disagree with the attachment.
5764
5892
  */
5893
+ /* Off compiles none of the glass lookup into a lit pipeline: see `PipelineCache.glassShadows`. */
5894
+ const glassShadows = quality.glassShadows !== 'off';
5765
5895
  this.pipelines = new PipelineCache(
5766
5896
  device,
5767
5897
  quality.screenEffects ? sceneColorFormat(quality.hdrScene) : surface.format,
5768
5898
  this.samples,
5899
+ glassShadows,
5769
5900
  );
5770
5901
  /*
5771
5902
  * The canvas' own state, for the pass `ensurePass` reopens after `endFrame`.
@@ -5778,7 +5909,12 @@ export class WebGPURenderer implements RendererApi {
5778
5909
  this.overlayPipelines =
5779
5910
  this.pipelines.format === surface.format && this.samples === 1
5780
5911
  ? this.pipelines
5781
- : new PipelineCache(device, surface.format, 1);
5912
+ : new PipelineCache(device, surface.format, 1, glassShadows);
5913
+ this.latePipelines =
5914
+ this.pipelines.format === RECON_HISTORY_FORMAT && this.samples === 1
5915
+ ? this.pipelines
5916
+ : new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows);
5917
+ this.issueBound = this.issueCommand.bind(this);
5782
5918
  this.pipelineTargets =
5783
5919
  this.overlayPipelines === this.pipelines
5784
5920
  ? [this.pipelines]
@@ -5929,42 +6065,35 @@ export class WebGPURenderer implements RendererApi {
5929
6065
  const blankDriftIndexView = blankVolume('flat.emptyDriftLightIndex', 'r32uint');
5930
6066
  const blankDriftAtlasView = blankVolume('flat.emptyDriftLightAtlas', 'rgba16float');
5931
6067
 
5932
- this.shadowMap = device.createTexture({
5933
- label: 'shadow.static',
5934
- size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
6068
+ /*
6069
+ * The static world, its movers and — where more than one static depth layer is asked for —
6070
+ * the peel, as layers of one texture, on the conditions `renderer.ts` builds its array on: one
6071
+ * decision read twice. Same size and format throughout, because the shader samples every
6072
+ * layer with one \`uShadowMapSize\` and one set of filter offsets.
6073
+ */
6074
+ const sunLayers = this.quality.directionalShadows
6075
+ ? this.quality.directionalShadowDepthLayers > 1
6076
+ ? 3
6077
+ : 2
6078
+ : 1;
6079
+ this.sunShadowTexture = device.createTexture({
6080
+ label: 'shadow.sun',
6081
+ size: [
6082
+ this.quality.directionalShadowMapSize,
6083
+ this.quality.directionalShadowMapSize,
6084
+ sunLayers,
6085
+ ],
5935
6086
  format: SHADOW_FORMAT,
5936
- usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
6087
+ /* Copyable both ways, because the first glass grows it by copying every layer across. */
6088
+ usage: SUN_USAGE,
5937
6089
  });
5938
- this.shadowView = this.shadowMap.createView();
5939
- /*
5940
- * The peel layer, on the condition `renderer.ts` builds its own on: more than one depth
5941
- * layer asked for. `depth.ts` fills it by discarding every fragment at or in front of what
5942
- * the static pass stored, so what lands here is the second independently fading occluder.
5943
- */
5944
- this.peelMap =
5945
- this.quality.directionalShadowDepthLayers > 1
5946
- ? device.createTexture({
5947
- label: 'shadow.peel',
5948
- size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
5949
- format: SHADOW_FORMAT,
5950
- usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
5951
- })
5952
- : null;
5953
- this.peelView = this.peelMap === null ? null : this.peelMap.createView();
5954
- /*
5955
- * The movers' layer, on the condition `renderer.ts` builds its own on: directional shadows
5956
- * at all. Same size and same format as the static map, because the shader samples all three
5957
- * with one `uShadowMapSize` and one set of PCF offsets.
5958
- */
5959
- this.dynamicMap = this.quality.directionalShadows
5960
- ? device.createTexture({
5961
- label: 'shadow.dynamic',
5962
- size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
5963
- format: SHADOW_FORMAT,
5964
- usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
5965
- })
5966
- : null;
5967
- this.dynamicView = this.dynamicMap === null ? null : this.dynamicMap.createView();
6090
+ this.sunLayerCount = sunLayers;
6091
+ const views = sunViewsOf(this.sunShadowTexture, sunLayers, sunLayers);
6092
+ this.sunArrayView = views.array;
6093
+ this.sunStaticArrayView = views.staticArray;
6094
+ this.shadowView = views.static;
6095
+ this.dynamicView = views.dynamic;
6096
+ this.peelView = views.peel;
5968
6097
 
5969
6098
  /*
5970
6099
  * **Cleared once, here, because a texture no pass has written reads as zero**, which the
@@ -6005,6 +6134,14 @@ export class WebGPURenderer implements RendererApi {
6005
6134
  * rather than the hardware's.
6006
6135
  */
6007
6136
  const shadowSampler = device.createSampler({ label: 'shadow.sampler' });
6137
+ this.tintSampler = device.createSampler({
6138
+ label: 'shadow.tintSampler',
6139
+ magFilter: 'linear',
6140
+ minFilter: 'linear',
6141
+ mipmapFilter: 'linear',
6142
+ addressModeU: 'clamp-to-edge',
6143
+ addressModeV: 'clamp-to-edge',
6144
+ });
6008
6145
  /*
6009
6146
  * The photometric atlas's own sampler, **created here rather than reusing `postSampler`.**
6010
6147
  *
@@ -6026,14 +6163,17 @@ export class WebGPURenderer implements RendererApi {
6026
6163
  });
6027
6164
 
6028
6165
  this.flatTextures = (name) => {
6029
- if (name === 'uStaticShadowMap') return { view: this.shadowView, sampler: shadowSampler };
6030
- /*
6031
- * The movers' layer. Cleared at construction like the others, so a profile with
6032
- * directional shadows on that never opens the layer reads the far plane and therefore
6033
- * "nothing moving is in the way" — not whatever the allocation happened to contain.
6034
- */
6035
- if (name === 'uDynamicShadowMap' && this.dynamicView !== null) {
6036
- return { view: this.dynamicView, sampler: shadowSampler };
6166
+ if (name === 'uSunShadows') return { view: this.sunArrayView, sampler: shadowSampler };
6167
+ /* What the sun keeps through glass, or the one-texel stand-in the lookup reads as none. */
6168
+ if (name === 'uSunGlassTints') {
6169
+ return { view: this.sunTint?.array ?? blankArrayView, sampler: this.tintSampler };
6170
+ }
6171
+ /* What the lamps keep through glass, one layer a light, or the same stand-in. */
6172
+ if (name === 'uPointGlassTints') {
6173
+ return {
6174
+ view: this.pointShadowArray?.glassTint?.view ?? blankArrayView,
6175
+ sampler: this.tintSampler,
6176
+ };
6037
6177
  }
6038
6178
  /*
6039
6179
  * The caller's image, or the stand-in. Read from the field rather than passed in, because
@@ -6071,10 +6211,6 @@ export class WebGPURenderer implements RendererApi {
6071
6211
  ? { view: blankView, sampler: blankSampler }
6072
6212
  : { view: albedo.view, sampler: albedo.sampler };
6073
6213
  }
6074
- /* The peel, where the profile asked for one. `uPeeledShadowEnabled` gates the sample. */
6075
- if (name === 'uPeeledShadowMap' && this.peelView !== null) {
6076
- return { view: this.peelView, sampler: shadowSampler };
6077
- }
6078
6214
  /*
6079
6215
  * The room, once a bake has filled it. Unbaked reads as the blank cube: nothing to mirror.
6080
6216
  *
@@ -6143,7 +6279,15 @@ export class WebGPURenderer implements RendererApi {
6143
6279
  * texels by construction, an offset that snapped to one being no offset at all.
6144
6280
  */
6145
6281
  if (name === 'uRefractScene') {
6146
- return { view: this.refractSnapshotView ?? blankView, sampler: iesSampler };
6282
+ this.refractSampler ??= device.createSampler({
6283
+ label: 'refract.sampler',
6284
+ magFilter: 'linear',
6285
+ minFilter: 'linear',
6286
+ mipmapFilter: 'linear',
6287
+ addressModeU: 'clamp-to-edge',
6288
+ addressModeV: 'clamp-to-edge',
6289
+ });
6290
+ return { view: this.refractSnapshotView ?? blankView, sampler: this.refractSampler };
6147
6291
  }
6148
6292
  /*
6149
6293
  * Every point light's shadow, in one binding.
@@ -6369,9 +6513,7 @@ export class WebGPURenderer implements RendererApi {
6369
6513
  */
6370
6514
  this.mediumLayout = createPostStageLayout(device, MEDIUM_UNIFORMS, MEDIUM_FRAG_SIZE, [
6371
6515
  { binding: MEDIUM_TEXTURES.uDepth, filterable: false },
6372
- { binding: MEDIUM_TEXTURES.uStaticShadowMap, filterable: false },
6373
- { binding: MEDIUM_TEXTURES.uPeeledShadowMap, filterable: false },
6374
- { binding: MEDIUM_TEXTURES.uDynamicShadowMap, filterable: false },
6516
+ { binding: MEDIUM_TEXTURES.uSunShadows, filterable: false, dimension: '2d-array' },
6375
6517
  ]);
6376
6518
  this.mediumUniforms = device.createBuffer({
6377
6519
  label: 'post.mediumUniforms',
@@ -6603,7 +6745,7 @@ export class WebGPURenderer implements RendererApi {
6603
6745
  this.scatterDepthDraws.buffer,
6604
6746
  { binding: DEPTH_FRAG_BINDING, buffer: depthPassUniforms, size: DEPTH_FRAG_SIZE },
6605
6747
  DEPTH_PREVIOUS_BINDING,
6606
- blankView,
6748
+ blankArrayView,
6607
6749
  blankSampler,
6608
6750
  );
6609
6751
  this.shadowBindGroup = createDepthBindGroup(
@@ -6611,10 +6753,10 @@ export class WebGPURenderer implements RendererApi {
6611
6753
  this.shadowLayout,
6612
6754
  this.shadowDraws.buffer,
6613
6755
  depthPassUniforms,
6614
- blankView,
6756
+ blankArrayView,
6615
6757
  blankSampler,
6616
6758
  );
6617
- this.shadowBlankView = blankView;
6759
+ this.shadowBlankView = blankArrayView;
6618
6760
  this.shadowBlankSampler = blankSampler;
6619
6761
  this.shadowPassUniforms = depthPassUniforms;
6620
6762
 
@@ -6652,7 +6794,7 @@ export class WebGPURenderer implements RendererApi {
6652
6794
  this.shadowLayout,
6653
6795
  this.shadowDraws.buffer,
6654
6796
  peelPassUniforms,
6655
- this.shadowView,
6797
+ this.sunStaticArrayView,
6656
6798
  shadowSampler,
6657
6799
  );
6658
6800
  this.scatterDepthPeelBindGroup =
@@ -6664,7 +6806,7 @@ export class WebGPURenderer implements RendererApi {
6664
6806
  this.scatterDepthDraws.buffer,
6665
6807
  { binding: DEPTH_FRAG_BINDING, buffer: peelPassUniforms, size: DEPTH_FRAG_SIZE },
6666
6808
  DEPTH_PREVIOUS_BINDING,
6667
- this.shadowView,
6809
+ this.sunStaticArrayView,
6668
6810
  shadowSampler,
6669
6811
  );
6670
6812
 
@@ -7070,7 +7212,10 @@ export class WebGPURenderer implements RendererApi {
7070
7212
  this.perFrame.reset();
7071
7213
  this.materials.dirty();
7072
7214
  /* Spent by `runMotion` at the end of the last frame; a frame states its movers afresh. */
7073
- this.motionCount = 0;
7215
+ this.motionPass?.reset();
7216
+ this.latePass?.reset();
7217
+ this.moverFrame += 1;
7218
+ this.inFrame = true;
7074
7219
  /*
7075
7220
  * The canvas rather than the swap chain's texture, and the difference is that reading this
7076
7221
  * does not *acquire* anything. See `swapView`.
@@ -7391,7 +7536,27 @@ export class WebGPURenderer implements RendererApi {
7391
7536
  'what is kept and what a buffer is hinted as.',
7392
7537
  );
7393
7538
  }
7394
- mesh.update(this.surface.device, positions, normals);
7539
+ /*
7540
+ * **The rows before this rewrite are last frame's**, kept only for a reconstruction and only
7541
+ * on the first rewrite of a frame — a second must not replace where the vertices were last
7542
+ * frame with where they were a moment ago. A rewrite between frames is the next frame's. See
7543
+ * `changeFrames.ts`.
7544
+ */
7545
+ const motion = mesh.motion;
7546
+ let previous: GPUBuffer | null = null;
7547
+ if (motion !== null && this.reconstructionWanted) {
7548
+ const frame = frameOfChange(this.moverFrame, this.inFrame);
7549
+ if (keepsPrevious(motion.changed, frame)) {
7550
+ motion.previous ??= this.surface.device.createBuffer({
7551
+ label: 'recon.dynamicPrevious',
7552
+ size: mesh.vertexBuffers[0]?.size ?? 0,
7553
+ usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
7554
+ });
7555
+ previous = motion.previous;
7556
+ }
7557
+ motion.changed = frame;
7558
+ }
7559
+ mesh.update(this.surface.device, positions, normals, previous);
7395
7560
  }
7396
7561
 
7397
7562
  /**
@@ -8050,7 +8215,7 @@ export class WebGPURenderer implements RendererApi {
8050
8215
  depthLayer = 0,
8051
8216
  tint: Vec3 | null = null,
8052
8217
  /** Where it was last frame. See `RendererApi.drawMesh`, and `runMotion` for what reads it. */
8053
- previousModel: ArrayLike<number> | null = null,
8218
+ previousModel: ArrayLike<number> | Mover | null = null,
8054
8219
  ): void {
8055
8220
  /*
8056
8221
  * Skipped where the flag asks and the bounds say so.
@@ -8075,7 +8240,7 @@ export class WebGPURenderer implements RendererApi {
8075
8240
  * depth, so a motion pass drawing it would either be depth-rejected everywhere — wasted — or,
8076
8241
  * where it happened to pass, write motion for a surface no pixel of the frame belongs to.
8077
8242
  */
8078
- if (previousModel !== null) this.recordMotion(mesh, model, previousModel);
8243
+ if (this.tracksMotionNow()) this.recordDrawMotion(mesh, model, previousModel);
8079
8244
  this.submitMesh(mesh, model, tint, 1, false, { depthLayer });
8080
8245
  }
8081
8246
 
@@ -8238,7 +8403,13 @@ export class WebGPURenderer implements RendererApi {
8238
8403
  return;
8239
8404
  }
8240
8405
 
8241
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, this.viewProj);
8406
+ /* A blended draw landing after the upscale is drawn into an unjittered picture. */
8407
+ const late = blend && (this.drawsLate() || (this.oitReplaying && this.reconstructing));
8408
+ this.perDraw.writeFloats(
8409
+ slot,
8410
+ FLAT_VERT_FIELDS.uViewProj.offset,
8411
+ late ? this.correctedViewProj : this.viewProj,
8412
+ );
8242
8413
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uModel.offset, model);
8243
8414
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uTint.offset, tint ?? WHITE);
8244
8415
  /* The frame's wind, for a mesh carrying a sway lane. Written on this path and not the
@@ -8298,17 +8469,7 @@ export class WebGPURenderer implements RendererApi {
8298
8469
  * shades as an ordinary translucent one rather than sampling a texture holding the frame
8299
8470
  * before last. That is the defined state the two-backends rule asks for.
8300
8471
  */
8301
- const refraction = options.refraction ?? 0;
8302
- const refracting = refraction > 0 && this.takeRefractSnapshot();
8303
- if (refracting) {
8304
- this.perFrameFloats[this.materialField('uRefractStrength')] = refraction;
8305
- const refractTint = options.refractTint;
8306
- const tintAt = this.materialField('uRefractTint');
8307
- this.perFrameFloats[tintAt] = refractTint?.[0] ?? 1;
8308
- this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
8309
- this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
8310
- this.perFrameFloats[this.materialField('uRefractThickness')] = options.thicknessM ?? 0;
8311
- }
8472
+ const refracting = this.bindSeeThrough(options);
8312
8473
  /* Taken for itself when any of those differs from the pass, and put back below: the rule both
8313
8474
  backends count material changes by. See `materialChanges.ts`. */
8314
8475
  const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
@@ -8368,7 +8529,7 @@ export class WebGPURenderer implements RendererApi {
8368
8529
  * pays the compile it was trying to avoid. That is a slow frame; the alternative is a
8369
8530
  * thrown error in the middle of one.
8370
8531
  */
8371
- const pipelines = this.targetPipelines();
8532
+ const pipelines = blend ? this.blendedPipelines() : this.targetPipelines();
8372
8533
  let pipeline = pipelines.peek(key);
8373
8534
  if (pipeline === undefined) {
8374
8535
  const present = this.meshPresent.get(base);
@@ -8416,7 +8577,7 @@ export class WebGPURenderer implements RendererApi {
8416
8577
  * the uniform rings are the state arena this design would otherwise have had to build —
8417
8578
  * so the command carries them as its two dynamic offsets.
8418
8579
  */
8419
- const command = this.recordDraw(0, this.currentTarget());
8580
+ const command = blend ? this.recordBlended(0) : this.recordDraw(0, this.currentTarget());
8420
8581
  if (command !== null) {
8421
8582
  command.pipeline = pipeline;
8422
8583
  command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
@@ -8453,11 +8614,9 @@ export class WebGPURenderer implements RendererApi {
8453
8614
  if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
8454
8615
  if (!fog) this.perFrameInts[this.materialField('uFogEnabled')] = 1;
8455
8616
  }
8456
- if (refracting) {
8457
- /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
8458
- shared, so a strength left set is worn by everything drawn after it. */
8459
- this.perFrameFloats[this.materialField('uRefractStrength')] = 0;
8460
- }
8617
+ /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
8618
+ shared, so a strength left set is worn by everything drawn after it. */
8619
+ if (refracting) this.releaseSeeThrough();
8461
8620
  if (!toneMapped) {
8462
8621
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
8463
8622
  }
@@ -8531,12 +8690,35 @@ export class WebGPURenderer implements RendererApi {
8531
8690
  /** Push placement and colour. Only the live prefix; see `GpuInstancedBatch.upload`. */
8532
8691
  uploadInstanced(batch: GpuInstancedBatch, data: MeshInstances): void {
8533
8692
  if (this.surface.lost) return;
8534
- batch.upload(this.surface.device.queue, data);
8693
+ /*
8694
+ * **The placement before this upload is last frame's**, kept only for a reconstruction and only
8695
+ * on a frame's first upload, for the reason `updateMesh` gives. See `changeFrames.ts`.
8696
+ */
8697
+ let previous: GPUBuffer | null = null;
8698
+ if (this.reconstructionWanted) {
8699
+ const frame = frameOfChange(this.moverFrame, this.inFrame);
8700
+ if (keepsPrevious(batch.changed, frame)) {
8701
+ batch.previousBuffer ??= this.surface.device.createBuffer({
8702
+ label: 'recon.instancesPrevious',
8703
+ size: batch.capacity * INSTANCE_STRIDE,
8704
+ usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
8705
+ });
8706
+ previous = batch.previousBuffer;
8707
+ }
8708
+ batch.changed = frame;
8709
+ }
8710
+ batch.upload(this.surface.device.queue, data, previous);
8535
8711
  }
8536
8712
 
8537
8713
  /** Draw every live instance, opaque. */
8538
8714
  drawInstanced(batch: GpuInstancedBatch, data: MeshInstances): void {
8539
8715
  this.submitInstanced(batch, data, 1, false, {});
8716
+ /* A batch that moved this frame pairs each slot with last frame's; the batch is the identity. */
8717
+ const previous = batch.previousBuffer;
8718
+ if (previous !== null && batch.changed === this.moverFrame && this.tracksMotionNow()) {
8719
+ const paired = pairedInstances(batch.previousCount, Math.min(data.count, batch.capacity));
8720
+ if (paired > 0) this.motionPass?.recordInstanced(batch.mesh, batch.buffer, previous, paired);
8721
+ }
8540
8722
  }
8541
8723
 
8542
8724
  /** Draw every live instance, blended. See `drawTranslucentMesh` for what the options mean. */
@@ -8555,6 +8737,45 @@ export class WebGPURenderer implements RendererApi {
8555
8737
  batch.dispose();
8556
8738
  }
8557
8739
 
8740
+ /**
8741
+ * Write what a translucent draw shows through it into the material scratch: refraction's bend and
8742
+ * absorption, and glass. Returns whether the frame's copy is there to read, and so whether
8743
+ * `releaseSeeThrough` owes the scratch back.
8744
+ *
8745
+ * **A glass draw takes the copy even when it does not bend**, since a pane shows what is behind it
8746
+ * either way. A copy that cannot be taken — a mirror, a probe bake — leaves both off, so the pane
8747
+ * shades as ordinary translucency rather than sampling a stand-in. See `glass.ts`.
8748
+ */
8749
+ private bindSeeThrough(options: TranslucentMeshOptions): boolean {
8750
+ const refraction = options.refraction ?? 0;
8751
+ const glassy = resolveGlass(options.glass, this.glassScratch);
8752
+ if ((refraction <= 0 && !glassy) || !this.takeRefractSnapshot()) return false;
8753
+ const glass = this.glassScratch;
8754
+ /* Strength, path thickness, transmission, frost: one vector, for the budget the shader states. */
8755
+ const seeAt = this.materialField('uSeeThrough');
8756
+ this.perFrameFloats[seeAt] = Math.max(refraction, 0);
8757
+ this.perFrameFloats[seeAt + 1] = options.thicknessM ?? 0;
8758
+ this.perFrameFloats[seeAt + 2] = glass.transmission;
8759
+ this.perFrameFloats[seeAt + 3] = glass.frost;
8760
+ const refractTint = options.refractTint;
8761
+ const tintAt = this.materialField('uRefractTint');
8762
+ this.perFrameFloats[tintAt] = refractTint?.[0] ?? 1;
8763
+ this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
8764
+ this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
8765
+ const glassTintAt = this.materialField('uGlassTint');
8766
+ this.perFrameFloats[glassTintAt] = glass.tint[0];
8767
+ this.perFrameFloats[glassTintAt + 1] = glass.tint[1];
8768
+ this.perFrameFloats[glassTintAt + 2] = glass.tint[2];
8769
+ return true;
8770
+ }
8771
+
8772
+ /** Put the scratch back after `bindSeeThrough`: refraction and glass off. */
8773
+ private releaseSeeThrough(): void {
8774
+ const seeAt = this.materialField('uSeeThrough');
8775
+ this.perFrameFloats[seeAt] = 0;
8776
+ this.perFrameFloats[seeAt + 2] = 0;
8777
+ }
8778
+
8558
8779
  /**
8559
8780
  * One draw, one material slot, one uniform slot, however many instances.
8560
8781
  *
@@ -8591,7 +8812,11 @@ export class WebGPURenderer implements RendererApi {
8591
8812
  return;
8592
8813
  }
8593
8814
 
8594
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, this.viewProj);
8815
+ this.perDraw.writeFloats(
8816
+ slot,
8817
+ FLAT_VERT_FIELDS.uViewProj.offset,
8818
+ blend && this.drawsLate() ? this.correctedViewProj : this.viewProj,
8819
+ );
8595
8820
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
8596
8821
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
8597
8822
  this.perDraw.writeInt(slot, FLAT_VERT_FIELDS.uHasTangents.offset, mesh.hasTangents ? 1 : 0);
@@ -8614,8 +8839,9 @@ export class WebGPURenderer implements RendererApi {
8614
8839
 
8615
8840
  const depthWrite = options.depthWrite ?? true;
8616
8841
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
8617
- /* An instanced draw does not refract. See `materialChanges.ts`. */
8618
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting: false });
8842
+ /* A blended batch shows what is behind it — refraction or glass — as a single draw does. */
8843
+ const refracting = blend && this.bindSeeThrough(options);
8844
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
8619
8845
  if (own) this.materials.dirty();
8620
8846
  const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
8621
8847
  /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
@@ -8626,7 +8852,7 @@ export class WebGPURenderer implements RendererApi {
8626
8852
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8627
8853
  `${this.materialDoubleSided ? '|2s' : ''}`;
8628
8854
 
8629
- const pipelines = this.targetPipelines();
8855
+ const pipelines = blend ? this.blendedPipelines() : this.targetPipelines();
8630
8856
  let pipeline = pipelines.peek(key);
8631
8857
  if (pipeline === undefined) {
8632
8858
  const present = this.meshPresent.get(base);
@@ -8653,7 +8879,7 @@ export class WebGPURenderer implements RendererApi {
8653
8879
 
8654
8880
  const material = this.materialSlotForDraw();
8655
8881
  if (material !== null) {
8656
- const command = this.recordDraw(0, this.currentTarget());
8882
+ const command = blend ? this.recordBlended(0) : this.recordDraw(0, this.currentTarget());
8657
8883
  if (command !== null) {
8658
8884
  command.pipeline = pipeline;
8659
8885
  command.bindGroup = this.flatBindGroupFor(false, false, null);
@@ -8697,6 +8923,7 @@ export class WebGPURenderer implements RendererApi {
8697
8923
  if (!toneMapped) {
8698
8924
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
8699
8925
  }
8926
+ if (refracting) this.releaseSeeThrough();
8700
8927
  if (own) this.materials.dirty();
8701
8928
  }
8702
8929
 
@@ -8890,6 +9117,8 @@ export class WebGPURenderer implements RendererApi {
8890
9117
  this.lightViewProj.set(lightViewProj as Float32Array);
8891
9118
  mat4.multiply(this.correctedLightViewProj, SHADOW_CLIP_CORRECTION, lightViewProj);
8892
9119
  this.shadowLayerIsPeel = peel;
9120
+ this.shadowLayerIsDynamic = dynamic;
9121
+ this.glassCasters.clear();
8893
9122
  /*
8894
9123
  * A frame's peel is only as good as the pass that fills it, and the static layer is the one
8895
9124
  * that opens first.
@@ -8997,12 +9226,349 @@ export class WebGPURenderer implements RendererApi {
8997
9226
  }
8998
9227
 
8999
9228
  /** Close the shadow pass and submit it, so the frame can sample what it wrote. */
9229
+ /**
9230
+ * Add the sun's two glass layers, once, carrying every layer it holds across on `encoder`.
9231
+ *
9232
+ * **Copied rather than re-baked**, for the reason `SunShadowArray.growForGlass` gives: the static
9233
+ * map is the consumer's to bake and may be baked once, and the glass that grows the texture
9234
+ * arrives in the very pass that drew it. Every view of the old texture is stale afterwards, so
9235
+ * every group holding one is rebuilt here — the lit pass's, the volumes', the medium's and the
9236
+ * peel's — and the old texture is destroyed only once this encoder is submitted.
9237
+ */
9238
+ private growSunForGlass(encoder: GPUCommandEncoder): void {
9239
+ if (this.sunGlassViews !== null) return;
9240
+ const device = this.surface.device;
9241
+ const size = this.quality.directionalShadowMapSize;
9242
+ const maps = this.sunLayerCount;
9243
+ /* The glass layers at their fixed places after the three maps: see `SUN_GLASS_STATIC_LAYER`. */
9244
+ const layers = SUN_GLASS_MOVING_LAYER + 1;
9245
+ const old = this.sunShadowTexture;
9246
+ const texture = device.createTexture({
9247
+ label: 'shadow.sun',
9248
+ size: [size, size, layers],
9249
+ format: SHADOW_FORMAT,
9250
+ usage: SUN_USAGE,
9251
+ });
9252
+ for (let layer = 0; layer < maps; layer++) {
9253
+ encoder.copyTextureToTexture(
9254
+ { texture: old, origin: { x: 0, y: 0, z: layer } },
9255
+ { texture, origin: { x: 0, y: 0, z: layer } },
9256
+ [size, size, 1],
9257
+ );
9258
+ }
9259
+ this.retiredSunTexture = old;
9260
+ this.sunShadowTexture = texture;
9261
+ this.sunLayerCount = layers;
9262
+ const views = sunViewsOf(texture, maps, layers);
9263
+ this.sunArrayView = views.array;
9264
+ this.sunStaticArrayView = views.staticArray;
9265
+ this.shadowView = views.static;
9266
+ this.dynamicView = views.dynamic;
9267
+ this.peelView = views.peel;
9268
+ this.sunGlassViews = views.glass;
9269
+ /* Both glass layers born at the far plane: nothing is nearer the sun than a pane not drawn. */
9270
+ for (const view of views.glass ?? []) {
9271
+ encoder
9272
+ .beginRenderPass({
9273
+ label: 'shadow.glass.clear',
9274
+ colorAttachments: [],
9275
+ depthStencilAttachment: {
9276
+ view,
9277
+ depthClearValue: 1,
9278
+ depthLoadOp: 'clear',
9279
+ depthStoreOp: 'store',
9280
+ },
9281
+ })
9282
+ .end();
9283
+ }
9284
+ this.rebuildSunGroups();
9285
+ }
9286
+
9287
+ /** Every group that holds a view of the sun's texture, rebuilt after it has been replaced. */
9288
+ private rebuildSunGroups(): void {
9289
+ const device = this.surface.device;
9290
+ const peelUniforms = this.shadowPeelPassUniforms;
9291
+ const peelSampler = this.shadowPeelSampler;
9292
+ if (peelUniforms !== null && peelSampler !== null) {
9293
+ this.shadowPeelBindGroup = createDepthBindGroup(
9294
+ device,
9295
+ this.shadowLayout,
9296
+ this.shadowDraws.buffer,
9297
+ peelUniforms,
9298
+ this.sunStaticArrayView,
9299
+ peelSampler,
9300
+ );
9301
+ this.scatterDepthPeelBindGroup = createScatterDepthBindGroup(
9302
+ device,
9303
+ this.scatterDepthLayout,
9304
+ this.scatterDepthDraws.buffer,
9305
+ { binding: DEPTH_FRAG_BINDING, buffer: peelUniforms, size: DEPTH_FRAG_SIZE },
9306
+ DEPTH_PREVIOUS_BINDING,
9307
+ this.sunStaticArrayView,
9308
+ peelSampler,
9309
+ );
9310
+ }
9311
+ this.cutoutPeelGroups.clear();
9312
+ this.rebuildFlatGroupsForNewRings();
9313
+ this.lightVolumeBindGroup = this.buildLightVolumeBindGroup();
9314
+ this.mediumBindGroup = this.buildMediumBindGroup();
9315
+ }
9316
+
9317
+ /** The sun's tint target, made the first time glass casts and born clear on `encoder`. */
9318
+ private ensureSunTint(encoder: GPUCommandEncoder): SunTintTarget {
9319
+ if (this.sunTint !== null) return this.sunTint;
9320
+ const size = this.quality.directionalShadowMapSize;
9321
+ const edge = this.quality.glassShadows === 'half' ? Math.max(1, size >> 1) : size;
9322
+ const device = this.surface.device;
9323
+ const texture = device.createTexture({
9324
+ label: 'shadow.sunTint',
9325
+ size: [edge, edge, 2],
9326
+ format: 'rgba8unorm',
9327
+ mipLevelCount: Math.floor(Math.log2(edge)) + 1,
9328
+ usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
9329
+ });
9330
+ const layer = (index: number): GPUTextureView =>
9331
+ texture.createView({
9332
+ dimension: '2d',
9333
+ baseArrayLayer: index,
9334
+ arrayLayerCount: 1,
9335
+ baseMipLevel: 0,
9336
+ mipLevelCount: 1,
9337
+ });
9338
+ const mips = (index: number): SnapshotMips =>
9339
+ new SnapshotMips(device, this.pipelines, texture, 'rgba8unorm', index, 'shadow.tint.mips');
9340
+ const target: SunTintTarget = {
9341
+ texture,
9342
+ layers: [layer(0), layer(1)],
9343
+ array: texture.createView({ dimension: '2d-array' }),
9344
+ mips: [mips(0), mips(1)],
9345
+ };
9346
+ for (const view of target.layers) {
9347
+ encoder
9348
+ .beginRenderPass({
9349
+ label: 'shadow.tint.clear',
9350
+ colorAttachments: [{ view, loadOp: 'clear', clearValue: [1, 1, 1, 1], storeOp: 'store' }],
9351
+ })
9352
+ .end();
9353
+ }
9354
+ this.sunTint = target;
9355
+ this.rebuildFlatGroupsForNewRings();
9356
+ return target;
9357
+ }
9358
+
9359
+ /**
9360
+ * Draw the pass's glass twice: where the nearest pane is, and what the panes let through.
9361
+ *
9362
+ * After the opaque casters, on the pass's own encoder. A layer that has held glass is redrawn
9363
+ * even when this pass offered none, so glass that has gone takes its colour with it.
9364
+ */
9365
+ private flushSunGlass(encoder: GPUCommandEncoder, which: 'static' | 'dynamic'): void {
9366
+ const list = this.glassCasters;
9367
+ if (this.quality.glassShadows === 'off') return;
9368
+ if (list.count === 0 && this.sunGlassViews === null) return;
9369
+ if (list.count > 0) this.growSunForGlass(encoder);
9370
+ const glassViews = this.sunGlassViews;
9371
+ if (glassViews === null) return;
9372
+
9373
+ /* Where the nearest pane is: the depth pipelines, through the sink every opaque caster took. */
9374
+ this.shadowPass = encoder.beginRenderPass({
9375
+ label: 'shadow.glass',
9376
+ colorAttachments: [],
9377
+ depthStencilAttachment: {
9378
+ view: glassViews[which === 'static' ? 0 : 1],
9379
+ depthClearValue: 1,
9380
+ depthLoadOp: 'clear',
9381
+ depthStoreOp: 'store',
9382
+ },
9383
+ });
9384
+ this.drawingGlassDepth = true;
9385
+ list.replay(this.glassDepthSink);
9386
+ this.drawingGlassDepth = false;
9387
+ this.shadowPass.end();
9388
+ this.shadowPass = null;
9389
+
9390
+ /* What the panes let through, every one on a ray, multiplied. */
9391
+ const target = this.ensureSunTint(encoder);
9392
+ const tint = (this.glassTint ??= new GpuGlassTint(this.surface.device));
9393
+ tint.frag.reset();
9394
+ /* The direction the light travels, off the light matrix's depth row; the sign is immaterial,
9395
+ since the tint takes the cosine absolutely, and w 0 says it is a direction. */
9396
+ const m = this.correctedLightViewProj;
9397
+ const length = Math.hypot(m[2] as number, m[6] as number, m[10] as number) || 1;
9398
+ this.glassLight[0] = (m[2] as number) / length;
9399
+ this.glassLight[1] = (m[6] as number) / length;
9400
+ this.glassLight[2] = (m[10] as number) / length;
9401
+ this.glassLight[3] = 0;
9402
+ this.tintPass = encoder.beginRenderPass({
9403
+ label: 'shadow.tint',
9404
+ colorAttachments: [
9405
+ {
9406
+ view: target.layers[which === 'static' ? 0 : 1],
9407
+ loadOp: 'clear',
9408
+ clearValue: [1, 1, 1, 1],
9409
+ storeOp: 'store',
9410
+ },
9411
+ ],
9412
+ });
9413
+ list.replay(this.glassTintSink);
9414
+ this.tintPass.end();
9415
+ this.tintPass = null;
9416
+ tint.frag.flush();
9417
+ /* The chain frost reads, for the layer just drawn. */
9418
+ target.mips[which === 'static' ? 0 : 1].record(encoder);
9419
+ }
9420
+
9421
+ /** The glass list into a glass depth layer: `casterSink` itself, with glass let through. */
9422
+ private readonly glassDepthSink: GlassReplaySink = {
9423
+ mesh: (mesh, model, material) => this.casterSink.mesh(mesh, model, material),
9424
+ instanced: (batch, data, material) => this.casterSink.instanced?.(batch, data, material),
9425
+ skinnedMesh: (mesh, model, palette, material) =>
9426
+ this.casterSink.skinnedMesh(mesh, model, palette, material),
9427
+ };
9428
+
9429
+ /**
9430
+ * The glass list into a tint layer, through the tint pipelines, **culling nothing**: every pane on
9431
+ * a ray counts, whichever way it faces. See the other backend's `glassTintSink`.
9432
+ */
9433
+ private readonly glassTintSink: GlassReplaySink = {
9434
+ mesh: (mesh, model, material, glass) => {
9435
+ const pass = this.tintPass;
9436
+ const tint = this.glassTint;
9437
+ if (pass === null || tint === null) return;
9438
+ const geometry = mesh as GpuMesh & { key?: string };
9439
+ if (geometry.vertexBuffers === undefined) return;
9440
+ const cutout = cutoutOf(material);
9441
+ const albedo = cutout === null ? null : (cutout.albedo as unknown as GpuSurfaceTexture);
9442
+ const slot = this.shadowDraws.allocate();
9443
+ const pane = tint.frag.allocate();
9444
+ if (slot === null || pane === null) return;
9445
+ this.shadowDraws.writeFloats(
9446
+ slot,
9447
+ DEPTH_VERT_FIELDS.uLightViewProj.offset,
9448
+ this.correctedLightViewProj,
9449
+ );
9450
+ this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uModel.offset, model as Float32Array);
9451
+ this.writeShadowWind(slot);
9452
+ if (cutout !== null) this.writeCutout(slot, cutout, DEPTH_CUTOUT_VERT_FIELDS);
9453
+ tint.writePane(pane, glass, this.glassLight);
9454
+ pass.setPipeline(
9455
+ tint.pipeline(
9456
+ this.pipelines,
9457
+ albedo === null ? 'rigid' : 'cutout',
9458
+ geometry.key ?? '',
9459
+ this.presentOf(geometry),
9460
+ 'none',
9461
+ ),
9462
+ );
9463
+ pass.setBindGroup(
9464
+ 0,
9465
+ albedo === null
9466
+ ? tint.group(this.shadowDraws.buffer)
9467
+ : tint.cutoutGroup(this.shadowDraws.buffer, albedo.view, albedo.sampler),
9468
+ [slot, pane],
9469
+ );
9470
+ for (let index = 0; index < geometry.vertexBuffers.length; index++) {
9471
+ pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
9472
+ }
9473
+ pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
9474
+ pass.drawIndexed(geometry.indexCount);
9475
+ },
9476
+ instanced: (batch, data, material, glass) => {
9477
+ const pass = this.tintPass;
9478
+ const tint = this.glassTint;
9479
+ if (pass === null || tint === null) return;
9480
+ const gpuBatch = batch as GpuInstancedBatch;
9481
+ const geometry = gpuBatch.mesh as GpuMesh & { key?: string };
9482
+ if (geometry.vertexBuffers === undefined) return;
9483
+ const count = Math.min(data.count, gpuBatch.capacity);
9484
+ if (count === 0) return;
9485
+ const cutout = cutoutOf(material);
9486
+ const albedo = cutout === null ? null : (cutout.albedo as unknown as GpuSurfaceTexture);
9487
+ const slot = this.shadowDraws.allocate();
9488
+ const pane = tint.frag.allocate();
9489
+ if (slot === null || pane === null) return;
9490
+ this.shadowDraws.writeFloats(
9491
+ slot,
9492
+ DEPTH_VERT_FIELDS.uLightViewProj.offset,
9493
+ this.correctedLightViewProj,
9494
+ );
9495
+ if (cutout !== null) this.writeCutout(slot, cutout, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS);
9496
+ tint.writePane(pane, glass, this.glassLight);
9497
+ pass.setPipeline(
9498
+ tint.pipeline(
9499
+ this.pipelines,
9500
+ albedo === null ? 'instanced' : 'instancedCutout',
9501
+ geometry.key ?? '',
9502
+ this.presentOf(geometry),
9503
+ 'none',
9504
+ ),
9505
+ );
9506
+ pass.setBindGroup(
9507
+ 0,
9508
+ albedo === null
9509
+ ? tint.group(this.shadowDraws.buffer)
9510
+ : tint.cutoutGroup(this.shadowDraws.buffer, albedo.view, albedo.sampler),
9511
+ [slot, pane],
9512
+ );
9513
+ let index = 0;
9514
+ for (; index < geometry.vertexBuffers.length; index++) {
9515
+ pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
9516
+ }
9517
+ pass.setVertexBuffer(index, gpuBatch.buffer);
9518
+ pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
9519
+ pass.drawIndexed(geometry.indexCount, count);
9520
+ },
9521
+ skinnedMesh: (mesh, model, palette, _material, glass) => {
9522
+ const pass = this.tintPass;
9523
+ const tint = this.glassTint;
9524
+ if (pass === null || tint === null) return;
9525
+ const geometry = mesh as GpuMesh & { key?: string };
9526
+ if (geometry.vertexBuffers === undefined) return;
9527
+ const paletteSlot = this.skinPalettes.take(this.surface.device, palette);
9528
+ const view = paletteSlot === null ? null : this.skinPalettes.view(paletteSlot);
9529
+ if (view === null) return;
9530
+ const slot = this.shadowDraws.allocate();
9531
+ const pane = tint.frag.allocate();
9532
+ if (slot === null || pane === null) return;
9533
+ this.shadowDraws.writeFloats(
9534
+ slot,
9535
+ DEPTH_VERT_FIELDS.uLightViewProj.offset,
9536
+ this.correctedLightViewProj,
9537
+ );
9538
+ this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uModel.offset, model as Float32Array);
9539
+ this.writeShadowWind(slot);
9540
+ tint.writePane(pane, glass, this.glassLight);
9541
+ pass.setPipeline(
9542
+ tint.pipeline(
9543
+ this.pipelines,
9544
+ 'skinned',
9545
+ geometry.key ?? '',
9546
+ this.presentOf(geometry),
9547
+ 'none',
9548
+ ),
9549
+ );
9550
+ pass.setBindGroup(0, tint.skinnedGroup(this.shadowDraws.buffer, view), [slot, pane]);
9551
+ for (let index = 0; index < geometry.vertexBuffers.length; index++) {
9552
+ pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
9553
+ }
9554
+ pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
9555
+ pass.drawIndexed(geometry.indexCount);
9556
+ },
9557
+ };
9558
+
9000
9559
  endShadowPass(): void {
9001
9560
  if (this.surface.lost || this.shadowPass === null || this.shadowEncoder === null) return;
9561
+ this.shadowPass.end();
9562
+ /* The peel keeps no glass: its layer is the second opaque occluder and nothing else. */
9563
+ if (!this.shadowLayerIsPeel) {
9564
+ this.flushSunGlass(this.shadowEncoder, this.shadowLayerIsDynamic ? 'dynamic' : 'static');
9565
+ }
9002
9566
  this.shadowDraws.flush();
9003
9567
  this.scatterDepthDraws.flush();
9004
- this.shadowPass.end();
9005
9568
  this.surface.device.queue.submit([this.shadowEncoder.finish()]);
9569
+ /* A growth's old texture, copied out of by what was just submitted and needed no longer. */
9570
+ this.retiredSunTexture?.destroy();
9571
+ this.retiredSunTexture = null;
9006
9572
  /* The map now holds a rendered peel rather than whatever it was created with. */
9007
9573
  if (this.shadowLayerIsPeel) this.peelFilled = true;
9008
9574
  this.shadowLayerIsPeel = false;
@@ -9032,6 +9598,8 @@ export class WebGPURenderer implements RendererApi {
9032
9598
  private preparedPointLightCount = 0;
9033
9599
  /** See `renderer.ts`: built when a world reports its lights, and rebuilt only to grow. */
9034
9600
  private pointShadowArray: GpuPointShadowArray | null = null;
9601
+ /** The array glass replaced, destroyed once the round that copied out of it is submitted. */
9602
+ private retiredPointTexture: GPUTexture | null = null;
9035
9603
  private readonly bakeScratch = createBakeScratch(POINT_SHADOW_POOL);
9036
9604
  /** Said once per renderer; see `updatePointShadows`. */
9037
9605
  private warnedNoPointShadowArray = false;
@@ -9217,6 +9785,7 @@ export class WebGPURenderer implements RendererApi {
9217
9785
  */
9218
9786
  this.shadowDraws.reset();
9219
9787
  this.scatterDepthDraws.reset();
9788
+ this.glassTint?.frag.reset();
9220
9789
  this.shadowEncoder = this.surface.device.createCommandEncoder({ label: 'pointShadow' });
9221
9790
  /* A cube face is a shadow pass like any other, and the courtyard is where it matters: six of
9222
9791
  them a frame is the difference this slot exists to make visible. */
@@ -9244,7 +9813,7 @@ export class WebGPURenderer implements RendererApi {
9244
9813
  light.z,
9245
9814
  light.radius,
9246
9815
  (view, viewProj) => this.bakeFace(view, viewProj, casters as ShadowCasters),
9247
- (layer, face, far, near) => this.resolveFace(layer, face, far, near),
9816
+ (layer, face, far, near, x, y, z) => this.resolveFace(layer, face, far, near, x, y, z),
9248
9817
  this.faceViewProj,
9249
9818
  light.shadowNear,
9250
9819
  light.sourceRadius,
@@ -9258,10 +9827,13 @@ export class WebGPURenderer implements RendererApi {
9258
9827
  * encoder, a second submit and a second `shadows` timer slot for work that is the same work.
9259
9828
  */
9260
9829
  this.updateAreaShadows(areaLights, frameDt, staticCasters, dynamicCasters);
9830
+ /* The chain frost reads, once for the round, for the lights whose glass it drew or cleared. */
9831
+ this.pointShadowArray?.glassTint?.recordMips(this.shadowEncoder);
9261
9832
  } finally {
9262
9833
  this.depthCullMode = 'back';
9263
9834
  this.shadowDraws.flush();
9264
9835
  this.scatterDepthDraws.flush();
9836
+ this.glassTint?.frag.flush();
9265
9837
  /*
9266
9838
  * **Before the submit, and that ordering is the whole point of the ring.** A write to the
9267
9839
  * queue lands ahead of the encoder's commands whenever it is issued, so uploading per
@@ -9273,6 +9845,9 @@ export class WebGPURenderer implements RendererApi {
9273
9845
  this.shadowEncoder = null;
9274
9846
  this.shadowPass = null;
9275
9847
  this.gpuTimer.end();
9848
+ /* An interleaving's old array, copied out of by what was just submitted. */
9849
+ this.retiredPointTexture?.destroy();
9850
+ this.retiredPointTexture = null;
9276
9851
  }
9277
9852
 
9278
9853
  /*
@@ -9283,6 +9858,10 @@ export class WebGPURenderer implements RendererApi {
9283
9858
  * written every frame regardless.
9284
9859
  */
9285
9860
  pool.resolve(this.resolvedPointShadows);
9861
+ /* As the array stores them: 2L once glass has interleaved it. See `strideLayers`. */
9862
+ const stride = this.pointShadowArray.stride;
9863
+ strideLayers(this.resolvedPointShadows.layers, stride);
9864
+ strideLayers(this.resolvedPointShadows.liveLayers, stride);
9286
9865
  }
9287
9866
 
9288
9867
  /**
@@ -9327,7 +9906,8 @@ export class WebGPURenderer implements RendererApi {
9327
9906
  light.z,
9328
9907
  range,
9329
9908
  (view, viewProj) => this.bakeFace(view, viewProj, casters as ShadowCasters),
9330
- (layer, face, far, mapNear) => this.resolveFace(layer, face, far, mapNear),
9909
+ (layer, face, far, mapNear, x, y, z) =>
9910
+ this.resolveFace(layer, face, far, mapNear, x, y, z),
9331
9911
  this.faceViewProj,
9332
9912
  near,
9333
9913
  /* The filter reads the emitter's size, never the bake. See `renderer.ts`'s copy. */
@@ -9336,6 +9916,9 @@ export class WebGPURenderer implements RendererApi {
9336
9916
  ),
9337
9917
  );
9338
9918
  set.resolve(areaLights, this.resolvedAreaShadows);
9919
+ const stride = this.pointShadowArray?.stride ?? 1;
9920
+ strideLayers(this.resolvedAreaShadows.layer, stride);
9921
+ strideLayers(this.resolvedAreaShadows.liveLayer, stride);
9339
9922
  }
9340
9923
 
9341
9924
  /**
@@ -9363,9 +9946,27 @@ export class WebGPURenderer implements RendererApi {
9363
9946
  depthStoreOp: 'store',
9364
9947
  },
9365
9948
  });
9949
+ /* A face is an enumeration of its own: its glass is its own. */
9950
+ this.glassCasters.clear();
9366
9951
  casters(this.casterSink);
9367
9952
  this.shadowPass.end();
9368
9953
  this.shadowPass = null;
9954
+ /*
9955
+ * The first face to offer glass interleaves the array, before its opaque depth is resolved:
9956
+ * `PointShadowArray.growForGlass`. The lit pass's groups hold the old array's view, so they
9957
+ * are rebuilt, and the old texture goes once this round's encoder — which copies out of it —
9958
+ * is submitted.
9959
+ */
9960
+ const array = this.pointShadowArray;
9961
+ if (array !== null && this.glassCasters.count > 0 && this.quality.glassShadows !== 'off') {
9962
+ const tintEdge =
9963
+ this.quality.glassShadows === 'half' ? OCTAHEDRAL_EDGE >> 1 : OCTAHEDRAL_EDGE;
9964
+ const old = array.growForGlass(encoder, tintEdge, this.pipelines);
9965
+ if (old !== null) {
9966
+ this.retiredPointTexture = old;
9967
+ this.rebuildFlatBindGroup();
9968
+ }
9969
+ }
9369
9970
  }
9370
9971
 
9371
9972
  /**
@@ -9375,11 +9976,70 @@ export class WebGPURenderer implements RendererApi {
9375
9976
  * serve every light: the face is consumed before the next one is drawn. See
9376
9977
  * `pointShadowArray.ts` for why the regions are disjoint.
9377
9978
  */
9378
- private resolveFace(layer: number, face: number, far: number, near: number): void {
9979
+ private resolveFace(
9980
+ layer: number,
9981
+ face: number,
9982
+ far: number,
9983
+ near: number,
9984
+ x: number,
9985
+ y: number,
9986
+ z: number,
9987
+ ): void {
9379
9988
  const encoder = this.shadowEncoder;
9380
9989
  const array = this.pointShadowArray;
9381
9990
  if (encoder === null || array === null) return;
9382
9991
  array.resolve(encoder, layer, face, far, near);
9992
+ const tintTarget = array.glassTint;
9993
+ if (tintTarget === null || this.quality.glassShadows === 'off') return;
9994
+ /*
9995
+ * The face's glass, twice, as `PointShadowMap.bakeGlassFace` draws it on the other backend. A
9996
+ * face that offers none still clears a region a previous owner's glass is in, from scratches
9997
+ * cleared and not drawn into, because a pool slot changes hands with its layer.
9998
+ */
9999
+ const offered = this.glassCasters.count > 0;
10000
+ if (!offered && !tintTarget.held(layer, face)) return;
10001
+ this.shadowPass = encoder.beginRenderPass({
10002
+ label: 'pointShadow.glass',
10003
+ colorAttachments: [],
10004
+ depthStencilAttachment: {
10005
+ view: array.scratchView,
10006
+ depthClearValue: 1,
10007
+ depthLoadOp: 'clear',
10008
+ depthStoreOp: 'store',
10009
+ },
10010
+ });
10011
+ if (offered) {
10012
+ this.drawingGlassDepth = true;
10013
+ this.glassCasters.replay(this.glassDepthSink);
10014
+ this.drawingGlassDepth = false;
10015
+ }
10016
+ this.shadowPass.end();
10017
+ this.shadowPass = null;
10018
+ array.resolveGlass(encoder, layer, face, far, near);
10019
+
10020
+ this.tintPass = encoder.beginRenderPass({
10021
+ label: 'pointShadow.tint',
10022
+ colorAttachments: [
10023
+ {
10024
+ view: tintTarget.scratchView,
10025
+ loadOp: 'clear',
10026
+ clearValue: [1, 1, 1, 1],
10027
+ storeOp: 'store',
10028
+ },
10029
+ ],
10030
+ });
10031
+ if (offered) {
10032
+ this.glassTint ??= new GpuGlassTint(this.surface.device);
10033
+ /* A place, w 1: each pane's angle to the lamp comes from its own position. */
10034
+ this.glassLight[0] = x;
10035
+ this.glassLight[1] = y;
10036
+ this.glassLight[2] = z;
10037
+ this.glassLight[3] = 1;
10038
+ this.glassCasters.replay(this.glassTintSink);
10039
+ }
10040
+ this.tintPass.end();
10041
+ this.tintPass = null;
10042
+ tintTarget.resolve(encoder, layer, face, offered);
9383
10043
  }
9384
10044
 
9385
10045
  private buildFlatBindGroup(): GPUBindGroup {
@@ -9739,8 +10399,9 @@ export class WebGPURenderer implements RendererApi {
9739
10399
  * drawn over a reflective floor rather than under it.
9740
10400
  */
9741
10401
  this.runReflections(encoder);
9742
- /* The translucent set, into its two buffers and back over the scene. */
9743
- this.runOit(encoder);
10402
+ /* The translucent set, into its two buffers and back over the scene — after the upscale instead
10403
+ while reconstructing, where `runLate` draws the rest of the blended frame. */
10404
+ if (!this.reconstructing) this.runOit(encoder);
9744
10405
  /*
9745
10406
  * **The medium, after everything that draws and before anything that composites.**
9746
10407
  *
@@ -9761,6 +10422,8 @@ export class WebGPURenderer implements RendererApi {
9761
10422
  */
9762
10423
  this.runMotion(encoder);
9763
10424
  this.runReconstruction(encoder);
10425
+ this.runLate(encoder);
10426
+ if (this.reconstructing) this.runOit(encoder);
9764
10427
  const aoStrength = this.quality.ambientOcclusion;
9765
10428
  if (aoStrength > 0 && this.frameProjection !== null) this.runOcclusion(encoder);
9766
10429
  const bloomStrength = this.quality.bloom * this.bloomScale;
@@ -9989,19 +10652,23 @@ export class WebGPURenderer implements RendererApi {
9989
10652
  label: 'recon.shown',
9990
10653
  size: [outputWidth, outputHeight],
9991
10654
  format: RECON_HISTORY_FORMAT,
9992
- usage: HISTORY_USAGE,
10655
+ /* An attachment too, because the blended draws land on it after the upscale, and a copy
10656
+ source for the panes that refract it. See `runLate`. */
10657
+ usage: HISTORY_USAGE | 0x10 | 0x1, // RENDER_ATTACHMENT | COPY_SRC
9993
10658
  });
9994
10659
  this.reconShownView = this.reconShown.createView();
9995
10660
  this.reconMotion = device.createTexture({
9996
10661
  label: 'recon.motion',
9997
10662
  size: [renderWidth, renderHeight],
9998
10663
  format: RECON_HISTORY_FORMAT,
9999
- /* An attachment as well, because the pass that will write it is a draw. Nothing writes it
10000
- today and nothing needs to clear it either: WebGPU zero-initialises a new texture, and
10001
- a zero in the fourth channel is what tells the resolve to derive the camera's motion. */
10664
+ /* An attachment as well, because the pass that writes it is a draw. It is cleared every
10665
+ frame by that pass, and a zero in the fourth channel is what tells the resolve to derive
10666
+ the camera's motion. */
10002
10667
  usage: 0x4 | 0x10,
10003
10668
  });
10004
10669
  this.reconMotionView = this.reconMotion.createView();
10670
+ this.motionPass ??= new MotionPass(device);
10671
+ this.latePass ??= new LatePass(device);
10005
10672
  this.reconPreviousDepth = device.createTexture({
10006
10673
  label: 'recon.previousDepth',
10007
10674
  size: [renderWidth, renderHeight],
@@ -10120,222 +10787,184 @@ export class WebGPURenderer implements RendererApi {
10120
10787
  }
10121
10788
 
10122
10789
  /**
10123
- * Remember that this mesh moved, so the motion pass can draw where it was.
10790
+ * Whether a blended draw recorded now lands after the upscale. See `lateRouting.ts`.
10124
10791
  *
10125
- * Silent unless a reconstruction is running: a scene that always states its previous transforms
10126
- * — which is the right thing for a scene to do, the alternative being to know what the renderer
10127
- * is configured as — must cost nothing at all when nobody is going to read them.
10792
+ * Not during the order-independent replay, which owns its pass and issues into it directly.
10128
10793
  */
10129
- private recordMotion(
10130
- mesh: GpuMesh,
10131
- model: ArrayLike<number>,
10132
- previousModel: ArrayLike<number>,
10133
- ): void {
10134
- if (!this.reconstructing) return;
10135
- const at = this.motionCount;
10136
- const wanted = (at + 1) * MOTION_DRAW_FLOATS;
10137
- if (this.motionMatrices.length < wanted) {
10138
- /* Doubling, so a scene settles on one allocation rather than one a draw. */
10139
- const grown = new Float32Array(Math.max(wanted, this.motionMatrices.length * 2, 64));
10140
- grown.set(this.motionMatrices);
10141
- this.motionMatrices = grown;
10142
- }
10143
- this.motionMatrices.set(model as ArrayLike<number> & Iterable<number>, at * MOTION_DRAW_FLOATS);
10144
- this.motionMatrices.set(
10145
- previousModel as ArrayLike<number> & Iterable<number>,
10146
- at * MOTION_DRAW_FLOATS + 16,
10794
+ private drawsLate(): boolean {
10795
+ return (
10796
+ this.latePass !== null &&
10797
+ this.oitEncoder === null &&
10798
+ routesLate(
10799
+ this.reconstructing,
10800
+ this.reflectionPassActive,
10801
+ this.probePassActive,
10802
+ this.framePresented,
10803
+ )
10147
10804
  );
10148
- this.motionMeshes[at] = mesh;
10149
- this.motionCount = at + 1;
10150
10805
  }
10151
10806
 
10152
10807
  /**
10153
- * The pipeline the motion pass draws with, built once.
10154
- *
10155
- * **The position buffer alone**, because that is all the shader reads and a mesh keeps one buffer
10156
- * an attribute — so this binds buffer zero and nothing else, whatever else the mesh carries.
10808
+ * The command a blended draw records into: the late pool while reconstructing, the frame graph
10809
+ * otherwise. **Late draws record whatever `quality.frameGraph` says**, since the late replay is
10810
+ * the only moment they can land; everything else keeps the path it has.
10157
10811
  */
10158
- private buildMotionPipeline(): void {
10159
- if (this.motionLayout !== null) return;
10160
- const { device } = this.surface;
10161
- this.motionLayout = device.createBindGroupLayout({
10162
- label: 'recon.motion.layout',
10163
- entries: [
10164
- { binding: 0, visibility: 0x1, buffer: { type: 'uniform' as const } },
10165
- {
10166
- binding: 1,
10167
- visibility: 0x1,
10168
- buffer: { type: 'uniform' as const, hasDynamicOffset: true },
10169
- },
10170
- ],
10171
- });
10172
- this.motionModule = device.createShaderModule({
10173
- label: 'recon.motion',
10174
- code: RECON_MOTION_WGSL,
10175
- });
10176
- this.motionPipelineLayout = device.createPipelineLayout({
10177
- bindGroupLayouts: [this.motionLayout],
10178
- });
10812
+ private recordBlended(reads: number): DrawCommand | null {
10813
+ const late = this.latePass;
10814
+ if (late !== null && this.drawsLate()) return late.take();
10815
+ return this.recordDraw(reads, this.currentTarget());
10179
10816
  }
10180
10817
 
10181
- /** The pipeline for one vertex stride, built on first sight of it. */
10182
- private motionPipelineFor(stride: number): GPURenderPipeline | null {
10183
- const held = this.motionPipelines.get(stride);
10184
- if (held !== undefined) return held;
10185
- const module = this.motionModule;
10186
- const layout = this.motionPipelineLayout;
10187
- if (module === null || layout === null) return null;
10188
- const { device } = this.surface;
10189
- const pipeline = device.createRenderPipeline({
10190
- label: `recon.motion.${String(stride)}`,
10191
- layout,
10192
- vertex: {
10193
- module,
10194
- entryPoint: 'motionVert',
10195
- /* Positions are the layout's first attribute and are never optional, so they are at offset
10196
- zero of every stride this can be handed. */
10197
- buffers: [
10198
- {
10199
- arrayStride: stride,
10200
- attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }],
10201
- },
10202
- ],
10203
- },
10204
- fragment: { module, entryPoint: 'motionFrag', targets: [{ format: MOTION_TARGET_FORMAT }] },
10205
- /*
10206
- * **The scene's own winding**, because this draws the scene's geometry through the scene's
10207
- * clip transform — the product the renderer hands over already carries the negation that
10208
- * `naga` writes into every generated vertex stage, so the faces come out the way they did
10209
- * and a `cw` front face here would draw the inside of every mesh. §3 row 57.
10210
- */
10211
- primitive: { topology: 'triangle-list', cullMode: 'back' },
10212
- depthStencil: {
10213
- format: DEPTH_FORMAT,
10214
- /*
10215
- * **Tested and not written.** The frame's depth is finished; this pass only wants the
10216
- * pixels the scene actually kept, and `DEPTH_COMPARE_EQUAL` is the comparison that admits
10217
- * exactly them — the same transform on the same positions gives the same depth, which is
10218
- * why the rasterisation matrix is handed over whole rather than rebuilt here.
10219
- *
10220
- * **And the equality is not fragile here**, which was worth measuring rather than
10221
- * assuming: this driver contracts multiply-adds (§3 row 107) and two shader modules can
10222
- * contract a matrix product differently, so an equality across them could have admitted
10223
- * only some of a surface. Run with `greater-equal` instead, the ghost measured the same
10224
- * to the pixel — 5,942 either way — so nothing is being dropped.
10225
- */
10226
- depthWriteEnabled: false,
10227
- depthCompare: DEPTH_COMPARE_EQUAL,
10228
- },
10229
- });
10230
- this.motionPipelines.set(stride, pipeline);
10231
- return pipeline;
10818
+ /** The pipelines a blended draw is built for: the reconstructed picture's while late. */
10819
+ private blendedPipelines(): PipelineCache {
10820
+ return this.drawsLate() ? this.latePipelines : this.targetPipelines();
10232
10821
  }
10233
10822
 
10234
10823
  /**
10235
- * Where every mover was last frame, into the motion target.
10236
- *
10237
- * **After everything that draws and before the resolve reads it.** The frame's depth has to be
10238
- * finished for the equality test to mean anything, and the resolve is what consumes the result.
10824
+ * The blended draws of a reconstructing frame, over the reconstructed picture at the output size
10825
+ * and unjittered, against the render's depth taken up to that size. Before occlusion, bloom and
10826
+ * the grade, so they are graded exactly as they were when they were drawn into the scene.
10239
10827
  */
10240
- private runMotion(encoder: GPUCommandEncoder): void {
10241
- const target = this.reconMotionView;
10828
+ private runLate(encoder: GPUCommandEncoder): void {
10829
+ const late = this.latePass;
10830
+ const shown = this.reconShownView;
10242
10831
  const depth = this.depthView;
10243
- if (!this.reconstructing || target === null || depth === null) return;
10244
- const { device } = this.surface;
10245
- this.buildMotionPipeline();
10246
- const layout = this.motionLayout;
10247
- if (layout === null) return;
10248
-
10249
- /*
10250
- * **Cleared every frame, even when nothing moved.** A texel left from last frame is a flag
10251
- * saying "a draw wrote this", and the resolve would take a motion belonging to a surface that
10252
- * is no longer there. The clear is the pass's own load operation, so an empty queue still runs
10253
- * it — which is why this is not returned from before the pass is opened.
10254
- */
10832
+ if (!this.reconstructing || late === null || shown === null || depth === null) return;
10833
+ const oitSet = this.oitActive && this.translucentQueue.length > 0;
10834
+ if (late.pending === 0 && !oitSet) return;
10835
+ const width = Math.max(1, this.surface.canvas.width);
10836
+ const height = Math.max(1, this.surface.canvas.height);
10837
+ /* The order-independent set tests against this depth too, so it is made for either. */
10838
+ late.upscale(encoder, depth, this.renderWidth, this.renderHeight, width, height);
10839
+ if (this.lateRefractWanted && this.refractSnapshot !== null && this.reconShown !== null) {
10840
+ encoder.copyTextureToTexture(
10841
+ { texture: this.reconShown },
10842
+ { texture: this.refractSnapshot },
10843
+ [this.refractSnapshot.width, this.refractSnapshot.height],
10844
+ );
10845
+ this.refractMips?.record(encoder);
10846
+ this.refractSnapshotTaken = true;
10847
+ }
10848
+ this.lateRefractWanted = false;
10849
+ if (late.pending === 0) return;
10255
10850
  const pass = encoder.beginRenderPass({
10256
- label: 'recon.motion',
10851
+ label: 'recon.late',
10257
10852
  timestampWrites: this.gpuTimer.writesFor(),
10258
- colorAttachments: [
10259
- { view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
10260
- ],
10853
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
10261
10854
  depthStencilAttachment: {
10262
- view: depth,
10855
+ view: late.depthView(width, height),
10263
10856
  depthLoadOp: 'load',
10857
+ /* Kept: the order-independent set replays against it after this pass. */
10264
10858
  depthStoreOp: 'store',
10265
- depthReadOnly: false,
10266
10859
  },
10267
10860
  });
10861
+ late.replay(pass, this.issueBound);
10862
+ pass.end();
10863
+ }
10268
10864
 
10269
- const count = this.motionCount;
10270
- if (count > 0) {
10271
- const bytes = count * MOTION_DRAW_STRIDE;
10272
- if (this.motionDrawCapacity < count) {
10273
- this.motionDrawBuffer?.destroy();
10274
- this.motionDrawCapacity = Math.max(count, this.motionDrawCapacity * 2, 16);
10275
- this.motionDrawBuffer = device.createBuffer({
10276
- label: 'recon.motion.draws',
10277
- size: this.motionDrawCapacity * MOTION_DRAW_STRIDE,
10278
- usage: 0x40 | 0x8, // UNIFORM | COPY_DST
10279
- });
10280
- this.motionDrawStaging = new Uint8Array(this.motionDrawCapacity * MOTION_DRAW_STRIDE);
10281
- this.motionGroup = null;
10282
- }
10283
- this.motionFrameBuffer ??= device.createBuffer({
10284
- label: 'recon.motion.frame',
10285
- size: MOTION_FRAME_FLOATS * 4,
10286
- usage: 0x40 | 0x8,
10287
- });
10288
- this.motionGroup ??= device.createBindGroup({
10289
- label: 'recon.motion.group',
10290
- layout,
10291
- entries: [
10292
- { binding: 0, resource: { buffer: this.motionFrameBuffer } },
10293
- {
10294
- binding: 1,
10295
- resource: { buffer: this.motionDrawBuffer as GPUBuffer, size: MOTION_DRAW_STRIDE },
10296
- },
10297
- ],
10298
- });
10299
-
10300
- /* The scene's clip transform, with the generated stages' y negation folded in. */
10301
- mat4.multiply(this.motionRaster, MOTION_CLIP_FLIP, this.viewProj ?? this.correctedViewProj);
10302
- this.motionFrameStaging.set(this.motionRaster as Float32Array, 0);
10303
- this.motionFrameStaging.set(this.frameRawViewProj as Float32Array, 16);
10304
- this.motionFrameStaging.set(this.reconPreviousRawViewProj as Float32Array, 32);
10305
- device.queue.writeBuffer(this.motionFrameBuffer, 0, this.motionFrameStaging);
10306
-
10307
- const draws = new Float32Array(this.motionDrawStaging.buffer);
10308
- for (let i = 0; i < count; i += 1) {
10309
- draws.set(
10310
- this.motionMatrices.subarray(i * MOTION_DRAW_FLOATS, (i + 1) * MOTION_DRAW_FLOATS),
10311
- (i * MOTION_DRAW_STRIDE) / 4,
10312
- );
10313
- }
10314
- device.queue.writeBuffer(
10315
- this.motionDrawBuffer as GPUBuffer,
10316
- 0,
10317
- this.motionDrawStaging,
10318
- 0,
10319
- bytes,
10320
- );
10865
+ /** Whether this draw lands in the reconstructed picture. See `lateRouting.ts`. */
10866
+ private tracksMotionNow(): boolean {
10867
+ return tracksMotion(
10868
+ this.reconstructing,
10869
+ this.reflectionPassActive,
10870
+ this.probePassActive,
10871
+ this.framePresented,
10872
+ );
10873
+ }
10321
10874
 
10322
- let bound: GPURenderPipeline | null = null;
10323
- for (let i = 0; i < count; i += 1) {
10324
- const mesh = this.motionMeshes[i];
10325
- if (mesh === undefined) continue;
10326
- const pipeline = this.motionPipelineFor(mesh.vertexStride);
10327
- if (pipeline === null) continue;
10328
- if (pipeline !== bound) {
10329
- pass.setPipeline(pipeline);
10330
- bound = pipeline;
10331
- }
10332
- pass.setBindGroup(0, this.motionGroup, [i * MOTION_DRAW_STRIDE]);
10333
- pass.setVertexBuffer(0, mesh.vertexBuffers[0] as GPUBuffer);
10334
- pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
10335
- pass.drawIndexed(mesh.indexCount);
10336
- }
10875
+ /**
10876
+ * What this draw's motion is, recorded for the motion pass.
10877
+ *
10878
+ * A mesh rewritten this frame carries its vertex motion whatever else the draw says — under last
10879
+ * frame's model where a mover or a matrix knows it, under this frame's where nothing does. A
10880
+ * mover otherwise carries a rigid draw's last model or a skinned draw's last pose; a matrix
10881
+ * carries a rigid draw's. A draw that says nothing and moved no vertex takes the camera's motion.
10882
+ */
10883
+ private recordDrawMotion(
10884
+ mesh: GpuMesh,
10885
+ model: ArrayLike<number>,
10886
+ previous: ArrayLike<number> | Mover | null,
10887
+ ): void {
10888
+ const dynamic = mesh.motion;
10889
+ const rows = dynamic?.previous ?? null;
10890
+ const rewritten = rows !== null && dynamic !== null && dynamic.changed === this.moverFrame;
10891
+ if (previous === null) {
10892
+ if (rewritten) this.motionPass?.recordDynamic(mesh, model, model, rows);
10893
+ return;
10337
10894
  }
10338
- pass.end();
10895
+ if (!isMover(previous)) {
10896
+ if (rewritten) this.motionPass?.recordDynamic(mesh, model, previous, rows);
10897
+ else this.motionPass?.recordRigid(mesh, model, previous);
10898
+ return;
10899
+ }
10900
+ /* A rig is only followed where the draw really skins: a palette set, a mesh with joints. */
10901
+ const skinned =
10902
+ !rewritten && this.skinPaletteSlot >= 0 && mesh.isSkinned && mesh.skinOffsets !== null;
10903
+ const verdict = advanceMover(
10904
+ previous,
10905
+ this.moverRenderer,
10906
+ this.moverFrame,
10907
+ model,
10908
+ skinned ? this.skinPaletteData : null,
10909
+ );
10910
+ if (verdict === MOVER_TWICE) {
10911
+ this.warnMoverTwice();
10912
+ return;
10913
+ }
10914
+ if (rewritten) {
10915
+ const was = verdict === MOVER_MOTION ? previous.previousModel : model;
10916
+ this.motionPass?.recordDynamic(mesh, model, was, rows);
10917
+ return;
10918
+ }
10919
+ if (verdict !== MOVER_MOTION) return;
10920
+ if (skinned) this.recordSkinnedMotion(mesh, model, previous);
10921
+ else this.motionPass?.recordRigid(mesh, model, previous.previousModel);
10922
+ }
10923
+
10924
+ /**
10925
+ * A skinned mover's motion: this frame's palette is the draw's own slot, and last frame's takes a
10926
+ * slot of its own, like any palette a draw binds — so it is uploaded once and nothing overwrites
10927
+ * it before the motion pass reads it. See `SkinPaletteRing`.
10928
+ */
10929
+ private recordSkinnedMotion(mesh: GpuMesh, model: ArrayLike<number>, mover: Mover): void {
10930
+ const now = this.skinPalettes.view(this.skinPaletteSlot);
10931
+ const slot = this.skinPalettes.take(this.surface.device, mover.previousPalette);
10932
+ const was = slot === null ? null : this.skinPalettes.view(slot);
10933
+ if (now === null || was === null) return;
10934
+ this.motionPass?.recordSkinned(mesh, model, mover.previousModel, now, was);
10935
+ }
10936
+
10937
+ private warnMoverTwice(): void {
10938
+ if (this.warnedMoverTwice) return;
10939
+ this.warnedMoverTwice = true;
10940
+ console.warn(
10941
+ 'WebGPU: a Mover was drawn twice in one frame. A mover is one object; the second draw ' +
10942
+ 'records no motion. Give each moving object its own `createMover()`.',
10943
+ );
10944
+ }
10945
+
10946
+ /**
10947
+ * Where every mover was last frame, into the motion target. See `MotionPass`.
10948
+ *
10949
+ * **After everything that draws and before the resolve reads it.** The frame's depth has to be
10950
+ * finished for the pass's equality test to mean anything, and the resolve is what consumes it.
10951
+ */
10952
+ private runMotion(encoder: GPUCommandEncoder): void {
10953
+ const target = this.reconMotionView;
10954
+ const depth = this.depthView;
10955
+ const motion = this.motionPass;
10956
+ if (!this.reconstructing || target === null || depth === null || motion === null) return;
10957
+ /* The scene's clip transform, with the generated stages' y negation folded in. */
10958
+ mat4.multiply(this.motionRaster, MOTION_CLIP_FLIP, this.viewProj ?? this.correctedViewProj);
10959
+ motion.run(
10960
+ encoder,
10961
+ target,
10962
+ depth,
10963
+ this.motionRaster as Float32Array,
10964
+ this.frameRawViewProj as Float32Array,
10965
+ this.reconPreviousRawViewProj as Float32Array,
10966
+ this.gpuTimer.writesFor(),
10967
+ );
10339
10968
  }
10340
10969
 
10341
10970
  /**
@@ -10781,9 +11410,15 @@ export class WebGPURenderer implements RendererApi {
10781
11410
  if (!this.oitActive || this.translucentQueue.length === 0) return;
10782
11411
  const accum = this.oitAccumView;
10783
11412
  const reveal = this.oitRevealView;
10784
- const scene = this.sceneColorView;
11413
+ /* After the upscale while reconstructing: over the reconstructed picture, against its depth. */
11414
+ const scene = this.reconstructing ? this.reconShownView : this.sceneColorView;
10785
11415
  const group = this.oitResolveGroup;
10786
- const depth = this.depthView;
11416
+ const depth = this.reconstructing
11417
+ ? (this.latePass?.depthView(
11418
+ Math.max(1, this.surface.canvas.width),
11419
+ Math.max(1, this.surface.canvas.height),
11420
+ ) ?? null)
11421
+ : this.depthView;
10787
11422
  if (accum === null || reveal === null || scene === null || group === null || depth === null) {
10788
11423
  return;
10789
11424
  }
@@ -10797,12 +11432,13 @@ export class WebGPURenderer implements RendererApi {
10797
11432
  */
10798
11433
  if (this.translucentQueue.refracts && !this.refractSnapshotTaken) {
10799
11434
  const snapshot = this.refractSnapshot;
10800
- const source = this.sceneColor;
11435
+ const source = this.reconstructing ? this.reconShown : this.sceneColor;
10801
11436
  if (snapshot !== null && source !== null) {
10802
11437
  encoder.copyTextureToTexture({ texture: source }, { texture: snapshot }, [
10803
11438
  snapshot.width,
10804
11439
  snapshot.height,
10805
11440
  ]);
11441
+ this.refractMips?.record(encoder);
10806
11442
  this.refractSnapshotTaken = true;
10807
11443
  }
10808
11444
  }
@@ -10843,6 +11479,7 @@ export class WebGPURenderer implements RendererApi {
10843
11479
  options.refraction = draw.refraction;
10844
11480
  options.refractTint = draw.refractTint as unknown as Vec3 | undefined;
10845
11481
  options.thicknessM = draw.thicknessM;
11482
+ options.glass = draw.glass ?? undefined;
10846
11483
  this.drawTranslucentMesh(draw.mesh as GpuMesh, draw.model, draw.opacity, options);
10847
11484
  });
10848
11485
  this.oitReplaying = false;
@@ -10873,9 +11510,9 @@ export class WebGPURenderer implements RendererApi {
10873
11510
  this.pipelines,
10874
11511
  device,
10875
11512
  this.oitResolveLayout,
10876
- 'post.oitResolve',
11513
+ this.reconstructing ? 'post.oitResolve.recon' : 'post.oitResolve',
10877
11514
  OIT_RESOLVE_FRAG_WGSL,
10878
- sceneColorFormat(this.quality.hdrScene),
11515
+ this.reconstructing ? RECON_HISTORY_FORMAT : sceneColorFormat(this.quality.hdrScene),
10879
11516
  OIT_COMPOSITE_BLEND,
10880
11517
  ),
10881
11518
  );
@@ -11265,18 +11902,14 @@ export class WebGPURenderer implements RendererApi {
11265
11902
  this.lightVolumeVertices.buffer,
11266
11903
  this.lightVolumeFragments.buffer,
11267
11904
  (name) =>
11268
- name === 'uStaticShadowMap'
11269
- ? { view: this.shadowView, sampler: this.volumeShadowSampler }
11270
- : name === 'uPeeledShadowMap' && this.peelView !== null
11271
- ? { view: this.peelView, sampler: this.volumeShadowSampler }
11272
- : name === 'uDynamicShadowMap' && this.dynamicView !== null
11273
- ? { view: this.dynamicView, sampler: this.volumeShadowSampler }
11274
- : /* The frame's own depth, snapshotted before the first volume of the frame draws.
11905
+ name === 'uSunShadows'
11906
+ ? { view: this.sunArrayView, sampler: this.volumeShadowSampler }
11907
+ : /* The frame's own depth, snapshotted before the first volume of the frame draws.
11275
11908
  Null under a profile with no composite, and then `uSceneDepthEnabled` is 0 and
11276
11909
  the stand-in is never sampled. */
11277
- name === 'uSceneDepth' && this.resolvedDepthView !== null
11278
- ? { view: this.resolvedDepthView, sampler: this.postDepthSampler }
11279
- : this.volumeBlank,
11910
+ name === 'uSceneDepth' && this.resolvedDepthView !== null
11911
+ ? { view: this.resolvedDepthView, sampler: this.postDepthSampler }
11912
+ : this.volumeBlank,
11280
11913
  );
11281
11914
  }
11282
11915
 
@@ -11302,6 +11935,12 @@ export class WebGPURenderer implements RendererApi {
11302
11935
  private takeRefractSnapshot(): boolean {
11303
11936
  if (this.reflectionPassActive || this.probePassActive) return false;
11304
11937
  if (this.refractSnapshotTaken) return true;
11938
+ /* A late draw refracts the reconstructed picture, which does not exist until the resolve has
11939
+ run; `runLate` takes the copy then, once, before any late draw is replayed. */
11940
+ if (this.drawsLate()) {
11941
+ this.lateRefractWanted = true;
11942
+ return true;
11943
+ }
11305
11944
  /* The replay of an order-independent set runs after the scene's pass has ended, and `runOit`
11306
11945
  took the copy for it before it began; there is no pass here to end and reopen. */
11307
11946
  if (this.oitReplaying) return false;
@@ -11340,6 +11979,8 @@ export class WebGPURenderer implements RendererApi {
11340
11979
  snapshot.width,
11341
11980
  snapshot.height,
11342
11981
  ]);
11982
+ /* The chain, between the passes, where nothing is attached: what frosted glass reads. */
11983
+ this.refractMips?.record(encoder);
11343
11984
  const multisampled = this.compositeMsaa() !== null;
11344
11985
  this.pass = encoder.beginRenderPass({
11345
11986
  label: 'refract.snapshotTaken',
@@ -11701,8 +12342,13 @@ export class WebGPURenderer implements RendererApi {
11701
12342
  }
11702
12343
  if (verb !== VERB_DRAW) return;
11703
12344
  const command = this.commands.commands[nodeState(this.arena, at)];
11704
- if (command === undefined || command.pipeline === null || command.bindGroup === null) return;
12345
+ if (command === undefined) return;
12346
+ this.issueCommand(pass, command);
12347
+ }
11705
12348
 
12349
+ /** One recorded draw, issued into an open pass. Shared by the graph's replay and `runLate`. */
12350
+ private issueCommand(pass: GPURenderPassEncoder, command: DrawCommand): void {
12351
+ if (command.pipeline === null || command.bindGroup === null) return;
11706
12352
  pass.setPipeline(command.pipeline);
11707
12353
  if (command.offsetCount === 2) {
11708
12354
  pass.setBindGroup(0, command.bindGroup, [command.offsetA, command.offsetB]);
@@ -11946,6 +12592,7 @@ export class WebGPURenderer implements RendererApi {
11946
12592
  }
11947
12593
 
11948
12594
  endFrame(): void {
12595
+ this.inFrame = false;
11949
12596
  if (this.surface.lost || this.encoder === null) return;
11950
12597
  /*
11951
12598
  * Anything still recorded goes now.
@@ -13212,6 +13859,10 @@ export class WebGPURenderer implements RendererApi {
13212
13859
  *
13213
13860
  * What it gives up is one frame of history: the frame after a cut has no blur and no temporal
13214
13861
  * blend, as the first frame of a session has none. Nothing else is reset.
13862
+ *
13863
+ * **A reconstructing renderer depends on it**: its history is the last shot's, and without this
13864
+ * call a cut blends that shot into the first frames of the next. Nothing detects a cut on the
13865
+ * caller's behalf, deliberately — a fast pan and a cut look alike from here.
13215
13866
  */
13216
13867
  cameraCut(): void {
13217
13868
  this.hasPreviousView = false;
@@ -13421,9 +14072,13 @@ export class WebGPURenderer implements RendererApi {
13421
14072
  this.causticsVerts.writeBlock(vertexSlot, this.causticsVertBlock);
13422
14073
  this.causticsFrags.writeBlock(fragmentSlot, this.causticsFragInts);
13423
14074
 
13424
- const causticsPipe = causticsPipeline(this.pipelines, this.surface.device, this.causticsLayout);
14075
+ const causticsPipe = causticsPipeline(
14076
+ this.blendedPipelines(),
14077
+ this.surface.device,
14078
+ this.causticsLayout,
14079
+ );
13425
14080
  /* The pattern is generated in the shader from the two uniform blocks; nothing is sampled. */
13426
- const causticsCommand = this.recordDraw(0, this.currentTarget());
14081
+ const causticsCommand = this.recordBlended(0);
13427
14082
  if (causticsCommand !== null) {
13428
14083
  causticsCommand.pipeline = causticsPipe;
13429
14084
  causticsCommand.bindGroup = this.causticsBindGroup;
@@ -14418,6 +15073,33 @@ export class WebGPURenderer implements RendererApi {
14418
15073
  * The occlusion and bloom targets are half size. Both are blurs read back at full size, so a
14419
15074
  * full-resolution intermediate buys nothing a bilinear fetch does not already give.
14420
15075
  */
15076
+ /**
15077
+ * The medium's march group, holding the shadow views it reads — built with the composite, and
15078
+ * again when the sun's texture grows its glass layers and every view of the old one is stale.
15079
+ *
15080
+ * **The stand-in has to read as unoccluded and not as absent**, which is `emptyTexture.ts`'s
15081
+ * point and `createLightVolumeBindGroup`'s: `occlusion()` calls any stored depth below the
15082
+ * receiver's a blocker — so a black stand-in would put the whole medium in shadow, which is a
15083
+ * picture rather than an error. `volumeBlank` is the white one-texel texture built for this.
15084
+ */
15085
+ private buildMediumBindGroup(): GPUBindGroup | null {
15086
+ if (this.resolvedDepthView === null) return null;
15087
+ return this.surface.device.createBindGroup({
15088
+ label: 'post.mediumBindGroup',
15089
+ layout: this.mediumLayout,
15090
+ entries: [
15091
+ {
15092
+ binding: MEDIUM_UNIFORMS,
15093
+ resource: { buffer: this.mediumUniforms, size: MEDIUM_FRAG_SIZE },
15094
+ },
15095
+ { binding: MEDIUM_TEXTURES.uDepth.texture, resource: this.resolvedDepthView },
15096
+ { binding: MEDIUM_TEXTURES.uDepth.sampler, resource: this.postDepthSampler },
15097
+ { binding: MEDIUM_TEXTURES.uSunShadows.texture, resource: this.sunArrayView },
15098
+ { binding: MEDIUM_TEXTURES.uSunShadows.sampler, resource: this.volumeShadowSampler },
15099
+ ],
15100
+ });
15101
+ }
15102
+
14421
15103
  private ensureComposite(width: number, height: number): void {
14422
15104
  if (!this.hasComposite) return;
14423
15105
  if (this.sceneColor !== null && this.frameWidth === width && this.frameHeight === height)
@@ -14471,13 +15153,39 @@ export class WebGPURenderer implements RendererApi {
14471
15153
  * the allocation the house rules are about. A frame that never refracts pays the memory and
14472
15154
  * takes no copy.
14473
15155
  */
15156
+ /*
15157
+ * **At the output size and the reconstructed picture's format while reconstructing**, because
15158
+ * every refracting draw is then blended, drawn after the upscale, and reads the reconstructed
15159
+ * picture rather than the render. See `runLate`.
15160
+ */
15161
+ const reconstructed = this.reconstructionWanted;
14474
15162
  this.refractSnapshot = device.createTexture({
14475
15163
  label: 'refract.snapshot',
14476
- size: [width, height],
14477
- format,
14478
- usage: 0x4 | 0x2,
15164
+ size: reconstructed
15165
+ ? [Math.max(1, this.surface.canvas.width), Math.max(1, this.surface.canvas.height)]
15166
+ : [width, height],
15167
+ format: reconstructed ? RECON_HISTORY_FORMAT : format,
15168
+ /*
15169
+ * **Every level, so glass can read a blurrier one**, and a render attachment so `SnapshotMips`
15170
+ * can fill them. Plain refraction reads level 0, as it always has. See `glass.ts`.
15171
+ */
15172
+ mipLevelCount:
15173
+ Math.floor(
15174
+ Math.log2(
15175
+ reconstructed
15176
+ ? Math.max(1, this.surface.canvas.width, this.surface.canvas.height)
15177
+ : Math.max(1, width, height),
15178
+ ),
15179
+ ) + 1,
15180
+ usage: 0x4 | 0x2 | 0x10, // TEXTURE_BINDING | COPY_DST | RENDER_ATTACHMENT
14479
15181
  });
14480
15182
  this.refractSnapshotView = this.refractSnapshot.createView();
15183
+ this.refractMips = new SnapshotMips(
15184
+ device,
15185
+ this.pipelines,
15186
+ this.refractSnapshot,
15187
+ reconstructed ? RECON_HISTORY_FORMAT : format,
15188
+ );
14481
15189
  /*
14482
15190
  * **Every cached flat bind group is stale the moment this view changes**, and the failure is
14483
15191
  * silent: `flatTextures` resolves `uRefractScene` to the one-texel white stand-in when there is
@@ -14619,10 +15327,14 @@ export class WebGPURenderer implements RendererApi {
14619
15327
  * that range — a step of 1/255 in how much scene shows through, below what a viewer can see
14620
15328
  * against a blended surface.
14621
15329
  */
15330
+ /* At the output size while reconstructing, where the set is replayed after the upscale. */
15331
+ const oitSize: [number, number] = this.reconstructionWanted
15332
+ ? [Math.max(1, this.surface.canvas.width), Math.max(1, this.surface.canvas.height)]
15333
+ : [width, height];
14622
15334
  this.oitAccum?.destroy();
14623
15335
  this.oitAccum = device.createTexture({
14624
15336
  label: 'post.oitAccum',
14625
- size: [width, height],
15337
+ size: oitSize,
14626
15338
  format: 'rgba16float',
14627
15339
  usage: USAGE,
14628
15340
  });
@@ -14630,7 +15342,7 @@ export class WebGPURenderer implements RendererApi {
14630
15342
  this.oitReveal?.destroy();
14631
15343
  this.oitReveal = device.createTexture({
14632
15344
  label: 'post.oitReveal',
14633
- size: [width, height],
15345
+ size: oitSize,
14634
15346
  format: 'r8unorm',
14635
15347
  usage: USAGE,
14636
15348
  });
@@ -14735,42 +15447,7 @@ export class WebGPURenderer implements RendererApi {
14735
15447
  ...entry(AO_TEXTURES.uDepth, this.resolvedDepthView, true),
14736
15448
  ],
14737
15449
  });
14738
- /*
14739
- * The medium's two groups, holding the shadow views the march reads.
14740
- *
14741
- * **The stand-in has to read as unoccluded and not as absent**, which is `emptyTexture.ts`'s
14742
- * point and `createLightVolumeBindGroup`'s: the dynamic layer is sampled unconditionally, and
14743
- * `occlusion()` calls any stored depth below the receiver's a blocker — so a black stand-in
14744
- * would put the whole medium in shadow, which is a picture rather than an error. `volumeBlank`
14745
- * is the white one-texel texture already built for exactly this.
14746
- */
14747
- const shadowPair = (
14748
- b: { texture: number; sampler: number },
14749
- view: GPUTextureView | null,
14750
- ): GPUBindGroupEntry[] => [
14751
- { binding: b.texture, resource: view ?? this.volumeBlank.view },
14752
- {
14753
- binding: b.sampler,
14754
- resource: view === null ? this.volumeBlank.sampler : this.volumeShadowSampler,
14755
- },
14756
- ];
14757
- this.mediumBindGroup =
14758
- this.resolvedDepthView === null
14759
- ? null
14760
- : device.createBindGroup({
14761
- label: 'post.mediumBindGroup',
14762
- layout: this.mediumLayout,
14763
- entries: [
14764
- {
14765
- binding: MEDIUM_UNIFORMS,
14766
- resource: { buffer: this.mediumUniforms, size: MEDIUM_FRAG_SIZE },
14767
- },
14768
- ...entry(MEDIUM_TEXTURES.uDepth, this.resolvedDepthView, true),
14769
- ...shadowPair(MEDIUM_TEXTURES.uStaticShadowMap, this.shadowView),
14770
- ...shadowPair(MEDIUM_TEXTURES.uPeeledShadowMap, this.peelView),
14771
- ...shadowPair(MEDIUM_TEXTURES.uDynamicShadowMap, this.dynamicView),
14772
- ],
14773
- });
15450
+ this.mediumBindGroup = this.buildMediumBindGroup();
14774
15451
  this.mediumUpsampleGroup =
14775
15452
  this.resolvedDepthView === null || this.mediumTargetView === null
14776
15453
  ? null
@@ -15002,6 +15679,10 @@ export class WebGPURenderer implements RendererApi {
15002
15679
  * the 2026-08-13 rule asks for — one decision, bound twice.
15003
15680
  */
15004
15681
  dispose(): void {
15682
+ this.motionPass?.dispose();
15683
+ this.motionPass = null;
15684
+ this.latePass?.dispose();
15685
+ this.latePass = null;
15005
15686
  for (const texture of this.retiredTextures) texture.destroy();
15006
15687
  this.retiredTextures.length = 0;
15007
15688
  const passDevice = this.passDevice();
@@ -15018,6 +15699,12 @@ export class WebGPURenderer implements RendererApi {
15018
15699
  this.fieldComposer = null;
15019
15700
  this.driftIndex?.destroy();
15020
15701
  this.driftAtlas?.destroy();
15702
+ this.sunTint?.texture.destroy();
15703
+ this.sunTint = null;
15704
+ this.glassTint?.dispose();
15705
+ this.glassTint = null;
15706
+ this.retiredSunTexture?.destroy();
15707
+ this.retiredSunTexture = null;
15021
15708
 
15022
15709
  this.pass = null;
15023
15710
  this.encoder = null;