reze-engine 0.57.0 → 0.57.2

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.
@@ -1,2 +1,17 @@
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 = 16;\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 /**\n * How much of this character is still THERE: 1 whole, 0 gone.\n *\n * What setModelDissolve last set on them, which the material pass has already\n * acted on by the time an effect runs \u2014 so an effect drawing what LEAVES a\n * dissolving body (sparks, ash, a ghost) reads the same number the body was\n * taken apart with, rather than keeping a clock of its own and hoping the two\n * agree. 1 on a scene that never dissolves anybody.\n */\n dissolve: f32,\n /**\n * Where she LOOKS: a unit direction from between the eyes, in world space.\n * Exact while her eyes are on something (Engine.setEyeTracking); otherwise\n * the motion's own eye turn undone through the range map \u2014 an eye bone\n * turns a fraction of the way toward what it looks at, and this gives back\n * the whole of it. An effect that should go where the look goes \u2014 a beam,\n * a light \u2014 takes this rather than an eye bone's axis, and it holds in\n * either mode. Zero, with looking false, only for a model without eyes.\n */\n gaze: vec3f,\n looking: bool,\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 * 4 + 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 * 4;\n s.root = _rzCast[b].xyz;\n s.dissolve = _rzCast[b].w;\n s.center = _rzCast[b + 1].xyz;\n s.bounds = _rzCast[b + 2];\n s.gaze = _rzCast[b + 3].xyz;\n s.looking = _rzCast[b + 3].w > 0.5;\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 = 16 + (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[16 + (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 = 208 + (_rzSlot(slot) * 4 + subject) * RZ_TRAIL_SAMPLES;\n return _rzCast[base + i];\n}\n";
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 = 16;\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 /**\n * How much of this character is still THERE: 1 whole, 0 gone.\n *\n * What setModelDissolve last set on them, which the material pass has already\n * acted on by the time an effect runs \u2014 so an effect drawing what LEAVES a\n * dissolving body (sparks, ash, a ghost) reads the same number the body was\n * taken apart with, rather than keeping a clock of its own and hoping the two\n * agree. 1 on a scene that never dissolves anybody.\n */\n dissolve: f32,\n /**\n * Where she LOOKS: a unit direction from between the eyes, in world space.\n * Exact while her eyes are on something (Engine.setEyeTracking); otherwise\n * the motion's own eye turn undone through the range map \u2014 an eye bone\n * turns a fraction of the way toward what it looks at, and this gives back\n * the whole of it. An effect that should go where the look goes \u2014 a beam,\n * a light \u2014 takes this rather than an eye bone's axis, and it holds in\n * either mode. Zero, with looking false, only for a model without eyes.\n */\n gaze: vec3f,\n looking: bool,\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/**\n * How many characters THIS EFFECT is on, up to four.\n *\n * Not how many the scene holds. An effect applies to the whole cast unless the\n * scene says otherwise (Engine.setEffectSubjects), and where it says otherwise\n * this counts only the models named \u2014 so an effect aimed at one dancer sees one\n * subject, at index 0, and an author who wrote a loop over the cast gets the\n * targeting for free.\n *\n * Which is the whole design: the FILTER IS IN THE INDEX SPACE, not in the\n * shaders. There is no mask an effect can read, because an effect asking \"am I\n * allowed on this one\" is an effect that can get the answer wrong, and 18 of the\n * shipped built-ins would have had to be rewritten to ask at all.\n */\nfn rzSubjectCount() -> i32 {\n let live = _rzCastLive();\n let mask = _rzSubjectMask();\n var n = 0;\n for (var i = 0; i < live; i++) {\n if ((mask & (1u << u32(i))) != 0u) { n++; }\n }\n return n;\n}\n\n/** This effect's i-th subject \u2192 the scene's slot, or -1 past its own count.\n * Every accessor below goes through it, which is what keeps one effect's\n * subject 1 from being another's. */\nfn _rzSubjectSlot(i: i32) -> i32 {\n if (i < 0) { return -1; }\n let live = _rzCastLive();\n let mask = _rzSubjectMask();\n var n = 0;\n for (var s = 0; s < live; s++) {\n if ((mask & (1u << u32(s))) == 0u) { continue; }\n if (n == i) { return s; }\n n++;\n }\n return -1;\n}\n\n/** Which model this is, stable across a scene \u2014 for per-subject variation. */\nfn rzSubjectId(i: i32) -> u32 {\n let g = _rzSubjectSlot(i);\n if (g < 0) { return 0u; }\n return u32(_rzCast[g * 4 + 1].w);\n}\n\nfn rzSubject(i: i32) -> RzSubject {\n var s: RzSubject;\n let g = _rzSubjectSlot(i);\n s.valid = g >= 0;\n if (!s.valid) { return s; }\n let b = g * 4;\n s.root = _rzCast[b].xyz;\n s.dissolve = _rzCast[b].w;\n s.center = _rzCast[b + 1].xyz;\n s.bounds = _rzCast[b + 2];\n s.gaze = _rzCast[b + 3].xyz;\n s.looking = _rzCast[b + 3].w > 0.5;\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 let s = _rzSubjectSlot(subject);\n if (s < 0 || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }\n let b = 16 + (g * 4 + s) * 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 let s = _rzSubjectSlot(subject);\n if (s < 0 || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }\n return i32(_rzCast[16 + (g * 4 + s) * 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 = 208 + (_rzSlot(slot) * 4 + _rzSubjectSlot(subject)) * RZ_TRAIL_SAMPLES;\n return _rzCast[base + i];\n}\n";
2
+ /**
3
+ * Which subjects this effect applies to, as WGSL — the second of the two names a
4
+ * mount owes CAST_API (`_rzCastLive` is the first, and a mount with a view
5
+ * uniform already has it from EFFECT_SCENE_API).
6
+ *
7
+ * One bit per cast slot. An EXPRESSION rather than a value: it resolves per
8
+ * draw, out of a uniform the engine writes once a frame, so a module that baked
9
+ * it would stop following a target being changed or a model being hidden.
10
+ */
11
+ export declare const subjectMaskApi: (mask: string) => string;
12
+ /** Every subject the scene has. For a mount that is not an effect — the
13
+ * composite's own module, where nothing user-written lands and the accessors
14
+ * exist only so the shared API compiles — and the value the engine writes for
15
+ * an effect the scene has not aimed at anybody in particular. */
16
+ export declare const CAST_MASK_ALL = "0xfu";
2
17
  //# sourceMappingURL=cast-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cast-api.d.ts","sourceRoot":"","sources":["../../src/shaders/cast-api.ts"],"names":[],"mappings":"AA+BA,eAAO,MAAM,QAAQ,4iJAmHpB,CAAA"}
1
+ {"version":3,"file":"cast-api.d.ts","sourceRoot":"","sources":["../../src/shaders/cast-api.ts"],"names":[],"mappings":"AAoCA,eAAO,MAAM,QAAQ,29LA+JpB,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,MAE7C,CAAA;AAED;;;kEAGkE;AAClE,eAAO,MAAM,aAAa,SAAS,CAAA"}
@@ -15,17 +15,22 @@
15
15
  // that did not exist. Baking them makes this a constant string, which is what
16
16
  // lets both families share it without either one deciding the shape.
17
17
  //
18
- // WHAT A HOST MUST SUPPLY. Three names, and deliberately only three:
18
+ // WHAT A HOST MUST SUPPLY. Four names, and deliberately only four:
19
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
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
+ // _rzCastLive() how many subjects the SCENE holds
23
+ // _rzSubjectMask() which of them THIS EFFECT applies to, one bit per slot
23
24
  //
24
- // The last is a host's because the two families genuinely disagree on it: the
25
+ // The third is a host's because the two families genuinely disagree on it: the
25
26
  // field module reads a count the engine wrote into the view uniform, and the
26
27
  // particle module scans the buffer, because it has no view uniform to read.
27
28
  // They agree in value. Unifying them would be a behaviour change to every
28
29
  // shipped effect for no gain, so the seam stays and is named here instead.
30
+ //
31
+ // The fourth is a host's because every mount carries it in a uniform of its own
32
+ // — the field clock's z, the particle and trail pads, the grid's and the light
33
+ // emitter's blocks — and each of those is written once a frame by the engine.
29
34
  import { EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES, EFFECT_SUBJECT_VEC4S } from "./cast-layout";
30
35
  export const CAST_API = /* wgsl */ `
31
36
  const RZ_SUBJECTS: i32 = ${EFFECT_SUBJECTS};
@@ -80,17 +85,59 @@ struct RzAnchor {
80
85
  valid: bool,
81
86
  }
82
87
 
88
+ /**
89
+ * How many characters THIS EFFECT is on, up to four.
90
+ *
91
+ * Not how many the scene holds. An effect applies to the whole cast unless the
92
+ * scene says otherwise (Engine.setEffectSubjects), and where it says otherwise
93
+ * this counts only the models named — so an effect aimed at one dancer sees one
94
+ * subject, at index 0, and an author who wrote a loop over the cast gets the
95
+ * targeting for free.
96
+ *
97
+ * Which is the whole design: the FILTER IS IN THE INDEX SPACE, not in the
98
+ * shaders. There is no mask an effect can read, because an effect asking "am I
99
+ * allowed on this one" is an effect that can get the answer wrong, and 18 of the
100
+ * shipped built-ins would have had to be rewritten to ask at all.
101
+ */
102
+ fn rzSubjectCount() -> i32 {
103
+ let live = _rzCastLive();
104
+ let mask = _rzSubjectMask();
105
+ var n = 0;
106
+ for (var i = 0; i < live; i++) {
107
+ if ((mask & (1u << u32(i))) != 0u) { n++; }
108
+ }
109
+ return n;
110
+ }
111
+
112
+ /** This effect's i-th subject → the scene's slot, or -1 past its own count.
113
+ * Every accessor below goes through it, which is what keeps one effect's
114
+ * subject 1 from being another's. */
115
+ fn _rzSubjectSlot(i: i32) -> i32 {
116
+ if (i < 0) { return -1; }
117
+ let live = _rzCastLive();
118
+ let mask = _rzSubjectMask();
119
+ var n = 0;
120
+ for (var s = 0; s < live; s++) {
121
+ if ((mask & (1u << u32(s))) == 0u) { continue; }
122
+ if (n == i) { return s; }
123
+ n++;
124
+ }
125
+ return -1;
126
+ }
127
+
83
128
  /** Which model this is, stable across a scene — for per-subject variation. */
84
129
  fn rzSubjectId(i: i32) -> u32 {
85
- if (i < 0 || i >= rzSubjectCount()) { return 0u; }
86
- return u32(_rzCast[i * ${EFFECT_SUBJECT_VEC4S} + 1].w);
130
+ let g = _rzSubjectSlot(i);
131
+ if (g < 0) { return 0u; }
132
+ return u32(_rzCast[g * ${EFFECT_SUBJECT_VEC4S} + 1].w);
87
133
  }
88
134
 
89
135
  fn rzSubject(i: i32) -> RzSubject {
90
136
  var s: RzSubject;
91
- s.valid = i >= 0 && i < rzSubjectCount();
137
+ let g = _rzSubjectSlot(i);
138
+ s.valid = g >= 0;
92
139
  if (!s.valid) { return s; }
93
- let b = i * ${EFFECT_SUBJECT_VEC4S};
140
+ let b = g * ${EFFECT_SUBJECT_VEC4S};
94
141
  s.root = _rzCast[b].xyz;
95
142
  s.dissolve = _rzCast[b].w;
96
143
  s.center = _rzCast[b + 1].xyz;
@@ -111,8 +158,9 @@ fn rzAnchor(subject: i32, slot: i32) -> RzAnchor {
111
158
  var a: RzAnchor;
112
159
  a.valid = false;
113
160
  let g = _rzSlot(slot);
114
- if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }
115
- let b = ${EFFECT_SUBJECTS * EFFECT_SUBJECT_VEC4S} + (g * ${EFFECT_SUBJECTS} + subject) * 3;
161
+ let s = _rzSubjectSlot(subject);
162
+ if (s < 0 || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }
163
+ let b = ${EFFECT_SUBJECTS * EFFECT_SUBJECT_VEC4S} + (g * ${EFFECT_SUBJECTS} + s) * 3;
116
164
  a.valid = _rzCast[b].w > 0.5;
117
165
  a.pos = _rzCast[b].xyz;
118
166
  a.vel = _rzCast[b + 1].xyz;
@@ -131,15 +179,33 @@ fn rzAnchor(subject: i32, slot: i32) -> RzAnchor {
131
179
  */
132
180
  fn rzTrailCount(subject: i32, slot: i32) -> i32 {
133
181
  let g = _rzSlot(slot);
134
- if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }
135
- return i32(_rzCast[${EFFECT_SUBJECTS * EFFECT_SUBJECT_VEC4S} + (g * ${EFFECT_SUBJECTS} + subject) * 3 + 2].w);
182
+ let s = _rzSubjectSlot(subject);
183
+ if (s < 0 || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }
184
+ return i32(_rzCast[${EFFECT_SUBJECTS * EFFECT_SUBJECT_VEC4S} + (g * ${EFFECT_SUBJECTS} + s) * 3 + 2].w);
136
185
  }
137
186
 
138
187
  /** Sample i of a path: xyz where it was, w how many seconds ago. i = 0 is now. */
139
188
  fn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {
140
189
  let n = rzTrailCount(subject, slot);
141
190
  if (i < 0 || i >= n) { return vec4f(0.0); }
142
- let base = ${EFFECT_TRAIL_BASE} + (_rzSlot(slot) * ${EFFECT_SUBJECTS} + subject) * RZ_TRAIL_SAMPLES;
191
+ let base = ${EFFECT_TRAIL_BASE} + (_rzSlot(slot) * ${EFFECT_SUBJECTS} + _rzSubjectSlot(subject)) * RZ_TRAIL_SAMPLES;
143
192
  return _rzCast[base + i];
144
193
  }
145
194
  `;
195
+ /**
196
+ * Which subjects this effect applies to, as WGSL — the second of the two names a
197
+ * mount owes CAST_API (`_rzCastLive` is the first, and a mount with a view
198
+ * uniform already has it from EFFECT_SCENE_API).
199
+ *
200
+ * One bit per cast slot. An EXPRESSION rather than a value: it resolves per
201
+ * draw, out of a uniform the engine writes once a frame, so a module that baked
202
+ * it would stop following a target being changed or a model being hidden.
203
+ */
204
+ export const subjectMaskApi = (mask) => /* wgsl */ `
205
+ fn _rzSubjectMask() -> u32 { return ${mask}; }
206
+ `;
207
+ /** Every subject the scene has. For a mount that is not an effect — the
208
+ * composite's own module, where nothing user-written lands and the accessors
209
+ * exist only so the shared API compiles — and the value the engine writes for
210
+ * an effect the scene has not aimed at anybody in particular. */
211
+ export const CAST_MASK_ALL = "0xfu";
@@ -1 +1 @@
1
- {"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../src/shaders/lights.ts"],"names":[],"mappings":"AAwCA;yEACyE;AACzE,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,+CAA+C;AAC/C,eAAO,MAAM,YAAY,KAAK,CAAA;AAC9B;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,UAAU,MAAM,CAAA;AAC7B,kCAAkC;AAClC,eAAO,MAAM,aAAa,QAA2C,CAAA;AAErE;;;;;;;;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,EAC5B,UAAU,SAAK,GACd,MAAM,CAiGR;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA0FhE"}
1
+ {"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../src/shaders/lights.ts"],"names":[],"mappings":"AAyCA;yEACyE;AACzE,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,+CAA+C;AAC/C,eAAO,MAAM,YAAY,KAAK,CAAA;AAC9B;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,UAAU,MAAM,CAAA;AAC7B,kCAAkC;AAClC,eAAO,MAAM,aAAa,QAA2C,CAAA;AAErE;;;;;;;;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,EAC5B,UAAU,SAAK,GACd,MAAM,CAoGR;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA0FhE"}
@@ -1,4 +1,5 @@
1
1
  import { SCENE_TAP_STUB } from "./scene-tap";
2
+ import { subjectMaskApi } from "./cast-api";
2
3
  // Positional lights, as data — the sibling of the cast, audio and score
3
4
  // interfaces, and shaped like them: one shared buffer, read through accessors,
4
5
  // never touched directly.
@@ -138,15 +139,18 @@ export function buildLightEmitShader(wgsl, sceneApi, cast, paramsDecl = "") {
138
139
  // read_write HERE and read-only in the material shaders. Different passes, so
139
140
  // the two never coexist: this compute runs before the scene pass that reads it.
140
141
  @group(0) @binding(0) var<storage, read_write> _rzLightsOut: array<f32>;
141
- // (time, base slot, count, weight) — see buildLightEmitShader on why the base
142
- // is here and not in the text.
143
- @group(0) @binding(1) var<uniform> _rzLightU: vec4f;
142
+ // [0] (time, base slot, count, weight) — see buildLightEmitShader on why the
143
+ // base is here and not in the text — and [1].x the subject mask, which is the
144
+ // only thing the second vec4 is for: an emitter reads the cast through the same
145
+ // accessors its drawing half does, so a rig aimed at one dancer must see one.
146
+ @group(0) @binding(1) var<uniform> _rzLightU: array<vec4f, 2>;
144
147
  // The camera block and the cast — the two buffers the scene API reads. Same
145
148
  // contents the field and grid modules bind, so an effect's lightEmit sees the
146
149
  // scene exactly as its drawing half does.
147
150
  @group(0) @binding(2) var<uniform> viewU: array<vec4<f32>, 15>;
148
151
  @group(0) @binding(3) var<storage, read> _rzCast: array<vec4f>;
149
152
  ${sceneApi}
153
+ ${subjectMaskApi("u32(_rzLightU[1].x)")}
150
154
  ${SCENE_TAP_STUB}
151
155
  // Audio and score at 4 and 5, the same bindings the particle and trail modules
152
156
  // put them on. A lamp that pulses on the beat or lights on a note is the whole
@@ -160,7 +164,7 @@ ${lyricsApi(0, 6)}
160
164
  // grows a lamp at its tip compiles its ribbon code in this module too. The math
161
165
  // helpers and the Particle struct are NOT repeated — they arrive with the scene
162
166
  // API above, and a second copy is a redefinition error in engine code.
163
- ${clockApi("_rzLightU.x", "0.0")}
167
+ ${clockApi("_rzLightU[0].x", "0.0")}
164
168
  // Canvas height — the same number the drawing modules read out of their camera
165
169
  // struct, both written from canvas.height, so this name means ONE value in
166
170
  // every module. Verified against the writers, not assumed: cameraMatrixData[35]
@@ -181,12 +185,12 @@ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
181
185
  let i = gid.x;
182
186
  // The dispatch is sized to the count, but a workgroup is 64 wide and the
183
187
  // count rarely is — the tail must not write into the next effect's slots.
184
- if (i >= u32(_rzLightU.z)) { return; }
188
+ if (i >= u32(_rzLightU[0].z)) { return; }
185
189
  // Time is a PARAMETER, not an rzTime() call: this same source compiles
186
190
  // inside the field, particle, trail and grid modules, and those already
187
191
  // define rzTime differently or not at all. A parameter needs nothing from
188
192
  // the module it lands in, which is why the field mounts take theirs too.
189
- let l = lightEmit(i, _rzLightU.x);
193
+ let l = lightEmit(i, _rzLightU[0].x);
190
194
  // SANITIZED at the one write site, because lightEmit is HOSTED USER CODE and
191
195
  // this buffer feeds every fragment of every material: one NaN position would
192
196
  // poison the whole frame, and WGSL leaves max(NaN, 0) indeterminate, so it
@@ -200,8 +204,8 @@ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
200
204
  // Weight rides along with the sanitisation, which is already the one place
201
205
  // this buffer is written: a lamp at half weight is half as bright, and one
202
206
  // at zero never reaches here because the dispatch is skipped.
203
- let c = select(vec3f(0.0), max(l.color * l.intensity, vec3f(0.0)), finite) * _rzLightU.w;
204
- let b = ${LIGHT_HEADER}u + (u32(_rzLightU.y) + i) * ${LIGHT_STRIDE}u;
207
+ let c = select(vec3f(0.0), max(l.color * l.intensity, vec3f(0.0)), finite) * _rzLightU[0].w;
208
+ let b = ${LIGHT_HEADER}u + (u32(_rzLightU[0].y) + i) * ${LIGHT_STRIDE}u;
205
209
  _rzLightsOut[b] = select(0.0, l.pos.x, finite);
206
210
  _rzLightsOut[b + 1u] = select(0.0, l.pos.y, finite);
207
211
  _rzLightsOut[b + 2u] = select(0.0, l.pos.z, finite);
@@ -209,7 +213,7 @@ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
209
213
  // weight above it. Scaling it with the fade would shrink a dimming lamp's
210
214
  // reach, which is a different thing than dimming it; zeroing it at nothing
211
215
  // is what lets a material's distance cull drop the slot entirely.
212
- _rzLightsOut[b + 3u] = select(0.0, max(l.radius, 0.0), finite && _rzLightU.w > 0.0);
216
+ _rzLightsOut[b + 3u] = select(0.0, max(l.radius, 0.0), finite && _rzLightU[0].w > 0.0);
213
217
  // Colour carries intensity, exactly as the CPU writer stores it — one product,
214
218
  // one place, so the two producers cannot disagree about what a slot means.
215
219
  _rzLightsOut[b + 4u] = c.x;
@@ -24,6 +24,14 @@ export declare const CAST_FIELD_DIV = 1;
24
24
  * HOLE in the seeds wherever it covers her, and the field grows into it from
25
25
  * her side, so the border ran round the inside of the prop instead of the
26
26
  * outside.
27
+ *
28
+ * WHICH SUBJECTS SEED IS A MASK, because an effect can be aimed at some of the
29
+ * cast and this field is how a screen-space look knows where anybody is. The
30
+ * distance to a model left out of the mask is not "large", it is a different
31
+ * question — so a field is built per distinct target set rather than filtered
32
+ * afterwards. The props are not masked: a prop belongs to whoever is holding it,
33
+ * and the engine does not know who that is. It seeds either way, which is the
34
+ * side that keeps a border whole.
27
35
  */
28
36
  export declare function buildCastSeedShader(samples: number): string;
29
37
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"cast-distance.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/cast-distance.ts"],"names":[],"mappings":"AA0CA;4EAC4E;AAC5E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;kEAEkE;AAClE,eAAO,MAAM,gBAAgB,EAAE,gBAA8B,CAAA;AAC7D;;8DAE8D;AAC9D,eAAO,MAAM,gBAAgB,EAAE,gBAA6B,CAAA;AAE5D;uDACuD;AACvD,eAAO,MAAM,oBAAoB,EAAE,gBAA4B,CAAA;AAE/D,iFAAiF;AACjF,eAAO,MAAM,cAAc,IAAI,CAAA;AAgB/B;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAqF3D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CA4B5C;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CA4B/C;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAmC/F;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAIzC"}
1
+ {"version":3,"file":"cast-distance.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/cast-distance.ts"],"names":[],"mappings":"AA0CA;4EAC4E;AAC5E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;kEAEkE;AAClE,eAAO,MAAM,gBAAgB,EAAE,gBAA8B,CAAA;AAC7D;;8DAE8D;AAC9D,eAAO,MAAM,gBAAgB,EAAE,gBAA6B,CAAA;AAE5D;uDACuD;AACvD,eAAO,MAAM,oBAAoB,EAAE,gBAA4B,CAAA;AAE/D,iFAAiF;AACjF,eAAO,MAAM,cAAc,IAAI,CAAA;AAgB/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAyF3D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CA4B5C;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CA4B/C;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAmC/F;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAIzC"}
@@ -79,6 +79,14 @@ fn vs(@builtin(vertex_index) i: u32) -> VSOut {
79
79
  * HOLE in the seeds wherever it covers her, and the field grows into it from
80
80
  * her side, so the border ran round the inside of the prop instead of the
81
81
  * outside.
82
+ *
83
+ * WHICH SUBJECTS SEED IS A MASK, because an effect can be aimed at some of the
84
+ * cast and this field is how a screen-space look knows where anybody is. The
85
+ * distance to a model left out of the mask is not "large", it is a different
86
+ * question — so a field is built per distinct target set rather than filtered
87
+ * afterwards. The props are not masked: a prop belongs to whoever is holding it,
88
+ * and the engine does not know who that is. It seeds either way, which is the
89
+ * side that keeps a border whole.
82
90
  */
83
91
  export function buildCastSeedShader(samples) {
84
92
  return (
@@ -92,6 +100,8 @@ const RZ_ID_SAMPLES: i32 = ${samples};
92
100
  /** The visible props' object ids, count first. Props are not subjects, so the
93
101
  * cast buffer does not carry them. */
94
102
  @group(0) @binding(2) var<storage, read> _rzSeedProps: array<u32>;
103
+ /** (subject mask, _, _, _) — which cast slots this field is built from. */
104
+ @group(0) @binding(3) var<uniform> _rzSeedU: vec4f;
95
105
 
96
106
  // The two cast accessors this pass needs, spelled out rather than pulled in.
97
107
  // CAST_API brings subjects, trails, anchors and their aliases with it, and a
@@ -107,9 +117,11 @@ fn rzSubjectId(i: i32) -> u32 {
107
117
  if (i < 0 || i >= rzSubjectCount()) { return 0u; }
108
118
  return u32(_rzCast[i * ${EFFECT_SUBJECT_VEC4S} + 1].w);
109
119
  }
110
- /** Does this object seed the field: a subject, or a prop. */
120
+ /** Does this object seed the field: a MASKED subject, or a prop. */
111
121
  fn rzSeeds(o: u32, subjects: i32) -> bool {
122
+ let mask = u32(_rzSeedU.x);
112
123
  for (var i = 0; i < subjects; i++) {
124
+ if ((mask & (1u << u32(i))) == 0u) { continue; }
113
125
  if (o == rzSubjectId(i)) { return true; }
114
126
  }
115
127
  let props = min(_rzSeedProps[0], arrayLength(&_rzSeedProps) - 1u);
@@ -97,7 +97,7 @@ type CompositeEffectSource = {
97
97
  * Depends on `viewU` and `_rzCast` being declared by the including module, and
98
98
  * on nothing else: no textures, no samplers.
99
99
  */
100
- export declare const EFFECT_SCENE_API = "\n// \u2500\u2500 The effect API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Named rz*, for the engine. The prefix earns its place twice: user code is\n// concatenated into THIS module, so an unprefixed rzAnchor() would collide with\n// exactly the helper an author would write, and the old bg* prefix stopped being\n// true in 0.41.0 when effects gained a mount over the finished frame.\n//\n// The bg* names below are permanent aliases, not a deprecation with an end date.\n// A published link is immutable, so a scene pinned to a bg* effect has to keep\n// compiling forever. They are one-line and inlined; no new function gets one.\n\n/** Canvas size in pixels \u2014 for aspect correction. */\nfn rzResolution() -> vec2f { return viewU[6].zw; }\n\n/** The camera's world position. */\nfn rzCameraPos() -> vec3f { return viewU[10].xyz; }\n\n/** How many characters are in the scene, up to four. */\nfn rzSubjectCount() -> i32 { return i32(viewU[10].w); }\n\n/**\n * A world point as the camera sees it: xy the uv it lands on, z its distance\n * along the VIEW AXIS in metres.\n *\n * The exact inverse of the ray this pass builds per pixel, so it is the cheap way\n * to work with anything anchored in the world. Marching a curve or a trail in 3D\n * costs a distance evaluation per sample per pixel; projecting its points once\n * and measuring in 2D costs a subtraction, which is the difference between a\n * ribbon that runs at 4K and one that does not.\n *\n * z is directly comparable to the depth handed to foreground(), so occlusion is\n * a single test: draw where your z is nearer than the scene's. It is returned\n * SIGNED and unclamped \u2014 behind the camera is negative, and worth rejecting\n * before you use the uv, which is meaningless there.\n */\nfn rzProject(p: vec3f) -> vec3f {\n let d = p - viewU[10].xyz;\n let z = dot(d, viewU[5].xyz);\n // Guard only the divide. z itself is returned as it is, so the caller can see\n // the sign; clamping it here would put points behind the lens on the horizon.\n let inv = 1.0 / select(z, 1e-4, z < 1e-4);\n let ndc = vec2f(dot(d, viewU[3].xyz) * inv / viewU[3].w, dot(d, viewU[4].xyz) * inv / viewU[4].w);\n return vec3f(ndc * 0.5 + 0.5, z);\n}\n\nfn rzCamPos() -> vec3f { return rzCameraPos(); }\nfn rzCameraRight() -> vec3f { return viewU[3].xyz; }\nfn rzCameraUp() -> vec3f { return viewU[4].xyz; }\nfn rzCameraForward() -> vec3f { return viewU[5].xyz; }\n\n// The cast \u2014 subjects, anchors, trails \u2014 is CAST_API, shared verbatim with the\n// particle and trail modules. It used to be written out here, a second time, and\n// the two copies had drifted: this one had rzAnchor and that one did not.\n\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 = 16;\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 /**\n * How much of this character is still THERE: 1 whole, 0 gone.\n *\n * What setModelDissolve last set on them, which the material pass has already\n * acted on by the time an effect runs \u2014 so an effect drawing what LEAVES a\n * dissolving body (sparks, ash, a ghost) reads the same number the body was\n * taken apart with, rather than keeping a clock of its own and hoping the two\n * agree. 1 on a scene that never dissolves anybody.\n */\n dissolve: f32,\n /**\n * Where she LOOKS: a unit direction from between the eyes, in world space.\n * Exact while her eyes are on something (Engine.setEyeTracking); otherwise\n * the motion's own eye turn undone through the range map \u2014 an eye bone\n * turns a fraction of the way toward what it looks at, and this gives back\n * the whole of it. An effect that should go where the look goes \u2014 a beam,\n * a light \u2014 takes this rather than an eye bone's axis, and it holds in\n * either mode. Zero, with looking false, only for a model without eyes.\n */\n gaze: vec3f,\n looking: bool,\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 * 4 + 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 * 4;\n s.root = _rzCast[b].xyz;\n s.dissolve = _rzCast[b].w;\n s.center = _rzCast[b + 1].xyz;\n s.bounds = _rzCast[b + 2];\n s.gaze = _rzCast[b + 3].xyz;\n s.looking = _rzCast[b + 3].w > 0.5;\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 = 16 + (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[16 + (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 = 208 + (_rzSlot(slot) * 4 + subject) * RZ_TRAIL_SAMPLES;\n return _rzCast[base + i];\n}\n\n\n// The hashes, the noise and rzFalloff \u2014 EFFECT_MATH_API, the same text the\n// particle and trail modules get. The field module used to be missing most of\n// it, so a helper an author wrote for one mount failed to compile in another\n// for no reason visible in the file.\n\nfn rzHash11(x: f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\nfn rzHash21(p: vec2f) -> f32 {\n var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n/** Three independent randoms from one seed \u2014 the usual need when spawning. */\nfn rzHash13(x: f32) -> vec3f {\n return vec3f(rzHash11(x), rzHash11(x + 17.13), rzHash11(x + 41.71));\n}\n/**\n * Compact-support falloff: 1 at the centre, exactly 0 at r, smooth between.\n *\n * It reaches exactly zero rather than merely getting small, because a glow that\n * never quite ends has to be culled somewhere, and culling it wherever it looks\n * close enough is what put a visible hard edge on the first halo effect.\n */\nfn rzFalloff(d: f32, r: f32) -> f32 {\n let x = clamp(d / max(r, 1e-6), 0.0, 1.0);\n let f = 1.0 - x;\n return f * f * f;\n}\nfn rzHash31(p: vec3f) -> f32 {\n var p3 = fract(p * 0.1031);\n p3 = p3 + dot(p3, p3.zyx + 31.32);\n return fract((p3.x + p3.y) * p3.z);\n}\nfn rzValueNoise(p: vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = rzHash31(i);\n let n100 = rzHash31(i + vec3f(1.0, 0.0, 0.0));\n let n010 = rzHash31(i + vec3f(0.0, 1.0, 0.0));\n let n110 = rzHash31(i + vec3f(1.0, 1.0, 0.0));\n let n001 = rzHash31(i + vec3f(0.0, 0.0, 1.0));\n let n101 = rzHash31(i + vec3f(1.0, 0.0, 1.0));\n let n011 = rzHash31(i + vec3f(0.0, 1.0, 1.0));\n let n111 = rzHash31(i + vec3f(1.0, 1.0, 1.0));\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n/** Divergence-free flow \u2014 the field a wisp of smoke follows without a solver. */\nfn rzCurlNoise(p: vec3f) -> vec3f {\n let e = 0.1;\n let dx = vec3f(e, 0.0, 0.0);\n let dy = vec3f(0.0, e, 0.0);\n let dz = vec3f(0.0, 0.0, e);\n let x0 = rzValueNoise(p - dx); let x1 = rzValueNoise(p + dx);\n let y0 = rzValueNoise(p - dy); let y1 = rzValueNoise(p + dy);\n let z0 = rzValueNoise(p - dz); let z1 = rzValueNoise(p + dz);\n return normalize(vec3f((y1 - y0) - (z1 - z0), (z1 - z0) - (x1 - x0), (x1 - x0) - (y1 - y0)) + vec3f(1e-6));\n}\n\nstruct Particle {\n pos: vec3f,\n age: f32,\n vel: vec3f,\n life: f32,\n size: f32,\n rot: f32,\n seed: f32,\n // Aspect along the direction of travel. 1 or less is a square billboard; a\n // raindrop is 10 or 20. Zero-initialised, so an effect that never sets it gets\n // the square it expects.\n stretch: f32,\n}\n\n\n\nfn bgResolution() -> vec2f { return rzResolution(); }\nfn bgCameraPos() -> vec3f { return rzCameraPos(); }\nfn bgSubjectCount() -> i32 { return rzSubjectCount(); }\n\n/**\n * Where a character IS, in world space \u2014 at the hips, not on the floor.\n *\n * An effect that wants to RESPOND to the cast \u2014 a glow that follows someone,\n * dust kicked up where they are \u2014 needs to know where they are, and the ray and\n * the depth cannot tell it: they describe the pixel, not the scene.\n *\n * The value is model.position + \u30BB\u30F3\u30BF\u30FC + \u5168\u3066\u306E\u89AA. \u30BB\u30F3\u30BF\u30FC sits at hip\n * height on every standard MMD rig, so this is a point in the middle of the\n * body. It is NOT the contact point: a ripple drawn here appears at the waist.\n * Ground effects want the .xz of this and their own floor height, which is what\n * the effects that shipped against it already do.\n *\n * The comment here used to claim it was \"between the feet on the floor\", which\n * is where that habit came from. Left as it is regardless of the name: a\n * published link is immutable, so every shared scene pinning an effect that\n * reads this depends on it meaning exactly what it has always meant.\n *\n * Clamped rather than bounds-checked: an effect looping past the count reads the\n * last subject instead of sampling whatever follows the array, which is a wrong\n * ripple rather than an undefined one.\n */\nfn rzSubjectHip(i: i32) -> vec3f { return viewU[11 + clamp(i, 0, 3)].xyz; }\n\nfn bgSubjectPos(i: i32) -> vec3f { return rzSubjectHip(i); }\n\n/** Where in the WORLD the scene drew this pixel \u2014 the depth handed to\n * foreground() turned into a place. Without it an effect can only think in\n * distances from the lens, which is no use to anything that belongs somewhere:\n * fog lying on the ground has to know where the ground is.\n *\n * depth measures along the VIEW AXIS, not along the ray, so it is divided by\n * the ray's projection onto camera-forward before being walked out. At the far\n * plane (nothing drawn) this lands a very long way off, which is what a sky\n * should do to anything reading it. */\nfn rzWorldPos(ray: vec3f, depth: f32) -> vec3f {\n let axis = max(dot(normalize(ray), viewU[5].xyz), 1e-4);\n return rzCameraPos() + normalize(ray) * (depth / axis);\n}\n\nfn bgWorldPos(ray: vec3f, depth: f32) -> vec3f { return rzWorldPos(ray, depth); }\n\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\n\n/** Color grading, applied to the tonemapped SCENE (not the background \u2014 see the\n * call site). The core is ASC CDL, the film-industry interchange standard:\n *\n * out = (in \u00B7 slope + offset) ^ power then saturation (SOP \u2192 SAT)\n *\n * Using the real standard rather than invented controls means a look authored\n * here maps onto any grading tool. slope/offset/power are derived on the CPU\n * from the UI's shadow/midtone/highlight colors (see setColorGrading), so the\n * per-pixel cost is one mul-add, one pow, one lerp. */\nfn grade(c: vec3f) -> vec3f {\n var x = pow(max(c * viewU[9].xyz + viewU[7].xyz, vec3f(0.0)), viewU[8].xyz);\n // Contrast pivots on 0.5 \u2014 display-referred midpoint, since we grade post-Filmic.\n x = (x - vec3f(0.5)) * viewU[7].w + vec3f(0.5);\n // Rec.709 luma, matching the ASC SAT node.\n let luma = dot(x, vec3f(0.2126, 0.7152, 0.0722));\n return max(mix(vec3f(luma), x, viewU[8].w), vec3f(0.0));\n}\n";
100
+ export declare const EFFECT_SCENE_API = "\n// \u2500\u2500 The effect API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Named rz*, for the engine. The prefix earns its place twice: user code is\n// concatenated into THIS module, so an unprefixed rzAnchor() would collide with\n// exactly the helper an author would write, and the old bg* prefix stopped being\n// true in 0.41.0 when effects gained a mount over the finished frame.\n//\n// The bg* names below are permanent aliases, not a deprecation with an end date.\n// A published link is immutable, so a scene pinned to a bg* effect has to keep\n// compiling forever. They are one-line and inlined; no new function gets one.\n\n/** Canvas size in pixels \u2014 for aspect correction. */\nfn rzResolution() -> vec2f { return viewU[6].zw; }\n\n/** The camera's world position. */\nfn rzCameraPos() -> vec3f { return viewU[10].xyz; }\n\n/** How many characters the SCENE holds, up to four \u2014 the number the cast api\n * narrows to this effect's own subjects. See subjectMaskApi. */\nfn _rzCastLive() -> i32 { return i32(viewU[10].w); }\n\n/**\n * A world point as the camera sees it: xy the uv it lands on, z its distance\n * along the VIEW AXIS in metres.\n *\n * The exact inverse of the ray this pass builds per pixel, so it is the cheap way\n * to work with anything anchored in the world. Marching a curve or a trail in 3D\n * costs a distance evaluation per sample per pixel; projecting its points once\n * and measuring in 2D costs a subtraction, which is the difference between a\n * ribbon that runs at 4K and one that does not.\n *\n * z is directly comparable to the depth handed to foreground(), so occlusion is\n * a single test: draw where your z is nearer than the scene's. It is returned\n * SIGNED and unclamped \u2014 behind the camera is negative, and worth rejecting\n * before you use the uv, which is meaningless there.\n */\nfn rzProject(p: vec3f) -> vec3f {\n let d = p - viewU[10].xyz;\n let z = dot(d, viewU[5].xyz);\n // Guard only the divide. z itself is returned as it is, so the caller can see\n // the sign; clamping it here would put points behind the lens on the horizon.\n let inv = 1.0 / select(z, 1e-4, z < 1e-4);\n let ndc = vec2f(dot(d, viewU[3].xyz) * inv / viewU[3].w, dot(d, viewU[4].xyz) * inv / viewU[4].w);\n return vec3f(ndc * 0.5 + 0.5, z);\n}\n\nfn rzCamPos() -> vec3f { return rzCameraPos(); }\nfn rzCameraRight() -> vec3f { return viewU[3].xyz; }\nfn rzCameraUp() -> vec3f { return viewU[4].xyz; }\nfn rzCameraForward() -> vec3f { return viewU[5].xyz; }\n\n// The cast \u2014 subjects, anchors, trails \u2014 is CAST_API, shared verbatim with the\n// particle and trail modules. It used to be written out here, a second time, and\n// the two copies had drifted: this one had rzAnchor and that one did not.\n\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 = 16;\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 /**\n * How much of this character is still THERE: 1 whole, 0 gone.\n *\n * What setModelDissolve last set on them, which the material pass has already\n * acted on by the time an effect runs \u2014 so an effect drawing what LEAVES a\n * dissolving body (sparks, ash, a ghost) reads the same number the body was\n * taken apart with, rather than keeping a clock of its own and hoping the two\n * agree. 1 on a scene that never dissolves anybody.\n */\n dissolve: f32,\n /**\n * Where she LOOKS: a unit direction from between the eyes, in world space.\n * Exact while her eyes are on something (Engine.setEyeTracking); otherwise\n * the motion's own eye turn undone through the range map \u2014 an eye bone\n * turns a fraction of the way toward what it looks at, and this gives back\n * the whole of it. An effect that should go where the look goes \u2014 a beam,\n * a light \u2014 takes this rather than an eye bone's axis, and it holds in\n * either mode. Zero, with looking false, only for a model without eyes.\n */\n gaze: vec3f,\n looking: bool,\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/**\n * How many characters THIS EFFECT is on, up to four.\n *\n * Not how many the scene holds. An effect applies to the whole cast unless the\n * scene says otherwise (Engine.setEffectSubjects), and where it says otherwise\n * this counts only the models named \u2014 so an effect aimed at one dancer sees one\n * subject, at index 0, and an author who wrote a loop over the cast gets the\n * targeting for free.\n *\n * Which is the whole design: the FILTER IS IN THE INDEX SPACE, not in the\n * shaders. There is no mask an effect can read, because an effect asking \"am I\n * allowed on this one\" is an effect that can get the answer wrong, and 18 of the\n * shipped built-ins would have had to be rewritten to ask at all.\n */\nfn rzSubjectCount() -> i32 {\n let live = _rzCastLive();\n let mask = _rzSubjectMask();\n var n = 0;\n for (var i = 0; i < live; i++) {\n if ((mask & (1u << u32(i))) != 0u) { n++; }\n }\n return n;\n}\n\n/** This effect's i-th subject \u2192 the scene's slot, or -1 past its own count.\n * Every accessor below goes through it, which is what keeps one effect's\n * subject 1 from being another's. */\nfn _rzSubjectSlot(i: i32) -> i32 {\n if (i < 0) { return -1; }\n let live = _rzCastLive();\n let mask = _rzSubjectMask();\n var n = 0;\n for (var s = 0; s < live; s++) {\n if ((mask & (1u << u32(s))) == 0u) { continue; }\n if (n == i) { return s; }\n n++;\n }\n return -1;\n}\n\n/** Which model this is, stable across a scene \u2014 for per-subject variation. */\nfn rzSubjectId(i: i32) -> u32 {\n let g = _rzSubjectSlot(i);\n if (g < 0) { return 0u; }\n return u32(_rzCast[g * 4 + 1].w);\n}\n\nfn rzSubject(i: i32) -> RzSubject {\n var s: RzSubject;\n let g = _rzSubjectSlot(i);\n s.valid = g >= 0;\n if (!s.valid) { return s; }\n let b = g * 4;\n s.root = _rzCast[b].xyz;\n s.dissolve = _rzCast[b].w;\n s.center = _rzCast[b + 1].xyz;\n s.bounds = _rzCast[b + 2];\n s.gaze = _rzCast[b + 3].xyz;\n s.looking = _rzCast[b + 3].w > 0.5;\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 let s = _rzSubjectSlot(subject);\n if (s < 0 || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }\n let b = 16 + (g * 4 + s) * 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 let s = _rzSubjectSlot(subject);\n if (s < 0 || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }\n return i32(_rzCast[16 + (g * 4 + s) * 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 = 208 + (_rzSlot(slot) * 4 + _rzSubjectSlot(subject)) * RZ_TRAIL_SAMPLES;\n return _rzCast[base + i];\n}\n\n\n// The hashes, the noise and rzFalloff \u2014 EFFECT_MATH_API, the same text the\n// particle and trail modules get. The field module used to be missing most of\n// it, so a helper an author wrote for one mount failed to compile in another\n// for no reason visible in the file.\n\nfn rzHash11(x: f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\nfn rzHash21(p: vec2f) -> f32 {\n var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n/** Three independent randoms from one seed \u2014 the usual need when spawning. */\nfn rzHash13(x: f32) -> vec3f {\n return vec3f(rzHash11(x), rzHash11(x + 17.13), rzHash11(x + 41.71));\n}\n/**\n * Compact-support falloff: 1 at the centre, exactly 0 at r, smooth between.\n *\n * It reaches exactly zero rather than merely getting small, because a glow that\n * never quite ends has to be culled somewhere, and culling it wherever it looks\n * close enough is what put a visible hard edge on the first halo effect.\n */\nfn rzFalloff(d: f32, r: f32) -> f32 {\n let x = clamp(d / max(r, 1e-6), 0.0, 1.0);\n let f = 1.0 - x;\n return f * f * f;\n}\nfn rzHash31(p: vec3f) -> f32 {\n var p3 = fract(p * 0.1031);\n p3 = p3 + dot(p3, p3.zyx + 31.32);\n return fract((p3.x + p3.y) * p3.z);\n}\nfn rzValueNoise(p: vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = rzHash31(i);\n let n100 = rzHash31(i + vec3f(1.0, 0.0, 0.0));\n let n010 = rzHash31(i + vec3f(0.0, 1.0, 0.0));\n let n110 = rzHash31(i + vec3f(1.0, 1.0, 0.0));\n let n001 = rzHash31(i + vec3f(0.0, 0.0, 1.0));\n let n101 = rzHash31(i + vec3f(1.0, 0.0, 1.0));\n let n011 = rzHash31(i + vec3f(0.0, 1.0, 1.0));\n let n111 = rzHash31(i + vec3f(1.0, 1.0, 1.0));\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n/** Divergence-free flow \u2014 the field a wisp of smoke follows without a solver. */\nfn rzCurlNoise(p: vec3f) -> vec3f {\n let e = 0.1;\n let dx = vec3f(e, 0.0, 0.0);\n let dy = vec3f(0.0, e, 0.0);\n let dz = vec3f(0.0, 0.0, e);\n let x0 = rzValueNoise(p - dx); let x1 = rzValueNoise(p + dx);\n let y0 = rzValueNoise(p - dy); let y1 = rzValueNoise(p + dy);\n let z0 = rzValueNoise(p - dz); let z1 = rzValueNoise(p + dz);\n return normalize(vec3f((y1 - y0) - (z1 - z0), (z1 - z0) - (x1 - x0), (x1 - x0) - (y1 - y0)) + vec3f(1e-6));\n}\n\nstruct Particle {\n pos: vec3f,\n age: f32,\n vel: vec3f,\n life: f32,\n size: f32,\n rot: f32,\n seed: f32,\n // Aspect along the direction of travel. 1 or less is a square billboard; a\n // raindrop is 10 or 20. Zero-initialised, so an effect that never sets it gets\n // the square it expects.\n stretch: f32,\n}\n\n\n\nfn bgResolution() -> vec2f { return rzResolution(); }\nfn bgCameraPos() -> vec3f { return rzCameraPos(); }\nfn bgSubjectCount() -> i32 { return rzSubjectCount(); }\n\n/**\n * Where a character IS, in world space \u2014 at the hips, not on the floor.\n *\n * An effect that wants to RESPOND to the cast \u2014 a glow that follows someone,\n * dust kicked up where they are \u2014 needs to know where they are, and the ray and\n * the depth cannot tell it: they describe the pixel, not the scene.\n *\n * The value is model.position + \u30BB\u30F3\u30BF\u30FC + \u5168\u3066\u306E\u89AA. \u30BB\u30F3\u30BF\u30FC sits at hip\n * height on every standard MMD rig, so this is a point in the middle of the\n * body. It is NOT the contact point: a ripple drawn here appears at the waist.\n * Ground effects want the .xz of this and their own floor height, which is what\n * the effects that shipped against it already do.\n *\n * The comment here used to claim it was \"between the feet on the floor\", which\n * is where that habit came from. Left as it is regardless of the name: a\n * published link is immutable, so every shared scene pinning an effect that\n * reads this depends on it meaning exactly what it has always meant.\n *\n * Clamped rather than bounds-checked: an effect looping past the count reads the\n * last subject instead of sampling whatever follows the array, which is a wrong\n * ripple rather than an undefined one.\n *\n * THE INDEX IS THE EFFECT'S OWN, like every other cast accessor \u2014 Dry Ice, Water,\n * Holy Light and Bloody Ash all read this one and all of them should place their\n * fog, their ripple and their shaft under the model the scene aimed them at. The\n * mapping is the identity for an effect aimed at nobody in particular, which is\n * every scene published before one could be aimed.\n */\nfn rzSubjectHip(i: i32) -> vec3f { return viewU[11 + clamp(_rzSubjectSlot(i), 0, 3)].xyz; }\n\nfn bgSubjectPos(i: i32) -> vec3f { return rzSubjectHip(i); }\n\n/** Where in the WORLD the scene drew this pixel \u2014 the depth handed to\n * foreground() turned into a place. Without it an effect can only think in\n * distances from the lens, which is no use to anything that belongs somewhere:\n * fog lying on the ground has to know where the ground is.\n *\n * depth measures along the VIEW AXIS, not along the ray, so it is divided by\n * the ray's projection onto camera-forward before being walked out. At the far\n * plane (nothing drawn) this lands a very long way off, which is what a sky\n * should do to anything reading it. */\nfn rzWorldPos(ray: vec3f, depth: f32) -> vec3f {\n let axis = max(dot(normalize(ray), viewU[5].xyz), 1e-4);\n return rzCameraPos() + normalize(ray) * (depth / axis);\n}\n\nfn bgWorldPos(ray: vec3f, depth: f32) -> vec3f { return rzWorldPos(ray, depth); }\n\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\n\n/** Color grading, applied to the tonemapped SCENE (not the background \u2014 see the\n * call site). The core is ASC CDL, the film-industry interchange standard:\n *\n * out = (in \u00B7 slope + offset) ^ power then saturation (SOP \u2192 SAT)\n *\n * Using the real standard rather than invented controls means a look authored\n * here maps onto any grading tool. slope/offset/power are derived on the CPU\n * from the UI's shadow/midtone/highlight colors (see setColorGrading), so the\n * per-pixel cost is one mul-add, one pow, one lerp. */\nfn grade(c: vec3f) -> vec3f {\n var x = pow(max(c * viewU[9].xyz + viewU[7].xyz, vec3f(0.0)), viewU[8].xyz);\n // Contrast pivots on 0.5 \u2014 display-referred midpoint, since we grade post-Filmic.\n x = (x - vec3f(0.5)) * viewU[7].w + vec3f(0.5);\n // Rec.709 luma, matching the ASC SAT node.\n let luma = dot(x, vec3f(0.2126, 0.7152, 0.0722));\n return max(mix(vec3f(luma), x, viewU[8].w), vec3f(0.0));\n}\n";
101
101
  export declare function buildCompositeShader(effect?: CompositeEffectSource | null): string;
102
102
  /**
103
103
  * The field pass: the user's background/foreground mounts at half resolution,
@@ -1 +1 @@
1
- {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AA2BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAgC+B;AAC/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EAAE,CAIhG;AAID,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAEzG,KAAK,qBAAqB,GAAG;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,aAAa,EAAE,OAAO,CAAA;IACtB,oEAAoE;IACpE,aAAa,EAAE,OAAO,CAAA;IACtB;6DACyD;IACzD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAA;IAChB;mFAC+E;IAC/E,GAAG,CAAC,EAAE,OAAO,CAAA;IACb;;;yEAGqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB;;8EAE0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;AA+LD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,40bA+H5B,CAAA;AA8MD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAQlF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAqGtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
1
+ {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AA2BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAgC+B;AAC/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EAAE,CAIhG;AAID,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAEzG,KAAK,qBAAqB,GAAG;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,aAAa,EAAE,OAAO,CAAA;IACtB,oEAAoE;IACpE,aAAa,EAAE,OAAO,CAAA;IACtB;6DACyD;IACzD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAA;IAChB;mFAC+E;IAC/E,GAAG,CAAC,EAAE,OAAO,CAAA;IACb;;;yEAGqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB;;8EAE0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;AA+LD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,+ufAsI5B,CAAA;AA8MD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAclF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAwGtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
@@ -1,7 +1,7 @@
1
1
  import { RZ_LIGHT_STRUCT_WGSL } from "../lights";
2
2
  import { sceneTapApi } from "../scene-tap";
3
3
  import { anchorAliasWgsl } from "../anchor-table";
4
- import { CAST_API } from "../cast-api";
4
+ import { CAST_API, CAST_MASK_ALL, subjectMaskApi } from "../cast-api";
5
5
  import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
6
6
  import { audioApi } from "../audio-api";
7
7
  import { idApi } from "../id-api";
@@ -302,8 +302,9 @@ fn rzResolution() -> vec2f { return viewU[6].zw; }
302
302
  /** The camera's world position. */
303
303
  fn rzCameraPos() -> vec3f { return viewU[10].xyz; }
304
304
 
305
- /** How many characters are in the scene, up to four. */
306
- fn rzSubjectCount() -> i32 { return i32(viewU[10].w); }
305
+ /** How many characters the SCENE holds, up to four — the number the cast api
306
+ * narrows to this effect's own subjects. See subjectMaskApi. */
307
+ fn _rzCastLive() -> i32 { return i32(viewU[10].w); }
307
308
 
308
309
  /**
309
310
  * A world point as the camera sees it: xy the uv it lands on, z its distance
@@ -372,8 +373,14 @@ fn bgSubjectCount() -> i32 { return rzSubjectCount(); }
372
373
  * Clamped rather than bounds-checked: an effect looping past the count reads the
373
374
  * last subject instead of sampling whatever follows the array, which is a wrong
374
375
  * ripple rather than an undefined one.
376
+ *
377
+ * THE INDEX IS THE EFFECT'S OWN, like every other cast accessor — Dry Ice, Water,
378
+ * Holy Light and Bloody Ash all read this one and all of them should place their
379
+ * fog, their ripple and their shaft under the model the scene aimed them at. The
380
+ * mapping is the identity for an effect aimed at nobody in particular, which is
381
+ * every scene published before one could be aimed.
375
382
  */
376
- fn rzSubjectHip(i: i32) -> vec3f { return viewU[11 + clamp(i, 0, 3)].xyz; }
383
+ fn rzSubjectHip(i: i32) -> vec3f { return viewU[11 + clamp(_rzSubjectSlot(i), 0, 3)].xyz; }
377
384
 
378
385
  fn bgSubjectPos(i: i32) -> vec3f { return rzSubjectHip(i); }
379
386
 
@@ -620,7 +627,13 @@ export function buildCompositeShader(effect) {
620
627
  // The composite is STATIC either way now: the user's code compiles in the
621
628
  // field module alone, and the composite only decides whether to sample it.
622
629
  return COMPOSITE_HEAD +
623
- EFFECT_SCENE_API + anchorAliasWgsl(effect?.alias ?? []) + audioApi(0, 13) + midiApi(0, 19) + lyricsApi(0, 24) + body;
630
+ EFFECT_SCENE_API +
631
+ subjectMaskApi(CAST_MASK_ALL) +
632
+ anchorAliasWgsl(effect?.alias ?? []) +
633
+ audioApi(0, 13) +
634
+ midiApi(0, 19) +
635
+ lyricsApi(0, 24) +
636
+ body;
624
637
  }
625
638
  /**
626
639
  * The field pass: the user's background/foreground mounts at half resolution,
@@ -650,6 +663,9 @@ export function buildFieldShader(effect) {
650
663
  : "";
651
664
  return (COMPOSITE_HEAD +
652
665
  EFFECT_SCENE_API +
666
+ // Which characters this effect is on, out of its own clock block — see the
667
+ // declaration of _rzFieldClock below for why that buffer is per effect.
668
+ subjectMaskApi("u32(_rzFieldClock.z)") +
653
669
  anchorAliasWgsl(effect.alias ?? []) +
654
670
  audioApi(0, 13) +
655
671
  midiApi(0, 19) +
@@ -686,7 +702,7 @@ export function buildFieldShader(effect) {
686
702
  * whose lightEmit read its own epoch disagreed with its own background()
687
703
  * about what time it was. One buffer per effect, one answer.
688
704
  */
689
- @group(0) @binding(22) var<uniform> _rzFieldClock: vec4f; // (time, weight, _, _)
705
+ @group(0) @binding(22) var<uniform> _rzFieldClock: vec4f; // (time, weight, subject mask, _)
690
706
 
691
707
  /** A filter's own straight-alpha output over the premultiplied layers it read
692
708
  * (the layer blend state, restated) and the additive form of the same. */
@@ -1 +1 @@
1
- {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAkC7C,eAAO,MAAM,QAAQ,OAAO,CAAA;AAC5B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,gDAAgD;AAChD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6B1F;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,SAAK,GAAG,MAAM,CAiFpG"}
1
+ {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAkC7C,eAAO,MAAM,QAAQ,OAAO,CAAA;AAC5B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,gDAAgD;AAChD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6B1F;AAgBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,SAAK,GAAG,MAAM,CAoFpG"}
@@ -3,6 +3,7 @@ import { lyricsApi } from "../lyrics-api";
3
3
  import { anchorAliasWgsl } from "../anchor-table";
4
4
  import { midiApi } from "../midi-api";
5
5
  import { EFFECT_SCENE_API } from "./composite";
6
+ import { subjectMaskApi } from "../cast-api";
6
7
  import { SCENE_TAP_STUB } from "../scene-tap";
7
8
  import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api";
8
9
  import { idApi } from "../id-api";
@@ -85,6 +86,11 @@ struct SimU {
85
86
  dt: f32,
86
87
  size: f32,
87
88
  frame: f32,
89
+ /** Which cast slots this effect is on, one bit each — see subjectMaskApi. */
90
+ mask: f32,
91
+ _pad0: f32,
92
+ _pad1: f32,
93
+ _pad2: f32,
88
94
  }
89
95
  `;
90
96
  /**
@@ -132,6 +138,9 @@ fn rzGridPrev(uv: vec2f) -> vec4f {
132
138
  }
133
139
  ` +
134
140
  EFFECT_SCENE_API +
141
+ // Which characters this kernel is on. A wake follows the feet it was aimed
142
+ // at, and a grid the whole cast walks through is the default.
143
+ subjectMaskApi("u32(su.mask)") +
135
144
  SCENE_TAP_STUB +
136
145
  // The alias _rzSlot, which the scene API's accessors route through. Every
137
146
  // module embedding EFFECT_SCENE_API must splice this; the sim module was
@@ -1 +1 @@
1
- {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AA4BA,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb;;mFAE+E;IAC/E,UAAU,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAiCD,qDAAqD;AACrD,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,CAAA;AAEzC,KAAK,cAAc,GAAG;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;IACd;;+BAE2B;IAC3B,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAuDjC,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAMlG;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA8CxF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6HvF"}
1
+ {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AA4BA,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb;;mFAE+E;IAC/E,UAAU,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAkCD,qDAAqD;AACrD,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,CAAA;AAEzC,KAAK,cAAc,GAAG;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;IACd;;+BAE2B;IAC3B,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAwDjC,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAMlG;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA8CxF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6HvF"}
@@ -4,7 +4,7 @@ import { audioApi } from "../audio-api";
4
4
  import { lyricsApi } from "../lyrics-api";
5
5
  import { anchorAliasWgsl } from "../anchor-table";
6
6
  import { midiApi } from "../midi-api";
7
- import { CAST_API } from "../cast-api";
7
+ import { CAST_API, subjectMaskApi } from "../cast-api";
8
8
  import { EFFECT_SUBJECT_VEC4S } from "../cast-layout";
9
9
  import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
10
10
  import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract";
@@ -21,12 +21,12 @@ import { idApi } from "../id-api";
21
21
  * a kernel that displaces fog has to know where the dancer's feet are. */
22
22
  function castApi(cast) {
23
23
  return (
24
- // rzSubjectCount FIRST, because CAST_API is written against it and this
25
- // module has no view uniform to read the engine's count out of. Scanning
26
- // for a subject whose bounding sphere has a radius is the same answer by a
24
+ // _rzCastLive FIRST, because CAST_API is written against it and this module
25
+ // has no view uniform to read the engine's count out of. Scanning for a
26
+ // subject whose bounding sphere has a radius is the same answer by a
27
27
  // different route — the seam is named at the top of cast-api.ts.
28
28
  `
29
- fn rzSubjectCount() -> i32 {
29
+ fn _rzCastLive() -> i32 {
30
30
  var n = 0;
31
31
  for (var i = 0; i < RZ_SUBJECTS; i++) {
32
32
  if (_rzCast[i * ${EFFECT_SUBJECT_VEC4S} + 2].w > 0.0) { n = i + 1; }
@@ -34,6 +34,7 @@ fn rzSubjectCount() -> i32 {
34
34
  return n;
35
35
  }
36
36
  ` +
37
+ subjectMaskApi("u32(pu.subjects)") +
37
38
  CAST_API +
38
39
  trailSlotsApi(cast.trailCount) +
39
40
  anchorAliasWgsl(cast.alias));
@@ -56,7 +57,8 @@ struct ParticleU {
56
57
  frame: u32,
57
58
  /** The effect's evaluated influence, applied at the one output site below. */
58
59
  weight: f32,
59
- _pad0: f32,
60
+ /** Which cast slots this effect is on, one bit each — see subjectMaskApi. */
61
+ subjects: f32,
60
62
  _pad1: f32,
61
63
  _pad2: f32,
62
64
  }
@@ -1 +1 @@
1
- {"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AA4BA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CAiVR"}
1
+ {"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AA4BA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CAsVR"}