reze-engine 0.43.0 → 0.50.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 (135) hide show
  1. package/README.md +40 -438
  2. package/dist/camera.d.ts +3 -0
  3. package/dist/camera.d.ts.map +1 -1
  4. package/dist/camera.js +33 -8
  5. package/dist/engine.d.ts +841 -53
  6. package/dist/engine.d.ts.map +1 -1
  7. package/dist/engine.js +3498 -451
  8. package/dist/graph/registry.d.ts +2 -2
  9. package/dist/graph/registry.d.ts.map +1 -1
  10. package/dist/graph/registry.js +1 -1
  11. package/dist/graph/slots.d.ts +0 -1
  12. package/dist/graph/slots.d.ts.map +1 -1
  13. package/dist/graph/slots.js +37 -9
  14. package/dist/hdr.d.ts +18 -0
  15. package/dist/hdr.d.ts.map +1 -0
  16. package/dist/hdr.js +162 -0
  17. package/dist/ibl.d.ts +19 -0
  18. package/dist/ibl.d.ts.map +1 -0
  19. package/dist/ibl.js +113 -0
  20. package/dist/ik-solver.d.ts +2 -1
  21. package/dist/ik-solver.d.ts.map +1 -1
  22. package/dist/index.d.ts +5 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +10 -0
  25. package/dist/math.d.ts +20 -1
  26. package/dist/math.d.ts.map +1 -1
  27. package/dist/math.js +23 -16
  28. package/dist/midi-loader.d.ts +10 -0
  29. package/dist/midi-loader.d.ts.map +1 -0
  30. package/dist/midi-loader.js +247 -0
  31. package/dist/model.d.ts +2 -13
  32. package/dist/model.d.ts.map +1 -1
  33. package/dist/param-track.d.ts +48 -0
  34. package/dist/param-track.d.ts.map +1 -0
  35. package/dist/param-track.js +80 -0
  36. package/dist/physics/types.d.ts.map +1 -1
  37. package/dist/physics/types.js +3 -0
  38. package/dist/reflection.d.ts +27 -0
  39. package/dist/reflection.d.ts.map +1 -0
  40. package/dist/reflection.js +93 -0
  41. package/dist/shaders/anchor-table.d.ts +56 -0
  42. package/dist/shaders/anchor-table.d.ts.map +1 -0
  43. package/dist/shaders/anchor-table.js +128 -0
  44. package/dist/shaders/cast-api.d.ts +2 -0
  45. package/dist/shaders/cast-api.d.ts.map +1 -0
  46. package/dist/shaders/cast-api.js +121 -0
  47. package/dist/shaders/cast-layout.d.ts +21 -0
  48. package/dist/shaders/cast-layout.d.ts.map +1 -0
  49. package/dist/shaders/cast-layout.js +20 -0
  50. package/dist/shaders/lights.d.ts +79 -0
  51. package/dist/shaders/lights.d.ts.map +1 -0
  52. package/dist/shaders/lights.js +269 -0
  53. package/dist/shaders/lyrics-api.d.ts +39 -0
  54. package/dist/shaders/lyrics-api.d.ts.map +1 -0
  55. package/dist/shaders/lyrics-api.js +187 -0
  56. package/dist/shaders/materials/common.d.ts +2 -4
  57. package/dist/shaders/materials/common.d.ts.map +1 -1
  58. package/dist/shaders/materials/common.js +87 -35
  59. package/dist/shaders/midi-api.d.ts +10 -0
  60. package/dist/shaders/midi-api.d.ts.map +1 -0
  61. package/dist/shaders/midi-api.js +114 -0
  62. package/dist/shaders/passes/composite.d.ts +28 -22
  63. package/dist/shaders/passes/composite.d.ts.map +1 -1
  64. package/dist/shaders/passes/composite.js +165 -138
  65. package/dist/shaders/passes/cull.d.ts +2 -0
  66. package/dist/shaders/passes/cull.d.ts.map +1 -0
  67. package/dist/shaders/passes/cull.js +138 -0
  68. package/dist/shaders/passes/field-blit.d.ts +26 -0
  69. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  70. package/dist/shaders/passes/field-blit.js +65 -0
  71. package/dist/shaders/passes/grid.d.ts +31 -0
  72. package/dist/shaders/passes/grid.d.ts.map +1 -0
  73. package/dist/shaders/passes/grid.js +169 -0
  74. package/dist/shaders/passes/ground.d.ts +13 -1
  75. package/dist/shaders/passes/ground.d.ts.map +1 -1
  76. package/dist/shaders/passes/ground.js +170 -25
  77. package/dist/shaders/passes/hosted-api.d.ts +57 -0
  78. package/dist/shaders/passes/hosted-api.d.ts.map +1 -0
  79. package/dist/shaders/passes/hosted-api.js +166 -0
  80. package/dist/shaders/passes/id-debug.d.ts +28 -0
  81. package/dist/shaders/passes/id-debug.d.ts.map +1 -0
  82. package/dist/shaders/passes/id-debug.js +74 -0
  83. package/dist/shaders/passes/particles.d.ts +10 -2
  84. package/dist/shaders/passes/particles.d.ts.map +1 -1
  85. package/dist/shaders/passes/particles.js +37 -109
  86. package/dist/shaders/passes/scene-contract.d.ts +128 -0
  87. package/dist/shaders/passes/scene-contract.d.ts.map +1 -0
  88. package/dist/shaders/passes/scene-contract.js +207 -0
  89. package/dist/shaders/passes/sim.d.ts +34 -0
  90. package/dist/shaders/passes/sim.d.ts.map +1 -0
  91. package/dist/shaders/passes/sim.js +169 -0
  92. package/dist/shaders/passes/trails.d.ts +14 -2
  93. package/dist/shaders/passes/trails.d.ts.map +1 -1
  94. package/dist/shaders/passes/trails.js +55 -90
  95. package/dist/shaders/score-api.d.ts +10 -0
  96. package/dist/shaders/score-api.d.ts.map +1 -0
  97. package/dist/shaders/score-api.js +114 -0
  98. package/dist/shadow-cascades.d.ts +45 -0
  99. package/dist/shadow-cascades.d.ts.map +1 -0
  100. package/dist/shadow-cascades.js +70 -0
  101. package/dist/vmd-loader.d.ts +3 -2
  102. package/dist/vmd-loader.d.ts.map +1 -1
  103. package/package.json +1 -1
  104. package/src/camera.ts +31 -8
  105. package/src/engine.ts +4015 -556
  106. package/src/graph/registry.ts +2 -2
  107. package/src/graph/slots.ts +37 -9
  108. package/src/hdr.ts +156 -0
  109. package/src/ibl.ts +115 -0
  110. package/src/ik-solver.ts +1 -1
  111. package/src/index.ts +12 -0
  112. package/src/math.ts +23 -17
  113. package/src/midi-loader.ts +246 -0
  114. package/src/model.ts +3 -3
  115. package/src/param-track.ts +83 -0
  116. package/src/physics/types.ts +4 -1
  117. package/src/reflection.ts +94 -0
  118. package/src/shaders/anchor-table.ts +147 -0
  119. package/src/shaders/cast-api.ts +123 -0
  120. package/src/shaders/cast-layout.ts +20 -0
  121. package/src/shaders/lights.ts +280 -0
  122. package/src/shaders/lyrics-api.ts +202 -0
  123. package/src/shaders/materials/common.ts +89 -35
  124. package/src/shaders/midi-api.ts +116 -0
  125. package/src/shaders/passes/composite.ts +182 -139
  126. package/src/shaders/passes/cull.ts +139 -0
  127. package/src/shaders/passes/grid.ts +178 -0
  128. package/src/shaders/passes/ground.ts +172 -25
  129. package/src/shaders/passes/hosted-api.ts +171 -0
  130. package/src/shaders/passes/id-debug.ts +75 -0
  131. package/src/shaders/passes/particles.ts +54 -112
  132. package/src/shaders/passes/scene-contract.ts +266 -0
  133. package/src/shaders/passes/trails.ts +77 -93
  134. package/src/shadow-cascades.ts +97 -0
  135. package/src/vmd-loader.ts +2 -2
