reze-engine 0.50.2 → 0.50.3
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 +57 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +107 -45
- package/dist/physics-debug.d.ts +30 -0
- package/dist/physics-debug.d.ts.map +1 -0
- package/dist/physics-debug.js +526 -0
- package/dist/shaders/materials/body.d.ts +2 -0
- package/dist/shaders/materials/body.d.ts.map +1 -0
- package/dist/shaders/materials/body.js +95 -0
- package/dist/shaders/materials/cloth_rough.d.ts +2 -0
- package/dist/shaders/materials/cloth_rough.d.ts.map +1 -0
- package/dist/shaders/materials/cloth_rough.js +69 -0
- package/dist/shaders/materials/cloth_smooth.d.ts +2 -0
- package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -0
- package/dist/shaders/materials/cloth_smooth.js +61 -0
- package/dist/shaders/materials/default.d.ts +2 -0
- package/dist/shaders/materials/default.d.ts.map +1 -0
- package/dist/shaders/materials/default.js +43 -0
- package/dist/shaders/materials/eye.d.ts +2 -0
- package/dist/shaders/materials/eye.d.ts.map +1 -0
- package/dist/shaders/materials/eye.js +60 -0
- package/dist/shaders/materials/face.d.ts +2 -0
- package/dist/shaders/materials/face.d.ts.map +1 -0
- package/dist/shaders/materials/face.js +95 -0
- package/dist/shaders/materials/hair.d.ts +2 -0
- package/dist/shaders/materials/hair.d.ts.map +1 -0
- package/dist/shaders/materials/hair.js +90 -0
- package/dist/shaders/materials/metal.d.ts +2 -0
- package/dist/shaders/materials/metal.d.ts.map +1 -0
- package/dist/shaders/materials/metal.js +77 -0
- package/dist/shaders/materials/mmd_classic.d.ts +2 -0
- package/dist/shaders/materials/mmd_classic.d.ts.map +1 -0
- package/dist/shaders/materials/mmd_classic.js +66 -0
- package/dist/shaders/materials/stockings.d.ts +2 -0
- package/dist/shaders/materials/stockings.d.ts.map +1 -0
- package/dist/shaders/materials/stockings.js +122 -0
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +12 -22
- package/dist/shaders/passes/physics-debug.d.ts +2 -0
- package/dist/shaders/passes/physics-debug.d.ts.map +1 -0
- package/dist/shaders/passes/physics-debug.js +69 -0
- package/package.json +2 -2
- package/src/engine.ts +113 -44
- package/src/shaders/passes/ground.ts +12 -22
- package/dist/physics/autofit.d.ts +0 -147
- package/dist/physics/autofit.d.ts.map +0 -1
- package/dist/physics/autofit.js +0 -501
- package/dist/shaders/passes/field-blit.d.ts +0 -26
- package/dist/shaders/passes/field-blit.d.ts.map +0 -1
- package/dist/shaders/passes/field-blit.js +0 -65
- package/dist/shaders/passes/ground-noise.d.ts +0 -7
- package/dist/shaders/passes/ground-noise.d.ts.map +0 -1
- package/dist/shaders/passes/ground-noise.js +0 -88
- package/dist/shaders/passes/sim.d.ts +0 -34
- package/dist/shaders/passes/sim.d.ts.map +0 -1
- package/dist/shaders/passes/sim.js +0 -169
- package/dist/shaders/score-api.d.ts +0 -10
- package/dist/shaders/score-api.d.ts.map +0 -1
- package/dist/shaders/score-api.js +0 -114
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
// Bakes the ground's frosted micro-texture into a seamless, mipmapped tile.
|
|
2
|
-
//
|
|
3
|
-
// The shader used to evaluate four octaves of value noise per pixel — sixteen
|
|
4
|
-
// hash rounds — and the ground covers the entire screen as soon as the camera
|
|
5
|
-
// tilts toward the horizon. This produces the same character of pattern once, at
|
|
6
|
-
// load, so the fragment shader pays one filtered fetch instead.
|
|
7
|
-
//
|
|
8
|
-
// SEAMLESS BY CONSTRUCTION. The original hash was not periodic, so a finite
|
|
9
|
-
// crop of it could never tile. Here every octave's lattice wraps at a whole
|
|
10
|
-
// number of cells across the tile, which makes the result repeat exactly at the
|
|
11
|
-
// edges. The pattern is therefore not pixel-identical to the analytic version —
|
|
12
|
-
// it is the same kind of noise, not the same noise. At the default strength of
|
|
13
|
-
// 0.05 it modulates brightness by ±2.5%, where the repeat is not findable by eye.
|
|
14
|
-
import { GROUND_NOISE_RES, GROUND_NOISE_TILE_WORLD } from "./ground";
|
|
15
|
-
/** Lattice cells across the tile at octave 0. The analytic version sampled fbm
|
|
16
|
-
* at worldXZ × 3, so a tile spanning 8 world units covers 24 base cells — the
|
|
17
|
-
* same spatial frequency the surface had before. */
|
|
18
|
-
const BASE_CELLS = GROUND_NOISE_TILE_WORLD * 3;
|
|
19
|
-
const OCTAVES = 4;
|
|
20
|
-
/** Deterministic lattice hash. Integer coordinates are wrapped by the caller,
|
|
21
|
-
* which is what makes the tile seamless. */
|
|
22
|
-
function hash(ix, iy) {
|
|
23
|
-
let h = Math.imul(ix, 374761393) + Math.imul(iy, 668265263);
|
|
24
|
-
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
|
25
|
-
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
26
|
-
}
|
|
27
|
-
/** Value noise on a lattice that repeats every `period` cells. */
|
|
28
|
-
function periodicValueNoise(x, y, period) {
|
|
29
|
-
const ix = Math.floor(x);
|
|
30
|
-
const iy = Math.floor(y);
|
|
31
|
-
const fx = x - ix;
|
|
32
|
-
const fy = y - iy;
|
|
33
|
-
// Smoothstep weights — same curve the WGSL used.
|
|
34
|
-
const ux = fx * fx * (3 - 2 * fx);
|
|
35
|
-
const uy = fy * fy * (3 - 2 * fy);
|
|
36
|
-
const x0 = ((ix % period) + period) % period;
|
|
37
|
-
const y0 = ((iy % period) + period) % period;
|
|
38
|
-
const x1 = (x0 + 1) % period;
|
|
39
|
-
const y1 = (y0 + 1) % period;
|
|
40
|
-
const a = hash(x0, y0);
|
|
41
|
-
const b = hash(x1, y0);
|
|
42
|
-
const c = hash(x0, y1);
|
|
43
|
-
const d = hash(x1, y1);
|
|
44
|
-
return (a + (b - a) * ux) * (1 - uy) + (c + (d - c) * ux) * uy;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* The tile as 8-bit luminance, plus its mip chain (box-filtered, which is exact
|
|
48
|
-
* for a repeating texture). Mips are the point as much as the speed: procedural
|
|
49
|
-
* noise has no mip chain, so at grazing angles it shimmered.
|
|
50
|
-
*/
|
|
51
|
-
export function buildGroundNoiseMips() {
|
|
52
|
-
const levels = [];
|
|
53
|
-
const res = GROUND_NOISE_RES;
|
|
54
|
-
const base = new Uint8Array(res * res);
|
|
55
|
-
for (let py = 0; py < res; py++) {
|
|
56
|
-
for (let px = 0; px < res; px++) {
|
|
57
|
-
// Texel centre, in units of octave-0 lattice cells.
|
|
58
|
-
const x = ((px + 0.5) / res) * BASE_CELLS;
|
|
59
|
-
const y = ((py + 0.5) / res) * BASE_CELLS;
|
|
60
|
-
let v = 0;
|
|
61
|
-
let amp = 0.5;
|
|
62
|
-
let freq = 1;
|
|
63
|
-
for (let o = 0; o < OCTAVES; o++) {
|
|
64
|
-
v += amp * periodicValueNoise(x * freq, y * freq, BASE_CELLS * freq);
|
|
65
|
-
freq *= 2;
|
|
66
|
-
amp *= 0.5;
|
|
67
|
-
}
|
|
68
|
-
base[py * res + px] = Math.max(0, Math.min(255, Math.round(v * 255)));
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
levels.push(base);
|
|
72
|
-
let src = base;
|
|
73
|
-
let size = res;
|
|
74
|
-
while (size > 1) {
|
|
75
|
-
const half = size >> 1;
|
|
76
|
-
const dst = new Uint8Array(half * half);
|
|
77
|
-
for (let y = 0; y < half; y++) {
|
|
78
|
-
for (let x = 0; x < half; x++) {
|
|
79
|
-
const s = y * 2 * size + x * 2;
|
|
80
|
-
dst[y * half + x] = (src[s] + src[s + 1] + src[s + size] + src[s + size + 1] + 2) >> 2;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
levels.push(dst);
|
|
84
|
-
src = dst;
|
|
85
|
-
size = half;
|
|
86
|
-
}
|
|
87
|
-
return levels;
|
|
88
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,169 +0,0 @@
|
|
|
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,10 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,114 +0,0 @@
|
|
|
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
|
-
}
|