@driftengine/core 4.4.1 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/CHANGELOG.json +72 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +5 -1
  4. package/dist/index.js +7 -0
  5. package/dist/render/backend/api.d.ts +17 -11
  6. package/dist/render/backend/webgl2/glassTintPrograms.d.ts +13 -0
  7. package/dist/render/backend/webgl2/glassTintPrograms.js +38 -0
  8. package/dist/render/backend/webgl2/renderer.d.ts +82 -8
  9. package/dist/render/backend/webgl2/renderer.js +373 -105
  10. package/dist/render/backend/webgpu/buffers.d.ts +19 -1
  11. package/dist/render/backend/webgpu/buffers.js +27 -1
  12. package/dist/render/backend/webgpu/depthPass.d.ts +1 -1
  13. package/dist/render/backend/webgpu/depthPass.js +2 -1
  14. package/dist/render/backend/webgpu/flatPass.d.ts +2 -0
  15. package/dist/render/backend/webgpu/flatPass.js +20 -1
  16. package/dist/render/backend/webgpu/glassTintPass.d.ts +28 -0
  17. package/dist/render/backend/webgpu/glassTintPass.js +205 -0
  18. package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +60 -1
  19. package/dist/render/backend/webgpu/gpuDrivenPass.js +305 -5
  20. package/dist/render/backend/webgpu/instanced.d.ts +12 -1
  21. package/dist/render/backend/webgpu/instanced.js +20 -1
  22. package/dist/render/backend/webgpu/latePass.d.ts +26 -0
  23. package/dist/render/backend/webgpu/latePass.js +133 -0
  24. package/dist/render/backend/webgpu/lightVolumePass.js +5 -1
  25. package/dist/render/backend/webgpu/motionPass.d.ts +95 -0
  26. package/dist/render/backend/webgpu/motionPass.js +491 -0
  27. package/dist/render/backend/webgpu/pipelineCache.d.ts +10 -1
  28. package/dist/render/backend/webgpu/pipelineCache.js +11 -1
  29. package/dist/render/backend/webgpu/pointGlassTintPass.d.ts +32 -0
  30. package/dist/render/backend/webgpu/pointGlassTintPass.js +193 -0
  31. package/dist/render/backend/webgpu/pointShadowPass.d.ts +35 -6
  32. package/dist/render/backend/webgpu/pointShadowPass.js +105 -20
  33. package/dist/render/backend/webgpu/postPass.d.ts +21 -15
  34. package/dist/render/backend/webgpu/postPass.js +17 -4
  35. package/dist/render/backend/webgpu/renderer.d.ts +181 -54
  36. package/dist/render/backend/webgpu/renderer.js +1011 -394
  37. package/dist/render/backend/webgpu/scatterPass.js +2 -1
  38. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  39. package/dist/render/backend/webgpu/skinPaletteRing.js +4 -0
  40. package/dist/render/backend/webgpu/snapshotMips.d.ts +29 -0
  41. package/dist/render/backend/webgpu/snapshotMips.js +52 -0
  42. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +5 -0
  43. package/dist/render/backend/webgpu/surfaceTexturePass.js +1 -1
  44. package/dist/render/flameLight.js +15 -2
  45. package/dist/render/glass.d.ts +45 -0
  46. package/dist/render/glass.js +39 -0
  47. package/dist/render/glassCasters.d.ts +40 -0
  48. package/dist/render/glassCasters.js +84 -0
  49. package/dist/render/glassShadow.d.ts +78 -0
  50. package/dist/render/glassShadow.js +125 -0
  51. package/dist/render/globalMediumPass.d.ts +2 -3
  52. package/dist/render/globalMediumPass.js +2 -8
  53. package/dist/render/gpudriven/lit.d.ts +12 -0
  54. package/dist/render/gpudriven/lit.js +23 -0
  55. package/dist/render/gpudriven/materialTable.d.ts +15 -5
  56. package/dist/render/gpudriven/materialTable.js +26 -6
  57. package/dist/render/gpudriven/pipeline.d.ts +1 -1
  58. package/dist/render/gpudriven/pipeline.js +4 -3
  59. package/dist/render/gpudriven/sunGlass.d.ts +35 -0
  60. package/dist/render/gpudriven/sunGlass.js +89 -0
  61. package/dist/render/lightBudget.d.ts +17 -5
  62. package/dist/render/lightBudget.js +17 -5
  63. package/dist/render/pointGlassTint.d.ts +59 -0
  64. package/dist/render/pointGlassTint.js +198 -0
  65. package/dist/render/pointShadowArray.d.ts +56 -3
  66. package/dist/render/pointShadowArray.js +127 -16
  67. package/dist/render/pointShadowBudget.js +51 -41
  68. package/dist/render/pointShadowMap.d.ts +23 -1
  69. package/dist/render/pointShadowMap.js +27 -1
  70. package/dist/render/recon/changeFrames.d.ts +15 -0
  71. package/dist/render/recon/changeFrames.js +21 -0
  72. package/dist/render/recon/disocclusion.d.ts +36 -3
  73. package/dist/render/recon/disocclusion.js +56 -4
  74. package/dist/render/recon/lateRouting.d.ts +14 -0
  75. package/dist/render/recon/lateRouting.js +18 -0
  76. package/dist/render/recon/mover.d.ts +57 -0
  77. package/dist/render/recon/mover.js +86 -0
  78. package/dist/render/recon/resolve.js +95 -12
  79. package/dist/render/renderQuality.d.ts +35 -0
  80. package/dist/render/renderQuality.js +7 -0
  81. package/dist/render/rendererHarness.js +12 -0
  82. package/dist/render/sceneTarget.d.ts +9 -1
  83. package/dist/render/sceneTarget.js +59 -13
  84. package/dist/render/shaders/depth.d.ts +16 -1
  85. package/dist/render/shaders/depth.js +85 -9
  86. package/dist/render/shaders/flat/directionalShadow.js +91 -6
  87. package/dist/render/shaders/flat/glassTint.d.ts +6 -0
  88. package/dist/render/shaders/flat/glassTint.js +25 -0
  89. package/dist/render/shaders/flat/index.d.ts +13 -0
  90. package/dist/render/shaders/flat/index.js +4 -0
  91. package/dist/render/shaders/flat/lampGlass.d.ts +1 -0
  92. package/dist/render/shaders/flat/lampGlass.js +183 -0
  93. package/dist/render/shaders/flat/main.d.ts +1 -1
  94. package/dist/render/shaders/flat/main.js +115 -15
  95. package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
  96. package/dist/render/shaders/flat/pointShadow.js +2 -0
  97. package/dist/render/shaders/flat/preamble.js +21 -6
  98. package/dist/render/shaders/generated/depth.wgsl.d.ts +227 -4
  99. package/dist/render/shaders/generated/depth.wgsl.js +227 -4
  100. package/dist/render/shaders/generated/flat.wgsl.d.ts +360 -312
  101. package/dist/render/shaders/generated/flat.wgsl.js +376 -328
  102. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +3 -13
  103. package/dist/render/shaders/generated/globalMedium.wgsl.js +3 -13
  104. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +3 -13
  105. package/dist/render/shaders/generated/lightVolume.wgsl.js +4 -14
  106. package/dist/render/shaders/generated/octahedralResolve.wgsl.d.ts +29 -0
  107. package/dist/render/shaders/generated/octahedralResolve.wgsl.js +29 -0
  108. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +31 -6
  109. package/dist/render/shaders/generated/temporalResolve.wgsl.js +31 -6
  110. package/dist/render/shaders/globalMedium.js +6 -6
  111. package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +7 -0
  112. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +86 -1
  113. package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +1 -1
  114. package/dist/render/shaders/gpudriven/cull.wgsl.js +11 -2
  115. package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +1 -1
  116. package/dist/render/shaders/gpudriven/materialTable.wgsl.js +3 -1
  117. package/dist/render/shaders/gpudriven/shade.wgsl.js +50 -1
  118. package/dist/render/shaders/gpudriven/sunGlass.wgsl.d.ts +9 -0
  119. package/dist/render/shaders/gpudriven/sunGlass.wgsl.js +133 -0
  120. package/dist/render/shaders/lightVolume.js +6 -6
  121. package/dist/render/shaders/octahedralResolve.d.ts +8 -0
  122. package/dist/render/shaders/octahedralResolve.js +48 -17
  123. package/dist/render/shaders/recon/lateDepth.wgsl.d.ts +1 -0
  124. package/dist/render/shaders/recon/lateDepth.wgsl.js +44 -0
  125. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  126. package/dist/render/shaders/recon/motion.wgsl.js +84 -0
  127. package/dist/render/shaders/recon/resolve.wgsl.d.ts +1 -1
  128. package/dist/render/shaders/recon/resolve.wgsl.js +112 -8
  129. package/dist/render/shaders/temporalResolve.js +133 -5
  130. package/dist/render/shadowCasters.d.ts +20 -1
  131. package/dist/render/shadowCasters.js +15 -1
  132. package/dist/render/shadowMap.d.ts +54 -5
  133. package/dist/render/shadowMap.js +190 -36
  134. package/dist/render/sunGlassTint.d.ts +25 -0
  135. package/dist/render/sunGlassTint.js +79 -0
  136. package/dist/render/temporalAa.d.ts +145 -0
  137. package/dist/render/temporalAa.js +190 -0
  138. package/dist/render/temporalPass.d.ts +16 -14
  139. package/dist/render/temporalPass.js +59 -14
  140. package/dist/render/translucentQueue.d.ts +6 -0
  141. package/dist/render/translucentQueue.js +15 -1
  142. package/package.json +6 -6
  143. package/src/index.ts +10 -0
  144. package/src/render/backend/api.ts +15 -9
  145. package/src/render/backend/webgl2/glassTintPrograms.ts +58 -0
  146. package/src/render/backend/webgl2/renderer.ts +422 -109
  147. package/src/render/backend/webgpu/buffers.ts +50 -2
  148. package/src/render/backend/webgpu/depthPass.ts +2 -1
  149. package/src/render/backend/webgpu/flatPass.ts +22 -1
  150. package/src/render/backend/webgpu/glassTintPass.ts +244 -0
  151. package/src/render/backend/webgpu/gpuDrivenPass.ts +349 -5
  152. package/src/render/backend/webgpu/instanced.ts +21 -1
  153. package/src/render/backend/webgpu/latePass.ts +152 -0
  154. package/src/render/backend/webgpu/lightVolumePass.ts +5 -1
  155. package/src/render/backend/webgpu/motionPass.ts +535 -0
  156. package/src/render/backend/webgpu/pipelineCache.ts +12 -1
  157. package/src/render/backend/webgpu/pointGlassTintPass.ts +233 -0
  158. package/src/render/backend/webgpu/pointShadowPass.ts +158 -26
  159. package/src/render/backend/webgpu/postPass.ts +34 -4
  160. package/src/render/backend/webgpu/renderer.ts +1178 -425
  161. package/src/render/backend/webgpu/scatterPass.ts +2 -1
  162. package/src/render/backend/webgpu/skinPaletteRing.ts +4 -0
  163. package/src/render/backend/webgpu/snapshotMips.ts +80 -0
  164. package/src/render/backend/webgpu/surfaceTexturePass.ts +1 -1
  165. package/src/render/flameLight.ts +15 -2
  166. package/src/render/glass.ts +74 -0
  167. package/src/render/glassCasters.ts +164 -0
  168. package/src/render/glassShadow.ts +153 -0
  169. package/src/render/globalMediumPass.ts +4 -11
  170. package/src/render/gpudriven/lit.ts +31 -0
  171. package/src/render/gpudriven/materialTable.ts +28 -6
  172. package/src/render/gpudriven/pipeline.ts +4 -3
  173. package/src/render/gpudriven/sunGlass.ts +117 -0
  174. package/src/render/lightBudget.ts +19 -5
  175. package/src/render/pointGlassTint.ts +250 -0
  176. package/src/render/pointShadowArray.ts +188 -22
  177. package/src/render/pointShadowBudget.ts +49 -39
  178. package/src/render/pointShadowMap.ts +41 -0
  179. package/src/render/recon/changeFrames.ts +24 -0
  180. package/src/render/recon/disocclusion.ts +54 -3
  181. package/src/render/recon/lateRouting.ts +29 -0
  182. package/src/render/recon/mover.ts +118 -0
  183. package/src/render/recon/resolve.ts +137 -10
  184. package/src/render/renderQuality.ts +47 -0
  185. package/src/render/rendererHarness.ts +12 -0
  186. package/src/render/sceneTarget.ts +66 -12
  187. package/src/render/shaders/depth.ts +109 -9
  188. package/src/render/shaders/flat/directionalShadow.ts +97 -6
  189. package/src/render/shaders/flat/glassTint.ts +25 -0
  190. package/src/render/shaders/flat/index.ts +17 -0
  191. package/src/render/shaders/flat/lampGlass.ts +185 -0
  192. package/src/render/shaders/flat/main.ts +115 -15
  193. package/src/render/shaders/flat/pointShadow.ts +2 -0
  194. package/src/render/shaders/flat/preamble.ts +21 -6
  195. package/src/render/shaders/generated/depth.wgsl.ts +234 -4
  196. package/src/render/shaders/generated/flat.wgsl.ts +376 -328
  197. package/src/render/shaders/generated/globalMedium.wgsl.ts +3 -13
  198. package/src/render/shaders/generated/lightVolume.wgsl.ts +4 -14
  199. package/src/render/shaders/generated/octahedralResolve.wgsl.ts +30 -0
  200. package/src/render/shaders/generated/temporalResolve.wgsl.ts +31 -6
  201. package/src/render/shaders/globalMedium.ts +6 -6
  202. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +88 -1
  203. package/src/render/shaders/gpudriven/cull.wgsl.ts +11 -2
  204. package/src/render/shaders/gpudriven/materialTable.wgsl.ts +3 -1
  205. package/src/render/shaders/gpudriven/shade.wgsl.ts +50 -1
  206. package/src/render/shaders/gpudriven/sunGlass.wgsl.ts +139 -0
  207. package/src/render/shaders/lightVolume.ts +6 -6
  208. package/src/render/shaders/octahedralResolve.ts +50 -17
  209. package/src/render/shaders/recon/lateDepth.wgsl.ts +47 -0
  210. package/src/render/shaders/recon/motion.wgsl.ts +84 -0
  211. package/src/render/shaders/recon/resolve.wgsl.ts +112 -8
  212. package/src/render/shaders/temporalResolve.ts +141 -5
  213. package/src/render/shadowCasters.ts +28 -1
  214. package/src/render/shadowMap.ts +231 -37
  215. package/src/render/sunGlassTint.ts +86 -0
  216. package/src/render/temporalAa.ts +227 -0
  217. package/src/render/temporalPass.ts +88 -12
  218. package/src/render/translucentQueue.ts +20 -1
