reze-engine 0.62.1 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/engine.d.ts +95 -57
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +466 -162
  4. package/dist/graph/compile.d.ts.map +1 -1
  5. package/dist/graph/compile.js +4 -1
  6. package/dist/graph/registry.d.ts +3 -0
  7. package/dist/graph/registry.d.ts.map +1 -1
  8. package/dist/graph/registry.js +133 -0
  9. package/dist/graph/schema.d.ts +12 -0
  10. package/dist/graph/schema.d.ts.map +1 -1
  11. package/dist/graph/slots.d.ts.map +1 -1
  12. package/dist/graph/slots.js +11 -20
  13. package/dist/index.d.ts +9 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/shaders/lights.d.ts +3 -2
  17. package/dist/shaders/lights.d.ts.map +1 -1
  18. package/dist/shaders/lights.js +16 -3
  19. package/dist/shaders/materials/common.d.ts.map +1 -1
  20. package/dist/shaders/materials/common.js +63 -56
  21. package/dist/shaders/materials/nodes.d.ts +1 -1
  22. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  23. package/dist/shaders/materials/nodes.js +210 -0
  24. package/dist/shaders/passes/composite.d.ts.map +1 -1
  25. package/dist/shaders/passes/composite.js +13 -1
  26. package/dist/shaders/passes/cull.d.ts +1 -1
  27. package/dist/shaders/passes/cull.d.ts.map +1 -1
  28. package/dist/shaders/passes/cull.js +7 -3
  29. package/dist/shaders/passes/ground.d.ts.map +1 -1
  30. package/dist/shaders/passes/ground.js +32 -55
  31. package/dist/shaders/passes/particles.d.ts +11 -1
  32. package/dist/shaders/passes/particles.d.ts.map +1 -1
  33. package/dist/shaders/passes/particles.js +9 -4
  34. package/dist/shaders/scene-light-api.d.ts +18 -10
  35. package/dist/shaders/scene-light-api.d.ts.map +1 -1
  36. package/dist/shaders/scene-light-api.js +58 -46
  37. package/dist/shadow-cascades.d.ts +47 -20
  38. package/dist/shadow-cascades.d.ts.map +1 -1
  39. package/dist/shadow-cascades.js +36 -13
  40. package/dist/unity/character.d.ts +59 -0
  41. package/dist/unity/character.d.ts.map +1 -0
  42. package/dist/unity/character.js +120 -0
  43. package/dist/unity/globals.d.ts +93 -0
  44. package/dist/unity/globals.d.ts.map +1 -0
  45. package/dist/unity/globals.js +340 -0
  46. package/dist/unity/host.d.ts +137 -0
  47. package/dist/unity/host.d.ts.map +1 -0
  48. package/dist/unity/host.js +489 -0
  49. package/dist/unity/looks.d.ts +88 -0
  50. package/dist/unity/looks.d.ts.map +1 -0
  51. package/dist/unity/looks.js +295 -0
  52. package/dist/unity/skin.d.ts +9 -0
  53. package/dist/unity/skin.d.ts.map +1 -0
  54. package/dist/unity/skin.js +117 -0
  55. package/dist/unity/stage.d.ts +148 -0
  56. package/dist/unity/stage.d.ts.map +1 -0
  57. package/dist/unity/stage.js +375 -0
  58. package/dist/unity/state.d.ts +22 -0
  59. package/dist/unity/state.d.ts.map +1 -0
  60. package/dist/unity/state.js +170 -0
  61. package/package.json +1 -1
  62. package/src/engine.ts +504 -176
  63. package/src/graph/compile.ts +3 -1
  64. package/src/graph/registry.ts +142 -0
  65. package/src/graph/schema.ts +9 -0
  66. package/src/graph/slots.ts +11 -20
  67. package/src/index.ts +11 -0
  68. package/src/shaders/lights.ts +16 -3
  69. package/src/shaders/materials/common.ts +63 -57
  70. package/src/shaders/materials/nodes.ts +210 -0
  71. package/src/shaders/passes/composite.ts +13 -1
  72. package/src/shaders/passes/cull.ts +7 -3
  73. package/src/shaders/passes/ground.ts +32 -55
  74. package/src/shaders/passes/particles.ts +19 -5
  75. package/src/shaders/scene-light-api.ts +59 -46
  76. package/src/shadow-cascades.ts +61 -32
  77. package/src/unity/character.ts +169 -0
  78. package/src/unity/globals.ts +443 -0
  79. package/src/unity/host.ts +587 -0
  80. package/src/unity/looks.ts +372 -0
  81. package/src/unity/skin.ts +128 -0
  82. package/src/unity/stage.ts +568 -0
  83. package/src/unity/state.ts +172 -0
@@ -129,10 +129,14 @@ fn cs(@builtin(global_invocation_id) gid: vec3<u32>) {
129
129
  // a hidden material is not culled, it is switched off, and answering "is this
130
130
  // gone because of culling?" requires the two to stay separable.
131
131
  let off = frusta.counts.y == 0u;
132
- let shown = hidden[i] == 0u;
132
+ // hidden: bit 0 switched off; bit 1 drawn by the native-material host
133
+ // (unity/host.ts) rather than by its graph — no camera or mirror draw here,
134
+ // and still its shadow, which the engine's pass casts for every material.
135
+ let shown = (hidden[i] & 1u) == 0u;
136
+ let graph = (hidden[i] & 2u) == 0u;
133
137
  let castsShadow = (dm.flags & DRAW_CASTS_SHADOW) != 0u;
134
- cameraArgs[i * 5u + 1u] = select(0u, 1u, (inCamera || off) && shown);
138
+ cameraArgs[i * 5u + 1u] = select(0u, 1u, (inCamera || off) && shown && graph);
135
139
  shadowArgs[i * 5u + 1u] = select(0u, 1u, ((inLight && castsShadow) || off) && shown);
136
- mirrorArgs[i * 5u + 1u] = select(0u, 1u, (inMirror || off) && shown);
140
+ mirrorArgs[i * 5u + 1u] = select(0u, 1u, (inMirror || off) && shown && graph);
137
141
  }
