@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
@@ -16,7 +16,25 @@ export declare const OCTAHEDRAL_EDGE = 1024;
16
16
  export interface PointShadowTarget {
17
17
  beginFace(gl: WebGL2RenderingContext): void;
18
18
  resolveFace(gl: WebGL2RenderingContext, layer: number, face: number, far: number, near: number): void;
19
+ /** Whether the array has grown its glass layers: `growForGlass`. */
20
+ readonly hasGlass: boolean;
21
+ /** Whether `face` of `layer` holds glass, which a face that offers none must clear. */
22
+ heldGlass(layer: number, face: number): boolean;
23
+ /** The depth scratch again, cleared, for the face's glass. */
24
+ beginGlassFace(gl: WebGL2RenderingContext): void;
25
+ resolveGlassFace(gl: WebGL2RenderingContext, layer: number, face: number, far: number, near: number): void;
26
+ /** The colour scratch, cleared to white, for what the face's glass lets through. */
27
+ beginTintFace(gl: WebGL2RenderingContext): void;
28
+ resolveTintFace(gl: WebGL2RenderingContext, layer: number, face: number, drawn: boolean): void;
19
29
  }
30
+ /**
31
+ * Every published layer as the array stores it: `layer × stride`, and −1 left as −1.
32
+ *
33
+ * **Applied to the resolved arrays right after each resolve**, so every consumer of an uploaded
34
+ * index reads the physical layer: once glass arrives, light L's depth is at 2L and its glass at
35
+ * 2L + 1 (`PointShadowArray.growForGlass`). In place, because the resolve rewrites these every time.
36
+ */
37
+ export declare function strideLayers(layers: Int32Array, stride: number): void;
20
38
  /**
21
39
  * Every point light's shadow as one array texture, and the scratch that fills it.
22
40
  *
@@ -34,12 +52,16 @@ export interface PointShadowTarget {
34
52
  * never the same object.
35
53
  */
36
54
  export declare class PointShadowArray implements PointShadowTarget {
37
- readonly texture: WebGLTexture;
38
55
  /**
39
- * How many layers this array holds: the point pool, the two live transition maps, and two more
40
- * for each rectangle a world said would cast. See the constructor's `extraLayers`.
56
+ * How many lights this array holds: the point pool, the two live transition maps, and two more
57
+ * for each rectangle a world said would cast. See the constructor's `extraLayers`. Counted in
58
+ * lights, so it does not move when glass doubles the storage under it.
41
59
  */
42
60
  readonly layers: number;
61
+ /** Replaced once, when glass first casts: see `growForGlass`. */
62
+ private current;
63
+ /** What the lights' glass lets through, made with the glass layers. */
64
+ private tint;
43
65
  private readonly framebuffer;
44
66
  private readonly scratchTexture;
45
67
  private readonly scratchFramebuffer;
@@ -62,6 +84,35 @@ export declare class PointShadowArray implements PointShadowTarget {
62
84
  * map.
63
85
  */
64
86
  extraLayers?: number);
87
+ /** The array texture the lit pass samples. Replaced once, when glass first casts. */
88
+ get texture(): WebGLTexture;
89
+ get hasGlass(): boolean;
90
+ /** Physical layers a light: 1, or 2 once glass has interleaved the array. */
91
+ get stride(): number;
92
+ /** The glass tint, one layer a light, once glass has cast; the lit pass samples it. */
93
+ get tintTexture(): WebGLTexture | null;
94
+ /**
95
+ * Interleave the array for glass: light L's depth moves to 2L and its glass takes 2L + 1, and
96
+ * the tint is made. Returns whether it grew — once, on the first face whose casters held glass.
97
+ *
98
+ * **Interleaved rather than stacked above the opaque half** because a shader cannot ask an array
99
+ * its layer count under naga, so it cannot find `L + N`; `layer + 1` it can. **Every map already
100
+ * baked is carried across** by a depth blit, so nothing goes dark, and **no map needs baking
101
+ * again**: this runs on the first face that saw glass, so no map baked before it saw any, and a
102
+ * glass layer born at the far plane with a white tint is exactly what its bake would draw. What
103
+ * it costs is the doubled depth (4.19 MB a light at 1024) and the tint (5.6 MB a light with its
104
+ * chain, 1.4 MB at `'half'`), from then on.
105
+ */
106
+ growForGlass(gl: WebGL2RenderingContext, tintEdge: number): boolean;
107
+ heldGlass(layer: number, face: number): boolean;
108
+ /** The depth scratch, cleared, for the face's glass: `beginFace` exactly. */
109
+ beginGlassFace(gl: WebGL2RenderingContext): void;
110
+ /** The face's glass depth into light `layer`'s glass layer, 2L + 1. */
111
+ resolveGlassFace(gl: WebGL2RenderingContext, layer: number, face: number, far: number, near: number): void;
112
+ beginTintFace(gl: WebGL2RenderingContext): void;
113
+ resolveTintFace(gl: WebGL2RenderingContext, layer: number, face: number, drawn: boolean): void;
114
+ /** The chain frost reads, for every light whose tint a round touched: once a round. */
115
+ finishTints(gl: WebGL2RenderingContext): void;
65
116
  /**
66
117
  * Bind the scratch as the render target for one face of one bake, and clear it.
67
118
  *
@@ -91,5 +142,7 @@ export declare class PointShadowArray implements PointShadowTarget {
91
142
  * from an empty vertex array.
92
143
  */
93
144
  resolveFace(gl: WebGL2RenderingContext, layer: number, face: number, far: number, near: number): void;
145
+ /** The scratch into physical layer `physical`'s region for `face`. */
146
+ private resolveInto;
94
147
  dispose(gl: WebGL2RenderingContext): void;
95
148
  }
