reze-engine 0.51.0 → 0.52.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 (44) hide show
  1. package/dist/effect-schedule.d.ts +67 -0
  2. package/dist/effect-schedule.d.ts.map +1 -0
  3. package/dist/effect-schedule.js +96 -0
  4. package/dist/engine.d.ts +113 -17
  5. package/dist/engine.d.ts.map +1 -1
  6. package/dist/engine.js +324 -169
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +8 -0
  10. package/dist/shaders/anchor-table.d.ts +1 -1
  11. package/dist/shaders/anchor-table.js +3 -3
  12. package/dist/shaders/cast-api.d.ts +1 -1
  13. package/dist/shaders/cast-api.js +2 -2
  14. package/dist/shaders/directives.d.ts +73 -0
  15. package/dist/shaders/directives.d.ts.map +1 -0
  16. package/dist/shaders/directives.js +238 -0
  17. package/dist/shaders/lights.d.ts +0 -10
  18. package/dist/shaders/lights.d.ts.map +1 -1
  19. package/dist/shaders/lights.js +11 -19
  20. package/dist/shaders/passes/composite.d.ts +5 -5
  21. package/dist/shaders/passes/composite.d.ts.map +1 -1
  22. package/dist/shaders/passes/composite.js +12 -5
  23. package/dist/shaders/passes/grid.d.ts +0 -2
  24. package/dist/shaders/passes/grid.d.ts.map +1 -1
  25. package/dist/shaders/passes/grid.js +0 -9
  26. package/dist/shaders/passes/particles.d.ts +0 -6
  27. package/dist/shaders/passes/particles.d.ts.map +1 -1
  28. package/dist/shaders/passes/particles.js +15 -19
  29. package/dist/shaders/passes/scene-contract.d.ts +1 -1
  30. package/dist/shaders/passes/trails.d.ts.map +1 -1
  31. package/dist/shaders/passes/trails.js +9 -4
  32. package/package.json +2 -2
  33. package/src/effect-schedule.ts +120 -0
  34. package/src/engine.ts +323 -134
  35. package/src/index.ts +15 -0
  36. package/src/shaders/anchor-table.ts +3 -3
  37. package/src/shaders/cast-api.ts +2 -2
  38. package/src/shaders/directives.ts +289 -0
  39. package/src/shaders/lights.ts +11 -19
  40. package/src/shaders/passes/composite.ts +13 -6
  41. package/src/shaders/passes/grid.ts +0 -9
  42. package/src/shaders/passes/particles.ts +15 -21
  43. package/src/shaders/passes/scene-contract.ts +1 -1
  44. package/src/shaders/passes/trails.ts +9 -4
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { parseMidi } from "./midi-loader";
4
4
  export { parseHDR, type HdrImage } from "./hdr";
5
5
  export { LYRIC_ATLAS_MAX_H, LYRIC_ATLAS_MAX_W, parseLRC, type LyricLine, type LyricRect } from "./shaders/lyrics-api";
6
6
  export { sampleParamTrack, type ParamKey, type ParamValue } from "./param-track";
7
+ export { effectState, type EffectWindow, type EffectState } from "./effect-schedule";
7
8
  export { compileGraph, validateGraph, assignStyleSlots, type CompileOptions, type CompileResult, type StyleSlot, } from "./graph/compile";
8
9
  export type { ShaderGraph, GraphNode, GraphLink, ExposedParam, SocketValue, Diagnostic, } from "./graph/schema";
9
10
  export { NODE_REGISTRY, type NodeSpec, type SockT } from "./graph/registry";
@@ -19,6 +20,7 @@ export { STOCKINGS_GRAPH } from "./graph/presets/stockings";
19
20
  export { EYE_GRAPH } from "./graph/presets/eye";
20
21
  export { FACE_GRAPH } from "./graph/presets/face";
21
22
  export { UNLIT_GRAPH } from "./graph/presets/unlit";
23
+ export { parseDirectives, stripDirectives, DIRECTIVE_LINE, DIRECTIVE_NOTE, type EffectDirectives, type EffectParamDecl, } from "./shaders/directives";
22
24
  export { Model, MATERIAL_MORPH_MULTIPLY, MATERIAL_MORPH_ADD, type ClipEventInfo, type RootMotionProfile, type Morph, type Morphing, type BoneMorphOffset, type MaterialMorphOffset, type UvMorphOffset, } from "./model";
