reze-engine 0.42.3 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +40 -410
  2. package/dist/camera.d.ts +3 -0
  3. package/dist/camera.d.ts.map +1 -1
  4. package/dist/camera.js +33 -8
  5. package/dist/engine.d.ts +924 -29
  6. package/dist/engine.d.ts.map +1 -1
  7. package/dist/engine.js +4043 -278
  8. package/dist/graph/registry.d.ts +2 -2
  9. package/dist/graph/registry.d.ts.map +1 -1
  10. package/dist/graph/registry.js +1 -1
  11. package/dist/graph/slots.d.ts +0 -1
  12. package/dist/graph/slots.d.ts.map +1 -1
  13. package/dist/graph/slots.js +37 -9
  14. package/dist/hdr.d.ts +18 -0
  15. package/dist/hdr.d.ts.map +1 -0
  16. package/dist/hdr.js +162 -0
  17. package/dist/ibl.d.ts +19 -0
  18. package/dist/ibl.d.ts.map +1 -0
  19. package/dist/ibl.js +113 -0
  20. package/dist/ik-solver.d.ts +2 -1
  21. package/dist/ik-solver.d.ts.map +1 -1
  22. package/dist/index.d.ts +5 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +10 -0
  25. package/dist/math.d.ts +20 -1
  26. package/dist/math.d.ts.map +1 -1
  27. package/dist/math.js +23 -16
  28. package/dist/midi-loader.d.ts +10 -0
  29. package/dist/midi-loader.d.ts.map +1 -0
  30. package/dist/midi-loader.js +247 -0
  31. package/dist/model.d.ts +2 -13
  32. package/dist/model.d.ts.map +1 -1
  33. package/dist/model.js +29 -0
  34. package/dist/param-track.d.ts +48 -0
  35. package/dist/param-track.d.ts.map +1 -0
  36. package/dist/param-track.js +80 -0
  37. package/dist/physics/types.d.ts.map +1 -1
  38. package/dist/physics/types.js +3 -0
  39. package/dist/reflection.d.ts +27 -0
  40. package/dist/reflection.d.ts.map +1 -0
  41. package/dist/reflection.js +93 -0
  42. package/dist/shaders/anchor-table.d.ts +56 -0
  43. package/dist/shaders/anchor-table.d.ts.map +1 -0
  44. package/dist/shaders/anchor-table.js +128 -0
  45. package/dist/shaders/audio-api.d.ts +3 -0
  46. package/dist/shaders/audio-api.d.ts.map +1 -0
  47. package/dist/shaders/audio-api.js +81 -0
  48. package/dist/shaders/cast-api.d.ts +2 -0
  49. package/dist/shaders/cast-api.d.ts.map +1 -0
  50. package/dist/shaders/cast-api.js +121 -0
  51. package/dist/shaders/cast-layout.d.ts +21 -0
  52. package/dist/shaders/cast-layout.d.ts.map +1 -0
  53. package/dist/shaders/cast-layout.js +20 -0
  54. package/dist/shaders/lights.d.ts +79 -0
  55. package/dist/shaders/lights.d.ts.map +1 -0
  56. package/dist/shaders/lights.js +269 -0
  57. package/dist/shaders/lyrics-api.d.ts +39 -0
  58. package/dist/shaders/lyrics-api.d.ts.map +1 -0
  59. package/dist/shaders/lyrics-api.js +187 -0
  60. package/dist/shaders/materials/common.d.ts +2 -4
  61. package/dist/shaders/materials/common.d.ts.map +1 -1
  62. package/dist/shaders/materials/common.js +87 -35
  63. package/dist/shaders/midi-api.d.ts +10 -0
  64. package/dist/shaders/midi-api.d.ts.map +1 -0
  65. package/dist/shaders/midi-api.js +114 -0
  66. package/dist/shaders/passes/composite.d.ts +31 -15
  67. package/dist/shaders/passes/composite.d.ts.map +1 -1
  68. package/dist/shaders/passes/composite.js +225 -135
  69. package/dist/shaders/passes/cull.d.ts +2 -0
  70. package/dist/shaders/passes/cull.d.ts.map +1 -0
  71. package/dist/shaders/passes/cull.js +138 -0
  72. package/dist/shaders/passes/field-blit.d.ts +26 -0
  73. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  74. package/dist/shaders/passes/field-blit.js +65 -0
  75. package/dist/shaders/passes/grid.d.ts +31 -0
  76. package/dist/shaders/passes/grid.d.ts.map +1 -0
  77. package/dist/shaders/passes/grid.js +169 -0
  78. package/dist/shaders/passes/ground.d.ts +13 -1
  79. package/dist/shaders/passes/ground.d.ts.map +1 -1
  80. package/dist/shaders/passes/ground.js +170 -25
  81. package/dist/shaders/passes/hosted-api.d.ts +57 -0
  82. package/dist/shaders/passes/hosted-api.d.ts.map +1 -0
  83. package/dist/shaders/passes/hosted-api.js +166 -0
  84. package/dist/shaders/passes/id-debug.d.ts +28 -0
  85. package/dist/shaders/passes/id-debug.d.ts.map +1 -0
  86. package/dist/shaders/passes/id-debug.js +74 -0
  87. package/dist/shaders/passes/particles.d.ts +66 -0
  88. package/dist/shaders/passes/particles.d.ts.map +1 -0
  89. package/dist/shaders/passes/particles.js +279 -0
  90. package/dist/shaders/passes/scene-contract.d.ts +128 -0
  91. package/dist/shaders/passes/scene-contract.d.ts.map +1 -0
  92. package/dist/shaders/passes/scene-contract.js +207 -0
  93. package/dist/shaders/passes/sim.d.ts +34 -0
  94. package/dist/shaders/passes/sim.d.ts.map +1 -0
  95. package/dist/shaders/passes/sim.js +169 -0
  96. package/dist/shaders/passes/trails.d.ts +59 -0
  97. package/dist/shaders/passes/trails.d.ts.map +1 -0
  98. package/dist/shaders/passes/trails.js +340 -0
  99. package/dist/shaders/score-api.d.ts +10 -0
  100. package/dist/shaders/score-api.d.ts.map +1 -0
  101. package/dist/shaders/score-api.js +114 -0
  102. package/dist/shadow-cascades.d.ts +45 -0
  103. package/dist/shadow-cascades.d.ts.map +1 -0
  104. package/dist/shadow-cascades.js +70 -0
  105. package/dist/vmd-loader.d.ts +3 -2
  106. package/dist/vmd-loader.d.ts.map +1 -1
  107. package/package.json +1 -1
  108. package/src/camera.ts +31 -8
  109. package/src/engine.ts +4535 -296
  110. package/src/graph/registry.ts +2 -2
  111. package/src/graph/slots.ts +37 -9
  112. package/src/hdr.ts +156 -0
  113. package/src/ibl.ts +115 -0
  114. package/src/ik-solver.ts +1 -1
  115. package/src/index.ts +12 -0
  116. package/src/math.ts +23 -17
  117. package/src/midi-loader.ts +246 -0
  118. package/src/model.ts +31 -3
  119. package/src/param-track.ts +83 -0
  120. package/src/physics/types.ts +4 -1
  121. package/src/reflection.ts +94 -0
  122. package/src/shaders/anchor-table.ts +147 -0
  123. package/src/shaders/audio-api.ts +82 -0
  124. package/src/shaders/cast-api.ts +123 -0
  125. package/src/shaders/cast-layout.ts +20 -0
  126. package/src/shaders/lights.ts +280 -0
  127. package/src/shaders/lyrics-api.ts +202 -0
  128. package/src/shaders/materials/common.ts +89 -35
  129. package/src/shaders/midi-api.ts +116 -0
  130. package/src/shaders/passes/composite.ts +244 -136
  131. package/src/shaders/passes/cull.ts +139 -0
  132. package/src/shaders/passes/grid.ts +178 -0
  133. package/src/shaders/passes/ground.ts +172 -25
  134. package/src/shaders/passes/hosted-api.ts +171 -0
  135. package/src/shaders/passes/id-debug.ts +75 -0
  136. package/src/shaders/passes/particles.ts +340 -0
  137. package/src/shaders/passes/scene-contract.ts +266 -0
  138. package/src/shaders/passes/trails.ts +390 -0
  139. package/src/shadow-cascades.ts +97 -0
  140. package/src/vmd-loader.ts +2 -2
