reze-engine 0.55.9 → 0.55.11
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/dist/engine.d.ts +30 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +197 -47
- package/dist/shaders/passes/composite.d.ts +5 -0
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +26 -4
- package/dist/shaders/scene-tap.d.ts +1 -1
- package/dist/shaders/scene-tap.d.ts.map +1 -1
- package/dist/shaders/scene-tap.js +57 -16
- package/package.json +1 -1
- package/src/engine.ts +208 -50
- package/src/shaders/passes/composite.ts +31 -4
- package/src/shaders/scene-tap.ts +62 -16
package/dist/engine.js
CHANGED
|
@@ -823,6 +823,13 @@ export class Engine {
|
|
|
823
823
|
this.fieldBgViews = [null, null];
|
|
824
824
|
this.fieldFgTextures = [null, null];
|
|
825
825
|
this.fieldFgViews = [null, null];
|
|
826
|
+
/** The filter's page: a second full-res pair, so a filter can read the pair
|
|
827
|
+
* the other effects drew into while it draws. Held only while a filter is
|
|
828
|
+
* installed — see ensureFilterPage. */
|
|
829
|
+
this.fieldPageBgTexture = null;
|
|
830
|
+
this.fieldPageFgTexture = null;
|
|
831
|
+
this.fieldPageBgView = null;
|
|
832
|
+
this.fieldPageFgView = null;
|
|
826
833
|
/** One per scale: the field shader reconstructs the full-res pixel it stands
|
|
827
834
|
* in for, so each pass needs its own (w, h, fullW, fullH). */
|
|
828
835
|
this.fieldUniformBuffers = [];
|
|
@@ -2169,6 +2176,56 @@ export class Engine {
|
|
|
2169
2176
|
this.fieldFgViews[i] = this.fieldFgTextures[i].createView();
|
|
2170
2177
|
this.device.queue.writeBuffer(this.fieldUniformBuffers[i], 0, new Float32Array([w, h, this.fieldFullW, this.fieldFullH]));
|
|
2171
2178
|
}
|
|
2179
|
+
this.ensureFilterPage();
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* The filter's page, held only while a filter is installed.
|
|
2183
|
+
*
|
|
2184
|
+
* A full-res rgba16f pair is real memory — two 16MB targets at 1080p, four
|
|
2185
|
+
* times that at 4K — and most scenes never install a filter. Created here
|
|
2186
|
+
* and on resize, released the moment the last filter goes. Sized off the
|
|
2187
|
+
* full pair, which is what a filter reads and what the composite reads back.
|
|
2188
|
+
*/
|
|
2189
|
+
ensureFilterPage() {
|
|
2190
|
+
const wanted = !!this.device && this.fieldFullW > 0 && this.effects.some((e) => e.filter);
|
|
2191
|
+
const w = Math.max(1, this.fieldFullW);
|
|
2192
|
+
const h = Math.max(1, this.fieldFullH);
|
|
2193
|
+
if (wanted && this.fieldPageBgTexture?.width === w && this.fieldPageBgTexture.height === h)
|
|
2194
|
+
return;
|
|
2195
|
+
this.fieldPageBgTexture?.destroy();
|
|
2196
|
+
this.fieldPageFgTexture?.destroy();
|
|
2197
|
+
this.fieldPageBgTexture = null;
|
|
2198
|
+
this.fieldPageFgTexture = null;
|
|
2199
|
+
this.fieldPageBgView = null;
|
|
2200
|
+
this.fieldPageFgView = null;
|
|
2201
|
+
if (!wanted)
|
|
2202
|
+
return;
|
|
2203
|
+
const make = (label) => this.device.createTexture({
|
|
2204
|
+
label,
|
|
2205
|
+
size: [w, h],
|
|
2206
|
+
format: "rgba16float",
|
|
2207
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
2208
|
+
});
|
|
2209
|
+
this.fieldPageBgTexture = make("field layer page (background)");
|
|
2210
|
+
this.fieldPageFgTexture = make("field layer page (foreground)");
|
|
2211
|
+
this.fieldPageBgView = this.fieldPageBgTexture.createView();
|
|
2212
|
+
this.fieldPageFgView = this.fieldPageFgTexture.createView();
|
|
2213
|
+
}
|
|
2214
|
+
/** Filters drawn this frame — the predicate renderFieldPass runs them by,
|
|
2215
|
+
* shared with the composite uniform that says the half pair was consumed. */
|
|
2216
|
+
filtersDrawn() {
|
|
2217
|
+
if (!this.fieldPageBgView || !this.fieldPageFgView)
|
|
2218
|
+
return 0;
|
|
2219
|
+
let n = 0;
|
|
2220
|
+
for (const e of this.effects)
|
|
2221
|
+
if (e.filter && e.fieldPipeline && e.fieldBindGroups && e.weight > 0)
|
|
2222
|
+
n++;
|
|
2223
|
+
return n;
|
|
2224
|
+
}
|
|
2225
|
+
/** Which of a filter's eight bind groups: the page it reads (0 the plain
|
|
2226
|
+
* pair, 1 the filter page), whether it absorbs the half pair, grid parity. */
|
|
2227
|
+
static filterBindIndex(readPage, absorbHalf, gridParity) {
|
|
2228
|
+
return (readPage * 2 + (absorbHalf ? 1 : 0)) * 2 + gridParity;
|
|
2172
2229
|
}
|
|
2173
2230
|
/**
|
|
2174
2231
|
* ONE PER GRID PARITY.
|
|
@@ -2184,7 +2241,7 @@ export class Engine {
|
|
|
2184
2241
|
// Captured, so the null guards above survive into the closure.
|
|
2185
2242
|
const depth = this.depthReadView;
|
|
2186
2243
|
const bloom = this.compositeBloomView;
|
|
2187
|
-
const build = (owner, grid) => this.device.createBindGroup({
|
|
2244
|
+
const build = (owner, grid, layers) => this.device.createBindGroup({
|
|
2188
2245
|
label: "field layer bind group",
|
|
2189
2246
|
layout: this.fieldBindGroupLayout,
|
|
2190
2247
|
entries: [
|
|
@@ -2206,6 +2263,11 @@ export class Engine {
|
|
|
2206
2263
|
{ binding: 26, resource: this.castDistView ?? this.castDistFallbackView },
|
|
2207
2264
|
{ binding: 27, resource: this.hdrResolveTexture.createView() },
|
|
2208
2265
|
{ binding: 28, resource: this.simSampler },
|
|
2266
|
+
// The other effects' layers, for rzSceneFrame — see the loop below.
|
|
2267
|
+
{ binding: 29, resource: layers[0] },
|
|
2268
|
+
{ binding: 30, resource: layers[1] },
|
|
2269
|
+
{ binding: 31, resource: layers[2] },
|
|
2270
|
+
{ binding: 32, resource: layers[3] },
|
|
2209
2271
|
{ binding: 2, resource: this.bloomSampler },
|
|
2210
2272
|
{ binding: 5, resource: this.filmicLutView },
|
|
2211
2273
|
{ binding: 10, resource: (this.agxLutTexture ?? this.agxFallbackTexture).createView({ dimension: "3d" }) },
|
|
@@ -2215,10 +2277,28 @@ export class Engine {
|
|
|
2215
2277
|
});
|
|
2216
2278
|
// Per effect: the params buffer and the grid are both its own, so two
|
|
2217
2279
|
// effects cannot share a bind group even when everything else matches.
|
|
2280
|
+
// What a filter reads, by variant. A plain effect draws into the pair it
|
|
2281
|
+
// would otherwise read, so it gets the transparent 1x1 at every layer slot
|
|
2282
|
+
// and rzSceneFrame answers the scene alone.
|
|
2283
|
+
const none = this.trailFallbackView;
|
|
2284
|
+
const pairA = [this.fieldBgViews[0] ?? none, this.fieldFgViews[0] ?? none];
|
|
2285
|
+
const page = [this.fieldPageBgView ?? none, this.fieldPageFgView ?? none];
|
|
2286
|
+
const half = [this.fieldBgViews[1] ?? none, this.fieldFgViews[1] ?? none];
|
|
2218
2287
|
for (const e of this.effects) {
|
|
2219
|
-
e.
|
|
2220
|
-
|
|
2221
|
-
|
|
2288
|
+
const grids = e.grid ? [e.grid.read[0], e.grid.read[1]] : [this.simFallbackView, this.simFallbackView];
|
|
2289
|
+
if (!e.filter) {
|
|
2290
|
+
e.fieldBindGroups = grids.map((g) => build(e, g, [none, none, none, none]));
|
|
2291
|
+
continue;
|
|
2292
|
+
}
|
|
2293
|
+
// Eight, in filterBindIndex order: read page × absorbs half × grid parity.
|
|
2294
|
+
const groups = [];
|
|
2295
|
+
for (const read of [pairA, page]) {
|
|
2296
|
+
for (const h of [[none, none], half]) {
|
|
2297
|
+
for (const g of grids)
|
|
2298
|
+
groups.push(build(e, g, [read[0], read[1], h[0], h[1]]));
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
e.fieldBindGroups = groups;
|
|
2222
2302
|
}
|
|
2223
2303
|
}
|
|
2224
2304
|
/**
|
|
@@ -2615,7 +2695,13 @@ export class Engine {
|
|
|
2615
2695
|
const gridSize = gridEntryPoint(wgsl) ? Math.min(d.grid || 256, GRID_MAX) : 0;
|
|
2616
2696
|
// `alias` goes in: a field effect reads bones through _rzSlot exactly as a
|
|
2617
2697
|
// particle one does, and it was the only module never handed the mapping.
|
|
2618
|
-
|
|
2698
|
+
// A FILTER by what it calls, like every other mount property. rzSceneFrame
|
|
2699
|
+
// answers the frame with the other effects' layers in it, which is only
|
|
2700
|
+
// possible from a pass that runs after theirs — see renderFieldPass.
|
|
2701
|
+
const filter = (hasBackground || hasForeground) && /\brzSceneFrame\s*\(/.test(wgsl);
|
|
2702
|
+
const fieldEffect = hasBackground || hasForeground
|
|
2703
|
+
? { wgsl, paramsDecl, hasBackground, hasForeground, gridSize, alias, trailCount: anchors.filter((a) => a.trail).length, filter, additiveLayer: d.additiveLayer }
|
|
2704
|
+
: null;
|
|
2619
2705
|
const source = buildCompositeShader(fieldEffect);
|
|
2620
2706
|
this.device.pushErrorScope("validation");
|
|
2621
2707
|
const module = this.device.createShaderModule({ label: "composite shader (effect)", code: source });
|
|
@@ -2658,10 +2744,15 @@ export class Engine {
|
|
|
2658
2744
|
// the premultiplication happens here, and the composite reads it
|
|
2659
2745
|
// back knowing that. With one effect over a cleared target the
|
|
2660
2746
|
// result is identical to the replace it used to do.
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2747
|
+
// A filter composes over the layers in its own shader and writes
|
|
2748
|
+
// the result outright: blending would layer it onto a page that
|
|
2749
|
+
// holds nothing.
|
|
2750
|
+
targets: filter
|
|
2751
|
+
? [{ format: "rgba16float" }, { format: "rgba16float" }]
|
|
2752
|
+
: [
|
|
2753
|
+
{ format: "rgba16float", blend: layerBlend },
|
|
2754
|
+
{ format: "rgba16float", blend: layerBlend },
|
|
2755
|
+
],
|
|
2665
2756
|
},
|
|
2666
2757
|
primitive: { topology: "triangle-list" },
|
|
2667
2758
|
multisample: { count: 1 },
|
|
@@ -2842,7 +2933,10 @@ export class Engine {
|
|
|
2842
2933
|
// against 4.5ms). It is the right call for a soft additive glow, which
|
|
2843
2934
|
// upsamples invisibly — and it is now a claim an author makes about
|
|
2844
2935
|
// their own effect rather than a fate that befalls one.
|
|
2845
|
-
|
|
2936
|
+
// A filter holds every other effect's pixels, so it holds them at full
|
|
2937
|
+
// resolution whatever it declared for itself.
|
|
2938
|
+
fieldLayer: filter ? 0 : d.fieldLayer,
|
|
2939
|
+
filter,
|
|
2846
2940
|
fieldPipeline,
|
|
2847
2941
|
fieldClock,
|
|
2848
2942
|
// Filled by rebuildFieldBindGroup below, which needs the instance to
|
|
@@ -2881,8 +2975,13 @@ export class Engine {
|
|
|
2881
2975
|
*/
|
|
2882
2976
|
async setEffects(list) {
|
|
2883
2977
|
const noMounts = { background: false, foreground: false };
|
|
2884
|
-
|
|
2978
|
+
// Clearing an engine that has nothing yet is already done; only an install
|
|
2979
|
+
// needs the device.
|
|
2980
|
+
if (!this.device) {
|
|
2981
|
+
if (!list || list.length === 0)
|
|
2982
|
+
return [];
|
|
2885
2983
|
return [{ ok: false, diagnostics: ["setEffects requires init() to have run"], mounts: noMounts, params: [], duration: 0 }];
|
|
2984
|
+
}
|
|
2886
2985
|
const requested = list ?? [];
|
|
2887
2986
|
if (requested.length === 0) {
|
|
2888
2987
|
for (const e of this.effects) {
|
|
@@ -3001,8 +3100,14 @@ export class Engine {
|
|
|
3001
3100
|
// fieldLayer when the instance is built, and both target pairs exist for
|
|
3002
3101
|
// the life of the surface. What used to be a scene-wide decision made here
|
|
3003
3102
|
// is now each effect's own.
|
|
3004
|
-
this.
|
|
3005
|
-
|
|
3103
|
+
this.ensureFilterPage();
|
|
3104
|
+
// EVERY shared buffer, not only the field and composite groups. The mounts
|
|
3105
|
+
// above were built across awaits, and audio, a score or lyrics landing in
|
|
3106
|
+
// one of those gaps replaced its buffer and destroyed the old one — which
|
|
3107
|
+
// the new particle, light, trail and grid groups had been built against,
|
|
3108
|
+
// so the next submit named a destroyed buffer. Rebinding at install closes
|
|
3109
|
+
// the gap from whichever side the asset arrived.
|
|
3110
|
+
this.rebindSharedBuffers();
|
|
3006
3111
|
this.writeCompositeViewUniforms();
|
|
3007
3112
|
return results;
|
|
3008
3113
|
}
|
|
@@ -3602,18 +3707,31 @@ export class Engine {
|
|
|
3602
3707
|
// The distance field, before anything reads it. Returns immediately when no
|
|
3603
3708
|
// installed effect names rzCastDistance, which is the common case.
|
|
3604
3709
|
this.encodeCastDistance(encoder);
|
|
3710
|
+
// FILTERS RUN LAST, each in a pass of its own, because each reads the pair
|
|
3711
|
+
// the others drew into — a texture no pass may sample while drawing into
|
|
3712
|
+
// it. So the full-res pair is two, the plain pair A and the filter's page,
|
|
3713
|
+
// and the passes alternate between them. The composite reads A and is not
|
|
3714
|
+
// rebuilt per frame, so the chain is arranged to END on A: with an odd
|
|
3715
|
+
// number of filters the plain effects draw into the page instead, and the
|
|
3716
|
+
// last filter lands where the composite looks. The half pair is read by
|
|
3717
|
+
// the first filter and consumed — viewU[5].w tells the composite so.
|
|
3718
|
+
const filters = drawn.filter((e) => e.filter);
|
|
3719
|
+
const pageA = [this.fieldBgViews[0], this.fieldFgViews[0]];
|
|
3720
|
+
const pageB = [this.fieldPageBgView, this.fieldPageFgView];
|
|
3721
|
+
const canFilter = filters.length > 0 && !!pageB[0] && !!pageB[1];
|
|
3722
|
+
const plainFull = canFilter && filters.length % 2 === 1 ? pageB : pageA;
|
|
3723
|
+
const attach = (views) => [
|
|
3724
|
+
{ view: views[0], clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" },
|
|
3725
|
+
{ view: views[1], clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" },
|
|
3726
|
+
];
|
|
3605
3727
|
let stamped = false;
|
|
3606
3728
|
for (let i = 0; i < Engine.FIELD_SCALES.length; i++) {
|
|
3607
|
-
const
|
|
3608
|
-
|
|
3609
|
-
if (!bg || !fg || !this.fieldPairUsed(i))
|
|
3729
|
+
const target = i === 0 ? plainFull : [this.fieldBgViews[i], this.fieldFgViews[i]];
|
|
3730
|
+
if (!target[0] || !target[1] || !this.fieldPairUsed(i))
|
|
3610
3731
|
continue;
|
|
3611
3732
|
const pass = encoder.beginRenderPass({
|
|
3612
3733
|
label: `field layer (${Engine.FIELD_SCALES[i] === 1 ? "full" : "half"})`,
|
|
3613
|
-
colorAttachments:
|
|
3614
|
-
{ view: bg, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" },
|
|
3615
|
-
{ view: fg, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" },
|
|
3616
|
-
],
|
|
3734
|
+
colorAttachments: attach(target),
|
|
3617
3735
|
// One query pair is reserved for "field", and it goes to the first pair
|
|
3618
3736
|
// that actually runs — full res when something declared #fullres, half
|
|
3619
3737
|
// otherwise. Pinning it to i === 0 would have measured a pass that, now
|
|
@@ -3622,7 +3740,7 @@ export class Engine {
|
|
|
3622
3740
|
});
|
|
3623
3741
|
stamped = true;
|
|
3624
3742
|
for (const e of drawn) {
|
|
3625
|
-
if (e.fieldLayer !== i)
|
|
3743
|
+
if (e.fieldLayer !== i || e.filter)
|
|
3626
3744
|
continue;
|
|
3627
3745
|
pass.setPipeline(e.fieldPipeline);
|
|
3628
3746
|
// The grid this effect just wrote — after its parity flip, the one at
|
|
@@ -3632,6 +3750,21 @@ export class Engine {
|
|
|
3632
3750
|
}
|
|
3633
3751
|
pass.end();
|
|
3634
3752
|
}
|
|
3753
|
+
if (!canFilter)
|
|
3754
|
+
return;
|
|
3755
|
+
// Each filter reads what the previous pass left and writes the other page.
|
|
3756
|
+
// The half pair goes to the first one, and only when something drew into
|
|
3757
|
+
// it this frame — an unused pair is never cleared and holds stale pixels.
|
|
3758
|
+
let read = plainFull;
|
|
3759
|
+
filters.forEach((e, k) => {
|
|
3760
|
+
const write = read === pageA ? pageB : pageA;
|
|
3761
|
+
const pass = encoder.beginRenderPass({ label: "field layer (filter)", colorAttachments: attach(write) });
|
|
3762
|
+
pass.setPipeline(e.fieldPipeline);
|
|
3763
|
+
pass.setBindGroup(0, e.fieldBindGroups[Engine.filterBindIndex(read === pageA ? 0 : 1, k === 0 && this.fieldPairUsed(1), e.grid?.parity ?? 0)]);
|
|
3764
|
+
pass.draw(3);
|
|
3765
|
+
pass.end();
|
|
3766
|
+
read = write;
|
|
3767
|
+
});
|
|
3635
3768
|
}
|
|
3636
3769
|
/** The trail bind group holds the depth view, which a resize recreates. */
|
|
3637
3770
|
rebindTrails() {
|
|
@@ -4570,6 +4703,11 @@ export class Engine {
|
|
|
4570
4703
|
// why it may not borrow the one at 18.
|
|
4571
4704
|
{ binding: 27, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
4572
4705
|
{ binding: 28, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
4706
|
+
// The other effects' layers, for a filter's rzSceneFrame (scene-tap.ts).
|
|
4707
|
+
{ binding: 29, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
4708
|
+
{ binding: 30, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
4709
|
+
{ binding: 31, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
4710
|
+
{ binding: 32, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
4573
4711
|
// THE VIEW TRANSFORM'S OWN RESOURCES. viewTransform() lives in the
|
|
4574
4712
|
// header both this module and the composite share, but its lookups did
|
|
4575
4713
|
// not: an effect calling it compiled and then failed at pipeline
|
|
@@ -10175,8 +10313,10 @@ export class Engine {
|
|
|
10175
10313
|
if (!inst)
|
|
10176
10314
|
return;
|
|
10177
10315
|
const target = this.selectedMaterial.materialName;
|
|
10178
|
-
|
|
10179
|
-
|
|
10316
|
+
// Every draw under the name — see materialIndexRanges for why there can be
|
|
10317
|
+
// more than one.
|
|
10318
|
+
const draws = inst.drawCalls.filter((d) => (d.type === "opaque" || d.type === "transparent") && d.materialName === target && this.shouldRenderDrawCall(inst, d));
|
|
10319
|
+
if (draws.length === 0)
|
|
10180
10320
|
return;
|
|
10181
10321
|
// Mask pass: fill the selected material's projected footprint with 1.0. Depth-always
|
|
10182
10322
|
// (no depth attachment) so the outline traces complete boundaries even when the
|
|
@@ -10189,7 +10329,8 @@ export class Engine {
|
|
|
10189
10329
|
mpass.setVertexBuffer(1, inst.jointsBuffer);
|
|
10190
10330
|
mpass.setVertexBuffer(2, inst.weightsBuffer);
|
|
10191
10331
|
mpass.setIndexBuffer(inst.indexBuffer, "uint32");
|
|
10192
|
-
|
|
10332
|
+
for (const draw of draws)
|
|
10333
|
+
mpass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
10193
10334
|
mpass.end();
|
|
10194
10335
|
// Edge pass: screen-space edge detect on the mask, alpha-blended over swapchain.
|
|
10195
10336
|
const edgeAttachment = this.selectionEdgePassDescriptor.colorAttachments[0];
|
|
@@ -10203,21 +10344,25 @@ export class Engine {
|
|
|
10203
10344
|
// Unique edges of the mesh, as a line-list index buffer. Each interior edge is
|
|
10204
10345
|
// shared by two triangles, so deduplicating halves both the buffer and the
|
|
10205
10346
|
// draw. Built once per model, on the first frame its wireframe is asked for.
|
|
10206
|
-
/** The index
|
|
10207
|
-
*
|
|
10208
|
-
* the
|
|
10209
|
-
*
|
|
10210
|
-
|
|
10347
|
+
/** The index runs `material` owns — EVERY run, because a PMX may carry
|
|
10348
|
+
* several materials under one name and a name is how the host asks; taking
|
|
10349
|
+
* the first left the rest of a hair or a coat out of its own wireframe —
|
|
10350
|
+
* or the whole list when it is null. Materials are consecutive runs in
|
|
10351
|
+
* declaration order, so each offset is a prefix sum, the same walk the draw
|
|
10352
|
+
* list does. Empty for a name the model does not have, which is what a
|
|
10353
|
+
* stale selection looks like after a reload. */
|
|
10354
|
+
materialIndexRanges(inst, material) {
|
|
10211
10355
|
const indices = inst.model.getIndices();
|
|
10212
10356
|
if (!material)
|
|
10213
|
-
return [0, indices.length];
|
|
10357
|
+
return [[0, indices.length]];
|
|
10358
|
+
const runs = [];
|
|
10214
10359
|
let offset = 0;
|
|
10215
10360
|
for (const m of inst.model.getMaterials()) {
|
|
10216
10361
|
if (m.name === material)
|
|
10217
|
-
|
|
10362
|
+
runs.push([offset, offset + m.vertexCount]);
|
|
10218
10363
|
offset += m.vertexCount;
|
|
10219
10364
|
}
|
|
10220
|
-
return
|
|
10365
|
+
return runs;
|
|
10221
10366
|
}
|
|
10222
10367
|
/**
|
|
10223
10368
|
* @param material one material's own edges, or null for the whole mesh
|
|
@@ -10263,10 +10408,9 @@ export class Engine {
|
|
|
10263
10408
|
}
|
|
10264
10409
|
}
|
|
10265
10410
|
else {
|
|
10266
|
-
const
|
|
10267
|
-
if (
|
|
10411
|
+
const runs = this.materialIndexRanges(inst, material);
|
|
10412
|
+
if (runs.length === 0)
|
|
10268
10413
|
return false;
|
|
10269
|
-
const [start, end] = range;
|
|
10270
10414
|
const seen = new Set();
|
|
10271
10415
|
const add = (a, b) => {
|
|
10272
10416
|
const lo = a < b ? a : b;
|
|
@@ -10277,10 +10421,12 @@ export class Engine {
|
|
|
10277
10421
|
seen.add(k);
|
|
10278
10422
|
edges.push(lo, hi);
|
|
10279
10423
|
};
|
|
10280
|
-
for (
|
|
10281
|
-
|
|
10282
|
-
|
|
10283
|
-
|
|
10424
|
+
for (const [start, end] of runs) {
|
|
10425
|
+
for (let i = start; i + 2 < end; i += 3) {
|
|
10426
|
+
add(indices[i], indices[i + 1]);
|
|
10427
|
+
add(indices[i + 1], indices[i + 2]);
|
|
10428
|
+
add(indices[i + 2], indices[i]);
|
|
10429
|
+
}
|
|
10284
10430
|
}
|
|
10285
10431
|
}
|
|
10286
10432
|
if (edges.length === 0) {
|
|
@@ -10332,8 +10478,8 @@ export class Engine {
|
|
|
10332
10478
|
// picked, since only the picked material draws at all then.
|
|
10333
10479
|
const hm = this.hoverMaterial;
|
|
10334
10480
|
const hoverName = material === null && hm?.modelName === inst.name ? hm.materialName : null;
|
|
10335
|
-
const
|
|
10336
|
-
const hover =
|
|
10481
|
+
const hoverRuns = hoverName ? this.materialIndexRanges(inst, hoverName) : [];
|
|
10482
|
+
const hover = hoverRuns.length > 0 && this.ensureEdgeBuffer(inst, hoverName) ? inst.wireEdges.get(hoverName) : null;
|
|
10337
10483
|
this.wireframeColorData.set(DEFAULT_VERTEX_COLOR);
|
|
10338
10484
|
this.wireframeColorData[4] = this.canvas.width;
|
|
10339
10485
|
this.wireframeColorData[5] = this.canvas.height;
|
|
@@ -10371,9 +10517,10 @@ export class Engine {
|
|
|
10371
10517
|
// before the base mesh below gets a chance to occlude it. The base mesh's
|
|
10372
10518
|
// depth write further down repeats the SAME geometry for these faces
|
|
10373
10519
|
// (identical z), so it neither disturbs this nor needs to skip them.
|
|
10374
|
-
if (hover &&
|
|
10520
|
+
if (hover && !xray) {
|
|
10375
10521
|
bindMesh();
|
|
10376
|
-
|
|
10522
|
+
for (const [start, end] of hoverRuns)
|
|
10523
|
+
pass.drawIndexed(end - start, 1, start);
|
|
10377
10524
|
pass.setBindGroup(0, this.wireframeHoverBindGroup);
|
|
10378
10525
|
pass.setBindGroup(1, hover.bindGroup);
|
|
10379
10526
|
pass.setPipeline(this.wireframePipeline);
|
|
@@ -10394,11 +10541,12 @@ export class Engine {
|
|
|
10394
10541
|
// the coat hide it does not answer that. Its own depth still goes in, so its
|
|
10395
10542
|
// back faces stay hidden and it reads as an object instead of a haze —
|
|
10396
10543
|
// which is the difference between this and turning x-ray on.
|
|
10397
|
-
const
|
|
10544
|
+
const runs = material !== null ? this.materialIndexRanges(inst, material) : null;
|
|
10398
10545
|
if (!xray) {
|
|
10399
10546
|
bindMesh();
|
|
10400
|
-
if (
|
|
10401
|
-
|
|
10547
|
+
if (runs)
|
|
10548
|
+
for (const [start, end] of runs)
|
|
10549
|
+
pass.drawIndexed(end - start, 1, start);
|
|
10402
10550
|
else
|
|
10403
10551
|
pass.drawIndexed(inst.model.getIndices().length);
|
|
10404
10552
|
}
|
|
@@ -12235,7 +12383,9 @@ export class Engine {
|
|
|
12235
12383
|
u[20] = v[2];
|
|
12236
12384
|
u[21] = v[6];
|
|
12237
12385
|
u[22] = v[10];
|
|
12238
|
-
|
|
12386
|
+
// A drawn filter consumed the half pair (renderFieldPass); the composite
|
|
12387
|
+
// skips it rather than laying it down a second time.
|
|
12388
|
+
u[23] = this.filtersDrawn() > 0 ? 1 : 0;
|
|
12239
12389
|
// Effect clock + canvas size (viewU[6]) — written on the same refresh.
|
|
12240
12390
|
// The effect clock, on the SCENE's time rather than the wall's.
|
|
12241
12391
|
//
|
|
@@ -64,6 +64,11 @@ type CompositeEffectSource = {
|
|
|
64
64
|
hasBackground: boolean;
|
|
65
65
|
/** Defines `fn foreground(...)` — mount over the finished frame. */
|
|
66
66
|
hasForeground: boolean;
|
|
67
|
+
/** Calls rzSceneFrame — a FILTER, which carries the layers it read forward
|
|
68
|
+
* (buildFieldShader) and so draws with blending off. */
|
|
69
|
+
filter?: boolean;
|
|
70
|
+
/** `#layer additive`: how a filter's own output joins the layers it carries. */
|
|
71
|
+
additiveLayer?: boolean;
|
|
67
72
|
/** Grid resolution when the effect declared `#grid`, else 0. */
|
|
68
73
|
gridSize: number;
|
|
69
74
|
/** Whether the scene pass carries the id attachment, so the field module can
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AA2BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAgC+B;AAC/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EAAE,CAIhG;AAID,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAEzG,KAAK,qBAAqB,GAAG;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,aAAa,EAAE,OAAO,CAAA;IACtB,oEAAoE;IACpE,aAAa,EAAE,OAAO,CAAA;IACtB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAA;IAChB;mFAC+E;IAC/E,GAAG,CAAC,EAAE,OAAO,CAAA;IACb;;;yEAGqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB;;8EAE0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;
|
|
1
|
+
{"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AA2BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAgC+B;AAC/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EAAE,CAIhG;AAID,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAEzG,KAAK,qBAAqB,GAAG;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,aAAa,EAAE,OAAO,CAAA;IACtB,oEAAoE;IACpE,aAAa,EAAE,OAAO,CAAA;IACtB;6DACyD;IACzD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAA;IAChB;mFAC+E;IAC/E,GAAG,CAAC,EAAE,OAAO,CAAA;IACb;;;yEAGqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB;;8EAE0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;AA8LD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,yqaA+H5B,CAAA;AA8MD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAQlF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAqGtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
|
|
@@ -117,7 +117,8 @@ override APPLY_GAMMA: bool = true;
|
|
|
117
117
|
// and no on/off uniform either: the pipeline is REBUILT per effect, so
|
|
118
118
|
// the compiled variant IS the flag.
|
|
119
119
|
// viewU[3] = (camera right, tanHalfFov·aspect); viewU[4] = (camera up, tanHalfFov);
|
|
120
|
-
// viewU[5] = (camera forward,
|
|
120
|
+
// viewU[5] = (camera forward, half pair consumed by a filter this frame) — refreshed
|
|
121
|
+
// per frame while skybox/effect active.
|
|
121
122
|
// viewU[6] = (time seconds, view transform id, canvas width, canvas height).
|
|
122
123
|
// viewU[7] = (grade offset.rgb, contrast); viewU[8] = (grade power.rgb, saturation);
|
|
123
124
|
// viewU[9] = (grade slope.rgb, grade on/off) — see grade() below.
|
|
@@ -573,7 +574,8 @@ const BACKGROUND_CALL = /* wgsl */ `
|
|
|
573
574
|
// straight form it replaced.
|
|
574
575
|
let bgFx = rzFieldMerge(
|
|
575
576
|
clamp(textureSampleLevel(fieldBgTex, bloomSamp, fragCoord.xy / fullSz, 0.0), vec4f(0.0), vec4f(1.0)),
|
|
576
|
-
|
|
577
|
+
// The half pair, unless a filter consumed it this frame (viewU[5].w).
|
|
578
|
+
clamp(textureSampleLevel(fieldBgHalfTex, bloomSamp, fragCoord.xy / fullSz, 0.0), vec4f(0.0), vec4f(1.0)) * (1.0 - viewU[5].w));
|
|
577
579
|
bgPm = bgFx.rgb + bgPm * (1.0 - bgFx.a);
|
|
578
580
|
bgA = bgFx.a + bgA * (1.0 - bgFx.a);
|
|
579
581
|
`;
|
|
@@ -584,7 +586,7 @@ const FOREGROUND_CALL = /* wgsl */ `
|
|
|
584
586
|
// Premultiplied, as the background layer above — same reason.
|
|
585
587
|
let fgFx = rzFieldMerge(
|
|
586
588
|
clamp(textureSampleLevel(fieldFgTex, bloomSamp, fragCoord.xy / fullSz, 0.0), vec4f(0.0), vec4f(1.0)),
|
|
587
|
-
clamp(textureSampleLevel(fieldFgHalfTex, bloomSamp, fragCoord.xy / fullSz, 0.0), vec4f(0.0), vec4f(1.0)));
|
|
589
|
+
clamp(textureSampleLevel(fieldFgHalfTex, bloomSamp, fragCoord.xy / fullSz, 0.0), vec4f(0.0), vec4f(1.0)) * (1.0 - viewU[5].w));
|
|
588
590
|
outRgb = fgFx.rgb + outRgb * (1.0 - fgFx.a);
|
|
589
591
|
outA = fgFx.a + outA * (1.0 - fgFx.a);
|
|
590
592
|
`;
|
|
@@ -635,6 +637,16 @@ export function buildFieldShader(effect) {
|
|
|
635
637
|
const fgLine = effect.hasForeground
|
|
636
638
|
? "out.fg = clamp(foreground(dir, uv, _rzFieldClock.x, linearDepth(vec2<i32>(min(fx, fullSz - 1.0)))), vec4f(0.0), vec4f(1.0));"
|
|
637
639
|
: "";
|
|
640
|
+
// A FILTER read the other effects' layers through rzSceneFrame and now
|
|
641
|
+
// carries them: its own output goes over (or adds to) what they drew, and
|
|
642
|
+
// the pipeline writes the result with blending off. Weight has already
|
|
643
|
+
// scaled its alpha, so a filter faded to nothing passes the layers through
|
|
644
|
+
// untouched.
|
|
645
|
+
const carry = effect.filter
|
|
646
|
+
? effect.additiveLayer
|
|
647
|
+
? "out.bg = _rzCarryAdd(out.bg, _rzLayerBg(uv));\n out.fg = _rzCarryAdd(out.fg, _rzLayerFg(uv));"
|
|
648
|
+
: "out.bg = _rzCarryOver(out.bg, _rzLayerBg(uv));\n out.fg = _rzCarryOver(out.fg, _rzLayerFg(uv));"
|
|
649
|
+
: "";
|
|
638
650
|
return (COMPOSITE_HEAD +
|
|
639
651
|
EFFECT_SCENE_API +
|
|
640
652
|
anchorAliasWgsl(effect.alias ?? []) +
|
|
@@ -656,7 +668,7 @@ export function buildFieldShader(effect) {
|
|
|
656
668
|
// texel is CAST_FIELD_DIV of them and the accessor scales on the way out.
|
|
657
669
|
// An author writes the width they mean and never learns how it is built.
|
|
658
670
|
castDistanceApi(0, 26, 18, CAST_FIELD_DIV) +
|
|
659
|
-
sceneTapApi(0, 27, 28) +
|
|
671
|
+
sceneTapApi(0, 27, 28, [29, 30, 31, 32]) +
|
|
660
672
|
"\n// ── user effect (setEffect) ──\n" +
|
|
661
673
|
effect.paramsDecl +
|
|
662
674
|
"\n" +
|
|
@@ -675,6 +687,15 @@ export function buildFieldShader(effect) {
|
|
|
675
687
|
*/
|
|
676
688
|
@group(0) @binding(22) var<uniform> _rzFieldClock: vec4f; // (time, weight, _, _)
|
|
677
689
|
|
|
690
|
+
/** A filter's own straight-alpha output over the premultiplied layers it read
|
|
691
|
+
* (the layer blend state, restated) and the additive form of the same. */
|
|
692
|
+
fn _rzCarryOver(own: vec4f, under: vec4f) -> vec4f {
|
|
693
|
+
return vec4f(own.rgb * own.a + under.rgb * (1.0 - own.a), own.a + under.a * (1.0 - own.a));
|
|
694
|
+
}
|
|
695
|
+
fn _rzCarryAdd(own: vec4f, under: vec4f) -> vec4f {
|
|
696
|
+
return vec4f(own.rgb * own.a + under.rgb, own.a + under.a);
|
|
697
|
+
}
|
|
698
|
+
|
|
678
699
|
@vertex fn fieldVs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
679
700
|
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
680
701
|
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
@@ -704,6 +725,7 @@ struct FieldOut {
|
|
|
704
725
|
// Scaling colour as well would fade as the square.
|
|
705
726
|
out.bg.a *= _rzFieldClock.y;
|
|
706
727
|
out.fg.a *= _rzFieldClock.y;
|
|
728
|
+
${carry}
|
|
707
729
|
return out;
|
|
708
730
|
}
|
|
709
731
|
`);
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* Clamp-to-edge is what stops a warped offset wrapping the far side of the
|
|
44
44
|
* screen into the picture.
|
|
45
45
|
*/
|
|
46
|
-
export declare function sceneTapApi(group: number, tex: number, samp: number): string;
|
|
46
|
+
export declare function sceneTapApi(group: number, tex: number, samp: number, layers: readonly [number, number, number, number]): string;
|
|
47
47
|
/**
|
|
48
48
|
* The same names, for the modules with no finished scene to give.
|
|
49
49
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scene-tap.d.ts","sourceRoot":"","sources":["../../src/shaders/scene-tap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,WAAW,
|
|
1
|
+
{"version":3,"file":"scene-tap.d.ts","sourceRoot":"","sources":["../../src/shaders/scene-tap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAChD,MAAM,CA6MR;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,kjBAW1B,CAAA"}
|
|
@@ -43,10 +43,36 @@
|
|
|
43
43
|
* Clamp-to-edge is what stops a warped offset wrapping the far side of the
|
|
44
44
|
* screen into the picture.
|
|
45
45
|
*/
|
|
46
|
-
export function sceneTapApi(group, tex, samp) {
|
|
46
|
+
export function sceneTapApi(group, tex, samp, layers) {
|
|
47
47
|
return /* wgsl */ `
|
|
48
48
|
@group(${group}) @binding(${tex}) var _rzSceneTex: texture_2d<f32>;
|
|
49
49
|
@group(${group}) @binding(${samp}) var _rzSceneSamp: sampler;
|
|
50
|
+
// The field layers as the other effects left them — see rzSceneFrame. Bound to
|
|
51
|
+
// the pair the others drew into for a filter, and to a 1x1 transparent for
|
|
52
|
+
// everything else: an effect cannot sample the pair it is drawing into.
|
|
53
|
+
@group(${group}) @binding(${layers[0]}) var _rzLayerBgFull: texture_2d<f32>;
|
|
54
|
+
@group(${group}) @binding(${layers[1]}) var _rzLayerFgFull: texture_2d<f32>;
|
|
55
|
+
@group(${group}) @binding(${layers[2]}) var _rzLayerBgHalf: texture_2d<f32>;
|
|
56
|
+
@group(${group}) @binding(${layers[3]}) var _rzLayerFgHalf: texture_2d<f32>;
|
|
57
|
+
|
|
58
|
+
/** Premultiplied OVER, top in front — the composite's rzFieldMerge, restated so
|
|
59
|
+
* this file keeps importing nothing. A resolution boundary is a layer
|
|
60
|
+
* boundary: full res over half, exactly as the composite reads them. */
|
|
61
|
+
fn _rzLayerOver(top: vec4f, bot: vec4f) -> vec4f {
|
|
62
|
+
return vec4f(top.rgb + bot.rgb * (1.0 - top.a), top.a + bot.a * (1.0 - top.a));
|
|
63
|
+
}
|
|
64
|
+
fn _rzLayerBg(uv: vec2f) -> vec4f {
|
|
65
|
+
let p = _rzTapUv(uv);
|
|
66
|
+
return _rzLayerOver(
|
|
67
|
+
clamp(textureSampleLevel(_rzLayerBgFull, _rzSceneSamp, p, 0.0), vec4f(0.0), vec4f(1.0)),
|
|
68
|
+
clamp(textureSampleLevel(_rzLayerBgHalf, _rzSceneSamp, p, 0.0), vec4f(0.0), vec4f(1.0)));
|
|
69
|
+
}
|
|
70
|
+
fn _rzLayerFg(uv: vec2f) -> vec4f {
|
|
71
|
+
let p = _rzTapUv(uv);
|
|
72
|
+
return _rzLayerOver(
|
|
73
|
+
clamp(textureSampleLevel(_rzLayerFgFull, _rzSceneSamp, p, 0.0), vec4f(0.0), vec4f(1.0)),
|
|
74
|
+
clamp(textureSampleLevel(_rzLayerFgHalf, _rzSceneSamp, p, 0.0), vec4f(0.0), vec4f(1.0)));
|
|
75
|
+
}
|
|
50
76
|
|
|
51
77
|
/** uv as the mount gives it (y=0 at the BOTTOM, the shadertoy convention the
|
|
52
78
|
* composite documents) turned into a texture coordinate (y=0 at the top).
|
|
@@ -175,25 +201,40 @@ fn rzSceneDisplay(uv: vec2f) -> vec3f {
|
|
|
175
201
|
}
|
|
176
202
|
|
|
177
203
|
/**
|
|
178
|
-
* The
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
204
|
+
* The frame as it stands: the scene over its background, and both layers of
|
|
205
|
+
* every other field effect, laid down in the order the composite would lay
|
|
206
|
+
* them. One call, and the answer already carries the ground's fade, its
|
|
207
|
+
* opacity, the background showing through both, and whatever the other
|
|
208
|
+
* effects drew — a waveform, a caption, a glow.
|
|
209
|
+
*
|
|
210
|
+
* CALLING THIS MAKES THE EFFECT A FILTER. A filter runs after every other
|
|
211
|
+
* field effect, at full resolution, and what it returns replaces the layers it
|
|
212
|
+
* read: its own output is composed over them in its pass (buildFieldShader),
|
|
213
|
+
* so nothing drawn before it is lost and nothing is drawn twice. Two filters
|
|
214
|
+
* run in document order, each reading the one before. The layers are a
|
|
215
|
+
* pass-order fact — a filter reads the pair the others drew into while it
|
|
216
|
+
* draws into a page of its own (renderFieldPass).
|
|
217
|
+
*
|
|
218
|
+
* This is what a FILTER wants — anything re-rendering the whole frame on a
|
|
219
|
+
* tube, a low-resolution screen, or a card. Reaching for rzSceneDisplay and a
|
|
220
|
+
* depth test instead rebuilds this by hand and gets it wrong in one specific
|
|
221
|
+
* way every time: coverage is not a yes or no, and treating it as one turns
|
|
222
|
+
* every soft edge in the scene into a hard one.
|
|
223
|
+
*
|
|
224
|
+
* NOT INCLUDED: depth of field, and an equirect background (a 360 dome reads
|
|
225
|
+
* as its average rather than its texture).
|
|
191
226
|
*/
|
|
192
227
|
fn rzSceneFrame(uv: vec2f) -> vec3f {
|
|
193
228
|
let a = clamp(rzSceneAlpha(uv), 0.0, 1.0);
|
|
194
229
|
let bg = viewU[2];
|
|
195
|
-
let
|
|
196
|
-
|
|
230
|
+
let bgA = select(0.0, 1.0, bg.w > 0.5);
|
|
231
|
+
// The background layer over the base, the scene over both, the foreground
|
|
232
|
+
// layer over the frame — the composite's own order.
|
|
233
|
+
let lb = _rzLayerBg(uv);
|
|
234
|
+
let under = lb.rgb + bg.rgb * bgA * (1.0 - lb.a);
|
|
235
|
+
let frame = rzSceneDisplay(uv) * a + under * (1.0 - a);
|
|
236
|
+
let lf = _rzLayerFg(uv);
|
|
237
|
+
return lf.rgb + frame * (1.0 - lf.a);
|
|
197
238
|
}
|
|
198
239
|
|
|
199
240
|
/**
|