@driftengine/core 4.3.0 → 4.4.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 (218) hide show
  1. package/CHANGELOG.json +282 -0
  2. package/README.md +1 -1
  3. package/dist/cinematic/cameraPath.d.ts +53 -0
  4. package/dist/cinematic/cameraPath.js +118 -0
  5. package/dist/environment/daylight.d.ts +77 -0
  6. package/dist/environment/daylight.js +77 -0
  7. package/dist/geometry/meshConcat.d.ts +9 -0
  8. package/dist/geometry/meshConcat.js +101 -0
  9. package/dist/index.d.ts +12 -2
  10. package/dist/index.js +14 -1
  11. package/dist/render/ambientOcclusionPass.js +5 -1
  12. package/dist/render/backend/createRenderer.d.ts +13 -1
  13. package/dist/render/backend/createRenderer.js +6 -5
  14. package/dist/render/backend/webgl2/renderer.d.ts +147 -1
  15. package/dist/render/backend/webgl2/renderer.js +402 -23
  16. package/dist/render/backend/webgpu/buffers.js +46 -8
  17. package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
  18. package/dist/render/backend/webgpu/depthPass.js +75 -14
  19. package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
  20. package/dist/render/backend/webgpu/exposurePass.js +190 -0
  21. package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
  22. package/dist/render/backend/webgpu/fieldCompose.js +13 -0
  23. package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
  24. package/dist/render/backend/webgpu/flatPass.js +12 -5
  25. package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
  26. package/dist/render/backend/webgpu/particlePass.js +1 -0
  27. package/dist/render/backend/webgpu/postPass.d.ts +13 -3
  28. package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
  29. package/dist/render/backend/webgpu/probeBake.js +97 -7
  30. package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
  31. package/dist/render/backend/webgpu/probeTargets.js +180 -0
  32. package/dist/render/backend/webgpu/renderer.d.ts +174 -11
  33. package/dist/render/backend/webgpu/renderer.js +682 -252
  34. package/dist/render/backend/webgpu/scatterPass.js +7 -7
  35. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  36. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  37. package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
  38. package/dist/render/backend/webgpu/skyPass.js +5 -1
  39. package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
  40. package/dist/render/backend/webgpu/uniformRing.js +53 -3
  41. package/dist/render/bloomChain.d.ts +6 -0
  42. package/dist/render/bloomChain.js +8 -0
  43. package/dist/render/clusteredLights.d.ts +19 -11
  44. package/dist/render/clusteredLights.js +96 -45
  45. package/dist/render/cutoutCaster.d.ts +23 -0
  46. package/dist/render/cutoutCaster.js +20 -0
  47. package/dist/render/driftLight/bake.d.ts +34 -0
  48. package/dist/render/driftLight/bake.js +95 -0
  49. package/dist/render/driftLight/falloff.d.ts +16 -0
  50. package/dist/render/driftLight/falloff.js +23 -0
  51. package/dist/render/driftLight/layout.d.ts +59 -0
  52. package/dist/render/driftLight/layout.js +138 -0
  53. package/dist/render/driftLight/lightField.d.ts +108 -0
  54. package/dist/render/driftLight/lightField.js +179 -0
  55. package/dist/render/driftLight/uniforms.d.ts +28 -0
  56. package/dist/render/driftLight/uniforms.js +41 -0
  57. package/dist/render/driftLight/visibility.d.ts +18 -0
  58. package/dist/render/driftLight/visibility.js +47 -0
  59. package/dist/render/emptyTexture.d.ts +6 -0
  60. package/dist/render/emptyTexture.js +19 -0
  61. package/dist/render/exposurePass.d.ts +48 -0
  62. package/dist/render/exposurePass.js +172 -0
  63. package/dist/render/flameLight.d.ts +71 -0
  64. package/dist/render/flameLight.js +63 -0
  65. package/dist/render/gi/bounceLights.d.ts +34 -0
  66. package/dist/render/gi/bounceLights.js +66 -0
  67. package/dist/render/gi/fieldReach.d.ts +12 -0
  68. package/dist/render/gi/fieldReach.js +62 -0
  69. package/dist/render/gi/globalField.d.ts +8 -0
  70. package/dist/render/gi/globalField.js +31 -0
  71. package/dist/render/gi/probeTrace.d.ts +3 -1
  72. package/dist/render/gi/probeTrace.js +3 -1
  73. package/dist/render/gpuCapability.js +19 -4
  74. package/dist/render/lightBudget.d.ts +11 -0
  75. package/dist/render/lightBudget.js +11 -0
  76. package/dist/render/pointLightSelection.d.ts +16 -0
  77. package/dist/render/pointLightSelection.js +178 -83
  78. package/dist/render/pointShadowBudget.d.ts +12 -2
  79. package/dist/render/pointShadowBudget.js +16 -10
  80. package/dist/render/probeGrid.d.ts +11 -1
  81. package/dist/render/probeGrid.js +5 -2
  82. package/dist/render/probeSweeps.d.ts +45 -0
  83. package/dist/render/probeSweeps.js +77 -0
  84. package/dist/render/reflectionProbe.d.ts +43 -1
  85. package/dist/render/reflectionProbe.js +17 -3
  86. package/dist/render/renderQuality.d.ts +3 -1
  87. package/dist/render/rendererHarness.d.ts +9 -0
  88. package/dist/render/rendererHarness.js +18 -3
  89. package/dist/render/sceneTarget.d.ts +20 -0
  90. package/dist/render/sceneTarget.js +62 -1
  91. package/dist/render/shaders/ambientOcclusion.d.ts +63 -6
  92. package/dist/render/shaders/ambientOcclusion.js +188 -71
  93. package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
  94. package/dist/render/shaders/cutoutCoverage.js +40 -0
  95. package/dist/render/shaders/depth.d.ts +25 -0
  96. package/dist/render/shaders/depth.js +69 -3
  97. package/dist/render/shaders/exposure.d.ts +52 -0
  98. package/dist/render/shaders/exposure.js +124 -0
  99. package/dist/render/shaders/filmLook.d.ts +47 -0
  100. package/dist/render/shaders/filmLook.js +98 -0
  101. package/dist/render/shaders/flat/directionalShadow.js +35 -4
  102. package/dist/render/shaders/flat/driftLight.d.ts +29 -0
  103. package/dist/render/shaders/flat/driftLight.js +75 -0
  104. package/dist/render/shaders/flat/index.js +5 -0
  105. package/dist/render/shaders/flat/lobes.js +13 -0
  106. package/dist/render/shaders/flat/main.d.ts +1 -1
  107. package/dist/render/shaders/flat/main.js +68 -9
  108. package/dist/render/shaders/flat/preamble.js +23 -5
  109. package/dist/render/shaders/flat/probeGrid.js +46 -3
  110. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +8 -3
  111. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +8 -3
  112. package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
  113. package/dist/render/shaders/generated/depth.wgsl.js +100 -0
  114. package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
  115. package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
  116. package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
  117. package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
  118. package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
  119. package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
  120. package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
  121. package/dist/render/shaders/generated/rush.wgsl.js +40 -10
  122. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  123. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  124. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
  125. package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
  126. package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
  127. package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
  128. package/dist/render/shaders/globalMedium.js +4 -0
  129. package/dist/render/shaders/localExposure.d.ts +35 -0
  130. package/dist/render/shaders/localExposure.js +138 -0
  131. package/dist/render/shaders/prefilter.d.ts +1 -1
  132. package/dist/render/shaders/prefilter.js +6 -0
  133. package/dist/render/shaders/rush.js +33 -5
  134. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  135. package/dist/render/shaders/tangentFrame.js +15 -3
  136. package/dist/render/shaders/temporalResolve.js +39 -1
  137. package/dist/render/shadowCasters.d.ts +5 -3
  138. package/dist/render/shadowMap.js +13 -0
  139. package/dist/render/surfaceTexture.d.ts +5 -0
  140. package/package.json +6 -6
  141. package/scripts/gpuCompute.mjs +24 -3
  142. package/scripts/png.mjs +23 -12
  143. package/src/cinematic/cameraPath.ts +178 -0
  144. package/src/environment/daylight.ts +152 -0
  145. package/src/geometry/meshConcat.ts +100 -0
  146. package/src/index.ts +32 -0
  147. package/src/render/ambientOcclusionPass.ts +5 -1
  148. package/src/render/backend/createRenderer.ts +22 -6
  149. package/src/render/backend/webgl2/renderer.ts +439 -20
  150. package/src/render/backend/webgpu/buffers.ts +51 -8
  151. package/src/render/backend/webgpu/depthPass.ts +78 -10
  152. package/src/render/backend/webgpu/exposurePass.ts +234 -0
  153. package/src/render/backend/webgpu/fieldCompose.ts +15 -0
  154. package/src/render/backend/webgpu/flatPass.ts +10 -2
  155. package/src/render/backend/webgpu/particlePass.ts +9 -0
  156. package/src/render/backend/webgpu/probeBake.ts +143 -9
  157. package/src/render/backend/webgpu/probeTargets.ts +211 -0
  158. package/src/render/backend/webgpu/renderer.ts +808 -273
  159. package/src/render/backend/webgpu/scatterPass.ts +7 -7
  160. package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
  161. package/src/render/backend/webgpu/skyPass.ts +5 -1
  162. package/src/render/backend/webgpu/uniformRing.ts +49 -3
  163. package/src/render/bloomChain.ts +9 -0
  164. package/src/render/clusteredLights.ts +108 -63
  165. package/src/render/cutoutCaster.ts +44 -0
  166. package/src/render/driftLight/bake.ts +125 -0
  167. package/src/render/driftLight/falloff.ts +26 -0
  168. package/src/render/driftLight/layout.ts +177 -0
  169. package/src/render/driftLight/lightField.ts +227 -0
  170. package/src/render/driftLight/uniforms.ts +61 -0
  171. package/src/render/driftLight/visibility.ts +58 -0
  172. package/src/render/emptyTexture.ts +41 -0
  173. package/src/render/exposurePass.ts +186 -0
  174. package/src/render/flameLight.ts +135 -0
  175. package/src/render/gi/bounceLights.ts +68 -0
  176. package/src/render/gi/fieldReach.ts +74 -0
  177. package/src/render/gi/globalField.ts +39 -0
  178. package/src/render/gi/probeTrace.ts +3 -1
  179. package/src/render/gpuCapability.ts +18 -5
  180. package/src/render/lightBudget.ts +12 -0
  181. package/src/render/pointLightSelection.ts +191 -79
  182. package/src/render/pointShadowBudget.ts +16 -8
  183. package/src/render/probeGrid.ts +13 -2
  184. package/src/render/probeSweeps.ts +80 -0
  185. package/src/render/reflectionProbe.ts +54 -2
  186. package/src/render/renderQuality.ts +3 -1
  187. package/src/render/rendererHarness.ts +25 -3
  188. package/src/render/sceneTarget.ts +75 -1
  189. package/src/render/shaders/ambientOcclusion.ts +205 -71
  190. package/src/render/shaders/cutoutCoverage.ts +43 -0
  191. package/src/render/shaders/depth.ts +80 -3
  192. package/src/render/shaders/exposure.ts +128 -0
  193. package/src/render/shaders/filmLook.ts +111 -0
  194. package/src/render/shaders/flat/directionalShadow.ts +35 -4
  195. package/src/render/shaders/flat/driftLight.ts +75 -0
  196. package/src/render/shaders/flat/index.ts +5 -0
  197. package/src/render/shaders/flat/lobes.ts +13 -0
  198. package/src/render/shaders/flat/main.ts +68 -9
  199. package/src/render/shaders/flat/preamble.ts +23 -5
  200. package/src/render/shaders/flat/probeGrid.ts +46 -3
  201. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +8 -3
  202. package/src/render/shaders/generated/depth.wgsl.ts +103 -0
  203. package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
  204. package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
  205. package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
  206. package/src/render/shaders/generated/rush.wgsl.ts +40 -10
  207. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  208. package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
  209. package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
  210. package/src/render/shaders/globalMedium.ts +4 -0
  211. package/src/render/shaders/localExposure.ts +154 -0
  212. package/src/render/shaders/prefilter.ts +6 -0
  213. package/src/render/shaders/rush.ts +33 -5
  214. package/src/render/shaders/tangentFrame.ts +15 -3
  215. package/src/render/shaders/temporalResolve.ts +39 -1
  216. package/src/render/shadowCasters.ts +5 -3
  217. package/src/render/shadowMap.ts +14 -0
  218. package/src/render/surfaceTexture.ts +5 -0