@@ -0,0 +1,65 @@
1
+ import { sceneFsOutWgsl } from "./scene-contract";
2
+ /**
3
+ * The field layer, drawn INTO the scene.
4
+ *
5
+ * Field effects render into their own targets at their own resolution — that
6
+ * part was always right. What was wrong was where the result landed: the
7
+ * composite sampled it AFTER tone mapping, which is why a field effect could
8
+ * never bloom, could never light anything, and tone mapped differently from the
9
+ * particle and ribbon mounts drawing the same kind of glow.
10
+ *
11
+ * Two draws inside the scene pass — background before any geometry, foreground
12
+ * after all of it — so the bloom pyramid sees field effects and one tone map
13
+ * covers the whole frame.
14
+ *
15
+ * NO DEPTH, neither written nor tested. A foreground is handed the scene's
16
+ * depth as a parameter and does its own occlusion; testing it here would
17
+ * occlude it a second time, and a background has nothing to test against.
18
+ *
19
+ * COVERAGE IS CLAMPED HERE. Additive field effects SUM their coverage into the
20
+ * layer (see FIELD_LAYER_BLEND_ADDITIVE), which is what stops the composite's
21
+ * unpremultiply from dividing colour by nothing — but N additive effects can
22
+ * sum past 1, and the layer is rgba16float so nothing stopped it on the way in.
23
+ * A coverage above 1 would darken what the layer is drawn over by more than
24
+ * fully covering it.
25
+ */
26
+ export function fieldBlitShaderWgsl() {
27
+ return /* wgsl */ `
28
+ @group(0) @binding(0) var _rzLayer: texture_2d<f32>;
29
+ @group(0) @binding(1) var _rzLayerSamp: sampler;
30
+ // (bloom, _, fullW, fullH). The size is the SCENE target's, not the layer's:
31
+ // the layer is often half resolution, and normalised uv is what makes that
32
+ // invisible.
33
+ @group(0) @binding(2) var<uniform> _rzBlitU: vec4f;
34
+
35
+ @vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
36
+ let x = f32((vi & 1u) << 2u) - 1.0;
37
+ let y = f32((vi & 2u) << 1u) - 1.0;
38
+ return vec4f(x, y, 0.0, 1.0);
39
+ }
40
+
41
+ ${sceneFsOutWgsl()}
42
+ @fragment fn fs(@builtin(position) fragCoord: vec4f) -> FSOut {
43
+ var out: FSOut;
44
+ // Bilinear, because the layer is usually half resolution and the whole point
45
+ // of that choice is that low-frequency effects upsample invisibly.
46
+ let uv = fragCoord.xy / max(_rzBlitU.zw, vec2f(1.0));
47
+ let c = textureSampleLevel(_rzLayer, _rzLayerSamp, uv, 0.0);
48
+ let cov = clamp(c.a, 0.0, 1.0);
49
+ // PREMULTIPLIED, and blitted with a premultiplied-over blend — the field
50
+ // targets have been premultiplied since N effects had to compose into them
51
+ // associatively. Handing this to the material blend would scale rgb by alpha
52
+ // a second time and every layer would come out dark.
53
+ out.color = vec4f(max(c.rgb, vec3f(0.0)), cov);
54
+ // The aux target: bloom mask, then coverage. THIS is what finally makes
55
+ // // @bloom mean something on a field effect — the flag has been parsed and
56
+ // ignored for every mount but particles and ribbons, which is the kind of
57
+ // author-surface lie the install-time guards exist to kill.
58
+ //
59
+ // The mask is written UNWEIGHTED and the blend weights it by src.a — the
60
+ // convention the materials already follow (they write vec4f(1, 1, 0, alpha)).
61
+ out.mask = vec4f(_rzBlitU.x, 1.0, 0.0, cov);
62
+ return out;
63
+ }
64
+ `;
65
+ }
@@ -0,0 +1,31 @@
1
+ import { type CastLayout } from "./particles";
2
+ export declare const GRID_MAX = 1024;
3
+ /** rgba16float, ping-ponged. Two of these is the whole memory cost. */
4
+ export declare const SIM_FORMAT: GPUTextureFormat;
5
+ /** `// @grid 256` — the grid's resolution, in texels per side. */
6
+ export declare function parseGridSize(wgsl: string, max: number): number;
7
+ /** Whether this effect drives a grid at all. */
8
+ export declare function gridEntryPoint(wgsl: string): boolean;
9
+ /**
10
+ * `rzGrid(uv)`, for a shader that READS the grid rather than steps it.
11
+ *
12
+ * Always compiled in, even for an effect with no grid — it then samples a 1×1
13
+ * of zeroes. An accessor that exists only sometimes is one an author has to
14
+ * guard, and a missing function is a compile error rather than a blank result.
15
+ */
16
+ export declare function gridReadApi(group: number, tex: number, samp: number, size: number): string;
17
+ /**
18
+ * The step shader: one invocation per texel, once per frame.
19
+ *
20
+ * The previous grid is bound as a SAMPLED texture and the next as a storage
21
+ * texture, which is what makes the two different resources and the whole thing
22
+ * legal — a shader cannot read and write one texture coherently, and the
23
+ * ping-pong is not an optimisation but the only correct way to do this.
24
+ *
25
+ * Sampled rather than only fetched, because advection needs to read BETWEEN
26
+ * texels: a semi-Lagrangian step asks "what was at the place this parcel came
27
+ * from", and that place is almost never a texel centre. Point-sampling it is
28
+ * what turns smoke into a staircase.
29
+ */
30
+ export declare function buildSimShader(wgsl: string, size: number, cast: CastLayout): string;
31
+ //# sourceMappingURL=grid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAiC7C,eAAO,MAAM,QAAQ,OAAO,CAAA;AAC5B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAM/D;AAED,gDAAgD;AAChD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6B1F;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6DnF"}
@@ -0,0 +1,169 @@
1
+ import { audioApi } from "../audio-api";
2
+ import { lyricsApi } from "../lyrics-api";
3
+ import { anchorAliasWgsl } from "../anchor-table";
4
+ import { midiApi } from "../midi-api";
5
+ import { EFFECT_SCENE_API } from "./composite";
6
+ import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api";
7
+ // A persistent grid an effect can step and read: the one thing an effect could
8
+ // not have before, which is MEMORY.
9
+ //
10
+ // Every other mount is a pure function of position and time. A field shader
11
+ // recomputes its noise from scratch every frame; a trail reads history the
12
+ // engine happens to keep. Neither can build on what it produced last frame, and
13
+ // a whole class of effect is nothing but that:
14
+ //
15
+ // Fog that a dancer walks through does not "look displaced near her" — it IS
16
+ // displaced, and stays displaced, and creeps back over seconds. Water does not
17
+ // look rippled where a foot landed; a ripple leaves and propagates and
18
+ // reflects off the far edge long after the foot is gone. Both are the same
19
+ // shape of thing — a grid, stepped by its own previous value.
20
+ //
21
+ // It is also the only way to get the LOOK of a fluid, not merely its motion.
22
+ // Advection compounds: each frame's field is the last one pushed sideways, and a
23
+ // few hundred frames of that stretches and folds a smooth field into filaments
24
+ // and vortex sheets finer than the grid storing them. No noise function
25
+ // produces that, at any octave count, because the structure is not a function of
26
+ // position at all — it is a function of history.
27
+ //
28
+ // The engine owns the ping-pong, the dispatch and the sampling. The author owns
29
+ // the KERNEL, which is the part that differs: advection for smoke, the wave
30
+ // equation for water, a decaying stamp for footprints. That split is the same
31
+ // one the particle mounts make, and for the same reason — the loop is identical
32
+ // in every such effect and the physics never is.
33
+ // A megatexel of rgba16float is 8MB, and two of those is the whole cost. What
34
+ // actually bounds this is the STEP: one invocation per texel per frame, each
35
+ // taking a dozen samples, so doubling the side quadruples the work.
36
+ export const GRID_MAX = 1024;
37
+ /** rgba16float, ping-ponged. Two of these is the whole memory cost. */
38
+ export const SIM_FORMAT = "rgba16float";
39
+ /** `// @grid 256` — the grid's resolution, in texels per side. */
40
+ export function parseGridSize(wgsl, max) {
41
+ const m = /^\s*\/\/\s*@grid\s+(\d+)\s*$/m.exec(wgsl);
42
+ if (!m)
43
+ return 0;
44
+ // Clamped rather than rejected, as the particle count is: an author asking for
45
+ // 4096 gets the most the engine will give and a scene that still runs.
46
+ return Math.max(8, Math.min(max, parseInt(m[1], 10)));
47
+ }
48
+ /** Whether this effect drives a grid at all. */
49
+ export function gridEntryPoint(wgsl) {
50
+ return /\bfn\s+gridStep\s*\(/.test(wgsl);
51
+ }
52
+ /**
53
+ * `rzGrid(uv)`, for a shader that READS the grid rather than steps it.
54
+ *
55
+ * Always compiled in, even for an effect with no grid — it then samples a 1×1
56
+ * of zeroes. An accessor that exists only sometimes is one an author has to
57
+ * guard, and a missing function is a compile error rather than a blank result.
58
+ */
59
+ export function gridReadApi(group, tex, samp, size) {
60
+ return /* wgsl */ `
61
+ const RZ_GRID_SIZE: f32 = ${size > 0 ? size : 1}.0;
62
+ @group(${group}) @binding(${tex}) var _rzGridTex: texture_2d<f32>;
63
+ @group(${group}) @binding(${samp}) var _rzGridSamp: sampler;
64
+
65
+ /**
66
+ * The grid, bilinearly sampled. uv is 0..1 across it, and what that MEANS in
67
+ * the world is the effect's own business — the engine deliberately does not
68
+ * impose a mapping, because a fog laid over a stage and a ripple field around a
69
+ * character want different ones and both are two lines of arithmetic.
70
+ */
71
+ fn rzGrid(uv: vec2f) -> vec4f {
72
+ return textureSampleLevel(_rzGridTex, _rzGridSamp, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0);
73
+ }
74
+ fn rzGridSize() -> f32 { return RZ_GRID_SIZE; }
75
+ fn rzGridTexel() -> f32 { return 1.0 / RZ_GRID_SIZE; }
76
+
77
+ // ── The step-only half, defined here too and never called here ──
78
+ //
79
+ // One effect file is spliced into EVERY module it has a mount in, so a file
80
+ // with a grid compiles its gridStep inside the field and particle shaders as
81
+ // well — where it is dead code that nothing calls, but still code that has to
82
+ // resolve. Leaving these out is a compile error on a function the author was
83
+ // right to write. rzGridPrev reads the current grid rather than a previous one,
84
+ // which is the honest answer outside the step: there is no previous frame here.
85
+ fn rzGridPrev(uv: vec2f) -> vec4f { return rzGrid(uv); }
86
+ fn rzGridFrame() -> i32 { return 1; }
87
+ `;
88
+ }
89
+ const SIM_UNIFORMS = /* wgsl */ `
90
+ struct SimU {
91
+ time: f32,
92
+ dt: f32,
93
+ size: f32,
94
+ frame: f32,
95
+ }
96
+ `;
97
+ /**
98
+ * The step shader: one invocation per texel, once per frame.
99
+ *
100
+ * The previous grid is bound as a SAMPLED texture and the next as a storage
101
+ * texture, which is what makes the two different resources and the whole thing
102
+ * legal — a shader cannot read and write one texture coherently, and the
103
+ * ping-pong is not an optimisation but the only correct way to do this.
104
+ *
105
+ * Sampled rather than only fetched, because advection needs to read BETWEEN
106
+ * texels: a semi-Lagrangian step asks "what was at the place this parcel came
107
+ * from", and that place is almost never a texel centre. Point-sampling it is
108
+ * what turns smoke into a staircase.
109
+ */
110
+ export function buildSimShader(wgsl, size, cast) {
111
+ return (SIM_UNIFORMS +
112
+ /* wgsl */ `
113
+ @group(0) @binding(0) var<uniform> su: SimU;
114
+ @group(0) @binding(1) var _rzGridPrevTex: texture_2d<f32>;
115
+ @group(0) @binding(2) var _rzGridSamp: sampler;
116
+ @group(0) @binding(3) var _rzGridOut: texture_storage_2d<${SIM_FORMAT}, write>;
117
+ @group(0) @binding(4) var<storage, read> _rzCast: array<vec4f>;
118
+ // The same view uniform the composite reads, so the scene API below is the real
119
+ // thing here rather than a stub — a kernel can ask where a bone is, and a
120
+ // foreground spliced into this module resolves its camera calls.
121
+ @group(0) @binding(6) var<uniform> viewU: array<vec4<f32>, 15>;
122
+ ${clockApi("su.time", "su.dt")}${viewportApi("viewU[6].w")}${trailSlotsApi(cast.trailCount)}
123
+ /** Texels per side. */
124
+ fn rzGridSize() -> f32 { return su.size; }
125
+ /** One texel, in uv — the step a neighbour lookup takes. */
126
+ fn rzGridTexel() -> f32 { return 1.0 / su.size; }
127
+ /**
128
+ * Steps since this effect was installed. Zero on the very first one, which is
129
+ * the only chance to seed a grid: everything after it builds on what is there.
130
+ */
131
+ fn rzGridFrame() -> i32 { return i32(su.frame); }
132
+
133
+ /** The grid as it was LAST frame, bilinear. The only input a kernel really has. */
134
+ fn rzGridPrev(uv: vec2f) -> vec4f {
135
+ return textureSampleLevel(_rzGridPrevTex, _rzGridSamp, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0);
136
+ }
137
+ ` +
138
+ EFFECT_SCENE_API +
139
+ // The alias _rzSlot, which the scene API's accessors route through. Every
140
+ // module embedding EFFECT_SCENE_API must splice this; the sim module was
141
+ // the one that did not, and every gridStep effect failed to install with
142
+ // "unresolved call target" from the day the identity fallback was removed.
143
+ // Found by tools/validate-wgsl.mjs on its first run.
144
+ anchorAliasWgsl(cast.alias) +
145
+ audioApi(0, 5) +
146
+ midiApi(0, 7) +
147
+ lyricsApi(0, 8) +
148
+ // rzGrid itself, so a kernel may read the grid it is writing — through the
149
+ // PREVIOUS frame's texture, which is the only version of it that exists
150
+ // while the current one is still being written.
151
+ /* wgsl */ `
152
+ fn rzGrid(uv: vec2f) -> vec4f { return rzGridPrev(uv); }
153
+ ` +
154
+ "\n// ── user effect (setEffect) ──\n" +
155
+ wgsl +
156
+ /* wgsl */ `
157
+
158
+ @compute @workgroup_size(8, 8)
159
+ fn main(@builtin(global_invocation_id) gid: vec3u) {
160
+ let n = u32(${size});
161
+ if (gid.x >= n || gid.y >= n) { return; }
162
+ let xy = vec2i(i32(gid.x), i32(gid.y));
163
+ // The texel CENTRE, so a kernel that reads its own uv back through rzGridPrev
164
+ // lands on itself rather than a quarter-texel off.
165
+ let uv = (vec2f(f32(gid.x), f32(gid.y)) + vec2f(0.5)) / su.size;
166
+ textureStore(_rzGridOut, xy, gridStep(uv, textureLoad(_rzGridPrevTex, xy, 0), su.dt));
167
+ }
168
+ `);
169
+ }
@@ -1,2 +1,14 @@
1
- export declare const GROUND_SHADOW_SHADER_WGSL = "\nstruct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };\nstruct Light { direction: vec4f, color: vec4f, };\nstruct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };\nstruct GroundShadowMat {\n diffuseColor: vec3f, fadeStart: f32,\n fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, gridSpacing: f32,\n gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, opacity: f32,\n gridLineColor: vec3f, _pad2: f32,\n};\nstruct LightVP { viewProj: mat4x4f, };\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var<uniform> light: LightUniforms;\n@group(0) @binding(2) var shadowMap: texture_depth_2d;\n@group(0) @binding(3) var shadowSampler: sampler_comparison;\n@group(0) @binding(4) var<uniform> material: GroundShadowMat;\n@group(0) @binding(5) var<uniform> lightVP: LightVP;\n\nfn hash2(p: vec2f) -> f32 {\n var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);\n p3 += dot(p3, vec3f(p3.y + 33.33, p3.z + 33.33, p3.x + 33.33));\n return fract((p3.x + p3.y) * p3.z);\n}\nfn valueNoise(p: vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n return mix(mix(hash2(i), hash2(i + vec2f(1.0, 0.0)), u.x),\n mix(hash2(i + vec2f(0.0, 1.0)), hash2(i + vec2f(1.0, 1.0)), u.x), u.y);\n}\nfn fbmNoise(p: vec2f) -> f32 {\n var v = 0.0;\n var a = 0.5;\n var pp = p;\n for (var i = 0; i < 4; i++) {\n v += a * valueNoise(pp);\n pp *= 2.0;\n a *= 0.5;\n }\n return v;\n}\n\nstruct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };\n@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {\n var o: VO; o.worldPos = position; o.normal = normal;\n o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;\n}\nstruct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };\n@fragment fn fs(i: VO) -> FSOut {\n // Derivatives first, unconditionally: WGSL requires fwidth in uniform control\n // flow and everything below branches on world position.\n let gp = i.worldPos.xz / material.gridSpacing;\n let gridDeriv = fwidth(gp);\n\n var out: FSOut;\n let n = normalize(i.normal);\n let centerDist = length(i.worldPos.xz);\n let edgeFade = 1.0 - smoothstep(0.0, 1.0, clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0));\n\n // Past the radial fade every term below is multiplied by edgeFade and the\n // pixel is fully transparent. Shading it anyway produces nothing.\n if (edgeFade <= 0.0) {\n out.color = vec4f(0.0);\n out.mask = vec4f(0.0, 1.0, 0.0, 0.0);\n return out;\n }\n\n let lclip = lightVP.viewProj * vec4f(i.worldPos, 1.0);\n let ndc = lclip.xyz / max(lclip.w, 1e-6);\n // Outside the light's frustum there IS no shadow information \u2014 the clamped\n // border samples compare against unrelated depths and read \"shadowed\",\n // darkening the whole far plane with a visible band at the frustum edge\n // (masked by the opaque surface normally, glaring in green-screen mode where\n // only the shadow-catcher term renders). Fade to fully lit near the border.\n let inZ = select(0.0, 1.0, ndc.z > 0.0 && ndc.z < 1.0);\n let frustum = (1.0 - smoothstep(0.88, 0.96, abs(ndc.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc.y))) * inZ;\n\n // THE cost of this shader, measured: a full-screen ground ran 25 shadow\n // comparisons per pixel, and each one is hardware-bilinear, so ~100 depth\n // samples per pixel. Gutting the rest of the shader changed nothing; cutting\n // this is what makes looking down cheap.\n //\n // 3x3 at two-texel spacing covers the same +/-2 texel penumbra the 5x5 at\n // one-texel spacing did \u2014 that kernel was oversampled, its taps overlapping\n // almost entirely once the comparison sampler's own 2x2 filter is counted.\n // Nine taps instead of twenty-five for the same blur radius.\n //\n // Skipped altogether outside the light frustum, where the mix below discards\n // the result anyway: the shadow map covers a small area around the character\n // and the ground is vastly larger than it.\n var vis = 1.0;\n if (frustum > 0.0) {\n let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);\n let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));\n let st = material.pcfTexel * 2.0;\n let compareZ = ndc.z - 0.0035;\n var acc = 0.0;\n for (var y = -1; y <= 1; y++) {\n for (var x = -1; x <= 1; x++) {\n // ...Level, not the implicit-derivative form: identical on a single-mip\n // shadow map, and legal inside this branch.\n acc += textureSampleCompareLevel(shadowMap, shadowSampler, suv_c + vec2f(f32(x), f32(y)) * st, compareZ);\n }\n }\n vis = mix(1.0, acc * (1.0 / 9.0), frustum);\n }\n\n // Frosted/matte micro-texture. Scenes leaving it at zero were still paying\n // sixteen hash rounds a pixel for a tint of exactly 1.\n var noiseTint = 1.0;\n if (material.noiseStrength != 0.0) {\n let noiseVal = fbmNoise(i.worldPos.xz * 3.0);\n noiseTint = 1.0 + (noiseVal - 0.5) * material.noiseStrength;\n }\n\n // Grid lines \u2014 anti-aliased via screen-space derivatives\n let gridFrac = abs(fract(gp - 0.5) - 0.5);\n let halfLine = material.gridLineWidth * 0.5;\n let gridLine = 1.0 - min(\n smoothstep(halfLine - gridDeriv.x, halfLine + gridDeriv.x, gridFrac.x),\n smoothstep(halfLine - gridDeriv.y, halfLine + gridDeriv.y, gridFrac.y)\n );\n let sun = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, -light.lights[0].direction.xyz), 0.0);\n let dark = (1.0 - vis) * material.shadowStrength;\n var baseColor = material.diffuseColor * sun * (1.0 - dark * 0.65);\n baseColor *= noiseTint;\n let finalColor = mix(baseColor, material.gridLineColor, gridLine * material.gridLineOpacity * edgeFade);\n // Whole-ground opacity fades the SURFACE (color, grid) but the shadow stays \u2014\n // as opacity drops, the received shadow becomes a translucent dark layer\n // (Blender's Shadow Catcher), so models still feel grounded on a photo or\n // 360 backdrop. At opacity 1 this reduces exactly to the plain surface.\n let surfA = edgeFade * material.opacity;\n let catchA = dark * 0.65 * edgeFade * (1.0 - material.opacity);\n let outA = surfA + catchA;\n out.color = vec4f(finalColor * surfA, outA);\n // mask.r = 0: ground never contributes to bloom. mask.g = 1.0 with src.a =\n // outA turns the aux blend into alpha-over, so the drawable alpha goes\n // from outA at the center to 0 at the radial edge \u2014 letting the page\n // background show through under the premultiplied canvas alphaMode.\n out.mask = vec4f(0.0, 1.0, 0.0, outA);\n return out;\n}\n";
1
+ export declare function groundShaderWgsl(): string;
2
+ /**
3
+ * The ground belongs to no model instance, so it takes ids of its own — at the
4
+ * TOP of the u16 range, not at the bottom.
5
+ *
6
+ * Both of the engine's counters are 1-based (model id is instance count + 1,
7
+ * material id counts from one), so 1 is the first model's first material and
8
+ * the floor would have shared it. 0 stays unclaimed in the other direction: it
9
+ * is what the attachment clears to, and so what every pixel nothing drew
10
+ * reports.
11
+ */
12
+ export declare const GROUND_MATERIAL_ID = 65535;
13
+ export declare const GROUND_OBJECT_ID = 65535;
2
14
  //# sourceMappingURL=ground.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ground.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,yBAAyB,gpNA8IrC,CAAA"}
1
+ {"version":3,"file":"ground.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground.ts"],"names":[],"mappings":"AAYA,wBAAgB,gBAAgB,IAAI,MAAM,CA2QzC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAA;AACxC,eAAO,MAAM,gBAAgB,QAAS,CAAA"}
@@ -1,22 +1,47 @@
1
+ import { sceneFsOutWgsl, sceneIdWriteWgsl } from "./scene-contract";
2
+ import { lightsApi } from "../lights";
3
+ import { SHADOW_CASCADES } from "../../shadow-cascades";
4
+ import { WORLD_AMBIENT_WGSL } from "../lights";
1
5
  // Ground shadow-catcher: receives directional shadow, grid lines, frosted noise,
