@driftengine/core 4.5.0 → 4.6.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 (207) hide show
  1. package/CHANGELOG.json +51 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/boxCsg.d.ts +28 -0
  4. package/dist/geometry/boxCsg.js +139 -0
  5. package/dist/geometry/csg.d.ts +36 -0
  6. package/dist/geometry/csg.js +329 -0
  7. package/dist/geometry/meshBuilder.js +13 -0
  8. package/dist/geometry/meshConcat.js +1 -0
  9. package/dist/geometry/pixelFont.js +2 -0
  10. package/dist/geometry/solid.d.ts +61 -0
  11. package/dist/geometry/solid.js +152 -0
  12. package/dist/geometry/solidFlat.d.ts +35 -0
  13. package/dist/geometry/solidFlat.js +428 -0
  14. package/dist/geometry/solidHarness.d.ts +19 -0
  15. package/dist/geometry/solidHarness.js +66 -0
  16. package/dist/geometry/solidNormals.d.ts +17 -0
  17. package/dist/geometry/solidNormals.js +86 -0
  18. package/dist/geometry/solidRound.d.ts +41 -0
  19. package/dist/geometry/solidRound.js +323 -0
  20. package/dist/geometry/solidWriter.d.ts +16 -0
  21. package/dist/geometry/solidWriter.js +42 -0
  22. package/dist/index.d.ts +23 -2
  23. package/dist/index.js +13 -1
  24. package/dist/render/backend/api.d.ts +17 -3
  25. package/dist/render/backend/drawFog.d.ts +26 -0
  26. package/dist/render/backend/drawFog.js +30 -0
  27. package/dist/render/backend/materialChanges.d.ts +2 -1
  28. package/dist/render/backend/materialChanges.js +6 -1
  29. package/dist/render/backend/webgl2/instanced.d.ts +24 -5
  30. package/dist/render/backend/webgl2/instanced.js +72 -15
  31. package/dist/render/backend/webgl2/renderer.d.ts +122 -8
  32. package/dist/render/backend/webgl2/renderer.js +238 -121
  33. package/dist/render/backend/webgpu/buffers.d.ts +10 -0
  34. package/dist/render/backend/webgpu/buffers.js +34 -3
  35. package/dist/render/backend/webgpu/depthPass.js +5 -1
  36. package/dist/render/backend/webgpu/drawCommand.d.ts +5 -0
  37. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  38. package/dist/render/backend/webgpu/flatPass.d.ts +4 -1
  39. package/dist/render/backend/webgpu/flatPass.js +38 -9
  40. package/dist/render/backend/webgpu/glassTintPass.js +2 -1
  41. package/dist/render/backend/webgpu/instanceCullPass.d.ts +34 -0
  42. package/dist/render/backend/webgpu/instanceCullPass.js +144 -0
  43. package/dist/render/backend/webgpu/instanced.d.ts +11 -1
  44. package/dist/render/backend/webgpu/instanced.js +21 -3
  45. package/dist/render/backend/webgpu/latePass.d.ts +6 -2
  46. package/dist/render/backend/webgpu/latePass.js +11 -4
  47. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  48. package/dist/render/backend/webgpu/renderer.d.ts +100 -10
  49. package/dist/render/backend/webgpu/renderer.js +462 -201
  50. package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
  51. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
  52. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
  53. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +17 -3
  54. package/dist/render/backend/webgpu/surfaceTexturePass.js +77 -38
  55. package/dist/render/driftLight/bake.d.ts +7 -0
  56. package/dist/render/driftLight/bake.js +61 -56
  57. package/dist/render/driftLight/denseField.d.ts +46 -0
  58. package/dist/render/driftLight/denseField.js +109 -0
  59. package/dist/render/driftLight/lightField.d.ts +10 -5
  60. package/dist/render/driftLight/lightField.js +25 -19
  61. package/dist/render/driftLight/presence.d.ts +50 -0
  62. package/dist/render/driftLight/presence.js +43 -0
  63. package/dist/render/driftLight/uniforms.d.ts +3 -3
  64. package/dist/render/driftLight/uniforms.js +5 -6
  65. package/dist/render/driftLight/worldLightField.d.ts +41 -0
  66. package/dist/render/driftLight/worldLightField.js +66 -0
  67. package/dist/render/fixtureAtlas.d.ts +20 -0
  68. package/dist/render/fixtureAtlas.js +49 -0
  69. package/dist/render/instanceCull.d.ts +63 -0
  70. package/dist/render/instanceCull.js +160 -0
  71. package/dist/render/instances.d.ts +15 -0
  72. package/dist/render/lightBudget.d.ts +22 -33
  73. package/dist/render/lightBudget.js +24 -35
  74. package/dist/render/lightGrid.d.ts +42 -0
  75. package/dist/render/lightGrid.js +87 -0
  76. package/dist/render/mesh.d.ts +18 -24
  77. package/dist/render/mesh.js +51 -68
  78. package/dist/render/occlusion.d.ts +10 -0
  79. package/dist/render/occlusion.js +36 -7
  80. package/dist/render/pointLightSelection.d.ts +7 -0
  81. package/dist/render/pointLightSelection.js +29 -4
  82. package/dist/render/rainField.d.ts +13 -0
  83. package/dist/render/rainField.js +27 -5
  84. package/dist/render/recon/lateDepth.d.ts +11 -0
  85. package/dist/render/recon/lateDepth.js +70 -0
  86. package/dist/render/recon/resolve.d.ts +34 -4
  87. package/dist/render/recon/resolve.js +161 -20
  88. package/dist/render/rendererHarness.js +4 -0
  89. package/dist/render/sceneTarget.d.ts +2 -0
  90. package/dist/render/sceneTarget.js +2 -0
  91. package/dist/render/shaders/depth.js +12 -9
  92. package/dist/render/shaders/flat/driftLight.d.ts +1 -1
  93. package/dist/render/shaders/flat/driftLight.js +32 -7
  94. package/dist/render/shaders/flat/index.js +5 -3
  95. package/dist/render/shaders/flat/main.d.ts +1 -1
  96. package/dist/render/shaders/flat/main.js +75 -23
  97. package/dist/render/shaders/flat/preamble.js +75 -20
  98. package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
  99. package/dist/render/shaders/flat/surfaceEffects.js +152 -0
  100. package/dist/render/shaders/generated/depth.wgsl.d.ts +8 -8
  101. package/dist/render/shaders/generated/depth.wgsl.js +8 -8
  102. package/dist/render/shaders/generated/flat.wgsl.d.ts +1320 -1320
  103. package/dist/render/shaders/generated/flat.wgsl.js +1359 -1345
  104. package/dist/render/shaders/generated/lightVolume.wgsl.js +18 -4
  105. package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
  106. package/dist/render/shaders/generated/rush.wgsl.js +21 -16
  107. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
  108. package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
  109. package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
  110. package/dist/render/shaders/generated/text.wgsl.js +1 -1
  111. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
  112. package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
  113. package/dist/render/shaders/recon/lateDepth.wgsl.js +38 -13
  114. package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
  115. package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
  116. package/dist/render/shaders/rush.js +11 -1
  117. package/dist/render/shaders/sdfText.d.ts +1 -1
  118. package/dist/render/shaders/sdfText.js +3 -2
  119. package/dist/render/shaders/text.d.ts +1 -1
  120. package/dist/render/shaders/text.js +18 -9
  121. package/dist/render/surfaceEffects.d.ts +83 -0
  122. package/dist/render/surfaceEffects.js +75 -0
  123. package/dist/render/surfaceScene.d.ts +8 -0
  124. package/dist/render/surfaceScene.js +8 -0
  125. package/dist/render/surfaceTexture.d.ts +18 -2
  126. package/dist/render/surfaceTexture.js +68 -15
  127. package/dist/render/temporalAa.d.ts +25 -6
  128. package/dist/render/temporalAa.js +45 -15
  129. package/dist/render/textureSource.d.ts +30 -0
  130. package/dist/render/textureSource.js +53 -0
  131. package/dist/render/vertexDefaults.js +2 -0
  132. package/dist/world/hlod.d.ts +104 -0
  133. package/dist/world/hlod.js +214 -0
  134. package/package.json +6 -6
  135. package/src/geometry/boxCsg.ts +186 -0
  136. package/src/geometry/csg.ts +384 -0
  137. package/src/geometry/meshBuilder.ts +15 -0
  138. package/src/geometry/meshConcat.ts +1 -0
  139. package/src/geometry/pixelFont.ts +2 -0
  140. package/src/geometry/solid.ts +189 -0
  141. package/src/geometry/solidFlat.ts +522 -0
  142. package/src/geometry/solidHarness.ts +83 -0
  143. package/src/geometry/solidNormals.ts +103 -0
  144. package/src/geometry/solidRound.ts +443 -0
  145. package/src/geometry/solidWriter.ts +58 -0
  146. package/src/index.ts +51 -1
  147. package/src/render/backend/api.ts +21 -2
  148. package/src/render/backend/drawFog.ts +31 -0
  149. package/src/render/backend/materialChanges.ts +9 -2
  150. package/src/render/backend/webgl2/instanced.ts +80 -14
  151. package/src/render/backend/webgl2/renderer.ts +300 -143
  152. package/src/render/backend/webgpu/buffers.ts +42 -3
  153. package/src/render/backend/webgpu/depthPass.ts +5 -1
  154. package/src/render/backend/webgpu/drawCommand.ts +6 -0
  155. package/src/render/backend/webgpu/flatPass.ts +41 -11
  156. package/src/render/backend/webgpu/glassTintPass.ts +2 -1
  157. package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
  158. package/src/render/backend/webgpu/instanced.ts +21 -3
  159. package/src/render/backend/webgpu/latePass.ts +12 -3
  160. package/src/render/backend/webgpu/renderer.ts +540 -226
  161. package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
  162. package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
  163. package/src/render/backend/webgpu/surfaceTexturePass.ts +90 -50
  164. package/src/render/driftLight/bake.ts +85 -62
  165. package/src/render/driftLight/denseField.ts +149 -0
  166. package/src/render/driftLight/lightField.ts +33 -18
  167. package/src/render/driftLight/presence.ts +69 -0
  168. package/src/render/driftLight/uniforms.ts +8 -9
  169. package/src/render/driftLight/worldLightField.ts +83 -0
  170. package/src/render/fixtureAtlas.ts +63 -0
  171. package/src/render/instanceCull.ts +205 -0
  172. package/src/render/instances.ts +16 -0
  173. package/src/render/lightBudget.ts +25 -36
  174. package/src/render/lightGrid.ts +130 -0
  175. package/src/render/mesh.ts +59 -72
  176. package/src/render/occlusion.ts +51 -7
  177. package/src/render/pointLightSelection.ts +72 -4
  178. package/src/render/rainField.ts +39 -5
  179. package/src/render/recon/lateDepth.ts +83 -0
  180. package/src/render/recon/resolve.ts +206 -20
  181. package/src/render/rendererHarness.ts +4 -0
  182. package/src/render/sceneTarget.ts +4 -0
  183. package/src/render/shaders/depth.ts +12 -9
  184. package/src/render/shaders/flat/driftLight.ts +32 -7
  185. package/src/render/shaders/flat/index.ts +5 -3
  186. package/src/render/shaders/flat/main.ts +75 -23
  187. package/src/render/shaders/flat/preamble.ts +75 -20
  188. package/src/render/shaders/flat/surfaceEffects.ts +152 -0
  189. package/src/render/shaders/generated/depth.wgsl.ts +8 -8
  190. package/src/render/shaders/generated/flat.wgsl.ts +1361 -1345
  191. package/src/render/shaders/generated/lightVolume.wgsl.ts +20 -4
  192. package/src/render/shaders/generated/rush.wgsl.ts +21 -16
  193. package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
  194. package/src/render/shaders/generated/text.wgsl.ts +1 -1
  195. package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
  196. package/src/render/shaders/recon/lateDepth.wgsl.ts +38 -13
  197. package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
  198. package/src/render/shaders/rush.ts +11 -1
  199. package/src/render/shaders/sdfText.ts +3 -2
  200. package/src/render/shaders/text.ts +18 -9
  201. package/src/render/surfaceEffects.ts +132 -0
  202. package/src/render/surfaceScene.ts +16 -0
  203. package/src/render/surfaceTexture.ts +110 -19
  204. package/src/render/temporalAa.ts +78 -14
  205. package/src/render/textureSource.ts +72 -0
  206. package/src/render/vertexDefaults.ts +2 -0
  207. package/src/world/hlod.ts +290 -0
