@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
@@ -46,6 +46,7 @@
46
46
  * the window it came through, and without it a barred floor under a smooth shaft is two
47
47
  * halves of one frame disagreeing about the same light.
48
48
  */
49
+ import { SUN_DYNAMIC_LAYER, SUN_PEELED_LAYER, SUN_STATIC_LAYER } from '../shadowMap.js';
49
50
  import { resolveConditionals } from './conditionals.js';
50
51
  export const LIGHT_VOLUME_VERT = `#version 300 es
51
52
  precision highp float;
@@ -143,9 +144,8 @@ uniform vec3 uDustOffset;
143
144
  uniform float uSunShadow;
144
145
  uniform mat4 uLightViewProj;
145
146
  uniform float uShadowMapSize;
146
- uniform sampler2D uStaticShadowMap;
147
- uniform sampler2D uPeeledShadowMap;
148
- uniform sampler2D uDynamicShadowMap;
147
+ /* The sun's static, moving and peeled maps, as layers of one array: see shadowMap.ts. */
148
+ uniform highp sampler2DArray uSunShadows;
149
149
  uniform int uPeeledShadowEnabled;
150
150
  #endif
151
151
 
@@ -308,11 +308,11 @@ float sunReach(vec3 worldPos) {
308
308
  if (edgeFade <= 0.0) return 1.0;
309
309
 
310
310
  float compare = p.z - BIAS;
311
- float blocked = occlusion(textureLod(uStaticShadowMap, p.xy, 0.0).r, compare);
311
+ float blocked = occlusion(textureLod(uSunShadows, vec3(p.xy, ${SUN_STATIC_LAYER}.0), 0.0).r, compare);
312
312
  if (uPeeledShadowEnabled != 0) {
313
- blocked = max(blocked, occlusion(textureLod(uPeeledShadowMap, p.xy, 0.0).r, compare));
313
+ blocked = max(blocked, occlusion(textureLod(uSunShadows, vec3(p.xy, ${SUN_PEELED_LAYER}.0), 0.0).r, compare));
314
314
  }
315
- blocked = max(blocked, occlusion(textureLod(uDynamicShadowMap, p.xy, 0.0).r, compare));
315
+ blocked = max(blocked, occlusion(textureLod(uSunShadows, vec3(p.xy, ${SUN_DYNAMIC_LAYER}.0), 0.0).r, compare));
316
316
  return mix(1.0, 1.0 - blocked, uSunShadow * edgeFade);
317
317
  }
318
318
  #endif
@@ -23,3 +23,11 @@ export declare const OCTAHEDRAL_RESOLVE_VERT = "#version 300 es\nvoid main() {\n
23
23
  * redeclaration and GLSL rejects the whole shader.
24
24
  */
25
25
  export declare const OCTAHEDRAL_RESOLVE_FRAG = "#version 300 es\nprecision highp float;\n\n/** The face the bake just rendered, as a depth texture. */\nuniform highp sampler2D uFace;\n/** That face's world-to-face rotation. See pointShadowFaceRotation. */\nuniform mat3 uFaceRotation;\n/** Which of the six this pass is resolving. Every other texel discards. */\nuniform int uFaceIndex;\nuniform float uFar;\nuniform float uNear;\n/** The octahedral map's edge, so this needs no viewport query. */\nuniform float uEdge;\n\nvec2 octEncode(vec3 d) {\n vec3 n = d / (abs(d.x) + abs(d.y) + abs(d.z));\n vec2 p = n.z >= 0.0\n ? n.xy\n : (1.0 - abs(vec2(n.y, n.x))) * vec2(n.x >= 0.0 ? 1.0 : -1.0, n.y >= 0.0 ? 1.0 : -1.0);\n return p * 0.5 + 0.5;\n}\n\nvec3 octDecode(vec2 uv) {\n vec2 f = uv * 2.0 - 1.0;\n vec3 n = vec3(f.x, f.y, 1.0 - abs(f.x) - abs(f.y));\n float t = max(-n.z, 0.0);\n n.x += n.x >= 0.0 ? -t : t;\n n.y += n.y >= 0.0 ? -t : t;\n return normalize(n);\n}\n\nvec2 octWrapUv(vec2 uv) {\n vec2 f = uv * 2.0 - 1.0;\n if (f.x < -1.0 || f.x > 1.0) {\n f.x = (f.x > 0.0 ? 2.0 : -2.0) - f.x;\n f.y = -f.y;\n }\n if (f.y < -1.0 || f.y > 1.0) {\n f.y = (f.y > 0.0 ? 2.0 : -2.0) - f.y;\n f.x = -f.x;\n }\n return f * 0.5 + 0.5;\n}\n\nvec2 octInsetUv(vec3 d, float edge) {\n return (octEncode(d) * (edge - 2.0) + 1.0) / edge;\n}\n\nvec3 octInsetDir(vec2 uv, float edge) {\n return octDecode(octWrapUv((uv * edge - 1.0) / (edge - 2.0)));\n}\n\nvoid main() {\n vec3 d = octDecode(gl_FragCoord.xy / uEdge);\n\n /*\n * Which face owns this texel. The tie-breaking is x, then y, then z, and it has to match the\n * CPU's exactly or a sliver of texels along a boundary belongs to two faces or to none.\n */\n vec3 a = abs(d);\n int face;\n if (a.x >= a.y && a.x >= a.z) face = d.x >= 0.0 ? 0 : 1;\n else if (a.y >= a.z) face = d.y >= 0.0 ? 2 : 3;\n else face = d.z >= 0.0 ? 4 : 5;\n if (face != uFaceIndex) discard;\n\n /* lookAt looks down -z, so a direction inside this face has a negative z here. */\n vec3 v = uFaceRotation * d;\n float forward = -v.z;\n vec2 faceUv = (v.xy / forward) * 0.5 + 0.5;\n\n /*\n * textureLod at level zero rather than plain texture. The scratch carries one storage level and\n * NEAREST filtering so the fetch is identical, and this call sits after a discard \u2014 which is\n * non-uniform control flow, where an implicit derivative is undefined in GLSL and rejected\n * outright in WGSL. AGENTS.md, 2026-08-07.\n */\n float stored = textureLod(uFace, faceUv, 0.0).r;\n if (stored >= 0.9999) {\n /* Nothing was rendered along this direction, so the stored radius is the far plane. */\n gl_FragDepth = 1.0;\n return;\n }\n float ndc = stored * 2.0 - 1.0;\n float faceLocalZ = (2.0 * uFar * uNear) / (uFar + uNear - ndc * (uFar - uNear));\n /* d is a unit vector, so forward is its cosine to the axis and this is the true radius. */\n gl_FragDepth = clamp((faceLocalZ / forward) / uFar, 0.0, 1.0);\n}";
26
+ /**
27
+ * One face of a lamp's glass tint into its region of the tint layer: the depth resolve's region,
28
+ * and a colour copied rather than a depth decoded.
29
+ *
30
+ * **Nearest, at level zero**, like the depth beside it: the tint's outline has to land on the
31
+ * glass depth's texels, and a filtered copy would smear a pane's colour a texel past its edge.
32
+ */
33
+ export declare const OCTAHEDRAL_RESOLVE_TINT_FRAG = "#version 300 es\nprecision highp float;\n\n/** The face the bake just tinted, as colour. */\nuniform highp sampler2D uFace;\nuniform mat3 uFaceRotation;\nuniform int uFaceIndex;\nuniform float uEdge;\n\nout vec4 outTint;\n\nvec2 octEncode(vec3 d) {\n vec3 n = d / (abs(d.x) + abs(d.y) + abs(d.z));\n vec2 p = n.z >= 0.0\n ? n.xy\n : (1.0 - abs(vec2(n.y, n.x))) * vec2(n.x >= 0.0 ? 1.0 : -1.0, n.y >= 0.0 ? 1.0 : -1.0);\n return p * 0.5 + 0.5;\n}\n\nvec3 octDecode(vec2 uv) {\n vec2 f = uv * 2.0 - 1.0;\n vec3 n = vec3(f.x, f.y, 1.0 - abs(f.x) - abs(f.y));\n float t = max(-n.z, 0.0);\n n.x += n.x >= 0.0 ? -t : t;\n n.y += n.y >= 0.0 ? -t : t;\n return normalize(n);\n}\n\nvec2 octWrapUv(vec2 uv) {\n vec2 f = uv * 2.0 - 1.0;\n if (f.x < -1.0 || f.x > 1.0) {\n f.x = (f.x > 0.0 ? 2.0 : -2.0) - f.x;\n f.y = -f.y;\n }\n if (f.y < -1.0 || f.y > 1.0) {\n f.y = (f.y > 0.0 ? 2.0 : -2.0) - f.y;\n f.x = -f.x;\n }\n return f * 0.5 + 0.5;\n}\n\nvec2 octInsetUv(vec3 d, float edge) {\n return (octEncode(d) * (edge - 2.0) + 1.0) / edge;\n}\n\nvec3 octInsetDir(vec2 uv, float edge) {\n return octDecode(octWrapUv((uv * edge - 1.0) / (edge - 2.0)));\n}\n\nvoid main() {\n vec3 d = octDecode(gl_FragCoord.xy / uEdge);\n\n /*\n * Which face owns this texel. The tie-breaking is x, then y, then z, and it has to match the\n * CPU's exactly or a sliver of texels along a boundary belongs to two faces or to none.\n */\n vec3 a = abs(d);\n int face;\n if (a.x >= a.y && a.x >= a.z) face = d.x >= 0.0 ? 0 : 1;\n else if (a.y >= a.z) face = d.y >= 0.0 ? 2 : 3;\n else face = d.z >= 0.0 ? 4 : 5;\n if (face != uFaceIndex) discard;\n\n /* lookAt looks down -z, so a direction inside this face has a negative z here. */\n vec3 v = uFaceRotation * d;\n float forward = -v.z;\n vec2 faceUv = (v.xy / forward) * 0.5 + 0.5;\n outTint = textureLod(uFace, faceUv, 0.0);\n}";
@@ -10,6 +10,29 @@ void main() {
10
10
  vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
11
11
  gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
12
12
  }`;