2
6
  // radial distance fade. Writes bloom mask = 0 (ground never bloom-bleeds).
3
- export const GROUND_SHADOW_SHADER_WGSL = /* wgsl */ `
7
+ //
8
+ // A FUNCTION, not a constant, since the id attachment is a device capability:
9
+ // the struct this returns depends on what the probe at init found, and a string
10
+ // baked at import time cannot know that. Called once, when the module is built.
11
+ export function groundShaderWgsl() {
12
+ return /* wgsl */ `
4
13
  struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
5
14
  struct Light { direction: vec4f, color: vec4f, };
6
- struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
15
+ struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, sh: array<vec4f, 9>, };
7
16
  struct GroundShadowMat {
8
17
  diffuseColor: vec3f, fadeStart: f32,
9
18
  fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, gridSpacing: f32,
10
19
  gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, opacity: f32,
11
- gridLineColor: vec3f, _pad2: f32,
20
+ gridLineColor: vec3f, mirror: f32,
21
+ mirrorBlur: f32, _mb0: f32, _mb1: f32, _mb2: f32,
12
22
  };
13
- struct LightVP { viewProj: mat4x4f, };
23
+ // One view-projection per shadow cascade, inner to outer — same buffer and
24
+ // same order the materials read.
25
+ struct LightVP { viewProj: array<mat4x4f, ${SHADOW_CASCADES.length}>, };
14
26
  @group(0) @binding(0) var<uniform> camera: CameraUniforms;