138
142
  `;
@@ -1 +1 @@
1
- {"version":3,"file":"ground.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground.ts"],"names":[],"mappings":"AAgFA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,UAAQ,GAAG,MAAM,CAoWrD;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAA;AACxC,eAAO,MAAM,gBAAgB,QAAS,CAAA;AAEtC;;;oEAGoE;AACpE,eAAO,MAAM,iBAAiB,OAAO,CAAA;AAErC;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,+oCAiClC,CAAA"}
1
+ {"version":3,"file":"ground.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground.ts"],"names":[],"mappings":"AAoFA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,UAAQ,GAAG,MAAM,CAyUrD;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAA;AACxC,eAAO,MAAM,gBAAgB,QAAS,CAAA;AAEtC;;;oEAGoE;AACpE,eAAO,MAAM,iBAAiB,OAAO,CAAA;AAErC;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,+oCAiClC,CAAA"}
@@ -1,6 +1,7 @@
1
1
  import { sceneFsOutWgsl, sceneIdWriteWgsl } from "./scene-contract";
2
2
  import { lightsApi } from "../lights";
3
- import { SHADOW_CASCADES } from "../../shadow-cascades";
3
+ import { SHADOW_ATLAS_SIZE, SHADOW_CASCADES } from "../../shadow-cascades";
4
+ import { SHADOW_MARGIN } from "../scene-light-api";
4
5
  import { WORLD_AMBIENT_WGSL } from "../lights";
5
6
  // Ground shadow-catcher: receives directional shadow, grid lines, frosted noise,
6
7
  // radial distance fade. Writes bloom mask = 0 (ground never bloom-bleeds).
@@ -33,6 +34,9 @@ const SOFT_MAX_SPREAD = 14;
33
34
  * normalised, so there is no `/ 9.0` left at the call site.
34
35
  */
35
36
  function pcfWgsl(map, uv, texel, z, acc, pad, soft) {
37
+ // Every tap stays inside the cascade's own tile (tileLo..tileHi, in scope at
38
+ // the call): the soft disk reaches further than the selection margin, and a
39
+ // tap in the neighbouring tile compares against another cascade's depths.
36
40
  const golden = 2.39996323;
37
41
  // ONE BRANCH IS COMPILED, NOT BOTH.
38
42
  //
@@ -55,7 +59,7 @@ function pcfWgsl(map, uv, texel, z, acc, pad, soft) {
55
59
  ` for (var x = -1; x <= 1; x++) {`,
56
60
  // ...Level, not the implicit-derivative form: identical on a single-mip
57
61
  // shadow map, and legal inside a branch.
58
- ` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, ${uv} + vec2f(f32(x), f32(y)) * st, ${z});`,
62
+ ` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, clamp(${uv} + vec2f(f32(x), f32(y)) * st, tileLo, tileHi), ${z});`,
59
63
  ` }`,
60
64
  `}`,
61
65
  `${acc} *= ${1 / 9};`,
@@ -68,7 +72,7 @@ function pcfWgsl(map, uv, texel, z, acc, pad, soft) {
68
72
  // keeps successive taps from lining up into spokes.
69
73
  ` let r = sqrt((fs + 0.5) * ${1 / SOFT_TAPS});`,
70
74
  ` let a = fs * ${golden} + rot;`,
71
- ` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, ${uv} + vec2f(cos(a), sin(a)) * (r * radius), ${z});`,
75
+ ` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, clamp(${uv} + vec2f(cos(a), sin(a)) * (r * radius), tileLo, tileHi), ${z});`,
72
76
  `}`,
73
77
  `${acc} *= ${1 / SOFT_TAPS};`,
74
78
  ];
@@ -85,12 +89,10 @@ struct Light { direction: vec4f, color: vec4f, };
85
89
  struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, sh: array<vec4f, 9>, };
86
90
  struct GroundShadowMat {
87
91
  diffuseColor: vec3f, fadeStart: f32,
88
- fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, gridSpacing: f32,
92
+ fadeEnd: f32, shadowStrength: f32, _pad6: f32, gridSpacing: f32,
89
93
  gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, opacity: f32,
90
94
  gridLineColor: vec3f, mirror: f32,
91
- // farCascade: 1 while a stage is loaded, 0 otherwise. See the branch below —
92
- // with no stage the far map is never drawn into, so its taps are known.
93
- mirrorBlur: f32, farCascade: f32, shadowSoftness: f32, groundY: f32,
95
+ mirrorBlur: f32, _pad17: f32, shadowSoftness: f32, groundY: f32,
94
96
  // Every shadow caster in one sphere, refreshed per frame. w = radius; 0 means
95
97
  // nothing casts, negative means "do not use this" (a rigid caster has no
96
98
  // sphere, so a scene with a stage keeps the taps). See rzShadowPossible.
@@ -101,12 +103,11 @@ struct GroundShadowMat {
101
103
  struct LightVP { viewProj: array<mat4x4f, ${SHADOW_CASCADES.length}>, };
102
104
  @group(0) @binding(0) var<uniform> camera: CameraUniforms;
103
105
  @group(0) @binding(1) var<uniform> light: LightUniforms;
104
- @group(0) @binding(2) var shadowMap: texture_depth_2d;
106
+ // The sun's shadow atlas — see shadow-cascades.ts.
107
+ @group(0) @binding(2) var shadowAtlas: texture_depth_2d;
105
108
  @group(0) @binding(3) var shadowSampler: sampler_comparison;
106
109
  @group(0) @binding(4) var<uniform> material: GroundShadowMat;
107
110
  @group(0) @binding(5) var<uniform> lightVP: LightVP;
108
- // The far cascade's map, binding 7 as in the materials' layout.
109
- @group(0) @binding(7) var shadowMapFar: texture_depth_2d;
110
111
  // The floor mirror (step 7D): the reflection target, the camera that rendered
111
112
  // it, and an ordinary sampler — the two shadow bindings above are comparison.
112
113
  // params = (projA, projB, _, _) — the depth-linearisation pair of the SHARED
@@ -140,7 +141,7 @@ struct MirrorVP { viewProj: mat4x4f, params: vec4f, };
140
141
  struct MirrorClip { plane: vec4f, on: f32, _p0: f32, _p1: f32, _p2: f32, };
141
142
  @group(0) @binding(14) var<uniform> clip: MirrorClip;
142
143
  ${WORLD_AMBIENT_WGSL}
143
- ${lightsApi(0, 6)}
144
+ ${lightsApi(0, 6, "1u")}
144
145
 
145
146
  struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };
146
147
  @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {
@@ -174,21 +175,6 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
174
175
  return out;
175
176
  }
176
177
 
177
- // Outside a cascade's frustum there IS no shadow information — the clamped
178
- // border samples compare against unrelated depths and read "shadowed",
179
- // darkening the whole far plane with a visible band at the frustum edge
180
- // (masked by the opaque surface normally, glaring in green-screen mode where
181
- // only the shadow-catcher term renders). So each cascade fades out near its
182
- // border — the near one INTO the far one's answer, the far one into lit.
183
- let l0 = lightVP.viewProj[0] * vec4f(i.worldPos, 1.0);
184
- let ndc = l0.xyz / max(l0.w, 1e-6);
185
- let inZ = select(0.0, 1.0, ndc.z > 0.0 && ndc.z < 1.0);
186
- let frustum = (1.0 - smoothstep(0.88, 0.96, abs(ndc.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc.y))) * inZ;
187
- let l1 = lightVP.viewProj[1] * vec4f(i.worldPos, 1.0);
188
- let ndc1 = l1.xyz / max(l1.w, 1e-6);
189
- let inZ1 = select(0.0, 1.0, ndc1.z > 0.0 && ndc1.z < 1.0);
190
- let frustum1 = (1.0 - smoothstep(0.88, 0.96, abs(ndc1.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc1.y))) * inZ1;
191
-
192
178
  // THE cost of this shader, measured: a full-screen ground ran 25 shadow
193
179
  // comparisons per pixel, and each one is hardware-bilinear, so ~100 depth
194
180
  // samples per pixel. Gutting the rest of the shader changed nothing; cutting
@@ -197,11 +183,9 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
197
183
  // 3x3 at two-texel spacing covers the same +/-2 texel penumbra the 5x5 at
198
184
  // one-texel spacing did — that kernel was oversampled, its taps overlapping
199
185
  // almost entirely once the comparison sampler's own 2x2 filter is counted.
200
- // Nine taps instead of twenty-five for the same blur radius.
186
+ // Nine taps instead of twenty-five for the same blur radius, and one
187
+ // cascade's worth per pixel: the first that holds it.
201
188
  //
202
- // The far cascade's taps run ONLY where the near one is fading or absent
203
- // (frustum < 1), so a pixel in the near core costs exactly what it did with
204
- // one cascade — and that core is where the camera usually looks.
205
189
  // Can anything cast a shadow ONTO this point at all?
206
190
  //
207
191
  // The ground is vastly larger than what stands on it, and the answer for most
@@ -255,33 +239,26 @@ ${soft
255
239
  let ign = fract(52.9829189 * fract(dot(i.position.xy, vec2f(0.06711056, 0.00583715))));
256
240
  let rot = ign * 6.28318530718;`
