@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.
- package/CHANGELOG.json +47 -0
- package/README.md +1 -1
- package/dist/render/ambientOcclusionPass.js +5 -1
- package/dist/render/backend/webgl2/renderer.d.ts +3 -0
- package/dist/render/backend/webgl2/renderer.js +13 -0
- package/dist/render/backend/webgpu/postPass.d.ts +20 -4
- package/dist/render/backend/webgpu/postPass.js +17 -4
- package/dist/render/backend/webgpu/renderer.d.ts +10 -0
- package/dist/render/backend/webgpu/renderer.js +74 -7
- package/dist/render/flameLight.js +15 -2
- package/dist/render/pointShadowBudget.d.ts +12 -2
- package/dist/render/pointShadowBudget.js +67 -51
- package/dist/render/recon/disocclusion.d.ts +36 -3
- package/dist/render/recon/disocclusion.js +56 -4
- package/dist/render/recon/resolve.js +95 -12
- package/dist/render/sceneTarget.d.ts +2 -0
- package/dist/render/sceneTarget.js +1 -1
- package/dist/render/shaders/ambientOcclusion.d.ts +39 -1
- package/dist/render/shaders/ambientOcclusion.js +69 -9
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +8 -3
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +8 -3
- package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +31 -6
- package/dist/render/shaders/generated/temporalResolve.wgsl.js +31 -6
- package/dist/render/shaders/recon/resolve.wgsl.d.ts +1 -1
- package/dist/render/shaders/recon/resolve.wgsl.js +112 -8
- package/dist/render/shaders/temporalResolve.js +133 -5
- package/dist/render/temporalAa.d.ts +145 -0
- package/dist/render/temporalAa.js +190 -0
- package/dist/render/temporalPass.d.ts +16 -14
- package/dist/render/temporalPass.js +59 -14
- package/package.json +6 -6
- package/src/render/ambientOcclusionPass.ts +5 -1
- package/src/render/backend/webgl2/renderer.ts +21 -1
- package/src/render/backend/webgpu/postPass.ts +34 -4
- package/src/render/backend/webgpu/renderer.ts +77 -7
- package/src/render/flameLight.ts +15 -2
- package/src/render/pointShadowBudget.ts +65 -47
- package/src/render/recon/disocclusion.ts +54 -3
- package/src/render/recon/resolve.ts +137 -10
- package/src/render/sceneTarget.ts +8 -1
- package/src/render/shaders/ambientOcclusion.ts +81 -9
- package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +8 -3
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +31 -6
- package/src/render/shaders/recon/resolve.wgsl.ts +112 -8
- package/src/render/shaders/temporalResolve.ts +141 -5
- package/src/render/temporalAa.ts +227 -0
- package/src/render/temporalPass.ts +88 -12
- package/dist/render/shaders/agx.d.ts +0 -31
- 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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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 {
|