reze-engine 0.42.3 → 0.50.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 (140) hide show
  1. package/README.md +40 -410
  2. package/dist/camera.d.ts +3 -0
  3. package/dist/camera.d.ts.map +1 -1
  4. package/dist/camera.js +33 -8
  5. package/dist/engine.d.ts +924 -29
  6. package/dist/engine.d.ts.map +1 -1
  7. package/dist/engine.js +4043 -278
  8. package/dist/graph/registry.d.ts +2 -2
  9. package/dist/graph/registry.d.ts.map +1 -1
  10. package/dist/graph/registry.js +1 -1
  11. package/dist/graph/slots.d.ts +0 -1
  12. package/dist/graph/slots.d.ts.map +1 -1
  13. package/dist/graph/slots.js +37 -9
  14. package/dist/hdr.d.ts +18 -0
  15. package/dist/hdr.d.ts.map +1 -0
  16. package/dist/hdr.js +162 -0
  17. package/dist/ibl.d.ts +19 -0
  18. package/dist/ibl.d.ts.map +1 -0
  19. package/dist/ibl.js +113 -0
  20. package/dist/ik-solver.d.ts +2 -1
  21. package/dist/ik-solver.d.ts.map +1 -1
  22. package/dist/index.d.ts +5 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +10 -0
  25. package/dist/math.d.ts +20 -1
  26. package/dist/math.d.ts.map +1 -1
  27. package/dist/math.js +23 -16
  28. package/dist/midi-loader.d.ts +10 -0
  29. package/dist/midi-loader.d.ts.map +1 -0
  30. package/dist/midi-loader.js +247 -0
  31. package/dist/model.d.ts +2 -13
  32. package/dist/model.d.ts.map +1 -1
  33. package/dist/model.js +29 -0
  34. package/dist/param-track.d.ts +48 -0
  35. package/dist/param-track.d.ts.map +1 -0
  36. package/dist/param-track.js +80 -0
  37. package/dist/physics/types.d.ts.map +1 -1
  38. package/dist/physics/types.js +3 -0
  39. package/dist/reflection.d.ts +27 -0
  40. package/dist/reflection.d.ts.map +1 -0
  41. package/dist/reflection.js +93 -0
  42. package/dist/shaders/anchor-table.d.ts +56 -0
  43. package/dist/shaders/anchor-table.d.ts.map +1 -0
  44. package/dist/shaders/anchor-table.js +128 -0
  45. package/dist/shaders/audio-api.d.ts +3 -0
  46. package/dist/shaders/audio-api.d.ts.map +1 -0
  47. package/dist/shaders/audio-api.js +81 -0
  48. package/dist/shaders/cast-api.d.ts +2 -0
  49. package/dist/shaders/cast-api.d.ts.map +1 -0
  50. package/dist/shaders/cast-api.js +121 -0
  51. package/dist/shaders/cast-layout.d.ts +21 -0
  52. package/dist/shaders/cast-layout.d.ts.map +1 -0
  53. package/dist/shaders/cast-layout.js +20 -0
  54. package/dist/shaders/lights.d.ts +79 -0
  55. package/dist/shaders/lights.d.ts.map +1 -0
  56. package/dist/shaders/lights.js +269 -0
  57. package/dist/shaders/lyrics-api.d.ts +39 -0
  58. package/dist/shaders/lyrics-api.d.ts.map +1 -0
  59. package/dist/shaders/lyrics-api.js +187 -0
  60. package/dist/shaders/materials/common.d.ts +2 -4
  61. package/dist/shaders/materials/common.d.ts.map +1 -1
  62. package/dist/shaders/materials/common.js +87 -35
  63. package/dist/shaders/midi-api.d.ts +10 -0
  64. package/dist/shaders/midi-api.d.ts.map +1 -0
  65. package/dist/shaders/midi-api.js +114 -0
  66. package/dist/shaders/passes/composite.d.ts +31 -15
  67. package/dist/shaders/passes/composite.d.ts.map +1 -1
  68. package/dist/shaders/passes/composite.js +225 -135
  69. package/dist/shaders/passes/cull.d.ts +2 -0
  70. package/dist/shaders/passes/cull.d.ts.map +1 -0
  71. package/dist/shaders/passes/cull.js +138 -0
  72. package/dist/shaders/passes/field-blit.d.ts +26 -0
  73. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  74. package/dist/shaders/passes/field-blit.js +65 -0
  75. package/dist/shaders/passes/grid.d.ts +31 -0
  76. package/dist/shaders/passes/grid.d.ts.map +1 -0
  77. package/dist/shaders/passes/grid.js +169 -0
  78. package/dist/shaders/passes/ground.d.ts +13 -1
  79. package/dist/shaders/passes/ground.d.ts.map +1 -1
  80. package/dist/shaders/passes/ground.js +170 -25
  81. package/dist/shaders/passes/hosted-api.d.ts +57 -0
  82. package/dist/shaders/passes/hosted-api.d.ts.map +1 -0
  83. package/dist/shaders/passes/hosted-api.js +166 -0
  84. package/dist/shaders/passes/id-debug.d.ts +28 -0
  85. package/dist/shaders/passes/id-debug.d.ts.map +1 -0
  86. package/dist/shaders/passes/id-debug.js +74 -0
  87. package/dist/shaders/passes/particles.d.ts +66 -0
  88. package/dist/shaders/passes/particles.d.ts.map +1 -0
  89. package/dist/shaders/passes/particles.js +279 -0
  90. package/dist/shaders/passes/scene-contract.d.ts +128 -0
  91. package/dist/shaders/passes/scene-contract.d.ts.map +1 -0
  92. package/dist/shaders/passes/scene-contract.js +207 -0
  93. package/dist/shaders/passes/sim.d.ts +34 -0
  94. package/dist/shaders/passes/sim.d.ts.map +1 -0
  95. package/dist/shaders/passes/sim.js +169 -0
  96. package/dist/shaders/passes/trails.d.ts +59 -0
  97. package/dist/shaders/passes/trails.d.ts.map +1 -0
  98. package/dist/shaders/passes/trails.js +340 -0
  99. package/dist/shaders/score-api.d.ts +10 -0
  100. package/dist/shaders/score-api.d.ts.map +1 -0
  101. package/dist/shaders/score-api.js +114 -0
  102. package/dist/shadow-cascades.d.ts +45 -0
  103. package/dist/shadow-cascades.d.ts.map +1 -0
  104. package/dist/shadow-cascades.js +70 -0
  105. package/dist/vmd-loader.d.ts +3 -2
  106. package/dist/vmd-loader.d.ts.map +1 -1
  107. package/package.json +1 -1
  108. package/src/camera.ts +31 -8
  109. package/src/engine.ts +4535 -296
  110. package/src/graph/registry.ts +2 -2
  111. package/src/graph/slots.ts +37 -9
  112. package/src/hdr.ts +156 -0
  113. package/src/ibl.ts +115 -0
  114. package/src/ik-solver.ts +1 -1
  115. package/src/index.ts +12 -0
  116. package/src/math.ts +23 -17
  117. package/src/midi-loader.ts +246 -0
  118. package/src/model.ts +31 -3
  119. package/src/param-track.ts +83 -0
  120. package/src/physics/types.ts +4 -1
  121. package/src/reflection.ts +94 -0
  122. package/src/shaders/anchor-table.ts +147 -0
  123. package/src/shaders/audio-api.ts +82 -0
  124. package/src/shaders/cast-api.ts +123 -0
  125. package/src/shaders/cast-layout.ts +20 -0
  126. package/src/shaders/lights.ts +280 -0
  127. package/src/shaders/lyrics-api.ts +202 -0
  128. package/src/shaders/materials/common.ts +89 -35
  129. package/src/shaders/midi-api.ts +116 -0
  130. package/src/shaders/passes/composite.ts +244 -136
  131. package/src/shaders/passes/cull.ts +139 -0
  132. package/src/shaders/passes/grid.ts +178 -0
  133. package/src/shaders/passes/ground.ts +172 -25
  134. package/src/shaders/passes/hosted-api.ts +171 -0
  135. package/src/shaders/passes/id-debug.ts +75 -0
  136. package/src/shaders/passes/particles.ts +340 -0
  137. package/src/shaders/passes/scene-contract.ts +266 -0
  138. package/src/shaders/passes/trails.ts +390 -0
  139. package/src/shadow-cascades.ts +97 -0
  140. package/src/vmd-loader.ts +2 -2
package/dist/engine.d.ts CHANGED
@@ -3,6 +3,9 @@ import { Model } from "./model";
3
3
  import { RezePhysics } from "./physics";
