@driftengine/core 4.5.0 → 4.6.1

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 (209) hide show
  1. package/CHANGELOG.json +62 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/boxCsg.d.ts +28 -0
  4. package/dist/geometry/boxCsg.js +139 -0
  5. package/dist/geometry/csg.d.ts +36 -0
  6. package/dist/geometry/csg.js +329 -0
  7. package/dist/geometry/meshBuilder.js +13 -0
  8. package/dist/geometry/meshConcat.js +1 -0
  9. package/dist/geometry/pixelFont.js +2 -0
  10. package/dist/geometry/solid.d.ts +61 -0
  11. package/dist/geometry/solid.js +152 -0
  12. package/dist/geometry/solidFlat.d.ts +35 -0
  13. package/dist/geometry/solidFlat.js +428 -0
  14. package/dist/geometry/solidHarness.d.ts +19 -0
  15. package/dist/geometry/solidHarness.js +66 -0
  16. package/dist/geometry/solidNormals.d.ts +17 -0
  17. package/dist/geometry/solidNormals.js +86 -0
  18. package/dist/geometry/solidRound.d.ts +41 -0
  19. package/dist/geometry/solidRound.js +323 -0
  20. package/dist/geometry/solidWriter.d.ts +16 -0
  21. package/dist/geometry/solidWriter.js +42 -0
  22. package/dist/index.d.ts +23 -2
  23. package/dist/index.js +13 -1
  24. package/dist/render/backend/api.d.ts +17 -3
  25. package/dist/render/backend/drawFog.d.ts +26 -0
  26. package/dist/render/backend/drawFog.js +30 -0
  27. package/dist/render/backend/materialChanges.d.ts +2 -1
  28. package/dist/render/backend/materialChanges.js +6 -1
  29. package/dist/render/backend/webgl2/instanced.d.ts +24 -5
  30. package/dist/render/backend/webgl2/instanced.js +72 -15
  31. package/dist/render/backend/webgl2/renderer.d.ts +122 -8
  32. package/dist/render/backend/webgl2/renderer.js +249 -128
  33. package/dist/render/backend/webgpu/buffers.d.ts +17 -0
  34. package/dist/render/backend/webgpu/buffers.js +34 -3
  35. package/dist/render/backend/webgpu/depthPass.js +5 -1
  36. package/dist/render/backend/webgpu/drawCommand.d.ts +5 -0
  37. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  38. package/dist/render/backend/webgpu/flatPass.d.ts +4 -1
  39. package/dist/render/backend/webgpu/flatPass.js +38 -9
  40. package/dist/render/backend/webgpu/glassTintPass.js +2 -1
  41. package/dist/render/backend/webgpu/instanceCullPass.d.ts +34 -0
  42. package/dist/render/backend/webgpu/instanceCullPass.js +144 -0
  43. package/dist/render/backend/webgpu/instanced.d.ts +11 -1
  44. package/dist/render/backend/webgpu/instanced.js +21 -3
  45. package/dist/render/backend/webgpu/latePass.d.ts +6 -2
  46. package/dist/render/backend/webgpu/latePass.js +11 -4
  47. package/dist/render/backend/webgpu/motionPass.js +2 -1
  48. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  49. package/dist/render/backend/webgpu/renderer.d.ts +100 -10
  50. package/dist/render/backend/webgpu/renderer.js +478 -214
  51. package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
  52. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
  53. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
  54. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +17 -3
  55. package/dist/render/backend/webgpu/surfaceTexturePass.js +77 -38
  56. package/dist/render/driftLight/bake.d.ts +7 -0
  57. package/dist/render/driftLight/bake.js +61 -56
  58. package/dist/render/driftLight/denseField.d.ts +46 -0
  59. package/dist/render/driftLight/denseField.js +109 -0
  60. package/dist/render/driftLight/lightField.d.ts +10 -5
  61. package/dist/render/driftLight/lightField.js +25 -19
  62. package/dist/render/driftLight/presence.d.ts +50 -0
  63. package/dist/render/driftLight/presence.js +43 -0
  64. package/dist/render/driftLight/uniforms.d.ts +3 -3
  65. package/dist/render/driftLight/uniforms.js +5 -6
  66. package/dist/render/driftLight/worldLightField.d.ts +41 -0
  67. package/dist/render/driftLight/worldLightField.js +66 -0
  68. package/dist/render/fixtureAtlas.d.ts +20 -0
  69. package/dist/render/fixtureAtlas.js +49 -0
  70. package/dist/render/instanceCull.d.ts +63 -0
  71. package/dist/render/instanceCull.js +160 -0
  72. package/dist/render/instances.d.ts +15 -0
  73. package/dist/render/lightBudget.d.ts +22 -33
  74. package/dist/render/lightBudget.js +24 -35
  75. package/dist/render/lightGrid.d.ts +42 -0
  76. package/dist/render/lightGrid.js +87 -0
  77. package/dist/render/mesh.d.ts +23 -24
  78. package/dist/render/mesh.js +56 -68
  79. package/dist/render/occlusion.d.ts +10 -0
  80. package/dist/render/occlusion.js +36 -7
  81. package/dist/render/pointLightSelection.d.ts +7 -0
  82. package/dist/render/pointLightSelection.js +29 -4
  83. package/dist/render/rainField.d.ts +13 -0
  84. package/dist/render/rainField.js +27 -5
  85. package/dist/render/recon/lateDepth.d.ts +11 -0
  86. package/dist/render/recon/lateDepth.js +70 -0
  87. package/dist/render/recon/resolve.d.ts +34 -4
  88. package/dist/render/recon/resolve.js +161 -20
  89. package/dist/render/rendererHarness.js +4 -0
  90. package/dist/render/sceneTarget.d.ts +2 -0
  91. package/dist/render/sceneTarget.js +2 -0
  92. package/dist/render/shaders/depth.js +12 -9
  93. package/dist/render/shaders/flat/driftLight.d.ts +1 -1
  94. package/dist/render/shaders/flat/driftLight.js +32 -7
  95. package/dist/render/shaders/flat/index.js +5 -3
  96. package/dist/render/shaders/flat/main.d.ts +1 -1
  97. package/dist/render/shaders/flat/main.js +75 -23
  98. package/dist/render/shaders/flat/preamble.js +75 -20
  99. package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
  100. package/dist/render/shaders/flat/surfaceEffects.js +152 -0
  101. package/dist/render/shaders/generated/depth.wgsl.d.ts +8 -8
  102. package/dist/render/shaders/generated/depth.wgsl.js +8 -8
  103. package/dist/render/shaders/generated/flat.wgsl.d.ts +1320 -1320
  104. package/dist/render/shaders/generated/flat.wgsl.js +1359 -1345
  105. package/dist/render/shaders/generated/lightVolume.wgsl.js +18 -4
  106. package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
  107. package/dist/render/shaders/generated/rush.wgsl.js +21 -16
  108. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
  109. package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
  110. package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
  111. package/dist/render/shaders/generated/text.wgsl.js +1 -1
  112. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
  113. package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
  114. package/dist/render/shaders/recon/lateDepth.wgsl.js +38 -13
  115. package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
  116. package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
  117. package/dist/render/shaders/rush.js +11 -1
  118. package/dist/render/shaders/sdfText.d.ts +1 -1
  119. package/dist/render/shaders/sdfText.js +3 -2
  120. package/dist/render/shaders/text.d.ts +1 -1
  121. package/dist/render/shaders/text.js +18 -9
  122. package/dist/render/surfaceEffects.d.ts +83 -0
  123. package/dist/render/surfaceEffects.js +75 -0
  124. package/dist/render/surfaceScene.d.ts +8 -0
  125. package/dist/render/surfaceScene.js +8 -0
  126. package/dist/render/surfaceTexture.d.ts +18 -2
  127. package/dist/render/surfaceTexture.js +68 -15
  128. package/dist/render/temporalAa.d.ts +25 -6
  129. package/dist/render/temporalAa.js +45 -15
  130. package/dist/render/textureSource.d.ts +30 -0
  131. package/dist/render/textureSource.js +53 -0
  132. package/dist/render/vertexDefaults.js +2 -0
  133. package/dist/world/hlod.d.ts +104 -0
  134. package/dist/world/hlod.js +214 -0
  135. package/package.json +6 -6
  136. package/src/geometry/boxCsg.ts +186 -0
  137. package/src/geometry/csg.ts +384 -0
  138. package/src/geometry/meshBuilder.ts +15 -0
  139. package/src/geometry/meshConcat.ts +1 -0
  140. package/src/geometry/pixelFont.ts +2 -0
  141. package/src/geometry/solid.ts +189 -0
  142. package/src/geometry/solidFlat.ts +522 -0
  143. package/src/geometry/solidHarness.ts +83 -0
  144. package/src/geometry/solidNormals.ts +103 -0
  145. package/src/geometry/solidRound.ts +443 -0
  146. package/src/geometry/solidWriter.ts +58 -0
  147. package/src/index.ts +51 -1
  148. package/src/render/backend/api.ts +21 -2
  149. package/src/render/backend/drawFog.ts +31 -0
  150. package/src/render/backend/materialChanges.ts +9 -2
  151. package/src/render/backend/webgl2/instanced.ts +80 -14
  152. package/src/render/backend/webgl2/renderer.ts +311 -150
  153. package/src/render/backend/webgpu/buffers.ts +49 -3
  154. package/src/render/backend/webgpu/depthPass.ts +5 -1
  155. package/src/render/backend/webgpu/drawCommand.ts +6 -0
  156. package/src/render/backend/webgpu/flatPass.ts +41 -11
  157. package/src/render/backend/webgpu/glassTintPass.ts +2 -1
  158. package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
  159. package/src/render/backend/webgpu/instanced.ts +21 -3
  160. package/src/render/backend/webgpu/latePass.ts +12 -3
  161. package/src/render/backend/webgpu/motionPass.ts +2 -1
  162. package/src/render/backend/webgpu/renderer.ts +556 -239
  163. package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
  164. package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
  165. package/src/render/backend/webgpu/surfaceTexturePass.ts +90 -50
  166. package/src/render/driftLight/bake.ts +85 -62
  167. package/src/render/driftLight/denseField.ts +149 -0
  168. package/src/render/driftLight/lightField.ts +33 -18
  169. package/src/render/driftLight/presence.ts +69 -0
  170. package/src/render/driftLight/uniforms.ts +8 -9
  171. package/src/render/driftLight/worldLightField.ts +83 -0
  172. package/src/render/fixtureAtlas.ts +63 -0
  173. package/src/render/instanceCull.ts +205 -0
  174. package/src/render/instances.ts +16 -0
  175. package/src/render/lightBudget.ts +25 -36
  176. package/src/render/lightGrid.ts +130 -0
  177. package/src/render/mesh.ts +64 -72
  178. package/src/render/occlusion.ts +51 -7
  179. package/src/render/pointLightSelection.ts +72 -4
  180. package/src/render/rainField.ts +39 -5
  181. package/src/render/recon/lateDepth.ts +83 -0
  182. package/src/render/recon/resolve.ts +206 -20
  183. package/src/render/rendererHarness.ts +4 -0
  184. package/src/render/sceneTarget.ts +4 -0
  185. package/src/render/shaders/depth.ts +12 -9
  186. package/src/render/shaders/flat/driftLight.ts +32 -7
  187. package/src/render/shaders/flat/index.ts +5 -3
  188. package/src/render/shaders/flat/main.ts +75 -23
  189. package/src/render/shaders/flat/preamble.ts +75 -20
  190. package/src/render/shaders/flat/surfaceEffects.ts +152 -0
  191. package/src/render/shaders/generated/depth.wgsl.ts +8 -8
  192. package/src/render/shaders/generated/flat.wgsl.ts +1361 -1345
  193. package/src/render/shaders/generated/lightVolume.wgsl.ts +20 -4
  194. package/src/render/shaders/generated/rush.wgsl.ts +21 -16
  195. package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
  196. package/src/render/shaders/generated/text.wgsl.ts +1 -1
  197. package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
  198. package/src/render/shaders/recon/lateDepth.wgsl.ts +38 -13
  199. package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
  200. package/src/render/shaders/rush.ts +11 -1
  201. package/src/render/shaders/sdfText.ts +3 -2
  202. package/src/render/shaders/text.ts +18 -9
  203. package/src/render/surfaceEffects.ts +132 -0
  204. package/src/render/surfaceScene.ts +16 -0
  205. package/src/render/surfaceTexture.ts +110 -19
  206. package/src/render/temporalAa.ts +78 -14
  207. package/src/render/textureSource.ts +72 -0
  208. package/src/render/vertexDefaults.ts +2 -0
  209. package/src/world/hlod.ts +290 -0
