reze-engine 0.64.0 → 0.66.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 +300 -94
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1628 -787
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/compile.js +5 -0
- package/dist/graph/presets/body.d.ts.map +1 -1
- package/dist/graph/presets/body.js +26 -24
- package/dist/graph/presets/cloth_rough.js +8 -8
- package/dist/graph/presets/cloth_smooth.js +8 -8
- package/dist/graph/presets/default.js +4 -4
- package/dist/graph/presets/eye.js +10 -10
- package/dist/graph/presets/face.d.ts.map +1 -1
- package/dist/graph/presets/face.js +28 -26
- package/dist/graph/presets/hair.js +6 -6
- package/dist/graph/presets/metal.js +8 -8
- package/dist/graph/presets/stockings.d.ts.map +1 -1
- package/dist/graph/presets/stockings.js +4 -11
- package/dist/graph/presets/unlit.d.ts.map +1 -1
- package/dist/graph/presets/unlit.js +9 -13
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +128 -157
- package/dist/graph/render-class.d.ts +5 -2
- package/dist/graph/render-class.d.ts.map +1 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +12 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/model.d.ts +23 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +41 -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 +7 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +17 -3
- package/dist/shaders/anchor-table.d.ts +11 -0
- package/dist/shaders/anchor-table.d.ts.map +1 -1
- package/dist/shaders/anchor-table.js +9 -3
- package/dist/shaders/directives.d.ts +42 -5
- package/dist/shaders/directives.d.ts.map +1 -1
- package/dist/shaders/directives.js +51 -4
- package/dist/shaders/lights.d.ts +11 -7
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +315 -271
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +30 -5
- 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 +191 -271
- 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 +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +121 -77
- package/dist/shaders/passes/ground.js +1 -1
- 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.map +1 -1
- package/dist/shaders/passes/particles.js +9 -3
- package/dist/shaders/passes/probe.d.ts +45 -0
- package/dist/shaders/passes/probe.d.ts.map +1 -0
- package/dist/shaders/passes/probe.js +230 -0
- package/dist/shaders/passes/scene-contract.d.ts +13 -0
- package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
- package/dist/shaders/passes/scene-contract.js +4 -0
- package/dist/shaders/scene-light-api.d.ts.map +1 -1
- package/dist/shaders/scene-light-api.js +1 -0
- package/dist/unity/globals.d.ts +7 -0
- package/dist/unity/globals.d.ts.map +1 -1
- package/dist/unity/globals.js +4 -3
- package/dist/unity/host.d.ts +18 -0
- package/dist/unity/host.d.ts.map +1 -1
- package/dist/unity/host.js +144 -70
- package/dist/unity/looks.d.ts +47 -0
- package/dist/unity/looks.d.ts.map +1 -1
- package/dist/unity/looks.js +150 -6
- package/dist/unity/skin.d.ts.map +1 -1
- package/dist/unity/skin.js +5 -2
- package/dist/unity/stage.d.ts +24 -0
- package/dist/unity/stage.d.ts.map +1 -1
- package/dist/unity/stage.js +59 -7
- package/package.json +1 -1
- package/src/engine.ts +2010 -1033
- package/src/graph/compile.ts +4 -0
- package/src/graph/presets/body.ts +26 -24
- package/src/graph/presets/cloth_rough.ts +8 -8
- package/src/graph/presets/cloth_smooth.ts +8 -8
- package/src/graph/presets/default.ts +4 -4
- package/src/graph/presets/eye.ts +10 -10
- package/src/graph/presets/face.ts +28 -26
- package/src/graph/presets/hair.ts +6 -6
- package/src/graph/presets/metal.ts +8 -8
- package/src/graph/presets/stockings.ts +4 -11
- package/src/graph/presets/unlit.ts +9 -13
- package/src/graph/registry.ts +133 -158
- package/src/graph/render-class.ts +5 -2
- package/src/graph/slots.ts +12 -2
- package/src/index.ts +1 -0
- package/src/model.ts +50 -1
- package/src/outline-normals.ts +110 -0
- package/src/pmx-loader.ts +18 -3
- package/src/shaders/anchor-table.ts +15 -3
- package/src/shaders/directives.ts +88 -6
- package/src/shaders/lights.ts +464 -415
- package/src/shaders/materials/common.ts +30 -5
- package/src/shaders/materials/nodes.ts +191 -271
- package/src/shaders/passes/bloom.ts +59 -87
- package/src/shaders/passes/composite.ts +121 -77
- package/src/shaders/passes/ground.ts +1 -1
- package/src/shaders/passes/mirror.ts +60 -13
- package/src/shaders/passes/outline.ts +71 -14
- package/src/shaders/passes/particles.ts +9 -3
- package/src/shaders/passes/probe.ts +240 -0
- package/src/shaders/passes/scene-contract.ts +17 -0
- package/src/shaders/scene-light-api.ts +1 -0
- package/src/unity/globals.ts +7 -3
- package/src/unity/host.ts +93 -20
- package/src/unity/looks.ts +182 -9
- package/src/unity/skin.ts +5 -2
- package/src/unity/stage.ts +66 -7
- package/dist/physics-debug.d.ts +0 -30
- package/dist/physics-debug.d.ts.map +0 -1
- package/dist/physics-debug.js +0 -526
- package/dist/shaders/agx-lut.d.ts +0 -11
- package/dist/shaders/agx-lut.d.ts.map +0 -1
- package/dist/shaders/agx-lut.js +0 -29
- package/dist/shaders/dfg_lut.d.ts +0 -3
- package/dist/shaders/dfg_lut.d.ts.map +0 -1
- package/dist/shaders/dfg_lut.js +0 -129
- package/dist/shaders/ltc_mag_lut.d.ts +0 -3
- package/dist/shaders/ltc_mag_lut.d.ts.map +0 -1
- package/dist/shaders/ltc_mag_lut.js +0 -1033
- package/dist/shaders/materials/body.d.ts +0 -2
- package/dist/shaders/materials/body.d.ts.map +0 -1
- package/dist/shaders/materials/body.js +0 -95
- package/dist/shaders/materials/cloth_rough.d.ts +0 -2
- package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_rough.js +0 -69
- package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
- package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_smooth.js +0 -61
- package/dist/shaders/materials/default.d.ts +0 -2
- package/dist/shaders/materials/default.d.ts.map +0 -1
- package/dist/shaders/materials/default.js +0 -43
- package/dist/shaders/materials/eye.d.ts +0 -2
- package/dist/shaders/materials/eye.d.ts.map +0 -1
- package/dist/shaders/materials/eye.js +0 -60
- package/dist/shaders/materials/face.d.ts +0 -2
- package/dist/shaders/materials/face.d.ts.map +0 -1
- package/dist/shaders/materials/face.js +0 -95
- package/dist/shaders/materials/hair.d.ts +0 -2
- package/dist/shaders/materials/hair.d.ts.map +0 -1
- package/dist/shaders/materials/hair.js +0 -90
- package/dist/shaders/materials/metal.d.ts +0 -2
- package/dist/shaders/materials/metal.d.ts.map +0 -1
- package/dist/shaders/materials/metal.js +0 -77
- package/dist/shaders/materials/mmd_classic.d.ts +0 -2
- package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
- package/dist/shaders/materials/mmd_classic.js +0 -66
- package/dist/shaders/materials/stockings.d.ts +0 -2
- package/dist/shaders/materials/stockings.d.ts.map +0 -1
- package/dist/shaders/materials/stockings.js +0 -122
- package/dist/shaders/passes/physics-debug.d.ts +0 -2
- package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
- package/dist/shaders/passes/physics-debug.js +0 -69
- package/src/shaders/agx-lut.ts +0 -30
- package/src/shaders/dfg_lut.ts +0 -131
- package/src/shaders/ltc_mag_lut.ts +0 -1035
package/dist/engine.js
CHANGED
|
@@ -15,8 +15,6 @@ import { CameraAnimation } from "./camera-animation";
|
|
|
15
15
|
import { PmxLoader } from "./pmx-loader";
|
|
16
16
|
import { RezePhysics } from "./physics";
|
|
17
17
|
import { createFetchAssetReader, createFileMapAssetReader, deriveBasePathFromPmxPath, fileListToMap, findFirstPmxFileInList, joinAssetPath, normalizeAssetPath, } from "./asset-reader";
|
|
18
|
-
import { BRDF_LUT_SIZE, BRDF_LUT_BAKE_WGSL } from "./shaders/dfg_lut";
|
|
19
|
-
import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut";
|
|
20
18
|
import { SHADOW_DEPTH_SHADER_WGSL } from "./shaders/passes/shadow";
|
|
21
19
|
import { ID_DEBUG_SHADER_WGSL } from "./shaders/passes/id-debug";
|
|
22
20
|
import { paramChanged, sampleParamTrack } from "./param-track";
|
|
@@ -27,9 +25,10 @@ import { NativeLooks } from "./unity/looks";
|
|
|
27
25
|
import { NativeStage } from "./unity/stage";
|
|
28
26
|
import { gameDir, unityFrameGlobals } from "./unity/globals";
|
|
29
27
|
import { outlineMaxOffsetMultiplier, towardMatrix } from "./unity/character";
|
|
28
|
+
import { sameValue } from "./unity/host";
|
|
30
29
|
import { SHADOW_ATLAS_SIZE, SHADOW_CASCADES, buildShadowCascades, cascadeSpheres } from "./shadow-cascades";
|
|
31
30
|
import { REFLECTION_DEBUG_WGSL, buildMirrorCamera, planeFromPointNormal } from "./reflection";
|
|
32
|
-
import { MIRROR_MASK_DOWNSAMPLE_WGSL, MIRROR_MAT_BYTES, mirrorShaderWgsl, mirrorShadowWgsl } from "./shaders/passes/mirror";
|
|
31
|
+
import { MIRROR_DOWNSAMPLE_WGSL, MIRROR_MASK_DOWNSAMPLE_WGSL, MIRROR_MAT_BYTES, mirrorShaderWgsl, mirrorShadowWgsl, } from "./shaders/passes/mirror";
|
|
33
32
|
import { packHalf } from "./hdr";
|
|
34
33
|
import { evalIrradianceSH, projectIrradianceSH, gradientIrradianceSH } from "./ibl";
|
|
35
34
|
import { LYRIC_ATLAS_MAX_H, LYRIC_ATLAS_MAX_W, LYRICS_FLOATS, packLyrics } from "./shaders/lyrics-api";
|
|
@@ -44,8 +43,7 @@ import { GIZMO_SHADER_WGSL } from "./shaders/passes/gizmo";
|
|
|
44
43
|
import { OVERLAY_SHADER_WGSL, OVERLAY_COMPOSITE_SHADER_WGSL } from "./shaders/passes/overlay";
|
|
45
44
|
import { WIREFRAME_SHADER_WGSL } from "./shaders/passes/wireframe";
|
|
46
45
|
import { boneOverlay, boneMarkerPositions, buildOverlayShapes, jointOverlay, lineBetween, rigidbodyOverlay, writeOverlayInstance, OVERLAY_INSTANCE_FLOATS, OVERLAY_VERTEX_FLOATS, OVERLAY_SHAPES, OVERLAY_SOLID_SHAPES, DEFAULT_VERTEX_COLOR, OVERLAY_STYLE, } from "./overlay";
|
|
47
|
-
import {
|
|
48
|
-
import { AGX_LUT_GZ, AGX_LUT_SIZE } from "./shaders/agx-lut";
|
|
46
|
+
import { BLOOM_BLUR_H_SHADER_WGSL, BLOOM_BLUR_V_SHADER_WGSL, BLOOM_PREFILTER_SHADER_WGSL, BLOOM_UPSAMPLE_SHADER_WGSL, } from "./shaders/passes/bloom";
|
|
49
47
|
import { buildCompositeShader, EFFECT_SCENE_API, buildFieldShader, EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES, } from "./shaders/passes/composite";
|
|
50
48
|
import { SUBSURFACE_WGSL, SSS_SPIKE } from "./shaders/passes/subsurface";
|
|
51
49
|
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";
|
|
@@ -56,6 +54,7 @@ import { buildCastResolveShader, buildCastSeedShader, buildCastStepShader, castD
|
|
|
56
54
|
import { buildTrailShader, trailEntryPoints, TRAIL_SUBDIVISIONS } from "./shaders/passes/trails";
|
|
57
55
|
import { PICK_SHADER_WGSL } from "./shaders/passes/pick";
|
|
58
56
|
import { MIPMAP_BLIT_SHADER_WGSL } from "./shaders/passes/mipmap";
|
|
57
|
+
import { PROBE_CAPTURE_LEVELS, PROBE_CAPTURE_SIZE, PROBE_COMPOSE_WGSL, PROBE_FILTER_WGSL, PROBE_LEVELS, PROBE_SIZE, SKY_PREFILTER_WGSL, probeLevelRoughness, skyLevelRoughness, } from "./shaders/passes/probe";
|
|
59
58
|
import { compileGraph } from "./graph/compile";
|
|
60
59
|
import { DEFAULT_GRAPH } from "./graph/presets/default";
|
|
61
60
|
import { parseDirectives, stripDirectives } from "./shaders/directives";
|
|
@@ -228,12 +227,10 @@ const CAST_TRAIL_BASE = EFFECT_TRAIL_BASE;
|
|
|
228
227
|
const CAST_VEC4S = CAST_TRAIL_BASE + MAX_EFFECT_ANCHORS * MAX_EFFECT_SUBJECTS * TRAIL_SAMPLES;
|
|
229
228
|
export const DEFAULT_BLOOM_OPTIONS = {
|
|
230
229
|
enabled: true,
|
|
231
|
-
threshold: 0.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
intensity: 0.05,
|
|
236
|
-
clamp: 0.0,
|
|
230
|
+
threshold: 0.7,
|
|
231
|
+
scatter: 0.8,
|
|
232
|
+
color: new Vec3(1, 1, 1),
|
|
233
|
+
intensity: 1,
|
|
237
234
|
};
|
|
238
235
|
export const DEFAULT_DEPTH_OF_FIELD_OPTIONS = {
|
|
239
236
|
enabled: false,
|
|
@@ -245,12 +242,10 @@ export const DEFAULT_DEPTH_OF_FIELD_OPTIONS = {
|
|
|
245
242
|
bladeCount: 6,
|
|
246
243
|
quality: "balanced",
|
|
247
244
|
};
|
|
248
|
-
// Matches the reference Blender project: Filmic view, Medium High Contrast look,
|
|
249
|
-
// exposure 0.3, gamma 1.0, sRGB display, no curves.
|
|
250
245
|
export const DEFAULT_VIEW_TRANSFORM = {
|
|
251
|
-
exposure: 0
|
|
246
|
+
exposure: 0,
|
|
252
247
|
gamma: 1.0,
|
|
253
|
-
transform: "
|
|
248
|
+
transform: "soft",
|
|
254
249
|
};
|
|
255
250
|
const NEUTRAL_GRADE_CHANNEL = 0.5;
|
|
256
251
|
export const DEFAULT_COLOR_GRADING = {
|
|
@@ -262,7 +257,7 @@ export const DEFAULT_COLOR_GRADING = {
|
|
|
262
257
|
};
|
|
263
258
|
const DEFAULT_ENGINE_OPTIONS = {
|
|
264
259
|
world: { color: new Vec3(0.4014, 0.4944, 0.647), strength: 0.3 },
|
|
265
|
-
sun: { color: new Vec3(1.0, 1.0, 1.0), strength:
|
|
260
|
+
sun: { color: new Vec3(1.0, 1.0, 1.0), strength: 0.64, direction: new Vec3(-0.0873, -0.3844, 0.919) },
|
|
266
261
|
camera: { distance: 26.6, target: new Vec3(0, 12.5, 0), fov: Math.PI / 4 },
|
|
267
262
|
onRaycast: undefined,
|
|
268
263
|
};
|
|
@@ -984,6 +979,9 @@ export class Engine {
|
|
|
984
979
|
/** Where addGround put the floor. The mirror reflects across it, so a raised
|
|
985
980
|
* floor that kept y = 0 would reflect the scene into the wrong plane. */
|
|
986
981
|
this.groundY = 0;
|
|
982
|
+
/** The built-in floor's half size on x and z — what it covers, for the
|
|
983
|
+
* shadow's reach (updateShadowSceneBounds). */
|
|
984
|
+
this.groundHalf = [0, 0];
|
|
987
985
|
this.mirrorPlane = new Float32Array([0, 1, 0, 0]);
|
|
988
986
|
this.mirrorCameraData = new Float32Array(40);
|
|
989
987
|
// proj x mirrorView for the ground's projective sample and the cull planes,
|
|
@@ -1009,6 +1007,7 @@ export class Engine {
|
|
|
1009
1007
|
* premultiplied quantity and a blur has to move them together. */
|
|
1010
1008
|
this.mirrorMaskMipViews = [];
|
|
1011
1009
|
this.mirrorMaskBlurBindGroups = null;
|
|
1010
|
+
this.mirrorDownsamplePipeline = null;
|
|
1012
1011
|
this.mirrorMaskDownsamplePipeline = null;
|
|
1013
1012
|
this.groundClipData = new Float32Array(8);
|
|
1014
1013
|
this.mirrorIdMsTexture = null;
|
|
@@ -1100,6 +1099,19 @@ export class Engine {
|
|
|
1100
1099
|
/** A diffuse ambient stated outright (setWorldAmbient), outranking the one
|
|
1101
1100
|
* fitted to the picture; the picture still answers reflections. */
|
|
1102
1101
|
this.worldAmbientSH = null;
|
|
1102
|
+
this.cookieLayers = new Map();
|
|
1103
|
+
/** What setLights was last given, rewritten when a cookie it names arrives. */
|
|
1104
|
+
this.sceneLights = null;
|
|
1105
|
+
this.probeData = new Float32Array(12);
|
|
1106
|
+
this.probeCameraData = new Float32Array(40);
|
|
1107
|
+
this.probeCaptureBindGroup = null;
|
|
1108
|
+
/** Set when the stage or what it wears changes; the capture runs once the
|
|
1109
|
+
* scene has stayed put for PROBE_SETTLE_FRAMES (see maybeCaptureProbe). */
|
|
1110
|
+
this.probeDirty = false;
|
|
1111
|
+
this.probeDirtyFrame = 0;
|
|
1112
|
+
this.probeFrame = 0;
|
|
1113
|
+
this.probeCapture = null;
|
|
1114
|
+
this.skyPrefilterPipeline = null;
|
|
1103
1115
|
// The scene's user WGSL effect (setEffect). ONE per scene, mounted under the
|
|
1104
1116
|
// scene, over it, or both — whichever of background()/foreground() the code
|
|
1105
1117
|
// defines. The composite pipelines are REBUILT with the user code injected;
|
|
@@ -1173,7 +1185,6 @@ export class Engine {
|
|
|
1173
1185
|
/** Trail samples owed this frame, computed once so every trail on every
|
|
1174
1186
|
* character samples in lockstep and their paths stay comparable. */
|
|
1175
1187
|
this.trailDue = 0;
|
|
1176
|
-
this.agxLutTexture = null;
|
|
1177
1188
|
/** The scene's own grade (setStageGrade): the cube as handed over, kept so a
|
|
1178
1189
|
* call before init() still lands, and its texture once uploaded. */
|
|
1179
1190
|
this.stageGradeCube = null;
|
|
@@ -1198,18 +1209,42 @@ export class Engine {
|
|
|
1198
1209
|
this.pipelineDescs = new WeakMap();
|
|
1199
1210
|
this.singleSided = new WeakMap();
|
|
1200
1211
|
this.singleSidedMirror = new WeakMap();
|
|
1212
|
+
/** Twins whose async build is in flight, so a draw asks for each once. */
|
|
1213
|
+
this.sidedPending = new WeakSet();
|
|
1214
|
+
this.sidedPendingMirror = new WeakSet();
|
|
1215
|
+
// The engine-wide shader cache (see cachedRenderPipeline). Modules by WGSL
|
|
1216
|
+
// source, pipelines by module + descriptor state: one compile per source,
|
|
1217
|
+
// whoever asks — a graph on two models, an effect reinstalled, the composite.
|
|
1218
|
+
this.shaderModuleCache = new Map();
|
|
1219
|
+
this.pipelineCache = new Map();
|
|
1220
|
+
this.bindGroupLayoutCache = new Map();
|
|
1221
|
+
this.pipelineLayoutCache = new Map();
|
|
1222
|
+
this.gpuObjectIds = new WeakMap();
|
|
1223
|
+
this.gpuObjectNext = 1;
|
|
1224
|
+
/** Init's pipelines, built async and in parallel; init awaits them. Null
|
|
1225
|
+
* outside init, where a pipeline job builds the way it always did. */
|
|
1226
|
+
this.initPipelineJobs = null;
|
|
1201
1227
|
// Stepped motion (#stepped). The models whose pose is being HELD this frame —
|
|
1202
1228
|
// their skinning and morph uploads wait for the stepped clock's next tick —
|
|
1203
1229
|
// and the tick each was last shown at. Empty while no effect declares it.
|
|
1204
1230
|
this.steppedHeld = new Set();
|
|
1205
1231
|
this.steppedTick = new Map();
|
|
1206
1232
|
this.steppedLastClock = -1;
|
|
1207
|
-
this.
|
|
1208
|
-
this.
|
|
1209
|
-
this.
|
|
1210
|
-
this.
|
|
1211
|
-
this.
|
|
1212
|
-
this.
|
|
1233
|
+
this.bloomPrefilterPipeline = null;
|
|
1234
|
+
this.bloomBlurHPipeline = null;
|
|
1235
|
+
this.bloomBlurVPipeline = null;
|
|
1236
|
+
this.bloomUpsamplePipeline = null;
|
|
1237
|
+
this.bloomUpsampleBindGroupLayout = null;
|
|
1238
|
+
this.bloomUniformBuffer = null;
|
|
1239
|
+
this.bloomUniformData = new Float32Array(4);
|
|
1240
|
+
this.bloomDownTexture = null;
|
|
1241
|
+
this.bloomUpTexture = null;
|
|
1242
|
+
this.bloomLevels = 0;
|
|
1243
|
+
this.bloomDownViews = [];
|
|
1244
|
+
this.bloomUpViews = [];
|
|
1245
|
+
this.bloomPrefilterBindGroup = null;
|
|
1246
|
+
this.bloomBlurHBindGroups = [];
|
|
1247
|
+
this.bloomBlurVBindGroups = [];
|
|
1213
1248
|
this.bloomUpsampleBindGroups = [];
|
|
1214
1249
|
this.hasGround = false;
|
|
1215
1250
|
/** The user's own "no ground" switch — see setGroundVisible. Distinct from
|
|
@@ -1289,8 +1324,8 @@ export class Engine {
|
|
|
1289
1324
|
contrast: DEFAULT_COLOR_GRADING.contrast,
|
|
1290
1325
|
saturation: DEFAULT_COLOR_GRADING.saturation,
|
|
1291
1326
|
};
|
|
1292
|
-
/** Sensor grain: how much
|
|
1293
|
-
this.grain = { amount: 0
|
|
1327
|
+
/** Sensor grain: how much. */
|
|
1328
|
+
this.grain = { amount: 0 };
|
|
1294
1329
|
/** Debug/diagnostic: skip every inverted-hull outline draw. */
|
|
1295
1330
|
// OFF by default — the product aesthetic. Modern high-detail models read
|
|
1296
1331
|
// better without hulls (babylon-mmd's own demos disable its outline renderer
|
|
@@ -1637,6 +1672,9 @@ export class Engine {
|
|
|
1637
1672
|
// install (pipeline + uniform buffer) when the graph/integration is byte-unchanged.
|
|
1638
1673
|
/** A graph's own images (ShaderGraph.images), fetched and decoded once per URL. */
|
|
1639
1674
|
this.graphImageCache = new Map();
|
|
1675
|
+
this.sortedDraws = new WeakMap();
|
|
1676
|
+
this.sortScratch = new Vec3(0, 0, 0);
|
|
1677
|
+
this.sortDistance = new Map();
|
|
1640
1678
|
this.canvas = canvas;
|
|
1641
1679
|
const d = DEFAULT_ENGINE_OPTIONS;
|
|
1642
1680
|
this.world = {
|
|
@@ -1668,18 +1706,16 @@ export class Engine {
|
|
|
1668
1706
|
const bg = options?.background;
|
|
1669
1707
|
this.backgroundColor = bg ? new Vec3(bg.x, bg.y, bg.z) : null;
|
|
1670
1708
|
}
|
|
1671
|
-
/** Merge partial bloom with
|
|
1709
|
+
/** Merge partial bloom with the defaults (same as constructor). */
|
|
1672
1710
|
static mergeBloomDefaults(partial) {
|
|
1673
1711
|
const d = DEFAULT_BLOOM_OPTIONS;
|
|
1674
1712
|
const c = partial?.color;
|
|
1675
1713
|
return {
|
|
1676
1714
|
enabled: partial?.enabled ?? d.enabled,
|
|
1677
1715
|
threshold: partial?.threshold ?? d.threshold,
|
|
1678
|
-
|
|
1679
|
-
radius: partial?.radius ?? d.radius,
|
|
1716
|
+
scatter: partial?.scatter ?? d.scatter,
|
|
1680
1717
|
color: c ? new Vec3(c.x, c.y, c.z) : new Vec3(d.color.x, d.color.y, d.color.z),
|
|
1681
1718
|
intensity: partial?.intensity ?? d.intensity,
|
|
1682
|
-
clamp: partial?.clamp ?? d.clamp,
|
|
1683
1719
|
};
|
|
1684
1720
|
}
|
|
1685
1721
|
static mergeViewTransformDefaults(partial) {
|
|
@@ -1690,17 +1726,15 @@ export class Engine {
|
|
|
1690
1726
|
transform: partial?.transform ?? d.transform,
|
|
1691
1727
|
};
|
|
1692
1728
|
}
|
|
1693
|
-
/** Current bloom settings (
|
|
1729
|
+
/** Current bloom settings (tint is a copied `Vec3`). */
|
|
1694
1730
|
getBloomOptions() {
|
|
1695
1731
|
const b = this.bloomSettings;
|
|
1696
1732
|
return {
|
|
1697
1733
|
enabled: b.enabled,
|
|
1698
1734
|
threshold: b.threshold,
|
|
1699
|
-
|
|
1700
|
-
radius: b.radius,
|
|
1735
|
+
scatter: b.scatter,
|
|
1701
1736
|
color: new Vec3(b.color.x, b.color.y, b.color.z),
|
|
1702
1737
|
intensity: b.intensity,
|
|
1703
|
-
clamp: b.clamp,
|
|
1704
1738
|
};
|
|
1705
1739
|
}
|
|
1706
1740
|
getViewTransformOptions() {
|
|
@@ -1798,16 +1832,11 @@ export class Engine {
|
|
|
1798
1832
|
* a background image or a backdrop video, which arrived with grain of their
|
|
1799
1833
|
* own and would be graded rather than matched by a second helping.
|
|
1800
1834
|
*
|
|
1801
|
-
* `animated` false freezes it. A still photograph's grain does not move, and
|
|
1802
|
-
* noise crawling over a frozen picture makes the rendering look more alive
|
|
1803
|
-
* than the thing it is standing in.
|
|
1804
|
-
*
|
|
1805
1835
|
* Costs one hash per pixel in a pass that already runs, and nothing at all at
|
|
1806
1836
|
* zero — the branch is on a uniform.
|
|
1807
1837
|
*/
|
|
1808
|
-
setFilmGrain(amount
|
|
1838
|
+
setFilmGrain(amount) {
|
|
1809
1839
|
this.grain.amount = Math.min(Math.max(amount, 0), 1);
|
|
1810
|
-
this.grain.animated = animated;
|
|
1811
1840
|
if (this.device && this.compositeUniformBuffer)
|
|
1812
1841
|
this.writeCompositeViewUniforms();
|
|
1813
1842
|
}
|
|
@@ -1857,10 +1886,8 @@ export class Engine {
|
|
|
1857
1886
|
// a uniform branch that skips the pow entirely in the common case.
|
|
1858
1887
|
u[1] = 1.0 / Math.max(v.gamma, 1e-4);
|
|
1859
1888
|
u[2] = this.grain.amount;
|
|
1860
|
-
// The seed
|
|
1861
|
-
|
|
1862
|
-
// look more alive than the footage — the opposite of the point.
|
|
1863
|
-
u[3] = this.grain.animated ? Math.floor(this.sceneClock * 24) % 1024 : 0;
|
|
1889
|
+
// The seed, stepped at 24 a second like a film gate.
|
|
1890
|
+
u[3] = Math.floor(this.sceneClock * 24) % 1024;
|
|
1864
1891
|
u[4] = b.color.x;
|
|
1865
1892
|
u[5] = b.color.y;
|
|
1866
1893
|
u[6] = b.color.z;
|
|
@@ -1872,7 +1899,7 @@ export class Engine {
|
|
|
1872
1899
|
// exposure and view transform as the scene — a sun rolls off like a sun).
|
|
1873
1900
|
// The camera basis at u[12..23] is refreshed per frame.
|
|
1874
1901
|
// In modes 2 and 3 the colour slot is dead, so mode 3 carries the world
|
|
1875
|
-
// STRENGTH in u[8] —
|
|
1902
|
+
// STRENGTH in u[8] — the World's intensity, the SAME number that
|
|
1876
1903
|
// scales the irradiance. A sky you can see at full brightness while it
|
|
1877
1904
|
// lights at two thirds is two skies.
|
|
1878
1905
|
const bg = this.backgroundColor;
|
|
@@ -1888,7 +1915,7 @@ export class Engine {
|
|
|
1888
1915
|
// rebuilt per effect, so the compiled variant IS the flag.
|
|
1889
1916
|
u[11] = this.backdropEquirectView ? 2 : showingWorld ? 3 : bg ? 1 : 0;
|
|
1890
1917
|
// Which display transform forms the frame (see viewTransform in composite.ts).
|
|
1891
|
-
u[25] =
|
|
1918
|
+
u[25] = Engine.VIEW_TRANSFORM_ID[v.transform] ?? 0;
|
|
1892
1919
|
u[26] = this.canvas.width;
|
|
1893
1920
|
u[27] = this.canvas.height;
|
|
1894
1921
|
// ── Grade (viewU[7..9]) ── The UI's three tonal COLORS map to ASC CDL here,
|
|
@@ -2533,7 +2560,7 @@ export class Engine {
|
|
|
2533
2560
|
this.renderMirrorBlurChain(encoder);
|
|
2534
2561
|
}
|
|
2535
2562
|
/**
|
|
2536
|
-
* Fill the mirror's mip levels —
|
|
2563
|
+
* Fill the mirror's mip levels — a 13-tap downsample (MIRROR_DOWNSAMPLE_WGSL),
|
|
2537
2564
|
* one pass per level. Only when the blur dial is up: at zero the ground
|
|
2538
2565
|
* samples level 0 exactly and the chain would be work nobody reads.
|
|
2539
2566
|
*/
|
|
@@ -2544,8 +2571,19 @@ export class Engine {
|
|
|
2544
2571
|
const blur = Math.max(this.groundMirrorBlur, this.mirrorSurface?.blur ?? 0);
|
|
2545
2572
|
if (blur <= 0 || this.mirrorMipCount < 2)
|
|
2546
2573
|
return;
|
|
2574
|
+
if (!this.mirrorDownsamplePipeline) {
|
|
2575
|
+
const module = this.device.createShaderModule({ label: "mirror downsample", code: MIRROR_DOWNSAMPLE_WGSL });
|
|
2576
|
+
this.mirrorDownsamplePipeline = this.device.createRenderPipeline({
|
|
2577
|
+
label: "mirror downsample pipeline",
|
|
2578
|
+
layout: "auto",
|
|
2579
|
+
vertex: { module, entryPoint: "vs" },
|
|
2580
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: this.hdrFormat }] },
|
|
2581
|
+
primitive: { topology: "triangle-list" },
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
const downsample = this.mirrorDownsamplePipeline;
|
|
2547
2585
|
if (!this.mirrorBlurBindGroups) {
|
|
2548
|
-
const layout =
|
|
2586
|
+
const layout = downsample.getBindGroupLayout(0);
|
|
2549
2587
|
this.mirrorBlurBindGroups = [];
|
|
2550
2588
|
for (let i = 1; i < this.mirrorMipCount; i++) {
|
|
2551
2589
|
this.mirrorBlurBindGroups.push(this.device.createBindGroup({
|
|
@@ -2563,7 +2601,7 @@ export class Engine {
|
|
|
2563
2601
|
label: `mirror blur ${i}`,
|
|
2564
2602
|
colorAttachments: [{ view: this.mirrorMipViews[i], loadOp: "clear", storeOp: "store" }],
|
|
2565
2603
|
});
|
|
2566
|
-
p.setPipeline(
|
|
2604
|
+
p.setPipeline(downsample);
|
|
2567
2605
|
p.setBindGroup(0, this.mirrorBlurBindGroups[i - 1]);
|
|
2568
2606
|
p.draw(3);
|
|
2569
2607
|
p.end();
|
|
@@ -2716,7 +2754,6 @@ export class Engine {
|
|
|
2716
2754
|
{ binding: 2, resource: this.bloomSampler },
|
|
2717
2755
|
{ binding: 3, resource: { buffer: this.compositeUniformBuffer } },
|
|
2718
2756
|
{ binding: 4, resource: this.maskResolveView },
|
|
2719
|
-
{ binding: 5, resource: this.filmicLutView },
|
|
2720
2757
|
// Whichever equirect is SHOWING — the backdrop if there is one, the
|
|
2721
2758
|
// world otherwise. The world's light does not come through here; it
|
|
2722
2759
|
// rides worldSH into the material shells.
|
|
@@ -2724,7 +2761,6 @@ export class Engine {
|
|
|
2724
2761
|
{ binding: 7, resource: { buffer: this.effect?.paramsBuffer ?? this.bgParamsDummyBuffer } },
|
|
2725
2762
|
{ binding: 8, resource: this.depthReadView },
|
|
2726
2763
|
{ binding: 9, resource: { buffer: this.dofUniformBuffer } },
|
|
2727
|
-
{ binding: 10, resource: (this.agxLutTexture ?? this.agxFallbackTexture).createView({ dimension: "3d" }) },
|
|
2728
2764
|
{ binding: 12, resource: (this.stageGradeTexture ?? this.stageGradeFallback).createView({ dimension: "3d" }) },
|
|
2729
2765
|
{ binding: 11, resource: { buffer: this.castBuffer } },
|
|
2730
2766
|
{ binding: 13, resource: { buffer: this.audioBuffer } },
|
|
@@ -3148,8 +3184,6 @@ export class Engine {
|
|
|
3148
3184
|
{ binding: 31, resource: layers[2] },
|
|
3149
3185
|
{ binding: 32, resource: layers[3] },
|
|
3150
3186
|
{ binding: 2, resource: this.bloomSampler },
|
|
3151
|
-
{ binding: 5, resource: this.filmicLutView },
|
|
3152
|
-
{ binding: 10, resource: (this.agxLutTexture ?? this.agxFallbackTexture).createView({ dimension: "3d" }) },
|
|
3153
3187
|
{ binding: 12, resource: (this.stageGradeTexture ?? this.stageGradeFallback).createView({ dimension: "3d" }) },
|
|
3154
3188
|
{ binding: 1, resource: bloom },
|
|
3155
3189
|
{ binding: 4, resource: this.maskResolveView },
|
|
@@ -3263,7 +3297,7 @@ export class Engine {
|
|
|
3263
3297
|
* whether or not it is the thing you see — and it IS the thing you see until
|
|
3264
3298
|
* a backdrop is set, which is what an HDRI on its own has always done.
|
|
3265
3299
|
*
|
|
3266
|
-
* `strength` is
|
|
3300
|
+
* `strength` is the World's intensity and is folded into the
|
|
3267
3301
|
* coefficients, so what lights her is what you see.
|
|
3268
3302
|
*/
|
|
3269
3303
|
setWorldEquirect(source) {
|
|
@@ -3326,8 +3360,11 @@ export class Engine {
|
|
|
3326
3360
|
}
|
|
3327
3361
|
this.device.queue.writeTexture({ texture: tex, mipLevel: level }, packHalf(data), { bytesPerRow: w * 8, rowsPerImage: h }, [w, h]);
|
|
3328
3362
|
}
|
|
3329
|
-
|
|
3330
|
-
|
|
3363
|
+
// The box chain above is the SOURCE: the levels the materials read are
|
|
3364
|
+
// the GGX convolution of it, one roughness per level (prefilterSky).
|
|
3365
|
+
const filtered = this.prefilterSky(tex, source.width, source.height, levels);
|
|
3366
|
+
this.worldEquirectTexture = filtered;
|
|
3367
|
+
this.worldEquirectView = filtered.createView();
|
|
3331
3368
|
// The sky lights the scene, not only backs it. The sun keeps the toon
|
|
3332
3369
|
// ramp — this is the ambient term, exactly where the flat world colour
|
|
3333
3370
|
// used to sit.
|
|
@@ -3348,6 +3385,9 @@ export class Engine {
|
|
|
3348
3385
|
this.worldBindingsDirty = true;
|
|
3349
3386
|
this.rebuildPerFrameBindGroups();
|
|
3350
3387
|
this.rebuildCompositeBindGroup();
|
|
3388
|
+
// The probe's compose step binds the sky it lays in behind the stage.
|
|
3389
|
+
this.dropProbeCapture();
|
|
3390
|
+
this.captureReflectionProbe();
|
|
3351
3391
|
if (this.device && this.compositeUniformBuffer)
|
|
3352
3392
|
this.writeCompositeViewUniforms();
|
|
3353
3393
|
}
|
|
@@ -3400,8 +3440,20 @@ export class Engine {
|
|
|
3400
3440
|
if (this.device && this.compositeUniformBuffer)
|
|
3401
3441
|
this.writeCompositeViewUniforms();
|
|
3402
3442
|
}
|
|
3403
|
-
|
|
3404
|
-
|
|
3443
|
+
/**
|
|
3444
|
+
* The scene composite's pair (gamma = 1, gamma ≠ 1), async and from the
|
|
3445
|
+
* shader cache. Its source depends only on which field mounts the scene
|
|
3446
|
+
* samples, so there are four at most, and an effect install that keeps the
|
|
3447
|
+
* mounts it had gets the pair already built.
|
|
3448
|
+
*/
|
|
3449
|
+
compositePipelines(hasBackground, hasForeground) {
|
|
3450
|
+
const code = buildCompositeShader(hasBackground || hasForeground
|
|
3451
|
+
? // No wgsl and so no trails: the composite hosts no effect source at
|
|
3452
|
+
// all, it only decides whether to sample the layer the field pass drew.
|
|
3453
|
+
{ wgsl: "", paramsDecl: "", hasBackground, hasForeground, gridSize: 0, trailCount: 0 }
|
|
3454
|
+
: null);
|
|
3455
|
+
const module = this.cachedShaderModule(code, "composite shader");
|
|
3456
|
+
const make = (applyGamma, label) => this.cachedRenderPipeline({
|
|
3405
3457
|
label,
|
|
3406
3458
|
layout: this.compositePipelineLayout,
|
|
3407
3459
|
vertex: { module, entryPoint: "vs" },
|
|
@@ -3413,6 +3465,7 @@ export class Engine {
|
|
|
3413
3465
|
},
|
|
3414
3466
|
primitive: { topology: "triangle-list" },
|
|
3415
3467
|
});
|
|
3468
|
+
return Promise.all([make(false, "composite pipeline (gamma=1)"), make(true, "composite pipeline (gamma!=1)")]);
|
|
3416
3469
|
}
|
|
3417
3470
|
/**
|
|
3418
3471
|
* Install the scene's WGSL effect (shadertoy-style), rendered per-pixel in the
|
|
@@ -3641,12 +3694,11 @@ export class Engine {
|
|
|
3641
3694
|
const fieldEffect = hasBackground || hasForeground
|
|
3642
3695
|
? { wgsl, paramsDecl, hasBackground, hasForeground, gridSize, alias, trailCount: anchors.filter((a) => a.trail).length, filter, additiveLayer: d.additiveLayer }
|
|
3643
3696
|
: null;
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
return { ok: false, diagnostics: [scopeErr.message], mounts, params: d.params, duration: d.duration, readsCast: false };
|
|
3697
|
+
// No composite is built here. One used to be — a module and two pipelines
|
|
3698
|
+
// per effect, then dropped: the composite is static (no user code reaches
|
|
3699
|
+
// it) and setEffects builds the scene's one after the list is in. Those
|
|
3700
|
+
// two compiles were most of a second each and validated nothing an author
|
|
3701
|
+
// wrote.
|
|
3650
3702
|
// Declared like every other mount property: by what the source says, not by
|
|
3651
3703
|
// a setting somewhere else that an author cannot see from the file.
|
|
3652
3704
|
const layerBlend = d.additiveLayer
|
|
@@ -3657,73 +3709,56 @@ export class Engine {
|
|
|
3657
3709
|
const fieldSource = buildFieldShader({ ...fieldEffect, ids: mrtIdsEnabled() });
|
|
3658
3710
|
const userLineOffset = fieldSource.slice(0, fieldSource.indexOf(wgsl)).split("\n").length - 1;
|
|
3659
3711
|
this.device.pushErrorScope("validation");
|
|
3660
|
-
const fieldModule = this.
|
|
3661
|
-
const
|
|
3662
|
-
|
|
3712
|
+
const fieldModule = this.cachedShaderModule(fieldSource, "field shader (effect)");
|
|
3713
|
+
const fieldScope = this.device.popErrorScope();
|
|
3714
|
+
// Started before the diagnostics are read, which only decide whether it
|
|
3715
|
+
// is kept: the compile and the check overlap.
|
|
3716
|
+
const fieldBuild = this.cachedRenderPipeline({
|
|
3717
|
+
label: "field layer pipeline",
|
|
3718
|
+
layout: this.fieldPipelineLayout,
|
|
3719
|
+
vertex: { module: fieldModule, entryPoint: "fieldVs" },
|
|
3720
|
+
fragment: {
|
|
3721
|
+
module: fieldModule,
|
|
3722
|
+
entryPoint: "fieldFs",
|
|
3723
|
+
// OVER, accumulating PREMULTIPLIED colour: several effects draw into
|
|
3724
|
+
// these two targets in document order, and each must layer onto what
|
|
3725
|
+
// the earlier ones left rather than replace it. src-alpha on colour
|
|
3726
|
+
// premultiplies as it writes; alpha accumulates as one-over. An
|
|
3727
|
+
// author still returns STRAIGHT colour+alpha, exactly as before —
|
|
3728
|
+
// the premultiplication happens here, and the composite reads it
|
|
3729
|
+
// back knowing that. With one effect over a cleared target the
|
|
3730
|
+
// result is identical to the replace it used to do.
|
|
3731
|
+
// A filter composes over the layers in its own shader and writes
|
|
3732
|
+
// the result outright: blending would layer it onto a page that
|
|
3733
|
+
// holds nothing.
|
|
3734
|
+
targets: filter
|
|
3735
|
+
? [{ format: "rgba16float" }, { format: "rgba16float" }]
|
|
3736
|
+
: [
|
|
3737
|
+
{ format: "rgba16float", blend: layerBlend },
|
|
3738
|
+
{ format: "rgba16float", blend: layerBlend },
|
|
3739
|
+
],
|
|
3740
|
+
},
|
|
3741
|
+
primitive: { topology: "triangle-list" },
|
|
3742
|
+
multisample: { count: 1 },
|
|
3743
|
+
});
|
|
3744
|
+
fieldBuild.catch(() => { });
|
|
3745
|
+
const [info, fieldScopeErr] = await Promise.all([fieldModule.getCompilationInfo(), fieldScope]);
|
|
3663
3746
|
const diagnostics = info.messages
|
|
3664
3747
|
.filter((m) => m.type === "error")
|
|
3665
3748
|
.map((m) => `${Math.max(0, m.lineNum - userLineOffset)}:${m.linePos} ${m.message}`);
|
|
3666
3749
|
if (diagnostics.length === 0 && fieldScopeErr)
|
|
3667
3750
|
diagnostics.push(fieldScopeErr.message);
|
|
3668
|
-
if (diagnostics.length > 0)
|
|
3751
|
+
if (diagnostics.length > 0) {
|
|
3752
|
+
this.forgetShaderModule(fieldSource);
|
|
3669
3753
|
return { ok: false, diagnostics, mounts, params: d.params, duration: d.duration, readsCast: false };
|
|
3754
|
+
}
|
|
3670
3755
|
try {
|
|
3671
|
-
fieldPipeline = await
|
|
3672
|
-
label: "field layer pipeline",
|
|
3673
|
-
layout: this.fieldPipelineLayout,
|
|
3674
|
-
vertex: { module: fieldModule, entryPoint: "fieldVs" },
|
|
3675
|
-
fragment: {
|
|
3676
|
-
module: fieldModule,
|
|
3677
|
-
entryPoint: "fieldFs",
|
|
3678
|
-
// OVER, accumulating PREMULTIPLIED colour: several effects draw into
|
|
3679
|
-
// these two targets in document order, and each must layer onto what
|
|
3680
|
-
// the earlier ones left rather than replace it. src-alpha on colour
|
|
3681
|
-
// premultiplies as it writes; alpha accumulates as one-over. An
|
|
3682
|
-
// author still returns STRAIGHT colour+alpha, exactly as before —
|
|
3683
|
-
// the premultiplication happens here, and the composite reads it
|
|
3684
|
-
// back knowing that. With one effect over a cleared target the
|
|
3685
|
-
// result is identical to the replace it used to do.
|
|
3686
|
-
// A filter composes over the layers in its own shader and writes
|
|
3687
|
-
// the result outright: blending would layer it onto a page that
|
|
3688
|
-
// holds nothing.
|
|
3689
|
-
targets: filter
|
|
3690
|
-
? [{ format: "rgba16float" }, { format: "rgba16float" }]
|
|
3691
|
-
: [
|
|
3692
|
-
{ format: "rgba16float", blend: layerBlend },
|
|
3693
|
-
{ format: "rgba16float", blend: layerBlend },
|
|
3694
|
-
],
|
|
3695
|
-
},
|
|
3696
|
-
primitive: { topology: "triangle-list" },
|
|
3697
|
-
multisample: { count: 1 },
|
|
3698
|
-
});
|
|
3756
|
+
fieldPipeline = await fieldBuild;
|
|
3699
3757
|
}
|
|
3700
3758
|
catch (e) {
|
|
3701
3759
|
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)], mounts, params: d.params, duration: d.duration, readsCast: false };
|
|
3702
3760
|
}
|
|
3703
3761
|
}
|
|
3704
|
-
let identity;
|
|
3705
|
-
let gamma;
|
|
3706
|
-
try {
|
|
3707
|
-
const make = (applyGamma, label) => this.device.createRenderPipelineAsync({
|
|
3708
|
-
label,
|
|
3709
|
-
layout: this.compositePipelineLayout,
|
|
3710
|
-
vertex: { module, entryPoint: "vs" },
|
|
3711
|
-
fragment: {
|
|
3712
|
-
module,
|
|
3713
|
-
entryPoint: "fs",
|
|
3714
|
-
constants: { APPLY_GAMMA: applyGamma ? 1 : 0 },
|
|
3715
|
-
targets: [{ format: this.presentationFormat }],
|
|
3716
|
-
},
|
|
3717
|
-
primitive: { topology: "triangle-list" },
|
|
3718
|
-
});
|
|
3719
|
-
[identity, gamma] = await Promise.all([
|
|
3720
|
-
make(false, "composite pipeline (effect, gamma=1)"),
|
|
3721
|
-
make(true, "composite pipeline (effect, gamma!=1)"),
|
|
3722
|
-
]);
|
|
3723
|
-
}
|
|
3724
|
-
catch (e) {
|
|
3725
|
-
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)], mounts, params: d.params, duration: d.duration, readsCast: false };
|
|
3726
|
-
}
|
|
3727
3762
|
// Built BEFORE the swap: a particle stage that fails to compile has to leave
|
|
3728
3763
|
// the previously installed effect running, exactly as a bad composite does.
|
|
3729
3764
|
// A stage that fails after an earlier one succeeded would otherwise strand
|
|
@@ -3950,6 +3985,9 @@ export class Engine {
|
|
|
3950
3985
|
}
|
|
3951
3986
|
const requested = list ?? [];
|
|
3952
3987
|
if (requested.length === 0) {
|
|
3988
|
+
// Built (or found) before anything is torn down, so no frame draws
|
|
3989
|
+
// between the old effects and the composite that no longer samples them.
|
|
3990
|
+
const [identity, gamma] = await this.compositePipelines(false, false);
|
|
3953
3991
|
for (const e of this.effects) {
|
|
3954
3992
|
e.paramsBuffer?.destroy();
|
|
3955
3993
|
e.lights?.uniform.destroy();
|
|
@@ -3962,9 +4000,8 @@ export class Engine {
|
|
|
3962
4000
|
this.allocateLightSlots();
|
|
3963
4001
|
this.anchorTable = EMPTY_ANCHOR_TABLE;
|
|
3964
4002
|
this.clearTrailHistory();
|
|
3965
|
-
|
|
3966
|
-
this.
|
|
3967
|
-
this.compositePipelineGamma = this.makeCompositePipeline(module, true, "composite pipeline (gamma!=1)");
|
|
4003
|
+
this.compositePipelineIdentity = identity;
|
|
4004
|
+
this.compositePipelineGamma = gamma;
|
|
3968
4005
|
this.rebuildCompositeBindGroup();
|
|
3969
4006
|
this.writeCompositeViewUniforms();
|
|
3970
4007
|
return [];
|
|
@@ -4010,6 +4047,10 @@ export class Engine {
|
|
|
4010
4047
|
if (r)
|
|
4011
4048
|
r.diagnostics.push(`anchor "${d.bone}" dropped: the scene is already using all ${MAX_EFFECT_ANCHORS} slots`);
|
|
4012
4049
|
}
|
|
4050
|
+
// The composite for the new list, built before the swap below so the swap
|
|
4051
|
+
// stays one synchronous step. From the shader cache: it was rebuilt, and
|
|
4052
|
+
// synchronously, on every install.
|
|
4053
|
+
const [compositeIdentity, compositeGamma] = await this.compositePipelines(instances.some((e) => e.hasBackground), instances.some((e) => e.hasForeground));
|
|
4013
4054
|
// ── Swap. Everything above either succeeded or was excluded, so the scene
|
|
4014
4055
|
// that was running is only torn down now.
|
|
4015
4056
|
for (const e of this.effects) {
|
|
@@ -4060,18 +4101,8 @@ export class Engine {
|
|
|
4060
4101
|
// Scene-level, from the union: the composite decides only whether to SAMPLE
|
|
4061
4102
|
// the field layers, so one effect with a background is enough to turn that
|
|
4062
4103
|
// on for the frame.
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
const compositeModule = this.device.createShaderModule({
|
|
4066
|
-
label: "composite shader (effects)",
|
|
4067
|
-
code: buildCompositeShader(hasBackground || hasForeground
|
|
4068
|
-
? // No wgsl and so no trails: the composite hosts no effect source at
|
|
4069
|
-
// all, it only decides whether to sample the layer the field pass drew.
|
|
4070
|
-
{ wgsl: "", paramsDecl: "", hasBackground, hasForeground, gridSize: 0, trailCount: 0 }
|
|
4071
|
-
: null),
|
|
4072
|
-
});
|
|
4073
|
-
this.compositePipelineIdentity = this.makeCompositePipeline(compositeModule, false, "composite pipeline (gamma=1)");
|
|
4074
|
-
this.compositePipelineGamma = this.makeCompositePipeline(compositeModule, true, "composite pipeline (gamma!=1)");
|
|
4104
|
+
this.compositePipelineIdentity = compositeIdentity;
|
|
4105
|
+
this.compositePipelineGamma = compositeGamma;
|
|
4075
4106
|
// Nothing to promote any more: `#fullres` is per effect, read into
|
|
4076
4107
|
// fieldLayer when the instance is built, and both target pairs exist for
|
|
4077
4108
|
// the life of the surface. What used to be a scene-wide decision made here
|
|
@@ -4163,26 +4194,33 @@ export class Engine {
|
|
|
4163
4194
|
trailCount: anchors.filter((x) => x.trail).length,
|
|
4164
4195
|
alias,
|
|
4165
4196
|
};
|
|
4197
|
+
// The scope is popped before the first await: compiles run side by side
|
|
4198
|
+
// (both modules here, other effects, style groups), and a scope held open
|
|
4199
|
+
// across an await would catch whichever of them errored first.
|
|
4166
4200
|
const compile = async (code, label) => {
|
|
4167
4201
|
const offset = code.slice(0, code.indexOf(wgsl)).split("\n").length - 1;
|
|
4168
4202
|
this.device.pushErrorScope("validation");
|
|
4169
|
-
const module = this.
|
|
4170
|
-
const
|
|
4171
|
-
const scopeErr = await
|
|
4203
|
+
const module = this.cachedShaderModule(code, label);
|
|
4204
|
+
const scope = this.device.popErrorScope();
|
|
4205
|
+
const [info, scopeErr] = await Promise.all([module.getCompilationInfo(), scope]);
|
|
4172
4206
|
const diagnostics = info.messages
|
|
4173
4207
|
.filter((m) => m.type === "error")
|
|
4174
4208
|
.map((m) => `${Math.max(0, m.lineNum - offset)}:${m.linePos} ${m.message}`);
|
|
4175
4209
|
if (diagnostics.length === 0 && scopeErr)
|
|
4176
4210
|
diagnostics.push(scopeErr.message);
|
|
4211
|
+
if (diagnostics.length)
|
|
4212
|
+
this.forgetShaderModule(code);
|
|
4177
4213
|
return diagnostics.length ? diagnostics : module;
|
|
4178
4214
|
};
|
|
4179
|
-
const computeModule = await
|
|
4215
|
+
const [computeModule, renderModule] = await Promise.all([
|
|
4216
|
+
compile(buildParticleComputeShader(src, cast), "particle compute"),
|
|
4217
|
+
compile(buildParticleRenderShader(src, cast), "particle render"),
|
|
4218
|
+
]);
|
|
4180
4219
|
if (Array.isArray(computeModule)) {
|
|
4181
4220
|
points?.buffer.destroy();
|
|
4182
4221
|
dropTextures();
|
|
4183
4222
|
return { ok: false, diagnostics: computeModule };
|
|
4184
4223
|
}
|
|
4185
|
-
const renderModule = await compile(buildParticleRenderShader(src, cast), "particle render");
|
|
4186
4224
|
if (Array.isArray(renderModule)) {
|
|
4187
4225
|
points?.buffer.destroy();
|
|
4188
4226
|
dropTextures();
|
|
@@ -4220,7 +4258,7 @@ export class Engine {
|
|
|
4220
4258
|
// Declaring one set of flags for both layouts is what made the pipeline
|
|
4221
4259
|
// layout invalid, and the error surfaces later and unhelpfully as "invalid
|
|
4222
4260
|
// due to a previous error".
|
|
4223
|
-
const layoutFor = (storage, visibility, shadow, withIndirect) => this.
|
|
4261
|
+
const layoutFor = (storage, visibility, shadow, withIndirect) => this.cachedBindGroupLayout({
|
|
4224
4262
|
entries: [
|
|
4225
4263
|
{ binding: 0, visibility, buffer: { type: storage } },
|
|
4226
4264
|
{ binding: 1, visibility, buffer: { type: "uniform" } },
|
|
@@ -4326,31 +4364,29 @@ export class Engine {
|
|
|
4326
4364
|
// blends would be a copy.
|
|
4327
4365
|
const targets = sceneTargetsFor(src.blend === "additive" ? "particle-additive" : "particle", this.sceneFormats);
|
|
4328
4366
|
const cutout = src.blend === "cutout";
|
|
4367
|
+
// ALL STAGES AT ONCE: compute, count, prepass and shading compile side by
|
|
4368
|
+
// side rather than in turn. The scope closes before the first await, for
|
|
4369
|
+
// the reason compile() gives.
|
|
4329
4370
|
this.device.pushErrorScope("validation");
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4371
|
+
const renderLayoutObj = this.cachedPipelineLayout([renderLayout]);
|
|
4372
|
+
const builds = Promise.all([
|
|
4373
|
+
this.cachedComputePipeline({
|
|
4333
4374
|
label: "particle compute pipeline",
|
|
4334
|
-
layout:
|
|
4375
|
+
layout: this.cachedPipelineLayout([computeLayout]),
|
|
4335
4376
|
compute: { module: computeModule, entryPoint: "main" },
|
|
4336
|
-
})
|
|
4337
|
-
|
|
4338
|
-
? {
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
indirect,
|
|
4345
|
-
binds: bindFor(countLayout, this.cameraUniformBuffer, false, true),
|
|
4346
|
-
}
|
|
4347
|
-
: null;
|
|
4348
|
-
const renderLayoutObj = this.device.createPipelineLayout({ bindGroupLayouts: [renderLayout] });
|
|
4377
|
+
}),
|
|
4378
|
+
indirect && countLayout
|
|
4379
|
+
? this.cachedComputePipeline({
|
|
4380
|
+
label: "particle count pipeline",
|
|
4381
|
+
layout: this.cachedPipelineLayout([countLayout]),
|
|
4382
|
+
compute: { module: computeModule, entryPoint: "rzCount" },
|
|
4383
|
+
})
|
|
4384
|
+
: null,
|
|
4349
4385
|
// The prepass, when the effect gave the cutout a cheap shape: coverage to
|
|
4350
4386
|
// depth, colour targets at writeMask 0 (the depth-prepass class), and
|
|
4351
4387
|
// the shading pass below then tests EQUAL and writes no depth.
|
|
4352
|
-
|
|
4353
|
-
?
|
|
4388
|
+
prepass
|
|
4389
|
+
? this.cachedRenderPipeline({
|
|
4354
4390
|
label: "particle depth prepass pipeline",
|
|
4355
4391
|
layout: renderLayoutObj,
|
|
4356
4392
|
vertex: { module: renderModule, entryPoint: "vs" },
|
|
@@ -4359,8 +4395,8 @@ export class Engine {
|
|
|
4359
4395
|
depthStencil: { format: this.depthFormat, depthWriteEnabled: true, depthCompare: this.depthAhead },
|
|
4360
4396
|
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
4361
4397
|
})
|
|
4362
|
-
: null
|
|
4363
|
-
|
|
4398
|
+
: null,
|
|
4399
|
+
this.cachedRenderPipeline({
|
|
4364
4400
|
label: "particle render pipeline",
|
|
4365
4401
|
layout: renderLayoutObj,
|
|
4366
4402
|
vertex: { module: renderModule, entryPoint: "vs" },
|
|
@@ -4379,10 +4415,25 @@ export class Engine {
|
|
|
4379
4415
|
// shading ten layers and shading about three.
|
|
4380
4416
|
depthStencil: prepass
|
|
4381
4417
|
? { format: this.depthFormat, depthWriteEnabled: false, depthCompare: "equal" }
|
|
4382
|
-
:
|
|
4418
|
+
: d.depthAlways
|
|
4419
|
+
? // `#depth always`: over the scene, Unity's ZTest Always (a moon's halo
|
|
4420
|
+
// drawn in front of the sky dome it sits behind)
|
|
4421
|
+
{ format: this.depthFormat, depthWriteEnabled: false, depthCompare: "always" }
|
|
4422
|
+
: { format: this.depthFormat, depthWriteEnabled: cutout, depthCompare: this.depthAhead },
|
|
4383
4423
|
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
4384
|
-
})
|
|
4385
|
-
|
|
4424
|
+
}),
|
|
4425
|
+
]);
|
|
4426
|
+
const scope = this.device.popErrorScope();
|
|
4427
|
+
try {
|
|
4428
|
+
const [compute, countPipeline, depth, render] = await builds;
|
|
4429
|
+
const live = indirect && countLayout && countPipeline
|
|
4430
|
+
? {
|
|
4431
|
+
pipeline: countPipeline,
|
|
4432
|
+
indirect,
|
|
4433
|
+
binds: bindFor(countLayout, this.cameraUniformBuffer, false, true),
|
|
4434
|
+
}
|
|
4435
|
+
: null;
|
|
4436
|
+
const scoped = await scope;
|
|
4386
4437
|
if (scoped) {
|
|
4387
4438
|
buffer.destroy();
|
|
4388
4439
|
uniform.destroy();
|
|
@@ -4423,7 +4474,7 @@ export class Engine {
|
|
|
4423
4474
|
};
|
|
4424
4475
|
}
|
|
4425
4476
|
catch (e) {
|
|
4426
|
-
await
|
|
4477
|
+
await scope;
|
|
4427
4478
|
buffer.destroy();
|
|
4428
4479
|
uniform.destroy();
|
|
4429
4480
|
points?.buffer.destroy();
|
|
@@ -4795,8 +4846,10 @@ export class Engine {
|
|
|
4795
4846
|
// point: a layer composited after tone mapping can never bloom.
|
|
4796
4847
|
//
|
|
4797
4848
|
// Additive, where this used to be MAX. Max was right for a post-tonemap
|
|
4798
|
-
// layer; in HDR before bloom, overlapping light sums.
|
|
4799
|
-
|
|
4849
|
+
// layer; in HDR before bloom, overlapping light sums. `#blend over` lays the
|
|
4850
|
+
// ribbon over the scene by its alpha instead — a game trail's own blend,
|
|
4851
|
+
// under which its dark smoke darkens rather than vanishing.
|
|
4852
|
+
const targets = sceneTargetsFor(d.trailBlend === "over" ? "trail-over" : "trail", this.sceneFormats);
|
|
4800
4853
|
this.device.pushErrorScope("validation");
|
|
4801
4854
|
try {
|
|
4802
4855
|
const pipeline = await this.device.createRenderPipelineAsync({
|
|
@@ -5660,10 +5713,8 @@ export class Engine {
|
|
|
5660
5713
|
b.enabled = patch.enabled;
|
|
5661
5714
|
if (patch.threshold !== undefined)
|
|
5662
5715
|
b.threshold = patch.threshold;
|
|
5663
|
-
if (patch.
|
|
5664
|
-
b.
|
|
5665
|
-
if (patch.radius !== undefined)
|
|
5666
|
-
b.radius = patch.radius;
|
|
5716
|
+
if (patch.scatter !== undefined)
|
|
5717
|
+
b.scatter = patch.scatter;
|
|
5667
5718
|
if (patch.color !== undefined) {
|
|
5668
5719
|
b.color.x = patch.color.x;
|
|
5669
5720
|
b.color.y = patch.color.y;
|
|
@@ -5671,33 +5722,156 @@ export class Engine {
|
|
|
5671
5722
|
}
|
|
5672
5723
|
if (patch.intensity !== undefined)
|
|
5673
5724
|
b.intensity = patch.intensity;
|
|
5674
|
-
if (
|
|
5675
|
-
b.clamp = patch.clamp;
|
|
5676
|
-
if (this.device && this.bloomBlitUniformBuffer) {
|
|
5725
|
+
if (this.device && this.compositeUniformBuffer) {
|
|
5677
5726
|
this.writeBloomUniforms();
|
|
5678
5727
|
this.writeCompositeViewUniforms();
|
|
5679
5728
|
}
|
|
5680
5729
|
}
|
|
5681
|
-
|
|
5730
|
+
ensureBloomPipelines() {
|
|
5731
|
+
// The buffer, not a pipeline, says "already built": at init the pipelines
|
|
5732
|
+
// land async (initRenderPipeline) and are absent for a moment.
|
|
5733
|
+
if (this.bloomUniformBuffer)
|
|
5734
|
+
return;
|
|
5735
|
+
const device = this.device;
|
|
5736
|
+
this.bloomUniformBuffer = device.createBuffer({
|
|
5737
|
+
label: "bloomuniforms",
|
|
5738
|
+
size: 16,
|
|
5739
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
5740
|
+
});
|
|
5741
|
+
this.bloomUpsampleBindGroupLayout = device.createBindGroupLayout({
|
|
5742
|
+
label: "bloomupsample layout",
|
|
5743
|
+
entries: [
|
|
5744
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
5745
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
5746
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
5747
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
5748
|
+
],
|
|
5749
|
+
});
|
|
5750
|
+
const pipeline = (label, code, layout, assign) => {
|
|
5751
|
+
const module = device.createShaderModule({ label, code });
|
|
5752
|
+
this.initRenderPipeline({
|
|
5753
|
+
label,
|
|
5754
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
|
|
5755
|
+
vertex: { module, entryPoint: "vs" },
|
|
5756
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: this.hdrFormat }] },
|
|
5757
|
+
primitive: { topology: "triangle-list" },
|
|
5758
|
+
}, assign);
|
|
5759
|
+
};
|
|
5760
|
+
pipeline("bloomprefilter", BLOOM_PREFILTER_SHADER_WGSL, this.bloomPrefilterBindGroupLayout, (p) => (this.bloomPrefilterPipeline = p));
|
|
5761
|
+
pipeline("bloomblur H", BLOOM_BLUR_H_SHADER_WGSL, this.bloomBlurBindGroupLayout, (p) => (this.bloomBlurHPipeline = p));
|
|
5762
|
+
pipeline("bloomblur V", BLOOM_BLUR_V_SHADER_WGSL, this.bloomBlurBindGroupLayout, (p) => (this.bloomBlurVPipeline = p));
|
|
5763
|
+
pipeline("bloomupsample", BLOOM_UPSAMPLE_SHADER_WGSL, this.bloomUpsampleBindGroupLayout, (p) => (this.bloomUpsamplePipeline = p));
|
|
5764
|
+
}
|
|
5765
|
+
/** The game's chain at the current canvas size (AGSimPostFX.Bloom): half
|
|
5766
|
+
* size, then floor(log2(longest side)) - 1 levels, each half the last. */
|
|
5767
|
+
buildBloomTargets() {
|
|
5768
|
+
if (!this.device || !this.hdrResolveTexture || !this.maskResolveView || !this.bloomPrefilterBindGroupLayout)
|
|
5769
|
+
return;
|
|
5770
|
+
this.ensureBloomPipelines();
|
|
5771
|
+
const w0 = Math.max(1, Math.floor(this.hdrResolveTexture.width / 2));
|
|
5772
|
+
const h0 = Math.max(1, Math.floor(this.hdrResolveTexture.height / 2));
|
|
5773
|
+
const levels = Math.min(Engine.BLOOM_MAX_LEVELS, Math.max(1, Math.floor(Math.log2(Math.max(w0, h0)) - 1)));
|
|
5774
|
+
// Mip chains, whose sizes are floor-halved exactly as the game's targets are.
|
|
5775
|
+
const make = (label) => this.device.createTexture({
|
|
5776
|
+
label,
|
|
5777
|
+
size: [w0, h0],
|
|
5778
|
+
mipLevelCount: levels,
|
|
5779
|
+
format: this.hdrFormat,
|
|
5780
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
5781
|
+
});
|
|
5782
|
+
this.bloomDownTexture?.destroy();
|
|
5783
|
+
this.bloomUpTexture?.destroy();
|
|
5784
|
+
this.bloomDownTexture = make("bloomdown");
|
|
5785
|
+
this.bloomUpTexture = make("bloomup");
|
|
5786
|
+
this.bloomLevels = levels;
|
|
5787
|
+
const views = (t) => Array.from({ length: levels }, (_, i) => t.createView({ baseMipLevel: i, mipLevelCount: 1 }));
|
|
5788
|
+
this.bloomDownViews = views(this.bloomDownTexture);
|
|
5789
|
+
this.bloomUpViews = views(this.bloomUpTexture);
|
|
5790
|
+
this.writeBloomUniforms();
|
|
5791
|
+
const uniform = { buffer: this.bloomUniformBuffer };
|
|
5792
|
+
this.bloomPrefilterBindGroup = this.device.createBindGroup({
|
|
5793
|
+
label: "bloomprefilter",
|
|
5794
|
+
layout: this.bloomPrefilterBindGroupLayout,
|
|
5795
|
+
entries: [
|
|
5796
|
+
{ binding: 0, resource: this.hdrResolveTexture.createView() },
|
|
5797
|
+
{ binding: 1, resource: uniform },
|
|
5798
|
+
{ binding: 2, resource: this.maskResolveView },
|
|
5799
|
+
],
|
|
5800
|
+
});
|
|
5801
|
+
const blur = (label, src) => this.device.createBindGroup({
|
|
5802
|
+
label,
|
|
5803
|
+
layout: this.bloomBlurBindGroupLayout,
|
|
5804
|
+
entries: [
|
|
5805
|
+
{ binding: 0, resource: src },
|
|
5806
|
+
{ binding: 1, resource: this.bloomSampler },
|
|
5807
|
+
],
|
|
5808
|
+
});
|
|
5809
|
+
// [i - 1] is level i: blurH reads down[i-1] into up[i], blurV up[i] into down[i].
|
|
5810
|
+
this.bloomBlurHBindGroups = [];
|
|
5811
|
+
this.bloomBlurVBindGroups = [];
|
|
5812
|
+
for (let i = 1; i < levels; i++) {
|
|
5813
|
+
this.bloomBlurHBindGroups.push(blur(`bloomblur H ${i}`, this.bloomDownViews[i - 1]));
|
|
5814
|
+
this.bloomBlurVBindGroups.push(blur(`bloomblur V ${i}`, this.bloomUpViews[i]));
|
|
5815
|
+
}
|
|
5816
|
+
// [k] writes up[levels - 2 - k], coarsest first.
|
|
5817
|
+
this.bloomUpsampleBindGroups = [];
|
|
5818
|
+
for (let i = levels - 2; i >= 0; i--) {
|
|
5819
|
+
const low = i === levels - 2 ? this.bloomDownViews[i + 1] : this.bloomUpViews[i + 1];
|
|
5820
|
+
this.bloomUpsampleBindGroups.push(this.device.createBindGroup({
|
|
5821
|
+
label: `bloomupsample ${i}`,
|
|
5822
|
+
layout: this.bloomUpsampleBindGroupLayout,
|
|
5823
|
+
entries: [
|
|
5824
|
+
{ binding: 0, resource: this.bloomDownViews[i] },
|
|
5825
|
+
{ binding: 1, resource: low },
|
|
5826
|
+
{ binding: 2, resource: this.bloomSampler },
|
|
5827
|
+
{ binding: 3, resource: uniform },
|
|
5828
|
+
],
|
|
5829
|
+
}));
|
|
5830
|
+
}
|
|
5831
|
+
}
|
|
5682
5832
|
writeBloomUniforms() {
|
|
5833
|
+
if (!this.bloomUniformBuffer)
|
|
5834
|
+
return;
|
|
5683
5835
|
const b = this.bloomSettings;
|
|
5684
|
-
const
|
|
5685
|
-
//
|
|
5686
|
-
//
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
this.device.queue.writeBuffer(this.
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5836
|
+
const u = this.bloomUniformData;
|
|
5837
|
+
// Threshold, its soft knee (half of it, as the game derives it), the clamp,
|
|
5838
|
+
// and scatter. Tint and intensity live in the composite.
|
|
5839
|
+
u[0] = Math.max(0, b.threshold);
|
|
5840
|
+
u[1] = Math.max(0, b.threshold) * 0.5;
|
|
5841
|
+
u[2] = Engine.BLOOM_CLAMP;
|
|
5842
|
+
// URP's mapping: never all the way to the widest level, which alone is a
|
|
5843
|
+
// few pixels smeared over the screen — the glow would vanish.
|
|
5844
|
+
u[3] = 0.05 + 0.9 * Math.min(1, Math.max(0, b.scatter));
|
|
5845
|
+
this.device.queue.writeBuffer(this.bloomUniformBuffer, 0, u);
|
|
5846
|
+
}
|
|
5847
|
+
/** The game's chain, pass for pass (AGSimPostFX.Bloom). */
|
|
5848
|
+
renderBloom(encoder) {
|
|
5849
|
+
const levels = this.bloomLevels;
|
|
5850
|
+
const att = this.bloomPassDescriptor.colorAttachments[0];
|
|
5851
|
+
const total = 1 + 3 * (levels - 1);
|
|
5852
|
+
let n = 0;
|
|
5853
|
+
const draw = (target, pipeline, group) => {
|
|
5854
|
+
att.view = target;
|
|
5855
|
+
// One timing span for the whole chain, as for the pyramid.
|
|
5856
|
+
const open = n === 0 ? this.stampOpen("bloom") : undefined;
|
|
5857
|
+
const close = n === total - 1 ? this.stampClose("bloom") : undefined;
|
|
5858
|
+
this.bloomPassDescriptor.timestampWrites = open && close ? { ...open, ...close } : (open ?? close);
|
|
5859
|
+
n++;
|
|
5860
|
+
const p = encoder.beginRenderPass(this.bloomPassDescriptor);
|
|
5861
|
+
p.setPipeline(pipeline);
|
|
5862
|
+
p.setBindGroup(0, group);
|
|
5863
|
+
p.draw(3);
|
|
5864
|
+
p.end();
|
|
5865
|
+
};
|
|
5866
|
+
draw(this.bloomDownViews[0], this.bloomPrefilterPipeline, this.bloomPrefilterBindGroup);
|
|
5867
|
+
for (let i = 1; i < levels; i++) {
|
|
5868
|
+
draw(this.bloomUpViews[i], this.bloomBlurHPipeline, this.bloomBlurHBindGroups[i - 1]);
|
|
5869
|
+
draw(this.bloomDownViews[i], this.bloomBlurVPipeline, this.bloomBlurVBindGroups[i - 1]);
|
|
5870
|
+
}
|
|
5871
|
+
for (let k = 0; k < levels - 1; k++) {
|
|
5872
|
+
draw(this.bloomUpViews[levels - 2 - k], this.bloomUpsamplePipeline, this.bloomUpsampleBindGroups[k]);
|
|
5873
|
+
}
|
|
5874
|
+
this.bloomPassDescriptor.timestampWrites = undefined;
|
|
5701
5875
|
}
|
|
5702
5876
|
// Step 1: Get WebGPU device and context
|
|
5703
5877
|
async init() {
|
|
@@ -5822,7 +5996,17 @@ export class Engine {
|
|
|
5822
5996
|
this.setupCamera();
|
|
5823
5997
|
this.setupLighting();
|
|
5824
5998
|
const tPipelines = performance.now();
|
|
5999
|
+
// Built async and side by side (see initRenderPipeline): a synchronous
|
|
6000
|
+
// build holds the GPU process for the whole compile, one after another,
|
|
6001
|
+
// which was a 2–3 s frozen frame on a cold open.
|
|
6002
|
+
this.initPipelineJobs = [];
|
|
5825
6003
|
this.createPipelines();
|
|
6004
|
+
try {
|
|
6005
|
+
await Promise.all(this.initPipelineJobs);
|
|
6006
|
+
}
|
|
6007
|
+
finally {
|
|
6008
|
+
this.initPipelineJobs = null;
|
|
6009
|
+
}
|
|
5826
6010
|
this.setupResize();
|
|
5827
6011
|
const ms = (a, b) => Math.round(b - a);
|
|
5828
6012
|
console.info(`[reze] init ${ms(t0, performance.now())}ms — adapter ${ms(t0, tAdapter)}ms · ` +
|
|
@@ -5864,187 +6048,12 @@ export class Engine {
|
|
|
5864
6048
|
pass.end();
|
|
5865
6049
|
this.device.queue.submit([encoder.finish()]);
|
|
5866
6050
|
}
|
|
5867
|
-
bakeBrdfLut() {
|
|
5868
|
-
if (BRDF_LUT_SIZE !== LTC_MAG_LUT_SIZE) {
|
|
5869
|
-
throw new Error("BRDF LUT bake requires DFG size == LTC size (both 64).");
|
|
5870
|
-
}
|
|
5871
|
-
// Temp rg16float LTC source — loaded 1:1 by the bake fragment shader, then dropped.
|
|
5872
|
-
const ltcTemp = this.device.createTexture({
|
|
5873
|
-
label: "LTC mag LUT (bake input)",
|
|
5874
|
-
size: [LTC_MAG_LUT_SIZE, LTC_MAG_LUT_SIZE],
|
|
5875
|
-
format: "rg16float",
|
|
5876
|
-
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
|
|
5877
|
-
});
|
|
5878
|
-
const n = LTC_MAG_LUT_DATA.length;
|
|
5879
|
-
const half = new Uint16Array(n);
|
|
5880
|
-
const f32 = new Float32Array(1);
|
|
5881
|
-
const u32 = new Uint32Array(f32.buffer);
|
|
5882
|
-
for (let i = 0; i < n; i++) {
|
|
5883
|
-
f32[0] = LTC_MAG_LUT_DATA[i];
|
|
5884
|
-
const x = u32[0];
|
|
5885
|
-
const sign = (x >>> 16) & 0x8000;
|
|
5886
|
-
let exp = ((x >>> 23) & 0xff) - 127 + 15;
|
|
5887
|
-
const mant = x & 0x7fffff;
|
|
5888
|
-
if (exp <= 0) {
|
|
5889
|
-
half[i] = sign;
|
|
5890
|
-
}
|
|
5891
|
-
else if (exp >= 31) {
|
|
5892
|
-
half[i] = sign | 0x7c00;
|
|
5893
|
-
}
|
|
5894
|
-
else {
|
|
5895
|
-
half[i] = sign | (exp << 10) | (mant >>> 13);
|
|
5896
|
-
}
|
|
5897
|
-
}
|
|
5898
|
-
this.device.queue.writeTexture({ texture: ltcTemp }, half, { bytesPerRow: LTC_MAG_LUT_SIZE * 4, rowsPerImage: LTC_MAG_LUT_SIZE }, { width: LTC_MAG_LUT_SIZE, height: LTC_MAG_LUT_SIZE, depthOrArrayLayers: 1 });
|
|
5899
|
-
this.brdfLutTexture = this.device.createTexture({
|
|
5900
|
-
label: "BRDF LUT (DFG + LTC packed)",
|
|
5901
|
-
size: [BRDF_LUT_SIZE, BRDF_LUT_SIZE],
|
|
5902
|
-
format: "rgba8unorm",
|
|
5903
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
5904
|
-
});
|
|
5905
|
-
this.brdfLutView = this.brdfLutTexture.createView();
|
|
5906
|
-
const module = this.device.createShaderModule({ label: "BRDF LUT bake", code: BRDF_LUT_BAKE_WGSL });
|
|
5907
|
-
const pipeline = this.device.createRenderPipeline({
|
|
5908
|
-
label: "BRDF LUT bake pipeline",
|
|
5909
|
-
layout: "auto",
|
|
5910
|
-
vertex: { module, entryPoint: "vs" },
|
|
5911
|
-
fragment: { module, entryPoint: "fs", targets: [{ format: "rgba8unorm" }] },
|
|
5912
|
-
primitive: { topology: "triangle-list" },
|
|
5913
|
-
});
|
|
5914
|
-
const bakeBindGroup = this.device.createBindGroup({
|
|
5915
|
-
label: "BRDF LUT bake bind group",
|
|
5916
|
-
layout: pipeline.getBindGroupLayout(0),
|
|
5917
|
-
entries: [{ binding: 0, resource: ltcTemp.createView() }],
|
|
5918
|
-
});
|
|
5919
|
-
const enc = this.device.createCommandEncoder({ label: "BRDF LUT bake encoder" });
|
|
5920
|
-
const pass = enc.beginRenderPass({
|
|
5921
|
-
colorAttachments: [
|
|
5922
|
-
{ view: this.brdfLutView, clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: "clear", storeOp: "store" },
|
|
5923
|
-
],
|
|
5924
|
-
});
|
|
5925
|
-
pass.setPipeline(pipeline);
|
|
5926
|
-
pass.setBindGroup(0, bakeBindGroup);
|
|
5927
|
-
pass.draw(3, 1, 0, 0);
|
|
5928
|
-
pass.end();
|
|
5929
|
-
this.device.queue.submit([enc.finish()]);
|
|
5930
|
-
ltcTemp.destroy();
|
|
5931
|
-
}
|
|
5932
|
-
// Bake the Blender 3.6 Filmic MHC tone curve into a WIDTH×1 r16float LUT sampled by the
|
|
5933
|
-
// composite pass. The 14 anchors are the same as the old inline array; we fit a monotone
|
|
5934
|
-
// cubic (Fritsch–Carlson) through them so the curve is C1-continuous (no Mach banding in
|
|
5935
|
-
// smooth gradients) while still passing through every anchor (look preserved) and staying
|
|
5936
|
-
// monotone (no tonemap overshoot/ringing). Domain is uniform in log2 space: anchor k sits
|
|
5937
|
-
// at t=k, k=0..13 (t = log2(linear)+10). See composite.ts::filmic for the sampling map.
|
|
5938
|
-
/**
|
|
5939
|
-
* Decompress and upload Blender's AgX cube.
|
|
5940
|
-
*
|
|
5941
|
-
* Deliberately off the critical path: it is 723 KB once inflated, and a frame
|
|
5942
|
-
* rendered before it lands should show the scene under whatever transform is
|
|
5943
|
-
* already there rather than wait. Until then binding 10 holds a 1×1×1 stand-in,
|
|
5944
|
-
* which is only ever sampled if someone selects AgX in that window.
|
|
5945
|
-
*/
|
|
5946
|
-
async loadAgxLut() {
|
|
5947
|
-
try {
|
|
5948
|
-
const packed = Uint8Array.from(atob(AGX_LUT_GZ), (ch) => ch.charCodeAt(0));
|
|
5949
|
-
const stream = new Blob([packed]).stream().pipeThrough(new DecompressionStream("gzip"));
|
|
5950
|
-
const bytes = new Uint8Array(await new Response(stream).arrayBuffer());
|
|
5951
|
-
const n = AGX_LUT_SIZE;
|
|
5952
|
-
if (bytes.byteLength !== n * n * n * 4)
|
|
5953
|
-
throw new Error(`AgX LUT is ${bytes.byteLength} bytes, expected ${n ** 3 * 4}`);
|
|
5954
|
-
const tex = this.device.createTexture({
|
|
5955
|
-
label: "AgX 57³ LUT",
|
|
5956
|
-
size: [n, n, n],
|
|
5957
|
-
dimension: "3d",
|
|
5958
|
-
format: "rgb10a2unorm",
|
|
5959
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
5960
|
-
});
|
|
5961
|
-
// .cube order is red fastest, which is exactly a 3D texture's own layout.
|
|
5962
|
-
this.device.queue.writeTexture({ texture: tex }, bytes, { bytesPerRow: n * 4, rowsPerImage: n }, [n, n, n]);
|
|
5963
|
-
this.agxLutTexture = tex;
|
|
5964
|
-
this.rebuildCompositeBindGroup();
|
|
5965
|
-
}
|
|
5966
|
-
catch {
|
|
5967
|
-
// A missing LUT costs AgX, not the renderer — the other transforms stand.
|
|
5968
|
-
}
|
|
5969
|
-
}
|
|
5970
|
-
bakeFilmicLut() {
|
|
5971
|
-
const anchors = [
|
|
5972
|
-
0.0028, 0.0068, 0.0151, 0.0313, 0.061, 0.112, 0.192, 0.306, 0.459, 0.631, 0.82, 0.907, 0.962, 0.989,
|
|
5973
|
-
];
|
|
5974
|
-
const n = anchors.length;
|
|
5975
|
-
// Secant slopes (unit spacing, so d_k = y_{k+1} - y_k).
|
|
5976
|
-
const d = new Array(n - 1);
|
|
5977
|
-
for (let k = 0; k < n - 1; k++)
|
|
5978
|
-
d[k] = anchors[k + 1] - anchors[k];
|
|
5979
|
-
// Endpoint + interior tangents.
|
|
5980
|
-
const m = new Array(n);
|
|
5981
|
-
m[0] = d[0];
|
|
5982
|
-
m[n - 1] = d[n - 2];
|
|
5983
|
-
for (let k = 1; k < n - 1; k++)
|
|
5984
|
-
m[k] = (d[k - 1] + d[k]) * 0.5;
|
|
5985
|
-
// Fritsch–Carlson monotonicity clamp.
|
|
5986
|
-
for (let k = 0; k < n - 1; k++) {
|
|
5987
|
-
if (d[k] === 0) {
|
|
5988
|
-
m[k] = 0;
|
|
5989
|
-
m[k + 1] = 0;
|
|
5990
|
-
continue;
|
|
5991
|
-
}
|
|
5992
|
-
const a = m[k] / d[k];
|
|
5993
|
-
const b = m[k + 1] / d[k];
|
|
5994
|
-
const s = a * a + b * b;
|
|
5995
|
-
if (s > 9) {
|
|
5996
|
-
const tau = 3 / Math.sqrt(s);
|
|
5997
|
-
m[k] = tau * a * d[k];
|
|
5998
|
-
m[k + 1] = tau * b * d[k];
|
|
5999
|
-
}
|
|
6000
|
-
}
|
|
6001
|
-
const W = Engine.FILMIC_LUT_WIDTH;
|
|
6002
|
-
const values = new Float32Array(W);
|
|
6003
|
-
for (let j = 0; j < W; j++) {
|
|
6004
|
-
const t = ((n - 1) * j) / (W - 1); // t ∈ [0, n-1]
|
|
6005
|
-
const k = Math.min(Math.floor(t), n - 2);
|
|
6006
|
-
const s = t - k; // local param in [0,1], unit-spaced segment
|
|
6007
|
-
const s2 = s * s;
|
|
6008
|
-
const s3 = s2 * s;
|
|
6009
|
-
// Hermite basis (h=1).
|
|
6010
|
-
const h00 = 2 * s3 - 3 * s2 + 1;
|
|
6011
|
-
const h10 = s3 - 2 * s2 + s;
|
|
6012
|
-
const h01 = -2 * s3 + 3 * s2;
|
|
6013
|
-
const h11 = s3 - s2;
|
|
6014
|
-
values[j] = h00 * anchors[k] + h10 * m[k] + h01 * anchors[k + 1] + h11 * m[k + 1];
|
|
6015
|
-
}
|
|
6016
|
-
// f32 → f16 bits (same conversion as bakeBrdfLut).
|
|
6017
|
-
const half = new Uint16Array(W);
|
|
6018
|
-
const f32 = new Float32Array(1);
|
|
6019
|
-
const u32 = new Uint32Array(f32.buffer);
|
|
6020
|
-
for (let j = 0; j < W; j++) {
|
|
6021
|
-
f32[0] = values[j];
|
|
6022
|
-
const x = u32[0];
|
|
6023
|
-
const sign = (x >>> 16) & 0x8000;
|
|
6024
|
-
const exp = ((x >>> 23) & 0xff) - 127 + 15;
|
|
6025
|
-
const mant = x & 0x7fffff;
|
|
6026
|
-
if (exp <= 0) {
|
|
6027
|
-
half[j] = sign;
|
|
6028
|
-
}
|
|
6029
|
-
else if (exp >= 31) {
|
|
6030
|
-
half[j] = sign | 0x7c00;
|
|
6031
|
-
}
|
|
6032
|
-
else {
|
|
6033
|
-
half[j] = sign | (exp << 10) | (mant >>> 13);
|
|
6034
|
-
}
|
|
6035
|
-
}
|
|
6036
|
-
this.filmicLutTexture = this.device.createTexture({
|
|
6037
|
-
label: "Filmic tone LUT",
|
|
6038
|
-
size: [W, 1],
|
|
6039
|
-
format: "r16float",
|
|
6040
|
-
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
|
|
6041
|
-
});
|
|
6042
|
-
this.filmicLutView = this.filmicLutTexture.createView();
|
|
6043
|
-
this.device.queue.writeTexture({ texture: this.filmicLutTexture }, half, { bytesPerRow: W * 2, rowsPerImage: 1 }, { width: W, height: 1, depthOrArrayLayers: 1 });
|
|
6044
|
-
}
|
|
6045
6051
|
createRenderPipeline(config) {
|
|
6052
|
+
return this.createModelPipeline(this.renderPipelineDesc(config));
|
|
6053
|
+
}
|
|
6054
|
+
renderPipelineDesc(config) {
|
|
6046
6055
|
const targets = config.fragmentTargets ?? (config.fragmentTarget ? [config.fragmentTarget] : undefined);
|
|
6047
|
-
return
|
|
6056
|
+
return {
|
|
6048
6057
|
label: config.label,
|
|
6049
6058
|
layout: config.layout,
|
|
6050
6059
|
vertex: {
|
|
@@ -6061,7 +6070,7 @@ export class Engine {
|
|
|
6061
6070
|
primitive: { cullMode: config.cullMode ?? "none" },
|
|
6062
6071
|
depthStencil: config.depthStencil,
|
|
6063
6072
|
multisample: config.multisample ?? { count: Engine.MULTISAMPLE_COUNT },
|
|
6064
|
-
}
|
|
6073
|
+
};
|
|
6065
6074
|
}
|
|
6066
6075
|
createPipelines() {
|
|
6067
6076
|
this.materialSampler = this.device.createSampler({
|
|
@@ -6179,8 +6188,6 @@ export class Engine {
|
|
|
6179
6188
|
// creation with "binding doesn't exist", naming a binding no effect
|
|
6180
6189
|
// author ever wrote. Sharing the code meant sharing these.
|
|
6181
6190
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
6182
|
-
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
6183
|
-
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "3d" } },
|
|
6184
6191
|
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "3d" } },
|
|
6185
6192
|
// And the scene's COVERAGE and bloom, without which the tap cannot
|
|
6186
6193
|
// reconstruct a pixel: the HDR target is premultiplied, so colour alone
|
|
@@ -6201,7 +6208,7 @@ export class Engine {
|
|
|
6201
6208
|
const seedModule = this.device.createShaderModule({ label: "cast distance seed", code: buildCastSeedShader(Engine.MULTISAMPLE_COUNT) });
|
|
6202
6209
|
const stepModule = this.device.createShaderModule({ label: "cast distance step", code: buildCastStepShader() });
|
|
6203
6210
|
const resolveModule = this.device.createShaderModule({ label: "cast distance resolve", code: buildCastResolveShader() });
|
|
6204
|
-
this.
|
|
6211
|
+
this.initRenderPipeline({
|
|
6205
6212
|
label: "cast distance seed",
|
|
6206
6213
|
layout: "auto",
|
|
6207
6214
|
vertex: { module: seedModule, entryPoint: "vs" },
|
|
@@ -6211,21 +6218,21 @@ export class Engine {
|
|
|
6211
6218
|
targets: [{ format: CAST_SEED_FORMAT }, { format: CAST_COVERAGE_FORMAT }],
|
|
6212
6219
|
},
|
|
6213
6220
|
primitive: { topology: "triangle-list" },
|
|
6214
|
-
});
|
|
6215
|
-
this.
|
|
6221
|
+
}, (p) => (this.castSeedPipeline = p));
|
|
6222
|
+
this.initRenderPipeline({
|
|
6216
6223
|
label: "cast distance step",
|
|
6217
6224
|
layout: "auto",
|
|
6218
6225
|
vertex: { module: stepModule, entryPoint: "vs" },
|
|
6219
6226
|
fragment: { module: stepModule, entryPoint: "fs", targets: [{ format: CAST_SEED_FORMAT }] },
|
|
6220
6227
|
primitive: { topology: "triangle-list" },
|
|
6221
|
-
});
|
|
6222
|
-
this.
|
|
6228
|
+
}, (p) => (this.castStepPipeline = p));
|
|
6229
|
+
this.initRenderPipeline({
|
|
6223
6230
|
label: "cast distance resolve",
|
|
6224
6231
|
layout: "auto",
|
|
6225
6232
|
vertex: { module: resolveModule, entryPoint: "vs" },
|
|
6226
6233
|
fragment: { module: resolveModule, entryPoint: "fs", targets: [{ format: CAST_DIST_FORMAT }] },
|
|
6227
6234
|
primitive: { topology: "triangle-list" },
|
|
6228
|
-
});
|
|
6235
|
+
}, (p) => (this.castResolvePipeline = p));
|
|
6229
6236
|
// 65504 is the largest half float. Bound wherever the field is not
|
|
6230
6237
|
// running, so rzCastDistance answers "unreachably far" and an effect keyed
|
|
6231
6238
|
// on it draws nothing at all.
|
|
@@ -6321,7 +6328,22 @@ export class Engine {
|
|
|
6321
6328
|
// The same attachments blended as light — for a group that declared
|
|
6322
6329
|
// blend: "additive". See scene-contract's material-additive.
|
|
6323
6330
|
this.sceneTargetsAdditive = sceneTargetsFor("material-additive", this.sceneFormats);
|
|
6331
|
+
this.sceneTargetsPremultiplied = sceneTargetsFor("material-premultiplied", this.sceneFormats);
|
|
6324
6332
|
this.fullVertexBufferLayouts = fullVertexBuffers;
|
|
6333
|
+
// Slot 3 stays empty: the outline hull binds its own stream there between
|
|
6334
|
+
// two material draws, and a material reading slot 3 would pick it up.
|
|
6335
|
+
const materialVertexBuffers = [
|
|
6336
|
+
...fullVertexBuffers,
|
|
6337
|
+
null,
|
|
6338
|
+
{
|
|
6339
|
+
arrayStride: Engine.ATTRIBUTE_FLOATS * 4,
|
|
6340
|
+
attributes: [
|
|
6341
|
+
{ shaderLocation: 5, offset: 0, format: "float32x4" },
|
|
6342
|
+
{ shaderLocation: 6, offset: 4 * 4, format: "float32x2" },
|
|
6343
|
+
],
|
|
6344
|
+
},
|
|
6345
|
+
];
|
|
6346
|
+
this.materialVertexBufferLayouts = materialVertexBuffers;
|
|
6325
6347
|
// group 0: per-frame (camera + light + sampler + shadow) — bound once per pass
|
|
6326
6348
|
this.mainPerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
6327
6349
|
label: "main per-frame bind group layout",
|
|
@@ -6336,14 +6358,19 @@ export class Engine {
|
|
|
6336
6358
|
// The positional lights. Always bound, empty or not, so every material
|
|
6337
6359
|
// pipeline shares one layout whether or not the scene has any.
|
|
6338
6360
|
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
6339
|
-
// The world's sky (8)
|
|
6340
|
-
//
|
|
6341
|
-
//
|
|
6342
|
-
// which the atlas at 3 replaces.)
|
|
6361
|
+
// The world's sky (8). Always bound — the 1x1 fallback when the scene
|
|
6362
|
+
// has none — so every material pipeline keeps sharing one layout. (7
|
|
6363
|
+
// was the far cascade's map, which the atlas at 3 replaces.)
|
|
6343
6364
|
{ binding: 8, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
6344
|
-
|
|
6365
|
+
// The spot lamps' cookies (loadLightCookie), a 1x1 white until one loads.
|
|
6366
|
+
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "2d-array" } },
|
|
6345
6367
|
// The cast's shadow on a stage — setStageCastShadow.
|
|
6346
6368
|
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
6369
|
+
// The stage's reflection probe and where it projects — see
|
|
6370
|
+
// captureReflectionProbe. Always bound: a 1x1 cube and the off flag
|
|
6371
|
+
// when there is no stage.
|
|
6372
|
+
{ binding: 11, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "cube" } },
|
|
6373
|
+
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
6347
6374
|
],
|
|
6348
6375
|
});
|
|
6349
6376
|
// group 1: per-instance (skinMats) — bound once per model
|
|
@@ -6407,12 +6434,12 @@ export class Engine {
|
|
|
6407
6434
|
const neutral = compileGraph(DEFAULT_GRAPH, { renderClass: "auto", alphaMode: "opaque" });
|
|
6408
6435
|
if (!neutral.ok)
|
|
6409
6436
|
throw new Error("failed to compile the neutral default graph");
|
|
6410
|
-
const neutralModule = this.
|
|
6411
|
-
this.
|
|
6437
|
+
const neutralModule = this.cachedShaderModule(neutral.wgsl, "neutral base (default graph)");
|
|
6438
|
+
this.initRenderPipeline(this.renderPipelineDesc({
|
|
6412
6439
|
label: "neutral base pipeline",
|
|
6413
6440
|
layout: mainPipelineLayout,
|
|
6414
6441
|
shaderModule: neutralModule,
|
|
6415
|
-
vertexBuffers:
|
|
6442
|
+
vertexBuffers: materialVertexBuffers,
|
|
6416
6443
|
fragmentTargets: sceneTargets,
|
|
6417
6444
|
cullMode: "none",
|
|
6418
6445
|
depthStencil: {
|
|
@@ -6420,13 +6447,13 @@ export class Engine {
|
|
|
6420
6447
|
depthWriteEnabled: true,
|
|
6421
6448
|
depthCompare: this.depthAhead,
|
|
6422
6449
|
},
|
|
6423
|
-
});
|
|
6450
|
+
}), (p) => (this.neutralPipeline = p), true);
|
|
6424
6451
|
// Depth-write-off twin for transparent-bucket draws (see pipelineForDrawCall).
|
|
6425
|
-
this.
|
|
6452
|
+
this.initRenderPipeline(this.renderPipelineDesc({
|
|
6426
6453
|
label: "neutral base pipeline (no depth write)",
|
|
6427
6454
|
layout: mainPipelineLayout,
|
|
6428
6455
|
shaderModule: neutralModule,
|
|
6429
|
-
vertexBuffers:
|
|
6456
|
+
vertexBuffers: materialVertexBuffers,
|
|
6430
6457
|
fragmentTargets: sceneTargets,
|
|
6431
6458
|
cullMode: "none",
|
|
6432
6459
|
depthStencil: {
|
|
@@ -6434,7 +6461,7 @@ export class Engine {
|
|
|
6434
6461
|
depthWriteEnabled: false,
|
|
6435
6462
|
depthCompare: this.depthAhead,
|
|
6436
6463
|
},
|
|
6437
|
-
});
|
|
6464
|
+
}), (p) => (this.neutralPipelineNoDepthWrite = p), true);
|
|
6438
6465
|
// Depth-only prepass for transparent draws (see depth-prepass.ts): writes the
|
|
6439
6466
|
// fabric's depth AFTER its color blended, so outlines drawn later are
|
|
6440
6467
|
// occluded behind it. Color targets kept for pass compatibility, writeMask 0.
|
|
@@ -6453,7 +6480,7 @@ export class Engine {
|
|
|
6453
6480
|
depthCompare: this.depthAhead,
|
|
6454
6481
|
},
|
|
6455
6482
|
};
|
|
6456
|
-
this.
|
|
6483
|
+
this.initRenderPipeline({
|
|
6457
6484
|
label: "opaque depth prepass",
|
|
6458
6485
|
...prepassDesc,
|
|
6459
6486
|
fragment: {
|
|
@@ -6461,11 +6488,11 @@ export class Engine {
|
|
|
6461
6488
|
entryPoint: "fs",
|
|
6462
6489
|
targets: sceneTargetsFor("depth-prepass", this.sceneFormats),
|
|
6463
6490
|
},
|
|
6464
|
-
});
|
|
6491
|
+
}, (p) => (this.depthPrepassPipeline = p), true);
|
|
6465
6492
|
// The SOLID prime: same module, cutoff forced to exactly 1.0. Only texels
|
|
6466
6493
|
// whose blend ignores the destination may pre-claim depth in the
|
|
6467
6494
|
// transparent phase — see the override's note in depth-prepass.ts.
|
|
6468
|
-
this.
|
|
6495
|
+
this.initRenderPipeline({
|
|
6469
6496
|
label: "transparent solid prepass",
|
|
6470
6497
|
...prepassDesc,
|
|
6471
6498
|
fragment: {
|
|
@@ -6474,14 +6501,14 @@ export class Engine {
|
|
|
6474
6501
|
constants: { CUTOFF: 1.0 },
|
|
6475
6502
|
targets: sceneTargetsFor("depth-prepass", this.sceneFormats),
|
|
6476
6503
|
},
|
|
6477
|
-
});
|
|
6504
|
+
}, (p) => (this.solidPrepassPipeline = p), true);
|
|
6478
6505
|
// The HAIR prime: solid texels only, and stencil-fenced off the eye
|
|
6479
6506
|
// silhouette. It records after the non-hair opaque draws, so the eye has
|
|
6480
6507
|
// already written its stencil — not-equal here is what keeps the primed
|
|
6481
6508
|
// hair depth from ever claiming the pixels the see-through-hair pass needs
|
|
6482
6509
|
// the eye to survive on. (Bundle draws use the PASS's stencil reference;
|
|
6483
6510
|
// only pipeline/bind/vertex state resets across executeBundles.)
|
|
6484
|
-
this.
|
|
6511
|
+
this.initRenderPipeline({
|
|
6485
6512
|
label: "hair depth prime",
|
|
6486
6513
|
...prepassDesc,
|
|
6487
6514
|
depthStencil: {
|
|
@@ -6497,7 +6524,7 @@ export class Engine {
|
|
|
6497
6524
|
constants: { CUTOFF: 1.0 },
|
|
6498
6525
|
targets: sceneTargetsFor("depth-prepass", this.sceneFormats),
|
|
6499
6526
|
},
|
|
6500
|
-
});
|
|
6527
|
+
}, (p) => (this.hairPrimePipeline = p), true);
|
|
6501
6528
|
// The matrices, then one vec4: the caster sphere, which effects' rzShadow
|
|
6502
6529
|
// tests before any tap. The materials and the ground bind the matrices only.
|
|
6503
6530
|
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
@@ -6521,7 +6548,7 @@ export class Engine {
|
|
|
6521
6548
|
label: "shadow depth",
|
|
6522
6549
|
code: SHADOW_DEPTH_SHADER_WGSL,
|
|
6523
6550
|
});
|
|
6524
|
-
this.
|
|
6551
|
+
this.initRenderPipeline({
|
|
6525
6552
|
label: "shadow depth pipeline",
|
|
6526
6553
|
// Group 1 is the main pass's per-material layout so each shadow draw can
|
|
6527
6554
|
// rebind the draw call's existing material bind group for the alpha test.
|
|
@@ -6542,7 +6569,7 @@ export class Engine {
|
|
|
6542
6569
|
depthBiasSlopeScale: 1.5,
|
|
6543
6570
|
depthBiasClamp: 0,
|
|
6544
6571
|
},
|
|
6545
|
-
});
|
|
6572
|
+
}, (p) => (this.shadowDepthPipeline = p));
|
|
6546
6573
|
this.shadowComparisonSampler = this.device.createSampler({
|
|
6547
6574
|
compare: "less",
|
|
6548
6575
|
magFilter: "linear",
|
|
@@ -6574,16 +6601,21 @@ export class Engine {
|
|
|
6574
6601
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
6575
6602
|
});
|
|
6576
6603
|
// One-shot bake of Blender EEVEE's combined BRDF LUT (DFG + LTC packed rgba8unorm).
|
|
6577
|
-
this.bakeBrdfLut();
|
|
6578
6604
|
this.bakeGroundNoise();
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6605
|
+
// The mipmap blit for the formats model textures arrive in, built here so
|
|
6606
|
+
// the first texture of the first model does not compile it synchronously
|
|
6607
|
+
// in the middle of a load (generateMipmaps builds any other on demand).
|
|
6608
|
+
{
|
|
6609
|
+
const mipModule = this.device.createShaderModule({ label: "mipmap blit", code: MIPMAP_BLIT_SHADER_WGSL });
|
|
6610
|
+
for (const format of ["rgba8unorm", "rgba8unorm-srgb"])
|
|
6611
|
+
this.initRenderPipeline({
|
|
6612
|
+
label: `mipmap blit pipeline (${format})`,
|
|
6613
|
+
layout: "auto",
|
|
6614
|
+
vertex: { module: mipModule, entryPoint: "vs" },
|
|
6615
|
+
fragment: { module: mipModule, entryPoint: "fs", targets: [{ format }] },
|
|
6616
|
+
primitive: { topology: "triangle-list" },
|
|
6617
|
+
}, (p) => this.mipBlitPipelines.set(format, p));
|
|
6618
|
+
}
|
|
6587
6619
|
this.stageGradeFallback = this.device.createTexture({
|
|
6588
6620
|
label: "stage grade fallback",
|
|
6589
6621
|
size: [1, 1, 1],
|
|
@@ -6592,7 +6624,6 @@ export class Engine {
|
|
|
6592
6624
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
6593
6625
|
});
|
|
6594
6626
|
this.uploadStageGrade();
|
|
6595
|
-
this.bakeFilmicLut();
|
|
6596
6627
|
// BEFORE the bind group below, which binds it. Full size from the start:
|
|
6597
6628
|
// every material pipeline binds this, so sizing it to the light count would
|
|
6598
6629
|
// mean rebuilding bind groups whenever a scene gained a lamp. Uploaded from
|
|
@@ -6622,6 +6653,48 @@ export class Engine {
|
|
|
6622
6653
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
6623
6654
|
});
|
|
6624
6655
|
this.fallbackEquirectView = this.fallbackEquirectTexture.createView();
|
|
6656
|
+
// The reflection probe's resources, bound from the first frame: the cube
|
|
6657
|
+
// (empty and switched off until a stage is captured), its stand-in and the
|
|
6658
|
+
// uniforms. Before the per-frame groups, which bind all of them.
|
|
6659
|
+
this.probeTexture = this.device.createTexture({
|
|
6660
|
+
label: "reflection probe",
|
|
6661
|
+
size: [PROBE_SIZE, PROBE_SIZE, 6],
|
|
6662
|
+
format: "rgba16float",
|
|
6663
|
+
mipLevelCount: PROBE_LEVELS,
|
|
6664
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
6665
|
+
});
|
|
6666
|
+
this.probeView = this.probeTexture.createView({ dimension: "cube" });
|
|
6667
|
+
this.probeFallbackView = this.device
|
|
6668
|
+
.createTexture({
|
|
6669
|
+
label: "reflection probe stand-in",
|
|
6670
|
+
size: [1, 1, 6],
|
|
6671
|
+
format: "rgba16float",
|
|
6672
|
+
usage: GPUTextureUsage.TEXTURE_BINDING,
|
|
6673
|
+
})
|
|
6674
|
+
.createView({ dimension: "cube" });
|
|
6675
|
+
this.cookieTexture = this.device.createTexture({
|
|
6676
|
+
label: "light cookies (none yet)",
|
|
6677
|
+
size: [1, 1, 1],
|
|
6678
|
+
format: "rgba8unorm",
|
|
6679
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
6680
|
+
});
|
|
6681
|
+
this.device.queue.writeTexture({ texture: this.cookieTexture }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1, 1]);
|
|
6682
|
+
this.cookieView = this.cookieTexture.createView({ dimension: "2d-array" });
|
|
6683
|
+
this.probeUniformBuffer = this.device.createBuffer({
|
|
6684
|
+
label: "reflection probe uniforms",
|
|
6685
|
+
size: 48,
|
|
6686
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
6687
|
+
});
|
|
6688
|
+
this.probeOffBuffer = this.device.createBuffer({
|
|
6689
|
+
label: "reflection probe uniforms (off)",
|
|
6690
|
+
size: 48,
|
|
6691
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
6692
|
+
});
|
|
6693
|
+
this.probeCameraBuffer = this.device.createBuffer({
|
|
6694
|
+
label: "reflection probe capture camera",
|
|
6695
|
+
size: 40 * 4,
|
|
6696
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
6697
|
+
});
|
|
6625
6698
|
// Zero-filled = zero lines, which every accessor answers gracefully.
|
|
6626
6699
|
this.lyricsBuffer = this.device.createBuffer({
|
|
6627
6700
|
label: "lyrics",
|
|
@@ -6666,6 +6739,8 @@ export class Engine {
|
|
|
6666
6739
|
{ binding: 13, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
6667
6740
|
// The mirror's plane, and whether this draw is inside the mirror pass.
|
|
6668
6741
|
{ binding: 14, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
6742
|
+
// The spot lamps' cookies — the floor takes their patterns too.
|
|
6743
|
+
{ binding: 15, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "2d-array" } },
|
|
6669
6744
|
],
|
|
6670
6745
|
});
|
|
6671
6746
|
this.groundShadowPipelineDesc = {
|
|
@@ -6679,7 +6754,7 @@ export class Engine {
|
|
|
6679
6754
|
cullMode: "back",
|
|
6680
6755
|
depthStencil: { format: this.depthFormat, depthWriteEnabled: true, depthCompare: this.depthAhead },
|
|
6681
6756
|
};
|
|
6682
|
-
this.
|
|
6757
|
+
this.initRenderPipeline(this.renderPipelineDesc(this.groundPipelineConfig(false)), (p) => (this.groundShadowPipeline = p));
|
|
6683
6758
|
// Outline: group 0 = per-frame (camera), group 1 = per-instance (skinMats), group 2 = per-material (edge uniforms)
|
|
6684
6759
|
this.outlinePerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
6685
6760
|
label: "outline per-frame bind group layout",
|
|
@@ -6742,7 +6817,12 @@ export class Engine {
|
|
|
6742
6817
|
label: "outline pipeline",
|
|
6743
6818
|
layout: outlinePipelineLayout,
|
|
6744
6819
|
shaderModule: outlineShaderModule,
|
|
6745
|
-
|
|
6820
|
+
// The selection mask's streams plus the hull's own: smoothed normal and
|
|
6821
|
+
// PMX edge scale (ModelInstance.outlineVertexBuffer).
|
|
6822
|
+
vertexBuffers: [
|
|
6823
|
+
...outlineVertexBuffers,
|
|
6824
|
+
{ arrayStride: 4 * 4, attributes: [{ shaderLocation: 5, offset: 0, format: "float32x4" }] },
|
|
6825
|
+
],
|
|
6746
6826
|
fragmentTargets: sceneTargetsFor("outline", this.sceneFormats),
|
|
6747
6827
|
cullMode: "back",
|
|
6748
6828
|
depthStencil: {
|
|
@@ -6776,12 +6856,12 @@ export class Engine {
|
|
|
6776
6856
|
stencilWriteMask: 0,
|
|
6777
6857
|
},
|
|
6778
6858
|
};
|
|
6779
|
-
this.
|
|
6780
|
-
this.
|
|
6859
|
+
this.initRenderPipeline(this.renderPipelineDesc(outlineDesc), (p) => (this.outlinePipeline = p), true);
|
|
6860
|
+
this.initRenderPipeline(this.renderPipelineDesc({
|
|
6781
6861
|
...outlineDesc,
|
|
6782
6862
|
label: "outline pipeline (mirror)",
|
|
6783
6863
|
cullMode: "front",
|
|
6784
|
-
});
|
|
6864
|
+
}), (p) => (this.outlineMirrorPipeline = p), true);
|
|
6785
6865
|
// ─── Selection overlay (screen-space edge-detect on a per-material mask) ───
|
|
6786
6866
|
// Reuses outline camera + main skinMats bind group layouts. No group 2 (no per-mat uniform).
|
|
6787
6867
|
const selectionMaskPipelineLayout = this.device.createPipelineLayout({
|
|
@@ -6792,7 +6872,7 @@ export class Engine {
|
|
|
6792
6872
|
label: "selection mask shader",
|
|
6793
6873
|
code: SELECTION_MASK_SHADER_WGSL,
|
|
6794
6874
|
});
|
|
6795
|
-
this.
|
|
6875
|
+
this.initRenderPipeline({
|
|
6796
6876
|
label: "selection mask pipeline",
|
|
6797
6877
|
layout: selectionMaskPipelineLayout,
|
|
6798
6878
|
vertex: { module: selectionMaskShaderModule, entryPoint: "vs", buffers: outlineVertexBuffers },
|
|
@@ -6804,7 +6884,7 @@ export class Engine {
|
|
|
6804
6884
|
primitive: { cullMode: "none" },
|
|
6805
6885
|
// Single-sample, no depth (depth-always via not attaching a depth buffer at all).
|
|
6806
6886
|
multisample: { count: 1 },
|
|
6807
|
-
});
|
|
6887
|
+
}, (p) => (this.selectionMaskPipeline = p));
|
|
6808
6888
|
this.selectionEdgeBindGroupLayout = this.device.createBindGroupLayout({
|
|
6809
6889
|
label: "selection edge bind group layout",
|
|
6810
6890
|
entries: [
|
|
@@ -6821,7 +6901,7 @@ export class Engine {
|
|
|
6821
6901
|
label: "selection edge shader",
|
|
6822
6902
|
code: SELECTION_EDGE_SHADER_WGSL,
|
|
6823
6903
|
});
|
|
6824
|
-
this.
|
|
6904
|
+
this.initRenderPipeline({
|
|
6825
6905
|
label: "selection edge pipeline",
|
|
6826
6906
|
layout: selectionEdgePipelineLayout,
|
|
6827
6907
|
vertex: { module: selectionEdgeShaderModule, entryPoint: "vs" },
|
|
@@ -6840,7 +6920,7 @@ export class Engine {
|
|
|
6840
6920
|
},
|
|
6841
6921
|
primitive: { topology: "triangle-list" },
|
|
6842
6922
|
multisample: { count: 1 },
|
|
6843
|
-
});
|
|
6923
|
+
}, (p) => (this.selectionEdgePipeline = p));
|
|
6844
6924
|
this.selectionSampler = this.device.createSampler({
|
|
6845
6925
|
label: "selection sampler",
|
|
6846
6926
|
magFilter: "linear",
|
|
@@ -6857,10 +6937,7 @@ export class Engine {
|
|
|
6857
6937
|
this.setupGizmo();
|
|
6858
6938
|
// ─── Editor overlays (instanced wireframe primitives) ────────────
|
|
6859
6939
|
this.setupOverlay();
|
|
6860
|
-
// ─── Bloom
|
|
6861
|
-
// Mirrors source/blender/draw/engines/eevee/shaders/effect_bloom_frag.glsl.
|
|
6862
|
-
// Firefly suppression lives in the blit (Karis luminance-weighted 4-tap average). A single-pass
|
|
6863
|
-
// Gaussian cannot reproduce this — hot pixels dominate and produce the sparkle halo.
|
|
6940
|
+
// ─── Bloom: Aether Gazer's chain (prefilter → Gaussian down chain → scatter up chain) ───
|
|
6864
6941
|
this.bloomSampler = this.device.createSampler({
|
|
6865
6942
|
label: "bloom sampler",
|
|
6866
6943
|
magFilter: "linear",
|
|
@@ -6868,81 +6945,25 @@ export class Engine {
|
|
|
6868
6945
|
addressModeU: "clamp-to-edge",
|
|
6869
6946
|
addressModeV: "clamp-to-edge",
|
|
6870
6947
|
});
|
|
6871
|
-
this.
|
|
6872
|
-
label: "bloom
|
|
6873
|
-
size: 16,
|
|
6874
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
6875
|
-
});
|
|
6876
|
-
this.bloomUpsampleUniformBuffer = this.device.createBuffer({
|
|
6877
|
-
label: "bloom upsample uniforms",
|
|
6878
|
-
size: 16,
|
|
6879
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
6880
|
-
});
|
|
6881
|
-
this.bloomBlitBindGroupLayout = this.device.createBindGroupLayout({
|
|
6882
|
-
label: "bloom blit layout",
|
|
6948
|
+
this.bloomPrefilterBindGroupLayout = this.device.createBindGroupLayout({
|
|
6949
|
+
label: "bloom prefilter layout",
|
|
6883
6950
|
entries: [
|
|
6884
6951
|
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float" } },
|
|
6885
6952
|
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
6886
6953
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float" } },
|
|
6887
6954
|
],
|
|
6888
6955
|
});
|
|
6889
|
-
this.
|
|
6890
|
-
label: "bloom
|
|
6956
|
+
this.bloomBlurBindGroupLayout = this.device.createBindGroupLayout({
|
|
6957
|
+
label: "bloom blur layout",
|
|
6891
6958
|
entries: [
|
|
6892
6959
|
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
6893
6960
|
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
6894
6961
|
],
|
|
6895
6962
|
});
|
|
6896
|
-
this.
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // matching downsample mip (base add)
|
|
6901
|
-
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
6902
|
-
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
6903
|
-
],
|
|
6904
|
-
});
|
|
6905
|
-
const bloomBlitShader = this.device.createShaderModule({
|
|
6906
|
-
label: "bloom blit (Karis prefilter)",
|
|
6907
|
-
code: BLOOM_BLIT_SHADER_WGSL,
|
|
6908
|
-
});
|
|
6909
|
-
const bloomDownsampleShader = this.device.createShaderModule({
|
|
6910
|
-
label: "bloom downsample 13-tap",
|
|
6911
|
-
code: BLOOM_DOWNSAMPLE_SHADER_WGSL,
|
|
6912
|
-
});
|
|
6913
|
-
const bloomUpsampleShader = this.device.createShaderModule({
|
|
6914
|
-
label: "bloom upsample 9-tap tent",
|
|
6915
|
-
code: BLOOM_UPSAMPLE_SHADER_WGSL,
|
|
6916
|
-
});
|
|
6917
|
-
const bloomBlitLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomBlitBindGroupLayout] });
|
|
6918
|
-
const bloomDownLayout = this.device.createPipelineLayout({
|
|
6919
|
-
bindGroupLayouts: [this.bloomDownsampleBindGroupLayout],
|
|
6920
|
-
});
|
|
6921
|
-
const bloomUpLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomUpsampleBindGroupLayout] });
|
|
6922
|
-
this.bloomBlitPipeline = this.device.createRenderPipeline({
|
|
6923
|
-
label: "bloom blit pipeline",
|
|
6924
|
-
layout: bloomBlitLayout,
|
|
6925
|
-
vertex: { module: bloomBlitShader, entryPoint: "vs" },
|
|
6926
|
-
fragment: { module: bloomBlitShader, entryPoint: "fs", targets: [{ format: this.hdrFormat }] },
|
|
6927
|
-
primitive: { topology: "triangle-list" },
|
|
6928
|
-
});
|
|
6929
|
-
this.bloomDownsamplePipeline = this.device.createRenderPipeline({
|
|
6930
|
-
label: "bloom downsample pipeline",
|
|
6931
|
-
layout: bloomDownLayout,
|
|
6932
|
-
vertex: { module: bloomDownsampleShader, entryPoint: "vs" },
|
|
6933
|
-
fragment: { module: bloomDownsampleShader, entryPoint: "fs", targets: [{ format: this.hdrFormat }] },
|
|
6934
|
-
primitive: { topology: "triangle-list" },
|
|
6935
|
-
});
|
|
6936
|
-
this.bloomUpsamplePipeline = this.device.createRenderPipeline({
|
|
6937
|
-
label: "bloom upsample pipeline",
|
|
6938
|
-
layout: bloomUpLayout,
|
|
6939
|
-
vertex: { module: bloomUpsampleShader, entryPoint: "vs" },
|
|
6940
|
-
fragment: { module: bloomUpsampleShader, entryPoint: "fs", targets: [{ format: this.hdrFormat }] },
|
|
6941
|
-
primitive: { topology: "triangle-list" },
|
|
6942
|
-
});
|
|
6943
|
-
// ─── Composite: HDR + bloom → Filmic → swapchain (premultiplied) ───
|
|
6944
|
-
// Bloom color/intensity applied HERE (pyramid is pure energy; tint belongs to the combine step,
|
|
6945
|
-
// mirroring EEVEE where bloom color/intensity are combine-stage params, not prefilter).
|
|
6963
|
+
this.ensureBloomPipelines();
|
|
6964
|
+
// ─── Composite: HDR + bloom → view transform → swapchain (premultiplied) ───
|
|
6965
|
+
// Bloom color/intensity applied HERE: the chain is pure energy, tint and
|
|
6966
|
+
// strength belong to the combine step.
|
|
6946
6967
|
this.compositeUniformBuffer = this.device.createBuffer({
|
|
6947
6968
|
label: "composite view uniforms",
|
|
6948
6969
|
// 15 × vec4f: (exposure, invGamma, _, _) · (bloom tint, intensity) ·
|
|
@@ -6984,8 +7005,6 @@ export class Engine {
|
|
|
6984
7005
|
// Aux mask/alpha texture — composite reads .g to reconstruct the alpha that
|
|
6985
7006
|
// used to live in the HDR target before the rg11b10ufloat switch.
|
|
6986
7007
|
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
6987
|
-
// Filmic tone LUT (r16float, filterable) — sampled with the binding-2 sampler.
|
|
6988
|
-
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
6989
7008
|
// 360 backdrop equirect (PhotoDome-style skybox) — 1×1 fallback when unset.
|
|
6990
7009
|
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
6991
7010
|
// User background-effect params — dummy buffer when no effect is set. The
|
|
@@ -7000,9 +7019,6 @@ export class Engine {
|
|
|
7000
7019
|
texture: { sampleType: "depth", viewDimension: "2d", multisampled: true },
|
|
7001
7020
|
},
|
|
7002
7021
|
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
7003
|
-
// AgX's 57³ cube. Decompressed and uploaded off the critical path, so a
|
|
7004
|
-
// 1×1×1 stand-in keeps the bind group valid until it arrives.
|
|
7005
|
-
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { viewDimension: "3d" } },
|
|
7006
7022
|
// The scene's own grade cube (setStageGrade) — 1×1×1 stand-in while it
|
|
7007
7023
|
// has none, which the flag bit keeps from ever being sampled.
|
|
7008
7024
|
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, texture: { viewDimension: "3d" } },
|
|
@@ -7028,12 +7044,12 @@ export class Engine {
|
|
|
7028
7044
|
this.compositePipelineLayout = this.device.createPipelineLayout({
|
|
7029
7045
|
bindGroupLayouts: [this.compositeBindGroupLayout],
|
|
7030
7046
|
});
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7047
|
+
// The same pair setEffects asks for with no field mounts, so an install
|
|
7048
|
+
// that adds none finds it built.
|
|
7049
|
+
this.initPipelineJobs?.push(this.compositePipelines(false, false).then(([identity, gamma]) => {
|
|
7050
|
+
this.compositePipelineIdentity = identity;
|
|
7051
|
+
this.compositePipelineGamma = gamma;
|
|
7052
|
+
}));
|
|
7037
7053
|
// GPU vertex-morph compute pipeline (shared by all models; per-model bind groups).
|
|
7038
7054
|
// Bindings: 0-4 read-only storage (base pos, CSR rowStart/colMorph/colOffset, weights),
|
|
7039
7055
|
// 5 read-write storage (vertex buffer), 6 uniform (params).
|
|
@@ -7050,14 +7066,14 @@ export class Engine {
|
|
|
7050
7066
|
{ binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
|
|
7051
7067
|
],
|
|
7052
7068
|
});
|
|
7053
|
-
this.
|
|
7069
|
+
this.initComputePipeline({
|
|
7054
7070
|
label: "morph compute pipeline",
|
|
7055
7071
|
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.morphComputeBindGroupLayout] }),
|
|
7056
7072
|
compute: {
|
|
7057
7073
|
module: this.device.createShaderModule({ label: "morph compute shader", code: MORPH_COMPUTE_WGSL }),
|
|
7058
7074
|
entryPoint: "cs",
|
|
7059
7075
|
},
|
|
7060
|
-
});
|
|
7076
|
+
}, (p) => (this.morphComputePipeline = p));
|
|
7061
7077
|
// GPU frustum cull. One pipeline for the whole scene; the bind group is rebuilt
|
|
7062
7078
|
// with the buffers whenever the draw list changes. See shaders/passes/cull.ts.
|
|
7063
7079
|
this.cullBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -7077,21 +7093,23 @@ export class Engine {
|
|
|
7077
7093
|
// encoder it is set on, so a WGSL slip here would take the whole frame down —
|
|
7078
7094
|
// every pass, every model, an unrelated-looking cascade of style-group and
|
|
7079
7095
|
// effect failures. Catching it turns that into "culling is off".
|
|
7080
|
-
|
|
7081
|
-
|
|
7096
|
+
// Async, so a failure arrives as the rejection rather than a scoped error.
|
|
7097
|
+
const cullBuild = this.device
|
|
7098
|
+
.createComputePipelineAsync({
|
|
7082
7099
|
label: "cull pipeline",
|
|
7083
7100
|
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.cullBindGroupLayout] }),
|
|
7084
7101
|
compute: {
|
|
7085
7102
|
module: this.device.createShaderModule({ label: "cull compute shader", code: CULL_COMPUTE_WGSL }),
|
|
7086
7103
|
entryPoint: "cs",
|
|
7087
7104
|
},
|
|
7088
|
-
})
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7105
|
+
})
|
|
7106
|
+
.then((p) => {
|
|
7107
|
+
this.cullPipeline = p;
|
|
7108
|
+
}, (err) => {
|
|
7092
7109
|
console.error(`[cull] pipeline failed to compile — frustum culling disabled:\n${err.message}`);
|
|
7093
7110
|
this.cullPipeline = null;
|
|
7094
7111
|
});
|
|
7112
|
+
this.initPipelineJobs?.push(cullBuild);
|
|
7095
7113
|
this.bloomPassDescriptor = {
|
|
7096
7114
|
label: "bloom pass",
|
|
7097
7115
|
colorAttachments: [
|
|
@@ -7132,7 +7150,7 @@ export class Engine {
|
|
|
7132
7150
|
layout: this.pickPerFrameBindGroupLayout,
|
|
7133
7151
|
entries: [{ binding: 0, resource: { buffer: this.cameraUniformBuffer } }],
|
|
7134
7152
|
});
|
|
7135
|
-
this.
|
|
7153
|
+
this.initRenderPipeline({
|
|
7136
7154
|
label: "pick pipeline",
|
|
7137
7155
|
layout: pickPipelineLayout,
|
|
7138
7156
|
vertex: { module: pickShaderModule, buffers: fullVertexBuffers },
|
|
@@ -7146,7 +7164,7 @@ export class Engine {
|
|
|
7146
7164
|
depthWriteEnabled: true,
|
|
7147
7165
|
depthCompare: this.depthAhead,
|
|
7148
7166
|
},
|
|
7149
|
-
});
|
|
7167
|
+
}, (p) => (this.pickPipeline = p));
|
|
7150
7168
|
this.pickReadbackBuffer = this.device.createBuffer({
|
|
7151
7169
|
label: "pick readback",
|
|
7152
7170
|
size: 256,
|
|
@@ -7203,6 +7221,10 @@ export class Engine {
|
|
|
7203
7221
|
// in full the moment there is something to apply it to.
|
|
7204
7222
|
if (!this.device)
|
|
7205
7223
|
return;
|
|
7224
|
+
// Nor while init's pipelines are still compiling: init ends with its own
|
|
7225
|
+
// handleResize, which applies whatever was asked for meanwhile.
|
|
7226
|
+
if (this.initPipelineJobs)
|
|
7227
|
+
return;
|
|
7206
7228
|
// Fixed override (offline/video rendering) wins; otherwise track CSS size × dpr.
|
|
7207
7229
|
const dpr = window.devicePixelRatio || 1;
|
|
7208
7230
|
const width = this.fixedRenderSize ? this.fixedRenderSize.width : Math.floor(this.canvas.clientWidth * dpr);
|
|
@@ -7420,37 +7442,6 @@ export class Engine {
|
|
|
7420
7442
|
// rebind both against the new one.
|
|
7421
7443
|
this.buildGroundBindGroup();
|
|
7422
7444
|
this.buildMirrorSurfaceBindGroup();
|
|
7423
|
-
// Bloom pyramid: mip 0 is half-res, each subsequent mip halves again.
|
|
7424
|
-
// Mip count chosen so the coarsest mip is ≥4 px on the short side, capped at BLOOM_MAX_LEVELS.
|
|
7425
|
-
const bw = Math.max(1, Math.floor(width / 2));
|
|
7426
|
-
const bh = Math.max(1, Math.floor(height / 2));
|
|
7427
|
-
const shortSide = Math.max(1, Math.min(bw, bh));
|
|
7428
|
-
this.bloomMipCount = Math.max(1, Math.min(Engine.BLOOM_MAX_LEVELS, Math.floor(Math.log2(shortSide)) - 1));
|
|
7429
|
-
this.bloomDownTexture?.destroy();
|
|
7430
|
-
this.bloomDownTexture = this.device.createTexture({
|
|
7431
|
-
label: "bloom down pyramid",
|
|
7432
|
-
size: [bw, bh],
|
|
7433
|
-
mipLevelCount: this.bloomMipCount,
|
|
7434
|
-
format: this.hdrFormat,
|
|
7435
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
7436
|
-
});
|
|
7437
|
-
this.bloomUpTexture?.destroy();
|
|
7438
|
-
this.bloomUpTexture = this.device.createTexture({
|
|
7439
|
-
label: "bloom up pyramid",
|
|
7440
|
-
size: [bw, bh],
|
|
7441
|
-
mipLevelCount: Math.max(1, this.bloomMipCount - 1),
|
|
7442
|
-
format: this.hdrFormat,
|
|
7443
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
7444
|
-
});
|
|
7445
|
-
this.bloomDownMipViews = [];
|
|
7446
|
-
for (let i = 0; i < this.bloomMipCount; i++) {
|
|
7447
|
-
this.bloomDownMipViews.push(this.bloomDownTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }));
|
|
7448
|
-
}
|
|
7449
|
-
this.bloomUpMipViews = [];
|
|
7450
|
-
const upLevels = Math.max(1, this.bloomMipCount - 1);
|
|
7451
|
-
for (let i = 0; i < upLevels; i++) {
|
|
7452
|
-
this.bloomUpMipViews.push(this.bloomUpTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }));
|
|
7453
|
-
}
|
|
7454
7445
|
this.depthTexture?.destroy();
|
|
7455
7446
|
this.depthTexture = this.device.createTexture({
|
|
7456
7447
|
label: "depth texture",
|
|
@@ -7562,49 +7553,10 @@ export class Engine {
|
|
|
7562
7553
|
},
|
|
7563
7554
|
],
|
|
7564
7555
|
};
|
|
7565
|
-
this.
|
|
7566
|
-
|
|
7567
|
-
//
|
|
7568
|
-
this.
|
|
7569
|
-
label: "bloom blit bind group",
|
|
7570
|
-
layout: this.bloomBlitBindGroupLayout,
|
|
7571
|
-
entries: [
|
|
7572
|
-
{ binding: 0, resource: this.hdrResolveTexture.createView() },
|
|
7573
|
-
{ binding: 1, resource: { buffer: this.bloomBlitUniformBuffer } },
|
|
7574
|
-
{ binding: 2, resource: this.maskResolveView },
|
|
7575
|
-
],
|
|
7576
|
-
});
|
|
7577
|
-
// Downsample[i] reads bloomDown mip (i-1), writes bloomDown mip i. i ∈ [1..N-1].
|
|
7578
|
-
this.bloomDownsampleBindGroups = [];
|
|
7579
|
-
for (let i = 1; i < this.bloomMipCount; i++) {
|
|
7580
|
-
this.bloomDownsampleBindGroups.push(this.device.createBindGroup({
|
|
7581
|
-
label: `bloom downsample ${i}`,
|
|
7582
|
-
layout: this.bloomDownsampleBindGroupLayout,
|
|
7583
|
-
entries: [
|
|
7584
|
-
{ binding: 0, resource: this.bloomDownMipViews[i - 1] },
|
|
7585
|
-
{ binding: 1, resource: this.bloomSampler },
|
|
7586
|
-
],
|
|
7587
|
-
}));
|
|
7588
|
-
}
|
|
7589
|
-
// Upsample[i] writes bloomUp mip i. Coarsest step reads bloomDown[N-1] (no prior up yet);
|
|
7590
|
-
// subsequent steps read bloomUp[i+1]. Both read bloomDown[i] as the base (additive combine).
|
|
7591
|
-
this.bloomUpsampleBindGroups = [];
|
|
7592
|
-
const topIdx = this.bloomMipCount - 2;
|
|
7593
|
-
for (let i = topIdx; i >= 0; i--) {
|
|
7594
|
-
const srcView = i === topIdx ? this.bloomDownMipViews[this.bloomMipCount - 1] : this.bloomUpMipViews[i + 1];
|
|
7595
|
-
this.bloomUpsampleBindGroups.push(this.device.createBindGroup({
|
|
7596
|
-
label: `bloom upsample ${i}`,
|
|
7597
|
-
layout: this.bloomUpsampleBindGroupLayout,
|
|
7598
|
-
entries: [
|
|
7599
|
-
{ binding: 0, resource: srcView },
|
|
7600
|
-
{ binding: 1, resource: this.bloomDownMipViews[i] },
|
|
7601
|
-
{ binding: 2, resource: this.bloomSampler },
|
|
7602
|
-
{ binding: 3, resource: { buffer: this.bloomUpsampleUniformBuffer } },
|
|
7603
|
-
],
|
|
7604
|
-
}));
|
|
7605
|
-
}
|
|
7606
|
-
// Composite reads bloomUp mip 0 (full pyramid collapsed); fallback to bloomDown mip 0 if no upsample level.
|
|
7607
|
-
this.compositeBloomView = this.bloomMipCount > 1 ? this.bloomUpMipViews[0] : this.bloomDownMipViews[0];
|
|
7556
|
+
if (this.compositeBindGroupLayout && this.bloomPrefilterBindGroupLayout) {
|
|
7557
|
+
this.buildBloomTargets();
|
|
7558
|
+
// The composite reads the finished chain: up[0], or down[0] when there is one level.
|
|
7559
|
+
this.compositeBloomView = this.bloomLevels > 1 ? this.bloomUpViews[0] : this.bloomDownViews[0];
|
|
7608
7560
|
this.rebuildCompositeBindGroup();
|
|
7609
7561
|
}
|
|
7610
7562
|
this.writeCompositeViewUniforms();
|
|
@@ -7867,16 +7819,16 @@ export class Engine {
|
|
|
7867
7819
|
},
|
|
7868
7820
|
multisample: { count: Engine.OVERLAY_SAMPLE_COUNT },
|
|
7869
7821
|
};
|
|
7870
|
-
this.overlayPipeline =
|
|
7822
|
+
this.initRenderPipeline(overlayPipelineDescriptor, (p) => (this.overlayPipeline = p));
|
|
7871
7823
|
// The solid volumes: the same shader and layout, with no depth write and no
|
|
7872
7824
|
// culling. A translucent body must not hide the rig behind it, and you have
|
|
7873
7825
|
// to see its far wall for it to read as a volume rather than a silhouette.
|
|
7874
|
-
this.
|
|
7826
|
+
this.initRenderPipeline({
|
|
7875
7827
|
...overlayPipelineDescriptor,
|
|
7876
7828
|
label: "overlay solid pipeline",
|
|
7877
7829
|
primitive: { topology: "triangle-list", cullMode: "none" },
|
|
7878
7830
|
depthStencil: { format: "depth24plus", depthWriteEnabled: false, depthCompare: "always" },
|
|
7879
|
-
});
|
|
7831
|
+
}, (p) => (this.overlaySolidPipeline = p));
|
|
7880
7832
|
const compositeShader = this.device.createShaderModule({
|
|
7881
7833
|
label: "overlay composite shader",
|
|
7882
7834
|
code: OVERLAY_COMPOSITE_SHADER_WGSL,
|
|
@@ -7964,7 +7916,7 @@ export class Engine {
|
|
|
7964
7916
|
],
|
|
7965
7917
|
});
|
|
7966
7918
|
const wireShader = this.device.createShaderModule({ label: "wireframe shader", code: WIREFRAME_SHADER_WGSL });
|
|
7967
|
-
this.
|
|
7919
|
+
this.initRenderPipeline({
|
|
7968
7920
|
label: "wireframe pipeline",
|
|
7969
7921
|
layout: this.device.createPipelineLayout({
|
|
7970
7922
|
label: "wireframe pipeline layout",
|
|
@@ -7991,7 +7943,7 @@ export class Engine {
|
|
|
7991
7943
|
// so a bone behind a triangle must not be punched out by it.
|
|
7992
7944
|
depthStencil: { format: "depth24plus", depthWriteEnabled: false, depthCompare: this.depthAhead },
|
|
7993
7945
|
multisample: { count: Engine.OVERLAY_SAMPLE_COUNT },
|
|
7994
|
-
});
|
|
7946
|
+
}, (p) => (this.wireframePipeline = p));
|
|
7995
7947
|
// The mesh's own depth, so the wireframe can be occluded by the body it
|
|
7996
7948
|
// belongs to. Occluded is the default everywhere — Blender's edit mode, Maya,
|
|
7997
7949
|
// three's and Babylon's wireframe materials all depth-test, and X-ray is a
|
|
@@ -8656,36 +8608,446 @@ export class Engine {
|
|
|
8656
8608
|
getCameraRoll() {
|
|
8657
8609
|
return this.camera.roll;
|
|
8658
8610
|
}
|
|
8659
|
-
/** Vertical field of view in radians (default π/4). While a camera VMD
|
|
8660
|
-
* drives the view it animates fov itself; the orbit value set here is
|
|
8661
|
-
* restored when the VMD releases the camera. */
|
|
8611
|
+
/** Vertical field of view in radians (default π/4). While a camera VMD
|
|
8612
|
+
* drives the view it animates fov itself; the orbit value set here is
|
|
8613
|
+
* restored when the VMD releases the camera. */
|
|
8614
|
+
/**
|
|
8615
|
+
* The shot as MMD states it — target, euler rotation, distance, fov.
|
|
8616
|
+
*
|
|
8617
|
+
* For a host writing the camera out to something else: an AE composition, a
|
|
8618
|
+
* .vmd, a log. The same five channels whichever mode is driving, so the
|
|
8619
|
+
* caller never asks and never decomposes a view matrix to find out.
|
|
8620
|
+
*/
|
|
8621
|
+
getCameraPose() {
|
|
8622
|
+
return this.camera.getPose();
|
|
8623
|
+
}
|
|
8624
|
+
getCameraFov() {
|
|
8625
|
+
return this.camera.fov;
|
|
8626
|
+
}
|
|
8627
|
+
setCameraFov(fov) {
|
|
8628
|
+
this.camera.fov = fov;
|
|
8629
|
+
}
|
|
8630
|
+
// Step 5: Create lighting buffers
|
|
8631
|
+
setupLighting() {
|
|
8632
|
+
this.lightUniformBuffer = this.device.createBuffer({
|
|
8633
|
+
label: "light uniforms",
|
|
8634
|
+
size: 112 * 4, // ambient (4) + 4 lights x 2 vec4 (32) + irradiance SH 9 x vec4 (36) + fog 4 x vec4 (16) + cast shadow (4 + 16)
|
|
8635
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
8636
|
+
});
|
|
8637
|
+
this.lightData.fill(0);
|
|
8638
|
+
this.lightCount = 0;
|
|
8639
|
+
this.writeWorld();
|
|
8640
|
+
this.writeSun(0);
|
|
8641
|
+
}
|
|
8642
|
+
// ─── The stage's reflection probe ───────────────────────────────────
|
|
8643
|
+
//
|
|
8644
|
+
// What a game bakes for every room: a cube of the stage itself, seen from
|
|
8645
|
+
// its middle, prefiltered down its mips by the specular lobe, and read by
|
|
8646
|
+
// every surface box-projected onto the stage's bounds — so a lacquered
|
|
8647
|
+
// counter reflects the bottles behind it and a wet floor the lamps above,
|
|
8648
|
+
// not a sky the room has no window onto. Captured here automatically once a
|
|
8649
|
+
// stage has loaded and its looks have settled; nothing to set.
|
|
8650
|
+
/** Drop the capture's resources, so the next capture rebinds the sky. */
|
|
8651
|
+
dropProbeCapture() {
|
|
8652
|
+
const cap = this.probeCapture;
|
|
8653
|
+
if (!cap)
|
|
8654
|
+
return;
|
|
8655
|
+
this.probeCapture = null;
|
|
8656
|
+
void this.device.queue.onSubmittedWorkDone().then(() => {
|
|
8657
|
+
for (const t of cap.msTargets)
|
|
8658
|
+
t.destroy();
|
|
8659
|
+
cap.depth.destroy();
|
|
8660
|
+
cap.color.destroy();
|
|
8661
|
+
cap.aux.destroy();
|
|
8662
|
+
cap.cube.destroy();
|
|
8663
|
+
cap.steps.destroy();
|
|
8664
|
+
});
|
|
8665
|
+
}
|
|
8666
|
+
/** Ask for a fresh capture of the stage's reflection probe. It runs at the
|
|
8667
|
+
* start of a frame once the scene has held still for a moment — asked for
|
|
8668
|
+
* by the engine itself whenever the stage or its looks change; a host may
|
|
8669
|
+
* call it after changing something the engine cannot see. */
|
|
8670
|
+
captureReflectionProbe() {
|
|
8671
|
+
if (!this.probeDirty)
|
|
8672
|
+
this.probeDirtyFrame = this.probeFrame;
|
|
8673
|
+
this.probeDirty = true;
|
|
8674
|
+
}
|
|
8675
|
+
/** The stage's world-space bounds: every stage model's material boxes
|
|
8676
|
+
* through its placement. Null without a stage. */
|
|
8677
|
+
stageBounds() {
|
|
8678
|
+
const min = [Infinity, Infinity, Infinity];
|
|
8679
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
8680
|
+
const p = new Vec3(0, 0, 0);
|
|
8681
|
+
let any = false;
|
|
8682
|
+
for (const inst of this.modelInstances.values()) {
|
|
8683
|
+
if (!inst.isStage || !inst.model.visible)
|
|
8684
|
+
continue;
|
|
8685
|
+
const m = inst.model;
|
|
8686
|
+
for (const draw of inst.drawCalls) {
|
|
8687
|
+
const b = draw.bounds;
|
|
8688
|
+
if (!b || b.length < 6 || !(b[0] <= b[3]))
|
|
8689
|
+
continue;
|
|
8690
|
+
// The eight corners of the material's model-space box, placed.
|
|
8691
|
+
for (let k = 0; k < 8; k++) {
|
|
8692
|
+
p.setXYZ((k & 1 ? b[3] : b[0]) * m.scale, (k & 2 ? b[4] : b[1]) * m.scale, (k & 4 ? b[5] : b[2]) * m.scale);
|
|
8693
|
+
Quat.rotateVecInto(m.rotation, p, p);
|
|
8694
|
+
const w = [p.x + m.position.x, p.y + m.position.y, p.z + m.position.z];
|
|
8695
|
+
for (let a = 0; a < 3; a++) {
|
|
8696
|
+
if (w[a] < min[a])
|
|
8697
|
+
min[a] = w[a];
|
|
8698
|
+
if (w[a] > max[a])
|
|
8699
|
+
max[a] = w[a];
|
|
8700
|
+
}
|
|
8701
|
+
any = true;
|
|
8702
|
+
}
|
|
8703
|
+
}
|
|
8704
|
+
}
|
|
8705
|
+
return any ? { min, max } : null;
|
|
8706
|
+
}
|
|
8707
|
+
/** Run a pending capture once the scene has settled — see captureReflectionProbe. */
|
|
8708
|
+
maybeCaptureProbe() {
|
|
8709
|
+
this.probeFrame++;
|
|
8710
|
+
if (!this.probeDirty || !this.probeCaptureBindGroup)
|
|
8711
|
+
return;
|
|
8712
|
+
if (this.probeFrame - this.probeDirtyFrame < Engine.PROBE_SETTLE_FRAMES)
|
|
8713
|
+
return;
|
|
8714
|
+
this.probeDirty = false;
|
|
8715
|
+
const bounds = this.stageBounds();
|
|
8716
|
+
if (!bounds) {
|
|
8717
|
+
// No stage: the probe goes dark and every reader falls back to the sky.
|
|
8718
|
+
this.probeData.fill(0);
|
|
8719
|
+
this.device.queue.writeBuffer(this.probeUniformBuffer, 0, this.probeData);
|
|
8720
|
+
return;
|
|
8721
|
+
}
|
|
8722
|
+
try {
|
|
8723
|
+
this.renderProbe(bounds);
|
|
8724
|
+
}
|
|
8725
|
+
catch (e) {
|
|
8726
|
+
console.warn("[reze-engine] reflection probe capture failed:", e);
|
|
8727
|
+
}
|
|
8728
|
+
}
|
|
8729
|
+
ensureProbeCapture() {
|
|
8730
|
+
if (this.probeCapture)
|
|
8731
|
+
return this.probeCapture;
|
|
8732
|
+
const size = PROBE_CAPTURE_SIZE;
|
|
8733
|
+
const formats = sceneColorFormats(this.sceneFormats);
|
|
8734
|
+
const msTargets = formats.map((format, i) => this.device.createTexture({
|
|
8735
|
+
label: `reflection probe capture target ${i}`,
|
|
8736
|
+
size: [size, size],
|
|
8737
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
8738
|
+
format,
|
|
8739
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
8740
|
+
}));
|
|
8741
|
+
const depth = this.device.createTexture({
|
|
8742
|
+
label: "reflection probe capture depth",
|
|
8743
|
+
size: [size, size],
|
|
8744
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
8745
|
+
format: this.depthFormat,
|
|
8746
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
8747
|
+
});
|
|
8748
|
+
const layered = (label, format) => this.device.createTexture({
|
|
8749
|
+
label,
|
|
8750
|
+
size: [size, size, 6],
|
|
8751
|
+
format,
|
|
8752
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
8753
|
+
});
|
|
8754
|
+
// The resolves take the scene pass's own formats: colour and coverage.
|
|
8755
|
+
const color = layered("reflection probe capture colour", formats[0]);
|
|
8756
|
+
const aux = layered("reflection probe capture coverage", formats[1]);
|
|
8757
|
+
const cube = this.device.createTexture({
|
|
8758
|
+
label: "reflection probe capture cube",
|
|
8759
|
+
size: [size, size, 6],
|
|
8760
|
+
format: "rgba16float",
|
|
8761
|
+
mipLevelCount: PROBE_CAPTURE_LEVELS,
|
|
8762
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
8763
|
+
});
|
|
8764
|
+
// One 256-byte slot per step: 6 composes, then the capture's mips, then
|
|
8765
|
+
// the probe's levels, six faces each.
|
|
8766
|
+
const slots = 6 + PROBE_CAPTURE_LEVELS * 6 + PROBE_LEVELS * 6;
|
|
8767
|
+
const steps = this.device.createBuffer({
|
|
8768
|
+
label: "reflection probe steps",
|
|
8769
|
+
size: slots * 256,
|
|
8770
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
8771
|
+
});
|
|
8772
|
+
const composeModule = this.device.createShaderModule({ label: "reflection probe compose", code: PROBE_COMPOSE_WGSL });
|
|
8773
|
+
const composePipeline = this.device.createRenderPipeline({
|
|
8774
|
+
label: "reflection probe compose",
|
|
8775
|
+
layout: "auto",
|
|
8776
|
+
vertex: { module: composeModule, entryPoint: "vs_full" },
|
|
8777
|
+
fragment: { module: composeModule, entryPoint: "fs_compose", targets: [{ format: "rgba16float" }] },
|
|
8778
|
+
primitive: { topology: "triangle-list" },
|
|
8779
|
+
});
|
|
8780
|
+
const filterModule = this.device.createShaderModule({ label: "reflection probe filter", code: PROBE_FILTER_WGSL });
|
|
8781
|
+
const filterPipeline = this.device.createRenderPipeline({
|
|
8782
|
+
label: "reflection probe filter",
|
|
8783
|
+
layout: "auto",
|
|
8784
|
+
vertex: { module: filterModule, entryPoint: "vs_full" },
|
|
8785
|
+
fragment: { module: filterModule, entryPoint: "fs_filter", targets: [{ format: "rgba16float" }] },
|
|
8786
|
+
primitive: { topology: "triangle-list" },
|
|
8787
|
+
});
|
|
8788
|
+
const colorArray = color.createView({ dimension: "2d-array" });
|
|
8789
|
+
const auxArray = aux.createView({ dimension: "2d-array" });
|
|
8790
|
+
const composeLayout = composePipeline.getBindGroupLayout(0);
|
|
8791
|
+
const composeGroups = [];
|
|
8792
|
+
for (let f = 0; f < 6; f++) {
|
|
8793
|
+
composeGroups.push(this.device.createBindGroup({
|
|
8794
|
+
label: `reflection probe compose ${f}`,
|
|
8795
|
+
layout: composeLayout,
|
|
8796
|
+
entries: [
|
|
8797
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
8798
|
+
{ binding: 2, resource: this.materialSampler },
|
|
8799
|
+
{ binding: 8, resource: this.worldEquirectView ?? this.fallbackEquirectView },
|
|
8800
|
+
{ binding: 20, resource: colorArray },
|
|
8801
|
+
{ binding: 21, resource: auxArray },
|
|
8802
|
+
{ binding: 22, resource: { buffer: steps, offset: f * 256, size: 32 } },
|
|
8803
|
+
],
|
|
8804
|
+
}));
|
|
8805
|
+
}
|
|
8806
|
+
const filterLayout = filterPipeline.getBindGroupLayout(0);
|
|
8807
|
+
const cubeView = cube.createView({ dimension: "cube" });
|
|
8808
|
+
const filterGroups = [];
|
|
8809
|
+
for (let i = 0; i < PROBE_LEVELS * 6; i++) {
|
|
8810
|
+
filterGroups.push(this.device.createBindGroup({
|
|
8811
|
+
label: `reflection probe level ${i}`,
|
|
8812
|
+
layout: filterLayout,
|
|
8813
|
+
entries: [
|
|
8814
|
+
{ binding: 0, resource: cubeView },
|
|
8815
|
+
{ binding: 1, resource: this.materialSampler },
|
|
8816
|
+
{ binding: 2, resource: { buffer: steps, offset: (6 + PROBE_CAPTURE_LEVELS * 6 + i) * 256, size: 32 } },
|
|
8817
|
+
],
|
|
8818
|
+
}));
|
|
8819
|
+
}
|
|
8820
|
+
this.probeCapture = {
|
|
8821
|
+
msTargets,
|
|
8822
|
+
depth,
|
|
8823
|
+
color,
|
|
8824
|
+
aux,
|
|
8825
|
+
cube,
|
|
8826
|
+
steps,
|
|
8827
|
+
composePipeline,
|
|
8828
|
+
filterPipeline,
|
|
8829
|
+
composeGroups,
|
|
8830
|
+
filterGroups,
|
|
8831
|
+
};
|
|
8832
|
+
return this.probeCapture;
|
|
8833
|
+
}
|
|
8834
|
+
/**
|
|
8835
|
+
* Capture the probe: the stage drawn six times from one point into a 128²
|
|
8836
|
+
* cube, the sky laid in behind it, a mip chain, and the GGX convolution
|
|
8837
|
+
* into the 64² cube the materials read.
|
|
8838
|
+
*
|
|
8839
|
+
* WHERE: the middle of the stage's bounds across the floor, at about a
|
|
8840
|
+
* character's chest height above the floor (y = 0 where the bounds span it,
|
|
8841
|
+
* the MMD convention, else their bottom) — where a game puts the probe of a
|
|
8842
|
+
* room the player stands in. The box it projects onto is the bounds.
|
|
8843
|
+
*
|
|
8844
|
+
* WHAT: the stage alone, in its own looks, lit by the scene's sun, lamps
|
|
8845
|
+
* and sky as they stand. The cast is not in it (a baked probe never holds
|
|
8846
|
+
* the characters), nor are effects.
|
|
8847
|
+
*/
|
|
8848
|
+
renderProbe(bounds) {
|
|
8849
|
+
const cap = this.ensureProbeCapture();
|
|
8850
|
+
const { min, max } = bounds;
|
|
8851
|
+
const floor = min[1] <= 0 && max[1] >= 0 ? 0 : min[1];
|
|
8852
|
+
const eye = [
|
|
8853
|
+
(min[0] + max[0]) * 0.5,
|
|
8854
|
+
Math.min(floor + 12, (min[1] + max[1]) * 0.5),
|
|
8855
|
+
(min[2] + max[2]) * 0.5,
|
|
8856
|
+
];
|
|
8857
|
+
const span = Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
|
|
8858
|
+
const near = 0.25;
|
|
8859
|
+
const far = Math.max(span * 2, 100);
|
|
8860
|
+
// Each face's forward and up, in the convention the GPU samples a cube
|
|
8861
|
+
// with (see rz_face_dir in passes/probe.ts): +X, -X, +Y, -Y, +Z, -Z.
|
|
8862
|
+
const faces = [
|
|
8863
|
+
[1, 0, 0, 0, 1, 0],
|
|
8864
|
+
[-1, 0, 0, 0, 1, 0],
|
|
8865
|
+
[0, 1, 0, 0, 0, -1],
|
|
8866
|
+
[0, -1, 0, 0, 0, 1],
|
|
8867
|
+
[0, 0, 1, 0, 1, 0],
|
|
8868
|
+
[0, 0, -1, 0, 1, 0],
|
|
8869
|
+
];
|
|
8870
|
+
const cam = this.probeCameraData;
|
|
8871
|
+
const view = { perFrame: this.probeCaptureBindGroup, args: "probe", outlines: false };
|
|
8872
|
+
const formats = sceneColorFormats(this.sceneFormats);
|
|
8873
|
+
for (let f = 0; f < 6; f++) {
|
|
8874
|
+
const [fx, fy, fz, ux, uy, uz] = faces[f];
|
|
8875
|
+
Mat4.lookAtInto(cam.subarray(0, 16), eye[0], eye[1], eye[2], eye[0] + fx, eye[1] + fy, eye[2] + fz, ux, uy, uz);
|
|
8876
|
+
if (this.reversedZ)
|
|
8877
|
+
Mat4.perspectiveInto(cam.subarray(16, 32), Math.PI / 2, 1, far, near);
|
|
8878
|
+
else
|
|
8879
|
+
Mat4.perspectiveInto(cam.subarray(16, 32), Math.PI / 2, 1, near, far);
|
|
8880
|
+
cam[32] = eye[0];
|
|
8881
|
+
cam[33] = eye[1];
|
|
8882
|
+
cam[34] = eye[2];
|
|
8883
|
+
cam[35] = PROBE_CAPTURE_SIZE;
|
|
8884
|
+
cam[36] = this.sceneClock;
|
|
8885
|
+
this.device.queue.writeBuffer(this.probeCameraBuffer, 0, cam);
|
|
8886
|
+
const encoder = this.device.createCommandEncoder({ label: `reflection probe face ${f}` });
|
|
8887
|
+
const colorAttachments = formats.map((_, i) => ({
|
|
8888
|
+
view: cap.msTargets[i].createView(),
|
|
8889
|
+
resolveTarget: i === 0
|
|
8890
|
+
? cap.color.createView({ dimension: "2d", baseArrayLayer: f, arrayLayerCount: 1 })
|
|
8891
|
+
: i === 1
|
|
8892
|
+
? cap.aux.createView({ dimension: "2d", baseArrayLayer: f, arrayLayerCount: 1 })
|
|
8893
|
+
: undefined,
|
|
8894
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
8895
|
+
loadOp: "clear",
|
|
8896
|
+
storeOp: "discard",
|
|
8897
|
+
}));
|
|
8898
|
+
const pass = encoder.beginRenderPass({
|
|
8899
|
+
label: `reflection probe face ${f}`,
|
|
8900
|
+
colorAttachments,
|
|
8901
|
+
depthStencilAttachment: {
|
|
8902
|
+
view: cap.depth.createView(),
|
|
8903
|
+
depthClearValue: this.depthClear,
|
|
8904
|
+
depthLoadOp: "clear",
|
|
8905
|
+
depthStoreOp: "discard",
|
|
8906
|
+
stencilClearValue: 0,
|
|
8907
|
+
stencilLoadOp: "clear",
|
|
8908
|
+
stencilStoreOp: "discard",
|
|
8909
|
+
},
|
|
8910
|
+
});
|
|
8911
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
8912
|
+
for (const inst of this.modelInstances.values()) {
|
|
8913
|
+
if (!inst.isStage || !inst.model.visible)
|
|
8914
|
+
continue;
|
|
8915
|
+
this.setModelDrawState(pass, inst);
|
|
8916
|
+
this.drawMaterials(pass, inst, "opaque", view);
|
|
8917
|
+
}
|
|
8918
|
+
for (const inst of this.modelInstances.values()) {
|
|
8919
|
+
if (!inst.isStage || !inst.model.visible)
|
|
8920
|
+
continue;
|
|
8921
|
+
this.setModelDrawState(pass, inst);
|
|
8922
|
+
this.drawMaterials(pass, inst, "transparent", view);
|
|
8923
|
+
}
|
|
8924
|
+
pass.end();
|
|
8925
|
+
this.device.queue.submit([encoder.finish()]);
|
|
8926
|
+
}
|
|
8927
|
+
// The steps: compose, capture mips, then the probe's levels.
|
|
8928
|
+
const steps = new ArrayBuffer((6 + PROBE_CAPTURE_LEVELS * 6 + PROBE_LEVELS * 6) * 256);
|
|
8929
|
+
const put = (slot, face, lod, roughness, srcSize, mode) => {
|
|
8930
|
+
const u = new Uint32Array(steps, slot * 256, 8);
|
|
8931
|
+
const fl = new Float32Array(steps, slot * 256, 8);
|
|
8932
|
+
u[0] = face;
|
|
8933
|
+
fl[1] = lod;
|
|
8934
|
+
fl[2] = roughness;
|
|
8935
|
+
fl[3] = srcSize;
|
|
8936
|
+
u[4] = mode;
|
|
8937
|
+
};
|
|
8938
|
+
for (let f = 0; f < 6; f++)
|
|
8939
|
+
put(f, f, 0, 0, PROBE_CAPTURE_SIZE, 0);
|
|
8940
|
+
for (let m = 0; m < PROBE_CAPTURE_LEVELS; m++)
|
|
8941
|
+
for (let f = 0; f < 6; f++)
|
|
8942
|
+
put(6 + m * 6 + f, f, Math.max(m - 1, 0), 0, PROBE_CAPTURE_SIZE, 0);
|
|
8943
|
+
for (let m = 0; m < PROBE_LEVELS; m++)
|
|
8944
|
+
for (let f = 0; f < 6; f++)
|
|
8945
|
+
// Level 0 is the capture at the probe's size: one tap of the 128²
|
|
8946
|
+
// level-0 cube at a 64² texel's centre is its 2×2 average.
|
|
8947
|
+
put(6 + PROBE_CAPTURE_LEVELS * 6 + m * 6 + f, f, 0, probeLevelRoughness(m), PROBE_CAPTURE_SIZE, m === 0 ? 0 : 1);
|
|
8948
|
+
this.device.queue.writeBuffer(cap.steps, 0, steps);
|
|
8949
|
+
const encoder = this.device.createCommandEncoder({ label: "reflection probe filter" });
|
|
8950
|
+
const run = (target, pipeline, group) => {
|
|
8951
|
+
const p = encoder.beginRenderPass({ colorAttachments: [{ view: target, loadOp: "clear", storeOp: "store" }] });
|
|
8952
|
+
p.setPipeline(pipeline);
|
|
8953
|
+
p.setBindGroup(0, group);
|
|
8954
|
+
p.draw(3);
|
|
8955
|
+
p.end();
|
|
8956
|
+
};
|
|
8957
|
+
const face = (tex, level, f) => tex.createView({ dimension: "2d", baseMipLevel: level, mipLevelCount: 1, baseArrayLayer: f, arrayLayerCount: 1 });
|
|
8958
|
+
for (let f = 0; f < 6; f++)
|
|
8959
|
+
run(face(cap.cube, 0, f), cap.composePipeline, cap.composeGroups[f]);
|
|
8960
|
+
// Each level from the one above. The source view spans the whole chain,
|
|
8961
|
+
// which a pass writing one of its levels may not bind — so the reads go
|
|
8962
|
+
// through a view of the levels ABOVE the one being written.
|
|
8963
|
+
for (let m = 1; m < PROBE_CAPTURE_LEVELS; m++) {
|
|
8964
|
+
const above = cap.cube.createView({ dimension: "cube", baseMipLevel: 0, mipLevelCount: m });
|
|
8965
|
+
for (let f = 0; f < 6; f++) {
|
|
8966
|
+
const group = this.device.createBindGroup({
|
|
8967
|
+
label: `reflection probe capture mip ${m}/${f}`,
|
|
8968
|
+
layout: cap.filterPipeline.getBindGroupLayout(0),
|
|
8969
|
+
entries: [
|
|
8970
|
+
{ binding: 0, resource: above },
|
|
8971
|
+
{ binding: 1, resource: this.materialSampler },
|
|
8972
|
+
{ binding: 2, resource: { buffer: cap.steps, offset: (6 + m * 6 + f) * 256, size: 32 } },
|
|
8973
|
+
],
|
|
8974
|
+
});
|
|
8975
|
+
run(face(cap.cube, m, f), cap.filterPipeline, group);
|
|
8976
|
+
}
|
|
8977
|
+
}
|
|
8978
|
+
for (let m = 0; m < PROBE_LEVELS; m++)
|
|
8979
|
+
for (let f = 0; f < 6; f++)
|
|
8980
|
+
run(face(this.probeTexture, m, f), cap.filterPipeline, cap.filterGroups[m * 6 + f]);
|
|
8981
|
+
this.device.queue.submit([encoder.finish()]);
|
|
8982
|
+
this.probeData.set([eye[0], eye[1], eye[2], 1, min[0], min[1], min[2], 1, max[0], max[1], max[2], 0]);
|
|
8983
|
+
this.device.queue.writeBuffer(this.probeUniformBuffer, 0, this.probeData);
|
|
8984
|
+
}
|
|
8662
8985
|
/**
|
|
8663
|
-
* The
|
|
8664
|
-
*
|
|
8665
|
-
*
|
|
8666
|
-
*
|
|
8667
|
-
* caller never asks and never decomposes a view matrix to find out.
|
|
8986
|
+
* The sky's mips, convolved with the GGX lobe each stands for — the box
|
|
8987
|
+
* average the upload built is only the source. Level 0 is copied as it is
|
|
8988
|
+
* (it is also what the backdrop draws); every level below it becomes the
|
|
8989
|
+
* sky as a surface of that roughness reflects it (skyLevelRoughness).
|
|
8668
8990
|
*/
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8991
|
+
prefilterSky(source, width, height, levels) {
|
|
8992
|
+
if (levels <= 1)
|
|
8993
|
+
return source;
|
|
8994
|
+
if (!this.skyPrefilterPipeline) {
|
|
8995
|
+
const module = this.device.createShaderModule({ label: "sky prefilter", code: SKY_PREFILTER_WGSL });
|
|
8996
|
+
this.skyPrefilterPipeline = this.device.createRenderPipeline({
|
|
8997
|
+
label: "sky prefilter",
|
|
8998
|
+
layout: "auto",
|
|
8999
|
+
vertex: { module, entryPoint: "vs_full" },
|
|
9000
|
+
fragment: { module, entryPoint: "fs_sky", targets: [{ format: "rgba16float" }] },
|
|
9001
|
+
primitive: { topology: "triangle-list" },
|
|
9002
|
+
});
|
|
9003
|
+
}
|
|
9004
|
+
const pipeline = this.skyPrefilterPipeline;
|
|
9005
|
+
const out = this.device.createTexture({
|
|
9006
|
+
label: "world equirect (HDR, GGX-prefiltered)",
|
|
9007
|
+
size: [width, height],
|
|
9008
|
+
format: "rgba16float",
|
|
9009
|
+
mipLevelCount: levels,
|
|
9010
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
9011
|
+
});
|
|
9012
|
+
const encoder = this.device.createCommandEncoder({ label: "sky prefilter" });
|
|
9013
|
+
encoder.copyTextureToTexture({ texture: source, mipLevel: 0 }, { texture: out, mipLevel: 0 }, [width, height]);
|
|
9014
|
+
const params = this.device.createBuffer({
|
|
9015
|
+
label: "sky prefilter steps",
|
|
9016
|
+
size: levels * 256,
|
|
8683
9017
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
8684
9018
|
});
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
this.
|
|
9019
|
+
const data = new Float32Array(levels * 64);
|
|
9020
|
+
for (let m = 1; m < levels; m++)
|
|
9021
|
+
data.set([skyLevelRoughness(m, levels), width, height, 0], m * 64);
|
|
9022
|
+
this.device.queue.writeBuffer(params, 0, data);
|
|
9023
|
+
const srcView = source.createView();
|
|
9024
|
+
for (let m = 1; m < levels; m++) {
|
|
9025
|
+
const group = this.device.createBindGroup({
|
|
9026
|
+
label: `sky prefilter ${m}`,
|
|
9027
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
9028
|
+
entries: [
|
|
9029
|
+
{ binding: 0, resource: srcView },
|
|
9030
|
+
{ binding: 1, resource: this.materialSampler },
|
|
9031
|
+
{ binding: 2, resource: { buffer: params, offset: m * 256, size: 16 } },
|
|
9032
|
+
],
|
|
9033
|
+
});
|
|
9034
|
+
const p = encoder.beginRenderPass({
|
|
9035
|
+
label: `sky prefilter ${m}`,
|
|
9036
|
+
colorAttachments: [
|
|
9037
|
+
{ view: out.createView({ baseMipLevel: m, mipLevelCount: 1 }), loadOp: "clear", storeOp: "store" },
|
|
9038
|
+
],
|
|
9039
|
+
});
|
|
9040
|
+
p.setPipeline(pipeline);
|
|
9041
|
+
p.setBindGroup(0, group);
|
|
9042
|
+
p.draw(3);
|
|
9043
|
+
p.end();
|
|
9044
|
+
}
|
|
9045
|
+
this.device.queue.submit([encoder.finish()]);
|
|
9046
|
+
// The box chain and the steps are done with once that work is: the source
|
|
9047
|
+
// retires with the skies (destroyed at the next frame's start).
|
|
9048
|
+
this.retiredSkies.push(source);
|
|
9049
|
+
void this.device.queue.onSubmittedWorkDone().then(() => params.destroy());
|
|
9050
|
+
return out;
|
|
8689
9051
|
}
|
|
8690
9052
|
/** Returns whether it actually rebuilt — see rebuildCompositeBindGroup. */
|
|
8691
9053
|
rebuildPerFrameBindGroups() {
|
|
@@ -8704,7 +9066,7 @@ export class Engine {
|
|
|
8704
9066
|
!this.shadowLightVPBuffer ||
|
|
8705
9067
|
!this.materialSampler ||
|
|
8706
9068
|
!this.shadowComparisonSampler ||
|
|
8707
|
-
!this.
|
|
9069
|
+
!this.probeUniformBuffer ||
|
|
8708
9070
|
!(this.worldEquirectView ?? this.fallbackEquirectView)) {
|
|
8709
9071
|
return false;
|
|
8710
9072
|
}
|
|
@@ -8726,8 +9088,31 @@ export class Engine {
|
|
|
8726
9088
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
8727
9089
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
8728
9090
|
{ binding: 8, resource: env },
|
|
8729
|
-
{ binding: 9, resource: this.
|
|
9091
|
+
{ binding: 9, resource: this.cookieView },
|
|
9092
|
+
{ binding: 10, resource: this.castShadowView },
|
|
9093
|
+
{ binding: 11, resource: this.probeView },
|
|
9094
|
+
{ binding: 12, resource: { buffer: this.probeUniformBuffer } },
|
|
9095
|
+
],
|
|
9096
|
+
});
|
|
9097
|
+
// The probe's own view of the scene: one cube face at a time through its
|
|
9098
|
+
// capture camera, and the probe itself switched off — a material inside
|
|
9099
|
+
// the capture reflects the sky, never the cube being filled.
|
|
9100
|
+
this.probeCaptureBindGroup = this.device.createBindGroup({
|
|
9101
|
+
label: "reflection probe capture per-frame bind group",
|
|
9102
|
+
layout: this.mainPerFrameBindGroupLayout,
|
|
9103
|
+
entries: [
|
|
9104
|
+
{ binding: 0, resource: { buffer: this.probeCameraBuffer } },
|
|
9105
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
9106
|
+
{ binding: 2, resource: this.materialSampler },
|
|
9107
|
+
{ binding: 3, resource: this.shadowAtlasView },
|
|
9108
|
+
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
9109
|
+
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
9110
|
+
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
9111
|
+
{ binding: 8, resource: env },
|
|
9112
|
+
{ binding: 9, resource: this.cookieView },
|
|
8730
9113
|
{ binding: 10, resource: this.castShadowView },
|
|
9114
|
+
{ binding: 11, resource: this.probeFallbackView },
|
|
9115
|
+
{ binding: 12, resource: { buffer: this.probeOffBuffer } },
|
|
8731
9116
|
],
|
|
8732
9117
|
});
|
|
8733
9118
|
this.mirrorPerFrameBindGroup = this.device.createBindGroup({
|
|
@@ -8742,8 +9127,10 @@ export class Engine {
|
|
|
8742
9127
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
8743
9128
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
8744
9129
|
{ binding: 8, resource: env },
|
|
8745
|
-
{ binding: 9, resource: this.
|
|
9130
|
+
{ binding: 9, resource: this.cookieView },
|
|
8746
9131
|
{ binding: 10, resource: this.castShadowView },
|
|
9132
|
+
{ binding: 11, resource: this.probeView },
|
|
9133
|
+
{ binding: 12, resource: { buffer: this.probeUniformBuffer } },
|
|
8747
9134
|
],
|
|
8748
9135
|
});
|
|
8749
9136
|
return true;
|
|
@@ -8805,13 +9192,13 @@ export class Engine {
|
|
|
8805
9192
|
this.lightData[base + 4] = this.sun.color.x;
|
|
8806
9193
|
this.lightData[base + 5] = this.sun.color.y;
|
|
8807
9194
|
this.lightData[base + 6] = this.sun.color.z;
|
|
9195
|
+
// Unity's light intensity: a white sun of 1 lights albedo 1:1 at N·L = 1.
|
|
8808
9196
|
this.lightData[base + 7] = this.sun.strength;
|
|
8809
9197
|
this.lightDataWords[DIR_LAYERS_AT + index] = this.sunLayers;
|
|
8810
9198
|
if (index >= this.lightCount)
|
|
8811
9199
|
this.lightCount = index + 1;
|
|
8812
9200
|
this.updateLightBuffer();
|
|
8813
9201
|
}
|
|
8814
|
-
/** Update the world environment (Blender: World Background). Ambient recomputes immediately. */
|
|
8815
9202
|
/**
|
|
8816
9203
|
* Distance fog on every lit surface, as a game on SimPipeline lays it: per
|
|
8817
9204
|
* layer, t = saturate(f − (1 − f)·h) with f = saturate(depth·distance[0] +
|
|
@@ -8939,7 +9326,7 @@ export class Engine {
|
|
|
8939
9326
|
}
|
|
8940
9327
|
this.writeWorld();
|
|
8941
9328
|
}
|
|
8942
|
-
/** Update the
|
|
9329
|
+
/** Update the main light (Unity's directional Sun). Direction change marks shadow VP dirty. */
|
|
8943
9330
|
setSun(options) {
|
|
8944
9331
|
if (options.color)
|
|
8945
9332
|
this.sun.color = options.color;
|
|
@@ -8990,6 +9377,7 @@ export class Engine {
|
|
|
8990
9377
|
...options,
|
|
8991
9378
|
};
|
|
8992
9379
|
this.groundY = opts.y;
|
|
9380
|
+
this.groundHalf = [opts.width / 2, opts.height / 2];
|
|
8993
9381
|
this.createGroundGeometry(opts.width, opts.height, opts.y);
|
|
8994
9382
|
this.createShadowGroundResources(opts);
|
|
8995
9383
|
this.hasGround = true;
|
|
@@ -9040,6 +9428,7 @@ export class Engine {
|
|
|
9040
9428
|
* params make: a scene document's JSON passes straight in, and so does a
|
|
9041
9429
|
* literal typed into a console. Vec3 satisfies it either way. */
|
|
9042
9430
|
lights) {
|
|
9431
|
+
this.sceneLights = lights;
|
|
9043
9432
|
// The directional ones go to the light uniform's slots beside the sun's —
|
|
9044
9433
|
// three of them, in order; the rest are records the grid indexes.
|
|
9045
9434
|
const directional = (lights ?? []).filter((l) => l.kind === "directional");
|
|
@@ -9096,8 +9485,12 @@ export class Engine {
|
|
|
9096
9485
|
this.lightsData[b + 12] = Math.max(inner, outer);
|
|
9097
9486
|
// The layers it does NOT reach, as bits — inverted so a zero reaches all.
|
|
9098
9487
|
this.lightsWords[b + 13] = ~(l.layers ?? ALL_LAYERS) >>> 0;
|
|
9099
|
-
|
|
9100
|
-
this.lightsData[b +
|
|
9488
|
+
// The bulb's radius (rzLightNear): 0 keeps the engine's default
|
|
9489
|
+
this.lightsData[b + 14] = l.near !== undefined && Number.isFinite(l.near) && l.near > 0 ? l.near : 0;
|
|
9490
|
+
// Its cookie's layer + 1, 0 for none or one not loaded yet (a load
|
|
9491
|
+
// rewrites the lamps when it lands).
|
|
9492
|
+
const cookie = l.cookie !== undefined ? this.cookieLayers.get(l.cookie) : undefined;
|
|
9493
|
+
this.lightsData[b + 15] = cookie !== undefined ? cookie + 1 : 0;
|
|
9101
9494
|
}
|
|
9102
9495
|
// The grid, from the records as STORED — normalised aim, clamped reach, the
|
|
9103
9496
|
// cosines in f32 — so it indexes exactly what the shader will evaluate.
|
|
@@ -9121,6 +9514,62 @@ export class Engine {
|
|
|
9121
9514
|
// count included — is written there.
|
|
9122
9515
|
this.allocateLightSlots();
|
|
9123
9516
|
}
|
|
9517
|
+
/**
|
|
9518
|
+
* A cookie, under `key`, for every spot lamp that names it (SceneLight.cookie):
|
|
9519
|
+
* the picture a stage light throws through its gobo, projected along the
|
|
9520
|
+
* cone — its RGB is the light let through, so black blocks, white passes and
|
|
9521
|
+
* a colour tints. Square, stretched to 512². Up to 16 at once; loading a key
|
|
9522
|
+
* again replaces its picture. False when every layer is taken.
|
|
9523
|
+
*/
|
|
9524
|
+
async loadLightCookie(key, source) {
|
|
9525
|
+
if (!this.device)
|
|
9526
|
+
return false;
|
|
9527
|
+
let layer = this.cookieLayers.get(key);
|
|
9528
|
+
if (layer === undefined) {
|
|
9529
|
+
const used = new Set(this.cookieLayers.values());
|
|
9530
|
+
for (let k = 0; k < Engine.COOKIE_LAYERS && layer === undefined; k++)
|
|
9531
|
+
if (!used.has(k))
|
|
9532
|
+
layer = k;
|
|
9533
|
+
if (layer === undefined) {
|
|
9534
|
+
console.warn(`[reze] light cookie "${key}": all ${Engine.COOKIE_LAYERS} cookie layers are in use`);
|
|
9535
|
+
return false;
|
|
9536
|
+
}
|
|
9537
|
+
}
|
|
9538
|
+
const size = Engine.COOKIE_SIZE;
|
|
9539
|
+
const bmp = await createImageBitmap(source, {
|
|
9540
|
+
resizeWidth: size,
|
|
9541
|
+
resizeHeight: size,
|
|
9542
|
+
resizeQuality: "high",
|
|
9543
|
+
colorSpaceConversion: "none",
|
|
9544
|
+
premultiplyAlpha: "none",
|
|
9545
|
+
});
|
|
9546
|
+
// The real array on the first cookie, and every group that binds the
|
|
9547
|
+
// stand-in rebuilt against it (the stand-in itself is a 1x1 and stays).
|
|
9548
|
+
if (this.cookieTexture.width === 1) {
|
|
9549
|
+
this.cookieTexture = this.device.createTexture({
|
|
9550
|
+
label: "light cookies",
|
|
9551
|
+
size: [size, size, Engine.COOKIE_LAYERS],
|
|
9552
|
+
format: "rgba8unorm",
|
|
9553
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
9554
|
+
});
|
|
9555
|
+
this.cookieView = this.cookieTexture.createView({ dimension: "2d-array" });
|
|
9556
|
+
this.worldBindingsDirty = true;
|
|
9557
|
+
this.buildGroundBindGroup();
|
|
9558
|
+
}
|
|
9559
|
+
this.device.queue.copyExternalImageToTexture({ source: bmp }, { texture: this.cookieTexture, origin: [0, 0, layer] }, [size, size, 1]);
|
|
9560
|
+
bmp.close();
|
|
9561
|
+
this.cookieLayers.set(key, layer);
|
|
9562
|
+
if (this.sceneLights)
|
|
9563
|
+
this.setLights(this.sceneLights);
|
|
9564
|
+
return true;
|
|
9565
|
+
}
|
|
9566
|
+
/** Forget a cookie: lamps naming it light plainly again. */
|
|
9567
|
+
removeLightCookie(key) {
|
|
9568
|
+
if (!this.cookieLayers.delete(key))
|
|
9569
|
+
return;
|
|
9570
|
+
if (this.sceneLights)
|
|
9571
|
+
this.setLights(this.sceneLights);
|
|
9572
|
+
}
|
|
9124
9573
|
/** How many positional lights the scene is carrying. */
|
|
9125
9574
|
getLightCount() {
|
|
9126
9575
|
return this.lightHeader[0];
|
|
@@ -9555,6 +10004,29 @@ export class Engine {
|
|
|
9555
10004
|
this.device.queue.writeBuffer(inst.lightBuffer, 0, o);
|
|
9556
10005
|
return true;
|
|
9557
10006
|
}
|
|
10007
|
+
/**
|
|
10008
|
+
* Light one model with more than the world gives it: a FILL, linear RGB times
|
|
10009
|
+
* its surface colour, added after its material graph. Null takes it away again.
|
|
10010
|
+
*
|
|
10011
|
+
* After the graph, not in its ambient: an NPR ramp reads an ambient fill as
|
|
10012
|
+
* light, so sliding it moved her shadows and flipped whole regions across a
|
|
10013
|
+
* hard step. Added after, it brightens her evenly and every shadow stays put.
|
|
10014
|
+
*
|
|
10015
|
+
* A game lights its stage and its characters apart. Aether Gazer's rooms are
|
|
10016
|
+
* lit by their lamps and a near-black ambient, while its characters take a
|
|
10017
|
+
* flat base light of their own — so a stage whose World is right for the room
|
|
10018
|
+
* leaves a face turned from the lamps in the dark. This is that second light,
|
|
10019
|
+
* for the models the host counts as cast. A model drawn by the game's own
|
|
10020
|
+
* shaders (src/unity) lights itself and takes none of it.
|
|
10021
|
+
*/
|
|
10022
|
+
setModelFill(name, fill) {
|
|
10023
|
+
const inst = this.modelInstances.get(name);
|
|
10024
|
+
if (!inst || !this.device)
|
|
10025
|
+
return false;
|
|
10026
|
+
inst.objectLight.set(fill ? [fill.x, fill.y, fill.z, 0] : [0, 0, 0, 0], 44);
|
|
10027
|
+
this.device.queue.writeBuffer(inst.lightBuffer, 0, inst.objectLight);
|
|
10028
|
+
return true;
|
|
10029
|
+
}
|
|
9558
10030
|
/** The per-model group: its skinning matrices and its ObjectLight. */
|
|
9559
10031
|
perInstanceBindGroup(name, skinMatrixBuffer, light) {
|
|
9560
10032
|
return this.device.createBindGroup({
|
|
@@ -9573,6 +10045,9 @@ export class Engine {
|
|
|
9573
10045
|
// Before the texture cache below frees it: a stale entry here would hand a
|
|
9574
10046
|
// destroyed texture to the next setPlaneFrame.
|
|
9575
10047
|
this.planeTextures.delete(name);
|
|
10048
|
+
// Its native look too: it skins from this model's buffers, which are
|
|
10049
|
+
// destroyed below, and would otherwise go on drawing from them.
|
|
10050
|
+
this.nativeLooks?.remove(name);
|
|
9576
10051
|
inst.model.stop();
|
|
9577
10052
|
for (const path of inst.textureCacheKeys) {
|
|
9578
10053
|
const tex = this.textureCache.get(path);
|
|
@@ -11198,8 +11673,11 @@ export class Engine {
|
|
|
11198
11673
|
if (d > most)
|
|
11199
11674
|
most = d;
|
|
11200
11675
|
}
|
|
11201
|
-
|
|
11202
|
-
|
|
11676
|
+
this.raiseSceneExtent(Math.sqrt(most));
|
|
11677
|
+
}
|
|
11678
|
+
/** Raise the scene's reach (never lower it) and hand it to the camera. */
|
|
11679
|
+
raiseSceneExtent(extent) {
|
|
11680
|
+
if (!(extent > this.sceneExtent))
|
|
11203
11681
|
return;
|
|
11204
11682
|
this.sceneExtent = extent;
|
|
11205
11683
|
this.camera?.setSceneExtent(extent);
|
|
@@ -11291,6 +11769,19 @@ export class Engine {
|
|
|
11291
11769
|
maxZ = Math.max(maxZ, data[o + 18] + r);
|
|
11292
11770
|
}
|
|
11293
11771
|
}
|
|
11772
|
+
// The built-in floor, while it is drawn. It is no draw of the cull table,
|
|
11773
|
+
// so without this the box ended at the cast: the cascades stopped just
|
|
11774
|
+
// past her, and a low sun's long shadow ran off the last of them — the
|
|
11775
|
+
// floor beyond read as lit and her head and shoulders went missing.
|
|
11776
|
+
if (this.hasGround && !this.groundIsSuppressed()) {
|
|
11777
|
+
const [hx, hz] = this.groundHalf;
|
|
11778
|
+
minX = Math.min(minX, -hx);
|
|
11779
|
+
maxX = Math.max(maxX, hx);
|
|
11780
|
+
minY = Math.min(minY, this.groundY);
|
|
11781
|
+
maxY = Math.max(maxY, this.groundY);
|
|
11782
|
+
minZ = Math.min(minZ, -hz);
|
|
11783
|
+
maxZ = Math.max(maxZ, hz);
|
|
11784
|
+
}
|
|
11294
11785
|
if (this.nativeStage) {
|
|
11295
11786
|
const nb = this.nativeStage.bounds;
|
|
11296
11787
|
minX = Math.min(minX, nb.min[0]);
|
|
@@ -11590,6 +12081,10 @@ export class Engine {
|
|
|
11590
12081
|
*/
|
|
11591
12082
|
recordBundles() {
|
|
11592
12083
|
this.bundlesDirty = false;
|
|
12084
|
+
// What the bundles record is what the probe would see: a stage arriving or
|
|
12085
|
+
// leaving, a look installed on it, a pipeline landing. Re-captured once it
|
|
12086
|
+
// settles (and switched off when the stage is gone).
|
|
12087
|
+
this.captureReflectionProbe();
|
|
11593
12088
|
const scene = {
|
|
11594
12089
|
colorFormats: sceneColorFormats(this.sceneFormats),
|
|
11595
12090
|
depthStencilFormat: this.depthFormat,
|
|
@@ -11851,7 +12346,14 @@ export class Engine {
|
|
|
11851
12346
|
* and any draw whose slot has not been assigned yet.
|
|
11852
12347
|
*/
|
|
11853
12348
|
issueDraw(pass, draw, kind) {
|
|
11854
|
-
|
|
12349
|
+
// The probe's cube faces have no cull of their own: drawn whole.
|
|
12350
|
+
const args = kind === "probe"
|
|
12351
|
+
? null
|
|
12352
|
+
: kind === "shadow"
|
|
12353
|
+
? this.cullShadowArgs
|
|
12354
|
+
: kind === "mirror"
|
|
12355
|
+
? this.cullMirrorArgs
|
|
12356
|
+
: this.cullCameraArgs;
|
|
11855
12357
|
if (args && draw.cullIndex >= 0) {
|
|
11856
12358
|
pass.drawIndexedIndirect(args, draw.cullIndex * Engine.CULL_ARG_WORDS * 4);
|
|
11857
12359
|
}
|
|
@@ -12095,8 +12597,9 @@ export class Engine {
|
|
|
12095
12597
|
}));
|
|
12096
12598
|
// Its ObjectLight: the world's ambient, and the layers its kind draws on —
|
|
12097
12599
|
// the cast on the character layer as well as the default one. Words:
|
|
12098
|
-
// layers [0..3], ambient flag [4..7], SH [8..43] — see
|
|
12099
|
-
|
|
12600
|
+
// layers [0..3], ambient flag [4..7], SH [8..43], fill [44..47] — see
|
|
12601
|
+
// materials/common.ts.
|
|
12602
|
+
const objectLight = new Float32Array(48);
|
|
12100
12603
|
new Uint32Array(objectLight.buffer)[0] =
|
|
12101
12604
|
isStage || isPlane || isProp ? RENDERING_LAYER_DEFAULT : (RENDERING_LAYER_DEFAULT | RENDERING_LAYER_CHARACTER) >>> 0;
|
|
12102
12605
|
const lightBuffer = this.device.createBuffer({
|
|
@@ -12111,7 +12614,8 @@ export class Engine {
|
|
|
12111
12614
|
layout: this.pickPerInstanceBindGroupLayout,
|
|
12112
12615
|
entries: [{ binding: 0, resource: { buffer: skinMatrixBuffer } }],
|
|
12113
12616
|
});
|
|
12114
|
-
const
|
|
12617
|
+
const attributeBuffer = this.createAttributeBuffer(name, model);
|
|
12618
|
+
const gpuBuffers = [vertexBuffer, indexBuffer, jointsBuffer, weightsBuffer, skinMatrixBuffer, attributeBuffer];
|
|
12115
12619
|
const gpuMorph = this.createGpuMorph(name, model, vertexBuffer, gpuBuffers);
|
|
12116
12620
|
// Cull bounds. The margin is the skinning reach plus the largest single
|
|
12117
12621
|
// vertex-morph displacement — a face morph is millimetres against a reach of
|
|
@@ -12132,6 +12636,7 @@ export class Engine {
|
|
|
12132
12636
|
indexBuffer,
|
|
12133
12637
|
jointsBuffer,
|
|
12134
12638
|
weightsBuffer,
|
|
12639
|
+
attributeBuffer,
|
|
12135
12640
|
skinMatrixBuffer,
|
|
12136
12641
|
wireEdges: new Map(),
|
|
12137
12642
|
drawCalls: [],
|
|
@@ -12177,6 +12682,47 @@ export class Engine {
|
|
|
12177
12682
|
this.bundlesDirty = true;
|
|
12178
12683
|
this.updateOrderDirty = true;
|
|
12179
12684
|
}
|
|
12685
|
+
/**
|
|
12686
|
+
* A model's attribute stream: vertex colour (RGBA) and a second UV set, six
|
|
12687
|
+
* floats a vertex, read by the material pipelines at slot 4.
|
|
12688
|
+
*
|
|
12689
|
+
* PMX has no vertex colour and one UV, but it has four ADDITIONAL UV
|
|
12690
|
+
* channels of four floats each, and that is where a model that needs more
|
|
12691
|
+
* keeps it: the first additional UV is the vertex colour, the second's xy
|
|
12692
|
+
* the second UV — the convention a converted game stage is written in (a
|
|
12693
|
+
* plant's occlusion rides in its vertex alpha, a light card's mask in its
|
|
12694
|
+
* second UV). A model without them reads white and its own UV, which is
|
|
12695
|
+
* what every graph that reads neither already assumed.
|
|
12696
|
+
*/
|
|
12697
|
+
createAttributeBuffer(name, model) {
|
|
12698
|
+
const vertices = model.getVertices();
|
|
12699
|
+
const count = Math.floor(vertices.length / 8);
|
|
12700
|
+
const data = new Float32Array(count * Engine.ATTRIBUTE_FLOATS);
|
|
12701
|
+
const color = model.getAdditionalUv(1);
|
|
12702
|
+
const uv2 = model.getAdditionalUv(2);
|
|
12703
|
+
for (let i = 0; i < count; i++) {
|
|
12704
|
+
const o = i * Engine.ATTRIBUTE_FLOATS;
|
|
12705
|
+
if (color) {
|
|
12706
|
+
data[o] = color[i * 4];
|
|
12707
|
+
data[o + 1] = color[i * 4 + 1];
|
|
12708
|
+
data[o + 2] = color[i * 4 + 2];
|
|
12709
|
+
data[o + 3] = color[i * 4 + 3];
|
|
12710
|
+
}
|
|
12711
|
+
else {
|
|
12712
|
+
data[o] = data[o + 1] = data[o + 2] = data[o + 3] = 1;
|
|
12713
|
+
}
|
|
12714
|
+
data[o + 4] = uv2 ? uv2[i * 4] : vertices[i * 8 + 6];
|
|
12715
|
+
data[o + 5] = uv2 ? uv2[i * 4 + 1] : vertices[i * 8 + 7];
|
|
12716
|
+
}
|
|
12717
|
+
const buffer = this.device.createBuffer({
|
|
12718
|
+
label: `${name}: attribute buffer`,
|
|
12719
|
+
size: Math.max(data.byteLength, 4 * Engine.ATTRIBUTE_FLOATS),
|
|
12720
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
12721
|
+
});
|
|
12722
|
+
if (data.byteLength > 0)
|
|
12723
|
+
this.device.queue.writeBuffer(buffer, 0, data);
|
|
12724
|
+
return buffer;
|
|
12725
|
+
}
|
|
12180
12726
|
// Build the per-model GPU vertex-morph state. Returns null (and leaves the model on the
|
|
12181
12727
|
// CPU morph path) when the model has no vertex morphs. Created buffers are pushed into
|
|
12182
12728
|
// gpuBuffers so they're released with the instance.
|
|
@@ -12303,7 +12849,10 @@ export class Engine {
|
|
|
12303
12849
|
this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices);
|
|
12304
12850
|
}
|
|
12305
12851
|
buildGroundPipeline(soft, mirrored = false) {
|
|
12306
|
-
return this.createRenderPipeline(
|
|
12852
|
+
return this.createRenderPipeline(this.groundPipelineConfig(soft, mirrored));
|
|
12853
|
+
}
|
|
12854
|
+
groundPipelineConfig(soft, mirrored = false) {
|
|
12855
|
+
return {
|
|
12307
12856
|
...this.groundShadowPipelineDesc,
|
|
12308
12857
|
// A REFLECTION FLIPS WINDING — determinant -1, see reflection.ts. The
|
|
12309
12858
|
// ground culls back faces, so drawn into the mirror pass with the ordinary
|
|
@@ -12313,11 +12862,8 @@ export class Engine {
|
|
|
12313
12862
|
// the mirror, and the same answer the scene-pass pipelines already use.
|
|
12314
12863
|
cullMode: mirrored ? "none" : this.groundShadowPipelineDesc.cullMode,
|
|
12315
12864
|
label: `ground shadow pipeline${soft ? " (soft)" : ""}${mirrored ? " (mirror)" : ""}`,
|
|
12316
|
-
shaderModule: this.
|
|
12317
|
-
|
|
12318
|
-
code: groundShaderWgsl(soft),
|
|
12319
|
-
}),
|
|
12320
|
-
});
|
|
12865
|
+
shaderModule: this.cachedShaderModule(groundShaderWgsl(soft), soft ? "ground shadow (soft)" : "ground shadow"),
|
|
12866
|
+
};
|
|
12321
12867
|
}
|
|
12322
12868
|
/** The ground's pipeline for the mirror pass, built on the first frame a
|
|
12323
12869
|
* mirror actually needs it. */
|
|
@@ -12452,6 +12998,7 @@ export class Engine {
|
|
|
12452
12998
|
{ binding: 12, resource: this.groundNoiseView },
|
|
12453
12999
|
{ binding: 13, resource: this.mirrorMaskView },
|
|
12454
13000
|
{ binding: 14, resource: { buffer: this.groundClipOffBuffer } },
|
|
13001
|
+
{ binding: 15, resource: this.cookieView },
|
|
12455
13002
|
],
|
|
12456
13003
|
});
|
|
12457
13004
|
this.groundMirrorViewBindGroup = this.device.createBindGroup({
|
|
@@ -12474,6 +13021,7 @@ export class Engine {
|
|
|
12474
13021
|
{ binding: 12, resource: this.groundNoiseView },
|
|
12475
13022
|
{ binding: 13, resource: this.mirrorDummyColorView },
|
|
12476
13023
|
{ binding: 14, resource: { buffer: this.groundClipMirrorBuffer } },
|
|
13024
|
+
{ binding: 15, resource: this.cookieView },
|
|
12477
13025
|
],
|
|
12478
13026
|
});
|
|
12479
13027
|
if (this.groundDrawCall)
|
|
@@ -12687,6 +13235,17 @@ export class Engine {
|
|
|
12687
13235
|
],
|
|
12688
13236
|
});
|
|
12689
13237
|
outline = { bindGroup: outlineBindGroup };
|
|
13238
|
+
if (!inst.outlineVertexBuffer) {
|
|
13239
|
+
const data = inst.model.getOutlineVertices();
|
|
13240
|
+
const buf = this.device.createBuffer({
|
|
13241
|
+
label: `${prefix}outline normals`,
|
|
13242
|
+
size: data.byteLength,
|
|
13243
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
13244
|
+
});
|
|
13245
|
+
this.device.queue.writeBuffer(buf, 0, data);
|
|
13246
|
+
inst.gpuBuffers.push(buf);
|
|
13247
|
+
inst.outlineVertexBuffer = buf;
|
|
13248
|
+
}
|
|
12690
13249
|
}
|
|
12691
13250
|
// Model-space AABB for the cull compute, grown by the three things that can
|
|
12692
13251
|
// put geometry outside the vertices it was measured from: a vertex morph,
|
|
@@ -13620,39 +14179,6 @@ export class Engine {
|
|
|
13620
14179
|
return new Vec3(x / w, y / w, z / w);
|
|
13621
14180
|
}
|
|
13622
14181
|
// World-space ray from camera through a canvas pixel. Uses WebGPU's NDC z ∈ [0,1].
|
|
13623
|
-
/**
|
|
13624
|
-
* Where a point on the canvas lands on a horizontal plane.
|
|
13625
|
-
*
|
|
13626
|
-
* `px,py` are canvas-relative pixels, top-left origin — what a pointer event
|
|
13627
|
-
* gives you after subtracting the element's rect. Returns null when the ray
|
|
13628
|
-
* cannot reach the plane: parallel to it, or pointing the other way, which is
|
|
13629
|
-
* what a click on the sky above the horizon is.
|
|
13630
|
-
*
|
|
13631
|
-
* The one primitive a placement UI needs. Dragging a thing across the floor is
|
|
13632
|
-
* otherwise three sliders in world units, which asks someone to guess numbers
|
|
13633
|
-
* that have no visible relation to the picture they are looking at — and it
|
|
13634
|
-
* throws away the property that makes pointing work at all: under perspective,
|
|
13635
|
-
* moving something further away makes it smaller by exactly the right amount,
|
|
13636
|
-
* so position and size stop being two controls to tune against each other.
|
|
13637
|
-
*/
|
|
13638
|
-
groundPointAt(px, py, planeY = 0) {
|
|
13639
|
-
const ray = this.buildMouseRay(px, py);
|
|
13640
|
-
if (!ray)
|
|
13641
|
-
return null;
|
|
13642
|
-
// Parallel to the plane: no intersection, and a huge one is not an answer.
|
|
13643
|
-
if (Math.abs(ray.dir.y) < 1e-6)
|
|
13644
|
-
return null;
|
|
13645
|
-
const t = (planeY - ray.origin.y) / ray.dir.y;
|
|
13646
|
-
// Behind the camera — the plane is there, but not in this shot.
|
|
13647
|
-
if (!(t > 0) || !isFinite(t))
|
|
13648
|
-
return null;
|
|
13649
|
-
return new Vec3(ray.origin.x + ray.dir.x * t, planeY, ray.origin.z + ray.dir.z * t);
|
|
13650
|
-
}
|
|
13651
|
-
/** Hand the pointer to something else — a placement drag, a gizmo, a host's own
|
|
13652
|
-
* overlay — so the orbit does not also act on it. */
|
|
13653
|
-
setCameraInputLocked(locked) {
|
|
13654
|
-
this.camera?.setInputLocked(locked);
|
|
13655
|
-
}
|
|
13656
14182
|
buildMouseRay(px, py) {
|
|
13657
14183
|
if (!this.camera)
|
|
13658
14184
|
return null;
|
|
@@ -14052,6 +14578,10 @@ export class Engine {
|
|
|
14052
14578
|
const idsRead = this.idDebug || this.effects.some((e) => e.readsIds || e.readsCastDistance);
|
|
14053
14579
|
idAtt.storeOp = idsRead ? "store" : "discard";
|
|
14054
14580
|
}
|
|
14581
|
+
// The stage's reflection probe, when one is due: its own submits, ahead of
|
|
14582
|
+
// this frame's, so every material below reads the fresh cube.
|
|
14583
|
+
if (hasModels)
|
|
14584
|
+
this.maybeCaptureProbe();
|
|
14055
14585
|
const encoder = this.device.createCommandEncoder();
|
|
14056
14586
|
// GPU vertex morphs: write morphed positions into vertex buffers before any pass reads
|
|
14057
14587
|
// them. WebGPU inserts the storage→vertex barrier between this pass and the render passes.
|
|
@@ -14252,54 +14782,16 @@ export class Engine {
|
|
|
14252
14782
|
// The field mounts, likewise: after the scene so foregrounds can read its
|
|
14253
14783
|
// depth, before the composite that samples both layers.
|
|
14254
14784
|
this.renderFieldPass(encoder);
|
|
14255
|
-
// Bloom
|
|
14256
|
-
//
|
|
14257
|
-
//
|
|
14258
|
-
//
|
|
14259
|
-
//
|
|
14260
|
-
//
|
|
14261
|
-
//
|
|
14262
|
-
|
|
14263
|
-
|
|
14264
|
-
//
|
|
14265
|
-
// is therefore invisible in the frame and nine render passes cheaper.
|
|
14266
|
-
if (this.bloomContributes() && this.bloomBlitBindGroup && this.compositeBindGroup && this.bloomMipCount > 0) {
|
|
14267
|
-
const bloomAtt = this.bloomPassDescriptor.colorAttachments;
|
|
14268
|
-
// 1. Blit — opens the pyramid's timing span. See stampOpen: the nine
|
|
14269
|
-
// passes below read as ONE component, which is the only useful grain.
|
|
14270
|
-
bloomAtt[0].view = this.bloomDownMipViews[0];
|
|
14271
|
-
this.bloomPassDescriptor.timestampWrites = this.stampOpen("bloom");
|
|
14272
|
-
const pBlit = encoder.beginRenderPass(this.bloomPassDescriptor);
|
|
14273
|
-
pBlit.setPipeline(this.bloomBlitPipeline);
|
|
14274
|
-
pBlit.setBindGroup(0, this.bloomBlitBindGroup);
|
|
14275
|
-
pBlit.draw(3);
|
|
14276
|
-
pBlit.end();
|
|
14277
|
-
// 2. Downsample chain
|
|
14278
|
-
this.bloomPassDescriptor.timestampWrites = undefined;
|
|
14279
|
-
for (let i = 1; i < this.bloomMipCount; i++) {
|
|
14280
|
-
bloomAtt[0].view = this.bloomDownMipViews[i];
|
|
14281
|
-
const p = encoder.beginRenderPass(this.bloomPassDescriptor);
|
|
14282
|
-
p.setPipeline(this.bloomDownsamplePipeline);
|
|
14283
|
-
p.setBindGroup(0, this.bloomDownsampleBindGroups[i - 1]);
|
|
14284
|
-
p.draw(3);
|
|
14285
|
-
p.end();
|
|
14286
|
-
}
|
|
14287
|
-
// 3. Upsample chain (coarsest to finest; bindGroups[0] is the coarsest step)
|
|
14288
|
-
const upSteps = this.bloomUpsampleBindGroups.length;
|
|
14289
|
-
const topIdx = this.bloomMipCount - 2;
|
|
14290
|
-
for (let k = 0; k < upSteps; k++) {
|
|
14291
|
-
const levelIdx = topIdx - k; // writes bloomUp[levelIdx]
|
|
14292
|
-
bloomAtt[0].view = this.bloomUpMipViews[levelIdx];
|
|
14293
|
-
// The LAST upsample closes the span opened on the blit.
|
|
14294
|
-
this.bloomPassDescriptor.timestampWrites = k === upSteps - 1 ? this.stampClose("bloom") : undefined;
|
|
14295
|
-
const p = encoder.beginRenderPass(this.bloomPassDescriptor);
|
|
14296
|
-
p.setPipeline(this.bloomUpsamplePipeline);
|
|
14297
|
-
p.setBindGroup(0, this.bloomUpsampleBindGroups[k]);
|
|
14298
|
-
p.draw(3);
|
|
14299
|
-
p.end();
|
|
14300
|
-
}
|
|
14301
|
-
}
|
|
14302
|
-
// Composite: HDR + bloom → Filmic tonemap → swapchain.
|
|
14785
|
+
// Bloom (renderBloom): prefilter, the Gaussian chain down, the scatter
|
|
14786
|
+
// blend back up; the composite adds up[0] × tint × intensity before the view
|
|
14787
|
+
// transform. bloomContributes() gates the whole chain, not just its
|
|
14788
|
+
// intensity. The composite still SAMPLES up[0] unconditionally, which is safe
|
|
14789
|
+
// and deliberate: it scales what it reads by the same effective intensity, so
|
|
14790
|
+
// a stale or never-written chain is multiplied by zero. Skipping the build is
|
|
14791
|
+
// therefore invisible in the frame and a couple of dozen render passes cheaper.
|
|
14792
|
+
if (this.bloomContributes() && this.bloomPrefilterBindGroup && this.compositeBindGroup && this.bloomLevels > 0)
|
|
14793
|
+
this.renderBloom(encoder);
|
|
14794
|
+
// Composite: HDR + bloom → view transform → swapchain.
|
|
14303
14795
|
const swapchainView = this.context.getCurrentTexture().createView();
|
|
14304
14796
|
const compositeAttachment = this.compositePassDescriptor.colorAttachments[0];
|
|
14305
14797
|
compositeAttachment.view = swapchainView;
|
|
@@ -14444,11 +14936,12 @@ export class Engine {
|
|
|
14444
14936
|
outgoing.push([id, install]);
|
|
14445
14937
|
}
|
|
14446
14938
|
}
|
|
14447
|
-
|
|
14448
|
-
|
|
14449
|
-
|
|
14450
|
-
|
|
14451
|
-
|
|
14939
|
+
// ALL GROUPS AT ONCE. Each install touches only its own id — its entry in
|
|
14940
|
+
// styleGroups, its uniform buffer, the draw calls already bound to it — so
|
|
14941
|
+
// the order they land in changes nothing, and each keeps its own staleness
|
|
14942
|
+
// guard (styleGroupGen). Awaiting them in turn made a model's styling the
|
|
14943
|
+
// sum of its compiles; the GPU process runs them side by side.
|
|
14944
|
+
const groupResults = (await Promise.all(groups.map((g) => this.compileAndInstallGroup(inst, g)))).map((r, i) => ({ groupId: groups[i].id, diagnostics: r.diagnostics, ok: r.ok }));
|
|
14452
14945
|
// Repoints every draw call away from the outgoing installs (among
|
|
14453
14946
|
// everything else) — only past this line is destroying them safe.
|
|
14454
14947
|
this.assignDrawCallGroups(inst, claimed);
|
|
@@ -14463,6 +14956,38 @@ export class Engine {
|
|
|
14463
14956
|
conflicts: [...conflicts],
|
|
14464
14957
|
};
|
|
14465
14958
|
}
|
|
14959
|
+
/**
|
|
14960
|
+
* Compile style groups' pipelines before any model needs them — a scene's
|
|
14961
|
+
* groups while its bundle is still downloading. Installs nothing: the
|
|
14962
|
+
* pipelines land in the shader cache, and applyStyleGroups later finds them
|
|
14963
|
+
* there (or still in flight). A group that does not compile is skipped
|
|
14964
|
+
* silently; its real install reports it.
|
|
14965
|
+
*/
|
|
14966
|
+
async prewarmStyleGroups(groups) {
|
|
14967
|
+
if (!this.device)
|
|
14968
|
+
return;
|
|
14969
|
+
await Promise.all(groups.map(async (g) => {
|
|
14970
|
+
const renderClass = g.renderClass ?? "auto";
|
|
14971
|
+
const alphaMode = g.alphaMode ?? "opaque";
|
|
14972
|
+
const blend = g.blend ?? "over";
|
|
14973
|
+
let result;
|
|
14974
|
+
try {
|
|
14975
|
+
result = compileGraph(g.graph, { renderClass, alphaMode, blend });
|
|
14976
|
+
}
|
|
14977
|
+
catch {
|
|
14978
|
+
return;
|
|
14979
|
+
}
|
|
14980
|
+
if (!result.ok)
|
|
14981
|
+
return;
|
|
14982
|
+
const module = this.cachedShaderModule(result.wgsl, `style group: ${g.id} (${renderClass})`);
|
|
14983
|
+
await Promise.all([
|
|
14984
|
+
this.createRenderClassPipeline(renderClass, module, false, true, false, blend),
|
|
14985
|
+
this.createRenderClassPipeline(renderClass, module, false, false, false, blend),
|
|
14986
|
+
renderClass === "hair" ? this.createRenderClassPipeline(renderClass, module, true, true, false, blend) : null,
|
|
14987
|
+
renderClass === "eye" ? this.createRenderClassPipeline(renderClass, module, false, true, true, blend) : null,
|
|
14988
|
+
]).catch(() => { });
|
|
14989
|
+
}));
|
|
14990
|
+
}
|
|
14466
14991
|
/** Add or replace a single style group by id. `opts` may carry a `previewNode` for the
|
|
14467
14992
|
* editor's node-output preview workflow. */
|
|
14468
14993
|
async upsertStyleGroup(modelName, group, opts) {
|
|
@@ -14814,9 +15339,35 @@ export class Engine {
|
|
|
14814
15339
|
const generation = (inst.styleGroupGen.get(group.id) ?? 0) + 1;
|
|
14815
15340
|
inst.styleGroupGen.set(group.id, generation);
|
|
14816
15341
|
this.device.pushErrorScope("validation");
|
|
14817
|
-
const module = this.
|
|
14818
|
-
const
|
|
14819
|
-
|
|
15342
|
+
const module = this.cachedShaderModule(result.wgsl, `style group: ${group.id} (${renderClass})`);
|
|
15343
|
+
const scopePromise = this.device.popErrorScope();
|
|
15344
|
+
// EVERY VARIANT AT ONCE, and the module's diagnostics read beside them
|
|
15345
|
+
// rather than first: the compiles run in parallel in the GPU process, and
|
|
15346
|
+
// awaiting each in turn (and the diagnostics before any) made a group cost
|
|
15347
|
+
// the sum of its pipelines instead of the longest.
|
|
15348
|
+
const blend = group.blend ?? "over";
|
|
15349
|
+
// The single-sided twins a draw of this group will ask for (sidedPipeline),
|
|
15350
|
+
// built with their pipelines rather than synchronously at the first draw.
|
|
15351
|
+
const singleSided = inst.drawCalls.some((dc) => !dc.doubleSided && group.materials.includes(dc.materialName));
|
|
15352
|
+
const variant = (overEyes, depthWrite, mirrored) => this.createRenderClassPipeline(renderClass, module, overEyes, depthWrite, mirrored, blend).then(async (p) => {
|
|
15353
|
+
if (singleSided && !mirrored)
|
|
15354
|
+
await this.buildSidedTwin(p, false);
|
|
15355
|
+
return p;
|
|
15356
|
+
});
|
|
15357
|
+
const variants = Promise.all([
|
|
15358
|
+
variant(false, true, false),
|
|
15359
|
+
// The depth-write-off twin: stage transparency draws with it (see
|
|
15360
|
+
// pipelineForDrawCall), and a future OIT path would too.
|
|
15361
|
+
variant(false, false, false),
|
|
15362
|
+
renderClass === "hair" ? variant(true, true, false) : undefined,
|
|
15363
|
+
// Only the eye needs one: every other class culls "none", which a flipped
|
|
15364
|
+
// winding leaves alone.
|
|
15365
|
+
renderClass === "eye" ? variant(false, true, true) : undefined,
|
|
15366
|
+
]);
|
|
15367
|
+
// Handled below either way; this keeps a rejection off the console while
|
|
15368
|
+
// the diagnostics are read.
|
|
15369
|
+
variants.catch(() => { });
|
|
15370
|
+
const [info, scopeError] = await Promise.all([module.getCompilationInfo(), scopePromise]);
|
|
14820
15371
|
const diagnostics = [...result.diagnostics];
|
|
14821
15372
|
for (const msg of info.messages) {
|
|
14822
15373
|
if (msg.type !== "error")
|
|
@@ -14826,6 +15377,7 @@ export class Engine {
|
|
|
14826
15377
|
if (diagnostics.some((d) => d.severity === "error") || scopeError) {
|
|
14827
15378
|
if (scopeError && !diagnostics.some((d) => d.severity === "error"))
|
|
14828
15379
|
diagnostics.push({ severity: "error", message: `WGSL: ${scopeError.message}` });
|
|
15380
|
+
this.forgetShaderModule(result.wgsl);
|
|
14829
15381
|
return { ok: false, diagnostics, slotMap: result.slotMap };
|
|
14830
15382
|
}
|
|
14831
15383
|
let pipeline;
|
|
@@ -14833,17 +15385,8 @@ export class Engine {
|
|
|
14833
15385
|
let overEyesPipeline;
|
|
14834
15386
|
let mirrorPipeline;
|
|
14835
15387
|
try {
|
|
14836
|
-
|
|
14837
|
-
pipeline
|
|
14838
|
-
// The depth-write-off twin: stage transparency draws with it (see
|
|
14839
|
-
// pipelineForDrawCall), and a future OIT path would too.
|
|
14840
|
-
pipelineNoDepthWrite = await this.createRenderClassPipeline(renderClass, module, false, false, false, blend);
|
|
14841
|
-
if (renderClass === "hair")
|
|
14842
|
-
overEyesPipeline = await this.createRenderClassPipeline(renderClass, module, true, true, false, blend);
|
|
14843
|
-
// Only the eye needs one: every other class culls "none", which a flipped
|
|
14844
|
-
// winding leaves alone.
|
|
14845
|
-
if (renderClass === "eye")
|
|
14846
|
-
mirrorPipeline = await this.createRenderClassPipeline(renderClass, module, false, true, true, blend);
|
|
15388
|
+
;
|
|
15389
|
+
[pipeline, pipelineNoDepthWrite, overEyesPipeline, mirrorPipeline] = await variants;
|
|
14847
15390
|
}
|
|
14848
15391
|
catch (e) {
|
|
14849
15392
|
diagnostics.push({ severity: "error", message: `pipeline creation failed: ${e.message}` });
|
|
@@ -15002,7 +15545,7 @@ export class Engine {
|
|
|
15002
15545
|
const base = {
|
|
15003
15546
|
label: `style ${renderClass}${overEyes ? " (over eyes)" : ""}`,
|
|
15004
15547
|
layout: this.mainPipelineLayout,
|
|
15005
|
-
vertex: { module, buffers: this.
|
|
15548
|
+
vertex: { module, buffers: this.materialVertexBufferLayouts },
|
|
15006
15549
|
// The eye front-culls — the see-through-hair look is draw order plus that
|
|
15007
15550
|
// cull plus the stencil stamp, and it is the deployed appearance of every
|
|
15008
15551
|
// published scene. In the MIRROR the winding is flipped (determinant -1,
|
|
@@ -15061,11 +15604,20 @@ export class Engine {
|
|
|
15061
15604
|
}
|
|
15062
15605
|
const desc = {
|
|
15063
15606
|
...base,
|
|
15064
|
-
fragment: {
|
|
15607
|
+
fragment: {
|
|
15608
|
+
module,
|
|
15609
|
+
constants,
|
|
15610
|
+
targets: blend === "additive"
|
|
15611
|
+
? this.sceneTargetsAdditive
|
|
15612
|
+
: blend === "premultiplied"
|
|
15613
|
+
? this.sceneTargetsPremultiplied
|
|
15614
|
+
: this.sceneTargets,
|
|
15615
|
+
},
|
|
15065
15616
|
depthStencil,
|
|
15066
15617
|
};
|
|
15067
|
-
return this.
|
|
15068
|
-
this.pipelineDescs.
|
|
15618
|
+
return this.cachedRenderPipeline(desc).then((pipeline) => {
|
|
15619
|
+
if (!this.pipelineDescs.has(pipeline))
|
|
15620
|
+
this.pipelineDescs.set(pipeline, desc);
|
|
15069
15621
|
return pipeline;
|
|
15070
15622
|
});
|
|
15071
15623
|
}
|
|
@@ -15076,6 +15628,151 @@ export class Engine {
|
|
|
15076
15628
|
this.pipelineDescs.set(pipeline, desc);
|
|
15077
15629
|
return pipeline;
|
|
15078
15630
|
}
|
|
15631
|
+
static trimCache(cache, max) {
|
|
15632
|
+
while (cache.size > max)
|
|
15633
|
+
cache.delete(cache.keys().next().value);
|
|
15634
|
+
}
|
|
15635
|
+
/** A small stable id per GPU object, for cache keys. */
|
|
15636
|
+
gpuObjectId(o) {
|
|
15637
|
+
let id = this.gpuObjectIds.get(o);
|
|
15638
|
+
if (id === undefined) {
|
|
15639
|
+
id = this.gpuObjectNext++;
|
|
15640
|
+
this.gpuObjectIds.set(o, id);
|
|
15641
|
+
}
|
|
15642
|
+
return id;
|
|
15643
|
+
}
|
|
15644
|
+
/** One module per WGSL source. */
|
|
15645
|
+
cachedShaderModule(code, label) {
|
|
15646
|
+
let module = this.shaderModuleCache.get(code);
|
|
15647
|
+
if (module) {
|
|
15648
|
+
this.shaderModuleCache.delete(code);
|
|
15649
|
+
}
|
|
15650
|
+
else {
|
|
15651
|
+
module = this.device.createShaderModule({ label, code });
|
|
15652
|
+
}
|
|
15653
|
+
this.shaderModuleCache.set(code, module);
|
|
15654
|
+
Engine.trimCache(this.shaderModuleCache, Engine.SHADER_CACHE_MAX);
|
|
15655
|
+
return module;
|
|
15656
|
+
}
|
|
15657
|
+
/** Drop a source whose module did not validate, so a retry reports afresh. */
|
|
15658
|
+
forgetShaderModule(code) {
|
|
15659
|
+
this.shaderModuleCache.delete(code);
|
|
15660
|
+
}
|
|
15661
|
+
/** One bind group layout per entry list. */
|
|
15662
|
+
cachedBindGroupLayout(desc) {
|
|
15663
|
+
const key = JSON.stringify(desc.entries);
|
|
15664
|
+
let layout = this.bindGroupLayoutCache.get(key);
|
|
15665
|
+
if (!layout) {
|
|
15666
|
+
layout = this.device.createBindGroupLayout(desc);
|
|
15667
|
+
this.bindGroupLayoutCache.set(key, layout);
|
|
15668
|
+
}
|
|
15669
|
+
return layout;
|
|
15670
|
+
}
|
|
15671
|
+
/** One pipeline layout per bind group layout list. */
|
|
15672
|
+
cachedPipelineLayout(bindGroupLayouts) {
|
|
15673
|
+
const key = bindGroupLayouts.map((l) => this.gpuObjectId(l)).join(",");
|
|
15674
|
+
let layout = this.pipelineLayoutCache.get(key);
|
|
15675
|
+
if (!layout) {
|
|
15676
|
+
layout = this.device.createPipelineLayout({ bindGroupLayouts });
|
|
15677
|
+
this.pipelineLayoutCache.set(key, layout);
|
|
15678
|
+
}
|
|
15679
|
+
return layout;
|
|
15680
|
+
}
|
|
15681
|
+
pipelineKey(kind, desc) {
|
|
15682
|
+
return (kind +
|
|
15683
|
+
JSON.stringify(desc, (k, v) => k === "label" ? undefined : (k === "module" || k === "layout") && v && typeof v === "object" ? `#${this.gpuObjectId(v)}` : v));
|
|
15684
|
+
}
|
|
15685
|
+
cachedPipeline(key, build) {
|
|
15686
|
+
let p = this.pipelineCache.get(key);
|
|
15687
|
+
if (p) {
|
|
15688
|
+
this.pipelineCache.delete(key);
|
|
15689
|
+
}
|
|
15690
|
+
else {
|
|
15691
|
+
p = build().catch((e) => {
|
|
15692
|
+
this.pipelineCache.delete(key);
|
|
15693
|
+
throw e;
|
|
15694
|
+
});
|
|
15695
|
+
}
|
|
15696
|
+
this.pipelineCache.set(key, p);
|
|
15697
|
+
Engine.trimCache(this.pipelineCache, Engine.PIPELINE_CACHE_MAX);
|
|
15698
|
+
return p;
|
|
15699
|
+
}
|
|
15700
|
+
/** createRenderPipelineAsync, once per module + state. */
|
|
15701
|
+
cachedRenderPipeline(desc) {
|
|
15702
|
+
return this.cachedPipeline(this.pipelineKey("render", desc), () => this.device.createRenderPipelineAsync(desc));
|
|
15703
|
+
}
|
|
15704
|
+
/** createComputePipelineAsync, once per module + state. */
|
|
15705
|
+
cachedComputePipeline(desc) {
|
|
15706
|
+
return this.cachedPipeline(this.pipelineKey("compute", desc), () => this.device.createComputePipelineAsync(desc));
|
|
15707
|
+
}
|
|
15708
|
+
/**
|
|
15709
|
+
* A pipeline init builds: async, in parallel with the rest, and in place by
|
|
15710
|
+
* the time init returns (it awaits every job). Outside init — a pass built
|
|
15711
|
+
* on first use — it is the synchronous build it always was.
|
|
15712
|
+
*/
|
|
15713
|
+
initRenderPipeline(desc, assign, twins = false) {
|
|
15714
|
+
if (!this.initPipelineJobs) {
|
|
15715
|
+
const pipeline = this.device.createRenderPipeline(desc);
|
|
15716
|
+
if (twins)
|
|
15717
|
+
this.pipelineDescs.set(pipeline, desc);
|
|
15718
|
+
assign(pipeline);
|
|
15719
|
+
return;
|
|
15720
|
+
}
|
|
15721
|
+
this.initPipelineJobs.push(this.cachedRenderPipeline(desc)
|
|
15722
|
+
// A rejected build is rebuilt the old way, which hands back the invalid
|
|
15723
|
+
// pipeline and reports through uncapturederror — a bad init pipeline
|
|
15724
|
+
// was never fatal to init, and is not now.
|
|
15725
|
+
.catch(() => this.device.createRenderPipeline(desc))
|
|
15726
|
+
.then((pipeline) => {
|
|
15727
|
+
assign(pipeline);
|
|
15728
|
+
if (!twins)
|
|
15729
|
+
return;
|
|
15730
|
+
this.pipelineDescs.set(pipeline, desc);
|
|
15731
|
+
// The mirror's twin too, but not waited for: only a scene with a
|
|
15732
|
+
// mirror draws it, and it is a cache hit on the module just built.
|
|
15733
|
+
void this.buildSidedTwin(pipeline, true);
|
|
15734
|
+
return this.buildSidedTwin(pipeline, false);
|
|
15735
|
+
}));
|
|
15736
|
+
}
|
|
15737
|
+
/** The compute twin of initRenderPipeline. */
|
|
15738
|
+
initComputePipeline(desc, assign) {
|
|
15739
|
+
if (!this.initPipelineJobs) {
|
|
15740
|
+
assign(this.device.createComputePipeline(desc));
|
|
15741
|
+
return;
|
|
15742
|
+
}
|
|
15743
|
+
this.initPipelineJobs.push(this.cachedComputePipeline(desc)
|
|
15744
|
+
.catch(() => this.device.createComputePipeline(desc))
|
|
15745
|
+
.then(assign));
|
|
15746
|
+
}
|
|
15747
|
+
/**
|
|
15748
|
+
* Build a model pipeline's single-sided twin off the draw path (see
|
|
15749
|
+
* sidedPipeline). Resolves once it is in place; never rejects — a twin that
|
|
15750
|
+
* fails leaves the draw on its double-sided pipeline, as before it existed.
|
|
15751
|
+
*/
|
|
15752
|
+
buildSidedTwin(pipeline, mirrored) {
|
|
15753
|
+
const desc = this.pipelineDescs.get(pipeline);
|
|
15754
|
+
if (!desc || (desc.primitive?.cullMode ?? "none") !== "none")
|
|
15755
|
+
return Promise.resolve();
|
|
15756
|
+
const cache = mirrored ? this.singleSidedMirror : this.singleSided;
|
|
15757
|
+
const pending = mirrored ? this.sidedPendingMirror : this.sidedPending;
|
|
15758
|
+
if (cache.has(pipeline) || pending.has(pipeline))
|
|
15759
|
+
return Promise.resolve();
|
|
15760
|
+
pending.add(pipeline);
|
|
15761
|
+
return this.cachedRenderPipeline({
|
|
15762
|
+
...desc,
|
|
15763
|
+
label: `${desc.label ?? "pipeline"} (single-sided${mirrored ? ", mirror" : ""})`,
|
|
15764
|
+
primitive: { ...desc.primitive, cullMode: mirrored ? "back" : "front" },
|
|
15765
|
+
}).then((twin) => {
|
|
15766
|
+
cache.set(pipeline, twin);
|
|
15767
|
+
pending.delete(pipeline);
|
|
15768
|
+
// A bundle recorded meanwhile holds the double-sided stand-in.
|
|
15769
|
+
this.bundlesDirty = true;
|
|
15770
|
+
}, () => {
|
|
15771
|
+
// Remembered as itself, so the draw stops asking.
|
|
15772
|
+
cache.set(pipeline, pipeline);
|
|
15773
|
+
pending.delete(pipeline);
|
|
15774
|
+
});
|
|
15775
|
+
}
|
|
15079
15776
|
/**
|
|
15080
15777
|
* The pipeline a draw should use once its double-sided flag is honoured.
|
|
15081
15778
|
*
|
|
@@ -15095,17 +15792,15 @@ export class Engine {
|
|
|
15095
15792
|
const desc = this.pipelineDescs.get(pipeline);
|
|
15096
15793
|
if (!desc || (desc.primitive?.cullMode ?? "none") !== "none")
|
|
15097
15794
|
return pipeline;
|
|
15098
|
-
const
|
|
15099
|
-
|
|
15100
|
-
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
}
|
|
15108
|
-
return twin;
|
|
15795
|
+
const twin = (mirrored ? this.singleSidedMirror : this.singleSided).get(pipeline);
|
|
15796
|
+
if (twin)
|
|
15797
|
+
return twin;
|
|
15798
|
+
// NEVER BUILT HERE SYNCHRONOUSLY: a sync build stalls the GPU process for
|
|
15799
|
+
// the whole compile, mid-frame. The camera's twins are built beside their
|
|
15800
|
+
// pipelines; one asked for first here (a mirror's) is started now and the
|
|
15801
|
+
// draw keeps its double-sided pipeline until it lands.
|
|
15802
|
+
void this.buildSidedTwin(pipeline, mirrored);
|
|
15803
|
+
return pipeline;
|
|
15109
15804
|
}
|
|
15110
15805
|
// Pipeline for a material draw call: its group's compiled pipeline when grouped, else
|
|
15111
15806
|
// the neutral base (ungrouped materials render the default graph). Transparent-bucket
|
|
@@ -15136,6 +15831,42 @@ export class Engine {
|
|
|
15136
15831
|
* makes outlines compose like MMD: every material drawn later in the author's
|
|
15137
15832
|
* order covers earlier hulls, and each hull sits over everything drawn before it.
|
|
15138
15833
|
*/
|
|
15834
|
+
/**
|
|
15835
|
+
* A stage's draws, far to near from the eye: its transparent queue, in the
|
|
15836
|
+
* order a game sorts one. Each draw's distance is its material box's centre,
|
|
15837
|
+
* placed, against the camera — a pane or a water surface is one material,
|
|
15838
|
+
* which is the unit a game sorts too. The array is kept per instance and
|
|
15839
|
+
* re-sorted in place, so an unchanged order costs a comparison per draw.
|
|
15840
|
+
*/
|
|
15841
|
+
backToFront(inst) {
|
|
15842
|
+
let list = this.sortedDraws.get(inst);
|
|
15843
|
+
if (!list || list.length !== inst.drawCalls.length || list.some((d) => !inst.drawCalls.includes(d))) {
|
|
15844
|
+
list = [...inst.drawCalls];
|
|
15845
|
+
this.sortedDraws.set(inst, list);
|
|
15846
|
+
}
|
|
15847
|
+
const m = inst.model;
|
|
15848
|
+
const ex = this.cameraMatrixData[32];
|
|
15849
|
+
const ey = this.cameraMatrixData[33];
|
|
15850
|
+
const ez = this.cameraMatrixData[34];
|
|
15851
|
+
const p = this.sortScratch;
|
|
15852
|
+
const dist = this.sortDistance;
|
|
15853
|
+
dist.clear();
|
|
15854
|
+
for (const d of list) {
|
|
15855
|
+
const b = d.bounds;
|
|
15856
|
+
if (!b || b.length < 6 || !(b[0] <= b[3])) {
|
|
15857
|
+
dist.set(d, 0);
|
|
15858
|
+
continue;
|
|
15859
|
+
}
|
|
15860
|
+
p.setXYZ(((b[0] + b[3]) * 0.5) * m.scale, ((b[1] + b[4]) * 0.5) * m.scale, ((b[2] + b[5]) * 0.5) * m.scale);
|
|
15861
|
+
Quat.rotateVecInto(m.rotation, p, p);
|
|
15862
|
+
const dx = p.x + m.position.x - ex;
|
|
15863
|
+
const dy = p.y + m.position.y - ey;
|
|
15864
|
+
const dz = p.z + m.position.z - ez;
|
|
15865
|
+
dist.set(d, dx * dx + dy * dy + dz * dz);
|
|
15866
|
+
}
|
|
15867
|
+
list.sort((a, b) => dist.get(b) - dist.get(a));
|
|
15868
|
+
return list;
|
|
15869
|
+
}
|
|
15139
15870
|
/** Is this draw's compiled class "hair"? Ungrouped draws never are — the
|
|
15140
15871
|
* neutral pipeline is the auto class. */
|
|
15141
15872
|
isHairDraw(inst, dc) {
|
|
@@ -15151,11 +15882,20 @@ export class Engine {
|
|
|
15151
15882
|
only) {
|
|
15152
15883
|
let currentPipeline = null;
|
|
15153
15884
|
let bound = false;
|
|
15154
|
-
|
|
15885
|
+
const outlined = [];
|
|
15886
|
+
// A stage's glass and water in the camera pass go far to near, as a
|
|
15887
|
+
// game's transparent queue does: author order put a far pane over a near
|
|
15888
|
+
// one wherever the author listed it last. Characters keep MMD's author
|
|
15889
|
+
// order — their layers are fabric, built around it.
|
|
15890
|
+
const draws = type === "transparent" && inst.isStage && view.args === "camera" ? this.backToFront(inst) : inst.drawCalls;
|
|
15891
|
+
for (const draw of draws) {
|
|
15155
15892
|
if (draw.type !== type)
|
|
15156
15893
|
continue;
|
|
15157
15894
|
if (only && (only === "hair") !== this.isHairDraw(inst, draw))
|
|
15158
15895
|
continue;
|
|
15896
|
+
// The probe draws directly, past the cull: honour the switches it holds.
|
|
15897
|
+
if (view.args === "probe" && !this.shouldRenderDrawCall(inst, draw))
|
|
15898
|
+
continue;
|
|
15159
15899
|
if (!bound) {
|
|
15160
15900
|
pass.setBindGroup(0, view.perFrame);
|
|
15161
15901
|
pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
|
|
@@ -15169,31 +15909,43 @@ export class Engine {
|
|
|
15169
15909
|
}
|
|
15170
15910
|
pass.setBindGroup(2, draw.bindGroup);
|
|
15171
15911
|
this.issueDraw(pass, draw, view.args);
|
|
15172
|
-
if (draw.outline && this.outlineEnabled && view.outlines)
|
|
15173
|
-
|
|
15174
|
-
|
|
15175
|
-
|
|
15176
|
-
|
|
15177
|
-
|
|
15178
|
-
|
|
15912
|
+
if (draw.outline && this.outlineEnabled && view.outlines)
|
|
15913
|
+
outlined.push(draw);
|
|
15914
|
+
}
|
|
15915
|
+
// THE HULLS AFTER THE SURFACES, as MMD draws a model's edges. A hull held
|
|
15916
|
+
// at the minimum width is faded, and it still writes depth: drawn between
|
|
15917
|
+
// two materials it hid the second wherever it stood in front of it — a
|
|
15918
|
+
// shoe's sole's hull cut the upper behind it, and the floor showed through
|
|
15919
|
+
// as pale lines along the seam. After every surface of this phase it only
|
|
15920
|
+
// ever blends over finished ones. Same index ranges; own pipeline and
|
|
15921
|
+
// groups 0/2 — group 1 (skinMats) is layout-identical and stays bound.
|
|
15922
|
+
if (outlined.length) {
|
|
15923
|
+
const mirrored = view.args === "mirror";
|
|
15924
|
+
pass.setPipeline(mirrored ? this.outlineMirrorPipeline : this.outlinePipeline);
|
|
15925
|
+
pass.setBindGroup(0, mirrored ? this.outlineMirrorPerFrameBindGroup : this.outlinePerFrameBindGroup);
|
|
15926
|
+
// Slot 3: the hull's smoothed normals + edge scale. Only the outline
|
|
15927
|
+
// pipeline reads it; the others leave the slot alone.
|
|
15928
|
+
if (inst.outlineVertexBuffer)
|
|
15929
|
+
pass.setVertexBuffer(3, inst.outlineVertexBuffer);
|
|
15930
|
+
for (const draw of outlined) {
|
|
15179
15931
|
pass.setBindGroup(2, draw.outline.bindGroup);
|
|
15180
15932
|
this.issueDraw(pass, draw, view.args);
|
|
15181
|
-
pass.setBindGroup(0, view.perFrame);
|
|
15182
|
-
currentPipeline = null;
|
|
15183
15933
|
}
|
|
15934
|
+
pass.setBindGroup(0, view.perFrame);
|
|
15184
15935
|
}
|
|
15185
15936
|
}
|
|
15186
15937
|
/**
|
|
15187
15938
|
* Main-pass render sequence for one model instance — babylon-mmd parity:
|
|
15188
15939
|
* opaque bucket, the hair-over-eyes stencil pass, then alpha-blend materials
|
|
15189
|
-
* in PMX author order with depth write ON (forceDepthWrite). Outlines
|
|
15190
|
-
*
|
|
15191
|
-
*
|
|
15940
|
+
* in PMX author order with depth write ON (forceDepthWrite). Outlines close
|
|
15941
|
+
* each drawMaterials call: the edge-flagged materials' hulls after all of its
|
|
15942
|
+
* surfaces, as MMD draws a model's edges.
|
|
15192
15943
|
*/
|
|
15193
15944
|
setModelDrawState(pass, inst) {
|
|
15194
15945
|
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
15195
15946
|
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
15196
15947
|
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
15948
|
+
pass.setVertexBuffer(4, inst.attributeBuffer);
|
|
15197
15949
|
pass.setIndexBuffer(inst.indexBuffer, "uint32");
|
|
15198
15950
|
// The stencil reference used to be set here. It is pass state, not bundle
|
|
15199
15951
|
// state — GPURenderBundleEncoder has no setStencilReference at all — so it
|
|
@@ -15527,7 +16279,7 @@ export class Engine {
|
|
|
15527
16279
|
// still pattern welded to the picture. On the SCENE clock like everything
|
|
15528
16280
|
// else here, so an export reproduces the editor exactly rather than
|
|
15529
16281
|
// scattering differently at whatever rate the encoder ran.
|
|
15530
|
-
u[3] =
|
|
16282
|
+
u[3] = Math.floor((this.sceneClock * 24) % 1024);
|
|
15531
16283
|
u[26] = this.canvas.width;
|
|
15532
16284
|
u[27] = this.canvas.height;
|
|
15533
16285
|
// Camera world position (viewU[10]) — the other half of bgWorldPos. It
|
|
@@ -15768,7 +16520,10 @@ export class Engine {
|
|
|
15768
16520
|
}
|
|
15769
16521
|
for (let s = 0; s < anchors.length; s++) {
|
|
15770
16522
|
const a = CAST_SUBJECT_VEC4S * 4 + (s * MAX_EFFECT_SUBJECTS + n) * 12;
|
|
15771
|
-
|
|
16523
|
+
// `along`: the point that far down the bone's own axis, still model space
|
|
16524
|
+
// here, so the placement below turns and scales it like the joint.
|
|
16525
|
+
const along = anchors[s].along;
|
|
16526
|
+
const pos = along ? m.getBonePointAlong(anchors[s].bone, along) : m.getBoneWorldPosition(anchors[s].bone);
|
|
15772
16527
|
if (!pos) {
|
|
15773
16528
|
cd[a + 3] = 0;
|
|
15774
16529
|
continue;
|
|
@@ -15791,7 +16546,7 @@ export class Engine {
|
|
|
15791
16546
|
cd[a + 5] = vy;
|
|
15792
16547
|
cd[a + 6] = vz;
|
|
15793
16548
|
if (anchors[s].trail)
|
|
15794
|
-
this.writeTrail(inst.name, s, n, pos, cd, a);
|
|
16549
|
+
this.writeTrail(inst.name, s, n, pos, cd, a, anchors[s].step);
|
|
15795
16550
|
const fwd = m.getBoneWorldForward(anchors[s].bone);
|
|
15796
16551
|
if (fwd) {
|
|
15797
16552
|
Quat.rotateVecInto(m.rotation, fwd, fwd);
|
|
@@ -15831,7 +16586,7 @@ export class Engine {
|
|
|
15831
16586
|
// effect's counts before the next upload replaces them.
|
|
15832
16587
|
this.castData.fill(0, CAST_SUBJECT_VEC4S * 4);
|
|
15833
16588
|
}
|
|
15834
|
-
writeTrail(model, slot, n, pos, cd, anchorBase) {
|
|
16589
|
+
writeTrail(model, slot, n, pos, cd, anchorBase, step) {
|
|
15835
16590
|
const key = `${model}\u0000${slot}`;
|
|
15836
16591
|
let ring = this.anchorTrail.get(key);
|
|
15837
16592
|
if (!ring) {
|
|
@@ -15852,9 +16607,47 @@ export class Engine {
|
|
|
15852
16607
|
if (Math.hypot(dx, dy, dz) / dt > 50) {
|
|
15853
16608
|
ring.pos.length = 0;
|
|
15854
16609
|
ring.t.length = 0;
|
|
16610
|
+
ring.live = false;
|
|
16611
|
+
}
|
|
16612
|
+
}
|
|
16613
|
+
if (step && ring.pos.length > 0) {
|
|
16614
|
+
// `trail step d` — the path a game's trail keeps (Unity's
|
|
16615
|
+
// minVertexDistance). Point 0 is the LIVE head: it follows the bone every
|
|
16616
|
+
// frame. It is left behind as a kept point only on a tick, and only once
|
|
16617
|
+
// the bone is `step` from the last point kept, so no segment is shorter
|
|
16618
|
+
// than that and a slowing hand cannot leave a knot of jittery stubs for a
|
|
16619
|
+
// wide ribbon to fan across. Still at most one point per tick, so a stepped
|
|
16620
|
+
// ring never covers LESS time than a sampled one.
|
|
16621
|
+
const live = ring.live === true;
|
|
16622
|
+
const k = live ? 3 : 0;
|
|
16623
|
+
const far = Math.hypot(pos.x - ring.pos[k], pos.y - ring.pos[k + 1], pos.z - ring.pos[k + 2]) >= step;
|
|
16624
|
+
if (live && !(far && this.trailDue > 0)) {
|
|
16625
|
+
ring.pos[0] = pos.x;
|
|
16626
|
+
ring.pos[1] = pos.y;
|
|
16627
|
+
ring.pos[2] = pos.z;
|
|
16628
|
+
ring.t[0] = this.sceneClock;
|
|
16629
|
+
}
|
|
16630
|
+
else {
|
|
16631
|
+
// keep the head where it is as a point (live) or start a new head
|
|
16632
|
+
if (live) {
|
|
16633
|
+
ring.pos[0] = pos.x;
|
|
16634
|
+
ring.pos[1] = pos.y;
|
|
16635
|
+
ring.pos[2] = pos.z;
|
|
16636
|
+
ring.t[0] = this.sceneClock;
|
|
16637
|
+
ring.live = false;
|
|
16638
|
+
}
|
|
16639
|
+
else {
|
|
16640
|
+
ring.pos.unshift(pos.x, pos.y, pos.z);
|
|
16641
|
+
ring.t.unshift(this.sceneClock);
|
|
16642
|
+
ring.live = !(far && this.trailDue > 0);
|
|
16643
|
+
}
|
|
16644
|
+
if (ring.t.length > TRAIL_SAMPLES) {
|
|
16645
|
+
ring.t.length = TRAIL_SAMPLES;
|
|
16646
|
+
ring.pos.length = TRAIL_SAMPLES * 3;
|
|
16647
|
+
}
|
|
15855
16648
|
}
|
|
15856
16649
|
}
|
|
15857
|
-
if (this.trailDue > 0 || ring.pos.length === 0) {
|
|
16650
|
+
else if (this.trailDue > 0 || ring.pos.length === 0) {
|
|
15858
16651
|
// ONE sample per frame, never one per due tick. A frame that spanned
|
|
15859
16652
|
// several 60Hz ticks only knows where the bone is NOW, and unshifting that
|
|
15860
16653
|
// position once per tick fabricated duplicate samples — same point, same
|
|
@@ -15938,10 +16731,32 @@ export class Engine {
|
|
|
15938
16731
|
headSkin: () => (head >= 0 ? inst.model.getSkinMatrices().subarray(head * 16, head * 16 + 16) : null),
|
|
15939
16732
|
headRest,
|
|
15940
16733
|
layers: new Uint32Array(inst.objectLight.buffer)[0],
|
|
16734
|
+
additionalUv: (channel) => inst.model.getAdditionalUv(channel),
|
|
16735
|
+
morphs: inst.model.getMorphing().morphs,
|
|
16736
|
+
morphWeights: () => inst.model.getEffectiveMorphWeights(),
|
|
15941
16737
|
}, look);
|
|
15942
16738
|
this.writeCullHidden(true);
|
|
15943
16739
|
return true;
|
|
15944
16740
|
}
|
|
16741
|
+
/** Resolves once a model's native look can draw: every pass's pipeline is
|
|
16742
|
+
* compiled (async, at setModelNativeLook). Until then its dressed materials
|
|
16743
|
+
* sit out — a host reveals the model after this to avoid that gap. */
|
|
16744
|
+
nativeLookReady(name) {
|
|
16745
|
+
return this.nativeLooks?.ready(name) ?? Promise.resolve();
|
|
16746
|
+
}
|
|
16747
|
+
/**
|
|
16748
|
+
* Set some of the values of one material a model's native look dresses —
|
|
16749
|
+
* its Unity material properties by name, colours linear, as the look's
|
|
16750
|
+
* `values` hold them — over the look's own, until set again. What a game
|
|
16751
|
+
* animates on a material over a take (its timeline's material curves)
|
|
16752
|
+
* reaches the shaders this way, one frame at a time: the caller samples its
|
|
16753
|
+
* keys on its own clock and sets what it got. Only the names the material's
|
|
16754
|
+
* look carries are taken, and a value equal to the one in place writes
|
|
16755
|
+
* nothing. False when the model wears no look dressing that material.
|
|
16756
|
+
*/
|
|
16757
|
+
setModelNativeUniforms(name, material, values) {
|
|
16758
|
+
return this.nativeLooks?.setUniforms(name, material, values) ?? false;
|
|
16759
|
+
}
|
|
15945
16760
|
/** The hosts for the game's shaders: the scene pass's and the shadow atlas's,
|
|
15946
16761
|
* with the stand-ins an empty slot reads. Built on first use. */
|
|
15947
16762
|
ensureNativeHosts() {
|
|
@@ -15990,6 +16805,18 @@ export class Engine {
|
|
|
15990
16805
|
white,
|
|
15991
16806
|
black,
|
|
15992
16807
|
blackCube: solid("native: black cube", [0, 0, 0, 255], true),
|
|
16808
|
+
// an empty volume (a froxel buffer no pass filled): no in-scatter, no extinction
|
|
16809
|
+
emptyVolume: (() => {
|
|
16810
|
+
const t = d.createTexture({
|
|
16811
|
+
label: "native: empty volume",
|
|
16812
|
+
size: [1, 1, 1],
|
|
16813
|
+
dimension: "3d",
|
|
16814
|
+
format: "rgba8unorm",
|
|
16815
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
16816
|
+
});
|
|
16817
|
+
d.queue.writeTexture({ texture: t }, new Uint8Array([0, 0, 0, 0]), { bytesPerRow: 4, rowsPerImage: 1 }, [1, 1, 1]);
|
|
16818
|
+
return t.createView({ dimension: "3d" });
|
|
16819
|
+
})(),
|
|
15993
16820
|
depth: this.nativeNoShadowView,
|
|
15994
16821
|
comparison: this.shadowComparisonSampler,
|
|
15995
16822
|
};
|
|
@@ -16018,6 +16845,8 @@ export class Engine {
|
|
|
16018
16845
|
return;
|
|
16019
16846
|
const scene = this.ensureNativeHosts();
|
|
16020
16847
|
this.nativeStage = await NativeStage.load(this.device, pkg, read, { scene, shadow: this.nativeShadowHost }, { mipmaps: (t, levels) => this.generateMipmaps(t, levels), fallback: this.nativeFallback }, onProgress);
|
|
16848
|
+
// The stage's sky is in it: the far plane reaches as far as the stage does.
|
|
16849
|
+
this.raiseSceneExtent(this.nativeStage.extent);
|
|
16021
16850
|
}
|
|
16022
16851
|
/** What went wrong in the game's shaders, and the names nothing supplied. */
|
|
16023
16852
|
nativeReport() {
|
|
@@ -16065,11 +16894,17 @@ export class Engine {
|
|
|
16065
16894
|
let sunDirection = [dir.x, dir.y, dir.z];
|
|
16066
16895
|
let sunColor = [sun.color.x * k, sun.color.y * k, sun.color.z * k];
|
|
16067
16896
|
let sunShadow = this.sunShadow;
|
|
16897
|
+
let sunUnity = null;
|
|
16068
16898
|
if (stage) {
|
|
16069
16899
|
// Its lamps are the stage's own; its key is the scene's sun, which the
|
|
16070
|
-
// host sets from the stage (and a person may then move)
|
|
16900
|
+
// host sets from the stage (and a person may then move) - along the
|
|
16901
|
+
// scene sun's direction, in the stage's own main-light colour. The scene
|
|
16902
|
+
// sun's colour is the cast's key, which the game keeps apart (a
|
|
16903
|
+
// character's light is _ProbeLightingBase, not the stage's main light).
|
|
16071
16904
|
const L = stage.pkg.lights;
|
|
16072
16905
|
const sc = stage.pkg.scale;
|
|
16906
|
+
sunColor = [L.main.color[0], L.main.color[1], L.main.color[2]];
|
|
16907
|
+
sunUnity = L.main.unity ?? null;
|
|
16073
16908
|
lights.length = 0;
|
|
16074
16909
|
for (const l of L.additional) {
|
|
16075
16910
|
lights.push({
|
|
@@ -16099,6 +16934,7 @@ export class Engine {
|
|
|
16099
16934
|
dt: 1 / 60,
|
|
16100
16935
|
sunDirection,
|
|
16101
16936
|
sunColor,
|
|
16937
|
+
sunUnity,
|
|
16102
16938
|
sunShadow,
|
|
16103
16939
|
lights,
|
|
16104
16940
|
ambientSH: sh ? Array.from(sh, (v) => v * s) : null,
|
|
@@ -16209,6 +17045,8 @@ Engine.GIZMO_WORLD_SIZE = 1.5;
|
|
|
16209
17045
|
Engine.GIZMO_THICKNESS_PX = 15.0;
|
|
16210
17046
|
Engine.GIZMO_PICK_THRESHOLD_PX = 17.0;
|
|
16211
17047
|
Engine.MULTISAMPLE_COUNT = 4;
|
|
17048
|
+
/** Floats per vertex in a model's attribute stream: colour RGBA, then UV2. */
|
|
17049
|
+
Engine.ATTRIBUTE_FLOATS = 6;
|
|
16212
17050
|
/**
|
|
16213
17051
|
* Shadow map depth format — 16-bit, deliberately.
|
|
16214
17052
|
*
|
|
@@ -16376,13 +17214,24 @@ Engine.TIMED_PASSES = [
|
|
|
16376
17214
|
// because two planes would need two of every attachment below. Which one is
|
|
16377
17215
|
// in force is mirrorPlane, refreshed by updateMirrorCamera.
|
|
16378
17216
|
Engine.GROUND_PLANE = [0, 1, 0, 0];
|
|
16379
|
-
Engine.
|
|
16380
|
-
|
|
16381
|
-
Engine.
|
|
17217
|
+
Engine.COOKIE_SIZE = 512;
|
|
17218
|
+
Engine.COOKIE_LAYERS = 16;
|
|
17219
|
+
Engine.PROBE_SETTLE_FRAMES = 20;
|
|
17220
|
+
/** The game's prefilter clamps at 6550.4 — a tenth of the half-float ceiling
|
|
17221
|
+
* (its BloomPass _Params.y of 100 never reaches the shader). */
|
|
17222
|
+
Engine.BLOOM_CLAMP = 6550.4;
|
|
17223
|
+
Engine.BLOOM_MAX_LEVELS = 16;
|
|
16382
17224
|
// Smoothness metrics are computed over a ring buffer of true frame intervals
|
|
16383
17225
|
// (vsync-to-vsync), recomputed at STATS_REFRESH_MS so the readout doesn't flicker.
|
|
16384
17226
|
Engine.STATS_WINDOW = 120;
|
|
16385
17227
|
Engine.STATS_REFRESH_MS = 500;
|
|
17228
|
+
/** viewU[6].y per transform — see viewTransform in composite.ts. */
|
|
17229
|
+
Engine.VIEW_TRANSFORM_ID = {
|
|
17230
|
+
soft: 0,
|
|
17231
|
+
none: 1,
|
|
17232
|
+
neutral: 2,
|
|
17233
|
+
aces: 3,
|
|
17234
|
+
};
|
|
16386
17235
|
/**
|
|
16387
17236
|
* The dials a `#mirror` effect is READ BY — the contract between an effect
|
|
16388
17237
|
* file and the reflection.
|
|
@@ -16446,6 +17295,17 @@ Engine.CULL_MODEL_RIGID = 2;
|
|
|
16446
17295
|
/** A cache key no material can collide with — a PMX name is never empty and
|
|
16447
17296
|
* never contains a NUL. */
|
|
16448
17297
|
Engine.SEAM_KEY = "\u0000seams";
|
|
17298
|
+
// ─── The shader cache ─────────────────────────────────────────────
|
|
17299
|
+
//
|
|
17300
|
+
// A compile is most of a second per distinct fragment shader on a cold
|
|
17301
|
+
// browser cache, and the same source used to be compiled once per CALLER: a
|
|
17302
|
+
// graph per model that carried it, the composite per effect install. These
|
|
17303
|
+
// key a module by its WGSL and a pipeline by its module and every descriptor
|
|
17304
|
+
// field but the label, so a second request for the same thing — in flight or
|
|
17305
|
+
// done — is the first one's promise. Bounded, oldest first, because an editor
|
|
17306
|
+
// session compiles a new source per keystroke.
|
|
17307
|
+
Engine.SHADER_CACHE_MAX = 256;
|
|
17308
|
+
Engine.PIPELINE_CACHE_MAX = 512;
|
|
16449
17309
|
/**
|
|
16450
17310
|
* This frame's Unity globals, keeping last frame's object for every value that
|
|
16451
17311
|
* did not change: the native host refills a uniform member only when its value
|
|
@@ -16459,22 +17319,3 @@ function stableGlobals(prev, next) {
|
|
|
16459
17319
|
}
|
|
16460
17320
|
return next;
|
|
16461
17321
|
}
|
|
16462
|
-
function sameValue(a, b) {
|
|
16463
|
-
if (typeof a === "number" || typeof b === "number")
|
|
16464
|
-
return a === b;
|
|
16465
|
-
if (a.length !== b.length)
|
|
16466
|
-
return false;
|
|
16467
|
-
if (a instanceof Uint32Array !== b instanceof Uint32Array)
|
|
16468
|
-
return false;
|
|
16469
|
-
for (let i = 0; i < a.length; i++) {
|
|
16470
|
-
const x = a[i];
|
|
16471
|
-
const y = b[i];
|
|
16472
|
-
if (typeof x === "number" || typeof y === "number") {
|
|
16473
|
-
if (x !== y)
|
|
16474
|
-
return false;
|
|
16475
|
-
}
|
|
16476
|
-
else if (!sameValue(x, y))
|
|
16477
|
-
return false;
|
|
16478
|
-
}
|
|
16479
|
-
return true;
|
|
16480
|
-
}
|