@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
|
@@ -84,7 +84,8 @@ chronic) {
|
|
|
84
84
|
return written;
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
|
-
* The shaded lights that also cast, in the
|
|
87
|
+
* The shaded lights that also cast, each at its own place in the shaded list, and −1 at the place
|
|
88
|
+
* of a light that does not.
|
|
88
89
|
*
|
|
89
90
|
* `castsShadow: false` says a light is not worth a cubemap. `selectPointLights` honours
|
|
90
91
|
* that when it builds the *eligible* list, and that was taken for the whole promise — but
|
|
@@ -96,20 +97,25 @@ chronic) {
|
|
|
96
97
|
* It showed up as a shadow with nothing to throw it — a lamp that rides the camera
|
|
97
98
|
* declines to cast for exactly that reason, and cast anyway.
|
|
98
99
|
*
|
|
99
|
-
*
|
|
100
|
+
* **The place is kept rather than the gap closed, because the place is the shader's index.** The
|
|
101
|
+
* pool publishes each map under its light's position in this list, and the shader reads a light's
|
|
102
|
+
* shadow at the light's position in the shaded one. Closing the gap made the two numberings
|
|
103
|
+
* disagree after the first light that declined: every lamp shaded after it was published one
|
|
104
|
+
* place down, the first on the declining light and each of the rest on its neighbour. A consumer
|
|
105
|
+
* whose headlights decline, and are nearly always shaded first, could only keep its lamps honest by
|
|
106
|
+
* dropping the shadows of every lamp ranked after them, and which lamps those were changed as the
|
|
107
|
+
* car moved.
|
|
108
|
+
*
|
|
109
|
+
* Writes into `out` and returns how many places it wrote; allocation-free, because this runs
|
|
100
110
|
* every frame.
|
|
101
111
|
*/
|
|
102
112
|
export function selectCastingLights(lights, activeWorldIndices, activeCount, out) {
|
|
103
|
-
|
|
104
|
-
for (let i = 0; i <
|
|
113
|
+
const count = Math.min(activeCount, out.length);
|
|
114
|
+
for (let i = 0; i < count; i++) {
|
|
105
115
|
const worldIndex = activeWorldIndices[i] ?? -1;
|
|
106
|
-
|
|
107
|
-
continue;
|
|
108
|
-
if (lights[worldIndex]?.castsShadow === false)
|
|
109
|
-
continue;
|
|
110
|
-
out[written++] = worldIndex;
|
|
116
|
+
out[i] = worldIndex >= 0 && lights[worldIndex]?.castsShadow !== false ? worldIndex : -1;
|
|
111
117
|
}
|
|
112
|
-
return
|
|
118
|
+
return count;
|
|
113
119
|
}
|
|
114
120
|
/**
|
|
115
121
|
* How many consecutive frames a slot must be stale before it is treated as chronically so.
|
|
@@ -171,15 +177,15 @@ export function runPointShadowBakes(pool, lights, facesPerFrame, liveFacesPerFra
|
|
|
171
177
|
* **The run decays rather than resetting, and it climbs past the threshold before it stops.**
|
|
172
178
|
*
|
|
173
179
|
* A light that wanders on a curve is not stale on *every* frame — near the turning points of
|
|
174
|
-
* its travel it moves less in a frame than the rebake tolerance
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
180
|
+
* its travel it moves less in a frame than the rebake tolerance. When the run decided whether a
|
|
181
|
+
* wanderer got its whole cube, a run that reset to zero dropped it back to two faces a frame
|
|
182
|
+
* there, and the shadow stepped through the slow part of the wander: reported as a vibration
|
|
183
|
+
* against a brazier. Every image-holding map is re-baked whole now, so the run only orders the
|
|
184
|
+
* queue, and the hysteresis keeps a wanderer behind the lights that settle instead of letting
|
|
185
|
+
* it jump ahead at every turning point.
|
|
179
186
|
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* so a light that genuinely settles stops re-baking rather than holding a budget for ever.
|
|
187
|
+
* It climbs to twice the threshold, which keeps a light chronic for three quiet frames, and
|
|
188
|
+
* decays one frame at a time so a light that genuinely settles rejoins the ordinary order.
|
|
183
189
|
*/
|
|
184
190
|
const previous = scratch.staleRun[slot] ?? 0;
|
|
185
191
|
const run = stale
|
|
@@ -192,37 +198,40 @@ export function runPointShadowBakes(pool, lights, facesPerFrame, liveFacesPerFra
|
|
|
192
198
|
let faces = facesPerFrame;
|
|
193
199
|
let urgentSpent = false;
|
|
194
200
|
/*
|
|
195
|
-
* **
|
|
196
|
-
*
|
|
197
|
-
* dribbled out at two faces a frame is a map nobody can sample correctly until it lands.
|
|
201
|
+
* **A map that holds an image is re-baked whole or not at all**, and one being shaded may take a
|
|
202
|
+
* whole cube a frame outside the budget to make that possible.
|
|
198
203
|
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* wanders about once a second.
|
|
204
|
+
* Faces land in the layer the shader samples as they are drawn, and `planBake` publishes the
|
|
205
|
+
* origin they were drawn from only with the last one. So a re-bake spread over frames is a cube
|
|
206
|
+
* the shader reads from the old origin with part of it drawn from the new one — and a face is a
|
|
207
|
+
* 90 degree frustum, so the part drawn from somewhere else is a straight-edged quadrilateral of
|
|
208
|
+
* wrong shadow on the ground under the lamp. With the budget spent elsewhere it could hold for a
|
|
209
|
+
* second and more. Reported under a brazier as a dark shape that came and went.
|
|
206
210
|
*
|
|
207
|
-
*
|
|
211
|
+
* A map with no image yet is not sampled until its sixth face lands (see
|
|
212
|
+
* `PointShadowSystem.bind`), so it still fills a few faces a frame, and a cold one being shaded
|
|
213
|
+
* still jumps the queue with all six.
|
|
208
214
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* cost the frame.
|
|
215
|
+
* The allowance used to be for a *chronically* stale light only — a wandering flame — whose
|
|
216
|
+
* dribbled cube published a new origin once every third frame, so its shadow stepped at 20 Hz
|
|
217
|
+
* where the flame wanders about once a second: reported as motion that was "really fast, not
|
|
218
|
+
* smooth, snappy, and unrealistic". Any shaded light is owed the same now, because a cube read
|
|
219
|
+
* part-way is wrong in the same place for the same reason whether or not it will be stale again.
|
|
215
220
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
221
|
+
* **Bounded to one light, and it has to be.** Eight braziers each taking six passes is the 48
|
|
222
|
+
* passes and 89 ms `pointShadowFacesPerFrame` exists to prevent. The first in the plan's order
|
|
223
|
+
* takes it, and `planPointShadowBakes` puts the lights that settled ahead of the ones that never
|
|
224
|
+
* do, so a flame refining its image cannot hold a correction hostage. The rest keep sampling a
|
|
225
|
+
* complete image from a few centimetres away until their turn, which is the trade
|
|
226
|
+
* `pointShadowRebakeDistance` already makes. A warm map, held but not shaded, is re-baked when a
|
|
227
|
+
* whole cube fits the budget or once it is shaded, and not before.
|
|
219
228
|
*
|
|
220
|
-
* **What would make it wrong** is a caster set heavy enough that six passes over it does not
|
|
221
|
-
*
|
|
222
|
-
*
|
|
229
|
+
* **What would make it wrong** is a caster set heavy enough that six passes over it does not fit
|
|
230
|
+
* the frame. Measured against a game whose whole static world is about 8,500 triangles in a
|
|
231
|
+
* handful of draws; a world an order of magnitude heavier wants `pointShadowFacesPerFrame`
|
|
223
232
|
* raised or this given up, and `gpuTiming` is how to tell which.
|
|
224
233
|
*/
|
|
225
|
-
let
|
|
234
|
+
let refreshSpent = false;
|
|
226
235
|
for (let i = 0; i < planned && faces > 0; i++) {
|
|
227
236
|
const slot = scratch.order[i] ?? -1;
|
|
228
237
|
/*
|
|
@@ -235,15 +244,22 @@ export function runPointShadowBakes(pool, lights, facesPerFrame, liveFacesPerFra
|
|
|
235
244
|
if (light === undefined || map === undefined)
|
|
236
245
|
continue;
|
|
237
246
|
const sampled = slot < pool.sampledCount;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
+
if (scratch.ready[slot] !== 1) {
|
|
248
|
+
/* No image to spoil: a few faces a frame, or all six at once for one being shaded. */
|
|
249
|
+
const urgent = sampled && !urgentSpent;
|
|
250
|
+
if (urgent)
|
|
251
|
+
urgentSpent = true;
|
|
252
|
+
faces -= bake(map, light, staticCasters, urgent ? faceCount : faces);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (faces >= faceCount) {
|
|
256
|
+
faces -= bake(map, light, staticCasters, faceCount);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (!sampled || refreshSpent)
|
|
260
|
+
continue;
|
|
261
|
+
refreshSpent = true;
|
|
262
|
+
faces -= bake(map, light, staticCasters, faceCount);
|
|
247
263
|
}
|
|
248
264
|
/*
|
|
249
265
|
* Live maps contain movers only and are composed with the static maps in the shader. A second
|
|
@@ -12,10 +12,15 @@
|
|
|
12
12
|
* a different part of a texel than the one it stands for, which at a grazing angle is a different
|
|
13
13
|
* depth; the tolerance grows with the motion's length.
|
|
14
14
|
*
|
|
15
|
+
* **And in proportion to the surface's own slope.** A new jitter samples a surface somewhere else, so
|
|
16
|
+
* on one turned from the eye two frames' samples of it differ by up to a texel's worth of its depth
|
|
17
|
+
* with nothing moved. `surfaceSlope` measures that step on the surface's own side of any silhouette.
|
|
18
|
+
*
|
|
15
19
|
* **Normals, for the case depth cannot see.** A leaf's two sides are at one depth, and the history of
|
|
16
20
|
* the front is not the back. The forward frame has no normal buffer, so both normals come from their
|
|
17
21
|
* frame's depth — `worldPositionFromDepth` and `normalFromPositions` — in world space, where the two
|
|
18
|
-
* frames agree.
|
|
22
|
+
* frames agree. **A normal differenced across an edge is not a normal**, so the caller weighs each by
|
|
23
|
+
* `planarity`: at an edge, a sliver, or anything finer than a texel the depth test decides alone.
|
|
19
24
|
*
|
|
20
25
|
* **Continuous in every input**, because a step is a line on screen along which the history is kept on
|
|
21
26
|
* one frame and dropped on the next: every threshold is a smoothstep, never a comparison.
|
|
@@ -38,14 +43,42 @@ export interface DisocclusionParams {
|
|
|
38
43
|
* normals trusted fully within sixty degrees and not at all past ninety.
|
|
39
44
|
*/
|
|
40
45
|
export declare const DEFAULT_DISOCCLUSION: DisocclusionParams;
|
|
46
|
+
/**
|
|
47
|
+
* The band over which three texels stop being one plane: how far their two steps in inverse depth
|
|
48
|
+
* fail to cancel, as a share of the two together. None of it is a plane at any angle; all of it is an
|
|
49
|
+
* edge — one side steps, the other does not — or a sliver, both stepping the same way.
|
|
50
|
+
*
|
|
51
|
+
* **A share and not a size**, which is the lesson of the rods in `resolve.test.ts`: a part a
|
|
52
|
+
* hundredth of the depth in front of a wall passes the depth test, and a normal differenced across it
|
|
53
|
+
* still leans by its height over a texel's width — nearly edge-on where texels are small. What would
|
|
54
|
+
* make this wrong is a curved surface judged at its apex, where both steps lean one way and the
|
|
55
|
+
* normal is sound; there it only gives the normals less say, which the depth test does not need.
|
|
56
|
+
*/
|
|
57
|
+
export declare const PLANAR_FROM = 0.25;
|
|
58
|
+
export declare const PLANAR_TO = 0.5;
|
|
59
|
+
/**
|
|
60
|
+
* How sure it is that `centre` and its neighbours either side along one axis lie on one plane, one to
|
|
61
|
+
* zero. Inverse depth is affine across a plane on screen, so a plane's two steps cancel whatever its
|
|
62
|
+
* angle. One where a neighbour is missing — zero, off the picture or undrawn — or where there is no
|
|
63
|
+
* step to compare: nothing to judge by, so a normal keeps the say it always had.
|
|
64
|
+
*/
|
|
65
|
+
export declare function planarity(before: number, centre: number, after: number): number;
|
|
66
|
+
/**
|
|
67
|
+
* How far a surface's depth moves across one texel along an axis, relative to the depth: the smaller
|
|
68
|
+
* of the steps to its two neighbours, because beside a silhouette one of them is the surface behind.
|
|
69
|
+
* Zero at a sliver — nearer or farther than both, so each step is to another surface — and one
|
|
70
|
+
* neighbour's step where only one is there.
|
|
71
|
+
*/
|
|
72
|
+
export declare function surfaceSlope(before: number, centre: number, after: number): number;
|
|
41
73
|
/**
|
|
42
74
|
* One where the history is trustworthy, zero where it is not, and continuous between.
|
|
43
75
|
*
|
|
44
76
|
* `currDepth` is the view depth this surface should have had last frame; `histDepth` the view depth
|
|
45
77
|
* last frame held where it lands, or zero where it landed off the screen; `motionLength` the motion's
|
|
46
|
-
* length in uv; `normalDot` the cosine between the two frames' world normals there
|
|
78
|
+
* length in uv; `normalDot` the cosine between the two frames' world normals there; `slope` the
|
|
79
|
+
* surface's `surfaceSlope`, which widens the tolerance one for one.
|
|
47
80
|
*/
|
|
48
|
-
export declare function disocclusionWeight(currDepth: number, histDepth: number, motionLength: number, normalDot: number, params: DisocclusionParams): number;
|
|
81
|
+
export declare function disocclusionWeight(currDepth: number, histDepth: number, motionLength: number, normalDot: number, params: DisocclusionParams, slope?: number): number;
|
|
49
82
|
/**
|
|
50
83
|
* The world position a depth stands for: `(u, v, clipDepth)` through the inverse view-projection,
|
|
51
84
|
* in whatever depth convention that matrix was built for. Returns false where the matrix has no
|
|
@@ -12,10 +12,15 @@
|
|
|
12
12
|
* a different part of a texel than the one it stands for, which at a grazing angle is a different
|
|
13
13
|
* depth; the tolerance grows with the motion's length.
|
|
14
14
|
*
|
|
15
|
+
* **And in proportion to the surface's own slope.** A new jitter samples a surface somewhere else, so
|
|
16
|
+
* on one turned from the eye two frames' samples of it differ by up to a texel's worth of its depth
|
|
17
|
+
* with nothing moved. `surfaceSlope` measures that step on the surface's own side of any silhouette.
|
|
18
|
+
*
|
|
15
19
|
* **Normals, for the case depth cannot see.** A leaf's two sides are at one depth, and the history of
|
|
16
20
|
* the front is not the back. The forward frame has no normal buffer, so both normals come from their
|
|
17
21
|
* frame's depth — `worldPositionFromDepth` and `normalFromPositions` — in world space, where the two
|
|
18
|
-
* frames agree.
|
|
22
|
+
* frames agree. **A normal differenced across an edge is not a normal**, so the caller weighs each by
|
|
23
|
+
* `planarity`: at an edge, a sliver, or anything finer than a texel the depth test decides alone.
|
|
19
24
|
*
|
|
20
25
|
* **Continuous in every input**, because a step is a line on screen along which the history is kept on
|
|
21
26
|
* one frame and dropped on the next: every threshold is a smoothstep, never a comparison.
|
|
@@ -37,19 +42,66 @@ function smoothstep(edge0, edge1, x) {
|
|
|
37
42
|
const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));
|
|
38
43
|
return t * t * (3 - 2 * t);
|
|
39
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The band over which three texels stop being one plane: how far their two steps in inverse depth
|
|
47
|
+
* fail to cancel, as a share of the two together. None of it is a plane at any angle; all of it is an
|
|
48
|
+
* edge — one side steps, the other does not — or a sliver, both stepping the same way.
|
|
49
|
+
*
|
|
50
|
+
* **A share and not a size**, which is the lesson of the rods in `resolve.test.ts`: a part a
|
|
51
|
+
* hundredth of the depth in front of a wall passes the depth test, and a normal differenced across it
|
|
52
|
+
* still leans by its height over a texel's width — nearly edge-on where texels are small. What would
|
|
53
|
+
* make this wrong is a curved surface judged at its apex, where both steps lean one way and the
|
|
54
|
+
* normal is sound; there it only gives the normals less say, which the depth test does not need.
|
|
55
|
+
*/
|
|
56
|
+
export const PLANAR_FROM = 0.25;
|
|
57
|
+
export const PLANAR_TO = 0.5;
|
|
58
|
+
/**
|
|
59
|
+
* How sure it is that `centre` and its neighbours either side along one axis lie on one plane, one to
|
|
60
|
+
* zero. Inverse depth is affine across a plane on screen, so a plane's two steps cancel whatever its
|
|
61
|
+
* angle. One where a neighbour is missing — zero, off the picture or undrawn — or where there is no
|
|
62
|
+
* step to compare: nothing to judge by, so a normal keeps the say it always had.
|
|
63
|
+
*/
|
|
64
|
+
export function planarity(before, centre, after) {
|
|
65
|
+
if (!(before > 0) || !(centre > 0) || !(after > 0))
|
|
66
|
+
return 1;
|
|
67
|
+
const a = 1 / after - 1 / centre;
|
|
68
|
+
const b = 1 / before - 1 / centre;
|
|
69
|
+
const spread = Math.abs(a) + Math.abs(b);
|
|
70
|
+
if (!(spread > 0))
|
|
71
|
+
return 1;
|
|
72
|
+
return 1 - smoothstep(PLANAR_FROM, PLANAR_TO, Math.abs(a + b) / spread);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* How far a surface's depth moves across one texel along an axis, relative to the depth: the smaller
|
|
76
|
+
* of the steps to its two neighbours, because beside a silhouette one of them is the surface behind.
|
|
77
|
+
* Zero at a sliver — nearer or farther than both, so each step is to another surface — and one
|
|
78
|
+
* neighbour's step where only one is there.
|
|
79
|
+
*/
|
|
80
|
+
export function surfaceSlope(before, centre, after) {
|
|
81
|
+
const a = after > 0 ? after - centre : Number.NaN;
|
|
82
|
+
const b = before > 0 ? before - centre : Number.NaN;
|
|
83
|
+
if (Number.isNaN(a))
|
|
84
|
+
return Number.isNaN(b) ? 0 : Math.abs(b) / centre;
|
|
85
|
+
if (Number.isNaN(b))
|
|
86
|
+
return Math.abs(a) / centre;
|
|
87
|
+
if (a * b > 0)
|
|
88
|
+
return 0;
|
|
89
|
+
return Math.min(Math.abs(a), Math.abs(b)) / centre;
|
|
90
|
+
}
|
|
40
91
|
/**
|
|
41
92
|
* One where the history is trustworthy, zero where it is not, and continuous between.
|
|
42
93
|
*
|
|
43
94
|
* `currDepth` is the view depth this surface should have had last frame; `histDepth` the view depth
|
|
44
95
|
* last frame held where it lands, or zero where it landed off the screen; `motionLength` the motion's
|
|
45
|
-
* length in uv; `normalDot` the cosine between the two frames' world normals there
|
|
96
|
+
* length in uv; `normalDot` the cosine between the two frames' world normals there; `slope` the
|
|
97
|
+
* surface's `surfaceSlope`, which widens the tolerance one for one.
|
|
46
98
|
*/
|
|
47
|
-
export function disocclusionWeight(currDepth, histDepth, motionLength, normalDot, params) {
|
|
99
|
+
export function disocclusionWeight(currDepth, histDepth, motionLength, normalDot, params, slope = 0) {
|
|
48
100
|
if (!(currDepth > 0) || !(histDepth > 0) || !(motionLength >= 0) || Number.isNaN(normalDot)) {
|
|
49
101
|
return 0;
|
|
50
102
|
}
|
|
51
103
|
const disagreement = Math.abs(currDepth - histDepth) / currDepth;
|
|
52
|
-
const tolerance = params.depthTolerance + params.motionScale * motionLength;
|
|
104
|
+
const tolerance = params.depthTolerance + params.motionScale * motionLength + slope;
|
|
53
105
|
const depth = 1 - smoothstep(tolerance, 2 * tolerance, disagreement);
|
|
54
106
|
const normal = smoothstep(params.normalFloor, params.normalCeiling, normalDot);
|
|
55
107
|
return depth * normal;
|
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
* previous view depth in its third channel; otherwise it is the camera's, through the surface's
|
|
22
22
|
* world position.
|
|
23
23
|
* 4. Where the surface was is read from the history — its colour and the weight it had gathered — and
|
|
24
|
-
* last frame's depth
|
|
25
|
-
*
|
|
24
|
+
* last frame's depth says whether it is the same surface: the nearest of the nine texels around
|
|
25
|
+
* where the dilated texel was, forgiven its own slope across a texel. The two frames' normals, from
|
|
26
|
+
* their depths, catch thin geometry, each with the say its planarity gives it. The disocclusion
|
|
27
|
+
* scales the history's weight.
|
|
26
28
|
* 5. The history is clipped to the box, its weight capped so a tenth of each frame is always new, and
|
|
27
29
|
* the two are mixed by their weights. Where the pixel has gathered less than one sample's worth,
|
|
28
30
|
* the render read bicubically fills the gap. The history keeps colour and weight; the picture shown
|
|
@@ -33,7 +35,7 @@
|
|
|
33
35
|
* buffer before a sample reaches this arithmetic, as the temporal resolve already does.
|
|
34
36
|
*/
|
|
35
37
|
import { clipToBox, rgbToYCoCg, varianceBox, yCoCgToRgb } from './clamp.js';
|
|
36
|
-
import { disocclusionWeight, normalFromPositions, worldPositionFromDepth } from './disocclusion.js';
|
|
38
|
+
import { disocclusionWeight, normalFromPositions, planarity, surfaceSlope, worldPositionFromDepth, } from './disocclusion.js';
|
|
37
39
|
import { sampleBilinear, sampleHistoryBicubic5 } from './reproject.js';
|
|
38
40
|
/**
|
|
39
41
|
* A tenth of each frame kept, as the temporal resolve keeps; a box of a standard deviation and a
|
|
@@ -131,6 +133,39 @@ function worldAt(depths, inverse, jitter, width, height, x, y, out) {
|
|
|
131
133
|
m[15];
|
|
132
134
|
return 1 / w;
|
|
133
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* A texel's view depth at its unjittered uv, or 0 — `worldAt` without the position, which is all a
|
|
138
|
+
* search over depths needs: one row of the inverse rather than four. Zero off the picture, where
|
|
139
|
+
* `worldAt` would clamp, because a neighbour that is not there is not a step.
|
|
140
|
+
*/
|
|
141
|
+
function viewDepthAt(depths, inverse, jitter, width, height, x, y) {
|
|
142
|
+
if (x < 0 || y < 0 || x >= width || y >= height)
|
|
143
|
+
return 0;
|
|
144
|
+
const u = (x + 0.5 - jitter[0]) / width;
|
|
145
|
+
const v = (y + 0.5 - jitter[1]) / height;
|
|
146
|
+
const m = inverse;
|
|
147
|
+
const w = m[3] * (u * 2 - 1) +
|
|
148
|
+
m[7] * (v * 2 - 1) +
|
|
149
|
+
m[11] * depths[y * width + x] +
|
|
150
|
+
m[15];
|
|
151
|
+
return Math.abs(w) > 1e-12 ? 1 / w : 0;
|
|
152
|
+
}
|
|
153
|
+
/** What a texel's four neighbours say about its surface: see `planarity` and `surfaceSlope`. */
|
|
154
|
+
const SURFACE = { planar: 1, slope: 0 };
|
|
155
|
+
function surfaceAt(depths, inverse, jitter, width, height, x, y, centre) {
|
|
156
|
+
let planar = 1;
|
|
157
|
+
let slope = 0;
|
|
158
|
+
for (let axis = 0; axis < 2; axis += 1) {
|
|
159
|
+
const ax = 1 - axis;
|
|
160
|
+
const ay = axis;
|
|
161
|
+
const after = viewDepthAt(depths, inverse, jitter, width, height, x + ax, y + ay);
|
|
162
|
+
const before = viewDepthAt(depths, inverse, jitter, width, height, x - ax, y - ay);
|
|
163
|
+
planar *= planarity(before, centre, after);
|
|
164
|
+
slope = Math.max(slope, surfaceSlope(before, centre, after));
|
|
165
|
+
}
|
|
166
|
+
SURFACE.planar = planar;
|
|
167
|
+
SURFACE.slope = slope;
|
|
168
|
+
}
|
|
134
169
|
/** The world normal at a texel, from its depth and its right and upper neighbours'. */
|
|
135
170
|
function normalAt(depths, inverse, jitter, eye, width, height, x, y, out) {
|
|
136
171
|
if (worldAt(depths, inverse, jitter, width, height, x, y, WORLD) <= 0)
|
|
@@ -190,18 +225,66 @@ function historyWeight(frame, u, v, dx, dy) {
|
|
|
190
225
|
if (!(pu >= 0 && pu <= 1 && pv >= 0 && pv <= 1))
|
|
191
226
|
return 0;
|
|
192
227
|
sampleHistoryBicubic5(frame.history, pu * frame.outputWidth - 0.5, pv * frame.outputHeight - 0.5, frame.outputWidth, frame.outputHeight, 4, HISTORY);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
228
|
+
/*
|
|
229
|
+
* **Last frame's depth is searched for this surface, around where the dilated texel was.** The
|
|
230
|
+
* surface being judged is the dilated texel's, which beside an edge is a neighbour's, so judging
|
|
231
|
+
* it by the texel under the pixel compares two surfaces: the side of every silhouette away from the
|
|
232
|
+
* eye dropped its history on every frame of a still picture. And a new jitter samples a surface
|
|
233
|
+
* somewhere else, so the nearest of last frame's nine is taken rather than the one the rounding
|
|
234
|
+
* picks. What it gives up is a disocclusion narrower than a texel, which keeps a history that the
|
|
235
|
+
* neighbourhood clip then bounds.
|
|
236
|
+
*/
|
|
237
|
+
const su = (dx + 0.5 - frame.jitter[0]) / rw + mu;
|
|
238
|
+
const sv = (dy + 0.5 - frame.jitter[1]) / rh + mv;
|
|
239
|
+
const cx = clampTexel(Math.round(su * rw + frame.previousJitter[0] - 0.5), rw);
|
|
240
|
+
const cy = clampTexel(Math.round(sv * rh + frame.previousJitter[1] - 0.5), rh);
|
|
241
|
+
/*
|
|
242
|
+
* The first texel looked at answers alone when it agrees within the tolerance, which it does
|
|
243
|
+
* everywhere but at an edge: the depth test trusts it fully whichever texel would have won, so
|
|
244
|
+
* only an edge pays for the other eight.
|
|
245
|
+
*/
|
|
246
|
+
let held = viewDepthAt(frame.previousDepth, frame.previousInverseViewProj, frame.previousJitter, rw, rh, cx, cy);
|
|
247
|
+
let px = cx;
|
|
248
|
+
let py = cy;
|
|
249
|
+
if (!(held > 0 && Math.abs(held - expected) <= frame.disocclusion.depthTolerance * expected)) {
|
|
250
|
+
held = 0;
|
|
251
|
+
let nearest = Number.POSITIVE_INFINITY;
|
|
252
|
+
for (let j = -1; j <= 1; j += 1) {
|
|
253
|
+
for (let i = -1; i <= 1; i += 1) {
|
|
254
|
+
const tx = clampTexel(cx + i, rw);
|
|
255
|
+
const ty = clampTexel(cy + j, rh);
|
|
256
|
+
const depth = viewDepthAt(frame.previousDepth, frame.previousInverseViewProj, frame.previousJitter, rw, rh, tx, ty);
|
|
257
|
+
if (!(depth > 0))
|
|
258
|
+
continue;
|
|
259
|
+
const off = Math.abs(depth - expected);
|
|
260
|
+
if (off < nearest) {
|
|
261
|
+
nearest = off;
|
|
262
|
+
held = depth;
|
|
263
|
+
px = tx;
|
|
264
|
+
py = ty;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const here = viewDepthAt(frame.depth, frame.inverseViewProj, frame.jitter, rw, rh, dx, dy);
|
|
270
|
+
surfaceAt(frame.depth, frame.inverseViewProj, frame.jitter, rw, rh, dx, dy, here);
|
|
271
|
+
const slope = SURFACE.slope;
|
|
272
|
+
let planar = SURFACE.planar;
|
|
273
|
+
if (planar > 0) {
|
|
274
|
+
surfaceAt(frame.previousDepth, frame.previousInverseViewProj, frame.previousJitter, rw, rh, px, py, held);
|
|
275
|
+
planar *= SURFACE.planar;
|
|
276
|
+
}
|
|
277
|
+
/* A normal has the say its planarity gives it; at an edge or a sliver, none, and none is computed. */
|
|
196
278
|
let normalDot = 1;
|
|
197
|
-
if (
|
|
279
|
+
if (planar > 0 &&
|
|
280
|
+
normalAt(frame.depth, frame.inverseViewProj, frame.jitter, frame.eye, rw, rh, dx, dy, NORMAL) &&
|
|
198
281
|
normalAt(frame.previousDepth, frame.previousInverseViewProj, frame.previousJitter, frame.previousEye, rw, rh, px, py, PREVIOUS_NORMAL)) {
|
|
199
|
-
|
|
200
|
-
NORMAL[
|
|
201
|
-
|
|
202
|
-
|
|
282
|
+
const cosine = NORMAL[0] * PREVIOUS_NORMAL[0] +
|
|
283
|
+
NORMAL[1] * PREVIOUS_NORMAL[1] +
|
|
284
|
+
NORMAL[2] * PREVIOUS_NORMAL[2];
|
|
285
|
+
normalDot = 1 - planar * (1 - cosine);
|
|
203
286
|
}
|
|
204
|
-
const trust = disocclusionWeight(expected, held, Math.hypot(mu, mv), normalDot, frame.disocclusion);
|
|
287
|
+
const trust = disocclusionWeight(expected, held, Math.hypot(mu, mv), normalDot, frame.disocclusion, slope);
|
|
205
288
|
return HISTORY[3] * trust;
|
|
206
289
|
}
|
|
207
290
|
/**
|
|
@@ -303,6 +303,8 @@ export declare class SceneTarget {
|
|
|
303
303
|
}, veilColor?: Vec3, veilAlpha?: number, temporal?: {
|
|
304
304
|
readonly reprojection: Float32Array;
|
|
305
305
|
readonly blend: number;
|
|
306
|
+
readonly periodStart: boolean;
|
|
307
|
+
readonly depthToViewZ: Float32Array;
|
|
306
308
|
}): void;
|
|
307
309
|
dispose(): void;
|
|
308
310
|
}
|
|
@@ -711,7 +711,7 @@ export class SceneTarget {
|
|
|
711
711
|
let scene = this.texture;
|
|
712
712
|
if (temporal !== undefined && this.texture !== null && this.depth !== null) {
|
|
713
713
|
this.temporal ??= new TemporalPass(gl, this.floatColor);
|
|
714
|
-
scene = this.temporal.temporalResolve(this.texture, this.depth, temporal.reprojection, this.width, this.height, temporal.blend);
|
|
714
|
+
scene = this.temporal.temporalResolve(this.texture, this.depth, temporal.reprojection, this.width, this.height, temporal.blend, temporal.periodStart, temporal.depthToViewZ);
|
|
715
715
|
}
|
|
716
716
|
let bloomTexture = null;
|
|
717
717
|
if (bloom !== undefined && scene !== null) {
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
* wherever the depth test cuts the window short, as a dotted line along every edge.
|
|
25
25
|
*/
|
|
26
26
|
/**
|
|
27
|
-
* The estimate. Writes one channel
|
|
27
|
+
* The estimate. Writes one channel at `AO_STORE`: that is open sky, 0 is fully enclosed, and the
|
|
28
|
+
* second axis of the blur decodes it.
|
|
28
29
|
*
|
|
29
30
|
* **Horizons, not neighbours: ground-truth ambient occlusion (Jimenez et al. 2016).** Each pixel
|
|
30
31
|
* walks two lines across the screen, both ways, and keeps the highest thing it can see along each
|
|
@@ -62,3 +63,40 @@ export declare function gtaoArc(h0: number, h1: number, normal: number): number;
|
|
|
62
63
|
export declare function nextHorizon(horizon: number, cosine: number): number;
|
|
63
64
|
/** The shader's thickness heuristic. See `AO_THIN` inside `AO_FRAG`. */
|
|
64
65
|
export declare const AO_THIN = 0.2;
|
|
66
|
+
/**
|
|
67
|
+
* What the estimate is multiplied by before it is stored, and divided by once the blur is done.
|
|
68
|
+
*
|
|
69
|
+
* **Half, because an unblurred estimate is allowed past one.** A pixel's two lines see a plane
|
|
70
|
+
* turned away from the eye from two directions, and the closed form credits one of them more than
|
|
71
|
+
* the whole sky and the other less: it is the average over every turn that is exactly one, and the
|
|
72
|
+
* average is what the blur is for. Stored as it came, an eight-bit target clipped every turn above
|
|
73
|
+
* one before the blur could average it, and every surface seen edge on came back a few percent
|
|
74
|
+
* shaded with nothing on it. A line reaches π/2 at most, so half fits the whole range.
|
|
75
|
+
*
|
|
76
|
+
* What it gives up is one bit of the intermediate: steps of 1/128 rather than 1/256 until the
|
|
77
|
+
* second axis decodes, which the eight taps a pixel averages carry below what a frame can show.
|
|
78
|
+
*/
|
|
79
|
+
export declare const AO_STORE = 0.5;
|
|
80
|
+
/** How far a neighbour may sit from this pixel's surface, as a fraction of its distance. */
|
|
81
|
+
export declare const AO_BLUR_DEPTH_TOLERANCE = 0.02;
|
|
82
|
+
/**
|
|
83
|
+
* The blur's weight for a neighbour `offset` pixels along the axis, from view depths: this pixel's
|
|
84
|
+
* own, the two either side of it, and the neighbour's. The shader's lines written out again.
|
|
85
|
+
*
|
|
86
|
+
* **The neighbour is compared with this pixel's plane, not with this pixel's depth.** Comparing
|
|
87
|
+
* depths refused every neighbour of a surface seen edge on: a floor forty metres out changes its
|
|
88
|
+
* depth by four percent a row, twice the tolerance, so the pass down the frame kept the centre alone
|
|
89
|
+
* and left the rotation tile's rows standing — four-pixel stripes across every ramp and far floor,
|
|
90
|
+
* reported as strips on a track. On a plane 1/z is affine in the screen, so one step of it predicts
|
|
91
|
+
* the plane's depth at every tap exactly, and a tap is refused only for leaving the plane.
|
|
92
|
+
*
|
|
93
|
+
* The step is taken from whichever neighbour is closer to the centre in 1/z. At a silhouette that is
|
|
94
|
+
* the one on this pixel's own side, so the far surface cannot bend the prediction toward it; on a
|
|
95
|
+
* plane the two agree to a quantisation step, which moves a weight by nothing a frame shows. What it
|
|
96
|
+
* gives up is a crease: past the fold between a floor and a wall the prediction runs on along the
|
|
97
|
+
* floor, so the wall's taps fade out over the tolerance rather than being kept.
|
|
98
|
+
*
|
|
99
|
+
* **Written here rather than in the shader**, because a comment inside the GLSL is part of the string
|
|
100
|
+
* a consumer ships.
|
|
101
|
+
*/
|
|
102
|
+
export declare function blurWeight(centre: number, before: number, after: number, neighbour: number, offset: number): number;
|