@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
@@ -0,0 +1,118 @@
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
+
19
+ export const MOVER_KIND = 'drift.mover' as const;
20
+
21
+ export interface Mover {
22
+ readonly kind: typeof MOVER_KIND;
23
+ /** The renderer that last drew it, from `claimRendererId`; -1 before its first draw. */
24
+ renderer: number;
25
+ /** That renderer's frame serial at the last draw. */
26
+ frame: number;
27
+ readonly model: Float32Array;
28
+ readonly previousModel: Float32Array;
29
+ /**
30
+ * This frame's palette, exactly `paletteLength` floats long once a skinned draw has set it.
31
+ * Swapped with the previous one each frame and reallocated only when the rig changes.
32
+ */
33
+ palette: Float32Array;
34
+ previousPalette: Float32Array;
35
+ paletteLength: number;
36
+ previousPaletteLength: number;
37
+ }
38
+
39
+ /** Nothing to reproject from: first draw, a missed frame, another renderer, or a changed rig. */
40
+ export const MOVER_NONE = 0;
41
+ /** Drawn on the frame before by this renderer: `previousModel` and `previousPalette` are real. */
42
+ export const MOVER_MOTION = 1;
43
+ /** Drawn already this frame. The second draw is a different object and the caller's error. */
44
+ export const MOVER_TWICE = 2;
45
+ export type MoverVerdict = typeof MOVER_NONE | typeof MOVER_MOTION | typeof MOVER_TWICE;
46
+
47
+ let nextRendererId = 1;
48
+
49
+ /** A number no other renderer in this page holds, so a mover can tell renderers apart. */
50
+ export function claimRendererId(): number {
51
+ const id = nextRendererId;
52
+ nextRendererId += 1;
53
+ return id;
54
+ }
55
+
56
+ export function createMover(): Mover {
57
+ return {
58
+ kind: MOVER_KIND,
59
+ renderer: -1,
60
+ frame: -2,
61
+ model: new Float32Array(16),
62
+ previousModel: new Float32Array(16),
63
+ palette: new Float32Array(0),
64
+ previousPalette: new Float32Array(0),
65
+ paletteLength: 0,
66
+ previousPaletteLength: 0,
67
+ };
68
+ }
69
+
70
+ export function isMover(value: unknown): value is Mover {
71
+ return (
72
+ typeof value === 'object' && value !== null && (value as { kind?: unknown }).kind === MOVER_KIND
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Advance a mover to this draw, and say whether last frame's state is usable.
78
+ *
79
+ * **A second draw in one frame changes nothing**, so the first draw's motion stands; the caller
80
+ * warns. Otherwise what the mover held becomes its previous and this draw becomes its current —
81
+ * the palettes by swapping two arrays, so a steady rig allocates nothing after its first frame.
82
+ *
83
+ * **A changed joint count is no motion**: skinning a vertex with a palette of a different rig is
84
+ * not an old pose of this one.
85
+ */
86
+ export function advanceMover(
87
+ mover: Mover,
88
+ renderer: number,
89
+ frame: number,
90
+ model: ArrayLike<number>,
91
+ palette: Float32Array | null,
92
+ ): MoverVerdict {
93
+ if (mover.renderer === renderer && mover.frame === frame) return MOVER_TWICE;
94
+ const drawnLast = mover.renderer === renderer && mover.frame === frame - 1;
95
+
96
+ mover.previousModel.set(mover.model);
97
+ const spare = mover.previousPalette;
98
+ mover.previousPalette = mover.palette;
99
+ mover.palette = spare;
100
+ mover.previousPaletteLength = mover.paletteLength;
101
+
102
+ mover.model.set(model);
103
+ if (palette === null) {
104
+ mover.paletteLength = 0;
105
+ } else {
106
+ /* Exactly the rig's length, because the array is uploaded as it stands; reallocated only when
107
+ the rig changes, which a steady character never does. */
108
+ if (mover.palette.length !== palette.length) mover.palette = new Float32Array(palette.length);
109
+ mover.palette.set(palette);
110
+ mover.paletteLength = palette.length;
111
+ }
112
+ mover.renderer = renderer;
113
+ mover.frame = frame;
114
+
115
+ if (!drawnLast) return MOVER_NONE;
116
+ if (mover.paletteLength !== mover.previousPaletteLength) return MOVER_NONE;
117
+ return MOVER_MOTION;
118
+ }
@@ -21,8 +21,10 @@
21
21
  * previous view depth in its third channel; otherwise it is the camera's, through the surface's
22
22
  * world position.
23
23
  * 4. Where the surface was is read from the history — its colour and the weight it had gathered — and
24
- * last frame's depth there says whether it is the same surface; the two frames' normals, from their
25
- * depths, catch thin geometry. The disocclusion scales the history's weight.
24
+ * last frame's depth says whether it is the same surface: the nearest of the nine texels around
25
+ * where the dilated texel was, forgiven its own slope across a texel. The two frames' normals, from
26
+ * their depths, catch thin geometry, each with the say its planarity gives it. The disocclusion
27
+ * scales the history's weight.
26
28
  * 5. The history is clipped to the box, its weight capped so a tenth of each frame is always new, and
27
29
  * the two are mixed by their weights. Where the pixel has gathered less than one sample's worth,
28
30
  * the render read bicubically fills the gap. The history keeps colour and weight; the picture shown
@@ -33,7 +35,13 @@
33
35
  * buffer before a sample reaches this arithmetic, as the temporal resolve already does.
34
36
  */
35
37
  import { clipToBox, rgbToYCoCg, varianceBox, yCoCgToRgb } from './clamp.ts';
36
- import { disocclusionWeight, normalFromPositions, worldPositionFromDepth } from './disocclusion.ts';
38
+ import {
39
+ disocclusionWeight,
40
+ normalFromPositions,
41
+ planarity,
42
+ surfaceSlope,
43
+ worldPositionFromDepth,
44
+ } from './disocclusion.ts';
37
45
  import type { DisocclusionParams } from './disocclusion.ts';
38
46
  import { sampleBilinear, sampleHistoryBicubic5 } from './reproject.ts';
39
47
 
@@ -209,6 +217,59 @@ function worldAt(
209
217
  return 1 / w;
210
218
  }
211
219
 
220
+ /**
221
+ * A texel's view depth at its unjittered uv, or 0 — `worldAt` without the position, which is all a
222
+ * search over depths needs: one row of the inverse rather than four. Zero off the picture, where
223
+ * `worldAt` would clamp, because a neighbour that is not there is not a step.
224
+ */
225
+ function viewDepthAt(
226
+ depths: ArrayLike<number>,
227
+ inverse: ArrayLike<number>,
228
+ jitter: ArrayLike<number>,
229
+ width: number,
230
+ height: number,
231
+ x: number,
232
+ y: number,
233
+ ): number {
234
+ if (x < 0 || y < 0 || x >= width || y >= height) return 0;
235
+ const u = (x + 0.5 - (jitter[0] as number)) / width;
236
+ const v = (y + 0.5 - (jitter[1] as number)) / height;
237
+ const m = inverse;
238
+ const w =
239
+ (m[3] as number) * (u * 2 - 1) +
240
+ (m[7] as number) * (v * 2 - 1) +
241
+ (m[11] as number) * (depths[y * width + x] as number) +
242
+ (m[15] as number);
243
+ return Math.abs(w) > 1e-12 ? 1 / w : 0;
244
+ }
245
+
246
+ /** What a texel's four neighbours say about its surface: see `planarity` and `surfaceSlope`. */
247
+ const SURFACE = { planar: 1, slope: 0 };
248
+
249
+ function surfaceAt(
250
+ depths: ArrayLike<number>,
251
+ inverse: ArrayLike<number>,
252
+ jitter: ArrayLike<number>,
253
+ width: number,
254
+ height: number,
255
+ x: number,
256
+ y: number,
257
+ centre: number,
258
+ ): void {
259
+ let planar = 1;
260
+ let slope = 0;
261
+ for (let axis = 0; axis < 2; axis += 1) {
262
+ const ax = 1 - axis;
263
+ const ay = axis;
264
+ const after = viewDepthAt(depths, inverse, jitter, width, height, x + ax, y + ay);
265
+ const before = viewDepthAt(depths, inverse, jitter, width, height, x - ax, y - ay);
266
+ planar *= planarity(before, centre, after);
267
+ slope = Math.max(slope, surfaceSlope(before, centre, after));
268
+ }
269
+ SURFACE.planar = planar;
270
+ SURFACE.slope = slope;
271
+ }
272
+
212
273
  /** The world normal at a texel, from its depth and its right and upper neighbours'. */
213
274
  function normalAt(
214
275
  depths: ArrayLike<number>,
@@ -282,20 +343,84 @@ function historyWeight(frame: ResolveFrame, u: number, v: number, dx: number, dy
282
343
  4,
283
344
  HISTORY,
284
345
  );
285
- const px = clampTexel(Math.round(pu * rw + (frame.previousJitter[0] as number) - 0.5), rw);
286
- const py = clampTexel(Math.round(pv * rh + (frame.previousJitter[1] as number) - 0.5), rh);
287
- const held = worldAt(
346
+ /*
347
+ * **Last frame's depth is searched for this surface, around where the dilated texel was.** The
348
+ * surface being judged is the dilated texel's, which beside an edge is a neighbour's, so judging
349
+ * it by the texel under the pixel compares two surfaces: the side of every silhouette away from the
350
+ * eye dropped its history on every frame of a still picture. And a new jitter samples a surface
351
+ * somewhere else, so the nearest of last frame's nine is taken rather than the one the rounding
352
+ * picks. What it gives up is a disocclusion narrower than a texel, which keeps a history that the
353
+ * neighbourhood clip then bounds.
354
+ */
355
+ const su = (dx + 0.5 - (frame.jitter[0] as number)) / rw + mu;
356
+ const sv = (dy + 0.5 - (frame.jitter[1] as number)) / rh + mv;
357
+ const cx = clampTexel(Math.round(su * rw + (frame.previousJitter[0] as number) - 0.5), rw);
358
+ const cy = clampTexel(Math.round(sv * rh + (frame.previousJitter[1] as number) - 0.5), rh);
359
+ /*
360
+ * The first texel looked at answers alone when it agrees within the tolerance, which it does
361
+ * everywhere but at an edge: the depth test trusts it fully whichever texel would have won, so
362
+ * only an edge pays for the other eight.
363
+ */
364
+ let held = viewDepthAt(
288
365
  frame.previousDepth,
289
366
  frame.previousInverseViewProj,
290
367
  frame.previousJitter,
291
368
  rw,
292
369
  rh,
293
- px,
294
- py,
295
- RIGHT,
370
+ cx,
371
+ cy,
296
372
  );
373
+ let px = cx;
374
+ let py = cy;
375
+ if (!(held > 0 && Math.abs(held - expected) <= frame.disocclusion.depthTolerance * expected)) {
376
+ held = 0;
377
+ let nearest = Number.POSITIVE_INFINITY;
378
+ for (let j = -1; j <= 1; j += 1) {
379
+ for (let i = -1; i <= 1; i += 1) {
380
+ const tx = clampTexel(cx + i, rw);
381
+ const ty = clampTexel(cy + j, rh);
382
+ const depth = viewDepthAt(
383
+ frame.previousDepth,
384
+ frame.previousInverseViewProj,
385
+ frame.previousJitter,
386
+ rw,
387
+ rh,
388
+ tx,
389
+ ty,
390
+ );
391
+ if (!(depth > 0)) continue;
392
+ const off = Math.abs(depth - expected);
393
+ if (off < nearest) {
394
+ nearest = off;
395
+ held = depth;
396
+ px = tx;
397
+ py = ty;
398
+ }
399
+ }
400
+ }
401
+ }
402
+
403
+ const here = viewDepthAt(frame.depth, frame.inverseViewProj, frame.jitter, rw, rh, dx, dy);
404
+ surfaceAt(frame.depth, frame.inverseViewProj, frame.jitter, rw, rh, dx, dy, here);
405
+ const slope = SURFACE.slope;
406
+ let planar = SURFACE.planar;
407
+ if (planar > 0) {
408
+ surfaceAt(
409
+ frame.previousDepth,
410
+ frame.previousInverseViewProj,
411
+ frame.previousJitter,
412
+ rw,
413
+ rh,
414
+ px,
415
+ py,
416
+ held,
417
+ );
418
+ planar *= SURFACE.planar;
419
+ }
420
+ /* A normal has the say its planarity gives it; at an edge or a sliver, none, and none is computed. */
297
421
  let normalDot = 1;
298
422
  if (
423
+ planar > 0 &&
299
424
  normalAt(frame.depth, frame.inverseViewProj, frame.jitter, frame.eye, rw, rh, dx, dy, NORMAL) &&
300
425
  normalAt(
301
426
  frame.previousDepth,
@@ -309,10 +434,11 @@ function historyWeight(frame: ResolveFrame, u: number, v: number, dx: number, dy
309
434
  PREVIOUS_NORMAL,
310
435
  )
311
436
  ) {
312
- normalDot =
437
+ const cosine =
313
438
  (NORMAL[0] as number) * (PREVIOUS_NORMAL[0] as number) +
314
439
  (NORMAL[1] as number) * (PREVIOUS_NORMAL[1] as number) +
315
440
  (NORMAL[2] as number) * (PREVIOUS_NORMAL[2] as number);
441
+ normalDot = 1 - planar * (1 - cosine);
316
442
  }
317
443
  const trust = disocclusionWeight(
318
444
  expected,
@@ -320,6 +446,7 @@ function historyWeight(frame: ResolveFrame, u: number, v: number, dx: number, dy
320
446
  Math.hypot(mu, mv),
321
447
  normalDot,
322
448
  frame.disocclusion,
449
+ slope,
323
450
  );
324
451
  return (HISTORY[3] as number) * trust;
325
452
  }
@@ -37,6 +37,9 @@ export type ShadowFilterTaps = 4 | 8 | 12;
37
37
  export type DirectionalShadowDepthLayers = 1 | 2;
38
38
  export type WaterReflectionFilterTaps = 1 | 5 | 9;
39
39
 
40
+ /** How light through glass is coloured: see `RenderQuality.glassShadows`. */
41
+ export type GlassShadows = 'full' | 'half' | 'off';
42
+
40
43
  export interface RenderQuality {
41
44
  /**
42
45
  * The transfer curve applied at the end of every shaded pass.
@@ -149,6 +152,25 @@ export interface RenderQuality {
149
152
  readonly lightVolumeSamples: number;
150
153
  /** Independently composited static sun depths; two preserves common overlaps. */
151
154
  readonly directionalShadowDepthLayers: DirectionalShadowDepthLayers;
155
+ /**
156
+ * Whether light through glass takes the glass's colour, and at what size.
157
+ *
158
+ * **`'full'`, the default**: every shadow a glass caster reaches gains a layer holding the depth of
159
+ * the nearest pane and a tint layer holding what the panes let through, at the size of the depth
160
+ * map beside it — 8 bytes a texel, so 32 MB for a 2048 sun map and 8 MB for a 1024 lamp layer.
161
+ * **`'half'`** halves the tint layers only (the glass depth must match its array): 20 MB and 5 MB,
162
+ * and a coloured patch softer than an opaque shadow's edge. **`'off'`** is glass before this
163
+ * existed: a pane casts nothing at all.
164
+ *
165
+ * Nothing is allocated until a glass caster first reaches a shadow, so a scene with no glass pays
166
+ * no memory and no pass whatever this says. **The lit shader is the exception**: `'full'` and
167
+ * `'half'` compile the glass lookup into it, and a lookup that finds no glass and returns at its
168
+ * first line still costs the lit pass its registers — measured on the courtyard at night, 0.27 ms
169
+ * at 720p on WebGPU, 0.42 ms on WebGL2 and 1.2 ms at 4K. `'off'` compiles none of it, so a world
170
+ * that never casts through glass is that much faster for saying so. What would make `'full'` wrong
171
+ * is a phone budget with a lot of glass; that is what `'half'` is for. See `glassShadow.ts`.
172
+ */
173
+ readonly glassShadows: GlassShadows;
152
174
  /** Horizontal world-space reach over which a directional shadow dissolves. */
153
175
  readonly directionalShadowMaxDistance: number;
154
176
  /** Maximum horizontal projection per vertical metre before low-angle shadows vanish. */
@@ -556,6 +578,20 @@ export interface RenderQuality {
556
578
  * the render is a quarter of the output and no reconstruction holds an edge through that. A
557
579
  * negative number is off, not a clamp to the bottom: it cannot mean "a little".
558
580
  *
581
+ * **What it needs from you.** A `createMover()` per moving object, passed with that object's
582
+ * draws where `drawMesh` takes last frame's matrix — the renderer keeps last frame's model and,
583
+ * for a skinned draw, last frame's pose. Instanced batches and meshes rewritten with
584
+ * `updateMesh` need nothing: the batch and the mesh are their own identity. And `cameraCut()` at
585
+ * every cut, because nothing detects one: without it a cut blends the previous shot into the
586
+ * next. Blended draws — translucent meshes, particles, plumes, film, bolts, lines, world text —
587
+ * are drawn after the upscale at the output size and need nothing either; light volumes stay in
588
+ * the reconstructed picture, being a march that keeps its history.
589
+ *
590
+ * **What it cannot give back** is the resolution it draws at: an edge that never moves is softer
591
+ * than a native frame's at any ratio, because two thirds of the pixels at 1.5 are a guess made
592
+ * sharp. Measured on `demo/dev/ghost.html` against a native frame at four samples, still edges
593
+ * err by about 8 levels where a native frame at one sample errs by 3.
594
+ *
559
595
  * `?recon=` is how it gets A/B'd in one reload.
560
596
  */
561
597
  readonly reconstruction: number;
@@ -838,6 +874,7 @@ export const DEFAULT_RENDER_QUALITY: Readonly<RenderQuality> = Object.freeze({
838
874
  shadowFilterTaps: MAX_SHADOW_FILTER_TAPS,
839
875
  lightVolumeSamples: 32,
840
876
  directionalShadowDepthLayers: 2,
877
+ glassShadows: 'full',
841
878
  directionalShadowMaxDistance: 6,
842
879
  directionalShadowMaxSlope: 3,
843
880
  pointShadowRebakeDistance: 0.001,
@@ -998,6 +1035,7 @@ export function resolveRenderQuality(options: RenderQualityOptions = {}): Readon
998
1035
  lightVolumeSamples: options.lightVolumeSamples ?? DEFAULT_RENDER_QUALITY.lightVolumeSamples,
999
1036
  directionalShadowDepthLayers:
1000
1037
  options.directionalShadowDepthLayers ?? DEFAULT_RENDER_QUALITY.directionalShadowDepthLayers,
1038
+ glassShadows: options.glassShadows ?? DEFAULT_RENDER_QUALITY.glassShadows,
1001
1039
  directionalShadowMaxDistance:
1002
1040
  options.directionalShadowMaxDistance ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxDistance,
1003
1041
  directionalShadowMaxSlope:
@@ -1128,6 +1166,15 @@ export function resolveRenderQuality(options: RenderQualityOptions = {}): Readon
1128
1166
  `RenderQuality.directionalShadowDepthLayers must be 1 or 2, got ${quality.directionalShadowDepthLayers}`,
1129
1167
  );
1130
1168
  }
1169
+ if (
1170
+ quality.glassShadows !== 'full' &&
1171
+ quality.glassShadows !== 'half' &&
1172
+ quality.glassShadows !== 'off'
1173
+ ) {
1174
+ throw new Error(
1175
+ `RenderQuality.glassShadows must be 'full', 'half' or 'off', got ${String(quality.glassShadows)}`,
1176
+ );
1177
+ }
1131
1178
  if (quality.plumeNoiseOctaves > 3) {
1132
1179
  throw new Error(
1133
1180
  `RenderQuality.plumeNoiseOctaves must be <= 3, got ${quality.plumeNoiseOctaves}`,
@@ -129,6 +129,18 @@ export function recordingGl(
129
129
  COLOR_BUFFER_BIT: 0x00004000,
130
130
  /* Named so a test can tell a mesh that declared itself deforming from one that did not. */
131
131
  ARRAY_BUFFER: 0x8892,
132
+ /* Named so a blend a pass sets can be read back as numbers: glass multiplies what it lets
133
+ through, and a test that cannot tell DST_COLOR from a recorder cannot tell it multiplies. */
134
+ ZERO: 0,
135
+ DST_ALPHA: 0x0304,
136
+ DST_COLOR: 0x0306,
137
+ FUNC_ADD: 0x8006,
138
+ COLOR: 0x1800,
139
+ DEPTH: 0x1801,
140
+ /* Named so a copy can be read as from-and-to: a blit's two framebuffers are the whole claim. */
141
+ READ_FRAMEBUFFER: 0x8ca8,
142
+ DRAW_FRAMEBUFFER: 0x8ca9,
143
+ BLEND: 0x0be2,
132
144
  STATIC_DRAW: 0x88e4,
133
145
  DYNAMIC_DRAW: 0x88e8,
134
146
  };
@@ -82,6 +82,8 @@ export class SceneTarget {
82
82
  private colorCopyFramebuffer: WebGLFramebuffer | null = null;
83
83
  /** Whether this frame has filled it. One snapshot serves every refracting draw in a frame. */
84
84
  private colorCopyFilled = false;
85
+ /** Whether this frame's copy has its mip chain, which frosted glass reads. See `snapshotColor`. */
86
+ private colorCopyMips = false;
85
87
  /**
86
88
  * The colour grade, as a 3D texture on unit 4, and the table it was built from.
87
89
  *
@@ -275,12 +277,24 @@ export class SceneTarget {
275
277
  */
276
278
  if (this.colorCopy === null) this.colorCopy = gl.createTexture();
277
279
  gl.bindTexture(gl.TEXTURE_2D, this.colorCopy);
278
- if (this.floatColor) {
279
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.HALF_FLOAT, null);
280
- } else {
281
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
280
+ /*
281
+ * **Every level, so glass can read a blurrier one.** Frosted glass reads the copy at a level its
282
+ * frost picks, and a texture sampled through a mipmapping filter with a level missing is
283
+ * incomplete and reads black — so the chain is allocated here, once per size, and filled only on
284
+ * a frame that draws glass (`snapshotColor`). Plain refraction reads level 0, as it always has.
285
+ */
286
+ const levels = Math.floor(Math.log2(Math.max(width, height))) + 1;
287
+ for (let level = 0; level < levels; level += 1) {
288
+ const w = Math.max(1, width >> level);
289
+ const h = Math.max(1, height >> level);
290
+ if (this.floatColor) {
291
+ gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA16F, w, h, 0, gl.RGBA, gl.HALF_FLOAT, null);
292
+ } else {
293
+ gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
294
+ }
282
295
  }
283
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
296
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, levels - 1);
297
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
284
298
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
285
299
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
286
300
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
@@ -419,6 +433,7 @@ export class SceneTarget {
419
433
  /* Last frame's snapshot is last frame's scene. See `snapshotDepth`. */
420
434
  this.depthCopyFilled = false;
421
435
  this.colorCopyFilled = false;
436
+ this.colorCopyMips = false;
422
437
  this.bind();
423
438
  }
424
439
 
@@ -516,12 +531,21 @@ export class SceneTarget {
516
531
  * Null where the copy could not be made, and a caller must read that as "do not refract" rather
517
532
  * than as a black texture — refracting against black paints every pane the colour of a hole.
518
533
  */
519
- snapshotColor(afresh = false): WebGLTexture | null {
534
+ snapshotColor(
535
+ afresh = false,
536
+ /** Fill the copy's mip chain as well, once a frame: what frosted glass reads. */
537
+ mips = false,
538
+ ): WebGLTexture | null {
520
539
  if (this.colorCopy === null || this.colorCopyFramebuffer === null) return null;
521
- if (this.colorCopyFilled && !afresh) return this.colorCopy;
540
+ if (this.colorCopyFilled && !afresh) {
541
+ if (mips) this.fillColorCopyMips();
542
+ return this.colorCopy;
543
+ }
522
544
  const { gl } = this;
523
545
  const source = this.msaaFramebuffer ?? this.framebuffer;
524
546
  if (source === null) return null;
547
+ const drawing = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
548
+ const reading = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
525
549
 
526
550
  gl.bindFramebuffer(gl.READ_FRAMEBUFFER, source);
527
551
  gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.colorCopyFramebuffer);
@@ -546,13 +570,33 @@ export class SceneTarget {
546
570
  gl.NEAREST,
547
571
  );
548
572
  const refused = gl.getError() !== gl.NO_ERROR;
549
- /* Back to what the caller was drawing into, whichever that was. */
550
- gl.bindFramebuffer(gl.FRAMEBUFFER, source);
573
+ /*
574
+ * **Back to what the caller was drawing into, whichever that was** — read before the copy and
575
+ * handed back, never assumed. It was the scene's framebuffer, assumed, and a pane drawn into a
576
+ * reflection probe's face then left the scene's bound: the probe attached its next face to it,
577
+ * a face-sized colour beside a screen-sized depth, and every draw after refused as incomplete.
578
+ */
579
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, drawing);
580
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, reading);
551
581
  if (refused) return null;
552
582
  this.colorCopyFilled = true;
583
+ this.colorCopyMips = false;
584
+ if (mips) this.fillColorCopyMips();
553
585
  return this.colorCopy;
554
586
  }
555
587
 
588
+ /** The copy's mip chain from its level 0, once per copy. */
589
+ private fillColorCopyMips(): void {
590
+ if (this.colorCopyMips || this.colorCopy === null) return;
591
+ const { gl } = this;
592
+ /* Bound on whichever unit is active and handed back, so the caller's texture there survives. */
593
+ const previous = gl.getParameter(gl.TEXTURE_BINDING_2D) as WebGLTexture | null;
594
+ gl.bindTexture(gl.TEXTURE_2D, this.colorCopy);
595
+ gl.generateMipmap(gl.TEXTURE_2D);
596
+ gl.bindTexture(gl.TEXTURE_2D, previous);
597
+ this.colorCopyMips = true;
598
+ }
599
+
556
600
  snapshotDepth(
557
601
  /**
558
602
  * Take the copy again even though one has already been taken this frame.
@@ -573,6 +617,8 @@ export class SceneTarget {
573
617
  const { gl } = this;
574
618
  const source = this.msaaFramebuffer ?? this.framebuffer;
575
619
  if (source === null) return null;
620
+ const drawing = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
621
+ const reading = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
576
622
 
577
623
  gl.bindFramebuffer(gl.READ_FRAMEBUFFER, source);
578
624
  gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.depthCopyFramebuffer);
@@ -595,8 +641,9 @@ export class SceneTarget {
595
641
  gl.NEAREST,
596
642
  );
597
643
  const refused = gl.getError() !== gl.NO_ERROR;
598
- /* Back to what the caller was drawing into, whichever that was. */
599
- gl.bindFramebuffer(gl.FRAMEBUFFER, source);
644
+ /* Back to what the caller was drawing into, read rather than assumed: see `snapshotColor`. */
645
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, drawing);
646
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, reading);
600
647
  if (refused) return null;
601
648
  this.depthCopyFilled = true;
602
649
  return this.depthCopy;
@@ -743,7 +790,12 @@ export class SceneTarget {
743
790
  * `veilColor` and `veilAlpha` are positional defaults and every existing caller passes them
744
791
  * by position.
745
792
  */
746
- temporal?: { readonly reprojection: Float32Array; readonly blend: number },
793
+ temporal?: {
794
+ readonly reprojection: Float32Array;
795
+ readonly blend: number;
796
+ readonly periodStart: boolean;
797
+ readonly depthToViewZ: Float32Array;
798
+ },
747
799
  ): void {
748
800
  const { gl } = this;
749
801
  this.depthWanted =
@@ -883,6 +935,8 @@ export class SceneTarget {
883
935
  this.width,
884
936
  this.height,
885
937
  temporal.blend,
938
+ temporal.periodStart,
939
+ temporal.depthToViewZ,
886
940
  );
887
941
  }
888
942