@driftengine/core 4.4.1 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/CHANGELOG.json +72 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +5 -1
  4. package/dist/index.js +7 -0
  5. package/dist/render/backend/api.d.ts +17 -11
  6. package/dist/render/backend/webgl2/glassTintPrograms.d.ts +13 -0
  7. package/dist/render/backend/webgl2/glassTintPrograms.js +38 -0
  8. package/dist/render/backend/webgl2/renderer.d.ts +82 -8
  9. package/dist/render/backend/webgl2/renderer.js +373 -105
  10. package/dist/render/backend/webgpu/buffers.d.ts +19 -1
  11. package/dist/render/backend/webgpu/buffers.js +27 -1
  12. package/dist/render/backend/webgpu/depthPass.d.ts +1 -1
  13. package/dist/render/backend/webgpu/depthPass.js +2 -1
  14. package/dist/render/backend/webgpu/flatPass.d.ts +2 -0
  15. package/dist/render/backend/webgpu/flatPass.js +20 -1
  16. package/dist/render/backend/webgpu/glassTintPass.d.ts +28 -0
  17. package/dist/render/backend/webgpu/glassTintPass.js +205 -0
  18. package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +60 -1
  19. package/dist/render/backend/webgpu/gpuDrivenPass.js +305 -5
  20. package/dist/render/backend/webgpu/instanced.d.ts +12 -1
  21. package/dist/render/backend/webgpu/instanced.js +20 -1
  22. package/dist/render/backend/webgpu/latePass.d.ts +26 -0
  23. package/dist/render/backend/webgpu/latePass.js +133 -0
  24. package/dist/render/backend/webgpu/lightVolumePass.js +5 -1
  25. package/dist/render/backend/webgpu/motionPass.d.ts +95 -0
  26. package/dist/render/backend/webgpu/motionPass.js +491 -0
  27. package/dist/render/backend/webgpu/pipelineCache.d.ts +10 -1
  28. package/dist/render/backend/webgpu/pipelineCache.js +11 -1
  29. package/dist/render/backend/webgpu/pointGlassTintPass.d.ts +32 -0
  30. package/dist/render/backend/webgpu/pointGlassTintPass.js +193 -0
  31. package/dist/render/backend/webgpu/pointShadowPass.d.ts +35 -6
  32. package/dist/render/backend/webgpu/pointShadowPass.js +105 -20
  33. package/dist/render/backend/webgpu/postPass.d.ts +21 -15
  34. package/dist/render/backend/webgpu/postPass.js +17 -4
  35. package/dist/render/backend/webgpu/renderer.d.ts +181 -54
  36. package/dist/render/backend/webgpu/renderer.js +1011 -394
  37. package/dist/render/backend/webgpu/scatterPass.js +2 -1
  38. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  39. package/dist/render/backend/webgpu/skinPaletteRing.js +4 -0
  40. package/dist/render/backend/webgpu/snapshotMips.d.ts +29 -0
  41. package/dist/render/backend/webgpu/snapshotMips.js +52 -0
  42. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +5 -0
  43. package/dist/render/backend/webgpu/surfaceTexturePass.js +1 -1
  44. package/dist/render/flameLight.js +15 -2
  45. package/dist/render/glass.d.ts +45 -0
  46. package/dist/render/glass.js +39 -0
  47. package/dist/render/glassCasters.d.ts +40 -0
  48. package/dist/render/glassCasters.js +84 -0
  49. package/dist/render/glassShadow.d.ts +78 -0
  50. package/dist/render/glassShadow.js +125 -0
  51. package/dist/render/globalMediumPass.d.ts +2 -3
  52. package/dist/render/globalMediumPass.js +2 -8
  53. package/dist/render/gpudriven/lit.d.ts +12 -0
  54. package/dist/render/gpudriven/lit.js +23 -0
  55. package/dist/render/gpudriven/materialTable.d.ts +15 -5
  56. package/dist/render/gpudriven/materialTable.js +26 -6
  57. package/dist/render/gpudriven/pipeline.d.ts +1 -1
  58. package/dist/render/gpudriven/pipeline.js +4 -3
  59. package/dist/render/gpudriven/sunGlass.d.ts +35 -0
  60. package/dist/render/gpudriven/sunGlass.js +89 -0
  61. package/dist/render/lightBudget.d.ts +17 -5
  62. package/dist/render/lightBudget.js +17 -5
  63. package/dist/render/pointGlassTint.d.ts +59 -0
  64. package/dist/render/pointGlassTint.js +198 -0
  65. package/dist/render/pointShadowArray.d.ts +56 -3
  66. package/dist/render/pointShadowArray.js +127 -16
  67. package/dist/render/pointShadowBudget.js +51 -41
  68. package/dist/render/pointShadowMap.d.ts +23 -1
  69. package/dist/render/pointShadowMap.js +27 -1
  70. package/dist/render/recon/changeFrames.d.ts +15 -0
  71. package/dist/render/recon/changeFrames.js +21 -0
  72. package/dist/render/recon/disocclusion.d.ts +36 -3
  73. package/dist/render/recon/disocclusion.js +56 -4
  74. package/dist/render/recon/lateRouting.d.ts +14 -0
  75. package/dist/render/recon/lateRouting.js +18 -0
  76. package/dist/render/recon/mover.d.ts +57 -0
  77. package/dist/render/recon/mover.js +86 -0
  78. package/dist/render/recon/resolve.js +95 -12
  79. package/dist/render/renderQuality.d.ts +35 -0
  80. package/dist/render/renderQuality.js +7 -0
  81. package/dist/render/rendererHarness.js +12 -0
  82. package/dist/render/sceneTarget.d.ts +9 -1
  83. package/dist/render/sceneTarget.js +59 -13
  84. package/dist/render/shaders/depth.d.ts +16 -1
  85. package/dist/render/shaders/depth.js +85 -9
  86. package/dist/render/shaders/flat/directionalShadow.js +91 -6
  87. package/dist/render/shaders/flat/glassTint.d.ts +6 -0
  88. package/dist/render/shaders/flat/glassTint.js +25 -0
  89. package/dist/render/shaders/flat/index.d.ts +13 -0
  90. package/dist/render/shaders/flat/index.js +4 -0
  91. package/dist/render/shaders/flat/lampGlass.d.ts +1 -0
  92. package/dist/render/shaders/flat/lampGlass.js +183 -0
  93. package/dist/render/shaders/flat/main.d.ts +1 -1
  94. package/dist/render/shaders/flat/main.js +115 -15
  95. package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
  96. package/dist/render/shaders/flat/pointShadow.js +2 -0
  97. package/dist/render/shaders/flat/preamble.js +21 -6
  98. package/dist/render/shaders/generated/depth.wgsl.d.ts +227 -4
  99. package/dist/render/shaders/generated/depth.wgsl.js +227 -4
  100. package/dist/render/shaders/generated/flat.wgsl.d.ts +360 -312
  101. package/dist/render/shaders/generated/flat.wgsl.js +376 -328
  102. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +3 -13
  103. package/dist/render/shaders/generated/globalMedium.wgsl.js +3 -13
  104. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +3 -13
  105. package/dist/render/shaders/generated/lightVolume.wgsl.js +4 -14
  106. package/dist/render/shaders/generated/octahedralResolve.wgsl.d.ts +29 -0
  107. package/dist/render/shaders/generated/octahedralResolve.wgsl.js +29 -0
  108. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +31 -6
  109. package/dist/render/shaders/generated/temporalResolve.wgsl.js +31 -6
  110. package/dist/render/shaders/globalMedium.js +6 -6
  111. package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +7 -0
  112. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +86 -1
  113. package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +1 -1
  114. package/dist/render/shaders/gpudriven/cull.wgsl.js +11 -2
  115. package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +1 -1
  116. package/dist/render/shaders/gpudriven/materialTable.wgsl.js +3 -1
  117. package/dist/render/shaders/gpudriven/shade.wgsl.js +50 -1
  118. package/dist/render/shaders/gpudriven/sunGlass.wgsl.d.ts +9 -0
  119. package/dist/render/shaders/gpudriven/sunGlass.wgsl.js +133 -0
  120. package/dist/render/shaders/lightVolume.js +6 -6
  121. package/dist/render/shaders/octahedralResolve.d.ts +8 -0
  122. package/dist/render/shaders/octahedralResolve.js +48 -17
  123. package/dist/render/shaders/recon/lateDepth.wgsl.d.ts +1 -0
  124. package/dist/render/shaders/recon/lateDepth.wgsl.js +44 -0
  125. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  126. package/dist/render/shaders/recon/motion.wgsl.js +84 -0
  127. package/dist/render/shaders/recon/resolve.wgsl.d.ts +1 -1
  128. package/dist/render/shaders/recon/resolve.wgsl.js +112 -8
  129. package/dist/render/shaders/temporalResolve.js +133 -5
  130. package/dist/render/shadowCasters.d.ts +20 -1
  131. package/dist/render/shadowCasters.js +15 -1
  132. package/dist/render/shadowMap.d.ts +54 -5
  133. package/dist/render/shadowMap.js +190 -36
  134. package/dist/render/sunGlassTint.d.ts +25 -0
  135. package/dist/render/sunGlassTint.js +79 -0
  136. package/dist/render/temporalAa.d.ts +145 -0
  137. package/dist/render/temporalAa.js +190 -0
  138. package/dist/render/temporalPass.d.ts +16 -14
  139. package/dist/render/temporalPass.js +59 -14
  140. package/dist/render/translucentQueue.d.ts +6 -0
  141. package/dist/render/translucentQueue.js +15 -1
  142. package/package.json +6 -6
  143. package/src/index.ts +10 -0
  144. package/src/render/backend/api.ts +15 -9
  145. package/src/render/backend/webgl2/glassTintPrograms.ts +58 -0
  146. package/src/render/backend/webgl2/renderer.ts +422 -109
  147. package/src/render/backend/webgpu/buffers.ts +50 -2
  148. package/src/render/backend/webgpu/depthPass.ts +2 -1
  149. package/src/render/backend/webgpu/flatPass.ts +22 -1
  150. package/src/render/backend/webgpu/glassTintPass.ts +244 -0
  151. package/src/render/backend/webgpu/gpuDrivenPass.ts +349 -5
  152. package/src/render/backend/webgpu/instanced.ts +21 -1
  153. package/src/render/backend/webgpu/latePass.ts +152 -0
  154. package/src/render/backend/webgpu/lightVolumePass.ts +5 -1
  155. package/src/render/backend/webgpu/motionPass.ts +535 -0
  156. package/src/render/backend/webgpu/pipelineCache.ts +12 -1
  157. package/src/render/backend/webgpu/pointGlassTintPass.ts +233 -0
  158. package/src/render/backend/webgpu/pointShadowPass.ts +158 -26
  159. package/src/render/backend/webgpu/postPass.ts +34 -4
  160. package/src/render/backend/webgpu/renderer.ts +1178 -425
  161. package/src/render/backend/webgpu/scatterPass.ts +2 -1
  162. package/src/render/backend/webgpu/skinPaletteRing.ts +4 -0
  163. package/src/render/backend/webgpu/snapshotMips.ts +80 -0
  164. package/src/render/backend/webgpu/surfaceTexturePass.ts +1 -1
  165. package/src/render/flameLight.ts +15 -2
  166. package/src/render/glass.ts +74 -0
  167. package/src/render/glassCasters.ts +164 -0
  168. package/src/render/glassShadow.ts +153 -0
  169. package/src/render/globalMediumPass.ts +4 -11
  170. package/src/render/gpudriven/lit.ts +31 -0
  171. package/src/render/gpudriven/materialTable.ts +28 -6
  172. package/src/render/gpudriven/pipeline.ts +4 -3
  173. package/src/render/gpudriven/sunGlass.ts +117 -0
  174. package/src/render/lightBudget.ts +19 -5
  175. package/src/render/pointGlassTint.ts +250 -0
  176. package/src/render/pointShadowArray.ts +188 -22
  177. package/src/render/pointShadowBudget.ts +49 -39
  178. package/src/render/pointShadowMap.ts +41 -0
  179. package/src/render/recon/changeFrames.ts +24 -0
  180. package/src/render/recon/disocclusion.ts +54 -3
  181. package/src/render/recon/lateRouting.ts +29 -0
  182. package/src/render/recon/mover.ts +118 -0
  183. package/src/render/recon/resolve.ts +137 -10
  184. package/src/render/renderQuality.ts +47 -0
  185. package/src/render/rendererHarness.ts +12 -0
  186. package/src/render/sceneTarget.ts +66 -12
  187. package/src/render/shaders/depth.ts +109 -9
  188. package/src/render/shaders/flat/directionalShadow.ts +97 -6
  189. package/src/render/shaders/flat/glassTint.ts +25 -0
  190. package/src/render/shaders/flat/index.ts +17 -0
  191. package/src/render/shaders/flat/lampGlass.ts +185 -0
  192. package/src/render/shaders/flat/main.ts +115 -15
  193. package/src/render/shaders/flat/pointShadow.ts +2 -0
  194. package/src/render/shaders/flat/preamble.ts +21 -6
  195. package/src/render/shaders/generated/depth.wgsl.ts +234 -4
  196. package/src/render/shaders/generated/flat.wgsl.ts +376 -328
  197. package/src/render/shaders/generated/globalMedium.wgsl.ts +3 -13
  198. package/src/render/shaders/generated/lightVolume.wgsl.ts +4 -14
  199. package/src/render/shaders/generated/octahedralResolve.wgsl.ts +30 -0
  200. package/src/render/shaders/generated/temporalResolve.wgsl.ts +31 -6
  201. package/src/render/shaders/globalMedium.ts +6 -6
  202. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +88 -1
  203. package/src/render/shaders/gpudriven/cull.wgsl.ts +11 -2
  204. package/src/render/shaders/gpudriven/materialTable.wgsl.ts +3 -1
  205. package/src/render/shaders/gpudriven/shade.wgsl.ts +50 -1
  206. package/src/render/shaders/gpudriven/sunGlass.wgsl.ts +139 -0
  207. package/src/render/shaders/lightVolume.ts +6 -6
  208. package/src/render/shaders/octahedralResolve.ts +50 -17
  209. package/src/render/shaders/recon/lateDepth.wgsl.ts +47 -0
  210. package/src/render/shaders/recon/motion.wgsl.ts +84 -0
  211. package/src/render/shaders/recon/resolve.wgsl.ts +112 -8
  212. package/src/render/shaders/temporalResolve.ts +141 -5
  213. package/src/render/shadowCasters.ts +28 -1
  214. package/src/render/shadowMap.ts +231 -37
  215. package/src/render/sunGlassTint.ts +86 -0
  216. package/src/render/temporalAa.ts +227 -0
  217. package/src/render/temporalPass.ts +88 -12
  218. package/src/render/translucentQueue.ts +20 -1