@@ -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;
@@ -1263,6 +1283,9 @@ export class WebGL2Renderer implements RendererApi {
1263
1283
  * Scratch, filled in `endFrame`, which may not allocate.
1264
1284
  */
1265
1285
  private frameProjection: ReadonlyMat4 | null = null;
1286
+ /** The temporal resolve's depth-to-metres row, from the inverse of `frameProjection`. */
1287
+ private readonly taaInvProjection = mat4.create();
1288
+ private readonly taaDepthToViewZ = Float32Array.of(0, 0, 0, 1);
1266
1289
  private readonly aoInvProjection = mat4.create();
1267
1290
  private readonly aoProjScale = new Float32Array(2);
1268
1291
  /**
@@ -1321,9 +1344,8 @@ export class WebGL2Renderer implements RendererApi {
1321
1344
  private readonly depthInstancedCutoutUniforms: Record<string, WebGLUniformLocation>;
1322
1345
  private readonly scatterDepthProgram: WebGLProgram;
1323
1346
  private readonly scatterDepthUniforms: Record<string, WebGLUniformLocation>;
1324
- private readonly shadowMap: ShadowMap | null;
1325
- private readonly peeledShadowMap: ShadowMap | null;
1326
- 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;
1327
1349
  private readonly pointShadows: PointShadowSystem<PointShadowMap> | null;
1328
1350
  /**
1329
1351
  * The layers a world's casting rectangles hold, above the point pool in the same array.
@@ -1548,7 +1570,7 @@ export class WebGL2Renderer implements RendererApi {
1548
1570
  /** Non-zero while the drawing buffer is pinned; see `lockDrawingBuffer`. */
1549
1571
  private lockedWidth = 0;
1550
1572
  private lockedHeight = 0;
1551
- private activeShadowMap: ShadowMap | null = null;
1573
+ private activeSunLayer: SunShadowLayer | null = null;
1552
1574
  private reflectionPassActive = false;
1553
1575
  private reflectionReadyThisFrame = false;
1554
1576
  private reflectionAtmosphereY = 0;
@@ -1572,7 +1594,7 @@ export class WebGL2Renderer implements RendererApi {
1572
1594
  *
1573
1595
  * **Not whether the map exists.** That was the test here, on the belief that a GL texture is
1574
1596
  * born reading as the far plane; it is born zeroed, and a scene that built the layer and never
1575
- * peeled it sampled an occluder at the light. `ShadowMap` now clears itself at birth, so an
1597
+ * peeled it sampled an occluder at the light. `SunShadowArray` clears itself at birth, so an
1576
1598
  * unfilled layer would read as lit either way — this is what keeps the two backends one
1577
1599
  * decision, and a frame that does not peel from paying a fetch per tap for nothing.
1578
1600
  */
@@ -1599,8 +1621,34 @@ export class WebGL2Renderer implements RendererApi {
1599
1621
  * Built once at construction, arrow properties closing over `this`: a sink created
1600
1622
  * per pass would allocate six objects per light per frame.
1601
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
+ };
1602
1643
  private readonly casterSink: ShadowCasterSink = {
1603
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
+ }
1604
1652
  this.shadowDrawBudget.ask();
1605
1653
  const { gl } = this;
1606
1654
  /* A two-sided caster casts from whichever face the light sees: nothing culled. */
@@ -1636,6 +1684,12 @@ export class WebGL2Renderer implements RendererApi {
1636
1684
  * columns the batch's own buffer supplies.
1637
1685
  */
1638
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
+ }
1639
1693
  const { gl } = this;
1640
1694
  const gpuBatch = batch as InstancedBatch;
1641
1695
  const count = Math.min(data.count, gpuBatch.capacity);
@@ -1662,7 +1716,13 @@ export class WebGL2Renderer implements RendererApi {
1662
1716
  if (twoSided && this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
1663
1717
  gl.useProgram(this.depthProgram);
1664
1718
  },
1665
- 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
+ }
1666
1726
  this.shadowDrawBudget.ask();
1667
1727
  const { gl } = this;
1668
1728
  const u = this.depthSkinnedUniforms;
@@ -1745,9 +1805,60 @@ export class WebGL2Renderer implements RendererApi {
1745
1805
  gl.useProgram(this.depthProgram);
1746
1806
  gl.uniform1i(this.depthUniforms['uPeelShadowLayer'] ?? null, 0);
1747
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();
1748
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
+ }
1820
+ };
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
+ },
1749
1855
  };
