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.
- package/README.md +40 -410
- package/dist/camera.d.ts +3 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +33 -8
- package/dist/engine.d.ts +924 -29
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +4043 -278
- package/dist/graph/registry.d.ts +2 -2
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +1 -1
- package/dist/graph/slots.d.ts +0 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +37 -9
- package/dist/hdr.d.ts +18 -0
- package/dist/hdr.d.ts.map +1 -0
- package/dist/hdr.js +162 -0
- package/dist/ibl.d.ts +19 -0
- package/dist/ibl.d.ts.map +1 -0
- package/dist/ibl.js +113 -0
- package/dist/ik-solver.d.ts +2 -1
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/math.d.ts +20 -1
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +23 -16
- package/dist/midi-loader.d.ts +10 -0
- package/dist/midi-loader.d.ts.map +1 -0
- package/dist/midi-loader.js +247 -0
- package/dist/model.d.ts +2 -13
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +29 -0
- package/dist/param-track.d.ts +48 -0
- package/dist/param-track.d.ts.map +1 -0
- package/dist/param-track.js +80 -0
- package/dist/physics/types.d.ts.map +1 -1
- package/dist/physics/types.js +3 -0
- package/dist/reflection.d.ts +27 -0
- package/dist/reflection.d.ts.map +1 -0
- package/dist/reflection.js +93 -0
- package/dist/shaders/anchor-table.d.ts +56 -0
- package/dist/shaders/anchor-table.d.ts.map +1 -0
- package/dist/shaders/anchor-table.js +128 -0
- package/dist/shaders/audio-api.d.ts +3 -0
- package/dist/shaders/audio-api.d.ts.map +1 -0
- package/dist/shaders/audio-api.js +81 -0
- package/dist/shaders/cast-api.d.ts +2 -0
- package/dist/shaders/cast-api.d.ts.map +1 -0
- package/dist/shaders/cast-api.js +121 -0
- package/dist/shaders/cast-layout.d.ts +21 -0
- package/dist/shaders/cast-layout.d.ts.map +1 -0
- package/dist/shaders/cast-layout.js +20 -0
- package/dist/shaders/lights.d.ts +79 -0
- package/dist/shaders/lights.d.ts.map +1 -0
- package/dist/shaders/lights.js +269 -0
- package/dist/shaders/lyrics-api.d.ts +39 -0
- package/dist/shaders/lyrics-api.d.ts.map +1 -0
- package/dist/shaders/lyrics-api.js +187 -0
- package/dist/shaders/materials/common.d.ts +2 -4
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +87 -35
- package/dist/shaders/midi-api.d.ts +10 -0
- package/dist/shaders/midi-api.d.ts.map +1 -0
- package/dist/shaders/midi-api.js +114 -0
- package/dist/shaders/passes/composite.d.ts +31 -15
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +225 -135
- package/dist/shaders/passes/cull.d.ts +2 -0
- package/dist/shaders/passes/cull.d.ts.map +1 -0
- package/dist/shaders/passes/cull.js +138 -0
- package/dist/shaders/passes/field-blit.d.ts +26 -0
- package/dist/shaders/passes/field-blit.d.ts.map +1 -0
- package/dist/shaders/passes/field-blit.js +65 -0
- package/dist/shaders/passes/grid.d.ts +31 -0
- package/dist/shaders/passes/grid.d.ts.map +1 -0
- package/dist/shaders/passes/grid.js +169 -0
- package/dist/shaders/passes/ground.d.ts +13 -1
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +170 -25
- package/dist/shaders/passes/hosted-api.d.ts +57 -0
- package/dist/shaders/passes/hosted-api.d.ts.map +1 -0
- package/dist/shaders/passes/hosted-api.js +166 -0
- package/dist/shaders/passes/id-debug.d.ts +28 -0
- package/dist/shaders/passes/id-debug.d.ts.map +1 -0
- package/dist/shaders/passes/id-debug.js +74 -0
- package/dist/shaders/passes/particles.d.ts +66 -0
- package/dist/shaders/passes/particles.d.ts.map +1 -0
- package/dist/shaders/passes/particles.js +279 -0
- package/dist/shaders/passes/scene-contract.d.ts +128 -0
- package/dist/shaders/passes/scene-contract.d.ts.map +1 -0
- package/dist/shaders/passes/scene-contract.js +207 -0
- package/dist/shaders/passes/sim.d.ts +34 -0
- package/dist/shaders/passes/sim.d.ts.map +1 -0
- package/dist/shaders/passes/sim.js +169 -0
- package/dist/shaders/passes/trails.d.ts +59 -0
- package/dist/shaders/passes/trails.d.ts.map +1 -0
- package/dist/shaders/passes/trails.js +340 -0
- package/dist/shaders/score-api.d.ts +10 -0
- package/dist/shaders/score-api.d.ts.map +1 -0
- package/dist/shaders/score-api.js +114 -0
- package/dist/shadow-cascades.d.ts +45 -0
- package/dist/shadow-cascades.d.ts.map +1 -0
- package/dist/shadow-cascades.js +70 -0
- package/dist/vmd-loader.d.ts +3 -2
- package/dist/vmd-loader.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/camera.ts +31 -8
- package/src/engine.ts +4535 -296
- package/src/graph/registry.ts +2 -2
- package/src/graph/slots.ts +37 -9
- package/src/hdr.ts +156 -0
- package/src/ibl.ts +115 -0
- package/src/ik-solver.ts +1 -1
- package/src/index.ts +12 -0
- package/src/math.ts +23 -17
- package/src/midi-loader.ts +246 -0
- package/src/model.ts +31 -3
- package/src/param-track.ts +83 -0
- package/src/physics/types.ts +4 -1
- package/src/reflection.ts +94 -0
- package/src/shaders/anchor-table.ts +147 -0
- package/src/shaders/audio-api.ts +82 -0
- package/src/shaders/cast-api.ts +123 -0
- package/src/shaders/cast-layout.ts +20 -0
- package/src/shaders/lights.ts +280 -0
- package/src/shaders/lyrics-api.ts +202 -0
- package/src/shaders/materials/common.ts +89 -35
- package/src/shaders/midi-api.ts +116 -0
- package/src/shaders/passes/composite.ts +244 -136
- package/src/shaders/passes/cull.ts +139 -0
- package/src/shaders/passes/grid.ts +178 -0
- package/src/shaders/passes/ground.ts +172 -25
- package/src/shaders/passes/hosted-api.ts +171 -0
- package/src/shaders/passes/id-debug.ts +75 -0
- package/src/shaders/passes/particles.ts +340 -0
- package/src/shaders/passes/scene-contract.ts +266 -0
- package/src/shaders/passes/trails.ts +390 -0
- package/src/shadow-cascades.ts +97 -0
- 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
|
|
2
|
-
export declare
|
|
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":"
|
|
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"}
|