257
241
  : ""}
258
- // The far cascade's taps, skipped entirely when nothing ever drew into it.
259
- //
260
- // This branch is the expensive one on a wide floor: it runs wherever the NEAR
261
- // cascade does not reach, and the near cascade is a 64-unit box around the
262
- // camera target, so a floor receding to the horizon takes it almost
263
- // everywhere. Nine comparison taps, per pixel, on the largest draw in the
264
- // frame — against a map that is cleared unless a stage is in the scene.
265
- //
266
- // Cleared depth compares as "no occluder", so the skipped path leaves vis at
267
- // 1.0, which is exactly what the taps returned. Free, not cheaper.
268
- if (material.farCascade > 0.0 && frustum < 1.0 && frustum1 > 0.0) {
269
- let suv1 = vec2f(ndc1.x * 0.5 + 0.5, 0.5 - ndc1.y * 0.5);
270
- let suv1_c = clamp(suv1, vec2f(0.02), vec2f(0.98));
271
- let compareZ1 = ndc1.z - 0.0035;
272
- var acc1 = 0.0;
273
- ${pcfWgsl("shadowMapFar", "suv1_c", `${1 / SHADOW_CASCADES[SHADOW_CASCADES.length - 1].mapSize}`, "compareZ1", "acc1", " ", soft)}
274
- vis = mix(1.0, acc1, frustum1);
275
- }
276
- if (frustum > 0.0) {
277
- let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
278
- let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));
242
+ // The first cascade whose box holds the point, as the materials pick it.
243
+ // Outside a cascade's box there IS no shadow information — the clamped
244
+ // border samples compare against unrelated depths and read "shadowed",
245
+ // darkening the far floor with a band at the edge — so the outermost one
246
+ // fades into lit near its border. Inner cascades hand over to the next.
247
+ let n = ${SHADOW_CASCADES.length}u;
248
+ for (var ci = 0u; ci < n; ci++) {
249
+ let lc = lightVP.viewProj[ci] * vec4f(i.worldPos, 1.0);
250
+ let ndc = lc.xyz / max(lc.w, 1e-6);
251
+ if (!(all(abs(ndc.xy) < vec2f(${SHADOW_MARGIN})) && ndc.z > 0.0 && ndc.z < 1.0)) { continue; }
252
+ let frustum = select(1.0, (1.0 - smoothstep(0.88, 0.96, abs(ndc.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc.y))), ci == n - 1u);
253
+ let tile = vec2f(f32(ci % 2u), f32(ci / 2u)) * ${SHADOW_CASCADES[0].mapSize / SHADOW_ATLAS_SIZE};
254
+ let tileLo = tile + vec2f(${0.5 / SHADOW_ATLAS_SIZE});
255
+ let tileHi = tile + vec2f(${(SHADOW_CASCADES[0].mapSize - 0.5) / SHADOW_ATLAS_SIZE});
256
+ let suv = tile + vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5) * ${SHADOW_CASCADES[0].mapSize / SHADOW_ATLAS_SIZE};
279
257
  let compareZ = ndc.z - 0.0035;