1750
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
+
1751
1862
  /**
1752
1863
  * Viewport in CSS pixels rather than device pixels.
1753
1864
  *
@@ -2713,20 +2824,18 @@ export class WebGL2Renderer implements RendererApi {
2713
2824
  gl.uniform1i(uniforms['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
2714
2825
  }
2715
2826
  gl.useProgram(null);
2716
- this.shadowMap = this.quality.directionalShadows
2717
- ? new ShadowMap(gl, this.quality.directionalShadowMapSize)
2718
- : null;
2719
- // A second static depth layer preserves both occluders where two world
2720
- // shadows overlap. It is depth-peeled from the first map below.
2721
- this.peeledShadowMap =
2722
- this.quality.directionalShadows && this.quality.directionalShadowDepthLayers > 1
2723
- ? new ShadowMap(gl, this.quality.directionalShadowMapSize)
2724
- : null;
2725
- // Movers get an independent layer. It preserves distance fading when a
2726
- // moving and a static caster overlap: their transmissions multiply rather
2727
- // than one depth replacing the other.
2728
- this.dynamicShadowMap = this.quality.directionalShadows
2729
- ? 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
+ )
2730
2839
  : null;
2731
2840
  // Static maps are allocated once the world reports its light count. No
2732
2841
  // cubemap resources exist at all when the quality profile disables them.
@@ -2918,9 +3027,9 @@ export class WebGL2Renderer implements RendererApi {
2918
3027
  * every one of them, per renderer, for as long as it lived; `renderer.test.ts` counts them.
2919
3028
  */