@@ -1,6 +1,7 @@
1
1
  import { mat3 } from 'gl-matrix';
2
2
  import { createEmptyTexture2D } from './emptyTexture.js';
3
3
  import { LIVE_POINT_SHADOW_MAPS, POINT_SHADOW_POOL } from './lightBudget.js';
4
+ import { PointGlassTint } from './pointGlassTint.js';
4
5
  import { pointShadowFaceRotation } from './pointShadowImage.js';
5
6
  import { compileProgram, uniformLocations } from './shader.js';
6
7
  import { OCTAHEDRAL_RESOLVE_FRAG, OCTAHEDRAL_RESOLVE_VERT } from './shaders/octahedralResolve.js';
@@ -15,6 +16,22 @@ import { OCTAHEDRAL_RESOLVE_FRAG, OCTAHEDRAL_RESOLVE_VERT } from './shaders/octa
15
16
  export const OCTAHEDRAL_EDGE = 1024;
16
17
  /** Filled per resolved face, at module scope, because a bake runs inside the frame's budget. */
17
18
  const faceRotation = mat3.create();
19
+ /**
20
+ * Every published layer as the array stores it: `layer × stride`, and −1 left as −1.
21
+ *
22
+ * **Applied to the resolved arrays right after each resolve**, so every consumer of an uploaded
23
+ * index reads the physical layer: once glass arrives, light L's depth is at 2L and its glass at
24
+ * 2L + 1 (`PointShadowArray.growForGlass`). In place, because the resolve rewrites these every time.
25
+ */
26
+ export function strideLayers(layers, stride) {
27
+ if (stride === 1)
28
+ return;
29
+ for (let i = 0; i < layers.length; i++) {
30
+ const layer = layers[i];
31
+ if (layer >= 0)
32
+ layers[i] = layer * stride;
33
+ }
34
+ }
18
35
  /**
19
36
  * Every point light's shadow as one array texture, and the scratch that fills it.
20
37
  *
@@ -32,12 +49,16 @@ const faceRotation = mat3.create();
32
49
  * never the same object.
33
50
  */