23
25
  export { Vec3, Quat, Mat4, easeInOut, type EulerOrder } from "./math";
24
26
  export type { AnimationClip, AnimationPlayOptions, AnimationProgress, BlendEntry, BoneKeyframe, IkKeyframe, MorphKeyframe, BoneInterpolation, ControlPoint, } from "./animation";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,qBAAqB,EACrB,8BAA8B,EAC9B,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,QAAQ,GACd,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AACvG,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAGzC,OAAO,EAAE,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE/C,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAIrH,OAAO,EAAE,gBAAgB,EAAE,KAAK,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAA;AAIhF,OAAO,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpF,OAAO,EACL,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,SAAS,GACf,MAAM,iBAAiB,CAAA;AACxB,YAAY,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACT,YAAY,EACZ,WAAW,EACX,UAAU,GACX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC3E,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC7G,YAAY,EACV,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAInD,OAAO,EACL,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,EACd,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,KAAK,EACL,uBAAuB,EACvB,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,KAAK,EACV,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,aAAa,GACnB,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAA;AACrE,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,UAAU,EACV,YAAY,EACZ,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,qBAAqB,EAAE,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAC5H,OAAO,EACL,GAAG,EACH,iBAAiB,EACjB,wBAAwB,EACxB,mCAAmC,GACpC,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,SAAS,EAAE,4BAA4B,EAAE,KAAK,cAAc,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAA;AACzG,OAAO,EAAE,SAAS,EAAE,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA"}
package/dist/index.js CHANGED
@@ -10,6 +10,10 @@ export { LYRIC_ATLAS_MAX_H, LYRIC_ATLAS_MAX_W, parseLRC } from "./shaders/lyrics
10
10
  // wants to draw a track, or scrub one, should read the same curve the engine
11
11
  // plays rather than reimplementing it a second time.
12
12
  export { sampleParamTrack } from "./param-track";
13
+ // The strip an effect is scheduled by, and the pure evaluator behind it —
14
+ // exported so a caller can draw a lane against the same numbers the engine
15
+ // renders from, rather than a second copy of the ramp maths.
16
+ export { effectState } from "./effect-schedule";
13
17
  export { compileGraph, validateGraph, assignStyleSlots, } from "./graph/compile";
14
18
  export { NODE_REGISTRY } from "./graph/registry";
15
19
  export { RENDER_CLASSES } from "./graph/render-class";
@@ -23,6 +27,10 @@ export { STOCKINGS_GRAPH } from "./graph/presets/stockings";
23
27
  export { EYE_GRAPH } from "./graph/presets/eye";
24
28
  export { FACE_GRAPH } from "./graph/presets/face";
25
29
  export { UNLIT_GRAPH } from "./graph/presets/unlit";
30
+ // What an effect declares, and the source with those lines blanked. Exported
31
+ // because a host builds parameter controls from the declarations and needs the
32
+ // same answer the engine got — two parsers is how they disagree.
33
+ export { parseDirectives, stripDirectives, DIRECTIVE_LINE, DIRECTIVE_NOTE, } from "./shaders/directives";
26
34
  export { Model, MATERIAL_MORPH_MULTIPLY, MATERIAL_MORPH_ADD, } from "./model";
27
35
  export { Vec3, Quat, Mat4, easeInOut } from "./math";
28
36
  export { LocomotionController, } from "./locomotion";