280
258
  var acc = 0.0;
281
- ${pcfWgsl("shadowMap", "suv_c", "material.pcfTexel", "compareZ", "acc", " ", soft)}
282
- // The base is whatever the far cascade decided, so the near border blends
283
- // cascade to cascade rather than snapping to lit mid-floor.
284
- vis = mix(vis, acc, frustum);
259
+ ${pcfWgsl("shadowAtlas", "suv", `${1 / SHADOW_ATLAS_SIZE}`, "compareZ", "acc", " ", soft)}
260
+ vis = mix(1.0, acc, frustum);
261
+ break;
285
262
  }
286
263
  }
287
264
 
@@ -36,6 +36,15 @@ type ParticleSource = {
36
36
  * where every stacked fragment is otherwise shaded in full; the prepass is
37
37
  * the shape of that rejection, done by hand and cheaply. */
38
38
  cover: boolean;
39
+ /** The effect defines `particleOrient(p: Particle, id: u32) -> mat3x3f`: the
40
+ * quad lies along its first two columns (right, up) instead of facing the
41
+ * camera. A card that belongs to a surface — a raindrop's glow on a window,
42
+ * a sheet of water standing in its fall — is flat on that surface and turns
43
+ * with it, not with the eye: Unity's Local and World render alignments. The
44
+ * particle's rotation still turns the corners within that plane, and a
45
+ * stretched particle stretches along its velocity projected into it. `id`
46
+ * is the particle's slot, so the axes can come from its emitter. */
47
+ orient?: boolean;
39
48
  /** The effect defines `particleCount() -> u32`: how many of the pool are
40
49
  * live this frame, as a function of its dials. The step is dispatched and
41
50
  * the quads drawn for that many slots and no more, through indirect
@@ -54,7 +63,7 @@ type ParticleSource = {
54
63
  * N > 0 — see texture-api.ts. Omitted = 0. */
55
64
  textures?: number;
56
65
  };
57
- /** Where the shading stage's five scene-light bindings start — see scene-light-api.ts. */
66
+ /** Where the shading stage's four scene-light bindings start — see scene-light-api.ts. */
58
67
  export declare const PARTICLE_LIGHT_BINDING = 10;
59
68
  /** The effect's named points (`#points`), in both stages — see points-api.ts. */
60
69
  export declare const PARTICLE_POINTS_BINDING = 15;
@@ -79,6 +88,7 @@ export declare function particleEntryPoints(wgsl: string): {
79
88
  shade: boolean;
80
89
  cover: boolean;
81
90
  count: boolean;
91
+ orient: boolean;
82
92
  };
83
93
  /**
84
94
  * Spawn, age, recycle.
@@ -1 +1 @@
1
- {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AAgCA,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;;;;;;;kFAO8E;IAC9E,IAAI,EAAE,OAAO,CAAA;IACb;;;wEAGoE;IACpE,QAAQ,EAAE,MAAM,CAAA;IAChB;mDAC+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,0FAA0F;AAC1F,eAAO,MAAM,sBAAsB,KAAK,CAAA;AACxC,iFAAiF;AACjF,eAAO,MAAM,uBAAuB,KAAK,CAAA;AACzC;;sDAEsD;AACtD,eAAO,MAAM,yBAAyB,KAAK,CAAA;AAC3C,eAAO,MAAM,uBAAuB,KAAK,CAAA;AACzC;uCACuC;AACvC,eAAO,MAAM,wBAAwB,KAAK,CAAA;AAC1C,eAAO,MAAM,gCAAgC,KAAK,CAAA;AAClD,eAAO,MAAM,6BAA6B,KAAK,CAAA;AAE/C,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAwDjC;;2EAE2E;AAC3E,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,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAQlI;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6ExF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6LvF"}
1
+ {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AAgCA,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;;;;;;;yEAOqE;IACrE,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;;;;kFAO8E;IAC9E,IAAI,EAAE,OAAO,CAAA;IACb;;;wEAGoE;IACpE,QAAQ,EAAE,MAAM,CAAA;IAChB;mDAC+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,0FAA0F;AAC1F,eAAO,MAAM,sBAAsB,KAAK,CAAA;AACxC,iFAAiF;AACjF,eAAO,MAAM,uBAAuB,KAAK,CAAA;AACzC;;sDAEsD;AACtD,eAAO,MAAM,yBAAyB,KAAK,CAAA;AAC3C,eAAO,MAAM,uBAAuB,KAAK,CAAA;AACzC;uCACuC;AACvC,eAAO,MAAM,wBAAwB,KAAK,CAAA;AAC1C,eAAO,MAAM,gCAAgC,KAAK,CAAA;AAClD,eAAO,MAAM,6BAA6B,KAAK,CAAA;AAE/C,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAwDjC;;2EAE2E;AAC3E,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,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CASnJ;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6ExF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAiMvF"}
@@ -43,7 +43,7 @@ fn _rzCastLive() -> i32 {
43
43
  trailSlotsApi(cast.trailCount) +
44
44
  anchorAliasWgsl(cast.alias));
45
45
  }
46
- /** Where the shading stage's five scene-light bindings start — see scene-light-api.ts. */
46
+ /** Where the shading stage's four scene-light bindings start — see scene-light-api.ts. */
47
47
  export const PARTICLE_LIGHT_BINDING = 10;
