reze-engine 0.61.6 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/_dbg-loader.mjs +1100 -0
  2. package/dist/engine.d.ts +24 -1
  3. package/dist/engine.d.ts.map +1 -1
  4. package/dist/engine.js +105 -9
  5. package/dist/graph/presets/cloth_rough.d.ts.map +1 -1
  6. package/dist/graph/presets/cloth_rough.js +5 -3
  7. package/dist/graph/schema.d.ts +1 -1
  8. package/dist/graph/schema.d.ts.map +1 -1
  9. package/dist/graph/schema.js +3 -1
  10. package/dist/physics-debug.d.ts +30 -0
  11. package/dist/physics-debug.d.ts.map +1 -0
  12. package/dist/physics-debug.js +526 -0
  13. package/dist/shaders/directives.d.ts +10 -0
  14. package/dist/shaders/directives.d.ts.map +1 -1
  15. package/dist/shaders/directives.js +11 -0
  16. package/dist/shaders/lights.d.ts.map +1 -1
  17. package/dist/shaders/lights.js +2 -1
  18. package/dist/shaders/materials/body.d.ts +2 -0
  19. package/dist/shaders/materials/body.d.ts.map +1 -0
  20. package/dist/shaders/materials/body.js +95 -0
  21. package/dist/shaders/materials/cloth_rough.d.ts +2 -0
  22. package/dist/shaders/materials/cloth_rough.d.ts.map +1 -0
  23. package/dist/shaders/materials/cloth_rough.js +69 -0
  24. package/dist/shaders/materials/cloth_smooth.d.ts +2 -0
  25. package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -0
  26. package/dist/shaders/materials/cloth_smooth.js +61 -0
  27. package/dist/shaders/materials/default.d.ts +2 -0
  28. package/dist/shaders/materials/default.d.ts.map +1 -0
  29. package/dist/shaders/materials/default.js +43 -0
  30. package/dist/shaders/materials/eye.d.ts +2 -0
  31. package/dist/shaders/materials/eye.d.ts.map +1 -0
  32. package/dist/shaders/materials/eye.js +60 -0
  33. package/dist/shaders/materials/face.d.ts +2 -0
  34. package/dist/shaders/materials/face.d.ts.map +1 -0
  35. package/dist/shaders/materials/face.js +95 -0
  36. package/dist/shaders/materials/hair.d.ts +2 -0
  37. package/dist/shaders/materials/hair.d.ts.map +1 -0
  38. package/dist/shaders/materials/hair.js +90 -0
  39. package/dist/shaders/materials/metal.d.ts +2 -0
  40. package/dist/shaders/materials/metal.d.ts.map +1 -0
  41. package/dist/shaders/materials/metal.js +77 -0
  42. package/dist/shaders/materials/mmd_classic.d.ts +2 -0
  43. package/dist/shaders/materials/mmd_classic.d.ts.map +1 -0
  44. package/dist/shaders/materials/mmd_classic.js +66 -0
  45. package/dist/shaders/materials/stockings.d.ts +2 -0
  46. package/dist/shaders/materials/stockings.d.ts.map +1 -0
  47. package/dist/shaders/materials/stockings.js +122 -0
  48. package/dist/shaders/passes/composite.d.ts.map +1 -1
  49. package/dist/shaders/passes/composite.js +2 -0
  50. package/dist/shaders/passes/grid.d.ts.map +1 -1
  51. package/dist/shaders/passes/grid.js +2 -0
  52. package/dist/shaders/passes/particles.d.ts +7 -0
  53. package/dist/shaders/passes/particles.d.ts.map +1 -1
  54. package/dist/shaders/passes/particles.js +10 -0
  55. package/dist/shaders/passes/physics-debug.d.ts +2 -0
  56. package/dist/shaders/passes/physics-debug.d.ts.map +1 -0
  57. package/dist/shaders/passes/physics-debug.js +69 -0
  58. package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
  59. package/dist/shaders/passes/scene-contract.js +6 -2
  60. package/dist/shaders/passes/trails.d.ts.map +1 -1
  61. package/dist/shaders/passes/trails.js +2 -1
  62. package/dist/shaders/texture-api.d.ts +9 -0
  63. package/dist/shaders/texture-api.d.ts.map +1 -0
  64. package/dist/shaders/texture-api.js +54 -0
  65. package/package.json +1 -1
  66. package/src/engine.ts +119 -5
  67. package/src/graph/presets/cloth_rough.ts +5 -3
  68. package/src/graph/schema.ts +3 -1
  69. package/src/shaders/directives.ts +21 -0
  70. package/src/shaders/lights.ts +2 -1
  71. package/src/shaders/passes/composite.ts +2 -0
  72. package/src/shaders/passes/grid.ts +2 -0
  73. package/src/shaders/passes/particles.ts +13 -0
  74. package/src/shaders/passes/scene-contract.ts +6 -2
  75. package/src/shaders/passes/trails.ts +2 -1
  76. package/src/shaders/texture-api.ts +56 -0
  77. package/dist/graph/presets/pool_floor.d.ts +0 -3
  78. package/dist/graph/presets/pool_floor.d.ts.map +0 -1
  79. package/dist/graph/presets/pool_floor.js +0 -42
  80. package/dist/graph/presets/water.d.ts +0 -3
  81. package/dist/graph/presets/water.d.ts.map +0 -1
  82. package/dist/graph/presets/water.js +0 -31
  83. package/dist/physics/autofit.d.ts +0 -147
  84. package/dist/physics/autofit.d.ts.map +0 -1
  85. package/dist/physics/autofit.js +0 -501
  86. package/dist/shaders/passes/field-blit.d.ts +0 -26
  87. package/dist/shaders/passes/field-blit.d.ts.map +0 -1
  88. package/dist/shaders/passes/field-blit.js +0 -65
  89. package/dist/shaders/passes/ghosts.d.ts +0 -22
  90. package/dist/shaders/passes/ghosts.d.ts.map +0 -1
  91. package/dist/shaders/passes/ghosts.js +0 -75
  92. package/dist/shaders/passes/ground-noise.d.ts +0 -7
  93. package/dist/shaders/passes/ground-noise.d.ts.map +0 -1
  94. package/dist/shaders/passes/ground-noise.js +0 -88
  95. package/dist/shaders/passes/sim.d.ts +0 -34
  96. package/dist/shaders/passes/sim.d.ts.map +0 -1
  97. package/dist/shaders/passes/sim.js +0 -169
  98. package/dist/shaders/score-api.d.ts +0 -10
  99. package/dist/shaders/score-api.d.ts.map +0 -1
  100. package/dist/shaders/score-api.js +0 -114
  101. package/dist/shaders/shadow-api.d.ts +0 -29
  102. package/dist/shaders/shadow-api.d.ts.map +0 -1
  103. package/dist/shaders/shadow-api.js +0 -112