13
+ /**
14
+ * Which cube face owns this octahedral texel, and where in that face it looks: `d`, `forward` and
15
+ * `faceUv`, with every texel another face owns discarded. **One text for both resolves**, depth
16
+ * and tint, because the tie-breaking has to agree with itself as well as with the CPU: a sliver
17
+ * of texels given to one face's depth and another face's colour is a tinted seam.
18
+ */
19
+ const FACE_REGION = ` vec3 d = octDecode(gl_FragCoord.xy / uEdge);
20
+
21
+ /*
22
+ * Which face owns this texel. The tie-breaking is x, then y, then z, and it has to match the
23
+ * CPU's exactly or a sliver of texels along a boundary belongs to two faces or to none.
24
+ */
25
+ vec3 a = abs(d);
26
+ int face;
27
+ if (a.x >= a.y && a.x >= a.z) face = d.x >= 0.0 ? 0 : 1;
28
+ else if (a.y >= a.z) face = d.y >= 0.0 ? 2 : 3;
29
+ else face = d.z >= 0.0 ? 4 : 5;
30
+ if (face != uFaceIndex) discard;
31
+
32
+ /* lookAt looks down -z, so a direction inside this face has a negative z here. */
33
+ vec3 v = uFaceRotation * d;
34
+ float forward = -v.z;
35
+ vec2 faceUv = (v.xy / forward) * 0.5 + 0.5;`;
13
36
  /**
14
37
  * One cube face into its region of an octahedral map, as linear radial distance.
15
38
  *
@@ -44,23 +67,7 @@ uniform float uEdge;
44
67
  ${OCTAHEDRAL_GLSL}
45
68
 
46
69
  void main() {
47
- vec3 d = octDecode(gl_FragCoord.xy / uEdge);
48
-
49
- /*
50
- * Which face owns this texel. The tie-breaking is x, then y, then z, and it has to match the
51
- * CPU's exactly or a sliver of texels along a boundary belongs to two faces or to none.
52
- */
53
- vec3 a = abs(d);
54
- int face;
55
- if (a.x >= a.y && a.x >= a.z) face = d.x >= 0.0 ? 0 : 1;
56
- else if (a.y >= a.z) face = d.y >= 0.0 ? 2 : 3;
57
- else face = d.z >= 0.0 ? 4 : 5;
58
- if (face != uFaceIndex) discard;
59
-
60
- /* lookAt looks down -z, so a direction inside this face has a negative z here. */
61
- vec3 v = uFaceRotation * d;
62
- float forward = -v.z;
63
- vec2 faceUv = (v.xy / forward) * 0.5 + 0.5;
70
+ ${FACE_REGION}
64
71
 
65
72
  /*
66
73
  * textureLod at level zero rather than plain texture. The scratch carries one storage level and
@@ -79,3 +86,27 @@ void main() {
79
86
  /* d is a unit vector, so forward is its cosine to the axis and this is the true radius. */
80
87
  gl_FragDepth = clamp((faceLocalZ / forward) / uFar, 0.0, 1.0);
