reze-engine 0.54.16 → 0.55.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/dist/engine.d.ts +210 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1063 -11
- package/dist/graph/presets/pool_floor.d.ts +3 -0
- package/dist/graph/presets/pool_floor.d.ts.map +1 -0
- package/dist/graph/presets/pool_floor.js +42 -0
- package/dist/graph/presets/water.d.ts +3 -0
- package/dist/graph/presets/water.d.ts.map +1 -0
- package/dist/graph/presets/water.js +31 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +10 -0
- package/dist/overlay.d.ts +200 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +806 -0
- package/dist/physics/autofit.d.ts +147 -0
- package/dist/physics/autofit.d.ts.map +1 -0
- package/dist/physics/autofit.js +501 -0
- package/dist/pmx-document.d.ts +218 -0
- package/dist/pmx-document.d.ts.map +1 -0
- package/dist/pmx-document.js +685 -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/ground-noise.d.ts +7 -0
- package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
- package/dist/shaders/passes/ground-noise.js +88 -0
- package/dist/shaders/passes/overlay.d.ts +3 -0
- package/dist/shaders/passes/overlay.d.ts.map +1 -0
- package/dist/shaders/passes/overlay.js +164 -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/wireframe.d.ts +2 -0
- package/dist/shaders/passes/wireframe.d.ts.map +1 -0
- package/dist/shaders/passes/wireframe.js +107 -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/package.json +1 -1
- package/src/engine.ts +1147 -13
- package/src/index.ts +50 -0
- package/src/model.ts +9 -0
- package/src/overlay.ts +1000 -0
- package/src/pmx-document.ts +882 -0
- package/src/shaders/passes/overlay.ts +166 -0
- package/src/shaders/passes/wireframe.ts +108 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// The mesh itself, as lines, in the overlay layer.
|
|
2
|
+
//
|
|
3
|
+
// It skins here rather than reading the loader's CPU positions, and that is most
|
|
4
|
+
// of why the pass exists. What is on screen is skinned and morphed on the GPU;
|
|
5
|
+
// the CPU-side array is BIND POSE. Draw from that and the wireframe sits
|
|
6
|
+
// perfectly on a T-posed model and slides off every animated one — which is
|
|
7
|
+
// exactly the state a user is in while looking at weights.
|
|
8
|
+
//
|
|
9
|
+
// Each edge is expanded into a screen-space QUAD rather than drawn with line
|
|
10
|
+
// topology. WebGPU pins a line to one device pixel, and one device pixel on a 2x
|
|
11
|
+
// display is half a CSS pixel, which 4x MSAA then spreads across two of them:
|
|
12
|
+
// the result is a grey suggestion of a wireframe rather than a wireframe. So the
|
|
13
|
+
// draw is six vertices per edge, instanced, and the mesh is read through STORAGE
|
|
14
|
+
// buffers — there is no per-vertex stream, because a quad's two corners come
|
|
15
|
+
// from two different vertices of the model.
|
|
16
|
+
//
|
|
17
|
+
// The depth prepass shares this module's `vs` through its own entry point: it
|
|
18
|
+
// draws the solid triangles with vertex buffers, writing depth only, so the
|
|
19
|
+
// wireframe can be occluded by the body it belongs to.
|
|
20
|
+
export const WIREFRAME_SHADER_WGSL = /* wgsl */ `
|
|
21
|
+
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _pad: f32 };
|
|
22
|
+
struct WireUniforms { color: vec4f, viewport: vec2f, thicknessPx: f32, _pad: f32 };
|
|
23
|
+
|
|
24
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
25
|
+
@group(0) @binding(1) var<uniform> wire: WireUniforms;
|
|
26
|
+
|
|
27
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
28
|
+
// The model's own vertex buffer: pos(3), normal(3), uv(2) — 8 floats a vertex.
|
|
29
|
+
@group(1) @binding(1) var<storage, read> vertices: array<f32>;
|
|
30
|
+
// uint16x4 per vertex, so two u32 each.
|
|
31
|
+
@group(1) @binding(2) var<storage, read> joints: array<u32>;
|
|
32
|
+
// unorm8x4 per vertex, one u32 each.
|
|
33
|
+
@group(1) @binding(3) var<storage, read> weights: array<u32>;
|
|
34
|
+
// Pairs of vertex indices, one pair an edge.
|
|
35
|
+
@group(1) @binding(4) var<storage, read> edges: array<u32>;
|
|
36
|
+
|
|
37
|
+
fn skinnedPos(i: u32) -> vec3f {
|
|
38
|
+
let o = i * 8u;
|
|
39
|
+
let rest = vec4f(vertices[o], vertices[o + 1u], vertices[o + 2u], 1.0);
|
|
40
|
+
|
|
41
|
+
let j0 = joints[i * 2u];
|
|
42
|
+
let j1 = joints[i * 2u + 1u];
|
|
43
|
+
let packed = weights[i];
|
|
44
|
+
var w = vec4f(
|
|
45
|
+
f32(packed & 0xffu),
|
|
46
|
+
f32((packed >> 8u) & 0xffu),
|
|
47
|
+
f32((packed >> 16u) & 0xffu),
|
|
48
|
+
f32((packed >> 24u) & 0xffu),
|
|
49
|
+
) / 255.0;
|
|
50
|
+
let sum = w.x + w.y + w.z + w.w;
|
|
51
|
+
w = select(vec4f(1.0, 0.0, 0.0, 0.0), w / max(sum, 1e-4), sum > 0.0001);
|
|
52
|
+
|
|
53
|
+
var acc = vec4f(0.0);
|
|
54
|
+
acc += (skinMats[j0 & 0xffffu] * rest) * w.x;
|
|
55
|
+
acc += (skinMats[j0 >> 16u] * rest) * w.y;
|
|
56
|
+
acc += (skinMats[j1 & 0xffffu] * rest) * w.z;
|
|
57
|
+
acc += (skinMats[j1 >> 16u] * rest) * w.w;
|
|
58
|
+
return acc.xyz;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Six vertices an edge: (a,-1) (a,+1) (b,-1) (a,+1) (b,+1) (b,-1).
|
|
62
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> @builtin(position) vec4f {
|
|
63
|
+
let a = edges[ii * 2u];
|
|
64
|
+
let b = edges[ii * 2u + 1u];
|
|
65
|
+
let vp = camera.projection * camera.view;
|
|
66
|
+
let ca = vp * vec4f(skinnedPos(a), 1.0);
|
|
67
|
+
let cb = vp * vec4f(skinnedPos(b), 1.0);
|
|
68
|
+
|
|
69
|
+
let atB = vi == 2u || vi == 4u || vi == 5u;
|
|
70
|
+
let side = select(-1.0, 1.0, vi == 1u || vi == 3u || vi == 4u);
|
|
71
|
+
let own = select(ca, cb, atB);
|
|
72
|
+
let other = select(cb, ca, atB);
|
|
73
|
+
|
|
74
|
+
let half = 0.5 * wire.viewport;
|
|
75
|
+
let s0 = (own.xy / max(abs(own.w), 1e-6)) * half;
|
|
76
|
+
let s1 = (other.xy / max(abs(other.w), 1e-6)) * half;
|
|
77
|
+
let d = s1 - s0;
|
|
78
|
+
// A degenerate edge has no direction on screen; any perpendicular will do,
|
|
79
|
+
// and it is a dot either way.
|
|
80
|
+
let tangent = select(vec2f(1.0, 0.0), normalize(d), length(d) > 1e-6);
|
|
81
|
+
let normalPx = vec2f(-tangent.y, tangent.x);
|
|
82
|
+
let offsetPx = normalPx * side * wire.thicknessPx * 0.5;
|
|
83
|
+
return vec4f(own.xy + (offsetPx / half) * own.w, own.z, own.w);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The depth prepass: the solid mesh through the ordinary vertex stream, writing
|
|
87
|
+
// depth and nothing else, so the far side of a body is hidden behind the near.
|
|
88
|
+
@vertex fn vsDepth(
|
|
89
|
+
@location(0) position: vec3f,
|
|
90
|
+
@location(1) joints0: vec4<u32>,
|
|
91
|
+
@location(2) weights0: vec4<f32>,
|
|
92
|
+
) -> @builtin(position) vec4f {
|
|
93
|
+
let rest = vec4f(position, 1.0);
|
|
94
|
+
let sum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
95
|
+
let w = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 / max(sum, 1e-4), sum > 0.0001);
|
|
96
|
+
var acc = vec4f(0.0);
|
|
97
|
+
for (var i = 0u; i < 4u; i++) {
|
|
98
|
+
acc += (skinMats[joints0[i]] * rest) * w[i];
|
|
99
|
+
}
|
|
100
|
+
return camera.projection * camera.view * vec4f(acc.xyz, 1.0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
104
|
+
// Premultiplied, like every other fragment in this layer.
|
|
105
|
+
return vec4f(wire.color.rgb * wire.color.a, wire.color.a);
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Floats before the key map: count, pitch range, clock, duration, release. */
|
|
2
|
+
export declare const SCORE_HEADER = 8;
|
|
3
|
+
/** One energy slot per MIDI pitch. 128 is the MIDI range, not a cap we chose. */
|
|
4
|
+
export declare const SCORE_KEYS = 128;
|
|
5
|
+
/** Where the note records begin. */
|
|
6
|
+
export declare const SCORE_NOTES: number;
|
|
7
|
+
/** Floats per note: start, duration, pitch, velocity. */
|
|
8
|
+
export declare const SCORE_STRIDE = 4;
|
|
9
|
+
/** The rzNote*/ export declare function scoreApi(group: number, binding: number): string;
|
|
10
|
+
//# sourceMappingURL=score-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score-api.d.ts","sourceRoot":"","sources":["../../src/shaders/score-api.ts"],"names":[],"mappings":"AA8BA,+EAA+E;AAC/E,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,iFAAiF;AACjF,eAAO,MAAM,UAAU,MAAM,CAAA;AAC7B,oCAAoC;AACpC,eAAO,MAAM,WAAW,QAA4B,CAAA;AACpD,yDAAyD;AACzD,eAAO,MAAM,YAAY,IAAI,CAAA;AAE7B,gBAAgB,CAChB,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA2E/D"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The score data interface, as WGSL — shared verbatim by every effect module.
|
|
2
|
+
//
|
|
3
|
+
// The sibling of the audio interface, and precomputed for the same reason: an
|
|
4
|
+
// export steps the engine frame by frame rather than playing in real time, so
|
|
5
|
+
// anything read live from a player would render empty into the exported video.
|
|
6
|
+
// A note list is inherently precomputed, so it cannot even be tempted.
|
|
7
|
+
//
|
|
8
|
+
// WHY THIS EXISTS BESIDE rzAudio* RATHER THAN INSTEAD OF IT. The audio interface
|
|
9
|
+
// answers "how loud is the bass right now" — a spectrum, smeared across
|
|
10
|
+
// frequency and time. That is the right shape for a reactive backdrop and the
|
|
11
|
+
// wrong shape for a note: you cannot recover a discrete onset, a pitch and a
|
|
12
|
+
// duration from band energy, and the crispness of a falling note is the whole
|
|
13
|
+
// point of the look. The two are complementary — a scene can pulse its fog on
|
|
14
|
+
// rzAudioOnset while its notes fall on rzNoteAge.
|
|
15
|
+
//
|
|
16
|
+
// LAYOUT. An 8-float header, then a 128-float live key map, then the notes at 4
|
|
17
|
+
// floats each (start, duration, pitch, velocity). `time` and `playing` are
|
|
18
|
+
// rewritten every frame by whoever owns playback, exactly as the audio header
|
|
19
|
+
// is, so "now" is always the clock actually running.
|
|
20
|
+
//
|
|
21
|
+
// THE KEY MAP IS THE POINT OF THE DESIGN. Falling notes index the note list
|
|
22
|
+
// directly — one particle per note, which is what makes the geometry trivial.
|
|
23
|
+
// But a keyboard glow asks the opposite question, per pixel: is anything
|
|
24
|
+
// sounding at THIS pitch? Answering that by scanning the note list would be
|
|
25
|
+
// thousands of iterations per fragment. So the engine keeps a 128-entry
|
|
26
|
+
// per-pitch energy map, updated once per frame on the CPU, and the glow becomes
|
|
27
|
+
// one lookup — the same trade the audio interface makes by precomputing onset
|
|
28
|
+
// so "a beat-triggered effect is one comparison instead of a per-pixel history
|
|
29
|
+
// scan".
|
|
30
|
+
/** Floats before the key map: count, pitch range, clock, duration, release. */
|
|
31
|
+
export const SCORE_HEADER = 8;
|
|
32
|
+
/** One energy slot per MIDI pitch. 128 is the MIDI range, not a cap we chose. */
|
|
33
|
+
export const SCORE_KEYS = 128;
|
|
34
|
+
/** Where the note records begin. */
|
|
35
|
+
export const SCORE_NOTES = SCORE_HEADER + SCORE_KEYS;
|
|
36
|
+
/** Floats per note: start, duration, pitch, velocity. */
|
|
37
|
+
export const SCORE_STRIDE = 4;
|
|
38
|
+
/** The rzNote*/ /* rzKey* accessors, with the buffer declared at the given binding. */
|
|
39
|
+
export function scoreApi(group, binding) {
|
|
40
|
+
return /* wgsl */ `
|
|
41
|
+
@group(${group}) @binding(${binding}) var<storage, read> _rzScore: array<f32>;
|
|
42
|
+
|
|
43
|
+
/** Notes in the score; 0 when none is loaded, which every accessor below
|
|
44
|
+
* tolerates by returning zero rather than reading past the end. */
|
|
45
|
+
fn rzNoteCount() -> i32 { return i32(_rzScore[0]); }
|
|
46
|
+
/** Where the score is NOW, in seconds — the clock every age below hangs from. */
|
|
47
|
+
fn rzScoreTime() -> f32 { return _rzScore[3]; }
|
|
48
|
+
/** 1 while the score is advancing, 0 paused or absent. */
|
|
49
|
+
fn rzScorePlaying() -> f32 { return select(0.0, _rzScore[4], i32(_rzScore[0]) > 0); }
|
|
50
|
+
/** Last note-off, in seconds — the length of the piece. */
|
|
51
|
+
fn rzScoreDuration() -> f32 { return _rzScore[5]; }
|
|
52
|
+
/** Lowest and highest pitch the score actually uses. Lay a keyboard out against
|
|
53
|
+
* these rather than against 0..127 and a piano piece fills the screen instead
|
|
54
|
+
* of occupying its middle third. */
|
|
55
|
+
fn rzPitchLow() -> f32 { return _rzScore[1]; }
|
|
56
|
+
fn rzPitchHigh() -> f32 { return _rzScore[2]; }
|
|
57
|
+
|
|
58
|
+
fn _rzNoteAt(i: i32, field: i32) -> f32 {
|
|
59
|
+
if (i < 0 || i >= i32(_rzScore[0])) { return 0.0; }
|
|
60
|
+
return _rzScore[${SCORE_NOTES} + i * ${SCORE_STRIDE} + field];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** When note i begins, in seconds. */
|
|
64
|
+
fn rzNoteStart(i: i32) -> f32 { return _rzNoteAt(i, 0); }
|
|
65
|
+
/** How long note i sounds, in seconds. */
|
|
66
|
+
fn rzNoteLength(i: i32) -> f32 { return _rzNoteAt(i, 1); }
|
|
67
|
+
/** MIDI pitch of note i — 60 is middle C. */
|
|
68
|
+
fn rzNotePitch(i: i32) -> f32 { return _rzNoteAt(i, 2); }
|
|
69
|
+
/** How hard note i was struck, 0..1. */
|
|
70
|
+
fn rzNoteVelocity(i: i32) -> f32 { return _rzNoteAt(i, 3); }
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Seconds since note i began — NEGATIVE before it does.
|
|
74
|
+
*
|
|
75
|
+
* The falling-note function, and the sign is the whole of it: a note's height is
|
|
76
|
+
* -rzNoteAge(i) * speed, so a note not yet played sits above the keyboard at a
|
|
77
|
+
* distance proportional to how long is left, crosses zero exactly as it sounds,
|
|
78
|
+
* and keeps going. One expression, no state, no spawning — which is why one
|
|
79
|
+
* particle per note is the natural mapping and the pool index IS the note index.
|
|
80
|
+
*/
|
|
81
|
+
fn rzNoteAge(i: i32) -> f32 { return _rzScore[3] - rzNoteStart(i); }
|
|
82
|
+
|
|
83
|
+
/** 1 while note i is sounding, 0 either side of it. */
|
|
84
|
+
fn rzNoteHeld(i: i32) -> f32 {
|
|
85
|
+
let age = rzNoteAge(i);
|
|
86
|
+
return select(0.0, 1.0, age >= 0.0 && age < rzNoteLength(i));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Energy at a MIDI pitch right now: 1 the instant it is struck, holding while
|
|
91
|
+
* sounding, then falling away over the release set at install time.
|
|
92
|
+
*
|
|
93
|
+
* Per PITCH rather than per note, because that is the question a keyboard asks
|
|
94
|
+
* and a note list cannot answer cheaply — see the header. Fractional pitches
|
|
95
|
+
* read the nearest key, so a caller can hand this a continuous x across the
|
|
96
|
+
* keyboard without rounding first.
|
|
97
|
+
*/
|
|
98
|
+
fn rzKeyEnergy(pitch: f32) -> f32 {
|
|
99
|
+
let k = i32(round(pitch));
|
|
100
|
+
if (k < 0 || k >= ${SCORE_KEYS}) { return 0.0; }
|
|
101
|
+
return _rzScore[${SCORE_HEADER} + k];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A pitch's place across the score's own range, 0..1 — the x of a falling note
|
|
106
|
+
* and of the key it lands on, which have to agree or the effect is nonsense.
|
|
107
|
+
*/
|
|
108
|
+
fn rzPitchX(pitch: f32) -> f32 {
|
|
109
|
+
let lo = _rzScore[1];
|
|
110
|
+
let hi = _rzScore[2];
|
|
111
|
+
return select(0.5, (pitch - lo) / max(hi - lo, 1.0), hi > lo);
|
|
112
|
+
}
|
|
113
|
+
`;
|
|
114
|
+
}
|