reze-engine 0.62.1 → 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.
Files changed (83) hide show
  1. package/dist/engine.d.ts +95 -57
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +466 -162
  4. package/dist/graph/compile.d.ts.map +1 -1
  5. package/dist/graph/compile.js +4 -1
  6. package/dist/graph/registry.d.ts +3 -0
  7. package/dist/graph/registry.d.ts.map +1 -1
  8. package/dist/graph/registry.js +133 -0
  9. package/dist/graph/schema.d.ts +12 -0
  10. package/dist/graph/schema.d.ts.map +1 -1
  11. package/dist/graph/slots.d.ts.map +1 -1
  12. package/dist/graph/slots.js +11 -20
  13. package/dist/index.d.ts +9 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/shaders/lights.d.ts +3 -2
  17. package/dist/shaders/lights.d.ts.map +1 -1
  18. package/dist/shaders/lights.js +16 -3
  19. package/dist/shaders/materials/common.d.ts.map +1 -1
  20. package/dist/shaders/materials/common.js +63 -56
  21. package/dist/shaders/materials/nodes.d.ts +1 -1
  22. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  23. package/dist/shaders/materials/nodes.js +210 -0
  24. package/dist/shaders/passes/composite.d.ts.map +1 -1
  25. package/dist/shaders/passes/composite.js +13 -1
  26. package/dist/shaders/passes/cull.d.ts +1 -1
  27. package/dist/shaders/passes/cull.d.ts.map +1 -1
  28. package/dist/shaders/passes/cull.js +7 -3
  29. package/dist/shaders/passes/ground.d.ts.map +1 -1
  30. package/dist/shaders/passes/ground.js +32 -55
  31. package/dist/shaders/passes/particles.d.ts +11 -1
  32. package/dist/shaders/passes/particles.d.ts.map +1 -1
  33. package/dist/shaders/passes/particles.js +9 -4
  34. package/dist/shaders/scene-light-api.d.ts +18 -10
  35. package/dist/shaders/scene-light-api.d.ts.map +1 -1
  36. package/dist/shaders/scene-light-api.js +58 -46
  37. package/dist/shadow-cascades.d.ts +47 -20
  38. package/dist/shadow-cascades.d.ts.map +1 -1
  39. package/dist/shadow-cascades.js +36 -13
  40. package/dist/unity/character.d.ts +59 -0
  41. package/dist/unity/character.d.ts.map +1 -0
  42. package/dist/unity/character.js +120 -0
  43. package/dist/unity/globals.d.ts +93 -0
  44. package/dist/unity/globals.d.ts.map +1 -0
  45. package/dist/unity/globals.js +340 -0
  46. package/dist/unity/host.d.ts +137 -0
  47. package/dist/unity/host.d.ts.map +1 -0
  48. package/dist/unity/host.js +489 -0
  49. package/dist/unity/looks.d.ts +88 -0
  50. package/dist/unity/looks.d.ts.map +1 -0
  51. package/dist/unity/looks.js +295 -0
  52. package/dist/unity/skin.d.ts +9 -0
  53. package/dist/unity/skin.d.ts.map +1 -0
  54. package/dist/unity/skin.js +117 -0
  55. package/dist/unity/stage.d.ts +148 -0
  56. package/dist/unity/stage.d.ts.map +1 -0
  57. package/dist/unity/stage.js +375 -0
  58. package/dist/unity/state.d.ts +22 -0
  59. package/dist/unity/state.d.ts.map +1 -0
  60. package/dist/unity/state.js +170 -0
  61. package/package.json +1 -1
  62. package/src/engine.ts +504 -176
  63. package/src/graph/compile.ts +3 -1
  64. package/src/graph/registry.ts +142 -0
  65. package/src/graph/schema.ts +9 -0
  66. package/src/graph/slots.ts +11 -20
  67. package/src/index.ts +11 -0
  68. package/src/shaders/lights.ts +16 -3
  69. package/src/shaders/materials/common.ts +63 -57
  70. package/src/shaders/materials/nodes.ts +210 -0
  71. package/src/shaders/passes/composite.ts +13 -1
  72. package/src/shaders/passes/cull.ts +7 -3
  73. package/src/shaders/passes/ground.ts +32 -55
  74. package/src/shaders/passes/particles.ts +19 -5
  75. package/src/shaders/scene-light-api.ts +59 -46
  76. package/src/shadow-cascades.ts +61 -32
  77. package/src/unity/character.ts +169 -0
  78. package/src/unity/globals.ts +443 -0
  79. package/src/unity/host.ts +587 -0
  80. package/src/unity/looks.ts +372 -0
  81. package/src/unity/skin.ts +128 -0
  82. package/src/unity/stage.ts +568 -0
  83. 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 { SHADOW_CASCADES, buildShadowCascades } from "./shadow-cascades";
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,
@@ -4096,6 +4129,7 @@ export class Engine {
4096
4129
  paramsDecl: params.wgsl(EFFECT_PARAMS_BINDING),
4097
4130
  gridSize: grid?.size ?? 0,
4098
4131
  cover: particleEntryPoints(wgsl).cover,
4132
+ orient: particleEntryPoints(wgsl).orient,
4099
4133
  live: particleEntryPoints(wgsl).count,
4100
4134
  textures: d.textures,
4101
4135
  };
