reze-engine 0.43.0 → 0.50.1
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 +53 -429
- package/dist/animation.d.ts +26 -0
- package/dist/animation.d.ts.map +1 -1
- package/dist/animation.js +42 -0
- 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 +841 -53
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +3498 -451
- 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 +19 -14
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +31 -2
- 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/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 +28 -22
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +165 -138
- 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 +10 -2
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +37 -109
- 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 +14 -2
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +55 -90
- 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/animation.ts +41 -0
- package/src/camera.ts +31 -8
- package/src/engine.ts +4015 -556
- 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 +34 -4
- 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/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 +182 -139
- 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 +54 -112
- package/src/shaders/passes/scene-contract.ts +266 -0
- package/src/shaders/passes/trails.ts +77 -93
- package/src/shadow-cascades.ts +97 -0
- package/src/vmd-loader.ts +2 -2
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { sceneFsOutWgsl } from "../passes/scene-contract"
|
|
2
|
+
import { lightsApi } from "../lights"
|
|
3
|
+
import { SHADOW_CASCADES } from "../../shadow-cascades"
|
|
4
|
+
import { WORLD_AMBIENT_WGSL } from "../lights"
|
|
5
|
+
|
|
1
6
|
// Shared WGSL blocks concatenated by every material shader.
|
|
2
7
|
// Splits the boilerplate (uniform structs, bind group layout, skinning VS, PCF shadow)
|
|
3
8
|
// away from the per-material fragment code so each material file only contains what
|
|
@@ -37,6 +42,8 @@ struct Light {
|
|
|
37
42
|
struct LightUniforms {
|
|
38
43
|
ambientColor: vec4f,
|
|
39
44
|
lights: array<Light, 4>,
|
|
45
|
+
/** Irradiance SH of the HDRI world; [0].w is the on-flag. */
|
|
46
|
+
sh: array<vec4f, 9>,
|
|
40
47
|
};
|
|
41
48
|
|
|
42
49
|
// Per-material uniforms. Every material binds this layout even if it ignores fields;
|
|
@@ -53,8 +60,15 @@ struct MaterialUniforms {
|
|
|
53
60
|
// Skeleton index of the 頭 (head) bone, or -1. Lets the eye shader gate
|
|
54
61
|
// the post-alpha-eye stencil by camera-vs-face hemisphere.
|
|
55
62
|
headBoneIndex: f32,
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
// The draw's identity, for the id attachment: which material, which object.
|
|
64
|
+
// They ride HERE, in padding this struct already carried, so the buffer's
|
|
65
|
+
// size and layout are untouched and the indirect-draw path keeps working —
|
|
66
|
+
// the ids arrive with the same per-draw uniform the material already binds,
|
|
67
|
+
// rather than needing a channel of their own. Zero while ids are off, and
|
|
68
|
+
// zero is the reserved "nothing" value, so reading them then is not wrong,
|
|
69
|
+
// just empty. f32 because the buffer is written as floats; the shader casts.
|
|
70
|
+
materialId: f32,
|
|
71
|
+
objectId: f32,
|
|
58
72
|
_pad2: f32,
|
|
59
73
|
};
|
|
60
74
|
|
|
@@ -72,7 +86,9 @@ struct VertexOutput {
|
|
|
72
86
|
@location(3) restPos: vec3f,
|
|
73
87
|
};
|
|
74
88
|
|
|
75
|
-
|
|
89
|
+
// One view-projection per shadow cascade, inner to outer — the volumes built
|
|
90
|
+
// by shadow-cascades.ts, in the same order.
|
|
91
|
+
struct LightVP { viewProj: array<mat4x4f, ${SHADOW_CASCADES.length}>, };
|
|
76
92
|
|
|
77
93
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
78
94
|
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
@@ -80,6 +96,10 @@ struct LightVP { viewProj: mat4x4f, };
|
|
|
80
96
|
@group(0) @binding(3) var shadowMap: texture_depth_2d;
|
|
81
97
|
@group(0) @binding(4) var shadowSampler: sampler_comparison;
|
|
82
98
|
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
99
|
+
// The far cascade's map — coarser texels over a much wider box, so the stage
|
|
100
|
+
// keeps its shadows when the crisp near volume ends. Binding 7: 6 is the
|
|
101
|
+
// positional lights, 9 the BRDF LUT.
|
|
102
|
+
@group(0) @binding(7) var shadowMapFar: texture_depth_2d;
|
|
83
103
|
// binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
|
|
84
104
|
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
85
105
|
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
@@ -111,33 +131,62 @@ fn safe_normal(nIn: vec3f) -> vec3f {
|
|
|
111
131
|
|
|
112
132
|
`;
|
|
113
133
|
|
|
114
|
-
// ─── Shadow sampler (3×3 PCF)
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// PCF taps landed TWO texels apart
|
|
119
|
-
//
|
|
134
|
+
// ─── Shadow sampler (3×3 PCF, cascade-selected) ─────────────────────
|
|
135
|
+
// Normal-bias 0.08, depth-bias 0.001 NDC. Unrolled — Safari's Metal backend
|
|
136
|
+
// doesn't unroll nested shadow loops reliably. Texel sizes are interpolated
|
|
137
|
+
// from SHADOW_CASCADES so they cannot go stale the way the hardcoded 1/2048
|
|
138
|
+
// once did (PCF taps landed TWO texels apart after the map grew to 4096,
|
|
139
|
+
// quantizing self-shadow edges into crawling gray squares).
|
|
140
|
+
//
|
|
141
|
+
// Selection: the crisp near cascade wherever its box covers the point — with a
|
|
142
|
+
// margin that keeps the whole PCF kernel inside the map, so selection never
|
|
143
|
+
// mixes maps mid-kernel — else the far cascade, else LIT. Lit is a fix, not
|
|
144
|
+
// merely a default: the old single-volume path clamped, and past the light's
|
|
145
|
+
// far plane the comparison failed against every stored depth, silently
|
|
146
|
+
// shadowing any stage deeper than the box.
|
|
147
|
+
|
|
148
|
+
const pcf9 = (map: string, ts: string) => /* wgsl */ `
|
|
149
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
150
|
+
let cmpZ = ndc.z - 0.001;
|
|
151
|
+
let ts = ${ts};
|
|
152
|
+
let s00 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
153
|
+
let s10 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
154
|
+
let s20 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
155
|
+
let s01 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
156
|
+
let s11 = textureSampleCompareLevel(${map}, shadowSampler, suv, cmpZ);
|
|
157
|
+
let s21 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
158
|
+
let s02 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
159
|
+
let s12 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
160
|
+
let s22 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
161
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
162
|
+
`
|
|
120
163
|
|
|
121
|
-
|
|
164
|
+
const SAMPLE_SHADOW_WGSL = /* wgsl */ `
|
|
165
|
+
|
|
166
|
+
fn sampleShadowNear(ndc: vec3f) -> f32 {
|
|
167
|
+
${pcf9("shadowMap", `1.0 / ${SHADOW_CASCADES[0].mapSize}.0`)}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fn sampleShadowFar(ndc: vec3f) -> f32 {
|
|
171
|
+
${pcf9("shadowMapFar", `1.0 / ${SHADOW_CASCADES[SHADOW_CASCADES.length - 1].mapSize}.0`)}
|
|
172
|
+
}
|
|
122
173
|
|
|
123
174
|
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
124
175
|
if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
|
|
125
176
|
let biasedPos = worldPos + n * 0.08;
|
|
126
|
-
let
|
|
127
|
-
let
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
let
|
|
132
|
-
let
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
140
|
-
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
177
|
+
let c0 = lightVP.viewProj[0] * vec4f(biasedPos, 1.0);
|
|
178
|
+
let n0 = c0.xyz / max(c0.w, 1e-6);
|
|
179
|
+
if (all(abs(n0.xy) < vec2f(0.98)) && n0.z > 0.0 && n0.z < 1.0) {
|
|
180
|
+
return sampleShadowNear(n0);
|
|
181
|
+
}
|
|
182
|
+
let c1 = lightVP.viewProj[1] * vec4f(biasedPos, 1.0);
|
|
183
|
+
let n1 = c1.xyz / max(c1.w, 1e-6);
|
|
184
|
+
if (all(abs(n1.xy) < vec2f(0.98)) && n1.z > 0.0 && n1.z < 1.0) {
|
|
185
|
+
return sampleShadowFar(n1);
|
|
186
|
+
}
|
|
187
|
+
// Outside every cascade there is no occlusion information; lit is the only
|
|
188
|
+
// honest answer.
|
|
189
|
+
return 1.0;
|
|
141
190
|
}
|
|
142
191
|
|
|
143
192
|
`;
|
|
@@ -148,7 +197,7 @@ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
|
148
197
|
// VS normalize on the outgoing normal is skipped — interpolation denormalizes it
|
|
149
198
|
// anyway and every fragment shader does `normalize(input.normal)` as its first line.
|
|
150
199
|
|
|
151
|
-
|
|
200
|
+
const COMMON_VS_WGSL = /* wgsl */ `
|
|
152
201
|
|
|
153
202
|
@vertex fn vs(
|
|
154
203
|
@location(0) position: vec3f,
|
|
@@ -200,18 +249,23 @@ export const COMMON_VS_WGSL = /* wgsl */ `
|
|
|
200
249
|
// out.r = mask_r · src.a + dst.r · (1-src.a) (bloom mask, weighted by alpha)
|
|
201
250
|
// out.g = 1.0 · src.a + dst.g · (1-src.a) (canonical premultiplied alpha-over)
|
|
202
251
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
252
|
+
// The struct itself comes from scene-contract, which owns what the scene pass's
|
|
253
|
+
// attachments ARE — this is one of the two shaders that gains an output when
|
|
254
|
+
// one is added, and the graph generator emits against this same declaration.
|
|
255
|
+
//
|
|
256
|
+
// A FUNCTION, and no longer part of the prelude constant below, because the id
|
|
257
|
+
// attachment is a device capability probed at init: a struct baked at import
|
|
258
|
+
// time would be built before the answer exists. The graph appends it where the
|
|
259
|
+
// constant used to end, so the assembled module is unchanged.
|
|
260
|
+
export function commonFsOutWgsl(): string {
|
|
261
|
+
return `\n\n${sceneFsOutWgsl()}\n`;
|
|
262
|
+
}
|
|
211
263
|
|
|
212
264
|
// ─── Convenience: full shared prelude ───────────────────────────────
|
|
213
265
|
// Material files compose this as `${NODES_WGSL}${COMMON_MATERIAL_PRELUDE_WGSL}` to
|
|
214
266
|
// pull in everything structural. Each material then adds its own constants + fs().
|
|
215
267
|
|
|
268
|
+
// The FSOut struct is NOT in here any more — see commonFsOutWgsl above. Every
|
|
269
|
+
// consumer of this constant appends it immediately, which is where it was.
|
|
216
270
|
export const COMMON_MATERIAL_PRELUDE_WGSL =
|
|
217
|
-
COMMON_BINDINGS_WGSL +
|
|
271
|
+
COMMON_BINDINGS_WGSL + WORLD_AMBIENT_WGSL + lightsApi(0, 6) + SAMPLE_SHADOW_WGSL + COMMON_VS_WGSL
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
|
|
31
|
+
/** Floats before the key map: count, pitch range, clock, duration, release. */
|
|
32
|
+
export const MIDI_HEADER = 8
|
|
33
|
+
/** One energy slot per MIDI pitch. 128 is the MIDI range, not a cap we chose. */
|
|
34
|
+
export const MIDI_KEYS = 128
|
|
35
|
+
/** Where the note records begin. */
|
|
36
|
+
export const MIDI_NOTES = MIDI_HEADER + MIDI_KEYS
|
|
37
|
+
/** Floats per note: start, duration, pitch, velocity. */
|
|
38
|
+
export const MIDI_STRIDE = 4
|
|
39
|
+
|
|
40
|
+
/** The rzNote*/ /* rzKey* accessors, with the buffer declared at the given binding. */
|
|
41
|
+
export function midiApi(group: number, binding: number): string {
|
|
42
|
+
return /* wgsl */ `
|
|
43
|
+
@group(${group}) @binding(${binding}) var<storage, read> _rzMidi: array<f32>;
|
|
44
|
+
|
|
45
|
+
/** Notes in the score; 0 when none is loaded, which every accessor below
|
|
46
|
+
* tolerates by returning zero rather than reading past the end. */
|
|
47
|
+
fn rzNoteCount() -> i32 { return i32(_rzMidi[0]); }
|
|
48
|
+
/** Where the score is NOW, in seconds — the clock every age below hangs from. */
|
|
49
|
+
fn rzMidiTime() -> f32 { return _rzMidi[3]; }
|
|
50
|
+
/** 1 while the score is advancing, 0 paused or absent. */
|
|
51
|
+
fn rzMidiPlaying() -> f32 { return select(0.0, _rzMidi[4], i32(_rzMidi[0]) > 0); }
|
|
52
|
+
/** Last note-off, in seconds — the length of the piece. */
|
|
53
|
+
fn rzMidiDuration() -> f32 { return _rzMidi[5]; }
|
|
54
|
+
/** Lowest and highest pitch the score actually uses. Lay a keyboard out against
|
|
55
|
+
* these rather than against 0..127 and a piano piece fills the screen instead
|
|
56
|
+
* of occupying its middle third. */
|
|
57
|
+
fn rzPitchLow() -> f32 { return _rzMidi[1]; }
|
|
58
|
+
fn rzPitchHigh() -> f32 { return _rzMidi[2]; }
|
|
59
|
+
|
|
60
|
+
fn _rzNoteAt(i: i32, field: i32) -> f32 {
|
|
61
|
+
if (i < 0 || i >= i32(_rzMidi[0])) { return 0.0; }
|
|
62
|
+
return _rzMidi[${MIDI_NOTES} + i * ${MIDI_STRIDE} + field];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** When note i begins, in seconds. */
|
|
66
|
+
fn rzNoteStart(i: i32) -> f32 { return _rzNoteAt(i, 0); }
|
|
67
|
+
/** How long note i sounds, in seconds. */
|
|
68
|
+
fn rzNoteLength(i: i32) -> f32 { return _rzNoteAt(i, 1); }
|
|
69
|
+
/** MIDI pitch of note i — 60 is middle C. */
|
|
70
|
+
fn rzNotePitch(i: i32) -> f32 { return _rzNoteAt(i, 2); }
|
|
71
|
+
/** How hard note i was struck, 0..1. */
|
|
72
|
+
fn rzNoteVelocity(i: i32) -> f32 { return _rzNoteAt(i, 3); }
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Seconds since note i began — NEGATIVE before it does.
|
|
76
|
+
*
|
|
77
|
+
* The falling-note function, and the sign is the whole of it: a note's height is
|
|
78
|
+
* -rzNoteAge(i) * speed, so a note not yet played sits above the keyboard at a
|
|
79
|
+
* distance proportional to how long is left, crosses zero exactly as it sounds,
|
|
80
|
+
* and keeps going. One expression, no state, no spawning — which is why one
|
|
81
|
+
* particle per note is the natural mapping and the pool index IS the note index.
|
|
82
|
+
*/
|
|
83
|
+
fn rzNoteAge(i: i32) -> f32 { return _rzMidi[3] - rzNoteStart(i); }
|
|
84
|
+
|
|
85
|
+
/** 1 while note i is sounding, 0 either side of it. */
|
|
86
|
+
fn rzNoteHeld(i: i32) -> f32 {
|
|
87
|
+
let age = rzNoteAge(i);
|
|
88
|
+
return select(0.0, 1.0, age >= 0.0 && age < rzNoteLength(i));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Energy at a MIDI pitch right now: 1 the instant it is struck, holding while
|
|
93
|
+
* sounding, then falling away over the release set at install time.
|
|
94
|
+
*
|
|
95
|
+
* Per PITCH rather than per note, because that is the question a keyboard asks
|
|
96
|
+
* and a note list cannot answer cheaply — see the header. Fractional pitches
|
|
97
|
+
* read the nearest key, so a caller can hand this a continuous x across the
|
|
98
|
+
* keyboard without rounding first.
|
|
99
|
+
*/
|
|
100
|
+
fn rzKeyEnergy(pitch: f32) -> f32 {
|
|
101
|
+
let k = i32(round(pitch));
|
|
102
|
+
if (k < 0 || k >= ${MIDI_KEYS}) { return 0.0; }
|
|
103
|
+
return _rzMidi[${MIDI_HEADER} + k];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A pitch's place across the score's own range, 0..1 — the x of a falling note
|
|
108
|
+
* and of the key it lands on, which have to agree or the effect is nonsense.
|
|
109
|
+
*/
|
|
110
|
+
fn rzPitchX(pitch: f32) -> f32 {
|
|
111
|
+
let lo = _rzMidi[1];
|
|
112
|
+
let hi = _rzMidi[2];
|
|
113
|
+
return select(0.5, (pitch - lo) / max(hi - lo, 1.0), hi > lo);
|
|
114
|
+
}
|
|
115
|
+
`
|
|
116
|
+
}
|