@@ -2,6 +2,7 @@ import { mat3 } from 'gl-matrix';
2
2
 
3
3
  import { createEmptyTexture2D } from './emptyTexture.ts';
4
4
  import { LIVE_POINT_SHADOW_MAPS, POINT_SHADOW_POOL } from './lightBudget.ts';
5
+ import { PointGlassTint } from './pointGlassTint.ts';
5
6
  import { pointShadowFaceRotation } from './pointShadowImage.ts';
6
7
  import { compileProgram, uniformLocations } from './shader.ts';
7
8
  import { OCTAHEDRAL_RESOLVE_FRAG, OCTAHEDRAL_RESOLVE_VERT } from './shaders/octahedralResolve.ts';
@@ -34,6 +35,37 @@ export interface PointShadowTarget {
34
35
  far: number,
35
36
  near: number,
36
37
  ): void;
38
+ /** Whether the array has grown its glass layers: `growForGlass`. */
39
+ readonly hasGlass: boolean;
40
+ /** Whether `face` of `layer` holds glass, which a face that offers none must clear. */
41
+ heldGlass(layer: number, face: number): boolean;
42
+ /** The depth scratch again, cleared, for the face's glass. */
43
+ beginGlassFace(gl: WebGL2RenderingContext): void;
44
+ resolveGlassFace(
45
+ gl: WebGL2RenderingContext,
46
+ layer: number,
47
+ face: number,
48
+ far: number,
49
+ near: number,
50
+ ): void;
51
+ /** The colour scratch, cleared to white, for what the face's glass lets through. */
52
+ beginTintFace(gl: WebGL2RenderingContext): void;
53
+ resolveTintFace(gl: WebGL2RenderingContext, layer: number, face: number, drawn: boolean): void;
54
+ }
55
+
56
+ /**
57
+ * Every published layer as the array stores it: `layer × stride`, and −1 left as −1.
58
+ *
59
+ * **Applied to the resolved arrays right after each resolve**, so every consumer of an uploaded
60
+ * index reads the physical layer: once glass arrives, light L's depth is at 2L and its glass at
61
+ * 2L + 1 (`PointShadowArray.growForGlass`). In place, because the resolve rewrites these every time.
62
+ */
63
+ export function strideLayers(layers: Int32Array, stride: number): void {
64
+ if (stride === 1) return;
65
+ for (let i = 0; i < layers.length; i++) {
66
+ const layer = layers[i] as number;
67
+ if (layer >= 0) layers[i] = layer * stride;
68
+ }
37
69
  }
