reze-engine 0.64.0 → 0.65.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/dist/engine.d.ts +146 -54
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +734 -582
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/model.d.ts +10 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +15 -1
- package/dist/outline-normals.d.ts +12 -0
- package/dist/outline-normals.d.ts.map +1 -0
- package/dist/outline-normals.js +100 -0
- package/dist/pmx-loader.d.ts +3 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +7 -2
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +4 -2
- package/dist/shaders/passes/bloom.d.ts +4 -3
- package/dist/shaders/passes/bloom.d.ts.map +1 -1
- package/dist/shaders/passes/bloom.js +58 -87
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +118 -72
- package/dist/shaders/passes/mirror.d.ts +18 -12
- package/dist/shaders/passes/mirror.d.ts.map +1 -1
- package/dist/shaders/passes/mirror.js +59 -13
- package/dist/shaders/passes/outline.d.ts.map +1 -1
- package/dist/shaders/passes/outline.js +71 -14
- package/dist/unity/host.d.ts +15 -0
- package/dist/unity/host.d.ts.map +1 -1
- package/dist/unity/host.js +103 -63
- package/dist/unity/looks.d.ts +5 -0
- package/dist/unity/looks.d.ts.map +1 -1
- package/dist/unity/looks.js +19 -2
- package/dist/unity/skin.d.ts.map +1 -1
- package/dist/unity/skin.js +5 -2
- package/dist/unity/stage.d.ts +7 -0
- package/dist/unity/stage.d.ts.map +1 -1
- package/dist/unity/stage.js +37 -2
- package/package.json +1 -1
- package/src/engine.ts +1042 -798
- package/src/graph/slots.ts +7 -1
- package/src/index.ts +1 -0
- package/src/model.ts +18 -1
- package/src/outline-normals.ts +110 -0
- package/src/pmx-loader.ts +8 -2
- package/src/shaders/materials/common.ts +4 -2
- package/src/shaders/passes/bloom.ts +59 -87
- package/src/shaders/passes/composite.ts +118 -72
- package/src/shaders/passes/mirror.ts +60 -13
- package/src/shaders/passes/outline.ts +71 -14
- package/src/unity/host.ts +57 -14
- package/src/unity/looks.ts +21 -2
- package/src/unity/skin.ts +5 -2
- package/src/unity/stage.ts +38 -2
- package/src/shaders/agx-lut.ts +0 -30
package/dist/engine.js
CHANGED
|
@@ -29,7 +29,7 @@ import { gameDir, unityFrameGlobals } from "./unity/globals";
|
|
|
29
29
|
import { outlineMaxOffsetMultiplier, towardMatrix } from "./unity/character";
|
|
30
30
|
import { SHADOW_ATLAS_SIZE, SHADOW_CASCADES, buildShadowCascades, cascadeSpheres } from "./shadow-cascades";
|
|
31
31
|
import { REFLECTION_DEBUG_WGSL, buildMirrorCamera, planeFromPointNormal } from "./reflection";
|
|
32
|
-
import { MIRROR_MASK_DOWNSAMPLE_WGSL, MIRROR_MAT_BYTES, mirrorShaderWgsl, mirrorShadowWgsl } from "./shaders/passes/mirror";
|
|
32
|
+
import { MIRROR_DOWNSAMPLE_WGSL, MIRROR_MASK_DOWNSAMPLE_WGSL, MIRROR_MAT_BYTES, mirrorShaderWgsl, mirrorShadowWgsl, } from "./shaders/passes/mirror";
|
|
33
33
|
import { packHalf } from "./hdr";
|
|
34
34
|
import { evalIrradianceSH, projectIrradianceSH, gradientIrradianceSH } from "./ibl";
|
|
35
35
|
import { LYRIC_ATLAS_MAX_H, LYRIC_ATLAS_MAX_W, LYRICS_FLOATS, packLyrics } from "./shaders/lyrics-api";
|
|
@@ -44,8 +44,7 @@ import { GIZMO_SHADER_WGSL } from "./shaders/passes/gizmo";
|
|
|
44
44
|
import { OVERLAY_SHADER_WGSL, OVERLAY_COMPOSITE_SHADER_WGSL } from "./shaders/passes/overlay";
|
|
45
45
|
import { WIREFRAME_SHADER_WGSL } from "./shaders/passes/wireframe";
|
|
46
46
|
import { boneOverlay, boneMarkerPositions, buildOverlayShapes, jointOverlay, lineBetween, rigidbodyOverlay, writeOverlayInstance, OVERLAY_INSTANCE_FLOATS, OVERLAY_VERTEX_FLOATS, OVERLAY_SHAPES, OVERLAY_SOLID_SHAPES, DEFAULT_VERTEX_COLOR, OVERLAY_STYLE, } from "./overlay";
|
|
47
|
-
import {
|
|
48
|
-
import { AGX_LUT_GZ, AGX_LUT_SIZE } from "./shaders/agx-lut";
|
|
47
|
+
import { BLOOM_BLUR_H_SHADER_WGSL, BLOOM_BLUR_V_SHADER_WGSL, BLOOM_PREFILTER_SHADER_WGSL, BLOOM_UPSAMPLE_SHADER_WGSL, } from "./shaders/passes/bloom";
|
|
49
48
|
import { buildCompositeShader, EFFECT_SCENE_API, buildFieldShader, EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES, } from "./shaders/passes/composite";
|
|
50
49
|
import { SUBSURFACE_WGSL, SSS_SPIKE } from "./shaders/passes/subsurface";
|
|
51
50
|
import { buildParticleComputeShader, buildParticleRenderShader, PARTICLE_LIGHT_BINDING, PARTICLE_POINTS_BINDING, PARTICLE_TEXTURE_BINDING, PARTICLE_TEXTURE_SAMPLER_BINDING, particleEntryPoints, PARTICLE_INDIRECT_BINDING, PARTICLE_INDIRECT_BYTES, PARTICLE_INDIRECT_DRAW_OFFSET, PARTICLE_STRIDE, } from "./shaders/passes/particles";
|
|
@@ -228,12 +227,10 @@ const CAST_TRAIL_BASE = EFFECT_TRAIL_BASE;
|
|
|
228
227
|
const CAST_VEC4S = CAST_TRAIL_BASE + MAX_EFFECT_ANCHORS * MAX_EFFECT_SUBJECTS * TRAIL_SAMPLES;
|
|
229
228
|
export const DEFAULT_BLOOM_OPTIONS = {
|
|
230
229
|
enabled: true,
|
|
231
|
-
threshold: 0.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
intensity: 0.05,
|
|
236
|
-
clamp: 0.0,
|
|
230
|
+
threshold: 0.7,
|
|
231
|
+
scatter: 0.77,
|
|
232
|
+
color: new Vec3(1, 1, 1),
|
|
233
|
+
intensity: 1,
|
|
237
234
|
};
|
|
238
235
|
export const DEFAULT_DEPTH_OF_FIELD_OPTIONS = {
|
|
239
236
|
enabled: false,
|
|
@@ -245,12 +242,10 @@ export const DEFAULT_DEPTH_OF_FIELD_OPTIONS = {
|
|
|
245
242
|
bladeCount: 6,
|
|
246
243
|
quality: "balanced",
|
|
247
244
|
};
|
|
248
|
-
// Matches the reference Blender project: Filmic view, Medium High Contrast look,
|
|
249
|
-
// exposure 0.3, gamma 1.0, sRGB display, no curves.
|
|
250
245
|
export const DEFAULT_VIEW_TRANSFORM = {
|
|
251
|
-
exposure: 0
|
|
246
|
+
exposure: 0,
|
|
252
247
|
gamma: 1.0,
|
|
253
|
-
transform: "
|
|
248
|
+
transform: "soft",
|
|
254
249
|
};
|
|
255
250
|
const NEUTRAL_GRADE_CHANNEL = 0.5;
|
|
256
251
|
export const DEFAULT_COLOR_GRADING = {
|
|
@@ -1009,6 +1004,7 @@ export class Engine {
|
|
|
1009
1004
|
* premultiplied quantity and a blur has to move them together. */
|
|
1010
1005
|
this.mirrorMaskMipViews = [];
|
|
1011
1006
|
this.mirrorMaskBlurBindGroups = null;
|
|
1007
|
+
this.mirrorDownsamplePipeline = null;
|
|
1012
1008
|
this.mirrorMaskDownsamplePipeline = null;
|
|
1013
1009
|
this.groundClipData = new Float32Array(8);
|
|
1014
1010
|
this.mirrorIdMsTexture = null;
|
|
@@ -1173,7 +1169,6 @@ export class Engine {
|
|
|
1173
1169
|
/** Trail samples owed this frame, computed once so every trail on every
|
|
1174
1170
|
* character samples in lockstep and their paths stay comparable. */
|
|
1175
1171
|
this.trailDue = 0;
|
|
1176
|
-
this.agxLutTexture = null;
|
|
1177
1172
|
/** The scene's own grade (setStageGrade): the cube as handed over, kept so a
|
|
1178
1173
|
* call before init() still lands, and its texture once uploaded. */
|
|
1179
1174
|
this.stageGradeCube = null;
|
|
@@ -1198,18 +1193,42 @@ export class Engine {
|
|
|
1198
1193
|
this.pipelineDescs = new WeakMap();
|
|
1199
1194
|
this.singleSided = new WeakMap();
|
|
1200
1195
|
this.singleSidedMirror = new WeakMap();
|
|
1196
|
+
/** Twins whose async build is in flight, so a draw asks for each once. */
|
|
1197
|
+
this.sidedPending = new WeakSet();
|
|
1198
|
+
this.sidedPendingMirror = new WeakSet();
|
|
1199
|
+
// The engine-wide shader cache (see cachedRenderPipeline). Modules by WGSL
|
|
1200
|
+
// source, pipelines by module + descriptor state: one compile per source,
|
|
1201
|
+
// whoever asks — a graph on two models, an effect reinstalled, the composite.
|
|
1202
|
+
this.shaderModuleCache = new Map();
|
|
1203
|
+
this.pipelineCache = new Map();
|
|
1204
|
+
this.bindGroupLayoutCache = new Map();
|
|
1205
|
+
this.pipelineLayoutCache = new Map();
|
|
1206
|
+
this.gpuObjectIds = new WeakMap();
|
|
1207
|
+
this.gpuObjectNext = 1;
|
|
1208
|
+
/** Init's pipelines, built async and in parallel; init awaits them. Null
|
|
1209
|
+
* outside init, where a pipeline job builds the way it always did. */
|
|
1210
|
+
this.initPipelineJobs = null;
|
|
1201
1211
|
// Stepped motion (#stepped). The models whose pose is being HELD this frame —
|
|
1202
1212
|
// their skinning and morph uploads wait for the stepped clock's next tick —
|
|
1203
1213
|
// and the tick each was last shown at. Empty while no effect declares it.
|
|
1204
1214
|
this.steppedHeld = new Set();
|
|
1205
1215
|
this.steppedTick = new Map();
|
|
1206
1216
|
this.steppedLastClock = -1;
|
|
1207
|
-
this.
|
|
1208
|
-
this.
|
|
1209
|
-
this.
|
|
1210
|
-
this.
|
|
1211
|
-
this.
|
|
1212
|
-
this.
|
|
1217
|
+
this.bloomPrefilterPipeline = null;
|
|
1218
|
+
this.bloomBlurHPipeline = null;
|
|
1219
|
+
this.bloomBlurVPipeline = null;
|
|
1220
|
+
this.bloomUpsamplePipeline = null;
|
|
1221
|
+
this.bloomUpsampleBindGroupLayout = null;
|
|
1222
|
+
this.bloomUniformBuffer = null;
|
|
1223
|
+
this.bloomUniformData = new Float32Array(4);
|
|
1224
|
+
this.bloomDownTexture = null;
|
|
1225
|
+
this.bloomUpTexture = null;
|
|
1226
|
+
this.bloomLevels = 0;
|
|
1227
|
+
this.bloomDownViews = [];
|
|
1228
|
+
this.bloomUpViews = [];
|
|
1229
|
+
this.bloomPrefilterBindGroup = null;
|
|
1230
|
+
this.bloomBlurHBindGroups = [];
|
|
1231
|
+
this.bloomBlurVBindGroups = [];
|
|
1213
1232
|
this.bloomUpsampleBindGroups = [];
|
|
1214
1233
|
this.hasGround = false;
|
|
1215
1234
|
/** The user's own "no ground" switch — see setGroundVisible. Distinct from
|
|
@@ -1668,18 +1687,16 @@ export class Engine {
|
|
|
1668
1687
|
const bg = options?.background;
|
|
1669
1688
|
this.backgroundColor = bg ? new Vec3(bg.x, bg.y, bg.z) : null;
|
|
1670
1689
|
}
|
|
1671
|
-
/** Merge partial bloom with
|
|
1690
|
+
/** Merge partial bloom with the defaults (same as constructor). */
|
|
1672
1691
|
static mergeBloomDefaults(partial) {
|
|
1673
1692
|
const d = DEFAULT_BLOOM_OPTIONS;
|
|
1674
1693
|
const c = partial?.color;
|
|
1675
1694
|
return {
|
|
1676
1695
|
enabled: partial?.enabled ?? d.enabled,
|
|
1677
1696
|
threshold: partial?.threshold ?? d.threshold,
|
|
1678
|
-
|
|
1679
|
-
radius: partial?.radius ?? d.radius,
|
|
1697
|
+
scatter: partial?.scatter ?? d.scatter,
|
|
1680
1698
|
color: c ? new Vec3(c.x, c.y, c.z) : new Vec3(d.color.x, d.color.y, d.color.z),
|
|
1681
1699
|
intensity: partial?.intensity ?? d.intensity,
|
|
1682
|
-
clamp: partial?.clamp ?? d.clamp,
|
|
1683
1700
|
};
|
|
1684
1701
|
}
|
|
1685
1702
|
static mergeViewTransformDefaults(partial) {
|
|
@@ -1690,17 +1707,15 @@ export class Engine {
|
|
|
1690
1707
|
transform: partial?.transform ?? d.transform,
|
|
1691
1708
|
};
|
|
1692
1709
|
}
|
|
1693
|
-
/** Current bloom settings (
|
|
1710
|
+
/** Current bloom settings (tint is a copied `Vec3`). */
|
|
1694
1711
|
getBloomOptions() {
|
|
1695
1712
|
const b = this.bloomSettings;
|
|
1696
1713
|
return {
|
|
1697
1714
|
enabled: b.enabled,
|
|
1698
1715
|
threshold: b.threshold,
|
|
1699
|
-
|
|
1700
|
-
radius: b.radius,
|
|
1716
|
+
scatter: b.scatter,
|
|
1701
1717
|
color: new Vec3(b.color.x, b.color.y, b.color.z),
|
|
1702
1718
|
intensity: b.intensity,
|
|
1703
|
-
clamp: b.clamp,
|
|
1704
1719
|
};
|
|
1705
1720
|
}
|
|
1706
1721
|
getViewTransformOptions() {
|
|
@@ -1888,7 +1903,7 @@ export class Engine {
|
|
|
1888
1903
|
// rebuilt per effect, so the compiled variant IS the flag.
|
|
1889
1904
|
u[11] = this.backdropEquirectView ? 2 : showingWorld ? 3 : bg ? 1 : 0;
|
|
1890
1905
|
// Which display transform forms the frame (see viewTransform in composite.ts).
|
|
1891
|
-
u[25] =
|
|
1906
|
+
u[25] = Engine.VIEW_TRANSFORM_ID[v.transform] ?? 0;
|
|
1892
1907
|
u[26] = this.canvas.width;
|
|
1893
1908
|
u[27] = this.canvas.height;
|
|
1894
1909
|
// ── Grade (viewU[7..9]) ── The UI's three tonal COLORS map to ASC CDL here,
|
|
@@ -2533,7 +2548,7 @@ export class Engine {
|
|
|
2533
2548
|
this.renderMirrorBlurChain(encoder);
|
|
2534
2549
|
}
|
|
2535
2550
|
/**
|
|
2536
|
-
* Fill the mirror's mip levels —
|
|
2551
|
+
* Fill the mirror's mip levels — a 13-tap downsample (MIRROR_DOWNSAMPLE_WGSL),
|
|
2537
2552
|
* one pass per level. Only when the blur dial is up: at zero the ground
|
|
2538
2553
|
* samples level 0 exactly and the chain would be work nobody reads.
|
|
2539
2554
|
*/
|
|
@@ -2544,8 +2559,19 @@ export class Engine {
|
|
|
2544
2559
|
const blur = Math.max(this.groundMirrorBlur, this.mirrorSurface?.blur ?? 0);
|
|
2545
2560
|
if (blur <= 0 || this.mirrorMipCount < 2)
|
|
2546
2561
|
return;
|
|
2562
|
+
if (!this.mirrorDownsamplePipeline) {
|
|
2563
|
+
const module = this.device.createShaderModule({ label: "mirror downsample", code: MIRROR_DOWNSAMPLE_WGSL });
|
|
2564
|
+
this.mirrorDownsamplePipeline = this.device.createRenderPipeline({
|
|
2565
|
+
label: "mirror downsample pipeline",
|
|
2566
|
+
layout: "auto",
|
|
2567
|
+
vertex: { module, entryPoint: "vs" },
|
|
2568
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: this.hdrFormat }] },
|
|
2569
|
+
primitive: { topology: "triangle-list" },
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
const downsample = this.mirrorDownsamplePipeline;
|
|
2547
2573
|
if (!this.mirrorBlurBindGroups) {
|
|
2548
|
-
const layout =
|
|
2574
|
+
const layout = downsample.getBindGroupLayout(0);
|
|
2549
2575
|
this.mirrorBlurBindGroups = [];
|
|
2550
2576
|
for (let i = 1; i < this.mirrorMipCount; i++) {
|
|
2551
2577
|
this.mirrorBlurBindGroups.push(this.device.createBindGroup({
|
|
@@ -2563,7 +2589,7 @@ export class Engine {
|
|
|
2563
2589
|
label: `mirror blur ${i}`,
|
|
2564
2590
|
colorAttachments: [{ view: this.mirrorMipViews[i], loadOp: "clear", storeOp: "store" }],
|
|
2565
2591
|
});
|
|
2566
|
-
p.setPipeline(
|
|
2592
|
+
p.setPipeline(downsample);
|
|
2567
2593
|
p.setBindGroup(0, this.mirrorBlurBindGroups[i - 1]);
|
|
2568
2594
|
p.draw(3);
|
|
2569
2595
|
p.end();
|
|
@@ -2716,7 +2742,6 @@ export class Engine {
|
|
|
2716
2742
|
{ binding: 2, resource: this.bloomSampler },
|
|
2717
2743
|
{ binding: 3, resource: { buffer: this.compositeUniformBuffer } },
|
|
2718
2744
|
{ binding: 4, resource: this.maskResolveView },
|
|
2719
|
-
{ binding: 5, resource: this.filmicLutView },
|
|
2720
2745
|
// Whichever equirect is SHOWING — the backdrop if there is one, the
|
|
2721
2746
|
// world otherwise. The world's light does not come through here; it
|
|
2722
2747
|
// rides worldSH into the material shells.
|
|
@@ -2724,7 +2749,6 @@ export class Engine {
|
|
|
2724
2749
|
{ binding: 7, resource: { buffer: this.effect?.paramsBuffer ?? this.bgParamsDummyBuffer } },
|
|
2725
2750
|
{ binding: 8, resource: this.depthReadView },
|
|
2726
2751
|
{ binding: 9, resource: { buffer: this.dofUniformBuffer } },
|
|
2727
|
-
{ binding: 10, resource: (this.agxLutTexture ?? this.agxFallbackTexture).createView({ dimension: "3d" }) },
|
|
2728
2752
|
{ binding: 12, resource: (this.stageGradeTexture ?? this.stageGradeFallback).createView({ dimension: "3d" }) },
|
|
2729
2753
|
{ binding: 11, resource: { buffer: this.castBuffer } },
|
|
2730
2754
|
{ binding: 13, resource: { buffer: this.audioBuffer } },
|
|
@@ -3148,8 +3172,6 @@ export class Engine {
|
|
|
3148
3172
|
{ binding: 31, resource: layers[2] },
|
|
3149
3173
|
{ binding: 32, resource: layers[3] },
|
|
3150
3174
|
{ binding: 2, resource: this.bloomSampler },
|
|
3151
|
-
{ binding: 5, resource: this.filmicLutView },
|
|
3152
|
-
{ binding: 10, resource: (this.agxLutTexture ?? this.agxFallbackTexture).createView({ dimension: "3d" }) },
|
|
3153
3175
|
{ binding: 12, resource: (this.stageGradeTexture ?? this.stageGradeFallback).createView({ dimension: "3d" }) },
|
|
3154
3176
|
{ binding: 1, resource: bloom },
|
|
3155
3177
|
{ binding: 4, resource: this.maskResolveView },
|
|
@@ -3400,8 +3422,20 @@ export class Engine {
|
|
|
3400
3422
|
if (this.device && this.compositeUniformBuffer)
|
|
3401
3423
|
this.writeCompositeViewUniforms();
|
|
3402
3424
|
}
|
|
3403
|
-
|
|
3404
|
-
|
|
3425
|
+
/**
|
|
3426
|
+
* The scene composite's pair (gamma = 1, gamma ≠ 1), async and from the
|
|
3427
|
+
* shader cache. Its source depends only on which field mounts the scene
|
|
3428
|
+
* samples, so there are four at most, and an effect install that keeps the
|
|
3429
|
+
* mounts it had gets the pair already built.
|
|
3430
|
+
*/
|
|
3431
|
+
compositePipelines(hasBackground, hasForeground) {
|
|
3432
|
+
const code = buildCompositeShader(hasBackground || hasForeground
|
|
3433
|
+
? // No wgsl and so no trails: the composite hosts no effect source at
|
|
3434
|
+
// all, it only decides whether to sample the layer the field pass drew.
|
|
3435
|
+
{ wgsl: "", paramsDecl: "", hasBackground, hasForeground, gridSize: 0, trailCount: 0 }
|
|
3436
|
+
: null);
|
|
3437
|
+
const module = this.cachedShaderModule(code, "composite shader");
|
|
3438
|
+
const make = (applyGamma, label) => this.cachedRenderPipeline({
|
|
3405
3439
|
label,
|
|
3406
3440
|
layout: this.compositePipelineLayout,
|
|
3407
3441
|
vertex: { module, entryPoint: "vs" },
|
|
@@ -3413,6 +3447,7 @@ export class Engine {
|
|
|
3413
3447
|
},
|
|
3414
3448
|
primitive: { topology: "triangle-list" },
|
|
3415
3449
|
});
|
|
3450
|
+
return Promise.all([make(false, "composite pipeline (gamma=1)"), make(true, "composite pipeline (gamma!=1)")]);
|
|
3416
3451
|
}
|
|
3417
3452
|
/**
|
|
3418
3453
|
* Install the scene's WGSL effect (shadertoy-style), rendered per-pixel in the
|
|
@@ -3641,12 +3676,11 @@ export class Engine {
|
|
|
3641
3676
|
const fieldEffect = hasBackground || hasForeground
|
|
3642
3677
|
? { wgsl, paramsDecl, hasBackground, hasForeground, gridSize, alias, trailCount: anchors.filter((a) => a.trail).length, filter, additiveLayer: d.additiveLayer }
|
|
3643
3678
|
: null;
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
return { ok: false, diagnostics: [scopeErr.message], mounts, params: d.params, duration: d.duration, readsCast: false };
|
|
3679
|
+
// No composite is built here. One used to be — a module and two pipelines
|
|
3680
|
+
// per effect, then dropped: the composite is static (no user code reaches
|
|
3681
|
+
// it) and setEffects builds the scene's one after the list is in. Those
|
|
3682
|
+
// two compiles were most of a second each and validated nothing an author
|
|
3683
|
+
// wrote.
|
|
3650
3684
|
// Declared like every other mount property: by what the source says, not by
|
|
3651
3685
|
// a setting somewhere else that an author cannot see from the file.
|
|
3652
3686
|
const layerBlend = d.additiveLayer
|
|
@@ -3657,73 +3691,56 @@ export class Engine {
|
|
|
3657
3691
|
const fieldSource = buildFieldShader({ ...fieldEffect, ids: mrtIdsEnabled() });
|
|
3658
3692
|
const userLineOffset = fieldSource.slice(0, fieldSource.indexOf(wgsl)).split("\n").length - 1;
|
|
3659
3693
|
this.device.pushErrorScope("validation");
|
|
3660
|
-
const fieldModule = this.
|
|
3661
|
-
const
|
|
3662
|
-
|
|
3694
|
+
const fieldModule = this.cachedShaderModule(fieldSource, "field shader (effect)");
|
|
3695
|
+
const fieldScope = this.device.popErrorScope();
|
|
3696
|
+
// Started before the diagnostics are read, which only decide whether it
|
|
3697
|
+
// is kept: the compile and the check overlap.
|
|
3698
|
+
const fieldBuild = this.cachedRenderPipeline({
|
|
3699
|
+
label: "field layer pipeline",
|
|
3700
|
+
layout: this.fieldPipelineLayout,
|
|
3701
|
+
vertex: { module: fieldModule, entryPoint: "fieldVs" },
|
|
3702
|
+
fragment: {
|
|
3703
|
+
module: fieldModule,
|
|
3704
|
+
entryPoint: "fieldFs",
|
|
3705
|
+
// OVER, accumulating PREMULTIPLIED colour: several effects draw into
|
|
3706
|
+
// these two targets in document order, and each must layer onto what
|
|
3707
|
+
// the earlier ones left rather than replace it. src-alpha on colour
|
|
3708
|
+
// premultiplies as it writes; alpha accumulates as one-over. An
|
|
3709
|
+
// author still returns STRAIGHT colour+alpha, exactly as before —
|
|
3710
|
+
// the premultiplication happens here, and the composite reads it
|
|
3711
|
+
// back knowing that. With one effect over a cleared target the
|
|
3712
|
+
// result is identical to the replace it used to do.
|
|
3713
|
+
// A filter composes over the layers in its own shader and writes
|
|
3714
|
+
// the result outright: blending would layer it onto a page that
|
|
3715
|
+
// holds nothing.
|
|
3716
|
+
targets: filter
|
|
3717
|
+
? [{ format: "rgba16float" }, { format: "rgba16float" }]
|
|
3718
|
+
: [
|
|
3719
|
+
{ format: "rgba16float", blend: layerBlend },
|
|
3720
|
+
{ format: "rgba16float", blend: layerBlend },
|
|
3721
|
+
],
|
|
3722
|
+
},
|
|
3723
|
+
primitive: { topology: "triangle-list" },
|
|
3724
|
+
multisample: { count: 1 },
|
|
3725
|
+
});
|
|
3726
|
+
fieldBuild.catch(() => { });
|
|
3727
|
+
const [info, fieldScopeErr] = await Promise.all([fieldModule.getCompilationInfo(), fieldScope]);
|
|
3663
3728
|
const diagnostics = info.messages
|
|
3664
3729
|
.filter((m) => m.type === "error")
|
|
3665
3730
|
.map((m) => `${Math.max(0, m.lineNum - userLineOffset)}:${m.linePos} ${m.message}`);
|
|
3666
3731
|
if (diagnostics.length === 0 && fieldScopeErr)
|
|
3667
3732
|
diagnostics.push(fieldScopeErr.message);
|
|
3668
|
-
if (diagnostics.length > 0)
|
|
3733
|
+
if (diagnostics.length > 0) {
|
|
3734
|
+
this.forgetShaderModule(fieldSource);
|
|
3669
3735
|
return { ok: false, diagnostics, mounts, params: d.params, duration: d.duration, readsCast: false };
|
|
3736
|
+
}
|
|
3670
3737
|
try {
|
|
3671
|
-
fieldPipeline = await
|
|
3672
|
-
label: "field layer pipeline",
|
|
3673
|
-
layout: this.fieldPipelineLayout,
|
|
3674
|
-
vertex: { module: fieldModule, entryPoint: "fieldVs" },
|
|
3675
|
-
fragment: {
|
|
3676
|
-
module: fieldModule,
|
|
3677
|
-
entryPoint: "fieldFs",
|
|
3678
|
-
// OVER, accumulating PREMULTIPLIED colour: several effects draw into
|
|
3679
|
-
// these two targets in document order, and each must layer onto what
|
|
3680
|
-
// the earlier ones left rather than replace it. src-alpha on colour
|
|
3681
|
-
// premultiplies as it writes; alpha accumulates as one-over. An
|
|
3682
|
-
// author still returns STRAIGHT colour+alpha, exactly as before —
|
|
3683
|
-
// the premultiplication happens here, and the composite reads it
|
|
3684
|
-
// back knowing that. With one effect over a cleared target the
|
|
3685
|
-
// result is identical to the replace it used to do.
|
|
3686
|
-
// A filter composes over the layers in its own shader and writes
|
|
3687
|
-
// the result outright: blending would layer it onto a page that
|
|
3688
|
-
// holds nothing.
|
|
3689
|
-
targets: filter
|
|
3690
|
-
? [{ format: "rgba16float" }, { format: "rgba16float" }]
|
|
3691
|
-
: [
|
|
3692
|
-
{ format: "rgba16float", blend: layerBlend },
|
|
3693
|
-
{ format: "rgba16float", blend: layerBlend },
|
|
3694
|
-
],
|
|
3695
|
-
},
|
|
3696
|
-
primitive: { topology: "triangle-list" },
|
|
3697
|
-
multisample: { count: 1 },
|
|
3698
|
-
});
|
|
3738
|
+
fieldPipeline = await fieldBuild;
|
|
3699
3739
|
}
|
|
3700
3740
|
catch (e) {
|
|
3701
3741
|
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)], mounts, params: d.params, duration: d.duration, readsCast: false };
|
|
3702
3742
|
}
|
|
3703
3743
|
}
|
|
3704
|
-
let identity;
|
|
3705
|
-
let gamma;
|
|
3706
|
-
try {
|
|
3707
|
-
const make = (applyGamma, label) => this.device.createRenderPipelineAsync({
|
|
3708
|
-
label,
|
|
3709
|
-
layout: this.compositePipelineLayout,
|
|
3710
|
-
vertex: { module, entryPoint: "vs" },
|
|
3711
|
-
fragment: {
|
|
3712
|
-
module,
|
|
3713
|
-
entryPoint: "fs",
|
|
3714
|
-
constants: { APPLY_GAMMA: applyGamma ? 1 : 0 },
|
|
3715
|
-
targets: [{ format: this.presentationFormat }],
|
|
3716
|
-
},
|
|
3717
|
-
primitive: { topology: "triangle-list" },
|
|
3718
|
-
});
|
|
3719
|
-
[identity, gamma] = await Promise.all([
|
|
3720
|
-
make(false, "composite pipeline (effect, gamma=1)"),
|
|
3721
|
-
make(true, "composite pipeline (effect, gamma!=1)"),
|
|
3722
|
-
]);
|
|
3723
|
-
}
|
|
3724
|
-
catch (e) {
|
|
3725
|
-
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)], mounts, params: d.params, duration: d.duration, readsCast: false };
|
|
3726
|
-
}
|
|
3727
3744
|
// Built BEFORE the swap: a particle stage that fails to compile has to leave
|
|
3728
3745
|
// the previously installed effect running, exactly as a bad composite does.
|
|
3729
3746
|
// A stage that fails after an earlier one succeeded would otherwise strand
|
|
@@ -3950,6 +3967,9 @@ export class Engine {
|
|
|
3950
3967
|
}
|
|
3951
3968
|
const requested = list ?? [];
|
|
3952
3969
|
if (requested.length === 0) {
|
|
3970
|
+
// Built (or found) before anything is torn down, so no frame draws
|
|
3971
|
+
// between the old effects and the composite that no longer samples them.
|
|
3972
|
+
const [identity, gamma] = await this.compositePipelines(false, false);
|
|
3953
3973
|
for (const e of this.effects) {
|
|
3954
3974
|
e.paramsBuffer?.destroy();
|
|
3955
3975
|
e.lights?.uniform.destroy();
|
|
@@ -3962,9 +3982,8 @@ export class Engine {
|
|
|
3962
3982
|
this.allocateLightSlots();
|
|
3963
3983
|
this.anchorTable = EMPTY_ANCHOR_TABLE;
|
|
3964
3984
|
this.clearTrailHistory();
|
|
3965
|
-
|
|
3966
|
-
this.
|
|
3967
|
-
this.compositePipelineGamma = this.makeCompositePipeline(module, true, "composite pipeline (gamma!=1)");
|
|
3985
|
+
this.compositePipelineIdentity = identity;
|
|
3986
|
+
this.compositePipelineGamma = gamma;
|
|
3968
3987
|
this.rebuildCompositeBindGroup();
|
|
3969
3988
|
this.writeCompositeViewUniforms();
|
|
3970
3989
|
return [];
|
|
@@ -4010,6 +4029,10 @@ export class Engine {
|
|
|
4010
4029
|
if (r)
|
|
4011
4030
|
r.diagnostics.push(`anchor "${d.bone}" dropped: the scene is already using all ${MAX_EFFECT_ANCHORS} slots`);
|
|
4012
4031
|
}
|
|
4032
|
+
// The composite for the new list, built before the swap below so the swap
|
|
4033
|
+
// stays one synchronous step. From the shader cache: it was rebuilt, and
|
|
4034
|
+
// synchronously, on every install.
|
|
4035
|
+
const [compositeIdentity, compositeGamma] = await this.compositePipelines(instances.some((e) => e.hasBackground), instances.some((e) => e.hasForeground));
|
|
4013
4036
|
// ── Swap. Everything above either succeeded or was excluded, so the scene
|
|
4014
4037
|
// that was running is only torn down now.
|
|
4015
4038
|
for (const e of this.effects) {
|
|
@@ -4060,18 +4083,8 @@ export class Engine {
|
|
|
4060
4083
|
// Scene-level, from the union: the composite decides only whether to SAMPLE
|
|
4061
4084
|
// the field layers, so one effect with a background is enough to turn that
|
|
4062
4085
|
// on for the frame.
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
const compositeModule = this.device.createShaderModule({
|
|
4066
|
-
label: "composite shader (effects)",
|
|
4067
|
-
code: buildCompositeShader(hasBackground || hasForeground
|
|
4068
|
-
? // No wgsl and so no trails: the composite hosts no effect source at
|
|
4069
|
-
// all, it only decides whether to sample the layer the field pass drew.
|
|
4070
|
-
{ wgsl: "", paramsDecl: "", hasBackground, hasForeground, gridSize: 0, trailCount: 0 }
|
|
4071
|
-
: null),
|
|
4072
|
-
});
|
|
4073
|
-
this.compositePipelineIdentity = this.makeCompositePipeline(compositeModule, false, "composite pipeline (gamma=1)");
|
|
4074
|
-
this.compositePipelineGamma = this.makeCompositePipeline(compositeModule, true, "composite pipeline (gamma!=1)");
|
|
4086
|
+
this.compositePipelineIdentity = compositeIdentity;
|
|
4087
|
+
this.compositePipelineGamma = compositeGamma;
|
|
4075
4088
|
// Nothing to promote any more: `#fullres` is per effect, read into
|
|
4076
4089
|
// fieldLayer when the instance is built, and both target pairs exist for
|
|
4077
4090
|
// the life of the surface. What used to be a scene-wide decision made here
|
|
@@ -4163,26 +4176,33 @@ export class Engine {
|
|
|
4163
4176
|
trailCount: anchors.filter((x) => x.trail).length,
|
|
4164
4177
|
alias,
|
|
4165
4178
|
};
|
|
4179
|
+
// The scope is popped before the first await: compiles run side by side
|
|
4180
|
+
// (both modules here, other effects, style groups), and a scope held open
|
|
4181
|
+
// across an await would catch whichever of them errored first.
|
|
4166
4182
|
const compile = async (code, label) => {
|
|
4167
4183
|
const offset = code.slice(0, code.indexOf(wgsl)).split("\n").length - 1;
|
|
4168
4184
|
this.device.pushErrorScope("validation");
|
|
4169
|
-
const module = this.
|
|
4170
|
-
const
|
|
4171
|
-
const scopeErr = await
|
|
4185
|
+
const module = this.cachedShaderModule(code, label);
|
|
4186
|
+
const scope = this.device.popErrorScope();
|
|
4187
|
+
const [info, scopeErr] = await Promise.all([module.getCompilationInfo(), scope]);
|
|
4172
4188
|
const diagnostics = info.messages
|
|
4173
4189
|
.filter((m) => m.type === "error")
|
|
4174
4190
|
.map((m) => `${Math.max(0, m.lineNum - offset)}:${m.linePos} ${m.message}`);
|
|
4175
4191
|
if (diagnostics.length === 0 && scopeErr)
|
|
4176
4192
|
diagnostics.push(scopeErr.message);
|
|
4193
|
+
if (diagnostics.length)
|
|
4194
|
+
this.forgetShaderModule(code);
|
|
4177
4195
|
return diagnostics.length ? diagnostics : module;
|
|
4178
4196
|
};
|
|
4179
|
-
const computeModule = await
|
|
4197
|
+
const [computeModule, renderModule] = await Promise.all([
|
|
4198
|
+
compile(buildParticleComputeShader(src, cast), "particle compute"),
|
|
4199
|
+
compile(buildParticleRenderShader(src, cast), "particle render"),
|
|
4200
|
+
]);
|
|
4180
4201
|
if (Array.isArray(computeModule)) {
|
|
4181
4202
|
points?.buffer.destroy();
|
|
4182
4203
|
dropTextures();
|
|
4183
4204
|
return { ok: false, diagnostics: computeModule };
|
|
4184
4205
|
}
|
|
4185
|
-
const renderModule = await compile(buildParticleRenderShader(src, cast), "particle render");
|
|
4186
4206
|
if (Array.isArray(renderModule)) {
|
|
4187
4207
|
points?.buffer.destroy();
|
|
4188
4208
|
dropTextures();
|
|
@@ -4220,7 +4240,7 @@ export class Engine {
|
|
|
4220
4240
|
// Declaring one set of flags for both layouts is what made the pipeline
|
|
4221
4241
|
// layout invalid, and the error surfaces later and unhelpfully as "invalid
|
|
4222
4242
|
// due to a previous error".
|
|
4223
|
-
const layoutFor = (storage, visibility, shadow, withIndirect) => this.
|
|
4243
|
+
const layoutFor = (storage, visibility, shadow, withIndirect) => this.cachedBindGroupLayout({
|
|
4224
4244
|
entries: [
|
|
4225
4245
|
{ binding: 0, visibility, buffer: { type: storage } },
|
|
4226
4246
|
{ binding: 1, visibility, buffer: { type: "uniform" } },
|
|
@@ -4326,31 +4346,29 @@ export class Engine {
|
|
|
4326
4346
|
// blends would be a copy.
|
|
4327
4347
|
const targets = sceneTargetsFor(src.blend === "additive" ? "particle-additive" : "particle", this.sceneFormats);
|
|
4328
4348
|
const cutout = src.blend === "cutout";
|
|
4349
|
+
// ALL STAGES AT ONCE: compute, count, prepass and shading compile side by
|
|
4350
|
+
// side rather than in turn. The scope closes before the first await, for
|
|
4351
|
+
// the reason compile() gives.
|
|
4329
4352
|
this.device.pushErrorScope("validation");
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4353
|
+
const renderLayoutObj = this.cachedPipelineLayout([renderLayout]);
|
|
4354
|
+
const builds = Promise.all([
|
|
4355
|
+
this.cachedComputePipeline({
|
|
4333
4356
|
label: "particle compute pipeline",
|
|
4334
|
-
layout:
|
|
4357
|
+
layout: this.cachedPipelineLayout([computeLayout]),
|
|
4335
4358
|
compute: { module: computeModule, entryPoint: "main" },
|
|
4336
|
-
})
|
|
4337
|
-
|
|
4338
|
-
? {
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
indirect,
|
|
4345
|
-
binds: bindFor(countLayout, this.cameraUniformBuffer, false, true),
|
|
4346
|
-
}
|
|
4347
|
-
: null;
|
|
4348
|
-
const renderLayoutObj = this.device.createPipelineLayout({ bindGroupLayouts: [renderLayout] });
|
|
4359
|
+
}),
|
|
4360
|
+
indirect && countLayout
|
|
4361
|
+
? this.cachedComputePipeline({
|
|
4362
|
+
label: "particle count pipeline",
|
|
4363
|
+
layout: this.cachedPipelineLayout([countLayout]),
|
|
4364
|
+
compute: { module: computeModule, entryPoint: "rzCount" },
|
|
4365
|
+
})
|
|
4366
|
+
: null,
|
|
4349
4367
|
// The prepass, when the effect gave the cutout a cheap shape: coverage to
|
|
4350
4368
|
// depth, colour targets at writeMask 0 (the depth-prepass class), and
|
|
4351
4369
|
// the shading pass below then tests EQUAL and writes no depth.
|
|
4352
|
-
|
|
4353
|
-
?
|
|
4370
|
+
prepass
|
|
4371
|
+
? this.cachedRenderPipeline({
|
|
4354
4372
|
label: "particle depth prepass pipeline",
|
|
4355
4373
|
layout: renderLayoutObj,
|
|
4356
4374
|
vertex: { module: renderModule, entryPoint: "vs" },
|
|
@@ -4359,8 +4377,8 @@ export class Engine {
|
|
|
4359
4377
|
depthStencil: { format: this.depthFormat, depthWriteEnabled: true, depthCompare: this.depthAhead },
|
|
4360
4378
|
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
4361
4379
|
})
|
|
4362
|
-
: null
|
|
4363
|
-
|
|
4380
|
+
: null,
|
|
4381
|
+
this.cachedRenderPipeline({
|
|
4364
4382
|
label: "particle render pipeline",
|
|
4365
4383
|
layout: renderLayoutObj,
|
|
4366
4384
|
vertex: { module: renderModule, entryPoint: "vs" },
|
|
@@ -4381,8 +4399,19 @@ export class Engine {
|
|
|
4381
4399
|
? { format: this.depthFormat, depthWriteEnabled: false, depthCompare: "equal" }
|
|
4382
4400
|
: { format: this.depthFormat, depthWriteEnabled: cutout, depthCompare: this.depthAhead },
|
|
4383
4401
|
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
4384
|
-
})
|
|
4385
|
-
|
|
4402
|
+
}),
|
|
4403
|
+
]);
|
|
4404
|
+
const scope = this.device.popErrorScope();
|
|
4405
|
+
try {
|
|
4406
|
+
const [compute, countPipeline, depth, render] = await builds;
|
|
4407
|
+
const live = indirect && countLayout && countPipeline
|
|
4408
|
+
? {
|
|
4409
|
+
pipeline: countPipeline,
|
|
4410
|
+
indirect,
|
|
4411
|
+
binds: bindFor(countLayout, this.cameraUniformBuffer, false, true),
|
|
4412
|
+
}
|
|
4413
|
+
: null;
|
|
4414
|
+
const scoped = await scope;
|
|
4386
4415
|
if (scoped) {
|
|
4387
4416
|
buffer.destroy();
|
|
4388
4417
|
uniform.destroy();
|
|
@@ -4423,7 +4452,7 @@ export class Engine {
|
|
|
4423
4452
|
};
|
|
4424
4453
|
}
|
|
4425
4454
|
catch (e) {
|
|
4426
|
-
await
|
|
4455
|
+
await scope;
|
|
4427
4456
|
buffer.destroy();
|
|
4428
4457
|
uniform.destroy();
|
|
4429
4458
|
points?.buffer.destroy();
|
|
@@ -5660,10 +5689,8 @@ export class Engine {
|
|
|
5660
5689
|
b.enabled = patch.enabled;
|
|
5661
5690
|
if (patch.threshold !== undefined)
|
|
5662
5691
|
b.threshold = patch.threshold;
|
|
5663
|
-
if (patch.
|
|
5664
|
-
b.
|
|
5665
|
-
if (patch.radius !== undefined)
|
|
5666
|
-
b.radius = patch.radius;
|
|
5692
|
+
if (patch.scatter !== undefined)
|
|
5693
|
+
b.scatter = patch.scatter;
|
|
5667
5694
|
if (patch.color !== undefined) {
|
|
5668
5695
|
b.color.x = patch.color.x;
|
|
5669
5696
|
b.color.y = patch.color.y;
|
|
@@ -5671,33 +5698,154 @@ export class Engine {
|
|
|
5671
5698
|
}
|
|
5672
5699
|
if (patch.intensity !== undefined)
|
|
5673
5700
|
b.intensity = patch.intensity;
|
|
5674
|
-
if (
|
|
5675
|
-
b.clamp = patch.clamp;
|
|
5676
|
-
if (this.device && this.bloomBlitUniformBuffer) {
|
|
5701
|
+
if (this.device && this.compositeUniformBuffer) {
|
|
5677
5702
|
this.writeBloomUniforms();
|
|
5678
5703
|
this.writeCompositeViewUniforms();
|
|
5679
5704
|
}
|
|
5680
5705
|
}
|
|
5681
|
-
|
|
5706
|
+
ensureBloomPipelines() {
|
|
5707
|
+
// The buffer, not a pipeline, says "already built": at init the pipelines
|
|
5708
|
+
// land async (initRenderPipeline) and are absent for a moment.
|
|
5709
|
+
if (this.bloomUniformBuffer)
|
|
5710
|
+
return;
|
|
5711
|
+
const device = this.device;
|
|
5712
|
+
this.bloomUniformBuffer = device.createBuffer({
|
|
5713
|
+
label: "bloomuniforms",
|
|
5714
|
+
size: 16,
|
|
5715
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
5716
|
+
});
|
|
5717
|
+
this.bloomUpsampleBindGroupLayout = device.createBindGroupLayout({
|
|
5718
|
+
label: "bloomupsample layout",
|
|
5719
|
+
entries: [
|
|
5720
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
5721
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
5722
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
5723
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
5724
|
+
],
|
|
5725
|
+
});
|
|
5726
|
+
const pipeline = (label, code, layout, assign) => {
|
|
5727
|
+
const module = device.createShaderModule({ label, code });
|
|
5728
|
+
this.initRenderPipeline({
|
|
5729
|
+
label,
|
|
5730
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
|
|
5731
|
+
vertex: { module, entryPoint: "vs" },
|
|
5732
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: this.hdrFormat }] },
|
|
5733
|
+
primitive: { topology: "triangle-list" },
|
|
5734
|
+
}, assign);
|
|
5735
|
+
};
|
|
5736
|
+
pipeline("bloomprefilter", BLOOM_PREFILTER_SHADER_WGSL, this.bloomPrefilterBindGroupLayout, (p) => (this.bloomPrefilterPipeline = p));
|
|
5737
|
+
pipeline("bloomblur H", BLOOM_BLUR_H_SHADER_WGSL, this.bloomBlurBindGroupLayout, (p) => (this.bloomBlurHPipeline = p));
|
|
5738
|
+
pipeline("bloomblur V", BLOOM_BLUR_V_SHADER_WGSL, this.bloomBlurBindGroupLayout, (p) => (this.bloomBlurVPipeline = p));
|
|
5739
|
+
pipeline("bloomupsample", BLOOM_UPSAMPLE_SHADER_WGSL, this.bloomUpsampleBindGroupLayout, (p) => (this.bloomUpsamplePipeline = p));
|
|
5740
|
+
}
|
|
5741
|
+
/** The game's chain at the current canvas size (AGSimPostFX.Bloom): half
|
|
5742
|
+
* size, then floor(log2(longest side)) - 1 levels, each half the last. */
|
|
5743
|
+
buildBloomTargets() {
|
|
5744
|
+
if (!this.device || !this.hdrResolveTexture || !this.maskResolveView || !this.bloomPrefilterBindGroupLayout)
|
|
5745
|
+
return;
|
|
5746
|
+
this.ensureBloomPipelines();
|
|
5747
|
+
const w0 = Math.max(1, Math.floor(this.hdrResolveTexture.width / 2));
|
|
5748
|
+
const h0 = Math.max(1, Math.floor(this.hdrResolveTexture.height / 2));
|
|
5749
|
+
const levels = Math.min(Engine.BLOOM_MAX_LEVELS, Math.max(1, Math.floor(Math.log2(Math.max(w0, h0)) - 1)));
|
|
5750
|
+
// Mip chains, whose sizes are floor-halved exactly as the game's targets are.
|
|
5751
|
+
const make = (label) => this.device.createTexture({
|
|
5752
|
+
label,
|
|
5753
|
+
size: [w0, h0],
|
|
5754
|
+
mipLevelCount: levels,
|
|
5755
|
+
format: this.hdrFormat,
|
|
5756
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
5757
|
+
});
|
|
5758
|
+
this.bloomDownTexture?.destroy();
|
|
5759
|
+
this.bloomUpTexture?.destroy();
|
|
5760
|
+
this.bloomDownTexture = make("bloomdown");
|
|
5761
|
+
this.bloomUpTexture = make("bloomup");
|
|
5762
|
+
this.bloomLevels = levels;
|
|
5763
|
+
const views = (t) => Array.from({ length: levels }, (_, i) => t.createView({ baseMipLevel: i, mipLevelCount: 1 }));
|
|
5764
|
+
this.bloomDownViews = views(this.bloomDownTexture);
|
|
5765
|
+
this.bloomUpViews = views(this.bloomUpTexture);
|
|
5766
|
+
this.writeBloomUniforms();
|
|
5767
|
+
const uniform = { buffer: this.bloomUniformBuffer };
|
|
5768
|
+
this.bloomPrefilterBindGroup = this.device.createBindGroup({
|
|
5769
|
+
label: "bloomprefilter",
|
|
5770
|
+
layout: this.bloomPrefilterBindGroupLayout,
|
|
5771
|
+
entries: [
|
|
5772
|
+
{ binding: 0, resource: this.hdrResolveTexture.createView() },
|
|
5773
|
+
{ binding: 1, resource: uniform },
|
|
5774
|
+
{ binding: 2, resource: this.maskResolveView },
|
|
5775
|
+
],
|
|
5776
|
+
});
|
|
5777
|
+
const blur = (label, src) => this.device.createBindGroup({
|
|
5778
|
+
label,
|
|
5779
|
+
layout: this.bloomBlurBindGroupLayout,
|
|
5780
|
+
entries: [
|
|
5781
|
+
{ binding: 0, resource: src },
|
|
5782
|
+
{ binding: 1, resource: this.bloomSampler },
|
|
5783
|
+
],
|
|
5784
|
+
});
|
|
5785
|
+
// [i - 1] is level i: blurH reads down[i-1] into up[i], blurV up[i] into down[i].
|
|
5786
|
+
this.bloomBlurHBindGroups = [];
|
|
5787
|
+
this.bloomBlurVBindGroups = [];
|
|
5788
|
+
for (let i = 1; i < levels; i++) {
|
|
5789
|
+
this.bloomBlurHBindGroups.push(blur(`bloomblur H ${i}`, this.bloomDownViews[i - 1]));
|
|
5790
|
+
this.bloomBlurVBindGroups.push(blur(`bloomblur V ${i}`, this.bloomUpViews[i]));
|
|
5791
|
+
}
|
|
5792
|
+
// [k] writes up[levels - 2 - k], coarsest first.
|
|
5793
|
+
this.bloomUpsampleBindGroups = [];
|
|
5794
|
+
for (let i = levels - 2; i >= 0; i--) {
|
|
5795
|
+
const low = i === levels - 2 ? this.bloomDownViews[i + 1] : this.bloomUpViews[i + 1];
|
|
5796
|
+
this.bloomUpsampleBindGroups.push(this.device.createBindGroup({
|
|
5797
|
+
label: `bloomupsample ${i}`,
|
|
5798
|
+
layout: this.bloomUpsampleBindGroupLayout,
|
|
5799
|
+
entries: [
|
|
5800
|
+
{ binding: 0, resource: this.bloomDownViews[i] },
|
|
5801
|
+
{ binding: 1, resource: low },
|
|
5802
|
+
{ binding: 2, resource: this.bloomSampler },
|
|
5803
|
+
{ binding: 3, resource: uniform },
|
|
5804
|
+
],
|
|
5805
|
+
}));
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5682
5808
|
writeBloomUniforms() {
|
|
5809
|
+
if (!this.bloomUniformBuffer)
|
|
5810
|
+
return;
|
|
5683
5811
|
const b = this.bloomSettings;
|
|
5684
|
-
const
|
|
5685
|
-
//
|
|
5686
|
-
//
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5812
|
+
const u = this.bloomUniformData;
|
|
5813
|
+
// Threshold, its soft knee (half of it, as the game derives it), the clamp,
|
|
5814
|
+
// and scatter. Tint and intensity live in the composite.
|
|
5815
|
+
u[0] = Math.max(0, b.threshold);
|
|
5816
|
+
u[1] = Math.max(0, b.threshold) * 0.5;
|
|
5817
|
+
u[2] = Engine.BLOOM_CLAMP;
|
|
5818
|
+
u[3] = Math.min(1, Math.max(0, b.scatter));
|
|
5819
|
+
this.device.queue.writeBuffer(this.bloomUniformBuffer, 0, u);
|
|
5820
|
+
}
|
|
5821
|
+
/** The game's chain, pass for pass (AGSimPostFX.Bloom). */
|
|
5822
|
+
renderBloom(encoder) {
|
|
5823
|
+
const levels = this.bloomLevels;
|
|
5824
|
+
const att = this.bloomPassDescriptor.colorAttachments[0];
|
|
5825
|
+
const total = 1 + 3 * (levels - 1);
|
|
5826
|
+
let n = 0;
|
|
5827
|
+
const draw = (target, pipeline, group) => {
|
|
5828
|
+
att.view = target;
|
|
5829
|
+
// One timing span for the whole chain, as for the pyramid.
|
|
5830
|
+
const open = n === 0 ? this.stampOpen("bloom") : undefined;
|
|
5831
|
+
const close = n === total - 1 ? this.stampClose("bloom") : undefined;
|
|
5832
|
+
this.bloomPassDescriptor.timestampWrites = open && close ? { ...open, ...close } : (open ?? close);
|
|
5833
|
+
n++;
|
|
5834
|
+
const p = encoder.beginRenderPass(this.bloomPassDescriptor);
|
|
5835
|
+
p.setPipeline(pipeline);
|
|
5836
|
+
p.setBindGroup(0, group);
|
|
5837
|
+
p.draw(3);
|
|
5838
|
+
p.end();
|
|
5839
|
+
};
|
|
5840
|
+
draw(this.bloomDownViews[0], this.bloomPrefilterPipeline, this.bloomPrefilterBindGroup);
|
|
5841
|
+
for (let i = 1; i < levels; i++) {
|
|
5842
|
+
draw(this.bloomUpViews[i], this.bloomBlurHPipeline, this.bloomBlurHBindGroups[i - 1]);
|
|
5843
|
+
draw(this.bloomDownViews[i], this.bloomBlurVPipeline, this.bloomBlurVBindGroups[i - 1]);
|
|
5844
|
+
}
|
|
5845
|
+
for (let k = 0; k < levels - 1; k++) {
|
|
5846
|
+
draw(this.bloomUpViews[levels - 2 - k], this.bloomUpsamplePipeline, this.bloomUpsampleBindGroups[k]);
|
|
5847
|
+
}
|
|
5848
|
+
this.bloomPassDescriptor.timestampWrites = undefined;
|
|
5701
5849
|
}
|
|
5702
5850
|
// Step 1: Get WebGPU device and context
|
|
5703
5851
|
async init() {
|
|
@@ -5822,7 +5970,17 @@ export class Engine {
|
|
|
5822
5970
|
this.setupCamera();
|
|
5823
5971
|
this.setupLighting();
|
|
5824
5972
|
const tPipelines = performance.now();
|
|
5973
|
+
// Built async and side by side (see initRenderPipeline): a synchronous
|
|
5974
|
+
// build holds the GPU process for the whole compile, one after another,
|
|
5975
|
+
// which was a 2–3 s frozen frame on a cold open.
|
|
5976
|
+
this.initPipelineJobs = [];
|
|
5825
5977
|
this.createPipelines();
|
|
5978
|
+
try {
|
|
5979
|
+
await Promise.all(this.initPipelineJobs);
|
|
5980
|
+
}
|
|
5981
|
+
finally {
|
|
5982
|
+
this.initPipelineJobs = null;
|
|
5983
|
+
}
|
|
5826
5984
|
this.setupResize();
|
|
5827
5985
|
const ms = (a, b) => Math.round(b - a);
|
|
5828
5986
|
console.info(`[reze] init ${ms(t0, performance.now())}ms — adapter ${ms(t0, tAdapter)}ms · ` +
|
|
@@ -5929,122 +6087,12 @@ export class Engine {
|
|
|
5929
6087
|
this.device.queue.submit([enc.finish()]);
|
|
5930
6088
|
ltcTemp.destroy();
|
|
5931
6089
|
}
|
|
5932
|
-
// Bake the Blender 3.6 Filmic MHC tone curve into a WIDTH×1 r16float LUT sampled by the
|
|
5933
|
-
// composite pass. The 14 anchors are the same as the old inline array; we fit a monotone
|
|
5934
|
-
// cubic (Fritsch–Carlson) through them so the curve is C1-continuous (no Mach banding in
|
|
5935
|
-
// smooth gradients) while still passing through every anchor (look preserved) and staying
|
|
5936
|
-
// monotone (no tonemap overshoot/ringing). Domain is uniform in log2 space: anchor k sits
|
|
5937
|
-
// at t=k, k=0..13 (t = log2(linear)+10). See composite.ts::filmic for the sampling map.
|
|
5938
|
-
/**
|
|
5939
|
-
* Decompress and upload Blender's AgX cube.
|
|
5940
|
-
*
|
|
5941
|
-
* Deliberately off the critical path: it is 723 KB once inflated, and a frame
|
|
5942
|
-
* rendered before it lands should show the scene under whatever transform is
|
|
5943
|
-
* already there rather than wait. Until then binding 10 holds a 1×1×1 stand-in,
|
|
5944
|
-
* which is only ever sampled if someone selects AgX in that window.
|
|
5945
|
-
*/
|
|
5946
|
-
async loadAgxLut() {
|
|
5947
|
-
try {
|
|
5948
|
-
const packed = Uint8Array.from(atob(AGX_LUT_GZ), (ch) => ch.charCodeAt(0));
|
|
5949
|
-
const stream = new Blob([packed]).stream().pipeThrough(new DecompressionStream("gzip"));
|
|
5950
|
-
const bytes = new Uint8Array(await new Response(stream).arrayBuffer());
|
|
5951
|
-
const n = AGX_LUT_SIZE;
|
|
5952
|
-
if (bytes.byteLength !== n * n * n * 4)
|
|
5953
|
-
throw new Error(`AgX LUT is ${bytes.byteLength} bytes, expected ${n ** 3 * 4}`);
|
|
5954
|
-
const tex = this.device.createTexture({
|
|
5955
|
-
label: "AgX 57³ LUT",
|
|
5956
|
-
size: [n, n, n],
|
|
5957
|
-
dimension: "3d",
|
|
5958
|
-
format: "rgb10a2unorm",
|
|
5959
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
5960
|
-
});
|
|
5961
|
-
// .cube order is red fastest, which is exactly a 3D texture's own layout.
|
|
5962
|
-
this.device.queue.writeTexture({ texture: tex }, bytes, { bytesPerRow: n * 4, rowsPerImage: n }, [n, n, n]);
|
|
5963
|
-
this.agxLutTexture = tex;
|
|
5964
|
-
this.rebuildCompositeBindGroup();
|
|
5965
|
-
}
|
|
5966
|
-
catch {
|
|
5967
|
-
// A missing LUT costs AgX, not the renderer — the other transforms stand.
|
|
5968
|
-
}
|
|
5969
|
-
}
|
|
5970
|
-
bakeFilmicLut() {
|
|
5971
|
-
const anchors = [
|
|
5972
|
-
0.0028, 0.0068, 0.0151, 0.0313, 0.061, 0.112, 0.192, 0.306, 0.459, 0.631, 0.82, 0.907, 0.962, 0.989,
|
|
5973
|
-
];
|
|
5974
|
-
const n = anchors.length;
|
|
5975
|
-
// Secant slopes (unit spacing, so d_k = y_{k+1} - y_k).
|
|
5976
|
-
const d = new Array(n - 1);
|
|
5977
|
-
for (let k = 0; k < n - 1; k++)
|
|
5978
|
-
d[k] = anchors[k + 1] - anchors[k];
|
|
5979
|
-
// Endpoint + interior tangents.
|
|
5980
|
-
const m = new Array(n);
|
|
5981
|
-
m[0] = d[0];
|
|
5982
|
-
m[n - 1] = d[n - 2];
|
|
5983
|
-
for (let k = 1; k < n - 1; k++)
|
|
5984
|
-
m[k] = (d[k - 1] + d[k]) * 0.5;
|
|
5985
|
-
// Fritsch–Carlson monotonicity clamp.
|
|
5986
|
-
for (let k = 0; k < n - 1; k++) {
|
|
5987
|
-
if (d[k] === 0) {
|
|
5988
|
-
m[k] = 0;
|
|
5989
|
-
m[k + 1] = 0;
|
|
5990
|
-
continue;
|
|
5991
|
-
}
|
|
5992
|
-
const a = m[k] / d[k];
|
|
5993
|
-
const b = m[k + 1] / d[k];
|
|
5994
|
-
const s = a * a + b * b;
|
|
5995
|
-
if (s > 9) {
|
|
5996
|
-
const tau = 3 / Math.sqrt(s);
|
|
5997
|
-
m[k] = tau * a * d[k];
|
|
5998
|
-
m[k + 1] = tau * b * d[k];
|
|
5999
|
-
}
|
|
6000
|
-
}
|
|
6001
|
-
const W = Engine.FILMIC_LUT_WIDTH;
|
|
6002
|
-
const values = new Float32Array(W);
|
|
6003
|
-
for (let j = 0; j < W; j++) {
|
|
6004
|
-
const t = ((n - 1) * j) / (W - 1); // t ∈ [0, n-1]
|
|
6005
|
-
const k = Math.min(Math.floor(t), n - 2);
|
|
6006
|
-
const s = t - k; // local param in [0,1], unit-spaced segment
|
|
6007
|
-
const s2 = s * s;
|
|
6008
|
-
const s3 = s2 * s;
|
|
6009
|
-
// Hermite basis (h=1).
|
|
6010
|
-
const h00 = 2 * s3 - 3 * s2 + 1;
|
|
6011
|
-
const h10 = s3 - 2 * s2 + s;
|
|
6012
|
-
const h01 = -2 * s3 + 3 * s2;
|
|
6013
|
-
const h11 = s3 - s2;
|
|
6014
|
-
values[j] = h00 * anchors[k] + h10 * m[k] + h01 * anchors[k + 1] + h11 * m[k + 1];
|
|
6015
|
-
}
|
|
6016
|
-
// f32 → f16 bits (same conversion as bakeBrdfLut).
|
|
6017
|
-
const half = new Uint16Array(W);
|
|
6018
|
-
const f32 = new Float32Array(1);
|
|
6019
|
-
const u32 = new Uint32Array(f32.buffer);
|
|
6020
|
-
for (let j = 0; j < W; j++) {
|
|
6021
|
-
f32[0] = values[j];
|
|
6022
|
-
const x = u32[0];
|
|
6023
|
-
const sign = (x >>> 16) & 0x8000;
|
|
6024
|
-
const exp = ((x >>> 23) & 0xff) - 127 + 15;
|
|
6025
|
-
const mant = x & 0x7fffff;
|
|
6026
|
-
if (exp <= 0) {
|
|
6027
|
-
half[j] = sign;
|
|
6028
|
-
}
|
|
6029
|
-
else if (exp >= 31) {
|
|
6030
|
-
half[j] = sign | 0x7c00;
|
|
6031
|
-
}
|
|
6032
|
-
else {
|
|
6033
|
-
half[j] = sign | (exp << 10) | (mant >>> 13);
|
|
6034
|
-
}
|
|
6035
|
-
}
|
|
6036
|
-
this.filmicLutTexture = this.device.createTexture({
|
|
6037
|
-
label: "Filmic tone LUT",
|
|
6038
|
-
size: [W, 1],
|
|
6039
|
-
format: "r16float",
|
|
6040
|
-
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
|
|
6041
|
-
});
|
|
6042
|
-
this.filmicLutView = this.filmicLutTexture.createView();
|
|
6043
|
-
this.device.queue.writeTexture({ texture: this.filmicLutTexture }, half, { bytesPerRow: W * 2, rowsPerImage: 1 }, { width: W, height: 1, depthOrArrayLayers: 1 });
|
|
6044
|
-
}
|
|
6045
6090
|
createRenderPipeline(config) {
|
|
6091
|
+
return this.createModelPipeline(this.renderPipelineDesc(config));
|
|
6092
|
+
}
|
|
6093
|
+
renderPipelineDesc(config) {
|
|
6046
6094
|
const targets = config.fragmentTargets ?? (config.fragmentTarget ? [config.fragmentTarget] : undefined);
|
|
6047
|
-
return
|
|
6095
|
+
return {
|
|
6048
6096
|
label: config.label,
|
|
6049
6097
|
layout: config.layout,
|
|
6050
6098
|
vertex: {
|
|
@@ -6061,7 +6109,7 @@ export class Engine {
|
|
|
6061
6109
|
primitive: { cullMode: config.cullMode ?? "none" },
|
|
6062
6110
|
depthStencil: config.depthStencil,
|
|
6063
6111
|
multisample: config.multisample ?? { count: Engine.MULTISAMPLE_COUNT },
|
|
6064
|
-
}
|
|
6112
|
+
};
|
|
6065
6113
|
}
|
|
6066
6114
|
createPipelines() {
|
|
6067
6115
|
this.materialSampler = this.device.createSampler({
|
|
@@ -6179,8 +6227,6 @@ export class Engine {
|
|
|
6179
6227
|
// creation with "binding doesn't exist", naming a binding no effect
|
|
6180
6228
|
// author ever wrote. Sharing the code meant sharing these.
|
|
6181
6229
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
6182
|
-
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
6183
|
-
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "3d" } },
|
|
6184
6230
|
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "3d" } },
|
|
6185
6231
|
// And the scene's COVERAGE and bloom, without which the tap cannot
|
|
6186
6232
|
// reconstruct a pixel: the HDR target is premultiplied, so colour alone
|
|
@@ -6201,7 +6247,7 @@ export class Engine {
|
|
|
6201
6247
|
const seedModule = this.device.createShaderModule({ label: "cast distance seed", code: buildCastSeedShader(Engine.MULTISAMPLE_COUNT) });
|
|
6202
6248
|
const stepModule = this.device.createShaderModule({ label: "cast distance step", code: buildCastStepShader() });
|
|
6203
6249
|
const resolveModule = this.device.createShaderModule({ label: "cast distance resolve", code: buildCastResolveShader() });
|
|
6204
|
-
this.
|
|
6250
|
+
this.initRenderPipeline({
|
|
6205
6251
|
label: "cast distance seed",
|
|
6206
6252
|
layout: "auto",
|
|
6207
6253
|
vertex: { module: seedModule, entryPoint: "vs" },
|
|
@@ -6211,21 +6257,21 @@ export class Engine {
|
|
|
6211
6257
|
targets: [{ format: CAST_SEED_FORMAT }, { format: CAST_COVERAGE_FORMAT }],
|
|
6212
6258
|
},
|
|
6213
6259
|
primitive: { topology: "triangle-list" },
|
|
6214
|
-
});
|
|
6215
|
-
this.
|
|
6260
|
+
}, (p) => (this.castSeedPipeline = p));
|
|
6261
|
+
this.initRenderPipeline({
|
|
6216
6262
|
label: "cast distance step",
|
|
6217
6263
|
layout: "auto",
|
|
6218
6264
|
vertex: { module: stepModule, entryPoint: "vs" },
|
|
6219
6265
|
fragment: { module: stepModule, entryPoint: "fs", targets: [{ format: CAST_SEED_FORMAT }] },
|
|
6220
6266
|
primitive: { topology: "triangle-list" },
|
|
6221
|
-
});
|
|
6222
|
-
this.
|
|
6267
|
+
}, (p) => (this.castStepPipeline = p));
|
|
6268
|
+
this.initRenderPipeline({
|
|
6223
6269
|
label: "cast distance resolve",
|
|
6224
6270
|
layout: "auto",
|
|
6225
6271
|
vertex: { module: resolveModule, entryPoint: "vs" },
|
|
6226
6272
|
fragment: { module: resolveModule, entryPoint: "fs", targets: [{ format: CAST_DIST_FORMAT }] },
|
|
6227
6273
|
primitive: { topology: "triangle-list" },
|
|
6228
|
-
});
|
|
6274
|
+
}, (p) => (this.castResolvePipeline = p));
|
|
6229
6275
|
// 65504 is the largest half float. Bound wherever the field is not
|
|
6230
6276
|
// running, so rzCastDistance answers "unreachably far" and an effect keyed
|
|
6231
6277
|
// on it draws nothing at all.
|
|
@@ -6407,8 +6453,8 @@ export class Engine {
|
|
|
6407
6453
|
const neutral = compileGraph(DEFAULT_GRAPH, { renderClass: "auto", alphaMode: "opaque" });
|
|
6408
6454
|
if (!neutral.ok)
|
|
6409
6455
|
throw new Error("failed to compile the neutral default graph");
|
|
6410
|
-
const neutralModule = this.
|
|
6411
|
-
this.
|
|
6456
|
+
const neutralModule = this.cachedShaderModule(neutral.wgsl, "neutral base (default graph)");
|
|
6457
|
+
this.initRenderPipeline(this.renderPipelineDesc({
|
|
6412
6458
|
label: "neutral base pipeline",
|
|
6413
6459
|
layout: mainPipelineLayout,
|
|
6414
6460
|
shaderModule: neutralModule,
|
|
@@ -6420,9 +6466,9 @@ export class Engine {
|
|
|
6420
6466
|
depthWriteEnabled: true,
|
|
6421
6467
|
depthCompare: this.depthAhead,
|
|
6422
6468
|
},
|
|
6423
|
-
});
|
|
6469
|
+
}), (p) => (this.neutralPipeline = p), true);
|
|
6424
6470
|
// Depth-write-off twin for transparent-bucket draws (see pipelineForDrawCall).
|
|
6425
|
-
this.
|
|
6471
|
+
this.initRenderPipeline(this.renderPipelineDesc({
|
|
6426
6472
|
label: "neutral base pipeline (no depth write)",
|
|
6427
6473
|
layout: mainPipelineLayout,
|
|
6428
6474
|
shaderModule: neutralModule,
|
|
@@ -6434,7 +6480,7 @@ export class Engine {
|
|
|
6434
6480
|
depthWriteEnabled: false,
|
|
6435
6481
|
depthCompare: this.depthAhead,
|
|
6436
6482
|
},
|
|
6437
|
-
});
|
|
6483
|
+
}), (p) => (this.neutralPipelineNoDepthWrite = p), true);
|
|
6438
6484
|
// Depth-only prepass for transparent draws (see depth-prepass.ts): writes the
|
|
6439
6485
|
// fabric's depth AFTER its color blended, so outlines drawn later are
|
|
6440
6486
|
// occluded behind it. Color targets kept for pass compatibility, writeMask 0.
|
|
@@ -6453,7 +6499,7 @@ export class Engine {
|
|
|
6453
6499
|
depthCompare: this.depthAhead,
|
|
6454
6500
|
},
|
|
6455
6501
|
};
|
|
6456
|
-
this.
|
|
6502
|
+
this.initRenderPipeline({
|
|
6457
6503
|
label: "opaque depth prepass",
|
|
6458
6504
|
...prepassDesc,
|
|
6459
6505
|
fragment: {
|
|
@@ -6461,11 +6507,11 @@ export class Engine {
|
|
|
6461
6507
|
entryPoint: "fs",
|
|
6462
6508
|
targets: sceneTargetsFor("depth-prepass", this.sceneFormats),
|
|
6463
6509
|
},
|
|
6464
|
-
});
|
|
6510
|
+
}, (p) => (this.depthPrepassPipeline = p), true);
|
|
6465
6511
|
// The SOLID prime: same module, cutoff forced to exactly 1.0. Only texels
|
|
6466
6512
|
// whose blend ignores the destination may pre-claim depth in the
|
|
6467
6513
|
// transparent phase — see the override's note in depth-prepass.ts.
|
|
6468
|
-
this.
|
|
6514
|
+
this.initRenderPipeline({
|
|
6469
6515
|
label: "transparent solid prepass",
|
|
6470
6516
|
...prepassDesc,
|
|
6471
6517
|
fragment: {
|
|
@@ -6474,14 +6520,14 @@ export class Engine {
|
|
|
6474
6520
|
constants: { CUTOFF: 1.0 },
|
|
6475
6521
|
targets: sceneTargetsFor("depth-prepass", this.sceneFormats),
|
|
6476
6522
|
},
|
|
6477
|
-
});
|
|
6523
|
+
}, (p) => (this.solidPrepassPipeline = p), true);
|
|
6478
6524
|
// The HAIR prime: solid texels only, and stencil-fenced off the eye
|
|
6479
6525
|
// silhouette. It records after the non-hair opaque draws, so the eye has
|
|
6480
6526
|
// already written its stencil — not-equal here is what keeps the primed
|
|
6481
6527
|
// hair depth from ever claiming the pixels the see-through-hair pass needs
|
|
6482
6528
|
// the eye to survive on. (Bundle draws use the PASS's stencil reference;
|
|
6483
6529
|
// only pipeline/bind/vertex state resets across executeBundles.)
|
|
6484
|
-
this.
|
|
6530
|
+
this.initRenderPipeline({
|
|
6485
6531
|
label: "hair depth prime",
|
|
6486
6532
|
...prepassDesc,
|
|
6487
6533
|
depthStencil: {
|
|
@@ -6497,7 +6543,7 @@ export class Engine {
|
|
|
6497
6543
|
constants: { CUTOFF: 1.0 },
|
|
6498
6544
|
targets: sceneTargetsFor("depth-prepass", this.sceneFormats),
|
|
6499
6545
|
},
|
|
6500
|
-
});
|
|
6546
|
+
}, (p) => (this.hairPrimePipeline = p), true);
|
|
6501
6547
|
// The matrices, then one vec4: the caster sphere, which effects' rzShadow
|
|
6502
6548
|
// tests before any tap. The materials and the ground bind the matrices only.
|
|
6503
6549
|
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
@@ -6521,7 +6567,7 @@ export class Engine {
|
|
|
6521
6567
|
label: "shadow depth",
|
|
6522
6568
|
code: SHADOW_DEPTH_SHADER_WGSL,
|
|
6523
6569
|
});
|
|
6524
|
-
this.
|
|
6570
|
+
this.initRenderPipeline({
|
|
6525
6571
|
label: "shadow depth pipeline",
|
|
6526
6572
|
// Group 1 is the main pass's per-material layout so each shadow draw can
|
|
6527
6573
|
// rebind the draw call's existing material bind group for the alpha test.
|
|
@@ -6542,7 +6588,7 @@ export class Engine {
|
|
|
6542
6588
|
depthBiasSlopeScale: 1.5,
|
|
6543
6589
|
depthBiasClamp: 0,
|
|
6544
6590
|
},
|
|
6545
|
-
});
|
|
6591
|
+
}, (p) => (this.shadowDepthPipeline = p));
|
|
6546
6592
|
this.shadowComparisonSampler = this.device.createSampler({
|
|
6547
6593
|
compare: "less",
|
|
6548
6594
|
magFilter: "linear",
|
|
@@ -6576,14 +6622,20 @@ export class Engine {
|
|
|
6576
6622
|
// One-shot bake of Blender EEVEE's combined BRDF LUT (DFG + LTC packed rgba8unorm).
|
|
6577
6623
|
this.bakeBrdfLut();
|
|
6578
6624
|
this.bakeGroundNoise();
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6625
|
+
// The mipmap blit for the formats model textures arrive in, built here so
|
|
6626
|
+
// the first texture of the first model does not compile it synchronously
|
|
6627
|
+
// in the middle of a load (generateMipmaps builds any other on demand).
|
|
6628
|
+
{
|
|
6629
|
+
const mipModule = this.device.createShaderModule({ label: "mipmap blit", code: MIPMAP_BLIT_SHADER_WGSL });
|
|
6630
|
+
for (const format of ["rgba8unorm", "rgba8unorm-srgb"])
|
|
6631
|
+
this.initRenderPipeline({
|
|
6632
|
+
label: `mipmap blit pipeline (${format})`,
|
|
6633
|
+
layout: "auto",
|
|
6634
|
+
vertex: { module: mipModule, entryPoint: "vs" },
|
|
6635
|
+
fragment: { module: mipModule, entryPoint: "fs", targets: [{ format }] },
|
|
6636
|
+
primitive: { topology: "triangle-list" },
|
|
6637
|
+
}, (p) => this.mipBlitPipelines.set(format, p));
|
|
6638
|
+
}
|
|
6587
6639
|
this.stageGradeFallback = this.device.createTexture({
|
|
6588
6640
|
label: "stage grade fallback",
|
|
6589
6641
|
size: [1, 1, 1],
|
|
@@ -6592,7 +6644,6 @@ export class Engine {
|
|
|
6592
6644
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
6593
6645
|
});
|
|
6594
6646
|
this.uploadStageGrade();
|
|
6595
|
-
this.bakeFilmicLut();
|
|
6596
6647
|
// BEFORE the bind group below, which binds it. Full size from the start:
|
|
6597
6648
|
// every material pipeline binds this, so sizing it to the light count would
|
|
6598
6649
|
// mean rebuilding bind groups whenever a scene gained a lamp. Uploaded from
|
|
@@ -6679,7 +6730,7 @@ export class Engine {
|
|
|
6679
6730
|
cullMode: "back",
|
|
6680
6731
|
depthStencil: { format: this.depthFormat, depthWriteEnabled: true, depthCompare: this.depthAhead },
|
|
6681
6732
|
};
|
|
6682
|
-
this.
|
|
6733
|
+
this.initRenderPipeline(this.renderPipelineDesc(this.groundPipelineConfig(false)), (p) => (this.groundShadowPipeline = p));
|
|
6683
6734
|
// Outline: group 0 = per-frame (camera), group 1 = per-instance (skinMats), group 2 = per-material (edge uniforms)
|
|
6684
6735
|
this.outlinePerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
6685
6736
|
label: "outline per-frame bind group layout",
|
|
@@ -6742,7 +6793,12 @@ export class Engine {
|
|
|
6742
6793
|
label: "outline pipeline",
|
|
6743
6794
|
layout: outlinePipelineLayout,
|
|
6744
6795
|
shaderModule: outlineShaderModule,
|
|
6745
|
-
|
|
6796
|
+
// The selection mask's streams plus the hull's own: smoothed normal and
|
|
6797
|
+
// PMX edge scale (ModelInstance.outlineVertexBuffer).
|
|
6798
|
+
vertexBuffers: [
|
|
6799
|
+
...outlineVertexBuffers,
|
|
6800
|
+
{ arrayStride: 4 * 4, attributes: [{ shaderLocation: 5, offset: 0, format: "float32x4" }] },
|
|
6801
|
+
],
|
|
6746
6802
|
fragmentTargets: sceneTargetsFor("outline", this.sceneFormats),
|
|
6747
6803
|
cullMode: "back",
|
|
6748
6804
|
depthStencil: {
|
|
@@ -6776,12 +6832,12 @@ export class Engine {
|
|
|
6776
6832
|
stencilWriteMask: 0,
|
|
6777
6833
|
},
|
|
6778
6834
|
};
|
|
6779
|
-
this.
|
|
6780
|
-
this.
|
|
6835
|
+
this.initRenderPipeline(this.renderPipelineDesc(outlineDesc), (p) => (this.outlinePipeline = p), true);
|
|
6836
|
+
this.initRenderPipeline(this.renderPipelineDesc({
|
|
6781
6837
|
...outlineDesc,
|
|
6782
6838
|
label: "outline pipeline (mirror)",
|
|
6783
6839
|
cullMode: "front",
|
|
6784
|
-
});
|
|
6840
|
+
}), (p) => (this.outlineMirrorPipeline = p), true);
|
|
6785
6841
|
// ─── Selection overlay (screen-space edge-detect on a per-material mask) ───
|
|
6786
6842
|
// Reuses outline camera + main skinMats bind group layouts. No group 2 (no per-mat uniform).
|
|
6787
6843
|
const selectionMaskPipelineLayout = this.device.createPipelineLayout({
|
|
@@ -6792,7 +6848,7 @@ export class Engine {
|
|
|
6792
6848
|
label: "selection mask shader",
|
|
6793
6849
|
code: SELECTION_MASK_SHADER_WGSL,
|
|
6794
6850
|
});
|
|
6795
|
-
this.
|
|
6851
|
+
this.initRenderPipeline({
|
|
6796
6852
|
label: "selection mask pipeline",
|
|
6797
6853
|
layout: selectionMaskPipelineLayout,
|
|
6798
6854
|
vertex: { module: selectionMaskShaderModule, entryPoint: "vs", buffers: outlineVertexBuffers },
|
|
@@ -6804,7 +6860,7 @@ export class Engine {
|
|
|
6804
6860
|
primitive: { cullMode: "none" },
|
|
6805
6861
|
// Single-sample, no depth (depth-always via not attaching a depth buffer at all).
|
|
6806
6862
|
multisample: { count: 1 },
|
|
6807
|
-
});
|
|
6863
|
+
}, (p) => (this.selectionMaskPipeline = p));
|
|
6808
6864
|
this.selectionEdgeBindGroupLayout = this.device.createBindGroupLayout({
|
|
6809
6865
|
label: "selection edge bind group layout",
|
|
6810
6866
|
entries: [
|
|
@@ -6821,7 +6877,7 @@ export class Engine {
|
|
|
6821
6877
|
label: "selection edge shader",
|
|
6822
6878
|
code: SELECTION_EDGE_SHADER_WGSL,
|
|
6823
6879
|
});
|
|
6824
|
-
this.
|
|
6880
|
+
this.initRenderPipeline({
|
|
6825
6881
|
label: "selection edge pipeline",
|
|
6826
6882
|
layout: selectionEdgePipelineLayout,
|
|
6827
6883
|
vertex: { module: selectionEdgeShaderModule, entryPoint: "vs" },
|
|
@@ -6840,7 +6896,7 @@ export class Engine {
|
|
|
6840
6896
|
},
|
|
6841
6897
|
primitive: { topology: "triangle-list" },
|
|
6842
6898
|
multisample: { count: 1 },
|
|
6843
|
-
});
|
|
6899
|
+
}, (p) => (this.selectionEdgePipeline = p));
|
|
6844
6900
|
this.selectionSampler = this.device.createSampler({
|
|
6845
6901
|
label: "selection sampler",
|
|
6846
6902
|
magFilter: "linear",
|
|
@@ -6857,10 +6913,7 @@ export class Engine {
|
|
|
6857
6913
|
this.setupGizmo();
|
|
6858
6914
|
// ─── Editor overlays (instanced wireframe primitives) ────────────
|
|
6859
6915
|
this.setupOverlay();
|
|
6860
|
-
// ─── Bloom
|
|
6861
|
-
// Mirrors source/blender/draw/engines/eevee/shaders/effect_bloom_frag.glsl.
|
|
6862
|
-
// Firefly suppression lives in the blit (Karis luminance-weighted 4-tap average). A single-pass
|
|
6863
|
-
// Gaussian cannot reproduce this — hot pixels dominate and produce the sparkle halo.
|
|
6916
|
+
// ─── Bloom: Aether Gazer's chain (prefilter → Gaussian down chain → scatter up chain) ───
|
|
6864
6917
|
this.bloomSampler = this.device.createSampler({
|
|
6865
6918
|
label: "bloom sampler",
|
|
6866
6919
|
magFilter: "linear",
|
|
@@ -6868,81 +6921,25 @@ export class Engine {
|
|
|
6868
6921
|
addressModeU: "clamp-to-edge",
|
|
6869
6922
|
addressModeV: "clamp-to-edge",
|
|
6870
6923
|
});
|
|
6871
|
-
this.
|
|
6872
|
-
label: "bloom
|
|
6873
|
-
size: 16,
|
|
6874
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
6875
|
-
});
|
|
6876
|
-
this.bloomUpsampleUniformBuffer = this.device.createBuffer({
|
|
6877
|
-
label: "bloom upsample uniforms",
|
|
6878
|
-
size: 16,
|
|
6879
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
6880
|
-
});
|
|
6881
|
-
this.bloomBlitBindGroupLayout = this.device.createBindGroupLayout({
|
|
6882
|
-
label: "bloom blit layout",
|
|
6924
|
+
this.bloomPrefilterBindGroupLayout = this.device.createBindGroupLayout({
|
|
6925
|
+
label: "bloom prefilter layout",
|
|
6883
6926
|
entries: [
|
|
6884
6927
|
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float" } },
|
|
6885
6928
|
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
6886
6929
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float" } },
|
|
6887
6930
|
],
|
|
6888
6931
|
});
|
|
6889
|
-
this.
|
|
6890
|
-
label: "bloom
|
|
6932
|
+
this.bloomBlurBindGroupLayout = this.device.createBindGroupLayout({
|
|
6933
|
+
label: "bloom blur layout",
|
|
6891
6934
|
entries: [
|
|
6892
6935
|
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
6893
6936
|
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
6894
6937
|
],
|
|
6895
6938
|
});
|
|
6896
|
-
this.
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // matching downsample mip (base add)
|
|
6901
|
-
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
6902
|
-
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
6903
|
-
],
|
|
6904
|
-
});
|
|
6905
|
-
const bloomBlitShader = this.device.createShaderModule({
|
|
6906
|
-
label: "bloom blit (Karis prefilter)",
|
|
6907
|
-
code: BLOOM_BLIT_SHADER_WGSL,
|
|
6908
|
-
});
|
|
6909
|
-
const bloomDownsampleShader = this.device.createShaderModule({
|
|
6910
|
-
label: "bloom downsample 13-tap",
|
|
6911
|
-
code: BLOOM_DOWNSAMPLE_SHADER_WGSL,
|
|
6912
|
-
});
|
|
6913
|
-
const bloomUpsampleShader = this.device.createShaderModule({
|
|
6914
|
-
label: "bloom upsample 9-tap tent",
|
|
6915
|
-
code: BLOOM_UPSAMPLE_SHADER_WGSL,
|
|
6916
|
-
});
|
|
6917
|
-
const bloomBlitLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomBlitBindGroupLayout] });
|
|
6918
|
-
const bloomDownLayout = this.device.createPipelineLayout({
|
|
6919
|
-
bindGroupLayouts: [this.bloomDownsampleBindGroupLayout],
|
|
6920
|
-
});
|
|
6921
|
-
const bloomUpLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomUpsampleBindGroupLayout] });
|
|
6922
|
-
this.bloomBlitPipeline = this.device.createRenderPipeline({
|
|
6923
|
-
label: "bloom blit pipeline",
|
|
6924
|
-
layout: bloomBlitLayout,
|
|
6925
|
-
vertex: { module: bloomBlitShader, entryPoint: "vs" },
|
|
6926
|
-
fragment: { module: bloomBlitShader, entryPoint: "fs", targets: [{ format: this.hdrFormat }] },
|
|
6927
|
-
primitive: { topology: "triangle-list" },
|
|
6928
|
-
});
|
|
6929
|
-
this.bloomDownsamplePipeline = this.device.createRenderPipeline({
|
|
6930
|
-
label: "bloom downsample pipeline",
|
|
6931
|
-
layout: bloomDownLayout,
|
|
6932
|
-
vertex: { module: bloomDownsampleShader, entryPoint: "vs" },
|
|
6933
|
-
fragment: { module: bloomDownsampleShader, entryPoint: "fs", targets: [{ format: this.hdrFormat }] },
|
|
6934
|
-
primitive: { topology: "triangle-list" },
|
|
6935
|
-
});
|
|
6936
|
-
this.bloomUpsamplePipeline = this.device.createRenderPipeline({
|
|
6937
|
-
label: "bloom upsample pipeline",
|
|
6938
|
-
layout: bloomUpLayout,
|
|
6939
|
-
vertex: { module: bloomUpsampleShader, entryPoint: "vs" },
|
|
6940
|
-
fragment: { module: bloomUpsampleShader, entryPoint: "fs", targets: [{ format: this.hdrFormat }] },
|
|
6941
|
-
primitive: { topology: "triangle-list" },
|
|
6942
|
-
});
|
|
6943
|
-
// ─── Composite: HDR + bloom → Filmic → swapchain (premultiplied) ───
|
|
6944
|
-
// Bloom color/intensity applied HERE (pyramid is pure energy; tint belongs to the combine step,
|
|
6945
|
-
// mirroring EEVEE where bloom color/intensity are combine-stage params, not prefilter).
|
|
6939
|
+
this.ensureBloomPipelines();
|
|
6940
|
+
// ─── Composite: HDR + bloom → view transform → swapchain (premultiplied) ───
|
|
6941
|
+
// Bloom color/intensity applied HERE: the chain is pure energy, tint and
|
|
6942
|
+
// strength belong to the combine step.
|
|
6946
6943
|
this.compositeUniformBuffer = this.device.createBuffer({
|
|
6947
6944
|
label: "composite view uniforms",
|
|
6948
6945
|
// 15 × vec4f: (exposure, invGamma, _, _) · (bloom tint, intensity) ·
|
|
@@ -6984,8 +6981,6 @@ export class Engine {
|
|
|
6984
6981
|
// Aux mask/alpha texture — composite reads .g to reconstruct the alpha that
|
|
6985
6982
|
// used to live in the HDR target before the rg11b10ufloat switch.
|
|
6986
6983
|
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
6987
|
-
// Filmic tone LUT (r16float, filterable) — sampled with the binding-2 sampler.
|
|
6988
|
-
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
6989
6984
|
// 360 backdrop equirect (PhotoDome-style skybox) — 1×1 fallback when unset.
|
|
6990
6985
|
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
6991
6986
|
// User background-effect params — dummy buffer when no effect is set. The
|
|
@@ -7000,9 +6995,6 @@ export class Engine {
|
|
|
7000
6995
|
texture: { sampleType: "depth", viewDimension: "2d", multisampled: true },
|
|
7001
6996
|
},
|
|
7002
6997
|
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
7003
|
-
// AgX's 57³ cube. Decompressed and uploaded off the critical path, so a
|
|
7004
|
-
// 1×1×1 stand-in keeps the bind group valid until it arrives.
|
|
7005
|
-
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { viewDimension: "3d" } },
|
|
7006
6998
|
// The scene's own grade cube (setStageGrade) — 1×1×1 stand-in while it
|
|
7007
6999
|
// has none, which the flag bit keeps from ever being sampled.
|
|
7008
7000
|
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, texture: { viewDimension: "3d" } },
|
|
@@ -7028,12 +7020,12 @@ export class Engine {
|
|
|
7028
7020
|
this.compositePipelineLayout = this.device.createPipelineLayout({
|
|
7029
7021
|
bindGroupLayouts: [this.compositeBindGroupLayout],
|
|
7030
7022
|
});
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7023
|
+
// The same pair setEffects asks for with no field mounts, so an install
|
|
7024
|
+
// that adds none finds it built.
|
|
7025
|
+
this.initPipelineJobs?.push(this.compositePipelines(false, false).then(([identity, gamma]) => {
|
|
7026
|
+
this.compositePipelineIdentity = identity;
|
|
7027
|
+
this.compositePipelineGamma = gamma;
|
|
7028
|
+
}));
|
|
7037
7029
|
// GPU vertex-morph compute pipeline (shared by all models; per-model bind groups).
|
|
7038
7030
|
// Bindings: 0-4 read-only storage (base pos, CSR rowStart/colMorph/colOffset, weights),
|
|
7039
7031
|
// 5 read-write storage (vertex buffer), 6 uniform (params).
|
|
@@ -7050,14 +7042,14 @@ export class Engine {
|
|
|
7050
7042
|
{ binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
|
|
7051
7043
|
],
|
|
7052
7044
|
});
|
|
7053
|
-
this.
|
|
7045
|
+
this.initComputePipeline({
|
|
7054
7046
|
label: "morph compute pipeline",
|
|
7055
7047
|
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.morphComputeBindGroupLayout] }),
|
|
7056
7048
|
compute: {
|
|
7057
7049
|
module: this.device.createShaderModule({ label: "morph compute shader", code: MORPH_COMPUTE_WGSL }),
|
|
7058
7050
|
entryPoint: "cs",
|
|
7059
7051
|
},
|
|
7060
|
-
});
|
|
7052
|
+
}, (p) => (this.morphComputePipeline = p));
|
|
7061
7053
|
// GPU frustum cull. One pipeline for the whole scene; the bind group is rebuilt
|
|
7062
7054
|
// with the buffers whenever the draw list changes. See shaders/passes/cull.ts.
|
|
7063
7055
|
this.cullBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -7077,21 +7069,23 @@ export class Engine {
|
|
|
7077
7069
|
// encoder it is set on, so a WGSL slip here would take the whole frame down —
|
|
7078
7070
|
// every pass, every model, an unrelated-looking cascade of style-group and
|
|
7079
7071
|
// effect failures. Catching it turns that into "culling is off".
|
|
7080
|
-
|
|
7081
|
-
|
|
7072
|
+
// Async, so a failure arrives as the rejection rather than a scoped error.
|
|
7073
|
+
const cullBuild = this.device
|
|
7074
|
+
.createComputePipelineAsync({
|
|
7082
7075
|
label: "cull pipeline",
|
|
7083
7076
|
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.cullBindGroupLayout] }),
|
|
7084
7077
|
compute: {
|
|
7085
7078
|
module: this.device.createShaderModule({ label: "cull compute shader", code: CULL_COMPUTE_WGSL }),
|
|
7086
7079
|
entryPoint: "cs",
|
|
7087
7080
|
},
|
|
7088
|
-
})
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7081
|
+
})
|
|
7082
|
+
.then((p) => {
|
|
7083
|
+
this.cullPipeline = p;
|
|
7084
|
+
}, (err) => {
|
|
7092
7085
|
console.error(`[cull] pipeline failed to compile — frustum culling disabled:\n${err.message}`);
|
|
7093
7086
|
this.cullPipeline = null;
|
|
7094
7087
|
});
|
|
7088
|
+
this.initPipelineJobs?.push(cullBuild);
|
|
7095
7089
|
this.bloomPassDescriptor = {
|
|
7096
7090
|
label: "bloom pass",
|
|
7097
7091
|
colorAttachments: [
|
|
@@ -7132,7 +7126,7 @@ export class Engine {
|
|
|
7132
7126
|
layout: this.pickPerFrameBindGroupLayout,
|
|
7133
7127
|
entries: [{ binding: 0, resource: { buffer: this.cameraUniformBuffer } }],
|
|
7134
7128
|
});
|
|
7135
|
-
this.
|
|
7129
|
+
this.initRenderPipeline({
|
|
7136
7130
|
label: "pick pipeline",
|
|
7137
7131
|
layout: pickPipelineLayout,
|
|
7138
7132
|
vertex: { module: pickShaderModule, buffers: fullVertexBuffers },
|
|
@@ -7146,7 +7140,7 @@ export class Engine {
|
|
|
7146
7140
|
depthWriteEnabled: true,
|
|
7147
7141
|
depthCompare: this.depthAhead,
|
|
7148
7142
|
},
|
|
7149
|
-
});
|
|
7143
|
+
}, (p) => (this.pickPipeline = p));
|
|
7150
7144
|
this.pickReadbackBuffer = this.device.createBuffer({
|
|
7151
7145
|
label: "pick readback",
|
|
7152
7146
|
size: 256,
|
|
@@ -7203,6 +7197,10 @@ export class Engine {
|
|
|
7203
7197
|
// in full the moment there is something to apply it to.
|
|
7204
7198
|
if (!this.device)
|
|
7205
7199
|
return;
|
|
7200
|
+
// Nor while init's pipelines are still compiling: init ends with its own
|
|
7201
|
+
// handleResize, which applies whatever was asked for meanwhile.
|
|
7202
|
+
if (this.initPipelineJobs)
|
|
7203
|
+
return;
|
|
7206
7204
|
// Fixed override (offline/video rendering) wins; otherwise track CSS size × dpr.
|
|
7207
7205
|
const dpr = window.devicePixelRatio || 1;
|
|
7208
7206
|
const width = this.fixedRenderSize ? this.fixedRenderSize.width : Math.floor(this.canvas.clientWidth * dpr);
|
|
@@ -7420,37 +7418,6 @@ export class Engine {
|
|
|
7420
7418
|
// rebind both against the new one.
|
|
7421
7419
|
this.buildGroundBindGroup();
|
|
7422
7420
|
this.buildMirrorSurfaceBindGroup();
|
|
7423
|
-
// Bloom pyramid: mip 0 is half-res, each subsequent mip halves again.
|
|
7424
|
-
// Mip count chosen so the coarsest mip is ≥4 px on the short side, capped at BLOOM_MAX_LEVELS.
|
|
7425
|
-
const bw = Math.max(1, Math.floor(width / 2));
|
|
7426
|
-
const bh = Math.max(1, Math.floor(height / 2));
|
|
7427
|
-
const shortSide = Math.max(1, Math.min(bw, bh));
|
|
7428
|
-
this.bloomMipCount = Math.max(1, Math.min(Engine.BLOOM_MAX_LEVELS, Math.floor(Math.log2(shortSide)) - 1));
|
|
7429
|
-
this.bloomDownTexture?.destroy();
|
|
7430
|
-
this.bloomDownTexture = this.device.createTexture({
|
|
7431
|
-
label: "bloom down pyramid",
|
|
7432
|
-
size: [bw, bh],
|
|
7433
|
-
mipLevelCount: this.bloomMipCount,
|
|
7434
|
-
format: this.hdrFormat,
|
|
7435
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
7436
|
-
});
|
|
7437
|
-
this.bloomUpTexture?.destroy();
|
|
7438
|
-
this.bloomUpTexture = this.device.createTexture({
|
|
7439
|
-
label: "bloom up pyramid",
|
|
7440
|
-
size: [bw, bh],
|
|
7441
|
-
mipLevelCount: Math.max(1, this.bloomMipCount - 1),
|
|
7442
|
-
format: this.hdrFormat,
|
|
7443
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
7444
|
-
});
|
|
7445
|
-
this.bloomDownMipViews = [];
|
|
7446
|
-
for (let i = 0; i < this.bloomMipCount; i++) {
|
|
7447
|
-
this.bloomDownMipViews.push(this.bloomDownTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }));
|
|
7448
|
-
}
|
|
7449
|
-
this.bloomUpMipViews = [];
|
|
7450
|
-
const upLevels = Math.max(1, this.bloomMipCount - 1);
|
|
7451
|
-
for (let i = 0; i < upLevels; i++) {
|
|
7452
|
-
this.bloomUpMipViews.push(this.bloomUpTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }));
|
|
7453
|
-
}
|
|
7454
7421
|
this.depthTexture?.destroy();
|
|
7455
7422
|
this.depthTexture = this.device.createTexture({
|
|
7456
7423
|
label: "depth texture",
|
|
@@ -7562,49 +7529,10 @@ export class Engine {
|
|
|
7562
7529
|
},
|
|
7563
7530
|
],
|
|
7564
7531
|
};
|
|
7565
|
-
this.
|
|
7566
|
-
|
|
7567
|
-
//
|
|
7568
|
-
this.
|
|
7569
|
-
label: "bloom blit bind group",
|
|
7570
|
-
layout: this.bloomBlitBindGroupLayout,
|
|
7571
|
-
entries: [
|
|
7572
|
-
{ binding: 0, resource: this.hdrResolveTexture.createView() },
|
|
7573
|
-
{ binding: 1, resource: { buffer: this.bloomBlitUniformBuffer } },
|
|
7574
|
-
{ binding: 2, resource: this.maskResolveView },
|
|
7575
|
-
],
|
|
7576
|
-
});
|
|
7577
|
-
// Downsample[i] reads bloomDown mip (i-1), writes bloomDown mip i. i ∈ [1..N-1].
|
|
7578
|
-
this.bloomDownsampleBindGroups = [];
|
|
7579
|
-
for (let i = 1; i < this.bloomMipCount; i++) {
|
|
7580
|
-
this.bloomDownsampleBindGroups.push(this.device.createBindGroup({
|
|
7581
|
-
label: `bloom downsample ${i}`,
|
|
7582
|
-
layout: this.bloomDownsampleBindGroupLayout,
|
|
7583
|
-
entries: [
|
|
7584
|
-
{ binding: 0, resource: this.bloomDownMipViews[i - 1] },
|
|
7585
|
-
{ binding: 1, resource: this.bloomSampler },
|
|
7586
|
-
],
|
|
7587
|
-
}));
|
|
7588
|
-
}
|
|
7589
|
-
// Upsample[i] writes bloomUp mip i. Coarsest step reads bloomDown[N-1] (no prior up yet);
|
|
7590
|
-
// subsequent steps read bloomUp[i+1]. Both read bloomDown[i] as the base (additive combine).
|
|
7591
|
-
this.bloomUpsampleBindGroups = [];
|
|
7592
|
-
const topIdx = this.bloomMipCount - 2;
|
|
7593
|
-
for (let i = topIdx; i >= 0; i--) {
|
|
7594
|
-
const srcView = i === topIdx ? this.bloomDownMipViews[this.bloomMipCount - 1] : this.bloomUpMipViews[i + 1];
|
|
7595
|
-
this.bloomUpsampleBindGroups.push(this.device.createBindGroup({
|
|
7596
|
-
label: `bloom upsample ${i}`,
|
|
7597
|
-
layout: this.bloomUpsampleBindGroupLayout,
|
|
7598
|
-
entries: [
|
|
7599
|
-
{ binding: 0, resource: srcView },
|
|
7600
|
-
{ binding: 1, resource: this.bloomDownMipViews[i] },
|
|
7601
|
-
{ binding: 2, resource: this.bloomSampler },
|
|
7602
|
-
{ binding: 3, resource: { buffer: this.bloomUpsampleUniformBuffer } },
|
|
7603
|
-
],
|
|
7604
|
-
}));
|
|
7605
|
-
}
|
|
7606
|
-
// Composite reads bloomUp mip 0 (full pyramid collapsed); fallback to bloomDown mip 0 if no upsample level.
|
|
7607
|
-
this.compositeBloomView = this.bloomMipCount > 1 ? this.bloomUpMipViews[0] : this.bloomDownMipViews[0];
|
|
7532
|
+
if (this.compositeBindGroupLayout && this.bloomPrefilterBindGroupLayout) {
|
|
7533
|
+
this.buildBloomTargets();
|
|
7534
|
+
// The composite reads the finished chain: up[0], or down[0] when there is one level.
|
|
7535
|
+
this.compositeBloomView = this.bloomLevels > 1 ? this.bloomUpViews[0] : this.bloomDownViews[0];
|
|
7608
7536
|
this.rebuildCompositeBindGroup();
|
|
7609
7537
|
}
|
|
7610
7538
|
this.writeCompositeViewUniforms();
|
|
@@ -7867,16 +7795,16 @@ export class Engine {
|
|
|
7867
7795
|
},
|
|
7868
7796
|
multisample: { count: Engine.OVERLAY_SAMPLE_COUNT },
|
|
7869
7797
|
};
|
|
7870
|
-
this.overlayPipeline =
|
|
7798
|
+
this.initRenderPipeline(overlayPipelineDescriptor, (p) => (this.overlayPipeline = p));
|
|
7871
7799
|
// The solid volumes: the same shader and layout, with no depth write and no
|
|
7872
7800
|
// culling. A translucent body must not hide the rig behind it, and you have
|
|
7873
7801
|
// to see its far wall for it to read as a volume rather than a silhouette.
|
|
7874
|
-
this.
|
|
7802
|
+
this.initRenderPipeline({
|
|
7875
7803
|
...overlayPipelineDescriptor,
|
|
7876
7804
|
label: "overlay solid pipeline",
|
|
7877
7805
|
primitive: { topology: "triangle-list", cullMode: "none" },
|
|
7878
7806
|
depthStencil: { format: "depth24plus", depthWriteEnabled: false, depthCompare: "always" },
|
|
7879
|
-
});
|
|
7807
|
+
}, (p) => (this.overlaySolidPipeline = p));
|
|
7880
7808
|
const compositeShader = this.device.createShaderModule({
|
|
7881
7809
|
label: "overlay composite shader",
|
|
7882
7810
|
code: OVERLAY_COMPOSITE_SHADER_WGSL,
|
|
@@ -7964,7 +7892,7 @@ export class Engine {
|
|
|
7964
7892
|
],
|
|
7965
7893
|
});
|
|
7966
7894
|
const wireShader = this.device.createShaderModule({ label: "wireframe shader", code: WIREFRAME_SHADER_WGSL });
|
|
7967
|
-
this.
|
|
7895
|
+
this.initRenderPipeline({
|
|
7968
7896
|
label: "wireframe pipeline",
|
|
7969
7897
|
layout: this.device.createPipelineLayout({
|
|
7970
7898
|
label: "wireframe pipeline layout",
|
|
@@ -7991,7 +7919,7 @@ export class Engine {
|
|
|
7991
7919
|
// so a bone behind a triangle must not be punched out by it.
|
|
7992
7920
|
depthStencil: { format: "depth24plus", depthWriteEnabled: false, depthCompare: this.depthAhead },
|
|
7993
7921
|
multisample: { count: Engine.OVERLAY_SAMPLE_COUNT },
|
|
7994
|
-
});
|
|
7922
|
+
}, (p) => (this.wireframePipeline = p));
|
|
7995
7923
|
// The mesh's own depth, so the wireframe can be occluded by the body it
|
|
7996
7924
|
// belongs to. Occluded is the default everywhere — Blender's edit mode, Maya,
|
|
7997
7925
|
// three's and Babylon's wireframe materials all depth-test, and X-ray is a
|
|
@@ -9555,6 +9483,29 @@ export class Engine {
|
|
|
9555
9483
|
this.device.queue.writeBuffer(inst.lightBuffer, 0, o);
|
|
9556
9484
|
return true;
|
|
9557
9485
|
}
|
|
9486
|
+
/**
|
|
9487
|
+
* Light one model with more than the world gives it: a FILL, linear RGB times
|
|
9488
|
+
* its surface colour, added after its material graph. Null takes it away again.
|
|
9489
|
+
*
|
|
9490
|
+
* After the graph, not in its ambient: an NPR ramp reads an ambient fill as
|
|
9491
|
+
* light, so sliding it moved her shadows and flipped whole regions across a
|
|
9492
|
+
* hard step. Added after, it brightens her evenly and every shadow stays put.
|
|
9493
|
+
*
|
|
9494
|
+
* A game lights its stage and its characters apart. Aether Gazer's rooms are
|
|
9495
|
+
* lit by their lamps and a near-black ambient, while its characters take a
|
|
9496
|
+
* flat base light of their own — so a stage whose World is right for the room
|
|
9497
|
+
* leaves a face turned from the lamps in the dark. This is that second light,
|
|
9498
|
+
* for the models the host counts as cast. A model drawn by the game's own
|
|
9499
|
+
* shaders (src/unity) lights itself and takes none of it.
|
|
9500
|
+
*/
|
|
9501
|
+
setModelFill(name, fill) {
|
|
9502
|
+
const inst = this.modelInstances.get(name);
|
|
9503
|
+
if (!inst || !this.device)
|
|
9504
|
+
return false;
|
|
9505
|
+
inst.objectLight.set(fill ? [fill.x, fill.y, fill.z, 0] : [0, 0, 0, 0], 44);
|
|
9506
|
+
this.device.queue.writeBuffer(inst.lightBuffer, 0, inst.objectLight);
|
|
9507
|
+
return true;
|
|
9508
|
+
}
|
|
9558
9509
|
/** The per-model group: its skinning matrices and its ObjectLight. */
|
|
9559
9510
|
perInstanceBindGroup(name, skinMatrixBuffer, light) {
|
|
9560
9511
|
return this.device.createBindGroup({
|
|
@@ -9573,6 +9524,9 @@ export class Engine {
|
|
|
9573
9524
|
// Before the texture cache below frees it: a stale entry here would hand a
|
|
9574
9525
|
// destroyed texture to the next setPlaneFrame.
|
|
9575
9526
|
this.planeTextures.delete(name);
|
|
9527
|
+
// Its native look too: it skins from this model's buffers, which are
|
|
9528
|
+
// destroyed below, and would otherwise go on drawing from them.
|
|
9529
|
+
this.nativeLooks?.remove(name);
|
|
9576
9530
|
inst.model.stop();
|
|
9577
9531
|
for (const path of inst.textureCacheKeys) {
|
|
9578
9532
|
const tex = this.textureCache.get(path);
|
|
@@ -12095,8 +12049,9 @@ export class Engine {
|
|
|
12095
12049
|
}));
|
|
12096
12050
|
// Its ObjectLight: the world's ambient, and the layers its kind draws on —
|
|
12097
12051
|
// the cast on the character layer as well as the default one. Words:
|
|
12098
|
-
// layers [0..3], ambient flag [4..7], SH [8..43] — see
|
|
12099
|
-
|
|
12052
|
+
// layers [0..3], ambient flag [4..7], SH [8..43], fill [44..47] — see
|
|
12053
|
+
// materials/common.ts.
|
|
12054
|
+
const objectLight = new Float32Array(48);
|
|
12100
12055
|
new Uint32Array(objectLight.buffer)[0] =
|
|
12101
12056
|
isStage || isPlane || isProp ? RENDERING_LAYER_DEFAULT : (RENDERING_LAYER_DEFAULT | RENDERING_LAYER_CHARACTER) >>> 0;
|
|
12102
12057
|
const lightBuffer = this.device.createBuffer({
|
|
@@ -12303,7 +12258,10 @@ export class Engine {
|
|
|
12303
12258
|
this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices);
|
|
12304
12259
|
}
|
|
12305
12260
|
buildGroundPipeline(soft, mirrored = false) {
|
|
12306
|
-
return this.createRenderPipeline(
|
|
12261
|
+
return this.createRenderPipeline(this.groundPipelineConfig(soft, mirrored));
|
|
12262
|
+
}
|
|
12263
|
+
groundPipelineConfig(soft, mirrored = false) {
|
|
12264
|
+
return {
|
|
12307
12265
|
...this.groundShadowPipelineDesc,
|
|
12308
12266
|
// A REFLECTION FLIPS WINDING — determinant -1, see reflection.ts. The
|
|
12309
12267
|
// ground culls back faces, so drawn into the mirror pass with the ordinary
|
|
@@ -12313,11 +12271,8 @@ export class Engine {
|
|
|
12313
12271
|
// the mirror, and the same answer the scene-pass pipelines already use.
|
|
12314
12272
|
cullMode: mirrored ? "none" : this.groundShadowPipelineDesc.cullMode,
|
|
12315
12273
|
label: `ground shadow pipeline${soft ? " (soft)" : ""}${mirrored ? " (mirror)" : ""}`,
|
|
12316
|
-
shaderModule: this.
|
|
12317
|
-
|
|
12318
|
-
code: groundShaderWgsl(soft),
|
|
12319
|
-
}),
|
|
12320
|
-
});
|
|
12274
|
+
shaderModule: this.cachedShaderModule(groundShaderWgsl(soft), soft ? "ground shadow (soft)" : "ground shadow"),
|
|
12275
|
+
};
|
|
12321
12276
|
}
|
|
12322
12277
|
/** The ground's pipeline for the mirror pass, built on the first frame a
|
|
12323
12278
|
* mirror actually needs it. */
|
|
@@ -12687,6 +12642,17 @@ export class Engine {
|
|
|
12687
12642
|
],
|
|
12688
12643
|
});
|
|
12689
12644
|
outline = { bindGroup: outlineBindGroup };
|
|
12645
|
+
if (!inst.outlineVertexBuffer) {
|
|
12646
|
+
const data = inst.model.getOutlineVertices();
|
|
12647
|
+
const buf = this.device.createBuffer({
|
|
12648
|
+
label: `${prefix}outline normals`,
|
|
12649
|
+
size: data.byteLength,
|
|
12650
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
12651
|
+
});
|
|
12652
|
+
this.device.queue.writeBuffer(buf, 0, data);
|
|
12653
|
+
inst.gpuBuffers.push(buf);
|
|
12654
|
+
inst.outlineVertexBuffer = buf;
|
|
12655
|
+
}
|
|
12690
12656
|
}
|
|
12691
12657
|
// Model-space AABB for the cull compute, grown by the three things that can
|
|
12692
12658
|
// put geometry outside the vertices it was measured from: a vertex morph,
|
|
@@ -14252,54 +14218,16 @@ export class Engine {
|
|
|
14252
14218
|
// The field mounts, likewise: after the scene so foregrounds can read its
|
|
14253
14219
|
// depth, before the composite that samples both layers.
|
|
14254
14220
|
this.renderFieldPass(encoder);
|
|
14255
|
-
// Bloom
|
|
14256
|
-
//
|
|
14257
|
-
//
|
|
14258
|
-
//
|
|
14259
|
-
//
|
|
14260
|
-
//
|
|
14261
|
-
//
|
|
14262
|
-
|
|
14263
|
-
|
|
14264
|
-
//
|
|
14265
|
-
// is therefore invisible in the frame and nine render passes cheaper.
|
|
14266
|
-
if (this.bloomContributes() && this.bloomBlitBindGroup && this.compositeBindGroup && this.bloomMipCount > 0) {
|
|
14267
|
-
const bloomAtt = this.bloomPassDescriptor.colorAttachments;
|
|
14268
|
-
// 1. Blit — opens the pyramid's timing span. See stampOpen: the nine
|
|
14269
|
-
// passes below read as ONE component, which is the only useful grain.
|
|
14270
|
-
bloomAtt[0].view = this.bloomDownMipViews[0];
|
|
14271
|
-
this.bloomPassDescriptor.timestampWrites = this.stampOpen("bloom");
|
|
14272
|
-
const pBlit = encoder.beginRenderPass(this.bloomPassDescriptor);
|
|
14273
|
-
pBlit.setPipeline(this.bloomBlitPipeline);
|
|
14274
|
-
pBlit.setBindGroup(0, this.bloomBlitBindGroup);
|
|
14275
|
-
pBlit.draw(3);
|
|
14276
|
-
pBlit.end();
|
|
14277
|
-
// 2. Downsample chain
|
|
14278
|
-
this.bloomPassDescriptor.timestampWrites = undefined;
|
|
14279
|
-
for (let i = 1; i < this.bloomMipCount; i++) {
|
|
14280
|
-
bloomAtt[0].view = this.bloomDownMipViews[i];
|
|
14281
|
-
const p = encoder.beginRenderPass(this.bloomPassDescriptor);
|
|
14282
|
-
p.setPipeline(this.bloomDownsamplePipeline);
|
|
14283
|
-
p.setBindGroup(0, this.bloomDownsampleBindGroups[i - 1]);
|
|
14284
|
-
p.draw(3);
|
|
14285
|
-
p.end();
|
|
14286
|
-
}
|
|
14287
|
-
// 3. Upsample chain (coarsest to finest; bindGroups[0] is the coarsest step)
|
|
14288
|
-
const upSteps = this.bloomUpsampleBindGroups.length;
|
|
14289
|
-
const topIdx = this.bloomMipCount - 2;
|
|
14290
|
-
for (let k = 0; k < upSteps; k++) {
|
|
14291
|
-
const levelIdx = topIdx - k; // writes bloomUp[levelIdx]
|
|
14292
|
-
bloomAtt[0].view = this.bloomUpMipViews[levelIdx];
|
|
14293
|
-
// The LAST upsample closes the span opened on the blit.
|
|
14294
|
-
this.bloomPassDescriptor.timestampWrites = k === upSteps - 1 ? this.stampClose("bloom") : undefined;
|
|
14295
|
-
const p = encoder.beginRenderPass(this.bloomPassDescriptor);
|
|
14296
|
-
p.setPipeline(this.bloomUpsamplePipeline);
|
|
14297
|
-
p.setBindGroup(0, this.bloomUpsampleBindGroups[k]);
|
|
14298
|
-
p.draw(3);
|
|
14299
|
-
p.end();
|
|
14300
|
-
}
|
|
14301
|
-
}
|
|
14302
|
-
// Composite: HDR + bloom → Filmic tonemap → swapchain.
|
|
14221
|
+
// Bloom (renderBloom): prefilter, the Gaussian chain down, the scatter
|
|
14222
|
+
// blend back up; the composite adds up[0] × tint × intensity before the view
|
|
14223
|
+
// transform. bloomContributes() gates the whole chain, not just its
|
|
14224
|
+
// intensity. The composite still SAMPLES up[0] unconditionally, which is safe
|
|
14225
|
+
// and deliberate: it scales what it reads by the same effective intensity, so
|
|
14226
|
+
// a stale or never-written chain is multiplied by zero. Skipping the build is
|
|
14227
|
+
// therefore invisible in the frame and a couple of dozen render passes cheaper.
|
|
14228
|
+
if (this.bloomContributes() && this.bloomPrefilterBindGroup && this.compositeBindGroup && this.bloomLevels > 0)
|
|
14229
|
+
this.renderBloom(encoder);
|
|
14230
|
+
// Composite: HDR + bloom → view transform → swapchain.
|
|
14303
14231
|
const swapchainView = this.context.getCurrentTexture().createView();
|
|
14304
14232
|
const compositeAttachment = this.compositePassDescriptor.colorAttachments[0];
|
|
14305
14233
|
compositeAttachment.view = swapchainView;
|
|
@@ -14444,11 +14372,12 @@ export class Engine {
|
|
|
14444
14372
|
outgoing.push([id, install]);
|
|
14445
14373
|
}
|
|
14446
14374
|
}
|
|
14447
|
-
|
|
14448
|
-
|
|
14449
|
-
|
|
14450
|
-
|
|
14451
|
-
|
|
14375
|
+
// ALL GROUPS AT ONCE. Each install touches only its own id — its entry in
|
|
14376
|
+
// styleGroups, its uniform buffer, the draw calls already bound to it — so
|
|
14377
|
+
// the order they land in changes nothing, and each keeps its own staleness
|
|
14378
|
+
// guard (styleGroupGen). Awaiting them in turn made a model's styling the
|
|
14379
|
+
// sum of its compiles; the GPU process runs them side by side.
|
|
14380
|
+
const groupResults = (await Promise.all(groups.map((g) => this.compileAndInstallGroup(inst, g)))).map((r, i) => ({ groupId: groups[i].id, diagnostics: r.diagnostics, ok: r.ok }));
|
|
14452
14381
|
// Repoints every draw call away from the outgoing installs (among
|
|
14453
14382
|
// everything else) — only past this line is destroying them safe.
|
|
14454
14383
|
this.assignDrawCallGroups(inst, claimed);
|
|
@@ -14463,6 +14392,38 @@ export class Engine {
|
|
|
14463
14392
|
conflicts: [...conflicts],
|
|
14464
14393
|
};
|
|
14465
14394
|
}
|
|
14395
|
+
/**
|
|
14396
|
+
* Compile style groups' pipelines before any model needs them — a scene's
|
|
14397
|
+
* groups while its bundle is still downloading. Installs nothing: the
|
|
14398
|
+
* pipelines land in the shader cache, and applyStyleGroups later finds them
|
|
14399
|
+
* there (or still in flight). A group that does not compile is skipped
|
|
14400
|
+
* silently; its real install reports it.
|
|
14401
|
+
*/
|
|
14402
|
+
async prewarmStyleGroups(groups) {
|
|
14403
|
+
if (!this.device)
|
|
14404
|
+
return;
|
|
14405
|
+
await Promise.all(groups.map(async (g) => {
|
|
14406
|
+
const renderClass = g.renderClass ?? "auto";
|
|
14407
|
+
const alphaMode = g.alphaMode ?? "opaque";
|
|
14408
|
+
const blend = g.blend ?? "over";
|
|
14409
|
+
let result;
|
|
14410
|
+
try {
|
|
14411
|
+
result = compileGraph(g.graph, { renderClass, alphaMode, blend });
|
|
14412
|
+
}
|
|
14413
|
+
catch {
|
|
14414
|
+
return;
|
|
14415
|
+
}
|
|
14416
|
+
if (!result.ok)
|
|
14417
|
+
return;
|
|
14418
|
+
const module = this.cachedShaderModule(result.wgsl, `style group: ${g.id} (${renderClass})`);
|
|
14419
|
+
await Promise.all([
|
|
14420
|
+
this.createRenderClassPipeline(renderClass, module, false, true, false, blend),
|
|
14421
|
+
this.createRenderClassPipeline(renderClass, module, false, false, false, blend),
|
|
14422
|
+
renderClass === "hair" ? this.createRenderClassPipeline(renderClass, module, true, true, false, blend) : null,
|
|
14423
|
+
renderClass === "eye" ? this.createRenderClassPipeline(renderClass, module, false, true, true, blend) : null,
|
|
14424
|
+
]).catch(() => { });
|
|
14425
|
+
}));
|
|
14426
|
+
}
|
|
14466
14427
|
/** Add or replace a single style group by id. `opts` may carry a `previewNode` for the
|
|
14467
14428
|
* editor's node-output preview workflow. */
|
|
14468
14429
|
async upsertStyleGroup(modelName, group, opts) {
|
|
@@ -14814,9 +14775,35 @@ export class Engine {
|
|
|
14814
14775
|
const generation = (inst.styleGroupGen.get(group.id) ?? 0) + 1;
|
|
14815
14776
|
inst.styleGroupGen.set(group.id, generation);
|
|
14816
14777
|
this.device.pushErrorScope("validation");
|
|
14817
|
-
const module = this.
|
|
14818
|
-
const
|
|
14819
|
-
|
|
14778
|
+
const module = this.cachedShaderModule(result.wgsl, `style group: ${group.id} (${renderClass})`);
|
|
14779
|
+
const scopePromise = this.device.popErrorScope();
|
|
14780
|
+
// EVERY VARIANT AT ONCE, and the module's diagnostics read beside them
|
|
14781
|
+
// rather than first: the compiles run in parallel in the GPU process, and
|
|
14782
|
+
// awaiting each in turn (and the diagnostics before any) made a group cost
|
|
14783
|
+
// the sum of its pipelines instead of the longest.
|
|
14784
|
+
const blend = group.blend ?? "over";
|
|
14785
|
+
// The single-sided twins a draw of this group will ask for (sidedPipeline),
|
|
14786
|
+
// built with their pipelines rather than synchronously at the first draw.
|
|
14787
|
+
const singleSided = inst.drawCalls.some((dc) => !dc.doubleSided && group.materials.includes(dc.materialName));
|
|
14788
|
+
const variant = (overEyes, depthWrite, mirrored) => this.createRenderClassPipeline(renderClass, module, overEyes, depthWrite, mirrored, blend).then(async (p) => {
|
|
14789
|
+
if (singleSided && !mirrored)
|
|
14790
|
+
await this.buildSidedTwin(p, false);
|
|
14791
|
+
return p;
|
|
14792
|
+
});
|
|
14793
|
+
const variants = Promise.all([
|
|
14794
|
+
variant(false, true, false),
|
|
14795
|
+
// The depth-write-off twin: stage transparency draws with it (see
|
|
14796
|
+
// pipelineForDrawCall), and a future OIT path would too.
|
|
14797
|
+
variant(false, false, false),
|
|
14798
|
+
renderClass === "hair" ? variant(true, true, false) : undefined,
|
|
14799
|
+
// Only the eye needs one: every other class culls "none", which a flipped
|
|
14800
|
+
// winding leaves alone.
|
|
14801
|
+
renderClass === "eye" ? variant(false, true, true) : undefined,
|
|
14802
|
+
]);
|
|
14803
|
+
// Handled below either way; this keeps a rejection off the console while
|
|
14804
|
+
// the diagnostics are read.
|
|
14805
|
+
variants.catch(() => { });
|
|
14806
|
+
const [info, scopeError] = await Promise.all([module.getCompilationInfo(), scopePromise]);
|
|
14820
14807
|
const diagnostics = [...result.diagnostics];
|
|
14821
14808
|
for (const msg of info.messages) {
|
|
14822
14809
|
if (msg.type !== "error")
|
|
@@ -14826,6 +14813,7 @@ export class Engine {
|
|
|
14826
14813
|
if (diagnostics.some((d) => d.severity === "error") || scopeError) {
|
|
14827
14814
|
if (scopeError && !diagnostics.some((d) => d.severity === "error"))
|
|
14828
14815
|
diagnostics.push({ severity: "error", message: `WGSL: ${scopeError.message}` });
|
|
14816
|
+
this.forgetShaderModule(result.wgsl);
|
|
14829
14817
|
return { ok: false, diagnostics, slotMap: result.slotMap };
|
|
14830
14818
|
}
|
|
14831
14819
|
let pipeline;
|
|
@@ -14833,17 +14821,8 @@ export class Engine {
|
|
|
14833
14821
|
let overEyesPipeline;
|
|
14834
14822
|
let mirrorPipeline;
|
|
14835
14823
|
try {
|
|
14836
|
-
|
|
14837
|
-
pipeline
|
|
14838
|
-
// The depth-write-off twin: stage transparency draws with it (see
|
|
14839
|
-
// pipelineForDrawCall), and a future OIT path would too.
|
|
14840
|
-
pipelineNoDepthWrite = await this.createRenderClassPipeline(renderClass, module, false, false, false, blend);
|
|
14841
|
-
if (renderClass === "hair")
|
|
14842
|
-
overEyesPipeline = await this.createRenderClassPipeline(renderClass, module, true, true, false, blend);
|
|
14843
|
-
// Only the eye needs one: every other class culls "none", which a flipped
|
|
14844
|
-
// winding leaves alone.
|
|
14845
|
-
if (renderClass === "eye")
|
|
14846
|
-
mirrorPipeline = await this.createRenderClassPipeline(renderClass, module, false, true, true, blend);
|
|
14824
|
+
;
|
|
14825
|
+
[pipeline, pipelineNoDepthWrite, overEyesPipeline, mirrorPipeline] = await variants;
|
|
14847
14826
|
}
|
|
14848
14827
|
catch (e) {
|
|
14849
14828
|
diagnostics.push({ severity: "error", message: `pipeline creation failed: ${e.message}` });
|
|
@@ -15064,8 +15043,9 @@ export class Engine {
|
|
|
15064
15043
|
fragment: { module, constants, targets: blend === "additive" ? this.sceneTargetsAdditive : this.sceneTargets },
|
|
15065
15044
|
depthStencil,
|
|
15066
15045
|
};
|
|
15067
|
-
return this.
|
|
15068
|
-
this.pipelineDescs.
|
|
15046
|
+
return this.cachedRenderPipeline(desc).then((pipeline) => {
|
|
15047
|
+
if (!this.pipelineDescs.has(pipeline))
|
|
15048
|
+
this.pipelineDescs.set(pipeline, desc);
|
|
15069
15049
|
return pipeline;
|
|
15070
15050
|
});
|
|
15071
15051
|
}
|
|
@@ -15076,6 +15056,151 @@ export class Engine {
|
|
|
15076
15056
|
this.pipelineDescs.set(pipeline, desc);
|
|
15077
15057
|
return pipeline;
|
|
15078
15058
|
}
|
|
15059
|
+
static trimCache(cache, max) {
|
|
15060
|
+
while (cache.size > max)
|
|
15061
|
+
cache.delete(cache.keys().next().value);
|
|
15062
|
+
}
|
|
15063
|
+
/** A small stable id per GPU object, for cache keys. */
|
|
15064
|
+
gpuObjectId(o) {
|
|
15065
|
+
let id = this.gpuObjectIds.get(o);
|
|
15066
|
+
if (id === undefined) {
|
|
15067
|
+
id = this.gpuObjectNext++;
|
|
15068
|
+
this.gpuObjectIds.set(o, id);
|
|
15069
|
+
}
|
|
15070
|
+
return id;
|
|
15071
|
+
}
|
|
15072
|
+
/** One module per WGSL source. */
|
|
15073
|
+
cachedShaderModule(code, label) {
|
|
15074
|
+
let module = this.shaderModuleCache.get(code);
|
|
15075
|
+
if (module) {
|
|
15076
|
+
this.shaderModuleCache.delete(code);
|
|
15077
|
+
}
|
|
15078
|
+
else {
|
|
15079
|
+
module = this.device.createShaderModule({ label, code });
|
|
15080
|
+
}
|
|
15081
|
+
this.shaderModuleCache.set(code, module);
|
|
15082
|
+
Engine.trimCache(this.shaderModuleCache, Engine.SHADER_CACHE_MAX);
|
|
15083
|
+
return module;
|
|
15084
|
+
}
|
|
15085
|
+
/** Drop a source whose module did not validate, so a retry reports afresh. */
|
|
15086
|
+
forgetShaderModule(code) {
|
|
15087
|
+
this.shaderModuleCache.delete(code);
|
|
15088
|
+
}
|
|
15089
|
+
/** One bind group layout per entry list. */
|
|
15090
|
+
cachedBindGroupLayout(desc) {
|
|
15091
|
+
const key = JSON.stringify(desc.entries);
|
|
15092
|
+
let layout = this.bindGroupLayoutCache.get(key);
|
|
15093
|
+
if (!layout) {
|
|
15094
|
+
layout = this.device.createBindGroupLayout(desc);
|
|
15095
|
+
this.bindGroupLayoutCache.set(key, layout);
|
|
15096
|
+
}
|
|
15097
|
+
return layout;
|
|
15098
|
+
}
|
|
15099
|
+
/** One pipeline layout per bind group layout list. */
|
|
15100
|
+
cachedPipelineLayout(bindGroupLayouts) {
|
|
15101
|
+
const key = bindGroupLayouts.map((l) => this.gpuObjectId(l)).join(",");
|
|
15102
|
+
let layout = this.pipelineLayoutCache.get(key);
|
|
15103
|
+
if (!layout) {
|
|
15104
|
+
layout = this.device.createPipelineLayout({ bindGroupLayouts });
|
|
15105
|
+
this.pipelineLayoutCache.set(key, layout);
|
|
15106
|
+
}
|
|
15107
|
+
return layout;
|
|
15108
|
+
}
|
|
15109
|
+
pipelineKey(kind, desc) {
|
|
15110
|
+
return (kind +
|
|
15111
|
+
JSON.stringify(desc, (k, v) => k === "label" ? undefined : (k === "module" || k === "layout") && v && typeof v === "object" ? `#${this.gpuObjectId(v)}` : v));
|
|
15112
|
+
}
|
|
15113
|
+
cachedPipeline(key, build) {
|
|
15114
|
+
let p = this.pipelineCache.get(key);
|
|
15115
|
+
if (p) {
|
|
15116
|
+
this.pipelineCache.delete(key);
|
|
15117
|
+
}
|
|
15118
|
+
else {
|
|
15119
|
+
p = build().catch((e) => {
|
|
15120
|
+
this.pipelineCache.delete(key);
|
|
15121
|
+
throw e;
|
|
15122
|
+
});
|
|
15123
|
+
}
|
|
15124
|
+
this.pipelineCache.set(key, p);
|
|
15125
|
+
Engine.trimCache(this.pipelineCache, Engine.PIPELINE_CACHE_MAX);
|
|
15126
|
+
return p;
|
|
15127
|
+
}
|
|
15128
|
+
/** createRenderPipelineAsync, once per module + state. */
|
|
15129
|
+
cachedRenderPipeline(desc) {
|
|
15130
|
+
return this.cachedPipeline(this.pipelineKey("render", desc), () => this.device.createRenderPipelineAsync(desc));
|
|
15131
|
+
}
|
|
15132
|
+
/** createComputePipelineAsync, once per module + state. */
|
|
15133
|
+
cachedComputePipeline(desc) {
|
|
15134
|
+
return this.cachedPipeline(this.pipelineKey("compute", desc), () => this.device.createComputePipelineAsync(desc));
|
|
15135
|
+
}
|
|
15136
|
+
/**
|
|
15137
|
+
* A pipeline init builds: async, in parallel with the rest, and in place by
|
|
15138
|
+
* the time init returns (it awaits every job). Outside init — a pass built
|
|
15139
|
+
* on first use — it is the synchronous build it always was.
|
|
15140
|
+
*/
|
|
15141
|
+
initRenderPipeline(desc, assign, twins = false) {
|
|
15142
|
+
if (!this.initPipelineJobs) {
|
|
15143
|
+
const pipeline = this.device.createRenderPipeline(desc);
|
|
15144
|
+
if (twins)
|
|
15145
|
+
this.pipelineDescs.set(pipeline, desc);
|
|
15146
|
+
assign(pipeline);
|
|
15147
|
+
return;
|
|
15148
|
+
}
|
|
15149
|
+
this.initPipelineJobs.push(this.cachedRenderPipeline(desc)
|
|
15150
|
+
// A rejected build is rebuilt the old way, which hands back the invalid
|
|
15151
|
+
// pipeline and reports through uncapturederror — a bad init pipeline
|
|
15152
|
+
// was never fatal to init, and is not now.
|
|
15153
|
+
.catch(() => this.device.createRenderPipeline(desc))
|
|
15154
|
+
.then((pipeline) => {
|
|
15155
|
+
assign(pipeline);
|
|
15156
|
+
if (!twins)
|
|
15157
|
+
return;
|
|
15158
|
+
this.pipelineDescs.set(pipeline, desc);
|
|
15159
|
+
// The mirror's twin too, but not waited for: only a scene with a
|
|
15160
|
+
// mirror draws it, and it is a cache hit on the module just built.
|
|
15161
|
+
void this.buildSidedTwin(pipeline, true);
|
|
15162
|
+
return this.buildSidedTwin(pipeline, false);
|
|
15163
|
+
}));
|
|
15164
|
+
}
|
|
15165
|
+
/** The compute twin of initRenderPipeline. */
|
|
15166
|
+
initComputePipeline(desc, assign) {
|
|
15167
|
+
if (!this.initPipelineJobs) {
|
|
15168
|
+
assign(this.device.createComputePipeline(desc));
|
|
15169
|
+
return;
|
|
15170
|
+
}
|
|
15171
|
+
this.initPipelineJobs.push(this.cachedComputePipeline(desc)
|
|
15172
|
+
.catch(() => this.device.createComputePipeline(desc))
|
|
15173
|
+
.then(assign));
|
|
15174
|
+
}
|
|
15175
|
+
/**
|
|
15176
|
+
* Build a model pipeline's single-sided twin off the draw path (see
|
|
15177
|
+
* sidedPipeline). Resolves once it is in place; never rejects — a twin that
|
|
15178
|
+
* fails leaves the draw on its double-sided pipeline, as before it existed.
|
|
15179
|
+
*/
|
|
15180
|
+
buildSidedTwin(pipeline, mirrored) {
|
|
15181
|
+
const desc = this.pipelineDescs.get(pipeline);
|
|
15182
|
+
if (!desc || (desc.primitive?.cullMode ?? "none") !== "none")
|
|
15183
|
+
return Promise.resolve();
|
|
15184
|
+
const cache = mirrored ? this.singleSidedMirror : this.singleSided;
|
|
15185
|
+
const pending = mirrored ? this.sidedPendingMirror : this.sidedPending;
|
|
15186
|
+
if (cache.has(pipeline) || pending.has(pipeline))
|
|
15187
|
+
return Promise.resolve();
|
|
15188
|
+
pending.add(pipeline);
|
|
15189
|
+
return this.cachedRenderPipeline({
|
|
15190
|
+
...desc,
|
|
15191
|
+
label: `${desc.label ?? "pipeline"} (single-sided${mirrored ? ", mirror" : ""})`,
|
|
15192
|
+
primitive: { ...desc.primitive, cullMode: mirrored ? "back" : "front" },
|
|
15193
|
+
}).then((twin) => {
|
|
15194
|
+
cache.set(pipeline, twin);
|
|
15195
|
+
pending.delete(pipeline);
|
|
15196
|
+
// A bundle recorded meanwhile holds the double-sided stand-in.
|
|
15197
|
+
this.bundlesDirty = true;
|
|
15198
|
+
}, () => {
|
|
15199
|
+
// Remembered as itself, so the draw stops asking.
|
|
15200
|
+
cache.set(pipeline, pipeline);
|
|
15201
|
+
pending.delete(pipeline);
|
|
15202
|
+
});
|
|
15203
|
+
}
|
|
15079
15204
|
/**
|
|
15080
15205
|
* The pipeline a draw should use once its double-sided flag is honoured.
|
|
15081
15206
|
*
|
|
@@ -15095,17 +15220,15 @@ export class Engine {
|
|
|
15095
15220
|
const desc = this.pipelineDescs.get(pipeline);
|
|
15096
15221
|
if (!desc || (desc.primitive?.cullMode ?? "none") !== "none")
|
|
15097
15222
|
return pipeline;
|
|
15098
|
-
const
|
|
15099
|
-
|
|
15100
|
-
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
}
|
|
15108
|
-
return twin;
|
|
15223
|
+
const twin = (mirrored ? this.singleSidedMirror : this.singleSided).get(pipeline);
|
|
15224
|
+
if (twin)
|
|
15225
|
+
return twin;
|
|
15226
|
+
// NEVER BUILT HERE SYNCHRONOUSLY: a sync build stalls the GPU process for
|
|
15227
|
+
// the whole compile, mid-frame. The camera's twins are built beside their
|
|
15228
|
+
// pipelines; one asked for first here (a mirror's) is started now and the
|
|
15229
|
+
// draw keeps its double-sided pipeline until it lands.
|
|
15230
|
+
void this.buildSidedTwin(pipeline, mirrored);
|
|
15231
|
+
return pipeline;
|
|
15109
15232
|
}
|
|
15110
15233
|
// Pipeline for a material draw call: its group's compiled pipeline when grouped, else
|
|
15111
15234
|
// the neutral base (ungrouped materials render the default graph). Transparent-bucket
|
|
@@ -15177,6 +15300,10 @@ export class Engine {
|
|
|
15177
15300
|
pass.setPipeline(mirrored ? this.outlineMirrorPipeline : this.outlinePipeline);
|
|
15178
15301
|
pass.setBindGroup(0, mirrored ? this.outlineMirrorPerFrameBindGroup : this.outlinePerFrameBindGroup);
|
|
15179
15302
|
pass.setBindGroup(2, draw.outline.bindGroup);
|
|
15303
|
+
// Slot 3: the hull's smoothed normals + edge scale. Only the outline
|
|
15304
|
+
// pipeline reads it; the others leave the slot alone.
|
|
15305
|
+
if (inst.outlineVertexBuffer)
|
|
15306
|
+
pass.setVertexBuffer(3, inst.outlineVertexBuffer);
|
|
15180
15307
|
this.issueDraw(pass, draw, view.args);
|
|
15181
15308
|
pass.setBindGroup(0, view.perFrame);
|
|
15182
15309
|
currentPipeline = null;
|
|
@@ -15942,6 +16069,12 @@ export class Engine {
|
|
|
15942
16069
|
this.writeCullHidden(true);
|
|
15943
16070
|
return true;
|
|
15944
16071
|
}
|
|
16072
|
+
/** Resolves once a model's native look can draw: every pass's pipeline is
|
|
16073
|
+
* compiled (async, at setModelNativeLook). Until then its dressed materials
|
|
16074
|
+
* sit out — a host reveals the model after this to avoid that gap. */
|
|
16075
|
+
nativeLookReady(name) {
|
|
16076
|
+
return this.nativeLooks?.ready(name) ?? Promise.resolve();
|
|
16077
|
+
}
|
|
15945
16078
|
/** The hosts for the game's shaders: the scene pass's and the shadow atlas's,
|
|
15946
16079
|
* with the stand-ins an empty slot reads. Built on first use. */
|
|
15947
16080
|
ensureNativeHosts() {
|
|
@@ -16376,13 +16509,21 @@ Engine.TIMED_PASSES = [
|
|
|
16376
16509
|
// because two planes would need two of every attachment below. Which one is
|
|
16377
16510
|
// in force is mirrorPlane, refreshed by updateMirrorCamera.
|
|
16378
16511
|
Engine.GROUND_PLANE = [0, 1, 0, 0];
|
|
16379
|
-
|
|
16380
|
-
|
|
16381
|
-
Engine.
|
|
16512
|
+
/** The game's prefilter clamps at 6550.4 — a tenth of the half-float ceiling
|
|
16513
|
+
* (its BloomPass _Params.y of 100 never reaches the shader). */
|
|
16514
|
+
Engine.BLOOM_CLAMP = 6550.4;
|
|
16515
|
+
Engine.BLOOM_MAX_LEVELS = 16;
|
|
16382
16516
|
// Smoothness metrics are computed over a ring buffer of true frame intervals
|
|
16383
16517
|
// (vsync-to-vsync), recomputed at STATS_REFRESH_MS so the readout doesn't flicker.
|
|
16384
16518
|
Engine.STATS_WINDOW = 120;
|
|
16385
16519
|
Engine.STATS_REFRESH_MS = 500;
|
|
16520
|
+
/** viewU[6].y per transform — see viewTransform in composite.ts. */
|
|
16521
|
+
Engine.VIEW_TRANSFORM_ID = {
|
|
16522
|
+
soft: 0,
|
|
16523
|
+
none: 1,
|
|
16524
|
+
neutral: 2,
|
|
16525
|
+
aces: 3,
|
|
16526
|
+
};
|
|
16386
16527
|
/**
|
|
16387
16528
|
* The dials a `#mirror` effect is READ BY — the contract between an effect
|
|
16388
16529
|
* file and the reflection.
|
|
@@ -16446,6 +16587,17 @@ Engine.CULL_MODEL_RIGID = 2;
|
|
|
16446
16587
|
/** A cache key no material can collide with — a PMX name is never empty and
|
|
16447
16588
|
* never contains a NUL. */
|
|
16448
16589
|
Engine.SEAM_KEY = "\u0000seams";
|
|
16590
|
+
// ─── The shader cache ─────────────────────────────────────────────
|
|
16591
|
+
//
|
|
16592
|
+
// A compile is most of a second per distinct fragment shader on a cold
|
|
16593
|
+
// browser cache, and the same source used to be compiled once per CALLER: a
|
|
16594
|
+
// graph per model that carried it, the composite per effect install. These
|
|
16595
|
+
// key a module by its WGSL and a pipeline by its module and every descriptor
|
|
16596
|
+
// field but the label, so a second request for the same thing — in flight or
|
|
16597
|
+
// done — is the first one's promise. Bounded, oldest first, because an editor
|
|
16598
|
+
// session compiles a new source per keystroke.
|
|
16599
|
+
Engine.SHADER_CACHE_MAX = 256;
|
|
16600
|
+
Engine.PIPELINE_CACHE_MAX = 512;
|
|
16449
16601
|
/**
|
|
16450
16602
|
* This frame's Unity globals, keeping last frame's object for every value that
|
|
16451
16603
|
* did not change: the native host refills a uniform member only when its value
|