48
48
  /** The effect's named points (`#points`), in both stages — see points-api.ts. */
49
49
  export const PARTICLE_POINTS_BINDING = 15;
@@ -114,6 +114,7 @@ fn rzCamPos() -> vec3f { return cam.camPos; }
114
114
  * `count` is optional and bounds the frame's work — see particleCount. */
115
115
  export function particleEntryPoints(wgsl) {
116
116
  return {
117
+ orient: /\bfn\s+particleOrient\s*\(/.test(wgsl),
117
118
  init: /\bfn\s+particleInit\s*\(/.test(wgsl),
118
119
  step: /\bfn\s+particleStep\s*\(/.test(wgsl),
119
120
  shade: /\bfn\s+particleShade\s*\(/.test(wgsl),
@@ -283,9 +284,13 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
283
284
  out.clip = vec4f(0.0, 0.0, -2.0, 1.0);
284
285
  return out;
285
286
  }
286
- let right = vec3f(cam.view[0][0], cam.view[1][0], cam.view[2][0]);
287
- let up = vec3f(cam.view[0][1], cam.view[1][1], cam.view[2][1]);
288
- let s = sin(p.rot);
287
+ var right = vec3f(cam.view[0][0], cam.view[1][0], cam.view[2][0]);
288
+ var up = vec3f(cam.view[0][1], cam.view[1][1], cam.view[2][1]);
289
+ ${src.orient ? ` // the author's own plane (particleOrient): a card on a surface, not facing the eye
290
+ let o = particleOrient(p, ii);
291
+ right = o[0];
292
+ up = o[1];
293
+ ` : ""} let s = sin(p.rot);
289
294
  let k = cos(p.rot);
290
295
  var r = vec2f(c.x * k - c.y * s, c.x * s + c.y * k);
291
296
  // Stretched along the direction of travel ON SCREEN — which is not the world
@@ -8,16 +8,25 @@
8
8
  */
9
9
  export declare const worldAmbientWgsl: (u: string) => string;
10
10
  /**
11
- * 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
11
+ * The sun's shadow atlas, as every reader looks it up: `<p>Locate(q)` finds
12
+ * world point q in the first cascade whose box holds it and answers
13
+ * (atlas uv, depth, cascade) — cascade -1 outside them all — and `<p>Taps(a)`
14
+ * is the 3×3 PCF at that place.
12
15
  *
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.
16
+ * ONE implementation for the materials, the ground and effects, so a blade of
17
+ * grass and her own skin go dark at the same edge. `vp` names the cascades'
18
+ * view-projection array in the module that includes it.
19
+ *
20
+ * The box test keeps a margin (SHADOW_MARGIN of the tile's half-width) so the
21
+ * whole filter stays inside the tile: a tap that crossed into the neighbouring
22
+ * tile would compare against another cascade's depths. The sampler is linear,
23
+ * so each tap is itself a 2×2 compare. Unrolled — Safari's Metal backend
24
+ * doesn't unroll nested shadow loops reliably.
17
25
  */
18
- export declare const pcf9: (map: string, ts: string, sampler?: string) => string;
26
+ export declare const SHADOW_MARGIN = 0.98;
27
+ export declare const sunShadowWgsl: (p: string, map: string, sampler: string, vp: string) => string;
19
28
  /** Bindings the real accessors take, from `binding` upward. */
20
- export declare const SCENE_LIGHT_API_BINDINGS = 5;
29
+ export declare const SCENE_LIGHT_API_BINDINGS = 4;
21
30
  /**
22
31
  * `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
23
32
  * caster's shadow, soft in between.
@@ -25,12 +34,11 @@ export declare const SCENE_LIGHT_API_BINDINGS = 5;
25
34
  * `rzWorldAmbient(n)`: the world's light arriving at a surface facing `n` —
26
35
  * the materials' own ambient, so an effect's shade can match hers.
27
36
  *
28
- * With `on`, declares five bindings from `binding`:
37
+ * With `on`, declares four bindings from `binding`:
29
38
  * +0 the light uniform (ambient, the sun — direction, and in .w the shadow
30
39
  * switch — and the HDRI's irradiance)
31
40
  * +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
41
+ * +2 the sun's shadow atlas +3 the comparison sampler
34
42
  *
35
43
  * It takes a POINT and no normal. The materials' sampleShadow also rejects a
36
44
  * surface facing away from the sun, which is right for skin and wrong for a
@@ -1 +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,WAwBzC,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"}
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,WAwBzC,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,OAAO,CAAA;AAEjC,eAAO,MAAM,aAAa,GAAI,GAAG,MAAM,EAAE,KAAK,MAAM,EAAE,SAAS,MAAM,EAAE,IAAI,MAAM,WAkChF,CAAA;AAED,+DAA+D;AAC/D,eAAO,MAAM,wBAAwB,IAAI,CAAA;AAEzC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA2CjF"}
@@ -1,4 +1,4 @@
1
- import { SHADOW_CASCADES } from "../shadow-cascades";
1
+ import { SHADOW_ATLAS_SIZE, SHADOW_CASCADES } from "../shadow-cascades";
2
2
  // The scene's own light, as an effect author sees it: the sun's shadow and the
3
3
  // world's ambient.
4
4
  //
@@ -49,30 +49,59 @@ fn rzWorldAmbientAvg() -> vec3f {
49
49
  }
50
50
  `;
51
51
  /**
52
- * 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
52
+ * The sun's shadow atlas, as every reader looks it up: `<p>Locate(q)` finds
53
+ * world point q in the first cascade whose box holds it and answers
54
+ * (atlas uv, depth, cascade) — cascade -1 outside them all — and `<p>Taps(a)`
55
+ * is the 3×3 PCF at that place.
53
56
  *
54
- * Shared with the materials, so an effect's shadow and her own shadow soften
55
- * alike. The sampler is linear-filtered, which makes each tap a 2×2 bilinear
56
- * compare. Unrolled — Safari's Metal backend doesn't unroll nested shadow
57
- * loops reliably. The body expects `ndc` in scope and returns.
57
+ * ONE implementation for the materials, the ground and effects, so a blade of
58
+ * grass and her own skin go dark at the same edge. `vp` names the cascades'
59
+ * view-projection array in the module that includes it.
60
+ *
61
+ * The box test keeps a margin (SHADOW_MARGIN of the tile's half-width) so the
62
+ * whole filter stays inside the tile: a tap that crossed into the neighbouring
63
+ * tile would compare against another cascade's depths. The sampler is linear,
64
+ * so each tap is itself a 2×2 compare. Unrolled — Safari's Metal backend
65
+ * doesn't unroll nested shadow loops reliably.
58
66
  */
59
- export const pcf9 = (map, ts, sampler = "shadowSampler") => /* wgsl */ `
60
- let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
61
- let cmpZ = ndc.z - 0.001;
62
- let ts = ${ts};
63
- let s00 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, -ts), cmpZ);
64
- let s10 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(0.0, -ts), cmpZ);
65
- let s20 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, -ts), cmpZ);
66
- let s01 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, 0.0), cmpZ);
67
- let s11 = textureSampleCompareLevel(${map}, ${sampler}, suv, cmpZ);
68
- let s21 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, 0.0), cmpZ);
69
- let s02 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, ts), cmpZ);
70
- let s12 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(0.0, ts), cmpZ);
71
- let s22 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, ts), cmpZ);
67
+ export const SHADOW_MARGIN = 0.98;
68
+ export const sunShadowWgsl = (p, map, sampler, vp) => {
69
+ const n = SHADOW_CASCADES.length;
70
+ const tiles = SHADOW_CASCADES.map((c) => `vec2f(${c.origin[0] / SHADOW_ATLAS_SIZE}, ${c.origin[1] / SHADOW_ATLAS_SIZE})`).join(", ");
71
+ const scale = SHADOW_CASCADES[0].mapSize / SHADOW_ATLAS_SIZE;
72
+ return /* wgsl */ `
73
+ const ${p}Tiles = array<vec2f, ${n}>(${tiles});
74
+
75
+ fn ${p}Locate(q: vec3f) -> vec4f {
76
+ for (var i = 0u; i < ${n}u; i++) {
77
+ let c = ${vp}[i] * vec4f(q, 1.0);
78
+ let ndc = c.xyz / max(c.w, 1e-6);
79
+ if (all(abs(ndc.xy) < vec2f(${SHADOW_MARGIN})) && ndc.z > 0.0 && ndc.z < 1.0) {
80
+ let uv = ${p}Tiles[i] + vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5) * ${scale};
81
+ return vec4f(uv, ndc.z, f32(i));
82
+ }
83
+ }
84
+ return vec4f(0.0, 0.0, 0.0, -1.0);
85
+ }
86
+
87
+ fn ${p}Taps(a: vec4f) -> f32 {
88
+ let cmpZ = a.z - 0.001;
89
+ let ts = ${1 / SHADOW_ATLAS_SIZE};
90
+ let s00 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(-ts, -ts), cmpZ);
91
+ let s10 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(0.0, -ts), cmpZ);
92
+ let s20 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f( ts, -ts), cmpZ);
93
+ let s01 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(-ts, 0.0), cmpZ);
94
+ let s11 = textureSampleCompareLevel(${map}, ${sampler}, a.xy, cmpZ);
95
+ let s21 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f( ts, 0.0), cmpZ);
96
+ let s02 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(-ts, ts), cmpZ);
97
+ let s12 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(0.0, ts), cmpZ);
98
+ let s22 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f( ts, ts), cmpZ);
72
99
  return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
