reze-engine 0.43.0 → 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 -438
- 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 +2 -13
- package/dist/model.d.ts.map +1 -1
- 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/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 +3 -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/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,4 +1,10 @@
|
|
|
1
|
+
import { RZ_LIGHT_STRUCT_WGSL } from "../lights"
|
|
1
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"
|
|
2
8
|
// Ribbons along a bone's recorded path, drawn as geometry.
|
|
3
9
|
//
|
|
4
10
|
// This is the effect the whole geometry path was built for. As a fullscreen
|
|
@@ -16,11 +22,14 @@ import { audioApi } from "../audio-api"
|
|
|
16
22
|
// colour. Everything else — which bones, which characters, where the samples
|
|
17
23
|
// are, how to face the camera — is the engine's.
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
type TrailSource = {
|
|
20
26
|
/** The author's WGSL verbatim. */
|
|
21
27
|
wgsl: string
|
|
22
|
-
/**
|
|
28
|
+
/** How many ribbons to draw — one per trailed anchor. */
|
|
23
29
|
slots: number
|
|
30
|
+
/** For each ribbon, the LOCAL anchor slot it belongs to. Identity when every
|
|
31
|
+
* anchor is trailed; otherwise it skips the untrailed ones. */
|
|
32
|
+
ribbonSlots: number[]
|
|
24
33
|
/** Additive, like most glowing ribbons, or straight alpha. */
|
|
25
34
|
blend: "alpha" | "additive"
|
|
26
35
|
bloom: boolean
|
|
@@ -61,62 +70,39 @@ export function trailEntryPoints(wgsl: string): { width: boolean; shade: boolean
|
|
|
61
70
|
* ribbon that loops back on itself stays visible along its entire length instead
|
|
62
71
|
* of vanishing edge-on where it turns.
|
|
63
72
|
*/
|
|
64
|
-
export function buildTrailShader(
|
|
73
|
+
export function buildTrailShader(
|
|
74
|
+
src: TrailSource,
|
|
75
|
+
cast: {
|
|
76
|
+
subjects: number
|
|
77
|
+
samples: number
|
|
78
|
+
base: number
|
|
79
|
+
trailBase: number
|
|
80
|
+
slots: number
|
|
81
|
+
alias: number[]
|
|
82
|
+
/** Depth convention of the scene buffer this layer tests against. The
|
|
83
|
+
* occlusion compare below is MANUAL (the layer has no depth attachment),
|
|
84
|
+
* so it does not flip with the pipelines' depthCompare — it has to be
|
|
85
|
+
* emitted the right way round at build time. On a reversed-Z device,
|
|
86
|
+
* larger z is CLOSER; the unflipped test drew ribbons only when occluded. */
|
|
87
|
+
reversedZ: boolean
|
|
88
|
+
},
|
|
89
|
+
): string {
|
|
65
90
|
return (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
91
|
+
CAST_API +
|
|
92
|
+
trailSlotsApi(src.slots) +
|
|
93
|
+
EFFECT_MATH_API +
|
|
94
|
+
PARTICLE_STRUCT_WGSL +
|
|
95
|
+
clockApi("tu.time", "0.0") +
|
|
96
|
+
viewportApi("cam.targetHeight") +
|
|
97
|
+
// RZ_SLOTS is this module's alone: how many ribbons to DRAW, which is the
|
|
98
|
+
// instance loop's bound. RZ_MAX_ANCHORS in CAST_API is the address space the
|
|
99
|
+
// accessors are bounded by, and the two being one number was the old trail
|
|
100
|
+
// bug — the comment that said so lived here, so it stays here.
|
|
101
|
+
`const RZ_SLOTS: i32 = ${src.slots};
|
|
102
|
+
${ribbonSlotWgsl(src.ribbonSlots)}
|
|
103
|
+
${anchorAliasWgsl(cast.alias)}
|
|
70
104
|
const SUB: i32 = ${TRAIL_SUBDIVISIONS};
|
|
71
105
|
|
|
72
|
-
// The same struct and helpers the particle modules define. The whole effect
|
|
73
|
-
// file is spliced into EVERY module its mounts compile into, so an effect that
|
|
74
|
-
// declares both trails and particles carries its particle functions through
|
|
75
|
-
// this module as dead code — dead code that still has to type-check. Keeping
|
|
76
|
-
// the two preludes' public surface identical is what makes one file, two
|
|
77
|
-
// mounts work.
|
|
78
|
-
struct Particle {
|
|
79
|
-
pos: vec3f,
|
|
80
|
-
age: f32,
|
|
81
|
-
vel: vec3f,
|
|
82
|
-
life: f32,
|
|
83
|
-
size: f32,
|
|
84
|
-
rot: f32,
|
|
85
|
-
seed: f32,
|
|
86
|
-
stretch: f32,
|
|
87
|
-
}
|
|
88
|
-
fn rzDt() -> f32 { return 0.0; }
|
|
89
|
-
fn rzHash21(p: vec2f) -> f32 {
|
|
90
|
-
var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
|
|
91
|
-
p3 = p3 + dot(p3, p3.yzx + 33.33);
|
|
92
|
-
return fract((p3.x + p3.y) * p3.z);
|
|
93
|
-
}
|
|
94
|
-
fn rzHash31(p: vec3f) -> f32 {
|
|
95
|
-
var p3 = fract(p * 0.1031);
|
|
96
|
-
p3 = p3 + dot(p3, p3.zyx + 31.32);
|
|
97
|
-
return fract((p3.x + p3.y) * p3.z);
|
|
98
|
-
}
|
|
99
|
-
fn rzHash13(x: f32) -> vec3f {
|
|
100
|
-
return vec3f(rzHash11(x), rzHash11(x + 17.13), rzHash11(x + 41.71));
|
|
101
|
-
}
|
|
102
|
-
fn rzValueNoise(p: vec3f) -> f32 {
|
|
103
|
-
let i = floor(p);
|
|
104
|
-
let f = fract(p);
|
|
105
|
-
let u = f * f * (3.0 - 2.0 * f);
|
|
106
|
-
let n000 = rzHash31(i);
|
|
107
|
-
let n100 = rzHash31(i + vec3f(1.0, 0.0, 0.0));
|
|
108
|
-
let n010 = rzHash31(i + vec3f(0.0, 1.0, 0.0));
|
|
109
|
-
let n110 = rzHash31(i + vec3f(1.0, 1.0, 0.0));
|
|
110
|
-
let n001 = rzHash31(i + vec3f(0.0, 0.0, 1.0));
|
|
111
|
-
let n101 = rzHash31(i + vec3f(1.0, 0.0, 1.0));
|
|
112
|
-
let n011 = rzHash31(i + vec3f(0.0, 1.0, 1.0));
|
|
113
|
-
let n111 = rzHash31(i + vec3f(1.0, 1.0, 1.0));
|
|
114
|
-
let x00 = mix(n000, n100, u.x);
|
|
115
|
-
let x10 = mix(n010, n110, u.x);
|
|
116
|
-
let x01 = mix(n001, n101, u.x);
|
|
117
|
-
let x11 = mix(n011, n111, u.x);
|
|
118
|
-
return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);
|
|
119
|
-
}
|
|
120
106
|
// World units a sample pair must span to count as a FULL contribution — the
|
|
121
107
|
// original's REF_SEG_PX, converted out of pixels.
|
|
122
108
|
//
|
|
@@ -144,9 +130,13 @@ struct TrailU {
|
|
|
144
130
|
@group(0) @binding(0) var<storage, read> _rzCast: array<vec4f>;
|
|
145
131
|
@group(0) @binding(1) var<uniform> tu: TrailU;
|
|
146
132
|
@group(0) @binding(2) var<uniform> cam: CameraU;
|
|
147
|
-
|
|
133
|
+
// Binding 3 is GONE, and it had to go. Ribbons draw inside the scene pass now,
|
|
134
|
+
// and the depth buffer is that pass's render attachment — binding it for
|
|
135
|
+
// sampling in the same pass is a usage conflict WebGPU rejects outright:
|
|
136
|
+
// "includes writable usage and another usage in the same synchronization
|
|
137
|
+
// scope". The hardware depth test replaced what it was read for.
|
|
148
138
|
|
|
149
|
-
|
|
139
|
+
${RZ_LIGHT_STRUCT_WGSL}
|
|
150
140
|
fn rzCameraPos() -> vec3f { return cam.camPos; }
|
|
151
141
|
fn rzCameraRight() -> vec3f { return vec3f(cam.view[0][0], cam.view[1][0], cam.view[2][0]); }
|
|
152
142
|
fn rzCameraUp() -> vec3f { return vec3f(cam.view[0][1], cam.view[1][1], cam.view[2][1]); }
|
|
@@ -157,7 +147,6 @@ fn rzProject(p: vec3f) -> vec3f {
|
|
|
157
147
|
let w = max(clip.w, 1e-4);
|
|
158
148
|
return vec3f(clip.xy / w * 0.5 + 0.5, clip.w);
|
|
159
149
|
}
|
|
160
|
-
fn rzViewportHeight() -> f32 { return cam.targetHeight; }
|
|
161
150
|
fn rzCamPos() -> vec3f { return cam.camPos; }
|
|
162
151
|
fn rzSubjectCount() -> i32 {
|
|
163
152
|
var n = 0;
|
|
@@ -166,16 +155,6 @@ fn rzSubjectCount() -> i32 {
|
|
|
166
155
|
}
|
|
167
156
|
return n;
|
|
168
157
|
}
|
|
169
|
-
fn rzTrailCount(subject: i32, slot: i32) -> i32 {
|
|
170
|
-
if (subject < 0 || subject >= RZ_SUBJECTS || slot < 0 || slot >= RZ_SLOTS) { return 0; }
|
|
171
|
-
return i32(_rzCast[${cast.base} + (slot * RZ_SUBJECTS + subject) * 3 + 2].w);
|
|
172
|
-
}
|
|
173
|
-
/** Sample i of a path: xyz where it was, w how many seconds ago. i = 0 is now. */
|
|
174
|
-
fn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {
|
|
175
|
-
let n = rzTrailCount(subject, slot);
|
|
176
|
-
if (i < 0 || i >= n) { return vec4f(0.0); }
|
|
177
|
-
return _rzCast[${cast.trailBase} + (slot * RZ_SUBJECTS + subject) * RZ_SAMPLES + i];
|
|
178
|
-
}
|
|
179
158
|
/** Catmull-Rom through four samples — passes through p1 and p2. */
|
|
180
159
|
fn rzSpline(p0: vec3f, p1: vec3f, p2: vec3f, p3: vec3f, t: f32) -> vec3f {
|
|
181
160
|
let t2 = t * t;
|
|
@@ -240,18 +219,10 @@ fn rzTurnRadius(a: vec3f, b: vec3f, c: vec3f) -> f32 {
|
|
|
240
219
|
if (la < 1e-5 || lb < 1e-5 || cr < 1e-7) { return 1e6; }
|
|
241
220
|
return (la * lb * length(ca)) / (2.0 * cr);
|
|
242
221
|
}
|
|
243
|
-
fn rzFalloff(d: f32, r: f32) -> f32 {
|
|
244
|
-
let x = clamp(d / max(r, 1e-6), 0.0, 1.0);
|
|
245
|
-
let f = 1.0 - x;
|
|
246
|
-
return f * f * f;
|
|
247
|
-
}
|
|
248
|
-
fn rzHash11(x: f32) -> f32 {
|
|
249
|
-
var p = fract(x * 0.1031);
|
|
250
|
-
p = p * (p + 33.33);
|
|
251
|
-
return fract(p * (p + p));
|
|
252
|
-
}
|
|
253
222
|
` +
|
|
254
223
|
audioApi(0, 4) +
|
|
224
|
+
midiApi(0, 5) +
|
|
225
|
+
lyricsApi(0, 6) +
|
|
255
226
|
"\n// ── user effect ──\n" +
|
|
256
227
|
src.wgsl +
|
|
257
228
|
/* wgsl */ `
|
|
@@ -274,10 +245,14 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
|
|
|
274
245
|
let seg = k / SUB;
|
|
275
246
|
let sub = k % SUB;
|
|
276
247
|
let subject = i32(rest % u32(RZ_SUBJECTS));
|
|
277
|
-
|
|
248
|
+
// Instance index counts RIBBONS; the cast buffer is addressed by anchor slot.
|
|
249
|
+
// One ribbon per trailed anchor, so these are the same number only when every
|
|
250
|
+
// anchor is trailed — _rzRibbonSlot is what bridges them.
|
|
251
|
+
let ribbon = i32(rest / u32(RZ_SUBJECTS));
|
|
252
|
+
let slot = _rzRibbonSlot(ribbon);
|
|
278
253
|
|
|
279
254
|
let n = rzTrailCount(subject, slot);
|
|
280
|
-
if (
|
|
255
|
+
if (ribbon >= RZ_SLOTS || seg + 1 >= n) {
|
|
281
256
|
out.clip = vec4f(0.0, 0.0, -2.0, 1.0);
|
|
282
257
|
out.uv = vec2f(0.0);
|
|
283
258
|
out.age = 0.0;
|
|
@@ -384,22 +359,31 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
|
|
|
384
359
|
return out;
|
|
385
360
|
}
|
|
386
361
|
|
|
362
|
+
struct TrailFSOut {
|
|
363
|
+
@location(0) color: vec4f,
|
|
364
|
+
// The scene's aux target: (bloom mask, coverage). Writing mask 1 is what
|
|
365
|
+
// makes a ribbon BLOOM — it is light, and the gate is what says so.
|
|
366
|
+
@location(1) aux: vec4f,
|
|
367
|
+
}
|
|
368
|
+
|
|
387
369
|
@fragment
|
|
388
|
-
fn fs(in: VSOut) ->
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
370
|
+
fn fs(in: VSOut) -> TrailFSOut {
|
|
371
|
+
// No manual depth test any more. This drew into its own attachment-less
|
|
372
|
+
// layer and compared position.z against the scene's depth texture by hand,
|
|
373
|
+
// with the direction baked per depth convention. Inside the scene pass the
|
|
374
|
+
// hardware does it, correctly and for free, and the reversed-Z trap that
|
|
375
|
+
// needed its own regression test goes with it.
|
|
394
376
|
let c = trailShade(in.uv.x, in.uv.y, in.age, in.weight, i32(in.slot));
|
|
395
377
|
if (c.a <= 0.0) { discard; }
|
|
396
|
-
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
|
|
378
|
+
var o: TrailFSOut;
|
|
379
|
+
// STRAIGHT colour into an ADDITIVE target, which reverses the old MAX rule
|
|
380
|
+
// deliberately: max existed so parallel strands could not double into bright
|
|
381
|
+
// dashes on a layer composited after tone mapping. In HDR before bloom,
|
|
382
|
+
// overlapping light SHOULD sum — that is what neon does — and the tone
|
|
383
|
+
// mapper is what keeps the sum from clipping.
|
|
384
|
+
o.color = vec4f(c.rgb, c.a);
|
|
385
|
+
o.aux = vec4f(${src.bloom ? "1.0" : "0.0"}, 1.0, 0.0, c.a);
|
|
386
|
+
return o;
|
|
403
387
|
}
|
|
404
388
|
`
|
|
405
389
|
)
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
|
|
14
|
+
import { Mat4, Vec3 } from "./math"
|
|
15
|
+
|
|
16
|
+
type ShadowCascade = {
|
|
17
|
+
/** World units across the ortho box, both axes. */
|
|
18
|
+
span: number
|
|
19
|
+
/** How far behind the target the light's eye sits, along -sunDir. */
|
|
20
|
+
back: number
|
|
21
|
+
/** Ortho near/far, in world units from that eye. */
|
|
22
|
+
near: number
|
|
23
|
+
far: number
|
|
24
|
+
/** Texels per side of this cascade's map — sets the snap quantum. */
|
|
25
|
+
mapSize: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The list, inner to outer. INVARIANT the sampler and the cull both lean on:
|
|
30
|
+
* each cascade's box must CONTAIN the previous one (same snapped target, wider
|
|
31
|
+
* span, deeper reach), because
|
|
32
|
+
*
|
|
33
|
+
* - the sampler falls from cascade i to i+1 at the box edge, which is only
|
|
34
|
+
* seamless if i+1 covers where i ends, and
|
|
35
|
+
* - the cull tests ONE frustum — the outermost — and the rasterizer clips
|
|
36
|
+
* each cascade to its own box. That is the same argument that made
|
|
37
|
+
* single-volume shadow culling exact: anything rejected was contributing
|
|
38
|
+
* nothing anywhere. Concentric containment is what keeps it true for a
|
|
39
|
+
* LIST. tests/shadow-cascades.test.mjs pins it.
|
|
40
|
+
*/
|
|
41
|
+
export const SHADOW_CASCADES: readonly ShadowCascade[] = [
|
|
42
|
+
// The shipped volume: 64 units at 4096² ≈ 64 texels/unit — crisp contact
|
|
43
|
+
// shadows on the ground catcher (2048 read visibly blurry).
|
|
44
|
+
{ span: 64, back: 72, near: 1, far: 140, mapSize: 4096 },
|
|
45
|
+
// The stage volume: 4× the span on each side, so a set piece 100 units out
|
|
46
|
+
// still throws and receives shade instead of popping lit at the near box's
|
|
47
|
+
// edge. 2048² over 256 units ≈ 8 texels/unit — soft, and read at distances
|
|
48
|
+
// where soft is what a shadow looks like anyway. 16 MB where the near map
|
|
49
|
+
// is 64. Depth reach scales with the span (same eye direction, deeper box),
|
|
50
|
+
// keeping the containment invariant checkable from the specs alone.
|
|
51
|
+
{ span: 256, back: 288, near: 1, far: 560, mapSize: 2048 },
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* One cascade's view-projection, following the camera target.
|
|
56
|
+
*
|
|
57
|
+
* The target is snapped to this cascade's OWN texel quantum in the light's
|
|
58
|
+
* right/up plane, so a moving volume doesn't shimmer its shadow edges while
|
|
59
|
+
* running — each cascade snaps to its own grid, coarser maps snapping coarser.
|
|
60
|
+
*
|
|
61
|
+
* Writes the 16 floats into `out` at `offset` and returns `out`.
|
|
62
|
+
*/
|
|
63
|
+
export function buildShadowVP(
|
|
64
|
+
target: { x: number; y: number; z: number },
|
|
65
|
+
sunDirection: { x: number; y: number; z: number },
|
|
66
|
+
cascade: ShadowCascade,
|
|
67
|
+
out: Float32Array,
|
|
68
|
+
offset: number,
|
|
69
|
+
): Float32Array {
|
|
70
|
+
const dir = new Vec3(sunDirection.x, sunDirection.y, sunDirection.z)
|
|
71
|
+
dir.normalize()
|
|
72
|
+
const up = Math.abs(dir.y) > 0.99 ? new Vec3(0, 0, -1) : new Vec3(0, 1, 0)
|
|
73
|
+
|
|
74
|
+
const t = new Vec3(target.x, target.y, target.z)
|
|
75
|
+
const right = Vec3.crossInto(up, dir, new Vec3(0, 0, 0)).normalize()
|
|
76
|
+
const upv = Vec3.crossInto(dir, right, new Vec3(0, 0, 0))
|
|
77
|
+
const texel = cascade.span / cascade.mapSize
|
|
78
|
+
const tr = Math.round(t.dot(right) / texel) * texel
|
|
79
|
+
const tu = Math.round(t.dot(upv) / texel) * texel
|
|
80
|
+
const td = t.dot(dir)
|
|
81
|
+
const snapped = new Vec3(
|
|
82
|
+
right.x * tr + upv.x * tu + dir.x * td,
|
|
83
|
+
right.y * tr + upv.y * tu + dir.y * td,
|
|
84
|
+
right.z * tr + upv.z * tu + dir.z * td,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const eye = new Vec3(snapped.x - dir.x * cascade.back, snapped.y - dir.y * cascade.back, snapped.z - dir.z * cascade.back)
|
|
88
|
+
const view = Mat4.lookAt(eye, snapped, up)
|
|
89
|
+
const half = cascade.span / 2
|
|
90
|
+
// The shadow map keeps the NON-reversed convention (orthographicLh maps z to
|
|
91
|
+
// [0,1] front-to-back) — reversing it buys nothing for an ortho box and the
|
|
92
|
+
// +2 pipeline depth bias is signed against this direction.
|
|
93
|
+
const proj = Mat4.orthographicLh(-half, half, -half, half, cascade.near, cascade.far)
|
|
94
|
+
const vp = proj.multiply(view)
|
|
95
|
+
out.set(vp.values, offset)
|
|
96
|
+
return out
|
|
97
|
+
}
|
package/src/vmd-loader.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Quat, Vec3 } from "./math"
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interface BoneFrame {
|
|
4
4
|
boneName: string
|
|
5
5
|
frame: number
|
|
6
6
|
rotation: Quat
|
|
@@ -8,7 +8,7 @@ export interface BoneFrame {
|
|
|
8
8
|
interpolation: Uint8Array // 64 bytes of interpolation parameters
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
interface MorphFrame {
|
|
12
12
|
morphName: string
|
|
13
13
|
frame: number
|
|
14
14
|
weight: number // 0.0 to 1.0
|