4
4
  import type { WindOptions } from "./physics/world";
5
5
  import { type AssetReader } from "./asset-reader";
6
+ import { type ParamKey } from "./param-track";
7
+ import { type HdrImage } from "./hdr";
8
+ import { type LyricLine, type LyricRect } from "./shaders/lyrics-api";
6
9
  import { type CompileOptions, type StyleSlot } from "./graph/compile";
7
10
  import type { AlphaMode, RenderClass } from "./graph/render-class";
8
11
  import type { ApplyStyleGroupResult, ApplyStyleGroupsResult, StyleGroup } from "./graph/style-group";
@@ -26,18 +29,35 @@ type GroupInstall = {
26
29
  * an unchanged group. */
27
30
  signature: string;
28
31
  };
29
- export type RaycastCallback = (modelName: string, material: string | null, bone: string | null, screenX: number, screenY: number) => void;
32
+ type RaycastCallback = (modelName: string, material: string | null, bone: string | null, screenX: number, screenY: number) => void;
30
33
  /** Select a folder (webkitdirectory) and pass FileList or File[]; pmxFile picks which .pmx when several exist. */
31
34
  export type LoadModelFromFilesOptions = {
32
35
  files: FileList | File[];
33
36
  pmxFile?: File;
34
37
  };
35
- export type WorldOptions = {
38
+ type WorldOptions = {
36
39
  /** Linear scene-referred color of the World Background (Blender: World > Surface > Color). */
37
40
  color?: Vec3;
38
41
  /** Multiplier on world color (Blender: World > Surface > Strength). */
39
42
  strength?: number;
40
43
  };
44
+ /**
45
+ * One note in a score: when it sounds, for how long, at what pitch, how hard.
46
+ *
47
+ * Seconds and MIDI pitch (60 = middle C) rather than ticks and a tempo map — a
48
+ * score is consumed against the scene clock, so anything the engine stores in
49
+ * musical time would have to be resolved to seconds before every read.
50
+ */
51
+ export interface MidiNote {
52
+ /** Seconds from the start of the piece. */
53
+ start: number;
54
+ /** Seconds the note sounds for. */
55
+ duration: number;
56
+ /** MIDI pitch, 0–127. */
57
+ pitch: number;
58
+ /** How hard it was struck, 0–1. Defaults to 1. */
59
+ velocity?: number;
60
+ }
41
61
  export type ModelTransform = {
42
62
  position: Vec3;
43
63
  rotation: Quat;
@@ -45,7 +65,7 @@ export type ModelTransform = {
45
65
  scale: number;
46
66
  visible: boolean;
47
67
  };
48
- export type SunOptions = {
68
+ type SunOptions = {
49
69
  /** Linear color of the sun lamp (Blender: Light > Color). */
50
70
  color?: Vec3;
51
71
  /** Lamp power in Blender units (Blender: Light > Strength). */
@@ -61,9 +81,19 @@ export type EffectParamValue = number | {
61
81
  y: number;
62
82
  z: number;
63
83
  };
84
+ /** A vector by shape rather than by class — Vec3 satisfies it, and so does a
85
+ * JSON object out of a scene document or a literal typed into a console. */
86
+ export type XYZ = {
87
+ x: number;
88
+ y: number;
89
+ z: number;
90
+ };
64
91
  export type EffectResult = {
65
92
  ok: boolean;
66
- /** Compile/validation errors, line:col relative to the USER's WGSL. */
93
+ /** Compile/validation errors, line:col relative to the USER's WGSL. Also
94
+ * carries non-fatal warnings on an effect that DID install — a directive
95
+ * that parsed but will never fire, an anchor the scene had no slot for. So
96
+ * a non-empty list is not a failure; `ok` is. */
67
97
  diagnostics: string[];
68
98
  /** Which mounts the WGSL declared — `fn background` / `fn foreground`. Both
69
99
  * false only on a failed compile, since defining neither IS the failure. */
@@ -72,7 +102,7 @@ export type EffectResult = {
72
102
  foreground: boolean;
73
103
  };
74
104
  };
75
- export type CameraOptions = {
105
+ type CameraOptions = {
76
106
  /** Orbit distance from target. */
77
107
  distance?: number;
78
108
  /** World-space orbit center. */
@@ -193,23 +223,6 @@ export type EngineOptions = {
193
223
  /** See {@link GizmoDragCallback}. */
194
224
  onGizmoDrag?: GizmoDragCallback;
195
225
  };
196
- export declare const DEFAULT_ENGINE_OPTIONS: {
197
- world: {
198
- color: Vec3;
199
- strength: number;
200
- };
201
- sun: {
202
- color: Vec3;
203
- strength: number;
204
- direction: Vec3;
205
- };
206
- camera: {
207
- distance: number;
208
- target: Vec3;
209
- fov: number;
210
- };
211
- onRaycast: undefined;
212
- };
213
226
  export interface EngineStats {
214
227
  fps: number;
215
228
  frameTime: number;
@@ -238,6 +251,75 @@ interface DrawCall {
238
251
  outline?: {
239
252
  bindGroup: GPUBindGroup;
240
253
  };
254
+ /** MODEL-SPACE AABB over this material's index range, computed at load:
255
+ * [minX, minY, minZ, maxX, maxY, maxZ]. Usable for culling only while the
256
+ * owning model is rigid (see ModelInstance.rigid) — animation moves vertices
257
+ * out of it, which is why a skinned model culls per model instead. */
258
+ bounds: Float32Array;
259
+ /** Slot in the cull metadata and indirect-argument buffers. Reassigned every
260
+ * time the flat draw list is rebuilt (model added/removed, draws re-sorted).
261
+ * -1 until the list is built. */
262
+ cullIndex: number;
263
+ }
264
+ /** What getCullDiagnostics() reports: the GPU's answer, an independent CPU
265
+ * answer over the same source data, and every draw where they disagree. */
266
+ export interface CullDiagnostics {
267
+ drawCount: number;
268
+ modelCount: number;
269
+ /** Draws the GPU compute left with instanceCount = 1. */
270
+ cameraVisibleGpu: number;
271
+ shadowVisibleGpu: number;
272
+ /** The same test run on the CPU from the same bounds and frusta. */
273
+ cameraVisibleCpu: number;
274
+ shadowVisibleCpu: number;
275
+ /** How each model was bounded this frame — the split that decides whether a
276
+ * stage culls per material or per model. */
277
+ rigidModels: number;
278
+ skinnedModels: number;
279
+ mismatches: {
280
+ model: string;
281
+ material: string;
282
+ pass: "camera" | "shadow";
283
+ gpu: boolean;
284
+ cpu: boolean;
285
+ }[];
286
+ /** What each model was actually tested against. Without this a report saying
287
+ * "everything visible" is unreadable — you cannot tell a working cull looking
288
+ * at geometry that genuinely fills the screen from a cull that never rejects
289
+ * anything. The radius against the camera distance answers it directly. */
290
+ models: {
291
+ name: string;
292
+ rigid: boolean;
293
+ visible: boolean;
294
+ draws: number;
295
+ /** How many of this model's draws survived the camera frustum. */
296
+ cameraVisible: number;
297
+ /** How many survived the light frustum AND carry the PMX cast-shadow flag.
298
+ * Split from `casters` so a zero here is readable: no casters at all means
299
+ * the author turned shadows off, while casters with zero survivors means
300
+ * the light-frustum test rejected them. */
301
+ shadowVisible: number;
302
+ /** Draws with the PMX cast-shadow flag set (bit 0x04), before any culling. */
303
+ casters: number;
304
+ /** Sphere path only: the world centre and radius tested against. Null for a
305
+ * rigid model, whose draws each carry their own box instead. */
306
+ sphere: [number, number, number, number] | null;
307
+ }[];
308
+ /** Where the camera was, so the numbers above can be read against it. */
309
+ camera: {
310
+ eye: [number, number, number];
311
+ target: [number, number, number];
312
+ };
313
+ /** Times the draw list has been rebuilt since the engine started. It should
314
+ * climb only when scene STRUCTURE changes — a model loaded, a style group
315
+ * applied — and then stop. A number that keeps rising while nothing but the
316
+ * animation is moving means something is dirtying the list every frame, which
317
+ * is the failure mode that makes render bundles cost more than they save. */
318
+ rebuilds: number;
319
+ /** Times the render bundles have been re-recorded. Held to the same standard
320
+ * as `rebuilds`, and for the sharper reason: a bundle exists to be replayed,
321
+ * so one re-recorded every frame is strictly worse than not having it. */
322
+ bundleRecords: number;
241
323
  }
242
324
  interface PickDrawCall {
243
325
  count: number;
@@ -246,6 +328,10 @@ interface PickDrawCall {
246
328
  }
247
329
  interface ModelInstance {
248
330
  name: string;
331
+ /** This model's id in the id attachment — 1-based, so 0 stays "nothing".
332
+ * The pick pass has always minted it; the cast carries it now too, so an
333
+ * effect can compare what it reads out of the id buffer against a subject. */
334
+ objectId: number;
249
335
  model: Model;
250
336
  basePath: string;
251
337
  assetReader: AssetReader;
@@ -258,7 +344,7 @@ interface ModelInstance {
258
344
  skinMatrixBuffer: GPUBuffer;
259
345
  drawCalls: DrawCall[];
260
346
  shadowDrawCalls: DrawCall[];
261
- shadowBindGroup: GPUBindGroup;
347
+ shadowBindGroups: GPUBindGroup[];
262
348
  mainPerInstanceBindGroup: GPUBindGroup;
263
349
  pickPerInstanceBindGroup: GPUBindGroup;
264
350
  pickDrawCalls: PickDrawCall[];
@@ -283,6 +369,24 @@ interface ModelInstance {
283
369
  styleGroups: Map<string, GroupInstall>;
284
370
  materialToGroup: Map<string, string>;
285
371
  styleGroupGen: Map<string, number>;
372
+ /** Slot in the per-model cull buffer, assigned when the draw list is rebuilt. */
373
+ cullModelIndex: number;
374
+ /** Every bone shares one skin matrix (within tolerance) and no vertex morph can
375
+ * move a vertex out of its material's box — so the model is a rigid transform
376
+ * of its bind pose and its per-material AABBs are live. True for a stage, and
377
+ * for any character still in bind pose. Re-evaluated only on the frames the
378
+ * skin matrices are re-uploaded, which is never for an idle stage. */
379
+ rigid: boolean;
380
+ /** The shared skin matrix, when rigid: model space → world, INCLUDING the
381
+ * scene placement (setModelTransform bakes the root into skinning). */
382
+ rigidXform: Float32Array;
383
+ /** Bound on how far skinning can carry a vertex from the bone that drives it:
384
+ * max over vertices of max over influencing joints of |v − bindPos(joint)|,
385
+ * plus the largest single vertex-morph displacement. An AABB over the model's
386
+ * POSED bone positions grown by this contains every skinned vertex, because a
387
+ * skinned position is a convex combination of rigid images of v, each within
388
+ * that distance of its bone's posed position. */
389
+ skinMargin: number;
286
390
  }
287
391
  /**
288
392
  * One material a type-8 morph can reach, with the uniform block as it loaded.
@@ -339,6 +443,10 @@ export declare class Engine {
339
443
  private zeroStyleBuffer;
340
444
  private mainPipelineLayout;
341
445
  private sceneTargets;
446
+ /** The scene pass's attachment formats, settled at init once the device has
447
+ * said which HDR format it will blend. Every scene-pass pipeline asks
448
+ * scene-contract for its targets against these. */
449
+ private get sceneFormats();
342
450
  private fullVertexBufferLayouts;
343
451
  private defaultToonRampTexture;
344
452
  private groundShadowPipeline;
@@ -381,6 +489,16 @@ export declare class Engine {
381
489
  private hdrResolveTexture;
382
490
  private static readonly MULTISAMPLE_COUNT;
383
491
  private hdrFormat;
492
+ /** Main-pass depth. Float when the adapter offers depth32float-stencil8, which
493
+ * is also what makes reversed-Z worth switching on. */
494
+ private depthFormat;
495
+ /** Near maps to 1 and far to 0. Set once at init and never toggled — every
496
+ * pipeline's compare function and both depth clears are chosen from it. */
497
+ private reversedZ;
498
+ /** The compare a "draw what is in front" pipeline wants, either way round. */
499
+ private get depthAhead();
500
+ /** The value a cleared depth buffer holds: the FAR plane, whichever end that is. */
501
+ private get depthClear();
384
502
  /** Stencil value stamped by eye draws so hair can stencil-test against it and
385
503
  * alpha-blend a second pass over eye silhouette pixels (see-through-hair effect). */
386
504
  private static readonly STENCIL_EYE_VALUE;
@@ -393,15 +511,229 @@ export declare class Engine {
393
511
  * cleared / edge-faded regions like before).
394
512
  * rg8unorm at 4× MSAA is 8 bytes/texel — still fits Apple TBDR tile memory comfortably. */
395
513
  private static readonly BLOOM_MASK_FORMAT;
514
+ /**
515
+ * The master switch for the id attachment. OFF — and off having been proven
516
+ * to work, not off because it was never finished.
517
+ *
518
+ * ON, because there is finally something that reads it: rzObjectAt and
519
+ * rzMaterialAt in the field module let an effect mask itself to one character
520
+ * or one material. It was switched off in the meantime rather than left
521
+ * running — rg16uint at the pass's sample count is around 33MB at 1080p,
522
+ * cleared and stored every frame, and paying that for a buffer nobody read
523
+ * would have handed back the same order of bandwidth the empty field-pass
524
+ * clears had just saved.
525
+ *
526
+ * Verified through setIdDebug against a real scene: flat colour per material
527
+ * with hard edges (so nothing interpolates or resolves them), the floor on
528
+ * its reserved id, black exactly where nothing drew. Turning it back off is
529
+ * this line, and the accessors then answer 0 rather than failing to compile.
530
+ */
531
+ private static readonly MRT_IDS;
532
+ /** The id attachment. Multisampled with the pass and NEVER resolved: an
533
+ * averaged id belongs to nothing, so consumers textureLoad sample 0. */
534
+ private idTexture;
535
+ private idView;
536
+ /** The id buffer drawn to the screen — see setIdDebug. */
537
+ private idDebugPipeline;
538
+ private idDebugBindGroupLayout;
539
+ private idDebugBindGroup;
540
+ private idDebug;
396
541
  private multisampleMaskTexture;
397
542
  private maskResolveTexture;
398
543
  private maskResolveView;
544
+ /**
545
+ * The installed effect's particle system, or null when it declared none.
546
+ *
547
+ * A fixed pool: the count is chosen at install and the slots recycle, so there
548
+ * is no allocation and no spawn-rate bookkeeping in the hot path. Dead slots
549
+ * cost a degenerate quad the rasteriser rejects, which is cheaper than the
550
+ * prefix sum and readback a compacted draw list would need every frame.
551
+ */
552
+ /** Ceiling for `// @particles`. Past this an author is asking for a stall. */
553
+ private static readonly MAX_PARTICLES;
554
+ private particleFrame;
555
+ /**
556
+ * The installed effect's persistent grid, or null when it declared none.
557
+ *
558
+ * Two textures, not one, and read/write alternate between them every frame:
559
+ * a shader cannot coherently read and write the same texture, so this is not
560
+ * an optimisation but the only correct shape. `parity` says which one holds
561
+ * the CURRENT grid — the one everything else samples.
562
+ */
563
+ private simSampler;
564
+ private simFallbackView;
565
+ /** 1×1 transparent stand-in, for every layer binding with nothing behind it. */
566
+ private trailFallbackView;
567
+ /**
568
+ * The field layer: user background/foreground mounts, ONE TARGET PAIR PER
569
+ * RESOLUTION. Index 0 is full, index 1 is half — coarsest last, so the
570
+ * composite reads them full-over-half.
571
+ *
572
+ * `@fullres` used to be a property of the shared targets: one effect
573
+ * declaring it promoted the pass for every effect installed, so a starfield
574
+ * that upsamples perfectly paid four times the pixels because a keyboard
575
+ * beside it needed crisp edges. Measured, that was the largest avoidable cost
576
+ * in the frame — Footprints went from about 1.2ms to 4.5ms purely by being
577
+ * dragged along.
578
+ *
579
+ * The price is that a resolution boundary is now a LAYER boundary. Within a
580
+ * pair, effects blend in document order; across pairs the full-res layer
581
+ * composites over the half-res one whatever the document said. Invisible for
582
+ * the additive glows this is nearly always used for, and stated because it is
583
+ * the one thing document order stopped deciding.
584
+ */
585
+ private static readonly FIELD_SCALES;
586
+ /** Reused for the per-frame field-clock upload — one 16-byte write per
587
+ * drawing effect, and allocating a fresh array for each would be garbage
588
+ * every frame. */
589
+ private fieldClockScratch;
590
+ /** Material parameters driven by the scene clock — see setStyleParamTrack. */
591
+ private paramTracks;
592
+ private fieldBgTextures;
593
+ private fieldBgViews;
594
+ private fieldFgTextures;
595
+ private fieldFgViews;
596
+ /** One per scale: the field shader reconstructs the full-res pixel it stands
597
+ * in for, so each pass needs its own (w, h, fullW, fullH). */
598
+ private fieldUniformBuffers;
599
+ private fieldFullW;
600
+ private fieldFullH;
601
+ private fieldBindGroupLayout;
602
+ private fieldPipelineLayout;
603
+ /**
604
+ * The audio analysis buffer every effect module binds: header
605
+ * [frames, bands, secondsPerFrame, audioTime], then [level, band0..bandN-1]
606
+ * per frame. Precomputed by the host for the whole track — never a live
607
+ * analyser, which would render silence during an export. Falls back to four
608
+ * zeroes (frames = 0) so layouts always bind.
609
+ */
610
+ private audioBuffer;
611
+ private audioFallbackBuffer;
612
+ private midiBuffer;
613
+ private midiFallbackBuffer;
614
+ /** Fixed-size (LYRICS_FLOATS): setLyrics is a write, never a reallocation,
615
+ * so lyric data arriving after any effect reaches it with no re-binding. */
616
+ private lyricsBuffer;
617
+ /** The rasterised lines, for rzLyricText. Sized to the track that arrives —
618
+ * a 1×1 placeholder until one does, so a scene with no lyrics pays nothing
619
+ * and a song's text is stored at the resolution it is drawn at. */
620
+ private lyricsTexture;
621
+ private lyricsTextureView;
622
+ /** The notes as installed, kept CPU-side because the per-pitch key map is
623
+ * rebuilt from them every time the clock moves. */
624
+ private midiNotes;
625
+ /** Header + key map, re-uploaded per clock write. */
626
+ private midiLiveScratch;
627
+ private midiRelease;
628
+ private audioTimeScratch;
399
629
  private renderPassDescriptor;
400
630
  private compositePassDescriptor;
401
631
  private compositePipelineIdentity;
402
632
  private compositePipelineGamma;
403
633
  private morphComputePipeline;
404
634
  private morphComputeBindGroupLayout;
635
+ /** Null once the pipeline has failed to compile — the pass then does nothing
636
+ * rather than invalidating every command buffer it touches. */
637
+ private cullPipeline;
638
+ private cullBindGroupLayout;
639
+ private cullBindGroup;
640
+ /** Every material draw in the scene, flat, in the order the passes walk them.
641
+ * A draw's position here IS its slot in every cull buffer. */
642
+ private cullDraws;
643
+ private cullModels;
644
+ /** Structure changed — model added or removed, draws re-sorted. NOT set by
645
+ * animation, physics or camera movement, which is the whole point. */
646
+ private cullListDirty;
647
+ private cullMetaBuffer;
648
+ private cullModelBuffer;
649
+ private cullHiddenBuffer;
650
+ private cullHidden;
651
+ private cullArgs;
652
+ /** Draws and models the current buffers can hold. A rebuild that fits inside
653
+ * these rewrites contents and allocates nothing. */
654
+ private cullCapacity;
655
+ private cullModelCapacity;
656
+ /** How many times the draw list has been rebuilt — a bundle-era regression
657
+ * guard, reported by getCullDiagnostics. Steady-state it must not climb. */
658
+ private cullRebuilds;
659
+ private opaqueBundle;
660
+ private transparentBundle;
661
+ private shadowBundles;
662
+ /** Set by scene STRUCTURE only. Every frame of animation, every physics step
663
+ * and every camera move must leave this alone — re-recording constantly is
664
+ * worse than having no bundles at all. */
665
+ private bundlesDirty;
666
+ /** Companion to cullRebuilds: how many times the bundles have been recorded.
667
+ * Steady-state it must not climb. */
668
+ private bundleRecords;
669
+ /** Every pass worth watching across a refactor. The query set, the resolve
670
+ * buffer and the readback are all sized from this, so adding one here is the
671
+ * whole change.
672
+ *
673
+ * `field` earns its place now that a scene runs SEVERAL field effects at
674
+ * once: it is one pass with N draws, its resolution is a property of the
675
+ * shared targets rather than of any one effect — so a single `@fullres`
676
+ * effect quadruples the pixel count for all of them — and it is the pass the
677
+ * field restructure moves. Restructuring it while it was the only untimed
678
+ * pass in the frame would have meant reasoning about the cost instead of
679
+ * reading it. */
680
+ private static readonly TIMED_PASSES;
681
+ private timestampQuerySet;
682
+ private timestampResolve;
683
+ private timestampRead;
684
+ /** A map is in flight; the readback buffer cannot be written while it is. */
685
+ private timestampBusy;
686
+ private gpuPassMs;
687
+ private cullCameraArgs;
688
+ private cullShadowArgs;
689
+ private cullMirrorArgs;
690
+ private static readonly REFLECTION_PLANE_Y;
691
+ private mirrorCameraData;
692
+ private mirrorCameraBuffer;
693
+ private mirrorVPData;
694
+ private mirrorVPBuffer;
695
+ private mirrorPerFrameBindGroup;
696
+ private mirrorColorMsTexture;
697
+ private mirrorColorTexture;
698
+ private mirrorColorView;
699
+ private mirrorMipCount;
700
+ private mirrorMipViews;
701
+ private mirrorBlurBindGroups;
702
+ private groundMirrorBlur;
703
+ private mirrorMaskMsTexture;
704
+ private mirrorIdMsTexture;
705
+ private mirrorDepthTexture;
706
+ private mirrorDepthReadView;
707
+ private mirrorPassDescriptor;
708
+ private mirrorOpaqueBundle;
709
+ private mirrorTransparentBundle;
710
+ /** setGroundMirror lands in step 7D; the debug dial is what exercises C. */
711
+ private groundMirror;
712
+ private reflectionDebug;
713
+ private reflectionDebugPipeline;
714
+ private reflectionDebugBindGroupLayout;
715
+ private reflectionDebugBindGroup;
716
+ private get reflectionActive();
717
+ private cullFrustaBuffer;
718
+ private cullFrustaBytes;
719
+ private cullFrustaF32;
720
+ private cullFrustaU32;
721
+ /** CPU-side mirrors of what was uploaded, so the reference test reads exactly
722
+ * the same numbers the compute did. */
723
+ private cullMetaBytes;
724
+ private cullMetaF32;
725
+ private cullMetaU32;
726
+ private cullModelData;
727
+ private cullModelFlags;
728
+ /** Per draw: bit0 = passes the camera frustum, bit1 = passes the light frustum
729
+ * and casts. Filled by the CPU reference; only computed when something asks. */
730
+ private cullReference;
731
+ private cullReferenceFrame;
732
+ private cullEnabled;
733
+ private cullFrame;
734
+ private cullScratchVp;
735
+ private cullReadback;
736
+ private cullReadbackInFlight;
405
737
  private compositeBindGroupLayout;
406
738
  private compositeBindGroup;
407
739
  private depthOfField;
@@ -416,15 +748,50 @@ export declare class Engine {
416
748
  private backgroundColor;
417
749
  private backdropEquirectTexture;
418
750
  private backdropEquirectView;
751
+ private backdropEquirectHDR;
752
+ private backdropStrength;
753
+ /** The installed HDRI's folded irradiance SH (27 floats), or null. */
754
+ private worldSH;
419
755
  private fallbackEquirectTexture;
420
756
  private fallbackEquirectView;
421
- private effect;
757
+ /**
758
+ * The scene's effects, in document order — the order they layer in.
759
+ *
760
+ * An array from here down even while setEffect installs exactly one, because
761
+ * the plural is the whole point of this step and a singleton that has to be
762
+ * "generalised later" is a singleton that shapes every call site against it.
763
+ */
764
+ private effects;
765
+ /** The first installed effect, for the many places that legitimately want
766
+ * "is anything installed" or the singleton API's one effect. */
767
+ private get effect();
422
768
  /** The cast, as the effect API sees it. Written per frame while an effect is
423
769
  * installed, and only up to what that effect actually declared. */
424
770
  private castBuffer;
771
+ /** The positional lights, as data — see shaders/lights.ts for the layout.
772
+ * Allocated once at full size and zero-filled, so "no lights" is a count of
773
+ * zero rather than an absent binding. */
774
+ private lightsBuffer;
775
+ private lightsData;
776
+ /** Just the header, for rewriting the total without touching a record. */
777
+ private lightHeader;
778
+ /** How many of the slots belong to the DOCUMENT. Effects get what follows. */
779
+ private docLightCount;
425
780
  private castData;
426
781
  /** Last frame's anchor world positions, for velocity. Keyed model id → slot. */
427
782
  private anchorPrev;
783
+ /**
784
+ * The SCENE's bones: which ones are recorded into the cast buffer, and which
785
+ * address each one holds. Built at install from every effect's requests, so
786
+ * the buffer is written once per bone however many effects read it.
787
+ *
788
+ * Everything that touches the cast buffer iterates THIS rather than any one
789
+ * effect's declarations — that is the whole change, and it is what makes N
790
+ * effects a loop instead of a rewrite. With one effect installed the table is
791
+ * exactly that effect's anchors in declaration order, which is why this lands
792
+ * with no visible difference.
793
+ */
794
+ private anchorTable;
428
795
  private castLastMs;
429
796
  /** Recent path per trailed anchor, keyed "model\0slot". Newest first, so the
430
797
  * shader's index 0 is now — written by unshifting rather than by tracking a
@@ -446,7 +813,6 @@ export declare class Engine {
446
813
  /** time=0 origin for the active effect — reset each setEffect. */
447
814
  /** Scene-clock reading when the current effect was installed. The effect's
448
815
  * `time` is measured from here — see where it is written. */
449
- private effectEpochScene;
450
816
  private compositeBloomView;
451
817
  private bloomSampler;
452
818
  private bloomBlitUniformBuffer;
@@ -473,16 +839,16 @@ export declare class Engine {
473
839
  private groundVertexBuffer?;
474
840
  private groundIndexBuffer?;
475
841
  private hasGround;
476
- private shadowMapTexture;
477
- private shadowMapDepthView;
842
+ private shadowMapTextures;
843
+ private shadowMapDepthViews;
478
844
  private brdfLutTexture;
479
845
  private brdfLutView;
480
846
  private filmicLutTexture;
481
847
  private filmicLutView;
482
848
  private static readonly FILMIC_LUT_WIDTH;
483
- private static readonly SHADOW_MAP_SIZE;
484
849
  private shadowDepthPipeline;
485
850
  private shadowLightVPBuffer;
851
+ private shadowCascadeVPBuffers;
486
852
  private shadowLightVPMatrix;
487
853
  private groundShadowBindGroup?;
488
854
  private shadowComparisonSampler;
@@ -563,7 +929,118 @@ export declare class Engine {
563
929
  /** Debug/diagnostic: skip every inverted-hull outline draw. */
564
930
  private outlineEnabled;
565
931
  setOutlineEnabled(on: boolean): void;
932
+ /**
933
+ * Draw the id attachment instead of the scene.
934
+ *
935
+ * The only way to SEE whether ids are right: with no consumer, a correct id
936
+ * buffer and a wrong one render the same frame. See id-debug.ts for what
937
+ * correct looks like and what each failure looks like instead.
938
+ *
939
+ * Returns false when there is nothing to show — ids compiled out, or a device
940
+ * that cannot multisample the format — rather than turning on and drawing
941
+ * black, which would read as "the ids are all zero".
942
+ */
943
+ setIdDebug(on: boolean): boolean;
944
+ /** True when the id attachment exists on this device. */
945
+ hasObjectIds(): boolean;
946
+ /** The pass that draws it, built lazily so a scene that never asks for the
947
+ * debug view never compiles it. */
948
+ private ensureIdDebugPipeline;
949
+ private renderIdDebugPass;
950
+ /**
951
+ * Show the floor mirror's target instead of the finished frame — the
952
+ * instrument that makes the reflection pass checkable before the ground
953
+ * consumes it. Dev surface, like setIdDebug beside it.
954
+ */
955
+ setReflectionDebug(on: boolean): void;
956
+ /**
957
+ * What is lighting the world seat right now — the dev-console answer to
958
+ * "did the HDRI actually arrive". Flat mode reports the same colour for
959
+ * every direction, which is what flat means.
960
+ */
961
+ getWorldLighting(): {
962
+ source: "hdri" | "flat";
963
+ strength: number;
964
+ up: [number, number, number];
965
+ down: [number, number, number];
966
+ };
967
+ /**
968
+ * Switch the floor mirror without rebuilding the ground — the adjust-tier
969
+ * sibling of addGround's own options. False when there is no ground.
970
+ *
971
+ * ON OR OFF, deliberately not a strength: the reflection is an independent
972
+ * LAYER beneath the floor surface, and how much of it shows is the ground's
973
+ * own opacity covering it. Blur 0 is a polished mirror; 1 samples the
974
+ * softest level, scaled by how far the reflected geometry sits behind the
975
+ * surface.
976
+ */
977
+ setGroundMirror(on: boolean, blur?: number): boolean;
978
+ private ensureReflectionDebugPipeline;
979
+ private renderReflectionDebugPass;
980
+ /** Refold the live camera with the reflection — a copy and a handful of
981
+ * sign flips; cheap enough to run every frame a mirror is on. */
982
+ private updateMirrorCamera;
983
+ /**
984
+ * The scene, mirrored about the floor, into the half-res reflection target.
985
+ *
986
+ * Models only: no ground (the mirror IS the ground), no particles, trails or
987
+ * field effects — the classic MMD stage-floor reflection is the cast, and
988
+ * each of those layers would need its own mirrored variant to join. Runs
989
+ * between emitLights (materials read the lights buffer) and the scene pass
990
+ * (whose ground will sample the resolve).
991
+ *
992
+ * KNOWN LIMIT, deliberate: geometry BELOW the floor plane would reflect up
993
+ * into the target — there is no oblique clip. MMD stages rarely have any;
994
+ * the clip is the follow-up if one shows.
995
+ */
996
+ private renderMirrorPass;
997
+ /**
998
+ * Fill the mirror's mip levels — the bloom pyramid's own 13-tap downsample,
999
+ * one pass per level. Only when the blur dial is up: at zero the ground
1000
+ * samples level 0 exactly and the chain would be work nobody reads.
1001
+ */
1002
+ private renderMirrorBlurChain;
1003
+ /**
1004
+ * Can this device multisample the id format at the pass's sample count?
1005
+ *
1006
+ * Asked by creating one and catching the validation error, because there is
1007
+ * no capability flag for it — WebGPU guarantees multisampling for renderable
1008
+ * colour formats but implementations have differed on uint targets, and the
1009
+ * cost of finding out the hard way is a device-lost on someone's machine and
1010
+ * a black canvas.
1011
+ *
1012
+ * The scope is popped in a finally: leaving an error scope pushed swallows
1013
+ * the NEXT error in this device, wherever it happens, and that error would
1014
+ * then be attributed to nothing.
1015
+ */
1016
+ private probeMultisampledIds;
566
1017
  private rebuildCompositeBindGroup;
1018
+ /**
1019
+ * Does any installed effect draw into field pair `layer`?
1020
+ *
1021
+ * Shared with renderFieldPass deliberately. The pass skips a pair nothing
1022
+ * draws into, so the composite must read the 1x1 fallback for that pair
1023
+ * rather than a target no one cleared. Two spellings of "empty" is two
1024
+ * spellings that eventually disagree, and the frame it disagreed on would
1025
+ * show last frame's effect after the effect was removed.
1026
+ *
1027
+ * It is only ever asked at bind-group build time, and setEffects rebuilds the
1028
+ * bind group after assigning this.effects — which is exactly the moment a
1029
+ * pair can change between empty and not.
1030
+ */
1031
+ private fieldPairUsed;
1032
+ /** One half of one field pair, as the composite should read it. */
1033
+ private fieldLayerView;
1034
+ private createFieldTargets;
1035
+ /**
1036
+ * ONE PER GRID PARITY.
1037
+ *
1038
+ * The grid alternates which texture holds the current grid, so the field pass
1039
+ * needs a bind group for each — built once here rather than rebuilt every
1040
+ * frame, which is what a single group would force and is pure waste for a
1041
+ * change that only ever toggles between two known states.
1042
+ */
1043
+ private rebuildFieldBindGroup;
567
1044
  /**
568
1045
  * Set a 360° backdrop from an equirectangular (2:1) image — a PhotoDome-style
569
1046
  * skybox at infinity, sampled per-pixel by view direction so it follows the
@@ -571,7 +1048,10 @@ export declare class Engine {
571
1048
  * affects lighting, bloom, or tonemapping. Pass null to remove (the background
572
1049
  * color, or transparency, takes over again).
573
1050
  */
574
- setBackdropEquirect(source: ImageBitmap | HTMLImageElement | HTMLCanvasElement | null): void;
1051
+ setBackdropEquirect(source: ImageBitmap | HTMLImageElement | HTMLCanvasElement | HdrImage | null, options?: {
1052
+ /** World strength for an HDR source, Blender's dial. Default 1. */
1053
+ strength?: number;
1054
+ }): void;
575
1055
  private makeCompositePipeline;
576
1056
  /**
577
1057
  * Install the scene's WGSL effect (shadertoy-style), rendered per-pixel in the
@@ -608,7 +1088,118 @@ export declare class Engine {
608
1088
  * is KEPT and diagnostics are returned with line numbers relative to the
609
1089
  * user's WGSL. Pass null to remove the effect.
610
1090
  */
1091
+ private compileEffect;
1092
+ /**
1093
+ * Compile an effect's particle stages and allocate its pool.
1094
+ *
1095
+ * Two modules, not one: the compute and render stages bind the same buffer
1096
+ * with different access (read_write vs read), and a single module would have
1097
+ * to pick one. Compiling them separately also means an author's helper names
1098
+ * live in their own compilation unit, which is what lets two effects both
1099
+ * define `hash21` without meeting.
1100
+ */
1101
+ /**
1102
+ * Install a LIST of effects, in document order — the order they layer in.
1103
+ *
1104
+ * Each is compiled independently and a failure is contained: it is reported in
1105
+ * its own slot of the returned array and left out of the scene, while the rest
1106
+ * install. That is the style-group discipline, and it matters more here — with
1107
+ * four effects on screen, "one bad shader blanks the scene" is the first bug
1108
+ * report anyone would file.
1109
+ *
1110
+ * The bones are allocated ONCE for the whole list: two effects naming the same
1111
+ * wrist share one address and one recorded path, and the cap is eight distinct
1112
+ * bones across the scene rather than per file.
1113
+ *
1114
+ * Null or empty clears everything.
1115
+ */
1116
+ setEffects(list: {
1117
+ wgsl: string;
1118
+ params?: Record<string, EffectParamValue>;
1119
+ }[] | null): Promise<EffectResult[]>;
1120
+ /**
1121
+ * Install ONE effect — the singleton API, kept because most scenes are one
1122
+ * effect and every existing caller uses it. A one-element setEffects.
1123
+ */
611
1124
  setEffect(wgsl: string | null, params?: Record<string, EffectParamValue>): Promise<EffectResult>;
1125
+ private buildParticles;
1126
+ /**
1127
+ * Step the pool, before the scene pass.
1128
+ *
1129
+ * Outside the render pass because a compute dispatch cannot be encoded inside
1130
+ * one — and it has to precede the draw that reads the same buffer, or the
1131
+ * quads render last frame's positions.
1132
+ */
1133
+ /**
1134
+ * Compile an effect's lightEmit stage and give it a bind group.
1135
+ *
1136
+ * Its own layout rather than a shared one: this is the only place the lights
1137
+ * buffer is WRITABLE, and every other binding of it is read-only. Keeping the
1138
+ * writable view here means a material pipeline cannot accidentally acquire
1139
+ * one.
1140
+ */
1141
+ private buildLightEmit;
1142
+ /**
1143
+ * Hand every emitting effect its slot range, and write the total.
1144
+ *
1145
+ * Document lights sit FIRST, at slot 0, and effects follow in document order.
1146
+ * That ordering is the stable one: a scene's own lamps are the thing a person
1147
+ * placed and can see in a list, so they should not move because an effect was
1148
+ * installed ahead of them.
1149
+ *
1150
+ * Called whenever either producer changes. The base lands in each effect's
1151
+ * uniform, so nothing recompiles.
1152
+ */
1153
+ private allocateLightSlots;
1154
+ /** Run every effect's lightEmit, before the pass that reads the result. */
1155
+ private emitLights;
1156
+ private stepParticles;
1157
+ /** Draw the pool. Inside the scene pass, so it is depth-tested and pre-bloom. */
1158
+ private renderParticles;
1159
+ /**
1160
+ * Compile an effect's ribbon stage.
1161
+ *
1162
+ * One instance per (slot, subject, segment), so a scene with several dancers
1163
+ * and several declared bones is still one draw and nothing is computed per
1164
+ * frame on the CPU.
1165
+ */
1166
+ private buildTrails;
1167
+ private releaseTrails;
1168
+ /**
1169
+ * Ribbons, drawn INSIDE the scene pass — as geometry, in HDR, before bloom.
1170
+ *
1171
+ * They used to own a colour target and be pasted over the finished frame
1172
+ * after tone mapping, which is exactly why they could not bloom: nothing
1173
+ * composited post-tonemap can. Here they are lit like anything else in the
1174
+ * scene, depth-tested against the body they circle, and their emission
1175
+ * reaches the bloom prefilter through the aux mask they now write.
1176
+ *
1177
+ * Takes the pass rather than opening one: that IS the change.
1178
+ */
1179
+ private drawTrails;
1180
+ /** The user's field mounts, drawn at half resolution for the composite to
1181
+ * upsample. Runs the whole quad — uniform control flow, so effects may use
1182
+ * derivatives freely, which the old inline path had to forbid. */
1183
+ private renderFieldPass;
1184
+ /** The trail bind group holds the depth view, which a resize recreates. */
1185
+ private rebindTrails;
1186
+ private releaseParticles;
1187
+ /**
1188
+ * Compile and allocate the effect's persistent grid.
1189
+ *
1190
+ * The textures are created ZEROED, which is the contract a kernel is written
1191
+ * against: rzGridFrame() is 0 on the first step and every value it reads is
1192
+ * zero, so seeding is just "if frame is 0, return the initial state".
1193
+ */
1194
+ private buildSim;
1195
+ private releaseGrid;
1196
+ /**
1197
+ * Step the grid, before anything reads it.
1198
+ *
1199
+ * Outside the render pass, like the particle step and for the same reason —
1200
+ * and before the field pass, or an effect samples a grid one frame stale.
1201
+ */
1202
+ private stepSim;
612
1203
  /** Which mounts the installed effect declared. Both false when none is set. */
613
1204
  getEffectMounts(): {
614
1205
  background: boolean;
@@ -684,6 +1275,67 @@ export declare class Engine {
684
1275
  * loaded. A timeline cannot draw a lane to scale without it, and the camera's
685
1276
  * length is its own: it does not have to match any model's clip. */
686
1277
  getCameraVmdDuration(): number;
1278
+ /**
1279
+ * Install a track's precomputed analysis for the rzAudio* effect functions:
1280
+ * `data` is frames × (2 + bands) floats — loudness, bass onset, then the band
1281
+ * magnitudes, all 0..1 — sampled by the clock given to setAudioTime. Null
1282
+ * clears back to silence.
1283
+ *
1284
+ * Precomputed for the WHOLE track, never fed live from an analyser: an export
1285
+ * steps the engine frame by frame rather than playing in real time, so live
1286
+ * analysis would render silence into the exported video.
1287
+ */
1288
+ setAudioData(data: Float32Array | null, bandsPerFrame: number, secondsPerFrame: number): void;
1289
+ /**
1290
+ * Install a score — note events — for the rzNote* and rzKey* effect functions.
1291
+ * Null clears it.
1292
+ *
1293
+ * The sibling of setAudioData, and deliberately a SEPARATE interface rather
1294
+ * than something derived from it: a spectrum cannot give back a discrete pitch
1295
+ * and onset, and that discreteness is the whole substance of a falling note.
1296
+ * A scene can hold both and read them together.
1297
+ *
1298
+ * `release` is how long a key keeps glowing after its note ends, in seconds.
1299
+ * It belongs here rather than in the effect because the key map it feeds is
1300
+ * computed on the CPU — see writeMidiClock.
1301
+ */
1302
+ /**
1303
+ * Install the track's lyric lines for the rzLyric* effect functions — the
1304
+ * timing of the words on the scene clock, and optionally the words
1305
+ * themselves: `atlas.source` is a canvas/bitmap of rasterised lines (the
1306
+ * host draws them — Canvas2D handles every script the platform does) with
1307
+ * `atlas.rects` saying where each line sits, in 0..1 [u0, vTop, u1, vBottom].
1308
+ * Null clears. A plain buffer write plus at most a texture copy: buffer and
1309
+ * atlas are both fixed-size, so nothing re-binds whenever lyrics arrive.
1310
+ */
1311
+ setLyrics(lines: LyricLine[] | null, atlas?: {
1312
+ source: GPUCopyExternalImageSource;
1313
+ width: number;
1314
+ height: number;
1315
+ rects: LyricRect[];
1316
+ }): void;
1317
+ setMidiNotes(notes: MidiNote[] | null, release?: number): void;
1318
+ /**
1319
+ * Move the score's clock, and rebuild the per-pitch key map from it.
1320
+ *
1321
+ * The map is why this is more than a header write. Falling notes index the
1322
+ * note list directly, but a keyboard glow asks the opposite question per
1323
+ * pixel — is anything sounding at THIS pitch — and answering that in the
1324
+ * shader would be a scan of the whole score per fragment. One O(notes) pass
1325
+ * here, once a frame, turns it into a single lookup.
1326
+ *
1327
+ * The pass is a full scan rather than a cursor because scrubbing exists: a
1328
+ * cursor is only correct while time moves forward, and a timeline drag is
1329
+ * exactly when a wrong answer is most visible. Ten thousand notes is ten
1330
+ * thousand comparisons, which is nothing beside the pose pass.
1331
+ */
1332
+ setMidiTime(seconds: number, playing?: boolean): void;
1333
+ /**
1334
+ * Where the track is NOW, in seconds — written by whoever owns playback: the
1335
+ * editor's audio clock, the viewer's, or the export loop with its exact
1336
+ * per-frame time. A 4-byte header write, cheap enough for every frame.
1337
+ */
1338
+ setAudioTime(seconds: number, playing?: boolean): void;
687
1339
  /** Every camera keyframe's frame index — what a timeline draws as its cuts.
688
1340
  * Empty when no camera VMD is loaded. */
689
1341
  getCameraVmdKeyframes(): number[];
@@ -739,7 +1391,39 @@ export declare class Engine {
739
1391
  noiseStrength?: number;
740
1392
  /** Whole-ground opacity, 0–1 (multiplies the radial edge fade). Default 1. */
741
1393
  opacity?: number;
1394
+ /** Floor mirror, on or off — NOT a strength: the reflection is its own
1395
+ * layer, and how much of it shows is the surface's own `opacity`
1396
+ * covering it. Off (default) never renders the reflection pass at all. */
1397
+ mirror?: boolean;
1398
+ /** Mirror softness, 0–1: 0 a polished mirror, 1 the softest blur level,
1399
+ * scaled by how far the reflected geometry sits behind the surface. */
1400
+ mirrorBlur?: number;
742
1401
  }): void;
1402
+ /**
1403
+ * The scene's positional lights — an ADDITIVE layer over the sun, which stays
1404
+ * the key light and keeps the toon ramp to itself.
1405
+ *
1406
+ * Colour and intensity are multiplied here rather than stored apart: every
1407
+ * read is the product, and two numbers that are only ever multiplied are two
1408
+ * numbers that can disagree.
1409
+ *
1410
+ * Past MAX_LIGHTS the extras are DROPPED, not wrapped: the lights that fit
1411
+ * keep the meaning the caller gave them, which is the same rule the anchor
1412
+ * table follows. Passing none (or an empty list) turns the layer off and the
1413
+ * scene renders exactly as it did before lights existed.
1414
+ */
1415
+ setLights(
1416
+ /** Structural {x,y,z} rather than the Vec3 class, the same choice effect
1417
+ * params make: a scene document's JSON passes straight in, and so does a
1418
+ * literal typed into a console. Vec3 satisfies it either way. */
1419
+ lights: {
1420
+ position: XYZ;
1421
+ color: XYZ;
1422
+ intensity?: number;
1423
+ radius?: number;
1424
+ }[] | null): void;
1425
+ /** How many positional lights the scene is carrying. */
1426
+ getLightCount(): number;
743
1427
  private updateLightBuffer;
744
1428
  getStats(): EngineStats;
745
1429
  runRenderLoop(callback?: () => void): void;
@@ -852,10 +1536,170 @@ export declare class Engine {
852
1536
  private updateInstances;
853
1537
  private updateVertexBuffer;
854
1538
  private dispatchMorphCompute;
1539
+ private static readonly CULL_META_BYTES;
1540
+ private static readonly CULL_MODEL_FLOATS;
1541
+ private static readonly CULL_ARG_WORDS;
1542
+ private static readonly CULL_DRAW_CASTS_SHADOW;
1543
+ private static readonly CULL_MODEL_VISIBLE;
1544
+ private static readonly CULL_MODEL_RIGID;
1545
+ /**
1546
+ * Flatten the scene's material draws into the order every cull buffer is
1547
+ * indexed by, and upload everything that only changes with STRUCTURE: the
1548
+ * per-draw metadata and the constant words of the indirect arguments.
1549
+ *
1550
+ * Runs on model add/remove and on any re-sort of a model's draws (a style
1551
+ * group assignment re-ranks them). Animation, physics and camera movement all
1552
+ * leave it alone — that is the same invalidation set the render bundles will
1553
+ * want, tested here first where being wrong is cheap.
1554
+ */
1555
+ private rebuildCullList;
1556
+ /**
1557
+ * Per-draw hidden state, uploaded only when it actually changes.
1558
+ *
1559
+ * The two sets behind it are cheap to read but the buffer is not worth
1560
+ * rewriting every frame: applyMaterialMorphs rebuilds morphHiddenMaterials on
1561
+ * every frame of any character carrying a face VMD, and almost every one of
1562
+ * those rebuilds produces the same answer. So compare, then upload.
1563
+ */
1564
+ private writeCullHidden;
1565
+ private releaseCullBuffers;
1566
+ /**
1567
+ * One record per model: the rigid transform its per-material boxes live under,
1568
+ * or the world sphere that bounds it in any pose. Written every frame, because
1569
+ * this is the part animation moves — a few hundred bytes per model.
1570
+ */
1571
+ private writeCullModels;
1572
+ /**
1573
+ * The world sphere for a skinned model: an AABB over its POSED bone positions,
1574
+ * grown by the model's skin margin, then carried through the scene placement.
1575
+ *
1576
+ * Bone positions come from the pose that already ran this frame, so a jump, a
1577
+ * run across the stage or a physics-driven skirt are all inside it by
1578
+ * construction — none of which a bind-pose box would have contained. See
1579
+ * ModelInstance.skinMargin for why growing by that one number is a bound and
1580
+ * not an estimate.
1581
+ */
1582
+ private writeCullSphere;
1583
+ /**
1584
+ * The camera's six frustum planes then the sun's, normalized, as inward
1585
+ * half-spaces (`dot(n, p) + d >= 0` is inside).
1586
+ *
1587
+ * Extracted from the combined view-projection rather than rebuilt from the
1588
+ * camera's own parameters, so a VMD-driven shot, an orbit and the shadow
1589
+ * volume's ortho box all go through one code path and none of them can
1590
+ * disagree with what the vertex shader actually projects.
1591
+ *
1592
+ * Culling shadow casters to the light's frustum looks like it should lose
1593
+ * casters that stand outside the volume and throw shade into it. It cannot:
1594
+ * the cull tests the OUTERMOST cascade's box, each cascade's rasterizer clips
1595
+ * to its own box, and every inner box lies inside the outer one (the
1596
+ * containment invariant in shadow-cascades.ts) — so anything rejected here
1597
+ * was contributing to no cascade at all.
1598
+ */
1599
+ private writeCullFrusta;
1600
+ /**
1601
+ * Cull every material draw against the camera and the sun, writing
1602
+ * `instanceCount` into two indirect-argument buffers.
1603
+ *
1604
+ * Nothing draws from those buffers yet. This increment stands the pass up and
1605
+ * leaves the draw path issuing direct draws, so the bounds and the frusta can
1606
+ * be checked against a scene that is definitely rendering correctly — see
1607
+ * getCullDiagnostics and setCullApply.
1608
+ */
1609
+ /**
1610
+ * Record the three bundles: the shadow pass, the opaque phase and the
1611
+ * transparent phase.
1612
+ *
1613
+ * The insight this rests on is that a character's DRAW COMMANDS are stable
1614
+ * frame to frame — same pipeline, same bind groups, same index ranges, with
1615
+ * only the contents of the skin-matrix buffer changing. So bundles apply to
1616
+ * the cast, not merely to scenery, and what invalidates one is scene
1617
+ * STRUCTURE. Animation, physics, camera movement, material morphs, a hidden
1618
+ * material and a hidden model all leave a bundle valid — the first three
1619
+ * because they touch buffers and not commands, the last two because their
1620
+ * switches live in the cull compute rather than in this encode loop.
1621
+ *
1622
+ * The ground and the particles are deliberately not in a bundle: the ground is
1623
+ * one draw that sits BETWEEN the two phases, and the particle count comes from
1624
+ * the CPU. Both are cheaper to leave direct than to invalidate around.
1625
+ */
1626
+ private recordBundles;
1627
+ /** The two query slots for a pass, or undefined where the device has no
1628
+ * timestamps — which every pass descriptor accepts as "do not measure". */
1629
+ private stamps;
1630
+ /**
1631
+ * Resolve this frame's timings and start a readback, at most one in flight.
1632
+ *
1633
+ * Deliberately not awaited anywhere in the frame: a timing that costs a stall
1634
+ * to collect would change the thing it is measuring. The numbers are therefore
1635
+ * a frame or two old, which is exactly right for what they are for — watching
1636
+ * a pass get more expensive across a refactor, not attributing one frame.
1637
+ */
1638
+ private resolveTimestamps;
1639
+ /**
1640
+ * Milliseconds on the GPU per pass, or null where the device cannot measure.
1641
+ *
1642
+ * The regression guard for the draw-path work: these are the numbers that say
1643
+ * whether restructuring cost anything, which is the claim being made — not
1644
+ * whether it made the scene faster, which was never the goal.
1645
+ */
1646
+ getGpuTimings(): Record<string, number> | null;
1647
+ private dispatchCull;
1648
+ /**
1649
+ * The same test the compute runs, on the CPU, from the same uploaded numbers.
1650
+ *
1651
+ * Deliberately a second implementation rather than a shared one: two
1652
+ * independent readings of the same data is what makes agreement evidence. It
1653
+ * reads the mirrors written by writeCullModels/writeCullFrusta, so it answers
1654
+ * for the frame that was last dispatched, and caches per frame because both
1655
+ * the debug draw gate and the diagnostics want it.
1656
+ */
1657
+ private cullReferencePass;
1658
+ /**
1659
+ * Issue one material draw, indirect when the cull owns it.
1660
+ *
1661
+ * The outline and the over-eyes pass share their material's slot rather than
1662
+ * getting their own: same index range, same bounds, so the same decision. That
1663
+ * also means an outline can never survive a material that was culled, which is
1664
+ * the only relationship between them that is ever correct.
1665
+ *
1666
+ * Falls back to a direct draw for anything outside the cull list — the ground,
1667
+ * and any draw whose slot has not been assigned yet.
1668
+ */
1669
+ private issueDraw;
1670
+ /**
1671
+ * Turn frustum culling off without turning the pass off.
1672
+ *
1673
+ * The compute keeps running and keeps reporting; it just writes "visible" for
1674
+ * every draw. That is the A/B a missing-geometry report needs — if it is still
1675
+ * missing with culling off, the cull was not what removed it — and it costs
1676
+ * one uniform word rather than a rebuild.
1677
+ */
1678
+ setCullEnabled(on: boolean): void;
1679
+ /**
1680
+ * Read the GPU's culling decisions back and diff them against the CPU
1681
+ * reference over the same frame's uploaded data.
1682
+ *
1683
+ * A clean report means the compute, its buffer layouts and the plane
1684
+ * extraction all agree with a second implementation. It does NOT prove the
1685
+ * bounds contain the geometry — that is what setCullApply(true) and looking at
1686
+ * the scene is for.
1687
+ */
1688
+ getCullDiagnostics(): Promise<CullDiagnostics>;
1689
+ /** The GPU half of getCullDiagnostics: copy both argument buffers back and
1690
+ * diff them against the reference. */
1691
+ private readCullArgs;
855
1692
  private setupModelInstance;
856
1693
  private createGpuMorph;
857
1694
  private createGroundGeometry;
858
1695
  private createShadowGroundResources;
1696
+ /**
1697
+ * (Re)build the ground's bind group. Its own method because the RESIZE path
1698
+ * needs it too: the reflection resolve is recreated at every canvas size,
1699
+ * and a bind group holding the old view would sample a destroyed texture on
1700
+ * the first resized frame with a mirror on.
1701
+ */
1702
+ private buildGroundBindGroup;
859
1703
  /** Whether the shadow map needs clearing — see the shadow pass in `render`.
860
1704
  *
861
1705
  * Starts true so the very first frame runs the pass even with an empty scene. The
@@ -889,6 +1733,14 @@ export declare class Engine {
889
1733
  */
890
1734
  private applyMaterialMorphs;
891
1735
  private createUniformBuffer;
1736
+ /** Whether a material is switched on — the user's own toggle, or a material
1737
+ * morph having driven its alpha to zero.
1738
+ *
1739
+ * The material passes no longer consult this: their draws are indirect, and
1740
+ * the cull compute zeroes the instance count of anything hidden, which is
1741
+ * what lets a render bundle survive a face VMD rewriting the morph-hidden set
1742
+ * sixty times a second. It remains the answer for the passes that draw
1743
+ * directly, where there is no argument buffer to zero. */
892
1744
  private shouldRenderDrawCall;
893
1745
  private createTextureFromLogicalPath;
894
1746
  private generateMipmaps;
@@ -950,6 +1802,31 @@ export declare class Engine {
950
1802
  /** Clear all style groups on a model — every material returns to the hand-shader path. */
951
1803
  resetStyleGroups(modelName: string): void;
952
1804
  /** Instant adjust-tier write: set one exposed slider on a group's applied graph. */
1805
+ /**
1806
+ * Drive a material parameter from the SCENE CLOCK.
1807
+ *
1808
+ * The channel this writes into already existed — setStyleParam below puts a
1809
+ * value straight into the style uniform. What a track adds is WHEN: the value
1810
+ * is a pure function of scene time, so a scene describes a dissolve once and
1811
+ * playback, a re-open and an offline export stepped at another rate all
1812
+ * produce the same frames.
1813
+ *
1814
+ * Addressed BY NAME (model, group, param), not by id. Worth stating because
1815
+ * the id attachment landed alongside this and the two look related: ids
1816
+ * answer "which object is this PIXEL", which is a screen-space question, and
1817
+ * a track answers "what is this parameter NOW". Nothing here needs MRT.
1818
+ *
1819
+ * Null or empty clears the track and leaves the parameter wherever it was —
1820
+ * removing an animation is not the same as resetting a value, and guessing
1821
+ * which the caller meant would be worse than either.
1822
+ */
1823
+ setStyleParamTrack(modelName: string, groupId: string, paramId: string, keys: ParamKey[] | null): boolean;
1824
+ /** Forget what a group's tracks last wrote, so the next frame writes it again.
1825
+ * Called whenever something else has written that uniform underneath them. */
1826
+ private invalidateParamTracks;
1827
+ /** Every track, at the current scene clock. Called once per frame, before the
1828
+ * pass that reads the uniforms it writes. */
1829
+ private evaluateParamTracks;
953
1830
  setStyleParam(modelName: string, groupId: string, paramId: string, value: number | [number, number, number]): boolean;
954
1831
  private currentClaims;
955
1832
  private compileAndInstallGroup;
@@ -980,6 +1857,14 @@ export declare class Engine {
980
1857
  * right after the material itself, like MMD's per-mesh outline stage.
981
1858
  */
982
1859
  private setModelDrawState;
1860
+ /**
1861
+ * Which eye the scene is being drawn FOR — the main camera or the floor
1862
+ * mirror. Threaded explicitly through the phase draws rather than read off
1863
+ * the engine, because both sets of bundles are recorded in one call and
1864
+ * ambient state at record time is how a mirror bundle ends up baked with the
1865
+ * main camera's bind group.
1866
+ */
1867
+ private sceneView;
983
1868
  private renderModelOpaquePhase;
984
1869
  private renderModelTransparentPhase;
985
1870
  /** Depth-only re-draw of transparent-bucket materials (see depth-prepass.ts).
@@ -1019,6 +1904,16 @@ export declare class Engine {
1019
1904
  * export record the same path at the same spacing. A frame that covers several
1020
1905
  * intervals emits several samples rather than one, or a fast hand would tear.
1021
1906
  */
1907
+ /**
1908
+ * Forget every recorded path, because the slots have been re-dealt.
1909
+ *
1910
+ * Called on every effect swap. The rings are keyed by (model, slot) and a slot
1911
+ * is an ADDRESS, so re-allocating the table can leave a left wrist's recorded
1912
+ * history sitting at the address a right wrist now occupies — a ribbon drawn
1913
+ * confidently along a path that belongs to another bone. Refilling from live
1914
+ * samples costs about two seconds of trail and cannot be wrong.
1915
+ */
1916
+ private clearTrailHistory;
1022
1917
  private writeTrail;
1023
1918
  private updateSkinMatrices;
1024
1919
  private updateStats;