@driftengine/core 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/CHANGELOG.json +261 -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/backend/createRenderer.d.ts +13 -1
  12. package/dist/render/backend/createRenderer.js +6 -5
  13. package/dist/render/backend/webgl2/renderer.d.ts +147 -1
  14. package/dist/render/backend/webgl2/renderer.js +402 -23
  15. package/dist/render/backend/webgpu/buffers.js +46 -8
  16. package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
  17. package/dist/render/backend/webgpu/depthPass.js +75 -14
  18. package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
  19. package/dist/render/backend/webgpu/exposurePass.js +190 -0
  20. package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
  21. package/dist/render/backend/webgpu/fieldCompose.js +13 -0
  22. package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
  23. package/dist/render/backend/webgpu/flatPass.js +12 -5
  24. package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
  25. package/dist/render/backend/webgpu/particlePass.js +1 -0
  26. package/dist/render/backend/webgpu/postPass.d.ts +12 -2
  27. package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
  28. package/dist/render/backend/webgpu/probeBake.js +97 -7
  29. package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
  30. package/dist/render/backend/webgpu/probeTargets.js +180 -0
  31. package/dist/render/backend/webgpu/renderer.d.ts +174 -11
  32. package/dist/render/backend/webgpu/renderer.js +675 -249
  33. package/dist/render/backend/webgpu/scatterPass.js +7 -7
  34. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  35. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  36. package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
  37. package/dist/render/backend/webgpu/skyPass.js +5 -1
  38. package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
  39. package/dist/render/backend/webgpu/uniformRing.js +53 -3
  40. package/dist/render/bloomChain.d.ts +6 -0
  41. package/dist/render/bloomChain.js +8 -0
  42. package/dist/render/clusteredLights.d.ts +19 -11
  43. package/dist/render/clusteredLights.js +96 -45
  44. package/dist/render/cutoutCaster.d.ts +23 -0
  45. package/dist/render/cutoutCaster.js +20 -0
  46. package/dist/render/driftLight/bake.d.ts +34 -0
  47. package/dist/render/driftLight/bake.js +95 -0
  48. package/dist/render/driftLight/falloff.d.ts +16 -0
  49. package/dist/render/driftLight/falloff.js +23 -0
  50. package/dist/render/driftLight/layout.d.ts +59 -0
  51. package/dist/render/driftLight/layout.js +138 -0
  52. package/dist/render/driftLight/lightField.d.ts +108 -0
  53. package/dist/render/driftLight/lightField.js +179 -0
  54. package/dist/render/driftLight/uniforms.d.ts +28 -0
  55. package/dist/render/driftLight/uniforms.js +41 -0
  56. package/dist/render/driftLight/visibility.d.ts +18 -0
  57. package/dist/render/driftLight/visibility.js +47 -0
  58. package/dist/render/emptyTexture.d.ts +6 -0
  59. package/dist/render/emptyTexture.js +19 -0
  60. package/dist/render/exposurePass.d.ts +48 -0
  61. package/dist/render/exposurePass.js +172 -0
  62. package/dist/render/flameLight.d.ts +71 -0
  63. package/dist/render/flameLight.js +63 -0
  64. package/dist/render/gi/bounceLights.d.ts +34 -0
  65. package/dist/render/gi/bounceLights.js +66 -0
  66. package/dist/render/gi/fieldReach.d.ts +12 -0
  67. package/dist/render/gi/fieldReach.js +62 -0
  68. package/dist/render/gi/globalField.d.ts +8 -0
  69. package/dist/render/gi/globalField.js +31 -0
  70. package/dist/render/gi/probeTrace.d.ts +3 -1
  71. package/dist/render/gi/probeTrace.js +3 -1
  72. package/dist/render/gpuCapability.js +19 -4
  73. package/dist/render/lightBudget.d.ts +11 -0
  74. package/dist/render/lightBudget.js +11 -0
  75. package/dist/render/pointLightSelection.d.ts +16 -0
  76. package/dist/render/pointLightSelection.js +178 -83
  77. package/dist/render/probeGrid.d.ts +11 -1
  78. package/dist/render/probeGrid.js +5 -2
  79. package/dist/render/probeSweeps.d.ts +45 -0
  80. package/dist/render/probeSweeps.js +77 -0
  81. package/dist/render/reflectionProbe.d.ts +43 -1
  82. package/dist/render/reflectionProbe.js +17 -3
  83. package/dist/render/renderQuality.d.ts +3 -1
  84. package/dist/render/rendererHarness.d.ts +9 -0
  85. package/dist/render/rendererHarness.js +18 -3
  86. package/dist/render/sceneTarget.d.ts +20 -0
  87. package/dist/render/sceneTarget.js +62 -1
  88. package/dist/render/shaders/agx.d.ts +31 -0
  89. package/dist/render/shaders/agx.js +81 -0
  90. package/dist/render/shaders/ambientOcclusion.d.ts +24 -5
  91. package/dist/render/shaders/ambientOcclusion.js +120 -63
  92. package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
  93. package/dist/render/shaders/cutoutCoverage.js +40 -0
  94. package/dist/render/shaders/depth.d.ts +25 -0
  95. package/dist/render/shaders/depth.js +69 -3
  96. package/dist/render/shaders/exposure.d.ts +52 -0
  97. package/dist/render/shaders/exposure.js +124 -0
  98. package/dist/render/shaders/filmLook.d.ts +47 -0
  99. package/dist/render/shaders/filmLook.js +98 -0
  100. package/dist/render/shaders/flat/directionalShadow.js +35 -4
  101. package/dist/render/shaders/flat/driftLight.d.ts +29 -0
  102. package/dist/render/shaders/flat/driftLight.js +75 -0
  103. package/dist/render/shaders/flat/index.js +5 -0
  104. package/dist/render/shaders/flat/lobes.js +13 -0
  105. package/dist/render/shaders/flat/main.d.ts +1 -1
  106. package/dist/render/shaders/flat/main.js +68 -9
  107. package/dist/render/shaders/flat/preamble.js +23 -5
  108. package/dist/render/shaders/flat/probeGrid.js +46 -3
  109. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +1 -1
  110. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +1 -1
  111. package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
  112. package/dist/render/shaders/generated/depth.wgsl.js +100 -0
  113. package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
  114. package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
  115. package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
  116. package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
  117. package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
  118. package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
  119. package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
  120. package/dist/render/shaders/generated/rush.wgsl.js +40 -10
  121. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  122. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  123. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
  124. package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
  125. package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
  126. package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
  127. package/dist/render/shaders/globalMedium.js +4 -0
  128. package/dist/render/shaders/localExposure.d.ts +35 -0
  129. package/dist/render/shaders/localExposure.js +138 -0
  130. package/dist/render/shaders/prefilter.d.ts +1 -1
  131. package/dist/render/shaders/prefilter.js +6 -0
  132. package/dist/render/shaders/rush.js +33 -5
  133. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  134. package/dist/render/shaders/tangentFrame.js +15 -3
  135. package/dist/render/shaders/temporalResolve.js +39 -1
  136. package/dist/render/shadowCasters.d.ts +5 -3
  137. package/dist/render/shadowMap.js +13 -0
  138. package/dist/render/surfaceTexture.d.ts +5 -0
  139. package/package.json +6 -6
  140. package/scripts/gpuCompute.mjs +24 -3
  141. package/scripts/png.mjs +23 -12
  142. package/src/cinematic/cameraPath.ts +178 -0
  143. package/src/environment/daylight.ts +152 -0
  144. package/src/geometry/meshConcat.ts +100 -0
  145. package/src/index.ts +32 -0
  146. package/src/render/backend/createRenderer.ts +22 -6
  147. package/src/render/backend/webgl2/renderer.ts +439 -20
  148. package/src/render/backend/webgpu/buffers.ts +51 -8
  149. package/src/render/backend/webgpu/depthPass.ts +78 -10
  150. package/src/render/backend/webgpu/exposurePass.ts +234 -0
  151. package/src/render/backend/webgpu/fieldCompose.ts +15 -0
  152. package/src/render/backend/webgpu/flatPass.ts +10 -2
  153. package/src/render/backend/webgpu/particlePass.ts +9 -0
  154. package/src/render/backend/webgpu/probeBake.ts +143 -9
  155. package/src/render/backend/webgpu/probeTargets.ts +211 -0
  156. package/src/render/backend/webgpu/renderer.ts +801 -270
  157. package/src/render/backend/webgpu/scatterPass.ts +7 -7
  158. package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
  159. package/src/render/backend/webgpu/skyPass.ts +5 -1
  160. package/src/render/backend/webgpu/uniformRing.ts +49 -3
  161. package/src/render/bloomChain.ts +9 -0
  162. package/src/render/clusteredLights.ts +108 -63
  163. package/src/render/cutoutCaster.ts +44 -0
  164. package/src/render/driftLight/bake.ts +125 -0
  165. package/src/render/driftLight/falloff.ts +26 -0
  166. package/src/render/driftLight/layout.ts +177 -0
  167. package/src/render/driftLight/lightField.ts +227 -0
  168. package/src/render/driftLight/uniforms.ts +61 -0
  169. package/src/render/driftLight/visibility.ts +58 -0
  170. package/src/render/emptyTexture.ts +41 -0
  171. package/src/render/exposurePass.ts +186 -0
  172. package/src/render/flameLight.ts +135 -0
  173. package/src/render/gi/bounceLights.ts +68 -0
  174. package/src/render/gi/fieldReach.ts +74 -0
  175. package/src/render/gi/globalField.ts +39 -0
  176. package/src/render/gi/probeTrace.ts +3 -1
  177. package/src/render/gpuCapability.ts +18 -5
  178. package/src/render/lightBudget.ts +12 -0
  179. package/src/render/pointLightSelection.ts +191 -79
  180. package/src/render/probeGrid.ts +13 -2
  181. package/src/render/probeSweeps.ts +80 -0
  182. package/src/render/reflectionProbe.ts +54 -2
  183. package/src/render/renderQuality.ts +3 -1
  184. package/src/render/rendererHarness.ts +25 -3
  185. package/src/render/sceneTarget.ts +75 -1
  186. package/src/render/shaders/ambientOcclusion.ts +125 -63
  187. package/src/render/shaders/cutoutCoverage.ts +43 -0
  188. package/src/render/shaders/depth.ts +80 -3
  189. package/src/render/shaders/exposure.ts +128 -0
  190. package/src/render/shaders/filmLook.ts +111 -0
  191. package/src/render/shaders/flat/directionalShadow.ts +35 -4
  192. package/src/render/shaders/flat/driftLight.ts +75 -0
  193. package/src/render/shaders/flat/index.ts +5 -0
  194. package/src/render/shaders/flat/lobes.ts +13 -0
  195. package/src/render/shaders/flat/main.ts +68 -9
  196. package/src/render/shaders/flat/preamble.ts +23 -5
  197. package/src/render/shaders/flat/probeGrid.ts +46 -3
  198. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +1 -1
  199. package/src/render/shaders/generated/depth.wgsl.ts +103 -0
  200. package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
  201. package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
  202. package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
  203. package/src/render/shaders/generated/rush.wgsl.ts +40 -10
  204. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  205. package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
  206. package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
  207. package/src/render/shaders/globalMedium.ts +4 -0
  208. package/src/render/shaders/localExposure.ts +154 -0
  209. package/src/render/shaders/prefilter.ts +6 -0
  210. package/src/render/shaders/rush.ts +33 -5
  211. package/src/render/shaders/tangentFrame.ts +15 -3
  212. package/src/render/shaders/temporalResolve.ts +39 -1
  213. package/src/render/shadowCasters.ts +5 -3
  214. package/src/render/shadowMap.ts +14 -0
  215. package/src/render/surfaceTexture.ts +5 -0
@@ -1,4 +1,5 @@
1
1
  import { mat4 } from 'gl-matrix';
2
+ import { probeFaceRange } from '../../reflectionProbe.js';
2
3
  import { TEMPORAL_HISTORY_BLEND, TemporalHistory, jitterOffset, jitterProjection, } from '../../temporalAa.js';
3
4
  import { OIT_MULTISAMPLE_REFUSAL } from '../../orderIndependent.js';
4
5
  import { TranslucentQueue } from '../../translucentQueue.js';
@@ -15,7 +16,7 @@ import { PickableSet } from '../../pickable.js';
15
16
  import { GpuTimestamps } from './gpuTimestamps.js';
16
17
  import { isWeakGpuFamily } from '../../gpuCapability.js';
17
18
  import { resolveAtmosphere } from '../../atmosphere.js';
18
- import { BLOOM_FILTER_RADIUS_UV, BLOOM_LEVELS, bloomLevelSizes, bloomProfileWarning, } from '../../bloomChain.js';
19
+ import { BLOOM_FILTER_RADIUS_UV, BLOOM_LEVELS, bloomLevelSizes, bloomThresholdOf, bloomProfileWarning, } from '../../bloomChain.js';
19
20
  import { createResolvedWater, resolveWater, waterAppearance, } from '../../waterDraw.js';
20
21
  import { createResolvedLightVolume, DEPTH_01_TO_CLIP_Y_DOWN, resolveLightVolume, volumeMediumGain, } from '../../lightVolumeDraw.js';
21
22
  import { WIND_STREAK_DEFAULTS, createResolvedWindStreaks, resolveWindStreaks, } from '../../windStreakDraw.js';
@@ -24,6 +25,7 @@ import { LIVE_POINT_SHADOW_MAPS, MAX_POINT_LIGHTS, POINT_SHADOW_POOL } from '../
24
25
  import { roughnessForLevel } from '../../prefilterEnvMap.js';
25
26
  import { equirectToCubeFaces } from '../../equirectToCube.js';
26
27
  import { MAX_ENV_PROBES, ProbeGrid, SINGLE_PROBE, UNIT_STEP, WORLD_ORIGIN, sameGrid, } from '../../probeGrid.js';
28
+ import { ProbeSweeps } from '../../probeSweeps.js';
27
29
  import { ggxMaxLevelFor, irradianceLevelFor, octahedralEdgeFor } from '../../prefilterEnvMap.js';
28
30
  import { environmentTexels } from './environmentTexels.js';
29
31
  import { packIesAtlas } from '../../iesProfile.js';
@@ -45,9 +47,13 @@ import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG
45
47
  import { reflectionTargetSize } from '../../planarReflectionDraw.js';
46
48
  import { drawingBufferSize } from '../../drawingBuffer.js';
47
49
  import { MOTION_BLUR_MAX_UV, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV } from '../../vertexDefaults.js';