@@ -1,6 +1,6 @@
1
1
  import { mat4 } from 'gl-matrix';
2
2
  import { probeFaceRange } from '../../reflectionProbe.js';
3
- import { TEMPORAL_HISTORY_BLEND, TemporalHistory, jitterOffset, jitterProjection, } from '../../temporalAa.js';
3
+ import { TEMPORAL_FLICKER, TEMPORAL_HISTORY_BLEND, TemporalHistory, flickerRule, jitterOffset, jitterProjection, } from '../../temporalAa.js';
4
4
  import { OIT_MULTISAMPLE_REFUSAL } from '../../orderIndependent.js';
5
5
  import { TranslucentQueue } from '../../translucentQueue.js';
6
6
  import { DEPTH_CLEAR, MAX_DEPTH_LAYER, REVERSED_DEPTH } from '../../depthConvention.js';
@@ -36,6 +36,8 @@ import { ProbeSweeps } from '../../probeSweeps.js';
36
36
  import { ggxMaxLevelFor, irradianceLevelFor, octahedralEdgeFor } from '../../prefilterEnvMap.js';
37
37
  import { environmentTexels } from './environmentTexels.js';
38
38
  import { packIesAtlas } from '../../iesProfile.js';
39
+ import { fixtureAtlasLayout, fixtureAtlasTexels } from '../../fixtureAtlas.js';
40
+ import { resolveSurfaceScene } from '../../surfaceScene.js';
39
41
  import { COOKIE_TILE } from '../../lightBudget.js';
40
42
  import { FULL_LIGHT_BUDGET } from '../../uniformVectorBudget.js';
41
43
  import { toHalfFloats } from '../../halfFloat.js';