81
88
  }`;
89
+ /**
90
+ * One face of a lamp's glass tint into its region of the tint layer: the depth resolve's region,
91
+ * and a colour copied rather than a depth decoded.
92
+ *
93
+ * **Nearest, at level zero**, like the depth beside it: the tint's outline has to land on the
94
+ * glass depth's texels, and a filtered copy would smear a pane's colour a texel past its edge.
95
+ */
96
+ export const OCTAHEDRAL_RESOLVE_TINT_FRAG = `#version 300 es
97
+ precision highp float;
98
+
99
+ /** The face the bake just tinted, as colour. */
100
+ uniform highp sampler2D uFace;
101
+ uniform mat3 uFaceRotation;
102
+ uniform int uFaceIndex;
103
+ uniform float uEdge;
104
+
105
+ out vec4 outTint;
106
+
107
+ ${OCTAHEDRAL_GLSL}
108
+
109
+ void main() {
110
+ ${FACE_REGION}
111
+ outTint = textureLod(uFace, faceUv, 0.0);
112
+ }`;
@@ -0,0 +1 @@
1
+ export declare const LATE_DEPTH_WGSL: string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The render's depth, taken up to the output size for the draws that land after the upscale.
3
+ *
4
+ * **The nearest of the four render texels around each output pixel**, so a translucent draw is
5
+ * never let through an opaque edge the render did cover. What it gives up: where a translucent
6
+ * surface crosses an opaque silhouette, the edge steps at the render's resolution.
7
+ *
8
+ * Hand-written, and built by concatenation rather than a template literal: the reconstruction's
9
+ * passes have no WebGL2 twin (the AGENTS.md exception for them), and a `${}` inside WGSL source is
10
+ * the thing that rule forbids.
11
+ */
12
+ import { REVERSED_DEPTH } from '../../depthConvention.js';
13
+ /** The nearer of two stored depths: the larger in a reversed buffer. */
14
+ const NEARER = REVERSED_DEPTH ? 'max' : 'min';
15
+ /** Where the search for the nearest starts: the far plane. */
16
+ const FARTHEST = REVERSED_DEPTH ? '0.0' : '1.0';
17
+ export const LATE_DEPTH_WGSL = '@group(0) @binding(0) var source: texture_depth_2d;\n' +
18
+ '@group(0) @binding(1) var<uniform> sizes: vec4<f32>;\n' +
19
+ '\n' +
20
+ '@vertex\n' +
21
+ 'fn lateDepthVert(@builtin(vertex_index) index: u32) -> @builtin(position) vec4<f32> {\n' +
22
+ ' let x = f32((index << 1u) & 2u);\n' +
23
+ ' let y = f32(index & 2u);\n' +
24
+ ' return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);\n' +
25
+ '}\n' +
26
+ '\n' +
27
+ '@fragment\n' +
28
+ 'fn lateDepthFrag(@builtin(position) at: vec4<f32>) -> @builtin(frag_depth) f32 {\n' +
29
+ ' let centre = at.xy * sizes.xy / sizes.zw - vec2<f32>(0.5);\n' +
30
+ ' let base = vec2<i32>(floor(centre));\n' +
31
+ ' let top = vec2<i32>(sizes.xy) - vec2<i32>(1);\n' +
32
+ ' var nearest = ' +
33
+ FARTHEST +
34
+ ';\n' +
35
+ ' for (var y = 0; y < 2; y += 1) {\n' +
36
+ ' for (var x = 0; x < 2; x += 1) {\n' +
37
+ ' let texel = clamp(base + vec2<i32>(x, y), vec2<i32>(0), top);\n' +
38
+ ' nearest = ' +
39
+ NEARER +
40
+ '(nearest, textureLoad(source, texel, 0));\n' +
41
+ ' }\n' +
42
+ ' }\n' +
43
+ ' return nearest;\n' +
44
+ '}\n';
@@ -41,4 +41,4 @@ export declare const MOTION_DRAW_STRIDE = 256;
41
41
  export declare const MOTION_DRAW_FLOATS = 32;
42
42
  /** The motion target's format. Half float holds a uv to a hundredth of a pixel and a view depth. */
43
43
  export declare const MOTION_TARGET_FORMAT: GPUTextureFormat;
44
- export declare const RECON_MOTION_WGSL = "\nstruct MotionFrame {\n /* The scene's own clip transform, with the generated shaders' y negation already folded in. */\n raster: mat4x4<f32>,\n /* This frame's and last frame's view-projections, unjittered, in the matrices' own convention. */\n viewProj: mat4x4<f32>,\n previousViewProj: mat4x4<f32>,\n}\n\nstruct MotionDraw {\n model: mat4x4<f32>,\n previousModel: mat4x4<f32>,\n}\n\n@group(0) @binding(0) var<uniform> frame: MotionFrame;\n@group(0) @binding(1) var<uniform> draw: MotionDraw;\n\nstruct Varyings {\n @builtin(position) clip: vec4<f32>,\n /* Clip positions rather than uv: an interpolator divides by w for us, so dividing these by their\n own interpolated w in the fragment stage is the perspective-correct answer at every pixel. */\n @location(0) now: vec4<f32>,\n @location(1) was: vec4<f32>,\n}\n\n@vertex\nfn motionVert(@location(0) position: vec3<f32>) -> Varyings {\n let world = draw.model * vec4<f32>(position, 1.0);\n let wasWorld = draw.previousModel * vec4<f32>(position, 1.0);\n var out: Varyings;\n out.clip = frame.raster * world;\n out.now = frame.viewProj * world;\n out.was = frame.previousViewProj * wasWorld;\n return out;\n}\n\n/* Normalised device coordinates to the uv the resolve measures in: v increases upward, as the\n references' images do, and the accessor that reads this target is what flips the texture's rows. */\nfn motionUv(clip: vec4<f32>) -> vec2<f32> {\n return clip.xy / clip.w * 0.5 + vec2<f32>(0.5);\n}\n\n@fragment\nfn motionFrag(in: Varyings) -> @location(0) vec4<f32> {\n /*\n * A surface behind last frame's eye has a w that is not positive, and there is no uv for it. The\n * flag stays set: the disocclusion refuses a depth that is not positive, so the resolve drops the\n * history rather than reading it from wherever a divide by a negative w would have pointed.\n */\n let offset = select(vec2<f32>(0.0), motionUv(in.was) - motionUv(in.now), in.was.w > 0.0);\n return vec4<f32>(offset, in.was.w, 1.0);\n}\n";
44
+ export declare const RECON_MOTION_WGSL = "\nstruct MotionFrame {\n /* The scene's own clip transform, with the generated shaders' y negation already folded in. */\n raster: mat4x4<f32>,\n /* This frame's and last frame's view-projections, unjittered, in the matrices' own convention. */\n viewProj: mat4x4<f32>,\n previousViewProj: mat4x4<f32>,\n}\n\nstruct MotionDraw {\n model: mat4x4<f32>,\n previousModel: mat4x4<f32>,\n}\n\n@group(0) @binding(0) var<uniform> frame: MotionFrame;\n@group(0) @binding(1) var<uniform> draw: MotionDraw;\n\nstruct Varyings {\n @builtin(position) clip: vec4<f32>,\n /* Clip positions rather than uv: an interpolator divides by w for us, so dividing these by their\n own interpolated w in the fragment stage is the perspective-correct answer at every pixel. */\n @location(0) now: vec4<f32>,\n @location(1) was: vec4<f32>,\n}\n\n@vertex\nfn motionVert(@location(0) position: vec3<f32>) -> Varyings {\n let world = draw.model * vec4<f32>(position, 1.0);\n let wasWorld = draw.previousModel * vec4<f32>(position, 1.0);\n var out: Varyings;\n out.clip = frame.raster * world;\n out.now = frame.viewProj * world;\n out.was = frame.previousViewProj * wasWorld;\n return out;\n}\n\n/* Normalised device coordinates to the uv the resolve measures in: v increases upward, as the\n references' images do, and the accessor that reads this target is what flips the texture's rows. */\nfn motionUv(clip: vec4<f32>) -> vec2<f32> {\n return clip.xy / clip.w * 0.5 + vec2<f32>(0.5);\n}\n\n/*\n * The skinned stage: the joints this frame and last frame, read from two palette textures of the\n * frame's ring. The same four weighted products in the same order as the scene's own skinning, so\n * the depth this stage produces is the depth the scene wrote.\n */\n@group(1) @binding(0) var palette: texture_2d<f32>;\n@group(1) @binding(1) var previousPalette: texture_2d<f32>;\n\nfn jointOf(source: texture_2d<f32>, index: i32) -> mat4x4<f32> {\n let x = index * 4;\n return mat4x4<f32>(\n textureLoad(source, vec2<i32>(x, 0), 0),\n textureLoad(source, vec2<i32>(x + 1, 0), 0),\n textureLoad(source, vec2<i32>(x + 2, 0), 0),\n textureLoad(source, vec2<i32>(x + 3, 0), 0));\n}\n\nfn skinOf(source: texture_2d<f32>, joints: vec4<f32>, weights: vec4<f32>) -> mat4x4<f32> {\n return jointOf(source, i32(joints.x)) * weights.x\n + jointOf(source, i32(joints.y)) * weights.y\n + jointOf(source, i32(joints.z)) * weights.z\n + jointOf(source, i32(joints.w)) * weights.w;\n}\n\n@vertex\nfn motionSkinnedVert(\n @location(0) position: vec3<f32>,\n @location(11) joints: vec4<f32>,\n @location(12) weights: vec4<f32>,\n) -> Varyings {\n let world = draw.model * (skinOf(palette, joints, weights) * vec4<f32>(position, 1.0));\n let wasWorld =\n draw.previousModel * (skinOf(previousPalette, joints, weights) * vec4<f32>(position, 1.0));\n var out: Varyings;\n out.clip = frame.raster * world;\n out.now = frame.viewProj * world;\n out.was = frame.previousViewProj * wasWorld;\n return out;\n}\n\n/*\n * The rewritten stage: a mesh whose vertices were rewritten this frame reads last frame's rows from\n * a second buffer of the same stride, so each vertex carries both of its positions.\n */\n@vertex\nfn motionDynamicVert(\n @location(0) position: vec3<f32>,\n @location(1) previousPosition: vec3<f32>,\n) -> Varyings {\n let world = draw.model * vec4<f32>(position, 1.0);\n let wasWorld = draw.previousModel * vec4<f32>(previousPosition, 1.0);\n var out: Varyings;\n out.clip = frame.raster * world;\n out.now = frame.viewProj * world;\n out.was = frame.previousViewProj * wasWorld;\n return out;\n}\n\n/*\n * The instanced stage: each instance's matrix this frame and last frame, read from the batch's\n * placement and the copy of last frame's it kept. Slot for slot, so an instance is paired with\n * itself; the per-draw matrices are not read.\n */\n@vertex\nfn motionInstancedVert(\n @location(0) position: vec3<f32>,\n @location(2) m0: vec4<f32>,\n @location(3) m1: vec4<f32>,\n @location(4) m2: vec4<f32>,\n @location(5) m3: vec4<f32>,\n @location(6) p0: vec4<f32>,\n @location(7) p1: vec4<f32>,\n @location(8) p2: vec4<f32>,\n @location(9) p3: vec4<f32>,\n) -> Varyings {\n let world = mat4x4<f32>(m0, m1, m2, m3) * vec4<f32>(position, 1.0);\n let wasWorld = mat4x4<f32>(p0, p1, p2, p3) * vec4<f32>(position, 1.0);\n var out: Varyings;\n out.clip = frame.raster * world;\n out.now = frame.viewProj * world;\n out.was = frame.previousViewProj * wasWorld;\n return out;\n}\n\n@fragment\nfn motionFrag(in: Varyings) -> @location(0) vec4<f32> {\n /*\n * A surface behind last frame's eye has a w that is not positive, and there is no uv for it. The\n * flag stays set: the disocclusion refuses a depth that is not positive, so the resolve drops the\n * history rather than reading it from wherever a divide by a negative w would have pointed.\n */\n let offset = select(vec2<f32>(0.0), motionUv(in.was) - motionUv(in.now), in.was.w > 0.0);\n return vec4<f32>(offset, in.was.w, 1.0);\n}\n";
@@ -83,6 +83,90 @@ fn motionUv(clip: vec4<f32>) -> vec2<f32> {
83
83
  return clip.xy / clip.w * 0.5 + vec2<f32>(0.5);
84
84
  }
85
85
 
86
+ /*
87
+ * The skinned stage: the joints this frame and last frame, read from two palette textures of the
88
+ * frame's ring. The same four weighted products in the same order as the scene's own skinning, so
89
+ * the depth this stage produces is the depth the scene wrote.
90
+ */
91
+ @group(1) @binding(0) var palette: texture_2d<f32>;
92
+ @group(1) @binding(1) var previousPalette: texture_2d<f32>;
93
+
94
+ fn jointOf(source: texture_2d<f32>, index: i32) -> mat4x4<f32> {
95
+ let x = index * 4;
96
+ return mat4x4<f32>(
97
+ textureLoad(source, vec2<i32>(x, 0), 0),
98
+ textureLoad(source, vec2<i32>(x + 1, 0), 0),
99
+ textureLoad(source, vec2<i32>(x + 2, 0), 0),
100
+ textureLoad(source, vec2<i32>(x + 3, 0), 0));
101
+ }
102
+
103
+ fn skinOf(source: texture_2d<f32>, joints: vec4<f32>, weights: vec4<f32>) -> mat4x4<f32> {
104
+ return jointOf(source, i32(joints.x)) * weights.x
105
+ + jointOf(source, i32(joints.y)) * weights.y
106
+ + jointOf(source, i32(joints.z)) * weights.z
107
+ + jointOf(source, i32(joints.w)) * weights.w;
108
+ }
109
+
110
+ @vertex
111
+ fn motionSkinnedVert(
112
+ @location(0) position: vec3<f32>,
113
+ @location(11) joints: vec4<f32>,
114
+ @location(12) weights: vec4<f32>,
115
+ ) -> Varyings {
116
+ let world = draw.model * (skinOf(palette, joints, weights) * vec4<f32>(position, 1.0));
117
+ let wasWorld =
118
+ draw.previousModel * (skinOf(previousPalette, joints, weights) * vec4<f32>(position, 1.0));
119
+ var out: Varyings;
120
+ out.clip = frame.raster * world;
121
+ out.now = frame.viewProj * world;
122
+ out.was = frame.previousViewProj * wasWorld;
123
+ return out;
124
+ }
125
+
126
+ /*
127
+ * The rewritten stage: a mesh whose vertices were rewritten this frame reads last frame's rows from
128
+ * a second buffer of the same stride, so each vertex carries both of its positions.
129
+ */
130
+ @vertex
131
+ fn motionDynamicVert(
132
+ @location(0) position: vec3<f32>,
133
+ @location(1) previousPosition: vec3<f32>,
134
+ ) -> Varyings {
135
+ let world = draw.model * vec4<f32>(position, 1.0);
136
+ let wasWorld = draw.previousModel * vec4<f32>(previousPosition, 1.0);
137
+ var out: Varyings;
138
+ out.clip = frame.raster * world;
139
+ out.now = frame.viewProj * world;
140
+ out.was = frame.previousViewProj * wasWorld;
141
+ return out;
142
+ }
143
+
144
+ /*
145
+ * The instanced stage: each instance's matrix this frame and last frame, read from the batch's
146
+ * placement and the copy of last frame's it kept. Slot for slot, so an instance is paired with
147
+ * itself; the per-draw matrices are not read.
148
+ */
149
+ @vertex
150
+ fn motionInstancedVert(
151
+ @location(0) position: vec3<f32>,
152
+ @location(2) m0: vec4<f32>,
153
+ @location(3) m1: vec4<f32>,
154
+ @location(4) m2: vec4<f32>,
155
+ @location(5) m3: vec4<f32>,
156
+ @location(6) p0: vec4<f32>,
157
+ @location(7) p1: vec4<f32>,
158
+ @location(8) p2: vec4<f32>,
159
+ @location(9) p3: vec4<f32>,
160
+ ) -> Varyings {
161
+ let world = mat4x4<f32>(m0, m1, m2, m3) * vec4<f32>(position, 1.0);
162
+ let wasWorld = mat4x4<f32>(p0, p1, p2, p3) * vec4<f32>(position, 1.0);
163
+ var out: Varyings;
164
+ out.clip = frame.raster * world;
165
+ out.now = frame.viewProj * world;
166
+ out.was = frame.previousViewProj * wasWorld;
167
+ return out;
168
+ }
169
+
86
170
  @fragment
87
171
  fn motionFrag(in: Varyings) -> @location(0) vec4<f32> {
88
172
  /*
@@ -27,7 +27,7 @@ export declare const RECON_HISTORY_FORMAT: GPUTextureFormat;
27
27
  * colour and the weight it has gathered — and `reconSharpenAt(ox, oy) -> vec3<f32>`, the picture
28
28
  * shown, which reads the history the first has already written.
29
29
  */
30
- export declare const RECON_RESOLVE_CORE_WGSL = "\nconst RECON_SAMPLE_SPREAD: f32 = 0.47;\nconst RECON_CONFIDENT_WEIGHT: f32 = 1.0;\n\nfn reconClampTexel(value: i32, size: i32) -> i32 {\n return clamp(value, 0, size - 1);\n}\n\n/* JavaScript's Math.round, which is not WGSL's round: a half goes up, not to even. */\nfn reconRound(value: f32) -> f32 {\n return floor(value + 0.5);\n}\n\n/* Written out rather than the built-in, whose result is undefined where the edges are equal. */\nfn reconSmoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {\n let t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);\n return t * t * (3.0 - 2.0 * t);\n}\n\nfn reconWeightAt(dx: f32, dy: f32) -> f32 {\n return exp(-(dx * dx + dy * dy) / (2.0 * RECON_SAMPLE_SPREAD * RECON_SAMPLE_SPREAD));\n}\n\nfn reconToYCoCg(c: vec3<f32>) -> vec3<f32> {\n return vec3<f32>(\n c.r * 0.25 + c.g * 0.5 + c.b * 0.25,\n c.r * 0.5 - c.b * 0.5,\n -c.r * 0.25 + c.g * 0.5 - c.b * 0.25,\n );\n}\n\nfn reconFromYCoCg(c: vec3<f32>) -> vec3<f32> {\n let base = c.x - c.z;\n return vec3<f32>(base + c.y, c.x + c.z, base - c.y);\n}\n\nstruct ReconBox {\n low: vec3<f32>,\n high: vec3<f32>,\n}\n\n/*\n * Each channel's mean plus or minus gamma of its standard deviation \u2014 and a channel whose nine\n * samples all agree is that value exactly, because a mean of nine equal floats is not always one\n * of them and a box a rounding wide lets a history through by a rounding.\n */\nfn reconVarianceBox(samples: array<vec3<f32>, 9>, gamma: f32) -> ReconBox {\n var s = samples;\n var box: ReconBox;\n for (var c = 0u; c < 3u; c = c + 1u) {\n let first = s[0][c];\n var same = true;\n var sum = 0.0;\n for (var i = 0u; i < 9u; i = i + 1u) {\n let value = s[i][c];\n sum = sum + value;\n if (value != first) { same = false; }\n }\n if (same) {\n box.low[c] = first;\n box.high[c] = first;\n continue;\n }\n let mean = sum / 9.0;\n var spread = 0.0;\n for (var i = 0u; i < 9u; i = i + 1u) {\n let d = s[i][c] - mean;\n spread = spread + d * d;\n }\n let deviation = sqrt(spread / 9.0);\n box.low[c] = mean - gamma * deviation;\n box.high[c] = mean + gamma * deviation;\n }\n return box;\n}\n\n/*\n * The history pulled towards the box's centre until it is inside, by one ratio for all three\n * channels \u2014 a clamp per channel would land it on a corner and change its hue, which is coloured\n * fringing on every moving edge. A channel with no extent bounds anything that differs from it\n * infinitely, so such a history collapses onto the centre.\n */\nfn reconClipToBox(history: vec3<f32>, low: vec3<f32>, high: vec3<f32>) -> vec3<f32> {\n let centre = (low + high) * 0.5;\n var offset = history - centre;\n var extent = (high - low) * 0.5;\n var ratio = 0.0;\n var infinite = false;\n for (var c = 0u; c < 3u; c = c + 1u) {\n if (extent[c] > 1e-7) {\n ratio = max(ratio, abs(offset[c]) / extent[c]);\n } else if (abs(offset[c]) > 1e-7) {\n infinite = true;\n }\n }\n if (!infinite && !(ratio > 1.0)) { return history; }\n var scale = 0.0;\n if (!infinite) { scale = 1.0 / ratio; }\n return centre + offset * scale;\n}\n\nfn reconAccumulate(current: vec3<f32>, history: vec3<f32>, weight: f32, alpha: f32) -> vec3<f32> {\n let kept = clamp(weight, 0.0, 1.0) * (1.0 - clamp(alpha, 0.0, 1.0));\n return current * (1.0 - kept) + history * kept;\n}\n\nfn reconCatmullRom(t: f32) -> vec4<f32> {\n let t2 = t * t;\n let t3 = t2 * t;\n return vec4<f32>(\n -0.5 * t3 + t2 - 0.5 * t,\n 1.5 * t3 - 2.5 * t2 + 1.0,\n -1.5 * t3 + 2.0 * t2 + 0.5 * t,\n 0.5 * t3 - 0.5 * t2,\n );\n}\n\n/*\n * Five bilinear taps standing for the sixteen-texel Catmull-Rom: each axis's middle pair folded\n * into one tap placed between them, the corners dropped, the five weights renormalised. It is the\n * five-tap form because the taps are the expensive part \u2014 which is only true while the bilinear is\n * the sampler's, so the production module's accessor is a filtered sample and the check's is the\n * reference's own arithmetic over loaded texels.\n */\nstruct ReconTaps {\n at0: vec2<f32>,\n at1: vec2<f32>,\n at2: vec2<f32>,\n at3: vec2<f32>,\n at4: vec2<f32>,\n weights: array<f32, 5>,\n total: f32,\n}\n\nfn reconBicubicTaps(x: f32, y: f32) -> ReconTaps {\n let x1 = floor(x);\n let y1 = floor(y);\n let wx = reconCatmullRom(x - x1);\n let wy = reconCatmullRom(y - y1);\n let wx12 = wx[1] + wx[2];\n let wy12 = wy[1] + wy[2];\n let x12 = x1 + wx[2] / wx12;\n let y12 = y1 + wy[2] / wy12;\n var taps: ReconTaps;\n taps.at0 = vec2<f32>(x12, y1 - 1.0);\n taps.at1 = vec2<f32>(x1 - 1.0, y12);\n taps.at2 = vec2<f32>(x12, y12);\n taps.at3 = vec2<f32>(x1 + 2.0, y12);\n taps.at4 = vec2<f32>(x12, y1 + 2.0);\n taps.weights[0] = wx12 * wy[0];\n taps.weights[1] = wx[0] * wy12;\n taps.weights[2] = wx12 * wy12;\n taps.weights[3] = wx[3] * wy12;\n taps.weights[4] = wx12 * wy[3];\n taps.total =\n taps.weights[0] + taps.weights[1] + taps.weights[2] + taps.weights[3] + taps.weights[4];\n return taps;\n}\n\n/* A colour cannot be negative, and Catmull-Rom's negative lobes undershoot beside a bright edge. */\nfn reconSceneBicubic5(x: f32, y: f32) -> vec3<f32> {\n let taps = reconBicubicTaps(x, y);\n var sum = reconSceneBilinear(taps.at0) * taps.weights[0];\n sum = sum + reconSceneBilinear(taps.at1) * taps.weights[1];\n sum = sum + reconSceneBilinear(taps.at2) * taps.weights[2];\n sum = sum + reconSceneBilinear(taps.at3) * taps.weights[3];\n sum = sum + reconSceneBilinear(taps.at4) * taps.weights[4];\n return max(sum / taps.total, vec3<f32>(0.0));\n}\n\nfn reconHistoryBicubic5(x: f32, y: f32) -> vec4<f32> {\n let taps = reconBicubicTaps(x, y);\n var sum = reconHistoryBilinear(taps.at0) * taps.weights[0];\n sum = sum + reconHistoryBilinear(taps.at1) * taps.weights[1];\n sum = sum + reconHistoryBilinear(taps.at2) * taps.weights[2];\n sum = sum + reconHistoryBilinear(taps.at3) * taps.weights[3];\n sum = sum + reconHistoryBilinear(taps.at4) * taps.weights[4];\n return max(sum / taps.total, vec4<f32>(0.0));\n}\n\nstruct ReconWorld {\n ok: bool,\n position: vec3<f32>,\n}\n\n/* The world position a clip depth stands for, and nothing where the matrix has no answer there. */\nfn reconWorldFromDepth(m: mat4x4<f32>, u: f32, v: f32, clipDepth: f32) -> ReconWorld {\n var out: ReconWorld;\n out.position = vec3<f32>(0.0);\n out.ok = false;\n let x = u * 2.0 - 1.0;\n let y = v * 2.0 - 1.0;\n let w = m[0][3] * x + m[1][3] * y + m[2][3] * clipDepth + m[3][3];\n if (!(abs(w) > 1e-12)) { return out; }\n let p = m * vec4<f32>(x, y, clipDepth, 1.0);\n out.position = p.xyz / w;\n out.ok = all(abs(out.position) < vec3<f32>(3.4e38));\n return out;\n}\n\nstruct ReconNormal {\n ok: bool,\n normal: vec3<f32>,\n}\n\n/* Turned to face the eye, so the answer is the same whichever way the target's rows run. */\nfn reconNormalFrom(\n centre: vec3<f32>,\n right: vec3<f32>,\n up: vec3<f32>,\n eye: vec3<f32>,\n) -> ReconNormal {\n var out: ReconNormal;\n out.normal = vec3<f32>(0.0);\n out.ok = false;\n let a = right - centre;\n let b = up - centre;\n let n = cross(a, b);\n let length2 = dot(n, n);\n let scale2 = dot(a, a) * dot(b, b);\n if (!(length2 > 1e-12 * scale2)) { return out; }\n let toEye = dot(n, eye - centre);\n var sign = 1.0;\n if (toEye < 0.0) { sign = -1.0; }\n out.normal = n * (sign / sqrt(length2));\n out.ok = true;\n return out;\n}\n\nfn reconDisocclusion(\n currDepth: f32,\n histDepth: f32,\n motionLength: f32,\n normalDot: f32,\n) -> f32 {\n if (!(currDepth > 0.0) || !(histDepth > 0.0) || !(motionLength >= 0.0)) { return 0.0; }\n let disagreement = abs(currDepth - histDepth) / currDepth;\n let tolerance = recon.depthTolerance + recon.motionScale * motionLength;\n let depth = 1.0 - reconSmoothstep(tolerance, 2.0 * tolerance, disagreement);\n let normal = reconSmoothstep(recon.normalFloor, recon.normalCeiling, normalDot);\n return depth * normal;\n}\n\nstruct ReconAt {\n depth: f32,\n position: vec3<f32>,\n}\n\n/* A texel's world position at its unjittered uv, and the view depth it stands at, or zero. */\nfn reconWorldAt(previous: bool, x: i32, y: i32) -> ReconAt {\n let w = i32(recon.renderSize.x);\n let h = i32(recon.renderSize.y);\n let tx = reconClampTexel(x, w);\n let ty = reconClampTexel(y, h);\n var jitter = recon.jitter;\n var m = recon.inverseViewProj;\n var z = reconDepth(tx, ty);\n if (previous) {\n jitter = recon.previousJitter;\n m = recon.previousInverseViewProj;\n z = reconPreviousDepth(tx, ty);\n }\n let u = (f32(tx) + 0.5 - jitter.x) / f32(w);\n let v = (f32(ty) + 0.5 - jitter.y) / f32(h);\n var out: ReconAt;\n out.depth = 0.0;\n out.position = vec3<f32>(0.0);\n let world = reconWorldFromDepth(m, u, v, z);\n if (!world.ok) { return out; }\n out.position = world.position;\n /* The view depth is the clip w, which the inverse's w is the reciprocal of. */\n let cw = m[0][3] * (u * 2.0 - 1.0) + m[1][3] * (v * 2.0 - 1.0) + m[2][3] * z + m[3][3];\n out.depth = 1.0 / cw;\n return out;\n}\n\n/* The neighbour on the inside of the border, so the edge texels have a normal too. */\nfn reconNormalAt(previous: bool, x: i32, y: i32) -> ReconNormal {\n var out: ReconNormal;\n out.normal = vec3<f32>(0.0);\n out.ok = false;\n let w = i32(recon.renderSize.x);\n let h = i32(recon.renderSize.y);\n let centre = reconWorldAt(previous, x, y);\n if (!(centre.depth > 0.0)) { return out; }\n var dx = -1;\n if (x + 1 < w) { dx = 1; }\n var dy = -1;\n if (y + 1 < h) { dy = 1; }\n let right = reconWorldAt(previous, x + dx, y);\n if (!(right.depth > 0.0)) { return out; }\n let up = reconWorldAt(previous, x, y + dy);\n if (!(up.depth > 0.0)) { return out; }\n var eye = recon.eye.xyz;\n if (previous) { eye = recon.previousEye.xyz; }\n return reconNormalFrom(centre.position, right.position, up.position, eye);\n}\n\nstruct ReconHistory {\n weight: f32,\n colour: vec4<f32>,\n}\n\n/*\n * The weight a pixel's history still deserves: none on the first frame, none where the surface was\n * behind last frame's eye or off its picture, and otherwise what it gathered, scaled by whether it\n * is the same surface.\n */\nfn reconHistoryWeight(u: f32, v: f32, dx: i32, dy: i32) -> ReconHistory {\n var out: ReconHistory;\n out.weight = 0.0;\n out.colour = vec4<f32>(0.0);\n if (recon.hasHistory == 0u) { return out; }\n let rw = f32(recon.renderSize.x);\n let rh = f32(recon.renderSize.y);\n let motion = reconMotion(dx, dy);\n var mu = 0.0;\n var mv = 0.0;\n var expected = 0.0;\n if (motion.w > 0.5) {\n mu = motion.x;\n mv = motion.y;\n expected = motion.z;\n } else {\n let here = reconWorldAt(false, dx, dy);\n if (!(here.depth > 0.0)) { return out; }\n let clip = recon.previousViewProj * vec4<f32>(here.position, 1.0);\n /*\n * A surface behind last frame's eye has a w that is not positive: the disocclusion trusts no\n * such depth, so it needs no refusal of its own here.\n */\n expected = clip.w;\n let du = (f32(dx) + 0.5 - recon.jitter.x) / rw;\n let dv = (f32(dy) + 0.5 - recon.jitter.y) / rh;\n mu = (clip.x / expected) * 0.5 + 0.5 - du;\n mv = (clip.y / expected) * 0.5 + 0.5 - dv;\n }\n\n let pu = u + mu;\n let pv = v + mv;\n if (!(pu >= 0.0 && pu <= 1.0 && pv >= 0.0 && pv <= 1.0)) { return out; }\n out.colour = reconHistoryBicubic5(\n pu * f32(recon.outputSize.x) - 0.5,\n pv * f32(recon.outputSize.y) - 0.5,\n );\n let px = reconClampTexel(i32(reconRound(pu * rw + recon.previousJitter.x - 0.5)), i32(rw));\n let py = reconClampTexel(i32(reconRound(pv * rh + recon.previousJitter.y - 0.5)), i32(rh));\n let held = reconWorldAt(true, px, py).depth;\n var normalDot = 1.0;\n let here = reconNormalAt(false, dx, dy);\n let there = reconNormalAt(true, px, py);\n if (here.ok && there.ok) { normalDot = dot(here.normal, there.normal); }\n let trust = reconDisocclusion(expected, held, length(vec2<f32>(mu, mv)), normalDot);\n out.weight = out.colour.w * trust;\n return out;\n}\n\n/* One output pixel: the history's next colour, before sharpening, and the weight it has gathered. */\nfn reconResolvePixel(ox: i32, oy: i32) -> vec4<f32> {\n let rw = i32(recon.renderSize.x);\n let rh = i32(recon.renderSize.y);\n let ow = i32(recon.outputSize.x);\n let oh = i32(recon.outputSize.y);\n let u = (f32(ox) + 0.5) / f32(ow);\n let v = (f32(oy) + 0.5) / f32(oh);\n let jx = recon.jitter.x;\n let jy = recon.jitter.y;\n\n /* Where this frame observed the pixel, and the render read there for the gaps. */\n let rx = u * f32(rw) + jx - 0.5;\n let ry = v * f32(rh) + jy - 0.5;\n let sample = reconSceneBicubic5(rx, ry);\n let nx = reconClampTexel(i32(reconRound(rx)), rw);\n let ny = reconClampTexel(i32(reconRound(ry)), rh);\n\n /* Each texel around: its weight, its colour in the box, and its depth. */\n var nearest = reconDepth(nx, ny);\n var dx = nx;\n var dy = ny;\n var gathered = 0.0;\n var accum = vec3<f32>(0.0);\n var neighbours: array<vec3<f32>, 9>;\n for (var j = -1; j <= 1; j = j + 1) {\n for (var i = -1; i <= 1; i = i + 1) {\n let tx = reconClampTexel(nx + i, rw);\n let ty = reconClampTexel(ny + j, rh);\n let colour = reconScene(tx, ty);\n let weight = reconWeightAt(\n ((f32(tx) + 0.5 - jx) / f32(rw)) * f32(ow) - (f32(ox) + 0.5),\n ((f32(ty) + 0.5 - jy) / f32(rh)) * f32(oh) - (f32(oy) + 0.5),\n );\n gathered = gathered + weight;\n accum = accum + colour * weight;\n neighbours[(j + 1) * 3 + (i + 1)] = reconToYCoCg(colour);\n let z = reconDepth(tx, ty);\n if (z < nearest) {\n nearest = z;\n dx = tx;\n dy = ty;\n }\n }\n }\n let box = reconVarianceBox(neighbours, recon.clampGamma);\n accum = accum / gathered;\n\n /* The history, and the weight it still deserves; a tenth of each frame is always new. */\n let cap = (1.0 - recon.alpha) / recon.alpha;\n let history = reconHistoryWeight(u, v, dx, dy);\n let kept = min(cap, history.weight);\n\n let total = kept + gathered;\n var mixed = accum;\n if (kept > 0.0) {\n let clipped = reconClipToBox(reconToYCoCg(history.colour.xyz), box.low, box.high);\n mixed = reconAccumulate(accum, reconFromYCoCg(clipped), 1.0, gathered / total);\n }\n let known = min(1.0, total / RECON_CONFIDENT_WEIGHT);\n return vec4<f32>(sample * (1.0 - known) + mixed * known, total);\n}\n\n/*\n * The centre pushed away from its four neighbours' mean, and held inside the range of the five \u2014\n * the halo a plain unsharp mask draws beside every edge is a value outside that range.\n */\nfn reconSharpenAt(ox: i32, oy: i32) -> vec3<f32> {\n let ow = i32(recon.outputSize.x);\n let oh = i32(recon.outputSize.y);\n let centre = reconHistory(reconClampTexel(ox, ow), reconClampTexel(oy, oh)).xyz;\n var offsets = array<vec2<i32>, 4>(\n vec2<i32>(-1, 0),\n vec2<i32>(1, 0),\n vec2<i32>(0, -1),\n vec2<i32>(0, 1),\n );\n var low = centre;\n var high = centre;\n var sum = vec3<f32>(0.0);\n for (var n = 0; n < 4; n = n + 1) {\n let at = offsets[n];\n let value = reconHistory(\n reconClampTexel(ox + at.x, ow),\n reconClampTexel(oy + at.y, oh),\n ).xyz;\n sum = sum + value;\n low = min(low, value);\n high = max(high, value);\n }\n let pushed = centre + recon.sharpenAmount * (centre - sum / 4.0);\n return min(high, max(low, pushed));\n}\n";
30
+ export declare const RECON_RESOLVE_CORE_WGSL = "\nconst RECON_SAMPLE_SPREAD: f32 = 0.47;\nconst RECON_CONFIDENT_WEIGHT: f32 = 1.0;\nconst RECON_PLANAR_FROM: f32 = 0.25;\nconst RECON_PLANAR_TO: f32 = 0.5;\n\nfn reconClampTexel(value: i32, size: i32) -> i32 {\n return clamp(value, 0, size - 1);\n}\n\n/* JavaScript's Math.round, which is not WGSL's round: a half goes up, not to even. */\nfn reconRound(value: f32) -> f32 {\n return floor(value + 0.5);\n}\n\n/* Written out rather than the built-in, whose result is undefined where the edges are equal. */\nfn reconSmoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {\n let t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);\n return t * t * (3.0 - 2.0 * t);\n}\n\nfn reconWeightAt(dx: f32, dy: f32) -> f32 {\n return exp(-(dx * dx + dy * dy) / (2.0 * RECON_SAMPLE_SPREAD * RECON_SAMPLE_SPREAD));\n}\n\nfn reconToYCoCg(c: vec3<f32>) -> vec3<f32> {\n return vec3<f32>(\n c.r * 0.25 + c.g * 0.5 + c.b * 0.25,\n c.r * 0.5 - c.b * 0.5,\n -c.r * 0.25 + c.g * 0.5 - c.b * 0.25,\n );\n}\n\nfn reconFromYCoCg(c: vec3<f32>) -> vec3<f32> {\n let base = c.x - c.z;\n return vec3<f32>(base + c.y, c.x + c.z, base - c.y);\n}\n\nstruct ReconBox {\n low: vec3<f32>,\n high: vec3<f32>,\n}\n\n/*\n * Each channel's mean plus or minus gamma of its standard deviation \u2014 and a channel whose nine\n * samples all agree is that value exactly, because a mean of nine equal floats is not always one\n * of them and a box a rounding wide lets a history through by a rounding.\n */\nfn reconVarianceBox(samples: array<vec3<f32>, 9>, gamma: f32) -> ReconBox {\n var s = samples;\n var box: ReconBox;\n for (var c = 0u; c < 3u; c = c + 1u) {\n let first = s[0][c];\n var same = true;\n var sum = 0.0;\n for (var i = 0u; i < 9u; i = i + 1u) {\n let value = s[i][c];\n sum = sum + value;\n if (value != first) { same = false; }\n }\n if (same) {\n box.low[c] = first;\n box.high[c] = first;\n continue;\n }\n let mean = sum / 9.0;\n var spread = 0.0;\n for (var i = 0u; i < 9u; i = i + 1u) {\n let d = s[i][c] - mean;\n spread = spread + d * d;\n }\n let deviation = sqrt(spread / 9.0);\n box.low[c] = mean - gamma * deviation;\n box.high[c] = mean + gamma * deviation;\n }\n return box;\n}\n\n/*\n * The history pulled towards the box's centre until it is inside, by one ratio for all three\n * channels \u2014 a clamp per channel would land it on a corner and change its hue, which is coloured\n * fringing on every moving edge. A channel with no extent bounds anything that differs from it\n * infinitely, so such a history collapses onto the centre.\n */\nfn reconClipToBox(history: vec3<f32>, low: vec3<f32>, high: vec3<f32>) -> vec3<f32> {\n let centre = (low + high) * 0.5;\n var offset = history - centre;\n var extent = (high - low) * 0.5;\n var ratio = 0.0;\n var infinite = false;\n for (var c = 0u; c < 3u; c = c + 1u) {\n if (extent[c] > 1e-7) {\n ratio = max(ratio, abs(offset[c]) / extent[c]);\n } else if (abs(offset[c]) > 1e-7) {\n infinite = true;\n }\n }\n if (!infinite && !(ratio > 1.0)) { return history; }\n var scale = 0.0;\n if (!infinite) { scale = 1.0 / ratio; }\n return centre + offset * scale;\n}\n\nfn reconAccumulate(current: vec3<f32>, history: vec3<f32>, weight: f32, alpha: f32) -> vec3<f32> {\n let kept = clamp(weight, 0.0, 1.0) * (1.0 - clamp(alpha, 0.0, 1.0));\n return current * (1.0 - kept) + history * kept;\n}\n\nfn reconCatmullRom(t: f32) -> vec4<f32> {\n let t2 = t * t;\n let t3 = t2 * t;\n return vec4<f32>(\n -0.5 * t3 + t2 - 0.5 * t,\n 1.5 * t3 - 2.5 * t2 + 1.0,\n -1.5 * t3 + 2.0 * t2 + 0.5 * t,\n 0.5 * t3 - 0.5 * t2,\n );\n}\n\n/*\n * Five bilinear taps standing for the sixteen-texel Catmull-Rom: each axis's middle pair folded\n * into one tap placed between them, the corners dropped, the five weights renormalised. It is the\n * five-tap form because the taps are the expensive part \u2014 which is only true while the bilinear is\n * the sampler's, so the production module's accessor is a filtered sample and the check's is the\n * reference's own arithmetic over loaded texels.\n */\nstruct ReconTaps {\n at0: vec2<f32>,\n at1: vec2<f32>,\n at2: vec2<f32>,\n at3: vec2<f32>,\n at4: vec2<f32>,\n weights: array<f32, 5>,\n total: f32,\n}\n\nfn reconBicubicTaps(x: f32, y: f32) -> ReconTaps {\n let x1 = floor(x);\n let y1 = floor(y);\n let wx = reconCatmullRom(x - x1);\n let wy = reconCatmullRom(y - y1);\n let wx12 = wx[1] + wx[2];\n let wy12 = wy[1] + wy[2];\n let x12 = x1 + wx[2] / wx12;\n let y12 = y1 + wy[2] / wy12;\n var taps: ReconTaps;\n taps.at0 = vec2<f32>(x12, y1 - 1.0);\n taps.at1 = vec2<f32>(x1 - 1.0, y12);\n taps.at2 = vec2<f32>(x12, y12);\n taps.at3 = vec2<f32>(x1 + 2.0, y12);\n taps.at4 = vec2<f32>(x12, y1 + 2.0);\n taps.weights[0] = wx12 * wy[0];\n taps.weights[1] = wx[0] * wy12;\n taps.weights[2] = wx12 * wy12;\n taps.weights[3] = wx[3] * wy12;\n taps.weights[4] = wx12 * wy[3];\n taps.total =\n taps.weights[0] + taps.weights[1] + taps.weights[2] + taps.weights[3] + taps.weights[4];\n return taps;\n}\n\n/* A colour cannot be negative, and Catmull-Rom's negative lobes undershoot beside a bright edge. */\nfn reconSceneBicubic5(x: f32, y: f32) -> vec3<f32> {\n let taps = reconBicubicTaps(x, y);\n var sum = reconSceneBilinear(taps.at0) * taps.weights[0];\n sum = sum + reconSceneBilinear(taps.at1) * taps.weights[1];\n sum = sum + reconSceneBilinear(taps.at2) * taps.weights[2];\n sum = sum + reconSceneBilinear(taps.at3) * taps.weights[3];\n sum = sum + reconSceneBilinear(taps.at4) * taps.weights[4];\n return max(sum / taps.total, vec3<f32>(0.0));\n}\n\nfn reconHistoryBicubic5(x: f32, y: f32) -> vec4<f32> {\n let taps = reconBicubicTaps(x, y);\n var sum = reconHistoryBilinear(taps.at0) * taps.weights[0];\n sum = sum + reconHistoryBilinear(taps.at1) * taps.weights[1];\n sum = sum + reconHistoryBilinear(taps.at2) * taps.weights[2];\n sum = sum + reconHistoryBilinear(taps.at3) * taps.weights[3];\n sum = sum + reconHistoryBilinear(taps.at4) * taps.weights[4];\n return max(sum / taps.total, vec4<f32>(0.0));\n}\n\nstruct ReconWorld {\n ok: bool,\n position: vec3<f32>,\n}\n\n/* The world position a clip depth stands for, and nothing where the matrix has no answer there. */\nfn reconWorldFromDepth(m: mat4x4<f32>, u: f32, v: f32, clipDepth: f32) -> ReconWorld {\n var out: ReconWorld;\n out.position = vec3<f32>(0.0);\n out.ok = false;\n let x = u * 2.0 - 1.0;\n let y = v * 2.0 - 1.0;\n let w = m[0][3] * x + m[1][3] * y + m[2][3] * clipDepth + m[3][3];\n if (!(abs(w) > 1e-12)) { return out; }\n let p = m * vec4<f32>(x, y, clipDepth, 1.0);\n out.position = p.xyz / w;\n out.ok = all(abs(out.position) < vec3<f32>(3.4e38));\n return out;\n}\n\nstruct ReconNormal {\n ok: bool,\n normal: vec3<f32>,\n}\n\n/* Turned to face the eye, so the answer is the same whichever way the target's rows run. */\nfn reconNormalFrom(\n centre: vec3<f32>,\n right: vec3<f32>,\n up: vec3<f32>,\n eye: vec3<f32>,\n) -> ReconNormal {\n var out: ReconNormal;\n out.normal = vec3<f32>(0.0);\n out.ok = false;\n let a = right - centre;\n let b = up - centre;\n let n = cross(a, b);\n let length2 = dot(n, n);\n let scale2 = dot(a, a) * dot(b, b);\n if (!(length2 > 1e-12 * scale2)) { return out; }\n let toEye = dot(n, eye - centre);\n var sign = 1.0;\n if (toEye < 0.0) { sign = -1.0; }\n out.normal = n * (sign / sqrt(length2));\n out.ok = true;\n return out;\n}\n\nfn reconDisocclusion(\n currDepth: f32,\n histDepth: f32,\n motionLength: f32,\n normalDot: f32,\n slope: f32,\n) -> f32 {\n if (!(currDepth > 0.0) || !(histDepth > 0.0) || !(motionLength >= 0.0)) { return 0.0; }\n let disagreement = abs(currDepth - histDepth) / currDepth;\n let tolerance = recon.depthTolerance + recon.motionScale * motionLength + slope;\n let depth = 1.0 - reconSmoothstep(tolerance, 2.0 * tolerance, disagreement);\n let normal = reconSmoothstep(recon.normalFloor, recon.normalCeiling, normalDot);\n return depth * normal;\n}\n\nstruct ReconAt {\n depth: f32,\n position: vec3<f32>,\n}\n\n/* A texel's world position at its unjittered uv, and the view depth it stands at, or zero. */\nfn reconWorldAt(previous: bool, x: i32, y: i32) -> ReconAt {\n let w = i32(recon.renderSize.x);\n let h = i32(recon.renderSize.y);\n let tx = reconClampTexel(x, w);\n let ty = reconClampTexel(y, h);\n var jitter = recon.jitter;\n var m = recon.inverseViewProj;\n var z = reconDepth(tx, ty);\n if (previous) {\n jitter = recon.previousJitter;\n m = recon.previousInverseViewProj;\n z = reconPreviousDepth(tx, ty);\n }\n let u = (f32(tx) + 0.5 - jitter.x) / f32(w);\n let v = (f32(ty) + 0.5 - jitter.y) / f32(h);\n var out: ReconAt;\n out.depth = 0.0;\n out.position = vec3<f32>(0.0);\n let world = reconWorldFromDepth(m, u, v, z);\n if (!world.ok) { return out; }\n out.position = world.position;\n /* The view depth is the clip w, which the inverse's w is the reciprocal of. */\n let cw = m[0][3] * (u * 2.0 - 1.0) + m[1][3] * (v * 2.0 - 1.0) + m[2][3] * z + m[3][3];\n out.depth = 1.0 / cw;\n return out;\n}\n\n/* The neighbour on the inside of the border, so the edge texels have a normal too. */\nfn reconNormalAt(previous: bool, x: i32, y: i32) -> ReconNormal {\n var out: ReconNormal;\n out.normal = vec3<f32>(0.0);\n out.ok = false;\n let w = i32(recon.renderSize.x);\n let h = i32(recon.renderSize.y);\n let centre = reconWorldAt(previous, x, y);\n if (!(centre.depth > 0.0)) { return out; }\n var dx = -1;\n if (x + 1 < w) { dx = 1; }\n var dy = -1;\n if (y + 1 < h) { dy = 1; }\n let right = reconWorldAt(previous, x + dx, y);\n if (!(right.depth > 0.0)) { return out; }\n let up = reconWorldAt(previous, x, y + dy);\n if (!(up.depth > 0.0)) { return out; }\n var eye = recon.eye.xyz;\n if (previous) { eye = recon.previousEye.xyz; }\n return reconNormalFrom(centre.position, right.position, up.position, eye);\n}\n\n/* resolve.ts's viewDepthAt. */\nfn reconViewDepthAt(previous: bool, x: i32, y: i32) -> f32 {\n let w = i32(recon.renderSize.x);\n let h = i32(recon.renderSize.y);\n if (x < 0 || y < 0 || x >= w || y >= h) { return 0.0; }\n var jitter = recon.jitter;\n var m = recon.inverseViewProj;\n var z = 0.0;\n if (previous) {\n jitter = recon.previousJitter;\n m = recon.previousInverseViewProj;\n z = reconPreviousDepth(x, y);\n } else {\n z = reconDepth(x, y);\n }\n let u = (f32(x) + 0.5 - jitter.x) / f32(w);\n let v = (f32(y) + 0.5 - jitter.y) / f32(h);\n let cw = m[0][3] * (u * 2.0 - 1.0) + m[1][3] * (v * 2.0 - 1.0) + m[2][3] * z + m[3][3];\n if (!(abs(cw) > 1e-12)) { return 0.0; }\n return 1.0 / cw;\n}\n\n/* disocclusion.ts's planarity. */\nfn reconPlanarity(before: f32, centre: f32, after: f32) -> f32 {\n if (!(before > 0.0) || !(centre > 0.0) || !(after > 0.0)) { return 1.0; }\n let a = 1.0 / after - 1.0 / centre;\n let b = 1.0 / before - 1.0 / centre;\n let spread = abs(a) + abs(b);\n if (!(spread > 0.0)) { return 1.0; }\n return 1.0 - reconSmoothstep(RECON_PLANAR_FROM, RECON_PLANAR_TO, abs(a + b) / spread);\n}\n\n/* disocclusion.ts's surfaceSlope. */\nfn reconSurfaceSlope(before: f32, centre: f32, after: f32) -> f32 {\n let hasAfter = after > 0.0;\n let hasBefore = before > 0.0;\n if (!hasAfter) {\n if (!hasBefore) { return 0.0; }\n return abs(before - centre) / centre;\n }\n if (!hasBefore) { return abs(after - centre) / centre; }\n let a = after - centre;\n let b = before - centre;\n if (a * b > 0.0) { return 0.0; }\n return min(abs(a), abs(b)) / centre;\n}\n\nstruct ReconSurface {\n planar: f32,\n slope: f32,\n}\n\n/* resolve.ts's surfaceAt. */\nfn reconSurfaceAt(previous: bool, x: i32, y: i32, centre: f32) -> ReconSurface {\n var out: ReconSurface;\n out.planar = 1.0;\n out.slope = 0.0;\n for (var axis = 0; axis < 2; axis = axis + 1) {\n let ax = 1 - axis;\n let ay = axis;\n let after = reconViewDepthAt(previous, x + ax, y + ay);\n let before = reconViewDepthAt(previous, x - ax, y - ay);\n out.planar = out.planar * reconPlanarity(before, centre, after);\n out.slope = max(out.slope, reconSurfaceSlope(before, centre, after));\n }\n return out;\n}\n\nstruct ReconHistory {\n weight: f32,\n colour: vec4<f32>,\n}\n\n/*\n * The weight a pixel's history still deserves: none on the first frame, none where the surface was\n * behind last frame's eye or off its picture, and otherwise what it gathered, scaled by whether it\n * is the same surface.\n */\nfn reconHistoryWeight(u: f32, v: f32, dx: i32, dy: i32) -> ReconHistory {\n var out: ReconHistory;\n out.weight = 0.0;\n out.colour = vec4<f32>(0.0);\n if (recon.hasHistory == 0u) { return out; }\n let rw = f32(recon.renderSize.x);\n let rh = f32(recon.renderSize.y);\n let motion = reconMotion(dx, dy);\n var mu = 0.0;\n var mv = 0.0;\n var expected = 0.0;\n if (motion.w > 0.5) {\n mu = motion.x;\n mv = motion.y;\n expected = motion.z;\n } else {\n let here = reconWorldAt(false, dx, dy);\n if (!(here.depth > 0.0)) { return out; }\n let clip = recon.previousViewProj * vec4<f32>(here.position, 1.0);\n /*\n * A surface behind last frame's eye has a w that is not positive: the disocclusion trusts no\n * such depth, so it needs no refusal of its own here.\n */\n expected = clip.w;\n let du = (f32(dx) + 0.5 - recon.jitter.x) / rw;\n let dv = (f32(dy) + 0.5 - recon.jitter.y) / rh;\n mu = (clip.x / expected) * 0.5 + 0.5 - du;\n mv = (clip.y / expected) * 0.5 + 0.5 - dv;\n }\n\n let pu = u + mu;\n let pv = v + mv;\n if (!(pu >= 0.0 && pu <= 1.0 && pv >= 0.0 && pv <= 1.0)) { return out; }\n out.colour = reconHistoryBicubic5(\n pu * f32(recon.outputSize.x) - 0.5,\n pv * f32(recon.outputSize.y) - 0.5,\n );\n /* Last frame's depth, searched around where the dilated texel was: see resolve.ts. */\n let su = (f32(dx) + 0.5 - recon.jitter.x) / rw + mu;\n let sv = (f32(dy) + 0.5 - recon.jitter.y) / rh + mv;\n let cx = reconClampTexel(i32(reconRound(su * rw + recon.previousJitter.x - 0.5)), i32(rw));\n let cy = reconClampTexel(i32(reconRound(sv * rh + recon.previousJitter.y - 0.5)), i32(rh));\n var held = reconViewDepthAt(true, cx, cy);\n var px = cx;\n var py = cy;\n if (!(held > 0.0 && abs(held - expected) <= recon.depthTolerance * expected)) {\n held = 0.0;\n var nearest = 3.4e38;\n for (var j = -1; j <= 1; j = j + 1) {\n for (var i = -1; i <= 1; i = i + 1) {\n let tx = reconClampTexel(cx + i, i32(rw));\n let ty = reconClampTexel(cy + j, i32(rh));\n let depth = reconViewDepthAt(true, tx, ty);\n if (!(depth > 0.0)) { continue; }\n let off = abs(depth - expected);\n if (off < nearest) {\n nearest = off;\n held = depth;\n px = tx;\n py = ty;\n }\n }\n }\n }\n\n let here = reconSurfaceAt(false, dx, dy, reconViewDepthAt(false, dx, dy));\n var planar = here.planar;\n if (planar > 0.0) { planar = planar * reconSurfaceAt(true, px, py, held).planar; }\n var normalDot = 1.0;\n if (planar > 0.0) {\n let hereNormal = reconNormalAt(false, dx, dy);\n let thereNormal = reconNormalAt(true, px, py);\n if (hereNormal.ok && thereNormal.ok) {\n normalDot = 1.0 - planar * (1.0 - dot(hereNormal.normal, thereNormal.normal));\n }\n }\n let trust = reconDisocclusion(expected, held, length(vec2<f32>(mu, mv)), normalDot, here.slope);\n out.weight = out.colour.w * trust;\n return out;\n}\n\n/* One output pixel: the history's next colour, before sharpening, and the weight it has gathered. */\nfn reconResolvePixel(ox: i32, oy: i32) -> vec4<f32> {\n let rw = i32(recon.renderSize.x);\n let rh = i32(recon.renderSize.y);\n let ow = i32(recon.outputSize.x);\n let oh = i32(recon.outputSize.y);\n let u = (f32(ox) + 0.5) / f32(ow);\n let v = (f32(oy) + 0.5) / f32(oh);\n let jx = recon.jitter.x;\n let jy = recon.jitter.y;\n\n /* Where this frame observed the pixel, and the render read there for the gaps. */\n let rx = u * f32(rw) + jx - 0.5;\n let ry = v * f32(rh) + jy - 0.5;\n let sample = reconSceneBicubic5(rx, ry);\n let nx = reconClampTexel(i32(reconRound(rx)), rw);\n let ny = reconClampTexel(i32(reconRound(ry)), rh);\n\n /* Each texel around: its weight, its colour in the box, and its depth. */\n var nearest = reconDepth(nx, ny);\n var dx = nx;\n var dy = ny;\n var gathered = 0.0;\n var accum = vec3<f32>(0.0);\n var neighbours: array<vec3<f32>, 9>;\n for (var j = -1; j <= 1; j = j + 1) {\n for (var i = -1; i <= 1; i = i + 1) {\n let tx = reconClampTexel(nx + i, rw);\n let ty = reconClampTexel(ny + j, rh);\n let colour = reconScene(tx, ty);\n let weight = reconWeightAt(\n ((f32(tx) + 0.5 - jx) / f32(rw)) * f32(ow) - (f32(ox) + 0.5),\n ((f32(ty) + 0.5 - jy) / f32(rh)) * f32(oh) - (f32(oy) + 0.5),\n );\n gathered = gathered + weight;\n accum = accum + colour * weight;\n neighbours[(j + 1) * 3 + (i + 1)] = reconToYCoCg(colour);\n let z = reconDepth(tx, ty);\n if (z < nearest) {\n nearest = z;\n dx = tx;\n dy = ty;\n }\n }\n }\n let box = reconVarianceBox(neighbours, recon.clampGamma);\n accum = accum / gathered;\n\n /* The history, and the weight it still deserves; a tenth of each frame is always new. */\n let cap = (1.0 - recon.alpha) / recon.alpha;\n let history = reconHistoryWeight(u, v, dx, dy);\n let kept = min(cap, history.weight);\n\n let total = kept + gathered;\n var mixed = accum;\n if (kept > 0.0) {\n let clipped = reconClipToBox(reconToYCoCg(history.colour.xyz), box.low, box.high);\n mixed = reconAccumulate(accum, reconFromYCoCg(clipped), 1.0, gathered / total);\n }\n let known = min(1.0, total / RECON_CONFIDENT_WEIGHT);\n return vec4<f32>(sample * (1.0 - known) + mixed * known, total);\n}\n\n/*\n * The centre pushed away from its four neighbours' mean, and held inside the range of the five \u2014\n * the halo a plain unsharp mask draws beside every edge is a value outside that range.\n */\nfn reconSharpenAt(ox: i32, oy: i32) -> vec3<f32> {\n let ow = i32(recon.outputSize.x);\n let oh = i32(recon.outputSize.y);\n let centre = reconHistory(reconClampTexel(ox, ow), reconClampTexel(oy, oh)).xyz;\n var offsets = array<vec2<i32>, 4>(\n vec2<i32>(-1, 0),\n vec2<i32>(1, 0),\n vec2<i32>(0, -1),\n vec2<i32>(0, 1),\n );\n var low = centre;\n var high = centre;\n var sum = vec3<f32>(0.0);\n for (var n = 0; n < 4; n = n + 1) {\n let at = offsets[n];\n let value = reconHistory(\n reconClampTexel(ox + at.x, ow),\n reconClampTexel(oy + at.y, oh),\n ).xyz;\n sum = sum + value;\n low = min(low, value);\n high = max(high, value);\n }\n let pushed = centre + recon.sharpenAmount * (centre - sum / 4.0);\n return min(high, max(low, pushed));\n}\n";
31
31
  /**
32
32
  * The parity module: the core over plain float textures written in the reference's own row order,
33
33
  * doing its own bilinear so the comparison is arithmetic rather than a tolerance on the device's
@@ -39,6 +39,7 @@
39
39
  * and the uniform `recon: ReconParams`, whose struct this module declares.
40
40
  */
41
41
  import { wgslSceneDepthToNdc } from '../../depthConvention.js';
42
+ import { PLANAR_FROM, PLANAR_TO } from '../../recon/disocclusion.js';
42
43
  /**
43
44
  * The parameters one resolve reads, as `ResolveFrame` minus its images.
44
45
  *
@@ -91,6 +92,8 @@ export const RECON_HISTORY_FORMAT = 'rgba16float';
91
92
  export const RECON_RESOLVE_CORE_WGSL = /* wgsl */ `
