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,82 @@
|
|
|
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
|
+
|
|
17
|
+
/** The rzAudio* accessors, with the buffer declared at the given binding. */
|
|
18
|
+
export function audioApi(group: number, binding: number): string {
|
|
19
|
+
return /* wgsl */ `
|
|
20
|
+
@group(${group}) @binding(${binding}) var<storage, read> _rzAudio: array<f32>;
|
|
21
|
+
|
|
22
|
+
/** Frames of analysis available; 0 when the scene has no audio. */
|
|
23
|
+
fn rzAudioFrames() -> i32 { return i32(_rzAudio[0]); }
|
|
24
|
+
/** 1 while the track is actually PLAYING, 0 paused or absent — so an effect can
|
|
25
|
+
* go calm instead of oscillating over a frozen spectrum. */
|
|
26
|
+
fn rzAudioPlaying() -> f32 { return select(0.0, _rzAudio[4], i32(_rzAudio[0]) > 0); }
|
|
27
|
+
/** Where the track is NOW, in seconds — the clock rzAudioLevelAt offsets hang
|
|
28
|
+
* from. An effect that detects an onset k seconds back can anchor a ring to
|
|
29
|
+
* the ABSOLUTE moment of the hit and age it continuously. */
|
|
30
|
+
fn rzAudioTime() -> f32 { return _rzAudio[3]; }
|
|
31
|
+
/** Bands per frame — log-spaced, bass first. */
|
|
32
|
+
fn rzAudioBandCount() -> i32 { return i32(_rzAudio[1]); }
|
|
33
|
+
|
|
34
|
+
fn _rzAudioFrameAt(offset: f32) -> i32 {
|
|
35
|
+
let frames = i32(_rzAudio[0]);
|
|
36
|
+
let t = _rzAudio[3] + offset;
|
|
37
|
+
return clamp(i32(t / max(_rzAudio[2], 1e-5)), 0, frames - 1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Loudness at now + offset seconds, 0..1.
|
|
42
|
+
*
|
|
43
|
+
* The offset is what makes a WAVEFORM drawable: a column at x samples the
|
|
44
|
+
* envelope at (x - 0.5) * window seconds, and the playhead is the centre of the
|
|
45
|
+
* screen by construction. Offsets past either end clamp to the track's edges.
|
|
46
|
+
*/
|
|
47
|
+
fn rzAudioLevelAt(offset: f32) -> f32 {
|
|
48
|
+
let frames = i32(_rzAudio[0]);
|
|
49
|
+
if (frames <= 0) { return 0.0; }
|
|
50
|
+
let bands = i32(_rzAudio[1]);
|
|
51
|
+
return _rzAudio[8 + _rzAudioFrameAt(offset) * (bands + 2)];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The KICK track: how hard the bass is rising at now + offset, 0..1 —
|
|
56
|
+
* precomputed, so a beat-triggered effect is one comparison instead of a
|
|
57
|
+
* per-pixel history scan. Scan a short window of NEGATIVE offsets to find
|
|
58
|
+
* recent hits and age things from them (quantise the hit to the analysis grid
|
|
59
|
+
* so ages stay continuous).
|
|
60
|
+
*/
|
|
61
|
+
fn rzAudioOnsetAt(offset: f32) -> f32 {
|
|
62
|
+
let frames = i32(_rzAudio[0]);
|
|
63
|
+
if (frames <= 0) { return 0.0; }
|
|
64
|
+
let bands = i32(_rzAudio[1]);
|
|
65
|
+
return _rzAudio[8 + _rzAudioFrameAt(offset) * (bands + 2) + 1];
|
|
66
|
+
}
|
|
67
|
+
fn rzAudioOnset() -> f32 { return rzAudioOnsetAt(0.0); }
|
|
68
|
+
/** Loudness now, 0..1. */
|
|
69
|
+
fn rzAudioLevel() -> f32 { return rzAudioLevelAt(0.0); }
|
|
70
|
+
|
|
71
|
+
/** Band i at now + offset seconds, 0..1. Band 0 is the deepest bass. */
|
|
72
|
+
fn rzAudioBandAt(i: i32, offset: f32) -> f32 {
|
|
73
|
+
let frames = i32(_rzAudio[0]);
|
|
74
|
+
if (frames <= 0) { return 0.0; }
|
|
75
|
+
let bands = i32(_rzAudio[1]);
|
|
76
|
+
if (i < 0 || i >= bands) { return 0.0; }
|
|
77
|
+
return _rzAudio[8 + _rzAudioFrameAt(offset) * (bands + 2) + 2 + i];
|
|
78
|
+
}
|
|
79
|
+
/** Band i now — the visualiser call. */
|
|
80
|
+
fn rzAudioBand(i: i32) -> f32 { return rzAudioBandAt(i, 0.0); }
|
|
81
|
+
`
|
|
82
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
|
|
30
|
+
import { EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES } from "./cast-layout"
|
|
31
|
+
|
|
32
|
+
export const CAST_API = /* wgsl */ `
|
|
33
|
+
const RZ_SUBJECTS: i32 = ${EFFECT_SUBJECTS};
|
|
34
|
+
const RZ_SAMPLES: i32 = ${EFFECT_TRAIL_SAMPLES};
|
|
35
|
+
/** The anchor ADDRESS SPACE — how many an effect may declare, not how many it
|
|
36
|
+
* did. RZ_TRAIL_SLOTS is the per-effect number and is not this one; the two
|
|
37
|
+
* being one number was the old trail bug. */
|
|
38
|
+
const RZ_MAX_ANCHORS: i32 = ${EFFECT_ANCHORS};
|
|
39
|
+
const RZ_TRAIL_SAMPLES: i32 = ${EFFECT_TRAIL_SAMPLES};
|
|
40
|
+
|
|
41
|
+
struct RzSubject {
|
|
42
|
+
/** On the FLOOR, under the body — where a ring or a magic circle belongs. */
|
|
43
|
+
root: vec3f,
|
|
44
|
+
/** At the hips, the middle of the body — where an aura belongs. */
|
|
45
|
+
center: vec3f,
|
|
46
|
+
/** Bounding sphere: xyz centre, w radius. Deliberately generous — cull with it. */
|
|
47
|
+
bounds: vec4f,
|
|
48
|
+
/** False past the end of the cast, and every field is then zero. */
|
|
49
|
+
valid: bool,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
struct RzAnchor {
|
|
53
|
+
pos: vec3f,
|
|
54
|
+
/** World units per second, from the previous frame. Direction for a trail,
|
|
55
|
+
* magnitude for anything that should react to how hard someone is moving. */
|
|
56
|
+
vel: vec3f,
|
|
57
|
+
/** The bone's forward axis — which way a foot points, where a head looks. */
|
|
58
|
+
fwd: vec3f,
|
|
59
|
+
/** False when this rig has no such bone. Check it: the alternative is drawing
|
|
60
|
+
* a hand effect at the world origin on every model that spells it differently. */
|
|
61
|
+
valid: bool,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Which model this is, stable across a scene — for per-subject variation. */
|
|
65
|
+
fn rzSubjectId(i: i32) -> u32 {
|
|
66
|
+
if (i < 0 || i >= rzSubjectCount()) { return 0u; }
|
|
67
|
+
return u32(_rzCast[i * 3 + 1].w);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn rzSubject(i: i32) -> RzSubject {
|
|
71
|
+
var s: RzSubject;
|
|
72
|
+
s.valid = i >= 0 && i < rzSubjectCount();
|
|
73
|
+
if (!s.valid) { return s; }
|
|
74
|
+
let b = i * 3;
|
|
75
|
+
s.root = _rzCast[b].xyz;
|
|
76
|
+
s.center = _rzCast[b + 1].xyz;
|
|
77
|
+
s.bounds = _rzCast[b + 2];
|
|
78
|
+
return s;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Where a named bone is, this frame.
|
|
83
|
+
*
|
|
84
|
+
* The slot is the author's own: the Nth @anchor in their file, in the order
|
|
85
|
+
* they wrote them. _rzSlot turns that into the scene's address, which is what
|
|
86
|
+
* keeps two effects that both anchor to a wrist from reading each other's.
|
|
87
|
+
*/
|
|
88
|
+
fn rzAnchor(subject: i32, slot: i32) -> RzAnchor {
|
|
89
|
+
var a: RzAnchor;
|
|
90
|
+
a.valid = false;
|
|
91
|
+
let g = _rzSlot(slot);
|
|
92
|
+
if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }
|
|
93
|
+
let b = ${EFFECT_SUBJECTS * 3} + (g * ${EFFECT_SUBJECTS} + subject) * 3;
|
|
94
|
+
a.valid = _rzCast[b].w > 0.5;
|
|
95
|
+
a.pos = _rzCast[b].xyz;
|
|
96
|
+
a.vel = _rzCast[b + 1].xyz;
|
|
97
|
+
a.fwd = _rzCast[b + 2].xyz;
|
|
98
|
+
return a;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* How many samples of a path are recorded — 0 for an anchor that asked for no
|
|
103
|
+
* trail, and for one that has not moved yet.
|
|
104
|
+
*
|
|
105
|
+
* Bounded by the anchor cap, NOT by how many anchors asked for a trail. Those
|
|
106
|
+
* are different index spaces: storage is addressed by anchor slot, so an
|
|
107
|
+
* untrailed @anchor followed by a trailed one put the trail at index 1 with a
|
|
108
|
+
* bound of 1 and rzTrail returned zero — a ribbon that silently did not draw.
|
|
109
|
+
*/
|
|
110
|
+
fn rzTrailCount(subject: i32, slot: i32) -> i32 {
|
|
111
|
+
let g = _rzSlot(slot);
|
|
112
|
+
if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }
|
|
113
|
+
return i32(_rzCast[${EFFECT_SUBJECTS * 3} + (g * ${EFFECT_SUBJECTS} + subject) * 3 + 2].w);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Sample i of a path: xyz where it was, w how many seconds ago. i = 0 is now. */
|
|
117
|
+
fn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {
|
|
118
|
+
let n = rzTrailCount(subject, slot);
|
|
119
|
+
if (i < 0 || i >= n) { return vec4f(0.0); }
|
|
120
|
+
let base = ${EFFECT_TRAIL_BASE} + (_rzSlot(slot) * ${EFFECT_SUBJECTS} + subject) * RZ_TRAIL_SAMPLES;
|
|
121
|
+
return _rzCast[base + i];
|
|
122
|
+
}
|
|
123
|
+
`
|
|
@@ -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,280 @@
|
|
|
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
|
+
|
|
28
|
+
import { audioApi } from "./audio-api"
|
|
29
|
+
import { midiApi } from "./midi-api"
|
|
30
|
+
import { lyricsApi } from "./lyrics-api"
|
|
31
|
+
import { clockApi, trailSlotsApi, viewportApi } from "./passes/hosted-api"
|
|
32
|
+
|
|
33
|
+
/** Floats before the first record. One is the count; the rest keep the records
|
|
34
|
+
* vec4-aligned, which is what lets a future pass read them as vec4s. */
|
|
35
|
+
export const LIGHT_HEADER = 4
|
|
36
|
+
/** Floats per light — see the layout above. */
|
|
37
|
+
export const LIGHT_STRIDE = 8
|
|
38
|
+
/**
|
|
39
|
+
* The cap, and it is a real one: the loop below runs per fragment, so this is
|
|
40
|
+
* the number that decides whether lights are free or a cost. Sixteen is the
|
|
41
|
+
* bounded middle tier the design settled on — enough for a stage rig, far below
|
|
42
|
+
* the point where clustering would start to pay for itself.
|
|
43
|
+
*/
|
|
44
|
+
export const MAX_LIGHTS = 16
|
|
45
|
+
/** Floats in the whole buffer. */
|
|
46
|
+
export const LIGHTS_FLOATS = LIGHT_HEADER + MAX_LIGHTS * LIGHT_STRIDE
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `// @lights 3` — how many lights this effect emits.
|
|
50
|
+
*
|
|
51
|
+
* Declared, like every other mount: what the file says is what gets allocated,
|
|
52
|
+
* so an effect that emits none costs no slots and nobody pays for a cap they
|
|
53
|
+
* did not ask for. Clamped rather than rejected, the same choice `@particles`
|
|
54
|
+
* makes — an author asking for a hundred gets the most the engine will give and
|
|
55
|
+
* a scene that still runs.
|
|
56
|
+
*/
|
|
57
|
+
export function parseLightCount(wgsl: string, max: number): number {
|
|
58
|
+
const m = /^\s*\/\/\s*@lights\s+(\d+)\s*$/m.exec(wgsl)
|
|
59
|
+
if (!m) return 0
|
|
60
|
+
return Math.max(1, Math.min(max, parseInt(m[1], 10)))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The RzLight struct, declared in EVERY module a user's source is spliced into.
|
|
65
|
+
*
|
|
66
|
+
* One effect file goes into every module it has a mount in, so a foreground
|
|
67
|
+
* effect that also emits lights compiles its lightEmit inside the FIELD shader
|
|
68
|
+
* too — where nothing calls it, but it still has to resolve. Leaving the struct
|
|
69
|
+
* out of those modules is a compile error on a function the author was right to
|
|
70
|
+
* write, which is the same trap the grid's step-only half documents.
|
|
71
|
+
*/
|
|
72
|
+
export const RZ_LIGHT_STRUCT_WGSL = /* wgsl */ `
|
|
73
|
+
/** What an effect returns for one of its lights. */
|
|
74
|
+
struct RzLight {
|
|
75
|
+
pos: vec3f,
|
|
76
|
+
color: vec3f,
|
|
77
|
+
intensity: f32,
|
|
78
|
+
radius: f32,
|
|
79
|
+
}
|
|
80
|
+
`
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The world's light at a surface facing n — the flat colour, or the installed
|
|
84
|
+
* HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
|
|
85
|
+
* ibl.ts for the folding; the shader is a plain polynomial in the normal).
|
|
86
|
+
*
|
|
87
|
+
* One string included by every module that declares LightUniforms with the sh
|
|
88
|
+
* block — the hosted-api lesson: a helper defined in some modules and not
|
|
89
|
+
* others is a compile error waiting for the first file that crosses them.
|
|
90
|
+
*/
|
|
91
|
+
export const WORLD_AMBIENT_WGSL = /* wgsl */ `
|
|
92
|
+
fn rzWorldAmbient(n: vec3f) -> vec3f {
|
|
93
|
+
if (light.sh[0].w < 0.5) { return light.ambientColor.xyz; }
|
|
94
|
+
let x = n.x;
|
|
95
|
+
let y = n.y;
|
|
96
|
+
let z = n.z;
|
|
97
|
+
let c = light.sh[0].xyz
|
|
98
|
+
+ light.sh[1].xyz * y + light.sh[2].xyz * z + light.sh[3].xyz * x
|
|
99
|
+
+ light.sh[4].xyz * (x * y) + light.sh[5].xyz * (y * z)
|
|
100
|
+
+ light.sh[6].xyz * (3.0 * z * z - 1.0) + light.sh[7].xyz * (x * z)
|
|
101
|
+
+ light.sh[8].xyz * (x * x - y * y);
|
|
102
|
+
return max(c, vec3f(0.0));
|
|
103
|
+
}
|
|
104
|
+
`
|
|
105
|
+
|
|
106
|
+
/** Does this source define the emit mount? */
|
|
107
|
+
export function hasLightEmit(wgsl: string): boolean {
|
|
108
|
+
return /\bfn\s+lightEmit\s*\(/.test(wgsl)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The compute module that runs an effect's lightEmit once per light per frame.
|
|
113
|
+
*
|
|
114
|
+
* fn lightEmit(i: u32, time: f32) -> RzLight
|
|
115
|
+
*
|
|
116
|
+
* A COMPUTE stage rather than a CPU callback, and that is the whole point:
|
|
117
|
+
* Fireworks knows where its bursts are as a closed form in WGSL, and mirroring
|
|
118
|
+
* that on the CPU to place a light would be two derivations of one trajectory
|
|
119
|
+
* that drift apart. Emitting in the shader means the light is wherever the
|
|
120
|
+
* effect says it is, on the scene clock — which is also what makes it survive
|
|
121
|
+
* an offline export frame-stepped at a different rate.
|
|
122
|
+
*
|
|
123
|
+
* The author writes local index 0..n-1 and never learns the global one, the
|
|
124
|
+
* same aliasing the anchor table uses — so installing another effect ahead of
|
|
125
|
+
* this one moves its lights without touching its source.
|
|
126
|
+
*
|
|
127
|
+
* The base arrives in the UNIFORM rather than baked into the text. Baking it
|
|
128
|
+
* would mean recompiling every emitting effect the moment a scene gained or
|
|
129
|
+
* lost a document light, because that is what shifts the slots underneath
|
|
130
|
+
* them — a shader rebuild triggered by moving a lamp.
|
|
131
|
+
*
|
|
132
|
+
* The scene API arrives as a STRING rather than being imported, so this module
|
|
133
|
+
* depends on nothing that depends on it. That API is what lets a lamp aim at
|
|
134
|
+
* someone: Stage Lights points its beams at rzSubject().root, and a light that
|
|
135
|
+
* did not know where she was could only sit where the fixture hangs. It also
|
|
136
|
+
* brings RzLight, which is why this builder does not declare it again.
|
|
137
|
+
*/
|
|
138
|
+
export function buildLightEmitShader(
|
|
139
|
+
wgsl: string,
|
|
140
|
+
sceneApi: string,
|
|
141
|
+
cast: { trailCount: number },
|
|
142
|
+
): string {
|
|
143
|
+
return /* wgsl */ `
|
|
144
|
+
// read_write HERE and read-only in the material shaders. Different passes, so
|
|
145
|
+
// the two never coexist: this compute runs before the scene pass that reads it.
|
|
146
|
+
@group(0) @binding(0) var<storage, read_write> _rzLightsOut: array<f32>;
|
|
147
|
+
// (time, base slot, count, _) — see buildLightEmitShader on why the base is
|
|
148
|
+
// here and not in the text.
|
|
149
|
+
@group(0) @binding(1) var<uniform> _rzLightU: vec4f;
|
|
150
|
+
// The camera block and the cast — the two buffers the scene API reads. Same
|
|
151
|
+
// contents the field and grid modules bind, so an effect's lightEmit sees the
|
|
152
|
+
// scene exactly as its drawing half does.
|
|
153
|
+
@group(0) @binding(2) var<uniform> viewU: array<vec4<f32>, 15>;
|
|
154
|
+
@group(0) @binding(3) var<storage, read> _rzCast: array<vec4f>;
|
|
155
|
+
${sceneApi}
|
|
156
|
+
// Audio and score at 4 and 5, the same bindings the particle and trail modules
|
|
157
|
+
// put them on. A lamp that pulses on the beat or lights on a note is the whole
|
|
158
|
+
// point of a light an effect owns rather than one the document places, so this
|
|
159
|
+
// is not compile-safety padding — it is the mount's reason to exist.
|
|
160
|
+
${audioApi(0, 4)}
|
|
161
|
+
${midiApi(0, 5)}
|
|
162
|
+
${lyricsApi(0, 6)}
|
|
163
|
+
// The rest of the hosted API. Every one of these is here because the AUTHOR'S
|
|
164
|
+
// WHOLE FILE lands below, not because lightEmit needs it: a trail effect that
|
|
165
|
+
// grows a lamp at its tip compiles its ribbon code in this module too. The math
|
|
166
|
+
// helpers and the Particle struct are NOT repeated — they arrive with the scene
|
|
167
|
+
// API above, and a second copy is a redefinition error in engine code.
|
|
168
|
+
${clockApi("_rzLightU.x", "0.0")}
|
|
169
|
+
// Canvas height — the same number the drawing modules read out of their camera
|
|
170
|
+
// struct, both written from canvas.height, so this name means ONE value in
|
|
171
|
+
// every module. Verified against the writers, not assumed: cameraMatrixData[35]
|
|
172
|
+
// and viewU[6].w have the same source.
|
|
173
|
+
${viewportApi("viewU[6].w")}
|
|
174
|
+
${trailSlotsApi(cast.trailCount)}
|
|
175
|
+
${wgsl}
|
|
176
|
+
|
|
177
|
+
@compute @workgroup_size(64)
|
|
178
|
+
fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
|
|
179
|
+
let i = gid.x;
|
|
180
|
+
// The dispatch is sized to the count, but a workgroup is 64 wide and the
|
|
181
|
+
// count rarely is — the tail must not write into the next effect's slots.
|
|
182
|
+
if (i >= u32(_rzLightU.z)) { return; }
|
|
183
|
+
// Time is a PARAMETER, not an rzTime() call: this same source compiles
|
|
184
|
+
// inside the field, particle, trail and grid modules, and those already
|
|
185
|
+
// define rzTime differently or not at all. A parameter needs nothing from
|
|
186
|
+
// the module it lands in, which is why the field mounts take theirs too.
|
|
187
|
+
let l = lightEmit(i, _rzLightU.x);
|
|
188
|
+
// SANITIZED at the one write site, because lightEmit is HOSTED USER CODE and
|
|
189
|
+
// this buffer feeds every fragment of every material: one NaN position would
|
|
190
|
+
// poison the whole frame, and WGSL leaves max(NaN, 0) indeterminate, so it
|
|
191
|
+
// would not even fail the same way on every GPU. A light that fails the
|
|
192
|
+
// check writes zeros — radius 0 is off. Colour is clamped at zero on top:
|
|
193
|
+
// this layer is ADDITIVE, and a negative channel would darken what it lands
|
|
194
|
+
// on and can push HDR negative into bloom.
|
|
195
|
+
let finite = l.pos.x == l.pos.x && l.pos.y == l.pos.y && l.pos.z == l.pos.z &&
|
|
196
|
+
l.radius == l.radius && l.intensity == l.intensity &&
|
|
197
|
+
l.color.x == l.color.x && l.color.y == l.color.y && l.color.z == l.color.z;
|
|
198
|
+
let c = select(vec3f(0.0), max(l.color * l.intensity, vec3f(0.0)), finite);
|
|
199
|
+
let b = ${LIGHT_HEADER}u + (u32(_rzLightU.y) + i) * ${LIGHT_STRIDE}u;
|
|
200
|
+
_rzLightsOut[b] = select(0.0, l.pos.x, finite);
|
|
201
|
+
_rzLightsOut[b + 1u] = select(0.0, l.pos.y, finite);
|
|
202
|
+
_rzLightsOut[b + 2u] = select(0.0, l.pos.z, finite);
|
|
203
|
+
_rzLightsOut[b + 3u] = select(0.0, max(l.radius, 0.0), finite);
|
|
204
|
+
// Colour carries intensity, exactly as the CPU writer stores it — one product,
|
|
205
|
+
// one place, so the two producers cannot disagree about what a slot means.
|
|
206
|
+
_rzLightsOut[b + 4u] = c.x;
|
|
207
|
+
_rzLightsOut[b + 5u] = c.y;
|
|
208
|
+
_rzLightsOut[b + 6u] = c.z;
|
|
209
|
+
}
|
|
210
|
+
`
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The rz*Light accessors, with the buffer declared at the given binding. */
|
|
214
|
+
export function lightsApi(group: number, binding: number): string {
|
|
215
|
+
return /* wgsl */ `
|
|
216
|
+
@group(${group}) @binding(${binding}) var<storage, read> _rzLights: array<f32>;
|
|
217
|
+
|
|
218
|
+
const RZ_MAX_LIGHTS: u32 = ${MAX_LIGHTS}u;
|
|
219
|
+
|
|
220
|
+
/** How many positional lights the scene has. Zero is the ordinary case. */
|
|
221
|
+
fn rzLightCount() -> u32 { return min(u32(_rzLights[0]), RZ_MAX_LIGHTS); }
|
|
222
|
+
|
|
223
|
+
/** Light i's world position. */
|
|
224
|
+
fn rzLightPos(i: u32) -> vec3f {
|
|
225
|
+
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u;
|
|
226
|
+
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** How far light i reaches. Its falloff is zero AT this distance, not merely
|
|
230
|
+
* small, so the light has a bound a cull can be derived from later. */
|
|
231
|
+
fn rzLightRadius(i: u32) -> f32 { return _rzLights[${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 3u]; }
|
|
232
|
+
|
|
233
|
+
/** Light i's colour, already multiplied by its intensity. */
|
|
234
|
+
fn rzLightColor(i: u32) -> vec3f {
|
|
235
|
+
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 4u;
|
|
236
|
+
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Every positional light's contribution at a surface point, as light — not as a
|
|
241
|
+
* finished colour. Multiply by whatever the surface's albedo is.
|
|
242
|
+
*
|
|
243
|
+
* WITH NO LIGHTS THIS RETURNS EXACTLY ZERO and the loop never runs, so a scene
|
|
244
|
+
* that declares none is arithmetically identical to one compiled before lights
|
|
245
|
+
* existed. That is the property the whole feature is gated on: adding this to
|
|
246
|
+
* every material must cost nothing until someone asks for a light.
|
|
247
|
+
*
|
|
248
|
+
* FALLOFF IS RELATIVE TO THE RADIUS, and deliberately not physical.
|
|
249
|
+
*
|
|
250
|
+
* The first version windowed a real inverse-square, and it was unusable: 1/d²
|
|
251
|
+
* is measured in world units, an MMD character is about 18 of them tall, so a
|
|
252
|
+
* lamp two metres off her shoulder divided by 37 and an intensity of 4 landed
|
|
253
|
+
* as 0.06 — invisible. Radius and intensity were fighting, and intensity had no
|
|
254
|
+
* scale a person could learn.
|
|
255
|
+
*
|
|
256
|
+
* So: intensity is the brightness AT the light, radius is where it reaches
|
|
257
|
+
* zero, and the curve between them is the same shape whatever the scene's
|
|
258
|
+
* scale. Both dials now mean what they say, which for a composer beats being
|
|
259
|
+
* right about photons. (1 - t²)² — smooth at both ends, exactly 0 at the
|
|
260
|
+
* radius, so the bound a cull could be derived from is still real.
|
|
261
|
+
*/
|
|
262
|
+
fn rzLightsDiffuse(p: vec3f, n: vec3f) -> vec3f {
|
|
263
|
+
var acc = vec3f(0.0);
|
|
264
|
+
let count = rzLightCount();
|
|
265
|
+
for (var i = 0u; i < count; i = i + 1u) {
|
|
266
|
+
let d = rzLightPos(i) - p;
|
|
267
|
+
let dist = length(d);
|
|
268
|
+
// Facing the light, and nothing behind it. No wrap or half-lambert: this
|
|
269
|
+
// layer adds light, and a wrapped term would lift the shadow side, which is
|
|
270
|
+
// the ramp's business and not this one's.
|
|
271
|
+
let ndl = max(dot(n, d / max(dist, 1e-4)), 0.0);
|
|
272
|
+
if (ndl <= 0.0) { continue; }
|
|
273
|
+
let t = clamp(dist / max(rzLightRadius(i), 1e-4), 0.0, 1.0);
|
|
274
|
+
let falloff = 1.0 - t * t;
|
|
275
|
+
acc = acc + rzLightColor(i) * (ndl * falloff * falloff);
|
|
276
|
+
}
|
|
277
|
+
return acc;
|
|
278
|
+
}
|
|
279
|
+
`
|
|
280
|
+
}
|