@driftengine/core 4.4.1 → 4.4.2

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 (38) hide show
  1. package/CHANGELOG.json +26 -0
  2. package/README.md +1 -1
  3. package/dist/render/backend/webgl2/renderer.d.ts +3 -0
  4. package/dist/render/backend/webgl2/renderer.js +13 -0
  5. package/dist/render/backend/webgpu/postPass.d.ts +19 -3
  6. package/dist/render/backend/webgpu/postPass.js +17 -4
  7. package/dist/render/backend/webgpu/renderer.d.ts +10 -0
  8. package/dist/render/backend/webgpu/renderer.js +67 -4
  9. package/dist/render/flameLight.js +15 -2
  10. package/dist/render/pointShadowBudget.js +51 -41
  11. package/dist/render/recon/disocclusion.d.ts +36 -3
  12. package/dist/render/recon/disocclusion.js +56 -4
  13. package/dist/render/recon/resolve.js +95 -12
  14. package/dist/render/sceneTarget.d.ts +2 -0
  15. package/dist/render/sceneTarget.js +1 -1
  16. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +31 -6
  17. package/dist/render/shaders/generated/temporalResolve.wgsl.js +31 -6
  18. package/dist/render/shaders/recon/resolve.wgsl.d.ts +1 -1
  19. package/dist/render/shaders/recon/resolve.wgsl.js +112 -8
  20. package/dist/render/shaders/temporalResolve.js +133 -5
  21. package/dist/render/temporalAa.d.ts +145 -0
  22. package/dist/render/temporalAa.js +190 -0
  23. package/dist/render/temporalPass.d.ts +16 -14
  24. package/dist/render/temporalPass.js +59 -14
  25. package/package.json +6 -6
  26. package/src/render/backend/webgl2/renderer.ts +21 -1
  27. package/src/render/backend/webgpu/postPass.ts +34 -4
  28. package/src/render/backend/webgpu/renderer.ts +70 -4
  29. package/src/render/flameLight.ts +15 -2
  30. package/src/render/pointShadowBudget.ts +49 -39
  31. package/src/render/recon/disocclusion.ts +54 -3
  32. package/src/render/recon/resolve.ts +137 -10
  33. package/src/render/sceneTarget.ts +8 -1
  34. package/src/render/shaders/generated/temporalResolve.wgsl.ts +31 -6
  35. package/src/render/shaders/recon/resolve.wgsl.ts +112 -8
  36. package/src/render/shaders/temporalResolve.ts +141 -5
  37. package/src/render/temporalAa.ts +227 -0
  38. package/src/render/temporalPass.ts +88 -12
@@ -39,6 +39,7 @@
39
39
  * and the uniform `recon: ReconParams`, whose struct this module declares.
40
40
  */
41
41
  import { wgslSceneDepthToNdc } from '../../depthConvention.ts';
42
+ import { PLANAR_FROM, PLANAR_TO } from '../../recon/disocclusion.ts';
42
43
 
