reze-engine 0.57.6 → 0.58.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/engine.d.ts +31 -0
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +231 -82
  4. package/dist/shaders/directives.d.ts +18 -1
  5. package/dist/shaders/directives.d.ts.map +1 -1
  6. package/dist/shaders/directives.js +22 -3
  7. package/dist/shaders/lights.d.ts +2 -10
  8. package/dist/shaders/lights.d.ts.map +1 -1
  9. package/dist/shaders/lights.js +4 -24
  10. package/dist/shaders/materials/common.d.ts.map +1 -1
  11. package/dist/shaders/materials/common.js +3 -17
  12. package/dist/shaders/passes/composite.d.ts.map +1 -1
  13. package/dist/shaders/passes/composite.js +2 -0
  14. package/dist/shaders/passes/grid.d.ts.map +1 -1
  15. package/dist/shaders/passes/grid.js +2 -0
  16. package/dist/shaders/passes/particles.d.ts +20 -2
  17. package/dist/shaders/passes/particles.d.ts.map +1 -1
  18. package/dist/shaders/passes/particles.js +77 -5
  19. package/dist/shaders/passes/trails.d.ts +3 -0
  20. package/dist/shaders/passes/trails.d.ts.map +1 -1
  21. package/dist/shaders/passes/trails.js +7 -1
  22. package/dist/shaders/scene-light-api.d.ts +42 -0
  23. package/dist/shaders/scene-light-api.d.ts.map +1 -0
  24. package/dist/shaders/scene-light-api.js +144 -0
  25. package/dist/shaders/shadow-api.d.ts +29 -0
  26. package/dist/shaders/shadow-api.d.ts.map +1 -0
  27. package/dist/shaders/shadow-api.js +112 -0
  28. package/package.json +1 -1
  29. package/src/engine.ts +272 -96
  30. package/src/shaders/directives.ts +37 -4
  31. package/src/shaders/lights.ts +4 -24
  32. package/src/shaders/materials/common.ts +3 -17
  33. package/src/shaders/passes/composite.ts +2 -0
  34. package/src/shaders/passes/grid.ts +2 -0
  35. package/src/shaders/passes/particles.ts +93 -6
  36. package/src/shaders/passes/trails.ts +10 -1
  37. package/src/shaders/scene-light-api.ts +148 -0
@@ -37,6 +37,7 @@ import { midiApi } from "./midi-api";
37
37
  import { lyricsApi } from "./lyrics-api";
38
38
  import { clockApi, trailSlotsApi, viewportApi } from "./passes/hosted-api";
39
39
  import { idApi } from "./id-api";
40
+ import { sceneLightApi, worldAmbientWgsl } from "./scene-light-api";
40
41
  /** Floats before the first record. One is the count; the rest keep the records
41
42
  * vec4-aligned, which is what lets a future pass read them as vec4s. */
42
43
  export const LIGHT_HEADER = 4;
@@ -80,29 +81,8 @@ struct RzLight {
80
81
  radius: f32,
81
82
  }
82
83
  `;
83
- /**
84
- * The world's light at a surface facing n — the flat colour, or the installed
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
- `;
84
+ /** rzWorldAmbient against the materials' and the ground's `light` uniform. */
85
+ export const WORLD_AMBIENT_WGSL = worldAmbientWgsl("light");
106
86
  /** Does this source define the emit mount? */
