@vitreajs/vitrea 0.1.0 → 0.2.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/README.md +226 -22
- package/dist/{dist-FGJI5LQM.js → dist-RQ4ZMA3D.js} +984 -98
- package/dist/dist-RQ4ZMA3D.js.map +1 -0
- package/dist/index.d.ts +555 -39
- package/dist/index.js +129 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/dist-FGJI5LQM.js.map +0 -1
|
@@ -83,6 +83,30 @@ fn cs_analysis(@builtin(local_invocation_index) lane : u32) {
|
|
|
83
83
|
}
|
|
84
84
|
}`;
|
|
85
85
|
|
|
86
|
+
// ../policy/dist/refraction.js
|
|
87
|
+
var REFRACTION_LADDER = ["none", "approximate", "true"];
|
|
88
|
+
function refractionRank(quality) {
|
|
89
|
+
return REFRACTION_LADDER.indexOf(quality);
|
|
90
|
+
}
|
|
91
|
+
function accessibilityRefractionCap(policy) {
|
|
92
|
+
switch (policy.refraction) {
|
|
93
|
+
case "nominal":
|
|
94
|
+
return "true";
|
|
95
|
+
case "reduced":
|
|
96
|
+
return "approximate";
|
|
97
|
+
case "none":
|
|
98
|
+
return "none";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function effectiveRefraction(a, b) {
|
|
102
|
+
return refractionRank(a) <= refractionRank(b) ? a : b;
|
|
103
|
+
}
|
|
104
|
+
var DEFAULT_REFRACTION_SCALE = {
|
|
105
|
+
none: 0,
|
|
106
|
+
approximate: 0.45,
|
|
107
|
+
true: 1
|
|
108
|
+
};
|
|
109
|
+
|
|
86
110
|
// ../renderer-webgpu/dist/color.js
|
|
87
111
|
var BACKDROP_COLOR_SPACES = ["srgb", "display-p3"];
|
|
88
112
|
var BACKDROP_ALPHA_MODES = ["premultiplied", "unpremultiplied", "opaque"];
|
|
@@ -161,26 +185,18 @@ function encodeOutputBytes(linear, alpha) {
|
|
|
161
185
|
}
|
|
162
186
|
|
|
163
187
|
// ../renderer-webgpu/dist/material.js
|
|
164
|
-
var
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return "approximate";
|
|
174
|
-
case "none":
|
|
175
|
-
return "none";
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
function effectiveRefraction(a, b) {
|
|
179
|
-
return refractionRank(a) <= refractionRank(b) ? a : b;
|
|
180
|
-
}
|
|
188
|
+
var NOMINAL_MATERIAL_POLICY = {
|
|
189
|
+
glass: "material",
|
|
190
|
+
frost: "nominal",
|
|
191
|
+
refraction: "nominal",
|
|
192
|
+
occlusion: "nominal",
|
|
193
|
+
border: "nominal",
|
|
194
|
+
ambientTint: "nominal",
|
|
195
|
+
foreground: "adaptive"
|
|
196
|
+
};
|
|
181
197
|
var MATERIAL_VARIANTS = ["regular", "clear"];
|
|
182
198
|
var SRGB_WHITE_TINT = [1, 1, 1];
|
|
183
|
-
var INCREASED_OCCLUSION_LIFT = 0.
|
|
199
|
+
var INCREASED_OCCLUSION_LIFT = 0.75;
|
|
184
200
|
function occlusionAlphaUnderPolicy(nominal, occlusion, lift = INCREASED_OCCLUSION_LIFT) {
|
|
185
201
|
switch (occlusion) {
|
|
186
202
|
case "nominal":
|
|
@@ -197,24 +213,48 @@ var DEFAULT_MATERIAL_PROFILE = {
|
|
|
197
213
|
// `platform-web`'s CSS-tier blur on the same number — and makes core's 24 px
|
|
198
214
|
// `samplingPadding` advisory exactly the 3σ S1 measured.
|
|
199
215
|
regular: {
|
|
200
|
-
|
|
216
|
+
/*
|
|
217
|
+
* REFITTED (recalibration cascade, 2026-08-31) against the ACTIVE-pose bed.
|
|
218
|
+
* C9a's 0.62 and σ = 8 were fitted against the inactive material and are
|
|
219
|
+
* kept in git history, not here.
|
|
220
|
+
*
|
|
221
|
+
* `blurSigma` 3 is the first value this constant has ever had that the
|
|
222
|
+
* fixtures could identify. §6.1 called it unidentifiable because the
|
|
223
|
+
* inactive reference passed almost no backdrop structure; the active one
|
|
224
|
+
* passes a great deal (interior standard deviation 0.1358 over the
|
|
225
|
+
* checkerboard against vitrea's 0.0176 at σ = 8), so the objective's spread
|
|
226
|
+
* term now bites — it triples from σ = 3 to σ = 6 at a fixed alpha.
|
|
227
|
+
*
|
|
228
|
+
* `tintAlpha` 0.46 is the minimiser over the eight tone-inert untinted rest
|
|
229
|
+
* calibration cells, a genuine interior optimum (0.38 → 0.16945, 0.46 →
|
|
230
|
+
* 0.15704, 0.54 → 0.17248) with ΔE and SSIM agreeing. It remains true that
|
|
231
|
+
* no single value is right for every scene: a lerp toward one tint colour
|
|
232
|
+
* implies an alpha of 0.24 over the checkerboard and 0.47 over the photo at
|
|
233
|
+
* the same 44 px span, which is the lerp-versus-multiply question C9a
|
|
234
|
+
* recorded and nobody has acted on. Claims §5.13.
|
|
235
|
+
*/
|
|
236
|
+
blurSigma: 3,
|
|
201
237
|
tint: srgbToLinear(SRGB_WHITE_TINT),
|
|
202
|
-
|
|
203
|
-
// 0.28 made vitrea's regular material roughly half as opaque as the
|
|
204
|
-
// reference: regressing interior level against backdrop level across the
|
|
205
|
-
// calibration scenes puts Apple's transmission at 0.26 of the backdrop
|
|
206
|
-
// where vitrea's was 0.70. 0.62 is the minimiser of the declared tuning
|
|
207
|
-
// objective; see docs/doperpowers/specs/c9a-fidelity-claims.md, and note
|
|
208
|
-
// that no single value can be right for every size — Apple's opacity falls
|
|
209
|
-
// with surface span (0.88 at 32 px to 0.56 at 96 px) and this renderer has
|
|
210
|
-
// no size term on the tint.
|
|
211
|
-
tintAlpha: 0.62,
|
|
238
|
+
tintAlpha: 0.46,
|
|
212
239
|
rimWidth: 1.5,
|
|
213
240
|
rimAlpha: 0.18,
|
|
214
241
|
specularPower: 6,
|
|
215
242
|
specularGain: 0.55,
|
|
216
243
|
shadowDepth: 0.35,
|
|
217
|
-
|
|
244
|
+
/*
|
|
245
|
+
* REFITTED 0.55 → 0.05 (2026-08-31), and it is the largest single
|
|
246
|
+
* improvement in the cascade: the objective falls 0.14615 → 0.09333 with
|
|
247
|
+
* ΔE and SSIM both improving.
|
|
248
|
+
*
|
|
249
|
+
* The active reference's contour is BRIGHTER than its own body — a rim peak
|
|
250
|
+
* of 0.025…0.129 above baseline, always at one pixel deep. vitrea's read
|
|
251
|
+
* 0.0000 with its peak 8-12 px in, meaning it had no edge feature at all:
|
|
252
|
+
* the inner shadow at 0.55 was darkening the contour faster than the rim lit
|
|
253
|
+
* it. The rim constants themselves did not move; the thing suppressing them
|
|
254
|
+
* did. On the inactive bed the reference's rim was 0.0000…0.0041 and this
|
|
255
|
+
* was invisible, which is what §6.2 recorded as "below quantisation".
|
|
256
|
+
*/
|
|
257
|
+
shadowAlpha: 0.05,
|
|
218
258
|
highlight: srgbToLinear(SRGB_WHITE_TINT)
|
|
219
259
|
},
|
|
220
260
|
// Persistently more transparent, so it frosts less and tints less.
|
|
@@ -260,20 +300,277 @@ var DEFAULT_MATERIAL_PROFILE = {
|
|
|
260
300
|
adaptiveTintLight: srgbToLinear(SRGB_WHITE_TINT),
|
|
261
301
|
adaptiveLuminanceLow: 0.12,
|
|
262
302
|
adaptiveLuminanceHigh: 0.42,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
303
|
+
// Authored in `@vitrea/policy` alongside the ladder it is keyed by, because the
|
|
304
|
+
// CSS tier needs the same table and used to hold two more copies of it
|
|
305
|
+
// (Decision Log #23(d)). The profile is still what the shaders read and still
|
|
306
|
+
// what `withMaterialOverrides` replaces — only the *default* literal moved, and
|
|
307
|
+
// it moved unchanged.
|
|
308
|
+
refractionScale: DEFAULT_REFRACTION_SCALE,
|
|
309
|
+
/*
|
|
310
|
+
* MEASURED (W2), against the settled apple-macos-26.5 bed, and set from the
|
|
311
|
+
* REFERENCE's own size-dependence rather than from the objective's minimum.
|
|
312
|
+
*
|
|
313
|
+
* The band was 28…420 while it served the lens alone and nothing had measured
|
|
314
|
+
* it, which put the whole canonical range (32…160 px) inside the first 17% of
|
|
315
|
+
* the curve. What the settled reference actually does, over a fixed checkerboard
|
|
316
|
+
* backdrop: it passes 0.244 of the backdrop's contrast at a 32 px span, 0.230 at
|
|
317
|
+
* 44 px and 0.144 at 96 px. So 6% of the movement happens between 32 and 44 and
|
|
318
|
+
* the rest between 44 and 96, and it is finished by 96 — which is what these two
|
|
319
|
+
* numbers are. `sizeSpanMin` is also the smallest canonical component's span, so
|
|
320
|
+
* that component is the law's exact zero by construction.
|
|
321
|
+
*
|
|
322
|
+
* The tuning objective would rather have 64 (0.1568 against 0.1637 on the rest
|
|
323
|
+
* cells, a 4% win inside a grid that spans 1.07×). Declined, and the reason is
|
|
324
|
+
* recorded rather than the win taken: the gain comes entirely from the interior
|
|
325
|
+
* *level* term, whose residual is the backdrop tone-adaptation gap that wave
|
|
326
|
+
* child W7 is chartered to close — so a band fitted to 64 would be using the
|
|
327
|
+
* size law as a proxy for a mechanism vitrea does not have yet, against the
|
|
328
|
+
* reference's own measurement of where its size-dependence lives, and W7 would
|
|
329
|
+
* have to unpick it. See the claims doc's size-law section.
|
|
330
|
+
*/
|
|
331
|
+
sizeSpanMin: 32,
|
|
332
|
+
sizeSpanMax: 96,
|
|
270
333
|
lensSizeGainMax: 2.6,
|
|
334
|
+
/*
|
|
335
|
+
* MEASURED (W2), each with its own status — see the fields' notes.
|
|
336
|
+
*
|
|
337
|
+
* `sizeScatterGainMax` = 1: implemented on both tiers and inert, because the
|
|
338
|
+
* canonical fixtures cannot resolve it. The objective is flat to 1.00× over
|
|
339
|
+
* gains 1…6, and that is not the backdrop chain's depth talking: re-run with the
|
|
340
|
+
* chain deepened (MIN_LEVEL_EXTENT 8 → 4, which lifts the reachable σ on the
|
|
341
|
+
* 320×200 canvas from 1.2× to 2.4×) the grid is still flat to 1.00× and still
|
|
342
|
+
* best at 1. A number the fixtures cannot see is a number that will be met by
|
|
343
|
+
* accident, so this ships at the identity with the mechanism in place.
|
|
344
|
+
*
|
|
345
|
+
* `sizeOcclusionGain` = 0: fitted, and the fit is a boundary optimum with real
|
|
346
|
+
* leverage against it — the objective rises monotonically, 0.168 → 0.224 across
|
|
347
|
+
* 0…0.5, a 1.33× spread. The diagnosis is not a tuning failure: vitrea's
|
|
348
|
+
* interior sits 0.16–0.19 above the reference's at every span because the
|
|
349
|
+
* reference's level is set by backdrop tone adaptation toward roughly 0.63 while
|
|
350
|
+
* vitrea lerps toward a white tint, so making a large surface *more* opaque can
|
|
351
|
+
* only take it further from the reference. The facet is Apple's ("a larger size
|
|
352
|
+
* is more opaque"); the axis that would let it fit is W7's.
|
|
353
|
+
*
|
|
354
|
+
* `sizeShadowGainMax` = 1.4: fitted on the calibration REST cells, where the
|
|
355
|
+
* grid 1.0/1.2/1.4/1.6/1.8/2.2 reads 0.1577/0.1551/0.1533/0.1530/0.1529/0.1531.
|
|
356
|
+
* 1.4 through 2.2 are one flat region (0.25% apart); 1.4 is the point inside it
|
|
357
|
+
* that costs the checks least (ΔE 0.01282 against the baseline's 0.01290, SSIM
|
|
358
|
+
* 0.9671 against 0.9689) and it is the per-cell minimum on both well-conditioned
|
|
359
|
+
* span-96 rest cells. The pressed cells prefer 2.4 monotonically and are
|
|
360
|
+
* excluded from the fit on §6.3's grounds — their native side carries no press
|
|
361
|
+
* pose, so they compare two different states and cannot arbitrate a material
|
|
362
|
+
* constant.
|
|
363
|
+
*
|
|
364
|
+
* ## Re-fitted against the ACTIVE bed (2026-08-31)
|
|
365
|
+
*
|
|
366
|
+
* `sizeScatterGainMax` stays 1 and is now POSITIVELY identified rather than
|
|
367
|
+
* flat: with `blurSigma` refitted to 3 the objective rises 6-7% at gains 2 and
|
|
368
|
+
* 4, so the identity is the optimum instead of a tie.
|
|
369
|
+
*
|
|
370
|
+
* `sizeOcclusionGain` stays 0, a boundary result for the fourth time and for a
|
|
371
|
+
* fourth distinct reason. The reasoning above is retired: the per-cell residual
|
|
372
|
+
* now points the gain's way in every backdrop (web − reference falls with span
|
|
373
|
+
* — checkerboard +0.105/+0.112/+0.046 at 32/44/96 px, photo −0.004/−0.087,
|
|
374
|
+
* light-solid −0.028/+0.001), so the facet finally has the right sign. What it
|
|
375
|
+
* does not have is magnitude: over 0/0.05/0.10/0.15 × spans ending 96/128/160
|
|
376
|
+
* the whole grid is flat to 1.04×, and a 0.2% preference is not evidence. It
|
|
377
|
+
* ships at the identity because the bed cannot identify it, not because the
|
|
378
|
+
* mechanism is wrong.
|
|
379
|
+
*
|
|
380
|
+
* ## Re-fitted against the FROZEN bed (2026-09-01)
|
|
381
|
+
*
|
|
382
|
+
* `sizeOcclusionGain` 0 → **0.05**, and this is the first time the bed could
|
|
383
|
+
* see it. Every earlier fit was a boundary result because calibration held no
|
|
384
|
+
* span above `sizeSpanMax`, so the curve this gain rides was saturated at every
|
|
385
|
+
* cell that could vote. The frozen bed adds three span-128 `rrect-ml`
|
|
386
|
+
* calibration cells, and with them the grid stops being flat: 0.05 is the
|
|
387
|
+
* optimum at **both** backing scales independently — 1× 0.08288 against the
|
|
388
|
+
* identity's 0.08374, 2× 0.09338 against 0.09557 — and ΔE and SSIM move the
|
|
389
|
+
* same way at both.
|
|
390
|
+
*
|
|
391
|
+
* It is still a small number and it is reported as one: a 1.0% preference at
|
|
392
|
+
* 1×, against the 0.2% this doc rightly refused a round ago. What changed is
|
|
393
|
+
* not the margin's size but that it now reproduces on an independent profile
|
|
394
|
+
* instead of resting on one flat grid.
|
|
395
|
+
*
|
|
396
|
+
* `sizeSpanMax` stays 96, and that is now a measurement rather than an
|
|
397
|
+
* assumption. With a span-128 cell voting, the grid rises monotonically —
|
|
398
|
+
* 96/112/128/144/160/192 reading 0.0837/0.0858/0.0943/0.1060/0.1171/0.1300 —
|
|
399
|
+
* so the band's top is where it was, and the suspicion that the bed simply
|
|
400
|
+
* could not see above it is answered rather than inherited.
|
|
401
|
+
*
|
|
402
|
+
* `sizeShadowGainMax` 1.4 → 1. Its evidence dissolved rather than reversing:
|
|
403
|
+
* this is a gain on the INNER SHADOW, and the inner shadow was refitted from
|
|
404
|
+
* `shadowAlpha` 0.55 to 0.05, so there is almost nothing left for it to gain
|
|
405
|
+
* on. The grid 1.0/1.4/2.2 is flat to 0.2%. Carrying a fitted-looking 1.4 whose
|
|
406
|
+
* measurement no longer exists would be the worse of the two errors.
|
|
407
|
+
*/
|
|
408
|
+
sizeScatterGainMax: 1,
|
|
409
|
+
sizeOcclusionGain: 0.05,
|
|
410
|
+
sizeShadowGainMax: 1,
|
|
271
411
|
lensBodyLodPerPx: 0.16,
|
|
272
412
|
lensRimLodBias: 2.5,
|
|
273
413
|
reducedTransparencyFrost: 1.75,
|
|
274
414
|
increasedOcclusionLift: INCREASED_OCCLUSION_LIFT,
|
|
275
415
|
strongBorderRim: { rimWidth: 2, rimAlpha: 0.95 },
|
|
276
416
|
reducedTintAdaptation: 0.35,
|
|
417
|
+
// ADVISORY (W3). The span 0.02 … 0.65 covers most of the canonical backdrop
|
|
418
|
+
// range (0.003 … 0.891 linear), and the two ends are deliberately symmetric —
|
|
419
|
+
// 0.45 of the seed's brightness at the dark end, 0.45 of the way to white at
|
|
420
|
+
// the bright end — so the seed itself sits mid-range and the excursion reads
|
|
421
|
+
// as glass rather than as two different colours.
|
|
422
|
+
/*
|
|
423
|
+
* FITTED (2026-08-31, active bed) — and the fit is that THE CURVE IS THE
|
|
424
|
+
* IDENTITY. `floor` → 1 and `ceilMix` → 0 make `tintTone` return the seed
|
|
425
|
+
* unchanged at every backdrop, which leaves `low` and `high` describing
|
|
426
|
+
* nothing at all; they keep their provisional values so a bed that can
|
|
427
|
+
* identify them has somewhere to land, exactly as `adaptiveTint` keeps two
|
|
428
|
+
* equal ends.
|
|
429
|
+
*
|
|
430
|
+
* Monotone and uncontested over floor ∈ {0.45, 0.7, 0.85, 1} × ceilMix ∈
|
|
431
|
+
* {0, 0.2, 0.45}: objective 0.26523 → 0.12676, a 2.09× spread, with ΔE
|
|
432
|
+
* (0.01331 → 0.00831) and SSIM (0.9749 → 0.9788) agreeing. The reference
|
|
433
|
+
* says the same thing directly — on three of the five calibration backdrops
|
|
434
|
+
* its tinted interior is the declared seed EXACTLY in linear light, at a
|
|
435
|
+
* per-channel standard deviation of 0.000. Apple's tint is the author's
|
|
436
|
+
* colour, not a range of tones mapped to it; S219's wording described the
|
|
437
|
+
* inactive material's residue. Claims §5.13.
|
|
438
|
+
*/
|
|
439
|
+
tintToneFloor: 1,
|
|
440
|
+
tintToneCeilMix: 0,
|
|
441
|
+
tintToneLow: 0.02,
|
|
442
|
+
tintToneHigh: 0.65,
|
|
443
|
+
/*
|
|
444
|
+
* MEASURED (W7), against the settled apple-macos-26.5 bed, on the light and
|
|
445
|
+
* dark standard profiles jointly and at both scales.
|
|
446
|
+
*
|
|
447
|
+
* The observable these four are set from is the one quantity in this bed that
|
|
448
|
+
* isolates adaptation from everything else: the *separation* between the light
|
|
449
|
+
* and dark references over the same backdrop, on the same component. Under this
|
|
450
|
+
* mechanism that separation is `(1 − tintAlpha)(1 − a)(tintLight − tintDark)`,
|
|
451
|
+
* so the material's transmission, the backdrop's structure and each scheme's
|
|
452
|
+
* own tint all cancel and what is left is `a`. Normalised at the checkerboard,
|
|
453
|
+
* where nothing adapts, it reads:
|
|
454
|
+
*
|
|
455
|
+
* span 44: 0.000 at backdrop 0.500, 0.030 at 0.205, 1.000 at 0.0117, 1.000 at 0.0049
|
|
456
|
+
* span 96: 0.000 at backdrop 0.500, 0.028 at 0.216, 0.256 at 0.0117
|
|
457
|
+
*
|
|
458
|
+
* and the 2× bed reproduces every one of those to three decimals (0.2553
|
|
459
|
+
* against 0.2556 on the one that is not a boundary). Two facts follow. The
|
|
460
|
+
* adaptation is off across the whole ordinary range and turns on only below
|
|
461
|
+
* roughly a fifth of the backdrop scale — so it cannot disturb a cell that
|
|
462
|
+
* already passes. And it is size-gated hard: same backdrop, same material, 1.000
|
|
463
|
+
* against 0.256.
|
|
464
|
+
*
|
|
465
|
+
* The band's dark end is not identifiable from this bed and the fit says so:
|
|
466
|
+
* the reference's backdrops jump from 0.0117 to 0.205 with nothing in between,
|
|
467
|
+
* so `backdropToneLow` is bounded only by "at or below the darkest calibration
|
|
468
|
+
* backdrop". What the two dark backdrops DO pin is the curve's slope near zero —
|
|
469
|
+
* the 96 px surface reads 0.256 at 0.0117 and 0.356 at 0.0039, which is a
|
|
470
|
+
* measured intermediate rather than a step, and it is what fixes
|
|
471
|
+
* `backdropToneSizeBias` against `backdropToneHigh`. `impulse__rrect-md__rest`
|
|
472
|
+
* is a VALIDATION cell and was not fitted to: the calibration set predicts
|
|
473
|
+
* 0.34 there and the cell reads 0.356.
|
|
474
|
+
*/
|
|
475
|
+
backdropToneMax: 1,
|
|
476
|
+
backdropToneLow: 0.03,
|
|
477
|
+
backdropToneHigh: 0.14,
|
|
478
|
+
/*
|
|
479
|
+
* REFITTED 0.09 → 0.13 (2026-08-31, active bed). The law's SHAPE is untouched
|
|
480
|
+
* (Decision Log 13 stands; the two-axis rework is next wave) and `max`, `low`
|
|
481
|
+
* and `high` are unmoved — only the size gate widened.
|
|
482
|
+
*
|
|
483
|
+
* What it changes is one cell: a 96 pt surface over the darkest backdrop now
|
|
484
|
+
* barely adapts, where at 0.09 it adapted by a quarter. Two independent reads
|
|
485
|
+
* of the reference disagreed about that and the validation set broke the tie.
|
|
486
|
+
* The light-versus-dark separation estimator §5.8 fitted on says the 96 pt
|
|
487
|
+
* surface adapts by 0.30; the reference's own interior LEVEL says it does not
|
|
488
|
+
* (0.4844, against 0.466 for an unadapted surface at the refitted tint alpha
|
|
489
|
+
* and 0.3566 for an adapted one). Measured once on
|
|
490
|
+
* `impulse__rrect-md__rest`, which is validation and was fitted to by
|
|
491
|
+
* neither: 0.09 renders 0.2858 against a reference of 0.4358 at ΔE 0.02344;
|
|
492
|
+
* 0.13 renders 0.4594 at ΔE 0.00378, six times better. The separation
|
|
493
|
+
* estimator's algebra assumes the two colour schemes share one tint alpha,
|
|
494
|
+
* and this profile pair does not (0.46 against 0.97) — recorded in §5.13 as
|
|
495
|
+
* the reason it is no longer the primary evidence for this constant.
|
|
496
|
+
*/
|
|
497
|
+
backdropToneSizeBias: 0.13,
|
|
498
|
+
/*
|
|
499
|
+
* FITTED (recalibration cascade, 2026-08-31). W8's geometry SURVIVES the fit
|
|
500
|
+
* unchanged — `sigmaPx` 15.55, `offsetPx` 7.95, `spreadPx` 3.10 — and only the
|
|
501
|
+
* two amplitudes moved: `occlusion` 0.33 → 0.285 and
|
|
502
|
+
* `reducedTransparencyOcclusion` 0.566 → 0.70. The dark scheme's amplitude
|
|
503
|
+
* (0.09) lands as a profile patch, not as a branch here.
|
|
504
|
+
*
|
|
505
|
+
* The fit is `scripts/sweep.ts --objective shadow`, whose term is the mean over
|
|
506
|
+
* calibration cells of |Δ meanDeparture| — the light each side removes from the
|
|
507
|
+
* whole exterior, in linear light, which is the one commensurate quantity the
|
|
508
|
+
* facet has. The amplitude is a genuine interior optimum over 0.18…0.44 (a
|
|
509
|
+
* 2.23× spread) and flat to 1.04% across 0.255…0.315, which is exactly the
|
|
510
|
+
* scene-to-scene amplitude spread the reference itself shows. The geometry is
|
|
511
|
+
* NOT identifiable against this objective — σ ∈ {13.5, 15.55, 17.5} × offset ∈
|
|
512
|
+
* {6.5, 7.95, 9.4} spans 1.04× with 15.55 the argmin — because an integral over
|
|
513
|
+
* the exterior is insensitive to how the darkening is distributed within it.
|
|
514
|
+
* W8's own two-dimensional fit against the occlusion field measures the
|
|
515
|
+
* geometry far more powerfully (RMS 0.0021 over 142,550 pixels), and the
|
|
516
|
+
* instrument's independent shadow axis agrees, so the geometry stands on those
|
|
517
|
+
* two and this objective is not asked to re-decide it.
|
|
518
|
+
*
|
|
519
|
+
* `reducedTransparencyOcclusion` 0.70 is sharp where the amplitude is flat: a
|
|
520
|
+
* 3.83× spread over 0.45…0.95 with a clear minimum. It also reconciles two
|
|
521
|
+
* routes — 0.285 × 0.70 = 0.1995 against the reference's directly measured
|
|
522
|
+
* reduce-transparency amplitude of 0.203 at a 44 px span. W8's 0.566 was the
|
|
523
|
+
* ratio of the reference's two amplitudes; this is the ratio that makes
|
|
524
|
+
* vitrea's shadow match the reference's under the preference, and the two
|
|
525
|
+
* differ because the base amplitude is a compromise across scenes whose spread
|
|
526
|
+
* is much wider in the standard profile than under reduced transparency.
|
|
527
|
+
*
|
|
528
|
+
* Extracted from the active bed's native fixtures directly —
|
|
529
|
+
* `results/2026-08-31-active-bed-stage0.json` measured the gap, these numbers
|
|
530
|
+
* measure the facet — and left provisional deliberately: X1 gives the fit to
|
|
531
|
+
* the recalibration cascade, which owns the holdout discipline. This child owns
|
|
532
|
+
* the mechanism.
|
|
533
|
+
*
|
|
534
|
+
* Method: for each `rest` fixture, the occlusion field `1 − L_capture/L_background`
|
|
535
|
+
* in linear Rec.709 luminance, over every pixel outside the declared component
|
|
536
|
+
* geometry with a backdrop bright enough to carry a signal; fitted in two
|
|
537
|
+
* dimensions against `occlusion · Φ(−sd/σ)`, where `sd` is the signed distance
|
|
538
|
+
* to the component's own rounded silhouette translated down by `offsetPx` and
|
|
539
|
+
* outset by `spreadPx`.
|
|
540
|
+
*
|
|
541
|
+
* The fit and its residual, on `1x-light-standard` and `2x-light-standard`:
|
|
542
|
+
*
|
|
543
|
+
* backdrop span occlusion σ (1×/2×) offset (1×/2×) RMS
|
|
544
|
+
* photo 44 0.323 15.5/31.1 7.9/15.8 0.0021
|
|
545
|
+
* checkerboard 44 0.338 15.7/31.0 8.0/15.9 0.0016
|
|
546
|
+
* hc-text 44 0.339 15.7/31.1 8.0/15.9 0.0014
|
|
547
|
+
* photo 32 0.321 15.5/30.9 8.1/16.2 0.0018
|
|
548
|
+
* mid-dark-solid 44 0.310 — — 0.0079
|
|
549
|
+
*
|
|
550
|
+
* — against a peak occlusion of 0.24, so the model carries the facet to under
|
|
551
|
+
* 1% of its own amplitude, over up to 142,550 pixels per cell. The three
|
|
552
|
+
* lengths are the consensus across every well-conditioned cell; `occlusion` is
|
|
553
|
+
* the light-standard amplitude at a span the size law leaves alone.
|
|
554
|
+
*
|
|
555
|
+
* What the amplitude does NOT yet have is a mechanism for its scene-to-scene
|
|
556
|
+
* spread. Over the flat near-white `light-solid` backdrop the same fit reads
|
|
557
|
+
* 0.123 rather than 0.33, reproducibly and at both scales, while the other flat
|
|
558
|
+
* backdrop (`mid-dark-solid`, linear 0.0595) reads 0.310 — so it is not a
|
|
559
|
+
* function of the backdrop's luminance, its structure, or the material's
|
|
560
|
+
* interior level, and no compositing model in either colour space produces both.
|
|
561
|
+
* The dark-scheme profile is a separate amplitude entirely (0.046…0.061 at a
|
|
562
|
+
* 44 px span — the dark material's shadow is nearly invisible) and lands as a
|
|
563
|
+
* profile patch, not as a branch here. Both are stated in the claims doc as the
|
|
564
|
+
* open question the cascade's fit inherits.
|
|
565
|
+
*/
|
|
566
|
+
outerShadow: {
|
|
567
|
+
offsetPx: 7.95,
|
|
568
|
+
sigmaPx: 15.55,
|
|
569
|
+
spreadPx: 3.1,
|
|
570
|
+
occlusion: 0.285,
|
|
571
|
+
reducedTransparencyOcclusion: 0.7,
|
|
572
|
+
sizeGain: 0
|
|
573
|
+
},
|
|
277
574
|
lightDirection: [-0.3714, -0.9285],
|
|
278
575
|
sweepBandRadians: 0.55,
|
|
279
576
|
glowRadiusCss: 44,
|
|
@@ -286,11 +583,19 @@ var ADAPTIVE_TINT_DARK = DEFAULT_MATERIAL_PROFILE.adaptiveTintDark;
|
|
|
286
583
|
var ADAPTIVE_TINT_LIGHT = DEFAULT_MATERIAL_PROFILE.adaptiveTintLight;
|
|
287
584
|
var ADAPTIVE_LUMINANCE_LOW = DEFAULT_MATERIAL_PROFILE.adaptiveLuminanceLow;
|
|
288
585
|
var ADAPTIVE_LUMINANCE_HIGH = DEFAULT_MATERIAL_PROFILE.adaptiveLuminanceHigh;
|
|
289
|
-
var
|
|
290
|
-
var
|
|
586
|
+
var SIZE_SPAN_MIN = DEFAULT_MATERIAL_PROFILE.sizeSpanMin;
|
|
587
|
+
var SIZE_SPAN_MAX = DEFAULT_MATERIAL_PROFILE.sizeSpanMax;
|
|
291
588
|
var LENS_SIZE_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.lensSizeGainMax;
|
|
589
|
+
var SIZE_SCATTER_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.sizeScatterGainMax;
|
|
590
|
+
var SIZE_OCCLUSION_GAIN = DEFAULT_MATERIAL_PROFILE.sizeOcclusionGain;
|
|
591
|
+
var SIZE_SHADOW_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.sizeShadowGainMax;
|
|
292
592
|
var LENS_BODY_LOD_PER_PX = DEFAULT_MATERIAL_PROFILE.lensBodyLodPerPx;
|
|
293
593
|
var LENS_RIM_LOD_BIAS = DEFAULT_MATERIAL_PROFILE.lensRimLodBias;
|
|
594
|
+
var BACKDROP_TONE_MAX = DEFAULT_MATERIAL_PROFILE.backdropToneMax;
|
|
595
|
+
var BACKDROP_TONE_LOW = DEFAULT_MATERIAL_PROFILE.backdropToneLow;
|
|
596
|
+
var BACKDROP_TONE_HIGH = DEFAULT_MATERIAL_PROFILE.backdropToneHigh;
|
|
597
|
+
var BACKDROP_TONE_SIZE_BIAS = DEFAULT_MATERIAL_PROFILE.backdropToneSizeBias;
|
|
598
|
+
var OUTER_SHADOW = DEFAULT_MATERIAL_PROFILE.outerShadow;
|
|
294
599
|
function withMaterialOverrides(base, patch) {
|
|
295
600
|
const optics = {};
|
|
296
601
|
for (const variant of MATERIAL_VARIANTS) {
|
|
@@ -307,15 +612,27 @@ function withMaterialOverrides(base, patch) {
|
|
|
307
612
|
adaptiveLuminanceLow: patch.adaptiveLuminanceLow ?? base.adaptiveLuminanceLow,
|
|
308
613
|
adaptiveLuminanceHigh: patch.adaptiveLuminanceHigh ?? base.adaptiveLuminanceHigh,
|
|
309
614
|
refractionScale,
|
|
310
|
-
|
|
311
|
-
|
|
615
|
+
sizeSpanMin: patch.sizeSpanMin ?? base.sizeSpanMin,
|
|
616
|
+
sizeSpanMax: patch.sizeSpanMax ?? base.sizeSpanMax,
|
|
312
617
|
lensSizeGainMax: patch.lensSizeGainMax ?? base.lensSizeGainMax,
|
|
618
|
+
sizeScatterGainMax: patch.sizeScatterGainMax ?? base.sizeScatterGainMax,
|
|
619
|
+
sizeOcclusionGain: patch.sizeOcclusionGain ?? base.sizeOcclusionGain,
|
|
620
|
+
sizeShadowGainMax: patch.sizeShadowGainMax ?? base.sizeShadowGainMax,
|
|
313
621
|
lensBodyLodPerPx: patch.lensBodyLodPerPx ?? base.lensBodyLodPerPx,
|
|
314
622
|
lensRimLodBias: patch.lensRimLodBias ?? base.lensRimLodBias,
|
|
315
623
|
reducedTransparencyFrost: patch.reducedTransparencyFrost ?? base.reducedTransparencyFrost,
|
|
316
624
|
increasedOcclusionLift: patch.increasedOcclusionLift ?? base.increasedOcclusionLift,
|
|
317
625
|
strongBorderRim: { ...base.strongBorderRim, ...patch.strongBorderRim },
|
|
318
626
|
reducedTintAdaptation: patch.reducedTintAdaptation ?? base.reducedTintAdaptation,
|
|
627
|
+
tintToneFloor: patch.tintToneFloor ?? base.tintToneFloor,
|
|
628
|
+
tintToneCeilMix: patch.tintToneCeilMix ?? base.tintToneCeilMix,
|
|
629
|
+
tintToneLow: patch.tintToneLow ?? base.tintToneLow,
|
|
630
|
+
tintToneHigh: patch.tintToneHigh ?? base.tintToneHigh,
|
|
631
|
+
backdropToneMax: patch.backdropToneMax ?? base.backdropToneMax,
|
|
632
|
+
backdropToneLow: patch.backdropToneLow ?? base.backdropToneLow,
|
|
633
|
+
backdropToneHigh: patch.backdropToneHigh ?? base.backdropToneHigh,
|
|
634
|
+
backdropToneSizeBias: patch.backdropToneSizeBias ?? base.backdropToneSizeBias,
|
|
635
|
+
outerShadow: { ...base.outerShadow, ...patch.outerShadow },
|
|
319
636
|
lightDirection: patch.lightDirection ?? base.lightDirection,
|
|
320
637
|
sweepBandRadians: patch.sweepBandRadians ?? base.sweepBandRadians,
|
|
321
638
|
glowRadiusCss: patch.glowRadiusCss ?? base.glowRadiusCss,
|
|
@@ -350,14 +667,147 @@ function adaptationStrength(policy, analysisExact, profile = DEFAULT_MATERIAL_PR
|
|
|
350
667
|
return 0;
|
|
351
668
|
}
|
|
352
669
|
}
|
|
670
|
+
function tintToneAdaptation(policy, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
671
|
+
switch (policy.ambientTint) {
|
|
672
|
+
case "nominal":
|
|
673
|
+
return 1;
|
|
674
|
+
case "reduced":
|
|
675
|
+
return profile.reducedTintAdaptation;
|
|
676
|
+
case "none":
|
|
677
|
+
return 0;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function tintTone(seed, backdropLuminance, toneAdaptation, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
681
|
+
const t = smoothstep(profile.tintToneLow, profile.tintToneHigh, backdropLuminance);
|
|
682
|
+
const k = Math.min(1, Math.max(0, toneAdaptation));
|
|
683
|
+
const channel = (index) => {
|
|
684
|
+
const s = seed[index];
|
|
685
|
+
const low = s * profile.tintToneFloor;
|
|
686
|
+
const high = s + (1 - s) * profile.tintToneCeilMix;
|
|
687
|
+
return s + (low + (high - low) * t - s) * k;
|
|
688
|
+
};
|
|
689
|
+
return [channel(0), channel(1), channel(2)];
|
|
690
|
+
}
|
|
691
|
+
function tintedTintColour(neutral, tint, backdropLuminance, toneAdaptation, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
692
|
+
if (tint === void 0)
|
|
693
|
+
return neutral;
|
|
694
|
+
const tone = tintTone(tint.color, backdropLuminance, toneAdaptation, profile);
|
|
695
|
+
const k = Math.min(1, Math.max(0, tint.strength));
|
|
696
|
+
return [
|
|
697
|
+
neutral[0] + (tone[0] - neutral[0]) * k,
|
|
698
|
+
neutral[1] + (tone[1] - neutral[1]) * k,
|
|
699
|
+
neutral[2] + (tone[2] - neutral[2]) * k
|
|
700
|
+
];
|
|
701
|
+
}
|
|
702
|
+
function backdropToneAdaptation(backdropLuminance, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
703
|
+
const x = backdropLuminance + profile.backdropToneSizeBias * Math.min(1, Math.max(0, thickness));
|
|
704
|
+
const span = Math.max(profile.backdropToneHigh - profile.backdropToneLow, 1e-6);
|
|
705
|
+
const t = Math.min(1, Math.max(0, (x - profile.backdropToneLow) / span));
|
|
706
|
+
return Math.min(1, Math.max(0, profile.backdropToneMax)) * (1 - t * t * (3 - 2 * t));
|
|
707
|
+
}
|
|
708
|
+
function backdropToneUnderPolicy(policy, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
709
|
+
return tintToneAdaptation(policy, profile) * profile.refractionScale[accessibilityRefractionCap(policy)];
|
|
710
|
+
}
|
|
711
|
+
function backdropToneSizeBiasUnderPolicy(policy, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
712
|
+
const cap = profile.refractionScale[accessibilityRefractionCap(policy)];
|
|
713
|
+
return cap <= 0 ? 0 : profile.backdropToneSizeBias / cap;
|
|
714
|
+
}
|
|
715
|
+
function adaptedTintColour(neutral, backdrop, adaptation, tintAlpha) {
|
|
716
|
+
const k = Math.min(1, Math.max(0, adaptation));
|
|
717
|
+
if (k === 0)
|
|
718
|
+
return neutral;
|
|
719
|
+
const alpha = adaptedTintAlpha(tintAlpha, k);
|
|
720
|
+
if (alpha <= 0)
|
|
721
|
+
return neutral;
|
|
722
|
+
const wNeutral = (1 - k) * tintAlpha;
|
|
723
|
+
return [
|
|
724
|
+
(neutral[0] * wNeutral + backdrop[0] * k) / alpha,
|
|
725
|
+
(neutral[1] * wNeutral + backdrop[1] * k) / alpha,
|
|
726
|
+
(neutral[2] * wNeutral + backdrop[2] * k) / alpha
|
|
727
|
+
];
|
|
728
|
+
}
|
|
729
|
+
function adaptedTintAlpha(tintAlpha, adaptation) {
|
|
730
|
+
const k = Math.min(1, Math.max(0, adaptation));
|
|
731
|
+
return tintAlpha + k * (1 - tintAlpha);
|
|
732
|
+
}
|
|
353
733
|
var smoothstep = (edge0, edge1, x) => {
|
|
354
734
|
if (edge1 <= edge0)
|
|
355
735
|
return x < edge0 ? 0 : 1;
|
|
356
736
|
const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));
|
|
357
737
|
return t * t * (3 - 2 * t);
|
|
358
738
|
};
|
|
739
|
+
function sizeThickness(spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
740
|
+
return smoothstep(profile.sizeSpanMin, profile.sizeSpanMax, spanPx);
|
|
741
|
+
}
|
|
742
|
+
function sizeThicknessUnderPolicy(spanPx, policy, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
743
|
+
return sizeThickness(spanPx, profile) * profile.refractionScale[accessibilityRefractionCap(policy)];
|
|
744
|
+
}
|
|
359
745
|
function lensSizeGain(spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
360
|
-
return 1 + (profile.lensSizeGainMax - 1) *
|
|
746
|
+
return 1 + (profile.lensSizeGainMax - 1) * sizeThickness(spanPx, profile);
|
|
747
|
+
}
|
|
748
|
+
function lensSizeGainFromThickness(thickness, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
749
|
+
return 1 + (profile.lensSizeGainMax - 1) * thickness;
|
|
750
|
+
}
|
|
751
|
+
function sizeScatterSigma(sigmaPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
752
|
+
return sizeScatterSigmaAt(sigmaPx, sizeThickness(spanPx, profile), profile);
|
|
753
|
+
}
|
|
754
|
+
function sizeScatterSigmaAt(sigmaPx, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
755
|
+
return sigmaPx * (1 + (profile.sizeScatterGainMax - 1) * thickness);
|
|
756
|
+
}
|
|
757
|
+
function sizeOcclusionAlpha(alpha, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
758
|
+
return sizeOcclusionAlphaAt(alpha, sizeThickness(spanPx, profile), profile);
|
|
759
|
+
}
|
|
760
|
+
function sizeOcclusionAlphaAt(alpha, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
761
|
+
return Math.min(1, alpha + profile.sizeOcclusionGain * thickness * (1 - alpha));
|
|
762
|
+
}
|
|
763
|
+
function sizeShadowDepth(shadowDepth, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
764
|
+
return sizeShadowDepthAt(shadowDepth, sizeThickness(spanPx, profile), profile);
|
|
765
|
+
}
|
|
766
|
+
function sizeShadowDepthAt(shadowDepth, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
767
|
+
return shadowDepth * (1 + (profile.sizeShadowGainMax - 1) * thickness);
|
|
768
|
+
}
|
|
769
|
+
function sizeOuterShadowOcclusion(occlusion, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
770
|
+
return sizeOuterShadowOcclusionAt(occlusion, sizeThickness(spanPx, profile), profile);
|
|
771
|
+
}
|
|
772
|
+
function sizeOuterShadowOcclusionAt(occlusion, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
773
|
+
return Math.min(1, occlusion + profile.outerShadow.sizeGain * thickness * (1 - occlusion));
|
|
774
|
+
}
|
|
775
|
+
function outerShadowUnderPolicy(policy, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
776
|
+
const shadow = profile.outerShadow;
|
|
777
|
+
if (policy.glass === "none" || policy.frost === "none")
|
|
778
|
+
return { ...shadow, occlusion: 0 };
|
|
779
|
+
if (policy.frost === "increased") {
|
|
780
|
+
return { ...shadow, occlusion: shadow.occlusion * shadow.reducedTransparencyOcclusion };
|
|
781
|
+
}
|
|
782
|
+
return shadow;
|
|
783
|
+
}
|
|
784
|
+
var SRGB_ENCODING_EXPONENT = 2.4;
|
|
785
|
+
function outerShadowFalloff(signedDistancePx, sigmaPx) {
|
|
786
|
+
const x = -signedDistancePx / Math.max(sigmaPx, 1e-4);
|
|
787
|
+
return 0.5 * (1 + Math.tanh(0.7978845608028654 * (x + 0.044715 * x * x * x)));
|
|
788
|
+
}
|
|
789
|
+
function outerShadowAlpha(occlusion) {
|
|
790
|
+
const occ = Math.min(1, Math.max(0, occlusion));
|
|
791
|
+
return 1 - Math.pow(1 - occ, 1 / SRGB_ENCODING_EXPONENT);
|
|
792
|
+
}
|
|
793
|
+
function outerShadowReachPx(shadow) {
|
|
794
|
+
const alpha = outerShadowAlpha(shadow.occlusion);
|
|
795
|
+
if (!(alpha > 0))
|
|
796
|
+
return 0;
|
|
797
|
+
const cutoff = 1 / 255 / alpha;
|
|
798
|
+
if (cutoff >= 1)
|
|
799
|
+
return 0;
|
|
800
|
+
const sigma = Math.max(shadow.sigmaPx, 1e-4);
|
|
801
|
+
let lo = -(8 * sigma + Math.abs(shadow.offsetPx) + Math.abs(shadow.spreadPx));
|
|
802
|
+
let hi = 8 * sigma;
|
|
803
|
+
for (let i = 0; i < 48; i += 1) {
|
|
804
|
+
const mid = (lo + hi) / 2;
|
|
805
|
+
if (outerShadowFalloff(mid, sigma) > cutoff)
|
|
806
|
+
lo = mid;
|
|
807
|
+
else
|
|
808
|
+
hi = mid;
|
|
809
|
+
}
|
|
810
|
+
return Math.max(0, hi + Math.abs(shadow.offsetPx) + shadow.spreadPx);
|
|
361
811
|
}
|
|
362
812
|
function lensDepthPx(thicknessPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
363
813
|
const gain = lensSizeGain(spanPx, profile);
|
|
@@ -1252,7 +1702,7 @@ var IDLE_CHANNELS = {
|
|
|
1252
1702
|
};
|
|
1253
1703
|
|
|
1254
1704
|
// ../renderer-webgpu/dist/instances.js
|
|
1255
|
-
var INSTANCE_FLOATS =
|
|
1705
|
+
var INSTANCE_FLOATS = 18;
|
|
1256
1706
|
var INSTANCE_BYTES = INSTANCE_FLOATS * 4;
|
|
1257
1707
|
var channelsOf = (input) => ({
|
|
1258
1708
|
...IDLE_CHANNELS,
|
|
@@ -1264,7 +1714,7 @@ function compressedChannels(shape, press) {
|
|
|
1264
1714
|
const scale = 1 - Math.min(1, Math.max(0, press)) * DEFAULT_MOTION_PROFILE.pressCompressionScale;
|
|
1265
1715
|
return { ...shape, size: [shape.size[0] * scale, shape.size[1] * scale] };
|
|
1266
1716
|
}
|
|
1267
|
-
function resolveSurfaces(group, family, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
1717
|
+
function resolveSurfaces(group, family, profile = DEFAULT_MATERIAL_PROFILE, policy = NOMINAL_MATERIAL_POLICY) {
|
|
1268
1718
|
const byId = /* @__PURE__ */ new Map();
|
|
1269
1719
|
for (const surface of group.surfaces) {
|
|
1270
1720
|
if (byId.has(surface.nodeId)) {
|
|
@@ -1308,6 +1758,7 @@ function resolveSurfaces(group, family, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
|
1308
1758
|
const fieldSource = surface.concentricOf === void 0 ? shape : resolveShapeOf(byId.get(surface.concentricOf.nodeId));
|
|
1309
1759
|
const inset = surface.concentricOf?.inset ?? 0;
|
|
1310
1760
|
const spanPx = Math.min(shape.channels.size[0], shape.channels.size[1]);
|
|
1761
|
+
const thickness = sizeThicknessUnderPolicy(spanPx, policy, profile);
|
|
1311
1762
|
return {
|
|
1312
1763
|
nodeId: surface.nodeId,
|
|
1313
1764
|
shape,
|
|
@@ -1316,11 +1767,13 @@ function resolveSurfaces(group, family, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
|
1316
1767
|
centre: [fieldSource.channels.center[0], fieldSource.channels.center[1]],
|
|
1317
1768
|
channels,
|
|
1318
1769
|
spanPx,
|
|
1319
|
-
lensDepthPx:
|
|
1770
|
+
lensDepthPx: Math.min(Math.max(shape.channels.thickness, 0) * lensSizeGainFromThickness(thickness, profile), spanPx * 0.5),
|
|
1771
|
+
sizeThickness: thickness,
|
|
1772
|
+
tintStrength: Math.min(1, Math.max(0, surface.tint?.strength ?? 0))
|
|
1320
1773
|
};
|
|
1321
1774
|
});
|
|
1322
1775
|
}
|
|
1323
|
-
function groupFieldRect(surfaces, union = DEFAULT_GROUP_UNION, rimWidthPx = 2) {
|
|
1776
|
+
function groupFieldRect(surfaces, union = DEFAULT_GROUP_UNION, rimWidthPx = 2, outerShadowReachPx2 = 0) {
|
|
1324
1777
|
if (surfaces.length === 0)
|
|
1325
1778
|
return { x: 0, y: 0, width: 0, height: 0 };
|
|
1326
1779
|
let minX = Number.POSITIVE_INFINITY;
|
|
@@ -1335,7 +1788,7 @@ function groupFieldRect(surfaces, union = DEFAULT_GROUP_UNION, rimWidthPx = 2) {
|
|
|
1335
1788
|
maxX = Math.max(maxX, cx + w / 2);
|
|
1336
1789
|
maxY = Math.max(maxY, cy + h / 2);
|
|
1337
1790
|
}
|
|
1338
|
-
const pad = rimWidthPx + union.maxBulge + 1;
|
|
1791
|
+
const pad = Math.max(rimWidthPx + union.maxBulge + 1, Math.max(outerShadowReachPx2, 0));
|
|
1339
1792
|
return {
|
|
1340
1793
|
x: minX - pad,
|
|
1341
1794
|
y: minY - pad,
|
|
@@ -1380,7 +1833,9 @@ function packInstances(surfaces, origin, into) {
|
|
|
1380
1833
|
data[o + 12] = s.channels.press;
|
|
1381
1834
|
data[o + 13] = s.channels.glow;
|
|
1382
1835
|
data[o + 14] = s.lensDepthPx * Math.min(1, Math.max(0, s.channels.lensStrength));
|
|
1383
|
-
data[o + 15] =
|
|
1836
|
+
data[o + 15] = s.tintStrength;
|
|
1837
|
+
data[o + 16] = s.sizeThickness;
|
|
1838
|
+
data[o + 17] = 0;
|
|
1384
1839
|
}
|
|
1385
1840
|
return { data, count: surfaces.length };
|
|
1386
1841
|
}
|
|
@@ -1656,7 +2111,9 @@ var WGSL_INSTANCE_STRUCT = `struct Instance {
|
|
|
1656
2111
|
press : f32, // 48 interaction channel value, 0..1
|
|
1657
2112
|
glow : f32, // 52 interaction channel value, 0..1
|
|
1658
2113
|
lensDepth : f32, // 56 CPU-resolved lens depth in CSS px (material.ts), already scaled by the lensStrength channel
|
|
1659
|
-
|
|
2114
|
+
tintK : f32, // 60 author tint strength, 0..1 \u2014 the seed itself is a group uniform
|
|
2115
|
+
sizeK : f32, // 64 the size law's thickness factor, 0..1 \u2014 material.ts's sizeThickness(span)
|
|
2116
|
+
_pad : f32, // 68 stride padding: vec2f aligns the struct to 8, so 72 is the next legal size
|
|
1660
2117
|
};`;
|
|
1661
2118
|
var WGSL_FIELD_SAMPLE = `struct FieldSample {
|
|
1662
2119
|
d : f32,
|
|
@@ -1821,7 +2278,29 @@ var WGSL_FIELD_PASS = `struct FieldUniforms {
|
|
|
1821
2278
|
// field but its members are not one size, and carrying them per pixel is what
|
|
1822
2279
|
// lets a 40 px button and a 320 px platter share a field pass and still lens by
|
|
1823
2280
|
// their own depth \u2014 parent acceptance #2 inside a GlassEffectContainer.
|
|
1824
|
-
// aux = (lensDepthPx, glow,
|
|
2281
|
+
// aux = (lensDepthPx, glow, sizeK, tintStrength)
|
|
2282
|
+
//
|
|
2283
|
+
// The third and fourth slots both changed hands, and neither of the values they
|
|
2284
|
+
// used to carry was ever read by a fragment stage. 'thick' left because
|
|
2285
|
+
// 'lensDepth' is the thickness times the size gain, so the raw number told the
|
|
2286
|
+
// shader nothing the depth had not already said; 'press' left because press
|
|
2287
|
+
// compression is resolved on the CPU, as a transform on the surface's own size
|
|
2288
|
+
// (instances.ts). Both are still packed into the instance buffer, unchanged, for
|
|
2289
|
+
// a future consumer that wants them per pixel; they simply no longer ride here.
|
|
2290
|
+
//
|
|
2291
|
+
// What replaced them are the two quantities that genuinely have to be per pixel,
|
|
2292
|
+
// because a group is one field pass and one optics pass while its members are
|
|
2293
|
+
// not one thing:
|
|
2294
|
+
//
|
|
2295
|
+
// 'sizeK' \u2014 the size law's thickness factor (W2). A 40 px button and a
|
|
2296
|
+
// 320 px platter in one container lens, scatter, occlude and
|
|
2297
|
+
// shadow by their own size.
|
|
2298
|
+
// 'tintStrength' \u2014 the author tint's strength (W3). One emphasised control
|
|
2299
|
+
// among plain ones is the composition Apple's guidance
|
|
2300
|
+
// describes; the seed itself is a group uniform.
|
|
2301
|
+
//
|
|
2302
|
+
// They compose rather than compete: the size law moves what the material does to
|
|
2303
|
+
// the backdrop, the tint moves what colour it does it in.
|
|
1825
2304
|
|
|
1826
2305
|
@group(0) @binding(0) var<uniform> fu : FieldUniforms;
|
|
1827
2306
|
@group(0) @binding(1) var<storage, read> instances : array<Instance>;
|
|
@@ -1842,7 +2321,7 @@ fn eval_instance(i : u32, p : vec2f) -> Member {
|
|
|
1842
2321
|
var m : Member;
|
|
1843
2322
|
m.d = f.d + s.inset;
|
|
1844
2323
|
m.g = f.g;
|
|
1845
|
-
m.aux = vec4f(s.lensDepth, s.glow, s.
|
|
2324
|
+
m.aux = vec4f(s.lensDepth, s.glow, s.sizeK, s.tintK);
|
|
1846
2325
|
return m;
|
|
1847
2326
|
}
|
|
1848
2327
|
|
|
@@ -1925,6 +2404,21 @@ var WGSL_HIGHLIGHT_PASS = `struct HighlightUniforms {
|
|
|
1925
2404
|
colour : vec4f,
|
|
1926
2405
|
/// fieldSize.xy, fieldUpsampled (z), unused (w)
|
|
1927
2406
|
flags : vec4f,
|
|
2407
|
+
/// backdrop tone adaptation (W7), exactly as the optics pass takes it:
|
|
2408
|
+
/// backdropToneLow, backdropToneHigh, the size bias already divided by the
|
|
2409
|
+
/// accessibility refraction cap, and the resolved strength \u2014 zero where there
|
|
2410
|
+
/// is no measured backdrop tone, which stands the fade down
|
|
2411
|
+
toneAdapt : vec4f,
|
|
2412
|
+
/// the backdrop source's own average luminance (x); yzw unused
|
|
2413
|
+
toneLevel : vec4f,
|
|
2414
|
+
/// this pass's uv into the FIELD texture's uv: scale (xy), offset (zw).
|
|
2415
|
+
///
|
|
2416
|
+
/// The identity for a pass whose scissor is the field's own rect, and not the
|
|
2417
|
+
/// identity since W8: the outer shadow made the field rect much larger than
|
|
2418
|
+
/// anything this pass draws in, so the highlight is scoped back to the rect it
|
|
2419
|
+
/// had before and reads the field through this remap instead. Derived from the
|
|
2420
|
+
/// two rects by highlightPass in passes.ts.
|
|
2421
|
+
fieldFit : vec4f,
|
|
1928
2422
|
};
|
|
1929
2423
|
|
|
1930
2424
|
@group(0) @binding(0) var<uniform> hu : HighlightUniforms;
|
|
@@ -1946,13 +2440,14 @@ fn fs_highlight(in : FullscreenOut) -> @location(0) vec4f {
|
|
|
1946
2440
|
// Exact load nominally; filtered when the governor's resolution knob had the
|
|
1947
2441
|
// field rasterised below the group's rect. The sweep rides the rim, which is
|
|
1948
2442
|
// the one place a nearest read of a coarse field would show its grid.
|
|
2443
|
+
let fieldUv = in.uv * hu.fieldFit.xy + hu.fieldFit.zw;
|
|
1949
2444
|
var field : vec4f;
|
|
1950
2445
|
var aux : vec4f;
|
|
1951
2446
|
if (hu.flags.z > 0.5) {
|
|
1952
|
-
field = textureSampleLevel(fieldTexture, fieldSampler,
|
|
1953
|
-
aux = textureSampleLevel(auxTexture, fieldSampler,
|
|
2447
|
+
field = textureSampleLevel(fieldTexture, fieldSampler, fieldUv, 0.0);
|
|
2448
|
+
aux = textureSampleLevel(auxTexture, fieldSampler, fieldUv, 0.0);
|
|
1954
2449
|
} else {
|
|
1955
|
-
let texel = vec2i(
|
|
2450
|
+
let texel = vec2i(fieldUv * hu.flags.xy);
|
|
1956
2451
|
field = textureLoad(fieldTexture, texel, 0);
|
|
1957
2452
|
aux = textureLoad(auxTexture, texel, 0);
|
|
1958
2453
|
}
|
|
@@ -1981,7 +2476,32 @@ fn fs_highlight(in : FullscreenOut) -> @location(0) vec4f {
|
|
|
1981
2476
|
// whose members glow independently does not need a pass each.
|
|
1982
2477
|
let press = radial * radial * hu.glow.w * aux.y;
|
|
1983
2478
|
|
|
1984
|
-
|
|
2479
|
+
/*
|
|
2480
|
+
* Backdrop tone adaptation (W7) fades this pass with the rest of the surface's
|
|
2481
|
+
* own appearance. A highlight is the material catching light, and a material
|
|
2482
|
+
* that has taken its backdrop's tone is not there to catch any \u2014 the settled
|
|
2483
|
+
* reference's capsule over a near-black backdrop is byte-identical to that
|
|
2484
|
+
* background, sweep included.
|
|
2485
|
+
*
|
|
2486
|
+
* The curve is the optics pass's, evaluated the same way off the same uniforms
|
|
2487
|
+
* and the same per-pixel 'sizeK', because two statements of one curve is how
|
|
2488
|
+
* they drift. This pass would otherwise have gone on drawing a bright crescent
|
|
2489
|
+
* around a surface the optics pass had made invisible \u2014 measured at 60 pixels
|
|
2490
|
+
* past 2/255, peaking at 97/255, on exactly that cell.
|
|
2491
|
+
*/
|
|
2492
|
+
let sizeK = clamp(aux.z, 0.0, 1.0);
|
|
2493
|
+
var toneAdapt = 0.0;
|
|
2494
|
+
if (hu.toneAdapt.w > 0.0) {
|
|
2495
|
+
let toneX = hu.toneLevel.x + hu.toneAdapt.z * sizeK;
|
|
2496
|
+
let toneT = clamp(
|
|
2497
|
+
(toneX - hu.toneAdapt.x) / max(hu.toneAdapt.y - hu.toneAdapt.x, 1e-6),
|
|
2498
|
+
0.0,
|
|
2499
|
+
1.0,
|
|
2500
|
+
);
|
|
2501
|
+
toneAdapt = clamp(hu.toneAdapt.w, 0.0, 1.0) * (1.0 - toneT * toneT * (3.0 - 2.0 * toneT));
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
let intensity = clamp(sweep + press, 0.0, 1.0) * coverage * (1.0 - toneAdapt);
|
|
1985
2505
|
if (intensity <= 0.0) {
|
|
1986
2506
|
return vec4f(0.0);
|
|
1987
2507
|
}
|
|
@@ -2000,12 +2520,40 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
|
|
|
2000
2520
|
tint : vec4f,
|
|
2001
2521
|
/// adapted tint colour, linear light (xyz), adaptation strength (w)
|
|
2002
2522
|
adapt : vec4f,
|
|
2523
|
+
/// author tint seed, linear light (xyz), tone adaptation under the contrast regime (w)
|
|
2524
|
+
seed : vec4f,
|
|
2525
|
+
/// tintToneFloor, tintToneCeilMix, tintToneLow, tintToneHigh
|
|
2526
|
+
tone : vec4f,
|
|
2003
2527
|
/// rimWidthPx, rimAlpha, specularPower, specularGain
|
|
2004
2528
|
rim : vec4f,
|
|
2005
2529
|
/// light direction, unit (xy), shadowDepth (z), shadowAlpha (w)
|
|
2006
2530
|
light : vec4f,
|
|
2007
2531
|
/// hasBackdrop, fieldSize.xy, fieldUpsampled
|
|
2008
2532
|
flags : vec4f,
|
|
2533
|
+
/// the size law's gains (W2): scatterGainMax, occlusionGain, shadowGainMax,
|
|
2534
|
+
/// bodyChainLod \u2014 the last being the chain level whose blur already matches the
|
|
2535
|
+
/// body texture, which is what the scattering term measures its octaves from
|
|
2536
|
+
size : vec4f,
|
|
2537
|
+
/// backdrop tone adaptation (W7): backdropToneLow, backdropToneHigh, the size
|
|
2538
|
+
/// bias ALREADY divided by the accessibility refraction cap (so that it may be
|
|
2539
|
+
/// multiplied by the policy-folded 'sizeK' below and still mean the geometric
|
|
2540
|
+
/// thickness), and the strength the accessibility policy resolved \u2014 zero where
|
|
2541
|
+
/// the host could not measure a backdrop tone, which stands the axis down
|
|
2542
|
+
toneAdapt : vec4f,
|
|
2543
|
+
/// the backdrop source's own average colour, linear (xyz), and its luminance (w)
|
|
2544
|
+
toneColour : vec4f,
|
|
2545
|
+
/// the outer shadow (W8): its compositing-space alpha before the size law (x),
|
|
2546
|
+
/// the blur's sigma in group-local CSS px (y), the silhouette's outward spread
|
|
2547
|
+
/// in the same units (z), and the downward offset expressed in field-texture UV
|
|
2548
|
+
/// (w) \u2014 a UV so the offset silhouette is one texture read rather than a
|
|
2549
|
+
/// gradient extrapolation, which is exact only on a straight edge and a capsule
|
|
2550
|
+
/// is mostly not one
|
|
2551
|
+
shadow : vec4f,
|
|
2552
|
+
/// the outer shadow's size-law gain (x), read against the casting surface's own
|
|
2553
|
+
/// thickness, and the field rect's height in CSS px (y) \u2014 the conversion the
|
|
2554
|
+
/// shadow's shift needs when it lands outside the texture; z and w are the
|
|
2555
|
+
/// padding a uniform's vec4 alignment requires
|
|
2556
|
+
shadowSize : vec4f,
|
|
2009
2557
|
};
|
|
2010
2558
|
|
|
2011
2559
|
@group(0) @binding(0) var<uniform> ou : OpticsUniforms;
|
|
@@ -2024,6 +2572,80 @@ fn rim_weight(d : f32, width : f32) -> f32 {
|
|
|
2024
2572
|
return t * t;
|
|
2025
2573
|
}
|
|
2026
2574
|
|
|
2575
|
+
/// The Gaussian CDF the outer shadow's edge falls off by: 1 deep inside the
|
|
2576
|
+
/// shadow's silhouette, 0.5 exactly on it, 0 far outside. The tanh form of the
|
|
2577
|
+
/// normal CDF \u2014 WGSL has no erf, and this is within 1.8e-4 of it everywhere,
|
|
2578
|
+
/// which is 0.015 of one 8-bit code at the shipped occlusion. Mirrors
|
|
2579
|
+
/// material.ts's 'outerShadowFalloff' term for term.
|
|
2580
|
+
fn outer_shadow_falloff(signedDistance : f32, sigma : f32) -> f32 {
|
|
2581
|
+
let x = -signedDistance / max(sigma, 1e-4);
|
|
2582
|
+
return 0.5 * (1.0 + tanh(0.7978845608028654 * (x + 0.044715 * x * x * x)));
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
/// The outer shadow's alpha at this pixel (W8).
|
|
2586
|
+
///
|
|
2587
|
+
/// The shadow is the group's OWN field, translated down by 'shadow.w' in field
|
|
2588
|
+
/// UV and outset by 'shadow.z', then blurred. Reading the offset silhouette from
|
|
2589
|
+
/// the field texture rather than extrapolating the local distance along the
|
|
2590
|
+
/// normal is what keeps a corner a corner: the first-order estimate is exact only
|
|
2591
|
+
/// on a straight edge, and a capsule is mostly not one.
|
|
2592
|
+
///
|
|
2593
|
+
/// The value is an ALPHA on pure black, so what it composites to is the backdrop
|
|
2594
|
+
/// times '1 - alpha' \u2014 multiplicative occlusion, and exactly zero over black,
|
|
2595
|
+
/// with no branch for it.
|
|
2596
|
+
fn outer_shadow_alpha(uv : vec2f, upsampled : f32, fieldSize : vec2f) -> f32 {
|
|
2597
|
+
if (ou.shadow.x <= 0.0) {
|
|
2598
|
+
return 0.0;
|
|
2599
|
+
}
|
|
2600
|
+
/*
|
|
2601
|
+
* The shift can leave the field texture, and clamping alone reads a lie there.
|
|
2602
|
+
*
|
|
2603
|
+
* Two ways it happens, and the second is not an edge case: the rect is clipped
|
|
2604
|
+
* to the canvas, so a surface within the shadow's reach of the viewport's TOP
|
|
2605
|
+
* has no rows above it to shift into; and even unclipped, the rect's pad is the
|
|
2606
|
+
* shadow's reach, so the topmost band of every group needs rows a further
|
|
2607
|
+
* 'offset' above that. Clamped, both repeat the edge texel \u2014 a distance that is
|
|
2608
|
+
* too SMALL, which reads as a flat, too-dark falloff exactly where the shadow
|
|
2609
|
+
* should be fading out, and diverges from the CSS tier, which has no texture to
|
|
2610
|
+
* run out of.
|
|
2611
|
+
*
|
|
2612
|
+
* Reconstructed instead of clamped. A signed distance field is 1-Lipschitz, and
|
|
2613
|
+
* in the region this happens in \u2014 directly above the surface \u2014 moving away from
|
|
2614
|
+
* it increases the distance by exactly the displacement. So adding back the
|
|
2615
|
+
* distance that was clamped off is exact there, and past the surface's corners
|
|
2616
|
+
* it over-estimates, which errs toward LESS shadow rather than more.
|
|
2617
|
+
*/
|
|
2618
|
+
let shiftedY = uv.y - ou.shadow.w;
|
|
2619
|
+
let clampedOffCss =
|
|
2620
|
+
(max(0.0, -shiftedY) + max(0.0, shiftedY - 1.0)) * ou.shadowSize.y;
|
|
2621
|
+
let shadowUv = clamp(vec2f(uv.x, shiftedY), vec2f(0.0), vec2f(1.0));
|
|
2622
|
+
var shadowField : vec4f;
|
|
2623
|
+
var shadowAux : vec4f;
|
|
2624
|
+
if (upsampled > 0.5) {
|
|
2625
|
+
shadowField = textureSampleLevel(fieldTexture, fieldSampler, shadowUv, 0.0);
|
|
2626
|
+
shadowAux = textureSampleLevel(auxTexture, fieldSampler, shadowUv, 0.0);
|
|
2627
|
+
} else {
|
|
2628
|
+
let texel = clamp(vec2i(shadowUv * fieldSize), vec2i(0), vec2i(fieldSize) - vec2i(1));
|
|
2629
|
+
shadowField = textureLoad(fieldTexture, texel, 0);
|
|
2630
|
+
shadowAux = textureLoad(auxTexture, texel, 0);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// The size law reaches the amplitude and nothing else \u2014 the reference's three
|
|
2634
|
+
// lengths are span-invariant across 32\u2026160 px. The gain rides the thickness of
|
|
2635
|
+
// the surface that CAST the shadow, which is the one read at the offset
|
|
2636
|
+
// position rather than the one under the pixel being shaded.
|
|
2637
|
+
//
|
|
2638
|
+
// Applied on '1 - alpha' rather than on the alpha: the gain is a fraction of
|
|
2639
|
+
// the remaining transparency in LINEAR light ('sizeOuterShadowOcclusionAt'),
|
|
2640
|
+
// and (1 - occ) is exactly what raising to 1/2.4 turns into (1 - alpha), so the
|
|
2641
|
+
// two forms are the same law and neither needs the other's space.
|
|
2642
|
+
let sizeK = clamp(shadowAux.z, 0.0, 1.0);
|
|
2643
|
+
let sizeFold = pow(max(1.0 - ou.shadowSize.x * sizeK, 0.0), 1.0 / 2.4);
|
|
2644
|
+
let alpha = 1.0 - (1.0 - ou.shadow.x) * sizeFold;
|
|
2645
|
+
|
|
2646
|
+
return alpha * outer_shadow_falloff(shadowField.x + clampedOffCss - ou.shadow.z, ou.shadow.y);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2027
2649
|
@fragment
|
|
2028
2650
|
fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
|
|
2029
2651
|
// Nominally the field is one texel per device pixel, so the read is an exact
|
|
@@ -2045,8 +2667,19 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
|
|
|
2045
2667
|
let d = field.x;
|
|
2046
2668
|
let normal = field.yz;
|
|
2047
2669
|
let coverage = field.w;
|
|
2670
|
+
|
|
2671
|
+
/*
|
|
2672
|
+
* The outer shadow (W8) sits UNDER the material, so it is resolved before the
|
|
2673
|
+
* body and carried into the output's alpha rather than into its colour: the
|
|
2674
|
+
* pass writes premultiplied black there, and premultiplied black over the page
|
|
2675
|
+
* IS the multiplication. Outside the contour that is the whole of what this
|
|
2676
|
+
* pixel is; inside it the surface covers the shadow, exactly as a 'box-shadow'
|
|
2677
|
+
* is clipped out of its own border box, and the two meet across the coverage
|
|
2678
|
+
* ramp with no seam because it is one expression.
|
|
2679
|
+
*/
|
|
2680
|
+
let shadowAlpha = outer_shadow_alpha(in.uv, ou.flags.w, ou.flags.yz);
|
|
2048
2681
|
if (coverage <= 0.0) {
|
|
2049
|
-
return vec4f(0.0);
|
|
2682
|
+
return vec4f(0.0, 0.0, 0.0, shadowAlpha);
|
|
2050
2683
|
}
|
|
2051
2684
|
|
|
2052
2685
|
let viewport01 = in.position.xy / ou.screen.xy;
|
|
@@ -2054,6 +2687,10 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
|
|
|
2054
2687
|
|
|
2055
2688
|
// Per-pixel, unioned through the field pass. See the module note.
|
|
2056
2689
|
let lensDepth = max(aux.x, 1e-4);
|
|
2690
|
+
// The size law's thickness factor for whichever surface owns this pixel, 0..1.
|
|
2691
|
+
// Zero on anything at or below the profile's 'sizeSpanMin', and every term
|
|
2692
|
+
// below multiplies by it \u2014 so a small control takes the pre-law path exactly.
|
|
2693
|
+
let sizeK = clamp(aux.z, 0.0, 1.0);
|
|
2057
2694
|
// '-d' is depth inside the surface, so the profile runs 1 at the contour to 0
|
|
2058
2695
|
// at 'lensDepth' inward, and the square makes the falloff read as curvature
|
|
2059
2696
|
// rather than as a linear ramp.
|
|
@@ -2069,36 +2706,176 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
|
|
|
2069
2706
|
let bodyLod = clamp(lensDepth * ou.lens.y, 0.0, ou.lens.w);
|
|
2070
2707
|
let lod = clamp(bodyLod - ou.lens.z * profile, 0.0, ou.lens.w);
|
|
2071
2708
|
|
|
2709
|
+
/*
|
|
2710
|
+
* The scattering facet of the size law: "a softer scattering of light".
|
|
2711
|
+
*
|
|
2712
|
+
* The body texture is one blur for the whole backdrop source \u2014 it is built once
|
|
2713
|
+
* per source per frame, so it cannot be per-surface. The chain beside it can:
|
|
2714
|
+
* 'size.w' is the chain level whose blur already matches that body, so
|
|
2715
|
+
* 'size.w + log2(scatterGain)' is the level whose blur is 'scatterGain' times
|
|
2716
|
+
* wider, and lerping the body toward it by 'sizeK' widens the kernel with the
|
|
2717
|
+
* surface. At sizeK = 0 the weight is zero and the body sample stands alone,
|
|
2718
|
+
* which is what makes the whole facet inert on a small control.
|
|
2719
|
+
*/
|
|
2720
|
+
let scatterGain = 1.0 + (ou.size.x - 1.0) * sizeK;
|
|
2721
|
+
let scatterLod = clamp(ou.size.w + log2(max(scatterGain, 1e-4)), 0.0, ou.lens.w);
|
|
2722
|
+
|
|
2072
2723
|
var backdrop = vec3f(0.0);
|
|
2073
2724
|
if (ou.flags.x > 0.5) {
|
|
2074
2725
|
let lensSample = textureSampleLevel(backdropChain, backdropSampler, refractedUv, lod);
|
|
2075
2726
|
let bodySample = textureSampleLevel(backdropBody, backdropSampler, straightUv, 0.0);
|
|
2727
|
+
let scatterSample = textureSampleLevel(backdropChain, backdropSampler, straightUv, scatterLod);
|
|
2076
2728
|
// Premultiplied linear in, straight colour out: the material composites over
|
|
2077
2729
|
// whatever is behind it, so a partially transparent backdrop must not darken
|
|
2078
2730
|
// the glass.
|
|
2079
2731
|
let lensColour = lensSample.rgb / max(lensSample.a, 1e-6);
|
|
2080
|
-
let
|
|
2732
|
+
let scatterColour = scatterSample.rgb / max(scatterSample.a, 1e-6);
|
|
2733
|
+
let bodyColour = mix(bodySample.rgb / max(bodySample.a, 1e-6), scatterColour, sizeK);
|
|
2081
2734
|
backdrop = mix(bodyColour, lensColour, profile);
|
|
2082
2735
|
}
|
|
2083
2736
|
|
|
2084
2737
|
// Adaptive tint. 'adapt.w' is the strength the accessibility policy and the
|
|
2085
2738
|
// group's analysis quality already agreed on; at 0 the fixed tint stands, which
|
|
2086
2739
|
// is what a 'hint' or 'none' group gets.
|
|
2087
|
-
let
|
|
2088
|
-
|
|
2740
|
+
let neutral = mix(ou.tint.rgb, ou.adapt.rgb, ou.adapt.w);
|
|
2741
|
+
|
|
2742
|
+
let backdropLuma = dot(backdrop, vec3f(0.2126, 0.7152, 0.0722));
|
|
2743
|
+
|
|
2744
|
+
/*
|
|
2745
|
+
* Backdrop tone adaptation (W7) \u2014 step two of the composition contract, between
|
|
2746
|
+
* the colour scheme's neutral and the author's tint.
|
|
2747
|
+
*
|
|
2748
|
+
* Read against 'toneColour': the backdrop SOURCE's own average, measured by the
|
|
2749
|
+
* host from the pixels it supplied and handed to both tiers as one number. Not a
|
|
2750
|
+
* per-pixel sample, and that is the load-bearing choice \u2014 see
|
|
2751
|
+
* 'GroupRenderInput.backdropTone' for the cross-tier measurement that settled
|
|
2752
|
+
* it, and note that the reference agrees: its capsule over a sparse bright grid
|
|
2753
|
+
* is a flat body rather than a window onto the grid.
|
|
2754
|
+
*
|
|
2755
|
+
* 'sizeK' is still per pixel, so a container holding a small control and a large
|
|
2756
|
+
* platter adapts each of them by its own thickness out of one pass.
|
|
2757
|
+
*
|
|
2758
|
+
* Strength is zero where the host measured no tone, and the whole axis stands
|
|
2759
|
+
* down where there is no backdrop at all rather than reading the zero vector as
|
|
2760
|
+
* a black backdrop and dissolving the surface into nothing.
|
|
2761
|
+
*
|
|
2762
|
+
* Written out instead of calling smoothstep() so that a profile patched with
|
|
2763
|
+
* low >= high degrades to a step rather than to NaN.
|
|
2764
|
+
*/
|
|
2765
|
+
var toneAdapt = 0.0;
|
|
2766
|
+
if (ou.flags.x > 0.5 && ou.toneAdapt.w > 0.0) {
|
|
2767
|
+
let toneX = ou.toneColour.w + ou.toneAdapt.z * sizeK;
|
|
2768
|
+
let toneT = clamp(
|
|
2769
|
+
(toneX - ou.toneAdapt.x) / max(ou.toneAdapt.y - ou.toneAdapt.x, 1e-6),
|
|
2770
|
+
0.0,
|
|
2771
|
+
1.0,
|
|
2772
|
+
);
|
|
2773
|
+
toneAdapt = clamp(ou.toneAdapt.w, 0.0, 1.0) * (1.0 - toneT * toneT * (3.0 - 2.0 * toneT));
|
|
2774
|
+
}
|
|
2775
|
+
/*
|
|
2776
|
+
* Both the colour and the alpha move, together and not separately. What the
|
|
2777
|
+
* adaptation means is that the INTERIOR converges on the backdrop's tone \u2014
|
|
2778
|
+
* mix(interior, tone, k) \u2014 and this is the (colour, alpha) pair that composites
|
|
2779
|
+
* to exactly that. Lerping the two independently makes a partially adapted
|
|
2780
|
+
* surface lighter than it started (more opaque toward a tint still mostly
|
|
2781
|
+
* neutral), which the 96 px cells caught at once: interior 0.4545 \u2192 0.5179
|
|
2782
|
+
* against a reference of 0.4542. The alpha half is not a colour axis reaching
|
|
2783
|
+
* the occlusion axis \u2014 an adapting material stops transmitting, which is what
|
|
2784
|
+
* the reference's flat body over the impulse grid is. See 'adaptedTintColour'
|
|
2785
|
+
* and 'adaptedTintAlpha' in material.ts.
|
|
2786
|
+
*/
|
|
2787
|
+
let sizedAlpha = ou.tint.w + ou.size.y * sizeK * (1.0 - ou.tint.w);
|
|
2788
|
+
let adaptedAlpha = sizedAlpha + toneAdapt * (1.0 - sizedAlpha);
|
|
2789
|
+
var adapted = neutral;
|
|
2790
|
+
if (toneAdapt > 0.0 && adaptedAlpha > 0.0) {
|
|
2791
|
+
adapted =
|
|
2792
|
+
(neutral * ((1.0 - toneAdapt) * sizedAlpha) + ou.toneColour.rgb * toneAdapt) / adaptedAlpha;
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
// The author tint. 'aux.w' is the per-pixel strength, unioned in the field pass,
|
|
2796
|
+
// so a toolbar can carry one tinted control among plain ones; the seed is a
|
|
2797
|
+
// group uniform. At strength 0 this is the identity and the material is the one
|
|
2798
|
+
// the calibration bed measures, byte for byte.
|
|
2799
|
+
//
|
|
2800
|
+
// Apple's mechanism, not a fill: the seed is the middle of a range of tones
|
|
2801
|
+
// 'mapped to content brightness underneath'. 'backdrop' is what this pixel is
|
|
2802
|
+
// actually looking through \u2014 lens-displaced, LOD-sampled, already the thing the
|
|
2803
|
+
// material transmits \u2014 so the tone is taken against the same light the tint is
|
|
2804
|
+
// about to be mixed into, and a tinted surface over a dark backdrop settles to a
|
|
2805
|
+
// shade of the author's colour rather than sitting on it as paint.
|
|
2806
|
+
//
|
|
2807
|
+
// That 'backdrop' is also where the size law's scattering has already been
|
|
2808
|
+
// applied (above), and that is the right order rather than a coincidence: a
|
|
2809
|
+
// large surface diffuses more of what is behind it, so the tone the tint maps
|
|
2810
|
+
// to should be the diffused light, not the sharp light nobody sees through it.
|
|
2811
|
+
//
|
|
2812
|
+
// It displaces the ADAPTED neutral, not the raw one: a full-strength tint is its
|
|
2813
|
+
// own adaptation and replaces this axis outright, while a partial one moves with
|
|
2814
|
+
// it. That order is the wave's composition contract, in one line.
|
|
2815
|
+
var tintColour = adapted;
|
|
2816
|
+
if (aux.w > 0.0) {
|
|
2817
|
+
let t = smoothstep(ou.tone.z, ou.tone.w, backdropLuma);
|
|
2818
|
+
let low = ou.seed.rgb * ou.tone.x;
|
|
2819
|
+
let high = ou.seed.rgb + (vec3f(1.0) - ou.seed.rgb) * ou.tone.y;
|
|
2820
|
+
// 'seed.w' is the contrast regime's grip on the excursion, never on the hue:
|
|
2821
|
+
// at 0 the material shows the author's colour flat and stops responding.
|
|
2822
|
+
let tone = mix(ou.seed.rgb, mix(low, high, t), clamp(ou.seed.w, 0.0, 1.0));
|
|
2823
|
+
tintColour = mix(adapted, tone, clamp(aux.w, 0.0, 1.0));
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
// The tint layer's ALPHA is untouched by the author's colour. It is the
|
|
2827
|
+
// material's occlusion \u2014 what reduced transparency lifts, and the axis the
|
|
2828
|
+
// system's own Clear-to-Tinted preference runs on \u2014 and an author choosing a
|
|
2829
|
+
// colour does not get to move it.
|
|
2830
|
+
//
|
|
2831
|
+
// The size law does move it, and the two do not collide: "a larger size is more
|
|
2832
|
+
// opaque, a smaller size is clearer" is a statement about how much material
|
|
2833
|
+
// there is, not about what colour it is. A fraction of whatever transparency
|
|
2834
|
+
// the resolved alpha still has, for the reason 'increasedOcclusionLift' is a
|
|
2835
|
+
// fraction \u2014 see material.ts.
|
|
2836
|
+
//
|
|
2837
|
+
// The backdrop adaptation moves it, above, and the author's colour does not.
|
|
2838
|
+
var colour = mix(backdrop, tintColour, adaptedAlpha);
|
|
2839
|
+
|
|
2840
|
+
/*
|
|
2841
|
+
* Everything below is the surface's own APPEARANCE \u2014 the marks that say a
|
|
2842
|
+
* surface is here rather than what is behind it \u2014 and all of it fades with the
|
|
2843
|
+
* adaptation, on the one factor.
|
|
2844
|
+
*
|
|
2845
|
+
* That is not symmetry for its own sake; it is what the reference does, and it
|
|
2846
|
+
* is a calibration cell rather than an inference: the reference's capsule over
|
|
2847
|
+
* the dark-solid backdrop is byte-identical to that background, rim included. A
|
|
2848
|
+
* material that has taken its backdrop's tone has no lit edge to show, because
|
|
2849
|
+
* there is no light in front of it to show one with.
|
|
2850
|
+
*
|
|
2851
|
+
* The gap was invisible until this axis existed. A rim of up to 130/255 sat
|
|
2852
|
+
* unnoticed inside a bright capsule body; with the body gone it is a white
|
|
2853
|
+
* outline around a surface that should not be there at all \u2014 595 pixels past
|
|
2854
|
+
* 2/255 on that one cell, and OKLab \u0394E max 0.47 where p95 already read 0.0000.
|
|
2855
|
+
* The same lesson W3 recorded from the other direction: a term that happens to
|
|
2856
|
+
* be hidden is one feature away from being visible.
|
|
2857
|
+
*/
|
|
2858
|
+
let present = 1.0 - toneAdapt;
|
|
2089
2859
|
|
|
2090
2860
|
// Inner shadow: the material's own occlusion, deepest where the lens is
|
|
2091
|
-
// strongest, which is what makes a thicker surface read as heavier
|
|
2092
|
-
|
|
2861
|
+
// strongest, which is what makes a thicker surface read as heavier \u2014 and the
|
|
2862
|
+
// shadow facet of the size law deepens it further with the span.
|
|
2863
|
+
let shadowDepth = ou.light.z * (1.0 + (ou.size.z - 1.0) * sizeK);
|
|
2864
|
+
colour = colour * (1.0 - profile * shadowDepth * ou.light.w * present);
|
|
2093
2865
|
|
|
2094
2866
|
// Rim and specular from the gradient. The rim is unlit ambient edge brightness;
|
|
2095
2867
|
// the specular term is the same edge lit from 'light.xy'.
|
|
2096
2868
|
let rw = rim_weight(d, ou.rim.x);
|
|
2097
2869
|
let facing = dot(normal, ou.light.xy);
|
|
2098
2870
|
let spec = pow(clamp(facing, 0.0, 1.0), max(ou.rim.z, 1e-3)) * ou.rim.w;
|
|
2099
|
-
colour = colour + vec3f(rw * (ou.rim.y + spec));
|
|
2871
|
+
colour = colour + vec3f(rw * (ou.rim.y + spec) * present);
|
|
2100
2872
|
|
|
2101
|
-
|
|
2873
|
+
// The material over its own shadow over the page, in one premultiplied vector:
|
|
2874
|
+
// the colour is the surface's, weighted by its coverage, and the alpha is what
|
|
2875
|
+
// the two of them together leave the page \u2014 'coverage' from the surface, and
|
|
2876
|
+
// the shadow filling whatever transparency is left.
|
|
2877
|
+
let body = encode_output(max(colour, vec3f(0.0)), coverage);
|
|
2878
|
+
return vec4f(body.rgb, body.a + shadowAlpha * (1.0 - body.a));
|
|
2102
2879
|
}`;
|
|
2103
2880
|
|
|
2104
2881
|
// ../renderer-webgpu/dist/wgsl/prelude.js
|
|
@@ -2345,7 +3122,7 @@ function createPassRunner(context) {
|
|
|
2345
3122
|
};
|
|
2346
3123
|
},
|
|
2347
3124
|
opticsPass(encoder, args) {
|
|
2348
|
-
const slot = uniformSlot(`optics:${args.groupId}`,
|
|
3125
|
+
const slot = uniformSlot(`optics:${args.groupId}`, 60);
|
|
2349
3126
|
const d = slot.data;
|
|
2350
3127
|
d[0] = args.viewportDevice[0];
|
|
2351
3128
|
d[1] = args.viewportDevice[1];
|
|
@@ -2367,18 +3144,46 @@ function createPassRunner(context) {
|
|
|
2367
3144
|
d[17] = args.adaptTint[1];
|
|
2368
3145
|
d[18] = args.adaptTint[2];
|
|
2369
3146
|
d[19] = args.adaptStrength;
|
|
2370
|
-
d[20] = args.
|
|
2371
|
-
d[21] = args.
|
|
2372
|
-
d[22] = args.
|
|
2373
|
-
d[23] = args.
|
|
2374
|
-
d[24] = args.
|
|
2375
|
-
d[25] = args.
|
|
2376
|
-
d[26] = args.
|
|
2377
|
-
d[27] = args.
|
|
2378
|
-
d[28] = args.
|
|
2379
|
-
d[29] = args.
|
|
2380
|
-
d[30] = args.
|
|
2381
|
-
d[31] = args.
|
|
3147
|
+
d[20] = args.tintSeed[0];
|
|
3148
|
+
d[21] = args.tintSeed[1];
|
|
3149
|
+
d[22] = args.tintSeed[2];
|
|
3150
|
+
d[23] = args.tintToneAdaptation;
|
|
3151
|
+
d[24] = args.tintTone[0];
|
|
3152
|
+
d[25] = args.tintTone[1];
|
|
3153
|
+
d[26] = args.tintTone[2];
|
|
3154
|
+
d[27] = Math.max(args.tintTone[3], args.tintTone[2] + 1e-4);
|
|
3155
|
+
d[28] = args.rimWidth;
|
|
3156
|
+
d[29] = args.rimAlpha;
|
|
3157
|
+
d[30] = args.specularPower;
|
|
3158
|
+
d[31] = args.specularGain;
|
|
3159
|
+
d[32] = args.lightDirection[0];
|
|
3160
|
+
d[33] = args.lightDirection[1];
|
|
3161
|
+
d[34] = args.shadowDepth;
|
|
3162
|
+
d[35] = args.shadowAlpha;
|
|
3163
|
+
d[36] = args.backdrop === void 0 ? 0 : 1;
|
|
3164
|
+
d[37] = args.fields.width;
|
|
3165
|
+
d[38] = args.fields.height;
|
|
3166
|
+
d[39] = args.fields.upsampled ? 1 : 0;
|
|
3167
|
+
d[40] = args.sizeScatterGainMax;
|
|
3168
|
+
d[41] = args.sizeOcclusionGain;
|
|
3169
|
+
d[42] = args.sizeShadowGainMax;
|
|
3170
|
+
d[43] = args.bodyChainLod;
|
|
3171
|
+
d[44] = args.backdropTone[0];
|
|
3172
|
+
d[45] = Math.max(args.backdropTone[1], args.backdropTone[0] + 1e-4);
|
|
3173
|
+
d[46] = args.backdropTone[2];
|
|
3174
|
+
d[47] = args.backdropTone[3];
|
|
3175
|
+
d[48] = args.backdropToneColour[0];
|
|
3176
|
+
d[49] = args.backdropToneColour[1];
|
|
3177
|
+
d[50] = args.backdropToneColour[2];
|
|
3178
|
+
d[51] = args.backdropToneColour[3];
|
|
3179
|
+
d[52] = args.outerShadow[0];
|
|
3180
|
+
d[53] = args.outerShadow[1];
|
|
3181
|
+
d[54] = args.outerShadow[2];
|
|
3182
|
+
d[55] = args.outerShadow[3];
|
|
3183
|
+
d[56] = args.outerShadowSizeGain;
|
|
3184
|
+
d[57] = args.outerShadowRectCssHeight;
|
|
3185
|
+
d[58] = 0;
|
|
3186
|
+
d[59] = 0;
|
|
2382
3187
|
slot.write();
|
|
2383
3188
|
const chain = args.backdrop?.chain ?? placeholderView;
|
|
2384
3189
|
const body = args.backdrop?.body ?? placeholderView;
|
|
@@ -2408,7 +3213,7 @@ function createPassRunner(context) {
|
|
|
2408
3213
|
pass.end();
|
|
2409
3214
|
},
|
|
2410
3215
|
highlightPass(encoder, args) {
|
|
2411
|
-
const slot = uniformSlot(`highlight:${args.groupId}`,
|
|
3216
|
+
const slot = uniformSlot(`highlight:${args.groupId}`, 32);
|
|
2412
3217
|
const d = slot.data;
|
|
2413
3218
|
d[0] = args.viewportDevice[0];
|
|
2414
3219
|
d[1] = args.viewportDevice[1];
|
|
@@ -2430,6 +3235,19 @@ function createPassRunner(context) {
|
|
|
2430
3235
|
d[17] = args.fields.height;
|
|
2431
3236
|
d[18] = args.fields.upsampled ? 1 : 0;
|
|
2432
3237
|
d[19] = 0;
|
|
3238
|
+
d[20] = args.backdropTone[0];
|
|
3239
|
+
d[21] = Math.max(args.backdropTone[1], args.backdropTone[0] + 1e-4);
|
|
3240
|
+
d[22] = args.backdropTone[2];
|
|
3241
|
+
d[23] = args.backdropTone[3];
|
|
3242
|
+
d[24] = args.backdropToneLevel;
|
|
3243
|
+
d[25] = 0;
|
|
3244
|
+
d[26] = 0;
|
|
3245
|
+
d[27] = 0;
|
|
3246
|
+
const fieldRect = args.fieldRectDevice;
|
|
3247
|
+
d[28] = args.rectDevice.width / Math.max(fieldRect.width, 1e-6);
|
|
3248
|
+
d[29] = args.rectDevice.height / Math.max(fieldRect.height, 1e-6);
|
|
3249
|
+
d[30] = (args.rectDevice.x - fieldRect.x) / Math.max(fieldRect.width, 1e-6);
|
|
3250
|
+
d[31] = (args.rectDevice.y - fieldRect.y) / Math.max(fieldRect.height, 1e-6);
|
|
2433
3251
|
slot.write();
|
|
2434
3252
|
const pipeline = highlightPipeline(args.targetFormat);
|
|
2435
3253
|
const pass = encoder.beginRenderPass({
|
|
@@ -2540,6 +3358,13 @@ function planPyramid(sourceWidth, sourceHeight, policy) {
|
|
|
2540
3358
|
};
|
|
2541
3359
|
}
|
|
2542
3360
|
var CHAIN_SIGMA_AT_LEVEL_1 = 1.2;
|
|
3361
|
+
function chainLodForSigma(sigmaTexels) {
|
|
3362
|
+
if (!(sigmaTexels > 0))
|
|
3363
|
+
return 0;
|
|
3364
|
+
if (sigmaTexels <= CHAIN_SIGMA_AT_LEVEL_1)
|
|
3365
|
+
return sigmaTexels / CHAIN_SIGMA_AT_LEVEL_1;
|
|
3366
|
+
return 1 + Math.log2(sigmaTexels / CHAIN_SIGMA_AT_LEVEL_1);
|
|
3367
|
+
}
|
|
2543
3368
|
function bodyBlurPlan(sigmaPx, plan) {
|
|
2544
3369
|
if (sigmaPx <= 0)
|
|
2545
3370
|
return { level: 0, residualSigmaTexels: 0 };
|
|
@@ -2683,7 +3508,7 @@ function createPyramidStore(context) {
|
|
|
2683
3508
|
return target;
|
|
2684
3509
|
};
|
|
2685
3510
|
const mipView = (texture, level) => texture.createView({ baseMipLevel: level, mipLevelCount: 1, dimension: "2d" });
|
|
2686
|
-
function allocate(sourceId, plan, bodyLevel, sizeEpoch, builtEpoch) {
|
|
3511
|
+
function allocate(sourceId, plan, bodyLevel, sizeEpoch, builtEpoch, bodySigmaTexels) {
|
|
2687
3512
|
const existing = resources.get(sourceId);
|
|
2688
3513
|
const bodyWidth = (plan.levels[bodyLevel] ?? plan.levels[0]).width;
|
|
2689
3514
|
const bodyHeight = (plan.levels[bodyLevel] ?? plan.levels[0]).height;
|
|
@@ -2720,7 +3545,8 @@ function createPyramidStore(context) {
|
|
|
2720
3545
|
body,
|
|
2721
3546
|
stats,
|
|
2722
3547
|
sizeEpoch,
|
|
2723
|
-
builtEpoch
|
|
3548
|
+
builtEpoch,
|
|
3549
|
+
bodySigmaTexels
|
|
2724
3550
|
};
|
|
2725
3551
|
resources.set(sourceId, next);
|
|
2726
3552
|
return next;
|
|
@@ -2929,8 +3755,9 @@ function createPyramidStore(context) {
|
|
|
2929
3755
|
const [viewportW, viewportH] = request.viewportCss;
|
|
2930
3756
|
const cover = viewportW > 0 && viewportH > 0 ? Math.max(frame.width / viewportW, frame.height / viewportH) : 1;
|
|
2931
3757
|
const planScale = frame.width > 0 ? plan.width / frame.width : 1;
|
|
2932
|
-
const
|
|
2933
|
-
const
|
|
3758
|
+
const bodySigmaTexels = request.bodySigmaCss * cover * planScale;
|
|
3759
|
+
const bodyPlan = bodyBlurPlan(bodySigmaTexels, plan);
|
|
3760
|
+
const target = allocate(request.sourceId, plan, bodyPlan.level, frame.sizeEpoch, request.epoch, bodySigmaTexels);
|
|
2934
3761
|
runImport(encoder, request.sourceId, frame, target.chain);
|
|
2935
3762
|
runChain(encoder, request.sourceId, plan, target.chain);
|
|
2936
3763
|
runBodyBlur(encoder, request.sourceId, plan, target.chain, target.body, bodyPlan.level, bodyPlan.residualSigmaTexels);
|
|
@@ -3046,15 +3873,6 @@ var RENDERER_PASS_IDS = [
|
|
|
3046
3873
|
OPTICS_PASS_ID,
|
|
3047
3874
|
HIGHLIGHT_PASS_ID
|
|
3048
3875
|
];
|
|
3049
|
-
var NOMINAL_MATERIAL_POLICY = {
|
|
3050
|
-
glass: "material",
|
|
3051
|
-
frost: "nominal",
|
|
3052
|
-
refraction: "nominal",
|
|
3053
|
-
occlusion: "nominal",
|
|
3054
|
-
border: "nominal",
|
|
3055
|
-
ambientTint: "nominal",
|
|
3056
|
-
foreground: "adaptive"
|
|
3057
|
-
};
|
|
3058
3876
|
function createWebGPURenderer(options = {}) {
|
|
3059
3877
|
const providers = /* @__PURE__ */ new Map();
|
|
3060
3878
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -3157,6 +3975,7 @@ function createWebGPURenderer(options = {}) {
|
|
|
3157
3975
|
};
|
|
3158
3976
|
const unionOf = (input) => input.union ?? DEFAULT_GROUP_UNION;
|
|
3159
3977
|
const variantOf = (input) => input.variant ?? "regular";
|
|
3978
|
+
const groupTintSeed = (input) => input.surfaces.find((surface) => surface.fieldReferenceOnly !== true && (surface.tint?.strength ?? 0) > 0)?.tint?.color;
|
|
3160
3979
|
const stateOf = (input, resolution) => {
|
|
3161
3980
|
const resolved = resolution?.groups.find((group) => group.groupId === input.groupId);
|
|
3162
3981
|
if (resolved === void 0) {
|
|
@@ -3246,9 +4065,10 @@ function createWebGPURenderer(options = {}) {
|
|
|
3246
4065
|
const input = entry.input;
|
|
3247
4066
|
if (input.surfaces.length === 0)
|
|
3248
4067
|
continue;
|
|
4068
|
+
const policy = resolution?.accessibility.material ?? accessibility;
|
|
3249
4069
|
let surfaces;
|
|
3250
4070
|
try {
|
|
3251
|
-
surfaces = resolveSurfaces(input, governor.knobs.fieldFamily, material);
|
|
4071
|
+
surfaces = resolveSurfaces(input, governor.knobs.fieldFamily, material, policy);
|
|
3252
4072
|
} catch (error) {
|
|
3253
4073
|
skipped.push({
|
|
3254
4074
|
groupId: input.groupId,
|
|
@@ -3257,10 +4077,17 @@ function createWebGPURenderer(options = {}) {
|
|
|
3257
4077
|
continue;
|
|
3258
4078
|
}
|
|
3259
4079
|
const union = unionOf(input);
|
|
3260
|
-
const
|
|
4080
|
+
const shadow = outerShadowUnderPolicy(policy, material);
|
|
4081
|
+
let reachOcclusion = shadow.occlusion;
|
|
4082
|
+
for (const surface of surfaces) {
|
|
4083
|
+
reachOcclusion = Math.max(reachOcclusion, sizeOuterShadowOcclusionAt(shadow.occlusion, surface.sizeThickness, material));
|
|
4084
|
+
}
|
|
4085
|
+
const shadowReachPx = outerShadowReachPx({ ...shadow, occlusion: reachOcclusion });
|
|
4086
|
+
const snapped = snapRectToDevicePixels(groupFieldRect(surfaces, union, void 0, shadowReachPx), dpr);
|
|
3261
4087
|
const rectDevice = clipFieldRectToCanvas(snapped, dpr, viewportDevice);
|
|
3262
4088
|
if (rectDevice === void 0)
|
|
3263
4089
|
continue;
|
|
4090
|
+
const surfaceRectDevice = shadowReachPx === 0 ? rectDevice : clipFieldRectToCanvas(snapRectToDevicePixels(groupFieldRect(surfaces, union), dpr), dpr, viewportDevice) ?? rectDevice;
|
|
3264
4091
|
const packed = packInstances(surfaces, [
|
|
3265
4092
|
rectDevice.x * cssPerDevice,
|
|
3266
4093
|
rectDevice.y * cssPerDevice
|
|
@@ -3279,9 +4106,15 @@ function createWebGPURenderer(options = {}) {
|
|
|
3279
4106
|
union
|
|
3280
4107
|
});
|
|
3281
4108
|
const state = stateOf(input, resolution);
|
|
3282
|
-
const policy = resolution?.accessibility.material ?? accessibility;
|
|
3283
4109
|
const variant = variantOf(input);
|
|
3284
4110
|
const optics = opticsUnderPolicy(material.optics[variant], policy, material);
|
|
4111
|
+
const backdropTone = [
|
|
4112
|
+
material.backdropToneLow,
|
|
4113
|
+
material.backdropToneHigh,
|
|
4114
|
+
backdropToneSizeBiasUnderPolicy(policy, material),
|
|
4115
|
+
input.backdropTone === void 0 ? 0 : backdropToneUnderPolicy(policy, material) * material.backdropToneMax
|
|
4116
|
+
];
|
|
4117
|
+
const backdropToneLevel = input.backdropTone === void 0 ? 0 : relativeLuminance(input.backdropTone);
|
|
3285
4118
|
const refraction = effectiveRefraction(accessibilityRefractionCap(policy), state.refraction);
|
|
3286
4119
|
const refractionScale = material.refractionScale[refraction];
|
|
3287
4120
|
const sourceId = input.backdropSourceId;
|
|
@@ -3305,6 +4138,26 @@ function createWebGPURenderer(options = {}) {
|
|
|
3305
4138
|
tintAlpha: optics.tintAlpha,
|
|
3306
4139
|
adaptTint: adapt?.tint ?? optics.tint,
|
|
3307
4140
|
adaptStrength: adapt?.observed === true ? adaptationStrength(policy, state.analysisExact, material) : 0,
|
|
4141
|
+
/*
|
|
4142
|
+
* `glass: "none"` is forced colours, and it takes the author's tint with
|
|
4143
|
+
* the rest of the material.
|
|
4144
|
+
*
|
|
4145
|
+
* The pass keeps running — it is what paints the surface at all on this
|
|
4146
|
+
* tier — but with no backdrop and an opaque occlusion, so whatever colour
|
|
4147
|
+
* sits here IS the surface. An author tint arriving into that lands as a
|
|
4148
|
+
* flat fill of the author's colour where the platform's palette is
|
|
4149
|
+
* required, which is the one composition forced colours exists to
|
|
4150
|
+
* prevent. Falling back to the material's own tint reproduces exactly
|
|
4151
|
+
* what an untinted surface draws.
|
|
4152
|
+
*/
|
|
4153
|
+
tintSeed: (policy.glass === "none" ? void 0 : groupTintSeed(input)) ?? optics.tint,
|
|
4154
|
+
tintToneAdaptation: tintToneAdaptation(policy, material),
|
|
4155
|
+
tintTone: [
|
|
4156
|
+
material.tintToneFloor,
|
|
4157
|
+
material.tintToneCeilMix,
|
|
4158
|
+
material.tintToneLow,
|
|
4159
|
+
material.tintToneHigh
|
|
4160
|
+
],
|
|
3308
4161
|
rimWidth: optics.rimWidth,
|
|
3309
4162
|
rimAlpha: optics.rimAlpha,
|
|
3310
4163
|
specularPower: optics.specularPower,
|
|
@@ -3312,6 +4165,33 @@ function createWebGPURenderer(options = {}) {
|
|
|
3312
4165
|
lightDirection: material.lightDirection,
|
|
3313
4166
|
shadowDepth: optics.shadowDepth,
|
|
3314
4167
|
shadowAlpha: optics.shadowAlpha,
|
|
4168
|
+
// The size law's gains, per group (W2); the per-pixel factor they
|
|
4169
|
+
// multiply rides the field pass's aux channel, so one uniform serves a
|
|
4170
|
+
// group whose members are not one size. `bodyChainLod` is where the body
|
|
4171
|
+
// blur already sits on the chain — only the pyramid that built it knows
|
|
4172
|
+
// the CSS-px-to-texel conversion, so it publishes the σ and this converts.
|
|
4173
|
+
sizeScatterGainMax: material.sizeScatterGainMax,
|
|
4174
|
+
sizeOcclusionGain: material.sizeOcclusionGain,
|
|
4175
|
+
sizeShadowGainMax: material.sizeShadowGainMax,
|
|
4176
|
+
bodyChainLod: chainLodForSigma(pyramid?.bodySigmaTexels ?? 0),
|
|
4177
|
+
backdropTone,
|
|
4178
|
+
backdropToneColour: [
|
|
4179
|
+
input.backdropTone?.[0] ?? 0,
|
|
4180
|
+
input.backdropTone?.[1] ?? 0,
|
|
4181
|
+
input.backdropTone?.[2] ?? 0,
|
|
4182
|
+
backdropToneLevel
|
|
4183
|
+
],
|
|
4184
|
+
outerShadow: [
|
|
4185
|
+
outerShadowAlpha(shadow.occlusion),
|
|
4186
|
+
shadow.sigmaPx,
|
|
4187
|
+
shadow.spreadPx,
|
|
4188
|
+
// The offset in field-texture UV. The field spans exactly this rect, so
|
|
4189
|
+
// one CSS px is one over the rect's CSS height however the governor
|
|
4190
|
+
// scaled the rasterisation.
|
|
4191
|
+
shadow.offsetPx / Math.max(rectDevice.height * cssPerDevice, 1e-6)
|
|
4192
|
+
],
|
|
4193
|
+
outerShadowSizeGain: shadow.sizeGain,
|
|
4194
|
+
outerShadowRectCssHeight: rectDevice.height * cssPerDevice,
|
|
3315
4195
|
backdrop: pyramid === void 0 || policy.glass === "none" ? void 0 : {
|
|
3316
4196
|
chain: pyramid.chain.createView(),
|
|
3317
4197
|
body: pyramid.body.createView()
|
|
@@ -3323,7 +4203,9 @@ function createWebGPURenderer(options = {}) {
|
|
|
3323
4203
|
groupId: input.groupId,
|
|
3324
4204
|
target: active.highlight,
|
|
3325
4205
|
targetFormat: active.format,
|
|
3326
|
-
|
|
4206
|
+
// The surface's own rect, not the shadow's — see `surfaceRectDevice`.
|
|
4207
|
+
rectDevice: surfaceRectDevice,
|
|
4208
|
+
fieldRectDevice: rectDevice,
|
|
3327
4209
|
fields,
|
|
3328
4210
|
viewportDevice,
|
|
3329
4211
|
cssPerDevice,
|
|
@@ -3335,7 +4217,11 @@ function createWebGPURenderer(options = {}) {
|
|
|
3335
4217
|
pressPointCss: lead.channels.pressPoint ?? lead.centre,
|
|
3336
4218
|
glowRadiusCss: material.glowRadiusCss,
|
|
3337
4219
|
glowGain: material.glowGain,
|
|
3338
|
-
colour: optics.highlight
|
|
4220
|
+
colour: optics.highlight,
|
|
4221
|
+
// The same four numbers the optics pass took, so the two passes fade
|
|
4222
|
+
// the surface on one curve rather than on two copies of it.
|
|
4223
|
+
backdropTone,
|
|
4224
|
+
backdropToneLevel
|
|
3339
4225
|
});
|
|
3340
4226
|
}
|
|
3341
4227
|
drawn += 1;
|
|
@@ -3568,6 +4454,6 @@ function createWebGPURenderer(options = {}) {
|
|
|
3568
4454
|
};
|
|
3569
4455
|
}
|
|
3570
4456
|
|
|
3571
|
-
export { ADAPTIVE_LUMINANCE_HIGH, ADAPTIVE_LUMINANCE_LOW, ADAPTIVE_TINT_DARK, ADAPTIVE_TINT_LIGHT, ANALYSIS_DISPATCH, ANALYSIS_GRID, ANALYSIS_PASS_ID, ANALYSIS_STATS_FLOATS, ANALYSIS_TARGET_EXTENT, ANALYSIS_WORKGROUP, BACKDROP_ALPHA_MODES, BACKDROP_COLOR_SPACES, BACKDROP_KINDS, BACKDROP_PASS_ID, CANVAS_FORMAT, CHAIN_SIGMA_AT_LEVEL_1, CROSS_CHECK_SHAPE_FLOATS, CROSS_CHECK_WORKGROUP, DEFAULT_MATERIAL_PROFILE, FAMILY_C_CROSS_CHECK, FIELD_PASS_ID, GOVERNOR_LADDER, HIGHLIGHT_PASS_ID, IDLE_CHANNELS, INCREASED_OCCLUSION_LIFT, INSTANCE_BYTES, INSTANCE_FLOATS, LENS_BODY_LOD_PER_PX, LENS_RIM_LOD_BIAS, LENS_SIZE_GAIN_MAX,
|
|
3572
|
-
//# sourceMappingURL=dist-
|
|
3573
|
-
//# sourceMappingURL=dist-
|
|
4457
|
+
export { ADAPTIVE_LUMINANCE_HIGH, ADAPTIVE_LUMINANCE_LOW, ADAPTIVE_TINT_DARK, ADAPTIVE_TINT_LIGHT, ANALYSIS_DISPATCH, ANALYSIS_GRID, ANALYSIS_PASS_ID, ANALYSIS_STATS_FLOATS, ANALYSIS_TARGET_EXTENT, ANALYSIS_WORKGROUP, BACKDROP_ALPHA_MODES, BACKDROP_COLOR_SPACES, BACKDROP_KINDS, BACKDROP_PASS_ID, BACKDROP_TONE_HIGH, BACKDROP_TONE_LOW, BACKDROP_TONE_MAX, BACKDROP_TONE_SIZE_BIAS, CANVAS_FORMAT, CHAIN_SIGMA_AT_LEVEL_1, CROSS_CHECK_SHAPE_FLOATS, CROSS_CHECK_WORKGROUP, DEFAULT_MATERIAL_PROFILE, FAMILY_C_CROSS_CHECK, FIELD_PASS_ID, GOVERNOR_LADDER, HIGHLIGHT_PASS_ID, IDLE_CHANNELS, INCREASED_OCCLUSION_LIFT, INSTANCE_BYTES, INSTANCE_FLOATS, LENS_BODY_LOD_PER_PX, LENS_RIM_LOD_BIAS, LENS_SIZE_GAIN_MAX, LUMINANCE_WEIGHTS, MATERIAL_OPTICS, MATERIAL_VARIANTS, MAX_CHAIN_LEVELS, MIN_LEVEL_EXTENT, NOMINAL_GOVERNOR, NOMINAL_MATERIAL_POLICY, OPTICS_PASS_ID, OUTER_SHADOW, OUTPUT_TEXTURE_FORMAT, PASS_LABEL, REFRACTION_LADDER, REFRACTION_SCALE, RENDERER_ERROR_CODES, RENDERER_PASS_IDS, RendererError, SIZE_OCCLUSION_GAIN, SIZE_SCATTER_GAIN_MAX, SIZE_SHADOW_GAIN_MAX, SIZE_SPAN_MAX, SIZE_SPAN_MIN, SRGB_ENCODING_EXPONENT, SUPPORTED_APP_TEXTURE_FORMATS, WGSL_ANALYSIS_PASS, WGSL_CROSS_CHECK_PASS, WGSL_DOWNSAMPLE_PASS, WGSL_FIELD_KERNELS, WGSL_FIELD_PASS, WGSL_FIELD_SAMPLE, WGSL_HIGHLIGHT_PASS, WGSL_IMPORT_PASS, WGSL_INSTANCE_STRUCT, WGSL_OPTICS_PASS, WGSL_PRELUDE, WGSL_RSUPN_GRAD, WGSL_RSUP_GRAD, WGSL_SMOOTH_UNION, WORKING_TEXTURE_FORMAT, ZERO_STATS, accessibilityRefractionCap, adaptationStrength, adaptedTintAlpha, adaptedTintColour, adaptiveTint, allShaderSource, alphaNormalisationMode, analysisModule, backdropToneAdaptation, backdropToneSizeBiasUnderPolicy, backdropToneUnderPolicy, bodyBlurPlan, bodyLod, chainModule, clipFieldRectToCanvas, createAdaptationState, createAppTextureProvider, createCopyProvider, createDeviceHost, createGovernor, createGpuContext, createGradientProvider, createPassRunner, createPipelineCache, createPyramidStore, createRebuildLedger, createStorageSlot, createTexturePool, createTimingCollector, createUniformSlot, createVideoProvider, createWebGPURenderer, crossCheckKernelModule, displayP3ToSrgbLinear, effectiveRefraction, encodeOutput, encodeOutputBytes, fieldModule, fieldPassSource, groupFieldRect, highlightModule, importColorMatrix, importModule, importPassSource, lensDepthPx, lensSizeGain, lensSizeGainFromThickness, linearGradientStops, linearToSrgb, linearToSrgbChannel, occlusionAlphaUnderPolicy, opticsModule, opticsUnderPolicy, outerShadowAlpha, outerShadowFalloff, outerShadowReachPx, outerShadowUnderPolicy, packInstances, pipelineKey, planPyramid, poolKey, readbackDue, refractionRank, relativeLuminance, rendererError, resolveSurfaces, sizeOcclusionAlpha, sizeOcclusionAlphaAt, sizeOuterShadowOcclusion, sizeOuterShadowOcclusionAt, sizeScatterSigma, sizeScatterSigmaAt, sizeShadowDepth, sizeShadowDepthAt, sizeThickness, sizeThicknessUnderPolicy, snapRectToDevicePixels, srgbToLinear, srgbToLinearChannel, statsFromBuffer, supportsTimestamps, tintTone, tintToneAdaptation, tintedTintColour, validateAppTexture, withMaterialOverrides };
|
|
4458
|
+
//# sourceMappingURL=dist-RQ4ZMA3D.js.map
|
|
4459
|
+
//# sourceMappingURL=dist-RQ4ZMA3D.js.map
|