38
70
 
39
71
  /**
@@ -53,12 +85,16 @@ export interface PointShadowTarget {
53
85
  * never the same object.
54
86
  */
55
87
  export class PointShadowArray implements PointShadowTarget {
56
- readonly texture: WebGLTexture;
57
88
  /**
58
- * How many layers this array holds: the point pool, the two live transition maps, and two more
59
- * for each rectangle a world said would cast. See the constructor's `extraLayers`.
89
+ * How many lights this array holds: the point pool, the two live transition maps, and two more
90
+ * for each rectangle a world said would cast. See the constructor's `extraLayers`. Counted in
91
+ * lights, so it does not move when glass doubles the storage under it.
60
92
  */
61
93
  readonly layers: number;
94
+ /** Replaced once, when glass first casts: see `growForGlass`. */
95
+ private current: WebGLTexture;
96
+ /** What the lights' glass lets through, made with the glass layers. */
97
+ private tint: PointGlassTint | null = null;
62
98
 
63
99
  private readonly framebuffer: WebGLFramebuffer;
64
100
  private readonly scratchTexture: WebGLTexture;
@@ -96,23 +132,8 @@ export class PointShadowArray implements PointShadowTarget {
96
132
  this.layers =
97
133
  Math.min(lightCount, POINT_SHADOW_POOL) + LIVE_POINT_SHADOW_MAPS + Math.max(0, extraLayers);
98
134
 
99
- const texture = gl.createTexture();
100
- if (texture === null) throw new Error('PointShadowArray: createTexture failed');
101
- this.texture = texture;
102
- gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
103
- gl.texStorage3D(
104
- gl.TEXTURE_2D_ARRAY,
105
- 1,
106
- gl.DEPTH_COMPONENT24,
107
- OCTAHEDRAL_EDGE,
108
- OCTAHEDRAL_EDGE,
109
- this.layers,
110
- );
111
- /* One storage level and no filtering, so textureLod(..., 0.0) is the only fetch there is. */
112
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
113
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
114
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
115
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
135
+ const texture = depthArray(gl, this.layers);
136
+ this.current = texture;
116
137
 
117
138
  const scratchTexture = gl.createTexture();
118
139
  if (scratchTexture === null) throw new Error('PointShadowArray: createTexture failed');
@@ -183,6 +204,117 @@ export class PointShadowArray implements PointShadowTarget {
183
204
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
184
205
  }
185
206
 
207
+ /** The array texture the lit pass samples. Replaced once, when glass first casts. */
208
+ get texture(): WebGLTexture {
209
+ return this.current;
210
+ }
211
+
212
+ get hasGlass(): boolean {
213
+ return this.tint !== null;
214
+ }
215
+
216
+ /** Physical layers a light: 1, or 2 once glass has interleaved the array. */
217
+ get stride(): number {
218
+ return this.tint === null ? 1 : 2;
219
+ }
220
+
221
+ /** The glass tint, one layer a light, once glass has cast; the lit pass samples it. */
222
+ get tintTexture(): WebGLTexture | null {
223
+ return this.tint?.texture ?? null;
224
+ }
225
+
226
+ /**
227
+ * Interleave the array for glass: light L's depth moves to 2L and its glass takes 2L + 1, and
228
+ * the tint is made. Returns whether it grew — once, on the first face whose casters held glass.
229
+ *
230
+ * **Interleaved rather than stacked above the opaque half** because a shader cannot ask an array
231
+ * its layer count under naga, so it cannot find `L + N`; `layer + 1` it can. **Every map already
232
+ * baked is carried across** by a depth blit, so nothing goes dark, and **no map needs baking
233
+ * again**: this runs on the first face that saw glass, so no map baked before it saw any, and a
234
+ * glass layer born at the far plane with a white tint is exactly what its bake would draw. What
235
+ * it costs is the doubled depth (4.19 MB a light at 1024) and the tint (5.6 MB a light with its
236
+ * chain, 1.4 MB at `'half'`), from then on.
237
+ */
238
+ growForGlass(gl: WebGL2RenderingContext, tintEdge: number): boolean {
239
+ if (this.tint !== null) return false;
240
+ const old = this.current;
241
+ const texture = depthArray(gl, this.layers * 2);
242
+ /* Off the unit it was made on, as the constructor leaves its own: see `beginFace`. */
243
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
244
+ const read = gl.createFramebuffer();
245
+ if (read === null) throw new Error('PointShadowArray: createFramebuffer failed');
246
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, read);
247
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.framebuffer);
248
+ for (let layer = 0; layer < this.layers; layer++) {
249
+ gl.framebufferTextureLayer(gl.READ_FRAMEBUFFER, gl.DEPTH_ATTACHMENT, old, 0, layer);
250
+ gl.framebufferTextureLayer(gl.DRAW_FRAMEBUFFER, gl.DEPTH_ATTACHMENT, texture, 0, 2 * layer);
251
+ gl.blitFramebuffer(
252
+ 0,
253
+ 0,
254
+ OCTAHEDRAL_EDGE,
255
+ OCTAHEDRAL_EDGE,
256
+ 0,
257
+ 0,
258
+ OCTAHEDRAL_EDGE,
259
+ OCTAHEDRAL_EDGE,
260
+ gl.DEPTH_BUFFER_BIT,
261
+ gl.NEAREST,
262
+ );
263
+ }
264
+ /* Nothing is nearer a lamp than a pane that has not been drawn. */
265
+ for (let layer = 0; layer < this.layers; layer++) {
266
+ gl.framebufferTextureLayer(
267
+ gl.DRAW_FRAMEBUFFER,
268
+ gl.DEPTH_ATTACHMENT,
269
+ texture,
270
+ 0,
271
+ 2 * layer + 1,
272
+ );
273
+ gl.clearBufferfv(gl.DEPTH, 0, FAR_PLANE);
274
+ }
275
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
276
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
277
+ gl.deleteFramebuffer(read);
278
+ gl.deleteTexture(old);
279
+ this.current = texture;
280
+ this.tint = new PointGlassTint(gl, this.faceSize, tintEdge, this.layers);
281
+ return true;
282
+ }
283
+
284
+ heldGlass(layer: number, face: number): boolean {
285
+ return this.tint?.held(layer, face) ?? false;
286
+ }
287
+
288
+ /** The depth scratch, cleared, for the face's glass: `beginFace` exactly. */
289
+ beginGlassFace(gl: WebGL2RenderingContext): void {
290
+ this.beginFace(gl);
291
+ }
292
+
293
+ /** The face's glass depth into light `layer`'s glass layer, 2L + 1. */
294
+ resolveGlassFace(
295
+ gl: WebGL2RenderingContext,
296
+ layer: number,
297
+ face: number,
298
+ far: number,
299
+ near: number,
300
+ ): void {
301
+ if (this.tint === null) return;
302
+ this.resolveInto(gl, 2 * layer + 1, face, far, near);
303
+ }
304
+
305
+ beginTintFace(gl: WebGL2RenderingContext): void {
306
+ this.tint?.beginFace(gl);
307
+ }
308
+
309
+ resolveTintFace(gl: WebGL2RenderingContext, layer: number, face: number, drawn: boolean): void {
310
+ this.tint?.resolveFace(gl, layer, face, drawn);
311
+ }
312
+
313
+ /** The chain frost reads, for every light whose tint a round touched: once a round. */
314
+ finishTints(gl: WebGL2RenderingContext): void {
315
+ this.tint?.fillMips(gl);
316
+ }
317
+
186
318
  /**
187
319
  * Bind the scratch as the render target for one face of one bake, and clear it.
188
320
  *
@@ -224,9 +356,20 @@ export class PointShadowArray implements PointShadowTarget {
224
356
  face: number,
225
357
  far: number,
226
358
  near: number,
359
+ ): void {
360
+ this.resolveInto(gl, layer * this.stride, face, far, near);
361
+ }
362
+
363
+ /** The scratch into physical layer `physical`'s region for `face`. */
364
+ private resolveInto(
365
+ gl: WebGL2RenderingContext,
366
+ physical: number,
367
+ face: number,
368
+ far: number,
369
+ near: number,
227
370
  ): void {
228
371
  gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
229
- gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, this.texture, 0, layer);
372
+ gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, this.current, 0, physical);
230
373
  gl.viewport(0, 0, OCTAHEDRAL_EDGE, OCTAHEDRAL_EDGE);