15
27
  @group(0) @binding(1) var<uniform> light: LightUniforms;
16
28
  @group(0) @binding(2) var shadowMap: texture_depth_2d;
17
29
  @group(0) @binding(3) var shadowSampler: sampler_comparison;
18
30
  @group(0) @binding(4) var<uniform> material: GroundShadowMat;
19
31
  @group(0) @binding(5) var<uniform> lightVP: LightVP;
32
+ // The far cascade's map, binding 7 as in the materials' layout.
33
+ @group(0) @binding(7) var shadowMapFar: texture_depth_2d;
34
+ // The floor mirror (step 7D): the reflection target, the camera that rendered
35
+ // it, and an ordinary sampler — the two shadow bindings above are comparison.
36
+ // params = (projA, projB, _, _) — the depth-linearisation pair of the SHARED
37
+ // projection, refreshed per frame beside the matrix.
38
+ struct MirrorVP { viewProj: mat4x4f, params: vec4f, };
39
+ @group(0) @binding(8) var<uniform> mirrorVP: MirrorVP;
40
+ @group(0) @binding(9) var mirrorTex: texture_2d<f32>;
41
+ @group(0) @binding(10) var linearSampler: sampler;
42
+ @group(0) @binding(11) var mirrorDepth: texture_depth_multisampled_2d;
43
+ ${WORLD_AMBIENT_WGSL}
44
+ ${lightsApi(0, 6)}
20
45
 
