@driftengine/core 4.4.1 → 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 (218) hide show
  1. package/CHANGELOG.json +72 -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 +82 -8
  9. package/dist/render/backend/webgl2/renderer.js +373 -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 +21 -15
  34. package/dist/render/backend/webgpu/postPass.js +17 -4
  35. package/dist/render/backend/webgpu/renderer.d.ts +181 -54
  36. package/dist/render/backend/webgpu/renderer.js +1011 -394
  37. package/dist/render/backend/webgpu/scatterPass.js +2 -1
  38. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  39. package/dist/render/backend/webgpu/skinPaletteRing.js +4 -0
  40. package/dist/render/backend/webgpu/snapshotMips.d.ts +29 -0
  41. package/dist/render/backend/webgpu/snapshotMips.js +52 -0
  42. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +5 -0
  43. package/dist/render/backend/webgpu/surfaceTexturePass.js +1 -1
  44. package/dist/render/flameLight.js +15 -2
  45. package/dist/render/glass.d.ts +45 -0
  46. package/dist/render/glass.js +39 -0
  47. package/dist/render/glassCasters.d.ts +40 -0
  48. package/dist/render/glassCasters.js +84 -0
  49. package/dist/render/glassShadow.d.ts +78 -0
  50. package/dist/render/glassShadow.js +125 -0
  51. package/dist/render/globalMediumPass.d.ts +2 -3
  52. package/dist/render/globalMediumPass.js +2 -8
  53. package/dist/render/gpudriven/lit.d.ts +12 -0
  54. package/dist/render/gpudriven/lit.js +23 -0
  55. package/dist/render/gpudriven/materialTable.d.ts +15 -5
  56. package/dist/render/gpudriven/materialTable.js +26 -6
  57. package/dist/render/gpudriven/pipeline.d.ts +1 -1
  58. package/dist/render/gpudriven/pipeline.js +4 -3
  59. package/dist/render/gpudriven/sunGlass.d.ts +35 -0
  60. package/dist/render/gpudriven/sunGlass.js +89 -0
  61. package/dist/render/lightBudget.d.ts +17 -5
  62. package/dist/render/lightBudget.js +17 -5
  63. package/dist/render/pointGlassTint.d.ts +59 -0
  64. package/dist/render/pointGlassTint.js +198 -0
  65. package/dist/render/pointShadowArray.d.ts +56 -3
  66. package/dist/render/pointShadowArray.js +127 -16
  67. package/dist/render/pointShadowBudget.js +51 -41
  68. package/dist/render/pointShadowMap.d.ts +23 -1
  69. package/dist/render/pointShadowMap.js +27 -1
  70. package/dist/render/recon/changeFrames.d.ts +15 -0
  71. package/dist/render/recon/changeFrames.js +21 -0
  72. package/dist/render/recon/disocclusion.d.ts +36 -3
  73. package/dist/render/recon/disocclusion.js +56 -4
  74. package/dist/render/recon/lateRouting.d.ts +14 -0
  75. package/dist/render/recon/lateRouting.js +18 -0
  76. package/dist/render/recon/mover.d.ts +57 -0
  77. package/dist/render/recon/mover.js +86 -0
  78. package/dist/render/recon/resolve.js +95 -12
  79. package/dist/render/renderQuality.d.ts +35 -0
  80. package/dist/render/renderQuality.js +7 -0
  81. package/dist/render/rendererHarness.js +12 -0
  82. package/dist/render/sceneTarget.d.ts +9 -1
  83. package/dist/render/sceneTarget.js +59 -13
  84. package/dist/render/shaders/depth.d.ts +16 -1
  85. package/dist/render/shaders/depth.js +85 -9
  86. package/dist/render/shaders/flat/directionalShadow.js +91 -6
  87. package/dist/render/shaders/flat/glassTint.d.ts +6 -0
  88. package/dist/render/shaders/flat/glassTint.js +25 -0
  89. package/dist/render/shaders/flat/index.d.ts +13 -0
  90. package/dist/render/shaders/flat/index.js +4 -0
  91. package/dist/render/shaders/flat/lampGlass.d.ts +1 -0
  92. package/dist/render/shaders/flat/lampGlass.js +183 -0
  93. package/dist/render/shaders/flat/main.d.ts +1 -1
  94. package/dist/render/shaders/flat/main.js +115 -15
  95. package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
  96. package/dist/render/shaders/flat/pointShadow.js +2 -0
  97. package/dist/render/shaders/flat/preamble.js +21 -6
  98. package/dist/render/shaders/generated/depth.wgsl.d.ts +227 -4
  99. package/dist/render/shaders/generated/depth.wgsl.js +227 -4
  100. package/dist/render/shaders/generated/flat.wgsl.d.ts +360 -312
  101. package/dist/render/shaders/generated/flat.wgsl.js +376 -328
  102. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +3 -13
  103. package/dist/render/shaders/generated/globalMedium.wgsl.js +3 -13
  104. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +3 -13
  105. package/dist/render/shaders/generated/lightVolume.wgsl.js +4 -14
  106. package/dist/render/shaders/generated/octahedralResolve.wgsl.d.ts +29 -0
  107. package/dist/render/shaders/generated/octahedralResolve.wgsl.js +29 -0
  108. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +31 -6
  109. package/dist/render/shaders/generated/temporalResolve.wgsl.js +31 -6
  110. package/dist/render/shaders/globalMedium.js +6 -6
  111. package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +7 -0
  112. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +86 -1
  113. package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +1 -1
  114. package/dist/render/shaders/gpudriven/cull.wgsl.js +11 -2
  115. package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +1 -1
  116. package/dist/render/shaders/gpudriven/materialTable.wgsl.js +3 -1
  117. package/dist/render/shaders/gpudriven/shade.wgsl.js +50 -1
  118. package/dist/render/shaders/gpudriven/sunGlass.wgsl.d.ts +9 -0
  119. package/dist/render/shaders/gpudriven/sunGlass.wgsl.js +133 -0
  120. package/dist/render/shaders/lightVolume.js +6 -6
  121. package/dist/render/shaders/octahedralResolve.d.ts +8 -0
  122. package/dist/render/shaders/octahedralResolve.js +48 -17
  123. package/dist/render/shaders/recon/lateDepth.wgsl.d.ts +1 -0
  124. package/dist/render/shaders/recon/lateDepth.wgsl.js +44 -0
  125. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  126. package/dist/render/shaders/recon/motion.wgsl.js +84 -0
  127. package/dist/render/shaders/recon/resolve.wgsl.d.ts +1 -1
  128. package/dist/render/shaders/recon/resolve.wgsl.js +112 -8
  129. package/dist/render/shaders/temporalResolve.js +133 -5
  130. package/dist/render/shadowCasters.d.ts +20 -1
  131. package/dist/render/shadowCasters.js +15 -1
  132. package/dist/render/shadowMap.d.ts +54 -5
  133. package/dist/render/shadowMap.js +190 -36
  134. package/dist/render/sunGlassTint.d.ts +25 -0
  135. package/dist/render/sunGlassTint.js +79 -0
  136. package/dist/render/temporalAa.d.ts +145 -0
  137. package/dist/render/temporalAa.js +190 -0
  138. package/dist/render/temporalPass.d.ts +16 -14
  139. package/dist/render/temporalPass.js +59 -14
  140. package/dist/render/translucentQueue.d.ts +6 -0
  141. package/dist/render/translucentQueue.js +15 -1
  142. package/package.json +6 -6
  143. package/src/index.ts +10 -0
  144. package/src/render/backend/api.ts +15 -9
  145. package/src/render/backend/webgl2/glassTintPrograms.ts +58 -0
  146. package/src/render/backend/webgl2/renderer.ts +422 -109
  147. package/src/render/backend/webgpu/buffers.ts +50 -2
  148. package/src/render/backend/webgpu/depthPass.ts +2 -1
  149. package/src/render/backend/webgpu/flatPass.ts +22 -1
  150. package/src/render/backend/webgpu/glassTintPass.ts +244 -0
  151. package/src/render/backend/webgpu/gpuDrivenPass.ts +349 -5
  152. package/src/render/backend/webgpu/instanced.ts +21 -1
  153. package/src/render/backend/webgpu/latePass.ts +152 -0
  154. package/src/render/backend/webgpu/lightVolumePass.ts +5 -1
  155. package/src/render/backend/webgpu/motionPass.ts +535 -0
  156. package/src/render/backend/webgpu/pipelineCache.ts +12 -1
  157. package/src/render/backend/webgpu/pointGlassTintPass.ts +233 -0
  158. package/src/render/backend/webgpu/pointShadowPass.ts +158 -26
  159. package/src/render/backend/webgpu/postPass.ts +34 -4
  160. package/src/render/backend/webgpu/renderer.ts +1178 -425
  161. package/src/render/backend/webgpu/scatterPass.ts +2 -1
  162. package/src/render/backend/webgpu/skinPaletteRing.ts +4 -0
  163. package/src/render/backend/webgpu/snapshotMips.ts +80 -0
  164. package/src/render/backend/webgpu/surfaceTexturePass.ts +1 -1
  165. package/src/render/flameLight.ts +15 -2
  166. package/src/render/glass.ts +74 -0
  167. package/src/render/glassCasters.ts +164 -0
  168. package/src/render/glassShadow.ts +153 -0
  169. package/src/render/globalMediumPass.ts +4 -11
  170. package/src/render/gpudriven/lit.ts +31 -0
  171. package/src/render/gpudriven/materialTable.ts +28 -6
  172. package/src/render/gpudriven/pipeline.ts +4 -3
  173. package/src/render/gpudriven/sunGlass.ts +117 -0
  174. package/src/render/lightBudget.ts +19 -5
  175. package/src/render/pointGlassTint.ts +250 -0
  176. package/src/render/pointShadowArray.ts +188 -22
  177. package/src/render/pointShadowBudget.ts +49 -39
  178. package/src/render/pointShadowMap.ts +41 -0
  179. package/src/render/recon/changeFrames.ts +24 -0
  180. package/src/render/recon/disocclusion.ts +54 -3
  181. package/src/render/recon/lateRouting.ts +29 -0
  182. package/src/render/recon/mover.ts +118 -0
  183. package/src/render/recon/resolve.ts +137 -10
  184. package/src/render/renderQuality.ts +47 -0
  185. package/src/render/rendererHarness.ts +12 -0
  186. package/src/render/sceneTarget.ts +66 -12
  187. package/src/render/shaders/depth.ts +109 -9
  188. package/src/render/shaders/flat/directionalShadow.ts +97 -6
  189. package/src/render/shaders/flat/glassTint.ts +25 -0
  190. package/src/render/shaders/flat/index.ts +17 -0
  191. package/src/render/shaders/flat/lampGlass.ts +185 -0
  192. package/src/render/shaders/flat/main.ts +115 -15
  193. package/src/render/shaders/flat/pointShadow.ts +2 -0
  194. package/src/render/shaders/flat/preamble.ts +21 -6
  195. package/src/render/shaders/generated/depth.wgsl.ts +234 -4
  196. package/src/render/shaders/generated/flat.wgsl.ts +376 -328
  197. package/src/render/shaders/generated/globalMedium.wgsl.ts +3 -13
  198. package/src/render/shaders/generated/lightVolume.wgsl.ts +4 -14
  199. package/src/render/shaders/generated/octahedralResolve.wgsl.ts +30 -0
  200. package/src/render/shaders/generated/temporalResolve.wgsl.ts +31 -6
  201. package/src/render/shaders/globalMedium.ts +6 -6
  202. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +88 -1
  203. package/src/render/shaders/gpudriven/cull.wgsl.ts +11 -2
  204. package/src/render/shaders/gpudriven/materialTable.wgsl.ts +3 -1
  205. package/src/render/shaders/gpudriven/shade.wgsl.ts +50 -1
  206. package/src/render/shaders/gpudriven/sunGlass.wgsl.ts +139 -0
  207. package/src/render/shaders/lightVolume.ts +6 -6
  208. package/src/render/shaders/octahedralResolve.ts +50 -17
  209. package/src/render/shaders/recon/lateDepth.wgsl.ts +47 -0
  210. package/src/render/shaders/recon/motion.wgsl.ts +84 -0
  211. package/src/render/shaders/recon/resolve.wgsl.ts +112 -8
  212. package/src/render/shaders/temporalResolve.ts +141 -5
  213. package/src/render/shadowCasters.ts +28 -1
  214. package/src/render/shadowMap.ts +231 -37
  215. package/src/render/sunGlassTint.ts +86 -0
  216. package/src/render/temporalAa.ts +227 -0
  217. package/src/render/temporalPass.ts +88 -12
  218. package/src/render/translucentQueue.ts +20 -1
@@ -9,12 +9,7 @@ import {
9
9
  } from '../../temporalAa.ts';
10
10
  import { OIT_MULTISAMPLE_REFUSAL } from '../../orderIndependent.ts';
11
11
  import { TranslucentQueue } from '../../translucentQueue.ts';
12
- import {
13
- DEPTH_CLEAR,
14
- DEPTH_COMPARE_EQUAL,
15
- MAX_DEPTH_LAYER,
16
- REVERSED_DEPTH,
17
- } from '../../depthConvention.ts';
12
+ import { DEPTH_CLEAR, MAX_DEPTH_LAYER, REVERSED_DEPTH } from '../../depthConvention.ts';
18
13
  import { RECONSTRUCTION_MULTISAMPLE_REFUSAL, reconRenderSize } from '../../recon/frameSizes.ts';
19
14
  import { jitterOffset as jitterOffsetFor, reconJitterPhases } from '../../recon/jitter.ts';
