@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
@@ -2,11 +2,14 @@ import { mat4 } from 'gl-matrix';
2
2
  import type { ProbeBakeOptions } from '../../reflectionProbe.ts';
3
3
  import { probeFaceRange } from '../../reflectionProbe.ts';
4
4
  import {
5
+ TEMPORAL_FLICKER,
5
6
  TEMPORAL_HISTORY_BLEND,
6
7
  TemporalHistory,
8
+ flickerRule,
7
9
  jitterOffset,
8
10
  jitterProjection,
9
11
  } from '../../temporalAa.ts';
12
+ import type { FlickerRule } from '../../temporalAa.ts';
10
13
  import { OIT_MULTISAMPLE_REFUSAL } from '../../orderIndependent.ts';
11
14
  import { TranslucentQueue } from '../../translucentQueue.ts';
12
15
  import { DEPTH_CLEAR, MAX_DEPTH_LAYER, REVERSED_DEPTH } from '../../depthConvention.ts';
@@ -103,6 +106,8 @@ import { ProbeSweeps } from '../../probeSweeps.ts';
103
106
  import { ggxMaxLevelFor, irradianceLevelFor, octahedralEdgeFor } from '../../prefilterEnvMap.ts';
104
107
  import { environmentTexels } from './environmentTexels.ts';
105
108
  import { packIesAtlas } from '../../iesProfile.ts';
109
+ import { fixtureAtlasLayout, fixtureAtlasTexels } from '../../fixtureAtlas.ts';
110
+ import { resolveSurfaceScene } from '../../surfaceScene.ts';
106
111
  import { COOKIE_TILE } from '../../lightBudget.ts';
107
112
  import { FULL_LIGHT_BUDGET } from '../../uniformVectorBudget.ts';
108
113
  import { toHalfFloats } from '../../halfFloat.ts';