21
46
  fn hash2(p: vec2f) -> f32 {
22
47
  var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
@@ -47,8 +72,7 @@ struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @l
47
72
  var o: VO; o.worldPos = position; o.normal = normal;
48
73
  o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;
49
74
  }
50
- struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
51
- @fragment fn fs(i: VO) -> FSOut {
75
+ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
52
76
  // Derivatives first, unconditionally: WGSL requires fwidth in uniform control
53
77
  // flow and everything below branches on world position.
54
78
  let gp = i.worldPos.xz / material.gridSpacing;
@@ -67,15 +91,20 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
67
91
  return out;
68
92
  }
69
93
 
70
- let lclip = lightVP.viewProj * vec4f(i.worldPos, 1.0);
71
- let ndc = lclip.xyz / max(lclip.w, 1e-6);
72
- // Outside the light's frustum there IS no shadow information — the clamped
94
+ // Outside a cascade's frustum there IS no shadow information — the clamped
73
95
  // border samples compare against unrelated depths and read "shadowed",
74
96
  // darkening the whole far plane with a visible band at the frustum edge
75
97
  // (masked by the opaque surface normally, glaring in green-screen mode where
76
- // only the shadow-catcher term renders). Fade to fully lit near the border.
98
+ // only the shadow-catcher term renders). So each cascade fades out near its
99
+ // border — the near one INTO the far one's answer, the far one into lit.
100
+ let l0 = lightVP.viewProj[0] * vec4f(i.worldPos, 1.0);
101
+ let ndc = l0.xyz / max(l0.w, 1e-6);
77
102
  let inZ = select(0.0, 1.0, ndc.z > 0.0 && ndc.z < 1.0);
