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.
Files changed (140) hide show
  1. package/README.md +53 -429
  2. package/dist/animation.d.ts +26 -0
  3. package/dist/animation.d.ts.map +1 -1
  4. package/dist/animation.js +42 -0
  5. package/dist/camera.d.ts +3 -0
  6. package/dist/camera.d.ts.map +1 -1
  7. package/dist/camera.js +33 -8
  8. package/dist/engine.d.ts +841 -53
  9. package/dist/engine.d.ts.map +1 -1
  10. package/dist/engine.js +3498 -451
  11. package/dist/graph/registry.d.ts +2 -2
  12. package/dist/graph/registry.d.ts.map +1 -1
  13. package/dist/graph/registry.js +1 -1
  14. package/dist/graph/slots.d.ts +0 -1
  15. package/dist/graph/slots.d.ts.map +1 -1
  16. package/dist/graph/slots.js +37 -9
  17. package/dist/hdr.d.ts +18 -0
  18. package/dist/hdr.d.ts.map +1 -0
  19. package/dist/hdr.js +162 -0
  20. package/dist/ibl.d.ts +19 -0
  21. package/dist/ibl.d.ts.map +1 -0
  22. package/dist/ibl.js +113 -0
  23. package/dist/ik-solver.d.ts +2 -1
  24. package/dist/ik-solver.d.ts.map +1 -1
  25. package/dist/index.d.ts +5 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +10 -0
  28. package/dist/math.d.ts +20 -1
  29. package/dist/math.d.ts.map +1 -1
  30. package/dist/math.js +23 -16
  31. package/dist/midi-loader.d.ts +10 -0
  32. package/dist/midi-loader.d.ts.map +1 -0
  33. package/dist/midi-loader.js +247 -0
  34. package/dist/model.d.ts +19 -14
  35. package/dist/model.d.ts.map +1 -1
  36. package/dist/model.js +31 -2
  37. package/dist/param-track.d.ts +48 -0
  38. package/dist/param-track.d.ts.map +1 -0
  39. package/dist/param-track.js +80 -0
  40. package/dist/physics/types.d.ts.map +1 -1
  41. package/dist/physics/types.js +3 -0
  42. package/dist/reflection.d.ts +27 -0
  43. package/dist/reflection.d.ts.map +1 -0
  44. package/dist/reflection.js +93 -0
  45. package/dist/shaders/anchor-table.d.ts +56 -0
  46. package/dist/shaders/anchor-table.d.ts.map +1 -0
  47. package/dist/shaders/anchor-table.js +128 -0
  48. package/dist/shaders/cast-api.d.ts +2 -0
  49. package/dist/shaders/cast-api.d.ts.map +1 -0
  50. package/dist/shaders/cast-api.js +121 -0
  51. package/dist/shaders/cast-layout.d.ts +21 -0
  52. package/dist/shaders/cast-layout.d.ts.map +1 -0
  53. package/dist/shaders/cast-layout.js +20 -0
  54. package/dist/shaders/lights.d.ts +79 -0
  55. package/dist/shaders/lights.d.ts.map +1 -0
  56. package/dist/shaders/lights.js +269 -0
  57. package/dist/shaders/lyrics-api.d.ts +39 -0
  58. package/dist/shaders/lyrics-api.d.ts.map +1 -0
  59. package/dist/shaders/lyrics-api.js +187 -0
  60. package/dist/shaders/materials/common.d.ts +2 -4
  61. package/dist/shaders/materials/common.d.ts.map +1 -1
  62. package/dist/shaders/materials/common.js +87 -35
  63. package/dist/shaders/midi-api.d.ts +10 -0
  64. package/dist/shaders/midi-api.d.ts.map +1 -0
  65. package/dist/shaders/midi-api.js +114 -0
  66. package/dist/shaders/passes/composite.d.ts +28 -22
  67. package/dist/shaders/passes/composite.d.ts.map +1 -1
  68. package/dist/shaders/passes/composite.js +165 -138
  69. package/dist/shaders/passes/cull.d.ts +2 -0
  70. package/dist/shaders/passes/cull.d.ts.map +1 -0
  71. package/dist/shaders/passes/cull.js +138 -0
  72. package/dist/shaders/passes/field-blit.d.ts +26 -0
  73. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  74. package/dist/shaders/passes/field-blit.js +65 -0
  75. package/dist/shaders/passes/grid.d.ts +31 -0
  76. package/dist/shaders/passes/grid.d.ts.map +1 -0
  77. package/dist/shaders/passes/grid.js +169 -0
  78. package/dist/shaders/passes/ground.d.ts +13 -1
  79. package/dist/shaders/passes/ground.d.ts.map +1 -1
  80. package/dist/shaders/passes/ground.js +170 -25
  81. package/dist/shaders/passes/hosted-api.d.ts +57 -0
  82. package/dist/shaders/passes/hosted-api.d.ts.map +1 -0
  83. package/dist/shaders/passes/hosted-api.js +166 -0
  84. package/dist/shaders/passes/id-debug.d.ts +28 -0
  85. package/dist/shaders/passes/id-debug.d.ts.map +1 -0
  86. package/dist/shaders/passes/id-debug.js +74 -0
  87. package/dist/shaders/passes/particles.d.ts +10 -2
  88. package/dist/shaders/passes/particles.d.ts.map +1 -1
  89. package/dist/shaders/passes/particles.js +37 -109
  90. package/dist/shaders/passes/scene-contract.d.ts +128 -0
  91. package/dist/shaders/passes/scene-contract.d.ts.map +1 -0
  92. package/dist/shaders/passes/scene-contract.js +207 -0
  93. package/dist/shaders/passes/sim.d.ts +34 -0
  94. package/dist/shaders/passes/sim.d.ts.map +1 -0
  95. package/dist/shaders/passes/sim.js +169 -0
  96. package/dist/shaders/passes/trails.d.ts +14 -2
  97. package/dist/shaders/passes/trails.d.ts.map +1 -1
  98. package/dist/shaders/passes/trails.js +55 -90
  99. package/dist/shaders/score-api.d.ts +10 -0
  100. package/dist/shaders/score-api.d.ts.map +1 -0
  101. package/dist/shaders/score-api.js +114 -0
  102. package/dist/shadow-cascades.d.ts +45 -0
  103. package/dist/shadow-cascades.d.ts.map +1 -0
  104. package/dist/shadow-cascades.js +70 -0
  105. package/dist/vmd-loader.d.ts +3 -2
  106. package/dist/vmd-loader.d.ts.map +1 -1
  107. package/package.json +1 -1
  108. package/src/animation.ts +41 -0
  109. package/src/camera.ts +31 -8
  110. package/src/engine.ts +4015 -556
  111. package/src/graph/registry.ts +2 -2
  112. package/src/graph/slots.ts +37 -9
  113. package/src/hdr.ts +156 -0
  114. package/src/ibl.ts +115 -0
  115. package/src/ik-solver.ts +1 -1
  116. package/src/index.ts +12 -0
  117. package/src/math.ts +23 -17
  118. package/src/midi-loader.ts +246 -0
  119. package/src/model.ts +34 -4
  120. package/src/param-track.ts +83 -0
  121. package/src/physics/types.ts +4 -1
  122. package/src/reflection.ts +94 -0
  123. package/src/shaders/anchor-table.ts +147 -0
  124. package/src/shaders/cast-api.ts +123 -0
  125. package/src/shaders/cast-layout.ts +20 -0
  126. package/src/shaders/lights.ts +280 -0
  127. package/src/shaders/lyrics-api.ts +202 -0
  128. package/src/shaders/materials/common.ts +89 -35
  129. package/src/shaders/midi-api.ts +116 -0
  130. package/src/shaders/passes/composite.ts +182 -139
  131. package/src/shaders/passes/cull.ts +139 -0
  132. package/src/shaders/passes/grid.ts +178 -0
  133. package/src/shaders/passes/ground.ts +172 -25
  134. package/src/shaders/passes/hosted-api.ts +171 -0
  135. package/src/shaders/passes/id-debug.ts +75 -0
  136. package/src/shaders/passes/particles.ts +54 -112
  137. package/src/shaders/passes/scene-contract.ts +266 -0
  138. package/src/shaders/passes/trails.ts +77 -93
  139. package/src/shadow-cascades.ts +97 -0
  140. 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
