@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,5 +1,6 @@
1
1
  import { FrameBudget } from '../budget.js';
2
2
  import { MaterialChanges, ownsMaterial } from '../materialChanges.js';
3
+ import { FOG_RECEDE, fogModeOf } from '../drawFog.js';
3
4
  import { mat4 } from 'gl-matrix';
4
5
  import { GL_DEPTH_REMAP, MAX_DEPTH_LAYER, depthClearFor, depthOffsetForLayer, GL_SHADOW_REMAP, REVERSED_DEPTH, SHADOW_DEPTH_CLEAR, glDepthFuncEqual, glShadowDepthFunc, } from '../../depthConvention.js';
5
6
  import { resolveGlass } from '../../glass.js';
@@ -16,6 +17,7 @@ import { OIT_MULTISAMPLE_REFUSAL } from '../../orderIndependent.js';
16
17
  import { INDIRECT_LIGHT_WEBGL2_REFUSAL } from '../../gi/probeTrace.js';
17
18
  import { TranslucentQueue } from '../../translucentQueue.js';
18
19
  import { createFrustum, frustumFromViewProjection } from '../../../math/frustum.js';
20
+ import { batchBoxVisible, cullsInstances } from '../../instanceCull.js';
19
21
  import { OcclusionBuffer } from '../../occlusion.js';
20
22
  import { boundsVisible } from '../../visibility.js';
21
23
  import { createPassRegistry, drainRegistry, passAt, registerIn, unregisterIn } from '../../pass.js';
@@ -32,6 +34,7 @@ import { FILM_LOOK_WITHOUT_COMPOSITE } from '../../shaders/filmLook.js';
32
34
  import { bloomProfileWarning, bloomThresholdOf } from '../../bloomChain.js';
33
35
  import { createEmptyTexture2D, createEmptyTexture2DArray, createEmptyTexture3D, createEmptyTextureCube, } from '../../emptyTexture.js';
34
36
  import { LightField } from '../../driftLight/lightField.js';
37
+ import { WorldLightField } from '../../driftLight/worldLightField.js';
35
38
  import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.js';
36
39
  import { flatFrag, flatVert } from '../../shaders/flat/index.js';
37
40
  import { MORPH_DELTA_TEXTURE_UNIT, SKIN_PALETTE_TEXTURE_UNIT } from '../../lightBudget.js';
@@ -47,6 +50,8 @@ import { ProbeSweeps } from '../../probeSweeps.js';
47
50
  import { octahedralEdgeFor } from '../../prefilterEnvMap.js';
48
51
  import { EnvironmentPrefilterPass } from '../../prefilterPass.js';
49
52
  import { packIesAtlas } from '../../iesProfile.js';
53
+ import { fixtureAtlasLayout, fixtureAtlasTexels } from '../../fixtureAtlas.js';
54
+ import { resolveSurfaceScene } from '../../surfaceScene.js';
50
55
  import { MAX_AREA_LIGHTS } from '../../areaLights.js';
51
56
  import { areaShadowLayerCount, castingRange, createResolvedAreaShadows, firstAreaShadowLayer, AreaShadowSet, } from '../../areaShadowSet.js';
52
57
  import { equirectToCubeFaces } from '../../equirectToCube.js';
@@ -62,7 +67,7 @@ import { GlassTintPrograms } from './glassTintPrograms.js';
62
67
  import { FACE_COUNT, PointShadowMap } from '../../pointShadowMap.js';
63
68
  import { createResolvedPointShadows, DEFAULT_SOURCE_RADIUS } from '../../pointShadowImage.js';
64
69
  import { createBakeScratch, runPointShadowBakes, selectCastingLights, } from '../../pointShadowBudget.js';
65
- import { DIRECTIONAL_SHADOW_UNITS, SUN_GLASS_TINT_TEXTURE_UNIT, POINT_GLASS_TINT_TEXTURE_UNIT, LIVE_POINT_SHADOW_MAPS, MAX_POINT_LIGHTS, NORMAL_TEXTURE_UNIT, ORM_TEXTURE_UNIT, POINT_SHADOW_POOL, POINT_SHADOW_UNITS, CLUSTER_TABLE_TEXTURE_UNIT, EMISSIVE_TEXTURE_UNIT, ENVIRONMENT_TEXTURE_UNIT, SURFACE_TEXTURE_UNIT, bindPointLights, COOKIE_ATLAS_TEXTURE_UNIT, REFRACT_SCENE_TEXTURE_UNIT, DRIFT_LIGHT_ATLAS_TEXTURE_UNIT, DRIFT_LIGHT_INDEX_TEXTURE_UNIT, COOKIE_TILE, IES_ATLAS_TEXTURE_UNIT, bindAreaLights, bindAreaShadows, openCones, } from '../../lightBudget.js';
70
+ import { DIRECTIONAL_SHADOW_UNITS, SUN_GLASS_TINT_TEXTURE_UNIT, POINT_GLASS_TINT_TEXTURE_UNIT, LIVE_POINT_SHADOW_MAPS, MAX_POINT_LIGHTS, NORMAL_TEXTURE_UNIT, ORM_TEXTURE_UNIT, POINT_SHADOW_POOL, POINT_SHADOW_UNITS, CLUSTER_TABLE_TEXTURE_UNIT, EMISSIVE_TEXTURE_UNIT, ENVIRONMENT_TEXTURE_UNIT, SURFACE_TEXTURE_UNIT, bindPointLights, REFRACT_SCENE_TEXTURE_UNIT, DRIFT_LIGHT_ATLAS_TEXTURE_UNIT, DRIFT_LIGHT_INDEX_TEXTURE_UNIT, COOKIE_TILE, FIXTURE_ATLAS_TEXTURE_UNIT, SURFACE_EFFECTS_TEXTURE_UNIT, bindAreaLights, bindAreaShadows, openCones, } from '../../lightBudget.js';
66
71
  import { resolveRenderQuality } from '../../renderQuality.js';
67
72
  import { drawingBufferSize } from '../../drawingBuffer.js';
68
73
  import { Mesh, createMeshIncremental } from '../../mesh.js';
@@ -236,6 +241,8 @@ const IDENTITY_MAT4 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
236
241
  export { MAX_DEPTH_LAYER } from '../../depthConvention.js';