78
103
  let frustum = (1.0 - smoothstep(0.88, 0.96, abs(ndc.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc.y))) * inZ;
104
+ let l1 = lightVP.viewProj[1] * vec4f(i.worldPos, 1.0);
105
+ let ndc1 = l1.xyz / max(l1.w, 1e-6);
106
+ let inZ1 = select(0.0, 1.0, ndc1.z > 0.0 && ndc1.z < 1.0);
107
+ let frustum1 = (1.0 - smoothstep(0.88, 0.96, abs(ndc1.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc1.y))) * inZ1;
79
108
 
80
109
  // THE cost of this shader, measured: a full-screen ground ran 25 shadow
81
110
  // comparisons per pixel, and each one is hardware-bilinear, so ~100 depth
@@ -87,10 +116,23 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
87
116
  // almost entirely once the comparison sampler's own 2x2 filter is counted.
88
117
  // Nine taps instead of twenty-five for the same blur radius.
89
118
  //
90
- // Skipped altogether outside the light frustum, where the mix below discards
91
- // the result anyway: the shadow map covers a small area around the character
92
- // and the ground is vastly larger than it.
119
+ // The far cascade's taps run ONLY where the near one is fading or absent
120
+ // (frustum < 1), so a pixel in the near core costs exactly what it did with
121
+ // one cascade — and that core is where the camera usually looks.
93
122
  var vis = 1.0;
123
+ if (frustum < 1.0 && frustum1 > 0.0) {
124
+ let suv1 = vec2f(ndc1.x * 0.5 + 0.5, 0.5 - ndc1.y * 0.5);
125
+ let suv1_c = clamp(suv1, vec2f(0.02), vec2f(0.98));
126
+ let st1 = ${1 / SHADOW_CASCADES[SHADOW_CASCADES.length - 1].mapSize} * 2.0;
127
+ let compareZ1 = ndc1.z - 0.0035;
128
+ var acc1 = 0.0;
129
+ for (var y = -1; y <= 1; y++) {
130
+ for (var x = -1; x <= 1; x++) {
131
+ acc1 += textureSampleCompareLevel(shadowMapFar, shadowSampler, suv1_c + vec2f(f32(x), f32(y)) * st1, compareZ1);
132
+ }
133
+ }
134
+ vis = mix(1.0, acc1 * (1.0 / 9.0), frustum1);
135
+ }
94
136
  if (frustum > 0.0) {
95
137
  let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
96
138
  let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));
@@ -104,7 +146,9 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
104
146
  acc += textureSampleCompareLevel(shadowMap, shadowSampler, suv_c + vec2f(f32(x), f32(y)) * st, compareZ);
105
147
  }
106
148
  }