231
374
 
232
375
  gl.useProgram(this.program);
@@ -267,10 +410,33 @@ export class PointShadowArray implements PointShadowTarget {
267
410
  dispose(gl: WebGL2RenderingContext): void {
268
411
  gl.deleteFramebuffer(this.framebuffer);
269
412
  gl.deleteFramebuffer(this.scratchFramebuffer);
270
- gl.deleteTexture(this.texture);
413
+ gl.deleteTexture(this.current);
271
414
  gl.deleteTexture(this.scratchTexture);
415
+ this.tint?.dispose(gl);
272
416
  gl.deleteTexture(this.parked);
273
417
  gl.deleteProgram(this.program);
274
418
  gl.deleteVertexArray(this.vao);
275
419
  }
276
420
  }
421
+
422
+ const FAR_PLANE = new Float32Array([1]);
423
+
424
+ /** The depth array: one storage level and no filtering, so textureLod(..., 0.0) is the only fetch. */
425
+ function depthArray(gl: WebGL2RenderingContext, layers: number): WebGLTexture {
426
+ const texture = gl.createTexture();
427
+ if (texture === null) throw new Error('PointShadowArray: createTexture failed');
428
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
429
+ gl.texStorage3D(
430
+ gl.TEXTURE_2D_ARRAY,
431
+ 1,
432
+ gl.DEPTH_COMPONENT24,
433
+ OCTAHEDRAL_EDGE,
434
+ OCTAHEDRAL_EDGE,
435
+ layers,
436
+ );
437
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
438
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
439
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
440
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
441
+ return texture;
442
+ }
@@ -256,15 +256,15 @@ export function runPointShadowBakes<M extends StaleCheck, L extends BakeableLigh
256
256
  * **The run decays rather than resetting, and it climbs past the threshold before it stops.**