237
242
  /** The identity tint, handed back after any draw that asked for one. See `drawMesh`. */
238
243
  const WHITE_TINT = new Float32Array([1, 1, 1]);
244
+ /** The lit stage's clip plane when there is no mirror: zero, which clips nothing. */
245
+ const NO_CLIP_PLANE = new Float32Array(4);
239
246
  /** Handed to the shader when a batch has no press field. See `drawScatter`. */
240
247
  /**
241
248
  * What a contributed pass pre-multiplies its projection by on this backend: nothing.
@@ -546,6 +553,11 @@ export class WebGL2Renderer {
546
553
  oitActive = false;
547
554
  /** True while the queue is being replayed, so a replayed draw does not record itself again. */
548
555
  oitReplaying = false;
556
+ /** `uSurfaceScene`, resolved once a pass. See `surfaceScene.ts`. */
557
+ surfaceScene = new Float32Array(4);
558
+ /** The two halves of `uWriteMode`, kept here because a `vec2` is written whole. */
559
+ oitWeight = 0;
560
+ ditherFade = 0;
549
561
  /** Said once, not per frame, where the context cannot give the float target the sum needs. */
550
562
  oitRefused = false;
551
563
  /** Said once for the whole renderer. See `refuseOitMultisampled`. */
@@ -639,6 +651,8 @@ export class WebGL2Renderer {
639
651
  taaDepthToViewZ = Float32Array.of(0, 0, 0, 1);
640
652
  aoInvProjection = mat4.create();
641
653
  aoProjScale = new Float32Array(2);
654
+ /** Where this frame's jitter put the occlusion, in the composite's uv. See `uAoOffset`. */
655
+ aoOffset = new Float32Array(2);
642
656
  /**
643
657
  * Whether this frame has already been resolved to the canvas.
644
658
  *
@@ -730,12 +744,15 @@ export class WebGL2Renderer {
730
744
  * world. See `prepareStaticPointShadows`.
731
745
  */
732
746
  pointShadowArray = null;
733
- /** The photometric atlas, allocated on first use. See `setIesProfiles`. */
734
- iesTexture = null;
735
- /** Rows in that atlas. One until a consumer loads a profile, which is the row of ones. */
747
+ /** The fixture atlas — cookies over profiles — allocated on first use. See `fixtureAtlas.ts`. */
748
+ fixtureTexture = null;
749
+ /** The profiles as packed, kept so a new set of cookies can rebuild the atlas around them. */
750
+ iesAtlas = packIesAtlas([]);
751
+ /** The cookie images, kept so a new set of profiles can rebuild the atlas around them. */
752
+ cookieImages = [];
753
+ /** Rows of profile in the atlas. One until a consumer loads a profile, which is the row of ones. */
736
754
  iesRows = 1;
737
- cookieTexture = null;
738
- /** Tiles in the cookie atlas. 0 is the white placeholder and the whole off path. */
755
+ /** Cookie tiles in the atlas. 0 is none, and the whole off path. */
739
756
  cookieTiles = 0;
740
757
  /** Horizontal planes each profile occupies. 1 unless something asymmetric was loaded. */
741
758
  iesPlanes = 1;
@@ -1036,7 +1053,7 @@ export class WebGL2Renderer {
1036
1053
  if (cutout !== null) {
1037
1054
  gl.useProgram(this.depthInstancedCutoutProgram);
1038
1055
  this.bindCutoutDepth(this.depthInstancedCutoutUniforms, cutout);
1039
- gpuBatch.mesh.drawInstances(gl, count);
1056
+ gpuBatch.draw(gl, count);
1040
1057
  if (twoSided && this.depthPassCullsFaces)
1041
1058
  gl.enable(gl.CULL_FACE);
1042
1059
  gl.useProgram(this.depthProgram);
@@ -1049,7 +1066,7 @@ export class WebGL2Renderer {
1049
1066
  gl.uniform1i(u['uPeelShadowLayer'] ?? null, this.activeDepthPeel ? 1 : 0);
1050
1067
  if (this.activeDepthPeel)
1051
1068
  gl.uniform1i(u['uPreviousShadowMap'] ?? null, 0);
1052
- gpuBatch.mesh.drawInstances(gl, count);
1069
+ gpuBatch.draw(gl, count);
1053
1070
  if (twoSided && this.depthPassCullsFaces)
1054
1071
  gl.enable(gl.CULL_FACE);
1055
1072
  gl.useProgram(this.depthProgram);
@@ -1305,6 +1322,13 @@ export class WebGL2Renderer {
1305
1322
  occluded(bounds, model) {
1306
1323
  return this.occlusion?.occluded(bounds, model) ?? false;
1307
1324
  }
1325
+ /**
1326
+ * Whether a world-space box is wholly behind the declared occluders — the tighter question for
1327
+ * something that is a box, such as a region. False with no occlusion buffer. See `occluded`.
1328
+ */
1329
+ occludedBox(min, max) {
1330
+ return this.occlusion?.occludedBox(min, max) ?? false;
1331
+ }
1308
1332
  /* -- Registered passes ---------------------------------------------------------------- */
1309
1333
  passes = createPassRegistry();
1310
1334
  /** Built once so invoking somebody else's pass allocates nothing. See `PassDefinition.draw`. */
@@ -1478,80 +1502,57 @@ export class WebGL2Renderer {
1478
1502
  * `createSurfaceTexture` and the material maps take.
1479
1503
  */
1480
1504
  setSpotCookies(images) {
1481
- const { gl } = this;
1482
- this.cookieTiles = images.length;
1483
- if (this.cookieTexture === null)
1484
- this.cookieTexture = gl.createTexture();
1485
- gl.activeTexture(gl.TEXTURE0 + COOKIE_ATLAS_TEXTURE_UNIT);
1486
- gl.bindTexture(gl.TEXTURE_2D, this.cookieTexture);
1487
- if (images.length === 0) {
1488
- /* White, not black: this multiplies a light's colour. */
1489
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255, 255]));
1490
- }
1491
- else {
1492
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, COOKIE_TILE * images.length, COOKIE_TILE, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
1493
- for (let tile = 0; tile < images.length; tile++) {
1494
- const image = images[tile];
1495
- if (image === undefined)
1496
- continue;
1497
- gl.texSubImage2D(gl.TEXTURE_2D, 0, tile * COOKIE_TILE, 0, gl.RGBA, gl.UNSIGNED_BYTE, image);
1498
- }
1499
- }
1500
- /* Linear and clamped: a cookie is an image, and the shader's own inset is what keeps a tile
1501
- from reaching its neighbour — clamping alone cannot, the seam being interior to the row. */
1502
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1503
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1504
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1505
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1506
- gl.activeTexture(gl.TEXTURE0);
1505
+ this.cookieImages = images.slice();
1506
+ this.buildFixtureAtlas();
1507
1507
  }
1508
1508
  setIesProfiles(profiles) {
1509
+ this.iesAtlas = packIesAtlas(profiles);
1510
+ this.buildFixtureAtlas();
1511
+ }
1512
+ /**
1513
+ * The fixture atlas from the profiles and cookies in hand: the profiles' rows at the bottom, the
1514
+ * cookies' tiles on top, one `RGBA16F` texture. See `fixtureAtlas.ts` for the layout.
1515
+ *
1516
+ * **Half float, matching what the other backend must use**: a 32-bit float texture is not
1517
+ * filterable in core WebGPU, and both tables are curves and images that want linear filtering.
1518
+ * Linear and clamped: a profile is sampled between two angles, and a cookie's own inset is what
1519
+ * keeps a tile from its neighbour — clamping alone cannot, the seam being interior to the row.
1520
+ */
1521
+ buildFixtureAtlas() {
1509
1522
  const { gl } = this;
1510
- const atlas = packIesAtlas(profiles);
1511
- if (this.iesTexture === null)
1512
- this.iesTexture = gl.createTexture();
1513
- gl.activeTexture(gl.TEXTURE0 + IES_ATLAS_TEXTURE_UNIT);
1514
- gl.bindTexture(gl.TEXTURE_2D, this.iesTexture);
1515
- /*
1516
- * `LINEAR` across a row and clamped, unlike the froxel table's `NEAREST`. This one *is* a
1517
- * curve rather than data addressed by index: interpolating between two columns is sampling
1518
- * the distribution between two angles, which is exactly what is wanted, and clamping stops a
1519
- * row bleeding into its neighbour at the edges.
1520
- */
1523
+ const layout = fixtureAtlasLayout(this.iesAtlas.height, this.cookieImages.length);
1524
+ if (this.fixtureTexture === null)
1525
+ this.fixtureTexture = gl.createTexture();
1526
+ gl.activeTexture(gl.TEXTURE0 + FIXTURE_ATLAS_TEXTURE_UNIT);
1527
+ gl.bindTexture(gl.TEXTURE_2D, this.fixtureTexture);
1528
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, layout.width, layout.height, 0, gl.RGBA, gl.FLOAT, fixtureAtlasTexels(layout, this.iesAtlas));
1529
+ for (let tile = 0; tile < this.cookieImages.length; tile++) {
1530
+ const image = this.cookieImages[tile];
1531
+ if (image === undefined)
1532
+ continue;
1533
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, tile * COOKIE_TILE, 0, gl.RGBA, gl.HALF_FLOAT, image);
1534
+ }
1521
1535
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1522
1536
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1523
1537
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1524
1538
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1525
- /*
1526
- * One channel, because a row holds one number per angle and the other three would be three
1527
- * quarters of the upload spent on nothing — and **half float rather than full**, matching what
1528
- * the other backend must use: a 32-bit float texture is not filterable in core WebGPU, and this
1529
- * row is a curve that wants linear filtering. Keeping the two formats the same is the
1530
- * 2026-08-13 rule rather than tidiness; the driver converts the float data on the way in.
1531
- */
1532
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.R16F, atlas.width, atlas.height, 0, gl.RED, gl.FLOAT, atlas.data);
1533
- this.iesRows = atlas.height;
1534
- this.iesPlanes = atlas.planes;
1539
+ this.iesRows = this.iesAtlas.height;
1540
+ this.iesPlanes = this.iesAtlas.planes;
1541
+ this.cookieTiles = this.cookieImages.length;
1535
1542
  gl.bindTexture(gl.TEXTURE_2D, null);
1543
+ gl.activeTexture(gl.TEXTURE0);
1536
1544
  }