107
- vis = mix(1.0, acc * (1.0 / 9.0), frustum);
149
+ // The base is whatever the far cascade decided, so the near border blends
150
+ // cascade to cascade rather than snapping to lit mid-floor.
151
+ vis = mix(vis, acc * (1.0 / 9.0), frustum);
108
152
  }
109
153
 
110
154
  // Frosted/matte micro-texture. Scenes leaving it at zero were still paying
@@ -122,24 +166,125 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
122
166
  smoothstep(halfLine - gridDeriv.x, halfLine + gridDeriv.x, gridFrac.x),
123
167
  smoothstep(halfLine - gridDeriv.y, halfLine + gridDeriv.y, gridFrac.y)
124
168
  );
125
- let sun = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, -light.lights[0].direction.xyz), 0.0);
169
+ let sun = rzWorldAmbient(n) + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, -light.lights[0].direction.xyz), 0.0);
170
+ // The positional layer reaches the floor too. A stage light that lit the
171
+ // cast and not the ground under her would read as a sticker, which is the
172
+ // same failure the shadow catcher exists to prevent.
173
+ let lamps = rzLightsDiffuse(i.worldPos, n);
126
174
  let dark = (1.0 - vis) * material.shadowStrength;
127
- var baseColor = material.diffuseColor * sun * (1.0 - dark * 0.65);
175
+ // The shadow darkens the SUN only. The dark term comes from the sun's shadow
176
+ // map, so applying it to the lamps too would have a lamp on the far side of
177
+ // the stage dimmed by a shadow cast away from it — the one light in the scene
178
+ // that provably is not blocking it.
179
+ // (No backticks in here: this string is a TS template literal and one ends it
180
+ // mid-shader, which is exactly how this line broke the build a moment ago.)
181
+ var baseColor = material.diffuseColor * (sun * (1.0 - dark * 0.65) + lamps);
128
182
  baseColor *= noiseTint;
129
- let finalColor = mix(baseColor, material.gridLineColor, gridLine * material.gridLineOpacity * edgeFade);
130
- // Whole-ground opacity fades the SURFACE (color, grid) but the shadow stays —
131
- // as opacity drops, the received shadow becomes a translucent dark layer
132
- // (Blender's Shadow Catcher), so models still feel grounded on a photo or
133
- // 360 backdrop. At opacity 1 this reduces exactly to the plain surface.
183
+ // The floor mirror — its OWN LAYER, not a blend into the surface. The
184
+ // reflection was rendered by the MIRROR camera, so projecting this
185
+ // fragment's world position through that camera yields exactly the texel
186
+ // where its reflection landed — projective mapping, no screen-space guess.
187
+ // There is deliberately NO strength dial: mirror is on or off, and how much
188
+ // of it shows is the SURFACE's own opacity covering it — the same
189
+ // independence the grid won earlier. The branch is on a uniform, so the
190
+ // whole cost vanishes for the scenes that leave it off.
191
+ var reflShadowed = vec3f(0.0);
192
+ if (material.mirror > 0.0) {
193
+ let mc = mirrorVP.viewProj * vec4f(i.worldPos, 1.0);
194
+ let mndc = mc.xyz / max(mc.w, 1e-6);
195
+ let muv = clamp(vec2f(mndc.x * 0.5 + 0.5, 0.5 - mndc.y * 0.5), vec2f(0.0), vec2f(1.0));
196
+ var lod = 0.0;
197
+ if (material.mirrorBlur > 0.0) {
198
+ // DEPTH-PROPORTIONAL softness: the real driver is how far BEHIND the
199
+ // mirror surface the reflected geometry sits — a foot on the floor
200
+ // reflects sharp, a head reflects soft, the empty backdrop softest.
201
+ //
202
+ // Read the mirror pass's own depth at this texel, linearise it with the
203
+ // shared projection's pair (viewZ = projB / (z - projA); the formula
204
+ // inverts both depth conventions, see the composite's linearDepth), and
205
+ // reconstruct the reflected image point along the ray from the MIRRORED
206
+ // eye through this fragment. The plane is y = 0, so mirroring the eye
207
+ // and the camera forward is a sign flip on y, and the image point's
208
+ // depth below the plane IS the reflected object's height above it.
209
+ let dims = vec2f(textureDimensions(mirrorDepth));
210
+ let texel = clamp(vec2i(muv * dims), vec2i(0), vec2i(dims) - vec2i(1));
211
+ let z = textureLoad(mirrorDepth, texel, 0);
212
+ let viewZ = clamp(mirrorVP.params.y / (z - mirrorVP.params.x), 0.05, 100000.0);
213
+ let eyeM = vec3f(camera.viewPos.x, -camera.viewPos.y, camera.viewPos.z);
214
+ let fwd = vec3f(camera.view[0][2], camera.view[1][2], camera.view[2][2]);
215
+ let fwdM = vec3f(fwd.x, -fwd.y, fwd.z);
216
+ let toFrag = i.worldPos - eyeM;
217
+ let dir = toFrag * inverseSqrt(max(dot(toFrag, toFrag), 1e-8));
218
+ let t = viewZ / max(dot(dir, fwdM), 1e-4);
219
+ let height = max(-(eyeM.y + dir.y * t), 0.0);
220
+ // Full softness by BLUR_SPAN units above the floor — about a character's
221
+ // height. The hardware clamps the level, so the nominal 5.0 needs no
222
+ // knowledge of the real chain length, and blur 0 reads exactly level 0.
223
+ let BLUR_SPAN = 14.0;
224
+ lod = material.mirrorBlur * 5.0 * clamp(height / BLUR_SPAN, 0.0, 1.0);
225
+ }
226
+ let refl = textureSampleLevel(mirrorTex, linearSampler, muv, lod).rgb;
227
+ // Still under the received shadow: a polished floor in shade shows a dim
228
+ // reflection, and a mirror that ignored the shadow would glow in it.
229
+ reflShadowed = refl * (1.0 - dark * 0.65);
230
+ }
231
+ // THREE LAYERS, composited premultiplied, bottom to top: the shadow the
232
+ // catcher receives, the ground surface, and the grid.
233
+ //
234
+ // The grid used to be MIXED INTO the surface and then weighted by the
235
+ // surface's own alpha, so turning the ground down to nothing took the grid
236
+ // with it — you could not have a bare grid over a photo, which is most of
237
+ // what a grid is for. It is its own layer now: the ground plane keeps an
238
+ // alpha, and the grid is simply on or off.
134
239
  let surfA = edgeFade * material.opacity;
