reze-engine 0.50.3 → 0.50.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/engine.d.ts +237 -2
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +793 -64
  4. package/dist/physics/autofit.d.ts +147 -0
  5. package/dist/physics/autofit.d.ts.map +1 -0
  6. package/dist/physics/autofit.js +501 -0
  7. package/dist/shaders/cast-api.d.ts +1 -1
  8. package/dist/shaders/cast-api.d.ts.map +1 -1
  9. package/dist/shaders/cast-layout.d.ts +44 -1
  10. package/dist/shaders/cast-layout.d.ts.map +1 -1
  11. package/dist/shaders/cast-layout.js +44 -1
  12. package/dist/shaders/materials/common.d.ts.map +1 -1
  13. package/dist/shaders/materials/common.js +7 -1
  14. package/dist/shaders/materials/nodes.d.ts +1 -1
  15. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  16. package/dist/shaders/materials/nodes.js +17 -9
  17. package/dist/shaders/passes/composite.d.ts +1 -1
  18. package/dist/shaders/passes/composite.d.ts.map +1 -1
  19. package/dist/shaders/passes/depth-prepass.d.ts +1 -1
  20. package/dist/shaders/passes/depth-prepass.d.ts.map +1 -1
  21. package/dist/shaders/passes/depth-prepass.js +52 -12
  22. package/dist/shaders/passes/field-blit.d.ts +26 -0
  23. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  24. package/dist/shaders/passes/field-blit.js +65 -0
  25. package/dist/shaders/passes/ground-noise.d.ts +7 -0
  26. package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
  27. package/dist/shaders/passes/ground-noise.js +88 -0
  28. package/dist/shaders/passes/ground.d.ts +16 -0
  29. package/dist/shaders/passes/ground.d.ts.map +1 -1
  30. package/dist/shaders/passes/ground.js +131 -27
  31. package/dist/shaders/passes/outline.d.ts +1 -1
  32. package/dist/shaders/passes/outline.d.ts.map +1 -1
  33. package/dist/shaders/passes/outline.js +12 -3
  34. package/dist/shaders/passes/particles.d.ts.map +1 -1
  35. package/dist/shaders/passes/particles.js +6 -2
  36. package/dist/shaders/passes/scene-contract.d.ts +38 -6
  37. package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
  38. package/dist/shaders/passes/scene-contract.js +53 -16
  39. package/dist/shaders/passes/sim.d.ts +34 -0
  40. package/dist/shaders/passes/sim.d.ts.map +1 -0
  41. package/dist/shaders/passes/sim.js +169 -0
  42. package/dist/shaders/passes/trails.d.ts.map +1 -1
  43. package/dist/shaders/passes/trails.js +36 -8
  44. package/dist/shaders/score-api.d.ts +10 -0
  45. package/dist/shaders/score-api.d.ts.map +1 -0
  46. package/dist/shaders/score-api.js +114 -0
  47. package/package.json +1 -1
  48. package/src/engine.ts +839 -56
  49. package/src/shaders/cast-layout.ts +44 -1
  50. package/src/shaders/materials/common.ts +7 -1
  51. package/src/shaders/materials/nodes.ts +17 -9
  52. package/src/shaders/passes/depth-prepass.ts +53 -12
  53. package/src/shaders/passes/ground.ts +133 -27
  54. package/src/shaders/passes/outline.ts +14 -3
  55. package/src/shaders/passes/particles.ts +6 -2
  56. package/src/shaders/passes/scene-contract.ts +55 -16
  57. package/src/shaders/passes/trails.ts +36 -8
  58. package/dist/physics-debug.d.ts +0 -30
  59. package/dist/physics-debug.d.ts.map +0 -1
  60. package/dist/physics-debug.js +0 -526
  61. package/dist/shaders/materials/body.d.ts +0 -2
  62. package/dist/shaders/materials/body.d.ts.map +0 -1
  63. package/dist/shaders/materials/body.js +0 -95
  64. package/dist/shaders/materials/cloth_rough.d.ts +0 -2
  65. package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
  66. package/dist/shaders/materials/cloth_rough.js +0 -69
  67. package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
  68. package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
  69. package/dist/shaders/materials/cloth_smooth.js +0 -61
  70. package/dist/shaders/materials/default.d.ts +0 -2
  71. package/dist/shaders/materials/default.d.ts.map +0 -1
  72. package/dist/shaders/materials/default.js +0 -43
  73. package/dist/shaders/materials/eye.d.ts +0 -2
  74. package/dist/shaders/materials/eye.d.ts.map +0 -1
  75. package/dist/shaders/materials/eye.js +0 -60
  76. package/dist/shaders/materials/face.d.ts +0 -2
  77. package/dist/shaders/materials/face.d.ts.map +0 -1
  78. package/dist/shaders/materials/face.js +0 -95
  79. package/dist/shaders/materials/hair.d.ts +0 -2
  80. package/dist/shaders/materials/hair.d.ts.map +0 -1
  81. package/dist/shaders/materials/hair.js +0 -90
  82. package/dist/shaders/materials/metal.d.ts +0 -2
  83. package/dist/shaders/materials/metal.d.ts.map +0 -1
  84. package/dist/shaders/materials/metal.js +0 -77
  85. package/dist/shaders/materials/mmd_classic.d.ts +0 -2
  86. package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
  87. package/dist/shaders/materials/mmd_classic.js +0 -66
  88. package/dist/shaders/materials/stockings.d.ts +0 -2
  89. package/dist/shaders/materials/stockings.d.ts.map +0 -1
  90. package/dist/shaders/materials/stockings.js +0 -122
  91. package/dist/shaders/passes/physics-debug.d.ts +0 -2
  92. package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
  93. package/dist/shaders/passes/physics-debug.js +0 -69
