reze-engine 0.54.3 → 0.54.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Reading the scene's id attachment from a field effect — the consumer the MRT
3
+ * work exists for.
4
+ *
5
+ * A field effect covers the whole screen and has no idea what it is drawing
6
+ * over. These make it addressable: mask a glow to one character, dissolve one
7
+ * material, outline the thing someone selected. Without them the id buffer is
8
+ * written every frame and read by nobody.
9
+ *
10
+ * MULTISAMPLED AND UNRESOLVED, so it is read the way it is written —
11
+ * textureLoad of sample 0, the same rule linearDepth already follows. An
12
+ * averaged id belongs to nothing.
13
+ *
14
+ * When ids are OFF the buffer does not exist, so the accessors are still
15
+ * DECLARED and answer 0 — the reserved nothing. An effect that masks by id then
16
+ * masks nothing at all, which is a scene that renders rather than a shader that
17
+ * will not compile.
18
+ */
19
+ export declare function idApi(on: boolean, group: number, binding: number): string;
20
+ //# sourceMappingURL=id-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id-api.d.ts","sourceRoot":"","sources":["../../src/shaders/id-api.ts"],"names":[],"mappings":"AAkBA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAwBzE"}
@@ -0,0 +1,60 @@
1
+ // The scene's id attachment, as an author sees it.
2
+ //
3
+ // ITS OWN MODULE because every mount compiles the author's WHOLE FILE, not just
4
+ // the entry point it needs. An effect that masks a glow by character has one
5
+ // function reading rzObjectAt and, very often, a lightEmit beside it — and the
6
+ // light module has no id texture, so the file failed to compile there with
7
+ // "unresolved call target". The gap, not the effect, was the bug.
8
+ //
9
+ // Only the field module can bind the real thing, and that is not an oversight
10
+ // to close later: the id attachment is WRITTEN BY THE SCENE PASS. The light
11
+ // emitter runs before it, and the particle, trail and grid steps run inside it,
12
+ // so there is no ordering in which any of them could read a finished buffer.
13
+ // They get the stub, permanently, and it answers 0 — the reserved nothing.
14
+ //
15
+ // lights.ts imports this and must not import the composite, which imports
16
+ // lights.ts for RzLight. One small module both can depend on is what keeps that
17
+ // from being a cycle.
18
+ /**
19
+ * Reading the scene's id attachment from a field effect — the consumer the MRT
20
+ * work exists for.
21
+ *
22
+ * A field effect covers the whole screen and has no idea what it is drawing
23
+ * over. These make it addressable: mask a glow to one character, dissolve one
24
+ * material, outline the thing someone selected. Without them the id buffer is
25
+ * written every frame and read by nobody.
26
+ *
27
+ * MULTISAMPLED AND UNRESOLVED, so it is read the way it is written —
28
+ * textureLoad of sample 0, the same rule linearDepth already follows. An
29
+ * averaged id belongs to nothing.
30
+ *
31
+ * When ids are OFF the buffer does not exist, so the accessors are still
32
+ * DECLARED and answer 0 — the reserved nothing. An effect that masks by id then
33
+ * masks nothing at all, which is a scene that renders rather than a shader that
34
+ * will not compile.
35
+ */
36
+ export function idApi(on, group, binding) {
37
+ if (!on) {
38
+ return /* wgsl */ `
39
+ fn rzObjectAt(uv: vec2f) -> u32 { return 0u; }
40
+ fn rzMaterialAt(uv: vec2f) -> u32 { return 0u; }
41
+ `;
42
+ }
43
+ return /* wgsl */ `
44
+ @group(${group}) @binding(${binding}) var _rzIdTex: texture_multisampled_2d<u32>;
45
+
46
+ /** Which OBJECT drew this pixel — compare against rzSubjectId(i). 0 = nothing. */
47
+ fn rzObjectAt(uv: vec2f) -> u32 {
48
+ let sz = vec2f(textureDimensions(_rzIdTex));
49
+ let p = vec2<i32>(clamp(uv, vec2f(0.0), vec2f(1.0)) * sz);
50
+ return textureLoad(_rzIdTex, clamp(p, vec2<i32>(0), vec2<i32>(sz) - vec2<i32>(1)), 0).y;
51
+ }
52
+
53
+ /** Which MATERIAL drew it, within that object. 0 = nothing. */
54
+ fn rzMaterialAt(uv: vec2f) -> u32 {
55
+ let sz = vec2f(textureDimensions(_rzIdTex));
56
+ let p = vec2<i32>(clamp(uv, vec2f(0.0), vec2f(1.0)) * sz);
57
+ return textureLoad(_rzIdTex, clamp(p, vec2<i32>(0), vec2<i32>(sz) - vec2<i32>(1)), 0).x;
58
+ }
59
+ `;
60
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../src/shaders/lights.ts"],"names":[],"mappings":"AAgCA;yEACyE;AACzE,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,+CAA+C;AAC/C,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B;;;;;GAKG;AACH,eAAO,MAAM,UAAU,KAAK,CAAA;AAC5B,kCAAkC;AAClC,eAAO,MAAM,aAAa,QAA2C,CAAA;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,qJAQhC,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,idAa9B,CAAA;AAED,8CAA8C;AAC9C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC3B,MAAM,CA4ER;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAkEhE"}
1
+ {"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../src/shaders/lights.ts"],"names":[],"mappings":"AAiCA;yEACyE;AACzE,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,+CAA+C;AAC/C,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B;;;;;GAKG;AACH,eAAO,MAAM,UAAU,KAAK,CAAA;AAC5B,kCAAkC;AAClC,eAAO,MAAM,aAAa,QAA2C,CAAA;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,qJAQhC,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,idAa9B,CAAA;AAED,8CAA8C;AAC9C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC3B,MAAM,CA+ER;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAkEhE"}
@@ -28,6 +28,7 @@ import { audioApi } from "./audio-api";
28
28
  import { midiApi } from "./midi-api";