1537
- /** Bind the atlas and say how many rows it has. Always, for the reason `bindClusters` gives. */
1545
+ /** Bind the fixture atlas and say what is in it. Always, for the reason `bindClusters` gives. */
1538
1546
  bindIesAtlas(u) {
1539
1547
  const { gl } = this;
1540
- if (this.iesTexture === null)
1541
- this.setIesProfiles([]);
1542
- gl.activeTexture(gl.TEXTURE0 + IES_ATLAS_TEXTURE_UNIT);
1543
- gl.bindTexture(gl.TEXTURE_2D, this.iesTexture);
1544
- gl.uniform1i(u['uIesAtlas'] ?? null, IES_ATLAS_TEXTURE_UNIT);
1548
+ /* Built on demand rather than at construction, for the reason the grade's placeholder is. */
1549
+ if (this.fixtureTexture === null)
1550
+ this.buildFixtureAtlas();
1551
+ gl.activeTexture(gl.TEXTURE0 + FIXTURE_ATLAS_TEXTURE_UNIT);
1552
+ gl.bindTexture(gl.TEXTURE_2D, this.fixtureTexture);
1553
+ gl.uniform1i(u['uFixtureAtlas'] ?? null, FIXTURE_ATLAS_TEXTURE_UNIT);
1545
1554
  gl.uniform1f(u['uIesAtlasRows'] ?? null, this.iesRows);
1546
1555
  gl.uniform1f(u['uIesPlaneCount'] ?? null, this.iesPlanes);
1547
- /* Built on demand rather than at construction, for the reason the grade's placeholder is: a
1548
- scene that never projects a cookie should not carry a texture, and a declared sampler still
1549
- needs a complete one. */
1550
- if (this.cookieTexture === null)
1551
- this.setSpotCookies([]);
1552
- gl.activeTexture(gl.TEXTURE0 + COOKIE_ATLAS_TEXTURE_UNIT);
1553
- gl.bindTexture(gl.TEXTURE_2D, this.cookieTexture);
1554
- gl.uniform1i(u['uCookieAtlas'] ?? null, COOKIE_ATLAS_TEXTURE_UNIT);
1555
1556
  gl.uniform1f(u['uCookieTiles'] ?? null, this.cookieTiles);
1556
1557
  gl.activeTexture(gl.TEXTURE0);
1557
1558
  }