257
257
  *
258
258
  * A light that wanders on a curve is not stale on *every* frame — near the turning points of
259
- * its travel it moves less in a frame than the rebake tolerance, so a run that reset to zero
260
- * lost the tracking allowance there and the map dropped back to two faces a frame. The shadow
261
- * then travelled smoothly through the fast part of the wander and stepped through the slow
262
- * part, which is not motion and not stillness: it reads as a vibration. Reported that way,
263
- * against a brazier, once the tracking allowance was in.
259
+ * its travel it moves less in a frame than the rebake tolerance. When the run decided whether a
260
+ * wanderer got its whole cube, a run that reset to zero dropped it back to two faces a frame
261
+ * there, and the shadow stepped through the slow part of the wander: reported as a vibration
262
+ * against a brazier. Every image-holding map is re-baked whole now, so the run only orders the
263
+ * queue, and the hysteresis keeps a wanderer behind the lights that settle instead of letting
264
+ * it jump ahead at every turning point.
264
265
  *
265
- * So the run is hysteresis. It climbs to twice the threshold, which buys a light that has
266
- * earned the allowance three quiet frames before it loses it, and decays one frame at a time
267
- * so a light that genuinely settles stops re-baking rather than holding a budget for ever.
266
+ * It climbs to twice the threshold, which keeps a light chronic for three quiet frames, and
267
+ * decays one frame at a time so a light that genuinely settles rejoins the ordinary order.
268
268
  */