135
- let catchA = dark * 0.65 * edgeFade * (1.0 - material.opacity);
136
- let outA = surfA + catchA;
137
- out.color = vec4f(finalColor * surfA, outA);
240
+ let gridA = gridLine * material.gridLineOpacity * edgeFade;
241
+ // The mirror is opaque across the ground's extent when on — it always shows
242
+ // SOMETHING, because the backdrop rides in the reflection target itself.
243
+ let mirrA = material.mirror * edgeFade;
244
+ // As opacity drops the received shadow becomes a translucent dark layer
245
+ // (Blender's Shadow Catcher), so models still feel grounded on a photo or a
246
+ // 360 backdrop. Colourless by construction: it darkens by covering. The
247
+ // MIRROR subsumes it: the reflection already carries the received shadow,
248
+ // and a catcher stacked on top would darken the same shadow twice.
249
+ let catchA = dark * 0.65 * edgeFade * (1.0 - material.opacity) * (1.0 - material.mirror);
250
+
251
+ // FOUR LAYERS, premultiplied, bottom to top: reflection, received shadow,
252
+ // ground surface, grid. Each with its own coverage, none scaling another —
253
+ // ground opacity REVEALS the mirror rather than the mirror carrying a
254
+ // strength of its own.
255
+ var pm = reflShadowed * mirrA;
256
+ var cov = mirrA;
257
+ // Shadow catcher over the reflection-or-nothing: black, it covers.
258
+ pm = pm * (1.0 - catchA);
259
+ cov = catchA + cov * (1.0 - catchA);
260
+ // Surface over shadow.
261
+ pm = baseColor * surfA + pm * (1.0 - surfA);
262
+ cov = surfA + cov * (1.0 - surfA);
263
+ // Grid over surface. Unshadowed, as it has always been — a painted line reads
264
+ // as paint, and darkening it was never what the mix did either.
265
+ pm = material.gridLineColor * gridA + pm * (1.0 - gridA);
266
+ cov = gridA + cov * (1.0 - gridA);
267
+
268
+ let outA = cov;
269
+ out.color = vec4f(pm, outA);
138
270
  // mask.r = 0: ground never contributes to bloom. mask.g = 1.0 with src.a =
139
271
  // outA turns the aux blend into alpha-over, so the drawable alpha goes
140
272
  // from outA at the center to 0 at the radial edge — letting the page
141
273
  // background show through under the premultiplied canvas alphaMode.
142
274
  out.mask = vec4f(0.0, 1.0, 0.0, outA);
143
- return out;
275
+ ${sceneIdWriteWgsl("out", `${GROUND_MATERIAL_ID}u`, `${GROUND_OBJECT_ID}u`)} return out;
144
276
  }
145
277
  `;
278
+ }
279
+ /**
280
+ * The ground belongs to no model instance, so it takes ids of its own — at the
281
+ * TOP of the u16 range, not at the bottom.
282
+ *
283
+ * Both of the engine's counters are 1-based (model id is instance count + 1,
284
+ * material id counts from one), so 1 is the first model's first material and
285
+ * the floor would have shared it. 0 stays unclaimed in the other direction: it
286
+ * is what the attachment clears to, and so what every pixel nothing drew
287
+ * reports.
288
+ */
289
+ export const GROUND_MATERIAL_ID = 0xffff;
290
+ export const GROUND_OBJECT_ID = 0xffff;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The particle record, laid out by hand — declared in EVERY hosting module.
3
+ *
4
+ * Not only in the particle ones: `fn particleStep(p: Particle)` is part of the
5
+ * author's file, so the trail, field and lightEmit modules compile that
6
+ * signature as dead code and a missing struct is a compile error on a function
7
+ * the author was right to write. The trail module used to carry a hand-copied
8
+ * duplicate for exactly this reason.
9
+ *
10
+ * `age` and `life` sit in the padding that vec3f alignment would waste anyway
11
+ * (a vec3f occupies 12 bytes but aligns the next field to 16), so the struct is
12
+ * 48 bytes rather than the 64 a naive ordering costs. At 4096 particles that is
13
+ * 192KB instead of 256KB, and it is read every frame by both stages.
14
+ *
15
+ * `life <= 0` means "not alive" and is what the pool checks to recycle a slot,
16
+ * so a freshly zeroed buffer is entirely dead and every particle is born on the
17
+ * first step rather than needing a separate seeding pass.
18
+ *
19
+ * RzLight is the same kind of thing and lives in lights.ts, because it is that
20
+ * module's own type; every hosting module splices it for this same reason.
21
+ */
22
+ export declare const PARTICLE_STRUCT_WGSL = "\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";
23
+ /**
24
+ * `rzTime`/`rzDt`, from whatever the module calls its clock.
25
+ *
26
+ * A module with no clock of its own passes a literal. Zero dt is honest there:
27
+ * nothing spliced into it is being stepped, so a kernel that integrates would
28
+ * integrate by nothing rather than by a plausible-looking wrong number.
29
+ */
30
+ export declare function clockApi(timeExpr: string, dtExpr: string): string;
31
+ /** `rzViewportHeight` — the render target's height in pixels, which is what a
32
+ * point size in world units has to be divided by to become a pixel radius. */
33
+ export declare function viewportApi(heightExpr: string): string;
34
+ /**
35
+ * The one cast number that is PER EFFECT rather than per engine.
36
+ *
37
+ * Author-visible, and published effects loop over it, so its meaning is pinned:
38
+ * how many of THIS effect's anchors asked for a trail. Deliberately not the
39
+ * anchor address space — that is RZ_MAX_ANCHORS in CAST_API, and the two being
40
+ * one number was the old trail bug. Everything else about the cast's shape is a
41
+ * constant and lives there.
42
+ */
43
+ export declare function trailSlotsApi(trailCount: number): string;
44
+ /**
45
+ * Pure math — no bindings, no uniforms, identical in every module.
46
+ *
47
+ * These were duplicated in the particle and trail modules, byte for byte in the
48
+ * case of rzFalloff and to a `+ vec3f(0.0)` in the case of rzValueNoise. Two
49
+ * copies of a noise function is one bad merge away from an effect that looks
50
+ * different depending on which mount drew it, which is not a bug anyone would
51
+ * think to look for.
52
+ *
53
+ * EVERY hosting module includes this and none defines any of these itself, so
54
+ * "which module am I in" can never change what a helper returns.
55
+ */
56
+ export declare const EFFECT_MATH_API = "\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";
57
+ //# sourceMappingURL=hosted-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosted-api.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/hosted-api.ts"],"names":[],"mappings":"AAuBA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,oBAAoB,+UAchC,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKjE;AAED;+EAC+E;AAC/E,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAItD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,ixEA6D3B,CAAA"}