@driftengine/core 4.4.0 → 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 (49) hide show
  1. package/CHANGELOG.json +47 -0
  2. package/README.md +1 -1
  3. package/dist/render/ambientOcclusionPass.js +5 -1
  4. package/dist/render/backend/webgl2/renderer.d.ts +3 -0
  5. package/dist/render/backend/webgl2/renderer.js +13 -0
  6. package/dist/render/backend/webgpu/postPass.d.ts +20 -4
  7. package/dist/render/backend/webgpu/postPass.js +17 -4
  8. package/dist/render/backend/webgpu/renderer.d.ts +10 -0
  9. package/dist/render/backend/webgpu/renderer.js +74 -7
  10. package/dist/render/flameLight.js +15 -2
  11. package/dist/render/pointShadowBudget.d.ts +12 -2
  12. package/dist/render/pointShadowBudget.js +67 -51
  13. package/dist/render/recon/disocclusion.d.ts +36 -3
  14. package/dist/render/recon/disocclusion.js +56 -4
  15. package/dist/render/recon/resolve.js +95 -12
  16. package/dist/render/sceneTarget.d.ts +2 -0
  17. package/dist/render/sceneTarget.js +1 -1
  18. package/dist/render/shaders/ambientOcclusion.d.ts +39 -1
  19. package/dist/render/shaders/ambientOcclusion.js +69 -9
  20. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +8 -3
  21. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +8 -3
  22. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +31 -6
  23. package/dist/render/shaders/generated/temporalResolve.wgsl.js +31 -6
  24. package/dist/render/shaders/recon/resolve.wgsl.d.ts +1 -1
  25. package/dist/render/shaders/recon/resolve.wgsl.js +112 -8
  26. package/dist/render/shaders/temporalResolve.js +133 -5
  27. package/dist/render/temporalAa.d.ts +145 -0
  28. package/dist/render/temporalAa.js +190 -0
  29. package/dist/render/temporalPass.d.ts +16 -14
  30. package/dist/render/temporalPass.js +59 -14
  31. package/package.json +6 -6
  32. package/src/render/ambientOcclusionPass.ts +5 -1
  33. package/src/render/backend/webgl2/renderer.ts +21 -1
  34. package/src/render/backend/webgpu/postPass.ts +34 -4
  35. package/src/render/backend/webgpu/renderer.ts +77 -7
  36. package/src/render/flameLight.ts +15 -2
  37. package/src/render/pointShadowBudget.ts +65 -47
  38. package/src/render/recon/disocclusion.ts +54 -3
  39. package/src/render/recon/resolve.ts +137 -10
  40. package/src/render/sceneTarget.ts +8 -1
  41. package/src/render/shaders/ambientOcclusion.ts +81 -9
  42. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +8 -3
  43. package/src/render/shaders/generated/temporalResolve.wgsl.ts +31 -6
  44. package/src/render/shaders/recon/resolve.wgsl.ts +112 -8
  45. package/src/render/shaders/temporalResolve.ts +141 -5
  46. package/src/render/temporalAa.ts +227 -0
  47. package/src/render/temporalPass.ts +88 -12
  48. package/dist/render/shaders/agx.d.ts +0 -31
  49. package/dist/render/shaders/agx.js +0 -81
