@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
@@ -13,6 +13,9 @@ import {
13
13
  glShadowDepthFunc,
14
14
  } from '../../depthConvention.ts';
15
15
  import type { FrameView } from '../../frameView.ts';
16
+ import type { Mover } from '../../recon/mover.ts';
17
+ import { resolveGlass } from '../../glass.ts';
18
+ import type { GlassOptions, ResolvedGlass } from '../../glass.ts';
16
19
  import type { ClipControlExtension } from '../../depthConvention.ts';
17
20
  import type { ReadonlyMat4 } from 'gl-matrix';
18
21
  import type { Camera } from '../../camera.ts';
@@ -148,12 +151,16 @@ import {
148
151
  DEPTH_SKINNED_VERT,
149
152
  DEPTH_VERT,
150
153
  } from '../../shaders/depth.ts';
151
- import { PointShadowArray } from '../../pointShadowArray.ts';
152
- import { ShadowMap } from '../../shadowMap.ts';
154
+ import { OCTAHEDRAL_EDGE, PointShadowArray, strideLayers } from '../../pointShadowArray.ts';
155
+ import { SunShadowArray, type SunShadowLayer } from '../../shadowMap.ts';
153
156
  import { POINT_SHADOW_SAMPLER, PointShadowSystem } from '../../pointShadowSystem.ts';
154
157
  import type { ShadowLight } from '../../pointShadowSystem.ts';
158
+ import { castsDepth, glassOf } from '../../shadowCasters.ts';
159
+ import { GlassCasterList, type GlassReplaySink } from '../../glassCasters.ts';
160
+ import { SunGlassTint } from '../../sunGlassTint.ts';
161
+ import { GlassTintPrograms, type GlassTintProgram } from './glassTintPrograms.ts';
155
162
  import type { SceneCasterMaterial, ShadowCasterSink, ShadowCasters } from '../../shadowCasters.ts';
156
- import { FACE_COUNT, PointShadowMap } from '../../pointShadowMap.ts';
163
+ import { FACE_COUNT, PointShadowMap, type PointGlassDraw } from '../../pointShadowMap.ts';
157
164
  import { createResolvedPointShadows, DEFAULT_SOURCE_RADIUS } from '../../pointShadowImage.ts';
