@vitreajs/vitrea 0.1.1 → 0.3.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 +89 -19
- package/dist/{dist-FGJI5LQM.js → dist-MVSXUP3K.js} +1440 -140
- package/dist/dist-MVSXUP3K.js.map +1 -0
- package/dist/index.d.ts +897 -182
- 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
|
@@ -17,6 +17,7 @@ var WGSL_ANALYSIS_PASS = `struct AnalysisUniforms {
|
|
|
17
17
|
@group(0) @binding(3) var<storage, read_write> stats : array<f32>;
|
|
18
18
|
|
|
19
19
|
var<workgroup> partialLum : array<f32, ${ANALYSIS_WORKGROUP}>;
|
|
20
|
+
var<workgroup> partialTone : array<f32, ${ANALYSIS_WORKGROUP}>;
|
|
20
21
|
var<workgroup> partialSq : array<f32, ${ANALYSIS_WORKGROUP}>;
|
|
21
22
|
var<workgroup> partialEdge : array<f32, ${ANALYSIS_WORKGROUP}>;
|
|
22
23
|
|
|
@@ -32,6 +33,7 @@ fn lum_at(uv : vec2f) -> f32 {
|
|
|
32
33
|
fn cs_analysis(@builtin(local_invocation_index) lane : u32) {
|
|
33
34
|
let total = u32(au.grid.x * au.grid.y);
|
|
34
35
|
var sumLum = 0.0;
|
|
36
|
+
var sumTone = 0.0;
|
|
35
37
|
var sumSq = 0.0;
|
|
36
38
|
var sumEdge = 0.0;
|
|
37
39
|
var n = 0.0;
|
|
@@ -47,7 +49,15 @@ fn cs_analysis(@builtin(local_invocation_index) lane : u32) {
|
|
|
47
49
|
let dx = lum_at(uv + vec2f(au.texel.x, 0.0)) - lum_at(uv - vec2f(au.texel.x, 0.0));
|
|
48
50
|
let dy = lum_at(uv + vec2f(0.0, au.texel.y)) - lum_at(uv - vec2f(0.0, au.texel.y));
|
|
49
51
|
|
|
52
|
+
// The published luminance accumulates ENCODED \u2014 the W9 model (claims
|
|
53
|
+
// 5.31): the reference's tone responses track the encoded-space mean of a
|
|
54
|
+
// structured backdrop, not its linear mean. The host decodes the mean once
|
|
55
|
+
// on readback, so every consumer downstream still speaks linear light.
|
|
56
|
+
// The linear sum stays, because the variance fold below needs a mean in
|
|
57
|
+
// the same space as its squares; variance and edge density stay linear \u2014
|
|
58
|
+
// nothing tonal reads them.
|
|
50
59
|
sumLum = sumLum + c;
|
|
60
|
+
sumTone = sumTone + linear_to_srgb(vec3f(c, c, c)).x;
|
|
51
61
|
sumSq = sumSq + c * c;
|
|
52
62
|
sumEdge = sumEdge + length(vec2f(dx, dy)) * 0.5;
|
|
53
63
|
n = n + 1.0;
|
|
@@ -56,6 +66,7 @@ fn cs_analysis(@builtin(local_invocation_index) lane : u32) {
|
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
partialLum[lane] = sumLum;
|
|
69
|
+
partialTone[lane] = sumTone;
|
|
59
70
|
partialSq[lane] = sumSq;
|
|
60
71
|
partialEdge[lane] = sumEdge;
|
|
61
72
|
workgroupBarrier();
|
|
@@ -66,6 +77,7 @@ fn cs_analysis(@builtin(local_invocation_index) lane : u32) {
|
|
|
66
77
|
if (stride == 0u) { break; }
|
|
67
78
|
if (lane < stride) {
|
|
68
79
|
partialLum[lane] = partialLum[lane] + partialLum[lane + stride];
|
|
80
|
+
partialTone[lane] = partialTone[lane] + partialTone[lane + stride];
|
|
69
81
|
partialSq[lane] = partialSq[lane] + partialSq[lane + stride];
|
|
70
82
|
partialEdge[lane] = partialEdge[lane] + partialEdge[lane + stride];
|
|
71
83
|
}
|
|
@@ -76,13 +88,38 @@ fn cs_analysis(@builtin(local_invocation_index) lane : u32) {
|
|
|
76
88
|
if (lane == 0u) {
|
|
77
89
|
let count = max(f32(total), 1.0);
|
|
78
90
|
let mean = partialLum[0] / count;
|
|
79
|
-
stats[0]
|
|
91
|
+
// stats[0] is the ENCODED-space mean; the host decodes it once (W9).
|
|
92
|
+
stats[0] = partialTone[0] / count;
|
|
80
93
|
stats[1] = max(partialSq[0] / count - mean * mean, 0.0);
|
|
81
94
|
stats[2] = partialEdge[0] / count;
|
|
82
95
|
stats[3] = count;
|
|
83
96
|
}
|
|
84
97
|
}`;
|
|
85
98
|
|
|
99
|
+
// ../policy/dist/refraction.js
|
|
100
|
+
var REFRACTION_LADDER = ["none", "approximate", "true"];
|
|
101
|
+
function refractionRank(quality) {
|
|
102
|
+
return REFRACTION_LADDER.indexOf(quality);
|
|
103
|
+
}
|
|
104
|
+
function accessibilityRefractionCap(policy) {
|
|
105
|
+
switch (policy.refraction) {
|
|
106
|
+
case "nominal":
|
|
107
|
+
return "true";
|
|
108
|
+
case "reduced":
|
|
109
|
+
return "approximate";
|
|
110
|
+
case "none":
|
|
111
|
+
return "none";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function effectiveRefraction(a, b) {
|
|
115
|
+
return refractionRank(a) <= refractionRank(b) ? a : b;
|
|
116
|
+
}
|
|
117
|
+
var DEFAULT_REFRACTION_SCALE = {
|
|
118
|
+
none: 0,
|
|
119
|
+
approximate: 0.45,
|
|
120
|
+
true: 1
|
|
121
|
+
};
|
|
122
|
+
|
|
86
123
|
// ../renderer-webgpu/dist/color.js
|
|
87
124
|
var BACKDROP_COLOR_SPACES = ["srgb", "display-p3"];
|
|
88
125
|
var BACKDROP_ALPHA_MODES = ["premultiplied", "unpremultiplied", "opaque"];
|
|
@@ -161,26 +198,18 @@ function encodeOutputBytes(linear, alpha) {
|
|
|
161
198
|
}
|
|
162
199
|
|
|
163
200
|
// ../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
|
-
}
|
|
201
|
+
var NOMINAL_MATERIAL_POLICY = {
|
|
202
|
+
glass: "material",
|
|
203
|
+
frost: "nominal",
|
|
204
|
+
refraction: "nominal",
|
|
205
|
+
occlusion: "nominal",
|
|
206
|
+
border: "nominal",
|
|
207
|
+
ambientTint: "nominal",
|
|
208
|
+
foreground: "adaptive"
|
|
209
|
+
};
|
|
181
210
|
var MATERIAL_VARIANTS = ["regular", "clear"];
|
|
182
211
|
var SRGB_WHITE_TINT = [1, 1, 1];
|
|
183
|
-
var INCREASED_OCCLUSION_LIFT = 0.
|
|
212
|
+
var INCREASED_OCCLUSION_LIFT = 0.75;
|
|
184
213
|
function occlusionAlphaUnderPolicy(nominal, occlusion, lift = INCREASED_OCCLUSION_LIFT) {
|
|
185
214
|
switch (occlusion) {
|
|
186
215
|
case "nominal":
|
|
@@ -197,24 +226,53 @@ var DEFAULT_MATERIAL_PROFILE = {
|
|
|
197
226
|
// `platform-web`'s CSS-tier blur on the same number — and makes core's 24 px
|
|
198
227
|
// `samplingPadding` advisory exactly the 3σ S1 measured.
|
|
199
228
|
regular: {
|
|
200
|
-
|
|
229
|
+
/*
|
|
230
|
+
* REFITTED (recalibration cascade, 2026-08-31) against the ACTIVE-pose bed.
|
|
231
|
+
* C9a's 0.62 and σ = 8 were fitted against the inactive material and are
|
|
232
|
+
* kept in git history, not here.
|
|
233
|
+
*
|
|
234
|
+
* `blurSigma` 3 is the first value this constant has ever had that the
|
|
235
|
+
* fixtures could identify. §6.1 called it unidentifiable because the
|
|
236
|
+
* inactive reference passed almost no backdrop structure; the active one
|
|
237
|
+
* passes a great deal (interior standard deviation 0.1358 over the
|
|
238
|
+
* checkerboard against vitrea's 0.0176 at σ = 8), so the objective's spread
|
|
239
|
+
* term now bites — it triples from σ = 3 to σ = 6 at a fixed alpha.
|
|
240
|
+
*
|
|
241
|
+
* `tintAlpha` 0.46 is the minimiser over the eight tone-inert untinted rest
|
|
242
|
+
* calibration cells, a genuine interior optimum (0.38 → 0.16945, 0.46 →
|
|
243
|
+
* 0.15704, 0.54 → 0.17248) with ΔE and SSIM agreeing. It remains true that
|
|
244
|
+
* no single value is right for every scene: a lerp toward one tint colour
|
|
245
|
+
* implies an alpha of 0.24 over the checkerboard and 0.47 over the photo at
|
|
246
|
+
* the same 44 px span, which is the lerp-versus-multiply question C9a
|
|
247
|
+
* recorded and nobody has acted on. Claims §5.13.
|
|
248
|
+
*/
|
|
249
|
+
// 1.25 since W11c G1 (claims §5.41): the reference's interior over
|
|
250
|
+
// structured content is a sharp component near σ 1.25 plus a heavy one
|
|
251
|
+
// the scatter facet now supplies (`sizeScatterFloor`); the cascade's 3
|
|
252
|
+
// (claims §5.16) was the one Gaussian that best split the difference on
|
|
253
|
+
// a bed with no pitch axis to tell the two apart.
|
|
254
|
+
blurSigma: 1.25,
|
|
201
255
|
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,
|
|
256
|
+
tintAlpha: 0.46,
|
|
212
257
|
rimWidth: 1.5,
|
|
213
258
|
rimAlpha: 0.18,
|
|
214
259
|
specularPower: 6,
|
|
215
260
|
specularGain: 0.55,
|
|
216
261
|
shadowDepth: 0.35,
|
|
217
|
-
|
|
262
|
+
/*
|
|
263
|
+
* REFITTED 0.55 → 0.05 (2026-08-31), and it is the largest single
|
|
264
|
+
* improvement in the cascade: the objective falls 0.14615 → 0.09333 with
|
|
265
|
+
* ΔE and SSIM both improving.
|
|
266
|
+
*
|
|
267
|
+
* The active reference's contour is BRIGHTER than its own body — a rim peak
|
|
268
|
+
* of 0.025…0.129 above baseline, always at one pixel deep. vitrea's read
|
|
269
|
+
* 0.0000 with its peak 8-12 px in, meaning it had no edge feature at all:
|
|
270
|
+
* the inner shadow at 0.55 was darkening the contour faster than the rim lit
|
|
271
|
+
* it. The rim constants themselves did not move; the thing suppressing them
|
|
272
|
+
* did. On the inactive bed the reference's rim was 0.0000…0.0041 and this
|
|
273
|
+
* was invisible, which is what §6.2 recorded as "below quantisation".
|
|
274
|
+
*/
|
|
275
|
+
shadowAlpha: 0.05,
|
|
218
276
|
highlight: srgbToLinear(SRGB_WHITE_TINT)
|
|
219
277
|
},
|
|
220
278
|
// Persistently more transparent, so it frosts less and tints less.
|
|
@@ -260,20 +318,311 @@ var DEFAULT_MATERIAL_PROFILE = {
|
|
|
260
318
|
adaptiveTintLight: srgbToLinear(SRGB_WHITE_TINT),
|
|
261
319
|
adaptiveLuminanceLow: 0.12,
|
|
262
320
|
adaptiveLuminanceHigh: 0.42,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
321
|
+
// Authored in `@vitrea/policy` alongside the ladder it is keyed by, because the
|
|
322
|
+
// CSS tier needs the same table and used to hold two more copies of it
|
|
323
|
+
// (Decision Log #23(d)). The profile is still what the shaders read and still
|
|
324
|
+
// what `withMaterialOverrides` replaces — only the *default* literal moved, and
|
|
325
|
+
// it moved unchanged.
|
|
326
|
+
refractionScale: DEFAULT_REFRACTION_SCALE,
|
|
327
|
+
/*
|
|
328
|
+
* MEASURED (W2), against the settled apple-macos-26.5 bed, and set from the
|
|
329
|
+
* REFERENCE's own size-dependence rather than from the objective's minimum.
|
|
330
|
+
*
|
|
331
|
+
* The band was 28…420 while it served the lens alone and nothing had measured
|
|
332
|
+
* it, which put the whole canonical range (32…160 px) inside the first 17% of
|
|
333
|
+
* the curve. What the settled reference actually does, over a fixed checkerboard
|
|
334
|
+
* backdrop: it passes 0.244 of the backdrop's contrast at a 32 px span, 0.230 at
|
|
335
|
+
* 44 px and 0.144 at 96 px. So 6% of the movement happens between 32 and 44 and
|
|
336
|
+
* the rest between 44 and 96, and it is finished by 96 — which is what these two
|
|
337
|
+
* numbers are. `sizeSpanMin` is also the smallest canonical component's span, so
|
|
338
|
+
* that component is the law's exact zero by construction.
|
|
339
|
+
*
|
|
340
|
+
* The tuning objective would rather have 64 (0.1568 against 0.1637 on the rest
|
|
341
|
+
* cells, a 4% win inside a grid that spans 1.07×). Declined, and the reason is
|
|
342
|
+
* recorded rather than the win taken: the gain comes entirely from the interior
|
|
343
|
+
* *level* term, whose residual is the backdrop tone-adaptation gap that wave
|
|
344
|
+
* child W7 is chartered to close — so a band fitted to 64 would be using the
|
|
345
|
+
* size law as a proxy for a mechanism vitrea does not have yet, against the
|
|
346
|
+
* reference's own measurement of where its size-dependence lives, and W7 would
|
|
347
|
+
* have to unpick it. See the claims doc's size-law section.
|
|
348
|
+
*/
|
|
349
|
+
sizeSpanMin: 32,
|
|
350
|
+
sizeSpanMax: 96,
|
|
270
351
|
lensSizeGainMax: 2.6,
|
|
271
|
-
|
|
272
|
-
|
|
352
|
+
/*
|
|
353
|
+
* MEASURED (W2), each with its own status — see the fields' notes.
|
|
354
|
+
*
|
|
355
|
+
* `sizeScatterGainMax` = 1: implemented on both tiers and inert, because the
|
|
356
|
+
* canonical fixtures cannot resolve it. The objective is flat to 1.00× over
|
|
357
|
+
* gains 1…6, and that is not the backdrop chain's depth talking: re-run with the
|
|
358
|
+
* chain deepened (MIN_LEVEL_EXTENT 8 → 4, which lifts the reachable σ on the
|
|
359
|
+
* 320×200 canvas from 1.2× to 2.4×) the grid is still flat to 1.00× and still
|
|
360
|
+
* best at 1. A number the fixtures cannot see is a number that will be met by
|
|
361
|
+
* accident, so this ships at the identity with the mechanism in place.
|
|
362
|
+
*
|
|
363
|
+
* `sizeOcclusionGain` = 0: fitted, and the fit is a boundary optimum with real
|
|
364
|
+
* leverage against it — the objective rises monotonically, 0.168 → 0.224 across
|
|
365
|
+
* 0…0.5, a 1.33× spread. The diagnosis is not a tuning failure: vitrea's
|
|
366
|
+
* interior sits 0.16–0.19 above the reference's at every span because the
|
|
367
|
+
* reference's level is set by backdrop tone adaptation toward roughly 0.63 while
|
|
368
|
+
* vitrea lerps toward a white tint, so making a large surface *more* opaque can
|
|
369
|
+
* only take it further from the reference. The facet is Apple's ("a larger size
|
|
370
|
+
* is more opaque"); the axis that would let it fit is W7's.
|
|
371
|
+
*
|
|
372
|
+
* `sizeShadowGainMax` = 1.4: fitted on the calibration REST cells, where the
|
|
373
|
+
* 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.
|
|
374
|
+
* 1.4 through 2.2 are one flat region (0.25% apart); 1.4 is the point inside it
|
|
375
|
+
* that costs the checks least (ΔE 0.01282 against the baseline's 0.01290, SSIM
|
|
376
|
+
* 0.9671 against 0.9689) and it is the per-cell minimum on both well-conditioned
|
|
377
|
+
* span-96 rest cells. The pressed cells prefer 2.4 monotonically and are
|
|
378
|
+
* excluded from the fit on §6.3's grounds — their native side carries no press
|
|
379
|
+
* pose, so they compare two different states and cannot arbitrate a material
|
|
380
|
+
* constant.
|
|
381
|
+
*
|
|
382
|
+
* ## Re-fitted against the ACTIVE bed (2026-08-31)
|
|
383
|
+
*
|
|
384
|
+
* `sizeScatterGainMax` stays 1 and is now POSITIVELY identified rather than
|
|
385
|
+
* flat: with `blurSigma` refitted to 3 the objective rises 6-7% at gains 2 and
|
|
386
|
+
* 4, so the identity is the optimum instead of a tie.
|
|
387
|
+
*
|
|
388
|
+
* `sizeOcclusionGain` stays 0, a boundary result for the fourth time and for a
|
|
389
|
+
* fourth distinct reason. The reasoning above is retired: the per-cell residual
|
|
390
|
+
* now points the gain's way in every backdrop (web − reference falls with span
|
|
391
|
+
* — checkerboard +0.105/+0.112/+0.046 at 32/44/96 px, photo −0.004/−0.087,
|
|
392
|
+
* light-solid −0.028/+0.001), so the facet finally has the right sign. What it
|
|
393
|
+
* does not have is magnitude: over 0/0.05/0.10/0.15 × spans ending 96/128/160
|
|
394
|
+
* the whole grid is flat to 1.04×, and a 0.2% preference is not evidence. It
|
|
395
|
+
* ships at the identity because the bed cannot identify it, not because the
|
|
396
|
+
* mechanism is wrong.
|
|
397
|
+
*
|
|
398
|
+
* ## Re-fitted against the FROZEN bed (2026-09-01)
|
|
399
|
+
*
|
|
400
|
+
* `sizeOcclusionGain` 0 → **0.05**, and this is the first time the bed could
|
|
401
|
+
* see it. Every earlier fit was a boundary result because calibration held no
|
|
402
|
+
* span above `sizeSpanMax`, so the curve this gain rides was saturated at every
|
|
403
|
+
* cell that could vote. The frozen bed adds three span-128 `rrect-ml`
|
|
404
|
+
* calibration cells, and with them the grid stops being flat: 0.05 is the
|
|
405
|
+
* optimum at **both** backing scales independently — 1× 0.08288 against the
|
|
406
|
+
* identity's 0.08374, 2× 0.09338 against 0.09557 — and ΔE and SSIM move the
|
|
407
|
+
* same way at both.
|
|
408
|
+
*
|
|
409
|
+
* It is still a small number and it is reported as one: a 1.0% preference at
|
|
410
|
+
* 1×, against the 0.2% this doc rightly refused a round ago. What changed is
|
|
411
|
+
* not the margin's size but that it now reproduces on an independent profile
|
|
412
|
+
* instead of resting on one flat grid.
|
|
413
|
+
*
|
|
414
|
+
* `sizeSpanMax` stays 96, and that is now a measurement rather than an
|
|
415
|
+
* assumption. With a span-128 cell voting, the grid rises monotonically —
|
|
416
|
+
* 96/112/128/144/160/192 reading 0.0837/0.0858/0.0943/0.1060/0.1171/0.1300 —
|
|
417
|
+
* so the band's top is where it was, and the suspicion that the bed simply
|
|
418
|
+
* could not see above it is answered rather than inherited.
|
|
419
|
+
*
|
|
420
|
+
* `sizeShadowGainMax` 1.4 → 1. Its evidence dissolved rather than reversing:
|
|
421
|
+
* this is a gain on the INNER SHADOW, and the inner shadow was refitted from
|
|
422
|
+
* `shadowAlpha` 0.55 to 0.05, so there is almost nothing left for it to gain
|
|
423
|
+
* on. The grid 1.0/1.4/2.2 is flat to 0.2%. Carrying a fitted-looking 1.4 whose
|
|
424
|
+
* measurement no longer exists would be the worse of the two errors.
|
|
425
|
+
*/
|
|
426
|
+
// MEASURED (W11c G1, claims §5.41): the scatter facet off the identity, on
|
|
427
|
+
// its own curve — see `MaterialProfile.sizeScatterFloor`. The paragraph
|
|
428
|
+
// above records why the identity was the right answer while `blurSigma`
|
|
429
|
+
// was 3 and the bed had no pitch axis; the W9 probe's pitch axis is what
|
|
430
|
+
// identified the two-component interior this expresses.
|
|
431
|
+
sizeScatterGainMax: 8,
|
|
432
|
+
sizeScatterFloor: 0.4,
|
|
433
|
+
sizeScatterSpanMax: 256,
|
|
434
|
+
sizeOcclusionGain: 0.05,
|
|
435
|
+
sizeShadowGainMax: 1,
|
|
436
|
+
// MEASURED (W11c G2, claims §5.43): the reference band's inward source
|
|
437
|
+
// displacement is 33 px at the contour on a 20.8 px lens depth — 1.6 lens
|
|
438
|
+
// depths — following the shader's own (1 − depth)² profile (per-shell fits
|
|
439
|
+
// on the probe's pitch-32/64 cells, spans 96/128 fitted, 160 held out; the
|
|
440
|
+
// gain's sweep 1.4/1.6/1.8/2.4 reads 0.0487/0.0478/0.0516/0.0567 at the
|
|
441
|
+
// shader's depth and profile). The band folds the plate from 1.6 lens depths
|
|
442
|
+
// inward; nothing in it is sharper or darker than the interior.
|
|
443
|
+
lensRefractionGain: 1.6,
|
|
273
444
|
reducedTransparencyFrost: 1.75,
|
|
274
445
|
increasedOcclusionLift: INCREASED_OCCLUSION_LIFT,
|
|
275
446
|
strongBorderRim: { rimWidth: 2, rimAlpha: 0.95 },
|
|
276
447
|
reducedTintAdaptation: 0.35,
|
|
448
|
+
/*
|
|
449
|
+
* MEASURED (W10, 2026-09-02) — per-pixel least squares of the reference's
|
|
450
|
+
* tinted pixel against its own untinted pixel on the five W9-probe tinted
|
|
451
|
+
* checkerboard cells (pitch 4…64 px, 17 700 px): shade = 0.5289 + 0.4886·u,
|
|
452
|
+
* RMS 0.0035. Out of sample on the canonical bed the orange cells fit at
|
|
453
|
+
* RMS 0.003 with zero bias; blue sits 0.011 darker (the second hue's
|
|
454
|
+
* residual, recorded and unmodelled). The light end extrapolates past 1 and
|
|
455
|
+
* is clamped in `tintShade`. Claims §5.36.
|
|
456
|
+
*
|
|
457
|
+
* §5.13's earlier "the curve is the identity" fit was made on the material's
|
|
458
|
+
* MEAN over solid backdrops, where u is either ~0.97 (shade 1.0) or the
|
|
459
|
+
* collapse has already folded the shade out — the identity was the law's
|
|
460
|
+
* two endpoints, seen without anything between them.
|
|
461
|
+
*/
|
|
462
|
+
tintShadeDark: 0.5289,
|
|
463
|
+
tintShadeLight: 1.0175,
|
|
464
|
+
tintShadeStrength: 1,
|
|
465
|
+
/*
|
|
466
|
+
* MEASURED (W7), against the settled apple-macos-26.5 bed, on the light and
|
|
467
|
+
* dark standard profiles jointly and at both scales.
|
|
468
|
+
*
|
|
469
|
+
* The observable these four are set from is the one quantity in this bed that
|
|
470
|
+
* isolates adaptation from everything else: the *separation* between the light
|
|
471
|
+
* and dark references over the same backdrop, on the same component. Under this
|
|
472
|
+
* mechanism that separation is `(1 − tintAlpha)(1 − a)(tintLight − tintDark)`,
|
|
473
|
+
* so the material's transmission, the backdrop's structure and each scheme's
|
|
474
|
+
* own tint all cancel and what is left is `a`. Normalised at the checkerboard,
|
|
475
|
+
* where nothing adapts, it reads:
|
|
476
|
+
*
|
|
477
|
+
* span 44: 0.000 at backdrop 0.500, 0.030 at 0.205, 1.000 at 0.0117, 1.000 at 0.0049
|
|
478
|
+
* span 96: 0.000 at backdrop 0.500, 0.028 at 0.216, 0.256 at 0.0117
|
|
479
|
+
*
|
|
480
|
+
* and the 2× bed reproduces every one of those to three decimals (0.2553
|
|
481
|
+
* against 0.2556 on the one that is not a boundary). Two facts follow. The
|
|
482
|
+
* adaptation is off across the whole ordinary range and turns on only below
|
|
483
|
+
* roughly a fifth of the backdrop scale — so it cannot disturb a cell that
|
|
484
|
+
* already passes. And it is size-gated hard: same backdrop, same material, 1.000
|
|
485
|
+
* against 0.256.
|
|
486
|
+
*
|
|
487
|
+
* The band's dark end is not identifiable from this bed and the fit says so:
|
|
488
|
+
* the reference's backdrops jump from 0.0117 to 0.205 with nothing in between,
|
|
489
|
+
* so `backdropToneLow` is bounded only by "at or below the darkest calibration
|
|
490
|
+
* backdrop". What the two dark backdrops DO pin is the curve's slope near zero —
|
|
491
|
+
* the 96 px surface reads 0.256 at 0.0117 and 0.356 at 0.0039, which is a
|
|
492
|
+
* measured intermediate rather than a step, and it is what fixes
|
|
493
|
+
* `backdropToneSizeBias` against `backdropToneHigh`. `impulse__rrect-md__rest`
|
|
494
|
+
* is a VALIDATION cell and was not fitted to: the calibration set predicts
|
|
495
|
+
* 0.34 there and the cell reads 0.356.
|
|
496
|
+
*/
|
|
497
|
+
/*
|
|
498
|
+
* RE-SCOPED (W9, claims §5.33). The four mix constants now own TEXTURE
|
|
499
|
+
* COLLAPSE alone — the interior mean moved to the response law below — and
|
|
500
|
+
* their band contracts to the one domain the collapse is real in: full at
|
|
501
|
+
* `dark-solid` (0.0117) for thin surfaces including the 44 px capsule
|
|
502
|
+
* (byte-identical collapse, arg 0.0164 ≤ low with margin), zero by
|
|
503
|
+
* `mid-dark-solid` (0.0595), where the reference's small surface keeps a
|
|
504
|
+
* textured body at 0.4561 and the old band's partial collapse (k = 0.81)
|
|
505
|
+
* was the measured 0.1375-vs-0.4561 overshoot. `impulse__rrect-md`
|
|
506
|
+
* (arg 0.0539) keeps k ≈ 0.003 against the old band's 0.008 — the same
|
|
507
|
+
* unadapted render that cell validated.
|
|
508
|
+
*/
|
|
509
|
+
backdropToneMax: 1,
|
|
510
|
+
backdropToneLow: 0.02,
|
|
511
|
+
backdropToneHigh: 0.055,
|
|
512
|
+
/*
|
|
513
|
+
* REFITTED 0.09 → 0.13 (2026-08-31, active bed). The law's SHAPE is untouched
|
|
514
|
+
* (Decision Log 13 stands; the two-axis rework is next wave) and `max`, `low`
|
|
515
|
+
* and `high` are unmoved — only the size gate widened.
|
|
516
|
+
*
|
|
517
|
+
* What it changes is one cell: a 96 pt surface over the darkest backdrop now
|
|
518
|
+
* barely adapts, where at 0.09 it adapted by a quarter. Two independent reads
|
|
519
|
+
* of the reference disagreed about that and the validation set broke the tie.
|
|
520
|
+
* The light-versus-dark separation estimator §5.8 fitted on says the 96 pt
|
|
521
|
+
* surface adapts by 0.30; the reference's own interior LEVEL says it does not
|
|
522
|
+
* (0.4844, against 0.466 for an unadapted surface at the refitted tint alpha
|
|
523
|
+
* and 0.3566 for an adapted one). Measured once on
|
|
524
|
+
* `impulse__rrect-md__rest`, which is validation and was fitted to by
|
|
525
|
+
* neither: 0.09 renders 0.2858 against a reference of 0.4358 at ΔE 0.02344;
|
|
526
|
+
* 0.13 renders 0.4594 at ΔE 0.00378, six times better. The separation
|
|
527
|
+
* estimator's algebra assumes the two colour schemes share one tint alpha,
|
|
528
|
+
* and this profile pair does not (0.46 against 0.97) — recorded in §5.13 as
|
|
529
|
+
* the reason it is no longer the primary evidence for this constant.
|
|
530
|
+
*
|
|
531
|
+
* RE-SCOPED 0.13 → 0.05 with the band above (W9): the bias's one remaining
|
|
532
|
+
* job is keeping the thick surface's collapse argument above `high` at the
|
|
533
|
+
* dark anchor (0.0117 + 0.05 = 0.0617 > 0.055) while the thin capsule's
|
|
534
|
+
* stays below `low` — the near-binary size snap the probe measured
|
|
535
|
+
* (claims §5.33). The smooth size trend on structured backdrops, which the
|
|
536
|
+
* old wide band tried and failed to carry, belongs to the response law's
|
|
537
|
+
* thickness axis now.
|
|
538
|
+
*/
|
|
539
|
+
backdropToneSizeBias: 0.05,
|
|
540
|
+
/*
|
|
541
|
+
* MEASURED (W9 probe, claims §5.30–§5.33): the anchors are the probe bed's
|
|
542
|
+
* settled reference levels, frequency-settled over seven attested runs,
|
|
543
|
+
* under the probe's own native-mask interior. Thick rows pool the 96 px
|
|
544
|
+
* and 160 px rrects (±0.012). Not tuned; re-measured only by a new probe.
|
|
545
|
+
*/
|
|
546
|
+
backdropToneAnchorX: [0.1104, 0.2706, 0.9505],
|
|
547
|
+
backdropToneResponseThin: [0.0126, 0.4561, 0.9713],
|
|
548
|
+
backdropToneResponseThick: [0.4953, 0.5744, 0.9358],
|
|
549
|
+
backdropToneResponseStrength: 1,
|
|
550
|
+
/*
|
|
551
|
+
* FITTED (recalibration cascade, 2026-08-31). W8's geometry SURVIVES the fit
|
|
552
|
+
* unchanged — `sigmaPx` 15.55, `offsetPx` 7.95, `spreadPx` 3.10 — and only the
|
|
553
|
+
* two amplitudes moved: `occlusion` 0.33 → 0.285 and
|
|
554
|
+
* `reducedTransparencyOcclusion` 0.566 → 0.70. The dark scheme's amplitude
|
|
555
|
+
* (0.09) lands as a profile patch, not as a branch here.
|
|
556
|
+
*
|
|
557
|
+
* The fit is `scripts/sweep.ts --objective shadow`, whose term is the mean over
|
|
558
|
+
* calibration cells of |Δ meanDeparture| — the light each side removes from the
|
|
559
|
+
* whole exterior, in linear light, which is the one commensurate quantity the
|
|
560
|
+
* facet has. The amplitude is a genuine interior optimum over 0.18…0.44 (a
|
|
561
|
+
* 2.23× spread) and flat to 1.04% across 0.255…0.315, which is exactly the
|
|
562
|
+
* scene-to-scene amplitude spread the reference itself shows. The geometry is
|
|
563
|
+
* NOT identifiable against this objective — σ ∈ {13.5, 15.55, 17.5} × offset ∈
|
|
564
|
+
* {6.5, 7.95, 9.4} spans 1.04× with 15.55 the argmin — because an integral over
|
|
565
|
+
* the exterior is insensitive to how the darkening is distributed within it.
|
|
566
|
+
* W8's own two-dimensional fit against the occlusion field measures the
|
|
567
|
+
* geometry far more powerfully (RMS 0.0021 over 142,550 pixels), and the
|
|
568
|
+
* instrument's independent shadow axis agrees, so the geometry stands on those
|
|
569
|
+
* two and this objective is not asked to re-decide it.
|
|
570
|
+
*
|
|
571
|
+
* `reducedTransparencyOcclusion` 0.70 is sharp where the amplitude is flat: a
|
|
572
|
+
* 3.83× spread over 0.45…0.95 with a clear minimum. It also reconciles two
|
|
573
|
+
* routes — 0.285 × 0.70 = 0.1995 against the reference's directly measured
|
|
574
|
+
* reduce-transparency amplitude of 0.203 at a 44 px span. W8's 0.566 was the
|
|
575
|
+
* ratio of the reference's two amplitudes; this is the ratio that makes
|
|
576
|
+
* vitrea's shadow match the reference's under the preference, and the two
|
|
577
|
+
* differ because the base amplitude is a compromise across scenes whose spread
|
|
578
|
+
* is much wider in the standard profile than under reduced transparency.
|
|
579
|
+
*
|
|
580
|
+
* Extracted from the active bed's native fixtures directly —
|
|
581
|
+
* `results/2026-08-31-active-bed-stage0.json` measured the gap, these numbers
|
|
582
|
+
* measure the facet — and left provisional deliberately: X1 gives the fit to
|
|
583
|
+
* the recalibration cascade, which owns the holdout discipline. This child owns
|
|
584
|
+
* the mechanism.
|
|
585
|
+
*
|
|
586
|
+
* Method: for each `rest` fixture, the occlusion field `1 − L_capture/L_background`
|
|
587
|
+
* in linear Rec.709 luminance, over every pixel outside the declared component
|
|
588
|
+
* geometry with a backdrop bright enough to carry a signal; fitted in two
|
|
589
|
+
* dimensions against `occlusion · Φ(−sd/σ)`, where `sd` is the signed distance
|
|
590
|
+
* to the component's own rounded silhouette translated down by `offsetPx` and
|
|
591
|
+
* outset by `spreadPx`.
|
|
592
|
+
*
|
|
593
|
+
* The fit and its residual, on `1x-light-standard` and `2x-light-standard`:
|
|
594
|
+
*
|
|
595
|
+
* backdrop span occlusion σ (1×/2×) offset (1×/2×) RMS
|
|
596
|
+
* photo 44 0.323 15.5/31.1 7.9/15.8 0.0021
|
|
597
|
+
* checkerboard 44 0.338 15.7/31.0 8.0/15.9 0.0016
|
|
598
|
+
* hc-text 44 0.339 15.7/31.1 8.0/15.9 0.0014
|
|
599
|
+
* photo 32 0.321 15.5/30.9 8.1/16.2 0.0018
|
|
600
|
+
* mid-dark-solid 44 0.310 — — 0.0079
|
|
601
|
+
*
|
|
602
|
+
* — against a peak occlusion of 0.24, so the model carries the facet to under
|
|
603
|
+
* 1% of its own amplitude, over up to 142,550 pixels per cell. The three
|
|
604
|
+
* lengths are the consensus across every well-conditioned cell; `occlusion` is
|
|
605
|
+
* the light-standard amplitude at a span the size law leaves alone.
|
|
606
|
+
*
|
|
607
|
+
* What the amplitude does NOT yet have is a mechanism for its scene-to-scene
|
|
608
|
+
* spread. Over the flat near-white `light-solid` backdrop the same fit reads
|
|
609
|
+
* 0.123 rather than 0.33, reproducibly and at both scales, while the other flat
|
|
610
|
+
* backdrop (`mid-dark-solid`, linear 0.0595) reads 0.310 — so it is not a
|
|
611
|
+
* function of the backdrop's luminance, its structure, or the material's
|
|
612
|
+
* interior level, and no compositing model in either colour space produces both.
|
|
613
|
+
* The dark-scheme profile is a separate amplitude entirely (0.046…0.061 at a
|
|
614
|
+
* 44 px span — the dark material's shadow is nearly invisible) and lands as a
|
|
615
|
+
* profile patch, not as a branch here. Both are stated in the claims doc as the
|
|
616
|
+
* open question the cascade's fit inherits.
|
|
617
|
+
*/
|
|
618
|
+
outerShadow: {
|
|
619
|
+
offsetPx: 7.95,
|
|
620
|
+
sigmaPx: 15.55,
|
|
621
|
+
spreadPx: 3.1,
|
|
622
|
+
occlusion: 0.285,
|
|
623
|
+
reducedTransparencyOcclusion: 0.7,
|
|
624
|
+
sizeGain: 0
|
|
625
|
+
},
|
|
277
626
|
lightDirection: [-0.3714, -0.9285],
|
|
278
627
|
sweepBandRadians: 0.55,
|
|
279
628
|
glowRadiusCss: 44,
|
|
@@ -286,11 +635,24 @@ var ADAPTIVE_TINT_DARK = DEFAULT_MATERIAL_PROFILE.adaptiveTintDark;
|
|
|
286
635
|
var ADAPTIVE_TINT_LIGHT = DEFAULT_MATERIAL_PROFILE.adaptiveTintLight;
|
|
287
636
|
var ADAPTIVE_LUMINANCE_LOW = DEFAULT_MATERIAL_PROFILE.adaptiveLuminanceLow;
|
|
288
637
|
var ADAPTIVE_LUMINANCE_HIGH = DEFAULT_MATERIAL_PROFILE.adaptiveLuminanceHigh;
|
|
289
|
-
var
|
|
290
|
-
var
|
|
638
|
+
var SIZE_SPAN_MIN = DEFAULT_MATERIAL_PROFILE.sizeSpanMin;
|
|
639
|
+
var SIZE_SPAN_MAX = DEFAULT_MATERIAL_PROFILE.sizeSpanMax;
|
|
291
640
|
var LENS_SIZE_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.lensSizeGainMax;
|
|
292
|
-
var
|
|
293
|
-
var
|
|
641
|
+
var SIZE_SCATTER_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.sizeScatterGainMax;
|
|
642
|
+
var SIZE_SCATTER_FLOOR = DEFAULT_MATERIAL_PROFILE.sizeScatterFloor;
|
|
643
|
+
var SIZE_SCATTER_SPAN_MAX = DEFAULT_MATERIAL_PROFILE.sizeScatterSpanMax;
|
|
644
|
+
var SIZE_OCCLUSION_GAIN = DEFAULT_MATERIAL_PROFILE.sizeOcclusionGain;
|
|
645
|
+
var SIZE_SHADOW_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.sizeShadowGainMax;
|
|
646
|
+
var LENS_REFRACTION_GAIN = DEFAULT_MATERIAL_PROFILE.lensRefractionGain;
|
|
647
|
+
var BACKDROP_TONE_MAX = DEFAULT_MATERIAL_PROFILE.backdropToneMax;
|
|
648
|
+
var BACKDROP_TONE_LOW = DEFAULT_MATERIAL_PROFILE.backdropToneLow;
|
|
649
|
+
var BACKDROP_TONE_HIGH = DEFAULT_MATERIAL_PROFILE.backdropToneHigh;
|
|
650
|
+
var BACKDROP_TONE_SIZE_BIAS = DEFAULT_MATERIAL_PROFILE.backdropToneSizeBias;
|
|
651
|
+
DEFAULT_MATERIAL_PROFILE.backdropToneAnchorX;
|
|
652
|
+
DEFAULT_MATERIAL_PROFILE.backdropToneResponseThin;
|
|
653
|
+
DEFAULT_MATERIAL_PROFILE.backdropToneResponseThick;
|
|
654
|
+
DEFAULT_MATERIAL_PROFILE.backdropToneResponseStrength;
|
|
655
|
+
var OUTER_SHADOW = DEFAULT_MATERIAL_PROFILE.outerShadow;
|
|
294
656
|
function withMaterialOverrides(base, patch) {
|
|
295
657
|
const optics = {};
|
|
296
658
|
for (const variant of MATERIAL_VARIANTS) {
|
|
@@ -307,15 +669,31 @@ function withMaterialOverrides(base, patch) {
|
|
|
307
669
|
adaptiveLuminanceLow: patch.adaptiveLuminanceLow ?? base.adaptiveLuminanceLow,
|
|
308
670
|
adaptiveLuminanceHigh: patch.adaptiveLuminanceHigh ?? base.adaptiveLuminanceHigh,
|
|
309
671
|
refractionScale,
|
|
310
|
-
|
|
311
|
-
|
|
672
|
+
sizeSpanMin: patch.sizeSpanMin ?? base.sizeSpanMin,
|
|
673
|
+
sizeSpanMax: patch.sizeSpanMax ?? base.sizeSpanMax,
|
|
312
674
|
lensSizeGainMax: patch.lensSizeGainMax ?? base.lensSizeGainMax,
|
|
313
|
-
|
|
314
|
-
|
|
675
|
+
sizeScatterGainMax: patch.sizeScatterGainMax ?? base.sizeScatterGainMax,
|
|
676
|
+
sizeScatterFloor: patch.sizeScatterFloor ?? base.sizeScatterFloor,
|
|
677
|
+
sizeScatterSpanMax: patch.sizeScatterSpanMax ?? base.sizeScatterSpanMax,
|
|
678
|
+
sizeOcclusionGain: patch.sizeOcclusionGain ?? base.sizeOcclusionGain,
|
|
679
|
+
sizeShadowGainMax: patch.sizeShadowGainMax ?? base.sizeShadowGainMax,
|
|
680
|
+
lensRefractionGain: patch.lensRefractionGain ?? base.lensRefractionGain,
|
|
315
681
|
reducedTransparencyFrost: patch.reducedTransparencyFrost ?? base.reducedTransparencyFrost,
|
|
316
682
|
increasedOcclusionLift: patch.increasedOcclusionLift ?? base.increasedOcclusionLift,
|
|
317
683
|
strongBorderRim: { ...base.strongBorderRim, ...patch.strongBorderRim },
|
|
318
684
|
reducedTintAdaptation: patch.reducedTintAdaptation ?? base.reducedTintAdaptation,
|
|
685
|
+
tintShadeDark: patch.tintShadeDark ?? base.tintShadeDark,
|
|
686
|
+
tintShadeLight: patch.tintShadeLight ?? base.tintShadeLight,
|
|
687
|
+
tintShadeStrength: patch.tintShadeStrength ?? base.tintShadeStrength,
|
|
688
|
+
backdropToneMax: patch.backdropToneMax ?? base.backdropToneMax,
|
|
689
|
+
backdropToneLow: patch.backdropToneLow ?? base.backdropToneLow,
|
|
690
|
+
backdropToneHigh: patch.backdropToneHigh ?? base.backdropToneHigh,
|
|
691
|
+
backdropToneSizeBias: patch.backdropToneSizeBias ?? base.backdropToneSizeBias,
|
|
692
|
+
backdropToneAnchorX: patch.backdropToneAnchorX ?? base.backdropToneAnchorX,
|
|
693
|
+
backdropToneResponseThin: patch.backdropToneResponseThin ?? base.backdropToneResponseThin,
|
|
694
|
+
backdropToneResponseThick: patch.backdropToneResponseThick ?? base.backdropToneResponseThick,
|
|
695
|
+
backdropToneResponseStrength: patch.backdropToneResponseStrength ?? base.backdropToneResponseStrength,
|
|
696
|
+
outerShadow: { ...base.outerShadow, ...patch.outerShadow },
|
|
319
697
|
lightDirection: patch.lightDirection ?? base.lightDirection,
|
|
320
698
|
sweepBandRadians: patch.sweepBandRadians ?? base.sweepBandRadians,
|
|
321
699
|
glowRadiusCss: patch.glowRadiusCss ?? base.glowRadiusCss,
|
|
@@ -350,21 +728,162 @@ function adaptationStrength(policy, analysisExact, profile = DEFAULT_MATERIAL_PR
|
|
|
350
728
|
return 0;
|
|
351
729
|
}
|
|
352
730
|
}
|
|
731
|
+
function tintToneAdaptation(policy, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
732
|
+
switch (policy.ambientTint) {
|
|
733
|
+
case "nominal":
|
|
734
|
+
return 1;
|
|
735
|
+
case "reduced":
|
|
736
|
+
return profile.reducedTintAdaptation;
|
|
737
|
+
case "none":
|
|
738
|
+
return 0;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
function tintShade(materialLuminance, grip, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
742
|
+
const u = Math.min(1, Math.max(0, materialLuminance));
|
|
743
|
+
const shade = Math.min(1, Math.max(0, profile.tintShadeDark + (profile.tintShadeLight - profile.tintShadeDark) * u));
|
|
744
|
+
const k = Math.min(1, Math.max(0, grip));
|
|
745
|
+
return 1 + (shade - 1) * k;
|
|
746
|
+
}
|
|
747
|
+
function tintShadeLayer(seed, materialLuminance, grip, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
748
|
+
const shade = tintShade(materialLuminance, grip, profile);
|
|
749
|
+
return [seed[0] * shade, seed[1] * shade, seed[2] * shade];
|
|
750
|
+
}
|
|
751
|
+
function tintedMaterialColour(material, tint, grip, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
752
|
+
if (tint === void 0 || tint.strength <= 0)
|
|
753
|
+
return material;
|
|
754
|
+
const s = Math.min(1, Math.max(0, tint.strength));
|
|
755
|
+
const layer = tintShadeLayer(tint.color, relativeLuminance(material), grip, profile);
|
|
756
|
+
const clamp01 = (value) => Math.min(1, Math.max(0, value));
|
|
757
|
+
const channel = (index) => {
|
|
758
|
+
const from = linearToSrgbChannel(clamp01(material[index]));
|
|
759
|
+
const to = linearToSrgbChannel(clamp01(layer[index]));
|
|
760
|
+
return srgbToLinearChannel(from + (to - from) * s);
|
|
761
|
+
};
|
|
762
|
+
return [channel(0), channel(1), channel(2)];
|
|
763
|
+
}
|
|
764
|
+
function backdropToneAdaptation(backdropLuminance, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
765
|
+
const x = backdropLuminance + profile.backdropToneSizeBias * Math.min(1, Math.max(0, thickness));
|
|
766
|
+
const span = Math.max(profile.backdropToneHigh - profile.backdropToneLow, 1e-6);
|
|
767
|
+
const t = Math.min(1, Math.max(0, (x - profile.backdropToneLow) / span));
|
|
768
|
+
return Math.min(1, Math.max(0, profile.backdropToneMax)) * (1 - t * t * (3 - 2 * t));
|
|
769
|
+
}
|
|
770
|
+
function backdropToneUnderPolicy(policy, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
771
|
+
return tintToneAdaptation(policy, profile) * profile.refractionScale[accessibilityRefractionCap(policy)];
|
|
772
|
+
}
|
|
773
|
+
function backdropToneSizeBiasUnderPolicy(policy, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
774
|
+
const cap = profile.refractionScale[accessibilityRefractionCap(policy)];
|
|
775
|
+
return cap <= 0 ? 0 : profile.backdropToneSizeBias / cap;
|
|
776
|
+
}
|
|
777
|
+
function adaptedTintColour(neutral, backdrop, adaptation, tintAlpha) {
|
|
778
|
+
const k = Math.min(1, Math.max(0, adaptation));
|
|
779
|
+
if (k === 0)
|
|
780
|
+
return neutral;
|
|
781
|
+
const alpha = adaptedTintAlpha(tintAlpha, k);
|
|
782
|
+
if (alpha <= 0)
|
|
783
|
+
return neutral;
|
|
784
|
+
const wNeutral = (1 - k) * tintAlpha;
|
|
785
|
+
return [
|
|
786
|
+
(neutral[0] * wNeutral + backdrop[0] * k) / alpha,
|
|
787
|
+
(neutral[1] * wNeutral + backdrop[1] * k) / alpha,
|
|
788
|
+
(neutral[2] * wNeutral + backdrop[2] * k) / alpha
|
|
789
|
+
];
|
|
790
|
+
}
|
|
791
|
+
function adaptedTintAlpha(tintAlpha, adaptation) {
|
|
792
|
+
const k = Math.min(1, Math.max(0, adaptation));
|
|
793
|
+
return tintAlpha + k * (1 - tintAlpha);
|
|
794
|
+
}
|
|
353
795
|
var smoothstep = (edge0, edge1, x) => {
|
|
354
796
|
if (edge1 <= edge0)
|
|
355
797
|
return x < edge0 ? 0 : 1;
|
|
356
798
|
const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));
|
|
357
799
|
return t * t * (3 - 2 * t);
|
|
358
800
|
};
|
|
801
|
+
function sizeThickness(spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
802
|
+
return smoothstep(profile.sizeSpanMin, profile.sizeSpanMax, spanPx);
|
|
803
|
+
}
|
|
804
|
+
function sizeThicknessUnderPolicy(spanPx, policy, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
805
|
+
return sizeThickness(spanPx, profile) * profile.refractionScale[accessibilityRefractionCap(policy)];
|
|
806
|
+
}
|
|
359
807
|
function lensSizeGain(spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
360
|
-
return 1 + (profile.lensSizeGainMax - 1) *
|
|
808
|
+
return 1 + (profile.lensSizeGainMax - 1) * sizeThickness(spanPx, profile);
|
|
809
|
+
}
|
|
810
|
+
function lensSizeGainFromThickness(thickness, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
811
|
+
return 1 + (profile.lensSizeGainMax - 1) * thickness;
|
|
812
|
+
}
|
|
813
|
+
function sizeScatterSigma(sigmaPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
814
|
+
return sizeScatterSigmaAt(sigmaPx, scatterThickness(spanPx, 1, profile), profile);
|
|
815
|
+
}
|
|
816
|
+
function scatterThickness(spanPx, fold, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
817
|
+
const floor = Math.min(1, Math.max(0, profile.sizeScatterFloor));
|
|
818
|
+
return floor + (1 - floor) * smoothstep(profile.sizeSpanMin, profile.sizeScatterSpanMax, spanPx) * fold;
|
|
819
|
+
}
|
|
820
|
+
function sizeScatterSigmaAt(sigmaPx, scatter, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
821
|
+
return sigmaPx * (1 + (profile.sizeScatterGainMax - 1) * scatter);
|
|
822
|
+
}
|
|
823
|
+
function sizeOcclusionAlpha(alpha, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
824
|
+
return sizeOcclusionAlphaAt(alpha, sizeThickness(spanPx, profile), profile);
|
|
825
|
+
}
|
|
826
|
+
function sizeOcclusionAlphaAt(alpha, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
827
|
+
return Math.min(1, alpha + profile.sizeOcclusionGain * thickness * (1 - alpha));
|
|
828
|
+
}
|
|
829
|
+
function sizeShadowDepth(shadowDepth, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
830
|
+
return sizeShadowDepthAt(shadowDepth, sizeThickness(spanPx, profile), profile);
|
|
831
|
+
}
|
|
832
|
+
function sizeShadowDepthAt(shadowDepth, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
833
|
+
return shadowDepth * (1 + (profile.sizeShadowGainMax - 1) * thickness);
|
|
834
|
+
}
|
|
835
|
+
function sizeOuterShadowOcclusion(occlusion, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
836
|
+
return sizeOuterShadowOcclusionAt(occlusion, sizeThickness(spanPx, profile), profile);
|
|
837
|
+
}
|
|
838
|
+
function sizeOuterShadowOcclusionAt(occlusion, thickness, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
839
|
+
return Math.min(1, occlusion + profile.outerShadow.sizeGain * thickness * (1 - occlusion));
|
|
840
|
+
}
|
|
841
|
+
function outerShadowUnderPolicy(policy, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
842
|
+
const shadow = profile.outerShadow;
|
|
843
|
+
if (policy.glass === "none" || policy.frost === "none")
|
|
844
|
+
return { ...shadow, occlusion: 0 };
|
|
845
|
+
if (policy.frost === "increased") {
|
|
846
|
+
return { ...shadow, occlusion: shadow.occlusion * shadow.reducedTransparencyOcclusion };
|
|
847
|
+
}
|
|
848
|
+
return shadow;
|
|
849
|
+
}
|
|
850
|
+
var SRGB_ENCODING_EXPONENT = 2.4;
|
|
851
|
+
function outerShadowFalloff(signedDistancePx, sigmaPx) {
|
|
852
|
+
const x = -signedDistancePx / Math.max(sigmaPx, 1e-4);
|
|
853
|
+
return 0.5 * (1 + Math.tanh(0.7978845608028654 * (x + 0.044715 * x * x * x)));
|
|
854
|
+
}
|
|
855
|
+
function outerShadowAlpha(occlusion) {
|
|
856
|
+
const occ = Math.min(1, Math.max(0, occlusion));
|
|
857
|
+
return 1 - Math.pow(1 - occ, 1 / SRGB_ENCODING_EXPONENT);
|
|
858
|
+
}
|
|
859
|
+
function outerShadowReachPx(shadow) {
|
|
860
|
+
const alpha = outerShadowAlpha(shadow.occlusion);
|
|
861
|
+
if (!(alpha > 0))
|
|
862
|
+
return 0;
|
|
863
|
+
const cutoff = 1 / 255 / alpha;
|
|
864
|
+
if (cutoff >= 1)
|
|
865
|
+
return 0;
|
|
866
|
+
const sigma = Math.max(shadow.sigmaPx, 1e-4);
|
|
867
|
+
let lo = -(8 * sigma + Math.abs(shadow.offsetPx) + Math.abs(shadow.spreadPx));
|
|
868
|
+
let hi = 8 * sigma;
|
|
869
|
+
for (let i = 0; i < 48; i += 1) {
|
|
870
|
+
const mid = (lo + hi) / 2;
|
|
871
|
+
if (outerShadowFalloff(mid, sigma) > cutoff)
|
|
872
|
+
lo = mid;
|
|
873
|
+
else
|
|
874
|
+
hi = mid;
|
|
875
|
+
}
|
|
876
|
+
return Math.max(0, hi + Math.abs(shadow.offsetPx) + shadow.spreadPx);
|
|
361
877
|
}
|
|
362
878
|
function lensDepthPx(thicknessPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
363
879
|
const gain = lensSizeGain(spanPx, profile);
|
|
364
880
|
return Math.min(Math.max(thicknessPx, 0) * gain, spanPx * 0.5);
|
|
365
881
|
}
|
|
366
|
-
function
|
|
367
|
-
|
|
882
|
+
function lensDisplacementPx(depthPx, lensDepthPx2, refractionScale, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
883
|
+
if (lensDepthPx2 <= 0)
|
|
884
|
+
return 0;
|
|
885
|
+
const depth = Math.min(Math.max(depthPx / lensDepthPx2, 0), 1);
|
|
886
|
+
return lensDepthPx2 * (1 - depth) * (1 - depth) * refractionScale * profile.lensRefractionGain;
|
|
368
887
|
}
|
|
369
888
|
|
|
370
889
|
// ../renderer-webgpu/dist/analysis.js
|
|
@@ -376,7 +895,11 @@ var ZERO_STATS = {
|
|
|
376
895
|
};
|
|
377
896
|
function statsFromBuffer(values) {
|
|
378
897
|
return {
|
|
379
|
-
|
|
898
|
+
// The reduction publishes the ENCODED-space mean (the W9 model, claims
|
|
899
|
+
// §5.31) and this is its one decode: every consumer downstream — the
|
|
900
|
+
// drivers, `adaptiveTint`'s fitted thresholds — still speaks linear light,
|
|
901
|
+
// and a solid backdrop reads identically under either convention.
|
|
902
|
+
luminance: srgbToLinearChannel(values[0] ?? 0),
|
|
380
903
|
variance: values[1] ?? 0,
|
|
381
904
|
edgeDensity: values[2] ?? 0,
|
|
382
905
|
sampleCount: values[3] ?? 0
|
|
@@ -850,6 +1373,44 @@ function createAppTextureProvider(options) {
|
|
|
850
1373
|
};
|
|
851
1374
|
}
|
|
852
1375
|
|
|
1376
|
+
// ../renderer-webgpu/dist/backdrop-fit.js
|
|
1377
|
+
var IDENTITY_FIT = [1, 1, 0, 0];
|
|
1378
|
+
function isUsablePlacement(placement) {
|
|
1379
|
+
return placement !== void 0 && Number.isFinite(placement.x) && Number.isFinite(placement.y) && placement.width > 0 && placement.height > 0;
|
|
1380
|
+
}
|
|
1381
|
+
function coverFit(sourceWidth, sourceHeight, viewportWidthCss, viewportHeightCss) {
|
|
1382
|
+
const viewportAspect = viewportHeightCss > 0 ? viewportWidthCss / viewportHeightCss : 1;
|
|
1383
|
+
const sourceAspect = sourceHeight > 0 ? sourceWidth / sourceHeight : 1;
|
|
1384
|
+
if (sourceAspect > viewportAspect) {
|
|
1385
|
+
const scaleX = viewportAspect / sourceAspect;
|
|
1386
|
+
return [scaleX, 1, (1 - scaleX) / 2, 0];
|
|
1387
|
+
}
|
|
1388
|
+
const scaleY = sourceAspect / viewportAspect;
|
|
1389
|
+
return [1, scaleY, 0, (1 - scaleY) / 2];
|
|
1390
|
+
}
|
|
1391
|
+
function placementFit(placement, viewportWidthCss, viewportHeightCss) {
|
|
1392
|
+
return [
|
|
1393
|
+
viewportWidthCss / placement.width,
|
|
1394
|
+
viewportHeightCss / placement.height,
|
|
1395
|
+
-placement.x / placement.width,
|
|
1396
|
+
-placement.y / placement.height
|
|
1397
|
+
];
|
|
1398
|
+
}
|
|
1399
|
+
function backdropFit(sourceWidth, sourceHeight, placement, viewportWidthCss, viewportHeightCss) {
|
|
1400
|
+
return isUsablePlacement(placement) ? placementFit(placement, viewportWidthCss, viewportHeightCss) : coverFit(sourceWidth, sourceHeight, viewportWidthCss, viewportHeightCss);
|
|
1401
|
+
}
|
|
1402
|
+
function texelsPerCssPx(sourceWidth, sourceHeight, placement, viewportWidthCss, viewportHeightCss) {
|
|
1403
|
+
if (isUsablePlacement(placement)) {
|
|
1404
|
+
return sourceWidth > 0 ? sourceWidth / placement.width : 1;
|
|
1405
|
+
}
|
|
1406
|
+
return viewportWidthCss > 0 && viewportHeightCss > 0 ? Math.max(sourceWidth / viewportWidthCss, sourceHeight / viewportHeightCss) : 1;
|
|
1407
|
+
}
|
|
1408
|
+
function samePlacement(a, b) {
|
|
1409
|
+
if (a === void 0 || b === void 0)
|
|
1410
|
+
return a === b;
|
|
1411
|
+
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
853
1414
|
// ../renderer-webgpu/dist/device.js
|
|
854
1415
|
function createDeviceHost(options = {}) {
|
|
855
1416
|
let status = {
|
|
@@ -1252,7 +1813,7 @@ var IDLE_CHANNELS = {
|
|
|
1252
1813
|
};
|
|
1253
1814
|
|
|
1254
1815
|
// ../renderer-webgpu/dist/instances.js
|
|
1255
|
-
var INSTANCE_FLOATS =
|
|
1816
|
+
var INSTANCE_FLOATS = 18;
|
|
1256
1817
|
var INSTANCE_BYTES = INSTANCE_FLOATS * 4;
|
|
1257
1818
|
var channelsOf = (input) => ({
|
|
1258
1819
|
...IDLE_CHANNELS,
|
|
@@ -1264,7 +1825,7 @@ function compressedChannels(shape, press) {
|
|
|
1264
1825
|
const scale = 1 - Math.min(1, Math.max(0, press)) * DEFAULT_MOTION_PROFILE.pressCompressionScale;
|
|
1265
1826
|
return { ...shape, size: [shape.size[0] * scale, shape.size[1] * scale] };
|
|
1266
1827
|
}
|
|
1267
|
-
function resolveSurfaces(group, family, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
1828
|
+
function resolveSurfaces(group, family, profile = DEFAULT_MATERIAL_PROFILE, policy = NOMINAL_MATERIAL_POLICY) {
|
|
1268
1829
|
const byId = /* @__PURE__ */ new Map();
|
|
1269
1830
|
for (const surface of group.surfaces) {
|
|
1270
1831
|
if (byId.has(surface.nodeId)) {
|
|
@@ -1308,6 +1869,7 @@ function resolveSurfaces(group, family, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
|
1308
1869
|
const fieldSource = surface.concentricOf === void 0 ? shape : resolveShapeOf(byId.get(surface.concentricOf.nodeId));
|
|
1309
1870
|
const inset = surface.concentricOf?.inset ?? 0;
|
|
1310
1871
|
const spanPx = Math.min(shape.channels.size[0], shape.channels.size[1]);
|
|
1872
|
+
const thickness = sizeThicknessUnderPolicy(spanPx, policy, profile);
|
|
1311
1873
|
return {
|
|
1312
1874
|
nodeId: surface.nodeId,
|
|
1313
1875
|
shape,
|
|
@@ -1316,11 +1878,13 @@ function resolveSurfaces(group, family, profile = DEFAULT_MATERIAL_PROFILE) {
|
|
|
1316
1878
|
centre: [fieldSource.channels.center[0], fieldSource.channels.center[1]],
|
|
1317
1879
|
channels,
|
|
1318
1880
|
spanPx,
|
|
1319
|
-
lensDepthPx:
|
|
1881
|
+
lensDepthPx: Math.min(Math.max(shape.channels.thickness, 0) * lensSizeGainFromThickness(thickness, profile), spanPx * 0.5),
|
|
1882
|
+
sizeThickness: thickness,
|
|
1883
|
+
tintStrength: Math.min(1, Math.max(0, surface.tint?.strength ?? 0))
|
|
1320
1884
|
};
|
|
1321
1885
|
});
|
|
1322
1886
|
}
|
|
1323
|
-
function groupFieldRect(surfaces, union = DEFAULT_GROUP_UNION, rimWidthPx = 2) {
|
|
1887
|
+
function groupFieldRect(surfaces, union = DEFAULT_GROUP_UNION, rimWidthPx = 2, outerShadowReachPx2 = 0) {
|
|
1324
1888
|
if (surfaces.length === 0)
|
|
1325
1889
|
return { x: 0, y: 0, width: 0, height: 0 };
|
|
1326
1890
|
let minX = Number.POSITIVE_INFINITY;
|
|
@@ -1335,7 +1899,7 @@ function groupFieldRect(surfaces, union = DEFAULT_GROUP_UNION, rimWidthPx = 2) {
|
|
|
1335
1899
|
maxX = Math.max(maxX, cx + w / 2);
|
|
1336
1900
|
maxY = Math.max(maxY, cy + h / 2);
|
|
1337
1901
|
}
|
|
1338
|
-
const pad = rimWidthPx + union.maxBulge + 1;
|
|
1902
|
+
const pad = Math.max(rimWidthPx + union.maxBulge + 1, Math.max(outerShadowReachPx2, 0));
|
|
1339
1903
|
return {
|
|
1340
1904
|
x: minX - pad,
|
|
1341
1905
|
y: minY - pad,
|
|
@@ -1380,7 +1944,9 @@ function packInstances(surfaces, origin, into) {
|
|
|
1380
1944
|
data[o + 12] = s.channels.press;
|
|
1381
1945
|
data[o + 13] = s.channels.glow;
|
|
1382
1946
|
data[o + 14] = s.lensDepthPx * Math.min(1, Math.max(0, s.channels.lensStrength));
|
|
1383
|
-
data[o + 15] =
|
|
1947
|
+
data[o + 15] = s.tintStrength;
|
|
1948
|
+
data[o + 16] = s.spanPx;
|
|
1949
|
+
data[o + 17] = 0;
|
|
1384
1950
|
}
|
|
1385
1951
|
return { data, count: surfaces.length };
|
|
1386
1952
|
}
|
|
@@ -1656,7 +2222,9 @@ var WGSL_INSTANCE_STRUCT = `struct Instance {
|
|
|
1656
2222
|
press : f32, // 48 interaction channel value, 0..1
|
|
1657
2223
|
glow : f32, // 52 interaction channel value, 0..1
|
|
1658
2224
|
lensDepth : f32, // 56 CPU-resolved lens depth in CSS px (material.ts), already scaled by the lensStrength channel
|
|
1659
|
-
|
|
2225
|
+
tintK : f32, // 60 author tint strength, 0..1 \u2014 the seed itself is a group uniform
|
|
2226
|
+
span : f32, // 64 the surface's shorter extent, CSS px \u2014 the size law's input, both curves (W11c)
|
|
2227
|
+
_pad : f32, // 68 stride padding: vec2f aligns the struct to 8, so 72 is the next legal size
|
|
1660
2228
|
};`;
|
|
1661
2229
|
var WGSL_FIELD_SAMPLE = `struct FieldSample {
|
|
1662
2230
|
d : f32,
|
|
@@ -1821,7 +2389,40 @@ var WGSL_FIELD_PASS = `struct FieldUniforms {
|
|
|
1821
2389
|
// field but its members are not one size, and carrying them per pixel is what
|
|
1822
2390
|
// lets a 40 px button and a 320 px platter share a field pass and still lens by
|
|
1823
2391
|
// their own depth \u2014 parent acceptance #2 inside a GlassEffectContainer.
|
|
1824
|
-
// aux = (lensDepthPx, glow,
|
|
2392
|
+
// aux = (lensDepthPx, glow, spanPx, tintStrength)
|
|
2393
|
+
//
|
|
2394
|
+
// The third and fourth slots both changed hands, and neither of the values they
|
|
2395
|
+
// used to carry was ever read by a fragment stage. 'thick' left because
|
|
2396
|
+
// 'lensDepth' is the thickness times the size gain, so the raw number told the
|
|
2397
|
+
// shader nothing the depth had not already said; 'press' left because press
|
|
2398
|
+
// compression is resolved on the CPU, as a transform on the surface's own size
|
|
2399
|
+
// (instances.ts). Both are still packed into the instance buffer, unchanged, for
|
|
2400
|
+
// a future consumer that wants them per pixel; they simply no longer ride here.
|
|
2401
|
+
//
|
|
2402
|
+
// What replaced them are the two quantities that genuinely have to be per pixel,
|
|
2403
|
+
// because a group is one field pass and one optics pass while its members are
|
|
2404
|
+
// not one thing:
|
|
2405
|
+
//
|
|
2406
|
+
// 'spanPx' \u2014 the size law's input (W2): the surface's shorter extent in
|
|
2407
|
+
// CSS px. A 40 px button and a 320 px platter in one container
|
|
2408
|
+
// lens, scatter, occlude and shadow by their own size. Until
|
|
2409
|
+
// W11c this slot carried the folded thickness factor itself;
|
|
2410
|
+
// the scatter facet now rides a curve with a different band
|
|
2411
|
+
// top, so the span travels and each consumer evaluates its own
|
|
2412
|
+
// curve from it with the bands and the fold as uniforms.
|
|
2413
|
+
// 'tintStrength' \u2014 the author tint's strength (W3). One emphasised control
|
|
2414
|
+
// among plain ones is the composition Apple's guidance
|
|
2415
|
+
// describes; the seed itself is a group uniform.
|
|
2416
|
+
//
|
|
2417
|
+
// They compose rather than compete: the size law moves what the material does to
|
|
2418
|
+
// the backdrop, the tint moves what colour it does it in.
|
|
2419
|
+
//
|
|
2420
|
+
// One consequence of carrying the span rather than the thickness, stated so it
|
|
2421
|
+
// is not mistaken for a drift: the union below blends 'aux' linearly between two
|
|
2422
|
+
// members, so a pixel in the neck between a button and a platter now blends
|
|
2423
|
+
// their SPANS and evaluates the curves on the blend, where it used to blend the
|
|
2424
|
+
// evaluated thicknesses. Inside either member the two are identical; only the
|
|
2425
|
+
// smooth-min neck differs, and nothing measured claims a value there.
|
|
1825
2426
|
|
|
1826
2427
|
@group(0) @binding(0) var<uniform> fu : FieldUniforms;
|
|
1827
2428
|
@group(0) @binding(1) var<storage, read> instances : array<Instance>;
|
|
@@ -1842,7 +2443,7 @@ fn eval_instance(i : u32, p : vec2f) -> Member {
|
|
|
1842
2443
|
var m : Member;
|
|
1843
2444
|
m.d = f.d + s.inset;
|
|
1844
2445
|
m.g = f.g;
|
|
1845
|
-
m.aux = vec4f(s.lensDepth, s.glow, s.
|
|
2446
|
+
m.aux = vec4f(s.lensDepth, s.glow, s.span, s.tintK);
|
|
1846
2447
|
return m;
|
|
1847
2448
|
}
|
|
1848
2449
|
|
|
@@ -1925,6 +2526,24 @@ var WGSL_HIGHLIGHT_PASS = `struct HighlightUniforms {
|
|
|
1925
2526
|
colour : vec4f,
|
|
1926
2527
|
/// fieldSize.xy, fieldUpsampled (z), unused (w)
|
|
1927
2528
|
flags : vec4f,
|
|
2529
|
+
/// backdrop tone adaptation (W7), exactly as the optics pass takes it:
|
|
2530
|
+
/// backdropToneLow, backdropToneHigh, the size bias already divided by the
|
|
2531
|
+
/// accessibility refraction cap, and the resolved strength \u2014 zero where there
|
|
2532
|
+
/// is no measured backdrop tone, which stands the fade down
|
|
2533
|
+
toneAdapt : vec4f,
|
|
2534
|
+
/// the backdrop source's own average luminance (x), and the size law's band
|
|
2535
|
+
/// and fold \u2014 sizeSpanMin (y), sizeSpanMax (z), the accessibility fold (w) \u2014
|
|
2536
|
+
/// so this pass evaluates the thickness curve off the per-pixel span exactly
|
|
2537
|
+
/// as the optics pass does (W11c: the span rides the field, not the factor)
|
|
2538
|
+
toneLevel : vec4f,
|
|
2539
|
+
/// this pass's uv into the FIELD texture's uv: scale (xy), offset (zw).
|
|
2540
|
+
///
|
|
2541
|
+
/// The identity for a pass whose scissor is the field's own rect, and not the
|
|
2542
|
+
/// identity since W8: the outer shadow made the field rect much larger than
|
|
2543
|
+
/// anything this pass draws in, so the highlight is scoped back to the rect it
|
|
2544
|
+
/// had before and reads the field through this remap instead. Derived from the
|
|
2545
|
+
/// two rects by highlightPass in passes.ts.
|
|
2546
|
+
fieldFit : vec4f,
|
|
1928
2547
|
};
|
|
1929
2548
|
|
|
1930
2549
|
@group(0) @binding(0) var<uniform> hu : HighlightUniforms;
|
|
@@ -1946,13 +2565,14 @@ fn fs_highlight(in : FullscreenOut) -> @location(0) vec4f {
|
|
|
1946
2565
|
// Exact load nominally; filtered when the governor's resolution knob had the
|
|
1947
2566
|
// field rasterised below the group's rect. The sweep rides the rim, which is
|
|
1948
2567
|
// the one place a nearest read of a coarse field would show its grid.
|
|
2568
|
+
let fieldUv = in.uv * hu.fieldFit.xy + hu.fieldFit.zw;
|
|
1949
2569
|
var field : vec4f;
|
|
1950
2570
|
var aux : vec4f;
|
|
1951
2571
|
if (hu.flags.z > 0.5) {
|
|
1952
|
-
field = textureSampleLevel(fieldTexture, fieldSampler,
|
|
1953
|
-
aux = textureSampleLevel(auxTexture, fieldSampler,
|
|
2572
|
+
field = textureSampleLevel(fieldTexture, fieldSampler, fieldUv, 0.0);
|
|
2573
|
+
aux = textureSampleLevel(auxTexture, fieldSampler, fieldUv, 0.0);
|
|
1954
2574
|
} else {
|
|
1955
|
-
let texel = vec2i(
|
|
2575
|
+
let texel = vec2i(fieldUv * hu.flags.xy);
|
|
1956
2576
|
field = textureLoad(fieldTexture, texel, 0);
|
|
1957
2577
|
aux = textureLoad(auxTexture, texel, 0);
|
|
1958
2578
|
}
|
|
@@ -1981,7 +2601,39 @@ fn fs_highlight(in : FullscreenOut) -> @location(0) vec4f {
|
|
|
1981
2601
|
// whose members glow independently does not need a pass each.
|
|
1982
2602
|
let press = radial * radial * hu.glow.w * aux.y;
|
|
1983
2603
|
|
|
1984
|
-
|
|
2604
|
+
/*
|
|
2605
|
+
* Backdrop tone adaptation (W7) fades this pass with the rest of the surface's
|
|
2606
|
+
* own appearance. A highlight is the material catching light, and a material
|
|
2607
|
+
* that has taken its backdrop's tone is not there to catch any \u2014 the settled
|
|
2608
|
+
* reference's capsule over a near-black backdrop is byte-identical to that
|
|
2609
|
+
* background, sweep included.
|
|
2610
|
+
*
|
|
2611
|
+
* The curve is the optics pass's, evaluated the same way off the same uniforms
|
|
2612
|
+
* and the same per-pixel 'sizeK', because two statements of one curve is how
|
|
2613
|
+
* they drift. This pass would otherwise have gone on drawing a bright crescent
|
|
2614
|
+
* around a surface the optics pass had made invisible \u2014 measured at 60 pixels
|
|
2615
|
+
* past 2/255, peaking at 97/255, on exactly that cell.
|
|
2616
|
+
*/
|
|
2617
|
+
// The thickness curve off the per-pixel span, written out as the optics pass
|
|
2618
|
+
// writes it, so the two passes fade on one curve and not on two copies.
|
|
2619
|
+
let spanT = clamp(
|
|
2620
|
+
(aux.z - hu.toneLevel.y) / max(hu.toneLevel.z - hu.toneLevel.y, 1e-6),
|
|
2621
|
+
0.0,
|
|
2622
|
+
1.0,
|
|
2623
|
+
);
|
|
2624
|
+
let sizeK = clamp(spanT * spanT * (3.0 - 2.0 * spanT) * hu.toneLevel.w, 0.0, 1.0);
|
|
2625
|
+
var toneAdapt = 0.0;
|
|
2626
|
+
if (hu.toneAdapt.w > 0.0) {
|
|
2627
|
+
let toneX = hu.toneLevel.x + hu.toneAdapt.z * sizeK;
|
|
2628
|
+
let toneT = clamp(
|
|
2629
|
+
(toneX - hu.toneAdapt.x) / max(hu.toneAdapt.y - hu.toneAdapt.x, 1e-6),
|
|
2630
|
+
0.0,
|
|
2631
|
+
1.0,
|
|
2632
|
+
);
|
|
2633
|
+
toneAdapt = clamp(hu.toneAdapt.w, 0.0, 1.0) * (1.0 - toneT * toneT * (3.0 - 2.0 * toneT));
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
let intensity = clamp(sweep + press, 0.0, 1.0) * coverage * (1.0 - toneAdapt);
|
|
1985
2637
|
if (intensity <= 0.0) {
|
|
1986
2638
|
return vec4f(0.0);
|
|
1987
2639
|
}
|
|
@@ -1994,18 +2646,63 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
|
|
|
1994
2646
|
screen : vec4f,
|
|
1995
2647
|
/// backdrop uv transform on viewport-normalised coords: scale (xy), offset (zw)
|
|
1996
2648
|
fit : vec4f,
|
|
1997
|
-
/// refractionScale,
|
|
2649
|
+
/// refractionScale, lensRefractionGain (W11c G2), unused, chainMaxLod
|
|
1998
2650
|
lens : vec4f,
|
|
1999
2651
|
/// fixed tint colour, linear light (xyz), tint alpha (w)
|
|
2000
2652
|
tint : vec4f,
|
|
2001
2653
|
/// adapted tint colour, linear light (xyz), adaptation strength (w)
|
|
2002
2654
|
adapt : vec4f,
|
|
2655
|
+
/// author tint seed, linear light (xyz), tone adaptation under the contrast regime (w)
|
|
2656
|
+
seed : vec4f,
|
|
2657
|
+
/// the author tint's shade law (W10): tintShadeDark, tintShadeLight,
|
|
2658
|
+
/// tintShadeStrength (the profile's provenance gate); and (w) the size law's
|
|
2659
|
+
/// accessibility fold \u2014 the refraction ladder read at the preference's cap,
|
|
2660
|
+
/// which every facet's span-dependent rise is multiplied by (W11c)
|
|
2661
|
+
tone : vec4f,
|
|
2003
2662
|
/// rimWidthPx, rimAlpha, specularPower, specularGain
|
|
2004
2663
|
rim : vec4f,
|
|
2005
2664
|
/// light direction, unit (xy), shadowDepth (z), shadowAlpha (w)
|
|
2006
2665
|
light : vec4f,
|
|
2007
2666
|
/// hasBackdrop, fieldSize.xy, fieldUpsampled
|
|
2008
2667
|
flags : vec4f,
|
|
2668
|
+
/// the size law's gains (W2): scatterGainMax, occlusionGain, shadowGainMax,
|
|
2669
|
+
/// bodyChainLod \u2014 the last being the chain level whose blur already matches the
|
|
2670
|
+
/// body texture, which is what the scattering term measures its octaves from
|
|
2671
|
+
size : vec4f,
|
|
2672
|
+
/// backdrop tone adaptation (W7): backdropToneLow, backdropToneHigh, the size
|
|
2673
|
+
/// bias ALREADY divided by the accessibility refraction cap (so that it may be
|
|
2674
|
+
/// multiplied by the policy-folded 'sizeK' below and still mean the geometric
|
|
2675
|
+
/// thickness), and the strength the accessibility policy resolved \u2014 zero where
|
|
2676
|
+
/// the host could not measure a backdrop tone, which stands the axis down
|
|
2677
|
+
toneAdapt : vec4f,
|
|
2678
|
+
/// the backdrop source's own average colour, linear (xyz), and its luminance (w)
|
|
2679
|
+
toneColour : vec4f,
|
|
2680
|
+
/// the outer shadow (W8): its compositing-space alpha before the size law (x),
|
|
2681
|
+
/// the blur's sigma in group-local CSS px (y), the silhouette's outward spread
|
|
2682
|
+
/// in the same units (z), and the downward offset expressed in field-texture UV
|
|
2683
|
+
/// (w) \u2014 a UV so the offset silhouette is one texture read rather than a
|
|
2684
|
+
/// gradient extrapolation, which is exact only on a straight edge and a capsule
|
|
2685
|
+
/// is mostly not one
|
|
2686
|
+
shadow : vec4f,
|
|
2687
|
+
/// the outer shadow's size-law gain (x), read against the casting surface's own
|
|
2688
|
+
/// thickness, and the field rect's height in CSS px (y) \u2014 the conversion the
|
|
2689
|
+
/// shadow's shift needs when it lands outside the texture; zw are the padding
|
|
2690
|
+
/// a uniform's vec4 alignment requires
|
|
2691
|
+
shadowSize : vec4f,
|
|
2692
|
+
/// the backdrop tone response's anchors (W9): the three solid anchors'
|
|
2693
|
+
/// ENCODED-space means (xyz), and the backdrop's linear-space mean (w) \u2014 the
|
|
2694
|
+
/// quantity the response solve composites against
|
|
2695
|
+
toneAnchor : vec4f,
|
|
2696
|
+
/// the response law's thin row: the reference's settled interior levels at the
|
|
2697
|
+
/// three anchors for a surface of sizeThickness 0 (xyz); w is the law's
|
|
2698
|
+
/// per-profile authority \u2014 0 on dark profiles, whose response is unmeasured
|
|
2699
|
+
toneRowThin : vec4f,
|
|
2700
|
+
/// the thick row (sizeThickness saturated), same layout
|
|
2701
|
+
toneRowThick : vec4f,
|
|
2702
|
+
/// the size law's bands (W11c): the scatter facet's floor (x) and its own
|
|
2703
|
+
/// band top (y), and the thickness curve's sizeSpanMin (z) and sizeSpanMax
|
|
2704
|
+
/// (w) \u2014 both curves start at z; the fold they multiply by is tone.w
|
|
2705
|
+
scatter : vec4f,
|
|
2009
2706
|
};
|
|
2010
2707
|
|
|
2011
2708
|
@group(0) @binding(0) var<uniform> ou : OpticsUniforms;
|
|
@@ -2016,6 +2713,42 @@ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
|
|
|
2016
2713
|
@group(0) @binding(5) var backdropBody : texture_2d<f32>;
|
|
2017
2714
|
@group(0) @binding(6) var fieldSampler : sampler;
|
|
2018
2715
|
|
|
2716
|
+
/// One encoded sRGB channel from a linear one \u2014 the space the backdrop tone
|
|
2717
|
+
/// response's anchors live in (W9). Mirrors material.ts's 'linearToSrgbChannel'.
|
|
2718
|
+
fn srgb_encode(c : f32) -> f32 {
|
|
2719
|
+
let x = clamp(c, 0.0, 1.0);
|
|
2720
|
+
return select(1.055 * pow(x, 1.0 / 2.4) - 0.055, x * 12.92, x <= 0.0031308);
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
/// The backdrop tone response R(encodedInput, sizeK) (W9): monotone
|
|
2724
|
+
/// (Fritsch\u2013Carlson) interpolation through the three anchors, clamped to their
|
|
2725
|
+
/// span; smoothstep between the thin and thick rows. Mirrors material.ts's
|
|
2726
|
+
/// 'backdropToneResponse' term for term \u2014 the constants are authored there.
|
|
2727
|
+
fn tone_response(x : f32, sizeK : f32) -> f32 {
|
|
2728
|
+
let f = sizeK * sizeK * (3.0 - 2.0 * sizeK);
|
|
2729
|
+
let ys = mix(ou.toneRowThin.xyz, ou.toneRowThick.xyz, vec3f(f));
|
|
2730
|
+
let xs = ou.toneAnchor.xyz;
|
|
2731
|
+
let xc = clamp(x, xs.x, xs.z);
|
|
2732
|
+
let h0 = max(xs.y - xs.x, 1e-4);
|
|
2733
|
+
let h1 = max(xs.z - xs.y, 1e-4);
|
|
2734
|
+
let d0 = (ys.y - ys.x) / h0;
|
|
2735
|
+
let d1 = (ys.z - ys.y) / h1;
|
|
2736
|
+
// The interior slope: the harmonic mean where the secants agree, 0 across a
|
|
2737
|
+
// sign change \u2014 what keeps the curve monotone between monotone anchors.
|
|
2738
|
+
var m1 = 0.0;
|
|
2739
|
+
if (d0 * d1 > 0.0) { m1 = 2.0 * d0 * d1 / (d0 + d1); }
|
|
2740
|
+
var h = h0; var t = (xc - xs.x) / h0;
|
|
2741
|
+
var y0 = ys.x; var y1 = ys.y; var s0 = d0; var s1 = m1;
|
|
2742
|
+
if (xc > xs.y) {
|
|
2743
|
+
h = h1; t = (xc - xs.y) / h1;
|
|
2744
|
+
y0 = ys.y; y1 = ys.z; s0 = m1; s1 = d1;
|
|
2745
|
+
}
|
|
2746
|
+
return y0 * (1.0 + 2.0 * t) * (1.0 - t) * (1.0 - t)
|
|
2747
|
+
+ s0 * h * t * (1.0 - t) * (1.0 - t)
|
|
2748
|
+
+ y1 * t * t * (3.0 - 2.0 * t)
|
|
2749
|
+
+ s1 * h * t * t * (t - 1.0);
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2019
2752
|
/// Rim proximity: 1 exactly on the contour, falling to 0 by 'width' on either
|
|
2020
2753
|
/// side. Symmetric, so the rim is a band on the boundary rather than a plateau
|
|
2021
2754
|
/// that keeps burning outward where coverage has already faded.
|
|
@@ -2024,6 +2757,87 @@ fn rim_weight(d : f32, width : f32) -> f32 {
|
|
|
2024
2757
|
return t * t;
|
|
2025
2758
|
}
|
|
2026
2759
|
|
|
2760
|
+
/// The Gaussian CDF the outer shadow's edge falls off by: 1 deep inside the
|
|
2761
|
+
/// shadow's silhouette, 0.5 exactly on it, 0 far outside. The tanh form of the
|
|
2762
|
+
/// normal CDF \u2014 WGSL has no erf, and this is within 1.8e-4 of it everywhere,
|
|
2763
|
+
/// which is 0.015 of one 8-bit code at the shipped occlusion. Mirrors
|
|
2764
|
+
/// material.ts's 'outerShadowFalloff' term for term.
|
|
2765
|
+
fn outer_shadow_falloff(signedDistance : f32, sigma : f32) -> f32 {
|
|
2766
|
+
let x = -signedDistance / max(sigma, 1e-4);
|
|
2767
|
+
return 0.5 * (1.0 + tanh(0.7978845608028654 * (x + 0.044715 * x * x * x)));
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
/// The outer shadow's alpha at this pixel (W8).
|
|
2771
|
+
///
|
|
2772
|
+
/// The shadow is the group's OWN field, translated down by 'shadow.w' in field
|
|
2773
|
+
/// UV and outset by 'shadow.z', then blurred. Reading the offset silhouette from
|
|
2774
|
+
/// the field texture rather than extrapolating the local distance along the
|
|
2775
|
+
/// normal is what keeps a corner a corner: the first-order estimate is exact only
|
|
2776
|
+
/// on a straight edge, and a capsule is mostly not one.
|
|
2777
|
+
///
|
|
2778
|
+
/// The value is an ALPHA on pure black, so what it composites to is the backdrop
|
|
2779
|
+
/// times '1 - alpha' \u2014 multiplicative occlusion, and exactly zero over black,
|
|
2780
|
+
/// with no branch for it.
|
|
2781
|
+
fn outer_shadow_alpha(uv : vec2f, upsampled : f32, fieldSize : vec2f) -> f32 {
|
|
2782
|
+
if (ou.shadow.x <= 0.0) {
|
|
2783
|
+
return 0.0;
|
|
2784
|
+
}
|
|
2785
|
+
/*
|
|
2786
|
+
* The shift can leave the field texture, and clamping alone reads a lie there.
|
|
2787
|
+
*
|
|
2788
|
+
* Two ways it happens, and the second is not an edge case: the rect is clipped
|
|
2789
|
+
* to the canvas, so a surface within the shadow's reach of the viewport's TOP
|
|
2790
|
+
* has no rows above it to shift into; and even unclipped, the rect's pad is the
|
|
2791
|
+
* shadow's reach, so the topmost band of every group needs rows a further
|
|
2792
|
+
* 'offset' above that. Clamped, both repeat the edge texel \u2014 a distance that is
|
|
2793
|
+
* too SMALL, which reads as a flat, too-dark falloff exactly where the shadow
|
|
2794
|
+
* should be fading out, and diverges from the CSS tier, which has no texture to
|
|
2795
|
+
* run out of.
|
|
2796
|
+
*
|
|
2797
|
+
* Reconstructed instead of clamped. A signed distance field is 1-Lipschitz, and
|
|
2798
|
+
* in the region this happens in \u2014 directly above the surface \u2014 moving away from
|
|
2799
|
+
* it increases the distance by exactly the displacement. So adding back the
|
|
2800
|
+
* distance that was clamped off is exact there, and past the surface's corners
|
|
2801
|
+
* it over-estimates, which errs toward LESS shadow rather than more.
|
|
2802
|
+
*/
|
|
2803
|
+
let shiftedY = uv.y - ou.shadow.w;
|
|
2804
|
+
let clampedOffCss =
|
|
2805
|
+
(max(0.0, -shiftedY) + max(0.0, shiftedY - 1.0)) * ou.shadowSize.y;
|
|
2806
|
+
let shadowUv = clamp(vec2f(uv.x, shiftedY), vec2f(0.0), vec2f(1.0));
|
|
2807
|
+
var shadowField : vec4f;
|
|
2808
|
+
var shadowAux : vec4f;
|
|
2809
|
+
if (upsampled > 0.5) {
|
|
2810
|
+
shadowField = textureSampleLevel(fieldTexture, fieldSampler, shadowUv, 0.0);
|
|
2811
|
+
shadowAux = textureSampleLevel(auxTexture, fieldSampler, shadowUv, 0.0);
|
|
2812
|
+
} else {
|
|
2813
|
+
let texel = clamp(vec2i(shadowUv * fieldSize), vec2i(0), vec2i(fieldSize) - vec2i(1));
|
|
2814
|
+
shadowField = textureLoad(fieldTexture, texel, 0);
|
|
2815
|
+
shadowAux = textureLoad(auxTexture, texel, 0);
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// The size law reaches the amplitude and nothing else \u2014 the reference's three
|
|
2819
|
+
// lengths are span-invariant across 32\u2026160 px. The gain rides the thickness of
|
|
2820
|
+
// the surface that CAST the shadow, which is the one read at the offset
|
|
2821
|
+
// position rather than the one under the pixel being shaded.
|
|
2822
|
+
//
|
|
2823
|
+
// Applied on '1 - alpha' rather than on the alpha: the gain is a fraction of
|
|
2824
|
+
// the remaining transparency in LINEAR light ('sizeOuterShadowOcclusionAt'),
|
|
2825
|
+
// and (1 - occ) is exactly what raising to 1/2.4 turns into (1 - alpha), so the
|
|
2826
|
+
// two forms are the same law and neither needs the other's space.
|
|
2827
|
+
// The thickness curve off the casting surface's span (W11c: the span rides
|
|
2828
|
+
// the field), written as the body's own is below.
|
|
2829
|
+
let castT = clamp(
|
|
2830
|
+
(shadowAux.z - ou.scatter.z) / max(ou.scatter.w - ou.scatter.z, 1e-6),
|
|
2831
|
+
0.0,
|
|
2832
|
+
1.0,
|
|
2833
|
+
);
|
|
2834
|
+
let sizeK = clamp(castT * castT * (3.0 - 2.0 * castT) * clamp(ou.tone.w, 0.0, 1.0), 0.0, 1.0);
|
|
2835
|
+
let sizeFold = pow(max(1.0 - ou.shadowSize.x * sizeK, 0.0), 1.0 / 2.4);
|
|
2836
|
+
let alpha = 1.0 - (1.0 - ou.shadow.x) * sizeFold;
|
|
2837
|
+
|
|
2838
|
+
return alpha * outer_shadow_falloff(shadowField.x + clampedOffCss - ou.shadow.z, ou.shadow.y);
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2027
2841
|
@fragment
|
|
2028
2842
|
fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
|
|
2029
2843
|
// Nominally the field is one texel per device pixel, so the read is an exact
|
|
@@ -2045,8 +2859,19 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
|
|
|
2045
2859
|
let d = field.x;
|
|
2046
2860
|
let normal = field.yz;
|
|
2047
2861
|
let coverage = field.w;
|
|
2862
|
+
|
|
2863
|
+
/*
|
|
2864
|
+
* The outer shadow (W8) sits UNDER the material, so it is resolved before the
|
|
2865
|
+
* body and carried into the output's alpha rather than into its colour: the
|
|
2866
|
+
* pass writes premultiplied black there, and premultiplied black over the page
|
|
2867
|
+
* IS the multiplication. Outside the contour that is the whole of what this
|
|
2868
|
+
* pixel is; inside it the surface covers the shadow, exactly as a 'box-shadow'
|
|
2869
|
+
* is clipped out of its own border box, and the two meet across the coverage
|
|
2870
|
+
* ramp with no seam because it is one expression.
|
|
2871
|
+
*/
|
|
2872
|
+
let shadowAlpha = outer_shadow_alpha(in.uv, ou.flags.w, ou.flags.yz);
|
|
2048
2873
|
if (coverage <= 0.0) {
|
|
2049
|
-
return vec4f(0.0);
|
|
2874
|
+
return vec4f(0.0, 0.0, 0.0, shadowAlpha);
|
|
2050
2875
|
}
|
|
2051
2876
|
|
|
2052
2877
|
let viewport01 = in.position.xy / ou.screen.xy;
|
|
@@ -2054,51 +2879,326 @@ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
|
|
|
2054
2879
|
|
|
2055
2880
|
// Per-pixel, unioned through the field pass. See the module note.
|
|
2056
2881
|
let lensDepth = max(aux.x, 1e-4);
|
|
2882
|
+
// The size law's two curves off the span of whichever surface owns this pixel
|
|
2883
|
+
// (W11c). 'sizeK' is the thickness factor, 0..1: zero on anything at or below
|
|
2884
|
+
// the profile's 'sizeSpanMin', saturated at 'sizeSpanMax', folded under the
|
|
2885
|
+
// preference \u2014 the occlusion, the inner shadow and the tone response multiply
|
|
2886
|
+
// by it, so a small control takes the pre-law path exactly. 'kScatter' is the
|
|
2887
|
+
// scatter facet's own mix, floor + (1 - floor) \xB7 smoothstep to
|
|
2888
|
+
// 'sizeScatterSpanMax' \xB7 fold: the floor is the material's frost at any size
|
|
2889
|
+
// and is not folded, the rise is depth and is. Both written out as
|
|
2890
|
+
// smoothsteps with a guarded denominator, so a profile that collapses a band
|
|
2891
|
+
// degrades to a step rather than to NaN.
|
|
2892
|
+
let span = aux.z;
|
|
2893
|
+
let fold = clamp(ou.tone.w, 0.0, 1.0);
|
|
2894
|
+
let thickT = clamp((span - ou.scatter.z) / max(ou.scatter.w - ou.scatter.z, 1e-6), 0.0, 1.0);
|
|
2895
|
+
let sizeK = clamp(thickT * thickT * (3.0 - 2.0 * thickT) * fold, 0.0, 1.0);
|
|
2896
|
+
let scatterFloor = clamp(ou.scatter.x, 0.0, 1.0);
|
|
2897
|
+
let scatterT = clamp((span - ou.scatter.z) / max(ou.scatter.y - ou.scatter.z, 1e-6), 0.0, 1.0);
|
|
2898
|
+
let kScatter = clamp(
|
|
2899
|
+
scatterFloor + (1.0 - scatterFloor) * scatterT * scatterT * (3.0 - 2.0 * scatterT) * fold,
|
|
2900
|
+
0.0,
|
|
2901
|
+
1.0,
|
|
2902
|
+
);
|
|
2057
2903
|
// '-d' is depth inside the surface, so the profile runs 1 at the contour to 0
|
|
2058
2904
|
// at 'lensDepth' inward, and the square makes the falloff read as curvature
|
|
2059
2905
|
// rather than as a linear ramp.
|
|
2060
2906
|
let depth = clamp(-d / lensDepth, 0.0, 1.0);
|
|
2061
2907
|
let profile = (1.0 - depth) * (1.0 - depth);
|
|
2062
2908
|
|
|
2063
|
-
|
|
2909
|
+
// W11c G2: the lens is the body read from further inside. 'lens.y' is the
|
|
2910
|
+
// profile's refraction gain, the contour displacement in lens depths.
|
|
2911
|
+
let displaceCss = -normal * lensDepth * profile * ou.lens.x * ou.lens.y;
|
|
2064
2912
|
let refracted01 = viewport01 + displaceCss / viewportCss;
|
|
2065
2913
|
|
|
2066
|
-
let straightUv = clamp(viewport01 * ou.fit.xy + ou.fit.zw, vec2f(0.0), vec2f(1.0));
|
|
2067
2914
|
let refractedUv = clamp(refracted01 * ou.fit.xy + ou.fit.zw, vec2f(0.0), vec2f(1.0));
|
|
2068
2915
|
|
|
2069
|
-
|
|
2070
|
-
|
|
2916
|
+
/*
|
|
2917
|
+
* The scattering facet of the size law: "a softer scattering of light".
|
|
2918
|
+
*
|
|
2919
|
+
* The body texture is one blur for the whole backdrop source \u2014 it is built once
|
|
2920
|
+
* per source per frame, so it cannot be per-surface. The chain beside it can:
|
|
2921
|
+
* 'size.w' is the chain level whose blur already matches that body, so
|
|
2922
|
+
* 'size.w + log2(sizeScatterGainMax)' is the level whose blur is the gain times
|
|
2923
|
+
* wider \u2014 the HEAVY component, at one fixed level \u2014 and lerping the body toward
|
|
2924
|
+
* it by 'kScatter' is what the reference's interior measures (W11c, claims
|
|
2925
|
+
* \xA75.41): a sharp component near the body's \u03C3 and a heavy one near \u03C3 10, mixed
|
|
2926
|
+
* by a share that is \u2248 0.4 on a small control and rises with the span. The
|
|
2927
|
+
* mix, not the level, is what the span moves.
|
|
2928
|
+
*/
|
|
2929
|
+
let scatterLod = clamp(ou.size.w + log2(max(ou.size.x, 1e-4)), 0.0, ou.lens.w);
|
|
2071
2930
|
|
|
2072
2931
|
var backdrop = vec3f(0.0);
|
|
2073
2932
|
if (ou.flags.x > 0.5) {
|
|
2074
|
-
|
|
2075
|
-
|
|
2933
|
+
// Both components at the refracted position (W11c G2): the band and the
|
|
2934
|
+
// interior are one body, and the displacement alone is the lens.
|
|
2935
|
+
let bodySample = textureSampleLevel(backdropBody, backdropSampler, refractedUv, 0.0);
|
|
2936
|
+
let scatterSample = textureSampleLevel(backdropChain, backdropSampler, refractedUv, scatterLod);
|
|
2076
2937
|
// Premultiplied linear in, straight colour out: the material composites over
|
|
2077
2938
|
// whatever is behind it, so a partially transparent backdrop must not darken
|
|
2078
2939
|
// the glass.
|
|
2079
|
-
let
|
|
2080
|
-
|
|
2081
|
-
backdrop = mix(bodyColour, lensColour, profile);
|
|
2940
|
+
let scatterColour = scatterSample.rgb / max(scatterSample.a, 1e-6);
|
|
2941
|
+
backdrop = mix(bodySample.rgb / max(bodySample.a, 1e-6), scatterColour, kScatter);
|
|
2082
2942
|
}
|
|
2083
2943
|
|
|
2084
2944
|
// Adaptive tint. 'adapt.w' is the strength the accessibility policy and the
|
|
2085
2945
|
// group's analysis quality already agreed on; at 0 the fixed tint stands, which
|
|
2086
2946
|
// is what a 'hint' or 'none' group gets.
|
|
2087
|
-
let
|
|
2088
|
-
|
|
2947
|
+
let neutral = mix(ou.tint.rgb, ou.adapt.rgb, ou.adapt.w);
|
|
2948
|
+
|
|
2949
|
+
/*
|
|
2950
|
+
* Backdrop tone adaptation (W7) \u2014 step two of the composition contract, between
|
|
2951
|
+
* the colour scheme's neutral and the author's tint.
|
|
2952
|
+
*
|
|
2953
|
+
* Read against 'toneColour': the backdrop SOURCE's own average, measured by the
|
|
2954
|
+
* host from the pixels it supplied and handed to both tiers as one number. Not a
|
|
2955
|
+
* per-pixel sample, and that is the load-bearing choice \u2014 see
|
|
2956
|
+
* 'GroupRenderInput.backdropTone' for the cross-tier measurement that settled
|
|
2957
|
+
* it, and note that the reference agrees: its capsule over a sparse bright grid
|
|
2958
|
+
* is a flat body rather than a window onto the grid.
|
|
2959
|
+
*
|
|
2960
|
+
* 'sizeK' is still per pixel, so a container holding a small control and a large
|
|
2961
|
+
* platter adapts each of them by its own thickness out of one pass.
|
|
2962
|
+
*
|
|
2963
|
+
* Strength is zero where the host measured no tone, and the whole axis stands
|
|
2964
|
+
* down where there is no backdrop at all rather than reading the zero vector as
|
|
2965
|
+
* a black backdrop and dissolving the surface into nothing.
|
|
2966
|
+
*
|
|
2967
|
+
* Written out instead of calling smoothstep() so that a profile patched with
|
|
2968
|
+
* low >= high degrades to a step rather than to NaN.
|
|
2969
|
+
*/
|
|
2970
|
+
var toneAdapt = 0.0;
|
|
2971
|
+
if (ou.flags.x > 0.5 && ou.toneAdapt.w > 0.0) {
|
|
2972
|
+
let toneX = ou.toneColour.w + ou.toneAdapt.z * sizeK;
|
|
2973
|
+
let toneT = clamp(
|
|
2974
|
+
(toneX - ou.toneAdapt.x) / max(ou.toneAdapt.y - ou.toneAdapt.x, 1e-6),
|
|
2975
|
+
0.0,
|
|
2976
|
+
1.0,
|
|
2977
|
+
);
|
|
2978
|
+
toneAdapt = clamp(ou.toneAdapt.w, 0.0, 1.0) * (1.0 - toneT * toneT * (3.0 - 2.0 * toneT));
|
|
2979
|
+
}
|
|
2980
|
+
/*
|
|
2981
|
+
* Both the colour and the alpha move, together and not separately. What the
|
|
2982
|
+
* adaptation means is that the INTERIOR converges on the backdrop's tone \u2014
|
|
2983
|
+
* mix(interior, tone, k) \u2014 and this is the (colour, alpha) pair that composites
|
|
2984
|
+
* to exactly that. Lerping the two independently makes a partially adapted
|
|
2985
|
+
* surface lighter than it started (more opaque toward a tint still mostly
|
|
2986
|
+
* neutral), which the 96 px cells caught at once: interior 0.4545 \u2192 0.5179
|
|
2987
|
+
* against a reference of 0.4542. The alpha half is not a colour axis reaching
|
|
2988
|
+
* the occlusion axis \u2014 an adapting material stops transmitting, which is what
|
|
2989
|
+
* the reference's flat body over the impulse grid is. See 'adaptedTintColour'
|
|
2990
|
+
* and 'adaptedTintAlpha' in material.ts.
|
|
2991
|
+
*/
|
|
2992
|
+
let sizedAlpha = ou.tint.w + ou.size.y * sizeK * (1.0 - ou.tint.w);
|
|
2993
|
+
|
|
2994
|
+
/*
|
|
2995
|
+
* The backdrop tone response solve (W9) \u2014 the law that owns the interior
|
|
2996
|
+
* MEAN, where the collapse below owns texture and nothing else (claims
|
|
2997
|
+
* \xA75.33). The composite under this mechanism reduces exactly to
|
|
2998
|
+
* mean = (1 \u2212 k)\xB7M\u2080 + k\xB7toneLuma with M\u2080 = (1 \u2212 \u03B1)\xB7bgLinear + \u03B1\xB7L(neutral),
|
|
2999
|
+
* so the neutral's tone is solved in closed form: shift its luma so the
|
|
3000
|
+
* post-collapse mean lands on R(encodedInput, sizeK), the reference's own
|
|
3001
|
+
* measured response. Chroma is untouched \u2014 the shift is achromatic \u2014 and
|
|
3002
|
+
* the author tint still displaces the result per the composition contract.
|
|
3003
|
+
*
|
|
3004
|
+
* Three stand-downs, each measured rather than defensive: the whole axis is
|
|
3005
|
+
* off where no backdrop tone was measured (same gate as the collapse); the
|
|
3006
|
+
* solve's authority fades to zero below the dark anchor, where the only
|
|
3007
|
+
* evidence is the impulse cell the collapse constants were fitted on; and
|
|
3008
|
+
* at k \u2192 1 the collapse owns the pixel outright, so the solve's
|
|
3009
|
+
* extrapolation is never evaluated against a vanishing (1 \u2212 k).
|
|
3010
|
+
*/
|
|
3011
|
+
var solvedNeutral = neutral;
|
|
3012
|
+
var solvedAlpha = sizedAlpha;
|
|
3013
|
+
if (ou.flags.x > 0.5 && ou.toneAdapt.w > 0.0 && ou.toneRowThin.w > 0.0 &&
|
|
3014
|
+
sizedAlpha > 1e-3 && toneAdapt < 0.995) {
|
|
3015
|
+
let encodedInput = srgb_encode(ou.toneColour.w);
|
|
3016
|
+
let anchor = max(ou.toneAnchor.x, 1e-4);
|
|
3017
|
+
let authority =
|
|
3018
|
+
smoothstep(anchor * 0.5, anchor, encodedInput) * clamp(ou.toneRowThin.w, 0.0, 1.0);
|
|
3019
|
+
if (authority > 0.0) {
|
|
3020
|
+
let response = tone_response(encodedInput, sizeK);
|
|
3021
|
+
// The collapse's mean pull is toward L(toneColour.rgb) \u2014 the LINEAR
|
|
3022
|
+
// mean, which toneAnchor.w carries \u2014 not toward the encoded level.
|
|
3023
|
+
let preCollapse = (response - toneAdapt * ou.toneAnchor.w) / (1.0 - toneAdapt);
|
|
3024
|
+
let neutralLuma = dot(neutral, vec3f(0.2126, 0.7152, 0.0722));
|
|
3025
|
+
let nominal = (1.0 - sizedAlpha) * ou.toneAnchor.w + sizedAlpha * neutralLuma;
|
|
3026
|
+
let shift = (preCollapse - nominal) / sizedAlpha * authority * ou.toneAdapt.w;
|
|
3027
|
+
solvedNeutral = clamp(neutral + vec3f(shift), vec3f(0.0), vec3f(1.0));
|
|
3028
|
+
/*
|
|
3029
|
+
* The light attractor needs OPACITY. The light scheme's neutral is
|
|
3030
|
+
* already at white, so an upward shift clamps to nothing \u2014 and the
|
|
3031
|
+
* reference's light-adapted state is the material gone opaque-bright,
|
|
3032
|
+
* the same "an adapting material stops transmitting" the collapse's
|
|
3033
|
+
* alpha half was measured on. Whatever the clamp truncated is carried
|
|
3034
|
+
* by the alpha, solved against the same composite and folded by the
|
|
3035
|
+
* same authority. One-sided by design: darkward opacity is the
|
|
3036
|
+
* collapse's own axis with its own fitted constants.
|
|
3037
|
+
*/
|
|
3038
|
+
let solvedLuma = dot(solvedNeutral, vec3f(0.2126, 0.7152, 0.0722));
|
|
3039
|
+
let achieved = (1.0 - sizedAlpha) * ou.toneAnchor.w + sizedAlpha * solvedLuma;
|
|
3040
|
+
if (preCollapse > achieved + 1e-4 && solvedLuma > ou.toneAnchor.w + 1e-3) {
|
|
3041
|
+
let alphaTarget = clamp(
|
|
3042
|
+
(preCollapse - ou.toneAnchor.w) / (solvedLuma - ou.toneAnchor.w),
|
|
3043
|
+
sizedAlpha,
|
|
3044
|
+
1.0,
|
|
3045
|
+
);
|
|
3046
|
+
solvedAlpha = mix(sizedAlpha, alphaTarget, authority * ou.toneAdapt.w);
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
let adaptedAlpha = solvedAlpha + toneAdapt * (1.0 - solvedAlpha);
|
|
3052
|
+
var adapted = solvedNeutral;
|
|
3053
|
+
if (toneAdapt > 0.0 && adaptedAlpha > 0.0) {
|
|
3054
|
+
adapted =
|
|
3055
|
+
(solvedNeutral * ((1.0 - toneAdapt) * solvedAlpha) + ou.toneColour.rgb * toneAdapt) /
|
|
3056
|
+
adaptedAlpha;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
// The material, untinted: the adapted neutral over what this pixel looks
|
|
3060
|
+
// through, at the material's own occlusion. That alpha is what reduced
|
|
3061
|
+
// transparency lifts and what the size law thickens ("a larger size is more
|
|
3062
|
+
// opaque" is a statement about how much material there is), and the backdrop
|
|
3063
|
+
// adaptation moved it above. The author's colour never touches it \u2014 but not
|
|
3064
|
+
// for the reason the composition contract first gave (claims \xA75.36).
|
|
3065
|
+
var colour = mix(backdrop, adapted, adaptedAlpha);
|
|
3066
|
+
/*
|
|
3067
|
+
* How much of this pixel the SURFACE owns, as the canvas will composite it.
|
|
3068
|
+
*
|
|
3069
|
+
* With a backdrop the composite above is the whole pixel \u2014 the pass sampled
|
|
3070
|
+
* what is behind the surface and mixed it in, so the output is opaque inside
|
|
3071
|
+
* the contour and the page beneath the canvas never shows. With no backdrop
|
|
3072
|
+
* the composite is not here to make: a 'css-backdrop' group's blurred
|
|
3073
|
+
* backdrop is a DOM proxy under this canvas, a 'none' group's is the page
|
|
3074
|
+
* itself, and the browser is the compositor. So the material leaves as a
|
|
3075
|
+
* LAYER \u2014 the adapted colour at the material's own alpha, premultiplied on
|
|
3076
|
+
* the way out \u2014 and every term below that would have shaped the composite
|
|
3077
|
+
* shapes the layer instead, in the form that composites to the same thing.
|
|
3078
|
+
*
|
|
3079
|
+
* Before W11a this path mixed the material over a black backdrop and wrote
|
|
3080
|
+
* it opaque: a nested surface over glass rendered as a flat 0.468 where the
|
|
3081
|
+
* reference reads 0.89 (claims \xA75.38 \xA75). The rows that floored were that
|
|
3082
|
+
* grey. The pair the layer is written at is the host's
|
|
3083
|
+
* ('GroupRenderInput.unsampledMaterial'): the browser composites this canvas
|
|
3084
|
+
* in encoded sRGB, so the alpha is the CSS tier's, not the linear profile's.
|
|
3085
|
+
*/
|
|
3086
|
+
var bodyAlpha = 1.0;
|
|
3087
|
+
if (ou.flags.x <= 0.5) {
|
|
3088
|
+
colour = adapted;
|
|
3089
|
+
bodyAlpha = adaptedAlpha;
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
// The author tint (W10). 'aux.w' is the per-pixel strength, unioned in the
|
|
3093
|
+
// field pass, so a toolbar can carry one tinted control among plain ones; the
|
|
3094
|
+
// seed is a group uniform. At strength 0 this is the identity and the material
|
|
3095
|
+
// is the one the calibration bed measures, byte for byte.
|
|
3096
|
+
//
|
|
3097
|
+
// Apple's mechanism, measured per pixel: the tinted material is an OPAQUE
|
|
3098
|
+
// layer of the seed at a shade that is linear in the luminance the untinted
|
|
3099
|
+
// material shows at this pixel \u2014 about half the seed's light over black
|
|
3100
|
+
// content, the seed itself over white, the seed's chromaticity intact
|
|
3101
|
+
// throughout. That layer composites over the material at the AUTHOR's
|
|
3102
|
+
// opacity in the encoded space, which is what a CALayer with 'opacity' does
|
|
3103
|
+
// and how the reference's half-strength cell measures. So a tinted surface
|
|
3104
|
+
// over a checkerboard shows the checker as light and dark ORANGE, not as
|
|
3105
|
+
// orange glass with the checker behind it \u2014 the material's own alpha is not
|
|
3106
|
+
// what a tinted surface shows.
|
|
3107
|
+
//
|
|
3108
|
+
// 'seed.w' is the contrast regime's grip on the excursion, never on the hue;
|
|
3109
|
+
// 'tone.z' is the profile's provenance gate (the dark scheme renders the pure
|
|
3110
|
+
// seed); and '1 \u2212 toneAdapt' folds the shade out where the collapse has made
|
|
3111
|
+
// the material a dark body, where the reference renders the pure seed too.
|
|
3112
|
+
// At zero grip the layer is the bare seed \u2014 the author's colour, flat.
|
|
3113
|
+
if (aux.w > 0.0) {
|
|
3114
|
+
// The untinted material's luminance at this pixel. Over a backdrop that is
|
|
3115
|
+
// the composite; as a layer it is the layer over the tone the host measured
|
|
3116
|
+
// for the group \u2014 zero where nothing was measured, the same reference-level
|
|
3117
|
+
// convention the CSS tier's 'materialLuminance' takes (within its 0.02).
|
|
3118
|
+
let u = bodyAlpha * dot(colour, vec3f(0.2126, 0.7152, 0.0722)) + (1.0 - bodyAlpha) * ou.toneColour.w;
|
|
3119
|
+
let grip = clamp(ou.seed.w, 0.0, 1.0) * clamp(ou.tone.z, 0.0, 1.0) * (1.0 - toneAdapt);
|
|
3120
|
+
let shade = mix(1.0, clamp(mix(ou.tone.x, ou.tone.y, clamp(u, 0.0, 1.0)), 0.0, 1.0), grip);
|
|
3121
|
+
let layer = ou.seed.rgb * shade;
|
|
3122
|
+
let s = clamp(aux.w, 0.0, 1.0);
|
|
3123
|
+
let encodedMaterial = linear_to_srgb(clamp(colour, vec3f(0.0), vec3f(1.0)));
|
|
3124
|
+
let encodedLayer = linear_to_srgb(layer);
|
|
3125
|
+
if (ou.flags.x > 0.5) {
|
|
3126
|
+
colour = srgb_to_linear(mix(encodedMaterial, encodedLayer, vec3f(s)));
|
|
3127
|
+
} else {
|
|
3128
|
+
// The opaque layer at the author's opacity over the material's own layer,
|
|
3129
|
+
// premultiplied: the fold 'tintedCssOptics' makes into one rgba.
|
|
3130
|
+
let premultiplied = (1.0 - s) * bodyAlpha * encodedMaterial + s * encodedLayer;
|
|
3131
|
+
bodyAlpha = 1.0 - (1.0 - s) * (1.0 - bodyAlpha);
|
|
3132
|
+
colour = srgb_to_linear(premultiplied / max(bodyAlpha, 1e-6));
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
/*
|
|
3137
|
+
* Everything below is the surface's own APPEARANCE \u2014 the marks that say a
|
|
3138
|
+
* surface is here rather than what is behind it \u2014 and all of it fades with the
|
|
3139
|
+
* adaptation, on the one factor.
|
|
3140
|
+
*
|
|
3141
|
+
* That is not symmetry for its own sake; it is what the reference does, and it
|
|
3142
|
+
* is a calibration cell rather than an inference: the reference's capsule over
|
|
3143
|
+
* the dark-solid backdrop is byte-identical to that background, rim included. A
|
|
3144
|
+
* material that has taken its backdrop's tone has no lit edge to show, because
|
|
3145
|
+
* there is no light in front of it to show one with.
|
|
3146
|
+
*
|
|
3147
|
+
* The gap was invisible until this axis existed. A rim of up to 130/255 sat
|
|
3148
|
+
* unnoticed inside a bright capsule body; with the body gone it is a white
|
|
3149
|
+
* outline around a surface that should not be there at all \u2014 595 pixels past
|
|
3150
|
+
* 2/255 on that one cell, and OKLab \u0394E max 0.47 where p95 already read 0.0000.
|
|
3151
|
+
* The same lesson W3 recorded from the other direction: a term that happens to
|
|
3152
|
+
* be hidden is one feature away from being visible.
|
|
3153
|
+
*/
|
|
3154
|
+
let present = 1.0 - toneAdapt;
|
|
2089
3155
|
|
|
2090
3156
|
// 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
|
-
|
|
3157
|
+
// strongest, which is what makes a thicker surface read as heavier \u2014 and the
|
|
3158
|
+
// shadow facet of the size law deepens it further with the span.
|
|
3159
|
+
let shadowDepth = ou.light.z * (1.0 + (ou.size.z - 1.0) * sizeK);
|
|
3160
|
+
// A multiplicative occlusion of the whole composite. As a layer that is the
|
|
3161
|
+
// colour scaled and the alpha raised \u2014 (k\xB7a\xB7c, 1 \u2212 k\xB7(1 \u2212 a)) composites to
|
|
3162
|
+
// k times what (a\xB7c, a) would \u2014 and at bodyAlpha 1 it is the plain product.
|
|
3163
|
+
let shadowKeep = 1.0 - profile * shadowDepth * ou.light.w * present;
|
|
3164
|
+
let shadowedAlpha = 1.0 - shadowKeep * (1.0 - bodyAlpha);
|
|
3165
|
+
colour = colour * (shadowKeep * bodyAlpha / max(shadowedAlpha, 1e-6));
|
|
3166
|
+
bodyAlpha = shadowedAlpha;
|
|
2093
3167
|
|
|
2094
3168
|
// Rim and specular from the gradient. The rim is unlit ambient edge brightness;
|
|
2095
3169
|
// the specular term is the same edge lit from 'light.xy'.
|
|
2096
3170
|
let rw = rim_weight(d, ou.rim.x);
|
|
2097
3171
|
let facing = dot(normal, ou.light.xy);
|
|
2098
3172
|
let spec = pow(clamp(facing, 0.0, 1.0), max(ou.rim.z, 1e-3)) * ou.rim.w;
|
|
2099
|
-
|
|
3173
|
+
let rim = rw * (ou.rim.y + spec) * present;
|
|
3174
|
+
if (ou.flags.x > 0.5) {
|
|
3175
|
+
colour = colour + vec3f(rim);
|
|
3176
|
+
} else {
|
|
3177
|
+
// Added light has no premultiplied form of its own \u2014 a canvas colour may
|
|
3178
|
+
// not exceed its alpha \u2014 so the layer carries the light in its opacity:
|
|
3179
|
+
// (a\xB7c + rim, a + rim) composites to dst\xB7(1 \u2212 a) + a\xB7c + rim \u2212 rim\xB7dst,
|
|
3180
|
+
// the additive term short of rim \xD7 dst. Exact wherever the layer is
|
|
3181
|
+
// opaque (an author tint at full strength, or a lit contour pixel whose
|
|
3182
|
+
// alpha the rim fills), and never further off than the rim times what
|
|
3183
|
+
// shows through \u2014 where the mix form ("white over the layer at the rim's
|
|
3184
|
+
// weight") is short by the rim times the whole composite, which halved
|
|
3185
|
+
// the rim on a tinted panel.
|
|
3186
|
+
let carried = clamp(bodyAlpha + rim, 0.0, 1.0);
|
|
3187
|
+
colour = min((colour * bodyAlpha + vec3f(rim)) / max(carried, 1e-6), vec3f(1.0));
|
|
3188
|
+
bodyAlpha = carried;
|
|
3189
|
+
}
|
|
2100
3190
|
|
|
2101
|
-
|
|
3191
|
+
// The material over its own shadow over the page, in one premultiplied vector:
|
|
3192
|
+
// the colour is the surface's, weighted by its coverage and by however much of
|
|
3193
|
+
// the pixel the layer owns, and the alpha is what the two of them together
|
|
3194
|
+
// leave the page. The shadow fills only what the surface's COVERAGE leaves \u2014
|
|
3195
|
+
// clipped out of the silhouette exactly as a 'box-shadow' is clipped out of
|
|
3196
|
+
// its border box \u2014 never what the layer's own transparency leaves: a
|
|
3197
|
+
// translucent surface shows the page through it, not its own shadow. (With
|
|
3198
|
+
// an opaque body the two are the same quantity, which is how the shadow was
|
|
3199
|
+
// first written and why the difference only surfaced with the layer form.)
|
|
3200
|
+
let body = encode_output(max(colour, vec3f(0.0)), coverage * bodyAlpha);
|
|
3201
|
+
return vec4f(body.rgb, body.a + shadowAlpha * (1.0 - coverage));
|
|
2102
3202
|
}`;
|
|
2103
3203
|
|
|
2104
3204
|
// ../renderer-webgpu/dist/wgsl/prelude.js
|
|
@@ -2345,7 +3445,7 @@ function createPassRunner(context) {
|
|
|
2345
3445
|
};
|
|
2346
3446
|
},
|
|
2347
3447
|
opticsPass(encoder, args) {
|
|
2348
|
-
const slot = uniformSlot(`optics:${args.groupId}`,
|
|
3448
|
+
const slot = uniformSlot(`optics:${args.groupId}`, 76);
|
|
2349
3449
|
const d = slot.data;
|
|
2350
3450
|
d[0] = args.viewportDevice[0];
|
|
2351
3451
|
d[1] = args.viewportDevice[1];
|
|
@@ -2356,8 +3456,8 @@ function createPassRunner(context) {
|
|
|
2356
3456
|
d[6] = args.fit[2];
|
|
2357
3457
|
d[7] = args.fit[3];
|
|
2358
3458
|
d[8] = args.refractionScale;
|
|
2359
|
-
d[9] = args.
|
|
2360
|
-
d[10] =
|
|
3459
|
+
d[9] = args.lensRefractionGain;
|
|
3460
|
+
d[10] = 0;
|
|
2361
3461
|
d[11] = args.chainMaxLod;
|
|
2362
3462
|
d[12] = args.tint[0];
|
|
2363
3463
|
d[13] = args.tint[1];
|
|
@@ -2367,18 +3467,62 @@ function createPassRunner(context) {
|
|
|
2367
3467
|
d[17] = args.adaptTint[1];
|
|
2368
3468
|
d[18] = args.adaptTint[2];
|
|
2369
3469
|
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.
|
|
3470
|
+
d[20] = args.tintSeed[0];
|
|
3471
|
+
d[21] = args.tintSeed[1];
|
|
3472
|
+
d[22] = args.tintSeed[2];
|
|
3473
|
+
d[23] = args.tintToneAdaptation;
|
|
3474
|
+
d[24] = args.tintShade[0];
|
|
3475
|
+
d[25] = args.tintShade[1];
|
|
3476
|
+
d[26] = args.tintShade[2];
|
|
3477
|
+
d[27] = args.sizeFold;
|
|
3478
|
+
d[28] = args.rimWidth;
|
|
3479
|
+
d[29] = args.rimAlpha;
|
|
3480
|
+
d[30] = args.specularPower;
|
|
3481
|
+
d[31] = args.specularGain;
|
|
3482
|
+
d[32] = args.lightDirection[0];
|
|
3483
|
+
d[33] = args.lightDirection[1];
|
|
3484
|
+
d[34] = args.shadowDepth;
|
|
3485
|
+
d[35] = args.shadowAlpha;
|
|
3486
|
+
d[36] = args.backdrop === void 0 ? 0 : 1;
|
|
3487
|
+
d[37] = args.fields.width;
|
|
3488
|
+
d[38] = args.fields.height;
|
|
3489
|
+
d[39] = args.fields.upsampled ? 1 : 0;
|
|
3490
|
+
d[40] = args.sizeScatterGainMax;
|
|
3491
|
+
d[41] = args.sizeOcclusionGain;
|
|
3492
|
+
d[42] = args.sizeShadowGainMax;
|
|
3493
|
+
d[43] = args.bodyChainLod;
|
|
3494
|
+
d[44] = args.backdropTone[0];
|
|
3495
|
+
d[45] = Math.max(args.backdropTone[1], args.backdropTone[0] + 1e-4);
|
|
3496
|
+
d[46] = args.backdropTone[2];
|
|
3497
|
+
d[47] = args.backdropTone[3];
|
|
3498
|
+
d[48] = args.backdropToneColour[0];
|
|
3499
|
+
d[49] = args.backdropToneColour[1];
|
|
3500
|
+
d[50] = args.backdropToneColour[2];
|
|
3501
|
+
d[51] = args.backdropToneColour[3];
|
|
3502
|
+
d[52] = args.outerShadow[0];
|
|
3503
|
+
d[53] = args.outerShadow[1];
|
|
3504
|
+
d[54] = args.outerShadow[2];
|
|
3505
|
+
d[55] = args.outerShadow[3];
|
|
3506
|
+
d[56] = args.outerShadowSizeGain;
|
|
3507
|
+
d[57] = args.outerShadowRectCssHeight;
|
|
3508
|
+
d[58] = 0;
|
|
3509
|
+
d[59] = 0;
|
|
3510
|
+
d[60] = args.backdropToneAnchorX[0];
|
|
3511
|
+
d[61] = args.backdropToneAnchorX[1];
|
|
3512
|
+
d[62] = args.backdropToneAnchorX[2];
|
|
3513
|
+
d[63] = args.backdropToneLinearMean;
|
|
3514
|
+
d[64] = args.backdropToneResponseThin[0];
|
|
3515
|
+
d[65] = args.backdropToneResponseThin[1];
|
|
3516
|
+
d[66] = args.backdropToneResponseThin[2];
|
|
3517
|
+
d[67] = args.backdropToneResponseStrength;
|
|
3518
|
+
d[68] = args.backdropToneResponseThick[0];
|
|
3519
|
+
d[69] = args.backdropToneResponseThick[1];
|
|
3520
|
+
d[70] = args.backdropToneResponseThick[2];
|
|
3521
|
+
d[71] = 0;
|
|
3522
|
+
d[72] = args.sizeScatterFloor;
|
|
3523
|
+
d[73] = args.sizeScatterSpanMax;
|
|
3524
|
+
d[74] = args.sizeSpanMin;
|
|
3525
|
+
d[75] = args.sizeSpanMax;
|
|
2382
3526
|
slot.write();
|
|
2383
3527
|
const chain = args.backdrop?.chain ?? placeholderView;
|
|
2384
3528
|
const body = args.backdrop?.body ?? placeholderView;
|
|
@@ -2408,7 +3552,7 @@ function createPassRunner(context) {
|
|
|
2408
3552
|
pass.end();
|
|
2409
3553
|
},
|
|
2410
3554
|
highlightPass(encoder, args) {
|
|
2411
|
-
const slot = uniformSlot(`highlight:${args.groupId}`,
|
|
3555
|
+
const slot = uniformSlot(`highlight:${args.groupId}`, 32);
|
|
2412
3556
|
const d = slot.data;
|
|
2413
3557
|
d[0] = args.viewportDevice[0];
|
|
2414
3558
|
d[1] = args.viewportDevice[1];
|
|
@@ -2430,6 +3574,19 @@ function createPassRunner(context) {
|
|
|
2430
3574
|
d[17] = args.fields.height;
|
|
2431
3575
|
d[18] = args.fields.upsampled ? 1 : 0;
|
|
2432
3576
|
d[19] = 0;
|
|
3577
|
+
d[20] = args.backdropTone[0];
|
|
3578
|
+
d[21] = Math.max(args.backdropTone[1], args.backdropTone[0] + 1e-4);
|
|
3579
|
+
d[22] = args.backdropTone[2];
|
|
3580
|
+
d[23] = args.backdropTone[3];
|
|
3581
|
+
d[24] = args.backdropToneLevel;
|
|
3582
|
+
d[25] = args.sizeSpanMin;
|
|
3583
|
+
d[26] = args.sizeSpanMax;
|
|
3584
|
+
d[27] = args.sizeFold;
|
|
3585
|
+
const fieldRect = args.fieldRectDevice;
|
|
3586
|
+
d[28] = args.rectDevice.width / Math.max(fieldRect.width, 1e-6);
|
|
3587
|
+
d[29] = args.rectDevice.height / Math.max(fieldRect.height, 1e-6);
|
|
3588
|
+
d[30] = (args.rectDevice.x - fieldRect.x) / Math.max(fieldRect.width, 1e-6);
|
|
3589
|
+
d[31] = (args.rectDevice.y - fieldRect.y) / Math.max(fieldRect.height, 1e-6);
|
|
2433
3590
|
slot.write();
|
|
2434
3591
|
const pipeline = highlightPipeline(args.targetFormat);
|
|
2435
3592
|
const pass = encoder.beginRenderPass({
|
|
@@ -2540,6 +3697,13 @@ function planPyramid(sourceWidth, sourceHeight, policy) {
|
|
|
2540
3697
|
};
|
|
2541
3698
|
}
|
|
2542
3699
|
var CHAIN_SIGMA_AT_LEVEL_1 = 1.2;
|
|
3700
|
+
function chainLodForSigma(sigmaTexels) {
|
|
3701
|
+
if (!(sigmaTexels > 0))
|
|
3702
|
+
return 0;
|
|
3703
|
+
if (sigmaTexels <= CHAIN_SIGMA_AT_LEVEL_1)
|
|
3704
|
+
return sigmaTexels / CHAIN_SIGMA_AT_LEVEL_1;
|
|
3705
|
+
return 1 + Math.log2(sigmaTexels / CHAIN_SIGMA_AT_LEVEL_1);
|
|
3706
|
+
}
|
|
2543
3707
|
function bodyBlurPlan(sigmaPx, plan) {
|
|
2544
3708
|
if (sigmaPx <= 0)
|
|
2545
3709
|
return { level: 0, residualSigmaTexels: 0 };
|
|
@@ -2608,6 +3772,11 @@ function createRebuildLedger() {
|
|
|
2608
3772
|
|
|
2609
3773
|
// ../renderer-webgpu/dist/pyramid.js
|
|
2610
3774
|
var chainUsage = () => GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING;
|
|
3775
|
+
var densityOf = (source, request) => texelsPerCssPx(source.width, source.height, request.placement, request.viewportCss[0], request.viewportCss[1]);
|
|
3776
|
+
var sameDensity = (existing, request) => {
|
|
3777
|
+
const next = densityOf({ width: existing.sourceWidth, height: existing.sourceHeight }, request);
|
|
3778
|
+
return Math.abs(next - existing.texelsPerCss) <= 1e-6 * Math.max(1, existing.texelsPerCss);
|
|
3779
|
+
};
|
|
2611
3780
|
function createPyramidStore(context) {
|
|
2612
3781
|
const { device, pool, cache } = context;
|
|
2613
3782
|
const resources = /* @__PURE__ */ new Map();
|
|
@@ -2683,7 +3852,7 @@ function createPyramidStore(context) {
|
|
|
2683
3852
|
return target;
|
|
2684
3853
|
};
|
|
2685
3854
|
const mipView = (texture, level) => texture.createView({ baseMipLevel: level, mipLevelCount: 1, dimension: "2d" });
|
|
2686
|
-
function allocate(sourceId, plan, bodyLevel, sizeEpoch, builtEpoch) {
|
|
3855
|
+
function allocate(sourceId, plan, bodyLevel, sizeEpoch, builtEpoch, bodySigmaTexels, density) {
|
|
2687
3856
|
const existing = resources.get(sourceId);
|
|
2688
3857
|
const bodyWidth = (plan.levels[bodyLevel] ?? plan.levels[0]).width;
|
|
2689
3858
|
const bodyHeight = (plan.levels[bodyLevel] ?? plan.levels[0]).height;
|
|
@@ -2720,7 +3889,11 @@ function createPyramidStore(context) {
|
|
|
2720
3889
|
body,
|
|
2721
3890
|
stats,
|
|
2722
3891
|
sizeEpoch,
|
|
2723
|
-
builtEpoch
|
|
3892
|
+
builtEpoch,
|
|
3893
|
+
bodySigmaTexels,
|
|
3894
|
+
texelsPerCss: density.texelsPerCss,
|
|
3895
|
+
sourceWidth: density.sourceWidth,
|
|
3896
|
+
sourceHeight: density.sourceHeight
|
|
2724
3897
|
};
|
|
2725
3898
|
resources.set(sourceId, next);
|
|
2726
3899
|
return next;
|
|
@@ -2908,7 +4081,7 @@ function createPyramidStore(context) {
|
|
|
2908
4081
|
},
|
|
2909
4082
|
build(request, provider, encoder) {
|
|
2910
4083
|
const existing = liveResources(request.sourceId);
|
|
2911
|
-
if (existing !== void 0 && existing.builtEpoch >= request.epoch && !provider.isDirty()) {
|
|
4084
|
+
if (existing !== void 0 && existing.builtEpoch >= request.epoch && !provider.isDirty() && sameDensity(existing, request)) {
|
|
2912
4085
|
ledger.recordClean();
|
|
2913
4086
|
return { status: "clean", resources: existing };
|
|
2914
4087
|
}
|
|
@@ -2926,11 +4099,11 @@ function createPyramidStore(context) {
|
|
|
2926
4099
|
}
|
|
2927
4100
|
pendingRelease.push(provider);
|
|
2928
4101
|
const plan = planPyramid(frame.width, frame.height, request.resolution);
|
|
2929
|
-
const
|
|
2930
|
-
const cover = viewportW > 0 && viewportH > 0 ? Math.max(frame.width / viewportW, frame.height / viewportH) : 1;
|
|
4102
|
+
const texelsPerCss = densityOf(frame, request);
|
|
2931
4103
|
const planScale = frame.width > 0 ? plan.width / frame.width : 1;
|
|
2932
|
-
const
|
|
2933
|
-
const
|
|
4104
|
+
const bodySigmaTexels = request.bodySigmaCss * texelsPerCss * planScale;
|
|
4105
|
+
const bodyPlan = bodyBlurPlan(bodySigmaTexels, plan);
|
|
4106
|
+
const target = allocate(request.sourceId, plan, bodyPlan.level, frame.sizeEpoch, request.epoch, bodySigmaTexels, { texelsPerCss, sourceWidth: frame.width, sourceHeight: frame.height });
|
|
2934
4107
|
runImport(encoder, request.sourceId, frame, target.chain);
|
|
2935
4108
|
runChain(encoder, request.sourceId, plan, target.chain);
|
|
2936
4109
|
runBodyBlur(encoder, request.sourceId, plan, target.chain, target.body, bodyPlan.level, bodyPlan.residualSigmaTexels);
|
|
@@ -3046,17 +4219,11 @@ var RENDERER_PASS_IDS = [
|
|
|
3046
4219
|
OPTICS_PASS_ID,
|
|
3047
4220
|
HIGHLIGHT_PASS_ID
|
|
3048
4221
|
];
|
|
3049
|
-
var
|
|
3050
|
-
glass: "material",
|
|
3051
|
-
frost: "nominal",
|
|
3052
|
-
refraction: "nominal",
|
|
3053
|
-
occlusion: "nominal",
|
|
3054
|
-
border: "nominal",
|
|
3055
|
-
ambientTint: "nominal",
|
|
3056
|
-
foreground: "adaptive"
|
|
3057
|
-
};
|
|
4222
|
+
var STANDALONE_RESOLUTION = { scale: 1, maxDimension: 2048 };
|
|
3058
4223
|
function createWebGPURenderer(options = {}) {
|
|
3059
4224
|
const providers = /* @__PURE__ */ new Map();
|
|
4225
|
+
const placements = /* @__PURE__ */ new Map();
|
|
4226
|
+
const lastResolution = /* @__PURE__ */ new Map();
|
|
3060
4227
|
const groups = /* @__PURE__ */ new Map();
|
|
3061
4228
|
const adaptation = /* @__PURE__ */ new Map();
|
|
3062
4229
|
const lastReadbackAt = /* @__PURE__ */ new Map();
|
|
@@ -3145,18 +4312,34 @@ function createWebGPURenderer(options = {}) {
|
|
|
3145
4312
|
}
|
|
3146
4313
|
return state;
|
|
3147
4314
|
};
|
|
3148
|
-
const
|
|
3149
|
-
const
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
4315
|
+
const fitFor = (sourceId, pyramid) => {
|
|
4316
|
+
const placement = placements.get(sourceId);
|
|
4317
|
+
return isUsablePlacement(placement) ? placementFit(placement, viewport.widthCss, viewport.heightCss) : coverFit(pyramid.plan.width, pyramid.plan.height, viewport.widthCss, viewport.heightCss);
|
|
4318
|
+
};
|
|
4319
|
+
const densityRebuilds = (explicit, pyramids) => {
|
|
4320
|
+
const requests = [];
|
|
4321
|
+
const named = new Set(explicit.map((request) => request.sourceId));
|
|
4322
|
+
for (const sourceId of providers.keys()) {
|
|
4323
|
+
if (named.has(sourceId))
|
|
4324
|
+
continue;
|
|
4325
|
+
const existing = pyramids.resources(sourceId);
|
|
4326
|
+
if (existing === void 0)
|
|
4327
|
+
continue;
|
|
4328
|
+
const next = texelsPerCssPx(existing.sourceWidth, existing.sourceHeight, placements.get(sourceId), viewport.widthCss, viewport.heightCss);
|
|
4329
|
+
if (Math.abs(next - existing.texelsPerCss) <= 1e-6 * Math.max(1, existing.texelsPerCss))
|
|
4330
|
+
continue;
|
|
4331
|
+
requests.push({
|
|
4332
|
+
sourceId,
|
|
4333
|
+
epoch: existing.builtEpoch,
|
|
4334
|
+
resolution: lastResolution.get(sourceId) ?? STANDALONE_RESOLUTION,
|
|
4335
|
+
groupIds: [...groups.values()].filter((entry) => entry.input.backdropSourceId === sourceId).map((entry) => entry.input.groupId)
|
|
4336
|
+
});
|
|
3154
4337
|
}
|
|
3155
|
-
|
|
3156
|
-
return [1, scaleY, 0, (1 - scaleY) / 2];
|
|
4338
|
+
return requests;
|
|
3157
4339
|
};
|
|
3158
4340
|
const unionOf = (input) => input.union ?? DEFAULT_GROUP_UNION;
|
|
3159
4341
|
const variantOf = (input) => input.variant ?? "regular";
|
|
4342
|
+
const groupTintSeed = (input) => input.surfaces.find((surface) => surface.fieldReferenceOnly !== true && (surface.tint?.strength ?? 0) > 0)?.tint?.color;
|
|
3160
4343
|
const stateOf = (input, resolution) => {
|
|
3161
4344
|
const resolved = resolution?.groups.find((group) => group.groupId === input.groupId);
|
|
3162
4345
|
if (resolved === void 0) {
|
|
@@ -3168,8 +4351,12 @@ function createWebGPURenderer(options = {}) {
|
|
|
3168
4351
|
};
|
|
3169
4352
|
};
|
|
3170
4353
|
function rebuildRequests(explicit) {
|
|
3171
|
-
|
|
3172
|
-
|
|
4354
|
+
const base = explicit ?? standaloneRequests();
|
|
4355
|
+
const { store: pyramids } = ensureContext();
|
|
4356
|
+
const density = densityRebuilds(base, pyramids);
|
|
4357
|
+
return density.length === 0 ? base : [...base, ...density];
|
|
4358
|
+
}
|
|
4359
|
+
function standaloneRequests() {
|
|
3173
4360
|
const sampled = /* @__PURE__ */ new Set();
|
|
3174
4361
|
for (const entry of groups.values()) {
|
|
3175
4362
|
const id = entry.input.backdropSourceId;
|
|
@@ -3187,7 +4374,7 @@ function createWebGPURenderer(options = {}) {
|
|
|
3187
4374
|
// keeping the books, so a monotonically rising number is the honest
|
|
3188
4375
|
// stand-in and it keeps `build`'s clean-skip from firing spuriously.
|
|
3189
4376
|
epoch: framesDrawn + 1,
|
|
3190
|
-
resolution:
|
|
4377
|
+
resolution: STANDALONE_RESOLUTION,
|
|
3191
4378
|
groupIds: [...groups.values()].filter((entry) => entry.input.backdropSourceId === sourceId).map((entry) => entry.input.groupId)
|
|
3192
4379
|
});
|
|
3193
4380
|
}
|
|
@@ -3205,12 +4392,15 @@ function createWebGPURenderer(options = {}) {
|
|
|
3205
4392
|
}
|
|
3206
4393
|
const variant = variantOf([...groups.values()].find((entry) => entry.input.backdropSourceId === request.sourceId)?.input ?? { });
|
|
3207
4394
|
const optics = opticsUnderPolicy(material.optics[variant], accessibility, material);
|
|
4395
|
+
lastResolution.set(request.sourceId, request.resolution);
|
|
4396
|
+
const placement = placements.get(request.sourceId);
|
|
3208
4397
|
const outcome = pyramids.build({
|
|
3209
4398
|
sourceId: request.sourceId,
|
|
3210
4399
|
epoch: request.epoch,
|
|
3211
4400
|
resolution: request.resolution,
|
|
3212
4401
|
bodySigmaCss: optics.blurSigma,
|
|
3213
|
-
viewportCss: [viewport.widthCss, viewport.heightCss]
|
|
4402
|
+
viewportCss: [viewport.widthCss, viewport.heightCss],
|
|
4403
|
+
...isUsablePlacement(placement) ? { placement } : {}
|
|
3214
4404
|
}, provider, encoder);
|
|
3215
4405
|
if (outcome.status === "built")
|
|
3216
4406
|
built += 1;
|
|
@@ -3246,9 +4436,10 @@ function createWebGPURenderer(options = {}) {
|
|
|
3246
4436
|
const input = entry.input;
|
|
3247
4437
|
if (input.surfaces.length === 0)
|
|
3248
4438
|
continue;
|
|
4439
|
+
const policy = resolution?.accessibility.material ?? accessibility;
|
|
3249
4440
|
let surfaces;
|
|
3250
4441
|
try {
|
|
3251
|
-
surfaces = resolveSurfaces(input, governor.knobs.fieldFamily, material);
|
|
4442
|
+
surfaces = resolveSurfaces(input, governor.knobs.fieldFamily, material, policy);
|
|
3252
4443
|
} catch (error) {
|
|
3253
4444
|
skipped.push({
|
|
3254
4445
|
groupId: input.groupId,
|
|
@@ -3257,10 +4448,17 @@ function createWebGPURenderer(options = {}) {
|
|
|
3257
4448
|
continue;
|
|
3258
4449
|
}
|
|
3259
4450
|
const union = unionOf(input);
|
|
3260
|
-
const
|
|
4451
|
+
const shadow = outerShadowUnderPolicy(policy, material);
|
|
4452
|
+
let reachOcclusion = shadow.occlusion;
|
|
4453
|
+
for (const surface of surfaces) {
|
|
4454
|
+
reachOcclusion = Math.max(reachOcclusion, sizeOuterShadowOcclusionAt(shadow.occlusion, surface.sizeThickness, material));
|
|
4455
|
+
}
|
|
4456
|
+
const shadowReachPx = outerShadowReachPx({ ...shadow, occlusion: reachOcclusion });
|
|
4457
|
+
const snapped = snapRectToDevicePixels(groupFieldRect(surfaces, union, void 0, shadowReachPx), dpr);
|
|
3261
4458
|
const rectDevice = clipFieldRectToCanvas(snapped, dpr, viewportDevice);
|
|
3262
4459
|
if (rectDevice === void 0)
|
|
3263
4460
|
continue;
|
|
4461
|
+
const surfaceRectDevice = shadowReachPx === 0 ? rectDevice : clipFieldRectToCanvas(snapRectToDevicePixels(groupFieldRect(surfaces, union), dpr), dpr, viewportDevice) ?? rectDevice;
|
|
3264
4462
|
const packed = packInstances(surfaces, [
|
|
3265
4463
|
rectDevice.x * cssPerDevice,
|
|
3266
4464
|
rectDevice.y * cssPerDevice
|
|
@@ -3279,14 +4477,25 @@ function createWebGPURenderer(options = {}) {
|
|
|
3279
4477
|
union
|
|
3280
4478
|
});
|
|
3281
4479
|
const state = stateOf(input, resolution);
|
|
3282
|
-
const policy = resolution?.accessibility.material ?? accessibility;
|
|
3283
4480
|
const variant = variantOf(input);
|
|
3284
|
-
const optics = opticsUnderPolicy(material.optics[variant], policy, material);
|
|
3285
|
-
const refraction = effectiveRefraction(accessibilityRefractionCap(policy), state.refraction);
|
|
3286
|
-
const refractionScale = material.refractionScale[refraction];
|
|
3287
4481
|
const sourceId = input.backdropSourceId;
|
|
3288
4482
|
const pyramid = sourceId === void 0 ? void 0 : pyramids.resources(sourceId);
|
|
3289
4483
|
const adapt = sourceId === void 0 ? void 0 : adaptationFor(sourceId).values;
|
|
4484
|
+
const nominalOptics = material.optics[variant];
|
|
4485
|
+
const optics = opticsUnderPolicy(pyramid === void 0 && input.unsampledMaterial !== void 0 ? {
|
|
4486
|
+
...nominalOptics,
|
|
4487
|
+
tint: input.unsampledMaterial.tint,
|
|
4488
|
+
tintAlpha: input.unsampledMaterial.tintAlpha
|
|
4489
|
+
} : nominalOptics, policy, material);
|
|
4490
|
+
const backdropTone = [
|
|
4491
|
+
material.backdropToneLow,
|
|
4492
|
+
material.backdropToneHigh,
|
|
4493
|
+
backdropToneSizeBiasUnderPolicy(policy, material),
|
|
4494
|
+
input.backdropTone === void 0 ? 0 : backdropToneUnderPolicy(policy, material) * material.backdropToneMax
|
|
4495
|
+
];
|
|
4496
|
+
const backdropToneLevel = input.backdropTone === void 0 ? 0 : input.backdropToneLevel ?? relativeLuminance(input.backdropTone);
|
|
4497
|
+
const refraction = effectiveRefraction(accessibilityRefractionCap(policy), state.refraction);
|
|
4498
|
+
const refractionScale = material.refractionScale[refraction];
|
|
3290
4499
|
passes.opticsPass(encoder, {
|
|
3291
4500
|
groupId: input.groupId,
|
|
3292
4501
|
target: active.optics,
|
|
@@ -3296,15 +4505,29 @@ function createWebGPURenderer(options = {}) {
|
|
|
3296
4505
|
viewportDevice,
|
|
3297
4506
|
cssPerDevice,
|
|
3298
4507
|
coverageRampCss: cssPerDevice,
|
|
3299
|
-
fit: pyramid === void 0 ? [1, 1, 0, 0] :
|
|
4508
|
+
fit: pyramid === void 0 || sourceId === void 0 ? [1, 1, 0, 0] : fitFor(sourceId, pyramid),
|
|
3300
4509
|
refractionScale,
|
|
3301
|
-
|
|
3302
|
-
rimLodBias: material.lensRimLodBias,
|
|
4510
|
+
lensRefractionGain: material.lensRefractionGain,
|
|
3303
4511
|
chainMaxLod: pyramid?.plan.maxLod ?? 0,
|
|
3304
4512
|
tint: optics.tint,
|
|
3305
4513
|
tintAlpha: optics.tintAlpha,
|
|
3306
4514
|
adaptTint: adapt?.tint ?? optics.tint,
|
|
3307
4515
|
adaptStrength: adapt?.observed === true ? adaptationStrength(policy, state.analysisExact, material) : 0,
|
|
4516
|
+
/*
|
|
4517
|
+
* `glass: "none"` is forced colours, and it takes the author's tint with
|
|
4518
|
+
* the rest of the material.
|
|
4519
|
+
*
|
|
4520
|
+
* The pass keeps running — it is what paints the surface at all on this
|
|
4521
|
+
* tier — but with no backdrop and an opaque occlusion, so whatever colour
|
|
4522
|
+
* sits here IS the surface. An author tint arriving into that lands as a
|
|
4523
|
+
* flat fill of the author's colour where the platform's palette is
|
|
4524
|
+
* required, which is the one composition forced colours exists to
|
|
4525
|
+
* prevent. Falling back to the material's own tint reproduces exactly
|
|
4526
|
+
* what an untinted surface draws.
|
|
4527
|
+
*/
|
|
4528
|
+
tintSeed: (policy.glass === "none" ? void 0 : groupTintSeed(input)) ?? optics.tint,
|
|
4529
|
+
tintToneAdaptation: tintToneAdaptation(policy, material),
|
|
4530
|
+
tintShade: [material.tintShadeDark, material.tintShadeLight, material.tintShadeStrength],
|
|
3308
4531
|
rimWidth: optics.rimWidth,
|
|
3309
4532
|
rimAlpha: optics.rimAlpha,
|
|
3310
4533
|
specularPower: optics.specularPower,
|
|
@@ -3312,6 +4535,60 @@ function createWebGPURenderer(options = {}) {
|
|
|
3312
4535
|
lightDirection: material.lightDirection,
|
|
3313
4536
|
shadowDepth: optics.shadowDepth,
|
|
3314
4537
|
shadowAlpha: optics.shadowAlpha,
|
|
4538
|
+
// The size law's gains, per group (W2); the per-pixel factor they
|
|
4539
|
+
// multiply rides the field pass's aux channel, so one uniform serves a
|
|
4540
|
+
// group whose members are not one size. `bodyChainLod` is where the body
|
|
4541
|
+
// blur already sits on the chain — only the pyramid that built it knows
|
|
4542
|
+
// the CSS-px-to-texel conversion, so it publishes the σ and this converts.
|
|
4543
|
+
sizeScatterGainMax: material.sizeScatterGainMax,
|
|
4544
|
+
sizeOcclusionGain: material.sizeOcclusionGain,
|
|
4545
|
+
sizeShadowGainMax: material.sizeShadowGainMax,
|
|
4546
|
+
bodyChainLod: chainLodForSigma(pyramid?.bodySigmaTexels ?? 0),
|
|
4547
|
+
// The size law's curves (W11c): the shader evaluates the thickness and
|
|
4548
|
+
// the scatter mix per pixel from the span the field carries, under the
|
|
4549
|
+
// same fold `sizeThicknessUnderPolicy` applies on the CPU.
|
|
4550
|
+
sizeSpanMin: material.sizeSpanMin,
|
|
4551
|
+
sizeSpanMax: material.sizeSpanMax,
|
|
4552
|
+
sizeScatterFloor: material.sizeScatterFloor,
|
|
4553
|
+
sizeScatterSpanMax: material.sizeScatterSpanMax,
|
|
4554
|
+
sizeFold: material.refractionScale[accessibilityRefractionCap(policy)],
|
|
4555
|
+
backdropTone,
|
|
4556
|
+
backdropToneColour: [
|
|
4557
|
+
input.backdropTone?.[0] ?? 0,
|
|
4558
|
+
input.backdropTone?.[1] ?? 0,
|
|
4559
|
+
input.backdropTone?.[2] ?? 0,
|
|
4560
|
+
backdropToneLevel
|
|
4561
|
+
],
|
|
4562
|
+
// The response law's anchors (W9) and the linear backdrop mean its
|
|
4563
|
+
// solve composites against. The shader re-derives the encoded input
|
|
4564
|
+
// from `backdropToneLevel`, so the fourth slot carries the linear
|
|
4565
|
+
// mean — the one quantity the solve needs that the tone colour does
|
|
4566
|
+
// not already hold.
|
|
4567
|
+
backdropToneAnchorX: material.backdropToneAnchorX,
|
|
4568
|
+
backdropToneResponseThin: material.backdropToneResponseThin,
|
|
4569
|
+
backdropToneResponseThick: material.backdropToneResponseThick,
|
|
4570
|
+
// The response law rides only the UN-DEGRADED regime, for the same
|
|
4571
|
+
// provenance reason the dark profiles set its strength to 0: the
|
|
4572
|
+
// anchors are standard-light-reference measurements, and the
|
|
4573
|
+
// accessibility references behave differently (nearly opaque, flat in
|
|
4574
|
+
// span — W2). Measured before this gate existed: reduced-transparency
|
|
4575
|
+
// photo ΔE p95 0.007 → 0.048 and cross-tier ratio 0.99 → 0.86. Where
|
|
4576
|
+
// any policy fold touches the tone axis, the solve stands down and the
|
|
4577
|
+
// collapse alone runs, exactly the pre-W9 behaviour those profiles
|
|
4578
|
+
// were fitted on.
|
|
4579
|
+
backdropToneResponseStrength: backdropToneUnderPolicy(policy, material) >= 0.999 ? material.backdropToneResponseStrength : 0,
|
|
4580
|
+
backdropToneLinearMean: input.backdropToneLinearLuminance ?? backdropToneLevel,
|
|
4581
|
+
outerShadow: [
|
|
4582
|
+
outerShadowAlpha(shadow.occlusion),
|
|
4583
|
+
shadow.sigmaPx,
|
|
4584
|
+
shadow.spreadPx,
|
|
4585
|
+
// The offset in field-texture UV. The field spans exactly this rect, so
|
|
4586
|
+
// one CSS px is one over the rect's CSS height however the governor
|
|
4587
|
+
// scaled the rasterisation.
|
|
4588
|
+
shadow.offsetPx / Math.max(rectDevice.height * cssPerDevice, 1e-6)
|
|
4589
|
+
],
|
|
4590
|
+
outerShadowSizeGain: shadow.sizeGain,
|
|
4591
|
+
outerShadowRectCssHeight: rectDevice.height * cssPerDevice,
|
|
3315
4592
|
backdrop: pyramid === void 0 || policy.glass === "none" ? void 0 : {
|
|
3316
4593
|
chain: pyramid.chain.createView(),
|
|
3317
4594
|
body: pyramid.body.createView()
|
|
@@ -3323,7 +4600,9 @@ function createWebGPURenderer(options = {}) {
|
|
|
3323
4600
|
groupId: input.groupId,
|
|
3324
4601
|
target: active.highlight,
|
|
3325
4602
|
targetFormat: active.format,
|
|
3326
|
-
|
|
4603
|
+
// The surface's own rect, not the shadow's — see `surfaceRectDevice`.
|
|
4604
|
+
rectDevice: surfaceRectDevice,
|
|
4605
|
+
fieldRectDevice: rectDevice,
|
|
3327
4606
|
fields,
|
|
3328
4607
|
viewportDevice,
|
|
3329
4608
|
cssPerDevice,
|
|
@@ -3335,7 +4614,15 @@ function createWebGPURenderer(options = {}) {
|
|
|
3335
4614
|
pressPointCss: lead.channels.pressPoint ?? lead.centre,
|
|
3336
4615
|
glowRadiusCss: material.glowRadiusCss,
|
|
3337
4616
|
glowGain: material.glowGain,
|
|
3338
|
-
colour: optics.highlight
|
|
4617
|
+
colour: optics.highlight,
|
|
4618
|
+
// The same four numbers the optics pass took, so the two passes fade
|
|
4619
|
+
// the surface on one curve rather than on two copies of it — and the
|
|
4620
|
+
// same band and fold, since the curve is evaluated off the span.
|
|
4621
|
+
backdropTone,
|
|
4622
|
+
backdropToneLevel,
|
|
4623
|
+
sizeSpanMin: material.sizeSpanMin,
|
|
4624
|
+
sizeSpanMax: material.sizeSpanMax,
|
|
4625
|
+
sizeFold: material.refractionScale[accessibilityRefractionCap(policy)]
|
|
3339
4626
|
});
|
|
3340
4627
|
}
|
|
3341
4628
|
drawn += 1;
|
|
@@ -3400,6 +4687,8 @@ function createWebGPURenderer(options = {}) {
|
|
|
3400
4687
|
providers.set(provider.id, provider);
|
|
3401
4688
|
},
|
|
3402
4689
|
unregisterBackdrop(sourceId) {
|
|
4690
|
+
placements.delete(sourceId);
|
|
4691
|
+
lastResolution.delete(sourceId);
|
|
3403
4692
|
const provider = providers.get(sourceId);
|
|
3404
4693
|
if (provider === void 0)
|
|
3405
4694
|
return;
|
|
@@ -3412,6 +4701,17 @@ function createWebGPURenderer(options = {}) {
|
|
|
3412
4701
|
backdrop(sourceId) {
|
|
3413
4702
|
return providers.get(sourceId);
|
|
3414
4703
|
},
|
|
4704
|
+
setBackdropPlacement(sourceId, placement) {
|
|
4705
|
+
if (samePlacement(placements.get(sourceId), placement))
|
|
4706
|
+
return;
|
|
4707
|
+
if (placement === void 0)
|
|
4708
|
+
placements.delete(sourceId);
|
|
4709
|
+
else
|
|
4710
|
+
placements.set(sourceId, { ...placement });
|
|
4711
|
+
},
|
|
4712
|
+
backdropPlacement(sourceId) {
|
|
4713
|
+
return placements.get(sourceId);
|
|
4714
|
+
},
|
|
3415
4715
|
setViewport(next) {
|
|
3416
4716
|
const changed = next.widthCss !== viewport.widthCss || next.heightCss !== viewport.heightCss || next.devicePixelRatio !== viewport.devicePixelRatio;
|
|
3417
4717
|
viewport = next;
|
|
@@ -3568,6 +4868,6 @@ function createWebGPURenderer(options = {}) {
|
|
|
3568
4868
|
};
|
|
3569
4869
|
}
|
|
3570
4870
|
|
|
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,
|
|
3572
|
-
//# sourceMappingURL=dist-
|
|
3573
|
-
//# sourceMappingURL=dist-
|
|
4871
|
+
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, IDENTITY_FIT, IDLE_CHANNELS, INCREASED_OCCLUSION_LIFT, INSTANCE_BYTES, INSTANCE_FLOATS, LENS_REFRACTION_GAIN, 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_FLOOR, SIZE_SCATTER_GAIN_MAX, SIZE_SCATTER_SPAN_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, backdropFit, backdropToneAdaptation, backdropToneSizeBiasUnderPolicy, backdropToneUnderPolicy, bodyBlurPlan, chainModule, clipFieldRectToCanvas, coverFit, 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, isUsablePlacement, lensDepthPx, lensDisplacementPx, lensSizeGain, lensSizeGainFromThickness, linearGradientStops, linearToSrgb, linearToSrgbChannel, occlusionAlphaUnderPolicy, opticsModule, opticsUnderPolicy, outerShadowAlpha, outerShadowFalloff, outerShadowReachPx, outerShadowUnderPolicy, packInstances, pipelineKey, placementFit, planPyramid, poolKey, readbackDue, refractionRank, relativeLuminance, rendererError, resolveSurfaces, samePlacement, scatterThickness, sizeOcclusionAlpha, sizeOcclusionAlphaAt, sizeOuterShadowOcclusion, sizeOuterShadowOcclusionAt, sizeScatterSigma, sizeScatterSigmaAt, sizeShadowDepth, sizeShadowDepthAt, sizeThickness, sizeThicknessUnderPolicy, snapRectToDevicePixels, srgbToLinear, srgbToLinearChannel, statsFromBuffer, supportsTimestamps, texelsPerCssPx, tintShade, tintShadeLayer, tintToneAdaptation, tintedMaterialColour, validateAppTexture, withMaterialOverrides };
|
|
4872
|
+
//# sourceMappingURL=dist-MVSXUP3K.js.map
|
|
4873
|
+
//# sourceMappingURL=dist-MVSXUP3K.js.map
|