@@ -96,6 +98,7 @@ import { LIGHT_VOLUME_VERT_FIELDS, LIGHT_VOLUME_VERT_SIZE, createLightVolumeBind
96
98
  import { aimReflection, createGpuReflection } from './reflectionPass.js';
97
99
  import { FrameBudget } from '../budget.js';
98
100
  import { MaterialChanges, ownsMaterial } from '../materialChanges.js';
101
+ import { FOG_RECEDE, fogModeOf } from '../drawFog.js';
99
102
  import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.js';
100
103
  import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.js';
101
104
  import { DistanceFieldScene } from '../../gi/fieldScene.js';
@@ -122,6 +125,8 @@ import { decalScissor } from '../../decalProjector.js';
122
125
  import { createCommandPool, resetPool, takeCommand } from './drawCommand.js';
123
126
  import { RESOURCE_COUNT, createArena, createFlushSchedule, keptNodes, maskOf, nodeCount, nodeState, nodeVerb, recordNode, resetArena, schedule, scheduleFlush, scratchFor, } from '../../frame/index.js';
124
127
  import { createFrustum, frustumFromViewProjection } from '../../../math/frustum.js';
128
+ import { batchBoxVisible, cullsInstances } from '../../instanceCull.js';
129
+ import { InstanceCullPass, MAX_CULL_SLOTS } from './instanceCullPass.js';
125
130
  import { OcclusionBuffer } from '../../occlusion.js';
126
131
  import { boundsVisible } from '../../visibility.js';
127
132
  import { createPassRegistry, drainRegistry, passAt, registerIn, unregisterIn } from '../../pass.js';
@@ -131,6 +136,7 @@ import { computeAt, createComputeRegistry, } from '../../compute.js';
131
136
  import { GpuInstancedBatch } from './instanced.js';
132
137
  import { cutoutOf } from '../../cutoutCaster.js';
133
138
  import { LightField } from '../../driftLight/lightField.js';
139
+ import { WorldLightField } from '../../driftLight/worldLightField.js';
134
140
  import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.js';
135
141
  /**
136
142
  * How many draws one frame may make before the ring is full.
@@ -1295,6 +1301,12 @@ export class WebGPURenderer {
1295
1301
  * what the layout already declares.
1296
1302
  */
1297
1303
  bloomGroups = [];
1304
+ /**
1305
+ * The picture the bloom reads, and its size: the scene target, or while reconstructing the
1306
+ * resolved picture at the drawing buffer's size. See `rebuildBloomGroups`.
1307
+ */
1308
+ bloomSourceWidth = 1;
1309
+ bloomSourceHeight = 1;
1298
1310
  frameWidth = 1;
1299
1311
  frameHeight = 1;
1300
1312
  /**
@@ -1453,6 +1465,18 @@ export class WebGPURenderer {
1453
1465
  */
1454
1466
  reconHistories = [null, null];
1455
1467
  reconHistoryViews = [null, null];
1468
+ /**
1469
+ * Each output pixel's flicker record, a pair swapped with the histories: the temporal resolve's
1470
+ * anti-flicker, over the reconstruction's own period (`recon/resolve.ts`).
1471
+ */
1472
+ reconFlickers = [null, null];
1473
+ reconFlickerViews = [null, null];
1474
+ /** Each output pixel's held depth swing, a pair of its own: see `recon/resolve.ts`. */
1475
+ reconSwings = [null, null];
1476
+ reconSwingViews = [null, null];
1477
+ /** The anti-flicker's rule for the period last asked for, kept so a frame makes none. */
1478
+ reconFlickerPhases = 0;
1479
+ reconFlickerRule = TEMPORAL_FLICKER;
1456
1480
  reconWrite = 0;
1457
1481
  reconHasHistory = false;
1458
1482
  reconShown = null;
@@ -1486,6 +1510,9 @@ export class WebGPURenderer {
1486
1510
  /** Last frame's raw view-projection and its inverse, and the two eyes. */
1487
1511
  reconPreviousRawViewProj = mat4.create();
1488
1512
  reconInverseViewProj = mat4.create();
1513
+ /** The inverse and the reprojection, multiplied in double precision. See `runReconstruction`. */
1514
+ reconInverse64 = new Float64Array(16);
1515
+ reconReprojection64 = new Float64Array(16);
1489
1516
  reconPreviousInverseViewProj = mat4.create();
1490
1517
  reconEye = new Float32Array(3);
1491
1518
  reconPreviousEye = new Float32Array(3);
@@ -1631,6 +1658,8 @@ export class WebGPURenderer {
1631
1658
  /** Said once for the whole renderer. See `refuseOitMultisampled`. */
1632
1659
  oitMultisampleRefused = false;
1633
1660
  oitReplaying = false;
1661
+ /** `uSurfaceScene`, resolved once a pass. See `surfaceScene.ts`. */
1662
+ surfaceScene = new Float32Array(4);
1634
1663
  /** Which buffer the replay is filling, which the pipeline key must carry. */
1635
1664
  oitMode = 'none';
1636
1665
  /**
@@ -1906,6 +1935,8 @@ export class WebGPURenderer {
1906
1935
  return this.ringList;
1907
1936
  }
1908
1937
  flushRings() {
1938
+ /* Culls first: a submit that follows may carry a draw reading what they write. */
1939
+ this.instanceCulls?.flush();
1909
1940
  for (const ring of this.rings())
1910
1941
  ring.flush();
1911
1942
  }
@@ -2102,6 +2133,10 @@ export class WebGPURenderer {
2102
2133
  createSurfaceTexture(source, options = {}) {
2103
2134
  return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
2104
2135
  }
2136
+ /** Several images of one size as an array's layers. See `RendererApi.createSurfaceTextureArray`. */
2137
+ createSurfaceTextureArray(sources, options = {}) {
2138
+ return new GpuSurfaceTexture(this.surface.device, this.pipelines, sources, options);
2139
+ }
2105
2140
  /**
2106
2141
  * GPU textures an `updateSurfaceTexture` replaced, destroyed when the next frame begins.
2107
2142
  *
@@ -2367,6 +2402,19 @@ export class WebGPURenderer {
2367
2402
  f[this.materialField('uGrain')] = Math.min(1, Math.max(0, amount));
2368
2403
  });
2369
2404
  }
2405
+ /**
2406
+ * How much of the surfaces drawn next survives a screen-door dither. See `renderer.ts`.
2407
+ *
2408
+ * Written straight into the scratch block rather than through `material()`'s callback, because
2409
+ * this one is called per crossfading region per frame and a closure per call is an allocation.
2410
+ */
2411
+ setDitherFade(amount) {
2412
+ if (this.surface.lost)
2413
+ return;
2414
+ /* The second half of `uWriteMode`, which the order-independent weight shares. */
2415
+ this.perFrameFloats[this.materialField('uWriteMode') + 1] = Math.min(1, Math.max(-1, amount));
2416
+ this.materials.dirty();
2417
+ }
2370
2418
  /**
2371
2419
  * How strong the microscopic relief on the surfaces drawn next is, and how coarse.
2372
2420
  *
@@ -3978,6 +4026,8 @@ export class WebGPURenderer {
3978
4026
  waterLayout;
3979
4027
  /** The one-pixel stand-in, kept so the water bind group can be rebuilt without one. */
3980
4028
  blankView;
4029
+ /** The blank texel as a one-layer array: the stand-in every surface-texture binding takes. */
4030
+ blankSurfaceView;
3981
4031
  blankSampler;
3982
4032
  /**
3983
4033
  * A slot per body, not one buffer rewritten before each draw.
@@ -4865,6 +4915,9 @@ export class WebGPURenderer {
4865
4915
  * `uStaticShadowMap` rather than by anything here changing shape.
4866
4916
  */
4867
4917
  const blankView = blank.createView();
4918
+ /* The same texel as a one-layer array, for the four material maps, which are arrays now. */
4919
+ const blankSurfaceView = blank.createView({ dimension: '2d-array' });
4920
+ this.blankSurfaceView = blankSurfaceView;
4868
4921
  const blankSampler = device.createSampler({ label: 'flat.sampler' });
4869
4922
  /*
4870
4923
  * A one-texel integer stand-in for the froxel table.
@@ -5037,7 +5090,7 @@ export class WebGPURenderer {
5037
5090
  if (name === 'uNormalMap') {
5038
5091
  const map = this.normalMap;
5039
5092
  return map === null
5040
- ? { view: blankView, sampler: blankSampler }
5093
+ ? { view: blankSurfaceView, sampler: blankSampler }
5041
5094
  : { view: map.view, sampler: map.sampler };
5042
5095
  }
5043
5096
  /* The caller's ORM map, or the stand-in, for the reason `uNormalMap` gives just above. */
@@ -5045,21 +5098,26 @@ export class WebGPURenderer {
5045
5098
  if (name === 'uEmissiveMap') {
5046
5099
  const map = this.emissiveMap;
5047
5100
  return map === null
5048
- ? { view: blankView, sampler: blankSampler }
5101
+ ? { view: blankSurfaceView, sampler: blankSampler }
5049
5102
  : { view: map.view, sampler: map.sampler };
5050
5103
  }
5051
5104
  if (name === 'uOrmMap') {
5052
5105
  const map = this.ormMap;
5053
5106
  return map === null
5054
- ? { view: blankView, sampler: blankSampler }
5107
+ ? { view: blankSurfaceView, sampler: blankSampler }
5055
5108
  : { view: map.view, sampler: map.sampler };
5056
5109
  }
5057
5110
  if (name === 'uAlbedo') {
5058
5111
  const albedo = this.albedo;
5059
5112
  return albedo === null
5060
- ? { view: blankView, sampler: blankSampler }
5113
+ ? { view: blankSurfaceView, sampler: blankSampler }
5061
5114
  : { view: albedo.view, sampler: albedo.sampler };
5062
5115
  }
5116
+ /* The albedo's effects table, or a one-texel stand-in the shader's size test reads as none.
5117
+ The albedo is the group's key, so its table needs no key of its own. */
5118
+ if (name === 'uSurfaceEffects') {
5119
+ return { view: this.albedo?.effectsView ?? blankView, sampler: blankSampler };
5120
+ }
5063
5121
  /*
5064
5122
  * The room, once a bake has filled it. Unbaked reads as the blank cube: nothing to mirror.
5065
5123
  *
@@ -5091,18 +5149,8 @@ export class WebGPURenderer {
5091
5149
  * this one is a curve sampled between two angles rather than data addressed by index, so
5092
5150
  * interpolation across a row is the point. `postSampler` is linear and clamped.
5093
5151
  */
5094
- if (name === 'uIesAtlas') {
5095
- return { view: this.iesView ?? blankView, sampler: iesSampler };
5096
- }
5097
- /*
5098
- * The cookie atlas, or the single white texel a scene with no cookie falls back to.
5099
- *
5100
- * A filtering sampler, like the photometric atlas and unlike the froxel table: a cookie is
5101
- * an image and its whole point is that it is soft at the edges. The shader's own half-texel
5102
- * inset is what keeps one tile out of the next; a clamp cannot, the seam being interior.
5103
- */
5104
- if (name === 'uCookieAtlas') {
5105
- return { view: this.cookieView ?? blankView, sampler: iesSampler };
5152
+ if (name === 'uFixtureAtlas') {
5153
+ return { view: this.fixtureAtlasView(), sampler: iesSampler };
5106
5154
  }
5107
5155
  /*
5108
5156
  * DriftLight's brick index, read by `texelFetch` alone, so it shares the shadow group's
@@ -5622,6 +5670,8 @@ export class WebGPURenderer {
5622
5670
  continue;
5623
5671
  present[String(attribute.name)] = key.includes(`:${String(attribute.name)}`);
5624
5672
  }
5673
+ /* Not a layout entry of its own — it widens `uvs` — so read from the key beside them. */
5674
+ present['layers'] = key.includes(':layers');
5625
5675
  return present;
5626
5676
  }
5627
5677
  /**
@@ -5633,6 +5683,7 @@ export class WebGPURenderer {
5633
5683
  * uncaught exceptions a second in somebody's error reporting.
5634
5684
  */
5635
5685
  beginFrame(clearColor) {
5686
+ this.cullFrame += 1;
5636
5687
  /* Whatever an update replaced during the last frame; nothing recorded can read it now. */
5637
5688
  for (const texture of this.retiredTextures)
5638
5689
  texture.destroy();
@@ -5890,6 +5941,15 @@ export class WebGPURenderer {
5890
5941
  present[String(attribute.name)] = supplied;
5891
5942
  key += supplied ? `:${String(attribute.name)}` : '';
5892
5943
  }
5944
+ /*
5945
+ * Texture-array layers widen the UV field to three floats (`LAYERED_UVS`), which is a different
5946
+ * vertex layout, so the key has to say so or a layered and a plain mesh would share a pipeline
5947
+ * and one of them would read every field after the UVs from the wrong offset.
5948
+ */
5949
+ if (data.layers !== undefined) {
5950
+ present['layers'] = true;
5951
+ key += ':layers';
5952
+ }
5893
5953
  const fullKey = `${this.variant}|${key}`;
5894
5954
  /*
5895
5955
  * **For every target this mesh can land on, which is two when the overlay is its own.**
@@ -6081,46 +6141,17 @@ export class WebGPURenderer {
6081
6141
  * Its sequence is its own: `reconJitterPhases` takes eight positions for every output pixel a
6082
6142
  * render pixel covers, where the temporal resolve's eight are enough at one to one.
6083
6143
  */
6084
- this.reconstructing = this.reconstructionWanted && !this.reflectionPassActive;
6085
- if (this.quality.reconstruction > 0 && this.samples > 1 && !this.reconMultisampleSaid) {
6086
- this.reconMultisampleSaid = true;
6087
- console.warn(RECONSTRUCTION_MULTISAMPLE_REFUSAL);
6088
- }
6089
- if (this.reconstructing) {
6090
- /*
6091
- * The offset `settleReconJitter` chose for the frame at `beginFrame`, applied here and not
6092
- * advanced here: a frame binding its mesh pass twice is one frame of the sequence.
6093
- *
6094
- * **The y offset is negated on the way in, and that is not a preference.** This shifts the
6095
- * *corrected* matrix, whose y `CLIP_CORRECTION` has already negated so that the negation
6096
- * every generated vertex stage ends with cancels out — §3 row 56. So an offset added here
6097
- * arrives on screen with its sign reversed, while x, which nothing negates, arrives as
6098
- * given. The temporal resolve cannot see this because its shader never reads the offset; the
6099
- * reconstruction reads it at every texel it unprojects, and a sign error there moves every
6100
- * sample two jitters away from where the depth says it is.
6101
- */
6102
- this.viewProj = jitterProjection(this.jitteredViewProj, this.correctedViewProj, this.reconJitter[0], -this.reconJitter[1], this.renderWidth, this.renderHeight);
6103
- this.temporalJittering = false;
6104
- this.temporalHistoryUsable = false;
6105
- }
6106
- this.temporalJittering =
6107
- !this.reconstructing &&
6108
- this.quality.temporalAa &&
6109
- this.quality.screenEffects &&
6110
- !this.reflectionPassActive;
6111
- if (this.temporalJittering) {
6112
- /* The scene's size, not the drawing buffer's: the history is a picture of the render. */
6113
- this.temporalHistoryUsable = this.temporalHistory.openFrame(this.renderWidth, this.renderHeight);
6114
- const [jx, jy] = jitterOffset(this.temporalHistory.frameIndex);
6115
- this.temporalJitterX = jx;
6116
- this.temporalJitterY = jy;
6117
- /* A jitter is half a *render* texel, which is what the sequence is spread over. */
6118
- this.viewProj = jitterProjection(this.jitteredViewProj, this.correctedViewProj, jx, jy, this.renderWidth, this.renderHeight);
6119
- }
6120
- else if (!this.reconstructing) {
6121
- this.temporalHistoryUsable = false;
6144
+ /*
6145
+ * **After the present there is no jitter to add and no temporal frame to open.** A mesh pass
6146
+ * bound then is the interface's, drawn over a picture already resolved: jittered, a map and every
6147
+ * edge of it moved by a fraction of a pixel each frame with nothing to resolve it; and opening a
6148
+ * second temporal frame stepped the sequence twice a frame, so the world saw every other
6149
+ * position and the anti-flicker's period never started.
6150
+ */
6151
+ if (this.framePresented)
6122
6152
  this.viewProj = this.correctedViewProj;
6123
- }
6153
+ else
6154
+ this.jitterFrame();
6124
6155
  /*
6125
6156
  * The frustum, once a frame, and from the **uncorrected** matrix.
6126
6157
  *
@@ -6213,8 +6244,11 @@ export class WebGPURenderer {
6213
6244
  * frame either way, because an unwritten one is four zeroes and `dot(p, 0) < 0` is false,
6214
6245
  * so the term would silently pass everything the first time somebody enabled it.
6215
6246
  */
6216
- i[at('uClipEnabled')] = this.reflectionPassActive ? 1 : 0;
6217
- f.set(this.reflection?.clipPlane ?? NO_CLIP_PLANE, at('uClipPlane'));
6247
+ /* The mirror's plane in a reflection pass and zero, which clips nothing, in every other: the lit
6248
+ stage has no flag beside the plane. See `uClipPlane` in `flat/preamble.ts`. */
6249
+ f.set(this.reflectionPassActive ? (this.reflection?.clipPlane ?? NO_CLIP_PLANE) : NO_CLIP_PLANE, at('uClipPlane'));
6250
+ resolveSurfaceScene(env, this.surfaceScene);
6251
+ f.set(this.surfaceScene, at('uSurfaceScene'));
6218
6252
  /*
6219
6253
  * The material and grading terms, matching `renderer.ts` value for value.
6220
6254
  *
@@ -6234,6 +6268,9 @@ export class WebGPURenderer {
6234
6268
  */
6235
6269
  f[at('uGrain')] = 1;
6236
6270
  f[at('uReflectivity')] = 0;
6271
+ /* No weighting and no dither until a pass or a crossfade asks. See `setDitherFade`. */
6272
+ f[at('uWriteMode')] = 0;
6273
+ f[at('uWriteMode') + 1] = 0;
6237
6274
  /* 1, not 0: a multiplier's identity is one. See `setEnvironmentGain`. */
6238
6275
  f[at('uEnvironmentGain')] = 1;
6239
6276
  /*
@@ -6753,7 +6790,7 @@ export class WebGPURenderer {
6753
6790
  * **Recorded rather than drawn**, when the effect is on and this is not already the replay.
6754
6791
  * Everything is copied — see `TranslucentQueue`, and the scratch matrix that made it necessary.
6755
6792
  */
6756
- if (this.oitActive && !this.oitReplaying) {
6793
+ if (this.oitActive && !this.oitReplaying && options.additive !== true) {
6757
6794
  this.translucentQueue.record(mesh, model, opacity, options);
6758
6795
  return;
6759
6796
  }
@@ -6798,7 +6835,10 @@ export class WebGPURenderer {
6798
6835
  return;
6799
6836
  }
6800
6837
  /* A blended draw landing after the upscale is drawn into an unjittered picture. */
6801
- const late = blend && (this.drawsLate() || (this.oitReplaying && this.reconstructing));
6838
+ /* Unless it moves with what it lies on, which the resolve reconstructs it with. See
6839
+ `TranslucentMeshOptions.reconstructed`. */
6840
+ const lands = blend && options.reconstructed !== true && this.drawsLate();
6841
+ const late = lands || (blend && this.oitReplaying && this.reconstructing);
6802
6842
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, late ? this.correctedViewProj : this.viewProj);
6803
6843
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uModel.offset, model);
6804
6844
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uTint.offset, tint ?? WHITE);
@@ -6832,19 +6872,19 @@ export class WebGPURenderer {
6832
6872
  this.perFrameFloats[this.materialField('uOpacity')] = opacity;
6833
6873
  /* Material state exactly like `uOpacity`, dirtied for the accumulation pass and restored
6834
6874
  below — the revealage pass draws with it at zero and its colour multiplied away. */
6835
- this.perFrameFloats[this.materialField('uOitWeighted')] = this.oitMode === 'accum' ? 1 : 0;
6875
+ this.perFrameFloats[this.materialField('uWriteMode')] = this.oitMode === 'accum' ? 1 : 0;
6836
6876
  }
6837
6877
  /* Same shape as `dimmed`: a slot is only worth taking when either differs from the default
6838
6878
  `bindMeshPass` already wrote, and both are put back together once the draw is submitted. */
6839
6879
  const lit = options.lit ?? true;
6840
- const fog = options.fog ?? true;
6880
+ /* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
6881
+ const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
6841
6882
  const toneMapped = options.toneMapped ?? true;
6842
- const unlitOrUnfogged = !lit || !fog;
6883
+ const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
6843
6884
  if (unlitOrUnfogged) {
6844
6885
  if (!lit)
6845
6886
  this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
6846
- if (!fog)
6847
- this.perFrameInts[this.materialField('uFogEnabled')] = 0;
6887
+ this.perFrameInts[this.materialField('uFogEnabled')] = fog;
6848
6888
  }
6849
6889
  /* `1` is sRGB alone: the conversion without the curve. `Math.min` rather than a literal, so
6850
6890
  a renderer asked for `none` stays at none. See `TranslucentMeshOptions.toneMapped`. */
@@ -6890,7 +6930,9 @@ export class WebGPURenderer {
6890
6930
  */
6891
6931
  const depthWrite = options.depthWrite ?? true;
6892
6932
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
6893
- const key = `${keyed}${blend ? '|blend' : ''}` +
6933
+ /* Added rather than blended over: its own blend state, so its own pipeline. */
6934
+ const adds = blend && options.additive === true;
6935
+ const key = `${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
6894
6936
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
6895
6937
  /* The two order-independent buffers are two formats and two blend states, so a draw into
6896
6938
  one must never share a cached pipeline with the same mesh drawn into the other. */
@@ -6919,14 +6961,14 @@ export class WebGPURenderer {
6919
6961
  * pays the compile it was trying to avoid. That is a slow frame; the alternative is a
6920
6962
  * thrown error in the middle of one.
6921
6963
  */
6922
- const pipelines = blend ? this.blendedPipelines() : this.targetPipelines();
6964
+ const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
6923
6965
  let pipeline = pipelines.peek(key);
6924
6966
  if (pipeline === undefined) {
6925
6967
  const present = this.meshPresent.get(base);
6926
6968
  if (present === undefined) {
6927
6969
  throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
6928
6970
  }
6929
- pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, blend, skinned, morphed, depthWrite, layer, false, this.oitMode, this.materialDoubleSided);
6971
+ pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, adds ? 'additive' : blend, skinned, morphed, depthWrite, layer, false, this.oitMode, this.materialDoubleSided);
6930
6972
  }
6931
6973
  /*
6932
6974
  * Guards the draw call only, not the restore below.
@@ -6951,7 +6993,9 @@ export class WebGPURenderer {
6951
6993
  * the uniform rings are the state arena this design would otherwise have had to build —
6952
6994
  * so the command carries them as its two dynamic offsets.
6953
6995
  */
6954
- const command = blend ? this.recordBlended(0) : this.recordDraw(0, this.currentTarget());
6996
+ const command = blend
6997
+ ? this.recordBlended(0, lands)
6998
+ : this.recordDraw(0, this.currentTarget());
6955
6999
  if (command !== null) {
6956
7000
  command.pipeline = pipeline;
6957
7001
  command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
@@ -6984,13 +7028,12 @@ export class WebGPURenderer {
6984
7028
  whether or not this one found a slot to draw with. */
6985
7029
  if (dimmed) {
6986
7030
  this.perFrameFloats[this.materialField('uOpacity')] = 1;
6987
- this.perFrameFloats[this.materialField('uOitWeighted')] = 0;
7031
+ this.perFrameFloats[this.materialField('uWriteMode')] = 0;
6988
7032
  }
6989
7033
  if (unlitOrUnfogged) {
6990
7034
  if (!lit)
6991
7035
  this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
6992
- if (!fog)
6993
- this.perFrameInts[this.materialField('uFogEnabled')] = 1;
7036
+ this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
6994
7037
  }
6995
7038
  /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
6996
7039
  shared, so a strength left set is worn by everything drawn after it. */
@@ -7015,7 +7058,7 @@ export class WebGPURenderer {
7015
7058
  * anyway, because WebGL2 *cannot* and a capability one backend has and the other does not is
7016
7059
  * the shape this repository spent a week removing.
7017
7060
  */
7018
- createInstanced(mesh, capacity) {
7061
+ createInstanced(mesh, capacity, options = {}) {
7019
7062
  /*
7020
7063
  * **A skinned mesh cannot be instanced, and the reason is the vertex layout rather than the
7021
7064
  * shader.** An instanced pipeline reclaims locations 11 and 12 from the joint indices and
@@ -7041,16 +7084,13 @@ export class WebGPURenderer {
7041
7084
  'an ordinary mesh, or drop the channel — the sway, sky and alpha lanes cannot reach an ' +
7042
7085
  'instanced pipeline.');
7043
7086
  }
7044
- if (this.instanced.has(mesh)) {
7045
- throw new Error('WebGPU: this mesh already has an instanced batch — a mesh may have one, because the ' +
7046
- "other backend binds the attributes to the mesh's own vertex array.");
7047
- }
7048
- const batch = new GpuInstancedBatch(this.surface.device, mesh, capacity, `flat.instances:${mesh.key ?? 'mesh'}`);
7049
- this.instanced.set(mesh, batch);
7087
+ const batch = new GpuInstancedBatch(this.surface.device, mesh, capacity, `flat.instances:${mesh.key ?? 'mesh'}`, options.cull ?? false);
7050
7088
  return batch;
7051
7089
  }
7052
- /** Which meshes already carry a batch, so a second is refused rather than silently shared. */
7053
- instanced = new Map();
7090
+ /** The device instance culls, built on the first culling draw. See `InstanceCullPass`. */
7091
+ instanceCulls = null;
7092
+ /** Counts frames, so a batch knows when its cull slots were last taken. */
7093
+ cullFrame = 0;
7054
7094
  /** Push placement and colour. Only the live prefix; see `GpuInstancedBatch.upload`. */
7055
7095
  uploadInstanced(batch, data) {
7056
7096
  if (this.surface.lost)
@@ -7091,7 +7131,6 @@ export class WebGPURenderer {
7091
7131
  }
7092
7132
  /** Release the placement. The mesh is the caller's and is not released. */
7093
7133
  disposeInstanced(batch) {
7094
- this.instanced.delete(batch.mesh);
7095
7134
  batch.dispose();
7096
7135
  }
7097
7136
  /**
@@ -7144,6 +7183,31 @@ export class WebGPURenderer {
7144
7183
  * **What it gives up** against `drawMesh` is skinning and morphing, refused where the variant
7145
7184
  * is built, and a per-instance opacity: `opacity` here dims the whole batch.
7146
7185
  */
7186
+ /**
7187
+ * The next of a batch's cull slots this frame, with its cull queued against the current view; null
7188
+ * when the batch has drawn from `MAX_CULL_SLOTS` views already or the frame's job ring is full,
7189
+ * and the draw then takes every instance.
7190
+ */
7191
+ takeCullSlot(batch, count) {
7192
+ if (batch.slotFrame !== this.cullFrame) {
7193
+ batch.slotFrame = this.cullFrame;
7194
+ batch.slotsTaken = 0;
7195
+ }
7196
+ if (batch.slotsTaken >= MAX_CULL_SLOTS)
7197
+ return null;
7198
+ this.instanceCulls ??= new InstanceCullPass(this.surface.device);
7199
+ let slot = batch.slots[batch.slotsTaken];
7200
+ if (slot === undefined) {
7201
+ slot = this.instanceCulls.createSlot(batch.buffer, batch.capacity, 'flat.instances');
7202
+ batch.slots.push(slot);
7203
+ }
7204
+ const bounds = batch.mesh.bounds;
7205
+ const queued = this.instanceCulls.queue(slot, this.frustum, bounds.centre, bounds.radius, count, batch.mesh.indexCount);
7206
+ if (!queued)
7207
+ return null;
7208
+ batch.slotsTaken += 1;
7209
+ return slot;
7210
+ }
7147
7211
  submitInstanced(batch, data, opacity, blend, options) {
7148
7212
  if (!this.canDraw() || this.viewProj === null)
7149
7213
  return;
@@ -7153,6 +7217,14 @@ export class WebGPURenderer {
7153
7217
  const count = Math.min(data.count, batch.capacity);
7154
7218
  if (count === 0)
7155
7219
  return;
7220
+ /* A culling batch's camera draw: the whole batch against the view and the occluders on the CPU,
7221
+ then instance by instance on the device into an indirect draw. Blended draws keep every
7222
+ instance and their order. See `InstancedOptions.cull`. */
7223
+ const culling = !blend && batch.cull;
7224
+ if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion))
7225
+ return;
7226
+ const cut = culling && cullsInstances(count, mesh.indexCount) ? this.takeCullSlot(batch, count) : null;
7227
+ const instanceBuffer = cut === null ? batch.buffer : cut.instances;
7156
7228
  const slot = this.perDraw.allocate();
7157
7229
  if (slot === null) {
7158
7230
  if (!this.warnedFull) {
@@ -7161,7 +7233,9 @@ export class WebGPURenderer {
7161
7233
  }
7162
7234
  return;
7163
7235
  }
7164
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, blend && this.drawsLate() ? this.correctedViewProj : this.viewProj);
7236
+ /* Late unless it moves with what it lies on. See `TranslucentMeshOptions.reconstructed`. */
7237
+ const lands = blend && options.reconstructed !== true && this.drawsLate();
7238
+ this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, lands ? this.correctedViewProj : this.viewProj);
7165
7239
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
7166
7240
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
7167
7241
  this.perDraw.writeInt(slot, FLAT_VERT_FIELDS.uHasTangents.offset, mesh.hasTangents ? 1 : 0);
@@ -7170,14 +7244,13 @@ export class WebGPURenderer {
7170
7244
  this.perFrameFloats[this.materialField('uOpacity')] = opacity;
7171
7245
  }
7172
7246
  const lit = options.lit ?? true;
7173
- const fog = options.fog ?? true;
7247
+ const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
7174
7248
  const toneMapped = options.toneMapped ?? true;
7175
- const unlitOrUnfogged = !lit || !fog;
7249
+ const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
7176
7250
  if (unlitOrUnfogged) {
7177
7251
  if (!lit)
7178
7252
  this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
7179
- if (!fog)
7180
- this.perFrameInts[this.materialField('uFogEnabled')] = 0;
7253
+ this.perFrameInts[this.materialField('uFogEnabled')] = fog;
7181
7254
  }
7182
7255
  if (!toneMapped) {
7183
7256
  this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.gradeCode(), 1);
@@ -7193,21 +7266,24 @@ export class WebGPURenderer {
7193
7266
  /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
7194
7267
  a third vertex buffer and a different vertex module, and sharing a cache entry with the
7195
7268
  plain one would hand a draw the wrong layout. */
7196
- const key = `${base}|inst${blend ? '|blend' : ''}` +
7269
+ const adds = blend && options.additive === true;
7270
+ const key = `${base}|inst${blend ? (adds ? '|add' : '|blend') : ''}` +
7197
7271
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
7198
7272
  `${this.materialDoubleSided ? '|2s' : ''}`;
7199
- const pipelines = blend ? this.blendedPipelines() : this.targetPipelines();
7273
+ const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
7200
7274
  let pipeline = pipelines.peek(key);
7201
7275
  if (pipeline === undefined) {
7202
7276
  const present = this.meshPresent.get(base);
7203
7277
  if (present === undefined) {
7204
7278
  throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
7205
7279
  }
7206
- pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(false, false), this.variant, key, present, blend, false, false, depthWrite, layer, true, 'none', this.materialDoubleSided);
7280
+ pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(false, false), this.variant, key, present, adds ? 'additive' : blend, false, false, depthWrite, layer, true, 'none', this.materialDoubleSided);
7207
7281
  }
7208
7282
  const material = this.materialSlotForDraw();
7209
7283
  if (material !== null) {
7210
- const command = blend ? this.recordBlended(0) : this.recordDraw(0, this.currentTarget());
7284
+ const command = blend
7285
+ ? this.recordBlended(0, lands)
7286
+ : this.recordDraw(0, this.currentTarget());
7211
7287
  if (command !== null) {
7212
7288
  command.pipeline = pipeline;
7213
7289
  command.bindGroup = this.flatBindGroupFor(false, false, null);
@@ -7218,12 +7294,13 @@ export class WebGPURenderer {
7218
7294
  for (; index < mesh.vertexBuffers.length; index++) {
7219
7295
  command.vertexBuffers[index] = mesh.vertexBuffers[index];
7220
7296
  }
7221
- command.vertexBuffers[index] = batch.buffer;
7297
+ command.vertexBuffers[index] = instanceBuffer;
7222
7298
  command.vertexCount = mesh.vertexBuffers.length + 1;
7223
7299
  command.indexBuffer = mesh.indexBuffer;
7224
7300
  command.indexed = true;
7225
7301
  command.count = mesh.indexCount;
7226
7302
  command.instances = count;
7303
+ command.indirect = cut === null ? null : cut.args;
7227
7304
  }
7228
7305
  else {
7229
7306
  const pass = this.openPass();
@@ -7235,9 +7312,12 @@ export class WebGPURenderer {
7235
7312
  for (; index < mesh.vertexBuffers.length; index++) {
7236
7313
  pass.setVertexBuffer(index, mesh.vertexBuffers[index]);
7237
7314
  }
7238
- pass.setVertexBuffer(index, batch.buffer);
7315
+ pass.setVertexBuffer(index, instanceBuffer);
7239
7316
  pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
7240
- pass.drawIndexed(mesh.indexCount, count);
7317
+ if (cut === null)
7318
+ pass.drawIndexed(mesh.indexCount, count);
7319
+ else
7320
+ pass.drawIndexedIndirect(cut.args, 0);
7241
7321
  }
7242
7322
  }
7243
7323
  /* Back to the defaults, unconditionally and for the reason `drawMesh` gives at length: this
@@ -7248,7 +7328,7 @@ export class WebGPURenderer {
7248
7328
  }
7249
7329
  if (unlitOrUnfogged) {
7250
7330
  this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
7251
- this.perFrameInts[this.materialField('uFogEnabled')] = 1;
7331
+ this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
7252
7332
  }
7253
7333
  if (!toneMapped) {
7254
7334
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
@@ -8574,6 +8654,23 @@ export class WebGPURenderer {
8574
8654
  f[at('uMotionStrength')] = motionStrength;
8575
8655
  f[at('uMotionMax')] = MOTION_BLUR_MAX_UV;
8576
8656
  f[at('uAoStrength')] = this.frameProjection === null ? 0 : aoStrength;
8657
+ /*
8658
+ * The occlusion read where this frame's jitter put it (`uAoOffset`): measured from the jittered
8659
+ * depth and applied to a picture the jitter has been taken out of. A texel stands at its index
8660
+ * plus a half less the jitter, so the occlusion of a point is a jitter further on. The
8661
+ * reconstruction's y is given upward and negated into the corrected matrix, which this pass's
8662
+ * rows run down from; the temporal resolve's goes in as it is, and arrives as it is.
8663
+ */
8664
+ f[at('uAoOffset')] = this.reconstructing
8665
+ ? this.reconJitter[0] / this.renderWidth
8666
+ : this.temporalJittering
8667
+ ? this.temporalJitterX / this.renderWidth
8668
+ : 0;
8669
+ f[at('uAoOffset') + 1] = this.reconstructing
8670
+ ? -this.reconJitter[1] / this.renderHeight
8671
+ : this.temporalJittering
8672
+ ? this.temporalJitterY / this.renderHeight
8673
+ : 0;
8577
8674
  f[at('uDofStrength')] = dofStrength;
8578
8675
  if (dofStrength > 0 && this.frameProjection !== null) {
8579
8676
  mat4.invert(this.dofInvProjection, this.frameProjection);
@@ -8715,6 +8812,8 @@ export class WebGPURenderer {
8715
8812
  this.reconSizeKey = key;
8716
8813
  for (const dead of [
8717
8814
  ...this.reconHistories,
8815
+ ...this.reconFlickers,
8816
+ ...this.reconSwings,
8718
8817
  this.reconShown,
8719
8818
  this.reconMotion,
8720
8819
  this.reconPreviousDepth,
@@ -8723,6 +8822,10 @@ export class WebGPURenderer {
8723
8822
  }
8724
8823
  this.reconHistories = [null, null];
8725
8824
  this.reconHistoryViews = [null, null];
8825
+ this.reconFlickers = [null, null];
8826
+ this.reconFlickerViews = [null, null];
8827
+ this.reconSwings = [null, null];
8828
+ this.reconSwingViews = [null, null];
8726
8829
  this.reconShown = null;
8727
8830
  this.reconShownView = null;
8728
8831
  this.reconMotion = null;
@@ -8747,6 +8850,22 @@ export class WebGPURenderer {
8747
8850
  });
8748
8851
  this.reconHistories[i] = texture;
8749
8852
  this.reconHistoryViews[i] = texture.createView();
8853
+ const record = device.createTexture({
8854
+ label: `recon.flicker${String(i)}`,
8855
+ size: [outputWidth, outputHeight],
8856
+ format: RECON_HISTORY_FORMAT,
8857
+ usage: HISTORY_USAGE,
8858
+ });
8859
+ this.reconFlickers[i] = record;
8860
+ this.reconFlickerViews[i] = record.createView();
8861
+ const swing = device.createTexture({
8862
+ label: `recon.swing${String(i)}`,
8863
+ size: [outputWidth, outputHeight],
8864
+ format: 'r32float',
8865
+ usage: HISTORY_USAGE,
8866
+ });
8867
+ this.reconSwings[i] = swing;
8868
+ this.reconSwingViews[i] = swing.createView();
8750
8869
  }
8751
8870
  this.reconShown = device.createTexture({
8752
8871
  label: 'recon.shown',
@@ -8822,6 +8941,26 @@ export class WebGPURenderer {
8822
8941
  viewDimension: '2d',
8823
8942
  },
8824
8943
  },
8944
+ sampled(8, 'float'),
8945
+ {
8946
+ binding: 9,
8947
+ visibility: COMPUTE,
8948
+ storageTexture: {
8949
+ access: 'write-only',
8950
+ format: RECON_HISTORY_FORMAT,
8951
+ viewDimension: '2d',
8952
+ },
8953
+ },
8954
+ sampled(10, 'unfilterable-float'),
8955
+ {
8956
+ binding: 11,
8957
+ visibility: COMPUTE,
8958
+ storageTexture: {
8959
+ access: 'write-only',
8960
+ format: 'r32float',
8961
+ viewDimension: '2d',
8962
+ },
8963
+ },
8825
8964
  ],
8826
8965
  });
8827
8966
  const module = device.createShaderModule({ label: 'recon.resolve', code: reconResolveWgsl() });
@@ -8856,7 +8995,7 @@ export class WebGPURenderer {
8856
8995
  if (this.reconShownView === null)
8857
8996
  return;
8858
8997
  const { device } = this.surface;
8859
- const build = (label, history, target) => device.createBindGroup({
8998
+ const build = (label, history, target, records, nextRecords, swings, nextSwings) => device.createBindGroup({
8860
8999
  label,
8861
9000
  layout,
8862
9001
  entries: [
@@ -8868,20 +9007,77 @@ export class WebGPURenderer {
8868
9007
  { binding: 5, resource: this.postSampler },
8869
9008
  { binding: 6, resource: { buffer: params } },
8870
9009
  { binding: 7, resource: target },
9010
+ { binding: 8, resource: records },
9011
+ { binding: 9, resource: nextRecords },
9012
+ { binding: 10, resource: swings },
9013
+ { binding: 11, resource: nextSwings },
8871
9014
  ],
8872
9015
  });
8873
9016
  for (let write = 0; write < 2; write += 1) {
8874
9017
  const read = this.reconHistoryViews[1 - write];
8875
9018
  const target = this.reconHistoryViews[write];
8876
- if (read === null || target === null)
9019
+ const records = this.reconFlickerViews[1 - write];
9020
+ const nextRecords = this.reconFlickerViews[write];
9021
+ const swings = this.reconSwingViews[1 - write];
9022
+ const nextSwings = this.reconSwingViews[write];
9023
+ if (read === null || target === null || records === null || nextRecords === null)
9024
+ continue;
9025
+ if (swings === null || nextSwings === null)
8877
9026
  continue;
8878
- this.reconGroups[write] = build(`recon.resolve${String(write)}`, read, target);
8879
- this.reconSharpenGroups[write] = build(`recon.sharpen${String(write)}`, target, this.reconShownView);
9027
+ this.reconGroups[write] = build(`recon.resolve${String(write)}`, read, target, records, nextRecords, swings, nextSwings);
9028
+ /* The sharpen reads no record, but one layout serves both dispatches: the same pair, so no
9029
+ texture is both read and written. */
9030
+ this.reconSharpenGroups[write] = build(`recon.sharpen${String(write)}`, target, this.reconShownView, records, nextRecords, swings, nextSwings);
8880
9031
  }
8881
9032
  /* The composite's own group, over the resolved picture instead of the scene target. */
8882
9033
  this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
8883
9034
  /* And the scene's, because `ensureComposite` built it before this one existed. */
8884
9035
  this.rebuildRushBindGroup();
9036
+ /* And the bloom's, which blooms the resolved picture: see `rebuildBloomGroups`. */
9037
+ this.rebuildBloomGroups();
9038
+ }
9039
+ /** This frame's jitter and temporal frame, decided where its scene camera is bound. See `bindMeshPass`. */
9040
+ jitterFrame() {
9041
+ this.reconstructing = this.reconstructionWanted && !this.reflectionPassActive;
9042
+ if (this.quality.reconstruction > 0 && this.samples > 1 && !this.reconMultisampleSaid) {
9043
+ this.reconMultisampleSaid = true;
9044
+ console.warn(RECONSTRUCTION_MULTISAMPLE_REFUSAL);
9045
+ }
9046
+ if (this.reconstructing) {
9047
+ /*
9048
+ * The offset `settleReconJitter` chose for the frame at `beginFrame`, applied here and not
9049
+ * advanced here: a frame binding its mesh pass twice is one frame of the sequence.
9050
+ *
9051
+ * **The y offset is negated on the way in, and that is not a preference.** This shifts the
9052
+ * *corrected* matrix, whose y `CLIP_CORRECTION` has already negated so that the negation
9053
+ * every generated vertex stage ends with cancels out — §3 row 56. So an offset added here
9054
+ * arrives on screen with its sign reversed, while x, which nothing negates, arrives as
9055
+ * given. The temporal resolve cannot see this because its shader never reads the offset; the
9056
+ * reconstruction reads it at every texel it unprojects, and a sign error there moves every
9057
+ * sample two jitters away from where the depth says it is.
9058
+ */
9059
+ this.viewProj = jitterProjection(this.jitteredViewProj, this.correctedViewProj, this.reconJitter[0], -this.reconJitter[1], this.renderWidth, this.renderHeight);
9060
+ this.temporalJittering = false;
9061
+ this.temporalHistoryUsable = false;
9062
+ }
9063
+ this.temporalJittering =
9064
+ !this.reconstructing &&
9065
+ this.quality.temporalAa &&
9066
+ this.quality.screenEffects &&
9067
+ !this.reflectionPassActive;
9068
+ if (this.temporalJittering) {
9069
+ /* The scene's size, not the drawing buffer's: the history is a picture of the render. */
9070
+ this.temporalHistoryUsable = this.temporalHistory.openFrame(this.renderWidth, this.renderHeight);
9071
+ const [jx, jy] = jitterOffset(this.temporalHistory.frameIndex);
9072
+ this.temporalJitterX = jx;
9073
+ this.temporalJitterY = jy;
9074
+ /* A jitter is half a *render* texel, which is what the sequence is spread over. */
9075
+ this.viewProj = jitterProjection(this.jitteredViewProj, this.correctedViewProj, jx, jy, this.renderWidth, this.renderHeight);
9076
+ }
9077
+ else if (!this.reconstructing) {
9078
+ this.temporalHistoryUsable = false;
9079
+ this.viewProj = this.correctedViewProj;
9080
+ }
8885
9081
  }
8886
9082
  /**
8887
9083
  * Whether a blended draw recorded now lands after the upscale. See `lateRouting.ts`.
@@ -8898,15 +9094,15 @@ export class WebGPURenderer {
8898
9094
  * otherwise. **Late draws record whatever `quality.frameGraph` says**, since the late replay is
8899
9095
  * the only moment they can land; everything else keeps the path it has.
8900
9096
  */
8901
- recordBlended(reads) {
9097
+ recordBlended(reads, lands = this.drawsLate()) {
8902
9098
  const late = this.latePass;
8903
- if (late !== null && this.drawsLate())
9099
+ if (late !== null && lands)
8904
9100
  return late.take();
8905
9101
  return this.recordDraw(reads, this.currentTarget());
8906
9102
  }
8907
9103
  /** The pipelines a blended draw is built for: the reconstructed picture's while late. */
8908
- blendedPipelines() {
8909
- return this.drawsLate() ? this.latePipelines : this.targetPipelines();
9104
+ blendedPipelines(lands = this.drawsLate()) {
9105
+ return lands ? this.latePipelines : this.targetPipelines();
8910
9106
  }
8911
9107
  /**
8912
9108
  * The blended draws of a reconstructing frame, over the reconstructed picture at the output size
@@ -8925,7 +9121,7 @@ export class WebGPURenderer {
8925
9121
  const width = Math.max(1, this.surface.canvas.width);
8926
9122
  const height = Math.max(1, this.surface.canvas.height);
8927
9123
  /* The order-independent set tests against this depth too, so it is made for either. */
8928
- late.upscale(encoder, depth, this.renderWidth, this.renderHeight, width, height);
9124
+ late.upscale(encoder, depth, this.renderWidth, this.renderHeight, width, height, this.reconJitter[0], this.reconJitter[1]);
8929
9125
  if (this.lateRefractWanted && this.refractSnapshot !== null && this.reconShown !== null) {
8930
9126
  encoder.copyTextureToTexture({ texture: this.reconShown }, { texture: this.refractSnapshot }, [this.refractSnapshot.width, this.refractSnapshot.height]);
8931
9127
  this.refractMips?.record(encoder);
@@ -9060,7 +9256,16 @@ export class WebGPURenderer {
9060
9256
  * OpenGL's depth convention — which is the convention the shader's accessor converts the
9061
9257
  * renderer's reversed buffer into.
9062
9258
  */
9063
- mat4.invert(this.reconInverseViewProj, this.frameRawViewProj);
9259
+ /*
9260
+ * The inverse and the reprojection in double precision, rounded once on the way into the
9261
+ * uniform: the product of two single-precision matrices this far apart in depth is not the
9262
+ * identity for a still camera, and the resolve read the difference as motion.
9263
+ */
9264
+ mat4.invert(this.reconInverse64, this.frameRawViewProj);
9265
+ mat4.multiply(this.reconReprojection64, this.reconPreviousRawViewProj, this.reconInverse64);
9266
+ for (let i = 0; i < 16; i += 1) {
9267
+ this.reconInverseViewProj[i] = this.reconInverse64[i];
9268
+ }
9064
9269
  const f = this.reconFloats;
9065
9270
  const u = this.reconInts;
9066
9271
  u[0] = this.renderWidth;
@@ -9084,6 +9289,17 @@ export class WebGPURenderer {
9084
9289
  f[69] = DEFAULT_DISOCCLUSION.motionScale;
9085
9290
  f[70] = DEFAULT_DISOCCLUSION.normalFloor;
9086
9291
  f[71] = DEFAULT_DISOCCLUSION.normalCeiling;
9292
+ /* The anti-flicker over the reconstruction's own period, its rule made once a period. */
9293
+ const phases = reconJitterPhases(this.renderWidth, outputWidth);
9294
+ if (phases !== this.reconFlickerPhases) {
9295
+ this.reconFlickerPhases = phases;
9296
+ this.reconFlickerRule = flickerRule(phases);
9297
+ }
9298
+ u[72] = this.reconFrameIndex % phases === 0 ? 1 : 0;
9299
+ u[73] = 1;
9300
+ f[74] = this.reconFlickerRule.memory;
9301
+ f[75] = this.reconFlickerRule.width;
9302
+ f.set(this.reconReprojection64, 76);
9087
9303
  device.queue.writeBuffer(params, 0, this.reconStaging);
9088
9304
  const groupsX = Math.ceil(outputWidth / RECON_WORKGROUP);
9089
9305
  const groupsY = Math.ceil(outputHeight / RECON_WORKGROUP);
@@ -10215,6 +10431,7 @@ export class WebGPURenderer {
10215
10431
  command.indexBuffer = null;
10216
10432
  command.indexed = false;
10217
10433
  command.instances = 1;
10434
+ command.indirect = null;
10218
10435
  recordNode(this.arena, VERB_DRAW, reads, writes, at, 0);
10219
10436
  return command;
10220
10437
  }
@@ -10266,7 +10483,10 @@ export class WebGPURenderer {
10266
10483
  }
10267
10484
  if (command.indexed && command.indexBuffer !== null) {
10268
10485
  pass.setIndexBuffer(command.indexBuffer, 'uint32');
10269
- pass.drawIndexed(command.count, command.instances);
10486
+ if (command.indirect !== null)
10487
+ pass.drawIndexedIndirect(command.indirect, 0);
10488
+ else
10489
+ pass.drawIndexed(command.count, command.instances);
10270
10490
  }
10271
10491
  else {
10272
10492
  pass.draw(command.count, command.instances);
@@ -10882,6 +11102,10 @@ export class WebGPURenderer {
10882
11102
  * declares nothing behaves exactly as it did before. Read it beside `visible`, which answers the
10883
11103
  * other half of the same question.
10884
11104
  */
11105
+ /** Whether a world-space box is wholly behind the declared occluders. See `Renderer.occludedBox`. */
11106
+ occludedBox(min, max) {
11107
+ return this.occlusion?.occludedBox(min, max) ?? false;
11108
+ }
10885
11109
  occluded(bounds, model) {
10886
11110
  return this.occlusion?.occluded(bounds, model) ?? false;
10887
11111
  }
@@ -11280,12 +11504,14 @@ export class WebGPURenderer {
11280
11504
  const cssHeight = Math.max(canvas.clientHeight, 1);
11281
11505
  const origin = canvas.getBoundingClientRect();
11282
11506
  /*
11283
- * **CSS pixels to the scene's pixels, not the viewer's.** A viewport is state on the frame's
11284
- * own pass, which draws into the render-size targets, so a box measured in the drawing buffer
11285
- * would be scaled by the reconstruction ratio and land off the side of a smaller attachment.
11507
+ * **CSS pixels to the pixels of the attachment this pass draws into.** Mid-frame that is the
11508
+ * scene's render size, which a reconstruction makes smaller than the drawing buffer; after the
11509
+ * present it is the swap image, the drawing buffer itself. Measured against the scene's size
11510
+ * there, an interface's inset landed two thirds of the way to where it was asked for.
11286
11511
  */
11287
- const scaleX = this.renderWidth / cssWidth;
11288
- const scaleY = this.renderHeight / cssHeight;
11512
+ const target = this.passSize();
11513
+ const scaleX = target[0] / cssWidth;
11514
+ const scaleY = target[1] / cssHeight;
11289
11515
  const left = rect.left - origin.left;
11290
11516
  const top = rect.top - origin.top;
11291
11517
  const w = Math.max(1, Math.round(rect.width * scaleX));
@@ -11312,9 +11538,8 @@ export class WebGPURenderer {
11312
11538
  * edge for the frames it is half-off. That asymmetry is the cost of the clamp, it is
11313
11539
  * recorded in the parity ledger, and the alternative was an exception.
11314
11540
  */
11315
- /* The attachment this pass draws into, which is the scene's size — see `renderWidth`. */
11316
- const targetWidth = this.renderWidth;
11317
- const targetHeight = this.renderHeight;
11541
+ const targetWidth = target[0];
11542
+ const targetHeight = target[1];
11318
11543
  const clampedX = Math.max(0, Math.min(x, targetWidth));
11319
11544
  const clampedY = Math.max(0, Math.min(y, targetHeight));
11320
11545
  const clampedW = Math.max(0, Math.min(x + w, targetWidth) - clampedX);
@@ -11386,10 +11611,29 @@ export class WebGPURenderer {
11386
11611
  const pass = this.pass;
11387
11612
  if (this.surface.lost || pass === null)
11388
11613
  return;
11389
- /* The whole of what this pass draws into, which is the scene's size and not the viewer's. */
11390
- pass.setViewport(0, 0, this.renderWidth, this.renderHeight, 0, 1);
11391
- pass.setScissorRect(0, 0, this.renderWidth, this.renderHeight);
11614
+ /* The whole of what this pass draws into: the scene's size mid-frame, the canvas after. */
11615
+ const [width, height] = this.passSize();
11616
+ pass.setViewport(0, 0, width, height, 0, 1);
11617
+ pass.setScissorRect(0, 0, width, height);
11392
11618
  }
11619
+ /**
11620
+ * The size of the attachment the open pass draws into: the swap image — the drawing buffer —
11621
+ * in the overlay after the present, the scene's render size in the frame's own pass. A tuple
11622
+ * held once rather than a new array a call.
11623
+ */
11624
+ passSize() {
11625
+ const size = this.passSizeOut;
11626
+ if (this.overlayActive) {
11627
+ size[0] = Math.max(1, this.surface.canvas.width);
11628
+ size[1] = Math.max(1, this.surface.canvas.height);
11629
+ }
11630
+ else {
11631
+ size[0] = this.renderWidth;
11632
+ size[1] = this.renderHeight;
11633
+ }
11634
+ return size;
11635
+ }
11636
+ passSizeOut = [0, 0];
11393
11637
  /**
11394
11638
  * Copy a rectangle of the frame into a 2D canvas, pixel for pixel.
11395
11639
  *
@@ -11911,8 +12155,12 @@ export class WebGPURenderer {
11911
12155
  probeView = null;
11912
12156
  probeSize;
11913
12157
  probeMipLayout;
11914
- /** The photometric atlas, allocated on first use. See `setIesProfiles`. */
11915
- iesTexture = null;
12158
+ /** The fixture atlas — cookies over profiles — built on first use. See `fixtureAtlas.ts`. */
12159
+ fixtureTexture = null;
12160
+ fixtureView = null;
12161
+ /** The profiles as packed and the cookie images, kept so either can rebuild the atlas. */
12162
+ iesAtlas = packIesAtlas([]);
12163
+ cookieImages = [];
11916
12164
  /** The scene's DriftLight field, and its two volumes once it is whole. See `createLightField`. */
11917
12165
  lightField = null;
11918
12166
  driftIndex = null;
@@ -11926,10 +12174,7 @@ export class WebGPURenderer {
11926
12174
  /** The frame's exact lights packed for the traced bake. See `gi/bounceLights.ts`. */
11927
12175
  bounceLights = createBounceLights();
11928
12176
  bakeLamps = { data: this.bounceLights, count: 0, falloff: 0 };
11929
- iesView = null;
11930
- cookieTexture = null;
11931
- cookieView = null;
11932
- /** Tiles in the cookie atlas. 0 is the white placeholder and the whole off path. */
12177
+ /** Cookie tiles in the atlas. 0 is none, and the whole off path. */
11933
12178
  cookieTiles = 0;
11934
12179
  /** Rows in that atlas. One until a consumer loads a profile, which is the row of ones. */
11935
12180
  iesRows = 1;
@@ -12376,35 +12621,50 @@ export class WebGPURenderer {
12376
12621
  setSpotCookies(images) {
12377
12622
  if (this.surface.lost)
12378
12623
  return;
12624
+ this.cookieImages = images.slice();
12625
+ this.forgetFixtureAtlas();
12626
+ this.rebuildFlatBindGroup();
12627
+ }
12628
+ /** Let the atlas go, to be built again from what is in hand the next time it is bound. */
12629
+ forgetFixtureAtlas() {
12630
+ this.fixtureTexture?.destroy();
12631
+ this.fixtureTexture = null;
12632
+ this.fixtureView = null;
12633
+ }
12634
+ /**
12635
+ * The fixture atlas's view, built from the profiles and cookies in hand if it is not already: the
12636
+ * profiles' rows at the bottom, the cookies' tiles on top. See `renderer.ts` and `fixtureAtlas.ts`.
12637
+ *
12638
+ * **`rgba16float`, not `rgba32float`**: a 32-bit float texture is not filterable in core WebGPU,
12639
+ * and binding one to a filtering sampler takes the whole bind group with it. Both tables want
12640
+ * linear filtering. `RENDER_ATTACHMENT` because `copyExternalImageToTexture` writes through one.
12641
+ */
12642
+ fixtureAtlasView() {
12643
+ if (this.fixtureView !== null)
12644
+ return this.fixtureView;
12379
12645
  const { device } = this.surface;
12380
- const tiles = images.length;
12381
- const width = tiles === 0 ? 1 : COOKIE_TILE * tiles;
12382
- const height = tiles === 0 ? 1 : COOKIE_TILE;
12383
- if (this.cookieTexture === null || this.cookieTexture.width !== width) {
12384
- this.cookieTexture?.destroy();
12385
- this.cookieTexture = device.createTexture({
12386
- label: 'cookie.atlas',
12387
- size: [width, height],
12388
- format: 'rgba8unorm',
12389
- /* `RENDER_ATTACHMENT` because `copyExternalImageToTexture` writes through one. */
12390
- usage: 0x4 | 0x2 | 0x10,
12391
- });
12392
- this.cookieView = this.cookieTexture.createView();
12393
- }
12394
- if (tiles === 0) {
12395
- /* White, not black: this multiplies a light's colour. */
12396
- device.queue.writeTexture({ texture: this.cookieTexture }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4, rowsPerImage: 1 }, { width: 1, height: 1, depthOrArrayLayers: 1 });
12397
- }
12398
- else {
12399
- for (let tile = 0; tile < tiles; tile++) {
12400
- const source = images[tile];
12401
- if (source === undefined)
12402
- continue;
12403
- device.queue.copyExternalImageToTexture({ source: source }, { texture: this.cookieTexture, origin: { x: tile * COOKIE_TILE, y: 0 } }, { width: COOKIE_TILE, height: COOKIE_TILE });
12404
- }
12646
+ const layout = fixtureAtlasLayout(this.iesAtlas.height, this.cookieImages.length);
12647
+ const texture = device.createTexture({
12648
+ label: 'fixture.atlas',
12649
+ size: [layout.width, layout.height],
12650
+ format: 'rgba16float',
12651
+ usage: 0x4 | 0x2 | 0x10,
12652
+ });
12653
+ device.queue.writeTexture({ texture },
12654
+ /* Converted, because `writeTexture` does not — see `environmentTexels`. */
12655
+ toHalfFloats(fixtureAtlasTexels(layout, this.iesAtlas)), { bytesPerRow: layout.width * 8, rowsPerImage: layout.height }, { width: layout.width, height: layout.height, depthOrArrayLayers: 1 });
12656
+ for (let tile = 0; tile < this.cookieImages.length; tile++) {
12657
+ const source = this.cookieImages[tile];
12658
+ if (source === undefined)
12659
+ continue;
12660
+ device.queue.copyExternalImageToTexture({ source: source }, { texture, origin: { x: tile * COOKIE_TILE, y: 0 } }, { width: COOKIE_TILE, height: COOKIE_TILE });
12405
12661
  }
12406
- this.cookieTiles = tiles;
12407
- this.rebuildFlatBindGroup();
12662
+ this.fixtureTexture = texture;
12663
+ this.fixtureView = texture.createView();
12664
+ this.iesRows = this.iesAtlas.height;
12665
+ this.iesPlanes = this.iesAtlas.planes;
12666
+ this.cookieTiles = this.cookieImages.length;
12667
+ return this.fixtureView;
12408
12668
  }
12409
12669
  /**
12410
12670
  * Sum a scene's many fixed lights into a DriftLight field. The same contract as
@@ -12416,6 +12676,19 @@ export class WebGPURenderer {
12416
12676
  this.lightField = field;
12417
12677
  return field;
12418
12678
  }
12679
+ /**
12680
+ * Hand a world's DriftLight volume to the renderer: every fixed light of a city, summed offline by
12681
+ * `bakeDenseField`, standing in past the frame's exact choice. Replaces any field made before, of
12682
+ * either kind; the scene calls `field.follow(buffer.complete, ...)` each frame as it would for
12683
+ * `createLightField`, and marks the lights the volume summed `inLightField` so none is counted
12684
+ * twice. See `driftLight/worldLightField.ts`.
12685
+ */
12686
+ createWorldLightField(volume, options = {}) {
12687
+ this.forgetLightFieldVolumes();
12688
+ const field = new WorldLightField(volume, options);
12689
+ this.lightField = field;
12690
+ return field;
12691
+ }
12419
12692
  /** Let go of the field and its volumes; the scene's lights are all exact again. */
12420
12693
  disposeLightField() {
12421
12694
  this.forgetLightFieldVolumes();
@@ -12441,13 +12714,13 @@ export class WebGPURenderer {
12441
12714
  */
12442
12715
  uploadLightField() {
12443
12716
  const field = this.lightField;
12444
- if (field === null || this.driftAtlas !== null || !field.ready || field.layout.count === 0) {
12717
+ if (field === null || this.driftAtlas !== null || !field.ready || field.empty) {
12445
12718
  return;
12446
12719
  }
12447
12720
  if (this.surface.lost)
12448
12721
  return;
12449
12722
  const { device } = this.surface;
12450
- const [ix, iy, iz] = field.layout.dims;
12723
+ const [ix, iy, iz] = field.indexDims;
12451
12724
  const index = device.createTexture({
12452
12725
  label: 'driftLight.index',
12453
12726
  size: [ix, iy, iz],
@@ -12455,7 +12728,7 @@ export class WebGPURenderer {
12455
12728
  format: 'r32uint',
12456
12729
  usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
12457
12730
  });
12458
- device.queue.writeTexture({ texture: index }, field.layout.index, { bytesPerRow: ix * 4, rowsPerImage: iy }, [ix, iy, iz]);
12731
+ device.queue.writeTexture({ texture: index }, field.index, { bytesPerRow: ix * 4, rowsPerImage: iy }, [ix, iy, iz]);
12459
12732
  const [aw, ah, ad] = field.atlasSize;
12460
12733
  const atlas = device.createTexture({
12461
12734
  label: 'driftLight.atlas',
@@ -12480,41 +12753,8 @@ export class WebGPURenderer {
12480
12753
  setIesProfiles(profiles) {
12481
12754
  if (this.surface.lost)
12482
12755
  return;
12483
- const { device } = this.surface;
12484
- const atlas = packIesAtlas(profiles);
12485
- /*
12486
- * Reallocated when the row count changes, because a texture's size is immutable. A set of the
12487
- * same size overwrites in place, which is the common case of a consumer swapping one fixture.
12488
- */
12489
- if (this.iesTexture === null || this.iesTexture.height !== atlas.height) {
12490
- this.iesTexture?.destroy();
12491
- this.iesTexture = device.createTexture({
12492
- label: 'ies.atlas',
12493
- size: [atlas.width, atlas.height],
12494
- /*
12495
- * **`r16float`, not `r32float`, and that is a filtering constraint rather than a size
12496
- * choice.** A 32-bit float texture is not filterable in core WebGPU — it needs the
12497
- * optional `float32-filterable` feature — and binding one to a filtering sampler is a
12498
- * validation failure that takes the whole bind group with it. This row *wants* linear
12499
- * filtering: it is a curve sampled between two angles, which is the entire reason it is
12500
- * not `NEAREST` like the froxel table beside it.
12501
- *
12502
- * Half precision holds a normalised intensity to about three decimal digits, which is far
12503
- * finer than the eight-bit frame it ends up in. **What would make it wrong** is a profile
12504
- * carrying absolute candela rather than a normalised curve, which would run out of range —
12505
- * and `packIesAtlas` normalises for its own reasons.
12506
- */
12507
- format: 'r16float',
12508
- usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
12509
- });
12510
- this.iesView = this.iesTexture.createView();
12511
- }
12512
- device.queue.writeTexture({ texture: this.iesTexture },
12513
- /* Converted, because `writeTexture` does not — see `environmentTexels` for what handing it
12514
- the wrong width silently produces. */
12515
- toHalfFloats(atlas.data), { bytesPerRow: atlas.width * 2, rowsPerImage: atlas.height }, { width: atlas.width, height: atlas.height, depthOrArrayLayers: 1 });
12516
- this.iesRows = atlas.height;
12517
- this.iesPlanes = atlas.planes;
12756
+ this.iesAtlas = packIesAtlas(profiles);
12757
+ this.forgetFixtureAtlas();
12518
12758
  this.rebuildFlatBindGroup();
12519
12759
  }
12520
12760
  /** Each level from the one above it, six faces at a time. See `probePass.ts`. */
@@ -12959,7 +13199,10 @@ export class WebGPURenderer {
12959
13199
  * An empty list is a real answer for a frame too small to hold one, and `runBloom` reads it
12960
13200
  * as no bloom rather than as a target it should have had.
12961
13201
  */
12962
- this.bloomLevels = bloomLevelSizes(width, height).map((size, index) => {
13202
+ /* The resolved picture's size while reconstructing, as the order-independent pair above is. */
13203
+ this.bloomSourceWidth = oitSize[0];
13204
+ this.bloomSourceHeight = oitSize[1];
13205
+ this.bloomLevels = bloomLevelSizes(oitSize[0], oitSize[1]).map((size, index) => {
12963
13206
  const texture = device.createTexture({
12964
13207
  label: `post.bloom${index}`,
12965
13208
  size: [size.width, size.height],
@@ -13124,13 +13367,30 @@ export class WebGPURenderer {
13124
13367
  /* The volume's group holds the depth snapshot, which is one of the targets just replaced.
13125
13368
  Keeping the old group would clamp every beam against a destroyed texture. */
13126
13369
  this.lightVolumeBindGroup = this.buildLightVolumeBindGroup();
13127
- /*
13128
- * One group per source the chain reads, in the order `runBloom` indexes them: the scene
13129
- * first, then every level. The prefilter reads the scene; the downsample into level `i`
13130
- * reads level `i - 1`, which is group `i`; the upsample into level `i - 1` reads level
13131
- * `i`, which is group `i + 1`.
13132
- */
13133
- this.bloomGroups = [this.sceneColorView, ...this.bloomLevels.map((level) => level.view)].map((view, index) => device.createBindGroup({
13370
+ this.rebuildBloomGroups();
13371
+ this.writeBloomBlocks();
13372
+ }
13373
+ /**
13374
+ * One group per source the bloom chain reads, in the order `runBloom` indexes them: the picture
13375
+ * first, then every level. The prefilter reads the picture; the downsample into level `i` reads
13376
+ * level `i - 1`, which is group `i`; the upsample into level `i - 1` reads level `i`, which is
13377
+ * group `i + 1`.
13378
+ *
13379
+ * **While reconstructing the picture is the resolved one**, which `ensureReconstruction` makes
13380
+ * after this runs and rebuilds these over. The scene target is the jittered render then: a bright
13381
+ * thing smaller than a render texel is caught in some phases and missed in others, and its halo,
13382
+ * wide and over everything round it, flashed with it — a night street's panels breathing a few
13383
+ * levels of luma frame to frame. Built at the drawing buffer's size, which `ensureComposite`
13384
+ * already sized the pyramid for.
13385
+ */
13386
+ rebuildBloomGroups() {
13387
+ const picture = this.reconstructionWanted && this.reconShownView !== null
13388
+ ? this.reconShownView
13389
+ : this.sceneColorView;
13390
+ if (picture === null)
13391
+ return;
13392
+ const { device } = this.surface;
13393
+ this.bloomGroups = [picture, ...this.bloomLevels.map((level) => level.view)].map((view, index) => device.createBindGroup({
13134
13394
  label: `post.bloomGroup${index}`,
13135
13395
  layout: this.bloomLayout,
13136
13396
  entries: [
@@ -13138,10 +13398,10 @@ export class WebGPURenderer {
13138
13398
  binding: BLOOM_UNIFORMS,
13139
13399
  resource: { buffer: this.bloomUniforms, size: BLOOM_STAGE_SIZE },
13140
13400
  },
13141
- ...entry(BLOOM_TEXTURES.uSource, view),
13401
+ { binding: BLOOM_TEXTURES.uSource.texture, resource: view },
13402
+ { binding: BLOOM_TEXTURES.uSource.sampler, resource: this.postSampler },
13142
13403
  ],
13143
13404
  }));
13144
- this.writeBloomBlocks();
13145
13405
  }
13146
13406
  /**
13147
13407
  * Every bloom stage's uniform block, written once per size rather than once per frame.
@@ -13158,12 +13418,12 @@ export class WebGPURenderer {
13158
13418
  if (levels.length === 0)
13159
13419
  return;
13160
13420
  const slot = (index) => (index * BLOOM_SLOT) / 4;
13161
- /* The prefilter reads the *scene*, so its texel is the frame's rather than a level's.
13421
+ /* The prefilter reads the *picture*, so its texel is the picture's rather than a level's.
13162
13422
  `uTexel`, not `uTexelSize`, and the block carries no strength at all: the composite
13163
13423
  applies that when it adds the result back. Both were written wrong here first. */
13164
13424
  const prefilter = (name) => this.postField(BLOOM_PREFILTER_FIELDS, name);
13165
- f[slot(0) + prefilter('uTexel')] = 1 / Math.max(1, this.frameWidth);
13166
- f[slot(0) + prefilter('uTexel') + 1] = 1 / Math.max(1, this.frameHeight);
13425
+ f[slot(0) + prefilter('uTexel')] = 1 / Math.max(1, this.bloomSourceWidth);
13426
+ f[slot(0) + prefilter('uTexel') + 1] = 1 / Math.max(1, this.bloomSourceHeight);
13167
13427
  /* In scene units, which is why an HDR target is what makes a threshold above 1 mean
13168
13428
  anything: against a clamped buffer nothing is ever brighter than white. */
13169
13429
  f[slot(0) + prefilter('uThreshold')] = this.bloomThresholdSet ?? this.quality.bloomThreshold;
@@ -13248,6 +13508,7 @@ export class WebGPURenderer {
13248
13508
  this.fieldComposer = null;
13249
13509
  this.driftIndex?.destroy();
13250
13510
  this.driftAtlas?.destroy();
13511
+ this.forgetFixtureAtlas();
13251
13512
  this.sunTint?.texture.destroy();
13252
13513
  this.sunTint = null;
13253
13514
  this.glassTint?.dispose();