@@ -89,12 +89,14 @@ const ADD_PREMULTIPLIED = {
89
89
  alpha: { srcFactor: "zero", dstFactor: "one", operation: "add" },
90
90
  };
91
91
  /**
92
- * Which classes actually WRITE an id, and so gain a fragment output for it.
92
+ * Which classes write a MEANINGFUL id.
93
93
  *
94
- * Everything else keeps its shader exactly as it is and takes the id target at
95
- * writeMask 0 — legal, specified, and free. The alternative (leaving the target
96
- * off those pipelines) is not available: every pipeline in a pass must agree
97
- * with the pass's attachments.
94
+ * Not the same question as which shaders declare the output — every scene-pass
95
+ * shader declares all three now (see sceneFsOutWgsl). This set decides only
96
+ * whose id survives: writeMask 0xf here, writeMask 0 for everyone else, so the
97
+ * others compute an id nothing stores. Leaving the target off those pipelines
98
+ * is not available: every pipeline in a pass must agree with the pass's
99
+ * attachments.
98
100
  *
99
101
  * The ground is in because a mark placed by id needs the floor to have one.
100
102
  * Transparent fabric writes ids too — dissolving a dress needs the dress's own
@@ -127,11 +129,10 @@ const BLENDS = {
127
129
  */
128
130
  export function sceneTargets(cls, formats) {
129
131
  const targets = cls === "depth-prepass"
130
- ? // Format only, and writeMask 0. Note the asymmetry this leans on, which
131
- // is the same one the id target leans on below: a target the shader has
132
- // no output for is legal at writeMask 0 (gpuweb#1918), while an output
133
- // with no target is NOT governed (gpuweb#5341). This is the specified
134
- // direction, and it is the only one this file ever uses.
132
+ ? // Format only, and writeMask 0 — the pipeline writes no colour. It still
133
+ // DECLARES every output, which is the part that used to be missing; see
134
+ // the note on sceneFsOutWgsl about why writeMask 0 is not a licence to
135
+ // leave the output off.
135
136
  [
136
137
  { format: formats.hdr, writeMask: 0 },
137
138
  { format: formats.aux, writeMask: 0 },
@@ -168,12 +169,19 @@ export function sceneColorFormats(formats) {
168
169
  * author. The struct and the targets above have to agree on count and order,
169
170
  * and they now disagree in one file rather than in five.
170
171
  *
171
- * Only the shaders that will GAIN an output take this today — the materials
172
- * (hand-written and graph-generated alike, through COMMON_FS_OUT_WGSL) and the
173
- * ground. Outline, particles and ribbons keep their own declarations on
174
- * purpose: they never write the id, so they would take an `id: false` argument
175
- * forever, and their structs carry comments about their own blend that belong
176
- * where they are.
172
+ * EVERY scene-pass shader takes this — outline, particles, ribbons and the
173
+ * depth prepass included, none of which write a meaningful id (and the prepass
174
+ * no meaningful colour either). They did not, once. The reasoning was that a
175
+ * target at writeMask 0 needs no matching output: true of Dawn, and the reading
176
+ * of gpuweb#1918 this file used to assert. It is not a reading every browser
177
+ * shares, and the failure mode when a browser disagrees is the worst kind —
178
+ * createRenderPipeline does not throw, it returns a pipeline that is already
179
+ * invalid, and the pass that binds it is dropped whole. Missing geometry, clean
180
+ * console, and a bug that reproduces on one vendor's browser only.
181
+ *
182
+ * So the rule is now the strict one, and it costs nothing to hold: declare
183
+ * every output the pass has attachments for, and let writeMask decide what is
184
+ * kept. A shader with no id to write pads it — see sceneIdPadWgsl.
177
185
  */
178
186
  export function sceneFsOutWgsl(opts) {
179
187
  const name = opts?.name ?? "FSOut";
@@ -205,3 +213,32 @@ ${id}};
205
213
  export function sceneIdWriteWgsl(out, material, object) {
206
214
  return mrtIds ? ` ${out}.id = vec2u(${material}, ${object});\n` : "";
207
215
  }
216
+ /**
217
+ * Just the id FIELD, for a shader that keeps its own struct.
218
+ *
219
+ * Particles and ribbons declare their outputs by hand because the comments on
220
+ * their aux field are about their own blend and belong beside it — taking the
221
+ * whole struct from sceneFsOutWgsl would move that prose away from what it
222
+ * explains. They still need the id output when the pass has the attachment, so
223
+ * they splice this in and pad it with sceneIdPadWgsl.
224
+ */
225
+ export function sceneIdFieldWgsl() {
226
+ return mrtIds ? ` @location(2) id: vec2u,\n` : "";
227
+ }
228
+ /**
229
+ * The id assignment for a shader that has no id to give.
230
+ *
231
+ * The counterpart to the strict rule on sceneFsOutWgsl: outline hulls,
232
+ * particles, ribbons and the depth prepass all declare the output because the
233
+ * pass has the attachment, and all of them take writeMask 0, so what they
234
+ * assign is never stored. Zero, which is the reserved "nothing" id the pass
235
+ * clears to — so if one of these ever did reach the target, it would read as
236
+ * absence rather than as a plausible wrong answer pointing at material 0.
237
+ *
238
+ * A separate function from sceneIdWriteWgsl rather than a call to it with "0u"
239
+ * twice, because the two say different things: that one records an identity,
240
+ * this one satisfies a declaration.
241
+ */
242
+ export function sceneIdPadWgsl(out) {
243
+ return mrtIds ? ` ${out}.id = vec2u(0u, 0u);\n` : "";
244
+ }
@@ -0,0 +1,34 @@
1
+ import { type CastLayout } from "./particles";
2
+ export declare const SIM_MAX = 1024;
3
+ /** rgba16float, ping-ponged. Two of these is the whole memory cost. */
4
+ export declare const SIM_FORMAT: GPUTextureFormat;
5
+ /** `// @sim 256` — the grid's resolution, in texels per side. */
6
+ export declare function parseSimSize(wgsl: string, max: number): number;
7
+ /** Whether this effect drives a grid at all. */
8
+ export declare function simEntryPoint(wgsl: string): boolean;
9
+ /**
10
+ * `rzSim(uv)`, for a shader that READS the grid rather than steps it.
11
+ *
12
+ * Always compiled in, even for an effect with no grid — it then samples a 1×1
13
+ * of zeroes. An accessor that exists only sometimes is one an author has to
14
+ * guard, and a missing function is a compile error rather than a blank result.
15
+ */
16
+ export declare function simReadApi(group: number, tex: number, samp: number, size: number): string;
17
+ /** `rzTime`/`rzDt`, for a module that has no clock of its own — the field pass.
18
+ * Same reason as above: a spliced simStep refers to them. */
19
+ export declare function simClockApi(timeExpr: string): string;
20
+ /**
21
+ * The step shader: one invocation per texel, once per frame.
22
+ *
23
+ * The previous grid is bound as a SAMPLED texture and the next as a storage
24
+ * texture, which is what makes the two different resources and the whole thing
25
+ * legal — a shader cannot read and write one texture coherently, and the
26
+ * ping-pong is not an optimisation but the only correct way to do this.
27
+ *
28
+ * Sampled rather than only fetched, because advection needs to read BETWEEN
29
+ * texels: a semi-Lagrangian step asks "what was at the place this parcel came
30
+ * from", and that place is almost never a texel centre. Point-sampling it is
31
+ * what turns smoke into a staircase.
32
+ */
33
+ export declare function buildSimShader(wgsl: string, size: number, _cast: CastLayout): string;
34
+ //# sourceMappingURL=sim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sim.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/sim.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAgC7C,eAAO,MAAM,OAAO,OAAO,CAAA;AAC3B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9D;AAED,gDAAgD;AAChD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6BzF;AAED;8DAC8D;AAC9D,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKpD;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAwDpF"}
@@ -0,0 +1,169 @@
1
+ import { audioApi } from "../audio-api";
2
+ import { scoreApi } from "../score-api";
3
+ import { EFFECT_SCENE_API } from "./composite";
4
+ // A persistent grid an effect can step and read: the one thing an effect could
5
+ // not have before, which is MEMORY.
6
+ //
7
+ // Every other mount is a pure function of position and time. A field shader
8
+ // recomputes its noise from scratch every frame; a trail reads history the
9
+ // engine happens to keep. Neither can build on what it produced last frame, and
10
+ // a whole class of effect is nothing but that:
11
+ //
12
+ // Fog that a dancer walks through does not "look displaced near her" — it IS
13
+ // displaced, and stays displaced, and creeps back over seconds. Water does not
14
+ // look rippled where a foot landed; a ripple leaves and propagates and
15
+ // reflects off the far edge long after the foot is gone. Both are the same
16
+ // shape of thing — a grid, stepped by its own previous value.
17
+ //
18
+ // It is also the only way to get the LOOK of a fluid, not merely its motion.
19
+ // Advection compounds: each frame's field is the last one pushed sideways, and a
20
+ // few hundred frames of that stretches and folds a smooth field into filaments
21
+ // and vortex sheets finer than the grid storing them. No noise function
22
+ // produces that, at any octave count, because the structure is not a function of
23
+ // position at all — it is a function of history.
24
+ //
25
+ // The engine owns the ping-pong, the dispatch and the sampling. The author owns
26
+ // the KERNEL, which is the part that differs: advection for smoke, the wave
27
+ // equation for water, a decaying stamp for footprints. That split is the same
28
+ // one the particle mounts make, and for the same reason — the loop is identical
29
+ // in every such effect and the physics never is.
30
+ // A megatexel of rgba16float is 8MB, and two of those is the whole cost. What
31
+ // actually bounds this is the STEP: one invocation per texel per frame, each
32
+ // taking a dozen samples, so doubling the side quadruples the work.
33
+ export const SIM_MAX = 1024;
34
+ /** rgba16float, ping-ponged. Two of these is the whole memory cost. */
35
+ export const SIM_FORMAT = "rgba16float";
36
+ /** `// @sim 256` — the grid's resolution, in texels per side. */
37
+ export function parseSimSize(wgsl, max) {
38
+ const m = /^\s*\/\/\s*@sim\s+(\d+)\s*$/m.exec(wgsl);
39
+ if (!m)
40
+ return 0;
41
+ // Clamped rather than rejected, as the particle count is: an author asking for
42
+ // 4096 gets the most the engine will give and a scene that still runs.
43
+ return Math.max(8, Math.min(max, parseInt(m[1], 10)));
44
+ }
45
+ /** Whether this effect drives a grid at all. */
46
+ export function simEntryPoint(wgsl) {
47
+ return /\bfn\s+simStep\s*\(/.test(wgsl);
48
+ }
49
+ /**
50
+ * `rzSim(uv)`, for a shader that READS the grid rather than steps it.
51
+ *
52
+ * Always compiled in, even for an effect with no grid — it then samples a 1×1
53
+ * of zeroes. An accessor that exists only sometimes is one an author has to
54
+ * guard, and a missing function is a compile error rather than a blank result.
55
+ */
56
+ export function simReadApi(group, tex, samp, size) {
57
+ return /* wgsl */ `
58
+ const RZ_SIM_SIZE: f32 = ${size > 0 ? size : 1}.0;
59
+ @group(${group}) @binding(${tex}) var _rzSimTex: texture_2d<f32>;
60
+ @group(${group}) @binding(${samp}) var _rzSimSamp: sampler;
61
+
62
+ /**
63
+ * The grid, bilinearly sampled. uv is 0..1 across it, and what that MEANS in
64
+ * the world is the effect's own business — the engine deliberately does not
65
+ * impose a mapping, because a fog laid over a stage and a ripple field around a
66
+ * character want different ones and both are two lines of arithmetic.
67
+ */
68
+ fn rzSim(uv: vec2f) -> vec4f {
69
+ return textureSampleLevel(_rzSimTex, _rzSimSamp, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0);
70
+ }
71
+ fn rzSimSize() -> f32 { return RZ_SIM_SIZE; }
72
+ fn rzSimTexel() -> f32 { return 1.0 / RZ_SIM_SIZE; }
73
+
74
+ // ── The step-only half, defined here too and never called here ──
75
+ //
76
+ // One effect file is spliced into EVERY module it has a mount in, so a file
77
+ // with a grid compiles its simStep inside the field and particle shaders as
78
+ // well — where it is dead code that nothing calls, but still code that has to
79
+ // resolve. Leaving these out is a compile error on a function the author was
80
+ // right to write. rzSimPrev reads the current grid rather than a previous one,
81
+ // which is the honest answer outside the step: there is no previous frame here.
82
+ fn rzSimPrev(uv: vec2f) -> vec4f { return rzSim(uv); }
83
+ fn rzSimFrame() -> i32 { return 1; }
84
+ `;
85
+ }
86
+ /** `rzTime`/`rzDt`, for a module that has no clock of its own — the field pass.
87
+ * Same reason as above: a spliced simStep refers to them. */
88
+ export function simClockApi(timeExpr) {
89
+ return /* wgsl */ `
90
+ fn rzTime() -> f32 { return ${timeExpr}; }
91
+ fn rzDt() -> f32 { return 0.0; }
92
+ `;
93
+ }
94
+ const SIM_UNIFORMS = /* wgsl */ `
95
+ struct SimU {
96
+ time: f32,
97
+ dt: f32,
98
+ size: f32,
99
+ frame: f32,
100
+ }
101
+ `;
102
+ /**
103
+ * The step shader: one invocation per texel, once per frame.
104
+ *
105
+ * The previous grid is bound as a SAMPLED texture and the next as a storage
106
+ * texture, which is what makes the two different resources and the whole thing
107
+ * legal — a shader cannot read and write one texture coherently, and the
108
+ * ping-pong is not an optimisation but the only correct way to do this.
109
+ *
110
+ * Sampled rather than only fetched, because advection needs to read BETWEEN
111
+ * texels: a semi-Lagrangian step asks "what was at the place this parcel came
112
+ * from", and that place is almost never a texel centre. Point-sampling it is
113
+ * what turns smoke into a staircase.
114
+ */
115
+ export function buildSimShader(wgsl, size, _cast) {
116
+ return (SIM_UNIFORMS +
117
+ /* wgsl */ `
118
+ @group(0) @binding(0) var<uniform> su: SimU;
119
+ @group(0) @binding(1) var _rzSimPrevTex: texture_2d<f32>;
120
+ @group(0) @binding(2) var _rzSimSamp: sampler;
121
+ @group(0) @binding(3) var _rzSimOut: texture_storage_2d<${SIM_FORMAT}, write>;
122
+ @group(0) @binding(4) var<storage, read> _rzCast: array<vec4f>;
123
+ // The same view uniform the composite reads, so the scene API below is the real
124
+ // thing here rather than a stub — a kernel can ask where a bone is, and a
125
+ // foreground spliced into this module resolves its camera calls.
126
+ @group(0) @binding(6) var<uniform> viewU: array<vec4<f32>, 15>;
127
+
128
+ fn rzTime() -> f32 { return su.time; }
129
+ fn rzDt() -> f32 { return su.dt; }
130
+ /** Texels per side. */
131
+ fn rzSimSize() -> f32 { return su.size; }
132
+ /** One texel, in uv — the step a neighbour lookup takes. */
133
+ fn rzSimTexel() -> f32 { return 1.0 / su.size; }
134
+ /**
135
+ * Steps since this effect was installed. Zero on the very first one, which is
136
+ * the only chance to seed a grid: everything after it builds on what is there.
137
+ */
138
+ fn rzSimFrame() -> i32 { return i32(su.frame); }
139
+
140
+ /** The grid as it was LAST frame, bilinear. The only input a kernel really has. */
141
+ fn rzSimPrev(uv: vec2f) -> vec4f {
142
+ return textureSampleLevel(_rzSimPrevTex, _rzSimSamp, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0);
143
+ }
144
+ ` +
145
+ EFFECT_SCENE_API +
146
+ audioApi(0, 5) +
147
+ scoreApi(0, 7) +
148
+ // rzSim itself, so a kernel may read the grid it is writing — through the
149
+ // PREVIOUS frame's texture, which is the only version of it that exists
150
+ // while the current one is still being written.
151
+ /* wgsl */ `
152
+ fn rzSim(uv: vec2f) -> vec4f { return rzSimPrev(uv); }
153
+ ` +
154
+ "\n// ── user effect (setEffect) ──\n" +
155
+ wgsl +
156
+ /* wgsl */ `
157
+
158
+ @compute @workgroup_size(8, 8)
159
+ fn main(@builtin(global_invocation_id) gid: vec3u) {
160
+ let n = u32(${size});
161
+ if (gid.x >= n || gid.y >= n) { return; }
162
+ let xy = vec2i(i32(gid.x), i32(gid.y));
163
+ // The texel CENTRE, so a kernel that reads its own uv back through rzSimPrev
164
+ // lands on itself rather than a quarter-texel off.
165
+ let uv = (vec2f(f32(gid.x), f32(gid.y)) + vec2f(0.5)) / su.size;
166
+ textureStore(_rzSimOut, xy, simStep(uv, textureLoad(_rzSimPrevTex, xy, 0), su.dt));
167
+ }
168
+ `);
169
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AAwBA,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,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,CA6SR"}
1
+ {"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AAyBA,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,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,CAwUR"}
@@ -5,6 +5,7 @@ import { anchorAliasWgsl, ribbonSlotWgsl } from "../anchor-table";
5
5
  import { midiApi } from "../midi-api";
6
6
  import { CAST_API } from "../cast-api";
7
7
  import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
8
+ import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract";
8
9
  /**
9
10
  * Sub-segments drawn between each pair of recorded samples.
10
11
  *
@@ -74,7 +75,14 @@ struct CameraU {
74
75
  }
75
76
  struct TrailU {
76
77
  time: f32,
77
- _pad0: f32,
78
+ // How many subjects the cast ACTUALLY holds this frame, not the four the
79
+ // layout has room for. The instance count is sized by this on the CPU and
80
+ // decoded by it here, and the two must use the same number or the flattened
81
+ // [ribbon][subject][segment] index lands on the wrong ribbon. It is a uniform
82
+ // rather than the RZ_SUBJECTS constant precisely because it changes when a
83
+ // model is added or removed, and recompiling every trail shader for that
84
+ // would be absurd.
85
+ subjects: f32,
78
86
  _pad1: f32,
79
87
  _pad2: f32,
80
88
  }
@@ -195,11 +203,14 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
195
203
  let rest = ii / subs;
196
204
  let seg = k / SUB;
197
205
  let sub = k % SUB;
198
- let subject = i32(rest % u32(RZ_SUBJECTS));
206
+ // The LIVE subject count, floored at one so a cast that is momentarily empty
207
+ // divides by something. See TrailU.subjects.
208
+ let live = max(1u, u32(tu.subjects));
209
+ let subject = i32(rest % live);
199
210
  // Instance index counts RIBBONS; the cast buffer is addressed by anchor slot.
200
211
  // One ribbon per trailed anchor, so these are the same number only when every
201
212
  // anchor is trailed — _rzRibbonSlot is what bridges them.
202
- let ribbon = i32(rest / u32(RZ_SUBJECTS));
213
+ let ribbon = i32(rest / live);
203
214
  let slot = _rzRibbonSlot(ribbon);
204
215
 
205
216
  let n = rzTrailCount(subject, slot);
@@ -293,9 +304,23 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
293
304
  if (dot(perpA, perpB) < 0.0) { perpB = -perpB; }
294
305
  let clipP = select(clipA, clipB, atEnd);
295
306
  let perp = select(perpA, perpB, atEnd);
296
- // trailWidth speaks PIXELS, like the original's constants; rzViewportHeight()
297
- // lets the effect scale them to the frame.
298
- let wPx = max(0.0, trailWidth(u, age));
307
+ // trailWidth speaks WORLD UNITS — the model's units, not the screen's.
308
+ //
309
+ // It spoke pixels, like the fullscreen original's constants, and that made
310
+ // the ribbon a screen ornament: zoom out and it grew relative to the hand
311
+ // that drew it, zoom in and it thinned to a thread. A ribbon is a thing in
312
+ // the scene, so its width belongs in the scene's units and should foreshorten
313
+ // like everything else.
314
+ //
315
+ // The EXTRUSION stays in 2D pixel space untouched — every 3D-extrusion
316
+ // artefact this ribbon ever had (fold-over wedges, bowties, twisted quads) is
317
+ // recorded above, and none of it comes back. Only the width's SOURCE changes:
318
+ // a world length L perpendicular to view at depth w projects to
319
+ // L * proj[1][1] / w in NDC y, so half the frame height converts it to the
320
+ // pixels the 2D offset wants. Per vertex, so a ribbon receding along its own
321
+ // length tapers with distance exactly as geometry would.
322
+ let wWorld = max(0.0, trailWidth(u, age));
323
+ let wPx = wWorld * cam.proj[1][1] * (H * 0.5) / max(clipP.w, 0.01);
299
324
  let ndc = clipP.xy / clipP.w + perp * (c.y * wPx) / toPx;
300
325
  out.clip = vec4f(ndc * clipP.w, clipP.z, clipP.w);
301
326
  out.slot = u32(slot);
@@ -315,7 +340,10 @@ struct TrailFSOut {
315
340
  // The scene's aux target: (bloom mask, coverage). Writing mask 1 is what
316
341
  // makes a ribbon BLOOM — it is light, and the gate is what says so.
317
342
  @location(1) aux: vec4f,
318
- }
343
+ // Declared whenever the pass carries the attachment, and padded rather than
344
+ // written: a ribbon is not a thing you would address by id, so its pipeline
345
+ // takes this target at writeMask 0. Declared anyway — see sceneFsOutWgsl.
346
+ ${sceneIdFieldWgsl()}}
319
347
 
320
348
  @fragment
321
349
  fn fs(in: VSOut) -> TrailFSOut {
@@ -334,7 +362,7 @@ fn fs(in: VSOut) -> TrailFSOut {
334
362
  // mapper is what keeps the sum from clipping.
335
363
  o.color = vec4f(c.rgb, c.a);
336
364
  o.aux = vec4f(${src.bloom ? "1.0" : "0.0"}, 1.0, 0.0, c.a);
337
- return o;
365
+ ${sceneIdPadWgsl("o")} return o;
338
366
  }
339
367
  `);
340
368
  }
@@ -0,0 +1,10 @@
1
+ /** Floats before the key map: count, pitch range, clock, duration, release. */
2
+ export declare const SCORE_HEADER = 8;
3
+ /** One energy slot per MIDI pitch. 128 is the MIDI range, not a cap we chose. */
4
+ export declare const SCORE_KEYS = 128;
5
+ /** Where the note records begin. */
6
+ export declare const SCORE_NOTES: number;
7
+ /** Floats per note: start, duration, pitch, velocity. */
8
+ export declare const SCORE_STRIDE = 4;
9
+ /** The rzNote*/ export declare function scoreApi(group: number, binding: number): string;
10
+ //# sourceMappingURL=score-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"score-api.d.ts","sourceRoot":"","sources":["../../src/shaders/score-api.ts"],"names":[],"mappings":"AA8BA,+EAA+E;AAC/E,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,iFAAiF;AACjF,eAAO,MAAM,UAAU,MAAM,CAAA;AAC7B,oCAAoC;AACpC,eAAO,MAAM,WAAW,QAA4B,CAAA;AACpD,yDAAyD;AACzD,eAAO,MAAM,YAAY,IAAI,CAAA;AAE7B,gBAAgB,CAChB,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA2E/D"}
@@ -0,0 +1,114 @@
1
+ // The score data interface, as WGSL — shared verbatim by every effect module.
2
+ //
3
+ // The sibling of the audio interface, and precomputed for the same reason: an
4
+ // export steps the engine frame by frame rather than playing in real time, so
5
+ // anything read live from a player would render empty into the exported video.
6
+ // A note list is inherently precomputed, so it cannot even be tempted.
7
+ //
8
+ // WHY THIS EXISTS BESIDE rzAudio* RATHER THAN INSTEAD OF IT. The audio interface
9
+ // answers "how loud is the bass right now" — a spectrum, smeared across
10
+ // frequency and time. That is the right shape for a reactive backdrop and the
11
+ // wrong shape for a note: you cannot recover a discrete onset, a pitch and a
12
+ // duration from band energy, and the crispness of a falling note is the whole
13
+ // point of the look. The two are complementary — a scene can pulse its fog on
14
+ // rzAudioOnset while its notes fall on rzNoteAge.
15
+ //
16
+ // LAYOUT. An 8-float header, then a 128-float live key map, then the notes at 4
17
+ // floats each (start, duration, pitch, velocity). `time` and `playing` are
18
+ // rewritten every frame by whoever owns playback, exactly as the audio header
19
+ // is, so "now" is always the clock actually running.
20
+ //
21
+ // THE KEY MAP IS THE POINT OF THE DESIGN. Falling notes index the note list
22
+ // directly — one particle per note, which is what makes the geometry trivial.
23
+ // But a keyboard glow asks the opposite question, per pixel: is anything
24
+ // sounding at THIS pitch? Answering that by scanning the note list would be
25
+ // thousands of iterations per fragment. So the engine keeps a 128-entry
26
+ // per-pitch energy map, updated once per frame on the CPU, and the glow becomes
27
+ // one lookup — the same trade the audio interface makes by precomputing onset
28
+ // so "a beat-triggered effect is one comparison instead of a per-pixel history
29
+ // scan".
30
+ /** Floats before the key map: count, pitch range, clock, duration, release. */
31
+ export const SCORE_HEADER = 8;
32
+ /** One energy slot per MIDI pitch. 128 is the MIDI range, not a cap we chose. */
33
+ export const SCORE_KEYS = 128;
34
+ /** Where the note records begin. */
35
+ export const SCORE_NOTES = SCORE_HEADER + SCORE_KEYS;
36
+ /** Floats per note: start, duration, pitch, velocity. */
37
+ export const SCORE_STRIDE = 4;
38
+ /** The rzNote*/ /* rzKey* accessors, with the buffer declared at the given binding. */
39
+ export function scoreApi(group, binding) {
40
+ return /* wgsl */ `
41
+ @group(${group}) @binding(${binding}) var<storage, read> _rzScore: array<f32>;
42
+
43
+ /** Notes in the score; 0 when none is loaded, which every accessor below
44
+ * tolerates by returning zero rather than reading past the end. */
45
+ fn rzNoteCount() -> i32 { return i32(_rzScore[0]); }
46
+ /** Where the score is NOW, in seconds — the clock every age below hangs from. */
47
+ fn rzScoreTime() -> f32 { return _rzScore[3]; }
48
+ /** 1 while the score is advancing, 0 paused or absent. */
49
+ fn rzScorePlaying() -> f32 { return select(0.0, _rzScore[4], i32(_rzScore[0]) > 0); }
50
+ /** Last note-off, in seconds — the length of the piece. */
51
+ fn rzScoreDuration() -> f32 { return _rzScore[5]; }
52
+ /** Lowest and highest pitch the score actually uses. Lay a keyboard out against
53
+ * these rather than against 0..127 and a piano piece fills the screen instead
54
+ * of occupying its middle third. */
55
+ fn rzPitchLow() -> f32 { return _rzScore[1]; }
56
+ fn rzPitchHigh() -> f32 { return _rzScore[2]; }
57
+
58
+ fn _rzNoteAt(i: i32, field: i32) -> f32 {
59
+ if (i < 0 || i >= i32(_rzScore[0])) { return 0.0; }
60
+ return _rzScore[${SCORE_NOTES} + i * ${SCORE_STRIDE} + field];
61
+ }
62
+
63
+ /** When note i begins, in seconds. */
64
+ fn rzNoteStart(i: i32) -> f32 { return _rzNoteAt(i, 0); }
65
+ /** How long note i sounds, in seconds. */
66
+ fn rzNoteLength(i: i32) -> f32 { return _rzNoteAt(i, 1); }
67
+ /** MIDI pitch of note i — 60 is middle C. */
68
+ fn rzNotePitch(i: i32) -> f32 { return _rzNoteAt(i, 2); }
69
+ /** How hard note i was struck, 0..1. */
70
+ fn rzNoteVelocity(i: i32) -> f32 { return _rzNoteAt(i, 3); }
71
+
72
+ /**
73
+ * Seconds since note i began — NEGATIVE before it does.
74
+ *
75
+ * The falling-note function, and the sign is the whole of it: a note's height is
76
+ * -rzNoteAge(i) * speed, so a note not yet played sits above the keyboard at a
77
+ * distance proportional to how long is left, crosses zero exactly as it sounds,
78
+ * and keeps going. One expression, no state, no spawning — which is why one
79
+ * particle per note is the natural mapping and the pool index IS the note index.
80
+ */
81
+ fn rzNoteAge(i: i32) -> f32 { return _rzScore[3] - rzNoteStart(i); }
82
+
83
+ /** 1 while note i is sounding, 0 either side of it. */
84
+ fn rzNoteHeld(i: i32) -> f32 {
85
+ let age = rzNoteAge(i);
86
+ return select(0.0, 1.0, age >= 0.0 && age < rzNoteLength(i));
87
+ }
88
+
89
+ /**
90
+ * Energy at a MIDI pitch right now: 1 the instant it is struck, holding while
91
+ * sounding, then falling away over the release set at install time.
92
+ *
93
+ * Per PITCH rather than per note, because that is the question a keyboard asks
94
+ * and a note list cannot answer cheaply — see the header. Fractional pitches
95
+ * read the nearest key, so a caller can hand this a continuous x across the
96
+ * keyboard without rounding first.
97
+ */
98
+ fn rzKeyEnergy(pitch: f32) -> f32 {
99
+ let k = i32(round(pitch));
100
+ if (k < 0 || k >= ${SCORE_KEYS}) { return 0.0; }
101
+ return _rzScore[${SCORE_HEADER} + k];
102
+ }
103
+
104
+ /**
105
+ * A pitch's place across the score's own range, 0..1 — the x of a falling note
106
+ * and of the key it lands on, which have to agree or the effect is nonsense.
107
+ */
108
+ fn rzPitchX(pitch: f32) -> f32 {
109
+ let lo = _rzScore[1];
110
+ let hi = _rzScore[2];
111
+ return select(0.5, (pitch - lo) / max(hi - lo, 1.0), hi > lo);
112
+ }
113
+ `;
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.50.3",
3
+ "version": "0.50.4",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",