@@ -0,0 +1,269 @@
1
+ // Positional lights, as data — the sibling of the cast, audio and score
2
+ // interfaces, and shaped like them: one shared buffer, read through accessors,
3
+ // never touched directly.
4
+ //
5
+ // WHAT THIS IS NOT. The sun is still the ONE key light and it still owns the
6
+ // toon ramp. These are an ADDITIVE layer on top of whatever the material's
7
+ // graph decided, and they deliberately do not re-ramp: two ramped terminators
8
+ // crossing a cheek read as plastic, which is the failure every stylised
9
+ // renderer that bolted a second key light onto a toon shader has shipped. A
10
+ // light here brightens; it does not restate the shading.
11
+ //
12
+ // So there are no per-light shadows, no area lights and no clustering. At this
13
+ // count a flat loop in the fragment shader is cheaper than anything that would
14
+ // avoid it, and the cap is what keeps that true.
15
+ //
16
+ // LAYOUT. A 4-float header (count, then padding that keeps the records
17
+ // vec4-aligned), then MAX_LIGHTS records of 8 floats:
18
+ //
19
+ // [0..2] position, world space [3] radius
20
+ // [4..6] colour PREMULTIPLIED by intensity [7] type
21
+ //
22
+ // Colour carries intensity because nothing reads them apart: every use is the
23
+ // product, and storing two numbers that are only ever multiplied is two numbers
24
+ // that can disagree. `type` is reserved — every light is a point light today
25
+ // and the loop does not branch on it, so it is honest padding rather than a
26
+ // switch with one case.
27
+ import { audioApi } from "./audio-api";
28
+ import { midiApi } from "./midi-api";
29
+ import { lyricsApi } from "./lyrics-api";
30
+ import { clockApi, trailSlotsApi, viewportApi } from "./passes/hosted-api";
31
+ /** Floats before the first record. One is the count; the rest keep the records
32
+ * vec4-aligned, which is what lets a future pass read them as vec4s. */
33
+ export const LIGHT_HEADER = 4;
34
+ /** Floats per light — see the layout above. */
35
+ export const LIGHT_STRIDE = 8;
36
+ /**
37
+ * The cap, and it is a real one: the loop below runs per fragment, so this is
38
+ * the number that decides whether lights are free or a cost. Sixteen is the
39
+ * bounded middle tier the design settled on — enough for a stage rig, far below
40
+ * the point where clustering would start to pay for itself.
41
+ */
42
+ export const MAX_LIGHTS = 16;
43
+ /** Floats in the whole buffer. */
44
+ export const LIGHTS_FLOATS = LIGHT_HEADER + MAX_LIGHTS * LIGHT_STRIDE;
45
+ /**
46
+ * `// @lights 3` — how many lights this effect emits.
47
+ *
48
+ * Declared, like every other mount: what the file says is what gets allocated,
49
+ * so an effect that emits none costs no slots and nobody pays for a cap they
50
+ * did not ask for. Clamped rather than rejected, the same choice `@particles`
51
+ * makes — an author asking for a hundred gets the most the engine will give and
52
+ * a scene that still runs.
53
+ */
54
+ export function parseLightCount(wgsl, max) {
55
+ const m = /^\s*\/\/\s*@lights\s+(\d+)\s*$/m.exec(wgsl);
56
+ if (!m)
57
+ return 0;
58
+ return Math.max(1, Math.min(max, parseInt(m[1], 10)));
59
+ }
60
+ /**
61
+ * The RzLight struct, declared in EVERY module a user's source is spliced into.
62
+ *
63
+ * One effect file goes into every module it has a mount in, so a foreground
64
+ * effect that also emits lights compiles its lightEmit inside the FIELD shader
65
+ * too — where nothing calls it, but it still has to resolve. Leaving the struct
66
+ * out of those modules is a compile error on a function the author was right to
67
+ * write, which is the same trap the grid's step-only half documents.
68
+ */
69
+ export const RZ_LIGHT_STRUCT_WGSL = /* wgsl */ `
70
+ /** What an effect returns for one of its lights. */
71
+ struct RzLight {
72
+ pos: vec3f,
73
+ color: vec3f,
74
+ intensity: f32,
75
+ radius: f32,
76
+ }
77
+ `;
78
+ /**
79
+ * The world's light at a surface facing n — the flat colour, or the installed
80
+ * HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
81
+ * ibl.ts for the folding; the shader is a plain polynomial in the normal).
82
+ *
83
+ * One string included by every module that declares LightUniforms with the sh
84
+ * block — the hosted-api lesson: a helper defined in some modules and not
85
+ * others is a compile error waiting for the first file that crosses them.
86
+ */
87
+ export const WORLD_AMBIENT_WGSL = /* wgsl */ `
88
+ fn rzWorldAmbient(n: vec3f) -> vec3f {
89
+ if (light.sh[0].w < 0.5) { return light.ambientColor.xyz; }
90
+ let x = n.x;
91
+ let y = n.y;
92
+ let z = n.z;
93
+ let c = light.sh[0].xyz
94
+ + light.sh[1].xyz * y + light.sh[2].xyz * z + light.sh[3].xyz * x
95
+ + light.sh[4].xyz * (x * y) + light.sh[5].xyz * (y * z)
96
+ + light.sh[6].xyz * (3.0 * z * z - 1.0) + light.sh[7].xyz * (x * z)
97
+ + light.sh[8].xyz * (x * x - y * y);
98
+ return max(c, vec3f(0.0));
99
+ }
100
+ `;
101
+ /** Does this source define the emit mount? */
102
+ export function hasLightEmit(wgsl) {
103
+ return /\bfn\s+lightEmit\s*\(/.test(wgsl);
104
+ }
105
+ /**
106
+ * The compute module that runs an effect's lightEmit once per light per frame.
107
+ *
108
+ * fn lightEmit(i: u32, time: f32) -> RzLight
109
+ *
110
+ * A COMPUTE stage rather than a CPU callback, and that is the whole point:
111
+ * Fireworks knows where its bursts are as a closed form in WGSL, and mirroring
112
+ * that on the CPU to place a light would be two derivations of one trajectory
113
+ * that drift apart. Emitting in the shader means the light is wherever the
114
+ * effect says it is, on the scene clock — which is also what makes it survive
115
+ * an offline export frame-stepped at a different rate.
116
+ *
117
+ * The author writes local index 0..n-1 and never learns the global one, the
118
+ * same aliasing the anchor table uses — so installing another effect ahead of
119
+ * this one moves its lights without touching its source.
120
+ *
121
+ * The base arrives in the UNIFORM rather than baked into the text. Baking it
122
+ * would mean recompiling every emitting effect the moment a scene gained or
123
+ * lost a document light, because that is what shifts the slots underneath
124
+ * them — a shader rebuild triggered by moving a lamp.
125
+ *
126
+ * The scene API arrives as a STRING rather than being imported, so this module
127
+ * depends on nothing that depends on it. That API is what lets a lamp aim at
128
+ * someone: Stage Lights points its beams at rzSubject().root, and a light that
129
+ * did not know where she was could only sit where the fixture hangs. It also
130
+ * brings RzLight, which is why this builder does not declare it again.
131
+ */
132
+ export function buildLightEmitShader(wgsl, sceneApi, cast) {
133
+ return /* wgsl */ `
134
+ // read_write HERE and read-only in the material shaders. Different passes, so
135
+ // the two never coexist: this compute runs before the scene pass that reads it.
136
+ @group(0) @binding(0) var<storage, read_write> _rzLightsOut: array<f32>;
137
+ // (time, base slot, count, _) — see buildLightEmitShader on why the base is
138
+ // here and not in the text.
139
+ @group(0) @binding(1) var<uniform> _rzLightU: vec4f;
140
+ // The camera block and the cast — the two buffers the scene API reads. Same
141
+ // contents the field and grid modules bind, so an effect's lightEmit sees the
142
+ // scene exactly as its drawing half does.
143
+ @group(0) @binding(2) var<uniform> viewU: array<vec4<f32>, 15>;
144
+ @group(0) @binding(3) var<storage, read> _rzCast: array<vec4f>;
145
+ ${sceneApi}
146
+ // Audio and score at 4 and 5, the same bindings the particle and trail modules
147
+ // put them on. A lamp that pulses on the beat or lights on a note is the whole
148
+ // point of a light an effect owns rather than one the document places, so this
149
+ // is not compile-safety padding — it is the mount's reason to exist.
150
+ ${audioApi(0, 4)}
151
+ ${midiApi(0, 5)}
152
+ ${lyricsApi(0, 6)}
153
+ // The rest of the hosted API. Every one of these is here because the AUTHOR'S
154
+ // WHOLE FILE lands below, not because lightEmit needs it: a trail effect that
155
+ // grows a lamp at its tip compiles its ribbon code in this module too. The math
156
+ // helpers and the Particle struct are NOT repeated — they arrive with the scene
157
+ // API above, and a second copy is a redefinition error in engine code.
158
+ ${clockApi("_rzLightU.x", "0.0")}
159
+ // Canvas height — the same number the drawing modules read out of their camera
160
+ // struct, both written from canvas.height, so this name means ONE value in
161
+ // every module. Verified against the writers, not assumed: cameraMatrixData[35]
162
+ // and viewU[6].w have the same source.
163
+ ${viewportApi("viewU[6].w")}
164
+ ${trailSlotsApi(cast.trailCount)}
165
+ ${wgsl}
166
+
167
+ @compute @workgroup_size(64)
168
+ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
169
+ let i = gid.x;
170
+ // The dispatch is sized to the count, but a workgroup is 64 wide and the
171
+ // count rarely is — the tail must not write into the next effect's slots.
172
+ if (i >= u32(_rzLightU.z)) { return; }
173
+ // Time is a PARAMETER, not an rzTime() call: this same source compiles
174
+ // inside the field, particle, trail and grid modules, and those already
175
+ // define rzTime differently or not at all. A parameter needs nothing from
176
+ // the module it lands in, which is why the field mounts take theirs too.
177
+ let l = lightEmit(i, _rzLightU.x);
178
+ // SANITIZED at the one write site, because lightEmit is HOSTED USER CODE and
179
+ // this buffer feeds every fragment of every material: one NaN position would
180
+ // poison the whole frame, and WGSL leaves max(NaN, 0) indeterminate, so it
181
+ // would not even fail the same way on every GPU. A light that fails the
182
+ // check writes zeros — radius 0 is off. Colour is clamped at zero on top:
183
+ // this layer is ADDITIVE, and a negative channel would darken what it lands
184
+ // on and can push HDR negative into bloom.
185
+ let finite = l.pos.x == l.pos.x && l.pos.y == l.pos.y && l.pos.z == l.pos.z &&
186
+ l.radius == l.radius && l.intensity == l.intensity &&
187
+ l.color.x == l.color.x && l.color.y == l.color.y && l.color.z == l.color.z;
188
+ let c = select(vec3f(0.0), max(l.color * l.intensity, vec3f(0.0)), finite);
189
+ let b = ${LIGHT_HEADER}u + (u32(_rzLightU.y) + i) * ${LIGHT_STRIDE}u;
190
+ _rzLightsOut[b] = select(0.0, l.pos.x, finite);
191
+ _rzLightsOut[b + 1u] = select(0.0, l.pos.y, finite);
192
+ _rzLightsOut[b + 2u] = select(0.0, l.pos.z, finite);
193
+ _rzLightsOut[b + 3u] = select(0.0, max(l.radius, 0.0), finite);
194
+ // Colour carries intensity, exactly as the CPU writer stores it — one product,
195
+ // one place, so the two producers cannot disagree about what a slot means.
196
+ _rzLightsOut[b + 4u] = c.x;
197
+ _rzLightsOut[b + 5u] = c.y;
198
+ _rzLightsOut[b + 6u] = c.z;
199
+ }
200
+ `;
201
+ }
202
+ /** The rz*Light accessors, with the buffer declared at the given binding. */
203
+ export function lightsApi(group, binding) {
204
+ return /* wgsl */ `
205
+ @group(${group}) @binding(${binding}) var<storage, read> _rzLights: array<f32>;
206
+
207
+ const RZ_MAX_LIGHTS: u32 = ${MAX_LIGHTS}u;
208
+
209
+ /** How many positional lights the scene has. Zero is the ordinary case. */
210
+ fn rzLightCount() -> u32 { return min(u32(_rzLights[0]), RZ_MAX_LIGHTS); }
211
+
212
+ /** Light i's world position. */
213
+ fn rzLightPos(i: u32) -> vec3f {
214
+ let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u;
215
+ return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
216
+ }
217
+
218
+ /** How far light i reaches. Its falloff is zero AT this distance, not merely
219
+ * small, so the light has a bound a cull can be derived from later. */
220
+ fn rzLightRadius(i: u32) -> f32 { return _rzLights[${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 3u]; }
221
+
222
+ /** Light i's colour, already multiplied by its intensity. */
223
+ fn rzLightColor(i: u32) -> vec3f {
224
+ let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 4u;
225
+ return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
226
+ }
227
+
228
+ /**
229
+ * Every positional light's contribution at a surface point, as light — not as a
230
+ * finished colour. Multiply by whatever the surface's albedo is.
231
+ *
232
+ * WITH NO LIGHTS THIS RETURNS EXACTLY ZERO and the loop never runs, so a scene
233
+ * that declares none is arithmetically identical to one compiled before lights
234
+ * existed. That is the property the whole feature is gated on: adding this to
235
+ * every material must cost nothing until someone asks for a light.
236
+ *
237
+ * FALLOFF IS RELATIVE TO THE RADIUS, and deliberately not physical.
238
+ *
239
+ * The first version windowed a real inverse-square, and it was unusable: 1/d²
240
+ * is measured in world units, an MMD character is about 18 of them tall, so a
241
+ * lamp two metres off her shoulder divided by 37 and an intensity of 4 landed
242
+ * as 0.06 — invisible. Radius and intensity were fighting, and intensity had no
243
+ * scale a person could learn.
244
+ *
245
+ * So: intensity is the brightness AT the light, radius is where it reaches
246
+ * zero, and the curve between them is the same shape whatever the scene's
247
+ * scale. Both dials now mean what they say, which for a composer beats being
248
+ * right about photons. (1 - t²)² — smooth at both ends, exactly 0 at the
249
+ * radius, so the bound a cull could be derived from is still real.
250
+ */
251
+ fn rzLightsDiffuse(p: vec3f, n: vec3f) -> vec3f {
252
+ var acc = vec3f(0.0);
253
+ let count = rzLightCount();
254
+ for (var i = 0u; i < count; i = i + 1u) {
255
+ let d = rzLightPos(i) - p;
256
+ let dist = length(d);
257
+ // Facing the light, and nothing behind it. No wrap or half-lambert: this
258
+ // layer adds light, and a wrapped term would lift the shadow side, which is
259
+ // the ramp's business and not this one's.
260
+ let ndl = max(dot(n, d / max(dist, 1e-4)), 0.0);
261
+ if (ndl <= 0.0) { continue; }
262
+ let t = clamp(dist / max(rzLightRadius(i), 1e-4), 0.0, 1.0);
263
+ let falloff = 1.0 - t * t;
264
+ acc = acc + rzLightColor(i) * (ndl * falloff * falloff);
265
+ }
266
+ return acc;
267
+ }
268
+ `;
269
+ }
@@ -0,0 +1,39 @@
1
+ export declare const LYRIC_LINES_MAX = 256;
2
+ export declare const LYRIC_HEADER = 4;
3
+ export declare const LYRIC_STRIDE = 8;
4
+ export declare const LYRICS_FLOATS: number;
5
+ /** Bounds on the line atlas the host packs rasterised lines into. It is sized
6
+ * to the track that arrives rather than allocated at the maximum: a scene with
7
+ * no lyrics carries a 1×1 placeholder, and a song's atlas is as tall as its
8
+ * own lines need. 8192 is the smallest texture dimension WebGPU guarantees. */
9
+ export declare const LYRIC_ATLAS_MAX_W = 2048;
10
+ export declare const LYRIC_ATLAS_MAX_H = 8192;
11
+ export type LyricLine = {
12
+ /** Seconds on the scene clock. */
13
+ start: number;
14
+ /** Seconds; a parser that has no better answer uses the next line's start. */
15
+ end: number;
16
+ text: string;
17
+ };
18
+ /** Where a rasterised line sits in the atlas: u0, vTop, u1, vBottom, in 0..1. */
19
+ export type LyricRect = [number, number, number, number];
20
+ /**
21
+ * Parse an .lrc file: `[mm:ss.xx]` tags (several per line share the text),
22
+ * an optional `[offset:±ms]` tag, blank-text tags kept as instrumental gaps'
23
+ * end markers. Lines come out sorted; each line's end is the next line's
24
+ * start, and the last line gets a ten-second hold. The offset follows the
25
+ * LRC convention: positive shows lines EARLIER — the knob to turn when the
26
+ * words feel late against this particular rip.
27
+ */
28
+ export declare function parseLRC(source: string): LyricLine[];
29
+ /** Fill the shared buffer's floats from parsed lines, clamped to the cap. */
30
+ export declare function packLyrics(lines: LyricLine[], rects?: LyricRect[]): Float32Array<ArrayBuffer>;
31
+ /** The rzLyric* timing accessors, with the buffer declared at the given binding. */
32
+ export declare function lyricsApi(group: number, binding: number): string;
33
+ /**
34
+ * The text half — field module only, where the atlas is bound. uv is 0..1
35
+ * across LINE i's own box, y-up like everything else; the return is glyph
36
+ * coverage. textureSampleLevel, so it is legal after any branch.
37
+ */
38
+ export declare function lyricsTextApi(group: number, texBinding: number, samplerName: string): string;
39
+ //# sourceMappingURL=lyrics-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lyrics-api.d.ts","sourceRoot":"","sources":["../../src/shaders/lyrics-api.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,eAAe,MAAM,CAAA;AAClC,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,eAAO,MAAM,aAAa,QAAgD,CAAA;AAE1E;;;gFAGgF;AAChF,eAAO,MAAM,iBAAiB,OAAO,CAAA;AACrC,eAAO,MAAM,iBAAiB,OAAO,CAAA;AAErC,MAAM,MAAM,SAAS,GAAG;IACtB,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,iFAAiF;AACjF,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAExD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,CA+BpD;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,KAAK,CAAC,EAAE,SAAS,EAAE,GAAG,YAAY,CAAC,WAAW,CAAC,CAkB7F;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAsDhE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAgC5F"}
@@ -0,0 +1,187 @@
1
+ // Lyrics, as data — the fourth timing interface beside audio, the score and
2
+ // the lights, and shaped like them: one shared buffer, read through accessors,
3
+ // never touched directly.
4
+ //
5
+ // An effect gets the TIMING of the words — which line is live at the scene
6
+ // clock, how far through it is — and, in the field module, the words
7
+ // themselves: the host rasterises each line once (Canvas2D; CJK rules out
8
+ // glyph atlases) into a fixed atlas the effect samples through rzLyricText.
9
+ // The look — fill, outline, wipe, motion — is the effect author's, in WGSL.
10
+ //
11
+ // LAYOUT. A 4-float header (count, then padding that keeps the records
12
+ // vec4-aligned), then LYRIC_LINES_MAX records of 8 floats:
13
+ //
14
+ // [0] start s [1] end s [2] character count [3] reserved
15
+ // [4..7] atlas rect: u0, vTop, u1, vBottom
16
+ //
17
+ // FIXED SIZE, unlike the score: a song carries tens of lines, not thousands
18
+ // of notes, so capping at 256 costs 8 KB and buys the property that setLyrics
19
+ // is a buffer write — the buffer identity never changes, so nothing ever has
20
+ // to re-bind for lyrics arriving late. The atlas holds the same property by
21
+ // being allocated once at a fixed size (LYRIC_ATLAS_W × LYRIC_ATLAS_H).
22
+ export const LYRIC_LINES_MAX = 256;
23
+ export const LYRIC_HEADER = 4;
24
+ export const LYRIC_STRIDE = 8;
25
+ export const LYRICS_FLOATS = LYRIC_HEADER + LYRIC_LINES_MAX * LYRIC_STRIDE;
26
+ /** Bounds on the line atlas the host packs rasterised lines into. It is sized
27
+ * to the track that arrives rather than allocated at the maximum: a scene with
28
+ * no lyrics carries a 1×1 placeholder, and a song's atlas is as tall as its
29
+ * own lines need. 8192 is the smallest texture dimension WebGPU guarantees. */
30
+ export const LYRIC_ATLAS_MAX_W = 2048;
31
+ export const LYRIC_ATLAS_MAX_H = 8192;
32
+ /**
33
+ * Parse an .lrc file: `[mm:ss.xx]` tags (several per line share the text),
34
+ * an optional `[offset:±ms]` tag, blank-text tags kept as instrumental gaps'
35
+ * end markers. Lines come out sorted; each line's end is the next line's
36
+ * start, and the last line gets a ten-second hold. The offset follows the
37
+ * LRC convention: positive shows lines EARLIER — the knob to turn when the
38
+ * words feel late against this particular rip.
39
+ */
40
+ export function parseLRC(source) {
41
+ let offset = 0;
42
+ const stamped = [];
43
+ for (const raw of source.split(/\r?\n/)) {
44
+ const off = /^\s*\[offset:\s*([+-]?\d+)\s*\]/i.exec(raw);
45
+ if (off) {
46
+ offset = parseInt(off[1], 10) / 1000;
47
+ continue;
48
+ }
49
+ const tags = [...raw.matchAll(/\[(\d+):(\d{1,2})(?:[.:](\d{1,3}))?\]/g)];
50
+ if (tags.length === 0)
51
+ continue;
52
+ const text = raw.slice(tags[tags.length - 1].index + tags[tags.length - 1][0].length).trim();
53
+ for (const t of tags) {
54
+ const frac = t[3] ? parseInt(t[3], 10) / 10 ** t[3].length : 0;
55
+ stamped.push({ start: Math.max(0, parseInt(t[1], 10) * 60 + parseInt(t[2], 10) + frac - offset), text });
56
+ }
57
+ }
58
+ stamped.sort((a, b) => a.start - b.start);
59
+ const lines = [];
60
+ for (let i = 0; i < stamped.length; i++) {
61
+ // An empty-text stamp is an .lrc idiom for "the previous line ends here";
62
+ // it closes its predecessor and is not a line of its own.
63
+ if (stamped[i].text === "")
64
+ continue;
65
+ const next = stamped[i + 1];
66
+ lines.push({
67
+ start: stamped[i].start,
68
+ end: next ? next.start : stamped[i].start + 10,
69
+ text: stamped[i].text,
70
+ });
71
+ }
72
+ return lines;
73
+ }
74
+ /** Fill the shared buffer's floats from parsed lines, clamped to the cap. */
75
+ export function packLyrics(lines, rects) {
76
+ const out = new Float32Array(new ArrayBuffer(LYRICS_FLOATS * 4));
77
+ const n = Math.min(lines.length, LYRIC_LINES_MAX);
78
+ out[0] = n;
79
+ for (let i = 0; i < n; i++) {
80
+ const b = LYRIC_HEADER + i * LYRIC_STRIDE;
81
+ out[b] = lines[i].start;
82
+ out[b + 1] = lines[i].end;
83
+ out[b + 2] = lines[i].text.length;
84
+ const r = rects?.[i];
85
+ if (r) {
86
+ out[b + 4] = r[0];
87
+ out[b + 5] = r[1];
88
+ out[b + 6] = r[2];
89
+ out[b + 7] = r[3];
90
+ }
91
+ }
92
+ return out;
93
+ }
94
+ /** The rzLyric* timing accessors, with the buffer declared at the given binding. */
95
+ export function lyricsApi(group, binding) {
96
+ return /* wgsl */ `
97
+ @group(${group}) @binding(${binding}) var<storage, read> _rzLyrics: array<f32>;
98
+
99
+ /** Lines in the lyric track; 0 when none is loaded, which every accessor
100
+ * below tolerates by answering zero rather than reading past the end. */
101
+ fn rzLyricCount() -> i32 { return i32(_rzLyrics[0]); }
102
+
103
+ fn rzLyricStart(i: i32) -> f32 {
104
+ if (i < 0 || i >= rzLyricCount()) { return 0.0; }
105
+ return _rzLyrics[${LYRIC_HEADER} + i * ${LYRIC_STRIDE}];
106
+ }
107
+
108
+ fn rzLyricEnd(i: i32) -> f32 {
109
+ if (i < 0 || i >= rzLyricCount()) { return 0.0; }
110
+ return _rzLyrics[${LYRIC_HEADER} + i * ${LYRIC_STRIDE} + 1];
111
+ }
112
+
113
+ /** Characters in line i — the number a per-character sweep divides by. */
114
+ fn rzLyricChars(i: i32) -> f32 {
115
+ if (i < 0 || i >= rzLyricCount()) { return 0.0; }
116
+ return _rzLyrics[${LYRIC_HEADER} + i * ${LYRIC_STRIDE} + 2];
117
+ }
118
+
119
+ /** Where line i sits in the lyric atlas: u0, vTop, u1, vBottom. Zero when the
120
+ * host never rasterised text — check with rzLyricHasText. */
121
+ fn rzLyricRect(i: i32) -> vec4f {
122
+ if (i < 0 || i >= rzLyricCount()) { return vec4f(0.0); }
123
+ let b = ${LYRIC_HEADER} + i * ${LYRIC_STRIDE};
124
+ return vec4f(_rzLyrics[b + 4], _rzLyrics[b + 5], _rzLyrics[b + 6], _rzLyrics[b + 7]);
125
+ }
126
+
127
+ fn rzLyricHasText(i: i32) -> bool {
128
+ let r = rzLyricRect(i);
129
+ return r.z > r.x;
130
+ }
131
+
132
+ /** The line live at time t, or -1 between lines and outside the track. */
133
+ fn rzLyricIndex(t: f32) -> i32 {
134
+ let n = rzLyricCount();
135
+ for (var i = 0; i < n; i = i + 1) {
136
+ if (t >= rzLyricStart(i) && t < rzLyricEnd(i)) { return i; }
137
+ }
138
+ return -1;
139
+ }
140
+
141
+ /** How far through line i the clock is, 0..1 — the karaoke sweep. */
142
+ fn rzLyricProgress(i: i32, t: f32) -> f32 {
143
+ let s = rzLyricStart(i);
144
+ let e = rzLyricEnd(i);
145
+ if (e <= s) { return 0.0; }
146
+ return clamp((t - s) / (e - s), 0.0, 1.0);
147
+ }
148
+ `;
149
+ }
150
+ /**
151
+ * The text half — field module only, where the atlas is bound. uv is 0..1
152
+ * across LINE i's own box, y-up like everything else; the return is glyph
153
+ * coverage. textureSampleLevel, so it is legal after any branch.
154
+ */
155
+ export function lyricsTextApi(group, texBinding, samplerName) {
156
+ return /* wgsl */ `
157
+ @group(${group}) @binding(${texBinding}) var _rzLyricTex: texture_2d<f32>;
158
+
159
+ fn rzLyricText(i: i32, uv: vec2f) -> f32 {
160
+ let r = rzLyricRect(i);
161
+ if (r.z <= r.x || uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { return 0.0; }
162
+ let at = vec2f(mix(r.x, r.z, uv.x), mix(r.w, r.y, uv.y));
163
+ return textureSampleLevel(_rzLyricTex, ${samplerName}, at, 0.0).r;
164
+ }
165
+
166
+ /** Line i's width over its height as rasterised — size a box with it so the
167
+ * glyphs keep their proportions on any canvas. */
168
+ fn rzLyricAspect(i: i32) -> f32 {
169
+ let r = rzLyricRect(i);
170
+ let h = r.w - r.y;
171
+ if (h <= 0.0) { return 1.0; }
172
+ let dim = vec2f(textureDimensions(_rzLyricTex));
173
+ return ((r.z - r.x) * dim.x) / (h * dim.y);
174
+ }
175
+
176
+ /**
177
+ * Line i's box in ATLAS TEXELS. Divide by the size you draw it at to learn
178
+ * whether you are magnifying or minifying, which is what an edge-sharpening
179
+ * step needs to know — and the honest way to get it, since a derivative
180
+ * builtin is illegal after the branches an effect of this kind opens with.
181
+ */
182
+ fn rzLyricPixels(i: i32) -> vec2f {
183
+ let r = rzLyricRect(i);
184
+ return vec2f(r.z - r.x, r.w - r.y) * vec2f(textureDimensions(_rzLyricTex));
185
+ }
186
+ `;
187
+ }
@@ -1,6 +1,4 @@
1
- export declare const COMMON_BINDINGS_WGSL = "\n\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n viewPos: vec3f,\n _padding: f32,\n};\n\nstruct Light {\n direction: vec4f,\n color: vec4f,\n};\n\nstruct LightUniforms {\n ambientColor: vec4f,\n lights: array<Light, 4>,\n};\n\n// Per-material uniforms. Every material binds this layout even if it ignores fields;\n// the engine keeps one bind group layout across all material pipelines. The PMX\n// classic-material fields (ambient/specular/shininess) are carried for graph nodes that\n// want them; most graphs read only diffuseColor + alpha.\nstruct MaterialUniforms {\n diffuseColor: vec3f, // PMX diffuse rgb \u2014 the material_diffuse node reads this\n alpha: f32, // 0 \u2192 discard; <1 \u2192 transparent draw call\n ambient: vec3f, // PMX ambient rgb\n shininess: f32, // PMX specular power\n specular: vec3f, // PMX specular rgb\n sphereMode: f32, // 0 none \u00B7 1 multiply (sph) \u00B7 2 add (spa)\n // Skeleton index of the \u982D (head) bone, or -1. Lets the eye shader gate\n // the post-alpha-eye stencil by camera-vs-face hemisphere.\n headBoneIndex: f32,\n _pad0: f32,\n _pad1: f32,\n _pad2: f32,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) normal: vec3f,\n @location(1) uv: vec2f,\n @location(2) worldPos: vec3f,\n // Bind-pose object-space position (the raw pre-skin vertex attribute). Procedural\n // textures (noise bump, sparkle, Generated-coord gradients) key off this instead of\n // worldPos so the pattern rides with the surface \u2014 otherwise the mesh swims through a\n // world-static noise field under any skinning deformation or root (\u30BB\u30F3\u30BF\u30FC) motion.\n // At rest skinMats are identity so restPos == worldPos, which is why existing noise-\n // scale constants stay valid without retuning.\n @location(3) restPos: vec3f,\n};\n\nstruct LightVP { viewProj: mat4x4f, };\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var<uniform> light: LightUniforms;\n@group(0) @binding(2) var diffuseSampler: sampler;\n@group(0) @binding(3) var shadowMap: texture_depth_2d;\n@group(0) @binding(4) var shadowSampler: sampler_comparison;\n@group(0) @binding(5) var<uniform> lightVP: LightVP;\n// binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).\n@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;\n@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;\n@group(2) @binding(1) var<uniform> material: MaterialUniforms;\n// Reserved for future sphere/toon graph nodes; graphs that don't read them get the\n// 1\u00D71 white fallback bound here.\n@group(2) @binding(2) var toonTexture: texture_2d<f32>;\n@group(2) @binding(3) var sphereTexture: texture_2d<f32>;\n// Extra maps supplied by the STYLE GROUP rather than by the PMX \u2014 the lightmap /\n// ILM / ramp textures a Blender-authored look is built on. A PMX material carries\n// exactly one image, and this whole family of shading encodes shadow thresholds,\n// specular masks and material IDs in the channels of a second and third. Unset\n// slots get the 1\u00D71 white fallback, so a graph reading one that was never\n// supplied sees white rather than garbage.\n@group(2) @binding(5) var groupTexture0: texture_2d<f32>;\n@group(2) @binding(6) var groupTexture1: texture_2d<f32>;\n@group(2) @binding(7) var groupTexture2: texture_2d<f32>;\n@group(2) @binding(8) var groupTexture3: texture_2d<f32>;\n\n// Four-bone blended normals can cancel to ~zero on physics-driven parts\n// (opposing bone rotations at 50/50 weights) \u2014 normalize(0) is 0/0 = NaN,\n// which poisons the whole shading stack and flashes through bloom. Fall\n// back to up for degenerate normals instead.\nfn safe_normal(nIn: vec3f) -> vec3f {\n let l2 = dot(nIn, nIn);\n if (l2 < 1e-12) { return vec3f(0.0, 1.0, 0.0); }\n return nIn * inverseSqrt(l2);\n}\n\n";
2
- export declare const SAMPLE_SHADOW_WGSL = "\n\nfn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {\n if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }\n let biasedPos = worldPos + n * 0.08;\n let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);\n let ndc = lclip.xyz / max(lclip.w, 1e-6);\n let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);\n let cmpZ = ndc.z - 0.001;\n let ts = 1.0 / 4096.0;\n let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);\n let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);\n let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);\n let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);\n let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);\n let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);\n let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);\n let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);\n let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);\n return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);\n}\n\n";
3
- export declare const COMMON_VS_WGSL = "\n\n@vertex fn vs(\n @location(0) position: vec3f,\n @location(1) normal: vec3f,\n @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>,\n @location(4) weights0: vec4<f32>\n) -> VertexOutput {\n var output: VertexOutput;\n let pos4 = vec4f(position, 1.0);\n let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;\n let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);\n let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);\n var skinnedPos = vec4f(0.0);\n var skinnedNrm = vec3f(0.0);\n for (var i = 0u; i < 4u; i++) {\n let m = skinMats[joints0[i]];\n let w = nw[i];\n skinnedPos += (m * pos4) * w;\n skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;\n }\n output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);\n output.normal = skinnedNrm;\n output.uv = uv;\n output.worldPos = skinnedPos.xyz;\n output.restPos = position;\n return output;\n}\n\n";
4
- export declare const COMMON_FS_OUT_WGSL = "\n\nstruct FSOut {\n @location(0) color: vec4f,\n @location(1) mask: vec4f,\n};\n\n";
1
+ export declare const COMMON_BINDINGS_WGSL: string;
2
+ export declare function commonFsOutWgsl(): string;
5
3
  export declare const COMMON_MATERIAL_PRELUDE_WGSL: string;
6
4
  //# sourceMappingURL=common.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,oBAAoB,w1HAyFhC,CAAC;AASF,eAAO,MAAM,kBAAkB,4xCAsB9B,CAAC;AAQF,eAAO,MAAM,cAAc,s+BA8B1B,CAAC;AAsBF,eAAO,MAAM,kBAAkB,0FAO9B,CAAC;AAMF,eAAO,MAAM,4BAA4B,QACwC,CAAA"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA2BA,eAAO,MAAM,oBAAoB,QAwGhC,CAAC;AAgIF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAQD,eAAO,MAAM,4BAA4B,QAC0D,CAAA"}