@driftengine/core 4.4.1 → 4.5.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 (218) hide show
  1. package/CHANGELOG.json +72 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +5 -1
  4. package/dist/index.js +7 -0
  5. package/dist/render/backend/api.d.ts +17 -11
  6. package/dist/render/backend/webgl2/glassTintPrograms.d.ts +13 -0
  7. package/dist/render/backend/webgl2/glassTintPrograms.js +38 -0
  8. package/dist/render/backend/webgl2/renderer.d.ts +82 -8
  9. package/dist/render/backend/webgl2/renderer.js +373 -105
  10. package/dist/render/backend/webgpu/buffers.d.ts +19 -1
  11. package/dist/render/backend/webgpu/buffers.js +27 -1
  12. package/dist/render/backend/webgpu/depthPass.d.ts +1 -1
  13. package/dist/render/backend/webgpu/depthPass.js +2 -1
  14. package/dist/render/backend/webgpu/flatPass.d.ts +2 -0
  15. package/dist/render/backend/webgpu/flatPass.js +20 -1
  16. package/dist/render/backend/webgpu/glassTintPass.d.ts +28 -0
  17. package/dist/render/backend/webgpu/glassTintPass.js +205 -0
  18. package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +60 -1
  19. package/dist/render/backend/webgpu/gpuDrivenPass.js +305 -5
  20. package/dist/render/backend/webgpu/instanced.d.ts +12 -1
  21. package/dist/render/backend/webgpu/instanced.js +20 -1
  22. package/dist/render/backend/webgpu/latePass.d.ts +26 -0
  23. package/dist/render/backend/webgpu/latePass.js +133 -0
  24. package/dist/render/backend/webgpu/lightVolumePass.js +5 -1
  25. package/dist/render/backend/webgpu/motionPass.d.ts +95 -0
  26. package/dist/render/backend/webgpu/motionPass.js +491 -0
  27. package/dist/render/backend/webgpu/pipelineCache.d.ts +10 -1
  28. package/dist/render/backend/webgpu/pipelineCache.js +11 -1
  29. package/dist/render/backend/webgpu/pointGlassTintPass.d.ts +32 -0
  30. package/dist/render/backend/webgpu/pointGlassTintPass.js +193 -0
  31. package/dist/render/backend/webgpu/pointShadowPass.d.ts +35 -6
  32. package/dist/render/backend/webgpu/pointShadowPass.js +105 -20
  33. package/dist/render/backend/webgpu/postPass.d.ts +21 -15
  34. package/dist/render/backend/webgpu/postPass.js +17 -4
  35. package/dist/render/backend/webgpu/renderer.d.ts +181 -54
  36. package/dist/render/backend/webgpu/renderer.js +1011 -394
  37. package/dist/render/backend/webgpu/scatterPass.js +2 -1
  38. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  39. package/dist/render/backend/webgpu/skinPaletteRing.js +4 -0
  40. package/dist/render/backend/webgpu/snapshotMips.d.ts +29 -0
  41. package/dist/render/backend/webgpu/snapshotMips.js +52 -0
  42. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +5 -0
  43. package/dist/render/backend/webgpu/surfaceTexturePass.js +1 -1
  44. package/dist/render/flameLight.js +15 -2
  45. package/dist/render/glass.d.ts +45 -0
  46. package/dist/render/glass.js +39 -0
  47. package/dist/render/glassCasters.d.ts +40 -0
  48. package/dist/render/glassCasters.js +84 -0
  49. package/dist/render/glassShadow.d.ts +78 -0
  50. package/dist/render/glassShadow.js +125 -0
  51. package/dist/render/globalMediumPass.d.ts +2 -3
  52. package/dist/render/globalMediumPass.js +2 -8
  53. package/dist/render/gpudriven/lit.d.ts +12 -0
  54. package/dist/render/gpudriven/lit.js +23 -0
  55. package/dist/render/gpudriven/materialTable.d.ts +15 -5
  56. package/dist/render/gpudriven/materialTable.js +26 -6
  57. package/dist/render/gpudriven/pipeline.d.ts +1 -1
  58. package/dist/render/gpudriven/pipeline.js +4 -3
  59. package/dist/render/gpudriven/sunGlass.d.ts +35 -0
  60. package/dist/render/gpudriven/sunGlass.js +89 -0
  61. package/dist/render/lightBudget.d.ts +17 -5
  62. package/dist/render/lightBudget.js +17 -5
  63. package/dist/render/pointGlassTint.d.ts +59 -0
  64. package/dist/render/pointGlassTint.js +198 -0
  65. package/dist/render/pointShadowArray.d.ts +56 -3
  66. package/dist/render/pointShadowArray.js +127 -16
  67. package/dist/render/pointShadowBudget.js +51 -41
  68. package/dist/render/pointShadowMap.d.ts +23 -1
  69. package/dist/render/pointShadowMap.js +27 -1
  70. package/dist/render/recon/changeFrames.d.ts +15 -0
  71. package/dist/render/recon/changeFrames.js +21 -0
  72. package/dist/render/recon/disocclusion.d.ts +36 -3
  73. package/dist/render/recon/disocclusion.js +56 -4
  74. package/dist/render/recon/lateRouting.d.ts +14 -0
  75. package/dist/render/recon/lateRouting.js +18 -0
  76. package/dist/render/recon/mover.d.ts +57 -0
  77. package/dist/render/recon/mover.js +86 -0
  78. package/dist/render/recon/resolve.js +95 -12
  79. package/dist/render/renderQuality.d.ts +35 -0
  80. package/dist/render/renderQuality.js +7 -0
  81. package/dist/render/rendererHarness.js +12 -0
  82. package/dist/render/sceneTarget.d.ts +9 -1
  83. package/dist/render/sceneTarget.js +59 -13
  84. package/dist/render/shaders/depth.d.ts +16 -1
  85. package/dist/render/shaders/depth.js +85 -9
  86. package/dist/render/shaders/flat/directionalShadow.js +91 -6
  87. package/dist/render/shaders/flat/glassTint.d.ts +6 -0
  88. package/dist/render/shaders/flat/glassTint.js +25 -0
  89. package/dist/render/shaders/flat/index.d.ts +13 -0
  90. package/dist/render/shaders/flat/index.js +4 -0
  91. package/dist/render/shaders/flat/lampGlass.d.ts +1 -0
  92. package/dist/render/shaders/flat/lampGlass.js +183 -0
  93. package/dist/render/shaders/flat/main.d.ts +1 -1
  94. package/dist/render/shaders/flat/main.js +115 -15
  95. package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
  96. package/dist/render/shaders/flat/pointShadow.js +2 -0
  97. package/dist/render/shaders/flat/preamble.js +21 -6
  98. package/dist/render/shaders/generated/depth.wgsl.d.ts +227 -4
  99. package/dist/render/shaders/generated/depth.wgsl.js +227 -4
  100. package/dist/render/shaders/generated/flat.wgsl.d.ts +360 -312
  101. package/dist/render/shaders/generated/flat.wgsl.js +376 -328
  102. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +3 -13
  103. package/dist/render/shaders/generated/globalMedium.wgsl.js +3 -13
  104. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +3 -13
  105. package/dist/render/shaders/generated/lightVolume.wgsl.js +4 -14
  106. package/dist/render/shaders/generated/octahedralResolve.wgsl.d.ts +29 -0
  107. package/dist/render/shaders/generated/octahedralResolve.wgsl.js +29 -0
  108. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +31 -6
  109. package/dist/render/shaders/generated/temporalResolve.wgsl.js +31 -6
  110. package/dist/render/shaders/globalMedium.js +6 -6
  111. package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +7 -0
  112. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +86 -1
  113. package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +1 -1
  114. package/dist/render/shaders/gpudriven/cull.wgsl.js +11 -2
  115. package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +1 -1
  116. package/dist/render/shaders/gpudriven/materialTable.wgsl.js +3 -1
  117. package/dist/render/shaders/gpudriven/shade.wgsl.js +50 -1
  118. package/dist/render/shaders/gpudriven/sunGlass.wgsl.d.ts +9 -0
  119. package/dist/render/shaders/gpudriven/sunGlass.wgsl.js +133 -0
  120. package/dist/render/shaders/lightVolume.js +6 -6
  121. package/dist/render/shaders/octahedralResolve.d.ts +8 -0
  122. package/dist/render/shaders/octahedralResolve.js +48 -17
  123. package/dist/render/shaders/recon/lateDepth.wgsl.d.ts +1 -0
  124. package/dist/render/shaders/recon/lateDepth.wgsl.js +44 -0
  125. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  126. package/dist/render/shaders/recon/motion.wgsl.js +84 -0
  127. package/dist/render/shaders/recon/resolve.wgsl.d.ts +1 -1
  128. package/dist/render/shaders/recon/resolve.wgsl.js +112 -8
  129. package/dist/render/shaders/temporalResolve.js +133 -5
  130. package/dist/render/shadowCasters.d.ts +20 -1
  131. package/dist/render/shadowCasters.js +15 -1
  132. package/dist/render/shadowMap.d.ts +54 -5
  133. package/dist/render/shadowMap.js +190 -36
  134. package/dist/render/sunGlassTint.d.ts +25 -0
  135. package/dist/render/sunGlassTint.js +79 -0
  136. package/dist/render/temporalAa.d.ts +145 -0
  137. package/dist/render/temporalAa.js +190 -0
  138. package/dist/render/temporalPass.d.ts +16 -14
  139. package/dist/render/temporalPass.js +59 -14
  140. package/dist/render/translucentQueue.d.ts +6 -0
  141. package/dist/render/translucentQueue.js +15 -1
  142. package/package.json +6 -6
  143. package/src/index.ts +10 -0
  144. package/src/render/backend/api.ts +15 -9
  145. package/src/render/backend/webgl2/glassTintPrograms.ts +58 -0
  146. package/src/render/backend/webgl2/renderer.ts +422 -109
  147. package/src/render/backend/webgpu/buffers.ts +50 -2
  148. package/src/render/backend/webgpu/depthPass.ts +2 -1
  149. package/src/render/backend/webgpu/flatPass.ts +22 -1
  150. package/src/render/backend/webgpu/glassTintPass.ts +244 -0
  151. package/src/render/backend/webgpu/gpuDrivenPass.ts +349 -5
  152. package/src/render/backend/webgpu/instanced.ts +21 -1
  153. package/src/render/backend/webgpu/latePass.ts +152 -0
  154. package/src/render/backend/webgpu/lightVolumePass.ts +5 -1
  155. package/src/render/backend/webgpu/motionPass.ts +535 -0
  156. package/src/render/backend/webgpu/pipelineCache.ts +12 -1
  157. package/src/render/backend/webgpu/pointGlassTintPass.ts +233 -0
  158. package/src/render/backend/webgpu/pointShadowPass.ts +158 -26
  159. package/src/render/backend/webgpu/postPass.ts +34 -4
  160. package/src/render/backend/webgpu/renderer.ts +1178 -425
  161. package/src/render/backend/webgpu/scatterPass.ts +2 -1
  162. package/src/render/backend/webgpu/skinPaletteRing.ts +4 -0
  163. package/src/render/backend/webgpu/snapshotMips.ts +80 -0
  164. package/src/render/backend/webgpu/surfaceTexturePass.ts +1 -1
  165. package/src/render/flameLight.ts +15 -2
  166. package/src/render/glass.ts +74 -0
  167. package/src/render/glassCasters.ts +164 -0
  168. package/src/render/glassShadow.ts +153 -0
  169. package/src/render/globalMediumPass.ts +4 -11
  170. package/src/render/gpudriven/lit.ts +31 -0
  171. package/src/render/gpudriven/materialTable.ts +28 -6
  172. package/src/render/gpudriven/pipeline.ts +4 -3
  173. package/src/render/gpudriven/sunGlass.ts +117 -0
  174. package/src/render/lightBudget.ts +19 -5
  175. package/src/render/pointGlassTint.ts +250 -0
  176. package/src/render/pointShadowArray.ts +188 -22
  177. package/src/render/pointShadowBudget.ts +49 -39
  178. package/src/render/pointShadowMap.ts +41 -0
  179. package/src/render/recon/changeFrames.ts +24 -0
  180. package/src/render/recon/disocclusion.ts +54 -3
  181. package/src/render/recon/lateRouting.ts +29 -0
  182. package/src/render/recon/mover.ts +118 -0
  183. package/src/render/recon/resolve.ts +137 -10
  184. package/src/render/renderQuality.ts +47 -0
  185. package/src/render/rendererHarness.ts +12 -0
  186. package/src/render/sceneTarget.ts +66 -12
  187. package/src/render/shaders/depth.ts +109 -9
  188. package/src/render/shaders/flat/directionalShadow.ts +97 -6
  189. package/src/render/shaders/flat/glassTint.ts +25 -0
  190. package/src/render/shaders/flat/index.ts +17 -0
  191. package/src/render/shaders/flat/lampGlass.ts +185 -0
  192. package/src/render/shaders/flat/main.ts +115 -15
  193. package/src/render/shaders/flat/pointShadow.ts +2 -0
  194. package/src/render/shaders/flat/preamble.ts +21 -6
  195. package/src/render/shaders/generated/depth.wgsl.ts +234 -4
  196. package/src/render/shaders/generated/flat.wgsl.ts +376 -328
  197. package/src/render/shaders/generated/globalMedium.wgsl.ts +3 -13
  198. package/src/render/shaders/generated/lightVolume.wgsl.ts +4 -14
  199. package/src/render/shaders/generated/octahedralResolve.wgsl.ts +30 -0
  200. package/src/render/shaders/generated/temporalResolve.wgsl.ts +31 -6
  201. package/src/render/shaders/globalMedium.ts +6 -6
  202. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +88 -1
  203. package/src/render/shaders/gpudriven/cull.wgsl.ts +11 -2
  204. package/src/render/shaders/gpudriven/materialTable.wgsl.ts +3 -1
  205. package/src/render/shaders/gpudriven/shade.wgsl.ts +50 -1
  206. package/src/render/shaders/gpudriven/sunGlass.wgsl.ts +139 -0
  207. package/src/render/shaders/lightVolume.ts +6 -6
  208. package/src/render/shaders/octahedralResolve.ts +50 -17
  209. package/src/render/shaders/recon/lateDepth.wgsl.ts +47 -0
  210. package/src/render/shaders/recon/motion.wgsl.ts +84 -0
  211. package/src/render/shaders/recon/resolve.wgsl.ts +112 -8
  212. package/src/render/shaders/temporalResolve.ts +141 -5
  213. package/src/render/shadowCasters.ts +28 -1
  214. package/src/render/shadowMap.ts +231 -37
  215. package/src/render/sunGlassTint.ts +86 -0
  216. package/src/render/temporalAa.ts +227 -0
  217. package/src/render/temporalPass.ts +88 -12
  218. package/src/render/translucentQueue.ts +20 -1