43
44
  /**
44
45
  * The parameters one resolve reads, as `ResolveFrame` minus its images.
@@ -97,6 +98,8 @@ export const RECON_HISTORY_FORMAT: GPUTextureFormat = 'rgba16float';
97
98
  export const RECON_RESOLVE_CORE_WGSL = /* wgsl */ `
98
99
  const RECON_SAMPLE_SPREAD: f32 = ${RECON_SAMPLE_SPREAD};
99
100
  const RECON_CONFIDENT_WEIGHT: f32 = ${RECON_CONFIDENT_WEIGHT}.0;
101
+ const RECON_PLANAR_FROM: f32 = ${PLANAR_FROM};
102
+ const RECON_PLANAR_TO: f32 = ${PLANAR_TO};
100
103
 
101
104
  fn reconClampTexel(value: i32, size: i32) -> i32 {
102
105
  return clamp(value, 0, size - 1);
@@ -328,10 +331,11 @@ fn reconDisocclusion(
328
331
  histDepth: f32,
329
332
  motionLength: f32,
330
333
  normalDot: f32,
334
+ slope: f32,
331
335
  ) -> f32 {
332
336
  if (!(currDepth > 0.0) || !(histDepth > 0.0) || !(motionLength >= 0.0)) { return 0.0; }
333
337
  let disagreement = abs(currDepth - histDepth) / currDepth;
334
- let tolerance = recon.depthTolerance + recon.motionScale * motionLength;
338
+ let tolerance = recon.depthTolerance + recon.motionScale * motionLength + slope;
335
339
  let depth = 1.0 - reconSmoothstep(tolerance, 2.0 * tolerance, disagreement);
336
340
  let normal = reconSmoothstep(recon.normalFloor, recon.normalCeiling, normalDot);
337
341
  return depth * normal;
@@ -392,6 +396,74 @@ fn reconNormalAt(previous: bool, x: i32, y: i32) -> ReconNormal {
392
396
  return reconNormalFrom(centre.position, right.position, up.position, eye);
393
397
  }
394
398
 
399
+ /* resolve.ts's viewDepthAt. */
400
+ fn reconViewDepthAt(previous: bool, x: i32, y: i32) -> f32 {
401
+ let w = i32(recon.renderSize.x);
402
+ let h = i32(recon.renderSize.y);
403
+ if (x < 0 || y < 0 || x >= w || y >= h) { return 0.0; }
404
+ var jitter = recon.jitter;
405
+ var m = recon.inverseViewProj;
406
+ var z = 0.0;
407
+ if (previous) {
408
+ jitter = recon.previousJitter;
409
+ m = recon.previousInverseViewProj;
410
+ z = reconPreviousDepth(x, y);
411
+ } else {
412
+ z = reconDepth(x, y);
413
+ }
414
+ let u = (f32(x) + 0.5 - jitter.x) / f32(w);
415
+ let v = (f32(y) + 0.5 - jitter.y) / f32(h);
416
+ 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];
417
+ if (!(abs(cw) > 1e-12)) { return 0.0; }
418
+ return 1.0 / cw;
419
+ }
420
+
421
+ /* disocclusion.ts's planarity. */
422
+ fn reconPlanarity(before: f32, centre: f32, after: f32) -> f32 {
423
+ if (!(before > 0.0) || !(centre > 0.0) || !(after > 0.0)) { return 1.0; }
424
+ let a = 1.0 / after - 1.0 / centre;
425
+ let b = 1.0 / before - 1.0 / centre;
426
+ let spread = abs(a) + abs(b);
427
+ if (!(spread > 0.0)) { return 1.0; }
428
+ return 1.0 - reconSmoothstep(RECON_PLANAR_FROM, RECON_PLANAR_TO, abs(a + b) / spread);
429
+ }
430
+
431
+ /* disocclusion.ts's surfaceSlope. */
432
+ fn reconSurfaceSlope(before: f32, centre: f32, after: f32) -> f32 {
433
+ let hasAfter = after > 0.0;
434
+ let hasBefore = before > 0.0;
435
+ if (!hasAfter) {
436
+ if (!hasBefore) { return 0.0; }
437
+ return abs(before - centre) / centre;
438
+ }
439
+ if (!hasBefore) { return abs(after - centre) / centre; }
440
+ let a = after - centre;
441
+ let b = before - centre;
442
+ if (a * b > 0.0) { return 0.0; }
443
+ return min(abs(a), abs(b)) / centre;
444
+ }
445
+
446
+ struct ReconSurface {
447
+ planar: f32,
448
+ slope: f32,
449
+ }
450
+
451
+ /* resolve.ts's surfaceAt. */
452
+ fn reconSurfaceAt(previous: bool, x: i32, y: i32, centre: f32) -> ReconSurface {
453
+ var out: ReconSurface;
454
+ out.planar = 1.0;
455
+ out.slope = 0.0;
456
+ for (var axis = 0; axis < 2; axis = axis + 1) {
457
+ let ax = 1 - axis;
458
+ let ay = axis;
459
+ let after = reconViewDepthAt(previous, x + ax, y + ay);
460
+ let before = reconViewDepthAt(previous, x - ax, y - ay);
461
+ out.planar = out.planar * reconPlanarity(before, centre, after);
462
+ out.slope = max(out.slope, reconSurfaceSlope(before, centre, after));
463
+ }
464
+ return out;
465
+ }
466
+
395
467
  struct ReconHistory {
396
468
  weight: f32,
397
469
  colour: vec4<f32>,
@@ -439,14 +511,46 @@ fn reconHistoryWeight(u: f32, v: f32, dx: i32, dy: i32) -> ReconHistory {
439
511
  pu * f32(recon.outputSize.x) - 0.5,
440
512
  pv * f32(recon.outputSize.y) - 0.5,
441
513
  );
442
- let px = reconClampTexel(i32(reconRound(pu * rw + recon.previousJitter.x - 0.5)), i32(rw));
443
- let py = reconClampTexel(i32(reconRound(pv * rh + recon.previousJitter.y - 0.5)), i32(rh));
444
- let held = reconWorldAt(true, px, py).depth;
514
+ /* Last frame's depth, searched around where the dilated texel was: see resolve.ts. */
515
+ let su = (f32(dx) + 0.5 - recon.jitter.x) / rw + mu;
516
+ let sv = (f32(dy) + 0.5 - recon.jitter.y) / rh + mv;
517
+ let cx = reconClampTexel(i32(reconRound(su * rw + recon.previousJitter.x - 0.5)), i32(rw));
518
+ let cy = reconClampTexel(i32(reconRound(sv * rh + recon.previousJitter.y - 0.5)), i32(rh));
519
+ var held = reconViewDepthAt(true, cx, cy);
520
+ var px = cx;
521
+ var py = cy;
522
+ if (!(held > 0.0 && abs(held - expected) <= recon.depthTolerance * expected)) {
523
+ held = 0.0;
524
+ var nearest = 3.4e38;
525
+ for (var j = -1; j <= 1; j = j + 1) {
526
+ for (var i = -1; i <= 1; i = i + 1) {
527
+ let tx = reconClampTexel(cx + i, i32(rw));
528
+ let ty = reconClampTexel(cy + j, i32(rh));
529
+ let depth = reconViewDepthAt(true, tx, ty);
530
+ if (!(depth > 0.0)) { continue; }
531
+ let off = abs(depth - expected);
532
+ if (off < nearest) {
533
+ nearest = off;
534
+ held = depth;
535
+ px = tx;
536
+ py = ty;
537
+ }
538
+ }
539
+ }
540
+ }
541
+
542
+ let here = reconSurfaceAt(false, dx, dy, reconViewDepthAt(false, dx, dy));
543
+ var planar = here.planar;
544
+ if (planar > 0.0) { planar = planar * reconSurfaceAt(true, px, py, held).planar; }
445
545
  var normalDot = 1.0;
446
- let here = reconNormalAt(false, dx, dy);
447
- let there = reconNormalAt(true, px, py);
448
- if (here.ok && there.ok) { normalDot = dot(here.normal, there.normal); }
449
- let trust = reconDisocclusion(expected, held, length(vec2<f32>(mu, mv)), normalDot);
546
+ if (planar > 0.0) {
547
+ let hereNormal = reconNormalAt(false, dx, dy);
548
+ let thereNormal = reconNormalAt(true, px, py);
549
+ if (hereNormal.ok && thereNormal.ok) {
550
+ normalDot = 1.0 - planar * (1.0 - dot(hereNormal.normal, thereNormal.normal));
551
+ }
552
+ }
553
+ let trust = reconDisocclusion(expected, held, length(vec2<f32>(mu, mv)), normalDot, here.slope);
450
554
  out.weight = out.colour.w * trust;
451
555
  return out;
452
556
  }
@@ -20,6 +20,15 @@
20
20
  * The vertex stage is `FULLSCREEN_VERT`, shared with the occlusion pass, its blur and the rush.
21
21
  */
22
22
  import { glslSceneDepthToNdc } from '../depthConvention.ts';
23
+ import {
24
+ FLICKER_BLEND,
25
+ FLICKER_MEMORY,
26
+ FLICKER_REPEAT,
27
+ FLICKER_SURFACE,
28
+ FLICKER_WIDTH,
29
+ STILL_FROM,
30
+ STILL_TO,
31
+ } from '../temporalAa.ts';
23
32
 
24
33
  export const TEMPORAL_RESOLVE_FRAG = `#version 300 es
