reze-engine 0.50.3 → 0.50.4
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 +237 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +793 -64
- 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/shaders/cast-api.d.ts +1 -1
- package/dist/shaders/cast-api.d.ts.map +1 -1
- package/dist/shaders/cast-layout.d.ts +44 -1
- package/dist/shaders/cast-layout.d.ts.map +1 -1
- package/dist/shaders/cast-layout.js +44 -1
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +7 -1
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +17 -9
- package/dist/shaders/passes/composite.d.ts +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/depth-prepass.d.ts +1 -1
- package/dist/shaders/passes/depth-prepass.d.ts.map +1 -1
- package/dist/shaders/passes/depth-prepass.js +52 -12
- 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/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/ground.d.ts +16 -0
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +131 -27
- package/dist/shaders/passes/outline.d.ts +1 -1
- package/dist/shaders/passes/outline.d.ts.map +1 -1
- package/dist/shaders/passes/outline.js +12 -3
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +6 -2
- package/dist/shaders/passes/scene-contract.d.ts +38 -6
- package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
- package/dist/shaders/passes/scene-contract.js +53 -16
- 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.map +1 -1
- package/dist/shaders/passes/trails.js +36 -8
- 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 +839 -56
- package/src/shaders/cast-layout.ts +44 -1
- package/src/shaders/materials/common.ts +7 -1
- package/src/shaders/materials/nodes.ts +17 -9
- package/src/shaders/passes/depth-prepass.ts +53 -12
- package/src/shaders/passes/ground.ts +133 -27
- package/src/shaders/passes/outline.ts +14 -3
- package/src/shaders/passes/particles.ts +6 -2
- package/src/shaders/passes/scene-contract.ts +55 -16
- package/src/shaders/passes/trails.ts +36 -8
- package/dist/physics-debug.d.ts +0 -30
- package/dist/physics-debug.d.ts.map +0 -1
- package/dist/physics-debug.js +0 -526
- package/dist/shaders/materials/body.d.ts +0 -2
- package/dist/shaders/materials/body.d.ts.map +0 -1
- package/dist/shaders/materials/body.js +0 -95
- package/dist/shaders/materials/cloth_rough.d.ts +0 -2
- package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_rough.js +0 -69
- package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
- package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_smooth.js +0 -61
- package/dist/shaders/materials/default.d.ts +0 -2
- package/dist/shaders/materials/default.d.ts.map +0 -1
- package/dist/shaders/materials/default.js +0 -43
- package/dist/shaders/materials/eye.d.ts +0 -2
- package/dist/shaders/materials/eye.d.ts.map +0 -1
- package/dist/shaders/materials/eye.js +0 -60
- package/dist/shaders/materials/face.d.ts +0 -2
- package/dist/shaders/materials/face.d.ts.map +0 -1
- package/dist/shaders/materials/face.js +0 -95
- package/dist/shaders/materials/hair.d.ts +0 -2
- package/dist/shaders/materials/hair.d.ts.map +0 -1
- package/dist/shaders/materials/hair.js +0 -90
- package/dist/shaders/materials/metal.d.ts +0 -2
- package/dist/shaders/materials/metal.d.ts.map +0 -1
- package/dist/shaders/materials/metal.js +0 -77
- package/dist/shaders/materials/mmd_classic.d.ts +0 -2
- package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
- package/dist/shaders/materials/mmd_classic.js +0 -66
- package/dist/shaders/materials/stockings.d.ts +0 -2
- package/dist/shaders/materials/stockings.d.ts.map +0 -1
- package/dist/shaders/materials/stockings.js +0 -122
- package/dist/shaders/passes/physics-debug.d.ts +0 -2
- package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
- package/dist/shaders/passes/physics-debug.js +0 -69
|
@@ -89,12 +89,14 @@ const ADD_PREMULTIPLIED = {
|
|
|
89
89
|
alpha: { srcFactor: "zero", dstFactor: "one", operation: "add" },
|
|
90
90
|
};
|
|
91
91
|
/**
|
|
92
|
-
* Which classes
|
|
92
|
+
* Which classes write a MEANINGFUL id.
|
|
93
93
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
94
|
+
* Not the same question as which shaders declare the output — every scene-pass
|
|
95
|
+
* shader declares all three now (see sceneFsOutWgsl). This set decides only
|
|
96
|
+
* whose id survives: writeMask 0xf here, writeMask 0 for everyone else, so the
|
|
97
|
+
* others compute an id nothing stores. Leaving the target off those pipelines
|
|
98
|
+
* is not available: every pipeline in a pass must agree with the pass's
|
|
99
|
+
* attachments.
|
|
98
100
|
*
|
|
99
101
|
* The ground is in because a mark placed by id needs the floor to have one.
|
|
100
102
|
* Transparent fabric writes ids too — dissolving a dress needs the dress's own
|
|
@@ -127,11 +129,10 @@ const BLENDS = {
|
|
|
127
129
|
*/
|
|
128
130
|
export function sceneTargets(cls, formats) {
|
|
129
131
|
const targets = cls === "depth-prepass"
|
|
130
|
-
? // Format only, and writeMask 0
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
// direction, and it is the only one this file ever uses.
|
|
132
|
+
? // Format only, and writeMask 0 — the pipeline writes no colour. It still
|
|
133
|
+
// DECLARES every output, which is the part that used to be missing; see
|
|
134
|
+
// the note on sceneFsOutWgsl about why writeMask 0 is not a licence to
|
|
135
|
+
// leave the output off.
|
|
135
136
|
[
|
|
136
137
|
{ format: formats.hdr, writeMask: 0 },
|
|
137
138
|
{ format: formats.aux, writeMask: 0 },
|
|
@@ -168,12 +169,19 @@ export function sceneColorFormats(formats) {
|
|
|
168
169
|
* author. The struct and the targets above have to agree on count and order,
|
|
169
170
|
* and they now disagree in one file rather than in five.
|
|
170
171
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
172
|
+
* EVERY scene-pass shader takes this — outline, particles, ribbons and the
|
|
173
|
+
* depth prepass included, none of which write a meaningful id (and the prepass
|
|
174
|
+
* no meaningful colour either). They did not, once. The reasoning was that a
|
|
175
|
+
* target at writeMask 0 needs no matching output: true of Dawn, and the reading
|
|
176
|
+
* of gpuweb#1918 this file used to assert. It is not a reading every browser
|
|
177
|
+
* shares, and the failure mode when a browser disagrees is the worst kind —
|
|
178
|
+
* createRenderPipeline does not throw, it returns a pipeline that is already
|
|
179
|
+
* invalid, and the pass that binds it is dropped whole. Missing geometry, clean
|
|
180
|
+
* console, and a bug that reproduces on one vendor's browser only.
|
|
181
|
+
*
|
|
182
|
+
* So the rule is now the strict one, and it costs nothing to hold: declare
|
|
183
|
+
* every output the pass has attachments for, and let writeMask decide what is
|
|
184
|
+
* kept. A shader with no id to write pads it — see sceneIdPadWgsl.
|
|
177
185
|
*/
|
|
178
186
|
export function sceneFsOutWgsl(opts) {
|
|
179
187
|
const name = opts?.name ?? "FSOut";
|
|
@@ -205,3 +213,32 @@ ${id}};
|
|
|
205
213
|
export function sceneIdWriteWgsl(out, material, object) {
|
|
206
214
|
return mrtIds ? ` ${out}.id = vec2u(${material}, ${object});\n` : "";
|
|
207
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Just the id FIELD, for a shader that keeps its own struct.
|
|
218
|
+
*
|
|
219
|
+
* Particles and ribbons declare their outputs by hand because the comments on
|
|
220
|
+
* their aux field are about their own blend and belong beside it — taking the
|
|
221
|
+
* whole struct from sceneFsOutWgsl would move that prose away from what it
|
|
222
|
+
* explains. They still need the id output when the pass has the attachment, so
|
|
223
|
+
* they splice this in and pad it with sceneIdPadWgsl.
|
|
224
|
+
*/
|
|
225
|
+
export function sceneIdFieldWgsl() {
|
|
226
|
+
return mrtIds ? ` @location(2) id: vec2u,\n` : "";
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The id assignment for a shader that has no id to give.
|
|
230
|
+
*
|
|
231
|
+
* The counterpart to the strict rule on sceneFsOutWgsl: outline hulls,
|
|
232
|
+
* particles, ribbons and the depth prepass all declare the output because the
|
|
233
|
+
* pass has the attachment, and all of them take writeMask 0, so what they
|
|
234
|
+
* assign is never stored. Zero, which is the reserved "nothing" id the pass
|
|
235
|
+
* clears to — so if one of these ever did reach the target, it would read as
|
|
236
|
+
* absence rather than as a plausible wrong answer pointing at material 0.
|
|
237
|
+
*
|
|
238
|
+
* A separate function from sceneIdWriteWgsl rather than a call to it with "0u"
|
|
239
|
+
* twice, because the two say different things: that one records an identity,
|
|
240
|
+
* this one satisfies a declaration.
|
|
241
|
+
*/
|
|
242
|
+
export function sceneIdPadWgsl(out) {
|
|
243
|
+
return mrtIds ? ` ${out}.id = vec2u(0u, 0u);\n` : "";
|
|
244
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type CastLayout } from "./particles";
|
|
2
|
+
export declare const SIM_MAX = 1024;
|
|
3
|
+
/** rgba16float, ping-ponged. Two of these is the whole memory cost. */
|
|
4
|
+
export declare const SIM_FORMAT: GPUTextureFormat;
|
|
5
|
+
/** `// @sim 256` — the grid's resolution, in texels per side. */
|
|
6
|
+
export declare function parseSimSize(wgsl: string, max: number): number;
|
|
7
|
+
/** Whether this effect drives a grid at all. */
|
|
8
|
+
export declare function simEntryPoint(wgsl: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* `rzSim(uv)`, for a shader that READS the grid rather than steps it.
|
|
11
|
+
*
|
|
12
|
+
* Always compiled in, even for an effect with no grid — it then samples a 1×1
|
|
13
|
+
* of zeroes. An accessor that exists only sometimes is one an author has to
|
|
14
|
+
* guard, and a missing function is a compile error rather than a blank result.
|
|
15
|
+
*/
|
|
16
|
+
export declare function simReadApi(group: number, tex: number, samp: number, size: number): string;
|
|
17
|
+
/** `rzTime`/`rzDt`, for a module that has no clock of its own — the field pass.
|
|
18
|
+
* Same reason as above: a spliced simStep refers to them. */
|
|
19
|
+
export declare function simClockApi(timeExpr: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* The step shader: one invocation per texel, once per frame.
|
|
22
|
+
*
|
|
23
|
+
* The previous grid is bound as a SAMPLED texture and the next as a storage
|
|
24
|
+
* texture, which is what makes the two different resources and the whole thing
|
|
25
|
+
* legal — a shader cannot read and write one texture coherently, and the
|
|
26
|
+
* ping-pong is not an optimisation but the only correct way to do this.
|
|
27
|
+
*
|
|
28
|
+
* Sampled rather than only fetched, because advection needs to read BETWEEN
|
|
29
|
+
* texels: a semi-Lagrangian step asks "what was at the place this parcel came
|
|
30
|
+
* from", and that place is almost never a texel centre. Point-sampling it is
|
|
31
|
+
* what turns smoke into a staircase.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildSimShader(wgsl: string, size: number, _cast: CastLayout): string;
|
|
34
|
+
//# sourceMappingURL=sim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sim.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/sim.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAgC7C,eAAO,MAAM,OAAO,OAAO,CAAA;AAC3B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9D;AAED,gDAAgD;AAChD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6BzF;AAED;8DAC8D;AAC9D,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKpD;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAwDpF"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { audioApi } from "../audio-api";
|
|
2
|
+
import { scoreApi } from "../score-api";
|
|
3
|
+
import { EFFECT_SCENE_API } from "./composite";
|
|
4
|
+
// A persistent grid an effect can step and read: the one thing an effect could
|
|
5
|
+
// not have before, which is MEMORY.
|
|
6
|
+
//
|
|
7
|
+
// Every other mount is a pure function of position and time. A field shader
|
|
8
|
+
// recomputes its noise from scratch every frame; a trail reads history the
|
|
9
|
+
// engine happens to keep. Neither can build on what it produced last frame, and
|
|
10
|
+
// a whole class of effect is nothing but that:
|
|
11
|
+
//
|
|
12
|
+
// Fog that a dancer walks through does not "look displaced near her" — it IS
|
|
13
|
+
// displaced, and stays displaced, and creeps back over seconds. Water does not
|
|
14
|
+
// look rippled where a foot landed; a ripple leaves and propagates and
|
|
15
|
+
// reflects off the far edge long after the foot is gone. Both are the same
|
|
16
|
+
// shape of thing — a grid, stepped by its own previous value.
|
|
17
|
+
//
|
|
18
|
+
// It is also the only way to get the LOOK of a fluid, not merely its motion.
|
|
19
|
+
// Advection compounds: each frame's field is the last one pushed sideways, and a
|
|
20
|
+
// few hundred frames of that stretches and folds a smooth field into filaments
|
|
21
|
+
// and vortex sheets finer than the grid storing them. No noise function
|
|
22
|
+
// produces that, at any octave count, because the structure is not a function of
|
|
23
|
+
// position at all — it is a function of history.
|
|
24
|
+
//
|
|
25
|
+
// The engine owns the ping-pong, the dispatch and the sampling. The author owns
|
|
26
|
+
// the KERNEL, which is the part that differs: advection for smoke, the wave
|
|
27
|
+
// equation for water, a decaying stamp for footprints. That split is the same
|
|
28
|
+
// one the particle mounts make, and for the same reason — the loop is identical
|
|
29
|
+
// in every such effect and the physics never is.
|
|
30
|
+
// A megatexel of rgba16float is 8MB, and two of those is the whole cost. What
|
|
31
|
+
// actually bounds this is the STEP: one invocation per texel per frame, each
|
|
32
|
+
// taking a dozen samples, so doubling the side quadruples the work.
|
|
33
|
+
export const SIM_MAX = 1024;
|
|
34
|
+
/** rgba16float, ping-ponged. Two of these is the whole memory cost. */
|
|
35
|
+
export const SIM_FORMAT = "rgba16float";
|
|
36
|
+
/** `// @sim 256` — the grid's resolution, in texels per side. */
|
|
37
|
+
export function parseSimSize(wgsl, max) {
|
|
38
|
+
const m = /^\s*\/\/\s*@sim\s+(\d+)\s*$/m.exec(wgsl);
|
|
39
|
+
if (!m)
|
|
40
|
+
return 0;
|
|
41
|
+
// Clamped rather than rejected, as the particle count is: an author asking for
|
|
42
|
+
// 4096 gets the most the engine will give and a scene that still runs.
|
|
43
|
+
return Math.max(8, Math.min(max, parseInt(m[1], 10)));
|
|
44
|
+
}
|
|
45
|
+
/** Whether this effect drives a grid at all. */
|
|
46
|
+
export function simEntryPoint(wgsl) {
|
|
47
|
+
return /\bfn\s+simStep\s*\(/.test(wgsl);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `rzSim(uv)`, for a shader that READS the grid rather than steps it.
|
|
51
|
+
*
|
|
52
|
+
* Always compiled in, even for an effect with no grid — it then samples a 1×1
|
|
53
|
+
* of zeroes. An accessor that exists only sometimes is one an author has to
|
|
54
|
+
* guard, and a missing function is a compile error rather than a blank result.
|
|
55
|
+
*/
|
|
56
|
+
export function simReadApi(group, tex, samp, size) {
|
|
57
|
+
return /* wgsl */ `
|
|
58
|
+
const RZ_SIM_SIZE: f32 = ${size > 0 ? size : 1}.0;
|
|
59
|
+
@group(${group}) @binding(${tex}) var _rzSimTex: texture_2d<f32>;
|
|
60
|
+
@group(${group}) @binding(${samp}) var _rzSimSamp: sampler;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The grid, bilinearly sampled. uv is 0..1 across it, and what that MEANS in
|
|
64
|
+
* the world is the effect's own business — the engine deliberately does not
|
|
65
|
+
* impose a mapping, because a fog laid over a stage and a ripple field around a
|
|
66
|
+
* character want different ones and both are two lines of arithmetic.
|
|
67
|
+
*/
|
|
68
|
+
fn rzSim(uv: vec2f) -> vec4f {
|
|
69
|
+
return textureSampleLevel(_rzSimTex, _rzSimSamp, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0);
|
|
70
|
+
}
|
|
71
|
+
fn rzSimSize() -> f32 { return RZ_SIM_SIZE; }
|
|
72
|
+
fn rzSimTexel() -> f32 { return 1.0 / RZ_SIM_SIZE; }
|
|
73
|
+
|
|
74
|
+
// ── The step-only half, defined here too and never called here ──
|
|
75
|
+
//
|
|
76
|
+
// One effect file is spliced into EVERY module it has a mount in, so a file
|
|
77
|
+
// with a grid compiles its simStep inside the field and particle shaders as
|
|
78
|
+
// well — where it is dead code that nothing calls, but still code that has to
|
|
79
|
+
// resolve. Leaving these out is a compile error on a function the author was
|
|
80
|
+
// right to write. rzSimPrev reads the current grid rather than a previous one,
|
|
81
|
+
// which is the honest answer outside the step: there is no previous frame here.
|
|
82
|
+
fn rzSimPrev(uv: vec2f) -> vec4f { return rzSim(uv); }
|
|
83
|
+
fn rzSimFrame() -> i32 { return 1; }
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
/** `rzTime`/`rzDt`, for a module that has no clock of its own — the field pass.
|
|
87
|
+
* Same reason as above: a spliced simStep refers to them. */
|
|
88
|
+
export function simClockApi(timeExpr) {
|
|
89
|
+
return /* wgsl */ `
|
|
90
|
+
fn rzTime() -> f32 { return ${timeExpr}; }
|
|
91
|
+
fn rzDt() -> f32 { return 0.0; }
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
const SIM_UNIFORMS = /* wgsl */ `
|
|
95
|
+
struct SimU {
|
|
96
|
+
time: f32,
|
|
97
|
+
dt: f32,
|
|
98
|
+
size: f32,
|
|
99
|
+
frame: f32,
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
/**
|
|
103
|
+
* The step shader: one invocation per texel, once per frame.
|
|
104
|
+
*
|
|
105
|
+
* The previous grid is bound as a SAMPLED texture and the next as a storage
|
|
106
|
+
* texture, which is what makes the two different resources and the whole thing
|
|
107
|
+
* legal — a shader cannot read and write one texture coherently, and the
|
|
108
|
+
* ping-pong is not an optimisation but the only correct way to do this.
|
|
109
|
+
*
|
|
110
|
+
* Sampled rather than only fetched, because advection needs to read BETWEEN
|
|
111
|
+
* texels: a semi-Lagrangian step asks "what was at the place this parcel came
|
|
112
|
+
* from", and that place is almost never a texel centre. Point-sampling it is
|
|
113
|
+
* what turns smoke into a staircase.
|
|
114
|
+
*/
|
|
115
|
+
export function buildSimShader(wgsl, size, _cast) {
|
|
116
|
+
return (SIM_UNIFORMS +
|
|
117
|
+
/* wgsl */ `
|
|
118
|
+
@group(0) @binding(0) var<uniform> su: SimU;
|
|
119
|
+
@group(0) @binding(1) var _rzSimPrevTex: texture_2d<f32>;
|
|
120
|
+
@group(0) @binding(2) var _rzSimSamp: sampler;
|
|
121
|
+
@group(0) @binding(3) var _rzSimOut: texture_storage_2d<${SIM_FORMAT}, write>;
|
|
122
|
+
@group(0) @binding(4) var<storage, read> _rzCast: array<vec4f>;
|
|
123
|
+
// The same view uniform the composite reads, so the scene API below is the real
|
|
124
|
+
// thing here rather than a stub — a kernel can ask where a bone is, and a
|
|
125
|
+
// foreground spliced into this module resolves its camera calls.
|
|
126
|
+
@group(0) @binding(6) var<uniform> viewU: array<vec4<f32>, 15>;
|
|
127
|
+
|
|
128
|
+
fn rzTime() -> f32 { return su.time; }
|
|
129
|
+
fn rzDt() -> f32 { return su.dt; }
|
|
130
|
+
/** Texels per side. */
|
|
131
|
+
fn rzSimSize() -> f32 { return su.size; }
|
|
132
|
+
/** One texel, in uv — the step a neighbour lookup takes. */
|
|
133
|
+
fn rzSimTexel() -> f32 { return 1.0 / su.size; }
|
|
134
|
+
/**
|
|
135
|
+
* Steps since this effect was installed. Zero on the very first one, which is
|
|
136
|
+
* the only chance to seed a grid: everything after it builds on what is there.
|
|
137
|
+
*/
|
|
138
|
+
fn rzSimFrame() -> i32 { return i32(su.frame); }
|
|
139
|
+
|
|
140
|
+
/** The grid as it was LAST frame, bilinear. The only input a kernel really has. */
|
|
141
|
+
fn rzSimPrev(uv: vec2f) -> vec4f {
|
|
142
|
+
return textureSampleLevel(_rzSimPrevTex, _rzSimSamp, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0);
|
|
143
|
+
}
|
|
144
|
+
` +
|
|
145
|
+
EFFECT_SCENE_API +
|
|
146
|
+
audioApi(0, 5) +
|
|
147
|
+
scoreApi(0, 7) +
|
|
148
|
+
// rzSim itself, so a kernel may read the grid it is writing — through the
|
|
149
|
+
// PREVIOUS frame's texture, which is the only version of it that exists
|
|
150
|
+
// while the current one is still being written.
|
|
151
|
+
/* wgsl */ `
|
|
152
|
+
fn rzSim(uv: vec2f) -> vec4f { return rzSimPrev(uv); }
|
|
153
|
+
` +
|
|
154
|
+
"\n// ── user effect (setEffect) ──\n" +
|
|
155
|
+
wgsl +
|
|
156
|
+
/* wgsl */ `
|
|
157
|
+
|
|
158
|
+
@compute @workgroup_size(8, 8)
|
|
159
|
+
fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
160
|
+
let n = u32(${size});
|
|
161
|
+
if (gid.x >= n || gid.y >= n) { return; }
|
|
162
|
+
let xy = vec2i(i32(gid.x), i32(gid.y));
|
|
163
|
+
// The texel CENTRE, so a kernel that reads its own uv back through rzSimPrev
|
|
164
|
+
// lands on itself rather than a quarter-texel off.
|
|
165
|
+
let uv = (vec2f(f32(gid.x), f32(gid.y)) + vec2f(0.5)) / su.size;
|
|
166
|
+
textureStore(_rzSimOut, xy, simStep(uv, textureLoad(_rzSimPrevTex, xy, 0), su.dt));
|
|
167
|
+
}
|
|
168
|
+
`);
|
|
169
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AAyBA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CAwUR"}
|
|
@@ -5,6 +5,7 @@ import { anchorAliasWgsl, ribbonSlotWgsl } from "../anchor-table";
|
|
|
5
5
|
import { midiApi } from "../midi-api";
|
|
6
6
|
import { CAST_API } from "../cast-api";
|
|
7
7
|
import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
|
|
8
|
+
import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract";
|
|
8
9
|
/**
|
|
9
10
|
* Sub-segments drawn between each pair of recorded samples.
|
|
10
11
|
*
|
|
@@ -74,7 +75,14 @@ struct CameraU {
|
|
|
74
75
|
}
|
|
75
76
|
struct TrailU {
|
|
76
77
|
time: f32,
|
|
77
|
-
|
|
78
|
+
// How many subjects the cast ACTUALLY holds this frame, not the four the
|
|
79
|
+
// layout has room for. The instance count is sized by this on the CPU and
|
|
80
|
+
// decoded by it here, and the two must use the same number or the flattened
|
|
81
|
+
// [ribbon][subject][segment] index lands on the wrong ribbon. It is a uniform
|
|
82
|
+
// rather than the RZ_SUBJECTS constant precisely because it changes when a
|
|
83
|
+
// model is added or removed, and recompiling every trail shader for that
|
|
84
|
+
// would be absurd.
|
|
85
|
+
subjects: f32,
|
|
78
86
|
_pad1: f32,
|
|
79
87
|
_pad2: f32,
|
|
80
88
|
}
|
|
@@ -195,11 +203,14 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
|
|
|
195
203
|
let rest = ii / subs;
|
|
196
204
|
let seg = k / SUB;
|
|
197
205
|
let sub = k % SUB;
|
|
198
|
-
|
|
206
|
+
// The LIVE subject count, floored at one so a cast that is momentarily empty
|
|
207
|
+
// divides by something. See TrailU.subjects.
|
|
208
|
+
let live = max(1u, u32(tu.subjects));
|
|
209
|
+
let subject = i32(rest % live);
|
|
199
210
|
// Instance index counts RIBBONS; the cast buffer is addressed by anchor slot.
|
|
200
211
|
// One ribbon per trailed anchor, so these are the same number only when every
|
|
201
212
|
// anchor is trailed — _rzRibbonSlot is what bridges them.
|
|
202
|
-
let ribbon = i32(rest /
|
|
213
|
+
let ribbon = i32(rest / live);
|
|
203
214
|
let slot = _rzRibbonSlot(ribbon);
|
|
204
215
|
|
|
205
216
|
let n = rzTrailCount(subject, slot);
|
|
@@ -293,9 +304,23 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
|
|
|
293
304
|
if (dot(perpA, perpB) < 0.0) { perpB = -perpB; }
|
|
294
305
|
let clipP = select(clipA, clipB, atEnd);
|
|
295
306
|
let perp = select(perpA, perpB, atEnd);
|
|
296
|
-
// trailWidth speaks
|
|
297
|
-
//
|
|
298
|
-
|
|
307
|
+
// trailWidth speaks WORLD UNITS — the model's units, not the screen's.
|
|
308
|
+
//
|
|
309
|
+
// It spoke pixels, like the fullscreen original's constants, and that made
|
|
310
|
+
// the ribbon a screen ornament: zoom out and it grew relative to the hand
|
|
311
|
+
// that drew it, zoom in and it thinned to a thread. A ribbon is a thing in
|
|
312
|
+
// the scene, so its width belongs in the scene's units and should foreshorten
|
|
313
|
+
// like everything else.
|
|
314
|
+
//
|
|
315
|
+
// The EXTRUSION stays in 2D pixel space untouched — every 3D-extrusion
|
|
316
|
+
// artefact this ribbon ever had (fold-over wedges, bowties, twisted quads) is
|
|
317
|
+
// recorded above, and none of it comes back. Only the width's SOURCE changes:
|
|
318
|
+
// a world length L perpendicular to view at depth w projects to
|
|
319
|
+
// L * proj[1][1] / w in NDC y, so half the frame height converts it to the
|
|
320
|
+
// pixels the 2D offset wants. Per vertex, so a ribbon receding along its own
|
|
321
|
+
// length tapers with distance exactly as geometry would.
|
|
322
|
+
let wWorld = max(0.0, trailWidth(u, age));
|
|
323
|
+
let wPx = wWorld * cam.proj[1][1] * (H * 0.5) / max(clipP.w, 0.01);
|
|
299
324
|
let ndc = clipP.xy / clipP.w + perp * (c.y * wPx) / toPx;
|
|
300
325
|
out.clip = vec4f(ndc * clipP.w, clipP.z, clipP.w);
|
|
301
326
|
out.slot = u32(slot);
|
|
@@ -315,7 +340,10 @@ struct TrailFSOut {
|
|
|
315
340
|
// The scene's aux target: (bloom mask, coverage). Writing mask 1 is what
|
|
316
341
|
// makes a ribbon BLOOM — it is light, and the gate is what says so.
|
|
317
342
|
@location(1) aux: vec4f,
|
|
318
|
-
|
|
343
|
+
// Declared whenever the pass carries the attachment, and padded rather than
|
|
344
|
+
// written: a ribbon is not a thing you would address by id, so its pipeline
|
|
345
|
+
// takes this target at writeMask 0. Declared anyway — see sceneFsOutWgsl.
|
|
346
|
+
${sceneIdFieldWgsl()}}
|
|
319
347
|
|
|
320
348
|
@fragment
|
|
321
349
|
fn fs(in: VSOut) -> TrailFSOut {
|
|
@@ -334,7 +362,7 @@ fn fs(in: VSOut) -> TrailFSOut {
|
|
|
334
362
|
// mapper is what keeps the sum from clipping.
|
|
335
363
|
o.color = vec4f(c.rgb, c.a);
|
|
336
364
|
o.aux = vec4f(${src.bloom ? "1.0" : "0.0"}, 1.0, 0.0, c.a);
|
|
337
|
-
return o;
|
|
365
|
+
${sceneIdPadWgsl("o")} return o;
|
|
338
366
|
}
|
|
339
367
|
`);
|
|
340
368
|
}
|
|
@@ -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
|
+
}
|