@@ -0,0 +1,69 @@
1
+ // Wireframe + solid overlay for physics rigid bodies (sphere / box / capsule).
2
+ // Rendered in its own pass after composite, straight to the swapchain — no
3
+ // MSAA, no depth, no MRT — so it sits cleanly on top of the tonemapped scene
4
+ // regardless of camera angle, model occlusion, or the composite alpha gate.
5
+ //
6
+ // SHAPE_KIND 0 = sphere → unit-sphere line/tri primitive scaled by size.x (radius)
7
+ // 1 = box → unit cube ±1 scaled by size.xyz (half-extents)
8
+ // 2 = capsule → unit cap+ring with per-vertex `axial` (-1/0/+1)
9
+ // that lifts cap rings/arcs ±halfHeight along Y;
10
+ // XZ scales by size.x (radius), Y by size.y/2.
11
+ // SOLID_ALPHA fragment alpha multiplier. Wireframe pipelines leave this at
12
+ // 1.0 for crisp edges; solid pipelines set it to ~0.2 so the fill
13
+ // stays gentle under stacked bodies.
14
+ export const PHYSICS_DEBUG_SHADER_WGSL = /* wgsl */ `
15
+ struct CameraUniforms {
16
+ view: mat4x4f,
17
+ projection: mat4x4f,
18
+ viewPos: vec3f,
19
+ _pad: f32,
20
+ };
21
+
22
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
23
+
24
+ override SHAPE_KIND: u32 = 0u;
25
+ override SOLID_ALPHA: f32 = 1.0;
26
+
27
+ struct VsIn {
28
+ @location(0) unit: vec4f, // xyz = unit pos; w = capsule axial anchor
29
+ @location(1) modelCol0: vec4f,
30
+ @location(2) modelCol1: vec4f,
31
+ @location(3) modelCol2: vec4f,
32
+ @location(4) modelCol3: vec4f,
33
+ @location(5) size: vec4f, // xyz = size; w unused
34
+ @location(6) color: vec4f,
35
+ };
36
+
37
+ struct VsOut {
38
+ @builtin(position) pos: vec4f,
39
+ @location(0) color: vec4f,
40
+ };
41
+
42
+ @vertex
43
+ fn vsMain(in: VsIn) -> VsOut {
44
+ let model = mat4x4f(in.modelCol0, in.modelCol1, in.modelCol2, in.modelCol3);
45
+ var local: vec3f;
46
+ if (SHAPE_KIND == 1u) {
47
+ local = in.unit.xyz * in.size.xyz;
48
+ } else if (SHAPE_KIND == 2u) {
49
+ // PMX capsule: size.x = radius, size.y = full cylinder height. Bullet 2.x's
50
+ // btCapsuleShape(radius, height) stores height/2 internally, so the actual
51
+ // collision cylinder spans ±size.y/2 — halve here to match.
52
+ let r = in.size.x;
53
+ let halfH = in.size.y * 0.5;
54
+ local = vec3f(in.unit.x * r, in.unit.y * r + halfH * in.unit.w, in.unit.z * r);
55
+ } else {
56
+ local = in.unit.xyz * in.size.x;
57
+ }
58
+ let world = model * vec4f(local, 1.0);
59
+ var out: VsOut;
60
+ out.pos = camera.projection * camera.view * world;
61
+ out.color = in.color;
62
+ return out;
63
+ }
64
+
65
+ @fragment
66
+ fn fsMain(in: VsOut) -> @location(0) vec4f {
67
+ return vec4f(in.color.rgb, in.color.a * SOLID_ALPHA);
68
+ }
69
+ `;
@@ -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.6",
3
+ "version": "0.62.0",
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,
@@ -183,10 +185,17 @@ export type MaterialPresetMap = Partial<Record<MaterialPreset, string[]>>
183
185
 