25
34
  precision highp float;
@@ -30,6 +39,22 @@ in vec2 vUv;
30
39
  uniform sampler2D uScene;
31
40
  /** What this pass wrote last frame: resolved, and conceptually unjittered. */
32
41
  uniform sampler2D uHistory;
42
+ /**
43
+ * Each pixel's flicker record from last frame, the anti-flicker in temporalAa.ts: its spread about
44
+ * its period's mean, the sum this period has gathered, the last period's mean, and how many periods
45
+ * running have repeated. highp for the reason the depth's is: a luma in a half-float scene is not a
46
+ * value from 0 to 1.
47
+ */
48
+ uniform highp sampler2D uFlicker;
49
+ /**
50
+ * The rest of the record: the view depth the pixel's sample stood at, so the next frame can tell
51
+ * whether it follows the same surface, and the pixel's own depth swing, which that test allows for.
52
+ */
53
+ uniform highp sampler2D uFlickerMotion;
54
+ /** The inverse projection's depth row and w row, as the other passes read a depth into metres. */
55
+ uniform vec4 uDepthToViewZ;
56
+ /** One where the records can hold what they hold, zero where they cannot: then 4.4.1's resolve. */
57
+ uniform float uAntiFlicker;
33
58
  /**
34
59
  * The frame's depth.
35
60
  *
@@ -53,6 +78,8 @@ uniform highp sampler2D uDepth;
53
78
  uniform mat4 uReprojection;
54
79
  /** One texel of the target, for walking the neighbourhood. */
