reze-engine 0.54.15 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.d.ts +210 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1140 -13
- package/dist/graph/presets/pool_floor.d.ts +3 -0
- package/dist/graph/presets/pool_floor.d.ts.map +1 -0
- package/dist/graph/presets/pool_floor.js +42 -0
- package/dist/graph/presets/water.d.ts +3 -0
- package/dist/graph/presets/water.d.ts.map +1 -0
- package/dist/graph/presets/water.js +31 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +10 -0
- package/dist/overlay.d.ts +200 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +806 -0
- package/dist/physics/autofit.d.ts +147 -0
- package/dist/physics/autofit.d.ts.map +1 -0
- package/dist/physics/autofit.js +501 -0
- package/dist/pmx-document.d.ts +218 -0
- package/dist/pmx-document.d.ts.map +1 -0
- package/dist/pmx-document.js +685 -0
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +2 -0
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +2 -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.map +1 -1
- package/dist/shaders/passes/grid.js +2 -0
- package/dist/shaders/passes/ground-noise.d.ts +7 -0
- package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
- package/dist/shaders/passes/ground-noise.js +88 -0
- package/dist/shaders/passes/overlay.d.ts +3 -0
- package/dist/shaders/passes/overlay.d.ts.map +1 -0
- package/dist/shaders/passes/overlay.js +164 -0
- package/dist/shaders/passes/sim.d.ts +34 -0
- package/dist/shaders/passes/sim.d.ts.map +1 -0
- package/dist/shaders/passes/sim.js +169 -0
- package/dist/shaders/passes/wireframe.d.ts +2 -0
- package/dist/shaders/passes/wireframe.d.ts.map +1 -0
- package/dist/shaders/passes/wireframe.js +107 -0
- package/dist/shaders/scene-tap.d.ts +60 -0
- package/dist/shaders/scene-tap.d.ts.map +1 -0
- package/dist/shaders/scene-tap.js +234 -0
- package/dist/shaders/score-api.d.ts +10 -0
- package/dist/shaders/score-api.d.ts.map +1 -0
- package/dist/shaders/score-api.js +114 -0
- package/package.json +1 -1
- package/src/engine.ts +1221 -15
- package/src/index.ts +50 -0
- package/src/model.ts +9 -0
- package/src/overlay.ts +1000 -0
- package/src/pmx-document.ts +882 -0
- package/src/shaders/lights.ts +2 -0
- package/src/shaders/passes/composite.ts +2 -0
- package/src/shaders/passes/grid.ts +2 -0
- package/src/shaders/passes/overlay.ts +166 -0
- package/src/shaders/passes/wireframe.ts +108 -0
- package/src/shaders/scene-tap.ts +236 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// The mesh itself, as lines, in the overlay layer.
|
|
2
|
+
//
|
|
3
|
+
// It skins here rather than reading the loader's CPU positions, and that is most
|
|
4
|
+
// of why the pass exists. What is on screen is skinned and morphed on the GPU;
|
|
5
|
+
// the CPU-side array is BIND POSE. Draw from that and the wireframe sits
|
|
6
|
+
// perfectly on a T-posed model and slides off every animated one — which is
|
|
7
|
+
// exactly the state a user is in while looking at weights.
|
|
8
|
+
//
|
|
9
|
+
// Each edge is expanded into a screen-space QUAD rather than drawn with line
|
|
10
|
+
// topology. WebGPU pins a line to one device pixel, and one device pixel on a 2x
|
|
11
|
+
// display is half a CSS pixel, which 4x MSAA then spreads across two of them:
|
|
12
|
+
// the result is a grey suggestion of a wireframe rather than a wireframe. So the
|
|
13
|
+
// draw is six vertices per edge, instanced, and the mesh is read through STORAGE
|
|
14
|
+
// buffers — there is no per-vertex stream, because a quad's two corners come
|
|
15
|
+
// from two different vertices of the model.
|
|
16
|
+
//
|
|
17
|
+
// The depth prepass shares this module's `vs` through its own entry point: it
|
|
18
|
+
// draws the solid triangles with vertex buffers, writing depth only, so the
|
|
19
|
+
// wireframe can be occluded by the body it belongs to.
|
|
20
|
+
export const WIREFRAME_SHADER_WGSL = /* wgsl */ `
|
|
21
|
+
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _pad: f32 };
|
|
22
|
+
struct WireUniforms { color: vec4f, viewport: vec2f, thicknessPx: f32, _pad: f32 };
|
|
23
|
+
|
|
24
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
25
|
+
@group(0) @binding(1) var<uniform> wire: WireUniforms;
|
|
26
|
+
|
|
27
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
28
|
+
// The model's own vertex buffer: pos(3), normal(3), uv(2) — 8 floats a vertex.
|
|
29
|
+
@group(1) @binding(1) var<storage, read> vertices: array<f32>;
|
|
30
|
+
// uint16x4 per vertex, so two u32 each.
|
|
31
|
+
@group(1) @binding(2) var<storage, read> joints: array<u32>;
|
|
32
|
+
// unorm8x4 per vertex, one u32 each.
|
|
33
|
+
@group(1) @binding(3) var<storage, read> weights: array<u32>;
|
|
34
|
+
// Pairs of vertex indices, one pair an edge.
|
|
35
|
+
@group(1) @binding(4) var<storage, read> edges: array<u32>;
|
|
36
|
+
|
|
37
|
+
fn skinnedPos(i: u32) -> vec3f {
|
|
38
|
+
let o = i * 8u;
|
|
39
|
+
let rest = vec4f(vertices[o], vertices[o + 1u], vertices[o + 2u], 1.0);
|
|
40
|
+
|
|
41
|
+
let j0 = joints[i * 2u];
|
|
42
|
+
let j1 = joints[i * 2u + 1u];
|
|
43
|
+
let packed = weights[i];
|
|
44
|
+
var w = vec4f(
|
|
45
|
+
f32(packed & 0xffu),
|
|
46
|
+
f32((packed >> 8u) & 0xffu),
|
|
47
|
+
f32((packed >> 16u) & 0xffu),
|
|
48
|
+
f32((packed >> 24u) & 0xffu),
|
|
49
|
+
) / 255.0;
|
|
50
|
+
let sum = w.x + w.y + w.z + w.w;
|
|
51
|
+
w = select(vec4f(1.0, 0.0, 0.0, 0.0), w / max(sum, 1e-4), sum > 0.0001);
|
|
52
|
+
|
|
53
|
+
var acc = vec4f(0.0);
|
|
54
|
+
acc += (skinMats[j0 & 0xffffu] * rest) * w.x;
|
|
55
|
+
acc += (skinMats[j0 >> 16u] * rest) * w.y;
|
|
56
|
+
acc += (skinMats[j1 & 0xffffu] * rest) * w.z;
|
|
57
|
+
acc += (skinMats[j1 >> 16u] * rest) * w.w;
|
|
58
|
+
return acc.xyz;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Six vertices an edge: (a,-1) (a,+1) (b,-1) (a,+1) (b,+1) (b,-1).
|
|
62
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> @builtin(position) vec4f {
|
|
63
|
+
let a = edges[ii * 2u];
|
|
64
|
+
let b = edges[ii * 2u + 1u];
|
|
65
|
+
let vp = camera.projection * camera.view;
|
|
66
|
+
let ca = vp * vec4f(skinnedPos(a), 1.0);
|
|
67
|
+
let cb = vp * vec4f(skinnedPos(b), 1.0);
|
|
68
|
+
|
|
69
|
+
let atB = vi == 2u || vi == 4u || vi == 5u;
|
|
70
|
+
let side = select(-1.0, 1.0, vi == 1u || vi == 3u || vi == 4u);
|
|
71
|
+
let own = select(ca, cb, atB);
|
|
72
|
+
let other = select(cb, ca, atB);
|
|
73
|
+
|
|
74
|
+
let half = 0.5 * wire.viewport;
|
|
75
|
+
let s0 = (own.xy / max(abs(own.w), 1e-6)) * half;
|
|
76
|
+
let s1 = (other.xy / max(abs(other.w), 1e-6)) * half;
|
|
77
|
+
let d = s1 - s0;
|
|
78
|
+
// A degenerate edge has no direction on screen; any perpendicular will do,
|
|
79
|
+
// and it is a dot either way.
|
|
80
|
+
let tangent = select(vec2f(1.0, 0.0), normalize(d), length(d) > 1e-6);
|
|
81
|
+
let normalPx = vec2f(-tangent.y, tangent.x);
|
|
82
|
+
let offsetPx = normalPx * side * wire.thicknessPx * 0.5;
|
|
83
|
+
return vec4f(own.xy + (offsetPx / half) * own.w, own.z, own.w);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The depth prepass: the solid mesh through the ordinary vertex stream, writing
|
|
87
|
+
// depth and nothing else, so the far side of a body is hidden behind the near.
|
|
88
|
+
@vertex fn vsDepth(
|
|
89
|
+
@location(0) position: vec3f,
|
|
90
|
+
@location(1) joints0: vec4<u32>,
|
|
91
|
+
@location(2) weights0: vec4<f32>,
|
|
92
|
+
) -> @builtin(position) vec4f {
|
|
93
|
+
let rest = vec4f(position, 1.0);
|
|
94
|
+
let sum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
95
|
+
let w = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 / max(sum, 1e-4), sum > 0.0001);
|
|
96
|
+
var acc = vec4f(0.0);
|
|
97
|
+
for (var i = 0u; i < 4u; i++) {
|
|
98
|
+
acc += (skinMats[joints0[i]] * rest) * w[i];
|
|
99
|
+
}
|
|
100
|
+
return camera.projection * camera.view * vec4f(acc.xyz, 1.0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
104
|
+
// Premultiplied, like every other fragment in this layer.
|
|
105
|
+
return vec4f(wire.color.rgb * wire.color.a, wire.color.a);
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scene tap: the frame as the scene left it, for effects that bend light.
|
|
3
|
+
*
|
|
4
|
+
* ITS OWN FILE, AND IT IMPORTS NOTHING — the same rule and the same reason as
|
|
5
|
+
* cast-layout.ts. composite.ts already imports from lights.ts, so putting the
|
|
6
|
+
* stub in composite and importing it back into lights closes a cycle, and the
|
|
7
|
+
* first thing to touch that cycle throws "Cannot access RZ_LIGHT_STRUCT_WGSL
|
|
8
|
+
* before initialization" at module load: the whole engine failing to start on
|
|
9
|
+
* an import order nobody chose. Three modules need these strings; none of them
|
|
10
|
+
* may depend on another to get them.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* rzScene(uv) — the frame as the scene left it.
|
|
14
|
+
*
|
|
15
|
+
* The one thing a foreground could not do: an effect knew WHERE the scene was,
|
|
16
|
+
* through bgWorldPos and the depth tap, but never what COLOUR it was. Anything
|
|
17
|
+
* that RESAMPLES the picture needs this — refraction, heat haze, glass, a
|
|
18
|
+
* pixelated cast, and every CRT trick worth having (barrel curve, the RGB
|
|
19
|
+
* ghost, a torn line dragged sideways) is a read of the frame at a uv other
|
|
20
|
+
* than this pixel's own.
|
|
21
|
+
*
|
|
22
|
+
* WHAT IT IS NOT. The field pass runs BEFORE the composite, so this is the
|
|
23
|
+
* resolved scene — models, stage, ground — and nothing the composite adds
|
|
24
|
+
* afterwards: not the background colour, not a background effect, not the tone
|
|
25
|
+
* map, not the grade. Warping it warps the cast and the floor while the
|
|
26
|
+
* backdrop behind them stays put. That is a real limit, not a bug to route
|
|
27
|
+
* around, and an effect that wants the whole finished picture wants a pass
|
|
28
|
+
* after the composite instead.
|
|
29
|
+
*
|
|
30
|
+
* HDR and LINEAR for the same reason — it is read before the tone map, so
|
|
31
|
+
* values run past 1 and an effect doing display-space arithmetic on them
|
|
32
|
+
* should tone them itself.
|
|
33
|
+
*
|
|
34
|
+
* Legal because of pass order, and only that: the field pass runs after the
|
|
35
|
+
* scene pass has ended and RESOLVED, so this samples a finished texture rather
|
|
36
|
+
* than one being written into. Same reason the depth tap works, and it breaks
|
|
37
|
+
* the same way if either ever moves.
|
|
38
|
+
*
|
|
39
|
+
* ITS OWN SAMPLER, deliberately. castDistanceApi and gridReadApi already both
|
|
40
|
+
* declare one at binding 18, which is legal only while no single entry point
|
|
41
|
+
* references both; borrowing either made every effect that also touches the
|
|
42
|
+
* grid reference two variables at one binding, and WGSL rejects the module.
|
|
43
|
+
* Clamp-to-edge is what stops a warped offset wrapping the far side of the
|
|
44
|
+
* screen into the picture.
|
|
45
|
+
*/
|
|
46
|
+
export declare function sceneTapApi(group: number, tex: number, samp: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* The same names, for the modules with no finished scene to give.
|
|
49
|
+
*
|
|
50
|
+
* An effect file is spliced into EVERY module it has a mount in, so a water
|
|
51
|
+
* effect with a gridStep compiles its foreground() inside the SIM shader as
|
|
52
|
+
* well — dead code there that nothing calls, but code that still has to
|
|
53
|
+
* resolve. Without this, adding a scene read to any effect that also drives a
|
|
54
|
+
* grid fails to install with "unresolved call target rzScene".
|
|
55
|
+
*
|
|
56
|
+
* Black, not a guess: every one of these modules steps BEFORE the scene pass
|
|
57
|
+
* resolves, so at the moment they run the frame does not exist.
|
|
58
|
+
*/
|
|
59
|
+
export declare const SCENE_TAP_STUB = "\nfn rzScene(uv: vec2f) -> vec3f { return vec3f(0.0); }\nfn bgScene(uv: vec2f) -> vec3f { return rzScene(uv); }\n/** Far, which in these modules is true: no geometry has been drawn yet. */\nfn rzSceneDepth(uv: vec2f) -> f32 { return 1e9; }\nfn rzSceneFar() -> f32 { return 1e9; }\nfn rzSceneHit(uv: vec2f) -> bool { return false; }\nfn rzSceneAlpha(uv: vec2f) -> f32 { return 0.0; }\nfn rzSceneDisplay(uv: vec2f) -> vec3f { return vec3f(0.0); }\nfn rzSceneFrame(uv: vec2f) -> vec3f { return vec3f(0.0); }\nfn rzBackground() -> vec4f { return vec4f(0.0); }\n";
|
|
60
|
+
//# sourceMappingURL=scene-tap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scene-tap.d.ts","sourceRoot":"","sources":["../../src/shaders/scene-tap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAoK5E;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,kjBAW1B,CAAA"}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scene tap: the frame as the scene left it, for effects that bend light.
|
|
3
|
+
*
|
|
4
|
+
* ITS OWN FILE, AND IT IMPORTS NOTHING — the same rule and the same reason as
|
|
5
|
+
* cast-layout.ts. composite.ts already imports from lights.ts, so putting the
|
|
6
|
+
* stub in composite and importing it back into lights closes a cycle, and the
|
|
7
|
+
* first thing to touch that cycle throws "Cannot access RZ_LIGHT_STRUCT_WGSL
|
|
8
|
+
* before initialization" at module load: the whole engine failing to start on
|
|
9
|
+
* an import order nobody chose. Three modules need these strings; none of them
|
|
10
|
+
* may depend on another to get them.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* rzScene(uv) — the frame as the scene left it.
|
|
14
|
+
*
|
|
15
|
+
* The one thing a foreground could not do: an effect knew WHERE the scene was,
|
|
16
|
+
* through bgWorldPos and the depth tap, but never what COLOUR it was. Anything
|
|
17
|
+
* that RESAMPLES the picture needs this — refraction, heat haze, glass, a
|
|
18
|
+
* pixelated cast, and every CRT trick worth having (barrel curve, the RGB
|
|
19
|
+
* ghost, a torn line dragged sideways) is a read of the frame at a uv other
|
|
20
|
+
* than this pixel's own.
|
|
21
|
+
*
|
|
22
|
+
* WHAT IT IS NOT. The field pass runs BEFORE the composite, so this is the
|
|
23
|
+
* resolved scene — models, stage, ground — and nothing the composite adds
|
|
24
|
+
* afterwards: not the background colour, not a background effect, not the tone
|
|
25
|
+
* map, not the grade. Warping it warps the cast and the floor while the
|
|
26
|
+
* backdrop behind them stays put. That is a real limit, not a bug to route
|
|
27
|
+
* around, and an effect that wants the whole finished picture wants a pass
|
|
28
|
+
* after the composite instead.
|
|
29
|
+
*
|
|
30
|
+
* HDR and LINEAR for the same reason — it is read before the tone map, so
|
|
31
|
+
* values run past 1 and an effect doing display-space arithmetic on them
|
|
32
|
+
* should tone them itself.
|
|
33
|
+
*
|
|
34
|
+
* Legal because of pass order, and only that: the field pass runs after the
|
|
35
|
+
* scene pass has ended and RESOLVED, so this samples a finished texture rather
|
|
36
|
+
* than one being written into. Same reason the depth tap works, and it breaks
|
|
37
|
+
* the same way if either ever moves.
|
|
38
|
+
*
|
|
39
|
+
* ITS OWN SAMPLER, deliberately. castDistanceApi and gridReadApi already both
|
|
40
|
+
* declare one at binding 18, which is legal only while no single entry point
|
|
41
|
+
* references both; borrowing either made every effect that also touches the
|
|
42
|
+
* grid reference two variables at one binding, and WGSL rejects the module.
|
|
43
|
+
* Clamp-to-edge is what stops a warped offset wrapping the far side of the
|
|
44
|
+
* screen into the picture.
|
|
45
|
+
*/
|
|
46
|
+
export function sceneTapApi(group, tex, samp) {
|
|
47
|
+
return /* wgsl */ `
|
|
48
|
+
@group(${group}) @binding(${tex}) var _rzSceneTex: texture_2d<f32>;
|
|
49
|
+
@group(${group}) @binding(${samp}) var _rzSceneSamp: sampler;
|
|
50
|
+
|
|
51
|
+
/** uv as the mount gives it (y=0 at the BOTTOM, the shadertoy convention the
|
|
52
|
+
* composite documents) turned into a texture coordinate (y=0 at the top).
|
|
53
|
+
* Sampling the mount's uv straight returns the scene upside down, which is a
|
|
54
|
+
* bug that looks like an art direction — so it is done here once rather than in
|
|
55
|
+
* every effect that reads the frame. */
|
|
56
|
+
fn _rzTapUv(uv: vec2f) -> vec2f {
|
|
57
|
+
return clamp(vec2f(uv.x, 1.0 - uv.y), vec2f(0.0), vec2f(1.0));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* How much of this pixel the scene COVERS, 0..1.
|
|
62
|
+
*
|
|
63
|
+
* The half of the frame that colour alone cannot tell you, and the reason a
|
|
64
|
+
* ground that fades to nothing at its rim came back as a hard-edged slab. The
|
|
65
|
+
* scene renders into a PREMULTIPLIED target, so a plane drawn at 48% with a
|
|
66
|
+
* radial falloff stores a dimmer colour, not a lighter one — read the colour and
|
|
67
|
+
* ignore this and you get an opaque dark rectangle where there was a fading
|
|
68
|
+
* sheet. It is also finer than any depth test: depth is binary and a plane
|
|
69
|
+
* writes it across its whole square whatever its alpha does.
|
|
70
|
+
*/
|
|
71
|
+
fn rzSceneAlpha(uv: vec2f) -> f32 {
|
|
72
|
+
let sz = vec2f(textureDimensions(maskTex));
|
|
73
|
+
let p = clamp(_rzTapUv(uv) * sz, vec2f(0.0), sz - vec2f(1.0));
|
|
74
|
+
return textureLoad(maskTex, vec2<i32>(p), 0).g;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The scene at uv in LINEAR light, straight rather than premultiplied, with the
|
|
79
|
+
* bloom the composite would add to it. What the view transform takes.
|
|
80
|
+
*
|
|
81
|
+
* Divided back out by coverage, so this is the colour the geometry HAS rather
|
|
82
|
+
* than the colour it contributes. Where nothing was drawn the division is
|
|
83
|
+
* meaningless and the result is black — pair it with rzSceneAlpha, never use it
|
|
84
|
+
* alone.
|
|
85
|
+
*/
|
|
86
|
+
fn rzScene(uv: vec2f) -> vec3f {
|
|
87
|
+
let p = _rzTapUv(uv);
|
|
88
|
+
let pm = textureSampleLevel(_rzSceneTex, _rzSceneSamp, p, 0.0).rgb;
|
|
89
|
+
let straight = pm / max(rzSceneAlpha(uv), 1e-6);
|
|
90
|
+
let bloom = textureSampleLevel(bloomTex, bloomSamp, p, 0.0).rgb * viewU[1].xyz * viewU[1].w;
|
|
91
|
+
return straight + bloom;
|
|
92
|
+
}
|
|
93
|
+
/** Alias, matching bgWorldPos and the rest of the bg-prefixed surface. */
|
|
94
|
+
fn bgScene(uv: vec2f) -> vec3f { return rzScene(uv); }
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* How far the scene is at ANY pixel — the depth tap, off this fragment's leash.
|
|
98
|
+
*
|
|
99
|
+
* foreground() is handed the depth of its OWN pixel, which answers "is this
|
|
100
|
+
* behind me" and nothing else. Anything that resamples asks a different
|
|
101
|
+
* question: it reads the frame somewhere else and needs to know whether the
|
|
102
|
+
* scene was even drawn THERE. One pixel's depth cannot say, and without it an
|
|
103
|
+
* effect that replaces pixels has to replace all of them — painting the empty
|
|
104
|
+
* sky black because that is what the scene texture holds where nothing drew.
|
|
105
|
+
*
|
|
106
|
+
* NO NEW BINDING. The depth buffer has been in this group since foreground got
|
|
107
|
+
* its argument and linearDepth already inverts it; this only lets an effect name
|
|
108
|
+
* a pixel other than its own. Metres along the view axis, the same units as the
|
|
109
|
+
* depth argument and as rzProject's z, so comparing them is a subtraction.
|
|
110
|
+
*
|
|
111
|
+
* Cleared depth inverts to the far plane, so "nothing here" reads as a very
|
|
112
|
+
* large number rather than as zero.
|
|
113
|
+
*/
|
|
114
|
+
fn rzSceneDepth(uv: vec2f) -> f32 {
|
|
115
|
+
let sz = vec2f(textureDimensions(depthTex));
|
|
116
|
+
let p = clamp(vec2f(uv.x, 1.0 - uv.y) * sz, vec2f(0.0), sz - vec2f(1.0));
|
|
117
|
+
return linearDepth(vec2<i32>(p));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The far plane, in the same metres rzSceneDepth answers in.
|
|
122
|
+
*
|
|
123
|
+
* Read straight from the uniform the camera writes it into, NOT re-derived by
|
|
124
|
+
* inverting a cleared depth. That inversion is projB / (1 - projA), whose sign
|
|
125
|
+
* depends on which way round the projection maps z — and when it comes out
|
|
126
|
+
* wrong it does not error, it clamps to the near plane and every rzSceneHit
|
|
127
|
+
* answers false, so the effect silently draws nothing at all.
|
|
128
|
+
*/
|
|
129
|
+
fn rzSceneFar() -> f32 {
|
|
130
|
+
return max(dofU[2].z, 0.05);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Was anything actually DRAWN at this uv?
|
|
135
|
+
*
|
|
136
|
+
* The question every effect that resamples has to ask, and the one no author
|
|
137
|
+
* should answer with a constant. The far plane is not a fixed number here — it
|
|
138
|
+
* tracks the camera (roughly its orbit radius times twelve), so a threshold
|
|
139
|
+
* picked by eye is right at one distance and wrong at every other. Guess high
|
|
140
|
+
* and empty sky reads as geometry, and an effect that replaces pixels paints the
|
|
141
|
+
* background black; guess low and the far half of a stage disappears.
|
|
142
|
+
*
|
|
143
|
+
* Compared against the plane itself, with a hair of margin for the precision the
|
|
144
|
+
* inversion loses out there.
|
|
145
|
+
*/
|
|
146
|
+
fn rzSceneHit(uv: vec2f) -> bool {
|
|
147
|
+
return rzSceneDepth(uv) < rzSceneFar() * 0.995;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The scene at uv, in the SAME space as the pixel it would replace.
|
|
152
|
+
*
|
|
153
|
+
* rzScene is linear and pre-tone-map. Anything that resamples has to put what it
|
|
154
|
+
* fetched back into display space, and every effect that tried did it by hand —
|
|
155
|
+
* a Reinhard shoulder and a 2.2 gamma, near enough at a glance and wrong
|
|
156
|
+
* everywhere it mattered. The tell is a mosaic or a glitch applied to part of
|
|
157
|
+
* the frame: the treated region reads a shade off from the untreated one beside
|
|
158
|
+
* it, and no amount of tuning closes it, because the engine's curve is Filmic or
|
|
159
|
+
* AgX with the scene's own exposure and grade, not a shoulder.
|
|
160
|
+
*
|
|
161
|
+
* So it is not approximated. This is the composite's own chain — exposure,
|
|
162
|
+
* viewTransform, the grade when the scene has one, then the display gamma —
|
|
163
|
+
* applied to the same sample. Fetch a pixel at its own uv through this and you
|
|
164
|
+
* get the pixel that is already there, so a displacement of zero is invisible
|
|
165
|
+
* and the boundary of any masked effect disappears.
|
|
166
|
+
*
|
|
167
|
+
* The gamma is applied unconditionally rather than behind APPLY_GAMMA: the
|
|
168
|
+
* override is a pipeline constant the field pass need not share, and pow(x, 1)
|
|
169
|
+
* is the identity, so writing it out is both safer and equivalent.
|
|
170
|
+
*/
|
|
171
|
+
fn rzSceneDisplay(uv: vec2f) -> vec3f {
|
|
172
|
+
var c = max(viewTransform(rzScene(uv) * exp2(viewU[0].x)), vec3f(0.0));
|
|
173
|
+
if (viewU[9].w > 0.5) { c = grade(c); }
|
|
174
|
+
return pow(max(c, vec3f(0.0)), vec3f(viewU[0].y));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The finished pixel: the scene over its background, exactly as the composite
|
|
179
|
+
* would lay it down. One call, and the answer already carries the ground's
|
|
180
|
+
* fade, its opacity, and the background showing through both.
|
|
181
|
+
*
|
|
182
|
+
* This is what a FILTER wants — anything re-rendering the whole frame on a tube
|
|
183
|
+
* or a low-resolution screen. Reaching for rzSceneDisplay and a depth test
|
|
184
|
+
* instead rebuilds this by hand and gets it wrong in one specific way every
|
|
185
|
+
* time: coverage is not a yes or no, and treating it as one turns every soft
|
|
186
|
+
* edge in the scene into a hard one.
|
|
187
|
+
*
|
|
188
|
+
* NOT INCLUDED: depth of field, background effects, and an equirect background
|
|
189
|
+
* (a 360 dome reads as its average rather than its texture). Foregrounds compose
|
|
190
|
+
* after this in any case.
|
|
191
|
+
*/
|
|
192
|
+
fn rzSceneFrame(uv: vec2f) -> vec3f {
|
|
193
|
+
let a = clamp(rzSceneAlpha(uv), 0.0, 1.0);
|
|
194
|
+
let bg = viewU[2];
|
|
195
|
+
let bgRgb = bg.rgb * select(0.0, 1.0, bg.w > 0.5);
|
|
196
|
+
return rzSceneDisplay(uv) * a + bgRgb * (1.0 - a);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The scene's background: rgb is the picked colour in DISPLAY space, and w says
|
|
201
|
+
* what kind it is — 0 transparent (a backdrop or an alpha export is showing
|
|
202
|
+
* through), 1 a flat colour, above 1 an equirect dome.
|
|
203
|
+
*
|
|
204
|
+
* For effects that need to know what sits behind the scene. It is NOT what a
|
|
205
|
+
* background effect drew — those compose in the pass after this one — so an
|
|
206
|
+
* effect that paints over the background with this erases them.
|
|
207
|
+
*/
|
|
208
|
+
fn rzBackground() -> vec4f { return viewU[2]; }
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The same names, for the modules with no finished scene to give.
|
|
213
|
+
*
|
|
214
|
+
* An effect file is spliced into EVERY module it has a mount in, so a water
|
|
215
|
+
* effect with a gridStep compiles its foreground() inside the SIM shader as
|
|
216
|
+
* well — dead code there that nothing calls, but code that still has to
|
|
217
|
+
* resolve. Without this, adding a scene read to any effect that also drives a
|
|
218
|
+
* grid fails to install with "unresolved call target rzScene".
|
|
219
|
+
*
|
|
220
|
+
* Black, not a guess: every one of these modules steps BEFORE the scene pass
|
|
221
|
+
* resolves, so at the moment they run the frame does not exist.
|
|
222
|
+
*/
|
|
223
|
+
export const SCENE_TAP_STUB = /* wgsl */ `
|
|
224
|
+
fn rzScene(uv: vec2f) -> vec3f { return vec3f(0.0); }
|
|
225
|
+
fn bgScene(uv: vec2f) -> vec3f { return rzScene(uv); }
|
|
226
|
+
/** Far, which in these modules is true: no geometry has been drawn yet. */
|
|
227
|
+
fn rzSceneDepth(uv: vec2f) -> f32 { return 1e9; }
|
|
228
|
+
fn rzSceneFar() -> f32 { return 1e9; }
|
|
229
|
+
fn rzSceneHit(uv: vec2f) -> bool { return false; }
|
|
230
|
+
fn rzSceneAlpha(uv: vec2f) -> f32 { return 0.0; }
|
|
231
|
+
fn rzSceneDisplay(uv: vec2f) -> vec3f { return vec3f(0.0); }
|
|
232
|
+
fn rzSceneFrame(uv: vec2f) -> vec3f { return vec3f(0.0); }
|
|
233
|
+
fn rzBackground() -> vec4f { return vec4f(0.0); }
|
|
234
|
+
`;
|
|
@@ -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
|
+
}
|