@@ -4216,8 +4250,7 @@ export class Engine {
4216
4250
  { binding: PARTICLE_LIGHT_BINDING, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
4217
4251
  { binding: PARTICLE_LIGHT_BINDING + 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
4218
4252
  { binding: PARTICLE_LIGHT_BINDING + 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
4219
- { binding: PARTICLE_LIGHT_BINDING + 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
4220
- { binding: PARTICLE_LIGHT_BINDING + 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
4253
+ { binding: PARTICLE_LIGHT_BINDING + 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
4221
4254
  ]
4222
4255
  : []),
4223
4256
  // The live count's arguments, for the COUNT kernel's layout alone. The
@@ -4264,9 +4297,8 @@ export class Engine {
4264
4297
  ? [
4265
4298
  { binding: PARTICLE_LIGHT_BINDING, resource: { buffer: this.lightUniformBuffer } },
4266
4299
  { binding: PARTICLE_LIGHT_BINDING + 1, resource: { buffer: this.shadowLightVPBuffer } },
4267
- { binding: PARTICLE_LIGHT_BINDING + 2, resource: this.shadowMapDepthViews[0] },
4268
- { binding: PARTICLE_LIGHT_BINDING + 3, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
4269
- { 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 },
4270
4302
  ]
4271
4303
  : []),
4272
4304
  ...(withIndirect && indirect ? [{ binding: PARTICLE_INDIRECT_BINDING, resource: { buffer: indirect } }] : []),
@@ -6213,11 +6245,6 @@ export class Engine {
6213
6245
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
6214
6246
  });
6215
6247
  this.device.queue.writeTexture({ texture: this.fallbackMaterialTexture }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1]);
6216
- this.noFillBuffer = this.device.createBuffer({
6217
- label: "model light (none)",
6218
- size: 48,
6219
- usage: GPUBufferUsage.UNIFORM,
6220
- });
6221
6248
  // Generic shared-toon ramp: lit white down to a soft cool shadow tone with
6222
6249
  // a tight terminator around the midpoint, approximating MMD's toon ramps.
6223
6250
  const TOON_H = 64;
@@ -6309,10 +6336,10 @@ export class Engine {
6309
6336
  // The positional lights. Always bound, empty or not, so every material
6310
6337
  // pipeline shares one layout whether or not the scene has any.
6311
6338
  { binding: 6, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
6312
- // The far cascade's shadow map, then the world's sky (8) and the BRDF
6313
- // LUT (9). The sky is always bound — the 1x1 fallback when the scene
6314
- // has none — so every material pipeline keeps sharing one layout.
6315
- { binding: 7, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
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.)
6316
6343
  { binding: 8, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
6317
6344
  { binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
6318
6345
  // The cast's shadow on a stage — setStageCastShadow.
@@ -6521,15 +6548,15 @@ export class Engine {
6521
6548
  magFilter: "linear",
6522
6549
  minFilter: "linear",
6523
6550
  });
6524
- // One map per cascade, each at its own resolution — the near one crisp,
6525
- // the far one wide. Same format so one pipeline records into both.
6526
- this.shadowMapTextures = SHADOW_CASCADES.map((c, i) => this.device.createTexture({
6527
- label: `shadow map cascade ${i}`,
6528
- size: [c.mapSize, c.mapSize],
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],
6529
6556
  format: Engine.SHADOW_DEPTH_FORMAT,
6530
6557
  usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
6531
- }));
6532
- this.shadowMapDepthViews = this.shadowMapTextures.map((t) => t.createView());
6558
+ });
6559
+ this.shadowAtlasView = this.shadowAtlasTexture.createView();
6533
6560
  // The cast's shadow on a stage (setStageCastShadow): one map, only the cast
6534
6561
  // in it, from the stage's own direction. Always allocated so every material
6535
6562
  // bind group and every instance's shadow bind group stays one shape; drawn
@@ -6624,7 +6651,6 @@ export class Engine {
6624
6651
  // Same lights the materials read. A lamp that lit the cast and not the
6625
6652
  // floor under her would read as a sticker.
6626
6653
  { binding: 6, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
6627
- { binding: 7, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
6628
6654
  // The floor mirror: the mirror camera's view-projection, the reflection
6629
6655
  // resolve, an ordinary sampler beside the comparison one, and the
6630
6656
  // mirror pass's own depth for the depth-proportional blur.
@@ -8670,7 +8696,7 @@ export class Engine {
8670
8696
  // picture later.
8671
8697
  if (!this.device ||
8672
8698
  !this.mainPerFrameBindGroupLayout ||
8673
- !this.shadowMapDepthViews?.length ||
8699
+ !this.shadowAtlasView ||
8674
8700
  !this.cameraUniformBuffer ||
8675
8701
  !this.mirrorCameraBuffer ||
8676
8702
  !this.lightUniformBuffer ||
@@ -8695,11 +8721,10 @@ export class Engine {
8695
8721
  { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
8696
8722
  { binding: 1, resource: { buffer: this.lightUniformBuffer } },
8697
8723
  { binding: 2, resource: this.materialSampler },
8698
- { binding: 3, resource: this.shadowMapDepthViews[0] },
8724
+ { binding: 3, resource: this.shadowAtlasView },
8699
8725
  { binding: 4, resource: this.shadowComparisonSampler },
8700
8726
  { binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
8701
8727
  { binding: 6, resource: { buffer: this.lightsBuffer } },
8702
- { binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
8703
8728
  { binding: 8, resource: env },
8704
8729
  { binding: 9, resource: this.brdfLutView },
8705
8730
  { binding: 10, resource: this.castShadowView },
@@ -8712,11 +8737,10 @@ export class Engine {
8712
8737
  { binding: 0, resource: { buffer: this.mirrorCameraBuffer } },
8713
8738
  { binding: 1, resource: { buffer: this.lightUniformBuffer } },
8714
8739
  { binding: 2, resource: this.materialSampler },
8715
- { binding: 3, resource: this.shadowMapDepthViews[0] },
8740
+ { binding: 3, resource: this.shadowAtlasView },
8716
8741
  { binding: 4, resource: this.shadowComparisonSampler },
8717
8742
  { binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
8718
8743
  { binding: 6, resource: { buffer: this.lightsBuffer } },
8719
- { binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
8720
8744
  { binding: 8, resource: env },
8721
8745
  { binding: 9, resource: this.brdfLutView },
8722
8746
  { binding: 10, resource: this.castShadowView },
@@ -8782,6 +8806,7 @@ export class Engine {
8782
8806
  this.lightData[base + 5] = this.sun.color.y;
8783
8807
  this.lightData[base + 6] = this.sun.color.z;
8784
8808
  this.lightData[base + 7] = this.sun.strength;
8809
+ this.lightDataWords[DIR_LAYERS_AT + index] = this.sunLayers;
8785
8810
  if (index >= this.lightCount)
8786
8811
  this.lightCount = index + 1;
8787
8812
  this.updateLightBuffer();
@@ -8922,6 +8947,8 @@ export class Engine {
8922
8947
  this.sun.strength = options.strength;
8923
8948
  if (options.shadow !== undefined)
8924
8949
  this.sunShadow = Math.min(Math.max(options.shadow, 0), 1);
8950
+ if (options.layers !== undefined)
8951
+ this.sunLayers = options.layers >>> 0;
8925
8952
  if (options.direction) {
8926
8953
  this.sun.direction = options.direction;
8927
8954
  this.shadowLightVPDirty = true;
@@ -9013,7 +9040,20 @@ export class Engine {
9013
9040
  * params make: a scene document's JSON passes straight in, and so does a
9014
9041
  * literal typed into a console. Vec3 satisfies it either way. */
9015
9042
  lights) {
9016
- const list = (lights ?? []).slice(0, MAX_LIGHTS);
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);
9017
9057
  this.docLightCount = list.length;
9018
9058
  // RECORDS only — the header belongs to allocateLightSlots, the one writer.
9019
9059
  // This used to zero-and-upload the header region too, which left two CPU
@@ -9042,7 +9082,7 @@ export class Engine {
9042
9082
  // vector cannot be normalised, and a cone around nothing would light
9043
9083
  // nothing at all — a light that vanishes because its aim was left at the
9044
9084
  // default is worse than one that shines everywhere.
9045
- 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;
9046
9086
  this.lightsData[b + 7] = len > 0 ? 1 : 0;
9047
9087
  this.lightsData[b + 8] = len > 0 ? l.aim.x / len : 0;
9048
9088
  this.lightsData[b + 9] = len > 0 ? l.aim.y / len : 0;
@@ -9054,7 +9094,8 @@ export class Engine {
9054
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;
9055
9095
  this.lightsData[b + 11] = outer;
9056
9096
  this.lightsData[b + 12] = Math.max(inner, outer);
9057
- this.lightsData[b + 13] = 0;
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;
9058
9099
  this.lightsData[b + 14] = 0;
9059
9100
  this.lightsData[b + 15] = 0;
9060
9101
  }
@@ -9443,6 +9484,8 @@ export class Engine {
9443
9484
  * ground plane must not draw, and the far shadow cascade has nothing to
9444
9485
  * cover without one (see the cascade loop). */
9445
9486
  hasStage() {
9487
+ if (this.nativeStage)
9488
+ return true;
9446
9489
  for (const inst of this.modelInstances.values())
9447
9490
  if (inst.isStage)
9448
9491
  return true;
@@ -9492,94 +9535,34 @@ export class Engine {
9492
9535
  this.modelInstances.set(newName, inst);
9493
9536
  }
9494
9537
  /**
9495
- * Light one model with more than the world gives it: a FILL, linear RGB times
9496
- * its surface colour, added after its material graph. Null takes it away again.
9497
- *
9498
- * After the graph, not in its ambient: an NPR ramp read an ambient fill as
9499
- * light, so sliding it moved her shadows and flipped whole regions across a
9500
- * hard step. Added after, it brightens her evenly and every shadow stays put.
9501
- *
9502
- * A game lights its stage and its characters apart. Aether Gazer's rooms are
9503
- * lit by their lamps and a near-black ambient, while its characters take a
9504
- * flat base light of their own — so a stage whose World is right for the room
9505
- * leaves a face turned from the lamps in the dark. This is that second light,
9506
- * for the models the host counts as cast.
9507
- */
9508
- setModelFill(name, fill) {
9509
- return this.writeModelLight(name, 0, fill);
9510
- }
9511
- /**
9512
- * A model's own sun: the colour and strength the scene's sun has FOR THIS
9513
- * 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.
9514
9541
  *
9515
- * The other half of lighting a stage and its cast apart. A game's stage is
9516
- * lit by its own daylight — Aether Gazer's kitchen at twenty times white —
9517
- * while its characters take a key light of their own that the stage never
9518
- * states. So a stage carries the sun it was lit by, and the scene's sun
9519
- * stays the cast's. Direction and shadow are still the scene's: one sun
9520
- * 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.
9521
9545
  */
9522
- setModelSun(name, sun) {
9523
- return this.writeModelLight(name, 4, sun);
9524
- }
9525
- /**
9526
- * Light one model by the sky's AVERAGE rather than its shape: the world's
9527
- * colour and brightness from every direction alike. False gives it the
9528
- * directional sky back.
9529
- *
9530
- * For a cast under an anime look. An HDRI's sky is brighter and warmer one
9531
- * way than another, and taken at each normal it shades a face with soft
9532
- * realistic gradients that no toon ramp drew — PBR on an anime face. Aether
9533
- * Gazer gives its characters one flat base light instead. World strength and
9534
- * colour still reach her; only the shape is gone. A flat World is its own
9535
- * average, so without an HDRI this changes nothing.
9536
- */
9537
- setModelFlatSky(name, flat) {
9538
- return this.writeModelLight(name, 8, flat ? new Vec3(1, 0, 0) : null);
9539
- }
9540
- /** One part of a model's light, at `at` (0 the fill, 4 the sun, 8 its
9541
- * options) in its ModelLight; w says whether that part is set. All unset
9542
- * releases the buffer and the model takes the shared zero stand-in again. */
9543
- writeModelLight(name, at, value) {
9546
+ setModelAmbient(name, sh) {
9544
9547
  const inst = this.modelInstances.get(name);
9545
9548
  if (!inst || !this.device)
9546
9549
  return false;
9547
- const light = inst.modelLight ?? new Float32Array(12);
9548
- light.set(value ? [value.x, value.y, value.z, 1] : [0, 0, 0, 0], at);
9549
- const any = light[3] > 0 || light[7] > 0 || light[11] > 0;
9550
- if (!any) {
9551
- if (!inst.lightBuffer)
9552
- return true;
9553
- const retired = inst.lightBuffer;
9554
- inst.lightBuffer = null;
9555
- inst.modelLight = null;
9556
- inst.mainPerInstanceBindGroup = this.perInstanceBindGroup(name, inst.skinMatrixBuffer, null);
9557
- this.bundlesDirty = true;
9558
- // Retired once the GPU is done with it: an encoded frame may still name it.
9559
- void this.device.queue.onSubmittedWorkDone().then(() => retired.destroy());
9560
- return true;
9561
- }
9562
- inst.modelLight = light;
9563
- if (!inst.lightBuffer) {
9564
- inst.lightBuffer = this.device.createBuffer({
9565
- label: `${name}: light`,
9566
- size: 48,
9567
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
9568
- });
9569
- inst.mainPerInstanceBindGroup = this.perInstanceBindGroup(name, inst.skinMatrixBuffer, inst.lightBuffer);
9570
- 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);
9571
9554
  }
9572
- this.device.queue.writeBuffer(inst.lightBuffer, 0, light.buffer);
9555
+ this.device.queue.writeBuffer(inst.lightBuffer, 0, o);
9573
9556
  return true;
9574
9557
  }
9575
- /** The per-model group: its skinning matrices and its light, or the zero stand-in. */
9558
+ /** The per-model group: its skinning matrices and its ObjectLight. */
9576
9559
  perInstanceBindGroup(name, skinMatrixBuffer, light) {
9577
9560
  return this.device.createBindGroup({
9578
9561
  label: `${name}: main per-instance bind group`,
9579
9562
  layout: this.mainPerInstanceBindGroupLayout,
9580
9563
  entries: [
9581
9564
  { binding: 0, resource: { buffer: skinMatrixBuffer } },
9582
- { binding: 1, resource: { buffer: light ?? this.noFillBuffer } },
9565
+ { binding: 1, resource: { buffer: light } },
9583
9566
  ],
9584
9567
  });
9585
9568
  }
@@ -11161,7 +11144,9 @@ export class Engine {
11161
11144
  let changed = force;
11162
11145
  for (let i = 0; i < this.cullDraws.length; i++) {
11163
11146
  const { inst, draw } = this.cullDraws[i];
11164
- const v = inst.hiddenMaterials.has(draw.materialName) || inst.morphHiddenMaterials.has(draw.materialName) ? 1 : 0;
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);
11165
11150
  if (out[i] !== v) {
11166
11151
  out[i] = v;
11167
11152
  changed = true;
@@ -11306,6 +11291,15 @@ export class Engine {
11306
11291
  maxZ = Math.max(maxZ, data[o + 18] + r);
11307
11292
  }
11308
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
+ }
11309
11303
  if (!Number.isFinite(minX)) {
11310
11304
  this.shadowSceneBounds = null;
11311
11305
  return;
@@ -12099,7 +12093,19 @@ export class Engine {
12099
12093
  { binding: 2, resource: this.materialSampler },
12100
12094
  ],
12101
12095
  }));
12102
- const mainPerInstanceBindGroup = this.perInstanceBindGroup(name, skinMatrixBuffer, null);
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);
12103
12109
  const pickPerInstanceBindGroup = this.device.createBindGroup({
12104
12110
  label: `${name}: pick per-instance bind group`,
12105
12111
  layout: this.pickPerInstanceBindGroupLayout,
@@ -12132,8 +12138,8 @@ export class Engine {
12132
12138
  shadowDrawCalls: [],
12133
12139
  shadowBindGroups,
12134
12140
  mainPerInstanceBindGroup,
12135
- lightBuffer: null,
12136
- modelLight: null,
12141
+ lightBuffer,
12142
+ objectLight,
12137
12143
  pickPerInstanceBindGroup,
12138
12144
  pickDrawCalls: [],
12139
12145
  isStage,
@@ -12347,7 +12353,6 @@ export class Engine {
12347
12353
  gb[3] = fadeStart;
12348
12354
  gb[4] = fadeEnd;
12349
12355
  gb[5] = shadowStrength;
12350
- gb[6] = 1 / SHADOW_CASCADES[0].mapSize;
12351
12356
  gb[7] = gridSpacing;
12352
12357
  gb[8] = gridLineWidth;
12353
12358
  gb[9] = gridLineOpacity;
@@ -12369,19 +12374,6 @@ export class Engine {
12369
12374
  // Which variant the draw picks. Zero is the sharp shader, which is the one
12370
12375
  // that existed before softness did.
12371
12376
  this.groundSoft = gb[18] > 0;
12372
- // gb[17] — does the FAR cascade hold anything?
12373
- //
12374
- // It holds something only when a stage is loaded; that is what it exists for
12375
- // and the cascade loop already skips drawing into it otherwise, leaving it
12376
- // cleared. A cleared depth map compares as "no occluder", so the ground's far
12377
- // branch is nine comparison taps whose answer is known in advance.
12378
- //
12379
- // That branch runs wherever the NEAR cascade does not reach, and the near one
12380
- // is a 64-unit box around the camera target — so on a floor receding to the
12381
- // horizon it is most of the visible pixels, on the most expensive
12382
- // full-coverage draw in the frame. Skipping it is free in the exact sense:
12383
- // the shader takes vis = 1.0, which is what the taps would have returned.
12384
- gb[17] = this.hasStage() ? 1 : 0;
12385
12377
  // gb[20..23] — the caster sphere, refreshed every frame by
12386
12378
  // writeGroundCasterSphere. Zero here so a frame that renders before the
12387
12379
  // first cull (there is one) reads "nothing casts" and skips the taps, which
@@ -12446,12 +12438,11 @@ export class Engine {
12446
12438
  entries: [
12447
12439
  { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
12448
12440
  { binding: 1, resource: { buffer: this.lightUniformBuffer } },
12449
- { binding: 2, resource: this.shadowMapDepthViews[0] },
12441
+ { binding: 2, resource: this.shadowAtlasView },
12450
12442
  { binding: 3, resource: this.shadowComparisonSampler },
12451
12443
  { binding: 4, resource: { buffer: this.groundShadowMaterialBuffer } },
12452
12444
  { binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
12453
12445
  { binding: 6, resource: { buffer: this.lightsBuffer } },
12454
- { binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
12455
12446
  { binding: 8, resource: { buffer: this.mirrorVPBuffer } },
12456
12447
  // Created in handleResize, which runs during init — before any ground
12457
12448
  // can exist to bind it.
@@ -12469,12 +12460,11 @@ export class Engine {
12469
12460
  entries: [
12470
12461
  { binding: 0, resource: { buffer: this.mirrorCameraBuffer } },
12471
12462
  { binding: 1, resource: { buffer: this.lightUniformBuffer } },
12472
- { binding: 2, resource: this.shadowMapDepthViews[0] },
12463
+ { binding: 2, resource: this.shadowAtlasView },
12473
12464
  { binding: 3, resource: this.shadowComparisonSampler },
12474
12465
  { binding: 4, resource: { buffer: this.groundShadowMaterialBuffer } },
12475
12466
  { binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
12476
12467
  { binding: 6, resource: { buffer: this.lightsBuffer } },
12477
- { binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
12478
12468
  { binding: 8, resource: { buffer: this.mirrorVPBuffer } },
12479
12469
  // Stand-ins, not the live mirror textures: this group draws INTO the
12480
12470
  // mirror pass, where those are the attachments.
@@ -14088,54 +14078,54 @@ export class Engine {
14088
14078
  // the final frame's depth — and the ground, which draws on `hasGround` alone,
14089
14079
  // keeps PCF-sampling a character that is no longer in the scene. One clearing
14090
14080
  // pass on the transition to empty, then it stops.
14091
- if (hasModels || this.shadowMapPopulated) {
14092
- // The far cascade is the STAGE cascade, and it costs a full pass over the
14093
- // whole cast every frame to say so. Its own spec explains what it is for —
14094
- // "a set piece 100 units out still throws" — and a scene with no stage has
14095
- // no set piece: every caster sits inside the near cascade's 64-unit box,
14096
- // which follows the camera target, and the far map's only readers are
14097
- // ground pixels beyond that box, where nothing is casting.
14098
- //
14099
- // So when no stage is loaded it is drawn ONCE, cleared, and then skipped —
14100
- // the same shape as shadowMapPopulated above, and for the same reason. A
14101
- // cleared depth map reads as "no occluder", which is the correct answer
14102
- // here rather than a missing one. Load a stage and it comes straight back.
14103
- //
14104
- // 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.
14105
14093
  const stage = this.hasStage();
14106
14094
  for (let ci = 0; ci < SHADOW_CASCADES.length; ci++) {
14107
- const wanted = ci === 0 || stage;
14108
- // Already cleared and still unwanted — nothing to do, and the map still
14109
- // holds the far plane from the pass that cleared it.
14110
- if (!wanted && this.shadowCascadeCleared[ci])
14095
+ if (ci > 0 && !stage)
14111
14096
  continue;
14097
+ const c = SHADOW_CASCADES[ci];
14112
14098
  const sp = encoder.beginRenderPass({
14113
- // One timestamp pair exists for "shadow"; the near cascade wears it.
14099
+ // One timestamp pair exists for "shadow"; the inner cascade wears it.
14114
14100
  timestampWrites: ci === 0 ? this.stamps("shadow") : undefined,
14115
14101
  colorAttachments: [],
14116
14102
  depthStencilAttachment: {
14117
- view: this.shadowMapDepthViews[ci],
14103
+ view: this.shadowAtlasView,
14118
14104
  depthClearValue: 1.0,
14119
- depthLoadOp: "clear",
14105
+ depthLoadOp: ci === 0 ? "clear" : "load",
14120
14106
  depthStoreOp: "store",
14121
14107
  },
14122
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);
14123
14112
  // The per-model `visible` test that used to guard this is gone: it is a
14124
14113
  // per-frame boolean, and baking it into a bundle would make toggling a
14125
14114
  // model re-record. It lives in the cull compute now, which zeroes the
14126
14115
  // instance count of an invisible model's draws.
14127
- if (wanted && this.shadowBundles[ci])
14116
+ if (this.shadowBundles[ci])
14128
14117
  sp.executeBundles([this.shadowBundles[ci]]);
14129
14118
  // The mirror throws shade like anything else standing on the floor.
14130
14119
  // Direct rather than in the bundle: the bundles are recorded on scene
14131
14120
  // STRUCTURE, and a mirror comes and goes with an effect's weight.
14132
- if (wanted && this.mirrorSurface && this.mirrorShadowPipeline) {
14121
+ if (this.mirrorSurface && this.mirrorShadowPipeline) {
14133
14122
  sp.setPipeline(this.mirrorShadowPipeline);
14134
14123
  sp.setBindGroup(0, this.mirrorShadowBindGroups[ci]);
14135
14124
  sp.draw(6);
14136
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);
14137
14128
  sp.end();
14138
- this.shadowCascadeCleared[ci] = !wanted;
14139
14129
  }
14140
14130
  // The cast's shadow on a stage: drawn while a stage asks for it and has a
14141
14131
  // cast to shade it with; cleared once when it stops, then left alone.
@@ -14150,7 +14140,7 @@ export class Engine {
14150
14140
  cp.end();
14151
14141
  this.castShadowCleared = !castOn;
14152
14142
  }
14153
- this.shadowMapPopulated = hasModels;
14143
+ this.shadowMapPopulated = hasModels || !!this.nativeStage;
14154
14144
  }
14155
14145
  // Before the particles and before the field pass: both may read the grid,
14156
14146
  // and a grid stepped after them is one frame stale in everything that used it.
@@ -14195,6 +14185,17 @@ export class Engine {
14195
14185
  // is FALSE for the cleared buffer — every such fragment silently rejected.
14196
14186
  // One redundant word against a whole class of invisible failure.
14197
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
+ }
14198
14199
  if (this.hasGround)
14199
14200
  this.renderGround(pass);
14200
14201
  // After the ground for the same early-z reason, and before the transparent
@@ -14227,6 +14228,14 @@ export class Engine {
14227
14228
  // which was never this renderer's bottleneck.
14228
14229
  const camView = this.sceneView("camera");
14229
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
+ }
14230
14239
  // Last in the pass: depth-tested against everything drawn above, so a
14231
14240
  // particle behind the character is simply hidden, and still inside the HDR
14232
14241
  // target so an `#bloom` effect reaches the pyramid below.
@@ -14749,9 +14758,35 @@ export class Engine {
14749
14758
  claimed.set(m, install.group.id);
14750
14759
  return claimed;
14751
14760
  }
14752
- // Compile a group's graph → WGSL → pipeline(s), install keyed by group id. Reuses the
14753
- // install (pipeline + uniform buffer) when the graph/integration is byte-unchanged.
14754
- async compileAndInstallGroup(inst, group, opts) {
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);
14755
14790
  const renderClass = group.renderClass ?? "auto";
14756
14791
  const alphaMode = group.alphaMode ?? "opaque";
14757
14792
  // THE MAPS ARE PART OF THE SIGNATURE, because a matching one skips the
@@ -15850,6 +15885,243 @@ export class Engine {
15850
15885
  // The count rides in the anchor's spare lane, so rzTrailCount is one read.
15851
15886
  cd[anchorBase + 11] = count;
15852
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
+ }
15853
16125
  updateSkinMatrices() {
15854
16126
  this.forEachInstance((inst) => {
15855
16127
  // Only a pose pass can change these, and an idle stage did not run one —
@@ -16174,3 +16446,35 @@ Engine.CULL_MODEL_RIGID = 2;
16174
16446
  /** A cache key no material can collide with — a PMX name is never empty and
16175
16447
  * never contains a NUL. */
16176
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
+ }