34
51
  export class PointShadowArray {
35
- texture;
36
52
  /**
37
- * How many layers this array holds: the point pool, the two live transition maps, and two more
38
- * for each rectangle a world said would cast. See the constructor's `extraLayers`.
53
+ * How many lights this array holds: the point pool, the two live transition maps, and two more
54
+ * for each rectangle a world said would cast. See the constructor's `extraLayers`. Counted in
55
+ * lights, so it does not move when glass doubles the storage under it.
39
56
  */
40
57
  layers;
58
+ /** Replaced once, when glass first casts: see `growForGlass`. */
59
+ current;
60
+ /** What the lights' glass lets through, made with the glass layers. */
61
+ tint = null;
41
62
  framebuffer;
42
63
  scratchTexture;
43
64
  scratchFramebuffer;
@@ -68,17 +89,8 @@ export class PointShadowArray {
68
89
  */
69
90
  this.layers =
70
91
  Math.min(lightCount, POINT_SHADOW_POOL) + LIVE_POINT_SHADOW_MAPS + Math.max(0, extraLayers);
71
- const texture = gl.createTexture();
72
- if (texture === null)
73
- throw new Error('PointShadowArray: createTexture failed');
74
- this.texture = texture;
75
- gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
76
- gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.DEPTH_COMPONENT24, OCTAHEDRAL_EDGE, OCTAHEDRAL_EDGE, this.layers);
77
- /* One storage level and no filtering, so textureLod(..., 0.0) is the only fetch there is. */
78
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
79
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
80
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
81
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
92
+ const texture = depthArray(gl, this.layers);
93
+ this.current = texture;
82
94
  const scratchTexture = gl.createTexture();
83
95
  if (scratchTexture === null)
84
96
  throw new Error('PointShadowArray: createTexture failed');
@@ -141,6 +153,86 @@ export class PointShadowArray {
141
153
  gl.bindTexture(gl.TEXTURE_2D, null);
142
154
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
143
155
  }
156
+ /** The array texture the lit pass samples. Replaced once, when glass first casts. */
157
+ get texture() {
158
+ return this.current;
159
+ }
160
+ get hasGlass() {
161
+ return this.tint !== null;
162
+ }
163
+ /** Physical layers a light: 1, or 2 once glass has interleaved the array. */
164
+ get stride() {
165
+ return this.tint === null ? 1 : 2;
166
+ }
167
+ /** The glass tint, one layer a light, once glass has cast; the lit pass samples it. */
168
+ get tintTexture() {
169
+ return this.tint?.texture ?? null;
170
+ }
171
+ /**
172
+ * Interleave the array for glass: light L's depth moves to 2L and its glass takes 2L + 1, and
173
+ * the tint is made. Returns whether it grew — once, on the first face whose casters held glass.
174
+ *
175
+ * **Interleaved rather than stacked above the opaque half** because a shader cannot ask an array
176
+ * its layer count under naga, so it cannot find `L + N`; `layer + 1` it can. **Every map already
177
+ * baked is carried across** by a depth blit, so nothing goes dark, and **no map needs baking
178
+ * again**: this runs on the first face that saw glass, so no map baked before it saw any, and a
179
+ * glass layer born at the far plane with a white tint is exactly what its bake would draw. What
180
+ * it costs is the doubled depth (4.19 MB a light at 1024) and the tint (5.6 MB a light with its
181
+ * chain, 1.4 MB at `'half'`), from then on.
182
+ */
183
+ growForGlass(gl, tintEdge) {
184
+ if (this.tint !== null)
185
+ return false;
186
+ const old = this.current;
187
+ const texture = depthArray(gl, this.layers * 2);
188
+ /* Off the unit it was made on, as the constructor leaves its own: see `beginFace`. */
189
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
190
+ const read = gl.createFramebuffer();
191
+ if (read === null)
192
+ throw new Error('PointShadowArray: createFramebuffer failed');
193
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, read);
194
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.framebuffer);
195
+ for (let layer = 0; layer < this.layers; layer++) {
196
+ gl.framebufferTextureLayer(gl.READ_FRAMEBUFFER, gl.DEPTH_ATTACHMENT, old, 0, layer);
197
+ gl.framebufferTextureLayer(gl.DRAW_FRAMEBUFFER, gl.DEPTH_ATTACHMENT, texture, 0, 2 * layer);
198
+ gl.blitFramebuffer(0, 0, OCTAHEDRAL_EDGE, OCTAHEDRAL_EDGE, 0, 0, OCTAHEDRAL_EDGE, OCTAHEDRAL_EDGE, gl.DEPTH_BUFFER_BIT, gl.NEAREST);
199
+ }
200
+ /* Nothing is nearer a lamp than a pane that has not been drawn. */
201
+ for (let layer = 0; layer < this.layers; layer++) {
202
+ gl.framebufferTextureLayer(gl.DRAW_FRAMEBUFFER, gl.DEPTH_ATTACHMENT, texture, 0, 2 * layer + 1);
203
+ gl.clearBufferfv(gl.DEPTH, 0, FAR_PLANE);
204
+ }
205
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
206
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
207
+ gl.deleteFramebuffer(read);
208
+ gl.deleteTexture(old);
209
+ this.current = texture;
210
+ this.tint = new PointGlassTint(gl, this.faceSize, tintEdge, this.layers);
211
+ return true;
212
+ }
213
+ heldGlass(layer, face) {
214
+ return this.tint?.held(layer, face) ?? false;
215
+ }
216
+ /** The depth scratch, cleared, for the face's glass: `beginFace` exactly. */
217
+ beginGlassFace(gl) {
218
+ this.beginFace(gl);
219
+ }
220
+ /** The face's glass depth into light `layer`'s glass layer, 2L + 1. */
221
+ resolveGlassFace(gl, layer, face, far, near) {
222
+ if (this.tint === null)
223
+ return;
224
+ this.resolveInto(gl, 2 * layer + 1, face, far, near);
225
+ }
226
+ beginTintFace(gl) {
227
+ this.tint?.beginFace(gl);
228
+ }
229
+ resolveTintFace(gl, layer, face, drawn) {
230
+ this.tint?.resolveFace(gl, layer, face, drawn);
231
+ }
232
+ /** The chain frost reads, for every light whose tint a round touched: once a round. */
233
+ finishTints(gl) {
234
+ this.tint?.fillMips(gl);
235
+ }
144
236
  /**
145
237
  * Bind the scratch as the render target for one face of one bake, and clear it.
146
238
  *
@@ -176,8 +268,12 @@ export class PointShadowArray {
176
268
  * from an empty vertex array.
177
269
  */