@@ -30,6 +30,29 @@
30
30
  * the render read bicubically fills the gap. The history keeps colour and weight; the picture shown
31
31
  * is it sharpened.
32
32
  *
33
+ * **And the temporal resolve's anti-flicker, where the caller carries its records** (`temporalAa.ts`
34
+ * says why it works and what it gives up). A thread finer than a render texel is caught in some
35
+ * phases of the jitter and missed in others, and in a phase that misses it the nine texels can all
36
+ * miss it: the box collapses onto what is behind, the history is clipped out, and a still picture
37
+ * flashes at every fine edge each period. A still camera over a still scene gathers every pixel the
38
+ * same way each period, so each pixel keeps a record of this frame's gathered luma over the period,
39
+ * and once two periods running have repeated its box widens by its own spread and it keeps more of
40
+ * its history. **The record stays at the pixel rather than following the reprojection**, because the
41
+ * proof is only ever for a pixel that has not moved; it starts over wherever the history did not
42
+ * survive, and is forgotten in proportion to motion.
43
+ *
44
+ * **And a still pixel whose depth has been seen to swing is not a disocclusion.** A rail finer than
45
+ * a texel in front of a wall is caught in some phases and missed in others, so last frame can hold
46
+ * the wall where this frame holds the rail with nothing moved — and the depth test threw the history
47
+ * away, leaving the record nothing to keep. So each pixel also keeps the largest relative miss of
48
+ * last frame's depth it has seen lately, kept whatever the test says, and while still its tolerance
49
+ * widens to `DEPTH_SWING_WIDTH` of it — the temporal resolve's answer to a crack of light round a
50
+ * door, as a held peak rather than its running mean: a rail caught in one phase of eighteen misses
51
+ * once a period, and a mean has worn that miss down to a thirtieth by the next. **What it gives up**
52
+ * is the depth test's word at such a pixel while still: a surface arriving there inside the swing
53
+ * keeps the history, and the neighbourhood clip, which the unproven sample of an arrival leaves
54
+ * unwidened, is what takes it out.
55
+ *
33
56
  * **Conventions.** Images are rows from `v = 0` upward. Depths are clip depths in the matrices'
