@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
@@ -129,6 +129,189 @@ export function clipToNeighbourhood(out, history, min, max) {
129
129
  out[2] = centre[2] + offset[2] * scale;
130
130
  return out;
131
131
  }
132
+ /**
133
+ * **The anti-flicker: a still pixel's neighbourhood widened by its own flicker.**
134
+ *
135
+ * A thread, a crack of light round a door, a leaf finer than a pixel: the jitter catches it in some
136
+ * phases and misses it in others, and in a phase that misses it the nine taps round a pixel can all
137
+ * miss it at once. The box collapses onto the background, the history — which had settled on the
138
+ * thread's share of the pixel — is clipped out, and it grows back a tenth a frame until the next
139
+ * miss. That is a paused frame flashing at every fine edge, and the clip is doing its job: nothing
140
+ * in one frame says the thread is still there.
141
+ *
142
+ * What does say so is the pixel's own record. **A still camera over a still scene samples every pixel
143
+ * the same way every period of the jitter, exactly**, so aliasing repeats itself; a surface that moves
144
+ * across the pixel, a flame, a light that shimmers, does not. So each pixel keeps the mean of its
145
+ * samples over the last period, the sum it is gathering for this one, how many periods running have
146
+ * repeated, and the running mean of how far its samples land from their period's mean — and its box
147
+ * widens by that spread only once two periods running have repeated. Measured on the samples rather
148
+ * than against the history, because the history is what the clip cuts. **And a sample farther from
149
+ * its period's mean than the box would widen to is not the flicker the pixel has shown**: something
150
+ * has arrived, and the count starts again on that frame rather than at the end of the period. **Two, and a mean rather than
151
+ * one sample**, because one sample repeats by coincidence: stripes sliding across a pixel land on the
152
+ * same colour at two period starts about as often as not.
153
+ *
154
+ * **What it gives up.** A surface arriving over a pixel that was flickering is held back until the
155
+ * period ends and fails to repeat — at most eight frames, fading a tenth a frame meanwhile; a still
156
+ * pixel needs three periods before it is helped at all; and a pixel that flickers in a scene that is
157
+ * itself moving is not helped, since nothing repeats there. Nothing moving past a twentieth of a
158
+ * pixel a frame is touched, since the reprojection is then what decides.
159
+ *
160
+ * **A moving camera had a weaker proof of its own, and it was taken out.** It widened a pixel's box
161
+ * while its samples swung both ways about their mean, with the record carried through the
162
+ * reprojection. Measured on a moving courtyard it removed a twentieth of what flickered, and it broke
163
+ * a sun's shadow line on the floor into dashes that were not there before: the record is read at the
164
+ * nearest texel, so under sub-pixel motion one pixel widened and its neighbour did not. The resolve
165
+ * already removes nineteen twentieths of a moving frame's flicker without it. **What would make it wrong** is a jitter
166
+ * whose sequence does not repeat, where no period is the same twice however still the scene is.
167
+ */
168
+ /** This frame's share of the running spread, and of a period's mean: one period of the jitter. */
169
+ export const FLICKER_MEMORY = 1 / JITTER_PERIOD;
170
+ /**
171
+ * How many spreads a flickering pixel's box widens by. The hardest thread is one missed in a single
172
+ * phase of eight: that miss needs the box to reach 7/8 of its contrast, and the running spread,
173
+ * worn down by seven catches that each land an eighth of the contrast from the period's mean, has
174
+ * fallen to a = 1/8 + (7/8 a + 7/64 − 1/8)·(7/8)⁷ of the contrast by then — 0.181, a steady state
175
+ * solved by hand — against the 7/32 it averages. Seven eighths over that is 4.8; six leaves room.
176
+ * Threads caught in a share nearer a half spread more and need fewer.
177
+ */
178
+ export const FLICKER_WIDTH = 6;
179
+ /**
180
+ * How far apart two periods' means may be, relative to the larger, and still repeat: fully below
181
+ * this, not at all at twice it. Above an eight-bit record's rounding and a lamp's slow breathing; far
182
+ * below what anything crossing the pixel does to it.
183
+ */
184
+ export const FLICKER_REPEAT = 0.05;
185
+ /**
186
+ * How much of the history a pixel proven to repeat keeps, where an unproven one keeps
187
+ * `TEMPORAL_HISTORY_BLEND`. A settled thread still breathes with the jitter by the share of each
188
+ * frame's sample that goes into what is shown — a tenth of its contrast at the base blend — and a
189
+ * pixel that has repeated for two periods has nothing to respond to, so it keeps more: three
190
+ * hundredths of each frame instead of a tenth. What it costs is a surface arriving inside the
191
+ * pixel's own swing, which fades in at that rate until the period's check lets it through.
192
+ */
193
+ export const FLICKER_BLEND = 0.97;
194
+ /**
195
+ * How far last frame's depth may be from the depth a surface should have had there, relative, and
196
+ * still be that surface: two percent beyond the pixel's own depth swing, taken as the nearest of the
197
+ * texel where it stood and the four beside it. **The nearest of five and not the one**, because this
198
+ * frame's sample and last frame's can straddle an edge with nothing moved — the lesson
199
+ * `recon/resolve.ts` learned the expensive way.
200
+ *
201
+ * **And beyond its own swing, because depth aliases the way colour does.** A crack of light round a
202
+ * door is a pixel whose sample lands on the door in some phases and on what is behind it in others,
203
+ * so its depth jumps metres with nothing moved, and a test held to two percent reset the record of
204
+ * exactly the pixels the anti-flicker is for: a held frame went from 5 flickering pixels back to 774.
205
+ * So each pixel keeps the running mean of how far last frame's depth missed — kept whatever the test
206
+ * says, since resetting it would stop it ever learning the swing — and the test allows
207
+ * `FLICKER_WIDTH` of those, as the box does of the colour's. A flat surface's swing is none and its
208
+ * test stays tight; a surface revealed by the camera is a jump nothing in the pixel's past explains.
209
+ */
210
+ export const FLICKER_SURFACE = 0.02;
211
+ /** Whether a record follows the surface it was kept for: see `FLICKER_SURFACE`. */
212
+ export function sameSurface(expected, stood, swing) {
213
+ return missedBy(expected, stood) <= FLICKER_WIDTH * swing + FLICKER_SURFACE * expected;
214
+ }
215
+ /**
216
+ * How far the nearest of last frame's depths missed the one expected. A depth of nought is a texel
217
+ * with nothing recorded — the frame after a cut — and is no texel; with none, nothing is missed by
218
+ * any amount.
219
+ */
220
+ export function missedBy(expected, stood) {
221
+ let nearest = Number.POSITIVE_INFINITY;
222
+ for (let i = 0; i < stood.length; i++) {
223
+ const depth = stood[i];
224
+ if (depth > 0)
225
+ nearest = Math.min(nearest, Math.abs(depth - expected));
226
+ }
227
+ return nearest;
228
+ }
229
+ /**
230
+ * The pixel's depth swing carried on: the running mean of `missedBy`, over a period, and none where
231
+ * there was nothing to miss — a swing learned from a cut would loosen the test for a whole period.
232
+ */
233
+ export function nextDepthSwing(swing, missed) {
234
+ return Number.isFinite(missed) ? swing + (missed - swing) * FLICKER_MEMORY : 0;
235
+ }
236
+ /**
237
+ * Reprojected motion in pixels a frame: still below the first, moving from the second. **Small,
238
+ * because repetition breaks long before motion is visible**: a camera drifting a fifth of a pixel a
239
+ * frame shifts the grid more than a pixel a period, so nothing repeats, and a ramp to half a pixel
240
+ * counted that as mostly still and kept vouching for samples that had stopped repeating.
241
+ */
242
+ export const STILL_FROM = 0.01;
243
+ export const STILL_TO = 0.05;
244
+ /**
245
+ * A fresh record for a surface first seen this frame: no spread and nothing repeated. **A
246
+ * record of another surface is no record**, so the resolve starts one over wherever last frame's
247
+ * depth says the surface is not the one it followed, before this frame widens by anything.
248
+ */
249
+ export function freshFlicker(out, sample) {
250
+ out.spread = 0;
251
+ out.sum = sample * FLICKER_MEMORY;
252
+ out.mean = sample;
253
+ out.repeated = 0;
254
+ return out;
255
+ }
256
+ function smoothstep(edge0, edge1, x) {
257
+ const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));
258
+ return t * t * (3 - 2 * t);
259
+ }
260
+ /** Rec. 709 luma, which is what the record measures in. */
261
+ export function luma(c) {
262
+ return c[0] * 0.2126 + c[1] * 0.7152 + c[2] * 0.0722;
263
+ }
264
+ /** One for a pixel whose reprojection did not move, falling to none at a twentieth of a pixel. */
265
+ export function stillness(motionPixels) {
266
+ return 1 - smoothstep(STILL_FROM, STILL_TO, motionPixels);
267
+ }
268
+ /**
269
+ * Whether a pixel is proven to repeat this frame: two periods running have, and this frame's sample
270
+ * lands within the reach of the box it would widen to. A sample beyond that reach is something
271
+ * arriving, and it disqualifies the frame it lands in as well as the count it carries out.
272
+ */
273
+ export function proven(record, sample) {
274
+ return record.repeated > 0.75 && within(record, sample);
275
+ }
276
+ /** Whether this frame's sample lands inside the reach of the box the record would widen to. */
277
+ function within(record, sample) {
278
+ return Math.abs(sample - record.mean) <= FLICKER_WIDTH * record.spread + 1 / 255;
279
+ }
280
+ /** How far a pixel's box widens this frame: by its spread, once proven, and only while still. */
281
+ export function flickerWidening(record, sample, still) {
282
+ return proven(record, sample) ? FLICKER_WIDTH * record.spread * still : 0;
283
+ }
284
+ /** How much of the history this frame keeps: the base blend, or more once proven. */
285
+ export function flickerBlend(base, record, sample, still) {
286
+ return proven(record, sample) ? base + (FLICKER_BLEND - base) * still : base;
287
+ }
288
+ /** Whether two periods' means are the same, one to zero. */
289
+ export function repeats(mean, previous) {
290
+ const apart = Math.abs(mean - previous) / Math.max(mean, previous, 1 / 255);
291
+ return 1 - smoothstep(FLICKER_REPEAT, 2 * FLICKER_REPEAT, apart);
292
+ }
293
+ /**
294
+ * The record carried out, from the one carried in and this frame's sample in luma. `periodStart`
295
+ * is the first frame of the jitter's period, where the period just gathered is compared with the
296
+ * one before. Forgotten in proportion to motion — a moving pixel's record would be a record of
297
+ * somewhere else.
298
+ */
299
+ export function nextFlicker(out, previous, sample, still, periodStart) {
300
+ const apart = Math.abs(sample - previous.mean);
301
+ let { sum, mean, repeated } = previous;
302
+ if (apart > FLICKER_WIDTH * previous.spread + 1 / 255)
303
+ repeated = 0;
304
+ if (periodStart) {
305
+ repeated = repeats(sum, mean) > 0.5 ? Math.min(1, repeated + 0.5) : 0;
306
+ mean = sum;
307
+ sum = 0;
308
+ }
309
+ out.spread = previous.spread + (apart - previous.spread) * FLICKER_MEMORY;
310
+ out.sum = sum + sample * FLICKER_MEMORY;
311
+ out.mean = mean;
312
+ out.repeated = repeated * still;
313
+ return out;
314
+ }
132
315
  /**
133
316
  * Whether the last frame left a picture this one may sample, and where the jitter has got to.
134
317
  *
@@ -158,6 +341,13 @@ export class TemporalHistory {
158
341
  this.height = height;
159
342
  return usable;
160
343
  }
344
+ /**
345
+ * Whether this frame samples the first phase of the jitter's period, where the anti-flicker
346
+ * compares the period just gathered with the one before. One answer for both backends.
347
+ */
348
+ get periodStart() {
349
+ return this.frameIndex % JITTER_PERIOD === 0;
350
+ }
161
351
  /** A resolved picture is now in the history, at the size the frame was opened with. */