2920
3029
  this.sceneTarget?.dispose();
2921
- this.shadowMap?.dispose(gl);
2922
- this.peeledShadowMap?.dispose(gl);
2923
- this.dynamicShadowMap?.dispose(gl);
3030
+ this.sunShadows?.dispose(gl);
3031
+ this.sunGlassTint?.dispose(gl);
3032
+ this.glassTintPrograms?.dispose(gl);
2924
3033
  this.planarReflection?.dispose(gl);
2925
3034
  this.pointShadows?.dispose();
2926
3035
  gl.deleteVertexArray(this.panelVao);
@@ -3603,8 +3712,10 @@ export class WebGL2Renderer implements RendererApi {
3603
3712
  const lit = options.lit ?? true;
3604
3713
  const fog = options.fog ?? true;
3605
3714
  const toneMapped = options.toneMapped ?? true;
3606
- /* An instanced draw does not refract. See `materialChanges.ts`. */
3607
- 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 });
3608
3719
  if (own) this.materials.dirty();
3609
3720
  this.takeMaterial();
3610
3721
  const depthWrite = options.depthWrite ?? true;
@@ -3624,20 +3735,9 @@ export class WebGL2Renderer implements RendererApi {
3624
3735
  * **A null snapshot leaves the strength at zero**, so the draw shades as an ordinary
3625
3736
  * translucent one rather than sampling a black texture and painting the pane the colour of a
3626
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.
3627
3740
  */
3628
- const refraction = options.refraction ?? 0;
3629
- let refracting = false;
3630
- if (refraction > 0) {
3631
- const snapshot = this.sceneTarget?.snapshotColor() ?? null;
3632
- if (snapshot !== null) {
3633
- refracting = true;
3634
- gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
3635
- gl.bindTexture(gl.TEXTURE_2D, snapshot);
3636
- gl.uniform1f(u['uRefractStrength'] ?? null, refraction);
3637
- gl.uniform3fv(u['uRefractTint'] ?? null, options.refractTint ?? WHITE_TINT);
3638
- gl.uniform1f(u['uRefractThickness'] ?? null, options.thicknessM ?? 0);
3639
- }
3640
- }
3641
3741
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
3642
3742
  if (!fog) gl.uniform1i(u['uFogEnabled'] ?? null, 0);
3643
3743
  if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.gradeCode(), 1));
