@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
@@ -3,13 +3,20 @@ import { probeFaceRange } from '../../reflectionProbe.js';
3
3
  import { TEMPORAL_HISTORY_BLEND, TemporalHistory, jitterOffset, jitterProjection, } from '../../temporalAa.js';
4
4
  import { OIT_MULTISAMPLE_REFUSAL } from '../../orderIndependent.js';
5
5
  import { TranslucentQueue } from '../../translucentQueue.js';
6
- import { DEPTH_CLEAR, DEPTH_COMPARE_EQUAL, MAX_DEPTH_LAYER, REVERSED_DEPTH, } from '../../depthConvention.js';
6
+ import { DEPTH_CLEAR, MAX_DEPTH_LAYER, REVERSED_DEPTH } from '../../depthConvention.js';
7
7
  import { RECONSTRUCTION_MULTISAMPLE_REFUSAL, reconRenderSize } from '../../recon/frameSizes.js';
8
8
  import { jitterOffset as jitterOffsetFor, reconJitterPhases } from '../../recon/jitter.js';
9
9
  import { RECON_HISTORY_FORMAT, RECON_PARAM_FLOATS, RECON_WORKGROUP, reconResolveWgsl, } from '../../shaders/recon/resolve.wgsl.js';
10
- import { MOTION_DRAW_FLOATS, MOTION_DRAW_STRIDE, MOTION_FRAME_FLOATS, MOTION_TARGET_FORMAT, RECON_MOTION_WGSL, } from '../../shaders/recon/motion.wgsl.js';
10
+ import { MotionPass } from './motionPass.js';
11
+ import { LatePass } from './latePass.js';
12
+ import { SnapshotMips } from './snapshotMips.js';
13
+ import { resolveGlass } from '../../glass.js';
14
+ import { routesLate, tracksMotion } from '../../recon/lateRouting.js';
15
+ import { frameOfChange, keepsPrevious, pairedInstances } from '../../recon/changeFrames.js';
16
+ import { INSTANCE_STRIDE } from '../../instances.js';
11
17
  import { DEFAULT_DISOCCLUSION } from '../../recon/disocclusion.js';
12
18
  import { DEFAULT_RECON_QUALITY } from '../../recon/resolve.js';
19
+ import { MOVER_MOTION, MOVER_TWICE, advanceMover, claimRendererId, isMover, } from '../../recon/mover.js';
13
20
  import { clamp } from '../../../math/scalar.js';
14
21
  import { Camera } from '../../camera.js';
15
22
  import { PickableSet } from '../../pickable.js';
@@ -38,6 +45,7 @@ import { areaShadowLayerCount, castingRange, createResolvedAreaShadows, firstAre
38
45
  import { createBakeScratch, runPointShadowBakes, selectCastingLights, } from '../../pointShadowBudget.js';
39
46
  import { POINT_SHADOW_SAMPLER, PointShadowSystem, } from '../../pointShadowSystem.js';
40
47
  import { GpuPointShadowArray, GpuPointShadowMap } from './pointShadowPass.js';
48
+ import { OCTAHEDRAL_EDGE, strideLayers } from '../../pointShadowArray.js';
41
49
  import { createPanelBindGroup, createPanelBindGroupLayout, createPanelCorners, panelPipeline, PANEL_FRAG_FIELDS, PANEL_FRAG_SIZE, PANEL_VERTEX_COUNT, PANEL_VERT_FIELDS, PANEL_VERT_SIZE, } from './panelPass.js';
42
50
  import { createGpuTextCube, createTextBindGroup, createTextBindGroupLayout, GpuText, textPipeline, TEXT_FRAG_FIELDS, TEXT_FRAG_SIZE, TEXT_VERT_FIELDS, TEXT_VERT_SIZE, } from './textPass.js';
43
51
  import { deviceSnappedCellSize, deviceSnappedOrigin } from '../../textLayout.js';
@@ -66,6 +74,10 @@ import { createScatterDeform, resolveScatterDeform, } from '../../scatterDeform.
66
74
  import { resolvePointLights } from '../../lightBudget.js';
67
75
  import { resolveRenderQuality } from '../../renderQuality.js';
68
76
  import { DEFAULT_GLOBAL_MEDIUM, mediumActive, mediumTargetSize, WEAK_GPU_MEDIUM_STEPS, resolveGlobalMedium, } from '../../globalMedium.js';
77
+ import { castsDepth, glassOf } from '../../shadowCasters.js';
78
+ import { GlassCasterList } from '../../glassCasters.js';
79
+ import { GpuGlassTint } from './glassTintPass.js';
80
+ import { SUN_DYNAMIC_LAYER, SUN_GLASS_MOVING_LAYER, SUN_GLASS_STATIC_LAYER, SUN_PEELED_LAYER, SUN_STATIC_LAYER, } from '../../shadowMap.js';
69
81
  import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental } from './buffers.js';
70
82
  import { SkinPaletteRing } from './skinPaletteRing.js';
71
83
  import { DEPTH_FORMAT, FLAT_VERT_FIELDS, FLAT_VERT_SIZE, createFlatBindGroup, createFlatBindGroupLayout, flatFragmentBindings, flatVariant, flatVertexBindings, flatPipeline, flatPipelineAsync, } from './flatPass.js';
@@ -762,6 +774,24 @@ function padded(direction) {
762
774
  SCRATCH_VEC4[3] = 0;
763
775
  return SCRATCH_VEC4;
764
776
  }