184
186
  // Substring hints mapping common PMX material names (JP/CN/EN) to a style category,
185
187
  // tried when a material isn't in the caller's explicit override map. Ordered: more
186
- // specific families first (靴下 must hit stockings before 靴 hits cloth). A material
188
+ // specific families first (靴下 must hit socks before 靴 hits cloth). A material
187
189
  // matching nothing resolves to null — it stays ungrouped (neutral default).
190
+ //
191
+ // SOCKS ARE CLOTH, STOCKINGS ARE SHEER. The game draws a white sock as opaque
192
+ // fabric on the ordinary cloth ramp; only a sheer stocking gets the stockings
193
+ // look, whose see-through blend made white socks glow flat or wash out. So the
194
+ // sock words go to rough cloth ahead of the stocking words — 袜子 before the
195
+ // bare 袜 that 丝袜 (silk stockings) still reaches.
188
196
  const PRESET_NAME_HINTS: Array<[MaterialPreset, string[]]> = [
189
- ["stockings", ["靴下", "ソックス", "タイツ", "ニーソ", "袜", "stocking", "socks", "tights"]],
197
+ ["cloth_rough", ["靴下", "ソックス", "ニーソ", "袜子", "短袜", "棉袜", "socks", "sock"]],
198
+ ["stockings", ["タイツ", "ストッキング", "袜", "stocking", "tights", "pantyhose"]],
190
199
  [
191
200
  "eye",
192
201
  ["白目", "目影", "二重", "睫", "まつげ", "まゆ", "眉", "目", "瞳", "眼", "eye", "iris", "pupil", "lash", "brow"],
@@ -481,6 +490,17 @@ export type EffectParamValue = number | { x: number; y: number; z: number }
481
490
  /** A vector by shape rather than by class — Vec3 satisfies it, and so does a
482
491
  * JSON object out of a scene document or a literal typed into a console. */
483
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
+
484
504
  export type EffectResult = {
485
505
  ok: boolean
486
506
  /** Compile/validation errors, line:col relative to the USER's WGSL. Also
@@ -1506,6 +1526,9 @@ interface EffectParticles {
1506
1526
  * indirect arguments it writes them to. Null when the effect declared none —
1507
1527
  * the whole pool is then stepped and drawn. */
1508
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[]
1509
1532
  }
1510
1533
 
1511
1534
  /**
@@ -4773,6 +4796,8 @@ export class Engine {
4773
4796
  anchors: { bone: string; trail: boolean }[],
4774
4797
  /** Its row of that table: local slot → scene slot. */
4775
4798
  alias: number[],
4799
+ /** The pictures for `#textures`, as the host passed them. */
4800
+ textures?: EffectTextureInput[],
4776
4801
  ): Promise<{ ok: true; instance: EffectInstance; warnings: string[] } | EffectResult> {
4777
4802
  const noMounts = { background: false, foreground: false }
4778
4803
  if (!this.device) return { ok: false, diagnostics: ["setEffect requires init() to have run"], mounts: noMounts, params: [], duration: 0, readsCast: false }
@@ -5093,6 +5118,7 @@ export class Engine {
5093
5118
  particles?.uniform.destroy()
5094
5119
  particles?.points?.buffer.destroy()
5095
5120
  particles?.live?.indirect.destroy()
5121
+ for (const t of particles?.textures ?? []) t.destroy()
5096
5122
  grid?.textures[0].destroy()
5097
5123
  grid?.textures[1].destroy()
5098
5124
  grid?.uniform.destroy()
@@ -5108,7 +5134,7 @@ export class Engine {
5108
5134
  grid = built.state
5109
5135
  }
5110
5136
  if (wantsParticles) {
5111
- const built = await this.buildParticles(wgsl, d, anchors, alias, paramsFor, grid)
5137
+ const built = await this.buildParticles(wgsl, d, anchors, alias, paramsFor, grid, textures)
5112
5138
  if (!built.ok) return abandon(built.diagnostics)
5113
5139
  particles = built.state
5114
5140
  }
@@ -5269,6 +5295,9 @@ export class Engine {
5269
5295
  * aimed at one dancer should not spend its first frame on all four,
5270
5296
  * which on a ribbon or a sigil reads as a flash. */
5271
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[]
5272
5301
  }[]
5273
5302
  | null,
5274
5303
  ): Promise<EffectResult[]> {
@@ -5322,6 +5351,7 @@ export class Engine {
5322
5351
  requested[i].params,
5323
5352
  perEffectAnchors[i],
5324
5353
  table.alias[i],
5354
+ requested[i].textures,
5325
5355
  )
5326
5356
  if (!("instance" in built)) {
5327
5357
  // Contained: this one is out, the others carry on.
@@ -5437,13 +5467,13 @@ export class Engine {
5437
5467
  * Install ONE effect — the singleton API, kept because most scenes are one
5438
5468
  * effect and every existing caller uses it. A one-element setEffects.
5439
5469
  */
5440
- 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> {
5441
5471
  const noMounts = { background: false, foreground: false }
5442
5472
  if (wgsl === null) {
5443
5473
  await this.setEffects(null)
5444
5474
  return { ok: true, diagnostics: [], mounts: noMounts, params: [], duration: 0, readsCast: false }
5445
5475
  }
5446
- const [result] = await this.setEffects([{ wgsl, params }])
5476
+ const [result] = await this.setEffects([{ wgsl, params, textures }])
5447
5477
  return result ?? { ok: false, diagnostics: ["effect failed to install"], mounts: noMounts, params: [], duration: 0, readsCast: false }
5448
5478
  }
5449
5479
 
@@ -5464,6 +5494,8 @@ export class Engine {
5464
5494
  /** This effect's own grid, already built, or null when it declared none.
5465
5495
  * Read-only here: the pool samples it, the grid pass owns it. */
5466
5496
  grid: EffectGrid | null,
5497
+ /** `#textures`: the host's pictures, slot for slot. */
5498
+ textureInputs?: EffectTextureInput[],
5467
5499
  ): Promise<{ ok: true; state: EffectParticles } | { ok: false; diagnostics: string[] }> {
5468
5500
  // No pragma means "some": an author who wrote the trio clearly wants
5469
5501
  // particles, and failing over a missing comment would be pedantry.
@@ -5477,8 +5509,15 @@ export class Engine {
5477
5509
  gridSize: grid?.size ?? 0,
5478
5510
  cover: particleEntryPoints(wgsl).cover,
5479
5511
  live: particleEntryPoints(wgsl).count,
5512
+ textures: d.textures,
5480
5513
  }
5481
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
+ }
5482
5521
  const points = d.points
5483
5522
  ? {
5484
5523
  prefix: d.points,
@@ -5518,11 +5557,13 @@ export class Engine {
5518
5557
  const computeModule = await compile(buildParticleComputeShader(src, cast), "particle compute")
5519
5558
  if (Array.isArray(computeModule)) {
5520
5559
  points?.buffer.destroy()
5560
+ dropTextures()
5521
5561
  return { ok: false, diagnostics: computeModule }
5522
5562
  }
5523
5563
  const renderModule = await compile(buildParticleRenderShader(src, cast), "particle render")
5524
5564
  if (Array.isArray(renderModule)) {
5525
5565
  points?.buffer.destroy()
5566
+ dropTextures()
5526
5567
  return { ok: false, diagnostics: renderModule }
5527
5568
  }
5528
5569
 
@@ -5602,6 +5643,18 @@ export class Engine {
5602
5643
  ...(withIndirect
5603
5644
  ? [{ binding: PARTICLE_INDIRECT_BINDING, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" as const } }]
5604
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
+ : []),
5605
5658
  ],
5606
5659
  })
5607
5660
  /** One per GRID PARITY, for the reason rebuildFieldBindGroup gives: the grid
@@ -5634,6 +5687,15 @@ export class Engine {
5634
5687
  ]
5635
5688
  : []),
5636
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
+ : []),
5637
5699
  ],
5638
5700
  }),
5639
5701
  ) as [GPUBindGroup, GPUBindGroup]
@@ -5716,6 +5778,7 @@ export class Engine {
5716
5778
  uniform.destroy()
5717
5779
  points?.buffer.destroy()
5718
5780
  indirect?.destroy()
5781
+ dropTextures()
5719
5782
  return { ok: false, diagnostics: [scoped.message] }
5720
5783
  }
5721
5784
  return {
@@ -5745,6 +5808,7 @@ export class Engine {
5745
5808
  }),
5746
5809
  points,
5747
5810
  live,
5811
+ textures: textures.filter((t): t is GPUTexture => t !== null),
5748
5812
  },
5749
5813
  }
5750
5814
  } catch (e) {
@@ -5753,6 +5817,7 @@ export class Engine {
5753
5817
  uniform.destroy()
5754
5818
  points?.buffer.destroy()
5755
5819
  indirect?.destroy()
5820
+ dropTextures()
5756
5821
  return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)] }
5757
5822
  }
5758
5823
  }
@@ -6521,6 +6586,7 @@ export class Engine {
6521
6586
  e.particles?.uniform.destroy()
6522
6587
  e.particles?.points?.buffer.destroy()
6523
6588
  e.particles?.live?.indirect.destroy()
6589
+ for (const t of e.particles?.textures ?? []) t.destroy()
6524
6590
  e.particles = null
6525
6591
  }
6526
6592
  }
@@ -12389,6 +12455,54 @@ export class Engine {
12389
12455
  for (const set of Object.values(install.imagesByMaterial ?? {})) for (const tex of set) tex?.destroy()
12390
12456
  }
12391
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
+
12392
12506
  /** Upload a group's image maps. Sources are decoded images the host already
12393
12507
  * holds; the engine never fetches, matching how models and motions arrive. */
12394
12508
  private uploadGroupImages(group: StyleGroup, slots = group.images): (GPUTexture | null)[] | undefined {
@@ -1,6 +1,8 @@
1
1
  // M_Rough_Cloth as a ShaderGraph — port of shaders/materials/cloth_rough.ts.
2
2
  // NPR graph identical to M_Smooth_Cloth, but the noise bump subtree IS live on
3
- // Principled.Normal (weave bump in rest space) and Roughness is raised to 0.8187.
3
+ // Principled.Normal (weave bump in rest space), and it is MATTE where smooth cloth
4
+ // shines: roughness 1, specular 0.25, and a wider toon band (0.20-0.45) so the
5
+ // light rolls off cotton instead of cutting. Smooth keeps 0.5 / 0.8 / 0.25-0.35.
4
6
  // The tex_noise node hits the detail=2 peephole → tex_noise_d2.
5
7
 
6
8
  import type { ShaderGraph } from "../schema"
@@ -19,7 +21,7 @@ export const CLOTH_ROUGH_GRAPH: ShaderGraph = {
19
21
  { id: "tex_base", type: "mix/multiply", inputs: { fac: 1.0 } },
20
22
  { id: "geo", type: "geometry" },
21
23
  { id: "str", type: "shader_to_rgb_diffuse" },
22
- { id: "ramp_008", type: "ramp_cardinal", inputs: { pos0: 0.2466, pos1: 0.3466 } },
24
+ { id: "ramp_008", type: "ramp_cardinal", inputs: { pos0: 0.2, pos1: 0.45 } },
23
25
  { id: "mix04_fac", type: "math/multiply", inputs: { b: 0.5 } },
24
26
  { id: "dark_tex", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.0, value: 0.19999998807907104, fac: 1.0 } },
25
27
  { id: "mix_004", type: "mix/blend" },
@@ -36,7 +38,7 @@ export const CLOTH_ROUGH_GRAPH: ShaderGraph = {
36
38
  {
37
39
  id: "principled",
38
40
  type: "principled",
39
- inputs: { metallic: 0.0, specular_ior_level: 0.8, roughness: 0.8187, spec_clamp: 10.0, sheen_weight: 0.0, sheen_tint: 0.0 },
41
+ inputs: { metallic: 0.0, specular_ior_level: 0.25, roughness: 1.0, spec_clamp: 10.0, sheen_weight: 0.0, sheen_tint: 0.0 },
40
42
  },
41
43
  { id: "mix_shader_001", type: "mix_shader", inputs: { fac: 0.8999999761581421 } },
42
44
  ],
@@ -74,5 +74,7 @@ export type Diagnostic = {
74
74
  message: string
75
75
  }
76
76
 
77
- export const MAX_NODES = 64
77
+ // A sanity bound, not a GPU one: a graph compiles to one fragment function and
78
+ // a longer one is only more WGSL. The game ports (X348's sea and water) run 60+.
79
+ export const MAX_NODES = 128
78
80
  export const MAX_PARAMS = 16
@@ -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