@@ -35,7 +35,7 @@ export declare function buildAnchorTable(requests: AnchorRequest[][], max: numbe
35
35
  * TRAILED anchor, so its instance index counts 0,1,2… over trailed anchors only
36
36
  * — while the cast buffer is addressed by DECLARATION slot. Those coincide
37
37
  * exactly when every anchor is trailed, which is true of all 14 library effects
38
- * and is why this stayed latent: declare `@anchor 頭` then `@anchor 左手首 trail`
38
+ * and is why this stayed latent: declare `#anchor 頭` then `#anchor 左手首 trail`
39
39
  * and ribbon 0 asked for the trail of 頭, which has none, so the ribbon silently
40
40
  * did not draw.
41
41
  *
@@ -73,7 +73,7 @@ export function buildAnchorTable(requests, max) {
73
73
  * TRAILED anchor, so its instance index counts 0,1,2… over trailed anchors only
74
74
  * — while the cast buffer is addressed by DECLARATION slot. Those coincide
75
75
  * exactly when every anchor is trailed, which is true of all 14 library effects
76
- * and is why this stayed latent: declare `@anchor 頭` then `@anchor 左手首 trail`
76
+ * and is why this stayed latent: declare `#anchor 頭` then `#anchor 左手首 trail`
77
77
  * and ribbon 0 asked for the trail of 頭, which has none, so the ribbon silently
78
78
  * did not draw.
79
79
  *
@@ -108,7 +108,7 @@ ${cases}
108
108
  * folds away.
109
109
  */
110
110
  export function anchorAliasWgsl(alias) {
111
- // NO @anchor AT ALL. Every slot is unmapped, and rzAnchor() must report
111
+ // NO #anchor AT ALL. Every slot is unmapped, and rzAnchor() must report
112
112
  // .valid false for all of them.
113
113
  //
114
114
  // This used to fall through to the identity branch below — `[].every()` is
@@ -120,7 +120,7 @@ export function anchorAliasWgsl(alias) {
120
120
  // reading.
121
121
  if (alias.length === 0) {
122
122
  return `
123
- /** No @anchor in this effect: every slot is unmapped, so rzAnchor() is invalid
123
+ /** No #anchor in this effect: every slot is unmapped, so rzAnchor() is invalid
124
124
  * rather than reading whichever bone another effect declared first. */
125
125
  fn _rzSlot(local: i32) -> i32 { return -1; }
126
126
  `;
@@ -1,2 +1,2 @@
1
- export declare const CAST_API = "\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";
1
+ export declare const CAST_API = "\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";
2
2
  //# sourceMappingURL=cast-api.d.ts.map
@@ -90,7 +90,7 @@ fn rzSubject(i: i32) -> RzSubject {
90
90
  /**
91
91
  * Where a named bone is, this frame.
92
92
  *
93
- * The slot is the author's own: the Nth @anchor in their file, in the order
93
+ * The slot is the author's own: the Nth #anchor in their file, in the order
94
94
  * they wrote them. _rzSlot turns that into the scene's address, which is what
95
95
  * keeps two effects that both anchor to a wrist from reading each other's.
96
96
  */
@@ -113,7 +113,7 @@ fn rzAnchor(subject: i32, slot: i32) -> RzAnchor {
113
113
  *
114
114
  * Bounded by the anchor cap, NOT by how many anchors asked for a trail. Those
115
115
  * are different index spaces: storage is addressed by anchor slot, so an
116
- * untrailed @anchor followed by a trailed one put the trail at index 1 with a
116
+ * untrailed #anchor followed by a trailed one put the trail at index 1 with a
117
117
  * bound of 1 and rzTrail returned zero — a ribbon that silently did not draw.
118
118
  */
119
119
  fn rzTrailCount(subject: i32, slot: i32) -> i32 {
@@ -0,0 +1,73 @@
1
+ /** A knob an effect exposes, for a host to build a control from. */
2
+ export type EffectParamDecl = {
3
+ name: string;
4
+ kind: "float" | "color" | "vec3";
5
+ /** Numbers for float/vec3; `#rrggbb` for a colour, which the host converts. */
6
+ value: number | [number, number, number] | string;
7
+ /** float only, and only when the author gave a range. */
8
+ min?: number;
9
+ max?: number;
10
+ };
11
+ export type EffectDirectives = {
12
+ /** Bones this effect follows, in declaration order — slot 0 is the first. */
13
+ anchors: {
14
+ bone: string;
15
+ trail: boolean;
16
+ }[];
17
+ params: EffectParamDecl[];
18
+ /** Field layer: 0 full, 1 half. Full unless `#halfres` says otherwise. */
19
+ fieldLayer: 0 | 1;
20
+ /** The field layer composites additively rather than over. */
21
+ additiveLayer: boolean;
22
+ /** Particle blend, which is a different axis from the field layer's. */
23
+ particleBlend: "alpha" | "additive";
24
+ particles: number;
25
+ lights: number;
26
+ grid: number;
27
+ bloom: boolean;
28
+ /** This effect takes the cast apart — the host reads the timing. */
29
+ dissolve: boolean;
30
+ /**
31
+ * How long ONE firing of this effect lasts, in seconds. 0 = undeclared.
32
+ *
33
+ * An effect is one of two things, and only its author knows which. A HIT has
34
+ * an arc — a circle flares, peaks and is gone — and its length is a fact
35
+ * about it, the way a video clip's length is a fact about the file. An
36
+ * AMBIENT effect (stars, fog, rain) has no length at all; it is a condition
37
+ * the scene is in.
38
+ *
39
+ * Declaring it is what lets a host place the effect instead of making someone
40
+ * construct it: dropping a hit on the timeline gives a strip already the
41
+ * right size, and an effect that declares nothing spans the scene. Every
42
+ * timeline works this way — a clip arrives at its own duration.
43
+ */
44
+ duration: number;
45
+ };
46
+ /** A line that declares something, and what it declares. Exported because an
47
+ * editor highlights by the same rule the parser reads by — a highlighter with
48
+ * its own idea of what counts is one that paints a line as configuration that
49
+ * the engine then ignores. */
50
+ export declare const DIRECTIVE_LINE: RegExp;
51
+ /**
52
+ * Split a directive's arguments from a trailing note.
53
+ *
54
+ * `#anchor 左手首 trail — her sword hand` is one line doing two jobs, and
55
+ * refusing it is what made the old spelling dangerous: the note is the natural
56
+ * thing to write, so it has to be the accepted thing to write.
57
+ */
58
+ export declare const DIRECTIVE_NOTE: RegExp;
59
+ export type DirectiveResult = {
60
+ directives: EffectDirectives;
61
+ errors: string[];
62
+ };
63
+ /** Read every declaration in a source. Errors name the line, one-based. */
64
+ export declare function parseDirectives(wgsl: string): DirectiveResult;
65
+ /**
66
+ * The source as the compiler should see it: every directive line blanked.
67
+ *
68
+ * Blanked rather than removed so line numbers survive — a diagnostic points at
69
+ * the line the author is looking at, which is the whole reason the engine
70
+ * rebases them in the first place.
71
+ */
72
+ export declare function stripDirectives(wgsl: string): string;
73
+ //# sourceMappingURL=directives.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directives.d.ts","sourceRoot":"","sources":["../../src/shaders/directives.ts"],"names":[],"mappings":"AA8BA,oEAAoE;AACpE,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;IAChC,+EAA+E;IAC/E,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IACjD,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,6EAA6E;IAC7E,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;IAC3C,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,0EAA0E;IAC1E,UAAU,EAAE,CAAC,GAAG,CAAC,CAAA;IACjB,8DAA8D;IAC9D,aAAa,EAAE,OAAO,CAAA;IACtB,wEAAwE;IACxE,aAAa,EAAE,OAAO,GAAG,UAAU,CAAA;IACnC,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAA;IACjB;;;;;;;;;;;;;OAaG;IACH,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAiBD;;;+BAG+B;AAC/B,eAAO,MAAM,cAAc,QAAmC,CAAA;AAG9D;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,QAA8B,CAAA;AAmBzD,MAAM,MAAM,eAAe,GAAG;IAAE,UAAU,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAEhF,2EAA2E;AAC3E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAqJ7D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKpD"}
@@ -0,0 +1,238 @@
1
+ // What an effect DECLARES about itself — parsed once, in one place.
2
+ //
3
+ // An effect file is WGSL plus a handful of lines that configure how the engine
4
+ // mounts it: which resolution it draws at, how it blends, which bones it
5
+ // follows, what knobs it exposes. Those lines are not comments. They decide
6
+ // whether the file gets what it asks for, and the failure they used to have was
7
+ // the worst kind available.
8
+ //
9
+ // WHY `#` AND NOT `// @`. The old spelling put directives inside comments,
10
+ // which meant two things. Authors read them as comments and wrote notes on the
11
+ // same line — every parser was anchored to end-of-line, so the note silently
12
+ // unmade the directive, and three shipped effects ran at half the resolution
13
+ // their own first line said they needed while a fourth quietly stopped being
14
+ // additive. Nothing failed and nothing was reported, because nothing had been
15
+ // declared to fail. And it could not be fixed by being strict: a comment
16
+ // beginning with `@` is a legitimate thing to write, so an unrecognised one can
17
+ // only ever be a warning.
18
+ //
19
+ // WGSL has no `#` syntax of its own. A line starting with `#` is therefore
20
+ // unambiguously ours, an unknown one is an ERROR rather than a guess, and the
21
+ // sigil is the one every shader author already reads as "directive" from
22
+ // `#pragma`. The cost usually quoted for this — that the file stops being valid
23
+ // WGSL — is not a cost we pay: an effect calls rzSubject, rzTrail and reads
24
+ // `params`, none of which exist until the engine splices them in, so these
25
+ // files have never compiled anywhere else.
26
+ //
27
+ // STRIPPED BY BLANKING, not by deleting: the compiler sees an empty line where
28
+ // each directive was, so every diagnostic's line number still points at the
29
+ // line the author is looking at.
30
+ /** Every directive, with how many words follow it. `rest` means free-form. */
31
+ const SPEC = {
32
+ anchor: "rest",
33
+ param: "rest",
34
+ halfres: 0,
35
+ layer: 1,
36
+ blend: 1,
37
+ particles: 1,
38
+ lights: 1,
39
+ grid: 1,
40
+ bloom: 0,
41
+ dissolve: 0,
42
+ duration: 1,
43
+ };
44
+ /** A line that declares something, and what it declares. Exported because an
45
+ * editor highlights by the same rule the parser reads by — a highlighter with
46
+ * its own idea of what counts is one that paints a line as configuration that
47
+ * the engine then ignores. */
48
+ export const DIRECTIVE_LINE = /^[ \t]*#([a-zA-Z]+)[ \t]*(.*)$/;
49
+ const LINE = DIRECTIVE_LINE;
50
+ /**
51
+ * Split a directive's arguments from a trailing note.
52
+ *
53
+ * `#anchor 左手首 trail — her sword hand` is one line doing two jobs, and
54
+ * refusing it is what made the old spelling dangerous: the note is the natural
55
+ * thing to write, so it has to be the accepted thing to write.
56
+ */
57
+ export const DIRECTIVE_NOTE = /(?:^|\s)(?:—|--|\/\/|#)\s/;
58
+ function argsOf(rest) {
59
+ // `^` as well as `\s`: the tag's own trailing space is eaten by LINE, so a
60
+ // note can begin at the very first character — which is what
61
+ // `#halfres — glyph edges` looks like by the time it reaches here.
62
+ const note = rest.search(DIRECTIVE_NOTE);
63
+ return (note >= 0 ? rest.slice(0, note) : rest).trim().split(/\s+/).filter(Boolean);
64
+ }
65
+ /** A number, or null. Empty is NULL, not zero: `Number("")` is 0, so a missing
66
+ * default silently became a real one — an author who wrote `#param float D`
67
+ * and meant to finish the line got a knob quietly pinned at zero. */
68
+ const num = (s) => {
69
+ if (!s || !s.trim())
70
+ return null;
71
+ const v = Number(s);
72
+ return Number.isFinite(v) ? v : null;
73
+ };
74
+ /** Read every declaration in a source. Errors name the line, one-based. */
75
+ export function parseDirectives(wgsl) {
76
+ const d = {
77
+ anchors: [],
78
+ params: [],
79
+ // FULL unless asked otherwise — the default is what an author gets for
80
+ // saying nothing, so it has to be the answer that cannot silently ruin an
81
+ // effect. `#halfres` is a claim about being cheap, which is a claim only
82
+ // the author can make.
83
+ fieldLayer: 0,
84
+ additiveLayer: false,
85
+ particleBlend: "alpha",
86
+ particles: 0,
87
+ lights: 0,
88
+ grid: 0,
89
+ bloom: false,
90
+ dissolve: false,
91
+ duration: 0,
92
+ };
93
+ const errors = [];
94
+ const lines = wgsl.split("\n");
95
+ lines.forEach((line, i) => {
96
+ const m = LINE.exec(line);
97
+ if (!m)
98
+ return;
99
+ const at = `line ${i + 1}`;
100
+ const tag = m[1].toLowerCase();
101
+ if (!(tag in SPEC)) {
102
+ errors.push(`${at}: #${m[1]} is not a directive. Known: ${Object.keys(SPEC).map((k) => `#${k}`).join(", ")}`);
103
+ return;
104
+ }
105
+ const args = argsOf(m[2]);
106
+ const want = SPEC[tag];
107
+ if (want !== "rest" && args.length !== want) {
108
+ errors.push(`${at}: #${tag} takes ${want} argument${want === 1 ? "" : "s"}, got ${args.length}`);
109
+ return;
110
+ }
111
+ switch (tag) {
112
+ case "anchor": {
113
+ if (args.length < 1 || args.length > 2 || (args[1] && args[1] !== "trail")) {
114
+ errors.push(`${at}: #anchor takes a bone name and optionally the word "trail"`);
115
+ return;
116
+ }
117
+ d.anchors.push({ bone: args[0], trail: args[1] === "trail" });
118
+ return;
119
+ }
120
+ case "param": {
121
+ const [kind, name, ...rest] = args;
122
+ if (!name || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
123
+ errors.push(`${at}: #param needs a WGSL identifier for a name`);
124
+ return;
125
+ }
126
+ if (kind === "color") {
127
+ if (!/^#[0-9a-fA-F]{6}$/.test(rest[0] ?? "")) {
128
+ errors.push(`${at}: #param color ${name} needs a default like #3b82f6`);
129
+ return;
130
+ }
131
+ d.params.push({ name, kind: "color", value: rest[0] });
132
+ return;
133
+ }
134
+ if (kind === "vec3") {
135
+ const v = rest.slice(0, 3).map(num);
136
+ if (v.length !== 3 || v.some((x) => x === null)) {
137
+ errors.push(`${at}: #param vec3 ${name} needs three numbers`);
138
+ return;
139
+ }
140
+ d.params.push({ name, kind: "vec3", value: v });
141
+ return;
142
+ }
143
+ if (kind === "float") {
144
+ const v = num(rest[0]);
145
+ if (v === null) {
146
+ errors.push(`${at}: #param float ${name} needs a default`);
147
+ return;
148
+ }
149
+ const lo = num(rest[1]);
150
+ const hi = num(rest[2]);
151
+ // A range is optional and all-or-nothing: half of one is a slider
152
+ // with an end nobody chose.
153
+ if ((rest[1] !== undefined) !== (rest[2] !== undefined) || (rest[1] !== undefined && (lo === null || hi === null))) {
154
+ errors.push(`${at}: #param float ${name} takes both a min and a max, or neither`);
155
+ return;
156
+ }
157
+ d.params.push({ name, kind: "float", value: v, ...(lo !== null && hi !== null ? { min: lo, max: hi } : {}) });
158
+ return;
159
+ }
160
+ errors.push(`${at}: #param kind must be float, color or vec3`);
161
+ return;
162
+ }
163
+ case "halfres":
164
+ d.fieldLayer = 1;
165
+ return;
166
+ case "layer":
167
+ if (args[0] !== "additive") {
168
+ errors.push(`${at}: #layer takes "additive" — over is the default`);
169
+ return;
170
+ }
171
+ d.additiveLayer = true;
172
+ return;
173
+ case "blend":
174
+ if (args[0] !== "additive") {
175
+ errors.push(`${at}: #blend takes "additive" — alpha is the default`);
176
+ return;
177
+ }
178
+ d.particleBlend = "additive";
179
+ return;
180
+ case "bloom":
181
+ d.bloom = true;
182
+ return;
183
+ case "dissolve":
184
+ d.dissolve = true;
185
+ return;
186
+ case "duration": {
187
+ // SECONDS, like every other time a directive states (see #dissolve).
188
+ // The document above works in frames and converts once; an author
189
+ // writing a shader is thinking about how long a flare takes, not about
190
+ // MMD's frame rate.
191
+ const n = num(args[0]);
192
+ if (n === null || n <= 0) {
193
+ errors.push(`${at}: #duration takes a length in seconds`);
194
+ return;
195
+ }
196
+ d.duration = n;
197
+ return;
198
+ }
199
+ default: {
200
+ // The three that take a count.
201
+ const n = num(args[0]);
202
+ if (n === null || n < 0) {
203
+ errors.push(`${at}: #${tag} takes a number`);
204
+ return;
205
+ }
206
+ if (tag === "particles")
207
+ d.particles = n;
208
+ else if (tag === "lights")
209
+ d.lights = n;
210
+ else if (tag === "grid")
211
+ d.grid = n;
212
+ return;
213
+ }
214
+ }
215
+ });
216
+ // Duplicates are an author editing one line and forgetting another; last-wins
217
+ // is a coin toss they never see resolved.
218
+ const names = new Set();
219
+ for (const p of d.params) {
220
+ if (names.has(p.name))
221
+ errors.push(`#param ${p.name} is declared twice`);
222
+ names.add(p.name);
223
+ }
224
+ return { directives: d, errors };
225
+ }
226
+ /**
227
+ * The source as the compiler should see it: every directive line blanked.
228
+ *
229
+ * Blanked rather than removed so line numbers survive — a diagnostic points at
230
+ * the line the author is looking at, which is the whole reason the engine
231
+ * rebases them in the first place.
232
+ */
233
+ export function stripDirectives(wgsl) {
234
+ return wgsl
235
+ .split("\n")
236
+ .map((line) => (LINE.test(line) ? "" : line))
237
+ .join("\n");
238
+ }
@@ -12,16 +12,6 @@ export declare const LIGHT_STRIDE = 8;
12
12
  export declare const MAX_LIGHTS = 16;
13
13
  /** Floats in the whole buffer. */
14
14
  export declare const LIGHTS_FLOATS: number;
15
- /**
16
- * `// @lights 3` — how many lights this effect emits.
17
- *
18
- * Declared, like every other mount: what the file says is what gets allocated,
19
- * so an effect that emits none costs no slots and nobody pays for a cap they
20
- * did not ask for. Clamped rather than rejected, the same choice `@particles`
21
- * makes — an author asking for a hundred gets the most the engine will give and
22
- * a scene that still runs.
23
- */
24
- export declare function parseLightCount(wgsl: string, max: number): number;
25
15
  /**
26
16
  * The RzLight struct, declared in EVERY module a user's source is spliced into.
27
17
  *
@@ -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,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIjE;AAED;;;;;;;;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,CAqER;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":"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"}
@@ -42,21 +42,6 @@ export const LIGHT_STRIDE = 8;
42
42
  export const MAX_LIGHTS = 16;
43
43
  /** Floats in the whole buffer. */
44
44
  export const LIGHTS_FLOATS = LIGHT_HEADER + MAX_LIGHTS * LIGHT_STRIDE;
45
- /**
46
- * `// @lights 3` — how many lights this effect emits.
47
- *
48
- * Declared, like every other mount: what the file says is what gets allocated,
49
- * so an effect that emits none costs no slots and nobody pays for a cap they
50
- * did not ask for. Clamped rather than rejected, the same choice `@particles`
51
- * makes — an author asking for a hundred gets the most the engine will give and
52
- * a scene that still runs.
53
- */
54
- export function parseLightCount(wgsl, max) {
55
- const m = /^\s*\/\/\s*@lights\s+(\d+)\s*$/m.exec(wgsl);
56
- if (!m)
57
- return 0;
58
- return Math.max(1, Math.min(max, parseInt(m[1], 10)));
59
- }
60
45
  /**
61
46
  * The RzLight struct, declared in EVERY module a user's source is spliced into.
62
47
  *
@@ -134,8 +119,8 @@ export function buildLightEmitShader(wgsl, sceneApi, cast) {
134
119
  // read_write HERE and read-only in the material shaders. Different passes, so
135
120
  // the two never coexist: this compute runs before the scene pass that reads it.
136
121
  @group(0) @binding(0) var<storage, read_write> _rzLightsOut: array<f32>;
137
- // (time, base slot, count, _) — see buildLightEmitShader on why the base is
138
- // here and not in the text.
122
+ // (time, base slot, count, weight) — see buildLightEmitShader on why the base
123
+ // is here and not in the text.
139
124
  @group(0) @binding(1) var<uniform> _rzLightU: vec4f;
140
125
  // The camera block and the cast — the two buffers the scene API reads. Same
141
126
  // contents the field and grid modules bind, so an effect's lightEmit sees the
@@ -185,12 +170,19 @@ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
185
170
  let finite = l.pos.x == l.pos.x && l.pos.y == l.pos.y && l.pos.z == l.pos.z &&
186
171
  l.radius == l.radius && l.intensity == l.intensity &&
187
172
  l.color.x == l.color.x && l.color.y == l.color.y && l.color.z == l.color.z;
188
- let c = select(vec3f(0.0), max(l.color * l.intensity, vec3f(0.0)), finite);
173
+ // Weight rides along with the sanitisation, which is already the one place
174
+ // this buffer is written: a lamp at half weight is half as bright, and one
175
+ // at zero never reaches here because the dispatch is skipped.
176
+ let c = select(vec3f(0.0), max(l.color * l.intensity, vec3f(0.0)), finite) * _rzLightU.w;
189
177
  let b = ${LIGHT_HEADER}u + (u32(_rzLightU.y) + i) * ${LIGHT_STRIDE}u;
190
178
  _rzLightsOut[b] = select(0.0, l.pos.x, finite);
191
179
  _rzLightsOut[b + 1u] = select(0.0, l.pos.y, finite);
192
180
  _rzLightsOut[b + 2u] = select(0.0, l.pos.z, finite);
193
- _rzLightsOut[b + 3u] = select(0.0, max(l.radius, 0.0), finite);
181
+ // Radius goes to zero when the effect is OFF, and is left alone at every
182
+ // weight above it. Scaling it with the fade would shrink a dimming lamp's
183
+ // reach, which is a different thing than dimming it; zeroing it at nothing
184
+ // is what lets a material's distance cull drop the slot entirely.
185
+ _rzLightsOut[b + 3u] = select(0.0, max(l.radius, 0.0), finite && _rzLightU.w > 0.0);
194
186
  // Colour carries intensity, exactly as the CPU writer stores it — one product,
195
187
  // one place, so the two producers cannot disagree about what a slot means.
196
188
  _rzLightsOut[b + 4u] = c.x;
@@ -34,13 +34,13 @@
34
34
  /**
35
35
  * The bones an effect asked for, in declaration order — the slots rzAnchor reads.
36
36
  *
37
- * // @anchor 左手首 trail
38
- * // @anchor 頭
37
+ * #anchor 左手首 trail
38
+ * #anchor 頭
39
39
  *
40
40
  * A declaration in the source, like the mounts: what a file names is what gets
41
41
  * resolved and uploaded, so naming none costs nothing and nobody pays for a
42
42
  * rig's other five hundred bones. Anchored to the start of a line so that
43
- * writing the word @anchor in ordinary prose does not silently add a slot —
43
+ * writing the word #anchor in ordinary prose does not silently add a slot —
44
44
  * which would shift every slot after it.
45
45
  *
46
46
  * `trail` additionally keeps that bone's recent PATH, for rzTrail. Opt-in
@@ -64,7 +64,7 @@ type CompositeEffectSource = {
64
64
  hasBackground: boolean;
65
65
  /** Defines `fn foreground(...)` — mount over the finished frame. */
66
66
  hasForeground: boolean;
67
- /** Grid resolution when the effect declared `// @grid`, else 0. */
67
+ /** Grid resolution when the effect declared `#grid`, else 0. */
68
68
  gridSize: number;
69
69
  /** Whether the scene pass carries the id attachment, so the field module can
70
70
  * bind it. False emits accessors that answer 0 rather than nothing at all. */
@@ -92,7 +92,7 @@ type CompositeEffectSource = {
92
92
  * Depends on `viewU` and `_rzCast` being declared by the including module, and
93
93
  * on nothing else: no textures, no samplers.
94
94
  */
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";
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
97
  export declare function buildFieldShader(effect: CompositeEffectSource): string;
98
98
  /** Kept for compatibility with existing imports (the base, no-effect shader). */
@@ -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,mEAAmE;IACnE,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,CAqEtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
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"}