269
269
  const previous = scratch.staleRun[slot] ?? 0;
270
270
  const run = stale
@@ -285,37 +285,40 @@ export function runPointShadowBakes<M extends StaleCheck, L extends BakeableLigh
285
285
  let faces = facesPerFrame;
286
286
  let urgentSpent = false;
287
287
  /*
288
- * **One wandering light finishes its cube in the frame it started it**, the same allowance a
289
- * light with no image at all gets, and for a reason that turns out to be the same one: a map
290
- * dribbled out at two faces a frame is a map nobody can sample correctly until it lands.
288
+ * **A map that holds an image is re-baked whole or not at all**, and one being shaded may take a
289
+ * whole cube a frame outside the budget to make that possible.
291
290
  *
292
- * A cold light cannot be sampled *at all* until its six faces are in, so it takes the whole
293
- * cube at once. A chronically stale one can be sampled throughout and is wrong in a quieter
294
- * way: `planBake` pins the origin for the whole of a resumed bake, so six faces spread over
295
- * three frames means the published origin steps once every third frame. The shader shoots from
296
- * that origin, so the shadow it draws steps with it — reported on a brazier as motion that was
297
- * "really fast, not smooth, snappy, and unrealistic", which is a 20 Hz sample of a flame that
298
- * wanders about once a second.
291
+ * Faces land in the layer the shader samples as they are drawn, and `planBake` publishes the
292
+ * origin they were drawn from only with the last one. So a re-bake spread over frames is a cube
293
+ * the shader reads from the old origin with part of it drawn from the new one — and a face is a
294
+ * 90 degree frustum, so the part drawn from somewhere else is a straight-edged quadrilateral of
295
+ * wrong shadow on the ground under the lamp. With the budget spent elsewhere it could hold for a
296
+ * second and more. Reported under a brazier as a dark shape that came and went.
299
297
  *
300
- * Given the cube in one frame the origin moves every frame and the shadow travels instead.
298
+ * A map with no image yet is not sampled until its sixth face lands (see
299
+ * `PointShadowSystem.bind`), so it still fills a few faces a frame, and a cold one being shaded
300
+ * still jumps the queue with all six.
301
301
  *
302
- * **Bounded to one light, and it has to be.** Eight braziers each taking six passes is the 48
303
- * passes and 89 ms `pointShadowFacesPerFrame` exists to prevent. One is the light being shaded
304
- * that most recently went stale, which is the one somebody is standing next to; the rest keep
305
- * dribbling under the ordinary budget, and their shadows step rather than travel. That is the
306
- * right way round: a shadow you are looking at moves, and a shadow across the square does not
307
- * cost the frame.
302
+ * The allowance used to be for a *chronically* stale light only — a wandering flame — whose
303
+ * dribbled cube published a new origin once every third frame, so its shadow stepped at 20 Hz
304
+ * where the flame wanders about once a second: reported as motion that was "really fast, not
305
+ * smooth, snappy, and unrealistic". Any shaded light is owed the same now, because a cube read
306
+ * part-way is wrong in the same place for the same reason whether or not it will be stale again.
308
307
  *
309
- * It is spent *after* the cold and settling groups, because `planPointShadowBakes` orders the
310
- * chronic ones last — so a light arriving in range still gets its first image ahead of a flame
311
- * refining one it already has.
308
+ * **Bounded to one light, and it has to be.** Eight braziers each taking six passes is the 48
309
+ * passes and 89 ms `pointShadowFacesPerFrame` exists to prevent. The first in the plan's order
310
+ * takes it, and `planPointShadowBakes` puts the lights that settled ahead of the ones that never
311
+ * do, so a flame refining its image cannot hold a correction hostage. The rest keep sampling a
312
+ * complete image from a few centimetres away until their turn, which is the trade
313
+ * `pointShadowRebakeDistance` already makes. A warm map, held but not shaded, is re-baked when a
314
+ * whole cube fits the budget or once it is shaded, and not before.
312
315
  *
313
- * **What would make it wrong** is a caster set heavy enough that six passes over it does not
314
- * fit the frame. Measured against a game whose whole static world is about 8,500 triangles in
315
- * a handful of draws; a world an order of magnitude heavier wants `pointShadowFacesPerFrame`
316
+ * **What would make it wrong** is a caster set heavy enough that six passes over it does not fit
317
+ * the frame. Measured against a game whose whole static world is about 8,500 triangles in a
318
+ * handful of draws; a world an order of magnitude heavier wants `pointShadowFacesPerFrame`
316
319
  * raised or this given up, and `gpuTiming` is how to tell which.
317
320
  */