- _pad0: f32,
57
- _pad1: f32,
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
- struct LightVP { viewProj: mat4x4f, };
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
- // 4096-map (MUST match Engine.SHADOW_MAP_SIZE), normal-bias 0.08, depth-bias
116
- // 0.001. Unrolled — Safari's Metal backend doesn't unroll nested shadow loops
117
- // reliably. The texel size was stale at 1/2048 after the map grew to 4096:
118
- // PCF taps landed TWO texels apart, quantizing self-shadow edges into
119
- // texel-sized gray/black squares that crawled with the animation.
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
- export const SAMPLE_SHADOW_WGSL = /* wgsl */ `
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 lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
127
- let ndc = lclip.xyz / max(lclip.w, 1e-6);
128
- let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
129
- let cmpZ = ndc.z - 0.001;
130
- let ts = 1.0 / 4096.0;
131
- let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
132
- let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
133
- let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
134
- let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
135
- let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
136
- let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
137
- let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
138
- let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
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
- export const COMMON_VS_WGSL = /* wgsl */ `
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
- export const COMMON_FS_OUT_WGSL = /* wgsl */ `
204
-
205
- struct FSOut {
206
- @location(0) color: vec4f,
207
- @location(1) mask: vec4f,
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 + SAMPLE_SHADOW_WGSL + COMMON_VS_WGSL + COMMON_FS_OUT_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
+ }