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.
@@ -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;AAoC7C,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,CAsFpG"}
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":"AA+BA,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;CACjB,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,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,CA0ExF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA2LvF"}
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;AAqFnB;;;;;;;;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"}
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 needs this and nothing else does. Every other class writes a
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
- particle: [ALPHA_OVER, ALPHA_OVER],
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":"AA+BA,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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.61.7",
3
+ "version": "0.62.1",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
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>): Promise<EffectResult> {
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
@@ -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 needs this and nothing else does. Every other class writes a
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
- particle: [ALPHA_OVER, ALPHA_OVER],
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
  }