@@ -1,7 +1,7 @@
1
1
  import { FrameBudget } from '../budget.js';
2
2
  import { MaterialChanges, ownsMaterial } from '../materialChanges.js';
3
3
  import { mat4 } from 'gl-matrix';
4
- import { DEPTH_OFFSET_SIGN, GL_DEPTH_REMAP, MAX_DEPTH_LAYER, depthClearFor, depthOffsetForLayer, GL_SHADOW_REMAP, REVERSED_DEPTH, SHADOW_DEPTH_CLEAR, glDepthFuncEqual, glShadowDepthFunc, } from '../../depthConvention.js';
4
+ import { GL_DEPTH_REMAP, MAX_DEPTH_LAYER, depthClearFor, depthOffsetForLayer, GL_SHADOW_REMAP, REVERSED_DEPTH, SHADOW_DEPTH_CLEAR, glDepthFuncEqual, glShadowDepthFunc, } from '../../depthConvention.js';
5
5
  import { PickableSet } from '../../pickable.js';
6
6
  import { TEMPORAL_HISTORY_BLEND, TemporalHistory, jitterOffset, jitterProjection, } from '../../temporalAa.js';
7
7
  import { DecalPass } from '../../decalPass.js';
@@ -27,8 +27,11 @@ import { compileProgram, uniformLocations } from '../../shader.js';
27
27
  import { FULL_LIGHT_BUDGET, nextLightBudget, planLightBudget, } from '../../uniformVectorBudget.js';
28
28
  import { FILM_FRAG, FILM_VERT } from '../../shaders/film.js';
29
29
  import { SceneTarget } from '../../sceneTarget.js';
30
- import { bloomProfileWarning } from '../../bloomChain.js';
31
- import { createEmptyTexture2D, createEmptyTexture2DArray, createEmptyTextureCube, } from '../../emptyTexture.js';
30
+ import { FILM_LOOK_WITHOUT_COMPOSITE } from '../../shaders/filmLook.js';
31
+ import { bloomProfileWarning, bloomThresholdOf } from '../../bloomChain.js';
32
+ import { createEmptyTexture2D, createEmptyTexture2DArray, createEmptyTexture3D, createEmptyTextureCube, } from '../../emptyTexture.js';
33
+ import { LightField } from '../../driftLight/lightField.js';
34
+ import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.js';
32
35
  import { flatFrag, flatVert } from '../../shaders/flat/index.js';
33
36
  import { MORPH_DELTA_TEXTURE_UNIT, SKIN_PALETTE_TEXTURE_UNIT } from '../../lightBudget.js';
34
37
  import { SkinPaletteTexture } from './skinPaletteTexture.js';
@@ -36,9 +39,10 @@ import { SkinPaletteTexture } from './skinPaletteTexture.js';
36
39
  /* The codes the shader reads, stated once in `vertexDefaults.ts` for both backends. */
37
40
  import { LIGHT_VOLUME_VERT, lightVolumeFrag } from '../../shaders/lightVolume.js';
38
41
  import { PANEL_FRAG, PANEL_VERT } from '../../shaders/panel.js';
39
- import { ReflectionProbe, cubeFaceProjection } from '../../reflectionProbe.js';
42
+ import { ReflectionProbe, cubeFaceProjection, probeFaceRange } from '../../reflectionProbe.js';
40
43
  import { EnvProbeArray } from '../../envProbeArray.js';
41
44
  import { ProbeGrid, SINGLE_PROBE, UNIT_STEP, WORLD_ORIGIN, sameGrid } from '../../probeGrid.js';
45
+ import { ProbeSweeps } from '../../probeSweeps.js';
42
46
  import { octahedralEdgeFor } from '../../prefilterEnvMap.js';
43
47
  import { EnvironmentPrefilterPass } from '../../prefilterPass.js';
44
48
  import { packIesAtlas } from '../../iesProfile.js';
