reze-engine 0.58.0 → 0.58.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/effect-points.d.ts +30 -0
- package/dist/effect-points.d.ts.map +1 -0
- package/dist/effect-points.js +49 -0
- package/dist/engine.d.ts +30 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +149 -19
- package/dist/light-grid.d.ts +25 -0
- package/dist/light-grid.d.ts.map +1 -0
- package/dist/light-grid.js +91 -0
- package/dist/model.d.ts +9 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +6 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +11 -6
- package/dist/shaders/cast-api.d.ts +1 -1
- package/dist/shaders/cast-layout.d.ts +5 -3
- package/dist/shaders/cast-layout.d.ts.map +1 -1
- package/dist/shaders/cast-layout.js +5 -3
- package/dist/shaders/directives.d.ts +3 -0
- package/dist/shaders/directives.d.ts.map +1 -1
- package/dist/shaders/directives.js +5 -0
- package/dist/shaders/lights.d.ts +16 -19
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +121 -88
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +3 -17
- package/dist/shaders/passes/composite.d.ts +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +4 -0
- package/dist/shaders/passes/grid.d.ts.map +1 -1
- package/dist/shaders/passes/grid.js +4 -0
- package/dist/shaders/passes/particles.d.ts +4 -0
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +13 -1
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +3 -1
- package/dist/shaders/points-api.d.ts +11 -0
- package/dist/shaders/points-api.d.ts.map +1 -0
- package/dist/shaders/points-api.js +58 -0
- package/dist/shaders/scene-light-api.d.ts +42 -0
- package/dist/shaders/scene-light-api.d.ts.map +1 -0
- package/dist/shaders/scene-light-api.js +144 -0
- package/dist/shaders/shadow-api.d.ts +29 -0
- package/dist/shaders/shadow-api.d.ts.map +1 -0
- package/dist/shaders/shadow-api.js +112 -0
- package/package.json +1 -1
- package/src/effect-points.ts +67 -0
- package/src/engine.ts +163 -18
- package/src/light-grid.ts +139 -0
- package/src/model.ts +12 -0
- package/src/pmx-loader.ts +13 -6
- package/src/shaders/cast-layout.ts +5 -3
- package/src/shaders/directives.ts +8 -0
- package/src/shaders/lights.ts +121 -88
- package/src/shaders/materials/common.ts +3 -17
- package/src/shaders/passes/composite.ts +4 -0
- package/src/shaders/passes/grid.ts +4 -0
- package/src/shaders/passes/particles.ts +14 -1
- package/src/shaders/passes/trails.ts +3 -1
- package/src/shaders/points-api.ts +66 -0
- package/src/shaders/scene-light-api.ts +148 -0
|
@@ -1,4 +1,4 @@
|
|
|
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 =
|
|
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 = 48;\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 /**\n * Where she LOOKS: a unit direction from between the eyes, in world space.\n * Exact while her eyes are on something (Engine.setEyeTracking); otherwise\n * the motion's own eye turn undone through the range map \u2014 an eye bone\n * turns a fraction of the way toward what it looks at, and this gives back\n * the whole of it. An effect that should go where the look goes \u2014 a beam,\n * a light \u2014 takes this rather than an eye bone's axis, and it holds in\n * either mode. Zero, with looking false, only for a model without eyes.\n */\n gaze: vec3f,\n looking: bool,\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/**\n * How many characters THIS EFFECT is on, up to four.\n *\n * Not how many the scene holds. An effect applies to the whole cast unless the\n * scene says otherwise (Engine.setEffectSubjects), and where it says otherwise\n * this counts only the models named \u2014 so an effect aimed at one dancer sees one\n * subject, at index 0, and an author who wrote a loop over the cast gets the\n * targeting for free.\n *\n * Which is the whole design: the FILTER IS IN THE INDEX SPACE, not in the\n * shaders. There is no mask an effect can read, because an effect asking \"am I\n * allowed on this one\" is an effect that can get the answer wrong, and 18 of the\n * shipped built-ins would have had to be rewritten to ask at all.\n */\nfn rzSubjectCount() -> i32 {\n let live = _rzCastLive();\n let mask = _rzSubjectMask();\n var n = 0;\n for (var i = 0; i < live; i++) {\n if ((mask & (1u << u32(i))) != 0u) { n++; }\n }\n return n;\n}\n\n/** This effect's i-th subject \u2192 the scene's slot, or -1 past its own count.\n * Every accessor below goes through it, which is what keeps one effect's\n * subject 1 from being another's. */\nfn _rzSubjectSlot(i: i32) -> i32 {\n if (i < 0) { return -1; }\n let live = _rzCastLive();\n let mask = _rzSubjectMask();\n var n = 0;\n for (var s = 0; s < live; s++) {\n if ((mask & (1u << u32(s))) == 0u) { continue; }\n if (n == i) { return s; }\n n++;\n }\n return -1;\n}\n\n/** Which model this is, stable across a scene \u2014 for per-subject variation. */\nfn rzSubjectId(i: i32) -> u32 {\n let g = _rzSubjectSlot(i);\n if (g < 0) { return 0u; }\n return u32(_rzCast[g * 4 + 1].w);\n}\n\nfn rzSubject(i: i32) -> RzSubject {\n var s: RzSubject;\n let g = _rzSubjectSlot(i);\n s.valid = g >= 0;\n if (!s.valid) { return s; }\n let b = g * 4;\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 s.gaze = _rzCast[b + 3].xyz;\n s.looking = _rzCast[b + 3].w > 0.5;\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 let s = _rzSubjectSlot(subject);\n if (s < 0 || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }\n let b = 16 + (g * 4 + s) * 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 let s = _rzSubjectSlot(subject);\n if (s < 0 || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }\n return i32(_rzCast[16 + (g * 4 + s) * 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 = 592 + (_rzSlot(slot) * 4 + _rzSubjectSlot(subject)) * RZ_TRAIL_SAMPLES;\n return _rzCast[base + i];\n}\n";
|
|
2
2
|
/**
|
|
3
3
|
* Which subjects this effect applies to, as WGSL — the second of the two names a
|
|
4
4
|
* mount owes CAST_API (`_rzCastLive` is the first, and a mount with a view
|
|
@@ -23,8 +23,10 @@ export declare const EFFECT_SUBJECT_VEC4S = 4;
|
|
|
23
23
|
*
|
|
24
24
|
* 8 was reachable, and quietly: two effects each wanting two hands, two feet and
|
|
25
25
|
* a head is ten, so the second one silently lost its ribbons to a diagnostic
|
|
26
|
-
* nobody was reading. 16
|
|
27
|
-
*
|
|
26
|
+
* nobody was reading. 16 held until an effect traced a HAND: an outline through
|
|
27
|
+
* every finger reads fifteen bones a hand, thirty for the pair, before any other
|
|
28
|
+
* effect in the scene asks for one. 48 is that and room beside it, for 393KB of
|
|
29
|
+
* storage buffer where 16 cost 131KB.
|
|
28
30
|
*
|
|
29
31
|
* The cost really is only that. The per-frame upload is bounded by the last
|
|
30
32
|
* TRAILED slot, not by this cap (see the writeBuffer in updateCastBuffer), so a
|
|
@@ -39,7 +41,7 @@ export declare const EFFECT_SUBJECT_VEC4S = 4;
|
|
|
39
41
|
* scene's anchor count grows, which is a different feature from raising a number
|
|
40
42
|
* that was never load-bearing.
|
|
41
43
|
*/
|
|
42
|
-
export declare const EFFECT_ANCHORS =
|
|
44
|
+
export declare const EFFECT_ANCHORS = 48;
|
|
43
45
|
/**
|
|
44
46
|
* Path samples kept per trailed anchor — 128 at the 60Hz sampling rate is a
|
|
45
47
|
* ~2.1 second ribbon.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cast-layout.d.ts","sourceRoot":"","sources":["../../src/shaders/cast-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,IAAI,CAAA;AAChC,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,IAAI,CAAA;AACrC
|
|
1
|
+
{"version":3,"file":"cast-layout.d.ts","sourceRoot":"","sources":["../../src/shaders/cast-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,IAAI,CAAA;AAChC,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,IAAI,CAAA;AACrC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,cAAc,KAAK,CAAA;AAChC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,oBAAoB,MAAM,CAAA;AACvC,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,QAAgF,CAAA"}
|
|
@@ -23,8 +23,10 @@ export const EFFECT_SUBJECT_VEC4S = 4;
|
|
|
23
23
|
*
|
|
24
24
|
* 8 was reachable, and quietly: two effects each wanting two hands, two feet and
|
|
25
25
|
* a head is ten, so the second one silently lost its ribbons to a diagnostic
|
|
26
|
-
* nobody was reading. 16
|
|
27
|
-
*
|
|
26
|
+
* nobody was reading. 16 held until an effect traced a HAND: an outline through
|
|
27
|
+
* every finger reads fifteen bones a hand, thirty for the pair, before any other
|
|
28
|
+
* effect in the scene asks for one. 48 is that and room beside it, for 393KB of
|
|
29
|
+
* storage buffer where 16 cost 131KB.
|
|
28
30
|
*
|
|
29
31
|
* The cost really is only that. The per-frame upload is bounded by the last
|
|
30
32
|
* TRAILED slot, not by this cap (see the writeBuffer in updateCastBuffer), so a
|
|
@@ -39,7 +41,7 @@ export const EFFECT_SUBJECT_VEC4S = 4;
|
|
|
39
41
|
* scene's anchor count grows, which is a different feature from raising a number
|
|
40
42
|
* that was never load-bearing.
|
|
41
43
|
*/
|
|
42
|
-
export const EFFECT_ANCHORS =
|
|
44
|
+
export const EFFECT_ANCHORS = 48;
|
|
43
45
|
/**
|
|
44
46
|
* Path samples kept per trailed anchor — 128 at the 60Hz sampling rate is a
|
|
45
47
|
* ~2.1 second ribbon.
|
|
@@ -29,6 +29,9 @@ export type EffectDirectives = {
|
|
|
29
29
|
particles: number;
|
|
30
30
|
lights: number;
|
|
31
31
|
grid: number;
|
|
32
|
+
/** `#points <prefix>`: every bone on every model whose name starts with it,
|
|
33
|
+
* as rzPoint(i) in the particle stages. Null when the file declares none. */
|
|
34
|
+
points: string | null;
|
|
32
35
|
bloom: boolean;
|
|
33
36
|
/** This effect takes the cast apart — the host reads the timing. */
|
|
34
37
|
dissolve: boolean;
|
|
@@ -1 +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;;;;iFAI6E;IAC7E,aAAa,EAAE,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAA;IAC9C,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;;;;;;;4CAOwC;IACxC,MAAM,EAAE;QAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACjE;;;;;;;;;;;;;OAaG;IACH,MAAM,EAAE,OAAO,CAAA;IACf;;;;;;;;;;;;;OAaG;IACH,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;
|
|
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;;;;iFAI6E;IAC7E,aAAa,EAAE,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAA;IAC9C,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ;kFAC8E;IAC9E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,EAAE,OAAO,CAAA;IACd,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAA;IACjB;;;;;;;4CAOwC;IACxC,MAAM,EAAE;QAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACjE;;;;;;;;;;;;;OAaG;IACH,MAAM,EAAE,OAAO,CAAA;IACf;;;;;;;;;;;;;OAaG;IACH,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAoBD;;;+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,CA+K7D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKpD"}
|
|
@@ -37,6 +37,7 @@ const SPEC = {
|
|
|
37
37
|
particles: 1,
|
|
38
38
|
lights: 1,
|
|
39
39
|
grid: 1,
|
|
40
|
+
points: 1,
|
|
40
41
|
bloom: 0,
|
|
41
42
|
dissolve: 0,
|
|
42
43
|
mirror: 0,
|
|
@@ -88,6 +89,7 @@ export function parseDirectives(wgsl) {
|
|
|
88
89
|
particles: 0,
|
|
89
90
|
lights: 0,
|
|
90
91
|
grid: 0,
|
|
92
|
+
points: null,
|
|
91
93
|
bloom: false,
|
|
92
94
|
dissolve: false,
|
|
93
95
|
ground: null,
|
|
@@ -204,6 +206,9 @@ export function parseDirectives(wgsl) {
|
|
|
204
206
|
case "dissolve":
|
|
205
207
|
d.dissolve = true;
|
|
206
208
|
return;
|
|
209
|
+
case "points":
|
|
210
|
+
d.points = args[0];
|
|
211
|
+
return;
|
|
207
212
|
case "mirror":
|
|
208
213
|
d.mirror = true;
|
|
209
214
|
return;
|
package/dist/shaders/lights.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
export declare const LIGHT_HEADER =
|
|
1
|
+
/** Words before the first record: the counts, the grid's placement and the
|
|
2
|
+
* outside mask, four vec4s — see the layout above. */
|
|
3
|
+
export declare const LIGHT_HEADER = 16;
|
|
4
4
|
/** Floats per light — see the layout above. */
|
|
5
5
|
export declare const LIGHT_STRIDE = 16;
|
|
6
6
|
/**
|
|
@@ -14,14 +14,19 @@ export declare const LIGHT_STRIDE = 16;
|
|
|
14
14
|
* interior brings a hundred or more, and the ceilings of sixteen and then
|
|
15
15
|
* forty-eight each cut a real scene in half. Past this the extras are dropped.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* size affordable at all; clustering is what replaces the linear walk when a
|
|
21
|
-
* scene wants every one of them close enough to matter.
|
|
17
|
+
* A fragment pays for the document lamps its grid cell names, so the cost
|
|
18
|
+
* follows how many reach a place rather than how many the scene holds. It is
|
|
19
|
+
* also why the cap is 128: one vec4u of bits per cell.
|
|
22
20
|
*/
|
|
23
21
|
export declare const MAX_LIGHTS = 128;
|
|
24
|
-
/**
|
|
22
|
+
/** Words of lamp bits per grid cell — one bit per lamp the cap allows. */
|
|
23
|
+
export declare const LIGHT_MASK_WORDS: number;
|
|
24
|
+
/** The grid's cell budget. 32k cells over a stage a couple of hundred units
|
|
25
|
+
* across is a cell of about four — finer than the lamps it separates. */
|
|
26
|
+
export declare const LIGHT_GRID_CELLS = 32768;
|
|
27
|
+
/** Where the grid starts, in words. */
|
|
28
|
+
export declare const LIGHT_GRID_BASE: number;
|
|
29
|
+
/** Words in the whole buffer: header, records, grid. */
|
|
25
30
|
export declare const LIGHTS_FLOATS: number;
|
|
26
31
|
/**
|
|
27
32
|
* The RzLight struct, declared in EVERY module a user's source is spliced into.
|
|
@@ -33,16 +38,8 @@ export declare const LIGHTS_FLOATS: number;
|
|
|
33
38
|
* write, which is the same trap the grid's step-only half documents.
|
|
34
39
|
*/
|
|
35
40
|
export declare const RZ_LIGHT_STRUCT_WGSL = "\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";
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
* HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
|
|
39
|
-
* ibl.ts for the folding; the shader is a plain polynomial in the normal).
|
|
40
|
-
*
|
|
41
|
-
* One string included by every module that declares LightUniforms with the sh
|
|
42
|
-
* block — the hosted-api lesson: a helper defined in some modules and not
|
|
43
|
-
* others is a compile error waiting for the first file that crosses them.
|
|
44
|
-
*/
|
|
45
|
-
export declare const WORLD_AMBIENT_WGSL = "\nfn rzWorldAmbient(n: vec3f) -> vec3f {\n if (light.sh[0].w < 0.5) { return light.ambientColor.xyz; }\n let x = n.x;\n let y = n.y;\n let z = n.z;\n let c = light.sh[0].xyz\n + light.sh[1].xyz * y + light.sh[2].xyz * z + light.sh[3].xyz * x\n + light.sh[4].xyz * (x * y) + light.sh[5].xyz * (y * z)\n + light.sh[6].xyz * (3.0 * z * z - 1.0) + light.sh[7].xyz * (x * z)\n + light.sh[8].xyz * (x * x - y * y);\n return max(c, vec3f(0.0));\n}\n";
|
|
41
|
+
/** rzWorldAmbient against the materials' and the ground's `light` uniform. */
|
|
42
|
+
export declare const WORLD_AMBIENT_WGSL: string;
|
|
46
43
|
/** Does this source define the emit mount? */
|
|
47
44
|
export declare function hasLightEmit(wgsl: string): boolean;
|
|
48
45
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../src/shaders/lights.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../src/shaders/lights.ts"],"names":[],"mappings":"AAoDA;uDACuD;AACvD,eAAO,MAAM,YAAY,KAAK,CAAA;AAC9B,+CAA+C;AAC/C,eAAO,MAAM,YAAY,KAAK,CAAA;AAC9B;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,MAAM,CAAA;AAC7B,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,QAAkB,CAAA;AAC/C;0EAC0E;AAC1E,eAAO,MAAM,gBAAgB,QAAQ,CAAA;AACrC,uCAAuC;AACvC,eAAO,MAAM,eAAe,QAA2C,CAAA;AACvE,wDAAwD;AACxD,eAAO,MAAM,aAAa,QAAwD,CAAA;AAElF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,qJAQhC,CAAA;AAED,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,QAA4B,CAAA;AAE3D,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,EAC5B,UAAU,SAAK,GACd,MAAM,CAoGR;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAgIhE"}
|
package/dist/shaders/lights.js
CHANGED
|
@@ -11,18 +11,27 @@ import { subjectMaskApi } from "./cast-api";
|
|
|
11
11
|
// renderer that bolted a second key light onto a toon shader has shipped. A
|
|
12
12
|
// light here brightens; it does not restate the shading.
|
|
13
13
|
//
|
|
14
|
-
// So there are no per-light shadows
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// So there are no per-light shadows and no area lights. A fragment walks the
|
|
15
|
+
// DOCUMENT's lamps through a world-space grid (see light-grid.ts): its cell
|
|
16
|
+
// names the lamps that can reach it, so a stage rig of fifty costs each pixel
|
|
17
|
+
// the handful standing near it. The lamps an EFFECT emits are placed by a
|
|
18
|
+
// compute pass every frame, which the grid never sees, and are walked in full.
|
|
17
19
|
//
|
|
18
|
-
// LAYOUT
|
|
19
|
-
//
|
|
20
|
+
// LAYOUT, in 32-bit words, read by the shader as vec4u. A 16-word header:
|
|
21
|
+
//
|
|
22
|
+
// [0] light count (f32) [1] how many are the document's (f32)
|
|
23
|
+
// [4..6] grid origin (f32) [7] 1 / cell edge (f32)
|
|
24
|
+
// [8..10] grid dims (u32) [12..15] the OUTSIDE mask (u32 bits)
|
|
25
|
+
//
|
|
26
|
+
// then MAX_LIGHTS records of 16 floats — four vec4s:
|
|
20
27
|
//
|
|
21
28
|
// [0..2] position, world space [3] radius
|
|
22
29
|
// [4..6] colour PREMULTIPLIED by intensity [7] type
|
|
23
30
|
// [8..10] aim, unit, pointing away from the light [11] cos of the outer angle
|
|
24
31
|
// [12] cos of the inner angle [13..15] spare
|
|
25
32
|
//
|
|
33
|
+
// then the grid: LIGHT_MASK_WORDS words of lamp bits per cell.
|
|
34
|
+
//
|
|
26
35
|
// Colour carries intensity because nothing reads them apart: every use is the
|
|
27
36
|
// product, and storing two numbers that are only ever multiplied is two numbers
|
|
28
37
|
// that can disagree.
|
|
@@ -37,9 +46,11 @@ import { midiApi } from "./midi-api";
|
|
|
37
46
|
import { lyricsApi } from "./lyrics-api";
|
|
38
47
|
import { clockApi, trailSlotsApi, viewportApi } from "./passes/hosted-api";
|
|
39
48
|
import { idApi } from "./id-api";
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
import { sceneLightApi, worldAmbientWgsl } from "./scene-light-api";
|
|
50
|
+
import { pointsApi } from "./points-api";
|
|
51
|
+
/** Words before the first record: the counts, the grid's placement and the
|
|
52
|
+
* outside mask, four vec4s — see the layout above. */
|
|
53
|
+
export const LIGHT_HEADER = 16;
|
|
43
54
|
/** Floats per light — see the layout above. */
|
|
44
55
|
export const LIGHT_STRIDE = 16;
|
|
45
56
|
/**
|
|
@@ -53,15 +64,20 @@ export const LIGHT_STRIDE = 16;
|
|
|
53
64
|
* interior brings a hundred or more, and the ceilings of sixteen and then
|
|
54
65
|
* forty-eight each cut a real scene in half. Past this the extras are dropped.
|
|
55
66
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* size affordable at all; clustering is what replaces the linear walk when a
|
|
60
|
-
* scene wants every one of them close enough to matter.
|
|
67
|
+
* A fragment pays for the document lamps its grid cell names, so the cost
|
|
68
|
+
* follows how many reach a place rather than how many the scene holds. It is
|
|
69
|
+
* also why the cap is 128: one vec4u of bits per cell.
|
|
61
70
|
*/
|
|
62
71
|
export const MAX_LIGHTS = 128;
|
|
63
|
-
/**
|
|
64
|
-
export const
|
|
72
|
+
/** Words of lamp bits per grid cell — one bit per lamp the cap allows. */
|
|
73
|
+
export const LIGHT_MASK_WORDS = MAX_LIGHTS / 32;
|
|
74
|
+
/** The grid's cell budget. 32k cells over a stage a couple of hundred units
|
|
75
|
+
* across is a cell of about four — finer than the lamps it separates. */
|
|
76
|
+
export const LIGHT_GRID_CELLS = 32768;
|
|
77
|
+
/** Where the grid starts, in words. */
|
|
78
|
+
export const LIGHT_GRID_BASE = LIGHT_HEADER + MAX_LIGHTS * LIGHT_STRIDE;
|
|
79
|
+
/** Words in the whole buffer: header, records, grid. */
|
|
80
|
+
export const LIGHTS_FLOATS = LIGHT_GRID_BASE + LIGHT_GRID_CELLS * LIGHT_MASK_WORDS;
|
|
65
81
|
/**
|
|
66
82
|
* The RzLight struct, declared in EVERY module a user's source is spliced into.
|
|
67
83
|
*
|
|
@@ -80,29 +96,8 @@ struct RzLight {
|
|
|
80
96
|
radius: f32,
|
|
81
97
|
}
|
|
82
98
|
`;
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
* HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
|
|
86
|
-
* ibl.ts for the folding; the shader is a plain polynomial in the normal).
|
|
87
|
-
*
|
|
88
|
-
* One string included by every module that declares LightUniforms with the sh
|
|
89
|
-
* block — the hosted-api lesson: a helper defined in some modules and not
|
|
90
|
-
* others is a compile error waiting for the first file that crosses them.
|
|
91
|
-
*/
|
|
92
|
-
export const WORLD_AMBIENT_WGSL = /* wgsl */ `
|
|
93
|
-
fn rzWorldAmbient(n: vec3f) -> vec3f {
|
|
94
|
-
if (light.sh[0].w < 0.5) { return light.ambientColor.xyz; }
|
|
95
|
-
let x = n.x;
|
|
96
|
-
let y = n.y;
|
|
97
|
-
let z = n.z;
|
|
98
|
-
let c = light.sh[0].xyz
|
|
99
|
-
+ light.sh[1].xyz * y + light.sh[2].xyz * z + light.sh[3].xyz * x
|
|
100
|
-
+ light.sh[4].xyz * (x * y) + light.sh[5].xyz * (y * z)
|
|
101
|
-
+ light.sh[6].xyz * (3.0 * z * z - 1.0) + light.sh[7].xyz * (x * z)
|
|
102
|
-
+ light.sh[8].xyz * (x * x - y * y);
|
|
103
|
-
return max(c, vec3f(0.0));
|
|
104
|
-
}
|
|
105
|
-
`;
|
|
99
|
+
/** rzWorldAmbient against the materials' and the ground's `light` uniform. */
|
|
100
|
+
export const WORLD_AMBIENT_WGSL = worldAmbientWgsl("light");
|
|
106
101
|
/** Does this source define the emit mount? */
|
|
107
102
|
export function hasLightEmit(wgsl) {
|
|
108
103
|
return /\bfn\s+lightEmit\s*\(/.test(wgsl);
|
|
@@ -173,7 +168,7 @@ ${viewportApi("viewU[6].w")}
|
|
|
173
168
|
${trailSlotsApi(cast.trailCount)}
|
|
174
169
|
// The id accessors, stubbed: this module cannot read an attachment the
|
|
175
170
|
// scene pass writes. See id-api.ts — the author's whole file compiles here.
|
|
176
|
-
${idApi(false, 0, 0) + castDistanceStub()}
|
|
171
|
+
${idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) + pointsApi(false)}
|
|
177
172
|
// The dials the author declared, if any. A lamp is exactly the thing someone
|
|
178
173
|
// retunes — its colour and its reach — so an emitter reads params like every
|
|
179
174
|
// other mount rather than being the one place a #param resolves to nothing.
|
|
@@ -237,51 +232,55 @@ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
237
232
|
}
|
|
238
233
|
/** The rz*Light accessors, with the buffer declared at the given binding. */
|
|
239
234
|
export function lightsApi(group, binding) {
|
|
235
|
+
const R = LIGHT_HEADER / 4;
|
|
236
|
+
const S = LIGHT_STRIDE / 4;
|
|
240
237
|
return /* wgsl */ `
|
|
241
|
-
|
|
238
|
+
// vec4u, so a record is four loads and a cell's lamp bits are one. Floats come
|
|
239
|
+
// out through bitcast; the bits must NOT pass through f32 on the way, where a
|
|
240
|
+
// mask that happens to spell a NaN is not guaranteed to survive a load.
|
|
241
|
+
@group(${group}) @binding(${binding}) var<storage, read> _rzLights: array<vec4u>;
|
|
242
242
|
|
|
243
243
|
const RZ_MAX_LIGHTS: u32 = ${MAX_LIGHTS}u;
|
|
244
244
|
|
|
245
245
|
/** How many positional lights the scene has. Zero is the ordinary case. */
|
|
246
|
-
fn rzLightCount() -> u32 { return min(u32(_rzLights[0]), RZ_MAX_LIGHTS); }
|
|
246
|
+
fn rzLightCount() -> u32 { return min(u32(bitcast<f32>(_rzLights[0].x)), RZ_MAX_LIGHTS); }
|
|
247
|
+
|
|
248
|
+
/** How many of them the document placed — the ones the grid indexes. */
|
|
249
|
+
fn _rzLightDocCount() -> u32 { return min(u32(bitcast<f32>(_rzLights[0].y)), rzLightCount()); }
|
|
250
|
+
|
|
251
|
+
/** One vec4 of light i's record. */
|
|
252
|
+
fn _rzLightVec(i: u32, k: u32) -> vec4f { return bitcast<vec4f>(_rzLights[${R}u + i * ${S}u + k]); }
|
|
247
253
|
|
|
248
254
|
/** Light i's world position. */
|
|
249
|
-
fn rzLightPos(i: u32) -> vec3f {
|
|
250
|
-
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u;
|
|
251
|
-
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
252
|
-
}
|
|
255
|
+
fn rzLightPos(i: u32) -> vec3f { return _rzLightVec(i, 0u).xyz; }
|
|
253
256
|
|
|
254
257
|
/** How far light i reaches. Its falloff is zero AT this distance, not merely
|
|
255
|
-
* small,
|
|
256
|
-
fn rzLightRadius(i: u32) -> f32 { return
|
|
258
|
+
* small, which is the bound the grid is built from. */
|
|
259
|
+
fn rzLightRadius(i: u32) -> f32 { return _rzLightVec(i, 0u).w; }
|
|
257
260
|
|
|
258
261
|
/** Light i's colour, already multiplied by its intensity. */
|
|
259
|
-
fn rzLightColor(i: u32) -> vec3f {
|
|
260
|
-
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 4u;
|
|
261
|
-
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
262
|
-
}
|
|
262
|
+
fn rzLightColor(i: u32) -> vec3f { return _rzLightVec(i, 1u).xyz; }
|
|
263
263
|
|
|
264
264
|
/** Where light i points, away from itself. The zero vector for a point light. */
|
|
265
|
-
fn rzLightAim(i: u32) -> vec3f {
|
|
266
|
-
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 8u;
|
|
267
|
-
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
268
|
-
}
|
|
265
|
+
fn rzLightAim(i: u32) -> vec3f { return _rzLightVec(i, 2u).xyz; }
|
|
269
266
|
|
|
270
267
|
/** The cosines a spot fades between: x its outer edge, y its inner one. A point
|
|
271
268
|
* light stores (-1, -1), which saturates the cone term to 1. */
|
|
272
|
-
fn rzLightCone(i: u32) -> vec2f {
|
|
273
|
-
|
|
274
|
-
|
|
269
|
+
fn rzLightCone(i: u32) -> vec2f { return vec2f(_rzLightVec(i, 2u).w, _rzLightVec(i, 3u).x); }
|
|
270
|
+
|
|
271
|
+
/** The document lamps that can reach p: its grid cell's bits, or the outside
|
|
272
|
+
* mask beyond the grid. Written so a NaN position fails the inside test. */
|
|
273
|
+
fn _rzLightCellMask(p: vec3f) -> vec4u {
|
|
274
|
+
let g = bitcast<vec4f>(_rzLights[1]);
|
|
275
|
+
let dims = _rzLights[2].xyz;
|
|
276
|
+
let c = floor((p - g.xyz) * g.w);
|
|
277
|
+
if (!(all(c >= vec3f(0.0)) && all(c < vec3f(dims)))) { return _rzLights[3]; }
|
|
278
|
+
let ci = vec3u(c);
|
|
279
|
+
return _rzLights[${LIGHT_GRID_BASE / 4}u + (ci.z * dims.y + ci.y) * dims.x + ci.x];
|
|
275
280
|
}
|
|
276
281
|
|
|
277
282
|
/**
|
|
278
|
-
*
|
|
279
|
-
* finished colour. Multiply by whatever the surface's albedo is.
|
|
280
|
-
*
|
|
281
|
-
* WITH NO LIGHTS THIS RETURNS EXACTLY ZERO and the loop never runs, so a scene
|
|
282
|
-
* that declares none is arithmetically identical to one compiled before lights
|
|
283
|
-
* existed. That is the property the whole feature is gated on: adding this to
|
|
284
|
-
* every material must cost nothing until someone asks for a light.
|
|
283
|
+
* One light's contribution at a surface point.
|
|
285
284
|
*
|
|
286
285
|
* FALLOFF IS RELATIVE TO THE RADIUS, and deliberately not physical.
|
|
287
286
|
*
|
|
@@ -295,33 +294,67 @@ fn rzLightCone(i: u32) -> vec2f {
|
|
|
295
294
|
* zero, and the curve between them is the same shape whatever the scene's
|
|
296
295
|
* scale. Both dials now mean what they say, which for a composer beats being
|
|
297
296
|
* right about photons. (1 - t²)² — smooth at both ends, exactly 0 at the
|
|
298
|
-
* radius, so the bound
|
|
297
|
+
* radius, so the bound the grid is built from is real.
|
|
298
|
+
*/
|
|
299
|
+
fn _rzLightOne(i: u32, p: vec3f, n: vec3f) -> vec3f {
|
|
300
|
+
let pr = _rzLightVec(i, 0u);
|
|
301
|
+
let d = pr.xyz - p;
|
|
302
|
+
let dist = length(d);
|
|
303
|
+
// Out of reach before anything else is computed. The grid is conservative —
|
|
304
|
+
// a cell's bit means the lamp CAN reach part of it — so this still runs.
|
|
305
|
+
if (dist >= pr.w) { return vec3f(0.0); }
|
|
306
|
+
let toLight = d / max(dist, 1e-4);
|
|
307
|
+
// Facing the light, and nothing behind it. No wrap or half-lambert: this
|
|
308
|
+
// layer adds light, and a wrapped term would lift the shadow side, which is
|
|
309
|
+
// the ramp's business and not this one's.
|
|
310
|
+
let ndl = max(dot(n, toLight), 0.0);
|
|
311
|
+
if (ndl <= 0.0) { return vec3f(0.0); }
|
|
312
|
+
let t = clamp(dist / max(pr.w, 1e-4), 0.0, 1.0);
|
|
313
|
+
let falloff = 1.0 - t * t;
|
|
314
|
+
// How far inside the cone this point sits: 1 within the inner angle, 0 past
|
|
315
|
+
// the outer one, squared for the same soft edge the falloff has. A point
|
|
316
|
+
// light's (-1, -1) divides by the floor and clamps to 1, so it pays one
|
|
317
|
+
// dot product and no branch.
|
|
318
|
+
let cone = rzLightCone(i);
|
|
319
|
+
let aim = clamp((dot(-toLight, rzLightAim(i)) - cone.x) / max(cone.y - cone.x, 1e-4), 0.0, 1.0);
|
|
320
|
+
return rzLightColor(i) * (ndl * falloff * falloff * aim * aim);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The lamps named by one word of a cell's bits, lowest first. */
|
|
324
|
+
fn _rzLightWord(bits0: u32, base: u32, p: vec3f, n: vec3f) -> vec3f {
|
|
325
|
+
var acc = vec3f(0.0);
|
|
326
|
+
var bits = bits0;
|
|
327
|
+
loop {
|
|
328
|
+
if (bits == 0u) { break; }
|
|
329
|
+
let i = base + firstTrailingBit(bits);
|
|
330
|
+
bits = bits & (bits - 1u);
|
|
331
|
+
acc = acc + _rzLightOne(i, p, n);
|
|
332
|
+
}
|
|
333
|
+
return acc;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Every positional light's contribution at a surface point, as light — not as a
|
|
338
|
+
* finished colour. Multiply by whatever the surface's albedo is.
|
|
339
|
+
*
|
|
340
|
+
* WITH NO LIGHTS THIS RETURNS EXACTLY ZERO and neither walk runs, so a scene
|
|
341
|
+
* that declares none is arithmetically identical to one compiled before lights
|
|
342
|
+
* existed. That is the property the whole feature is gated on: adding this to
|
|
343
|
+
* every material must cost nothing until someone asks for a light.
|
|
299
344
|
*/
|
|
300
345
|
fn rzLightsDiffuse(p: vec3f, n: vec3f) -> vec3f {
|
|
301
346
|
var acc = vec3f(0.0);
|
|
302
347
|
let count = rzLightCount();
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
// the ramp's business and not this one's.
|
|
314
|
-
let ndl = max(dot(n, toLight), 0.0);
|
|
315
|
-
if (ndl <= 0.0) { continue; }
|
|
316
|
-
let t = clamp(dist / max(rzLightRadius(i), 1e-4), 0.0, 1.0);
|
|
317
|
-
let falloff = 1.0 - t * t;
|
|
318
|
-
// How far inside the cone this point sits: 1 within the inner angle, 0 past
|
|
319
|
-
// the outer one, squared for the same soft edge the falloff has. A point
|
|
320
|
-
// light's (-1, -1) divides by the floor and clamps to 1, so it pays one
|
|
321
|
-
// dot product and no branch.
|
|
322
|
-
let cone = rzLightCone(i);
|
|
323
|
-
let aim = clamp((dot(-toLight, rzLightAim(i)) - cone.x) / max(cone.y - cone.x, 1e-4), 0.0, 1.0);
|
|
324
|
-
acc = acc + rzLightColor(i) * (ndl * falloff * falloff * aim * aim);
|
|
348
|
+
let docs = _rzLightDocCount();
|
|
349
|
+
// The document's lamps: only those the grid says can reach this cell.
|
|
350
|
+
if (docs > 0u) {
|
|
351
|
+
let m = _rzLightCellMask(p);
|
|
352
|
+
acc = acc + _rzLightWord(m.x, 0u, p, n) + _rzLightWord(m.y, 32u, p, n) +
|
|
353
|
+
_rzLightWord(m.z, 64u, p, n) + _rzLightWord(m.w, 96u, p, n);
|
|
354
|
+
}
|
|
355
|
+
// The effects' lamps, which move every frame on the GPU: all of them.
|
|
356
|
+
for (var i = docs; i < count; i = i + 1u) {
|
|
357
|
+
acc = acc + _rzLightOne(i, p, n);
|
|
325
358
|
}
|
|
326
359
|
return acc;
|
|
327
360
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA4BA,eAAO,MAAM,oBAAoB,QAmJhC,CAAC;AA8HF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,orMAgHzB,CAAA;AAQD,eAAO,MAAM,4BAA4B,QAC0D,CAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sceneFsOutWgsl } from "../passes/scene-contract";
|
|
2
2
|
import { lightsApi } from "../lights";
|
|
3
3
|
import { SHADOW_CASCADES } from "../../shadow-cascades";
|
|
4
|
+
import { pcf9 } from "../scene-light-api";
|
|
4
5
|
import { WORLD_AMBIENT_WGSL } from "../lights";
|
|
5
6
|
// Shared WGSL blocks concatenated by every material shader.
|
|
6
7
|
// Splits the boilerplate (uniform structs, bind group layout, skinning VS, PCF shadow)
|
|
@@ -171,8 +172,8 @@ fn safe_normal(nIn: vec3f) -> vec3f {
|
|
|
171
172
|
|
|
172
173
|
`;
|
|
173
174
|
// ─── Shadow sampler (3×3 PCF, cascade-selected) ─────────────────────
|
|
174
|
-
// Normal-bias 0.08, depth-bias 0.001 NDC
|
|
175
|
-
//
|
|
175
|
+
// Normal-bias 0.08, depth-bias 0.001 NDC; the kernel is pcf9 in scene-light-api.ts,
|
|
176
|
+
// shared with effects. Texel sizes are interpolated
|
|
176
177
|
// from SHADOW_CASCADES so they cannot go stale the way the hardcoded 1/2048
|
|
177
178
|
// once did (PCF taps landed TWO texels apart after the map grew to 4096,
|
|
178
179
|
// quantizing self-shadow edges into crawling gray squares).
|
|
@@ -183,21 +184,6 @@ fn safe_normal(nIn: vec3f) -> vec3f {
|
|
|
183
184
|
// merely a default: the old single-volume path clamped, and past the light's
|
|
184
185
|
// far plane the comparison failed against every stored depth, silently
|
|
185
186
|
// shadowing any stage deeper than the box.
|
|
186
|
-
const pcf9 = (map, ts) => /* wgsl */ `
|
|
187
|
-
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
188
|
-
let cmpZ = ndc.z - 0.001;
|
|
189
|
-
let ts = ${ts};
|
|
190
|
-
let s00 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
191
|
-
let s10 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
192
|
-
let s20 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
193
|
-
let s01 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
194
|
-
let s11 = textureSampleCompareLevel(${map}, shadowSampler, suv, cmpZ);
|
|
195
|
-
let s21 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
196
|
-
let s02 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
197
|
-
let s12 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
198
|
-
let s22 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
199
|
-
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
200
|
-
`;
|
|
201
187
|
const SAMPLE_SHADOW_WGSL = /* wgsl */ `
|
|
202
188
|
|
|
203
189
|
fn sampleShadowNear(ndc: vec3f) -> f32 {
|
|
@@ -97,7 +97,7 @@ type CompositeEffectSource = {
|
|
|
97
97
|
* Depends on `viewU` and `_rzCast` being declared by the including module, and
|
|
98
98
|
* on nothing else: no textures, no samplers.
|
|
99
99
|
*/
|
|
100
|
-
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 the SCENE holds, up to four \u2014 the number the cast api\n * narrows to this effect's own subjects. See subjectMaskApi. */\nfn _rzCastLive() -> 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 /**\n * Where she LOOKS: a unit direction from between the eyes, in world space.\n * Exact while her eyes are on something (Engine.setEyeTracking); otherwise\n * the motion's own eye turn undone through the range map \u2014 an eye bone\n * turns a fraction of the way toward what it looks at, and this gives back\n * the whole of it. An effect that should go where the look goes \u2014 a beam,\n * a light \u2014 takes this rather than an eye bone's axis, and it holds in\n * either mode. Zero, with looking false, only for a model without eyes.\n */\n gaze: vec3f,\n looking: bool,\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/**\n * How many characters THIS EFFECT is on, up to four.\n *\n * Not how many the scene holds. An effect applies to the whole cast unless the\n * scene says otherwise (Engine.setEffectSubjects), and where it says otherwise\n * this counts only the models named \u2014 so an effect aimed at one dancer sees one\n * subject, at index 0, and an author who wrote a loop over the cast gets the\n * targeting for free.\n *\n * Which is the whole design: the FILTER IS IN THE INDEX SPACE, not in the\n * shaders. There is no mask an effect can read, because an effect asking \"am I\n * allowed on this one\" is an effect that can get the answer wrong, and 18 of the\n * shipped built-ins would have had to be rewritten to ask at all.\n */\nfn rzSubjectCount() -> i32 {\n let live = _rzCastLive();\n let mask = _rzSubjectMask();\n var n = 0;\n for (var i = 0; i < live; i++) {\n if ((mask & (1u << u32(i))) != 0u) { n++; }\n }\n return n;\n}\n\n/** This effect's i-th subject \u2192 the scene's slot, or -1 past its own count.\n * Every accessor below goes through it, which is what keeps one effect's\n * subject 1 from being another's. */\nfn _rzSubjectSlot(i: i32) -> i32 {\n if (i < 0) { return -1; }\n let live = _rzCastLive();\n let mask = _rzSubjectMask();\n var n = 0;\n for (var s = 0; s < live; s++) {\n if ((mask & (1u << u32(s))) == 0u) { continue; }\n if (n == i) { return s; }\n n++;\n }\n return -1;\n}\n\n/** Which model this is, stable across a scene \u2014 for per-subject variation. */\nfn rzSubjectId(i: i32) -> u32 {\n let g = _rzSubjectSlot(i);\n if (g < 0) { return 0u; }\n return u32(_rzCast[g * 4 + 1].w);\n}\n\nfn rzSubject(i: i32) -> RzSubject {\n var s: RzSubject;\n let g = _rzSubjectSlot(i);\n s.valid = g >= 0;\n if (!s.valid) { return s; }\n let b = g * 4;\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 s.gaze = _rzCast[b + 3].xyz;\n s.looking = _rzCast[b + 3].w > 0.5;\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 let s = _rzSubjectSlot(subject);\n if (s < 0 || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }\n let b = 16 + (g * 4 + s) * 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 let s = _rzSubjectSlot(subject);\n if (s < 0 || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }\n return i32(_rzCast[16 + (g * 4 + s) * 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 = 208 + (_rzSlot(slot) * 4 + _rzSubjectSlot(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 *\n * THE INDEX IS THE EFFECT'S OWN, like every other cast accessor \u2014 Dry Ice, Water,\n * Holy Light and Bloody Ash all read this one and all of them should place their\n * fog, their ripple and their shaft under the model the scene aimed them at. The\n * mapping is the identity for an effect aimed at nobody in particular, which is\n * every scene published before one could be aimed.\n */\nfn rzSubjectHip(i: i32) -> vec3f { return viewU[11 + clamp(_rzSubjectSlot(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";
|
|
100
|
+
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 the SCENE holds, up to four \u2014 the number the cast api\n * narrows to this effect's own subjects. See subjectMaskApi. */\nfn _rzCastLive() -> 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 = 48;\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 /**\n * Where she LOOKS: a unit direction from between the eyes, in world space.\n * Exact while her eyes are on something (Engine.setEyeTracking); otherwise\n * the motion's own eye turn undone through the range map \u2014 an eye bone\n * turns a fraction of the way toward what it looks at, and this gives back\n * the whole of it. An effect that should go where the look goes \u2014 a beam,\n * a light \u2014 takes this rather than an eye bone's axis, and it holds in\n * either mode. Zero, with looking false, only for a model without eyes.\n */\n gaze: vec3f,\n looking: bool,\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/**\n * How many characters THIS EFFECT is on, up to four.\n *\n * Not how many the scene holds. An effect applies to the whole cast unless the\n * scene says otherwise (Engine.setEffectSubjects), and where it says otherwise\n * this counts only the models named \u2014 so an effect aimed at one dancer sees one\n * subject, at index 0, and an author who wrote a loop over the cast gets the\n * targeting for free.\n *\n * Which is the whole design: the FILTER IS IN THE INDEX SPACE, not in the\n * shaders. There is no mask an effect can read, because an effect asking \"am I\n * allowed on this one\" is an effect that can get the answer wrong, and 18 of the\n * shipped built-ins would have had to be rewritten to ask at all.\n */\nfn rzSubjectCount() -> i32 {\n let live = _rzCastLive();\n let mask = _rzSubjectMask();\n var n = 0;\n for (var i = 0; i < live; i++) {\n if ((mask & (1u << u32(i))) != 0u) { n++; }\n }\n return n;\n}\n\n/** This effect's i-th subject \u2192 the scene's slot, or -1 past its own count.\n * Every accessor below goes through it, which is what keeps one effect's\n * subject 1 from being another's. */\nfn _rzSubjectSlot(i: i32) -> i32 {\n if (i < 0) { return -1; }\n let live = _rzCastLive();\n let mask = _rzSubjectMask();\n var n = 0;\n for (var s = 0; s < live; s++) {\n if ((mask & (1u << u32(s))) == 0u) { continue; }\n if (n == i) { return s; }\n n++;\n }\n return -1;\n}\n\n/** Which model this is, stable across a scene \u2014 for per-subject variation. */\nfn rzSubjectId(i: i32) -> u32 {\n let g = _rzSubjectSlot(i);\n if (g < 0) { return 0u; }\n return u32(_rzCast[g * 4 + 1].w);\n}\n\nfn rzSubject(i: i32) -> RzSubject {\n var s: RzSubject;\n let g = _rzSubjectSlot(i);\n s.valid = g >= 0;\n if (!s.valid) { return s; }\n let b = g * 4;\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 s.gaze = _rzCast[b + 3].xyz;\n s.looking = _rzCast[b + 3].w > 0.5;\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 let s = _rzSubjectSlot(subject);\n if (s < 0 || g < 0 || g >= RZ_MAX_ANCHORS) { return a; }\n let b = 16 + (g * 4 + s) * 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 let s = _rzSubjectSlot(subject);\n if (s < 0 || g < 0 || g >= RZ_MAX_ANCHORS) { return 0; }\n return i32(_rzCast[16 + (g * 4 + s) * 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 = 592 + (_rzSlot(slot) * 4 + _rzSubjectSlot(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 *\n * THE INDEX IS THE EFFECT'S OWN, like every other cast accessor \u2014 Dry Ice, Water,\n * Holy Light and Bloody Ash all read this one and all of them should place their\n * fog, their ripple and their shaft under the model the scene aimed them at. The\n * mapping is the identity for an effect aimed at nobody in particular, which is\n * every scene published before one could be aimed.\n */\nfn rzSubjectHip(i: i32) -> vec3f { return viewU[11 + clamp(_rzSubjectSlot(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";
|
|
101
101
|
export declare function buildCompositeShader(effect?: CompositeEffectSource | null): string;
|
|
102
102
|
/**
|
|
103
103
|
* The field pass: the user's background/foreground mounts at half resolution,
|