reze-engine 0.63.0 → 0.64.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 +95 -57
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +465 -162
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/compile.js +4 -1
- package/dist/graph/registry.d.ts +3 -0
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +133 -0
- package/dist/graph/schema.d.ts +12 -0
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +11 -20
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/shaders/lights.d.ts +3 -2
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +16 -3
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +63 -56
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +210 -0
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +13 -1
- package/dist/shaders/passes/cull.d.ts +1 -1
- package/dist/shaders/passes/cull.d.ts.map +1 -1
- package/dist/shaders/passes/cull.js +7 -3
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +32 -55
- package/dist/shaders/passes/particles.d.ts +1 -1
- package/dist/shaders/passes/particles.js +1 -1
- package/dist/shaders/scene-light-api.d.ts +18 -10
- package/dist/shaders/scene-light-api.d.ts.map +1 -1
- package/dist/shaders/scene-light-api.js +58 -46
- package/dist/shadow-cascades.d.ts +47 -20
- package/dist/shadow-cascades.d.ts.map +1 -1
- package/dist/shadow-cascades.js +36 -13
- package/dist/unity/character.d.ts +59 -0
- package/dist/unity/character.d.ts.map +1 -0
- package/dist/unity/character.js +120 -0
- package/dist/unity/globals.d.ts +93 -0
- package/dist/unity/globals.d.ts.map +1 -0
- package/dist/unity/globals.js +340 -0
- package/dist/unity/host.d.ts +137 -0
- package/dist/unity/host.d.ts.map +1 -0
- package/dist/unity/host.js +489 -0
- package/dist/unity/looks.d.ts +88 -0
- package/dist/unity/looks.d.ts.map +1 -0
- package/dist/unity/looks.js +295 -0
- package/dist/unity/skin.d.ts +9 -0
- package/dist/unity/skin.d.ts.map +1 -0
- package/dist/unity/skin.js +117 -0
- package/dist/unity/stage.d.ts +148 -0
- package/dist/unity/stage.d.ts.map +1 -0
- package/dist/unity/stage.js +375 -0
- package/dist/unity/state.d.ts +22 -0
- package/dist/unity/state.d.ts.map +1 -0
- package/dist/unity/state.js +170 -0
- package/package.json +1 -1
- package/src/engine.ts +503 -176
- package/src/graph/compile.ts +3 -1
- package/src/graph/registry.ts +142 -0
- package/src/graph/schema.ts +9 -0
- package/src/graph/slots.ts +11 -20
- package/src/index.ts +11 -0
- package/src/shaders/lights.ts +16 -3
- package/src/shaders/materials/common.ts +63 -57
- package/src/shaders/materials/nodes.ts +210 -0
- package/src/shaders/passes/composite.ts +13 -1
- package/src/shaders/passes/cull.ts +7 -3
- package/src/shaders/passes/ground.ts +32 -55
- package/src/shaders/passes/particles.ts +1 -1
- package/src/shaders/scene-light-api.ts +59 -46
- package/src/shadow-cascades.ts +61 -32
- package/src/unity/character.ts +169 -0
- package/src/unity/globals.ts +443 -0
- package/src/unity/host.ts +587 -0
- package/src/unity/looks.ts +372 -0
- package/src/unity/skin.ts +128 -0
- package/src/unity/stage.ts +568 -0
- package/src/unity/state.ts +172 -0
package/dist/engine.js
CHANGED
|
@@ -22,7 +22,12 @@ import { ID_DEBUG_SHADER_WGSL } from "./shaders/passes/id-debug";
|
|
|
22
22
|
import { paramChanged, sampleParamTrack } from "./param-track";
|
|
23
23
|
import { advanceSim, DISSOLVE_PARAMS, dissolveConstants, dissolveCycleOf, effectState, sampleDissolveCycle, scheduledDissolve, } from "./effect-schedule";
|
|
24
24
|
import { parentKeySpan } from "./parent-keys";
|
|
25
|
-
import {
|
|
25
|
+
import { NativeHost } from "./unity/host";
|
|
26
|
+
import { NativeLooks } from "./unity/looks";
|
|
27
|
+
import { NativeStage } from "./unity/stage";
|
|
28
|
+
import { gameDir, unityFrameGlobals } from "./unity/globals";
|
|
29
|
+
import { outlineMaxOffsetMultiplier, towardMatrix } from "./unity/character";
|
|
30
|
+
import { SHADOW_ATLAS_SIZE, SHADOW_CASCADES, buildShadowCascades, cascadeSpheres } from "./shadow-cascades";
|
|
26
31
|
import { REFLECTION_DEBUG_WGSL, buildMirrorCamera, planeFromPointNormal } from "./reflection";
|
|
27
32
|
import { MIRROR_MASK_DOWNSAMPLE_WGSL, MIRROR_MAT_BYTES, mirrorShaderWgsl, mirrorShadowWgsl } from "./shaders/passes/mirror";
|
|
28
33
|
import { packHalf } from "./hdr";
|
|
@@ -706,6 +711,18 @@ const FIELD_LAYER_BLEND_ADDITIVE = {
|
|
|
706
711
|
// construction rather than by hoping an author stayed inside it.
|
|
707
712
|
alpha: { srcFactor: "one", dstFactor: "one", operation: "add" },
|
|
708
713
|
};
|
|
714
|
+
/**
|
|
715
|
+
* Rendering layers, as the game's pipeline (URP's rendering layers) uses them:
|
|
716
|
+
* every drawing carries layer bits and a light reaches it only where its mask
|
|
717
|
+
* shares one. A stage, prop or plane draws on DEFAULT; the cast on DEFAULT and
|
|
718
|
+
* CHARACTER, the bits Aether Gazer's characters carry (0x40000001).
|
|
719
|
+
*/
|
|
720
|
+
export const RENDERING_LAYER_DEFAULT = 1;
|
|
721
|
+
export const RENDERING_LAYER_CHARACTER = 1 << 30;
|
|
722
|
+
/** Every layer — what a light without a mask reaches. */
|
|
723
|
+
const ALL_LAYERS = 0xffffffff;
|
|
724
|
+
/** Where the directional slots' layer bits sit in the light uniform, in words. */
|
|
725
|
+
const DIR_LAYERS_AT = 108;
|
|
709
726
|
export class Engine {
|
|
710
727
|
static getInstance() {
|
|
711
728
|
if (!Engine.instance) {
|
|
@@ -741,7 +758,11 @@ export class Engine {
|
|
|
741
758
|
// padded to 80. sh[0].w is the IBL flag: 0 = flat world colour, 1 = the sky.
|
|
742
759
|
// …then the scene fog at [72..87] — see setSceneFog — and the cast's
|
|
743
760
|
// shadow on a stage at [88..107] (colour, amount; its view-projection).
|
|
761
|
+
// …and the directional slots' rendering layers at [108..111], as u32 bits
|
|
762
|
+
// through lightDataWords — never through a float.
|
|
744
763
|
this.lightData = new Float32Array(112);
|
|
764
|
+
this.lightDataWords = new Uint32Array(this.lightData.buffer);
|
|
765
|
+
this.sunLayers = ALL_LAYERS;
|
|
745
766
|
this.castShadow = null;
|
|
746
767
|
this.castShadowBundle = null;
|
|
747
768
|
this.castShadowCleared = false;
|
|
@@ -1134,6 +1155,20 @@ export class Engine {
|
|
|
1134
1155
|
/** Scene seconds, advanced by the frame delta — NOT wall time, so an offline
|
|
1135
1156
|
* export samples the same path the editor showed. */
|
|
1136
1157
|
this.sceneClock = 0;
|
|
1158
|
+
/** Models dressed in the game's own materials (unity/looks.ts); built on the
|
|
1159
|
+
* first look installed. */
|
|
1160
|
+
this.nativeLooks = null;
|
|
1161
|
+
/** The scene pass's host for the game's shaders, and the shadow atlas's. */
|
|
1162
|
+
this.nativeHost = null;
|
|
1163
|
+
this.nativeShadowHost = null;
|
|
1164
|
+
/** A game stage drawn by its own shaders (unity/stage.ts). */
|
|
1165
|
+
this.nativeStage = null;
|
|
1166
|
+
/** The frame's Unity globals, computed once for every native draw. */
|
|
1167
|
+
this.nativeGlobals = {};
|
|
1168
|
+
this.nativeFallback = {};
|
|
1169
|
+
/** A 1x1 depth map at the far plane: a game shader's shadow lookup into a
|
|
1170
|
+
* map the scene does not have reads lit. */
|
|
1171
|
+
this.nativeNoShadowView = null;
|
|
1137
1172
|
this.trailAccum = 0;
|
|
1138
1173
|
/** Trail samples owed this frame, computed once so every trail on every
|
|
1139
1174
|
* character samples in lockstep and their paths stay comparable. */
|
|
@@ -1180,8 +1215,6 @@ export class Engine {
|
|
|
1180
1215
|
/** The user's own "no ground" switch — see setGroundVisible. Distinct from
|
|
1181
1216
|
* hasGround, which records whether a ground was ever built. */
|
|
1182
1217
|
this.groundHidden = false;
|
|
1183
|
-
this.shadowMapTextures = [];
|
|
1184
|
-
this.shadowMapDepthViews = [];
|
|
1185
1218
|
// The shadow PASS reads one cascade's matrix per pass, and a uniform binding
|
|
1186
1219
|
// into the aggregate would need 256-byte alignment padding — two tiny buffers
|
|
1187
1220
|
// are simpler than teaching every reader about a stride.
|
|
@@ -1312,10 +1345,6 @@ export class Engine {
|
|
|
1312
1345
|
* resources are half-built, which is exactly the moment a host swaps a sky.
|
|
1313
1346
|
* Retried at the top of a frame until one of them takes. */
|
|
1314
1347
|
this.worldBindingsDirty = false;
|
|
1315
|
-
/** Per cascade: does its map currently hold nothing but the cleared far plane?
|
|
1316
|
-
* Set by the cascade loop, which skips a cascade that is unwanted and already
|
|
1317
|
-
* cleared rather than re-clearing it every frame. */
|
|
1318
|
-
this.shadowCascadeCleared = [];
|
|
1319
1348
|
this.tweenFrom = { position: new Vec3(0, 0, 0), rotation: Quat.identity() };
|
|
1320
1349
|
this.tweenTo = { position: new Vec3(0, 0, 0), rotation: Quat.identity() };
|
|
1321
1350
|
this.tweenPosition = new Vec3(0, 0, 0);
|
|
@@ -1604,6 +1633,10 @@ export class Engine {
|
|
|
1604
1633
|
/** Models this wrote a dissolve onto last frame, so one nothing asks for any
|
|
1605
1634
|
* more comes back whole instead of holding wherever it was left. */
|
|
1606
1635
|
this.dissolveTouched = new Set();
|
|
1636
|
+
// Compile a group's graph → WGSL → pipeline(s), install keyed by group id. Reuses the
|
|
1637
|
+
// install (pipeline + uniform buffer) when the graph/integration is byte-unchanged.
|
|
1638
|
+
/** A graph's own images (ShaderGraph.images), fetched and decoded once per URL. */
|
|
1639
|
+
this.graphImageCache = new Map();
|
|
1607
1640
|
this.canvas = canvas;
|
|
1608
1641
|
const d = DEFAULT_ENGINE_OPTIONS;
|
|
1609
1642
|
this.world = {
|
|
@@ -1855,7 +1888,7 @@ export class Engine {
|
|
|
1855
1888
|
// rebuilt per effect, so the compiled variant IS the flag.
|
|
1856
1889
|
u[11] = this.backdropEquirectView ? 2 : showingWorld ? 3 : bg ? 1 : 0;
|
|
1857
1890
|
// Which display transform forms the frame (see viewTransform in composite.ts).
|
|
1858
|
-
u[25] = v.transform === "agx" ? 2 : v.transform === "standard" ? 1 : 0;
|
|
1891
|
+
u[25] = v.transform === "aether-gazer" ? 3 : v.transform === "agx" ? 2 : v.transform === "standard" ? 1 : 0;
|
|
1859
1892
|
u[26] = this.canvas.width;
|
|
1860
1893
|
u[27] = this.canvas.height;
|
|
1861
1894
|
// ── Grade (viewU[7..9]) ── The UI's three tonal COLORS map to ASC CDL here,
|
|
@@ -4217,8 +4250,7 @@ export class Engine {
|
|
|
4217
4250
|
{ binding: PARTICLE_LIGHT_BINDING, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
4218
4251
|
{ binding: PARTICLE_LIGHT_BINDING + 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
4219
4252
|
{ binding: PARTICLE_LIGHT_BINDING + 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
4220
|
-
{ binding: PARTICLE_LIGHT_BINDING + 3, visibility: GPUShaderStage.FRAGMENT,
|
|
4221
|
-
{ binding: PARTICLE_LIGHT_BINDING + 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
4253
|
+
{ binding: PARTICLE_LIGHT_BINDING + 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
4222
4254
|
]
|
|
4223
4255
|
: []),
|
|
4224
4256
|
// The live count's arguments, for the COUNT kernel's layout alone. The
|
|
@@ -4265,9 +4297,8 @@ export class Engine {
|
|
|
4265
4297
|
? [
|
|
4266
4298
|
{ binding: PARTICLE_LIGHT_BINDING, resource: { buffer: this.lightUniformBuffer } },
|
|
4267
4299
|
{ binding: PARTICLE_LIGHT_BINDING + 1, resource: { buffer: this.shadowLightVPBuffer } },
|
|
4268
|
-
{ binding: PARTICLE_LIGHT_BINDING + 2, resource: this.
|
|
4269
|
-
{ binding: PARTICLE_LIGHT_BINDING + 3, resource: this.
|
|
4270
|
-
{ binding: PARTICLE_LIGHT_BINDING + 4, resource: this.shadowComparisonSampler },
|
|
4300
|
+
{ binding: PARTICLE_LIGHT_BINDING + 2, resource: this.shadowAtlasView },
|
|
4301
|
+
{ binding: PARTICLE_LIGHT_BINDING + 3, resource: this.shadowComparisonSampler },
|
|
4271
4302
|
]
|
|
4272
4303
|
: []),
|
|
4273
4304
|
...(withIndirect && indirect ? [{ binding: PARTICLE_INDIRECT_BINDING, resource: { buffer: indirect } }] : []),
|
|
@@ -6214,11 +6245,6 @@ export class Engine {
|
|
|
6214
6245
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
6215
6246
|
});
|
|
6216
6247
|
this.device.queue.writeTexture({ texture: this.fallbackMaterialTexture }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1]);
|
|
6217
|
-
this.noFillBuffer = this.device.createBuffer({
|
|
6218
|
-
label: "model light (none)",
|
|
6219
|
-
size: 48,
|
|
6220
|
-
usage: GPUBufferUsage.UNIFORM,
|
|
6221
|
-
});
|
|
6222
6248
|
// Generic shared-toon ramp: lit white down to a soft cool shadow tone with
|
|
6223
6249
|
// a tight terminator around the midpoint, approximating MMD's toon ramps.
|
|
6224
6250
|
const TOON_H = 64;
|
|
@@ -6310,10 +6336,10 @@ export class Engine {
|
|
|
6310
6336
|
// The positional lights. Always bound, empty or not, so every material
|
|
6311
6337
|
// pipeline shares one layout whether or not the scene has any.
|
|
6312
6338
|
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
6313
|
-
// The
|
|
6314
|
-
//
|
|
6315
|
-
//
|
|
6316
|
-
|
|
6339
|
+
// The world's sky (8) and the BRDF LUT (9). The sky is always bound —
|
|
6340
|
+
// the 1x1 fallback when the scene has none — so every material
|
|
6341
|
+
// pipeline keeps sharing one layout. (7 was the far cascade's map,
|
|
6342
|
+
// which the atlas at 3 replaces.)
|
|
6317
6343
|
{ binding: 8, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
6318
6344
|
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
6319
6345
|
// The cast's shadow on a stage — setStageCastShadow.
|
|
@@ -6522,15 +6548,15 @@ export class Engine {
|
|
|
6522
6548
|
magFilter: "linear",
|
|
6523
6549
|
minFilter: "linear",
|
|
6524
6550
|
});
|
|
6525
|
-
// One
|
|
6526
|
-
//
|
|
6527
|
-
this.
|
|
6528
|
-
label:
|
|
6529
|
-
size: [
|
|
6551
|
+
// One atlas, a tile per cascade — the layout the game's pipeline uses, and
|
|
6552
|
+
// one binding for every reader.
|
|
6553
|
+
this.shadowAtlasTexture = this.device.createTexture({
|
|
6554
|
+
label: "sun shadow atlas",
|
|
6555
|
+
size: [SHADOW_ATLAS_SIZE, SHADOW_ATLAS_SIZE],
|
|
6530
6556
|
format: Engine.SHADOW_DEPTH_FORMAT,
|
|
6531
6557
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
6532
|
-
})
|
|
6533
|
-
this.
|
|
6558
|
+
});
|
|
6559
|
+
this.shadowAtlasView = this.shadowAtlasTexture.createView();
|
|
6534
6560
|
// The cast's shadow on a stage (setStageCastShadow): one map, only the cast
|
|
6535
6561
|
// in it, from the stage's own direction. Always allocated so every material
|
|
6536
6562
|
// bind group and every instance's shadow bind group stays one shape; drawn
|
|
@@ -6625,7 +6651,6 @@ export class Engine {
|
|
|
6625
6651
|
// Same lights the materials read. A lamp that lit the cast and not the
|
|
6626
6652
|
// floor under her would read as a sticker.
|
|
6627
6653
|
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
6628
|
-
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
6629
6654
|
// The floor mirror: the mirror camera's view-projection, the reflection
|
|
6630
6655
|
// resolve, an ordinary sampler beside the comparison one, and the
|
|
6631
6656
|
// mirror pass's own depth for the depth-proportional blur.
|
|
@@ -8671,7 +8696,7 @@ export class Engine {
|
|
|
8671
8696
|
// picture later.
|
|
8672
8697
|
if (!this.device ||
|
|
8673
8698
|
!this.mainPerFrameBindGroupLayout ||
|
|
8674
|
-
!this.
|
|
8699
|
+
!this.shadowAtlasView ||
|
|
8675
8700
|
!this.cameraUniformBuffer ||
|
|
8676
8701
|
!this.mirrorCameraBuffer ||
|
|
8677
8702
|
!this.lightUniformBuffer ||
|
|
@@ -8696,11 +8721,10 @@ export class Engine {
|
|
|
8696
8721
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
8697
8722
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
8698
8723
|
{ binding: 2, resource: this.materialSampler },
|
|
8699
|
-
{ binding: 3, resource: this.
|
|
8724
|
+
{ binding: 3, resource: this.shadowAtlasView },
|
|
8700
8725
|
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
8701
8726
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
8702
8727
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
8703
|
-
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
8704
8728
|
{ binding: 8, resource: env },
|
|
8705
8729
|
{ binding: 9, resource: this.brdfLutView },
|
|
8706
8730
|
{ binding: 10, resource: this.castShadowView },
|
|
@@ -8713,11 +8737,10 @@ export class Engine {
|
|
|
8713
8737
|
{ binding: 0, resource: { buffer: this.mirrorCameraBuffer } },
|
|
8714
8738
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
8715
8739
|
{ binding: 2, resource: this.materialSampler },
|
|
8716
|
-
{ binding: 3, resource: this.
|
|
8740
|
+
{ binding: 3, resource: this.shadowAtlasView },
|
|
8717
8741
|
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
8718
8742
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
8719
8743
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
8720
|
-
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
8721
8744
|
{ binding: 8, resource: env },
|
|
8722
8745
|
{ binding: 9, resource: this.brdfLutView },
|
|
8723
8746
|
{ binding: 10, resource: this.castShadowView },
|
|
@@ -8783,6 +8806,7 @@ export class Engine {
|
|
|
8783
8806
|
this.lightData[base + 5] = this.sun.color.y;
|
|
8784
8807
|
this.lightData[base + 6] = this.sun.color.z;
|
|
8785
8808
|
this.lightData[base + 7] = this.sun.strength;
|
|
8809
|
+
this.lightDataWords[DIR_LAYERS_AT + index] = this.sunLayers;
|
|
8786
8810
|
if (index >= this.lightCount)
|
|
8787
8811
|
this.lightCount = index + 1;
|
|
8788
8812
|
this.updateLightBuffer();
|
|
@@ -8923,6 +8947,8 @@ export class Engine {
|
|
|
8923
8947
|
this.sun.strength = options.strength;
|
|
8924
8948
|
if (options.shadow !== undefined)
|
|
8925
8949
|
this.sunShadow = Math.min(Math.max(options.shadow, 0), 1);
|
|
8950
|
+
if (options.layers !== undefined)
|
|
8951
|
+
this.sunLayers = options.layers >>> 0;
|
|
8926
8952
|
if (options.direction) {
|
|
8927
8953
|
this.sun.direction = options.direction;
|
|
8928
8954
|
this.shadowLightVPDirty = true;
|
|
@@ -9014,7 +9040,20 @@ export class Engine {
|
|
|
9014
9040
|
* params make: a scene document's JSON passes straight in, and so does a
|
|
9015
9041
|
* literal typed into a console. Vec3 satisfies it either way. */
|
|
9016
9042
|
lights) {
|
|
9017
|
-
|
|
9043
|
+
// The directional ones go to the light uniform's slots beside the sun's —
|
|
9044
|
+
// three of them, in order; the rest are records the grid indexes.
|
|
9045
|
+
const directional = (lights ?? []).filter((l) => l.kind === "directional");
|
|
9046
|
+
for (let k = 1; k < 4; k++) {
|
|
9047
|
+
const l = directional[k - 1];
|
|
9048
|
+
const base = 4 + k * 8;
|
|
9049
|
+
const a = l?.aim;
|
|
9050
|
+
const len = a ? Math.hypot(a.x, a.y, a.z) : 0;
|
|
9051
|
+
const i = l ? Math.max(l.intensity ?? 1, 0) : 0;
|
|
9052
|
+
this.lightData.set(l && len > 0 ? [a.x / len, a.y / len, a.z / len, 0, l.color.x * i, l.color.y * i, l.color.z * i, 1] : [0, 0, -1, 0, 0, 0, 0, 0], base);
|
|
9053
|
+
this.lightDataWords[DIR_LAYERS_AT + k] = l && len > 0 ? (l.layers ?? ALL_LAYERS) >>> 0 : 0;
|
|
9054
|
+
}
|
|
9055
|
+
this.updateLightBuffer();
|
|
9056
|
+
const list = (lights ?? []).filter((l) => l.kind !== "directional").slice(0, MAX_LIGHTS);
|
|
9018
9057
|
this.docLightCount = list.length;
|
|
9019
9058
|
// RECORDS only — the header belongs to allocateLightSlots, the one writer.
|
|
9020
9059
|
// This used to zero-and-upload the header region too, which left two CPU
|
|
@@ -9043,7 +9082,7 @@ export class Engine {
|
|
|
9043
9082
|
// vector cannot be normalised, and a cone around nothing would light
|
|
9044
9083
|
// nothing at all — a light that vanishes because its aim was left at the
|
|
9045
9084
|
// default is worse than one that shines everywhere.
|
|
9046
|
-
const len = l.aim ? Math.hypot(l.aim.x, l.aim.y, l.aim.z) : 0;
|
|
9085
|
+
const len = l.aim && l.kind !== "point" ? Math.hypot(l.aim.x, l.aim.y, l.aim.z) : 0;
|
|
9047
9086
|
this.lightsData[b + 7] = len > 0 ? 1 : 0;
|
|
9048
9087
|
this.lightsData[b + 8] = len > 0 ? l.aim.x / len : 0;
|
|
9049
9088
|
this.lightsData[b + 9] = len > 0 ? l.aim.y / len : 0;
|
|
@@ -9055,7 +9094,8 @@ export class Engine {
|
|
|
9055
9094
|
const inner = len > 0 ? Math.cos((Math.min(Math.max(l.innerAngle ?? (l.angle ?? 45) * 0.8, 0), 179) / 2) * (Math.PI / 180)) : -1;
|
|
9056
9095
|
this.lightsData[b + 11] = outer;
|
|
9057
9096
|
this.lightsData[b + 12] = Math.max(inner, outer);
|
|
9058
|
-
|
|
9097
|
+
// The layers it does NOT reach, as bits — inverted so a zero reaches all.
|
|
9098
|
+
this.lightsWords[b + 13] = ~(l.layers ?? ALL_LAYERS) >>> 0;
|
|
9059
9099
|
this.lightsData[b + 14] = 0;
|
|
9060
9100
|
this.lightsData[b + 15] = 0;
|
|
9061
9101
|
}
|
|
@@ -9444,6 +9484,8 @@ export class Engine {
|
|
|
9444
9484
|
* ground plane must not draw, and the far shadow cascade has nothing to
|
|
9445
9485
|
* cover without one (see the cascade loop). */
|
|
9446
9486
|
hasStage() {
|
|
9487
|
+
if (this.nativeStage)
|
|
9488
|
+
return true;
|
|
9447
9489
|
for (const inst of this.modelInstances.values())
|
|
9448
9490
|
if (inst.isStage)
|
|
9449
9491
|
return true;
|
|
@@ -9493,94 +9535,34 @@ export class Engine {
|
|
|
9493
9535
|
this.modelInstances.set(newName, inst);
|
|
9494
9536
|
}
|
|
9495
9537
|
/**
|
|
9496
|
-
*
|
|
9497
|
-
*
|
|
9498
|
-
*
|
|
9499
|
-
* After the graph, not in its ambient: an NPR ramp read an ambient fill as
|
|
9500
|
-
* light, so sliding it moved her shadows and flipped whole regions across a
|
|
9501
|
-
* hard step. Added after, it brightens her evenly and every shadow stays put.
|
|
9502
|
-
*
|
|
9503
|
-
* A game lights its stage and its characters apart. Aether Gazer's rooms are
|
|
9504
|
-
* lit by their lamps and a near-black ambient, while its characters take a
|
|
9505
|
-
* flat base light of their own — so a stage whose World is right for the room
|
|
9506
|
-
* leaves a face turned from the lamps in the dark. This is that second light,
|
|
9507
|
-
* for the models the host counts as cast.
|
|
9508
|
-
*/
|
|
9509
|
-
setModelFill(name, fill) {
|
|
9510
|
-
return this.writeModelLight(name, 0, fill);
|
|
9511
|
-
}
|
|
9512
|
-
/**
|
|
9513
|
-
* A model's own sun: the colour and strength the scene's sun has FOR THIS
|
|
9514
|
-
* MODEL, in place of the scene's. Null gives it the scene's sun back.
|
|
9538
|
+
* One model's own ambient: the irradiance arriving at it, as the 27 floats of
|
|
9539
|
+
* folded SH setWorldAmbient takes (ibl.ts), in place of the world's. Null
|
|
9540
|
+
* gives it the world's again.
|
|
9515
9541
|
*
|
|
9516
|
-
* The
|
|
9517
|
-
*
|
|
9518
|
-
*
|
|
9519
|
-
* states. So a stage carries the sun it was lit by, and the scene's sun
|
|
9520
|
-
* stays the cast's. Direction and shadow are still the scene's: one sun
|
|
9521
|
-
* casts, and it casts the same way on both.
|
|
9542
|
+
* The game's pipeline lights every renderer by its own probe sample, and a
|
|
9543
|
+
* character by an ambient of its own; a loader that carries those hands them
|
|
9544
|
+
* in here. Linear radiance, strength included.
|
|
9522
9545
|
*/
|
|
9523
|
-
|
|
9524
|
-
return this.writeModelLight(name, 4, sun);
|
|
9525
|
-
}
|
|
9526
|
-
/**
|
|
9527
|
-
* Light one model by the sky's AVERAGE rather than its shape: the world's
|
|
9528
|
-
* colour and brightness from every direction alike. False gives it the
|
|
9529
|
-
* directional sky back.
|
|
9530
|
-
*
|
|
9531
|
-
* For a cast under an anime look. An HDRI's sky is brighter and warmer one
|
|
9532
|
-
* way than another, and taken at each normal it shades a face with soft
|
|
9533
|
-
* realistic gradients that no toon ramp drew — PBR on an anime face. Aether
|
|
9534
|
-
* Gazer gives its characters one flat base light instead. World strength and
|
|
9535
|
-
* colour still reach her; only the shape is gone. A flat World is its own
|
|
9536
|
-
* average, so without an HDRI this changes nothing.
|
|
9537
|
-
*/
|
|
9538
|
-
setModelFlatSky(name, flat) {
|
|
9539
|
-
return this.writeModelLight(name, 8, flat ? new Vec3(1, 0, 0) : null);
|
|
9540
|
-
}
|
|
9541
|
-
/** One part of a model's light, at `at` (0 the fill, 4 the sun, 8 its
|
|
9542
|
-
* options) in its ModelLight; w says whether that part is set. All unset
|
|
9543
|
-
* releases the buffer and the model takes the shared zero stand-in again. */
|
|
9544
|
-
writeModelLight(name, at, value) {
|
|
9546
|
+
setModelAmbient(name, sh) {
|
|
9545
9547
|
const inst = this.modelInstances.get(name);
|
|
9546
9548
|
if (!inst || !this.device)
|
|
9547
9549
|
return false;
|
|
9548
|
-
const
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
if (!inst.lightBuffer)
|
|
9553
|
-
return true;
|
|
9554
|
-
const retired = inst.lightBuffer;
|
|
9555
|
-
inst.lightBuffer = null;
|
|
9556
|
-
inst.modelLight = null;
|
|
9557
|
-
inst.mainPerInstanceBindGroup = this.perInstanceBindGroup(name, inst.skinMatrixBuffer, null);
|
|
9558
|
-
this.bundlesDirty = true;
|
|
9559
|
-
// Retired once the GPU is done with it: an encoded frame may still name it.
|
|
9560
|
-
void this.device.queue.onSubmittedWorkDone().then(() => retired.destroy());
|
|
9561
|
-
return true;
|
|
9562
|
-
}
|
|
9563
|
-
inst.modelLight = light;
|
|
9564
|
-
if (!inst.lightBuffer) {
|
|
9565
|
-
inst.lightBuffer = this.device.createBuffer({
|
|
9566
|
-
label: `${name}: light`,
|
|
9567
|
-
size: 48,
|
|
9568
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
9569
|
-
});
|
|
9570
|
-
inst.mainPerInstanceBindGroup = this.perInstanceBindGroup(name, inst.skinMatrixBuffer, inst.lightBuffer);
|
|
9571
|
-
this.bundlesDirty = true;
|
|
9550
|
+
const o = inst.objectLight;
|
|
9551
|
+
o[4] = sh && sh.length >= 27 ? 1 : 0;
|
|
9552
|
+
for (let i = 0; i < 9; i++) {
|
|
9553
|
+
o.set(sh && sh.length >= 27 ? [sh[i * 3], sh[i * 3 + 1], sh[i * 3 + 2], 0] : [0, 0, 0, 0], 8 + i * 4);
|
|
9572
9554
|
}
|
|
9573
|
-
this.device.queue.writeBuffer(inst.lightBuffer, 0,
|
|
9555
|
+
this.device.queue.writeBuffer(inst.lightBuffer, 0, o);
|
|
9574
9556
|
return true;
|
|
9575
9557
|
}
|
|
9576
|
-
/** The per-model group: its skinning matrices and its
|
|
9558
|
+
/** The per-model group: its skinning matrices and its ObjectLight. */
|
|
9577
9559
|
perInstanceBindGroup(name, skinMatrixBuffer, light) {
|
|
9578
9560
|
return this.device.createBindGroup({
|
|
9579
9561
|
label: `${name}: main per-instance bind group`,
|
|
9580
9562
|
layout: this.mainPerInstanceBindGroupLayout,
|
|
9581
9563
|
entries: [
|
|
9582
9564
|
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
9583
|
-
{ binding: 1, resource: { buffer: light
|
|
9565
|
+
{ binding: 1, resource: { buffer: light } },
|
|
9584
9566
|
],
|
|
9585
9567
|
});
|
|
9586
9568
|
}
|
|
@@ -11162,7 +11144,9 @@ export class Engine {
|
|
|
11162
11144
|
let changed = force;
|
|
11163
11145
|
for (let i = 0; i < this.cullDraws.length; i++) {
|
|
11164
11146
|
const { inst, draw } = this.cullDraws[i];
|
|
11165
|
-
|
|
11147
|
+
// bit 0: switched off; bit 1: drawn by its native look, not its graph
|
|
11148
|
+
const v = (inst.hiddenMaterials.has(draw.materialName) || inst.morphHiddenMaterials.has(draw.materialName) ? 1 : 0) |
|
|
11149
|
+
(this.nativeLooks?.dresses(inst.name, draw.materialName) ? 2 : 0);
|
|
11166
11150
|
if (out[i] !== v) {
|
|
11167
11151
|
out[i] = v;
|
|
11168
11152
|
changed = true;
|
|
@@ -11307,6 +11291,15 @@ export class Engine {
|
|
|
11307
11291
|
maxZ = Math.max(maxZ, data[o + 18] + r);
|
|
11308
11292
|
}
|
|
11309
11293
|
}
|
|
11294
|
+
if (this.nativeStage) {
|
|
11295
|
+
const nb = this.nativeStage.bounds;
|
|
11296
|
+
minX = Math.min(minX, nb.min[0]);
|
|
11297
|
+
maxX = Math.max(maxX, nb.max[0]);
|
|
11298
|
+
minY = Math.min(minY, nb.min[1]);
|
|
11299
|
+
maxY = Math.max(maxY, nb.max[1]);
|
|
11300
|
+
minZ = Math.min(minZ, nb.min[2]);
|
|
11301
|
+
maxZ = Math.max(maxZ, nb.max[2]);
|
|
11302
|
+
}
|
|
11310
11303
|
if (!Number.isFinite(minX)) {
|
|
11311
11304
|
this.shadowSceneBounds = null;
|
|
11312
11305
|
return;
|
|
@@ -12100,7 +12093,19 @@ export class Engine {
|
|
|
12100
12093
|
{ binding: 2, resource: this.materialSampler },
|
|
12101
12094
|
],
|
|
12102
12095
|
}));
|
|
12103
|
-
|
|
12096
|
+
// Its ObjectLight: the world's ambient, and the layers its kind draws on —
|
|
12097
|
+
// 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 materials/common.ts.
|
|
12099
|
+
const objectLight = new Float32Array(44);
|
|
12100
|
+
new Uint32Array(objectLight.buffer)[0] =
|
|
12101
|
+
isStage || isPlane || isProp ? RENDERING_LAYER_DEFAULT : (RENDERING_LAYER_DEFAULT | RENDERING_LAYER_CHARACTER) >>> 0;
|
|
12102
|
+
const lightBuffer = this.device.createBuffer({
|
|
12103
|
+
label: `${name}: object light`,
|
|
12104
|
+
size: objectLight.byteLength,
|
|
12105
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
12106
|
+
});
|
|
12107
|
+
this.device.queue.writeBuffer(lightBuffer, 0, objectLight);
|
|
12108
|
+
const mainPerInstanceBindGroup = this.perInstanceBindGroup(name, skinMatrixBuffer, lightBuffer);
|
|
12104
12109
|
const pickPerInstanceBindGroup = this.device.createBindGroup({
|
|
12105
12110
|
label: `${name}: pick per-instance bind group`,
|
|
12106
12111
|
layout: this.pickPerInstanceBindGroupLayout,
|
|
@@ -12133,8 +12138,8 @@ export class Engine {
|
|
|
12133
12138
|
shadowDrawCalls: [],
|
|
12134
12139
|
shadowBindGroups,
|
|
12135
12140
|
mainPerInstanceBindGroup,
|
|
12136
|
-
lightBuffer
|
|
12137
|
-
|
|
12141
|
+
lightBuffer,
|
|
12142
|
+
objectLight,
|
|
12138
12143
|
pickPerInstanceBindGroup,
|
|
12139
12144
|
pickDrawCalls: [],
|
|
12140
12145
|
isStage,
|
|
@@ -12348,7 +12353,6 @@ export class Engine {
|
|
|
12348
12353
|
gb[3] = fadeStart;
|
|
12349
12354
|
gb[4] = fadeEnd;
|
|
12350
12355
|
gb[5] = shadowStrength;
|
|
12351
|
-
gb[6] = 1 / SHADOW_CASCADES[0].mapSize;
|
|
12352
12356
|
gb[7] = gridSpacing;
|
|
12353
12357
|
gb[8] = gridLineWidth;
|
|
12354
12358
|
gb[9] = gridLineOpacity;
|
|
@@ -12370,19 +12374,6 @@ export class Engine {
|
|
|
12370
12374
|
// Which variant the draw picks. Zero is the sharp shader, which is the one
|
|
12371
12375
|
// that existed before softness did.
|
|
12372
12376
|
this.groundSoft = gb[18] > 0;
|
|
12373
|
-
// gb[17] — does the FAR cascade hold anything?
|
|
12374
|
-
//
|
|
12375
|
-
// It holds something only when a stage is loaded; that is what it exists for
|
|
12376
|
-
// and the cascade loop already skips drawing into it otherwise, leaving it
|
|
12377
|
-
// cleared. A cleared depth map compares as "no occluder", so the ground's far
|
|
12378
|
-
// branch is nine comparison taps whose answer is known in advance.
|
|
12379
|
-
//
|
|
12380
|
-
// That branch runs wherever the NEAR cascade does not reach, and the near one
|
|
12381
|
-
// is a 64-unit box around the camera target — so on a floor receding to the
|
|
12382
|
-
// horizon it is most of the visible pixels, on the most expensive
|
|
12383
|
-
// full-coverage draw in the frame. Skipping it is free in the exact sense:
|
|
12384
|
-
// the shader takes vis = 1.0, which is what the taps would have returned.
|
|
12385
|
-
gb[17] = this.hasStage() ? 1 : 0;
|
|
12386
12377
|
// gb[20..23] — the caster sphere, refreshed every frame by
|
|
12387
12378
|
// writeGroundCasterSphere. Zero here so a frame that renders before the
|
|
12388
12379
|
// first cull (there is one) reads "nothing casts" and skips the taps, which
|
|
@@ -12447,12 +12438,11 @@ export class Engine {
|
|
|
12447
12438
|
entries: [
|
|
12448
12439
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
12449
12440
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
12450
|
-
{ binding: 2, resource: this.
|
|
12441
|
+
{ binding: 2, resource: this.shadowAtlasView },
|
|
12451
12442
|
{ binding: 3, resource: this.shadowComparisonSampler },
|
|
12452
12443
|
{ binding: 4, resource: { buffer: this.groundShadowMaterialBuffer } },
|
|
12453
12444
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
12454
12445
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
12455
|
-
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
12456
12446
|
{ binding: 8, resource: { buffer: this.mirrorVPBuffer } },
|
|
12457
12447
|
// Created in handleResize, which runs during init — before any ground
|
|
12458
12448
|
// can exist to bind it.
|
|
@@ -12470,12 +12460,11 @@ export class Engine {
|
|
|
12470
12460
|
entries: [
|
|
12471
12461
|
{ binding: 0, resource: { buffer: this.mirrorCameraBuffer } },
|
|
12472
12462
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
12473
|
-
{ binding: 2, resource: this.
|
|
12463
|
+
{ binding: 2, resource: this.shadowAtlasView },
|
|
12474
12464
|
{ binding: 3, resource: this.shadowComparisonSampler },
|
|
12475
12465
|
{ binding: 4, resource: { buffer: this.groundShadowMaterialBuffer } },
|
|
12476
12466
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
12477
12467
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
12478
|
-
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
12479
12468
|
{ binding: 8, resource: { buffer: this.mirrorVPBuffer } },
|
|
12480
12469
|
// Stand-ins, not the live mirror textures: this group draws INTO the
|
|
12481
12470
|
// mirror pass, where those are the attachments.
|
|
@@ -14089,54 +14078,54 @@ export class Engine {
|
|
|
14089
14078
|
// the final frame's depth — and the ground, which draws on `hasGround` alone,
|
|
14090
14079
|
// keeps PCF-sampling a character that is no longer in the scene. One clearing
|
|
14091
14080
|
// pass on the transition to empty, then it stops.
|
|
14092
|
-
|
|
14093
|
-
|
|
14094
|
-
|
|
14095
|
-
|
|
14096
|
-
|
|
14097
|
-
//
|
|
14098
|
-
//
|
|
14099
|
-
//
|
|
14100
|
-
//
|
|
14101
|
-
//
|
|
14102
|
-
//
|
|
14103
|
-
//
|
|
14104
|
-
//
|
|
14105
|
-
// 0.43 had ONE shadow map. This is half of what the second one costs.
|
|
14081
|
+
// The game's globals before anything native draws: the stage's casters
|
|
14082
|
+
// into the atlas are the first.
|
|
14083
|
+
if (this.nativeStage || this.nativeLooks?.size)
|
|
14084
|
+
this.prepareNativeLooks(encoder);
|
|
14085
|
+
if (hasModels || this.nativeStage || this.shadowMapPopulated) {
|
|
14086
|
+
// The outer cascades are the STAGE's, and each costs a pass over the whole
|
|
14087
|
+
// cast. A scene with no stage has no set piece out there: every caster
|
|
14088
|
+
// sits inside the inner cascade, which follows the camera target, and
|
|
14089
|
+
// the outer tiles' only readers are ground pixels beyond it, where
|
|
14090
|
+
// nothing is casting. So without a stage only the inner tile is drawn;
|
|
14091
|
+
// the others keep the far plane cascade 0's pass cleared the whole atlas
|
|
14092
|
+
// to, which compares as "no occluder" — the correct answer there.
|
|
14106
14093
|
const stage = this.hasStage();
|
|
14107
14094
|
for (let ci = 0; ci < SHADOW_CASCADES.length; ci++) {
|
|
14108
|
-
|
|
14109
|
-
// Already cleared and still unwanted — nothing to do, and the map still
|
|
14110
|
-
// holds the far plane from the pass that cleared it.
|
|
14111
|
-
if (!wanted && this.shadowCascadeCleared[ci])
|
|
14095
|
+
if (ci > 0 && !stage)
|
|
14112
14096
|
continue;
|
|
14097
|
+
const c = SHADOW_CASCADES[ci];
|
|
14113
14098
|
const sp = encoder.beginRenderPass({
|
|
14114
|
-
// One timestamp pair exists for "shadow"; the
|
|
14099
|
+
// One timestamp pair exists for "shadow"; the inner cascade wears it.
|
|
14115
14100
|
timestampWrites: ci === 0 ? this.stamps("shadow") : undefined,
|
|
14116
14101
|
colorAttachments: [],
|
|
14117
14102
|
depthStencilAttachment: {
|
|
14118
|
-
view: this.
|
|
14103
|
+
view: this.shadowAtlasView,
|
|
14119
14104
|
depthClearValue: 1.0,
|
|
14120
|
-
depthLoadOp: "clear",
|
|
14105
|
+
depthLoadOp: ci === 0 ? "clear" : "load",
|
|
14121
14106
|
depthStoreOp: "store",
|
|
14122
14107
|
},
|
|
14123
14108
|
});
|
|
14109
|
+
// The tile: a bundle draws through the pass's viewport, which it
|
|
14110
|
+
// cannot set itself, so one recording serves whichever tile is live.
|
|
14111
|
+
sp.setViewport(c.origin[0], c.origin[1], c.mapSize, c.mapSize, 0, 1);
|
|
14124
14112
|
// The per-model `visible` test that used to guard this is gone: it is a
|
|
14125
14113
|
// per-frame boolean, and baking it into a bundle would make toggling a
|
|
14126
14114
|
// model re-record. It lives in the cull compute now, which zeroes the
|
|
14127
14115
|
// instance count of an invisible model's draws.
|
|
14128
|
-
if (
|
|
14116
|
+
if (this.shadowBundles[ci])
|
|
14129
14117
|
sp.executeBundles([this.shadowBundles[ci]]);
|
|
14130
14118
|
// The mirror throws shade like anything else standing on the floor.
|
|
14131
14119
|
// Direct rather than in the bundle: the bundles are recorded on scene
|
|
14132
14120
|
// STRUCTURE, and a mirror comes and goes with an effect's weight.
|
|
14133
|
-
if (
|
|
14121
|
+
if (this.mirrorSurface && this.mirrorShadowPipeline) {
|
|
14134
14122
|
sp.setPipeline(this.mirrorShadowPipeline);
|
|
14135
14123
|
sp.setBindGroup(0, this.mirrorShadowBindGroups[ci]);
|
|
14136
14124
|
sp.draw(6);
|
|
14137
14125
|
}
|
|
14126
|
+
if (this.nativeStage && this.nativeShadowHost)
|
|
14127
|
+
this.nativeStage.drawShadow(sp, this.nativeShadowHost, this.nativeGlobals, ci, this.shadowLightVPMatrix.subarray(ci * 16, ci * 16 + 16), 8);
|
|
14138
14128
|
sp.end();
|
|
14139
|
-
this.shadowCascadeCleared[ci] = !wanted;
|
|
14140
14129
|
}
|
|
14141
14130
|
// The cast's shadow on a stage: drawn while a stage asks for it and has a
|
|
14142
14131
|
// cast to shade it with; cleared once when it stops, then left alone.
|
|
@@ -14151,7 +14140,7 @@ export class Engine {
|
|
|
14151
14140
|
cp.end();
|
|
14152
14141
|
this.castShadowCleared = !castOn;
|
|
14153
14142
|
}
|
|
14154
|
-
this.shadowMapPopulated = hasModels;
|
|
14143
|
+
this.shadowMapPopulated = hasModels || !!this.nativeStage;
|
|
14155
14144
|
}
|
|
14156
14145
|
// Before the particles and before the field pass: both may read the grid,
|
|
14157
14146
|
// and a grid stepped after them is one frame stale in everything that used it.
|
|
@@ -14196,6 +14185,17 @@ export class Engine {
|
|
|
14196
14185
|
// is FALSE for the cleared buffer — every such fragment silently rejected.
|
|
14197
14186
|
// One redundant word against a whole class of invisible failure.
|
|
14198
14187
|
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
14188
|
+
// The models dressed in the game's materials, and the game's character
|
|
14189
|
+
// passes after them (unity/looks.ts). They set their own stencil
|
|
14190
|
+
// references, so the engine's is put back after.
|
|
14191
|
+
if (this.nativeLooks?.size) {
|
|
14192
|
+
this.nativeLooks.drawOpaque(pass);
|
|
14193
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
14194
|
+
}
|
|
14195
|
+
if (this.nativeStage && this.nativeHost) {
|
|
14196
|
+
this.nativeStage.drawOpaque(pass, this.nativeHost, this.nativeGlobals, this.nativeFrameTextures(), this.eyeArray());
|
|
14197
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
14198
|
+
}
|
|
14199
14199
|
if (this.hasGround)
|
|
14200
14200
|
this.renderGround(pass);
|
|
14201
14201
|
// After the ground for the same early-z reason, and before the transparent
|
|
@@ -14228,6 +14228,14 @@ export class Engine {
|
|
|
14228
14228
|
// which was never this renderer's bottleneck.
|
|
14229
14229
|
const camView = this.sceneView("camera");
|
|
14230
14230
|
this.forEachInstance((inst) => this.renderModelTransparentPhase(pass, inst, camView));
|
|
14231
|
+
if (this.nativeStage && this.nativeHost) {
|
|
14232
|
+
this.nativeStage.drawTransparent(pass, this.nativeHost, this.nativeGlobals, this.nativeFrameTextures(), this.eyeArray());
|
|
14233
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
14234
|
+
}
|
|
14235
|
+
if (this.nativeLooks?.size) {
|
|
14236
|
+
this.nativeLooks.drawTransparent(pass);
|
|
14237
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
14238
|
+
}
|
|
14231
14239
|
// Last in the pass: depth-tested against everything drawn above, so a
|
|
14232
14240
|
// particle behind the character is simply hidden, and still inside the HDR
|
|
14233
14241
|
// target so an `#bloom` effect reaches the pyramid below.
|
|
@@ -14750,9 +14758,35 @@ export class Engine {
|
|
|
14750
14758
|
claimed.set(m, install.group.id);
|
|
14751
14759
|
return claimed;
|
|
14752
14760
|
}
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14761
|
+
graphImage(url) {
|
|
14762
|
+
let p = this.graphImageCache.get(url);
|
|
14763
|
+
if (!p) {
|
|
14764
|
+
p = fetch(url)
|
|
14765
|
+
.then((r) => (r.ok ? r.blob() : Promise.reject(new Error(`${r.status} ${url}`))))
|
|
14766
|
+
.then((b) => createImageBitmap(b, { colorSpaceConversion: "none", premultiplyAlpha: "none" }))
|
|
14767
|
+
.catch((e) => {
|
|
14768
|
+
console.warn(`[style] graph image: ${e.message}`);
|
|
14769
|
+
return null;
|
|
14770
|
+
});
|
|
14771
|
+
this.graphImageCache.set(url, p);
|
|
14772
|
+
}
|
|
14773
|
+
return p;
|
|
14774
|
+
}
|
|
14775
|
+
/** The group with its graph's images, where the group brings none of its own. */
|
|
14776
|
+
async withGraphImages(group) {
|
|
14777
|
+
const wanted = group.graph.images;
|
|
14778
|
+
if (!wanted?.length || group.images?.length)
|
|
14779
|
+
return group;
|
|
14780
|
+
const images = await Promise.all(wanted.slice(0, 4).map(async (w) => {
|
|
14781
|
+
if (!w)
|
|
14782
|
+
return null;
|
|
14783
|
+
const source = await this.graphImage(w.url);
|
|
14784
|
+
return source ? { source, srgb: w.srgb ?? false } : null;
|
|
14785
|
+
}));
|
|
14786
|
+
return { ...group, images };
|
|
14787
|
+
}
|
|
14788
|
+
async compileAndInstallGroup(inst, groupIn, opts) {
|
|
14789
|
+
const group = await this.withGraphImages(groupIn);
|
|
14756
14790
|
const renderClass = group.renderClass ?? "auto";
|
|
14757
14791
|
const alphaMode = group.alphaMode ?? "opaque";
|
|
14758
14792
|
// THE MAPS ARE PART OF THE SIGNATURE, because a matching one skips the
|
|
@@ -15851,6 +15885,243 @@ export class Engine {
|
|
|
15851
15885
|
// The count rides in the anchor's spare lane, so rzTrailCount is one read.
|
|
15852
15886
|
cd[anchorBase + 11] = count;
|
|
15853
15887
|
}
|
|
15888
|
+
/**
|
|
15889
|
+
* Dress a model in the game's own materials — a look exported with ag-rip
|
|
15890
|
+
* (translated shaders, their images, per-material passes and values, and a
|
|
15891
|
+
* character rig) — or take it off with null. The materials it names are
|
|
15892
|
+
* drawn by the game's shaders from then on; the rest keep their graphs, and
|
|
15893
|
+
* every one still casts its shadow through the engine's pass.
|
|
15894
|
+
*/
|
|
15895
|
+
setModelNativeLook(name, look) {
|
|
15896
|
+
const inst = this.modelInstances.get(name);
|
|
15897
|
+
if (!inst || !this.device)
|
|
15898
|
+
return false;
|
|
15899
|
+
if (!look) {
|
|
15900
|
+
this.nativeLooks?.remove(name);
|
|
15901
|
+
this.writeCullHidden(true);
|
|
15902
|
+
return true;
|
|
15903
|
+
}
|
|
15904
|
+
if (!this.nativeLooks)
|
|
15905
|
+
this.nativeLooks = new NativeLooks(this.device, this.ensureNativeHosts());
|
|
15906
|
+
const skeleton = inst.model.getSkeleton();
|
|
15907
|
+
const head = skeleton.bones.findIndex((b) => b.name === "頭");
|
|
15908
|
+
// The head's rest position: a rigid inverse bind [R t] is the bind pose
|
|
15909
|
+
// inverted, so the bind position is −Rᵀt.
|
|
15910
|
+
let headRest = [0, 0, 0];
|
|
15911
|
+
if (head >= 0) {
|
|
15912
|
+
const m = skeleton.inverseBindMatrices.subarray(head * 16, head * 16 + 16);
|
|
15913
|
+
headRest = [
|
|
15914
|
+
-(m[0] * m[12] + m[1] * m[13] + m[2] * m[14]),
|
|
15915
|
+
-(m[4] * m[12] + m[5] * m[13] + m[6] * m[14]),
|
|
15916
|
+
-(m[8] * m[12] + m[9] * m[13] + m[10] * m[14]),
|
|
15917
|
+
];
|
|
15918
|
+
}
|
|
15919
|
+
const draws = inst.drawCalls
|
|
15920
|
+
.filter((dc) => dc.baseBindGroupEntries)
|
|
15921
|
+
.map((dc) => ({
|
|
15922
|
+
materialName: dc.materialName,
|
|
15923
|
+
firstIndex: dc.firstIndex,
|
|
15924
|
+
count: dc.count,
|
|
15925
|
+
diffuse: dc.baseBindGroupEntries.find((e) => e.binding === 0).resource,
|
|
15926
|
+
}));
|
|
15927
|
+
this.nativeLooks.install({
|
|
15928
|
+
name,
|
|
15929
|
+
vertices: inst.model.getVertices(),
|
|
15930
|
+
indices: inst.model.getIndices(),
|
|
15931
|
+
vertexBuffer: inst.vertexBuffer,
|
|
15932
|
+
jointsBuffer: inst.jointsBuffer,
|
|
15933
|
+
weightsBuffer: inst.weightsBuffer,
|
|
15934
|
+
skinMatrixBuffer: inst.skinMatrixBuffer,
|
|
15935
|
+
indexBuffer: inst.indexBuffer,
|
|
15936
|
+
draws,
|
|
15937
|
+
hidden: (m) => inst.hiddenMaterials.has(m) || inst.morphHiddenMaterials.has(m),
|
|
15938
|
+
headSkin: () => (head >= 0 ? inst.model.getSkinMatrices().subarray(head * 16, head * 16 + 16) : null),
|
|
15939
|
+
headRest,
|
|
15940
|
+
layers: new Uint32Array(inst.objectLight.buffer)[0],
|
|
15941
|
+
}, look);
|
|
15942
|
+
this.writeCullHidden(true);
|
|
15943
|
+
return true;
|
|
15944
|
+
}
|
|
15945
|
+
/** The hosts for the game's shaders: the scene pass's and the shadow atlas's,
|
|
15946
|
+
* with the stand-ins an empty slot reads. Built on first use. */
|
|
15947
|
+
ensureNativeHosts() {
|
|
15948
|
+
if (this.nativeHost)
|
|
15949
|
+
return this.nativeHost;
|
|
15950
|
+
const d = this.device;
|
|
15951
|
+
const noShadow = d.createTexture({
|
|
15952
|
+
label: "native: no shadow",
|
|
15953
|
+
size: [1, 1],
|
|
15954
|
+
format: "depth32float",
|
|
15955
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
15956
|
+
});
|
|
15957
|
+
const enc = d.createCommandEncoder();
|
|
15958
|
+
enc
|
|
15959
|
+
.beginRenderPass({
|
|
15960
|
+
colorAttachments: [],
|
|
15961
|
+
depthStencilAttachment: { view: noShadow.createView(), depthClearValue: 1, depthLoadOp: "clear", depthStoreOp: "store" },
|
|
15962
|
+
})
|
|
15963
|
+
.end();
|
|
15964
|
+
d.queue.submit([enc.finish()]);
|
|
15965
|
+
this.nativeNoShadowView = noShadow.createView();
|
|
15966
|
+
const solid = (label, rgba, cube = false) => {
|
|
15967
|
+
const t = d.createTexture({
|
|
15968
|
+
label,
|
|
15969
|
+
size: [1, 1, cube ? 6 : 1],
|
|
15970
|
+
format: "rgba8unorm",
|
|
15971
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
15972
|
+
});
|
|
15973
|
+
for (let l = 0; l < (cube ? 6 : 1); l++)
|
|
15974
|
+
d.queue.writeTexture({ texture: t, origin: [0, 0, l] }, new Uint8Array(rgba), { bytesPerRow: 4 }, [1, 1]);
|
|
15975
|
+
return t.createView(cube ? { dimension: "cube" } : undefined);
|
|
15976
|
+
};
|
|
15977
|
+
const white = solid("native: white", [255, 255, 255, 255]);
|
|
15978
|
+
const black = solid("native: black", [0, 0, 0, 255]);
|
|
15979
|
+
// Unity's stand-ins for an empty slot, by the name a shader declares.
|
|
15980
|
+
this.nativeFallback = {
|
|
15981
|
+
white,
|
|
15982
|
+
black,
|
|
15983
|
+
"": white,
|
|
15984
|
+
grey: solid("native: grey", [128, 128, 128, 255]),
|
|
15985
|
+
gray: solid("native: gray", [128, 128, 128, 255]),
|
|
15986
|
+
bump: solid("native: bump", [128, 128, 255, 255]),
|
|
15987
|
+
red: solid("native: red", [255, 0, 0, 255]),
|
|
15988
|
+
};
|
|
15989
|
+
const fallback = {
|
|
15990
|
+
white,
|
|
15991
|
+
black,
|
|
15992
|
+
blackCube: solid("native: black cube", [0, 0, 0, 255], true),
|
|
15993
|
+
depth: this.nativeNoShadowView,
|
|
15994
|
+
comparison: this.shadowComparisonSampler,
|
|
15995
|
+
};
|
|
15996
|
+
this.nativeHost = new NativeHost(d, {
|
|
15997
|
+
colorFormats: sceneColorFormats(this.sceneFormats),
|
|
15998
|
+
depthFormat: this.depthFormat,
|
|
15999
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
16000
|
+
reversedZ: this.reversedZ,
|
|
16001
|
+
}, fallback);
|
|
16002
|
+
this.nativeShadowHost = new NativeHost(d, { colorFormats: [], depthFormat: Engine.SHADOW_DEPTH_FORMAT, sampleCount: 1, reversedZ: false }, fallback);
|
|
16003
|
+
return this.nativeHost;
|
|
16004
|
+
}
|
|
16005
|
+
/**
|
|
16006
|
+
* Load a game stage as the game draws it - a package ag-rip's
|
|
16007
|
+
* stage_native.py exported (stage.json and the files beside it, read through
|
|
16008
|
+
* `read` by their paths in it) - or remove the one loaded with null. Its
|
|
16009
|
+
* scene settings (fog, tint, ambient, environment) become the game shaders'
|
|
16010
|
+
* for the whole scene; its lights are the caller's to set, as lights.
|
|
16011
|
+
*/
|
|
16012
|
+
async setNativeStage(pkg, read, onProgress) {
|
|
16013
|
+
const old = this.nativeStage;
|
|
16014
|
+
this.nativeStage = null;
|
|
16015
|
+
if (old)
|
|
16016
|
+
old.destroy([this.nativeHost, this.nativeShadowHost]);
|
|
16017
|
+
if (!pkg || !read || !this.device)
|
|
16018
|
+
return;
|
|
16019
|
+
const scene = this.ensureNativeHosts();
|
|
16020
|
+
this.nativeStage = await NativeStage.load(this.device, pkg, read, { scene, shadow: this.nativeShadowHost }, { mipmaps: (t, levels) => this.generateMipmaps(t, levels), fallback: this.nativeFallback }, onProgress);
|
|
16021
|
+
}
|
|
16022
|
+
/** What went wrong in the game's shaders, and the names nothing supplied. */
|
|
16023
|
+
nativeReport() {
|
|
16024
|
+
const errors = [...(this.nativeHost?.errors ?? []), ...(this.nativeShadowHost?.errors ?? [])];
|
|
16025
|
+
const missing = {};
|
|
16026
|
+
for (const h of [this.nativeHost, this.nativeShadowHost])
|
|
16027
|
+
for (const [k, v] of h?.missing ?? [])
|
|
16028
|
+
missing[k] = (missing[k] ?? 0) + v;
|
|
16029
|
+
return { errors, missing };
|
|
16030
|
+
}
|
|
16031
|
+
nativeFrameTextures() {
|
|
16032
|
+
return { _MainLightShadowmapTexture: this.shadowAtlasView, sim_CharacterShadowmap: this.nativeNoShadowView ?? undefined };
|
|
16033
|
+
}
|
|
16034
|
+
eyeArray() {
|
|
16035
|
+
const e = this.camera.getEyePosition();
|
|
16036
|
+
return [e.x, e.y, e.z];
|
|
16037
|
+
}
|
|
16038
|
+
/** Before the scene pass: the dressed models' skinning and the game's globals. */
|
|
16039
|
+
prepareNativeLooks(encoder) {
|
|
16040
|
+
const looks = this.nativeLooks;
|
|
16041
|
+
const view = this.camera.getViewMatrix().values;
|
|
16042
|
+
const proj = this.camera.getProjectionMatrix().values;
|
|
16043
|
+
const eye = this.camera.getEyePosition();
|
|
16044
|
+
const sun = this.sun;
|
|
16045
|
+
const dir = sun.direction.normalize();
|
|
16046
|
+
const k = sun.strength;
|
|
16047
|
+
// The document's lamps as their records store them.
|
|
16048
|
+
const lights = [];
|
|
16049
|
+
for (let i = 0; i < this.docLightCount; i++) {
|
|
16050
|
+
const b = LIGHT_HEADER + i * LIGHT_STRIDE;
|
|
16051
|
+
const d = this.lightsData;
|
|
16052
|
+
lights.push({
|
|
16053
|
+
position: [d[b], d[b + 1], d[b + 2]],
|
|
16054
|
+
radius: d[b + 3],
|
|
16055
|
+
color: [d[b + 4], d[b + 5], d[b + 6]],
|
|
16056
|
+
aim: [d[b + 8], d[b + 9], d[b + 10]],
|
|
16057
|
+
cosOuter: d[b + 11],
|
|
16058
|
+
cosInner: d[b + 12],
|
|
16059
|
+
layers: ~this.lightsWords[b + 13] >>> 0,
|
|
16060
|
+
});
|
|
16061
|
+
}
|
|
16062
|
+
// A game stage brings its own lights: the game's shaders get exactly those
|
|
16063
|
+
// (its sun, its lamps, all layers), whatever the scene's lamps are.
|
|
16064
|
+
const stage = this.nativeStage;
|
|
16065
|
+
let sunDirection = [dir.x, dir.y, dir.z];
|
|
16066
|
+
let sunColor = [sun.color.x * k, sun.color.y * k, sun.color.z * k];
|
|
16067
|
+
let sunShadow = this.sunShadow;
|
|
16068
|
+
if (stage) {
|
|
16069
|
+
// 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).
|
|
16071
|
+
const L = stage.pkg.lights;
|
|
16072
|
+
const sc = stage.pkg.scale;
|
|
16073
|
+
lights.length = 0;
|
|
16074
|
+
for (const l of L.additional) {
|
|
16075
|
+
lights.push({
|
|
16076
|
+
position: [-l.position[0] * sc, l.position[1] * sc, -l.position[2] * sc],
|
|
16077
|
+
radius: l.range * sc,
|
|
16078
|
+
color: [l.color[0] * sc * sc, l.color[1] * sc * sc, l.color[2] * sc * sc],
|
|
16079
|
+
aim: (l.aim ? gameDir(l.aim) : [0, 0, 0]),
|
|
16080
|
+
cosOuter: l.cosOuter ?? -1,
|
|
16081
|
+
cosInner: l.cosInner ?? -1,
|
|
16082
|
+
layers: 0xffffffff,
|
|
16083
|
+
unity: l.unity,
|
|
16084
|
+
});
|
|
16085
|
+
}
|
|
16086
|
+
}
|
|
16087
|
+
const s = this.world.strength;
|
|
16088
|
+
const sh = this.worldAmbientSH ?? this.worldSH ?? this.worldGradientSH;
|
|
16089
|
+
const g = unityFrameGlobals({
|
|
16090
|
+
scale: 8,
|
|
16091
|
+
view,
|
|
16092
|
+
proj,
|
|
16093
|
+
eye: [eye.x, eye.y, eye.z],
|
|
16094
|
+
near: this.camera.near,
|
|
16095
|
+
far: this.camera.far,
|
|
16096
|
+
width: this.canvas.width,
|
|
16097
|
+
height: this.canvas.height,
|
|
16098
|
+
time: this.sceneClock,
|
|
16099
|
+
dt: 1 / 60,
|
|
16100
|
+
sunDirection,
|
|
16101
|
+
sunColor,
|
|
16102
|
+
sunShadow,
|
|
16103
|
+
lights,
|
|
16104
|
+
ambientSH: sh ? Array.from(sh, (v) => v * s) : null,
|
|
16105
|
+
ambientFlat: [this.world.color.x * s, this.world.color.y * s, this.world.color.z * s],
|
|
16106
|
+
shadow: {
|
|
16107
|
+
viewProj: SHADOW_CASCADES.map((_, i) => this.shadowLightVPMatrix.subarray(i * 16, i * 16 + 16)),
|
|
16108
|
+
tiles: SHADOW_CASCADES.map((c) => [c.origin[0], c.origin[1]]),
|
|
16109
|
+
tileSize: SHADOW_CASCADES[0].mapSize,
|
|
16110
|
+
atlasSize: SHADOW_ATLAS_SIZE,
|
|
16111
|
+
spheres: cascadeSpheres(this.shadowView, this.shadowSceneBounds).map((p) => ({
|
|
16112
|
+
center: [p.center.x, p.center.y, p.center.z],
|
|
16113
|
+
radius: p.radius,
|
|
16114
|
+
})),
|
|
16115
|
+
},
|
|
16116
|
+
settings: this.nativeStage?.pkg.settings ?? null,
|
|
16117
|
+
});
|
|
16118
|
+
g.sim_TowardMatrix = towardMatrix(gameDir([view[2], view[6], view[10]]));
|
|
16119
|
+
g._OutlineMaxOffsetMultiplier = outlineMaxOffsetMultiplier(this.canvas.width);
|
|
16120
|
+
this.nativeGlobals = stableGlobals(this.nativeGlobals, g);
|
|
16121
|
+
this.nativeHost?.beginFrame();
|
|
16122
|
+
this.nativeShadowHost?.beginFrame();
|
|
16123
|
+
looks?.prepare(encoder, this.nativeGlobals, 8, this.nativeFrameTextures());
|
|
16124
|
+
}
|
|
15854
16125
|
updateSkinMatrices() {
|
|
15855
16126
|
this.forEachInstance((inst) => {
|
|
15856
16127
|
// Only a pose pass can change these, and an idle stage did not run one —
|
|
@@ -16175,3 +16446,35 @@ Engine.CULL_MODEL_RIGID = 2;
|
|
|
16175
16446
|
/** A cache key no material can collide with — a PMX name is never empty and
|
|
16176
16447
|
* never contains a NUL. */
|
|
16177
16448
|
Engine.SEAM_KEY = "\u0000seams";
|
|
16449
|
+
/**
|
|
16450
|
+
* This frame's Unity globals, keeping last frame's object for every value that
|
|
16451
|
+
* did not change: the native host refills a uniform member only when its value
|
|
16452
|
+
* object is new, so a light table or an SH that holds still costs nothing.
|
|
16453
|
+
*/
|
|
16454
|
+
function stableGlobals(prev, next) {
|
|
16455
|
+
for (const k in next) {
|
|
16456
|
+
const a = prev[k];
|
|
16457
|
+
if (a !== undefined && sameValue(a, next[k]))
|
|
16458
|
+
next[k] = a;
|
|
16459
|
+
}
|
|
16460
|
+
return next;
|
|
16461
|
+
}
|
|
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
|
+
}
|