@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
@@ -56,6 +56,7 @@ export const DEFAULT_RENDER_QUALITY = Object.freeze({
56
56
  shadowFilterTaps: MAX_SHADOW_FILTER_TAPS,
57
57
  lightVolumeSamples: 32,
58
58
  directionalShadowDepthLayers: 2,
59
+ glassShadows: 'full',
59
60
  directionalShadowMaxDistance: 6,
60
61
  directionalShadowMaxSlope: 3,
61
62
  pointShadowRebakeDistance: 0.001,
@@ -205,6 +206,7 @@ export function resolveRenderQuality(options = {}) {
205
206
  shadowFilterTaps: options.shadowFilterTaps ?? DEFAULT_RENDER_QUALITY.shadowFilterTaps,
206
207
  lightVolumeSamples: options.lightVolumeSamples ?? DEFAULT_RENDER_QUALITY.lightVolumeSamples,
207
208
  directionalShadowDepthLayers: options.directionalShadowDepthLayers ?? DEFAULT_RENDER_QUALITY.directionalShadowDepthLayers,
209
+ glassShadows: options.glassShadows ?? DEFAULT_RENDER_QUALITY.glassShadows,
208
210
  directionalShadowMaxDistance: options.directionalShadowMaxDistance ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxDistance,
209
211
  directionalShadowMaxSlope: options.directionalShadowMaxSlope ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxSlope,
210
212
  pointShadowRebakeDistance: options.pointShadowRebakeDistance ?? DEFAULT_RENDER_QUALITY.pointShadowRebakeDistance,
@@ -286,6 +288,11 @@ export function resolveRenderQuality(options = {}) {
286
288
  if (quality.directionalShadowDepthLayers !== 1 && quality.directionalShadowDepthLayers !== 2) {
287
289
  throw new Error(`RenderQuality.directionalShadowDepthLayers must be 1 or 2, got ${quality.directionalShadowDepthLayers}`);
288
290
  }
291
+ if (quality.glassShadows !== 'full' &&
292
+ quality.glassShadows !== 'half' &&
293
+ quality.glassShadows !== 'off') {
294
+ throw new Error(`RenderQuality.glassShadows must be 'full', 'half' or 'off', got ${String(quality.glassShadows)}`);
295
+ }
289
296
  if (quality.plumeNoiseOctaves > 3) {
290
297
  throw new Error(`RenderQuality.plumeNoiseOctaves must be <= 3, got ${quality.plumeNoiseOctaves}`);
291
298
  }
@@ -86,6 +86,18 @@ export function recordingGl(options = {}) {
86
86
  COLOR_BUFFER_BIT: 0x00004000,
87
87
  /* Named so a test can tell a mesh that declared itself deforming from one that did not. */
88
88
  ARRAY_BUFFER: 0x8892,
89
+ /* Named so a blend a pass sets can be read back as numbers: glass multiplies what it lets
90
+ through, and a test that cannot tell DST_COLOR from a recorder cannot tell it multiplies. */
91
+ ZERO: 0,
92
+ DST_ALPHA: 0x0304,
93
+ DST_COLOR: 0x0306,
94
+ FUNC_ADD: 0x8006,
95
+ COLOR: 0x1800,
96
+ DEPTH: 0x1801,
97
+ /* Named so a copy can be read as from-and-to: a blit's two framebuffers are the whole claim. */
98
+ READ_FRAMEBUFFER: 0x8ca8,
99
+ DRAW_FRAMEBUFFER: 0x8ca9,
100
+ BLEND: 0x0be2,
89
101
  STATIC_DRAW: 0x88e4,
90
102
  DYNAMIC_DRAW: 0x88e8,
91
103
  };
@@ -72,6 +72,8 @@ export declare class SceneTarget {
72
72
  private colorCopyFramebuffer;
73
73
  /** Whether this frame has filled it. One snapshot serves every refracting draw in a frame. */
74
74
  private colorCopyFilled;
75
+ /** Whether this frame's copy has its mip chain, which frosted glass reads. See `snapshotColor`. */
76
+ private colorCopyMips;
75
77
  /**
76
78
  * The colour grade, as a 3D texture on unit 4, and the table it was built from.
77
79
  *
@@ -221,7 +223,11 @@ export declare class SceneTarget {
221
223
  * Null where the copy could not be made, and a caller must read that as "do not refract" rather
222
224
  * than as a black texture — refracting against black paints every pane the colour of a hole.
223
225
  */
224
- snapshotColor(afresh?: boolean): WebGLTexture | null;
226
+ snapshotColor(afresh?: boolean,
227
+ /** Fill the copy's mip chain as well, once a frame: what frosted glass reads. */
228
+ mips?: boolean): WebGLTexture | null;
229
+ /** The copy's mip chain from its level 0, once per copy. */
230
+ private fillColorCopyMips;
225
231
  snapshotDepth(
226
232
  /**
227
233
  * Take the copy again even though one has already been taken this frame.
@@ -76,6 +76,8 @@ export class SceneTarget {
76
76
  colorCopyFramebuffer = null;
77
77
  /** Whether this frame has filled it. One snapshot serves every refracting draw in a frame. */
78
78
  colorCopyFilled = false;
79
+ /** Whether this frame's copy has its mip chain, which frosted glass reads. See `snapshotColor`. */
80
+ colorCopyMips = false;
79
81
  /**
80
82
  * The colour grade, as a 3D texture on unit 4, and the table it was built from.
81
83
  *
@@ -241,13 +243,25 @@ export class SceneTarget {
241
243
  if (this.colorCopy === null)
242
244
  this.colorCopy = gl.createTexture();
243
245
  gl.bindTexture(gl.TEXTURE_2D, this.colorCopy);
244
- if (this.floatColor) {
245
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.HALF_FLOAT, null);
246
- }
247
- else {
248
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
246
+ /*
247
+ * **Every level, so glass can read a blurrier one.** Frosted glass reads the copy at a level its
248
+ * frost picks, and a texture sampled through a mipmapping filter with a level missing is
249
+ * incomplete and reads black — so the chain is allocated here, once per size, and filled only on
250
+ * a frame that draws glass (`snapshotColor`). Plain refraction reads level 0, as it always has.
251
+ */
252
+ const levels = Math.floor(Math.log2(Math.max(width, height))) + 1;
253
+ for (let level = 0; level < levels; level += 1) {
254
+ const w = Math.max(1, width >> level);
255
+ const h = Math.max(1, height >> level);
256
+ if (this.floatColor) {
257
+ gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA16F, w, h, 0, gl.RGBA, gl.HALF_FLOAT, null);
258
+ }
259
+ else {
260
+ gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
261
+ }
249
262
  }
250
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
263
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, levels - 1);
264
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
251
265
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
252
266
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
253
267
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
@@ -359,6 +373,7 @@ export class SceneTarget {
359
373
  /* Last frame's snapshot is last frame's scene. See `snapshotDepth`. */
360
374
  this.depthCopyFilled = false;
361
375
  this.colorCopyFilled = false;
376
+ this.colorCopyMips = false;
362
377
  this.bind();
363
378
  }
364
379
  /**
@@ -449,15 +464,22 @@ export class SceneTarget {
449
464
  * Null where the copy could not be made, and a caller must read that as "do not refract" rather
450
465
  * than as a black texture — refracting against black paints every pane the colour of a hole.
451
466
  */
452
- snapshotColor(afresh = false) {
467
+ snapshotColor(afresh = false,
468
+ /** Fill the copy's mip chain as well, once a frame: what frosted glass reads. */
469
+ mips = false) {
453
470
  if (this.colorCopy === null || this.colorCopyFramebuffer === null)
454
471
  return null;
455
- if (this.colorCopyFilled && !afresh)
472
+ if (this.colorCopyFilled && !afresh) {
473
+ if (mips)
474
+ this.fillColorCopyMips();
456
475
  return this.colorCopy;
476
+ }
457
477
  const { gl } = this;
458
478
  const source = this.msaaFramebuffer ?? this.framebuffer;
459
479
  if (source === null)
460
480
  return null;
481
+ const drawing = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
482
+ const reading = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING);
461
483
  gl.bindFramebuffer(gl.READ_FRAMEBUFFER, source);
462
484
  gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.colorCopyFramebuffer);
463
485
  /* Drained first, for the reason `resolve` gives at length: `getError` reports whatever is
@@ -470,13 +492,34 @@ export class SceneTarget {
470
492
  choice only decides which refusals are possible. */
471
493
  gl.blitFramebuffer(0, 0, this.width, this.height, 0, 0, this.width, this.height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
472
494
  const refused = gl.getError() !== gl.NO_ERROR;
473
- /* Back to what the caller was drawing into, whichever that was. */
474
- gl.bindFramebuffer(gl.FRAMEBUFFER, source);
495
+ /*
496
+ * **Back to what the caller was drawing into, whichever that was** — read before the copy and
497
+ * handed back, never assumed. It was the scene's framebuffer, assumed, and a pane drawn into a
498
+ * reflection probe's face then left the scene's bound: the probe attached its next face to it,
499
+ * a face-sized colour beside a screen-sized depth, and every draw after refused as incomplete.
500
+ */
501
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, drawing);
502
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, reading);
475
503
  if (refused)
476
504
  return null;
477
505
  this.colorCopyFilled = true;
506
+ this.colorCopyMips = false;
507
+ if (mips)
508
+ this.fillColorCopyMips();
478
509
  return this.colorCopy;
479
510
  }
511
+ /** The copy's mip chain from its level 0, once per copy. */
512
+ fillColorCopyMips() {
513
+ if (this.colorCopyMips || this.colorCopy === null)
514
+ return;
515
+ const { gl } = this;
516
+ /* Bound on whichever unit is active and handed back, so the caller's texture there survives. */
517
+ const previous = gl.getParameter(gl.TEXTURE_BINDING_2D);
518
+ gl.bindTexture(gl.TEXTURE_2D, this.colorCopy);
519
+ gl.generateMipmap(gl.TEXTURE_2D);
520
+ gl.bindTexture(gl.TEXTURE_2D, previous);
521
+ this.colorCopyMips = true;
522
+ }
480
523
  snapshotDepth(
481
524
  /**
482
525
  * Take the copy again even though one has already been taken this frame.
@@ -499,6 +542,8 @@ export class SceneTarget {
499
542
  const source = this.msaaFramebuffer ?? this.framebuffer;
500
543
  if (source === null)
501
544
  return null;
545
+ const drawing = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
546
+ const reading = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING);
502
547
  gl.bindFramebuffer(gl.READ_FRAMEBUFFER, source);
503
548
  gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.depthCopyFramebuffer);
504
549
  /* Drained first, for the reason `resolve` gives at length: `getError` reports whatever is
@@ -509,8 +554,9 @@ export class SceneTarget {
509
554
  }
510
555
  gl.blitFramebuffer(0, 0, this.width, this.height, 0, 0, this.width, this.height, gl.DEPTH_BUFFER_BIT, gl.NEAREST);
511
556
  const refused = gl.getError() !== gl.NO_ERROR;
512
- /* Back to what the caller was drawing into, whichever that was. */
513
- gl.bindFramebuffer(gl.FRAMEBUFFER, source);
557
+ /* Back to what the caller was drawing into, read rather than assumed: see `snapshotColor`. */
558
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, drawing);
559
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, reading);
514
560
  if (refused)
515
561
  return null;
516
562
  this.depthCopyFilled = true;
@@ -23,7 +23,7 @@ export declare const DEPTH_INSTANCED_VERT: string;
23
23
  */
24
24
  export declare const DEPTH_CUTOUT_VERT: string;
25
25
  export declare const DEPTH_INSTANCED_CUTOUT_VERT: string;
26
- export declare const DEPTH_FRAG = "#version 300 es\nprecision highp float;\nin vec4 vLightPosition;\n\nuniform highp sampler2D uPreviousShadowMap;\nuniform int uPeelShadowLayer;\n\nvoid main() {\n if (uPeelShadowLayer != 0) {\n vec3 p = vLightPosition.xyz / vLightPosition.w;\n vec2 uv = p.xy * 0.5 + 0.5;\n float previousDepth = texture(uPreviousShadowMap, uv).r;\n // Discard the already-recorded surface and everything in front of it. The\n // ordinary depth test then stores the next independently fading occluder.\n if (gl_FragCoord.z <= previousDepth + 0.00001) discard;\n }\n}\n";
26
+ export declare const DEPTH_FRAG = "#version 300 es\nprecision highp float;\nin vec4 vLightPosition;\n\nuniform highp sampler2DArray uPreviousShadowMap;\nuniform int uPeelShadowLayer;\n\nvoid main() {\n if (uPeelShadowLayer != 0) {\n vec3 p = vLightPosition.xyz / vLightPosition.w;\n vec2 uv = p.xy * 0.5 + 0.5;\n float previousDepth = texture(uPreviousShadowMap, vec3(uv, 0.0)).r;\n // Discard the already-recorded surface and everything in front of it. The\n // ordinary depth test then stores the next independently fading occluder.\n if (gl_FragCoord.z <= previousDepth + 0.00001) discard;\n }\n}\n";
27
27
  /**
28
28
  * The cutout caster's depth: the plain pass, and a texel of alpha under the cutoff writes nothing.
29
29
  *
@@ -37,3 +37,18 @@ export declare const DEPTH_FRAG = "#version 300 es\nprecision highp float;\nin v
37
37
  * map takes the next.
38
38
  */
39
39
  export declare const DEPTH_CUTOUT_FRAG: string;
40
+ /**
41
+ * The glass tint's vertex stages: every way a caster is drawn, with the world position handed on.
42
+ *
43
+ * **The same five as the depth pass, and for the same reason**: a pane must cast its colour from
44
+ * exactly where it casts its depth, deformed and placed the same way, or the coloured patch parts
45
+ * from the pane's own shadow edge. The only addition is `vGlassWorld`.
46
+ */
47
+ export declare const GLASS_TINT_VERT: string;
48
+ export declare const GLASS_TINT_SKINNED_VERT: string;
49
+ export declare const GLASS_TINT_INSTANCED_VERT: string;
50
+ export declare const GLASS_TINT_CUTOUT_VERT: string;
51
+ export declare const GLASS_TINT_INSTANCED_CUTOUT_VERT: string;
52
+ export declare const GLASS_TINT_FRAG = "#version 300 es\nprecision highp float;\nin vec4 vLightPosition;\nin vec3 vGlassWorld;\n\n/* rgb: transmission times tint; a: clarity, one less the frost. */\nuniform vec4 uGlassPane;\n/* xyz: the light's position (w 1) or the direction toward it (w 0). */\nuniform vec4 uGlassLight;\n\nout vec4 outTint;\n\nvoid main() {\n\n vec3 n = normalize(cross(dFdx(vGlassWorld), dFdy(vGlassWorld)));\n vec3 toLight = uGlassLight.w > 0.5 ? normalize(uGlassLight.xyz - vGlassWorld) : uGlassLight.xyz;\n float cosLight = abs(dot(n, toLight));\n float reflected = 0.04 + 0.96 * pow(1.0 - cosLight, 5.0);\n outTint = vec4(uGlassPane.rgb * (1.0 - reflected), uGlassPane.a);\n\n}\n";
53
+ /** A cut-out pane: its colour lands only where its depth does, by the depth cutout's own test. */
54
+ export declare const GLASS_TINT_CUTOUT_FRAG: string;
@@ -1,4 +1,5 @@
1
1
  import { resolveConditionals } from './conditionals.js';
2
+ import { SUN_STATIC_LAYER } from '../shadowMap.js';
2
3
  import { CUTOUT_COVERAGE_GLSL } from './cutoutCoverage.js';
3
4
  import { SKINNING_GLSL } from './skinning.js';
4
5
  import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from './vertexChannel.js';
@@ -72,6 +73,11 @@ out vec4 vLightPosition;
72
73
  out vec2 vUv;
73
74
  flat out float vAlphaCutout;
74
75
  #endif
76
+ #if GLASS
77
+ /* Where on the pane this is, for the tint's own normal and its angle to the light. Last, so the
78
+ outputs before it keep the locations the plain variants give them. */
79
+ out vec3 vGlassWorld;
80
+ #endif
75
81
 
76
82
  void main() {
77
83
  #if SKINNED
@@ -99,12 +105,15 @@ void main() {
99
105
  vUv = aUv * uUvScale;
100
106
  vAlphaCutout = uAlphaCutout;
101
107
  #endif
108
+ #if GLASS
109
+ vGlassWorld = bent;
110
+ #endif
102
111
  }
103
112
  `;
104
113
  /** The rigid variant: the world, a prop, a mesh with no rig behind it. */
105
- export const DEPTH_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: false }, 'depth');
114
+ export const DEPTH_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: false, GLASS: false }, 'depth');
106
115
  /** The skinned variant, which reads a joint palette and moves the vertex by it. */
107
- export const DEPTH_SKINNED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: true, INSTANCED: false, CUTOUT: false }, 'depth-skinned');
116
+ export const DEPTH_SKINNED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: true, INSTANCED: false, CUTOUT: false, GLASS: false }, 'depth-skinned');
108
117
  /**
109
118
  * The variant an instanced draw casts its shadow through.
110
119
  *
@@ -113,7 +122,7 @@ export const DEPTH_SKINNED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINN
113
122
  * right place and the rest stacked underneath it, which reads as a lighting fault rather than as
114
123
  * a missing variant, and is the shape the per-draw ring exists to prevent one level up.
115
124
  */
116
- export const DEPTH_INSTANCED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: false }, 'depth-instanced');
125
+ export const DEPTH_INSTANCED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: false, GLASS: false }, 'depth-instanced');
117
126
  /**
118
127
  * The variants a cutout caster casts through: a leaf card, a chain link, a fence, whose shape is in
119
128
  * its texture's alpha rather than in its geometry.
@@ -124,20 +133,20 @@ export const DEPTH_INSTANCED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKI
124
133
  * leaves every opaque caster byte for byte as it was. What it costs is one more program each, and
125
134
  * a skinned cutout, which nothing has asked for, is not among them.
126
135
  */
127
- export const DEPTH_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: true }, 'depth-cutout');
128
- export const DEPTH_INSTANCED_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: true }, 'depth-instanced-cutout');
136
+ export const DEPTH_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: true, GLASS: false }, 'depth-cutout');
137
+ export const DEPTH_INSTANCED_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: true, GLASS: false }, 'depth-instanced-cutout');
129
138
  export const DEPTH_FRAG = `#version 300 es
130
139
  precision highp float;
131
140
  in vec4 vLightPosition;
132
141
 
133
- uniform highp sampler2D uPreviousShadowMap;
142
+ uniform highp sampler2DArray uPreviousShadowMap;
134
143
  uniform int uPeelShadowLayer;
135
144
 
136
145
  void main() {
137
146
  if (uPeelShadowLayer != 0) {
138
147
  vec3 p = vLightPosition.xyz / vLightPosition.w;
139
148
  vec2 uv = p.xy * 0.5 + 0.5;
140
- float previousDepth = texture(uPreviousShadowMap, uv).r;
149
+ float previousDepth = texture(uPreviousShadowMap, vec3(uv, ${SUN_STATIC_LAYER}.0)).r;
141
150
  // Discard the already-recorded surface and everything in front of it. The
142
151
  // ordinary depth test then stores the next independently fading occluder.
143
152
  if (gl_FragCoord.z <= previousDepth + 0.00001) discard;
@@ -162,7 +171,7 @@ in vec4 vLightPosition;
162
171
  in vec2 vUv;
163
172
  flat in float vAlphaCutout;
164
173
 
165
- uniform highp sampler2D uPreviousShadowMap;
174
+ uniform highp sampler2DArray uPreviousShadowMap;
166
175
  uniform int uPeelShadowLayer;
167
176
  uniform highp sampler2D uCutoutMap;
168
177
  ${CUTOUT_COVERAGE_GLSL}
@@ -172,9 +181,76 @@ void main() {
172
181
  if (uPeelShadowLayer != 0) {
173
182
  vec3 p = vLightPosition.xyz / vLightPosition.w;
174
183
  vec2 uv = p.xy * 0.5 + 0.5;
175
- float previousDepth = textureLod(uPreviousShadowMap, uv, 0.0).r;
184
+ float previousDepth = textureLod(uPreviousShadowMap, vec3(uv, ${SUN_STATIC_LAYER}.0), 0.0).r;
176
185
  if (gl_FragCoord.z <= previousDepth + 0.00001) discard;
177
186
  }
178
187
  if (alpha < vAlphaCutout) discard;
179
188
  }
180
189
  `;
190
+ /**
191
+ * The glass tint's vertex stages: every way a caster is drawn, with the world position handed on.
192
+ *
193
+ * **The same five as the depth pass, and for the same reason**: a pane must cast its colour from
194
+ * exactly where it casts its depth, deformed and placed the same way, or the coloured patch parts
195
+ * from the pane's own shadow edge. The only addition is `vGlassWorld`.
196
+ */
197
+ export const GLASS_TINT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: false, GLASS: true }, 'glass-tint');
198
+ export const GLASS_TINT_SKINNED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: true, INSTANCED: false, CUTOUT: false, GLASS: true }, 'glass-tint-skinned');
199
+ export const GLASS_TINT_INSTANCED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: false, GLASS: true }, 'glass-tint-instanced');
200
+ export const GLASS_TINT_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: true, GLASS: true }, 'glass-tint-cutout');
201
+ export const GLASS_TINT_INSTANCED_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: true, GLASS: true }, 'glass-tint-instanced-cutout');
202
+ /**
203
+ * What a pane lets through: its transmission and colour, less what it reflects toward the light,
204
+ * and how clear it is — `glassShadow.ts`'s `paneTexel`, stated for the device. Drawn with a
205
+ * multiplying blend and no depth test, so every pane on a ray contributes whatever order they
206
+ * come in.
207
+ *
208
+ * **The pane's normal is its triangle's**, from the derivatives of its world position, first and in
209
+ * uniform control flow: a pane is flat, a sign does not matter to a cosine taken absolutely, and it
210
+ * spares the five vertex stages a normal they would carry for this alone. What it gives up is a
211
+ * curved pane, whose Fresnel steps per triangle; what would change it is glass that is mostly bulbs.
212
+ */
213
+ const GLASS_TINT_BODY = /* glsl */ `
214
+ vec3 n = normalize(cross(dFdx(vGlassWorld), dFdy(vGlassWorld)));
215
+ vec3 toLight = uGlassLight.w > 0.5 ? normalize(uGlassLight.xyz - vGlassWorld) : uGlassLight.xyz;
216
+ float cosLight = abs(dot(n, toLight));
217
+ float reflected = 0.04 + 0.96 * pow(1.0 - cosLight, 5.0);
218
+ outTint = vec4(uGlassPane.rgb * (1.0 - reflected), uGlassPane.a);
219
+ `;
220
+ export const GLASS_TINT_FRAG = `#version 300 es
221
+ precision highp float;
222
+ in vec4 vLightPosition;
223
+ in vec3 vGlassWorld;
224
+
225
+ /* rgb: transmission times tint; a: clarity, one less the frost. */
226
+ uniform vec4 uGlassPane;
227
+ /* xyz: the light's position (w 1) or the direction toward it (w 0). */
228
+ uniform vec4 uGlassLight;
229
+
230
+ out vec4 outTint;
231
+
232
+ void main() {
233
+ ${GLASS_TINT_BODY}
234
+ }
235
+ `;
236
+ /** A cut-out pane: its colour lands only where its depth does, by the depth cutout's own test. */
237
+ export const GLASS_TINT_CUTOUT_FRAG = `#version 300 es
238
+ precision highp float;
239
+ in vec4 vLightPosition;
240
+ in vec2 vUv;
241
+ flat in float vAlphaCutout;
242
+ in vec3 vGlassWorld;
243
+
244
+ uniform vec4 uGlassPane;
245
+ uniform vec4 uGlassLight;
246
+ uniform highp sampler2D uCutoutMap;
247
+ ${CUTOUT_COVERAGE_GLSL}
248
+ out vec4 outTint;
249
+
250
+ void main() {
251
+ /* First, in uniform control flow, as the depth cutout samples it. */
252
+ float alpha = cutoutAlpha(texture(uCutoutMap, vUv).a, vUv * vec2(textureSize(uCutoutMap, 0)));
253
+ ${GLASS_TINT_BODY}
254
+ if (alpha < vAlphaCutout) discard;
255
+ }
256
+ `;
@@ -1,5 +1,7 @@
1
1
  /** Filtering the directional cascade, with receiver-plane depth gradient compensation. */
2
2
  import { DIRECTIONAL_SHADOW_FADE_START, MAX_SHADOW_FILTER_TAPS } from '../../renderQuality.js';
3
+ import { SUN_DYNAMIC_LAYER, SUN_GLASS_MOVING_LAYER, SUN_GLASS_STATIC_LAYER, SUN_PEELED_LAYER, SUN_STATIC_LAYER, } from '../../shadowMap.js';
4
+ import { FROST_RADIUS_CAP, FROST_SPREAD } from '../../glassShadow.js';
3
5
  export const DIRECTIONALSHADOW_GLSL = `vec2 receiverPlaneDepthGradient(vec3 p) {
4
6
  vec3 dx = dFdx(p);
5
7
  vec3 dy = dFdy(p);
@@ -126,10 +128,10 @@ float shadowFactor(float directionalNdl) {
126
128
  */
127
129
  float coarseCompare = p.z - 1.0 / uShadowDepthSpan;
128
130
  float coarse =
129
- directionalVisibility(p.z, coarseCompare, textureLod(uStaticShadowMap, p.xy, 0.0).r) *
130
- directionalVisibility(p.z, coarseCompare, textureLod(uDynamicShadowMap, p.xy, 0.0).r);
131
+ directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_STATIC_LAYER}.0), 0.0).r) *
132
+ directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_DYNAMIC_LAYER}.0), 0.0).r);
131
133
  if (uPeeledShadowEnabled != 0) {
132
- coarse *= directionalVisibility(p.z, coarseCompare, textureLod(uPeeledShadowMap, p.xy, 0.0).r);
134
+ coarse *= directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_PEELED_LAYER}.0), 0.0).r);
133
135
  }
