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,93 @@
|
|
|
1
|
+
// The mirror camera, as arithmetic — pure and headlessly testable, the
|
|
2
|
+
// shadow-cascades precedent.
|
|
3
|
+
//
|
|
4
|
+
// A planar reflection is not a second camera aimed by hand; it is the SAME
|
|
5
|
+
// camera with the world reflected about the floor plane. Fold the reflection
|
|
6
|
+
// into the view matrix and everything downstream is untouched: world positions
|
|
7
|
+
// stay TRUE world positions, so sun, shadows and positional lights evaluate at
|
|
8
|
+
// the unmirrored point — which is exactly what a mirror shows, an object lit
|
|
9
|
+
// as it is, seen from a mirrored eye. The only other value that must mirror is
|
|
10
|
+
// the eye itself, because specular reads the view direction from it.
|
|
11
|
+
//
|
|
12
|
+
// Winding: a reflection has determinant -1, so triangle orientation flips.
|
|
13
|
+
// Every scene-pass pipeline that draws into the mirror culls "none", which is
|
|
14
|
+
// what makes this legal without a flipped-frontFace pipeline set. The OUTLINE
|
|
15
|
+
// culls "back" and is therefore skipped in the mirror — its hull would face
|
|
16
|
+
// the wrong way and ink over the model.
|
|
17
|
+
/**
|
|
18
|
+
* The debug view: the reflection target drawn over the finished frame — the
|
|
19
|
+
* only way to SEE whether the mirror pass is right before anything consumes
|
|
20
|
+
* it, the same instrument discipline as setIdDebug. The target is HDR linear;
|
|
21
|
+
* a Reinhard fold plus a square-root keeps highlights readable without
|
|
22
|
+
* involving the real view transform, which a diagnostic does not need.
|
|
23
|
+
*/
|
|
24
|
+
export const REFLECTION_DEBUG_WGSL = /* wgsl */ `
|
|
25
|
+
@group(0) @binding(0) var t: texture_2d<f32>;
|
|
26
|
+
@group(0) @binding(1) var s: sampler;
|
|
27
|
+
|
|
28
|
+
struct VSOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f, };
|
|
29
|
+
|
|
30
|
+
@vertex fn vs(@builtin(vertex_index) i: u32) -> VSOut {
|
|
31
|
+
var out: VSOut;
|
|
32
|
+
let x = f32(i32(i / 2u) * 4 - 1);
|
|
33
|
+
let y = f32(i32(i % 2u) * 4 - 1);
|
|
34
|
+
out.pos = vec4f(x, y, 0.0, 1.0);
|
|
35
|
+
out.uv = vec2f(x * 0.5 + 0.5, 0.5 - y * 0.5);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@fragment fn fs(in: VSOut) -> @location(0) vec4f {
|
|
40
|
+
let c = textureSample(t, s, in.uv).rgb;
|
|
41
|
+
return vec4f(sqrt(c / (vec3f(1.0) + c)), 1.0);
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
/**
|
|
45
|
+
* Reflection about the horizontal plane y = h, column-major.
|
|
46
|
+
*
|
|
47
|
+
* p' = (x, 2h - y, z)
|
|
48
|
+
*/
|
|
49
|
+
export function reflectionAboutY(h) {
|
|
50
|
+
// prettier-ignore
|
|
51
|
+
return new Float32Array([
|
|
52
|
+
1, 0, 0, 0,
|
|
53
|
+
0, -1, 0, 0,
|
|
54
|
+
0, 0, 1, 0,
|
|
55
|
+
0, 2 * h, 0, 1,
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Fill a camera-uniform block for the mirror pass from the live one.
|
|
60
|
+
*
|
|
61
|
+
* Layout is the material CameraUniforms: view at 0, projection at 16, eye at
|
|
62
|
+
* 32, render-target height at 35 — the same 36 floats the main camera writes,
|
|
63
|
+
* copied rather than re-derived so the two cannot disagree about anything but
|
|
64
|
+
* the reflection.
|
|
65
|
+
*
|
|
66
|
+
* view' = view × R (column-vector convention, matching `projection * view *
|
|
67
|
+
* pos` in the vertex shaders); projection unchanged; eye reflected.
|
|
68
|
+
*/
|
|
69
|
+
export function buildMirrorCamera(camera, planeY, out) {
|
|
70
|
+
const v = camera;
|
|
71
|
+
const h = planeY;
|
|
72
|
+
// view × R where R = reflectionAboutY(h). R only touches column 1 (scaled by
|
|
73
|
+
// -1) and adds 2h·col1 to the translation — write the product directly
|
|
74
|
+
// rather than through a generic multiply, so the arithmetic is exact and the
|
|
75
|
+
// cost is a handful of ops.
|
|
76
|
+
for (let i = 0; i < 16; i++)
|
|
77
|
+
out[i] = v[i];
|
|
78
|
+
out[4] = -v[4];
|
|
79
|
+
out[5] = -v[5];
|
|
80
|
+
out[6] = -v[6];
|
|
81
|
+
out[7] = -v[7];
|
|
82
|
+
out[12] = v[12] + 2 * h * v[4];
|
|
83
|
+
out[13] = v[13] + 2 * h * v[5];
|
|
84
|
+
out[14] = v[14] + 2 * h * v[6];
|
|
85
|
+
out[15] = v[15] + 2 * h * v[7];
|
|
86
|
+
for (let i = 16; i < 32; i++)
|
|
87
|
+
out[i] = v[i];
|
|
88
|
+
out[32] = v[32];
|
|
89
|
+
out[33] = 2 * h - v[33];
|
|
90
|
+
out[34] = v[34];
|
|
91
|
+
out[35] = v[35];
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
interface AnchorRequest {
|
|
2
|
+
bone: string;
|
|
3
|
+
trail: boolean;
|
|
4
|
+
}
|
|
5
|
+
/** The empty table — a scene with no effect installed asks for no bones. */
|
|
6
|
+
export declare const EMPTY_ANCHOR_TABLE: AnchorTable;
|
|
7
|
+
export interface AnchorTable {
|
|
8
|
+
/** The scene's bones, deduplicated, in allocation order. Storage addresses. */
|
|
9
|
+
entries: AnchorRequest[];
|
|
10
|
+
/** Per effect, local slot → global slot; -1 for a request the cap refused. */
|
|
11
|
+
alias: number[][];
|
|
12
|
+
/** What the cap refused, so an install can say so instead of going quiet. */
|
|
13
|
+
dropped: {
|
|
14
|
+
effect: number;
|
|
15
|
+
bone: string;
|
|
16
|
+
}[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Allocate the scene's anchors from what each effect asked for.
|
|
20
|
+
*
|
|
21
|
+
* Deduplicated by BONE, not by (bone, trail): a request for a trail and a
|
|
22
|
+
* request for the bare position are the same bone, and one entry with the trail
|
|
23
|
+
* turned on satisfies both. Keying on the pair would spend two of eight slots
|
|
24
|
+
* describing one wrist.
|
|
25
|
+
*
|
|
26
|
+
* Order is first-come, so a single effect gets the identity alias and the whole
|
|
27
|
+
* mechanism is a no-op until a second effect exists — which is what makes this
|
|
28
|
+
* safe to land before setEffects does.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildAnchorTable(requests: AnchorRequest[][], max: number): AnchorTable;
|
|
31
|
+
/**
|
|
32
|
+
* Ribbon index → LOCAL anchor slot, for the trail draw.
|
|
33
|
+
*
|
|
34
|
+
* A THIRD index space, and the one that bit. The trail pass draws one ribbon per
|
|
35
|
+
* TRAILED anchor, so its instance index counts 0,1,2… over trailed anchors only
|
|
36
|
+
* — while the cast buffer is addressed by DECLARATION slot. Those coincide
|
|
37
|
+
* exactly when every anchor is trailed, which is true of all 14 library effects
|
|
38
|
+
* and is why this stayed latent: declare `@anchor 頭` then `@anchor 左手首 trail`
|
|
39
|
+
* and ribbon 0 asked for the trail of 頭, which has none, so the ribbon silently
|
|
40
|
+
* did not draw.
|
|
41
|
+
*
|
|
42
|
+
* Feeding this into _rzSlot afterwards is what makes the chain complete:
|
|
43
|
+
* ribbon → local slot → scene slot.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ribbonSlotWgsl(localSlots: number[]): string;
|
|
46
|
+
/**
|
|
47
|
+
* The alias as WGSL, spliced into each effect's module.
|
|
48
|
+
*
|
|
49
|
+
* A switch rather than an array because a const array indexed by a runtime value
|
|
50
|
+
* lowers badly on the Metal backend — the same reason the filmic curve became a
|
|
51
|
+
* texture. With one effect this compiles to `return local`, which every backend
|
|
52
|
+
* folds away.
|
|
53
|
+
*/
|
|
54
|
+
export declare function anchorAliasWgsl(alias: number[]): string;
|
|
55
|
+
export {};
|
|
56
|
+
//# sourceMappingURL=anchor-table.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anchor-table.d.ts","sourceRoot":"","sources":["../../src/shaders/anchor-table.ts"],"names":[],"mappings":"AAsBA,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;CACf;AAED,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,EAAE,WAAqD,CAAA;AAEtF,MAAM,WAAW,WAAW;IAC1B,+EAA+E;IAC/E,OAAO,EAAE,aAAa,EAAE,CAAA;IACxB,8EAA8E;IAC9E,KAAK,EAAE,MAAM,EAAE,EAAE,CAAA;IACjB,6EAA6E;IAC7E,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC5C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,CAiCtF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAkB3D;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAkBvD"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// The scene's anchor table: one deduplicated set of bones for every effect in
|
|
2
|
+
// the scene, plus the per-effect alias that maps an author's slot onto it.
|
|
3
|
+
//
|
|
4
|
+
// THE PROBLEM IT SOLVES. A slot number is currently two things at once: the
|
|
5
|
+
// author's name for a bone, and its storage address in the cast buffer. Those
|
|
6
|
+
// coincide only while one effect exists. Install two and slot 0 means a
|
|
7
|
+
// different bone to each of them, so whichever wrote last wins and the other
|
|
8
|
+
// silently reads someone else's hand.
|
|
9
|
+
//
|
|
10
|
+
// The fix is to stop conflating them. Bones are allocated ONCE for the scene,
|
|
11
|
+
// deduplicated, and each effect is spliced a private `_rzSlot(local) -> global`
|
|
12
|
+
// that the accessors route through. Author source is untouched — a published
|
|
13
|
+
// effect keeps compiling and keeps its line numbers, which matters because line
|
|
14
|
+
// numbers are what an author debugs against.
|
|
15
|
+
//
|
|
16
|
+
// Three things fall out of it beyond fixing the clash:
|
|
17
|
+
// · two effects wanting the same trail SHARE one ring, and trails are the
|
|
18
|
+
// expensive resource — 128 samples × 4 subjects each;
|
|
19
|
+
// · the cap becomes 8 distinct bones per SCENE rather than per file;
|
|
20
|
+
// · it is the same indirection the skeleton data interface needs later, so
|
|
21
|
+
// this is a bridge rather than a detour.
|
|
22
|
+
/** The empty table — a scene with no effect installed asks for no bones. */
|
|
23
|
+
export const EMPTY_ANCHOR_TABLE = { entries: [], alias: [], dropped: [] };
|
|
24
|
+
/**
|
|
25
|
+
* Allocate the scene's anchors from what each effect asked for.
|
|
26
|
+
*
|
|
27
|
+
* Deduplicated by BONE, not by (bone, trail): a request for a trail and a
|
|
28
|
+
* request for the bare position are the same bone, and one entry with the trail
|
|
29
|
+
* turned on satisfies both. Keying on the pair would spend two of eight slots
|
|
30
|
+
* describing one wrist.
|
|
31
|
+
*
|
|
32
|
+
* Order is first-come, so a single effect gets the identity alias and the whole
|
|
33
|
+
* mechanism is a no-op until a second effect exists — which is what makes this
|
|
34
|
+
* safe to land before setEffects does.
|
|
35
|
+
*/
|
|
36
|
+
export function buildAnchorTable(requests, max) {
|
|
37
|
+
const entries = [];
|
|
38
|
+
const index = new Map();
|
|
39
|
+
const alias = [];
|
|
40
|
+
const dropped = [];
|
|
41
|
+
for (let e = 0; e < requests.length; e++) {
|
|
42
|
+
const local = [];
|
|
43
|
+
for (const req of requests[e]) {
|
|
44
|
+
let g = index.get(req.bone);
|
|
45
|
+
if (g === undefined) {
|
|
46
|
+
if (entries.length >= max) {
|
|
47
|
+
// Refused, and the effect still installs: an effect that loses one of
|
|
48
|
+
// its anchors draws that one wrong, where refusing the install would
|
|
49
|
+
// lose the whole scene's visuals over a bone.
|
|
50
|
+
dropped.push({ effect: e, bone: req.bone });
|
|
51
|
+
local.push(-1);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
g = entries.length;
|
|
55
|
+
index.set(req.bone, g);
|
|
56
|
+
entries.push({ bone: req.bone, trail: req.trail });
|
|
57
|
+
}
|
|
58
|
+
else if (req.trail) {
|
|
59
|
+
// A later request for the same bone can only ever ADD the trail — the
|
|
60
|
+
// ring is shared, and turning it on for one reader turns it on for all.
|
|
61
|
+
entries[g].trail = true;
|
|
62
|
+
}
|
|
63
|
+
local.push(g);
|
|
64
|
+
}
|
|
65
|
+
alias.push(local);
|
|
66
|
+
}
|
|
67
|
+
return { entries, alias, dropped };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Ribbon index → LOCAL anchor slot, for the trail draw.
|
|
71
|
+
*
|
|
72
|
+
* A THIRD index space, and the one that bit. The trail pass draws one ribbon per
|
|
73
|
+
* TRAILED anchor, so its instance index counts 0,1,2… over trailed anchors only
|
|
74
|
+
* — while the cast buffer is addressed by DECLARATION slot. Those coincide
|
|
75
|
+
* exactly when every anchor is trailed, which is true of all 14 library effects
|
|
76
|
+
* and is why this stayed latent: declare `@anchor 頭` then `@anchor 左手首 trail`
|
|
77
|
+
* and ribbon 0 asked for the trail of 頭, which has none, so the ribbon silently
|
|
78
|
+
* did not draw.
|
|
79
|
+
*
|
|
80
|
+
* Feeding this into _rzSlot afterwards is what makes the chain complete:
|
|
81
|
+
* ribbon → local slot → scene slot.
|
|
82
|
+
*/
|
|
83
|
+
export function ribbonSlotWgsl(localSlots) {
|
|
84
|
+
const identity = localSlots.every((s, i) => s === i);
|
|
85
|
+
if (identity) {
|
|
86
|
+
return `
|
|
87
|
+
/** Ribbon index → local anchor slot. Identity: every anchor is trailed. */
|
|
88
|
+
fn _rzRibbonSlot(ribbon: i32) -> i32 { return ribbon; }
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
const cases = localSlots.map((s, i) => ` case ${i}: { return ${s}; }`).join("\n");
|
|
92
|
+
return `
|
|
93
|
+
/** Ribbon index → local anchor slot, skipping the anchors with no trail. */
|
|
94
|
+
fn _rzRibbonSlot(ribbon: i32) -> i32 {
|
|
95
|
+
switch ribbon {
|
|
96
|
+
${cases}
|
|
97
|
+
default: { return -1; }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The alias as WGSL, spliced into each effect's module.
|
|
104
|
+
*
|
|
105
|
+
* A switch rather than an array because a const array indexed by a runtime value
|
|
106
|
+
* lowers badly on the Metal backend — the same reason the filmic curve became a
|
|
107
|
+
* texture. With one effect this compiles to `return local`, which every backend
|
|
108
|
+
* folds away.
|
|
109
|
+
*/
|
|
110
|
+
export function anchorAliasWgsl(alias) {
|
|
111
|
+
const identity = alias.every((g, i) => g === i);
|
|
112
|
+
if (identity) {
|
|
113
|
+
return `
|
|
114
|
+
/** Local slot → scene slot. Identity here: this effect owns the table. */
|
|
115
|
+
fn _rzSlot(local: i32) -> i32 { return local; }
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
const cases = alias.map((g, i) => ` case ${i}: { return ${g}; }`).join("\n");
|
|
119
|
+
return `
|
|
120
|
+
/** Local slot → scene slot, from the deduplicated table this effect shares. */
|
|
121
|
+
fn _rzSlot(local: i32) -> i32 {
|
|
122
|
+
switch local {
|
|
123
|
+
${cases}
|
|
124
|
+
default: { return -1; }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-api.d.ts","sourceRoot":"","sources":["../../src/shaders/audio-api.ts"],"names":[],"mappings":"AAgBA,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAgE/D"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// The audio data interface, as WGSL — shared verbatim by every effect module.
|
|
2
|
+
//
|
|
3
|
+
// The buffer is PRECOMPUTED for the whole track and sampled by time, never fed
|
|
4
|
+
// live from an AnalyserNode. That is not an optimisation: an export steps the
|
|
5
|
+
// engine frame by frame instead of playing in real time, so a live analyser
|
|
6
|
+
// would return silence during a render and every audio-reactive effect would
|
|
7
|
+
// quietly vanish from the exported video — the same class of bug the scene
|
|
8
|
+
// clock exists to prevent. Layout: an 8-float header [frames, bands,
|
|
9
|
+
// secondsPerFrame, audioTime, playing, 3 spare] then per frame
|
|
10
|
+
// [level, onset, band0..bandN-1]. audioTime and playing are rewritten every frame by
|
|
11
|
+
// whoever owns playback — the editor's audio clock, the viewer's, or the export
|
|
12
|
+
// loop — so "now" is always the clock that is actually running.
|
|
13
|
+
//
|
|
14
|
+
// Every module binds the SAME buffer, so a spawn rule in a particle effect and
|
|
15
|
+
// a bar in a background read identical numbers for the same instant.
|
|
16
|
+
/** The rzAudio* accessors, with the buffer declared at the given binding. */
|
|
17
|
+
export function audioApi(group, binding) {
|
|
18
|
+
return /* wgsl */ `
|
|
19
|
+
@group(${group}) @binding(${binding}) var<storage, read> _rzAudio: array<f32>;
|
|
20
|
+
|
|
21
|
+
/** Frames of analysis available; 0 when the scene has no audio. */
|
|
22
|
+
fn rzAudioFrames() -> i32 { return i32(_rzAudio[0]); }
|
|
23
|
+
/** 1 while the track is actually PLAYING, 0 paused or absent — so an effect can
|
|
24
|
+
* go calm instead of oscillating over a frozen spectrum. */
|
|
25
|
+
fn rzAudioPlaying() -> f32 { return select(0.0, _rzAudio[4], i32(_rzAudio[0]) > 0); }
|
|
26
|
+
/** Where the track is NOW, in seconds — the clock rzAudioLevelAt offsets hang
|
|
27
|
+
* from. An effect that detects an onset k seconds back can anchor a ring to
|
|
28
|
+
* the ABSOLUTE moment of the hit and age it continuously. */
|
|
29
|
+
fn rzAudioTime() -> f32 { return _rzAudio[3]; }
|
|
30
|
+
/** Bands per frame — log-spaced, bass first. */
|
|
31
|
+
fn rzAudioBandCount() -> i32 { return i32(_rzAudio[1]); }
|
|
32
|
+
|
|
33
|
+
fn _rzAudioFrameAt(offset: f32) -> i32 {
|
|
34
|
+
let frames = i32(_rzAudio[0]);
|
|
35
|
+
let t = _rzAudio[3] + offset;
|
|
36
|
+
return clamp(i32(t / max(_rzAudio[2], 1e-5)), 0, frames - 1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Loudness at now + offset seconds, 0..1.
|
|
41
|
+
*
|
|
42
|
+
* The offset is what makes a WAVEFORM drawable: a column at x samples the
|
|
43
|
+
* envelope at (x - 0.5) * window seconds, and the playhead is the centre of the
|
|
44
|
+
* screen by construction. Offsets past either end clamp to the track's edges.
|
|
45
|
+
*/
|
|
46
|
+
fn rzAudioLevelAt(offset: f32) -> f32 {
|
|
47
|
+
let frames = i32(_rzAudio[0]);
|
|
48
|
+
if (frames <= 0) { return 0.0; }
|
|
49
|
+
let bands = i32(_rzAudio[1]);
|
|
50
|
+
return _rzAudio[8 + _rzAudioFrameAt(offset) * (bands + 2)];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The KICK track: how hard the bass is rising at now + offset, 0..1 —
|
|
55
|
+
* precomputed, so a beat-triggered effect is one comparison instead of a
|
|
56
|
+
* per-pixel history scan. Scan a short window of NEGATIVE offsets to find
|
|
57
|
+
* recent hits and age things from them (quantise the hit to the analysis grid
|
|
58
|
+
* so ages stay continuous).
|
|
59
|
+
*/
|
|
60
|
+
fn rzAudioOnsetAt(offset: f32) -> f32 {
|
|
61
|
+
let frames = i32(_rzAudio[0]);
|
|
62
|
+
if (frames <= 0) { return 0.0; }
|
|
63
|
+
let bands = i32(_rzAudio[1]);
|
|
64
|
+
return _rzAudio[8 + _rzAudioFrameAt(offset) * (bands + 2) + 1];
|
|
65
|
+
}
|
|
66
|
+
fn rzAudioOnset() -> f32 { return rzAudioOnsetAt(0.0); }
|
|
67
|
+
/** Loudness now, 0..1. */
|
|
68
|
+
fn rzAudioLevel() -> f32 { return rzAudioLevelAt(0.0); }
|
|
69
|
+
|
|
70
|
+
/** Band i at now + offset seconds, 0..1. Band 0 is the deepest bass. */
|
|
71
|
+
fn rzAudioBandAt(i: i32, offset: f32) -> f32 {
|
|
72
|
+
let frames = i32(_rzAudio[0]);
|
|
73
|
+
if (frames <= 0) { return 0.0; }
|
|
74
|
+
let bands = i32(_rzAudio[1]);
|
|
75
|
+
if (i < 0 || i >= bands) { return 0.0; }
|
|
76
|
+
return _rzAudio[8 + _rzAudioFrameAt(offset) * (bands + 2) + 2 + i];
|
|
77
|
+
}
|
|
78
|
+
/** Band i now — the visualiser call. */
|
|
79
|
+
fn rzAudioBand(i: i32) -> f32 { return rzAudioBandAt(i, 0.0); }
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const CAST_API = "\nconst RZ_SUBJECTS: i32 = 4;\nconst RZ_SAMPLES: i32 = 128;\n/** The anchor ADDRESS SPACE \u2014 how many an effect may declare, not how many it\n * did. RZ_TRAIL_SLOTS is the per-effect number and is not this one; the two\n * being one number was the old trail bug. */\nconst RZ_MAX_ANCHORS: i32 = 8;\nconst RZ_TRAIL_SAMPLES: i32 = 128;\n\nstruct RzSubject {\n /** On the FLOOR, under the body \u2014 where a ring or a magic circle belongs. */\n root: vec3f,\n /** At the hips, the middle of the body \u2014 where an aura belongs. */\n center: vec3f,\n /** Bounding sphere: xyz centre, w radius. Deliberately generous \u2014 cull with it. */\n bounds: vec4f,\n /** False past the end of the cast, and every field is then zero. */\n valid: bool,\n}\n\nstruct RzAnchor {\n pos: vec3f,\n /** World units per second, from the previous frame. Direction for a trail,\n * magnitude for anything that should react to how hard someone is moving. */\n vel: vec3f,\n /** The bone's forward axis \u2014 which way a foot points, where a head looks. */\n fwd: vec3f,\n /** False when this rig has no such bone. Check it: the alternative is drawing\n * a hand effect at the world origin on every model that spells it differently. */\n valid: bool,\n}\n\n/** Which model this is, stable across a scene \u2014 for per-subject variation. */\nfn rzSubjectId(i: i32) -> u32 {\n if (i < 0 || i >= rzSubjectCount()) { return 0u; }\n return u32(_rzCast[i * 3 + 1].w);\n}\n\nfn rzSubject(i: i32) -> RzSubject {\n var s: RzSubject;\n s.valid = i >= 0 && i < rzSubjectCount();\n if (!s.valid) { return s; }\n let b = i * 3;\n s.root = _rzCast[b].xyz;\n s.center = _rzCast[b + 1].xyz;\n s.bounds = _rzCast[b + 2];\n return s;\n}\n\n/**\n * Where a named bone is, this frame.\n *\n * The slot is the author's own: the Nth @anchor in their file, in the order\n * they wrote them. _rzSlot turns that into the scene's address, which is what\n * keeps two effects that both anchor to a wrist from reading each other's.\n */\nfn rzAnchor(subject: i32, slot: i32) -> RzAnchor {\n var a: RzAnchor;\n a.valid = false;\n let g = _rzSlot(slot);\n if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }\n let b = 12 + (g * 4 + subject) * 3;\n a.valid = _rzCast[b].w > 0.5;\n a.pos = _rzCast[b].xyz;\n a.vel = _rzCast[b + 1].xyz;\n a.fwd = _rzCast[b + 2].xyz;\n return a;\n}\n\n/**\n * How many samples of a path are recorded \u2014 0 for an anchor that asked for no\n * trail, and for one that has not moved yet.\n *\n * Bounded by the anchor cap, NOT by how many anchors asked for a trail. Those\n * are different index spaces: storage is addressed by anchor slot, so an\n * untrailed @anchor followed by a trailed one put the trail at index 1 with a\n * bound of 1 and rzTrail returned zero \u2014 a ribbon that silently did not draw.\n */\nfn rzTrailCount(subject: i32, slot: i32) -> i32 {\n let g = _rzSlot(slot);\n if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }\n return i32(_rzCast[12 + (g * 4 + subject) * 3 + 2].w);\n}\n\n/** Sample i of a path: xyz where it was, w how many seconds ago. i = 0 is now. */\nfn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {\n let n = rzTrailCount(subject, slot);\n if (i < 0 || i >= n) { return vec4f(0.0); }\n let base = 108 + (_rzSlot(slot) * 4 + subject) * RZ_TRAIL_SAMPLES;\n return _rzCast[base + i];\n}\n";
|
|
2
|
+
//# sourceMappingURL=cast-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cast-api.d.ts","sourceRoot":"","sources":["../../src/shaders/cast-api.ts"],"names":[],"mappings":"AA+BA,eAAO,MAAM,QAAQ,k4GA2FpB,CAAA"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// The cast, as data — the sibling of the audio and score interfaces, and shaped
|
|
2
|
+
// like them: one shared buffer, read through accessors, never touched directly.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS FILE EXISTS. There were two of these. The field, grid and lightEmit
|
|
5
|
+
// modules read the cast through one implementation; the particle and trail
|
|
6
|
+
// modules read the SAME BUFFER through another, written separately, and the
|
|
7
|
+
// particle one had no rzAnchor at all — so a particle effect could ask where a
|
|
8
|
+
// trail had been but not where a wrist is. Neither was wrong; they had simply
|
|
9
|
+
// never been the same code, and the split was invisible until an effect used a
|
|
10
|
+
// mount from each family and its own file stopped compiling in one of them.
|
|
11
|
+
//
|
|
12
|
+
// The two differed only in how the layout reached them: one baked the engine's
|
|
13
|
+
// constants, the other took them as a CastLayout. Every caller of that layout
|
|
14
|
+
// passed the same five constants, so the parameterisation described a freedom
|
|
15
|
+
// that did not exist. Baking them makes this a constant string, which is what
|
|
16
|
+
// lets both families share it without either one deciding the shape.
|
|
17
|
+
//
|
|
18
|
+
// WHAT A HOST MUST SUPPLY. Three names, and deliberately only three:
|
|
19
|
+
//
|
|
20
|
+
// _rzCast the buffer, at whatever binding the module puts it on
|
|
21
|
+
// _rzSlot(i) the effect's local slot → the scene's, from its alias
|
|
22
|
+
// rzSubjectCount() how many subjects are live
|
|
23
|
+
//
|
|
24
|
+
// The last is a host's because the two families genuinely disagree on it: the
|
|
25
|
+
// field module reads a count the engine wrote into the view uniform, and the
|
|
26
|
+
// particle module scans the buffer, because it has no view uniform to read.
|
|
27
|
+
// They agree in value. Unifying them would be a behaviour change to every
|
|
28
|
+
// shipped effect for no gain, so the seam stays and is named here instead.
|
|
29
|
+
import { EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES } from "./cast-layout";
|
|
30
|
+
export const CAST_API = /* wgsl */ `
|
|
31
|
+
const RZ_SUBJECTS: i32 = ${EFFECT_SUBJECTS};
|
|
32
|
+
const RZ_SAMPLES: i32 = ${EFFECT_TRAIL_SAMPLES};
|
|
33
|
+
/** The anchor ADDRESS SPACE — how many an effect may declare, not how many it
|
|
34
|
+
* did. RZ_TRAIL_SLOTS is the per-effect number and is not this one; the two
|
|
35
|
+
* being one number was the old trail bug. */
|
|
36
|
+
const RZ_MAX_ANCHORS: i32 = ${EFFECT_ANCHORS};
|
|
37
|
+
const RZ_TRAIL_SAMPLES: i32 = ${EFFECT_TRAIL_SAMPLES};
|
|
38
|
+
|
|
39
|
+
struct RzSubject {
|
|
40
|
+
/** On the FLOOR, under the body — where a ring or a magic circle belongs. */
|
|
41
|
+
root: vec3f,
|
|
42
|
+
/** At the hips, the middle of the body — where an aura belongs. */
|
|
43
|
+
center: vec3f,
|
|
44
|
+
/** Bounding sphere: xyz centre, w radius. Deliberately generous — cull with it. */
|
|
45
|
+
bounds: vec4f,
|
|
46
|
+
/** False past the end of the cast, and every field is then zero. */
|
|
47
|
+
valid: bool,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
struct RzAnchor {
|
|
51
|
+
pos: vec3f,
|
|
52
|
+
/** World units per second, from the previous frame. Direction for a trail,
|
|
53
|
+
* magnitude for anything that should react to how hard someone is moving. */
|
|
54
|
+
vel: vec3f,
|
|
55
|
+
/** The bone's forward axis — which way a foot points, where a head looks. */
|
|
56
|
+
fwd: vec3f,
|
|
57
|
+
/** False when this rig has no such bone. Check it: the alternative is drawing
|
|
58
|
+
* a hand effect at the world origin on every model that spells it differently. */
|
|
59
|
+
valid: bool,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Which model this is, stable across a scene — for per-subject variation. */
|
|
63
|
+
fn rzSubjectId(i: i32) -> u32 {
|
|
64
|
+
if (i < 0 || i >= rzSubjectCount()) { return 0u; }
|
|
65
|
+
return u32(_rzCast[i * 3 + 1].w);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn rzSubject(i: i32) -> RzSubject {
|
|
69
|
+
var s: RzSubject;
|
|
70
|
+
s.valid = i >= 0 && i < rzSubjectCount();
|
|
71
|
+
if (!s.valid) { return s; }
|
|
72
|
+
let b = i * 3;
|
|
73
|
+
s.root = _rzCast[b].xyz;
|
|
74
|
+
s.center = _rzCast[b + 1].xyz;
|
|
75
|
+
s.bounds = _rzCast[b + 2];
|
|
76
|
+
return s;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Where a named bone is, this frame.
|
|
81
|
+
*
|
|
82
|
+
* The slot is the author's own: the Nth @anchor in their file, in the order
|
|
83
|
+
* they wrote them. _rzSlot turns that into the scene's address, which is what
|
|
84
|
+
* keeps two effects that both anchor to a wrist from reading each other's.
|
|
85
|
+
*/
|
|
86
|
+
fn rzAnchor(subject: i32, slot: i32) -> RzAnchor {
|
|
87
|
+
var a: RzAnchor;
|
|
88
|
+
a.valid = false;
|
|
89
|
+
let g = _rzSlot(slot);
|
|
90
|
+
if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }
|
|
91
|
+
let b = ${EFFECT_SUBJECTS * 3} + (g * ${EFFECT_SUBJECTS} + subject) * 3;
|
|
92
|
+
a.valid = _rzCast[b].w > 0.5;
|
|
93
|
+
a.pos = _rzCast[b].xyz;
|
|
94
|
+
a.vel = _rzCast[b + 1].xyz;
|
|
95
|
+
a.fwd = _rzCast[b + 2].xyz;
|
|
96
|
+
return a;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* How many samples of a path are recorded — 0 for an anchor that asked for no
|
|
101
|
+
* trail, and for one that has not moved yet.
|
|
102
|
+
*
|
|
103
|
+
* Bounded by the anchor cap, NOT by how many anchors asked for a trail. Those
|
|
104
|
+
* are different index spaces: storage is addressed by anchor slot, so an
|
|
105
|
+
* untrailed @anchor followed by a trailed one put the trail at index 1 with a
|
|
106
|
+
* bound of 1 and rzTrail returned zero — a ribbon that silently did not draw.
|
|
107
|
+
*/
|
|
108
|
+
fn rzTrailCount(subject: i32, slot: i32) -> i32 {
|
|
109
|
+
let g = _rzSlot(slot);
|
|
110
|
+
if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }
|
|
111
|
+
return i32(_rzCast[${EFFECT_SUBJECTS * 3} + (g * ${EFFECT_SUBJECTS} + subject) * 3 + 2].w);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Sample i of a path: xyz where it was, w how many seconds ago. i = 0 is now. */
|
|
115
|
+
fn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {
|
|
116
|
+
let n = rzTrailCount(subject, slot);
|
|
117
|
+
if (i < 0 || i >= n) { return vec4f(0.0); }
|
|
118
|
+
let base = ${EFFECT_TRAIL_BASE} + (_rzSlot(slot) * ${EFFECT_SUBJECTS} + subject) * RZ_TRAIL_SAMPLES;
|
|
119
|
+
return _rzCast[base + i];
|
|
120
|
+
}
|
|
121
|
+
`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The caps the cast buffer is built to, shared by every shader that reads it and
|
|
3
|
+
* by the engine that fills it. Interpolated into the WGSL rather than written
|
|
4
|
+
* twice: the layout arithmetic on both sides has to agree exactly, and two
|
|
5
|
+
* literals that must match are two literals that eventually will not.
|
|
6
|
+
*
|
|
7
|
+
* All three are MINIMUMS. Raising one breaks nothing, because effects read
|
|
8
|
+
* through accessors and loop to the count functions; lowering one does.
|
|
9
|
+
*
|
|
10
|
+
* THEIR OWN FILE, and it imports nothing: cast-api.ts needs them to write the
|
|
11
|
+
* accessors, composite.ts needs them for the rest of its module, and cast-api
|
|
12
|
+
* is spliced into composite. Left where they were, that is an import cycle,
|
|
13
|
+
* and the first thing to touch the cycle throws ReferenceError at module load —
|
|
14
|
+
* the whole engine failing to start on an import order nobody chose.
|
|
15
|
+
*/
|
|
16
|
+
export declare const EFFECT_SUBJECTS = 4;
|
|
17
|
+
export declare const EFFECT_ANCHORS = 8;
|
|
18
|
+
export declare const EFFECT_TRAIL_SAMPLES = 128;
|
|
19
|
+
/** vec4 slot where the trails begin — after the subjects and the anchors. */
|
|
20
|
+
export declare const EFFECT_TRAIL_BASE: number;
|
|
21
|
+
//# sourceMappingURL=cast-layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cast-layout.d.ts","sourceRoot":"","sources":["../../src/shaders/cast-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,IAAI,CAAA;AAChC,eAAO,MAAM,cAAc,IAAI,CAAA;AAC/B,eAAO,MAAM,oBAAoB,MAAM,CAAA;AACvC,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,QAA6D,CAAA"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The caps the cast buffer is built to, shared by every shader that reads it and
|
|
3
|
+
* by the engine that fills it. Interpolated into the WGSL rather than written
|
|
4
|
+
* twice: the layout arithmetic on both sides has to agree exactly, and two
|
|
5
|
+
* literals that must match are two literals that eventually will not.
|
|
6
|
+
*
|
|
7
|
+
* All three are MINIMUMS. Raising one breaks nothing, because effects read
|
|
8
|
+
* through accessors and loop to the count functions; lowering one does.
|
|
9
|
+
*
|
|
10
|
+
* THEIR OWN FILE, and it imports nothing: cast-api.ts needs them to write the
|
|
11
|
+
* accessors, composite.ts needs them for the rest of its module, and cast-api
|
|
12
|
+
* is spliced into composite. Left where they were, that is an import cycle,
|
|
13
|
+
* and the first thing to touch the cycle throws ReferenceError at module load —
|
|
14
|
+
* the whole engine failing to start on an import order nobody chose.
|
|
15
|
+
*/
|
|
16
|
+
export const EFFECT_SUBJECTS = 4;
|
|
17
|
+
export const EFFECT_ANCHORS = 8;
|
|
18
|
+
export const EFFECT_TRAIL_SAMPLES = 128;
|
|
19
|
+
/** vec4 slot where the trails begin — after the subjects and the anchors. */
|
|
20
|
+
export const EFFECT_TRAIL_BASE = EFFECT_SUBJECTS * 3 + EFFECT_ANCHORS * EFFECT_SUBJECTS * 3;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Floats before the first record. One is the count; the rest keep the records
|
|
2
|
+
* vec4-aligned, which is what lets a future pass read them as vec4s. */
|
|
3
|
+
export declare const LIGHT_HEADER = 4;
|
|
4
|
+
/** Floats per light — see the layout above. */
|
|
5
|
+
export declare const LIGHT_STRIDE = 8;
|
|
6
|
+
/**
|
|
7
|
+
* The cap, and it is a real one: the loop below runs per fragment, so this is
|
|
8
|
+
* the number that decides whether lights are free or a cost. Sixteen is the
|
|
9
|
+
* bounded middle tier the design settled on — enough for a stage rig, far below
|
|
10
|
+
* the point where clustering would start to pay for itself.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MAX_LIGHTS = 16;
|
|
13
|
+
/** Floats in the whole buffer. */
|
|
14
|
+
export declare const LIGHTS_FLOATS: number;
|
|
15
|
+
/**
|
|
16
|
+
* `// @lights 3` — how many lights this effect emits.
|
|
17
|
+
*
|
|
18
|
+
* Declared, like every other mount: what the file says is what gets allocated,
|
|
19
|
+
* so an effect that emits none costs no slots and nobody pays for a cap they
|
|
20
|
+
* did not ask for. Clamped rather than rejected, the same choice `@particles`
|
|
21
|
+
* makes — an author asking for a hundred gets the most the engine will give and
|
|
22
|
+
* a scene that still runs.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseLightCount(wgsl: string, max: number): number;
|
|
25
|
+
/**
|
|
26
|
+
* The RzLight struct, declared in EVERY module a user's source is spliced into.
|
|
27
|
+
*
|
|
28
|
+
* One effect file goes into every module it has a mount in, so a foreground
|
|
29
|
+
* effect that also emits lights compiles its lightEmit inside the FIELD shader
|
|
30
|
+
* too — where nothing calls it, but it still has to resolve. Leaving the struct
|
|
31
|
+
* out of those modules is a compile error on a function the author was right to
|
|
32
|
+
* write, which is the same trap the grid's step-only half documents.
|
|
33
|
+
*/
|
|
34
|
+
export declare const RZ_LIGHT_STRUCT_WGSL = "\n/** What an effect returns for one of its lights. */\nstruct RzLight {\n pos: vec3f,\n color: vec3f,\n intensity: f32,\n radius: f32,\n}\n";
|
|
35
|
+
/**
|
|
36
|
+
* The world's light at a surface facing n — the flat colour, or the installed
|
|
37
|
+
* HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
|
|
38
|
+
* ibl.ts for the folding; the shader is a plain polynomial in the normal).
|
|
39
|
+
*
|
|
40
|
+
* One string included by every module that declares LightUniforms with the sh
|
|
41
|
+
* block — the hosted-api lesson: a helper defined in some modules and not
|
|
42
|
+
* others is a compile error waiting for the first file that crosses them.
|
|
43
|
+
*/
|
|
44
|
+
export declare const WORLD_AMBIENT_WGSL = "\nfn rzWorldAmbient(n: vec3f) -> vec3f {\n if (light.sh[0].w < 0.5) { return light.ambientColor.xyz; }\n let x = n.x;\n let y = n.y;\n let z = n.z;\n let c = light.sh[0].xyz\n + light.sh[1].xyz * y + light.sh[2].xyz * z + light.sh[3].xyz * x\n + light.sh[4].xyz * (x * y) + light.sh[5].xyz * (y * z)\n + light.sh[6].xyz * (3.0 * z * z - 1.0) + light.sh[7].xyz * (x * z)\n + light.sh[8].xyz * (x * x - y * y);\n return max(c, vec3f(0.0));\n}\n";
|
|
45
|
+
/** Does this source define the emit mount? */
|
|
46
|
+
export declare function hasLightEmit(wgsl: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The compute module that runs an effect's lightEmit once per light per frame.
|
|
49
|
+
*
|
|
50
|
+
* fn lightEmit(i: u32, time: f32) -> RzLight
|
|
51
|
+
*
|
|
52
|
+
* A COMPUTE stage rather than a CPU callback, and that is the whole point:
|
|
53
|
+
* Fireworks knows where its bursts are as a closed form in WGSL, and mirroring
|
|
54
|
+
* that on the CPU to place a light would be two derivations of one trajectory
|
|
55
|
+
* that drift apart. Emitting in the shader means the light is wherever the
|
|
56
|
+
* effect says it is, on the scene clock — which is also what makes it survive
|
|
57
|
+
* an offline export frame-stepped at a different rate.
|
|
58
|
+
*
|
|
59
|
+
* The author writes local index 0..n-1 and never learns the global one, the
|
|
60
|
+
* same aliasing the anchor table uses — so installing another effect ahead of
|
|
61
|
+
* this one moves its lights without touching its source.
|
|
62
|
+
*
|
|
63
|
+
* The base arrives in the UNIFORM rather than baked into the text. Baking it
|
|
64
|
+
* would mean recompiling every emitting effect the moment a scene gained or
|
|
65
|
+
* lost a document light, because that is what shifts the slots underneath
|
|
66
|
+
* them — a shader rebuild triggered by moving a lamp.
|
|
67
|
+
*
|
|
68
|
+
* The scene API arrives as a STRING rather than being imported, so this module
|
|
69
|
+
* depends on nothing that depends on it. That API is what lets a lamp aim at
|
|
70
|
+
* someone: Stage Lights points its beams at rzSubject().root, and a light that
|
|
71
|
+
* did not know where she was could only sit where the fixture hangs. It also
|
|
72
|
+
* brings RzLight, which is why this builder does not declare it again.
|
|
73
|
+
*/
|
|
74
|
+
export declare function buildLightEmitShader(wgsl: string, sceneApi: string, cast: {
|
|
75
|
+
trailCount: number;
|
|
76
|
+
}): string;
|
|
77
|
+
/** The rz*Light accessors, with the buffer declared at the given binding. */
|
|
78
|
+
export declare function lightsApi(group: number, binding: number): string;
|
|
79
|
+
//# sourceMappingURL=lights.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../src/shaders/lights.ts"],"names":[],"mappings":"AAgCA;yEACyE;AACzE,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,+CAA+C;AAC/C,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B;;;;;GAKG;AACH,eAAO,MAAM,UAAU,KAAK,CAAA;AAC5B,kCAAkC;AAClC,eAAO,MAAM,aAAa,QAA2C,CAAA;AAErE;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIjE;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,qJAQhC,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,idAa9B,CAAA;AAED,8CAA8C;AAC9C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC3B,MAAM,CAqER;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAkEhE"}
|