@@ -3655,7 +3755,11 @@ export class WebGL2Renderer implements RendererApi {
3655
3755
  gl.polygonOffset(offset.slope, offset.units);
3656
3756
  }
3657
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);
3658
3761
  mesh.drawInstances(gl, count);
3762
+ if (this.materialDoubleSided) gl.enable(gl.CULL_FACE);
3659
3763
 
3660
3764
  if (layer > 0) {
3661
3765
  gl.polygonOffset(0, 0);
@@ -3670,11 +3774,7 @@ export class WebGL2Renderer implements RendererApi {
3670
3774
  if (opacity < 1) gl.uniform1f(u['uOpacity'] ?? null, 1);
3671
3775
  /* The binding is released with the uniform, which is the 2026-08-27 rule: a WebGL2 binding
3672
3776
  outlives its frame, so a snapshot left on unit 14 is held against the next frame's copy. */
3673
- if (refracting) {
3674
- gl.uniform1f(u['uRefractStrength'] ?? null, 0);
3675
- gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
3676
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
3677
- }
3777
+ if (refracting) this.unbindSeeThrough(u);
3678
3778
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
3679
3779
  if (!fog) gl.uniform1i(u['uFogEnabled'] ?? null, 1);
3680
3780
  if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
@@ -3682,6 +3782,53 @@ export class WebGL2Renderer implements RendererApi {
3682
3782
  this.useFlatProgram();
3683
3783
  }
3684
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
+
3685
3832
  disposeMesh(mesh: Mesh): void {
3686
3833
  /*
3687
3834
  * Nothing to release when the context is gone: it took every buffer and VAO with it,
@@ -4350,6 +4497,13 @@ export class WebGL2Renderer implements RendererApi {
4350
4497
  */
4351
4498
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.pointShadowArray?.texture ?? this.emptyTexture2DArray);
4352
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);
4353
4507
 
4354
4508
  gl.uniform1iv(uniforms['uPointShadowLayer[0]'] ?? null, resolved.layers);
4355
4509
  /* Origin and far plane in one row; see `uPointShadowProjection` in the preamble. */
@@ -4723,21 +4877,17 @@ export class WebGL2Renderer implements RendererApi {
4723
4877
  lightViewProj: ReadonlyMat4,
4724
4878
  layer: 'static' | 'static-peel' | 'dynamic' = 'static',
4725
4879
  ): void {
4726
- const shadowMap =
4727
- layer === 'static'
4728
- ? this.shadowMap
4729
- : layer === 'static-peel'
4730
- ? this.peeledShadowMap
4731
- : this.dynamicShadowMap;
4732
- if (shadowMap === null) {
4880
+ const sun = this.sunShadows;
4881
+ if (sun === null || (layer === 'static-peel' && sun.layers < 3)) {
4733
4882
  this.shadowPassActive = false;
4734
- this.activeShadowMap = null;
4883
+ this.activeSunLayer = null;
4735
4884
  return;
4736
4885
  }
4737
4886
  const { gl } = this;
4738
4887
  this.gpuTimer.begin('shadows');
4739
4888
  this.shadowPassActive = true;
4740
- this.activeShadowMap = shadowMap;
4889
+ this.activeSunLayer = layer;
4890
+ this.glassCasters.clear();
4741
4891
  /* The static layer opens first, so a frame's peel counts from here. See `peelFilled`. */
4742
4892
  if (layer === 'static') this.peelFilled = false;
4743
4893
  /*
@@ -4764,16 +4914,16 @@ export class WebGL2Renderer implements RendererApi {
4764
4914
  // The previous mesh pass left all directional maps bound for sampling.
4765
4915
  // A texture cannot be sampled while it is attached to the active depth
4766
4916
  // target, even when the shader branch would skip that sample.
4767
- const targetUnit = layer === 'static' ? 0 : layer === 'static-peel' ? 1 : 2;
4768
- gl.activeTexture(gl.TEXTURE0 + targetUnit);
4769
- gl.bindTexture(gl.TEXTURE_2D, null);
4770
- shadowMap.begin(gl);
4917
+ gl.activeTexture(gl.TEXTURE0);
4918
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
4919
+ sun.begin(gl, layer);
4771
4920
  gl.useProgram(this.depthProgram);
4772
4921
  const peel = layer === 'static-peel';
4773
4922
  gl.uniform1i(this.depthUniforms['uPeelShadowLayer'] ?? null, peel ? 1 : 0);
4774
4923
  if (peel) {
4924
+ /* The static layer, read while the peel is drawn aside: see `SunShadowArray`. */
4775
4925
  gl.activeTexture(gl.TEXTURE0);
4776
- gl.bindTexture(gl.TEXTURE_2D, this.shadowMap?.texture ?? null);
4926
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, sun.texture);
4777
4927
  gl.uniform1i(this.depthUniforms['uPreviousShadowMap'] ?? null, 0);
4778
4928
  }
4779
4929
  /* The corrected one, matching `activeDepthViewProj` above: a pass must render its map with the
@@ -4885,18 +5035,165 @@ export class WebGL2Renderer implements RendererApi {
4885
5035
  casters(this.casterSink);
4886
5036
  }
4887
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
+
4888
5182
  endShadowPass(): void {
4889
5183
  if (this.contextLost) return;
4890
- const shadowMap = this.activeShadowMap;
4891
- if (!this.shadowPassActive || shadowMap === null) return;
4892
- shadowMap.end(this.gl);
4893
- 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');
4894
5191
  /* Back to the frame's sense; see `beginShadowPass` for why it left it. */
4895
5192
  this.gl.depthFunc(glDepthFuncEqual(this.gl));
4896
5193
  this.gl.clearDepth(this.depthClear);
4897
5194
  this.gpuTimer.end();
4898
5195
  this.shadowPassActive = false;
4899
- this.activeShadowMap = null;
5196
+ this.activeSunLayer = null;
4900
5197
  // Restore the drawing-buffer viewport the shadow pass overrode.
4901
5198
  this.restoreViewport();
4902
5199
  }