20
15
  import {
@@ -23,16 +18,25 @@ import {
23
18
  RECON_WORKGROUP,
24
19
  reconResolveWgsl,
25
20
  } from '../../shaders/recon/resolve.wgsl.ts';
26
- import {
27
- MOTION_DRAW_FLOATS,
28
- MOTION_DRAW_STRIDE,
29
- MOTION_FRAME_FLOATS,
30
- MOTION_TARGET_FORMAT,
31
- RECON_MOTION_WGSL,
32
- } from '../../shaders/recon/motion.wgsl.ts';
21
+ import { MotionPass } from './motionPass.ts';
22
+ import { LatePass } from './latePass.ts';
23
+ import { SnapshotMips } from './snapshotMips.ts';
24
+ import { resolveGlass } from '../../glass.ts';
25
+ import type { GlassOptions, ResolvedGlass } from '../../glass.ts';
26
+ import { routesLate, tracksMotion } from '../../recon/lateRouting.ts';
27
+ import { frameOfChange, keepsPrevious, pairedInstances } from '../../recon/changeFrames.ts';
28
+ import { INSTANCE_STRIDE } from '../../instances.ts';
33
29
  import { DEFAULT_DISOCCLUSION } from '../../recon/disocclusion.ts';
34
30
  import { DEFAULT_RECON_QUALITY } from '../../recon/resolve.ts';
35
31
  import type { ReadonlyMat4 } from 'gl-matrix';
32
+ import {
33
+ MOVER_MOTION,
34
+ MOVER_TWICE,
35
+ advanceMover,
36
+ claimRendererId,
37
+ isMover,
38
+ } from '../../recon/mover.ts';
39
+ import type { Mover } from '../../recon/mover.ts';
36
40
 
37
41
  import type { Vec3 } from '../../../math/color.ts';
38
42
  import { clamp } from '../../../math/scalar.ts';
@@ -128,6 +132,7 @@ import {
128
132
  type ShadowLight,
129
133
  } from '../../pointShadowSystem.ts';
130
134
  import { GpuPointShadowArray, GpuPointShadowMap } from './pointShadowPass.ts';
135
+ import { OCTAHEDRAL_EDGE, strideLayers } from '../../pointShadowArray.ts';
131
136
  import {
132
137
  createPanelBindGroup,
133
138
  createPanelBindGroupLayout,
@@ -190,6 +195,7 @@ import {
190
195
  createPostStageLayout,
191
196
  depthResolvePipeline,
192
197
  postPipeline,
198
+ postPipelineTargets,
193
199
  sceneColorFormat,
194
200
  AO_BLUR_FIELDS,
195
201
  AO_BLUR_FRAG_WGSL,
@@ -325,6 +331,16 @@ import {
325
331
  } from '../../globalMedium.ts';
326
332
  import type { GlobalMediumOptions } from '../../globalMedium.ts';
327
333
  import type { MeshData } from '../../mesh.ts';
334
+ import { castsDepth, glassOf } from '../../shadowCasters.ts';
335
+ import { GlassCasterList, type GlassReplaySink } from '../../glassCasters.ts';
336
+ import { GpuGlassTint } from './glassTintPass.ts';
337
+ import {
338
+ SUN_DYNAMIC_LAYER,
339
+ SUN_GLASS_MOVING_LAYER,
340
+ SUN_GLASS_STATIC_LAYER,
341
+ SUN_PEELED_LAYER,
342
+ SUN_STATIC_LAYER,
343
+ } from '../../shadowMap.ts';
328
344
  import type { SceneCasterMaterial, ShadowCasterSink, ShadowCasters } from '../../shadowCasters.ts';
329
345
  import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental, type GpuMesh } from './buffers.ts';
330
346
  import type { GpuSurface } from './device.ts';
@@ -1307,6 +1323,49 @@ function padded(direction: ArrayLike<number>): Float32Array {
1307
1323
  return SCRATCH_VEC4;
1308
1324
  }
1309
1325
 
1326
+ /** The sun texture's usage: attached, sampled, and copied both ways when glass grows it. */
1327
+ const SUN_USAGE = 0x10 | 0x4 | 0x1 | 0x2; // RENDER_ATTACHMENT | TEXTURE_BINDING | COPY_SRC | COPY_DST
1328
+
1329
+ /** Every view of the sun's texture a pass reads or attaches, for `maps` of its layers. */
1330
+ function sunViewsOf(
1331
+ texture: GPUTexture,
1332
+ maps: number,
1333
+ layers: number,
1334
+ ): {
1335
+ array: GPUTextureView;
1336
+ staticArray: GPUTextureView;
1337
+ static: GPUTextureView;
1338
+ dynamic: GPUTextureView | null;
1339
+ peel: GPUTextureView | null;
1340
+ glass: readonly [GPUTextureView, GPUTextureView] | null;
1341
+ } {
1342
+ const layer = (index: number): GPUTextureView =>
1343
+ texture.createView({ dimension: '2d', baseArrayLayer: index, arrayLayerCount: 1 });
1344
+ return {
1345
+ array: texture.createView({ dimension: '2d-array' }),
1346
+ staticArray: texture.createView({
1347
+ dimension: '2d-array',
1348
+ baseArrayLayer: SUN_STATIC_LAYER,
1349
+ arrayLayerCount: 1,
1350
+ }),
1351
+ static: layer(SUN_STATIC_LAYER),
1352
+ dynamic: maps > SUN_DYNAMIC_LAYER ? layer(SUN_DYNAMIC_LAYER) : null,
1353
+ peel: maps > SUN_PEELED_LAYER ? layer(SUN_PEELED_LAYER) : null,
1354
+ glass: layers > maps ? [layer(SUN_GLASS_STATIC_LAYER), layer(SUN_GLASS_MOVING_LAYER)] : null,
1355
+ };
1356
+ }
1357
+
1358
+ /**
1359
+ * What the sun keeps through glass: two `rgba8unorm` layers, attached one at a time at level 0,
1360
+ * sampled whole with their mip chains, which are rebuilt after each layer is drawn.
1361
+ */
1362
+ interface SunTintTarget {
1363
+ readonly texture: GPUTexture;
1364
+ readonly layers: readonly [GPUTextureView, GPUTextureView];
1365
+ readonly array: GPUTextureView;
1366
+ readonly mips: readonly [SnapshotMips, SnapshotMips];
1367
+ }
1368
+
1310
1369
  export class WebGPURenderer implements RendererApi {
1311
1370
  /**
1312
1371
  * Real timestamp queries, where the adapter offers them.
@@ -1416,6 +1475,8 @@ export class WebGPURenderer implements RendererApi {
1416
1475
  * caller who passed null gets — rather than reading a slot this frame has already re-let.
1417
1476
  */
1418
1477
  private skinPaletteSlot = -1;
1478
+ /** The palette `skinPaletteSlot` was uploaded from, for a skinned mover to copy. See `Mover`. */
1479
+ private skinPaletteData: Float32Array | null = null;
1419
1480
 
1420
1481
  private readonly surface: GpuSurface;
1421
1482
  /** The encoder for the frame in flight, or null between frames. */
@@ -1425,6 +1486,27 @@ export class WebGPURenderer implements RendererApi {
1425
1486
  private pass: GPURenderPassEncoder | null = null;
1426
1487
 
1427
1488
  private readonly pipelines: PipelineCache;
1489
+ /**
1490
+ * The pipelines a reconstructing frame's blended draws are built for: the reconstructed picture's
1491
+ * format at one sample, which is where `LatePass` draws them. Aliased to `pipelines` when the
1492
+ * world already lands on exactly that, as `overlayPipelines` is.
1493
+ */
1494
+ private readonly latePipelines: PipelineCache;
1495
+ /** The blended draws of a reconstructing frame, drawn after the upscale. See `LatePass`. */
1496
+ private latePass: LatePass | null = null;
1497
+ /** A late draw asked to refract: `runLate` copies the reconstructed picture before replaying. */
1498
+ private lateRefractWanted = false;
1499
+ /** The refraction copy's mip chain, recorded after each copy: what frosted glass reads. */
1500
+ private refractMips: SnapshotMips | null = null;
1501
+ /**
1502
+ * The refraction copy's sampler: filtering, and **between levels as well**, so a frosted pane's
1503
+ * level falls smoothly between two blurs rather than stepping from one to the next.
1504
+ */
1505
+ private refractSampler: GPUSampler | null = null;
1506
+ /** One glass resolution reused by every draw, so a pane allocates nothing. See `glass.ts`. */
1507
+ private readonly glassScratch: ResolvedGlass = { transmission: 0, frost: 0, tint: [1, 1, 1] };
1508
+ /** `issueCommand` bound once, so the late replay allocates no closure per frame. */
1509
+ private readonly issueBound: (pass: GPURenderPassEncoder, command: DrawCommand) => void;
1428
1510
  /**
1429
1511
  * The same pipelines again, built for the canvas, for anything drawn after `endFrame`.
1430
1512
  *
@@ -1926,6 +2008,16 @@ export class WebGPURenderer implements RendererApi {
1926
2008
  private taaUniforms!: GPUBuffer;
1927
2009
  private taaTextures: (GPUTexture | null)[] = [null, null];
1928
2010
  private taaViews: (GPUTextureView | null)[] = [null, null];
2011
+ /** Each pixel's flicker record, paired with the history and swapped with it: see `temporalAa.ts`. */
2012
+ private taaRecords: (GPUTexture | null)[] = [null, null];
2013
+ private taaRecordViews: (GPUTextureView | null)[] = [null, null];
2014
+ /** The rest of each pixel's record — depth, pull and recent swing — beside the first. */
2015
+ private taaMotions: (GPUTexture | null)[] = [null, null];
2016
+ private taaMotionViews: (GPUTextureView | null)[] = [null, null];
2017
+ /** The resolve's depth-to-metres row, from the inverse of `frameProjection`. */
2018
+ private readonly taaInvProjection = new Float32Array(16);
2019
+ /** The resolve's two targets, the picture and the record, held for the pipeline cache's sake. */
2020
+ private taaTargets: GPUColorTargetState[] = [];
1929
2021
  /** Group `i` reads history `i` and the pass writes into the other one. */
1930
2022
  private taaBindGroups: (GPUBindGroup | null)[] = [null, null];
1931
2023
  private taaWrite = 0;
@@ -1960,10 +2052,10 @@ export class WebGPURenderer implements RendererApi {
1960
2052
  * the *unsharpened* result — sharpening into the history would sharpen a sharpened picture every
1961
2053
  * frame, which is the halo the reference's own clamp exists to prevent, compounded.
1962
2054
  *
1963
- * The motion target is render size and carries a drawn object's own motion where something wrote
1964
- * one. **Nothing writes it yet**, so it is cleared every frame and every pixel takes the camera's
1965
- * motion, which the resolve derives from the depth and the reprojection. Static geometry is
1966
- * exact that way and a moving object ghosts until the motion pass lands.
2055
+ * The motion target is render size and carries a drawn object's own motion where the motion pass
2056
+ * wrote one — a draw that named its mover, or passed last frame's matrix. Every other texel is
2057
+ * cleared, and the resolve takes the camera's motion there, derived from the depth and the
2058
+ * reprojection: exact for anything that did not move. See `MotionPass`.
1967
2059
  */
1968
2060
  private reconHistories: (GPUTexture | null)[] = [null, null];
1969
2061
  private reconHistoryViews: (GPUTextureView | null)[] = [null, null];
@@ -1985,34 +2077,15 @@ export class WebGPURenderer implements RendererApi {
1985
2077
  private reconGroups: (GPUBindGroup | null)[] = [null, null];
1986
2078
  private reconSharpenGroups: (GPUBindGroup | null)[] = [null, null];
1987
2079
  private reconRushGroup: GPUBindGroup | null = null;
1988
- /**
1989
- * What the motion pass will draw, recorded by `drawMesh` and spent once a frame.
1990
- *
1991
- * A parallel array of meshes and one flat matrix buffer rather than an array of objects, because
1992
- * this is written on the frame's hot path and the house rule about allocating there binds it: the
1993
- * matrices are copied into a buffer that only ever grows, and the count is what resets.
1994
- */
1995
- private readonly motionMeshes: GpuMesh[] = [];
1996
- private motionMatrices = new Float32Array(0);
1997
- private motionCount = 0;
1998
- private motionLayout: GPUBindGroupLayout | null = null;
1999
- /**
2000
- * One pipeline per vertex stride, because a stride is a property of the *mesh*.
2001
- *
2002
- * Which optional attributes a mesh supplied decides how far apart its positions are, and this
2003
- * pass reads nothing but positions — so two meshes drawn by the same shader still need two
2004
- * vertex layouts. A map rather than a cache key on the pipeline cache: there is one shader here
2005
- * and a handful of strides in any scene.
2006
- */
2007
- private readonly motionPipelines = new Map<number, GPURenderPipeline>();
2008
- private motionModule: GPUShaderModule | null = null;
2009
- private motionPipelineLayout: GPUPipelineLayout | null = null;
2010
- private motionFrameBuffer: GPUBuffer | null = null;
2011
- private motionDrawBuffer: GPUBuffer | null = null;
2012
- private motionDrawCapacity = 0;
2013
- private motionGroup: GPUBindGroup | null = null;
2014
- private readonly motionFrameStaging = new Float32Array(MOTION_FRAME_FLOATS);
2015
- private motionDrawStaging = new Uint8Array(0);
2080
+ /** The motion pass, made with the reconstruction's targets. See `MotionPass`. */
2081
+ private motionPass: MotionPass | null = null;
2082
+ /** This renderer's identity to a `Mover`. See `claimRendererId`. */
2083
+ private readonly moverRenderer = claimRendererId();
2084
+ /** Frames begun, so a mover can tell last frame from any other. Advanced in `beginFrame`. */
2085
+ private moverFrame = 0;
2086
+ /** Between `beginFrame` and `endFrame`. A change outside it is the next frame's. */
2087
+ private inFrame = false;
2088
+ private warnedMoverTwice = false;
2016
2089
  /** The scene's own clip transform with the generated shaders' y negation folded in. */
2017
2090
  private readonly motionRaster = mat4.create();
2018
2091
  private reconSizeKey = '';
@@ -2265,6 +2338,7 @@ export class WebGPURenderer implements RendererApi {
2265
2338
  refraction: number;
2266
2339
  refractTint: Vec3 | undefined;
2267
2340
  thicknessM: number;
2341
+ glass: GlassOptions | undefined;
2268
2342
  } = {
2269
2343
  lit: true,
2270
2344
  fog: true,
@@ -2275,6 +2349,7 @@ export class WebGPURenderer implements RendererApi {
2275
2349
  refraction: 0,
2276
2350
  refractTint: undefined,
2277
2351
  thicknessM: 0,
2352
+ glass: undefined,
2278
2353
  };
2279
2354
 
2280
2355
  private reflectionPassActive = false;
@@ -2580,7 +2655,7 @@ export class WebGPURenderer implements RendererApi {
2580
2655
  this.filmFrags.writeBlock(fragmentSlot, this.filmFragInts);
2581
2656
 
2582
2657
  const filmPipe = filmPipeline(
2583
- this.pipelines,
2658
+ this.blendedPipelines(),
2584
2659
  this.surface.device,
2585
2660
  this.filmLayout,
2586
2661
  `film|${geometry.key ?? ''}`,
@@ -2594,10 +2669,7 @@ export class WebGPURenderer implements RendererApi {
2594
2669
  * holds the mirror's colour whatever `uReflectionEnabled` says, and WebGPU validates the
2595
2670
  * binding rather than the read — the argument `filmBindGroupBlank` exists for.
2596
2671
  */
2597
- const filmCommand = this.recordDraw(
2598
- filmGroup === this.filmBindGroup ? MIRROR_COLOR : 0,
2599
- this.currentTarget(),
2600
- );
2672
+ const filmCommand = this.recordBlended(filmGroup === this.filmBindGroup ? MIRROR_COLOR : 0);
2601
2673
  if (filmCommand !== null) {
2602
2674
  filmCommand.pipeline = filmPipe;
2603
2675
  filmCommand.bindGroup = filmGroup;
@@ -2788,6 +2860,7 @@ export class WebGPURenderer implements RendererApi {
2788
2860
  */
2789
2861
  setSkinPalette(palette: Float32Array | null): void {
2790
2862
  if (this.surface.lost) return;
2863
+ this.skinPaletteData = palette;
2791
2864
  if (palette === null) {
2792
2865
  this.skinPaletteSlot = -1;
2793
2866
  return;
@@ -3156,7 +3229,8 @@ export class WebGPURenderer implements RendererApi {
3156
3229
  * no-op before the first `bindMeshPass` of a session, same as every other pass that reads it.
3157
3230
  */
3158
3231
  drawSdfText(text: GpuSdfText, model: Float32Array, color: Vec3, opacity: number): void {
3159
- const viewProj = this.viewProj;
3232
+ /* Unjittered when it lands after the upscale, like every late draw. See `drawsLate`. */
3233
+ const viewProj = this.drawsLate() ? this.correctedViewProj : this.viewProj;
3160
3234
  if (!this.canDraw() || viewProj === null) return;
3161
3235
  const font = text.font;
3162
3236
  const atlas = text.atlas;
@@ -3190,12 +3264,12 @@ export class WebGPURenderer implements RendererApi {
3190
3264
  this.sdfTextFrags.writeFloat(fragmentSlot, fragAt('uOutputExposure'), this.gradeExposure());
3191
3265
 
3192
3266
  const sdfPipe = sdfTextPipeline(
3193
- this.targetPipelines(),
3267
+ this.blendedPipelines(),
3194
3268
  this.surface.device,
3195
3269
  this.sdfTextLayout,
3196
3270
  );
3197
3271
  const sdfGroup = this.sdfTextBindGroupFor(atlas);
3198
- const sdfCommand = this.recordDraw(0, this.currentTarget());
3272
+ const sdfCommand = this.recordBlended(0);
3199
3273
  if (sdfCommand !== null) {
3200
3274
  sdfCommand.pipeline = sdfPipe;
3201
3275
  sdfCommand.bindGroup = sdfGroup;
@@ -3311,8 +3385,8 @@ export class WebGPURenderer implements RendererApi {
3311
3385
 
3312
3386
  const pass = this.openTextPass();
3313
3387
  if (pass === null) return;
3314
- const textPipe = textPipeline(this.targetPipelines(), this.surface.device, this.textLayout);
3315
- const textCommand = this.recordDraw(0, this.currentTarget());
3388
+ const textPipe = textPipeline(this.blendedPipelines(), this.surface.device, this.textLayout);
3389
+ const textCommand = this.recordBlended(0);
3316
3390
  if (textCommand !== null) {
3317
3391
  textCommand.pipeline = textPipe;
3318
3392
  textCommand.bindGroup = this.textBindGroup;
@@ -3762,13 +3836,13 @@ export class WebGPURenderer implements RendererApi {
3762
3836
 
3763
3837
  const layout = this.particleLayoutFor(batch.material).layout;
3764
3838
  const particlePipe = particlePipeline(
3765
- this.pipelines,
3839
+ this.blendedPipelines(),
3766
3840
  device,
3767
3841
  layout,
3768
3842
  batch.material,
3769
3843
  batch.blend,
3770
3844
  );
3771
- const particleCommand = this.recordDraw(0, this.currentTarget());
3845
+ const particleCommand = this.recordBlended(0);
3772
3846
  if (particleCommand !== null) {
3773
3847
  particleCommand.pipeline = particlePipe;
3774
3848
  particleCommand.bindGroup = batch.bindGroup;
@@ -3913,13 +3987,13 @@ export class WebGPURenderer implements RendererApi {
3913
3987
  frags.writeFloats(fragmentSlot, atF('uTint'), plumes.tint);
3914
3988
 
3915
3989
  const plumePipe = plumePipeline(
3916
- this.pipelines,
3990
+ this.blendedPipelines(),
3917
3991
  this.surface.device,
3918
3992
  this.plumeLayout,
3919
3993
  plumes.material,
3920
3994
  plumes.blend,
3921
3995
  );
3922
- const plumeCommand = this.recordDraw(0, this.currentTarget());
3996
+ const plumeCommand = this.recordBlended(0);
3923
3997
  if (plumeCommand !== null) {
3924
3998
  plumeCommand.pipeline = plumePipe;
3925
3999
  plumeCommand.bindGroup = this.plumeBindGroup;
@@ -4474,9 +4548,13 @@ export class WebGPURenderer implements RendererApi {
4474
4548
  this.windStreakVerts.writeBlock(vertexSlot, this.windStreakVertBlock);
4475
4549
  this.windStreakFrags.writeBlock(fragmentSlot, this.windStreakFragBlock);
4476
4550
 
4477
- const windPipe = windStreakPipeline(this.pipelines, this.surface.device, this.windStreakLayout);
4551
+ const windPipe = windStreakPipeline(
4552
+ this.blendedPipelines(),
4553
+ this.surface.device,
4554
+ this.windStreakLayout,
4555
+ );
4478
4556
  /* Two uniform buffers and two vertex buffers. Nothing is sampled, so nothing is read. */
4479
- const windCommand = this.recordDraw(0, this.currentTarget());
4557
+ const windCommand = this.recordBlended(0);
4480
4558
  if (windCommand !== null) {
4481
4559
  windCommand.pipeline = windPipe;
4482
4560
  windCommand.bindGroup = this.windStreakBindGroup;
@@ -4704,8 +4782,8 @@ export class WebGPURenderer implements RendererApi {
4704
4782
  this.boltVerts.writeBlock(vertexSlot, this.boltVertBlock);
4705
4783
  this.boltFrags.writeBlock(fragmentSlot, this.boltFragInts);
4706
4784
 
4707
- const boltPipe = boltPipeline(this.pipelines, this.surface.device, this.boltLayout);
4708
- const boltCommand = this.recordDraw(0, this.currentTarget());
4785
+ const boltPipe = boltPipeline(this.blendedPipelines(), this.surface.device, this.boltLayout);
4786
+ const boltCommand = this.recordBlended(0);
4709
4787
  if (boltCommand !== null) {
4710
4788
  boltCommand.pipeline = boltPipe;
4711
4789
  boltCommand.bindGroup = this.boltBindGroup;
@@ -4842,9 +4920,14 @@ export class WebGPURenderer implements RendererApi {
4842
4920
  this.lineFrags.writeFloat(fragmentSlot, atF('uFogNear'), medium.fogNear);
4843
4921
  this.lineFrags.writeFloat(fragmentSlot, atF('uFogFar'), medium.fogFar);
4844
4922
 
4845
- const linePipe = linePipeline(this.pipelines, this.surface.device, this.lineLayout, additive);
4923
+ const linePipe = linePipeline(
4924
+ this.blendedPipelines(),
4925
+ this.surface.device,
4926
+ this.lineLayout,
4927
+ additive,
4928
+ );
4846
4929
  /* Two uniform blocks and three vertex buffers, and no attachment among them. */
4847
- const lineCommand = this.recordDraw(0, this.currentTarget());
4930
+ const lineCommand = this.recordBlended(0);
4848
4931
  if (lineCommand !== null) {
4849
4932
  lineCommand.pipeline = linePipe;
4850
4933
  lineCommand.bindGroup = this.lineBindGroup;
@@ -5067,8 +5150,39 @@ export class WebGPURenderer implements RendererApi {
5067
5150
 
5068
5151
  /* -- The shadow pass -------------------------------------------------------------- */
5069
5152
 
5070
- private readonly shadowMap: GPUTexture;
5071
- private readonly shadowView: GPUTextureView;
5153
+ /**
5154
+ * The sun's static, moving and peeled maps, as layers of one texture (`shadowMap.ts`). The three
5155
+ * views below are single layers, what each pass attaches; the two here are what is sampled.
5156
+ */
5157
+ private sunShadowTexture: GPUTexture;
5158
+ /** Every layer, for the lit pass, the light volumes and the medium. */
5159
+ private sunArrayView: GPUTextureView;
5160
+ /**
5161
+ * The static layer alone, as an array, for the peel: it draws into another layer of the same
5162
+ * texture, and a view covering that layer too would put it in two usages in one pass.
5163
+ */
5164
+ private sunStaticArrayView: GPUTextureView;
5165
+ /** How many layers the sun's texture holds: its maps, and the two glass layers once glass casts. */
5166
+ private sunLayerCount: number;
5167
+ /** The glass depth layers, static then moving, once glass has cast. See `growSunForGlass`. */
5168
+ private sunGlassViews: readonly [GPUTextureView, GPUTextureView] | null = null;
5169
+ /** The texture a growth replaced, destroyed once the encoder that copied out of it is submitted. */
5170
+ private retiredSunTexture: GPUTexture | null = null;
5171
+ /** What the sun's light keeps through glass: two `rgba8unorm` layers, made when glass first casts. */
5172
+ private sunTint: SunTintTarget | null = null;
5173
+ /** The tint pass's layouts, per-draw block, groups and pipelines, made when glass first casts. */
5174
+ private glassTint: GpuGlassTint | null = null;
5175
+ /** The tint pass open while the glass list is replayed into it. */
5176
+ private tintPass: GPURenderPassEncoder | null = null;
5177
+ /** True while the glass list is replayed into a glass depth layer through `casterSink`. */
5178
+ private drawingGlassDepth = false;
5179
+ /** Whether the open sun pass is the movers' layer, whose glass goes to the moving glass layer. */
5180
+ private shadowLayerIsDynamic = false;
5181
+ /** The glass tints' sampler: filtering, and between levels, for frost's coarser reads. */
5182
+ private readonly tintSampler: GPUSampler;
5183
+ /** The light a tint pass is drawn for: xyz a direction (w 0) or a position (w 1). */
5184
+ private readonly glassLight = new Float32Array(4);
5185
+ private shadowView: GPUTextureView;
5072
5186
  private readonly shadowLayout: GPUBindGroupLayout;
5073
5187
  /** The same layout plus the joint palette. See `createDepthBindGroupLayout`. */
5074
5188
  private readonly shadowSkinnedLayout: GPUBindGroupLayout;
@@ -5094,8 +5208,7 @@ export class WebGPURenderer implements RendererApi {
5094
5208
  * read twice — so a profile that switches peeling off gets no map and no pass on either
5095
5209
  * backend rather than a map nothing writes.
5096
5210
  */
5097
- private readonly peelMap: GPUTexture | null;
5098
- private readonly peelView: GPUTextureView | null;
5211
+ private peelView: GPUTextureView | null;
5099
5212
  /**
5100
5213
  * The movers' own layer: the character, the rope it swings on, and anything else that walks.
5101
5214
  *
@@ -5116,10 +5229,9 @@ export class WebGPURenderer implements RendererApi {
5116
5229
  * and therefore as lit — so nothing looked broken, only empty, which is why it survived the
5117
5230
  * port and every gate in it.
5118
5231
  */
5119
- private readonly dynamicMap: GPUTexture | null;
5120
- private readonly dynamicView: GPUTextureView | null;
5232
+ private dynamicView: GPUTextureView | null;
5121
5233
  /** The peel's own bind groups: `uPeelShadowLayer` at 1, and the static map to peel against. */
5122
- private readonly shadowPeelBindGroup: GPUBindGroup | null;
5234
+ private shadowPeelBindGroup: GPUBindGroup | null;
5123
5235
  /**
5124
5236
  * The cutout casters' layout, and a bind group per cutout map for each layer, built the first
5125
5237
  * time a map casts and kept: a `createBindGroup` per draw is the allocation the house rules forbid.
@@ -5131,7 +5243,7 @@ export class WebGPURenderer implements RendererApi {
5131
5243
  private readonly cutoutPeelGroups = new Map<GpuSurfaceTexture, GPUBindGroup>();
5132
5244
  private shadowPeelPassUniforms: GPUBuffer | null = null;
5133
5245
  private shadowPeelSampler: GPUSampler | null = null;
5134
- private readonly scatterDepthPeelBindGroup: GPUBindGroup | null;
5246
+ private scatterDepthPeelBindGroup: GPUBindGroup | null;
5135
5247
  /** Which layer the open pass is writing, so the sink picks the bindings that match it. */
5136
5248
  private shadowLayerIsPeel = false;
5137
5249
  /**
@@ -5190,7 +5302,7 @@ export class WebGPURenderer implements RendererApi {
5190
5302
  const held = groups.get(texture);
5191
5303
  if (held !== undefined) return held;
5192
5304
  const passUniforms = peel ? this.shadowPeelPassUniforms : this.shadowPassUniforms;
5193
- const previousView = peel ? this.shadowView : this.shadowBlankView;
5305
+ const previousView = peel ? this.sunStaticArrayView : this.shadowBlankView;
5194
5306
  const previousSampler = peel ? this.shadowPeelSampler : this.shadowBlankSampler;
5195
5307
  if (passUniforms === null || previousView === null || previousSampler === null) return null;
5196
5308
  const group = createDepthBindGroup(
@@ -5268,8 +5380,23 @@ export class WebGPURenderer implements RendererApi {
5268
5380
  * Built once and held, rather than made per call: it closes over `this` and a fresh one
5269
5381
  * every frame would be an allocation in a per-frame path.
5270
5382
  */
5383
+ /** The glass the open shadow pass was offered, kept for its own passes: `glassCasters.ts`. */
5384
+ private readonly glassCasters = new GlassCasterList();
5385
+ /** One resolution every glass caster is read through, so recording one allocates nothing. */
5386
+ private readonly glassCasterScratch: ResolvedGlass = {
5387
+ transmission: 0,
5388
+ frost: 0,
5389
+ tint: [1, 1, 1],
5390
+ };
5271
5391
  private readonly casterSink: ShadowCasterSink = {
5272
5392
  mesh: (mesh, model, material) => {
5393
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
5394
+ /* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
5395
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
5396
+ this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
5397
+ }
5398
+ return;
5399
+ }
5273
5400
  const pass = this.shadowPass;
5274
5401
  if (pass === null) return;
5275
5402
  /* A cutout caster's group is looked up before a slot is spent, so a miss costs nothing. */
@@ -5319,6 +5446,12 @@ export class WebGPURenderer implements RendererApi {
5319
5446
  * uses — one draw, however many instances, exactly as the colour pass does it.
5320
5447
  */
5321
5448
  instanced: (batch, data, material) => {
5449
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
5450
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
5451
+ this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
5452
+ }
5453
+ return;
5454
+ }
5322
5455
  const pass = this.shadowPass;
5323
5456
  if (pass === null) return;
5324
5457
  const gpuBatch = batch as GpuInstancedBatch;
@@ -5367,7 +5500,13 @@ export class WebGPURenderer implements RendererApi {
5367
5500
  pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
5368
5501
  pass.drawIndexed(geometry.indexCount, count);
5369
5502
  },
5370
- skinnedMesh: (mesh, model, palette) => {
5503
+ skinnedMesh: (mesh, model, palette, material) => {
5504
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
5505
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
5506
+ this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
5507
+ }
5508
+ return;
5509
+ }
5371
5510
  const pass = this.shadowPass;
5372
5511
  if (pass === null) return;
5373
5512
  /* Its own slot, so two characters casting into one bake do not both take the second one's
@@ -5751,10 +5890,13 @@ export class WebGPURenderer implements RendererApi {
5751
5890
  * nothing at all under the default profile, which is on. The cache carries it for the same
5752
5891
  * reason it carries the sample count: no pass can then disagree with the attachment.
5753
5892
  */
5893
+ /* Off compiles none of the glass lookup into a lit pipeline: see `PipelineCache.glassShadows`. */
5894
+ const glassShadows = quality.glassShadows !== 'off';
5754
5895
  this.pipelines = new PipelineCache(
5755
5896
  device,
5756
5897
  quality.screenEffects ? sceneColorFormat(quality.hdrScene) : surface.format,
5757
5898
  this.samples,
5899
+ glassShadows,
5758
5900
  );
5759
5901
  /*
5760
5902
  * The canvas' own state, for the pass `ensurePass` reopens after `endFrame`.
@@ -5767,7 +5909,12 @@ export class WebGPURenderer implements RendererApi {
5767
5909
  this.overlayPipelines =
5768
5910
  this.pipelines.format === surface.format && this.samples === 1
5769
5911
  ? this.pipelines
5770
- : new PipelineCache(device, surface.format, 1);
5912
+ : new PipelineCache(device, surface.format, 1, glassShadows);
5913
+ this.latePipelines =
5914
+ this.pipelines.format === RECON_HISTORY_FORMAT && this.samples === 1
5915
+ ? this.pipelines
5916
+ : new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows);
5917
+ this.issueBound = this.issueCommand.bind(this);
5771
5918
  this.pipelineTargets =
5772
5919
  this.overlayPipelines === this.pipelines
5773
5920
  ? [this.pipelines]
@@ -5918,42 +6065,35 @@ export class WebGPURenderer implements RendererApi {
5918
6065
  const blankDriftIndexView = blankVolume('flat.emptyDriftLightIndex', 'r32uint');
5919
6066
  const blankDriftAtlasView = blankVolume('flat.emptyDriftLightAtlas', 'rgba16float');
5920
6067
 
5921
- this.shadowMap = device.createTexture({
5922
- label: 'shadow.static',
5923
- size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
6068
+ /*
6069
+ * The static world, its movers and — where more than one static depth layer is asked for —
6070
+ * the peel, as layers of one texture, on the conditions `renderer.ts` builds its array on: one
6071
+ * decision read twice. Same size and format throughout, because the shader samples every
6072
+ * layer with one \`uShadowMapSize\` and one set of filter offsets.
6073
+ */
6074
+ const sunLayers = this.quality.directionalShadows
6075
+ ? this.quality.directionalShadowDepthLayers > 1
6076
+ ? 3
6077
+ : 2
6078
+ : 1;
6079
+ this.sunShadowTexture = device.createTexture({
6080
+ label: 'shadow.sun',
6081
+ size: [
6082
+ this.quality.directionalShadowMapSize,
6083
+ this.quality.directionalShadowMapSize,
6084
+ sunLayers,
6085
+ ],
5924
6086
  format: SHADOW_FORMAT,
5925
- usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
6087
+ /* Copyable both ways, because the first glass grows it by copying every layer across. */
6088
+ usage: SUN_USAGE,
5926
6089
  });
5927
- this.shadowView = this.shadowMap.createView();
5928
- /*
5929
- * The peel layer, on the condition `renderer.ts` builds its own on: more than one depth
5930
- * layer asked for. `depth.ts` fills it by discarding every fragment at or in front of what
5931
- * the static pass stored, so what lands here is the second independently fading occluder.
5932
- */
5933
- this.peelMap =
5934
- this.quality.directionalShadowDepthLayers > 1
5935
- ? device.createTexture({
5936
- label: 'shadow.peel',
5937
- size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
5938
- format: SHADOW_FORMAT,
5939
- usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
5940
- })
5941
- : null;
5942
- this.peelView = this.peelMap === null ? null : this.peelMap.createView();
5943
- /*
5944
- * The movers' layer, on the condition `renderer.ts` builds its own on: directional shadows
5945
- * at all. Same size and same format as the static map, because the shader samples all three
5946
- * with one `uShadowMapSize` and one set of PCF offsets.
5947
- */
5948
- this.dynamicMap = this.quality.directionalShadows
5949
- ? device.createTexture({
5950
- label: 'shadow.dynamic',
5951
- size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
5952
- format: SHADOW_FORMAT,
5953
- usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
5954
- })
5955
- : null;
5956
- this.dynamicView = this.dynamicMap === null ? null : this.dynamicMap.createView();
6090
+ this.sunLayerCount = sunLayers;
6091
+ const views = sunViewsOf(this.sunShadowTexture, sunLayers, sunLayers);
6092
+ this.sunArrayView = views.array;
6093
+ this.sunStaticArrayView = views.staticArray;
6094
+ this.shadowView = views.static;
6095
+ this.dynamicView = views.dynamic;
6096
+ this.peelView = views.peel;
5957
6097
 
5958
6098
  /*
5959
6099
  * **Cleared once, here, because a texture no pass has written reads as zero**, which the
@@ -5994,6 +6134,14 @@ export class WebGPURenderer implements RendererApi {
5994
6134
  * rather than the hardware's.
5995
6135
  */
5996
6136
  const shadowSampler = device.createSampler({ label: 'shadow.sampler' });
6137
+ this.tintSampler = device.createSampler({
6138
+ label: 'shadow.tintSampler',
6139
+ magFilter: 'linear',
6140
+ minFilter: 'linear',
6141
+ mipmapFilter: 'linear',
6142
+ addressModeU: 'clamp-to-edge',
6143
+ addressModeV: 'clamp-to-edge',
6144
+ });
5997
6145
  /*
5998
6146
  * The photometric atlas's own sampler, **created here rather than reusing `postSampler`.**
5999
6147
  *
@@ -6015,14 +6163,17 @@ export class WebGPURenderer implements RendererApi {
6015
6163
  });
6016
6164
 
6017
6165
  this.flatTextures = (name) => {
6018
- if (name === 'uStaticShadowMap') return { view: this.shadowView, sampler: shadowSampler };
6019
- /*
6020
- * The movers' layer. Cleared at construction like the others, so a profile with
6021
- * directional shadows on that never opens the layer reads the far plane and therefore
6022
- * "nothing moving is in the way" — not whatever the allocation happened to contain.
6023
- */
6024
- if (name === 'uDynamicShadowMap' && this.dynamicView !== null) {
6025
- return { view: this.dynamicView, sampler: shadowSampler };
6166
+ if (name === 'uSunShadows') return { view: this.sunArrayView, sampler: shadowSampler };
6167
+ /* What the sun keeps through glass, or the one-texel stand-in the lookup reads as none. */
6168
+ if (name === 'uSunGlassTints') {
6169
+ return { view: this.sunTint?.array ?? blankArrayView, sampler: this.tintSampler };
6170
+ }
6171
+ /* What the lamps keep through glass, one layer a light, or the same stand-in. */
6172
+ if (name === 'uPointGlassTints') {
6173
+ return {
6174
+ view: this.pointShadowArray?.glassTint?.view ?? blankArrayView,
6175
+ sampler: this.tintSampler,
6176
+ };
6026
6177
  }
6027
6178
  /*
6028
6179
  * The caller's image, or the stand-in. Read from the field rather than passed in, because
@@ -6060,10 +6211,6 @@ export class WebGPURenderer implements RendererApi {
6060
6211
  ? { view: blankView, sampler: blankSampler }
6061
6212
  : { view: albedo.view, sampler: albedo.sampler };
6062
6213
  }
6063
- /* The peel, where the profile asked for one. `uPeeledShadowEnabled` gates the sample. */
6064
- if (name === 'uPeeledShadowMap' && this.peelView !== null) {
6065
- return { view: this.peelView, sampler: shadowSampler };
6066
- }
6067
6214
  /*
6068
6215
  * The room, once a bake has filled it. Unbaked reads as the blank cube: nothing to mirror.
6069
6216
  *
@@ -6132,7 +6279,15 @@ export class WebGPURenderer implements RendererApi {
6132
6279
  * texels by construction, an offset that snapped to one being no offset at all.
6133
6280
  */
6134
6281
  if (name === 'uRefractScene') {
6135
- return { view: this.refractSnapshotView ?? blankView, sampler: iesSampler };
6282
+ this.refractSampler ??= device.createSampler({
6283
+ label: 'refract.sampler',
6284
+ magFilter: 'linear',
6285
+ minFilter: 'linear',
6286
+ mipmapFilter: 'linear',
6287
+ addressModeU: 'clamp-to-edge',
6288
+ addressModeV: 'clamp-to-edge',
6289
+ });
6290
+ return { view: this.refractSnapshotView ?? blankView, sampler: this.refractSampler };
6136
6291
  }
6137
6292
  /*
6138
6293
  * Every point light's shadow, in one binding.
@@ -6358,9 +6513,7 @@ export class WebGPURenderer implements RendererApi {
6358
6513
  */
6359
6514
  this.mediumLayout = createPostStageLayout(device, MEDIUM_UNIFORMS, MEDIUM_FRAG_SIZE, [
6360
6515
  { binding: MEDIUM_TEXTURES.uDepth, filterable: false },
6361
- { binding: MEDIUM_TEXTURES.uStaticShadowMap, filterable: false },
6362
- { binding: MEDIUM_TEXTURES.uPeeledShadowMap, filterable: false },
6363
- { binding: MEDIUM_TEXTURES.uDynamicShadowMap, filterable: false },
6516
+ { binding: MEDIUM_TEXTURES.uSunShadows, filterable: false, dimension: '2d-array' },
6364
6517
  ]);
6365
6518
  this.mediumUniforms = device.createBuffer({
6366
6519
  label: 'post.mediumUniforms',
@@ -6395,6 +6548,8 @@ export class WebGPURenderer implements RendererApi {
6395
6548
  this.taaLayout = createPostStageLayout(device, TAA_UNIFORMS, TAA_FRAG_SIZE, [
6396
6549
  { binding: TAA_TEXTURES.uScene },
6397
6550
  { binding: TAA_TEXTURES.uHistory },
6551
+ { binding: TAA_TEXTURES.uFlicker },
6552
+ { binding: TAA_TEXTURES.uFlickerMotion },
6398
6553
  { binding: TAA_TEXTURES.uDepth, filterable: false },
6399
6554
  ]);
6400
6555
  this.taaUniforms = device.createBuffer({
@@ -6590,7 +6745,7 @@ export class WebGPURenderer implements RendererApi {
6590
6745
  this.scatterDepthDraws.buffer,
6591
6746
  { binding: DEPTH_FRAG_BINDING, buffer: depthPassUniforms, size: DEPTH_FRAG_SIZE },
6592
6747
  DEPTH_PREVIOUS_BINDING,
6593
- blankView,
6748
+ blankArrayView,
6594
6749
  blankSampler,
6595
6750
  );
6596
6751
  this.shadowBindGroup = createDepthBindGroup(
@@ -6598,10 +6753,10 @@ export class WebGPURenderer implements RendererApi {
6598
6753
  this.shadowLayout,
6599
6754
  this.shadowDraws.buffer,
6600
6755
  depthPassUniforms,
6601
- blankView,
6756
+ blankArrayView,
6602
6757
  blankSampler,
6603
6758
  );
6604
- this.shadowBlankView = blankView;
6759
+ this.shadowBlankView = blankArrayView;
6605
6760
  this.shadowBlankSampler = blankSampler;
6606
6761
  this.shadowPassUniforms = depthPassUniforms;
6607
6762
 
@@ -6639,7 +6794,7 @@ export class WebGPURenderer implements RendererApi {
6639
6794
  this.shadowLayout,
6640
6795
  this.shadowDraws.buffer,
6641
6796
  peelPassUniforms,
6642
- this.shadowView,
6797
+ this.sunStaticArrayView,
6643
6798
  shadowSampler,
6644
6799
  );
6645
6800
  this.scatterDepthPeelBindGroup =
@@ -6651,7 +6806,7 @@ export class WebGPURenderer implements RendererApi {
6651
6806
  this.scatterDepthDraws.buffer,
6652
6807
  { binding: DEPTH_FRAG_BINDING, buffer: peelPassUniforms, size: DEPTH_FRAG_SIZE },
6653
6808
  DEPTH_PREVIOUS_BINDING,
6654
- this.shadowView,
6809
+ this.sunStaticArrayView,
6655
6810
  shadowSampler,
6656
6811
  );
6657
6812
 
@@ -7057,7 +7212,10 @@ export class WebGPURenderer implements RendererApi {
7057
7212
  this.perFrame.reset();
7058
7213
  this.materials.dirty();
7059
7214
  /* Spent by `runMotion` at the end of the last frame; a frame states its movers afresh. */
7060
- this.motionCount = 0;
7215
+ this.motionPass?.reset();
7216
+ this.latePass?.reset();
7217
+ this.moverFrame += 1;
7218
+ this.inFrame = true;
7061
7219
  /*
7062
7220
  * The canvas rather than the swap chain's texture, and the difference is that reading this
7063
7221
  * does not *acquire* anything. See `swapView`.
@@ -7378,7 +7536,27 @@ export class WebGPURenderer implements RendererApi {
7378
7536
  'what is kept and what a buffer is hinted as.',
7379
7537
  );
7380
7538
  }
7381
- mesh.update(this.surface.device, positions, normals);
7539
+ /*
7540
+ * **The rows before this rewrite are last frame's**, kept only for a reconstruction and only
7541
+ * on the first rewrite of a frame — a second must not replace where the vertices were last
7542
+ * frame with where they were a moment ago. A rewrite between frames is the next frame's. See
7543
+ * `changeFrames.ts`.
7544
+ */
7545
+ const motion = mesh.motion;
7546
+ let previous: GPUBuffer | null = null;
7547
+ if (motion !== null && this.reconstructionWanted) {
7548
+ const frame = frameOfChange(this.moverFrame, this.inFrame);
7549
+ if (keepsPrevious(motion.changed, frame)) {
7550
+ motion.previous ??= this.surface.device.createBuffer({
7551
+ label: 'recon.dynamicPrevious',
7552
+ size: mesh.vertexBuffers[0]?.size ?? 0,
7553
+ usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
7554
+ });
7555
+ previous = motion.previous;
7556
+ }
7557
+ motion.changed = frame;
7558
+ }
7559
+ mesh.update(this.surface.device, positions, normals, previous);
7382
7560
  }
7383
7561
 
7384
7562
  /**
@@ -8037,7 +8215,7 @@ export class WebGPURenderer implements RendererApi {
8037
8215
  depthLayer = 0,
8038
8216
  tint: Vec3 | null = null,
8039
8217
  /** Where it was last frame. See `RendererApi.drawMesh`, and `runMotion` for what reads it. */
8040
- previousModel: ArrayLike<number> | null = null,
8218
+ previousModel: ArrayLike<number> | Mover | null = null,
8041
8219
  ): void {
8042
8220
  /*
8043
8221
  * Skipped where the flag asks and the bounds say so.
@@ -8062,7 +8240,7 @@ export class WebGPURenderer implements RendererApi {
8062
8240
  * depth, so a motion pass drawing it would either be depth-rejected everywhere — wasted — or,
8063
8241
  * where it happened to pass, write motion for a surface no pixel of the frame belongs to.
8064
8242
  */
8065
- if (previousModel !== null) this.recordMotion(mesh, model, previousModel);
8243
+ if (this.tracksMotionNow()) this.recordDrawMotion(mesh, model, previousModel);
8066
8244
  this.submitMesh(mesh, model, tint, 1, false, { depthLayer });
8067
8245
  }
8068
8246
 
@@ -8225,7 +8403,13 @@ export class WebGPURenderer implements RendererApi {
8225
8403
  return;
8226
8404
  }
8227
8405
 
8228
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, this.viewProj);
8406
+ /* A blended draw landing after the upscale is drawn into an unjittered picture. */
8407
+ const late = blend && (this.drawsLate() || (this.oitReplaying && this.reconstructing));
8408
+ this.perDraw.writeFloats(
8409
+ slot,
8410
+ FLAT_VERT_FIELDS.uViewProj.offset,
8411
+ late ? this.correctedViewProj : this.viewProj,
8412
+ );
8229
8413
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uModel.offset, model);
8230
8414
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uTint.offset, tint ?? WHITE);
8231
8415
  /* The frame's wind, for a mesh carrying a sway lane. Written on this path and not the
@@ -8285,17 +8469,7 @@ export class WebGPURenderer implements RendererApi {
8285
8469
  * shades as an ordinary translucent one rather than sampling a texture holding the frame
8286
8470
  * before last. That is the defined state the two-backends rule asks for.
8287
8471
  */
8288
- const refraction = options.refraction ?? 0;
8289
- const refracting = refraction > 0 && this.takeRefractSnapshot();
8290
- if (refracting) {
8291
- this.perFrameFloats[this.materialField('uRefractStrength')] = refraction;
8292
- const refractTint = options.refractTint;
8293
- const tintAt = this.materialField('uRefractTint');
8294
- this.perFrameFloats[tintAt] = refractTint?.[0] ?? 1;
8295
- this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
8296
- this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
8297
- this.perFrameFloats[this.materialField('uRefractThickness')] = options.thicknessM ?? 0;
8298
- }
8472
+ const refracting = this.bindSeeThrough(options);
8299
8473
  /* Taken for itself when any of those differs from the pass, and put back below: the rule both
8300
8474
  backends count material changes by. See `materialChanges.ts`. */
8301
8475
  const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
@@ -8355,7 +8529,7 @@ export class WebGPURenderer implements RendererApi {
8355
8529
  * pays the compile it was trying to avoid. That is a slow frame; the alternative is a
8356
8530
  * thrown error in the middle of one.
8357
8531
  */
8358
- const pipelines = this.targetPipelines();
8532
+ const pipelines = blend ? this.blendedPipelines() : this.targetPipelines();
8359
8533
  let pipeline = pipelines.peek(key);
8360
8534
  if (pipeline === undefined) {
8361
8535
  const present = this.meshPresent.get(base);
@@ -8403,7 +8577,7 @@ export class WebGPURenderer implements RendererApi {
8403
8577
  * the uniform rings are the state arena this design would otherwise have had to build —
8404
8578
  * so the command carries them as its two dynamic offsets.
8405
8579
  */
8406
- const command = this.recordDraw(0, this.currentTarget());
8580
+ const command = blend ? this.recordBlended(0) : this.recordDraw(0, this.currentTarget());
8407
8581
  if (command !== null) {
8408
8582
  command.pipeline = pipeline;
8409
8583
  command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
@@ -8440,11 +8614,9 @@ export class WebGPURenderer implements RendererApi {
8440
8614
  if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
8441
8615
  if (!fog) this.perFrameInts[this.materialField('uFogEnabled')] = 1;
8442
8616
  }
8443
- if (refracting) {
8444
- /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
8445
- shared, so a strength left set is worn by everything drawn after it. */
8446
- this.perFrameFloats[this.materialField('uRefractStrength')] = 0;
8447
- }
8617
+ /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
8618
+ shared, so a strength left set is worn by everything drawn after it. */
8619
+ if (refracting) this.releaseSeeThrough();
8448
8620
  if (!toneMapped) {
8449
8621
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
8450
8622
  }
@@ -8518,12 +8690,35 @@ export class WebGPURenderer implements RendererApi {
8518
8690
  /** Push placement and colour. Only the live prefix; see `GpuInstancedBatch.upload`. */
8519
8691
  uploadInstanced(batch: GpuInstancedBatch, data: MeshInstances): void {
8520
8692
  if (this.surface.lost) return;
8521
- batch.upload(this.surface.device.queue, data);
8693
+ /*
8694
+ * **The placement before this upload is last frame's**, kept only for a reconstruction and only
8695
+ * on a frame's first upload, for the reason `updateMesh` gives. See `changeFrames.ts`.
8696
+ */
8697
+ let previous: GPUBuffer | null = null;
8698
+ if (this.reconstructionWanted) {
8699
+ const frame = frameOfChange(this.moverFrame, this.inFrame);
8700
+ if (keepsPrevious(batch.changed, frame)) {
8701
+ batch.previousBuffer ??= this.surface.device.createBuffer({
8702
+ label: 'recon.instancesPrevious',
8703
+ size: batch.capacity * INSTANCE_STRIDE,
8704
+ usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
8705
+ });
8706
+ previous = batch.previousBuffer;
8707
+ }
8708
+ batch.changed = frame;
8709
+ }
8710
+ batch.upload(this.surface.device.queue, data, previous);
8522
8711
  }
8523
8712
 
8524
8713
  /** Draw every live instance, opaque. */
8525
8714
  drawInstanced(batch: GpuInstancedBatch, data: MeshInstances): void {
8526
8715
  this.submitInstanced(batch, data, 1, false, {});
8716
+ /* A batch that moved this frame pairs each slot with last frame's; the batch is the identity. */
8717
+ const previous = batch.previousBuffer;
8718
+ if (previous !== null && batch.changed === this.moverFrame && this.tracksMotionNow()) {
8719
+ const paired = pairedInstances(batch.previousCount, Math.min(data.count, batch.capacity));
8720
+ if (paired > 0) this.motionPass?.recordInstanced(batch.mesh, batch.buffer, previous, paired);
8721
+ }
8527
8722
  }
8528
8723
 
8529
8724
  /** Draw every live instance, blended. See `drawTranslucentMesh` for what the options mean. */
@@ -8542,6 +8737,45 @@ export class WebGPURenderer implements RendererApi {
8542
8737
  batch.dispose();
8543
8738
  }
8544
8739
 
8740
+ /**
8741
+ * Write what a translucent draw shows through it into the material scratch: refraction's bend and
8742
+ * absorption, and glass. Returns whether the frame's copy is there to read, and so whether
8743
+ * `releaseSeeThrough` owes the scratch back.
8744
+ *
8745
+ * **A glass draw takes the copy even when it does not bend**, since a pane shows what is behind it
8746
+ * either way. A copy that cannot be taken — a mirror, a probe bake — leaves both off, so the pane
8747
+ * shades as ordinary translucency rather than sampling a stand-in. See `glass.ts`.
8748
+ */
8749
+ private bindSeeThrough(options: TranslucentMeshOptions): boolean {
8750
+ const refraction = options.refraction ?? 0;
8751
+ const glassy = resolveGlass(options.glass, this.glassScratch);
8752
+ if ((refraction <= 0 && !glassy) || !this.takeRefractSnapshot()) return false;
8753
+ const glass = this.glassScratch;
8754
+ /* Strength, path thickness, transmission, frost: one vector, for the budget the shader states. */
8755
+ const seeAt = this.materialField('uSeeThrough');
8756
+ this.perFrameFloats[seeAt] = Math.max(refraction, 0);
8757
+ this.perFrameFloats[seeAt + 1] = options.thicknessM ?? 0;
8758
+ this.perFrameFloats[seeAt + 2] = glass.transmission;
8759
+ this.perFrameFloats[seeAt + 3] = glass.frost;
8760
+ const refractTint = options.refractTint;
8761
+ const tintAt = this.materialField('uRefractTint');
8762
+ this.perFrameFloats[tintAt] = refractTint?.[0] ?? 1;
8763
+ this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
8764
+ this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
8765
+ const glassTintAt = this.materialField('uGlassTint');
8766
+ this.perFrameFloats[glassTintAt] = glass.tint[0];
8767
+ this.perFrameFloats[glassTintAt + 1] = glass.tint[1];
8768
+ this.perFrameFloats[glassTintAt + 2] = glass.tint[2];
8769
+ return true;
8770
+ }
8771
+
8772
+ /** Put the scratch back after `bindSeeThrough`: refraction and glass off. */
8773
+ private releaseSeeThrough(): void {
8774
+ const seeAt = this.materialField('uSeeThrough');
8775
+ this.perFrameFloats[seeAt] = 0;
8776
+ this.perFrameFloats[seeAt + 2] = 0;
8777
+ }
8778
+
8545
8779
  /**
8546
8780
  * One draw, one material slot, one uniform slot, however many instances.
8547
8781
  *
@@ -8578,7 +8812,11 @@ export class WebGPURenderer implements RendererApi {
8578
8812
  return;
8579
8813
  }
8580
8814
 
8581
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, this.viewProj);
8815
+ this.perDraw.writeFloats(
8816
+ slot,
8817
+ FLAT_VERT_FIELDS.uViewProj.offset,
8818
+ blend && this.drawsLate() ? this.correctedViewProj : this.viewProj,
8819
+ );
8582
8820
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
8583
8821
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
8584
8822
  this.perDraw.writeInt(slot, FLAT_VERT_FIELDS.uHasTangents.offset, mesh.hasTangents ? 1 : 0);
@@ -8601,8 +8839,9 @@ export class WebGPURenderer implements RendererApi {
8601
8839
 
8602
8840
  const depthWrite = options.depthWrite ?? true;
8603
8841
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
8604
- /* An instanced draw does not refract. See `materialChanges.ts`. */
8605
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting: false });
8842
+ /* A blended batch shows what is behind it — refraction or glass — as a single draw does. */
8843
+ const refracting = blend && this.bindSeeThrough(options);
8844
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
8606
8845
  if (own) this.materials.dirty();
8607
8846
  const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
8608
8847
  /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
@@ -8613,7 +8852,7 @@ export class WebGPURenderer implements RendererApi {
8613
8852
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8614
8853
  `${this.materialDoubleSided ? '|2s' : ''}`;
8615
8854
 
8616
- const pipelines = this.targetPipelines();
8855
+ const pipelines = blend ? this.blendedPipelines() : this.targetPipelines();
8617
8856
  let pipeline = pipelines.peek(key);
8618
8857
  if (pipeline === undefined) {
8619
8858
  const present = this.meshPresent.get(base);
@@ -8640,7 +8879,7 @@ export class WebGPURenderer implements RendererApi {
8640
8879
 
8641
8880
  const material = this.materialSlotForDraw();
8642
8881
  if (material !== null) {
8643
- const command = this.recordDraw(0, this.currentTarget());
8882
+ const command = blend ? this.recordBlended(0) : this.recordDraw(0, this.currentTarget());
8644
8883
  if (command !== null) {
8645
8884
  command.pipeline = pipeline;
8646
8885
  command.bindGroup = this.flatBindGroupFor(false, false, null);
@@ -8684,6 +8923,7 @@ export class WebGPURenderer implements RendererApi {
8684
8923
  if (!toneMapped) {
8685
8924
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
8686
8925
  }
8926
+ if (refracting) this.releaseSeeThrough();
8687
8927
  if (own) this.materials.dirty();
8688
8928
  }
8689
8929
 
@@ -8877,6 +9117,8 @@ export class WebGPURenderer implements RendererApi {
8877
9117
  this.lightViewProj.set(lightViewProj as Float32Array);
8878
9118
  mat4.multiply(this.correctedLightViewProj, SHADOW_CLIP_CORRECTION, lightViewProj);
8879
9119
  this.shadowLayerIsPeel = peel;
9120
+ this.shadowLayerIsDynamic = dynamic;
9121
+ this.glassCasters.clear();
8880
9122
  /*
8881
9123
  * A frame's peel is only as good as the pass that fills it, and the static layer is the one
8882
9124
  * that opens first.
@@ -8984,12 +9226,349 @@ export class WebGPURenderer implements RendererApi {
8984
9226
  }
8985
9227
 
8986
9228
  /** Close the shadow pass and submit it, so the frame can sample what it wrote. */
9229
+ /**
9230
+ * Add the sun's two glass layers, once, carrying every layer it holds across on `encoder`.
9231
+ *
9232
+ * **Copied rather than re-baked**, for the reason `SunShadowArray.growForGlass` gives: the static
9233
+ * map is the consumer's to bake and may be baked once, and the glass that grows the texture
9234
+ * arrives in the very pass that drew it. Every view of the old texture is stale afterwards, so
9235
+ * every group holding one is rebuilt here — the lit pass's, the volumes', the medium's and the
9236
+ * peel's — and the old texture is destroyed only once this encoder is submitted.
9237
+ */
9238
+ private growSunForGlass(encoder: GPUCommandEncoder): void {
9239
+ if (this.sunGlassViews !== null) return;
9240
+ const device = this.surface.device;
9241
+ const size = this.quality.directionalShadowMapSize;
9242
+ const maps = this.sunLayerCount;
9243
+ /* The glass layers at their fixed places after the three maps: see `SUN_GLASS_STATIC_LAYER`. */
9244
+ const layers = SUN_GLASS_MOVING_LAYER + 1;
9245
+ const old = this.sunShadowTexture;
9246
+ const texture = device.createTexture({
9247
+ label: 'shadow.sun',
9248
+ size: [size, size, layers],
9249
+ format: SHADOW_FORMAT,
9250
+ usage: SUN_USAGE,
9251
+ });
9252
+ for (let layer = 0; layer < maps; layer++) {
9253
+ encoder.copyTextureToTexture(
9254
+ { texture: old, origin: { x: 0, y: 0, z: layer } },
9255
+ { texture, origin: { x: 0, y: 0, z: layer } },
9256
+ [size, size, 1],
9257
+ );
9258
+ }
9259
+ this.retiredSunTexture = old;
9260
+ this.sunShadowTexture = texture;
9261
+ this.sunLayerCount = layers;
9262
+ const views = sunViewsOf(texture, maps, layers);
9263
+ this.sunArrayView = views.array;
9264
+ this.sunStaticArrayView = views.staticArray;
9265
+ this.shadowView = views.static;
9266
+ this.dynamicView = views.dynamic;
9267
+ this.peelView = views.peel;
9268
+ this.sunGlassViews = views.glass;
9269
+ /* Both glass layers born at the far plane: nothing is nearer the sun than a pane not drawn. */
9270
+ for (const view of views.glass ?? []) {
9271
+ encoder
9272
+ .beginRenderPass({
9273
+ label: 'shadow.glass.clear',
9274
+ colorAttachments: [],
9275
+ depthStencilAttachment: {
9276
+ view,
9277
+ depthClearValue: 1,
9278
+ depthLoadOp: 'clear',
9279
+ depthStoreOp: 'store',
9280
+ },
9281
+ })
9282
+ .end();
9283
+ }
9284
+ this.rebuildSunGroups();
9285
+ }
9286
+
9287
+ /** Every group that holds a view of the sun's texture, rebuilt after it has been replaced. */
9288
+ private rebuildSunGroups(): void {
9289
+ const device = this.surface.device;
9290
+ const peelUniforms = this.shadowPeelPassUniforms;
9291
+ const peelSampler = this.shadowPeelSampler;
9292
+ if (peelUniforms !== null && peelSampler !== null) {
9293
+ this.shadowPeelBindGroup = createDepthBindGroup(
9294
+ device,
9295
+ this.shadowLayout,
9296
+ this.shadowDraws.buffer,
9297
+ peelUniforms,
9298
+ this.sunStaticArrayView,
9299
+ peelSampler,
9300
+ );
9301
+ this.scatterDepthPeelBindGroup = createScatterDepthBindGroup(
9302
+ device,
9303
+ this.scatterDepthLayout,
9304
+ this.scatterDepthDraws.buffer,
9305
+ { binding: DEPTH_FRAG_BINDING, buffer: peelUniforms, size: DEPTH_FRAG_SIZE },
9306
+ DEPTH_PREVIOUS_BINDING,
9307
+ this.sunStaticArrayView,
9308
+ peelSampler,
9309
+ );
9310
+ }
9311
+ this.cutoutPeelGroups.clear();
9312
+ this.rebuildFlatGroupsForNewRings();
9313
+ this.lightVolumeBindGroup = this.buildLightVolumeBindGroup();
9314
+ this.mediumBindGroup = this.buildMediumBindGroup();
9315
+ }
9316
+
9317
+ /** The sun's tint target, made the first time glass casts and born clear on `encoder`. */
9318
+ private ensureSunTint(encoder: GPUCommandEncoder): SunTintTarget {
9319
+ if (this.sunTint !== null) return this.sunTint;
9320
+ const size = this.quality.directionalShadowMapSize;
9321
+ const edge = this.quality.glassShadows === 'half' ? Math.max(1, size >> 1) : size;
9322
+ const device = this.surface.device;
9323
+ const texture = device.createTexture({
9324
+ label: 'shadow.sunTint',
9325
+ size: [edge, edge, 2],
9326
+ format: 'rgba8unorm',
9327
+ mipLevelCount: Math.floor(Math.log2(edge)) + 1,
9328
+ usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
9329
+ });
9330
+ const layer = (index: number): GPUTextureView =>
9331
+ texture.createView({
9332
+ dimension: '2d',
9333
+ baseArrayLayer: index,
9334
+ arrayLayerCount: 1,
9335
+ baseMipLevel: 0,
9336
+ mipLevelCount: 1,
9337
+ });
9338
+ const mips = (index: number): SnapshotMips =>
9339
+ new SnapshotMips(device, this.pipelines, texture, 'rgba8unorm', index, 'shadow.tint.mips');
9340
+ const target: SunTintTarget = {
9341
+ texture,
9342
+ layers: [layer(0), layer(1)],
9343
+ array: texture.createView({ dimension: '2d-array' }),
9344
+ mips: [mips(0), mips(1)],
9345
+ };
9346
+ for (const view of target.layers) {
9347
+ encoder
9348
+ .beginRenderPass({
9349
+ label: 'shadow.tint.clear',
9350
+ colorAttachments: [{ view, loadOp: 'clear', clearValue: [1, 1, 1, 1], storeOp: 'store' }],
9351
+ })
9352
+ .end();
9353
+ }
9354
+ this.sunTint = target;
9355
+ this.rebuildFlatGroupsForNewRings();
9356
+ return target;
9357
+ }
9358
+
9359
+ /**
9360
+ * Draw the pass's glass twice: where the nearest pane is, and what the panes let through.
9361
+ *
9362
+ * After the opaque casters, on the pass's own encoder. A layer that has held glass is redrawn
9363
+ * even when this pass offered none, so glass that has gone takes its colour with it.
9364
+ */
9365
+ private flushSunGlass(encoder: GPUCommandEncoder, which: 'static' | 'dynamic'): void {
9366
+ const list = this.glassCasters;
9367
+ if (this.quality.glassShadows === 'off') return;
9368
+ if (list.count === 0 && this.sunGlassViews === null) return;
9369
+ if (list.count > 0) this.growSunForGlass(encoder);
9370
+ const glassViews = this.sunGlassViews;
9371
+ if (glassViews === null) return;
9372
+
9373
+ /* Where the nearest pane is: the depth pipelines, through the sink every opaque caster took. */
9374
+ this.shadowPass = encoder.beginRenderPass({
9375
+ label: 'shadow.glass',
9376
+ colorAttachments: [],
9377
+ depthStencilAttachment: {
9378
+ view: glassViews[which === 'static' ? 0 : 1],
9379
+ depthClearValue: 1,
9380
+ depthLoadOp: 'clear',
9381
+ depthStoreOp: 'store',
9382
+ },
9383
+ });
9384
+ this.drawingGlassDepth = true;
9385
+ list.replay(this.glassDepthSink);
9386
+ this.drawingGlassDepth = false;
9387
+ this.shadowPass.end();
9388
+ this.shadowPass = null;
9389
+
9390
+ /* What the panes let through, every one on a ray, multiplied. */
9391
+ const target = this.ensureSunTint(encoder);
9392
+ const tint = (this.glassTint ??= new GpuGlassTint(this.surface.device));
9393
+ tint.frag.reset();
9394
+ /* The direction the light travels, off the light matrix's depth row; the sign is immaterial,
9395
+ since the tint takes the cosine absolutely, and w 0 says it is a direction. */
9396
+ const m = this.correctedLightViewProj;
9397
+ const length = Math.hypot(m[2] as number, m[6] as number, m[10] as number) || 1;
9398
+ this.glassLight[0] = (m[2] as number) / length;
9399
+ this.glassLight[1] = (m[6] as number) / length;
9400
+ this.glassLight[2] = (m[10] as number) / length;
9401
+ this.glassLight[3] = 0;
9402
+ this.tintPass = encoder.beginRenderPass({
9403
+ label: 'shadow.tint',
9404
+ colorAttachments: [
9405
+ {
9406
+ view: target.layers[which === 'static' ? 0 : 1],
9407
+ loadOp: 'clear',
9408
+ clearValue: [1, 1, 1, 1],
9409
+ storeOp: 'store',
9410
+ },
9411
+ ],
9412
+ });
9413
+ list.replay(this.glassTintSink);
9414
+ this.tintPass.end();
9415
+ this.tintPass = null;
9416
+ tint.frag.flush();
9417
+ /* The chain frost reads, for the layer just drawn. */
9418
+ target.mips[which === 'static' ? 0 : 1].record(encoder);
9419
+ }
9420
+
9421
+ /** The glass list into a glass depth layer: `casterSink` itself, with glass let through. */
9422
+ private readonly glassDepthSink: GlassReplaySink = {
9423
+ mesh: (mesh, model, material) => this.casterSink.mesh(mesh, model, material),
9424
+ instanced: (batch, data, material) => this.casterSink.instanced?.(batch, data, material),
9425
+ skinnedMesh: (mesh, model, palette, material) =>
9426
+ this.casterSink.skinnedMesh(mesh, model, palette, material),
9427
+ };
9428
+
9429
+ /**
9430
+ * The glass list into a tint layer, through the tint pipelines, **culling nothing**: every pane on
9431
+ * a ray counts, whichever way it faces. See the other backend's `glassTintSink`.
9432
+ */
9433
+ private readonly glassTintSink: GlassReplaySink = {
9434
+ mesh: (mesh, model, material, glass) => {
9435
+ const pass = this.tintPass;
9436
+ const tint = this.glassTint;
9437
+ if (pass === null || tint === null) return;
9438
+ const geometry = mesh as GpuMesh & { key?: string };
9439
+ if (geometry.vertexBuffers === undefined) return;
9440
+ const cutout = cutoutOf(material);
9441
+ const albedo = cutout === null ? null : (cutout.albedo as unknown as GpuSurfaceTexture);
9442
+ const slot = this.shadowDraws.allocate();
9443
+ const pane = tint.frag.allocate();
9444
+ if (slot === null || pane === null) return;
9445
+ this.shadowDraws.writeFloats(
9446
+ slot,
9447
+ DEPTH_VERT_FIELDS.uLightViewProj.offset,
9448
+ this.correctedLightViewProj,
9449
+ );
9450
+ this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uModel.offset, model as Float32Array);
9451
+ this.writeShadowWind(slot);
9452
+ if (cutout !== null) this.writeCutout(slot, cutout, DEPTH_CUTOUT_VERT_FIELDS);
9453
+ tint.writePane(pane, glass, this.glassLight);
9454
+ pass.setPipeline(
9455
+ tint.pipeline(
9456
+ this.pipelines,
9457
+ albedo === null ? 'rigid' : 'cutout',
9458
+ geometry.key ?? '',
9459
+ this.presentOf(geometry),
9460
+ 'none',
9461
+ ),
9462
+ );
9463
+ pass.setBindGroup(
9464
+ 0,
9465
+ albedo === null
9466
+ ? tint.group(this.shadowDraws.buffer)
9467
+ : tint.cutoutGroup(this.shadowDraws.buffer, albedo.view, albedo.sampler),
9468
+ [slot, pane],
9469
+ );
9470
+ for (let index = 0; index < geometry.vertexBuffers.length; index++) {
9471
+ pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
9472
+ }
9473
+ pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
9474
+ pass.drawIndexed(geometry.indexCount);
9475
+ },
9476
+ instanced: (batch, data, material, glass) => {
9477
+ const pass = this.tintPass;
9478
+ const tint = this.glassTint;
9479
+ if (pass === null || tint === null) return;
9480
+ const gpuBatch = batch as GpuInstancedBatch;
9481
+ const geometry = gpuBatch.mesh as GpuMesh & { key?: string };
9482
+ if (geometry.vertexBuffers === undefined) return;
9483
+ const count = Math.min(data.count, gpuBatch.capacity);
9484
+ if (count === 0) return;
9485
+ const cutout = cutoutOf(material);
9486
+ const albedo = cutout === null ? null : (cutout.albedo as unknown as GpuSurfaceTexture);
9487
+ const slot = this.shadowDraws.allocate();
9488
+ const pane = tint.frag.allocate();
9489
+ if (slot === null || pane === null) return;
9490
+ this.shadowDraws.writeFloats(
9491
+ slot,
9492
+ DEPTH_VERT_FIELDS.uLightViewProj.offset,
9493
+ this.correctedLightViewProj,
9494
+ );
9495
+ if (cutout !== null) this.writeCutout(slot, cutout, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS);
9496
+ tint.writePane(pane, glass, this.glassLight);
9497
+ pass.setPipeline(
9498
+ tint.pipeline(
9499
+ this.pipelines,
9500
+ albedo === null ? 'instanced' : 'instancedCutout',
9501
+ geometry.key ?? '',
9502
+ this.presentOf(geometry),
9503
+ 'none',
9504
+ ),
9505
+ );
9506
+ pass.setBindGroup(
9507
+ 0,
9508
+ albedo === null
9509
+ ? tint.group(this.shadowDraws.buffer)
9510
+ : tint.cutoutGroup(this.shadowDraws.buffer, albedo.view, albedo.sampler),
9511
+ [slot, pane],
9512
+ );
9513
+ let index = 0;
9514
+ for (; index < geometry.vertexBuffers.length; index++) {
9515
+ pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
9516
+ }
9517
+ pass.setVertexBuffer(index, gpuBatch.buffer);
9518
+ pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
9519
+ pass.drawIndexed(geometry.indexCount, count);
9520
+ },
9521
+ skinnedMesh: (mesh, model, palette, _material, glass) => {
9522
+ const pass = this.tintPass;
9523
+ const tint = this.glassTint;
9524
+ if (pass === null || tint === null) return;
9525
+ const geometry = mesh as GpuMesh & { key?: string };
9526
+ if (geometry.vertexBuffers === undefined) return;
9527
+ const paletteSlot = this.skinPalettes.take(this.surface.device, palette);
9528
+ const view = paletteSlot === null ? null : this.skinPalettes.view(paletteSlot);
9529
+ if (view === null) return;
9530
+ const slot = this.shadowDraws.allocate();
9531
+ const pane = tint.frag.allocate();
9532
+ if (slot === null || pane === null) return;
9533
+ this.shadowDraws.writeFloats(
9534
+ slot,
9535
+ DEPTH_VERT_FIELDS.uLightViewProj.offset,
9536
+ this.correctedLightViewProj,
9537
+ );
9538
+ this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uModel.offset, model as Float32Array);
9539
+ this.writeShadowWind(slot);
9540
+ tint.writePane(pane, glass, this.glassLight);
9541
+ pass.setPipeline(
9542
+ tint.pipeline(
9543
+ this.pipelines,
9544
+ 'skinned',
9545
+ geometry.key ?? '',
9546
+ this.presentOf(geometry),
9547
+ 'none',
9548
+ ),
9549
+ );
9550
+ pass.setBindGroup(0, tint.skinnedGroup(this.shadowDraws.buffer, view), [slot, pane]);
9551
+ for (let index = 0; index < geometry.vertexBuffers.length; index++) {
9552
+ pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
9553
+ }
9554
+ pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
9555
+ pass.drawIndexed(geometry.indexCount);
9556
+ },
9557
+ };
9558
+
8987
9559
  endShadowPass(): void {
8988
9560
  if (this.surface.lost || this.shadowPass === null || this.shadowEncoder === null) return;
9561
+ this.shadowPass.end();
9562
+ /* The peel keeps no glass: its layer is the second opaque occluder and nothing else. */
9563
+ if (!this.shadowLayerIsPeel) {
9564
+ this.flushSunGlass(this.shadowEncoder, this.shadowLayerIsDynamic ? 'dynamic' : 'static');
9565
+ }
8989
9566
  this.shadowDraws.flush();
8990
9567
  this.scatterDepthDraws.flush();
8991
- this.shadowPass.end();
8992
9568
  this.surface.device.queue.submit([this.shadowEncoder.finish()]);
9569
+ /* A growth's old texture, copied out of by what was just submitted and needed no longer. */
9570
+ this.retiredSunTexture?.destroy();
9571
+ this.retiredSunTexture = null;
8993
9572
  /* The map now holds a rendered peel rather than whatever it was created with. */
8994
9573
  if (this.shadowLayerIsPeel) this.peelFilled = true;
8995
9574
  this.shadowLayerIsPeel = false;
@@ -9019,6 +9598,8 @@ export class WebGPURenderer implements RendererApi {
9019
9598
  private preparedPointLightCount = 0;
9020
9599
  /** See `renderer.ts`: built when a world reports its lights, and rebuilt only to grow. */
9021
9600
  private pointShadowArray: GpuPointShadowArray | null = null;
9601
+ /** The array glass replaced, destroyed once the round that copied out of it is submitted. */
9602
+ private retiredPointTexture: GPUTexture | null = null;
9022
9603
  private readonly bakeScratch = createBakeScratch(POINT_SHADOW_POOL);
9023
9604
  /** Said once per renderer; see `updatePointShadows`. */
9024
9605
  private warnedNoPointShadowArray = false;
@@ -9204,6 +9785,7 @@ export class WebGPURenderer implements RendererApi {
9204
9785
  */
9205
9786
  this.shadowDraws.reset();
9206
9787
  this.scatterDepthDraws.reset();
9788
+ this.glassTint?.frag.reset();
9207
9789
  this.shadowEncoder = this.surface.device.createCommandEncoder({ label: 'pointShadow' });
9208
9790
  /* A cube face is a shadow pass like any other, and the courtyard is where it matters: six of
9209
9791
  them a frame is the difference this slot exists to make visible. */
@@ -9231,7 +9813,7 @@ export class WebGPURenderer implements RendererApi {
9231
9813
  light.z,
9232
9814
  light.radius,
9233
9815
  (view, viewProj) => this.bakeFace(view, viewProj, casters as ShadowCasters),
9234
- (layer, face, far, near) => this.resolveFace(layer, face, far, near),
9816
+ (layer, face, far, near, x, y, z) => this.resolveFace(layer, face, far, near, x, y, z),
9235
9817
  this.faceViewProj,
9236
9818
  light.shadowNear,
9237
9819
  light.sourceRadius,
@@ -9245,10 +9827,13 @@ export class WebGPURenderer implements RendererApi {
9245
9827
  * encoder, a second submit and a second `shadows` timer slot for work that is the same work.
9246
9828
  */
9247
9829
  this.updateAreaShadows(areaLights, frameDt, staticCasters, dynamicCasters);
9830
+ /* The chain frost reads, once for the round, for the lights whose glass it drew or cleared. */
9831
+ this.pointShadowArray?.glassTint?.recordMips(this.shadowEncoder);
9248
9832
  } finally {
9249
9833
  this.depthCullMode = 'back';
9250
9834
  this.shadowDraws.flush();
9251
9835
  this.scatterDepthDraws.flush();
9836
+ this.glassTint?.frag.flush();
9252
9837
  /*
9253
9838
  * **Before the submit, and that ordering is the whole point of the ring.** A write to the
9254
9839
  * queue lands ahead of the encoder's commands whenever it is issued, so uploading per
@@ -9260,6 +9845,9 @@ export class WebGPURenderer implements RendererApi {
9260
9845
  this.shadowEncoder = null;
9261
9846
  this.shadowPass = null;
9262
9847
  this.gpuTimer.end();
9848
+ /* An interleaving's old array, copied out of by what was just submitted. */
9849
+ this.retiredPointTexture?.destroy();
9850
+ this.retiredPointTexture = null;
9263
9851
  }
9264
9852
 
9265
9853
  /*
@@ -9270,6 +9858,10 @@ export class WebGPURenderer implements RendererApi {
9270
9858
  * written every frame regardless.
9271
9859
  */
9272
9860
  pool.resolve(this.resolvedPointShadows);
9861
+ /* As the array stores them: 2L once glass has interleaved it. See `strideLayers`. */
9862
+ const stride = this.pointShadowArray.stride;
9863
+ strideLayers(this.resolvedPointShadows.layers, stride);
9864
+ strideLayers(this.resolvedPointShadows.liveLayers, stride);
9273
9865
  }
9274
9866
 
9275
9867
  /**
@@ -9314,7 +9906,8 @@ export class WebGPURenderer implements RendererApi {
9314
9906
  light.z,
9315
9907
  range,
9316
9908
  (view, viewProj) => this.bakeFace(view, viewProj, casters as ShadowCasters),
9317
- (layer, face, far, mapNear) => this.resolveFace(layer, face, far, mapNear),
9909
+ (layer, face, far, mapNear, x, y, z) =>
9910
+ this.resolveFace(layer, face, far, mapNear, x, y, z),
9318
9911
  this.faceViewProj,
9319
9912
  near,
9320
9913
  /* The filter reads the emitter's size, never the bake. See `renderer.ts`'s copy. */
@@ -9323,6 +9916,9 @@ export class WebGPURenderer implements RendererApi {
9323
9916
  ),
9324
9917
  );
9325
9918
  set.resolve(areaLights, this.resolvedAreaShadows);
9919
+ const stride = this.pointShadowArray?.stride ?? 1;
9920
+ strideLayers(this.resolvedAreaShadows.layer, stride);
9921
+ strideLayers(this.resolvedAreaShadows.liveLayer, stride);
9326
9922
  }
9327
9923
 
9328
9924
  /**
@@ -9350,9 +9946,27 @@ export class WebGPURenderer implements RendererApi {
9350
9946
  depthStoreOp: 'store',
9351
9947
  },
9352
9948
  });
9949
+ /* A face is an enumeration of its own: its glass is its own. */
9950
+ this.glassCasters.clear();
9353
9951
  casters(this.casterSink);
9354
9952
  this.shadowPass.end();
9355
9953
  this.shadowPass = null;
9954
+ /*
9955
+ * The first face to offer glass interleaves the array, before its opaque depth is resolved:
9956
+ * `PointShadowArray.growForGlass`. The lit pass's groups hold the old array's view, so they
9957
+ * are rebuilt, and the old texture goes once this round's encoder — which copies out of it —
9958
+ * is submitted.
9959
+ */
9960
+ const array = this.pointShadowArray;
9961
+ if (array !== null && this.glassCasters.count > 0 && this.quality.glassShadows !== 'off') {
9962
+ const tintEdge =
9963
+ this.quality.glassShadows === 'half' ? OCTAHEDRAL_EDGE >> 1 : OCTAHEDRAL_EDGE;
9964
+ const old = array.growForGlass(encoder, tintEdge, this.pipelines);
9965
+ if (old !== null) {
9966
+ this.retiredPointTexture = old;
9967
+ this.rebuildFlatBindGroup();
9968
+ }
9969
+ }
9356
9970
  }
9357
9971
 
9358
9972
  /**
@@ -9362,11 +9976,70 @@ export class WebGPURenderer implements RendererApi {
9362
9976
  * serve every light: the face is consumed before the next one is drawn. See
9363
9977
  * `pointShadowArray.ts` for why the regions are disjoint.
9364
9978
  */
9365
- private resolveFace(layer: number, face: number, far: number, near: number): void {
9979
+ private resolveFace(
9980
+ layer: number,
9981
+ face: number,
9982
+ far: number,
9983
+ near: number,
9984
+ x: number,
9985
+ y: number,
9986
+ z: number,
9987
+ ): void {
9366
9988
  const encoder = this.shadowEncoder;
9367
9989
  const array = this.pointShadowArray;
9368
9990
  if (encoder === null || array === null) return;
9369
9991
  array.resolve(encoder, layer, face, far, near);
9992
+ const tintTarget = array.glassTint;
9993
+ if (tintTarget === null || this.quality.glassShadows === 'off') return;
9994
+ /*
9995
+ * The face's glass, twice, as `PointShadowMap.bakeGlassFace` draws it on the other backend. A
9996
+ * face that offers none still clears a region a previous owner's glass is in, from scratches
9997
+ * cleared and not drawn into, because a pool slot changes hands with its layer.
9998
+ */
9999
+ const offered = this.glassCasters.count > 0;
10000
+ if (!offered && !tintTarget.held(layer, face)) return;
10001
+ this.shadowPass = encoder.beginRenderPass({
10002
+ label: 'pointShadow.glass',
10003
+ colorAttachments: [],
10004
+ depthStencilAttachment: {
10005
+ view: array.scratchView,
10006
+ depthClearValue: 1,
10007
+ depthLoadOp: 'clear',
10008
+ depthStoreOp: 'store',
10009
+ },
10010
+ });
10011
+ if (offered) {
10012
+ this.drawingGlassDepth = true;
10013
+ this.glassCasters.replay(this.glassDepthSink);
10014
+ this.drawingGlassDepth = false;
10015
+ }
10016
+ this.shadowPass.end();
10017
+ this.shadowPass = null;
10018
+ array.resolveGlass(encoder, layer, face, far, near);
10019
+
10020
+ this.tintPass = encoder.beginRenderPass({
10021
+ label: 'pointShadow.tint',
10022
+ colorAttachments: [
10023
+ {
10024
+ view: tintTarget.scratchView,
10025
+ loadOp: 'clear',
10026
+ clearValue: [1, 1, 1, 1],
10027
+ storeOp: 'store',
10028
+ },
10029
+ ],
10030
+ });
10031
+ if (offered) {
10032
+ this.glassTint ??= new GpuGlassTint(this.surface.device);
10033
+ /* A place, w 1: each pane's angle to the lamp comes from its own position. */
10034
+ this.glassLight[0] = x;
10035
+ this.glassLight[1] = y;
10036
+ this.glassLight[2] = z;
10037
+ this.glassLight[3] = 1;
10038
+ this.glassCasters.replay(this.glassTintSink);
10039
+ }
10040
+ this.tintPass.end();
10041
+ this.tintPass = null;
10042
+ tintTarget.resolve(encoder, layer, face, offered);
9370
10043
  }
9371
10044
 
9372
10045
  private buildFlatBindGroup(): GPUBindGroup {
@@ -9726,8 +10399,9 @@ export class WebGPURenderer implements RendererApi {
9726
10399
  * drawn over a reflective floor rather than under it.
9727
10400
  */
9728
10401
  this.runReflections(encoder);
9729
- /* The translucent set, into its two buffers and back over the scene. */
9730
- this.runOit(encoder);
10402
+ /* The translucent set, into its two buffers and back over the scene — after the upscale instead
10403
+ while reconstructing, where `runLate` draws the rest of the blended frame. */
10404
+ if (!this.reconstructing) this.runOit(encoder);
9731
10405
  /*
9732
10406
  * **The medium, after everything that draws and before anything that composites.**
9733
10407
  *
@@ -9748,6 +10422,8 @@ export class WebGPURenderer implements RendererApi {
9748
10422
  */
9749
10423
  this.runMotion(encoder);
9750
10424
  this.runReconstruction(encoder);
10425
+ this.runLate(encoder);
10426
+ if (this.reconstructing) this.runOit(encoder);
9751
10427
  const aoStrength = this.quality.ambientOcclusion;
9752
10428
  if (aoStrength > 0 && this.frameProjection !== null) this.runOcclusion(encoder);
9753
10429
  const bloomStrength = this.quality.bloom * this.bloomScale;
@@ -9976,19 +10652,23 @@ export class WebGPURenderer implements RendererApi {
9976
10652
  label: 'recon.shown',
9977
10653
  size: [outputWidth, outputHeight],
9978
10654
  format: RECON_HISTORY_FORMAT,
9979
- usage: HISTORY_USAGE,
10655
+ /* An attachment too, because the blended draws land on it after the upscale, and a copy
10656
+ source for the panes that refract it. See `runLate`. */
10657
+ usage: HISTORY_USAGE | 0x10 | 0x1, // RENDER_ATTACHMENT | COPY_SRC
9980
10658
  });
9981
10659
  this.reconShownView = this.reconShown.createView();
9982
10660
  this.reconMotion = device.createTexture({
9983
10661
  label: 'recon.motion',
9984
10662
  size: [renderWidth, renderHeight],
9985
10663
  format: RECON_HISTORY_FORMAT,
9986
- /* An attachment as well, because the pass that will write it is a draw. Nothing writes it
9987
- today and nothing needs to clear it either: WebGPU zero-initialises a new texture, and
9988
- a zero in the fourth channel is what tells the resolve to derive the camera's motion. */
10664
+ /* An attachment as well, because the pass that writes it is a draw. It is cleared every
10665
+ frame by that pass, and a zero in the fourth channel is what tells the resolve to derive
10666
+ the camera's motion. */
9989
10667
  usage: 0x4 | 0x10,
9990
10668
  });
9991
10669
  this.reconMotionView = this.reconMotion.createView();
10670
+ this.motionPass ??= new MotionPass(device);
10671
+ this.latePass ??= new LatePass(device);
9992
10672
  this.reconPreviousDepth = device.createTexture({
9993
10673
  label: 'recon.previousDepth',
9994
10674
  size: [renderWidth, renderHeight],
@@ -10107,222 +10787,184 @@ export class WebGPURenderer implements RendererApi {
10107
10787
  }
10108
10788
 
10109
10789
  /**
10110
- * Remember that this mesh moved, so the motion pass can draw where it was.
10790
+ * Whether a blended draw recorded now lands after the upscale. See `lateRouting.ts`.
10111
10791
  *
10112
- * Silent unless a reconstruction is running: a scene that always states its previous transforms
10113
- * — which is the right thing for a scene to do, the alternative being to know what the renderer
10114
- * is configured as — must cost nothing at all when nobody is going to read them.
10792
+ * Not during the order-independent replay, which owns its pass and issues into it directly.
10115
10793
  */
10116
- private recordMotion(
10117
- mesh: GpuMesh,
10118
- model: ArrayLike<number>,
10119
- previousModel: ArrayLike<number>,
10120
- ): void {
10121
- if (!this.reconstructing) return;
10122
- const at = this.motionCount;
10123
- const wanted = (at + 1) * MOTION_DRAW_FLOATS;
10124
- if (this.motionMatrices.length < wanted) {
10125
- /* Doubling, so a scene settles on one allocation rather than one a draw. */
10126
- const grown = new Float32Array(Math.max(wanted, this.motionMatrices.length * 2, 64));
10127
- grown.set(this.motionMatrices);
10128
- this.motionMatrices = grown;
10129
- }
10130
- this.motionMatrices.set(model as ArrayLike<number> & Iterable<number>, at * MOTION_DRAW_FLOATS);
10131
- this.motionMatrices.set(
10132
- previousModel as ArrayLike<number> & Iterable<number>,
10133
- at * MOTION_DRAW_FLOATS + 16,
10794
+ private drawsLate(): boolean {
10795
+ return (
10796
+ this.latePass !== null &&
10797
+ this.oitEncoder === null &&
10798
+ routesLate(
10799
+ this.reconstructing,
10800
+ this.reflectionPassActive,
10801
+ this.probePassActive,
10802
+ this.framePresented,
10803
+ )
10134
10804
  );
10135
- this.motionMeshes[at] = mesh;
10136
- this.motionCount = at + 1;
10137
10805
  }
10138
10806
 
10139
10807
  /**
10140
- * The pipeline the motion pass draws with, built once.
10141
- *
10142
- * **The position buffer alone**, because that is all the shader reads and a mesh keeps one buffer
10143
- * an attribute — so this binds buffer zero and nothing else, whatever else the mesh carries.
10808
+ * The command a blended draw records into: the late pool while reconstructing, the frame graph
10809
+ * otherwise. **Late draws record whatever `quality.frameGraph` says**, since the late replay is
10810
+ * the only moment they can land; everything else keeps the path it has.
10144
10811
  */
10145
- private buildMotionPipeline(): void {
10146
- if (this.motionLayout !== null) return;
10147
- const { device } = this.surface;
10148
- this.motionLayout = device.createBindGroupLayout({
10149
- label: 'recon.motion.layout',
10150
- entries: [
10151
- { binding: 0, visibility: 0x1, buffer: { type: 'uniform' as const } },
10152
- {
10153
- binding: 1,
10154
- visibility: 0x1,
10155
- buffer: { type: 'uniform' as const, hasDynamicOffset: true },
10156
- },
10157
- ],
10158
- });
10159
- this.motionModule = device.createShaderModule({
10160
- label: 'recon.motion',
10161
- code: RECON_MOTION_WGSL,
10162
- });
10163
- this.motionPipelineLayout = device.createPipelineLayout({
10164
- bindGroupLayouts: [this.motionLayout],
10165
- });
10812
+ private recordBlended(reads: number): DrawCommand | null {
10813
+ const late = this.latePass;
10814
+ if (late !== null && this.drawsLate()) return late.take();
10815
+ return this.recordDraw(reads, this.currentTarget());
10166
10816
  }
10167
10817
 
10168
- /** The pipeline for one vertex stride, built on first sight of it. */
10169
- private motionPipelineFor(stride: number): GPURenderPipeline | null {
10170
- const held = this.motionPipelines.get(stride);
10171
- if (held !== undefined) return held;
10172
- const module = this.motionModule;
10173
- const layout = this.motionPipelineLayout;
10174
- if (module === null || layout === null) return null;
10175
- const { device } = this.surface;
10176
- const pipeline = device.createRenderPipeline({
10177
- label: `recon.motion.${String(stride)}`,
10178
- layout,
10179
- vertex: {
10180
- module,
10181
- entryPoint: 'motionVert',
10182
- /* Positions are the layout's first attribute and are never optional, so they are at offset
10183
- zero of every stride this can be handed. */
10184
- buffers: [
10185
- {
10186
- arrayStride: stride,
10187
- attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }],
10188
- },
10189
- ],
10190
- },
10191
- fragment: { module, entryPoint: 'motionFrag', targets: [{ format: MOTION_TARGET_FORMAT }] },
10192
- /*
10193
- * **The scene's own winding**, because this draws the scene's geometry through the scene's
10194
- * clip transform — the product the renderer hands over already carries the negation that
10195
- * `naga` writes into every generated vertex stage, so the faces come out the way they did
10196
- * and a `cw` front face here would draw the inside of every mesh. §3 row 57.
10197
- */
10198
- primitive: { topology: 'triangle-list', cullMode: 'back' },
10199
- depthStencil: {
10200
- format: DEPTH_FORMAT,
10201
- /*
10202
- * **Tested and not written.** The frame's depth is finished; this pass only wants the
10203
- * pixels the scene actually kept, and `DEPTH_COMPARE_EQUAL` is the comparison that admits
10204
- * exactly them — the same transform on the same positions gives the same depth, which is
10205
- * why the rasterisation matrix is handed over whole rather than rebuilt here.
10206
- *
10207
- * **And the equality is not fragile here**, which was worth measuring rather than
10208
- * assuming: this driver contracts multiply-adds (§3 row 107) and two shader modules can
10209
- * contract a matrix product differently, so an equality across them could have admitted
10210
- * only some of a surface. Run with `greater-equal` instead, the ghost measured the same
10211
- * to the pixel — 5,942 either way — so nothing is being dropped.
10212
- */
10213
- depthWriteEnabled: false,
10214
- depthCompare: DEPTH_COMPARE_EQUAL,
10215
- },
10216
- });
10217
- this.motionPipelines.set(stride, pipeline);
10218
- return pipeline;
10818
+ /** The pipelines a blended draw is built for: the reconstructed picture's while late. */
10819
+ private blendedPipelines(): PipelineCache {
10820
+ return this.drawsLate() ? this.latePipelines : this.targetPipelines();
10219
10821
  }
10220
10822
 
10221
10823
  /**
10222
- * Where every mover was last frame, into the motion target.
10223
- *
10224
- * **After everything that draws and before the resolve reads it.** The frame's depth has to be
10225
- * finished for the equality test to mean anything, and the resolve is what consumes the result.
10824
+ * The blended draws of a reconstructing frame, over the reconstructed picture at the output size
10825
+ * and unjittered, against the render's depth taken up to that size. Before occlusion, bloom and
10826
+ * the grade, so they are graded exactly as they were when they were drawn into the scene.
10226
10827
  */
10227
- private runMotion(encoder: GPUCommandEncoder): void {
10228
- const target = this.reconMotionView;
10828
+ private runLate(encoder: GPUCommandEncoder): void {
10829
+ const late = this.latePass;
10830
+ const shown = this.reconShownView;
10229
10831
  const depth = this.depthView;
10230
- if (!this.reconstructing || target === null || depth === null) return;
10231
- const { device } = this.surface;
10232
- this.buildMotionPipeline();
10233
- const layout = this.motionLayout;
10234
- if (layout === null) return;
10235
-
10236
- /*
10237
- * **Cleared every frame, even when nothing moved.** A texel left from last frame is a flag
10238
- * saying "a draw wrote this", and the resolve would take a motion belonging to a surface that
10239
- * is no longer there. The clear is the pass's own load operation, so an empty queue still runs
10240
- * it — which is why this is not returned from before the pass is opened.
10241
- */
10832
+ if (!this.reconstructing || late === null || shown === null || depth === null) return;
10833
+ const oitSet = this.oitActive && this.translucentQueue.length > 0;
10834
+ if (late.pending === 0 && !oitSet) return;
10835
+ const width = Math.max(1, this.surface.canvas.width);
10836
+ const height = Math.max(1, this.surface.canvas.height);
10837
+ /* The order-independent set tests against this depth too, so it is made for either. */
10838
+ late.upscale(encoder, depth, this.renderWidth, this.renderHeight, width, height);
10839
+ if (this.lateRefractWanted && this.refractSnapshot !== null && this.reconShown !== null) {
10840
+ encoder.copyTextureToTexture(
10841
+ { texture: this.reconShown },
10842
+ { texture: this.refractSnapshot },
10843
+ [this.refractSnapshot.width, this.refractSnapshot.height],
10844
+ );
10845
+ this.refractMips?.record(encoder);
10846
+ this.refractSnapshotTaken = true;
10847
+ }
10848
+ this.lateRefractWanted = false;
10849
+ if (late.pending === 0) return;
10242
10850
  const pass = encoder.beginRenderPass({
10243
- label: 'recon.motion',
10851
+ label: 'recon.late',
10244
10852
  timestampWrites: this.gpuTimer.writesFor(),
10245
- colorAttachments: [
10246
- { view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
10247
- ],
10853
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
10248
10854
  depthStencilAttachment: {
10249
- view: depth,
10855
+ view: late.depthView(width, height),
10250
10856
  depthLoadOp: 'load',
10857
+ /* Kept: the order-independent set replays against it after this pass. */
10251
10858
  depthStoreOp: 'store',
10252
- depthReadOnly: false,
10253
10859
  },
10254
10860
  });
10861
+ late.replay(pass, this.issueBound);
10862
+ pass.end();
10863
+ }
10255
10864
 
10256
- const count = this.motionCount;
10257
- if (count > 0) {
10258
- const bytes = count * MOTION_DRAW_STRIDE;
10259
- if (this.motionDrawCapacity < count) {
10260
- this.motionDrawBuffer?.destroy();
10261
- this.motionDrawCapacity = Math.max(count, this.motionDrawCapacity * 2, 16);
10262
- this.motionDrawBuffer = device.createBuffer({
10263
- label: 'recon.motion.draws',
10264
- size: this.motionDrawCapacity * MOTION_DRAW_STRIDE,
10265
- usage: 0x40 | 0x8, // UNIFORM | COPY_DST
10266
- });
10267
- this.motionDrawStaging = new Uint8Array(this.motionDrawCapacity * MOTION_DRAW_STRIDE);
10268
- this.motionGroup = null;
10269
- }
10270
- this.motionFrameBuffer ??= device.createBuffer({
10271
- label: 'recon.motion.frame',
10272
- size: MOTION_FRAME_FLOATS * 4,
10273
- usage: 0x40 | 0x8,
10274
- });
10275
- this.motionGroup ??= device.createBindGroup({
10276
- label: 'recon.motion.group',
10277
- layout,
10278
- entries: [
10279
- { binding: 0, resource: { buffer: this.motionFrameBuffer } },
10280
- {
10281
- binding: 1,
10282
- resource: { buffer: this.motionDrawBuffer as GPUBuffer, size: MOTION_DRAW_STRIDE },
10283
- },
10284
- ],
10285
- });
10286
-
10287
- /* The scene's clip transform, with the generated stages' y negation folded in. */
10288
- mat4.multiply(this.motionRaster, MOTION_CLIP_FLIP, this.viewProj ?? this.correctedViewProj);
10289
- this.motionFrameStaging.set(this.motionRaster as Float32Array, 0);
10290
- this.motionFrameStaging.set(this.frameRawViewProj as Float32Array, 16);
10291
- this.motionFrameStaging.set(this.reconPreviousRawViewProj as Float32Array, 32);
10292
- device.queue.writeBuffer(this.motionFrameBuffer, 0, this.motionFrameStaging);
10293
-
10294
- const draws = new Float32Array(this.motionDrawStaging.buffer);
10295
- for (let i = 0; i < count; i += 1) {
10296
- draws.set(
10297
- this.motionMatrices.subarray(i * MOTION_DRAW_FLOATS, (i + 1) * MOTION_DRAW_FLOATS),
10298
- (i * MOTION_DRAW_STRIDE) / 4,
10299
- );
10300
- }
10301
- device.queue.writeBuffer(
10302
- this.motionDrawBuffer as GPUBuffer,
10303
- 0,
10304
- this.motionDrawStaging,
10305
- 0,
10306
- bytes,
10307
- );
10865
+ /** Whether this draw lands in the reconstructed picture. See `lateRouting.ts`. */
10866
+ private tracksMotionNow(): boolean {
10867
+ return tracksMotion(
10868
+ this.reconstructing,
10869
+ this.reflectionPassActive,
10870
+ this.probePassActive,
10871
+ this.framePresented,
10872
+ );
10873
+ }
10308
10874
 
10309
- let bound: GPURenderPipeline | null = null;
10310
- for (let i = 0; i < count; i += 1) {
10311
- const mesh = this.motionMeshes[i];
10312
- if (mesh === undefined) continue;
10313
- const pipeline = this.motionPipelineFor(mesh.vertexStride);
10314
- if (pipeline === null) continue;
10315
- if (pipeline !== bound) {
10316
- pass.setPipeline(pipeline);
10317
- bound = pipeline;
10318
- }
10319
- pass.setBindGroup(0, this.motionGroup, [i * MOTION_DRAW_STRIDE]);
10320
- pass.setVertexBuffer(0, mesh.vertexBuffers[0] as GPUBuffer);
10321
- pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
10322
- pass.drawIndexed(mesh.indexCount);
10323
- }
10875
+ /**
10876
+ * What this draw's motion is, recorded for the motion pass.
10877
+ *
10878
+ * A mesh rewritten this frame carries its vertex motion whatever else the draw says — under last
10879
+ * frame's model where a mover or a matrix knows it, under this frame's where nothing does. A
10880
+ * mover otherwise carries a rigid draw's last model or a skinned draw's last pose; a matrix
10881
+ * carries a rigid draw's. A draw that says nothing and moved no vertex takes the camera's motion.
10882
+ */
10883
+ private recordDrawMotion(
10884
+ mesh: GpuMesh,
10885
+ model: ArrayLike<number>,
10886
+ previous: ArrayLike<number> | Mover | null,
10887
+ ): void {
10888
+ const dynamic = mesh.motion;
10889
+ const rows = dynamic?.previous ?? null;
10890
+ const rewritten = rows !== null && dynamic !== null && dynamic.changed === this.moverFrame;
10891
+ if (previous === null) {
10892
+ if (rewritten) this.motionPass?.recordDynamic(mesh, model, model, rows);
10893
+ return;
10324
10894
  }
10325
- pass.end();
10895
+ if (!isMover(previous)) {
10896
+ if (rewritten) this.motionPass?.recordDynamic(mesh, model, previous, rows);
10897
+ else this.motionPass?.recordRigid(mesh, model, previous);
10898
+ return;
10899
+ }
10900
+ /* A rig is only followed where the draw really skins: a palette set, a mesh with joints. */
10901
+ const skinned =
10902
+ !rewritten && this.skinPaletteSlot >= 0 && mesh.isSkinned && mesh.skinOffsets !== null;
10903
+ const verdict = advanceMover(
10904
+ previous,
10905
+ this.moverRenderer,
10906
+ this.moverFrame,
10907
+ model,
10908
+ skinned ? this.skinPaletteData : null,
10909
+ );
10910
+ if (verdict === MOVER_TWICE) {
10911
+ this.warnMoverTwice();
10912
+ return;
10913
+ }
10914
+ if (rewritten) {
10915
+ const was = verdict === MOVER_MOTION ? previous.previousModel : model;
10916
+ this.motionPass?.recordDynamic(mesh, model, was, rows);
10917
+ return;
10918
+ }
10919
+ if (verdict !== MOVER_MOTION) return;
10920
+ if (skinned) this.recordSkinnedMotion(mesh, model, previous);
10921
+ else this.motionPass?.recordRigid(mesh, model, previous.previousModel);
10922
+ }
10923
+
10924
+ /**
10925
+ * A skinned mover's motion: this frame's palette is the draw's own slot, and last frame's takes a
10926
+ * slot of its own, like any palette a draw binds — so it is uploaded once and nothing overwrites
10927
+ * it before the motion pass reads it. See `SkinPaletteRing`.
10928
+ */
10929
+ private recordSkinnedMotion(mesh: GpuMesh, model: ArrayLike<number>, mover: Mover): void {
10930
+ const now = this.skinPalettes.view(this.skinPaletteSlot);
10931
+ const slot = this.skinPalettes.take(this.surface.device, mover.previousPalette);
10932
+ const was = slot === null ? null : this.skinPalettes.view(slot);
10933
+ if (now === null || was === null) return;
10934
+ this.motionPass?.recordSkinned(mesh, model, mover.previousModel, now, was);
10935
+ }
10936
+
10937
+ private warnMoverTwice(): void {
10938
+ if (this.warnedMoverTwice) return;
10939
+ this.warnedMoverTwice = true;
10940
+ console.warn(
10941
+ 'WebGPU: a Mover was drawn twice in one frame. A mover is one object; the second draw ' +
10942
+ 'records no motion. Give each moving object its own `createMover()`.',
10943
+ );
10944
+ }
10945
+
10946
+ /**
10947
+ * Where every mover was last frame, into the motion target. See `MotionPass`.
10948
+ *
10949
+ * **After everything that draws and before the resolve reads it.** The frame's depth has to be
10950
+ * finished for the pass's equality test to mean anything, and the resolve is what consumes it.
10951
+ */
10952
+ private runMotion(encoder: GPUCommandEncoder): void {
10953
+ const target = this.reconMotionView;
10954
+ const depth = this.depthView;
10955
+ const motion = this.motionPass;
10956
+ if (!this.reconstructing || target === null || depth === null || motion === null) return;
10957
+ /* The scene's clip transform, with the generated stages' y negation folded in. */
10958
+ mat4.multiply(this.motionRaster, MOTION_CLIP_FLIP, this.viewProj ?? this.correctedViewProj);
10959
+ motion.run(
10960
+ encoder,
10961
+ target,
10962
+ depth,
10963
+ this.motionRaster as Float32Array,
10964
+ this.frameRawViewProj as Float32Array,
10965
+ this.reconPreviousRawViewProj as Float32Array,
10966
+ this.gpuTimer.writesFor(),
10967
+ );
10326
10968
  }
10327
10969
 
10328
10970
  /**
@@ -10468,8 +11110,10 @@ export class WebGPURenderer implements RendererApi {
10468
11110
  if (!this.temporalJittering) return;
10469
11111
  const write = this.taaWrite;
10470
11112
  const target = this.taaViews[write];
11113
+ const record = this.taaRecordViews[write];
11114
+ const motion = this.taaMotionViews[write];
10471
11115
  const group = this.taaBindGroups[1 - write];
10472
- if (target === null || group === null) return;
11116
+ if (target === null || record === null || motion === null || group === null) return;
10473
11117
  const { device } = this.surface;
10474
11118
 
10475
11119
  const f = this.taaFloats;
@@ -10478,6 +11122,22 @@ export class WebGPURenderer implements RendererApi {
10478
11122
  f[at('uTexel')] = 1 / this.renderWidth;
10479
11123
  f[at('uTexel') + 1] = 1 / this.renderHeight;
10480
11124
  f[at('uHistoryBlend')] = this.temporalHistoryUsable ? TEMPORAL_HISTORY_BLEND : 0;
11125
+ f[at('uPeriodStart')] = this.temporalHistory.periodStart ? 1 : 0;
11126
+ /* The anti-flicker's same-surface test reads depths in metres, as the occlusion does. */
11127
+ const depthRow = at('uDepthToViewZ');
11128
+ if (this.frameProjection !== null) {
11129
+ mat4.invert(this.taaInvProjection, this.frameProjection);
11130
+ f[depthRow] = this.taaInvProjection[10] ?? 0;
11131
+ f[depthRow + 1] = this.taaInvProjection[14] ?? 0;
11132
+ f[depthRow + 2] = this.taaInvProjection[11] ?? 0;
11133
+ f[depthRow + 3] = this.taaInvProjection[15] ?? 1;
11134
+ } else {
11135
+ f[depthRow] = 0;
11136
+ f[depthRow + 1] = 0;
11137
+ f[depthRow + 2] = 0;
11138
+ f[depthRow + 3] = 1;
11139
+ }
11140
+ f[at('uAntiFlicker')] = 1;
10481
11141
  device.queue.writeBuffer(this.taaUniforms, 0, this.taaStaging);
10482
11142
 
10483
11143
  const pass = encoder.beginRenderPass({
@@ -10485,16 +11145,18 @@ export class WebGPURenderer implements RendererApi {
10485
11145
  timestampWrites: this.gpuTimer.writesFor(),
10486
11146
  colorAttachments: [
10487
11147
  { view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 1] },
11148
+ { view: record, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
11149
+ { view: motion, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
10488
11150
  ],
10489
11151
  });
10490
11152
  pass.setPipeline(
10491
- postPipeline(
11153
+ postPipelineTargets(
10492
11154
  this.pipelines,
10493
11155
  device,
10494
11156
  this.taaLayout,
10495
11157
  'post.taa',
10496
11158
  TEMPORAL_RESOLVE_FRAG_WGSL,
10497
- sceneColorFormat(this.quality.hdrScene),
11159
+ this.taaTargets,
10498
11160
  ),
10499
11161
  );
10500
11162
  pass.setBindGroup(0, group, [0]);
@@ -10748,9 +11410,15 @@ export class WebGPURenderer implements RendererApi {
10748
11410
  if (!this.oitActive || this.translucentQueue.length === 0) return;
10749
11411
  const accum = this.oitAccumView;
10750
11412
  const reveal = this.oitRevealView;
10751
- const scene = this.sceneColorView;
11413
+ /* After the upscale while reconstructing: over the reconstructed picture, against its depth. */
11414
+ const scene = this.reconstructing ? this.reconShownView : this.sceneColorView;
10752
11415
  const group = this.oitResolveGroup;
10753
- const depth = this.depthView;
11416
+ const depth = this.reconstructing
11417
+ ? (this.latePass?.depthView(
11418
+ Math.max(1, this.surface.canvas.width),
11419
+ Math.max(1, this.surface.canvas.height),
11420
+ ) ?? null)
11421
+ : this.depthView;
10754
11422
  if (accum === null || reveal === null || scene === null || group === null || depth === null) {
10755
11423
  return;
10756
11424
  }
@@ -10764,12 +11432,13 @@ export class WebGPURenderer implements RendererApi {
10764
11432
  */
10765
11433
  if (this.translucentQueue.refracts && !this.refractSnapshotTaken) {
10766
11434
  const snapshot = this.refractSnapshot;
10767
- const source = this.sceneColor;
11435
+ const source = this.reconstructing ? this.reconShown : this.sceneColor;
10768
11436
  if (snapshot !== null && source !== null) {
10769
11437
  encoder.copyTextureToTexture({ texture: source }, { texture: snapshot }, [
10770
11438
  snapshot.width,
10771
11439
  snapshot.height,
10772
11440
  ]);
11441
+ this.refractMips?.record(encoder);
10773
11442
  this.refractSnapshotTaken = true;
10774
11443
  }
10775
11444
  }
@@ -10810,6 +11479,7 @@ export class WebGPURenderer implements RendererApi {
10810
11479
  options.refraction = draw.refraction;
10811
11480
  options.refractTint = draw.refractTint as unknown as Vec3 | undefined;
10812
11481
  options.thicknessM = draw.thicknessM;
11482
+ options.glass = draw.glass ?? undefined;
10813
11483
  this.drawTranslucentMesh(draw.mesh as GpuMesh, draw.model, draw.opacity, options);
10814
11484
  });
10815
11485
  this.oitReplaying = false;
@@ -10840,9 +11510,9 @@ export class WebGPURenderer implements RendererApi {
10840
11510
  this.pipelines,
10841
11511
  device,
10842
11512
  this.oitResolveLayout,
10843
- 'post.oitResolve',
11513
+ this.reconstructing ? 'post.oitResolve.recon' : 'post.oitResolve',
10844
11514
  OIT_RESOLVE_FRAG_WGSL,
10845
- sceneColorFormat(this.quality.hdrScene),
11515
+ this.reconstructing ? RECON_HISTORY_FORMAT : sceneColorFormat(this.quality.hdrScene),
10846
11516
  OIT_COMPOSITE_BLEND,
10847
11517
  ),
10848
11518
  );
@@ -11232,18 +11902,14 @@ export class WebGPURenderer implements RendererApi {
11232
11902
  this.lightVolumeVertices.buffer,
11233
11903
  this.lightVolumeFragments.buffer,
11234
11904
  (name) =>
11235
- name === 'uStaticShadowMap'
11236
- ? { view: this.shadowView, sampler: this.volumeShadowSampler }
11237
- : name === 'uPeeledShadowMap' && this.peelView !== null
11238
- ? { view: this.peelView, sampler: this.volumeShadowSampler }
11239
- : name === 'uDynamicShadowMap' && this.dynamicView !== null
11240
- ? { view: this.dynamicView, sampler: this.volumeShadowSampler }
11241
- : /* The frame's own depth, snapshotted before the first volume of the frame draws.
11905
+ name === 'uSunShadows'
11906
+ ? { view: this.sunArrayView, sampler: this.volumeShadowSampler }
11907
+ : /* The frame's own depth, snapshotted before the first volume of the frame draws.
11242
11908
  Null under a profile with no composite, and then `uSceneDepthEnabled` is 0 and
11243
11909
  the stand-in is never sampled. */
11244
- name === 'uSceneDepth' && this.resolvedDepthView !== null
11245
- ? { view: this.resolvedDepthView, sampler: this.postDepthSampler }
11246
- : this.volumeBlank,
11910
+ name === 'uSceneDepth' && this.resolvedDepthView !== null
11911
+ ? { view: this.resolvedDepthView, sampler: this.postDepthSampler }
11912
+ : this.volumeBlank,
11247
11913
  );
11248
11914
  }
11249
11915
 
@@ -11269,6 +11935,12 @@ export class WebGPURenderer implements RendererApi {
11269
11935
  private takeRefractSnapshot(): boolean {
11270
11936
  if (this.reflectionPassActive || this.probePassActive) return false;
11271
11937
  if (this.refractSnapshotTaken) return true;
11938
+ /* A late draw refracts the reconstructed picture, which does not exist until the resolve has
11939
+ run; `runLate` takes the copy then, once, before any late draw is replayed. */
11940
+ if (this.drawsLate()) {
11941
+ this.lateRefractWanted = true;
11942
+ return true;
11943
+ }
11272
11944
  /* The replay of an order-independent set runs after the scene's pass has ended, and `runOit`
11273
11945
  took the copy for it before it began; there is no pass here to end and reopen. */
11274
11946
  if (this.oitReplaying) return false;
@@ -11307,6 +11979,8 @@ export class WebGPURenderer implements RendererApi {
11307
11979
  snapshot.width,
11308
11980
  snapshot.height,
11309
11981
  ]);
11982
+ /* The chain, between the passes, where nothing is attached: what frosted glass reads. */
11983
+ this.refractMips?.record(encoder);
11310
11984
  const multisampled = this.compositeMsaa() !== null;
11311
11985
  this.pass = encoder.beginRenderPass({
11312
11986
  label: 'refract.snapshotTaken',
@@ -11668,8 +12342,13 @@ export class WebGPURenderer implements RendererApi {
11668
12342
  }
11669
12343
  if (verb !== VERB_DRAW) return;
11670
12344
  const command = this.commands.commands[nodeState(this.arena, at)];
11671
- if (command === undefined || command.pipeline === null || command.bindGroup === null) return;
12345
+ if (command === undefined) return;
12346
+ this.issueCommand(pass, command);
12347
+ }
11672
12348
 
12349
+ /** One recorded draw, issued into an open pass. Shared by the graph's replay and `runLate`. */
12350
+ private issueCommand(pass: GPURenderPassEncoder, command: DrawCommand): void {
12351
+ if (command.pipeline === null || command.bindGroup === null) return;
11673
12352
  pass.setPipeline(command.pipeline);
11674
12353
  if (command.offsetCount === 2) {
11675
12354
  pass.setBindGroup(0, command.bindGroup, [command.offsetA, command.offsetB]);
@@ -11913,6 +12592,7 @@ export class WebGPURenderer implements RendererApi {
11913
12592
  }
11914
12593
 
11915
12594
  endFrame(): void {
12595
+ this.inFrame = false;
11916
12596
  if (this.surface.lost || this.encoder === null) return;
11917
12597
  /*
11918
12598
  * Anything still recorded goes now.
@@ -13179,6 +13859,10 @@ export class WebGPURenderer implements RendererApi {
13179
13859
  *
13180
13860
  * What it gives up is one frame of history: the frame after a cut has no blur and no temporal
13181
13861
  * blend, as the first frame of a session has none. Nothing else is reset.
13862
+ *
13863
+ * **A reconstructing renderer depends on it**: its history is the last shot's, and without this
13864
+ * call a cut blends that shot into the first frames of the next. Nothing detects a cut on the
13865
+ * caller's behalf, deliberately — a fast pan and a cut look alike from here.
13182
13866
  */
13183
13867
  cameraCut(): void {
13184
13868
  this.hasPreviousView = false;
@@ -13388,9 +14072,13 @@ export class WebGPURenderer implements RendererApi {
13388
14072
  this.causticsVerts.writeBlock(vertexSlot, this.causticsVertBlock);
13389
14073
  this.causticsFrags.writeBlock(fragmentSlot, this.causticsFragInts);
13390
14074
 
13391
- const causticsPipe = causticsPipeline(this.pipelines, this.surface.device, this.causticsLayout);
14075
+ const causticsPipe = causticsPipeline(
14076
+ this.blendedPipelines(),
14077
+ this.surface.device,
14078
+ this.causticsLayout,
14079
+ );
13392
14080
  /* The pattern is generated in the shader from the two uniform blocks; nothing is sampled. */
13393
- const causticsCommand = this.recordDraw(0, this.currentTarget());
14081
+ const causticsCommand = this.recordBlended(0);
13394
14082
  if (causticsCommand !== null) {
13395
14083
  causticsCommand.pipeline = causticsPipe;
13396
14084
  causticsCommand.bindGroup = this.causticsBindGroup;
@@ -14385,6 +15073,33 @@ export class WebGPURenderer implements RendererApi {
14385
15073
  * The occlusion and bloom targets are half size. Both are blurs read back at full size, so a
14386
15074
  * full-resolution intermediate buys nothing a bilinear fetch does not already give.
14387
15075
  */
15076
+ /**
15077
+ * The medium's march group, holding the shadow views it reads — built with the composite, and
15078
+ * again when the sun's texture grows its glass layers and every view of the old one is stale.
15079
+ *
15080
+ * **The stand-in has to read as unoccluded and not as absent**, which is `emptyTexture.ts`'s
15081
+ * point and `createLightVolumeBindGroup`'s: `occlusion()` calls any stored depth below the
15082
+ * receiver's a blocker — so a black stand-in would put the whole medium in shadow, which is a
15083
+ * picture rather than an error. `volumeBlank` is the white one-texel texture built for this.
15084
+ */
15085
+ private buildMediumBindGroup(): GPUBindGroup | null {
15086
+ if (this.resolvedDepthView === null) return null;
15087
+ return this.surface.device.createBindGroup({
15088
+ label: 'post.mediumBindGroup',
15089
+ layout: this.mediumLayout,
15090
+ entries: [
15091
+ {
15092
+ binding: MEDIUM_UNIFORMS,
15093
+ resource: { buffer: this.mediumUniforms, size: MEDIUM_FRAG_SIZE },
15094
+ },
15095
+ { binding: MEDIUM_TEXTURES.uDepth.texture, resource: this.resolvedDepthView },
15096
+ { binding: MEDIUM_TEXTURES.uDepth.sampler, resource: this.postDepthSampler },
15097
+ { binding: MEDIUM_TEXTURES.uSunShadows.texture, resource: this.sunArrayView },
15098
+ { binding: MEDIUM_TEXTURES.uSunShadows.sampler, resource: this.volumeShadowSampler },
15099
+ ],
15100
+ });
15101
+ }
15102
+
14388
15103
  private ensureComposite(width: number, height: number): void {
14389
15104
  if (!this.hasComposite) return;
14390
15105
  if (this.sceneColor !== null && this.frameWidth === width && this.frameHeight === height)
@@ -14438,13 +15153,39 @@ export class WebGPURenderer implements RendererApi {
14438
15153
  * the allocation the house rules are about. A frame that never refracts pays the memory and
14439
15154
  * takes no copy.
14440
15155
  */
15156
+ /*
15157
+ * **At the output size and the reconstructed picture's format while reconstructing**, because
15158
+ * every refracting draw is then blended, drawn after the upscale, and reads the reconstructed
15159
+ * picture rather than the render. See `runLate`.
15160
+ */
15161
+ const reconstructed = this.reconstructionWanted;
14441
15162
  this.refractSnapshot = device.createTexture({
14442
15163
  label: 'refract.snapshot',
14443
- size: [width, height],
14444
- format,
14445
- usage: 0x4 | 0x2,
15164
+ size: reconstructed
15165
+ ? [Math.max(1, this.surface.canvas.width), Math.max(1, this.surface.canvas.height)]
15166
+ : [width, height],
15167
+ format: reconstructed ? RECON_HISTORY_FORMAT : format,
15168
+ /*
15169
+ * **Every level, so glass can read a blurrier one**, and a render attachment so `SnapshotMips`
15170
+ * can fill them. Plain refraction reads level 0, as it always has. See `glass.ts`.
15171
+ */
15172
+ mipLevelCount:
15173
+ Math.floor(
15174
+ Math.log2(
15175
+ reconstructed
15176
+ ? Math.max(1, this.surface.canvas.width, this.surface.canvas.height)
15177
+ : Math.max(1, width, height),
15178
+ ),
15179
+ ) + 1,
15180
+ usage: 0x4 | 0x2 | 0x10, // TEXTURE_BINDING | COPY_DST | RENDER_ATTACHMENT
14446
15181
  });
14447
15182
  this.refractSnapshotView = this.refractSnapshot.createView();
15183
+ this.refractMips = new SnapshotMips(
15184
+ device,
15185
+ this.pipelines,
15186
+ this.refractSnapshot,
15187
+ reconstructed ? RECON_HISTORY_FORMAT : format,
15188
+ );
14448
15189
  /*
14449
15190
  * **Every cached flat bind group is stale the moment this view changes**, and the failure is
14450
15191
  * silent: `flatTextures` resolves `uRefractScene` to the one-texel white stand-in when there is
@@ -14551,7 +15292,28 @@ export class WebGPURenderer implements RendererApi {
14551
15292
  });
14552
15293
  this.taaTextures[i] = texture;
14553
15294
  this.taaViews[i] = texture.createView();
15295
+ /* The records in half float, whatever the picture's format: one of them holds metres. */
15296
+ this.taaRecords[i]?.destroy();
15297
+ const record = device.createTexture({
15298
+ label: `post.taaRecord${i}`,
15299
+ size: [width, height],
15300
+ format: 'rgba16float',
15301
+ usage: USAGE,
15302
+ });
15303
+ this.taaRecords[i] = record;
15304
+ this.taaRecordViews[i] = record.createView();
15305
+ this.taaMotions[i]?.destroy();
15306
+ /* Two numbers a pixel: the depth its sample stood at, in metres, and how far that swings. */
15307
+ const motion = device.createTexture({
15308
+ label: `post.taaMotion${i}`,
15309
+ size: [width, height],
15310
+ format: 'rg16float',
15311
+ usage: USAGE,
15312
+ });
15313
+ this.taaMotions[i] = motion;
15314
+ this.taaMotionViews[i] = motion.createView();
14554
15315
  }
15316
+ this.taaTargets = [{ format }, { format: 'rgba16float' }, { format: 'rg16float' }];
14555
15317
  this.taaWrite = 0;
14556
15318
  /* A resized target is a new texture, so whatever the history held is gone. */
14557
15319
  this.temporalHistory.invalidate();
@@ -14565,10 +15327,14 @@ export class WebGPURenderer implements RendererApi {
14565
15327
  * that range — a step of 1/255 in how much scene shows through, below what a viewer can see
14566
15328
  * against a blended surface.
14567
15329
  */
15330
+ /* At the output size while reconstructing, where the set is replayed after the upscale. */
15331
+ const oitSize: [number, number] = this.reconstructionWanted
15332
+ ? [Math.max(1, this.surface.canvas.width), Math.max(1, this.surface.canvas.height)]
15333
+ : [width, height];
14568
15334
  this.oitAccum?.destroy();
14569
15335
  this.oitAccum = device.createTexture({
14570
15336
  label: 'post.oitAccum',
14571
- size: [width, height],
15337
+ size: oitSize,
14572
15338
  format: 'rgba16float',
14573
15339
  usage: USAGE,
14574
15340
  });
@@ -14576,7 +15342,7 @@ export class WebGPURenderer implements RendererApi {
14576
15342
  this.oitReveal?.destroy();
14577
15343
  this.oitReveal = device.createTexture({
14578
15344
  label: 'post.oitReveal',
14579
- size: [width, height],
15345
+ size: oitSize,
14580
15346
  format: 'r8unorm',
14581
15347
  usage: USAGE,
14582
15348
  });
@@ -14681,42 +15447,7 @@ export class WebGPURenderer implements RendererApi {
14681
15447
  ...entry(AO_TEXTURES.uDepth, this.resolvedDepthView, true),
14682
15448
  ],
14683
15449
  });
14684
- /*
14685
- * The medium's two groups, holding the shadow views the march reads.
14686
- *
14687
- * **The stand-in has to read as unoccluded and not as absent**, which is `emptyTexture.ts`'s
14688
- * point and `createLightVolumeBindGroup`'s: the dynamic layer is sampled unconditionally, and
14689
- * `occlusion()` calls any stored depth below the receiver's a blocker — so a black stand-in
14690
- * would put the whole medium in shadow, which is a picture rather than an error. `volumeBlank`
14691
- * is the white one-texel texture already built for exactly this.
14692
- */
14693
- const shadowPair = (
14694
- b: { texture: number; sampler: number },
14695
- view: GPUTextureView | null,
14696
- ): GPUBindGroupEntry[] => [
14697
- { binding: b.texture, resource: view ?? this.volumeBlank.view },
14698
- {
14699
- binding: b.sampler,
14700
- resource: view === null ? this.volumeBlank.sampler : this.volumeShadowSampler,
14701
- },
14702
- ];
14703
- this.mediumBindGroup =
14704
- this.resolvedDepthView === null
14705
- ? null
14706
- : device.createBindGroup({
14707
- label: 'post.mediumBindGroup',
14708
- layout: this.mediumLayout,
14709
- entries: [
14710
- {
14711
- binding: MEDIUM_UNIFORMS,
14712
- resource: { buffer: this.mediumUniforms, size: MEDIUM_FRAG_SIZE },
14713
- },
14714
- ...entry(MEDIUM_TEXTURES.uDepth, this.resolvedDepthView, true),
14715
- ...shadowPair(MEDIUM_TEXTURES.uStaticShadowMap, this.shadowView),
14716
- ...shadowPair(MEDIUM_TEXTURES.uPeeledShadowMap, this.peelView),
14717
- ...shadowPair(MEDIUM_TEXTURES.uDynamicShadowMap, this.dynamicView),
14718
- ],
14719
- });
15450
+ this.mediumBindGroup = this.buildMediumBindGroup();
14720
15451
  this.mediumUpsampleGroup =
14721
15452
  this.resolvedDepthView === null || this.mediumTargetView === null
14722
15453
  ? null
@@ -14747,7 +15478,17 @@ export class WebGPURenderer implements RendererApi {
14747
15478
  const scene = this.sceneColorView;
14748
15479
  const history = this.taaViews[i];
14749
15480
  const resolved = this.taaViews[1 - i];
14750
- if (scene === null || history === null || resolved === null) continue;
15481
+ const record = this.taaRecordViews[i];
15482
+ const motion = this.taaMotionViews[i];
15483
+ if (
15484
+ scene === null ||
15485
+ history === null ||
15486
+ resolved === null ||
15487
+ record === null ||
15488
+ motion === null
15489
+ ) {
15490
+ continue;
15491
+ }
14751
15492
  this.taaBindGroups[i] = device.createBindGroup({
14752
15493
  label: `post.taaBindGroup${i}`,
14753
15494
  layout: this.taaLayout,
@@ -14755,6 +15496,8 @@ export class WebGPURenderer implements RendererApi {
14755
15496
  { binding: TAA_UNIFORMS, resource: { buffer: this.taaUniforms, size: TAA_FRAG_SIZE } },
14756
15497
  ...entry(TAA_TEXTURES.uScene, scene),
14757
15498
  ...entry(TAA_TEXTURES.uHistory, history),
15499
+ ...entry(TAA_TEXTURES.uFlicker, record),
15500
+ ...entry(TAA_TEXTURES.uFlickerMotion, motion),
14758
15501
  ...entry(TAA_TEXTURES.uDepth, this.resolvedDepthView, true),
14759
15502
  ],
14760
15503
  });
@@ -14936,6 +15679,10 @@ export class WebGPURenderer implements RendererApi {
14936
15679
  * the 2026-08-13 rule asks for — one decision, bound twice.
14937
15680
  */
14938
15681
  dispose(): void {
15682
+ this.motionPass?.dispose();
15683
+ this.motionPass = null;
15684
+ this.latePass?.dispose();
15685
+ this.latePass = null;
14939
15686
  for (const texture of this.retiredTextures) texture.destroy();
14940
15687
  this.retiredTextures.length = 0;
14941
15688
  const passDevice = this.passDevice();
@@ -14952,6 +15699,12 @@ export class WebGPURenderer implements RendererApi {
14952
15699
  this.fieldComposer = null;
14953
15700
  this.driftIndex?.destroy();
14954
15701
  this.driftAtlas?.destroy();
15702
+ this.sunTint?.texture.destroy();
15703
+ this.sunTint = null;
15704
+ this.glassTint?.dispose();
15705
+ this.glassTint = null;
15706
+ this.retiredSunTexture?.destroy();
15707
+ this.retiredSunTexture = null;
14955
15708
 
14956
15709
  this.pass = null;
14957
15710
  this.encoder = null;