318
- let trackingSpent = false;
321
+ let refreshSpent = false;
319
322
  for (let i = 0; i < planned && faces > 0; i++) {
320
323
  const slot = scratch.order[i] ?? -1;
321
324
  /*
@@ -328,13 +331,20 @@ export function runPointShadowBakes<M extends StaleCheck, L extends BakeableLigh
328
331
  if (light === undefined || map === undefined) continue;
329
332
 
330
333
  const sampled = slot < pool.sampledCount;
331
- const cold = sampled && scratch.ready[slot] === 0;
332
- const urgent = cold && !urgentSpent;
333
- /* A light being shaded whose map will be stale again next frame however it is served. */
334
- const tracking = !cold && sampled && scratch.chronic[slot] === 1 && !trackingSpent;
335
- if (urgent) urgentSpent = true;
336
- if (tracking) trackingSpent = true;
337
- faces -= bake(map, light, staticCasters, urgent || tracking ? faceCount : faces);
334
+ if (scratch.ready[slot] !== 1) {
335
+ /* No image to spoil: a few faces a frame, or all six at once for one being shaded. */
336
+ const urgent = sampled && !urgentSpent;
337
+ if (urgent) urgentSpent = true;
338
+ faces -= bake(map, light, staticCasters, urgent ? faceCount : faces);
339
+ continue;
340
+ }
341
+ if (faces >= faceCount) {
342
+ faces -= bake(map, light, staticCasters, faceCount);
343
+ continue;
344
+ }
345
+ if (!sampled || refreshSpent) continue;
346
+ refreshSpent = true;
347
+ faces -= bake(map, light, staticCasters, faceCount);
338
348
  }
339
349
 
340
350
  /*
@@ -36,6 +36,19 @@ export { FACE_COUNT, POINT_SHADOW_NEAR };
36
36
 
37
37
  const scratchViewProj = mat4.create();
38
38
 
39
+ /**
40
+ * What a bake asks of the renderer about glass, after each face's opaque depth.
41
+ *
42
+ * **Asked before it is drawn** because the answer decides whether the face binds a glass target at
43
+ * all: a face that offers no glass and holds none from a previous owner does no glass work.
44
+ */
45
+ export interface PointGlassDraw {
46
+ /** Whether the face just drawn offered any glass. */
47
+ offered(): boolean;
48
+ /** Draw that glass: its depth, or what it lets through lit from the bake origin. */
49
+ draw(pass: 'depth' | 'tint', x: number, y: number, z: number): void;
50
+ }
51
+
39
52
  export class PointShadowMap {
40
53
  readonly size: number;
41
54
  /**
@@ -163,6 +176,8 @@ export class PointShadowMap {
163
176
  near = POINT_SHADOW_NEAR,
164
177
  sourceRadius = DEFAULT_SOURCE_RADIUS,
165
178
  maxFaces = FACE_COUNT,
179
+ /** The face's glass, when glass shadows are on: `PointGlassDraw`. */
180
+ glass: PointGlassDraw | null = null,
166
181
  ): number {
167
182
  /*
168
183
  * What to render now, **and what to render it for**. A bake in flight owns the parameters
@@ -220,6 +235,7 @@ export class PointShadowMap {
220
235
  * next face's pass does care about.
221
236
  */
222
237
  array.resolveFace(gl, this.layer, face, plan.range, plan.near);
238
+ if (glass !== null) this.bakeGlassFace(gl, array, glass, face, plan);
223
239
  }
224
240
 
225
241
  gl.enable(gl.CULL_FACE);
@@ -237,5 +253,30 @@ export class PointShadowMap {
237
253
  * needing disposal is exactly the shape of a leak nobody notices. The array is disposed by the
238
254
  * renderer that built it.
239
255
  */
256
+ /**
257
+ * The face's glass, twice: where the nearest pane is, then what the panes let through.
258
+ *
259
+ * A face that offers no glass still clears a region a previous owner's glass is in — the depth to
260
+ * the far plane and the tint to white, from scratches cleared and not drawn into — because a pool
261
+ * slot changes hands with its layer.
262
+ */
263
+ private bakeGlassFace(
264
+ gl: WebGL2RenderingContext,
265
+ array: PointShadowTarget,
266
+ glass: PointGlassDraw,
267
+ face: number,
268
+ plan: FaceRange,
269
+ ): void {
270
+ if (!array.hasGlass) return;
271
+ const offered = glass.offered();
272
+ if (!offered && !array.heldGlass(this.layer, face)) return;
273
+ array.beginGlassFace(gl);
274
+ if (offered) glass.draw('depth', plan.x, plan.y, plan.z);
275
+ array.resolveGlassFace(gl, this.layer, face, plan.range, plan.near);
276
+ array.beginTintFace(gl);
277
+ if (offered) glass.draw('tint', plan.x, plan.y, plan.z);
278
+ array.resolveTintFace(gl, this.layer, face, offered);
279
+ }
280
+
240
281
  dispose(_gl: WebGL2RenderingContext): void {}
241
282
  }
@@ -0,0 +1,24 @@
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
+
13
+ export function frameOfChange(serial: number, inFrame: boolean): number {
14
+ return inFrame ? serial : serial + 1;
15
+ }
16
+
17
+ export function keepsPrevious(lastChange: number, frame: number): boolean {
18
+ return lastChange !== frame;
19
+ }
20
+
21
+ /** Slots `0 … n-1` pair with last frame's; a slot past last frame's count is new and has no past. */
22
+ export function pairedInstances(previousCount: number, count: number): number {
23
+ return Math.min(previousCount, count);
24
+ }
@@ -12,10 +12,15 @@
12
12
  * a different part of a texel than the one it stands for, which at a grazing angle is a different
13
13
  * depth; the tolerance grows with the motion's length.
14
14
  *
15
+ * **And in proportion to the surface's own slope.** A new jitter samples a surface somewhere else, so
16
+ * on one turned from the eye two frames' samples of it differ by up to a texel's worth of its depth
17
+ * with nothing moved. `surfaceSlope` measures that step on the surface's own side of any silhouette.
18
+ *
15
19
  * **Normals, for the case depth cannot see.** A leaf's two sides are at one depth, and the history of
