reze-engine 0.63.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 +226 -96
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1187 -732
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/compile.js +4 -1
- package/dist/graph/registry.d.ts +3 -0
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +133 -0
- package/dist/graph/schema.d.ts +12 -0
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +15 -18
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +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/lights.d.ts +3 -2
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +16 -3
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +65 -56
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +210 -0
- 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 +122 -64
- package/dist/shaders/passes/cull.d.ts +1 -1
- package/dist/shaders/passes/cull.d.ts.map +1 -1
- package/dist/shaders/passes/cull.js +7 -3
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +32 -55
- 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/shaders/passes/particles.d.ts +1 -1
- package/dist/shaders/passes/particles.js +1 -1
- package/dist/shaders/scene-light-api.d.ts +18 -10
- package/dist/shaders/scene-light-api.d.ts.map +1 -1
- package/dist/shaders/scene-light-api.js +58 -46
- package/dist/shadow-cascades.d.ts +47 -20
- package/dist/shadow-cascades.d.ts.map +1 -1
- package/dist/shadow-cascades.js +36 -13
- package/dist/unity/character.d.ts +59 -0
- package/dist/unity/character.d.ts.map +1 -0
- package/dist/unity/character.js +120 -0
- package/dist/unity/globals.d.ts +93 -0
- package/dist/unity/globals.d.ts.map +1 -0
- package/dist/unity/globals.js +340 -0
- package/dist/unity/host.d.ts +152 -0
- package/dist/unity/host.d.ts.map +1 -0
- package/dist/unity/host.js +529 -0
- package/dist/unity/looks.d.ts +93 -0
- package/dist/unity/looks.d.ts.map +1 -0
- package/dist/unity/looks.js +312 -0
- package/dist/unity/skin.d.ts +9 -0
- package/dist/unity/skin.d.ts.map +1 -0
- package/dist/unity/skin.js +120 -0
- package/dist/unity/stage.d.ts +155 -0
- package/dist/unity/stage.d.ts.map +1 -0
- package/dist/unity/stage.js +410 -0
- package/dist/unity/state.d.ts +22 -0
- package/dist/unity/state.d.ts.map +1 -0
- package/dist/unity/state.js +170 -0
- package/package.json +1 -1
- package/src/engine.ts +1535 -964
- package/src/graph/compile.ts +3 -1
- package/src/graph/registry.ts +142 -0
- package/src/graph/schema.ts +9 -0
- package/src/graph/slots.ts +15 -18
- package/src/index.ts +12 -0
- package/src/model.ts +18 -1
- package/src/outline-normals.ts +110 -0
- package/src/pmx-loader.ts +8 -2
- package/src/shaders/lights.ts +16 -3
- package/src/shaders/materials/common.ts +65 -57
- package/src/shaders/materials/nodes.ts +210 -0
- package/src/shaders/passes/bloom.ts +59 -87
- package/src/shaders/passes/composite.ts +122 -64
- package/src/shaders/passes/cull.ts +7 -3
- package/src/shaders/passes/ground.ts +32 -55
- package/src/shaders/passes/mirror.ts +60 -13
- package/src/shaders/passes/outline.ts +71 -14
- package/src/shaders/passes/particles.ts +1 -1
- package/src/shaders/scene-light-api.ts +59 -46
- package/src/shadow-cascades.ts +61 -32
- package/src/unity/character.ts +169 -0
- package/src/unity/globals.ts +443 -0
- package/src/unity/host.ts +630 -0
- package/src/unity/looks.ts +391 -0
- package/src/unity/skin.ts +131 -0
- package/src/unity/stage.ts +604 -0
- package/src/unity/state.ts +172 -0
- package/src/shaders/agx-lut.ts +0 -30
package/dist/engine.js
CHANGED
|
@@ -22,9 +22,14 @@ import { ID_DEBUG_SHADER_WGSL } from "./shaders/passes/id-debug";
|
|
|
22
22
|
import { paramChanged, sampleParamTrack } from "./param-track";
|
|
23
23
|
import { advanceSim, DISSOLVE_PARAMS, dissolveConstants, dissolveCycleOf, effectState, sampleDissolveCycle, scheduledDissolve, } from "./effect-schedule";
|
|
24
24
|
import { parentKeySpan } from "./parent-keys";
|
|
25
|
-
import {
|
|
25
|
+
import { NativeHost } from "./unity/host";
|
|
26
|
+
import { NativeLooks } from "./unity/looks";
|
|
27
|
+
import { NativeStage } from "./unity/stage";
|
|
28
|
+
import { gameDir, unityFrameGlobals } from "./unity/globals";
|
|
29
|
+
import { outlineMaxOffsetMultiplier, towardMatrix } from "./unity/character";
|
|
30
|
+
import { SHADOW_ATLAS_SIZE, SHADOW_CASCADES, buildShadowCascades, cascadeSpheres } from "./shadow-cascades";
|
|
26
31
|
import { REFLECTION_DEBUG_WGSL, buildMirrorCamera, planeFromPointNormal } from "./reflection";
|
|
27
|
-
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";
|
|
28
33
|
import { packHalf } from "./hdr";
|
|
29
34
|
import { evalIrradianceSH, projectIrradianceSH, gradientIrradianceSH } from "./ibl";
|
|
30
35
|
import { LYRIC_ATLAS_MAX_H, LYRIC_ATLAS_MAX_W, LYRICS_FLOATS, packLyrics } from "./shaders/lyrics-api";
|
|
@@ -39,8 +44,7 @@ import { GIZMO_SHADER_WGSL } from "./shaders/passes/gizmo";
|
|
|
39
44
|
import { OVERLAY_SHADER_WGSL, OVERLAY_COMPOSITE_SHADER_WGSL } from "./shaders/passes/overlay";
|
|
40
45
|
import { WIREFRAME_SHADER_WGSL } from "./shaders/passes/wireframe";
|
|
41
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";
|
|
42
|
-
import {
|
|
43
|
-
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";
|
|
44
48
|
import { buildCompositeShader, EFFECT_SCENE_API, buildFieldShader, EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES, } from "./shaders/passes/composite";
|
|
45
49
|
import { SUBSURFACE_WGSL, SSS_SPIKE } from "./shaders/passes/subsurface";
|
|
46
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";
|
|
@@ -223,12 +227,10 @@ const CAST_TRAIL_BASE = EFFECT_TRAIL_BASE;
|
|
|
223
227
|
const CAST_VEC4S = CAST_TRAIL_BASE + MAX_EFFECT_ANCHORS * MAX_EFFECT_SUBJECTS * TRAIL_SAMPLES;
|
|
224
228
|
export const DEFAULT_BLOOM_OPTIONS = {
|
|
225
229
|
enabled: true,
|
|
226
|
-
threshold: 0.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
intensity: 0.05,
|
|
231
|
-
clamp: 0.0,
|
|
230
|
+
threshold: 0.7,
|
|
231
|
+
scatter: 0.77,
|
|
232
|
+
color: new Vec3(1, 1, 1),
|
|
233
|
+
intensity: 1,
|
|
232
234
|
};
|
|
233
235
|
export const DEFAULT_DEPTH_OF_FIELD_OPTIONS = {
|
|
234
236
|
enabled: false,
|
|
@@ -240,12 +242,10 @@ export const DEFAULT_DEPTH_OF_FIELD_OPTIONS = {
|
|
|
240
242
|
bladeCount: 6,
|
|
241
243
|
quality: "balanced",
|
|
242
244
|
};
|
|
243
|
-
// Matches the reference Blender project: Filmic view, Medium High Contrast look,
|
|
244
|
-
// exposure 0.3, gamma 1.0, sRGB display, no curves.
|
|
245
245
|
export const DEFAULT_VIEW_TRANSFORM = {
|
|
246
|
-
exposure: 0
|
|
246
|
+
exposure: 0,
|
|
247
247
|
gamma: 1.0,
|
|
248
|
-
transform: "
|
|
248
|
+
transform: "soft",
|
|
249
249
|
};
|
|
250
250
|
const NEUTRAL_GRADE_CHANNEL = 0.5;
|
|
251
251
|
export const DEFAULT_COLOR_GRADING = {
|
|
@@ -706,6 +706,18 @@ const FIELD_LAYER_BLEND_ADDITIVE = {
|
|
|
706
706
|
// construction rather than by hoping an author stayed inside it.
|
|
707
707
|
alpha: { srcFactor: "one", dstFactor: "one", operation: "add" },
|
|
708
708
|
};
|
|
709
|
+
/**
|
|
710
|
+
* Rendering layers, as the game's pipeline (URP's rendering layers) uses them:
|
|
711
|
+
* every drawing carries layer bits and a light reaches it only where its mask
|
|
712
|
+
* shares one. A stage, prop or plane draws on DEFAULT; the cast on DEFAULT and
|
|
713
|
+
* CHARACTER, the bits Aether Gazer's characters carry (0x40000001).
|
|
714
|
+
*/
|
|
715
|
+
export const RENDERING_LAYER_DEFAULT = 1;
|
|
716
|
+
export const RENDERING_LAYER_CHARACTER = 1 << 30;
|
|
717
|
+
/** Every layer — what a light without a mask reaches. */
|
|
718
|
+
const ALL_LAYERS = 0xffffffff;
|
|
719
|
+
/** Where the directional slots' layer bits sit in the light uniform, in words. */
|
|
720
|
+
const DIR_LAYERS_AT = 108;
|
|
709
721
|
export class Engine {
|
|
710
722
|
static getInstance() {
|
|
711
723
|
if (!Engine.instance) {
|
|
@@ -741,7 +753,11 @@ export class Engine {
|
|
|
741
753
|
// padded to 80. sh[0].w is the IBL flag: 0 = flat world colour, 1 = the sky.
|
|
742
754
|
// …then the scene fog at [72..87] — see setSceneFog — and the cast's
|
|
743
755
|
// shadow on a stage at [88..107] (colour, amount; its view-projection).
|
|
756
|
+
// …and the directional slots' rendering layers at [108..111], as u32 bits
|
|
757
|
+
// through lightDataWords — never through a float.
|
|
744
758
|
this.lightData = new Float32Array(112);
|
|
759
|
+
this.lightDataWords = new Uint32Array(this.lightData.buffer);
|
|
760
|
+
this.sunLayers = ALL_LAYERS;
|
|
745
761
|
this.castShadow = null;
|
|
746
762
|
this.castShadowBundle = null;
|
|
747
763
|
this.castShadowCleared = false;
|
|
@@ -988,6 +1004,7 @@ export class Engine {
|
|
|
988
1004
|
* premultiplied quantity and a blur has to move them together. */
|
|
989
1005
|
this.mirrorMaskMipViews = [];
|
|
990
1006
|
this.mirrorMaskBlurBindGroups = null;
|
|
1007
|
+
this.mirrorDownsamplePipeline = null;
|
|
991
1008
|
this.mirrorMaskDownsamplePipeline = null;
|
|
992
1009
|
this.groundClipData = new Float32Array(8);
|
|
993
1010
|
this.mirrorIdMsTexture = null;
|
|
@@ -1134,11 +1151,24 @@ export class Engine {
|
|
|
1134
1151
|
/** Scene seconds, advanced by the frame delta — NOT wall time, so an offline
|
|
1135
1152
|
* export samples the same path the editor showed. */
|
|
1136
1153
|
this.sceneClock = 0;
|
|
1154
|
+
/** Models dressed in the game's own materials (unity/looks.ts); built on the
|
|
1155
|
+
* first look installed. */
|
|
1156
|
+
this.nativeLooks = null;
|
|
1157
|
+
/** The scene pass's host for the game's shaders, and the shadow atlas's. */
|
|
1158
|
+
this.nativeHost = null;
|
|
1159
|
+
this.nativeShadowHost = null;
|
|
1160
|
+
/** A game stage drawn by its own shaders (unity/stage.ts). */
|
|
1161
|
+
this.nativeStage = null;
|
|
1162
|
+
/** The frame's Unity globals, computed once for every native draw. */
|
|
1163
|
+
this.nativeGlobals = {};
|
|
1164
|
+
this.nativeFallback = {};
|
|
1165
|
+
/** A 1x1 depth map at the far plane: a game shader's shadow lookup into a
|
|
1166
|
+
* map the scene does not have reads lit. */
|
|
1167
|
+
this.nativeNoShadowView = null;
|
|
1137
1168
|
this.trailAccum = 0;
|
|
1138
1169
|
/** Trail samples owed this frame, computed once so every trail on every
|
|
1139
1170
|
* character samples in lockstep and their paths stay comparable. */
|
|
1140
1171
|
this.trailDue = 0;
|
|
1141
|
-
this.agxLutTexture = null;
|
|
1142
1172
|
/** The scene's own grade (setStageGrade): the cube as handed over, kept so a
|
|
1143
1173
|
* call before init() still lands, and its texture once uploaded. */
|
|
1144
1174
|
this.stageGradeCube = null;
|
|
@@ -1163,25 +1193,47 @@ export class Engine {
|
|
|
1163
1193
|
this.pipelineDescs = new WeakMap();
|
|
1164
1194
|
this.singleSided = new WeakMap();
|
|
1165
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;
|
|
1166
1211
|
// Stepped motion (#stepped). The models whose pose is being HELD this frame —
|
|
1167
1212
|
// their skinning and morph uploads wait for the stepped clock's next tick —
|
|
1168
1213
|
// and the tick each was last shown at. Empty while no effect declares it.
|
|
1169
1214
|
this.steppedHeld = new Set();
|
|
1170
1215
|
this.steppedTick = new Map();
|
|
1171
1216
|
this.steppedLastClock = -1;
|
|
1172
|
-
this.
|
|
1173
|
-
this.
|
|
1174
|
-
this.
|
|
1175
|
-
this.
|
|
1176
|
-
this.
|
|
1177
|
-
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 = [];
|
|
1178
1232
|
this.bloomUpsampleBindGroups = [];
|
|
1179
1233
|
this.hasGround = false;
|
|
1180
1234
|
/** The user's own "no ground" switch — see setGroundVisible. Distinct from
|
|
1181
1235
|
* hasGround, which records whether a ground was ever built. */
|
|
1182
1236
|
this.groundHidden = false;
|
|
1183
|
-
this.shadowMapTextures = [];
|
|
1184
|
-
this.shadowMapDepthViews = [];
|
|
1185
1237
|
// The shadow PASS reads one cascade's matrix per pass, and a uniform binding
|
|
1186
1238
|
// into the aggregate would need 256-byte alignment padding — two tiny buffers
|
|
1187
1239
|
// are simpler than teaching every reader about a stride.
|
|
@@ -1312,10 +1364,6 @@ export class Engine {
|
|
|
1312
1364
|
* resources are half-built, which is exactly the moment a host swaps a sky.
|
|
1313
1365
|
* Retried at the top of a frame until one of them takes. */
|
|
1314
1366
|
this.worldBindingsDirty = false;
|
|
1315
|
-
/** Per cascade: does its map currently hold nothing but the cleared far plane?
|
|
1316
|
-
* Set by the cascade loop, which skips a cascade that is unwanted and already
|
|
1317
|
-
* cleared rather than re-clearing it every frame. */
|
|
1318
|
-
this.shadowCascadeCleared = [];
|
|
1319
1367
|
this.tweenFrom = { position: new Vec3(0, 0, 0), rotation: Quat.identity() };
|
|
1320
1368
|
this.tweenTo = { position: new Vec3(0, 0, 0), rotation: Quat.identity() };
|
|
1321
1369
|
this.tweenPosition = new Vec3(0, 0, 0);
|
|
@@ -1604,6 +1652,10 @@ export class Engine {
|
|
|
1604
1652
|
/** Models this wrote a dissolve onto last frame, so one nothing asks for any
|
|
1605
1653
|
* more comes back whole instead of holding wherever it was left. */
|
|
1606
1654
|
this.dissolveTouched = new Set();
|
|
1655
|
+
// Compile a group's graph → WGSL → pipeline(s), install keyed by group id. Reuses the
|
|
1656
|
+
// install (pipeline + uniform buffer) when the graph/integration is byte-unchanged.
|
|
1657
|
+
/** A graph's own images (ShaderGraph.images), fetched and decoded once per URL. */
|
|
1658
|
+
this.graphImageCache = new Map();
|
|
1607
1659
|
this.canvas = canvas;
|
|
1608
1660
|
const d = DEFAULT_ENGINE_OPTIONS;
|
|
1609
1661
|
this.world = {
|
|
@@ -1635,18 +1687,16 @@ export class Engine {
|
|
|
1635
1687
|
const bg = options?.background;
|
|
1636
1688
|
this.backgroundColor = bg ? new Vec3(bg.x, bg.y, bg.z) : null;
|
|
1637
1689
|
}
|
|
1638
|
-
/** Merge partial bloom with
|
|
1690
|
+
/** Merge partial bloom with the defaults (same as constructor). */
|
|
1639
1691
|
static mergeBloomDefaults(partial) {
|
|
1640
1692
|
const d = DEFAULT_BLOOM_OPTIONS;
|
|
1641
1693
|
const c = partial?.color;
|
|
1642
1694
|
return {
|
|
1643
1695
|
enabled: partial?.enabled ?? d.enabled,
|
|
1644
1696
|
threshold: partial?.threshold ?? d.threshold,
|
|
1645
|
-
|
|
1646
|
-
radius: partial?.radius ?? d.radius,
|
|
1697
|
+
scatter: partial?.scatter ?? d.scatter,
|
|
1647
1698
|
color: c ? new Vec3(c.x, c.y, c.z) : new Vec3(d.color.x, d.color.y, d.color.z),
|
|
1648
1699
|
intensity: partial?.intensity ?? d.intensity,
|
|
1649
|
-
clamp: partial?.clamp ?? d.clamp,
|
|
1650
1700
|
};
|
|
1651
1701
|
}
|
|
1652
1702
|
static mergeViewTransformDefaults(partial) {
|
|
@@ -1657,17 +1707,15 @@ export class Engine {
|
|
|
1657
1707
|
transform: partial?.transform ?? d.transform,
|
|
1658
1708
|
};
|
|
1659
1709
|
}
|
|
1660
|
-
/** Current bloom settings (
|
|
1710
|
+
/** Current bloom settings (tint is a copied `Vec3`). */
|
|
1661
1711
|
getBloomOptions() {
|
|
1662
1712
|
const b = this.bloomSettings;
|
|
1663
1713
|
return {
|
|
1664
1714
|
enabled: b.enabled,
|
|
1665
1715
|
threshold: b.threshold,
|
|
1666
|
-
|
|
1667
|
-
radius: b.radius,
|
|
1716
|
+
scatter: b.scatter,
|
|
1668
1717
|
color: new Vec3(b.color.x, b.color.y, b.color.z),
|
|
1669
1718
|
intensity: b.intensity,
|
|
1670
|
-
clamp: b.clamp,
|
|
1671
1719
|
};
|
|
1672
1720
|
}
|
|
1673
1721
|
getViewTransformOptions() {
|
|
@@ -1855,7 +1903,7 @@ export class Engine {
|
|
|
1855
1903
|
// rebuilt per effect, so the compiled variant IS the flag.
|
|
1856
1904
|
u[11] = this.backdropEquirectView ? 2 : showingWorld ? 3 : bg ? 1 : 0;
|
|
1857
1905
|
// Which display transform forms the frame (see viewTransform in composite.ts).
|
|
1858
|
-
u[25] =
|
|
1906
|
+
u[25] = Engine.VIEW_TRANSFORM_ID[v.transform] ?? 0;
|
|
1859
1907
|
u[26] = this.canvas.width;
|
|
1860
1908
|
u[27] = this.canvas.height;
|
|
1861
1909
|
// ── Grade (viewU[7..9]) ── The UI's three tonal COLORS map to ASC CDL here,
|
|
@@ -2500,7 +2548,7 @@ export class Engine {
|
|
|
2500
2548
|
this.renderMirrorBlurChain(encoder);
|
|
2501
2549
|
}
|
|
2502
2550
|
/**
|
|
2503
|
-
* Fill the mirror's mip levels —
|
|
2551
|
+
* Fill the mirror's mip levels — a 13-tap downsample (MIRROR_DOWNSAMPLE_WGSL),
|
|
2504
2552
|
* one pass per level. Only when the blur dial is up: at zero the ground
|
|
2505
2553
|
* samples level 0 exactly and the chain would be work nobody reads.
|
|
2506
2554
|
*/
|
|
@@ -2511,8 +2559,19 @@ export class Engine {
|
|
|
2511
2559
|
const blur = Math.max(this.groundMirrorBlur, this.mirrorSurface?.blur ?? 0);
|
|
2512
2560
|
if (blur <= 0 || this.mirrorMipCount < 2)
|
|
2513
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;
|
|
2514
2573
|
if (!this.mirrorBlurBindGroups) {
|
|
2515
|
-
const layout =
|
|
2574
|
+
const layout = downsample.getBindGroupLayout(0);
|
|
2516
2575
|
this.mirrorBlurBindGroups = [];
|
|
2517
2576
|
for (let i = 1; i < this.mirrorMipCount; i++) {
|
|
2518
2577
|
this.mirrorBlurBindGroups.push(this.device.createBindGroup({
|
|
@@ -2530,7 +2589,7 @@ export class Engine {
|
|
|
2530
2589
|
label: `mirror blur ${i}`,
|
|
2531
2590
|
colorAttachments: [{ view: this.mirrorMipViews[i], loadOp: "clear", storeOp: "store" }],
|
|
2532
2591
|
});
|
|
2533
|
-
p.setPipeline(
|
|
2592
|
+
p.setPipeline(downsample);
|
|
2534
2593
|
p.setBindGroup(0, this.mirrorBlurBindGroups[i - 1]);
|
|
2535
2594
|
p.draw(3);
|
|
2536
2595
|
p.end();
|
|
@@ -2683,7 +2742,6 @@ export class Engine {
|
|
|
2683
2742
|
{ binding: 2, resource: this.bloomSampler },
|
|
2684
2743
|
{ binding: 3, resource: { buffer: this.compositeUniformBuffer } },
|
|
2685
2744
|
{ binding: 4, resource: this.maskResolveView },
|
|
2686
|
-
{ binding: 5, resource: this.filmicLutView },
|
|
2687
2745
|
// Whichever equirect is SHOWING — the backdrop if there is one, the
|
|
2688
2746
|
// world otherwise. The world's light does not come through here; it
|
|
2689
2747
|
// rides worldSH into the material shells.
|
|
@@ -2691,7 +2749,6 @@ export class Engine {
|
|
|
2691
2749
|
{ binding: 7, resource: { buffer: this.effect?.paramsBuffer ?? this.bgParamsDummyBuffer } },
|
|
2692
2750
|
{ binding: 8, resource: this.depthReadView },
|
|
2693
2751
|
{ binding: 9, resource: { buffer: this.dofUniformBuffer } },
|
|
2694
|
-
{ binding: 10, resource: (this.agxLutTexture ?? this.agxFallbackTexture).createView({ dimension: "3d" }) },
|
|
2695
2752
|
{ binding: 12, resource: (this.stageGradeTexture ?? this.stageGradeFallback).createView({ dimension: "3d" }) },
|
|
2696
2753
|
{ binding: 11, resource: { buffer: this.castBuffer } },
|
|
2697
2754
|
{ binding: 13, resource: { buffer: this.audioBuffer } },
|
|
@@ -3115,8 +3172,6 @@ export class Engine {
|
|
|
3115
3172
|
{ binding: 31, resource: layers[2] },
|
|
3116
3173
|
{ binding: 32, resource: layers[3] },
|
|
3117
3174
|
{ binding: 2, resource: this.bloomSampler },
|
|
3118
|
-
{ binding: 5, resource: this.filmicLutView },
|
|
3119
|
-
{ binding: 10, resource: (this.agxLutTexture ?? this.agxFallbackTexture).createView({ dimension: "3d" }) },
|
|
3120
3175
|
{ binding: 12, resource: (this.stageGradeTexture ?? this.stageGradeFallback).createView({ dimension: "3d" }) },
|
|
3121
3176
|
{ binding: 1, resource: bloom },
|
|
3122
3177
|
{ binding: 4, resource: this.maskResolveView },
|
|
@@ -3367,8 +3422,20 @@ export class Engine {
|
|
|
3367
3422
|
if (this.device && this.compositeUniformBuffer)
|
|
3368
3423
|
this.writeCompositeViewUniforms();
|
|
3369
3424
|
}
|
|
3370
|
-
|
|
3371
|
-
|
|
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({
|
|
3372
3439
|
label,
|
|
3373
3440
|
layout: this.compositePipelineLayout,
|
|
3374
3441
|
vertex: { module, entryPoint: "vs" },
|
|
@@ -3380,6 +3447,7 @@ export class Engine {
|
|
|
3380
3447
|
},
|
|
3381
3448
|
primitive: { topology: "triangle-list" },
|
|
3382
3449
|
});
|
|
3450
|
+
return Promise.all([make(false, "composite pipeline (gamma=1)"), make(true, "composite pipeline (gamma!=1)")]);
|
|
3383
3451
|
}
|
|
3384
3452
|
/**
|
|
3385
3453
|
* Install the scene's WGSL effect (shadertoy-style), rendered per-pixel in the
|
|
@@ -3608,12 +3676,11 @@ export class Engine {
|
|
|
3608
3676
|
const fieldEffect = hasBackground || hasForeground
|
|
3609
3677
|
? { wgsl, paramsDecl, hasBackground, hasForeground, gridSize, alias, trailCount: anchors.filter((a) => a.trail).length, filter, additiveLayer: d.additiveLayer }
|
|
3610
3678
|
: null;
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
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.
|
|
3617
3684
|
// Declared like every other mount property: by what the source says, not by
|
|
3618
3685
|
// a setting somewhere else that an author cannot see from the file.
|
|
3619
3686
|
const layerBlend = d.additiveLayer
|
|
@@ -3624,73 +3691,56 @@ export class Engine {
|
|
|
3624
3691
|
const fieldSource = buildFieldShader({ ...fieldEffect, ids: mrtIdsEnabled() });
|
|
3625
3692
|
const userLineOffset = fieldSource.slice(0, fieldSource.indexOf(wgsl)).split("\n").length - 1;
|
|
3626
3693
|
this.device.pushErrorScope("validation");
|
|
3627
|
-
const fieldModule = this.
|
|
3628
|
-
const
|
|
3629
|
-
|
|
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]);
|
|
3630
3728
|
const diagnostics = info.messages
|
|
3631
3729
|
.filter((m) => m.type === "error")
|
|
3632
3730
|
.map((m) => `${Math.max(0, m.lineNum - userLineOffset)}:${m.linePos} ${m.message}`);
|
|
3633
3731
|
if (diagnostics.length === 0 && fieldScopeErr)
|
|
3634
3732
|
diagnostics.push(fieldScopeErr.message);
|
|
3635
|
-
if (diagnostics.length > 0)
|
|
3733
|
+
if (diagnostics.length > 0) {
|
|
3734
|
+
this.forgetShaderModule(fieldSource);
|
|
3636
3735
|
return { ok: false, diagnostics, mounts, params: d.params, duration: d.duration, readsCast: false };
|
|
3736
|
+
}
|
|
3637
3737
|
try {
|
|
3638
|
-
fieldPipeline = await
|
|
3639
|
-
label: "field layer pipeline",
|
|
3640
|
-
layout: this.fieldPipelineLayout,
|
|
3641
|
-
vertex: { module: fieldModule, entryPoint: "fieldVs" },
|
|
3642
|
-
fragment: {
|
|
3643
|
-
module: fieldModule,
|
|
3644
|
-
entryPoint: "fieldFs",
|
|
3645
|
-
// OVER, accumulating PREMULTIPLIED colour: several effects draw into
|
|
3646
|
-
// these two targets in document order, and each must layer onto what
|
|
3647
|
-
// the earlier ones left rather than replace it. src-alpha on colour
|
|
3648
|
-
// premultiplies as it writes; alpha accumulates as one-over. An
|
|
3649
|
-
// author still returns STRAIGHT colour+alpha, exactly as before —
|
|
3650
|
-
// the premultiplication happens here, and the composite reads it
|
|
3651
|
-
// back knowing that. With one effect over a cleared target the
|
|
3652
|
-
// result is identical to the replace it used to do.
|
|
3653
|
-
// A filter composes over the layers in its own shader and writes
|
|
3654
|
-
// the result outright: blending would layer it onto a page that
|
|
3655
|
-
// holds nothing.
|
|
3656
|
-
targets: filter
|
|
3657
|
-
? [{ format: "rgba16float" }, { format: "rgba16float" }]
|
|
3658
|
-
: [
|
|
3659
|
-
{ format: "rgba16float", blend: layerBlend },
|
|
3660
|
-
{ format: "rgba16float", blend: layerBlend },
|
|
3661
|
-
],
|
|
3662
|
-
},
|
|
3663
|
-
primitive: { topology: "triangle-list" },
|
|
3664
|
-
multisample: { count: 1 },
|
|
3665
|
-
});
|
|
3738
|
+
fieldPipeline = await fieldBuild;
|
|
3666
3739
|
}
|
|
3667
3740
|
catch (e) {
|
|
3668
3741
|
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)], mounts, params: d.params, duration: d.duration, readsCast: false };
|
|
3669
3742
|
}
|
|
3670
3743
|
}
|
|
3671
|
-
let identity;
|
|
3672
|
-
let gamma;
|
|
3673
|
-
try {
|
|
3674
|
-
const make = (applyGamma, label) => this.device.createRenderPipelineAsync({
|
|
3675
|
-
label,
|
|
3676
|
-
layout: this.compositePipelineLayout,
|
|
3677
|
-
vertex: { module, entryPoint: "vs" },
|
|
3678
|
-
fragment: {
|
|
3679
|
-
module,
|
|
3680
|
-
entryPoint: "fs",
|
|
3681
|
-
constants: { APPLY_GAMMA: applyGamma ? 1 : 0 },
|
|
3682
|
-
targets: [{ format: this.presentationFormat }],
|
|
3683
|
-
},
|
|
3684
|
-
primitive: { topology: "triangle-list" },
|
|
3685
|
-
});
|
|
3686
|
-
[identity, gamma] = await Promise.all([
|
|
3687
|
-
make(false, "composite pipeline (effect, gamma=1)"),
|
|
3688
|
-
make(true, "composite pipeline (effect, gamma!=1)"),
|
|
3689
|
-
]);
|
|
3690
|
-
}
|
|
3691
|
-
catch (e) {
|
|
3692
|
-
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)], mounts, params: d.params, duration: d.duration, readsCast: false };
|
|
3693
|
-
}
|
|
3694
3744
|
// Built BEFORE the swap: a particle stage that fails to compile has to leave
|
|
3695
3745
|
// the previously installed effect running, exactly as a bad composite does.
|
|
3696
3746
|
// A stage that fails after an earlier one succeeded would otherwise strand
|
|
@@ -3917,6 +3967,9 @@ export class Engine {
|
|
|
3917
3967
|
}
|
|
3918
3968
|
const requested = list ?? [];
|
|
3919
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);
|
|
3920
3973
|
for (const e of this.effects) {
|
|
3921
3974
|
e.paramsBuffer?.destroy();
|
|
3922
3975
|
e.lights?.uniform.destroy();
|
|
@@ -3929,9 +3982,8 @@ export class Engine {
|
|
|
3929
3982
|
this.allocateLightSlots();
|
|
3930
3983
|
this.anchorTable = EMPTY_ANCHOR_TABLE;
|
|
3931
3984
|
this.clearTrailHistory();
|
|
3932
|
-
|
|
3933
|
-
this.
|
|
3934
|
-
this.compositePipelineGamma = this.makeCompositePipeline(module, true, "composite pipeline (gamma!=1)");
|
|
3985
|
+
this.compositePipelineIdentity = identity;
|
|
3986
|
+
this.compositePipelineGamma = gamma;
|
|
3935
3987
|
this.rebuildCompositeBindGroup();
|
|
3936
3988
|
this.writeCompositeViewUniforms();
|
|
3937
3989
|
return [];
|
|
@@ -3977,6 +4029,10 @@ export class Engine {
|
|
|
3977
4029
|
if (r)
|
|
3978
4030
|
r.diagnostics.push(`anchor "${d.bone}" dropped: the scene is already using all ${MAX_EFFECT_ANCHORS} slots`);
|
|
3979
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));
|
|
3980
4036
|
// ── Swap. Everything above either succeeded or was excluded, so the scene
|
|
3981
4037
|
// that was running is only torn down now.
|
|
3982
4038
|
for (const e of this.effects) {
|
|
@@ -4027,18 +4083,8 @@ export class Engine {
|
|
|
4027
4083
|
// Scene-level, from the union: the composite decides only whether to SAMPLE
|
|
4028
4084
|
// the field layers, so one effect with a background is enough to turn that
|
|
4029
4085
|
// on for the frame.
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
const compositeModule = this.device.createShaderModule({
|
|
4033
|
-
label: "composite shader (effects)",
|
|
4034
|
-
code: buildCompositeShader(hasBackground || hasForeground
|
|
4035
|
-
? // No wgsl and so no trails: the composite hosts no effect source at
|
|
4036
|
-
// all, it only decides whether to sample the layer the field pass drew.
|
|
4037
|
-
{ wgsl: "", paramsDecl: "", hasBackground, hasForeground, gridSize: 0, trailCount: 0 }
|
|
4038
|
-
: null),
|
|
4039
|
-
});
|
|
4040
|
-
this.compositePipelineIdentity = this.makeCompositePipeline(compositeModule, false, "composite pipeline (gamma=1)");
|
|
4041
|
-
this.compositePipelineGamma = this.makeCompositePipeline(compositeModule, true, "composite pipeline (gamma!=1)");
|
|
4086
|
+
this.compositePipelineIdentity = compositeIdentity;
|
|
4087
|
+
this.compositePipelineGamma = compositeGamma;
|
|
4042
4088
|
// Nothing to promote any more: `#fullres` is per effect, read into
|
|
4043
4089
|
// fieldLayer when the instance is built, and both target pairs exist for
|
|
4044
4090
|
// the life of the surface. What used to be a scene-wide decision made here
|
|
@@ -4130,26 +4176,33 @@ export class Engine {
|
|
|
4130
4176
|
trailCount: anchors.filter((x) => x.trail).length,
|
|
4131
4177
|
alias,
|
|
4132
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.
|
|
4133
4182
|
const compile = async (code, label) => {
|
|
4134
4183
|
const offset = code.slice(0, code.indexOf(wgsl)).split("\n").length - 1;
|
|
4135
4184
|
this.device.pushErrorScope("validation");
|
|
4136
|
-
const module = this.
|
|
4137
|
-
const
|
|
4138
|
-
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]);
|
|
4139
4188
|
const diagnostics = info.messages
|
|
4140
4189
|
.filter((m) => m.type === "error")
|
|
4141
4190
|
.map((m) => `${Math.max(0, m.lineNum - offset)}:${m.linePos} ${m.message}`);
|
|
4142
4191
|
if (diagnostics.length === 0 && scopeErr)
|
|
4143
4192
|
diagnostics.push(scopeErr.message);
|
|
4193
|
+
if (diagnostics.length)
|
|
4194
|
+
this.forgetShaderModule(code);
|
|
4144
4195
|
return diagnostics.length ? diagnostics : module;
|
|
4145
4196
|
};
|
|
4146
|
-
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
|
+
]);
|
|
4147
4201
|
if (Array.isArray(computeModule)) {
|
|
4148
4202
|
points?.buffer.destroy();
|
|
4149
4203
|
dropTextures();
|
|
4150
4204
|
return { ok: false, diagnostics: computeModule };
|
|
4151
4205
|
}
|
|
4152
|
-
const renderModule = await compile(buildParticleRenderShader(src, cast), "particle render");
|
|
4153
4206
|
if (Array.isArray(renderModule)) {
|
|
4154
4207
|
points?.buffer.destroy();
|
|
4155
4208
|
dropTextures();
|
|
@@ -4187,7 +4240,7 @@ export class Engine {
|
|
|
4187
4240
|
// Declaring one set of flags for both layouts is what made the pipeline
|
|
4188
4241
|
// layout invalid, and the error surfaces later and unhelpfully as "invalid
|
|
4189
4242
|
// due to a previous error".
|
|
4190
|
-
const layoutFor = (storage, visibility, shadow, withIndirect) => this.
|
|
4243
|
+
const layoutFor = (storage, visibility, shadow, withIndirect) => this.cachedBindGroupLayout({
|
|
4191
4244
|
entries: [
|
|
4192
4245
|
{ binding: 0, visibility, buffer: { type: storage } },
|
|
4193
4246
|
{ binding: 1, visibility, buffer: { type: "uniform" } },
|
|
@@ -4217,8 +4270,7 @@ export class Engine {
|
|
|
4217
4270
|
{ binding: PARTICLE_LIGHT_BINDING, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
4218
4271
|
{ binding: PARTICLE_LIGHT_BINDING + 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
4219
4272
|
{ binding: PARTICLE_LIGHT_BINDING + 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
4220
|
-
{ binding: PARTICLE_LIGHT_BINDING + 3, visibility: GPUShaderStage.FRAGMENT,
|
|
4221
|
-
{ binding: PARTICLE_LIGHT_BINDING + 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
4273
|
+
{ binding: PARTICLE_LIGHT_BINDING + 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
4222
4274
|
]
|
|
4223
4275
|
: []),
|
|
4224
4276
|
// The live count's arguments, for the COUNT kernel's layout alone. The
|
|
@@ -4265,9 +4317,8 @@ export class Engine {
|
|
|
4265
4317
|
? [
|
|
4266
4318
|
{ binding: PARTICLE_LIGHT_BINDING, resource: { buffer: this.lightUniformBuffer } },
|
|
4267
4319
|
{ binding: PARTICLE_LIGHT_BINDING + 1, resource: { buffer: this.shadowLightVPBuffer } },
|
|
4268
|
-
{ binding: PARTICLE_LIGHT_BINDING + 2, resource: this.
|
|
4269
|
-
{ binding: PARTICLE_LIGHT_BINDING + 3, resource: this.
|
|
4270
|
-
{ binding: PARTICLE_LIGHT_BINDING + 4, resource: this.shadowComparisonSampler },
|
|
4320
|
+
{ binding: PARTICLE_LIGHT_BINDING + 2, resource: this.shadowAtlasView },
|
|
4321
|
+
{ binding: PARTICLE_LIGHT_BINDING + 3, resource: this.shadowComparisonSampler },
|
|
4271
4322
|
]
|
|
4272
4323
|
: []),
|
|
4273
4324
|
...(withIndirect && indirect ? [{ binding: PARTICLE_INDIRECT_BINDING, resource: { buffer: indirect } }] : []),
|
|
@@ -4295,31 +4346,29 @@ export class Engine {
|
|
|
4295
4346
|
// blends would be a copy.
|
|
4296
4347
|
const targets = sceneTargetsFor(src.blend === "additive" ? "particle-additive" : "particle", this.sceneFormats);
|
|
4297
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.
|
|
4298
4352
|
this.device.pushErrorScope("validation");
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4353
|
+
const renderLayoutObj = this.cachedPipelineLayout([renderLayout]);
|
|
4354
|
+
const builds = Promise.all([
|
|
4355
|
+
this.cachedComputePipeline({
|
|
4302
4356
|
label: "particle compute pipeline",
|
|
4303
|
-
layout:
|
|
4357
|
+
layout: this.cachedPipelineLayout([computeLayout]),
|
|
4304
4358
|
compute: { module: computeModule, entryPoint: "main" },
|
|
4305
|
-
})
|
|
4306
|
-
|
|
4307
|
-
? {
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
indirect,
|
|
4314
|
-
binds: bindFor(countLayout, this.cameraUniformBuffer, false, true),
|
|
4315
|
-
}
|
|
4316
|
-
: null;
|
|
4317
|
-
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,
|
|
4318
4367
|
// The prepass, when the effect gave the cutout a cheap shape: coverage to
|
|
4319
4368
|
// depth, colour targets at writeMask 0 (the depth-prepass class), and
|
|
4320
4369
|
// the shading pass below then tests EQUAL and writes no depth.
|
|
4321
|
-
|
|
4322
|
-
?
|
|
4370
|
+
prepass
|
|
4371
|
+
? this.cachedRenderPipeline({
|
|
4323
4372
|
label: "particle depth prepass pipeline",
|
|
4324
4373
|
layout: renderLayoutObj,
|
|
4325
4374
|
vertex: { module: renderModule, entryPoint: "vs" },
|
|
@@ -4328,8 +4377,8 @@ export class Engine {
|
|
|
4328
4377
|
depthStencil: { format: this.depthFormat, depthWriteEnabled: true, depthCompare: this.depthAhead },
|
|
4329
4378
|
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
4330
4379
|
})
|
|
4331
|
-
: null
|
|
4332
|
-
|
|
4380
|
+
: null,
|
|
4381
|
+
this.cachedRenderPipeline({
|
|
4333
4382
|
label: "particle render pipeline",
|
|
4334
4383
|
layout: renderLayoutObj,
|
|
4335
4384
|
vertex: { module: renderModule, entryPoint: "vs" },
|
|
@@ -4350,8 +4399,19 @@ export class Engine {
|
|
|
4350
4399
|
? { format: this.depthFormat, depthWriteEnabled: false, depthCompare: "equal" }
|
|
4351
4400
|
: { format: this.depthFormat, depthWriteEnabled: cutout, depthCompare: this.depthAhead },
|
|
4352
4401
|
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
4353
|
-
})
|
|
4354
|
-
|
|
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;
|
|
4355
4415
|
if (scoped) {
|
|
4356
4416
|
buffer.destroy();
|
|
4357
4417
|
uniform.destroy();
|
|
@@ -4392,7 +4452,7 @@ export class Engine {
|
|
|
4392
4452
|
};
|
|
4393
4453
|
}
|
|
4394
4454
|
catch (e) {
|
|
4395
|
-
await
|
|
4455
|
+
await scope;
|
|
4396
4456
|
buffer.destroy();
|
|
4397
4457
|
uniform.destroy();
|
|
4398
4458
|
points?.buffer.destroy();
|
|
@@ -5629,10 +5689,8 @@ export class Engine {
|
|
|
5629
5689
|
b.enabled = patch.enabled;
|
|
5630
5690
|
if (patch.threshold !== undefined)
|
|
5631
5691
|
b.threshold = patch.threshold;
|
|
5632
|
-
if (patch.
|
|
5633
|
-
b.
|
|
5634
|
-
if (patch.radius !== undefined)
|
|
5635
|
-
b.radius = patch.radius;
|
|
5692
|
+
if (patch.scatter !== undefined)
|
|
5693
|
+
b.scatter = patch.scatter;
|
|
5636
5694
|
if (patch.color !== undefined) {
|
|
5637
5695
|
b.color.x = patch.color.x;
|
|
5638
5696
|
b.color.y = patch.color.y;
|
|
@@ -5640,33 +5698,154 @@ export class Engine {
|
|
|
5640
5698
|
}
|
|
5641
5699
|
if (patch.intensity !== undefined)
|
|
5642
5700
|
b.intensity = patch.intensity;
|
|
5643
|
-
if (
|
|
5644
|
-
b.clamp = patch.clamp;
|
|
5645
|
-
if (this.device && this.bloomBlitUniformBuffer) {
|
|
5701
|
+
if (this.device && this.compositeUniformBuffer) {
|
|
5646
5702
|
this.writeBloomUniforms();
|
|
5647
5703
|
this.writeCompositeViewUniforms();
|
|
5648
5704
|
}
|
|
5649
5705
|
}
|
|
5650
|
-
|
|
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
|
+
}
|
|
5651
5808
|
writeBloomUniforms() {
|
|
5809
|
+
if (!this.bloomUniformBuffer)
|
|
5810
|
+
return;
|
|
5652
5811
|
const b = this.bloomSettings;
|
|
5653
|
-
const
|
|
5654
|
-
//
|
|
5655
|
-
//
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
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;
|
|
5670
5849
|
}
|
|
5671
5850
|
// Step 1: Get WebGPU device and context
|
|
5672
5851
|
async init() {
|
|
@@ -5791,7 +5970,17 @@ export class Engine {
|
|
|
5791
5970
|
this.setupCamera();
|
|
5792
5971
|
this.setupLighting();
|
|
5793
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 = [];
|
|
5794
5977
|
this.createPipelines();
|
|
5978
|
+
try {
|
|
5979
|
+
await Promise.all(this.initPipelineJobs);
|
|
5980
|
+
}
|
|
5981
|
+
finally {
|
|
5982
|
+
this.initPipelineJobs = null;
|
|
5983
|
+
}
|
|
5795
5984
|
this.setupResize();
|
|
5796
5985
|
const ms = (a, b) => Math.round(b - a);
|
|
5797
5986
|
console.info(`[reze] init ${ms(t0, performance.now())}ms — adapter ${ms(t0, tAdapter)}ms · ` +
|
|
@@ -5898,122 +6087,12 @@ export class Engine {
|
|
|
5898
6087
|
this.device.queue.submit([enc.finish()]);
|
|
5899
6088
|
ltcTemp.destroy();
|
|
5900
6089
|
}
|
|
5901
|
-
// Bake the Blender 3.6 Filmic MHC tone curve into a WIDTH×1 r16float LUT sampled by the
|
|
5902
|
-
// composite pass. The 14 anchors are the same as the old inline array; we fit a monotone
|
|
5903
|
-
// cubic (Fritsch–Carlson) through them so the curve is C1-continuous (no Mach banding in
|
|
5904
|
-
// smooth gradients) while still passing through every anchor (look preserved) and staying
|
|
5905
|
-
// monotone (no tonemap overshoot/ringing). Domain is uniform in log2 space: anchor k sits
|
|
5906
|
-
// at t=k, k=0..13 (t = log2(linear)+10). See composite.ts::filmic for the sampling map.
|
|
5907
|
-
/**
|
|
5908
|
-
* Decompress and upload Blender's AgX cube.
|
|
5909
|
-
*
|
|
5910
|
-
* Deliberately off the critical path: it is 723 KB once inflated, and a frame
|
|
5911
|
-
* rendered before it lands should show the scene under whatever transform is
|
|
5912
|
-
* already there rather than wait. Until then binding 10 holds a 1×1×1 stand-in,
|
|
5913
|
-
* which is only ever sampled if someone selects AgX in that window.
|
|
5914
|
-
*/
|
|
5915
|
-
async loadAgxLut() {
|
|
5916
|
-
try {
|
|
5917
|
-
const packed = Uint8Array.from(atob(AGX_LUT_GZ), (ch) => ch.charCodeAt(0));
|
|
5918
|
-
const stream = new Blob([packed]).stream().pipeThrough(new DecompressionStream("gzip"));
|
|
5919
|
-
const bytes = new Uint8Array(await new Response(stream).arrayBuffer());
|
|
5920
|
-
const n = AGX_LUT_SIZE;
|
|
5921
|
-
if (bytes.byteLength !== n * n * n * 4)
|
|
5922
|
-
throw new Error(`AgX LUT is ${bytes.byteLength} bytes, expected ${n ** 3 * 4}`);
|
|
5923
|
-
const tex = this.device.createTexture({
|
|
5924
|
-
label: "AgX 57³ LUT",
|
|
5925
|
-
size: [n, n, n],
|
|
5926
|
-
dimension: "3d",
|
|
5927
|
-
format: "rgb10a2unorm",
|
|
5928
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
5929
|
-
});
|
|
5930
|
-
// .cube order is red fastest, which is exactly a 3D texture's own layout.
|
|
5931
|
-
this.device.queue.writeTexture({ texture: tex }, bytes, { bytesPerRow: n * 4, rowsPerImage: n }, [n, n, n]);
|
|
5932
|
-
this.agxLutTexture = tex;
|
|
5933
|
-
this.rebuildCompositeBindGroup();
|
|
5934
|
-
}
|
|
5935
|
-
catch {
|
|
5936
|
-
// A missing LUT costs AgX, not the renderer — the other transforms stand.
|
|
5937
|
-
}
|
|
5938
|
-
}
|
|
5939
|
-
bakeFilmicLut() {
|
|
5940
|
-
const anchors = [
|
|
5941
|
-
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,
|
|
5942
|
-
];
|
|
5943
|
-
const n = anchors.length;
|
|
5944
|
-
// Secant slopes (unit spacing, so d_k = y_{k+1} - y_k).
|
|
5945
|
-
const d = new Array(n - 1);
|
|
5946
|
-
for (let k = 0; k < n - 1; k++)
|
|
5947
|
-
d[k] = anchors[k + 1] - anchors[k];
|
|
5948
|
-
// Endpoint + interior tangents.
|
|
5949
|
-
const m = new Array(n);
|
|
5950
|
-
m[0] = d[0];
|
|
5951
|
-
m[n - 1] = d[n - 2];
|
|
5952
|
-
for (let k = 1; k < n - 1; k++)
|
|
5953
|
-
m[k] = (d[k - 1] + d[k]) * 0.5;
|
|
5954
|
-
// Fritsch–Carlson monotonicity clamp.
|
|
5955
|
-
for (let k = 0; k < n - 1; k++) {
|
|
5956
|
-
if (d[k] === 0) {
|
|
5957
|
-
m[k] = 0;
|
|
5958
|
-
m[k + 1] = 0;
|
|
5959
|
-
continue;
|
|
5960
|
-
}
|
|
5961
|
-
const a = m[k] / d[k];
|
|
5962
|
-
const b = m[k + 1] / d[k];
|
|
5963
|
-
const s = a * a + b * b;
|
|
5964
|
-
if (s > 9) {
|
|
5965
|
-
const tau = 3 / Math.sqrt(s);
|
|
5966
|
-
m[k] = tau * a * d[k];
|
|
5967
|
-
m[k + 1] = tau * b * d[k];
|
|
5968
|
-
}
|
|
5969
|
-
}
|
|
5970
|
-
const W = Engine.FILMIC_LUT_WIDTH;
|
|
5971
|
-
const values = new Float32Array(W);
|
|
5972
|
-
for (let j = 0; j < W; j++) {
|
|
5973
|
-
const t = ((n - 1) * j) / (W - 1); // t ∈ [0, n-1]
|
|
5974
|
-
const k = Math.min(Math.floor(t), n - 2);
|
|
5975
|
-
const s = t - k; // local param in [0,1], unit-spaced segment
|
|
5976
|
-
const s2 = s * s;
|
|
5977
|
-
const s3 = s2 * s;
|
|
5978
|
-
// Hermite basis (h=1).
|
|
5979
|
-
const h00 = 2 * s3 - 3 * s2 + 1;
|
|
5980
|
-
const h10 = s3 - 2 * s2 + s;
|
|
5981
|
-
const h01 = -2 * s3 + 3 * s2;
|
|
5982
|
-
const h11 = s3 - s2;
|
|
5983
|
-
values[j] = h00 * anchors[k] + h10 * m[k] + h01 * anchors[k + 1] + h11 * m[k + 1];
|
|
5984
|
-
}
|
|
5985
|
-
// f32 → f16 bits (same conversion as bakeBrdfLut).
|
|
5986
|
-
const half = new Uint16Array(W);
|
|
5987
|
-
const f32 = new Float32Array(1);
|
|
5988
|
-
const u32 = new Uint32Array(f32.buffer);
|
|
5989
|
-
for (let j = 0; j < W; j++) {
|
|
5990
|
-
f32[0] = values[j];
|
|
5991
|
-
const x = u32[0];
|
|
5992
|
-
const sign = (x >>> 16) & 0x8000;
|
|
5993
|
-
const exp = ((x >>> 23) & 0xff) - 127 + 15;
|
|
5994
|
-
const mant = x & 0x7fffff;
|
|
5995
|
-
if (exp <= 0) {
|
|
5996
|
-
half[j] = sign;
|
|
5997
|
-
}
|
|
5998
|
-
else if (exp >= 31) {
|
|
5999
|
-
half[j] = sign | 0x7c00;
|
|
6000
|
-
}
|
|
6001
|
-
else {
|
|
6002
|
-
half[j] = sign | (exp << 10) | (mant >>> 13);
|
|
6003
|
-
}
|
|
6004
|
-
}
|
|
6005
|
-
this.filmicLutTexture = this.device.createTexture({
|
|
6006
|
-
label: "Filmic tone LUT",
|
|
6007
|
-
size: [W, 1],
|
|
6008
|
-
format: "r16float",
|
|
6009
|
-
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
|
|
6010
|
-
});
|
|
6011
|
-
this.filmicLutView = this.filmicLutTexture.createView();
|
|
6012
|
-
this.device.queue.writeTexture({ texture: this.filmicLutTexture }, half, { bytesPerRow: W * 2, rowsPerImage: 1 }, { width: W, height: 1, depthOrArrayLayers: 1 });
|
|
6013
|
-
}
|
|
6014
6090
|
createRenderPipeline(config) {
|
|
6091
|
+
return this.createModelPipeline(this.renderPipelineDesc(config));
|
|
6092
|
+
}
|
|
6093
|
+
renderPipelineDesc(config) {
|
|
6015
6094
|
const targets = config.fragmentTargets ?? (config.fragmentTarget ? [config.fragmentTarget] : undefined);
|
|
6016
|
-
return
|
|
6095
|
+
return {
|
|
6017
6096
|
label: config.label,
|
|
6018
6097
|
layout: config.layout,
|
|
6019
6098
|
vertex: {
|
|
@@ -6030,7 +6109,7 @@ export class Engine {
|
|
|
6030
6109
|
primitive: { cullMode: config.cullMode ?? "none" },
|
|
6031
6110
|
depthStencil: config.depthStencil,
|
|
6032
6111
|
multisample: config.multisample ?? { count: Engine.MULTISAMPLE_COUNT },
|
|
6033
|
-
}
|
|
6112
|
+
};
|
|
6034
6113
|
}
|
|
6035
6114
|
createPipelines() {
|
|
6036
6115
|
this.materialSampler = this.device.createSampler({
|
|
@@ -6148,8 +6227,6 @@ export class Engine {
|
|
|
6148
6227
|
// creation with "binding doesn't exist", naming a binding no effect
|
|
6149
6228
|
// author ever wrote. Sharing the code meant sharing these.
|
|
6150
6229
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
6151
|
-
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
6152
|
-
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "3d" } },
|
|
6153
6230
|
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "3d" } },
|
|
6154
6231
|
// And the scene's COVERAGE and bloom, without which the tap cannot
|
|
6155
6232
|
// reconstruct a pixel: the HDR target is premultiplied, so colour alone
|
|
@@ -6170,7 +6247,7 @@ export class Engine {
|
|
|
6170
6247
|
const seedModule = this.device.createShaderModule({ label: "cast distance seed", code: buildCastSeedShader(Engine.MULTISAMPLE_COUNT) });
|
|
6171
6248
|
const stepModule = this.device.createShaderModule({ label: "cast distance step", code: buildCastStepShader() });
|
|
6172
6249
|
const resolveModule = this.device.createShaderModule({ label: "cast distance resolve", code: buildCastResolveShader() });
|
|
6173
|
-
this.
|
|
6250
|
+
this.initRenderPipeline({
|
|
6174
6251
|
label: "cast distance seed",
|
|
6175
6252
|
layout: "auto",
|
|
6176
6253
|
vertex: { module: seedModule, entryPoint: "vs" },
|
|
@@ -6180,21 +6257,21 @@ export class Engine {
|
|
|
6180
6257
|
targets: [{ format: CAST_SEED_FORMAT }, { format: CAST_COVERAGE_FORMAT }],
|
|
6181
6258
|
},
|
|
6182
6259
|
primitive: { topology: "triangle-list" },
|
|
6183
|
-
});
|
|
6184
|
-
this.
|
|
6260
|
+
}, (p) => (this.castSeedPipeline = p));
|
|
6261
|
+
this.initRenderPipeline({
|
|
6185
6262
|
label: "cast distance step",
|
|
6186
6263
|
layout: "auto",
|
|
6187
6264
|
vertex: { module: stepModule, entryPoint: "vs" },
|
|
6188
6265
|
fragment: { module: stepModule, entryPoint: "fs", targets: [{ format: CAST_SEED_FORMAT }] },
|
|
6189
6266
|
primitive: { topology: "triangle-list" },
|
|
6190
|
-
});
|
|
6191
|
-
this.
|
|
6267
|
+
}, (p) => (this.castStepPipeline = p));
|
|
6268
|
+
this.initRenderPipeline({
|
|
6192
6269
|
label: "cast distance resolve",
|
|
6193
6270
|
layout: "auto",
|
|
6194
6271
|
vertex: { module: resolveModule, entryPoint: "vs" },
|
|
6195
6272
|
fragment: { module: resolveModule, entryPoint: "fs", targets: [{ format: CAST_DIST_FORMAT }] },
|
|
6196
6273
|
primitive: { topology: "triangle-list" },
|
|
6197
|
-
});
|
|
6274
|
+
}, (p) => (this.castResolvePipeline = p));
|
|
6198
6275
|
// 65504 is the largest half float. Bound wherever the field is not
|
|
6199
6276
|
// running, so rzCastDistance answers "unreachably far" and an effect keyed
|
|
6200
6277
|
// on it draws nothing at all.
|
|
@@ -6214,11 +6291,6 @@ export class Engine {
|
|
|
6214
6291
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
6215
6292
|
});
|
|
6216
6293
|
this.device.queue.writeTexture({ texture: this.fallbackMaterialTexture }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1]);
|
|
6217
|
-
this.noFillBuffer = this.device.createBuffer({
|
|
6218
|
-
label: "model light (none)",
|
|
6219
|
-
size: 48,
|
|
6220
|
-
usage: GPUBufferUsage.UNIFORM,
|
|
6221
|
-
});
|
|
6222
6294
|
// Generic shared-toon ramp: lit white down to a soft cool shadow tone with
|
|
6223
6295
|
// a tight terminator around the midpoint, approximating MMD's toon ramps.
|
|
6224
6296
|
const TOON_H = 64;
|
|
@@ -6310,10 +6382,10 @@ export class Engine {
|
|
|
6310
6382
|
// The positional lights. Always bound, empty or not, so every material
|
|
6311
6383
|
// pipeline shares one layout whether or not the scene has any.
|
|
6312
6384
|
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
6313
|
-
// The
|
|
6314
|
-
//
|
|
6315
|
-
//
|
|
6316
|
-
|
|
6385
|
+
// The world's sky (8) and the BRDF LUT (9). The sky is always bound —
|
|
6386
|
+
// the 1x1 fallback when the scene has none — so every material
|
|
6387
|
+
// pipeline keeps sharing one layout. (7 was the far cascade's map,
|
|
6388
|
+
// which the atlas at 3 replaces.)
|
|
6317
6389
|
{ binding: 8, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
6318
6390
|
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
6319
6391
|
// The cast's shadow on a stage — setStageCastShadow.
|
|
@@ -6381,8 +6453,8 @@ export class Engine {
|
|
|
6381
6453
|
const neutral = compileGraph(DEFAULT_GRAPH, { renderClass: "auto", alphaMode: "opaque" });
|
|
6382
6454
|
if (!neutral.ok)
|
|
6383
6455
|
throw new Error("failed to compile the neutral default graph");
|
|
6384
|
-
const neutralModule = this.
|
|
6385
|
-
this.
|
|
6456
|
+
const neutralModule = this.cachedShaderModule(neutral.wgsl, "neutral base (default graph)");
|
|
6457
|
+
this.initRenderPipeline(this.renderPipelineDesc({
|
|
6386
6458
|
label: "neutral base pipeline",
|
|
6387
6459
|
layout: mainPipelineLayout,
|
|
6388
6460
|
shaderModule: neutralModule,
|
|
@@ -6394,9 +6466,9 @@ export class Engine {
|
|
|
6394
6466
|
depthWriteEnabled: true,
|
|
6395
6467
|
depthCompare: this.depthAhead,
|
|
6396
6468
|
},
|
|
6397
|
-
});
|
|
6469
|
+
}), (p) => (this.neutralPipeline = p), true);
|
|
6398
6470
|
// Depth-write-off twin for transparent-bucket draws (see pipelineForDrawCall).
|
|
6399
|
-
this.
|
|
6471
|
+
this.initRenderPipeline(this.renderPipelineDesc({
|
|
6400
6472
|
label: "neutral base pipeline (no depth write)",
|
|
6401
6473
|
layout: mainPipelineLayout,
|
|
6402
6474
|
shaderModule: neutralModule,
|
|
@@ -6408,7 +6480,7 @@ export class Engine {
|
|
|
6408
6480
|
depthWriteEnabled: false,
|
|
6409
6481
|
depthCompare: this.depthAhead,
|
|
6410
6482
|
},
|
|
6411
|
-
});
|
|
6483
|
+
}), (p) => (this.neutralPipelineNoDepthWrite = p), true);
|
|
6412
6484
|
// Depth-only prepass for transparent draws (see depth-prepass.ts): writes the
|
|
6413
6485
|
// fabric's depth AFTER its color blended, so outlines drawn later are
|
|
6414
6486
|
// occluded behind it. Color targets kept for pass compatibility, writeMask 0.
|
|
@@ -6427,7 +6499,7 @@ export class Engine {
|
|
|
6427
6499
|
depthCompare: this.depthAhead,
|
|
6428
6500
|
},
|
|
6429
6501
|
};
|
|
6430
|
-
this.
|
|
6502
|
+
this.initRenderPipeline({
|
|
6431
6503
|
label: "opaque depth prepass",
|
|
6432
6504
|
...prepassDesc,
|
|
6433
6505
|
fragment: {
|
|
@@ -6435,11 +6507,11 @@ export class Engine {
|
|
|
6435
6507
|
entryPoint: "fs",
|
|
6436
6508
|
targets: sceneTargetsFor("depth-prepass", this.sceneFormats),
|
|
6437
6509
|
},
|
|
6438
|
-
});
|
|
6510
|
+
}, (p) => (this.depthPrepassPipeline = p), true);
|
|
6439
6511
|
// The SOLID prime: same module, cutoff forced to exactly 1.0. Only texels
|
|
6440
6512
|
// whose blend ignores the destination may pre-claim depth in the
|
|
6441
6513
|
// transparent phase — see the override's note in depth-prepass.ts.
|
|
6442
|
-
this.
|
|
6514
|
+
this.initRenderPipeline({
|
|
6443
6515
|
label: "transparent solid prepass",
|
|
6444
6516
|
...prepassDesc,
|
|
6445
6517
|
fragment: {
|
|
@@ -6448,14 +6520,14 @@ export class Engine {
|
|
|
6448
6520
|
constants: { CUTOFF: 1.0 },
|
|
6449
6521
|
targets: sceneTargetsFor("depth-prepass", this.sceneFormats),
|
|
6450
6522
|
},
|
|
6451
|
-
});
|
|
6523
|
+
}, (p) => (this.solidPrepassPipeline = p), true);
|
|
6452
6524
|
// The HAIR prime: solid texels only, and stencil-fenced off the eye
|
|
6453
6525
|
// silhouette. It records after the non-hair opaque draws, so the eye has
|
|
6454
6526
|
// already written its stencil — not-equal here is what keeps the primed
|
|
6455
6527
|
// hair depth from ever claiming the pixels the see-through-hair pass needs
|
|
6456
6528
|
// the eye to survive on. (Bundle draws use the PASS's stencil reference;
|
|
6457
6529
|
// only pipeline/bind/vertex state resets across executeBundles.)
|
|
6458
|
-
this.
|
|
6530
|
+
this.initRenderPipeline({
|
|
6459
6531
|
label: "hair depth prime",
|
|
6460
6532
|
...prepassDesc,
|
|
6461
6533
|
depthStencil: {
|
|
@@ -6471,7 +6543,7 @@ export class Engine {
|
|
|
6471
6543
|
constants: { CUTOFF: 1.0 },
|
|
6472
6544
|
targets: sceneTargetsFor("depth-prepass", this.sceneFormats),
|
|
6473
6545
|
},
|
|
6474
|
-
});
|
|
6546
|
+
}, (p) => (this.hairPrimePipeline = p), true);
|
|
6475
6547
|
// The matrices, then one vec4: the caster sphere, which effects' rzShadow
|
|
6476
6548
|
// tests before any tap. The materials and the ground bind the matrices only.
|
|
6477
6549
|
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
@@ -6495,7 +6567,7 @@ export class Engine {
|
|
|
6495
6567
|
label: "shadow depth",
|
|
6496
6568
|
code: SHADOW_DEPTH_SHADER_WGSL,
|
|
6497
6569
|
});
|
|
6498
|
-
this.
|
|
6570
|
+
this.initRenderPipeline({
|
|
6499
6571
|
label: "shadow depth pipeline",
|
|
6500
6572
|
// Group 1 is the main pass's per-material layout so each shadow draw can
|
|
6501
6573
|
// rebind the draw call's existing material bind group for the alpha test.
|
|
@@ -6516,21 +6588,21 @@ export class Engine {
|
|
|
6516
6588
|
depthBiasSlopeScale: 1.5,
|
|
6517
6589
|
depthBiasClamp: 0,
|
|
6518
6590
|
},
|
|
6519
|
-
});
|
|
6591
|
+
}, (p) => (this.shadowDepthPipeline = p));
|
|
6520
6592
|
this.shadowComparisonSampler = this.device.createSampler({
|
|
6521
6593
|
compare: "less",
|
|
6522
6594
|
magFilter: "linear",
|
|
6523
6595
|
minFilter: "linear",
|
|
6524
6596
|
});
|
|
6525
|
-
// One
|
|
6526
|
-
//
|
|
6527
|
-
this.
|
|
6528
|
-
label:
|
|
6529
|
-
size: [
|
|
6597
|
+
// One atlas, a tile per cascade — the layout the game's pipeline uses, and
|
|
6598
|
+
// one binding for every reader.
|
|
6599
|
+
this.shadowAtlasTexture = this.device.createTexture({
|
|
6600
|
+
label: "sun shadow atlas",
|
|
6601
|
+
size: [SHADOW_ATLAS_SIZE, SHADOW_ATLAS_SIZE],
|
|
6530
6602
|
format: Engine.SHADOW_DEPTH_FORMAT,
|
|
6531
6603
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
6532
|
-
})
|
|
6533
|
-
this.
|
|
6604
|
+
});
|
|
6605
|
+
this.shadowAtlasView = this.shadowAtlasTexture.createView();
|
|
6534
6606
|
// The cast's shadow on a stage (setStageCastShadow): one map, only the cast
|
|
6535
6607
|
// in it, from the stage's own direction. Always allocated so every material
|
|
6536
6608
|
// bind group and every instance's shadow bind group stays one shape; drawn
|
|
@@ -6550,14 +6622,20 @@ export class Engine {
|
|
|
6550
6622
|
// One-shot bake of Blender EEVEE's combined BRDF LUT (DFG + LTC packed rgba8unorm).
|
|
6551
6623
|
this.bakeBrdfLut();
|
|
6552
6624
|
this.bakeGroundNoise();
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
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
|
+
}
|
|
6561
6639
|
this.stageGradeFallback = this.device.createTexture({
|
|
6562
6640
|
label: "stage grade fallback",
|
|
6563
6641
|
size: [1, 1, 1],
|
|
@@ -6566,7 +6644,6 @@ export class Engine {
|
|
|
6566
6644
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
6567
6645
|
});
|
|
6568
6646
|
this.uploadStageGrade();
|
|
6569
|
-
this.bakeFilmicLut();
|
|
6570
6647
|
// BEFORE the bind group below, which binds it. Full size from the start:
|
|
6571
6648
|
// every material pipeline binds this, so sizing it to the light count would
|
|
6572
6649
|
// mean rebuilding bind groups whenever a scene gained a lamp. Uploaded from
|
|
@@ -6625,7 +6702,6 @@ export class Engine {
|
|
|
6625
6702
|
// Same lights the materials read. A lamp that lit the cast and not the
|
|
6626
6703
|
// floor under her would read as a sticker.
|
|
6627
6704
|
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
6628
|
-
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
6629
6705
|
// The floor mirror: the mirror camera's view-projection, the reflection
|
|
6630
6706
|
// resolve, an ordinary sampler beside the comparison one, and the
|
|
6631
6707
|
// mirror pass's own depth for the depth-proportional blur.
|
|
@@ -6654,7 +6730,7 @@ export class Engine {
|
|
|
6654
6730
|
cullMode: "back",
|
|
6655
6731
|
depthStencil: { format: this.depthFormat, depthWriteEnabled: true, depthCompare: this.depthAhead },
|
|
6656
6732
|
};
|
|
6657
|
-
this.
|
|
6733
|
+
this.initRenderPipeline(this.renderPipelineDesc(this.groundPipelineConfig(false)), (p) => (this.groundShadowPipeline = p));
|
|
6658
6734
|
// Outline: group 0 = per-frame (camera), group 1 = per-instance (skinMats), group 2 = per-material (edge uniforms)
|
|
6659
6735
|
this.outlinePerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
6660
6736
|
label: "outline per-frame bind group layout",
|
|
@@ -6717,7 +6793,12 @@ export class Engine {
|
|
|
6717
6793
|
label: "outline pipeline",
|
|
6718
6794
|
layout: outlinePipelineLayout,
|
|
6719
6795
|
shaderModule: outlineShaderModule,
|
|
6720
|
-
|
|
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
|
+
],
|
|
6721
6802
|
fragmentTargets: sceneTargetsFor("outline", this.sceneFormats),
|
|
6722
6803
|
cullMode: "back",
|
|
6723
6804
|
depthStencil: {
|
|
@@ -6751,12 +6832,12 @@ export class Engine {
|
|
|
6751
6832
|
stencilWriteMask: 0,
|
|
6752
6833
|
},
|
|
6753
6834
|
};
|
|
6754
|
-
this.
|
|
6755
|
-
this.
|
|
6835
|
+
this.initRenderPipeline(this.renderPipelineDesc(outlineDesc), (p) => (this.outlinePipeline = p), true);
|
|
6836
|
+
this.initRenderPipeline(this.renderPipelineDesc({
|
|
6756
6837
|
...outlineDesc,
|
|
6757
6838
|
label: "outline pipeline (mirror)",
|
|
6758
6839
|
cullMode: "front",
|
|
6759
|
-
});
|
|
6840
|
+
}), (p) => (this.outlineMirrorPipeline = p), true);
|
|
6760
6841
|
// ─── Selection overlay (screen-space edge-detect on a per-material mask) ───
|
|
6761
6842
|
// Reuses outline camera + main skinMats bind group layouts. No group 2 (no per-mat uniform).
|
|
6762
6843
|
const selectionMaskPipelineLayout = this.device.createPipelineLayout({
|
|
@@ -6767,7 +6848,7 @@ export class Engine {
|
|
|
6767
6848
|
label: "selection mask shader",
|
|
6768
6849
|
code: SELECTION_MASK_SHADER_WGSL,
|
|
6769
6850
|
});
|
|
6770
|
-
this.
|
|
6851
|
+
this.initRenderPipeline({
|
|
6771
6852
|
label: "selection mask pipeline",
|
|
6772
6853
|
layout: selectionMaskPipelineLayout,
|
|
6773
6854
|
vertex: { module: selectionMaskShaderModule, entryPoint: "vs", buffers: outlineVertexBuffers },
|
|
@@ -6779,7 +6860,7 @@ export class Engine {
|
|
|
6779
6860
|
primitive: { cullMode: "none" },
|
|
6780
6861
|
// Single-sample, no depth (depth-always via not attaching a depth buffer at all).
|
|
6781
6862
|
multisample: { count: 1 },
|
|
6782
|
-
});
|
|
6863
|
+
}, (p) => (this.selectionMaskPipeline = p));
|
|
6783
6864
|
this.selectionEdgeBindGroupLayout = this.device.createBindGroupLayout({
|
|
6784
6865
|
label: "selection edge bind group layout",
|
|
6785
6866
|
entries: [
|
|
@@ -6796,7 +6877,7 @@ export class Engine {
|
|
|
6796
6877
|
label: "selection edge shader",
|
|
6797
6878
|
code: SELECTION_EDGE_SHADER_WGSL,
|
|
6798
6879
|
});
|
|
6799
|
-
this.
|
|
6880
|
+
this.initRenderPipeline({
|
|
6800
6881
|
label: "selection edge pipeline",
|
|
6801
6882
|
layout: selectionEdgePipelineLayout,
|
|
6802
6883
|
vertex: { module: selectionEdgeShaderModule, entryPoint: "vs" },
|
|
@@ -6815,7 +6896,7 @@ export class Engine {
|
|
|
6815
6896
|
},
|
|
6816
6897
|
primitive: { topology: "triangle-list" },
|
|
6817
6898
|
multisample: { count: 1 },
|
|
6818
|
-
});
|
|
6899
|
+
}, (p) => (this.selectionEdgePipeline = p));
|
|
6819
6900
|
this.selectionSampler = this.device.createSampler({
|
|
6820
6901
|
label: "selection sampler",
|
|
6821
6902
|
magFilter: "linear",
|
|
@@ -6832,10 +6913,7 @@ export class Engine {
|
|
|
6832
6913
|
this.setupGizmo();
|
|
6833
6914
|
// ─── Editor overlays (instanced wireframe primitives) ────────────
|
|
6834
6915
|
this.setupOverlay();
|
|
6835
|
-
// ─── Bloom
|
|
6836
|
-
// Mirrors source/blender/draw/engines/eevee/shaders/effect_bloom_frag.glsl.
|
|
6837
|
-
// Firefly suppression lives in the blit (Karis luminance-weighted 4-tap average). A single-pass
|
|
6838
|
-
// 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) ───
|
|
6839
6917
|
this.bloomSampler = this.device.createSampler({
|
|
6840
6918
|
label: "bloom sampler",
|
|
6841
6919
|
magFilter: "linear",
|
|
@@ -6843,81 +6921,25 @@ export class Engine {
|
|
|
6843
6921
|
addressModeU: "clamp-to-edge",
|
|
6844
6922
|
addressModeV: "clamp-to-edge",
|
|
6845
6923
|
});
|
|
6846
|
-
this.
|
|
6847
|
-
label: "bloom
|
|
6848
|
-
size: 16,
|
|
6849
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
6850
|
-
});
|
|
6851
|
-
this.bloomUpsampleUniformBuffer = this.device.createBuffer({
|
|
6852
|
-
label: "bloom upsample uniforms",
|
|
6853
|
-
size: 16,
|
|
6854
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
6855
|
-
});
|
|
6856
|
-
this.bloomBlitBindGroupLayout = this.device.createBindGroupLayout({
|
|
6857
|
-
label: "bloom blit layout",
|
|
6924
|
+
this.bloomPrefilterBindGroupLayout = this.device.createBindGroupLayout({
|
|
6925
|
+
label: "bloom prefilter layout",
|
|
6858
6926
|
entries: [
|
|
6859
6927
|
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float" } },
|
|
6860
6928
|
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
6861
6929
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float" } },
|
|
6862
6930
|
],
|
|
6863
6931
|
});
|
|
6864
|
-
this.
|
|
6865
|
-
label: "bloom
|
|
6932
|
+
this.bloomBlurBindGroupLayout = this.device.createBindGroupLayout({
|
|
6933
|
+
label: "bloom blur layout",
|
|
6866
6934
|
entries: [
|
|
6867
6935
|
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
6868
6936
|
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
6869
6937
|
],
|
|
6870
6938
|
});
|
|
6871
|
-
this.
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // matching downsample mip (base add)
|
|
6876
|
-
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
6877
|
-
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
6878
|
-
],
|
|
6879
|
-
});
|
|
6880
|
-
const bloomBlitShader = this.device.createShaderModule({
|
|
6881
|
-
label: "bloom blit (Karis prefilter)",
|
|
6882
|
-
code: BLOOM_BLIT_SHADER_WGSL,
|
|
6883
|
-
});
|
|
6884
|
-
const bloomDownsampleShader = this.device.createShaderModule({
|
|
6885
|
-
label: "bloom downsample 13-tap",
|
|
6886
|
-
code: BLOOM_DOWNSAMPLE_SHADER_WGSL,
|
|
6887
|
-
});
|
|
6888
|
-
const bloomUpsampleShader = this.device.createShaderModule({
|
|
6889
|
-
label: "bloom upsample 9-tap tent",
|
|
6890
|
-
code: BLOOM_UPSAMPLE_SHADER_WGSL,
|
|
6891
|
-
});
|
|
6892
|
-
const bloomBlitLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomBlitBindGroupLayout] });
|
|
6893
|
-
const bloomDownLayout = this.device.createPipelineLayout({
|
|
6894
|
-
bindGroupLayouts: [this.bloomDownsampleBindGroupLayout],
|
|
6895
|
-
});
|
|
6896
|
-
const bloomUpLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomUpsampleBindGroupLayout] });
|
|
6897
|
-
this.bloomBlitPipeline = this.device.createRenderPipeline({
|
|
6898
|
-
label: "bloom blit pipeline",
|
|
6899
|
-
layout: bloomBlitLayout,
|
|
6900
|
-
vertex: { module: bloomBlitShader, entryPoint: "vs" },
|
|
6901
|
-
fragment: { module: bloomBlitShader, entryPoint: "fs", targets: [{ format: this.hdrFormat }] },
|
|
6902
|
-
primitive: { topology: "triangle-list" },
|
|
6903
|
-
});
|
|
6904
|
-
this.bloomDownsamplePipeline = this.device.createRenderPipeline({
|
|
6905
|
-
label: "bloom downsample pipeline",
|
|
6906
|
-
layout: bloomDownLayout,
|
|
6907
|
-
vertex: { module: bloomDownsampleShader, entryPoint: "vs" },
|
|
6908
|
-
fragment: { module: bloomDownsampleShader, entryPoint: "fs", targets: [{ format: this.hdrFormat }] },
|
|
6909
|
-
primitive: { topology: "triangle-list" },
|
|
6910
|
-
});
|
|
6911
|
-
this.bloomUpsamplePipeline = this.device.createRenderPipeline({
|
|
6912
|
-
label: "bloom upsample pipeline",
|
|
6913
|
-
layout: bloomUpLayout,
|
|
6914
|
-
vertex: { module: bloomUpsampleShader, entryPoint: "vs" },
|
|
6915
|
-
fragment: { module: bloomUpsampleShader, entryPoint: "fs", targets: [{ format: this.hdrFormat }] },
|
|
6916
|
-
primitive: { topology: "triangle-list" },
|
|
6917
|
-
});
|
|
6918
|
-
// ─── Composite: HDR + bloom → Filmic → swapchain (premultiplied) ───
|
|
6919
|
-
// Bloom color/intensity applied HERE (pyramid is pure energy; tint belongs to the combine step,
|
|
6920
|
-
// 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.
|
|
6921
6943
|
this.compositeUniformBuffer = this.device.createBuffer({
|
|
6922
6944
|
label: "composite view uniforms",
|
|
6923
6945
|
// 15 × vec4f: (exposure, invGamma, _, _) · (bloom tint, intensity) ·
|
|
@@ -6959,8 +6981,6 @@ export class Engine {
|
|
|
6959
6981
|
// Aux mask/alpha texture — composite reads .g to reconstruct the alpha that
|
|
6960
6982
|
// used to live in the HDR target before the rg11b10ufloat switch.
|
|
6961
6983
|
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
6962
|
-
// Filmic tone LUT (r16float, filterable) — sampled with the binding-2 sampler.
|
|
6963
|
-
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
6964
6984
|
// 360 backdrop equirect (PhotoDome-style skybox) — 1×1 fallback when unset.
|
|
6965
6985
|
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
6966
6986
|
// User background-effect params — dummy buffer when no effect is set. The
|
|
@@ -6975,9 +6995,6 @@ export class Engine {
|
|
|
6975
6995
|
texture: { sampleType: "depth", viewDimension: "2d", multisampled: true },
|
|
6976
6996
|
},
|
|
6977
6997
|
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
6978
|
-
// AgX's 57³ cube. Decompressed and uploaded off the critical path, so a
|
|
6979
|
-
// 1×1×1 stand-in keeps the bind group valid until it arrives.
|
|
6980
|
-
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { viewDimension: "3d" } },
|
|
6981
6998
|
// The scene's own grade cube (setStageGrade) — 1×1×1 stand-in while it
|
|
6982
6999
|
// has none, which the flag bit keeps from ever being sampled.
|
|
6983
7000
|
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, texture: { viewDimension: "3d" } },
|
|
@@ -7003,12 +7020,12 @@ export class Engine {
|
|
|
7003
7020
|
this.compositePipelineLayout = this.device.createPipelineLayout({
|
|
7004
7021
|
bindGroupLayouts: [this.compositeBindGroupLayout],
|
|
7005
7022
|
});
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
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
|
+
}));
|
|
7012
7029
|
// GPU vertex-morph compute pipeline (shared by all models; per-model bind groups).
|
|
7013
7030
|
// Bindings: 0-4 read-only storage (base pos, CSR rowStart/colMorph/colOffset, weights),
|
|
7014
7031
|
// 5 read-write storage (vertex buffer), 6 uniform (params).
|
|
@@ -7025,14 +7042,14 @@ export class Engine {
|
|
|
7025
7042
|
{ binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
|
|
7026
7043
|
],
|
|
7027
7044
|
});
|
|
7028
|
-
this.
|
|
7045
|
+
this.initComputePipeline({
|
|
7029
7046
|
label: "morph compute pipeline",
|
|
7030
7047
|
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.morphComputeBindGroupLayout] }),
|
|
7031
7048
|
compute: {
|
|
7032
7049
|
module: this.device.createShaderModule({ label: "morph compute shader", code: MORPH_COMPUTE_WGSL }),
|
|
7033
7050
|
entryPoint: "cs",
|
|
7034
7051
|
},
|
|
7035
|
-
});
|
|
7052
|
+
}, (p) => (this.morphComputePipeline = p));
|
|
7036
7053
|
// GPU frustum cull. One pipeline for the whole scene; the bind group is rebuilt
|
|
7037
7054
|
// with the buffers whenever the draw list changes. See shaders/passes/cull.ts.
|
|
7038
7055
|
this.cullBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -7052,21 +7069,23 @@ export class Engine {
|
|
|
7052
7069
|
// encoder it is set on, so a WGSL slip here would take the whole frame down —
|
|
7053
7070
|
// every pass, every model, an unrelated-looking cascade of style-group and
|
|
7054
7071
|
// effect failures. Catching it turns that into "culling is off".
|
|
7055
|
-
|
|
7056
|
-
|
|
7072
|
+
// Async, so a failure arrives as the rejection rather than a scoped error.
|
|
7073
|
+
const cullBuild = this.device
|
|
7074
|
+
.createComputePipelineAsync({
|
|
7057
7075
|
label: "cull pipeline",
|
|
7058
7076
|
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.cullBindGroupLayout] }),
|
|
7059
7077
|
compute: {
|
|
7060
7078
|
module: this.device.createShaderModule({ label: "cull compute shader", code: CULL_COMPUTE_WGSL }),
|
|
7061
7079
|
entryPoint: "cs",
|
|
7062
7080
|
},
|
|
7063
|
-
})
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7081
|
+
})
|
|
7082
|
+
.then((p) => {
|
|
7083
|
+
this.cullPipeline = p;
|
|
7084
|
+
}, (err) => {
|
|
7067
7085
|
console.error(`[cull] pipeline failed to compile — frustum culling disabled:\n${err.message}`);
|
|
7068
7086
|
this.cullPipeline = null;
|
|
7069
7087
|
});
|
|
7088
|
+
this.initPipelineJobs?.push(cullBuild);
|
|
7070
7089
|
this.bloomPassDescriptor = {
|
|
7071
7090
|
label: "bloom pass",
|
|
7072
7091
|
colorAttachments: [
|
|
@@ -7107,7 +7126,7 @@ export class Engine {
|
|
|
7107
7126
|
layout: this.pickPerFrameBindGroupLayout,
|
|
7108
7127
|
entries: [{ binding: 0, resource: { buffer: this.cameraUniformBuffer } }],
|
|
7109
7128
|
});
|
|
7110
|
-
this.
|
|
7129
|
+
this.initRenderPipeline({
|
|
7111
7130
|
label: "pick pipeline",
|
|
7112
7131
|
layout: pickPipelineLayout,
|
|
7113
7132
|
vertex: { module: pickShaderModule, buffers: fullVertexBuffers },
|
|
@@ -7121,7 +7140,7 @@ export class Engine {
|
|
|
7121
7140
|
depthWriteEnabled: true,
|
|
7122
7141
|
depthCompare: this.depthAhead,
|
|
7123
7142
|
},
|
|
7124
|
-
});
|
|
7143
|
+
}, (p) => (this.pickPipeline = p));
|
|
7125
7144
|
this.pickReadbackBuffer = this.device.createBuffer({
|
|
7126
7145
|
label: "pick readback",
|
|
7127
7146
|
size: 256,
|
|
@@ -7178,6 +7197,10 @@ export class Engine {
|
|
|
7178
7197
|
// in full the moment there is something to apply it to.
|
|
7179
7198
|
if (!this.device)
|
|
7180
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;
|
|
7181
7204
|
// Fixed override (offline/video rendering) wins; otherwise track CSS size × dpr.
|
|
7182
7205
|
const dpr = window.devicePixelRatio || 1;
|
|
7183
7206
|
const width = this.fixedRenderSize ? this.fixedRenderSize.width : Math.floor(this.canvas.clientWidth * dpr);
|
|
@@ -7395,37 +7418,6 @@ export class Engine {
|
|
|
7395
7418
|
// rebind both against the new one.
|
|
7396
7419
|
this.buildGroundBindGroup();
|
|
7397
7420
|
this.buildMirrorSurfaceBindGroup();
|
|
7398
|
-
// Bloom pyramid: mip 0 is half-res, each subsequent mip halves again.
|
|
7399
|
-
// Mip count chosen so the coarsest mip is ≥4 px on the short side, capped at BLOOM_MAX_LEVELS.
|
|
7400
|
-
const bw = Math.max(1, Math.floor(width / 2));
|
|
7401
|
-
const bh = Math.max(1, Math.floor(height / 2));
|
|
7402
|
-
const shortSide = Math.max(1, Math.min(bw, bh));
|
|
7403
|
-
this.bloomMipCount = Math.max(1, Math.min(Engine.BLOOM_MAX_LEVELS, Math.floor(Math.log2(shortSide)) - 1));
|
|
7404
|
-
this.bloomDownTexture?.destroy();
|
|
7405
|
-
this.bloomDownTexture = this.device.createTexture({
|
|
7406
|
-
label: "bloom down pyramid",
|
|
7407
|
-
size: [bw, bh],
|
|
7408
|
-
mipLevelCount: this.bloomMipCount,
|
|
7409
|
-
format: this.hdrFormat,
|
|
7410
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
7411
|
-
});
|
|
7412
|
-
this.bloomUpTexture?.destroy();
|
|
7413
|
-
this.bloomUpTexture = this.device.createTexture({
|
|
7414
|
-
label: "bloom up pyramid",
|
|
7415
|
-
size: [bw, bh],
|
|
7416
|
-
mipLevelCount: Math.max(1, this.bloomMipCount - 1),
|
|
7417
|
-
format: this.hdrFormat,
|
|
7418
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
7419
|
-
});
|
|
7420
|
-
this.bloomDownMipViews = [];
|
|
7421
|
-
for (let i = 0; i < this.bloomMipCount; i++) {
|
|
7422
|
-
this.bloomDownMipViews.push(this.bloomDownTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }));
|
|
7423
|
-
}
|
|
7424
|
-
this.bloomUpMipViews = [];
|
|
7425
|
-
const upLevels = Math.max(1, this.bloomMipCount - 1);
|
|
7426
|
-
for (let i = 0; i < upLevels; i++) {
|
|
7427
|
-
this.bloomUpMipViews.push(this.bloomUpTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }));
|
|
7428
|
-
}
|
|
7429
7421
|
this.depthTexture?.destroy();
|
|
7430
7422
|
this.depthTexture = this.device.createTexture({
|
|
7431
7423
|
label: "depth texture",
|
|
@@ -7537,49 +7529,10 @@ export class Engine {
|
|
|
7537
7529
|
},
|
|
7538
7530
|
],
|
|
7539
7531
|
};
|
|
7540
|
-
this.
|
|
7541
|
-
|
|
7542
|
-
//
|
|
7543
|
-
this.
|
|
7544
|
-
label: "bloom blit bind group",
|
|
7545
|
-
layout: this.bloomBlitBindGroupLayout,
|
|
7546
|
-
entries: [
|
|
7547
|
-
{ binding: 0, resource: this.hdrResolveTexture.createView() },
|
|
7548
|
-
{ binding: 1, resource: { buffer: this.bloomBlitUniformBuffer } },
|
|
7549
|
-
{ binding: 2, resource: this.maskResolveView },
|
|
7550
|
-
],
|
|
7551
|
-
});
|
|
7552
|
-
// Downsample[i] reads bloomDown mip (i-1), writes bloomDown mip i. i ∈ [1..N-1].
|
|
7553
|
-
this.bloomDownsampleBindGroups = [];
|
|
7554
|
-
for (let i = 1; i < this.bloomMipCount; i++) {
|
|
7555
|
-
this.bloomDownsampleBindGroups.push(this.device.createBindGroup({
|
|
7556
|
-
label: `bloom downsample ${i}`,
|
|
7557
|
-
layout: this.bloomDownsampleBindGroupLayout,
|
|
7558
|
-
entries: [
|
|
7559
|
-
{ binding: 0, resource: this.bloomDownMipViews[i - 1] },
|
|
7560
|
-
{ binding: 1, resource: this.bloomSampler },
|
|
7561
|
-
],
|
|
7562
|
-
}));
|
|
7563
|
-
}
|
|
7564
|
-
// Upsample[i] writes bloomUp mip i. Coarsest step reads bloomDown[N-1] (no prior up yet);
|
|
7565
|
-
// subsequent steps read bloomUp[i+1]. Both read bloomDown[i] as the base (additive combine).
|
|
7566
|
-
this.bloomUpsampleBindGroups = [];
|
|
7567
|
-
const topIdx = this.bloomMipCount - 2;
|
|
7568
|
-
for (let i = topIdx; i >= 0; i--) {
|
|
7569
|
-
const srcView = i === topIdx ? this.bloomDownMipViews[this.bloomMipCount - 1] : this.bloomUpMipViews[i + 1];
|
|
7570
|
-
this.bloomUpsampleBindGroups.push(this.device.createBindGroup({
|
|
7571
|
-
label: `bloom upsample ${i}`,
|
|
7572
|
-
layout: this.bloomUpsampleBindGroupLayout,
|
|
7573
|
-
entries: [
|
|
7574
|
-
{ binding: 0, resource: srcView },
|
|
7575
|
-
{ binding: 1, resource: this.bloomDownMipViews[i] },
|
|
7576
|
-
{ binding: 2, resource: this.bloomSampler },
|
|
7577
|
-
{ binding: 3, resource: { buffer: this.bloomUpsampleUniformBuffer } },
|
|
7578
|
-
],
|
|
7579
|
-
}));
|
|
7580
|
-
}
|
|
7581
|
-
// Composite reads bloomUp mip 0 (full pyramid collapsed); fallback to bloomDown mip 0 if no upsample level.
|
|
7582
|
-
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];
|
|
7583
7536
|
this.rebuildCompositeBindGroup();
|
|
7584
7537
|
}
|
|
7585
7538
|
this.writeCompositeViewUniforms();
|
|
@@ -7842,16 +7795,16 @@ export class Engine {
|
|
|
7842
7795
|
},
|
|
7843
7796
|
multisample: { count: Engine.OVERLAY_SAMPLE_COUNT },
|
|
7844
7797
|
};
|
|
7845
|
-
this.overlayPipeline =
|
|
7798
|
+
this.initRenderPipeline(overlayPipelineDescriptor, (p) => (this.overlayPipeline = p));
|
|
7846
7799
|
// The solid volumes: the same shader and layout, with no depth write and no
|
|
7847
7800
|
// culling. A translucent body must not hide the rig behind it, and you have
|
|
7848
7801
|
// to see its far wall for it to read as a volume rather than a silhouette.
|
|
7849
|
-
this.
|
|
7802
|
+
this.initRenderPipeline({
|
|
7850
7803
|
...overlayPipelineDescriptor,
|
|
7851
7804
|
label: "overlay solid pipeline",
|
|
7852
7805
|
primitive: { topology: "triangle-list", cullMode: "none" },
|
|
7853
7806
|
depthStencil: { format: "depth24plus", depthWriteEnabled: false, depthCompare: "always" },
|
|
7854
|
-
});
|
|
7807
|
+
}, (p) => (this.overlaySolidPipeline = p));
|
|
7855
7808
|
const compositeShader = this.device.createShaderModule({
|
|
7856
7809
|
label: "overlay composite shader",
|
|
7857
7810
|
code: OVERLAY_COMPOSITE_SHADER_WGSL,
|
|
@@ -7939,7 +7892,7 @@ export class Engine {
|
|
|
7939
7892
|
],
|
|
7940
7893
|
});
|
|
7941
7894
|
const wireShader = this.device.createShaderModule({ label: "wireframe shader", code: WIREFRAME_SHADER_WGSL });
|
|
7942
|
-
this.
|
|
7895
|
+
this.initRenderPipeline({
|
|
7943
7896
|
label: "wireframe pipeline",
|
|
7944
7897
|
layout: this.device.createPipelineLayout({
|
|
7945
7898
|
label: "wireframe pipeline layout",
|
|
@@ -7966,7 +7919,7 @@ export class Engine {
|
|
|
7966
7919
|
// so a bone behind a triangle must not be punched out by it.
|
|
7967
7920
|
depthStencil: { format: "depth24plus", depthWriteEnabled: false, depthCompare: this.depthAhead },
|
|
7968
7921
|
multisample: { count: Engine.OVERLAY_SAMPLE_COUNT },
|
|
7969
|
-
});
|
|
7922
|
+
}, (p) => (this.wireframePipeline = p));
|
|
7970
7923
|
// The mesh's own depth, so the wireframe can be occluded by the body it
|
|
7971
7924
|
// belongs to. Occluded is the default everywhere — Blender's edit mode, Maya,
|
|
7972
7925
|
// three's and Babylon's wireframe materials all depth-test, and X-ray is a
|
|
@@ -8671,7 +8624,7 @@ export class Engine {
|
|
|
8671
8624
|
// picture later.
|
|
8672
8625
|
if (!this.device ||
|
|
8673
8626
|
!this.mainPerFrameBindGroupLayout ||
|
|
8674
|
-
!this.
|
|
8627
|
+
!this.shadowAtlasView ||
|
|
8675
8628
|
!this.cameraUniformBuffer ||
|
|
8676
8629
|
!this.mirrorCameraBuffer ||
|
|
8677
8630
|
!this.lightUniformBuffer ||
|
|
@@ -8696,11 +8649,10 @@ export class Engine {
|
|
|
8696
8649
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
8697
8650
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
8698
8651
|
{ binding: 2, resource: this.materialSampler },
|
|
8699
|
-
{ binding: 3, resource: this.
|
|
8652
|
+
{ binding: 3, resource: this.shadowAtlasView },
|
|
8700
8653
|
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
8701
8654
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
8702
8655
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
8703
|
-
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
8704
8656
|
{ binding: 8, resource: env },
|
|
8705
8657
|
{ binding: 9, resource: this.brdfLutView },
|
|
8706
8658
|
{ binding: 10, resource: this.castShadowView },
|
|
@@ -8713,11 +8665,10 @@ export class Engine {
|
|
|
8713
8665
|
{ binding: 0, resource: { buffer: this.mirrorCameraBuffer } },
|
|
8714
8666
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
8715
8667
|
{ binding: 2, resource: this.materialSampler },
|
|
8716
|
-
{ binding: 3, resource: this.
|
|
8668
|
+
{ binding: 3, resource: this.shadowAtlasView },
|
|
8717
8669
|
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
8718
8670
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
8719
8671
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
8720
|
-
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
8721
8672
|
{ binding: 8, resource: env },
|
|
8722
8673
|
{ binding: 9, resource: this.brdfLutView },
|
|
8723
8674
|
{ binding: 10, resource: this.castShadowView },
|
|
@@ -8783,6 +8734,7 @@ export class Engine {
|
|
|
8783
8734
|
this.lightData[base + 5] = this.sun.color.y;
|
|
8784
8735
|
this.lightData[base + 6] = this.sun.color.z;
|
|
8785
8736
|
this.lightData[base + 7] = this.sun.strength;
|
|
8737
|
+
this.lightDataWords[DIR_LAYERS_AT + index] = this.sunLayers;
|
|
8786
8738
|
if (index >= this.lightCount)
|
|
8787
8739
|
this.lightCount = index + 1;
|
|
8788
8740
|
this.updateLightBuffer();
|
|
@@ -8923,6 +8875,8 @@ export class Engine {
|
|
|
8923
8875
|
this.sun.strength = options.strength;
|
|
8924
8876
|
if (options.shadow !== undefined)
|
|
8925
8877
|
this.sunShadow = Math.min(Math.max(options.shadow, 0), 1);
|
|
8878
|
+
if (options.layers !== undefined)
|
|
8879
|
+
this.sunLayers = options.layers >>> 0;
|
|
8926
8880
|
if (options.direction) {
|
|
8927
8881
|
this.sun.direction = options.direction;
|
|
8928
8882
|
this.shadowLightVPDirty = true;
|
|
@@ -9014,7 +8968,20 @@ export class Engine {
|
|
|
9014
8968
|
* params make: a scene document's JSON passes straight in, and so does a
|
|
9015
8969
|
* literal typed into a console. Vec3 satisfies it either way. */
|
|
9016
8970
|
lights) {
|
|
9017
|
-
|
|
8971
|
+
// The directional ones go to the light uniform's slots beside the sun's —
|
|
8972
|
+
// three of them, in order; the rest are records the grid indexes.
|
|
8973
|
+
const directional = (lights ?? []).filter((l) => l.kind === "directional");
|
|
8974
|
+
for (let k = 1; k < 4; k++) {
|
|
8975
|
+
const l = directional[k - 1];
|
|
8976
|
+
const base = 4 + k * 8;
|
|
8977
|
+
const a = l?.aim;
|
|
8978
|
+
const len = a ? Math.hypot(a.x, a.y, a.z) : 0;
|
|
8979
|
+
const i = l ? Math.max(l.intensity ?? 1, 0) : 0;
|
|
8980
|
+
this.lightData.set(l && len > 0 ? [a.x / len, a.y / len, a.z / len, 0, l.color.x * i, l.color.y * i, l.color.z * i, 1] : [0, 0, -1, 0, 0, 0, 0, 0], base);
|
|
8981
|
+
this.lightDataWords[DIR_LAYERS_AT + k] = l && len > 0 ? (l.layers ?? ALL_LAYERS) >>> 0 : 0;
|
|
8982
|
+
}
|
|
8983
|
+
this.updateLightBuffer();
|
|
8984
|
+
const list = (lights ?? []).filter((l) => l.kind !== "directional").slice(0, MAX_LIGHTS);
|
|
9018
8985
|
this.docLightCount = list.length;
|
|
9019
8986
|
// RECORDS only — the header belongs to allocateLightSlots, the one writer.
|
|
9020
8987
|
// This used to zero-and-upload the header region too, which left two CPU
|
|
@@ -9043,7 +9010,7 @@ export class Engine {
|
|
|
9043
9010
|
// vector cannot be normalised, and a cone around nothing would light
|
|
9044
9011
|
// nothing at all — a light that vanishes because its aim was left at the
|
|
9045
9012
|
// default is worse than one that shines everywhere.
|
|
9046
|
-
const len = l.aim ? Math.hypot(l.aim.x, l.aim.y, l.aim.z) : 0;
|
|
9013
|
+
const len = l.aim && l.kind !== "point" ? Math.hypot(l.aim.x, l.aim.y, l.aim.z) : 0;
|
|
9047
9014
|
this.lightsData[b + 7] = len > 0 ? 1 : 0;
|
|
9048
9015
|
this.lightsData[b + 8] = len > 0 ? l.aim.x / len : 0;
|
|
9049
9016
|
this.lightsData[b + 9] = len > 0 ? l.aim.y / len : 0;
|
|
@@ -9055,7 +9022,8 @@ export class Engine {
|
|
|
9055
9022
|
const inner = len > 0 ? Math.cos((Math.min(Math.max(l.innerAngle ?? (l.angle ?? 45) * 0.8, 0), 179) / 2) * (Math.PI / 180)) : -1;
|
|
9056
9023
|
this.lightsData[b + 11] = outer;
|
|
9057
9024
|
this.lightsData[b + 12] = Math.max(inner, outer);
|
|
9058
|
-
|
|
9025
|
+
// The layers it does NOT reach, as bits — inverted so a zero reaches all.
|
|
9026
|
+
this.lightsWords[b + 13] = ~(l.layers ?? ALL_LAYERS) >>> 0;
|
|
9059
9027
|
this.lightsData[b + 14] = 0;
|
|
9060
9028
|
this.lightsData[b + 15] = 0;
|
|
9061
9029
|
}
|
|
@@ -9444,6 +9412,8 @@ export class Engine {
|
|
|
9444
9412
|
* ground plane must not draw, and the far shadow cascade has nothing to
|
|
9445
9413
|
* cover without one (see the cascade loop). */
|
|
9446
9414
|
hasStage() {
|
|
9415
|
+
if (this.nativeStage)
|
|
9416
|
+
return true;
|
|
9447
9417
|
for (const inst of this.modelInstances.values())
|
|
9448
9418
|
if (inst.isStage)
|
|
9449
9419
|
return true;
|
|
@@ -9492,11 +9462,32 @@ export class Engine {
|
|
|
9492
9462
|
inst.model.setName(newName);
|
|
9493
9463
|
this.modelInstances.set(newName, inst);
|
|
9494
9464
|
}
|
|
9465
|
+
/**
|
|
9466
|
+
* One model's own ambient: the irradiance arriving at it, as the 27 floats of
|
|
9467
|
+
* folded SH setWorldAmbient takes (ibl.ts), in place of the world's. Null
|
|
9468
|
+
* gives it the world's again.
|
|
9469
|
+
*
|
|
9470
|
+
* The game's pipeline lights every renderer by its own probe sample, and a
|
|
9471
|
+
* character by an ambient of its own; a loader that carries those hands them
|
|
9472
|
+
* in here. Linear radiance, strength included.
|
|
9473
|
+
*/
|
|
9474
|
+
setModelAmbient(name, sh) {
|
|
9475
|
+
const inst = this.modelInstances.get(name);
|
|
9476
|
+
if (!inst || !this.device)
|
|
9477
|
+
return false;
|
|
9478
|
+
const o = inst.objectLight;
|
|
9479
|
+
o[4] = sh && sh.length >= 27 ? 1 : 0;
|
|
9480
|
+
for (let i = 0; i < 9; i++) {
|
|
9481
|
+
o.set(sh && sh.length >= 27 ? [sh[i * 3], sh[i * 3 + 1], sh[i * 3 + 2], 0] : [0, 0, 0, 0], 8 + i * 4);
|
|
9482
|
+
}
|
|
9483
|
+
this.device.queue.writeBuffer(inst.lightBuffer, 0, o);
|
|
9484
|
+
return true;
|
|
9485
|
+
}
|
|
9495
9486
|
/**
|
|
9496
9487
|
* Light one model with more than the world gives it: a FILL, linear RGB times
|
|
9497
9488
|
* its surface colour, added after its material graph. Null takes it away again.
|
|
9498
9489
|
*
|
|
9499
|
-
* After the graph, not in its ambient: an NPR ramp
|
|
9490
|
+
* After the graph, not in its ambient: an NPR ramp reads an ambient fill as
|
|
9500
9491
|
* light, so sliding it moved her shadows and flipped whole regions across a
|
|
9501
9492
|
* hard step. Added after, it brightens her evenly and every shadow stays put.
|
|
9502
9493
|
*
|
|
@@ -9504,83 +9495,25 @@ export class Engine {
|
|
|
9504
9495
|
* lit by their lamps and a near-black ambient, while its characters take a
|
|
9505
9496
|
* flat base light of their own — so a stage whose World is right for the room
|
|
9506
9497
|
* leaves a face turned from the lamps in the dark. This is that second light,
|
|
9507
|
-
* for the models the host counts as cast.
|
|
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.
|
|
9508
9500
|
*/
|
|
9509
9501
|
setModelFill(name, fill) {
|
|
9510
|
-
return this.writeModelLight(name, 0, fill);
|
|
9511
|
-
}
|
|
9512
|
-
/**
|
|
9513
|
-
* A model's own sun: the colour and strength the scene's sun has FOR THIS
|
|
9514
|
-
* MODEL, in place of the scene's. Null gives it the scene's sun back.
|
|
9515
|
-
*
|
|
9516
|
-
* The other half of lighting a stage and its cast apart. A game's stage is
|
|
9517
|
-
* lit by its own daylight — Aether Gazer's kitchen at twenty times white —
|
|
9518
|
-
* while its characters take a key light of their own that the stage never
|
|
9519
|
-
* states. So a stage carries the sun it was lit by, and the scene's sun
|
|
9520
|
-
* stays the cast's. Direction and shadow are still the scene's: one sun
|
|
9521
|
-
* casts, and it casts the same way on both.
|
|
9522
|
-
*/
|
|
9523
|
-
setModelSun(name, sun) {
|
|
9524
|
-
return this.writeModelLight(name, 4, sun);
|
|
9525
|
-
}
|
|
9526
|
-
/**
|
|
9527
|
-
* Light one model by the sky's AVERAGE rather than its shape: the world's
|
|
9528
|
-
* colour and brightness from every direction alike. False gives it the
|
|
9529
|
-
* directional sky back.
|
|
9530
|
-
*
|
|
9531
|
-
* For a cast under an anime look. An HDRI's sky is brighter and warmer one
|
|
9532
|
-
* way than another, and taken at each normal it shades a face with soft
|
|
9533
|
-
* realistic gradients that no toon ramp drew — PBR on an anime face. Aether
|
|
9534
|
-
* Gazer gives its characters one flat base light instead. World strength and
|
|
9535
|
-
* colour still reach her; only the shape is gone. A flat World is its own
|
|
9536
|
-
* average, so without an HDRI this changes nothing.
|
|
9537
|
-
*/
|
|
9538
|
-
setModelFlatSky(name, flat) {
|
|
9539
|
-
return this.writeModelLight(name, 8, flat ? new Vec3(1, 0, 0) : null);
|
|
9540
|
-
}
|
|
9541
|
-
/** One part of a model's light, at `at` (0 the fill, 4 the sun, 8 its
|
|
9542
|
-
* options) in its ModelLight; w says whether that part is set. All unset
|
|
9543
|
-
* releases the buffer and the model takes the shared zero stand-in again. */
|
|
9544
|
-
writeModelLight(name, at, value) {
|
|
9545
9502
|
const inst = this.modelInstances.get(name);
|
|
9546
9503
|
if (!inst || !this.device)
|
|
9547
9504
|
return false;
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
const any = light[3] > 0 || light[7] > 0 || light[11] > 0;
|
|
9551
|
-
if (!any) {
|
|
9552
|
-
if (!inst.lightBuffer)
|
|
9553
|
-
return true;
|
|
9554
|
-
const retired = inst.lightBuffer;
|
|
9555
|
-
inst.lightBuffer = null;
|
|
9556
|
-
inst.modelLight = null;
|
|
9557
|
-
inst.mainPerInstanceBindGroup = this.perInstanceBindGroup(name, inst.skinMatrixBuffer, null);
|
|
9558
|
-
this.bundlesDirty = true;
|
|
9559
|
-
// Retired once the GPU is done with it: an encoded frame may still name it.
|
|
9560
|
-
void this.device.queue.onSubmittedWorkDone().then(() => retired.destroy());
|
|
9561
|
-
return true;
|
|
9562
|
-
}
|
|
9563
|
-
inst.modelLight = light;
|
|
9564
|
-
if (!inst.lightBuffer) {
|
|
9565
|
-
inst.lightBuffer = this.device.createBuffer({
|
|
9566
|
-
label: `${name}: light`,
|
|
9567
|
-
size: 48,
|
|
9568
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
9569
|
-
});
|
|
9570
|
-
inst.mainPerInstanceBindGroup = this.perInstanceBindGroup(name, inst.skinMatrixBuffer, inst.lightBuffer);
|
|
9571
|
-
this.bundlesDirty = true;
|
|
9572
|
-
}
|
|
9573
|
-
this.device.queue.writeBuffer(inst.lightBuffer, 0, light.buffer);
|
|
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);
|
|
9574
9507
|
return true;
|
|
9575
9508
|
}
|
|
9576
|
-
/** The per-model group: its skinning matrices and its
|
|
9509
|
+
/** The per-model group: its skinning matrices and its ObjectLight. */
|
|
9577
9510
|
perInstanceBindGroup(name, skinMatrixBuffer, light) {
|
|
9578
9511
|
return this.device.createBindGroup({
|
|
9579
9512
|
label: `${name}: main per-instance bind group`,
|
|
9580
9513
|
layout: this.mainPerInstanceBindGroupLayout,
|
|
9581
9514
|
entries: [
|
|
9582
9515
|
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
9583
|
-
{ binding: 1, resource: { buffer: light
|
|
9516
|
+
{ binding: 1, resource: { buffer: light } },
|
|
9584
9517
|
],
|
|
9585
9518
|
});
|
|
9586
9519
|
}
|
|
@@ -9591,6 +9524,9 @@ export class Engine {
|
|
|
9591
9524
|
// Before the texture cache below frees it: a stale entry here would hand a
|
|
9592
9525
|
// destroyed texture to the next setPlaneFrame.
|
|
9593
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);
|
|
9594
9530
|
inst.model.stop();
|
|
9595
9531
|
for (const path of inst.textureCacheKeys) {
|
|
9596
9532
|
const tex = this.textureCache.get(path);
|
|
@@ -11162,7 +11098,9 @@ export class Engine {
|
|
|
11162
11098
|
let changed = force;
|
|
11163
11099
|
for (let i = 0; i < this.cullDraws.length; i++) {
|
|
11164
11100
|
const { inst, draw } = this.cullDraws[i];
|
|
11165
|
-
|
|
11101
|
+
// bit 0: switched off; bit 1: drawn by its native look, not its graph
|
|
11102
|
+
const v = (inst.hiddenMaterials.has(draw.materialName) || inst.morphHiddenMaterials.has(draw.materialName) ? 1 : 0) |
|
|
11103
|
+
(this.nativeLooks?.dresses(inst.name, draw.materialName) ? 2 : 0);
|
|
11166
11104
|
if (out[i] !== v) {
|
|
11167
11105
|
out[i] = v;
|
|
11168
11106
|
changed = true;
|
|
@@ -11307,6 +11245,15 @@ export class Engine {
|
|
|
11307
11245
|
maxZ = Math.max(maxZ, data[o + 18] + r);
|
|
11308
11246
|
}
|
|
11309
11247
|
}
|
|
11248
|
+
if (this.nativeStage) {
|
|
11249
|
+
const nb = this.nativeStage.bounds;
|
|
11250
|
+
minX = Math.min(minX, nb.min[0]);
|
|
11251
|
+
maxX = Math.max(maxX, nb.max[0]);
|
|
11252
|
+
minY = Math.min(minY, nb.min[1]);
|
|
11253
|
+
maxY = Math.max(maxY, nb.max[1]);
|
|
11254
|
+
minZ = Math.min(minZ, nb.min[2]);
|
|
11255
|
+
maxZ = Math.max(maxZ, nb.max[2]);
|
|
11256
|
+
}
|
|
11310
11257
|
if (!Number.isFinite(minX)) {
|
|
11311
11258
|
this.shadowSceneBounds = null;
|
|
11312
11259
|
return;
|
|
@@ -12100,7 +12047,20 @@ export class Engine {
|
|
|
12100
12047
|
{ binding: 2, resource: this.materialSampler },
|
|
12101
12048
|
],
|
|
12102
12049
|
}));
|
|
12103
|
-
|
|
12050
|
+
// Its ObjectLight: the world's ambient, and the layers its kind draws on —
|
|
12051
|
+
// the cast on the character layer as well as the default one. Words:
|
|
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);
|
|
12055
|
+
new Uint32Array(objectLight.buffer)[0] =
|
|
12056
|
+
isStage || isPlane || isProp ? RENDERING_LAYER_DEFAULT : (RENDERING_LAYER_DEFAULT | RENDERING_LAYER_CHARACTER) >>> 0;
|
|
12057
|
+
const lightBuffer = this.device.createBuffer({
|
|
12058
|
+
label: `${name}: object light`,
|
|
12059
|
+
size: objectLight.byteLength,
|
|
12060
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
12061
|
+
});
|
|
12062
|
+
this.device.queue.writeBuffer(lightBuffer, 0, objectLight);
|
|
12063
|
+
const mainPerInstanceBindGroup = this.perInstanceBindGroup(name, skinMatrixBuffer, lightBuffer);
|
|
12104
12064
|
const pickPerInstanceBindGroup = this.device.createBindGroup({
|
|
12105
12065
|
label: `${name}: pick per-instance bind group`,
|
|
12106
12066
|
layout: this.pickPerInstanceBindGroupLayout,
|
|
@@ -12133,8 +12093,8 @@ export class Engine {
|
|
|
12133
12093
|
shadowDrawCalls: [],
|
|
12134
12094
|
shadowBindGroups,
|
|
12135
12095
|
mainPerInstanceBindGroup,
|
|
12136
|
-
lightBuffer
|
|
12137
|
-
|
|
12096
|
+
lightBuffer,
|
|
12097
|
+
objectLight,
|
|
12138
12098
|
pickPerInstanceBindGroup,
|
|
12139
12099
|
pickDrawCalls: [],
|
|
12140
12100
|
isStage,
|
|
@@ -12298,7 +12258,10 @@ export class Engine {
|
|
|
12298
12258
|
this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices);
|
|
12299
12259
|
}
|
|
12300
12260
|
buildGroundPipeline(soft, mirrored = false) {
|
|
12301
|
-
return this.createRenderPipeline(
|
|
12261
|
+
return this.createRenderPipeline(this.groundPipelineConfig(soft, mirrored));
|
|
12262
|
+
}
|
|
12263
|
+
groundPipelineConfig(soft, mirrored = false) {
|
|
12264
|
+
return {
|
|
12302
12265
|
...this.groundShadowPipelineDesc,
|
|
12303
12266
|
// A REFLECTION FLIPS WINDING — determinant -1, see reflection.ts. The
|
|
12304
12267
|
// ground culls back faces, so drawn into the mirror pass with the ordinary
|
|
@@ -12308,11 +12271,8 @@ export class Engine {
|
|
|
12308
12271
|
// the mirror, and the same answer the scene-pass pipelines already use.
|
|
12309
12272
|
cullMode: mirrored ? "none" : this.groundShadowPipelineDesc.cullMode,
|
|
12310
12273
|
label: `ground shadow pipeline${soft ? " (soft)" : ""}${mirrored ? " (mirror)" : ""}`,
|
|
12311
|
-
shaderModule: this.
|
|
12312
|
-
|
|
12313
|
-
code: groundShaderWgsl(soft),
|
|
12314
|
-
}),
|
|
12315
|
-
});
|
|
12274
|
+
shaderModule: this.cachedShaderModule(groundShaderWgsl(soft), soft ? "ground shadow (soft)" : "ground shadow"),
|
|
12275
|
+
};
|
|
12316
12276
|
}
|
|
12317
12277
|
/** The ground's pipeline for the mirror pass, built on the first frame a
|
|
12318
12278
|
* mirror actually needs it. */
|
|
@@ -12348,7 +12308,6 @@ export class Engine {
|
|
|
12348
12308
|
gb[3] = fadeStart;
|
|
12349
12309
|
gb[4] = fadeEnd;
|
|
12350
12310
|
gb[5] = shadowStrength;
|
|
12351
|
-
gb[6] = 1 / SHADOW_CASCADES[0].mapSize;
|
|
12352
12311
|
gb[7] = gridSpacing;
|
|
12353
12312
|
gb[8] = gridLineWidth;
|
|
12354
12313
|
gb[9] = gridLineOpacity;
|
|
@@ -12370,19 +12329,6 @@ export class Engine {
|
|
|
12370
12329
|
// Which variant the draw picks. Zero is the sharp shader, which is the one
|
|
12371
12330
|
// that existed before softness did.
|
|
12372
12331
|
this.groundSoft = gb[18] > 0;
|
|
12373
|
-
// gb[17] — does the FAR cascade hold anything?
|
|
12374
|
-
//
|
|
12375
|
-
// It holds something only when a stage is loaded; that is what it exists for
|
|
12376
|
-
// and the cascade loop already skips drawing into it otherwise, leaving it
|
|
12377
|
-
// cleared. A cleared depth map compares as "no occluder", so the ground's far
|
|
12378
|
-
// branch is nine comparison taps whose answer is known in advance.
|
|
12379
|
-
//
|
|
12380
|
-
// That branch runs wherever the NEAR cascade does not reach, and the near one
|
|
12381
|
-
// is a 64-unit box around the camera target — so on a floor receding to the
|
|
12382
|
-
// horizon it is most of the visible pixels, on the most expensive
|
|
12383
|
-
// full-coverage draw in the frame. Skipping it is free in the exact sense:
|
|
12384
|
-
// the shader takes vis = 1.0, which is what the taps would have returned.
|
|
12385
|
-
gb[17] = this.hasStage() ? 1 : 0;
|
|
12386
12332
|
// gb[20..23] — the caster sphere, refreshed every frame by
|
|
12387
12333
|
// writeGroundCasterSphere. Zero here so a frame that renders before the
|
|
12388
12334
|
// first cull (there is one) reads "nothing casts" and skips the taps, which
|
|
@@ -12447,12 +12393,11 @@ export class Engine {
|
|
|
12447
12393
|
entries: [
|
|
12448
12394
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
12449
12395
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
12450
|
-
{ binding: 2, resource: this.
|
|
12396
|
+
{ binding: 2, resource: this.shadowAtlasView },
|
|
12451
12397
|
{ binding: 3, resource: this.shadowComparisonSampler },
|
|
12452
12398
|
{ binding: 4, resource: { buffer: this.groundShadowMaterialBuffer } },
|
|
12453
12399
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
12454
12400
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
12455
|
-
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
12456
12401
|
{ binding: 8, resource: { buffer: this.mirrorVPBuffer } },
|
|
12457
12402
|
// Created in handleResize, which runs during init — before any ground
|
|
12458
12403
|
// can exist to bind it.
|
|
@@ -12470,12 +12415,11 @@ export class Engine {
|
|
|
12470
12415
|
entries: [
|
|
12471
12416
|
{ binding: 0, resource: { buffer: this.mirrorCameraBuffer } },
|
|
12472
12417
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
12473
|
-
{ binding: 2, resource: this.
|
|
12418
|
+
{ binding: 2, resource: this.shadowAtlasView },
|
|
12474
12419
|
{ binding: 3, resource: this.shadowComparisonSampler },
|
|
12475
12420
|
{ binding: 4, resource: { buffer: this.groundShadowMaterialBuffer } },
|
|
12476
12421
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
12477
12422
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
12478
|
-
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
12479
12423
|
{ binding: 8, resource: { buffer: this.mirrorVPBuffer } },
|
|
12480
12424
|
// Stand-ins, not the live mirror textures: this group draws INTO the
|
|
12481
12425
|
// mirror pass, where those are the attachments.
|
|
@@ -12698,6 +12642,17 @@ export class Engine {
|
|
|
12698
12642
|
],
|
|
12699
12643
|
});
|
|
12700
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
|
+
}
|
|
12701
12656
|
}
|
|
12702
12657
|
// Model-space AABB for the cull compute, grown by the three things that can
|
|
12703
12658
|
// put geometry outside the vertices it was measured from: a vertex morph,
|
|
@@ -14089,54 +14044,54 @@ export class Engine {
|
|
|
14089
14044
|
// the final frame's depth — and the ground, which draws on `hasGround` alone,
|
|
14090
14045
|
// keeps PCF-sampling a character that is no longer in the scene. One clearing
|
|
14091
14046
|
// pass on the transition to empty, then it stops.
|
|
14092
|
-
|
|
14093
|
-
|
|
14094
|
-
|
|
14095
|
-
|
|
14096
|
-
|
|
14097
|
-
//
|
|
14098
|
-
//
|
|
14099
|
-
//
|
|
14100
|
-
//
|
|
14101
|
-
//
|
|
14102
|
-
//
|
|
14103
|
-
//
|
|
14104
|
-
//
|
|
14105
|
-
// 0.43 had ONE shadow map. This is half of what the second one costs.
|
|
14047
|
+
// The game's globals before anything native draws: the stage's casters
|
|
14048
|
+
// into the atlas are the first.
|
|
14049
|
+
if (this.nativeStage || this.nativeLooks?.size)
|
|
14050
|
+
this.prepareNativeLooks(encoder);
|
|
14051
|
+
if (hasModels || this.nativeStage || this.shadowMapPopulated) {
|
|
14052
|
+
// The outer cascades are the STAGE's, and each costs a pass over the whole
|
|
14053
|
+
// cast. A scene with no stage has no set piece out there: every caster
|
|
14054
|
+
// sits inside the inner cascade, which follows the camera target, and
|
|
14055
|
+
// the outer tiles' only readers are ground pixels beyond it, where
|
|
14056
|
+
// nothing is casting. So without a stage only the inner tile is drawn;
|
|
14057
|
+
// the others keep the far plane cascade 0's pass cleared the whole atlas
|
|
14058
|
+
// to, which compares as "no occluder" — the correct answer there.
|
|
14106
14059
|
const stage = this.hasStage();
|
|
14107
14060
|
for (let ci = 0; ci < SHADOW_CASCADES.length; ci++) {
|
|
14108
|
-
|
|
14109
|
-
// Already cleared and still unwanted — nothing to do, and the map still
|
|
14110
|
-
// holds the far plane from the pass that cleared it.
|
|
14111
|
-
if (!wanted && this.shadowCascadeCleared[ci])
|
|
14061
|
+
if (ci > 0 && !stage)
|
|
14112
14062
|
continue;
|
|
14063
|
+
const c = SHADOW_CASCADES[ci];
|
|
14113
14064
|
const sp = encoder.beginRenderPass({
|
|
14114
|
-
// One timestamp pair exists for "shadow"; the
|
|
14065
|
+
// One timestamp pair exists for "shadow"; the inner cascade wears it.
|
|
14115
14066
|
timestampWrites: ci === 0 ? this.stamps("shadow") : undefined,
|
|
14116
14067
|
colorAttachments: [],
|
|
14117
14068
|
depthStencilAttachment: {
|
|
14118
|
-
view: this.
|
|
14069
|
+
view: this.shadowAtlasView,
|
|
14119
14070
|
depthClearValue: 1.0,
|
|
14120
|
-
depthLoadOp: "clear",
|
|
14071
|
+
depthLoadOp: ci === 0 ? "clear" : "load",
|
|
14121
14072
|
depthStoreOp: "store",
|
|
14122
14073
|
},
|
|
14123
14074
|
});
|
|
14075
|
+
// The tile: a bundle draws through the pass's viewport, which it
|
|
14076
|
+
// cannot set itself, so one recording serves whichever tile is live.
|
|
14077
|
+
sp.setViewport(c.origin[0], c.origin[1], c.mapSize, c.mapSize, 0, 1);
|
|
14124
14078
|
// The per-model `visible` test that used to guard this is gone: it is a
|
|
14125
14079
|
// per-frame boolean, and baking it into a bundle would make toggling a
|
|
14126
14080
|
// model re-record. It lives in the cull compute now, which zeroes the
|
|
14127
14081
|
// instance count of an invisible model's draws.
|
|
14128
|
-
if (
|
|
14082
|
+
if (this.shadowBundles[ci])
|
|
14129
14083
|
sp.executeBundles([this.shadowBundles[ci]]);
|
|
14130
14084
|
// The mirror throws shade like anything else standing on the floor.
|
|
14131
14085
|
// Direct rather than in the bundle: the bundles are recorded on scene
|
|
14132
14086
|
// STRUCTURE, and a mirror comes and goes with an effect's weight.
|
|
14133
|
-
if (
|
|
14087
|
+
if (this.mirrorSurface && this.mirrorShadowPipeline) {
|
|
14134
14088
|
sp.setPipeline(this.mirrorShadowPipeline);
|
|
14135
14089
|
sp.setBindGroup(0, this.mirrorShadowBindGroups[ci]);
|
|
14136
14090
|
sp.draw(6);
|
|
14137
14091
|
}
|
|
14092
|
+
if (this.nativeStage && this.nativeShadowHost)
|
|
14093
|
+
this.nativeStage.drawShadow(sp, this.nativeShadowHost, this.nativeGlobals, ci, this.shadowLightVPMatrix.subarray(ci * 16, ci * 16 + 16), 8);
|
|
14138
14094
|
sp.end();
|
|
14139
|
-
this.shadowCascadeCleared[ci] = !wanted;
|
|
14140
14095
|
}
|
|
14141
14096
|
// The cast's shadow on a stage: drawn while a stage asks for it and has a
|
|
14142
14097
|
// cast to shade it with; cleared once when it stops, then left alone.
|
|
@@ -14151,7 +14106,7 @@ export class Engine {
|
|
|
14151
14106
|
cp.end();
|
|
14152
14107
|
this.castShadowCleared = !castOn;
|
|
14153
14108
|
}
|
|
14154
|
-
this.shadowMapPopulated = hasModels;
|
|
14109
|
+
this.shadowMapPopulated = hasModels || !!this.nativeStage;
|
|
14155
14110
|
}
|
|
14156
14111
|
// Before the particles and before the field pass: both may read the grid,
|
|
14157
14112
|
// and a grid stepped after them is one frame stale in everything that used it.
|
|
@@ -14196,6 +14151,17 @@ export class Engine {
|
|
|
14196
14151
|
// is FALSE for the cleared buffer — every such fragment silently rejected.
|
|
14197
14152
|
// One redundant word against a whole class of invisible failure.
|
|
14198
14153
|
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
14154
|
+
// The models dressed in the game's materials, and the game's character
|
|
14155
|
+
// passes after them (unity/looks.ts). They set their own stencil
|
|
14156
|
+
// references, so the engine's is put back after.
|
|
14157
|
+
if (this.nativeLooks?.size) {
|
|
14158
|
+
this.nativeLooks.drawOpaque(pass);
|
|
14159
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
14160
|
+
}
|
|
14161
|
+
if (this.nativeStage && this.nativeHost) {
|
|
14162
|
+
this.nativeStage.drawOpaque(pass, this.nativeHost, this.nativeGlobals, this.nativeFrameTextures(), this.eyeArray());
|
|
14163
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
14164
|
+
}
|
|
14199
14165
|
if (this.hasGround)
|
|
14200
14166
|
this.renderGround(pass);
|
|
14201
14167
|
// After the ground for the same early-z reason, and before the transparent
|
|
@@ -14228,6 +14194,14 @@ export class Engine {
|
|
|
14228
14194
|
// which was never this renderer's bottleneck.
|
|
14229
14195
|
const camView = this.sceneView("camera");
|
|
14230
14196
|
this.forEachInstance((inst) => this.renderModelTransparentPhase(pass, inst, camView));
|
|
14197
|
+
if (this.nativeStage && this.nativeHost) {
|
|
14198
|
+
this.nativeStage.drawTransparent(pass, this.nativeHost, this.nativeGlobals, this.nativeFrameTextures(), this.eyeArray());
|
|
14199
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
14200
|
+
}
|
|
14201
|
+
if (this.nativeLooks?.size) {
|
|
14202
|
+
this.nativeLooks.drawTransparent(pass);
|
|
14203
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
14204
|
+
}
|
|
14231
14205
|
// Last in the pass: depth-tested against everything drawn above, so a
|
|
14232
14206
|
// particle behind the character is simply hidden, and still inside the HDR
|
|
14233
14207
|
// target so an `#bloom` effect reaches the pyramid below.
|
|
@@ -14244,54 +14218,16 @@ export class Engine {
|
|
|
14244
14218
|
// The field mounts, likewise: after the scene so foregrounds can read its
|
|
14245
14219
|
// depth, before the composite that samples both layers.
|
|
14246
14220
|
this.renderFieldPass(encoder);
|
|
14247
|
-
// Bloom
|
|
14248
|
-
//
|
|
14249
|
-
//
|
|
14250
|
-
//
|
|
14251
|
-
//
|
|
14252
|
-
//
|
|
14253
|
-
//
|
|
14254
|
-
|
|
14255
|
-
|
|
14256
|
-
//
|
|
14257
|
-
// is therefore invisible in the frame and nine render passes cheaper.
|
|
14258
|
-
if (this.bloomContributes() && this.bloomBlitBindGroup && this.compositeBindGroup && this.bloomMipCount > 0) {
|
|
14259
|
-
const bloomAtt = this.bloomPassDescriptor.colorAttachments;
|
|
14260
|
-
// 1. Blit — opens the pyramid's timing span. See stampOpen: the nine
|
|
14261
|
-
// passes below read as ONE component, which is the only useful grain.
|
|
14262
|
-
bloomAtt[0].view = this.bloomDownMipViews[0];
|
|
14263
|
-
this.bloomPassDescriptor.timestampWrites = this.stampOpen("bloom");
|
|
14264
|
-
const pBlit = encoder.beginRenderPass(this.bloomPassDescriptor);
|
|
14265
|
-
pBlit.setPipeline(this.bloomBlitPipeline);
|
|
14266
|
-
pBlit.setBindGroup(0, this.bloomBlitBindGroup);
|
|
14267
|
-
pBlit.draw(3);
|
|
14268
|
-
pBlit.end();
|
|
14269
|
-
// 2. Downsample chain
|
|
14270
|
-
this.bloomPassDescriptor.timestampWrites = undefined;
|
|
14271
|
-
for (let i = 1; i < this.bloomMipCount; i++) {
|
|
14272
|
-
bloomAtt[0].view = this.bloomDownMipViews[i];
|
|
14273
|
-
const p = encoder.beginRenderPass(this.bloomPassDescriptor);
|
|
14274
|
-
p.setPipeline(this.bloomDownsamplePipeline);
|
|
14275
|
-
p.setBindGroup(0, this.bloomDownsampleBindGroups[i - 1]);
|
|
14276
|
-
p.draw(3);
|
|
14277
|
-
p.end();
|
|
14278
|
-
}
|
|
14279
|
-
// 3. Upsample chain (coarsest to finest; bindGroups[0] is the coarsest step)
|
|
14280
|
-
const upSteps = this.bloomUpsampleBindGroups.length;
|
|
14281
|
-
const topIdx = this.bloomMipCount - 2;
|
|
14282
|
-
for (let k = 0; k < upSteps; k++) {
|
|
14283
|
-
const levelIdx = topIdx - k; // writes bloomUp[levelIdx]
|
|
14284
|
-
bloomAtt[0].view = this.bloomUpMipViews[levelIdx];
|
|
14285
|
-
// The LAST upsample closes the span opened on the blit.
|
|
14286
|
-
this.bloomPassDescriptor.timestampWrites = k === upSteps - 1 ? this.stampClose("bloom") : undefined;
|
|
14287
|
-
const p = encoder.beginRenderPass(this.bloomPassDescriptor);
|
|
14288
|
-
p.setPipeline(this.bloomUpsamplePipeline);
|
|
14289
|
-
p.setBindGroup(0, this.bloomUpsampleBindGroups[k]);
|
|
14290
|
-
p.draw(3);
|
|
14291
|
-
p.end();
|
|
14292
|
-
}
|
|
14293
|
-
}
|
|
14294
|
-
// 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.
|
|
14295
14231
|
const swapchainView = this.context.getCurrentTexture().createView();
|
|
14296
14232
|
const compositeAttachment = this.compositePassDescriptor.colorAttachments[0];
|
|
14297
14233
|
compositeAttachment.view = swapchainView;
|
|
@@ -14436,11 +14372,12 @@ export class Engine {
|
|
|
14436
14372
|
outgoing.push([id, install]);
|
|
14437
14373
|
}
|
|
14438
14374
|
}
|
|
14439
|
-
|
|
14440
|
-
|
|
14441
|
-
|
|
14442
|
-
|
|
14443
|
-
|
|
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 }));
|
|
14444
14381
|
// Repoints every draw call away from the outgoing installs (among
|
|
14445
14382
|
// everything else) — only past this line is destroying them safe.
|
|
14446
14383
|
this.assignDrawCallGroups(inst, claimed);
|
|
@@ -14455,6 +14392,38 @@ export class Engine {
|
|
|
14455
14392
|
conflicts: [...conflicts],
|
|
14456
14393
|
};
|
|
14457
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
|
+
}
|
|
14458
14427
|
/** Add or replace a single style group by id. `opts` may carry a `previewNode` for the
|
|
14459
14428
|
* editor's node-output preview workflow. */
|
|
14460
14429
|
async upsertStyleGroup(modelName, group, opts) {
|
|
@@ -14750,9 +14719,35 @@ export class Engine {
|
|
|
14750
14719
|
claimed.set(m, install.group.id);
|
|
14751
14720
|
return claimed;
|
|
14752
14721
|
}
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14722
|
+
graphImage(url) {
|
|
14723
|
+
let p = this.graphImageCache.get(url);
|
|
14724
|
+
if (!p) {
|
|
14725
|
+
p = fetch(url)
|
|
14726
|
+
.then((r) => (r.ok ? r.blob() : Promise.reject(new Error(`${r.status} ${url}`))))
|
|
14727
|
+
.then((b) => createImageBitmap(b, { colorSpaceConversion: "none", premultiplyAlpha: "none" }))
|
|
14728
|
+
.catch((e) => {
|
|
14729
|
+
console.warn(`[style] graph image: ${e.message}`);
|
|
14730
|
+
return null;
|
|
14731
|
+
});
|
|
14732
|
+
this.graphImageCache.set(url, p);
|
|
14733
|
+
}
|
|
14734
|
+
return p;
|
|
14735
|
+
}
|
|
14736
|
+
/** The group with its graph's images, where the group brings none of its own. */
|
|
14737
|
+
async withGraphImages(group) {
|
|
14738
|
+
const wanted = group.graph.images;
|
|
14739
|
+
if (!wanted?.length || group.images?.length)
|
|
14740
|
+
return group;
|
|
14741
|
+
const images = await Promise.all(wanted.slice(0, 4).map(async (w) => {
|
|
14742
|
+
if (!w)
|
|
14743
|
+
return null;
|
|
14744
|
+
const source = await this.graphImage(w.url);
|
|
14745
|
+
return source ? { source, srgb: w.srgb ?? false } : null;
|
|
14746
|
+
}));
|
|
14747
|
+
return { ...group, images };
|
|
14748
|
+
}
|
|
14749
|
+
async compileAndInstallGroup(inst, groupIn, opts) {
|
|
14750
|
+
const group = await this.withGraphImages(groupIn);
|
|
14756
14751
|
const renderClass = group.renderClass ?? "auto";
|
|
14757
14752
|
const alphaMode = group.alphaMode ?? "opaque";
|
|
14758
14753
|
// THE MAPS ARE PART OF THE SIGNATURE, because a matching one skips the
|
|
@@ -14780,9 +14775,35 @@ export class Engine {
|
|
|
14780
14775
|
const generation = (inst.styleGroupGen.get(group.id) ?? 0) + 1;
|
|
14781
14776
|
inst.styleGroupGen.set(group.id, generation);
|
|
14782
14777
|
this.device.pushErrorScope("validation");
|
|
14783
|
-
const module = this.
|
|
14784
|
-
const
|
|
14785
|
-
|
|
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]);
|
|
14786
14807
|
const diagnostics = [...result.diagnostics];
|
|
14787
14808
|
for (const msg of info.messages) {
|
|
14788
14809
|
if (msg.type !== "error")
|
|
@@ -14792,6 +14813,7 @@ export class Engine {
|
|
|
14792
14813
|
if (diagnostics.some((d) => d.severity === "error") || scopeError) {
|
|
14793
14814
|
if (scopeError && !diagnostics.some((d) => d.severity === "error"))
|
|
14794
14815
|
diagnostics.push({ severity: "error", message: `WGSL: ${scopeError.message}` });
|
|
14816
|
+
this.forgetShaderModule(result.wgsl);
|
|
14795
14817
|
return { ok: false, diagnostics, slotMap: result.slotMap };
|
|
14796
14818
|
}
|
|
14797
14819
|
let pipeline;
|
|
@@ -14799,17 +14821,8 @@ export class Engine {
|
|
|
14799
14821
|
let overEyesPipeline;
|
|
14800
14822
|
let mirrorPipeline;
|
|
14801
14823
|
try {
|
|
14802
|
-
|
|
14803
|
-
pipeline
|
|
14804
|
-
// The depth-write-off twin: stage transparency draws with it (see
|
|
14805
|
-
// pipelineForDrawCall), and a future OIT path would too.
|
|
14806
|
-
pipelineNoDepthWrite = await this.createRenderClassPipeline(renderClass, module, false, false, false, blend);
|
|
14807
|
-
if (renderClass === "hair")
|
|
14808
|
-
overEyesPipeline = await this.createRenderClassPipeline(renderClass, module, true, true, false, blend);
|
|
14809
|
-
// Only the eye needs one: every other class culls "none", which a flipped
|
|
14810
|
-
// winding leaves alone.
|
|
14811
|
-
if (renderClass === "eye")
|
|
14812
|
-
mirrorPipeline = await this.createRenderClassPipeline(renderClass, module, false, true, true, blend);
|
|
14824
|
+
;
|
|
14825
|
+
[pipeline, pipelineNoDepthWrite, overEyesPipeline, mirrorPipeline] = await variants;
|
|
14813
14826
|
}
|
|
14814
14827
|
catch (e) {
|
|
14815
14828
|
diagnostics.push({ severity: "error", message: `pipeline creation failed: ${e.message}` });
|
|
@@ -15030,8 +15043,9 @@ export class Engine {
|
|
|
15030
15043
|
fragment: { module, constants, targets: blend === "additive" ? this.sceneTargetsAdditive : this.sceneTargets },
|
|
15031
15044
|
depthStencil,
|
|
15032
15045
|
};
|
|
15033
|
-
return this.
|
|
15034
|
-
this.pipelineDescs.
|
|
15046
|
+
return this.cachedRenderPipeline(desc).then((pipeline) => {
|
|
15047
|
+
if (!this.pipelineDescs.has(pipeline))
|
|
15048
|
+
this.pipelineDescs.set(pipeline, desc);
|
|
15035
15049
|
return pipeline;
|
|
15036
15050
|
});
|
|
15037
15051
|
}
|
|
@@ -15042,6 +15056,151 @@ export class Engine {
|
|
|
15042
15056
|
this.pipelineDescs.set(pipeline, desc);
|
|
15043
15057
|
return pipeline;
|
|
15044
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
|
+
}
|
|
15045
15204
|
/**
|
|
15046
15205
|
* The pipeline a draw should use once its double-sided flag is honoured.
|
|
15047
15206
|
*
|
|
@@ -15061,17 +15220,15 @@ export class Engine {
|
|
|
15061
15220
|
const desc = this.pipelineDescs.get(pipeline);
|
|
15062
15221
|
if (!desc || (desc.primitive?.cullMode ?? "none") !== "none")
|
|
15063
15222
|
return pipeline;
|
|
15064
|
-
const
|
|
15065
|
-
|
|
15066
|
-
|
|
15067
|
-
|
|
15068
|
-
|
|
15069
|
-
|
|
15070
|
-
|
|
15071
|
-
|
|
15072
|
-
|
|
15073
|
-
}
|
|
15074
|
-
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;
|
|
15075
15232
|
}
|
|
15076
15233
|
// Pipeline for a material draw call: its group's compiled pipeline when grouped, else
|
|
15077
15234
|
// the neutral base (ungrouped materials render the default graph). Transparent-bucket
|
|
@@ -15143,6 +15300,10 @@ export class Engine {
|
|
|
15143
15300
|
pass.setPipeline(mirrored ? this.outlineMirrorPipeline : this.outlinePipeline);
|
|
15144
15301
|
pass.setBindGroup(0, mirrored ? this.outlineMirrorPerFrameBindGroup : this.outlinePerFrameBindGroup);
|
|
15145
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);
|
|
15146
15307
|
this.issueDraw(pass, draw, view.args);
|
|
15147
15308
|
pass.setBindGroup(0, view.perFrame);
|
|
15148
15309
|
currentPipeline = null;
|
|
@@ -15851,6 +16012,249 @@ export class Engine {
|
|
|
15851
16012
|
// The count rides in the anchor's spare lane, so rzTrailCount is one read.
|
|
15852
16013
|
cd[anchorBase + 11] = count;
|
|
15853
16014
|
}
|
|
16015
|
+
/**
|
|
16016
|
+
* Dress a model in the game's own materials — a look exported with ag-rip
|
|
16017
|
+
* (translated shaders, their images, per-material passes and values, and a
|
|
16018
|
+
* character rig) — or take it off with null. The materials it names are
|
|
16019
|
+
* drawn by the game's shaders from then on; the rest keep their graphs, and
|
|
16020
|
+
* every one still casts its shadow through the engine's pass.
|
|
16021
|
+
*/
|
|
16022
|
+
setModelNativeLook(name, look) {
|
|
16023
|
+
const inst = this.modelInstances.get(name);
|
|
16024
|
+
if (!inst || !this.device)
|
|
16025
|
+
return false;
|
|
16026
|
+
if (!look) {
|
|
16027
|
+
this.nativeLooks?.remove(name);
|
|
16028
|
+
this.writeCullHidden(true);
|
|
16029
|
+
return true;
|
|
16030
|
+
}
|
|
16031
|
+
if (!this.nativeLooks)
|
|
16032
|
+
this.nativeLooks = new NativeLooks(this.device, this.ensureNativeHosts());
|
|
16033
|
+
const skeleton = inst.model.getSkeleton();
|
|
16034
|
+
const head = skeleton.bones.findIndex((b) => b.name === "頭");
|
|
16035
|
+
// The head's rest position: a rigid inverse bind [R t] is the bind pose
|
|
16036
|
+
// inverted, so the bind position is −Rᵀt.
|
|
16037
|
+
let headRest = [0, 0, 0];
|
|
16038
|
+
if (head >= 0) {
|
|
16039
|
+
const m = skeleton.inverseBindMatrices.subarray(head * 16, head * 16 + 16);
|
|
16040
|
+
headRest = [
|
|
16041
|
+
-(m[0] * m[12] + m[1] * m[13] + m[2] * m[14]),
|
|
16042
|
+
-(m[4] * m[12] + m[5] * m[13] + m[6] * m[14]),
|
|
16043
|
+
-(m[8] * m[12] + m[9] * m[13] + m[10] * m[14]),
|
|
16044
|
+
];
|
|
16045
|
+
}
|
|
16046
|
+
const draws = inst.drawCalls
|
|
16047
|
+
.filter((dc) => dc.baseBindGroupEntries)
|
|
16048
|
+
.map((dc) => ({
|
|
16049
|
+
materialName: dc.materialName,
|
|
16050
|
+
firstIndex: dc.firstIndex,
|
|
16051
|
+
count: dc.count,
|
|
16052
|
+
diffuse: dc.baseBindGroupEntries.find((e) => e.binding === 0).resource,
|
|
16053
|
+
}));
|
|
16054
|
+
this.nativeLooks.install({
|
|
16055
|
+
name,
|
|
16056
|
+
vertices: inst.model.getVertices(),
|
|
16057
|
+
indices: inst.model.getIndices(),
|
|
16058
|
+
vertexBuffer: inst.vertexBuffer,
|
|
16059
|
+
jointsBuffer: inst.jointsBuffer,
|
|
16060
|
+
weightsBuffer: inst.weightsBuffer,
|
|
16061
|
+
skinMatrixBuffer: inst.skinMatrixBuffer,
|
|
16062
|
+
indexBuffer: inst.indexBuffer,
|
|
16063
|
+
draws,
|
|
16064
|
+
hidden: (m) => inst.hiddenMaterials.has(m) || inst.morphHiddenMaterials.has(m),
|
|
16065
|
+
headSkin: () => (head >= 0 ? inst.model.getSkinMatrices().subarray(head * 16, head * 16 + 16) : null),
|
|
16066
|
+
headRest,
|
|
16067
|
+
layers: new Uint32Array(inst.objectLight.buffer)[0],
|
|
16068
|
+
}, look);
|
|
16069
|
+
this.writeCullHidden(true);
|
|
16070
|
+
return true;
|
|
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
|
+
}
|
|
16078
|
+
/** The hosts for the game's shaders: the scene pass's and the shadow atlas's,
|
|
16079
|
+
* with the stand-ins an empty slot reads. Built on first use. */
|
|
16080
|
+
ensureNativeHosts() {
|
|
16081
|
+
if (this.nativeHost)
|
|
16082
|
+
return this.nativeHost;
|
|
16083
|
+
const d = this.device;
|
|
16084
|
+
const noShadow = d.createTexture({
|
|
16085
|
+
label: "native: no shadow",
|
|
16086
|
+
size: [1, 1],
|
|
16087
|
+
format: "depth32float",
|
|
16088
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
16089
|
+
});
|
|
16090
|
+
const enc = d.createCommandEncoder();
|
|
16091
|
+
enc
|
|
16092
|
+
.beginRenderPass({
|
|
16093
|
+
colorAttachments: [],
|
|
16094
|
+
depthStencilAttachment: { view: noShadow.createView(), depthClearValue: 1, depthLoadOp: "clear", depthStoreOp: "store" },
|
|
16095
|
+
})
|
|
16096
|
+
.end();
|
|
16097
|
+
d.queue.submit([enc.finish()]);
|
|
16098
|
+
this.nativeNoShadowView = noShadow.createView();
|
|
16099
|
+
const solid = (label, rgba, cube = false) => {
|
|
16100
|
+
const t = d.createTexture({
|
|
16101
|
+
label,
|
|
16102
|
+
size: [1, 1, cube ? 6 : 1],
|
|
16103
|
+
format: "rgba8unorm",
|
|
16104
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
16105
|
+
});
|
|
16106
|
+
for (let l = 0; l < (cube ? 6 : 1); l++)
|
|
16107
|
+
d.queue.writeTexture({ texture: t, origin: [0, 0, l] }, new Uint8Array(rgba), { bytesPerRow: 4 }, [1, 1]);
|
|
16108
|
+
return t.createView(cube ? { dimension: "cube" } : undefined);
|
|
16109
|
+
};
|
|
16110
|
+
const white = solid("native: white", [255, 255, 255, 255]);
|
|
16111
|
+
const black = solid("native: black", [0, 0, 0, 255]);
|
|
16112
|
+
// Unity's stand-ins for an empty slot, by the name a shader declares.
|
|
16113
|
+
this.nativeFallback = {
|
|
16114
|
+
white,
|
|
16115
|
+
black,
|
|
16116
|
+
"": white,
|
|
16117
|
+
grey: solid("native: grey", [128, 128, 128, 255]),
|
|
16118
|
+
gray: solid("native: gray", [128, 128, 128, 255]),
|
|
16119
|
+
bump: solid("native: bump", [128, 128, 255, 255]),
|
|
16120
|
+
red: solid("native: red", [255, 0, 0, 255]),
|
|
16121
|
+
};
|
|
16122
|
+
const fallback = {
|
|
16123
|
+
white,
|
|
16124
|
+
black,
|
|
16125
|
+
blackCube: solid("native: black cube", [0, 0, 0, 255], true),
|
|
16126
|
+
depth: this.nativeNoShadowView,
|
|
16127
|
+
comparison: this.shadowComparisonSampler,
|
|
16128
|
+
};
|
|
16129
|
+
this.nativeHost = new NativeHost(d, {
|
|
16130
|
+
colorFormats: sceneColorFormats(this.sceneFormats),
|
|
16131
|
+
depthFormat: this.depthFormat,
|
|
16132
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
16133
|
+
reversedZ: this.reversedZ,
|
|
16134
|
+
}, fallback);
|
|
16135
|
+
this.nativeShadowHost = new NativeHost(d, { colorFormats: [], depthFormat: Engine.SHADOW_DEPTH_FORMAT, sampleCount: 1, reversedZ: false }, fallback);
|
|
16136
|
+
return this.nativeHost;
|
|
16137
|
+
}
|
|
16138
|
+
/**
|
|
16139
|
+
* Load a game stage as the game draws it - a package ag-rip's
|
|
16140
|
+
* stage_native.py exported (stage.json and the files beside it, read through
|
|
16141
|
+
* `read` by their paths in it) - or remove the one loaded with null. Its
|
|
16142
|
+
* scene settings (fog, tint, ambient, environment) become the game shaders'
|
|
16143
|
+
* for the whole scene; its lights are the caller's to set, as lights.
|
|
16144
|
+
*/
|
|
16145
|
+
async setNativeStage(pkg, read, onProgress) {
|
|
16146
|
+
const old = this.nativeStage;
|
|
16147
|
+
this.nativeStage = null;
|
|
16148
|
+
if (old)
|
|
16149
|
+
old.destroy([this.nativeHost, this.nativeShadowHost]);
|
|
16150
|
+
if (!pkg || !read || !this.device)
|
|
16151
|
+
return;
|
|
16152
|
+
const scene = this.ensureNativeHosts();
|
|
16153
|
+
this.nativeStage = await NativeStage.load(this.device, pkg, read, { scene, shadow: this.nativeShadowHost }, { mipmaps: (t, levels) => this.generateMipmaps(t, levels), fallback: this.nativeFallback }, onProgress);
|
|
16154
|
+
}
|
|
16155
|
+
/** What went wrong in the game's shaders, and the names nothing supplied. */
|
|
16156
|
+
nativeReport() {
|
|
16157
|
+
const errors = [...(this.nativeHost?.errors ?? []), ...(this.nativeShadowHost?.errors ?? [])];
|
|
16158
|
+
const missing = {};
|
|
16159
|
+
for (const h of [this.nativeHost, this.nativeShadowHost])
|
|
16160
|
+
for (const [k, v] of h?.missing ?? [])
|
|
16161
|
+
missing[k] = (missing[k] ?? 0) + v;
|
|
16162
|
+
return { errors, missing };
|
|
16163
|
+
}
|
|
16164
|
+
nativeFrameTextures() {
|
|
16165
|
+
return { _MainLightShadowmapTexture: this.shadowAtlasView, sim_CharacterShadowmap: this.nativeNoShadowView ?? undefined };
|
|
16166
|
+
}
|
|
16167
|
+
eyeArray() {
|
|
16168
|
+
const e = this.camera.getEyePosition();
|
|
16169
|
+
return [e.x, e.y, e.z];
|
|
16170
|
+
}
|
|
16171
|
+
/** Before the scene pass: the dressed models' skinning and the game's globals. */
|
|
16172
|
+
prepareNativeLooks(encoder) {
|
|
16173
|
+
const looks = this.nativeLooks;
|
|
16174
|
+
const view = this.camera.getViewMatrix().values;
|
|
16175
|
+
const proj = this.camera.getProjectionMatrix().values;
|
|
16176
|
+
const eye = this.camera.getEyePosition();
|
|
16177
|
+
const sun = this.sun;
|
|
16178
|
+
const dir = sun.direction.normalize();
|
|
16179
|
+
const k = sun.strength;
|
|
16180
|
+
// The document's lamps as their records store them.
|
|
16181
|
+
const lights = [];
|
|
16182
|
+
for (let i = 0; i < this.docLightCount; i++) {
|
|
16183
|
+
const b = LIGHT_HEADER + i * LIGHT_STRIDE;
|
|
16184
|
+
const d = this.lightsData;
|
|
16185
|
+
lights.push({
|
|
16186
|
+
position: [d[b], d[b + 1], d[b + 2]],
|
|
16187
|
+
radius: d[b + 3],
|
|
16188
|
+
color: [d[b + 4], d[b + 5], d[b + 6]],
|
|
16189
|
+
aim: [d[b + 8], d[b + 9], d[b + 10]],
|
|
16190
|
+
cosOuter: d[b + 11],
|
|
16191
|
+
cosInner: d[b + 12],
|
|
16192
|
+
layers: ~this.lightsWords[b + 13] >>> 0,
|
|
16193
|
+
});
|
|
16194
|
+
}
|
|
16195
|
+
// A game stage brings its own lights: the game's shaders get exactly those
|
|
16196
|
+
// (its sun, its lamps, all layers), whatever the scene's lamps are.
|
|
16197
|
+
const stage = this.nativeStage;
|
|
16198
|
+
let sunDirection = [dir.x, dir.y, dir.z];
|
|
16199
|
+
let sunColor = [sun.color.x * k, sun.color.y * k, sun.color.z * k];
|
|
16200
|
+
let sunShadow = this.sunShadow;
|
|
16201
|
+
if (stage) {
|
|
16202
|
+
// Its lamps are the stage's own; its key is the scene's sun, which the
|
|
16203
|
+
// host sets from the stage (and a person may then move).
|
|
16204
|
+
const L = stage.pkg.lights;
|
|
16205
|
+
const sc = stage.pkg.scale;
|
|
16206
|
+
lights.length = 0;
|
|
16207
|
+
for (const l of L.additional) {
|
|
16208
|
+
lights.push({
|
|
16209
|
+
position: [-l.position[0] * sc, l.position[1] * sc, -l.position[2] * sc],
|
|
16210
|
+
radius: l.range * sc,
|
|
16211
|
+
color: [l.color[0] * sc * sc, l.color[1] * sc * sc, l.color[2] * sc * sc],
|
|
16212
|
+
aim: (l.aim ? gameDir(l.aim) : [0, 0, 0]),
|
|
16213
|
+
cosOuter: l.cosOuter ?? -1,
|
|
16214
|
+
cosInner: l.cosInner ?? -1,
|
|
16215
|
+
layers: 0xffffffff,
|
|
16216
|
+
unity: l.unity,
|
|
16217
|
+
});
|
|
16218
|
+
}
|
|
16219
|
+
}
|
|
16220
|
+
const s = this.world.strength;
|
|
16221
|
+
const sh = this.worldAmbientSH ?? this.worldSH ?? this.worldGradientSH;
|
|
16222
|
+
const g = unityFrameGlobals({
|
|
16223
|
+
scale: 8,
|
|
16224
|
+
view,
|
|
16225
|
+
proj,
|
|
16226
|
+
eye: [eye.x, eye.y, eye.z],
|
|
16227
|
+
near: this.camera.near,
|
|
16228
|
+
far: this.camera.far,
|
|
16229
|
+
width: this.canvas.width,
|
|
16230
|
+
height: this.canvas.height,
|
|
16231
|
+
time: this.sceneClock,
|
|
16232
|
+
dt: 1 / 60,
|
|
16233
|
+
sunDirection,
|
|
16234
|
+
sunColor,
|
|
16235
|
+
sunShadow,
|
|
16236
|
+
lights,
|
|
16237
|
+
ambientSH: sh ? Array.from(sh, (v) => v * s) : null,
|
|
16238
|
+
ambientFlat: [this.world.color.x * s, this.world.color.y * s, this.world.color.z * s],
|
|
16239
|
+
shadow: {
|
|
16240
|
+
viewProj: SHADOW_CASCADES.map((_, i) => this.shadowLightVPMatrix.subarray(i * 16, i * 16 + 16)),
|
|
16241
|
+
tiles: SHADOW_CASCADES.map((c) => [c.origin[0], c.origin[1]]),
|
|
16242
|
+
tileSize: SHADOW_CASCADES[0].mapSize,
|
|
16243
|
+
atlasSize: SHADOW_ATLAS_SIZE,
|
|
16244
|
+
spheres: cascadeSpheres(this.shadowView, this.shadowSceneBounds).map((p) => ({
|
|
16245
|
+
center: [p.center.x, p.center.y, p.center.z],
|
|
16246
|
+
radius: p.radius,
|
|
16247
|
+
})),
|
|
16248
|
+
},
|
|
16249
|
+
settings: this.nativeStage?.pkg.settings ?? null,
|
|
16250
|
+
});
|
|
16251
|
+
g.sim_TowardMatrix = towardMatrix(gameDir([view[2], view[6], view[10]]));
|
|
16252
|
+
g._OutlineMaxOffsetMultiplier = outlineMaxOffsetMultiplier(this.canvas.width);
|
|
16253
|
+
this.nativeGlobals = stableGlobals(this.nativeGlobals, g);
|
|
16254
|
+
this.nativeHost?.beginFrame();
|
|
16255
|
+
this.nativeShadowHost?.beginFrame();
|
|
16256
|
+
looks?.prepare(encoder, this.nativeGlobals, 8, this.nativeFrameTextures());
|
|
16257
|
+
}
|
|
15854
16258
|
updateSkinMatrices() {
|
|
15855
16259
|
this.forEachInstance((inst) => {
|
|
15856
16260
|
// Only a pose pass can change these, and an idle stage did not run one —
|
|
@@ -16105,13 +16509,21 @@ Engine.TIMED_PASSES = [
|
|
|
16105
16509
|
// because two planes would need two of every attachment below. Which one is
|
|
16106
16510
|
// in force is mirrorPlane, refreshed by updateMirrorCamera.
|
|
16107
16511
|
Engine.GROUND_PLANE = [0, 1, 0, 0];
|
|
16108
|
-
|
|
16109
|
-
|
|
16110
|
-
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;
|
|
16111
16516
|
// Smoothness metrics are computed over a ring buffer of true frame intervals
|
|
16112
16517
|
// (vsync-to-vsync), recomputed at STATS_REFRESH_MS so the readout doesn't flicker.
|
|
16113
16518
|
Engine.STATS_WINDOW = 120;
|
|
16114
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
|
+
};
|
|
16115
16527
|
/**
|
|
16116
16528
|
* The dials a `#mirror` effect is READ BY — the contract between an effect
|
|
16117
16529
|
* file and the reflection.
|
|
@@ -16175,3 +16587,46 @@ Engine.CULL_MODEL_RIGID = 2;
|
|
|
16175
16587
|
/** A cache key no material can collide with — a PMX name is never empty and
|
|
16176
16588
|
* never contains a NUL. */
|
|
16177
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;
|
|
16601
|
+
/**
|
|
16602
|
+
* This frame's Unity globals, keeping last frame's object for every value that
|
|
16603
|
+
* did not change: the native host refills a uniform member only when its value
|
|
16604
|
+
* object is new, so a light table or an SH that holds still costs nothing.
|
|
16605
|
+
*/
|
|
16606
|
+
function stableGlobals(prev, next) {
|
|
16607
|
+
for (const k in next) {
|
|
16608
|
+
const a = prev[k];
|
|
16609
|
+
if (a !== undefined && sameValue(a, next[k]))
|
|
16610
|
+
next[k] = a;
|
|
16611
|
+
}
|
|
16612
|
+
return next;
|
|
16613
|
+
}
|
|
16614
|
+
function sameValue(a, b) {
|
|
16615
|
+
if (typeof a === "number" || typeof b === "number")
|
|
16616
|
+
return a === b;
|
|
16617
|
+
if (a.length !== b.length)
|
|
16618
|
+
return false;
|
|
16619
|
+
if (a instanceof Uint32Array !== b instanceof Uint32Array)
|
|
16620
|
+
return false;
|
|
16621
|
+
for (let i = 0; i < a.length; i++) {
|
|
16622
|
+
const x = a[i];
|
|
16623
|
+
const y = b[i];
|
|
16624
|
+
if (typeof x === "number" || typeof y === "number") {
|
|
16625
|
+
if (x !== y)
|
|
16626
|
+
return false;
|
|
16627
|
+
}
|
|
16628
|
+
else if (!sameValue(x, y))
|
|
16629
|
+
return false;
|
|
16630
|
+
}
|
|
16631
|
+
return true;
|
|
16632
|
+
}
|