29
29
  import { lyricsApi } from "./lyrics-api";
30
30
  import { clockApi, trailSlotsApi, viewportApi } from "./passes/hosted-api";
31
+ import { idApi } from "./id-api";
31
32
  /** Floats before the first record. One is the count; the rest keep the records
32
33
  * vec4-aligned, which is what lets a future pass read them as vec4s. */
33
34
  export const LIGHT_HEADER = 4;
@@ -147,6 +148,9 @@ ${clockApi("_rzLightU.x", "0.0")}
147
148
  // and viewU[6].w have the same source.
148
149
  ${viewportApi("viewU[6].w")}
149
150
  ${trailSlotsApi(cast.trailCount)}
151
+ // The id accessors, stubbed: this module cannot read an attachment the
152
+ // scene pass writes. See id-api.ts — the author's whole file compiles here.
153
+ ${idApi(false, 0, 0)}
150
154
  ${wgsl}
151
155
 
152
156
  @compute @workgroup_size(64)
@@ -94,6 +94,15 @@ type CompositeEffectSource = {
94
94
  */
95
95
  export declare const EFFECT_SCENE_API = "\n// \u2500\u2500 The effect API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Named rz*, for the engine. The prefix earns its place twice: user code is\n// concatenated into THIS module, so an unprefixed rzAnchor() would collide with\n// exactly the helper an author would write, and the old bg* prefix stopped being\n// true in 0.41.0 when effects gained a mount over the finished frame.\n//\n// The bg* names below are permanent aliases, not a deprecation with an end date.\n// A published link is immutable, so a scene pinned to a bg* effect has to keep\n// compiling forever. They are one-line and inlined; no new function gets one.\n\n/** Canvas size in pixels \u2014 for aspect correction. */\nfn rzResolution() -> vec2f { return viewU[6].zw; }\n\n/** The camera's world position. */\nfn rzCameraPos() -> vec3f { return viewU[10].xyz; }\n\n/** How many characters are in the scene, up to four. */\nfn rzSubjectCount() -> i32 { return i32(viewU[10].w); }\n\n/**\n * A world point as the camera sees it: xy the uv it lands on, z its distance\n * along the VIEW AXIS in metres.\n *\n * The exact inverse of the ray this pass builds per pixel, so it is the cheap way\n * to work with anything anchored in the world. Marching a curve or a trail in 3D\n * costs a distance evaluation per sample per pixel; projecting its points once\n * and measuring in 2D costs a subtraction, which is the difference between a\n * ribbon that runs at 4K and one that does not.\n *\n * z is directly comparable to the depth handed to foreground(), so occlusion is\n * a single test: draw where your z is nearer than the scene's. It is returned\n * SIGNED and unclamped \u2014 behind the camera is negative, and worth rejecting\n * before you use the uv, which is meaningless there.\n */\nfn rzProject(p: vec3f) -> vec3f {\n let d = p - viewU[10].xyz;\n let z = dot(d, viewU[5].xyz);\n // Guard only the divide. z itself is returned as it is, so the caller can see\n // the sign; clamping it here would put points behind the lens on the horizon.\n let inv = 1.0 / select(z, 1e-4, z < 1e-4);\n let ndc = vec2f(dot(d, viewU[3].xyz) * inv / viewU[3].w, dot(d, viewU[4].xyz) * inv / viewU[4].w);\n return vec3f(ndc * 0.5 + 0.5, z);\n}\n\nfn rzCamPos() -> vec3f { return rzCameraPos(); }\nfn rzCameraRight() -> vec3f { return viewU[3].xyz; }\nfn rzCameraUp() -> vec3f { return viewU[4].xyz; }\nfn rzCameraForward() -> vec3f { return viewU[5].xyz; }\n\n// The cast \u2014 subjects, anchors, trails \u2014 is CAST_API, shared verbatim with the\n// particle and trail modules. It used to be written out here, a second time, and\n// the two copies had drifted: this one had rzAnchor and that one did not.\n\nconst RZ_SUBJECTS: i32 = 4;\nconst RZ_SAMPLES: i32 = 128;\n/** The anchor ADDRESS SPACE \u2014 how many an effect may declare, not how many it\n * did. RZ_TRAIL_SLOTS is the per-effect number and is not this one; the two\n * being one number was the old trail bug. */\nconst RZ_MAX_ANCHORS: i32 = 16;\nconst RZ_TRAIL_SAMPLES: i32 = 128;\n\nstruct RzSubject {\n /** On the FLOOR, under the body \u2014 where a ring or a magic circle belongs. */\n root: vec3f,\n /** At the hips, the middle of the body \u2014 where an aura belongs. */\n center: vec3f,\n /** Bounding sphere: xyz centre, w radius. Deliberately generous \u2014 cull with it. */\n bounds: vec4f,\n /**\n * How much of this character is still THERE: 1 whole, 0 gone.\n *\n * What setModelDissolve last set on them, which the material pass has already\n * acted on by the time an effect runs \u2014 so an effect drawing what LEAVES a\n * dissolving body (sparks, ash, a ghost) reads the same number the body was\n * taken apart with, rather than keeping a clock of its own and hoping the two\n * agree. 1 on a scene that never dissolves anybody.\n */\n dissolve: f32,\n /** False past the end of the cast, and every field is then zero. */\n valid: bool,\n}\n\nstruct RzAnchor {\n pos: vec3f,\n /** World units per second, from the previous frame. Direction for a trail,\n * magnitude for anything that should react to how hard someone is moving. */\n vel: vec3f,\n /** The bone's forward axis \u2014 which way a foot points, where a head looks. */\n fwd: vec3f,\n /** False when this rig has no such bone. Check it: the alternative is drawing\n * a hand effect at the world origin on every model that spells it differently. */\n valid: bool,\n}\n\n/** Which model this is, stable across a scene \u2014 for per-subject variation. */\nfn rzSubjectId(i: i32) -> u32 {\n if (i < 0 || i >= rzSubjectCount()) { return 0u; }\n return u32(_rzCast[i * 3 + 1].w);\n}\n\nfn rzSubject(i: i32) -> RzSubject {\n var s: RzSubject;\n s.valid = i >= 0 && i < rzSubjectCount();\n if (!s.valid) { return s; }\n let b = i * 3;\n s.root = _rzCast[b].xyz;\n s.dissolve = _rzCast[b].w;\n s.center = _rzCast[b + 1].xyz;\n s.bounds = _rzCast[b + 2];\n return s;\n}\n\n/**\n * Where a named bone is, this frame.\n *\n * The slot is the author's own: the Nth #anchor in their file, in the order\n * they wrote them. _rzSlot turns that into the scene's address, which is what\n * keeps two effects that both anchor to a wrist from reading each other's.\n */\nfn rzAnchor(subject: i32, slot: i32) -> RzAnchor {\n var a: RzAnchor;\n a.valid = false;\n let g = _rzSlot(slot);\n if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }\n let b = 12 + (g * 4 + subject) * 3;\n a.valid = _rzCast[b].w > 0.5;\n a.pos = _rzCast[b].xyz;\n a.vel = _rzCast[b + 1].xyz;\n a.fwd = _rzCast[b + 2].xyz;\n return a;\n}\n\n/**\n * How many samples of a path are recorded \u2014 0 for an anchor that asked for no\n * trail, and for one that has not moved yet.\n *\n * Bounded by the anchor cap, NOT by how many anchors asked for a trail. Those\n * are different index spaces: storage is addressed by anchor slot, so an\n * untrailed #anchor followed by a trailed one put the trail at index 1 with a\n * bound of 1 and rzTrail returned zero \u2014 a ribbon that silently did not draw.\n */\nfn rzTrailCount(subject: i32, slot: i32) -> i32 {\n let g = _rzSlot(slot);\n if (subject < 0 || subject >= rzSubjectCount() || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }\n return i32(_rzCast[12 + (g * 4 + subject) * 3 + 2].w);\n}\n\n/** Sample i of a path: xyz where it was, w how many seconds ago. i = 0 is now. */\nfn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {\n let n = rzTrailCount(subject, slot);\n if (i < 0 || i >= n) { return vec4f(0.0); }\n let base = 204 + (_rzSlot(slot) * 4 + subject) * RZ_TRAIL_SAMPLES;\n return _rzCast[base + i];\n}\n\n\n// The hashes, the noise and rzFalloff \u2014 EFFECT_MATH_API, the same text the\n// particle and trail modules get. The field module used to be missing most of\n// it, so a helper an author wrote for one mount failed to compile in another\n// for no reason visible in the file.\n\nfn rzHash11(x: f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\nfn rzHash21(p: vec2f) -> f32 {\n var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n/** Three independent randoms from one seed \u2014 the usual need when spawning. */\nfn rzHash13(x: f32) -> vec3f {\n return vec3f(rzHash11(x), rzHash11(x + 17.13), rzHash11(x + 41.71));\n}\n/**\n * Compact-support falloff: 1 at the centre, exactly 0 at r, smooth between.\n *\n * It reaches exactly zero rather than merely getting small, because a glow that\n * never quite ends has to be culled somewhere, and culling it wherever it looks\n * close enough is what put a visible hard edge on the first halo effect.\n */\nfn rzFalloff(d: f32, r: f32) -> f32 {\n let x = clamp(d / max(r, 1e-6), 0.0, 1.0);\n let f = 1.0 - x;\n return f * f * f;\n}\nfn rzHash31(p: vec3f) -> f32 {\n var p3 = fract(p * 0.1031);\n p3 = p3 + dot(p3, p3.zyx + 31.32);\n return fract((p3.x + p3.y) * p3.z);\n}\nfn rzValueNoise(p: vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = rzHash31(i);\n let n100 = rzHash31(i + vec3f(1.0, 0.0, 0.0));\n let n010 = rzHash31(i + vec3f(0.0, 1.0, 0.0));\n let n110 = rzHash31(i + vec3f(1.0, 1.0, 0.0));\n let n001 = rzHash31(i + vec3f(0.0, 0.0, 1.0));\n let n101 = rzHash31(i + vec3f(1.0, 0.0, 1.0));\n let n011 = rzHash31(i + vec3f(0.0, 1.0, 1.0));\n let n111 = rzHash31(i + vec3f(1.0, 1.0, 1.0));\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n/** Divergence-free flow \u2014 the field a wisp of smoke follows without a solver. */\nfn rzCurlNoise(p: vec3f) -> vec3f {\n let e = 0.1;\n let dx = vec3f(e, 0.0, 0.0);\n let dy = vec3f(0.0, e, 0.0);\n let dz = vec3f(0.0, 0.0, e);\n let x0 = rzValueNoise(p - dx); let x1 = rzValueNoise(p + dx);\n let y0 = rzValueNoise(p - dy); let y1 = rzValueNoise(p + dy);\n let z0 = rzValueNoise(p - dz); let z1 = rzValueNoise(p + dz);\n return normalize(vec3f((y1 - y0) - (z1 - z0), (z1 - z0) - (x1 - x0), (x1 - x0) - (y1 - y0)) + vec3f(1e-6));\n}\n\nstruct Particle {\n pos: vec3f,\n age: f32,\n vel: vec3f,\n life: f32,\n size: f32,\n rot: f32,\n seed: f32,\n // Aspect along the direction of travel. 1 or less is a square billboard; a\n // raindrop is 10 or 20. Zero-initialised, so an effect that never sets it gets\n // the square it expects.\n stretch: f32,\n}\n\n\n\nfn bgResolution() -> vec2f { return rzResolution(); }\nfn bgCameraPos() -> vec3f { return rzCameraPos(); }\nfn bgSubjectCount() -> i32 { return rzSubjectCount(); }\n\n/**\n * Where a character IS, in world space \u2014 at the hips, not on the floor.\n *\n * An effect that wants to RESPOND to the cast \u2014 a glow that follows someone,\n * dust kicked up where they are \u2014 needs to know where they are, and the ray and\n * the depth cannot tell it: they describe the pixel, not the scene.\n *\n * The value is model.position + \u30BB\u30F3\u30BF\u30FC + \u5168\u3066\u306E\u89AA. \u30BB\u30F3\u30BF\u30FC sits at hip\n * height on every standard MMD rig, so this is a point in the middle of the\n * body. It is NOT the contact point: a ripple drawn here appears at the waist.\n * Ground effects want the .xz of this and their own floor height, which is what\n * the effects that shipped against it already do.\n *\n * The comment here used to claim it was \"between the feet on the floor\", which\n * is where that habit came from. Left as it is regardless of the name: a\n * published link is immutable, so every shared scene pinning an effect that\n * reads this depends on it meaning exactly what it has always meant.\n *\n * Clamped rather than bounds-checked: an effect looping past the count reads the\n * last subject instead of sampling whatever follows the array, which is a wrong\n * ripple rather than an undefined one.\n */\nfn rzSubjectHip(i: i32) -> vec3f { return viewU[11 + clamp(i, 0, 3)].xyz; }\n\nfn bgSubjectPos(i: i32) -> vec3f { return rzSubjectHip(i); }\n\n/** Where in the WORLD the scene drew this pixel \u2014 the depth handed to\n * foreground() turned into a place. Without it an effect can only think in\n * distances from the lens, which is no use to anything that belongs somewhere:\n * fog lying on the ground has to know where the ground is.\n *\n * depth measures along the VIEW AXIS, not along the ray, so it is divided by\n * the ray's projection onto camera-forward before being walked out. At the far\n * plane (nothing drawn) this lands a very long way off, which is what a sky\n * should do to anything reading it. */\nfn rzWorldPos(ray: vec3f, depth: f32) -> vec3f {\n let axis = max(dot(normalize(ray), viewU[5].xyz), 1e-4);\n return rzCameraPos() + normalize(ray) * (depth / axis);\n}\n\nfn bgWorldPos(ray: vec3f, depth: f32) -> vec3f { return rzWorldPos(ray, depth); }\n\n/** What an effect returns for one of its lights. */\nstruct RzLight {\n pos: vec3f,\n color: vec3f,\n intensity: f32,\n radius: f32,\n}\n\n\n/** Color grading, applied to the tonemapped SCENE (not the background \u2014 see the\n * call site). The core is ASC CDL, the film-industry interchange standard:\n *\n * out = (in \u00B7 slope + offset) ^ power then saturation (SOP \u2192 SAT)\n *\n * Using the real standard rather than invented controls means a look authored\n * here maps onto any grading tool. slope/offset/power are derived on the CPU\n * from the UI's shadow/midtone/highlight colors (see setColorGrading), so the\n * per-pixel cost is one mul-add, one pow, one lerp. */\nfn grade(c: vec3f) -> vec3f {\n var x = pow(max(c * viewU[9].xyz + viewU[7].xyz, vec3f(0.0)), viewU[8].xyz);\n // Contrast pivots on 0.5 \u2014 display-referred midpoint, since we grade post-Filmic.\n x = (x - vec3f(0.5)) * viewU[7].w + vec3f(0.5);\n // Rec.709 luma, matching the ASC SAT node.\n let luma = dot(x, vec3f(0.2126, 0.7152, 0.0722));\n return max(mix(vec3f(luma), x, viewU[8].w), vec3f(0.0));\n}\n";
96
96
  export declare function buildCompositeShader(effect?: CompositeEffectSource | null): string;
97
+ /**
98
+ * The field pass: the user's background/foreground mounts at half resolution,
99
+ * into two rgba16f targets the composite bilinearly upsamples.
100
+ *
101
+ * The fragment reconstructs the FULL-resolution pixel it stands in for and runs
102
+ * the original derivation verbatim — same ndc, same ray, same uv, same depth
103
+ * read — so an effect cannot tell it moved; it is simply asked half as often
104
+ * in each direction.
105
+ */
97
106
  export declare function buildFieldShader(effect: CompositeEffectSource): string;
98
107
  /** Kept for compatibility with existing imports (the base, no-effect shader). */
99
108
  export declare const COMPOSITE_SHADER_WGSL: string;
@@ -1 +1 @@
1
- {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAgC+B;AAC/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EAAE,CAIhG;AAID,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAEzG,KAAK,qBAAqB,GAAG;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,aAAa,EAAE,OAAO,CAAA;IACtB,oEAAoE;IACpE,aAAa,EAAE,OAAO,CAAA;IACtB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAA;IAChB;mFAC+E;IAC/E,GAAG,CAAC,EAAE,OAAO,CAAA;IACb;;;yEAGqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB;;8EAE0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;AA6LD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,yqaA+H5B,CAAA;AA4LD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAQlF;AAuDD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CA4EtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
1
+ {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AAyBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAgC+B;AAC/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EAAE,CAIhG;AAID,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAEzG,KAAK,qBAAqB,GAAG;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,aAAa,EAAE,OAAO,CAAA;IACtB,oEAAoE;IACpE,aAAa,EAAE,OAAO,CAAA;IACtB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAA;IAChB;mFAC+E;IAC/E,GAAG,CAAC,EAAE,OAAO,CAAA;IACb;;;yEAGqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB;;8EAE0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;AA6LD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,yqaA+H5B,CAAA;AA4LD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAQlF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CA4EtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
@@ -3,6 +3,7 @@ import { anchorAliasWgsl } from "../anchor-table";
3
3
  import { CAST_API } from "../cast-api";
4
4
  import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
5
5
  import { audioApi } from "../audio-api";
6
+ import { idApi } from "../id-api";
6
7
  import { lyricsApi, lyricsTextApi } from "../lyrics-api";
7
8
  import { midiApi } from "../midi-api";
8
9
  import { gridReadApi } from "./grid";
@@ -608,49 +609,6 @@ export function buildCompositeShader(effect) {
608
609
  * read — so an effect cannot tell it moved; it is simply asked half as often
609
610
  * in each direction.
610
611
  */
611
- /**
612
- * Reading the scene's id attachment from a field effect — the consumer the MRT
613
- * work exists for.
614
- *
615
- * A field effect covers the whole screen and has no idea what it is drawing
616
- * over. These make it addressable: mask a glow to one character, dissolve one
617
- * material, outline the thing someone selected. Without them the id buffer is
618
- * written every frame and read by nobody.
619
- *
620
- * MULTISAMPLED AND UNRESOLVED, so it is read the way it is written —
621
- * textureLoad of sample 0, the same rule linearDepth already follows. An
622
- * averaged id belongs to nothing.
623
- *
624
- * When ids are OFF the buffer does not exist, so the accessors are still
625
- * DECLARED and answer 0 — the reserved nothing. An effect that masks by id then
626
- * masks nothing at all, which is a scene that renders rather than a shader that
627
- * will not compile.
628
- */
629
- function idApi(on, group, binding) {
630
- if (!on) {
631
- return /* wgsl */ `
632
- fn rzObjectAt(uv: vec2f) -> u32 { return 0u; }
633
- fn rzMaterialAt(uv: vec2f) -> u32 { return 0u; }
634
- `;
635
- }
636
- return /* wgsl */ `
637
- @group(${group}) @binding(${binding}) var _rzIdTex: texture_multisampled_2d<u32>;
638
-
639
- /** Which OBJECT drew this pixel — compare against rzSubjectId(i). 0 = nothing. */
640
- fn rzObjectAt(uv: vec2f) -> u32 {
641
- let sz = vec2f(textureDimensions(_rzIdTex));
642
- let p = vec2<i32>(clamp(uv, vec2f(0.0), vec2f(1.0)) * sz);
643
- return textureLoad(_rzIdTex, clamp(p, vec2<i32>(0), vec2<i32>(sz) - vec2<i32>(1)), 0).y;
644
- }
645
-
646
- /** Which MATERIAL drew it, within that object. 0 = nothing. */
647
- fn rzMaterialAt(uv: vec2f) -> u32 {
648
- let sz = vec2f(textureDimensions(_rzIdTex));
649
- let p = vec2<i32>(clamp(uv, vec2f(0.0), vec2f(1.0)) * sz);
650
- return textureLoad(_rzIdTex, clamp(p, vec2<i32>(0), vec2<i32>(sz) - vec2<i32>(1)), 0).x;
651
- }
652
- `;
653
- }
654
612
  export function buildFieldShader(effect) {
655
613
  const bgLine = effect.hasBackground
656
614
  ? "out.bg = clamp(background(dir, uv, _rzFieldClock.x), vec4f(0.0), vec4f(1.0));"
@@ -1 +1 @@
1
- {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAiC7C,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;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6DnF"}
1
+ {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAkC7C,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;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAgEnF"}
@@ -4,6 +4,7 @@ import { anchorAliasWgsl } from "../anchor-table";
4
4
  import { midiApi } from "../midi-api";
5
5
  import { EFFECT_SCENE_API } from "./composite";
6
6
  import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api";
7
+ import { idApi } from "../id-api";
7
8
  // A persistent grid an effect can step and read: the one thing an effect could
8
9
  // not have before, which is MEMORY.
9
10
  //
@@ -142,6 +143,9 @@ fn rzGridPrev(uv: vec2f) -> vec4f {
142
143
  /* wgsl */ `
143
144
  fn rzGrid(uv: vec2f) -> vec4f { return rzGridPrev(uv); }
144
145
  ` +
146
+ // Stubbed: this module cannot read an attachment the scene pass writes — see
147
+ // id-api.ts. The author's whole file compiles here, so the names must exist.
148
+ idApi(false, 0, 0) +
145
149
  "\n// ── user effect (setEffect) ──\n" +
146
150
  wgsl +
147
151
  /* wgsl */ `
@@ -1 +1 @@
1
- {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AAyBA,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;AAiCD,qDAAqD;AACrD,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,CAAA;AAEzC,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;CACf,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAuDjC,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAMlG;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA0CxF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAyHvF"}
1
+ {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AA0BA,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;AAiCD,qDAAqD;AACrD,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,CAAA;AAEzC,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;CACf,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAuDjC,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAMlG;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6CxF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA4HvF"}
@@ -6,6 +6,7 @@ import { midiApi } from "../midi-api";
6
6
  import { CAST_API } from "../cast-api";
7
7
  import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
8
8
  import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract";
9
+ import { idApi } from "../id-api";
9
10
  /**
10
11
  * The trail accessors, in the PARTICLE module.
11
12
  *
@@ -118,6 +119,9 @@ export function buildParticleComputeShader(src, cast) {
118
119
  midiApi(0, 5) +
119
120
  lyricsApi(0, 6) +
120
121
  PRELUDE +
122
+ // Stubbed: this module cannot read an attachment the scene pass writes — see
123
+ // id-api.ts. The author's whole file compiles here, so the names must exist.
124
+ idApi(false, 0, 0) +
121
125
  "\n// ── user effect ──\n" +
122
126
  src.wgsl +
123
127
  /* wgsl */ `
@@ -174,6 +178,9 @@ override ADDITIVE: bool = ${src.blend === "additive" ? "true" : "false"};\n` +
174
178
  midiApi(0, 5) +
175
179
  lyricsApi(0, 6) +
176
180
  PRELUDE +
181
+ // Stubbed: this module cannot read an attachment the scene pass writes — see
182
+ // id-api.ts. The author's whole file compiles here, so the names must exist.
183
+ idApi(false, 0, 0) +
177
184
  "\n// ── user effect ──\n" +
178
185
  src.wgsl +
179
186
  /* wgsl */ `
@@ -1 +1 @@
1
- {"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AAyBA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CA6UR"}
1
+ {"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AA0BA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CAgVR"}
@@ -6,6 +6,7 @@ import { midiApi } from "../midi-api";
6
6
  import { CAST_API } from "../cast-api";
7
7
  import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
8
8
  import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract";
9
+ import { idApi } from "../id-api";
9
10
  /**
10
11
  * Sub-segments drawn between each pair of recorded samples.
11
12
  *
@@ -183,6 +184,9 @@ fn rzTurnRadius(a: vec3f, b: vec3f, c: vec3f) -> f32 {
183
184
  audioApi(0, 4) +
184
185
  midiApi(0, 5) +
185
186
  lyricsApi(0, 6) +
187
+ // Stubbed: this module cannot read an attachment the scene pass writes — see
188
+ // id-api.ts. The author's whole file compiles here, so the names must exist.
189
+ idApi(false, 0, 0) +
186
190
  "\n// ── user effect ──\n" +
187
191
  src.wgsl +
188
192
  /* wgsl */ `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.54.3",
3
+ "version": "0.54.4",
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",
@@ -0,0 +1,61 @@
1
+ // The scene's id attachment, as an author sees it.
2
+ //
3
+ // ITS OWN MODULE because every mount compiles the author's WHOLE FILE, not just
4
+ // the entry point it needs. An effect that masks a glow by character has one
5
+ // function reading rzObjectAt and, very often, a lightEmit beside it — and the
6
+ // light module has no id texture, so the file failed to compile there with
7
+ // "unresolved call target". The gap, not the effect, was the bug.
8
+ //
9
+ // Only the field module can bind the real thing, and that is not an oversight
10
+ // to close later: the id attachment is WRITTEN BY THE SCENE PASS. The light
11
+ // emitter runs before it, and the particle, trail and grid steps run inside it,
12
+ // so there is no ordering in which any of them could read a finished buffer.
13
+ // They get the stub, permanently, and it answers 0 — the reserved nothing.
14
+ //
15
+ // lights.ts imports this and must not import the composite, which imports
16
+ // lights.ts for RzLight. One small module both can depend on is what keeps that
17
+ // from being a cycle.
18
+
19
+ /**
20
+ * Reading the scene's id attachment from a field effect — the consumer the MRT
21
+ * work exists for.
22
+ *
23
+ * A field effect covers the whole screen and has no idea what it is drawing
24
+ * over. These make it addressable: mask a glow to one character, dissolve one
25
+ * material, outline the thing someone selected. Without them the id buffer is
26
+ * written every frame and read by nobody.
27
+ *
28
+ * MULTISAMPLED AND UNRESOLVED, so it is read the way it is written —
29
+ * textureLoad of sample 0, the same rule linearDepth already follows. An
30
+ * averaged id belongs to nothing.
31
+ *
32
+ * When ids are OFF the buffer does not exist, so the accessors are still
33
+ * DECLARED and answer 0 — the reserved nothing. An effect that masks by id then
34
+ * masks nothing at all, which is a scene that renders rather than a shader that
35
+ * will not compile.
36
+ */
37
+ export function idApi(on: boolean, group: number, binding: number): string {
38
+ if (!on) {
39
+ return /* wgsl */ `
40
+ fn rzObjectAt(uv: vec2f) -> u32 { return 0u; }
41
+ fn rzMaterialAt(uv: vec2f) -> u32 { return 0u; }
42
+ `
43
+ }
44
+ return /* wgsl */ `
45
+ @group(${group}) @binding(${binding}) var _rzIdTex: texture_multisampled_2d<u32>;
46
+
47
+ /** Which OBJECT drew this pixel — compare against rzSubjectId(i). 0 = nothing. */
48
+ fn rzObjectAt(uv: vec2f) -> u32 {
49
+ let sz = vec2f(textureDimensions(_rzIdTex));
50
+ let p = vec2<i32>(clamp(uv, vec2f(0.0), vec2f(1.0)) * sz);
51
+ return textureLoad(_rzIdTex, clamp(p, vec2<i32>(0), vec2<i32>(sz) - vec2<i32>(1)), 0).y;
52
+ }
53
+
54
+ /** Which MATERIAL drew it, within that object. 0 = nothing. */
55
+ fn rzMaterialAt(uv: vec2f) -> u32 {
56
+ let sz = vec2f(textureDimensions(_rzIdTex));
57
+ let p = vec2<i32>(clamp(uv, vec2f(0.0), vec2f(1.0)) * sz);
58
+ return textureLoad(_rzIdTex, clamp(p, vec2<i32>(0), vec2<i32>(sz) - vec2<i32>(1)), 0).x;
59
+ }
60
+ `
61
+ }
@@ -29,6 +29,7 @@ import { audioApi } from "./audio-api"
29
29
  import { midiApi } from "./midi-api"
30
30
  import { lyricsApi } from "./lyrics-api"
31
31
  import { clockApi, trailSlotsApi, viewportApi } from "./passes/hosted-api"
32
+ import { idApi } from "./id-api"
32
33
 
33
34
  /** Floats before the first record. One is the count; the rest keep the records
34
35
  * vec4-aligned, which is what lets a future pass read them as vec4s. */
@@ -157,6 +158,9 @@ ${clockApi("_rzLightU.x", "0.0")}
157
158
  // and viewU[6].w have the same source.
158
159
  ${viewportApi("viewU[6].w")}
159
160
  ${trailSlotsApi(cast.trailCount)}
161
+ // The id accessors, stubbed: this module cannot read an attachment the
162
+ // scene pass writes. See id-api.ts — the author's whole file compiles here.
163
+ ${idApi(false, 0, 0)}
160
164
  ${wgsl}
161
165
 
162
166
  @compute @workgroup_size(64)
@@ -4,6 +4,7 @@ import { CAST_API } from "../cast-api"
4
4
  import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api"
5
5
  import { EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES } from "../cast-layout"
6
6
  import { audioApi } from "../audio-api"
7
+ import { idApi } from "../id-api"
7
8
  import { lyricsApi, lyricsTextApi } from "../lyrics-api"
8
9
  import { midiApi } from "../midi-api"
9
10
  import { gridReadApi } from "./grid"
@@ -643,50 +644,6 @@ export function buildCompositeShader(effect?: CompositeEffectSource | null): str
643
644
  * read — so an effect cannot tell it moved; it is simply asked half as often
644
645
  * in each direction.
645
646
  */
646
- /**
647
- * Reading the scene's id attachment from a field effect — the consumer the MRT
648
- * work exists for.
649
- *
650
- * A field effect covers the whole screen and has no idea what it is drawing
651
- * over. These make it addressable: mask a glow to one character, dissolve one
652
- * material, outline the thing someone selected. Without them the id buffer is
653
- * written every frame and read by nobody.
654
- *
655
- * MULTISAMPLED AND UNRESOLVED, so it is read the way it is written —
656
- * textureLoad of sample 0, the same rule linearDepth already follows. An
657
- * averaged id belongs to nothing.
658
- *
659
- * When ids are OFF the buffer does not exist, so the accessors are still
660
- * DECLARED and answer 0 — the reserved nothing. An effect that masks by id then
661
- * masks nothing at all, which is a scene that renders rather than a shader that
662
- * will not compile.
663
- */
664
- function idApi(on: boolean, group: number, binding: number): string {
665
- if (!on) {
666
- return /* wgsl */ `
667
- fn rzObjectAt(uv: vec2f) -> u32 { return 0u; }
668
- fn rzMaterialAt(uv: vec2f) -> u32 { return 0u; }
669
- `
670
- }
671
- return /* wgsl */ `
672
- @group(${group}) @binding(${binding}) var _rzIdTex: texture_multisampled_2d<u32>;
673
-
674
- /** Which OBJECT drew this pixel — compare against rzSubjectId(i). 0 = nothing. */
675
- fn rzObjectAt(uv: vec2f) -> u32 {
676
- let sz = vec2f(textureDimensions(_rzIdTex));
677
- let p = vec2<i32>(clamp(uv, vec2f(0.0), vec2f(1.0)) * sz);
678
- return textureLoad(_rzIdTex, clamp(p, vec2<i32>(0), vec2<i32>(sz) - vec2<i32>(1)), 0).y;
679
- }
680
-
681
- /** Which MATERIAL drew it, within that object. 0 = nothing. */
682
- fn rzMaterialAt(uv: vec2f) -> u32 {
683
- let sz = vec2f(textureDimensions(_rzIdTex));
684
- let p = vec2<i32>(clamp(uv, vec2f(0.0), vec2f(1.0)) * sz);
685
- return textureLoad(_rzIdTex, clamp(p, vec2<i32>(0), vec2<i32>(sz) - vec2<i32>(1)), 0).x;
686
- }
687
- `
688
- }
689
-
690
647
  export function buildFieldShader(effect: CompositeEffectSource): string {
691
648
  const bgLine = effect.hasBackground
692
649
  ? "out.bg = clamp(background(dir, uv, _rzFieldClock.x), vec4f(0.0), vec4f(1.0));"
@@ -5,6 +5,7 @@ import { midiApi } from "../midi-api"
5
5
  import { EFFECT_SCENE_API } from "./composite"
6
6
  import { type CastLayout } from "./particles"
7
7
  import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api"
8
+ import { idApi } from "../id-api"
8
9
 
9
10
  // A persistent grid an effect can step and read: the one thing an effect could
10
11
  // not have before, which is MEMORY.
@@ -150,6 +151,9 @@ fn rzGridPrev(uv: vec2f) -> vec4f {
150
151
  /* wgsl */ `
151
152
  fn rzGrid(uv: vec2f) -> vec4f { return rzGridPrev(uv); }
152
153
  ` +
154
+ // Stubbed: this module cannot read an attachment the scene pass writes — see
155
+ // id-api.ts. The author's whole file compiles here, so the names must exist.
156
+ idApi(false, 0, 0) +
153
157
  "\n// ── user effect (setEffect) ──\n" +
154
158
  wgsl +
155
159
  /* wgsl */ `
@@ -6,6 +6,7 @@ import { midiApi } from "../midi-api"
6
6
  import { CAST_API } from "../cast-api"
7
7
  import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api"
8
8
  import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract"
9
+ import { idApi } from "../id-api"
9
10
  // GPU particles for user effects: a compute step and an instanced quad draw.
10
11
  //
11
12
  // Its own shader MODULE rather than more source spliced into composite.ts, for
@@ -173,6 +174,9 @@ export function buildParticleComputeShader(src: ParticleSource, cast: CastLayout
173
174
  midiApi(0, 5) +
174
175
  lyricsApi(0, 6) +
175
176
  PRELUDE +
177
+ // Stubbed: this module cannot read an attachment the scene pass writes — see
178
+ // id-api.ts. The author's whole file compiles here, so the names must exist.
179
+ idApi(false, 0, 0) +
176
180
  "\n// ── user effect ──\n" +
177
181
  src.wgsl +
178
182
  /* wgsl */ `
@@ -232,6 +236,9 @@ override ADDITIVE: bool = ${src.blend === "additive" ? "true" : "false"};\n` +
232
236
  midiApi(0, 5) +
233
237
  lyricsApi(0, 6) +
234
238
  PRELUDE +
239
+ // Stubbed: this module cannot read an attachment the scene pass writes — see
240
+ // id-api.ts. The author's whole file compiles here, so the names must exist.
241
+ idApi(false, 0, 0) +
235
242
  "\n// ── user effect ──\n" +
236
243
  src.wgsl +
237
244
  /* wgsl */ `
@@ -6,6 +6,7 @@ import { midiApi } from "../midi-api"
6
6
  import { CAST_API } from "../cast-api"
7
7
  import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api"
8
8
  import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract"
9
+ import { idApi } from "../id-api"
9
10
  // Ribbons along a bone's recorded path, drawn as geometry.
10
11
  //
11
12
  // This is the effect the whole geometry path was built for. As a fullscreen
@@ -232,6 +233,9 @@ fn rzTurnRadius(a: vec3f, b: vec3f, c: vec3f) -> f32 {
232
233
  audioApi(0, 4) +
233
234
  midiApi(0, 5) +
234
235
  lyricsApi(0, 6) +
236
+ // Stubbed: this module cannot read an attachment the scene pass writes — see
237
+ // id-api.ts. The author's whole file compiles here, so the names must exist.
238
+ idApi(false, 0, 0) +
235
239
  "\n// ── user effect ──\n" +
236
240
  src.wgsl +
237
241
  /* wgsl */ `