reze-engine 0.43.0 → 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 (135) hide show
  1. package/README.md +40 -438
  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 +841 -53
  6. package/dist/engine.d.ts.map +1 -1
  7. package/dist/engine.js +3498 -451
  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/param-track.d.ts +48 -0
  34. package/dist/param-track.d.ts.map +1 -0
  35. package/dist/param-track.js +80 -0
  36. package/dist/physics/types.d.ts.map +1 -1
  37. package/dist/physics/types.js +3 -0
  38. package/dist/reflection.d.ts +27 -0
  39. package/dist/reflection.d.ts.map +1 -0
  40. package/dist/reflection.js +93 -0
  41. package/dist/shaders/anchor-table.d.ts +56 -0
  42. package/dist/shaders/anchor-table.d.ts.map +1 -0
  43. package/dist/shaders/anchor-table.js +128 -0
  44. package/dist/shaders/cast-api.d.ts +2 -0
  45. package/dist/shaders/cast-api.d.ts.map +1 -0
  46. package/dist/shaders/cast-api.js +121 -0
  47. package/dist/shaders/cast-layout.d.ts +21 -0
  48. package/dist/shaders/cast-layout.d.ts.map +1 -0
  49. package/dist/shaders/cast-layout.js +20 -0
  50. package/dist/shaders/lights.d.ts +79 -0
  51. package/dist/shaders/lights.d.ts.map +1 -0
  52. package/dist/shaders/lights.js +269 -0
  53. package/dist/shaders/lyrics-api.d.ts +39 -0
  54. package/dist/shaders/lyrics-api.d.ts.map +1 -0
  55. package/dist/shaders/lyrics-api.js +187 -0
  56. package/dist/shaders/materials/common.d.ts +2 -4
  57. package/dist/shaders/materials/common.d.ts.map +1 -1
  58. package/dist/shaders/materials/common.js +87 -35
  59. package/dist/shaders/midi-api.d.ts +10 -0
  60. package/dist/shaders/midi-api.d.ts.map +1 -0
  61. package/dist/shaders/midi-api.js +114 -0
  62. package/dist/shaders/passes/composite.d.ts +28 -22
  63. package/dist/shaders/passes/composite.d.ts.map +1 -1
  64. package/dist/shaders/passes/composite.js +165 -138
  65. package/dist/shaders/passes/cull.d.ts +2 -0
  66. package/dist/shaders/passes/cull.d.ts.map +1 -0
  67. package/dist/shaders/passes/cull.js +138 -0
  68. package/dist/shaders/passes/field-blit.d.ts +26 -0
  69. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  70. package/dist/shaders/passes/field-blit.js +65 -0
  71. package/dist/shaders/passes/grid.d.ts +31 -0
  72. package/dist/shaders/passes/grid.d.ts.map +1 -0
  73. package/dist/shaders/passes/grid.js +169 -0
  74. package/dist/shaders/passes/ground.d.ts +13 -1
  75. package/dist/shaders/passes/ground.d.ts.map +1 -1
  76. package/dist/shaders/passes/ground.js +170 -25
  77. package/dist/shaders/passes/hosted-api.d.ts +57 -0
  78. package/dist/shaders/passes/hosted-api.d.ts.map +1 -0
  79. package/dist/shaders/passes/hosted-api.js +166 -0
  80. package/dist/shaders/passes/id-debug.d.ts +28 -0
  81. package/dist/shaders/passes/id-debug.d.ts.map +1 -0
  82. package/dist/shaders/passes/id-debug.js +74 -0
  83. package/dist/shaders/passes/particles.d.ts +10 -2
  84. package/dist/shaders/passes/particles.d.ts.map +1 -1
  85. package/dist/shaders/passes/particles.js +37 -109
  86. package/dist/shaders/passes/scene-contract.d.ts +128 -0
  87. package/dist/shaders/passes/scene-contract.d.ts.map +1 -0
  88. package/dist/shaders/passes/scene-contract.js +207 -0
  89. package/dist/shaders/passes/sim.d.ts +34 -0
  90. package/dist/shaders/passes/sim.d.ts.map +1 -0
  91. package/dist/shaders/passes/sim.js +169 -0
  92. package/dist/shaders/passes/trails.d.ts +14 -2
  93. package/dist/shaders/passes/trails.d.ts.map +1 -1
  94. package/dist/shaders/passes/trails.js +55 -90
  95. package/dist/shaders/score-api.d.ts +10 -0
  96. package/dist/shaders/score-api.d.ts.map +1 -0
  97. package/dist/shaders/score-api.js +114 -0
  98. package/dist/shadow-cascades.d.ts +45 -0
  99. package/dist/shadow-cascades.d.ts.map +1 -0
  100. package/dist/shadow-cascades.js +70 -0
  101. package/dist/vmd-loader.d.ts +3 -2
  102. package/dist/vmd-loader.d.ts.map +1 -1
  103. package/package.json +1 -1
  104. package/src/camera.ts +31 -8
  105. package/src/engine.ts +4015 -556
  106. package/src/graph/registry.ts +2 -2
  107. package/src/graph/slots.ts +37 -9
  108. package/src/hdr.ts +156 -0
  109. package/src/ibl.ts +115 -0
  110. package/src/ik-solver.ts +1 -1
  111. package/src/index.ts +12 -0
  112. package/src/math.ts +23 -17
  113. package/src/midi-loader.ts +246 -0
  114. package/src/model.ts +3 -3
  115. package/src/param-track.ts +83 -0
  116. package/src/physics/types.ts +4 -1
  117. package/src/reflection.ts +94 -0
  118. package/src/shaders/anchor-table.ts +147 -0
  119. package/src/shaders/cast-api.ts +123 -0
  120. package/src/shaders/cast-layout.ts +20 -0
  121. package/src/shaders/lights.ts +280 -0
  122. package/src/shaders/lyrics-api.ts +202 -0
  123. package/src/shaders/materials/common.ts +89 -35
  124. package/src/shaders/midi-api.ts +116 -0
  125. package/src/shaders/passes/composite.ts +182 -139
  126. package/src/shaders/passes/cull.ts +139 -0
  127. package/src/shaders/passes/grid.ts +178 -0
  128. package/src/shaders/passes/ground.ts +172 -25
  129. package/src/shaders/passes/hosted-api.ts +171 -0
  130. package/src/shaders/passes/id-debug.ts +75 -0
  131. package/src/shaders/passes/particles.ts +54 -112
  132. package/src/shaders/passes/scene-contract.ts +266 -0
  133. package/src/shaders/passes/trails.ts +77 -93
  134. package/src/shadow-cascades.ts +97 -0
  135. package/src/vmd-loader.ts +2 -2