134
136
  float strength = uShadowStrength * edgeFade * lowElevationFade;
135
137
  if (receiverFade <= 0.0) return mix(1.0, coarse, strength);
@@ -190,11 +192,11 @@ float shadowFactor(float directionalNdl) {
190
192
  * \`shadowMap.ts\` allocates these with one storage level and \`NEAREST\` filtering, so
191
193
  * there is no mip to select and level zero is the only thing \`texture\` could have read.
192
194
  */
193
- float staticDepth = textureLod(uStaticShadowMap, sampleUv, 0.0).r;
194
- float dynamicDepth = textureLod(uDynamicShadowMap, sampleUv, 0.0).r;
195
+ float staticDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_STATIC_LAYER}.0), 0.0).r;
196
+ float dynamicDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_DYNAMIC_LAYER}.0), 0.0).r;
195
197
  staticLit += directionalVisibility(tapReceiverDepth, compare, staticDepth);
196
198
  if (uPeeledShadowEnabled != 0) {
197
- float peeledDepth = textureLod(uPeeledShadowMap, sampleUv, 0.0).r;
199
+ float peeledDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_PEELED_LAYER}.0), 0.0).r;
198
200
  peeledLit += directionalVisibility(tapReceiverDepth, compare, peeledDepth);
199
201
  } else {
200
202
  peeledLit += 1.0;
@@ -214,6 +216,89 @@ float shadowFactor(float directionalNdl) {
214
216
 
215
217
  return mix(1.0, mix(coarse, lit, receiverFade), strength);
216
218
  }
219
+
220
+ /*
221
+ * One of the sun's two glass layers at receiver p: how milky the nearest pane is and how far behind
222
+ * it this receiver stands, the spread that follows (glassShadow.ts's frostRadius, capped), and the
223
+ * tint's level for that spread. Then glassShadow.ts's spreadTint: the outline from taps at the base
224
+ * radius — where the clear pane's patch ends — and the colour from the spread taps, each read coarse,
225
+ * unmixed from the ground around it and weighed by the pane's share of it, so a frosted patch mixes
226
+ * its panes' colours and keeps the light the clear one lets through. What it gives up is a pane lying
227
+ * behind the receiver lending it colour within the spread, which only a receiver between two panes
228
+ * meets. One function for both layers, because two copies were most of the lit shaders' growth.
229
+ */
230
+ vec3 sunGlassLayer(vec3 p, float paneLayer, float tintLayer, float bias, float texel, float uvPerMetre) {
231
+ float tintSize = float(textureSize(uSunGlassTints, 0).x);
232
+ float clarity = textureLod(uSunGlassTints, vec3(p.xy, tintLayer), 0.0).a;
233
+ float behind =
234
+ max(p.z - textureLod(uSunShadows, vec3(p.xy, paneLayer), 0.0).r, 0.0) * uShadowDepthSpan;
235
+ float radius = clamp(
236
+ (1.0 - clarity) * ${FROST_SPREAD.toFixed(6)} * behind * uvPerMetre,
237
+ texel,
238
+ texel * ${FROST_RADIUS_CAP.toFixed(1)}
239
+ );
240
+ float lod = log2(max(radius * tintSize, 1.0));
241
+ float cover = 0.0;
242
+ vec3 colour = vec3(0.0);
243
+ float found = 0.0;
244
+ for (int i = 0; i < ${MAX_SHADOW_FILTER_TAPS}; i++) {
245
+ if (i >= uShadowFilterTaps) break;
246
+ vec2 offset = DIRECTIONAL_PCF_OFFSETS[i];
247
+ vec2 edgeUv = p.xy + offset * texel;
248
+ cover += p.z > textureLod(uSunShadows, vec3(edgeUv, paneLayer), 0.0).r + bias ? 1.0 : 0.0;
249
+ /* The spread taps weigh by the pane's unmixed share rather than a depth test: continuous, so a
250
+ colour crossing between two panes blends rather than stepping through sixteen bands. */
251
+ vec4 pane = glassUnmix(textureLod(uSunGlassTints, vec3(p.xy + offset * radius, tintLayer), lod), clarity);
252
+ colour += pane.rgb * pane.a;
253
+ found += pane.a;
254
+ }
255
+ float taps = float(max(uShadowFilterTaps, 1));
256
+ return mix(vec3(1.0), found > 0.0 ? colour / found : vec3(1.0), cover / taps);
257
+ }
258
+
259
+ /*
260
+ * What the sun's light keeps of itself through glass at this receiver: glassShadow.ts's tap rule
261
+ * and frost spread, over shadowFactor's own taps. A tap counts a pane only when the receiver is
262
+ * farther from the sun than that pane (the glass depth layers), so a surface on the sun's side of a
263
+ * pane, and the pane itself, keep the sun's own colour; the taps average with equal weight, so the
264
+ * spread moves light rather than losing it. Frost widens the taps by how far behind the nearest pane
265
+ * the receiver is, capped, with the tap count unchanged. What it gives up is spread past the edge of
266
+ * an opaque frame's shadow, which this lookup never sees. Faded with the opaque shadow's own fades.
267
+ */
268
+ vec3 sunGlassLookup(float directionalNdl) {
269
+ vec3 p = vLightPos.xyz / vLightPos.w;
270
+ p = p * 0.5 + 0.5;
271
+ /* Map units per metre along the receiver, from derivatives taken first in uniform control flow,
272
+ so a frost spread stated in metres lands the same whatever size of light matrix a consumer built. */
273
+ float uvPerMetre = length(dFdx(p.xy)) / max(length(dFdx(vWorldPos)), 1e-6);
274
+ if (uShadowStrength <= 0.0 || textureSize(uSunGlassTints, 0).x <= 1) return vec3(1.0);
275
+ if (directionalNdl <= 0.0) return vec3(1.0);
276
+ if (p.z > 1.0 || p.x < 0.0 || p.x > 1.0 || p.y < 0.0 || p.y > 1.0) return vec3(1.0);
277
+ vec2 fromCentre = abs(p.xy - 0.5) * 2.0;
278
+ float edgeFade = 1.0 - smoothstep(0.72, 0.98, max(fromCentre.x, fromCentre.y));
279
+ edgeFade *= 1.0 - smoothstep(0.90, 1.0, p.z);
280
+ float shadowSlope = length(uDirectionalDir.xz) / max(uDirectionalDir.y, 0.001);
281
+ float lowElevationFade = 1.0 - smoothstep(
282
+ uShadowMaxSlope * ${DIRECTIONAL_SHADOW_FADE_START.toFixed(2)},
283
+ uShadowMaxSlope,
284
+ shadowSlope
285
+ );
286
+ float strength = uShadowStrength * edgeFade * lowElevationFade;
287
+ if (strength <= 0.0) return vec3(1.0);
288
+
289
+ float bias = 0.14 / uShadowDepthSpan;
290
+ float texel = 1.35 / uShadowMapSize;
291
+ vec3 staticTint = sunGlassLayer(p, ${SUN_GLASS_STATIC_LAYER}.0, 0.0, bias, texel, uvPerMetre);
292
+ vec3 movingTint = sunGlassLayer(p, ${SUN_GLASS_MOVING_LAYER}.0, 1.0, bias, texel, uvPerMetre);
293
+ return mix(vec3(1.0), staticTint * movingTint, strength);
294
+ }
295
+
296
+ /* The way in, asking first whether this build reads glass at all (GLASS_SHADOWS). A constant, so
297
+ the lookup's derivatives are still taken in uniform control flow. */
298
+ vec3 sunGlassTint(float directionalNdl) {
299
+ if (GLASS_SHADOWS) return sunGlassLookup(directionalNdl);
300
+ return vec3(1.0);
301
+ }
217
302
  #endif
218
303
 
219
304
  /**
@@ -0,0 +1,6 @@
1
+ /**
2
+ * What the lit stage shares between every glass tint it reads: whether it reads any, and
3
+ * `glassShadow.ts`'s `unmixTint`, stated once for the sun's lookup and the lamps'. At depth zero,
4
+ * outside every conditional, so each lookup can call it whichever shadows a permutation compiles.
5
+ */
6
+ export declare function glassTintGlsl(glassShadows: boolean): string;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * What the lit stage shares between every glass tint it reads: whether it reads any, and
3
+ * `glassShadow.ts`'s `unmixTint`, stated once for the sun's lookup and the lamps'. At depth zero,
4
+ * outside every conditional, so each lookup can call it whichever shadows a permutation compiles.
5
+ */
6
+ export function glassTintGlsl(glassShadows) {
7
+ return /* glsl */ `
8
+ /*
9
+ * Whether this build reads glass at all, which every way into the lookups asks first — and as a
10
+ * branch's own condition, never negated: see \`overridableConstants\` in scripts/wgsl/transform.mjs.
11
+ */
12
+ const bool GLASS_SHADOWS = ${glassShadows ? 'true' : 'false'}; // wgsl:override
13
+ /*
14
+ * A coarse level of a glass tint with the open ground's white taken back out, by how milky the pane
15
+ * at the centre is: rgb the pane's colour, a its share of the texel (glassShadow.ts's unmixTint).
16
+ * A clear centre is never read coarse, so it is returned as it stands.
17
+ */
18
+ vec4 glassUnmix(vec4 mip, float centreClarity) {
19
+ if (centreClarity >= 1.0) return vec4(mip.rgb, 1.0);
20
+ float share = clamp((1.0 - mip.a) / (1.0 - centreClarity), 0.0, 1.0);
21
+ if (share <= 0.0) return vec4(1.0, 1.0, 1.0, 0.0);
22
+ return vec4(clamp((mip.rgb - vec3(1.0 - share)) / share, 0.0, 1.0), share);
23
+ }
24
+ `;
25
+ }
@@ -79,6 +79,19 @@ export interface FlatShaderOptions {
79
79
  * for its instructions not to be there.
80
80
  */
81
81
  readonly nightEmissive: boolean;
82
+ /**
83
+ * Whether this build reads glass in its shadows at all: `glassShadows` other than `'off'`. True
84
+ * when absent, which is what the generated WGSL is built with.
85
+ *
86
+ * **Not a permutation axis, and the payload is why.** Every lit permutation carries the glass
87
+ * lookups, and a second copy of each without them would be most of the generated file again. So
88
+ * it is one switch, `GLASS_SHADOWS`, that every way into the lookups branches on first: written
89
+ * into this source for WebGL2, and a pipeline-overridable constant on the generated WGSL, which
90
+ * the device compiles away as it would under `#if`. That matters because the lookups returning
91
+ * at their first line still cost the lit pass its registers — 0.42 ms at 720p on WebGL2 on the
92
+ * courtyard at night, measured, and nothing once compiled out.
93
+ */
94
+ readonly glassShadows?: boolean;
82
95
  /**
83
96
  * How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
84
97
  *
@@ -7,6 +7,7 @@ import { preambleGlsl } from './preamble.js';
7
7
  import { LOBES_GLSL } from './lobes.js';
8
8
  import { POINTSHADOW_GLSL } from './pointShadow.js';
9
9
  import { DIRECTIONALSHADOW_GLSL } from './directionalShadow.js';
10
+ import { glassTintGlsl } from './glassTint.js';
10
11
  import { MORPH_GLSL } from '../morph.js';
11
12
  import { SKINNING_GLSL } from '../skinning.js';
12
13
  import { SURFACE_GLSL } from './surface.js';
@@ -354,6 +355,9 @@ export function flatFrag(options) {
354
355
  * is at depth zero, and it is still declared before `pointShadow` uses it.
355
356
  */
356
357
  OCTAHEDRAL_GLSL,
358
+ /* Depth zero as well: both glass lookups, sun and lamps, call it, and every permutation
359
+ declares its switch, which the pipelines set whether the permutation reads it or not. */
360
+ glassTintGlsl(options.glassShadows ?? true),
357
361
  LOBES_GLSL,
358
362
  POINTSHADOW_GLSL,
359
363
  DIRECTIONALSHADOW_GLSL,
@@ -0,0 +1 @@
1
+ export declare const LAMP_GLASS_GLSL: string;