@@ -4935,6 +5232,8 @@ export class WebGL2Renderer implements RendererApi {
4935
5232
  directionalShadows: this.quality.directionalShadows,
4936
5233
  environmentProbe: this.probeCapture !== null,
4937
5234
  nightEmissive: this.quality.nightEmissive,
5235
+ /* Off compiles none of the glass lookup: see `FlatShaderOptions.glassShadows`. */
5236
+ glassShadows: this.quality.glassShadows !== 'off',
4938
5237
  };
4939
5238
  const build = (budget: LightBudget): string =>
4940
5239
  flatFrag({ ...variant, maxLights: budget.maxLights, maxAreaLights: budget.maxAreaLights });
@@ -5251,6 +5550,8 @@ export class WebGL2Renderer implements RendererApi {
5251
5550
  this.bakeOne(map, light, casters as ShadowCasters, maxFaces),
5252
5551
  );
5253
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);
5254
5555
  this.restoreViewport();
5255
5556
  }
5256
5557
 
@@ -5311,6 +5612,10 @@ export class WebGL2Renderer implements RendererApi {
5311
5612
  * reason: it already holds everything it needs.
5312
5613
  */
5313
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);
5314
5619
  }
5315
5620
 
5316
5621
  /**
@@ -5375,6 +5680,7 @@ export class WebGL2Renderer implements RendererApi {
5375
5680
  near,
5376
5681
  DEFAULT_SOURCE_RADIUS,
5377
5682
  maxFaces,
5683
+ this.pointGlass,
5378
5684
  ),
5379
5685
  );
5380
5686
  }
@@ -5398,6 +5704,7 @@ export class WebGL2Renderer implements RendererApi {
5398
5704
  light.shadowNear,
5399
5705
  light.sourceRadius,
5400
5706
  maxFaces,
5707
+ this.pointGlass,
5401
5708
  ),
5402
5709
  );
5403
5710
  }
@@ -5632,6 +5939,10 @@ export class WebGL2Renderer implements RendererApi {
5632
5939
  *
5633
5940
  * What it gives up is one frame of history: the frame after a cut has no blur and no temporal
5634
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.
5635
5946
  */
