@driftengine/core 4.2.1 → 4.4.0
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 +272 -0
- package/README.md +1 -1
- package/dist/cinematic/cameraPath.d.ts +53 -0
- package/dist/cinematic/cameraPath.js +118 -0
- package/dist/environment/daylight.d.ts +77 -0
- package/dist/environment/daylight.js +77 -0
- package/dist/geometry/meshConcat.d.ts +9 -0
- package/dist/geometry/meshConcat.js +101 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +14 -1
- package/dist/render/backend/createRenderer.d.ts +13 -1
- package/dist/render/backend/createRenderer.js +6 -5
- package/dist/render/backend/webgl2/renderer.d.ts +166 -1
- package/dist/render/backend/webgl2/renderer.js +424 -23
- package/dist/render/backend/webgpu/buffers.js +46 -8
- package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
- package/dist/render/backend/webgpu/depthPass.js +75 -14
- package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
- package/dist/render/backend/webgpu/exposurePass.js +190 -0
- package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
- package/dist/render/backend/webgpu/fieldCompose.js +13 -0
- package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
- package/dist/render/backend/webgpu/flatPass.js +12 -5
- package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
- package/dist/render/backend/webgpu/particlePass.js +1 -0
- package/dist/render/backend/webgpu/postPass.d.ts +12 -2
- package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
- package/dist/render/backend/webgpu/probeBake.js +97 -7
- package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
- package/dist/render/backend/webgpu/probeTargets.js +180 -0
- package/dist/render/backend/webgpu/renderer.d.ts +176 -11
- package/dist/render/backend/webgpu/renderer.js +680 -250
- package/dist/render/backend/webgpu/scatterPass.js +7 -7
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
- package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
- package/dist/render/backend/webgpu/skyPass.js +5 -1
- package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
- package/dist/render/backend/webgpu/uniformRing.js +53 -3
- package/dist/render/bloomChain.d.ts +6 -0
- package/dist/render/bloomChain.js +8 -0
- package/dist/render/clusteredLights.d.ts +19 -11
- package/dist/render/clusteredLights.js +96 -45
- package/dist/render/cutoutCaster.d.ts +23 -0
- package/dist/render/cutoutCaster.js +20 -0
- package/dist/render/driftLight/bake.d.ts +34 -0
- package/dist/render/driftLight/bake.js +95 -0
- package/dist/render/driftLight/falloff.d.ts +16 -0
- package/dist/render/driftLight/falloff.js +23 -0
- package/dist/render/driftLight/layout.d.ts +59 -0
- package/dist/render/driftLight/layout.js +138 -0
- package/dist/render/driftLight/lightField.d.ts +108 -0
- package/dist/render/driftLight/lightField.js +179 -0
- package/dist/render/driftLight/uniforms.d.ts +28 -0
- package/dist/render/driftLight/uniforms.js +41 -0
- package/dist/render/driftLight/visibility.d.ts +18 -0
- package/dist/render/driftLight/visibility.js +47 -0
- package/dist/render/emptyTexture.d.ts +6 -0
- package/dist/render/emptyTexture.js +19 -0
- package/dist/render/exposurePass.d.ts +48 -0
- package/dist/render/exposurePass.js +172 -0
- package/dist/render/flameLight.d.ts +71 -0
- package/dist/render/flameLight.js +63 -0
- package/dist/render/gi/bounceLights.d.ts +34 -0
- package/dist/render/gi/bounceLights.js +66 -0
- package/dist/render/gi/fieldReach.d.ts +12 -0
- package/dist/render/gi/fieldReach.js +62 -0
- package/dist/render/gi/globalField.d.ts +8 -0
- package/dist/render/gi/globalField.js +31 -0
- package/dist/render/gi/probeTrace.d.ts +3 -1
- package/dist/render/gi/probeTrace.js +3 -1
- package/dist/render/gpuCapability.js +19 -4
- package/dist/render/lightBudget.d.ts +11 -0
- package/dist/render/lightBudget.js +11 -0
- package/dist/render/pointLightSelection.d.ts +16 -0
- package/dist/render/pointLightSelection.js +178 -83
- package/dist/render/probeGrid.d.ts +11 -1
- package/dist/render/probeGrid.js +5 -2
- package/dist/render/probeSweeps.d.ts +45 -0
- package/dist/render/probeSweeps.js +77 -0
- package/dist/render/reflectionProbe.d.ts +43 -1
- package/dist/render/reflectionProbe.js +17 -3
- package/dist/render/renderQuality.d.ts +3 -1
- package/dist/render/rendererHarness.d.ts +9 -0
- package/dist/render/rendererHarness.js +18 -3
- package/dist/render/sceneTarget.d.ts +20 -0
- package/dist/render/sceneTarget.js +62 -1
- package/dist/render/shaders/agx.d.ts +31 -0
- package/dist/render/shaders/agx.js +81 -0
- package/dist/render/shaders/ambientOcclusion.d.ts +24 -5
- package/dist/render/shaders/ambientOcclusion.js +120 -63
- package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
- package/dist/render/shaders/cutoutCoverage.js +40 -0
- package/dist/render/shaders/depth.d.ts +25 -0
- package/dist/render/shaders/depth.js +69 -3
- package/dist/render/shaders/exposure.d.ts +52 -0
- package/dist/render/shaders/exposure.js +124 -0
- package/dist/render/shaders/filmLook.d.ts +47 -0
- package/dist/render/shaders/filmLook.js +98 -0
- package/dist/render/shaders/flat/directionalShadow.js +35 -4
- package/dist/render/shaders/flat/driftLight.d.ts +29 -0
- package/dist/render/shaders/flat/driftLight.js +75 -0
- package/dist/render/shaders/flat/index.js +5 -0
- package/dist/render/shaders/flat/lobes.js +13 -0
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +68 -9
- package/dist/render/shaders/flat/preamble.js +23 -5
- package/dist/render/shaders/flat/probeGrid.js +46 -3
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +1 -1
- package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
- package/dist/render/shaders/generated/depth.wgsl.js +100 -0
- package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
- package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
- package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
- package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
- package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
- package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
- package/dist/render/shaders/generated/rush.wgsl.js +40 -10
- package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
- package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
- package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
- package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
- package/dist/render/shaders/globalMedium.js +4 -0
- package/dist/render/shaders/localExposure.d.ts +35 -0
- package/dist/render/shaders/localExposure.js +138 -0
- package/dist/render/shaders/prefilter.d.ts +1 -1
- package/dist/render/shaders/prefilter.js +6 -0
- package/dist/render/shaders/rush.js +33 -5
- package/dist/render/shaders/tangentFrame.d.ts +1 -1
- package/dist/render/shaders/tangentFrame.js +15 -3
- package/dist/render/shaders/temporalResolve.js +39 -1
- package/dist/render/shadowCasters.d.ts +5 -3
- package/dist/render/shadowMap.js +13 -0
- package/dist/render/surfaceTexture.d.ts +5 -0
- package/package.json +6 -6
- package/scripts/gpuCompute.mjs +24 -3
- package/scripts/png.mjs +23 -12
- package/src/cinematic/cameraPath.ts +178 -0
- package/src/environment/daylight.ts +152 -0
- package/src/geometry/meshConcat.ts +100 -0
- package/src/index.ts +32 -0
- package/src/render/backend/createRenderer.ts +22 -6
- package/src/render/backend/webgl2/renderer.ts +462 -20
- package/src/render/backend/webgpu/buffers.ts +51 -8
- package/src/render/backend/webgpu/depthPass.ts +78 -10
- package/src/render/backend/webgpu/exposurePass.ts +234 -0
- package/src/render/backend/webgpu/fieldCompose.ts +15 -0
- package/src/render/backend/webgpu/flatPass.ts +10 -2
- package/src/render/backend/webgpu/particlePass.ts +9 -0
- package/src/render/backend/webgpu/probeBake.ts +143 -9
- package/src/render/backend/webgpu/probeTargets.ts +211 -0
- package/src/render/backend/webgpu/renderer.ts +807 -271
- package/src/render/backend/webgpu/scatterPass.ts +7 -7
- package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
- package/src/render/backend/webgpu/skyPass.ts +5 -1
- package/src/render/backend/webgpu/uniformRing.ts +49 -3
- package/src/render/bloomChain.ts +9 -0
- package/src/render/clusteredLights.ts +108 -63
- package/src/render/cutoutCaster.ts +44 -0
- package/src/render/driftLight/bake.ts +125 -0
- package/src/render/driftLight/falloff.ts +26 -0
- package/src/render/driftLight/layout.ts +177 -0
- package/src/render/driftLight/lightField.ts +227 -0
- package/src/render/driftLight/uniforms.ts +61 -0
- package/src/render/driftLight/visibility.ts +58 -0
- package/src/render/emptyTexture.ts +41 -0
- package/src/render/exposurePass.ts +186 -0
- package/src/render/flameLight.ts +135 -0
- package/src/render/gi/bounceLights.ts +68 -0
- package/src/render/gi/fieldReach.ts +74 -0
- package/src/render/gi/globalField.ts +39 -0
- package/src/render/gi/probeTrace.ts +3 -1
- package/src/render/gpuCapability.ts +18 -5
- package/src/render/lightBudget.ts +12 -0
- package/src/render/pointLightSelection.ts +191 -79
- package/src/render/probeGrid.ts +13 -2
- package/src/render/probeSweeps.ts +80 -0
- package/src/render/reflectionProbe.ts +54 -2
- package/src/render/renderQuality.ts +3 -1
- package/src/render/rendererHarness.ts +25 -3
- package/src/render/sceneTarget.ts +75 -1
- package/src/render/shaders/ambientOcclusion.ts +125 -63
- package/src/render/shaders/cutoutCoverage.ts +43 -0
- package/src/render/shaders/depth.ts +80 -3
- package/src/render/shaders/exposure.ts +128 -0
- package/src/render/shaders/filmLook.ts +111 -0
- package/src/render/shaders/flat/directionalShadow.ts +35 -4
- package/src/render/shaders/flat/driftLight.ts +75 -0
- package/src/render/shaders/flat/index.ts +5 -0
- package/src/render/shaders/flat/lobes.ts +13 -0
- package/src/render/shaders/flat/main.ts +68 -9
- package/src/render/shaders/flat/preamble.ts +23 -5
- package/src/render/shaders/flat/probeGrid.ts +46 -3
- package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +1 -1
- package/src/render/shaders/generated/depth.wgsl.ts +103 -0
- package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
- package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
- package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
- package/src/render/shaders/generated/rush.wgsl.ts +40 -10
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
- package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
- package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
- package/src/render/shaders/globalMedium.ts +4 -0
- package/src/render/shaders/localExposure.ts +154 -0
- package/src/render/shaders/prefilter.ts +6 -0
- package/src/render/shaders/rush.ts +33 -5
- package/src/render/shaders/tangentFrame.ts +15 -3
- package/src/render/shaders/temporalResolve.ts +39 -1
- package/src/render/shadowCasters.ts +5 -3
- package/src/render/shadowMap.ts +14 -0
- package/src/render/surfaceTexture.ts +5 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the traced field reaches the probes it lights: how many cascades a grid needs, and which
|
|
3
|
+
* scheduled probes the field reaches this frame.
|
|
4
|
+
*
|
|
5
|
+
* **A probe traced where the field is not learns that the world is sky.** Every ray it casts finds
|
|
6
|
+
* nothing and falls to the sky colour, the floor under it and the wall beside it included, so it
|
|
7
|
+
* reports the open sky from every side: a covered arcade lit like a field at noon. The field is
|
|
8
|
+
* composed around the camera, and three cascades from a 4 m inner one reach 16 m, so a courtyard 36
|
|
9
|
+
* m long had its far end lit exactly that way from wherever the viewer stood, a milky wash over
|
|
10
|
+
* everything past the outermost cascade that the rasterised grid did not have.
|
|
11
|
+
*
|
|
12
|
+
* Two answers, one for each way it happens. A grid is given the cascades that reach all of it from
|
|
13
|
+
* anywhere inside it; and a probe the field still does not reach, because the camera has left the
|
|
14
|
+
* grid, is left out of the schedule and keeps the light it has, rather than being overwritten with
|
|
15
|
+
* a sky it never saw. What it gives up is that probe following a change while the camera is away.
|
|
16
|
+
*/
|
|
17
|
+
import { GLOBAL_FIELD_BLEND } from './globalField.js';
|
|
18
|
+
/** The share of the outermost cascade's half-extent a probe can trust, inside its fading faces. */
|
|
19
|
+
const TRUSTED = 1 - 2 * GLOBAL_FIELD_BLEND;
|
|
20
|
+
/**
|
|
21
|
+
* Cascades enough for the outermost to hold the whole grid, and one probe step past it, within the
|
|
22
|
+
* part of it that is trusted, wherever inside the grid the camera stands. At least `least` and at
|
|
23
|
+
* most `most`: each cascade is the same number of samples, so the bound is on memory, not reach.
|
|
24
|
+
*/
|
|
25
|
+
export function cascadesForGrid(grid, radius, least, most) {
|
|
26
|
+
let span = 0;
|
|
27
|
+
let step = 0;
|
|
28
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
29
|
+
const spacing = grid.spacing[axis];
|
|
30
|
+
span = Math.max(span, spacing * (grid.counts[axis] - 1));
|
|
31
|
+
step = Math.max(step, spacing);
|
|
32
|
+
}
|
|
33
|
+
const needed = (span + step) / TRUSTED;
|
|
34
|
+
let cascades = 1;
|
|
35
|
+
while (radius * 2 ** (cascades - 1) < needed && cascades < most)
|
|
36
|
+
cascades++;
|
|
37
|
+
return Math.min(most, Math.max(least, cascades));
|
|
38
|
+
}
|
|
39
|
+
const at = [0, 0, 0];
|
|
40
|
+
/**
|
|
41
|
+
* Keeps, in place and in order, the scheduled probes inside the trusted part of the outermost
|
|
42
|
+
* cascade, `bounds` as `[minX, minY, minZ, maxX, maxY, maxZ]`. Returns how many are kept.
|
|
43
|
+
*/
|
|
44
|
+
export function probesTheFieldReaches(grid, scheduled, count, bounds) {
|
|
45
|
+
let kept = 0;
|
|
46
|
+
for (let i = 0; i < count; i++) {
|
|
47
|
+
const layer = scheduled[i];
|
|
48
|
+
grid.positionOf(layer, at);
|
|
49
|
+
let inside = true;
|
|
50
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
51
|
+
const low = bounds[axis];
|
|
52
|
+
const high = bounds[axis + 3];
|
|
53
|
+
const band = GLOBAL_FIELD_BLEND * (high - low);
|
|
54
|
+
const p = at[axis];
|
|
55
|
+
if (p < low + band || p > high - band)
|
|
56
|
+
inside = false;
|
|
57
|
+
}
|
|
58
|
+
if (inside)
|
|
59
|
+
scheduled[kept++] = layer;
|
|
60
|
+
}
|
|
61
|
+
return kept;
|
|
62
|
+
}
|
|
@@ -131,6 +131,14 @@ export declare function composeGlobalField(instances: readonly GlobalFieldInstan
|
|
|
131
131
|
* is the honest one.
|
|
132
132
|
*/
|
|
133
133
|
export declare function sampleGlobalField(field: GlobalField, x: number, y: number, z: number): number;
|
|
134
|
+
/**
|
|
135
|
+
* The world distance to the nearest of `instances` at any point, rather than composed onto a grid
|
|
136
|
+
* around the camera: for a caller that traces through the scene once instead of every frame, such
|
|
137
|
+
* as DriftLight's occlusion bake. The same sampling `composeGlobalField` does, under the same
|
|
138
|
+
* refusals of a non-uniform scale and non-cubic voxels, so the two cannot disagree about a surface.
|
|
139
|
+
* Nothing placed answers a very large distance: open space everywhere.
|
|
140
|
+
*/
|
|
141
|
+
export declare function distanceToInstances(instances: readonly GlobalFieldInstance[]): (x: number, y: number, z: number) => number;
|
|
134
142
|
/**
|
|
135
143
|
* Refuse a source whose voxels are not cubic, which `sourceAt` and the shader both assume.
|
|
136
144
|
*
|
|
@@ -187,6 +187,37 @@ function insideBy(cascade, x, y, z) {
|
|
|
187
187
|
}
|
|
188
188
|
return least;
|
|
189
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* The world distance to the nearest of `instances` at any point, rather than composed onto a grid
|
|
192
|
+
* around the camera: for a caller that traces through the scene once instead of every frame, such
|
|
193
|
+
* as DriftLight's occlusion bake. The same sampling `composeGlobalField` does, under the same
|
|
194
|
+
* refusals of a non-uniform scale and non-cubic voxels, so the two cannot disagree about a surface.
|
|
195
|
+
* Nothing placed answers a very large distance: open space everywhere.
|
|
196
|
+
*/
|
|
197
|
+
export function distanceToInstances(instances) {
|
|
198
|
+
const inverses = [];
|
|
199
|
+
const scales = [];
|
|
200
|
+
const kept = [];
|
|
201
|
+
for (const instance of instances) {
|
|
202
|
+
assertUniformScale(instance.transform);
|
|
203
|
+
assertCubicVoxels(instance.source);
|
|
204
|
+
const inverse = new Float32Array(16);
|
|
205
|
+
if (mat4.invert(inverse, instance.transform) === null)
|
|
206
|
+
continue;
|
|
207
|
+
inverses.push(inverse);
|
|
208
|
+
scales.push(uniformScaleOf(instance.transform));
|
|
209
|
+
kept.push(instance);
|
|
210
|
+
}
|
|
211
|
+
return (x, y, z) => {
|
|
212
|
+
let nearest = 1e9;
|
|
213
|
+
for (let at = 0; at < kept.length; at++) {
|
|
214
|
+
const distance = sampleInstance(kept[at], inverses[at], scales[at], x, y, z);
|
|
215
|
+
if (distance < nearest)
|
|
216
|
+
nearest = distance;
|
|
217
|
+
}
|
|
218
|
+
return nearest;
|
|
219
|
+
};
|
|
220
|
+
}
|
|
190
221
|
/**
|
|
191
222
|
* The distance one instance reports at a world point.
|
|
192
223
|
*
|
|
@@ -53,8 +53,10 @@ export declare const GOLDEN_ANGLE: number;
|
|
|
53
53
|
/**
|
|
54
54
|
* How far the set turns between one refresh and the next, in turns.
|
|
55
55
|
*
|
|
56
|
-
* The golden ratio's fractional part: successive
|
|
56
|
+
* The golden ratio's fractional part: successive turns land their offsets as far from each other
|
|
57
57
|
* as the sequence allows, for the same reason the spiral's own points do, and it never repeats.
|
|
58
|
+
* The WebGPU bake holds the turn at 0, because a set that turns makes every refresh a new estimate
|
|
59
|
+
* and a still scene flicker; see `ProbeBaker.bake`.
|
|
58
60
|
*/
|
|
59
61
|
export declare const FRAME_TURN = 0.618033988749895;
|
|
60
62
|
/**
|
|
@@ -56,8 +56,10 @@ export const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
|
|
|
56
56
|
/**
|
|
57
57
|
* How far the set turns between one refresh and the next, in turns.
|
|
58
58
|
*
|
|
59
|
-
* The golden ratio's fractional part: successive
|
|
59
|
+
* The golden ratio's fractional part: successive turns land their offsets as far from each other
|
|
60
60
|
* as the sequence allows, for the same reason the spiral's own points do, and it never repeats.
|
|
61
|
+
* The WebGPU bake holds the turn at 0, because a set that turns makes every refresh a new estimate
|
|
62
|
+
* and a still scene flicker; see `ProbeBaker.bake`.
|
|
61
63
|
*/
|
|
62
64
|
export const FRAME_TURN = 0.618033988749895;
|
|
63
65
|
/**
|
|
@@ -143,10 +143,25 @@ export async function describeGpu(options = {}) {
|
|
|
143
143
|
weak: isWeakGpuFamily(rendererName),
|
|
144
144
|
source,
|
|
145
145
|
});
|
|
146
|
-
/*
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
/*
|
|
147
|
+
* WebGL2 first: it is the API that gives a model number. See the header for the measurement.
|
|
148
|
+
*
|
|
149
|
+
* **The canvas is made inside the guard as well as the context.** A host can have a `document`
|
|
150
|
+
* and still refuse to make one: the native host installs a page so a game finds what it expects,
|
|
151
|
+
* and its `createElement` throws, because there is no DOM behind it. Made outside the guard, that
|
|
152
|
+
* took a scene's mount down on the second host; inside it, such a host is simply one without
|
|
153
|
+
* WebGL2, and the adapter below names the part.
|
|
154
|
+
*/
|
|
155
|
+
let canvas = null;
|
|
156
|
+
try {
|
|
157
|
+
canvas =
|
|
158
|
+
options.createCanvas === undefined
|
|
159
|
+
? (globalThis.document?.createElement('canvas') ?? null)
|
|
160
|
+
: options.createCanvas();
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
canvas = null;
|
|
164
|
+
}
|
|
150
165
|
if (canvas !== null) {
|
|
151
166
|
let gl = null;
|
|
152
167
|
try {
|
|
@@ -201,6 +201,17 @@ export declare const COOKIE_ATLAS_TEXTURE_UNIT: number;
|
|
|
201
201
|
* `textureUnitBudget.test.ts` carries the phone that budget was written for.
|
|
202
202
|
*/
|
|
203
203
|
export declare const REFRACT_SCENE_TEXTURE_UNIT: number;
|
|
204
|
+
/**
|
|
205
|
+
* DriftLight's brick index and atlas, the lit program's fourteenth and fifteenth fragment samplers.
|
|
206
|
+
*
|
|
207
|
+
* Units past fifteen are legal: `MAX_TEXTURE_IMAGE_UNITS` caps how many samplers a stage *uses*,
|
|
208
|
+
* sixteen at the least, while a unit's number only has to be under the combined limit, which WebGL2
|
|
209
|
+
* guarantees is at least thirty-two. The skin palette and morph deltas below them are vertex
|
|
210
|
+
* textures and count against the vertex stage instead. What would make this wrong is a sixteenth
|
|
211
|
+
* fragment sampler, which the lit program would then be one short of on the guaranteed minimum.
|
|
212
|
+
*/
|
|
213
|
+
export declare const DRIFT_LIGHT_INDEX_TEXTURE_UNIT: number;
|
|
214
|
+
export declare const DRIFT_LIGHT_ATLAS_TEXTURE_UNIT: number;
|
|
204
215
|
/**
|
|
205
216
|
* A cookie's tile, in texels a side.
|
|
206
217
|
*
|
|
@@ -199,6 +199,17 @@ export const COOKIE_ATLAS_TEXTURE_UNIT = MORPH_DELTA_TEXTURE_UNIT + 1;
|
|
|
199
199
|
* `textureUnitBudget.test.ts` carries the phone that budget was written for.
|
|
200
200
|
*/
|
|
201
201
|
export const REFRACT_SCENE_TEXTURE_UNIT = COOKIE_ATLAS_TEXTURE_UNIT + 1;
|
|
202
|
+
/**
|
|
203
|
+
* DriftLight's brick index and atlas, the lit program's fourteenth and fifteenth fragment samplers.
|
|
204
|
+
*
|
|
205
|
+
* Units past fifteen are legal: `MAX_TEXTURE_IMAGE_UNITS` caps how many samplers a stage *uses*,
|
|
206
|
+
* sixteen at the least, while a unit's number only has to be under the combined limit, which WebGL2
|
|
207
|
+
* guarantees is at least thirty-two. The skin palette and morph deltas below them are vertex
|
|
208
|
+
* textures and count against the vertex stage instead. What would make this wrong is a sixteenth
|
|
209
|
+
* fragment sampler, which the lit program would then be one short of on the guaranteed minimum.
|
|
210
|
+
*/
|
|
211
|
+
export const DRIFT_LIGHT_INDEX_TEXTURE_UNIT = REFRACT_SCENE_TEXTURE_UNIT + 1;
|
|
212
|
+
export const DRIFT_LIGHT_ATLAS_TEXTURE_UNIT = DRIFT_LIGHT_INDEX_TEXTURE_UNIT + 1;
|
|
202
213
|
/**
|
|
203
214
|
* A cookie's tile, in texels a side.
|
|
204
215
|
*
|
|
@@ -59,6 +59,15 @@ export interface PointLightSource {
|
|
|
59
59
|
* They are floor lighting 35 cm off the deck and were never meant to cast at all.
|
|
60
60
|
*/
|
|
61
61
|
castsShadow?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Whether a DriftLight field also sums this light (`createLightField` sets it).
|
|
64
|
+
*
|
|
65
|
+
* Then the lit shader shades it exactly only where the choice is complete and lets the field's
|
|
66
|
+
* summed light take over past it, so the light is never counted twice. Carried to the shader as
|
|
67
|
+
* the sign of the light's weight, which both light paths already have: no new storage on a part
|
|
68
|
+
* whose uniform budget has none to spare.
|
|
69
|
+
*/
|
|
70
|
+
inLightField?: boolean;
|
|
62
71
|
}
|
|
63
72
|
export interface PointLightBuffer {
|
|
64
73
|
readonly positions: Float32Array;
|
|
@@ -132,6 +141,13 @@ export interface PointLightBuffer {
|
|
|
132
141
|
*/
|
|
133
142
|
readonly shadowIndex: Int32Array;
|
|
134
143
|
shadowCount: number;
|
|
144
|
+
/**
|
|
145
|
+
* How far from the selection's centre the choice is complete for DriftLight field lights: a
|
|
146
|
+
* point nearer than this is reached only by field lights chosen at full strength. A field light
|
|
147
|
+
* left out at distance d reaching r first reaches d - r; one dimmed at the cut counts as left
|
|
148
|
+
* out. At most the view range, past which nothing is chosen.
|
|
149
|
+
*/
|
|
150
|
+
complete: number;
|
|
135
151
|
}
|
|
136
152
|
/**
|
|
137
153
|
* A buffer for the selection to fill, sized to what the caller can actually bind.
|
|
@@ -40,6 +40,82 @@ const FADE_BAND_M = 40;
|
|
|
40
40
|
const CONTENTION_BAND_M = 3;
|
|
41
41
|
/** Sort keys for the shadow list, which is ordered independently of the shaded one. */
|
|
42
42
|
const shadowRank = new Float32Array(POINT_SHADOW_POOL);
|
|
43
|
+
/**
|
|
44
|
+
* Every light in range this frame, by source index and squared distance, before the nearest are
|
|
45
|
+
* chosen. Grown when a world offers more than it has held, and never shrunk, so a frame allocates
|
|
46
|
+
* only when the world has grown. Float64, so two lights a hair apart are ordered by their real
|
|
47
|
+
* distances rather than by what survives a float's rounding.
|
|
48
|
+
*/
|
|
49
|
+
let candidateIndex = new Int32Array(64);
|
|
50
|
+
let candidateDistance = new Float64Array(64);
|
|
51
|
+
function growCandidates(needed) {
|
|
52
|
+
if (needed <= candidateIndex.length)
|
|
53
|
+
return;
|
|
54
|
+
const size = Math.max(needed, candidateIndex.length * 2);
|
|
55
|
+
const index = new Int32Array(size);
|
|
56
|
+
const distance = new Float64Array(size);
|
|
57
|
+
index.set(candidateIndex);
|
|
58
|
+
distance.set(candidateDistance);
|
|
59
|
+
candidateIndex = index;
|
|
60
|
+
candidateDistance = distance;
|
|
61
|
+
}
|
|
62
|
+
/** Whether candidate `a` goes before candidate `b`: nearer, or as near and offered first. */
|
|
63
|
+
function before(a, b) {
|
|
64
|
+
const da = candidateDistance[a] ?? 0;
|
|
65
|
+
const db = candidateDistance[b] ?? 0;
|
|
66
|
+
return da < db || (da === db && (candidateIndex[a] ?? 0) < (candidateIndex[b] ?? 0));
|
|
67
|
+
}
|
|
68
|
+
function swap(a, b) {
|
|
69
|
+
const index = candidateIndex[a] ?? 0;
|
|
70
|
+
const distance = candidateDistance[a] ?? 0;
|
|
71
|
+
candidateIndex[a] = candidateIndex[b] ?? 0;
|
|
72
|
+
candidateDistance[a] = candidateDistance[b] ?? 0;
|
|
73
|
+
candidateIndex[b] = index;
|
|
74
|
+
candidateDistance[b] = distance;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Move the `keep` nearest of the first `count` candidates into `[0, keep)`, in no order: a
|
|
78
|
+
* quickselect, linear on average. The pivot is the median of three, which keeps a list that
|
|
79
|
+
* arrives already sorted — a world laid out along the camera's path — away from the quadratic case.
|
|
80
|
+
*/
|
|
81
|
+
function chooseNearest(count, keep) {
|
|
82
|
+
let lo = 0;
|
|
83
|
+
let hi = count - 1;
|
|
84
|
+
while (lo < hi) {
|
|
85
|
+
const mid = (lo + hi) >> 1;
|
|
86
|
+
if (before(mid, lo))
|
|
87
|
+
swap(mid, lo);
|
|
88
|
+
if (before(hi, lo))
|
|
89
|
+
swap(hi, lo);
|
|
90
|
+
if (before(hi, mid))
|
|
91
|
+
swap(hi, mid);
|
|
92
|
+
swap(mid, hi);
|
|
93
|
+
let store = lo;
|
|
94
|
+
for (let at = lo; at < hi; at++) {
|
|
95
|
+
if (before(at, hi)) {
|
|
96
|
+
swap(at, store);
|
|
97
|
+
store++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
swap(store, hi);
|
|
101
|
+
if (store === keep || store === keep - 1)
|
|
102
|
+
return;
|
|
103
|
+
if (store < keep)
|
|
104
|
+
lo = store + 1;
|
|
105
|
+
else
|
|
106
|
+
hi = store - 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Sort the first `count` candidates nearest first. An insertion sort: `count` is a slot budget. */
|
|
110
|
+
function sortCandidates(count) {
|
|
111
|
+
for (let at = 1; at < count; at++) {
|
|
112
|
+
let back = at;
|
|
113
|
+
while (back > 0 && before(back, back - 1)) {
|
|
114
|
+
swap(back, back - 1);
|
|
115
|
+
back--;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
43
119
|
/**
|
|
44
120
|
* A buffer for the selection to fill, sized to what the caller can actually bind.
|
|
45
121
|
*
|
|
@@ -72,6 +148,7 @@ export function createPointLightBuffer(capacity = MAX_POINT_LIGHTS) {
|
|
|
72
148
|
scratchDistance: new Float32Array(Math.max(shaded, POINT_SHADOW_POOL)),
|
|
73
149
|
shadowIndex: new Int32Array(POINT_SHADOW_POOL),
|
|
74
150
|
shadowCount: 0,
|
|
151
|
+
complete: 0,
|
|
75
152
|
};
|
|
76
153
|
}
|
|
77
154
|
/**
|
|
@@ -115,6 +192,7 @@ export function selectPointLights(sources, x, y, z, out, timeSeconds = 0, viewRa
|
|
|
115
192
|
* shader binds, which is the case where nothing is contested and nothing dims.
|
|
116
193
|
*/
|
|
117
194
|
let cutSq = Infinity;
|
|
195
|
+
let candidateCount = 0;
|
|
118
196
|
for (let sourceIndex = 0; sourceIndex < sources.length; sourceIndex++) {
|
|
119
197
|
const light = sources[sourceIndex];
|
|
120
198
|
if (light === undefined)
|
|
@@ -153,88 +231,14 @@ export function selectPointLights(sources, x, y, z, out, timeSeconds = 0, viewRa
|
|
|
153
231
|
*
|
|
154
232
|
* Sharing one sorted list is what made them drift apart, so they do not share one.
|
|
155
233
|
*/
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
else {
|
|
165
|
-
// Full, so making room drops whoever was last off the end — and that is the
|
|
166
|
-
// nearest light not in the set unless something nearer is rejected later.
|
|
167
|
-
if (out.count >= shadedCapacity) {
|
|
168
|
-
const displaced = out.scratchDistance[shadedCapacity - 1] ?? Infinity;
|
|
169
|
-
if (displaced < cutSq)
|
|
170
|
-
cutSq = displaced;
|
|
171
|
-
}
|
|
172
|
-
const last = Math.min(out.count, shadedCapacity - 1);
|
|
173
|
-
for (let index = last; index > slot; index--) {
|
|
174
|
-
out.scratchDistance[index] = out.scratchDistance[index - 1] ?? 0;
|
|
175
|
-
out.radii[index] = out.radii[index - 1] ?? 0;
|
|
176
|
-
out.sourceRadii[index] = out.sourceRadii[index - 1] ?? 0;
|
|
177
|
-
out.sourceIndex[index] = out.sourceIndex[index - 1] ?? -1;
|
|
178
|
-
for (let component = 0; component < 3; component++) {
|
|
179
|
-
out.positions[index * 3 + component] = out.positions[(index - 1) * 3 + component] ?? 0;
|
|
180
|
-
out.colors[index * 3 + component] = out.colors[(index - 1) * 3 + component] ?? 0;
|
|
181
|
-
out.directions[index * 3 + component] = out.directions[(index - 1) * 3 + component] ?? 0;
|
|
182
|
-
}
|
|
183
|
-
out.coneCos[index * 2] = out.coneCos[(index - 1) * 2] ?? 0;
|
|
184
|
-
out.coneCos[index * 2 + 1] = out.coneCos[(index - 1) * 2 + 1] ?? 0;
|
|
185
|
-
out.iesProfiles[index] = out.iesProfiles[index - 1] ?? -1;
|
|
186
|
-
}
|
|
187
|
-
out.scratchDistance[slot] = distanceSq;
|
|
188
|
-
out.sourceIndex[slot] = sourceIndex;
|
|
189
|
-
out.radii[slot] = light.radius;
|
|
190
|
-
out.sourceRadii[slot] = Math.max(light.sourceRadius, 0);
|
|
191
|
-
out.positions[slot * 3] = light.x;
|
|
192
|
-
out.positions[slot * 3 + 1] = light.y;
|
|
193
|
-
out.positions[slot * 3 + 2] = light.z;
|
|
194
|
-
const scale = flickerScale(light, timeSeconds);
|
|
195
|
-
out.colors[slot * 3] = light.r * scale;
|
|
196
|
-
out.colors[slot * 3 + 1] = light.g * scale;
|
|
197
|
-
out.colors[slot * 3 + 2] = light.b * scale;
|
|
198
|
-
/*
|
|
199
|
-
* **Normalised here rather than trusted**, because the cone is a comparison against a cosine
|
|
200
|
-
* and an un-normalised direction scales that cosine — so a vector of length 2 gives a cone of
|
|
201
|
-
* the wrong width, which reads as the angle having been set wrong rather than the vector.
|
|
202
|
-
* A zero-length direction is a light that declared no cone, and it takes the open pair.
|
|
203
|
-
*/
|
|
204
|
-
const dx = light.dirX ?? 0;
|
|
205
|
-
const dy = light.dirY ?? 0;
|
|
206
|
-
const dz = light.dirZ ?? 0;
|
|
207
|
-
const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
208
|
-
const aimed = length > 1e-6 && light.coneOuterDeg !== undefined;
|
|
209
|
-
const inverse = aimed ? 1 / length : 0;
|
|
210
|
-
out.directions[slot * 3] = dx * inverse;
|
|
211
|
-
out.directions[slot * 3 + 1] = dy * inverse;
|
|
212
|
-
out.directions[slot * 3 + 2] = dz * inverse;
|
|
213
|
-
if (aimed) {
|
|
214
|
-
/*
|
|
215
|
-
* Clamped so the inner edge never passes the outer. A caller that swaps them would
|
|
216
|
-
* otherwise get `smoothstep` with its edges inverted, which is a cone that is dark in the
|
|
217
|
-
* middle and bright at the rim — a picture nobody would attribute to two numbers being the
|
|
218
|
-
* wrong way round.
|
|
219
|
-
*/
|
|
220
|
-
const outerDeg = Math.min(89.9, Math.max(0, light.coneOuterDeg ?? 45));
|
|
221
|
-
const innerDeg = Math.min(outerDeg, Math.max(0, light.coneInnerDeg ?? outerDeg * 0.75));
|
|
222
|
-
out.coneCos[slot * 2] = Math.cos((innerDeg * Math.PI) / 180);
|
|
223
|
-
out.coneCos[slot * 2 + 1] = Math.cos((outerDeg * Math.PI) / 180);
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
out.coneCos[slot * 2] = POINT_LIGHT_COS_INNER;
|
|
227
|
-
out.coneCos[slot * 2 + 1] = POINT_LIGHT_COS_OUTER;
|
|
228
|
-
}
|
|
229
|
-
/*
|
|
230
|
-
* The profile applies to a point light as readily as to a spot: a bare bulb has a measured
|
|
231
|
-
* distribution too, and the arithmetic does not care whether a cone is also being applied.
|
|
232
|
-
* Restricting it to spots would be a smaller feature for no saving.
|
|
233
|
-
*/
|
|
234
|
-
out.iesProfiles[slot] = light.iesProfile ?? -1;
|
|
235
|
-
if (out.count < shadedCapacity)
|
|
236
|
-
out.count++;
|
|
237
|
-
}
|
|
234
|
+
/*
|
|
235
|
+
* A candidate for the shaded list. Which of them win is decided once every source has been
|
|
236
|
+
* seen, below, rather than by keeping a sorted list as they arrive: see `chooseNearest`.
|
|
237
|
+
*/
|
|
238
|
+
growCandidates(candidateCount + 1);
|
|
239
|
+
candidateIndex[candidateCount] = sourceIndex;
|
|
240
|
+
candidateDistance[candidateCount] = distanceSq;
|
|
241
|
+
candidateCount++;
|
|
238
242
|
/*
|
|
239
243
|
* Inside its own radius, so it is lighting something and therefore has something
|
|
240
244
|
* to cast. Nearest first, capped at the pool, and independent of whether the
|
|
@@ -275,6 +279,86 @@ export function selectPointLights(sources, x, y, z, out, timeSeconds = 0, viewRa
|
|
|
275
279
|
if (out.shadowCount < POINT_SHADOW_POOL)
|
|
276
280
|
out.shadowCount++;
|
|
277
281
|
}
|
|
282
|
+
/*
|
|
283
|
+
* The nearest `shadedCapacity` candidates, nearest first, and the nearest one left out.
|
|
284
|
+
*
|
|
285
|
+
* **Partitioned, then only the winners sorted.** This kept a sorted list as the sources arrived,
|
|
286
|
+
* shifting fourteen fields a slot to make room, which is O(sources × slots): 3.2 ms a frame to
|
|
287
|
+
* choose 320 of a courtyard's 10,000 candles, most of it moving lights that were then displaced.
|
|
288
|
+
* A quickselect finds the winners in time linear in the sources and the sort touches only them,
|
|
289
|
+
* and each winner is packed once. Ties go to the earlier source, as they did.
|
|
290
|
+
*/
|
|
291
|
+
const chosen = Math.min(candidateCount, shadedCapacity);
|
|
292
|
+
let complete = viewRange;
|
|
293
|
+
if (candidateCount > shadedCapacity) {
|
|
294
|
+
chooseNearest(candidateCount, shadedCapacity);
|
|
295
|
+
for (let at = shadedCapacity; at < candidateCount; at++) {
|
|
296
|
+
const distanceSq = candidateDistance[at] ?? Infinity;
|
|
297
|
+
if (distanceSq < cutSq)
|
|
298
|
+
cutSq = distanceSq;
|
|
299
|
+
const left = sources[candidateIndex[at] ?? 0];
|
|
300
|
+
if (left?.inLightField === true) {
|
|
301
|
+
complete = Math.min(complete, Math.sqrt(distanceSq) - left.radius);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
sortCandidates(chosen);
|
|
306
|
+
for (let slot = 0; slot < chosen; slot++) {
|
|
307
|
+
const sourceIndex = candidateIndex[slot] ?? 0;
|
|
308
|
+
const light = sources[sourceIndex];
|
|
309
|
+
if (light === undefined)
|
|
310
|
+
continue;
|
|
311
|
+
const distanceSq = candidateDistance[slot] ?? 0;
|
|
312
|
+
out.scratchDistance[slot] = distanceSq;
|
|
313
|
+
out.sourceIndex[slot] = sourceIndex;
|
|
314
|
+
out.radii[slot] = light.radius;
|
|
315
|
+
out.sourceRadii[slot] = Math.max(light.sourceRadius, 0);
|
|
316
|
+
out.positions[slot * 3] = light.x;
|
|
317
|
+
out.positions[slot * 3 + 1] = light.y;
|
|
318
|
+
out.positions[slot * 3 + 2] = light.z;
|
|
319
|
+
const scale = flickerScale(light, timeSeconds);
|
|
320
|
+
out.colors[slot * 3] = light.r * scale;
|
|
321
|
+
out.colors[slot * 3 + 1] = light.g * scale;
|
|
322
|
+
out.colors[slot * 3 + 2] = light.b * scale;
|
|
323
|
+
/*
|
|
324
|
+
* **Normalised here rather than trusted**, because the cone is a comparison against a cosine
|
|
325
|
+
* and an un-normalised direction scales that cosine — so a vector of length 2 gives a cone of
|
|
326
|
+
* the wrong width, which reads as the angle having been set wrong rather than the vector.
|
|
327
|
+
* A zero-length direction is a light that declared no cone, and it takes the open pair.
|
|
328
|
+
*/
|
|
329
|
+
const dx = light.dirX ?? 0;
|
|
330
|
+
const dy = light.dirY ?? 0;
|
|
331
|
+
const dz = light.dirZ ?? 0;
|
|
332
|
+
const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
333
|
+
const aimed = length > 1e-6 && light.coneOuterDeg !== undefined;
|
|
334
|
+
const inverse = aimed ? 1 / length : 0;
|
|
335
|
+
out.directions[slot * 3] = dx * inverse;
|
|
336
|
+
out.directions[slot * 3 + 1] = dy * inverse;
|
|
337
|
+
out.directions[slot * 3 + 2] = dz * inverse;
|
|
338
|
+
if (aimed) {
|
|
339
|
+
/*
|
|
340
|
+
* Clamped so the inner edge never passes the outer. A caller that swaps them would
|
|
341
|
+
* otherwise get `smoothstep` with its edges inverted, which is a cone that is dark in the
|
|
342
|
+
* middle and bright at the rim — a picture nobody would attribute to two numbers being the
|
|
343
|
+
* wrong way round.
|
|
344
|
+
*/
|
|
345
|
+
const outerDeg = Math.min(89.9, Math.max(0, light.coneOuterDeg ?? 45));
|
|
346
|
+
const innerDeg = Math.min(outerDeg, Math.max(0, light.coneInnerDeg ?? outerDeg * 0.75));
|
|
347
|
+
out.coneCos[slot * 2] = Math.cos((innerDeg * Math.PI) / 180);
|
|
348
|
+
out.coneCos[slot * 2 + 1] = Math.cos((outerDeg * Math.PI) / 180);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
out.coneCos[slot * 2] = POINT_LIGHT_COS_INNER;
|
|
352
|
+
out.coneCos[slot * 2 + 1] = POINT_LIGHT_COS_OUTER;
|
|
353
|
+
}
|
|
354
|
+
/*
|
|
355
|
+
* The profile applies to a point light as readily as to a spot: a bare bulb has a measured
|
|
356
|
+
* distribution too, and the arithmetic does not care whether a cone is also being applied.
|
|
357
|
+
* Restricting it to spots would be a smaller feature for no saving.
|
|
358
|
+
*/
|
|
359
|
+
out.iesProfiles[slot] = light.iesProfile ?? -1;
|
|
360
|
+
}
|
|
361
|
+
out.count = chosen;
|
|
278
362
|
/*
|
|
279
363
|
* How present each of them is, now that the set — and with it the boundary — is
|
|
280
364
|
* known. A second pass over at most eight entries, which is why the cut can be a
|
|
@@ -289,8 +373,19 @@ export function selectPointLights(sources, x, y, z, out, timeSeconds = 0, viewRa
|
|
|
289
373
|
const cullDistance = viewRange + (out.radii[index] ?? 0);
|
|
290
374
|
const arriving = (cullDistance - distance) / FADE_BAND_M;
|
|
291
375
|
const contested = (cut - distance) / CONTENTION_BAND_M;
|
|
292
|
-
out.
|
|
376
|
+
const field = sources[out.sourceIndex[index] ?? 0]?.inLightField === true;
|
|
377
|
+
/*
|
|
378
|
+
* **A field light does not fade in at the edge of the view range**, because the field is what
|
|
379
|
+
* stands in for it past the choice: the fade exists so a light does not switch on in front of
|
|
380
|
+
* the player, and a light the field sums was never off. Kept, it dimmed every light across a
|
|
381
|
+
* short range, and a dimmed light is an incomplete one, so the field took over at the camera.
|
|
382
|
+
*/
|
|
383
|
+
const weight = Math.max(0, Math.min(1, field ? contested : Math.min(arriving, contested)));
|
|
384
|
+
out.weights[index] = field ? -weight : weight;
|
|
385
|
+
if (field && weight < 1)
|
|
386
|
+
complete = Math.min(complete, distance - (out.radii[index] ?? 0));
|
|
293
387
|
}
|
|
388
|
+
out.complete = Math.max(0, complete);
|
|
294
389
|
}
|
|
295
390
|
function flickerScale(light, timeSeconds) {
|
|
296
391
|
if (light.flicker <= 0)
|
|
@@ -32,6 +32,14 @@ export interface ProbeGridOptions {
|
|
|
32
32
|
* convolution and the shader instead of a grid path and a legacy path that drift.
|
|
33
33
|
*/
|
|
34
34
|
readonly counts: Vec3;
|
|
35
|
+
/**
|
|
36
|
+
* Blend each sweep of bakes into the next rather than showing each probe's bake the moment it
|
|
37
|
+
* lands. For a grid re-baked a probe a frame under a moving light, where each probe otherwise
|
|
38
|
+
* jumps once a sweep and the jumps travel across the scene as a pass of light. Costs three times
|
|
39
|
+
* the layers and a sweep of lag; ignored where indirect light is traced, which writes the grid's
|
|
40
|
+
* layers itself. See `probeSweeps.ts`. Off by default.
|
|
41
|
+
*/
|
|
42
|
+
readonly crossfade?: boolean;
|
|
35
43
|
}
|
|
36
44
|
/**
|
|
37
45
|
* The eight probes surrounding a point, and how much of each it takes.
|
|
@@ -87,8 +95,10 @@ export declare class ProbeGrid {
|
|
|
87
95
|
readonly origin: Vec3;
|
|
88
96
|
readonly spacing: Vec3;
|
|
89
97
|
readonly counts: Vec3;
|
|
90
|
-
/** Probes in total
|
|
98
|
+
/** Probes in total: the array's layer count, or a third of it for a crossfading grid. */
|
|
91
99
|
readonly layers: number;
|
|
100
|
+
/** Whether sweeps blend into one another. See `ProbeGridOptions.crossfade`. */
|
|
101
|
+
readonly crossfade: boolean;
|
|
92
102
|
/** The reciprocal of the spacing, because the shader multiplies and so does everything here. */
|
|
93
103
|
readonly invSpacing: Vec3;
|
|
94
104
|
constructor(options: ProbeGridOptions);
|
package/dist/render/probeGrid.js
CHANGED
|
@@ -46,14 +46,16 @@ export function sameGrid(a, b) {
|
|
|
46
46
|
if (a.spacing[axis] !== b.spacing[axis])
|
|
47
47
|
return false;
|
|
48
48
|
}
|
|
49
|
-
return
|
|
49
|
+
return a.crossfade === b.crossfade;
|
|
50
50
|
}
|
|
51
51
|
export class ProbeGrid {
|
|
52
52
|
origin;
|
|
53
53
|
spacing;
|
|
54
54
|
counts;
|
|
55
|
-
/** Probes in total
|
|
55
|
+
/** Probes in total: the array's layer count, or a third of it for a crossfading grid. */
|
|
56
56
|
layers;
|
|
57
|
+
/** Whether sweeps blend into one another. See `ProbeGridOptions.crossfade`. */
|
|
58
|
+
crossfade;
|
|
57
59
|
/** The reciprocal of the spacing, because the shader multiplies and so does everything here. */
|
|
58
60
|
invSpacing;
|
|
59
61
|
constructor(options) {
|
|
@@ -91,6 +93,7 @@ export class ProbeGrid {
|
|
|
91
93
|
this.invSpacing = [1 / this.spacing[0], 1 / this.spacing[1], 1 / this.spacing[2]];
|
|
92
94
|
this.counts = counts;
|
|
93
95
|
this.layers = layers;
|
|
96
|
+
this.crossfade = options.crossfade === true;
|
|
94
97
|
}
|
|
95
98
|
/**
|
|
96
99
|
* The layer a probe at whole lattice coordinates occupies.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A probe grid's sweeps: which layer a bake writes, when a sweep is whole, and how far the shading
|
|
3
|
+
* has moved from the last whole sweep toward the newest.
|
|
4
|
+
*
|
|
5
|
+
* **A grid re-baked a probe a frame changes in steps.** Each probe holds the light of the moment it
|
|
6
|
+
* was baked until its turn comes round again, a sweep later, and then jumps to the light of that
|
|
7
|
+
* moment. Under a moving sun the jump is a pass of light across the scene once a sweep: reported as
|
|
8
|
+
* "a pass every second or two", which at sixty-four probes a frame at 60 Hz is exactly one sweep.
|
|
9
|
+
*
|
|
10
|
+
* **So a crossfading grid keeps three sets of layers**: the last whole sweep, the newest whole
|
|
11
|
+
* sweep, and the one being baked. The shading blends the first toward the second by how much of the
|
|
12
|
+
* third has been baked, so it reaches the newest sweep at the moment the next one is whole, and the
|
|
13
|
+
* light moves a sixty-fourth of a sweep a frame rather than a probe's worth at once. The set being
|
|
14
|
+
* written is never one the shading reads.
|
|
15
|
+
*
|
|
16
|
+
* What it gives up is three times the layers, and a sweep of lag: the light shown is a blend of what
|
|
17
|
+
* the grid saw one and two sweeps ago. What would make it wrong is a light that must follow a change
|
|
18
|
+
* faster than a sweep, which a burst of bakes serves, since the blend follows the bakes rather than
|
|
19
|
+
* the clock. A grid that does not crossfade is one set, ready once every probe is baked, as before.
|
|
20
|
+
*
|
|
21
|
+
* Backend-neutral, per the rule that a decision lives in one place and only the binding is per
|
|
22
|
+
* backend: both renderers ask this where to write and what to upload.
|
|
23
|
+
*/
|
|
24
|
+
export declare class ProbeSweeps {
|
|
25
|
+
/** Sets of layers the array holds: three for a crossfading grid, one otherwise. */
|
|
26
|
+
readonly sets: 1 | 3;
|
|
27
|
+
private readonly layers;
|
|
28
|
+
/** Which probes the sweep being baked has reached, and how many. */
|
|
29
|
+
private readonly reached;
|
|
30
|
+
private reachedCount;
|
|
31
|
+
private from;
|
|
32
|
+
private to;
|
|
33
|
+
private write;
|
|
34
|
+
/** Whether a whole sweep has been rotated in. */
|
|
35
|
+
private whole;
|
|
36
|
+
constructor(layers: number, crossfade: boolean);
|
|
37
|
+
/** The array layer a bake of probe `layer` writes. */
|
|
38
|
+
writeLayer(layer: number): number;
|
|
39
|
+
/** Record that probe `layer` was baked into the set being written; a whole sweep rotates. */
|
|
40
|
+
baked(layer: number): void;
|
|
41
|
+
/** Whether the shading may read the grid: once a sweep is whole. */
|
|
42
|
+
get ready(): boolean;
|
|
43
|
+
/** The layer offsets of the two sets shown and the blend between them, into `out`. */
|
|
44
|
+
uniforms(out: Float32Array): Float32Array;
|
|
45
|
+
}
|