107
87
  export function hasLightEmit(wgsl) {
108
88
  return /\bfn\s+lightEmit\s*\(/.test(wgsl);
@@ -173,7 +153,7 @@ ${viewportApi("viewU[6].w")}
173
153
  ${trailSlotsApi(cast.trailCount)}
174
154
  // The id accessors, stubbed: this module cannot read an attachment the
175
155
  // scene pass writes. See id-api.ts — the author's whole file compiles here.
176
- ${idApi(false, 0, 0) + castDistanceStub()}
156
+ ${idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0)}
177
157
  // The dials the author declared, if any. A lamp is exactly the thing someone
178
158
  // retunes — its colour and its reach — so an emitter reads params like every
179
159
  // other mount rather than being the one place a #param resolves to nothing.
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA2BA,eAAO,MAAM,oBAAoB,QAmJhC,CAAC;AA6IF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,orMAgHzB,CAAA;AAQD,eAAO,MAAM,4BAA4B,QAC0D,CAAA"}
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. Unrolled — Safari's Metal backend
175
- // doesn't unroll nested shadow loops reliably. Texel sizes are interpolated
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 {
@@ -1 +1 @@
1
- {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AA2BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+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;6DACyD;IACzD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,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;AA+LD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,+ufAsI5B,CAAA;AA8MD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAclF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAwGtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
1
+ {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AA4BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+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;6DACyD;IACzD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,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;AA+LD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,+ufAsI5B,CAAA;AA8MD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAclF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAyGtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
@@ -5,6 +5,7 @@ import { CAST_API, CAST_MASK_ALL, subjectMaskApi } from "../cast-api";
5
5
  import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
6
6
  import { audioApi } from "../audio-api";
7
7
  import { idApi } from "../id-api";
8
+ import { sceneLightApi } from "../scene-light-api";
8
9
  import { castDistanceApi, CAST_FIELD_DIV } from "./cast-distance";
9
10
  import { lyricsApi, lyricsTextApi } from "../lyrics-api";
10
11
  import { midiApi } from "../midi-api";
@@ -681,6 +682,7 @@ export function buildFieldShader(effect) {
681
682
  viewportApi("viewU[6].w") +
682
683
  trailSlotsApi(effect.trailCount) +
683
684
  idApi(effect.ids === true, 0, 23) +
685
+ sceneLightApi(false, 0, 0) +
684
686
  // Distance to the cast, in SCREEN pixels: the field is half-res, so a field
685
687
  // texel is CAST_FIELD_DIV of them and the accessor scales on the way out.
686
688
  // An author writes the width they mean and never learns how it is built.
@@ -1 +1 @@
1
- {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAkC7C,eAAO,MAAM,QAAQ,OAAO,CAAA;AAC5B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,gDAAgD;AAChD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6B1F;AAgBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,SAAK,GAAG,MAAM,CAoFpG"}
1
+ {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAmC7C,eAAO,MAAM,QAAQ,OAAO,CAAA;AAC5B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,gDAAgD;AAChD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6B1F;AAgBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,SAAK,GAAG,MAAM,CAqFpG"}
@@ -7,6 +7,7 @@ import { subjectMaskApi } from "../cast-api";
7
7
  import { SCENE_TAP_STUB } from "../scene-tap";
8
8
  import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api";
9
9
  import { idApi } from "../id-api";
10
+ import { sceneLightApi } from "../scene-light-api";
10
11
  // A persistent grid an effect can step and read: the one thing an effect could
11
12
  // not have before, which is MEMORY.
12
13
  //
@@ -160,6 +161,7 @@ fn rzGrid(uv: vec2f) -> vec4f { return rzGridPrev(uv); }
160
161
  // Stubbed: this module cannot read an attachment the scene pass writes — see
161
162
  // id-api.ts. The author's whole file compiles here, so the names must exist.
162
163
  idApi(false, 0, 0) +
164
+ sceneLightApi(false, 0, 0) +
163
165
  // Inlined rather than imported from cast-distance, and NOT for tidiness.
164
166
  //
165
167
  // This module and composite.ts import each other — grid takes
@@ -14,7 +14,9 @@ export type CastLayout = {
14
14
  alias: number[];
15
15
  };
16
16
  /** How the author's quads combine with the scene. */
17
- type ParticleBlend = "alpha" | "additive";
17
+ /** `cutout` shades exactly like `alpha`; the difference is entirely in the
18
+ * pipeline — depth write on, alpha-to-coverage on. See directives.ts. */
19
+ type ParticleBlend = "alpha" | "additive" | "cutout";
18
20
  type ParticleSource = {
19
21
  /** The author's WGSL verbatim. */
20
22
  wgsl: string;
@@ -27,14 +29,30 @@ type ParticleSource = {
27
29
  * rule reads the same dial the shading does, and rain's fall speed is used
28
30
  * in the compute half. */
29
31
  paramsDecl: string;
32
+ /** The effect defines `particleCover(p, uv) -> f32`: the shape alone, cheap.
33
+ * A cutout then draws TWICE — coverage to depth first, then the shading at
34
+ * depthCompare equal — so the lighting runs only for the visible layer. A
35
+ * discard forbids the hardware's own early rejection on a tile-based GPU,
36
+ * where every stacked fragment is otherwise shaded in full; the prepass is
37
+ * the shape of that rejection, done by hand and cheaply. */
38
+ cover: boolean;
39
+ /** The side of this effect's grid, or 0 when it declared none. Particles READ
40
+ * the grid; the kernel that steps it lives in the grid pass. A blade of grass
41
+ * bending where a foot pressed is the whole case: the bend has to persist and
42
+ * recover, which is memory, and memory is what the grid is for. */
43
+ gridSize: number;
30
44
  };
45
+ /** Where the shading stage's five scene-light bindings start — see scene-light-api.ts. */
46
+ export declare const PARTICLE_LIGHT_BINDING = 10;
31
47
  /** Bytes per particle. Explicitly padded — see the struct below. */
32
48
  export declare const PARTICLE_STRIDE = 48;
33
- /** Does the source define the particle contract? All three are required together. */
49
+ /** Does the source define the particle contract? All three are required
50
+ * together; `cover` is optional and only means anything to a cutout. */
34
51
  export declare function particleEntryPoints(wgsl: string): {
35
52
  init: boolean;
36
53
  step: boolean;
37
54
  shade: boolean;
55
+ cover: boolean;
38
56
  };
39
57
  /**
40
58
  * Spawn, age, recycle.
@@ -1 +1 @@
1
- {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AA4BA,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb;;mFAE+E;IAC/E,UAAU,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAkCD,qDAAqD;AACrD,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,CAAA;AAEzC,KAAK,cAAc,GAAG;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;IACd;;+BAE2B;IAC3B,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAwDjC,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAMlG;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA8CxF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6HvF"}
1
+ {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AA8BA,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb;;mFAE+E;IAC/E,UAAU,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAkCD,qDAAqD;AACrD;0EAC0E;AAC1E,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAA;AAEpD,KAAK,cAAc,GAAG;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;IACd;;+BAE2B;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;iEAK6D;IAC7D,KAAK,EAAE,OAAO,CAAA;IACd;;;wEAGoE;IACpE,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,0FAA0F;AAC1F,eAAO,MAAM,sBAAsB,KAAK,CAAA;AAExC,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAwDjC;yEACyE;AACzE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAOlH;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAmDxF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA0LvF"}
@@ -1,4 +1,5 @@
1
1
  import { castDistanceStub } from "./cast-distance";
2
+ import { gridReadApi } from "./grid";
2
3
  import { RZ_LIGHT_STRUCT_WGSL } from "../lights";
3
4
  import { audioApi } from "../audio-api";
4
5
  import { lyricsApi } from "../lyrics-api";
@@ -9,6 +10,7 @@ import { EFFECT_SUBJECT_VEC4S } from "../cast-layout";
9
10
  import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
10
11
  import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract";
11
12
  import { idApi } from "../id-api";
13
+ import { sceneLightApi } from "../scene-light-api";
12
14
  /**
13
15
  * The trail accessors, in the PARTICLE module.
14
16
  *
@@ -39,6 +41,8 @@ fn _rzCastLive() -> i32 {
39
41
  trailSlotsApi(cast.trailCount) +
40
42
  anchorAliasWgsl(cast.alias));
41
43
  }
44
+ /** Where the shading stage's five scene-light bindings start — see scene-light-api.ts. */
45
+ export const PARTICLE_LIGHT_BINDING = 10;
42
46
  /** Bytes per particle. Explicitly padded — see the struct below. */
43
47
  export const PARTICLE_STRIDE = 48;
44
48
  const CAMERA_STRUCT = /* wgsl */ `
@@ -91,12 +95,14 @@ fn rzProject(p: vec3f) -> vec3f {
91
95
  }
92
96
  fn rzCamPos() -> vec3f { return cam.camPos; }
93
97
  `;
94
- /** Does the source define the particle contract? All three are required together. */
98
+ /** Does the source define the particle contract? All three are required
99
+ * together; `cover` is optional and only means anything to a cutout. */
95
100
  export function particleEntryPoints(wgsl) {
96
101
  return {
97
102
  init: /\bfn\s+particleInit\s*\(/.test(wgsl),
98
103
  step: /\bfn\s+particleStep\s*\(/.test(wgsl),
99
104
  shade: /\bfn\s+particleShade\s*\(/.test(wgsl),
105
+ cover: /\bfn\s+particleCover\s*\(/.test(wgsl),
100
106
  };
101
107
  }
102
108
  /**
@@ -123,10 +129,15 @@ ${src.paramsDecl}
123
129
  audioApi(0, 4) +
124
130
  midiApi(0, 5) +
125
131
  lyricsApi(0, 6) +
132
+ // Always, even with no grid — then it samples a 1x1 of zeroes. Same reason
133
+ // composite compiles it unconditionally, plus one this module owns: an
134
+ // effect's whole file is spliced here, so a `gridStep` written for the grid
135
+ // pass has to RESOLVE here too, and the stubs that let it are in here.
136
+ gridReadApi(0, 8, 9, src.gridSize) +
126
137
  PRELUDE +
127
138
  // Stubbed: this module cannot read an attachment the scene pass writes — see
128
139
  // id-api.ts. The author's whole file compiles here, so the names must exist.
129
- idApi(false, 0, 0) + castDistanceStub() +
140
+ idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) +
130
141
  "\n// ── user effect ──\n" +
131
142
  src.wgsl +
132
143
  /* wgsl */ `
@@ -168,7 +179,13 @@ fn main(@builtin(global_invocation_id) gid: vec3u) {
168
179
  */
169
180
  export function buildParticleRenderShader(src, cast) {
170
181
  return (`override BLOOM: bool = ${src.bloom ? "true" : "false"};
171
- override ADDITIVE: bool = ${src.blend === "additive" ? "true" : "false"};\n` +
182
+ override ADDITIVE: bool = ${src.blend === "additive" ? "true" : "false"};
183
+ override CUTOUT: bool = ${src.blend === "cutout" ? "true" : "false"};
184
+ // A cutout with a prepass: the depth is already written and this pass draws at
185
+ // depthCompare equal, so it neither discards nor masks — the depth test IS the
186
+ // coverage, per sample, and a shader with no discard keeps the hardware's
187
+ // early rejection.
188
+ override PREPASSED: bool = ${src.blend === "cutout" && src.cover ? "true" : "false"};\n` +
172
189
  PARTICLE_STRUCT_WGSL +
173
190
  CAMERA_STRUCT +
174
191
  PARTICLE_UNIFORMS +
@@ -183,10 +200,18 @@ ${src.paramsDecl}
183
200
  audioApi(0, 4) +
184
201
  midiApi(0, 5) +
185
202
  lyricsApi(0, 6) +
203
+ // Always, even with no grid — then it samples a 1x1 of zeroes. Same reason
204
+ // composite compiles it unconditionally, plus one this module owns: an
205
+ // effect's whole file is spliced here, so a `gridStep` written for the grid
206
+ // pass has to RESOLVE here too, and the stubs that let it are in here.
207
+ gridReadApi(0, 8, 9, src.gridSize) +
186
208
  PRELUDE +
187
209
  // Stubbed: this module cannot read an attachment the scene pass writes — see
188
210
  // id-api.ts. The author's whole file compiles here, so the names must exist.
189
211
  idApi(false, 0, 0) + castDistanceStub() +
212
+ // The sun's shadow and the world's ambient, for real: the shadow pass has
213
+ // run by the time this draws, inside the scene pass. See scene-light-api.ts.
214
+ sceneLightApi(true, 0, PARTICLE_LIGHT_BINDING) +
190
215
  "\n// ── user effect ──\n" +
191
216
  src.wgsl +
192
217
  /* wgsl */ `
@@ -239,6 +264,12 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
239
264
 
240
265
  struct FSOut {
241
266
  @location(0) color: vec4f,
267
+ // Which of the pixel's MSAA samples this fragment covers. Alpha-to-coverage
268
+ // by hand: the hardware kind reads alpha off the first colour target, and
269
+ // the HDR target is rg11b10, which has no alpha to read. Every particle
270
+ // writes it; a blended one writes all four, which is what not declaring it
271
+ // would have meant.
272
+ @builtin(sample_mask) samples: u32,
242
273
  // The scene's aux target: (bloom mask, coverage). Materials write it, so a
243
274
  // particle that skipped it would punch a hole in the mask of whatever it drew
244
275
  // over.
@@ -266,8 +297,25 @@ fn fs(in: VSOut) -> FSOut {
266
297
  c.a *= pu.weight;
267
298
  // Weight 0 therefore discards every fragment, so an effect faded out costs
268
299
  // nothing past the vertex stage even on the frame the draw is still issued.
269
- if (c.a <= 0.0) { discard; }
300
+ // Not after a prepass: that pass already decided coverage into depth, and
301
+ // a discard here would cost this pass the early rejection it exists for.
302
+ if (!PREPASSED && c.a <= 0.0) { discard; }
270
303
  var out: FSOut;
304
+ out.samples = 0xFu;
305
+ if (PREPASSED) {
306
+ c.a = 1.0;
307
+ } else if (CUTOUT) {
308
+ // Alpha becomes coverage: 0..4 of the pixel's samples, and the fragment is
309
+ // then opaque wherever it lands. The pattern is turned by pixel position
310
+ // so a half-covered edge is not the same two samples on every pixel along
311
+ // it, which is what makes a2c edges look dithered rather than stepped.
312
+ let n = u32(clamp(c.a, 0.0, 1.0) * 4.0 + 0.5);
313
+ if (n == 0u) { discard; }
314
+ let low = (1u << n) - 1u;
315
+ let turn = (u32(in.clip.x) + u32(in.clip.y)) & 3u;
316
+ out.samples = ((low << turn) | (low >> (4u - turn))) & 0xFu;
317
+ c.a = 1.0;
318
+ }
271
319
  // PREMULTIPLIED: the scene's colour target blends with srcFactor \"one\", so a
272
320
  // straight-alpha fragment would come out over-bright wherever it is
273
321
  // translucent — which is most of a soft particle.
@@ -288,5 +336,29 @@ fn fs(in: VSOut) -> FSOut {
288
336
  out.mask = vec4f(mg.x, mg.y, 0.0, c.a);
289
337
  ${sceneIdPadWgsl("out")} return out;
290
338
  }
291
- `);
339
+ ` +
340
+ (src.blend === "cutout" && src.cover
341
+ ? /* wgsl */ `
342
+ /**
343
+ * The prepass: the shape to depth, nothing to colour. Every output is still
344
+ * declared — the pipeline takes the colour targets at writeMask 0 — and the
345
+ * sample mask is exactly what the single-pass cutout would have written, so
346
+ * the shading pass's equal test lands on the same samples.
347
+ */
348
+ @fragment
349
+ fn fsDepth(in: VSOut) -> FSOut {
350
+ let p = particles[in.id];
351
+ let a = particleCover(p, in.uv) * pu.weight;
352
+ let n = u32(clamp(a, 0.0, 1.0) * 4.0 + 0.5);
353
+ if (n == 0u) { discard; }
354
+ let low = (1u << n) - 1u;
355
+ let turn = (u32(in.clip.x) + u32(in.clip.y)) & 3u;
356
+ var out: FSOut;
357
+ out.samples = ((low << turn) | (low >> (4u - turn))) & 0xFu;
358
+ out.color = vec4f(0.0);
359
+ out.mask = vec4f(0.0);
360
+ ${sceneIdPadWgsl("out")} return out;
361
+ }
362
+ `
363
+ : ""));
292
364
  }
@@ -11,6 +11,9 @@ type TrailSource = {
11
11
  /** Additive, like most glowing ribbons, or straight alpha. */
12
12
  blend: "alpha" | "additive";
13
13
  bloom: boolean;
14
+ /** The side of this effect's grid, or 0 when it declared none. See the note
15
+ * on the same field in particles.ts: a ribbon reads the grid, never steps it. */
16
+ gridSize: number;
14
17
  };
15
18
  /**
16
19
  * Sub-segments drawn between each pair of recorded samples.
@@ -1 +1 @@
1
- {"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AA4BA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CAyZR"}
1
+ {"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AA8BA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;IACd;sFACkF;IAClF,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CA6ZR"}
@@ -1,4 +1,5 @@
1
1
  import { castDistanceStub } from "./cast-distance";
2
+ import { gridReadApi } from "./grid";
2
3
  import { RZ_LIGHT_STRUCT_WGSL } from "../lights";
3
4
  import { audioApi } from "../audio-api";
4
5
  import { lyricsApi } from "../lyrics-api";
@@ -9,6 +10,7 @@ import { EFFECT_SUBJECT_VEC4S } from "../cast-layout";
9
10
  import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
10
11
  import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract";
11
12
  import { idApi } from "../id-api";
13
+ import { sceneLightApi } from "../scene-light-api";
12
14
  /**
13
15
  * Sub-segments drawn between each pair of recorded samples.
14
16
  *
@@ -240,9 +242,13 @@ fn rzTurnRadius(a: vec3f, b: vec3f, c: vec3f) -> f32 {
240
242
  audioApi(0, 4) +
241
243
  midiApi(0, 5) +
242
244
  lyricsApi(0, 6) +
245
+ // Always, with or without a grid — and required either way, because an
246
+ // effect's whole file is spliced here and a `gridStep` written for the grid
247
+ // pass has to resolve in this module too.
248
+ gridReadApi(0, 8, 9, src.gridSize) +
243
249
  // Stubbed: this module cannot read an attachment the scene pass writes — see
244
250
  // id-api.ts. The author's whole file compiles here, so the names must exist.
245
- idApi(false, 0, 0) + castDistanceStub() +
251
+ idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) +
246
252
  "\n// ── user effect ──\n" +
247
253
  src.wgsl +
248
254
  /* wgsl */ `
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The world's light at a surface facing n — the flat colour, or the installed
3
+ * HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
4
+ * ibl.ts for the folding; the shader is a plain polynomial in the normal).
5
+ *
6
+ * Written against whichever name the module gives the light uniform, so the
7
+ * materials, the ground and an effect all evaluate one polynomial.
8
+ */
9
+ export declare const worldAmbientWgsl: (u: string) => string;
10
+ /**
11
+ * 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
12
+ *
13
+ * Shared with the materials, so an effect's shadow and her own shadow soften
14
+ * alike. The sampler is linear-filtered, which makes each tap a 2×2 bilinear
15
+ * compare. Unrolled — Safari's Metal backend doesn't unroll nested shadow
16
+ * loops reliably. The body expects `ndc` in scope and returns.
17
+ */
18
+ export declare const pcf9: (map: string, ts: string, sampler?: string) => string;
19
+ /** Bindings the real accessors take, from `binding` upward. */
20
+ export declare const SCENE_LIGHT_API_BINDINGS = 5;
21
+ /**
22
+ * `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
23
+ * caster's shadow, soft in between.
24
+ *
25
+ * `rzWorldAmbient(n)`: the world's light arriving at a surface facing `n` —
26
+ * the materials' own ambient, so an effect's shade can match hers.
27
+ *
28
+ * With `on`, declares five bindings from `binding`:
29
+ * +0 the light uniform (ambient, the sun — direction, and in .w the shadow
30
+ * switch — and the HDRI's irradiance)
31
+ * +1 the cascade view-projections, then the caster sphere
32
+ * +2 the near cascade's map +3 the far cascade's map
33
+ * +4 the comparison sampler
34
+ *
35
+ * It takes a POINT and no normal. The materials' sampleShadow also rejects a
36
+ * surface facing away from the sun, which is right for skin and wrong for a
37
+ * blade of grass or a petal: those are thin, lit through, and shaded by their
38
+ * own N·L. The point is nudged toward the sun instead, which lifts a blade's
39
+ * root off a stage floor that casts into the same map.
40
+ */
41
+ export declare function sceneLightApi(on: boolean, group: number, binding: number): string;
42
+ //# sourceMappingURL=scene-light-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-light-api.d.ts","sourceRoot":"","sources":["../../src/shaders/scene-light-api.ts"],"names":[],"mappings":"AAkBA;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,GAAG,MAAM,WAazC,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,GAAI,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,gBAAyB,WActE,CAAA;AAED,+DAA+D;AAC/D,eAAO,MAAM,wBAAwB,IAAI,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA2DjF"}
@@ -0,0 +1,144 @@
1
+ import { SHADOW_CASCADES } from "../shadow-cascades";
2
+ // The scene's own light, as an effect author sees it: the sun's shadow and the
3
+ // world's ambient.
4
+ //
5
+ // A lawn standing in a character's shadow has to darken where the floor under
6
+ // it does, or the figure floats: the ground catcher draws her shadow, and the
7
+ // blades drawn over it hide it. And the shade has to be the SCENE'S shade — her
8
+ // shaded side takes the world's colour, and a lawn shaded by its own guess
9
+ // reads as a patch of different flowers beside her. Both are already computed
10
+ // for the materials every frame; this hands the same lookups to effects.
11
+ //
12
+ // Bound for real in the particle SHADING stage only. That is where geometry an
13
+ // effect draws inside the scene pass is shaded, and the shadow pass has already
14
+ // run by then. Every other module that compiles the author's file gets stubs:
15
+ // rzShadow answers 1, lit, which is what a point with no occlusion information
16
+ // is — the same answer sampleShadow gives outside every cascade — and
17
+ // rzWorldAmbient answers black, no light.
18
+ /**
19
+ * The world's light at a surface facing n — the flat colour, or the installed
20
+ * HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
21
+ * ibl.ts for the folding; the shader is a plain polynomial in the normal).
22
+ *
23
+ * Written against whichever name the module gives the light uniform, so the
24
+ * materials, the ground and an effect all evaluate one polynomial.
25
+ */
26
+ export const worldAmbientWgsl = (u) => /* wgsl */ `
27
+ fn rzWorldAmbient(n: vec3f) -> vec3f {
28
+ if (${u}.sh[0].w < 0.5) { return ${u}.ambientColor.xyz; }
29
+ let x = n.x;
30
+ let y = n.y;
31
+ let z = n.z;
32
+ let c = ${u}.sh[0].xyz
33
+ + ${u}.sh[1].xyz * y + ${u}.sh[2].xyz * z + ${u}.sh[3].xyz * x
34
+ + ${u}.sh[4].xyz * (x * y) + ${u}.sh[5].xyz * (y * z)
35
+ + ${u}.sh[6].xyz * (3.0 * z * z - 1.0) + ${u}.sh[7].xyz * (x * z)
36
+ + ${u}.sh[8].xyz * (x * x - y * y);
37
+ return max(c, vec3f(0.0));
38
+ }
39
+ `;
40
+ /**
41
+ * 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
42
+ *
43
+ * Shared with the materials, so an effect's shadow and her own shadow soften
44
+ * alike. The sampler is linear-filtered, which makes each tap a 2×2 bilinear
45
+ * compare. Unrolled — Safari's Metal backend doesn't unroll nested shadow
46
+ * loops reliably. The body expects `ndc` in scope and returns.
47
+ */
48
+ export const pcf9 = (map, ts, sampler = "shadowSampler") => /* wgsl */ `
49
+ let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
50
+ let cmpZ = ndc.z - 0.001;
51
+ let ts = ${ts};
52
+ let s00 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, -ts), cmpZ);
53
+ let s10 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(0.0, -ts), cmpZ);
54
+ let s20 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, -ts), cmpZ);
55
+ let s01 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, 0.0), cmpZ);
56
+ let s11 = textureSampleCompareLevel(${map}, ${sampler}, suv, cmpZ);
57
+ let s21 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, 0.0), cmpZ);
58
+ let s02 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, ts), cmpZ);
59
+ let s12 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(0.0, ts), cmpZ);
60
+ let s22 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, ts), cmpZ);
61
+ return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
62
+ `;
63
+ /** Bindings the real accessors take, from `binding` upward. */
64
+ export const SCENE_LIGHT_API_BINDINGS = 5;
65
+ /**
66
+ * `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
67
+ * caster's shadow, soft in between.
68
+ *
69
+ * `rzWorldAmbient(n)`: the world's light arriving at a surface facing `n` —
70
+ * the materials' own ambient, so an effect's shade can match hers.
71
+ *
72
+ * With `on`, declares five bindings from `binding`:
73
+ * +0 the light uniform (ambient, the sun — direction, and in .w the shadow
74
+ * switch — and the HDRI's irradiance)
75
+ * +1 the cascade view-projections, then the caster sphere
76
+ * +2 the near cascade's map +3 the far cascade's map
77
+ * +4 the comparison sampler
78
+ *
79
+ * It takes a POINT and no normal. The materials' sampleShadow also rejects a
80
+ * surface facing away from the sun, which is right for skin and wrong for a
81
+ * blade of grass or a petal: those are thin, lit through, and shaded by their
82
+ * own N·L. The point is nudged toward the sun instead, which lifts a blade's
83
+ * root off a stage floor that casts into the same map.
84
+ */
85
+ export function sceneLightApi(on, group, binding) {
86
+ if (!on) {
87
+ return /* wgsl */ `
88
+ fn rzShadow(p: vec3f) -> f32 { return 1.0; }
89
+ fn rzWorldAmbient(n: vec3f) -> vec3f { return vec3f(0.0); }
90
+ `;
91
+ }
92
+ const n = SHADOW_CASCADES.length;
93
+ return /* wgsl */ `
94
+ // The light uniform, as the materials lay it out; lights[0] is the sun.
95
+ struct _RzSceneLight { direction: vec4f, color: vec4f, }
96
+ struct _RzSceneLightU { ambientColor: vec4f, lights: array<_RzSceneLight, 4>, sh: array<vec4f, 9>, }
97
+ // The cascades' matrices, inner to outer, then every caster in one sphere
98
+ // (x, y, z, radius): radius 0 = nothing casts, negative = do not test.
99
+ struct _RzShadowBlock { viewProj: array<mat4x4f, ${n}>, casters: vec4f, }
100
+ @group(${group}) @binding(${binding}) var<uniform> _rzLight: _RzSceneLightU;
101
+ @group(${group}) @binding(${binding + 1}) var<uniform> _rzShadowVP: _RzShadowBlock;
102
+ @group(${group}) @binding(${binding + 2}) var _rzShadowNear: texture_depth_2d;
103
+ @group(${group}) @binding(${binding + 3}) var _rzShadowFar: texture_depth_2d;
104
+ @group(${group}) @binding(${binding + 4}) var _rzShadowCmp: sampler_comparison;
105
+
106
+ fn _rzShadowNearTaps(ndc: vec3f) -> f32 {
107
+ ${pcf9("_rzShadowNear", `1.0 / ${SHADOW_CASCADES[0].mapSize}.0`, "_rzShadowCmp")}
108
+ }
109
+
110
+ fn _rzShadowFarTaps(ndc: vec3f) -> f32 {
111
+ ${pcf9("_rzShadowFar", `1.0 / ${SHADOW_CASCADES[n - 1].mapSize}.0`, "_rzShadowCmp")}
112
+ }
113
+
114
+ fn rzShadow(p: vec3f) -> f32 {
115
+ // The scene's one shadow switch, on the sun — see sampleShadow.
116
+ let castAmt = _rzLight.lights[0].direction.w;
117
+ if (castAmt <= 0.0) { return 1.0; }
118
+ let toSun = normalize(-_rzLight.lights[0].direction.xyz);
119
+ // Can anything cast onto this point at all? The ground's test, for the same
120
+ // two reasons: most of a lawn is nowhere near her, and a scene with no
121
+ // models never renders the map, whose untouched zeroes compare as shadowed.
122
+ let cs = _rzShadowVP.casters;
123
+ if (cs.w == 0.0) { return 1.0; }
124
+ if (cs.w > 0.0) {
125
+ let toCaster = cs.xyz - p;
126
+ let along = dot(toCaster, toSun);
127
+ let perp = length(toCaster - toSun * along);
128
+ if (along <= -cs.w || perp > cs.w) { return 1.0; }
129
+ }
130
+ let q = p + toSun * 0.08;
131
+ let c0 = _rzShadowVP.viewProj[0] * vec4f(q, 1.0);
132
+ let n0 = c0.xyz / max(c0.w, 1e-6);
133
+ if (all(abs(n0.xy) < vec2f(0.98)) && n0.z > 0.0 && n0.z < 1.0) {
134
+ return mix(1.0, _rzShadowNearTaps(n0), castAmt);
135
+ }
136
+ let c1 = _rzShadowVP.viewProj[${n - 1}] * vec4f(q, 1.0);
137
+ let n1 = c1.xyz / max(c1.w, 1e-6);
138
+ if (all(abs(n1.xy) < vec2f(0.98)) && n1.z > 0.0 && n1.z < 1.0) {
139
+ return mix(1.0, _rzShadowFarTaps(n1), castAmt);
140
+ }
141
+ return 1.0;
142
+ }
143
+ ${worldAmbientWgsl("_rzLight")}`;
144
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
3
+ *
4
+ * Shared with the materials, so an effect's shadow and her own shadow soften
5
+ * alike. The sampler is linear-filtered, which makes each tap a 2×2 bilinear
6
+ * compare. Unrolled — Safari's Metal backend doesn't unroll nested shadow
7
+ * loops reliably. The body expects `ndc` in scope and returns.
8
+ */
9
+ export declare const pcf9: (map: string, ts: string, sampler?: string) => string;
10
+ /** Bindings the real accessor takes, from `binding` upward. */
11
+ export declare const SHADOW_API_BINDINGS = 5;
12
+ /**
13
+ * `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
14
+ * caster's shadow, soft in between.
15
+ *
16
+ * With `on`, declares five bindings from `binding`:
17
+ * +0 the light uniform (the sun's direction, and in .w the shadow switch)
18
+ * +1 the cascade view-projections, then the caster sphere
19
+ * +2 the near cascade's map +3 the far cascade's map
20
+ * +4 the comparison sampler
21
+ *
22
+ * It takes a POINT and no normal. The materials' sampleShadow also rejects a
23
+ * surface facing away from the sun, which is right for skin and wrong for a
24
+ * blade of grass or a petal: those are thin, lit through, and shaded by their
25
+ * own N·L. The point is nudged toward the sun instead, which lifts a blade's
26
+ * root off a stage floor that casts into the same map.
27
+ */
28
+ export declare function shadowApi(on: boolean, group: number, binding: number): string;
29
+ //# sourceMappingURL=shadow-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shadow-api.d.ts","sourceRoot":"","sources":["../../src/shaders/shadow-api.ts"],"names":[],"mappings":"AAcA;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,GAAI,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,gBAAyB,WActE,CAAA;AAED,+DAA+D;AAC/D,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAyD7E"}