777
+ /** The sun texture's usage: attached, sampled, and copied both ways when glass grows it. */
778
+ const SUN_USAGE = 0x10 | 0x4 | 0x1 | 0x2; // RENDER_ATTACHMENT | TEXTURE_BINDING | COPY_SRC | COPY_DST
779
+ /** Every view of the sun's texture a pass reads or attaches, for `maps` of its layers. */
780
+ function sunViewsOf(texture, maps, layers) {
781
+ const layer = (index) => texture.createView({ dimension: '2d', baseArrayLayer: index, arrayLayerCount: 1 });
782
+ return {
783
+ array: texture.createView({ dimension: '2d-array' }),
784
+ staticArray: texture.createView({
785
+ dimension: '2d-array',
786
+ baseArrayLayer: SUN_STATIC_LAYER,
787
+ arrayLayerCount: 1,
788
+ }),
789
+ static: layer(SUN_STATIC_LAYER),
790
+ dynamic: maps > SUN_DYNAMIC_LAYER ? layer(SUN_DYNAMIC_LAYER) : null,
791
+ peel: maps > SUN_PEELED_LAYER ? layer(SUN_PEELED_LAYER) : null,
792
+ glass: layers > maps ? [layer(SUN_GLASS_STATIC_LAYER), layer(SUN_GLASS_MOVING_LAYER)] : null,
793
+ };
794
+ }
765
795
  export class WebGPURenderer {
766
796
  /**
767
797
  * Real timestamp queries, where the adapter offers them.
@@ -863,6 +893,8 @@ export class WebGPURenderer {
863
893
  * caller who passed null gets — rather than reading a slot this frame has already re-let.
864
894
  */
865
895
  skinPaletteSlot = -1;
896
+ /** The palette `skinPaletteSlot` was uploaded from, for a skinned mover to copy. See `Mover`. */
897
+ skinPaletteData = null;
866
898
  surface;
867
899
  /** The encoder for the frame in flight, or null between frames. */
868
900
  encoder = null;
@@ -870,6 +902,27 @@ export class WebGPURenderer {
870
902
  meshPresent = new Map();
871
903
  pass = null;
872
904
  pipelines;
905
+ /**
906
+ * The pipelines a reconstructing frame's blended draws are built for: the reconstructed picture's
907
+ * format at one sample, which is where `LatePass` draws them. Aliased to `pipelines` when the
908
+ * world already lands on exactly that, as `overlayPipelines` is.
909
+ */
910
+ latePipelines;
911
+ /** The blended draws of a reconstructing frame, drawn after the upscale. See `LatePass`. */
912
+ latePass = null;
913
+ /** A late draw asked to refract: `runLate` copies the reconstructed picture before replaying. */
914
+ lateRefractWanted = false;
915
+ /** The refraction copy's mip chain, recorded after each copy: what frosted glass reads. */
916
+ refractMips = null;
917
+ /**
918
+ * The refraction copy's sampler: filtering, and **between levels as well**, so a frosted pane's
919
+ * level falls smoothly between two blurs rather than stepping from one to the next.
920
+ */
921
+ refractSampler = null;
922
+ /** One glass resolution reused by every draw, so a pane allocates nothing. See `glass.ts`. */
923
+ glassScratch = { transmission: 0, frost: 0, tint: [1, 1, 1] };
924
+ /** `issueCommand` bound once, so the late replay allocates no closure per frame. */
925
+ issueBound;
873
926
  /**
874
927
  * The same pipelines again, built for the canvas, for anything drawn after `endFrame`.
875
928
  *
@@ -1393,10 +1446,10 @@ export class WebGPURenderer {
1393
1446
  * the *unsharpened* result — sharpening into the history would sharpen a sharpened picture every
1394
1447
  * frame, which is the halo the reference's own clamp exists to prevent, compounded.
1395
1448
  *
1396
- * The motion target is render size and carries a drawn object's own motion where something wrote
1397
- * one. **Nothing writes it yet**, so it is cleared every frame and every pixel takes the camera's
1398
- * motion, which the resolve derives from the depth and the reprojection. Static geometry is
1399
- * exact that way and a moving object ghosts until the motion pass lands.
1449
+ * The motion target is render size and carries a drawn object's own motion where the motion pass
1450
+ * wrote one — a draw that named its mover, or passed last frame's matrix. Every other texel is
1451
+ * cleared, and the resolve takes the camera's motion there, derived from the depth and the
1452
+ * reprojection: exact for anything that did not move. See `MotionPass`.
1400
1453
  */
1401
1454
  reconHistories = [null, null];
1402
1455
  reconHistoryViews = [null, null];
@@ -1418,34 +1471,15 @@ export class WebGPURenderer {
1418
1471
  reconGroups = [null, null];
1419
1472
  reconSharpenGroups = [null, null];
1420
1473
  reconRushGroup = null;
1421
- /**
1422
- * What the motion pass will draw, recorded by `drawMesh` and spent once a frame.
1423
- *
1424
- * A parallel array of meshes and one flat matrix buffer rather than an array of objects, because
1425
- * this is written on the frame's hot path and the house rule about allocating there binds it: the
1426
- * matrices are copied into a buffer that only ever grows, and the count is what resets.
1427
- */
1428
- motionMeshes = [];
1429
- motionMatrices = new Float32Array(0);
1430
- motionCount = 0;
1431
- motionLayout = null;
1432
- /**
1433
- * One pipeline per vertex stride, because a stride is a property of the *mesh*.
1434
- *
1435
- * Which optional attributes a mesh supplied decides how far apart its positions are, and this
1436
- * pass reads nothing but positions — so two meshes drawn by the same shader still need two
1437
- * vertex layouts. A map rather than a cache key on the pipeline cache: there is one shader here
1438
- * and a handful of strides in any scene.
1439
- */
1440
- motionPipelines = new Map();
1441
- motionModule = null;
1442
- motionPipelineLayout = null;
1443
- motionFrameBuffer = null;
1444
- motionDrawBuffer = null;
1445
- motionDrawCapacity = 0;
1446
- motionGroup = null;
1447
- motionFrameStaging = new Float32Array(MOTION_FRAME_FLOATS);
1448
- motionDrawStaging = new Uint8Array(0);
1474
+ /** The motion pass, made with the reconstruction's targets. See `MotionPass`. */
1475
+ motionPass = null;
1476
+ /** This renderer's identity to a `Mover`. See `claimRendererId`. */
1477
+ moverRenderer = claimRendererId();
1478
+ /** Frames begun, so a mover can tell last frame from any other. Advanced in `beginFrame`. */
1479
+ moverFrame = 0;
1480
+ /** Between `beginFrame` and `endFrame`. A change outside it is the next frame's. */
1481
+ inFrame = false;
1482
+ warnedMoverTwice = false;
1449
1483
  /** The scene's own clip transform with the generated shaders' y negation folded in. */
1450
1484
  motionRaster = mat4.create();
1451
1485
  reconSizeKey = '';
@@ -1692,6 +1726,7 @@ export class WebGPURenderer {
1692
1726
  refraction: 0,
1693
1727
  refractTint: undefined,
1694
1728
  thicknessM: 0,
1729
+ glass: undefined,
1695
1730
  };
1696
1731
  reflectionPassActive = false;
1697
1732
  /** Said once: a mirror asked for outside a frame is a call-order fault, not a device limit. */
@@ -1968,7 +2003,7 @@ export class WebGPURenderer {
1968
2003
  f[atF('uFogNear')] = medium.fogNear;
1969
2004
  f[atF('uFogFar')] = medium.fogFar;
1970
2005
  this.filmFrags.writeBlock(fragmentSlot, this.filmFragInts);
1971
- const filmPipe = filmPipeline(this.pipelines, this.surface.device, this.filmLayout, `film|${geometry.key ?? ''}`, this.presentOf(geometry));
2006
+ const filmPipe = filmPipeline(this.blendedPipelines(), this.surface.device, this.filmLayout, `film|${geometry.key ?? ''}`, this.presentOf(geometry));
1972
2007
  /* The mirror is an attachment right now, so the group that does not hold it. */
1973
2008
  const filmGroup = this.reflectionPassActive ? this.filmBindGroupBlank : this.filmBindGroup;
1974
2009
  /*
@@ -1977,7 +2012,7 @@ export class WebGPURenderer {
1977
2012
  * holds the mirror's colour whatever `uReflectionEnabled` says, and WebGPU validates the
1978
2013
  * binding rather than the read — the argument `filmBindGroupBlank` exists for.
1979
2014
  */
1980
- const filmCommand = this.recordDraw(filmGroup === this.filmBindGroup ? MIRROR_COLOR : 0, this.currentTarget());
2015
+ const filmCommand = this.recordBlended(filmGroup === this.filmBindGroup ? MIRROR_COLOR : 0);
1981
2016
  if (filmCommand !== null) {
1982
2017
  filmCommand.pipeline = filmPipe;
1983
2018
  filmCommand.bindGroup = filmGroup;
@@ -2159,6 +2194,7 @@ export class WebGPURenderer {
2159
2194
  setSkinPalette(palette) {
2160
2195
  if (this.surface.lost)
2161
2196
  return;
2197
+ this.skinPaletteData = palette;
2162
2198
  if (palette === null) {
2163
2199
  this.skinPaletteSlot = -1;
2164
2200
  return;
@@ -2482,7 +2518,8 @@ export class WebGPURenderer {
2482
2518
  * no-op before the first `bindMeshPass` of a session, same as every other pass that reads it.
2483
2519
  */
2484
2520
  drawSdfText(text, model, color, opacity) {
2485
- const viewProj = this.viewProj;
2521
+ /* Unjittered when it lands after the upscale, like every late draw. See `drawsLate`. */
2522
+ const viewProj = this.drawsLate() ? this.correctedViewProj : this.viewProj;
2486
2523
  if (!this.canDraw() || viewProj === null)
2487
2524
  return;
2488
2525
  const font = text.font;
@@ -2511,9 +2548,9 @@ export class WebGPURenderer {
2511
2548
  ]);
2512
2549
  this.sdfTextFrags.writeInt(fragmentSlot, fragAt('uOutputTransform'), this.gradeCode());
2513
2550
  this.sdfTextFrags.writeFloat(fragmentSlot, fragAt('uOutputExposure'), this.gradeExposure());
2514
- const sdfPipe = sdfTextPipeline(this.targetPipelines(), this.surface.device, this.sdfTextLayout);
2551
+ const sdfPipe = sdfTextPipeline(this.blendedPipelines(), this.surface.device, this.sdfTextLayout);
2515
2552
  const sdfGroup = this.sdfTextBindGroupFor(atlas);
2516
- const sdfCommand = this.recordDraw(0, this.currentTarget());
2553
+ const sdfCommand = this.recordBlended(0);
2517
2554
  if (sdfCommand !== null) {
2518
2555
  sdfCommand.pipeline = sdfPipe;
2519
2556
  sdfCommand.bindGroup = sdfGroup;
@@ -2615,8 +2652,8 @@ export class WebGPURenderer {
2615
2652
  const pass = this.openTextPass();
2616
2653
  if (pass === null)
2617
2654
  return;
2618
- const textPipe = textPipeline(this.targetPipelines(), this.surface.device, this.textLayout);
2619
- const textCommand = this.recordDraw(0, this.currentTarget());
2655
+ const textPipe = textPipeline(this.blendedPipelines(), this.surface.device, this.textLayout);
2656
+ const textCommand = this.recordBlended(0);
2620
2657
  if (textCommand !== null) {
2621
2658
  textCommand.pipeline = textPipe;
2622
2659
  textCommand.bindGroup = this.textBindGroup;
@@ -3035,8 +3072,8 @@ export class WebGPURenderer {
3035
3072
  device.queue.writeBuffer(batch.vertUniforms, 0, batch.vertStaging);
3036
3073
  device.queue.writeBuffer(batch.fragUniforms, 0, batch.fragStaging);
3037
3074
  const layout = this.particleLayoutFor(batch.material).layout;
3038
- const particlePipe = particlePipeline(this.pipelines, device, layout, batch.material, batch.blend);
3039
- const particleCommand = this.recordDraw(0, this.currentTarget());
3075
+ const particlePipe = particlePipeline(this.blendedPipelines(), device, layout, batch.material, batch.blend);
3076
+ const particleCommand = this.recordBlended(0);
3040
3077
  if (particleCommand !== null) {
3041
3078
  particleCommand.pipeline = particlePipe;
3042
3079
  particleCommand.bindGroup = batch.bindGroup;
@@ -3154,8 +3191,8 @@ export class WebGPURenderer {
3154
3191
  * with `PlumeRenderer`, which has set `uTint` on every draw since before this backend.
3155
3192
  */
3156
3193
  frags.writeFloats(fragmentSlot, atF('uTint'), plumes.tint);
3157
- const plumePipe = plumePipeline(this.pipelines, this.surface.device, this.plumeLayout, plumes.material, plumes.blend);
3158
- const plumeCommand = this.recordDraw(0, this.currentTarget());
3194
+ const plumePipe = plumePipeline(this.blendedPipelines(), this.surface.device, this.plumeLayout, plumes.material, plumes.blend);
3195
+ const plumeCommand = this.recordBlended(0);
3159
3196
  if (plumeCommand !== null) {
3160
3197
  plumeCommand.pipeline = plumePipe;
3161
3198
  plumeCommand.bindGroup = this.plumeBindGroup;
@@ -3598,9 +3635,9 @@ export class WebGPURenderer {
3598
3635
  this.windStreakFragFloats.set(tint, (WIND_STREAK_FRAG_FIELDS.uTint.offset ?? 0) / 4);
3599
3636
  this.windStreakVerts.writeBlock(vertexSlot, this.windStreakVertBlock);
3600
3637
  this.windStreakFrags.writeBlock(fragmentSlot, this.windStreakFragBlock);
3601
- const windPipe = windStreakPipeline(this.pipelines, this.surface.device, this.windStreakLayout);
3638
+ const windPipe = windStreakPipeline(this.blendedPipelines(), this.surface.device, this.windStreakLayout);
3602
3639
  /* Two uniform buffers and two vertex buffers. Nothing is sampled, so nothing is read. */
3603
- const windCommand = this.recordDraw(0, this.currentTarget());
3640
+ const windCommand = this.recordBlended(0);
3604
3641
  if (windCommand !== null) {
3605
3642
  windCommand.pipeline = windPipe;
3606
3643
  windCommand.bindGroup = this.windStreakBindGroup;
@@ -3788,8 +3825,8 @@ export class WebGPURenderer {
3788
3825
  f[atF('uFogFar')] = medium.fogFar;
3789
3826
  this.boltVerts.writeBlock(vertexSlot, this.boltVertBlock);
3790
3827
  this.boltFrags.writeBlock(fragmentSlot, this.boltFragInts);
3791
- const boltPipe = boltPipeline(this.pipelines, this.surface.device, this.boltLayout);
3792
- const boltCommand = this.recordDraw(0, this.currentTarget());
3828
+ const boltPipe = boltPipeline(this.blendedPipelines(), this.surface.device, this.boltLayout);
3829
+ const boltCommand = this.recordBlended(0);
3793
3830
  if (boltCommand !== null) {
3794
3831
  boltCommand.pipeline = boltPipe;
3795
3832
  boltCommand.bindGroup = this.boltBindGroup;
@@ -3898,9 +3935,9 @@ export class WebGPURenderer {
3898
3935
  this.lineFrags.writeInt(fragmentSlot, atF('uFogMode'), medium.fogMode);
3899
3936
  this.lineFrags.writeFloat(fragmentSlot, atF('uFogNear'), medium.fogNear);
3900
3937
  this.lineFrags.writeFloat(fragmentSlot, atF('uFogFar'), medium.fogFar);
3901
- const linePipe = linePipeline(this.pipelines, this.surface.device, this.lineLayout, additive);
3938
+ const linePipe = linePipeline(this.blendedPipelines(), this.surface.device, this.lineLayout, additive);
3902
3939
  /* Two uniform blocks and three vertex buffers, and no attachment among them. */
3903
- const lineCommand = this.recordDraw(0, this.currentTarget());
3940
+ const lineCommand = this.recordBlended(0);
3904
3941
  if (lineCommand !== null) {
3905
3942
  lineCommand.pipeline = linePipe;
3906
3943
  lineCommand.bindGroup = this.lineBindGroup;
@@ -4107,7 +4144,38 @@ export class WebGPURenderer {
4107
4144
  sdfTextFrags;
4108
4145
  sdfTextBindGroups = new Map();
4109
4146
  /* -- The shadow pass -------------------------------------------------------------- */
4110
- shadowMap;
4147
+ /**
4148
+ * The sun's static, moving and peeled maps, as layers of one texture (`shadowMap.ts`). The three
4149
+ * views below are single layers, what each pass attaches; the two here are what is sampled.
4150
+ */
4151
+ sunShadowTexture;
4152
+ /** Every layer, for the lit pass, the light volumes and the medium. */
4153
+ sunArrayView;
4154
+ /**
4155
+ * The static layer alone, as an array, for the peel: it draws into another layer of the same
4156
+ * texture, and a view covering that layer too would put it in two usages in one pass.
4157
+ */
4158
+ sunStaticArrayView;
4159
+ /** How many layers the sun's texture holds: its maps, and the two glass layers once glass casts. */
4160
+ sunLayerCount;
4161
+ /** The glass depth layers, static then moving, once glass has cast. See `growSunForGlass`. */
4162
+ sunGlassViews = null;
4163
+ /** The texture a growth replaced, destroyed once the encoder that copied out of it is submitted. */
4164
+ retiredSunTexture = null;
4165
+ /** What the sun's light keeps through glass: two `rgba8unorm` layers, made when glass first casts. */
4166
+ sunTint = null;
4167
+ /** The tint pass's layouts, per-draw block, groups and pipelines, made when glass first casts. */
4168
+ glassTint = null;
4169
+ /** The tint pass open while the glass list is replayed into it. */
4170
+ tintPass = null;
4171
+ /** True while the glass list is replayed into a glass depth layer through `casterSink`. */
4172
+ drawingGlassDepth = false;
4173
+ /** Whether the open sun pass is the movers' layer, whose glass goes to the moving glass layer. */
4174
+ shadowLayerIsDynamic = false;
4175
+ /** The glass tints' sampler: filtering, and between levels, for frost's coarser reads. */
4176
+ tintSampler;
4177
+ /** The light a tint pass is drawn for: xyz a direction (w 0) or a position (w 1). */
4178
+ glassLight = new Float32Array(4);
4111
4179
  shadowView;
4112
4180
  shadowLayout;
4113
4181
  /** The same layout plus the joint palette. See `createDepthBindGroupLayout`. */
@@ -4134,7 +4202,6 @@ export class WebGPURenderer {
4134
4202
  * read twice — so a profile that switches peeling off gets no map and no pass on either
4135
4203
  * backend rather than a map nothing writes.
4136
4204
  */
4137
- peelMap;
4138
4205
  peelView;
4139
4206
  /**
4140
4207
  * The movers' own layer: the character, the rope it swings on, and anything else that walks.
@@ -4156,7 +4223,6 @@ export class WebGPURenderer {
4156
4223
  * and therefore as lit — so nothing looked broken, only empty, which is why it survived the
4157
4224
  * port and every gate in it.
4158
4225
  */
4159
- dynamicMap;
4160
4226
  dynamicView;
4161
4227
  /** The peel's own bind groups: `uPeelShadowLayer` at 1, and the static map to peel against. */
4162
4228
  shadowPeelBindGroup;
@@ -4230,7 +4296,7 @@ export class WebGPURenderer {
4230
4296
  if (held !== undefined)
4231
4297
  return held;
4232
4298
  const passUniforms = peel ? this.shadowPeelPassUniforms : this.shadowPassUniforms;
4233
- const previousView = peel ? this.shadowView : this.shadowBlankView;
4299
+ const previousView = peel ? this.sunStaticArrayView : this.shadowBlankView;
4234
4300
  const previousSampler = peel ? this.shadowPeelSampler : this.shadowBlankSampler;
4235
4301
  if (passUniforms === null || previousView === null || previousSampler === null)
4236
4302
  return null;
@@ -4278,8 +4344,23 @@ export class WebGPURenderer {
4278
4344
  * Built once and held, rather than made per call: it closes over `this` and a fresh one
4279
4345
  * every frame would be an allocation in a per-frame path.
4280
4346
  */
4347
+ /** The glass the open shadow pass was offered, kept for its own passes: `glassCasters.ts`. */
4348
+ glassCasters = new GlassCasterList();
4349
+ /** One resolution every glass caster is read through, so recording one allocates nothing. */
4350
+ glassCasterScratch = {
4351
+ transmission: 0,
4352
+ frost: 0,
4353
+ tint: [1, 1, 1],
4354
+ };
4281
4355
  casterSink = {
4282
4356
  mesh: (mesh, model, material) => {
4357
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
4358
+ /* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
4359
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
4360
+ this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
4361
+ }
4362
+ return;
4363
+ }
4283
4364
  const pass = this.shadowPass;
4284
4365
  if (pass === null)
4285
4366
  return;
@@ -4313,6 +4394,12 @@ export class WebGPURenderer {
4313
4394
  * uses — one draw, however many instances, exactly as the colour pass does it.
4314
4395
  */
4315
4396
  instanced: (batch, data, material) => {
4397
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
4398
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
4399
+ this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
4400
+ }
4401
+ return;
4402
+ }
4316
4403
  const pass = this.shadowPass;
4317
4404
  if (pass === null)
4318
4405
  return;
@@ -4346,7 +4433,13 @@ export class WebGPURenderer {
4346
4433
  pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
4347
4434
  pass.drawIndexed(geometry.indexCount, count);
4348
4435
  },
4349
- skinnedMesh: (mesh, model, palette) => {
4436
+ skinnedMesh: (mesh, model, palette, material) => {
4437
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
4438
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
4439
+ this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
4440
+ }
4441
+ return;
4442
+ }
4350
4443
  const pass = this.shadowPass;
4351
4444
  if (pass === null)
4352
4445
  return;
@@ -4681,7 +4774,9 @@ export class WebGPURenderer {
4681
4774
  * nothing at all under the default profile, which is on. The cache carries it for the same
4682
4775
  * reason it carries the sample count: no pass can then disagree with the attachment.
4683
4776
  */
4684
- this.pipelines = new PipelineCache(device, quality.screenEffects ? sceneColorFormat(quality.hdrScene) : surface.format, this.samples);
4777
+ /* Off compiles none of the glass lookup into a lit pipeline: see `PipelineCache.glassShadows`. */
4778
+ const glassShadows = quality.glassShadows !== 'off';
4779
+ this.pipelines = new PipelineCache(device, quality.screenEffects ? sceneColorFormat(quality.hdrScene) : surface.format, this.samples, glassShadows);
4685
4780
  /*
4686
4781
  * The canvas' own state, for the pass `ensurePass` reopens after `endFrame`.
4687
4782
  *
@@ -4693,7 +4788,12 @@ export class WebGPURenderer {
4693
4788
  this.overlayPipelines =
4694
4789
  this.pipelines.format === surface.format && this.samples === 1
4695
4790
  ? this.pipelines
4696
- : new PipelineCache(device, surface.format, 1);
4791
+ : new PipelineCache(device, surface.format, 1, glassShadows);
4792
+ this.latePipelines =
4793
+ this.pipelines.format === RECON_HISTORY_FORMAT && this.samples === 1
4794
+ ? this.pipelines
4795
+ : new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows);
4796
+ this.issueBound = this.issueCommand.bind(this);
4697
4797
  this.pipelineTargets =
4698
4798
  this.overlayPipelines === this.pipelines
4699
4799
  ? [this.pipelines]
@@ -4814,42 +4914,35 @@ export class WebGPURenderer {
4814
4914
  .createView({ dimension: '3d' });
4815
4915
  const blankDriftIndexView = blankVolume('flat.emptyDriftLightIndex', 'r32uint');
4816
4916
  const blankDriftAtlasView = blankVolume('flat.emptyDriftLightAtlas', 'rgba16float');
4817
- this.shadowMap = device.createTexture({
4818
- label: 'shadow.static',
4819
- size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
4917
+ /*
4918
+ * The static world, its movers and — where more than one static depth layer is asked for —
4919
+ * the peel, as layers of one texture, on the conditions `renderer.ts` builds its array on: one
4920
+ * decision read twice. Same size and format throughout, because the shader samples every
4921
+ * layer with one \`uShadowMapSize\` and one set of filter offsets.
4922
+ */
4923
+ const sunLayers = this.quality.directionalShadows
4924
+ ? this.quality.directionalShadowDepthLayers > 1
4925
+ ? 3
4926
+ : 2
4927
+ : 1;
4928
+ this.sunShadowTexture = device.createTexture({
4929
+ label: 'shadow.sun',
4930
+ size: [
4931
+ this.quality.directionalShadowMapSize,
4932
+ this.quality.directionalShadowMapSize,
4933
+ sunLayers,
4934
+ ],
4820
4935
  format: SHADOW_FORMAT,
4821
- usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
4936
+ /* Copyable both ways, because the first glass grows it by copying every layer across. */
4937
+ usage: SUN_USAGE,
4822
4938
  });
4823
- this.shadowView = this.shadowMap.createView();
4824
- /*
4825
- * The peel layer, on the condition `renderer.ts` builds its own on: more than one depth
4826
- * layer asked for. `depth.ts` fills it by discarding every fragment at or in front of what
4827
- * the static pass stored, so what lands here is the second independently fading occluder.
4828
- */
4829
- this.peelMap =
4830
- this.quality.directionalShadowDepthLayers > 1
4831
- ? device.createTexture({
4832
- label: 'shadow.peel',
4833
- size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
4834
- format: SHADOW_FORMAT,
4835
- usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
4836
- })
4837
- : null;
4838
- this.peelView = this.peelMap === null ? null : this.peelMap.createView();
4839
- /*
4840
- * The movers' layer, on the condition `renderer.ts` builds its own on: directional shadows
4841
- * at all. Same size and same format as the static map, because the shader samples all three
4842
- * with one `uShadowMapSize` and one set of PCF offsets.
4843
- */
4844
- this.dynamicMap = this.quality.directionalShadows
4845
- ? device.createTexture({
4846
- label: 'shadow.dynamic',
4847
- size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
4848
- format: SHADOW_FORMAT,
4849
- usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
4850
- })
4851
- : null;
4852
- this.dynamicView = this.dynamicMap === null ? null : this.dynamicMap.createView();
4939
+ this.sunLayerCount = sunLayers;
4940
+ const views = sunViewsOf(this.sunShadowTexture, sunLayers, sunLayers);
4941
+ this.sunArrayView = views.array;
4942
+ this.sunStaticArrayView = views.staticArray;
4943
+ this.shadowView = views.static;
4944
+ this.dynamicView = views.dynamic;
4945
+ this.peelView = views.peel;
4853
4946
  /*
4854
4947
  * **Cleared once, here, because a texture no pass has written reads as zero**, which the
4855
4948
  * specification requires, and a zero in a map compared with `LEQUAL` is an occluder at the
@@ -4890,6 +4983,14 @@ export class WebGPURenderer {
4890
4983
  * rather than the hardware's.
4891
4984
  */
4892
4985
  const shadowSampler = device.createSampler({ label: 'shadow.sampler' });
4986
+ this.tintSampler = device.createSampler({
4987
+ label: 'shadow.tintSampler',
4988
+ magFilter: 'linear',
4989
+ minFilter: 'linear',
4990
+ mipmapFilter: 'linear',
4991
+ addressModeU: 'clamp-to-edge',
4992
+ addressModeV: 'clamp-to-edge',
4993
+ });
4893
4994
  /*
4894
4995
  * The photometric atlas's own sampler, **created here rather than reusing `postSampler`.**
4895
4996
  *
@@ -4910,15 +5011,18 @@ export class WebGPURenderer {
4910
5011
  addressModeV: 'clamp-to-edge',
4911
5012
  });
4912
5013
  this.flatTextures = (name) => {
4913
- if (name === 'uStaticShadowMap')
4914
- return { view: this.shadowView, sampler: shadowSampler };
4915
- /*
4916
- * The movers' layer. Cleared at construction like the others, so a profile with
4917
- * directional shadows on that never opens the layer reads the far plane and therefore
4918
- * "nothing moving is in the way" — not whatever the allocation happened to contain.
4919
- */
4920
- if (name === 'uDynamicShadowMap' && this.dynamicView !== null) {
4921
- return { view: this.dynamicView, sampler: shadowSampler };
5014
+ if (name === 'uSunShadows')
5015
+ return { view: this.sunArrayView, sampler: shadowSampler };
5016
+ /* What the sun keeps through glass, or the one-texel stand-in the lookup reads as none. */
5017
+ if (name === 'uSunGlassTints') {
5018
+ return { view: this.sunTint?.array ?? blankArrayView, sampler: this.tintSampler };
5019
+ }
5020
+ /* What the lamps keep through glass, one layer a light, or the same stand-in. */
5021
+ if (name === 'uPointGlassTints') {
5022
+ return {
5023
+ view: this.pointShadowArray?.glassTint?.view ?? blankArrayView,
5024
+ sampler: this.tintSampler,
5025
+ };
4922
5026
  }
4923
5027
  /*
4924
5028
  * The caller's image, or the stand-in. Read from the field rather than passed in, because
@@ -4956,10 +5060,6 @@ export class WebGPURenderer {
4956
5060
  ? { view: blankView, sampler: blankSampler }
4957
5061
  : { view: albedo.view, sampler: albedo.sampler };
4958
5062
  }
4959
- /* The peel, where the profile asked for one. `uPeeledShadowEnabled` gates the sample. */
4960
- if (name === 'uPeeledShadowMap' && this.peelView !== null) {
4961
- return { view: this.peelView, sampler: shadowSampler };
4962
- }
4963
5063
  /*
4964
5064
  * The room, once a bake has filled it. Unbaked reads as the blank cube: nothing to mirror.
4965
5065
  *
@@ -5028,7 +5128,15 @@ export class WebGPURenderer {
5028
5128
  * texels by construction, an offset that snapped to one being no offset at all.
5029
5129
  */
5030
5130
  if (name === 'uRefractScene') {
5031
- return { view: this.refractSnapshotView ?? blankView, sampler: iesSampler };
5131
+ this.refractSampler ??= device.createSampler({
5132
+ label: 'refract.sampler',
5133
+ magFilter: 'linear',
5134
+ minFilter: 'linear',
5135
+ mipmapFilter: 'linear',
5136
+ addressModeU: 'clamp-to-edge',
5137
+ addressModeV: 'clamp-to-edge',
5138
+ });
5139
+ return { view: this.refractSnapshotView ?? blankView, sampler: this.refractSampler };
5032
5140
  }
5033
5141
  /*
5034
5142
  * Every point light's shadow, in one binding.
@@ -5208,9 +5316,7 @@ export class WebGPURenderer {
5208
5316
  */
5209
5317
  this.mediumLayout = createPostStageLayout(device, MEDIUM_UNIFORMS, MEDIUM_FRAG_SIZE, [
5210
5318
  { binding: MEDIUM_TEXTURES.uDepth, filterable: false },
5211
- { binding: MEDIUM_TEXTURES.uStaticShadowMap, filterable: false },
5212
- { binding: MEDIUM_TEXTURES.uPeeledShadowMap, filterable: false },
5213
- { binding: MEDIUM_TEXTURES.uDynamicShadowMap, filterable: false },
5319
+ { binding: MEDIUM_TEXTURES.uSunShadows, filterable: false, dimension: '2d-array' },
5214
5320
  ]);
5215
5321
  this.mediumUniforms = device.createBuffer({
5216
5322
  label: 'post.mediumUniforms',
@@ -5343,9 +5449,9 @@ export class WebGPURenderer {
5343
5449
  device.queue.writeBuffer(depthPassUniforms, 0, new Int32Array([0]));
5344
5450
  this.scatterDepthLayout = createScatterDepthBindGroupLayout(device, DEPTH_FRAG_BINDING, DEPTH_FRAG_SIZE, DEPTH_PREVIOUS_BINDING);
5345
5451
  this.scatterDepthDraws = new UniformRing(device, SCATTER_DEPTH_SIZE, MAX_SCATTER_DEPTH_DRAWS, USAGE_UNIFORM_DST, 'scatter.depthDrawRing', this.scatterDepthBudget);
5346
- this.scatterDepthBindGroup = createScatterDepthBindGroup(device, this.scatterDepthLayout, this.scatterDepthDraws.buffer, { binding: DEPTH_FRAG_BINDING, buffer: depthPassUniforms, size: DEPTH_FRAG_SIZE }, DEPTH_PREVIOUS_BINDING, blankView, blankSampler);
5347
- this.shadowBindGroup = createDepthBindGroup(device, this.shadowLayout, this.shadowDraws.buffer, depthPassUniforms, blankView, blankSampler);
5348
- this.shadowBlankView = blankView;
5452
+ this.scatterDepthBindGroup = createScatterDepthBindGroup(device, this.scatterDepthLayout, this.scatterDepthDraws.buffer, { binding: DEPTH_FRAG_BINDING, buffer: depthPassUniforms, size: DEPTH_FRAG_SIZE }, DEPTH_PREVIOUS_BINDING, blankArrayView, blankSampler);
5453
+ this.shadowBindGroup = createDepthBindGroup(device, this.shadowLayout, this.shadowDraws.buffer, depthPassUniforms, blankArrayView, blankSampler);
5454
+ this.shadowBlankView = blankArrayView;
5349
5455
  this.shadowBlankSampler = blankSampler;
5350
5456
  this.shadowPassUniforms = depthPassUniforms;
5351
5457
  /*
@@ -5376,11 +5482,11 @@ export class WebGPURenderer {
5376
5482
  this.shadowPeelBindGroup =
5377
5483
  peelPassUniforms === null
5378
5484
  ? null
5379
- : createDepthBindGroup(device, this.shadowLayout, this.shadowDraws.buffer, peelPassUniforms, this.shadowView, shadowSampler);
5485
+ : createDepthBindGroup(device, this.shadowLayout, this.shadowDraws.buffer, peelPassUniforms, this.sunStaticArrayView, shadowSampler);
5380
5486
  this.scatterDepthPeelBindGroup =
5381
5487
  peelPassUniforms === null
5382
5488
  ? null
5383
- : createScatterDepthBindGroup(device, this.scatterDepthLayout, this.scatterDepthDraws.buffer, { binding: DEPTH_FRAG_BINDING, buffer: peelPassUniforms, size: DEPTH_FRAG_SIZE }, DEPTH_PREVIOUS_BINDING, this.shadowView, shadowSampler);
5489
+ : createScatterDepthBindGroup(device, this.scatterDepthLayout, this.scatterDepthDraws.buffer, { binding: DEPTH_FRAG_BINDING, buffer: peelPassUniforms, size: DEPTH_FRAG_SIZE }, DEPTH_PREVIOUS_BINDING, this.sunStaticArrayView, shadowSampler);
5384
5490
  /*
5385
5491
  * The overlays. Rings rather than single blocks because a frame draws several of each —
5386
5492
  * the loader alone is two panels and two strings — and `queue.writeBuffer` does not
@@ -5621,7 +5727,10 @@ export class WebGPURenderer {
5621
5727
  this.perFrame.reset();
5622
5728
  this.materials.dirty();
5623
5729
  /* Spent by `runMotion` at the end of the last frame; a frame states its movers afresh. */
5624
- this.motionCount = 0;
5730
+ this.motionPass?.reset();
5731
+ this.latePass?.reset();
5732
+ this.moverFrame += 1;
5733
+ this.inFrame = true;
5625
5734
  /*
5626
5735
  * The canvas rather than the swap chain's texture, and the difference is that reading this
5627
5736
  * does not *acquire* anything. See `swapView`.
@@ -5904,7 +6013,27 @@ export class WebGPURenderer {
5904
6013
  'would be patched from was not kept. Say so at `createMesh` — the flag is what decides ' +
5905
6014
  'what is kept and what a buffer is hinted as.');
5906
6015
  }
5907
- mesh.update(this.surface.device, positions, normals);
6016
+ /*
6017
+ * **The rows before this rewrite are last frame's**, kept only for a reconstruction and only
6018
+ * on the first rewrite of a frame — a second must not replace where the vertices were last
6019
+ * frame with where they were a moment ago. A rewrite between frames is the next frame's. See
6020
+ * `changeFrames.ts`.
6021
+ */
6022
+ const motion = mesh.motion;
6023
+ let previous = null;
6024
+ if (motion !== null && this.reconstructionWanted) {
6025
+ const frame = frameOfChange(this.moverFrame, this.inFrame);
6026
+ if (keepsPrevious(motion.changed, frame)) {
6027
+ motion.previous ??= this.surface.device.createBuffer({
6028
+ label: 'recon.dynamicPrevious',
6029
+ size: mesh.vertexBuffers[0]?.size ?? 0,
6030
+ usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
6031
+ });
6032
+ previous = motion.previous;
6033
+ }
6034
+ motion.changed = frame;
6035
+ }
6036
+ mesh.update(this.surface.device, positions, normals, previous);
5908
6037
  }
5909
6038
  /**
5910
6039
  * Settle everything the frame shares: the camera, the lights and the fog.
@@ -6532,8 +6661,8 @@ export class WebGPURenderer {
6532
6661
  * depth, so a motion pass drawing it would either be depth-rejected everywhere — wasted — or,
6533
6662
  * where it happened to pass, write motion for a surface no pixel of the frame belongs to.
6534
6663
  */
6535
- if (previousModel !== null)
6536
- this.recordMotion(mesh, model, previousModel);
6664
+ if (this.tracksMotionNow())
6665
+ this.recordDrawMotion(mesh, model, previousModel);
6537
6666
  this.submitMesh(mesh, model, tint, 1, false, { depthLayer });
6538
6667
  }
6539
6668
  /**
@@ -6668,7 +6797,9 @@ export class WebGPURenderer {
6668
6797
  }
6669
6798
  return;
6670
6799
  }
6671
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, this.viewProj);
6800
+ /* A blended draw landing after the upscale is drawn into an unjittered picture. */
6801
+ const late = blend && (this.drawsLate() || (this.oitReplaying && this.reconstructing));
6802
+ this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, late ? this.correctedViewProj : this.viewProj);
6672
6803
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uModel.offset, model);
6673
6804
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uTint.offset, tint ?? WHITE);
6674
6805
  /* The frame's wind, for a mesh carrying a sway lane. Written on this path and not the
@@ -6729,17 +6860,7 @@ export class WebGPURenderer {
6729
6860
  * shades as an ordinary translucent one rather than sampling a texture holding the frame
6730
6861
  * before last. That is the defined state the two-backends rule asks for.
6731
6862
  */
6732
- const refraction = options.refraction ?? 0;
6733
- const refracting = refraction > 0 && this.takeRefractSnapshot();
6734
- if (refracting) {
6735
- this.perFrameFloats[this.materialField('uRefractStrength')] = refraction;
6736
- const refractTint = options.refractTint;
6737
- const tintAt = this.materialField('uRefractTint');
6738
- this.perFrameFloats[tintAt] = refractTint?.[0] ?? 1;
6739
- this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
6740
- this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
6741
- this.perFrameFloats[this.materialField('uRefractThickness')] = options.thicknessM ?? 0;
6742
- }
6863
+ const refracting = this.bindSeeThrough(options);
6743
6864
  /* Taken for itself when any of those differs from the pass, and put back below: the rule both
6744
6865
  backends count material changes by. See `materialChanges.ts`. */
6745
6866
  const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
@@ -6798,7 +6919,7 @@ export class WebGPURenderer {
6798
6919
  * pays the compile it was trying to avoid. That is a slow frame; the alternative is a
6799
6920
  * thrown error in the middle of one.
6800
6921
  */
6801
- const pipelines = this.targetPipelines();
6922
+ const pipelines = blend ? this.blendedPipelines() : this.targetPipelines();
6802
6923
  let pipeline = pipelines.peek(key);
6803
6924
  if (pipeline === undefined) {
6804
6925
  const present = this.meshPresent.get(base);
@@ -6830,7 +6951,7 @@ export class WebGPURenderer {
6830
6951
  * the uniform rings are the state arena this design would otherwise have had to build —
6831
6952
  * so the command carries them as its two dynamic offsets.
6832
6953
  */
6833
- const command = this.recordDraw(0, this.currentTarget());
6954
+ const command = blend ? this.recordBlended(0) : this.recordDraw(0, this.currentTarget());
6834
6955
  if (command !== null) {
6835
6956
  command.pipeline = pipeline;
6836
6957
  command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
@@ -6871,11 +6992,10 @@ export class WebGPURenderer {
6871
6992
  if (!fog)
6872
6993
  this.perFrameInts[this.materialField('uFogEnabled')] = 1;
6873
6994
  }
6874
- if (refracting) {
6875
- /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
6876
- shared, so a strength left set is worn by everything drawn after it. */
6877
- this.perFrameFloats[this.materialField('uRefractStrength')] = 0;
6878
- }
6995
+ /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
6996
+ shared, so a strength left set is worn by everything drawn after it. */
6997
+ if (refracting)
6998
+ this.releaseSeeThrough();
6879
6999
  if (!toneMapped) {
6880
7000
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
6881
7001
  }
@@ -6935,11 +7055,35 @@ export class WebGPURenderer {
6935
7055
  uploadInstanced(batch, data) {
6936
7056
  if (this.surface.lost)
6937
7057
  return;
6938
- batch.upload(this.surface.device.queue, data);
7058
+ /*
7059
+ * **The placement before this upload is last frame's**, kept only for a reconstruction and only
7060
+ * on a frame's first upload, for the reason `updateMesh` gives. See `changeFrames.ts`.
7061
+ */
7062
+ let previous = null;
7063
+ if (this.reconstructionWanted) {
7064
+ const frame = frameOfChange(this.moverFrame, this.inFrame);
7065
+ if (keepsPrevious(batch.changed, frame)) {
7066
+ batch.previousBuffer ??= this.surface.device.createBuffer({
7067
+ label: 'recon.instancesPrevious',
7068
+ size: batch.capacity * INSTANCE_STRIDE,
7069
+ usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
7070
+ });
7071
+ previous = batch.previousBuffer;
7072
+ }
7073
+ batch.changed = frame;
7074
+ }
7075
+ batch.upload(this.surface.device.queue, data, previous);
6939
7076
  }
6940
7077
  /** Draw every live instance, opaque. */
6941
7078
  drawInstanced(batch, data) {
6942
7079
  this.submitInstanced(batch, data, 1, false, {});
7080
+ /* A batch that moved this frame pairs each slot with last frame's; the batch is the identity. */
7081
+ const previous = batch.previousBuffer;
7082
+ if (previous !== null && batch.changed === this.moverFrame && this.tracksMotionNow()) {
7083
+ const paired = pairedInstances(batch.previousCount, Math.min(data.count, batch.capacity));
7084
+ if (paired > 0)
7085
+ this.motionPass?.recordInstanced(batch.mesh, batch.buffer, previous, paired);
7086
+ }
6943
7087
  }
6944
7088
  /** Draw every live instance, blended. See `drawTranslucentMesh` for what the options mean. */
6945
7089
  drawTranslucentInstanced(batch, data, opacity, options = {}) {
@@ -6950,6 +7094,44 @@ export class WebGPURenderer {
6950
7094
  this.instanced.delete(batch.mesh);
6951
7095
  batch.dispose();
6952
7096
  }
7097
+ /**
7098
+ * Write what a translucent draw shows through it into the material scratch: refraction's bend and
7099
+ * absorption, and glass. Returns whether the frame's copy is there to read, and so whether
7100
+ * `releaseSeeThrough` owes the scratch back.
7101
+ *
7102
+ * **A glass draw takes the copy even when it does not bend**, since a pane shows what is behind it
7103
+ * either way. A copy that cannot be taken — a mirror, a probe bake — leaves both off, so the pane
7104
+ * shades as ordinary translucency rather than sampling a stand-in. See `glass.ts`.
7105
+ */
7106
+ bindSeeThrough(options) {
7107
+ const refraction = options.refraction ?? 0;
7108
+ const glassy = resolveGlass(options.glass, this.glassScratch);
7109
+ if ((refraction <= 0 && !glassy) || !this.takeRefractSnapshot())
7110
+ return false;
7111
+ const glass = this.glassScratch;
7112
+ /* Strength, path thickness, transmission, frost: one vector, for the budget the shader states. */
7113
+ const seeAt = this.materialField('uSeeThrough');
7114
+ this.perFrameFloats[seeAt] = Math.max(refraction, 0);
7115
+ this.perFrameFloats[seeAt + 1] = options.thicknessM ?? 0;
7116
+ this.perFrameFloats[seeAt + 2] = glass.transmission;
7117
+ this.perFrameFloats[seeAt + 3] = glass.frost;
7118
+ const refractTint = options.refractTint;
7119
+ const tintAt = this.materialField('uRefractTint');
7120
+ this.perFrameFloats[tintAt] = refractTint?.[0] ?? 1;
7121
+ this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
7122
+ this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
7123
+ const glassTintAt = this.materialField('uGlassTint');
7124
+ this.perFrameFloats[glassTintAt] = glass.tint[0];
7125
+ this.perFrameFloats[glassTintAt + 1] = glass.tint[1];
7126
+ this.perFrameFloats[glassTintAt + 2] = glass.tint[2];
7127
+ return true;
7128
+ }
7129
+ /** Put the scratch back after `bindSeeThrough`: refraction and glass off. */
7130
+ releaseSeeThrough() {
7131
+ const seeAt = this.materialField('uSeeThrough');
7132
+ this.perFrameFloats[seeAt] = 0;
7133
+ this.perFrameFloats[seeAt + 2] = 0;
7134
+ }
6953
7135
  /**
6954
7136
  * One draw, one material slot, one uniform slot, however many instances.
6955
7137
  *
@@ -6979,7 +7161,7 @@ export class WebGPURenderer {
6979
7161
  }
6980
7162
  return;
6981
7163
  }
6982
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, this.viewProj);
7164
+ this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, blend && this.drawsLate() ? this.correctedViewProj : this.viewProj);
6983
7165
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
6984
7166
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
6985
7167
  this.perDraw.writeInt(slot, FLAT_VERT_FIELDS.uHasTangents.offset, mesh.hasTangents ? 1 : 0);
@@ -7002,8 +7184,9 @@ export class WebGPURenderer {
7002
7184
  }
7003
7185
  const depthWrite = options.depthWrite ?? true;
7004
7186
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
7005
- /* An instanced draw does not refract. See `materialChanges.ts`. */
7006
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting: false });
7187
+ /* A blended batch shows what is behind it — refraction or glass — as a single draw does. */
7188
+ const refracting = blend && this.bindSeeThrough(options);
7189
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
7007
7190
  if (own)
7008
7191
  this.materials.dirty();
7009
7192
  const base = mesh.key ?? 'flat:s0:u0';
@@ -7013,7 +7196,7 @@ export class WebGPURenderer {
7013
7196
  const key = `${base}|inst${blend ? '|blend' : ''}` +
7014
7197
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
7015
7198
  `${this.materialDoubleSided ? '|2s' : ''}`;
7016
- const pipelines = this.targetPipelines();
7199
+ const pipelines = blend ? this.blendedPipelines() : this.targetPipelines();
7017
7200
  let pipeline = pipelines.peek(key);
7018
7201
  if (pipeline === undefined) {
7019
7202
  const present = this.meshPresent.get(base);
@@ -7024,7 +7207,7 @@ export class WebGPURenderer {
7024
7207
  }
7025
7208
  const material = this.materialSlotForDraw();
7026
7209
  if (material !== null) {
7027
- const command = this.recordDraw(0, this.currentTarget());
7210
+ const command = blend ? this.recordBlended(0) : this.recordDraw(0, this.currentTarget());
7028
7211
  if (command !== null) {
7029
7212
  command.pipeline = pipeline;
7030
7213
  command.bindGroup = this.flatBindGroupFor(false, false, null);
@@ -7070,6 +7253,8 @@ export class WebGPURenderer {
7070
7253
  if (!toneMapped) {
7071
7254
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
7072
7255
  }
7256
+ if (refracting)
7257
+ this.releaseSeeThrough();
7073
7258
  if (own)
7074
7259
  this.materials.dirty();
7075
7260
  }
@@ -7243,6 +7428,8 @@ export class WebGPURenderer {
7243
7428
  this.lightViewProj.set(lightViewProj);
7244
7429
  mat4.multiply(this.correctedLightViewProj, SHADOW_CLIP_CORRECTION, lightViewProj);
7245
7430
  this.shadowLayerIsPeel = peel;
7431
+ this.shadowLayerIsDynamic = dynamic;
7432
+ this.glassCasters.clear();
7246
7433
  /*
7247
7434
  * A frame's peel is only as good as the pass that fills it, and the static layer is the one
7248
7435
  * that opens first.
@@ -7348,13 +7535,295 @@ export class WebGPURenderer {
7348
7535
  casters(this.casterSink);
7349
7536
  }
7350
7537
  /** Close the shadow pass and submit it, so the frame can sample what it wrote. */
7538
+ /**
7539
+ * Add the sun's two glass layers, once, carrying every layer it holds across on `encoder`.
7540
+ *
7541
+ * **Copied rather than re-baked**, for the reason `SunShadowArray.growForGlass` gives: the static
7542
+ * map is the consumer's to bake and may be baked once, and the glass that grows the texture
7543
+ * arrives in the very pass that drew it. Every view of the old texture is stale afterwards, so
7544
+ * every group holding one is rebuilt here — the lit pass's, the volumes', the medium's and the
7545
+ * peel's — and the old texture is destroyed only once this encoder is submitted.
7546
+ */
7547
+ growSunForGlass(encoder) {
7548
+ if (this.sunGlassViews !== null)
7549
+ return;
7550
+ const device = this.surface.device;
7551
+ const size = this.quality.directionalShadowMapSize;
7552
+ const maps = this.sunLayerCount;
7553
+ /* The glass layers at their fixed places after the three maps: see `SUN_GLASS_STATIC_LAYER`. */
7554
+ const layers = SUN_GLASS_MOVING_LAYER + 1;
7555
+ const old = this.sunShadowTexture;
7556
+ const texture = device.createTexture({
7557
+ label: 'shadow.sun',
7558
+ size: [size, size, layers],
7559
+ format: SHADOW_FORMAT,
7560
+ usage: SUN_USAGE,
7561
+ });
7562
+ for (let layer = 0; layer < maps; layer++) {
7563
+ encoder.copyTextureToTexture({ texture: old, origin: { x: 0, y: 0, z: layer } }, { texture, origin: { x: 0, y: 0, z: layer } }, [size, size, 1]);
7564
+ }
7565
+ this.retiredSunTexture = old;
7566
+ this.sunShadowTexture = texture;
7567
+ this.sunLayerCount = layers;
7568
+ const views = sunViewsOf(texture, maps, layers);
7569
+ this.sunArrayView = views.array;
7570
+ this.sunStaticArrayView = views.staticArray;
7571
+ this.shadowView = views.static;
7572
+ this.dynamicView = views.dynamic;
7573
+ this.peelView = views.peel;
7574
+ this.sunGlassViews = views.glass;
7575
+ /* Both glass layers born at the far plane: nothing is nearer the sun than a pane not drawn. */
7576
+ for (const view of views.glass ?? []) {
7577
+ encoder
7578
+ .beginRenderPass({
7579
+ label: 'shadow.glass.clear',
7580
+ colorAttachments: [],
7581
+ depthStencilAttachment: {
7582
+ view,
7583
+ depthClearValue: 1,
7584
+ depthLoadOp: 'clear',
7585
+ depthStoreOp: 'store',
7586
+ },
7587
+ })
7588
+ .end();
7589
+ }
7590
+ this.rebuildSunGroups();
7591
+ }
7592
+ /** Every group that holds a view of the sun's texture, rebuilt after it has been replaced. */
7593
+ rebuildSunGroups() {
7594
+ const device = this.surface.device;
7595
+ const peelUniforms = this.shadowPeelPassUniforms;
7596
+ const peelSampler = this.shadowPeelSampler;
7597
+ if (peelUniforms !== null && peelSampler !== null) {
7598
+ this.shadowPeelBindGroup = createDepthBindGroup(device, this.shadowLayout, this.shadowDraws.buffer, peelUniforms, this.sunStaticArrayView, peelSampler);
7599
+ this.scatterDepthPeelBindGroup = createScatterDepthBindGroup(device, this.scatterDepthLayout, this.scatterDepthDraws.buffer, { binding: DEPTH_FRAG_BINDING, buffer: peelUniforms, size: DEPTH_FRAG_SIZE }, DEPTH_PREVIOUS_BINDING, this.sunStaticArrayView, peelSampler);
7600
+ }
7601
+ this.cutoutPeelGroups.clear();
7602
+ this.rebuildFlatGroupsForNewRings();
7603
+ this.lightVolumeBindGroup = this.buildLightVolumeBindGroup();
7604
+ this.mediumBindGroup = this.buildMediumBindGroup();
7605
+ }
7606
+ /** The sun's tint target, made the first time glass casts and born clear on `encoder`. */
7607
+ ensureSunTint(encoder) {
7608
+ if (this.sunTint !== null)
7609
+ return this.sunTint;
7610
+ const size = this.quality.directionalShadowMapSize;
7611
+ const edge = this.quality.glassShadows === 'half' ? Math.max(1, size >> 1) : size;
7612
+ const device = this.surface.device;
7613
+ const texture = device.createTexture({
7614
+ label: 'shadow.sunTint',
7615
+ size: [edge, edge, 2],
7616
+ format: 'rgba8unorm',
7617
+ mipLevelCount: Math.floor(Math.log2(edge)) + 1,
7618
+ usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
7619
+ });
7620
+ const layer = (index) => texture.createView({
7621
+ dimension: '2d',
7622
+ baseArrayLayer: index,
7623
+ arrayLayerCount: 1,
7624
+ baseMipLevel: 0,
7625
+ mipLevelCount: 1,
7626
+ });
7627
+ const mips = (index) => new SnapshotMips(device, this.pipelines, texture, 'rgba8unorm', index, 'shadow.tint.mips');
7628
+ const target = {
7629
+ texture,
7630
+ layers: [layer(0), layer(1)],
7631
+ array: texture.createView({ dimension: '2d-array' }),
7632
+ mips: [mips(0), mips(1)],
7633
+ };
7634
+ for (const view of target.layers) {
7635
+ encoder
7636
+ .beginRenderPass({
7637
+ label: 'shadow.tint.clear',
7638
+ colorAttachments: [{ view, loadOp: 'clear', clearValue: [1, 1, 1, 1], storeOp: 'store' }],
7639
+ })
7640
+ .end();
7641
+ }
7642
+ this.sunTint = target;
7643
+ this.rebuildFlatGroupsForNewRings();
7644
+ return target;
7645
+ }
7646
+ /**
7647
+ * Draw the pass's glass twice: where the nearest pane is, and what the panes let through.
7648
+ *
7649
+ * After the opaque casters, on the pass's own encoder. A layer that has held glass is redrawn
7650
+ * even when this pass offered none, so glass that has gone takes its colour with it.
7651
+ */
7652
+ flushSunGlass(encoder, which) {
7653
+ const list = this.glassCasters;
7654
+ if (this.quality.glassShadows === 'off')
7655
+ return;
7656
+ if (list.count === 0 && this.sunGlassViews === null)
7657
+ return;
7658
+ if (list.count > 0)
7659
+ this.growSunForGlass(encoder);
7660
+ const glassViews = this.sunGlassViews;
7661
+ if (glassViews === null)
7662
+ return;
7663
+ /* Where the nearest pane is: the depth pipelines, through the sink every opaque caster took. */
7664
+ this.shadowPass = encoder.beginRenderPass({
7665
+ label: 'shadow.glass',
7666
+ colorAttachments: [],
7667
+ depthStencilAttachment: {
7668
+ view: glassViews[which === 'static' ? 0 : 1],
7669
+ depthClearValue: 1,
7670
+ depthLoadOp: 'clear',
7671
+ depthStoreOp: 'store',
7672
+ },
7673
+ });
7674
+ this.drawingGlassDepth = true;
7675
+ list.replay(this.glassDepthSink);
7676
+ this.drawingGlassDepth = false;
7677
+ this.shadowPass.end();
7678
+ this.shadowPass = null;
7679
+ /* What the panes let through, every one on a ray, multiplied. */
7680
+ const target = this.ensureSunTint(encoder);
7681
+ const tint = (this.glassTint ??= new GpuGlassTint(this.surface.device));
7682
+ tint.frag.reset();
7683
+ /* The direction the light travels, off the light matrix's depth row; the sign is immaterial,
7684
+ since the tint takes the cosine absolutely, and w 0 says it is a direction. */
7685
+ const m = this.correctedLightViewProj;
7686
+ const length = Math.hypot(m[2], m[6], m[10]) || 1;
7687
+ this.glassLight[0] = m[2] / length;
7688
+ this.glassLight[1] = m[6] / length;
7689
+ this.glassLight[2] = m[10] / length;
7690
+ this.glassLight[3] = 0;
7691
+ this.tintPass = encoder.beginRenderPass({
7692
+ label: 'shadow.tint',
7693
+ colorAttachments: [
7694
+ {
7695
+ view: target.layers[which === 'static' ? 0 : 1],
7696
+ loadOp: 'clear',
7697
+ clearValue: [1, 1, 1, 1],
7698
+ storeOp: 'store',
7699
+ },
7700
+ ],
7701
+ });
7702
+ list.replay(this.glassTintSink);
7703
+ this.tintPass.end();
7704
+ this.tintPass = null;
7705
+ tint.frag.flush();
7706
+ /* The chain frost reads, for the layer just drawn. */
7707
+ target.mips[which === 'static' ? 0 : 1].record(encoder);
7708
+ }
7709
+ /** The glass list into a glass depth layer: `casterSink` itself, with glass let through. */
7710
+ glassDepthSink = {
7711
+ mesh: (mesh, model, material) => this.casterSink.mesh(mesh, model, material),
7712
+ instanced: (batch, data, material) => this.casterSink.instanced?.(batch, data, material),
7713
+ skinnedMesh: (mesh, model, palette, material) => this.casterSink.skinnedMesh(mesh, model, palette, material),
7714
+ };
7715
+ /**
7716
+ * The glass list into a tint layer, through the tint pipelines, **culling nothing**: every pane on
7717
+ * a ray counts, whichever way it faces. See the other backend's `glassTintSink`.
7718
+ */
7719
+ glassTintSink = {
7720
+ mesh: (mesh, model, material, glass) => {
7721
+ const pass = this.tintPass;
7722
+ const tint = this.glassTint;
7723
+ if (pass === null || tint === null)
7724
+ return;
7725
+ const geometry = mesh;
7726
+ if (geometry.vertexBuffers === undefined)
7727
+ return;
7728
+ const cutout = cutoutOf(material);
7729
+ const albedo = cutout === null ? null : cutout.albedo;
7730
+ const slot = this.shadowDraws.allocate();
7731
+ const pane = tint.frag.allocate();
7732
+ if (slot === null || pane === null)
7733
+ return;
7734
+ this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uLightViewProj.offset, this.correctedLightViewProj);
7735
+ this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uModel.offset, model);
7736
+ this.writeShadowWind(slot);
7737
+ if (cutout !== null)
7738
+ this.writeCutout(slot, cutout, DEPTH_CUTOUT_VERT_FIELDS);
7739
+ tint.writePane(pane, glass, this.glassLight);
7740
+ pass.setPipeline(tint.pipeline(this.pipelines, albedo === null ? 'rigid' : 'cutout', geometry.key ?? '', this.presentOf(geometry), 'none'));
7741
+ pass.setBindGroup(0, albedo === null
7742
+ ? tint.group(this.shadowDraws.buffer)
7743
+ : tint.cutoutGroup(this.shadowDraws.buffer, albedo.view, albedo.sampler), [slot, pane]);
7744
+ for (let index = 0; index < geometry.vertexBuffers.length; index++) {
7745
+ pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
7746
+ }
7747
+ pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
7748
+ pass.drawIndexed(geometry.indexCount);
7749
+ },
7750
+ instanced: (batch, data, material, glass) => {
7751
+ const pass = this.tintPass;
7752
+ const tint = this.glassTint;
7753
+ if (pass === null || tint === null)
7754
+ return;
7755
+ const gpuBatch = batch;
7756
+ const geometry = gpuBatch.mesh;
7757
+ if (geometry.vertexBuffers === undefined)
7758
+ return;
7759
+ const count = Math.min(data.count, gpuBatch.capacity);
7760
+ if (count === 0)
7761
+ return;
7762
+ const cutout = cutoutOf(material);
7763
+ const albedo = cutout === null ? null : cutout.albedo;
7764
+ const slot = this.shadowDraws.allocate();
7765
+ const pane = tint.frag.allocate();
7766
+ if (slot === null || pane === null)
7767
+ return;
7768
+ this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uLightViewProj.offset, this.correctedLightViewProj);
7769
+ if (cutout !== null)
7770
+ this.writeCutout(slot, cutout, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS);
7771
+ tint.writePane(pane, glass, this.glassLight);
7772
+ pass.setPipeline(tint.pipeline(this.pipelines, albedo === null ? 'instanced' : 'instancedCutout', geometry.key ?? '', this.presentOf(geometry), 'none'));
7773
+ pass.setBindGroup(0, albedo === null
7774
+ ? tint.group(this.shadowDraws.buffer)
7775
+ : tint.cutoutGroup(this.shadowDraws.buffer, albedo.view, albedo.sampler), [slot, pane]);
7776
+ let index = 0;
7777
+ for (; index < geometry.vertexBuffers.length; index++) {
7778
+ pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
7779
+ }
7780
+ pass.setVertexBuffer(index, gpuBatch.buffer);
7781
+ pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
7782
+ pass.drawIndexed(geometry.indexCount, count);
7783
+ },
7784
+ skinnedMesh: (mesh, model, palette, _material, glass) => {
7785
+ const pass = this.tintPass;
7786
+ const tint = this.glassTint;
7787
+ if (pass === null || tint === null)
7788
+ return;
7789
+ const geometry = mesh;
7790
+ if (geometry.vertexBuffers === undefined)
7791
+ return;
7792
+ const paletteSlot = this.skinPalettes.take(this.surface.device, palette);
7793
+ const view = paletteSlot === null ? null : this.skinPalettes.view(paletteSlot);
7794
+ if (view === null)
7795
+ return;
7796
+ const slot = this.shadowDraws.allocate();
7797
+ const pane = tint.frag.allocate();
7798
+ if (slot === null || pane === null)
7799
+ return;
7800
+ this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uLightViewProj.offset, this.correctedLightViewProj);
7801
+ this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uModel.offset, model);
7802
+ this.writeShadowWind(slot);
7803
+ tint.writePane(pane, glass, this.glassLight);
7804
+ pass.setPipeline(tint.pipeline(this.pipelines, 'skinned', geometry.key ?? '', this.presentOf(geometry), 'none'));
7805
+ pass.setBindGroup(0, tint.skinnedGroup(this.shadowDraws.buffer, view), [slot, pane]);
7806
+ for (let index = 0; index < geometry.vertexBuffers.length; index++) {
7807
+ pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
7808
+ }
7809
+ pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
7810
+ pass.drawIndexed(geometry.indexCount);
7811
+ },
7812
+ };
7351
7813
  endShadowPass() {
7352
7814
  if (this.surface.lost || this.shadowPass === null || this.shadowEncoder === null)
7353
7815
  return;
7816
+ this.shadowPass.end();
7817
+ /* The peel keeps no glass: its layer is the second opaque occluder and nothing else. */
7818
+ if (!this.shadowLayerIsPeel) {
7819
+ this.flushSunGlass(this.shadowEncoder, this.shadowLayerIsDynamic ? 'dynamic' : 'static');
7820
+ }
7354
7821
  this.shadowDraws.flush();
7355
7822
  this.scatterDepthDraws.flush();
7356
- this.shadowPass.end();
7357
7823
  this.surface.device.queue.submit([this.shadowEncoder.finish()]);
7824
+ /* A growth's old texture, copied out of by what was just submitted and needed no longer. */
7825
+ this.retiredSunTexture?.destroy();
7826
+ this.retiredSunTexture = null;
7358
7827
  /* The map now holds a rendered peel rather than whatever it was created with. */
7359
7828
  if (this.shadowLayerIsPeel)
7360
7829
  this.peelFilled = true;
@@ -7383,6 +7852,8 @@ export class WebGPURenderer {
7383
7852
  preparedPointLightCount = 0;
7384
7853
  /** See `renderer.ts`: built when a world reports its lights, and rebuilt only to grow. */
7385
7854
  pointShadowArray = null;
7855
+ /** The array glass replaced, destroyed once the round that copied out of it is submitted. */
7856
+ retiredPointTexture = null;
7386
7857
  bakeScratch = createBakeScratch(POINT_SHADOW_POOL);
7387
7858
  /** Said once per renderer; see `updatePointShadows`. */
7388
7859
  warnedNoPointShadowArray = false;
@@ -7535,6 +8006,7 @@ export class WebGPURenderer {
7535
8006
  */
7536
8007
  this.shadowDraws.reset();
7537
8008
  this.scatterDepthDraws.reset();
8009
+ this.glassTint?.frag.reset();
7538
8010
  this.shadowEncoder = this.surface.device.createCommandEncoder({ label: 'pointShadow' });
7539
8011
  /* A cube face is a shadow pass like any other, and the courtyard is where it matters: six of
7540
8012
  them a frame is the difference this slot exists to make visible. */
@@ -7545,7 +8017,7 @@ export class WebGPURenderer {
7545
8017
  /* A frame's resolves take slots in the array's ring; see it for why they cannot share one. */
7546
8018
  this.pointShadowArray?.beginFrame();
7547
8019
  try {
7548
- runPointShadowBakes(pool, lights, this.quality.pointShadowFacesPerFrame, this.quality.liveShadowFacesPerFrame, this.quality.pointShadowRebakeDistance, FACE_COUNT, this.bakeScratch, staticCasters, dynamicCasters, (map, light, casters, maxFaces) => map.bake(light.x, light.y, light.z, light.radius, (view, viewProj) => this.bakeFace(view, viewProj, casters), (layer, face, far, near) => this.resolveFace(layer, face, far, near), this.faceViewProj, light.shadowNear, light.sourceRadius, maxFaces));
8020
+ runPointShadowBakes(pool, lights, this.quality.pointShadowFacesPerFrame, this.quality.liveShadowFacesPerFrame, this.quality.pointShadowRebakeDistance, FACE_COUNT, this.bakeScratch, staticCasters, dynamicCasters, (map, light, casters, maxFaces) => map.bake(light.x, light.y, light.z, light.radius, (view, viewProj) => this.bakeFace(view, viewProj, casters), (layer, face, far, near, x, y, z) => this.resolveFace(layer, face, far, near, x, y, z), this.faceViewProj, light.shadowNear, light.sourceRadius, maxFaces));
7549
8021
  /*
7550
8022
  * **Inside the same encoder and the same try, which is the whole reason the rectangles' bakes
7551
8023
  * ride this call.** Every face is its own render pass, they share one submission, and the
@@ -7553,11 +8025,14 @@ export class WebGPURenderer {
7553
8025
  * encoder, a second submit and a second `shadows` timer slot for work that is the same work.
7554
8026
  */
7555
8027
  this.updateAreaShadows(areaLights, frameDt, staticCasters, dynamicCasters);
8028
+ /* The chain frost reads, once for the round, for the lights whose glass it drew or cleared. */
8029
+ this.pointShadowArray?.glassTint?.recordMips(this.shadowEncoder);
7556
8030
  }
7557
8031
  finally {
7558
8032
  this.depthCullMode = 'back';
7559
8033
  this.shadowDraws.flush();
7560
8034
  this.scatterDepthDraws.flush();
8035
+ this.glassTint?.frag.flush();
7561
8036
  /*
7562
8037
  * **Before the submit, and that ordering is the whole point of the ring.** A write to the
7563
8038
  * queue lands ahead of the encoder's commands whenever it is issued, so uploading per
@@ -7569,6 +8044,9 @@ export class WebGPURenderer {
7569
8044
  this.shadowEncoder = null;
7570
8045
  this.shadowPass = null;
7571
8046
  this.gpuTimer.end();
8047
+ /* An interleaving's old array, copied out of by what was just submitted. */
8048
+ this.retiredPointTexture?.destroy();
8049
+ this.retiredPointTexture = null;
7572
8050
  }
7573
8051
  /*
7574
8052
  * **No rebind here any more.** Twelve cube views meant the bind group named twelve textures,
@@ -7578,6 +8056,10 @@ export class WebGPURenderer {
7578
8056
  * written every frame regardless.
7579
8057
  */
7580
8058
  pool.resolve(this.resolvedPointShadows);
8059
+ /* As the array stores them: 2L once glass has interleaved it. See `strideLayers`. */
8060
+ const stride = this.pointShadowArray.stride;
8061
+ strideLayers(this.resolvedPointShadows.layers, stride);
8062
+ strideLayers(this.resolvedPointShadows.liveLayers, stride);
7581
8063
  }
7582
8064
  /**
7583
8065
  * The rectangles' round, inside the shadow encoder this backend already opened.
@@ -7603,10 +8085,13 @@ export class WebGPURenderer {
7603
8085
  set.release();
7604
8086
  }
7605
8087
  }
7606
- set.update(areaLights, frameDt, this.quality.pointShadowFacesPerFrame, this.quality.liveShadowFacesPerFrame, this.quality.pointShadowRebakeDistance, FACE_COUNT, this.areaBakeScratch, staticCasters, dynamicCasters, (map, light, range, near, casters, maxFaces) => map.bake(light.x, light.y, light.z, range, (view, viewProj) => this.bakeFace(view, viewProj, casters), (layer, face, far, mapNear) => this.resolveFace(layer, face, far, mapNear), this.faceViewProj, near,
8088
+ set.update(areaLights, frameDt, this.quality.pointShadowFacesPerFrame, this.quality.liveShadowFacesPerFrame, this.quality.pointShadowRebakeDistance, FACE_COUNT, this.areaBakeScratch, staticCasters, dynamicCasters, (map, light, range, near, casters, maxFaces) => map.bake(light.x, light.y, light.z, range, (view, viewProj) => this.bakeFace(view, viewProj, casters), (layer, face, far, mapNear, x, y, z) => this.resolveFace(layer, face, far, mapNear, x, y, z), this.faceViewProj, near,
7607
8089
  /* The filter reads the emitter's size, never the bake. See `renderer.ts`'s copy. */
7608
8090
  DEFAULT_SOURCE_RADIUS, maxFaces));
7609
8091
  set.resolve(areaLights, this.resolvedAreaShadows);
8092
+ const stride = this.pointShadowArray?.stride ?? 1;
8093
+ strideLayers(this.resolvedAreaShadows.layer, stride);
8094
+ strideLayers(this.resolvedAreaShadows.liveLayer, stride);
7610
8095
  }
7611
8096
  /**
7612
8097
  * One cubemap face: a depth pass, the casters, done.
@@ -7634,9 +8119,26 @@ export class WebGPURenderer {
7634
8119
  depthStoreOp: 'store',
7635
8120
  },
7636
8121
  });
8122
+ /* A face is an enumeration of its own: its glass is its own. */
8123
+ this.glassCasters.clear();
7637
8124
  casters(this.casterSink);
7638
8125
  this.shadowPass.end();
7639
8126
  this.shadowPass = null;
8127
+ /*
8128
+ * The first face to offer glass interleaves the array, before its opaque depth is resolved:
8129
+ * `PointShadowArray.growForGlass`. The lit pass's groups hold the old array's view, so they
8130
+ * are rebuilt, and the old texture goes once this round's encoder — which copies out of it —
8131
+ * is submitted.
8132
+ */
8133
+ const array = this.pointShadowArray;
8134
+ if (array !== null && this.glassCasters.count > 0 && this.quality.glassShadows !== 'off') {
8135
+ const tintEdge = this.quality.glassShadows === 'half' ? OCTAHEDRAL_EDGE >> 1 : OCTAHEDRAL_EDGE;
8136
+ const old = array.growForGlass(encoder, tintEdge, this.pipelines);
8137
+ if (old !== null) {
8138
+ this.retiredPointTexture = old;
8139
+ this.rebuildFlatBindGroup();
8140
+ }
8141
+ }
7640
8142
  }
7641
8143
  /**
7642
8144
  * One rendered face into its region of the light's octahedral layer.
@@ -7645,12 +8147,64 @@ export class WebGPURenderer {
7645
8147
  * serve every light: the face is consumed before the next one is drawn. See
7646
8148
  * `pointShadowArray.ts` for why the regions are disjoint.
7647
8149
  */
7648
- resolveFace(layer, face, far, near) {
8150
+ resolveFace(layer, face, far, near, x, y, z) {
7649
8151
  const encoder = this.shadowEncoder;
7650
8152
  const array = this.pointShadowArray;
7651
8153
  if (encoder === null || array === null)
7652
8154
  return;
7653
8155
  array.resolve(encoder, layer, face, far, near);
8156
+ const tintTarget = array.glassTint;
8157
+ if (tintTarget === null || this.quality.glassShadows === 'off')
8158
+ return;
8159
+ /*
8160
+ * The face's glass, twice, as `PointShadowMap.bakeGlassFace` draws it on the other backend. A
8161
+ * face that offers none still clears a region a previous owner's glass is in, from scratches
8162
+ * cleared and not drawn into, because a pool slot changes hands with its layer.
8163
+ */
8164
+ const offered = this.glassCasters.count > 0;
8165
+ if (!offered && !tintTarget.held(layer, face))
8166
+ return;
8167
+ this.shadowPass = encoder.beginRenderPass({
8168
+ label: 'pointShadow.glass',
8169
+ colorAttachments: [],
8170
+ depthStencilAttachment: {
8171
+ view: array.scratchView,
8172
+ depthClearValue: 1,
8173
+ depthLoadOp: 'clear',
8174
+ depthStoreOp: 'store',
8175
+ },
8176
+ });
8177
+ if (offered) {
8178
+ this.drawingGlassDepth = true;
8179
+ this.glassCasters.replay(this.glassDepthSink);
8180
+ this.drawingGlassDepth = false;
8181
+ }
8182
+ this.shadowPass.end();
8183
+ this.shadowPass = null;
8184
+ array.resolveGlass(encoder, layer, face, far, near);
8185
+ this.tintPass = encoder.beginRenderPass({
8186
+ label: 'pointShadow.tint',
8187
+ colorAttachments: [
8188
+ {
8189
+ view: tintTarget.scratchView,
8190
+ loadOp: 'clear',
8191
+ clearValue: [1, 1, 1, 1],
8192
+ storeOp: 'store',
8193
+ },
8194
+ ],
8195
+ });
8196
+ if (offered) {
8197
+ this.glassTint ??= new GpuGlassTint(this.surface.device);
8198
+ /* A place, w 1: each pane's angle to the lamp comes from its own position. */
8199
+ this.glassLight[0] = x;
8200
+ this.glassLight[1] = y;
8201
+ this.glassLight[2] = z;
8202
+ this.glassLight[3] = 1;
8203
+ this.glassCasters.replay(this.glassTintSink);
8204
+ }
8205
+ this.tintPass.end();
8206
+ this.tintPass = null;
8207
+ tintTarget.resolve(encoder, layer, face, offered);
7654
8208
  }
7655
8209
  buildFlatBindGroup() {
7656
8210
  return createFlatBindGroup(this.surface.device, this.bindGroupLayout, this.variant, this.perDraw.buffer, this.perFrame.buffer, this.flatTextures);
@@ -7959,8 +8513,10 @@ export class WebGPURenderer {
7959
8513
  * drawn over a reflective floor rather than under it.
7960
8514
  */
7961
8515
  this.runReflections(encoder);
7962
- /* The translucent set, into its two buffers and back over the scene. */
7963
- this.runOit(encoder);
8516
+ /* The translucent set, into its two buffers and back over the scene — after the upscale instead
8517
+ while reconstructing, where `runLate` draws the rest of the blended frame. */
8518
+ if (!this.reconstructing)
8519
+ this.runOit(encoder);
7964
8520
  /*
7965
8521
  * **The medium, after everything that draws and before anything that composites.**
7966
8522
  *
@@ -7982,6 +8538,9 @@ export class WebGPURenderer {
7982
8538
  */
7983
8539
  this.runMotion(encoder);
7984
8540
  this.runReconstruction(encoder);
8541
+ this.runLate(encoder);
8542
+ if (this.reconstructing)
8543
+ this.runOit(encoder);
7985
8544
  const aoStrength = this.quality.ambientOcclusion;
7986
8545
  if (aoStrength > 0 && this.frameProjection !== null)
7987
8546
  this.runOcclusion(encoder);
@@ -8193,19 +8752,23 @@ export class WebGPURenderer {
8193
8752
  label: 'recon.shown',
8194
8753
  size: [outputWidth, outputHeight],
8195
8754
  format: RECON_HISTORY_FORMAT,
8196
- usage: HISTORY_USAGE,
8755
+ /* An attachment too, because the blended draws land on it after the upscale, and a copy
8756
+ source for the panes that refract it. See `runLate`. */
8757
+ usage: HISTORY_USAGE | 0x10 | 0x1, // RENDER_ATTACHMENT | COPY_SRC
8197
8758
  });
8198
8759
  this.reconShownView = this.reconShown.createView();
8199
8760
  this.reconMotion = device.createTexture({
8200
8761
  label: 'recon.motion',
8201
8762
  size: [renderWidth, renderHeight],
8202
8763
  format: RECON_HISTORY_FORMAT,
8203
- /* An attachment as well, because the pass that will write it is a draw. Nothing writes it
8204
- today and nothing needs to clear it either: WebGPU zero-initialises a new texture, and
8205
- a zero in the fourth channel is what tells the resolve to derive the camera's motion. */
8764
+ /* An attachment as well, because the pass that writes it is a draw. It is cleared every
8765
+ frame by that pass, and a zero in the fourth channel is what tells the resolve to derive
8766
+ the camera's motion. */
8206
8767
  usage: 0x4 | 0x10,
8207
8768
  });
8208
8769
  this.reconMotionView = this.reconMotion.createView();
8770
+ this.motionPass ??= new MotionPass(device);
8771
+ this.latePass ??= new LatePass(device);
8209
8772
  this.reconPreviousDepth = device.createTexture({
8210
8773
  label: 'recon.previousDepth',
8211
8774
  size: [renderWidth, renderHeight],
@@ -8321,207 +8884,153 @@ export class WebGPURenderer {
8321
8884
  this.rebuildRushBindGroup();
8322
8885
  }
8323
8886
  /**
8324
- * Remember that this mesh moved, so the motion pass can draw where it was.
8887
+ * Whether a blended draw recorded now lands after the upscale. See `lateRouting.ts`.
8325
8888
  *
8326
- * Silent unless a reconstruction is running: a scene that always states its previous transforms
8327
- * — which is the right thing for a scene to do, the alternative being to know what the renderer
8328
- * is configured as — must cost nothing at all when nobody is going to read them.
8889
+ * Not during the order-independent replay, which owns its pass and issues into it directly.
8329
8890
  */
8330
- recordMotion(mesh, model, previousModel) {
8331
- if (!this.reconstructing)
8332
- return;
8333
- const at = this.motionCount;
8334
- const wanted = (at + 1) * MOTION_DRAW_FLOATS;
8335
- if (this.motionMatrices.length < wanted) {
8336
- /* Doubling, so a scene settles on one allocation rather than one a draw. */
8337
- const grown = new Float32Array(Math.max(wanted, this.motionMatrices.length * 2, 64));
8338
- grown.set(this.motionMatrices);
8339
- this.motionMatrices = grown;
8340
- }
8341
- this.motionMatrices.set(model, at * MOTION_DRAW_FLOATS);
8342
- this.motionMatrices.set(previousModel, at * MOTION_DRAW_FLOATS + 16);
8343
- this.motionMeshes[at] = mesh;
8344
- this.motionCount = at + 1;
8891
+ drawsLate() {
8892
+ return (this.latePass !== null &&
8893
+ this.oitEncoder === null &&
8894
+ routesLate(this.reconstructing, this.reflectionPassActive, this.probePassActive, this.framePresented));
8345
8895
  }
8346
8896
  /**
8347
- * The pipeline the motion pass draws with, built once.
8348
- *
8349
- * **The position buffer alone**, because that is all the shader reads and a mesh keeps one buffer
8350
- * an attribute — so this binds buffer zero and nothing else, whatever else the mesh carries.
8897
+ * The command a blended draw records into: the late pool while reconstructing, the frame graph
8898
+ * otherwise. **Late draws record whatever `quality.frameGraph` says**, since the late replay is
8899
+ * the only moment they can land; everything else keeps the path it has.
8351
8900
  */
8352
- buildMotionPipeline() {
8353
- if (this.motionLayout !== null)
8354
- return;
8355
- const { device } = this.surface;
8356
- this.motionLayout = device.createBindGroupLayout({
8357
- label: 'recon.motion.layout',
8358
- entries: [
8359
- { binding: 0, visibility: 0x1, buffer: { type: 'uniform' } },
8360
- {
8361
- binding: 1,
8362
- visibility: 0x1,
8363
- buffer: { type: 'uniform', hasDynamicOffset: true },
8364
- },
8365
- ],
8366
- });
8367
- this.motionModule = device.createShaderModule({
8368
- label: 'recon.motion',
8369
- code: RECON_MOTION_WGSL,
8370
- });
8371
- this.motionPipelineLayout = device.createPipelineLayout({
8372
- bindGroupLayouts: [this.motionLayout],
8373
- });
8901
+ recordBlended(reads) {
8902
+ const late = this.latePass;
8903
+ if (late !== null && this.drawsLate())
8904
+ return late.take();
8905
+ return this.recordDraw(reads, this.currentTarget());
8374
8906
  }
8375
- /** The pipeline for one vertex stride, built on first sight of it. */
8376
- motionPipelineFor(stride) {
8377
- const held = this.motionPipelines.get(stride);
8378
- if (held !== undefined)
8379
- return held;
8380
- const module = this.motionModule;
8381
- const layout = this.motionPipelineLayout;
8382
- if (module === null || layout === null)
8383
- return null;
8384
- const { device } = this.surface;
8385
- const pipeline = device.createRenderPipeline({
8386
- label: `recon.motion.${String(stride)}`,
8387
- layout,
8388
- vertex: {
8389
- module,
8390
- entryPoint: 'motionVert',
8391
- /* Positions are the layout's first attribute and are never optional, so they are at offset
8392
- zero of every stride this can be handed. */
8393
- buffers: [
8394
- {
8395
- arrayStride: stride,
8396
- attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }],
8397
- },
8398
- ],
8399
- },
8400
- fragment: { module, entryPoint: 'motionFrag', targets: [{ format: MOTION_TARGET_FORMAT }] },
8401
- /*
8402
- * **The scene's own winding**, because this draws the scene's geometry through the scene's
8403
- * clip transform — the product the renderer hands over already carries the negation that
8404
- * `naga` writes into every generated vertex stage, so the faces come out the way they did
8405
- * and a `cw` front face here would draw the inside of every mesh. §3 row 57.
8406
- */
8407
- primitive: { topology: 'triangle-list', cullMode: 'back' },
8408
- depthStencil: {
8409
- format: DEPTH_FORMAT,
8410
- /*
8411
- * **Tested and not written.** The frame's depth is finished; this pass only wants the
8412
- * pixels the scene actually kept, and `DEPTH_COMPARE_EQUAL` is the comparison that admits
8413
- * exactly them — the same transform on the same positions gives the same depth, which is
8414
- * why the rasterisation matrix is handed over whole rather than rebuilt here.
8415
- *
8416
- * **And the equality is not fragile here**, which was worth measuring rather than
8417
- * assuming: this driver contracts multiply-adds (§3 row 107) and two shader modules can
8418
- * contract a matrix product differently, so an equality across them could have admitted
8419
- * only some of a surface. Run with `greater-equal` instead, the ghost measured the same
8420
- * to the pixel — 5,942 either way — so nothing is being dropped.
8421
- */
8422
- depthWriteEnabled: false,
8423
- depthCompare: DEPTH_COMPARE_EQUAL,
8424
- },
8425
- });
8426
- this.motionPipelines.set(stride, pipeline);
8427
- return pipeline;
8907
+ /** The pipelines a blended draw is built for: the reconstructed picture's while late. */
8908
+ blendedPipelines() {
8909
+ return this.drawsLate() ? this.latePipelines : this.targetPipelines();
8428
8910
  }
8429
8911
  /**
8430
- * Where every mover was last frame, into the motion target.
8431
- *
8432
- * **After everything that draws and before the resolve reads it.** The frame's depth has to be
8433
- * finished for the equality test to mean anything, and the resolve is what consumes the result.
8912
+ * The blended draws of a reconstructing frame, over the reconstructed picture at the output size
8913
+ * and unjittered, against the render's depth taken up to that size. Before occlusion, bloom and
8914
+ * the grade, so they are graded exactly as they were when they were drawn into the scene.
8434
8915
  */
8435
- runMotion(encoder) {
8436
- const target = this.reconMotionView;
8916
+ runLate(encoder) {
8917
+ const late = this.latePass;
8918
+ const shown = this.reconShownView;
8437
8919
  const depth = this.depthView;
8438
- if (!this.reconstructing || target === null || depth === null)
8920
+ if (!this.reconstructing || late === null || shown === null || depth === null)
8439
8921
  return;
8440
- const { device } = this.surface;
8441
- this.buildMotionPipeline();
8442
- const layout = this.motionLayout;
8443
- if (layout === null)
8922
+ const oitSet = this.oitActive && this.translucentQueue.length > 0;
8923
+ if (late.pending === 0 && !oitSet)
8924
+ return;
8925
+ const width = Math.max(1, this.surface.canvas.width);
8926
+ const height = Math.max(1, this.surface.canvas.height);
8927
+ /* The order-independent set tests against this depth too, so it is made for either. */
8928
+ late.upscale(encoder, depth, this.renderWidth, this.renderHeight, width, height);
8929
+ if (this.lateRefractWanted && this.refractSnapshot !== null && this.reconShown !== null) {
8930
+ encoder.copyTextureToTexture({ texture: this.reconShown }, { texture: this.refractSnapshot }, [this.refractSnapshot.width, this.refractSnapshot.height]);
8931
+ this.refractMips?.record(encoder);
8932
+ this.refractSnapshotTaken = true;
8933
+ }
8934
+ this.lateRefractWanted = false;
8935
+ if (late.pending === 0)
8444
8936
  return;
8445
- /*
8446
- * **Cleared every frame, even when nothing moved.** A texel left from last frame is a flag
8447
- * saying "a draw wrote this", and the resolve would take a motion belonging to a surface that
8448
- * is no longer there. The clear is the pass's own load operation, so an empty queue still runs
8449
- * it — which is why this is not returned from before the pass is opened.
8450
- */
8451
8937
  const pass = encoder.beginRenderPass({
8452
- label: 'recon.motion',
8938
+ label: 'recon.late',
8453
8939
  timestampWrites: this.gpuTimer.writesFor(),
8454
- colorAttachments: [
8455
- { view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
8456
- ],
8940
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
8457
8941
  depthStencilAttachment: {
8458
- view: depth,
8942
+ view: late.depthView(width, height),
8459
8943
  depthLoadOp: 'load',
8944
+ /* Kept: the order-independent set replays against it after this pass. */
8460
8945
  depthStoreOp: 'store',
8461
- depthReadOnly: false,
8462
8946
  },
8463
8947
  });
8464
- const count = this.motionCount;
8465
- if (count > 0) {
8466
- const bytes = count * MOTION_DRAW_STRIDE;
8467
- if (this.motionDrawCapacity < count) {
8468
- this.motionDrawBuffer?.destroy();
8469
- this.motionDrawCapacity = Math.max(count, this.motionDrawCapacity * 2, 16);
8470
- this.motionDrawBuffer = device.createBuffer({
8471
- label: 'recon.motion.draws',
8472
- size: this.motionDrawCapacity * MOTION_DRAW_STRIDE,
8473
- usage: 0x40 | 0x8, // UNIFORM | COPY_DST
8474
- });
8475
- this.motionDrawStaging = new Uint8Array(this.motionDrawCapacity * MOTION_DRAW_STRIDE);
8476
- this.motionGroup = null;
8477
- }
8478
- this.motionFrameBuffer ??= device.createBuffer({
8479
- label: 'recon.motion.frame',
8480
- size: MOTION_FRAME_FLOATS * 4,
8481
- usage: 0x40 | 0x8,
8482
- });
8483
- this.motionGroup ??= device.createBindGroup({
8484
- label: 'recon.motion.group',
8485
- layout,
8486
- entries: [
8487
- { binding: 0, resource: { buffer: this.motionFrameBuffer } },
8488
- {
8489
- binding: 1,
8490
- resource: { buffer: this.motionDrawBuffer, size: MOTION_DRAW_STRIDE },
8491
- },
8492
- ],
8493
- });
8494
- /* The scene's clip transform, with the generated stages' y negation folded in. */
8495
- mat4.multiply(this.motionRaster, MOTION_CLIP_FLIP, this.viewProj ?? this.correctedViewProj);
8496
- this.motionFrameStaging.set(this.motionRaster, 0);
8497
- this.motionFrameStaging.set(this.frameRawViewProj, 16);
8498
- this.motionFrameStaging.set(this.reconPreviousRawViewProj, 32);
8499
- device.queue.writeBuffer(this.motionFrameBuffer, 0, this.motionFrameStaging);
8500
- const draws = new Float32Array(this.motionDrawStaging.buffer);
8501
- for (let i = 0; i < count; i += 1) {
8502
- draws.set(this.motionMatrices.subarray(i * MOTION_DRAW_FLOATS, (i + 1) * MOTION_DRAW_FLOATS), (i * MOTION_DRAW_STRIDE) / 4);
8503
- }
8504
- device.queue.writeBuffer(this.motionDrawBuffer, 0, this.motionDrawStaging, 0, bytes);
8505
- let bound = null;
8506
- for (let i = 0; i < count; i += 1) {
8507
- const mesh = this.motionMeshes[i];
8508
- if (mesh === undefined)
8509
- continue;
8510
- const pipeline = this.motionPipelineFor(mesh.vertexStride);
8511
- if (pipeline === null)
8512
- continue;
8513
- if (pipeline !== bound) {
8514
- pass.setPipeline(pipeline);
8515
- bound = pipeline;
8516
- }
8517
- pass.setBindGroup(0, this.motionGroup, [i * MOTION_DRAW_STRIDE]);
8518
- pass.setVertexBuffer(0, mesh.vertexBuffers[0]);
8519
- pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
8520
- pass.drawIndexed(mesh.indexCount);
8521
- }
8522
- }
8948
+ late.replay(pass, this.issueBound);
8523
8949
  pass.end();
8524
8950
  }
8951
+ /** Whether this draw lands in the reconstructed picture. See `lateRouting.ts`. */
8952
+ tracksMotionNow() {
8953
+ return tracksMotion(this.reconstructing, this.reflectionPassActive, this.probePassActive, this.framePresented);
8954
+ }
8955
+ /**
8956
+ * What this draw's motion is, recorded for the motion pass.
8957
+ *
8958
+ * A mesh rewritten this frame carries its vertex motion whatever else the draw says — under last
8959
+ * frame's model where a mover or a matrix knows it, under this frame's where nothing does. A
8960
+ * mover otherwise carries a rigid draw's last model or a skinned draw's last pose; a matrix
8961
+ * carries a rigid draw's. A draw that says nothing and moved no vertex takes the camera's motion.
8962
+ */
8963
+ recordDrawMotion(mesh, model, previous) {
8964
+ const dynamic = mesh.motion;
8965
+ const rows = dynamic?.previous ?? null;
8966
+ const rewritten = rows !== null && dynamic !== null && dynamic.changed === this.moverFrame;
8967
+ if (previous === null) {
8968
+ if (rewritten)
8969
+ this.motionPass?.recordDynamic(mesh, model, model, rows);
8970
+ return;
8971
+ }
8972
+ if (!isMover(previous)) {
8973
+ if (rewritten)
8974
+ this.motionPass?.recordDynamic(mesh, model, previous, rows);
8975
+ else
8976
+ this.motionPass?.recordRigid(mesh, model, previous);
8977
+ return;
8978
+ }
8979
+ /* A rig is only followed where the draw really skins: a palette set, a mesh with joints. */
8980
+ const skinned = !rewritten && this.skinPaletteSlot >= 0 && mesh.isSkinned && mesh.skinOffsets !== null;
8981
+ const verdict = advanceMover(previous, this.moverRenderer, this.moverFrame, model, skinned ? this.skinPaletteData : null);
8982
+ if (verdict === MOVER_TWICE) {
8983
+ this.warnMoverTwice();
8984
+ return;
8985
+ }
8986
+ if (rewritten) {
8987
+ const was = verdict === MOVER_MOTION ? previous.previousModel : model;
8988
+ this.motionPass?.recordDynamic(mesh, model, was, rows);
8989
+ return;
8990
+ }
8991
+ if (verdict !== MOVER_MOTION)
8992
+ return;
8993
+ if (skinned)
8994
+ this.recordSkinnedMotion(mesh, model, previous);
8995
+ else
8996
+ this.motionPass?.recordRigid(mesh, model, previous.previousModel);
8997
+ }
8998
+ /**
8999
+ * A skinned mover's motion: this frame's palette is the draw's own slot, and last frame's takes a
9000
+ * slot of its own, like any palette a draw binds — so it is uploaded once and nothing overwrites
9001
+ * it before the motion pass reads it. See `SkinPaletteRing`.
9002
+ */
9003
+ recordSkinnedMotion(mesh, model, mover) {
9004
+ const now = this.skinPalettes.view(this.skinPaletteSlot);
9005
+ const slot = this.skinPalettes.take(this.surface.device, mover.previousPalette);
9006
+ const was = slot === null ? null : this.skinPalettes.view(slot);
9007
+ if (now === null || was === null)
9008
+ return;
9009
+ this.motionPass?.recordSkinned(mesh, model, mover.previousModel, now, was);
9010
+ }
9011
+ warnMoverTwice() {
9012
+ if (this.warnedMoverTwice)
9013
+ return;
9014
+ this.warnedMoverTwice = true;
9015
+ console.warn('WebGPU: a Mover was drawn twice in one frame. A mover is one object; the second draw ' +
9016
+ 'records no motion. Give each moving object its own `createMover()`.');
9017
+ }
9018
+ /**
9019
+ * Where every mover was last frame, into the motion target. See `MotionPass`.
9020
+ *
9021
+ * **After everything that draws and before the resolve reads it.** The frame's depth has to be
9022
+ * finished for the pass's equality test to mean anything, and the resolve is what consumes it.
9023
+ */
9024
+ runMotion(encoder) {
9025
+ const target = this.reconMotionView;
9026
+ const depth = this.depthView;
9027
+ const motion = this.motionPass;
9028
+ if (!this.reconstructing || target === null || depth === null || motion === null)
9029
+ return;
9030
+ /* The scene's clip transform, with the generated stages' y negation folded in. */
9031
+ mat4.multiply(this.motionRaster, MOTION_CLIP_FLIP, this.viewProj ?? this.correctedViewProj);
9032
+ motion.run(encoder, target, depth, this.motionRaster, this.frameRawViewProj, this.reconPreviousRawViewProj, this.gpuTimer.writesFor());
9033
+ }
8525
9034
  /**
8526
9035
  * DriftTR tier 0: the jittered render at the render size into the picture at the output size.
8527
9036
  *
@@ -8904,9 +9413,12 @@ export class WebGPURenderer {
8904
9413
  return;
8905
9414
  const accum = this.oitAccumView;
8906
9415
  const reveal = this.oitRevealView;
8907
- const scene = this.sceneColorView;
9416
+ /* After the upscale while reconstructing: over the reconstructed picture, against its depth. */
9417
+ const scene = this.reconstructing ? this.reconShownView : this.sceneColorView;
8908
9418
  const group = this.oitResolveGroup;
8909
- const depth = this.depthView;
9419
+ const depth = this.reconstructing
9420
+ ? (this.latePass?.depthView(Math.max(1, this.surface.canvas.width), Math.max(1, this.surface.canvas.height)) ?? null)
9421
+ : this.depthView;
8910
9422
  if (accum === null || reveal === null || scene === null || group === null || depth === null) {
8911
9423
  return;
8912
9424
  }
@@ -8920,12 +9432,13 @@ export class WebGPURenderer {
8920
9432
  */
8921
9433
  if (this.translucentQueue.refracts && !this.refractSnapshotTaken) {
8922
9434
  const snapshot = this.refractSnapshot;
8923
- const source = this.sceneColor;
9435
+ const source = this.reconstructing ? this.reconShown : this.sceneColor;
8924
9436
  if (snapshot !== null && source !== null) {
8925
9437
  encoder.copyTextureToTexture({ texture: source }, { texture: snapshot }, [
8926
9438
  snapshot.width,
8927
9439
  snapshot.height,
8928
9440
  ]);
9441
+ this.refractMips?.record(encoder);
8929
9442
  this.refractSnapshotTaken = true;
8930
9443
  }
8931
9444
  }
@@ -8964,6 +9477,7 @@ export class WebGPURenderer {
8964
9477
  options.refraction = draw.refraction;
8965
9478
  options.refractTint = draw.refractTint;
8966
9479
  options.thicknessM = draw.thicknessM;
9480
+ options.glass = draw.glass ?? undefined;
8967
9481
  this.drawTranslucentMesh(draw.mesh, draw.model, draw.opacity, options);
8968
9482
  });
8969
9483
  this.oitReplaying = false;
@@ -8987,7 +9501,7 @@ export class WebGPURenderer {
8987
9501
  label: 'post.oit.resolve',
8988
9502
  colorAttachments: [{ view: scene, loadOp: 'load', storeOp: 'store' }],
8989
9503
  });
8990
- resolve.setPipeline(postPipeline(this.pipelines, device, this.oitResolveLayout, 'post.oitResolve', OIT_RESOLVE_FRAG_WGSL, sceneColorFormat(this.quality.hdrScene), OIT_COMPOSITE_BLEND));
9504
+ resolve.setPipeline(postPipeline(this.pipelines, device, this.oitResolveLayout, this.reconstructing ? 'post.oitResolve.recon' : 'post.oitResolve', OIT_RESOLVE_FRAG_WGSL, this.reconstructing ? RECON_HISTORY_FORMAT : sceneColorFormat(this.quality.hdrScene), OIT_COMPOSITE_BLEND));
8991
9505
  resolve.setBindGroup(0, group);
8992
9506
  resolve.draw(3);
8993
9507
  resolve.end();
@@ -9294,18 +9808,14 @@ export class WebGPURenderer {
9294
9808
  * the drawing buffer: `ensureComposite` calls this again whenever it rebuilds that target.
9295
9809
  */
9296
9810
  buildLightVolumeBindGroup() {
9297
- return createLightVolumeBindGroup(this.surface.device, this.lightVolumeLayout, this.lightVolumeVariant, this.lightVolumeVertices.buffer, this.lightVolumeFragments.buffer, (name) => name === 'uStaticShadowMap'
9298
- ? { view: this.shadowView, sampler: this.volumeShadowSampler }
9299
- : name === 'uPeeledShadowMap' && this.peelView !== null
9300
- ? { view: this.peelView, sampler: this.volumeShadowSampler }
9301
- : name === 'uDynamicShadowMap' && this.dynamicView !== null
9302
- ? { view: this.dynamicView, sampler: this.volumeShadowSampler }
9303
- : /* The frame's own depth, snapshotted before the first volume of the frame draws.
9304
- Null under a profile with no composite, and then `uSceneDepthEnabled` is 0 and
9305
- the stand-in is never sampled. */
9306
- name === 'uSceneDepth' && this.resolvedDepthView !== null
9307
- ? { view: this.resolvedDepthView, sampler: this.postDepthSampler }
9308
- : this.volumeBlank);
9811
+ return createLightVolumeBindGroup(this.surface.device, this.lightVolumeLayout, this.lightVolumeVariant, this.lightVolumeVertices.buffer, this.lightVolumeFragments.buffer, (name) => name === 'uSunShadows'
9812
+ ? { view: this.sunArrayView, sampler: this.volumeShadowSampler }
9813
+ : /* The frame's own depth, snapshotted before the first volume of the frame draws.
9814
+ Null under a profile with no composite, and then `uSceneDepthEnabled` is 0 and
9815
+ the stand-in is never sampled. */
9816
+ name === 'uSceneDepth' && this.resolvedDepthView !== null
9817
+ ? { view: this.resolvedDepthView, sampler: this.postDepthSampler }
9818
+ : this.volumeBlank);
9309
9819
  }
9310
9820
  /**
9311
9821
  * Copy the colour the frame has drawn so far, so a refracting draw can read it.
@@ -9331,6 +9841,12 @@ export class WebGPURenderer {
9331
9841
  return false;
9332
9842
  if (this.refractSnapshotTaken)
9333
9843
  return true;
9844
+ /* A late draw refracts the reconstructed picture, which does not exist until the resolve has
9845
+ run; `runLate` takes the copy then, once, before any late draw is replayed. */
9846
+ if (this.drawsLate()) {
9847
+ this.lateRefractWanted = true;
9848
+ return true;
9849
+ }
9334
9850
  /* The replay of an order-independent set runs after the scene's pass has ended, and `runOit`
9335
9851
  took the copy for it before it began; there is no pass here to end and reopen. */
9336
9852
  if (this.oitReplaying)
@@ -9372,6 +9888,8 @@ export class WebGPURenderer {
9372
9888
  snapshot.width,
9373
9889
  snapshot.height,
9374
9890
  ]);
9891
+ /* The chain, between the passes, where nothing is attached: what frosted glass reads. */
9892
+ this.refractMips?.record(encoder);
9375
9893
  const multisampled = this.compositeMsaa() !== null;
9376
9894
  this.pass = encoder.beginRenderPass({
9377
9895
  label: 'refract.snapshotTaken',
@@ -9723,7 +10241,13 @@ export class WebGPURenderer {
9723
10241
  if (verb !== VERB_DRAW)
9724
10242
  return;
9725
10243
  const command = this.commands.commands[nodeState(this.arena, at)];
9726
- if (command === undefined || command.pipeline === null || command.bindGroup === null)
10244
+ if (command === undefined)
10245
+ return;
10246
+ this.issueCommand(pass, command);
10247
+ }
10248
+ /** One recorded draw, issued into an open pass. Shared by the graph's replay and `runLate`. */
10249
+ issueCommand(pass, command) {
10250
+ if (command.pipeline === null || command.bindGroup === null)
9727
10251
  return;
9728
10252
  pass.setPipeline(command.pipeline);
9729
10253
  if (command.offsetCount === 2) {
@@ -9972,6 +10496,7 @@ export class WebGPURenderer {
9972
10496
  this.encoder = null;
9973
10497
  }
9974
10498
  endFrame() {
10499
+ this.inFrame = false;
9975
10500
  if (this.surface.lost || this.encoder === null)
9976
10501
  return;
9977
10502
  /*
@@ -11102,6 +11627,10 @@ export class WebGPURenderer {
11102
11627
  *
11103
11628
  * What it gives up is one frame of history: the frame after a cut has no blur and no temporal
11104
11629
  * blend, as the first frame of a session has none. Nothing else is reset.
11630
+ *
11631
+ * **A reconstructing renderer depends on it**: its history is the last shot's, and without this
11632
+ * call a cut blends that shot into the first frames of the next. Nothing detects a cut on the
11633
+ * caller's behalf, deliberately — a fast pan and a cut look alike from here.
11105
11634
  */
11106
11635
  cameraCut() {
11107
11636
  this.hasPreviousView = false;
@@ -11277,9 +11806,9 @@ export class WebGPURenderer {
11277
11806
  f[atF('uFogFar')] = medium.fogFar;
11278
11807
  this.causticsVerts.writeBlock(vertexSlot, this.causticsVertBlock);
11279
11808
  this.causticsFrags.writeBlock(fragmentSlot, this.causticsFragInts);
11280
- const causticsPipe = causticsPipeline(this.pipelines, this.surface.device, this.causticsLayout);
11809
+ const causticsPipe = causticsPipeline(this.blendedPipelines(), this.surface.device, this.causticsLayout);
11281
11810
  /* The pattern is generated in the shader from the two uniform blocks; nothing is sampled. */
11282
- const causticsCommand = this.recordDraw(0, this.currentTarget());
11811
+ const causticsCommand = this.recordBlended(0);
11283
11812
  if (causticsCommand !== null) {
11284
11813
  causticsCommand.pipeline = causticsPipe;
11285
11814
  causticsCommand.bindGroup = this.causticsBindGroup;
@@ -12158,6 +12687,33 @@ export class WebGPURenderer {
12158
12687
  * The occlusion and bloom targets are half size. Both are blurs read back at full size, so a
12159
12688
  * full-resolution intermediate buys nothing a bilinear fetch does not already give.
12160
12689
  */
12690
+ /**
12691
+ * The medium's march group, holding the shadow views it reads — built with the composite, and
12692
+ * again when the sun's texture grows its glass layers and every view of the old one is stale.
12693
+ *
12694
+ * **The stand-in has to read as unoccluded and not as absent**, which is `emptyTexture.ts`'s
12695
+ * point and `createLightVolumeBindGroup`'s: `occlusion()` calls any stored depth below the
12696
+ * receiver's a blocker — so a black stand-in would put the whole medium in shadow, which is a
12697
+ * picture rather than an error. `volumeBlank` is the white one-texel texture built for this.
12698
+ */
12699
+ buildMediumBindGroup() {
12700
+ if (this.resolvedDepthView === null)
12701
+ return null;
12702
+ return this.surface.device.createBindGroup({
12703
+ label: 'post.mediumBindGroup',
12704
+ layout: this.mediumLayout,
12705
+ entries: [
12706
+ {
12707
+ binding: MEDIUM_UNIFORMS,
12708
+ resource: { buffer: this.mediumUniforms, size: MEDIUM_FRAG_SIZE },
12709
+ },
12710
+ { binding: MEDIUM_TEXTURES.uDepth.texture, resource: this.resolvedDepthView },
12711
+ { binding: MEDIUM_TEXTURES.uDepth.sampler, resource: this.postDepthSampler },
12712
+ { binding: MEDIUM_TEXTURES.uSunShadows.texture, resource: this.sunArrayView },
12713
+ { binding: MEDIUM_TEXTURES.uSunShadows.sampler, resource: this.volumeShadowSampler },
12714
+ ],
12715
+ });
12716
+ }
12161
12717
  ensureComposite(width, height) {
12162
12718
  if (!this.hasComposite)
12163
12719
  return;
@@ -12213,13 +12769,29 @@ export class WebGPURenderer {
12213
12769
  * the allocation the house rules are about. A frame that never refracts pays the memory and
12214
12770
  * takes no copy.
12215
12771
  */
12772
+ /*
12773
+ * **At the output size and the reconstructed picture's format while reconstructing**, because
12774
+ * every refracting draw is then blended, drawn after the upscale, and reads the reconstructed
12775
+ * picture rather than the render. See `runLate`.
12776
+ */
12777
+ const reconstructed = this.reconstructionWanted;
12216
12778
  this.refractSnapshot = device.createTexture({
12217
12779
  label: 'refract.snapshot',
12218
- size: [width, height],
12219
- format,
12220
- usage: 0x4 | 0x2,
12780
+ size: reconstructed
12781
+ ? [Math.max(1, this.surface.canvas.width), Math.max(1, this.surface.canvas.height)]
12782
+ : [width, height],
12783
+ format: reconstructed ? RECON_HISTORY_FORMAT : format,
12784
+ /*
12785
+ * **Every level, so glass can read a blurrier one**, and a render attachment so `SnapshotMips`
12786
+ * can fill them. Plain refraction reads level 0, as it always has. See `glass.ts`.
12787
+ */
12788
+ mipLevelCount: Math.floor(Math.log2(reconstructed
12789
+ ? Math.max(1, this.surface.canvas.width, this.surface.canvas.height)
12790
+ : Math.max(1, width, height))) + 1,
12791
+ usage: 0x4 | 0x2 | 0x10, // TEXTURE_BINDING | COPY_DST | RENDER_ATTACHMENT
12221
12792
  });
12222
12793
  this.refractSnapshotView = this.refractSnapshot.createView();
12794
+ this.refractMips = new SnapshotMips(device, this.pipelines, this.refractSnapshot, reconstructed ? RECON_HISTORY_FORMAT : format);
12223
12795
  /*
12224
12796
  * **Every cached flat bind group is stale the moment this view changes**, and the failure is
12225
12797
  * silent: `flatTextures` resolves `uRefractScene` to the one-texel white stand-in when there is
@@ -12359,10 +12931,14 @@ export class WebGPURenderer {
12359
12931
  * that range — a step of 1/255 in how much scene shows through, below what a viewer can see
12360
12932
  * against a blended surface.
12361
12933
  */
12934
+ /* At the output size while reconstructing, where the set is replayed after the upscale. */
12935
+ const oitSize = this.reconstructionWanted
12936
+ ? [Math.max(1, this.surface.canvas.width), Math.max(1, this.surface.canvas.height)]
12937
+ : [width, height];
12362
12938
  this.oitAccum?.destroy();
12363
12939
  this.oitAccum = device.createTexture({
12364
12940
  label: 'post.oitAccum',
12365
- size: [width, height],
12941
+ size: oitSize,
12366
12942
  format: 'rgba16float',
12367
12943
  usage: USAGE,
12368
12944
  });
@@ -12370,7 +12946,7 @@ export class WebGPURenderer {
12370
12946
  this.oitReveal?.destroy();
12371
12947
  this.oitReveal = device.createTexture({
12372
12948
  label: 'post.oitReveal',
12373
- size: [width, height],
12949
+ size: oitSize,
12374
12950
  format: 'r8unorm',
12375
12951
  usage: USAGE,
12376
12952
  });
@@ -12469,39 +13045,7 @@ export class WebGPURenderer {
12469
13045
  ...entry(AO_TEXTURES.uDepth, this.resolvedDepthView, true),
12470
13046
  ],
12471
13047
  });
12472
- /*
12473
- * The medium's two groups, holding the shadow views the march reads.
12474
- *
12475
- * **The stand-in has to read as unoccluded and not as absent**, which is `emptyTexture.ts`'s
12476
- * point and `createLightVolumeBindGroup`'s: the dynamic layer is sampled unconditionally, and
12477
- * `occlusion()` calls any stored depth below the receiver's a blocker — so a black stand-in
12478
- * would put the whole medium in shadow, which is a picture rather than an error. `volumeBlank`
12479
- * is the white one-texel texture already built for exactly this.
12480
- */
12481
- const shadowPair = (b, view) => [
12482
- { binding: b.texture, resource: view ?? this.volumeBlank.view },
12483
- {
12484
- binding: b.sampler,
12485
- resource: view === null ? this.volumeBlank.sampler : this.volumeShadowSampler,
12486
- },
12487
- ];
12488
- this.mediumBindGroup =
12489
- this.resolvedDepthView === null
12490
- ? null
12491
- : device.createBindGroup({
12492
- label: 'post.mediumBindGroup',
12493
- layout: this.mediumLayout,
12494
- entries: [
12495
- {
12496
- binding: MEDIUM_UNIFORMS,
12497
- resource: { buffer: this.mediumUniforms, size: MEDIUM_FRAG_SIZE },
12498
- },
12499
- ...entry(MEDIUM_TEXTURES.uDepth, this.resolvedDepthView, true),
12500
- ...shadowPair(MEDIUM_TEXTURES.uStaticShadowMap, this.shadowView),
12501
- ...shadowPair(MEDIUM_TEXTURES.uPeeledShadowMap, this.peelView),
12502
- ...shadowPair(MEDIUM_TEXTURES.uDynamicShadowMap, this.dynamicView),
12503
- ],
12504
- });
13048
+ this.mediumBindGroup = this.buildMediumBindGroup();
12505
13049
  this.mediumUpsampleGroup =
12506
13050
  this.resolvedDepthView === null || this.mediumTargetView === null
12507
13051
  ? null
@@ -12684,6 +13228,10 @@ export class WebGPURenderer {
12684
13228
  * the 2026-08-13 rule asks for — one decision, bound twice.
12685
13229
  */
12686
13230
  dispose() {
13231
+ this.motionPass?.dispose();
13232
+ this.motionPass = null;
13233
+ this.latePass?.dispose();
13234
+ this.latePass = null;
12687
13235
  for (const texture of this.retiredTextures)
12688
13236
  texture.destroy();
12689
13237
  this.retiredTextures.length = 0;
@@ -12700,6 +13248,12 @@ export class WebGPURenderer {
12700
13248
  this.fieldComposer = null;
12701
13249
  this.driftIndex?.destroy();
12702
13250
  this.driftAtlas?.destroy();
13251
+ this.sunTint?.texture.destroy();
13252
+ this.sunTint = null;
13253
+ this.glassTint?.dispose();
13254
+ this.glassTint = null;
13255
+ this.retiredSunTexture?.destroy();
13256
+ this.retiredSunTexture = null;
12703
13257
  this.pass = null;
12704
13258
  this.encoder = null;
12705
13259
  this.skinPalettes.dispose();