@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
@@ -2,7 +2,6 @@ import { FrameBudget } from '../budget.ts';
2
2
  import { MaterialChanges, ownsMaterial } from '../materialChanges.ts';
3
3
  import { mat4 } from 'gl-matrix';
4
4
  import {
5
- DEPTH_OFFSET_SIGN,
6
5
  GL_DEPTH_REMAP,
7
6
  MAX_DEPTH_LAYER,
8
7
  depthClearFor,
@@ -81,12 +80,17 @@ import {
81
80
  import { FILM_FRAG, FILM_VERT } from '../../shaders/film.ts';
82
81
  import { SceneTarget } from '../../sceneTarget.ts';
83
82
  import type { ColourGradeLut } from '../../colourGrade.ts';
84
- import { bloomProfileWarning } from '../../bloomChain.ts';
83
+ import { FILM_LOOK_WITHOUT_COMPOSITE } from '../../shaders/filmLook.ts';
84
+ import { bloomProfileWarning, bloomThresholdOf } from '../../bloomChain.ts';
85
85
  import {
86
86
  createEmptyTexture2D,
87
87
  createEmptyTexture2DArray,
88
+ createEmptyTexture3D,
88
89
  createEmptyTextureCube,
89
90
  } from '../../emptyTexture.ts';
91
+ import { LightField } from '../../driftLight/lightField.ts';
92
+ import type { LightFieldOptions, LightFieldSource } from '../../driftLight/lightField.ts';
93
+ import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.ts';
90
94
 
91
95
  /**
92
96
  * How far the rush's outermost tap reaches at full strength, in UV.
@@ -113,11 +117,12 @@ import { SkinPaletteTexture } from './skinPaletteTexture.ts';
113
117
  /* The codes the shader reads, stated once in `vertexDefaults.ts` for both backends. */
114
118
  import { LIGHT_VOLUME_VERT, lightVolumeFrag } from '../../shaders/lightVolume.ts';
115
119
  import { PANEL_FRAG, PANEL_VERT } from '../../shaders/panel.ts';
116
- import { ReflectionProbe, cubeFaceProjection } from '../../reflectionProbe.ts';
120
+ import { ReflectionProbe, cubeFaceProjection, probeFaceRange } from '../../reflectionProbe.ts';
117
121
  import type { ProbeBakeOptions } from '../../reflectionProbe.ts';
118
122
  import { EnvProbeArray } from '../../envProbeArray.ts';
119
123
  import { ProbeGrid, SINGLE_PROBE, UNIT_STEP, WORLD_ORIGIN, sameGrid } from '../../probeGrid.ts';
120
124
  import type { ProbeGridOptions } from '../../probeGrid.ts';
125
+ import { ProbeSweeps } from '../../probeSweeps.ts';
121
126
  import { octahedralEdgeFor } from '../../prefilterEnvMap.ts';
122
127
  import { EnvironmentPrefilterPass } from '../../prefilterPass.ts';
123
128
  import { packIesAtlas } from '../../iesProfile.ts';
@@ -135,7 +140,10 @@ import { equirectToCubeFaces } from '../../equirectToCube.ts';
135
140
 
136
141
  import { SKY_FRAG, skyVertFor } from '../../shaders/sky.ts';
137
142
  import {
143
+ DEPTH_CUTOUT_FRAG,
144
+ DEPTH_CUTOUT_VERT,
138
145
  DEPTH_FRAG,
146
+ DEPTH_INSTANCED_CUTOUT_VERT,
139
147
  DEPTH_INSTANCED_VERT,
140
148
  DEPTH_SKINNED_VERT,
141
149
  DEPTH_VERT,
@@ -167,6 +175,8 @@ import {
167
175
  bindPointLights,
168
176
  COOKIE_ATLAS_TEXTURE_UNIT,
169
177
  REFRACT_SCENE_TEXTURE_UNIT,
178
+ DRIFT_LIGHT_ATLAS_TEXTURE_UNIT,
179
+ DRIFT_LIGHT_INDEX_TEXTURE_UNIT,
170
180
  COOKIE_TILE,
171
181
  IES_ATLAS_TEXTURE_UNIT,
172
182
  bindAreaLights,
@@ -226,6 +236,9 @@ import type { WindField } from '../../windField.ts';
226
236
  import type { FlockParams } from '../../flockRenderer.ts';
227
237
  import { InstancedBatch } from './instanced.ts';
228
238
  import type { MeshInstances } from '../../instances.ts';
239
+ import { cutoutOf } from '../../cutoutCaster.ts';
240
+ import type { CutoutCaster } from '../../cutoutCaster.ts';
241
+ import type { SurfaceTextureHandle } from '../api.ts';
229
242
 
230
243
  /**
231
244
  * How many frames `firstFrameSettled` will ask before it stops asking.
@@ -789,6 +802,14 @@ export interface TranslucentMeshOptions {
789
802
  * that two surfaces overlapping in view blend in submission order. And a surface that writes no
790
803
  * depth cannot occlude the sky. A caller wanting both wants the pieces sorted and this left
791
804
  * alone.
805
+ *
806
+ * **And depth a blended surface writes is where the frame's depth-reading passes stop.** The
807
+ * global medium marches to it on both backends, and on WebGPU ambient occlusion, the temporal
808
+ * resolve and motion blur take it for the surface too. Right for a pane that is most of a pixel's
809
+ * colour; wrong for a faint sheet. A bought courtyard's dirt decals stand a centimetre off their
810
+ * stone and overhang every corner they wrap, and the haze stopped at each overhang, a clear band
811
+ * down a pier's whole height. A decal lying on an opaque surface needs no depth to hold the sky
812
+ * off, since the surface under it already does, so `false` costs it only an order among decals.
792
813
  */
793
814
  readonly depthWrite?: boolean;
794
815
  /**
@@ -1060,6 +1081,8 @@ export class WebGL2Renderer implements RendererApi {
1060
1081
  private readonly panelProgram: WebGLProgram;
1061
1082
  private readonly panelUniforms: Record<string, WebGLUniformLocation>;
1062
1083
  private readonly panelVao: WebGLVertexArrayObject;
1084
+ /** The unit quad behind `panelVao`, held so `dispose` can release it. */
1085
+ private readonly panelBuffer: WebGLBuffer | null;
1063
1086
  private readonly filmProgram: WebGLProgram;
1064
1087
  /**
1065
1088
  * The off-screen colour target, when screen effects are on. Null is not a failure —
@@ -1091,6 +1114,14 @@ export class WebGL2Renderer implements RendererApi {
1091
1114
  private skinPaletteSet = false;
1092
1115
 
1093
1116
  private readonly emptyTexture2D: WebGLTexture;
1117
+ /** DriftLight's stand-ins while no field is whole: no brick anywhere, and no light. */
1118
+ private readonly emptyDriftIndex: WebGLTexture;
1119
+ private readonly emptyDriftAtlas: WebGLTexture;
1120
+ /** The scene's DriftLight field, and its two volumes once it is whole. See `createLightField`. */
1121
+ private lightField: LightField | null = null;
1122
+ private driftIndex: WebGLTexture | null = null;
1123
+ private driftAtlas: WebGLTexture | null = null;
1124
+ private readonly driftUniforms = createDriftLightUniforms();
1094
1125
  private readonly emptyTexture2DArray: WebGLTexture;
1095
1126
  private readonly emptyTextureCube: WebGLTexture;
1096
1127
  /** How hard the rush blurs this frame, and how far its outermost tap reaches. */
@@ -1170,6 +1201,8 @@ export class WebGL2Renderer implements RendererApi {
1170
1201
  private motionBlurScale = 1;
1171
1202
  /** How much of the bloom ceiling this frame takes. See `setBloom`. */
1172
1203
  private bloomScale = 1;
1204
+ /** In scene units, once `setBloom` has moved it; null reads the profile's. */
1205
+ private bloomThresholdSet: number | null = null;
1173
1206
  /**
1174
1207
  * This frame's exposure into the tone curve. See `setOutputExposure`.
1175
1208
  *
@@ -1277,6 +1310,15 @@ export class WebGL2Renderer implements RendererApi {
1277
1310
  private readonly depthUniforms: Record<string, WebGLUniformLocation>;
1278
1311
  private readonly depthInstancedProgram: WebGLProgram;
1279
1312
  private readonly depthInstancedUniforms: Record<string, WebGLUniformLocation>;
1313
+ /**
1314
+ * The cutout casters' depth programs, rigid and instanced: a leaf or a chain link casts the shape
1315
+ * in its texture's alpha, not its whole card. See `DEPTH_CUTOUT_FRAG`. Chosen per draw by
1316
+ * `cutoutOf`, so an opaque caster never touches them.
1317
+ */
1318
+ private readonly depthCutoutProgram: WebGLProgram;
1319
+ private readonly depthCutoutUniforms: Record<string, WebGLUniformLocation>;
1320
+ private readonly depthInstancedCutoutProgram: WebGLProgram;
1321
+ private readonly depthInstancedCutoutUniforms: Record<string, WebGLUniformLocation>;
1280
1322
  private readonly scatterDepthProgram: WebGLProgram;
1281
1323
  private readonly scatterDepthUniforms: Record<string, WebGLUniformLocation>;
1282
1324
  private readonly shadowMap: ShadowMap | null;
@@ -1335,6 +1377,10 @@ export class WebGL2Renderer implements RendererApi {
1335
1377
  private readonly probeCapture: ReflectionProbe | null;
1336
1378
  /** Every probe of the grid, as one array texture. Allocated when a grid is declared. */
1337
1379
  private probeArray: EnvProbeArray | null = null;
1380
+ /** Which layers a bake writes and the shading reads. See `probeSweeps.ts`. */
1381
+ private probeSweeps: ProbeSweeps | null = null;
1382
+ /** `uProbeGridSets`, rewritten in place. */
1383
+ private readonly probeSets = new Float32Array(3);
1338
1384
  /**
1339
1385
  * Where those probes stand. A single baked probe is a grid of one at that probe's own origin.
1340
1386
  *
@@ -1360,6 +1406,8 @@ export class WebGL2Renderer implements RendererApi {
1360
1406
  private warnedEnvironmentAmbient = false;
1361
1407
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
1362
1408
  private probePassActive = false;
1409
+ /** Whether the bake under way lights its faces by the grid. `ProbeBakeOptions.bounce`. */
1410
+ private probeBounce = false;
1363
1411
  /** The clip-control extension, or null where this context does not offer it. */
1364
1412
  private clipControl: ClipControlExtension | null = null;
1365
1413
  /**
@@ -1518,6 +1566,17 @@ export class WebGL2Renderer implements RendererApi {
1518
1566
  */
1519
1567
  private activeDepthViewProj: ReadonlyMat4 = IDENTITY_MAT4;
1520
1568
  private activeDepthPeel = false;
1569
+ /**
1570
+ * Whether a peel pass has filled the second layer since the first one last opened, which is
1571
+ * what the frame samples it on: the rule the other backend's `peelFilled` states, held here too.
1572
+ *
1573
+ * **Not whether the map exists.** That was the test here, on the belief that a GL texture is
1574
+ * born reading as the far plane; it is born zeroed, and a scene that built the layer and never
1575
+ * peeled it sampled an occluder at the light. `ShadowMap` now clears itself at birth, so an
1576
+ * unfilled layer would read as lit either way — this is what keeps the two backends one
1577
+ * decision, and a frame that does not peel from paying a fetch per tap for nothing.
1578
+ */
1579
+ private peelFilled = false;
1521
1580
  /**
1522
1581
  * Whether the depth pass this scatter draw interrupts wants back-face culling.
1523
1582
  *
@@ -1528,6 +1587,11 @@ export class WebGL2Renderer implements RendererApi {
1528
1587
  * cubemap — so the pass states what it wants and this restores that.
1529
1588
  */
1530
1589
  private depthPassCullsFaces = true;
1590
+ /**
1591
+ * Whether the bound material is seen from both faces, so its draws cull nothing. Toggled around
1592
+ * each such draw and handed back, where the other backend bakes it into a pipeline.
1593
+ */
1594
+ private materialDoubleSided = false;
1531
1595
 
1532
1596
  /**
1533
1597
  * The one sink handed to every caster enumeration.
@@ -1536,16 +1600,34 @@ export class WebGL2Renderer implements RendererApi {
1536
1600
  * per pass would allocate six objects per light per frame.
1537
1601
  */
1538
1602
  private readonly casterSink: ShadowCasterSink = {
1539
- mesh: (mesh, model) => {
1603
+ mesh: (mesh, model, material) => {
1540
1604
  this.shadowDrawBudget.ask();
1541
1605
  const { gl } = this;
1606
+ /* A two-sided caster casts from whichever face the light sees: nothing culled. */
1607
+ const twoSided = material?.doubleSided === true;
1608
+ const cutout = cutoutOf(material);
1609
+ if (cutout !== null) {
1610
+ const u = this.depthCutoutUniforms;
1611
+ gl.useProgram(this.depthCutoutProgram);
1612
+ this.bindCutoutDepth(u, cutout);
1613
+ gl.uniformMatrix4fv(u['uModel'] ?? null, false, model);
1614
+ if (twoSided) gl.disable(gl.CULL_FACE);
1615
+ (mesh as Mesh).draw(gl);
1616
+ if (twoSided && this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
1617
+ /* Hand the pass back as it was found: the next caster may be rigid and opaque. */
1618
+ gl.useProgram(this.depthProgram);
1619
+ this.restoreSurfaceTextureUnit();
1620
+ return;
1621
+ }
1542
1622
  gl.uniformMatrix4fv(this.depthUniforms['uModel'] ?? null, false, model);
1543
1623
  /*
1544
1624
  * The sink takes the opaque handle both backends share, and this renderer only ever
1545
1625
  * handed out its own `Mesh`. Narrowing is safe here for that reason and nowhere else:
1546
1626
  * a handle from the other backend would have to have crossed renderers to arrive.
1547
1627
  */
1628
+ if (twoSided) gl.disable(gl.CULL_FACE);
1548
1629
  (mesh as Mesh).draw(gl);
1630
+ if (twoSided && this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
1549
1631
  },
1550
1632
  /**
1551
1633
  * A batch into the depth map, placed by the same matrices the visible draw uses.
@@ -1553,18 +1635,31 @@ export class WebGL2Renderer implements RendererApi {
1553
1635
  * No `uModel`: the instanced depth program has none, its placement being the four attribute
1554
1636
  * columns the batch's own buffer supplies.
1555
1637
  */
1556
- instanced: (batch, data) => {
1638
+ instanced: (batch, data, material) => {
1557
1639
  const { gl } = this;
1558
1640
  const gpuBatch = batch as InstancedBatch;
1559
1641
  const count = Math.min(data.count, gpuBatch.capacity);
1560
1642
  if (count === 0) return;
1561
1643
  this.shadowDrawBudget.ask();
1644
+ const twoSided = material?.doubleSided === true;
1645
+ if (twoSided) gl.disable(gl.CULL_FACE);
1646
+ const cutout = cutoutOf(material);
1647
+ if (cutout !== null) {
1648
+ gl.useProgram(this.depthInstancedCutoutProgram);
1649
+ this.bindCutoutDepth(this.depthInstancedCutoutUniforms, cutout);
1650
+ gpuBatch.mesh.drawInstances(gl, count);
1651
+ if (twoSided && this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
1652
+ gl.useProgram(this.depthProgram);
1653
+ this.restoreSurfaceTextureUnit();
1654
+ return;
1655
+ }
1562
1656
  const u = this.depthInstancedUniforms;
1563
1657
  gl.useProgram(this.depthInstancedProgram);
1564
1658
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, this.activeDepthViewProj);
1565
1659
  gl.uniform1i(u['uPeelShadowLayer'] ?? null, this.activeDepthPeel ? 1 : 0);
1566
1660
  if (this.activeDepthPeel) gl.uniform1i(u['uPreviousShadowMap'] ?? null, 0);
1567
1661
  gpuBatch.mesh.drawInstances(gl, count);
1662
+ if (twoSided && this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
1568
1663
  gl.useProgram(this.depthProgram);
1569
1664
  },
1570
1665
  skinnedMesh: (mesh, model, palette) => {
@@ -1612,6 +1707,24 @@ export class WebGL2Renderer implements RendererApi {
1612
1707
  },
1613
1708
  };
1614
1709
 
1710
+ /**
1711
+ * What a cutout depth draw needs beyond the plain one: the pass's matrix and peel, the material's
1712
+ * cutoff and UV scale, and its albedo on the surface unit, which the caller gives back after.
1713
+ */
1714
+ private bindCutoutDepth(
1715
+ u: Record<string, WebGLUniformLocation>,
1716
+ cutout: CutoutCaster<SurfaceTextureHandle>,
1717
+ ): void {
1718
+ const { gl } = this;
1719
+ gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, this.activeDepthViewProj);
1720
+ gl.uniform1i(u['uPeelShadowLayer'] ?? null, this.activeDepthPeel ? 1 : 0);
1721
+ if (this.activeDepthPeel) gl.uniform1i(u['uPreviousShadowMap'] ?? null, 0);
1722
+ gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
1723
+ gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
1724
+ /* Narrowed for the reason the sink's own `mesh` gives: this renderer only hands out its own. */
1725
+ (cutout.albedo as SurfaceTexture).bind(gl, SURFACE_TEXTURE_UNIT);
1726
+ }
1727
+
1615
1728
  private readonly drawPointShadowFace = (viewProj: ReadonlyMat4): void => {
1616
1729
  const casters = this.pointShadowCasters;
1617
1730
  if (casters === null) return;
@@ -2262,6 +2375,8 @@ export class WebGL2Renderer implements RendererApi {
2262
2375
  * "unbound". Two 1x1 uploads for the life of the renderer.
2263
2376
  */
2264
2377
  this.emptyTexture2D = createEmptyTexture2D(gl);
2378
+ this.emptyDriftIndex = createEmptyTexture3D(gl, true);
2379
+ this.emptyDriftAtlas = createEmptyTexture3D(gl, false);
2265
2380
  this.emptyTexture2DArray = createEmptyTexture2DArray(gl);
2266
2381
  this.emptyTextureCube = createEmptyTextureCube(gl);
2267
2382
  this.gpuTimer = new GpuTimer(gl);
@@ -2483,6 +2598,7 @@ export class WebGL2Renderer implements RendererApi {
2483
2598
  this.panelVao = panelVao;
2484
2599
  gl.bindVertexArray(panelVao);
2485
2600
  const panelBuffer = gl.createBuffer();
2601
+ this.panelBuffer = panelBuffer;
2486
2602
  gl.bindBuffer(gl.ARRAY_BUFFER, panelBuffer);
2487
2603
  // A unit quad. The rectangle it becomes is a uniform, so this is uploaded once
2488
2604
  // for the life of the renderer however many panels are drawn.
@@ -2570,6 +2686,33 @@ export class WebGL2Renderer implements RendererApi {
2570
2686
  */
2571
2687
  this.scatterDepthProgram = compileProgram(gl, SCATTER_DEPTH_VERT, DEPTH_FRAG, 'scatterDepth');
2572
2688
  this.scatterDepthUniforms = uniformLocations(gl, this.scatterDepthProgram, 'scatterDepth');
2689
+ this.depthCutoutProgram = compileProgram(
2690
+ gl,
2691
+ DEPTH_CUTOUT_VERT,
2692
+ DEPTH_CUTOUT_FRAG,
2693
+ 'depth-cutout',
2694
+ );
2695
+ this.depthCutoutUniforms = uniformLocations(gl, this.depthCutoutProgram, 'depth-cutout');
2696
+ this.depthInstancedCutoutProgram = compileProgram(
2697
+ gl,
2698
+ DEPTH_INSTANCED_CUTOUT_VERT,
2699
+ DEPTH_CUTOUT_FRAG,
2700
+ 'depth-instanced-cutout',
2701
+ );
2702
+ this.depthInstancedCutoutUniforms = uniformLocations(
2703
+ gl,
2704
+ this.depthInstancedCutoutProgram,
2705
+ 'depth-instanced-cutout',
2706
+ );
2707
+ /* The cutout map is always read from the surface unit, which the draw borrows and gives back. */
2708
+ for (const [program, uniforms] of [
2709
+ [this.depthCutoutProgram, this.depthCutoutUniforms],
2710
+ [this.depthInstancedCutoutProgram, this.depthInstancedCutoutUniforms],
2711
+ ] as const) {
2712
+ gl.useProgram(program);
2713
+ gl.uniform1i(uniforms['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
2714
+ }
2715
+ gl.useProgram(null);
2573
2716
  this.shadowMap = this.quality.directionalShadows
2574
2717
  ? new ShadowMap(gl, this.quality.directionalShadowMapSize)
2575
2718
  : null;
@@ -2736,6 +2879,7 @@ export class WebGL2Renderer implements RendererApi {
2736
2879
  this.flatSkinnedProgram,
2737
2880
  this.flatMorphedProgram,
2738
2881
  this.flatBothProgram,
2882
+ this.flatInstancedProgram,
2739
2883
  ]) {
2740
2884
  if (extra !== null) gl.deleteProgram(extra);
2741
2885
  }
@@ -2746,12 +2890,20 @@ export class WebGL2Renderer implements RendererApi {
2746
2890
  this.skyProgram,
2747
2891
  this.scatterProgram,
2748
2892
  this.depthProgram,
2893
+ this.depthSkinnedProgram,
2894
+ this.depthInstancedProgram,
2749
2895
  this.scatterDepthProgram,
2896
+ this.lightVolumeProgram,
2897
+ this.depthCutoutProgram,
2898
+ this.depthInstancedCutoutProgram,
2750
2899
  ]) {
2751
2900
  gl.deleteProgram(program);
2752
2901
  }
2753
2902
  this.skinPalette.dispose(gl);
2754
2903
  gl.deleteTexture(this.emptyTexture2D);
2904
+ gl.deleteTexture(this.emptyDriftIndex);
2905
+ gl.deleteTexture(this.emptyDriftAtlas);
2906
+ this.forgetLightFieldVolumes();
2755
2907
  gl.deleteTexture(this.emptyTexture2DArray);
2756
2908
  gl.deleteTexture(this.emptyTextureCube);
2757
2909
  /* A cubemap and its whole mip chain, which is the largest single thing this
@@ -2759,6 +2911,24 @@ export class WebGL2Renderer implements RendererApi {
2759
2911
  this.probeCapture?.dispose(gl);
2760
2912
  this.probeArray?.dispose(gl);
2761
2913
  this.environmentPrefilter?.dispose(gl);
2914
+ /*
2915
+ * **Everything else the constructor made, which this method never released.** The screen
2916
+ * chain and its bloom and occlusion, three sun maps, the mirror, the point-shadow array, two
2917
+ * vertex arrays, a quad and three textures made on first use. A page swapping renderers kept
2918
+ * every one of them, per renderer, for as long as it lived; `renderer.test.ts` counts them.
2919
+ */
2920
+ this.sceneTarget?.dispose();
2921
+ this.shadowMap?.dispose(gl);
2922
+ this.peeledShadowMap?.dispose(gl);
2923
+ this.dynamicShadowMap?.dispose(gl);
2924
+ this.planarReflection?.dispose(gl);
2925
+ this.pointShadows?.dispose();
2926
+ gl.deleteVertexArray(this.panelVao);
2927
+ gl.deleteVertexArray(this.skyVao);
2928
+ if (this.panelBuffer !== null) gl.deleteBuffer(this.panelBuffer);
2929
+ if (this.cookieTexture !== null) gl.deleteTexture(this.cookieTexture);
2930
+ if (this.iesTexture !== null) gl.deleteTexture(this.iesTexture);
2931
+ if (this.clusterTexture !== null) gl.deleteTexture(this.clusterTexture);
2762
2932
  }
2763
2933
 
2764
2934
  if (options.releaseContext === true) {
@@ -2987,6 +3157,7 @@ export class WebGL2Renderer implements RendererApi {
2987
3157
  if (this.contextLost) return;
2988
3158
  this.materials.dirty();
2989
3159
  this.currentSurfaceTexture = material?.albedo ?? null;
3160
+ this.materialDoubleSided = material?.doubleSided === true;
2990
3161
  /*
2991
3162
  * Written to every flat program, because material state persists across draws and a skinned
2992
3163
  * draw is entitled to the material the caller set before it. A second program holding none of
@@ -3066,10 +3237,11 @@ export class WebGL2Renderer implements RendererApi {
3066
3237
  * The emissive map, on exactly the terms the two maps above it are bound: before the albedo
3067
3238
  * early return, and the stand-in rather than `null`.
3068
3239
  *
3069
- * **White is the right stand-in here and black would be a bug.** The shader multiplies by this
3070
- * map, so the placeholder has to be the multiplicative identity; `emptyTexture2D` is white,
3071
- * which is what makes an unbound material collapse to the arithmetic it had before. A black
3072
- * placeholder would switch every glow in the scene off the moment the gate was ever wrong.
3240
+ * **The stand-in is black, so the gate is what keeps a glow on.** The shader multiplies by
3241
+ * this map, and `emptyTexture2D` is opaque black rather than the multiplicative identity, so an
3242
+ * unbound material keeps the arithmetic it had before only because `uEmissiveMapEnabled` is 0
3243
+ * and the read never happens. What would make that wrong is a read taken outside that branch:
3244
+ * every glow on a material without a map would go out.
3073
3245
  */
3074
3246
  const emissiveMap = material?.emissive ?? null;
3075
3247
  gl.activeTexture(gl.TEXTURE0 + EMISSIVE_TEXTURE_UNIT);
@@ -3093,6 +3265,7 @@ export class WebGL2Renderer implements RendererApi {
3093
3265
  * how big a dome was — WebGPU writes it before its own early return and WebGL2 did not.
3094
3266
  */
3095
3267
  gl.uniform2f(u['uUvScale'] ?? null, material?.uScale ?? 1, material?.vScale ?? 1);
3268
+ gl.uniform1i(u['uDoubleSided'] ?? null, material?.doubleSided === true ? 1 : 0);
3096
3269
 
3097
3270
  if (albedo === null) {
3098
3271
  gl.uniform1i(u['uAlbedoEnabled'] ?? null, 0);
@@ -3889,7 +4062,9 @@ export class WebGL2Renderer implements RendererApi {
3889
4062
  this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0),
3890
4063
  this.quality.underwaterAtmosphere,
3891
4064
  );
4065
+ if (this.materialDoubleSided) gl.disable(gl.CULL_FACE);
3892
4066
  batch.drawTo(gl, data.count);
4067
+ if (this.materialDoubleSided) gl.enable(gl.CULL_FACE);
3893
4068
  }
3894
4069
 
3895
4070
  /** Build a batch for electrical arcs. `capacity` is in segments, not arcs. */
@@ -4563,6 +4738,8 @@ export class WebGL2Renderer implements RendererApi {
4563
4738
  this.gpuTimer.begin('shadows');
4564
4739
  this.shadowPassActive = true;
4565
4740
  this.activeShadowMap = shadowMap;
4741
+ /* The static layer opens first, so a frame's peel counts from here. See `peelFilled`. */
4742
+ if (layer === 'static') this.peelFilled = false;
4566
4743
  /*
4567
4744
  * **Range-corrected but not reversed.** `EXT_clip_control` is context state, so a light matrix
4568
4745
  * that still emits OpenGL's `[-1, 1]` loses everything below zero the moment it is on. See
@@ -4620,6 +4797,8 @@ export class WebGL2Renderer implements RendererApi {
4620
4797
  this.uploadWind(this.depthUniforms, this.frameWind);
4621
4798
  gl.useProgram(this.depthSkinnedProgram);
4622
4799
  this.uploadWind(this.depthSkinnedUniforms, this.frameWind);
4800
+ gl.useProgram(this.depthCutoutProgram);
4801
+ this.uploadWind(this.depthCutoutUniforms, this.frameWind);
4623
4802
  gl.useProgram(this.depthProgram);
4624
4803
  }
4625
4804
 
@@ -4711,6 +4890,7 @@ export class WebGL2Renderer implements RendererApi {
4711
4890
  const shadowMap = this.activeShadowMap;
4712
4891
  if (!this.shadowPassActive || shadowMap === null) return;
4713
4892
  shadowMap.end(this.gl);
4893
+ if (shadowMap === this.peeledShadowMap) this.peelFilled = true;
4714
4894
  /* Back to the frame's sense; see `beginShadowPass` for why it left it. */
4715
4895
  this.gl.depthFunc(glDepthFuncEqual(this.gl));
4716
4896
  this.gl.clearDepth(this.depthClear);
@@ -5441,6 +5621,73 @@ export class WebGL2Renderer implements RendererApi {
5441
5621
  this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
5442
5622
  }
5443
5623
 
5624
+ /**
5625
+ * The camera has cut: this frame is a new shot, not the next moment of the last one.
5626
+ *
5627
+ * **Everything temporal reprojects through the previous frame's view**: the motion blur measures
5628
+ * its smear against it, and the temporal resolve reads its history through it. A cut is a jump
5629
+ * the renderer cannot tell from a fast camera, so without this a transport that seeks, a respawn
5630
+ * or an edit smears its first frame along the whole jump and blends in a picture of somewhere
5631
+ * else. The caller knows it cut; this is how it says so, before the frame's `beginFrame`.
5632
+ *
5633
+ * What it gives up is one frame of history: the frame after a cut has no blur and no temporal
5634
+ * blend, as the first frame of a session has none. Nothing else is reset.
5635
+ */
5636
+ cameraCut(): void {
5637
+ this.hasPreviousView = false;
5638
+ this.temporalHistory.invalidate();
5639
+ /* The eye was adapted to the last shot. */
5640
+ this.sceneTarget?.cutExposure();
5641
+ }
5642
+
5643
+ /**
5644
+ * **Eye adaptation**: how far the frame's exposure follows its own brightness, 0 to 1, and the
5645
+ * seconds since the last frame. 0 is off and the default, and at 0 nothing is metered or allocated.
5646
+ *
5647
+ * The finished scene is metered on the GPU each frame — the log-mean of its luminance — and a
5648
+ * held brightness moves toward it at a fixed rate, about 78% of the way in a second; the composite
5649
+ * then scales scene light toward middle grey by \`strength\` of the stops between them, at most six
5650
+ * either way. So a shaded courtyard floor opens up the way a camera or an eye would, and
5651
+ * \`setOutputExposure\` becomes a bias on top: a night kept darker than a day is a bias under one.
5652
+ * **The time is the caller's**, because the engine takes time from its caller; a held capture
5653
+ * passes 0 and holds. \`cameraCut\` snaps it, so a new shot is metered rather than eased into.
5654
+ *
5655
+ * What it gives up is regional metering: every pixel counts the same. Needs \`screenEffects\` and
5656
+ * \`hdrScene\`, and says so once rather than doing nothing. See \`shaders/exposure.ts\`.
5657
+ */
5658
+ setAutoExposure(strength: number, dtSec: number): void {
5659
+ if (this.sceneTarget === null || !this.quality.hdrScene) {
5660
+ this.warnLookWithoutComposite(strength);
5661
+ return;
5662
+ }
5663
+ this.sceneTarget.setAutoExposure(strength, dtSec);
5664
+ }
5665
+
5666
+ /**
5667
+ * **Local exposure**: how far each region of the frame is brought toward the frame's own
5668
+ * brightness, 0 to 1. 0 is off and the default, and at 0 nothing more is measured or allocated.
5669
+ *
5670
+ * A tone curve maps one range, and a sunlit courtyard is two: its shaded arcades hold a fiftieth
5671
+ * of the light on its sunlit paving, so exposed for either the other is black or white. This moves
5672
+ * each region `strength` of the stops between it and the frame's held brightness, at most three,
5673
+ * read from a bilateral grid over the exposure meter's tiles, so shade beside sun is lifted as
5674
+ * shade rather than as the average of the two. The eye's held brightness is what regions move
5675
+ * toward, so it is measured whether or not `setAutoExposure` is on; with adaptation off the frame
5676
+ * as a whole keeps the exposure it was given. About 0.5 is a photograph's latitude, and 1 is a
5677
+ * frame with no light and shade left in it.
5678
+ *
5679
+ * What it gives up is contrast between regions, which is the point, and a halo a tile wide where
5680
+ * two regions of one brightness band differ. Needs `screenEffects` and `hdrScene`, and says so
5681
+ * once rather than doing nothing. See `shaders/localExposure.ts`.
5682
+ */
5683
+ setLocalExposure(strength: number): void {
5684
+ if (this.sceneTarget === null || !this.quality.hdrScene) {
5685
+ this.warnLookWithoutComposite(strength);
5686
+ return;
5687
+ }
5688
+ this.sceneTarget.setLocalExposure(strength);
5689
+ }
5690
+
5444
5691
  /**
5445
5692
  * Where this frame's lens is focused, how deep the sharp zone is, and how much of the ceiling
5446
5693
  * to take.
@@ -5495,9 +5742,17 @@ export class WebGL2Renderer implements RendererApi {
5495
5742
  *
5496
5743
  * Set before `beginFrame` and held until changed. Ignored entirely when `bloom` is 0, since
5497
5744
  * the chain is never built.
5745
+ *
5746
+ * **`threshold` moves `bloomThreshold` for this frame and every one after**, in the same scene
5747
+ * units. A threshold is compared before exposure is applied, so one fixed at construction means a
5748
+ * different brightness on screen at every exposure. That is harmless while exposure stands still
5749
+ * and wrong for a day whose exposure spans 2.5 to 14: a courtyard blooming at noon, or candles
5750
+ * never blooming at night. A caller whose exposure moves passes what it wants on screen over the
5751
+ * exposure. Omitted leaves it where it was, so a caller that never passes one keeps the profile's.
5498
5752
  */
5499
- setBloom(scale: number): void {
5753
+ setBloom(scale: number, threshold?: number): void {
5500
5754
  this.bloomScale = Math.min(Math.max(scale, 0), 1);
5755
+ if (threshold !== undefined) this.bloomThresholdSet = bloomThresholdOf(threshold);
5501
5756
  }
5502
5757
 
5503
5758
  /**
@@ -5644,6 +5899,41 @@ export class WebGL2Renderer implements RendererApi {
5644
5899
  this.sceneTarget.setColourGrade(lut, strength);
5645
5900
  }
5646
5901
 
5902
+ /**
5903
+ * How strongly the lens darkens the frame's corners, held until changed: 0 is none, 0.5 is about
5904
+ * 1.2 stops at the corner. Applied to scene light before the tone curve, as a lens loses light;
5905
+ * see `filmLook.ts`. Needs `screenEffects`, and says so once rather than doing nothing.
5906
+ */
5907
+ setVignette(strength: number): void {
5908
+ if (this.sceneTarget === null) {
5909
+ this.warnLookWithoutComposite(strength);
5910
+ return;
5911
+ }
5912
+ this.sceneTarget.setVignette(strength);
5913
+ }
5914
+
5915
+ /**
5916
+ * Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
5917
+ * grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
5918
+ * its caller: pass a new one each frame for grain that moves, and the same one for a still that
5919
+ * is identical run to run. Applied last, after the grade and the veil; see `filmLook.ts`.
5920
+ */
5921
+ setFilmGrain(strength: number, seed: number): void {
5922
+ if (this.sceneTarget === null) {
5923
+ this.warnLookWithoutComposite(strength);
5924
+ return;
5925
+ }
5926
+ this.sceneTarget.setFilmGrain(strength, seed);
5927
+ }
5928
+
5929
+ private warnedLookWithoutComposite = false;
5930
+ private warnLookWithoutComposite(strength: number): void {
5931
+ if (strength > 0 && !this.warnedLookWithoutComposite) {
5932
+ this.warnedLookWithoutComposite = true;
5933
+ console.warn(FILM_LOOK_WITHOUT_COMPOSITE);
5934
+ }
5935
+ }
5936
+
5647
5937
  /**
5648
5938
  * What this frame asked for. **Every ceiling here is `null`, and that is the finding.**
5649
5939
  *
@@ -5982,7 +6272,10 @@ export class WebGL2Renderer implements RendererApi {
5982
6272
  const bloomStrength = this.quality.bloom * this.bloomScale;
5983
6273
  const bloom =
5984
6274
  bloomStrength > 0
5985
- ? { strength: bloomStrength, threshold: this.quality.bloomThreshold }
6275
+ ? {
6276
+ strength: bloomStrength,
6277
+ threshold: this.bloomThresholdSet ?? this.quality.bloomThreshold,
6278
+ }
5986
6279
  : undefined;
5987
6280
 
5988
6281
  /* The grade travels to the resolve only where the mesh pass gave it up. See `bindMeshPass`. */
@@ -6189,11 +6482,12 @@ export class WebGL2Renderer implements RendererApi {
6189
6482
  sunShadow,
6190
6483
  lightViewProj: env?.lightViewProj ?? IDENTITY_MAT4,
6191
6484
  /* The placeholder rather than null where a map is merely absent, for the reason
6192
- `emptyTexture.ts` gives: white is depth 1, the far plane, nothing in the way. */
6485
+ `emptyTexture.ts` gives. It is black, which as a depth is an occluder at the light,
6486
+ so it is safe only because `sunShadow` is 0 whenever these maps do not exist. */
6193
6487
  staticShadowMap: this.shadowMap?.texture ?? this.emptyTexture2D,
6194
6488
  peeledShadowMap: this.peeledShadowMap?.texture ?? this.emptyTexture2D,
6195
6489
  dynamicShadowMap: this.dynamicShadowMap?.texture ?? this.emptyTexture2D,
6196
- peeledEnabled: this.peeledShadowMap !== null,
6490
+ peeledEnabled: this.peelFilled,
6197
6491
  },
6198
6492
  );
6199
6493
  if (marched) {
@@ -6415,6 +6709,100 @@ export class WebGL2Renderer implements RendererApi {
6415
6709
  this.useFlatProgram();
6416
6710
  }
6417
6711
 
6712
+ /**
6713
+ * Sum a scene's many fixed lights into a DriftLight field, which stands in for each of them
6714
+ * wherever the frame's choice of exact lights does not reach. See `driftLight/lightField.ts`.
6715
+ *
6716
+ * Replaces any field made before. The scene then paces `field.bake(bricks)` and calls
6717
+ * `field.follow(buffer.complete, ...)` each frame with the selection that chose its exact lights;
6718
+ * the volume is summed into the frame once every brick has landed.
6719
+ */
6720
+ createLightField(
6721
+ lights: readonly LightFieldSource[],
6722
+ options: Omit<LightFieldOptions, 'falloff'> = {},
6723
+ ): LightField {
6724
+ this.forgetLightFieldVolumes();
6725
+ const field = new LightField(lights, { ...options, falloff: this.quality.pointLightFalloff });
6726
+ this.lightField = field;
6727
+ return field;
6728
+ }
6729
+
6730
+ /** Let go of the field and its volumes; the scene's lights are all exact again. */
6731
+ disposeLightField(): void {
6732
+ this.forgetLightFieldVolumes();
6733
+ this.lightField = null;
6734
+ }
6735
+
6736
+ private forgetLightFieldVolumes(): void {
6737
+ if (this.driftIndex !== null) this.gl.deleteTexture(this.driftIndex);
6738
+ if (this.driftAtlas !== null) this.gl.deleteTexture(this.driftAtlas);
6739
+ this.driftIndex = null;
6740
+ this.driftAtlas = null;
6741
+ }
6742
+
6743
+ /**
6744
+ * Upload the field's two volumes, once, when its last brick has baked: nothing samples them
6745
+ * before that, so uploading as bricks land would be work for no picture.
6746
+ */
6747
+ private uploadLightField(): void {
6748
+ const field = this.lightField;
6749
+ if (field === null || this.driftAtlas !== null || !field.ready || field.layout.count === 0) {
6750
+ return;
6751
+ }
6752
+ const { gl } = this;
6753
+ const [ix, iy, iz] = field.layout.dims;
6754
+ const index = gl.createTexture();
6755
+ gl.bindTexture(gl.TEXTURE_3D, index);
6756
+ gl.texImage3D(
6757
+ gl.TEXTURE_3D,
6758
+ 0,
6759
+ gl.R32UI,
6760
+ ix,
6761
+ iy,
6762
+ iz,
6763
+ 0,
6764
+ gl.RED_INTEGER,
6765
+ gl.UNSIGNED_INT,
6766
+ field.layout.index,
6767
+ );
6768
+ gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
6769
+ gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
6770
+ const [aw, ah, ad] = field.atlasSize;
6771
+ const atlas = gl.createTexture();
6772
+ gl.bindTexture(gl.TEXTURE_3D, atlas);
6773
+ gl.texImage3D(gl.TEXTURE_3D, 0, gl.RGBA16F, aw, ah, ad, 0, gl.RGBA, gl.HALF_FLOAT, field.atlas);
6774
+ /* Filtered, because a brick's samples share their faces: the lookup is seamless inside one. */
6775
+ gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
6776
+ gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
6777
+ for (const wrap of [gl.TEXTURE_WRAP_S, gl.TEXTURE_WRAP_T, gl.TEXTURE_WRAP_R]) {
6778
+ gl.texParameteri(gl.TEXTURE_3D, wrap, gl.CLAMP_TO_EDGE);
6779
+ }
6780
+ gl.bindTexture(gl.TEXTURE_3D, null);
6781
+ this.driftIndex = index;
6782
+ this.driftAtlas = atlas;
6783
+ }
6784
+
6785
+ /** The field's uniforms and volumes for this pass, or the stand-ins that switch it off. */
6786
+ private bindDriftLight(u: Record<string, WebGLUniformLocation>, eye: ArrayLike<number>): void {
6787
+ const { gl } = this;
6788
+ this.uploadLightField();
6789
+ const d = this.driftUniforms;
6790
+ resolveDriftLight(
6791
+ this.driftAtlas === null ? null : this.lightField,
6792
+ this.probePassActive,
6793
+ eye,
6794
+ d,
6795
+ );
6796
+ gl.uniform4fv(u['uDriftLight'] ?? null, d.light);
6797
+ gl.uniform4fv(u['uDriftLightOrigin'] ?? null, d.origin);
6798
+ gl.activeTexture(gl.TEXTURE0 + DRIFT_LIGHT_INDEX_TEXTURE_UNIT);
6799
+ gl.bindTexture(gl.TEXTURE_3D, this.driftIndex ?? this.emptyDriftIndex);
6800
+ gl.uniform1i(u['uDriftLightIndex'] ?? null, DRIFT_LIGHT_INDEX_TEXTURE_UNIT);
6801
+ gl.activeTexture(gl.TEXTURE0 + DRIFT_LIGHT_ATLAS_TEXTURE_UNIT);
6802
+ gl.bindTexture(gl.TEXTURE_3D, this.driftAtlas ?? this.emptyDriftAtlas);
6803
+ gl.uniform1i(u['uDriftLightAtlas'] ?? null, DRIFT_LIGHT_ATLAS_TEXTURE_UNIT);
6804
+ }
6805
+
6418
6806
  /**
6419
6807
  * Everything one flat program needs to know about this frame.
6420
6808
  *
@@ -6553,7 +6941,7 @@ export class WebGL2Renderer implements RendererApi {
6553
6941
  gl.uniform1f(u['uShadowMapSize'] ?? null, this.quality.directionalShadowMapSize);
6554
6942
  gl.uniform1f(u['uShadowDepthSpan'] ?? null, env.shadowDepthSpan);
6555
6943
  gl.uniform1f(u['uShadowMaxDistance'] ?? null, this.quality.directionalShadowMaxDistance);
6556
- gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peeledShadowMap === null ? 0 : 1);
6944
+ gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peelFilled ? 1 : 0);
6557
6945
  gl.uniform1f(u['uShadowMaxSlope'] ?? null, this.quality.directionalShadowMaxSlope);
6558
6946
  gl.uniform1i(u['uShadowFilterTaps'] ?? null, this.quality.shadowFilterTaps);
6559
6947
  gl.uniform3fv(u['uHighlightMin'] ?? null, env.highlightMin);
@@ -6633,11 +7021,20 @@ export class WebGL2Renderer implements RendererApi {
6633
7021
  * zero would not be enough on its own — a driver may fetch a sampler's descriptor before it
6634
7022
  * evaluates the arithmetic that discards the result, which is the same reason
6635
7023
  * `emptyTexture.ts` exists.
7024
+ *
7025
+ * **Except a bounce bake, which reads it on purpose.** Its faces are drawn into the capture
7026
+ * cube, and the array is attached only by the convolution after them, so no pass both samples
7027
+ * and attaches it.
6636
7028
  */
6637
7029
  if (this.probeCapture !== null) {
6638
7030
  const array = this.probeArray;
6639
7031
  const grid = this.probes;
6640
- const usable = array !== null && grid !== null && array.ready && !this.probePassActive;
7032
+ const usable =
7033
+ array !== null &&
7034
+ grid !== null &&
7035
+ array.usable &&
7036
+ this.probeSweeps?.ready === true &&
7037
+ (!this.probePassActive || this.probeBounce);
6641
7038
  gl.activeTexture(gl.TEXTURE0 + ENVIRONMENT_TEXTURE_UNIT);
6642
7039
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, usable ? array.texture : this.emptyTexture2DArray);
6643
7040
  gl.uniform1i(u['uEnvironment'] ?? null, ENVIRONMENT_TEXTURE_UNIT);
@@ -6678,14 +7075,21 @@ export class WebGL2Renderer implements RendererApi {
6678
7075
  * times over two days as four different bugs before it could be asked for.
6679
7076
  */
6680
7077
  gl.uniform1f(u['uProbeGridAmbient'] ?? null, usable && this.probeAmbient ? 1 : 0);
7078
+ /* Which sets the shading reads, and how far between them. See `probeSweeps.ts`. */
7079
+ const sets = this.probeSweeps?.uniforms(this.probeSets) ?? this.probeSets.fill(0);
7080
+ gl.uniform3f(u['uProbeGridSets'] ?? null, sets[0] ?? 0, sets[1] ?? 0, sets[2] ?? 0);
6681
7081
  }
6682
7082
 
7083
+ this.bindDriftLight(u, camera.position);
7084
+
6683
7085
  gl.activeTexture(gl.TEXTURE0 + SURFACE_TEXTURE_UNIT);
6684
7086
  gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6685
7087
  gl.uniform1i(u['uAlbedo'] ?? null, SURFACE_TEXTURE_UNIT);
6686
7088
  gl.uniform1i(u['uAlbedoEnabled'] ?? null, 0);
6687
7089
  gl.uniform2f(u['uUvScale'] ?? null, 1, 1);
6688
7090
  gl.uniform1f(u['uAlbedoCutout'] ?? null, 0);
7091
+ gl.uniform1i(u['uDoubleSided'] ?? null, 0);
7092
+ this.materialDoubleSided = false;
6689
7093
  this.currentSurfaceTexture = null;
6690
7094
 
6691
7095
  /*
@@ -6918,7 +7322,9 @@ export class WebGL2Renderer implements RendererApi {
6918
7322
  */
6919
7323
  gl.depthFunc(this.reversedDepth ? gl.GREATER : gl.LESS);
6920
7324
  if (layer === 0) {
7325
+ if (this.materialDoubleSided) gl.disable(gl.CULL_FACE);
6921
7326
  mesh.draw(gl);
7327
+ if (this.materialDoubleSided) gl.enable(gl.CULL_FACE);
6922
7328
  gl.depthFunc(this.reversedDepth ? gl.GEQUAL : gl.LEQUAL);
6923
7329
  if (tint !== null) gl.uniform3fv(u['uTint'] ?? null, WHITE_TINT);
6924
7330
  if (program !== null) this.useFlatProgram();
@@ -6937,7 +7343,9 @@ export class WebGL2Renderer implements RendererApi {
6937
7343
  */
6938
7344
  const offset = depthOffsetForLayer(layer, this.reversedDepth);
6939
7345
  gl.polygonOffset(offset.slope, offset.units);
7346
+ if (this.materialDoubleSided) gl.disable(gl.CULL_FACE);
6940
7347
  mesh.draw(gl);
7348
+ if (this.materialDoubleSided) gl.enable(gl.CULL_FACE);
6941
7349
  gl.polygonOffset(0, 0);
6942
7350
  gl.disable(gl.POLYGON_OFFSET_FILL);
6943
7351
  if (program !== null) this.useFlatProgram();
@@ -7090,7 +7498,9 @@ export class WebGL2Renderer implements RendererApi {
7090
7498
  gl.enable(gl.POLYGON_OFFSET_FILL);
7091
7499
  gl.polygonOffset(offset.slope, offset.units);
7092
7500
  }
7501
+ if (this.materialDoubleSided) gl.disable(gl.CULL_FACE);
7093
7502
  mesh.draw(gl);
7503
+ if (this.materialDoubleSided) gl.enable(gl.CULL_FACE);
7094
7504
  if (offset.units !== 0) {
7095
7505
  gl.polygonOffset(0, 0);
7096
7506
  gl.disable(gl.POLYGON_OFFSET_FILL);
@@ -7163,10 +7573,11 @@ export class WebGL2Renderer implements RendererApi {
7163
7573
 
7164
7574
  this.probeArray?.dispose(this.gl);
7165
7575
  this.probes = grid;
7576
+ this.probeSweeps = new ProbeSweeps(grid.layers, grid.crossfade);
7166
7577
  this.probeArray = new EnvProbeArray(
7167
7578
  this.gl,
7168
7579
  octahedralEdgeFor(this.quality.reflectionProbeSize),
7169
- grid.layers,
7580
+ grid.layers * this.probeSweeps.sets,
7170
7581
  this.quality.hdrScene,
7171
7582
  );
7172
7583
  return this.probeArray.usable;
@@ -7193,12 +7604,14 @@ export class WebGL2Renderer implements RendererApi {
7193
7604
  const probe = this.probeCapture;
7194
7605
  const array = this.probeArray;
7195
7606
  const grid = this.probes;
7196
- if (probe === null || array === null || grid === null) return false;
7607
+ const sweeps = this.probeSweeps;
7608
+ if (probe === null || array === null || grid === null || sweeps === null) return false;
7197
7609
  if (layer < 0 || layer >= grid.layers) return false;
7198
7610
 
7199
7611
  this.probeAmbient = options?.irradiance ?? true;
7200
7612
  grid.positionOf(layer, this.probeOrigin);
7201
7613
 
7614
+ this.probeBounce = options?.bounce ?? false;
7202
7615
  this.probePassActive = true;
7203
7616
  /*
7204
7617
  * **A probe stores radiance, not display pixels.**
@@ -7236,7 +7649,10 @@ export class WebGL2Renderer implements RendererApi {
7236
7649
  this.gl.frontFace(this.gl.CW);
7237
7650
  let baked = false;
7238
7651
  try {
7239
- baked = probe.bake(this.gl, this.probeOrigin, clearColor, drawFace);
7652
+ const [first, end] = probeFaceRange(options);
7653
+ baked = probe.bake(this.gl, this.probeOrigin, clearColor, drawFace, first, end);
7654
+ /* A bake that has not reached its sixth face has nothing whole to convolve yet. */
7655
+ if (end < 6) return baked;
7240
7656
  /*
7241
7657
  * The convolution, immediately after the faces and inside the same guard, because it reads
7242
7658
  * the chain `bake` has just generated. A bake that failed leaves the layer untouched rather
@@ -7247,11 +7663,12 @@ export class WebGL2Renderer implements RendererApi {
7247
7663
  this.gl,
7248
7664
  probe,
7249
7665
  array,
7250
- layer,
7666
+ sweeps.writeLayer(layer),
7251
7667
  ENVIRONMENT_TEXTURE_UNIT,
7252
7668
  this.quality.environmentPrefilterSamples,
7253
7669
  this.quality.environmentPrefilter,
7254
7670
  );
7671
+ sweeps.baked(layer);
7255
7672
  }
7256
7673
  } finally {
7257
7674
  this.gl.frontFace(this.gl.CCW);
@@ -7264,6 +7681,7 @@ export class WebGL2Renderer implements RendererApi {
7264
7681
  * later frame reading the placeholder and quietly reflecting nothing.
7265
7682
  */
7266
7683
  this.probePassActive = false;
7684
+ this.probeBounce = false;
7267
7685
  /* Back to the canvas, or to the frame's own target if one is bound. See `SceneTarget`. */
7268
7686
  if (this.sceneTarget !== null && !this.framePresented) this.sceneTarget.bind();
7269
7687
  else {
@@ -7337,6 +7755,7 @@ export class WebGL2Renderer implements RendererApi {
7337
7755
  this.quality.environmentPrefilterSamples,
7338
7756
  this.quality.environmentPrefilter,
7339
7757
  );
7758
+ this.probeSweeps?.baked(0);
7340
7759
  return true;
7341
7760
  }
7342
7761
 
@@ -7474,7 +7893,7 @@ export class WebGL2Renderer implements RendererApi {
7474
7893
  lookup does its own `* 0.5 + 0.5` and correcting the matrix as well doubles it. */
7475
7894
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, env.lightViewProj);
7476
7895
  gl.uniform1f(u['uShadowMapSize'] ?? null, this.quality.directionalShadowMapSize);
7477
- gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peeledShadowMap === null ? 0 : 1);
7896
+ gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peelFilled ? 1 : 0);
7478
7897
 
7479
7898
  gl.activeTexture(gl.TEXTURE0);
7480
7899
  gl.bindTexture(gl.TEXTURE_2D, this.shadowMap?.texture ?? this.emptyTexture2D);