5636
5947
  cameraCut(): void {
5637
5948
  this.hasPreviousView = false;
@@ -6193,7 +6504,14 @@ export class WebGL2Renderer implements RendererApi {
6193
6504
  const view = this.frameViewProj;
6194
6505
  const blur = this.quality.cameraMotionBlur * this.motionBlurScale;
6195
6506
  let motion: { reprojection: Float32Array; strength: number; max: number } | undefined;
6196
- let temporal: { reprojection: Float32Array; blend: number } | undefined;
6507
+ let temporal:
6508
+ | {
6509
+ reprojection: Float32Array;
6510
+ blend: number;
6511
+ periodStart: boolean;
6512
+ depthToViewZ: Float32Array;
6513
+ }
6514
+ | undefined;
6197
6515
  /*
6198
6516
  * **Both effects want the same one matrix**, so it is built once when either asks. The
6199
6517
  * reprojection was previously kept only while the blur was on, which is why this reads as it
@@ -6222,9 +6540,19 @@ export class WebGL2Renderer implements RendererApi {
6222
6540
  * its early exit on it — so this is present every jittered frame rather than absent on some,
6223
6541
  * and the pass still runs and fills the history for the next one.
6224
6542
  */
6543
+ /* The anti-flicker's same-surface test reads depths in metres, as the occlusion does. */
6544
+ if (this.frameProjection !== null) {
6545
+ mat4.invert(this.taaInvProjection, this.frameProjection);
6546
+ this.taaDepthToViewZ[0] = this.taaInvProjection[10] ?? 0;
6547
+ this.taaDepthToViewZ[1] = this.taaInvProjection[14] ?? 0;
6548
+ this.taaDepthToViewZ[2] = this.taaInvProjection[11] ?? 0;
6549
+ this.taaDepthToViewZ[3] = this.taaInvProjection[15] ?? 1;
6550
+ }
6225
6551
  temporal = {
6226
6552
  reprojection: this.reprojection as Float32Array,
6227
6553
  blend: this.temporalHistoryUsable ? TEMPORAL_HISTORY_BLEND : 0,
6554
+ periodStart: this.temporalHistory.periodStart,
6555
+ depthToViewZ: this.taaDepthToViewZ,
6228
6556
  };
6229
6557
  }
6230
6558
 
@@ -6405,7 +6733,9 @@ export class WebGL2Renderer implements RendererApi {
6405
6733
  * flat paint with the effect on. The opaque frame is finished here, which is what a pane
6406
6734
  * should show; the latch then serves every pane in the replay. Only when something refracts.
6407
6735
  */
6408
- if (this.translucentQueue.refracts) this.sceneTarget?.snapshotColor();
6736
+ if (this.translucentQueue.refracts) {
6737
+ this.sceneTarget?.snapshotColor(false, this.translucentQueue.glassy);
6738
+ }
6409
6739
  this.oit.accumulateOit(this.canvas.width, this.canvas.height, oitDepth, (weighted) => {
6410
6740
  this.oitReplaying = true;
6411
6741
  this.useFlatProgram();
@@ -6422,6 +6752,7 @@ export class WebGL2Renderer implements RendererApi {
6422
6752
  options.refraction = draw.refraction;
6423
6753
  options.refractTint = draw.refractTint as unknown as Vec3 | undefined;
6424
6754
  options.thicknessM = draw.thicknessM;
6755
+ options.glass = draw.glass ?? undefined;
6425
6756
  this.drawTranslucentMesh(draw.mesh as Mesh, draw.model, draw.opacity, options);
6426
6757
  });
6427
6758
  /* Back to zero for every other draw in the frame, exactly as `uOpacity` and `uTint` are. */
@@ -6484,9 +6815,7 @@ export class WebGL2Renderer implements RendererApi {
6484
6815
  /* The placeholder rather than null where a map is merely absent, for the reason
6485
6816
  `emptyTexture.ts` gives. It is black, which as a depth is an occluder at the light,
6486
6817
  so it is safe only because `sunShadow` is 0 whenever these maps do not exist. */
6487
- staticShadowMap: this.shadowMap?.texture ?? this.emptyTexture2D,
6488
- peeledShadowMap: this.peeledShadowMap?.texture ?? this.emptyTexture2D,
6489
- dynamicShadowMap: this.dynamicShadowMap?.texture ?? this.emptyTexture2D,
6818
+ sunShadows: this.sunShadows?.texture ?? this.emptyTexture2DArray,
6490
6819
  peeledEnabled: this.peelFilled,
6491
6820
  },
6492
6821
  );
@@ -6833,9 +7162,11 @@ export class WebGL2Renderer implements RendererApi {
6833
7162
  gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
6834
7163
  gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6835
7164
  gl.uniform1i(u['uRefractScene'] ?? null, REFRACT_SCENE_TEXTURE_UNIT);
6836
- 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);
6837
7168
  gl.uniform3fv(u['uRefractTint'] ?? null, WHITE_TINT);
6838
- gl.uniform1f(u['uRefractThickness'] ?? null, 0);
7169
+ gl.uniform3fv(u['uGlassTint'] ?? null, WHITE_TINT);
6839
7170
  gl.uniform3fv(u['uDirectionalDir'] ?? null, env.directionalDir);
6840
7171
  gl.uniform3fv(u['uDirectionalColor'] ?? null, env.directionalColor);
6841
7172
  gl.uniform3fv(u['uAmbient'] ?? null, env.ambient);
@@ -6937,7 +7268,7 @@ export class WebGL2Renderer implements RendererApi {
6937
7268
  * its own copy of this lookup; the two backends have to project the same way.
6938
7269
  */
6939
7270
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, env.lightViewProj);
6940
- gl.uniform1f(u['uShadowStrength'] ?? null, this.shadowMap === null ? 0 : env.shadowStrength);
7271
+ gl.uniform1f(u['uShadowStrength'] ?? null, this.sunShadows === null ? 0 : env.shadowStrength);
6941
7272
  gl.uniform1f(u['uShadowMapSize'] ?? null, this.quality.directionalShadowMapSize);
6942
7273
  gl.uniform1f(u['uShadowDepthSpan'] ?? null, env.shadowDepthSpan);
6943
7274
  gl.uniform1f(u['uShadowMaxDistance'] ?? null, this.quality.directionalShadowMaxDistance);
@@ -6964,16 +7295,12 @@ export class WebGL2Renderer implements RendererApi {
6964
7295
  */
6965
7296
  if (this.quality.directionalShadows) {
6966
7297
  gl.activeTexture(gl.TEXTURE0);
6967
- gl.bindTexture(gl.TEXTURE_2D, this.shadowMap?.texture ?? this.emptyTexture2D);
6968
- gl.uniform1i(u['uStaticShadowMap'] ?? null, 0);
6969
-
6970
- gl.activeTexture(gl.TEXTURE1);
6971
- gl.bindTexture(gl.TEXTURE_2D, this.peeledShadowMap?.texture ?? this.emptyTexture2D);
6972
- gl.uniform1i(u['uPeeledShadowMap'] ?? null, 1);
6973
-
6974
- gl.activeTexture(gl.TEXTURE2);
6975
- gl.bindTexture(gl.TEXTURE_2D, this.dynamicShadowMap?.texture ?? this.emptyTexture2D);
6976
- 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);
6977
7304
  }
6978
7305
 
6979
7306
  if (this.pointShadows === null) {
@@ -6986,6 +7313,10 @@ export class WebGL2Renderer implements RendererApi {
6986
7313
  } else {
6987
7314
  /* Chosen in `pointShadowSystem.ts` and bound here; see `resolve` for the split. */
6988
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);
6989
7320
  this.bindPointShadows(u, DIRECTIONAL_SHADOW_UNITS);
6990
7321
  /*
6991
7322
  * The rectangles' layers, into the array `bindPointShadows` just put on its unit. Resolved
@@ -7203,13 +7534,14 @@ export class WebGL2Renderer implements RendererApi {
7203
7534
  */
7204
7535
  tint: Vec3 | null = null,
7205
7536
  /**
7206
- * 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.
7207
7538
  *
7208
7539
  * Accepted rather than absent so that one scene draws through both renderers unchanged — the
7209
7540
  * parity rule this repository is built on. Reconstruction is WebGPU's, having no compute stage
7210
- * 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.
7211
7543
  */
7212
- _previousModel: ReadonlyMat4 | null = null,
7544
+ _previousModel: ReadonlyMat4 | Mover | null = null,
7213
7545
  ): void {
7214
7546
  if (this.contextLost) return;
7215
7547
  /* Geometry that has not all arrived is not drawn. See `Mesh.complete`. */
@@ -7454,19 +7786,7 @@ export class WebGL2Renderer implements RendererApi {
7454
7786
  * translucent one rather than sampling a black texture and painting the pane the colour of a
7455
7787
  * hole. That is the defined state the two-backends rule asks for instead of a silent no-op.
7456
7788
  */
7457
- const refraction = options.refraction ?? 0;
7458
- let refracting = false;
7459
- if (refraction > 0) {
7460
- const snapshot = this.sceneTarget?.snapshotColor() ?? null;
7461
- if (snapshot !== null) {
7462
- refracting = true;
7463
- gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
7464
- gl.bindTexture(gl.TEXTURE_2D, snapshot);
7465
- gl.uniform1f(u['uRefractStrength'] ?? null, refraction);
7466
- gl.uniform3fv(u['uRefractTint'] ?? null, options.refractTint ?? WHITE_TINT);
7467
- gl.uniform1f(u['uRefractThickness'] ?? null, options.thicknessM ?? 0);
7468
- }
7469
- }
7789
+ const refracting = this.bindSeeThrough(u, options);
7470
7790
  /*
7471
7791
  * **The blend and the depth mask belong to whoever owns the pass.** In the ordinary case that
7472
7792
  * is this draw; during an order-independent replay it is `OitPass`, which has set an additive
@@ -7509,6 +7829,7 @@ export class WebGL2Renderer implements RendererApi {
7509
7829
  if (ownsState) gl.disable(gl.BLEND);
7510
7830
  gl.uniform1f(u['uOpacity'] ?? null, 1);
7511
7831
  if (tint !== null) gl.uniform3fv(u['uTint'] ?? null, WHITE_TINT);
7832
+ if (refracting) this.unbindSeeThrough(u);
7512
7833
  /* Put back for whatever is drawn next, exactly as `uOpacity` and `uTint` are — a pass
7513
7834
  cannot inherit a material from the draw before it. */
7514
7835
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
@@ -7896,16 +8217,8 @@ export class WebGL2Renderer implements RendererApi {
7896
8217
  gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peelFilled ? 1 : 0);
7897
8218
 
7898
8219
  gl.activeTexture(gl.TEXTURE0);
7899
- gl.bindTexture(gl.TEXTURE_2D, this.shadowMap?.texture ?? this.emptyTexture2D);
7900
- gl.uniform1i(u['uStaticShadowMap'] ?? null, 0);
7901
-
7902
- gl.activeTexture(gl.TEXTURE1);
7903
- gl.bindTexture(gl.TEXTURE_2D, this.peeledShadowMap?.texture ?? this.emptyTexture2D);
7904
- gl.uniform1i(u['uPeeledShadowMap'] ?? null, 1);
7905
-
7906
- gl.activeTexture(gl.TEXTURE2);
7907
- gl.bindTexture(gl.TEXTURE_2D, this.dynamicShadowMap?.texture ?? this.emptyTexture2D);
7908
- 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);
7909
8222
  }
7910
8223
  }
7911
8224