@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
@@ -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.
@@ -37,19 +42,66 @@ function smoothstep(edge0, edge1, x) {
37
42
  const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));
38
43
  return t * t * (3 - 2 * t);
39
44
  }
45
+ /**
46
+ * The band over which three texels stop being one plane: how far their two steps in inverse depth
47
+ * fail to cancel, as a share of the two together. None of it is a plane at any angle; all of it is an
48
+ * edge — one side steps, the other does not — or a sliver, both stepping the same way.
49
+ *
50
+ * **A share and not a size**, which is the lesson of the rods in `resolve.test.ts`: a part a
51
+ * hundredth of the depth in front of a wall passes the depth test, and a normal differenced across it
52
+ * still leans by its height over a texel's width — nearly edge-on where texels are small. What would
53
+ * make this wrong is a curved surface judged at its apex, where both steps lean one way and the
54
+ * normal is sound; there it only gives the normals less say, which the depth test does not need.
55
+ */
56
+ export const PLANAR_FROM = 0.25;
57
+ export const PLANAR_TO = 0.5;
58
+ /**
59
+ * How sure it is that `centre` and its neighbours either side along one axis lie on one plane, one to
60
+ * zero. Inverse depth is affine across a plane on screen, so a plane's two steps cancel whatever its
61
+ * angle. One where a neighbour is missing — zero, off the picture or undrawn — or where there is no
62
+ * step to compare: nothing to judge by, so a normal keeps the say it always had.
63
+ */
64
+ export function planarity(before, centre, after) {
65
+ if (!(before > 0) || !(centre > 0) || !(after > 0))
66
+ return 1;
67
+ const a = 1 / after - 1 / centre;
68
+ const b = 1 / before - 1 / centre;
69
+ const spread = Math.abs(a) + Math.abs(b);
70
+ if (!(spread > 0))
71
+ return 1;
72
+ return 1 - smoothstep(PLANAR_FROM, PLANAR_TO, Math.abs(a + b) / spread);
73
+ }
74
+ /**
75
+ * How far a surface's depth moves across one texel along an axis, relative to the depth: the smaller
76
+ * of the steps to its two neighbours, because beside a silhouette one of them is the surface behind.
77
+ * Zero at a sliver — nearer or farther than both, so each step is to another surface — and one
78
+ * neighbour's step where only one is there.
79
+ */
80
+ export function surfaceSlope(before, centre, after) {
81
+ const a = after > 0 ? after - centre : Number.NaN;
82
+ const b = before > 0 ? before - centre : Number.NaN;
83
+ if (Number.isNaN(a))
84
+ return Number.isNaN(b) ? 0 : Math.abs(b) / centre;
85
+ if (Number.isNaN(b))
86
+ return Math.abs(a) / centre;
87
+ if (a * b > 0)
88
+ return 0;
89
+ return Math.min(Math.abs(a), Math.abs(b)) / centre;
90
+ }
40
91
  /**
41
92
  * One where the history is trustworthy, zero where it is not, and continuous between.
42
93
  *
43
94
  * `currDepth` is the view depth this surface should have had last frame; `histDepth` the view depth
44
95
  * last frame held where it lands, or zero where it landed off the screen; `motionLength` the motion's
45
- * length in uv; `normalDot` the cosine between the two frames' world normals there.
96
+ * length in uv; `normalDot` the cosine between the two frames' world normals there; `slope` the
97
+ * surface's `surfaceSlope`, which widens the tolerance one for one.
46
98
  */