55
80
  uniform vec2 uTexel;
81
+ /** One on the first frame of the jitter's period, where the anti-flicker compares two periods. */
82
+ uniform float uPeriodStart;
56
83
  /**
57
84
  * How much of the clipped history to keep, 0 to 1.
58
85
  *
@@ -63,7 +90,41 @@ uniform vec2 uTexel;
63
90
  */
64
91
  uniform float uHistoryBlend;
65
92
 
66
- out vec4 fragColor;
93
+ layout(location = 0) out vec4 fragColor;
94
+ /** The record carried out, in the texture uFlicker reads next frame. */
95
+ layout(location = 1) out vec4 flickerOut;
96
+ /** And the rest of it, in the texture uFlickerMotion reads. */
97
+ layout(location = 2) out vec4 flickerMotionOut;
98
+
99
+ const float FLICKER_MEMORY = ${FLICKER_MEMORY.toFixed(3)};
100
+ const float FLICKER_WIDTH = ${FLICKER_WIDTH.toFixed(1)};
101
+ const float FLICKER_REPEAT = ${FLICKER_REPEAT.toFixed(2)};
102
+ const float FLICKER_BLEND = ${FLICKER_BLEND.toFixed(2)};
103
+ const float FLICKER_SURFACE = ${FLICKER_SURFACE.toFixed(2)};
104
+ const float STILL_FROM = ${STILL_FROM.toFixed(2)};
105
+ const float STILL_TO = ${STILL_TO.toFixed(2)};
106
+
107
+ float luma(vec3 c) {
108
+ return dot(c, vec3(0.2126, 0.7152, 0.0722));
109
+ }
110
+
111
+ /** A record starting over: no spread, and nothing repeated yet. */
112
+ vec4 freshRecord(vec3 current) {
113
+ float seen = luma(current);
114
+ return vec4(0.0, seen * FLICKER_MEMORY, seen, 0.0);
115
+ }
116
+
117
+ /** Metres in front of the camera, from a depth sample, as depthOfField.ts reads one. */
118
+ float viewDepthOf(float depth) {
119
+ float z = ${glslSceneDepthToNdc('depth')};
120
+ return -(uDepthToViewZ.x * z + uDepthToViewZ.y) / (uDepthToViewZ.z * z + uDepthToViewZ.w);
121
+ }
122
+
123
+ /** Whether two periods' means are the same, one to zero. */
124
+ float repeats(float mean, float previous) {
125
+ float apart = abs(mean - previous) / max(max(mean, previous), 1.0 / 255.0);
126
+ return 1.0 - smoothstep(FLICKER_REPEAT, 2.0 * FLICKER_REPEAT, apart);
127
+ }
67
128
 
68
129
  /**
69
130
  * The colours actually present around this pixel this frame, as a box.
@@ -155,19 +216,45 @@ void main() {
155
216
  /* Nothing to blend towards: the first frame, a resize, a cut, or the effect switched off. */
156
217
  if (uHistoryBlend <= 0.0) {
157
218
  fragColor = vec4(current, 1.0);
219
+ flickerOut = freshRecord(current);
220
+ flickerMotionOut = vec4(0.0);
158
221
  return;
159
222
  }
160
223
 
224
+ /*
225
+ * **The motion is the nearest surface's among the nine**, the dilation every temporal resolve
226
+ * uses: a pixel at a thin thing samples it in some frames and what is behind it in others, and the
227
+ * two move apart as the camera does, so reprojecting each frame by whichever it caught sends its
228
+ * history and its record somewhere different every frame. A crack of light round a door moves with
229
+ * the door that frames it, and so does its pixel. The depth kept is that surface's too, so the record
230
+ * keeps and next frame tests the depth the reprojection followed.
231
+ */
232
+ vec2 nearestAt = vUv;
161
233
  float depth = textureLod(uDepth, vUv, 0.0).r;
162
- vec4 clip = vec4(vUv * 2.0 - 1.0, ${glslSceneDepthToNdc('depth')}, 1.0);
234
+ float metres = viewDepthOf(depth);
235
+ for (int y = -1; y <= 1; y++) {
236
+ for (int x = -1; x <= 1; x++) {
237
+ vec2 at = vUv + vec2(float(x), float(y)) * uTexel;
238
+ float tap = textureLod(uDepth, at, 0.0).r;
239
+ float tapMetres = viewDepthOf(tap);
240
+ if (tapMetres < metres) {
241
+ metres = tapMetres;
242
+ depth = tap;
243
+ nearestAt = at;
244
+ }
245
+ }
246
+ }
247
+ vec4 clip = vec4(nearestAt * 2.0 - 1.0, ${glslSceneDepthToNdc('depth')}, 1.0);
163
248
  vec4 previous = uReprojection * clip;
