@driftengine/core 4.5.0 → 4.6.1

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 (209) hide show
  1. package/CHANGELOG.json +62 -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 +249 -128
  33. package/dist/render/backend/webgpu/buffers.d.ts +17 -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/motionPass.js +2 -1
  48. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  49. package/dist/render/backend/webgpu/renderer.d.ts +100 -10
  50. package/dist/render/backend/webgpu/renderer.js +478 -214
  51. package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
  52. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
  53. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
  54. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +17 -3
  55. package/dist/render/backend/webgpu/surfaceTexturePass.js +77 -38
  56. package/dist/render/driftLight/bake.d.ts +7 -0
  57. package/dist/render/driftLight/bake.js +61 -56
  58. package/dist/render/driftLight/denseField.d.ts +46 -0
  59. package/dist/render/driftLight/denseField.js +109 -0
  60. package/dist/render/driftLight/lightField.d.ts +10 -5
  61. package/dist/render/driftLight/lightField.js +25 -19
  62. package/dist/render/driftLight/presence.d.ts +50 -0
  63. package/dist/render/driftLight/presence.js +43 -0
  64. package/dist/render/driftLight/uniforms.d.ts +3 -3
  65. package/dist/render/driftLight/uniforms.js +5 -6
  66. package/dist/render/driftLight/worldLightField.d.ts +41 -0
  67. package/dist/render/driftLight/worldLightField.js +66 -0
  68. package/dist/render/fixtureAtlas.d.ts +20 -0
  69. package/dist/render/fixtureAtlas.js +49 -0
  70. package/dist/render/instanceCull.d.ts +63 -0
  71. package/dist/render/instanceCull.js +160 -0
  72. package/dist/render/instances.d.ts +15 -0
  73. package/dist/render/lightBudget.d.ts +22 -33
  74. package/dist/render/lightBudget.js +24 -35
  75. package/dist/render/lightGrid.d.ts +42 -0
  76. package/dist/render/lightGrid.js +87 -0
  77. package/dist/render/mesh.d.ts +23 -24
  78. package/dist/render/mesh.js +56 -68
  79. package/dist/render/occlusion.d.ts +10 -0
  80. package/dist/render/occlusion.js +36 -7
  81. package/dist/render/pointLightSelection.d.ts +7 -0
  82. package/dist/render/pointLightSelection.js +29 -4
  83. package/dist/render/rainField.d.ts +13 -0
  84. package/dist/render/rainField.js +27 -5
  85. package/dist/render/recon/lateDepth.d.ts +11 -0
  86. package/dist/render/recon/lateDepth.js +70 -0
  87. package/dist/render/recon/resolve.d.ts +34 -4
  88. package/dist/render/recon/resolve.js +161 -20
  89. package/dist/render/rendererHarness.js +4 -0
  90. package/dist/render/sceneTarget.d.ts +2 -0
  91. package/dist/render/sceneTarget.js +2 -0
  92. package/dist/render/shaders/depth.js +12 -9
  93. package/dist/render/shaders/flat/driftLight.d.ts +1 -1
  94. package/dist/render/shaders/flat/driftLight.js +32 -7
  95. package/dist/render/shaders/flat/index.js +5 -3
  96. package/dist/render/shaders/flat/main.d.ts +1 -1
  97. package/dist/render/shaders/flat/main.js +75 -23
  98. package/dist/render/shaders/flat/preamble.js +75 -20
  99. package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
  100. package/dist/render/shaders/flat/surfaceEffects.js +152 -0
  101. package/dist/render/shaders/generated/depth.wgsl.d.ts +8 -8
  102. package/dist/render/shaders/generated/depth.wgsl.js +8 -8
  103. package/dist/render/shaders/generated/flat.wgsl.d.ts +1320 -1320
  104. package/dist/render/shaders/generated/flat.wgsl.js +1359 -1345
  105. package/dist/render/shaders/generated/lightVolume.wgsl.js +18 -4
  106. package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
  107. package/dist/render/shaders/generated/rush.wgsl.js +21 -16
  108. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
  109. package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
  110. package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
  111. package/dist/render/shaders/generated/text.wgsl.js +1 -1
  112. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
  113. package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
  114. package/dist/render/shaders/recon/lateDepth.wgsl.js +38 -13
  115. package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
  116. package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
  117. package/dist/render/shaders/rush.js +11 -1
  118. package/dist/render/shaders/sdfText.d.ts +1 -1
  119. package/dist/render/shaders/sdfText.js +3 -2
  120. package/dist/render/shaders/text.d.ts +1 -1
  121. package/dist/render/shaders/text.js +18 -9
  122. package/dist/render/surfaceEffects.d.ts +83 -0
  123. package/dist/render/surfaceEffects.js +75 -0
  124. package/dist/render/surfaceScene.d.ts +8 -0
  125. package/dist/render/surfaceScene.js +8 -0
  126. package/dist/render/surfaceTexture.d.ts +18 -2
  127. package/dist/render/surfaceTexture.js +68 -15
  128. package/dist/render/temporalAa.d.ts +25 -6
  129. package/dist/render/temporalAa.js +45 -15
  130. package/dist/render/textureSource.d.ts +30 -0
  131. package/dist/render/textureSource.js +53 -0
  132. package/dist/render/vertexDefaults.js +2 -0
  133. package/dist/world/hlod.d.ts +104 -0
  134. package/dist/world/hlod.js +214 -0
  135. package/package.json +6 -6
  136. package/src/geometry/boxCsg.ts +186 -0
  137. package/src/geometry/csg.ts +384 -0
  138. package/src/geometry/meshBuilder.ts +15 -0
  139. package/src/geometry/meshConcat.ts +1 -0
  140. package/src/geometry/pixelFont.ts +2 -0
  141. package/src/geometry/solid.ts +189 -0
  142. package/src/geometry/solidFlat.ts +522 -0
  143. package/src/geometry/solidHarness.ts +83 -0
  144. package/src/geometry/solidNormals.ts +103 -0
  145. package/src/geometry/solidRound.ts +443 -0
  146. package/src/geometry/solidWriter.ts +58 -0
  147. package/src/index.ts +51 -1
  148. package/src/render/backend/api.ts +21 -2
  149. package/src/render/backend/drawFog.ts +31 -0
  150. package/src/render/backend/materialChanges.ts +9 -2
  151. package/src/render/backend/webgl2/instanced.ts +80 -14
  152. package/src/render/backend/webgl2/renderer.ts +311 -150
  153. package/src/render/backend/webgpu/buffers.ts +49 -3
  154. package/src/render/backend/webgpu/depthPass.ts +5 -1
  155. package/src/render/backend/webgpu/drawCommand.ts +6 -0
  156. package/src/render/backend/webgpu/flatPass.ts +41 -11
  157. package/src/render/backend/webgpu/glassTintPass.ts +2 -1
  158. package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
  159. package/src/render/backend/webgpu/instanced.ts +21 -3
  160. package/src/render/backend/webgpu/latePass.ts +12 -3
  161. package/src/render/backend/webgpu/motionPass.ts +2 -1
  162. package/src/render/backend/webgpu/renderer.ts +556 -239
  163. package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
  164. package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
  165. package/src/render/backend/webgpu/surfaceTexturePass.ts +90 -50
  166. package/src/render/driftLight/bake.ts +85 -62
  167. package/src/render/driftLight/denseField.ts +149 -0
  168. package/src/render/driftLight/lightField.ts +33 -18
  169. package/src/render/driftLight/presence.ts +69 -0
  170. package/src/render/driftLight/uniforms.ts +8 -9
  171. package/src/render/driftLight/worldLightField.ts +83 -0
  172. package/src/render/fixtureAtlas.ts +63 -0
  173. package/src/render/instanceCull.ts +205 -0
  174. package/src/render/instances.ts +16 -0
  175. package/src/render/lightBudget.ts +25 -36
  176. package/src/render/lightGrid.ts +130 -0
  177. package/src/render/mesh.ts +64 -72
  178. package/src/render/occlusion.ts +51 -7
  179. package/src/render/pointLightSelection.ts +72 -4
  180. package/src/render/rainField.ts +39 -5
  181. package/src/render/recon/lateDepth.ts +83 -0
  182. package/src/render/recon/resolve.ts +206 -20
  183. package/src/render/rendererHarness.ts +4 -0
  184. package/src/render/sceneTarget.ts +4 -0
  185. package/src/render/shaders/depth.ts +12 -9
  186. package/src/render/shaders/flat/driftLight.ts +32 -7
  187. package/src/render/shaders/flat/index.ts +5 -3
  188. package/src/render/shaders/flat/main.ts +75 -23
  189. package/src/render/shaders/flat/preamble.ts +75 -20
  190. package/src/render/shaders/flat/surfaceEffects.ts +152 -0
  191. package/src/render/shaders/generated/depth.wgsl.ts +8 -8
  192. package/src/render/shaders/generated/flat.wgsl.ts +1361 -1345
  193. package/src/render/shaders/generated/lightVolume.wgsl.ts +20 -4
  194. package/src/render/shaders/generated/rush.wgsl.ts +21 -16
  195. package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
  196. package/src/render/shaders/generated/text.wgsl.ts +1 -1
  197. package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
  198. package/src/render/shaders/recon/lateDepth.wgsl.ts +38 -13
  199. package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
  200. package/src/render/shaders/rush.ts +11 -1
  201. package/src/render/shaders/sdfText.ts +3 -2
  202. package/src/render/shaders/text.ts +18 -9
  203. package/src/render/surfaceEffects.ts +132 -0
  204. package/src/render/surfaceScene.ts +16 -0
  205. package/src/render/surfaceTexture.ts +110 -19
  206. package/src/render/temporalAa.ts +78 -14
  207. package/src/render/textureSource.ts +72 -0
  208. package/src/render/vertexDefaults.ts +2 -0
  209. 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) {
@@ -2785,11 +2844,18 @@ export class WebGL2Renderer {
2785
2844
  if (this.contextLost)
2786
2845
  return;
2787
2846
  const mesh = batch.mesh;
2788
- if (!mesh.complete)
2847
+ /* Nor a batch of a mesh with nothing in it: see `Mesh.indexCount`. */
2848
+ if (!mesh.complete || mesh.indexCount === 0)
2789
2849
  return;
2790
2850
  const count = Math.min(data.count, batch.capacity);
2791
2851
  if (count === 0)
2792
2852
  return;
2853
+ /* A culling batch's camera draw: the whole batch against the view and the occluders first,
2854
+ then instance by instance where it survives. Blended draws keep every instance and their
2855
+ order. See `InstancedOptions.cull`. */
2856
+ const culling = !blend && batch.cull;
2857
+ if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion))
2858
+ return;
2793
2859
  this.drawBudget.ask();