162
352
  accumulated() {
163
353
  this.picture = true;
@@ -1,16 +1,3 @@
1
- /**
2
- * The temporal resolve on WebGL2: this frame blended into where the last one was.
3
- *
4
- * **Two targets and a swap, because a pass cannot read the texture it is writing.** The history is
5
- * sampled at a reprojected coordinate, which is somewhere else in the same image, so reading and
6
- * writing one texture is a feedback loop — undefined in WebGL2 and in practice whatever the driver
7
- * left in the cache. So the resolve writes into the one it did not read and the pair swaps, which
8
- * costs one more full-size texture and nothing per pixel.
9
- *
10
- * **What it returns is what the composite must then treat as the scene.** Bloom, occlusion and the
11
- * grade all read the scene texture, and reading the unresolved one would grade a stable picture
12
- * from an unstable source and put the crawl back on screen through the bloom.
13
- */
14
1
  export declare class TemporalPass {
15
2
  private readonly gl;
16
3
  /**
@@ -27,6 +14,21 @@ export declare class TemporalPass {
27
14
  private readonly vao;
28
15
  /** The pair, and which of them the next resolve writes into. */
29
16
  private textures;
17
+ /**
18
+ * Each pixel's flicker record, a pair beside the history's and swapped with it: see the
19
+ * anti-flicker in `temporalAa.ts`. In the history's own format, which holds what the record does —
20
+ * a change and a sample are colours' luma, and eight bits hold them wherever eight bits hold the
21
+ * colour.
22
+ */
23
+ private records;
24
+ /** The rest of each pixel's record — depth, pull and recent swing — beside the first. */
25
+ private motions;
26
+ /**
27
+ * Whether the records can be half float, which the depth in them needs. Asked of the context
28
+ * rather than taken from the scene's format, since a record of an eight-bit scene still holds
29
+ * metres. Without it the anti-flicker is off and the resolve is the one before it existed.
30
+ */
31
+ private readonly recordFloat;
30
32
  private framebuffers;
31
33
  private write;
32
34
  private width;
@@ -49,6 +51,6 @@ export declare class TemporalPass {
49
51
  * first frame, the frame after a resize and the frame after a cut pass zero, and the shader
50
52
  * then returns this frame before it touches depth or history at all.
51
53
  */
52
- temporalResolve(scene: WebGLTexture, depth: WebGLTexture, reprojection: Float32Array, width: number, height: number, blend: number): WebGLTexture;
54
+ temporalResolve(scene: WebGLTexture, depth: WebGLTexture, reprojection: Float32Array, width: number, height: number, blend: number, periodStart: boolean, depthToViewZ: Float32Array): WebGLTexture;
53
55
  dispose(): void;
54
56
  }
@@ -14,6 +14,8 @@ import { TEMPORAL_RESOLVE_FRAG } from './shaders/temporalResolve.js';
14
14
  * grade all read the scene texture, and reading the unresolved one would grade a stable picture
15
15
  * from an unstable source and put the crawl back on screen through the bloom.
16
16
  */
17
+ /** The resolve's three outputs: the picture and the two halves of the flicker record. */
18
+ const DRAW_ALL = [0x8ce0, 0x8ce1, 0x8ce2]; // COLOR_ATTACHMENT0, 1, 2
17
19
  export class TemporalPass {
18
20
  gl;
19
21
  floatColor;
@@ -22,6 +24,21 @@ export class TemporalPass {
22
24
  vao;
23
25
  /** The pair, and which of them the next resolve writes into. */
24
26
  textures = [null, null];
27
+ /**
28
+ * Each pixel's flicker record, a pair beside the history's and swapped with it: see the
29
+ * anti-flicker in `temporalAa.ts`. In the history's own format, which holds what the record does —
30
+ * a change and a sample are colours' luma, and eight bits hold them wherever eight bits hold the
31
+ * colour.
32
+ */
33
+ records = [null, null];
34
+ /** The rest of each pixel's record — depth, pull and recent swing — beside the first. */
35
+ motions = [null, null];
36
+ /**
37
+ * Whether the records can be half float, which the depth in them needs. Asked of the context
38
+ * rather than taken from the scene's format, since a record of an eight-bit scene still holds
39
+ * metres. Without it the anti-flicker is off and the resolve is the one before it existed.
40
+ */
41
+ recordFloat;
25
42
  framebuffers = [null, null];
26
43
  write = 0;
27
44
  width = 0;
@@ -38,6 +55,7 @@ export class TemporalPass {
38
55
  floatColor) {
39
56
  this.gl = gl;
40
57
  this.floatColor = floatColor;
58
+ this.recordFloat = gl.getExtension('EXT_color_buffer_float') !== null;
41
59
  this.program = compileProgram(gl, FULLSCREEN_VERT, TEMPORAL_RESOLVE_FRAG, 'temporalResolve');
42
60
  this.uniforms = uniformLocations(gl, this.program, 'temporalPass');
43
61
  const vao = gl.createVertexArray();
@@ -54,24 +72,36 @@ export class TemporalPass {
54
72
  for (let i = 0; i < 2; i++) {
55
73
  if (this.textures[i] === null)
56
74
  this.textures[i] = gl.createTexture();
57
- gl.bindTexture(gl.TEXTURE_2D, this.textures[i]);
58
- if (this.floatColor) {
59
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.HALF_FLOAT, null);
60
- }
61
- else {
62
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
75
+ if (this.records[i] === null)
76
+ this.records[i] = gl.createTexture();
77
+ if (this.motions[i] === null)
78
+ this.motions[i] = gl.createTexture();
79
+ for (const texture of [this.textures[i], this.records[i], this.motions[i]]) {
80
+ gl.bindTexture(gl.TEXTURE_2D, texture);
81
+ if (texture === this.textures[i] ? this.floatColor : this.recordFloat) {
82
+ /* The depth record is two numbers a pixel: metres, and how far they swing. */
83
+ const pair = texture === this.motions[i];
84
+ gl.texImage2D(gl.TEXTURE_2D, 0, pair ? gl.RG16F : gl.RGBA16F, width, height, 0, pair ? gl.RG : gl.RGBA, gl.HALF_FLOAT, null);
85
+ }
86
+ else {
87
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
88
+ }
89
+ /* Linear, because the history is sampled between texels wherever the reprojection lands;
90
+ clamped, because a sample that wrapped would pull the far edge of the screen into the
91
+ corner — and the shader rejects an off-screen reprojection before it can anyway. */
92
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
93
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
94
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
95
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
63
96
  }
64
- /* Linear, because the history is sampled between texels wherever the reprojection lands;
65
- clamped, because a sample that wrapped would pull the far edge of the screen into the
66
- corner — and the shader rejects an off-screen reprojection before it can anyway. */
67
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
68
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
69
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
70
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
71
97
  if (this.framebuffers[i] === null)
72
98
  this.framebuffers[i] = gl.createFramebuffer();
73
99
  gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffers[i]);
74
100
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.textures[i], 0);
101
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, this.records[i], 0);
102
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, this.motions[i], 0);
103
+ /* Per framebuffer, so set once here: the resolve writes the picture and the record. */
104
+ gl.drawBuffers(DRAW_ALL);
75
105
  }
76
106
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
77
107
  }
@@ -82,7 +112,7 @@ export class TemporalPass {
82
112
  * first frame, the frame after a resize and the frame after a cut pass zero, and the shader
83
113
  * then returns this frame before it touches depth or history at all.
84
114
  */
85
- temporalResolve(scene, depth, reprojection, width, height, blend) {
115
+ temporalResolve(scene, depth, reprojection, width, height, blend, periodStart, depthToViewZ) {
86
116
  const { gl } = this;
87
117
  this.ensureSize(width, height);
88
118
  const target = this.write;
@@ -103,9 +133,18 @@ export class TemporalPass {
103
133
  gl.activeTexture(gl.TEXTURE2);
104
134
  gl.bindTexture(gl.TEXTURE_2D, depth);
105
135
  gl.uniform1i(this.uniforms['uDepth'] ?? null, 2);
136
+ gl.activeTexture(gl.TEXTURE3);
137
+ gl.bindTexture(gl.TEXTURE_2D, this.records[history]);
138
+ gl.uniform1i(this.uniforms['uFlicker'] ?? null, 3);
139
+ gl.activeTexture(gl.TEXTURE4);
140
+ gl.bindTexture(gl.TEXTURE_2D, this.motions[history]);
141
+ gl.uniform1i(this.uniforms['uFlickerMotion'] ?? null, 4);
142
+ gl.uniform4fv(this.uniforms['uDepthToViewZ'] ?? null, depthToViewZ);
143
+ gl.uniform1f(this.uniforms['uAntiFlicker'] ?? null, this.recordFloat ? 1 : 0);
106
144
  gl.uniformMatrix4fv(this.uniforms['uReprojection'] ?? null, false, reprojection);
107
145
  gl.uniform2f(this.uniforms['uTexel'] ?? null, 1 / width, 1 / height);
108
146
  gl.uniform1f(this.uniforms['uHistoryBlend'] ?? null, blend);
147
+ gl.uniform1f(this.uniforms['uPeriodStart'] ?? null, periodStart ? 1 : 0);
109
148
  gl.bindVertexArray(this.vao);
110
149
  gl.drawArrays(gl.TRIANGLES, 0, 3);
111
150
  gl.bindVertexArray(null);
@@ -122,9 +161,15 @@ export class TemporalPass {
122
161
  for (let i = 0; i < 2; i++) {
123
162
  if (this.textures[i] !== null)
124
163
  gl.deleteTexture(this.textures[i]);
164
+ if (this.records[i] !== null)
165
+ gl.deleteTexture(this.records[i]);
166
+ if (this.motions[i] !== null)
167
+ gl.deleteTexture(this.motions[i]);
125
168
  if (this.framebuffers[i] !== null)
126
169
  gl.deleteFramebuffer(this.framebuffers[i]);
127
170
  this.textures[i] = null;
171
+ this.records[i] = null;
172
+ this.motions[i] = null;
128
173
  this.framebuffers[i] = null;
129
174
  }
130
175
  gl.deleteProgram(this.program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/core",
3
- "version": "4.4.1",
3
+ "version": "4.4.2",
4
4
  "description": "DriftEngine reusable browser 3D runtime",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,8 +29,8 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
- "@driftengine/drft": "4.4.1",
33
- "@driftengine/physics": "4.4.1",
32
+ "@driftengine/drft": "4.4.2",
33
+ "@driftengine/physics": "4.4.2",
34
34
  "gl-matrix": "^3.4.3"
35
35
  },
36
36
  "author": "Drift Technologies",
@@ -63,8 +63,8 @@
63
63
  "prepack": "node ../../scripts/build.mjs @driftengine/core"
64
64
  },
65
65
  "devDependencies": {
66
- "@driftengine/nav": "4.4.1",
67
- "@driftengine/network": "4.4.1",
68
- "@driftengine/texture": "4.4.1"
66
+ "@driftengine/nav": "4.4.2",
67
+ "@driftengine/network": "4.4.2",
68
+ "@driftengine/texture": "4.4.2"
69
69
  }
70
70
  }
@@ -1263,6 +1263,9 @@ export class WebGL2Renderer implements RendererApi {
1263
1263
  * Scratch, filled in `endFrame`, which may not allocate.
1264
1264
  */
1265
1265
  private frameProjection: ReadonlyMat4 | null = null;
1266
+ /** The temporal resolve's depth-to-metres row, from the inverse of `frameProjection`. */
1267
+ private readonly taaInvProjection = mat4.create();
1268
+ private readonly taaDepthToViewZ = Float32Array.of(0, 0, 0, 1);
1266
1269
  private readonly aoInvProjection = mat4.create();
1267
1270
  private readonly aoProjScale = new Float32Array(2);
1268
1271
  /**
@@ -6193,7 +6196,14 @@ export class WebGL2Renderer implements RendererApi {
6193
6196
  const view = this.frameViewProj;
6194
6197
  const blur = this.quality.cameraMotionBlur * this.motionBlurScale;
6195
6198
  let motion: { reprojection: Float32Array; strength: number; max: number } | undefined;
6196
- let temporal: { reprojection: Float32Array; blend: number } | undefined;
6199
+ let temporal:
6200
+ | {
6201
+ reprojection: Float32Array;
6202
+ blend: number;
6203
+ periodStart: boolean;
6204
+ depthToViewZ: Float32Array;
6205
+ }
6206
+ | undefined;
6197
6207
  /*
6198
6208
  * **Both effects want the same one matrix**, so it is built once when either asks. The
6199
6209
  * reprojection was previously kept only while the blur was on, which is why this reads as it
@@ -6222,9 +6232,19 @@ export class WebGL2Renderer implements RendererApi {
6222
6232
  * its early exit on it — so this is present every jittered frame rather than absent on some,
6223
6233
  * and the pass still runs and fills the history for the next one.
6224
6234
  */
6235
+ /* The anti-flicker's same-surface test reads depths in metres, as the occlusion does. */
6236
+ if (this.frameProjection !== null) {
6237
+ mat4.invert(this.taaInvProjection, this.frameProjection);
6238
+ this.taaDepthToViewZ[0] = this.taaInvProjection[10] ?? 0;
6239
+ this.taaDepthToViewZ[1] = this.taaInvProjection[14] ?? 0;
6240
+ this.taaDepthToViewZ[2] = this.taaInvProjection[11] ?? 0;
6241
+ this.taaDepthToViewZ[3] = this.taaInvProjection[15] ?? 1;
6242
+ }
6225
6243
  temporal = {
6226
6244
  reprojection: this.reprojection as Float32Array,
6227
6245
  blend: this.temporalHistoryUsable ? TEMPORAL_HISTORY_BLEND : 0,
6246
+ periodStart: this.temporalHistory.periodStart,
6247
+ depthToViewZ: this.taaDepthToViewZ,
6228
6248
  };
6229
6249
  }
6230
6250
 
@@ -331,7 +331,37 @@ export function postPipeline(
331
331
  format: GPUTextureFormat,
332
332
  blend?: GPUBlendState,
333
333
  ): GPURenderPipeline {
334
- return cache.get(key, () => ({
334
+ return cache.get(key, () =>
335
+ describePost(device, layout, key, fragment, [
336
+ blend === undefined ? { format } : { format, blend },
337
+ ]),
338
+ );
339
+ }
340
+
341
+ /**
342
+ * The same over several colour targets, in the fragment's output order: the temporal resolve
343
+ * writes its picture and each pixel's flicker record in one pass. `targets` is the caller's, held
344
+ * rather than built per call, since this is asked for every frame.
345
+ */
346
+ export function postPipelineTargets(
347
+ cache: PipelineCache,
348
+ device: GPUDevice,
349
+ layout: GPUBindGroupLayout,
350
+ key: string,
351
+ fragment: string,
352
+ targets: GPUColorTargetState[],
353
+ ): GPURenderPipeline {
354
+ return cache.get(key, () => describePost(device, layout, key, fragment, targets));
355
+ }
356
+
357
+ function describePost(
358
+ device: GPUDevice,
359
+ layout: GPUBindGroupLayout,
360
+ key: string,
361
+ fragment: string,
362
+ targets: GPUColorTargetState[],
363
+ ): GPURenderPipelineDescriptor {
364
+ return {
335
365
  label: key,
336
366
  layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
337
367
  vertex: {
@@ -341,7 +371,7 @@ export function postPipeline(
341
371
  fragment: {
342
372
  module: shaderModule(device, { label: key, code: fragment }),
343
373
  entryPoint: 'main',
344
- targets: [blend === undefined ? { format } : { format, blend }],
374
+ targets,
345
375
  },
346
376
  /*
347
377
  * No depth at all, which is what makes this different from every other pipeline here: the
@@ -349,11 +379,11 @@ export function postPipeline(
349
379
  * disables the test, the write and culling by hand for the same draw.
350
380
  */
351
381
  primitive: { topology: 'triangle-list' as const, cullMode: 'none' as const },
352
- }));
382
+ };
353
383
  }
354
384
 
355
385
  /**
356
- * The temporal resolve's block and its three inputs.
386
+ * The temporal resolve's block and its four inputs.
357
387
  *
358
388
  * Named like the occlusion's beside it, and for the same reason `BLOOM_DOWNSAMPLE_FIELDS` is: a
359
389
  * block read by assumption rather than by name is a uniform written to the wrong offset, which
@@ -190,6 +190,7 @@ import {
190
190
  createPostStageLayout,
191
191
  depthResolvePipeline,
192
192
  postPipeline,
193
+ postPipelineTargets,
193
194
  sceneColorFormat,
194
195
  AO_BLUR_FIELDS,
195
196
  AO_BLUR_FRAG_WGSL,
@@ -1926,6 +1927,16 @@ export class WebGPURenderer implements RendererApi {
1926
1927
  private taaUniforms!: GPUBuffer;
1927
1928
  private taaTextures: (GPUTexture | null)[] = [null, null];
1928
1929
  private taaViews: (GPUTextureView | null)[] = [null, null];
1930
+ /** Each pixel's flicker record, paired with the history and swapped with it: see `temporalAa.ts`. */
1931
+ private taaRecords: (GPUTexture | null)[] = [null, null];
1932
+ private taaRecordViews: (GPUTextureView | null)[] = [null, null];
1933
+ /** The rest of each pixel's record — depth, pull and recent swing — beside the first. */
1934
+ private taaMotions: (GPUTexture | null)[] = [null, null];
1935
+ private taaMotionViews: (GPUTextureView | null)[] = [null, null];
1936
+ /** The resolve's depth-to-metres row, from the inverse of `frameProjection`. */
1937
+ private readonly taaInvProjection = new Float32Array(16);
1938
+ /** The resolve's two targets, the picture and the record, held for the pipeline cache's sake. */
1939
+ private taaTargets: GPUColorTargetState[] = [];
1929
1940
  /** Group `i` reads history `i` and the pass writes into the other one. */
1930
1941
  private taaBindGroups: (GPUBindGroup | null)[] = [null, null];
1931
1942
  private taaWrite = 0;
@@ -6395,6 +6406,8 @@ export class WebGPURenderer implements RendererApi {
6395
6406
  this.taaLayout = createPostStageLayout(device, TAA_UNIFORMS, TAA_FRAG_SIZE, [
6396
6407
  { binding: TAA_TEXTURES.uScene },
6397
6408
  { binding: TAA_TEXTURES.uHistory },
6409
+ { binding: TAA_TEXTURES.uFlicker },
6410
+ { binding: TAA_TEXTURES.uFlickerMotion },
6398
6411
  { binding: TAA_TEXTURES.uDepth, filterable: false },
6399
6412
  ]);
6400
6413
  this.taaUniforms = device.createBuffer({
@@ -10468,8 +10481,10 @@ export class WebGPURenderer implements RendererApi {
10468
10481
  if (!this.temporalJittering) return;
10469
10482
  const write = this.taaWrite;
10470
10483
  const target = this.taaViews[write];
10484
+ const record = this.taaRecordViews[write];
10485
+ const motion = this.taaMotionViews[write];
10471
10486
  const group = this.taaBindGroups[1 - write];
10472
- if (target === null || group === null) return;
10487
+ if (target === null || record === null || motion === null || group === null) return;
10473
10488
  const { device } = this.surface;
10474
10489
 
10475
10490
  const f = this.taaFloats;
@@ -10478,6 +10493,22 @@ export class WebGPURenderer implements RendererApi {
10478
10493
  f[at('uTexel')] = 1 / this.renderWidth;
10479
10494
  f[at('uTexel') + 1] = 1 / this.renderHeight;
10480
10495
  f[at('uHistoryBlend')] = this.temporalHistoryUsable ? TEMPORAL_HISTORY_BLEND : 0;
10496
+ f[at('uPeriodStart')] = this.temporalHistory.periodStart ? 1 : 0;
10497
+ /* The anti-flicker's same-surface test reads depths in metres, as the occlusion does. */
10498
+ const depthRow = at('uDepthToViewZ');
10499
+ if (this.frameProjection !== null) {
10500
+ mat4.invert(this.taaInvProjection, this.frameProjection);
10501
+ f[depthRow] = this.taaInvProjection[10] ?? 0;
10502
+ f[depthRow + 1] = this.taaInvProjection[14] ?? 0;
10503
+ f[depthRow + 2] = this.taaInvProjection[11] ?? 0;
10504
+ f[depthRow + 3] = this.taaInvProjection[15] ?? 1;
10505
+ } else {
10506
+ f[depthRow] = 0;
10507
+ f[depthRow + 1] = 0;
10508
+ f[depthRow + 2] = 0;
10509
+ f[depthRow + 3] = 1;
10510
+ }
10511
+ f[at('uAntiFlicker')] = 1;
10481
10512
  device.queue.writeBuffer(this.taaUniforms, 0, this.taaStaging);
10482
10513
 
10483
10514
  const pass = encoder.beginRenderPass({
@@ -10485,16 +10516,18 @@ export class WebGPURenderer implements RendererApi {
10485
10516
  timestampWrites: this.gpuTimer.writesFor(),
10486
10517
  colorAttachments: [
10487
10518
  { view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 1] },
10519
+ { view: record, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
10520
+ { view: motion, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
10488
10521
  ],
10489
10522
  });
10490
10523
  pass.setPipeline(
10491
- postPipeline(
10524
+ postPipelineTargets(
10492
10525
  this.pipelines,
10493
10526
  device,
10494
10527
  this.taaLayout,
10495
10528
  'post.taa',
10496
10529
  TEMPORAL_RESOLVE_FRAG_WGSL,
10497
- sceneColorFormat(this.quality.hdrScene),
10530
+ this.taaTargets,
10498
10531
  ),
10499
10532
  );
10500
10533
  pass.setBindGroup(0, group, [0]);
@@ -14551,7 +14584,28 @@ export class WebGPURenderer implements RendererApi {
14551
14584
  });
14552
14585
  this.taaTextures[i] = texture;
14553
14586
  this.taaViews[i] = texture.createView();
14587
+ /* The records in half float, whatever the picture's format: one of them holds metres. */
14588
+ this.taaRecords[i]?.destroy();
14589
+ const record = device.createTexture({
14590
+ label: `post.taaRecord${i}`,
14591
+ size: [width, height],
14592
+ format: 'rgba16float',
14593
+ usage: USAGE,
14594
+ });
14595
+ this.taaRecords[i] = record;
14596
+ this.taaRecordViews[i] = record.createView();
14597
+ this.taaMotions[i]?.destroy();
14598
+ /* Two numbers a pixel: the depth its sample stood at, in metres, and how far that swings. */
14599
+ const motion = device.createTexture({
14600
+ label: `post.taaMotion${i}`,
14601
+ size: [width, height],
14602
+ format: 'rg16float',
14603
+ usage: USAGE,
14604
+ });
14605
+ this.taaMotions[i] = motion;
14606
+ this.taaMotionViews[i] = motion.createView();
14554
14607
  }
14608
+ this.taaTargets = [{ format }, { format: 'rgba16float' }, { format: 'rg16float' }];
14555
14609
  this.taaWrite = 0;
14556
14610
  /* A resized target is a new texture, so whatever the history held is gone. */
14557
14611
  this.temporalHistory.invalidate();
@@ -14747,7 +14801,17 @@ export class WebGPURenderer implements RendererApi {
14747
14801
  const scene = this.sceneColorView;
14748
14802
  const history = this.taaViews[i];
14749
14803
  const resolved = this.taaViews[1 - i];
14750
- if (scene === null || history === null || resolved === null) continue;
14804
+ const record = this.taaRecordViews[i];
14805
+ const motion = this.taaMotionViews[i];
14806
+ if (
14807
+ scene === null ||
14808
+ history === null ||
14809
+ resolved === null ||
14810
+ record === null ||
14811
+ motion === null
14812
+ ) {
14813
+ continue;
14814
+ }
14751
14815
  this.taaBindGroups[i] = device.createBindGroup({
14752
14816
  label: `post.taaBindGroup${i}`,
14753
14817
  layout: this.taaLayout,
@@ -14755,6 +14819,8 @@ export class WebGPURenderer implements RendererApi {
14755
14819
  { binding: TAA_UNIFORMS, resource: { buffer: this.taaUniforms, size: TAA_FRAG_SIZE } },
14756
14820
  ...entry(TAA_TEXTURES.uScene, scene),
14757
14821
  ...entry(TAA_TEXTURES.uHistory, history),
14822
+ ...entry(TAA_TEXTURES.uFlicker, record),
14823
+ ...entry(TAA_TEXTURES.uFlickerMotion, motion),
14758
14824
  ...entry(TAA_TEXTURES.uDepth, this.resolvedDepthView, true),
14759
14825
  ],
14760
14826
  });