34
57
  * convention, OpenGL's here — the nearest is the smallest — and the renderer converts its reversed
35
58
  * buffer before a sample reaches this arithmetic, as the temporal resolve already does.
@@ -37,6 +60,7 @@
37
60
  import { clipToBox, rgbToYCoCg, varianceBox, yCoCgToRgb } from './clamp.js';
38
61
  import { disocclusionWeight, normalFromPositions, planarity, surfaceSlope, worldPositionFromDepth, } from './disocclusion.js';
39
62
  import { sampleBilinear, sampleHistoryBicubic5 } from './reproject.js';
63
+ import { TEMPORAL_FLICKER, flickerBlend, flickerRule, flickerWidening, freshFlicker, nextFlicker, stillness, } from '../temporalAa.js';
40
64
  /**
41
65
  * A tenth of each frame kept, as the temporal resolve keeps; a box of a standard deviation and a
42
66
  * quarter; a light sharpen. At two thirds of the output a side.
@@ -98,6 +122,12 @@ export function reconstructionWeight(dx, dy) {
98
122
  * pixel away.
99
123
  */
100
124
  export const CONFIDENT_WEIGHT = 1;
125
+ /**
126
+ * How many of its held depth swings a still pixel's depth test tolerates. The peak halves over a
127
+ * period, so the frame before the next once-a-period miss holds just over half of it: tolerating
128
+ * twice the peak passes that miss outright, and three leaves the same room again.
129
+ */
130
+ export const DEPTH_SWING_WIDTH = 3;
101
131
  const SAMPLE = new Float32Array(3);
102
132
  const GATHERED = new Float64Array(3);
103
133
  const HISTORY = new Float32Array(4);
@@ -113,6 +143,37 @@ const UP = new Float64Array(3);
113
143
  const NORMAL = new Float64Array(3);
114
144
  const PREVIOUS_NORMAL = new Float64Array(3);
115
145
  const CLIP = new Float64Array(4);
146
+ /** The last frame's reprojection, derived once a frame rather than once a pixel. */
147
+ const REPROJECTION = new Float64Array(16);
148
+ let reprojected = null;
149
+ /** `frame.reprojection`, or the product of its two matrices, multiplied once per frame. */
150
+ function reprojectionOf(frame) {
151
+ if (frame.reprojection !== undefined)
152
+ return frame.reprojection;
153
+ if (reprojected !== frame) {
154
+ reprojected = frame;
155
+ const a = frame.previousViewProj;
156
+ const b = frame.inverseViewProj;
157
+ for (let c = 0; c < 4; c += 1) {
158
+ for (let r = 0; r < 4; r += 1) {
159
+ let sum = 0;
160
+ for (let k = 0; k < 4; k += 1)
161
+ sum += a[k * 4 + r] * b[c * 4 + k];
162
+ REPROJECTION[c * 4 + r] = sum;
163
+ }
164
+ }
165
+ }
166
+ return REPROJECTION;
167
+ }
168
+ const HELD = { spread: 0, sum: 0, mean: 0, repeated: 0 };
169
+ const CARRIED = { spread: 0, sum: 0, mean: 0, repeated: 0 };
170
+ /** How far, in output pixels, the last `historyWeight` found its surface had moved. */
171
+ let moved = 0;
172
+ /** How far, relative, last frame's depth missed the one expected there; -1 where nothing was held. */
173
+ let missed = -1;
174
+ /** The rule for the last period asked for, kept: one is made per period, not per pixel. */
175
+ let rulePhases = 0;
176
+ let rule = TEMPORAL_FLICKER;
116
177
  function clampTexel(value, size) {
117
178
  return value < 0 ? 0 : value >= size ? size - 1 : value;
118
179
  }
