reze-engine 0.61.7 → 0.62.1
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 +24 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +96 -7
- package/dist/shaders/directives.d.ts +10 -0
- package/dist/shaders/directives.d.ts.map +1 -1
- package/dist/shaders/directives.js +11 -0
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +2 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +2 -0
- package/dist/shaders/passes/grid.d.ts.map +1 -1
- package/dist/shaders/passes/grid.js +2 -0
- package/dist/shaders/passes/particles.d.ts +7 -0
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +10 -0
- package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
- package/dist/shaders/passes/scene-contract.js +6 -2
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +2 -1
- package/dist/shaders/texture-api.d.ts +9 -0
- package/dist/shaders/texture-api.d.ts.map +1 -0
- package/dist/shaders/texture-api.js +54 -0
- package/package.json +1 -1
- package/src/engine.ts +110 -3
- package/src/shaders/directives.ts +21 -0
- package/src/shaders/lights.ts +2 -1
- package/src/shaders/passes/composite.ts +2 -0
- package/src/shaders/passes/grid.ts +2 -0
- package/src/shaders/passes/particles.ts +13 -0
- package/src/shaders/passes/scene-contract.ts +6 -2
- package/src/shaders/passes/trails.ts +2 -1
- package/src/shaders/texture-api.ts +56 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAqC7C,eAAO,MAAM,QAAQ,OAAO,CAAA;AAC5B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,gDAAgD;AAChD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6B1F;AAgBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,SAAK,GAAG,MAAM,CAuFpG"}
|
|
@@ -9,6 +9,7 @@ import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api";
|
|
|
9
9
|
import { idApi } from "../id-api";
|
|
10
10
|
import { sceneLightApi } from "../scene-light-api";
|
|
11
11
|
import { pointsApi } from "../points-api";
|
|
12
|
+
import { textureApi } from "../texture-api";
|
|
12
13
|
// A persistent grid an effect can step and read: the one thing an effect could
|
|
13
14
|
// not have before, which is MEMORY.
|
|
14
15
|
//
|
|
@@ -164,6 +165,7 @@ fn rzGrid(uv: vec2f) -> vec4f { return rzGridPrev(uv); }
|
|
|
164
165
|
idApi(false, 0, 0) +
|
|
165
166
|
sceneLightApi(false, 0, 0) +
|
|
166
167
|
pointsApi(false) +
|
|
168
|
+
textureApi(0) +
|
|
167
169
|
// Inlined rather than imported from cast-distance, and NOT for tidiness.
|
|
168
170
|
//
|
|
169
171
|
// This module and composite.ts import each other — grid takes
|
|
@@ -50,6 +50,9 @@ type ParticleSource = {
|
|
|
50
50
|
* bending where a foot pressed is the whole case: the bend has to persist and
|
|
51
51
|
* recover, which is memory, and memory is what the grid is for. */
|
|
52
52
|
gridSize: number;
|
|
53
|
+
/** `#textures N`: pictures the shading samples (rzTexture), bound only when
|
|
54
|
+
* N > 0 — see texture-api.ts. Omitted = 0. */
|
|
55
|
+
textures?: number;
|
|
53
56
|
};
|
|
54
57
|
/** Where the shading stage's five scene-light bindings start — see scene-light-api.ts. */
|
|
55
58
|
export declare const PARTICLE_LIGHT_BINDING = 10;
|
|
@@ -60,6 +63,10 @@ export declare const PARTICLE_POINTS_BINDING = 15;
|
|
|
60
63
|
* (6, count, 0, 0), so the draw reads at byte 16. */
|
|
61
64
|
export declare const PARTICLE_INDIRECT_BINDING = 16;
|
|
62
65
|
export declare const PARTICLE_INDIRECT_BYTES = 32;
|
|
66
|
+
/** `#textures`: the pictures at 17..20 and their one sampler at 21, render
|
|
67
|
+
* stage only — see texture-api.ts. */
|
|
68
|
+
export declare const PARTICLE_TEXTURE_BINDING = 17;
|
|
69
|
+
export declare const PARTICLE_TEXTURE_SAMPLER_BINDING = 21;
|
|
63
70
|
export declare const PARTICLE_INDIRECT_DRAW_OFFSET = 16;
|
|
64
71
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
65
72
|
export declare const PARTICLE_STRIDE = 48;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AAgCA,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb;;mFAE+E;IAC/E,UAAU,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAkCD,qDAAqD;AACrD;0EAC0E;AAC1E,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAA;AAEpD,KAAK,cAAc,GAAG;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;IACd;;+BAE2B;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;iEAK6D;IAC7D,KAAK,EAAE,OAAO,CAAA;IACd;;;;;;;kFAO8E;IAC9E,IAAI,EAAE,OAAO,CAAA;IACb;;;wEAGoE;IACpE,QAAQ,EAAE,MAAM,CAAA;IAChB;mDAC+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,0FAA0F;AAC1F,eAAO,MAAM,sBAAsB,KAAK,CAAA;AACxC,iFAAiF;AACjF,eAAO,MAAM,uBAAuB,KAAK,CAAA;AACzC;;sDAEsD;AACtD,eAAO,MAAM,yBAAyB,KAAK,CAAA;AAC3C,eAAO,MAAM,uBAAuB,KAAK,CAAA;AACzC;uCACuC;AACvC,eAAO,MAAM,wBAAwB,KAAK,CAAA;AAC1C,eAAO,MAAM,gCAAgC,KAAK,CAAA;AAClD,eAAO,MAAM,6BAA6B,KAAK,CAAA;AAE/C,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAwDjC;;2EAE2E;AAC3E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAQlI;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6ExF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6LvF"}
|
|
@@ -12,6 +12,7 @@ import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract";
|
|
|
12
12
|
import { idApi } from "../id-api";
|
|
13
13
|
import { sceneLightApi } from "../scene-light-api";
|
|
14
14
|
import { pointsApi } from "../points-api";
|
|
15
|
+
import { textureApi } from "../texture-api";
|
|
15
16
|
/**
|
|
16
17
|
* The trail accessors, in the PARTICLE module.
|
|
17
18
|
*
|
|
@@ -51,6 +52,10 @@ export const PARTICLE_POINTS_BINDING = 15;
|
|
|
51
52
|
* (6, count, 0, 0), so the draw reads at byte 16. */
|
|
52
53
|
export const PARTICLE_INDIRECT_BINDING = 16;
|
|
53
54
|
export const PARTICLE_INDIRECT_BYTES = 32;
|
|
55
|
+
/** `#textures`: the pictures at 17..20 and their one sampler at 21, render
|
|
56
|
+
* stage only — see texture-api.ts. */
|
|
57
|
+
export const PARTICLE_TEXTURE_BINDING = 17;
|
|
58
|
+
export const PARTICLE_TEXTURE_SAMPLER_BINDING = 21;
|
|
54
59
|
export const PARTICLE_INDIRECT_DRAW_OFFSET = 16;
|
|
55
60
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
56
61
|
export const PARTICLE_STRIDE = 48;
|
|
@@ -151,6 +156,9 @@ ${src.paramsDecl}
|
|
|
151
156
|
idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) +
|
|
152
157
|
// The named points, for real: particleInit is where a flame is put on a wick.
|
|
153
158
|
pointsApi(true, 0, PARTICLE_POINTS_BINDING) +
|
|
159
|
+
// Stubs: textureSample is fragment-only, and a spawn rule has no use for a
|
|
160
|
+
// picture. The names resolve so the file compiles here.
|
|
161
|
+
textureApi(0) +
|
|
154
162
|
(src.live
|
|
155
163
|
? `@group(0) @binding(${PARTICLE_INDIRECT_BINDING}) var<storage, read_write> _rzIndirect: array<u32, 8>;\n`
|
|
156
164
|
: "") +
|
|
@@ -247,6 +255,8 @@ ${src.paramsDecl}
|
|
|
247
255
|
// run by the time this draws, inside the scene pass. See scene-light-api.ts.
|
|
248
256
|
sceneLightApi(true, 0, PARTICLE_LIGHT_BINDING) +
|
|
249
257
|
pointsApi(true, 0, PARTICLE_POINTS_BINDING) +
|
|
258
|
+
// The host's pictures, for real — see texture-api.ts.
|
|
259
|
+
textureApi(src.textures ?? 0, 0, PARTICLE_TEXTURE_BINDING, PARTICLE_TEXTURE_SAMPLER_BINDING) +
|
|
250
260
|
"\n// ── user effect ──\n" +
|
|
251
261
|
src.wgsl +
|
|
252
262
|
/* wgsl */ `
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scene-contract.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/scene-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;uDAEuD;AACvD,MAAM,MAAM,YAAY,GAAG;IACzB,+CAA+C;IAC/C,GAAG,EAAE,gBAAgB,CAAA;IACrB,iEAAiE;IACjE,GAAG,EAAE,gBAAgB,CAAA;CACtB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,gBAA6B,CAAA;AAkB3D,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAE3C;AAED,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED;;;;;;GAMG;AACH,KAAK,gBAAgB;AACnB,2DAA2D;AACzD,UAAU;AACZ;;+CAE+C;GAC7C,QAAQ;AACV,uEAAuE;GACrE,SAAS;AACX;;;;6EAI6E;GAC3E,QAAQ;AACV;;;;;;yEAMyE;GACvE,mBAAmB;AACrB,iEAAiE;GAC/D,UAAU;AACZ;;;mDAGmD;GACjD,mBAAmB;AACrB;;;oCAGoC;GAClC,OAAO;AACT;;;kDAGkD;GAChD,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"scene-contract.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/scene-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;uDAEuD;AACvD,MAAM,MAAM,YAAY,GAAG;IACzB,+CAA+C;IAC/C,GAAG,EAAE,gBAAgB,CAAA;IACrB,iEAAiE;IACjE,GAAG,EAAE,gBAAgB,CAAA;CACtB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,gBAA6B,CAAA;AAkB3D,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAE3C;AAED,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED;;;;;;GAMG;AACH,KAAK,gBAAgB;AACnB,2DAA2D;AACzD,UAAU;AACZ;;+CAE+C;GAC7C,QAAQ;AACV,uEAAuE;GACrE,SAAS;AACX;;;;6EAI6E;GAC3E,QAAQ;AACV;;;;;;yEAMyE;GACvE,mBAAmB;AACrB,iEAAiE;GAC/D,UAAU;AACZ;;;mDAGmD;GACjD,mBAAmB;AACrB;;;oCAGoC;GAClC,OAAO;AACT;;;kDAGkD;GAChD,eAAe,CAAA;AAyFnB;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,gBAAgB,EAAE,OAAO,EAAE,YAAY,GAAG,mBAAmB,EAAE,CAoBhG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAE3E;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAAC,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAmB7E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD"}
|
|
@@ -72,7 +72,7 @@ const ADD_BOTH = {
|
|
|
72
72
|
* OVER for something that arrives ALREADY premultiplied: take the source whole
|
|
73
73
|
* and let it displace the destination by its own coverage.
|
|
74
74
|
*
|
|
75
|
-
* The ground
|
|
75
|
+
* The ground, the mirror and particles need it. The other classes write a
|
|
76
76
|
* straight colour and an alpha, and the src-alpha factor premultiplies it once
|
|
77
77
|
* on the way in. The ground cannot: its coverage is the SUM of a lit surface
|
|
78
78
|
* and a colourless shadow-catcher layer, so it has to weight its own colour by
|
|
@@ -125,7 +125,11 @@ const BLENDS = {
|
|
|
125
125
|
// everything else, and coverage is what the blend applies.
|
|
126
126
|
ground: [PREMULTIPLIED_OVER, ALPHA_OVER],
|
|
127
127
|
outline: [ALPHA_OVER, ALPHA_OVER],
|
|
128
|
-
|
|
128
|
+
// PREMULTIPLIED colour: the particle fragment weights its colour by its own
|
|
129
|
+
// alpha before it writes (passes/particles.ts), so the src-alpha blend would
|
|
130
|
+
// weight it a second time — a splash at alpha 0.1 arrived at a hundredth of
|
|
131
|
+
// its colour instead of a tenth. The aux mask is written straight, as ever.
|
|
132
|
+
particle: [PREMULTIPLIED_OVER, ALPHA_OVER],
|
|
129
133
|
"particle-additive": [ADD_KEEP_ALPHA, ADD_BOTH],
|
|
130
134
|
trail: [ADD_PREMULTIPLIED, ALPHA_OVER],
|
|
131
135
|
};
|
|
@@ -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":"AAgCA,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,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;IACd;sFACkF;IAClF,QAAQ,EAAE,MAAM,CAAA;CACjB,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,CA6ZR"}
|
|
@@ -12,6 +12,7 @@ import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract";
|
|
|
12
12
|
import { idApi } from "../id-api";
|
|
13
13
|
import { sceneLightApi } from "../scene-light-api";
|
|
14
14
|
import { pointsApi } from "../points-api";
|
|
15
|
+
import { textureApi } from "../texture-api";
|
|
15
16
|
/**
|
|
16
17
|
* Sub-segments drawn between each pair of recorded samples.
|
|
17
18
|
*
|
|
@@ -249,7 +250,7 @@ fn rzTurnRadius(a: vec3f, b: vec3f, c: vec3f) -> f32 {
|
|
|
249
250
|
gridReadApi(0, 8, 9, src.gridSize) +
|
|
250
251
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
251
252
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
252
|
-
idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) + pointsApi(false) +
|
|
253
|
+
idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) + pointsApi(false) + textureApi(0) +
|
|
253
254
|
"\n// ── user effect ──\n" +
|
|
254
255
|
src.wgsl +
|
|
255
256
|
/* wgsl */ `
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** How many pictures one effect can read. */
|
|
2
|
+
export declare const MAX_EFFECT_TEXTURES = 4;
|
|
3
|
+
/**
|
|
4
|
+
* rzTexture / rzTextureLod. `count` is the effect's `#textures` (0 = stubs);
|
|
5
|
+
* the textures take bindings first..first+count-1 in `group` and the one
|
|
6
|
+
* sampler `sampler`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function textureApi(count: number, group?: number, first?: number, sampler?: number): string;
|
|
9
|
+
//# sourceMappingURL=texture-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"texture-api.d.ts","sourceRoot":"","sources":["../../src/shaders/texture-api.ts"],"names":[],"mappings":"AAkBA,6CAA6C;AAC7C,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAI,EAAE,KAAK,SAAI,EAAE,OAAO,SAAI,GAAG,MAAM,CA6BnF"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Pictures an effect is handed by its host — `#textures N`.
|
|
2
|
+
//
|
|
3
|
+
// #textures 2
|
|
4
|
+
//
|
|
5
|
+
// gives the PARTICLE SHADING rzTexture(i, uv) and rzTextureLod(i, uv, lod) over
|
|
6
|
+
// N images the host passes with the install (setEffects' `textures`, slot i =
|
|
7
|
+
// picture i). uv (0,0) is the image's top-left row, the style graph's
|
|
8
|
+
// tex_image convention, and it repeats. A slot the host left empty reads white.
|
|
9
|
+
//
|
|
10
|
+
// WHY: a game's splash is a picture on a card, and an effect that may only draw
|
|
11
|
+
// procedurally can only ever guess at it — shape, coverage, veins, each a guess
|
|
12
|
+
// to be corrected. A converted stage carries the game's own textures, and this
|
|
13
|
+
// is how its particles draw them.
|
|
14
|
+
//
|
|
15
|
+
// Fragment-only, because textureSample is: the compute stage and every other
|
|
16
|
+
// module the author's file is spliced into get STUBS returning zero, so one
|
|
17
|
+
// effect file compiles in every mount it has.
|
|
18
|
+
/** How many pictures one effect can read. */
|
|
19
|
+
export const MAX_EFFECT_TEXTURES = 4;
|
|
20
|
+
/**
|
|
21
|
+
* rzTexture / rzTextureLod. `count` is the effect's `#textures` (0 = stubs);
|
|
22
|
+
* the textures take bindings first..first+count-1 in `group` and the one
|
|
23
|
+
* sampler `sampler`.
|
|
24
|
+
*/
|
|
25
|
+
export function textureApi(count, group = 0, first = 0, sampler = 0) {
|
|
26
|
+
if (count <= 0) {
|
|
27
|
+
return /* wgsl */ `
|
|
28
|
+
fn rzTexture(i: u32, uv: vec2f) -> vec4f { return vec4f(0.0); }
|
|
29
|
+
fn rzTextureLod(i: u32, uv: vec2f, lod: f32) -> vec4f { return vec4f(0.0); }
|
|
30
|
+
`;
|
|
31
|
+
}
|
|
32
|
+
const n = Math.min(count, MAX_EFFECT_TEXTURES);
|
|
33
|
+
let decl = `@group(${group}) @binding(${sampler}) var _rzTexSampler: sampler;\n`;
|
|
34
|
+
for (let k = 0; k < n; k++)
|
|
35
|
+
decl += `@group(${group}) @binding(${first + k}) var _rzTex${k}: texture_2d<f32>;\n`;
|
|
36
|
+
// Every slot is sampled unconditionally and the one asked for is selected:
|
|
37
|
+
// textureSample must be reached in uniform control flow, and a switch on a
|
|
38
|
+
// per-fragment index is not.
|
|
39
|
+
const pick = (call) => {
|
|
40
|
+
let body = " var c = vec4f(0.0);\n";
|
|
41
|
+
for (let k = 0; k < n; k++)
|
|
42
|
+
body += ` let s${k} = ${call(k)};\n if (i == ${k}u) { c = s${k}; }\n`;
|
|
43
|
+
return body + " return c;\n";
|
|
44
|
+
};
|
|
45
|
+
return (decl +
|
|
46
|
+
/* wgsl */ `
|
|
47
|
+
/** Picture i of the effect's #textures, at uv ((0,0) top-left, repeating). */
|
|
48
|
+
fn rzTexture(i: u32, uv: vec2f) -> vec4f {
|
|
49
|
+
${pick((k) => `textureSample(_rzTex${k}, _rzTexSampler, uv)`)}}
|
|
50
|
+
/** The same at an explicit mip level — usable where derivatives are not. */
|
|
51
|
+
fn rzTextureLod(i: u32, uv: vec2f, lod: f32) -> vec4f {
|
|
52
|
+
${pick((k) => `textureSampleLevel(_rzTex${k}, _rzTexSampler, uv, lod)`)}}
|
|
53
|
+
`);
|
|
54
|
+
}
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -118,6 +118,8 @@ import {
|
|
|
118
118
|
buildParticleRenderShader,
|
|
119
119
|
PARTICLE_LIGHT_BINDING,
|
|
120
120
|
PARTICLE_POINTS_BINDING,
|
|
121
|
+
PARTICLE_TEXTURE_BINDING,
|
|
122
|
+
PARTICLE_TEXTURE_SAMPLER_BINDING,
|
|
121
123
|
particleEntryPoints,
|
|
122
124
|
PARTICLE_INDIRECT_BINDING,
|
|
123
125
|
PARTICLE_INDIRECT_BYTES,
|
|
@@ -488,6 +490,17 @@ export type EffectParamValue = number | { x: number; y: number; z: number }
|
|
|
488
490
|
/** A vector by shape rather than by class — Vec3 satisfies it, and so does a
|
|
489
491
|
* JSON object out of a scene document or a literal typed into a console. */
|
|
490
492
|
export type XYZ = { x: number; y: number; z: number }
|
|
493
|
+
/**
|
|
494
|
+
* One picture an effect's `#textures` reads, as the host holds it — slot i of
|
|
495
|
+
* the install's `textures` is rzTexture(i, uv). `srgb` for a colour picture
|
|
496
|
+
* (decoded to linear on sample), false for data. Null, or a slot past the end,
|
|
497
|
+
* reads white. The engine never fetches: images arrive decoded, as models do.
|
|
498
|
+
*/
|
|
499
|
+
export type EffectTextureInput = {
|
|
500
|
+
source: ImageBitmap | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | ImageData
|
|
501
|
+
srgb: boolean
|
|
502
|
+
} | null
|
|
503
|
+
|
|
491
504
|
export type EffectResult = {
|
|
492
505
|
ok: boolean
|
|
493
506
|
/** Compile/validation errors, line:col relative to the USER's WGSL. Also
|
|
@@ -1513,6 +1526,9 @@ interface EffectParticles {
|
|
|
1513
1526
|
* indirect arguments it writes them to. Null when the effect declared none —
|
|
1514
1527
|
* the whole pool is then stepped and drawn. */
|
|
1515
1528
|
live: { pipeline: GPUComputePipeline; indirect: GPUBuffer; binds: [GPUBindGroup, GPUBindGroup] } | null
|
|
1529
|
+
/** `#textures`: the pictures the host handed over, uploaded for this effect
|
|
1530
|
+
* alone and destroyed with it. Empty when it declared none. */
|
|
1531
|
+
textures: GPUTexture[]
|
|
1516
1532
|
}
|
|
1517
1533
|
|
|
1518
1534
|
/**
|
|
@@ -4780,6 +4796,8 @@ export class Engine {
|
|
|
4780
4796
|
anchors: { bone: string; trail: boolean }[],
|
|
4781
4797
|
/** Its row of that table: local slot → scene slot. */
|
|
4782
4798
|
alias: number[],
|
|
4799
|
+
/** The pictures for `#textures`, as the host passed them. */
|
|
4800
|
+
textures?: EffectTextureInput[],
|
|
4783
4801
|
): Promise<{ ok: true; instance: EffectInstance; warnings: string[] } | EffectResult> {
|
|
4784
4802
|
const noMounts = { background: false, foreground: false }
|
|
4785
4803
|
if (!this.device) return { ok: false, diagnostics: ["setEffect requires init() to have run"], mounts: noMounts, params: [], duration: 0, readsCast: false }
|
|
@@ -5100,6 +5118,7 @@ export class Engine {
|
|
|
5100
5118
|
particles?.uniform.destroy()
|
|
5101
5119
|
particles?.points?.buffer.destroy()
|
|
5102
5120
|
particles?.live?.indirect.destroy()
|
|
5121
|
+
for (const t of particles?.textures ?? []) t.destroy()
|
|
5103
5122
|
grid?.textures[0].destroy()
|
|
5104
5123
|
grid?.textures[1].destroy()
|
|
5105
5124
|
grid?.uniform.destroy()
|
|
@@ -5115,7 +5134,7 @@ export class Engine {
|
|
|
5115
5134
|
grid = built.state
|
|
5116
5135
|
}
|
|
5117
5136
|
if (wantsParticles) {
|
|
5118
|
-
const built = await this.buildParticles(wgsl, d, anchors, alias, paramsFor, grid)
|
|
5137
|
+
const built = await this.buildParticles(wgsl, d, anchors, alias, paramsFor, grid, textures)
|
|
5119
5138
|
if (!built.ok) return abandon(built.diagnostics)
|
|
5120
5139
|
particles = built.state
|
|
5121
5140
|
}
|
|
@@ -5276,6 +5295,9 @@ export class Engine {
|
|
|
5276
5295
|
* aimed at one dancer should not spend its first frame on all four,
|
|
5277
5296
|
* which on a ribbon or a sigil reads as a flash. */
|
|
5278
5297
|
subjects?: readonly string[] | null
|
|
5298
|
+
/** `#textures`: slot i is the effect's rzTexture(i, uv). See
|
|
5299
|
+
* EffectTextureInput. Ignored by an effect that declares none. */
|
|
5300
|
+
textures?: EffectTextureInput[]
|
|
5279
5301
|
}[]
|
|
5280
5302
|
| null,
|
|
5281
5303
|
): Promise<EffectResult[]> {
|
|
@@ -5329,6 +5351,7 @@ export class Engine {
|
|
|
5329
5351
|
requested[i].params,
|
|
5330
5352
|
perEffectAnchors[i],
|
|
5331
5353
|
table.alias[i],
|
|
5354
|
+
requested[i].textures,
|
|
5332
5355
|
)
|
|
5333
5356
|
if (!("instance" in built)) {
|
|
5334
5357
|
// Contained: this one is out, the others carry on.
|
|
@@ -5444,13 +5467,13 @@ export class Engine {
|
|
|
5444
5467
|
* Install ONE effect — the singleton API, kept because most scenes are one
|
|
5445
5468
|
* effect and every existing caller uses it. A one-element setEffects.
|
|
5446
5469
|
*/
|
|
5447
|
-
async setEffect(wgsl: string | null, params?: Record<string, EffectParamValue
|
|
5470
|
+
async setEffect(wgsl: string | null, params?: Record<string, EffectParamValue>, textures?: EffectTextureInput[]): Promise<EffectResult> {
|
|
5448
5471
|
const noMounts = { background: false, foreground: false }
|
|
5449
5472
|
if (wgsl === null) {
|
|
5450
5473
|
await this.setEffects(null)
|
|
5451
5474
|
return { ok: true, diagnostics: [], mounts: noMounts, params: [], duration: 0, readsCast: false }
|
|
5452
5475
|
}
|
|
5453
|
-
const [result] = await this.setEffects([{ wgsl, params }])
|
|
5476
|
+
const [result] = await this.setEffects([{ wgsl, params, textures }])
|
|
5454
5477
|
return result ?? { ok: false, diagnostics: ["effect failed to install"], mounts: noMounts, params: [], duration: 0, readsCast: false }
|
|
5455
5478
|
}
|
|
5456
5479
|
|
|
@@ -5471,6 +5494,8 @@ export class Engine {
|
|
|
5471
5494
|
/** This effect's own grid, already built, or null when it declared none.
|
|
5472
5495
|
* Read-only here: the pool samples it, the grid pass owns it. */
|
|
5473
5496
|
grid: EffectGrid | null,
|
|
5497
|
+
/** `#textures`: the host's pictures, slot for slot. */
|
|
5498
|
+
textureInputs?: EffectTextureInput[],
|
|
5474
5499
|
): Promise<{ ok: true; state: EffectParticles } | { ok: false; diagnostics: string[] }> {
|
|
5475
5500
|
// No pragma means "some": an author who wrote the trio clearly wants
|
|
5476
5501
|
// particles, and failing over a missing comment would be pedantry.
|
|
@@ -5484,8 +5509,15 @@ export class Engine {
|
|
|
5484
5509
|
gridSize: grid?.size ?? 0,
|
|
5485
5510
|
cover: particleEntryPoints(wgsl).cover,
|
|
5486
5511
|
live: particleEntryPoints(wgsl).count,
|
|
5512
|
+
textures: d.textures,
|
|
5487
5513
|
}
|
|
5488
5514
|
const prepass = src.blend === "cutout" && src.cover
|
|
5515
|
+
// `#textures`: uploaded now, each with its mips, for this effect alone. A
|
|
5516
|
+
// slot the host left empty is null here and binds the white fallback.
|
|
5517
|
+
const textures = this.uploadEffectTextures(textureInputs, d.textures)
|
|
5518
|
+
const dropTextures = () => {
|
|
5519
|
+
for (const t of textures) t?.destroy()
|
|
5520
|
+
}
|
|
5489
5521
|
const points = d.points
|
|
5490
5522
|
? {
|
|
5491
5523
|
prefix: d.points,
|
|
@@ -5525,11 +5557,13 @@ export class Engine {
|
|
|
5525
5557
|
const computeModule = await compile(buildParticleComputeShader(src, cast), "particle compute")
|
|
5526
5558
|
if (Array.isArray(computeModule)) {
|
|
5527
5559
|
points?.buffer.destroy()
|
|
5560
|
+
dropTextures()
|
|
5528
5561
|
return { ok: false, diagnostics: computeModule }
|
|
5529
5562
|
}
|
|
5530
5563
|
const renderModule = await compile(buildParticleRenderShader(src, cast), "particle render")
|
|
5531
5564
|
if (Array.isArray(renderModule)) {
|
|
5532
5565
|
points?.buffer.destroy()
|
|
5566
|
+
dropTextures()
|
|
5533
5567
|
return { ok: false, diagnostics: renderModule }
|
|
5534
5568
|
}
|
|
5535
5569
|
|
|
@@ -5609,6 +5643,18 @@ export class Engine {
|
|
|
5609
5643
|
...(withIndirect
|
|
5610
5644
|
? [{ binding: PARTICLE_INDIRECT_BINDING, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" as const } }]
|
|
5611
5645
|
: []),
|
|
5646
|
+
// `#textures`, the shading stage only and only when declared — an
|
|
5647
|
+
// effect without them keeps exactly the layout it always had.
|
|
5648
|
+
...(shadow && textures.length
|
|
5649
|
+
? [
|
|
5650
|
+
...textures.map((_, k) => ({
|
|
5651
|
+
binding: PARTICLE_TEXTURE_BINDING + k,
|
|
5652
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
5653
|
+
texture: { sampleType: "float" as const },
|
|
5654
|
+
})),
|
|
5655
|
+
{ binding: PARTICLE_TEXTURE_SAMPLER_BINDING, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" as const } },
|
|
5656
|
+
]
|
|
5657
|
+
: []),
|
|
5612
5658
|
],
|
|
5613
5659
|
})
|
|
5614
5660
|
/** One per GRID PARITY, for the reason rebuildFieldBindGroup gives: the grid
|
|
@@ -5641,6 +5687,15 @@ export class Engine {
|
|
|
5641
5687
|
]
|
|
5642
5688
|
: []),
|
|
5643
5689
|
...(withIndirect && indirect ? [{ binding: PARTICLE_INDIRECT_BINDING, resource: { buffer: indirect } }] : []),
|
|
5690
|
+
...(shadow && textures.length
|
|
5691
|
+
? [
|
|
5692
|
+
...textures.map((t, k) => ({
|
|
5693
|
+
binding: PARTICLE_TEXTURE_BINDING + k,
|
|
5694
|
+
resource: (t ?? this.fallbackMaterialTexture).createView(),
|
|
5695
|
+
})),
|
|
5696
|
+
{ binding: PARTICLE_TEXTURE_SAMPLER_BINDING, resource: this.effectTextureSampler() },
|
|
5697
|
+
]
|
|
5698
|
+
: []),
|
|
5644
5699
|
],
|
|
5645
5700
|
}),
|
|
5646
5701
|
) as [GPUBindGroup, GPUBindGroup]
|
|
@@ -5723,6 +5778,7 @@ export class Engine {
|
|
|
5723
5778
|
uniform.destroy()
|
|
5724
5779
|
points?.buffer.destroy()
|
|
5725
5780
|
indirect?.destroy()
|
|
5781
|
+
dropTextures()
|
|
5726
5782
|
return { ok: false, diagnostics: [scoped.message] }
|
|
5727
5783
|
}
|
|
5728
5784
|
return {
|
|
@@ -5752,6 +5808,7 @@ export class Engine {
|
|
|
5752
5808
|
}),
|
|
5753
5809
|
points,
|
|
5754
5810
|
live,
|
|
5811
|
+
textures: textures.filter((t): t is GPUTexture => t !== null),
|
|
5755
5812
|
},
|
|
5756
5813
|
}
|
|
5757
5814
|
} catch (e) {
|
|
@@ -5760,6 +5817,7 @@ export class Engine {
|
|
|
5760
5817
|
uniform.destroy()
|
|
5761
5818
|
points?.buffer.destroy()
|
|
5762
5819
|
indirect?.destroy()
|
|
5820
|
+
dropTextures()
|
|
5763
5821
|
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)] }
|
|
5764
5822
|
}
|
|
5765
5823
|
}
|
|
@@ -6528,6 +6586,7 @@ export class Engine {
|
|
|
6528
6586
|
e.particles?.uniform.destroy()
|
|
6529
6587
|
e.particles?.points?.buffer.destroy()
|
|
6530
6588
|
e.particles?.live?.indirect.destroy()
|
|
6589
|
+
for (const t of e.particles?.textures ?? []) t.destroy()
|
|
6531
6590
|
e.particles = null
|
|
6532
6591
|
}
|
|
6533
6592
|
}
|
|
@@ -12396,6 +12455,54 @@ export class Engine {
|
|
|
12396
12455
|
for (const set of Object.values(install.imagesByMaterial ?? {})) for (const tex of set) tex?.destroy()
|
|
12397
12456
|
}
|
|
12398
12457
|
|
|
12458
|
+
/**
|
|
12459
|
+
* An effect's `#textures`, uploaded: `count` slots, each with a full mip chain
|
|
12460
|
+
* (a splash card seen from across a stage is a few pixels, and unmipped it
|
|
12461
|
+
* sparkles). Slot k is null where the host gave nothing — the bind group then
|
|
12462
|
+
* reads the white fallback.
|
|
12463
|
+
*/
|
|
12464
|
+
private uploadEffectTextures(inputs: EffectTextureInput[] | undefined, count: number): (GPUTexture | null)[] {
|
|
12465
|
+
const out: (GPUTexture | null)[] = []
|
|
12466
|
+
for (let k = 0; k < count; k++) {
|
|
12467
|
+
const entry = inputs?.[k]
|
|
12468
|
+
if (!entry) {
|
|
12469
|
+
out.push(null)
|
|
12470
|
+
continue
|
|
12471
|
+
}
|
|
12472
|
+
const src = entry.source
|
|
12473
|
+
const width = Math.max(1, "naturalWidth" in src ? src.naturalWidth : src.width)
|
|
12474
|
+
const height = Math.max(1, "naturalHeight" in src ? src.naturalHeight : src.height)
|
|
12475
|
+
const mipLevelCount = Math.floor(Math.log2(Math.max(width, height))) + 1
|
|
12476
|
+
const tex = this.device.createTexture({
|
|
12477
|
+
label: `effect texture ${k}`,
|
|
12478
|
+
size: [width, height],
|
|
12479
|
+
mipLevelCount,
|
|
12480
|
+
format: entry.srgb ? "rgba8unorm-srgb" : "rgba8unorm",
|
|
12481
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
12482
|
+
})
|
|
12483
|
+
this.device.queue.copyExternalImageToTexture({ source: src }, { texture: tex }, [width, height])
|
|
12484
|
+
if (mipLevelCount > 1) this.generateMipmaps(tex, mipLevelCount)
|
|
12485
|
+
out.push(tex)
|
|
12486
|
+
}
|
|
12487
|
+
return out
|
|
12488
|
+
}
|
|
12489
|
+
|
|
12490
|
+
private effectTextureSamplerCache: GPUSampler | null = null
|
|
12491
|
+
/** Linear, mipmapped, repeating — one for every effect's pictures. */
|
|
12492
|
+
private effectTextureSampler(): GPUSampler {
|
|
12493
|
+
if (!this.effectTextureSamplerCache) {
|
|
12494
|
+
this.effectTextureSamplerCache = this.device.createSampler({
|
|
12495
|
+
label: "effect texture sampler",
|
|
12496
|
+
magFilter: "linear",
|
|
12497
|
+
minFilter: "linear",
|
|
12498
|
+
mipmapFilter: "linear",
|
|
12499
|
+
addressModeU: "repeat",
|
|
12500
|
+
addressModeV: "repeat",
|
|
12501
|
+
})
|
|
12502
|
+
}
|
|
12503
|
+
return this.effectTextureSamplerCache
|
|
12504
|
+
}
|
|
12505
|
+
|
|
12399
12506
|
/** Upload a group's image maps. Sources are decoded images the host already
|
|
12400
12507
|
* holds; the engine never fetches, matching how models and motions arrive. */
|
|
12401
12508
|
private uploadGroupImages(group: StyleGroup, slots = group.images): (GPUTexture | null)[] | undefined {
|
|
@@ -60,6 +60,16 @@ export type EffectDirectives = {
|
|
|
60
60
|
/** `#points <prefix>`: every bone on every model whose name starts with it,
|
|
61
61
|
* as rzPoint(i) in the particle stages. Null when the file declares none. */
|
|
62
62
|
points: string | null
|
|
63
|
+
/**
|
|
64
|
+
* `#textures N` (1-4): the particle shading reads N pictures the HOST hands
|
|
65
|
+
* over with the install (setEffects' `textures`), as rzTexture(i, uv) — uv
|
|
66
|
+
* (0,0) at the image's top-left, repeating. A game's own splash or spray is a
|
|
67
|
+
* picture on a card, and drawn procedurally it can only ever be a guess at
|
|
68
|
+
* it; this is how a converted stage's particles carry the real one. The
|
|
69
|
+
* other modules an effect is spliced into get the names as stubs returning
|
|
70
|
+
* zero. 0 when undeclared.
|
|
71
|
+
*/
|
|
72
|
+
textures: number
|
|
63
73
|
bloom: boolean
|
|
64
74
|
/** This effect takes the cast apart — the host reads the timing. */
|
|
65
75
|
dissolve: boolean
|
|
@@ -124,6 +134,7 @@ const SPEC = {
|
|
|
124
134
|
lights: 1,
|
|
125
135
|
grid: 1,
|
|
126
136
|
points: 1,
|
|
137
|
+
textures: 1,
|
|
127
138
|
bloom: 0,
|
|
128
139
|
dissolve: 0,
|
|
129
140
|
mirror: 0,
|
|
@@ -183,6 +194,7 @@ export function parseDirectives(wgsl: string): DirectiveResult {
|
|
|
183
194
|
lights: 0,
|
|
184
195
|
grid: 0,
|
|
185
196
|
points: null,
|
|
197
|
+
textures: 0,
|
|
186
198
|
bloom: false,
|
|
187
199
|
dissolve: false,
|
|
188
200
|
ground: null,
|
|
@@ -304,6 +316,15 @@ export function parseDirectives(wgsl: string): DirectiveResult {
|
|
|
304
316
|
case "points":
|
|
305
317
|
d.points = args[0]
|
|
306
318
|
return
|
|
319
|
+
case "textures": {
|
|
320
|
+
const n = num(args[0])
|
|
321
|
+
if (n === null || !Number.isInteger(n) || n < 1 || n > 4) {
|
|
322
|
+
errors.push(`${at}: #textures takes a count from 1 to 4`)
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
d.textures = n
|
|
326
|
+
return
|
|
327
|
+
}
|
|
307
328
|
case "mirror":
|
|
308
329
|
d.mirror = true
|
|
309
330
|
return
|
package/src/shaders/lights.ts
CHANGED
|
@@ -49,6 +49,7 @@ import { clockApi, trailSlotsApi, viewportApi } from "./passes/hosted-api"
|
|
|
49
49
|
import { idApi } from "./id-api"
|
|
50
50
|
import { sceneLightApi, worldAmbientWgsl } from "./scene-light-api"
|
|
51
51
|
import { pointsApi } from "./points-api"
|
|
52
|
+
import { textureApi } from "./texture-api"
|
|
52
53
|
|
|
53
54
|
/** Words before the first record: the counts, the grid's placement and the
|
|
54
55
|
* outside mask, four vec4s — see the layout above. */
|
|
@@ -179,7 +180,7 @@ ${viewportApi("viewU[6].w")}
|
|
|
179
180
|
${trailSlotsApi(cast.trailCount)}
|
|
180
181
|
// The id accessors, stubbed: this module cannot read an attachment the
|
|
181
182
|
// scene pass writes. See id-api.ts — the author's whole file compiles here.
|
|
182
|
-
${idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) + pointsApi(false)}
|
|
183
|
+
${idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) + pointsApi(false) + textureApi(0)}
|
|
183
184
|
// The dials the author declared, if any. A lamp is exactly the thing someone
|
|
184
185
|
// retunes — its colour and its reach — so an emitter reads params like every
|
|
185
186
|
// other mount rather than being the one place a #param resolves to nothing.
|
|
@@ -8,6 +8,7 @@ import { audioApi } from "../audio-api"
|
|
|
8
8
|
import { idApi } from "../id-api"
|
|
9
9
|
import { sceneLightApi } from "../scene-light-api"
|
|
10
10
|
import { pointsApi } from "../points-api"
|
|
11
|
+
import { textureApi } from "../texture-api"
|
|
11
12
|
import { castDistanceApi, CAST_FIELD_DIV } from "./cast-distance"
|
|
12
13
|
import { lyricsApi, lyricsTextApi } from "../lyrics-api"
|
|
13
14
|
import { midiApi } from "../midi-api"
|
|
@@ -758,6 +759,7 @@ export function buildFieldShader(effect: CompositeEffectSource): string {
|
|
|
758
759
|
idApi(effect.ids === true, 0, 23) +
|
|
759
760
|
sceneLightApi(false, 0, 0) +
|
|
760
761
|
pointsApi(false) +
|
|
762
|
+
textureApi(0) +
|
|
761
763
|
// Distance to the cast, in SCREEN pixels: the field is half-res, so a field
|
|
762
764
|
// texel is CAST_FIELD_DIV of them and the accessor scales on the way out.
|
|
763
765
|
// An author writes the width they mean and never learns how it is built.
|
|
@@ -10,6 +10,7 @@ import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api"
|
|
|
10
10
|
import { idApi } from "../id-api"
|
|
11
11
|
import { sceneLightApi } from "../scene-light-api"
|
|
12
12
|
import { pointsApi } from "../points-api"
|
|
13
|
+
import { textureApi } from "../texture-api"
|
|
13
14
|
|
|
14
15
|
// A persistent grid an effect can step and read: the one thing an effect could
|
|
15
16
|
// not have before, which is MEMORY.
|
|
@@ -172,6 +173,7 @@ fn rzGrid(uv: vec2f) -> vec4f { return rzGridPrev(uv); }
|
|
|
172
173
|
idApi(false, 0, 0) +
|
|
173
174
|
sceneLightApi(false, 0, 0) +
|
|
174
175
|
pointsApi(false) +
|
|
176
|
+
textureApi(0) +
|
|
175
177
|
// Inlined rather than imported from cast-distance, and NOT for tidiness.
|
|
176
178
|
//
|
|
177
179
|
// This module and composite.ts import each other — grid takes
|
|
@@ -12,6 +12,7 @@ import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract"
|
|
|
12
12
|
import { idApi } from "../id-api"
|
|
13
13
|
import { sceneLightApi } from "../scene-light-api"
|
|
14
14
|
import { pointsApi } from "../points-api"
|
|
15
|
+
import { textureApi } from "../texture-api"
|
|
15
16
|
// GPU particles for user effects: a compute step and an instanced quad draw.
|
|
16
17
|
//
|
|
17
18
|
// Its own shader MODULE rather than more source spliced into composite.ts, for
|
|
@@ -115,6 +116,9 @@ type ParticleSource = {
|
|
|
115
116
|
* bending where a foot pressed is the whole case: the bend has to persist and
|
|
116
117
|
* recover, which is memory, and memory is what the grid is for. */
|
|
117
118
|
gridSize: number
|
|
119
|
+
/** `#textures N`: pictures the shading samples (rzTexture), bound only when
|
|
120
|
+
* N > 0 — see texture-api.ts. Omitted = 0. */
|
|
121
|
+
textures?: number
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
/** Where the shading stage's five scene-light bindings start — see scene-light-api.ts. */
|
|
@@ -126,6 +130,10 @@ export const PARTICLE_POINTS_BINDING = 15
|
|
|
126
130
|
* (6, count, 0, 0), so the draw reads at byte 16. */
|
|
127
131
|
export const PARTICLE_INDIRECT_BINDING = 16
|
|
128
132
|
export const PARTICLE_INDIRECT_BYTES = 32
|
|
133
|
+
/** `#textures`: the pictures at 17..20 and their one sampler at 21, render
|
|
134
|
+
* stage only — see texture-api.ts. */
|
|
135
|
+
export const PARTICLE_TEXTURE_BINDING = 17
|
|
136
|
+
export const PARTICLE_TEXTURE_SAMPLER_BINDING = 21
|
|
129
137
|
export const PARTICLE_INDIRECT_DRAW_OFFSET = 16
|
|
130
138
|
|
|
131
139
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
@@ -234,6 +242,9 @@ ${src.paramsDecl}
|
|
|
234
242
|
idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) +
|
|
235
243
|
// The named points, for real: particleInit is where a flame is put on a wick.
|
|
236
244
|
pointsApi(true, 0, PARTICLE_POINTS_BINDING) +
|
|
245
|
+
// Stubs: textureSample is fragment-only, and a spawn rule has no use for a
|
|
246
|
+
// picture. The names resolve so the file compiles here.
|
|
247
|
+
textureApi(0) +
|
|
237
248
|
(src.live
|
|
238
249
|
? `@group(0) @binding(${PARTICLE_INDIRECT_BINDING}) var<storage, read_write> _rzIndirect: array<u32, 8>;\n`
|
|
239
250
|
: "") +
|
|
@@ -333,6 +344,8 @@ ${src.paramsDecl}
|
|
|
333
344
|
// run by the time this draws, inside the scene pass. See scene-light-api.ts.
|
|
334
345
|
sceneLightApi(true, 0, PARTICLE_LIGHT_BINDING) +
|
|
335
346
|
pointsApi(true, 0, PARTICLE_POINTS_BINDING) +
|
|
347
|
+
// The host's pictures, for real — see texture-api.ts.
|
|
348
|
+
textureApi(src.textures ?? 0, 0, PARTICLE_TEXTURE_BINDING, PARTICLE_TEXTURE_SAMPLER_BINDING) +
|
|
336
349
|
"\n// ── user effect ──\n" +
|
|
337
350
|
src.wgsl +
|
|
338
351
|
/* wgsl */ `
|
|
@@ -138,7 +138,7 @@ const ADD_BOTH: GPUBlendState = {
|
|
|
138
138
|
* OVER for something that arrives ALREADY premultiplied: take the source whole
|
|
139
139
|
* and let it displace the destination by its own coverage.
|
|
140
140
|
*
|
|
141
|
-
* The ground
|
|
141
|
+
* The ground, the mirror and particles need it. The other classes write a
|
|
142
142
|
* straight colour and an alpha, and the src-alpha factor premultiplies it once
|
|
143
143
|
* on the way in. The ground cannot: its coverage is the SUM of a lit surface
|
|
144
144
|
* and a colourless shadow-catcher layer, so it has to weight its own colour by
|
|
@@ -194,7 +194,11 @@ const BLENDS: Record<Exclude<SceneRenderClass, "depth-prepass">, [GPUBlendState,
|
|
|
194
194
|
// everything else, and coverage is what the blend applies.
|
|
195
195
|
ground: [PREMULTIPLIED_OVER, ALPHA_OVER],
|
|
196
196
|
outline: [ALPHA_OVER, ALPHA_OVER],
|
|
197
|
-
|
|
197
|
+
// PREMULTIPLIED colour: the particle fragment weights its colour by its own
|
|
198
|
+
// alpha before it writes (passes/particles.ts), so the src-alpha blend would
|
|
199
|
+
// weight it a second time — a splash at alpha 0.1 arrived at a hundredth of
|
|
200
|
+
// its colour instead of a tenth. The aux mask is written straight, as ever.
|
|
201
|
+
particle: [PREMULTIPLIED_OVER, ALPHA_OVER],
|
|
198
202
|
"particle-additive": [ADD_KEEP_ALPHA, ADD_BOTH],
|
|
199
203
|
trail: [ADD_PREMULTIPLIED, ALPHA_OVER],
|
|
200
204
|
}
|