@@ -46,14 +50,14 @@ import { MAX_AREA_LIGHTS } from '../../areaLights.js';
46
50
  import { areaShadowLayerCount, castingRange, createResolvedAreaShadows, firstAreaShadowLayer, AreaShadowSet, } from '../../areaShadowSet.js';
47
51
  import { equirectToCubeFaces } from '../../equirectToCube.js';
48
52
  import { SKY_FRAG, skyVertFor } from '../../shaders/sky.js';
49
- import { DEPTH_FRAG, DEPTH_INSTANCED_VERT, DEPTH_SKINNED_VERT, DEPTH_VERT, } from '../../shaders/depth.js';
53
+ import { DEPTH_CUTOUT_FRAG, DEPTH_CUTOUT_VERT, DEPTH_FRAG, DEPTH_INSTANCED_CUTOUT_VERT, DEPTH_INSTANCED_VERT, DEPTH_SKINNED_VERT, DEPTH_VERT, } from '../../shaders/depth.js';
50
54
  import { PointShadowArray } from '../../pointShadowArray.js';
51
55
  import { ShadowMap } from '../../shadowMap.js';
52
56
  import { POINT_SHADOW_SAMPLER, PointShadowSystem } from '../../pointShadowSystem.js';
53
57
  import { FACE_COUNT, PointShadowMap } from '../../pointShadowMap.js';
54
58
  import { createResolvedPointShadows, DEFAULT_SOURCE_RADIUS } from '../../pointShadowImage.js';
55
59
  import { createBakeScratch, runPointShadowBakes, selectCastingLights, } from '../../pointShadowBudget.js';