164
249
  /* Behind the previous eye: there is no last-frame position to sample. */
165
250
  if (previous.w <= 0.0) {
166
251
  fragColor = vec4(current, 1.0);
252
+ flickerOut = freshRecord(current);
253
+ flickerMotionOut = vec4(metres, 0.0, 0.0, 0.0);
167
254
  return;
168
255
  }
169
256
 
170
- vec2 wasUv = (previous.xy / previous.w) * 0.5 + 0.5;
257
+ vec2 wasUv = vUv + ((previous.xy / previous.w) * 0.5 + 0.5 - nearestAt);
171
258
  /*
172
259
  * **Off the edge of the last frame is a disocclusion**, and the honest answer is this frame's
173
260
  * colour rather than a clamped sample of the border, which would drag one row of pixels inward
@@ -175,6 +262,8 @@ void main() {
175
262
  */
176
263
  if (wasUv.x < 0.0 || wasUv.x > 1.0 || wasUv.y < 0.0 || wasUv.y > 1.0) {
177
264
  fragColor = vec4(current, 1.0);
265
+ flickerOut = freshRecord(current);
266
+ flickerMotionOut = vec4(metres, 0.0, 0.0, 0.0);
178
267
  return;
179
268
  }
180
269
 
@@ -182,8 +271,55 @@ void main() {
182
271
  vec3 hi;
183
272
  neighbourhood(lo, hi);
184
273
  vec3 history = historyCatmullRom(wasUv);
185
- vec3 bounded = clipToNeighbourhood(history, lo, hi);
186
274
 
187
- fragColor = vec4(mix(current, bounded, uHistoryBlend), 1.0);
275
+ /*
276
+ * The anti-flicker, operation for operation the one in temporalAa.ts. The record is read at the
277
+ * texel where the surface was rather than filtered, so two surfaces' records never mix at an
278
+ * edge; it is the surface's own if last frame's depth there, or at one of the texels beside it,
279
+ * is the depth this surface should have had — the nearest of five, because this frame's sample
280
+ * and last frame's can straddle an edge with nothing moved. Otherwise it is no record.
281
+ */
282
+ ivec2 size = textureSize(uFlicker, 0);
283
+ ivec2 was = clamp(ivec2(wasUv * vec2(size)), ivec2(0), size - 1);
284
+ float nearest = 1e9;
285
+ for (int n = 0; n < 5; n++) {
286
+ ivec2 at = was + (n == 1 ? ivec2(1, 0) : n == 2 ? ivec2(-1, 0) : n == 3 ? ivec2(0, 1) : n == 4 ? ivec2(0, -1) : ivec2(0));
287
+ float stood = texelFetch(uFlickerMotion, clamp(at, ivec2(0), size - 1), 0).r;
288
+ if (stood > 0.0) nearest = min(nearest, abs(stood - previous.w));
289
+ }
290
+ /* The swing is the pixel's whatever the test says: resetting it would stop it learning one. */
291
+ vec4 motionThere = texelFetch(uFlickerMotion, was, 0);
292
+ bool missedAll = nearest > 1e8;
293
+ bool same = uAntiFlicker > 0.5 && !missedAll &&
294
+ nearest <= FLICKER_WIDTH * motionThere.g + FLICKER_SURFACE * previous.w;
295
+ float swingOut = missedAll ? 0.0 : motionThere.g + (nearest - motionThere.g) * FLICKER_MEMORY;
296
+ float seen = luma(current);
297
+ vec4 held = same ? texelFetch(uFlicker, was, 0) : freshRecord(current);
298
+
299
+ float still = 1.0 - smoothstep(STILL_FROM, STILL_TO, length((wasUv - vUv) / uTexel));
300
+ float apart = abs(seen - held.b);
301
+ float reach = FLICKER_WIDTH * held.r + 1.0 / 255.0;
302
+ bool within = apart <= reach;
303
+ bool proven = held.a > 0.75 && within;
304
+ float widen = proven ? FLICKER_WIDTH * held.r * still : 0.0;
305
+ float blend = proven ? mix(uHistoryBlend, FLICKER_BLEND, still) : uHistoryBlend;
306
+ vec3 bounded = clipToNeighbourhood(history, lo - widen, hi + widen);
307
+
308
+ float sum = held.g;
309
+ float mean = held.b;
310
+ float repeated = within ? held.a : 0.0;
311
+ if (uPeriodStart > 0.5) {
312
+ repeated = repeats(sum, mean) > 0.5 ? min(1.0, repeated + 0.5) : 0.0;
313
+ mean = sum;
314
+ sum = 0.0;
315
+ }
316
+ flickerOut = vec4(
317
+ held.r + (apart - held.r) * FLICKER_MEMORY,
318
+ sum + seen * FLICKER_MEMORY,
319
+ mean,
320
+ repeated * still
321
+ );
322
+ flickerMotionOut = vec4(metres, swingOut, 0.0, 0.0);
323
+ fragColor = vec4(mix(current, bounded, blend), 1.0);
188
324
  }