178
270
  resolveFace(gl, layer, face, far, near) {
271
+ this.resolveInto(gl, layer * this.stride, face, far, near);
272
+ }
273
+ /** The scratch into physical layer `physical`'s region for `face`. */
274
+ resolveInto(gl, physical, face, far, near) {
179
275
  gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
180
- gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, this.texture, 0, layer);
276
+ gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, this.current, 0, physical);
181
277
  gl.viewport(0, 0, OCTAHEDRAL_EDGE, OCTAHEDRAL_EDGE);
182
278
  gl.useProgram(this.program);
183
279
  gl.bindVertexArray(this.vao);
@@ -210,10 +306,25 @@ export class PointShadowArray {
210
306
  dispose(gl) {
211
307
  gl.deleteFramebuffer(this.framebuffer);
212
308
  gl.deleteFramebuffer(this.scratchFramebuffer);
213
- gl.deleteTexture(this.texture);
309
+ gl.deleteTexture(this.current);
214
310
  gl.deleteTexture(this.scratchTexture);
311
+ this.tint?.dispose(gl);
215
312
  gl.deleteTexture(this.parked);
216
313
  gl.deleteProgram(this.program);
217
314
  gl.deleteVertexArray(this.vao);
218
315
  }
219
316
  }
317
+ const FAR_PLANE = new Float32Array([1]);
318
+ /** The depth array: one storage level and no filtering, so textureLod(..., 0.0) is the only fetch. */
319
+ function depthArray(gl, layers) {
320
+ const texture = gl.createTexture();
321
+ if (texture === null)
322
+ throw new Error('PointShadowArray: createTexture failed');
323
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
324
+ gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.DEPTH_COMPONENT24, OCTAHEDRAL_EDGE, OCTAHEDRAL_EDGE, layers);
325
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
326
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
327
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
328
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
329
+ return texture;
330
+ }
@@ -21,6 +21,18 @@ import { FACE_COUNT, POINT_SHADOW_NEAR } from './pointShadowImage.ts';
21
21
  * projection the shader reads. Nothing about the bake's cost, order or resumption moved.
22
22
  */
23
23
  export { FACE_COUNT, POINT_SHADOW_NEAR };
24
+ /**
25
+ * What a bake asks of the renderer about glass, after each face's opaque depth.
26
+ *
27
+ * **Asked before it is drawn** because the answer decides whether the face binds a glass target at
28
+ * all: a face that offers no glass and holds none from a previous owner does no glass work.
29
+ */
30
+ export interface PointGlassDraw {
31
+ /** Whether the face just drawn offered any glass. */
32
+ offered(): boolean;
33
+ /** Draw that glass: its depth, or what it lets through lit from the bake origin. */
34
+ draw(pass: 'depth' | 'tint', x: number, y: number, z: number): void;
35
+ }
24
36
  export declare class PointShadowMap {
25
37
  readonly size: number;
26
38
  /**
@@ -85,7 +97,9 @@ export declare class PointShadowMap {
85
97
  * bake is not sampled at all until it completes, so this only matters for a re-bake,
86
98
  * where holding the old values keeps them matched to the old faces.
87
99
  */
88
- bake(gl: WebGL2RenderingContext, x: number, y: number, z: number, range: number, drawCasters: (viewProj: mat4) => void, near?: number, sourceRadius?: number, maxFaces?: number): number;
100
+ bake(gl: WebGL2RenderingContext, x: number, y: number, z: number, range: number, drawCasters: (viewProj: mat4) => void, near?: number, sourceRadius?: number, maxFaces?: number,
101
+ /** The face's glass, when glass shadows are on: `PointGlassDraw`. */
102
+ glass?: PointGlassDraw | null): number;
89
103
  /**
90
104
  * Nothing to free: every GPU object this map used to own belongs to `PointShadowArray` now.
91
105
  *
@@ -93,5 +107,13 @@ export declare class PointShadowMap {
93
107
  * needing disposal is exactly the shape of a leak nobody notices. The array is disposed by the
94
108
  * renderer that built it.
95
109
  */
110
+ /**
111
+ * The face's glass, twice: where the nearest pane is, then what the panes let through.
112
+ *
113
+ * A face that offers no glass still clears a region a previous owner's glass is in — the depth to
114
+ * the far plane and the tint to white, from scratches cleared and not drawn into — because a pool
115
+ * slot changes hands with its layer.
116
+ */
117
+ private bakeGlassFace;
96
118
  dispose(_gl: WebGL2RenderingContext): void;
97
119
  }