47
- export function disocclusionWeight(currDepth, histDepth, motionLength, normalDot, params) {
99
+ export function disocclusionWeight(currDepth, histDepth, motionLength, normalDot, params, slope = 0) {
48
100
  if (!(currDepth > 0) || !(histDepth > 0) || !(motionLength >= 0) || Number.isNaN(normalDot)) {
49
101
  return 0;
50
102
  }
51
103
  const disagreement = Math.abs(currDepth - histDepth) / currDepth;
52
- const tolerance = params.depthTolerance + params.motionScale * motionLength;
104
+ const tolerance = params.depthTolerance + params.motionScale * motionLength + slope;
53
105
  const depth = 1 - smoothstep(tolerance, 2 * tolerance, disagreement);
54
106
  const normal = smoothstep(params.normalFloor, params.normalCeiling, normalDot);
55
107
  return depth * normal;
@@ -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
+ }
@@ -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,7 @@
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.js';
36
- import { disocclusionWeight, normalFromPositions, worldPositionFromDepth } from './disocclusion.js';
38
+ import { disocclusionWeight, normalFromPositions, planarity, surfaceSlope, worldPositionFromDepth, } from './disocclusion.js';
37
39
  import { sampleBilinear, sampleHistoryBicubic5 } from './reproject.js';
38
40
  /**
39
41
  * A tenth of each frame kept, as the temporal resolve keeps; a box of a standard deviation and a
@@ -131,6 +133,39 @@ function worldAt(depths, inverse, jitter, width, height, x, y, out) {
131
133
  m[15];
132
134
  return 1 / w;
133
135
  }
136
+ /**
137
+ * A texel's view depth at its unjittered uv, or 0 — `worldAt` without the position, which is all a
138
+ * search over depths needs: one row of the inverse rather than four. Zero off the picture, where
139
+ * `worldAt` would clamp, because a neighbour that is not there is not a step.
140
+ */
141
+ function viewDepthAt(depths, inverse, jitter, width, height, x, y) {
142
+ if (x < 0 || y < 0 || x >= width || y >= height)
143
+ return 0;
144
+ const u = (x + 0.5 - jitter[0]) / width;
145
+ const v = (y + 0.5 - jitter[1]) / height;
146
+ const m = inverse;
147
+ const w = m[3] * (u * 2 - 1) +
148
+ m[7] * (v * 2 - 1) +
149
+ m[11] * depths[y * width + x] +
150
+ m[15];
151
+ return Math.abs(w) > 1e-12 ? 1 / w : 0;
152
+ }
153
+ /** What a texel's four neighbours say about its surface: see `planarity` and `surfaceSlope`. */
154
+ const SURFACE = { planar: 1, slope: 0 };
155
+ function surfaceAt(depths, inverse, jitter, width, height, x, y, centre) {
156
+ let planar = 1;
157
+ let slope = 0;
158
+ for (let axis = 0; axis < 2; axis += 1) {
159
+ const ax = 1 - axis;
160
+ const ay = axis;
161
+ const after = viewDepthAt(depths, inverse, jitter, width, height, x + ax, y + ay);
162
+ const before = viewDepthAt(depths, inverse, jitter, width, height, x - ax, y - ay);
163
+ planar *= planarity(before, centre, after);
164
+ slope = Math.max(slope, surfaceSlope(before, centre, after));
165
+ }
166
+ SURFACE.planar = planar;
167
+ SURFACE.slope = slope;
168
+ }
134
169
  /** The world normal at a texel, from its depth and its right and upper neighbours'. */
135
170
  function normalAt(depths, inverse, jitter, eye, width, height, x, y, out) {
136
171
  if (worldAt(depths, inverse, jitter, width, height, x, y, WORLD) <= 0)
@@ -190,18 +225,66 @@ function historyWeight(frame, u, v, dx, dy) {
190
225
  if (!(pu >= 0 && pu <= 1 && pv >= 0 && pv <= 1))
191
226
  return 0;
192
227
  sampleHistoryBicubic5(frame.history, pu * frame.outputWidth - 0.5, pv * frame.outputHeight - 0.5, frame.outputWidth, frame.outputHeight, 4, HISTORY);
193
- const px = clampTexel(Math.round(pu * rw + frame.previousJitter[0] - 0.5), rw);
194
- const py = clampTexel(Math.round(pv * rh + frame.previousJitter[1] - 0.5), rh);
195
- const held = worldAt(frame.previousDepth, frame.previousInverseViewProj, frame.previousJitter, rw, rh, px, py, RIGHT);
228
+ /*
229
+ * **Last frame's depth is searched for this surface, around where the dilated texel was.** The
230
+ * surface being judged is the dilated texel's, which beside an edge is a neighbour's, so judging
231
+ * it by the texel under the pixel compares two surfaces: the side of every silhouette away from the
232
+ * eye dropped its history on every frame of a still picture. And a new jitter samples a surface
233
+ * somewhere else, so the nearest of last frame's nine is taken rather than the one the rounding
234
+ * picks. What it gives up is a disocclusion narrower than a texel, which keeps a history that the
235
+ * neighbourhood clip then bounds.
236
+ */
237
+ const su = (dx + 0.5 - frame.jitter[0]) / rw + mu;
238
+ const sv = (dy + 0.5 - frame.jitter[1]) / rh + mv;
239
+ const cx = clampTexel(Math.round(su * rw + frame.previousJitter[0] - 0.5), rw);
240
+ const cy = clampTexel(Math.round(sv * rh + frame.previousJitter[1] - 0.5), rh);
241
+ /*
242
+ * The first texel looked at answers alone when it agrees within the tolerance, which it does
243
+ * everywhere but at an edge: the depth test trusts it fully whichever texel would have won, so
244
+ * only an edge pays for the other eight.
245
+ */
246
+ let held = viewDepthAt(frame.previousDepth, frame.previousInverseViewProj, frame.previousJitter, rw, rh, cx, cy);
247
+ let px = cx;
248
+ let py = cy;
249
+ if (!(held > 0 && Math.abs(held - expected) <= frame.disocclusion.depthTolerance * expected)) {
250
+ held = 0;
251
+ let nearest = Number.POSITIVE_INFINITY;
252
+ for (let j = -1; j <= 1; j += 1) {
253
+ for (let i = -1; i <= 1; i += 1) {
254
+ const tx = clampTexel(cx + i, rw);
255
+ const ty = clampTexel(cy + j, rh);
256
+ const depth = viewDepthAt(frame.previousDepth, frame.previousInverseViewProj, frame.previousJitter, rw, rh, tx, ty);
257
+ if (!(depth > 0))
258
+ continue;
259
+ const off = Math.abs(depth - expected);
260
+ if (off < nearest) {
261
+ nearest = off;
262
+ held = depth;
263
+ px = tx;
264
+ py = ty;
265
+ }
266
+ }
267
+ }
268
+ }
269
+ const here = viewDepthAt(frame.depth, frame.inverseViewProj, frame.jitter, rw, rh, dx, dy);
270
+ surfaceAt(frame.depth, frame.inverseViewProj, frame.jitter, rw, rh, dx, dy, here);
271
+ const slope = SURFACE.slope;
272
+ let planar = SURFACE.planar;
273
+ if (planar > 0) {
274
+ surfaceAt(frame.previousDepth, frame.previousInverseViewProj, frame.previousJitter, rw, rh, px, py, held);
275
+ planar *= SURFACE.planar;
276
+ }
277
+ /* A normal has the say its planarity gives it; at an edge or a sliver, none, and none is computed. */
196
278
  let normalDot = 1;
197
- if (normalAt(frame.depth, frame.inverseViewProj, frame.jitter, frame.eye, rw, rh, dx, dy, NORMAL) &&
279
+ if (planar > 0 &&
280
+ normalAt(frame.depth, frame.inverseViewProj, frame.jitter, frame.eye, rw, rh, dx, dy, NORMAL) &&
198
281
  normalAt(frame.previousDepth, frame.previousInverseViewProj, frame.previousJitter, frame.previousEye, rw, rh, px, py, PREVIOUS_NORMAL)) {
199
- normalDot =
200
- NORMAL[0] * PREVIOUS_NORMAL[0] +
201
- NORMAL[1] * PREVIOUS_NORMAL[1] +
202
- NORMAL[2] * PREVIOUS_NORMAL[2];
282
+ const cosine = NORMAL[0] * PREVIOUS_NORMAL[0] +
283
+ NORMAL[1] * PREVIOUS_NORMAL[1] +
284
+ NORMAL[2] * PREVIOUS_NORMAL[2];
285
+ normalDot = 1 - planar * (1 - cosine);
203
286
  }
204
- const trust = disocclusionWeight(expected, held, Math.hypot(mu, mv), normalDot, frame.disocclusion);
287
+ const trust = disocclusionWeight(expected, held, Math.hypot(mu, mv), normalDot, frame.disocclusion, slope);
205
288
  return HISTORY[3] * trust;
206
289
  }
207
290
  /**
@@ -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;
@@ -56,6 +56,7 @@ export const DEFAULT_RENDER_QUALITY = Object.freeze({
56
56
  shadowFilterTaps: MAX_SHADOW_FILTER_TAPS,
57
57
  lightVolumeSamples: 32,
58
58
  directionalShadowDepthLayers: 2,
59
+ glassShadows: 'full',
59
60
  directionalShadowMaxDistance: 6,
60
61
  directionalShadowMaxSlope: 3,
61
62
  pointShadowRebakeDistance: 0.001,
@@ -205,6 +206,7 @@ export function resolveRenderQuality(options = {}) {
205
206
  shadowFilterTaps: options.shadowFilterTaps ?? DEFAULT_RENDER_QUALITY.shadowFilterTaps,
206
207
  lightVolumeSamples: options.lightVolumeSamples ?? DEFAULT_RENDER_QUALITY.lightVolumeSamples,
207
208
  directionalShadowDepthLayers: options.directionalShadowDepthLayers ?? DEFAULT_RENDER_QUALITY.directionalShadowDepthLayers,
209
+ glassShadows: options.glassShadows ?? DEFAULT_RENDER_QUALITY.glassShadows,
208
210
  directionalShadowMaxDistance: options.directionalShadowMaxDistance ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxDistance,
209
211
  directionalShadowMaxSlope: options.directionalShadowMaxSlope ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxSlope,
210
212
  pointShadowRebakeDistance: options.pointShadowRebakeDistance ?? DEFAULT_RENDER_QUALITY.pointShadowRebakeDistance,
@@ -286,6 +288,11 @@ export function resolveRenderQuality(options = {}) {
286
288
  if (quality.directionalShadowDepthLayers !== 1 && quality.directionalShadowDepthLayers !== 2) {
287
289
  throw new Error(`RenderQuality.directionalShadowDepthLayers must be 1 or 2, got ${quality.directionalShadowDepthLayers}`);
288
290
  }
291
+ if (quality.glassShadows !== 'full' &&
292
+ quality.glassShadows !== 'half' &&
293
+ quality.glassShadows !== 'off') {
294
+ throw new Error(`RenderQuality.glassShadows must be 'full', 'half' or 'off', got ${String(quality.glassShadows)}`);
295
+ }
289
296
  if (quality.plumeNoiseOctaves > 3) {
290
297
  throw new Error(`RenderQuality.plumeNoiseOctaves must be <= 3, got ${quality.plumeNoiseOctaves}`);
291
298
  }
@@ -86,6 +86,18 @@ export function recordingGl(options = {}) {
86
86
  COLOR_BUFFER_BIT: 0x00004000,
87
87
  /* Named so a test can tell a mesh that declared itself deforming from one that did not. */
88
88
  ARRAY_BUFFER: 0x8892,
89
+ /* Named so a blend a pass sets can be read back as numbers: glass multiplies what it lets
90
+ through, and a test that cannot tell DST_COLOR from a recorder cannot tell it multiplies. */
91
+ ZERO: 0,
92
+ DST_ALPHA: 0x0304,
93
+ DST_COLOR: 0x0306,
94
+ FUNC_ADD: 0x8006,
95
+ COLOR: 0x1800,
96
+ DEPTH: 0x1801,
97
+ /* Named so a copy can be read as from-and-to: a blit's two framebuffers are the whole claim. */
98
+ READ_FRAMEBUFFER: 0x8ca8,
99
+ DRAW_FRAMEBUFFER: 0x8ca9,
100
+ BLEND: 0x0be2,
89
101
  STATIC_DRAW: 0x88e4,
90
102
  DYNAMIC_DRAW: 0x88e8,
91
103
  };
@@ -72,6 +72,8 @@ export declare class SceneTarget {
72
72
  private colorCopyFramebuffer;
73
73
  /** Whether this frame has filled it. One snapshot serves every refracting draw in a frame. */
74
74
  private colorCopyFilled;
75
+ /** Whether this frame's copy has its mip chain, which frosted glass reads. See `snapshotColor`. */
76
+ private colorCopyMips;
75
77
  /**
76
78
  * The colour grade, as a 3D texture on unit 4, and the table it was built from.
77
79
  *
@@ -221,7 +223,11 @@ export declare class SceneTarget {
221
223
  * Null where the copy could not be made, and a caller must read that as "do not refract" rather
222
224
  * than as a black texture — refracting against black paints every pane the colour of a hole.
223
225
  */
224
- snapshotColor(afresh?: boolean): WebGLTexture | null;
226
+ snapshotColor(afresh?: boolean,
227
+ /** Fill the copy's mip chain as well, once a frame: what frosted glass reads. */
228
+ mips?: boolean): WebGLTexture | null;
229
+ /** The copy's mip chain from its level 0, once per copy. */
230
+ private fillColorCopyMips;
225
231
  snapshotDepth(
226
232
  /**
227
233
  * Take the copy again even though one has already been taken this frame.
@@ -303,6 +309,8 @@ export declare class SceneTarget {
303
309
  }, veilColor?: Vec3, veilAlpha?: number, temporal?: {
304
310
  readonly reprojection: Float32Array;
305
311
  readonly blend: number;
312
+ readonly periodStart: boolean;
313
+ readonly depthToViewZ: Float32Array;
306
314
  }): void;
307
315
  dispose(): void;
308
316
  }