16
20
  * the front is not the back. The forward frame has no normal buffer, so both normals come from their
17
21
  * frame's depth — `worldPositionFromDepth` and `normalFromPositions` — in world space, where the two
18
- * frames agree.
22
+ * frames agree. **A normal differenced across an edge is not a normal**, so the caller weighs each by
23
+ * `planarity`: at an edge, a sliver, or anything finer than a texel the depth test decides alone.
19
24
  *
20
25
  * **Continuous in every input**, because a step is a line on screen along which the history is kept on
21
26
  * one frame and dropped on the next: every threshold is a smoothstep, never a comparison.
@@ -51,12 +56,57 @@ function smoothstep(edge0: number, edge1: number, x: number): number {
51
56
  return t * t * (3 - 2 * t);
52
57
  }
53
58
 
59
+ /**
60
+ * The band over which three texels stop being one plane: how far their two steps in inverse depth
61
+ * fail to cancel, as a share of the two together. None of it is a plane at any angle; all of it is an
62
+ * edge — one side steps, the other does not — or a sliver, both stepping the same way.
63
+ *
64
+ * **A share and not a size**, which is the lesson of the rods in `resolve.test.ts`: a part a
65
+ * hundredth of the depth in front of a wall passes the depth test, and a normal differenced across it
66
+ * still leans by its height over a texel's width — nearly edge-on where texels are small. What would
67
+ * make this wrong is a curved surface judged at its apex, where both steps lean one way and the
68
+ * normal is sound; there it only gives the normals less say, which the depth test does not need.
69
+ */
70
+ export const PLANAR_FROM = 0.25;
71
+ export const PLANAR_TO = 0.5;
72
+
73
+ /**
74
+ * How sure it is that `centre` and its neighbours either side along one axis lie on one plane, one to
75
+ * zero. Inverse depth is affine across a plane on screen, so a plane's two steps cancel whatever its
76
+ * angle. One where a neighbour is missing — zero, off the picture or undrawn — or where there is no
77
+ * step to compare: nothing to judge by, so a normal keeps the say it always had.
78
+ */
79
+ export function planarity(before: number, centre: number, after: number): number {
80
+ if (!(before > 0) || !(centre > 0) || !(after > 0)) return 1;
81
+ const a = 1 / after - 1 / centre;
82
+ const b = 1 / before - 1 / centre;
83
+ const spread = Math.abs(a) + Math.abs(b);
84
+ if (!(spread > 0)) return 1;
85
+ return 1 - smoothstep(PLANAR_FROM, PLANAR_TO, Math.abs(a + b) / spread);
86
+ }
87
+
88
+ /**
89
+ * How far a surface's depth moves across one texel along an axis, relative to the depth: the smaller
90
+ * of the steps to its two neighbours, because beside a silhouette one of them is the surface behind.
91
+ * Zero at a sliver — nearer or farther than both, so each step is to another surface — and one
92
+ * neighbour's step where only one is there.
93
+ */
94
+ export function surfaceSlope(before: number, centre: number, after: number): number {
95
+ const a = after > 0 ? after - centre : Number.NaN;
96
+ const b = before > 0 ? before - centre : Number.NaN;
97
+ if (Number.isNaN(a)) return Number.isNaN(b) ? 0 : Math.abs(b) / centre;
98
+ if (Number.isNaN(b)) return Math.abs(a) / centre;
99
+ if (a * b > 0) return 0;
100
+ return Math.min(Math.abs(a), Math.abs(b)) / centre;
101
+ }
102
+
54
103
  /**
55
104
  * One where the history is trustworthy, zero where it is not, and continuous between.
56
105
  *
57
106
  * `currDepth` is the view depth this surface should have had last frame; `histDepth` the view depth
58
107
  * last frame held where it lands, or zero where it landed off the screen; `motionLength` the motion's
59
- * length in uv; `normalDot` the cosine between the two frames' world normals there.
108
+ * length in uv; `normalDot` the cosine between the two frames' world normals there; `slope` the
109
+ * surface's `surfaceSlope`, which widens the tolerance one for one.
60
110
  */
61
111
  export function disocclusionWeight(
62
112
  currDepth: number,
@@ -64,12 +114,13 @@ export function disocclusionWeight(
64
114
  motionLength: number,
65
115
  normalDot: number,
66
116
  params: DisocclusionParams,
117
+ slope = 0,
67
118
  ): number {
68
119
  if (!(currDepth > 0) || !(histDepth > 0) || !(motionLength >= 0) || Number.isNaN(normalDot)) {
69
120
  return 0;
70
121
  }
71
122
  const disagreement = Math.abs(currDepth - histDepth) / currDepth;
72
- const tolerance = params.depthTolerance + params.motionScale * motionLength;
123
+ const tolerance = params.depthTolerance + params.motionScale * motionLength + slope;
73
124
  const depth = 1 - smoothstep(tolerance, 2 * tolerance, disagreement);
74
125
  const normal = smoothstep(params.normalFloor, params.normalCeiling, normalDot);
75
126
  return depth * normal;
@@ -0,0 +1,29 @@
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(
13
+ reconstructing: boolean,
14
+ mirror: boolean,
15
+ probe: boolean,
16
+ presented: boolean,
17
+ ): boolean {
18
+ return reconstructing && !mirror && !probe && !presented;
19
+ }
20
+
21
+ /** The same set as `routesLate`, named for the other reader. */
22
+ export function tracksMotion(
23
+ reconstructing: boolean,
24
+ mirror: boolean,
25
+ probe: boolean,
26
+ presented: boolean,
27
+ ): boolean {
28
+ return routesLate(reconstructing, mirror, probe, presented);
29
+ }