@@ -485,6 +490,7 @@ import {
485
490
  import { aimReflection, createGpuReflection, type GpuReflection } from './reflectionPass.ts';
486
491
  import { FrameBudget } from '../budget.ts';
487
492
  import { MaterialChanges, ownsMaterial } from '../materialChanges.ts';
493
+ import { FOG_RECEDE, fogModeOf } from '../drawFog.ts';
488
494
  import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.ts';
489
495
  import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.ts';
490
496
  import { DistanceFieldScene } from '../../gi/fieldScene.ts';
@@ -540,6 +546,9 @@ import {
540
546
  import { createFrustum, frustumFromViewProjection } from '../../../math/frustum.ts';
541
547
  import type { Frustum } from '../../../math/frustum.ts';
542
548
  import type { Bounds } from '../../../math/bounds.ts';
549
+ import { batchBoxVisible, cullsInstances } from '../../instanceCull.ts';
550
+ import { InstanceCullPass, MAX_CULL_SLOTS } from './instanceCullPass.ts';
551
+ import type { CullSlot } from './instanceCullPass.ts';
543
552
  import { OcclusionBuffer } from '../../occlusion.ts';
544
553
  import { boundsVisible } from '../../visibility.ts';
545
554
  import { createPassRegistry, drainRegistry, passAt, registerIn, unregisterIn } from '../../pass.ts';
@@ -565,11 +574,15 @@ import type {
565
574
  } from '../../pass.ts';
566
575
  import type { Arena, FlushSchedule, ScheduledPass } from '../../frame/index.ts';
567
576
  import { GpuInstancedBatch } from './instanced.ts';
568
- import type { MeshInstances } from '../../instances.ts';
577
+ import type { InstancedOptions, MeshInstances } from '../../instances.ts';
569
578
  import { cutoutOf } from '../../cutoutCaster.ts';
570
579
  import type { CutoutCaster } from '../../cutoutCaster.ts';
571
580
  import { LightField } from '../../driftLight/lightField.ts';
572
581
  import type { LightFieldOptions, LightFieldSource } from '../../driftLight/lightField.ts';
582
+ import type { DriftLightVolumes } from '../../driftLight/presence.ts';
583
+ import type { DenseLightVolume } from '../../driftLight/denseField.ts';
584
+ import { WorldLightField } from '../../driftLight/worldLightField.ts';
585
+ import type { WorldLightFieldOptions } from '../../driftLight/worldLightField.ts';
573
586
  import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.ts';
574
587
 
575
588
  /**
@@ -1899,6 +1912,12 @@ export class WebGPURenderer implements RendererApi {
1899
1912
  * what the layout already declares.
1900
1913
  */
1901
1914
  private bloomGroups: GPUBindGroup[] = [];
1915
+ /**
1916
+ * The picture the bloom reads, and its size: the scene target, or while reconstructing the
1917
+ * resolved picture at the drawing buffer's size. See `rebuildBloomGroups`.
1918
+ */
1919
+ private bloomSourceWidth = 1;
1920
+ private bloomSourceHeight = 1;
1902
1921
  private frameWidth = 1;
1903
1922
  private frameHeight = 1;
1904
1923
  /**
@@ -2059,6 +2078,18 @@ export class WebGPURenderer implements RendererApi {
2059
2078
  */
2060
2079
  private reconHistories: (GPUTexture | null)[] = [null, null];
2061
2080
  private reconHistoryViews: (GPUTextureView | null)[] = [null, null];
2081
+ /**
2082
+ * Each output pixel's flicker record, a pair swapped with the histories: the temporal resolve's
2083
+ * anti-flicker, over the reconstruction's own period (`recon/resolve.ts`).
2084
+ */
2085
+ private reconFlickers: (GPUTexture | null)[] = [null, null];
2086
+ private reconFlickerViews: (GPUTextureView | null)[] = [null, null];
2087
+ /** Each output pixel's held depth swing, a pair of its own: see `recon/resolve.ts`. */
2088
+ private reconSwings: (GPUTexture | null)[] = [null, null];
2089
+ private reconSwingViews: (GPUTextureView | null)[] = [null, null];
2090
+ /** The anti-flicker's rule for the period last asked for, kept so a frame makes none. */
2091
+ private reconFlickerPhases = 0;
2092
+ private reconFlickerRule: FlickerRule = TEMPORAL_FLICKER;
2062
2093
  private reconWrite = 0;
2063
2094
  private reconHasHistory = false;
2064
2095
  private reconShown: GPUTexture | null = null;
@@ -2092,6 +2123,9 @@ export class WebGPURenderer implements RendererApi {
2092
2123
  /** Last frame's raw view-projection and its inverse, and the two eyes. */
2093
2124
  private readonly reconPreviousRawViewProj = mat4.create();
2094
2125
  private readonly reconInverseViewProj = mat4.create();
2126
+ /** The inverse and the reprojection, multiplied in double precision. See `runReconstruction`. */
2127
+ private readonly reconInverse64 = new Float64Array(16);
2128
+ private readonly reconReprojection64 = new Float64Array(16);
2095
2129
  private readonly reconPreviousInverseViewProj = mat4.create();
2096
2130
  private readonly reconEye = new Float32Array(3);
2097
2131
  private readonly reconPreviousEye = new Float32Array(3);
@@ -2243,6 +2277,8 @@ export class WebGPURenderer implements RendererApi {
2243
2277
  /** Said once for the whole renderer. See `refuseOitMultisampled`. */
2244
2278
  private oitMultisampleRefused = false;
2245
2279
  private oitReplaying = false;
2280
+ /** `uSurfaceScene`, resolved once a pass. See `surfaceScene.ts`. */
2281
+ private readonly surfaceScene = new Float32Array(4);
2246
2282
  /** Which buffer the replay is filling, which the pipeline key must carry. */
2247
2283
  private oitMode: OitTarget = 'none';
2248
2284
  /**
@@ -2534,6 +2570,8 @@ export class WebGPURenderer implements RendererApi {
2534
2570
  }
2535
2571
 
2536
2572
  private flushRings(): void {
2573
+ /* Culls first: a submit that follows may carry a draw reading what they write. */
2574
+ this.instanceCulls?.flush();
2537
2575
  for (const ring of this.rings()) ring.flush();
2538
2576
  }
2539
2577
 
@@ -2771,6 +2809,14 @@ export class WebGPURenderer implements RendererApi {
2771
2809
  return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
2772
2810
  }
2773
2811
 
2812
+ /** Several images of one size as an array's layers. See `RendererApi.createSurfaceTextureArray`. */
2813
+ createSurfaceTextureArray(
2814
+ sources: readonly TexImageSource[],
2815
+ options: SurfaceTextureOptions = {},
2816
+ ): GpuSurfaceTexture {
2817
+ return new GpuSurfaceTexture(this.surface.device, this.pipelines, sources, options);
2818
+ }
2819
+
2774
2820
  /**
2775
2821
  * GPU textures an `updateSurfaceTexture` replaced, destroyed when the next frame begins.
2776
2822
  *
@@ -3049,6 +3095,19 @@ export class WebGPURenderer implements RendererApi {
3049
3095
  });
3050
3096
  }
3051
3097
 
3098
+ /**
3099
+ * How much of the surfaces drawn next survives a screen-door dither. See `renderer.ts`.
3100
+ *
3101
+ * Written straight into the scratch block rather than through `material()`'s callback, because
3102
+ * this one is called per crossfading region per frame and a closure per call is an allocation.
3103
+ */
3104
+ setDitherFade(amount: number): void {
3105
+ if (this.surface.lost) return;
3106
+ /* The second half of `uWriteMode`, which the order-independent weight shares. */
3107
+ this.perFrameFloats[this.materialField('uWriteMode') + 1] = Math.min(1, Math.max(-1, amount));
3108
+ this.materials.dirty();
3109
+ }
3110
+
3052
3111
  /**
3053
3112
  * How strong the microscopic relief on the surfaces drawn next is, and how coarse.
3054
3113
  *
@@ -4972,6 +5031,8 @@ export class WebGPURenderer implements RendererApi {
4972
5031
  private readonly waterLayout: GPUBindGroupLayout;
4973
5032
  /** The one-pixel stand-in, kept so the water bind group can be rebuilt without one. */
4974
5033
  private readonly blankView: GPUTextureView;
5034
+ /** The blank texel as a one-layer array: the stand-in every surface-texture binding takes. */
5035
+ private readonly blankSurfaceView: GPUTextureView;
4975
5036
  private readonly blankSampler: GPUSampler;
4976
5037
  /**
4977
5038
  * A slot per body, not one buffer rewritten before each draw.
@@ -6002,6 +6063,9 @@ export class WebGPURenderer implements RendererApi {
6002
6063
  * `uStaticShadowMap` rather than by anything here changing shape.
6003
6064
  */
6004
6065
  const blankView = blank.createView();
6066
+ /* The same texel as a one-layer array, for the four material maps, which are arrays now. */
6067
+ const blankSurfaceView = blank.createView({ dimension: '2d-array' });
6068
+ this.blankSurfaceView = blankSurfaceView;
6005
6069
  const blankSampler = device.createSampler({ label: 'flat.sampler' });
6006
6070
 
6007
6071
  /*
@@ -6188,7 +6252,7 @@ export class WebGPURenderer implements RendererApi {
6188
6252
  if (name === 'uNormalMap') {
6189
6253
  const map = this.normalMap;
6190
6254
  return map === null
6191
- ? { view: blankView, sampler: blankSampler }
6255
+ ? { view: blankSurfaceView, sampler: blankSampler }
6192
6256
  : { view: map.view, sampler: map.sampler };
6193
6257
  }
6194
6258
  /* The caller's ORM map, or the stand-in, for the reason `uNormalMap` gives just above. */
@@ -6196,21 +6260,26 @@ export class WebGPURenderer implements RendererApi {
6196
6260
  if (name === 'uEmissiveMap') {
6197
6261
  const map = this.emissiveMap;
6198
6262
  return map === null
6199
- ? { view: blankView, sampler: blankSampler }
6263
+ ? { view: blankSurfaceView, sampler: blankSampler }
6200
6264
  : { view: map.view, sampler: map.sampler };
6201
6265
  }
6202
6266
  if (name === 'uOrmMap') {
6203
6267
  const map = this.ormMap;
6204
6268
  return map === null
6205
- ? { view: blankView, sampler: blankSampler }
6269
+ ? { view: blankSurfaceView, sampler: blankSampler }
6206
6270
  : { view: map.view, sampler: map.sampler };
6207
6271
  }
6208
6272
  if (name === 'uAlbedo') {
6209
6273
  const albedo = this.albedo;
6210
6274
  return albedo === null
6211
- ? { view: blankView, sampler: blankSampler }
6275
+ ? { view: blankSurfaceView, sampler: blankSampler }
6212
6276
  : { view: albedo.view, sampler: albedo.sampler };
6213
6277
  }
6278
+ /* The albedo's effects table, or a one-texel stand-in the shader's size test reads as none.
6279
+ The albedo is the group's key, so its table needs no key of its own. */
6280
+ if (name === 'uSurfaceEffects') {
6281
+ return { view: this.albedo?.effectsView ?? blankView, sampler: blankSampler };
6282
+ }
6214
6283
  /*
6215
6284
  * The room, once a bake has filled it. Unbaked reads as the blank cube: nothing to mirror.
6216
6285
  *
@@ -6242,18 +6311,8 @@ export class WebGPURenderer implements RendererApi {
6242
6311
  * this one is a curve sampled between two angles rather than data addressed by index, so
6243
6312
  * interpolation across a row is the point. `postSampler` is linear and clamped.
6244
6313
  */
6245
- if (name === 'uIesAtlas') {
6246
- return { view: this.iesView ?? blankView, sampler: iesSampler };
6247
- }
6248
- /*
6249
- * The cookie atlas, or the single white texel a scene with no cookie falls back to.
6250
- *
6251
- * A filtering sampler, like the photometric atlas and unlike the froxel table: a cookie is
6252
- * an image and its whole point is that it is soft at the edges. The shader's own half-texel
6253
- * inset is what keeps one tile out of the next; a clamp cannot, the seam being interior.
6254
- */
6255
- if (name === 'uCookieAtlas') {
6256
- return { view: this.cookieView ?? blankView, sampler: iesSampler };
6314
+ if (name === 'uFixtureAtlas') {
6315
+ return { view: this.fixtureAtlasView(), sampler: iesSampler };
6257
6316
  }
6258
6317
  /*
6259
6318
  * DriftLight's brick index, read by `texelFetch` alone, so it shares the shadow group's
@@ -7106,6 +7165,8 @@ export class WebGPURenderer implements RendererApi {
7106
7165
  if (!attribute.optional) continue;
7107
7166
  present[String(attribute.name)] = key.includes(`:${String(attribute.name)}`);
7108
7167
  }
7168
+ /* Not a layout entry of its own — it widens `uvs` — so read from the key beside them. */
7169
+ present['layers'] = key.includes(':layers');
7109
7170
  return present;
7110
7171
  }
7111
7172
 
@@ -7118,6 +7179,7 @@ export class WebGPURenderer implements RendererApi {
7118
7179
  * uncaught exceptions a second in somebody's error reporting.
7119
7180
  */
7120
7181
  beginFrame(clearColor: Vec3): void {
7182
+ this.cullFrame += 1;
7121
7183
  /* Whatever an update replaced during the last frame; nothing recorded can read it now. */
7122
7184
  for (const texture of this.retiredTextures) texture.destroy();
7123
7185
  this.retiredTextures.length = 0;
@@ -7380,6 +7442,15 @@ export class WebGPURenderer implements RendererApi {
7380
7442
  present[String(attribute.name)] = supplied;
7381
7443
  key += supplied ? `:${String(attribute.name)}` : '';
7382
7444
  }
7445
+ /*
7446
+ * Texture-array layers widen the UV field to three floats (`LAYERED_UVS`), which is a different
7447
+ * vertex layout, so the key has to say so or a layered and a plain mesh would share a pipeline
7448
+ * and one of them would read every field after the UVs from the wrong offset.
7449
+ */
7450
+ if (data.layers !== undefined) {
7451
+ present['layers'] = true;
7452
+ key += ':layers';
7453
+ }
7383
7454
  const fullKey = `${this.variant}|${key}`;
7384
7455
  /*
7385
7456
  * **For every target this mesh can land on, which is two when the overlay is its own.**
@@ -7605,62 +7676,15 @@ export class WebGPURenderer implements RendererApi {
7605
7676
  * Its sequence is its own: `reconJitterPhases` takes eight positions for every output pixel a
7606
7677
  * render pixel covers, where the temporal resolve's eight are enough at one to one.
7607
7678
  */
7608
- this.reconstructing = this.reconstructionWanted && !this.reflectionPassActive;
7609
- if (this.quality.reconstruction > 0 && this.samples > 1 && !this.reconMultisampleSaid) {
7610
- this.reconMultisampleSaid = true;
7611
- console.warn(RECONSTRUCTION_MULTISAMPLE_REFUSAL);
7612
- }
7613
- if (this.reconstructing) {
7614
- /*
7615
- * The offset `settleReconJitter` chose for the frame at `beginFrame`, applied here and not
7616
- * advanced here: a frame binding its mesh pass twice is one frame of the sequence.
7617
- *
7618
- * **The y offset is negated on the way in, and that is not a preference.** This shifts the
7619
- * *corrected* matrix, whose y `CLIP_CORRECTION` has already negated so that the negation
7620
- * every generated vertex stage ends with cancels out — §3 row 56. So an offset added here
7621
- * arrives on screen with its sign reversed, while x, which nothing negates, arrives as
7622
- * given. The temporal resolve cannot see this because its shader never reads the offset; the
7623
- * reconstruction reads it at every texel it unprojects, and a sign error there moves every
7624
- * sample two jitters away from where the depth says it is.
7625
- */
7626
- this.viewProj = jitterProjection(
7627
- this.jitteredViewProj,
7628
- this.correctedViewProj,
7629
- this.reconJitter[0] as number,
7630
- -(this.reconJitter[1] as number),
7631
- this.renderWidth,
7632
- this.renderHeight,
7633
- ) as Float32Array;
7634
- this.temporalJittering = false;
7635
- this.temporalHistoryUsable = false;
7636
- }
7637
- this.temporalJittering =
7638
- !this.reconstructing &&
7639
- this.quality.temporalAa &&
7640
- this.quality.screenEffects &&
7641
- !this.reflectionPassActive;
7642
- if (this.temporalJittering) {
7643
- /* The scene's size, not the drawing buffer's: the history is a picture of the render. */
7644
- this.temporalHistoryUsable = this.temporalHistory.openFrame(
7645
- this.renderWidth,
7646
- this.renderHeight,
7647
- );
7648
- const [jx, jy] = jitterOffset(this.temporalHistory.frameIndex);
7649
- this.temporalJitterX = jx;
7650
- this.temporalJitterY = jy;
7651
- /* A jitter is half a *render* texel, which is what the sequence is spread over. */
7652
- this.viewProj = jitterProjection(
7653
- this.jitteredViewProj,
7654
- this.correctedViewProj,
7655
- jx,
7656
- jy,
7657
- this.renderWidth,
7658
- this.renderHeight,
7659
- ) as Float32Array;
7660
- } else if (!this.reconstructing) {
7661
- this.temporalHistoryUsable = false;
7662
- this.viewProj = this.correctedViewProj;
7663
- }
7679
+ /*
7680
+ * **After the present there is no jitter to add and no temporal frame to open.** A mesh pass
7681
+ * bound then is the interface's, drawn over a picture already resolved: jittered, a map and every
7682
+ * edge of it moved by a fraction of a pixel each frame with nothing to resolve it; and opening a
7683
+ * second temporal frame stepped the sequence twice a frame, so the world saw every other
7684
+ * position and the anti-flicker's period never started.
7685
+ */
7686
+ if (this.framePresented) this.viewProj = this.correctedViewProj;
7687
+ else this.jitterFrame();
7664
7688
  /*
7665
7689
  * The frustum, once a frame, and from the **uncorrected** matrix.
7666
7690
  *
@@ -7760,8 +7784,14 @@ export class WebGPURenderer implements RendererApi {
7760
7784
  * frame either way, because an unwritten one is four zeroes and `dot(p, 0) < 0` is false,
7761
7785
  * so the term would silently pass everything the first time somebody enabled it.
7762
7786
  */
7763
- i[at('uClipEnabled')] = this.reflectionPassActive ? 1 : 0;
7764
- f.set(this.reflection?.clipPlane ?? NO_CLIP_PLANE, at('uClipPlane'));
7787
+ /* The mirror's plane in a reflection pass and zero, which clips nothing, in every other: the lit
7788
+ stage has no flag beside the plane. See `uClipPlane` in `flat/preamble.ts`. */
7789
+ f.set(
7790
+ this.reflectionPassActive ? (this.reflection?.clipPlane ?? NO_CLIP_PLANE) : NO_CLIP_PLANE,
7791
+ at('uClipPlane'),
7792
+ );
7793
+ resolveSurfaceScene(env, this.surfaceScene);
7794
+ f.set(this.surfaceScene, at('uSurfaceScene'));
7765
7795
 
7766
7796
  /*
7767
7797
  * The material and grading terms, matching `renderer.ts` value for value.
@@ -7782,6 +7812,9 @@ export class WebGPURenderer implements RendererApi {
7782
7812
  */
7783
7813
  f[at('uGrain')] = 1;
7784
7814
  f[at('uReflectivity')] = 0;
7815
+ /* No weighting and no dither until a pass or a crossfade asks. See `setDitherFade`. */
7816
+ f[at('uWriteMode')] = 0;
7817
+ f[at('uWriteMode') + 1] = 0;
7785
7818
  /* 1, not 0: a multiplier's identity is one. See `setEnvironmentGain`. */
7786
7819
  f[at('uEnvironmentGain')] = 1;
7787
7820
  /*
@@ -8341,7 +8374,7 @@ export class WebGPURenderer implements RendererApi {
8341
8374
  * **Recorded rather than drawn**, when the effect is on and this is not already the replay.
8342
8375
  * Everything is copied — see `TranslucentQueue`, and the scratch matrix that made it necessary.
8343
8376
  */
8344
- if (this.oitActive && !this.oitReplaying) {
8377
+ if (this.oitActive && !this.oitReplaying && options.additive !== true) {
8345
8378
  this.translucentQueue.record(mesh, model, opacity, options);
8346
8379
  return;
8347
8380
  }
@@ -8404,7 +8437,10 @@ export class WebGPURenderer implements RendererApi {
8404
8437
  }
8405
8438
 
8406
8439
  /* A blended draw landing after the upscale is drawn into an unjittered picture. */
8407
- const late = blend && (this.drawsLate() || (this.oitReplaying && this.reconstructing));
8440
+ /* Unless it moves with what it lies on, which the resolve reconstructs it with. See
8441
+ `TranslucentMeshOptions.reconstructed`. */
8442
+ const lands = blend && options.reconstructed !== true && this.drawsLate();
8443
+ const late = lands || (blend && this.oitReplaying && this.reconstructing);
8408
8444
  this.perDraw.writeFloats(
8409
8445
  slot,
8410
8446
  FLAT_VERT_FIELDS.uViewProj.offset,
@@ -8443,17 +8479,18 @@ export class WebGPURenderer implements RendererApi {
8443
8479
  this.perFrameFloats[this.materialField('uOpacity')] = opacity;
8444
8480
  /* Material state exactly like `uOpacity`, dirtied for the accumulation pass and restored
8445
8481
  below — the revealage pass draws with it at zero and its colour multiplied away. */
8446
- this.perFrameFloats[this.materialField('uOitWeighted')] = this.oitMode === 'accum' ? 1 : 0;
8482
+ this.perFrameFloats[this.materialField('uWriteMode')] = this.oitMode === 'accum' ? 1 : 0;
8447
8483
  }
8448
8484
  /* Same shape as `dimmed`: a slot is only worth taking when either differs from the default
8449
8485
  `bindMeshPass` already wrote, and both are put back together once the draw is submitted. */
8450
8486
  const lit = options.lit ?? true;
8451
- const fog = options.fog ?? true;
8487
+ /* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
8488
+ const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
8452
8489
  const toneMapped = options.toneMapped ?? true;
8453
- const unlitOrUnfogged = !lit || !fog;
8490
+ const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
8454
8491
  if (unlitOrUnfogged) {
8455
8492
  if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
8456
- if (!fog) this.perFrameInts[this.materialField('uFogEnabled')] = 0;
8493
+ this.perFrameInts[this.materialField('uFogEnabled')] = fog;
8457
8494
  }
8458
8495
  /* `1` is sRGB alone: the conversion without the curve. `Math.min` rather than a literal, so
8459
8496
  a renderer asked for `none` stays at none. See `TranslucentMeshOptions.toneMapped`. */
@@ -8498,8 +8535,10 @@ export class WebGPURenderer implements RendererApi {
8498
8535
  */
8499
8536
  const depthWrite = options.depthWrite ?? true;
8500
8537
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
8538
+ /* Added rather than blended over: its own blend state, so its own pipeline. */
8539
+ const adds = blend && options.additive === true;
8501
8540
  const key =
8502
- `${keyed}${blend ? '|blend' : ''}` +
8541
+ `${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
8503
8542
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8504
8543
  /* The two order-independent buffers are two formats and two blend states, so a draw into
8505
8544
  one must never share a cached pipeline with the same mesh drawn into the other. */
@@ -8529,7 +8568,7 @@ export class WebGPURenderer implements RendererApi {
8529
8568
  * pays the compile it was trying to avoid. That is a slow frame; the alternative is a
8530
8569
  * thrown error in the middle of one.
8531
8570
  */
8532
- const pipelines = blend ? this.blendedPipelines() : this.targetPipelines();
8571
+ const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
8533
8572
  let pipeline = pipelines.peek(key);
8534
8573
  if (pipeline === undefined) {
8535
8574
  const present = this.meshPresent.get(base);
@@ -8543,7 +8582,7 @@ export class WebGPURenderer implements RendererApi {
8543
8582
  this.variant,
8544
8583
  key,
8545
8584
  present,
8546
- blend,
8585
+ adds ? 'additive' : blend,
8547
8586
  skinned,
8548
8587
  morphed,
8549
8588
  depthWrite,
@@ -8577,7 +8616,9 @@ export class WebGPURenderer implements RendererApi {
8577
8616
  * the uniform rings are the state arena this design would otherwise have had to build —
8578
8617
  * so the command carries them as its two dynamic offsets.
8579
8618
  */
8580
- const command = blend ? this.recordBlended(0) : this.recordDraw(0, this.currentTarget());
8619
+ const command = blend
8620
+ ? this.recordBlended(0, lands)
8621
+ : this.recordDraw(0, this.currentTarget());
8581
8622
  if (command !== null) {
8582
8623
  command.pipeline = pipeline;
8583
8624
  command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
@@ -8608,11 +8649,11 @@ export class WebGPURenderer implements RendererApi {
8608
8649
  whether or not this one found a slot to draw with. */
8609
8650
  if (dimmed) {
8610
8651
  this.perFrameFloats[this.materialField('uOpacity')] = 1;
8611
- this.perFrameFloats[this.materialField('uOitWeighted')] = 0;
8652
+ this.perFrameFloats[this.materialField('uWriteMode')] = 0;
8612
8653
  }
8613
8654
  if (unlitOrUnfogged) {
8614
8655
  if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
8615
- if (!fog) this.perFrameInts[this.materialField('uFogEnabled')] = 1;
8656
+ this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
8616
8657
  }
8617
8658
  /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
8618
8659
  shared, so a strength left set is worn by everything drawn after it. */
@@ -8638,7 +8679,11 @@ export class WebGPURenderer implements RendererApi {
8638
8679
  * anyway, because WebGL2 *cannot* and a capability one backend has and the other does not is
8639
8680
  * the shape this repository spent a week removing.
8640
8681
  */
8641
- createInstanced(mesh: GpuMesh, capacity: number): GpuInstancedBatch {
8682
+ createInstanced(
8683
+ mesh: GpuMesh,
8684
+ capacity: number,
8685
+ options: InstancedOptions = {},
8686
+ ): GpuInstancedBatch {
8642
8687
  /*
8643
8688
  * **A skinned mesh cannot be instanced, and the reason is the vertex layout rather than the
8644
8689
  * shader.** An instanced pipeline reclaims locations 11 and 12 from the joint indices and
@@ -8668,24 +8713,20 @@ export class WebGPURenderer implements RendererApi {
8668
8713
  'instanced pipeline.',
8669
8714
  );
8670
8715
  }
8671
- if (this.instanced.has(mesh)) {
8672
- throw new Error(
8673
- 'WebGPU: this mesh already has an instanced batch — a mesh may have one, because the ' +
8674
- "other backend binds the attributes to the mesh's own vertex array.",
8675
- );
8676
- }
8677
8716
  const batch = new GpuInstancedBatch(
8678
8717
  this.surface.device,
8679
8718
  mesh,
8680
8719
  capacity,
8681
8720
  `flat.instances:${(mesh as GpuMesh & { key?: string }).key ?? 'mesh'}`,
8721
+ options.cull ?? false,
8682
8722
  );
8683
- this.instanced.set(mesh, batch);
8684
8723
  return batch;
8685
8724
  }
8686
8725
 
8687
- /** Which meshes already carry a batch, so a second is refused rather than silently shared. */
8688
- private readonly instanced = new Map<GpuMesh, GpuInstancedBatch>();
8726
+ /** The device instance culls, built on the first culling draw. See `InstanceCullPass`. */
8727
+ private instanceCulls: InstanceCullPass | null = null;
8728
+ /** Counts frames, so a batch knows when its cull slots were last taken. */
8729
+ private cullFrame = 0;
8689
8730
 
8690
8731
  /** Push placement and colour. Only the live prefix; see `GpuInstancedBatch.upload`. */
8691
8732
  uploadInstanced(batch: GpuInstancedBatch, data: MeshInstances): void {
@@ -8733,7 +8774,6 @@ export class WebGPURenderer implements RendererApi {
8733
8774
 
8734
8775
  /** Release the placement. The mesh is the caller's and is not released. */
8735
8776
  disposeInstanced(batch: GpuInstancedBatch): void {
8736
- this.instanced.delete(batch.mesh);
8737
8777
  batch.dispose();
8738
8778
  }
8739
8779
 
@@ -8788,6 +8828,37 @@ export class WebGPURenderer implements RendererApi {
8788
8828
  * **What it gives up** against `drawMesh` is skinning and morphing, refused where the variant
8789
8829
  * is built, and a per-instance opacity: `opacity` here dims the whole batch.
8790
8830
  */
8831
+ /**
8832
+ * The next of a batch's cull slots this frame, with its cull queued against the current view; null
8833
+ * when the batch has drawn from `MAX_CULL_SLOTS` views already or the frame's job ring is full,
8834
+ * and the draw then takes every instance.
8835
+ */
8836
+ private takeCullSlot(batch: GpuInstancedBatch, count: number): CullSlot | null {
8837
+ if (batch.slotFrame !== this.cullFrame) {
8838
+ batch.slotFrame = this.cullFrame;
8839
+ batch.slotsTaken = 0;
8840
+ }
8841
+ if (batch.slotsTaken >= MAX_CULL_SLOTS) return null;
8842
+ this.instanceCulls ??= new InstanceCullPass(this.surface.device);
8843
+ let slot = batch.slots[batch.slotsTaken];
8844
+ if (slot === undefined) {
8845
+ slot = this.instanceCulls.createSlot(batch.buffer, batch.capacity, 'flat.instances');
8846
+ batch.slots.push(slot);
8847
+ }
8848
+ const bounds = batch.mesh.bounds;
8849
+ const queued = this.instanceCulls.queue(
8850
+ slot,
8851
+ this.frustum,
8852
+ bounds.centre,
8853
+ bounds.radius,
8854
+ count,
8855
+ batch.mesh.indexCount,
8856
+ );
8857
+ if (!queued) return null;
8858
+ batch.slotsTaken += 1;
8859
+ return slot;
8860
+ }
8861
+
8791
8862
  private submitInstanced(
8792
8863
  batch: GpuInstancedBatch,
8793
8864
  data: MeshInstances,
@@ -8800,6 +8871,14 @@ export class WebGPURenderer implements RendererApi {
8800
8871
  if (!mesh.complete) return;
8801
8872
  const count = Math.min(data.count, batch.capacity);
8802
8873
  if (count === 0) return;
8874
+ /* A culling batch's camera draw: the whole batch against the view and the occluders on the CPU,
8875
+ then instance by instance on the device into an indirect draw. Blended draws keep every
8876
+ instance and their order. See `InstancedOptions.cull`. */
8877
+ const culling = !blend && batch.cull;
8878
+ if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion)) return;
8879
+ const cut =
8880
+ culling && cullsInstances(count, mesh.indexCount) ? this.takeCullSlot(batch, count) : null;
8881
+ const instanceBuffer = cut === null ? batch.buffer : cut.instances;
8803
8882
 
8804
8883
  const slot = this.perDraw.allocate();
8805
8884
  if (slot === null) {
@@ -8812,10 +8891,12 @@ export class WebGPURenderer implements RendererApi {
8812
8891
  return;
8813
8892
  }
8814
8893
 
8894
+ /* Late unless it moves with what it lies on. See `TranslucentMeshOptions.reconstructed`. */
8895
+ const lands = blend && options.reconstructed !== true && this.drawsLate();
8815
8896
  this.perDraw.writeFloats(
8816
8897
  slot,
8817
8898
  FLAT_VERT_FIELDS.uViewProj.offset,
8818
- blend && this.drawsLate() ? this.correctedViewProj : this.viewProj,
8899
+ lands ? this.correctedViewProj : this.viewProj,
8819
8900
  );
8820
8901
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
8821
8902
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
@@ -8826,12 +8907,12 @@ export class WebGPURenderer implements RendererApi {
8826
8907
  this.perFrameFloats[this.materialField('uOpacity')] = opacity;
8827
8908
  }
8828
8909
  const lit = options.lit ?? true;
8829
- const fog = options.fog ?? true;
8910
+ const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
8830
8911
  const toneMapped = options.toneMapped ?? true;
8831
- const unlitOrUnfogged = !lit || !fog;
8912
+ const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
8832
8913
  if (unlitOrUnfogged) {
8833
8914
  if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
8834
- if (!fog) this.perFrameInts[this.materialField('uFogEnabled')] = 0;
8915
+ this.perFrameInts[this.materialField('uFogEnabled')] = fog;
8835
8916
  }
8836
8917
  if (!toneMapped) {
8837
8918
  this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.gradeCode(), 1);
@@ -8847,12 +8928,13 @@ export class WebGPURenderer implements RendererApi {
8847
8928
  /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
8848
8929
  a third vertex buffer and a different vertex module, and sharing a cache entry with the
8849
8930
  plain one would hand a draw the wrong layout. */
8931
+ const adds = blend && options.additive === true;
8850
8932
  const key =
8851
- `${base}|inst${blend ? '|blend' : ''}` +
8933
+ `${base}|inst${blend ? (adds ? '|add' : '|blend') : ''}` +
8852
8934
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8853
8935
  `${this.materialDoubleSided ? '|2s' : ''}`;
8854
8936
 
8855
- const pipelines = blend ? this.blendedPipelines() : this.targetPipelines();
8937
+ const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
8856
8938
  let pipeline = pipelines.peek(key);
8857
8939
  if (pipeline === undefined) {
8858
8940
  const present = this.meshPresent.get(base);
@@ -8866,7 +8948,7 @@ export class WebGPURenderer implements RendererApi {
8866
8948
  this.variant,
8867
8949
  key,
8868
8950
  present,
8869
- blend,
8951
+ adds ? 'additive' : blend,
8870
8952
  false,
8871
8953
  false,
8872
8954
  depthWrite,
@@ -8879,7 +8961,9 @@ export class WebGPURenderer implements RendererApi {
8879
8961
 
8880
8962
  const material = this.materialSlotForDraw();
8881
8963
  if (material !== null) {
8882
- const command = blend ? this.recordBlended(0) : this.recordDraw(0, this.currentTarget());
8964
+ const command = blend
8965
+ ? this.recordBlended(0, lands)
8966
+ : this.recordDraw(0, this.currentTarget());
8883
8967
  if (command !== null) {
8884
8968
  command.pipeline = pipeline;
8885
8969
  command.bindGroup = this.flatBindGroupFor(false, false, null);
@@ -8890,12 +8974,13 @@ export class WebGPURenderer implements RendererApi {
8890
8974
  for (; index < mesh.vertexBuffers.length; index++) {
8891
8975
  command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
8892
8976
  }
8893
- command.vertexBuffers[index] = batch.buffer;
8977
+ command.vertexBuffers[index] = instanceBuffer;
8894
8978
  command.vertexCount = mesh.vertexBuffers.length + 1;
8895
8979
  command.indexBuffer = mesh.indexBuffer;
8896
8980
  command.indexed = true;
8897
8981
  command.count = mesh.indexCount;
8898
8982
  command.instances = count;
8983
+ command.indirect = cut === null ? null : cut.args;
8899
8984
  } else {
8900
8985
  const pass = this.openPass();
8901
8986
  if (pass === null) return;
@@ -8905,9 +8990,10 @@ export class WebGPURenderer implements RendererApi {
8905
8990
  for (; index < mesh.vertexBuffers.length; index++) {
8906
8991
  pass.setVertexBuffer(index, mesh.vertexBuffers[index] as GPUBuffer);
8907
8992
  }
8908
- pass.setVertexBuffer(index, batch.buffer);
8993
+ pass.setVertexBuffer(index, instanceBuffer);
8909
8994
  pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
8910
- pass.drawIndexed(mesh.indexCount, count);
8995
+ if (cut === null) pass.drawIndexed(mesh.indexCount, count);
8996
+ else pass.drawIndexedIndirect(cut.args, 0);
8911
8997
  }
8912
8998
  }
8913
8999
  /* Back to the defaults, unconditionally and for the reason `drawMesh` gives at length: this
@@ -8918,7 +9004,7 @@ export class WebGPURenderer implements RendererApi {
8918
9004
  }
8919
9005
  if (unlitOrUnfogged) {
8920
9006
  this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
8921
- this.perFrameInts[this.materialField('uFogEnabled')] = 1;
9007
+ this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
8922
9008
  }
8923
9009
  if (!toneMapped) {
8924
9010
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
@@ -10456,6 +10542,23 @@ export class WebGPURenderer implements RendererApi {
10456
10542
  f[at('uMotionStrength')] = motionStrength;
10457
10543
  f[at('uMotionMax')] = MOTION_BLUR_MAX_UV;
10458
10544
  f[at('uAoStrength')] = this.frameProjection === null ? 0 : aoStrength;
10545
+ /*
10546
+ * The occlusion read where this frame's jitter put it (`uAoOffset`): measured from the jittered
10547
+ * depth and applied to a picture the jitter has been taken out of. A texel stands at its index
10548
+ * plus a half less the jitter, so the occlusion of a point is a jitter further on. The
10549
+ * reconstruction's y is given upward and negated into the corrected matrix, which this pass's
10550
+ * rows run down from; the temporal resolve's goes in as it is, and arrives as it is.
10551
+ */
10552
+ f[at('uAoOffset')] = this.reconstructing
10553
+ ? (this.reconJitter[0] as number) / this.renderWidth
10554
+ : this.temporalJittering
10555
+ ? this.temporalJitterX / this.renderWidth
10556
+ : 0;
10557
+ f[at('uAoOffset') + 1] = this.reconstructing
10558
+ ? -(this.reconJitter[1] as number) / this.renderHeight
10559
+ : this.temporalJittering
10560
+ ? this.temporalJitterY / this.renderHeight
10561
+ : 0;
10459
10562
  f[at('uDofStrength')] = dofStrength;
10460
10563
  if (dofStrength > 0 && this.frameProjection !== null) {
10461
10564
  mat4.invert(this.dofInvProjection, this.frameProjection);
@@ -10615,6 +10718,8 @@ export class WebGPURenderer implements RendererApi {
10615
10718
 
10616
10719
  for (const dead of [
10617
10720
  ...this.reconHistories,
10721
+ ...this.reconFlickers,
10722
+ ...this.reconSwings,
10618
10723
  this.reconShown,
10619
10724
  this.reconMotion,
10620
10725
  this.reconPreviousDepth,
@@ -10623,6 +10728,10 @@ export class WebGPURenderer implements RendererApi {
10623
10728
  }
10624
10729
  this.reconHistories = [null, null];
10625
10730
  this.reconHistoryViews = [null, null];
10731
+ this.reconFlickers = [null, null];
10732
+ this.reconFlickerViews = [null, null];
10733
+ this.reconSwings = [null, null];
10734
+ this.reconSwingViews = [null, null];
10626
10735
  this.reconShown = null;
10627
10736
  this.reconShownView = null;
10628
10737
  this.reconMotion = null;
@@ -10647,6 +10756,22 @@ export class WebGPURenderer implements RendererApi {
10647
10756
  });
10648
10757
  this.reconHistories[i] = texture;
10649
10758
  this.reconHistoryViews[i] = texture.createView();
10759
+ const record = device.createTexture({
10760
+ label: `recon.flicker${String(i)}`,
10761
+ size: [outputWidth, outputHeight],
10762
+ format: RECON_HISTORY_FORMAT,
10763
+ usage: HISTORY_USAGE,
10764
+ });
10765
+ this.reconFlickers[i] = record;
10766
+ this.reconFlickerViews[i] = record.createView();
10767
+ const swing = device.createTexture({
10768
+ label: `recon.swing${String(i)}`,
10769
+ size: [outputWidth, outputHeight],
10770
+ format: 'r32float',
10771
+ usage: HISTORY_USAGE,
10772
+ });
10773
+ this.reconSwings[i] = swing;
10774
+ this.reconSwingViews[i] = swing.createView();
10650
10775
  }
10651
10776
  this.reconShown = device.createTexture({
10652
10777
  label: 'recon.shown',
@@ -10723,6 +10848,26 @@ export class WebGPURenderer implements RendererApi {
10723
10848
  viewDimension: '2d' as const,
10724
10849
  },
10725
10850
  },
10851
+ sampled(8, 'float'),
10852
+ {
10853
+ binding: 9,
10854
+ visibility: COMPUTE,
10855
+ storageTexture: {
10856
+ access: 'write-only' as const,
10857
+ format: RECON_HISTORY_FORMAT,
10858
+ viewDimension: '2d' as const,
10859
+ },
10860
+ },
10861
+ sampled(10, 'unfilterable-float'),
10862
+ {
10863
+ binding: 11,
10864
+ visibility: COMPUTE,
10865
+ storageTexture: {
10866
+ access: 'write-only' as const,
10867
+ format: 'r32float' as const,
10868
+ viewDimension: '2d' as const,
10869
+ },
10870
+ },
10726
10871
  ],
10727
10872
  });
10728
10873
  const module = device.createShaderModule({ label: 'recon.resolve', code: reconResolveWgsl() });
@@ -10754,7 +10899,15 @@ export class WebGPURenderer implements RendererApi {
10754
10899
  if (this.reconMotionView === null || this.reconPreviousDepthView === null) return;
10755
10900
  if (this.reconShownView === null) return;
10756
10901
  const { device } = this.surface;
10757
- const build = (label: string, history: GPUTextureView, target: GPUTextureView) =>
10902
+ const build = (
10903
+ label: string,
10904
+ history: GPUTextureView,
10905
+ target: GPUTextureView,
10906
+ records: GPUTextureView,
10907
+ nextRecords: GPUTextureView,
10908
+ swings: GPUTextureView,
10909
+ nextSwings: GPUTextureView,
10910
+ ) =>
10758
10911
  device.createBindGroup({
10759
10912
  label,
10760
10913
  layout,
@@ -10767,23 +10920,108 @@ export class WebGPURenderer implements RendererApi {
10767
10920
  { binding: 5, resource: this.postSampler },
10768
10921
  { binding: 6, resource: { buffer: params } },
10769
10922
  { binding: 7, resource: target },
10923
+ { binding: 8, resource: records },
10924
+ { binding: 9, resource: nextRecords },
10925
+ { binding: 10, resource: swings },
10926
+ { binding: 11, resource: nextSwings },
10770
10927
  ],
10771
10928
  });
10772
10929
  for (let write = 0; write < 2; write += 1) {
10773
10930
  const read = this.reconHistoryViews[1 - write];
10774
10931
  const target = this.reconHistoryViews[write];
10775
- if (read === null || target === null) continue;
10776
- this.reconGroups[write] = build(`recon.resolve${String(write)}`, read, target);
10932
+ const records = this.reconFlickerViews[1 - write];
10933
+ const nextRecords = this.reconFlickerViews[write];
10934
+ const swings = this.reconSwingViews[1 - write];
10935
+ const nextSwings = this.reconSwingViews[write];
10936
+ if (read === null || target === null || records === null || nextRecords === null) continue;
10937
+ if (swings === null || nextSwings === null) continue;
10938
+ this.reconGroups[write] = build(
10939
+ `recon.resolve${String(write)}`,
10940
+ read,
10941
+ target,
10942
+ records,
10943
+ nextRecords,
10944
+ swings,
10945
+ nextSwings,
10946
+ );
10947
+ /* The sharpen reads no record, but one layout serves both dispatches: the same pair, so no
10948
+ texture is both read and written. */
10777
10949
  this.reconSharpenGroups[write] = build(
10778
10950
  `recon.sharpen${String(write)}`,
10779
10951
  target,
10780
10952
  this.reconShownView,
10953
+ records,
10954
+ nextRecords,
10955
+ swings,
10956
+ nextSwings,
10781
10957
  );
10782
10958
  }
10783
10959
  /* The composite's own group, over the resolved picture instead of the scene target. */
10784
10960
  this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
10785
10961
  /* And the scene's, because `ensureComposite` built it before this one existed. */
10786
10962
  this.rebuildRushBindGroup();
10963
+ /* And the bloom's, which blooms the resolved picture: see `rebuildBloomGroups`. */
10964
+ this.rebuildBloomGroups();
10965
+ }
10966
+
10967
+ /** This frame's jitter and temporal frame, decided where its scene camera is bound. See `bindMeshPass`. */
10968
+ private jitterFrame(): void {
10969
+ this.reconstructing = this.reconstructionWanted && !this.reflectionPassActive;
10970
+ if (this.quality.reconstruction > 0 && this.samples > 1 && !this.reconMultisampleSaid) {
10971
+ this.reconMultisampleSaid = true;
10972
+ console.warn(RECONSTRUCTION_MULTISAMPLE_REFUSAL);
10973
+ }
10974
+ if (this.reconstructing) {
10975
+ /*
10976
+ * The offset `settleReconJitter` chose for the frame at `beginFrame`, applied here and not
10977
+ * advanced here: a frame binding its mesh pass twice is one frame of the sequence.
10978
+ *
10979
+ * **The y offset is negated on the way in, and that is not a preference.** This shifts the
10980
+ * *corrected* matrix, whose y `CLIP_CORRECTION` has already negated so that the negation
10981
+ * every generated vertex stage ends with cancels out — §3 row 56. So an offset added here
10982
+ * arrives on screen with its sign reversed, while x, which nothing negates, arrives as
10983
+ * given. The temporal resolve cannot see this because its shader never reads the offset; the
10984
+ * reconstruction reads it at every texel it unprojects, and a sign error there moves every
10985
+ * sample two jitters away from where the depth says it is.
10986
+ */
10987
+ this.viewProj = jitterProjection(
10988
+ this.jitteredViewProj,
10989
+ this.correctedViewProj,
10990
+ this.reconJitter[0] as number,
10991
+ -(this.reconJitter[1] as number),
10992
+ this.renderWidth,
10993
+ this.renderHeight,
10994
+ ) as Float32Array;
10995
+ this.temporalJittering = false;
10996
+ this.temporalHistoryUsable = false;
10997
+ }
10998
+ this.temporalJittering =
10999
+ !this.reconstructing &&
11000
+ this.quality.temporalAa &&
11001
+ this.quality.screenEffects &&
11002
+ !this.reflectionPassActive;
11003
+ if (this.temporalJittering) {
11004
+ /* The scene's size, not the drawing buffer's: the history is a picture of the render. */
11005
+ this.temporalHistoryUsable = this.temporalHistory.openFrame(
11006
+ this.renderWidth,
11007
+ this.renderHeight,
11008
+ );
11009
+ const [jx, jy] = jitterOffset(this.temporalHistory.frameIndex);
11010
+ this.temporalJitterX = jx;
11011
+ this.temporalJitterY = jy;
11012
+ /* A jitter is half a *render* texel, which is what the sequence is spread over. */
11013
+ this.viewProj = jitterProjection(
11014
+ this.jitteredViewProj,
11015
+ this.correctedViewProj,
11016
+ jx,
11017
+ jy,
11018
+ this.renderWidth,
11019
+ this.renderHeight,
11020
+ ) as Float32Array;
11021
+ } else if (!this.reconstructing) {
11022
+ this.temporalHistoryUsable = false;
11023
+ this.viewProj = this.correctedViewProj;
11024
+ }
10787
11025
  }
10788
11026
 
10789
11027
  /**
@@ -10809,15 +11047,15 @@ export class WebGPURenderer implements RendererApi {
10809
11047
  * otherwise. **Late draws record whatever `quality.frameGraph` says**, since the late replay is
10810
11048
  * the only moment they can land; everything else keeps the path it has.
10811
11049
  */
10812
- private recordBlended(reads: number): DrawCommand | null {
11050
+ private recordBlended(reads: number, lands = this.drawsLate()): DrawCommand | null {
10813
11051
  const late = this.latePass;
10814
- if (late !== null && this.drawsLate()) return late.take();
11052
+ if (late !== null && lands) return late.take();
10815
11053
  return this.recordDraw(reads, this.currentTarget());
10816
11054
  }
10817
11055
 
10818
11056
  /** 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();
11057
+ private blendedPipelines(lands = this.drawsLate()): PipelineCache {
11058
+ return lands ? this.latePipelines : this.targetPipelines();
10821
11059
  }
10822
11060
 
10823
11061
  /**
@@ -10835,7 +11073,16 @@ export class WebGPURenderer implements RendererApi {
10835
11073
  const width = Math.max(1, this.surface.canvas.width);
10836
11074
  const height = Math.max(1, this.surface.canvas.height);
10837
11075
  /* 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);
11076
+ late.upscale(
11077
+ encoder,
11078
+ depth,
11079
+ this.renderWidth,
11080
+ this.renderHeight,
11081
+ width,
11082
+ height,
11083
+ this.reconJitter[0] as number,
11084
+ this.reconJitter[1] as number,
11085
+ );
10839
11086
  if (this.lateRefractWanted && this.refractSnapshot !== null && this.reconShown !== null) {
10840
11087
  encoder.copyTextureToTexture(
10841
11088
  { texture: this.reconShown },
@@ -10994,7 +11241,16 @@ export class WebGPURenderer implements RendererApi {
10994
11241
  * OpenGL's depth convention — which is the convention the shader's accessor converts the
10995
11242
  * renderer's reversed buffer into.
10996
11243
  */
10997
- mat4.invert(this.reconInverseViewProj, this.frameRawViewProj);
11244
+ /*
11245
+ * The inverse and the reprojection in double precision, rounded once on the way into the
11246
+ * uniform: the product of two single-precision matrices this far apart in depth is not the
11247
+ * identity for a still camera, and the resolve read the difference as motion.
11248
+ */
11249
+ mat4.invert(this.reconInverse64, this.frameRawViewProj);
11250
+ mat4.multiply(this.reconReprojection64, this.reconPreviousRawViewProj, this.reconInverse64);
11251
+ for (let i = 0; i < 16; i += 1) {
11252
+ this.reconInverseViewProj[i] = this.reconInverse64[i] as number;
11253
+ }
10998
11254
  const f = this.reconFloats;
10999
11255
  const u = this.reconInts;
11000
11256
  u[0] = this.renderWidth;
@@ -11018,6 +11274,17 @@ export class WebGPURenderer implements RendererApi {
11018
11274
  f[69] = DEFAULT_DISOCCLUSION.motionScale;
11019
11275
  f[70] = DEFAULT_DISOCCLUSION.normalFloor;
11020
11276
  f[71] = DEFAULT_DISOCCLUSION.normalCeiling;
11277
+ /* The anti-flicker over the reconstruction's own period, its rule made once a period. */
11278
+ const phases = reconJitterPhases(this.renderWidth, outputWidth);
11279
+ if (phases !== this.reconFlickerPhases) {
11280
+ this.reconFlickerPhases = phases;
11281
+ this.reconFlickerRule = flickerRule(phases);
11282
+ }
11283
+ u[72] = this.reconFrameIndex % phases === 0 ? 1 : 0;
11284
+ u[73] = 1;
11285
+ f[74] = this.reconFlickerRule.memory;
11286
+ f[75] = this.reconFlickerRule.width;
11287
+ f.set(this.reconReprojection64, 76);
11021
11288
  device.queue.writeBuffer(params, 0, this.reconStaging);
11022
11289
 
11023
11290
  const groupsX = Math.ceil(outputWidth / RECON_WORKGROUP);
@@ -12317,6 +12584,7 @@ export class WebGPURenderer implements RendererApi {
12317
12584
  command.indexBuffer = null;
12318
12585
  command.indexed = false;
12319
12586
  command.instances = 1;
12587
+ command.indirect = null;
12320
12588
  recordNode(this.arena, VERB_DRAW, reads, writes, at, 0);
12321
12589
  return command;
12322
12590
  }
@@ -12363,7 +12631,8 @@ export class WebGPURenderer implements RendererApi {
12363
12631
  }
12364
12632
  if (command.indexed && command.indexBuffer !== null) {
12365
12633
  pass.setIndexBuffer(command.indexBuffer, 'uint32');
12366
- pass.drawIndexed(command.count, command.instances);
12634
+ if (command.indirect !== null) pass.drawIndexedIndirect(command.indirect, 0);
12635
+ else pass.drawIndexed(command.count, command.instances);
12367
12636
  } else {
12368
12637
  pass.draw(command.count, command.instances);
12369
12638
  }
@@ -13020,6 +13289,11 @@ export class WebGPURenderer implements RendererApi {
13020
13289
  * declares nothing behaves exactly as it did before. Read it beside `visible`, which answers the
13021
13290
  * other half of the same question.
13022
13291
  */
13292
+ /** Whether a world-space box is wholly behind the declared occluders. See `Renderer.occludedBox`. */
13293
+ occludedBox(min: ArrayLike<number>, max: ArrayLike<number>): boolean {
13294
+ return this.occlusion?.occludedBox(min, max) ?? false;
13295
+ }
13296
+
13023
13297
  occluded(bounds: Bounds, model: ReadonlyMat4): boolean {
13024
13298
  return this.occlusion?.occluded(bounds, model) ?? false;
13025
13299
  }
@@ -13475,12 +13749,14 @@ export class WebGPURenderer implements RendererApi {
13475
13749
  const cssHeight = Math.max(canvas.clientHeight, 1);
13476
13750
  const origin = canvas.getBoundingClientRect();
13477
13751
  /*
13478
- * **CSS pixels to the scene's pixels, not the viewer's.** A viewport is state on the frame's
13479
- * own pass, which draws into the render-size targets, so a box measured in the drawing buffer
13480
- * would be scaled by the reconstruction ratio and land off the side of a smaller attachment.
13752
+ * **CSS pixels to the pixels of the attachment this pass draws into.** Mid-frame that is the
13753
+ * scene's render size, which a reconstruction makes smaller than the drawing buffer; after the
13754
+ * present it is the swap image, the drawing buffer itself. Measured against the scene's size
13755
+ * there, an interface's inset landed two thirds of the way to where it was asked for.
13481
13756
  */
13482
- const scaleX = this.renderWidth / cssWidth;
13483
- const scaleY = this.renderHeight / cssHeight;
13757
+ const target = this.passSize();
13758
+ const scaleX = target[0] / cssWidth;
13759
+ const scaleY = target[1] / cssHeight;
13484
13760
  const left = rect.left - origin.left;
13485
13761
  const top = rect.top - origin.top;
13486
13762
  const w = Math.max(1, Math.round(rect.width * scaleX));
@@ -13508,9 +13784,8 @@ export class WebGPURenderer implements RendererApi {
13508
13784
  * edge for the frames it is half-off. That asymmetry is the cost of the clamp, it is
13509
13785
  * recorded in the parity ledger, and the alternative was an exception.
13510
13786
  */
13511
- /* The attachment this pass draws into, which is the scene's size — see `renderWidth`. */
13512
- const targetWidth = this.renderWidth;
13513
- const targetHeight = this.renderHeight;
13787
+ const targetWidth = target[0];
13788
+ const targetHeight = target[1];
13514
13789
  const clampedX = Math.max(0, Math.min(x, targetWidth));
13515
13790
  const clampedY = Math.max(0, Math.min(y, targetHeight));
13516
13791
  const clampedW = Math.max(0, Math.min(x + w, targetWidth) - clampedX);
@@ -13587,11 +13862,31 @@ export class WebGPURenderer implements RendererApi {
13587
13862
  if (this.quality.frameGraph) this.flushGraph();
13588
13863
  const pass = this.pass;
13589
13864
  if (this.surface.lost || pass === null) return;
13590
- /* The whole of what this pass draws into, which is the scene's size and not the viewer's. */
13591
- pass.setViewport(0, 0, this.renderWidth, this.renderHeight, 0, 1);
13592
- pass.setScissorRect(0, 0, this.renderWidth, this.renderHeight);
13865
+ /* The whole of what this pass draws into: the scene's size mid-frame, the canvas after. */
13866
+ const [width, height] = this.passSize();
13867
+ pass.setViewport(0, 0, width, height, 0, 1);
13868
+ pass.setScissorRect(0, 0, width, height);
13593
13869
  }
13594
13870
 
13871
+ /**
13872
+ * The size of the attachment the open pass draws into: the swap image — the drawing buffer —
13873
+ * in the overlay after the present, the scene's render size in the frame's own pass. A tuple
13874
+ * held once rather than a new array a call.
13875
+ */
13876
+ private passSize(): readonly [number, number] {
13877
+ const size = this.passSizeOut;
13878
+ if (this.overlayActive) {
13879
+ size[0] = Math.max(1, this.surface.canvas.width);
13880
+ size[1] = Math.max(1, this.surface.canvas.height);
13881
+ } else {
13882
+ size[0] = this.renderWidth;
13883
+ size[1] = this.renderHeight;
13884
+ }
13885
+ return size;
13886
+ }
13887
+
13888
+ private readonly passSizeOut: [number, number] = [0, 0];
13889
+
13595
13890
  /**
13596
13891
  * Copy a rectangle of the frame into a 2D canvas, pixel for pixel.
13597
13892
  *
@@ -14182,10 +14477,14 @@ export class WebGPURenderer implements RendererApi {
14182
14477
  private probeView: GPUTextureView | null = null;
14183
14478
  private readonly probeSize: number;
14184
14479
  private readonly probeMipLayout: GPUBindGroupLayout;
14185
- /** The photometric atlas, allocated on first use. See `setIesProfiles`. */
14186
- private iesTexture: GPUTexture | null = null;
14480
+ /** The fixture atlas — cookies over profiles — built on first use. See `fixtureAtlas.ts`. */
14481
+ private fixtureTexture: GPUTexture | null = null;
14482
+ private fixtureView: GPUTextureView | null = null;
14483
+ /** The profiles as packed and the cookie images, kept so either can rebuild the atlas. */
14484
+ private iesAtlas = packIesAtlas([]);
14485
+ private cookieImages: readonly TexImageSource[] = [];
14187
14486
  /** The scene's DriftLight field, and its two volumes once it is whole. See `createLightField`. */
14188
- private lightField: LightField | null = null;
14487
+ private lightField: DriftLightVolumes | null = null;
14189
14488
  private driftIndex: GPUTexture | null = null;
14190
14489
  private driftAtlas: GPUTexture | null = null;
14191
14490
  private driftIndexView: GPUTextureView | null = null;
@@ -14197,10 +14496,7 @@ export class WebGPURenderer implements RendererApi {
14197
14496
  /** The frame's exact lights packed for the traced bake. See `gi/bounceLights.ts`. */
14198
14497
  private readonly bounceLights = createBounceLights();
14199
14498
  private readonly bakeLamps = { data: this.bounceLights, count: 0, falloff: 0 };
14200
- private iesView: GPUTextureView | null = null;
14201
- private cookieTexture: GPUTexture | null = null;
14202
- private cookieView: GPUTextureView | null = null;
14203
- /** Tiles in the cookie atlas. 0 is the white placeholder and the whole off path. */
14499
+ /** Cookie tiles in the atlas. 0 is none, and the whole off path. */
14204
14500
  private cookieTiles = 0;
14205
14501
  /** Rows in that atlas. One until a consumer loads a profile, which is the row of ones. */
14206
14502
  private iesRows = 1;
@@ -14688,42 +14984,58 @@ export class WebGPURenderer implements RendererApi {
14688
14984
  */
14689
14985
  setSpotCookies(images: readonly TexImageSource[]): void {
14690
14986
  if (this.surface.lost) return;
14987
+ this.cookieImages = images.slice();
14988
+ this.forgetFixtureAtlas();
14989
+ this.rebuildFlatBindGroup();
14990
+ }
14991
+
14992
+ /** Let the atlas go, to be built again from what is in hand the next time it is bound. */
14993
+ private forgetFixtureAtlas(): void {
14994
+ this.fixtureTexture?.destroy();
14995
+ this.fixtureTexture = null;
14996
+ this.fixtureView = null;
14997
+ }
14998
+
14999
+ /**
15000
+ * The fixture atlas's view, built from the profiles and cookies in hand if it is not already: the
15001
+ * profiles' rows at the bottom, the cookies' tiles on top. See `renderer.ts` and `fixtureAtlas.ts`.
15002
+ *
15003
+ * **`rgba16float`, not `rgba32float`**: a 32-bit float texture is not filterable in core WebGPU,
15004
+ * and binding one to a filtering sampler takes the whole bind group with it. Both tables want
15005
+ * linear filtering. `RENDER_ATTACHMENT` because `copyExternalImageToTexture` writes through one.
15006
+ */
15007
+ private fixtureAtlasView(): GPUTextureView {
15008
+ if (this.fixtureView !== null) return this.fixtureView;
14691
15009
  const { device } = this.surface;
14692
- const tiles = images.length;
14693
- const width = tiles === 0 ? 1 : COOKIE_TILE * tiles;
14694
- const height = tiles === 0 ? 1 : COOKIE_TILE;
14695
- if (this.cookieTexture === null || this.cookieTexture.width !== width) {
14696
- this.cookieTexture?.destroy();
14697
- this.cookieTexture = device.createTexture({
14698
- label: 'cookie.atlas',
14699
- size: [width, height],
14700
- format: 'rgba8unorm',
14701
- /* `RENDER_ATTACHMENT` because `copyExternalImageToTexture` writes through one. */
14702
- usage: 0x4 | 0x2 | 0x10,
14703
- });
14704
- this.cookieView = this.cookieTexture.createView();
14705
- }
14706
- if (tiles === 0) {
14707
- /* White, not black: this multiplies a light's colour. */
14708
- device.queue.writeTexture(
14709
- { texture: this.cookieTexture },
14710
- new Uint8Array([255, 255, 255, 255]),
14711
- { bytesPerRow: 4, rowsPerImage: 1 },
14712
- { width: 1, height: 1, depthOrArrayLayers: 1 },
15010
+ const layout = fixtureAtlasLayout(this.iesAtlas.height, this.cookieImages.length);
15011
+ const texture = device.createTexture({
15012
+ label: 'fixture.atlas',
15013
+ size: [layout.width, layout.height],
15014
+ format: 'rgba16float',
15015
+ usage: 0x4 | 0x2 | 0x10,
15016
+ });
15017
+ device.queue.writeTexture(
15018
+ { texture },
15019
+ /* Converted, because `writeTexture` does not — see `environmentTexels`. */
15020
+ toHalfFloats(fixtureAtlasTexels(layout, this.iesAtlas)),
15021
+ { bytesPerRow: layout.width * 8, rowsPerImage: layout.height },
15022
+ { width: layout.width, height: layout.height, depthOrArrayLayers: 1 },
15023
+ );
15024
+ for (let tile = 0; tile < this.cookieImages.length; tile++) {
15025
+ const source = this.cookieImages[tile];
15026
+ if (source === undefined) continue;
15027
+ device.queue.copyExternalImageToTexture(
15028
+ { source: source as GPUCopyExternalImageSource },
15029
+ { texture, origin: { x: tile * COOKIE_TILE, y: 0 } },
15030
+ { width: COOKIE_TILE, height: COOKIE_TILE },
14713
15031
  );
14714
- } else {
14715
- for (let tile = 0; tile < tiles; tile++) {
14716
- const source = images[tile];
14717
- if (source === undefined) continue;
14718
- device.queue.copyExternalImageToTexture(
14719
- { source: source as GPUCopyExternalImageSource },
14720
- { texture: this.cookieTexture, origin: { x: tile * COOKIE_TILE, y: 0 } },
14721
- { width: COOKIE_TILE, height: COOKIE_TILE },
14722
- );
14723
- }
14724
15032
  }
14725
- this.cookieTiles = tiles;
14726
- this.rebuildFlatBindGroup();
15033
+ this.fixtureTexture = texture;
15034
+ this.fixtureView = texture.createView();
15035
+ this.iesRows = this.iesAtlas.height;
15036
+ this.iesPlanes = this.iesAtlas.planes;
15037
+ this.cookieTiles = this.cookieImages.length;
15038
+ return this.fixtureView;
14727
15039
  }
14728
15040
 
14729
15041
  /**
@@ -14740,6 +15052,23 @@ export class WebGPURenderer implements RendererApi {
14740
15052
  return field;
14741
15053
  }
14742
15054
 
15055
+ /**
15056
+ * Hand a world's DriftLight volume to the renderer: every fixed light of a city, summed offline by
15057
+ * `bakeDenseField`, standing in past the frame's exact choice. Replaces any field made before, of
15058
+ * either kind; the scene calls `field.follow(buffer.complete, ...)` each frame as it would for
15059
+ * `createLightField`, and marks the lights the volume summed `inLightField` so none is counted
15060
+ * twice. See `driftLight/worldLightField.ts`.
15061
+ */
15062
+ createWorldLightField(
15063
+ volume: DenseLightVolume,
15064
+ options: WorldLightFieldOptions = {},
15065
+ ): WorldLightField {
15066
+ this.forgetLightFieldVolumes();
15067
+ const field = new WorldLightField(volume, options);
15068
+ this.lightField = field;
15069
+ return field;
15070
+ }
15071
+
14743
15072
  /** Let go of the field and its volumes; the scene's lights are all exact again. */
14744
15073
  disposeLightField(): void {
14745
15074
  this.forgetLightFieldVolumes();
@@ -14766,12 +15095,12 @@ export class WebGPURenderer implements RendererApi {
14766
15095
  */
14767
15096
  private uploadLightField(): void {
14768
15097
  const field = this.lightField;
14769
- if (field === null || this.driftAtlas !== null || !field.ready || field.layout.count === 0) {
15098
+ if (field === null || this.driftAtlas !== null || !field.ready || field.empty) {
14770
15099
  return;
14771
15100
  }
14772
15101
  if (this.surface.lost) return;
14773
15102
  const { device } = this.surface;
14774
- const [ix, iy, iz] = field.layout.dims;
15103
+ const [ix, iy, iz] = field.indexDims;
14775
15104
  const index = device.createTexture({
14776
15105
  label: 'driftLight.index',
14777
15106
  size: [ix, iy, iz],
@@ -14781,7 +15110,7 @@ export class WebGPURenderer implements RendererApi {
14781
15110
  });
14782
15111
  device.queue.writeTexture(
14783
15112
  { texture: index },
14784
- field.layout.index,
15113
+ field.index,
14785
15114
  { bytesPerRow: ix * 4, rowsPerImage: iy },
14786
15115
  [ix, iy, iz],
14787
15116
  );
@@ -14814,45 +15143,8 @@ export class WebGPURenderer implements RendererApi {
14814
15143
 
14815
15144
  setIesProfiles(profiles: readonly PhotometricProfile[]): void {
14816
15145
  if (this.surface.lost) return;
14817
- const { device } = this.surface;
14818
- const atlas = packIesAtlas(profiles);
14819
- /*
14820
- * Reallocated when the row count changes, because a texture's size is immutable. A set of the
14821
- * same size overwrites in place, which is the common case of a consumer swapping one fixture.
14822
- */
14823
- if (this.iesTexture === null || this.iesTexture.height !== atlas.height) {
14824
- this.iesTexture?.destroy();
14825
- this.iesTexture = device.createTexture({
14826
- label: 'ies.atlas',
14827
- size: [atlas.width, atlas.height],
14828
- /*
14829
- * **`r16float`, not `r32float`, and that is a filtering constraint rather than a size
14830
- * choice.** A 32-bit float texture is not filterable in core WebGPU — it needs the
14831
- * optional `float32-filterable` feature — and binding one to a filtering sampler is a
14832
- * validation failure that takes the whole bind group with it. This row *wants* linear
14833
- * filtering: it is a curve sampled between two angles, which is the entire reason it is
14834
- * not `NEAREST` like the froxel table beside it.
14835
- *
14836
- * Half precision holds a normalised intensity to about three decimal digits, which is far
14837
- * finer than the eight-bit frame it ends up in. **What would make it wrong** is a profile
14838
- * carrying absolute candela rather than a normalised curve, which would run out of range —
14839
- * and `packIesAtlas` normalises for its own reasons.
14840
- */
14841
- format: 'r16float',
14842
- usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
14843
- });
14844
- this.iesView = this.iesTexture.createView();
14845
- }
14846
- device.queue.writeTexture(
14847
- { texture: this.iesTexture },
14848
- /* Converted, because `writeTexture` does not — see `environmentTexels` for what handing it
14849
- the wrong width silently produces. */
14850
- toHalfFloats(atlas.data),
14851
- { bytesPerRow: atlas.width * 2, rowsPerImage: atlas.height },
14852
- { width: atlas.width, height: atlas.height, depthOrArrayLayers: 1 },
14853
- );
14854
- this.iesRows = atlas.height;
14855
- this.iesPlanes = atlas.planes;
15146
+ this.iesAtlas = packIesAtlas(profiles);
15147
+ this.forgetFixtureAtlas();
14856
15148
  this.rebuildFlatBindGroup();
14857
15149
  }
14858
15150
 
@@ -15356,7 +15648,10 @@ export class WebGPURenderer implements RendererApi {
15356
15648
  * An empty list is a real answer for a frame too small to hold one, and `runBloom` reads it
15357
15649
  * as no bloom rather than as a target it should have had.
15358
15650
  */
15359
- this.bloomLevels = bloomLevelSizes(width, height).map((size, index) => {
15651
+ /* The resolved picture's size while reconstructing, as the order-independent pair above is. */
15652
+ this.bloomSourceWidth = oitSize[0];
15653
+ this.bloomSourceHeight = oitSize[1];
15654
+ this.bloomLevels = bloomLevelSizes(oitSize[0], oitSize[1]).map((size, index) => {
15360
15655
  const texture = device.createTexture({
15361
15656
  label: `post.bloom${index}`,
15362
15657
  size: [size.width, size.height],
@@ -15529,13 +15824,31 @@ export class WebGPURenderer implements RendererApi {
15529
15824
  /* The volume's group holds the depth snapshot, which is one of the targets just replaced.
15530
15825
  Keeping the old group would clamp every beam against a destroyed texture. */
15531
15826
  this.lightVolumeBindGroup = this.buildLightVolumeBindGroup();
15532
- /*
15533
- * One group per source the chain reads, in the order `runBloom` indexes them: the scene
15534
- * first, then every level. The prefilter reads the scene; the downsample into level `i`
15535
- * reads level `i - 1`, which is group `i`; the upsample into level `i - 1` reads level
15536
- * `i`, which is group `i + 1`.
15537
- */
15538
- this.bloomGroups = [this.sceneColorView, ...this.bloomLevels.map((level) => level.view)].map(
15827
+ this.rebuildBloomGroups();
15828
+ this.writeBloomBlocks();
15829
+ }
15830
+
15831
+ /**
15832
+ * One group per source the bloom chain reads, in the order `runBloom` indexes them: the picture
15833
+ * first, then every level. The prefilter reads the picture; the downsample into level `i` reads
15834
+ * level `i - 1`, which is group `i`; the upsample into level `i - 1` reads level `i`, which is
15835
+ * group `i + 1`.
15836
+ *
15837
+ * **While reconstructing the picture is the resolved one**, which `ensureReconstruction` makes
15838
+ * after this runs and rebuilds these over. The scene target is the jittered render then: a bright
15839
+ * thing smaller than a render texel is caught in some phases and missed in others, and its halo,
15840
+ * wide and over everything round it, flashed with it — a night street's panels breathing a few
15841
+ * levels of luma frame to frame. Built at the drawing buffer's size, which `ensureComposite`
15842
+ * already sized the pyramid for.
15843
+ */
15844
+ private rebuildBloomGroups(): void {
15845
+ const picture =
15846
+ this.reconstructionWanted && this.reconShownView !== null
15847
+ ? this.reconShownView
15848
+ : this.sceneColorView;
15849
+ if (picture === null) return;
15850
+ const { device } = this.surface;
15851
+ this.bloomGroups = [picture, ...this.bloomLevels.map((level) => level.view)].map(
15539
15852
  (view, index) =>
15540
15853
  device.createBindGroup({
15541
15854
  label: `post.bloomGroup${index}`,
@@ -15545,11 +15858,11 @@ export class WebGPURenderer implements RendererApi {
15545
15858
  binding: BLOOM_UNIFORMS,
15546
15859
  resource: { buffer: this.bloomUniforms, size: BLOOM_STAGE_SIZE },
15547
15860
  },
15548
- ...entry(BLOOM_TEXTURES.uSource, view),
15861
+ { binding: BLOOM_TEXTURES.uSource.texture, resource: view },
15862
+ { binding: BLOOM_TEXTURES.uSource.sampler, resource: this.postSampler },
15549
15863
  ],
15550
15864
  }),
15551
15865
  );
15552
- this.writeBloomBlocks();
15553
15866
  }
15554
15867
 
15555
15868
  /**
@@ -15567,12 +15880,12 @@ export class WebGPURenderer implements RendererApi {
15567
15880
  if (levels.length === 0) return;
15568
15881
  const slot = (index: number): number => (index * BLOOM_SLOT) / 4;
15569
15882
 
15570
- /* The prefilter reads the *scene*, so its texel is the frame's rather than a level's.
15883
+ /* The prefilter reads the *picture*, so its texel is the picture's rather than a level's.
15571
15884
  `uTexel`, not `uTexelSize`, and the block carries no strength at all: the composite
15572
15885
  applies that when it adds the result back. Both were written wrong here first. */
15573
15886
  const prefilter = (name: string): number => this.postField(BLOOM_PREFILTER_FIELDS, name);
15574
- f[slot(0) + prefilter('uTexel')] = 1 / Math.max(1, this.frameWidth);
15575
- f[slot(0) + prefilter('uTexel') + 1] = 1 / Math.max(1, this.frameHeight);
15887
+ f[slot(0) + prefilter('uTexel')] = 1 / Math.max(1, this.bloomSourceWidth);
15888
+ f[slot(0) + prefilter('uTexel') + 1] = 1 / Math.max(1, this.bloomSourceHeight);
15576
15889
  /* In scene units, which is why an HDR target is what makes a threshold above 1 mean
15577
15890
  anything: against a clamped buffer nothing is ever brighter than white. */
15578
15891
  f[slot(0) + prefilter('uThreshold')] = this.bloomThresholdSet ?? this.quality.bloomThreshold;
@@ -15699,6 +16012,7 @@ export class WebGPURenderer implements RendererApi {
15699
16012
  this.fieldComposer = null;
15700
16013
  this.driftIndex?.destroy();
15701
16014
  this.driftAtlas?.destroy();
16015
+ this.forgetFixtureAtlas();
15702
16016
  this.sunTint?.texture.destroy();
15703
16017
  this.sunTint = null;
15704
16018
  this.glassTint?.dispose();