100
+ }
73
101
  `;
102
+ };
74
103
  /** Bindings the real accessors take, from `binding` upward. */
75
- export const SCENE_LIGHT_API_BINDINGS = 5;
104
+ export const SCENE_LIGHT_API_BINDINGS = 4;
76
105
  /**
77
106
  * `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
78
107
  * caster's shadow, soft in between.
@@ -80,12 +109,11 @@ export const SCENE_LIGHT_API_BINDINGS = 5;
80
109
  * `rzWorldAmbient(n)`: the world's light arriving at a surface facing `n` —
81
110
  * the materials' own ambient, so an effect's shade can match hers.
82
111
  *
83
- * With `on`, declares five bindings from `binding`:
112
+ * With `on`, declares four bindings from `binding`:
84
113
  * +0 the light uniform (ambient, the sun — direction, and in .w the shadow
85
114
  * switch — and the HDRI's irradiance)
86
115
  * +1 the cascade view-projections, then the caster sphere
87
- * +2 the near cascade's map +3 the far cascade's map
88
- * +4 the comparison sampler
116
+ * +2 the sun's shadow atlas +3 the comparison sampler
89
117
  *
90
118
  * It takes a POINT and no normal. The materials' sampleShadow also rejects a
91
119
  * surface facing away from the sun, which is right for skin and wrong for a
@@ -110,17 +138,9 @@ struct _RzSceneLightU { ambientColor: vec4f, lights: array<_RzSceneLight, 4>, sh
110
138
  struct _RzShadowBlock { viewProj: array<mat4x4f, ${n}>, casters: vec4f, }
111
139
  @group(${group}) @binding(${binding}) var<uniform> _rzLight: _RzSceneLightU;
112
140
  @group(${group}) @binding(${binding + 1}) var<uniform> _rzShadowVP: _RzShadowBlock;
113
- @group(${group}) @binding(${binding + 2}) var _rzShadowNear: texture_depth_2d;
114
- @group(${group}) @binding(${binding + 3}) var _rzShadowFar: texture_depth_2d;
115
- @group(${group}) @binding(${binding + 4}) var _rzShadowCmp: sampler_comparison;
116
-
117
- fn _rzShadowNearTaps(ndc: vec3f) -> f32 {
118
- ${pcf9("_rzShadowNear", `1.0 / ${SHADOW_CASCADES[0].mapSize}.0`, "_rzShadowCmp")}
119
- }
120
-
121
- fn _rzShadowFarTaps(ndc: vec3f) -> f32 {
122
- ${pcf9("_rzShadowFar", `1.0 / ${SHADOW_CASCADES[n - 1].mapSize}.0`, "_rzShadowCmp")}
123
- }
141
+ @group(${group}) @binding(${binding + 2}) var _rzShadowAtlas: texture_depth_2d;
142
+ @group(${group}) @binding(${binding + 3}) var _rzShadowCmp: sampler_comparison;
143
+ ${sunShadowWgsl("_rzSun", "_rzShadowAtlas", "_rzShadowCmp", "_rzShadowVP.viewProj")}
124
144
 
125
145
  fn rzShadow(p: vec3f) -> f32 {
126
146
  // The scene's one shadow switch, on the sun — see sampleShadow.
@@ -138,18 +158,10 @@ fn rzShadow(p: vec3f) -> f32 {
138
158
  let perp = length(toCaster - toSun * along);
139
159
  if (along <= -cs.w || perp > cs.w) { return 1.0; }
140
160
  }
141
- let q = p + toSun * 0.08;
142
- let c0 = _rzShadowVP.viewProj[0] * vec4f(q, 1.0);
143
- let n0 = c0.xyz / max(c0.w, 1e-6);
144
- if (all(abs(n0.xy) < vec2f(0.98)) && n0.z > 0.0 && n0.z < 1.0) {
145
- return mix(1.0, _rzShadowNearTaps(n0), castAmt);
146
- }
147
- let c1 = _rzShadowVP.viewProj[${n - 1}] * vec4f(q, 1.0);
148
- let n1 = c1.xyz / max(c1.w, 1e-6);
149
- if (all(abs(n1.xy) < vec2f(0.98)) && n1.z > 0.0 && n1.z < 1.0) {
150
- return mix(1.0, _rzShadowFarTaps(n1), castAmt);
151
- }
152
- return 1.0;
161
+ let a = _rzSunLocate(p + toSun * 0.08);
162
+ // Outside every cascade there is no occlusion information: lit.
163
+ if (a.w < 0.0) { return 1.0; }
164
+ return mix(1.0, _rzSunTaps(a), castAmt);
153
165
  }
154
166
  ${worldAmbientWgsl("_rzLight")}`;
155
167
  }
@@ -1,34 +1,47 @@
1
+ import { Vec3 } from "./math";
1
2
  /**
2
3
  * THE SUN'S SHADOW FOLLOWS THE CAMERA, the way Blender's does.
3
4
  *
4
- * Each cascade is fitted every frame to a slice of the view frustum: the near
5
- * one to the stretch around what the camera is looking at, the far one to the
6
- * whole of what it sees. Whatever is in view is in a cascade, and whatever
7
- * could throw a shadow onto it is in the cascade's depth range, which is fitted
8
- * to the scene's bounds along the light. A room forty metres across with its
9
- * window frames behind the camera shadows its floor as the game does; a lone
10
- * dancer on an empty floor keeps a crisp near map.
5
+ * Each cascade is fitted every frame to a slice of the view frustum: the
6
+ * innermost to the stretch around what the camera is looking at, the
7
+ * outermost to the whole of what it sees, two more between. Whatever is in
8
+ * view is in a cascade, and whatever could throw a shadow onto it is in the
9
+ * cascade's depth range, which is fitted to the scene's bounds along the
10
+ * light. A room forty metres across with its window frames behind the camera
11
+ * shadows its floor as the game does; a lone dancer on an empty floor keeps a
12
+ * crisp inner tile.
11
13
  *
12
- * The earlier shape was two fixed boxes around the camera target, 64 and 256
13
- * units across, and a stage reached past them: the floor near the windows lay
14
- * outside every box and was drawn lit, the shadows stopping at the box's edge
15
- * in a straight line.
14
+ * The earlier shapes were two fixed boxes around the camera target, then two
15
+ * fitted maps; the game's pipeline draws four cascades into one atlas, and so
16
+ * does this.
16
17
  *
17
- * INVARIANT the sampler and the cull both lean on: the outer cascade CONTAINS
18
- * the inner one. The sampler falls from cascade 0 to 1 at the box edge, which
19
- * is only seamless if 1 covers where 0 ends, and the cull tests the OUTERMOST
20
- * frustum alone. Both hold because the outer slice is the whole frustum, of
21
- * which the inner slice is a part, and both take the same depth range.
18
+ * INVARIANT the sampler and the cull both lean on: each cascade CONTAINS the
19
+ * ones inside it. The sampler takes the first box that holds a point, which
20
+ * is only seamless if the next covers where one ends, and the cull tests the
21
+ * OUTERMOST frustum alone. Both hold because every slice starts at the near
22
+ * plane and the outermost is the whole frustum, and all take the same depth
23
+ * range.
22
24
  * tests/shadow-cascades.test.mjs pins it.
23
25
  */
24
26
  export type ShadowCascade = {
25
- /** Texels per side of this cascade's map — sets the snap quantum. */
27
+ /** Texels per side of this cascade's tile — sets the snap quantum. */
26
28
  mapSize: number;
29
+ /** Where the tile sits in the atlas, in texels from its top-left corner. */
30
+ origin: readonly [number, number];
27
31
  };
32
+ /**
33
+ * ONE ATLAS, FOUR TILES, as the game's pipeline (and URP) lays its sun shadow
34
+ * out: one depth texture, cascade i in tile (i mod 2, i div 2). One texture
35
+ * means one binding for every reader — materials, ground, effects, particles —
36
+ * and room in the same scheme for the local lights' shadows later.
37
+ */
38
+ export declare const SHADOW_ATLAS_SIZE = 4096;
28
39
  export declare const SHADOW_CASCADES: readonly ShadowCascade[];
29
- /** How far past the camera's point of interest the near cascade reaches, in
30
- * world units: the dancer and the floor around her, at the near map's texel. */
31
- export declare const NEAR_REACH = 40;
40
+ /** How far past the camera's point of interest the inner cascade reaches, in
41
+ * world units: the dancer and the floor around her. Half the old two-map
42
+ * reach, which keeps her texel where it was at half the tile; the three
43
+ * outer cascades carry the rest of the room. */
44
+ export declare const NEAR_REACH = 20;
32
45
  /** The view the cascades are fitted to: the camera's eye and basis (world
33
46
  * space, left-handed, +Z forward as the projection is), its vertical field of
34
47
  * view, aspect and clip planes, and how far away the thing it looks at is. */
@@ -70,6 +83,14 @@ type XYZ = {
70
83
  y: number;
71
84
  z: number;
72
85
  };
86
+ /**
87
+ * Where each cascade's slice of the view starts and ends, as distances along
88
+ * the view, inner to outer: every slice starts at the near plane and they
89
+ * nest, the innermost ending just past the point of interest and the
90
+ * outermost where the scene ends, the two between spaced evenly in ratio.
91
+ * Nested rather than abutting so the invariant above holds: the outermost
92
+ * box contains all of them, and the sampler takes the first that holds a point.
93
+ */
73
94
  export declare function cascadeSlices(view: ShadowView, bounds: ShadowBounds): [number, number][];
74
95
  /** The eight corners of a slice, for tests and for the fit's own checks. */
75
96
  export declare function sliceCorners(view: ShadowView, n: number, f: number): XYZ[];
@@ -83,6 +104,12 @@ export declare function sliceCorners(view: ShadowView, n: number, f: number): XY
83
104
  * Writes the 16 floats into `out` at `offset` and returns `out`.
84
105
  */
85
106
  export declare function fitShadowVP(view: ShadowView, slice: [number, number], sunDirection: XYZ, bounds: ShadowBounds, cascade: ShadowCascade, out: Float32Array, offset: number): Float32Array;
107
+ /** Every cascade's bounding sphere (centre, radius), inner to outer — the
108
+ * split spheres a game shader picks its cascade by (_CascadeShadowSplitSpheres). */
109
+ export declare function cascadeSpheres(view: ShadowView, bounds: ShadowBounds): {
110
+ center: Vec3;
111
+ radius: number;
112
+ }[];
86
113
  /** Every cascade's view-projection in a row, inner to outer. */
87
114
  export declare function buildShadowCascades(view: ShadowView, sunDirection: XYZ, bounds: ShadowBounds, out: Float32Array): Float32Array;
88
115
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"shadow-cascades.d.ts","sourceRoot":"","sources":["../src/shadow-cascades.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,SAAS,aAAa,EAA2C,CAAA;AAE/F;iFACiF;AACjF,eAAO,MAAM,UAAU,KAAK,CAAA;AAE5B;;+EAE+E;AAC/E,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACxC,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1C,EAAE,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACvC,OAAO,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5C,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,2EAA2E;AAC3E,MAAM,MAAM,YAAY,GAAG;IAAE,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAAA;AAElG,KAAK,GAAG,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAY9C,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAqBxF;AAwBD,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,GAAG,EAAE,CAc1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EACvB,YAAY,EAAE,GAAG,EACjB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,YAAY,EACjB,MAAM,EAAE,MAAM,GACb,YAAY,CA0Cd;AAED,gEAAgE;AAChE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,GAAG,YAAY,CAI9H"}
1
+ {"version":3,"file":"shadow-cascades.d.ts","sourceRoot":"","sources":["../src/shadow-cascades.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEnC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAA;IACf,4EAA4E;IAC5E,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAA;AAIrC,eAAO,MAAM,eAAe,EAAE,SAAS,aAAa,EAGjD,CAAA;AAEH;;;iDAGiD;AACjD,eAAO,MAAM,UAAU,KAAK,CAAA;AAE5B;;+EAE+E;AAC/E,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACxC,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1C,EAAE,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACvC,OAAO,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5C,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,2EAA2E;AAC3E,MAAM,MAAM,YAAY,GAAG;IAAE,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAAA;AAElG,KAAK,GAAG,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAM9C;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAwBxF;AAwBD,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,GAAG,EAAE,CAc1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EACvB,YAAY,EAAE,GAAG,EACjB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,YAAY,EACjB,MAAM,EAAE,MAAM,GACb,YAAY,CA0Cd;AAED;qFACqF;AACrF,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,GAAG;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,CAEzG;AAED,gEAAgE;AAChE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,GAAG,YAAY,CAI9H"}