@driftengine/core 4.1.3 → 4.1.5
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 +3796 -3739
- package/README.md +1 -1
- package/dist/index.js +12 -2
- package/dist/render/backend/webgl2/renderer.d.ts +15 -0
- package/dist/render/backend/webgl2/renderer.js +28 -7
- package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +46 -0
- package/dist/render/backend/webgpu/gpuDrivenPass.js +105 -0
- package/dist/render/backend/webgpu/pointShadowPass.d.ts +4 -0
- package/dist/render/backend/webgpu/pointShadowPass.js +10 -0
- package/dist/render/backend/webgpu/renderer.js +17 -4
- package/dist/render/depthConvention.d.ts +19 -0
- package/dist/render/depthConvention.js +21 -0
- package/dist/render/livePointShadowSet.js +8 -0
- package/dist/render/pointShadowBudget.js +59 -5
- package/dist/render/pointShadowImage.d.ts +40 -0
- package/dist/render/pointShadowImage.js +20 -0
- package/dist/render/pointShadowMap.d.ts +4 -0
- package/dist/render/pointShadowMap.js +10 -0
- package/dist/render/pointShadowSystem.js +9 -0
- package/dist/render/renderQuality.d.ts +27 -4
- package/dist/render/renderQuality.js +1 -1
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +104 -10
- package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
- package/dist/render/shaders/flat/pointShadow.js +84 -46
- package/dist/render/shaders/flat/preamble.js +70 -9
- package/dist/render/shaders/generated/flat.wgsl.d.ts +16 -16
- package/dist/render/shaders/generated/flat.wgsl.js +32 -32
- package/dist/render/shaders/sky.d.ts +10 -6
- package/dist/render/shaders/sky.js +35 -2
- package/dist/render/textLayout.d.ts +58 -0
- package/dist/render/textLayout.js +83 -0
- package/dist/render/textRenderer.js +7 -3
- package/package.json +6 -6
- package/src/index.ts +12 -2
- package/src/render/backend/webgl2/renderer.ts +28 -7
- package/src/render/backend/webgpu/gpuDrivenPass.ts +105 -0
- package/src/render/backend/webgpu/pointShadowPass.ts +13 -0
- package/src/render/backend/webgpu/renderer.ts +26 -4
- package/src/render/depthConvention.ts +22 -0
- package/src/render/livePointShadowSet.ts +7 -0
- package/src/render/pointShadowBudget.ts +58 -5
- package/src/render/pointShadowImage.ts +51 -0
- package/src/render/pointShadowMap.ts +13 -0
- package/src/render/pointShadowSystem.ts +8 -0
- package/src/render/renderQuality.ts +28 -5
- package/src/render/shaders/flat/main.ts +104 -10
- package/src/render/shaders/flat/pointShadow.ts +84 -46
- package/src/render/shaders/flat/preamble.ts +70 -9
- package/src/render/shaders/generated/flat.wgsl.ts +32 -32
- package/src/render/shaders/sky.ts +37 -2
- package/src/render/textLayout.ts +88 -0
- package/src/render/textRenderer.ts +22 -3
|
@@ -104,6 +104,28 @@ export const REVERSED_DEPTH = true;
|
|
|
104
104
|
/** What a scene depth attachment is cleared to: the far plane. */
|
|
105
105
|
export const DEPTH_CLEAR = REVERSED_DEPTH ? 0 : 1;
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* The same value for a backend that learned its convention at runtime rather than at compile time.
|
|
109
|
+
*
|
|
110
|
+
* **`DEPTH_CLEAR` above says what the engine wants; this says what a context got.** Reversed depth
|
|
111
|
+
* on WebGL2 needs `EXT_clip_control`, and a context not granted it runs conventional depth while
|
|
112
|
+
* the constant still reads 0. Clearing to 0 with a `LEQUAL` compare rejects every fragment in the
|
|
113
|
+
* scene — every one sits at depth >= 0 and only depth <= 0 is admitted — so the frame that reaches
|
|
114
|
+
* the screen is the colour clear and nothing else.
|
|
115
|
+
*
|
|
116
|
+
* Reported from Firefox on Linux, which exposes no `EXT_clip_control`: every consumer drew one flat
|
|
117
|
+
* colour with its interface still on top, at a healthy sixty frames a second. The depth *compare*
|
|
118
|
+
* beside it was already chosen at runtime, and that asymmetry is what made this total rather than
|
|
119
|
+
* subtle — two halves of one convention would still have drawn a picture.
|
|
120
|
+
*
|
|
121
|
+
* **Here rather than in the backend** for the 2026-08-13 rule's first clause: the decision is
|
|
122
|
+
* backend-neutral and only the binding is per-backend. It is also what lets a test hold the clear
|
|
123
|
+
* and the compare to each other, which a private field on a renderer could not.
|
|
124
|
+
*/
|
|
125
|
+
export function depthClearFor(reversed: boolean): number {
|
|
126
|
+
return reversed ? 0 : 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
107
129
|
/** The WebGPU compare for "nearer than what is there". */
|
|
108
130
|
export const DEPTH_COMPARE: GPUCompareFunction = REVERSED_DEPTH ? 'greater' : 'less';
|
|
109
131
|
|
|
@@ -182,6 +182,8 @@ export class LivePointShadowSet<M extends PointShadowSource> {
|
|
|
182
182
|
out.liveFar.fill(1);
|
|
183
183
|
out.liveNear.fill(0.01);
|
|
184
184
|
out.liveSourceRadius.fill(0);
|
|
185
|
+
out.liveProjections.fill(0);
|
|
186
|
+
for (let k = 0; k < out.liveLayers.length; k++) out.liveProjections[k * 4 + 3] = 1;
|
|
185
187
|
/*
|
|
186
188
|
* Zero, then written under the owning light below. The weights used to be a straight copy of
|
|
187
189
|
* this set's own two, because the arrays were addressed by live slot; they are addressed by
|
|
@@ -214,6 +216,11 @@ export class LivePointShadowSet<M extends PointShadowSource> {
|
|
|
214
216
|
out.liveFar[shaderSlot] = map.far;
|
|
215
217
|
out.liveNear[shaderSlot] = map.near;
|
|
216
218
|
out.liveSourceRadius[shaderSlot] = map.sourceRadius;
|
|
219
|
+
/* Where the image was rendered from, not where the light is now. See `projections`. */
|
|
220
|
+
out.liveProjections[shaderSlot * 4] = map.originX;
|
|
221
|
+
out.liveProjections[shaderSlot * 4 + 1] = map.originY;
|
|
222
|
+
out.liveProjections[shaderSlot * 4 + 2] = map.originZ;
|
|
223
|
+
out.liveProjections[shaderSlot * 4 + 3] = map.far;
|
|
217
224
|
/*
|
|
218
225
|
* The handover weight *and* the image's own arrival, multiplied.
|
|
219
226
|
*
|
|
@@ -244,7 +244,24 @@ export function runPointShadowBakes<M extends StaleCheck, L extends BakeableLigh
|
|
|
244
244
|
scratch.staleOwner[slot] = lightIndex;
|
|
245
245
|
scratch.staleRun[slot] = 0;
|
|
246
246
|
}
|
|
247
|
-
|
|
247
|
+
/*
|
|
248
|
+
* **The run decays rather than resetting, and it climbs past the threshold before it stops.**
|
|
249
|
+
*
|
|
250
|
+
* 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, so a run that reset to zero
|
|
252
|
+
* lost the tracking allowance there and the map dropped back to two faces a frame. The shadow
|
|
253
|
+
* then travelled smoothly through the fast part of the wander and stepped through the slow
|
|
254
|
+
* part, which is not motion and not stillness: it reads as a vibration. Reported that way,
|
|
255
|
+
* against a brazier, once the tracking allowance was in.
|
|
256
|
+
*
|
|
257
|
+
* So the run is hysteresis. It climbs to twice the threshold, which buys a light that has
|
|
258
|
+
* earned the allowance three quiet frames before it loses it, and decays one frame at a time
|
|
259
|
+
* so a light that genuinely settles stops re-baking rather than holding a budget for ever.
|
|
260
|
+
*/
|
|
261
|
+
const previous = scratch.staleRun[slot] ?? 0;
|
|
262
|
+
const run = stale
|
|
263
|
+
? Math.min(previous + 1, CHRONIC_STALE_FRAMES * 2)
|
|
264
|
+
: Math.max(previous - 1, 0);
|
|
248
265
|
scratch.staleRun[slot] = run;
|
|
249
266
|
scratch.chronic[slot] = run >= CHRONIC_STALE_FRAMES ? 1 : 0;
|
|
250
267
|
}
|
|
@@ -259,6 +276,38 @@ export function runPointShadowBakes<M extends StaleCheck, L extends BakeableLigh
|
|
|
259
276
|
|
|
260
277
|
let faces = facesPerFrame;
|
|
261
278
|
let urgentSpent = false;
|
|
279
|
+
/*
|
|
280
|
+
* **One wandering light finishes its cube in the frame it started it**, the same allowance a
|
|
281
|
+
* light with no image at all gets, and for a reason that turns out to be the same one: a map
|
|
282
|
+
* dribbled out at two faces a frame is a map nobody can sample correctly until it lands.
|
|
283
|
+
*
|
|
284
|
+
* A cold light cannot be sampled *at all* until its six faces are in, so it takes the whole
|
|
285
|
+
* cube at once. A chronically stale one can be sampled throughout and is wrong in a quieter
|
|
286
|
+
* way: `planBake` pins the origin for the whole of a resumed bake, so six faces spread over
|
|
287
|
+
* three frames means the published origin steps once every third frame. The shader shoots from
|
|
288
|
+
* that origin, so the shadow it draws steps with it — reported on a brazier as motion that was
|
|
289
|
+
* "really fast, not smooth, snappy, and unrealistic", which is a 20 Hz sample of a flame that
|
|
290
|
+
* wanders about once a second.
|
|
291
|
+
*
|
|
292
|
+
* Given the cube in one frame the origin moves every frame and the shadow travels instead.
|
|
293
|
+
*
|
|
294
|
+
* **Bounded to one light, and it has to be.** Eight braziers each taking six passes is the 48
|
|
295
|
+
* passes and 89 ms `pointShadowFacesPerFrame` exists to prevent. One is the light being shaded
|
|
296
|
+
* that most recently went stale, which is the one somebody is standing next to; the rest keep
|
|
297
|
+
* dribbling under the ordinary budget, and their shadows step rather than travel. That is the
|
|
298
|
+
* right way round: a shadow you are looking at moves, and a shadow across the square does not
|
|
299
|
+
* cost the frame.
|
|
300
|
+
*
|
|
301
|
+
* It is spent *after* the cold and settling groups, because `planPointShadowBakes` orders the
|
|
302
|
+
* chronic ones last — so a light arriving in range still gets its first image ahead of a flame
|
|
303
|
+
* refining one it already has.
|
|
304
|
+
*
|
|
305
|
+
* **What would make it wrong** is a caster set heavy enough that six passes over it does not
|
|
306
|
+
* fit the frame. Measured against a game whose whole static world is about 8,500 triangles in
|
|
307
|
+
* a handful of draws; a world an order of magnitude heavier wants `pointShadowFacesPerFrame`
|
|
308
|
+
* raised or this given up, and `gpuTiming` is how to tell which.
|
|
309
|
+
*/
|
|
310
|
+
let trackingSpent = false;
|
|
262
311
|
for (let i = 0; i < planned && faces > 0; i++) {
|
|
263
312
|
const slot = scratch.order[i] ?? -1;
|
|
264
313
|
/*
|
|
@@ -270,10 +319,14 @@ export function runPointShadowBakes<M extends StaleCheck, L extends BakeableLigh
|
|
|
270
319
|
const map = lightIndex >= 0 ? pool.mapForLight(lightIndex) : undefined;
|
|
271
320
|
if (light === undefined || map === undefined) continue;
|
|
272
321
|
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
322
|
+
const sampled = slot < pool.sampledCount;
|
|
323
|
+
const cold = sampled && scratch.ready[slot] === 0;
|
|
324
|
+
const urgent = cold && !urgentSpent;
|
|
325
|
+
/* A light being shaded whose map will be stale again next frame however it is served. */
|
|
326
|
+
const tracking = !cold && sampled && scratch.chronic[slot] === 1 && !trackingSpent;
|
|
327
|
+
if (urgent) urgentSpent = true;
|
|
328
|
+
if (tracking) trackingSpent = true;
|
|
329
|
+
faces -= bake(map, light, staticCasters, urgent || tracking ? faceCount : faces);
|
|
277
330
|
}
|
|
278
331
|
|
|
279
332
|
/*
|
|
@@ -202,6 +202,27 @@ export class PointShadowImage {
|
|
|
202
202
|
private pendingNear = 0;
|
|
203
203
|
private pendingSourceRadius = 0;
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Where this image was rendered from, which is what a sampler has to shoot its ray from.
|
|
207
|
+
*
|
|
208
|
+
* **Not the light's position**, and the difference is the whole point: `matchesSource` lets a
|
|
209
|
+
* light drift up to the caller's rebake tolerance from here before the image is re-rendered,
|
|
210
|
+
* so for any light that wanders these two are apart on nearly every frame. A shader that
|
|
211
|
+
* builds its direction and its distance from the live light asks this picture a question the
|
|
212
|
+
* picture cannot answer. See `ResolvedPointShadows.origins`.
|
|
213
|
+
*/
|
|
214
|
+
get originX(): number {
|
|
215
|
+
return this.x;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
get originY(): number {
|
|
219
|
+
return this.y;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
get originZ(): number {
|
|
223
|
+
return this.z;
|
|
224
|
+
}
|
|
225
|
+
|
|
205
226
|
/** Whether this map holds an image worth sampling. False until a bake completes. */
|
|
206
227
|
get hasBaked(): boolean {
|
|
207
228
|
return this.baked;
|
|
@@ -401,6 +422,10 @@ export interface PointShadowSource {
|
|
|
401
422
|
readonly far: number;
|
|
402
423
|
readonly near: number;
|
|
403
424
|
readonly sourceRadius: number;
|
|
425
|
+
/** Where the image was rendered from. See `PointShadowImage.originX`. */
|
|
426
|
+
readonly originX: number;
|
|
427
|
+
readonly originY: number;
|
|
428
|
+
readonly originZ: number;
|
|
404
429
|
/** Whether this map holds an image worth sampling. */
|
|
405
430
|
readonly hasBaked: boolean;
|
|
406
431
|
/** How present the image is, 0 to 1. */
|
|
@@ -433,6 +458,28 @@ export interface ResolvedPointShadows {
|
|
|
433
458
|
readonly far: Float32Array;
|
|
434
459
|
readonly near: Float32Array;
|
|
435
460
|
readonly sourceRadius: Float32Array;
|
|
461
|
+
/**
|
|
462
|
+
* The projection each bound map was rendered under: origin in `xyz`, far plane in `w`, four
|
|
463
|
+
* floats a light. The origin is **not** the light's position.
|
|
464
|
+
*
|
|
465
|
+
* A light is allowed to drift `pointShadowRebakeDistance` from its image before the image is
|
|
466
|
+
* re-rendered — that tolerance is what stops a flame re-baking six faces of the static world
|
|
467
|
+
* every frame — so for anything that flickers these two are apart on nearly every frame. The
|
|
468
|
+
* shader builds both its sample direction and the distance it compares from this point, so the
|
|
469
|
+
* ray it shoots is the ray the picture was drawn along and the drift costs nothing.
|
|
470
|
+
*
|
|
471
|
+
* **It used to build them from the live light**, and the drift went straight into the
|
|
472
|
+
* comparison: `dist` moved one for one with it against a bias of a few centimetres, and the
|
|
473
|
+
* single-tap blocker search moved across the occluder's silhouette, swapping `occluderDistance`
|
|
474
|
+
* between the caster and nothing. Reported on a brazier at night as a hard square under the
|
|
475
|
+
* fire, flashing in time with the flame while the camera stood still.
|
|
476
|
+
*
|
|
477
|
+
* **The far plane rides in `w` rather than in an array of its own**, because a default-block
|
|
478
|
+
* `float[N]` spends a whole uniform vector per element: declared separately the origin cost
|
|
479
|
+
* sixteen rows on each of the two sets, which the budget ladder answers on a 256-vector
|
|
480
|
+
* device by halving `MAX_LIGHTS`. See `uPointShadowProjection` in the preamble.
|
|
481
|
+
*/
|
|
482
|
+
readonly projections: Float32Array;
|
|
436
483
|
/** How present each bound map is, so a re-baked one arrives rather than appears. */
|
|
437
484
|
readonly presence: Float32Array;
|
|
438
485
|
/** The live pair, by the same light index: a light owns at most one of the two. */
|
|
@@ -440,6 +487,8 @@ export interface ResolvedPointShadows {
|
|
|
440
487
|
readonly liveFar: Float32Array;
|
|
441
488
|
readonly liveNear: Float32Array;
|
|
442
489
|
readonly liveSourceRadius: Float32Array;
|
|
490
|
+
/** The live pair's own, by the same rule and for the same reason. */
|
|
491
|
+
readonly liveProjections: Float32Array;
|
|
443
492
|
readonly liveWeights: Float32Array;
|
|
444
493
|
}
|
|
445
494
|
|
|
@@ -460,11 +509,13 @@ export function createResolvedPointShadows(lights: number): ResolvedPointShadows
|
|
|
460
509
|
far: new Float32Array(lights),
|
|
461
510
|
near: new Float32Array(lights),
|
|
462
511
|
sourceRadius: new Float32Array(lights),
|
|
512
|
+
projections: new Float32Array(lights * 4),
|
|
463
513
|
presence: new Float32Array(lights),
|
|
464
514
|
liveLayers: new Int32Array(lights).fill(-1),
|
|
465
515
|
liveFar: new Float32Array(lights),
|
|
466
516
|
liveNear: new Float32Array(lights),
|
|
467
517
|
liveSourceRadius: new Float32Array(lights),
|
|
518
|
+
liveProjections: new Float32Array(lights * 4),
|
|
468
519
|
liveWeights: new Float32Array(lights),
|
|
469
520
|
};
|
|
470
521
|
}
|
|
@@ -73,6 +73,19 @@ export class PointShadowMap {
|
|
|
73
73
|
return this.image.sourceRadius;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** Where the image was rendered from, not where the light is now. See the image. */
|
|
77
|
+
get originX(): number {
|
|
78
|
+
return this.image.originX;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get originY(): number {
|
|
82
|
+
return this.image.originY;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get originZ(): number {
|
|
86
|
+
return this.image.originZ;
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
/** Whether this map holds an image worth sampling. False until a bake completes. */
|
|
77
90
|
get hasBaked(): boolean {
|
|
78
91
|
return this.image.hasBaked;
|
|
@@ -343,6 +343,9 @@ export class PointShadowSystem<M extends PointShadowSource> {
|
|
|
343
343
|
out.far.fill(1);
|
|
344
344
|
out.near.fill(0.01);
|
|
345
345
|
out.sourceRadius.fill(0);
|
|
346
|
+
/* Origin zero, far plane one: the same neutral the separate `far` array is filled with. */
|
|
347
|
+
out.projections.fill(0);
|
|
348
|
+
for (let k = 0; k < MAX_POINT_LIGHTS; k++) out.projections[k * 4 + 3] = 1;
|
|
346
349
|
out.presence.fill(0);
|
|
347
350
|
|
|
348
351
|
for (let k = 0; k < MAX_POINT_LIGHTS; k++) {
|
|
@@ -369,6 +372,11 @@ export class PointShadowSystem<M extends PointShadowSource> {
|
|
|
369
372
|
out.far[light] = map.far;
|
|
370
373
|
out.near[light] = map.near;
|
|
371
374
|
out.sourceRadius[light] = map.sourceRadius;
|
|
375
|
+
/* Where the image was rendered from, not where the light is now. See `projections`. */
|
|
376
|
+
out.projections[light * 4] = map.originX;
|
|
377
|
+
out.projections[light * 4 + 1] = map.originY;
|
|
378
|
+
out.projections[light * 4 + 2] = map.originZ;
|
|
379
|
+
out.projections[light * 4 + 3] = map.far;
|
|
372
380
|
out.presence[light] = map.presence;
|
|
373
381
|
out.layers[light] = map.layer;
|
|
374
382
|
}
|
|
@@ -162,10 +162,33 @@ export interface RenderQuality {
|
|
|
162
162
|
* sampled lights flickering at once accounted for 174 of the frame's 296 draw
|
|
163
163
|
* calls, and for the game being unplayable exactly where lamps cluster.
|
|
164
164
|
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
165
|
+
* **It was 0.4 until 4.1.5, chosen to clear a flame's whole wander with room to spare** — so
|
|
166
|
+
* a flame never re-baked at all, and its shadow of the static world stood still while the live
|
|
167
|
+
* map under the same light, which is re-rendered every frame because it holds the movers, swung
|
|
168
|
+
* with the flame. One light, two shadows, visibly disagreeing about where it was. Reported as
|
|
169
|
+
* the runner's shadow following the fire and the brazier's not.
|
|
170
|
+
*
|
|
171
|
+
* **A millimetre instead, which is as near to "whenever it moved" as makes sense**, so a shadow
|
|
172
|
+
* travels with its light rather than freezing between re-bakes. A centimetre was tried first and
|
|
173
|
+
* left a visible vibration: a light wandering on a curve moves less than that in a frame near
|
|
174
|
+
* the turning points of its travel, so the shadow ran smoothly through the fast part and stepped
|
|
175
|
+
* through the slow part. What the tolerance is still for is not re-baking on arithmetic noise.
|
|
176
|
+
*
|
|
177
|
+
* A light that does not move never goes stale at any positive value, so the whole cost falls on
|
|
178
|
+
* the lights that need it, and it is bounded rather than proportional: one such light takes a
|
|
179
|
+
* whole cube in a frame and the rest dribble under the ordinary face budget.
|
|
180
|
+
*
|
|
181
|
+
* **What makes that affordable is not this number.** The 174-of-296 draw calls this paragraph
|
|
182
|
+
* used to cite was measured before `pointShadowFacesPerFrame` capped the whole static bake, and
|
|
183
|
+
* before `planPointShadowBakes` learned to put a chronically stale map behind every cold and
|
|
184
|
+
* settling one. A flame now spends leftover budget and can never hold another light's first
|
|
185
|
+
* image hostage. One such light also finishes its cube inside a single frame rather than
|
|
186
|
+
* dribbling it over three, which is what makes the motion smooth instead of stepped; see
|
|
187
|
+
* `pointShadowBudget.ts`.
|
|
188
|
+
*
|
|
189
|
+
* **What would make it wrong** is a caster set heavy enough that six passes over it does not
|
|
190
|
+
* fit the frame, where a stepped shadow is the better trade and this should go back up.
|
|
191
|
+
* `gpuTiming` is how to tell which, and it is a number rather than an opinion.
|
|
169
192
|
*/
|
|
170
193
|
readonly pointShadowRebakeDistance: number;
|
|
171
194
|
/**
|
|
@@ -815,7 +838,7 @@ export const DEFAULT_RENDER_QUALITY: Readonly<RenderQuality> = Object.freeze({
|
|
|
815
838
|
directionalShadowDepthLayers: 2,
|
|
816
839
|
directionalShadowMaxDistance: 6,
|
|
817
840
|
directionalShadowMaxSlope: 3,
|
|
818
|
-
pointShadowRebakeDistance: 0.
|
|
841
|
+
pointShadowRebakeDistance: 0.001,
|
|
819
842
|
pointShadowFacesPerFrame: 2,
|
|
820
843
|
/*
|
|
821
844
|
* Twelve, which is `LIVE_POINT_SHADOW_MAPS` cubemaps of six faces: exactly what the live
|
|
@@ -793,6 +793,27 @@ export const MAIN_GLSL = `void main() {
|
|
|
793
793
|
*/
|
|
794
794
|
float lightShade = 1.0;
|
|
795
795
|
|
|
796
|
+
/*
|
|
797
|
+
* **The lamps' light twice: once as it arrives, once as their shadows leave it.**
|
|
798
|
+
*
|
|
799
|
+
* Every point light's contribution used to go straight into \`lit\`. It is accumulated here
|
|
800
|
+
* instead, in two sums that differ only by the shadow term, so that after the loop there is
|
|
801
|
+
* a single number per channel saying how much of the lamps' light survived — and *that* is
|
|
802
|
+
* the thing the resolve below can average across a pair of pixels without touching albedo,
|
|
803
|
+
* a highlight, or anything else the frame has in it.
|
|
804
|
+
*
|
|
805
|
+
* **The two sums are not an approximation.** \`lampOpen\` multiplied by
|
|
806
|
+
* \`lampShadowed / lampOpen\` is \`lampShadowed\`, per channel, whatever mixture of lamps
|
|
807
|
+
* and shadows produced it. With the resolve switched off this is arithmetically the line it
|
|
808
|
+
* replaces; a lamp that is shadowed and a lamp beside it that is not keep their own shares.
|
|
809
|
+
*
|
|
810
|
+
* **What it costs** is two vector adds per light and six floats of live state. **What would
|
|
811
|
+
* make it wrong** is a term wanting to sit between the lamps and \`lit\` — a per-light
|
|
812
|
+
* tone map, say — because these two are summed before anything can get between them.
|
|
813
|
+
*/
|
|
814
|
+
vec3 lampOpen = vec3(0.0);
|
|
815
|
+
vec3 lampShadowed = vec3(0.0);
|
|
816
|
+
|
|
796
817
|
/*
|
|
797
818
|
* Which froxel this fragment is in.
|
|
798
819
|
*
|
|
@@ -969,7 +990,7 @@ export const MAIN_GLSL = `void main() {
|
|
|
969
990
|
#if POINT_SHADOWS
|
|
970
991
|
float grazing = 1.0 - ndl;
|
|
971
992
|
float normalOffset = (0.06 + dist * 0.05) * grazing * grazing + 0.02;
|
|
972
|
-
vec3
|
|
993
|
+
vec3 receiverPoint = vWorldPos + n * normalOffset;
|
|
973
994
|
|
|
974
995
|
/*
|
|
975
996
|
* uPointShadowWeight is how present the cubemap is, and it is the difference
|
|
@@ -1010,8 +1031,15 @@ export const MAIN_GLSL = `void main() {
|
|
|
1010
1031
|
pointShadow(
|
|
1011
1032
|
uPointShadows,
|
|
1012
1033
|
float(layer),
|
|
1013
|
-
|
|
1014
|
-
|
|
1034
|
+
/*
|
|
1035
|
+
* **From where the map was baked, not from where the light is.** The two are apart
|
|
1036
|
+
* by up to \`pointShadowRebakeDistance\` for anything that wanders, and both things
|
|
1037
|
+
* this asks the picture move with that gap: the distance it compares against the
|
|
1038
|
+
* stored one, and which texel the single-tap blocker search lands in. See
|
|
1039
|
+
* \`uPointShadowOrigin\`.
|
|
1040
|
+
*/
|
|
1041
|
+
receiverPoint - uPointShadowProjection[shadowRead].xyz,
|
|
1042
|
+
uPointShadowProjection[shadowRead].w,
|
|
1015
1043
|
uPointShadowNear[shadowRead],
|
|
1016
1044
|
uPointShadowSize[shadowRead]
|
|
1017
1045
|
),
|
|
@@ -1030,8 +1058,9 @@ export const MAIN_GLSL = `void main() {
|
|
|
1030
1058
|
liveOccl = pointShadow(
|
|
1031
1059
|
uPointShadows,
|
|
1032
1060
|
float(liveLayer),
|
|
1033
|
-
|
|
1034
|
-
|
|
1061
|
+
/* Its own bake origin, for the reason the static layer above gives. */
|
|
1062
|
+
receiverPoint - uLivePointShadowProjection[shadowRead].xyz,
|
|
1063
|
+
uLivePointShadowProjection[shadowRead].w,
|
|
1035
1064
|
uLivePointShadowNear[shadowRead],
|
|
1036
1065
|
uLivePointShadowSize[shadowRead]
|
|
1037
1066
|
);
|
|
@@ -1192,7 +1221,13 @@ export const MAIN_GLSL = `void main() {
|
|
|
1192
1221
|
*
|
|
1193
1222
|
* At metal 0 this is one multiply by 1.0 away from the expression it replaces.
|
|
1194
1223
|
*/
|
|
1195
|
-
|
|
1224
|
+
/*
|
|
1225
|
+
* Held back from \`lit\` until the loop is over, so the shadow can be resolved across a
|
|
1226
|
+
* pair of pixels first. See the block below the loop; nothing else about this term moved.
|
|
1227
|
+
*/
|
|
1228
|
+
vec3 lampDiffuse = albedo * lightColor * ndl * shape * lightWeight * (1.0 - metal);
|
|
1229
|
+
lampOpen += lampDiffuse;
|
|
1230
|
+
lampShadowed += lampDiffuse * shaded;
|
|
1196
1231
|
|
|
1197
1232
|
/*
|
|
1198
1233
|
* A lamp reflected in a polished surface, which point lights could not do at all.
|
|
@@ -1222,8 +1257,12 @@ export const MAIN_GLSL = `void main() {
|
|
|
1222
1257
|
vec3(1.0),
|
|
1223
1258
|
pow(1.0 - max(dot(toEyeLamp, lampHalfway), 0.0), 5.0) * metal
|
|
1224
1259
|
);
|
|
1225
|
-
|
|
1226
|
-
|
|
1260
|
+
/* Held back with the diffuse half, and shadowed by the same term it always was. */
|
|
1261
|
+
vec3 lampHighlight =
|
|
1262
|
+
lightColor * sphereLobe(max(dot(n, lampHalfway), 0.0), surfaceRoughness, lightSourceRadius, dist)
|
|
1263
|
+
* lampSpec * shape * lightWeight;
|
|
1264
|
+
lampOpen += lampHighlight;
|
|
1265
|
+
lampShadowed += lampHighlight * shaded;
|
|
1227
1266
|
}
|
|
1228
1267
|
/*
|
|
1229
1268
|
* The most any light in range shadows this point, for the emissive term below —
|
|
@@ -1371,7 +1410,10 @@ export const MAIN_GLSL = `void main() {
|
|
|
1371
1410
|
}
|
|
1372
1411
|
#endif
|
|
1373
1412
|
|
|
1374
|
-
|
|
1413
|
+
/* Held back with the point lamps', and shadowed by the same term it always was. */
|
|
1414
|
+
vec3 areaDiffuse = albedo * uAreaLightColor[a] * form * (1.0 - metal);
|
|
1415
|
+
lampOpen += areaDiffuse;
|
|
1416
|
+
lampShadowed += areaDiffuse * areaOccl;
|
|
1375
1417
|
|
|
1376
1418
|
if (vSpecular > 0.0 || metal > 0.0) {
|
|
1377
1419
|
/*
|
|
@@ -1396,11 +1438,63 @@ export const MAIN_GLSL = `void main() {
|
|
|
1396
1438
|
if (coverage > 0.0) {
|
|
1397
1439
|
vec2 areaDfg = envBrdfApprox(max(dot(n, toEyeArea), 0.0), surfaceRoughness);
|
|
1398
1440
|
vec3 areaSpec = specColor * areaDfg.x + vec3(areaDfg.y);
|
|
1399
|
-
|
|
1441
|
+
vec3 areaHighlight = areaSpec * uAreaLightColor[a] * coverage;
|
|
1442
|
+
lampOpen += areaHighlight;
|
|
1443
|
+
lampShadowed += areaHighlight * areaOccl;
|
|
1400
1444
|
}
|
|
1401
1445
|
}
|
|
1402
1446
|
}
|
|
1403
1447
|
|
|
1448
|
+
/*
|
|
1449
|
+
* **The other half of the shadow filter: the two pixels of a column pair, folded together.**
|
|
1450
|
+
*
|
|
1451
|
+
* \`pointShadow\` turns its twelve taps by half a golden angle on every other column, so a
|
|
1452
|
+
* pair of neighbouring pixels holds twenty-four places on the disk between them and neither
|
|
1453
|
+
* holds more than twelve. This is where the pair becomes one filter. \`dFdx\` of a value is
|
|
1454
|
+
* the difference across the pair, and half of it, signed by which of the two this pixel is,
|
|
1455
|
+
* is exactly the average of both — so each pixel ends up shaded by all twenty-four taps
|
|
1456
|
+
* while paying for twelve. It is the pairing \`ambientOcclusion.ts\` describes between its
|
|
1457
|
+
* rotation tile and its blur, at the smallest size that needs no second pass.
|
|
1458
|
+
*
|
|
1459
|
+
* **After both lamp loops rather than beside the taps, and that is why this block exists at
|
|
1460
|
+
* all.** A derivative is only defined where every pixel of the quad reaches it, and neither
|
|
1461
|
+
* loop above reaches anything uniformly: the point loop \`continue\`s on falloff and on
|
|
1462
|
+
* facing, and in the clustered arm its trip count is read from a froxel, so two neighbouring
|
|
1463
|
+
* pixels can be shading different lights with a froxel boundary drawn across the screen
|
|
1464
|
+
* where they stop agreeing. A \`dFdx\` in there is the 2026-08-07 rule's own case, and it
|
|
1465
|
+
* would also invite a compiler to flatten the loop. Measured, too: with the turn taken from
|
|
1466
|
+
* a per-fragment hash instead of the column, the same resolve made the penumbra *worse*
|
|
1467
|
+
* than no resolve at all — 3.14 against 3.00 — because the hardware here answers a coarse
|
|
1468
|
+
* derivative and the second row of a quad then borrows the first row's difference. The
|
|
1469
|
+
* column tile is what makes that borrowing correct, so the two halves are one mechanism.
|
|
1470
|
+
*
|
|
1471
|
+
* Both lamp kinds are in the sums, because a rectangle's shadow is filtered by the same
|
|
1472
|
+
* twelve taps and stippled the same way.
|
|
1473
|
+
*
|
|
1474
|
+
* **Clamped, and the clamp is a guard rather than tidying.** A fragment killed by the
|
|
1475
|
+
* cutout or the clip plane above leaves its lane without a value for its neighbour to
|
|
1476
|
+
* difference against, and GLSL ES says nothing about what comes back. What could reach the
|
|
1477
|
+
* frame is a shadow term outside 0..1 on the pixel beside a cutout hole, which the clamp
|
|
1478
|
+
* turns into a pixel that is merely fully lit or fully shadowed. **What would make this
|
|
1479
|
+
* wrong** is a consumer whose cutout edges are most of the frame — dense foliage drawn as
|
|
1480
|
+
* one alpha-tested sheet — where the honest answer is to resolve in a pass of its own,
|
|
1481
|
+
* against a shadow term written to a target.
|
|
1482
|
+
*
|
|
1483
|
+
* The floor under the divisor costs nothing and answers the case where no lamp reached this
|
|
1484
|
+
* pixel at all: both sums are zero there, so the ratio is zero and multiplying it back by
|
|
1485
|
+
* the floor adds nothing.
|
|
1486
|
+
*/
|
|
1487
|
+
vec3 lampOpenFloor = max(lampOpen, vec3(1e-6));
|
|
1488
|
+
vec3 lampShade = lampShadowed / lampOpenFloor;
|
|
1489
|
+
#if POINT_SHADOWS
|
|
1490
|
+
lampShade = clamp(
|
|
1491
|
+
lampShade - dFdx(lampShade) * (float(int(gl_FragCoord.x) & 1) - 0.5),
|
|
1492
|
+
0.0,
|
|
1493
|
+
1.0
|
|
1494
|
+
);
|
|
1495
|
+
#endif
|
|
1496
|
+
lit += lampOpenFloor * lampShade;
|
|
1497
|
+
|
|
1404
1498
|
/*
|
|
1405
1499
|
* Self-illuminated geometry, dimmed where a lamp's shadow crosses it.
|
|
1406
1500
|
*
|