package/CHANGELOG.json CHANGED
@@ -1,4 +1,51 @@
1
1
  [
2
+ {
3
+ "version": "4.4.2",
4
+ "date": "2026-09-28",
5
+ "entries": [
6
+ {
7
+ "kind": "fixed",
8
+ "short": "A reconstructed frame no longer flickers or softens along the edges of still objects: the far side of every silhouette keeps its history.",
9
+ "text": "**The disocclusion test compared two different surfaces at every edge.** The reconstruction reprojects each output pixel by the nearest of its nine samples, the usual dilation, and then asked whether last frame held the same surface by reading last frame's depth under the output pixel rather than where the dilated sample had stood. At a silhouette those are the object and what is behind it, so the far side of every edge, every surface turned away from the eye and every part thinner than a sample dropped its history on a still frame: a paused picture flashed white along its outlines and read softer than a single frame of it.\n\nThe test now searches last frame's three by three around where the dilated sample stood, allows for how steeply the surface recedes, and weights the normal test by how planar the surface really is, so a thin rod no longer fails it. Measured on a consumer reconstructing at 1.5: a paused frame went from 3,269 flickering pixels to 338, and a still panel from up to 1,787 to 7, with no added ghosting in motion. The cost is 0.065 ms of GPU time at 2280 by 1291."
10
+ },
11
+ {
12
+ "kind": "fixed",
13
+ "short": "Temporal antialiasing no longer flashes on detail finer than a pixel while the camera is still: a paused grille, lace or crack of light holds steady.",
14
+ "text": "**A still pixel's neighbourhood now widens by its own proven flicker.** A thread finer than a pixel is caught by the jitter in some phases and missed in others, and in a phase that misses it the nine taps round the pixel miss it too: the box collapses onto the background, the settled history is clipped away, and a paused frame flashed at every fine edge. A still camera over a still scene samples every pixel the same way in every period of the jitter, so each pixel now keeps a small record of its own samples, and once two periods running have repeated, its box widens by the spread it has shown and it keeps more of its history. A surface that moves, a flame, or anything arriving breaks the repetition, and a sample beyond what the pixel has shown disqualifies the frame it lands in.\n\nMeasured on a scene of fine ironwork and fabric: a held frame went from 839 flickering pixels to 3 on WebGPU and from 1,802 to 14 on WebGL2, and the device check's edge figures are unchanged. It costs two half-float record targets beside the history. A moving camera is resolved exactly as before, and so is WebGL2 without float colour targets."
15
+ },
16
+ {
17
+ "kind": "fixed",
18
+ "short": "A point light's shadow no longer shows a straight-edged patch of wrong shadow while its map is redrawn: a map being sampled is re-baked whole or not at all.",
19
+ "text": "**A re-bake spread across frames was sampled half done.** Faces land in the layer the shader reads as they are drawn, and the origin they were drawn from is published only with the last one, so a map re-baked two faces a frame was read from its old origin with part of it drawn from the new one. A cube face is a 90 degree frustum, so the wrong part was a straight-edged quadrilateral of shadow on the ground under the lamp, and with the face budget spent elsewhere it could hold for over a second.\n\nA map that already holds an image is now re-baked whole or not at all, and a light being shaded may take one whole cube a frame outside `pointShadowFacesPerFrame`, the allowance a wandering light already had. A map with no image yet still fills a few faces a frame, since nothing samples it until it is complete. What it costs is six faces in the frame a shaded light goes stale, rather than two in each of three."
20
+ },
21
+ {
22
+ "kind": "fixed",
23
+ "short": "A flame's shadow no longer switches its own fixture in and out as the flame sways: the default near plane clears what the fire burns from at every point of the sway.",
24
+ "text": "**The near plane moved with the flame, and the fixture under it did not.** `createFlame` puts the light a third of the way up the flame, 0.83 of its width above what it burns from, with a near plane of one width, and sways it by 0.15 of a width on each axis. At the top of the sway the cut sat 7 mm under a 40 cm brazier's coals and at the bottom 13 cm, so the bowl beneath went in and out of the shadow map depending on where the flame stood when the map was last drawn, and the floor under the brazier took a dark disc on some re-bakes and none on others.\n\nThe default `shadowNear` now adds the sway, so it clears the same margin under the fire wherever the flame has wandered. A fixture reaching further below the flame than a sixth of its width sets its own `shadowNear` from its own depth."
25
+ }
26
+ ]
27
+ },
28
+ {
29
+ "version": "4.4.1",
30
+ "date": "2026-09-28",
31
+ "entries": [
32
+ {
33
+ "kind": "fixed",
34
+ "short": "Ambient occlusion no longer lays stripes across a surface seen edge on, or darkens one with nothing on it: a far floor or a ramp reads smooth and open.",
35
+ "text": "**The blur refused every neighbour of a surface seen edge on**, because it compared each tap's depth with the centre's: a floor forty metres out changes its depth by four percent a row, twice the tolerance, so the pass down the frame kept the centre alone and the estimate's rotation pattern stayed on screen as four-pixel stripes across every ramp and far floor. A tap is now compared with the depth this pixel's own plane predicts there, which is exact because 1/z is affine across a plane, so it is refused only for leaving the surface. Measured on a game's ramp at night, the row-to-row ripple fell from 0.028 to 0.0017, and silhouettes are unchanged.\n\n**And the estimate is stored at half scale.** On a plane turned from the eye one of a pixel's two lines sees more than the whole sky and the other less, and only the average over every turn is exactly one; an eight-bit target clipped each turn above one before the blur could average it, so an open plane seen at 80° came back 3.6% shaded. Scenes with occlusion on change where they have surfaces seen edge on."
36
+ },
37
+ {
38
+ "kind": "fixed",
39
+ "short": "A light that declines to cast no longer moves every lamp after it onto its neighbour's shadow, so street lamps keep their own shadows while a car's headlights are on.",
40
+ "text": "**`updatePointShadows` published each shadow at the wrong place after a light that declines to cast.** The shader reads a light's shadow at the light's own place in the shaded list; the lights that cast were gathered into a shorter list first, and each shadow was published at its place in that one. So a light with `castsShadow: false` moved every casting light after it one place down: the first lamp's shadow landed on the declining light and each lamp after it wore the next one's. A light riding a camera or a car is exactly the light that declines, and it is nearly always shaded first. The place is now kept, and left empty.\n\nA consumer that worked around this by handing over only the lights before the first one that declines can hand over the whole count. That cap is what switched lamps' shadows on and off as a car moved, because which lamps rank ahead of its headlights changes with every metre."
41
+ },
42
+ {
43
+ "kind": "fixed",
44
+ "short": "The published packages carry only what the engine is: no local build caches in the packager, and no compiled leftovers of modules since deleted.",
45
+ "text": "**`@driftengine/package` had shipped the packager's local Gradle caches and a build report since at least 4.2.0.** Its `files` names `android` whole, and the ignore rules for that directory's build output live in the repository's `.gitignore`, which npm does not read for a package. Nineteen files, none of them the engine's and none carrying a path or a name from the machine that built them, checked; they are excluded now, with the rest of what an Android build writes.\n\n**And 4.4.0 shipped two modules whose sources had been deleted**, `core`'s `render/shaders/agx.js` and `texture`'s `tensor/reuse.js`, because the build wrote into `dist` and never emptied it. It empties it first now, and `npm run cleanroom` refuses a tarball holding any file that is neither tracked nor built from a source that exists."
46
+ }
47
+ ]
48
+ },
2
49
  {
3
50
  "version": "4.4.0",
4
51
  "date": "2026-09-28",
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The runtime everything needs: renderer, loop, camera, input, geometry. Collision and dynamics live in [`@driftengine/physics`](../physics/README.md), which this package depends on and re-exports whole, so reaching for them through this barrel works as it always did.
4
4
 
5
- **Cost: 789.3 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
5
+ **Cost: 795.8 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
6
6
  3% — the number is a fact about the build rather than a claim in a document.
7
7
 
8
8
  Part of [DriftEngine](../../README.md). See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md)
@@ -1,4 +1,4 @@
1
- import { AO_BLUR_FRAG, AO_FRAG } from './shaders/ambientOcclusion.js';
1
+ import { AO_BLUR_FRAG, AO_FRAG, AO_STORE } from './shaders/ambientOcclusion.js';
2
2
  import { FULLSCREEN_VERT } from './shaders/fullscreen.js';
3
3
  import { compileProgram, uniformLocations } from './shader.js';
4
4
  /**
@@ -146,6 +146,8 @@ export class AmbientOcclusionPass {
146
146
  this.step[0] = 1 / width;
147
147
  this.step[1] = 0;
148
148
  gl.uniform2fv(this.blurUniforms['uStep'] ?? null, this.step);
149
+ /* Still at the estimate's scale, which the second axis needs whole. */
150
+ gl.uniform1f(this.blurUniforms['uAoScale'] ?? null, 1);
149
151
  gl.drawArrays(gl.TRIANGLES, 0, 3);
150
152
  /* And down, back into the estimate, which is the texture handed out. */
151
153
  gl.bindFramebuffer(gl.FRAMEBUFFER, this.estimateFramebuffer);
@@ -153,6 +155,8 @@ export class AmbientOcclusionPass {
153
155
  this.step[0] = 0;
154
156
  this.step[1] = 1 / height;
155
157
  gl.uniform2fv(this.blurUniforms['uStep'] ?? null, this.step);
158
+ /* And decoded, so the composite reads open sky as one. */
159
+ gl.uniform1f(this.blurUniforms['uAoScale'] ?? null, 1 / AO_STORE);
156
160
  gl.drawArrays(gl.TRIANGLES, 0, 3);
157
161
  gl.bindVertexArray(null);
158
162
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
@@ -947,6 +947,9 @@ export declare class WebGL2Renderer implements RendererApi {
947
947
  * Scratch, filled in `endFrame`, which may not allocate.
948
948
  */
949
949
  private frameProjection;
950
+ /** The temporal resolve's depth-to-metres row, from the inverse of `frameProjection`. */
951
+ private readonly taaInvProjection;
952
+ private readonly taaDepthToViewZ;
950
953
  private readonly aoInvProjection;
951
954
  private readonly aoProjScale;
952
955
  /**
@@ -626,6 +626,9 @@ export class WebGL2Renderer {
626
626
  * Scratch, filled in `endFrame`, which may not allocate.
627
627
  */
628
628
  frameProjection = null;
629
+ /** The temporal resolve's depth-to-metres row, from the inverse of `frameProjection`. */
630
+ taaInvProjection = mat4.create();
631
+ taaDepthToViewZ = Float32Array.of(0, 0, 0, 1);
629
632
  aoInvProjection = mat4.create();
630
633
  aoProjScale = new Float32Array(2);
631
634
  /**
@@ -4943,9 +4946,19 @@ export class WebGL2Renderer {
4943
4946
  * its early exit on it — so this is present every jittered frame rather than absent on some,
4944
4947
  * and the pass still runs and fills the history for the next one.
4945
4948
  */
4949
+ /* The anti-flicker's same-surface test reads depths in metres, as the occlusion does. */
4950
+ if (this.frameProjection !== null) {
4951
+ mat4.invert(this.taaInvProjection, this.frameProjection);
4952
+ this.taaDepthToViewZ[0] = this.taaInvProjection[10] ?? 0;
4953
+ this.taaDepthToViewZ[1] = this.taaInvProjection[14] ?? 0;
4954
+ this.taaDepthToViewZ[2] = this.taaInvProjection[11] ?? 0;
4955
+ this.taaDepthToViewZ[3] = this.taaInvProjection[15] ?? 1;
4956
+ }
4946
4957
  temporal = {
4947
4958
  reprojection: this.reprojection,
4948
4959
  blend: this.temporalHistoryUsable ? TEMPORAL_HISTORY_BLEND : 0,
4960
+ periodStart: this.temporalHistory.periodStart,
4961
+ depthToViewZ: this.taaDepthToViewZ,
4949
4962
  };
4950
4963
  }
4951
4964
  /*
@@ -68,7 +68,7 @@ declare const RUSH_TEXTURES: {
68
68
  export declare const AO_FRAG_FIELDS: UniformFields;
69
69
  export declare const AO_FRAG_SIZE: 96;
70
70
  export declare const AO_BLUR_FIELDS: UniformFields;
71
- export declare const AO_BLUR_SIZE: 32;
71
+ export declare const AO_BLUR_SIZE: 48;
72
72
  export declare const MEDIUM_FRAG_FIELDS: UniformFields;
73
73
  export declare const MEDIUM_FRAG_SIZE: 240;
74
74
  export declare const MEDIUM_UPSAMPLE_FIELDS: UniformFields;
@@ -174,14 +174,20 @@ export declare const ADDITIVE_BLEND: GPUBlendState;
174
174
  /** A fullscreen pass over one or more inputs, writing to one colour target. */
175
175
  export declare function postPipeline(cache: PipelineCache, device: GPUDevice, layout: GPUBindGroupLayout, key: string, fragment: string, format: GPUTextureFormat, blend?: GPUBlendState): GPURenderPipeline;
176
176
  /**
177
- * The temporal resolve's block and its three inputs.
177
+ * The same over several colour targets, in the fragment's output order: the temporal resolve
178
+ * writes its picture and each pixel's flicker record in one pass. `targets` is the caller's, held
179
+ * rather than built per call, since this is asked for every frame.
180
+ */
181
+ export declare function postPipelineTargets(cache: PipelineCache, device: GPUDevice, layout: GPUBindGroupLayout, key: string, fragment: string, targets: GPUColorTargetState[]): GPURenderPipeline;
182
+ /**
183
+ * The temporal resolve's block and its four inputs.
178
184
  *
179
185
  * Named like the occlusion's beside it, and for the same reason `BLOOM_DOWNSAMPLE_FIELDS` is: a
180
186
  * block read by assumption rather than by name is a uniform written to the wrong offset, which
181
187
  * draws a plausible picture.
182
188
  */
183
189
  export declare const TAA_FRAG_FIELDS: UniformFields;
184
- export declare const TAA_FRAG_SIZE: 80;
190
+ export declare const TAA_FRAG_SIZE: 112;
185
191
  export declare const TAA_UNIFORMS: 1;
186
192
  export declare const TAA_TEXTURES: {
187
193
  readonly uScene: {
@@ -194,11 +200,21 @@ export declare const TAA_TEXTURES: {
194
200
  readonly sampler: 35;
195
201
  readonly type: "sampler2D";
196
202
  };
197
- readonly uDepth: {
203
+ readonly uFlicker: {
198
204
  readonly texture: 36;
199
205
  readonly sampler: 37;
200
206
  readonly type: "sampler2D";
201
207
  };
208
+ readonly uFlickerMotion: {
209
+ readonly texture: 38;
210
+ readonly sampler: 39;
211
+ readonly type: "sampler2D";
212
+ };
213
+ readonly uDepth: {
214
+ readonly texture: 40;
215
+ readonly sampler: 41;
216
+ readonly type: "sampler2D";
217
+ };
202
218
  };
203
219
  /**
204
220
  * The order-independent resolve's inputs. It has no uniform block of its own — the whole pass is
@@ -244,7 +244,20 @@ export const ADDITIVE_BLEND = {
244
244
  };
245
245
  /** A fullscreen pass over one or more inputs, writing to one colour target. */
246
246
  export function postPipeline(cache, device, layout, key, fragment, format, blend) {
247
- return cache.get(key, () => ({
247
+ return cache.get(key, () => describePost(device, layout, key, fragment, [
248
+ blend === undefined ? { format } : { format, blend },
249
+ ]));
250
+ }
251
+ /**
252
+ * The same over several colour targets, in the fragment's output order: the temporal resolve
253
+ * writes its picture and each pixel's flicker record in one pass. `targets` is the caller's, held
254
+ * rather than built per call, since this is asked for every frame.
255
+ */
256
+ export function postPipelineTargets(cache, device, layout, key, fragment, targets) {
257
+ return cache.get(key, () => describePost(device, layout, key, fragment, targets));
258
+ }
259
+ function describePost(device, layout, key, fragment, targets) {
260
+ return {
248
261
  label: key,
249
262
  layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
250
263
  vertex: {
@@ -254,7 +267,7 @@ export function postPipeline(cache, device, layout, key, fragment, format, blend
254
267
  fragment: {
255
268
  module: shaderModule(device, { label: key, code: fragment }),
256
269
  entryPoint: 'main',
257
- targets: [blend === undefined ? { format } : { format, blend }],
270
+ targets,
258
271
  },
259
272
  /*
260
273
  * No depth at all, which is what makes this different from every other pipeline here: the
@@ -262,10 +275,10 @@ export function postPipeline(cache, device, layout, key, fragment, format, blend
262
275
  * disables the test, the write and culling by hand for the same draw.
263
276
  */
264
277
  primitive: { topology: 'triangle-list', cullMode: 'none' },
265
- }));
278
+ };
266
279
  }
267
280
  /**
268
- * The temporal resolve's block and its three inputs.
281
+ * The temporal resolve's block and its four inputs.
269
282
  *
270
283
  * Named like the occlusion's beside it, and for the same reason `BLOOM_DOWNSAMPLE_FIELDS` is: a
271
284
  * block read by assumption rather than by name is a uniform written to the wrong offset, which
@@ -596,6 +596,16 @@ export declare class WebGPURenderer implements RendererApi {
596
596
  private taaUniforms;
597
597
  private taaTextures;
598
598
  private taaViews;
599
+ /** Each pixel's flicker record, paired with the history and swapped with it: see `temporalAa.ts`. */
600
+ private taaRecords;
601
+ private taaRecordViews;
602
+ /** The rest of each pixel's record — depth, pull and recent swing — beside the first. */
603
+ private taaMotions;
604
+ private taaMotionViews;
605
+ /** The resolve's depth-to-metres row, from the inverse of `frameProjection`. */
606
+ private readonly taaInvProjection;
607
+ /** The resolve's two targets, the picture and the record, held for the pipeline cache's sake. */
608
+ private taaTargets;
599
609
  /** Group `i` reads history `i` and the pass writes into the other one. */
600
610
  private taaBindGroups;
601
611
  private taaWrite;
@@ -48,9 +48,10 @@ import { reflectionTargetSize } from '../../planarReflectionDraw.js';
48
48
  import { drawingBufferSize } from '../../drawingBuffer.js';
49
49
  import { MOTION_BLUR_MAX_UV, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV } from '../../vertexDefaults.js';
50
50
  import { ExposurePass } from './exposurePass.js';
51
+ import { AO_STORE } from '../../shaders/ambientOcclusion.js';
51
52
  import { clampAutoExposure } from '../../shaders/exposure.js';
52
53
  import { clampLocalExposure } from '../../shaders/localExposure.js';
53
- import { createDepthResolveLayout, createPostStageLayout, depthResolvePipeline, postPipeline, sceneColorFormat, AO_BLUR_FIELDS, AO_BLUR_FRAG_WGSL, AO_BLUR_SIZE, AO_BLUR_TEXTURES, AO_BLUR_UNIFORMS, AO_FRAG_FIELDS, AO_FRAG_SIZE, AO_FRAG_WGSL, AO_TEXTURES, AO_UNIFORMS, ADDITIVE_BLEND, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, BLOOM_UPSAMPLE_FRAG_WGSL, RESOLVED_DEPTH_FORMAT, RUSH_FRAG_FIELDS, RUSH_FRAG_SIZE, RUSH_FRAG_WGSL, RUSH_TEXTURES, RUSH_UNIFORMS, TAA_FRAG_FIELDS, TAA_FRAG_SIZE, TAA_TEXTURES, TAA_UNIFORMS, TEMPORAL_RESOLVE_FRAG_WGSL, MEDIUM_FRAG_FIELDS, MEDIUM_FRAG_SIZE, MEDIUM_FRAG_WGSL, MEDIUM_TEXTURES, MEDIUM_TRANSMIT_BLEND, MEDIUM_UNIFORMS, MEDIUM_UPSAMPLE_FIELDS, MEDIUM_UPSAMPLE_FRAG_WGSL, MEDIUM_UPSAMPLE_SIZE, MEDIUM_UPSAMPLE_TEXTURES, MEDIUM_UPSAMPLE_UNIFORMS, OIT_COMPOSITE_BLEND, OIT_RESOLVE_FRAG_WGSL, OIT_TEXTURES, DECAL_FRAG_FIELDS, DECAL_FRAG_SIZE, DECAL_MULTIPLY_BLEND, DECAL_PROJECT_FRAG_WGSL, DECAL_TEXTURES, DECAL_UNIFORMS, PREMULTIPLIED_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
54
+ import { createDepthResolveLayout, createPostStageLayout, depthResolvePipeline, postPipeline, postPipelineTargets, sceneColorFormat, AO_BLUR_FIELDS, AO_BLUR_FRAG_WGSL, AO_BLUR_SIZE, AO_BLUR_TEXTURES, AO_BLUR_UNIFORMS, AO_FRAG_FIELDS, AO_FRAG_SIZE, AO_FRAG_WGSL, AO_TEXTURES, AO_UNIFORMS, ADDITIVE_BLEND, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, BLOOM_UPSAMPLE_FRAG_WGSL, RESOLVED_DEPTH_FORMAT, RUSH_FRAG_FIELDS, RUSH_FRAG_SIZE, RUSH_FRAG_WGSL, RUSH_TEXTURES, RUSH_UNIFORMS, TAA_FRAG_FIELDS, TAA_FRAG_SIZE, TAA_TEXTURES, TAA_UNIFORMS, TEMPORAL_RESOLVE_FRAG_WGSL, MEDIUM_FRAG_FIELDS, MEDIUM_FRAG_SIZE, MEDIUM_FRAG_WGSL, MEDIUM_TEXTURES, MEDIUM_TRANSMIT_BLEND, MEDIUM_UNIFORMS, MEDIUM_UPSAMPLE_FIELDS, MEDIUM_UPSAMPLE_FRAG_WGSL, MEDIUM_UPSAMPLE_SIZE, MEDIUM_UPSAMPLE_TEXTURES, MEDIUM_UPSAMPLE_UNIFORMS, OIT_COMPOSITE_BLEND, OIT_RESOLVE_FRAG_WGSL, OIT_TEXTURES, DECAL_FRAG_FIELDS, DECAL_FRAG_SIZE, DECAL_MULTIPLY_BLEND, DECAL_PROJECT_FRAG_WGSL, DECAL_TEXTURES, DECAL_UNIFORMS, PREMULTIPLIED_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
54
55
  import { GRADE_PLACEHOLDER_SIZE, identityGradeLut, validateGradeLut, } from '../../colourGrade.js';
55
56
  import { FILM_LOOK_WITHOUT_COMPOSITE, clampGrain, clampVignette, grainSeed, } from '../../shaders/filmLook.js';
56
57
  import { createProbeMipBindGroupLayout, createProbePrefilterBindGroupLayout, PREFILTER_UNIFORM_OFFSETS, probeLevels, probeMipPipeline, probePrefilterPipeline, PROBE_FACES, PROBE_FACE_BASIS, PROBE_FAR_M, PROBE_FOV_DEG, PROBE_MIP_UNIFORM_SIZE, PROBE_NEAR_M, } from './probePass.js';
@@ -1348,6 +1349,16 @@ export class WebGPURenderer {
1348
1349
  taaUniforms;
1349
1350
  taaTextures = [null, null];
1350
1351
  taaViews = [null, null];
1352
+ /** Each pixel's flicker record, paired with the history and swapped with it: see `temporalAa.ts`. */
1353
+ taaRecords = [null, null];
1354
+ taaRecordViews = [null, null];
1355
+ /** The rest of each pixel's record — depth, pull and recent swing — beside the first. */
1356
+ taaMotions = [null, null];
1357
+ taaMotionViews = [null, null];
1358
+ /** The resolve's depth-to-metres row, from the inverse of `frameProjection`. */
1359
+ taaInvProjection = new Float32Array(16);
1360
+ /** The resolve's two targets, the picture and the record, held for the pipeline cache's sake. */
1361
+ taaTargets = [];
1351
1362
  /** Group `i` reads history `i` and the pass writes into the other one. */
1352
1363
  taaBindGroups = [null, null];
1353
1364
  taaWrite = 0;
@@ -5229,6 +5240,8 @@ export class WebGPURenderer {
5229
5240
  this.taaLayout = createPostStageLayout(device, TAA_UNIFORMS, TAA_FRAG_SIZE, [
5230
5241
  { binding: TAA_TEXTURES.uScene },
5231
5242
  { binding: TAA_TEXTURES.uHistory },
5243
+ { binding: TAA_TEXTURES.uFlicker },
5244
+ { binding: TAA_TEXTURES.uFlickerMotion },
5232
5245
  { binding: TAA_TEXTURES.uDepth, filterable: false },
5233
5246
  ]);
5234
5247
  this.taaUniforms = device.createBuffer({
@@ -8646,8 +8659,10 @@ export class WebGPURenderer {
8646
8659
  return;
8647
8660
  const write = this.taaWrite;
8648
8661
  const target = this.taaViews[write];
8662
+ const record = this.taaRecordViews[write];
8663
+ const motion = this.taaMotionViews[write];
8649
8664
  const group = this.taaBindGroups[1 - write];
8650
- if (target === null || group === null)
8665
+ if (target === null || record === null || motion === null || group === null)
8651
8666
  return;
8652
8667
  const { device } = this.surface;
8653
8668
  const f = this.taaFloats;
@@ -8656,15 +8671,34 @@ export class WebGPURenderer {
8656
8671
  f[at('uTexel')] = 1 / this.renderWidth;
8657
8672
  f[at('uTexel') + 1] = 1 / this.renderHeight;
8658
8673
  f[at('uHistoryBlend')] = this.temporalHistoryUsable ? TEMPORAL_HISTORY_BLEND : 0;
8674
+ f[at('uPeriodStart')] = this.temporalHistory.periodStart ? 1 : 0;
8675
+ /* The anti-flicker's same-surface test reads depths in metres, as the occlusion does. */
8676
+ const depthRow = at('uDepthToViewZ');
8677
+ if (this.frameProjection !== null) {
8678
+ mat4.invert(this.taaInvProjection, this.frameProjection);
8679
+ f[depthRow] = this.taaInvProjection[10] ?? 0;
8680
+ f[depthRow + 1] = this.taaInvProjection[14] ?? 0;
8681
+ f[depthRow + 2] = this.taaInvProjection[11] ?? 0;
8682
+ f[depthRow + 3] = this.taaInvProjection[15] ?? 1;
8683
+ }
8684
+ else {
8685
+ f[depthRow] = 0;
8686
+ f[depthRow + 1] = 0;
8687
+ f[depthRow + 2] = 0;
8688
+ f[depthRow + 3] = 1;
8689
+ }
8690
+ f[at('uAntiFlicker')] = 1;
8659
8691
  device.queue.writeBuffer(this.taaUniforms, 0, this.taaStaging);
8660
8692
  const pass = encoder.beginRenderPass({
8661
8693
  label: 'post.taa',
8662
8694
  timestampWrites: this.gpuTimer.writesFor(),
8663
8695
  colorAttachments: [
8664
8696
  { view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 1] },
8697
+ { view: record, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
8698
+ { view: motion, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
8665
8699
  ],
8666
8700
  });
8667
- pass.setPipeline(postPipeline(this.pipelines, device, this.taaLayout, 'post.taa', TEMPORAL_RESOLVE_FRAG_WGSL, sceneColorFormat(this.quality.hdrScene)));
8701
+ pass.setPipeline(postPipelineTargets(this.pipelines, device, this.taaLayout, 'post.taa', TEMPORAL_RESOLVE_FRAG_WGSL, this.taaTargets));
8668
8702
  pass.setBindGroup(0, group, [0]);
8669
8703
  pass.draw(3);
8670
8704
  pass.end();
@@ -9124,12 +9158,15 @@ export class WebGPURenderer {
9124
9158
  otherwise halve the blur and leave half the pattern standing. */
9125
9159
  const width = Math.max(1, this.aoTarget?.width ?? 1);
9126
9160
  const height = Math.max(1, this.aoTarget?.height ?? 1);
9127
- for (const [index, step] of [
9128
- [0, [1 / width, 0]],
9129
- [1, [0, 1 / height]],
9161
+ /* Across at the estimate's own scale, which the second axis needs whole; down decoded, so the
9162
+ composite reads open sky as one. The same two `ambientOcclusionPass.ts` writes. */
9163
+ for (const [index, step, scale] of [
9164
+ [0, [1 / width, 0], 1],
9165
+ [1, [0, 1 / height], 1 / AO_STORE],
9130
9166
  ]) {
9131
9167
  f[slot(index) + at('uStep')] = step[0];
9132
9168
  f[slot(index) + at('uStep') + 1] = step[1];
9169
+ f[slot(index) + at('uAoScale')] = scale;
9133
9170
  f[slot(index) + at('uDepthToViewZ')] = inv[10] ?? 0;
9134
9171
  f[slot(index) + at('uDepthToViewZ') + 1] = inv[14] ?? 0;
9135
9172
  f[slot(index) + at('uDepthToViewZ') + 2] = inv[11] ?? 0;
@@ -12288,7 +12325,28 @@ export class WebGPURenderer {
12288
12325
  });
12289
12326
  this.taaTextures[i] = texture;
12290
12327
  this.taaViews[i] = texture.createView();
12328
+ /* The records in half float, whatever the picture's format: one of them holds metres. */
12329
+ this.taaRecords[i]?.destroy();
12330
+ const record = device.createTexture({
12331
+ label: `post.taaRecord${i}`,
12332
+ size: [width, height],
12333
+ format: 'rgba16float',
12334
+ usage: USAGE,
12335
+ });
12336
+ this.taaRecords[i] = record;
12337
+ this.taaRecordViews[i] = record.createView();
12338
+ this.taaMotions[i]?.destroy();
12339
+ /* Two numbers a pixel: the depth its sample stood at, in metres, and how far that swings. */
12340
+ const motion = device.createTexture({
12341
+ label: `post.taaMotion${i}`,
12342
+ size: [width, height],
12343
+ format: 'rg16float',
12344
+ usage: USAGE,
12345
+ });
12346
+ this.taaMotions[i] = motion;
12347
+ this.taaMotionViews[i] = motion.createView();
12291
12348
  }
12349
+ this.taaTargets = [{ format }, { format: 'rgba16float' }, { format: 'rg16float' }];
12292
12350
  this.taaWrite = 0;
12293
12351
  /* A resized target is a new texture, so whatever the history held is gone. */
12294
12352
  this.temporalHistory.invalidate();
@@ -12474,8 +12532,15 @@ export class WebGPURenderer {
12474
12532
  const scene = this.sceneColorView;
12475
12533
  const history = this.taaViews[i];
12476
12534
  const resolved = this.taaViews[1 - i];
12477
- if (scene === null || history === null || resolved === null)
12535
+ const record = this.taaRecordViews[i];
12536
+ const motion = this.taaMotionViews[i];
12537
+ if (scene === null ||
12538
+ history === null ||
12539
+ resolved === null ||
12540
+ record === null ||
12541
+ motion === null) {
12478
12542
  continue;
12543
+ }
12479
12544
  this.taaBindGroups[i] = device.createBindGroup({
12480
12545
  label: `post.taaBindGroup${i}`,
12481
12546
  layout: this.taaLayout,
@@ -12483,6 +12548,8 @@ export class WebGPURenderer {
12483
12548
  { binding: TAA_UNIFORMS, resource: { buffer: this.taaUniforms, size: TAA_FRAG_SIZE } },
12484
12549
  ...entry(TAA_TEXTURES.uScene, scene),
12485
12550
  ...entry(TAA_TEXTURES.uHistory, history),
12551
+ ...entry(TAA_TEXTURES.uFlicker, record),
12552
+ ...entry(TAA_TEXTURES.uFlickerMotion, motion),
12486
12553
  ...entry(TAA_TEXTURES.uDepth, this.resolvedDepthView, true),
12487
12554
  ],
12488
12555
  });
@@ -11,6 +11,7 @@ export function flameFrequencyHz(diameterM) {
11
11
  export function createFlame(options) {
12
12
  const height = HEIGHT_PER_DIAMETER * options.diameterM;
13
13
  const centre = [options.x, options.y + height / 3, options.z];
14
+ const swayM = (options.sway ?? SWAY_PER_HEIGHT) * height;
14
15
  /* A seed spread over the whole turn, so seeds 1 and 2 are not neighbours in phase. */
15
16
  const phase = ((options.seed ?? 0) * 2.399963) % (Math.PI * 2);
16
17
  const light = {
@@ -23,7 +24,19 @@ export function createFlame(options) {
23
24
  radius: options.radius,
24
25
  /* Off, because this helper is the flicker; the selection's own would apply a second one. */
25
26
  flicker: 0,
26
- shadowNear: Math.max(0.05, options.diameterM),
27
+ /*
28
+ * **Clear of what the fire burns from wherever the light has wandered**, which is the sway added
29
+ * to the near plane a still flame would want. The shadow map is baked from where the light
30
+ * stood, so the cut moves with it, and each axis moves by at most `swayM`: a near plane of D
31
+ * alone left a brazier's bowl 7 mm inside it at the top of the sway and 13 cm outside at the
32
+ * bottom, and the bowl's shadow on the floor under the brazier came and went with every
33
+ * re-bake — a dark disc on some, none on others.
34
+ *
35
+ * What it gives up is `swayM` of shadow nearest the light, which is inside the fixture. What
36
+ * would make it wrong is a fixture reaching further below the flame than D / 6: this clears
37
+ * that far and no further, so a deep bowl sets its own `shadowNear` from its own depth.
38
+ */
39
+ shadowNear: Math.max(0.05, options.diameterM) + swayM,
27
40
  sourceRadius: options.diameterM / 2,
28
41
  ...(options.castsShadow === false ? { castsShadow: false } : {}),
29
42
  };
@@ -41,7 +54,7 @@ export function createFlame(options) {
41
54
  color: [options.color[0], options.color[1], options.color[2]],
42
55
  flicker: options.flicker ?? 0.18,
43
56
  phase,
44
- swayM: (options.sway ?? SWAY_PER_HEIGHT) * height,
57
+ swayM,
45
58
  };
46
59
  }
47
60
  /** Where the flame's light is and how bright, at `timeSeconds`. Writes `flame.light` in place. */
@@ -45,7 +45,8 @@ ready: Uint8Array, count: number, out: Int32Array,
45
45
  */
46
46
  chronic?: Uint8Array): number;
47
47
  /**
48
- * The shaded lights that also cast, in the order they were shaded.
48
+ * The shaded lights that also cast, each at its own place in the shaded list, and −1 at the place
49
+ * of a light that does not.
49
50
  *
50
51
  * `castsShadow: false` says a light is not worth a cubemap. `selectPointLights` honours
51
52
  * that when it builds the *eligible* list, and that was taken for the whole promise — but
@@ -57,7 +58,16 @@ chronic?: Uint8Array): number;
57
58
  * It showed up as a shadow with nothing to throw it — a lamp that rides the camera
58
59
  * declines to cast for exactly that reason, and cast anyway.
59
60
  *
60
- * Writes into `out` and returns how many it wrote; allocation-free, because this runs
61
+ * **The place is kept rather than the gap closed, because the place is the shader's index.** The
62
+ * pool publishes each map under its light's position in this list, and the shader reads a light's
63
+ * shadow at the light's position in the shaded one. Closing the gap made the two numberings
64
+ * disagree after the first light that declined: every lamp shaded after it was published one
65
+ * place down, the first on the declining light and each of the rest on its neighbour. A consumer
66
+ * whose headlights decline, and are nearly always shaded first, could only keep its lamps honest by
67
+ * dropping the shadows of every lamp ranked after them, and which lamps those were changed as the
68
+ * car moved.
69
+ *
70
+ * Writes into `out` and returns how many places it wrote; allocation-free, because this runs
61
71
  * every frame.
62
72
  */
63
73
  export declare function selectCastingLights(lights: readonly {