@driftengine/core 4.3.0 → 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 +261 -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 +147 -1
- package/dist/render/backend/webgl2/renderer.js +402 -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 +174 -11
- package/dist/render/backend/webgpu/renderer.js +675 -249
- 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 +439 -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 +801 -270
- 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
|
@@ -46,6 +46,81 @@ const CONTENTION_BAND_M = 3;
|
|
|
46
46
|
/** Sort keys for the shadow list, which is ordered independently of the shaded one. */
|
|
47
47
|
const shadowRank = new Float32Array(POINT_SHADOW_POOL);
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Every light in range this frame, by source index and squared distance, before the nearest are
|
|
51
|
+
* chosen. Grown when a world offers more than it has held, and never shrunk, so a frame allocates
|
|
52
|
+
* only when the world has grown. Float64, so two lights a hair apart are ordered by their real
|
|
53
|
+
* distances rather than by what survives a float's rounding.
|
|
54
|
+
*/
|
|
55
|
+
let candidateIndex = new Int32Array(64);
|
|
56
|
+
let candidateDistance = new Float64Array(64);
|
|
57
|
+
|
|
58
|
+
function growCandidates(needed: number): void {
|
|
59
|
+
if (needed <= candidateIndex.length) return;
|
|
60
|
+
const size = Math.max(needed, candidateIndex.length * 2);
|
|
61
|
+
const index = new Int32Array(size);
|
|
62
|
+
const distance = new Float64Array(size);
|
|
63
|
+
index.set(candidateIndex);
|
|
64
|
+
distance.set(candidateDistance);
|
|
65
|
+
candidateIndex = index;
|
|
66
|
+
candidateDistance = distance;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Whether candidate `a` goes before candidate `b`: nearer, or as near and offered first. */
|
|
70
|
+
function before(a: number, b: number): boolean {
|
|
71
|
+
const da = candidateDistance[a] ?? 0;
|
|
72
|
+
const db = candidateDistance[b] ?? 0;
|
|
73
|
+
return da < db || (da === db && (candidateIndex[a] ?? 0) < (candidateIndex[b] ?? 0));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function swap(a: number, b: number): void {
|
|
77
|
+
const index = candidateIndex[a] ?? 0;
|
|
78
|
+
const distance = candidateDistance[a] ?? 0;
|
|
79
|
+
candidateIndex[a] = candidateIndex[b] ?? 0;
|
|
80
|
+
candidateDistance[a] = candidateDistance[b] ?? 0;
|
|
81
|
+
candidateIndex[b] = index;
|
|
82
|
+
candidateDistance[b] = distance;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Move the `keep` nearest of the first `count` candidates into `[0, keep)`, in no order: a
|
|
87
|
+
* quickselect, linear on average. The pivot is the median of three, which keeps a list that
|
|
88
|
+
* arrives already sorted — a world laid out along the camera's path — away from the quadratic case.
|
|
89
|
+
*/
|
|
90
|
+
function chooseNearest(count: number, keep: number): void {
|
|
91
|
+
let lo = 0;
|
|
92
|
+
let hi = count - 1;
|
|
93
|
+
while (lo < hi) {
|
|
94
|
+
const mid = (lo + hi) >> 1;
|
|
95
|
+
if (before(mid, lo)) swap(mid, lo);
|
|
96
|
+
if (before(hi, lo)) swap(hi, lo);
|
|
97
|
+
if (before(hi, mid)) swap(hi, mid);
|
|
98
|
+
swap(mid, hi);
|
|
99
|
+
let store = lo;
|
|
100
|
+
for (let at = lo; at < hi; at++) {
|
|
101
|
+
if (before(at, hi)) {
|
|
102
|
+
swap(at, store);
|
|
103
|
+
store++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
swap(store, hi);
|
|
107
|
+
if (store === keep || store === keep - 1) return;
|
|
108
|
+
if (store < keep) lo = store + 1;
|
|
109
|
+
else hi = store - 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Sort the first `count` candidates nearest first. An insertion sort: `count` is a slot budget. */
|
|
114
|
+
function sortCandidates(count: number): void {
|
|
115
|
+
for (let at = 1; at < count; at++) {
|
|
116
|
+
let back = at;
|
|
117
|
+
while (back > 0 && before(back, back - 1)) {
|
|
118
|
+
swap(back, back - 1);
|
|
119
|
+
back--;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
49
124
|
export interface PointLightSource {
|
|
50
125
|
x: number;
|
|
51
126
|
y: number;
|
|
@@ -105,6 +180,15 @@ export interface PointLightSource {
|
|
|
105
180
|
* They are floor lighting 35 cm off the deck and were never meant to cast at all.
|
|
106
181
|
*/
|
|
107
182
|
castsShadow?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Whether a DriftLight field also sums this light (`createLightField` sets it).
|
|
185
|
+
*
|
|
186
|
+
* Then the lit shader shades it exactly only where the choice is complete and lets the field's
|
|
187
|
+
* summed light take over past it, so the light is never counted twice. Carried to the shader as
|
|
188
|
+
* the sign of the light's weight, which both light paths already have: no new storage on a part
|
|
189
|
+
* whose uniform budget has none to spare.
|
|
190
|
+
*/
|
|
191
|
+
inLightField?: boolean;
|
|
108
192
|
}
|
|
109
193
|
|
|
110
194
|
export interface PointLightBuffer {
|
|
@@ -179,6 +263,13 @@ export interface PointLightBuffer {
|
|
|
179
263
|
*/
|
|
180
264
|
readonly shadowIndex: Int32Array;
|
|
181
265
|
shadowCount: number;
|
|
266
|
+
/**
|
|
267
|
+
* How far from the selection's centre the choice is complete for DriftLight field lights: a
|
|
268
|
+
* point nearer than this is reached only by field lights chosen at full strength. A field light
|
|
269
|
+
* left out at distance d reaching r first reaches d - r; one dimmed at the cut counts as left
|
|
270
|
+
* out. At most the view range, past which nothing is chosen.
|
|
271
|
+
*/
|
|
272
|
+
complete: number;
|
|
182
273
|
}
|
|
183
274
|
|
|
184
275
|
/**
|
|
@@ -213,6 +304,7 @@ export function createPointLightBuffer(capacity: number = MAX_POINT_LIGHTS): Poi
|
|
|
213
304
|
scratchDistance: new Float32Array(Math.max(shaded, POINT_SHADOW_POOL)),
|
|
214
305
|
shadowIndex: new Int32Array(POINT_SHADOW_POOL),
|
|
215
306
|
shadowCount: 0,
|
|
307
|
+
complete: 0,
|
|
216
308
|
};
|
|
217
309
|
}
|
|
218
310
|
|
|
@@ -268,6 +360,7 @@ export function selectPointLights(
|
|
|
268
360
|
* shader binds, which is the case where nothing is contested and nothing dims.
|
|
269
361
|
*/
|
|
270
362
|
let cutSq = Infinity;
|
|
363
|
+
let candidateCount = 0;
|
|
271
364
|
|
|
272
365
|
for (let sourceIndex = 0; sourceIndex < sources.length; sourceIndex++) {
|
|
273
366
|
const light = sources[sourceIndex];
|
|
@@ -305,84 +398,14 @@ export function selectPointLights(
|
|
|
305
398
|
*
|
|
306
399
|
* Sharing one sorted list is what made them drift apart, so they do not share one.
|
|
307
400
|
*/
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (out.count >= shadedCapacity) {
|
|
317
|
-
const displaced = out.scratchDistance[shadedCapacity - 1] ?? Infinity;
|
|
318
|
-
if (displaced < cutSq) cutSq = displaced;
|
|
319
|
-
}
|
|
320
|
-
const last = Math.min(out.count, shadedCapacity - 1);
|
|
321
|
-
for (let index = last; index > slot; index--) {
|
|
322
|
-
out.scratchDistance[index] = out.scratchDistance[index - 1] ?? 0;
|
|
323
|
-
out.radii[index] = out.radii[index - 1] ?? 0;
|
|
324
|
-
out.sourceRadii[index] = out.sourceRadii[index - 1] ?? 0;
|
|
325
|
-
out.sourceIndex[index] = out.sourceIndex[index - 1] ?? -1;
|
|
326
|
-
for (let component = 0; component < 3; component++) {
|
|
327
|
-
out.positions[index * 3 + component] = out.positions[(index - 1) * 3 + component] ?? 0;
|
|
328
|
-
out.colors[index * 3 + component] = out.colors[(index - 1) * 3 + component] ?? 0;
|
|
329
|
-
out.directions[index * 3 + component] = out.directions[(index - 1) * 3 + component] ?? 0;
|
|
330
|
-
}
|
|
331
|
-
out.coneCos[index * 2] = out.coneCos[(index - 1) * 2] ?? 0;
|
|
332
|
-
out.coneCos[index * 2 + 1] = out.coneCos[(index - 1) * 2 + 1] ?? 0;
|
|
333
|
-
out.iesProfiles[index] = out.iesProfiles[index - 1] ?? -1;
|
|
334
|
-
}
|
|
335
|
-
out.scratchDistance[slot] = distanceSq;
|
|
336
|
-
out.sourceIndex[slot] = sourceIndex;
|
|
337
|
-
out.radii[slot] = light.radius;
|
|
338
|
-
out.sourceRadii[slot] = Math.max(light.sourceRadius, 0);
|
|
339
|
-
out.positions[slot * 3] = light.x;
|
|
340
|
-
out.positions[slot * 3 + 1] = light.y;
|
|
341
|
-
out.positions[slot * 3 + 2] = light.z;
|
|
342
|
-
const scale = flickerScale(light, timeSeconds);
|
|
343
|
-
out.colors[slot * 3] = light.r * scale;
|
|
344
|
-
out.colors[slot * 3 + 1] = light.g * scale;
|
|
345
|
-
out.colors[slot * 3 + 2] = light.b * scale;
|
|
346
|
-
|
|
347
|
-
/*
|
|
348
|
-
* **Normalised here rather than trusted**, because the cone is a comparison against a cosine
|
|
349
|
-
* and an un-normalised direction scales that cosine — so a vector of length 2 gives a cone of
|
|
350
|
-
* the wrong width, which reads as the angle having been set wrong rather than the vector.
|
|
351
|
-
* A zero-length direction is a light that declared no cone, and it takes the open pair.
|
|
352
|
-
*/
|
|
353
|
-
const dx = light.dirX ?? 0;
|
|
354
|
-
const dy = light.dirY ?? 0;
|
|
355
|
-
const dz = light.dirZ ?? 0;
|
|
356
|
-
const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
357
|
-
const aimed = length > 1e-6 && light.coneOuterDeg !== undefined;
|
|
358
|
-
const inverse = aimed ? 1 / length : 0;
|
|
359
|
-
out.directions[slot * 3] = dx * inverse;
|
|
360
|
-
out.directions[slot * 3 + 1] = dy * inverse;
|
|
361
|
-
out.directions[slot * 3 + 2] = dz * inverse;
|
|
362
|
-
if (aimed) {
|
|
363
|
-
/*
|
|
364
|
-
* Clamped so the inner edge never passes the outer. A caller that swaps them would
|
|
365
|
-
* otherwise get `smoothstep` with its edges inverted, which is a cone that is dark in the
|
|
366
|
-
* middle and bright at the rim — a picture nobody would attribute to two numbers being the
|
|
367
|
-
* wrong way round.
|
|
368
|
-
*/
|
|
369
|
-
const outerDeg = Math.min(89.9, Math.max(0, light.coneOuterDeg ?? 45));
|
|
370
|
-
const innerDeg = Math.min(outerDeg, Math.max(0, light.coneInnerDeg ?? outerDeg * 0.75));
|
|
371
|
-
out.coneCos[slot * 2] = Math.cos((innerDeg * Math.PI) / 180);
|
|
372
|
-
out.coneCos[slot * 2 + 1] = Math.cos((outerDeg * Math.PI) / 180);
|
|
373
|
-
} else {
|
|
374
|
-
out.coneCos[slot * 2] = POINT_LIGHT_COS_INNER;
|
|
375
|
-
out.coneCos[slot * 2 + 1] = POINT_LIGHT_COS_OUTER;
|
|
376
|
-
}
|
|
377
|
-
/*
|
|
378
|
-
* The profile applies to a point light as readily as to a spot: a bare bulb has a measured
|
|
379
|
-
* distribution too, and the arithmetic does not care whether a cone is also being applied.
|
|
380
|
-
* Restricting it to spots would be a smaller feature for no saving.
|
|
381
|
-
*/
|
|
382
|
-
out.iesProfiles[slot] = light.iesProfile ?? -1;
|
|
383
|
-
|
|
384
|
-
if (out.count < shadedCapacity) out.count++;
|
|
385
|
-
}
|
|
401
|
+
/*
|
|
402
|
+
* A candidate for the shaded list. Which of them win is decided once every source has been
|
|
403
|
+
* seen, below, rather than by keeping a sorted list as they arrive: see `chooseNearest`.
|
|
404
|
+
*/
|
|
405
|
+
growCandidates(candidateCount + 1);
|
|
406
|
+
candidateIndex[candidateCount] = sourceIndex;
|
|
407
|
+
candidateDistance[candidateCount] = distanceSq;
|
|
408
|
+
candidateCount++;
|
|
386
409
|
|
|
387
410
|
/*
|
|
388
411
|
* Inside its own radius, so it is lighting something and therefore has something
|
|
@@ -420,6 +443,85 @@ export function selectPointLights(
|
|
|
420
443
|
if (out.shadowCount < POINT_SHADOW_POOL) out.shadowCount++;
|
|
421
444
|
}
|
|
422
445
|
|
|
446
|
+
/*
|
|
447
|
+
* The nearest `shadedCapacity` candidates, nearest first, and the nearest one left out.
|
|
448
|
+
*
|
|
449
|
+
* **Partitioned, then only the winners sorted.** This kept a sorted list as the sources arrived,
|
|
450
|
+
* shifting fourteen fields a slot to make room, which is O(sources × slots): 3.2 ms a frame to
|
|
451
|
+
* choose 320 of a courtyard's 10,000 candles, most of it moving lights that were then displaced.
|
|
452
|
+
* A quickselect finds the winners in time linear in the sources and the sort touches only them,
|
|
453
|
+
* and each winner is packed once. Ties go to the earlier source, as they did.
|
|
454
|
+
*/
|
|
455
|
+
const chosen = Math.min(candidateCount, shadedCapacity);
|
|
456
|
+
let complete = viewRange;
|
|
457
|
+
if (candidateCount > shadedCapacity) {
|
|
458
|
+
chooseNearest(candidateCount, shadedCapacity);
|
|
459
|
+
for (let at = shadedCapacity; at < candidateCount; at++) {
|
|
460
|
+
const distanceSq = candidateDistance[at] ?? Infinity;
|
|
461
|
+
if (distanceSq < cutSq) cutSq = distanceSq;
|
|
462
|
+
const left = sources[candidateIndex[at] ?? 0];
|
|
463
|
+
if (left?.inLightField === true) {
|
|
464
|
+
complete = Math.min(complete, Math.sqrt(distanceSq) - left.radius);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
sortCandidates(chosen);
|
|
469
|
+
for (let slot = 0; slot < chosen; slot++) {
|
|
470
|
+
const sourceIndex = candidateIndex[slot] ?? 0;
|
|
471
|
+
const light = sources[sourceIndex];
|
|
472
|
+
if (light === undefined) continue;
|
|
473
|
+
const distanceSq = candidateDistance[slot] ?? 0;
|
|
474
|
+
out.scratchDistance[slot] = distanceSq;
|
|
475
|
+
out.sourceIndex[slot] = sourceIndex;
|
|
476
|
+
out.radii[slot] = light.radius;
|
|
477
|
+
out.sourceRadii[slot] = Math.max(light.sourceRadius, 0);
|
|
478
|
+
out.positions[slot * 3] = light.x;
|
|
479
|
+
out.positions[slot * 3 + 1] = light.y;
|
|
480
|
+
out.positions[slot * 3 + 2] = light.z;
|
|
481
|
+
const scale = flickerScale(light, timeSeconds);
|
|
482
|
+
out.colors[slot * 3] = light.r * scale;
|
|
483
|
+
out.colors[slot * 3 + 1] = light.g * scale;
|
|
484
|
+
out.colors[slot * 3 + 2] = light.b * scale;
|
|
485
|
+
|
|
486
|
+
/*
|
|
487
|
+
* **Normalised here rather than trusted**, because the cone is a comparison against a cosine
|
|
488
|
+
* and an un-normalised direction scales that cosine — so a vector of length 2 gives a cone of
|
|
489
|
+
* the wrong width, which reads as the angle having been set wrong rather than the vector.
|
|
490
|
+
* A zero-length direction is a light that declared no cone, and it takes the open pair.
|
|
491
|
+
*/
|
|
492
|
+
const dx = light.dirX ?? 0;
|
|
493
|
+
const dy = light.dirY ?? 0;
|
|
494
|
+
const dz = light.dirZ ?? 0;
|
|
495
|
+
const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
496
|
+
const aimed = length > 1e-6 && light.coneOuterDeg !== undefined;
|
|
497
|
+
const inverse = aimed ? 1 / length : 0;
|
|
498
|
+
out.directions[slot * 3] = dx * inverse;
|
|
499
|
+
out.directions[slot * 3 + 1] = dy * inverse;
|
|
500
|
+
out.directions[slot * 3 + 2] = dz * inverse;
|
|
501
|
+
if (aimed) {
|
|
502
|
+
/*
|
|
503
|
+
* Clamped so the inner edge never passes the outer. A caller that swaps them would
|
|
504
|
+
* otherwise get `smoothstep` with its edges inverted, which is a cone that is dark in the
|
|
505
|
+
* middle and bright at the rim — a picture nobody would attribute to two numbers being the
|
|
506
|
+
* wrong way round.
|
|
507
|
+
*/
|
|
508
|
+
const outerDeg = Math.min(89.9, Math.max(0, light.coneOuterDeg ?? 45));
|
|
509
|
+
const innerDeg = Math.min(outerDeg, Math.max(0, light.coneInnerDeg ?? outerDeg * 0.75));
|
|
510
|
+
out.coneCos[slot * 2] = Math.cos((innerDeg * Math.PI) / 180);
|
|
511
|
+
out.coneCos[slot * 2 + 1] = Math.cos((outerDeg * Math.PI) / 180);
|
|
512
|
+
} else {
|
|
513
|
+
out.coneCos[slot * 2] = POINT_LIGHT_COS_INNER;
|
|
514
|
+
out.coneCos[slot * 2 + 1] = POINT_LIGHT_COS_OUTER;
|
|
515
|
+
}
|
|
516
|
+
/*
|
|
517
|
+
* The profile applies to a point light as readily as to a spot: a bare bulb has a measured
|
|
518
|
+
* distribution too, and the arithmetic does not care whether a cone is also being applied.
|
|
519
|
+
* Restricting it to spots would be a smaller feature for no saving.
|
|
520
|
+
*/
|
|
521
|
+
out.iesProfiles[slot] = light.iesProfile ?? -1;
|
|
522
|
+
}
|
|
523
|
+
out.count = chosen;
|
|
524
|
+
|
|
423
525
|
/*
|
|
424
526
|
* How present each of them is, now that the set — and with it the boundary — is
|
|
425
527
|
* known. A second pass over at most eight entries, which is why the cut can be a
|
|
@@ -434,8 +536,18 @@ export function selectPointLights(
|
|
|
434
536
|
const cullDistance = viewRange + (out.radii[index] ?? 0);
|
|
435
537
|
const arriving = (cullDistance - distance) / FADE_BAND_M;
|
|
436
538
|
const contested = (cut - distance) / CONTENTION_BAND_M;
|
|
437
|
-
out.
|
|
539
|
+
const field = sources[out.sourceIndex[index] ?? 0]?.inLightField === true;
|
|
540
|
+
/*
|
|
541
|
+
* **A field light does not fade in at the edge of the view range**, because the field is what
|
|
542
|
+
* stands in for it past the choice: the fade exists so a light does not switch on in front of
|
|
543
|
+
* the player, and a light the field sums was never off. Kept, it dimmed every light across a
|
|
544
|
+
* short range, and a dimmed light is an incomplete one, so the field took over at the camera.
|
|
545
|
+
*/
|
|
546
|
+
const weight = Math.max(0, Math.min(1, field ? contested : Math.min(arriving, contested)));
|
|
547
|
+
out.weights[index] = field ? -weight : weight;
|
|
548
|
+
if (field && weight < 1) complete = Math.min(complete, distance - (out.radii[index] ?? 0));
|
|
438
549
|
}
|
|
550
|
+
out.complete = Math.max(0, complete);
|
|
439
551
|
}
|
|
440
552
|
|
|
441
553
|
function flickerScale(light: PointLightSource, timeSeconds: number): number {
|
package/src/render/probeGrid.ts
CHANGED
|
@@ -34,6 +34,14 @@ export interface ProbeGridOptions {
|
|
|
34
34
|
* convolution and the shader instead of a grid path and a legacy path that drift.
|
|
35
35
|
*/
|
|
36
36
|
readonly counts: Vec3;
|
|
37
|
+
/**
|
|
38
|
+
* Blend each sweep of bakes into the next rather than showing each probe's bake the moment it
|
|
39
|
+
* lands. For a grid re-baked a probe a frame under a moving light, where each probe otherwise
|
|
40
|
+
* jumps once a sweep and the jumps travel across the scene as a pass of light. Costs three times
|
|
41
|
+
* the layers and a sweep of lag; ignored where indirect light is traced, which writes the grid's
|
|
42
|
+
* layers itself. See `probeSweeps.ts`. Off by default.
|
|
43
|
+
*/
|
|
44
|
+
readonly crossfade?: boolean;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
/**
|
|
@@ -97,15 +105,17 @@ export function sameGrid(a: ProbeGrid, b: ProbeGrid): boolean {
|
|
|
97
105
|
if (a.origin[axis] !== b.origin[axis]) return false;
|
|
98
106
|
if (a.spacing[axis] !== b.spacing[axis]) return false;
|
|
99
107
|
}
|
|
100
|
-
return
|
|
108
|
+
return a.crossfade === b.crossfade;
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
export class ProbeGrid {
|
|
104
112
|
readonly origin: Vec3;
|
|
105
113
|
readonly spacing: Vec3;
|
|
106
114
|
readonly counts: Vec3;
|
|
107
|
-
/** Probes in total
|
|
115
|
+
/** Probes in total: the array's layer count, or a third of it for a crossfading grid. */
|
|
108
116
|
readonly layers: number;
|
|
117
|
+
/** Whether sweeps blend into one another. See `ProbeGridOptions.crossfade`. */
|
|
118
|
+
readonly crossfade: boolean;
|
|
109
119
|
/** The reciprocal of the spacing, because the shader multiplies and so does everything here. */
|
|
110
120
|
readonly invSpacing: Vec3;
|
|
111
121
|
|
|
@@ -149,6 +159,7 @@ export class ProbeGrid {
|
|
|
149
159
|
this.invSpacing = [1 / this.spacing[0], 1 / this.spacing[1], 1 / this.spacing[2]];
|
|
150
160
|
this.counts = counts;
|
|
151
161
|
this.layers = layers;
|
|
162
|
+
this.crossfade = options.crossfade === true;
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
/**
|
|
@@ -0,0 +1,80 @@
|
|
|
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 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: number;
|
|
28
|
+
/** Which probes the sweep being baked has reached, and how many. */
|
|
29
|
+
private readonly reached: Uint8Array;
|
|
30
|
+
private reachedCount = 0;
|
|
31
|
+
private from = 0;
|
|
32
|
+
private to = 0;
|
|
33
|
+
private write = 0;
|
|
34
|
+
/** Whether a whole sweep has been rotated in. */
|
|
35
|
+
private whole = false;
|
|
36
|
+
|
|
37
|
+
constructor(layers: number, crossfade: boolean) {
|
|
38
|
+
this.layers = Math.max(1, Math.trunc(layers));
|
|
39
|
+
this.sets = crossfade ? 3 : 1;
|
|
40
|
+
this.reached = new Uint8Array(this.layers);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The array layer a bake of probe `layer` writes. */
|
|
44
|
+
writeLayer(layer: number): number {
|
|
45
|
+
return this.write * this.layers + layer;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Record that probe `layer` was baked into the set being written; a whole sweep rotates. */
|
|
49
|
+
baked(layer: number): void {
|
|
50
|
+
if (layer < 0 || layer >= this.layers || this.reached[layer] === 1) return;
|
|
51
|
+
this.reached[layer] = 1;
|
|
52
|
+
this.reachedCount++;
|
|
53
|
+
if (this.reachedCount < this.layers) return;
|
|
54
|
+
this.reached.fill(0);
|
|
55
|
+
this.reachedCount = 0;
|
|
56
|
+
if (this.sets === 1) {
|
|
57
|
+
this.whole = true;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
/* The newest is shown from now on; the first whole sweep is shown alone, blending into itself. */
|
|
61
|
+
this.from = this.whole ? this.to : this.write;
|
|
62
|
+
this.to = this.write;
|
|
63
|
+
this.whole = true;
|
|
64
|
+
/* The set neither end of the blend holds. */
|
|
65
|
+
this.write = this.from === this.to ? (this.to + 1) % 3 : 3 - this.from - this.to;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Whether the shading may read the grid: once a sweep is whole. */
|
|
69
|
+
get ready(): boolean {
|
|
70
|
+
return this.whole;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The layer offsets of the two sets shown and the blend between them, into `out`. */
|
|
74
|
+
uniforms(out: Float32Array): Float32Array {
|
|
75
|
+
out[0] = this.from * this.layers;
|
|
76
|
+
out[1] = this.to * this.layers;
|
|
77
|
+
out[2] = this.sets === 1 ? 0 : this.reachedCount / this.layers;
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -70,6 +70,52 @@ export interface ProbeBakeOptions {
|
|
|
70
70
|
* nothing gets the frame it already had.
|
|
71
71
|
*/
|
|
72
72
|
readonly irradiance?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Whether what the probe sees is lit by the grid's last bake, so each bake adds a bounce.
|
|
75
|
+
*
|
|
76
|
+
* Defaults to **false**, which is what every bake did before this existed. Then everything a
|
|
77
|
+
* probe sees is lit by the sun and by the hemispheric ambient, so the grid holds one bounce. An
|
|
78
|
+
* enclosed space is mostly lit at the second bounce and later. A courtyard's arcades are the
|
|
79
|
+
* case this was built for: one bounce came out about two stops under a reference render.
|
|
80
|
+
*
|
|
81
|
+
* With it on, the surfaces in each face take their ambient from the grid instead. Round-robin
|
|
82
|
+
* baking then converges on the space's full interreflection, about a bounce each time the grid
|
|
83
|
+
* is swept, and a scene holding one moment bakes the grid two or three times. It costs no pass
|
|
84
|
+
* and no memory. The faces are drawn into their own cube and only the resolve writes the array,
|
|
85
|
+
* so the array is never read and written in one pass.
|
|
86
|
+
*
|
|
87
|
+
* **Nothing changes until every probe has been baked once**, because until then the grid is not
|
|
88
|
+
* readable; the first sweep lights by the ambient as before. What it gives up: light that leaks
|
|
89
|
+
* through a wall into a probe is bounced again, so a leak grows with each sweep. What would make
|
|
90
|
+
* it wrong is a space with near-white walls, where a sweep adds almost as much as the last and
|
|
91
|
+
* convergence takes many sweeps.
|
|
92
|
+
*/
|
|
93
|
+
readonly bounce?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Which of the six faces this call draws, as `[first, count]`: all six when absent.
|
|
96
|
+
*
|
|
97
|
+
* **A bake is six draws of the scene, and a scene of millions of triangles makes it the most
|
|
98
|
+
* expensive thing in a frame.** Measured on a bought courtyard of eleven million: 6.4 ms of a
|
|
99
|
+
* 14 ms frame, on the frames it ran, which is a frame rate no display can hold steady. Spread over
|
|
100
|
+
* calls, each draws only its faces into the capture, and the convolution into the layer, which
|
|
101
|
+
* reads all six, runs when the sixth is drawn. Faces in the order a cube numbers them,
|
|
102
|
+
* `+X, -X, +Y, -Y, +Z, -Z`.
|
|
103
|
+
*
|
|
104
|
+
* **The caller finishes one probe before starting the next**, because the capture the faces are
|
|
105
|
+
* drawn into is one for the whole grid: faces of two probes interleaved make a cube of neither.
|
|
106
|
+
* What it gives up is a probe whose faces were drawn a few frames apart, under a light that moved
|
|
107
|
+
* a little between them.
|
|
108
|
+
*/
|
|
109
|
+
readonly faces?: readonly [first: number, count: number];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The faces a bake draws, `[first, end)`, from `ProbeBakeOptions.faces`. All six when absent. */
|
|
113
|
+
export function probeFaceRange(options: ProbeBakeOptions | undefined): [number, number] {
|
|
114
|
+
const asked = options?.faces;
|
|
115
|
+
if (asked === undefined) return [0, 6];
|
|
116
|
+
const first = Math.min(Math.max(Math.trunc(asked[0]), 0), 5);
|
|
117
|
+
const count = Math.max(Math.trunc(asked[1]), 1);
|
|
118
|
+
return [first, Math.min(first + count, 6)];
|
|
73
119
|
}
|
|
74
120
|
|
|
75
121
|
/**
|
|
@@ -407,12 +453,17 @@ export class ReflectionProbe {
|
|
|
407
453
|
* Returns whether the bake happened. A false means the allocation was refused and the caller
|
|
408
454
|
* should carry on without a probe, which is a picture with a gradient in its paint rather
|
|
409
455
|
* than a room.
|
|
456
|
+
*
|
|
457
|
+
* `first` and `end` draw only those faces, and the chain the roughness reads is built when the
|
|
458
|
+
* sixth is drawn. See `ProbeBakeOptions.faces`.
|
|
410
459
|
*/
|
|
411
460
|
bake(
|
|
412
461
|
gl: WebGL2RenderingContext,
|
|
413
462
|
origin: Vec3,
|
|
414
463
|
clearColor: Vec3,
|
|
415
464
|
drawFace: (camera: Camera) => void,
|
|
465
|
+
first = 0,
|
|
466
|
+
end = FACES.length,
|
|
416
467
|
): boolean {
|
|
417
468
|
if (this.unusable) return false;
|
|
418
469
|
|
|
@@ -429,7 +480,7 @@ export class ReflectionProbe {
|
|
|
429
480
|
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this.depth);
|
|
430
481
|
gl.viewport(0, 0, this.size, this.size);
|
|
431
482
|
|
|
432
|
-
for (let face =
|
|
483
|
+
for (let face = first; face < end; face++) {
|
|
433
484
|
const aim = FACES[face];
|
|
434
485
|
if (aim === undefined) continue;
|
|
435
486
|
gl.framebufferTexture2D(
|
|
@@ -439,7 +490,7 @@ export class ReflectionProbe {
|
|
|
439
490
|
this.texture,
|
|
440
491
|
0,
|
|
441
492
|
);
|
|
442
|
-
if (face ===
|
|
493
|
+
if (face === first && gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
|
|
443
494
|
this.unusable = true;
|
|
444
495
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
445
496
|
console.warn(
|
|
@@ -464,6 +515,7 @@ export class ReflectionProbe {
|
|
|
464
515
|
}
|
|
465
516
|
|
|
466
517
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
518
|
+
if (end < FACES.length) return true;
|
|
467
519
|
/* The blur the roughness reads. Once per bake, not once per frame. */
|
|
468
520
|
gl.bindTexture(gl.TEXTURE_CUBE_MAP, this.texture);
|
|
469
521
|
gl.generateMipmap(gl.TEXTURE_CUBE_MAP);
|
|
@@ -560,7 +560,9 @@ export interface RenderQuality {
|
|
|
560
560
|
*/
|
|
561
561
|
readonly reconstruction: number;
|
|
562
562
|
/**
|
|
563
|
-
* Light a scene from what bounces, rather than from what was baked
|
|
563
|
+
* Light a scene from what bounces, rather than from what was baked: **DriftRay**, this engine's
|
|
564
|
+
* ray tracing, rays marched through the declared distance fields in GPU compute rather than on
|
|
565
|
+
* the dedicated ray-tracing units WebGPU does not expose. **Off by default.**
|
|
564
566
|
*
|
|
565
567
|
* On, the probe grid the shading already samples is filled by tracing `render/gi/`'s three-level
|
|
566
568
|
* chain — the world distance field, and behind it the probes themselves, which is what makes the
|
|
@@ -49,9 +49,21 @@ export function recordingGl(
|
|
|
49
49
|
* `Infinity` by default, so a link only ever fails where a test asked for it.
|
|
50
50
|
*/
|
|
51
51
|
readonly refuseLinkAbove?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Uniforms every program reports as active, each located by a handle carrying its name.
|
|
54
|
+
*
|
|
55
|
+
* **None by default**, which ends every reflection loop at once and leaves every upload aimed
|
|
56
|
+
* at `null`. A test about what a frame *uploads* names the uniform here and then finds the
|
|
57
|
+
* calls whose location says that name, rather than guessing at an upload's position in the
|
|
58
|
+
* recording.
|
|
59
|
+
*/
|
|
60
|
+
readonly uniforms?: readonly string[];
|
|
52
61
|
} = {},
|
|
53
62
|
) {
|
|
54
63
|
const extensions = new Set(options.extensions ?? []);
|
|
64
|
+
const uniforms = options.uniforms ?? [];
|
|
65
|
+
/* What `clearDepth` last set, so a module that borrows the value can be seen to put it back. */
|
|
66
|
+
let depthClear = 1;
|
|
55
67
|
const reportedVectors = options.fragmentUniformVectors ?? 1024;
|
|
56
68
|
const enforcedVectors = options.refuseLinkAbove ?? Number.POSITIVE_INFINITY;
|
|
57
69
|
/** Fragment sources by shader handle, so a refusal can be decided from what was compiled. */
|
|
@@ -71,6 +83,7 @@ export function recordingGl(
|
|
|
71
83
|
MAX_TEXTURE_IMAGE_UNITS: 0x8872,
|
|
72
84
|
MAX_VERTEX_UNIFORM_VECTORS: 0x8dfb,
|
|
73
85
|
MAX_FRAGMENT_UNIFORM_VECTORS: 0x8dfd,
|
|
86
|
+
DEPTH_CLEAR_VALUE: 0x0b73,
|
|
74
87
|
/*
|
|
75
88
|
* Enums a test needs to compare against, rather than merely to pass through.
|
|
76
89
|
*
|
|
@@ -126,6 +139,7 @@ export function recordingGl(
|
|
|
126
139
|
if (pname === constants.MAX_SAMPLES) return 4;
|
|
127
140
|
if (pname === constants.MAX_FRAGMENT_UNIFORM_VECTORS) return reportedVectors;
|
|
128
141
|
if (pname === constants.MAX_VERTEX_UNIFORM_VECTORS) return 1024;
|
|
142
|
+
if (pname === constants.DEPTH_CLEAR_VALUE) return depthClear;
|
|
129
143
|
/* Generous by default: every remaining limit is a ceiling something is checked against,
|
|
130
144
|
and a small answer is read as a device that cannot do the thing. */
|
|
131
145
|
return 4096;
|
|
@@ -156,16 +170,24 @@ export function recordingGl(
|
|
|
156
170
|
/* Linked yes; zero active uniforms and attributes, which ends every reflection loop at once
|
|
157
171
|
and is why this harness does not have to know a single uniform name. */
|
|
158
172
|
getProgramParameter: (program: unknown, pname: unknown) => {
|
|
159
|
-
if (pname === constants.ACTIVE_UNIFORMS
|
|
173
|
+
if (pname === constants.ACTIVE_UNIFORMS) return uniforms.length;
|
|
174
|
+
if (pname === constants.ACTIVE_ATTRIBUTES) return 0;
|
|
160
175
|
if (pname !== constants.LINK_STATUS) return true;
|
|
161
176
|
if (!Number.isFinite(enforcedVectors)) return true;
|
|
162
177
|
if (typeof program !== 'object' || program === null) return true;
|
|
163
178
|
const source = programFragments.get(program);
|
|
164
179
|
return source === undefined || countUniformVectors(source) <= enforcedVectors;
|
|
165
180
|
},
|
|
166
|
-
getActiveUniform: (
|
|
181
|
+
getActiveUniform: (_program: unknown, index: unknown) => ({
|
|
182
|
+
name: uniforms[Number(index)] ?? '',
|
|
183
|
+
size: 1,
|
|
184
|
+
type: 0,
|
|
185
|
+
}),
|
|
167
186
|
getActiveAttrib: () => ({ name: '', size: 1, type: 0 }),
|
|
168
|
-
getUniformLocation: () => ({}),
|
|
187
|
+
getUniformLocation: (_program: unknown, name: unknown) => ({ name }),
|
|
188
|
+
clearDepth: (value: unknown) => {
|
|
189
|
+
depthClear = Number(value);
|
|
190
|
+
},
|
|
169
191
|
getAttribLocation: () => -1,
|
|
170
192
|
checkFramebufferStatus: () => constants.FRAMEBUFFER_COMPLETE,
|
|
171
193
|
getError: () => constants.NO_ERROR,
|