@driftengine/core 4.4.2 → 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 (193) hide show
  1. package/CHANGELOG.json +46 -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 +79 -8
  9. package/dist/render/backend/webgl2/renderer.js +360 -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 +2 -12
  34. package/dist/render/backend/webgpu/renderer.d.ts +171 -54
  35. package/dist/render/backend/webgpu/renderer.js +944 -390
  36. package/dist/render/backend/webgpu/scatterPass.js +2 -1
  37. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  38. package/dist/render/backend/webgpu/skinPaletteRing.js +4 -0
  39. package/dist/render/backend/webgpu/snapshotMips.d.ts +29 -0
  40. package/dist/render/backend/webgpu/snapshotMips.js +52 -0
  41. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +5 -0
  42. package/dist/render/backend/webgpu/surfaceTexturePass.js +1 -1
  43. package/dist/render/glass.d.ts +45 -0
  44. package/dist/render/glass.js +39 -0
  45. package/dist/render/glassCasters.d.ts +40 -0
  46. package/dist/render/glassCasters.js +84 -0
  47. package/dist/render/glassShadow.d.ts +78 -0
  48. package/dist/render/glassShadow.js +125 -0
  49. package/dist/render/globalMediumPass.d.ts +2 -3
  50. package/dist/render/globalMediumPass.js +2 -8
  51. package/dist/render/gpudriven/lit.d.ts +12 -0
  52. package/dist/render/gpudriven/lit.js +23 -0
  53. package/dist/render/gpudriven/materialTable.d.ts +15 -5
  54. package/dist/render/gpudriven/materialTable.js +26 -6
  55. package/dist/render/gpudriven/pipeline.d.ts +1 -1
  56. package/dist/render/gpudriven/pipeline.js +4 -3
  57. package/dist/render/gpudriven/sunGlass.d.ts +35 -0
  58. package/dist/render/gpudriven/sunGlass.js +89 -0
  59. package/dist/render/lightBudget.d.ts +17 -5
  60. package/dist/render/lightBudget.js +17 -5
  61. package/dist/render/pointGlassTint.d.ts +59 -0
  62. package/dist/render/pointGlassTint.js +198 -0
  63. package/dist/render/pointShadowArray.d.ts +56 -3
  64. package/dist/render/pointShadowArray.js +127 -16
  65. package/dist/render/pointShadowMap.d.ts +23 -1
  66. package/dist/render/pointShadowMap.js +27 -1
  67. package/dist/render/recon/changeFrames.d.ts +15 -0
  68. package/dist/render/recon/changeFrames.js +21 -0
  69. package/dist/render/recon/lateRouting.d.ts +14 -0
  70. package/dist/render/recon/lateRouting.js +18 -0
  71. package/dist/render/recon/mover.d.ts +57 -0
  72. package/dist/render/recon/mover.js +86 -0
  73. package/dist/render/renderQuality.d.ts +35 -0
  74. package/dist/render/renderQuality.js +7 -0
  75. package/dist/render/rendererHarness.js +12 -0
  76. package/dist/render/sceneTarget.d.ts +7 -1
  77. package/dist/render/sceneTarget.js +58 -12
  78. package/dist/render/shaders/depth.d.ts +16 -1
  79. package/dist/render/shaders/depth.js +85 -9
  80. package/dist/render/shaders/flat/directionalShadow.js +91 -6
  81. package/dist/render/shaders/flat/glassTint.d.ts +6 -0
  82. package/dist/render/shaders/flat/glassTint.js +25 -0
  83. package/dist/render/shaders/flat/index.d.ts +13 -0
  84. package/dist/render/shaders/flat/index.js +4 -0
  85. package/dist/render/shaders/flat/lampGlass.d.ts +1 -0
  86. package/dist/render/shaders/flat/lampGlass.js +183 -0
  87. package/dist/render/shaders/flat/main.d.ts +1 -1
  88. package/dist/render/shaders/flat/main.js +115 -15
  89. package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
  90. package/dist/render/shaders/flat/pointShadow.js +2 -0
  91. package/dist/render/shaders/flat/preamble.js +21 -6
  92. package/dist/render/shaders/generated/depth.wgsl.d.ts +227 -4
  93. package/dist/render/shaders/generated/depth.wgsl.js +227 -4
  94. package/dist/render/shaders/generated/flat.wgsl.d.ts +360 -312
  95. package/dist/render/shaders/generated/flat.wgsl.js +376 -328
  96. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +3 -13
  97. package/dist/render/shaders/generated/globalMedium.wgsl.js +3 -13
  98. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +3 -13
  99. package/dist/render/shaders/generated/lightVolume.wgsl.js +4 -14
  100. package/dist/render/shaders/generated/octahedralResolve.wgsl.d.ts +29 -0
  101. package/dist/render/shaders/generated/octahedralResolve.wgsl.js +29 -0
  102. package/dist/render/shaders/globalMedium.js +6 -6
  103. package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +7 -0
  104. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +86 -1
  105. package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +1 -1
  106. package/dist/render/shaders/gpudriven/cull.wgsl.js +11 -2
  107. package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +1 -1
  108. package/dist/render/shaders/gpudriven/materialTable.wgsl.js +3 -1
  109. package/dist/render/shaders/gpudriven/shade.wgsl.js +50 -1
  110. package/dist/render/shaders/gpudriven/sunGlass.wgsl.d.ts +9 -0
  111. package/dist/render/shaders/gpudriven/sunGlass.wgsl.js +133 -0
  112. package/dist/render/shaders/lightVolume.js +6 -6
  113. package/dist/render/shaders/octahedralResolve.d.ts +8 -0
  114. package/dist/render/shaders/octahedralResolve.js +48 -17
  115. package/dist/render/shaders/recon/lateDepth.wgsl.d.ts +1 -0
  116. package/dist/render/shaders/recon/lateDepth.wgsl.js +44 -0
  117. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  118. package/dist/render/shaders/recon/motion.wgsl.js +84 -0
  119. package/dist/render/shadowCasters.d.ts +20 -1
  120. package/dist/render/shadowCasters.js +15 -1
  121. package/dist/render/shadowMap.d.ts +54 -5
  122. package/dist/render/shadowMap.js +190 -36
  123. package/dist/render/sunGlassTint.d.ts +25 -0
  124. package/dist/render/sunGlassTint.js +79 -0
  125. package/dist/render/translucentQueue.d.ts +6 -0
  126. package/dist/render/translucentQueue.js +15 -1
  127. package/package.json +6 -6
  128. package/src/index.ts +10 -0
  129. package/src/render/backend/api.ts +15 -9
  130. package/src/render/backend/webgl2/glassTintPrograms.ts +58 -0
  131. package/src/render/backend/webgl2/renderer.ts +401 -108
  132. package/src/render/backend/webgpu/buffers.ts +50 -2
  133. package/src/render/backend/webgpu/depthPass.ts +2 -1
  134. package/src/render/backend/webgpu/flatPass.ts +22 -1
  135. package/src/render/backend/webgpu/glassTintPass.ts +244 -0
  136. package/src/render/backend/webgpu/gpuDrivenPass.ts +349 -5
  137. package/src/render/backend/webgpu/instanced.ts +21 -1
  138. package/src/render/backend/webgpu/latePass.ts +152 -0
  139. package/src/render/backend/webgpu/lightVolumePass.ts +5 -1
  140. package/src/render/backend/webgpu/motionPass.ts +535 -0
  141. package/src/render/backend/webgpu/pipelineCache.ts +12 -1
  142. package/src/render/backend/webgpu/pointGlassTintPass.ts +233 -0
  143. package/src/render/backend/webgpu/pointShadowPass.ts +158 -26
  144. package/src/render/backend/webgpu/renderer.ts +1108 -421
  145. package/src/render/backend/webgpu/scatterPass.ts +2 -1
  146. package/src/render/backend/webgpu/skinPaletteRing.ts +4 -0
  147. package/src/render/backend/webgpu/snapshotMips.ts +80 -0
  148. package/src/render/backend/webgpu/surfaceTexturePass.ts +1 -1
  149. package/src/render/glass.ts +74 -0
  150. package/src/render/glassCasters.ts +164 -0
  151. package/src/render/glassShadow.ts +153 -0
  152. package/src/render/globalMediumPass.ts +4 -11
  153. package/src/render/gpudriven/lit.ts +31 -0
  154. package/src/render/gpudriven/materialTable.ts +28 -6
  155. package/src/render/gpudriven/pipeline.ts +4 -3
  156. package/src/render/gpudriven/sunGlass.ts +117 -0
  157. package/src/render/lightBudget.ts +19 -5
  158. package/src/render/pointGlassTint.ts +250 -0
  159. package/src/render/pointShadowArray.ts +188 -22
  160. package/src/render/pointShadowMap.ts +41 -0
  161. package/src/render/recon/changeFrames.ts +24 -0
  162. package/src/render/recon/lateRouting.ts +29 -0
  163. package/src/render/recon/mover.ts +118 -0
  164. package/src/render/renderQuality.ts +47 -0
  165. package/src/render/rendererHarness.ts +12 -0
  166. package/src/render/sceneTarget.ts +58 -11
  167. package/src/render/shaders/depth.ts +109 -9
  168. package/src/render/shaders/flat/directionalShadow.ts +97 -6
  169. package/src/render/shaders/flat/glassTint.ts +25 -0
  170. package/src/render/shaders/flat/index.ts +17 -0
  171. package/src/render/shaders/flat/lampGlass.ts +185 -0
  172. package/src/render/shaders/flat/main.ts +115 -15
  173. package/src/render/shaders/flat/pointShadow.ts +2 -0
  174. package/src/render/shaders/flat/preamble.ts +21 -6
  175. package/src/render/shaders/generated/depth.wgsl.ts +234 -4
  176. package/src/render/shaders/generated/flat.wgsl.ts +376 -328
  177. package/src/render/shaders/generated/globalMedium.wgsl.ts +3 -13
  178. package/src/render/shaders/generated/lightVolume.wgsl.ts +4 -14
  179. package/src/render/shaders/generated/octahedralResolve.wgsl.ts +30 -0
  180. package/src/render/shaders/globalMedium.ts +6 -6
  181. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +88 -1
  182. package/src/render/shaders/gpudriven/cull.wgsl.ts +11 -2
  183. package/src/render/shaders/gpudriven/materialTable.wgsl.ts +3 -1
  184. package/src/render/shaders/gpudriven/shade.wgsl.ts +50 -1
  185. package/src/render/shaders/gpudriven/sunGlass.wgsl.ts +139 -0
  186. package/src/render/shaders/lightVolume.ts +6 -6
  187. package/src/render/shaders/octahedralResolve.ts +50 -17
  188. package/src/render/shaders/recon/lateDepth.wgsl.ts +47 -0
  189. package/src/render/shaders/recon/motion.wgsl.ts +84 -0
  190. package/src/render/shadowCasters.ts +28 -1
  191. package/src/render/shadowMap.ts +231 -37
  192. package/src/render/sunGlassTint.ts +86 -0
  193. 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`. */
@@ -687,9 +695,8 @@ export class WebGL2Renderer {
687
695
  depthInstancedCutoutUniforms;
688
696
  scatterDepthProgram;
689
697
  scatterDepthUniforms;
690
- shadowMap;
691
- peeledShadowMap;
692
- dynamicShadowMap;
698
+ /** The sun's static, moving and peeled maps, as layers of one array. See `shadowMap.ts`. */
699
+ sunShadows;
693
700
  pointShadows;
694
701
  /**
695
702
  * The layers a world's casting rectangles hold, above the point pool in the same array.
@@ -895,7 +902,7 @@ export class WebGL2Renderer {
895
902
  /** Non-zero while the drawing buffer is pinned; see `lockDrawingBuffer`. */
896
903
  lockedWidth = 0;
897
904
  lockedHeight = 0;
898
- activeShadowMap = null;
905
+ activeSunLayer = null;
899
906
  reflectionPassActive = false;
900
907
  reflectionReadyThisFrame = false;
901
908
  reflectionAtmosphereY = 0;
@@ -917,7 +924,7 @@ export class WebGL2Renderer {
917
924
  *
918
925
  * **Not whether the map exists.** That was the test here, on the belief that a GL texture is
919
926
  * born reading as the far plane; it is born zeroed, and a scene that built the layer and never
920
- * 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
921
928
  * unfilled layer would read as lit either way — this is what keeps the two backends one
922
929
  * decision, and a frame that does not peel from paying a fetch per tap for nothing.
923
930
  */
@@ -943,8 +950,34 @@ export class WebGL2Renderer {
943
950
  * Built once at construction, arrow properties closing over `this`: a sink created
944
951
  * per pass would allocate six objects per light per frame.
945
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
+ };
946
972
  casterSink = {
947
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
+ }
948
981
  this.shadowDrawBudget.ask();
949
982
  const { gl } = this;
950
983
  /* A two-sided caster casts from whichever face the light sees: nothing culled. */
@@ -984,6 +1017,12 @@ export class WebGL2Renderer {
984
1017
  * columns the batch's own buffer supplies.
985
1018
  */
986
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
+ }
987
1026
  const { gl } = this;
988
1027
  const gpuBatch = batch;
989
1028
  const count = Math.min(data.count, gpuBatch.capacity);
@@ -1015,7 +1054,13 @@ export class WebGL2Renderer {
1015
1054
  gl.enable(gl.CULL_FACE);
1016
1055
  gl.useProgram(this.depthProgram);
1017
1056
  },
1018
- 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
+ }
1019
1064
  this.shadowDrawBudget.ask();
1020
1065
  const { gl } = this;
1021
1066
  const u = this.depthSkinnedUniforms;
@@ -1098,8 +1143,52 @@ export class WebGL2Renderer {
1098
1143
  gl.useProgram(this.depthProgram);
1099
1144
  gl.uniform1i(this.depthUniforms['uPeelShadowLayer'] ?? null, 0);
1100
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();
1101
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
+ }
1102
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
+ }
1103
1192
  /**
1104
1193
  * Viewport in CSS pixels rather than device pixels.
1105
1194
  *
@@ -1906,20 +1995,14 @@ export class WebGL2Renderer {
1906
1995
  gl.uniform1i(uniforms['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
1907
1996
  }
1908
1997
  gl.useProgram(null);
1909
- this.shadowMap = this.quality.directionalShadows
1910
- ? new ShadowMap(gl, this.quality.directionalShadowMapSize)
1911
- : null;
1912
- // A second static depth layer preserves both occluders where two world
1913
- // shadows overlap. It is depth-peeled from the first map below.
1914
- this.peeledShadowMap =
1915
- this.quality.directionalShadows && this.quality.directionalShadowDepthLayers > 1
1916
- ? new ShadowMap(gl, this.quality.directionalShadowMapSize)
1917
- : null;
1918
- // Movers get an independent layer. It preserves distance fading when a
1919
- // moving and a static caster overlap: their transmissions multiply rather
1920
- // than one depth replacing the other.
1921
- this.dynamicShadowMap = this.quality.directionalShadows
1922
- ? 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)
1923
2006
  : null;
1924
2007
  // Static maps are allocated once the world reports its light count. No
1925
2008
  // cubemap resources exist at all when the quality profile disables them.
@@ -2088,9 +2171,9 @@ export class WebGL2Renderer {
2088
2171
  * every one of them, per renderer, for as long as it lived; `renderer.test.ts` counts them.
2089
2172
  */
2090
2173
  this.sceneTarget?.dispose();
2091
- this.shadowMap?.dispose(gl);
2092
- this.peeledShadowMap?.dispose(gl);
2093
- this.dynamicShadowMap?.dispose(gl);
2174
+ this.sunShadows?.dispose(gl);
2175
+ this.sunGlassTint?.dispose(gl);
2176
+ this.glassTintPrograms?.dispose(gl);
2094
2177
  this.planarReflection?.dispose(gl);
2095
2178
  this.pointShadows?.dispose();
2096
2179
  gl.deleteVertexArray(this.panelVao);
@@ -2719,8 +2802,10 @@ export class WebGL2Renderer {
2719
2802
  const lit = options.lit ?? true;
2720
2803
  const fog = options.fog ?? true;
2721
2804
  const toneMapped = options.toneMapped ?? true;
2722
- /* An instanced draw does not refract. See `materialChanges.ts`. */
2723
- 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 });
2724
2809
  if (own)
2725
2810
  this.materials.dirty();
2726
2811
  this.takeMaterial();
@@ -2741,20 +2826,9 @@ export class WebGL2Renderer {
2741
2826
  * **A null snapshot leaves the strength at zero**, so the draw shades as an ordinary
2742
2827
  * translucent one rather than sampling a black texture and painting the pane the colour of a
2743
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.
2744
2831
  */
2745
- const refraction = options.refraction ?? 0;
2746
- let refracting = false;
2747
- if (refraction > 0) {
2748
- const snapshot = this.sceneTarget?.snapshotColor() ?? null;
2749
- if (snapshot !== null) {
2750
- refracting = true;
2751
- gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
2752
- gl.bindTexture(gl.TEXTURE_2D, snapshot);
2753
- gl.uniform1f(u['uRefractStrength'] ?? null, refraction);
2754
- gl.uniform3fv(u['uRefractTint'] ?? null, options.refractTint ?? WHITE_TINT);
2755
- gl.uniform1f(u['uRefractThickness'] ?? null, options.thicknessM ?? 0);
2756
- }
2757
- }
2758
2832
  if (!lit)
2759
2833
  gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
2760
2834
  if (!fog)
@@ -2774,7 +2848,13 @@ export class WebGL2Renderer {
2774
2848
  gl.enable(gl.POLYGON_OFFSET_FILL);
2775
2849
  gl.polygonOffset(offset.slope, offset.units);
2776
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);
2777
2855
  mesh.drawInstances(gl, count);
2856
+ if (this.materialDoubleSided)
2857
+ gl.enable(gl.CULL_FACE);
2778
2858
  if (layer > 0) {
2779
2859
  gl.polygonOffset(0, 0);
2780
2860
  gl.disable(gl.POLYGON_OFFSET_FILL);
@@ -2790,11 +2870,8 @@ export class WebGL2Renderer {
2790
2870
  gl.uniform1f(u['uOpacity'] ?? null, 1);
2791
2871
  /* The binding is released with the uniform, which is the 2026-08-27 rule: a WebGL2 binding
2792
2872
  outlives its frame, so a snapshot left on unit 14 is held against the next frame's copy. */
2793
- if (refracting) {
2794
- gl.uniform1f(u['uRefractStrength'] ?? null, 0);
2795
- gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
2796
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
2797
- }
2873
+ if (refracting)
2874
+ this.unbindSeeThrough(u);
2798
2875
  if (!lit)
2799
2876
  gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
2800
2877
  if (!fog)
@@ -2805,6 +2882,45 @@ export class WebGL2Renderer {
2805
2882
  this.materials.dirty();
2806
2883
  this.useFlatProgram();
2807
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
+ }
2808
2924
  disposeMesh(mesh) {
2809
2925
  /*
2810
2926
  * Nothing to release when the context is gone: it took every buffer and VAO with it,
@@ -3338,6 +3454,10 @@ export class WebGL2Renderer {
3338
3454
  */
3339
3455
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.pointShadowArray?.texture ?? this.emptyTexture2DArray);
3340
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);
3341
3461
  gl.uniform1iv(uniforms['uPointShadowLayer[0]'] ?? null, resolved.layers);
3342
3462
  /* Origin and far plane in one row; see `uPointShadowProjection` in the preamble. */
3343
3463
  gl.uniform4fv(uniforms['uPointShadowProjection[0]'] ?? null, resolved.projections);
@@ -3614,20 +3734,17 @@ export class WebGL2Renderer {
3614
3734
  * emitting, because a stale map must never be sampled.
3615
3735
  */
3616
3736
  beginShadowPass(lightViewProj, layer = 'static') {
3617
- const shadowMap = layer === 'static'
3618
- ? this.shadowMap
3619
- : layer === 'static-peel'
3620
- ? this.peeledShadowMap
3621
- : this.dynamicShadowMap;
3622
- if (shadowMap === null) {
3737
+ const sun = this.sunShadows;
3738
+ if (sun === null || (layer === 'static-peel' && sun.layers < 3)) {
3623
3739
  this.shadowPassActive = false;
3624
- this.activeShadowMap = null;
3740
+ this.activeSunLayer = null;
3625
3741
  return;
3626
3742
  }
3627
3743
  const { gl } = this;
3628
3744
  this.gpuTimer.begin('shadows');
3629
3745
  this.shadowPassActive = true;
3630
- this.activeShadowMap = shadowMap;
3746
+ this.activeSunLayer = layer;
3747
+ this.glassCasters.clear();
3631
3748
  /* The static layer opens first, so a frame's peel counts from here. See `peelFilled`. */
3632
3749
  if (layer === 'static')
3633
3750
  this.peelFilled = false;
@@ -3655,16 +3772,16 @@ export class WebGL2Renderer {
3655
3772
  // The previous mesh pass left all directional maps bound for sampling.
3656
3773
  // A texture cannot be sampled while it is attached to the active depth
3657
3774
  // target, even when the shader branch would skip that sample.
3658
- const targetUnit = layer === 'static' ? 0 : layer === 'static-peel' ? 1 : 2;
3659
- gl.activeTexture(gl.TEXTURE0 + targetUnit);
3660
- gl.bindTexture(gl.TEXTURE_2D, null);
3661
- shadowMap.begin(gl);
3775
+ gl.activeTexture(gl.TEXTURE0);
3776
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3777
+ sun.begin(gl, layer);
3662
3778
  gl.useProgram(this.depthProgram);
3663
3779
  const peel = layer === 'static-peel';
3664
3780
  gl.uniform1i(this.depthUniforms['uPeelShadowLayer'] ?? null, peel ? 1 : 0);
3665
3781
  if (peel) {
3782
+ /* The static layer, read while the peel is drawn aside: see `SunShadowArray`. */
3666
3783
  gl.activeTexture(gl.TEXTURE0);
3667
- gl.bindTexture(gl.TEXTURE_2D, this.shadowMap?.texture ?? null);
3784
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, sun.texture);
3668
3785
  gl.uniform1i(this.depthUniforms['uPreviousShadowMap'] ?? null, 0);
3669
3786
  }
3670
3787
  /* The corrected one, matching `activeDepthViewProj` above: a pass must render its map with the
@@ -3770,21 +3887,158 @@ export class WebGL2Renderer {
3770
3887
  return;
3771
3888
  casters(this.casterSink);
3772
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
+ };
3773
4023
  endShadowPass() {
3774
4024
  if (this.contextLost)
3775
4025
  return;
3776
- const shadowMap = this.activeShadowMap;
3777
- if (!this.shadowPassActive || shadowMap === null)
4026
+ const sun = this.sunShadows;
4027
+ const layer = this.activeSunLayer;
4028
+ if (!this.shadowPassActive || sun === null || layer === null)
3778
4029
  return;
3779
- shadowMap.end(this.gl);
3780
- if (shadowMap === this.peeledShadowMap)
4030
+ sun.end(this.gl);
4031
+ if (layer === 'static-peel')
3781
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');
3782
4036
  /* Back to the frame's sense; see `beginShadowPass` for why it left it. */
3783
4037
  this.gl.depthFunc(glDepthFuncEqual(this.gl));
3784
4038
  this.gl.clearDepth(this.depthClear);
3785
4039
  this.gpuTimer.end();
3786
4040
  this.shadowPassActive = false;
3787
- this.activeShadowMap = null;
4041
+ this.activeSunLayer = null;
3788
4042
  // Restore the drawing-buffer viewport the shadow pass overrode.
3789
4043
  this.restoreViewport();
3790
4044
  }
@@ -3818,6 +4072,8 @@ export class WebGL2Renderer {
3818
4072
  directionalShadows: this.quality.directionalShadows,
3819
4073
  environmentProbe: this.probeCapture !== null,
3820
4074
  nightEmissive: this.quality.nightEmissive,
4075
+ /* Off compiles none of the glass lookup: see `FlatShaderOptions.glassShadows`. */
4076
+ glassShadows: this.quality.glassShadows !== 'off',
3821
4077
  };
3822
4078
  const build = (budget) => flatFrag({ ...variant, maxLights: budget.maxLights, maxAreaLights: budget.maxAreaLights });
3823
4079
  const vert = flatVert({ skinned: false, morphed: false, instanced: false });
@@ -4081,6 +4337,8 @@ export class WebGL2Renderer {
4081
4337
  */
4082
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));
4083
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);
4084
4342
  this.restoreViewport();
4085
4343
  }
4086
4344
  /**
@@ -4127,6 +4385,10 @@ export class WebGL2Renderer {
4127
4385
  * reason: it already holds everything it needs.
4128
4386
  */
4129
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);
4130
4392
  }
4131
4393
  /**
4132
4394
  * Every point-shadow bake, under the one set of conventions all of them need.
@@ -4172,12 +4434,12 @@ export class WebGL2Renderer {
4172
4434
  * map is stale on every frame it is checked — `pointShadowImage.ts` records that exact bug
4173
4435
  * from two readers disagreeing about this argument's default.
4174
4436
  */
4175
- 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));
4176
4438
  }
4177
4439
  /** Spend up to `maxFaces` on this map, returning how many it used. */
4178
4440
  bakeOne(map, light, drawCasters, maxFaces) {
4179
4441
  const { gl } = this;
4180
- 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));
4181
4443
  }
4182
4444
  /**
4183
4445
  * Draw into a rectangle of the canvas, on its own terms.
@@ -4388,6 +4650,10 @@ export class WebGL2Renderer {
4388
4650
  *
4389
4651
  * What it gives up is one frame of history: the frame after a cut has no blur and no temporal
4390
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.
4391
4657
  */
4392
4658
  cameraCut() {
4393
4659
  this.hasPreviousView = false;
@@ -5103,8 +5369,9 @@ export class WebGL2Renderer {
5103
5369
  * flat paint with the effect on. The opaque frame is finished here, which is what a pane
5104
5370
  * should show; the latch then serves every pane in the replay. Only when something refracts.
5105
5371
  */
5106
- if (this.translucentQueue.refracts)
5107
- this.sceneTarget?.snapshotColor();
5372
+ if (this.translucentQueue.refracts) {
5373
+ this.sceneTarget?.snapshotColor(false, this.translucentQueue.glassy);
5374
+ }
5108
5375
  this.oit.accumulateOit(this.canvas.width, this.canvas.height, oitDepth, (weighted) => {
5109
5376
  this.oitReplaying = true;
5110
5377
  this.useFlatProgram();
@@ -5121,6 +5388,7 @@ export class WebGL2Renderer {
5121
5388
  options.refraction = draw.refraction;
5122
5389
  options.refractTint = draw.refractTint;
5123
5390
  options.thicknessM = draw.thicknessM;
5391
+ options.glass = draw.glass ?? undefined;
5124
5392
  this.drawTranslucentMesh(draw.mesh, draw.model, draw.opacity, options);
5125
5393
  });
5126
5394
  /* Back to zero for every other draw in the frame, exactly as `uOpacity` and `uTint` are. */
@@ -5170,9 +5438,7 @@ export class WebGL2Renderer {
5170
5438
  /* The placeholder rather than null where a map is merely absent, for the reason
5171
5439
  `emptyTexture.ts` gives. It is black, which as a depth is an occluder at the light,
5172
5440
  so it is safe only because `sunShadow` is 0 whenever these maps do not exist. */
5173
- staticShadowMap: this.shadowMap?.texture ?? this.emptyTexture2D,
5174
- peeledShadowMap: this.peeledShadowMap?.texture ?? this.emptyTexture2D,
5175
- dynamicShadowMap: this.dynamicShadowMap?.texture ?? this.emptyTexture2D,
5441
+ sunShadows: this.sunShadows?.texture ?? this.emptyTexture2DArray,
5176
5442
  peeledEnabled: this.peelFilled,
5177
5443
  });
5178
5444
  if (marched) {
@@ -5475,9 +5741,11 @@ export class WebGL2Renderer {
5475
5741
  gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
5476
5742
  gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
5477
5743
  gl.uniform1i(u['uRefractScene'] ?? null, REFRACT_SCENE_TEXTURE_UNIT);
5478
- 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);
5479
5747
  gl.uniform3fv(u['uRefractTint'] ?? null, WHITE_TINT);
5480
- gl.uniform1f(u['uRefractThickness'] ?? null, 0);
5748
+ gl.uniform3fv(u['uGlassTint'] ?? null, WHITE_TINT);
5481
5749
  gl.uniform3fv(u['uDirectionalDir'] ?? null, env.directionalDir);
5482
5750
  gl.uniform3fv(u['uDirectionalColor'] ?? null, env.directionalColor);
5483
5751
  gl.uniform3fv(u['uAmbient'] ?? null, env.ambient);
@@ -5573,7 +5841,7 @@ export class WebGL2Renderer {
5573
5841
  * its own copy of this lookup; the two backends have to project the same way.
5574
5842
  */
5575
5843
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, env.lightViewProj);
5576
- gl.uniform1f(u['uShadowStrength'] ?? null, this.shadowMap === null ? 0 : env.shadowStrength);
5844
+ gl.uniform1f(u['uShadowStrength'] ?? null, this.sunShadows === null ? 0 : env.shadowStrength);
5577
5845
  gl.uniform1f(u['uShadowMapSize'] ?? null, this.quality.directionalShadowMapSize);
5578
5846
  gl.uniform1f(u['uShadowDepthSpan'] ?? null, env.shadowDepthSpan);
5579
5847
  gl.uniform1f(u['uShadowMaxDistance'] ?? null, this.quality.directionalShadowMaxDistance);
@@ -5598,14 +5866,12 @@ export class WebGL2Renderer {
5598
5866
  */
5599
5867
  if (this.quality.directionalShadows) {
5600
5868
  gl.activeTexture(gl.TEXTURE0);
5601
- gl.bindTexture(gl.TEXTURE_2D, this.shadowMap?.texture ?? this.emptyTexture2D);
5602
- gl.uniform1i(u['uStaticShadowMap'] ?? null, 0);
5603
- gl.activeTexture(gl.TEXTURE1);
5604
- gl.bindTexture(gl.TEXTURE_2D, this.peeledShadowMap?.texture ?? this.emptyTexture2D);
5605
- gl.uniform1i(u['uPeeledShadowMap'] ?? null, 1);
5606
- gl.activeTexture(gl.TEXTURE2);
5607
- gl.bindTexture(gl.TEXTURE_2D, this.dynamicShadowMap?.texture ?? this.emptyTexture2D);
5608
- 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);
5609
5875
  }
5610
5876
  if (this.pointShadows === null) {
5611
5877
  /*
@@ -5618,6 +5884,10 @@ export class WebGL2Renderer {
5618
5884
  else {
5619
5885
  /* Chosen in `pointShadowSystem.ts` and bound here; see `resolve` for the split. */
5620
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);
5621
5891
  this.bindPointShadows(u, DIRECTIONAL_SHADOW_UNITS);
5622
5892
  /*
5623
5893
  * The rectangles' layers, into the array `bindPointShadows` just put on its unit. Resolved
@@ -5823,11 +6093,12 @@ export class WebGL2Renderer {
5823
6093
  */
5824
6094
  tint = null,
5825
6095
  /**
5826
- * 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.
5827
6097
  *
5828
6098
  * Accepted rather than absent so that one scene draws through both renderers unchanged — the
5829
6099
  * parity rule this repository is built on. Reconstruction is WebGPU's, having no compute stage
5830
- * 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.
5831
6102
  */
5832
6103
  _previousModel = null) {
5833
6104
  if (this.contextLost)
@@ -6081,19 +6352,7 @@ export class WebGL2Renderer {
6081
6352
  * translucent one rather than sampling a black texture and painting the pane the colour of a
6082
6353
  * hole. That is the defined state the two-backends rule asks for instead of a silent no-op.
6083
6354
  */
6084
- const refraction = options.refraction ?? 0;
6085
- let refracting = false;
6086
- if (refraction > 0) {
6087
- const snapshot = this.sceneTarget?.snapshotColor() ?? null;
6088
- if (snapshot !== null) {
6089
- refracting = true;
6090
- gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
6091
- gl.bindTexture(gl.TEXTURE_2D, snapshot);
6092
- gl.uniform1f(u['uRefractStrength'] ?? null, refraction);
6093
- gl.uniform3fv(u['uRefractTint'] ?? null, options.refractTint ?? WHITE_TINT);
6094
- gl.uniform1f(u['uRefractThickness'] ?? null, options.thicknessM ?? 0);
6095
- }
6096
- }
6355
+ const refracting = this.bindSeeThrough(u, options);
6097
6356
  /*
6098
6357
  * **The blend and the depth mask belong to whoever owns the pass.** In the ordinary case that
6099
6358
  * is this draw; during an order-independent replay it is `OitPass`, which has set an additive
@@ -6143,6 +6402,8 @@ export class WebGL2Renderer {
6143
6402
  gl.uniform1f(u['uOpacity'] ?? null, 1);
6144
6403
  if (tint !== null)
6145
6404
  gl.uniform3fv(u['uTint'] ?? null, WHITE_TINT);
6405
+ if (refracting)
6406
+ this.unbindSeeThrough(u);
6146
6407
  /* Put back for whatever is drawn next, exactly as `uOpacity` and `uTint` are — a pass
6147
6408
  cannot inherit a material from the draw before it. */
6148
6409
  if (!lit)
@@ -6479,14 +6740,8 @@ export class WebGL2Renderer {
6479
6740
  gl.uniform1f(u['uShadowMapSize'] ?? null, this.quality.directionalShadowMapSize);
6480
6741
  gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peelFilled ? 1 : 0);
6481
6742
  gl.activeTexture(gl.TEXTURE0);
6482
- gl.bindTexture(gl.TEXTURE_2D, this.shadowMap?.texture ?? this.emptyTexture2D);
6483
- gl.uniform1i(u['uStaticShadowMap'] ?? null, 0);
6484
- gl.activeTexture(gl.TEXTURE1);
6485
- gl.bindTexture(gl.TEXTURE_2D, this.peeledShadowMap?.texture ?? this.emptyTexture2D);
6486
- gl.uniform1i(u['uPeeledShadowMap'] ?? null, 1);
6487
- gl.activeTexture(gl.TEXTURE2);
6488
- gl.bindTexture(gl.TEXTURE_2D, this.dynamicShadowMap?.texture ?? this.emptyTexture2D);
6489
- 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);
6490
6745
  }
6491
6746
  }
6492
6747
  gl.uniform3fv(u['uCameraLocal'] ?? null, cameraLocal);