@@ -2180,10 +2181,8 @@ export class WebGL2Renderer {
2180
2181
  gl.deleteVertexArray(this.skyVao);
2181
2182
  if (this.panelBuffer !== null)
2182
2183
  gl.deleteBuffer(this.panelBuffer);
2183
- if (this.cookieTexture !== null)
2184
- gl.deleteTexture(this.cookieTexture);
2185
- if (this.iesTexture !== null)
2186
- gl.deleteTexture(this.iesTexture);
2184
+ if (this.fixtureTexture !== null)
2185
+ gl.deleteTexture(this.fixtureTexture);
2187
2186
  if (this.clusterTexture !== null)
2188
2187
  gl.deleteTexture(this.clusterTexture);
2189
2188
  }
@@ -2331,6 +2330,15 @@ export class WebGL2Renderer {
2331
2330
  createSurfaceTexture(source, options = {}) {
2332
2331
  return new SurfaceTexture(this.gl, source, options);
2333
2332
  }
2333
+ /**
2334
+ * Upload several images of one size as the layers of one array, for a merged mesh that names a
2335
+ * layer per vertex (`MeshData.layers`). One material binding then covers every image, which is
2336
+ * what makes a block wearing forty facades one draw. Every layer must be the same size, and the
2337
+ * refusal names the first that is not.
2338
+ */
2339
+ createSurfaceTextureArray(sources, options = {}) {
2340
+ return new SurfaceTexture(this.gl, sources, options);
2341
+ }
2334
2342
  /**
2335
2343
  * Replace a surface texture's pixels, keeping the GPU object and its sampler state.
2336
2344
  *
@@ -2444,7 +2452,7 @@ export class WebGL2Renderer {
2444
2452
  * page-faulted an RDNA4 card and wedged the device. See `emptyTexture.ts`.
2445
2453
  */
2446
2454
  gl.activeTexture(gl.TEXTURE0 + NORMAL_TEXTURE_UNIT);
2447
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
2455
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
2448
2456
  if (normal !== null)
2449
2457
  normal.bind(gl, NORMAL_TEXTURE_UNIT);
2450
2458
  gl.uniform1i(u['uNormalMap'] ?? null, NORMAL_TEXTURE_UNIT);
@@ -2462,7 +2470,7 @@ export class WebGL2Renderer {
2462
2470
  */
2463
2471
  const orm = material?.orm ?? null;
2464
2472
  gl.activeTexture(gl.TEXTURE0 + ORM_TEXTURE_UNIT);
2465
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
2473
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
2466
2474
  if (orm !== null)
2467
2475
  orm.bind(gl, ORM_TEXTURE_UNIT);
2468
2476
  gl.uniform1i(u['uOrmMap'] ?? null, ORM_TEXTURE_UNIT);
@@ -2480,7 +2488,7 @@ export class WebGL2Renderer {
2480
2488
  */
2481
2489
  const emissiveMap = material?.emissive ?? null;
2482
2490
  gl.activeTexture(gl.TEXTURE0 + EMISSIVE_TEXTURE_UNIT);
2483
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
2491
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
2484
2492
  if (emissiveMap !== null)
2485
2493
  emissiveMap.bind(gl, EMISSIVE_TEXTURE_UNIT);
2486
2494
  gl.uniform1i(u['uEmissiveMap'] ?? null, EMISSIVE_TEXTURE_UNIT);
@@ -2496,7 +2504,21 @@ export class WebGL2Renderer {
2496
2504
  */
2497
2505
  gl.uniform2f(u['uUvScale'] ?? null, material?.uScale ?? 1, material?.vScale ?? 1);
2498
2506
  gl.uniform1i(u['uDoubleSided'] ?? null, material?.doubleSided === true ? 1 : 0);
2507
+ /* The albedo's effects table, or the stand-in the shader reads as none. Before the early
2508
+ return, so a material with no albedo still leaves a complete texture on the unit. */
2499
2509
  if (albedo === null) {
2510
+ gl.activeTexture(gl.TEXTURE0 + SURFACE_EFFECTS_TEXTURE_UNIT);
2511
+ gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
2512
+ }
2513
+ else {
2514
+ albedo.bindEffects(gl, SURFACE_EFFECTS_TEXTURE_UNIT, this.emptyTexture2D);
2515
+ }
2516
+ gl.uniform1i(u['uSurfaceEffects'] ?? null, SURFACE_EFFECTS_TEXTURE_UNIT);
2517
+ if (albedo === null) {
2518
+ /* The array stand-in, for the reason the maps above take one: a sampler reads its unit's
2519
+ array target, and with nothing there it is incomplete. */
2520
+ gl.activeTexture(gl.TEXTURE0 + SURFACE_TEXTURE_UNIT);
2521
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
2500
2522
  gl.uniform1i(u['uAlbedoEnabled'] ?? null, 0);
2501
2523
  return;
2502
2524
  }
@@ -2531,7 +2553,7 @@ export class WebGL2Renderer {
2531
2553
  return;
2532
2554
  }
2533
2555
  gl.activeTexture(gl.TEXTURE0 + SURFACE_TEXTURE_UNIT);
2534
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
2556
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
2535
2557
  }
2536
2558
  /**
2537
2559
  * A pass-level scale over the grain the geometry itself declared, 0 to 1.
@@ -2559,8 +2581,7 @@ export class WebGL2Renderer {
2559
2581
  if (this.contextLost)
2560
2582
  return;
2561
2583
  this.materials.dirty();
2562
- this.useFlatProgram();
2563
- this.gl.uniform1f(this.flatUniforms['uReflectivity'] ?? null, Math.min(1, Math.max(0, amount)));
2584
+ this.writeFlatFloat('uReflectivity', Math.min(1, Math.max(0, amount)));
2564
2585
  }
2565
2586
  /**
2566
2587
  * How bright the environment the surfaces drawn next reflect is. 1 is the default and identity.
@@ -2578,8 +2599,7 @@ export class WebGL2Renderer {
2578
2599
  if (this.contextLost)
2579
2600
  return;
2580
2601
  this.materials.dirty();
2581
- this.useFlatProgram();
2582
- this.gl.uniform1f(this.flatUniforms['uEnvironmentGain'] ?? null, Math.max(0, gain));
2602
+ this.writeFlatFloat('uEnvironmentGain', Math.max(0, gain));
2583
2603
  }
2584
2604
  /**
2585
2605
  * Kept as a no-op that says so once, because it was a stand-in for a term that now exists.
@@ -2606,8 +2626,51 @@ export class WebGL2Renderer {
2606
2626
  if (this.contextLost)
2607
2627
  return;
2608
2628
  this.materials.dirty();
2629
+ this.writeFlatFloat('uGrain', Math.min(1, Math.max(0, amount)));
2630
+ }
2631
+ /**
2632
+ * One float of material state, into every flat program.
2633
+ *
2634
+ * **Every one, for `setMaterial`'s reason**: an instanced or skinned draw runs a program of its
2635
+ * own and is entitled to the material its caller set before it. These setters wrote the plain
2636
+ * program alone until 2026-09-30, so on this backend a batch drew with the pass defaults — grain
2637
+ * full, no reflection, no relief — while the other backend, whose material block every variant
2638
+ * reads, drew what was asked.
2639
+ */
2640
+ writeFlatFloat(name, value) {
2641
+ for (const target of this.flatTargets) {
2642
+ this.gl.useProgram(target.program);
2643
+ this.gl.uniform1f(target.uniforms[name] ?? null, value);
2644
+ }
2645
+ this.useFlatProgram();
2646
+ }
2647
+ /**
2648
+ * How much of the surfaces drawn next survives a screen-door dither, for crossfading two levels
2649
+ * of detail of one thing.
2650
+ *
2651
+ * 0 is off and is what `bindMeshPass` restores. A positive amount keeps that share of the
2652
+ * pattern's cells, a negative one keeps the complement, so the level fading in drawn at `t` and
2653
+ * the one fading out at `-t` cover every pixel exactly once — no hole, no pixel drawn twice.
2654
+ * Material state like `setSurfaceGrain`; instanced draws read it too, and shadow casters, which
2655
+ * are a pass of their own, do not. See `uDither` in `flat/preamble.ts` for what it costs.
2656
+ */
2657
+ setDitherFade(amount) {
2658
+ if (this.contextLost)
2659
+ return;
2660
+ this.materials.dirty();
2661
+ this.useFlatProgram();
2662
+ this.ditherFade = Math.min(1, Math.max(-1, amount));
2663
+ /* Every flat program, as `setMaterial` writes them: an instanced or skinned draw is entitled
2664
+ to the fade the caller set before it, and a second program holding none draws whole. */
2665
+ for (const target of this.flatTargets) {
2666
+ this.gl.useProgram(target.program);
2667
+ this.writeMode(target.uniforms);
2668
+ }
2609
2669
  this.useFlatProgram();
2610
- this.gl.uniform1f(this.flatUniforms['uGrain'] ?? null, Math.min(1, Math.max(0, amount)));
2670
+ }
2671
+ /** `uWriteMode`: the order-independent weight and the dither, which share one uniform row. */
2672
+ writeMode(u) {
2673
+ this.gl.uniform2f(u['uWriteMode'] ?? null, this.oitWeight, this.ditherFade);
2611
2674
  }
2612
2675
  /**
2613
2676
  * How strong the microscopic relief on the surfaces drawn next is, and how coarse.
@@ -2636,10 +2699,8 @@ export class WebGL2Renderer {
2636
2699
  if (this.contextLost)
2637
2700
  return;
2638
2701
  this.materials.dirty();
2639
- this.useFlatProgram();
2640
- const u = this.flatUniforms;
2641
- this.gl.uniform1f(u['uRelief'] ?? null, Math.min(1, Math.max(0, amount)));
2642
- this.gl.uniform1f(u['uReliefCycles'] ?? null, Math.max(0.01, cyclesPerMetre));
2702
+ this.writeFlatFloat('uRelief', Math.min(1, Math.max(0, amount)));
2703
+ this.writeFlatFloat('uReliefCycles', Math.max(0.01, cyclesPerMetre));
2643
2704
  }
2644
2705
  /**
2645
2706
  * How hard the bound surface texture's own luminance turns the shading normal, 0 for not at all.
@@ -2670,8 +2731,7 @@ export class WebGL2Renderer {
2670
2731
  if (this.contextLost)
2671
2732
  return;
2672
2733
  this.materials.dirty();
2673
- this.useFlatProgram();
2674
- this.gl.uniform1f(this.flatUniforms['uTextureRelief'] ?? null, Number.isFinite(scale) ? scale : 0);
2734
+ this.writeFlatFloat('uTextureRelief', Number.isFinite(scale) ? scale : 0);
2675
2735
  }
2676
2736
  /**
2677
2737
  * Release a mesh's GPU buffers.
@@ -2698,8 +2758,7 @@ export class WebGL2Renderer {
2698
2758
  if (this.contextLost)
2699
2759
  return;
2700
2760
  this.materials.dirty();
2701
- this.useFlatProgram();
2702
- this.gl.uniform1f(this.flatUniforms['uEmissiveGain'] ?? null, gain);
2761
+ this.writeFlatFloat('uEmissiveGain', gain);
2703
2762
  }
2704
2763
  /* -- Instanced meshes --------------------------------------------------------------- */
2705
2764
  /** The instanced program, compiled on first use. See `ensureInstancedProgram`. */
@@ -2738,7 +2797,7 @@ export class WebGL2Renderer {
2738
2797
  * One batch per mesh: the attributes bind to the mesh's own vertex array, of which a mesh has
2739
2798
  * one. `Mesh.attachInstances` is what refuses a second, and says why.
2740
2799
  */
2741
- createInstanced(mesh, capacity) {
2800
+ createInstanced(mesh, capacity, options = {}) {
2742
2801
  /* The same refusal as the other backend, for the same reason: the instanced attributes take
2743
2802
  the two locations a skinned mesh's joints and weights already occupy. */
2744
2803
  if (mesh.isSkinned) {
@@ -2746,7 +2805,7 @@ export class WebGL2Renderer {
2746
2805
  'locations the joint indices and weights occupy, and this mesh supplies them.');
2747
2806
  }
2748
2807
  this.ensureInstancedProgram();
2749
- return new InstancedBatch(this.gl, mesh, capacity);
2808
+ return new InstancedBatch(this.gl, mesh, capacity, options.cull ?? false);
2750
2809
  }
2751
2810
  /** Push placement and colour. Only the live prefix. */
2752
2811
  uploadInstanced(batch, data) {
@@ -2790,6 +2849,12 @@ export class WebGL2Renderer {
2790
2849
  const count = Math.min(data.count, batch.capacity);
2791
2850
  if (count === 0)
2792
2851
  return;
2852
+ /* A culling batch's camera draw: the whole batch against the view and the occluders first,
2853
+ then instance by instance where it survives. Blended draws keep every instance and their
2854
+ order. See `InstancedOptions.cull`. */
2855
+ const culling = !blend && batch.cull;
2856
+ if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion))
2857
+ return;
2793
2858
  this.drawBudget.ask();
2794
2859
  this.ensureInstancedProgram();
2795
2860
  const program = this.flatInstancedProgram;
@@ -2800,7 +2865,8 @@ export class WebGL2Renderer {
2800
2865
  gl.useProgram(program);
2801
2866
  gl.uniform1i(u['uHasTangents'] ?? null, mesh.hasTangents ? 1 : 0);
2802
2867
  const lit = options.lit ?? true;
2803
- const fog = options.fog ?? true;
2868
+ /* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
2869
+ const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
2804
2870
  const toneMapped = options.toneMapped ?? true;
2805
2871
  /* A blended batch shows what is behind it — refraction or glass — as a single draw does, and
2806
2872
  counts a material of its own for it by the rule both backends share (`materialChanges.ts`). */
@@ -2831,13 +2897,16 @@ export class WebGL2Renderer {
2831
2897
  */
2832
2898
  if (!lit)
2833
2899
  gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
2834
- if (!fog)
2835
- gl.uniform1i(u['uFogEnabled'] ?? null, 0);
2900
+ if (fog !== FOG_RECEDE)
2901
+ gl.uniform1i(u['uFogEnabled'] ?? null, fog);
2836
2902
  if (!toneMapped)
2837
2903
  gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.gradeCode(), 1));
2838
2904
  if (blend) {
2839
2905
  gl.enable(gl.BLEND);
2840
- gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
2906
+ if (options.additive === true)
2907
+ gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ZERO, gl.ONE);
2908
+ else
2909
+ gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
2841
2910
  if (!depthWrite)
2842
2911
  gl.depthMask(false);
2843
2912
  }
@@ -2852,7 +2921,10 @@ export class WebGL2Renderer {
2852
2921
  for both: without it a batch's far faces were culled here alone. */
2853
2922
  if (this.materialDoubleSided)
2854
2923
  gl.disable(gl.CULL_FACE);
2855
- mesh.drawInstances(gl, count);
2924
+ if (culling && cullsInstances(count, mesh.indexCount))
2925
+ batch.drawCulled(gl, data, this.frustum);
2926
+ else
2927
+ batch.draw(gl, count);
2856
2928
  if (this.materialDoubleSided)
2857
2929
  gl.enable(gl.CULL_FACE);
2858
2930
  if (layer > 0) {
@@ -2874,8 +2946,8 @@ export class WebGL2Renderer {
2874
2946
  this.unbindSeeThrough(u);
2875
2947
  if (!lit)
2876
2948
  gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
2877
- if (!fog)
2878
- gl.uniform1i(u['uFogEnabled'] ?? null, 1);
2949
+ if (fog !== FOG_RECEDE)
2950
+ gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
2879
2951
  if (!toneMapped)
2880
2952
  gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
2881
2953
  if (own)
@@ -3997,7 +4069,7 @@ export class WebGL2Renderer {
3997
4069
  const cutout = cutoutOf(material);
3998
4070
  this.bindGlassTint(cutout !== null ? programs.instancedCutout : programs.instanced, glass, cutout);
3999
4071
  gl.disable(gl.CULL_FACE);
4000
- gpuBatch.mesh.drawInstances(gl, count);
4072
+ gpuBatch.draw(gl, count);
4001
4073
  if (this.depthPassCullsFaces)
4002
4074
  gl.enable(gl.CULL_FACE);
4003
4075
  if (cutout !== null)
@@ -5247,11 +5319,20 @@ export class WebGL2Renderer {
5247
5319
  metres into a radius in UV. */
5248
5320
  this.aoProjScale[0] = projection[0] ?? 1;
5249
5321
  this.aoProjScale[1] = projection[5] ?? 1;
5322
+ /* The occlusion read where this frame's jitter put it (`uAoOffset`): a texel stands at its
5323
+ index plus a half less the jitter, in the drawing buffer the jitter was spread over. */
5324
+ this.aoOffset[0] = this.temporalJittering
5325
+ ? this.temporalJitterX / Math.max(1, this.gl.drawingBufferWidth)
5326
+ : 0;
5327
+ this.aoOffset[1] = this.temporalJittering
5328
+ ? this.temporalJitterY / Math.max(1, this.gl.drawingBufferHeight)
5329
+ : 0;
5250
5330
  ao = {
5251
5331
  strength: aoStrength,
5252
5332
  radius: this.quality.ambientOcclusionRadius,
5253
5333
  projScale: this.aoProjScale,
5254
5334
  invProjection: this.aoInvProjection,
5335
+ offset: this.aoOffset,
5255
5336
  };
5256
5337
  }
5257
5338
  /*
@@ -5375,7 +5456,8 @@ export class WebGL2Renderer {
5375
5456
  this.oit.accumulateOit(this.canvas.width, this.canvas.height, oitDepth, (weighted) => {
5376
5457
  this.oitReplaying = true;
5377
5458
  this.useFlatProgram();
5378
- this.gl.uniform1f(flat['uOitWeighted'] ?? null, weighted ? 1 : 0);
5459
+ this.oitWeight = weighted ? 1 : 0;
5460
+ this.writeMode(flat);
5379
5461
  this.translucentQueue.replay((draw) => {
5380
5462
  options.lit = draw.lit;
5381
5463
  options.fog = draw.fog;
@@ -5392,7 +5474,8 @@ export class WebGL2Renderer {
5392
5474
  this.drawTranslucentMesh(draw.mesh, draw.model, draw.opacity, options);
5393
5475
  });
5394
5476
  /* Back to zero for every other draw in the frame, exactly as `uOpacity` and `uTint` are. */
5395
- this.gl.uniform1f(flat['uOitWeighted'] ?? null, 0);
5477
+ this.oitWeight = 0;
5478
+ this.writeMode(flat);
5396
5479
  this.oitReplaying = false;
5397
5480
  });
5398
5481
  this.sceneTarget?.bind();
@@ -5551,7 +5634,15 @@ export class WebGL2Renderer {
5551
5634
  * A reflection pass binds its own camera and is skipped: blurring toward a mirror's
5552
5635
  * previous view is not what a viewer moved through.
5553
5636
  */
5554
- if (!this.reflectionPassActive && !this.probePassActive) {
5637
+ /*
5638
+ * **Not after the present**: a mesh pass bound then is the interface's, over a picture already
5639
+ * resolved. Deciding the frame again drew the interface jittered with nothing to resolve it,
5640
+ * and opened a second temporal frame, so the sequence stepped twice a frame, the world saw every
5641
+ * other position, and the anti-flicker's period never started.
5642
+ */
5643
+ if (this.framePresented)
5644
+ this.temporalJittering = false;
5645
+ if (!this.reflectionPassActive && !this.probePassActive && !this.framePresented) {
5555
5646
  this.frameViewProj = camera.viewProjection;
5556
5647
  /* Copied rather than referenced: a caller moves its camera in place, and the decal pass
5557
5648
  reads this at the end of the frame. */
@@ -5657,6 +5748,19 @@ export class WebGL2Renderer {
5657
5748
  this.lightField = field;
5658
5749
  return field;
5659
5750
  }
5751
+ /**
5752
+ * Hand a world's DriftLight volume to the renderer: every fixed light of a city, summed offline by
5753
+ * `bakeDenseField`, standing in past the frame's exact choice. Replaces any field made before, of
5754
+ * either kind; the scene calls `field.follow(buffer.complete, ...)` each frame as it would for
5755
+ * `createLightField`, and marks the lights the volume summed `inLightField` so none is counted
5756
+ * twice. See `driftLight/worldLightField.ts`.
5757
+ */
5758
+ createWorldLightField(volume, options = {}) {
5759
+ this.forgetLightFieldVolumes();
5760
+ const field = new WorldLightField(volume, options);
5761
+ this.lightField = field;
5762
+ return field;
5763
+ }
5660
5764
  /** Let go of the field and its volumes; the scene's lights are all exact again. */
5661
5765
  disposeLightField() {
5662
5766
  this.forgetLightFieldVolumes();
@@ -5676,14 +5780,14 @@ export class WebGL2Renderer {
5676
5780
  */
5677
5781
  uploadLightField() {
5678
5782
  const field = this.lightField;
5679
- if (field === null || this.driftAtlas !== null || !field.ready || field.layout.count === 0) {
5783
+ if (field === null || this.driftAtlas !== null || !field.ready || field.empty) {
5680
5784
  return;
5681
5785
  }
5682
5786
  const { gl } = this;
5683
- const [ix, iy, iz] = field.layout.dims;
5787
+ const [ix, iy, iz] = field.indexDims;
5684
5788
  const index = gl.createTexture();
5685
5789
  gl.bindTexture(gl.TEXTURE_3D, index);
5686
- gl.texImage3D(gl.TEXTURE_3D, 0, gl.R32UI, ix, iy, iz, 0, gl.RED_INTEGER, gl.UNSIGNED_INT, field.layout.index);
5790
+ gl.texImage3D(gl.TEXTURE_3D, 0, gl.R32UI, ix, iy, iz, 0, gl.RED_INTEGER, gl.UNSIGNED_INT, field.index);
5687
5791
  gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
5688
5792
  gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
5689
5793
  const [aw, ah, ad] = field.atlasSize;
@@ -5811,6 +5915,10 @@ export class WebGL2Renderer {
5811
5915
  gl.uniform1i(u['uFogEnabled'] ?? null, 1);
5812
5916
  /* Grain on by default, so every scene written before it was a choice looks unchanged. */
5813
5917
  gl.uniform1f(u['uGrain'] ?? null, 1);
5918
+ /* No dither and no weighting: every draw whole, until a crossfade asks. See `setDitherFade`. */
5919
+ this.oitWeight = 0;
5920
+ this.ditherFade = 0;
5921
+ gl.uniform2f(u['uWriteMode'] ?? null, 0, 0);
5814
5922
  /* No environment reflection unless a caller asks, so existing scenes are unchanged. */
5815
5923
  gl.uniform1f(u['uReflectivity'] ?? null, 0);
5816
5924
  /* 1, not 0: this one is a multiplier and its identity is one. See `setEnvironmentGain`. */
@@ -5820,11 +5928,11 @@ export class WebGL2Renderer {
5820
5928
  gl.uniform1f(u['uReliefCycles'] ?? null, 60);
5821
5929
  /* And no relief read off a surface texture either, for the same reason. */
5822
5930
  gl.uniform1f(u['uTextureRelief'] ?? null, 0);
5823
- gl.uniform1i(u['uClipEnabled'] ?? null, this.reflectionPassActive ? 1 : 0);
5931
+ /* The mirror's plane in a reflection pass, and zero — which clips nothing — in every other. */
5824
5932
  const reflection = this.planarReflection;
5825
- if (this.reflectionPassActive && reflection !== null) {
5826
- gl.uniform4fv(u['uClipPlane'] ?? null, reflection.clipPlane);
5827
- }
5933
+ gl.uniform4fv(u['uClipPlane'] ?? null, this.reflectionPassActive && reflection !== null ? reflection.clipPlane : NO_CLIP_PLANE);
5934
+ resolveSurfaceScene(env, this.surfaceScene);
5935
+ gl.uniform4fv(u['uSurfaceScene'] ?? null, this.surfaceScene);
5828
5936
  bindAtmosphere(gl, u, env, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.quality.underwaterAtmosphere);
5829
5937
  gl.uniform1f(u['uEmissiveGain'] ?? null, env.emissiveGain);
5830
5938
  gl.uniform1f(u['uNightFactor'] ?? null, env.nightFactor);
@@ -5981,9 +6089,13 @@ export class WebGL2Renderer {
5981
6089
  }
5982
6090
  this.bindDriftLight(u, camera.position);
5983
6091
  gl.activeTexture(gl.TEXTURE0 + SURFACE_TEXTURE_UNIT);
5984
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6092
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
5985
6093
  gl.uniform1i(u['uAlbedo'] ?? null, SURFACE_TEXTURE_UNIT);
5986
6094
  gl.uniform1i(u['uAlbedoEnabled'] ?? null, 0);
6095
+ /* No albedo, so no effects table: the stand-in, which the shader reads as none. */
6096
+ gl.activeTexture(gl.TEXTURE0 + SURFACE_EFFECTS_TEXTURE_UNIT);
6097
+ gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6098
+ gl.uniform1i(u['uSurfaceEffects'] ?? null, SURFACE_EFFECTS_TEXTURE_UNIT);
5987
6099
  gl.uniform2f(u['uUvScale'] ?? null, 1, 1);
5988
6100
  gl.uniform1f(u['uAlbedoCutout'] ?? null, 0);
5989
6101
  gl.uniform1i(u['uDoubleSided'] ?? null, 0);
@@ -6007,18 +6119,18 @@ export class WebGL2Renderer {
6007
6119
  * fetch a sampler's descriptor before it evaluates the branch that would have skipped the read.
6008
6120
  */
6009
6121
  gl.activeTexture(gl.TEXTURE0 + NORMAL_TEXTURE_UNIT);
6010
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6122
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
6011
6123
  gl.uniform1i(u['uNormalMap'] ?? null, NORMAL_TEXTURE_UNIT);
6012
6124
  gl.uniform1f(u['uNormalStrength'] ?? null, 0);
6013
6125
  /* And the ORM map, for the reason directly above and the one above that. */
6014
6126
  gl.activeTexture(gl.TEXTURE0 + ORM_TEXTURE_UNIT);
6015
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6127
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
6016
6128
  gl.uniform1i(u['uOrmMap'] ?? null, ORM_TEXTURE_UNIT);
6017
6129
  gl.uniform1i(u['uOrmEnabled'] ?? null, 0);
6018
6130
  gl.uniform3f(u['uOrmScale'] ?? null, 1, 1, 1);
6019
6131
  /* And the emissive map. The scale goes back to one, which is the identity it multiplies by. */
6020
6132
  gl.activeTexture(gl.TEXTURE0 + EMISSIVE_TEXTURE_UNIT);
6021
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6133
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
6022
6134
  gl.uniform1i(u['uEmissiveMap'] ?? null, EMISSIVE_TEXTURE_UNIT);
6023
6135
  gl.uniform1i(u['uEmissiveMapEnabled'] ?? null, 0);
6024
6136
  gl.uniform3f(u['uEmissiveScale'] ?? null, 1, 1, 1);
@@ -6304,7 +6416,7 @@ export class WebGL2Renderer {
6304
6416
  * The two buffers are two blend states over the same geometry, so the set has to survive until
6305
6417
  * the end of the pass. Everything is copied — see `TranslucentQueue`.
6306
6418
  */
6307
- if (this.oitActive && !this.oitReplaying) {
6419
+ if (this.oitActive && !this.oitReplaying && options.additive !== true) {
6308
6420
  this.translucentQueue.record(mesh, model, opacity, options);
6309
6421
  return;
6310
6422
  }
@@ -6322,12 +6434,13 @@ export class WebGL2Renderer {
6322
6434
  * drew with, and only a caller that asks pays for the branch that skips them.
6323
6435
  */
6324
6436
  const lit = options.lit ?? true;
6325
- const fog = options.fog ?? true;
6437
+ /* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
6438
+ const fog = fogModeOf(options.fog ?? true, options.additive === true);
6326
6439
  const toneMapped = options.toneMapped ?? true;
6327
6440
  if (!lit)
6328
6441
  gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
6329
- if (!fog)
6330
- gl.uniform1i(u['uFogEnabled'] ?? null, 0);
6442
+ if (fog !== FOG_RECEDE)
6443
+ gl.uniform1i(u['uFogEnabled'] ?? null, fog);
6331
6444
  /* `1` is sRGB alone: the conversion without the curve, which is what dropping the tone map
6332
6445
  means rather than dropping the whole transform. `Math.min` rather than a literal, so a
6333
6446
  renderer asked for `none` stays at none and one asked for `srgb` is already there. */
@@ -6368,7 +6481,11 @@ export class WebGL2Renderer {
6368
6481
  const ownsState = !this.oitReplaying;
6369
6482
  if (ownsState) {
6370
6483
  gl.enable(gl.BLEND);
6371
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
6484
+ /* Added, where asked: colour times alpha onto what is there, and the target's alpha kept. */
6485
+ if (options.additive === true)
6486
+ gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ZERO, gl.ONE);
6487
+ else
6488
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
6372
6489
  }
6373
6490
  /*
6374
6491
  * The two depth options, both scoped to this draw and handed straight back — the same
@@ -6408,8 +6525,8 @@ export class WebGL2Renderer {
6408
6525
  cannot inherit a material from the draw before it. */
6409
6526
  if (!lit)
6410
6527
  gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
6411
- if (!fog)
6412
- gl.uniform1i(u['uFogEnabled'] ?? null, 1);
6528
+ if (fog !== FOG_RECEDE)
6529
+ gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
6413
6530
  if (!toneMapped)
6414
6531
  gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
6415
6532
  if (own)