189
325
  `;
@@ -154,6 +154,225 @@ export function clipToNeighbourhood(
154
154
  return out;
155
155
  }
156
156
 
157
+ /**
158
+ * **The anti-flicker: a still pixel's neighbourhood widened by its own flicker.**
159
+ *
160
+ * A thread, a crack of light round a door, a leaf finer than a pixel: the jitter catches it in some
161
+ * phases and misses it in others, and in a phase that misses it the nine taps round a pixel can all
162
+ * miss it at once. The box collapses onto the background, the history — which had settled on the
163
+ * thread's share of the pixel — is clipped out, and it grows back a tenth a frame until the next
164
+ * miss. That is a paused frame flashing at every fine edge, and the clip is doing its job: nothing
165
+ * in one frame says the thread is still there.
166
+ *
167
+ * What does say so is the pixel's own record. **A still camera over a still scene samples every pixel
168
+ * the same way every period of the jitter, exactly**, so aliasing repeats itself; a surface that moves
169
+ * across the pixel, a flame, a light that shimmers, does not. So each pixel keeps the mean of its
170
+ * samples over the last period, the sum it is gathering for this one, how many periods running have
171
+ * repeated, and the running mean of how far its samples land from their period's mean — and its box
172
+ * widens by that spread only once two periods running have repeated. Measured on the samples rather
173
+ * than against the history, because the history is what the clip cuts. **And a sample farther from
174
+ * its period's mean than the box would widen to is not the flicker the pixel has shown**: something
175
+ * has arrived, and the count starts again on that frame rather than at the end of the period. **Two, and a mean rather than
176
+ * one sample**, because one sample repeats by coincidence: stripes sliding across a pixel land on the
177
+ * same colour at two period starts about as often as not.
178
+ *
179
+ * **What it gives up.** A surface arriving over a pixel that was flickering is held back until the
180
+ * period ends and fails to repeat — at most eight frames, fading a tenth a frame meanwhile; a still
181
+ * pixel needs three periods before it is helped at all; and a pixel that flickers in a scene that is
182
+ * itself moving is not helped, since nothing repeats there. Nothing moving past a twentieth of a
183
+ * pixel a frame is touched, since the reprojection is then what decides.
184
+ *
185
+ * **A moving camera had a weaker proof of its own, and it was taken out.** It widened a pixel's box
186
+ * while its samples swung both ways about their mean, with the record carried through the
187
+ * reprojection. Measured on a moving courtyard it removed a twentieth of what flickered, and it broke
188
+ * a sun's shadow line on the floor into dashes that were not there before: the record is read at the
189
+ * nearest texel, so under sub-pixel motion one pixel widened and its neighbour did not. The resolve
190
+ * already removes nineteen twentieths of a moving frame's flicker without it. **What would make it wrong** is a jitter
191
+ * whose sequence does not repeat, where no period is the same twice however still the scene is.
192
+ */
193
+
194
+ /** This frame's share of the running spread, and of a period's mean: one period of the jitter. */
195
+ export const FLICKER_MEMORY = 1 / JITTER_PERIOD;
196
+
197
+ /**
198
+ * How many spreads a flickering pixel's box widens by. The hardest thread is one missed in a single
199
+ * phase of eight: that miss needs the box to reach 7/8 of its contrast, and the running spread,
200
+ * worn down by seven catches that each land an eighth of the contrast from the period's mean, has
201
+ * fallen to a = 1/8 + (7/8 a + 7/64 − 1/8)·(7/8)⁷ of the contrast by then — 0.181, a steady state
202
+ * solved by hand — against the 7/32 it averages. Seven eighths over that is 4.8; six leaves room.
203
+ * Threads caught in a share nearer a half spread more and need fewer.
204
+ */
205
+ export const FLICKER_WIDTH = 6;
206
+
207
+ /**
208
+ * How far apart two periods' means may be, relative to the larger, and still repeat: fully below
209
+ * this, not at all at twice it. Above an eight-bit record's rounding and a lamp's slow breathing; far
210
+ * below what anything crossing the pixel does to it.
211
+ */
212
+ export const FLICKER_REPEAT = 0.05;
213
+
214
+ /**
215
+ * How much of the history a pixel proven to repeat keeps, where an unproven one keeps
216
+ * `TEMPORAL_HISTORY_BLEND`. A settled thread still breathes with the jitter by the share of each
217
+ * frame's sample that goes into what is shown — a tenth of its contrast at the base blend — and a
218
+ * pixel that has repeated for two periods has nothing to respond to, so it keeps more: three
219
+ * hundredths of each frame instead of a tenth. What it costs is a surface arriving inside the
220
+ * pixel's own swing, which fades in at that rate until the period's check lets it through.
221
+ */
222
+ export const FLICKER_BLEND = 0.97;
223
+
224
+ /**
225
+ * How far last frame's depth may be from the depth a surface should have had there, relative, and
226
+ * still be that surface: two percent beyond the pixel's own depth swing, taken as the nearest of the
227
+ * texel where it stood and the four beside it. **The nearest of five and not the one**, because this
228
+ * frame's sample and last frame's can straddle an edge with nothing moved — the lesson
229
+ * `recon/resolve.ts` learned the expensive way.
230
+ *
231
+ * **And beyond its own swing, because depth aliases the way colour does.** A crack of light round a
232
+ * door is a pixel whose sample lands on the door in some phases and on what is behind it in others,
233
+ * so its depth jumps metres with nothing moved, and a test held to two percent reset the record of
234
+ * exactly the pixels the anti-flicker is for: a held frame went from 5 flickering pixels back to 774.
235
+ * So each pixel keeps the running mean of how far last frame's depth missed — kept whatever the test
236
+ * says, since resetting it would stop it ever learning the swing — and the test allows
237
+ * `FLICKER_WIDTH` of those, as the box does of the colour's. A flat surface's swing is none and its
238
+ * test stays tight; a surface revealed by the camera is a jump nothing in the pixel's past explains.
239
+ */
240
+ export const FLICKER_SURFACE = 0.02;
241
+
242
+ /** Whether a record follows the surface it was kept for: see `FLICKER_SURFACE`. */
243
+ export function sameSurface(expected: number, stood: ArrayLike<number>, swing: number): boolean {
244
+ return missedBy(expected, stood) <= FLICKER_WIDTH * swing + FLICKER_SURFACE * expected;
245
+ }
246
+
247
+ /**
248
+ * How far the nearest of last frame's depths missed the one expected. A depth of nought is a texel
249
+ * with nothing recorded — the frame after a cut — and is no texel; with none, nothing is missed by
250
+ * any amount.
251
+ */
252
+ export function missedBy(expected: number, stood: ArrayLike<number>): number {
253
+ let nearest = Number.POSITIVE_INFINITY;
254
+ for (let i = 0; i < stood.length; i++) {
255
+ const depth = stood[i] as number;
256
+ if (depth > 0) nearest = Math.min(nearest, Math.abs(depth - expected));
257
+ }
258
+ return nearest;
259
+ }
260
+
261
+ /**
262
+ * The pixel's depth swing carried on: the running mean of `missedBy`, over a period, and none where
263
+ * there was nothing to miss — a swing learned from a cut would loosen the test for a whole period.
264
+ */
265
+ export function nextDepthSwing(swing: number, missed: number): number {
266
+ return Number.isFinite(missed) ? swing + (missed - swing) * FLICKER_MEMORY : 0;
267
+ }
268
+
269
+ /**
270
+ * Reprojected motion in pixels a frame: still below the first, moving from the second. **Small,
271
+ * because repetition breaks long before motion is visible**: a camera drifting a fifth of a pixel a
272
+ * frame shifts the grid more than a pixel a period, so nothing repeats, and a ramp to half a pixel
273
+ * counted that as mostly still and kept vouching for samples that had stopped repeating.
274
+ */
275
+ export const STILL_FROM = 0.01;
276
+ export const STILL_TO = 0.05;
277
+
278
+ /**
279
+ * A pixel's flicker record, in luma: the running spread of its samples about their period's mean,
280
+ * the sum this period has gathered so far, the last period's mean, and how many periods running have
281
+ * repeated — none, one or two, as 0, 0.5 and 1.
282
+ */
283
+ export interface Flicker {
284
+ spread: number;
285
+ sum: number;
286
+ mean: number;
287
+ repeated: number;
288
+ }
289
+
290
+ /**
291
+ * A fresh record for a surface first seen this frame: no spread and nothing repeated. **A
292
+ * record of another surface is no record**, so the resolve starts one over wherever last frame's
293
+ * depth says the surface is not the one it followed, before this frame widens by anything.
294
+ */
295
+ export function freshFlicker(out: Flicker, sample: number): Flicker {
296
+ out.spread = 0;
297
+ out.sum = sample * FLICKER_MEMORY;
298
+ out.mean = sample;
299
+ out.repeated = 0;
300
+ return out;
301
+ }
302
+
303
+ function smoothstep(edge0: number, edge1: number, x: number): number {
304
+ const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));
305
+ return t * t * (3 - 2 * t);
306
+ }
307
+
308
+ /** Rec. 709 luma, which is what the record measures in. */
309
+ export function luma(c: readonly [number, number, number]): number {
310
+ return c[0] * 0.2126 + c[1] * 0.7152 + c[2] * 0.0722;
311
+ }
312
+
313
+ /** One for a pixel whose reprojection did not move, falling to none at a twentieth of a pixel. */
314
+ export function stillness(motionPixels: number): number {
315
+ return 1 - smoothstep(STILL_FROM, STILL_TO, motionPixels);
316
+ }
317
+
318
+ /**
319
+ * Whether a pixel is proven to repeat this frame: two periods running have, and this frame's sample
320
+ * lands within the reach of the box it would widen to. A sample beyond that reach is something
321
+ * arriving, and it disqualifies the frame it lands in as well as the count it carries out.
322
+ */
323
+ export function proven(record: Flicker, sample: number): boolean {
324
+ return record.repeated > 0.75 && within(record, sample);
325
+ }
326
+
327
+ /** Whether this frame's sample lands inside the reach of the box the record would widen to. */
328
+ function within(record: Flicker, sample: number): boolean {
329
+ return Math.abs(sample - record.mean) <= FLICKER_WIDTH * record.spread + 1 / 255;
330
+ }
331
+
332
+ /** How far a pixel's box widens this frame: by its spread, once proven, and only while still. */
333
+ export function flickerWidening(record: Flicker, sample: number, still: number): number {
334
+ return proven(record, sample) ? FLICKER_WIDTH * record.spread * still : 0;
335
+ }
336
+
337
+ /** How much of the history this frame keeps: the base blend, or more once proven. */
338
+ export function flickerBlend(base: number, record: Flicker, sample: number, still: number): number {
339
+ return proven(record, sample) ? base + (FLICKER_BLEND - base) * still : base;
340
+ }
341
+
342
+ /** Whether two periods' means are the same, one to zero. */
343
+ export function repeats(mean: number, previous: number): number {
344
+ const apart = Math.abs(mean - previous) / Math.max(mean, previous, 1 / 255);
345
+ return 1 - smoothstep(FLICKER_REPEAT, 2 * FLICKER_REPEAT, apart);
346
+ }
347
+
348
+ /**
349
+ * The record carried out, from the one carried in and this frame's sample in luma. `periodStart`
350
+ * is the first frame of the jitter's period, where the period just gathered is compared with the
351
+ * one before. Forgotten in proportion to motion — a moving pixel's record would be a record of
352
+ * somewhere else.
353
+ */
354
+ export function nextFlicker(
355
+ out: Flicker,
356
+ previous: Flicker,
357
+ sample: number,
358
+ still: number,
359
+ periodStart: boolean,
360
+ ): Flicker {
361
+ const apart = Math.abs(sample - previous.mean);
362
+ let { sum, mean, repeated } = previous;
363
+ if (apart > FLICKER_WIDTH * previous.spread + 1 / 255) repeated = 0;
364
+ if (periodStart) {
365
+ repeated = repeats(sum, mean) > 0.5 ? Math.min(1, repeated + 0.5) : 0;
366
+ mean = sum;
367
+ sum = 0;
368
+ }
369
+ out.spread = previous.spread + (apart - previous.spread) * FLICKER_MEMORY;
370
+ out.sum = sum + sample * FLICKER_MEMORY;
371
+ out.mean = mean;
372
+ out.repeated = repeated * still;
373
+ return out;
374
+ }
375
+
157
376
  /**
158
377
  * Whether the last frame left a picture this one may sample, and where the jitter has got to.
159
378
  *
@@ -185,6 +404,14 @@ export class TemporalHistory {
185
404
  return usable;
186
405
  }
187
406
 
407
+ /**
408
+ * Whether this frame samples the first phase of the jitter's period, where the anti-flicker
409
+ * compares the period just gathered with the one before. One answer for both backends.
410
+ */
411
+ get periodStart(): boolean {
412
+ return this.frameIndex % JITTER_PERIOD === 0;
413
+ }
414
+
188
415
  /** A resolved picture is now in the history, at the size the frame was opened with. */
189
416
  accumulated(): void {
190
417
  this.picture = true;