2794
2860
  this.ensureInstancedProgram();
2795
2861
  const program = this.flatInstancedProgram;
@@ -2800,7 +2866,8 @@ export class WebGL2Renderer {
2800
2866
  gl.useProgram(program);
2801
2867
  gl.uniform1i(u['uHasTangents'] ?? null, mesh.hasTangents ? 1 : 0);
2802
2868
  const lit = options.lit ?? true;
2803
- const fog = options.fog ?? true;
2869
+ /* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
2870
+ const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
2804
2871
  const toneMapped = options.toneMapped ?? true;
2805
2872
  /* A blended batch shows what is behind it — refraction or glass — as a single draw does, and
2806
2873
  counts a material of its own for it by the rule both backends share (`materialChanges.ts`). */
@@ -2831,13 +2898,16 @@ export class WebGL2Renderer {
2831
2898
  */
2832
2899
  if (!lit)
2833
2900
  gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
2834
- if (!fog)
2835
- gl.uniform1i(u['uFogEnabled'] ?? null, 0);
2901
+ if (fog !== FOG_RECEDE)
2902
+ gl.uniform1i(u['uFogEnabled'] ?? null, fog);
2836
2903
  if (!toneMapped)
2837
2904
  gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.gradeCode(), 1));
2838
2905
  if (blend) {
2839
2906
  gl.enable(gl.BLEND);
2840
- gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
2907
+ if (options.additive === true)
2908
+ gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ZERO, gl.ONE);
2909
+ else
2910
+ gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
2841
2911
  if (!depthWrite)