@@ -2,6 +2,7 @@ import { FrameBudget } from '../budget.js';
2
2
  import { MaterialChanges, ownsMaterial } from '../materialChanges.js';
3
3
  import { mat4 } from 'gl-matrix';
4
4
  import { GL_DEPTH_REMAP, MAX_DEPTH_LAYER, depthClearFor, depthOffsetForLayer, GL_SHADOW_REMAP, REVERSED_DEPTH, SHADOW_DEPTH_CLEAR, glDepthFuncEqual, glShadowDepthFunc, } from '../../depthConvention.js';
5
+ import { resolveGlass } from '../../glass.js';
5
6
  import { PickableSet } from '../../pickable.js';
6
7
  import { TEMPORAL_HISTORY_BLEND, TemporalHistory, jitterOffset, jitterProjection, } from '../../temporalAa.js';
7
8
  import { DecalPass } from '../../decalPass.js';
@@ -51,13 +52,17 @@ import { areaShadowLayerCount, castingRange, createResolvedAreaShadows, firstAre
51
52
  import { equirectToCubeFaces } from '../../equirectToCube.js';
52
53
  import { SKY_FRAG, skyVertFor } from '../../shaders/sky.js';
53
54
  import { DEPTH_CUTOUT_FRAG, DEPTH_CUTOUT_VERT, DEPTH_FRAG, DEPTH_INSTANCED_CUTOUT_VERT, DEPTH_INSTANCED_VERT, DEPTH_SKINNED_VERT, DEPTH_VERT, } from '../../shaders/depth.js';
54
- import { PointShadowArray } from '../../pointShadowArray.js';
55
- import { ShadowMap } from '../../shadowMap.js';
55
+ import { OCTAHEDRAL_EDGE, PointShadowArray, strideLayers } from '../../pointShadowArray.js';
56
+ import { SunShadowArray } from '../../shadowMap.js';
56
57
  import { POINT_SHADOW_SAMPLER, PointShadowSystem } from '../../pointShadowSystem.js';
58
+ import { castsDepth, glassOf } from '../../shadowCasters.js';
59
+ import { GlassCasterList } from '../../glassCasters.js';
60
+ import { SunGlassTint } from '../../sunGlassTint.js';
61
+ import { GlassTintPrograms } from './glassTintPrograms.js';
57
62
  import { FACE_COUNT, PointShadowMap } from '../../pointShadowMap.js';
58
63
  import { createResolvedPointShadows, DEFAULT_SOURCE_RADIUS } from '../../pointShadowImage.js';
59
64
  import { createBakeScratch, runPointShadowBakes, selectCastingLights, } from '../../pointShadowBudget.js';
60
- import { DIRECTIONAL_SHADOW_UNITS, LIVE_POINT_SHADOW_MAPS, MAX_POINT_LIGHTS, NORMAL_TEXTURE_UNIT, ORM_TEXTURE_UNIT, POINT_SHADOW_POOL, POINT_SHADOW_UNITS, CLUSTER_TABLE_TEXTURE_UNIT, EMISSIVE_TEXTURE_UNIT, ENVIRONMENT_TEXTURE_UNIT, SURFACE_TEXTURE_UNIT, bindPointLights, COOKIE_ATLAS_TEXTURE_UNIT, REFRACT_SCENE_TEXTURE_UNIT, DRIFT_LIGHT_ATLAS_TEXTURE_UNIT, DRIFT_LIGHT_INDEX_TEXTURE_UNIT, COOKIE_TILE, IES_ATLAS_TEXTURE_UNIT, bindAreaLights, bindAreaShadows, openCones, } from '../../lightBudget.js';
65
+ import { DIRECTIONAL_SHADOW_UNITS, SUN_GLASS_TINT_TEXTURE_UNIT, POINT_GLASS_TINT_TEXTURE_UNIT, LIVE_POINT_SHADOW_MAPS, MAX_POINT_LIGHTS, NORMAL_TEXTURE_UNIT, ORM_TEXTURE_UNIT, POINT_SHADOW_POOL, POINT_SHADOW_UNITS, CLUSTER_TABLE_TEXTURE_UNIT, EMISSIVE_TEXTURE_UNIT, ENVIRONMENT_TEXTURE_UNIT, SURFACE_TEXTURE_UNIT, bindPointLights, COOKIE_ATLAS_TEXTURE_UNIT, REFRACT_SCENE_TEXTURE_UNIT, DRIFT_LIGHT_ATLAS_TEXTURE_UNIT, DRIFT_LIGHT_INDEX_TEXTURE_UNIT, COOKIE_TILE, IES_ATLAS_TEXTURE_UNIT, bindAreaLights, bindAreaShadows, openCones, } from '../../lightBudget.js';
61
66
  import { resolveRenderQuality } from '../../renderQuality.js';
62
67
  import { drawingBufferSize } from '../../drawingBuffer.js';
63
68
  import { Mesh, createMeshIncremental } from '../../mesh.js';
@@ -488,6 +493,8 @@ export class WebGL2Renderer {
488
493
  /** Whether a palette is currently chosen. Distinct from whether one was ever uploaded. */
489
494
  skinPaletteSet = false;
490
495
  emptyTexture2D;
496
+ /** One glass resolution reused by every draw, so a pane allocates nothing. See `glass.ts`. */
497
+ glassScratch = { transmission: 0, frost: 0, tint: [1, 1, 1] };
491
498
  /** DriftLight's stand-ins while no field is whole: no brick anywhere, and no light. */
492
499
  emptyDriftIndex;
493
500
  emptyDriftAtlas;
@@ -560,6 +567,7 @@ export class WebGL2Renderer {
560
567
  refraction: 0,
561
568
  refractTint: undefined,
562
569
  thicknessM: 0,
570
+ glass: undefined,
563
571
  };
564
572
  motionBlurScale = 1;
565
573
  /** How much of the bloom ceiling this frame takes. See `setBloom`. */
@@ -626,6 +634,9 @@ export class WebGL2Renderer {
626
634
  * Scratch, filled in `endFrame`, which may not allocate.
627
635
  */
628
636
  frameProjection = null;
637
+ /** The temporal resolve's depth-to-metres row, from the inverse of `frameProjection`. */
638
+ taaInvProjection = mat4.create();
639
+ taaDepthToViewZ = Float32Array.of(0, 0, 0, 1);
629
640
  aoInvProjection = mat4.create();
630
641
  aoProjScale = new Float32Array(2);
631
642
  /**
@@ -684,9 +695,8 @@ export class WebGL2Renderer {
684
695
  depthInstancedCutoutUniforms;
685
696
  scatterDepthProgram;
686
697
  scatterDepthUniforms;
687
- shadowMap;
688
- peeledShadowMap;
689
- dynamicShadowMap;
698
+ /** The sun's static, moving and peeled maps, as layers of one array. See `shadowMap.ts`. */
699
+ sunShadows;
690
700
  pointShadows;
691
701
  /**
692
702
  * The layers a world's casting rectangles hold, above the point pool in the same array.
@@ -892,7 +902,7 @@ export class WebGL2Renderer {
892
902
  /** Non-zero while the drawing buffer is pinned; see `lockDrawingBuffer`. */
893
903
  lockedWidth = 0;
894
904
  lockedHeight = 0;
895
- activeShadowMap = null;
905
+ activeSunLayer = null;
896
906
  reflectionPassActive = false;
897
907
  reflectionReadyThisFrame = false;
898
908
  reflectionAtmosphereY = 0;
@@ -914,7 +924,7 @@ export class WebGL2Renderer {
914
924
  *
915
925
  * **Not whether the map exists.** That was the test here, on the belief that a GL texture is
916
926
  * born reading as the far plane; it is born zeroed, and a scene that built the layer and never
917
- * peeled it sampled an occluder at the light. `ShadowMap` now clears itself at birth, so an
927
+ * peeled it sampled an occluder at the light. `SunShadowArray` clears itself at birth, so an
918
928
  * unfilled layer would read as lit either way — this is what keeps the two backends one
919
929
  * decision, and a frame that does not peel from paying a fetch per tap for nothing.
920
930
  */
@@ -940,8 +950,34 @@ export class WebGL2Renderer {
940
950
  * Built once at construction, arrow properties closing over `this`: a sink created
941
951
  * per pass would allocate six objects per light per frame.
942
952
  */
953
+ /** What the sun's light keeps through glass, made the first time glass casts: `sunGlassTint.ts`. */
954
+ sunGlassTint = null;
955
+ /** The five programs a pane casts its colour through, compiled the first time glass casts. */
956
+ glassTintPrograms = null;
957
+ /** True while the glass list is replayed into the glass depth layer through `casterSink`. */
958
+ drawingGlassDepth = false;
959
+ /** How many times a shadow array grew its glass layers: once per family, and never again. */
960
+ sunGlassRebuilds = 0;
961
+ /** The light a tint pass is drawn for, and one pane's colour and clarity: uploaded per entry. */
962
+ glassLight = new Float32Array(4);
963
+ glassPane = new Float32Array(4);
964
+ /** The glass the open shadow pass was offered, kept for its own passes: `glassCasters.ts`. */
965
+ glassCasters = new GlassCasterList();
966
+ /** One resolution every glass caster is read through, so recording one allocates nothing. */
967
+ glassCasterScratch = {
968
+ transmission: 0,
969
+ frost: 0,
970
+ tint: [1, 1, 1],
971
+ };
943
972
  casterSink = {
944
973
  mesh: (mesh, model, material) => {
974
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
975
+ /* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
976
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
977
+ this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
978
+ }
979
+ return;
980
+ }
945
981
  this.shadowDrawBudget.ask();
946
982
  const { gl } = this;
947
983
  /* A two-sided caster casts from whichever face the light sees: nothing culled. */
@@ -981,6 +1017,12 @@ export class WebGL2Renderer {
981
1017
  * columns the batch's own buffer supplies.
982
1018
  */
983
1019
  instanced: (batch, data, material) => {
1020
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
1021
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1022
+ this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
1023
+ }
1024
+ return;
1025
+ }
984
1026
  const { gl } = this;
985
1027
  const gpuBatch = batch;
986
1028
  const count = Math.min(data.count, gpuBatch.capacity);
@@ -1012,7 +1054,13 @@ export class WebGL2Renderer {
1012
1054
  gl.enable(gl.CULL_FACE);
1013
1055
  gl.useProgram(this.depthProgram);
1014
1056
  },
1015
- skinnedMesh: (mesh, model, palette) => {
1057
+ skinnedMesh: (mesh, model, palette, material) => {
1058
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
1059
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1060
+ this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
1061
+ }
1062
+ return;
1063
+ }
1016
1064
  this.shadowDrawBudget.ask();
1017
1065
  const { gl } = this;
1018
1066
  const u = this.depthSkinnedUniforms;
@@ -1095,8 +1143,52 @@ export class WebGL2Renderer {
1095
1143
  gl.useProgram(this.depthProgram);
1096
1144
  gl.uniform1i(this.depthUniforms['uPeelShadowLayer'] ?? null, 0);
1097
1145
  gl.uniformMatrix4fv(this.depthUniforms['uLightViewProj'] ?? null, false, faceViewProj);
1146
+ /* A face is an enumeration of its own: its glass is its own. */
1147
+ this.glassCasters.clear();
1098
1148
  casters(this.casterSink);
1149
+ /*
1150
+ * The first face to offer glass interleaves the array, before its opaque depth is resolved:
1151
+ * `PointShadowArray.growForGlass`. Nothing baked before this saw glass, so nothing re-bakes.
1152
+ */
1153
+ if (this.glassCasters.count > 0 && this.quality.glassShadows !== 'off') {
1154
+ const tintEdge = this.quality.glassShadows === 'half' ? OCTAHEDRAL_EDGE >> 1 : OCTAHEDRAL_EDGE;
1155
+ this.pointShadowArray?.growForGlass(gl, tintEdge);
1156
+ }
1099
1157
  };
1158
+ /**
1159
+ * A lamp's glass, after each face's opaque depth: `PointShadowMap.bake` asks and binds, this
1160
+ * draws, through the same sinks the sun's glass takes and in the face's own matrix.
1161
+ */
1162
+ pointGlassDraw = {
1163
+ offered: () => this.glassCasters.count > 0,
1164
+ draw: (pass, x, y, z) => {
1165
+ const { gl } = this;
1166
+ if (pass === 'depth') {
1167
+ /* Nothing on the unit the depth programs declare a previous map on: see `flushSunGlass`. */
1168
+ gl.activeTexture(gl.TEXTURE0);
1169
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
1170
+ this.activeDepthPeel = false;
1171
+ gl.useProgram(this.depthProgram);
1172
+ gl.uniform1i(this.depthUniforms['uPeelShadowLayer'] ?? null, 0);
1173
+ gl.uniformMatrix4fv(this.depthUniforms['uLightViewProj'] ?? null, false, this.activeDepthViewProj ?? IDENTITY_MAT4);
1174
+ this.drawingGlassDepth = true;
1175
+ this.glassCasters.replay(this.glassDepthSink);
1176
+ this.drawingGlassDepth = false;
1177
+ return;
1178
+ }
1179
+ this.glassTintPrograms ??= new GlassTintPrograms(gl);
1180
+ /* A place, w 1: the tint takes each pane's angle to the lamp from its own position. */
1181
+ this.glassLight[0] = x;
1182
+ this.glassLight[1] = y;
1183
+ this.glassLight[2] = z;
1184
+ this.glassLight[3] = 1;
1185
+ this.glassCasters.replay(this.glassTintSink);
1186
+ },
1187
+ };
1188
+ /** The glass hook a bake is handed: none at all when glass shadows are off. */
1189
+ get pointGlass() {
1190
+ return this.quality.glassShadows === 'off' ? null : this.pointGlassDraw;
1191
+ }
1100
1192
  /**
1101
1193
  * Viewport in CSS pixels rather than device pixels.
1102
1194
  *
@@ -1903,20 +1995,14 @@ export class WebGL2Renderer {
1903
1995
  gl.uniform1i(uniforms['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
1904
1996
  }
1905
1997
  gl.useProgram(null);
1906
- this.shadowMap = this.quality.directionalShadows
1907
- ? new ShadowMap(gl, this.quality.directionalShadowMapSize)
1908
- : null;
1909
- // A second static depth layer preserves both occluders where two world
1910
- // shadows overlap. It is depth-peeled from the first map below.
1911
- this.peeledShadowMap =
1912
- this.quality.directionalShadows && this.quality.directionalShadowDepthLayers > 1
1913
- ? new ShadowMap(gl, this.quality.directionalShadowMapSize)
1914
- : null;
1915
- // Movers get an independent layer. It preserves distance fading when a
1916
- // moving and a static caster overlap: their transmissions multiply rather
1917
- // than one depth replacing the other.
1918
- this.dynamicShadowMap = this.quality.directionalShadows
1919
- ? new ShadowMap(gl, this.quality.directionalShadowMapSize)
1998
+ /*
1999
+ * The static world, its movers and — where more than one static depth layer is asked for —
2000
+ * the peel, as layers of one array. Movers get a layer of their own so a moving and a static
2001
+ * caster that overlap keep both distance fades: their transmissions multiply rather than one
2002
+ * depth replacing the other. The peel keeps the second static occluder the same way.
2003
+ */
2004
+ this.sunShadows = this.quality.directionalShadows
2005
+ ? new SunShadowArray(gl, this.quality.directionalShadowMapSize, this.quality.directionalShadowDepthLayers > 1)
1920
2006
  : null;
1921
2007
  // Static maps are allocated once the world reports its light count. No
1922
2008
  // cubemap resources exist at all when the quality profile disables them.
@@ -2085,9 +2171,9 @@ export class WebGL2Renderer {
2085
2171
  * every one of them, per renderer, for as long as it lived; `renderer.test.ts` counts them.
2086
2172
  */
2087
2173
  this.sceneTarget?.dispose();
2088
- this.shadowMap?.dispose(gl);
2089
- this.peeledShadowMap?.dispose(gl);
2090
- this.dynamicShadowMap?.dispose(gl);
2174
+ this.sunShadows?.dispose(gl);
2175
+ this.sunGlassTint?.dispose(gl);
2176
+ this.glassTintPrograms?.dispose(gl);
2091
2177
  this.planarReflection?.dispose(gl);
2092
2178
  this.pointShadows?.dispose();
2093
2179
  gl.deleteVertexArray(this.panelVao);
@@ -2716,8 +2802,10 @@ export class WebGL2Renderer {
2716
2802
  const lit = options.lit ?? true;
2717
2803
  const fog = options.fog ?? true;
2718
2804
  const toneMapped = options.toneMapped ?? true;
2719
- /* An instanced draw does not refract. See `materialChanges.ts`. */
2720
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting: false });
2805
+ /* A blended batch shows what is behind it — refraction or glass — as a single draw does, and
2806
+ counts a material of its own for it by the rule both backends share (`materialChanges.ts`). */
2807
+ const refracting = blend && this.bindSeeThrough(u, options);
2808
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
2721
2809
  if (own)
2722
2810
  this.materials.dirty();
2723
2811
  this.takeMaterial();
@@ -2738,20 +2826,9 @@ export class WebGL2Renderer {
2738
2826
  * **A null snapshot leaves the strength at zero**, so the draw shades as an ordinary
2739
2827
  * translucent one rather than sampling a black texture and painting the pane the colour of a
2740
2828
  * hole. That is the defined state the two-backends rule asks for instead of a silent no-op.
2829
+ * The copy is bound above, before the material is counted, because whether it was taken is
2830
+ * part of what the count asks.
2741
2831
  */
2742
- const refraction = options.refraction ?? 0;
2743
- let refracting = false;
2744
- if (refraction > 0) {
2745
- const snapshot = this.sceneTarget?.snapshotColor() ?? null;
2746
- if (snapshot !== null) {
2747
- refracting = true;
2748
- gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
2749
- gl.bindTexture(gl.TEXTURE_2D, snapshot);
2750
- gl.uniform1f(u['uRefractStrength'] ?? null, refraction);
2751
- gl.uniform3fv(u['uRefractTint'] ?? null, options.refractTint ?? WHITE_TINT);
2752
- gl.uniform1f(u['uRefractThickness'] ?? null, options.thicknessM ?? 0);
2753
- }
2754
- }
2755
2832
  if (!lit)
2756
2833
  gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
2757
2834
  if (!fog)
@@ -2771,7 +2848,13 @@ export class WebGL2Renderer {
2771
2848
  gl.enable(gl.POLYGON_OFFSET_FILL);
2772
2849
  gl.polygonOffset(offset.slope, offset.units);
2773
2850
  }
2851
+ /* Both sides of a double-sided material, as every mesh path does and WebGPU's pipeline does
2852
+ for both: without it a batch's far faces were culled here alone. */
2853
+ if (this.materialDoubleSided)
2854
+ gl.disable(gl.CULL_FACE);
2774
2855
  mesh.drawInstances(gl, count);
2856
+ if (this.materialDoubleSided)
2857
+ gl.enable(gl.CULL_FACE);
2775
2858
  if (layer > 0) {
2776
2859
  gl.polygonOffset(0, 0);
2777
2860
  gl.disable(gl.POLYGON_OFFSET_FILL);
@@ -2787,11 +2870,8 @@ export class WebGL2Renderer {
2787
2870
  gl.uniform1f(u['uOpacity'] ?? null, 1);
2788
2871
  /* The binding is released with the uniform, which is the 2026-08-27 rule: a WebGL2 binding
2789
2872
  outlives its frame, so a snapshot left on unit 14 is held against the next frame's copy. */
2790
- if (refracting) {
2791
- gl.uniform1f(u['uRefractStrength'] ?? null, 0);
2792
- gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
2793
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
2794
- }
2873
+ if (refracting)
2874
+ this.unbindSeeThrough(u);
2795
2875
  if (!lit)
2796
2876
  gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
2797
2877
  if (!fog)
@@ -2802,6 +2882,45 @@ export class WebGL2Renderer {
2802
2882
  this.materials.dirty();
2803
2883
  this.useFlatProgram();
2804
2884
  }
2885
+ /**
2886
+ * Bind what a translucent draw shows through it: the frame's copy, refraction's bend and absorption,
2887
+ * and glass. Returns whether the copy is bound, and so whether `unbindSeeThrough` owes it back.
2888
+ *
2889
+ * **A glass draw takes the copy even when it does not bend**, since a pane shows what is behind it
2890
+ * either way, and asks for the copy's mip chain, which a frosted pane reads blurred. A null copy
2891
+ * leaves both off, so the pane shades as ordinary translucency rather than sampling black.
2892
+ *
2893
+ * **Refused inside a probe's face or a mirror**, as WebGPU's `takeRefractSnapshot` refuses: the
2894
+ * copy is of the frame drawn to the screen, so a pane there would show the wrong picture, and
2895
+ * taking it mid-bake is how a probe's next face came to be attached to the scene's framebuffer.
2896
+ */
2897
+ bindSeeThrough(u, options) {
2898
+ const { gl } = this;
2899
+ const refraction = options.refraction ?? 0;
2900
+ const glassy = resolveGlass(options.glass, this.glassScratch);
2901
+ if (refraction <= 0 && !glassy)
2902
+ return false;
2903
+ if (this.probePassActive || this.reflectionPassActive)
2904
+ return false;
2905
+ const snapshot = this.sceneTarget?.snapshotColor(false, glassy) ?? null;
2906
+ if (snapshot === null)
2907
+ return false;
2908
+ gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
2909
+ gl.bindTexture(gl.TEXTURE_2D, snapshot);
2910
+ const glass = this.glassScratch;
2911
+ /* Strength, path thickness, transmission, frost: one vector, for the budget the shader states. */
2912
+ gl.uniform4f(u['uSeeThrough'] ?? null, Math.max(refraction, 0), options.thicknessM ?? 0, glass.transmission, glass.frost);
2913
+ gl.uniform3fv(u['uRefractTint'] ?? null, options.refractTint ?? WHITE_TINT);
2914
+ gl.uniform3fv(u['uGlassTint'] ?? null, glass.tint);
2915
+ return true;
2916
+ }
2917
+ /** Put back what `bindSeeThrough` bound: the binding outlives its frame (the 2026-08-27 rule). */
2918
+ unbindSeeThrough(u) {
2919
+ const { gl } = this;
2920
+ gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
2921
+ gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
2922
+ gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
2923
+ }
2805
2924
  disposeMesh(mesh) {
2806
2925
  /*
2807
2926
  * Nothing to release when the context is gone: it took every buffer and VAO with it,
@@ -3335,6 +3454,10 @@ export class WebGL2Renderer {
3335
3454
  */
3336
3455
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.pointShadowArray?.texture ?? this.emptyTexture2DArray);
3337
3456
  gl.uniform1i(uniforms[POINT_SHADOW_SAMPLER] ?? null, firstUnit);
3457
+ /* What the lamps keep through glass: the one-texel stand-in, read as none, until glass casts. */
3458
+ gl.activeTexture(gl.TEXTURE0 + POINT_GLASS_TINT_TEXTURE_UNIT);
3459
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.pointShadowArray?.tintTexture ?? this.emptyTexture2DArray);
3460
+ gl.uniform1i(uniforms['uPointGlassTints'] ?? null, POINT_GLASS_TINT_TEXTURE_UNIT);
3338
3461
  gl.uniform1iv(uniforms['uPointShadowLayer[0]'] ?? null, resolved.layers);
3339
3462
  /* Origin and far plane in one row; see `uPointShadowProjection` in the preamble. */
3340
3463
  gl.uniform4fv(uniforms['uPointShadowProjection[0]'] ?? null, resolved.projections);
@@ -3611,20 +3734,17 @@ export class WebGL2Renderer {
3611
3734
  * emitting, because a stale map must never be sampled.
3612
3735
  */
3613
3736
  beginShadowPass(lightViewProj, layer = 'static') {
3614
- const shadowMap = layer === 'static'
3615
- ? this.shadowMap
3616
- : layer === 'static-peel'
3617
- ? this.peeledShadowMap
3618
- : this.dynamicShadowMap;
3619
- if (shadowMap === null) {
3737
+ const sun = this.sunShadows;
3738
+ if (sun === null || (layer === 'static-peel' && sun.layers < 3)) {
3620
3739
  this.shadowPassActive = false;
3621
- this.activeShadowMap = null;
3740
+ this.activeSunLayer = null;
3622
3741
  return;
3623
3742
  }
3624
3743
  const { gl } = this;
3625
3744
  this.gpuTimer.begin('shadows');
3626
3745
  this.shadowPassActive = true;
3627
- this.activeShadowMap = shadowMap;
3746
+ this.activeSunLayer = layer;
3747
+ this.glassCasters.clear();
3628
3748
  /* The static layer opens first, so a frame's peel counts from here. See `peelFilled`. */
3629
3749
  if (layer === 'static')
3630
3750
  this.peelFilled = false;
@@ -3652,16 +3772,16 @@ export class WebGL2Renderer {
3652
3772
  // The previous mesh pass left all directional maps bound for sampling.
3653
3773
  // A texture cannot be sampled while it is attached to the active depth
3654
3774
  // target, even when the shader branch would skip that sample.
3655
- const targetUnit = layer === 'static' ? 0 : layer === 'static-peel' ? 1 : 2;
3656
- gl.activeTexture(gl.TEXTURE0 + targetUnit);
3657
- gl.bindTexture(gl.TEXTURE_2D, null);
3658
- shadowMap.begin(gl);
3775
+ gl.activeTexture(gl.TEXTURE0);
3776
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3777
+ sun.begin(gl, layer);
3659
3778
  gl.useProgram(this.depthProgram);
3660
3779
  const peel = layer === 'static-peel';
3661
3780
  gl.uniform1i(this.depthUniforms['uPeelShadowLayer'] ?? null, peel ? 1 : 0);
3662
3781
  if (peel) {
3782
+ /* The static layer, read while the peel is drawn aside: see `SunShadowArray`. */
3663
3783
  gl.activeTexture(gl.TEXTURE0);
3664
- gl.bindTexture(gl.TEXTURE_2D, this.shadowMap?.texture ?? null);
3784
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, sun.texture);
3665
3785
  gl.uniform1i(this.depthUniforms['uPreviousShadowMap'] ?? null, 0);
3666
3786
  }
3667
3787
  /* The corrected one, matching `activeDepthViewProj` above: a pass must render its map with the
@@ -3767,21 +3887,158 @@ export class WebGL2Renderer {
3767
3887
  return;
3768
3888
  casters(this.casterSink);
3769
3889
  }
3890
+ /**
3891
+ * Draw the pass's glass twice: where the nearest pane is, and what the panes let through.
3892
+ *
3893
+ * After the opaque casters, in the pass's own light matrix. A layer that has held glass is
3894
+ * redrawn even when this pass offered none, so glass that has gone takes its colour with it. The
3895
+ * first glass grows the array (`SunShadowArray.growForGlass`) and makes the tint target.
3896
+ */
3897
+ flushSunGlass(which) {
3898
+ const sun = this.sunShadows;
3899
+ const list = this.glassCasters;
3900
+ if (sun === null || this.quality.glassShadows === 'off')
3901
+ return;
3902
+ if (list.count === 0 && !sun.hasGlass)
3903
+ return;
3904
+ const { gl } = this;
3905
+ if (list.count > 0 && sun.growForGlass(gl))
3906
+ this.sunGlassRebuilds++;
3907
+ /* Where the nearest pane is: the depth programs, through the sink every opaque caster took. */
3908
+ gl.activeTexture(gl.TEXTURE0);
3909
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3910
+ sun.beginGlass(gl, which);
3911
+ this.activeDepthPeel = false;
3912
+ gl.useProgram(this.depthProgram);
3913
+ gl.uniform1i(this.depthUniforms['uPeelShadowLayer'] ?? null, 0);
3914
+ gl.uniformMatrix4fv(this.depthUniforms['uLightViewProj'] ?? null, false, this.activeDepthViewProj ?? IDENTITY_MAT4);
3915
+ this.drawingGlassDepth = true;
3916
+ list.replay(this.glassDepthSink);
3917
+ this.drawingGlassDepth = false;
3918
+ sun.end(gl);
3919
+ /* What the panes let through, every one on a ray, multiplied. */
3920
+ this.glassTintPrograms ??= new GlassTintPrograms(gl);
3921
+ const size = this.quality.glassShadows === 'half' ? Math.max(1, sun.size >> 1) : sun.size;
3922
+ const tint = (this.sunGlassTint ??= new SunGlassTint(gl, size));
3923
+ /*
3924
+ * The direction the light travels, off the light matrix's depth row: an orthographic
3925
+ * projection's depth grows along it. The sign is immaterial — the tint takes the cosine
3926
+ * absolutely — and w 0 says it is a direction rather than a place.
3927
+ */
3928
+ const m = this.activeDepthViewProj ?? IDENTITY_MAT4;
3929
+ const length = Math.hypot(m[2], m[6], m[10]) || 1;
3930
+ this.glassLight[0] = m[2] / length;
3931
+ this.glassLight[1] = m[6] / length;
3932
+ this.glassLight[2] = m[10] / length;
3933
+ this.glassLight[3] = 0;
3934
+ tint.begin(gl, which === 'static' ? 0 : 1);
3935
+ list.replay(this.glassTintSink);
3936
+ tint.end(gl);
3937
+ gl.useProgram(this.depthProgram);
3938
+ }
3939
+ /** The glass list into the glass depth layer: `casterSink` itself, with glass let through. */
3940
+ glassDepthSink = {
3941
+ mesh: (mesh, model, material) => this.casterSink.mesh(mesh, model, material),
3942
+ instanced: (batch, data, material) => this.casterSink.instanced?.(batch, data, material),
3943
+ skinnedMesh: (mesh, model, palette, material) => this.casterSink.skinnedMesh(mesh, model, palette, material),
3944
+ };
3945
+ /** One pane's tint uniforms, and its cutout where it has one. */
3946
+ bindGlassTint(program, glass, cutout) {
3947
+ const { gl } = this;
3948
+ const u = program.uniforms;
3949
+ gl.useProgram(program.program);
3950
+ gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, this.activeDepthViewProj ?? IDENTITY_MAT4);
3951
+ this.uploadWind(u, this.frameWind);
3952
+ this.glassPane[0] = glass.transmission * glass.tint[0];
3953
+ this.glassPane[1] = glass.transmission * glass.tint[1];
3954
+ this.glassPane[2] = glass.transmission * glass.tint[2];
3955
+ this.glassPane[3] = 1 - glass.frost;
3956
+ gl.uniform4fv(u['uGlassPane'] ?? null, this.glassPane);
3957
+ gl.uniform4fv(u['uGlassLight'] ?? null, this.glassLight);
3958
+ if (cutout !== null) {
3959
+ gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
3960
+ gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
3961
+ cutout.albedo.bind(gl, SURFACE_TEXTURE_UNIT);
3962
+ gl.uniform1i(u['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
3963
+ }
3964
+ }
3965
+ /**
3966
+ * The glass list into the tint layer, through the tint programs, **culling nothing**: a light
3967
+ * crosses a pane whichever way the pane faces, and a closed glass object has two surfaces on a
3968
+ * ray, so every one counts. What that asks of a model is that a pane be one surface — two-sided
3969
+ * glass says so with `doubleSided`, and a second copy of its triangles is a second pane.
3970
+ */
3971
+ glassTintSink = {
3972
+ mesh: (mesh, model, material, glass) => {
3973
+ const programs = this.glassTintPrograms;
3974
+ if (programs === null)
3975
+ return;
3976
+ const { gl } = this;
3977
+ const cutout = cutoutOf(material);
3978
+ const program = cutout !== null ? programs.cutout : programs.rigid;
3979
+ this.bindGlassTint(program, glass, cutout);
3980
+ gl.uniformMatrix4fv(program.uniforms['uModel'] ?? null, false, model);
3981
+ gl.disable(gl.CULL_FACE);
3982
+ mesh.draw(gl);
3983
+ if (this.depthPassCullsFaces)
3984
+ gl.enable(gl.CULL_FACE);
3985
+ if (cutout !== null)
3986
+ this.restoreSurfaceTextureUnit();
3987
+ },
3988
+ instanced: (batch, data, material, glass) => {
3989
+ const programs = this.glassTintPrograms;
3990
+ if (programs === null)
3991
+ return;
3992
+ const { gl } = this;
3993
+ const gpuBatch = batch;
3994
+ const count = Math.min(data.count, gpuBatch.capacity);
3995
+ if (count === 0)
3996
+ return;
3997
+ const cutout = cutoutOf(material);
3998
+ this.bindGlassTint(cutout !== null ? programs.instancedCutout : programs.instanced, glass, cutout);
3999
+ gl.disable(gl.CULL_FACE);
4000
+ gpuBatch.mesh.drawInstances(gl, count);
4001
+ if (this.depthPassCullsFaces)
4002
+ gl.enable(gl.CULL_FACE);
4003
+ if (cutout !== null)
4004
+ this.restoreSurfaceTextureUnit();
4005
+ },
4006
+ skinnedMesh: (mesh, model, palette, _material, glass) => {
4007
+ const programs = this.glassTintPrograms;
4008
+ if (programs === null)
4009
+ return;
4010
+ const { gl } = this;
4011
+ const program = programs.skinned;
4012
+ this.bindGlassTint(program, glass, null);
4013
+ gl.uniformMatrix4fv(program.uniforms['uModel'] ?? null, false, model);
4014
+ this.skinPalette.update(gl, palette);
4015
+ this.skinPalette.bind(gl, SKIN_PALETTE_TEXTURE_UNIT);
4016
+ gl.uniform1i(program.uniforms['uJointPalette'] ?? null, SKIN_PALETTE_TEXTURE_UNIT);
4017
+ gl.disable(gl.CULL_FACE);
4018
+ mesh.draw(gl);
4019
+ if (this.depthPassCullsFaces)
4020
+ gl.enable(gl.CULL_FACE);
4021
+ },
4022
+ };
3770
4023
  endShadowPass() {
3771
4024
  if (this.contextLost)
3772
4025
  return;
3773
- const shadowMap = this.activeShadowMap;
3774
- if (!this.shadowPassActive || shadowMap === null)
4026
+ const sun = this.sunShadows;
4027
+ const layer = this.activeSunLayer;
4028
+ if (!this.shadowPassActive || sun === null || layer === null)
3775
4029
  return;
3776
- shadowMap.end(this.gl);
3777
- if (shadowMap === this.peeledShadowMap)
4030
+ sun.end(this.gl);
4031
+ if (layer === 'static-peel')
3778
4032
  this.peelFilled = true;
4033
+ /* The peel keeps no glass: its layer is the second opaque occluder and nothing else. */
4034
+ else
4035
+ this.flushSunGlass(layer === 'dynamic' ? 'dynamic' : 'static');
3779
4036
  /* Back to the frame's sense; see `beginShadowPass` for why it left it. */
3780
4037
  this.gl.depthFunc(glDepthFuncEqual(this.gl));
3781
4038
  this.gl.clearDepth(this.depthClear);
3782
4039
  this.gpuTimer.end();
3783
4040
  this.shadowPassActive = false;
3784
- this.activeShadowMap = null;
4041
+ this.activeSunLayer = null;
3785
4042
  // Restore the drawing-buffer viewport the shadow pass overrode.
3786
4043
  this.restoreViewport();
3787
4044
  }
@@ -3815,6 +4072,8 @@ export class WebGL2Renderer {
3815
4072
  directionalShadows: this.quality.directionalShadows,
3816
4073
  environmentProbe: this.probeCapture !== null,
3817
4074
  nightEmissive: this.quality.nightEmissive,
4075
+ /* Off compiles none of the glass lookup: see `FlatShaderOptions.glassShadows`. */
4076
+ glassShadows: this.quality.glassShadows !== 'off',
3818
4077
  };
3819
4078
  const build = (budget) => flatFrag({ ...variant, maxLights: budget.maxLights, maxAreaLights: budget.maxAreaLights });
3820
4079
  const vert = flatVert({ skinned: false, morphed: false, instanced: false });
@@ -4078,6 +4337,8 @@ export class WebGL2Renderer {
4078
4337
  */
4079
4338
  runPointShadowBakes(pointShadows, lights, this.quality.pointShadowFacesPerFrame, this.quality.liveShadowFacesPerFrame, this.quality.pointShadowRebakeDistance, FACE_COUNT, this.bakeScratch, staticCasters, dynamicCasters, (map, light, casters, maxFaces) => this.bakeOne(map, light, casters, maxFaces));
4080
4339
  this.updateAreaShadows(areaLights, frameDt, staticCasters, dynamicCasters);
4340
+ /* The chain frost reads, once for the round, for the lights whose glass it drew or cleared. */
4341
+ this.pointShadowArray.finishTints(this.gl);
4081
4342
  this.restoreViewport();
4082
4343
  }
4083
4344
  /**
@@ -4124,6 +4385,10 @@ export class WebGL2Renderer {
4124
4385
  * reason: it already holds everything it needs.
4125
4386
  */
4126
4387
  set.resolve(areaLights, this.resolvedAreaShadows);
4388
+ /* As the array stores them: 2L once glass has interleaved it. See `strideLayers`. */
4389
+ const stride = this.pointShadowArray?.stride ?? 1;
4390
+ strideLayers(this.resolvedAreaShadows.layer, stride);
4391
+ strideLayers(this.resolvedAreaShadows.liveLayer, stride);
4127
4392
  }
4128
4393
  /**
4129
4394
  * Every point-shadow bake, under the one set of conventions all of them need.
@@ -4169,12 +4434,12 @@ export class WebGL2Renderer {
4169
4434
  * map is stale on every frame it is checked — `pointShadowImage.ts` records that exact bug
4170
4435
  * from two readers disagreeing about this argument's default.
4171
4436
  */
4172
- return this.bakePointShadow(drawCasters, () => map.bake(gl, light.x, light.y, light.z, range, this.drawPointShadowFace, near, DEFAULT_SOURCE_RADIUS, maxFaces));
4437
+ return this.bakePointShadow(drawCasters, () => map.bake(gl, light.x, light.y, light.z, range, this.drawPointShadowFace, near, DEFAULT_SOURCE_RADIUS, maxFaces, this.pointGlass));
4173
4438
  }
4174
4439
  /** Spend up to `maxFaces` on this map, returning how many it used. */
4175
4440
  bakeOne(map, light, drawCasters, maxFaces) {
4176
4441
  const { gl } = this;
4177
- return this.bakePointShadow(drawCasters, () => map.bake(gl, light.x, light.y, light.z, light.radius, this.drawPointShadowFace, light.shadowNear, light.sourceRadius, maxFaces));
4442
+ return this.bakePointShadow(drawCasters, () => map.bake(gl, light.x, light.y, light.z, light.radius, this.drawPointShadowFace, light.shadowNear, light.sourceRadius, maxFaces, this.pointGlass));
4178
4443
  }
4179
4444
  /**
4180
4445
  * Draw into a rectangle of the canvas, on its own terms.
@@ -4385,6 +4650,10 @@ export class WebGL2Renderer {
4385
4650
  *
4386
4651
  * What it gives up is one frame of history: the frame after a cut has no blur and no temporal
4387
4652
  * blend, as the first frame of a session has none. Nothing else is reset.
4653
+ *
4654
+ * **A reconstructing renderer depends on it**: its history is the last shot's, and without this
4655
+ * call a cut blends that shot into the first frames of the next. Nothing detects a cut on the
4656
+ * caller's behalf, deliberately — a fast pan and a cut look alike from here.
4388
4657
  */
4389
4658
  cameraCut() {
4390
4659
  this.hasPreviousView = false;
@@ -4943,9 +5212,19 @@ export class WebGL2Renderer {
4943
5212
  * its early exit on it — so this is present every jittered frame rather than absent on some,
4944
5213
  * and the pass still runs and fills the history for the next one.
4945
5214
  */
5215
+ /* The anti-flicker's same-surface test reads depths in metres, as the occlusion does. */
5216
+ if (this.frameProjection !== null) {
5217
+ mat4.invert(this.taaInvProjection, this.frameProjection);
5218
+ this.taaDepthToViewZ[0] = this.taaInvProjection[10] ?? 0;
5219
+ this.taaDepthToViewZ[1] = this.taaInvProjection[14] ?? 0;
5220
+ this.taaDepthToViewZ[2] = this.taaInvProjection[11] ?? 0;
5221
+ this.taaDepthToViewZ[3] = this.taaInvProjection[15] ?? 1;
5222
+ }
4946
5223
  temporal = {
4947
5224
  reprojection: this.reprojection,
4948
5225
  blend: this.temporalHistoryUsable ? TEMPORAL_HISTORY_BLEND : 0,
5226
+ periodStart: this.temporalHistory.periodStart,
5227
+ depthToViewZ: this.taaDepthToViewZ,
4949
5228
  };
4950
5229
  }
4951
5230
  /*
@@ -5090,8 +5369,9 @@ export class WebGL2Renderer {
5090
5369
  * flat paint with the effect on. The opaque frame is finished here, which is what a pane
5091
5370
  * should show; the latch then serves every pane in the replay. Only when something refracts.
5092
5371
  */
5093
- if (this.translucentQueue.refracts)
5094
- this.sceneTarget?.snapshotColor();
5372
+ if (this.translucentQueue.refracts) {
5373
+ this.sceneTarget?.snapshotColor(false, this.translucentQueue.glassy);
5374
+ }
5095
5375
  this.oit.accumulateOit(this.canvas.width, this.canvas.height, oitDepth, (weighted) => {
5096
5376
  this.oitReplaying = true;
5097
5377
  this.useFlatProgram();
@@ -5108,6 +5388,7 @@ export class WebGL2Renderer {
5108
5388
  options.refraction = draw.refraction;
5109
5389
  options.refractTint = draw.refractTint;
5110
5390
  options.thicknessM = draw.thicknessM;
5391
+ options.glass = draw.glass ?? undefined;
5111
5392
  this.drawTranslucentMesh(draw.mesh, draw.model, draw.opacity, options);
5112
5393
  });
5113
5394
  /* Back to zero for every other draw in the frame, exactly as `uOpacity` and `uTint` are. */
@@ -5157,9 +5438,7 @@ export class WebGL2Renderer {
5157
5438
  /* The placeholder rather than null where a map is merely absent, for the reason
5158
5439
  `emptyTexture.ts` gives. It is black, which as a depth is an occluder at the light,
5159
5440
  so it is safe only because `sunShadow` is 0 whenever these maps do not exist. */
5160
- staticShadowMap: this.shadowMap?.texture ?? this.emptyTexture2D,
5161
- peeledShadowMap: this.peeledShadowMap?.texture ?? this.emptyTexture2D,
5162
- dynamicShadowMap: this.dynamicShadowMap?.texture ?? this.emptyTexture2D,
5441
+ sunShadows: this.sunShadows?.texture ?? this.emptyTexture2DArray,
5163
5442
  peeledEnabled: this.peelFilled,
5164
5443
  });
5165
5444
  if (marched) {
@@ -5462,9 +5741,11 @@ export class WebGL2Renderer {
5462
5741
  gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
5463
5742
  gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
5464
5743
  gl.uniform1i(u['uRefractScene'] ?? null, REFRACT_SCENE_TEXTURE_UNIT);
5465
- gl.uniform1f(u['uRefractStrength'] ?? null, 0);
5744
+ /* Neither refracting nor glass, which is every draw that does not say otherwise. See
5745
+ `bindSeeThrough` for the four numbers in the one vector. */
5746
+ gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
5466
5747
  gl.uniform3fv(u['uRefractTint'] ?? null, WHITE_TINT);
5467
- gl.uniform1f(u['uRefractThickness'] ?? null, 0);
5748
+ gl.uniform3fv(u['uGlassTint'] ?? null, WHITE_TINT);
5468
5749
  gl.uniform3fv(u['uDirectionalDir'] ?? null, env.directionalDir);
5469
5750
  gl.uniform3fv(u['uDirectionalColor'] ?? null, env.directionalColor);
5470
5751
  gl.uniform3fv(u['uAmbient'] ?? null, env.ambient);
@@ -5560,7 +5841,7 @@ export class WebGL2Renderer {
5560
5841
  * its own copy of this lookup; the two backends have to project the same way.
5561
5842
  */
5562
5843
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, env.lightViewProj);
5563
- gl.uniform1f(u['uShadowStrength'] ?? null, this.shadowMap === null ? 0 : env.shadowStrength);
5844
+ gl.uniform1f(u['uShadowStrength'] ?? null, this.sunShadows === null ? 0 : env.shadowStrength);
5564
5845
  gl.uniform1f(u['uShadowMapSize'] ?? null, this.quality.directionalShadowMapSize);
5565
5846
  gl.uniform1f(u['uShadowDepthSpan'] ?? null, env.shadowDepthSpan);
5566
5847
  gl.uniform1f(u['uShadowMaxDistance'] ?? null, this.quality.directionalShadowMaxDistance);
@@ -5585,14 +5866,12 @@ export class WebGL2Renderer {
5585
5866
  */
5586
5867
  if (this.quality.directionalShadows) {
5587
5868
  gl.activeTexture(gl.TEXTURE0);
5588
- gl.bindTexture(gl.TEXTURE_2D, this.shadowMap?.texture ?? this.emptyTexture2D);
5589
- gl.uniform1i(u['uStaticShadowMap'] ?? null, 0);
5590
- gl.activeTexture(gl.TEXTURE1);
5591
- gl.bindTexture(gl.TEXTURE_2D, this.peeledShadowMap?.texture ?? this.emptyTexture2D);
5592
- gl.uniform1i(u['uPeeledShadowMap'] ?? null, 1);
5593
- gl.activeTexture(gl.TEXTURE2);
5594
- gl.bindTexture(gl.TEXTURE_2D, this.dynamicShadowMap?.texture ?? this.emptyTexture2D);
5595
- gl.uniform1i(u['uDynamicShadowMap'] ?? null, 2);
5869
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.sunShadows?.texture ?? this.emptyTexture2DArray);
5870
+ gl.uniform1i(u['uSunShadows'] ?? null, 0);
5871
+ /* What the sun keeps through glass, or the one-texel stand-in the lookup reads as none. */
5872
+ gl.activeTexture(gl.TEXTURE0 + SUN_GLASS_TINT_TEXTURE_UNIT);
5873
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.sunGlassTint?.texture ?? this.emptyTexture2DArray);
5874
+ gl.uniform1i(u['uSunGlassTints'] ?? null, SUN_GLASS_TINT_TEXTURE_UNIT);
5596
5875
  }
5597
5876
  if (this.pointShadows === null) {
5598
5877
  /*
@@ -5605,6 +5884,10 @@ export class WebGL2Renderer {
5605
5884
  else {
5606
5885
  /* Chosen in `pointShadowSystem.ts` and bound here; see `resolve` for the split. */
5607
5886
  this.pointShadows.resolve(this.resolvedPointShadows);
5887
+ /* As the array stores them: 2L once glass has interleaved it. See `strideLayers`. */
5888
+ const stride = this.pointShadowArray?.stride ?? 1;
5889
+ strideLayers(this.resolvedPointShadows.layers, stride);
5890
+ strideLayers(this.resolvedPointShadows.liveLayers, stride);
5608
5891
  this.bindPointShadows(u, DIRECTIONAL_SHADOW_UNITS);
5609
5892
  /*
5610
5893
  * The rectangles' layers, into the array `bindPointShadows` just put on its unit. Resolved
@@ -5810,11 +6093,12 @@ export class WebGL2Renderer {
5810
6093
  */
5811
6094
  tint = null,
5812
6095
  /**
5813
- * Where the mesh was last frame, which this backend takes and ignores.
6096
+ * Where the mesh was last frame — a `Mover` or a matrix — which this backend takes and ignores.
5814
6097
  *
5815
6098
  * Accepted rather than absent so that one scene draws through both renderers unchanged — the
5816
6099
  * parity rule this repository is built on. Reconstruction is WebGPU's, having no compute stage
5817
- * here, so there is no motion target for this to be written into.
6100
+ * here, so there is no motion target for this to be written into, and a mover passed here is
6101
+ * neither read nor advanced.
5818
6102
  */
5819
6103
  _previousModel = null) {
5820
6104
  if (this.contextLost)
@@ -6068,19 +6352,7 @@ export class WebGL2Renderer {
6068
6352
  * translucent one rather than sampling a black texture and painting the pane the colour of a
6069
6353
  * hole. That is the defined state the two-backends rule asks for instead of a silent no-op.
6070
6354
  */
6071
- const refraction = options.refraction ?? 0;
6072
- let refracting = false;
6073
- if (refraction > 0) {
6074
- const snapshot = this.sceneTarget?.snapshotColor() ?? null;
6075
- if (snapshot !== null) {
6076
- refracting = true;
6077
- gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
6078
- gl.bindTexture(gl.TEXTURE_2D, snapshot);
6079
- gl.uniform1f(u['uRefractStrength'] ?? null, refraction);
6080
- gl.uniform3fv(u['uRefractTint'] ?? null, options.refractTint ?? WHITE_TINT);
6081
- gl.uniform1f(u['uRefractThickness'] ?? null, options.thicknessM ?? 0);
6082
- }
6083
- }
6355
+ const refracting = this.bindSeeThrough(u, options);
6084
6356
  /*
6085
6357
  * **The blend and the depth mask belong to whoever owns the pass.** In the ordinary case that
6086
6358
  * is this draw; during an order-independent replay it is `OitPass`, which has set an additive
@@ -6130,6 +6402,8 @@ export class WebGL2Renderer {
6130
6402
  gl.uniform1f(u['uOpacity'] ?? null, 1);
6131
6403
  if (tint !== null)
6132
6404
  gl.uniform3fv(u['uTint'] ?? null, WHITE_TINT);
6405
+ if (refracting)
6406
+ this.unbindSeeThrough(u);
6133
6407
  /* Put back for whatever is drawn next, exactly as `uOpacity` and `uTint` are — a pass
6134
6408
  cannot inherit a material from the draw before it. */
6135
6409
  if (!lit)
@@ -6466,14 +6740,8 @@ export class WebGL2Renderer {
6466
6740
  gl.uniform1f(u['uShadowMapSize'] ?? null, this.quality.directionalShadowMapSize);
6467
6741
  gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peelFilled ? 1 : 0);
6468
6742
  gl.activeTexture(gl.TEXTURE0);
6469
- gl.bindTexture(gl.TEXTURE_2D, this.shadowMap?.texture ?? this.emptyTexture2D);
6470
- gl.uniform1i(u['uStaticShadowMap'] ?? null, 0);
6471
- gl.activeTexture(gl.TEXTURE1);
6472
- gl.bindTexture(gl.TEXTURE_2D, this.peeledShadowMap?.texture ?? this.emptyTexture2D);
6473
- gl.uniform1i(u['uPeeledShadowMap'] ?? null, 1);
6474
- gl.activeTexture(gl.TEXTURE2);
6475
- gl.bindTexture(gl.TEXTURE_2D, this.dynamicShadowMap?.texture ?? this.emptyTexture2D);
6476
- gl.uniform1i(u['uDynamicShadowMap'] ?? null, 2);
6743
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.sunShadows?.texture ?? this.emptyTexture2DArray);
6744
+ gl.uniform1i(u['uSunShadows'] ?? null, 0);
6477
6745
  }
6478
6746
  }
6479
6747
  gl.uniform3fv(u['uCameraLocal'] ?? null, cameraLocal);