92
93
  const RECON_SAMPLE_SPREAD: f32 = ${RECON_SAMPLE_SPREAD};
93
94
  const RECON_CONFIDENT_WEIGHT: f32 = ${RECON_CONFIDENT_WEIGHT}.0;
95
+ const RECON_PLANAR_FROM: f32 = ${PLANAR_FROM};
96
+ const RECON_PLANAR_TO: f32 = ${PLANAR_TO};
94
97
 
95
98
  fn reconClampTexel(value: i32, size: i32) -> i32 {
96
99
  return clamp(value, 0, size - 1);
@@ -322,10 +325,11 @@ fn reconDisocclusion(
322
325
  histDepth: f32,
323
326
  motionLength: f32,
324
327
  normalDot: f32,
328
+ slope: f32,
325
329
  ) -> f32 {
326
330
  if (!(currDepth > 0.0) || !(histDepth > 0.0) || !(motionLength >= 0.0)) { return 0.0; }
327
331
  let disagreement = abs(currDepth - histDepth) / currDepth;
328
- let tolerance = recon.depthTolerance + recon.motionScale * motionLength;
332
+ let tolerance = recon.depthTolerance + recon.motionScale * motionLength + slope;
329
333
  let depth = 1.0 - reconSmoothstep(tolerance, 2.0 * tolerance, disagreement);
330
334
  let normal = reconSmoothstep(recon.normalFloor, recon.normalCeiling, normalDot);
331
335
  return depth * normal;
@@ -386,6 +390,74 @@ fn reconNormalAt(previous: bool, x: i32, y: i32) -> ReconNormal {
386
390
  return reconNormalFrom(centre.position, right.position, up.position, eye);
387
391
  }
388
392
 
393
+ /* resolve.ts's viewDepthAt. */
394
+ fn reconViewDepthAt(previous: bool, x: i32, y: i32) -> f32 {
395
+ let w = i32(recon.renderSize.x);
396
+ let h = i32(recon.renderSize.y);
397
+ if (x < 0 || y < 0 || x >= w || y >= h) { return 0.0; }
398
+ var jitter = recon.jitter;
399
+ var m = recon.inverseViewProj;
400
+ var z = 0.0;
401
+ if (previous) {
402
+ jitter = recon.previousJitter;
403
+ m = recon.previousInverseViewProj;
404
+ z = reconPreviousDepth(x, y);
405
+ } else {
406
+ z = reconDepth(x, y);
407
+ }
408
+ let u = (f32(x) + 0.5 - jitter.x) / f32(w);
409
+ let v = (f32(y) + 0.5 - jitter.y) / f32(h);
410
+ let cw = m[0][3] * (u * 2.0 - 1.0) + m[1][3] * (v * 2.0 - 1.0) + m[2][3] * z + m[3][3];
411
+ if (!(abs(cw) > 1e-12)) { return 0.0; }
412
+ return 1.0 / cw;
413
+ }
414
+
415
+ /* disocclusion.ts's planarity. */
416
+ fn reconPlanarity(before: f32, centre: f32, after: f32) -> f32 {
417
+ if (!(before > 0.0) || !(centre > 0.0) || !(after > 0.0)) { return 1.0; }
418
+ let a = 1.0 / after - 1.0 / centre;
419
+ let b = 1.0 / before - 1.0 / centre;
420
+ let spread = abs(a) + abs(b);
421
+ if (!(spread > 0.0)) { return 1.0; }
422
+ return 1.0 - reconSmoothstep(RECON_PLANAR_FROM, RECON_PLANAR_TO, abs(a + b) / spread);
423
+ }
424
+
425
+ /* disocclusion.ts's surfaceSlope. */
426
+ fn reconSurfaceSlope(before: f32, centre: f32, after: f32) -> f32 {
427
+ let hasAfter = after > 0.0;
428
+ let hasBefore = before > 0.0;
429
+ if (!hasAfter) {
430
+ if (!hasBefore) { return 0.0; }
431
+ return abs(before - centre) / centre;
432
+ }
433
+ if (!hasBefore) { return abs(after - centre) / centre; }
434
+ let a = after - centre;
435
+ let b = before - centre;
436
+ if (a * b > 0.0) { return 0.0; }
437
+ return min(abs(a), abs(b)) / centre;
438
+ }
439
+
440
+ struct ReconSurface {
441
+ planar: f32,
442
+ slope: f32,
443
+ }
444
+
445
+ /* resolve.ts's surfaceAt. */
446
+ fn reconSurfaceAt(previous: bool, x: i32, y: i32, centre: f32) -> ReconSurface {
447
+ var out: ReconSurface;
448
+ out.planar = 1.0;
449
+ out.slope = 0.0;
450
+ for (var axis = 0; axis < 2; axis = axis + 1) {
451
+ let ax = 1 - axis;
452
+ let ay = axis;
453
+ let after = reconViewDepthAt(previous, x + ax, y + ay);
454
+ let before = reconViewDepthAt(previous, x - ax, y - ay);
455
+ out.planar = out.planar * reconPlanarity(before, centre, after);
456
+ out.slope = max(out.slope, reconSurfaceSlope(before, centre, after));
457
+ }
458
+ return out;
459
+ }
460
+
389
461
  struct ReconHistory {
390
462
  weight: f32,
391
463
  colour: vec4<f32>,
@@ -433,14 +505,46 @@ fn reconHistoryWeight(u: f32, v: f32, dx: i32, dy: i32) -> ReconHistory {
433
505
  pu * f32(recon.outputSize.x) - 0.5,
434
506
  pv * f32(recon.outputSize.y) - 0.5,
435
507
  );
436
- let px = reconClampTexel(i32(reconRound(pu * rw + recon.previousJitter.x - 0.5)), i32(rw));
437
- let py = reconClampTexel(i32(reconRound(pv * rh + recon.previousJitter.y - 0.5)), i32(rh));
438
- let held = reconWorldAt(true, px, py).depth;
508
+ /* Last frame's depth, searched around where the dilated texel was: see resolve.ts. */
509
+ let su = (f32(dx) + 0.5 - recon.jitter.x) / rw + mu;
510
+ let sv = (f32(dy) + 0.5 - recon.jitter.y) / rh + mv;
511
+ let cx = reconClampTexel(i32(reconRound(su * rw + recon.previousJitter.x - 0.5)), i32(rw));
512
+ let cy = reconClampTexel(i32(reconRound(sv * rh + recon.previousJitter.y - 0.5)), i32(rh));
513
+ var held = reconViewDepthAt(true, cx, cy);
514
+ var px = cx;
515
+ var py = cy;
516
+ if (!(held > 0.0 && abs(held - expected) <= recon.depthTolerance * expected)) {
517
+ held = 0.0;
518
+ var nearest = 3.4e38;
519
+ for (var j = -1; j <= 1; j = j + 1) {
520
+ for (var i = -1; i <= 1; i = i + 1) {
521
+ let tx = reconClampTexel(cx + i, i32(rw));
522
+ let ty = reconClampTexel(cy + j, i32(rh));
523
+ let depth = reconViewDepthAt(true, tx, ty);
524
+ if (!(depth > 0.0)) { continue; }
525
+ let off = abs(depth - expected);
526
+ if (off < nearest) {
527
+ nearest = off;
528
+ held = depth;
529
+ px = tx;
530
+ py = ty;
531
+ }
532
+ }
533
+ }
534
+ }
535
+
536
+ let here = reconSurfaceAt(false, dx, dy, reconViewDepthAt(false, dx, dy));
537
+ var planar = here.planar;
538
+ if (planar > 0.0) { planar = planar * reconSurfaceAt(true, px, py, held).planar; }
439
539
  var normalDot = 1.0;
440
- let here = reconNormalAt(false, dx, dy);
441
- let there = reconNormalAt(true, px, py);
442
- if (here.ok && there.ok) { normalDot = dot(here.normal, there.normal); }
443
- let trust = reconDisocclusion(expected, held, length(vec2<f32>(mu, mv)), normalDot);
540
+ if (planar > 0.0) {
541
+ let hereNormal = reconNormalAt(false, dx, dy);
542
+ let thereNormal = reconNormalAt(true, px, py);
543
+ if (hereNormal.ok && thereNormal.ok) {
544
+ normalDot = 1.0 - planar * (1.0 - dot(hereNormal.normal, thereNormal.normal));
545
+ }
546
+ }
547
+ let trust = reconDisocclusion(expected, held, length(vec2<f32>(mu, mv)), normalDot, here.slope);
444
548
  out.weight = out.colour.w * trust;
445
549
  return out;
446
550
  }