@@ -113,7 +113,9 @@ export class PointShadowMap {
113
113
  * bake is not sampled at all until it completes, so this only matters for a re-bake,
114
114
  * where holding the old values keeps them matched to the old faces.
115
115
  */
116
- bake(gl, x, y, z, range, drawCasters, near = POINT_SHADOW_NEAR, sourceRadius = DEFAULT_SOURCE_RADIUS, maxFaces = FACE_COUNT) {
116
+ bake(gl, x, y, z, range, drawCasters, near = POINT_SHADOW_NEAR, sourceRadius = DEFAULT_SOURCE_RADIUS, maxFaces = FACE_COUNT,
117
+ /** The face's glass, when glass shadows are on: `PointGlassDraw`. */
118
+ glass = null) {
117
119
  /*
118
120
  * What to render now, **and what to render it for**. A bake in flight owns the parameters
119
121
  * it started with, so the arguments here are only a proposal for a bake that is not already
@@ -159,6 +161,8 @@ export class PointShadowMap {
159
161
  * next face's pass does care about.
160
162
  */
161
163
  array.resolveFace(gl, this.layer, face, plan.range, plan.near);
164
+ if (glass !== null)
165
+ this.bakeGlassFace(gl, array, glass, face, plan);
162
166
  }
163
167
  gl.enable(gl.CULL_FACE);
164
168
  gl.cullFace(gl.BACK);
@@ -173,5 +177,27 @@ export class PointShadowMap {
173
177
  * needing disposal is exactly the shape of a leak nobody notices. The array is disposed by the
174
178
  * renderer that built it.
175
179
  */
180
+ /**
181
+ * The face's glass, twice: where the nearest pane is, then what the panes let through.
182
+ *
183
+ * A face that offers no glass still clears a region a previous owner's glass is in — the depth to
184
+ * the far plane and the tint to white, from scratches cleared and not drawn into — because a pool
185
+ * slot changes hands with its layer.
186
+ */
187
+ bakeGlassFace(gl, array, glass, face, plan) {
188
+ if (!array.hasGlass)
189
+ return;
190
+ const offered = glass.offered();
191
+ if (!offered && !array.heldGlass(this.layer, face))
192
+ return;
193
+ array.beginGlassFace(gl);
194
+ if (offered)
195
+ glass.draw('depth', plan.x, plan.y, plan.z);
196
+ array.resolveGlassFace(gl, this.layer, face, plan.range, plan.near);
197
+ array.beginTintFace(gl);
198
+ if (offered)
199
+ glass.draw('tint', plan.x, plan.y, plan.z);
200
+ array.resolveTintFace(gl, this.layer, face, offered);
201
+ }
176
202
  dispose(_gl) { }
177
203
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Which frame a change to a dynamic mesh or an instanced batch belongs to.
3
+ *
4
+ * **A consumer may change geometry in its simulation step, before `beginFrame`.** That change is
5
+ * what the *next* frame draws, so it is that frame's — counting it against the frame last begun
6
+ * would say the geometry changed a frame early and then did not change at all, which is a motion
7
+ * vector on the wrong frame and none on the right one.
8
+ *
9
+ * Only the **first** change in a frame keeps the state before it: a second rewrite in the same
10
+ * frame must not replace "where it was last frame" with "where it was a moment ago".
11
+ */
12
+ export declare function frameOfChange(serial: number, inFrame: boolean): number;
13
+ export declare function keepsPrevious(lastChange: number, frame: number): boolean;
14
+ /** Slots `0 … n-1` pair with last frame's; a slot past last frame's count is new and has no past. */
15
+ export declare function pairedInstances(previousCount: number, count: number): number;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Which frame a change to a dynamic mesh or an instanced batch belongs to.
3
+ *
4
+ * **A consumer may change geometry in its simulation step, before `beginFrame`.** That change is
5
+ * what the *next* frame draws, so it is that frame's — counting it against the frame last begun
6
+ * would say the geometry changed a frame early and then did not change at all, which is a motion
7
+ * vector on the wrong frame and none on the right one.
8
+ *
9
+ * Only the **first** change in a frame keeps the state before it: a second rewrite in the same
10
+ * frame must not replace "where it was last frame" with "where it was a moment ago".
11
+ */
12
+ export function frameOfChange(serial, inFrame) {
13
+ return inFrame ? serial : serial + 1;
14
+ }
15
+ export function keepsPrevious(lastChange, frame) {
16
+ return lastChange !== frame;
17
+ }
18
+ /** Slots `0 … n-1` pair with last frame's; a slot past last frame's count is new and has no past. */
19
+ export function pairedInstances(previousCount, count) {
20
+ return Math.min(previousCount, count);
21
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Two decisions about one draw in a reconstructing frame, and why they are the same set.
3
+ *
4
+ * A blended draw goes **late** — after the reconstruction, at output resolution, unjittered — and a
5
+ * mover's motion is **tracked**, only for draws that land in the picture being reconstructed. A
6
+ * planar mirror and a probe bake are other pictures drawn through the same verbs, and the overlay
7
+ * after `endFrame` is past the picture altogether; routing any of them late, or counting a mirrored
8
+ * draw as a mover's second draw of the frame, would be wrong in the way that reads as a ghost.
9
+ *
10
+ * Plain booleans rather than a state object, so the per-draw call allocates nothing.
11
+ */
12
+ export declare function routesLate(reconstructing: boolean, mirror: boolean, probe: boolean, presented: boolean): boolean;
13
+ /** The same set as `routesLate`, named for the other reader. */
14
+ export declare function tracksMotion(reconstructing: boolean, mirror: boolean, probe: boolean, presented: boolean): boolean;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Two decisions about one draw in a reconstructing frame, and why they are the same set.
3
+ *
4
+ * A blended draw goes **late** — after the reconstruction, at output resolution, unjittered — and a
5
+ * mover's motion is **tracked**, only for draws that land in the picture being reconstructed. A
6
+ * planar mirror and a probe bake are other pictures drawn through the same verbs, and the overlay
7
+ * after `endFrame` is past the picture altogether; routing any of them late, or counting a mirrored
8
+ * draw as a mover's second draw of the frame, would be wrong in the way that reads as a ghost.
9
+ *
10
+ * Plain booleans rather than a state object, so the per-draw call allocates nothing.
11
+ */
12
+ export function routesLate(reconstructing, mirror, probe, presented) {
13
+ return reconstructing && !mirror && !probe && !presented;
14
+ }
15
+ /** The same set as `routesLate`, named for the other reader. */
16
+ export function tracksMotion(reconstructing, mirror, probe, presented) {
17
+ return routesLate(reconstructing, mirror, probe, presented);
18
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Which object a draw is, so a reconstruction can find where that object was last frame.
3
+ *
4
+ * **The engine cannot infer this and does not try.** `drawMesh` is immediate: a mesh drawn three
5
+ * times is three objects, and nothing in the call says which of last frame's three each one is. A
6
+ * consumer that makes one `Mover` per moving object and passes it with every draw of that object
7
+ * says exactly that, and the renderer keeps the rest — last frame's model and, for a skinned
8
+ * draw, last frame's palette.
9
+ *
10
+ * **Plain data, advanced by the renderer.** No renderer is needed to make one, so a consumer holds
11
+ * it beside the object it belongs to. Only a reconstructing WebGPU renderer reads or writes it; it
12
+ * costs nothing anywhere else. The fields past `kind` are the renderer's and a caller leaves them
13
+ * alone.
14
+ *
15
+ * **What would make this wrong** is a consumer whose objects are rebuilt every frame with no state
16
+ * of their own to hold a mover in; the identity then has to live in whatever *does* persist.
17
+ */
18
+ export declare const MOVER_KIND: 'drift.mover';
19
+ export interface Mover {
20
+ readonly kind: typeof MOVER_KIND;
21
+ /** The renderer that last drew it, from `claimRendererId`; -1 before its first draw. */
22
+ renderer: number;
23
+ /** That renderer's frame serial at the last draw. */
24
+ frame: number;
25
+ readonly model: Float32Array;
26
+ readonly previousModel: Float32Array;
27
+ /**
28
+ * This frame's palette, exactly `paletteLength` floats long once a skinned draw has set it.
29
+ * Swapped with the previous one each frame and reallocated only when the rig changes.
30
+ */
31
+ palette: Float32Array;
32
+ previousPalette: Float32Array;
33
+ paletteLength: number;
34
+ previousPaletteLength: number;
35
+ }
36
+ /** Nothing to reproject from: first draw, a missed frame, another renderer, or a changed rig. */
37
+ export declare const MOVER_NONE = 0;
38
+ /** Drawn on the frame before by this renderer: `previousModel` and `previousPalette` are real. */
39
+ export declare const MOVER_MOTION = 1;
40
+ /** Drawn already this frame. The second draw is a different object and the caller's error. */
41
+ export declare const MOVER_TWICE = 2;
42
+ export type MoverVerdict = typeof MOVER_NONE | typeof MOVER_MOTION | typeof MOVER_TWICE;
43
+ /** A number no other renderer in this page holds, so a mover can tell renderers apart. */
44
+ export declare function claimRendererId(): number;
45
+ export declare function createMover(): Mover;
46
+ export declare function isMover(value: unknown): value is Mover;
47
+ /**
48
+ * Advance a mover to this draw, and say whether last frame's state is usable.
49
+ *
50
+ * **A second draw in one frame changes nothing**, so the first draw's motion stands; the caller
51
+ * warns. Otherwise what the mover held becomes its previous and this draw becomes its current —
52
+ * the palettes by swapping two arrays, so a steady rig allocates nothing after its first frame.
53
+ *
54
+ * **A changed joint count is no motion**: skinning a vertex with a palette of a different rig is
55
+ * not an old pose of this one.
56
+ */
57
+ export declare function advanceMover(mover: Mover, renderer: number, frame: number, model: ArrayLike<number>, palette: Float32Array | null): MoverVerdict;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Which object a draw is, so a reconstruction can find where that object was last frame.
3
+ *
4
+ * **The engine cannot infer this and does not try.** `drawMesh` is immediate: a mesh drawn three
5
+ * times is three objects, and nothing in the call says which of last frame's three each one is. A
6
+ * consumer that makes one `Mover` per moving object and passes it with every draw of that object
7
+ * says exactly that, and the renderer keeps the rest — last frame's model and, for a skinned
8
+ * draw, last frame's palette.
9
+ *
10
+ * **Plain data, advanced by the renderer.** No renderer is needed to make one, so a consumer holds
11
+ * it beside the object it belongs to. Only a reconstructing WebGPU renderer reads or writes it; it
12
+ * costs nothing anywhere else. The fields past `kind` are the renderer's and a caller leaves them
13
+ * alone.
14
+ *
15
+ * **What would make this wrong** is a consumer whose objects are rebuilt every frame with no state
16
+ * of their own to hold a mover in; the identity then has to live in whatever *does* persist.
17
+ */
18
+ export const MOVER_KIND = 'drift.mover';
19
+ /** Nothing to reproject from: first draw, a missed frame, another renderer, or a changed rig. */
20
+ export const MOVER_NONE = 0;
21
+ /** Drawn on the frame before by this renderer: `previousModel` and `previousPalette` are real. */
22
+ export const MOVER_MOTION = 1;
23
+ /** Drawn already this frame. The second draw is a different object and the caller's error. */
24
+ export const MOVER_TWICE = 2;
25
+ let nextRendererId = 1;
26
+ /** A number no other renderer in this page holds, so a mover can tell renderers apart. */
27
+ export function claimRendererId() {
28
+ const id = nextRendererId;
29
+ nextRendererId += 1;
30
+ return id;
31
+ }
32
+ export function createMover() {
33
+ return {
34
+ kind: MOVER_KIND,
35
+ renderer: -1,
36
+ frame: -2,
37
+ model: new Float32Array(16),
38
+ previousModel: new Float32Array(16),
39
+ palette: new Float32Array(0),
40
+ previousPalette: new Float32Array(0),
41
+ paletteLength: 0,
42
+ previousPaletteLength: 0,
43
+ };
44
+ }
45
+ export function isMover(value) {
46
+ return (typeof value === 'object' && value !== null && value.kind === MOVER_KIND);
47
+ }
48
+ /**
49
+ * Advance a mover to this draw, and say whether last frame's state is usable.
50
+ *
51
+ * **A second draw in one frame changes nothing**, so the first draw's motion stands; the caller
52
+ * warns. Otherwise what the mover held becomes its previous and this draw becomes its current —
53
+ * the palettes by swapping two arrays, so a steady rig allocates nothing after its first frame.
54
+ *
55
+ * **A changed joint count is no motion**: skinning a vertex with a palette of a different rig is
56
+ * not an old pose of this one.
57
+ */
58
+ export function advanceMover(mover, renderer, frame, model, palette) {
59
+ if (mover.renderer === renderer && mover.frame === frame)
60
+ return MOVER_TWICE;
61
+ const drawnLast = mover.renderer === renderer && mover.frame === frame - 1;
62
+ mover.previousModel.set(mover.model);
63
+ const spare = mover.previousPalette;
64
+ mover.previousPalette = mover.palette;
65
+ mover.palette = spare;
66
+ mover.previousPaletteLength = mover.paletteLength;
67
+ mover.model.set(model);
68
+ if (palette === null) {
69
+ mover.paletteLength = 0;
70
+ }
71
+ else {
72
+ /* Exactly the rig's length, because the array is uploaded as it stands; reallocated only when
73
+ the rig changes, which a steady character never does. */
74
+ if (mover.palette.length !== palette.length)
75
+ mover.palette = new Float32Array(palette.length);
76
+ mover.palette.set(palette);
77
+ mover.paletteLength = palette.length;
78
+ }
79
+ mover.renderer = renderer;
80
+ mover.frame = frame;
81
+ if (!drawnLast)
82
+ return MOVER_NONE;
83
+ if (mover.paletteLength !== mover.previousPaletteLength)
84
+ return MOVER_NONE;
85
+ return MOVER_MOTION;
86
+ }
@@ -29,6 +29,8 @@ export type OutputTransform = 'none' | 'srgb' | 'aces';
29
29
  export type ShadowFilterTaps = 4 | 8 | 12;
30
30
  export type DirectionalShadowDepthLayers = 1 | 2;
31
31
  export type WaterReflectionFilterTaps = 1 | 5 | 9;
32
+ /** How light through glass is coloured: see `RenderQuality.glassShadows`. */
33
+ export type GlassShadows = 'full' | 'half' | 'off';
32
34
  export interface RenderQuality {
33
35
  /**
34
36
  * The transfer curve applied at the end of every shaded pass.
@@ -141,6 +143,25 @@ export interface RenderQuality {
141
143
  readonly lightVolumeSamples: number;
142
144
  /** Independently composited static sun depths; two preserves common overlaps. */
143
145
  readonly directionalShadowDepthLayers: DirectionalShadowDepthLayers;
146
+ /**
147
+ * Whether light through glass takes the glass's colour, and at what size.
148
+ *
149
+ * **`'full'`, the default**: every shadow a glass caster reaches gains a layer holding the depth of
150
+ * the nearest pane and a tint layer holding what the panes let through, at the size of the depth
151
+ * map beside it — 8 bytes a texel, so 32 MB for a 2048 sun map and 8 MB for a 1024 lamp layer.
152
+ * **`'half'`** halves the tint layers only (the glass depth must match its array): 20 MB and 5 MB,
153
+ * and a coloured patch softer than an opaque shadow's edge. **`'off'`** is glass before this
154
+ * existed: a pane casts nothing at all.
155
+ *
156
+ * Nothing is allocated until a glass caster first reaches a shadow, so a scene with no glass pays
157
+ * no memory and no pass whatever this says. **The lit shader is the exception**: `'full'` and
158
+ * `'half'` compile the glass lookup into it, and a lookup that finds no glass and returns at its
159
+ * first line still costs the lit pass its registers — measured on the courtyard at night, 0.27 ms
160
+ * at 720p on WebGPU, 0.42 ms on WebGL2 and 1.2 ms at 4K. `'off'` compiles none of it, so a world
161
+ * that never casts through glass is that much faster for saying so. What would make `'full'` wrong
162
+ * is a phone budget with a lot of glass; that is what `'half'` is for. See `glassShadow.ts`.
163
+ */
164
+ readonly glassShadows: GlassShadows;
144
165
  /** Horizontal world-space reach over which a directional shadow dissolves. */
145
166
  readonly directionalShadowMaxDistance: number;
146
167
  /** Maximum horizontal projection per vertical metre before low-angle shadows vanish. */
@@ -548,6 +569,20 @@ export interface RenderQuality {
548
569
  * the render is a quarter of the output and no reconstruction holds an edge through that. A
549
570
  * negative number is off, not a clamp to the bottom: it cannot mean "a little".
550
571
  *
572
+ * **What it needs from you.** A `createMover()` per moving object, passed with that object's
573
+ * draws where `drawMesh` takes last frame's matrix — the renderer keeps last frame's model and,
574
+ * for a skinned draw, last frame's pose. Instanced batches and meshes rewritten with
575
+ * `updateMesh` need nothing: the batch and the mesh are their own identity. And `cameraCut()` at
576
+ * every cut, because nothing detects one: without it a cut blends the previous shot into the
577
+ * next. Blended draws — translucent meshes, particles, plumes, film, bolts, lines, world text —
578
+ * are drawn after the upscale at the output size and need nothing either; light volumes stay in
579
+ * the reconstructed picture, being a march that keeps its history.
580
+ *
581
+ * **What it cannot give back** is the resolution it draws at: an edge that never moves is softer
582
+ * than a native frame's at any ratio, because two thirds of the pixels at 1.5 are a guess made
583
+ * sharp. Measured on `demo/dev/ghost.html` against a native frame at four samples, still edges
584
+ * err by about 8 levels where a native frame at one sample errs by 3.
585
+ *
551
586
  * `?recon=` is how it gets A/B'd in one reload.
552
587
  */
553
588
  readonly reconstruction: number;