56
- import { DIRECTIONAL_SHADOW_UNITS, 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, COOKIE_TILE, IES_ATLAS_TEXTURE_UNIT, bindAreaLights, bindAreaShadows, openCones, } from '../../lightBudget.js';
60
+ import { DIRECTIONAL_SHADOW_UNITS, 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';
57
61
  import { resolveRenderQuality } from '../../renderQuality.js';
58
62
  import { drawingBufferSize } from '../../drawingBuffer.js';
59
63
  import { Mesh, createMeshIncremental } from '../../mesh.js';
@@ -78,6 +82,7 @@ import { OUTPUT_TRANSFORM_CODE } from '../../vertexDefaults.js';
78
82
  import { deviceSnappedCellSize } from '../../textLayout.js';
79
83
  import { WindStreakRenderer } from '../../windStreakRenderer.js';
80
84
  import { InstancedBatch } from './instanced.js';
85
+ import { cutoutOf } from '../../cutoutCaster.js';
81
86
  /**
82
87
  * How many frames `firstFrameSettled` will ask before it stops asking.
83
88
  *
@@ -451,6 +456,8 @@ export class WebGL2Renderer {
451
456
  panelProgram;
452
457
  panelUniforms;
453
458
  panelVao;
459
+ /** The unit quad behind `panelVao`, held so `dispose` can release it. */
460
+ panelBuffer;
454
461
  filmProgram;
455
462
  /**
456
463
  * The off-screen colour target, when screen effects are on. Null is not a failure —
@@ -481,6 +488,14 @@ export class WebGL2Renderer {
481
488
  /** Whether a palette is currently chosen. Distinct from whether one was ever uploaded. */
482
489
  skinPaletteSet = false;
483
490
  emptyTexture2D;
491
+ /** DriftLight's stand-ins while no field is whole: no brick anywhere, and no light. */
492
+ emptyDriftIndex;
493
+ emptyDriftAtlas;
494
+ /** The scene's DriftLight field, and its two volumes once it is whole. See `createLightField`. */
495
+ lightField = null;
496
+ driftIndex = null;
497
+ driftAtlas = null;
498
+ driftUniforms = createDriftLightUniforms();
484
499
  emptyTexture2DArray;
485
500
  emptyTextureCube;
486
501
  /** How hard the rush blurs this frame, and how far its outermost tap reaches. */
@@ -549,6 +564,8 @@ export class WebGL2Renderer {
549
564
  motionBlurScale = 1;
550
565
  /** How much of the bloom ceiling this frame takes. See `setBloom`. */
551
566
  bloomScale = 1;
567
+ /** In scene units, once `setBloom` has moved it; null reads the profile's. */
568
+ bloomThresholdSet = null;
552
569
  /**
553
570
  * This frame's exposure into the tone curve. See `setOutputExposure`.
554
571
  *
@@ -656,6 +673,15 @@ export class WebGL2Renderer {
656
673
  depthUniforms;
657
674
  depthInstancedProgram;
658
675
  depthInstancedUniforms;
676
+ /**
677
+ * The cutout casters' depth programs, rigid and instanced: a leaf or a chain link casts the shape
678
+ * in its texture's alpha, not its whole card. See `DEPTH_CUTOUT_FRAG`. Chosen per draw by
679
+ * `cutoutOf`, so an opaque caster never touches them.
680
+ */
681
+ depthCutoutProgram;
682
+ depthCutoutUniforms;
683
+ depthInstancedCutoutProgram;
684
+ depthInstancedCutoutUniforms;
659
685
  scatterDepthProgram;
660
686
  scatterDepthUniforms;
661
687
  shadowMap;
@@ -714,6 +740,10 @@ export class WebGL2Renderer {
714
740
  probeCapture;
715
741
  /** Every probe of the grid, as one array texture. Allocated when a grid is declared. */
716
742
  probeArray = null;
743
+ /** Which layers a bake writes and the shading reads. See `probeSweeps.ts`. */
744
+ probeSweeps = null;
745
+ /** `uProbeGridSets`, rewritten in place. */
746
+ probeSets = new Float32Array(3);
717
747
  /**
718
748
  * Where those probes stand. A single baked probe is a grid of one at that probe's own origin.
719
749
  *
@@ -739,6 +769,8 @@ export class WebGL2Renderer {
739
769
  warnedEnvironmentAmbient = false;
740
770
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
741
771
  probePassActive = false;
772
+ /** Whether the bake under way lights its faces by the grid. `ProbeBakeOptions.bounce`. */
773
+ probeBounce = false;
742
774
  /** The clip-control extension, or null where this context does not offer it. */
743
775
  clipControl = null;
744
776
  /**
@@ -876,6 +908,17 @@ export class WebGL2Renderer {
876
908
  */
877
909
  activeDepthViewProj = IDENTITY_MAT4;
878
910
  activeDepthPeel = false;
911
+ /**
912
+ * Whether a peel pass has filled the second layer since the first one last opened, which is
913
+ * what the frame samples it on: the rule the other backend's `peelFilled` states, held here too.
914
+ *
915
+ * **Not whether the map exists.** That was the test here, on the belief that a GL texture is
916
+ * born reading as the far plane; it is born zeroed, and a scene that built the layer and never
917
+ * peeled it sampled an occluder at the light. `ShadowMap` now clears itself at birth, so an
918
+ * unfilled layer would read as lit either way — this is what keeps the two backends one
919
+ * decision, and a frame that does not peel from paying a fetch per tap for nothing.
920
+ */
921
+ peelFilled = false;
879
922
  /**
880
923
  * Whether the depth pass this scatter draw interrupts wants back-face culling.
881
924
  *
@@ -886,6 +929,11 @@ export class WebGL2Renderer {
886
929
  * cubemap — so the pass states what it wants and this restores that.
887
930
  */
888
931
  depthPassCullsFaces = true;
932
+ /**
933
+ * Whether the bound material is seen from both faces, so its draws cull nothing. Toggled around
934
+ * each such draw and handed back, where the other backend bakes it into a pipeline.
935
+ */
936
+ materialDoubleSided = false;
889
937
  /**
890
938
  * The one sink handed to every caster enumeration.
891
939
  *
@@ -893,16 +941,38 @@ export class WebGL2Renderer {
893
941
  * per pass would allocate six objects per light per frame.
894
942
  */
895
943
  casterSink = {
896
- mesh: (mesh, model) => {
944
+ mesh: (mesh, model, material) => {
897
945
  this.shadowDrawBudget.ask();
898
946
  const { gl } = this;
947
+ /* A two-sided caster casts from whichever face the light sees: nothing culled. */
948
+ const twoSided = material?.doubleSided === true;
949
+ const cutout = cutoutOf(material);
950
+ if (cutout !== null) {
951
+ const u = this.depthCutoutUniforms;
952
+ gl.useProgram(this.depthCutoutProgram);
953
+ this.bindCutoutDepth(u, cutout);
954
+ gl.uniformMatrix4fv(u['uModel'] ?? null, false, model);
955
+ if (twoSided)
956
+ gl.disable(gl.CULL_FACE);
957
+ mesh.draw(gl);
958
+ if (twoSided && this.depthPassCullsFaces)
959
+ gl.enable(gl.CULL_FACE);
960
+ /* Hand the pass back as it was found: the next caster may be rigid and opaque. */
961
+ gl.useProgram(this.depthProgram);
962
+ this.restoreSurfaceTextureUnit();
963
+ return;
964
+ }
899
965
  gl.uniformMatrix4fv(this.depthUniforms['uModel'] ?? null, false, model);
900
966
  /*
901
967
  * The sink takes the opaque handle both backends share, and this renderer only ever
902
968
  * handed out its own `Mesh`. Narrowing is safe here for that reason and nowhere else:
903
969
  * a handle from the other backend would have to have crossed renderers to arrive.
904
970
  */
971
+ if (twoSided)
972
+ gl.disable(gl.CULL_FACE);
905
973
  mesh.draw(gl);
974
+ if (twoSided && this.depthPassCullsFaces)
975
+ gl.enable(gl.CULL_FACE);
906
976
  },
907
977
  /**
908
978
  * A batch into the depth map, placed by the same matrices the visible draw uses.
@@ -910,13 +980,27 @@ export class WebGL2Renderer {
910
980
  * No `uModel`: the instanced depth program has none, its placement being the four attribute
911
981
  * columns the batch's own buffer supplies.
912
982
  */
913
- instanced: (batch, data) => {
983
+ instanced: (batch, data, material) => {
914
984
  const { gl } = this;
915
985
  const gpuBatch = batch;
916
986
  const count = Math.min(data.count, gpuBatch.capacity);
917
987
  if (count === 0)
918
988
  return;
919
989
  this.shadowDrawBudget.ask();
990
+ const twoSided = material?.doubleSided === true;
991
+ if (twoSided)
992
+ gl.disable(gl.CULL_FACE);
993
+ const cutout = cutoutOf(material);
994
+ if (cutout !== null) {
995
+ gl.useProgram(this.depthInstancedCutoutProgram);
996
+ this.bindCutoutDepth(this.depthInstancedCutoutUniforms, cutout);
997
+ gpuBatch.mesh.drawInstances(gl, count);
998
+ if (twoSided && this.depthPassCullsFaces)
999
+ gl.enable(gl.CULL_FACE);
1000
+ gl.useProgram(this.depthProgram);
1001
+ this.restoreSurfaceTextureUnit();
1002
+ return;
1003
+ }
920
1004
  const u = this.depthInstancedUniforms;
921
1005
  gl.useProgram(this.depthInstancedProgram);
922
1006
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, this.activeDepthViewProj);
@@ -924,6 +1008,8 @@ export class WebGL2Renderer {
924
1008
  if (this.activeDepthPeel)
925
1009
  gl.uniform1i(u['uPreviousShadowMap'] ?? null, 0);
926
1010
  gpuBatch.mesh.drawInstances(gl, count);
1011
+ if (twoSided && this.depthPassCullsFaces)
1012
+ gl.enable(gl.CULL_FACE);
927
1013
  gl.useProgram(this.depthProgram);
928
1014
  },
929
1015
  skinnedMesh: (mesh, model, palette) => {
@@ -973,6 +1059,21 @@ export class WebGL2Renderer {
973
1059
  gl.useProgram(this.depthProgram);
974
1060
  },
975
1061
  };
1062
+ /**
1063
+ * What a cutout depth draw needs beyond the plain one: the pass's matrix and peel, the material's
1064
+ * cutoff and UV scale, and its albedo on the surface unit, which the caller gives back after.
1065
+ */
1066
+ bindCutoutDepth(u, cutout) {
1067
+ const { gl } = this;
1068
+ gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, this.activeDepthViewProj);
1069
+ gl.uniform1i(u['uPeelShadowLayer'] ?? null, this.activeDepthPeel ? 1 : 0);
1070
+ if (this.activeDepthPeel)
1071
+ gl.uniform1i(u['uPreviousShadowMap'] ?? null, 0);
1072
+ gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
1073
+ gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
1074
+ /* Narrowed for the reason the sink's own `mesh` gives: this renderer only hands out its own. */
1075
+ cutout.albedo.bind(gl, SURFACE_TEXTURE_UNIT);
1076
+ }
976
1077
  drawPointShadowFace = (viewProj) => {
977
1078
  const casters = this.pointShadowCasters;
978
1079
  if (casters === null)
@@ -1529,6 +1630,8 @@ export class WebGL2Renderer {
1529
1630
  * "unbound". Two 1x1 uploads for the life of the renderer.
1530
1631
  */
1531
1632
  this.emptyTexture2D = createEmptyTexture2D(gl);
1633
+ this.emptyDriftIndex = createEmptyTexture3D(gl, true);
1634
+ this.emptyDriftAtlas = createEmptyTexture3D(gl, false);
1532
1635
  this.emptyTexture2DArray = createEmptyTexture2DArray(gl);
1533
1636
  this.emptyTextureCube = createEmptyTextureCube(gl);
1534
1637
  this.gpuTimer = new GpuTimer(gl);
@@ -1720,6 +1823,7 @@ export class WebGL2Renderer {
1720
1823
  this.panelVao = panelVao;
1721
1824
  gl.bindVertexArray(panelVao);
1722
1825
  const panelBuffer = gl.createBuffer();
1826
+ this.panelBuffer = panelBuffer;
1723
1827
  gl.bindBuffer(gl.ARRAY_BUFFER, panelBuffer);
1724
1828
  // A unit quad. The rectangle it becomes is a uniform, so this is uploaded once
1725
1829
  // for the life of the renderer however many panels are drawn.
@@ -1786,6 +1890,19 @@ export class WebGL2Renderer {
1786
1890
  */
1787
1891
  this.scatterDepthProgram = compileProgram(gl, SCATTER_DEPTH_VERT, DEPTH_FRAG, 'scatterDepth');
1788
1892
  this.scatterDepthUniforms = uniformLocations(gl, this.scatterDepthProgram, 'scatterDepth');
1893
+ this.depthCutoutProgram = compileProgram(gl, DEPTH_CUTOUT_VERT, DEPTH_CUTOUT_FRAG, 'depth-cutout');
1894
+ this.depthCutoutUniforms = uniformLocations(gl, this.depthCutoutProgram, 'depth-cutout');
1895
+ this.depthInstancedCutoutProgram = compileProgram(gl, DEPTH_INSTANCED_CUTOUT_VERT, DEPTH_CUTOUT_FRAG, 'depth-instanced-cutout');
1896
+ this.depthInstancedCutoutUniforms = uniformLocations(gl, this.depthInstancedCutoutProgram, 'depth-instanced-cutout');
1897
+ /* The cutout map is always read from the surface unit, which the draw borrows and gives back. */
1898
+ for (const [program, uniforms] of [
1899
+ [this.depthCutoutProgram, this.depthCutoutUniforms],
1900
+ [this.depthInstancedCutoutProgram, this.depthInstancedCutoutUniforms],
1901
+ ]) {
1902
+ gl.useProgram(program);
1903
+ gl.uniform1i(uniforms['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
1904
+ }
1905
+ gl.useProgram(null);
1789
1906
  this.shadowMap = this.quality.directionalShadows
1790
1907
  ? new ShadowMap(gl, this.quality.directionalShadowMapSize)
1791
1908
  : null;
@@ -1928,6 +2045,7 @@ export class WebGL2Renderer {
1928
2045
  this.flatSkinnedProgram,
1929
2046
  this.flatMorphedProgram,
1930
2047
  this.flatBothProgram,
2048
+ this.flatInstancedProgram,
1931
2049
  ]) {
1932
2050
  if (extra !== null)
1933
2051
  gl.deleteProgram(extra);
@@ -1939,12 +2057,20 @@ export class WebGL2Renderer {
1939
2057
  this.skyProgram,
1940
2058
  this.scatterProgram,
1941
2059
  this.depthProgram,
2060
+ this.depthSkinnedProgram,
2061
+ this.depthInstancedProgram,
1942
2062
  this.scatterDepthProgram,
2063
+ this.lightVolumeProgram,
2064
+ this.depthCutoutProgram,
2065
+ this.depthInstancedCutoutProgram,
1943
2066
  ]) {
1944
2067
  gl.deleteProgram(program);
1945
2068
  }
1946
2069
  this.skinPalette.dispose(gl);
1947
2070
  gl.deleteTexture(this.emptyTexture2D);
2071
+ gl.deleteTexture(this.emptyDriftIndex);
2072
+ gl.deleteTexture(this.emptyDriftAtlas);
2073
+ this.forgetLightFieldVolumes();
1948
2074
  gl.deleteTexture(this.emptyTexture2DArray);
1949
2075
  gl.deleteTexture(this.emptyTextureCube);
1950
2076
  /* A cubemap and its whole mip chain, which is the largest single thing this
@@ -1952,6 +2078,28 @@ export class WebGL2Renderer {
1952
2078
  this.probeCapture?.dispose(gl);
1953
2079
  this.probeArray?.dispose(gl);
1954
2080
  this.environmentPrefilter?.dispose(gl);
2081
+ /*
2082
+ * **Everything else the constructor made, which this method never released.** The screen
2083
+ * chain and its bloom and occlusion, three sun maps, the mirror, the point-shadow array, two
2084
+ * vertex arrays, a quad and three textures made on first use. A page swapping renderers kept
2085
+ * every one of them, per renderer, for as long as it lived; `renderer.test.ts` counts them.
2086
+ */
2087
+ this.sceneTarget?.dispose();
2088
+ this.shadowMap?.dispose(gl);
2089
+ this.peeledShadowMap?.dispose(gl);
2090
+ this.dynamicShadowMap?.dispose(gl);
2091
+ this.planarReflection?.dispose(gl);
2092
+ this.pointShadows?.dispose();
2093
+ gl.deleteVertexArray(this.panelVao);
2094
+ gl.deleteVertexArray(this.skyVao);
2095
+ if (this.panelBuffer !== null)
2096
+ gl.deleteBuffer(this.panelBuffer);
2097
+ if (this.cookieTexture !== null)
2098
+ gl.deleteTexture(this.cookieTexture);
2099
+ if (this.iesTexture !== null)
2100
+ gl.deleteTexture(this.iesTexture);
2101
+ if (this.clusterTexture !== null)
2102
+ gl.deleteTexture(this.clusterTexture);
1955
2103
  }
1956
2104
  if (options.releaseContext === true) {
1957
2105
  gl.getExtension('WEBGL_lose_context')?.loseContext();
@@ -2171,6 +2319,7 @@ export class WebGL2Renderer {
2171
2319
  return;
2172
2320
  this.materials.dirty();
2173
2321
  this.currentSurfaceTexture = material?.albedo ?? null;
2322
+ this.materialDoubleSided = material?.doubleSided === true;
2174
2323
  /*
2175
2324
  * Written to every flat program, because material state persists across draws and a skinned
2176
2325
  * draw is entitled to the material the caller set before it. A second program holding none of
@@ -2237,10 +2386,11 @@ export class WebGL2Renderer {
2237
2386
  * The emissive map, on exactly the terms the two maps above it are bound: before the albedo
2238
2387
  * early return, and the stand-in rather than `null`.
2239
2388
  *
2240
- * **White is the right stand-in here and black would be a bug.** The shader multiplies by this
2241
- * map, so the placeholder has to be the multiplicative identity; `emptyTexture2D` is white,
2242
- * which is what makes an unbound material collapse to the arithmetic it had before. A black
2243
- * placeholder would switch every glow in the scene off the moment the gate was ever wrong.
2389
+ * **The stand-in is black, so the gate is what keeps a glow on.** The shader multiplies by
2390
+ * this map, and `emptyTexture2D` is opaque black rather than the multiplicative identity, so an
2391
+ * unbound material keeps the arithmetic it had before only because `uEmissiveMapEnabled` is 0
2392
+ * and the read never happens. What would make that wrong is a read taken outside that branch:
2393
+ * every glow on a material without a map would go out.
2244
2394
  */
2245
2395
  const emissiveMap = material?.emissive ?? null;
2246
2396
  gl.activeTexture(gl.TEXTURE0 + EMISSIVE_TEXTURE_UNIT);
@@ -2259,6 +2409,7 @@ export class WebGL2Renderer {
2259
2409
  * how big a dome was — WebGPU writes it before its own early return and WebGL2 did not.
2260
2410
  */
2261
2411
  gl.uniform2f(u['uUvScale'] ?? null, material?.uScale ?? 1, material?.vScale ?? 1);
2412
+ gl.uniform1i(u['uDoubleSided'] ?? null, material?.doubleSided === true ? 1 : 0);
2262
2413
  if (albedo === null) {
2263
2414
  gl.uniform1i(u['uAlbedoEnabled'] ?? null, 0);
2264
2415
  return;
@@ -2963,7 +3114,11 @@ export class WebGL2Renderer {
2963
3114
  */
2964
3115
  bindPointLights(gl, u, env, this.quality.pointLightFalloff);
2965
3116
  bindAtmosphere(gl, u, env, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.quality.underwaterAtmosphere);
3117
+ if (this.materialDoubleSided)
3118
+ gl.disable(gl.CULL_FACE);
2966
3119
  batch.drawTo(gl, data.count);
3120
+ if (this.materialDoubleSided)
3121
+ gl.enable(gl.CULL_FACE);
2967
3122
  }
2968
3123
  /** Build a batch for electrical arcs. `capacity` is in segments, not arcs. */
2969
3124
  createBolts(segmentCapacity, label = 'bolts') {
@@ -3470,6 +3625,9 @@ export class WebGL2Renderer {
3470
3625
  this.gpuTimer.begin('shadows');
3471
3626
  this.shadowPassActive = true;
3472
3627
  this.activeShadowMap = shadowMap;
3628
+ /* The static layer opens first, so a frame's peel counts from here. See `peelFilled`. */
3629
+ if (layer === 'static')
3630
+ this.peelFilled = false;
3473
3631
  /*
3474
3632
  * **Range-corrected but not reversed.** `EXT_clip_control` is context state, so a light matrix
3475
3633
  * that still emits OpenGL's `[-1, 1]` loses everything below zero the moment it is on. See
@@ -3523,6 +3681,8 @@ export class WebGL2Renderer {
3523
3681
  this.uploadWind(this.depthUniforms, this.frameWind);
3524
3682
  gl.useProgram(this.depthSkinnedProgram);
3525
3683
  this.uploadWind(this.depthSkinnedUniforms, this.frameWind);
3684
+ gl.useProgram(this.depthCutoutProgram);
3685
+ this.uploadWind(this.depthCutoutUniforms, this.frameWind);
3526
3686
  gl.useProgram(this.depthProgram);
3527
3687
  }
3528
3688
  /**
@@ -3614,6 +3774,8 @@ export class WebGL2Renderer {
3614
3774
  if (!this.shadowPassActive || shadowMap === null)
3615
3775
  return;
3616
3776
  shadowMap.end(this.gl);
3777
+ if (shadowMap === this.peeledShadowMap)
3778
+ this.peelFilled = true;
3617
3779
  /* Back to the frame's sense; see `beginShadowPass` for why it left it. */
3618
3780
  this.gl.depthFunc(glDepthFuncEqual(this.gl));
3619
3781
  this.gl.clearDepth(this.depthClear);
@@ -4212,6 +4374,70 @@ export class WebGL2Renderer {
4212
4374
  setCameraMotionBlur(scale) {
4213
4375
  this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
4214
4376
  }
4377
+ /**
4378
+ * The camera has cut: this frame is a new shot, not the next moment of the last one.
4379
+ *
4380
+ * **Everything temporal reprojects through the previous frame's view**: the motion blur measures
4381
+ * its smear against it, and the temporal resolve reads its history through it. A cut is a jump
4382
+ * the renderer cannot tell from a fast camera, so without this a transport that seeks, a respawn
4383
+ * or an edit smears its first frame along the whole jump and blends in a picture of somewhere
4384
+ * else. The caller knows it cut; this is how it says so, before the frame's `beginFrame`.
4385
+ *
4386
+ * What it gives up is one frame of history: the frame after a cut has no blur and no temporal
4387
+ * blend, as the first frame of a session has none. Nothing else is reset.
4388
+ */
4389
+ cameraCut() {
4390
+ this.hasPreviousView = false;
4391
+ this.temporalHistory.invalidate();
4392
+ /* The eye was adapted to the last shot. */
4393
+ this.sceneTarget?.cutExposure();
4394
+ }
4395
+ /**
4396
+ * **Eye adaptation**: how far the frame's exposure follows its own brightness, 0 to 1, and the
4397
+ * seconds since the last frame. 0 is off and the default, and at 0 nothing is metered or allocated.
4398
+ *
4399
+ * The finished scene is metered on the GPU each frame — the log-mean of its luminance — and a
4400
+ * held brightness moves toward it at a fixed rate, about 78% of the way in a second; the composite
4401
+ * then scales scene light toward middle grey by \`strength\` of the stops between them, at most six
4402
+ * either way. So a shaded courtyard floor opens up the way a camera or an eye would, and
4403
+ * \`setOutputExposure\` becomes a bias on top: a night kept darker than a day is a bias under one.
4404
+ * **The time is the caller's**, because the engine takes time from its caller; a held capture
4405
+ * passes 0 and holds. \`cameraCut\` snaps it, so a new shot is metered rather than eased into.
4406
+ *
4407
+ * What it gives up is regional metering: every pixel counts the same. Needs \`screenEffects\` and
4408
+ * \`hdrScene\`, and says so once rather than doing nothing. See \`shaders/exposure.ts\`.
4409
+ */
4410
+ setAutoExposure(strength, dtSec) {
4411
+ if (this.sceneTarget === null || !this.quality.hdrScene) {
4412
+ this.warnLookWithoutComposite(strength);
4413
+ return;
4414
+ }
4415
+ this.sceneTarget.setAutoExposure(strength, dtSec);
4416
+ }
4417
+ /**
4418
+ * **Local exposure**: how far each region of the frame is brought toward the frame's own
4419
+ * brightness, 0 to 1. 0 is off and the default, and at 0 nothing more is measured or allocated.
4420
+ *
4421
+ * A tone curve maps one range, and a sunlit courtyard is two: its shaded arcades hold a fiftieth
4422
+ * of the light on its sunlit paving, so exposed for either the other is black or white. This moves
4423
+ * each region `strength` of the stops between it and the frame's held brightness, at most three,
4424
+ * read from a bilateral grid over the exposure meter's tiles, so shade beside sun is lifted as
4425
+ * shade rather than as the average of the two. The eye's held brightness is what regions move
4426
+ * toward, so it is measured whether or not `setAutoExposure` is on; with adaptation off the frame
4427
+ * as a whole keeps the exposure it was given. About 0.5 is a photograph's latitude, and 1 is a
4428
+ * frame with no light and shade left in it.
4429
+ *
4430
+ * What it gives up is contrast between regions, which is the point, and a halo a tile wide where
4431
+ * two regions of one brightness band differ. Needs `screenEffects` and `hdrScene`, and says so
4432
+ * once rather than doing nothing. See `shaders/localExposure.ts`.
4433
+ */
4434
+ setLocalExposure(strength) {
4435
+ if (this.sceneTarget === null || !this.quality.hdrScene) {
4436
+ this.warnLookWithoutComposite(strength);
4437
+ return;
4438
+ }
4439
+ this.sceneTarget.setLocalExposure(strength);
4440
+ }
4215
4441
  /**
4216
4442
  * Where this frame's lens is focused, how deep the sharp zone is, and how much of the ceiling
4217
4443
  * to take.
@@ -4264,9 +4490,18 @@ export class WebGL2Renderer {
4264
4490
  *
4265
4491
  * Set before `beginFrame` and held until changed. Ignored entirely when `bloom` is 0, since
4266
4492
  * the chain is never built.
4493
+ *
4494
+ * **`threshold` moves `bloomThreshold` for this frame and every one after**, in the same scene
4495
+ * units. A threshold is compared before exposure is applied, so one fixed at construction means a
4496
+ * different brightness on screen at every exposure. That is harmless while exposure stands still
4497
+ * and wrong for a day whose exposure spans 2.5 to 14: a courtyard blooming at noon, or candles
4498
+ * never blooming at night. A caller whose exposure moves passes what it wants on screen over the
4499
+ * exposure. Omitted leaves it where it was, so a caller that never passes one keeps the profile's.
4267
4500
  */
4268
- setBloom(scale) {
4501
+ setBloom(scale, threshold) {
4269
4502
  this.bloomScale = Math.min(Math.max(scale, 0), 1);
4503
+ if (threshold !== undefined)
4504
+ this.bloomThresholdSet = bloomThresholdOf(threshold);
4270
4505
  }
4271
4506
  /**
4272
4507
  * How thick the air is, this frame: a global participating medium filling the whole frustum.
@@ -4400,6 +4635,38 @@ export class WebGL2Renderer {
4400
4635
  }
4401
4636
  this.sceneTarget.setColourGrade(lut, strength);
4402
4637
  }
4638
+ /**
4639
+ * How strongly the lens darkens the frame's corners, held until changed: 0 is none, 0.5 is about
4640
+ * 1.2 stops at the corner. Applied to scene light before the tone curve, as a lens loses light;
4641
+ * see `filmLook.ts`. Needs `screenEffects`, and says so once rather than doing nothing.
4642
+ */
4643
+ setVignette(strength) {
4644
+ if (this.sceneTarget === null) {
4645
+ this.warnLookWithoutComposite(strength);
4646
+ return;
4647
+ }
4648
+ this.sceneTarget.setVignette(strength);
4649
+ }
4650
+ /**
4651
+ * Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
4652
+ * grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
4653
+ * its caller: pass a new one each frame for grain that moves, and the same one for a still that
4654
+ * is identical run to run. Applied last, after the grade and the veil; see `filmLook.ts`.
4655
+ */
4656
+ setFilmGrain(strength, seed) {
4657
+ if (this.sceneTarget === null) {
4658
+ this.warnLookWithoutComposite(strength);
4659
+ return;
4660
+ }
4661
+ this.sceneTarget.setFilmGrain(strength, seed);
4662
+ }
4663
+ warnedLookWithoutComposite = false;
4664
+ warnLookWithoutComposite(strength) {
4665
+ if (strength > 0 && !this.warnedLookWithoutComposite) {
4666
+ this.warnedLookWithoutComposite = true;
4667
+ console.warn(FILM_LOOK_WITHOUT_COMPOSITE);
4668
+ }
4669
+ }
4403
4670
  /**
4404
4671
  * What this frame asked for. **Every ceiling here is `null`, and that is the finding.**
4405
4672
  *
@@ -4716,7 +4983,10 @@ export class WebGL2Renderer {
4716
4983
  */
4717
4984
  const bloomStrength = this.quality.bloom * this.bloomScale;
4718
4985
  const bloom = bloomStrength > 0
4719
- ? { strength: bloomStrength, threshold: this.quality.bloomThreshold }
4986
+ ? {
4987
+ strength: bloomStrength,
4988
+ threshold: this.bloomThresholdSet ?? this.quality.bloomThreshold,
4989
+ }
4720
4990
  : undefined;
4721
4991
  /* The grade travels to the resolve only where the mesh pass gave it up. See `bindMeshPass`. */
4722
4992
  const lateGrade = this.quality.hdrScene
@@ -4885,11 +5155,12 @@ export class WebGL2Renderer {
4885
5155
  sunShadow,
4886
5156
  lightViewProj: env?.lightViewProj ?? IDENTITY_MAT4,
4887
5157
  /* The placeholder rather than null where a map is merely absent, for the reason
4888
- `emptyTexture.ts` gives: white is depth 1, the far plane, nothing in the way. */
5158
+ `emptyTexture.ts` gives. It is black, which as a depth is an occluder at the light,
5159
+ so it is safe only because `sunShadow` is 0 whenever these maps do not exist. */
4889
5160
  staticShadowMap: this.shadowMap?.texture ?? this.emptyTexture2D,
4890
5161
  peeledShadowMap: this.peeledShadowMap?.texture ?? this.emptyTexture2D,
4891
5162
  dynamicShadowMap: this.dynamicShadowMap?.texture ?? this.emptyTexture2D,
4892
- peeledEnabled: this.peeledShadowMap !== null,
5163
+ peeledEnabled: this.peelFilled,
4893
5164
  });
4894
5165
  if (marched) {
4895
5166
  /* The march finished on the default framebuffer, which is not where the frame is being
@@ -5093,6 +5364,78 @@ export class WebGL2Renderer {
5093
5364
  /* Leave the plain program current, which is what every other pass expects to find. */
5094
5365
  this.useFlatProgram();
5095
5366
  }
5367
+ /**
5368
+ * Sum a scene's many fixed lights into a DriftLight field, which stands in for each of them
5369
+ * wherever the frame's choice of exact lights does not reach. See `driftLight/lightField.ts`.
5370
+ *
5371
+ * Replaces any field made before. The scene then paces `field.bake(bricks)` and calls
5372
+ * `field.follow(buffer.complete, ...)` each frame with the selection that chose its exact lights;
5373
+ * the volume is summed into the frame once every brick has landed.
5374
+ */
5375
+ createLightField(lights, options = {}) {
5376
+ this.forgetLightFieldVolumes();
5377
+ const field = new LightField(lights, { ...options, falloff: this.quality.pointLightFalloff });
5378
+ this.lightField = field;
5379
+ return field;
5380
+ }
5381
+ /** Let go of the field and its volumes; the scene's lights are all exact again. */
5382
+ disposeLightField() {
5383
+ this.forgetLightFieldVolumes();
5384
+ this.lightField = null;
5385
+ }
5386
+ forgetLightFieldVolumes() {
5387
+ if (this.driftIndex !== null)
5388
+ this.gl.deleteTexture(this.driftIndex);
5389
+ if (this.driftAtlas !== null)
5390
+ this.gl.deleteTexture(this.driftAtlas);
5391
+ this.driftIndex = null;
5392
+ this.driftAtlas = null;
5393
+ }
5394
+ /**
5395
+ * Upload the field's two volumes, once, when its last brick has baked: nothing samples them
5396
+ * before that, so uploading as bricks land would be work for no picture.
5397
+ */
5398
+ uploadLightField() {
5399
+ const field = this.lightField;
5400
+ if (field === null || this.driftAtlas !== null || !field.ready || field.layout.count === 0) {
5401
+ return;
5402
+ }
5403
+ const { gl } = this;
5404
+ const [ix, iy, iz] = field.layout.dims;
5405
+ const index = gl.createTexture();
5406
+ gl.bindTexture(gl.TEXTURE_3D, index);
5407
+ gl.texImage3D(gl.TEXTURE_3D, 0, gl.R32UI, ix, iy, iz, 0, gl.RED_INTEGER, gl.UNSIGNED_INT, field.layout.index);
5408
+ gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
5409
+ gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
5410
+ const [aw, ah, ad] = field.atlasSize;
5411
+ const atlas = gl.createTexture();
5412
+ gl.bindTexture(gl.TEXTURE_3D, atlas);
5413
+ gl.texImage3D(gl.TEXTURE_3D, 0, gl.RGBA16F, aw, ah, ad, 0, gl.RGBA, gl.HALF_FLOAT, field.atlas);
5414
+ /* Filtered, because a brick's samples share their faces: the lookup is seamless inside one. */
5415
+ gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
5416
+ gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
5417
+ for (const wrap of [gl.TEXTURE_WRAP_S, gl.TEXTURE_WRAP_T, gl.TEXTURE_WRAP_R]) {
5418
+ gl.texParameteri(gl.TEXTURE_3D, wrap, gl.CLAMP_TO_EDGE);
5419
+ }
5420
+ gl.bindTexture(gl.TEXTURE_3D, null);
5421
+ this.driftIndex = index;
5422
+ this.driftAtlas = atlas;
5423
+ }
5424
+ /** The field's uniforms and volumes for this pass, or the stand-ins that switch it off. */
5425
+ bindDriftLight(u, eye) {
5426
+ const { gl } = this;
5427
+ this.uploadLightField();
5428
+ const d = this.driftUniforms;
5429
+ resolveDriftLight(this.driftAtlas === null ? null : this.lightField, this.probePassActive, eye, d);
5430
+ gl.uniform4fv(u['uDriftLight'] ?? null, d.light);
5431
+ gl.uniform4fv(u['uDriftLightOrigin'] ?? null, d.origin);
5432
+ gl.activeTexture(gl.TEXTURE0 + DRIFT_LIGHT_INDEX_TEXTURE_UNIT);
5433
+ gl.bindTexture(gl.TEXTURE_3D, this.driftIndex ?? this.emptyDriftIndex);
5434
+ gl.uniform1i(u['uDriftLightIndex'] ?? null, DRIFT_LIGHT_INDEX_TEXTURE_UNIT);
5435
+ gl.activeTexture(gl.TEXTURE0 + DRIFT_LIGHT_ATLAS_TEXTURE_UNIT);
5436
+ gl.bindTexture(gl.TEXTURE_3D, this.driftAtlas ?? this.emptyDriftAtlas);
5437
+ gl.uniform1i(u['uDriftLightAtlas'] ?? null, DRIFT_LIGHT_ATLAS_TEXTURE_UNIT);
5438
+ }
5096
5439
  /**
5097
5440
  * Everything one flat program needs to know about this frame.
5098
5441
  *
@@ -5221,7 +5564,7 @@ export class WebGL2Renderer {
5221
5564
  gl.uniform1f(u['uShadowMapSize'] ?? null, this.quality.directionalShadowMapSize);
5222
5565
  gl.uniform1f(u['uShadowDepthSpan'] ?? null, env.shadowDepthSpan);
5223
5566
  gl.uniform1f(u['uShadowMaxDistance'] ?? null, this.quality.directionalShadowMaxDistance);
5224
- gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peeledShadowMap === null ? 0 : 1);
5567
+ gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peelFilled ? 1 : 0);
5225
5568
  gl.uniform1f(u['uShadowMaxSlope'] ?? null, this.quality.directionalShadowMaxSlope);
5226
5569
  gl.uniform1i(u['uShadowFilterTaps'] ?? null, this.quality.shadowFilterTaps);
5227
5570
  gl.uniform3fv(u['uHighlightMin'] ?? null, env.highlightMin);
@@ -5296,11 +5639,19 @@ export class WebGL2Renderer {
5296
5639
  * zero would not be enough on its own — a driver may fetch a sampler's descriptor before it
5297
5640
  * evaluates the arithmetic that discards the result, which is the same reason
5298
5641
  * `emptyTexture.ts` exists.
5642
+ *
5643
+ * **Except a bounce bake, which reads it on purpose.** Its faces are drawn into the capture
5644
+ * cube, and the array is attached only by the convolution after them, so no pass both samples
5645
+ * and attaches it.
5299
5646
  */
5300
5647
  if (this.probeCapture !== null) {
5301
5648
  const array = this.probeArray;
5302
5649
  const grid = this.probes;
5303
- const usable = array !== null && grid !== null && array.ready && !this.probePassActive;
5650
+ const usable = array !== null &&
5651
+ grid !== null &&
5652
+ array.usable &&
5653
+ this.probeSweeps?.ready === true &&
5654
+ (!this.probePassActive || this.probeBounce);
5304
5655
  gl.activeTexture(gl.TEXTURE0 + ENVIRONMENT_TEXTURE_UNIT);
5305
5656
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, usable ? array.texture : this.emptyTexture2DArray);
5306
5657
  gl.uniform1i(u['uEnvironment'] ?? null, ENVIRONMENT_TEXTURE_UNIT);
@@ -5341,13 +5692,19 @@ export class WebGL2Renderer {
5341
5692
  * times over two days as four different bugs before it could be asked for.
5342
5693
  */
5343
5694
  gl.uniform1f(u['uProbeGridAmbient'] ?? null, usable && this.probeAmbient ? 1 : 0);
5695
+ /* Which sets the shading reads, and how far between them. See `probeSweeps.ts`. */
5696
+ const sets = this.probeSweeps?.uniforms(this.probeSets) ?? this.probeSets.fill(0);
5697
+ gl.uniform3f(u['uProbeGridSets'] ?? null, sets[0] ?? 0, sets[1] ?? 0, sets[2] ?? 0);
5344
5698
  }
5699
+ this.bindDriftLight(u, camera.position);
5345
5700
  gl.activeTexture(gl.TEXTURE0 + SURFACE_TEXTURE_UNIT);
5346
5701
  gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
5347
5702
  gl.uniform1i(u['uAlbedo'] ?? null, SURFACE_TEXTURE_UNIT);
5348
5703
  gl.uniform1i(u['uAlbedoEnabled'] ?? null, 0);
5349
5704
  gl.uniform2f(u['uUvScale'] ?? null, 1, 1);
5350
5705
  gl.uniform1f(u['uAlbedoCutout'] ?? null, 0);
5706
+ gl.uniform1i(u['uDoubleSided'] ?? null, 0);
5707
+ this.materialDoubleSided = false;
5351
5708
  this.currentSurfaceTexture = null;
5352
5709
  /*
5353
5710
  * **And the normal map, which this did not reset and had to.**
@@ -5573,7 +5930,11 @@ export class WebGL2Renderer {
5573
5930
  */
5574
5931
  gl.depthFunc(this.reversedDepth ? gl.GREATER : gl.LESS);
5575
5932
  if (layer === 0) {
5933
+ if (this.materialDoubleSided)
5934
+ gl.disable(gl.CULL_FACE);
5576
5935
  mesh.draw(gl);
5936
+ if (this.materialDoubleSided)
5937
+ gl.enable(gl.CULL_FACE);
5577
5938
  gl.depthFunc(this.reversedDepth ? gl.GEQUAL : gl.LEQUAL);
5578
5939
  if (tint !== null)
5579
5940
  gl.uniform3fv(u['uTint'] ?? null, WHITE_TINT);
@@ -5594,7 +5955,11 @@ export class WebGL2Renderer {
5594
5955
  */
5595
5956
  const offset = depthOffsetForLayer(layer, this.reversedDepth);
5596
5957
  gl.polygonOffset(offset.slope, offset.units);
5958
+ if (this.materialDoubleSided)
5959
+ gl.disable(gl.CULL_FACE);
5597
5960
  mesh.draw(gl);
5961
+ if (this.materialDoubleSided)
5962
+ gl.enable(gl.CULL_FACE);
5598
5963
  gl.polygonOffset(0, 0);
5599
5964
  gl.disable(gl.POLYGON_OFFSET_FILL);
5600
5965
  if (program !== null)
@@ -5749,7 +6114,11 @@ export class WebGL2Renderer {
5749
6114
  gl.enable(gl.POLYGON_OFFSET_FILL);
5750
6115
  gl.polygonOffset(offset.slope, offset.units);
5751
6116
  }
6117
+ if (this.materialDoubleSided)
6118
+ gl.disable(gl.CULL_FACE);
5752
6119
  mesh.draw(gl);
6120
+ if (this.materialDoubleSided)
6121
+ gl.enable(gl.CULL_FACE);
5753
6122
  if (offset.units !== 0) {
5754
6123
  gl.polygonOffset(0, 0);
5755
6124
  gl.disable(gl.POLYGON_OFFSET_FILL);
@@ -5824,7 +6193,8 @@ export class WebGL2Renderer {
5824
6193
  return true;
5825
6194
  this.probeArray?.dispose(this.gl);
5826
6195
  this.probes = grid;
5827
- this.probeArray = new EnvProbeArray(this.gl, octahedralEdgeFor(this.quality.reflectionProbeSize), grid.layers, this.quality.hdrScene);
6196
+ this.probeSweeps = new ProbeSweeps(grid.layers, grid.crossfade);
6197
+ this.probeArray = new EnvProbeArray(this.gl, octahedralEdgeFor(this.quality.reflectionProbeSize), grid.layers * this.probeSweeps.sets, this.quality.hdrScene);
5828
6198
  return this.probeArray.usable;
5829
6199
  }
5830
6200
  /**
@@ -5844,12 +6214,14 @@ export class WebGL2Renderer {
5844
6214
  const probe = this.probeCapture;
5845
6215
  const array = this.probeArray;
5846
6216
  const grid = this.probes;
5847
- if (probe === null || array === null || grid === null)
6217
+ const sweeps = this.probeSweeps;
6218
+ if (probe === null || array === null || grid === null || sweeps === null)
5848
6219
  return false;
5849
6220
  if (layer < 0 || layer >= grid.layers)
5850
6221
  return false;
5851
6222
  this.probeAmbient = options?.irradiance ?? true;
5852
6223
  grid.positionOf(layer, this.probeOrigin);
6224
+ this.probeBounce = options?.bounce ?? false;
5853
6225
  this.probePassActive = true;
5854
6226
  /*
5855
6227
  * **A probe stores radiance, not display pixels.**
@@ -5887,14 +6259,19 @@ export class WebGL2Renderer {
5887
6259
  this.gl.frontFace(this.gl.CW);
5888
6260
  let baked = false;
5889
6261
  try {
5890
- baked = probe.bake(this.gl, this.probeOrigin, clearColor, drawFace);
6262
+ const [first, end] = probeFaceRange(options);
6263
+ baked = probe.bake(this.gl, this.probeOrigin, clearColor, drawFace, first, end);
6264
+ /* A bake that has not reached its sixth face has nothing whole to convolve yet. */
6265
+ if (end < 6)
6266
+ return baked;
5891
6267
  /*
5892
6268
  * The convolution, immediately after the faces and inside the same guard, because it reads
5893
6269
  * the chain `bake` has just generated. A bake that failed leaves the layer untouched rather
5894
6270
  * than convolving whatever the driver left in the capture.
5895
6271
  */
5896
6272
  if (baked && this.environmentPrefilter !== null) {
5897
- this.environmentPrefilter.run(this.gl, probe, array, layer, ENVIRONMENT_TEXTURE_UNIT, this.quality.environmentPrefilterSamples, this.quality.environmentPrefilter);
6273
+ this.environmentPrefilter.run(this.gl, probe, array, sweeps.writeLayer(layer), ENVIRONMENT_TEXTURE_UNIT, this.quality.environmentPrefilterSamples, this.quality.environmentPrefilter);
6274
+ sweeps.baked(layer);
5898
6275
  }
5899
6276
  }
5900
6277
  finally {
@@ -5908,6 +6285,7 @@ export class WebGL2Renderer {
5908
6285
  * later frame reading the placeholder and quietly reflecting nothing.
5909
6286
  */
5910
6287
  this.probePassActive = false;
6288
+ this.probeBounce = false;
5911
6289
  /* Back to the canvas, or to the frame's own target if one is bound. See `SceneTarget`. */
5912
6290
  if (this.sceneTarget !== null && !this.framePresented)
5913
6291
  this.sceneTarget.bind();
@@ -5963,6 +6341,7 @@ export class WebGL2Renderer {
5963
6341
  return false;
5964
6342
  this.probeAmbient = options?.irradiance ?? true;
5965
6343
  this.environmentPrefilter?.run(this.gl, probe, array, 0, ENVIRONMENT_TEXTURE_UNIT, this.quality.environmentPrefilterSamples, this.quality.environmentPrefilter);
6344
+ this.probeSweeps?.baked(0);
5966
6345
  return true;
5967
6346
  }
5968
6347
  /**
@@ -6085,7 +6464,7 @@ export class WebGL2Renderer {
6085
6464
  lookup does its own `* 0.5 + 0.5` and correcting the matrix as well doubles it. */
6086
6465
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, env.lightViewProj);
6087
6466
  gl.uniform1f(u['uShadowMapSize'] ?? null, this.quality.directionalShadowMapSize);
6088
- gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peeledShadowMap === null ? 0 : 1);
6467
+ gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peelFilled ? 1 : 0);
6089
6468
  gl.activeTexture(gl.TEXTURE0);
6090
6469
  gl.bindTexture(gl.TEXTURE_2D, this.shadowMap?.texture ?? this.emptyTexture2D);
6091
6470
  gl.uniform1i(u['uStaticShadowMap'] ?? null, 0);