@@ -0,0 +1,93 @@
1
+ // The mirror camera, as arithmetic — pure and headlessly testable, the
2
+ // shadow-cascades precedent.
3
+ //
4
+ // A planar reflection is not a second camera aimed by hand; it is the SAME
5
+ // camera with the world reflected about the floor plane. Fold the reflection
6
+ // into the view matrix and everything downstream is untouched: world positions
7
+ // stay TRUE world positions, so sun, shadows and positional lights evaluate at
8
+ // the unmirrored point — which is exactly what a mirror shows, an object lit
9
+ // as it is, seen from a mirrored eye. The only other value that must mirror is
10
+ // the eye itself, because specular reads the view direction from it.
11
+ //
12
+ // Winding: a reflection has determinant -1, so triangle orientation flips.
13
+ // Every scene-pass pipeline that draws into the mirror culls "none", which is
14
+ // what makes this legal without a flipped-frontFace pipeline set. The OUTLINE
15
+ // culls "back" and is therefore skipped in the mirror — its hull would face
16
+ // the wrong way and ink over the model.
17
+ /**
18
+ * The debug view: the reflection target drawn over the finished frame — the
19
+ * only way to SEE whether the mirror pass is right before anything consumes
20
+ * it, the same instrument discipline as setIdDebug. The target is HDR linear;
21
+ * a Reinhard fold plus a square-root keeps highlights readable without
22
+ * involving the real view transform, which a diagnostic does not need.
23
+ */
24
+ export const REFLECTION_DEBUG_WGSL = /* wgsl */ `
25
+ @group(0) @binding(0) var t: texture_2d<f32>;
26
+ @group(0) @binding(1) var s: sampler;
27
+
28
+ struct VSOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f, };
29
+
30
+ @vertex fn vs(@builtin(vertex_index) i: u32) -> VSOut {
31
+ var out: VSOut;
32
+ let x = f32(i32(i / 2u) * 4 - 1);
33
+ let y = f32(i32(i % 2u) * 4 - 1);
34
+ out.pos = vec4f(x, y, 0.0, 1.0);
35
+ out.uv = vec2f(x * 0.5 + 0.5, 0.5 - y * 0.5);
36
+ return out;
37
+ }
38
+
39
+ @fragment fn fs(in: VSOut) -> @location(0) vec4f {
40
+ let c = textureSample(t, s, in.uv).rgb;
41
+ return vec4f(sqrt(c / (vec3f(1.0) + c)), 1.0);
42
+ }
43
+ `;
44
+ /**
45
+ * Reflection about the horizontal plane y = h, column-major.
46
+ *
47
+ * p' = (x, 2h - y, z)
48
+ */
49
+ export function reflectionAboutY(h) {
50
+ // prettier-ignore
51
+ return new Float32Array([
52
+ 1, 0, 0, 0,
53
+ 0, -1, 0, 0,
54
+ 0, 0, 1, 0,
55
+ 0, 2 * h, 0, 1,
56
+ ]);
57
+ }
58
+ /**
59
+ * Fill a camera-uniform block for the mirror pass from the live one.
60
+ *
61
+ * Layout is the material CameraUniforms: view at 0, projection at 16, eye at
62
+ * 32, render-target height at 35 — the same 36 floats the main camera writes,
63
+ * copied rather than re-derived so the two cannot disagree about anything but
64
+ * the reflection.
65
+ *
66
+ * view' = view × R (column-vector convention, matching `projection * view *
67
+ * pos` in the vertex shaders); projection unchanged; eye reflected.
68
+ */
69
+ export function buildMirrorCamera(camera, planeY, out) {
70
+ const v = camera;
71
+ const h = planeY;
72
+ // view × R where R = reflectionAboutY(h). R only touches column 1 (scaled by
73
+ // -1) and adds 2h·col1 to the translation — write the product directly
74
+ // rather than through a generic multiply, so the arithmetic is exact and the
75
+ // cost is a handful of ops.
76
+ for (let i = 0; i < 16; i++)
77
+ out[i] = v[i];
78
+ out[4] = -v[4];
79
+ out[5] = -v[5];
80
+ out[6] = -v[6];
81
+ out[7] = -v[7];
82
+ out[12] = v[12] + 2 * h * v[4];
83
+ out[13] = v[13] + 2 * h * v[5];
84
+ out[14] = v[14] + 2 * h * v[6];
85
+ out[15] = v[15] + 2 * h * v[7];
86
+ for (let i = 16; i < 32; i++)
87
+ out[i] = v[i];
88
+ out[32] = v[32];
89
+ out[33] = 2 * h - v[33];
90
+ out[34] = v[34];
91
+ out[35] = v[35];
92
+ return out;
93
+ }
@@ -0,0 +1,56 @@
1
+ interface AnchorRequest {
2
+ bone: string;
3
+ trail: boolean;
4
+ }
5
+ /** The empty table — a scene with no effect installed asks for no bones. */
6
+ export declare const EMPTY_ANCHOR_TABLE: AnchorTable;
7
+ export interface AnchorTable {
8
+ /** The scene's bones, deduplicated, in allocation order. Storage addresses. */
9
+ entries: AnchorRequest[];
10
+ /** Per effect, local slot → global slot; -1 for a request the cap refused. */
11
+ alias: number[][];
12
+ /** What the cap refused, so an install can say so instead of going quiet. */
13
+ dropped: {
14
+ effect: number;
15
+ bone: string;
16
+ }[];
17
+ }
18
+ /**
19
+ * Allocate the scene's anchors from what each effect asked for.
20
+ *
21
+ * Deduplicated by BONE, not by (bone, trail): a request for a trail and a
22
+ * request for the bare position are the same bone, and one entry with the trail
23
+ * turned on satisfies both. Keying on the pair would spend two of eight slots
24
+ * describing one wrist.
25
+ *
26
+ * Order is first-come, so a single effect gets the identity alias and the whole
27
+ * mechanism is a no-op until a second effect exists — which is what makes this
28
+ * safe to land before setEffects does.
29
+ */
30
+ export declare function buildAnchorTable(requests: AnchorRequest[][], max: number): AnchorTable;
31
+ /**
32
+ * Ribbon index → LOCAL anchor slot, for the trail draw.
33
+ *
34
+ * A THIRD index space, and the one that bit. The trail pass draws one ribbon per
35
+ * TRAILED anchor, so its instance index counts 0,1,2… over trailed anchors only
36
+ * — while the cast buffer is addressed by DECLARATION slot. Those coincide
37
+ * exactly when every anchor is trailed, which is true of all 14 library effects
38
+ * and is why this stayed latent: declare `@anchor 頭` then `@anchor 左手首 trail`
39
+ * and ribbon 0 asked for the trail of 頭, which has none, so the ribbon silently
40
+ * did not draw.
41
+ *
42
+ * Feeding this into _rzSlot afterwards is what makes the chain complete:
43
+ * ribbon → local slot → scene slot.
44
+ */
45
+ export declare function ribbonSlotWgsl(localSlots: number[]): string;
46
+ /**
47
+ * The alias as WGSL, spliced into each effect's module.
48
+ *
49
+ * A switch rather than an array because a const array indexed by a runtime value
50
+ * lowers badly on the Metal backend — the same reason the filmic curve became a
51
+ * texture. With one effect this compiles to `return local`, which every backend
52
+ * folds away.
53
+ */
54
+ export declare function anchorAliasWgsl(alias: number[]): string;
55
+ export {};
56
+ //# sourceMappingURL=anchor-table.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anchor-table.d.ts","sourceRoot":"","sources":["../../src/shaders/anchor-table.ts"],"names":[],"mappings":"AAsBA,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;CACf;AAED,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,EAAE,WAAqD,CAAA;AAEtF,MAAM,WAAW,WAAW;IAC1B,+EAA+E;IAC/E,OAAO,EAAE,aAAa,EAAE,CAAA;IACxB,8EAA8E;IAC9E,KAAK,EAAE,MAAM,EAAE,EAAE,CAAA;IACjB,6EAA6E;IAC7E,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC5C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,CAiCtF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAkB3D;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAkBvD"}
@@ -0,0 +1,128 @@
1
+ // The scene's anchor table: one deduplicated set of bones for every effect in
2
+ // the scene, plus the per-effect alias that maps an author's slot onto it.
3
+ //
4
+ // THE PROBLEM IT SOLVES. A slot number is currently two things at once: the
5
+ // author's name for a bone, and its storage address in the cast buffer. Those
6
+ // coincide only while one effect exists. Install two and slot 0 means a
7
+ // different bone to each of them, so whichever wrote last wins and the other
8
+ // silently reads someone else's hand.
9
+ //
10
+ // The fix is to stop conflating them. Bones are allocated ONCE for the scene,
11
+ // deduplicated, and each effect is spliced a private `_rzSlot(local) -> global`
12
+ // that the accessors route through. Author source is untouched — a published
13
+ // effect keeps compiling and keeps its line numbers, which matters because line
14
+ // numbers are what an author debugs against.
15
+ //
16
+ // Three things fall out of it beyond fixing the clash:
17
+ // · two effects wanting the same trail SHARE one ring, and trails are the
18
+ // expensive resource — 128 samples × 4 subjects each;
19
+ // · the cap becomes 8 distinct bones per SCENE rather than per file;
20
+ // · it is the same indirection the skeleton data interface needs later, so
21
+ // this is a bridge rather than a detour.
22
+ /** The empty table — a scene with no effect installed asks for no bones. */
23
+ export const EMPTY_ANCHOR_TABLE = { entries: [], alias: [], dropped: [] };
24
+ /**
25
+ * Allocate the scene's anchors from what each effect asked for.
26
+ *
27
+ * Deduplicated by BONE, not by (bone, trail): a request for a trail and a
28
+ * request for the bare position are the same bone, and one entry with the trail
29
+ * turned on satisfies both. Keying on the pair would spend two of eight slots
30
+ * describing one wrist.
31
+ *
32
+ * Order is first-come, so a single effect gets the identity alias and the whole
33
+ * mechanism is a no-op until a second effect exists — which is what makes this
34
+ * safe to land before setEffects does.
35
+ */
36
+ export function buildAnchorTable(requests, max) {
37
+ const entries = [];
38
+ const index = new Map();
39
+ const alias = [];
40
+ const dropped = [];
41
+ for (let e = 0; e < requests.length; e++) {
42
+ const local = [];
43
+ for (const req of requests[e]) {
44
+ let g = index.get(req.bone);
45
+ if (g === undefined) {
46
+ if (entries.length >= max) {
47
+ // Refused, and the effect still installs: an effect that loses one of
48
+ // its anchors draws that one wrong, where refusing the install would
49
+ // lose the whole scene's visuals over a bone.
50
+ dropped.push({ effect: e, bone: req.bone });
51
+ local.push(-1);
52
+ continue;
53
+ }
54
+ g = entries.length;
55
+ index.set(req.bone, g);
56
+ entries.push({ bone: req.bone, trail: req.trail });
57
+ }
58
+ else if (req.trail) {
59
+ // A later request for the same bone can only ever ADD the trail — the
60
+ // ring is shared, and turning it on for one reader turns it on for all.
61
+ entries[g].trail = true;
62
+ }
63
+ local.push(g);
64
+ }
65
+ alias.push(local);
66
+ }
67
+ return { entries, alias, dropped };
68
+ }
69
+ /**
70
+ * Ribbon index → LOCAL anchor slot, for the trail draw.
71
+ *
72
+ * A THIRD index space, and the one that bit. The trail pass draws one ribbon per
73
+ * TRAILED anchor, so its instance index counts 0,1,2… over trailed anchors only
74
+ * — while the cast buffer is addressed by DECLARATION slot. Those coincide
75
+ * exactly when every anchor is trailed, which is true of all 14 library effects
76
+ * and is why this stayed latent: declare `@anchor 頭` then `@anchor 左手首 trail`
77
+ * and ribbon 0 asked for the trail of 頭, which has none, so the ribbon silently
78
+ * did not draw.
79
+ *
80
+ * Feeding this into _rzSlot afterwards is what makes the chain complete:
81
+ * ribbon → local slot → scene slot.
82
+ */
83
+ export function ribbonSlotWgsl(localSlots) {
84
+ const identity = localSlots.every((s, i) => s === i);
85
+ if (identity) {
86
+ return `
87
+ /** Ribbon index → local anchor slot. Identity: every anchor is trailed. */
88
+ fn _rzRibbonSlot(ribbon: i32) -> i32 { return ribbon; }
89
+ `;
90
+ }
91
+ const cases = localSlots.map((s, i) => ` case ${i}: { return ${s}; }`).join("\n");
92
+ return `
93
+ /** Ribbon index → local anchor slot, skipping the anchors with no trail. */
94
+ fn _rzRibbonSlot(ribbon: i32) -> i32 {
95
+ switch ribbon {
96
+ ${cases}
97
+ default: { return -1; }
98
+ }
99
+ }
100
+ `;
101
+ }
102
+ /**
103
+ * The alias as WGSL, spliced into each effect's module.
104
+ *
105
+ * A switch rather than an array because a const array indexed by a runtime value
106
+ * lowers badly on the Metal backend — the same reason the filmic curve became a
107
+ * texture. With one effect this compiles to `return local`, which every backend
108
+ * folds away.
109
+ */
110
+ export function anchorAliasWgsl(alias) {
111
+ const identity = alias.every((g, i) => g === i);
112
+ if (identity) {
113
+ return `
114
+ /** Local slot → scene slot. Identity here: this effect owns the table. */
115
+ fn _rzSlot(local: i32) -> i32 { return local; }
116
+ `;
117
+ }
118
+ const cases = alias.map((g, i) => ` case ${i}: { return ${g}; }`).join("\n");
119
+ return `
120
+ /** Local slot → scene slot, from the deduplicated table this effect shares. */
121
+ fn _rzSlot(local: i32) -> i32 {
122
+ switch local {
123
+ ${cases}
124
+ default: { return -1; }
125
+ }
126
+ }
127
+ `;
128
+ }
@@ -0,0 +1,2 @@
1
+ export declare const CAST_API = "\nconst RZ_SUBJECTS: i32 = 4;\nconst RZ_SAMPLES: i32 = 128;\n/** The anchor ADDRESS SPACE \u2014 how many an effect may declare, not how many it\n * did. RZ_TRAIL_SLOTS is the per-effect number and is not this one; the two\n * being one number was the old trail bug. */\nconst RZ_MAX_ANCHORS: i32 = 8;\nconst RZ_TRAIL_SAMPLES: i32 = 128;\n\nstruct RzSubject {\n /** On the FLOOR, under the body \u2014 where a ring or a magic circle belongs. */\n root: vec3f,\n /** At the hips, the middle of the body \u2014 where an aura belongs. */\n center: vec3f,\n /** Bounding sphere: xyz centre, w radius. Deliberately generous \u2014 cull with it. */\n bounds: vec4f,\n /** False past the end of the cast, and every field is then zero. */\n valid: bool,\n}\n\nstruct RzAnchor {\n pos: vec3f,\n /** World units per second, from the previous frame. Direction for a trail,\n * magnitude for anything that should react to how hard someone is moving. */\n vel: vec3f,\n /** The bone's forward axis \u2014 which way a foot points, where a head looks. */\n fwd: vec3f,\n /** False when this rig has no such bone. Check it: the alternative is drawing\n * a hand effect at the world origin on every model that spells it differently. */\n valid: bool,\n}\n\n/** Which model this is, stable across a scene \u2014 for per-subject variation. */\nfn rzSubjectId(i: i32) -> u32 {\n if (i < 0 || i >= rzSubjectCount()) { return 0u; }\n return u32(_rzCast[i * 3 + 1].w);\n}\n\nfn rzSubject(i: i32) -> RzSubject {\n var s: RzSubject;\n s.valid = i >= 0 && i < rzSubjectCount();\n if (!s.valid) { return s; }\n let b = i * 3;\n s.root = _rzCast[b].xyz;\n s.center = _rzCast[b + 1].xyz;\n s.bounds = _rzCast[b + 2];\n return s;\n}\n\n/**\n * Where a named bone is, this frame.\n *\n * The slot is the author's own: the Nth @anchor in their file, in the order\n * they wrote them. _rzSlot turns that into the scene's address, which is what\n * keeps two effects that both anchor to a wrist from reading each other's.\n */\nfn rzAnchor(subject: i32, slot: i32) -> RzAnchor {\n var a: RzAnchor;\n a.valid = false;\n let g = _rzSlot(slot);\n if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }\n let b = 12 + (g * 4 + subject) * 3;\n a.valid = _rzCast[b].w > 0.5;\n a.pos = _rzCast[b].xyz;\n a.vel = _rzCast[b + 1].xyz;\n a.fwd = _rzCast[b + 2].xyz;\n return a;\n}\n\n/**\n * How many samples of a path are recorded \u2014 0 for an anchor that asked for no\n * trail, and for one that has not moved yet.\n *\n * Bounded by the anchor cap, NOT by how many anchors asked for a trail. Those\n * are different index spaces: storage is addressed by anchor slot, so an\n * untrailed @anchor followed by a trailed one put the trail at index 1 with a\n * bound of 1 and rzTrail returned zero \u2014 a ribbon that silently did not draw.\n */\nfn rzTrailCount(subject: i32, slot: i32) -> i32 {\n let g = _rzSlot(slot);\n if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }\n return i32(_rzCast[12 + (g * 4 + subject) * 3 + 2].w);\n}\n\n/** Sample i of a path: xyz where it was, w how many seconds ago. i = 0 is now. */\nfn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {\n let n = rzTrailCount(subject, slot);\n if (i < 0 || i >= n) { return vec4f(0.0); }\n let base = 108 + (_rzSlot(slot) * 4 + subject) * RZ_TRAIL_SAMPLES;\n return _rzCast[base + i];\n}\n";
2
+ //# sourceMappingURL=cast-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cast-api.d.ts","sourceRoot":"","sources":["../../src/shaders/cast-api.ts"],"names":[],"mappings":"AA+BA,eAAO,MAAM,QAAQ,k4GA2FpB,CAAA"}
@@ -0,0 +1,121 @@
1
+ // The cast, as data — the sibling of the audio and score interfaces, and shaped
2
+ // like them: one shared buffer, read through accessors, never touched directly.
3
+ //
4
+ // WHY THIS FILE EXISTS. There were two of these. The field, grid and lightEmit
5
+ // modules read the cast through one implementation; the particle and trail
6
+ // modules read the SAME BUFFER through another, written separately, and the
7
+ // particle one had no rzAnchor at all — so a particle effect could ask where a
8
+ // trail had been but not where a wrist is. Neither was wrong; they had simply
9
+ // never been the same code, and the split was invisible until an effect used a
10
+ // mount from each family and its own file stopped compiling in one of them.
11
+ //
12
+ // The two differed only in how the layout reached them: one baked the engine's
13
+ // constants, the other took them as a CastLayout. Every caller of that layout
14
+ // passed the same five constants, so the parameterisation described a freedom
15
+ // that did not exist. Baking them makes this a constant string, which is what
16
+ // lets both families share it without either one deciding the shape.
17
+ //
18
+ // WHAT A HOST MUST SUPPLY. Three names, and deliberately only three:
19
+ //
20
+ // _rzCast the buffer, at whatever binding the module puts it on
21
+ // _rzSlot(i) the effect's local slot → the scene's, from its alias
22
+ // rzSubjectCount() how many subjects are live
23
+ //
24
+ // The last is a host's because the two families genuinely disagree on it: the
25
+ // field module reads a count the engine wrote into the view uniform, and the
26
+ // particle module scans the buffer, because it has no view uniform to read.
27
+ // They agree in value. Unifying them would be a behaviour change to every
28
+ // shipped effect for no gain, so the seam stays and is named here instead.
29
+ import { EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES } from "./cast-layout";
30
+ export const CAST_API = /* wgsl */ `
31
+ const RZ_SUBJECTS: i32 = ${EFFECT_SUBJECTS};
32
+ const RZ_SAMPLES: i32 = ${EFFECT_TRAIL_SAMPLES};
33
+ /** The anchor ADDRESS SPACE — how many an effect may declare, not how many it
34
+ * did. RZ_TRAIL_SLOTS is the per-effect number and is not this one; the two
35
+ * being one number was the old trail bug. */
36
+ const RZ_MAX_ANCHORS: i32 = ${EFFECT_ANCHORS};
37
+ const RZ_TRAIL_SAMPLES: i32 = ${EFFECT_TRAIL_SAMPLES};
38
+
39
+ struct RzSubject {
40
+ /** On the FLOOR, under the body — where a ring or a magic circle belongs. */
41
+ root: vec3f,
42
+ /** At the hips, the middle of the body — where an aura belongs. */
43
+ center: vec3f,
44
+ /** Bounding sphere: xyz centre, w radius. Deliberately generous — cull with it. */
45
+ bounds: vec4f,
46
+ /** False past the end of the cast, and every field is then zero. */
47
+ valid: bool,
48
+ }
49
+
50
+ struct RzAnchor {
51
+ pos: vec3f,
52
+ /** World units per second, from the previous frame. Direction for a trail,
53
+ * magnitude for anything that should react to how hard someone is moving. */
54
+ vel: vec3f,
55
+ /** The bone's forward axis — which way a foot points, where a head looks. */
56
+ fwd: vec3f,
57
+ /** False when this rig has no such bone. Check it: the alternative is drawing
58
+ * a hand effect at the world origin on every model that spells it differently. */
59
+ valid: bool,
60
+ }
61
+
62
+ /** Which model this is, stable across a scene — for per-subject variation. */
63
+ fn rzSubjectId(i: i32) -> u32 {
64
+ if (i < 0 || i >= rzSubjectCount()) { return 0u; }
65
+ return u32(_rzCast[i * 3 + 1].w);
66
+ }
67
+
68
+ fn rzSubject(i: i32) -> RzSubject {
69
+ var s: RzSubject;
70
+ s.valid = i >= 0 && i < rzSubjectCount();
71
+ if (!s.valid) { return s; }
72
+ let b = i * 3;
73
+ s.root = _rzCast[b].xyz;
74
+ s.center = _rzCast[b + 1].xyz;
75
+ s.bounds = _rzCast[b + 2];
76
+ return s;
77
+ }
78
+
79
+ /**
80
+ * Where a named bone is, this frame.
81
+ *
82
+ * The slot is the author's own: the Nth @anchor in their file, in the order
83
+ * they wrote them. _rzSlot turns that into the scene's address, which is what
84
+ * keeps two effects that both anchor to a wrist from reading each other's.
85
+ */
86
+ fn rzAnchor(subject: i32, slot: i32) -> RzAnchor {
87
+ var a: RzAnchor;
88
+ a.valid = false;
89
+ let g = _rzSlot(slot);
90
+ if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }
91
+ let b = ${EFFECT_SUBJECTS * 3} + (g * ${EFFECT_SUBJECTS} + subject) * 3;
92
+ a.valid = _rzCast[b].w > 0.5;
93
+ a.pos = _rzCast[b].xyz;
94
+ a.vel = _rzCast[b + 1].xyz;
95
+ a.fwd = _rzCast[b + 2].xyz;
96
+ return a;
97
+ }
98
+
99
+ /**
100
+ * How many samples of a path are recorded — 0 for an anchor that asked for no
101
+ * trail, and for one that has not moved yet.
102
+ *
103
+ * Bounded by the anchor cap, NOT by how many anchors asked for a trail. Those
104
+ * are different index spaces: storage is addressed by anchor slot, so an
105
+ * untrailed @anchor followed by a trailed one put the trail at index 1 with a
106
+ * bound of 1 and rzTrail returned zero — a ribbon that silently did not draw.
107
+ */
108
+ fn rzTrailCount(subject: i32, slot: i32) -> i32 {
109
+ let g = _rzSlot(slot);
110
+ if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }
111
+ return i32(_rzCast[${EFFECT_SUBJECTS * 3} + (g * ${EFFECT_SUBJECTS} + subject) * 3 + 2].w);
112
+ }
113
+
114
+ /** Sample i of a path: xyz where it was, w how many seconds ago. i = 0 is now. */
115
+ fn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {
116
+ let n = rzTrailCount(subject, slot);
117
+ if (i < 0 || i >= n) { return vec4f(0.0); }
118
+ let base = ${EFFECT_TRAIL_BASE} + (_rzSlot(slot) * ${EFFECT_SUBJECTS} + subject) * RZ_TRAIL_SAMPLES;
119
+ return _rzCast[base + i];
120
+ }
121
+ `;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The caps the cast buffer is built to, shared by every shader that reads it and
3
+ * by the engine that fills it. Interpolated into the WGSL rather than written
4
+ * twice: the layout arithmetic on both sides has to agree exactly, and two
5
+ * literals that must match are two literals that eventually will not.
6
+ *
7
+ * All three are MINIMUMS. Raising one breaks nothing, because effects read
8
+ * through accessors and loop to the count functions; lowering one does.
9
+ *
10
+ * THEIR OWN FILE, and it imports nothing: cast-api.ts needs them to write the
11
+ * accessors, composite.ts needs them for the rest of its module, and cast-api
12
+ * is spliced into composite. Left where they were, that is an import cycle,
13
+ * and the first thing to touch the cycle throws ReferenceError at module load —
14
+ * the whole engine failing to start on an import order nobody chose.
15
+ */
16
+ export declare const EFFECT_SUBJECTS = 4;
17
+ export declare const EFFECT_ANCHORS = 8;
18
+ export declare const EFFECT_TRAIL_SAMPLES = 128;
19
+ /** vec4 slot where the trails begin — after the subjects and the anchors. */
20
+ export declare const EFFECT_TRAIL_BASE: number;
21
+ //# sourceMappingURL=cast-layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cast-layout.d.ts","sourceRoot":"","sources":["../../src/shaders/cast-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,IAAI,CAAA;AAChC,eAAO,MAAM,cAAc,IAAI,CAAA;AAC/B,eAAO,MAAM,oBAAoB,MAAM,CAAA;AACvC,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,QAA6D,CAAA"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The caps the cast buffer is built to, shared by every shader that reads it and
3
+ * by the engine that fills it. Interpolated into the WGSL rather than written
4
+ * twice: the layout arithmetic on both sides has to agree exactly, and two
5
+ * literals that must match are two literals that eventually will not.
6
+ *
7
+ * All three are MINIMUMS. Raising one breaks nothing, because effects read
8
+ * through accessors and loop to the count functions; lowering one does.
9
+ *
10
+ * THEIR OWN FILE, and it imports nothing: cast-api.ts needs them to write the
11
+ * accessors, composite.ts needs them for the rest of its module, and cast-api
12
+ * is spliced into composite. Left where they were, that is an import cycle,
13
+ * and the first thing to touch the cycle throws ReferenceError at module load —
14
+ * the whole engine failing to start on an import order nobody chose.
15
+ */
16
+ export const EFFECT_SUBJECTS = 4;
17
+ export const EFFECT_ANCHORS = 8;
18
+ export const EFFECT_TRAIL_SAMPLES = 128;
19
+ /** vec4 slot where the trails begin — after the subjects and the anchors. */
20
+ export const EFFECT_TRAIL_BASE = EFFECT_SUBJECTS * 3 + EFFECT_ANCHORS * EFFECT_SUBJECTS * 3;
@@ -0,0 +1,79 @@
1
+ /** Floats before the first record. One is the count; the rest keep the records
2
+ * vec4-aligned, which is what lets a future pass read them as vec4s. */
3
+ export declare const LIGHT_HEADER = 4;
4
+ /** Floats per light — see the layout above. */
5
+ export declare const LIGHT_STRIDE = 8;
6
+ /**
7
+ * The cap, and it is a real one: the loop below runs per fragment, so this is
8
+ * the number that decides whether lights are free or a cost. Sixteen is the
9
+ * bounded middle tier the design settled on — enough for a stage rig, far below
10
+ * the point where clustering would start to pay for itself.
11
+ */
12
+ export declare const MAX_LIGHTS = 16;
13
+ /** Floats in the whole buffer. */
14
+ export declare const LIGHTS_FLOATS: number;
15
+ /**
16
+ * `// @lights 3` — how many lights this effect emits.
17
+ *
18
+ * Declared, like every other mount: what the file says is what gets allocated,
19
+ * so an effect that emits none costs no slots and nobody pays for a cap they
20
+ * did not ask for. Clamped rather than rejected, the same choice `@particles`
21
+ * makes — an author asking for a hundred gets the most the engine will give and
22
+ * a scene that still runs.
23
+ */
24
+ export declare function parseLightCount(wgsl: string, max: number): number;
25
+ /**
26
+ * The RzLight struct, declared in EVERY module a user's source is spliced into.
27
+ *
28
+ * One effect file goes into every module it has a mount in, so a foreground
29
+ * effect that also emits lights compiles its lightEmit inside the FIELD shader
30
+ * too — where nothing calls it, but it still has to resolve. Leaving the struct
31
+ * out of those modules is a compile error on a function the author was right to
32
+ * write, which is the same trap the grid's step-only half documents.
33
+ */
34
+ export declare const RZ_LIGHT_STRUCT_WGSL = "\n/** What an effect returns for one of its lights. */\nstruct RzLight {\n pos: vec3f,\n color: vec3f,\n intensity: f32,\n radius: f32,\n}\n";
35
+ /**
36
+ * The world's light at a surface facing n — the flat colour, or the installed
37
+ * HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
38
+ * ibl.ts for the folding; the shader is a plain polynomial in the normal).
39
+ *
40
+ * One string included by every module that declares LightUniforms with the sh
41
+ * block — the hosted-api lesson: a helper defined in some modules and not
42
+ * others is a compile error waiting for the first file that crosses them.
43
+ */
44
+ export declare const WORLD_AMBIENT_WGSL = "\nfn rzWorldAmbient(n: vec3f) -> vec3f {\n if (light.sh[0].w < 0.5) { return light.ambientColor.xyz; }\n let x = n.x;\n let y = n.y;\n let z = n.z;\n let c = light.sh[0].xyz\n + light.sh[1].xyz * y + light.sh[2].xyz * z + light.sh[3].xyz * x\n + light.sh[4].xyz * (x * y) + light.sh[5].xyz * (y * z)\n + light.sh[6].xyz * (3.0 * z * z - 1.0) + light.sh[7].xyz * (x * z)\n + light.sh[8].xyz * (x * x - y * y);\n return max(c, vec3f(0.0));\n}\n";
45
+ /** Does this source define the emit mount? */
46
+ export declare function hasLightEmit(wgsl: string): boolean;
47
+ /**
48
+ * The compute module that runs an effect's lightEmit once per light per frame.
49
+ *
50
+ * fn lightEmit(i: u32, time: f32) -> RzLight
51
+ *
52
+ * A COMPUTE stage rather than a CPU callback, and that is the whole point:
53
+ * Fireworks knows where its bursts are as a closed form in WGSL, and mirroring
54
+ * that on the CPU to place a light would be two derivations of one trajectory
55
+ * that drift apart. Emitting in the shader means the light is wherever the
56
+ * effect says it is, on the scene clock — which is also what makes it survive
57
+ * an offline export frame-stepped at a different rate.
58
+ *
59
+ * The author writes local index 0..n-1 and never learns the global one, the
60
+ * same aliasing the anchor table uses — so installing another effect ahead of
61
+ * this one moves its lights without touching its source.
62
+ *
63
+ * The base arrives in the UNIFORM rather than baked into the text. Baking it
64
+ * would mean recompiling every emitting effect the moment a scene gained or
65
+ * lost a document light, because that is what shifts the slots underneath
66
+ * them — a shader rebuild triggered by moving a lamp.
67
+ *
68
+ * The scene API arrives as a STRING rather than being imported, so this module
69
+ * depends on nothing that depends on it. That API is what lets a lamp aim at
70
+ * someone: Stage Lights points its beams at rzSubject().root, and a light that
71
+ * did not know where she was could only sit where the fixture hangs. It also
72
+ * brings RzLight, which is why this builder does not declare it again.
73
+ */
74
+ export declare function buildLightEmitShader(wgsl: string, sceneApi: string, cast: {
75
+ trailCount: number;
76
+ }): string;
77
+ /** The rz*Light accessors, with the buffer declared at the given binding. */
78
+ export declare function lightsApi(group: number, binding: number): string;
79
+ //# sourceMappingURL=lights.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../src/shaders/lights.ts"],"names":[],"mappings":"AAgCA;yEACyE;AACzE,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,+CAA+C;AAC/C,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B;;;;;GAKG;AACH,eAAO,MAAM,UAAU,KAAK,CAAA;AAC5B,kCAAkC;AAClC,eAAO,MAAM,aAAa,QAA2C,CAAA;AAErE;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIjE;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,qJAQhC,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,idAa9B,CAAA;AAED,8CAA8C;AAC9C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC3B,MAAM,CAqER;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAkEhE"}