@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
|
@@ -331,7 +331,37 @@ export function postPipeline(
|
|
|
331
331
|
format: GPUTextureFormat,
|
|
332
332
|
blend?: GPUBlendState,
|
|
333
333
|
): GPURenderPipeline {
|
|
334
|
-
return cache.get(key, () =>
|
|
334
|
+
return cache.get(key, () =>
|
|
335
|
+
describePost(device, layout, key, fragment, [
|
|
336
|
+
blend === undefined ? { format } : { format, blend },
|
|
337
|
+
]),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The same over several colour targets, in the fragment's output order: the temporal resolve
|
|
343
|
+
* writes its picture and each pixel's flicker record in one pass. `targets` is the caller's, held
|
|
344
|
+
* rather than built per call, since this is asked for every frame.
|
|
345
|
+
*/
|
|
346
|
+
export function postPipelineTargets(
|
|
347
|
+
cache: PipelineCache,
|
|
348
|
+
device: GPUDevice,
|
|
349
|
+
layout: GPUBindGroupLayout,
|
|
350
|
+
key: string,
|
|
351
|
+
fragment: string,
|
|
352
|
+
targets: GPUColorTargetState[],
|
|
353
|
+
): GPURenderPipeline {
|
|
354
|
+
return cache.get(key, () => describePost(device, layout, key, fragment, targets));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function describePost(
|
|
358
|
+
device: GPUDevice,
|
|
359
|
+
layout: GPUBindGroupLayout,
|
|
360
|
+
key: string,
|
|
361
|
+
fragment: string,
|
|
362
|
+
targets: GPUColorTargetState[],
|
|
363
|
+
): GPURenderPipelineDescriptor {
|
|
364
|
+
return {
|
|
335
365
|
label: key,
|
|
336
366
|
layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
|
|
337
367
|
vertex: {
|
|
@@ -341,7 +371,7 @@ export function postPipeline(
|
|
|
341
371
|
fragment: {
|
|
342
372
|
module: shaderModule(device, { label: key, code: fragment }),
|
|
343
373
|
entryPoint: 'main',
|
|
344
|
-
targets
|
|
374
|
+
targets,
|
|
345
375
|
},
|
|
346
376
|
/*
|
|
347
377
|
* No depth at all, which is what makes this different from every other pipeline here: the
|
|
@@ -349,11 +379,11 @@ export function postPipeline(
|
|
|
349
379
|
* disables the test, the write and culling by hand for the same draw.
|
|
350
380
|
*/
|
|
351
381
|
primitive: { topology: 'triangle-list' as const, cullMode: 'none' as const },
|
|
352
|
-
}
|
|
382
|
+
};
|
|
353
383
|
}
|
|
354
384
|
|
|
355
385
|
/**
|
|
356
|
-
* The temporal resolve's block and its
|
|
386
|
+
* The temporal resolve's block and its four inputs.
|
|
357
387
|
*
|
|
358
388
|
* Named like the occlusion's beside it, and for the same reason `BLOOM_DOWNSAMPLE_FIELDS` is: a
|
|
359
389
|
* block read by assumption rather than by name is a uniform written to the wrong offset, which
|
|
@@ -181,6 +181,7 @@ import { reflectionTargetSize, type ReflectionSize } from '../../planarReflectio
|
|
|
181
181
|
import { drawingBufferSize } from '../../drawingBuffer.ts';
|
|
182
182
|
import { MOTION_BLUR_MAX_UV, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV } from '../../vertexDefaults.ts';
|
|
183
183
|
import { ExposurePass } from './exposurePass.ts';
|
|
184
|
+
import { AO_STORE } from '../../shaders/ambientOcclusion.ts';
|
|
184
185
|
import { clampAutoExposure } from '../../shaders/exposure.ts';
|
|
185
186
|
import { clampLocalExposure } from '../../shaders/localExposure.ts';
|
|
186
187
|
import type { UniformFields } from './scatterPass.ts';
|
|
@@ -189,6 +190,7 @@ import {
|
|
|
189
190
|
createPostStageLayout,
|
|
190
191
|
depthResolvePipeline,
|
|
191
192
|
postPipeline,
|
|
193
|
+
postPipelineTargets,
|
|
192
194
|
sceneColorFormat,
|
|
193
195
|
AO_BLUR_FIELDS,
|
|
194
196
|
AO_BLUR_FRAG_WGSL,
|
|
@@ -1925,6 +1927,16 @@ export class WebGPURenderer implements RendererApi {
|
|
|
1925
1927
|
private taaUniforms!: GPUBuffer;
|
|
1926
1928
|
private taaTextures: (GPUTexture | null)[] = [null, null];
|
|
1927
1929
|
private taaViews: (GPUTextureView | null)[] = [null, null];
|
|
1930
|
+
/** Each pixel's flicker record, paired with the history and swapped with it: see `temporalAa.ts`. */
|
|
1931
|
+
private taaRecords: (GPUTexture | null)[] = [null, null];
|
|
1932
|
+
private taaRecordViews: (GPUTextureView | null)[] = [null, null];
|
|
1933
|
+
/** The rest of each pixel's record — depth, pull and recent swing — beside the first. */
|
|
1934
|
+
private taaMotions: (GPUTexture | null)[] = [null, null];
|
|
1935
|
+
private taaMotionViews: (GPUTextureView | null)[] = [null, null];
|
|
1936
|
+
/** The resolve's depth-to-metres row, from the inverse of `frameProjection`. */
|
|
1937
|
+
private readonly taaInvProjection = new Float32Array(16);
|
|
1938
|
+
/** The resolve's two targets, the picture and the record, held for the pipeline cache's sake. */
|
|
1939
|
+
private taaTargets: GPUColorTargetState[] = [];
|
|
1928
1940
|
/** Group `i` reads history `i` and the pass writes into the other one. */
|
|
1929
1941
|
private taaBindGroups: (GPUBindGroup | null)[] = [null, null];
|
|
1930
1942
|
private taaWrite = 0;
|
|
@@ -6394,6 +6406,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6394
6406
|
this.taaLayout = createPostStageLayout(device, TAA_UNIFORMS, TAA_FRAG_SIZE, [
|
|
6395
6407
|
{ binding: TAA_TEXTURES.uScene },
|
|
6396
6408
|
{ binding: TAA_TEXTURES.uHistory },
|
|
6409
|
+
{ binding: TAA_TEXTURES.uFlicker },
|
|
6410
|
+
{ binding: TAA_TEXTURES.uFlickerMotion },
|
|
6397
6411
|
{ binding: TAA_TEXTURES.uDepth, filterable: false },
|
|
6398
6412
|
]);
|
|
6399
6413
|
this.taaUniforms = device.createBuffer({
|
|
@@ -10467,8 +10481,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10467
10481
|
if (!this.temporalJittering) return;
|
|
10468
10482
|
const write = this.taaWrite;
|
|
10469
10483
|
const target = this.taaViews[write];
|
|
10484
|
+
const record = this.taaRecordViews[write];
|
|
10485
|
+
const motion = this.taaMotionViews[write];
|
|
10470
10486
|
const group = this.taaBindGroups[1 - write];
|
|
10471
|
-
if (target === null || group === null) return;
|
|
10487
|
+
if (target === null || record === null || motion === null || group === null) return;
|
|
10472
10488
|
const { device } = this.surface;
|
|
10473
10489
|
|
|
10474
10490
|
const f = this.taaFloats;
|
|
@@ -10477,6 +10493,22 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10477
10493
|
f[at('uTexel')] = 1 / this.renderWidth;
|
|
10478
10494
|
f[at('uTexel') + 1] = 1 / this.renderHeight;
|
|
10479
10495
|
f[at('uHistoryBlend')] = this.temporalHistoryUsable ? TEMPORAL_HISTORY_BLEND : 0;
|
|
10496
|
+
f[at('uPeriodStart')] = this.temporalHistory.periodStart ? 1 : 0;
|
|
10497
|
+
/* The anti-flicker's same-surface test reads depths in metres, as the occlusion does. */
|
|
10498
|
+
const depthRow = at('uDepthToViewZ');
|
|
10499
|
+
if (this.frameProjection !== null) {
|
|
10500
|
+
mat4.invert(this.taaInvProjection, this.frameProjection);
|
|
10501
|
+
f[depthRow] = this.taaInvProjection[10] ?? 0;
|
|
10502
|
+
f[depthRow + 1] = this.taaInvProjection[14] ?? 0;
|
|
10503
|
+
f[depthRow + 2] = this.taaInvProjection[11] ?? 0;
|
|
10504
|
+
f[depthRow + 3] = this.taaInvProjection[15] ?? 1;
|
|
10505
|
+
} else {
|
|
10506
|
+
f[depthRow] = 0;
|
|
10507
|
+
f[depthRow + 1] = 0;
|
|
10508
|
+
f[depthRow + 2] = 0;
|
|
10509
|
+
f[depthRow + 3] = 1;
|
|
10510
|
+
}
|
|
10511
|
+
f[at('uAntiFlicker')] = 1;
|
|
10480
10512
|
device.queue.writeBuffer(this.taaUniforms, 0, this.taaStaging);
|
|
10481
10513
|
|
|
10482
10514
|
const pass = encoder.beginRenderPass({
|
|
@@ -10484,16 +10516,18 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10484
10516
|
timestampWrites: this.gpuTimer.writesFor(),
|
|
10485
10517
|
colorAttachments: [
|
|
10486
10518
|
{ view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 1] },
|
|
10519
|
+
{ view: record, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
|
|
10520
|
+
{ view: motion, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
|
|
10487
10521
|
],
|
|
10488
10522
|
});
|
|
10489
10523
|
pass.setPipeline(
|
|
10490
|
-
|
|
10524
|
+
postPipelineTargets(
|
|
10491
10525
|
this.pipelines,
|
|
10492
10526
|
device,
|
|
10493
10527
|
this.taaLayout,
|
|
10494
10528
|
'post.taa',
|
|
10495
10529
|
TEMPORAL_RESOLVE_FRAG_WGSL,
|
|
10496
|
-
|
|
10530
|
+
this.taaTargets,
|
|
10497
10531
|
),
|
|
10498
10532
|
);
|
|
10499
10533
|
pass.setBindGroup(0, group, [0]);
|
|
@@ -11045,12 +11079,15 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11045
11079
|
otherwise halve the blur and leave half the pattern standing. */
|
|
11046
11080
|
const width = Math.max(1, this.aoTarget?.width ?? 1);
|
|
11047
11081
|
const height = Math.max(1, this.aoTarget?.height ?? 1);
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
|
|
11082
|
+
/* Across at the estimate's own scale, which the second axis needs whole; down decoded, so the
|
|
11083
|
+
composite reads open sky as one. The same two `ambientOcclusionPass.ts` writes. */
|
|
11084
|
+
for (const [index, step, scale] of [
|
|
11085
|
+
[0, [1 / width, 0], 1],
|
|
11086
|
+
[1, [0, 1 / height], 1 / AO_STORE],
|
|
11051
11087
|
] as const) {
|
|
11052
11088
|
f[slot(index) + at('uStep')] = step[0];
|
|
11053
11089
|
f[slot(index) + at('uStep') + 1] = step[1];
|
|
11090
|
+
f[slot(index) + at('uAoScale')] = scale;
|
|
11054
11091
|
f[slot(index) + at('uDepthToViewZ')] = inv[10] ?? 0;
|
|
11055
11092
|
f[slot(index) + at('uDepthToViewZ') + 1] = inv[14] ?? 0;
|
|
11056
11093
|
f[slot(index) + at('uDepthToViewZ') + 2] = inv[11] ?? 0;
|
|
@@ -14547,7 +14584,28 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14547
14584
|
});
|
|
14548
14585
|
this.taaTextures[i] = texture;
|
|
14549
14586
|
this.taaViews[i] = texture.createView();
|
|
14587
|
+
/* The records in half float, whatever the picture's format: one of them holds metres. */
|
|
14588
|
+
this.taaRecords[i]?.destroy();
|
|
14589
|
+
const record = device.createTexture({
|
|
14590
|
+
label: `post.taaRecord${i}`,
|
|
14591
|
+
size: [width, height],
|
|
14592
|
+
format: 'rgba16float',
|
|
14593
|
+
usage: USAGE,
|
|
14594
|
+
});
|
|
14595
|
+
this.taaRecords[i] = record;
|
|
14596
|
+
this.taaRecordViews[i] = record.createView();
|
|
14597
|
+
this.taaMotions[i]?.destroy();
|
|
14598
|
+
/* Two numbers a pixel: the depth its sample stood at, in metres, and how far that swings. */
|
|
14599
|
+
const motion = device.createTexture({
|
|
14600
|
+
label: `post.taaMotion${i}`,
|
|
14601
|
+
size: [width, height],
|
|
14602
|
+
format: 'rg16float',
|
|
14603
|
+
usage: USAGE,
|
|
14604
|
+
});
|
|
14605
|
+
this.taaMotions[i] = motion;
|
|
14606
|
+
this.taaMotionViews[i] = motion.createView();
|
|
14550
14607
|
}
|
|
14608
|
+
this.taaTargets = [{ format }, { format: 'rgba16float' }, { format: 'rg16float' }];
|
|
14551
14609
|
this.taaWrite = 0;
|
|
14552
14610
|
/* A resized target is a new texture, so whatever the history held is gone. */
|
|
14553
14611
|
this.temporalHistory.invalidate();
|
|
@@ -14743,7 +14801,17 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14743
14801
|
const scene = this.sceneColorView;
|
|
14744
14802
|
const history = this.taaViews[i];
|
|
14745
14803
|
const resolved = this.taaViews[1 - i];
|
|
14746
|
-
|
|
14804
|
+
const record = this.taaRecordViews[i];
|
|
14805
|
+
const motion = this.taaMotionViews[i];
|
|
14806
|
+
if (
|
|
14807
|
+
scene === null ||
|
|
14808
|
+
history === null ||
|
|
14809
|
+
resolved === null ||
|
|
14810
|
+
record === null ||
|
|
14811
|
+
motion === null
|
|
14812
|
+
) {
|
|
14813
|
+
continue;
|
|
14814
|
+
}
|
|
14747
14815
|
this.taaBindGroups[i] = device.createBindGroup({
|
|
14748
14816
|
label: `post.taaBindGroup${i}`,
|
|
14749
14817
|
layout: this.taaLayout,
|
|
@@ -14751,6 +14819,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
14751
14819
|
{ binding: TAA_UNIFORMS, resource: { buffer: this.taaUniforms, size: TAA_FRAG_SIZE } },
|
|
14752
14820
|
...entry(TAA_TEXTURES.uScene, scene),
|
|
14753
14821
|
...entry(TAA_TEXTURES.uHistory, history),
|
|
14822
|
+
...entry(TAA_TEXTURES.uFlicker, record),
|
|
14823
|
+
...entry(TAA_TEXTURES.uFlickerMotion, motion),
|
|
14754
14824
|
...entry(TAA_TEXTURES.uDepth, this.resolvedDepthView, true),
|
|
14755
14825
|
],
|
|
14756
14826
|
});
|
package/src/render/flameLight.ts
CHANGED
|
@@ -82,6 +82,7 @@ export function flameFrequencyHz(diameterM: number): number {
|
|
|
82
82
|
export function createFlame(options: FlameOptions): Flame {
|
|
83
83
|
const height = HEIGHT_PER_DIAMETER * options.diameterM;
|
|
84
84
|
const centre: Vec3 = [options.x, options.y + height / 3, options.z];
|
|
85
|
+
const swayM = (options.sway ?? SWAY_PER_HEIGHT) * height;
|
|
85
86
|
/* A seed spread over the whole turn, so seeds 1 and 2 are not neighbours in phase. */
|
|
86
87
|
const phase = ((options.seed ?? 0) * 2.399963) % (Math.PI * 2);
|
|
87
88
|
const light: PointLightSource = {
|
|
@@ -94,7 +95,19 @@ export function createFlame(options: FlameOptions): Flame {
|
|
|
94
95
|
radius: options.radius,
|
|
95
96
|
/* Off, because this helper is the flicker; the selection's own would apply a second one. */
|
|
96
97
|
flicker: 0,
|
|
97
|
-
|
|
98
|
+
/*
|
|
99
|
+
* **Clear of what the fire burns from wherever the light has wandered**, which is the sway added
|
|
100
|
+
* to the near plane a still flame would want. The shadow map is baked from where the light
|
|
101
|
+
* stood, so the cut moves with it, and each axis moves by at most `swayM`: a near plane of D
|
|
102
|
+
* alone left a brazier's bowl 7 mm inside it at the top of the sway and 13 cm outside at the
|
|
103
|
+
* bottom, and the bowl's shadow on the floor under the brazier came and went with every
|
|
104
|
+
* re-bake — a dark disc on some, none on others.
|
|
105
|
+
*
|
|
106
|
+
* What it gives up is `swayM` of shadow nearest the light, which is inside the fixture. What
|
|
107
|
+
* would make it wrong is a fixture reaching further below the flame than D / 6: this clears
|
|
108
|
+
* that far and no further, so a deep bowl sets its own `shadowNear` from its own depth.
|
|
109
|
+
*/
|
|
110
|
+
shadowNear: Math.max(0.05, options.diameterM) + swayM,
|
|
98
111
|
sourceRadius: options.diameterM / 2,
|
|
99
112
|
...(options.castsShadow === false ? { castsShadow: false } : {}),
|
|
100
113
|
};
|
|
@@ -112,7 +125,7 @@ export function createFlame(options: FlameOptions): Flame {
|
|
|
112
125
|
color: [options.color[0], options.color[1], options.color[2]],
|
|
113
126
|
flicker: options.flicker ?? 0.18,
|
|
114
127
|
phase,
|
|
115
|
-
swayM
|
|
128
|
+
swayM,
|
|
116
129
|
};
|
|
117
130
|
}
|
|
118
131
|
|
|
@@ -86,7 +86,8 @@ export function planPointShadowBakes(
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* The shaded lights that also cast, in the
|
|
89
|
+
* The shaded lights that also cast, each at its own place in the shaded list, and −1 at the place
|
|
90
|
+
* of a light that does not.
|
|
90
91
|
*
|
|
91
92
|
* `castsShadow: false` says a light is not worth a cubemap. `selectPointLights` honours
|
|
92
93
|
* that when it builds the *eligible* list, and that was taken for the whole promise — but
|
|
@@ -98,7 +99,16 @@ export function planPointShadowBakes(
|
|
|
98
99
|
* It showed up as a shadow with nothing to throw it — a lamp that rides the camera
|
|
99
100
|
* declines to cast for exactly that reason, and cast anyway.
|
|
100
101
|
*
|
|
101
|
-
*
|
|
102
|
+
* **The place is kept rather than the gap closed, because the place is the shader's index.** The
|
|
103
|
+
* pool publishes each map under its light's position in this list, and the shader reads a light's
|
|
104
|
+
* shadow at the light's position in the shaded one. Closing the gap made the two numberings
|
|
105
|
+
* disagree after the first light that declined: every lamp shaded after it was published one
|
|
106
|
+
* place down, the first on the declining light and each of the rest on its neighbour. A consumer
|
|
107
|
+
* whose headlights decline, and are nearly always shaded first, could only keep its lamps honest by
|
|
108
|
+
* dropping the shadows of every lamp ranked after them, and which lamps those were changed as the
|
|
109
|
+
* car moved.
|
|
110
|
+
*
|
|
111
|
+
* Writes into `out` and returns how many places it wrote; allocation-free, because this runs
|
|
102
112
|
* every frame.
|
|
103
113
|
*/
|
|
104
114
|
export function selectCastingLights(
|
|
@@ -107,14 +117,12 @@ export function selectCastingLights(
|
|
|
107
117
|
activeCount: number,
|
|
108
118
|
out: Int32Array,
|
|
109
119
|
): number {
|
|
110
|
-
|
|
111
|
-
for (let i = 0; i <
|
|
120
|
+
const count = Math.min(activeCount, out.length);
|
|
121
|
+
for (let i = 0; i < count; i++) {
|
|
112
122
|
const worldIndex = activeWorldIndices[i] ?? -1;
|
|
113
|
-
|
|
114
|
-
if (lights[worldIndex]?.castsShadow === false) continue;
|
|
115
|
-
out[written++] = worldIndex;
|
|
123
|
+
out[i] = worldIndex >= 0 && lights[worldIndex]?.castsShadow !== false ? worldIndex : -1;
|
|
116
124
|
}
|
|
117
|
-
return
|
|
125
|
+
return count;
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
/**
|
|
@@ -248,15 +256,15 @@ export function runPointShadowBakes<M extends StaleCheck, L extends BakeableLigh
|
|
|
248
256
|
* **The run decays rather than resetting, and it climbs past the threshold before it stops.**
|
|
249
257
|
*
|
|
250
258
|
* A light that wanders on a curve is not stale on *every* frame — near the turning points of
|
|
251
|
-
* its travel it moves less in a frame than the rebake tolerance
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
259
|
+
* its travel it moves less in a frame than the rebake tolerance. When the run decided whether a
|
|
260
|
+
* wanderer got its whole cube, a run that reset to zero dropped it back to two faces a frame
|
|
261
|
+
* there, and the shadow stepped through the slow part of the wander: reported as a vibration
|
|
262
|
+
* against a brazier. Every image-holding map is re-baked whole now, so the run only orders the
|
|
263
|
+
* queue, and the hysteresis keeps a wanderer behind the lights that settle instead of letting
|
|
264
|
+
* it jump ahead at every turning point.
|
|
256
265
|
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* so a light that genuinely settles stops re-baking rather than holding a budget for ever.
|
|
266
|
+
* It climbs to twice the threshold, which keeps a light chronic for three quiet frames, and
|
|
267
|
+
* decays one frame at a time so a light that genuinely settles rejoins the ordinary order.
|
|
260
268
|
*/
|
|
261
269
|
const previous = scratch.staleRun[slot] ?? 0;
|
|
262
270
|
const run = stale
|
|
@@ -277,37 +285,40 @@ export function runPointShadowBakes<M extends StaleCheck, L extends BakeableLigh
|
|
|
277
285
|
let faces = facesPerFrame;
|
|
278
286
|
let urgentSpent = false;
|
|
279
287
|
/*
|
|
280
|
-
* **
|
|
281
|
-
*
|
|
282
|
-
* dribbled out at two faces a frame is a map nobody can sample correctly until it lands.
|
|
288
|
+
* **A map that holds an image is re-baked whole or not at all**, and one being shaded may take a
|
|
289
|
+
* whole cube a frame outside the budget to make that possible.
|
|
283
290
|
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* wanders about once a second.
|
|
291
|
+
* Faces land in the layer the shader samples as they are drawn, and `planBake` publishes the
|
|
292
|
+
* origin they were drawn from only with the last one. So a re-bake spread over frames is a cube
|
|
293
|
+
* the shader reads from the old origin with part of it drawn from the new one — and a face is a
|
|
294
|
+
* 90 degree frustum, so the part drawn from somewhere else is a straight-edged quadrilateral of
|
|
295
|
+
* wrong shadow on the ground under the lamp. With the budget spent elsewhere it could hold for a
|
|
296
|
+
* second and more. Reported under a brazier as a dark shape that came and went.
|
|
291
297
|
*
|
|
292
|
-
*
|
|
298
|
+
* A map with no image yet is not sampled until its sixth face lands (see
|
|
299
|
+
* `PointShadowSystem.bind`), so it still fills a few faces a frame, and a cold one being shaded
|
|
300
|
+
* still jumps the queue with all six.
|
|
293
301
|
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
* cost the frame.
|
|
302
|
+
* The allowance used to be for a *chronically* stale light only — a wandering flame — whose
|
|
303
|
+
* dribbled cube published a new origin once every third frame, so its shadow stepped at 20 Hz
|
|
304
|
+
* where the flame wanders about once a second: reported as motion that was "really fast, not
|
|
305
|
+
* smooth, snappy, and unrealistic". Any shaded light is owed the same now, because a cube read
|
|
306
|
+
* part-way is wrong in the same place for the same reason whether or not it will be stale again.
|
|
300
307
|
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
308
|
+
* **Bounded to one light, and it has to be.** Eight braziers each taking six passes is the 48
|
|
309
|
+
* passes and 89 ms `pointShadowFacesPerFrame` exists to prevent. The first in the plan's order
|
|
310
|
+
* takes it, and `planPointShadowBakes` puts the lights that settled ahead of the ones that never
|
|
311
|
+
* do, so a flame refining its image cannot hold a correction hostage. The rest keep sampling a
|
|
312
|
+
* complete image from a few centimetres away until their turn, which is the trade
|
|
313
|
+
* `pointShadowRebakeDistance` already makes. A warm map, held but not shaded, is re-baked when a
|
|
314
|
+
* whole cube fits the budget or once it is shaded, and not before.
|
|
304
315
|
*
|
|
305
|
-
* **What would make it wrong** is a caster set heavy enough that six passes over it does not
|
|
306
|
-
*
|
|
307
|
-
*
|
|
316
|
+
* **What would make it wrong** is a caster set heavy enough that six passes over it does not fit
|
|
317
|
+
* the frame. Measured against a game whose whole static world is about 8,500 triangles in a
|
|
318
|
+
* handful of draws; a world an order of magnitude heavier wants `pointShadowFacesPerFrame`
|
|
308
319
|
* raised or this given up, and `gpuTiming` is how to tell which.
|
|
309
320
|
*/
|
|
310
|
-
let
|
|
321
|
+
let refreshSpent = false;
|
|
311
322
|
for (let i = 0; i < planned && faces > 0; i++) {
|
|
312
323
|
const slot = scratch.order[i] ?? -1;
|
|
313
324
|
/*
|
|
@@ -320,13 +331,20 @@ export function runPointShadowBakes<M extends StaleCheck, L extends BakeableLigh
|
|
|
320
331
|
if (light === undefined || map === undefined) continue;
|
|
321
332
|
|
|
322
333
|
const sampled = slot < pool.sampledCount;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
334
|
+
if (scratch.ready[slot] !== 1) {
|
|
335
|
+
/* No image to spoil: a few faces a frame, or all six at once for one being shaded. */
|
|
336
|
+
const urgent = sampled && !urgentSpent;
|
|
337
|
+
if (urgent) urgentSpent = true;
|
|
338
|
+
faces -= bake(map, light, staticCasters, urgent ? faceCount : faces);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (faces >= faceCount) {
|
|
342
|
+
faces -= bake(map, light, staticCasters, faceCount);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (!sampled || refreshSpent) continue;
|
|
346
|
+
refreshSpent = true;
|
|
347
|
+
faces -= bake(map, light, staticCasters, faceCount);
|
|
330
348
|
}
|
|
331
349
|
|
|
332
350
|
/*
|
|
@@ -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.
|
|
@@ -51,12 +56,57 @@ function smoothstep(edge0: number, edge1: number, x: number): number {
|
|
|
51
56
|
return t * t * (3 - 2 * t);
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The band over which three texels stop being one plane: how far their two steps in inverse depth
|
|
61
|
+
* fail to cancel, as a share of the two together. None of it is a plane at any angle; all of it is an
|
|
62
|
+
* edge — one side steps, the other does not — or a sliver, both stepping the same way.
|
|
63
|
+
*
|
|
64
|
+
* **A share and not a size**, which is the lesson of the rods in `resolve.test.ts`: a part a
|
|
65
|
+
* hundredth of the depth in front of a wall passes the depth test, and a normal differenced across it
|
|
66
|
+
* still leans by its height over a texel's width — nearly edge-on where texels are small. What would
|
|
67
|
+
* make this wrong is a curved surface judged at its apex, where both steps lean one way and the
|
|
68
|
+
* normal is sound; there it only gives the normals less say, which the depth test does not need.
|
|
69
|
+
*/
|
|
70
|
+
export const PLANAR_FROM = 0.25;
|
|
71
|
+
export const PLANAR_TO = 0.5;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* How sure it is that `centre` and its neighbours either side along one axis lie on one plane, one to
|
|
75
|
+
* zero. Inverse depth is affine across a plane on screen, so a plane's two steps cancel whatever its
|
|
76
|
+
* angle. One where a neighbour is missing — zero, off the picture or undrawn — or where there is no
|
|
77
|
+
* step to compare: nothing to judge by, so a normal keeps the say it always had.
|
|
78
|
+
*/
|
|
79
|
+
export function planarity(before: number, centre: number, after: number): number {
|
|
80
|
+
if (!(before > 0) || !(centre > 0) || !(after > 0)) return 1;
|
|
81
|
+
const a = 1 / after - 1 / centre;
|
|
82
|
+
const b = 1 / before - 1 / centre;
|
|
83
|
+
const spread = Math.abs(a) + Math.abs(b);
|
|
84
|
+
if (!(spread > 0)) return 1;
|
|
85
|
+
return 1 - smoothstep(PLANAR_FROM, PLANAR_TO, Math.abs(a + b) / spread);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* How far a surface's depth moves across one texel along an axis, relative to the depth: the smaller
|
|
90
|
+
* of the steps to its two neighbours, because beside a silhouette one of them is the surface behind.
|
|
91
|
+
* Zero at a sliver — nearer or farther than both, so each step is to another surface — and one
|
|
92
|
+
* neighbour's step where only one is there.
|
|
93
|
+
*/
|
|
94
|
+
export function surfaceSlope(before: number, centre: number, after: number): number {
|
|
95
|
+
const a = after > 0 ? after - centre : Number.NaN;
|
|
96
|
+
const b = before > 0 ? before - centre : Number.NaN;
|
|
97
|
+
if (Number.isNaN(a)) return Number.isNaN(b) ? 0 : Math.abs(b) / centre;
|
|
98
|
+
if (Number.isNaN(b)) return Math.abs(a) / centre;
|
|
99
|
+
if (a * b > 0) return 0;
|
|
100
|
+
return Math.min(Math.abs(a), Math.abs(b)) / centre;
|
|
101
|
+
}
|
|
102
|
+
|
|
54
103
|
/**
|
|
55
104
|
* One where the history is trustworthy, zero where it is not, and continuous between.
|
|
56
105
|
*
|
|
57
106
|
* `currDepth` is the view depth this surface should have had last frame; `histDepth` the view depth
|
|
58
107
|
* last frame held where it lands, or zero where it landed off the screen; `motionLength` the motion's
|
|
59
|
-
* length in uv; `normalDot` the cosine between the two frames' world normals there
|
|
108
|
+
* length in uv; `normalDot` the cosine between the two frames' world normals there; `slope` the
|
|
109
|
+
* surface's `surfaceSlope`, which widens the tolerance one for one.
|
|
60
110
|
*/
|
|
61
111
|
export function disocclusionWeight(
|
|
62
112
|
currDepth: number,
|
|
@@ -64,12 +114,13 @@ export function disocclusionWeight(
|
|
|
64
114
|
motionLength: number,
|
|
65
115
|
normalDot: number,
|
|
66
116
|
params: DisocclusionParams,
|
|
117
|
+
slope = 0,
|
|
67
118
|
): number {
|
|
68
119
|
if (!(currDepth > 0) || !(histDepth > 0) || !(motionLength >= 0) || Number.isNaN(normalDot)) {
|
|
69
120
|
return 0;
|
|
70
121
|
}
|
|
71
122
|
const disagreement = Math.abs(currDepth - histDepth) / currDepth;
|
|
72
|
-
const tolerance = params.depthTolerance + params.motionScale * motionLength;
|
|
123
|
+
const tolerance = params.depthTolerance + params.motionScale * motionLength + slope;
|
|
73
124
|
const depth = 1 - smoothstep(tolerance, 2 * tolerance, disagreement);
|
|
74
125
|
const normal = smoothstep(params.normalFloor, params.normalCeiling, normalDot);
|
|
75
126
|
return depth * normal;
|