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,340 @@
|
|
|
1
|
+
import { RZ_LIGHT_STRUCT_WGSL } from "../lights";
|
|
2
|
+
import { audioApi } from "../audio-api";
|
|
3
|
+
import { lyricsApi } from "../lyrics-api";
|
|
4
|
+
import { anchorAliasWgsl, ribbonSlotWgsl } from "../anchor-table";
|
|
5
|
+
import { midiApi } from "../midi-api";
|
|
6
|
+
import { CAST_API } from "../cast-api";
|
|
7
|
+
import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
|
|
8
|
+
/**
|
|
9
|
+
* Sub-segments drawn between each pair of recorded samples.
|
|
10
|
+
*
|
|
11
|
+
* The path is sampled at a fixed rate on the scene clock, so a fast hand leaves
|
|
12
|
+
* its samples far apart and a strip drawn straight between them is visibly
|
|
13
|
+
* faceted. Four sub-segments on a Catmull-Rom curve through the neighbours costs
|
|
14
|
+
* four times the vertices — which is nothing, they are vertices — and removes
|
|
15
|
+
* both the faceting and most of the jitter, since a spline tangent varies
|
|
16
|
+
* smoothly where a per-segment direction snaps about whenever the hand slows.
|
|
17
|
+
*/
|
|
18
|
+
export const TRAIL_SUBDIVISIONS = 6;
|
|
19
|
+
/** Does the source define the trail contract? Both are required together. */
|
|
20
|
+
export function trailEntryPoints(wgsl) {
|
|
21
|
+
return {
|
|
22
|
+
width: /\bfn\s+trailWidth\s*\(/.test(wgsl),
|
|
23
|
+
shade: /\bfn\s+trailShade\s*\(/.test(wgsl),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* One quad per segment, laid out flat across every anchor and character.
|
|
28
|
+
*
|
|
29
|
+
* The instance index encodes all three — segment, subject, slot — so a scene
|
|
30
|
+
* with three dancers and eight declared bones is still ONE draw call and needs
|
|
31
|
+
* nothing computed on the CPU per frame. Instances past the end of a real trail
|
|
32
|
+
* collapse to a degenerate quad, which costs a vertex shader invocation and no
|
|
33
|
+
* fragments; the alternative is a compacted instance list, which costs a
|
|
34
|
+
* readback every frame to save exactly that.
|
|
35
|
+
*
|
|
36
|
+
* The ribbon faces the camera per SEGMENT rather than as a whole: the side
|
|
37
|
+
* vector is the segment direction crossed with the direction to the eye, so a
|
|
38
|
+
* ribbon that loops back on itself stays visible along its entire length instead
|
|
39
|
+
* of vanishing edge-on where it turns.
|
|
40
|
+
*/
|
|
41
|
+
export function buildTrailShader(src, cast) {
|
|
42
|
+
return (CAST_API +
|
|
43
|
+
trailSlotsApi(src.slots) +
|
|
44
|
+
EFFECT_MATH_API +
|
|
45
|
+
PARTICLE_STRUCT_WGSL +
|
|
46
|
+
clockApi("tu.time", "0.0") +
|
|
47
|
+
viewportApi("cam.targetHeight") +
|
|
48
|
+
// RZ_SLOTS is this module's alone: how many ribbons to DRAW, which is the
|
|
49
|
+
// instance loop's bound. RZ_MAX_ANCHORS in CAST_API is the address space the
|
|
50
|
+
// accessors are bounded by, and the two being one number was the old trail
|
|
51
|
+
// bug — the comment that said so lived here, so it stays here.
|
|
52
|
+
`const RZ_SLOTS: i32 = ${src.slots};
|
|
53
|
+
${ribbonSlotWgsl(src.ribbonSlots)}
|
|
54
|
+
${anchorAliasWgsl(cast.alias)}
|
|
55
|
+
const SUB: i32 = ${TRAIL_SUBDIVISIONS};
|
|
56
|
+
|
|
57
|
+
// World units a sample pair must span to count as a FULL contribution — the
|
|
58
|
+
// original's REF_SEG_PX, converted out of pixels.
|
|
59
|
+
//
|
|
60
|
+
// This is a line integral along a path, so a segment should contribute in
|
|
61
|
+
// proportion to the path it covers. Without it a hand that pauses stacks fifty
|
|
62
|
+
// samples on one point and every one of them contributes fully, which is what
|
|
63
|
+
// turned the ribbon white wherever it slowed. It scales INTENSITY, not width:
|
|
64
|
+
// scaling width pinches the strip shut at exactly the moments a hand decelerates,
|
|
65
|
+
// which is every turn — and a ribbon that closes to nothing at each turn reads as
|
|
66
|
+
// dark breaks in the middle of it.
|
|
67
|
+
const RZ_REF_SPAN: f32 = 0.37;
|
|
68
|
+
|
|
69
|
+
struct CameraU {
|
|
70
|
+
view: mat4x4f,
|
|
71
|
+
proj: mat4x4f,
|
|
72
|
+
camPos: vec3f,
|
|
73
|
+
targetHeight: f32,
|
|
74
|
+
}
|
|
75
|
+
struct TrailU {
|
|
76
|
+
time: f32,
|
|
77
|
+
_pad0: f32,
|
|
78
|
+
_pad1: f32,
|
|
79
|
+
_pad2: f32,
|
|
80
|
+
}
|
|
81
|
+
@group(0) @binding(0) var<storage, read> _rzCast: array<vec4f>;
|
|
82
|
+
@group(0) @binding(1) var<uniform> tu: TrailU;
|
|
83
|
+
@group(0) @binding(2) var<uniform> cam: CameraU;
|
|
84
|
+
// Binding 3 is GONE, and it had to go. Ribbons draw inside the scene pass now,
|
|
85
|
+
// and the depth buffer is that pass's render attachment — binding it for
|
|
86
|
+
// sampling in the same pass is a usage conflict WebGPU rejects outright:
|
|
87
|
+
// "includes writable usage and another usage in the same synchronization
|
|
88
|
+
// scope". The hardware depth test replaced what it was read for.
|
|
89
|
+
|
|
90
|
+
${RZ_LIGHT_STRUCT_WGSL}
|
|
91
|
+
fn rzCameraPos() -> vec3f { return cam.camPos; }
|
|
92
|
+
fn rzCameraRight() -> vec3f { return vec3f(cam.view[0][0], cam.view[1][0], cam.view[2][0]); }
|
|
93
|
+
fn rzCameraUp() -> vec3f { return vec3f(cam.view[0][1], cam.view[1][1], cam.view[2][1]); }
|
|
94
|
+
fn rzCameraForward() -> vec3f { return vec3f(cam.view[0][2], cam.view[1][2], cam.view[2][2]); }
|
|
95
|
+
/** World point → (uv, view distance), same contract as the field mounts' rzProject. */
|
|
96
|
+
fn rzProject(p: vec3f) -> vec3f {
|
|
97
|
+
let clip = cam.proj * cam.view * vec4f(p, 1.0);
|
|
98
|
+
let w = max(clip.w, 1e-4);
|
|
99
|
+
return vec3f(clip.xy / w * 0.5 + 0.5, clip.w);
|
|
100
|
+
}
|
|
101
|
+
fn rzCamPos() -> vec3f { return cam.camPos; }
|
|
102
|
+
fn rzSubjectCount() -> i32 {
|
|
103
|
+
var n = 0;
|
|
104
|
+
for (var i = 0; i < RZ_SUBJECTS; i++) {
|
|
105
|
+
if (_rzCast[i * 3 + 2].w > 0.0) { n = i + 1; }
|
|
106
|
+
}
|
|
107
|
+
return n;
|
|
108
|
+
}
|
|
109
|
+
/** Catmull-Rom through four samples — passes through p1 and p2. */
|
|
110
|
+
fn rzSpline(p0: vec3f, p1: vec3f, p2: vec3f, p3: vec3f, t: f32) -> vec3f {
|
|
111
|
+
let t2 = t * t;
|
|
112
|
+
let t3 = t2 * t;
|
|
113
|
+
return 0.5 * ((2.0 * p1) + (-p0 + p2) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
|
|
114
|
+
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
|
|
115
|
+
}
|
|
116
|
+
/** Its derivative — the ribbon's direction, smooth by construction. */
|
|
117
|
+
fn rzSplineTangent(p0: vec3f, p1: vec3f, p2: vec3f, p3: vec3f, t: f32) -> vec3f {
|
|
118
|
+
let t2 = t * t;
|
|
119
|
+
let d = 0.5 * ((-p0 + p2) + 2.0 * (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t +
|
|
120
|
+
3.0 * (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t2);
|
|
121
|
+
if (length(d) < 1e-6) { return vec3f(0.0, 1.0, 0.0); }
|
|
122
|
+
return normalize(d);
|
|
123
|
+
}
|
|
124
|
+
fn rzTrailAt(subject: i32, slot: i32, i: i32, n: i32) -> vec4f {
|
|
125
|
+
return rzTrail(subject, slot, clamp(i, 0, n - 1));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The direction the ribbon runs AT a sample, not along one segment.
|
|
130
|
+
*
|
|
131
|
+
* Averaging the incoming and outgoing directions gives a mitre joint: both quads
|
|
132
|
+
* meeting at a sample compute the same tangent, so they share an edge exactly.
|
|
133
|
+
* Taking each segment's own direction instead leaves a wedge at every bend —
|
|
134
|
+
* which under additive blending is a bright band, and is what put visible
|
|
135
|
+
* stripes across the ribbon wherever the hand turned.
|
|
136
|
+
*/
|
|
137
|
+
fn rzTangentAt(subject: i32, slot: i32, i: i32, n: i32) -> vec3f {
|
|
138
|
+
let p = rzTrail(subject, slot, i).xyz;
|
|
139
|
+
var d = vec3f(0.0);
|
|
140
|
+
if (i > 0) {
|
|
141
|
+
let q = rzTrail(subject, slot, i - 1).xyz;
|
|
142
|
+
let e = p - q;
|
|
143
|
+
if (length(e) > 1e-7) { d = d + normalize(e); }
|
|
144
|
+
}
|
|
145
|
+
if (i + 1 < n) {
|
|
146
|
+
let q = rzTrail(subject, slot, i + 1).xyz;
|
|
147
|
+
let e = q - p;
|
|
148
|
+
if (length(e) > 1e-7) { d = d + normalize(e); }
|
|
149
|
+
}
|
|
150
|
+
if (length(d) < 1e-6) { return vec3f(0.0, 1.0, 0.0); }
|
|
151
|
+
return normalize(d);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Circumradius of three consecutive samples — the path's local turn radius.
|
|
155
|
+
*
|
|
156
|
+
* The ribbon's half-width must not exceed this. Where it does, the inner edges
|
|
157
|
+
* of adjacent quads cross the centre of the turn and overlap, and under
|
|
158
|
+
* additive blending every overlap doubles into a bright transverse wedge — the
|
|
159
|
+
* bands that survived every brightness fix, because they were geometry, not
|
|
160
|
+
* shading. Collinear or stationary samples return a huge radius: a straight
|
|
161
|
+
* path clamps nothing.
|
|
162
|
+
*/
|
|
163
|
+
fn rzTurnRadius(a: vec3f, b: vec3f, c: vec3f) -> f32 {
|
|
164
|
+
let ab = b - a;
|
|
165
|
+
let bc = c - b;
|
|
166
|
+
let ca = a - c;
|
|
167
|
+
let la = length(ab);
|
|
168
|
+
let lb = length(bc);
|
|
169
|
+
let cr = length(cross(ab, ca));
|
|
170
|
+
if (la < 1e-5 || lb < 1e-5 || cr < 1e-7) { return 1e6; }
|
|
171
|
+
return (la * lb * length(ca)) / (2.0 * cr);
|
|
172
|
+
}
|
|
173
|
+
` +
|
|
174
|
+
audioApi(0, 4) +
|
|
175
|
+
midiApi(0, 5) +
|
|
176
|
+
lyricsApi(0, 6) +
|
|
177
|
+
"\n// ── user effect ──\n" +
|
|
178
|
+
src.wgsl +
|
|
179
|
+
/* wgsl */ `
|
|
180
|
+
struct VSOut {
|
|
181
|
+
@builtin(position) clip: vec4f,
|
|
182
|
+
@location(0) uv: vec2f,
|
|
183
|
+
@location(1) age: f32,
|
|
184
|
+
@location(2) weight: f32,
|
|
185
|
+
// Which declared anchor this quad belongs to — so one effect can dress its
|
|
186
|
+
// hand and foot ribbons differently instead of sharing a single look.
|
|
187
|
+
@location(3) @interpolate(flat) slot: u32,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@vertex
|
|
191
|
+
fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut {
|
|
192
|
+
var out: VSOut;
|
|
193
|
+
let subs = u32((RZ_SAMPLES - 1) * SUB);
|
|
194
|
+
let k = i32(ii % subs);
|
|
195
|
+
let rest = ii / subs;
|
|
196
|
+
let seg = k / SUB;
|
|
197
|
+
let sub = k % SUB;
|
|
198
|
+
let subject = i32(rest % u32(RZ_SUBJECTS));
|
|
199
|
+
// Instance index counts RIBBONS; the cast buffer is addressed by anchor slot.
|
|
200
|
+
// One ribbon per trailed anchor, so these are the same number only when every
|
|
201
|
+
// anchor is trailed — _rzRibbonSlot is what bridges them.
|
|
202
|
+
let ribbon = i32(rest / u32(RZ_SUBJECTS));
|
|
203
|
+
let slot = _rzRibbonSlot(ribbon);
|
|
204
|
+
|
|
205
|
+
let n = rzTrailCount(subject, slot);
|
|
206
|
+
if (ribbon >= RZ_SLOTS || seg + 1 >= n) {
|
|
207
|
+
out.clip = vec4f(0.0, 0.0, -2.0, 1.0);
|
|
208
|
+
out.uv = vec2f(0.0);
|
|
209
|
+
out.age = 0.0;
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let quad = array<vec2f, 6>(
|
|
214
|
+
vec2f(0.0, -1.0), vec2f(1.0, -1.0), vec2f(0.0, 1.0),
|
|
215
|
+
vec2f(0.0, 1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0),
|
|
216
|
+
);
|
|
217
|
+
let c = quad[vi];
|
|
218
|
+
let atEnd = c.x > 0.5;
|
|
219
|
+
|
|
220
|
+
// Position ON THE CURVE, not on the chord. Both quads meeting at a sub-sample
|
|
221
|
+
// evaluate the same t, so they share an edge exactly — no wedge to double-blend
|
|
222
|
+
// into a bright band, which is what the per-segment version left at every bend.
|
|
223
|
+
let t0 = f32(sub) / f32(SUB);
|
|
224
|
+
let t1 = (f32(sub) + 1.0) / f32(SUB);
|
|
225
|
+
let t = select(t0, t1, atEnd);
|
|
226
|
+
let p0 = rzTrailAt(subject, slot, seg - 1, n).xyz;
|
|
227
|
+
let p1 = rzTrail(subject, slot, seg).xyz;
|
|
228
|
+
let p2 = rzTrail(subject, slot, seg + 1).xyz;
|
|
229
|
+
let p3 = rzTrailAt(subject, slot, seg + 2, n).xyz;
|
|
230
|
+
let H = cam.targetHeight;
|
|
231
|
+
let toPx = vec2f((cam.proj[1][1] / cam.proj[0][0]) * H, H) * 0.5;
|
|
232
|
+
// The original's MIN_SEG_PX gate, restored as a HARD gate — not a fade.
|
|
233
|
+
//
|
|
234
|
+
// "Below this the hand is not moving", its comment said, and it meant it: a
|
|
235
|
+
// segment under 0.7px contributed NOTHING. Softening that into a weight that
|
|
236
|
+
// approaches zero let a pause stack dozens of near-stationary quads at full
|
|
237
|
+
// glow width, each individually faint, summing into a wide bright bar exactly
|
|
238
|
+
// where the hand slows — aggregation the width taper never clamps, because
|
|
239
|
+
// width follows AGE, not speed. Where the hand is not moving there is no path,
|
|
240
|
+
// and a path effect should draw nothing there at all.
|
|
241
|
+
let c1 = cam.proj * cam.view * vec4f(p1, 1.0);
|
|
242
|
+
let c2 = cam.proj * cam.view * vec4f(p2, 1.0);
|
|
243
|
+
if (c1.w <= 0.01 || c2.w <= 0.01 ||
|
|
244
|
+
distance(c1.xy / c1.w * toPx, c2.xy / c2.w * toPx) < 0.7 * (H / 1080.0)) {
|
|
245
|
+
out.clip = vec4f(0.0, 0.0, -2.0, 1.0);
|
|
246
|
+
out.uv = vec2f(0.0);
|
|
247
|
+
out.age = 0.0;
|
|
248
|
+
out.weight = 0.0;
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
let pA = rzSpline(p0, p1, p2, p3, t0);
|
|
252
|
+
let pB = rzSpline(p0, p1, p2, p3, t1);
|
|
253
|
+
let p = select(pA, pB, atEnd);
|
|
254
|
+
|
|
255
|
+
// Age and position along the ribbon interpolate across the sub-segment too.
|
|
256
|
+
let ageA = rzTrail(subject, slot, seg).w;
|
|
257
|
+
let ageB = rzTrail(subject, slot, seg + 1).w;
|
|
258
|
+
let age = mix(ageA, ageB, t);
|
|
259
|
+
let u = (f32(seg) + t) / f32(max(1, n - 1));
|
|
260
|
+
|
|
261
|
+
// SCREEN-SPACE extrusion — the fresh take, and the faithful one.
|
|
262
|
+
//
|
|
263
|
+
// Every artefact this ribbon has had came from building it in 3D with a
|
|
264
|
+
// camera-facing side vector: fold-over wedges at tight turns, bowties when the
|
|
265
|
+
// tangent swept past the view axis, and twisted quads whose core line rotated
|
|
266
|
+
// ACROSS the strip and drew as a bright rung. The fullscreen original had none
|
|
267
|
+
// of them, because it never left 2D: it shaded distance to the PROJECTED path,
|
|
268
|
+
// in pixels. So project the spline to pixel space and extrude there — the same
|
|
269
|
+
// geometry the original shaded, at a quad's price. A 2D perpendicular cannot
|
|
270
|
+
// twist, and pixel width is what the original's constants meant all along.
|
|
271
|
+
let clipA = cam.proj * cam.view * vec4f(pA, 1.0);
|
|
272
|
+
let clipB = cam.proj * cam.view * vec4f(pB, 1.0);
|
|
273
|
+
if (clipA.w <= 0.01 || clipB.w <= 0.01) {
|
|
274
|
+
out.clip = vec4f(0.0, 0.0, -2.0, 1.0);
|
|
275
|
+
out.uv = vec2f(0.0);
|
|
276
|
+
out.age = 0.0;
|
|
277
|
+
out.weight = 0.0;
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
// The projected tangent by the quotient rule — exact at the knot, so the two
|
|
281
|
+
// quads meeting there derive the identical perpendicular and share their edge.
|
|
282
|
+
let dA4 = cam.proj * cam.view * vec4f(rzSplineTangent(p0, p1, p2, p3, t0), 0.0);
|
|
283
|
+
let dB4 = cam.proj * cam.view * vec4f(rzSplineTangent(p0, p1, p2, p3, t1), 0.0);
|
|
284
|
+
var tanA = (dA4.xy * clipA.w - clipA.xy * dA4.w) * toPx;
|
|
285
|
+
var tanB = (dB4.xy * clipB.w - clipB.xy * dB4.w) * toPx;
|
|
286
|
+
if (length(tanA) < 1e-5) { tanA = vec2f(1.0, 0.0); }
|
|
287
|
+
if (length(tanB) < 1e-5) { tanB = vec2f(1.0, 0.0); }
|
|
288
|
+
var perpA = normalize(vec2f(-tanA.y, tanA.x));
|
|
289
|
+
var perpB = normalize(vec2f(-tanB.y, tanB.x));
|
|
290
|
+
// A projected cusp flips the perpendicular. The shade is symmetric in |v|, so
|
|
291
|
+
// forcing the quad's ends to agree is invisible — and without it the quad is a
|
|
292
|
+
// 2D bowtie.
|
|
293
|
+
if (dot(perpA, perpB) < 0.0) { perpB = -perpB; }
|
|
294
|
+
let clipP = select(clipA, clipB, atEnd);
|
|
295
|
+
let perp = select(perpA, perpB, atEnd);
|
|
296
|
+
// trailWidth speaks PIXELS, like the original's constants; rzViewportHeight()
|
|
297
|
+
// lets the effect scale them to the frame.
|
|
298
|
+
let wPx = max(0.0, trailWidth(u, age));
|
|
299
|
+
let ndc = clipP.xy / clipP.w + perp * (c.y * wPx) / toPx;
|
|
300
|
+
out.clip = vec4f(ndc * clipP.w, clipP.z, clipP.w);
|
|
301
|
+
out.slot = u32(slot);
|
|
302
|
+
// The line-integral weight: how far the hand travelled, evaluated AT EACH KNOT
|
|
303
|
+
// by central difference and mixed by t so it is continuous across boundaries —
|
|
304
|
+
// per-pair spans made dashes, per-segment averages made terraces.
|
|
305
|
+
let spanA = distance(p2, p0) * 0.5;
|
|
306
|
+
let spanB = distance(p3, p1) * 0.5;
|
|
307
|
+
out.weight = clamp(mix(spanA, spanB, t) / RZ_REF_SPAN, 0.0, 1.0);
|
|
308
|
+
out.uv = vec2f(u, c.y);
|
|
309
|
+
out.age = age;
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
struct TrailFSOut {
|
|
314
|
+
@location(0) color: vec4f,
|
|
315
|
+
// The scene's aux target: (bloom mask, coverage). Writing mask 1 is what
|
|
316
|
+
// makes a ribbon BLOOM — it is light, and the gate is what says so.
|
|
317
|
+
@location(1) aux: vec4f,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
@fragment
|
|
321
|
+
fn fs(in: VSOut) -> TrailFSOut {
|
|
322
|
+
// No manual depth test any more. This drew into its own attachment-less
|
|
323
|
+
// layer and compared position.z against the scene's depth texture by hand,
|
|
324
|
+
// with the direction baked per depth convention. Inside the scene pass the
|
|
325
|
+
// hardware does it, correctly and for free, and the reversed-Z trap that
|
|
326
|
+
// needed its own regression test goes with it.
|
|
327
|
+
let c = trailShade(in.uv.x, in.uv.y, in.age, in.weight, i32(in.slot));
|
|
328
|
+
if (c.a <= 0.0) { discard; }
|
|
329
|
+
var o: TrailFSOut;
|
|
330
|
+
// STRAIGHT colour into an ADDITIVE target, which reverses the old MAX rule
|
|
331
|
+
// deliberately: max existed so parallel strands could not double into bright
|
|
332
|
+
// dashes on a layer composited after tone mapping. In HDR before bloom,
|
|
333
|
+
// overlapping light SHOULD sum — that is what neon does — and the tone
|
|
334
|
+
// mapper is what keeps the sum from clipping.
|
|
335
|
+
o.color = vec4f(c.rgb, c.a);
|
|
336
|
+
o.aux = vec4f(${src.bloom ? "1.0" : "0.0"}, 1.0, 0.0, c.a);
|
|
337
|
+
return o;
|
|
338
|
+
}
|
|
339
|
+
`);
|
|
340
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
type ShadowCascade = {
|
|
2
|
+
/** World units across the ortho box, both axes. */
|
|
3
|
+
span: number;
|
|
4
|
+
/** How far behind the target the light's eye sits, along -sunDir. */
|
|
5
|
+
back: number;
|
|
6
|
+
/** Ortho near/far, in world units from that eye. */
|
|
7
|
+
near: number;
|
|
8
|
+
far: number;
|
|
9
|
+
/** Texels per side of this cascade's map — sets the snap quantum. */
|
|
10
|
+
mapSize: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The list, inner to outer. INVARIANT the sampler and the cull both lean on:
|
|
14
|
+
* each cascade's box must CONTAIN the previous one (same snapped target, wider
|
|
15
|
+
* span, deeper reach), because
|
|
16
|
+
*
|
|
17
|
+
* - the sampler falls from cascade i to i+1 at the box edge, which is only
|
|
18
|
+
* seamless if i+1 covers where i ends, and
|
|
19
|
+
* - the cull tests ONE frustum — the outermost — and the rasterizer clips
|
|
20
|
+
* each cascade to its own box. That is the same argument that made
|
|
21
|
+
* single-volume shadow culling exact: anything rejected was contributing
|
|
22
|
+
* nothing anywhere. Concentric containment is what keeps it true for a
|
|
23
|
+
* LIST. tests/shadow-cascades.test.mjs pins it.
|
|
24
|
+
*/
|
|
25
|
+
export declare const SHADOW_CASCADES: readonly ShadowCascade[];
|
|
26
|
+
/**
|
|
27
|
+
* One cascade's view-projection, following the camera target.
|
|
28
|
+
*
|
|
29
|
+
* The target is snapped to this cascade's OWN texel quantum in the light's
|
|
30
|
+
* right/up plane, so a moving volume doesn't shimmer its shadow edges while
|
|
31
|
+
* running — each cascade snaps to its own grid, coarser maps snapping coarser.
|
|
32
|
+
*
|
|
33
|
+
* Writes the 16 floats into `out` at `offset` and returns `out`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildShadowVP(target: {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
z: number;
|
|
39
|
+
}, sunDirection: {
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
z: number;
|
|
43
|
+
}, cascade: ShadowCascade, out: Float32Array, offset: number): Float32Array;
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=shadow-cascades.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadow-cascades.d.ts","sourceRoot":"","sources":["../src/shadow-cascades.ts"],"names":[],"mappings":"AAeA,KAAK,aAAa,GAAG;IACnB,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAA;IACZ,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAA;IACZ,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,aAAa,EAWnD,CAAA;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAC3C,YAAY,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACjD,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,YAAY,EACjB,MAAM,EAAE,MAAM,GACb,YAAY,CA4Bd"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The shadow volumes, as data — a list the engine iterates rather than one
|
|
2
|
+
// hardcoded box, so a second cascade is a list entry and not a rewrite.
|
|
3
|
+
//
|
|
4
|
+
// Pure math, its own module for the same reason param-track.ts is: the engine
|
|
5
|
+
// class needs a GPU to construct, and the one thing that ever goes WRONG with a
|
|
6
|
+
// shadow volume is arithmetic — a snap that stops snapping, an eye that lands
|
|
7
|
+
// inside the near plane. Headless tests can hold this half to golden values.
|
|
8
|
+
//
|
|
9
|
+
// The arithmetic is the shipped single-volume code, operation for operation:
|
|
10
|
+
// float addition is not associative, so "the same formula, reordered" is not
|
|
11
|
+
// the same matrix, and cascade 0 must be BIT-IDENTICAL to the volume every
|
|
12
|
+
// published scene was lit by.
|
|
13
|
+
import { Mat4, Vec3 } from "./math";
|
|
14
|
+
/**
|
|
15
|
+
* The list, inner to outer. INVARIANT the sampler and the cull both lean on:
|
|
16
|
+
* each cascade's box must CONTAIN the previous one (same snapped target, wider
|
|
17
|
+
* span, deeper reach), because
|
|
18
|
+
*
|
|
19
|
+
* - the sampler falls from cascade i to i+1 at the box edge, which is only
|
|
20
|
+
* seamless if i+1 covers where i ends, and
|
|
21
|
+
* - the cull tests ONE frustum — the outermost — and the rasterizer clips
|
|
22
|
+
* each cascade to its own box. That is the same argument that made
|
|
23
|
+
* single-volume shadow culling exact: anything rejected was contributing
|
|
24
|
+
* nothing anywhere. Concentric containment is what keeps it true for a
|
|
25
|
+
* LIST. tests/shadow-cascades.test.mjs pins it.
|
|
26
|
+
*/
|
|
27
|
+
export const SHADOW_CASCADES = [
|
|
28
|
+
// The shipped volume: 64 units at 4096² ≈ 64 texels/unit — crisp contact
|
|
29
|
+
// shadows on the ground catcher (2048 read visibly blurry).
|
|
30
|
+
{ span: 64, back: 72, near: 1, far: 140, mapSize: 4096 },
|
|
31
|
+
// The stage volume: 4× the span on each side, so a set piece 100 units out
|
|
32
|
+
// still throws and receives shade instead of popping lit at the near box's
|
|
33
|
+
// edge. 2048² over 256 units ≈ 8 texels/unit — soft, and read at distances
|
|
34
|
+
// where soft is what a shadow looks like anyway. 16 MB where the near map
|
|
35
|
+
// is 64. Depth reach scales with the span (same eye direction, deeper box),
|
|
36
|
+
// keeping the containment invariant checkable from the specs alone.
|
|
37
|
+
{ span: 256, back: 288, near: 1, far: 560, mapSize: 2048 },
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* One cascade's view-projection, following the camera target.
|
|
41
|
+
*
|
|
42
|
+
* The target is snapped to this cascade's OWN texel quantum in the light's
|
|
43
|
+
* right/up plane, so a moving volume doesn't shimmer its shadow edges while
|
|
44
|
+
* running — each cascade snaps to its own grid, coarser maps snapping coarser.
|
|
45
|
+
*
|
|
46
|
+
* Writes the 16 floats into `out` at `offset` and returns `out`.
|
|
47
|
+
*/
|
|
48
|
+
export function buildShadowVP(target, sunDirection, cascade, out, offset) {
|
|
49
|
+
const dir = new Vec3(sunDirection.x, sunDirection.y, sunDirection.z);
|
|
50
|
+
dir.normalize();
|
|
51
|
+
const up = Math.abs(dir.y) > 0.99 ? new Vec3(0, 0, -1) : new Vec3(0, 1, 0);
|
|
52
|
+
const t = new Vec3(target.x, target.y, target.z);
|
|
53
|
+
const right = Vec3.crossInto(up, dir, new Vec3(0, 0, 0)).normalize();
|
|
54
|
+
const upv = Vec3.crossInto(dir, right, new Vec3(0, 0, 0));
|
|
55
|
+
const texel = cascade.span / cascade.mapSize;
|
|
56
|
+
const tr = Math.round(t.dot(right) / texel) * texel;
|
|
57
|
+
const tu = Math.round(t.dot(upv) / texel) * texel;
|
|
58
|
+
const td = t.dot(dir);
|
|
59
|
+
const snapped = new Vec3(right.x * tr + upv.x * tu + dir.x * td, right.y * tr + upv.y * tu + dir.y * td, right.z * tr + upv.z * tu + dir.z * td);
|
|
60
|
+
const eye = new Vec3(snapped.x - dir.x * cascade.back, snapped.y - dir.y * cascade.back, snapped.z - dir.z * cascade.back);
|
|
61
|
+
const view = Mat4.lookAt(eye, snapped, up);
|
|
62
|
+
const half = cascade.span / 2;
|
|
63
|
+
// The shadow map keeps the NON-reversed convention (orthographicLh maps z to
|
|
64
|
+
// [0,1] front-to-back) — reversing it buys nothing for an ortho box and the
|
|
65
|
+
// +2 pipeline depth bias is signed against this direction.
|
|
66
|
+
const proj = Mat4.orthographicLh(-half, half, -half, half, cascade.near, cascade.far);
|
|
67
|
+
const vp = proj.multiply(view);
|
|
68
|
+
out.set(vp.values, offset);
|
|
69
|
+
return out;
|
|
70
|
+
}
|
package/dist/vmd-loader.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Quat, Vec3 } from "./math";
|
|
2
|
-
|
|
2
|
+
interface BoneFrame {
|
|
3
3
|
boneName: string;
|
|
4
4
|
frame: number;
|
|
5
5
|
rotation: Quat;
|
|
6
6
|
translation: Vec3;
|
|
7
7
|
interpolation: Uint8Array;
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
interface MorphFrame {
|
|
10
10
|
morphName: string;
|
|
11
11
|
frame: number;
|
|
12
12
|
weight: number;
|
|
@@ -70,4 +70,5 @@ export declare class VMDLoader {
|
|
|
70
70
|
private getString;
|
|
71
71
|
private skip;
|
|
72
72
|
}
|
|
73
|
+
export {};
|
|
73
74
|
//# sourceMappingURL=vmd-loader.d.ts.map
|
package/dist/vmd-loader.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vmd-loader.d.ts","sourceRoot":"","sources":["../src/vmd-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEnC,
|
|
1
|
+
{"version":3,"file":"vmd-loader.d.ts","sourceRoot":"","sources":["../src/vmd-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEnC,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,IAAI,CAAA;IACd,WAAW,EAAE,IAAI,CAAA;IACjB,aAAa,EAAE,UAAU,CAAA;CAC1B;AAED,UAAU,UAAU;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,SAAS,EAAE,CAAA;IACvB,WAAW,EAAE,UAAU,EAAE,CAAA;CAC1B;AAED;;qFAEqF;AACrF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,IAAI,CAAA;IACZ,QAAQ,EAAE,IAAI,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,aAAa,EAAE,UAAU,CAAA;CAC1B;AAED,0EAA0E;AAC1E,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;CACjD;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,OAAO,CAAa;IAE5B,OAAO;WAWM,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAKtD,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,EAAE;IAKzD;mGAC+F;WAClF,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAK/D,MAAM,CAAC,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,cAAc,EAAE;IAOlE,OAAO,CAAC,WAAW;IA4BnB,MAAM,CAAC,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,EAAE;IAIvD;;;;;;;OAOG;IACH,OAAO,CAAC,OAAO;IA8Bf,OAAO,CAAC,KAAK;IAgGb,OAAO,CAAC,aAAa;IAuDrB,OAAO,CAAC,cAAc;IAqCtB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,UAAU;IASlB,gFAAgF;IAChF,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,IAAI;CAMb"}
|