reze-engine 0.56.14 → 0.57.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.d.ts +164 -13
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +468 -109
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +30 -0
- package/dist/graph/style-group.d.ts +14 -0
- package/dist/graph/style-group.d.ts.map +1 -1
- package/dist/shaders/cast-api.d.ts +16 -1
- package/dist/shaders/cast-api.d.ts.map +1 -1
- package/dist/shaders/cast-api.js +80 -14
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +13 -9
- package/dist/shaders/passes/cast-distance.d.ts +8 -0
- package/dist/shaders/passes/cast-distance.d.ts.map +1 -1
- package/dist/shaders/passes/cast-distance.js +13 -1
- package/dist/shaders/passes/composite.d.ts +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +22 -6
- package/dist/shaders/passes/grid.d.ts.map +1 -1
- package/dist/shaders/passes/grid.js +9 -0
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +8 -6
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +16 -11
- package/package.json +1 -1
- package/src/engine.ts +550 -109
- package/src/graph/registry.ts +32 -0
- package/src/graph/style-group.ts +14 -0
- package/src/shaders/cast-api.ts +82 -14
- package/src/shaders/lights.ts +13 -9
- package/src/shaders/passes/cast-distance.ts +13 -1
- package/src/shaders/passes/composite.ts +22 -6
- package/src/shaders/passes/grid.ts +9 -0
- package/src/shaders/passes/particles.ts +8 -6
- package/src/shaders/passes/trails.ts +15 -10
package/src/graph/registry.ts
CHANGED
|
@@ -217,6 +217,38 @@ export const NODE_REGISTRY: Record<string, NodeSpec> = {
|
|
|
217
217
|
// PMX material's diffuse color (the authored base tint). Multiply the diffuse texture
|
|
218
218
|
// by this for the MMD-correct base — untextured materials carry their color here, so a
|
|
219
219
|
// texture-only base would render them white.
|
|
220
|
+
// WHAT THE SURFACE IS MADE OF, when whoever wrote the model said so.
|
|
221
|
+
//
|
|
222
|
+
// PMX carries a specular colour its own renderer barely uses, and a stage
|
|
223
|
+
// converted out of a game engine packs its material's (metal, roughness,
|
|
224
|
+
// occlusion) there — the same triple that game's property map holds, averaged
|
|
225
|
+
// per material. One shared look can then read it and a whole stage keeps ONE
|
|
226
|
+
// pipeline, where a look per material was what cost a garden its frame rate.
|
|
227
|
+
//
|
|
228
|
+
// A hand-authored PMX leaves it at whatever its exporter wrote, so a graph
|
|
229
|
+
// that reads this is choosing to trust the model. Nothing else does.
|
|
230
|
+
// How opaque the PMX said this material is, before its texture's alpha is
|
|
231
|
+
// multiplied in. A graph that computes its own opacity still wants it: glass
|
|
232
|
+
// is see-through by the amount its author chose and a mirror at a grazing
|
|
233
|
+
// angle, and without this every pane in a stage gets the same transparency
|
|
234
|
+
// whatever its material says.
|
|
235
|
+
material_alpha: {
|
|
236
|
+
inputs: {},
|
|
237
|
+
outputs: { value: "float" },
|
|
238
|
+
contextOutputs: { value: "material.alpha" },
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
material_specular: {
|
|
242
|
+
inputs: {},
|
|
243
|
+
outputs: { color: "color" },
|
|
244
|
+
contextOutputs: { color: "material.specular" },
|
|
245
|
+
},
|
|
246
|
+
material_shininess: {
|
|
247
|
+
inputs: {},
|
|
248
|
+
outputs: { value: "float" },
|
|
249
|
+
contextOutputs: { value: "material.shininess" },
|
|
250
|
+
},
|
|
251
|
+
|
|
220
252
|
material_diffuse: {
|
|
221
253
|
inputs: {},
|
|
222
254
|
outputs: { color: "color" },
|
package/src/graph/style-group.ts
CHANGED
|
@@ -36,6 +36,20 @@ export type StyleGroup = {
|
|
|
36
36
|
* no-op rather than a failure.
|
|
37
37
|
*/
|
|
38
38
|
images?: (GroupImage | GroupImageSource | null)[]
|
|
39
|
+
/**
|
|
40
|
+
* The same slots, but PER MATERIAL — for a group whose members each bring
|
|
41
|
+
* their own maps.
|
|
42
|
+
*
|
|
43
|
+
* `images` belongs to the LOOK and is right when a look carries its own ramp
|
|
44
|
+
* or detail map. A converted stage is the other case: twenty-seven props, one
|
|
45
|
+
* shading model, and a different normal map on every one of them. Keyed here,
|
|
46
|
+
* they share a pipeline and differ only in what the bind group points at —
|
|
47
|
+
* the alternative is a group per material, which is a pipeline per material,
|
|
48
|
+
* which is what cost a garden stage its frame rate.
|
|
49
|
+
*
|
|
50
|
+
* A material with no entry falls back to `images`, and then to 1×1 white.
|
|
51
|
+
*/
|
|
52
|
+
imagesByMaterial?: Record<string, (GroupImage | GroupImageSource | null)[]>
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
export type GroupImageSource = ImageBitmap | HTMLImageElement | HTMLCanvasElement
|
package/src/shaders/cast-api.ts
CHANGED
|
@@ -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.
|
|
18
|
+
// WHAT A HOST MUST SUPPLY. Four names, and deliberately only four:
|
|
19
19
|
//
|
|
20
|
-
// _rzCast
|
|
21
|
-
// _rzSlot(i)
|
|
22
|
-
//
|
|
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
|
|
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
|
|
|
30
35
|
import { EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES, EFFECT_SUBJECT_VEC4S } from "./cast-layout"
|
|
31
36
|
|
|
@@ -82,17 +87,59 @@ struct RzAnchor {
|
|
|
82
87
|
valid: bool,
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
/**
|
|
91
|
+
* How many characters THIS EFFECT is on, up to four.
|
|
92
|
+
*
|
|
93
|
+
* Not how many the scene holds. An effect applies to the whole cast unless the
|
|
94
|
+
* scene says otherwise (Engine.setEffectSubjects), and where it says otherwise
|
|
95
|
+
* this counts only the models named — so an effect aimed at one dancer sees one
|
|
96
|
+
* subject, at index 0, and an author who wrote a loop over the cast gets the
|
|
97
|
+
* targeting for free.
|
|
98
|
+
*
|
|
99
|
+
* Which is the whole design: the FILTER IS IN THE INDEX SPACE, not in the
|
|
100
|
+
* shaders. There is no mask an effect can read, because an effect asking "am I
|
|
101
|
+
* allowed on this one" is an effect that can get the answer wrong, and 18 of the
|
|
102
|
+
* shipped built-ins would have had to be rewritten to ask at all.
|
|
103
|
+
*/
|
|
104
|
+
fn rzSubjectCount() -> i32 {
|
|
105
|
+
let live = _rzCastLive();
|
|
106
|
+
let mask = _rzSubjectMask();
|
|
107
|
+
var n = 0;
|
|
108
|
+
for (var i = 0; i < live; i++) {
|
|
109
|
+
if ((mask & (1u << u32(i))) != 0u) { n++; }
|
|
110
|
+
}
|
|
111
|
+
return n;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** This effect's i-th subject → the scene's slot, or -1 past its own count.
|
|
115
|
+
* Every accessor below goes through it, which is what keeps one effect's
|
|
116
|
+
* subject 1 from being another's. */
|
|
117
|
+
fn _rzSubjectSlot(i: i32) -> i32 {
|
|
118
|
+
if (i < 0) { return -1; }
|
|
119
|
+
let live = _rzCastLive();
|
|
120
|
+
let mask = _rzSubjectMask();
|
|
121
|
+
var n = 0;
|
|
122
|
+
for (var s = 0; s < live; s++) {
|
|
123
|
+
if ((mask & (1u << u32(s))) == 0u) { continue; }
|
|
124
|
+
if (n == i) { return s; }
|
|
125
|
+
n++;
|
|
126
|
+
}
|
|
127
|
+
return -1;
|
|
128
|
+
}
|
|
129
|
+
|
|
85
130
|
/** Which model this is, stable across a scene — for per-subject variation. */
|
|
86
131
|
fn rzSubjectId(i: i32) -> u32 {
|
|
87
|
-
|
|
88
|
-
|
|
132
|
+
let g = _rzSubjectSlot(i);
|
|
133
|
+
if (g < 0) { return 0u; }
|
|
134
|
+
return u32(_rzCast[g * ${EFFECT_SUBJECT_VEC4S} + 1].w);
|
|
89
135
|
}
|
|
90
136
|
|
|
91
137
|
fn rzSubject(i: i32) -> RzSubject {
|
|
92
138
|
var s: RzSubject;
|
|
93
|
-
|
|
139
|
+
let g = _rzSubjectSlot(i);
|
|
140
|
+
s.valid = g >= 0;
|
|
94
141
|
if (!s.valid) { return s; }
|
|
95
|
-
let b =
|
|
142
|
+
let b = g * ${EFFECT_SUBJECT_VEC4S};
|
|
96
143
|
s.root = _rzCast[b].xyz;
|
|
97
144
|
s.dissolve = _rzCast[b].w;
|
|
98
145
|
s.center = _rzCast[b + 1].xyz;
|
|
@@ -113,8 +160,9 @@ fn rzAnchor(subject: i32, slot: i32) -> RzAnchor {
|
|
|
113
160
|
var a: RzAnchor;
|
|
114
161
|
a.valid = false;
|
|
115
162
|
let g = _rzSlot(slot);
|
|
116
|
-
|
|
117
|
-
|
|
163
|
+
let s = _rzSubjectSlot(subject);
|
|
164
|
+
if (s < 0 || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }
|
|
165
|
+
let b = ${EFFECT_SUBJECTS * EFFECT_SUBJECT_VEC4S} + (g * ${EFFECT_SUBJECTS} + s) * 3;
|
|
118
166
|
a.valid = _rzCast[b].w > 0.5;
|
|
119
167
|
a.pos = _rzCast[b].xyz;
|
|
120
168
|
a.vel = _rzCast[b + 1].xyz;
|
|
@@ -133,15 +181,35 @@ fn rzAnchor(subject: i32, slot: i32) -> RzAnchor {
|
|
|
133
181
|
*/
|
|
134
182
|
fn rzTrailCount(subject: i32, slot: i32) -> i32 {
|
|
135
183
|
let g = _rzSlot(slot);
|
|
136
|
-
|
|
137
|
-
|
|
184
|
+
let s = _rzSubjectSlot(subject);
|
|
185
|
+
if (s < 0 || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }
|
|
186
|
+
return i32(_rzCast[${EFFECT_SUBJECTS * EFFECT_SUBJECT_VEC4S} + (g * ${EFFECT_SUBJECTS} + s) * 3 + 2].w);
|
|
138
187
|
}
|
|
139
188
|
|
|
140
189
|
/** Sample i of a path: xyz where it was, w how many seconds ago. i = 0 is now. */
|
|
141
190
|
fn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {
|
|
142
191
|
let n = rzTrailCount(subject, slot);
|
|
143
192
|
if (i < 0 || i >= n) { return vec4f(0.0); }
|
|
144
|
-
let base = ${EFFECT_TRAIL_BASE} + (_rzSlot(slot) * ${EFFECT_SUBJECTS} + subject) * RZ_TRAIL_SAMPLES;
|
|
193
|
+
let base = ${EFFECT_TRAIL_BASE} + (_rzSlot(slot) * ${EFFECT_SUBJECTS} + _rzSubjectSlot(subject)) * RZ_TRAIL_SAMPLES;
|
|
145
194
|
return _rzCast[base + i];
|
|
146
195
|
}
|
|
147
196
|
`
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Which subjects this effect applies to, as WGSL — the second of the two names a
|
|
200
|
+
* mount owes CAST_API (`_rzCastLive` is the first, and a mount with a view
|
|
201
|
+
* uniform already has it from EFFECT_SCENE_API).
|
|
202
|
+
*
|
|
203
|
+
* One bit per cast slot. An EXPRESSION rather than a value: it resolves per
|
|
204
|
+
* draw, out of a uniform the engine writes once a frame, so a module that baked
|
|
205
|
+
* it would stop following a target being changed or a model being hidden.
|
|
206
|
+
*/
|
|
207
|
+
export const subjectMaskApi = (mask: string): string => /* wgsl */ `
|
|
208
|
+
fn _rzSubjectMask() -> u32 { return ${mask}; }
|
|
209
|
+
`
|
|
210
|
+
|
|
211
|
+
/** Every subject the scene has. For a mount that is not an effect — the
|
|
212
|
+
* composite's own module, where nothing user-written lands and the accessors
|
|
213
|
+
* exist only so the shared API compiles — and the value the engine writes for
|
|
214
|
+
* an effect the scene has not aimed at anybody in particular. */
|
|
215
|
+
export const CAST_MASK_ALL = "0xfu"
|
package/src/shaders/lights.ts
CHANGED
|
@@ -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.
|
|
@@ -149,15 +150,18 @@ export function buildLightEmitShader(
|
|
|
149
150
|
// read_write HERE and read-only in the material shaders. Different passes, so
|
|
150
151
|
// the two never coexist: this compute runs before the scene pass that reads it.
|
|
151
152
|
@group(0) @binding(0) var<storage, read_write> _rzLightsOut: array<f32>;
|
|
152
|
-
// (time, base slot, count, weight) — see buildLightEmitShader on why the
|
|
153
|
-
// is here and not in the text.
|
|
154
|
-
|
|
153
|
+
// [0] (time, base slot, count, weight) — see buildLightEmitShader on why the
|
|
154
|
+
// base is here and not in the text — and [1].x the subject mask, which is the
|
|
155
|
+
// only thing the second vec4 is for: an emitter reads the cast through the same
|
|
156
|
+
// accessors its drawing half does, so a rig aimed at one dancer must see one.
|
|
157
|
+
@group(0) @binding(1) var<uniform> _rzLightU: array<vec4f, 2>;
|
|
155
158
|
// The camera block and the cast — the two buffers the scene API reads. Same
|
|
156
159
|
// contents the field and grid modules bind, so an effect's lightEmit sees the
|
|
157
160
|
// scene exactly as its drawing half does.
|
|
158
161
|
@group(0) @binding(2) var<uniform> viewU: array<vec4<f32>, 15>;
|
|
159
162
|
@group(0) @binding(3) var<storage, read> _rzCast: array<vec4f>;
|
|
160
163
|
${sceneApi}
|
|
164
|
+
${subjectMaskApi("u32(_rzLightU[1].x)")}
|
|
161
165
|
${SCENE_TAP_STUB}
|
|
162
166
|
// Audio and score at 4 and 5, the same bindings the particle and trail modules
|
|
163
167
|
// put them on. A lamp that pulses on the beat or lights on a note is the whole
|
|
@@ -171,7 +175,7 @@ ${lyricsApi(0, 6)}
|
|
|
171
175
|
// grows a lamp at its tip compiles its ribbon code in this module too. The math
|
|
172
176
|
// helpers and the Particle struct are NOT repeated — they arrive with the scene
|
|
173
177
|
// API above, and a second copy is a redefinition error in engine code.
|
|
174
|
-
${clockApi("_rzLightU.x", "0.0")}
|
|
178
|
+
${clockApi("_rzLightU[0].x", "0.0")}
|
|
175
179
|
// Canvas height — the same number the drawing modules read out of their camera
|
|
176
180
|
// struct, both written from canvas.height, so this name means ONE value in
|
|
177
181
|
// every module. Verified against the writers, not assumed: cameraMatrixData[35]
|
|
@@ -192,12 +196,12 @@ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
192
196
|
let i = gid.x;
|
|
193
197
|
// The dispatch is sized to the count, but a workgroup is 64 wide and the
|
|
194
198
|
// count rarely is — the tail must not write into the next effect's slots.
|
|
195
|
-
if (i >= u32(_rzLightU.z)) { return; }
|
|
199
|
+
if (i >= u32(_rzLightU[0].z)) { return; }
|
|
196
200
|
// Time is a PARAMETER, not an rzTime() call: this same source compiles
|
|
197
201
|
// inside the field, particle, trail and grid modules, and those already
|
|
198
202
|
// define rzTime differently or not at all. A parameter needs nothing from
|
|
199
203
|
// the module it lands in, which is why the field mounts take theirs too.
|
|
200
|
-
let l = lightEmit(i, _rzLightU.x);
|
|
204
|
+
let l = lightEmit(i, _rzLightU[0].x);
|
|
201
205
|
// SANITIZED at the one write site, because lightEmit is HOSTED USER CODE and
|
|
202
206
|
// this buffer feeds every fragment of every material: one NaN position would
|
|
203
207
|
// poison the whole frame, and WGSL leaves max(NaN, 0) indeterminate, so it
|
|
@@ -211,8 +215,8 @@ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
211
215
|
// Weight rides along with the sanitisation, which is already the one place
|
|
212
216
|
// this buffer is written: a lamp at half weight is half as bright, and one
|
|
213
217
|
// at zero never reaches here because the dispatch is skipped.
|
|
214
|
-
let c = select(vec3f(0.0), max(l.color * l.intensity, vec3f(0.0)), finite) * _rzLightU.w;
|
|
215
|
-
let b = ${LIGHT_HEADER}u + (u32(_rzLightU.y) + i) * ${LIGHT_STRIDE}u;
|
|
218
|
+
let c = select(vec3f(0.0), max(l.color * l.intensity, vec3f(0.0)), finite) * _rzLightU[0].w;
|
|
219
|
+
let b = ${LIGHT_HEADER}u + (u32(_rzLightU[0].y) + i) * ${LIGHT_STRIDE}u;
|
|
216
220
|
_rzLightsOut[b] = select(0.0, l.pos.x, finite);
|
|
217
221
|
_rzLightsOut[b + 1u] = select(0.0, l.pos.y, finite);
|
|
218
222
|
_rzLightsOut[b + 2u] = select(0.0, l.pos.z, finite);
|
|
@@ -220,7 +224,7 @@ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
220
224
|
// weight above it. Scaling it with the fade would shrink a dimming lamp's
|
|
221
225
|
// reach, which is a different thing than dimming it; zeroing it at nothing
|
|
222
226
|
// is what lets a material's distance cull drop the slot entirely.
|
|
223
|
-
_rzLightsOut[b + 3u] = select(0.0, max(l.radius, 0.0), finite && _rzLightU.w > 0.0);
|
|
227
|
+
_rzLightsOut[b + 3u] = select(0.0, max(l.radius, 0.0), finite && _rzLightU[0].w > 0.0);
|
|
224
228
|
// Colour carries intensity, exactly as the CPU writer stores it — one product,
|
|
225
229
|
// one place, so the two producers cannot disagree about what a slot means.
|
|
226
230
|
_rzLightsOut[b + 4u] = c.x;
|
|
@@ -86,6 +86,14 @@ fn vs(@builtin(vertex_index) i: u32) -> VSOut {
|
|
|
86
86
|
* HOLE in the seeds wherever it covers her, and the field grows into it from
|
|
87
87
|
* her side, so the border ran round the inside of the prop instead of the
|
|
88
88
|
* outside.
|
|
89
|
+
*
|
|
90
|
+
* WHICH SUBJECTS SEED IS A MASK, because an effect can be aimed at some of the
|
|
91
|
+
* cast and this field is how a screen-space look knows where anybody is. The
|
|
92
|
+
* distance to a model left out of the mask is not "large", it is a different
|
|
93
|
+
* question — so a field is built per distinct target set rather than filtered
|
|
94
|
+
* afterwards. The props are not masked: a prop belongs to whoever is holding it,
|
|
95
|
+
* and the engine does not know who that is. It seeds either way, which is the
|
|
96
|
+
* side that keeps a border whole.
|
|
89
97
|
*/
|
|
90
98
|
export function buildCastSeedShader(samples: number): string {
|
|
91
99
|
return (
|
|
@@ -99,6 +107,8 @@ const RZ_ID_SAMPLES: i32 = ${samples};
|
|
|
99
107
|
/** The visible props' object ids, count first. Props are not subjects, so the
|
|
100
108
|
* cast buffer does not carry them. */
|
|
101
109
|
@group(0) @binding(2) var<storage, read> _rzSeedProps: array<u32>;
|
|
110
|
+
/** (subject mask, _, _, _) — which cast slots this field is built from. */
|
|
111
|
+
@group(0) @binding(3) var<uniform> _rzSeedU: vec4f;
|
|
102
112
|
|
|
103
113
|
// The two cast accessors this pass needs, spelled out rather than pulled in.
|
|
104
114
|
// CAST_API brings subjects, trails, anchors and their aliases with it, and a
|
|
@@ -114,9 +124,11 @@ fn rzSubjectId(i: i32) -> u32 {
|
|
|
114
124
|
if (i < 0 || i >= rzSubjectCount()) { return 0u; }
|
|
115
125
|
return u32(_rzCast[i * ${EFFECT_SUBJECT_VEC4S} + 1].w);
|
|
116
126
|
}
|
|
117
|
-
/** Does this object seed the field: a subject, or a prop. */
|
|
127
|
+
/** Does this object seed the field: a MASKED subject, or a prop. */
|
|
118
128
|
fn rzSeeds(o: u32, subjects: i32) -> bool {
|
|
129
|
+
let mask = u32(_rzSeedU.x);
|
|
119
130
|
for (var i = 0; i < subjects; i++) {
|
|
131
|
+
if ((mask & (1u << u32(i))) == 0u) { continue; }
|
|
120
132
|
if (o == rzSubjectId(i)) { return true; }
|
|
121
133
|
}
|
|
122
134
|
let props = min(_rzSeedProps[0], arrayLength(&_rzSeedProps) - 1u);
|
|
@@ -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 { EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES } from "../cast-layout"
|
|
7
7
|
import { audioApi } from "../audio-api"
|
|
@@ -337,8 +337,9 @@ fn rzResolution() -> vec2f { return viewU[6].zw; }
|
|
|
337
337
|
/** The camera's world position. */
|
|
338
338
|
fn rzCameraPos() -> vec3f { return viewU[10].xyz; }
|
|
339
339
|
|
|
340
|
-
/** How many characters
|
|
341
|
-
|
|
340
|
+
/** How many characters the SCENE holds, up to four — the number the cast api
|
|
341
|
+
* narrows to this effect's own subjects. See subjectMaskApi. */
|
|
342
|
+
fn _rzCastLive() -> i32 { return i32(viewU[10].w); }
|
|
342
343
|
|
|
343
344
|
/**
|
|
344
345
|
* A world point as the camera sees it: xy the uv it lands on, z its distance
|
|
@@ -407,8 +408,14 @@ fn bgSubjectCount() -> i32 { return rzSubjectCount(); }
|
|
|
407
408
|
* Clamped rather than bounds-checked: an effect looping past the count reads the
|
|
408
409
|
* last subject instead of sampling whatever follows the array, which is a wrong
|
|
409
410
|
* ripple rather than an undefined one.
|
|
411
|
+
*
|
|
412
|
+
* THE INDEX IS THE EFFECT'S OWN, like every other cast accessor — Dry Ice, Water,
|
|
413
|
+
* Holy Light and Bloody Ash all read this one and all of them should place their
|
|
414
|
+
* fog, their ripple and their shaft under the model the scene aimed them at. The
|
|
415
|
+
* mapping is the identity for an effect aimed at nobody in particular, which is
|
|
416
|
+
* every scene published before one could be aimed.
|
|
410
417
|
*/
|
|
411
|
-
fn rzSubjectHip(i: i32) -> vec3f { return viewU[11 + clamp(i, 0, 3)].xyz; }
|
|
418
|
+
fn rzSubjectHip(i: i32) -> vec3f { return viewU[11 + clamp(_rzSubjectSlot(i), 0, 3)].xyz; }
|
|
412
419
|
|
|
413
420
|
fn bgSubjectPos(i: i32) -> vec3f { return rzSubjectHip(i); }
|
|
414
421
|
|
|
@@ -659,7 +666,13 @@ export function buildCompositeShader(effect?: CompositeEffectSource | null): str
|
|
|
659
666
|
// The composite is STATIC either way now: the user's code compiles in the
|
|
660
667
|
// field module alone, and the composite only decides whether to sample it.
|
|
661
668
|
return COMPOSITE_HEAD +
|
|
662
|
-
EFFECT_SCENE_API +
|
|
669
|
+
EFFECT_SCENE_API +
|
|
670
|
+
subjectMaskApi(CAST_MASK_ALL) +
|
|
671
|
+
anchorAliasWgsl(effect?.alias ?? []) +
|
|
672
|
+
audioApi(0, 13) +
|
|
673
|
+
midiApi(0, 19) +
|
|
674
|
+
lyricsApi(0, 24) +
|
|
675
|
+
body
|
|
663
676
|
}
|
|
664
677
|
|
|
665
678
|
/**
|
|
@@ -691,6 +704,9 @@ export function buildFieldShader(effect: CompositeEffectSource): string {
|
|
|
691
704
|
return (
|
|
692
705
|
COMPOSITE_HEAD +
|
|
693
706
|
EFFECT_SCENE_API +
|
|
707
|
+
// Which characters this effect is on, out of its own clock block — see the
|
|
708
|
+
// declaration of _rzFieldClock below for why that buffer is per effect.
|
|
709
|
+
subjectMaskApi("u32(_rzFieldClock.z)") +
|
|
694
710
|
anchorAliasWgsl(effect.alias ?? []) +
|
|
695
711
|
audioApi(0, 13) +
|
|
696
712
|
midiApi(0, 19) +
|
|
@@ -727,7 +743,7 @@ export function buildFieldShader(effect: CompositeEffectSource): string {
|
|
|
727
743
|
* whose lightEmit read its own epoch disagreed with its own background()
|
|
728
744
|
* about what time it was. One buffer per effect, one answer.
|
|
729
745
|
*/
|
|
730
|
-
@group(0) @binding(22) var<uniform> _rzFieldClock: vec4f; // (time, weight,
|
|
746
|
+
@group(0) @binding(22) var<uniform> _rzFieldClock: vec4f; // (time, weight, subject mask, _)
|
|
731
747
|
|
|
732
748
|
/** A filter's own straight-alpha output over the premultiplied layers it read
|
|
733
749
|
* (the layer blend state, restated) and the additive form of the same. */
|
|
@@ -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 { type CastLayout } from "./particles"
|
|
8
9
|
import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api"
|
|
@@ -91,6 +92,11 @@ struct SimU {
|
|
|
91
92
|
dt: f32,
|
|
92
93
|
size: f32,
|
|
93
94
|
frame: f32,
|
|
95
|
+
/** Which cast slots this effect is on, one bit each — see subjectMaskApi. */
|
|
96
|
+
mask: f32,
|
|
97
|
+
_pad0: f32,
|
|
98
|
+
_pad1: f32,
|
|
99
|
+
_pad2: f32,
|
|
94
100
|
}
|
|
95
101
|
`
|
|
96
102
|
|
|
@@ -140,6 +146,9 @@ fn rzGridPrev(uv: vec2f) -> vec4f {
|
|
|
140
146
|
}
|
|
141
147
|
` +
|
|
142
148
|
EFFECT_SCENE_API +
|
|
149
|
+
// Which characters this kernel is on. A wake follows the feet it was aimed
|
|
150
|
+
// at, and a grid the whole cast walks through is the default.
|
|
151
|
+
subjectMaskApi("u32(su.mask)") +
|
|
143
152
|
SCENE_TAP_STUB +
|
|
144
153
|
// The alias _rzSlot, which the scene API's accessors route through. Every
|
|
145
154
|
// module embedding EFFECT_SCENE_API must splice this; the sim module was
|
|
@@ -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"
|
|
@@ -54,12 +54,12 @@ export type CastLayout = {
|
|
|
54
54
|
* a kernel that displaces fog has to know where the dancer's feet are. */
|
|
55
55
|
function castApi(cast: CastLayout): string {
|
|
56
56
|
return (
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
57
|
+
// _rzCastLive FIRST, because CAST_API is written against it and this module
|
|
58
|
+
// has no view uniform to read the engine's count out of. Scanning for a
|
|
59
|
+
// subject whose bounding sphere has a radius is the same answer by a
|
|
60
60
|
// different route — the seam is named at the top of cast-api.ts.
|
|
61
61
|
`
|
|
62
|
-
fn
|
|
62
|
+
fn _rzCastLive() -> i32 {
|
|
63
63
|
var n = 0;
|
|
64
64
|
for (var i = 0; i < RZ_SUBJECTS; i++) {
|
|
65
65
|
if (_rzCast[i * ${EFFECT_SUBJECT_VEC4S} + 2].w > 0.0) { n = i + 1; }
|
|
@@ -67,6 +67,7 @@ fn rzSubjectCount() -> i32 {
|
|
|
67
67
|
return n;
|
|
68
68
|
}
|
|
69
69
|
` +
|
|
70
|
+
subjectMaskApi("u32(pu.subjects)") +
|
|
70
71
|
CAST_API +
|
|
71
72
|
trailSlotsApi(cast.trailCount) +
|
|
72
73
|
anchorAliasWgsl(cast.alias)
|
|
@@ -111,7 +112,8 @@ struct ParticleU {
|
|
|
111
112
|
frame: u32,
|
|
112
113
|
/** The effect's evaluated influence, applied at the one output site below. */
|
|
113
114
|
weight: f32,
|
|
114
|
-
|
|
115
|
+
/** Which cast slots this effect is on, one bit each — see subjectMaskApi. */
|
|
116
|
+
subjects: f32,
|
|
115
117
|
_pad1: f32,
|
|
116
118
|
_pad2: f32,
|
|
117
119
|
}
|
|
@@ -4,7 +4,7 @@ import { audioApi } from "../audio-api"
|
|
|
4
4
|
import { lyricsApi } from "../lyrics-api"
|
|
5
5
|
import { anchorAliasWgsl, ribbonSlotWgsl } 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"
|
|
@@ -94,6 +94,7 @@ export function buildTrailShader(
|
|
|
94
94
|
},
|
|
95
95
|
): string {
|
|
96
96
|
return (
|
|
97
|
+
subjectMaskApi("u32(tu.mask)") +
|
|
97
98
|
CAST_API +
|
|
98
99
|
trailSlotsApi(src.slots) +
|
|
99
100
|
EFFECT_MATH_API +
|
|
@@ -129,17 +130,21 @@ struct CameraU {
|
|
|
129
130
|
}
|
|
130
131
|
struct TrailU {
|
|
131
132
|
time: f32,
|
|
132
|
-
// How many subjects
|
|
133
|
-
//
|
|
134
|
-
// decoded by it here, and the two must use the same number or the
|
|
135
|
-
// [ribbon][subject][segment] index lands on the wrong ribbon. It is
|
|
136
|
-
// rather than the RZ_SUBJECTS constant precisely because it changes
|
|
137
|
-
// model is added
|
|
138
|
-
// would be absurd.
|
|
133
|
+
// How many subjects THIS EFFECT ribbons this frame, not the four the layout
|
|
134
|
+
// has room for and not the whole cast. The instance count is sized by this on
|
|
135
|
+
// the CPU and decoded by it here, and the two must use the same number or the
|
|
136
|
+
// flattened [ribbon][subject][segment] index lands on the wrong ribbon. It is
|
|
137
|
+
// a uniform rather than the RZ_SUBJECTS constant precisely because it changes
|
|
138
|
+
// when a model is added, removed, hidden or taken off this effect's list, and
|
|
139
|
+
// recompiling every trail shader for that would be absurd.
|
|
140
|
+
//
|
|
141
|
+
// The subject it decodes is the effect's OWN index, which is what rzTrail
|
|
142
|
+
// takes — so a ribbon aimed at one dancer draws one ribbon, on her.
|
|
139
143
|
subjects: f32,
|
|
140
144
|
/** The effect's evaluated influence, applied at the one output site below. */
|
|
141
145
|
weight: f32,
|
|
142
|
-
|
|
146
|
+
/** Which cast slots this effect is on, one bit each — see subjectMaskApi. */
|
|
147
|
+
mask: f32,
|
|
143
148
|
}
|
|
144
149
|
${src.paramsDecl}
|
|
145
150
|
@group(0) @binding(0) var<storage, read> _rzCast: array<vec4f>;
|
|
@@ -163,7 +168,7 @@ fn rzProject(p: vec3f) -> vec3f {
|
|
|
163
168
|
return vec3f(clip.xy / w * 0.5 + 0.5, clip.w);
|
|
164
169
|
}
|
|
165
170
|
fn rzCamPos() -> vec3f { return cam.camPos; }
|
|
166
|
-
fn
|
|
171
|
+
fn _rzCastLive() -> i32 {
|
|
167
172
|
var n = 0;
|
|
168
173
|
for (var i = 0; i < RZ_SUBJECTS; i++) {
|
|
169
174
|
if (_rzCast[i * ${EFFECT_SUBJECT_VEC4S} + 2].w > 0.0) { n = i + 1; }
|