158
165
  import {
159
166
  createBakeScratch,
@@ -162,6 +169,8 @@ import {
162
169
  } from '../../pointShadowBudget.ts';
163
170
  import {
164
171
  DIRECTIONAL_SHADOW_UNITS,
172
+ SUN_GLASS_TINT_TEXTURE_UNIT,
173
+ POINT_GLASS_TINT_TEXTURE_UNIT,
165
174
  LIVE_POINT_SHADOW_MAPS,
166
175
  MAX_POINT_LIGHTS,
167
176
  NORMAL_TEXTURE_UNIT,
@@ -780,6 +789,13 @@ export interface TranslucentMeshOptions {
780
789
  * clear, and it is the whole difference between this and a flat tint.
781
790
  */
782
791
  readonly thicknessM?: number;
792
+ /**
793
+ * Glass: the pane shows what is behind it, by how much it lets through and how little it reflects
794
+ * at the view angle, blurred by how frosted it is, tinted, and glowing with the lights behind it.
795
+ * It keeps its own lighting and highlight, which `refraction` alone replaces. Absent, or letting
796
+ * nothing through, is not glass. See `glass.ts`; `refraction` still bends what a pane shows.
797
+ */
798
+ readonly glass?: GlassOptions;
783
799
  /**
784
800
  * Whether this draw writes depth. `true`, which is what it has always done, unless a caller
785
801
  * says otherwise.
@@ -1114,6 +1130,8 @@ export class WebGL2Renderer implements RendererApi {
1114
1130
  private skinPaletteSet = false;
1115
1131
 
1116
1132
  private readonly emptyTexture2D: WebGLTexture;
1133
+ /** One glass resolution reused by every draw, so a pane allocates nothing. See `glass.ts`. */
1134
+ private readonly glassScratch: ResolvedGlass = { transmission: 0, frost: 0, tint: [1, 1, 1] };
1117
1135
  /** DriftLight's stand-ins while no field is whole: no brick anywhere, and no light. */
1118
1136
  private readonly emptyDriftIndex: WebGLTexture;
1119
1137
  private readonly emptyDriftAtlas: WebGLTexture;
@@ -1186,6 +1204,7 @@ export class WebGL2Renderer implements RendererApi {
1186
1204
  refraction: number;
1187
1205
  refractTint: Vec3 | undefined;
1188
1206
  thicknessM: number;
1207
+ glass: GlassOptions | undefined;
1189
1208
  } = {
1190
1209
  lit: true,
1191
1210
  fog: true,
@@ -1196,6 +1215,7 @@ export class WebGL2Renderer implements RendererApi {
1196
1215
  refraction: 0,
1197
1216
  refractTint: undefined,
1198
1217
  thicknessM: 0,
1218
+ glass: undefined,
1199
1219
  };
1200
1220
 
1201
1221
  private motionBlurScale = 1;
@@ -1324,9 +1344,8 @@ export class WebGL2Renderer implements RendererApi {
1324
1344
  private readonly depthInstancedCutoutUniforms: Record<string, WebGLUniformLocation>;
1325
1345
  private readonly scatterDepthProgram: WebGLProgram;
1326
1346
  private readonly scatterDepthUniforms: Record<string, WebGLUniformLocation>;
1327
- private readonly shadowMap: ShadowMap | null;
1328
- private readonly peeledShadowMap: ShadowMap | null;
1329
- private readonly dynamicShadowMap: ShadowMap | null;
1347
+ /** The sun's static, moving and peeled maps, as layers of one array. See `shadowMap.ts`. */
1348
+ private readonly sunShadows: SunShadowArray | null;
1330
1349
  private readonly pointShadows: PointShadowSystem<PointShadowMap> | null;
1331
1350
  /**
1332
1351
  * The layers a world's casting rectangles hold, above the point pool in the same array.
@@ -1551,7 +1570,7 @@ export class WebGL2Renderer implements RendererApi {
1551
1570
  /** Non-zero while the drawing buffer is pinned; see `lockDrawingBuffer`. */
1552
1571
  private lockedWidth = 0;
1553
1572
  private lockedHeight = 0;
1554
- private activeShadowMap: ShadowMap | null = null;
1573
+ private activeSunLayer: SunShadowLayer | null = null;
1555
1574
  private reflectionPassActive = false;
1556
1575
  private reflectionReadyThisFrame = false;
1557
1576
  private reflectionAtmosphereY = 0;
@@ -1575,7 +1594,7 @@ export class WebGL2Renderer implements RendererApi {
1575
1594
  *
1576
1595
  * **Not whether the map exists.** That was the test here, on the belief that a GL texture is
1577
1596
  * born reading as the far plane; it is born zeroed, and a scene that built the layer and never
1578
- * peeled it sampled an occluder at the light. `ShadowMap` now clears itself at birth, so an
1597
+ * peeled it sampled an occluder at the light. `SunShadowArray` clears itself at birth, so an
1579
1598
  * unfilled layer would read as lit either way — this is what keeps the two backends one
1580
1599
  * decision, and a frame that does not peel from paying a fetch per tap for nothing.
1581
1600
  */
@@ -1602,8 +1621,34 @@ export class WebGL2Renderer implements RendererApi {
1602
1621
  * Built once at construction, arrow properties closing over `this`: a sink created
1603
1622
  * per pass would allocate six objects per light per frame.
1604
1623
  */
1624
+ /** What the sun's light keeps through glass, made the first time glass casts: `sunGlassTint.ts`. */
1625
+ private sunGlassTint: SunGlassTint | null = null;
1626
+ /** The five programs a pane casts its colour through, compiled the first time glass casts. */
1627
+ private glassTintPrograms: GlassTintPrograms | null = null;
1628
+ /** True while the glass list is replayed into the glass depth layer through `casterSink`. */
1629
+ private drawingGlassDepth = false;
1630
+ /** How many times a shadow array grew its glass layers: once per family, and never again. */
1631
+ private sunGlassRebuilds = 0;
1632
+ /** The light a tint pass is drawn for, and one pane's colour and clarity: uploaded per entry. */
1633
+ private readonly glassLight = new Float32Array(4);
1634
+ private readonly glassPane = new Float32Array(4);
1635
+ /** The glass the open shadow pass was offered, kept for its own passes: `glassCasters.ts`. */
1636
+ private readonly glassCasters = new GlassCasterList();
1637
+ /** One resolution every glass caster is read through, so recording one allocates nothing. */
1638
+ private readonly glassCasterScratch: ResolvedGlass = {
1639
+ transmission: 0,
1640
+ frost: 0,
1641
+ tint: [1, 1, 1],
1642
+ };
1605
1643
  private readonly casterSink: ShadowCasterSink = {
1606
1644
  mesh: (mesh, model, material) => {
1645
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
1646
+ /* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
1647
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1648
+ this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
1649
+ }
1650
+ return;
1651
+ }
1607
1652
  this.shadowDrawBudget.ask();
1608
1653
  const { gl } = this;
1609
1654
  /* A two-sided caster casts from whichever face the light sees: nothing culled. */
@@ -1639,6 +1684,12 @@ export class WebGL2Renderer implements RendererApi {
1639
1684
  * columns the batch's own buffer supplies.
1640
1685
  */
1641
1686
  instanced: (batch, data, material) => {
1687
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
1688
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1689
+ this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
1690
+ }
1691
+ return;
1692
+ }
1642
1693
  const { gl } = this;
1643
1694
  const gpuBatch = batch as InstancedBatch;
1644
1695
  const count = Math.min(data.count, gpuBatch.capacity);
@@ -1665,7 +1716,13 @@ export class WebGL2Renderer implements RendererApi {
1665
1716
  if (twoSided && this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
1666
1717
  gl.useProgram(this.depthProgram);
1667
1718
  },
1668
- skinnedMesh: (mesh, model, palette) => {
1719
+ skinnedMesh: (mesh, model, palette, material) => {
1720
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
1721
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1722
+ this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
1723
+ }
1724
+ return;
1725
+ }
1669
1726
  this.shadowDrawBudget.ask();
1670
1727
  const { gl } = this;
1671
1728
  const u = this.depthSkinnedUniforms;
@@ -1748,9 +1805,60 @@ export class WebGL2Renderer implements RendererApi {
1748
1805
  gl.useProgram(this.depthProgram);
1749
1806
  gl.uniform1i(this.depthUniforms['uPeelShadowLayer'] ?? null, 0);
1750
1807
  gl.uniformMatrix4fv(this.depthUniforms['uLightViewProj'] ?? null, false, faceViewProj);
1808
+ /* A face is an enumeration of its own: its glass is its own. */
1809
+ this.glassCasters.clear();
1751
1810
  casters(this.casterSink);
1811
+ /*
1812
+ * The first face to offer glass interleaves the array, before its opaque depth is resolved:
1813
+ * `PointShadowArray.growForGlass`. Nothing baked before this saw glass, so nothing re-bakes.
1814
+ */
1815
+ if (this.glassCasters.count > 0 && this.quality.glassShadows !== 'off') {
1816
+ const tintEdge =
1817
+ this.quality.glassShadows === 'half' ? OCTAHEDRAL_EDGE >> 1 : OCTAHEDRAL_EDGE;
1818
+ this.pointShadowArray?.growForGlass(gl, tintEdge);
1819
+ }
1752
1820
  };
1753
1821
 
1822
+ /**
1823
+ * A lamp's glass, after each face's opaque depth: `PointShadowMap.bake` asks and binds, this
1824
+ * draws, through the same sinks the sun's glass takes and in the face's own matrix.
1825
+ */
1826
+ private readonly pointGlassDraw: PointGlassDraw = {
1827
+ offered: () => this.glassCasters.count > 0,
1828
+ draw: (pass, x, y, z) => {
1829
+ const { gl } = this;
1830
+ if (pass === 'depth') {
1831
+ /* Nothing on the unit the depth programs declare a previous map on: see `flushSunGlass`. */
1832
+ gl.activeTexture(gl.TEXTURE0);
1833
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
1834
+ this.activeDepthPeel = false;
1835
+ gl.useProgram(this.depthProgram);
1836
+ gl.uniform1i(this.depthUniforms['uPeelShadowLayer'] ?? null, 0);
1837
+ gl.uniformMatrix4fv(
1838
+ this.depthUniforms['uLightViewProj'] ?? null,
1839
+ false,
1840
+ this.activeDepthViewProj ?? IDENTITY_MAT4,
1841
+ );
1842
+ this.drawingGlassDepth = true;
1843
+ this.glassCasters.replay(this.glassDepthSink);
1844
+ this.drawingGlassDepth = false;
1845
+ return;
1846
+ }
1847
+ this.glassTintPrograms ??= new GlassTintPrograms(gl);
1848
+ /* A place, w 1: the tint takes each pane's angle to the lamp from its own position. */
1849
+ this.glassLight[0] = x;
1850
+ this.glassLight[1] = y;
1851
+ this.glassLight[2] = z;
1852
+ this.glassLight[3] = 1;
1853
+ this.glassCasters.replay(this.glassTintSink);
1854
+ },
1855
+ };
1856
+
1857
+ /** The glass hook a bake is handed: none at all when glass shadows are off. */
1858
+ private get pointGlass(): PointGlassDraw | null {
1859
+ return this.quality.glassShadows === 'off' ? null : this.pointGlassDraw;
1860
+ }
1861
+
1754
1862
  /**
1755
1863
  * Viewport in CSS pixels rather than device pixels.
1756
1864
  *
@@ -2716,20 +2824,18 @@ export class WebGL2Renderer implements RendererApi {
2716
2824
  gl.uniform1i(uniforms['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
2717
2825
  }
2718
2826
  gl.useProgram(null);
2719
- this.shadowMap = this.quality.directionalShadows
2720
- ? new ShadowMap(gl, this.quality.directionalShadowMapSize)
2721
- : null;
2722
- // A second static depth layer preserves both occluders where two world
2723
- // shadows overlap. It is depth-peeled from the first map below.
2724
- this.peeledShadowMap =
2725
- this.quality.directionalShadows && this.quality.directionalShadowDepthLayers > 1
2726
- ? new ShadowMap(gl, this.quality.directionalShadowMapSize)
2727
- : null;
2728
- // Movers get an independent layer. It preserves distance fading when a
2729
- // moving and a static caster overlap: their transmissions multiply rather
2730
- // than one depth replacing the other.
2731
- this.dynamicShadowMap = this.quality.directionalShadows
2732
- ? new ShadowMap(gl, this.quality.directionalShadowMapSize)
2827
+ /*
2828
+ * The static world, its movers and — where more than one static depth layer is asked for —
2829
+ * the peel, as layers of one array. Movers get a layer of their own so a moving and a static
2830
+ * caster that overlap keep both distance fades: their transmissions multiply rather than one
2831
+ * depth replacing the other. The peel keeps the second static occluder the same way.
2832
+ */
2833
+ this.sunShadows = this.quality.directionalShadows
2834
+ ? new SunShadowArray(
2835
+ gl,
2836
+ this.quality.directionalShadowMapSize,
2837
+ this.quality.directionalShadowDepthLayers > 1,
2838
+ )
2733
2839
  : null;
2734
2840
  // Static maps are allocated once the world reports its light count. No
2735
2841
  // cubemap resources exist at all when the quality profile disables them.
@@ -2921,9 +3027,9 @@ export class WebGL2Renderer implements RendererApi {
2921
3027
  * every one of them, per renderer, for as long as it lived; `renderer.test.ts` counts them.
2922
3028
  */
2923
3029
  this.sceneTarget?.dispose();
2924
- this.shadowMap?.dispose(gl);
2925
- this.peeledShadowMap?.dispose(gl);
2926
- this.dynamicShadowMap?.dispose(gl);
3030
+ this.sunShadows?.dispose(gl);
3031
+ this.sunGlassTint?.dispose(gl);
3032
+ this.glassTintPrograms?.dispose(gl);
2927
3033
  this.planarReflection?.dispose(gl);
2928
3034
  this.pointShadows?.dispose();
2929
3035
  gl.deleteVertexArray(this.panelVao);
@@ -3606,8 +3712,10 @@ export class WebGL2Renderer implements RendererApi {
3606
3712
  const lit = options.lit ?? true;
3607
3713
  const fog = options.fog ?? true;
3608
3714
  const toneMapped = options.toneMapped ?? true;
3609
- /* An instanced draw does not refract. See `materialChanges.ts`. */
3610
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting: false });
3715
+ /* A blended batch shows what is behind it — refraction or glass — as a single draw does, and
3716
+ counts a material of its own for it by the rule both backends share (`materialChanges.ts`). */
3717
+ const refracting = blend && this.bindSeeThrough(u, options);
3718
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
3611
3719
  if (own) this.materials.dirty();
3612
3720
  this.takeMaterial();
3613
3721
  const depthWrite = options.depthWrite ?? true;
@@ -3627,20 +3735,9 @@ export class WebGL2Renderer implements RendererApi {
3627
3735
  * **A null snapshot leaves the strength at zero**, so the draw shades as an ordinary
3628
3736
  * translucent one rather than sampling a black texture and painting the pane the colour of a
3629
3737
  * hole. That is the defined state the two-backends rule asks for instead of a silent no-op.
3738
+ * The copy is bound above, before the material is counted, because whether it was taken is
3739
+ * part of what the count asks.
3630
3740
  */
3631
- const refraction = options.refraction ?? 0;
3632
- let refracting = false;
3633
- if (refraction > 0) {
3634
- const snapshot = this.sceneTarget?.snapshotColor() ?? null;
3635
- if (snapshot !== null) {
3636
- refracting = true;
3637
- gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
3638
- gl.bindTexture(gl.TEXTURE_2D, snapshot);
3639
- gl.uniform1f(u['uRefractStrength'] ?? null, refraction);
3640
- gl.uniform3fv(u['uRefractTint'] ?? null, options.refractTint ?? WHITE_TINT);
3641
- gl.uniform1f(u['uRefractThickness'] ?? null, options.thicknessM ?? 0);
3642
- }
3643
- }
3644
3741
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
3645
3742
  if (!fog) gl.uniform1i(u['uFogEnabled'] ?? null, 0);
3646
3743
  if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.gradeCode(), 1));
@@ -3658,7 +3755,11 @@ export class WebGL2Renderer implements RendererApi {
3658
3755
  gl.polygonOffset(offset.slope, offset.units);
3659
3756
  }
3660
3757
 
3758
+ /* Both sides of a double-sided material, as every mesh path does and WebGPU's pipeline does
3759
+ for both: without it a batch's far faces were culled here alone. */
3760
+ if (this.materialDoubleSided) gl.disable(gl.CULL_FACE);
3661
3761
  mesh.drawInstances(gl, count);
3762
+ if (this.materialDoubleSided) gl.enable(gl.CULL_FACE);
3662
3763
 
3663
3764
  if (layer > 0) {
3664
3765
  gl.polygonOffset(0, 0);
@@ -3673,11 +3774,7 @@ export class WebGL2Renderer implements RendererApi {
3673
3774
  if (opacity < 1) gl.uniform1f(u['uOpacity'] ?? null, 1);
3674
3775
  /* The binding is released with the uniform, which is the 2026-08-27 rule: a WebGL2 binding
3675
3776
  outlives its frame, so a snapshot left on unit 14 is held against the next frame's copy. */
3676
- if (refracting) {
3677
- gl.uniform1f(u['uRefractStrength'] ?? null, 0);
3678
- gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
3679
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
3680
- }
3777
+ if (refracting) this.unbindSeeThrough(u);
3681
3778
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
3682
3779
  if (!fog) gl.uniform1i(u['uFogEnabled'] ?? null, 1);
3683
3780
  if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
@@ -3685,6 +3782,53 @@ export class WebGL2Renderer implements RendererApi {
3685
3782
  this.useFlatProgram();
3686
3783
  }
3687
3784
 
3785
+ /**
3786
+ * Bind what a translucent draw shows through it: the frame's copy, refraction's bend and absorption,
3787
+ * and glass. Returns whether the copy is bound, and so whether `unbindSeeThrough` owes it back.
3788
+ *
3789
+ * **A glass draw takes the copy even when it does not bend**, since a pane shows what is behind it
3790
+ * either way, and asks for the copy's mip chain, which a frosted pane reads blurred. A null copy
3791
+ * leaves both off, so the pane shades as ordinary translucency rather than sampling black.
3792
+ *
3793
+ * **Refused inside a probe's face or a mirror**, as WebGPU's `takeRefractSnapshot` refuses: the
3794
+ * copy is of the frame drawn to the screen, so a pane there would show the wrong picture, and
3795
+ * taking it mid-bake is how a probe's next face came to be attached to the scene's framebuffer.
3796
+ */
3797
+ private bindSeeThrough(
3798
+ u: Record<string, WebGLUniformLocation>,
3799
+ options: TranslucentMeshOptions,
3800
+ ): boolean {
3801
+ const { gl } = this;
3802
+ const refraction = options.refraction ?? 0;
3803
+ const glassy = resolveGlass(options.glass, this.glassScratch);
3804
+ if (refraction <= 0 && !glassy) return false;
3805
+ if (this.probePassActive || this.reflectionPassActive) return false;
3806
+ const snapshot = this.sceneTarget?.snapshotColor(false, glassy) ?? null;
3807
+ if (snapshot === null) return false;
3808
+ gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
3809
+ gl.bindTexture(gl.TEXTURE_2D, snapshot);
3810
+ const glass = this.glassScratch;
3811
+ /* Strength, path thickness, transmission, frost: one vector, for the budget the shader states. */
3812
+ gl.uniform4f(
3813
+ u['uSeeThrough'] ?? null,
3814
+ Math.max(refraction, 0),
3815
+ options.thicknessM ?? 0,
3816
+ glass.transmission,
3817
+ glass.frost,
3818
+ );
3819
+ gl.uniform3fv(u['uRefractTint'] ?? null, options.refractTint ?? WHITE_TINT);
3820
+ gl.uniform3fv(u['uGlassTint'] ?? null, glass.tint);
3821
+ return true;
3822
+ }
3823
+
3824
+ /** Put back what `bindSeeThrough` bound: the binding outlives its frame (the 2026-08-27 rule). */
3825
+ private unbindSeeThrough(u: Record<string, WebGLUniformLocation>): void {
3826
+ const { gl } = this;
3827
+ gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
3828
+ gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
3829
+ gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
3830
+ }
3831
+
3688
3832
  disposeMesh(mesh: Mesh): void {
3689
3833
  /*
3690
3834
  * Nothing to release when the context is gone: it took every buffer and VAO with it,
@@ -4353,6 +4497,13 @@ export class WebGL2Renderer implements RendererApi {
4353
4497
  */
4354
4498
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.pointShadowArray?.texture ?? this.emptyTexture2DArray);
4355
4499
  gl.uniform1i(uniforms[POINT_SHADOW_SAMPLER] ?? null, firstUnit);
4500
+ /* What the lamps keep through glass: the one-texel stand-in, read as none, until glass casts. */
4501
+ gl.activeTexture(gl.TEXTURE0 + POINT_GLASS_TINT_TEXTURE_UNIT);
4502
+ gl.bindTexture(
4503
+ gl.TEXTURE_2D_ARRAY,
4504
+ this.pointShadowArray?.tintTexture ?? this.emptyTexture2DArray,
4505
+ );
4506
+ gl.uniform1i(uniforms['uPointGlassTints'] ?? null, POINT_GLASS_TINT_TEXTURE_UNIT);
4356
4507
 
4357
4508
  gl.uniform1iv(uniforms['uPointShadowLayer[0]'] ?? null, resolved.layers);
4358
4509
  /* Origin and far plane in one row; see `uPointShadowProjection` in the preamble. */
@@ -4726,21 +4877,17 @@ export class WebGL2Renderer implements RendererApi {
4726
4877
  lightViewProj: ReadonlyMat4,
4727
4878
  layer: 'static' | 'static-peel' | 'dynamic' = 'static',
4728
4879
  ): void {
4729
- const shadowMap =
4730
- layer === 'static'
4731
- ? this.shadowMap
4732
- : layer === 'static-peel'
4733
- ? this.peeledShadowMap
4734
- : this.dynamicShadowMap;
4735
- if (shadowMap === null) {
4880
+ const sun = this.sunShadows;
4881
+ if (sun === null || (layer === 'static-peel' && sun.layers < 3)) {
4736
4882
  this.shadowPassActive = false;
4737
- this.activeShadowMap = null;
4883
+ this.activeSunLayer = null;
4738
4884
  return;
4739
4885
  }
4740
4886
  const { gl } = this;
4741
4887
  this.gpuTimer.begin('shadows');
4742
4888
  this.shadowPassActive = true;
4743
- this.activeShadowMap = shadowMap;
4889
+ this.activeSunLayer = layer;
4890
+ this.glassCasters.clear();
4744
4891
  /* The static layer opens first, so a frame's peel counts from here. See `peelFilled`. */
4745
4892
  if (layer === 'static') this.peelFilled = false;
4746
4893
  /*
@@ -4767,16 +4914,16 @@ export class WebGL2Renderer implements RendererApi {
4767
4914
  // The previous mesh pass left all directional maps bound for sampling.
4768
4915
  // A texture cannot be sampled while it is attached to the active depth
4769
4916
  // target, even when the shader branch would skip that sample.
4770
- const targetUnit = layer === 'static' ? 0 : layer === 'static-peel' ? 1 : 2;
4771
- gl.activeTexture(gl.TEXTURE0 + targetUnit);
4772
- gl.bindTexture(gl.TEXTURE_2D, null);
4773
- shadowMap.begin(gl);
4917
+ gl.activeTexture(gl.TEXTURE0);
4918
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
4919
+ sun.begin(gl, layer);
4774
4920
  gl.useProgram(this.depthProgram);
4775
4921
  const peel = layer === 'static-peel';
4776
4922
  gl.uniform1i(this.depthUniforms['uPeelShadowLayer'] ?? null, peel ? 1 : 0);
4777
4923
  if (peel) {
4924
+ /* The static layer, read while the peel is drawn aside: see `SunShadowArray`. */
4778
4925
  gl.activeTexture(gl.TEXTURE0);
4779
- gl.bindTexture(gl.TEXTURE_2D, this.shadowMap?.texture ?? null);
4926
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, sun.texture);
4780
4927
  gl.uniform1i(this.depthUniforms['uPreviousShadowMap'] ?? null, 0);
4781
4928
  }
4782
4929
  /* The corrected one, matching `activeDepthViewProj` above: a pass must render its map with the
@@ -4888,18 +5035,165 @@ export class WebGL2Renderer implements RendererApi {
4888
5035
  casters(this.casterSink);
4889
5036
  }
4890
5037
 
5038
+ /**
5039
+ * Draw the pass's glass twice: where the nearest pane is, and what the panes let through.
5040
+ *
5041
+ * After the opaque casters, in the pass's own light matrix. A layer that has held glass is
5042
+ * redrawn even when this pass offered none, so glass that has gone takes its colour with it. The
5043
+ * first glass grows the array (`SunShadowArray.growForGlass`) and makes the tint target.
5044
+ */
5045
+ private flushSunGlass(which: 'static' | 'dynamic'): void {
5046
+ const sun = this.sunShadows;
5047
+ const list = this.glassCasters;
5048
+ if (sun === null || this.quality.glassShadows === 'off') return;
5049
+ if (list.count === 0 && !sun.hasGlass) return;
5050
+ const { gl } = this;
5051
+ if (list.count > 0 && sun.growForGlass(gl)) this.sunGlassRebuilds++;
5052
+
5053
+ /* Where the nearest pane is: the depth programs, through the sink every opaque caster took. */
5054
+ gl.activeTexture(gl.TEXTURE0);
5055
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
5056
+ sun.beginGlass(gl, which);
5057
+ this.activeDepthPeel = false;
5058
+ gl.useProgram(this.depthProgram);
5059
+ gl.uniform1i(this.depthUniforms['uPeelShadowLayer'] ?? null, 0);
5060
+ gl.uniformMatrix4fv(
5061
+ this.depthUniforms['uLightViewProj'] ?? null,
5062
+ false,
5063
+ this.activeDepthViewProj ?? IDENTITY_MAT4,
5064
+ );
5065
+ this.drawingGlassDepth = true;
5066
+ list.replay(this.glassDepthSink);
5067
+ this.drawingGlassDepth = false;
5068
+ sun.end(gl);
5069
+
5070
+ /* What the panes let through, every one on a ray, multiplied. */
5071
+ this.glassTintPrograms ??= new GlassTintPrograms(gl);
5072
+ const size = this.quality.glassShadows === 'half' ? Math.max(1, sun.size >> 1) : sun.size;
5073
+ const tint = (this.sunGlassTint ??= new SunGlassTint(gl, size));
5074
+ /*
5075
+ * The direction the light travels, off the light matrix's depth row: an orthographic
5076
+ * projection's depth grows along it. The sign is immaterial — the tint takes the cosine
5077
+ * absolutely — and w 0 says it is a direction rather than a place.
5078
+ */
5079
+ const m = this.activeDepthViewProj ?? IDENTITY_MAT4;
5080
+ const length = Math.hypot(m[2] as number, m[6] as number, m[10] as number) || 1;
5081
+ this.glassLight[0] = (m[2] as number) / length;
5082
+ this.glassLight[1] = (m[6] as number) / length;
5083
+ this.glassLight[2] = (m[10] as number) / length;
5084
+ this.glassLight[3] = 0;
5085
+ tint.begin(gl, which === 'static' ? 0 : 1);
5086
+ list.replay(this.glassTintSink);
5087
+ tint.end(gl);
5088
+ gl.useProgram(this.depthProgram);
5089
+ }
5090
+
5091
+ /** The glass list into the glass depth layer: `casterSink` itself, with glass let through. */
5092
+ private readonly glassDepthSink: GlassReplaySink = {
5093
+ mesh: (mesh, model, material) => this.casterSink.mesh(mesh, model, material),
5094
+ instanced: (batch, data, material) => this.casterSink.instanced?.(batch, data, material),
5095
+ skinnedMesh: (mesh, model, palette, material) =>
5096
+ this.casterSink.skinnedMesh(mesh, model, palette, material),
5097
+ };
5098
+
5099
+ /** One pane's tint uniforms, and its cutout where it has one. */
5100
+ private bindGlassTint(
5101
+ program: GlassTintProgram,
5102
+ glass: ResolvedGlass,
5103
+ cutout: CutoutCaster<SurfaceTextureHandle> | null,
5104
+ ): void {
5105
+ const { gl } = this;
5106
+ const u = program.uniforms;
5107
+ gl.useProgram(program.program);
5108
+ gl.uniformMatrix4fv(
5109
+ u['uLightViewProj'] ?? null,
5110
+ false,
5111
+ this.activeDepthViewProj ?? IDENTITY_MAT4,
5112
+ );
5113
+ this.uploadWind(u, this.frameWind);
5114
+ this.glassPane[0] = glass.transmission * glass.tint[0];
5115
+ this.glassPane[1] = glass.transmission * glass.tint[1];
5116
+ this.glassPane[2] = glass.transmission * glass.tint[2];
5117
+ this.glassPane[3] = 1 - glass.frost;
5118
+ gl.uniform4fv(u['uGlassPane'] ?? null, this.glassPane);
5119
+ gl.uniform4fv(u['uGlassLight'] ?? null, this.glassLight);
5120
+ if (cutout !== null) {
5121
+ gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
5122
+ gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
5123
+ (cutout.albedo as SurfaceTexture).bind(gl, SURFACE_TEXTURE_UNIT);
5124
+ gl.uniform1i(u['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
5125
+ }
5126
+ }
5127
+
5128
+ /**
5129
+ * The glass list into the tint layer, through the tint programs, **culling nothing**: a light
5130
+ * crosses a pane whichever way the pane faces, and a closed glass object has two surfaces on a
5131
+ * ray, so every one counts. What that asks of a model is that a pane be one surface — two-sided
5132
+ * glass says so with `doubleSided`, and a second copy of its triangles is a second pane.
5133
+ */
5134
+ private readonly glassTintSink: GlassReplaySink = {
5135
+ mesh: (mesh, model, material, glass) => {
5136
+ const programs = this.glassTintPrograms;
5137
+ if (programs === null) return;
5138
+ const { gl } = this;
5139
+ const cutout = cutoutOf(material);
5140
+ const program = cutout !== null ? programs.cutout : programs.rigid;
5141
+ this.bindGlassTint(program, glass, cutout);
5142
+ gl.uniformMatrix4fv(program.uniforms['uModel'] ?? null, false, model);
5143
+ gl.disable(gl.CULL_FACE);
5144
+ (mesh as Mesh).draw(gl);
5145
+ if (this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
5146
+ if (cutout !== null) this.restoreSurfaceTextureUnit();
5147
+ },
5148
+ instanced: (batch, data, material, glass) => {
5149
+ const programs = this.glassTintPrograms;
5150
+ if (programs === null) return;
5151
+ const { gl } = this;
5152
+ const gpuBatch = batch as InstancedBatch;
5153
+ const count = Math.min(data.count, gpuBatch.capacity);
5154
+ if (count === 0) return;
5155
+ const cutout = cutoutOf(material);
5156
+ this.bindGlassTint(
5157
+ cutout !== null ? programs.instancedCutout : programs.instanced,
5158
+ glass,
5159
+ cutout,
5160
+ );
5161
+ gl.disable(gl.CULL_FACE);
5162
+ gpuBatch.mesh.drawInstances(gl, count);
5163
+ if (this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
5164
+ if (cutout !== null) this.restoreSurfaceTextureUnit();
5165
+ },
5166
+ skinnedMesh: (mesh, model, palette, _material, glass) => {
5167
+ const programs = this.glassTintPrograms;
5168
+ if (programs === null) return;
5169
+ const { gl } = this;
5170
+ const program = programs.skinned;
5171
+ this.bindGlassTint(program, glass, null);
5172
+ gl.uniformMatrix4fv(program.uniforms['uModel'] ?? null, false, model);
5173
+ this.skinPalette.update(gl, palette);
5174
+ this.skinPalette.bind(gl, SKIN_PALETTE_TEXTURE_UNIT);
5175
+ gl.uniform1i(program.uniforms['uJointPalette'] ?? null, SKIN_PALETTE_TEXTURE_UNIT);
5176
+ gl.disable(gl.CULL_FACE);
5177
+ (mesh as Mesh).draw(gl);
5178
+ if (this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
5179
+ },
5180
+ };
5181
+
4891
5182
  endShadowPass(): void {
4892
5183
  if (this.contextLost) return;
4893
- const shadowMap = this.activeShadowMap;
4894
- if (!this.shadowPassActive || shadowMap === null) return;
4895
- shadowMap.end(this.gl);
4896
- if (shadowMap === this.peeledShadowMap) this.peelFilled = true;
5184
+ const sun = this.sunShadows;
5185
+ const layer = this.activeSunLayer;
5186
+ if (!this.shadowPassActive || sun === null || layer === null) return;
5187
+ sun.end(this.gl);
5188
+ if (layer === 'static-peel') this.peelFilled = true;
5189
+ /* The peel keeps no glass: its layer is the second opaque occluder and nothing else. */
5190
+ else this.flushSunGlass(layer === 'dynamic' ? 'dynamic' : 'static');
4897
5191
  /* Back to the frame's sense; see `beginShadowPass` for why it left it. */
4898
5192
  this.gl.depthFunc(glDepthFuncEqual(this.gl));
4899
5193
  this.gl.clearDepth(this.depthClear);
4900
5194
  this.gpuTimer.end();
4901
5195
  this.shadowPassActive = false;
4902
- this.activeShadowMap = null;
5196
+ this.activeSunLayer = null;
4903
5197
  // Restore the drawing-buffer viewport the shadow pass overrode.
4904
5198
  this.restoreViewport();
4905
5199
  }
@@ -4938,6 +5232,8 @@ export class WebGL2Renderer implements RendererApi {
4938
5232
  directionalShadows: this.quality.directionalShadows,
4939
5233
  environmentProbe: this.probeCapture !== null,
4940
5234
  nightEmissive: this.quality.nightEmissive,
5235
+ /* Off compiles none of the glass lookup: see `FlatShaderOptions.glassShadows`. */
5236
+ glassShadows: this.quality.glassShadows !== 'off',
4941
5237
  };
4942
5238
  const build = (budget: LightBudget): string =>
4943
5239
  flatFrag({ ...variant, maxLights: budget.maxLights, maxAreaLights: budget.maxAreaLights });
@@ -5254,6 +5550,8 @@ export class WebGL2Renderer implements RendererApi {
5254
5550
  this.bakeOne(map, light, casters as ShadowCasters, maxFaces),
5255
5551
  );
5256
5552
  this.updateAreaShadows(areaLights, frameDt, staticCasters, dynamicCasters);
5553
+ /* The chain frost reads, once for the round, for the lights whose glass it drew or cleared. */
5554
+ this.pointShadowArray.finishTints(this.gl);
5257
5555
  this.restoreViewport();
5258
5556
  }
5259
5557
 
@@ -5314,6 +5612,10 @@ export class WebGL2Renderer implements RendererApi {
5314
5612
  * reason: it already holds everything it needs.
5315
5613
  */
5316
5614
  set.resolve(areaLights, this.resolvedAreaShadows);
5615
+ /* As the array stores them: 2L once glass has interleaved it. See `strideLayers`. */
5616
+ const stride = this.pointShadowArray?.stride ?? 1;
5617
+ strideLayers(this.resolvedAreaShadows.layer, stride);
5618
+ strideLayers(this.resolvedAreaShadows.liveLayer, stride);
5317
5619
  }
5318
5620
 
5319
5621
  /**
@@ -5378,6 +5680,7 @@ export class WebGL2Renderer implements RendererApi {
5378
5680
  near,
5379
5681
  DEFAULT_SOURCE_RADIUS,
5380
5682
  maxFaces,
5683
+ this.pointGlass,
5381
5684
  ),
5382
5685
  );
5383
5686
  }
@@ -5401,6 +5704,7 @@ export class WebGL2Renderer implements RendererApi {
5401
5704
  light.shadowNear,
5402
5705
  light.sourceRadius,
5403
5706
  maxFaces,
5707
+ this.pointGlass,
5404
5708
  ),
5405
5709
  );
5406
5710
  }
@@ -5635,6 +5939,10 @@ export class WebGL2Renderer implements RendererApi {
5635
5939
  *
5636
5940
  * What it gives up is one frame of history: the frame after a cut has no blur and no temporal
5637
5941
  * blend, as the first frame of a session has none. Nothing else is reset.
5942
+ *
5943
+ * **A reconstructing renderer depends on it**: its history is the last shot's, and without this
5944
+ * call a cut blends that shot into the first frames of the next. Nothing detects a cut on the
5945
+ * caller's behalf, deliberately — a fast pan and a cut look alike from here.
5638
5946
  */
5639
5947
  cameraCut(): void {
5640
5948
  this.hasPreviousView = false;
@@ -6425,7 +6733,9 @@ export class WebGL2Renderer implements RendererApi {
6425
6733
  * flat paint with the effect on. The opaque frame is finished here, which is what a pane
6426
6734
  * should show; the latch then serves every pane in the replay. Only when something refracts.
6427
6735
  */
6428
- if (this.translucentQueue.refracts) this.sceneTarget?.snapshotColor();
6736
+ if (this.translucentQueue.refracts) {
6737
+ this.sceneTarget?.snapshotColor(false, this.translucentQueue.glassy);
6738
+ }
6429
6739
  this.oit.accumulateOit(this.canvas.width, this.canvas.height, oitDepth, (weighted) => {
6430
6740
  this.oitReplaying = true;
6431
6741
  this.useFlatProgram();
@@ -6442,6 +6752,7 @@ export class WebGL2Renderer implements RendererApi {
6442
6752
  options.refraction = draw.refraction;
6443
6753
  options.refractTint = draw.refractTint as unknown as Vec3 | undefined;
6444
6754
  options.thicknessM = draw.thicknessM;
6755
+ options.glass = draw.glass ?? undefined;
6445
6756
  this.drawTranslucentMesh(draw.mesh as Mesh, draw.model, draw.opacity, options);
6446
6757
  });
6447
6758
  /* Back to zero for every other draw in the frame, exactly as `uOpacity` and `uTint` are. */
@@ -6504,9 +6815,7 @@ export class WebGL2Renderer implements RendererApi {
6504
6815
  /* The placeholder rather than null where a map is merely absent, for the reason
6505
6816
  `emptyTexture.ts` gives. It is black, which as a depth is an occluder at the light,
6506
6817
  so it is safe only because `sunShadow` is 0 whenever these maps do not exist. */
6507
- staticShadowMap: this.shadowMap?.texture ?? this.emptyTexture2D,
6508
- peeledShadowMap: this.peeledShadowMap?.texture ?? this.emptyTexture2D,
6509
- dynamicShadowMap: this.dynamicShadowMap?.texture ?? this.emptyTexture2D,
6818
+ sunShadows: this.sunShadows?.texture ?? this.emptyTexture2DArray,
6510
6819
  peeledEnabled: this.peelFilled,
6511
6820
  },
6512
6821
  );
@@ -6853,9 +7162,11 @@ export class WebGL2Renderer implements RendererApi {
6853
7162
  gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
6854
7163
  gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6855
7164
  gl.uniform1i(u['uRefractScene'] ?? null, REFRACT_SCENE_TEXTURE_UNIT);
6856
- gl.uniform1f(u['uRefractStrength'] ?? null, 0);
7165
+ /* Neither refracting nor glass, which is every draw that does not say otherwise. See
7166
+ `bindSeeThrough` for the four numbers in the one vector. */
7167
+ gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
6857
7168
  gl.uniform3fv(u['uRefractTint'] ?? null, WHITE_TINT);
6858
- gl.uniform1f(u['uRefractThickness'] ?? null, 0);
7169
+ gl.uniform3fv(u['uGlassTint'] ?? null, WHITE_TINT);
6859
7170
  gl.uniform3fv(u['uDirectionalDir'] ?? null, env.directionalDir);
6860
7171
  gl.uniform3fv(u['uDirectionalColor'] ?? null, env.directionalColor);
6861
7172
  gl.uniform3fv(u['uAmbient'] ?? null, env.ambient);
@@ -6957,7 +7268,7 @@ export class WebGL2Renderer implements RendererApi {
6957
7268
  * its own copy of this lookup; the two backends have to project the same way.
6958
7269
  */
6959
7270
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, env.lightViewProj);
6960
- gl.uniform1f(u['uShadowStrength'] ?? null, this.shadowMap === null ? 0 : env.shadowStrength);
7271
+ gl.uniform1f(u['uShadowStrength'] ?? null, this.sunShadows === null ? 0 : env.shadowStrength);
6961
7272
  gl.uniform1f(u['uShadowMapSize'] ?? null, this.quality.directionalShadowMapSize);
6962
7273
  gl.uniform1f(u['uShadowDepthSpan'] ?? null, env.shadowDepthSpan);
6963
7274
  gl.uniform1f(u['uShadowMaxDistance'] ?? null, this.quality.directionalShadowMaxDistance);
@@ -6984,16 +7295,12 @@ export class WebGL2Renderer implements RendererApi {
6984
7295
  */
6985
7296
  if (this.quality.directionalShadows) {
6986
7297
  gl.activeTexture(gl.TEXTURE0);
6987
- gl.bindTexture(gl.TEXTURE_2D, this.shadowMap?.texture ?? this.emptyTexture2D);
6988
- gl.uniform1i(u['uStaticShadowMap'] ?? null, 0);
6989
-
6990
- gl.activeTexture(gl.TEXTURE1);
6991
- gl.bindTexture(gl.TEXTURE_2D, this.peeledShadowMap?.texture ?? this.emptyTexture2D);
6992
- gl.uniform1i(u['uPeeledShadowMap'] ?? null, 1);
6993
-
6994
- gl.activeTexture(gl.TEXTURE2);
6995
- gl.bindTexture(gl.TEXTURE_2D, this.dynamicShadowMap?.texture ?? this.emptyTexture2D);
6996
- gl.uniform1i(u['uDynamicShadowMap'] ?? null, 2);
7298
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.sunShadows?.texture ?? this.emptyTexture2DArray);
7299
+ gl.uniform1i(u['uSunShadows'] ?? null, 0);
7300
+ /* What the sun keeps through glass, or the one-texel stand-in the lookup reads as none. */
7301
+ gl.activeTexture(gl.TEXTURE0 + SUN_GLASS_TINT_TEXTURE_UNIT);
7302
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.sunGlassTint?.texture ?? this.emptyTexture2DArray);
7303
+ gl.uniform1i(u['uSunGlassTints'] ?? null, SUN_GLASS_TINT_TEXTURE_UNIT);
6997
7304
  }
6998
7305
 
6999
7306
  if (this.pointShadows === null) {
@@ -7006,6 +7313,10 @@ export class WebGL2Renderer implements RendererApi {
7006
7313
  } else {
7007
7314
  /* Chosen in `pointShadowSystem.ts` and bound here; see `resolve` for the split. */
7008
7315
  this.pointShadows.resolve(this.resolvedPointShadows);
7316
+ /* As the array stores them: 2L once glass has interleaved it. See `strideLayers`. */
7317
+ const stride = this.pointShadowArray?.stride ?? 1;
7318
+ strideLayers(this.resolvedPointShadows.layers, stride);
7319
+ strideLayers(this.resolvedPointShadows.liveLayers, stride);
7009
7320
  this.bindPointShadows(u, DIRECTIONAL_SHADOW_UNITS);
7010
7321
  /*
7011
7322
  * The rectangles' layers, into the array `bindPointShadows` just put on its unit. Resolved
@@ -7223,13 +7534,14 @@ export class WebGL2Renderer implements RendererApi {
7223
7534
  */
7224
7535
  tint: Vec3 | null = null,
7225
7536
  /**
7226
- * Where the mesh was last frame, which this backend takes and ignores.
7537
+ * Where the mesh was last frame — a `Mover` or a matrix — which this backend takes and ignores.
7227
7538
  *
7228
7539
  * Accepted rather than absent so that one scene draws through both renderers unchanged — the
7229
7540
  * parity rule this repository is built on. Reconstruction is WebGPU's, having no compute stage
7230
- * here, so there is no motion target for this to be written into.
7541
+ * here, so there is no motion target for this to be written into, and a mover passed here is
7542
+ * neither read nor advanced.
7231
7543
  */
7232
- _previousModel: ReadonlyMat4 | null = null,
7544
+ _previousModel: ReadonlyMat4 | Mover | null = null,
7233
7545
  ): void {
7234
7546
  if (this.contextLost) return;
7235
7547
  /* Geometry that has not all arrived is not drawn. See `Mesh.complete`. */
@@ -7474,19 +7786,7 @@ export class WebGL2Renderer implements RendererApi {
7474
7786
  * translucent one rather than sampling a black texture and painting the pane the colour of a
7475
7787
  * hole. That is the defined state the two-backends rule asks for instead of a silent no-op.
7476
7788
  */
7477
- const refraction = options.refraction ?? 0;
7478
- let refracting = false;
7479
- if (refraction > 0) {
7480
- const snapshot = this.sceneTarget?.snapshotColor() ?? null;
7481
- if (snapshot !== null) {
7482
- refracting = true;
7483
- gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
7484
- gl.bindTexture(gl.TEXTURE_2D, snapshot);
7485
- gl.uniform1f(u['uRefractStrength'] ?? null, refraction);
7486
- gl.uniform3fv(u['uRefractTint'] ?? null, options.refractTint ?? WHITE_TINT);
7487
- gl.uniform1f(u['uRefractThickness'] ?? null, options.thicknessM ?? 0);
7488
- }
7489
- }
7789
+ const refracting = this.bindSeeThrough(u, options);
7490
7790
  /*
7491
7791
  * **The blend and the depth mask belong to whoever owns the pass.** In the ordinary case that
7492
7792
  * is this draw; during an order-independent replay it is `OitPass`, which has set an additive
@@ -7529,6 +7829,7 @@ export class WebGL2Renderer implements RendererApi {
7529
7829
  if (ownsState) gl.disable(gl.BLEND);
7530
7830
  gl.uniform1f(u['uOpacity'] ?? null, 1);
7531
7831
  if (tint !== null) gl.uniform3fv(u['uTint'] ?? null, WHITE_TINT);
7832
+ if (refracting) this.unbindSeeThrough(u);
7532
7833
  /* Put back for whatever is drawn next, exactly as `uOpacity` and `uTint` are — a pass
7533
7834
  cannot inherit a material from the draw before it. */
7534
7835
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
@@ -7916,16 +8217,8 @@ export class WebGL2Renderer implements RendererApi {
7916
8217
  gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peelFilled ? 1 : 0);
7917
8218
 
7918
8219
  gl.activeTexture(gl.TEXTURE0);
7919
- gl.bindTexture(gl.TEXTURE_2D, this.shadowMap?.texture ?? this.emptyTexture2D);
7920
- gl.uniform1i(u['uStaticShadowMap'] ?? null, 0);
7921
-
7922
- gl.activeTexture(gl.TEXTURE1);
7923
- gl.bindTexture(gl.TEXTURE_2D, this.peeledShadowMap?.texture ?? this.emptyTexture2D);
7924
- gl.uniform1i(u['uPeeledShadowMap'] ?? null, 1);
7925
-
7926
- gl.activeTexture(gl.TEXTURE2);
7927
- gl.bindTexture(gl.TEXTURE_2D, this.dynamicShadowMap?.texture ?? this.emptyTexture2D);
7928
- gl.uniform1i(u['uDynamicShadowMap'] ?? null, 2);
8220
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.sunShadows?.texture ?? this.emptyTexture2DArray);
8221
+ gl.uniform1i(u['uSunShadows'] ?? null, 0);
7929
8222
  }
7930
8223
  }
7931
8224