50
+ import { ExposurePass } from './exposurePass.js';
51
+ import { clampAutoExposure } from '../../shaders/exposure.js';
52
+ import { clampLocalExposure } from '../../shaders/localExposure.js';
48
53
  import { createDepthResolveLayout, createPostStageLayout, depthResolvePipeline, postPipeline, sceneColorFormat, AO_BLUR_FIELDS, AO_BLUR_FRAG_WGSL, AO_BLUR_SIZE, AO_BLUR_TEXTURES, AO_BLUR_UNIFORMS, AO_FRAG_FIELDS, AO_FRAG_SIZE, AO_FRAG_WGSL, AO_TEXTURES, AO_UNIFORMS, ADDITIVE_BLEND, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, BLOOM_UPSAMPLE_FRAG_WGSL, RESOLVED_DEPTH_FORMAT, RUSH_FRAG_FIELDS, RUSH_FRAG_SIZE, RUSH_FRAG_WGSL, RUSH_TEXTURES, RUSH_UNIFORMS, TAA_FRAG_FIELDS, TAA_FRAG_SIZE, TAA_TEXTURES, TAA_UNIFORMS, TEMPORAL_RESOLVE_FRAG_WGSL, MEDIUM_FRAG_FIELDS, MEDIUM_FRAG_SIZE, MEDIUM_FRAG_WGSL, MEDIUM_TEXTURES, MEDIUM_TRANSMIT_BLEND, MEDIUM_UNIFORMS, MEDIUM_UPSAMPLE_FIELDS, MEDIUM_UPSAMPLE_FRAG_WGSL, MEDIUM_UPSAMPLE_SIZE, MEDIUM_UPSAMPLE_TEXTURES, MEDIUM_UPSAMPLE_UNIFORMS, OIT_COMPOSITE_BLEND, OIT_RESOLVE_FRAG_WGSL, OIT_TEXTURES, DECAL_FRAG_FIELDS, DECAL_FRAG_SIZE, DECAL_MULTIPLY_BLEND, DECAL_PROJECT_FRAG_WGSL, DECAL_TEXTURES, DECAL_UNIFORMS, PREMULTIPLIED_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
49
54
  import { GRADE_PLACEHOLDER_SIZE, identityGradeLut, validateGradeLut, } from '../../colourGrade.js';
50
- import { createProbeMipBindGroupLayout, createProbePrefilterBindGroupLayout, PREFILTER_SAMPLER_BINDING, PREFILTER_TEXTURE_BINDING, PREFILTER_UNIFORM_BINDING, PREFILTER_UNIFORM_OFFSETS, probeLevels, probeMipPipeline, probePrefilterPipeline, PROBE_FACES, PROBE_FACE_BASIS, PROBE_FAR_M, PROBE_FOV_DEG, PROBE_MIP_UNIFORM_SIZE, PROBE_NEAR_M, } from './probePass.js';
55
+ import { FILM_LOOK_WITHOUT_COMPOSITE, clampGrain, clampVignette, grainSeed, } from '../../shaders/filmLook.js';
56
+ import { createProbeMipBindGroupLayout, createProbePrefilterBindGroupLayout, PREFILTER_UNIFORM_OFFSETS, probeLevels, probeMipPipeline, probePrefilterPipeline, PROBE_FACES, PROBE_FACE_BASIS, PROBE_FAR_M, PROBE_FOV_DEG, PROBE_MIP_UNIFORM_SIZE, PROBE_NEAR_M, } from './probePass.js';
51
57
  import { CAUSTICS_CELL_M, CAUSTICS_MAX_DROP_M } from '../../causticsRenderer.js';
52
58
  import { buildSheets } from '../../surfaceSheet.js';
53
59
  import { seaStateForWind } from '../../seaState.js';
@@ -62,13 +68,13 @@ import { DEFAULT_GLOBAL_MEDIUM, mediumActive, mediumTargetSize, WEAK_GPU_MEDIUM_
62
68
  import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental } from './buffers.js';
63
69
  import { SkinPaletteRing } from './skinPaletteRing.js';
64
70
  import { DEPTH_FORMAT, FLAT_VERT_FIELDS, FLAT_VERT_SIZE, createFlatBindGroup, createFlatBindGroupLayout, flatFragmentBindings, flatVariant, flatVertexBindings, flatPipeline, flatPipelineAsync, } from './flatPass.js';
65
- import { DEPTH_FRAG_BINDING, DEPTH_FRAG_SIZE, DEPTH_FRAG_WGSL, DEPTH_PREVIOUS_BINDING, DEPTH_VERT_FIELDS, DEPTH_VERT_SIZE, SHADOW_FORMAT, createDepthBindGroup, createDepthBindGroupLayout, depthPipeline, } from './depthPass.js';
71
+ import { DEPTH_FRAG_BINDING, DEPTH_FRAG_SIZE, DEPTH_FRAG_WGSL, DEPTH_PREVIOUS_BINDING, DEPTH_CUTOUT_VERT_FIELDS, DEPTH_CUTOUT_VERT_SIZE, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS, DEPTH_VERT_FIELDS, DEPTH_VERT_SIZE, SHADOW_FORMAT, createDepthBindGroup, createDepthBindGroupLayout, depthPipeline, } from './depthPass.js';
66
72
  import { PipelineCache } from './pipelineCache.js';
67
73
  import { SKY_FIELDS, SKY_UNIFORM_SIZE, createSkyBindGroup, createSkyBindGroupLayout, skyPipeline, } from './skyPass.js';
68
74
  import { SCATTER_DEPTH_FIELDS, SCATTER_DEPTH_SIZE, SCATTER_FRAG_FIELDS, SCATTER_FRAG_SIZE, SCATTER_VERT_FIELDS, SCATTER_VERT_SIZE, createGpuScatter, createScatterBindGroup, createScatterBindGroupLayout, createScatterDepthBindGroup, createScatterDepthBindGroupLayout, scatterDepthPipeline, scatterPipeline, } from './scatterPass.js';
69
75
  import { WATER_FRAG_FIELDS, WATER_FRAG_SIZE, WATER_VERT_FIELDS, WATER_VERT_SIZE, createGpuWater, createWaterBindGroup, createWaterBindGroupLayout, waterPipeline, } from './waterPass.js';
70
76
  import { PLUME_FRAG_FIELDS, PLUME_FRAG_SIZE, PLUME_VERT_FIELDS, PLUME_VERT_SIZE, createGpuPlumes, createPlumeBindGroup, createPlumeBindGroupLayout, plumePipeline, } from './plumePass.js';
71
- import { PARTICLE_VERT_FIELDS, PARTICLE_VERT_SIZE, createGpuParticles, createParticleBindGroupLayout, packInstances, particleFragBindings, particlePipeline, } from './particlePass.js';
77
+ import { PARTICLE_VERT_FIELDS, createGpuParticles, createParticleBindGroupLayout, packInstances, particleFragBindings, particlePipeline, } from './particlePass.js';
72
78
  import { WIND_STREAK_FRAG_FIELDS, WIND_STREAK_FRAG_SIZE, WIND_STREAK_VERT_FIELDS, WIND_STREAK_VERT_SIZE, createGpuWindStreaks, createWindStreakBindGroup, createWindStreakBindGroupLayout, windStreakPipeline, } from './windStreakPass.js';
73
79
  import { FLOCK_FRAG_FIELDS, FLOCK_FRAG_SIZE, FLOCK_VERT_FIELDS, FLOCK_VERT_SIZE, createFlockBindGroup, createFlockBindGroupLayout, createGpuFlock, flockPipeline, } from './flockPass.js';
74
80
  import { BOLT_FRAG_FIELDS, BOLT_FRAG_SIZE, BOLT_VERT_FIELDS, BOLT_VERT_SIZE, boltPipeline, createBoltBindGroup, createBoltBindGroupLayout, createGpuBolts, } from './boltPass.js';
@@ -80,8 +86,11 @@ import { MaterialChanges, ownsMaterial } from '../materialChanges.js';
80
86
  import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.js';
81
87
  import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.js';
82
88
  import { DistanceFieldScene } from '../../gi/fieldScene.js';
83
- import { DEFAULT_FIELD_COMPOSE, FieldComposer } from './fieldCompose.js';
89
+ import { DEFAULT_FIELD_COMPOSE, FieldComposer, MAX_FIELD_CASCADES } from './fieldCompose.js';
90
+ import { cascadesForGrid } from '../../gi/fieldReach.js';
84
91
  import { ProbeBaker } from './probeBake.js';
92
+ import { createBounceLights, resolveBounceLights } from '../../gi/bounceLights.js';
93
+ import { ProbeTargets } from './probeTargets.js';
85
94
  import { distanceFieldBounds } from '../../gi/fieldScene.js';
86
95
  import { fitProbeGrid } from '../../gi/probeGridFit.js';
87
96
  /**
@@ -107,6 +116,9 @@ import { ClusterBinner } from './clusterBinner.js';
107
116
  import { MAX_CLUSTERED_LIGHTS } from '../../clusteredLights.js';
108
117
  import { computeAt, createComputeRegistry, } from '../../compute.js';
109
118
  import { GpuInstancedBatch } from './instanced.js';
119
+ import { cutoutOf } from '../../cutoutCaster.js';
120
+ import { LightField } from '../../driftLight/lightField.js';
121
+ import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.js';
110
122
  /**
111
123
  * How many draws one frame may make before the ring is full.
112
124
  *
@@ -157,6 +169,12 @@ const MAX_SCATTER_DEPTH_DRAWS = 256;
157
169
  * against the world.
158
170
  */
159
171
  const MAX_OVERLAYS = 64;
172
+ /**
173
+ * Sky draws a frame can hold. A frame draws one, a mirror adds one, and a probe bake six a probe —
174
+ * given back after each probe's submit, so a whole grid needs six. Sixty-four is room for insets
175
+ * and passes nobody has written yet; the cost is sixteen kilobytes.
176
+ */
177
+ const MAX_SKY_DRAWS = 64;
160
178
  /**
161
179
  * Distinct materials a frame may set.
162
180
  *
@@ -678,8 +696,6 @@ const AO_BLUR_SLOT = DYNAMIC_UNIFORM_ALIGNMENT;
678
696
  const BLOOM_UPSAMPLE_SLOT = BLOOM_LEVELS;
679
697
  /** The tint a draw gets when it asks for none, matching the WebGL2 path's reset-to-white. */
680
698
  const WHITE = new Float32Array([1, 1, 1]);
681
- /** No surface-texture scaling, which is what an untextured mesh wants. */
682
- const UNIT_UV = new Float32Array([1, 1]);
683
699
  /** A dust field standing still, for a light volume that asked for dust and not for wind. */
684
700
  const NO_DRIFT = new Float32Array([0, 0, 0]);
685
701
  /**
@@ -728,6 +744,23 @@ const NO_CLIP_PLANE = new Float32Array(4);
728
744
  * matrix the scene drew with and hands over the product.
729
745
  */
730
746
  const MOTION_CLIP_FLIP = mat4.fromValues(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
747
+ /* Scratch for the probe blur's and convolution's uniform writes, so a bake writes no array literal. */
748
+ const SCRATCH_ONE = new Float32Array(1);
749
+ const SCRATCH_VEC4 = new Float32Array(4);
750
+ const ZERO4 = new Float32Array(4);
751
+ /** `value` in a one-float scratch, overwritten by the next call. */
752
+ function one(value) {
753
+ SCRATCH_ONE[0] = value;
754
+ return SCRATCH_ONE;
755
+ }
756
+ /** A direction padded to a `vec4`, in a scratch overwritten by the next call. */
757
+ function padded(direction) {
758
+ SCRATCH_VEC4[0] = direction[0] ?? 0;
759
+ SCRATCH_VEC4[1] = direction[1] ?? 0;
760
+ SCRATCH_VEC4[2] = direction[2] ?? 0;
761
+ SCRATCH_VEC4[3] = 0;
762
+ return SCRATCH_VEC4;
763
+ }
731
764
  export class WebGPURenderer {
732
765
  /**
733
766
  * Real timestamp queries, where the adapter offers them.
@@ -1508,12 +1541,20 @@ export class WebGPURenderer {
1508
1541
  frameEye = new Float32Array(3);
1509
1542
  /** The environment the frame was drawn with. See `bindMeshPass`. */
1510
1543
  frameEnv = null;
1544
+ /**
1545
+ * The sky this frame drew, or null until it draws one. A traced probe's escaping rays read it,
1546
+ * so a frame that stops drawing a sky must not go on lighting its probes with the last one.
1547
+ */
1548
+ frameSky = null;
1511
1549
  /** That matrix with the clip-space correction folded in, rebuilt once a frame. */
1512
1550
  correctedViewProj = new Float32Array(16);
1513
1551
  skyLayout;
1552
+ /**
1553
+ * A slot a sky draw, as every per-draw resource here is: see `UniformRing` and the 2026-08-27 rule.
1554
+ * It was one buffer rewritten at each draw, which gave every sky in an encoder the last camera.
1555
+ */
1514
1556
  skyUniforms;
1515
- skyStaging = new ArrayBuffer(SKY_UNIFORM_SIZE);
1516
- skyFloats = new Float32Array(this.skyStaging);
1557
+ skyFloats = new Float32Array(SKY_UNIFORM_SIZE / 4);
1517
1558
  skyBindGroup;
1518
1559
  /** The camera's inverse with the clip correction undone, rebuilt once a frame. */
1519
1560
  skyInvViewProj = new Float32Array(16);
@@ -1774,35 +1815,75 @@ export class WebGPURenderer {
1774
1815
  * prefix that has already been uploaded costs one write of unchanged bytes and cannot be
1775
1816
  * wrong: a ring only ever appends within a frame.
1776
1817
  */
1818
+ /**
1819
+ * Every per-draw ring the frame writes and flushes together, in one list.
1820
+ *
1821
+ * **One list, read by the flush and by the probe bake's rewind**, so a ring added to one cannot be
1822
+ * forgotten by the other: a ring flushed but never rewound would let a grid bake spend it, and one
1823
+ * rewound but never flushed would upload nothing. Built on first use, after the constructor has
1824
+ * set every field; `growTo` replaces a ring's buffer, never the ring, so the list stays true.
1825
+ */
1826
+ ringList = null;
1827
+ rings() {
1828
+ this.ringList ??= [
1829
+ this.perDraw,
1830
+ this.lightVolumeVertices,
1831
+ this.lightVolumeFragments,
1832
+ this.plumeVerts,
1833
+ this.plumeFrags,
1834
+ this.waterVerts,
1835
+ this.waterFrags,
1836
+ this.boltVerts,
1837
+ this.boltFrags,
1838
+ this.causticsVerts,
1839
+ this.causticsFrags,
1840
+ this.flockVerts,
1841
+ this.flockFrags,
1842
+ this.windStreakVerts,
1843
+ this.windStreakFrags,
1844
+ this.scatterVerts,
1845
+ this.scatterFrags,
1846
+ this.perFrame,
1847
+ this.filmVerts,
1848
+ this.filmFrags,
1849
+ this.insetUniforms,
1850
+ this.panelVerts,
1851
+ this.panelFrags,
1852
+ this.textVerts,
1853
+ this.textFrags,
1854
+ this.sdfTextVerts,
1855
+ this.sdfTextFrags,
1856
+ this.lineVerts,
1857
+ this.lineFrags,
1858
+ this.skyUniforms,
1859
+ ];
1860
+ return this.ringList;
1861
+ }
1777
1862
  flushRings() {
1778
- this.perDraw.flush();
1779
- this.lightVolumeVertices.flush();
1780
- this.lightVolumeFragments.flush();
1781
- this.plumeVerts.flush();
1782
- this.plumeFrags.flush();
1783
- this.waterVerts.flush();
1784
- this.waterFrags.flush();
1785
- this.boltVerts.flush();
1786
- this.boltFrags.flush();
1787
- this.causticsVerts.flush();
1788
- this.causticsFrags.flush();
1789
- this.flockVerts.flush();
1790
- this.flockFrags.flush();
1791
- this.windStreakVerts.flush();
1792
- this.windStreakFrags.flush();
1793
- this.scatterVerts.flush();
1794
- this.perFrame.flush();
1795
- this.filmVerts.flush();
1796
- this.filmFrags.flush();
1797
- this.insetUniforms.flush();
1798
- this.panelVerts.flush();
1799
- this.panelFrags.flush();
1800
- this.textVerts.flush();
1801
- this.textFrags.flush();
1802
- this.sdfTextVerts.flush();
1803
- this.sdfTextFrags.flush();
1804
- this.lineVerts.flush();
1805
- this.lineFrags.flush();
1863
+ for (const ring of this.rings())
1864
+ ring.flush();
1865
+ }
1866
+ /** Where every ring stands before work that submits an encoder of its own. */
1867
+ ringMarks = [];
1868
+ markRings() {
1869
+ const rings = this.rings();
1870
+ this.ringMarks.length = rings.length;
1871
+ for (let i = 0; i < rings.length; i++)
1872
+ this.ringMarks[i] = rings[i].mark();
1873
+ }
1874
+ /**
1875
+ * Give back every slot taken since `markRings`, once that work has been submitted.
1876
+ *
1877
+ * See `UniformRing.rewind` for why this is safe only after the submit. The palette ring goes back
1878
+ * with them, and the cached palette slot is dropped if it was one of those given back.
1879
+ */
1880
+ rewindRings(skinMark) {
1881
+ const rings = this.rings();
1882
+ for (let i = 0; i < rings.length; i++)
1883
+ rings[i].rewind(this.ringMarks[i] ?? 0);
1884
+ this.skinPalettes.rewind(skinMark);
1885
+ if (this.skinPaletteSlot >= skinMark)
1886
+ this.skinPaletteSlot = -1;
1806
1887
  }
1807
1888
  /* -- The film ------------------------------------------------------------------------- */
1808
1889
  filmLayout;
@@ -1970,6 +2051,8 @@ export class WebGPURenderer {
1970
2051
  blankAlbedoBindGroup;
1971
2052
  /** Repeats across the mesh's own UV range. Per draw, because `uUvScale` is a vertex uniform. */
1972
2053
  uvScale = new Float32Array([1, 1]);
2054
+ /** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
2055
+ materialDoubleSided = false;
1973
2056
  createSurfaceTexture(source, options = {}) {
1974
2057
  return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
1975
2058
  }
@@ -2032,6 +2115,8 @@ export class WebGPURenderer {
2032
2115
  * a group holding the old view hands a destroyed texture to the next draw that asks.
2033
2116
  */
2034
2117
  forgetBindingsOf(texture) {
2118
+ this.cutoutGroups.delete(texture);
2119
+ this.cutoutPeelGroups.delete(texture);
2035
2120
  /* Same reasoning, for whichever SDF labels were bound against this atlas. */
2036
2121
  this.sdfTextBindGroups.delete(texture);
2037
2122
  this.flatBindGroups.clear();
@@ -2088,12 +2173,14 @@ export class WebGPURenderer {
2088
2173
  const albedo = material?.albedo ?? null;
2089
2174
  this.uvScale[0] = material?.uScale ?? 1;
2090
2175
  this.uvScale[1] = material?.vScale ?? 1;
2176
+ this.materialDoubleSided = material?.doubleSided === true;
2091
2177
  const normal = (material?.normal ?? null);
2092
2178
  const orm = (material?.orm ?? null);
2093
2179
  const emissiveMap = (material?.emissive ?? null);
2094
2180
  this.material((f, i) => {
2095
2181
  i[this.materialField('uAlbedoEnabled')] = albedo === null ? 0 : 1;
2096
2182
  f[this.materialField('uAlbedoCutout')] = material?.cutout ?? 0;
2183
+ i[this.materialField('uDoubleSided')] = material?.doubleSided === true ? 1 : 0;
2097
2184
  /* 0 is no map, and gates the whole block in the shader. See `uNormalStrength`. */
2098
2185
  f[this.materialField('uNormalStrength')] =
2099
2186
  normal === null ? 0 : (material?.normalStrength ?? 1);
@@ -2862,6 +2949,16 @@ export class WebGPURenderer {
2862
2949
  drawParticles(batch, data, camera, env, timeSeconds) {
2863
2950
  if (!this.canDraw() || data.count === 0)
2864
2951
  return;
2952
+ if (batch.drawnInEncoder === this.encoderEpoch) {
2953
+ if (!this.warnedParticlesTwice) {
2954
+ this.warnedParticlesTwice = true;
2955
+ console.warn('WebGPU: a particle batch was drawn twice into one encoder — a mirror and the view, or a ' +
2956
+ "probe's faces — and the second draw is skipped, because both would show the second " +
2957
+ "draw's particles. Give each view its own batch.");
2958
+ }
2959
+ return;
2960
+ }
2961
+ batch.drawnInEncoder = this.encoderEpoch;
2865
2962
  const live = packInstances(batch, data);
2866
2963
  if (live === 0)
2867
2964
  return;
@@ -3488,7 +3585,6 @@ export class WebGPURenderer {
3488
3585
  v[at('uCellSize')] = streaks.cellSize;
3489
3586
  v[at('uTime')] = timeSeconds;
3490
3587
  this.windStreakFragFloats.set(tint, (WIND_STREAK_FRAG_FIELDS.uTint.offset ?? 0) / 4);
3491
- const { queue } = this.surface.device;
3492
3588
  this.windStreakVerts.writeBlock(vertexSlot, this.windStreakVertBlock);
3493
3589
  this.windStreakFrags.writeBlock(fragmentSlot, this.windStreakFragBlock);
3494
3590
  const windPipe = windStreakPipeline(this.pipelines, this.surface.device, this.windStreakLayout);
@@ -3574,7 +3670,6 @@ export class WebGPURenderer {
3574
3670
  v[at('uWind')] = windX;
3575
3671
  v[at('uWind') + 1] = windZ;
3576
3672
  this.flockFragFloats.set(tint, (FLOCK_FRAG_FIELDS.uTint.offset ?? 0) / 4);
3577
- const { queue } = this.surface.device;
3578
3673
  this.flockVerts.writeBlock(vertexSlot, this.flockVertBlock);
3579
3674
  this.flockFrags.writeBlock(fragmentSlot, this.flockFragBlock);
3580
3675
  const flockPipe = flockPipeline(this.pipelines, this.surface.device, this.flockLayout);
@@ -3868,7 +3963,8 @@ export class WebGPURenderer {
3868
3963
  scatterLayout;
3869
3964
  /** One slot per batch: the gust and the trample field belong to the batch, not the frame. */
3870
3965
  scatterVerts;
3871
- scatterFragUniforms;
3966
+ /** A slot a draw, beside the vertex block's: see `createScatterBindGroupLayout` for why. */
3967
+ scatterFrags;
3872
3968
  scatterFragStaging = new ArrayBuffer(SCATTER_FRAG_SIZE);
3873
3969
  scatterFragFloats = new Float32Array(this.scatterFragStaging);
3874
3970
  scatterFragInts = new Int32Array(this.scatterFragStaging);
@@ -4053,17 +4149,28 @@ export class WebGPURenderer {
4053
4149
  dynamicView;
4054
4150
  /** The peel's own bind groups: `uPeelShadowLayer` at 1, and the static map to peel against. */
4055
4151
  shadowPeelBindGroup;
4152
+ /**
4153
+ * The cutout casters' layout, and a bind group per cutout map for each layer, built the first
4154
+ * time a map casts and kept: a `createBindGroup` per draw is the allocation the house rules forbid.
4155
+ * Cleared in `forgetBindingsOf`, because a group outliving its texture is a destroyed-texture error
4156
+ * at submit, and a texture's view is new when an update changes its size.
4157
+ */
4158
+ shadowCutoutLayout;
4159
+ cutoutGroups = new Map();
4160
+ cutoutPeelGroups = new Map();
4161
+ shadowPeelPassUniforms = null;
4162
+ shadowPeelSampler = null;
4056
4163
  scatterDepthPeelBindGroup;
4057
4164
  /** Which layer the open pass is writing, so the sink picks the bindings that match it. */
4058
4165
  shadowLayerIsPeel = false;
4059
4166
  /**
4060
4167
  * Whether a peel pass has filled the map, which is what the frame may sample it on.
4061
4168
  *
4062
- * **Not whether the map exists.** `renderer.ts` can ask that question because a GL texture is
4063
- * born cleared; a WebGPU texture no pass has written holds undefined contents, so a scene
4064
- * that builds a peel map and never opens the layer would sample garbage. Cleared when the
4065
- * static layer opens and set when a peel pass ends, which is per frame because both demo
4066
- * scenes that peel do so every frame.
4169
+ * **Not whether the map exists.** `renderer.ts` asked that question, on the belief that a GL
4170
+ * texture is born reading as the far plane, and it is born zeroed: a scene that built a peel
4171
+ * map and never opened the layer had no sun at all under ANGLE's GL backend. It holds this
4172
+ * rule now as well. Cleared when the static layer opens and set when a peel pass ends, which
4173
+ * is per frame because every scene that peels does so every frame.
4067
4174
  */
4068
4175
  peelFilled = false;
4069
4176
  /** One slot per caster, the same trick `perDraw` uses and for the same reason. */
@@ -4104,6 +4211,28 @@ export class WebGPURenderer {
4104
4211
  * otherwise. A lookup rather than a rebind, because a `createBindGroup` per draw is the
4105
4212
  * per-frame allocation the house rules forbid; both groups are built once at init.
4106
4213
  */
4214
+ /** The cutout bind group for `texture` in the open layer, built the first time it casts there. */
4215
+ cutoutGroup(texture) {
4216
+ const peel = this.shadowLayerIsPeel && this.shadowPeelPassUniforms !== null;
4217
+ const groups = peel ? this.cutoutPeelGroups : this.cutoutGroups;
4218
+ const held = groups.get(texture);
4219
+ if (held !== undefined)
4220
+ return held;
4221
+ const passUniforms = peel ? this.shadowPeelPassUniforms : this.shadowPassUniforms;
4222
+ const previousView = peel ? this.shadowView : this.shadowBlankView;
4223
+ const previousSampler = peel ? this.shadowPeelSampler : this.shadowBlankSampler;
4224
+ if (passUniforms === null || previousView === null || previousSampler === null)
4225
+ return null;
4226
+ const group = createDepthBindGroup(this.surface.device, this.shadowCutoutLayout, this.shadowDraws.buffer, passUniforms, previousView, previousSampler, null, { view: texture.view, sampler: texture.sampler });
4227
+ groups.set(texture, group);
4228
+ return group;
4229
+ }
4230
+ /** The cutoff and UV scale into a cutout draw's slot, at the offsets its own variant declares. */
4231
+ writeCutout(slot, cutout, fields) {
4232
+ this.shadowDraws.writeFloat(slot, fields.uUvScale.offset, cutout.u);
4233
+ this.shadowDraws.writeFloat(slot, fields.uUvScale.offset + 4, cutout.v);
4234
+ this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset, cutout.cutoff);
4235
+ }
4107
4236
  shadowGroup() {
4108
4237
  return this.shadowLayerIsPeel && this.shadowPeelBindGroup !== null
4109
4238
  ? this.shadowPeelBindGroup
@@ -4139,21 +4268,29 @@ export class WebGPURenderer {
4139
4268
  * every frame would be an allocation in a per-frame path.
4140
4269
  */
4141
4270
  casterSink = {
4142
- mesh: (mesh, model) => {
4271
+ mesh: (mesh, model, material) => {
4143
4272
  const pass = this.shadowPass;
4144
4273
  if (pass === null)
4145
4274
  return;
4275
+ /* A cutout caster's group is looked up before a slot is spent, so a miss costs nothing. */
4276
+ const cutout = cutoutOf(material);
4277
+ const cutoutGroup = cutout === null ? null : this.cutoutGroup(cutout.albedo);
4146
4278
  const slot = this.shadowDraws.allocate();
4147
4279
  if (slot === null)
4148
4280
  return;
4149
4281
  this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uLightViewProj.offset, this.correctedLightViewProj);
4150
4282
  this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uModel.offset, model);
4151
4283
  this.writeShadowWind(slot);
4284
+ if (cutout !== null && cutoutGroup !== null)
4285
+ this.writeCutout(slot, cutout, DEPTH_CUTOUT_VERT_FIELDS);
4152
4286
  const geometry = mesh;
4153
4287
  if (geometry.vertexBuffers === undefined)
4154
4288
  return;
4155
- pass.setPipeline(depthPipeline(this.pipelines, this.surface.device, this.shadowLayout, `depth|${this.depthCullMode}|${geometry.key ?? ''}`, this.presentOf(geometry), this.depthCullMode));
4156
- pass.setBindGroup(0, this.shadowGroup(), [slot]);
4289
+ const cuts = cutoutGroup !== null;
4290
+ /* A two-sided caster casts from whichever face the light sees: nothing culled. */
4291
+ const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
4292
+ pass.setPipeline(depthPipeline(this.pipelines, this.surface.device, cuts ? this.shadowCutoutLayout : this.shadowLayout, `${cuts ? 'depth-cutout' : 'depth'}|${cull}|${geometry.key ?? ''}`, this.presentOf(geometry), cull, false, false, cuts));
4293
+ pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
4157
4294
  for (let index = 0; index < geometry.vertexBuffers.length; index++) {
4158
4295
  pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
4159
4296
  }
@@ -4164,7 +4301,7 @@ export class WebGPURenderer {
4164
4301
  * A batch of rigid meshes into the depth map, placed by the same matrices the visible draw
4165
4302
  * uses — one draw, however many instances, exactly as the colour pass does it.
4166
4303
  */
4167
- instanced: (batch, data) => {
4304
+ instanced: (batch, data, material) => {
4168
4305
  const pass = this.shadowPass;
4169
4306
  if (pass === null)
4170
4307
  return;
@@ -4175,14 +4312,21 @@ export class WebGPURenderer {
4175
4312
  const count = Math.min(data.count, gpuBatch.capacity);
4176
4313
  if (count === 0)
4177
4314
  return;
4315
+ const cutout = cutoutOf(material);
4316
+ const cutoutGroup = cutout === null ? null : this.cutoutGroup(cutout.albedo);
4178
4317
  const slot = this.shadowDraws.allocate();
4179
4318
  if (slot === null)
4180
4319
  return;
4181
4320
  /* Only the light matrix: the instanced depth variant has no uModel, its placement being
4182
4321
  the four attribute columns the batch's buffer supplies. */
4183
4322
  this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uLightViewProj.offset, this.correctedLightViewProj);
4184
- pass.setPipeline(depthPipeline(this.pipelines, this.surface.device, this.shadowLayout, `depth-instanced|${this.depthCullMode}|${geometry.key ?? ''}`, this.presentOf(geometry), this.depthCullMode, false, true));
4185
- pass.setBindGroup(0, this.shadowGroup(), [slot]);
4323
+ if (cutout !== null && cutoutGroup !== null) {
4324
+ this.writeCutout(slot, cutout, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS);
4325
+ }
4326
+ const cuts = cutoutGroup !== null;
4327
+ const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
4328
+ pass.setPipeline(depthPipeline(this.pipelines, this.surface.device, cuts ? this.shadowCutoutLayout : this.shadowLayout, `${cuts ? 'depth-instanced-cutout' : 'depth-instanced'}|${cull}|${geometry.key ?? ''}`, this.presentOf(geometry), cull, false, true, cuts));
4329
+ pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
4186
4330
  let index = 0;
4187
4331
  for (; index < geometry.vertexBuffers.length; index++) {
4188
4332
  pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
@@ -4611,28 +4755,6 @@ export class WebGPURenderer {
4611
4755
  */
4612
4756
  const blankView = blank.createView();
4613
4757
  const blankSampler = device.createSampler({ label: 'flat.sampler' });
4614
- /*
4615
- * A one-texel cube for every point-shadow sampler the variant declares.
4616
- *
4617
- * Twelve of them exist in the `pointShadows` permutation and none has a map behind it
4618
- * until row 3, but WebGPU requires every declared binding to be filled — and a cube view
4619
- * needs six array layers, so the flat 1×1 stand-in above cannot serve.
4620
- *
4621
- * **White is the second guard, and it is a deliberate value rather than a spare one.** The
4622
- * first is `uPointShadowIndex`, written to −1 for every light in `bindMeshPass`. If that
4623
- * ever failed, a full-depth texel reads as an occluder at the far plane — `pointShadow`
4624
- * converts the stored value back to a distance and 1.0 comes out as `far` — so the worst a
4625
- * mis-selected light can do is find nothing in the way. Zeroes would have meant an
4626
- * occluder pressed against the lamp, and every surface in the scene in shadow.
4627
- */
4628
- const blankCube = device.createTexture({
4629
- label: 'flat.emptyPointShadow',
4630
- size: [1, 1, 6],
4631
- format: 'rgba8unorm',
4632
- usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
4633
- });
4634
- device.queue.writeTexture({ texture: blankCube }, new Uint8Array(4 * 6).fill(255), { bytesPerRow: 4, rowsPerImage: 1 }, [1, 1, 6]);
4635
- const blankCubeView = blankCube.createView({ dimension: 'cube' });
4636
4758
  /*
4637
4759
  * A one-texel integer stand-in for the froxel table.
4638
4760
  *
@@ -4671,6 +4793,16 @@ export class WebGPURenderer {
4671
4793
  });
4672
4794
  device.queue.writeTexture({ texture: blankArray }, new Uint8Array(4).fill(255), { bytesPerRow: 4, rowsPerImage: 1 }, [1, 1, 1]);
4673
4795
  const blankArrayView = blankArray.createView({ dimension: '2d-array' });
4796
+ /*
4797
+ * DriftLight's two volumes before a field is whole: an index of one empty cell and an atlas
4798
+ * of one zero texel. Never read while `uDriftLight.x` is zero, and bound anyway, because a
4799
+ * group missing an entry its layout declares is a validation failure at `submit`.
4800
+ */
4801
+ const blankVolume = (label, format) => device
4802
+ .createTexture({ label, size: [1, 1, 1], dimension: '3d', format, usage: 0x2 | 0x4 })
4803
+ .createView({ dimension: '3d' });
4804
+ const blankDriftIndexView = blankVolume('flat.emptyDriftLightIndex', 'r32uint');
4805
+ const blankDriftAtlasView = blankVolume('flat.emptyDriftLightAtlas', 'rgba16float');
4674
4806
  this.shadowMap = device.createTexture({
4675
4807
  label: 'shadow.static',
4676
4808
  size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
@@ -4708,14 +4840,15 @@ export class WebGPURenderer {
4708
4840
  : null;
4709
4841
  this.dynamicView = this.dynamicMap === null ? null : this.dynamicMap.createView();
4710
4842
  /*
4711
- * **Cleared once, here, because a texture no pass has written holds undefined contents.**
4843
+ * **Cleared once, here, because a texture no pass has written reads as zero**, which the
4844
+ * specification requires, and a zero in a map compared with `LEQUAL` is an occluder at the
4845
+ * light. A scene is entitled to switch directional shadows *on* and never open a pass —
4846
+ * `night-street` does exactly that, with `shadowStrength` at 0.85 and no `beginShadowPass`
4847
+ * anywhere in it — and sampling that zero shadows everything, which is a picture rather than
4848
+ * an error and reads as a lighting bug.
4712
4849
  *
4713
- * A GL depth texture is born defined and reads as the far plane, so `renderer.ts` can hand
4714
- * an unrendered shadow map to the shader and get "nothing occludes anything". WebGPU makes
4715
- * no such promise, and a scene is entitled to switch directional shadows *on* and never
4716
- * open a pass — `night-street` does exactly that, with `shadowStrength` at 0.85 and no
4717
- * `beginShadowPass` anywhere in it. Sampling garbage then shadows whatever the garbage
4718
- * happens to say, which is a picture rather than an error and reads as a lighting bug.
4850
+ * `shadowMap.ts` does the same for the other backend, for the same reason found later: WebGL
4851
+ * zeroes a new texture too, and only ANGLE on Vulkan happened to hand one back reading 1.
4719
4852
  *
4720
4853
  * Found by dumping the flat block off both live renderers and comparing it field by field;
4721
4854
  * `uPeeledShadowEnabled` came back 1 against 0 and pointed straight at the maps behind it.
@@ -4826,7 +4959,7 @@ export class WebGPURenderer {
4826
4959
  * is writing, so the binding has to move whatever the shader intends to do with it.
4827
4960
  */
4828
4961
  if (name === 'uEnvironment') {
4829
- return this.probeView === null || !this.probeBaked || this.probePassActive
4962
+ return this.probeView === null || !this.probeReadable()
4830
4963
  ? { view: blankArrayView, sampler: blankSampler }
4831
4964
  : { view: this.probeView, sampler: this.probeSampler };
4832
4965
  }
@@ -4860,6 +4993,17 @@ export class WebGPURenderer {
4860
4993
  if (name === 'uCookieAtlas') {
4861
4994
  return { view: this.cookieView ?? blankView, sampler: iesSampler };
4862
4995
  }
4996
+ /*
4997
+ * DriftLight's brick index, read by `texelFetch` alone, so it shares the shadow group's
4998
+ * sampler the way the froxel table does; and its atlas, which is filtered inside a brick and
4999
+ * so takes the linear, clamped one the photometric atlas uses.
5000
+ */
5001
+ if (name === 'uDriftLightIndex') {
5002
+ return { view: this.driftIndexView ?? blankDriftIndexView, sampler: shadowSampler };
5003
+ }
5004
+ if (name === 'uDriftLightAtlas') {
5005
+ return { view: this.driftAtlasView ?? blankDriftAtlasView, sampler: iesSampler };
5006
+ }
4863
5007
  /*
4864
5008
  * The colour the frame had already drawn, or the blank texel a frame that refracts nothing
4865
5009
  * falls back to.
@@ -4914,12 +5058,8 @@ export class WebGPURenderer {
4914
5058
  /* The same group: no texture is set at construction, so this is the stand-in's. */
4915
5059
  this.blankAlbedoBindGroup = this.bindGroup;
4916
5060
  this.skyLayout = createSkyBindGroupLayout(device);
4917
- this.skyUniforms = device.createBuffer({
4918
- label: 'sky.uniforms',
4919
- size: SKY_UNIFORM_SIZE,
4920
- usage: USAGE_UNIFORM_DST,
4921
- });
4922
- this.skyBindGroup = createSkyBindGroup(device, this.skyLayout, this.skyUniforms);
5061
+ this.skyUniforms = new UniformRing(device, SKY_UNIFORM_SIZE, MAX_SKY_DRAWS, USAGE_UNIFORM_DST, 'sky.ring');
5062
+ this.skyBindGroup = createSkyBindGroup(device, this.skyLayout, this.skyUniforms.buffer);
4923
5063
  this.plumeLayout = createPlumeBindGroupLayout(device);
4924
5064
  this.plumeVerts = new UniformRing(device, PLUME_VERT_SIZE, MAX_PLUMES_PER_FRAME, USAGE_UNIFORM_DST);
4925
5065
  this.plumeFrags = new UniformRing(device, PLUME_FRAG_SIZE, MAX_PLUMES_PER_FRAME, USAGE_UNIFORM_DST);
@@ -4958,6 +5098,8 @@ export class WebGPURenderer {
4958
5098
  usage: 0x10 | 0x4 | 0x2, // RENDER_ATTACHMENT | TEXTURE_BINDING | COPY_DST
4959
5099
  })
4960
5100
  : null;
5101
+ this.probeTargets =
5102
+ this.probe === null ? null : new ProbeTargets(device, this.probe, this.probeSize);
4961
5103
  /*
4962
5104
  * The prefiltered twin, allocated on exactly the condition the capture is and with exactly
4963
5105
  * its format and level count. Same format because a pipeline's colour target must equal its
@@ -5001,6 +5143,10 @@ export class WebGPURenderer {
5001
5143
  is not a screen-sized target. Filtering is what makes a 32-lattice table smooth rather
5002
5144
  than banded, so it is declared filterable like the colour inputs and unlike the depth. */
5003
5145
  { binding: RUSH_TEXTURES.uGradeLut, dimension: '3d' },
5146
+ /* Eye adaptation's held brightness: one half-float texel, or the scene where none is held. */
5147
+ { binding: RUSH_TEXTURES.uExposureHeld },
5148
+ /* Local exposure's grid, read filtered between tiles. See `localExposure.ts`. */
5149
+ { binding: RUSH_TEXTURES.uExposureLocal },
5004
5150
  ]);
5005
5151
  this.rushUniforms = device.createBuffer({
5006
5152
  label: 'post.rushUniforms',
@@ -5167,15 +5313,14 @@ export class WebGPURenderer {
5167
5313
  this.filmBindGroupBlank = this.filmBindGroup;
5168
5314
  this.scatterLayout = createScatterBindGroupLayout(device);
5169
5315
  this.scatterVerts = new UniformRing(device, SCATTER_VERT_SIZE, MAX_PLUMES_PER_FRAME, USAGE_UNIFORM_DST);
5170
- this.scatterFragUniforms = device.createBuffer({
5171
- label: 'scatter.fragUniforms',
5172
- size: SCATTER_FRAG_SIZE,
5173
- usage: USAGE_UNIFORM_DST,
5174
- });
5175
- this.scatterBindGroup = createScatterBindGroup(device, this.scatterLayout, this.scatterVerts.buffer, this.scatterFragUniforms);
5316
+ this.scatterFrags = new UniformRing(device, SCATTER_FRAG_SIZE, MAX_PLUMES_PER_FRAME, USAGE_UNIFORM_DST, 'scatter.fragRing');
5317
+ this.scatterBindGroup = createScatterBindGroup(device, this.scatterLayout, this.scatterVerts.buffer, this.scatterFrags.buffer);
5176
5318
  this.shadowLayout = createDepthBindGroupLayout(device);
5177
5319
  this.shadowSkinnedLayout = createDepthBindGroupLayout(device, true);
5178
- this.shadowDraws = new UniformRing(device, DEPTH_VERT_SIZE, MAX_DRAWS_PER_FRAME, USAGE_UNIFORM_DST, 'shadow.drawRing', this.shadowDrawBudget);
5320
+ this.shadowCutoutLayout = createDepthBindGroupLayout(device, false, true);
5321
+ this.shadowDraws = new UniformRing(device,
5322
+ /* The cutout block is the plain one and two fields more, and a slot must hold either. */
5323
+ Math.max(DEPTH_VERT_SIZE, DEPTH_CUTOUT_VERT_SIZE), MAX_DRAWS_PER_FRAME, USAGE_UNIFORM_DST, 'shadow.drawRing', this.shadowDrawBudget);
5179
5324
  const depthPassUniforms = device.createBuffer({
5180
5325
  label: 'depth.passUniforms',
5181
5326
  size: DEPTH_FRAG_SIZE,
@@ -5213,6 +5358,8 @@ export class WebGPURenderer {
5213
5358
  if (peelPassUniforms !== null) {
5214
5359
  device.queue.writeBuffer(peelPassUniforms, 0, new Int32Array([1]));
5215
5360
  }
5361
+ this.shadowPeelPassUniforms = peelPassUniforms;
5362
+ this.shadowPeelSampler = shadowSampler;
5216
5363
  this.shadowPeelBindGroup =
5217
5364
  peelPassUniforms === null
5218
5365
  ? null
@@ -5221,13 +5368,6 @@ export class WebGPURenderer {
5221
5368
  peelPassUniforms === null
5222
5369
  ? null
5223
5370
  : createScatterDepthBindGroup(device, this.scatterDepthLayout, this.scatterDepthDraws.buffer, { binding: DEPTH_FRAG_BINDING, buffer: peelPassUniforms, size: DEPTH_FRAG_SIZE }, DEPTH_PREVIOUS_BINDING, this.shadowView, shadowSampler);
5224
- /*
5225
- * The light volume, last because it wants the shadow map and the stand-in that everything
5226
- * above already built. The peel and dynamic maps get the white one-pixel texture, which is
5227
- * depth 1 and therefore nothing in the way — see `createLightVolumeBindGroup` for why a
5228
- * stand-in of zero would put every beam in shadow instead.
5229
- */
5230
- const uniformBuffer = (label, size) => device.createBuffer({ label, size, usage: USAGE_UNIFORM_DST });
5231
5371
  /*
5232
5372
  * The overlays. Rings rather than single blocks because a frame draws several of each —
5233
5373
  * the loader alone is two panels and two strings — and `queue.writeBuffer` does not
@@ -5389,6 +5529,7 @@ export class WebGPURenderer {
5389
5529
  this.reflectionQueue.reset();
5390
5530
  /* Redeclared every frame, so a field a consumer stops declaring stops lighting. */
5391
5531
  this.distanceFields.reset();
5532
+ this.frameSky = null;
5392
5533
  this.oitActive =
5393
5534
  this.quality.orderIndependent && this.quality.screenEffects && this.samples === 1;
5394
5535
  /* Read off the line above rather than restating its last term: with the other two true, the
@@ -5410,6 +5551,7 @@ export class WebGPURenderer {
5410
5551
  /* Anything drawn after the last `endFrame` goes now, before its encoder is replaced. */
5411
5552
  this.flushOverlay();
5412
5553
  this.framePresented = false;
5554
+ this.encoderEpoch = ++this.encoderEpochs;
5413
5555
  /* Before the budget is cleared, because what to grow to is what the last frame asked for. */
5414
5556
  this.growRings();
5415
5557
  this.budget.reset();
@@ -5449,6 +5591,7 @@ export class WebGPURenderer {
5449
5591
  this.windStreakVerts.reset();
5450
5592
  this.windStreakFrags.reset();
5451
5593
  this.scatterVerts.reset();
5594
+ this.scatterFrags.reset();
5452
5595
  this.panelVerts.reset();
5453
5596
  this.panelFrags.reset();
5454
5597
  this.filmVerts.reset();
@@ -5460,6 +5603,7 @@ export class WebGPURenderer {
5460
5603
  this.sdfTextFrags.reset();
5461
5604
  this.lineVerts.reset();
5462
5605
  this.lineFrags.reset();
5606
+ this.skyUniforms.reset();
5463
5607
  /* The material ring, and the open slot with it: last frame's slots are gone. */
5464
5608
  this.perFrame.reset();
5465
5609
  this.materials.dirty();
@@ -5993,9 +6137,24 @@ export class WebGPURenderer {
5993
6137
  f[base + 1] = counts[1];
5994
6138
  f[base + 2] = counts[2];
5995
6139
  }
6140
+ /* Which sets the shading reads, and how far between them. See `probeSweeps.ts`. */
6141
+ const setsAt = this.fragment.fields['uProbeGridSets'];
6142
+ if (setsAt !== undefined) {
6143
+ const sets = this.probeSweeps?.uniforms(this.probeSets) ?? this.probeSets.fill(0);
6144
+ const base = setsAt.offset / 4;
6145
+ f[base] = sets[0] ?? 0;
6146
+ f[base + 1] = sets[1] ?? 0;
6147
+ f[base + 2] = sets[2] ?? 0;
6148
+ }
5996
6149
  /* Which chain is bound, because the level and the weight both depend on it. */
5997
6150
  f[at('uEnvironmentPrefiltered')] = this.quality.environmentPrefilter ? 1 : 0;
5998
6151
  }
6152
+ /* DriftLight, or zeros that switch it off: see `driftLight/uniforms.ts`. */
6153
+ this.uploadLightField();
6154
+ const drift = this.driftUniforms;
6155
+ resolveDriftLight(this.driftAtlas === null ? null : this.lightField, this.probePassActive, camera.position, drift);
6156
+ f.set(drift.light, at('uDriftLight'));
6157
+ f.set(drift.origin, at('uDriftLightOrigin'));
5999
6158
  /*
6000
6159
  * The ORM scales, whose base is 1 and not 0 — the value an unwritten uniform would hold.
6001
6160
  *
@@ -6074,7 +6233,7 @@ export class WebGPURenderer {
6074
6233
  * it every frame.
6075
6234
  */
6076
6235
  if (this.probeEnabled) {
6077
- const usable = this.probeBaked && !this.probePassActive;
6236
+ const usable = this.probeReadable();
6078
6237
  f[at('uEnvironmentEnabled')] = usable ? 1 : 0;
6079
6238
  f[at('uEnvironmentMaxLod')] = this.probeMaxLod;
6080
6239
  /*
@@ -6310,6 +6469,14 @@ export class WebGPURenderer {
6310
6469
  }
6311
6470
  /** Said once rather than every frame, for the reason `warnedFull` gives. */
6312
6471
  warnedMaterialsFull = false;
6472
+ warnedSkyFull = false;
6473
+ warnedParticlesTwice = false;
6474
+ /**
6475
+ * Which encoder draws are being recorded into, by count: a new one at every `beginFrame`, and one
6476
+ * of its own for each probe bake, restored after. What a batch drawn per encoder is checked by.
6477
+ */
6478
+ encoderEpoch = 0;
6479
+ encoderEpochs = 0;
6313
6480
  /**
6314
6481
  * Draw one mesh.
6315
6482
  *
@@ -6593,7 +6760,9 @@ export class WebGPURenderer {
6593
6760
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
6594
6761
  /* The two order-independent buffers are two formats and two blend states, so a draw into
6595
6762
  one must never share a cached pipeline with the same mesh drawn into the other. */
6596
- `${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}`;
6763
+ `${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
6764
+ /* Culling is pipeline state here: a two-sided surface culls nothing. */
6765
+ `${this.materialDoubleSided ? '|2s' : ''}`;
6597
6766
  /*
6598
6767
  * The morph uniforms, written only for a morphed draw and read from the *morphed* variant's
6599
6768
  * field table. `FLAT_VERT_FIELDS` is the plain variant's and does not name them at all — every
@@ -6623,7 +6792,7 @@ export class WebGPURenderer {
6623
6792
  if (present === undefined) {
6624
6793
  throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
6625
6794
  }
6626
- pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, blend, skinned, morphed, depthWrite, layer, false, this.oitMode);
6795
+ pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, blend, skinned, morphed, depthWrite, layer, false, this.oitMode, this.materialDoubleSided);
6627
6796
  }
6628
6797
  /*
6629
6798
  * Guards the draw call only, not the restore below.
@@ -6829,7 +6998,8 @@ export class WebGPURenderer {
6829
6998
  a third vertex buffer and a different vertex module, and sharing a cache entry with the
6830
6999
  plain one would hand a draw the wrong layout. */
6831
7000
  const key = `${base}|inst${blend ? '|blend' : ''}` +
6832
- `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}`;
7001
+ `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
7002
+ `${this.materialDoubleSided ? '|2s' : ''}`;
6833
7003
  const pipelines = this.targetPipelines();
6834
7004
  let pipeline = pipelines.peek(key);
6835
7005
  if (pipeline === undefined) {
@@ -6837,7 +7007,7 @@ export class WebGPURenderer {
6837
7007
  if (present === undefined) {
6838
7008
  throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
6839
7009
  }
6840
- pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(false, false), this.variant, key, present, blend, false, false, depthWrite, layer, true);
7010
+ pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(false, false), this.variant, key, present, blend, false, false, depthWrite, layer, true, 'none', this.materialDoubleSided);
6841
7011
  }
6842
7012
  const material = this.materialSlotForDraw();
6843
7013
  if (material !== null) {
@@ -6978,7 +7148,10 @@ export class WebGPURenderer {
6978
7148
  i[atF('uFogMode')] = medium.fogMode;
6979
7149
  f[atF('uFogNear')] = medium.fogNear;
6980
7150
  f[atF('uFogFar')] = medium.fogFar;
6981
- this.surface.device.queue.writeBuffer(this.scatterFragUniforms, 0, this.scatterFragStaging);
7151
+ const fragmentSlot = this.scatterFrags.allocate();
7152
+ if (fragmentSlot === null)
7153
+ return;
7154
+ this.scatterFrags.writeBlock(fragmentSlot, this.scatterFragInts);
6982
7155
  const scatterPipe = scatterPipeline(this.pipelines, this.surface.device, this.scatterLayout);
6983
7156
  /* One dynamic offset, and a group of two uniform buffers: nothing sampled, nothing read. */
6984
7157
  const scatterCommand = this.recordDraw(0, this.currentTarget());
@@ -6986,7 +7159,8 @@ export class WebGPURenderer {
6986
7159
  scatterCommand.pipeline = scatterPipe;
6987
7160
  scatterCommand.bindGroup = this.scatterBindGroup;
6988
7161
  scatterCommand.offsetA = slot;
6989
- scatterCommand.offsetCount = 1;
7162
+ scatterCommand.offsetB = fragmentSlot;
7163
+ scatterCommand.offsetCount = 2;
6990
7164
  for (let index = 0; index < scatter.vertexBuffers.length; index++) {
6991
7165
  scatterCommand.vertexBuffers[index] = scatter.vertexBuffers[index];
6992
7166
  }
@@ -7001,7 +7175,7 @@ export class WebGPURenderer {
7001
7175
  if (pass === null)
7002
7176
  return;
7003
7177
  pass.setPipeline(scatterPipe);
7004
- pass.setBindGroup(0, this.scatterBindGroup, [slot]);
7178
+ pass.setBindGroup(0, this.scatterBindGroup, [slot, fragmentSlot]);
7005
7179
  for (let index = 0; index < scatter.vertexBuffers.length; index++) {
7006
7180
  pass.setVertexBuffer(index, scatter.vertexBuffers[index]);
7007
7181
  }
@@ -7556,6 +7730,7 @@ export class WebGPURenderer {
7556
7730
  */
7557
7731
  rebuildFlatGroupsForNewRings() {
7558
7732
  this.flatBindGroups.clear();
7733
+ this.flatEnvironment = this.flatTextures('uEnvironment').view;
7559
7734
  this.skinnedGroups.length = 0;
7560
7735
  const albedo = this.albedo;
7561
7736
  const normalMap = this.normalMap;
@@ -7574,6 +7749,25 @@ export class WebGPURenderer {
7574
7749
  /* The open material lives in a slot of a buffer that no longer exists. */
7575
7750
  this.materials.dirty();
7576
7751
  }
7752
+ /**
7753
+ * The environment view the cached flat groups bind: the grid, or the blank stand-in while the
7754
+ * grid may not be read. What `rebindEnvironment` compares against.
7755
+ */
7756
+ flatEnvironment = null;
7757
+ /**
7758
+ * Rebuild the flat groups if, and only if, the environment they bind has changed.
7759
+ *
7760
+ * **Every cached group holds the environment view**, so a change of it makes all of them stale;
7761
+ * but nothing else a bake does touches a group. Rebuilding on every bake's two edges regardless
7762
+ * emptied the cache twice a bake, and every material built its group again inside the bake and
7763
+ * again after it: a courtyard re-baking a few faces a frame spent 28% of its CPU there on the
7764
+ * native host, where a `createBindGroup` costs most. What would make this wrong is a second
7765
+ * binding that a bake changes, which would have to be compared here too.
7766
+ */
7767
+ rebindEnvironment() {
7768
+ if (this.flatTextures('uEnvironment').view !== this.flatEnvironment)
7769
+ this.rebuildFlatBindGroup();
7770
+ }
7577
7771
  /**
7578
7772
  * Rebuild the group for the open material, and drop every cached one.
7579
7773
  *
@@ -7585,6 +7779,7 @@ export class WebGPURenderer {
7585
7779
  */
7586
7780
  rebuildFlatBindGroup() {
7587
7781
  this.flatBindGroups.clear();
7782
+ this.flatEnvironment = this.flatTextures('uEnvironment').view;
7588
7783
  if (this.albedo === null &&
7589
7784
  this.normalMap === null &&
7590
7785
  this.ormMap === null &&
@@ -7618,6 +7813,7 @@ export class WebGPURenderer {
7618
7813
  drawSky(camera, sky, env) {
7619
7814
  if (!this.canDraw())
7620
7815
  return;
7816
+ this.frameSky = sky;
7621
7817
  const f = this.skyFloats;
7622
7818
  const at = (name) => SKY_FIELDS[name].offset / 4;
7623
7819
  /*
@@ -7643,13 +7839,23 @@ export class WebGPURenderer {
7643
7839
  const medium = resolveAtmosphere(env, this.atmosphereHeight(camera), this.quality.underwaterAtmosphere, this.medium);
7644
7840
  f.set(medium.underwaterColor, at('uUnderwaterColor'));
7645
7841
  f[at('uUnderwaterFactor')] = medium.underwaterFactor;
7646
- this.surface.device.queue.writeBuffer(this.skyUniforms, 0, this.skyStaging);
7842
+ const slot = this.skyUniforms.allocate();
7843
+ if (slot === null) {
7844
+ if (!this.warnedSkyFull) {
7845
+ this.warnedSkyFull = true;
7846
+ console.warn(`WebGPU: more than ${MAX_SKY_DRAWS} sky draws in a frame; the rest are skipped`);
7847
+ }
7848
+ return;
7849
+ }
7850
+ this.skyUniforms.writeFloats(slot, 0, f);
7647
7851
  const skyPipe = skyPipeline(this.pipelines, this.surface.device, this.skyLayout);
7648
7852
  /* Reads nothing: the group is one uniform buffer, and the sky samples no attachment. */
7649
7853
  const skyCommand = this.recordDraw(0, this.currentTarget());
7650
7854
  if (skyCommand !== null) {
7651
7855
  skyCommand.pipeline = skyPipe;
7652
7856
  skyCommand.bindGroup = this.skyBindGroup;
7857
+ skyCommand.offsetA = slot;
7858
+ skyCommand.offsetCount = 1;
7653
7859
  skyCommand.count = 3;
7654
7860
  }
7655
7861
  else {
@@ -7657,7 +7863,7 @@ export class WebGPURenderer {
7657
7863
  if (pass === null)
7658
7864
  return;
7659
7865
  pass.setPipeline(skyPipe);
7660
- pass.setBindGroup(0, this.skyBindGroup);
7866
+ pass.setBindGroup(0, this.skyBindGroup, [slot]);
7661
7867
  pass.draw(3);
7662
7868
  }
7663
7869
  }
@@ -7769,6 +7975,18 @@ export class WebGPURenderer {
7769
7975
  const bloomStrength = this.quality.bloom * this.bloomScale;
7770
7976
  if (bloomStrength > 0)
7771
7977
  this.runBloom(encoder);
7978
+ /*
7979
+ * Eye adaptation, metered from the scene the composite shows, and local exposure's grid beside
7980
+ * it. The held brightness is kept for either, since local exposure moves regions relative to it.
7981
+ * See `setAutoExposure` and `setLocalExposure`.
7982
+ */
7983
+ const localising = this.localExposure > 0;
7984
+ const measuring = (this.autoExposure > 0 || localising) && this.exposurePass !== null;
7985
+ const adapting = measuring && this.autoExposure > 0;
7986
+ const shownScene = this.reconstructing ? this.reconShownView : this.sceneColorView;
7987
+ if (measuring && shownScene !== null) {
7988
+ this.exposurePass?.run(encoder, shownScene, this.autoExposureDt, localising);
7989
+ }
7772
7990
  /* The grade travels here only where the mesh pass gave it up. See `bindMeshPass`. */
7773
7991
  const graded = this.quality.hdrScene;
7774
7992
  const f = this.rushFloats;
@@ -7807,6 +8025,12 @@ export class WebGPURenderer {
7807
8025
  /* The grade, sampled after the curve and before the veil. See `uGradeLut` in `rush.ts`. */
7808
8026
  f[at('uGradeStrength')] = this.gradeStrength;
7809
8027
  f[at('uGradeSize')] = this.gradeSize;
8028
+ /* The lens and the print. See `setVignette` and `setFilmGrain`. */
8029
+ f[at('uVignette')] = this.vignette;
8030
+ f[at('uAutoExposure')] = adapting ? this.autoExposure : 0;
8031
+ f[at('uLocalExposure')] = measuring && localising ? this.localExposure : 0;
8032
+ f[at('uGrain')] = this.grain;
8033
+ f[at('uGrain') + 1] = this.grainSeed;
7810
8034
  device.queue.writeBuffer(this.rushUniforms, 0, this.rushStaging);
7811
8035
  const pass = encoder.beginRenderPass({
7812
8036
  label: 'post.composite',
@@ -7877,6 +8101,10 @@ export class WebGPURenderer {
7877
8101
  a frame that never grades anything should not carry a texture, and a declared binding
7878
8102
  must still have a complete one — the argument the bloom fallback above makes. */
7879
8103
  ...entry(RUSH_TEXTURES.uGradeLut, this.ensureGradeView()),
8104
+ /* The scene as the placeholder until adaptation is asked for: the strength is zero then,
8105
+ and a declared binding still needs a complete texture, as the bloom fallback says. */
8106
+ ...entry(RUSH_TEXTURES.uExposureHeld, this.exposurePass?.heldView ?? sceneView),
8107
+ ...entry(RUSH_TEXTURES.uExposureLocal, this.exposurePass?.localView ?? sceneView),
7880
8108
  ],
7881
8109
  layout: this.rushLayout,
7882
8110
  });
@@ -9586,8 +9814,13 @@ export class WebGPURenderer {
9586
9814
  const swap = this.swapView();
9587
9815
  if (swap === null)
9588
9816
  return null;
9589
- const width = this.depth?.width ?? 1;
9590
- const height = this.depth?.height ?? 1;
9817
+ /*
9818
+ * **The drawing buffer's size, not the scene depth's.** The overlay draws on the swap image,
9819
+ * past the resolve, and a reconstruction draws the scene depth small: sized from that, the
9820
+ * device refused the pass beside a full-size swap image and every overlay went missing.
9821
+ */
9822
+ const width = Math.max(1, this.surface.canvas.width);
9823
+ const height = Math.max(1, this.surface.canvas.height);
9591
9824
  if (this.overlayDepth === null ||
9592
9825
  this.overlayDepth.width !== width ||
9593
9826
  this.overlayDepth.height !== height) {
@@ -9926,10 +10159,26 @@ export class WebGPURenderer {
9926
10159
  * had stopped declaring. What the guard is actually for is not *building* a composer for a
9927
10160
  * profile that never declares anything, so that is all it now guards.
9928
10161
  */
10162
+ if (this.fieldComposer === null && this.distanceFields.length === 0)
10163
+ return;
10164
+ /*
10165
+ * **As many cascades as reach the probe grid**, and rebuilt when the grid asks for a different
10166
+ * number: the far end of a courtyard longer than the default 16 m was traced against no field
10167
+ * at all and lit as open sky from every side. See `gi/fieldReach.ts`.
10168
+ */
10169
+ const grid = this.indirectGrid();
10170
+ const cascades = grid === null
10171
+ ? DEFAULT_FIELD_COMPOSE.cascades
10172
+ : cascadesForGrid(grid, DEFAULT_FIELD_COMPOSE.radius, DEFAULT_FIELD_COMPOSE.cascades, MAX_FIELD_CASCADES);
10173
+ if (this.fieldComposer !== null && this.fieldComposer.cascadeCount !== cascades) {
10174
+ this.fieldComposer.dispose();
10175
+ this.fieldComposer = null;
10176
+ }
9929
10177
  if (this.fieldComposer === null) {
9930
- if (this.distanceFields.length === 0)
9931
- return;
9932
- this.fieldComposer = new FieldComposer(this.surface.device, DEFAULT_FIELD_COMPOSE);
10178
+ this.fieldComposer = new FieldComposer(this.surface.device, {
10179
+ ...DEFAULT_FIELD_COMPOSE,
10180
+ cascades,
10181
+ });
9933
10182
  }
9934
10183
  this.fieldComposer.compose(encoder, this.distanceFields, this.frameEye);
9935
10184
  }
@@ -9996,11 +10245,20 @@ export class WebGPURenderer {
9996
10245
  * never said what was lighting it.
9997
10246
  */
9998
10247
  const env = this.frameEnv;
9999
- this.probeBaker.bake(encoder, field, grid, this.probeBaked, {
10248
+ /*
10249
+ * And the scene's summed fixed lights, which the trace adds to what a hit receives: the bounce
10250
+ * of every candle, not only of the ones near enough to be shaded exactly.
10251
+ */
10252
+ resolveDriftLight(this.bakeLightField === null ? null : this.lightField, true, this.frameEye, this.bakeDriftUniforms);
10253
+ /* And the frame's exact lights, flickering and moving as the frame shades them. */
10254
+ this.bakeLamps.count = resolveBounceLights(env, this.bounceLights);
10255
+ this.bakeLamps.falloff = this.lights.falloff;
10256
+ this.probeBaker.bake(encoder, field, grid, this.probeBaked, this.bakeLightField, this.bakeLamps, {
10000
10257
  direction: env?.directionalDir ?? NO_SUN,
10001
10258
  colour: env?.directionalColor ?? NO_SUN,
10002
- /* What a ray that left the world finds. The frame's own ambient, not a capture. */
10003
- sky: env?.ambient ?? NO_SUN,
10259
+ /* What a ray that left the world finds: the sky this frame drew, or its ambient. */
10260
+ ambient: env?.ambient ?? NO_SUN,
10261
+ sky: this.frameSky,
10004
10262
  });
10005
10263
  /*
10006
10264
  * **The bake is what fills the layers now, so it is what marks them filled.** `probeBaked`
@@ -10191,7 +10449,7 @@ export class WebGPURenderer {
10191
10449
  * currently open. Offering it earlier hands a pass a texture of undefined contents, which is
10192
10450
  * the failure `probeBaked` was introduced for on the forward path.
10193
10451
  */
10194
- const usable = this.probeView !== null && this.probeBaked && !this.probePassActive;
10452
+ const usable = this.probeView !== null && this.probeReadable();
10195
10453
  if (usable) {
10196
10454
  const environment = this.passEnvironment;
10197
10455
  environment.view = this.probeView;
@@ -10631,6 +10889,8 @@ export class WebGPURenderer {
10631
10889
  * -----------------------------------------------------------------------------------------
10632
10890
  */
10633
10891
  bloomScale = 1;
10892
+ /** In scene units, once `setBloom` has moved it; null reads the profile's. */
10893
+ bloomThresholdSet = null;
10634
10894
  rushStrength = 0;
10635
10895
  motionBlurScale = 1;
10636
10896
  /**
@@ -10642,6 +10902,48 @@ export class WebGPURenderer {
10642
10902
  */
10643
10903
  veilColor = [0, 0, 0];
10644
10904
  veilAlpha = 0;
10905
+ vignette = 0;
10906
+ grain = 0;
10907
+ grainSeed = 0;
10908
+ warnedLookWithoutComposite = false;
10909
+ /** Eye adaptation: how far, this frame's time step, and the pass once asked for. */
10910
+ autoExposure = 0;
10911
+ autoExposureDt = 0;
10912
+ /** How far each region is brought toward the frame. See `setLocalExposure`. */
10913
+ localExposure = 0;
10914
+ exposurePass = null;
10915
+ /**
10916
+ * How strongly the lens darkens the frame's corners, held until changed: 0 is none, 0.5 is about
10917
+ * 1.2 stops at the corner. Applied to scene light before the tone curve, as a lens loses light;
10918
+ * see `filmLook.ts`. Needs `screenEffects`, and says so once rather than doing nothing.
10919
+ */
10920
+ setVignette(strength) {
10921
+ if (!this.hasComposite) {
10922
+ this.warnLookWithoutComposite(strength);
10923
+ return;
10924
+ }
10925
+ this.vignette = clampVignette(strength);
10926
+ }
10927
+ /**
10928
+ * Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
10929
+ * grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
10930
+ * its caller: pass a new one each frame for grain that moves, and the same one for a still that
10931
+ * is identical run to run. Applied last, after the grade and the veil; see `filmLook.ts`.
10932
+ */
10933
+ setFilmGrain(strength, seed) {
10934
+ if (!this.hasComposite) {
10935
+ this.warnLookWithoutComposite(strength);
10936
+ return;
10937
+ }
10938
+ this.grain = clampGrain(strength);
10939
+ this.grainSeed = grainSeed(seed);
10940
+ }
10941
+ warnLookWithoutComposite(strength) {
10942
+ if (strength > 0 && !this.warnedLookWithoutComposite) {
10943
+ this.warnedLookWithoutComposite = true;
10944
+ console.warn(FILM_LOOK_WITHOUT_COMPOSITE);
10945
+ }
10946
+ }
10645
10947
  /*
10646
10948
  * **These three warned that the composite did not exist, and went on warning after it did.**
10647
10949
  *
@@ -10717,8 +11019,17 @@ export class WebGPURenderer {
10717
11019
  this.gradeView = this.gradeTexture.createView({ dimension: '3d' });
10718
11020
  this.gradeSize = lut.size;
10719
11021
  }
10720
- setBloom(scale) {
11022
+ setBloom(scale, threshold) {
10721
11023
  this.bloomScale = Math.min(Math.max(scale, 0), 1);
11024
+ if (threshold === undefined)
11025
+ return;
11026
+ const next = bloomThresholdOf(threshold);
11027
+ if (next === this.bloomThresholdSet)
11028
+ return;
11029
+ this.bloomThresholdSet = next;
11030
+ /* The blocks are written once per size, so a moved threshold rewrites them now. */
11031
+ if (this.bloomLevels.length > 0)
11032
+ this.writeBloomBlocks();
10722
11033
  }
10723
11034
  /**
10724
11035
  * How thick the air is, this frame: a global participating medium filling the whole frustum.
@@ -10743,6 +11054,62 @@ export class WebGPURenderer {
10743
11054
  setCameraMotionBlur(scale) {
10744
11055
  this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
10745
11056
  }
11057
+ /**
11058
+ * The camera has cut: this frame is a new shot, not the next moment of the last one.
11059
+ *
11060
+ * **Everything temporal reprojects through the previous frame's view**: the motion blur measures
11061
+ * its smear against it, and the temporal resolve reads its history through it. A cut is a jump
11062
+ * the renderer cannot tell from a fast camera, so without this a transport that seeks, a respawn
11063
+ * or an edit smears its first frame along the whole jump and blends in a picture of somewhere
11064
+ * else. The caller knows it cut; this is how it says so, before the frame's `beginFrame`.
11065
+ *
11066
+ * What it gives up is one frame of history: the frame after a cut has no blur and no temporal
11067
+ * blend, as the first frame of a session has none. Nothing else is reset.
11068
+ */
11069
+ cameraCut() {
11070
+ this.hasPreviousView = false;
11071
+ this.temporalHistory.invalidate();
11072
+ /* The reconstruction keeps histories of its own, and they are the last shot's too. */
11073
+ this.reconHasHistory = false;
11074
+ /* And the eye was adapted to the last shot. */
11075
+ this.exposurePass?.cut();
11076
+ }
11077
+ /**
11078
+ * Eye adaptation. **The other backend's `setAutoExposure` carries the reasoning**, whole; this is
11079
+ * the same setter over the same state, and the pass is built the first time it is asked for, so
11080
+ * a renderer that never adapts carries none of it.
11081
+ */
11082
+ setAutoExposure(strength, dtSec) {
11083
+ if (!this.hasComposite || !this.quality.hdrScene) {
11084
+ this.warnLookWithoutComposite(strength);
11085
+ return;
11086
+ }
11087
+ this.autoExposure = clampAutoExposure(strength);
11088
+ this.autoExposureDt = dtSec;
11089
+ if (this.autoExposure > 0)
11090
+ this.ensureExposurePass();
11091
+ }
11092
+ /**
11093
+ * Local exposure. **The other backend's `setLocalExposure` carries the reasoning**, whole; this is
11094
+ * the same setter over the same state, sharing eye adaptation's pass, which is built the first
11095
+ * time either is asked for.
11096
+ */
11097
+ setLocalExposure(strength) {
11098
+ if (!this.hasComposite || !this.quality.hdrScene) {
11099
+ this.warnLookWithoutComposite(strength);
11100
+ return;
11101
+ }
11102
+ this.localExposure = clampLocalExposure(strength);
11103
+ if (this.localExposure > 0)
11104
+ this.ensureExposurePass();
11105
+ }
11106
+ /** The exposure pass, built once; the composite's group holds placeholders until it exists. */
11107
+ ensureExposurePass() {
11108
+ if (this.exposurePass !== null)
11109
+ return;
11110
+ this.exposurePass = new ExposurePass(this.surface.device, this.pipelines, this.postSampler);
11111
+ this.rebuildRushBindGroup();
11112
+ }
10746
11113
  /**
10747
11114
  * Where this frame's lens is focused, how deep the sharp zone is, and how much of the ceiling
10748
11115
  * to take. **The other backend's `setDepthOfField` carries the reasoning**, whole: the
@@ -10947,6 +11314,8 @@ export class WebGPURenderer {
10947
11314
  * over a lobe-convolved one.
10948
11315
  */
10949
11316
  probe;
11317
+ /** The views, groups and attachments a bake draws with, made once. See `probeTargets.ts`. */
11318
+ probeTargets;
10950
11319
  /**
10951
11320
  * The prefiltered cube: what the lit pass samples, one GGX-convolved level per roughness.
10952
11321
  *
@@ -10978,6 +11347,19 @@ export class WebGPURenderer {
10978
11347
  probeMipLayout;
10979
11348
  /** The photometric atlas, allocated on first use. See `setIesProfiles`. */
10980
11349
  iesTexture = null;
11350
+ /** The scene's DriftLight field, and its two volumes once it is whole. See `createLightField`. */
11351
+ lightField = null;
11352
+ driftIndex = null;
11353
+ driftAtlas = null;
11354
+ driftIndexView = null;
11355
+ driftAtlasView = null;
11356
+ driftUniforms = createDriftLightUniforms();
11357
+ /** The same field resolved for a traced probe, and what the baker binds; null while not whole. */
11358
+ bakeDriftUniforms = createDriftLightUniforms();
11359
+ bakeLightField = null;
11360
+ /** The frame's exact lights packed for the traced bake. See `gi/bounceLights.ts`. */
11361
+ bounceLights = createBounceLights();
11362
+ bakeLamps = { data: this.bounceLights, count: 0, falloff: 0 };
10981
11363
  iesView = null;
10982
11364
  cookieTexture = null;
10983
11365
  cookieView = null;
@@ -11002,8 +11384,9 @@ export class WebGPURenderer {
11002
11384
  * mirroring uninitialised memory is worse than a car mirroring a gradient — the rule one probe
11003
11385
  * already had, unchanged by there being more of them.
11004
11386
  */
11005
- probeFilled = new Uint8Array(1);
11006
- probeFilledCount = 0;
11387
+ probeSweeps = null;
11388
+ /** `uProbeGridSets`, rewritten in place. */
11389
+ probeSets = new Float32Array(3);
11007
11390
  /**
11008
11391
  * Whether a bake is running, which is when the cube may be neither sampled nor bound.
11009
11392
  *
@@ -11011,13 +11394,23 @@ export class WebGPURenderer {
11011
11394
  * load-bearing rather than defensive: WebGPU rejects a pass that holds the texture it writes.
11012
11395
  */
11013
11396
  probePassActive = false;
11397
+ /** Whether the bake under way lights its faces by the grid. `ProbeBakeOptions.bounce`. */
11398
+ probeBounce = false;
11399
+ /**
11400
+ * Whether the shading may read the grid now: once every probe is filled, and not during a bake
11401
+ * unless it is a bounce bake. A bounce bake's faces are drawn into the probe's own cube and only
11402
+ * the resolve writes the array, in a pass of its own, so no pass both samples and writes it.
11403
+ */
11404
+ probeReadable() {
11405
+ return this.probeBaked && (!this.probePassActive || this.probeBounce);
11406
+ }
11014
11407
  /** How many mip levels the roughness blur has to work with. `flat.ts` reads this. */
11015
11408
  get probeMaxLod() {
11016
11409
  return ggxMaxLevelFor(this.probeEdge);
11017
11410
  }
11018
- /** Whether every layer of the grid has been convolved. See `probeFilled`. */
11411
+ /** Whether a whole sweep of the grid has been convolved. See `probeSweeps.ts`. */
11019
11412
  get probeBaked() {
11020
- return this.probes !== null && this.probeFilledCount === this.probes.layers;
11413
+ return this.probes !== null && this.probeSweeps?.ready === true;
11021
11414
  }
11022
11415
  /**
11023
11416
  * Record that one layer now holds a convolution, without counting a rebake twice.
@@ -11036,13 +11429,9 @@ export class WebGPURenderer {
11036
11429
  * mistake, and `setProbeGrid` says it about the array being replaced.
11037
11430
  */
11038
11431
  markProbeFilled(layer) {
11039
- if (layer < 0 || layer >= this.probeFilled.length)
11040
- return;
11041
- if (this.probeFilled[layer] === 1)
11042
- return;
11043
- this.probeFilled[layer] = 1;
11044
- this.probeFilledCount++;
11045
- if (this.probeBaked)
11432
+ const was = this.probeBaked;
11433
+ this.probeSweeps?.baked(layer);
11434
+ if (!was && this.probeBaked)
11046
11435
  this.rebuildFlatGroupsForNewRings();
11047
11436
  }
11048
11437
  /**
@@ -11053,6 +11442,7 @@ export class WebGPURenderer {
11053
11442
  */
11054
11443
  allocateProbeArray(layers, device) {
11055
11444
  this.probeArray?.destroy();
11445
+ this.probeTargets?.forgetArray();
11056
11446
  this.probeArray = device.createTexture({
11057
11447
  label: 'probe.array',
11058
11448
  size: [this.probeEdge, this.probeEdge, layers],
@@ -11062,8 +11452,6 @@ export class WebGPURenderer {
11062
11452
  usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
11063
11453
  });
11064
11454
  this.probeView = this.probeArray.createView({ dimension: '2d-array' });
11065
- this.probeFilled = new Uint8Array(layers);
11066
- this.probeFilledCount = 0;
11067
11455
  /*
11068
11456
  * **Cleared here, because a texture no pass has written holds undefined contents** — the same
11069
11457
  * sentence the directional shadow maps are cleared under, arrived at from the other end.
@@ -11120,8 +11508,14 @@ export class WebGPURenderer {
11120
11508
  if (current !== null && this.probeArray !== null && sameGrid(current, grid))
11121
11509
  return true;
11122
11510
  this.probes = grid;
11123
- if (this.probeArray === null || this.probeArray.depthOrArrayLayers !== grid.layers) {
11124
- this.allocateProbeArray(grid.layers, this.surface.device);
11511
+ /* A traced grid writes its layers itself, so it keeps one set. See `ProbeGridOptions.crossfade`. */
11512
+ const crossfade = grid.crossfade && !this.quality.indirectLight;
11513
+ const sets = crossfade ? 3 : 1;
11514
+ if (this.probeArray === null ||
11515
+ this.probeArray.depthOrArrayLayers !== grid.layers * sets ||
11516
+ this.probeSweeps?.sets !== sets) {
11517
+ this.probeSweeps = new ProbeSweeps(grid.layers, crossfade);
11518
+ this.allocateProbeArray(grid.layers * sets, this.surface.device);
11125
11519
  this.rebuildFlatGroupsForNewRings();
11126
11520
  }
11127
11521
  return true;
@@ -11183,39 +11577,18 @@ export class WebGPURenderer {
11183
11577
  camera.far = PROBE_FAR_M;
11184
11578
  camera.roll = 0;
11185
11579
  const { device } = this.surface;
11186
- const depth = device.createTexture({
11187
- label: 'probe.depth',
11188
- size: [this.probeSize, this.probeSize],
11189
- format: DEPTH_FORMAT,
11190
- /*
11191
- * **The world's sample count, because a bake draws the world with the world's pipelines.**
11192
- * A pipeline whose count disagrees with its attachment is rejected at `finish` and takes
11193
- * the whole bake with it, and WebGPU requires every attachment in a pass to agree — so a
11194
- * single-sample depth beside a four-sample colour is refused in words that read as a depth
11195
- * problem and are a multisampling one.
11196
- */
11197
- sampleCount: this.samples,
11198
- usage: 0x10, // RENDER_ATTACHMENT
11199
- });
11200
- const depthView = depth.createView();
11580
+ const targets = this.probeTargets;
11581
+ if (targets === null)
11582
+ return false;
11201
11583
  /*
11202
- * The multisampled twin the faces are drawn into, resolved into the cube.
11203
- *
11204
- * **A cube face cannot be both multisampled and sampled**, which is the same bind the mirror
11205
- * is in and it is solved the same way: render into this, name the face as `resolveTarget`,
11206
- * and the averaging happens on the way out. One twin for all six faces, because they are
11207
- * drawn one at a time and nothing reads it between them.
11584
+ * The depth, at the world's sample count because a bake draws the world with the world's
11585
+ * pipelines: a pipeline whose count disagrees with its attachment is rejected at `finish` and
11586
+ * takes the whole bake with it, in words that read as a depth problem and are a multisampling
11587
+ * one. And the multisampled twin the faces are drawn into and resolved from, since **a cube
11588
+ * face cannot be both multisampled and sampled** — the same bind the mirror is in. Both kept
11589
+ * between bakes: see `probeTargets.ts`.
11208
11590
  */
11209
- const colorMsaa = this.samples > 1
11210
- ? device.createTexture({
11211
- label: 'probe.colorMsaa',
11212
- size: [this.probeSize, this.probeSize],
11213
- format: this.pipelines.format,
11214
- sampleCount: this.samples,
11215
- usage: 0x10, // RENDER_ATTACHMENT
11216
- })
11217
- : null;
11218
- const colorMsaaView = colorMsaa?.createView() ?? null;
11591
+ const { depth: depthView, colorMsaa: colorMsaaView } = targets.attachments(this.samples, this.pipelines.format);
11219
11592
  /*
11220
11593
  * The frame's own pass is set aside and restored, because `drawFace` calls straight back into
11221
11594
  * `bindMeshPass` and `drawMesh`, which record into whatever `this.pass` is. That is the same
@@ -11234,16 +11607,26 @@ export class WebGPURenderer {
11234
11607
  if (this.quality.frameGraph)
11235
11608
  this.flushGraph();
11236
11609
  const savedEncoder = this.encoder;
11610
+ /*
11611
+ * **The rings are marked here and given back after the submit below**, because this bake is its
11612
+ * own encoder: once it is submitted nothing in the frame can read its slots, and a grid baked in
11613
+ * one call would otherwise spend the frame's whole ring on probes and skip the draws past it.
11614
+ */
11615
+ this.markRings();
11616
+ const skinMark = this.skinPalettes.mark();
11617
+ const frameEpoch = this.encoderEpoch;
11618
+ this.encoderEpoch = ++this.encoderEpochs;
11237
11619
  const encoder = device.createCommandEncoder({ label: 'probe' });
11238
11620
  this.encoder = encoder;
11239
11621
  /*
11240
- * Fenced before the first face and released after the last, with the group rebuilt on both
11241
- * edges so the cube is not bound while it is being written. Two `createBindGroup` calls per
11242
- * bake, which is not a frame path: a bake is six passes and a mip chain, asked for by a
11243
- * scene at a moment of its choosing.
11622
+ * Fenced before the first face and released after the last, so the grid is not bound while a
11623
+ * plain bake is drawn. **Rebuilt only where that changes what the group binds**: a bounce bake
11624
+ * of a whole grid binds it exactly as the frame does, and it is the bake a scene repeats every
11625
+ * frame, a few faces at a time. See `rebindEnvironment`.
11244
11626
  */
11627
+ this.probeBounce = options?.bounce ?? false;
11245
11628
  this.probePassActive = true;
11246
- this.rebuildFlatBindGroup();
11629
+ this.rebindEnvironment();
11247
11630
  /*
11248
11631
  * **A probe stores radiance, not display pixels.** See `renderer.ts` for the whole argument:
11249
11632
  * baking through the output transform crushes every highlight with the tone curve before it
@@ -11255,7 +11638,9 @@ export class WebGPURenderer {
11255
11638
  this.perFrameInts[this.materialField('uOutputTransform')] = 0;
11256
11639
  this.perFrameFloats[this.materialField('uOutputExposure')] = 1;
11257
11640
  this.materials.dirty();
11258
- for (let face = 0; face < PROBE_FACES.length; face++) {
11641
+ /* Only the faces asked for; the chain waits for the sixth. See `ProbeBakeOptions.faces`. */
11642
+ const [firstFace, endFace] = probeFaceRange(options);
11643
+ for (let face = firstFace; face < endFace; face++) {
11259
11644
  const aim = PROBE_FACES[face];
11260
11645
  if (aim === undefined)
11261
11646
  continue;
@@ -11266,13 +11651,7 @@ export class WebGPURenderer {
11266
11651
  * face and shows up as a reflection that slides at the wrong rate across a curved surface.
11267
11652
  */
11268
11653
  camera.updateMatrices(1);
11269
- const faceView = probe.createView({
11270
- dimension: '2d',
11271
- baseArrayLayer: face,
11272
- arrayLayerCount: 1,
11273
- baseMipLevel: 0,
11274
- mipLevelCount: 1,
11275
- });
11654
+ const faceView = targets.faceViews[face];
11276
11655
  this.pass = encoder.beginRenderPass({
11277
11656
  label: `probe.face${face}`,
11278
11657
  colorAttachments: [
@@ -11318,19 +11697,23 @@ export class WebGPURenderer {
11318
11697
  this.pass.end();
11319
11698
  }
11320
11699
  this.pass = null;
11321
- this.buildProbeChain(encoder, probe, layer);
11700
+ const whole = endFace === PROBE_FACES.length;
11701
+ if (whole)
11702
+ this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
11322
11703
  this.flushRings();
11323
11704
  device.queue.submit([encoder.finish()]);
11324
- depth.destroy();
11325
- colorMsaa?.destroy();
11705
+ this.rewindRings(skinMark);
11706
+ this.encoderEpoch = frameEpoch;
11326
11707
  this.encoder = savedEncoder;
11327
11708
  this.pass = savedPass;
11328
- this.markProbeFilled(layer);
11709
+ if (whole)
11710
+ this.markProbeFilled(layer);
11329
11711
  this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
11330
11712
  this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
11331
11713
  this.materials.dirty();
11332
11714
  this.probePassActive = false;
11333
- this.rebuildFlatBindGroup();
11715
+ this.probeBounce = false;
11716
+ this.rebindEnvironment();
11334
11717
  return true;
11335
11718
  }
11336
11719
  /**
@@ -11457,6 +11840,77 @@ export class WebGPURenderer {
11457
11840
  this.cookieTiles = tiles;
11458
11841
  this.rebuildFlatBindGroup();
11459
11842
  }
11843
+ /**
11844
+ * Sum a scene's many fixed lights into a DriftLight field. The same contract as
11845
+ * `Renderer.createLightField`; see `driftLight/lightField.ts`.
11846
+ */
11847
+ createLightField(lights, options = {}) {
11848
+ this.forgetLightFieldVolumes();
11849
+ const field = new LightField(lights, { ...options, falloff: this.quality.pointLightFalloff });
11850
+ this.lightField = field;
11851
+ return field;
11852
+ }
11853
+ /** Let go of the field and its volumes; the scene's lights are all exact again. */
11854
+ disposeLightField() {
11855
+ this.forgetLightFieldVolumes();
11856
+ this.lightField = null;
11857
+ }
11858
+ forgetLightFieldVolumes() {
11859
+ const bound = this.driftAtlas !== null;
11860
+ this.driftIndex?.destroy();
11861
+ this.driftAtlas?.destroy();
11862
+ this.driftIndex = null;
11863
+ this.driftAtlas = null;
11864
+ this.driftIndexView = null;
11865
+ this.driftAtlasView = null;
11866
+ this.bakeLightField = null;
11867
+ /* The groups hold the destroyed volumes, and a group holding a destroyed texture fails. */
11868
+ if (bound && !this.surface.lost)
11869
+ this.rebuildFlatBindGroup();
11870
+ }
11871
+ /**
11872
+ * Upload the field's two volumes, once, when its last brick has baked, and rebuild the groups
11873
+ * that bind them: nothing samples them before that, so uploading as bricks land would be work
11874
+ * for no picture, and a rebuild per brick would be a bind group per brick.
11875
+ */
11876
+ uploadLightField() {
11877
+ const field = this.lightField;
11878
+ if (field === null || this.driftAtlas !== null || !field.ready || field.layout.count === 0) {
11879
+ return;
11880
+ }
11881
+ if (this.surface.lost)
11882
+ return;
11883
+ const { device } = this.surface;
11884
+ const [ix, iy, iz] = field.layout.dims;
11885
+ const index = device.createTexture({
11886
+ label: 'driftLight.index',
11887
+ size: [ix, iy, iz],
11888
+ dimension: '3d',
11889
+ format: 'r32uint',
11890
+ usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
11891
+ });
11892
+ device.queue.writeTexture({ texture: index }, field.layout.index, { bytesPerRow: ix * 4, rowsPerImage: iy }, [ix, iy, iz]);
11893
+ const [aw, ah, ad] = field.atlasSize;
11894
+ const atlas = device.createTexture({
11895
+ label: 'driftLight.atlas',
11896
+ size: [aw, ah, ad],
11897
+ dimension: '3d',
11898
+ format: 'rgba16float',
11899
+ usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
11900
+ });
11901
+ device.queue.writeTexture({ texture: atlas }, field.atlas, { bytesPerRow: aw * 8, rowsPerImage: ah }, [aw, ah, ad]);
11902
+ this.driftIndex = index;
11903
+ this.driftAtlas = atlas;
11904
+ this.driftIndexView = index.createView({ dimension: '3d' });
11905
+ this.driftAtlasView = atlas.createView({ dimension: '3d' });
11906
+ this.bakeLightField = {
11907
+ index: this.driftIndexView,
11908
+ atlas: this.driftAtlasView,
11909
+ light: this.bakeDriftUniforms.light,
11910
+ origin: this.bakeDriftUniforms.origin,
11911
+ };
11912
+ this.rebuildFlatBindGroup();
11913
+ }
11460
11914
  setIesProfiles(profiles) {
11461
11915
  if (this.surface.lost)
11462
11916
  return;
@@ -11500,6 +11954,9 @@ export class WebGPURenderer {
11500
11954
  /** Each level from the one above it, six faces at a time. See `probePass.ts`. */
11501
11955
  buildProbeChain(encoder, probe, layer) {
11502
11956
  const { device } = this.surface;
11957
+ const targets = this.probeTargets;
11958
+ if (targets === null)
11959
+ return;
11503
11960
  /*
11504
11961
  * **Reset and flushed here, because a bake is not a frame.**
11505
11962
  *
@@ -11523,12 +11980,8 @@ export class WebGPURenderer {
11523
11980
  const pipeline = probeMipPipeline(this.pipelines, device, this.probeMipLayout, this.pipelines.format);
11524
11981
  const levels = probeLevels(this.probeSize);
11525
11982
  for (let level = 1; level < levels; level++) {
11526
- /* A view of the whole cube at the level above, so the blur can cross a face edge. */
11527
- const source = probe.createView({
11528
- dimension: 'cube',
11529
- baseMipLevel: level - 1,
11530
- mipLevelCount: 1,
11531
- });
11983
+ /* The whole cube at the level above, so the blur can cross a face edge, in a group kept. */
11984
+ const group = targets.chainGroup(this.probeMipLayout, this.probeMipUniforms.buffer, this.probeSampler, level);
11532
11985
  for (let face = 0; face < PROBE_FACE_BASIS.length; face++) {
11533
11986
  const basis = PROBE_FACE_BASIS[face];
11534
11987
  if (basis === undefined)
@@ -11544,21 +11997,15 @@ export class WebGPURenderer {
11544
11997
  this.probeMipUniforms.flush();
11545
11998
  return;
11546
11999
  }
11547
- this.probeMipUniforms.writeFloats(slot, 0, [...basis.basisX, 0]);
11548
- this.probeMipUniforms.writeFloats(slot, 16, [...basis.basisY, 0]);
11549
- this.probeMipUniforms.writeFloats(slot, 32, [...basis.forward, 0]);
11550
- this.probeMipUniforms.writeFloats(slot, 48, [0, 0, 0, 0]);
12000
+ this.probeMipUniforms.writeFloats(slot, 0, padded(basis.basisX));
12001
+ this.probeMipUniforms.writeFloats(slot, 16, padded(basis.basisY));
12002
+ this.probeMipUniforms.writeFloats(slot, 32, padded(basis.forward));
12003
+ this.probeMipUniforms.writeFloats(slot, 48, ZERO4);
11551
12004
  const pass = encoder.beginRenderPass({
11552
12005
  label: `probe.mip${level}.face${face}`,
11553
12006
  colorAttachments: [
11554
12007
  {
11555
- view: probe.createView({
11556
- dimension: '2d',
11557
- baseArrayLayer: face,
11558
- arrayLayerCount: 1,
11559
- baseMipLevel: level,
11560
- mipLevelCount: 1,
11561
- }),
12008
+ view: targets.chainTarget(level, face),
11562
12009
  loadOp: 'clear',
11563
12010
  storeOp: 'store',
11564
12011
  clearValue: { r: 0, g: 0, b: 0, a: 1 },
@@ -11566,17 +12013,7 @@ export class WebGPURenderer {
11566
12013
  ],
11567
12014
  });
11568
12015
  pass.setPipeline(pipeline);
11569
- pass.setBindGroup(0, device.createBindGroup({
11570
- layout: this.probeMipLayout,
11571
- entries: [
11572
- {
11573
- binding: 0,
11574
- resource: { buffer: this.probeMipUniforms.buffer, size: PROBE_MIP_UNIFORM_SIZE },
11575
- },
11576
- { binding: 1, resource: source },
11577
- { binding: 2, resource: this.probeSampler },
11578
- ],
11579
- }), [slot]);
12016
+ pass.setBindGroup(0, group, [slot]);
11580
12017
  pass.draw(3);
11581
12018
  pass.end();
11582
12019
  }
@@ -11597,8 +12034,8 @@ export class WebGPURenderer {
11597
12034
  const prefilterTarget = this.probeArray;
11598
12035
  if (prefilterTarget !== null) {
11599
12036
  const prefilter = probePrefilterPipeline(this.pipelines, device, this.probePrefilterLayout, this.pipelines.format);
11600
- /* The whole chain, so a wide sample can select a level from it. */
11601
- const source = probe.createView({ dimension: 'cube' });
12037
+ /* The whole chain, so a wide sample can select a level from it, in a group kept. */
12038
+ const group = targets.prefilterGroupFor(this.probePrefilterLayout, this.probeMipUniforms.buffer, this.probeSampler);
11602
12039
  const offsets = PREFILTER_UNIFORM_OFFSETS;
11603
12040
  const topLevel = levels - 1;
11604
12041
  const samples = this.quality.environmentPrefilterSamples;
@@ -11606,12 +12043,18 @@ export class WebGPURenderer {
11606
12043
  const irradianceLevel = irradianceLevelFor(edge);
11607
12044
  const ggxTop = ggxMaxLevelFor(edge);
11608
12045
  const boxChain = !this.quality.environmentPrefilter;
12046
+ /*
12047
+ * **The diffuse level of a layer the trace has written is the trace's**, so a rasterised bake
12048
+ * stops one level short of it. See `ProbeBaker.traced`: two writers of one level was light
12049
+ * sweeping down a courtyard a probe at a time with the clock paused.
12050
+ */
12051
+ const lastLevel = this.probeBaker?.traced(layer) === true ? irradianceLevel - 1 : irradianceLevel;
11609
12052
  /*
11610
12053
  * **One draw a level, where the cube needed six.** An octahedral map has no faces, so the
11611
12054
  * six-way loop is gone rather than moved, and a probe's convolution is six times fewer
11612
12055
  * passes than the cube it replaces.
11613
12056
  */
11614
- for (let level = 0; level <= irradianceLevel; level++) {
12057
+ for (let level = 0; level <= lastLevel; level++) {
11615
12058
  const irradiance = level === irradianceLevel;
11616
12059
  const roughness = irradiance ? 1 : roughnessForLevel(level, ggxTop);
11617
12060
  const slot = this.probeMipUniforms.allocate();
@@ -11628,31 +12071,19 @@ export class WebGPURenderer {
11628
12071
  /* Written at the generator's own offsets rather than at literals, because a roughness at
11629
12072
  the wrong offset is a level convolved for some other roughness — which looks like a
11630
12073
  plausible reflection and points at nothing. */
11631
- this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterEdge.offset, [
11632
- Math.max(1, edge >> level),
11633
- ]);
11634
- this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterIrradiance.offset, [
11635
- irradiance ? 1 : 0,
11636
- ]);
11637
- this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterBox.offset, [boxChain ? 1 : 0]);
11638
- this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterLevel.offset, [level]);
11639
- this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterRoughness.offset, [roughness]);
11640
- this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSamples.offset, [samples]);
11641
- this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSourceTexels.offset, [
11642
- this.probeSize,
11643
- ]);
11644
- this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSourceMaxLod.offset, [topLevel]);
12074
+ this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterEdge.offset, one(Math.max(1, edge >> level)));
12075
+ this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterIrradiance.offset, one(irradiance ? 1 : 0));
12076
+ this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterBox.offset, one(boxChain ? 1 : 0));
12077
+ this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterLevel.offset, one(level));
12078
+ this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterRoughness.offset, one(roughness));
12079
+ this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSamples.offset, one(samples));
12080
+ this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSourceTexels.offset, one(this.probeSize));
12081
+ this.probeMipUniforms.writeFloats(slot, offsets.uPrefilterSourceMaxLod.offset, one(topLevel));
11645
12082
  const pass = encoder.beginRenderPass({
11646
12083
  label: `probe.prefilter${level}.layer${layer}`,
11647
12084
  colorAttachments: [
11648
12085
  {
11649
- view: prefilterTarget.createView({
11650
- dimension: '2d',
11651
- baseArrayLayer: layer,
11652
- arrayLayerCount: 1,
11653
- baseMipLevel: level,
11654
- mipLevelCount: 1,
11655
- }),
12086
+ view: targets.arrayView(prefilterTarget, layer, level),
11656
12087
  loadOp: 'clear',
11657
12088
  storeOp: 'store',
11658
12089
  clearValue: { r: 0, g: 0, b: 0, a: 1 },
@@ -11660,17 +12091,7 @@ export class WebGPURenderer {
11660
12091
  ],
11661
12092
  });
11662
12093
  pass.setPipeline(prefilter);
11663
- pass.setBindGroup(0, device.createBindGroup({
11664
- layout: this.probePrefilterLayout,
11665
- entries: [
11666
- {
11667
- binding: PREFILTER_UNIFORM_BINDING,
11668
- resource: { buffer: this.probeMipUniforms.buffer, size: PROBE_MIP_UNIFORM_SIZE },
11669
- },
11670
- { binding: PREFILTER_TEXTURE_BINDING, resource: source },
11671
- { binding: PREFILTER_SAMPLER_BINDING, resource: this.probeSampler },
11672
- ],
11673
- }), [slot]);
12094
+ pass.setBindGroup(0, group, [slot]);
11674
12095
  pass.draw(3);
11675
12096
  pass.end();
11676
12097
  }
@@ -12134,7 +12555,7 @@ export class WebGPURenderer {
12134
12555
  f[slot(0) + prefilter('uTexel') + 1] = 1 / Math.max(1, this.frameHeight);
12135
12556
  /* In scene units, which is why an HDR target is what makes a threshold above 1 mean
12136
12557
  anything: against a clamped buffer nothing is ever brighter than white. */
12137
- f[slot(0) + prefilter('uThreshold')] = this.quality.bloomThreshold;
12558
+ f[slot(0) + prefilter('uThreshold')] = this.bloomThresholdSet ?? this.quality.bloomThreshold;
12138
12559
  /* Each downsample's texel is its *source's*, which is the level above it. */
12139
12560
  const down = (name) => this.postField(BLOOM_DOWNSAMPLE_FIELDS, name);
12140
12561
  for (let index = 1; index < levels.length; index++) {
@@ -12210,12 +12631,17 @@ export class WebGPURenderer {
12210
12631
  drainRegistry(this.computes, (definition) => definition.dispose?.(computeDevice));
12211
12632
  this.fieldComposer?.dispose();
12212
12633
  this.fieldComposer = null;
12634
+ this.driftIndex?.destroy();
12635
+ this.driftAtlas?.destroy();
12213
12636
  this.pass = null;
12214
12637
  this.encoder = null;
12215
12638
  this.skinPalettes.dispose();
12216
12639
  this.gpuTimer.dispose();
12217
12640
  this.reflection?.dispose();
12218
12641
  this.reflection = null;
12642
+ this.probeTargets?.dispose();
12643
+ this.exposurePass?.dispose();
12644
+ this.exposurePass = null;
12219
12645
  this.surface.dispose();
12220
12646
  }
12221
12647
  }