2842
2912
  gl.depthMask(false);
2843
2913
  }
@@ -2852,7 +2922,10 @@ export class WebGL2Renderer {
2852
2922
  for both: without it a batch's far faces were culled here alone. */
2853
2923
  if (this.materialDoubleSided)
2854
2924
  gl.disable(gl.CULL_FACE);
2855
- mesh.drawInstances(gl, count);
2925
+ if (culling && cullsInstances(count, mesh.indexCount))
2926
+ batch.drawCulled(gl, data, this.frustum);
2927
+ else
2928
+ batch.draw(gl, count);
2856
2929
  if (this.materialDoubleSided)
2857
2930
  gl.enable(gl.CULL_FACE);
2858
2931
  if (layer > 0) {
@@ -2874,8 +2947,8 @@ export class WebGL2Renderer {
2874
2947
  this.unbindSeeThrough(u);
2875
2948
  if (!lit)
2876
2949
  gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
2877
- if (!fog)
2878
- gl.uniform1i(u['uFogEnabled'] ?? null, 1);
2950
+ if (fog !== FOG_RECEDE)
2951
+ gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
2879
2952
  if (!toneMapped)
2880
2953
  gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
2881
2954
  if (own)
@@ -3997,7 +4070,7 @@ export class WebGL2Renderer {
3997
4070
  const cutout = cutoutOf(material);
3998
4071
  this.bindGlassTint(cutout !== null ? programs.instancedCutout : programs.instanced, glass, cutout);
3999
4072
  gl.disable(gl.CULL_FACE);
4000
- gpuBatch.mesh.drawInstances(gl, count);
4073
+ gpuBatch.draw(gl, count);
4001
4074
  if (this.depthPassCullsFaces)
4002
4075
  gl.enable(gl.CULL_FACE);
4003
4076
  if (cutout !== null)
@@ -5247,11 +5320,20 @@ export class WebGL2Renderer {
5247
5320
  metres into a radius in UV. */
5248
5321
  this.aoProjScale[0] = projection[0] ?? 1;
5249
5322
  this.aoProjScale[1] = projection[5] ?? 1;
5323
+ /* The occlusion read where this frame's jitter put it (`uAoOffset`): a texel stands at its
5324
+ index plus a half less the jitter, in the drawing buffer the jitter was spread over. */
5325
+ this.aoOffset[0] = this.temporalJittering
5326
+ ? this.temporalJitterX / Math.max(1, this.gl.drawingBufferWidth)
5327
+ : 0;
5328
+ this.aoOffset[1] = this.temporalJittering
5329
+ ? this.temporalJitterY / Math.max(1, this.gl.drawingBufferHeight)
5330
+ : 0;
5250
5331
  ao = {
5251
5332
  strength: aoStrength,
5252
5333
  radius: this.quality.ambientOcclusionRadius,
5253
5334
  projScale: this.aoProjScale,
5254
5335
  invProjection: this.aoInvProjection,
5336
+ offset: this.aoOffset,
5255
5337
  };
5256
5338
  }
5257
5339
  /*
@@ -5375,7 +5457,8 @@ export class WebGL2Renderer {
5375
5457
  this.oit.accumulateOit(this.canvas.width, this.canvas.height, oitDepth, (weighted) => {
5376
5458
  this.oitReplaying = true;
5377
5459
  this.useFlatProgram();
5378
- this.gl.uniform1f(flat['uOitWeighted'] ?? null, weighted ? 1 : 0);
5460
+ this.oitWeight = weighted ? 1 : 0;
5461
+ this.writeMode(flat);
5379
5462
  this.translucentQueue.replay((draw) => {
5380
5463
  options.lit = draw.lit;
5381
5464
  options.fog = draw.fog;
@@ -5392,7 +5475,8 @@ export class WebGL2Renderer {
5392
5475
  this.drawTranslucentMesh(draw.mesh, draw.model, draw.opacity, options);
5393
5476
  });
5394
5477
  /* Back to zero for every other draw in the frame, exactly as `uOpacity` and `uTint` are. */
5395
- this.gl.uniform1f(flat['uOitWeighted'] ?? null, 0);
5478
+ this.oitWeight = 0;
5479
+ this.writeMode(flat);
5396
5480
  this.oitReplaying = false;
5397
5481
  });
5398
5482
  this.sceneTarget?.bind();
@@ -5551,7 +5635,15 @@ export class WebGL2Renderer {
5551
5635
  * A reflection pass binds its own camera and is skipped: blurring toward a mirror's
5552
5636
  * previous view is not what a viewer moved through.
5553
5637
  */
5554
- if (!this.reflectionPassActive && !this.probePassActive) {
5638
+ /*
5639
+ * **Not after the present**: a mesh pass bound then is the interface's, over a picture already
5640
+ * resolved. Deciding the frame again drew the interface jittered with nothing to resolve it,
5641
+ * and opened a second temporal frame, so the sequence stepped twice a frame, the world saw every
5642
+ * other position, and the anti-flicker's period never started.
5643
+ */
5644
+ if (this.framePresented)
5645
+ this.temporalJittering = false;
5646
+ if (!this.reflectionPassActive && !this.probePassActive && !this.framePresented) {
5555
5647
  this.frameViewProj = camera.viewProjection;
5556
5648
  /* Copied rather than referenced: a caller moves its camera in place, and the decal pass
5557
5649
  reads this at the end of the frame. */
@@ -5657,6 +5749,19 @@ export class WebGL2Renderer {
5657
5749
  this.lightField = field;
5658
5750
  return field;
5659
5751
  }
5752
+ /**
5753
+ * Hand a world's DriftLight volume to the renderer: every fixed light of a city, summed offline by
5754
+ * `bakeDenseField`, standing in past the frame's exact choice. Replaces any field made before, of
5755
+ * either kind; the scene calls `field.follow(buffer.complete, ...)` each frame as it would for
5756
+ * `createLightField`, and marks the lights the volume summed `inLightField` so none is counted
5757
+ * twice. See `driftLight/worldLightField.ts`.
5758
+ */
5759
+ createWorldLightField(volume, options = {}) {
5760
+ this.forgetLightFieldVolumes();
5761
+ const field = new WorldLightField(volume, options);
5762
+ this.lightField = field;
5763
+ return field;
5764
+ }
5660
5765
  /** Let go of the field and its volumes; the scene's lights are all exact again. */
5661
5766
  disposeLightField() {
5662
5767
  this.forgetLightFieldVolumes();
@@ -5676,14 +5781,14 @@ export class WebGL2Renderer {
5676
5781
  */
5677
5782
  uploadLightField() {
5678
5783
  const field = this.lightField;
5679
- if (field === null || this.driftAtlas !== null || !field.ready || field.layout.count === 0) {
5784
+ if (field === null || this.driftAtlas !== null || !field.ready || field.empty) {
5680
5785
  return;
5681
5786
  }
5682
5787
  const { gl } = this;
5683
- const [ix, iy, iz] = field.layout.dims;
5788
+ const [ix, iy, iz] = field.indexDims;
5684
5789
  const index = gl.createTexture();
5685
5790
  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);
5791
+ gl.texImage3D(gl.TEXTURE_3D, 0, gl.R32UI, ix, iy, iz, 0, gl.RED_INTEGER, gl.UNSIGNED_INT, field.index);
5687
5792
  gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
5688
5793
  gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
5689
5794
  const [aw, ah, ad] = field.atlasSize;
@@ -5811,6 +5916,10 @@ export class WebGL2Renderer {
5811
5916
  gl.uniform1i(u['uFogEnabled'] ?? null, 1);
5812
5917
  /* Grain on by default, so every scene written before it was a choice looks unchanged. */
5813
5918
  gl.uniform1f(u['uGrain'] ?? null, 1);
5919
+ /* No dither and no weighting: every draw whole, until a crossfade asks. See `setDitherFade`. */
5920
+ this.oitWeight = 0;
5921
+ this.ditherFade = 0;
5922
+ gl.uniform2f(u['uWriteMode'] ?? null, 0, 0);
5814
5923
  /* No environment reflection unless a caller asks, so existing scenes are unchanged. */
5815
5924
  gl.uniform1f(u['uReflectivity'] ?? null, 0);
5816
5925
  /* 1, not 0: this one is a multiplier and its identity is one. See `setEnvironmentGain`. */
@@ -5820,11 +5929,11 @@ export class WebGL2Renderer {
5820
5929
  gl.uniform1f(u['uReliefCycles'] ?? null, 60);
5821
5930
  /* And no relief read off a surface texture either, for the same reason. */
5822
5931
  gl.uniform1f(u['uTextureRelief'] ?? null, 0);
5823
- gl.uniform1i(u['uClipEnabled'] ?? null, this.reflectionPassActive ? 1 : 0);
5932
+ /* The mirror's plane in a reflection pass, and zero — which clips nothing — in every other. */
5824
5933
  const reflection = this.planarReflection;
5825
- if (this.reflectionPassActive && reflection !== null) {
5826
- gl.uniform4fv(u['uClipPlane'] ?? null, reflection.clipPlane);
5827
- }
5934
+ gl.uniform4fv(u['uClipPlane'] ?? null, this.reflectionPassActive && reflection !== null ? reflection.clipPlane : NO_CLIP_PLANE);
5935
+ resolveSurfaceScene(env, this.surfaceScene);
5936
+ gl.uniform4fv(u['uSurfaceScene'] ?? null, this.surfaceScene);
5828
5937
  bindAtmosphere(gl, u, env, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.quality.underwaterAtmosphere);
5829
5938
  gl.uniform1f(u['uEmissiveGain'] ?? null, env.emissiveGain);
5830
5939
  gl.uniform1f(u['uNightFactor'] ?? null, env.nightFactor);
@@ -5981,9 +6090,13 @@ export class WebGL2Renderer {
5981
6090
  }
5982
6091
  this.bindDriftLight(u, camera.position);
5983
6092
  gl.activeTexture(gl.TEXTURE0 + SURFACE_TEXTURE_UNIT);
5984
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6093
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
5985
6094
  gl.uniform1i(u['uAlbedo'] ?? null, SURFACE_TEXTURE_UNIT);
5986
6095
  gl.uniform1i(u['uAlbedoEnabled'] ?? null, 0);
6096
+ /* No albedo, so no effects table: the stand-in, which the shader reads as none. */
6097
+ gl.activeTexture(gl.TEXTURE0 + SURFACE_EFFECTS_TEXTURE_UNIT);
6098
+ gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6099
+ gl.uniform1i(u['uSurfaceEffects'] ?? null, SURFACE_EFFECTS_TEXTURE_UNIT);
5987
6100
  gl.uniform2f(u['uUvScale'] ?? null, 1, 1);
5988
6101
  gl.uniform1f(u['uAlbedoCutout'] ?? null, 0);
5989
6102
  gl.uniform1i(u['uDoubleSided'] ?? null, 0);
@@ -6007,18 +6120,18 @@ export class WebGL2Renderer {
6007
6120
  * fetch a sampler's descriptor before it evaluates the branch that would have skipped the read.
6008
6121
  */
6009
6122
  gl.activeTexture(gl.TEXTURE0 + NORMAL_TEXTURE_UNIT);
6010
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6123
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
6011
6124
  gl.uniform1i(u['uNormalMap'] ?? null, NORMAL_TEXTURE_UNIT);
6012
6125
  gl.uniform1f(u['uNormalStrength'] ?? null, 0);
6013
6126
  /* And the ORM map, for the reason directly above and the one above that. */
6014
6127
  gl.activeTexture(gl.TEXTURE0 + ORM_TEXTURE_UNIT);
6015
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6128
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
6016
6129
  gl.uniform1i(u['uOrmMap'] ?? null, ORM_TEXTURE_UNIT);
6017
6130
  gl.uniform1i(u['uOrmEnabled'] ?? null, 0);
6018
6131
  gl.uniform3f(u['uOrmScale'] ?? null, 1, 1, 1);
6019
6132
  /* And the emissive map. The scale goes back to one, which is the identity it multiplies by. */
6020
6133
  gl.activeTexture(gl.TEXTURE0 + EMISSIVE_TEXTURE_UNIT);
6021
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6134
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
6022
6135
  gl.uniform1i(u['uEmissiveMap'] ?? null, EMISSIVE_TEXTURE_UNIT);
6023
6136
  gl.uniform1i(u['uEmissiveMapEnabled'] ?? null, 0);
6024
6137
  gl.uniform3f(u['uEmissiveScale'] ?? null, 1, 1, 1);
@@ -6103,8 +6216,9 @@ export class WebGL2Renderer {
6103
6216
  _previousModel = null) {
6104
6217
  if (this.contextLost)
6105
6218
  return;
6106
- /* Geometry that has not all arrived is not drawn. See `Mesh.complete`. */
6107
- if (!mesh.complete)
6219
+ /* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
6220
+ and `Mesh.indexCount`. */
6221
+ if (!mesh.complete || mesh.indexCount === 0)
6108
6222
  return;
6109
6223
  /*
6110
6224
  * Skipped where the flag asks and the bounds say so.
@@ -6294,8 +6408,9 @@ export class WebGL2Renderer {
6294
6408
  drawTranslucentMesh(mesh, model, opacity, options = {}) {
6295
6409
  if (this.contextLost)
6296
6410
  return;
6297
- /* Geometry that has not all arrived is not drawn. See `Mesh.complete`. */
6298
- if (!mesh.complete)
6411
+ /* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
6412
+ and `Mesh.indexCount`. */
6413
+ if (!mesh.complete || mesh.indexCount === 0)
6299
6414
  return;
6300
6415
  if (opacity <= 0)
6301
6416
  return;
@@ -6304,7 +6419,7 @@ export class WebGL2Renderer {
6304
6419
  * The two buffers are two blend states over the same geometry, so the set has to survive until
6305
6420
  * the end of the pass. Everything is copied — see `TranslucentQueue`.
6306
6421
  */
6307
- if (this.oitActive && !this.oitReplaying) {
6422
+ if (this.oitActive && !this.oitReplaying && options.additive !== true) {
6308
6423
  this.translucentQueue.record(mesh, model, opacity, options);
6309
6424
  return;
6310
6425
  }
@@ -6322,12 +6437,13 @@ export class WebGL2Renderer {
6322
6437
  * drew with, and only a caller that asks pays for the branch that skips them.
6323
6438
  */
6324
6439
  const lit = options.lit ?? true;
6325
- const fog = options.fog ?? true;
6440
+ /* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
6441
+ const fog = fogModeOf(options.fog ?? true, options.additive === true);
6326
6442
  const toneMapped = options.toneMapped ?? true;
6327
6443
  if (!lit)
6328
6444
  gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
6329
- if (!fog)
6330
- gl.uniform1i(u['uFogEnabled'] ?? null, 0);
6445
+ if (fog !== FOG_RECEDE)
6446
+ gl.uniform1i(u['uFogEnabled'] ?? null, fog);
6331
6447
  /* `1` is sRGB alone: the conversion without the curve, which is what dropping the tone map
6332
6448
  means rather than dropping the whole transform. `Math.min` rather than a literal, so a
6333
6449
  renderer asked for `none` stays at none and one asked for `srgb` is already there. */
@@ -6368,7 +6484,11 @@ export class WebGL2Renderer {
6368
6484
  const ownsState = !this.oitReplaying;
6369
6485
  if (ownsState) {
6370
6486
  gl.enable(gl.BLEND);
6371
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
6487
+ /* Added, where asked: colour times alpha onto what is there, and the target's alpha kept. */
6488
+ if (options.additive === true)
6489
+ gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ZERO, gl.ONE);
6490
+ else
6491
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
6372
6492
  }
6373
6493
  /*
6374
6494
  * The two depth options, both scoped to this draw and handed straight back — the same
@@ -6408,8 +6528,8 @@ export class WebGL2Renderer {
6408
6528
  cannot inherit a material from the draw before it. */
6409
6529
  if (!lit)
6410
6530
  gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
6411
- if (!fog)
6412
- gl.uniform1i(u['uFogEnabled'] ?? null, 1);
6531
+ if (fog !== FOG_RECEDE)
6532
+ gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
6413
6533
  if (!toneMapped)
6414
6534
  gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
6415
6535
  if (own)
@@ -6686,8 +6806,9 @@ export class WebGL2Renderer {
6686
6806
  drawLightVolume(mesh, model, camera, strength, length, spread, options = {}) {
6687
6807
  if (this.contextLost)
6688
6808
  return;
6689
- /* Geometry that has not all arrived is not drawn. See `Mesh.complete`. */
6690
- if (!mesh.complete)
6809
+ /* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
6810
+ and `Mesh.indexCount`. */
6811
+ if (!mesh.complete || mesh.indexCount === 0)
6691
6812
  return;
6692
6813
  const shown = options.medium === undefined
6693
6814
  ? strength