@@ -184,7 +245,8 @@ function normalAt(depths, inverse, jitter, eye, width, height, x, y, out) {
184
245
  * was behind last frame's eye or off its picture, and otherwise what it gathered, scaled by whether it
185
246
  * is the same surface. Reads `HISTORY`; writes the history's colour there.
186
247
  */
187
- function historyWeight(frame, u, v, dx, dy) {
248
+ function historyWeight(frame, u, v, dx, dy, swing) {
249
+ missed = -1;
188
250
  if (!frame.hasHistory)
189
251
  return 0;
190
252
  const rw = frame.renderWidth;
@@ -199,27 +261,45 @@ function historyWeight(frame, u, v, dx, dy) {
199
261
  expected = frame.motion[flagAt + 2];
200
262
  }
201
263
  else {
202
- if (worldAt(frame.depth, frame.inverseViewProj, frame.jitter, rw, rh, dx, dy, WORLD) <= 0) {
264
+ /*
265
+ * **Through one matrix, last frame's view-projection times this frame's inverse**, rather than
266
+ * out to a world position and back. The two are the same arithmetic in exact numbers; in single
267
+ * precision the round trip is not the identity — a near plane a quarter of a metre from a far one
268
+ * five kilometres off — and a still camera measured a few hundredths of a pixel of motion growing
269
+ * out from the middle of the frame, which crept every still history and kept every pixel from
270
+ * the anti-flicker's stillness. The product is taken where the precision is (`reprojection`),
271
+ * and for a still camera it is the identity.
272
+ */
273
+ const du = (dx + 0.5 - frame.jitter[0]) / rw;
274
+ const dv = (dy + 0.5 - frame.jitter[1]) / rh;
275
+ const x = du * 2 - 1;
276
+ const y = dv * 2 - 1;
277
+ const z = frame.depth[dy * rw + dx];
278
+ const inverse = frame.inverseViewProj;
279
+ /* The homogeneous w of the world position: none, or behind the eye, is no surface. */
280
+ const hw = inverse[3] * x +
281
+ inverse[7] * y +
282
+ inverse[11] * z +
283
+ inverse[15];
284
+ if (!(hw > 1e-12))
203
285
  return 0;
204
- }
205
- const m = frame.previousViewProj;
286
+ const m = reprojectionOf(frame);
206
287
  for (let r = 0; r < 4; r += 1) {
207
288
  CLIP[r] =
208
- m[r] * WORLD[0] +
209
- m[4 + r] * WORLD[1] +
210
- m[8 + r] * WORLD[2] +
289
+ m[r] * x +
290
+ m[4 + r] * y +
291
+ m[8 + r] * z +
211
292
  m[12 + r];
212
293
  }
213
294
  /*
214
295
  * A surface behind last frame's eye has a w that is not positive: the disocclusion trusts no
215
296
  * such depth, so it needs no refusal of its own here.
216
297
  */
217
- expected = CLIP[3];
218
- const du = (dx + 0.5 - frame.jitter[0]) / rw;
219
- const dv = (dy + 0.5 - frame.jitter[1]) / rh;
220
- mu = (CLIP[0] / expected) * 0.5 + 0.5 - du;
221
- mv = (CLIP[1] / expected) * 0.5 + 0.5 - dv;
298
+ expected = CLIP[3] / hw;
299
+ mu = (CLIP[0] / CLIP[3]) * 0.5 + 0.5 - du;
300
+ mv = (CLIP[1] / CLIP[3]) * 0.5 + 0.5 - dv;
222
301
  }
302
+ moved = Math.hypot(mu * frame.outputWidth, mv * frame.outputHeight);
223
303
  const pu = u + mu;
224
304
  const pv = v + mv;
225
305
  if (!(pu >= 0 && pu <= 1 && pv >= 0 && pv <= 1))
@@ -284,14 +364,18 @@ function historyWeight(frame, u, v, dx, dy) {
284
364
  NORMAL[2] * PREVIOUS_NORMAL[2];
285
365
  normalDot = 1 - planar * (1 - cosine);
286
366
  }
287
- const trust = disocclusionWeight(expected, held, Math.hypot(mu, mv), normalDot, frame.disocclusion, slope);
367
+ if (held > 0 && expected > 0)
368
+ missed = Math.abs(held - expected) / expected;
369
+ /* A still pixel's own depth swing, as wide as the anti-flicker's box: see the header. */
370
+ const swung = swing > 0 ? DEPTH_SWING_WIDTH * swing * stillness(moved) : 0;
371
+ const trust = disocclusionWeight(expected, held, Math.hypot(mu, mv), normalDot, frame.disocclusion, slope + swung);
288
372
  return HISTORY[3] * trust;
289
373
  }
290
374
  /**
291
375
  * One output pixel: the history's next colour, before sharpening, and the weight it has gathered —
292
- * four values into `out`.
376
+ * four values into `out` — and, where the frame carries records, the pixel's next one into `record`.
293
377
  */
294
- export function resolvePixel(frame, ox, oy, out) {
378
+ export function resolvePixel(frame, ox, oy, out, record, swingOut) {
295
379
  const rw = frame.renderWidth;
296
380
  const rh = frame.renderHeight;
297
381
  const ow = frame.outputWidth;
@@ -339,8 +423,57 @@ export function resolvePixel(frame, ox, oy, out) {
339
423
  for (let c = 0; c < 3; c += 1)
340
424
  GATHERED[c] = GATHERED[c] / gathered;
341
425
  /* 3 and 4. The history, and the weight it still deserves. */
342
- const cap = (1 - frame.quality.alpha) / frame.quality.alpha;
343
- const kept = Math.min(cap, historyWeight(frame, u, v, dx, dy));
426
+ const alpha = frame.quality.alpha;
427
+ const records = frame.flicker;
428
+ const anti = records !== undefined && records !== null;
429
+ if (anti) {
430
+ const phases = frame.flickerPhases ?? 0;
431
+ if (phases !== rulePhases) {
432
+ rulePhases = phases;
433
+ rule = phases > 0 ? flickerRule(phases) : TEMPORAL_FLICKER;
434
+ }
435
+ }
436
+ const swings = frame.depthSwing;
437
+ const swing = anti && swings !== undefined && swings !== null ? swings[oy * ow + ox] : 0;
438
+ const weight = historyWeight(frame, u, v, dx, dy, swing);
439
+ let cap = (1 - alpha) / alpha;
440
+ /* The anti-flicker: a proven pixel's box widened by its spread, and more of its history kept. */
441
+ if (anti) {
442
+ const sample = GATHERED[0] * 0.2126 +
443
+ GATHERED[1] * 0.7152 +
444
+ GATHERED[2] * 0.0722;
445
+ const at = (oy * ow + ox) * 4;
446
+ if (weight > 0) {
447
+ HELD.spread = records[at];
448
+ HELD.sum = records[at + 1];
449
+ HELD.mean = records[at + 2];
450
+ HELD.repeated = records[at + 3];
451
+ }
452
+ else {
453
+ freshFlicker(HELD, sample, rule);
454
+ }
455
+ const still = weight > 0 ? stillness(moved) : 0;
456
+ const widen = flickerWidening(HELD, sample, still, rule);
457
+ for (let c = 0; c < 3; c += 1) {
458
+ LOW[c] = LOW[c] - widen;
459
+ HIGH[c] = HIGH[c] + widen;
460
+ }
461
+ const keep = flickerBlend(1 - alpha, HELD, sample, still, rule);
462
+ cap = keep / (1 - keep);
463
+ nextFlicker(CARRIED, HELD, sample, still, frame.periodStart === true, rule);
464
+ if (record !== undefined) {
465
+ record[0] = CARRIED.spread;
466
+ record[1] = CARRIED.sum;
467
+ record[2] = CARRIED.mean;
468
+ record[3] = CARRIED.repeated;
469
+ }
470
+ /* The peak carried on, halving over a period, whatever the test said; none where nothing was
471
+ held to miss. */
472
+ if (swingOut !== undefined) {
473
+ swingOut[0] = missed >= 0 ? Math.max(missed, swing * 0.5 ** rule.memory) : 0;
474
+ }
475
+ }
476
+ const kept = Math.min(cap, weight);
344
477
  /* 5. Clipped, mixed by weight, and the gaps filled from the render. */
345
478
  const total = kept + gathered;
346
479
  if (kept > 0) {
@@ -359,6 +492,8 @@ export function resolvePixel(frame, ox, oy, out) {
359
492
  out[3] = total;
360
493
  }
361
494
  const TAP = new Float32Array(4);
495
+ const RECORD = new Float32Array(4);
496
+ const SWING = new Float32Array(1);
362
497
  const CENTRE = new Float32Array(4);
363
498
  const AROUND = new Float32Array(12);
364
499
  const SHOWN = new Float32Array(3);
@@ -370,15 +505,21 @@ const OFFSETS = [
370
505
  ];
371
506
  /**
372
507
  * The whole frame: the next history into `history`, four floats a pixel, and the picture shown into
373
- * `shown`, three — the history sharpened against the four direct neighbours it has just been given.
508
+ * `shown`, three — the history sharpened against the four direct neighbours it has just been given —
509
+ * and, where the frame carries records, the next ones into `records`, four floats a pixel, and the
510
+ * next depth swings into `swings`, one.
374
511
  */
375
- export function resolveFrame(frame, history, shown) {
512
+ export function resolveFrame(frame, history, shown, records, swings) {
376
513
  const ow = frame.outputWidth;
377
514
  const oh = frame.outputHeight;
378
515
  for (let y = 0; y < oh; y += 1) {
379
516
  for (let x = 0; x < ow; x += 1) {
380
- resolvePixel(frame, x, y, TAP);
517
+ resolvePixel(frame, x, y, TAP, RECORD, SWING);
381
518
  history.set(TAP, (y * ow + x) * 4);
519
+ if (records !== undefined)
520
+ records.set(RECORD, (y * ow + x) * 4);
521
+ if (swings !== undefined)
522
+ swings[y * ow + x] = SWING[0];
382
523
  }
383
524
  }
384
525
  for (let y = 0; y < oh; y += 1) {
@@ -29,6 +29,10 @@ export function recordingGl(options = {}) {
29
29
  const programFragments = new WeakMap();
30
30
  const calls = [];
31
31
  const constants = {
32
+ /* The canvas's size, as a real context reports its drawing buffer; a function here made every
33
+ jitter and every per-pixel offset divided by it a NaN. */
34
+ drawingBufferWidth: 640,
35
+ drawingBufferHeight: 480,
32
36
  ACTIVE_UNIFORMS: 0x8b86,
33
37
  ACTIVE_ATTRIBUTES: 0x8b89,
34
38
  COMPILE_STATUS: 0x8b81,
@@ -293,6 +293,8 @@ export declare class SceneTarget {
293
293
  /** The projection's x and y scales, which turn a world radius into a screen one. */
294
294
  readonly projScale: Float32Array;
295
295
  readonly invProjection: Float32Array;
296
+ /** Where the frame's jitter put the occlusion, in uv: see the shader's `uAoOffset`. */
297
+ readonly offset?: Float32Array;
296
298
  }, bloom?: {
297
299
  readonly strength: number;
298
300
  /** In scene units, which is why this wants `keepsRange`. See `BloomPass`. */
@@ -834,6 +834,8 @@ export class SceneTarget {
834
834
  gl.bindTexture(gl.TEXTURE_2D, aoTexture ?? scene);
835
835
  gl.uniform1i(this.uniforms['uAo'] ?? null, 2);
836
836
  gl.uniform1f(this.uniforms['uAoStrength'] ?? null, aoTexture === null ? 0 : (ao?.strength ?? 0));
837
+ const offset = ao?.offset;
838
+ gl.uniform2f(this.uniforms['uAoOffset'] ?? null, offset?.[0] ?? 0, offset?.[1] ?? 0);
837
839
  /* Bloom on its own unit, and the placeholder for the same reason as the one above: a
838
840
  driver may fetch a texture's descriptor before it evaluates the branch that skips the
839
841
  read, and an incomplete texture is not a thing to hand it. */
@@ -63,14 +63,15 @@ ${CHANNEL_BEND}
63
63
  * uniforms declared after every other, so no field of the plain variant moves and a binder written
64
64
  * against it stays right for this one; and two outputs after vLightPosition, which keeps location 0.
65
65
  */
66
- layout(location = 5) in vec2 aUv;
66
+ layout(location = 5) in vec3 aUv;
67
67
  uniform vec2 uUvScale;
68
68
  uniform float uAlphaCutout;
69
69
  #endif
70
70
 
71
71
  out vec4 vLightPosition;
72
72
  #if CUTOUT
73
- out vec2 vUv;
73
+ /* The texture coordinate and, in z, the texture-array layer the face wears. */
74
+ out vec3 vUv;
74
75
  flat out float vAlphaCutout;
75
76
  #endif
76
77
  #if GLASS
@@ -102,7 +103,7 @@ void main() {
102
103
  vLightPosition = uLightViewProj * vec4(bent, world.w);
103
104
  gl_Position = vLightPosition;
104
105
  #if CUTOUT
105
- vUv = aUv * uUvScale;
106
+ vUv = vec3(aUv.xy * uUvScale, aUv.z);
106
107
  vAlphaCutout = uAlphaCutout;
107
108
  #endif
108
109
  #if GLASS
@@ -168,16 +169,17 @@ void main() {
168
169
  export const DEPTH_CUTOUT_FRAG = `#version 300 es
169
170
  precision highp float;
170
171
  in vec4 vLightPosition;
171
- in vec2 vUv;
172
+ in vec3 vUv;
172
173
  flat in float vAlphaCutout;
173
174
 
174
175
  uniform highp sampler2DArray uPreviousShadowMap;
175
176
  uniform int uPeelShadowLayer;
176
- uniform highp sampler2D uCutoutMap;
177
+ uniform highp sampler2DArray uCutoutMap;
177
178
  ${CUTOUT_COVERAGE_GLSL}
178
179
  void main() {
179
180
  /* Credited for its mip level as the surface's own test is, so a leaf casts the shape it draws. */
180
- float alpha = cutoutAlpha(texture(uCutoutMap, vUv).a, vUv * vec2(textureSize(uCutoutMap, 0)));
181
+ vec3 at = vec3(vUv.xy, floor(vUv.z + 0.5));
182
+ float alpha = cutoutAlpha(texture(uCutoutMap, at).a, vUv.xy * vec2(textureSize(uCutoutMap, 0).xy));
181
183
  if (uPeelShadowLayer != 0) {
182
184
  vec3 p = vLightPosition.xyz / vLightPosition.w;
183
185
  vec2 uv = p.xy * 0.5 + 0.5;
@@ -237,19 +239,20 @@ ${GLASS_TINT_BODY}
237
239
  export const GLASS_TINT_CUTOUT_FRAG = `#version 300 es
238
240
  precision highp float;
239
241
  in vec4 vLightPosition;
240
- in vec2 vUv;
242
+ in vec3 vUv;
241
243
  flat in float vAlphaCutout;
242
244
  in vec3 vGlassWorld;
243
245
 
244
246
  uniform vec4 uGlassPane;
245
247
  uniform vec4 uGlassLight;
246
- uniform highp sampler2D uCutoutMap;
248
+ uniform highp sampler2DArray uCutoutMap;
247
249
  ${CUTOUT_COVERAGE_GLSL}
248
250
  out vec4 outTint;
249
251
 
250
252
  void main() {
251
253
  /* First, in uniform control flow, as the depth cutout samples it. */
252
- float alpha = cutoutAlpha(texture(uCutoutMap, vUv).a, vUv * vec2(textureSize(uCutoutMap, 0)));
254
+ vec3 at = vec3(vUv.xy, floor(vUv.z + 0.5));
255
+ float alpha = cutoutAlpha(texture(uCutoutMap, at).a, vUv.xy * vec2(textureSize(uCutoutMap, 0).xy));
253
256
  ${GLASS_TINT_BODY}
254
257
  if (alpha < vAlphaCutout) discard;
255
258
  }
@@ -26,4 +26,4 @@
26
26
  * `textureLod` throughout, by the 2026-08-07 rule: this is reached through branches no compiler can
27
27
  * prove uniform. Both samplers are declared `highp` because GLSL ES gives 3D samplers no default.
28
28
  */
29
- export declare const DRIFT_LIGHT_GLSL = "\n/** x how much of the summed light is in the frame (0 off), y the radius, z the band, w the scale. */\nuniform vec4 uDriftLight;\n/** xyz the first brick's first sample; w metres between samples, a third of a brick. */\nuniform vec4 uDriftLightOrigin;\nuniform highp usampler3D uDriftLightIndex; // wgsl:share shadow\nuniform highp sampler3D uDriftLightAtlas;\n\n/** The share of this pixel's field light the volume stands in for: 0 inside the radius, 1 past it. */\nfloat driftLightShare(vec3 world) {\n if (uDriftLight.x <= 0.0) return 0.0;\n float away = distance(world, uCameraPos);\n return smoothstep(uDriftLight.y - uDriftLight.z, uDriftLight.y, away) * uDriftLight.x;\n}\n\n/** The summed light arriving on a surface at `world` facing `n`. */\nvec3 driftLightIrradiance(vec3 world, vec3 n) {\n float spacing = uDriftLightOrigin.w;\n vec3 at = (world + n * (0.5 * spacing) - uDriftLightOrigin.xyz) / (3.0 * spacing);\n ivec3 cell = ivec3(floor(at));\n if (any(lessThan(cell, ivec3(0))) || any(greaterThanEqual(cell, textureSize(uDriftLightIndex, 0)))) {\n return vec3(0.0);\n }\n uint id = texelFetch(uDriftLightIndex, cell, 0).r;\n if (id == 0u) return vec3(0.0);\n /* A brick is eight texels across (its light block, then its direction block) and four up and deep. */\n ivec3 size = textureSize(uDriftLightAtlas, 0);\n int brick = int(id) - 1;\n int across = size.x / 8;\n int down = size.y / 4;\n vec3 corner = vec3(\n float((brick % across) * 8),\n float(((brick / across) % down) * 4),\n float((brick / (across * down)) * 4)\n );\n vec3 texel = 1.0 / vec3(size);\n vec3 inside = (at - vec3(cell)) * 3.0 + 0.5;\n vec4 light = textureLod(uDriftLightAtlas, (corner + inside) * texel, 0.0);\n if (light.a < 1e-3) return vec3(0.0);\n vec4 toward = textureLod(uDriftLightAtlas, (corner + vec3(4.0, 0.0, 0.0) + inside) * texel, 0.0);\n vec3 arriving = light.rgb / light.a;\n vec3 from = toward.xyz / light.a;\n /* Lambert for a single light, a quarter for light arriving from everywhere: see `bake.ts`. */\n float facing = max(dot(n, from), 0.0) + (1.0 - min(length(from), 1.0)) * 0.25;\n return arriving * facing;\n}\n";
29
+ export declare const DRIFT_LIGHT_GLSL = "\n/** x how much of the summed light is in the frame (0 off), y the radius, z the band, w the scale. */\nuniform vec4 uDriftLight;\n/** xyz the first sample; w metres between samples \u2014 a third of a brick, or negated for a dense volume. */\nuniform vec4 uDriftLightOrigin;\nuniform highp usampler3D uDriftLightIndex; // wgsl:share shadow\nuniform highp sampler3D uDriftLightAtlas;\n\n/** The share of this pixel's field light the volume stands in for: 0 inside the radius, 1 past it. */\nfloat driftLightShare(vec3 world) {\n if (uDriftLight.x <= 0.0) return 0.0;\n float away = distance(world, uCameraPos);\n return smoothstep(uDriftLight.y - uDriftLight.z, uDriftLight.y, away) * uDriftLight.x;\n}\n\n/**\n * A sample's light and direction turned into the light on a surface facing `n`: Lambert for a\n * single light, a quarter for light arriving from everywhere (see `bake.ts`). One statement for\n * both kinds of volume.\n */\nvec3 driftLightFacing(vec4 light, vec4 toward, vec3 n) {\n vec3 arriving = light.rgb / light.a;\n vec3 from = toward.xyz / light.a;\n float facing = max(dot(n, from), 0.0) + (1.0 - min(length(from), 1.0)) * 0.25;\n return arriving * facing;\n}\n\n/**\n * The same from a dense volume: one sample a texel, the light in the lower half of y and its\n * direction in the upper. A lookup kept inside the samples never filters across the halves.\n */\nvec3 driftLightDense(vec3 world, vec3 n, float spacing) {\n ivec3 size = textureSize(uDriftLightAtlas, 0);\n vec3 dims = vec3(float(size.x), float(size.y / 2), float(size.z));\n vec3 at = (world + n * (0.5 * spacing) - uDriftLightOrigin.xyz) / spacing;\n if (any(lessThan(at, vec3(0.0))) || any(greaterThan(at, dims - 1.0))) return vec3(0.0);\n vec3 uvw = (at + 0.5) / vec3(dims.x, dims.y * 2.0, dims.z);\n vec4 light = textureLod(uDriftLightAtlas, uvw, 0.0);\n if (light.a < 1e-3) return vec3(0.0);\n vec4 toward = textureLod(uDriftLightAtlas, uvw + vec3(0.0, 0.5, 0.0), 0.0);\n return driftLightFacing(light, toward, n);\n}\n\n/** The summed light arriving on a surface at `world` facing `n`. A negative spacing is dense. */\nvec3 driftLightIrradiance(vec3 world, vec3 n) {\n float spacing = uDriftLightOrigin.w;\n if (spacing < 0.0) return driftLightDense(world, n, -spacing);\n vec3 at = (world + n * (0.5 * spacing) - uDriftLightOrigin.xyz) / (3.0 * spacing);\n ivec3 cell = ivec3(floor(at));\n if (any(lessThan(cell, ivec3(0))) || any(greaterThanEqual(cell, textureSize(uDriftLightIndex, 0)))) {\n return vec3(0.0);\n }\n uint id = texelFetch(uDriftLightIndex, cell, 0).r;\n if (id == 0u) return vec3(0.0);\n /* A brick is eight texels across (its light block, then its direction block) and four up and deep. */\n ivec3 size = textureSize(uDriftLightAtlas, 0);\n int brick = int(id) - 1;\n int across = size.x / 8;\n int down = size.y / 4;\n vec3 corner = vec3(\n float((brick % across) * 8),\n float(((brick / across) % down) * 4),\n float((brick / (across * down)) * 4)\n );\n vec3 texel = 1.0 / vec3(size);\n vec3 inside = (at - vec3(cell)) * 3.0 + 0.5;\n vec4 light = textureLod(uDriftLightAtlas, (corner + inside) * texel, 0.0);\n if (light.a < 1e-3) return vec3(0.0);\n vec4 toward = textureLod(uDriftLightAtlas, (corner + vec3(4.0, 0.0, 0.0) + inside) * texel, 0.0);\n return driftLightFacing(light, toward, n);\n}\n";
@@ -29,7 +29,7 @@
29
29
  export const DRIFT_LIGHT_GLSL = /* glsl */ `
30
30
  /** x how much of the summed light is in the frame (0 off), y the radius, z the band, w the scale. */
31
31
  uniform vec4 uDriftLight;
32
- /** xyz the first brick's first sample; w metres between samples, a third of a brick. */
32
+ /** xyz the first sample; w metres between samples — a third of a brick, or negated for a dense volume. */
33
33
  uniform vec4 uDriftLightOrigin;
34
34
  uniform highp usampler3D uDriftLightIndex; // wgsl:share shadow
35
35
  uniform highp sampler3D uDriftLightAtlas;
@@ -41,9 +41,38 @@ float driftLightShare(vec3 world) {
41
41
  return smoothstep(uDriftLight.y - uDriftLight.z, uDriftLight.y, away) * uDriftLight.x;
42
42
  }
43
43
 
44
- /** The summed light arriving on a surface at \`world\` facing \`n\`. */
44
+ /**
45
+ * A sample's light and direction turned into the light on a surface facing \`n\`: Lambert for a
46
+ * single light, a quarter for light arriving from everywhere (see \`bake.ts\`). One statement for
47
+ * both kinds of volume.
48
+ */
49
+ vec3 driftLightFacing(vec4 light, vec4 toward, vec3 n) {
50
+ vec3 arriving = light.rgb / light.a;
51
+ vec3 from = toward.xyz / light.a;
52
+ float facing = max(dot(n, from), 0.0) + (1.0 - min(length(from), 1.0)) * 0.25;
53
+ return arriving * facing;
54
+ }
55
+
56
+ /**
57
+ * The same from a dense volume: one sample a texel, the light in the lower half of y and its
58
+ * direction in the upper. A lookup kept inside the samples never filters across the halves.
59
+ */
60
+ vec3 driftLightDense(vec3 world, vec3 n, float spacing) {
61
+ ivec3 size = textureSize(uDriftLightAtlas, 0);
62
+ vec3 dims = vec3(float(size.x), float(size.y / 2), float(size.z));
63
+ vec3 at = (world + n * (0.5 * spacing) - uDriftLightOrigin.xyz) / spacing;
64
+ if (any(lessThan(at, vec3(0.0))) || any(greaterThan(at, dims - 1.0))) return vec3(0.0);
65
+ vec3 uvw = (at + 0.5) / vec3(dims.x, dims.y * 2.0, dims.z);
66
+ vec4 light = textureLod(uDriftLightAtlas, uvw, 0.0);
67
+ if (light.a < 1e-3) return vec3(0.0);
68
+ vec4 toward = textureLod(uDriftLightAtlas, uvw + vec3(0.0, 0.5, 0.0), 0.0);
69
+ return driftLightFacing(light, toward, n);
70
+ }
71
+
72
+ /** The summed light arriving on a surface at \`world\` facing \`n\`. A negative spacing is dense. */
45
73
  vec3 driftLightIrradiance(vec3 world, vec3 n) {
46
74
  float spacing = uDriftLightOrigin.w;
75
+ if (spacing < 0.0) return driftLightDense(world, n, -spacing);
47
76
  vec3 at = (world + n * (0.5 * spacing) - uDriftLightOrigin.xyz) / (3.0 * spacing);
48
77
  ivec3 cell = ivec3(floor(at));
49
78
  if (any(lessThan(cell, ivec3(0))) || any(greaterThanEqual(cell, textureSize(uDriftLightIndex, 0)))) {
@@ -66,10 +95,6 @@ vec3 driftLightIrradiance(vec3 world, vec3 n) {
66
95
  vec4 light = textureLod(uDriftLightAtlas, (corner + inside) * texel, 0.0);
67
96
  if (light.a < 1e-3) return vec3(0.0);
68
97
  vec4 toward = textureLod(uDriftLightAtlas, (corner + vec3(4.0, 0.0, 0.0) + inside) * texel, 0.0);
69
- vec3 arriving = light.rgb / light.a;
70
- vec3 from = toward.xyz / light.a;
71
- /* Lambert for a single light, a quarter for light arriving from everywhere: see \`bake.ts\`. */
72
- float facing = max(dot(n, from), 0.0) + (1.0 - min(length(from), 1.0)) * 0.25;
73
- return arriving * facing;
98
+ return driftLightFacing(light, toward, n);
74
99
  }
75
100
  `;
@@ -13,6 +13,7 @@ import { SKINNING_GLSL } from '../skinning.js';
13
13
  import { SURFACE_GLSL } from './surface.js';
14
14
  import { MAIN_GLSL } from './main.js';
15
15
  import { DRIFT_LIGHT_GLSL } from './driftLight.js';
16
+ import { SURFACE_EFFECTS_GLSL } from './surfaceEffects.js';
16
17
  import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from '../vertexChannel.js';
17
18
  import { FULL_LIGHT_BUDGET } from '../../uniformVectorBudget.js';
18
19
  /**
@@ -27,7 +28,7 @@ layout(location = 1) in vec3 aNormal;
27
28
  layout(location = 2) in vec3 aColor;
28
29
  layout(location = 3) in float aEmissive;
29
30
  layout(location = 4) in float aSpecular;
30
- layout(location = 5) in vec2 aUv;
31
+ layout(location = 5) in vec3 aUv;
31
32
  layout(location = 6) in vec3 aEmissiveColor;
32
33
  layout(location = 7) in float aRoughness;
33
34
  layout(location = 8) in float aGrain;
@@ -145,7 +146,7 @@ out vec3 vWorldPos;
145
146
  out float vEmissive;
146
147
  out float vSpecular;
147
148
  out vec4 vLightPos;
148
- out vec2 vUv;
149
+ out vec3 vUv;
149
150
  out vec3 vEmissiveColor;
150
151
  out float vRoughness;
151
152
  out float vGrain;
@@ -242,7 +243,7 @@ void main() {
242
243
  vColor = aColor * tint;
243
244
  vEmissive = aEmissive;
244
245
  vSpecular = aSpecular;
245
- vUv = aUv * uUvScale;
246
+ vUv = vec3(aUv.xy * uUvScale, aUv.z);
246
247
  vEmissiveColor = aEmissiveColor;
247
248
  vRoughness = aRoughness;
248
249
  vGrain = aGrain;
@@ -386,6 +387,7 @@ export function flatFrag(options) {
386
387
  /* Unconditional for the same reason, and called only where a cutoff was asked for. */
387
388
  CUTOUT_COVERAGE_GLSL,
388
389
  DRIFT_LIGHT_GLSL,
390
+ SURFACE_EFFECTS_GLSL,
389
391
  MAIN_GLSL,
390
392
  ].join('\n'), {
391
393
  POINT_SHADOWS: options.pointShadows,