reze-engine 0.50.3 → 0.50.4

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 (93) hide show
  1. package/dist/engine.d.ts +237 -2
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +793 -64
  4. package/dist/physics/autofit.d.ts +147 -0
  5. package/dist/physics/autofit.d.ts.map +1 -0
  6. package/dist/physics/autofit.js +501 -0
  7. package/dist/shaders/cast-api.d.ts +1 -1
  8. package/dist/shaders/cast-api.d.ts.map +1 -1
  9. package/dist/shaders/cast-layout.d.ts +44 -1
  10. package/dist/shaders/cast-layout.d.ts.map +1 -1
  11. package/dist/shaders/cast-layout.js +44 -1
  12. package/dist/shaders/materials/common.d.ts.map +1 -1
  13. package/dist/shaders/materials/common.js +7 -1
  14. package/dist/shaders/materials/nodes.d.ts +1 -1
  15. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  16. package/dist/shaders/materials/nodes.js +17 -9
  17. package/dist/shaders/passes/composite.d.ts +1 -1
  18. package/dist/shaders/passes/composite.d.ts.map +1 -1
  19. package/dist/shaders/passes/depth-prepass.d.ts +1 -1
  20. package/dist/shaders/passes/depth-prepass.d.ts.map +1 -1
  21. package/dist/shaders/passes/depth-prepass.js +52 -12
  22. package/dist/shaders/passes/field-blit.d.ts +26 -0
  23. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  24. package/dist/shaders/passes/field-blit.js +65 -0
  25. package/dist/shaders/passes/ground-noise.d.ts +7 -0
  26. package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
  27. package/dist/shaders/passes/ground-noise.js +88 -0
  28. package/dist/shaders/passes/ground.d.ts +16 -0
  29. package/dist/shaders/passes/ground.d.ts.map +1 -1
  30. package/dist/shaders/passes/ground.js +131 -27
  31. package/dist/shaders/passes/outline.d.ts +1 -1
  32. package/dist/shaders/passes/outline.d.ts.map +1 -1
  33. package/dist/shaders/passes/outline.js +12 -3
  34. package/dist/shaders/passes/particles.d.ts.map +1 -1
  35. package/dist/shaders/passes/particles.js +6 -2
  36. package/dist/shaders/passes/scene-contract.d.ts +38 -6
  37. package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
  38. package/dist/shaders/passes/scene-contract.js +53 -16
  39. package/dist/shaders/passes/sim.d.ts +34 -0
  40. package/dist/shaders/passes/sim.d.ts.map +1 -0
  41. package/dist/shaders/passes/sim.js +169 -0
  42. package/dist/shaders/passes/trails.d.ts.map +1 -1
  43. package/dist/shaders/passes/trails.js +36 -8
  44. package/dist/shaders/score-api.d.ts +10 -0
  45. package/dist/shaders/score-api.d.ts.map +1 -0
  46. package/dist/shaders/score-api.js +114 -0
  47. package/package.json +1 -1
  48. package/src/engine.ts +839 -56
  49. package/src/shaders/cast-layout.ts +44 -1
  50. package/src/shaders/materials/common.ts +7 -1
  51. package/src/shaders/materials/nodes.ts +17 -9
  52. package/src/shaders/passes/depth-prepass.ts +53 -12
  53. package/src/shaders/passes/ground.ts +133 -27
  54. package/src/shaders/passes/outline.ts +14 -3
  55. package/src/shaders/passes/particles.ts +6 -2
  56. package/src/shaders/passes/scene-contract.ts +55 -16
  57. package/src/shaders/passes/trails.ts +36 -8
  58. package/dist/physics-debug.d.ts +0 -30
  59. package/dist/physics-debug.d.ts.map +0 -1
  60. package/dist/physics-debug.js +0 -526
  61. package/dist/shaders/materials/body.d.ts +0 -2
  62. package/dist/shaders/materials/body.d.ts.map +0 -1
  63. package/dist/shaders/materials/body.js +0 -95
  64. package/dist/shaders/materials/cloth_rough.d.ts +0 -2
  65. package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
  66. package/dist/shaders/materials/cloth_rough.js +0 -69
  67. package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
  68. package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
  69. package/dist/shaders/materials/cloth_smooth.js +0 -61
  70. package/dist/shaders/materials/default.d.ts +0 -2
  71. package/dist/shaders/materials/default.d.ts.map +0 -1
  72. package/dist/shaders/materials/default.js +0 -43
  73. package/dist/shaders/materials/eye.d.ts +0 -2
  74. package/dist/shaders/materials/eye.d.ts.map +0 -1
  75. package/dist/shaders/materials/eye.js +0 -60
  76. package/dist/shaders/materials/face.d.ts +0 -2
  77. package/dist/shaders/materials/face.d.ts.map +0 -1
  78. package/dist/shaders/materials/face.js +0 -95
  79. package/dist/shaders/materials/hair.d.ts +0 -2
  80. package/dist/shaders/materials/hair.d.ts.map +0 -1
  81. package/dist/shaders/materials/hair.js +0 -90
  82. package/dist/shaders/materials/metal.d.ts +0 -2
  83. package/dist/shaders/materials/metal.d.ts.map +0 -1
  84. package/dist/shaders/materials/metal.js +0 -77
  85. package/dist/shaders/materials/mmd_classic.d.ts +0 -2
  86. package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
  87. package/dist/shaders/materials/mmd_classic.js +0 -66
  88. package/dist/shaders/materials/stockings.d.ts +0 -2
  89. package/dist/shaders/materials/stockings.d.ts.map +0 -1
  90. package/dist/shaders/materials/stockings.js +0 -122
  91. package/dist/shaders/passes/physics-debug.d.ts +0 -2
  92. package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
  93. package/dist/shaders/passes/physics-debug.js +0 -69
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The field layer, drawn INTO the scene.
3
+ *
4
+ * Field effects render into their own targets at their own resolution — that
5
+ * part was always right. What was wrong was where the result landed: the
6
+ * composite sampled it AFTER tone mapping, which is why a field effect could
7
+ * never bloom, could never light anything, and tone mapped differently from the
8
+ * particle and ribbon mounts drawing the same kind of glow.
9
+ *
10
+ * Two draws inside the scene pass — background before any geometry, foreground
11
+ * after all of it — so the bloom pyramid sees field effects and one tone map
12
+ * covers the whole frame.
13
+ *
14
+ * NO DEPTH, neither written nor tested. A foreground is handed the scene's
15
+ * depth as a parameter and does its own occlusion; testing it here would
16
+ * occlude it a second time, and a background has nothing to test against.
17
+ *
18
+ * COVERAGE IS CLAMPED HERE. Additive field effects SUM their coverage into the
19
+ * layer (see FIELD_LAYER_BLEND_ADDITIVE), which is what stops the composite's
20
+ * unpremultiply from dividing colour by nothing — but N additive effects can
21
+ * sum past 1, and the layer is rgba16float so nothing stopped it on the way in.
22
+ * A coverage above 1 would darken what the layer is drawn over by more than
23
+ * fully covering it.
24
+ */
25
+ export declare function fieldBlitShaderWgsl(): string;
26
+ //# sourceMappingURL=field-blit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-blit.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/field-blit.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAuC5C"}
@@ -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,7 @@
1
+ /**
2
+ * The tile as 8-bit luminance, plus its mip chain (box-filtered, which is exact
3
+ * for a repeating texture). Mips are the point as much as the speed: procedural
4
+ * noise has no mip chain, so at grazing angles it shimmered.
5
+ */
6
+ export declare function buildGroundNoiseMips(): Uint8Array[];
7
+ //# sourceMappingURL=ground-noise.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ground-noise.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground-noise.ts"],"names":[],"mappings":"AAkDA;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAsCnD"}
@@ -0,0 +1,88 @@
1
+ // Bakes the ground's frosted micro-texture into a seamless, mipmapped tile.
2
+ //
3
+ // The shader used to evaluate four octaves of value noise per pixel — sixteen
4
+ // hash rounds — and the ground covers the entire screen as soon as the camera
5
+ // tilts toward the horizon. This produces the same character of pattern once, at
6
+ // load, so the fragment shader pays one filtered fetch instead.
7
+ //
8
+ // SEAMLESS BY CONSTRUCTION. The original hash was not periodic, so a finite
9
+ // crop of it could never tile. Here every octave's lattice wraps at a whole
10
+ // number of cells across the tile, which makes the result repeat exactly at the
11
+ // edges. The pattern is therefore not pixel-identical to the analytic version —
12
+ // it is the same kind of noise, not the same noise. At the default strength of
13
+ // 0.05 it modulates brightness by ±2.5%, where the repeat is not findable by eye.
14
+ import { GROUND_NOISE_RES, GROUND_NOISE_TILE_WORLD } from "./ground";
15
+ /** Lattice cells across the tile at octave 0. The analytic version sampled fbm
16
+ * at worldXZ × 3, so a tile spanning 8 world units covers 24 base cells — the
17
+ * same spatial frequency the surface had before. */
18
+ const BASE_CELLS = GROUND_NOISE_TILE_WORLD * 3;
19
+ const OCTAVES = 4;
20
+ /** Deterministic lattice hash. Integer coordinates are wrapped by the caller,
21
+ * which is what makes the tile seamless. */
22
+ function hash(ix, iy) {
23
+ let h = Math.imul(ix, 374761393) + Math.imul(iy, 668265263);
24
+ h = Math.imul(h ^ (h >>> 13), 1274126177);
25
+ return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
26
+ }
27
+ /** Value noise on a lattice that repeats every `period` cells. */
28
+ function periodicValueNoise(x, y, period) {
29
+ const ix = Math.floor(x);
30
+ const iy = Math.floor(y);
31
+ const fx = x - ix;
32
+ const fy = y - iy;
33
+ // Smoothstep weights — same curve the WGSL used.
34
+ const ux = fx * fx * (3 - 2 * fx);
35
+ const uy = fy * fy * (3 - 2 * fy);
36
+ const x0 = ((ix % period) + period) % period;
37
+ const y0 = ((iy % period) + period) % period;
38
+ const x1 = (x0 + 1) % period;
39
+ const y1 = (y0 + 1) % period;
40
+ const a = hash(x0, y0);
41
+ const b = hash(x1, y0);
42
+ const c = hash(x0, y1);
43
+ const d = hash(x1, y1);
44
+ return (a + (b - a) * ux) * (1 - uy) + (c + (d - c) * ux) * uy;
45
+ }
46
+ /**
47
+ * The tile as 8-bit luminance, plus its mip chain (box-filtered, which is exact
48
+ * for a repeating texture). Mips are the point as much as the speed: procedural
49
+ * noise has no mip chain, so at grazing angles it shimmered.
50
+ */
51
+ export function buildGroundNoiseMips() {
52
+ const levels = [];
53
+ const res = GROUND_NOISE_RES;
54
+ const base = new Uint8Array(res * res);
55
+ for (let py = 0; py < res; py++) {
56
+ for (let px = 0; px < res; px++) {
57
+ // Texel centre, in units of octave-0 lattice cells.
58
+ const x = ((px + 0.5) / res) * BASE_CELLS;
59
+ const y = ((py + 0.5) / res) * BASE_CELLS;
60
+ let v = 0;
61
+ let amp = 0.5;
62
+ let freq = 1;
63
+ for (let o = 0; o < OCTAVES; o++) {
64
+ v += amp * periodicValueNoise(x * freq, y * freq, BASE_CELLS * freq);
65
+ freq *= 2;
66
+ amp *= 0.5;
67
+ }
68
+ base[py * res + px] = Math.max(0, Math.min(255, Math.round(v * 255)));
69
+ }
70
+ }
71
+ levels.push(base);
72
+ let src = base;
73
+ let size = res;
74
+ while (size > 1) {
75
+ const half = size >> 1;
76
+ const dst = new Uint8Array(half * half);
77
+ for (let y = 0; y < half; y++) {
78
+ for (let x = 0; x < half; x++) {
79
+ const s = y * 2 * size + x * 2;
80
+ dst[y * half + x] = (src[s] + src[s + 1] + src[s + size] + src[s + size + 1] + 2) >> 2;
81
+ }
82
+ }
83
+ levels.push(dst);
84
+ src = dst;
85
+ size = half;
86
+ }
87
+ return levels;
88
+ }
@@ -11,4 +11,20 @@ export declare function groundShaderWgsl(): string;
11
11
  */
12
12
  export declare const GROUND_MATERIAL_ID = 65535;
13
13
  export declare const GROUND_OBJECT_ID = 65535;
14
+ /** Texel edge of the baked frost tile. 1024 over a 64-p-unit period is two
15
+ * texels per finest-octave noise cell — the bake resolves everything the
16
+ * evaluation produced. r8unorm: the value lives in [0,1] and feeds a 0.05
17
+ * tint; eight bits is already 5x below what that tint can show. */
18
+ export declare const GROUND_NOISE_SIZE = 1024;
19
+ /**
20
+ * The frost fbm as a bake: identical math, periodic lattice.
21
+ *
22
+ * hash2/valueNoise/fbm are the ground's old helpers verbatim, with one change —
23
+ * each octave's lattice wraps at its own period (64 p-units at octave 0,
24
+ * doubling with frequency), so the tile is seamless under the repeat sampler.
25
+ * Wrapping only renames lattice cells on the seam; every interior cell hashes
26
+ * exactly as before, which is what makes the bake a relocation of the old
27
+ * pixels rather than a new look.
28
+ */
29
+ export declare const GROUND_NOISE_BAKE_WGSL = "\n@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {\n let x = f32((vi & 1u) << 2u) - 1.0;\n let y = f32((vi & 2u) << 1u) - 1.0;\n return vec4f(x, y, 0.0, 1.0);\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 wrapCell(i: vec2f, period: f32) -> vec2f { return i - period * floor(i / period); }\nfn valueNoiseP(p: vec2f, period: f32) -> 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(wrapCell(i, period)), hash2(wrapCell(i + vec2f(1.0, 0.0), period)), u.x),\n mix(hash2(wrapCell(i + vec2f(0.0, 1.0), period)), hash2(wrapCell(i + vec2f(1.0, 1.0), period)), u.x), u.y);\n}\n@fragment fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f {\n let p = pos.xy * (64.0 / 1024.0);\n var v = 0.0;\n var a = 0.5;\n var pp = p;\n var period = 64.0;\n for (var o = 0; o < 4; o++) {\n v += a * valueNoiseP(pp, period);\n pp *= 2.0;\n period *= 2.0;\n a *= 0.5;\n }\n return vec4f(v, 0.0, 0.0, 1.0);\n}\n";
14
30
  //# sourceMappingURL=ground.d.ts.map
@@ -1 +1 @@
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
+ {"version":3,"file":"ground.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground.ts"],"names":[],"mappings":"AAYA,wBAAgB,gBAAgB,IAAI,MAAM,CAkUzC;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"}
@@ -18,7 +18,13 @@ struct GroundShadowMat {
18
18
  fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, gridSpacing: f32,
19
19
  gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, opacity: f32,
20
20
  gridLineColor: vec3f, mirror: f32,
21
- mirrorBlur: f32, _mb0: f32, _mb1: f32, _mb2: f32,
21
+ // farCascade: 1 while a stage is loaded, 0 otherwise. See the branch below —
22
+ // with no stage the far map is never drawn into, so its taps are known.
23
+ mirrorBlur: f32, farCascade: f32, _mb1: f32, _mb2: f32,
24
+ // Every shadow caster in one sphere, refreshed per frame. w = radius; 0 means
25
+ // nothing casts, negative means "do not use this" (a rigid caster has no
26
+ // sphere, so a scene with a stage keeps the taps). See rzShadowPossible.
27
+ casterSphere: vec4f,
22
28
  };
23
29
  // One view-projection per shadow cascade, inner to outer — same buffer and
24
30
  // same order the materials read.
@@ -40,33 +46,12 @@ struct MirrorVP { viewProj: mat4x4f, params: vec4f, };
40
46
  @group(0) @binding(9) var mirrorTex: texture_2d<f32>;
41
47
  @group(0) @binding(10) var linearSampler: sampler;
42
48
  @group(0) @binding(11) var mirrorDepth: texture_depth_multisampled_2d;
49
+ // The frost noise, PRE-BAKED — see GROUND_NOISE_BAKE_WGSL below. Sampled with
50
+ // the repeat sampler already at binding 10.
51
+ @group(0) @binding(12) var noiseTex: texture_2d<f32>;
43
52
  ${WORLD_AMBIENT_WGSL}
44
53
  ${lightsApi(0, 6)}
45
54
 
46
- fn hash2(p: vec2f) -> f32 {
47
- var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
48
- p3 += dot(p3, vec3f(p3.y + 33.33, p3.z + 33.33, p3.x + 33.33));
49
- return fract((p3.x + p3.y) * p3.z);
50
- }
51
- fn valueNoise(p: vec2f) -> f32 {
52
- let i = floor(p);
53
- let f = fract(p);
54
- let u = f * f * (3.0 - 2.0 * f);
55
- return mix(mix(hash2(i), hash2(i + vec2f(1.0, 0.0)), u.x),
56
- mix(hash2(i + vec2f(0.0, 1.0)), hash2(i + vec2f(1.0, 1.0)), u.x), u.y);
57
- }
58
- fn fbmNoise(p: vec2f) -> f32 {
59
- var v = 0.0;
60
- var a = 0.5;
61
- var pp = p;
62
- for (var i = 0; i < 4; i++) {
63
- v += a * valueNoise(pp);
64
- pp *= 2.0;
65
- a *= 0.5;
66
- }
67
- return v;
68
- }
69
-
70
55
  struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };
71
56
  @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {
72
57
  var o: VO; o.worldPos = position; o.normal = normal;
@@ -119,8 +104,63 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
119
104
  // The far cascade's taps run ONLY where the near one is fading or absent
120
105
  // (frustum < 1), so a pixel in the near core costs exactly what it did with
121
106
  // one cascade — and that core is where the camera usually looks.
107
+ // Can anything cast a shadow ONTO this point at all?
108
+ //
109
+ // The ground is vastly larger than what stands on it, and the answer for most
110
+ // of it is no. A point is shadowed only if the ray from it toward the light
111
+ // meets a caster, so testing that ray against one bounding sphere decides in a
112
+ // few ALU ops what nine hardware-bilinear depth comparisons would otherwise be
113
+ // spent discovering. Same pixels: outside the sphere vis stays 1.0, which is
114
+ // exactly what the taps return.
115
+ //
116
+ // Conservative in all three directions that matter. The sphere is a union of
117
+ // per-model bounds built from POSED bones plus the skin margin, so a jump or a
118
+ // flying skirt is inside it. A negative radius disables the test outright. And
119
+ // the halfspace check uses -R rather than 0, so a caster the point is standing
120
+ // inside still counts.
121
+ var shadowPossible = material.casterSphere.w > 0.0;
122
+ if (shadowPossible) {
123
+ let toLight = normalize(-light.lights[0].direction.xyz);
124
+ let toCaster = material.casterSphere.xyz - i.worldPos;
125
+ let along = dot(toCaster, toLight);
126
+ let perp = length(toCaster - toLight * along);
127
+ shadowPossible = along > -material.casterSphere.w && perp <= material.casterSphere.w;
128
+ } else {
129
+ // Negative radius is the opt-out, not "nothing casts".
130
+ shadowPossible = material.casterSphere.w < 0.0;
131
+ }
132
+
122
133
  var vis = 1.0;
123
- if (frustum < 1.0 && frustum1 > 0.0) {
134
+ // The whole cascade lookup, behind the strength dial.
135
+ //
136
+ // This is a FULL-COVERAGE draw and each branch below is nine comparison-sampler
137
+ // taps per pixel — eighteen where the cascades overlap — so on a retina canvas
138
+ // it is tens of millions of shadow-map fetches a frame. All of it was computed
139
+ // and then multiplied by material.shadowStrength, which is ZERO whenever the
140
+ // host turns the ground's shadow catcher off. Turning shadows off in the UI
141
+ // therefore cost exactly what leaving them on cost, which is what a report of
142
+ // "no fps gain from disabling shadows" looks like from the outside.
143
+ //
144
+ // shadowStrength is a uniform, so this branch is uniform across the draw:
145
+ // every invocation takes the same side and the taps are genuinely skipped
146
+ // rather than merely masked. The dark term below is (1.0 - vis) * strength, so
147
+ // with strength at zero the result is identical either way — a pure
148
+ // deletion of work, not a change of look.
149
+ //
150
+ // The same reasoning the noise tint below already got, applied to the term
151
+ // that costs a hundred times more.
152
+ if (material.shadowStrength > 0.0 && shadowPossible) {
153
+ // The far cascade's taps, skipped entirely when nothing ever drew into it.
154
+ //
155
+ // This branch is the expensive one on a wide floor: it runs wherever the NEAR
156
+ // cascade does not reach, and the near cascade is a 64-unit box around the
157
+ // camera target, so a floor receding to the horizon takes it almost
158
+ // everywhere. Nine comparison taps, per pixel, on the largest draw in the
159
+ // frame — against a map that is cleared unless a stage is in the scene.
160
+ //
161
+ // Cleared depth compares as "no occluder", so the skipped path leaves vis at
162
+ // 1.0, which is exactly what the taps returned. Free, not cheaper.
163
+ if (material.farCascade > 0.0 && frustum < 1.0 && frustum1 > 0.0) {
124
164
  let suv1 = vec2f(ndc1.x * 0.5 + 0.5, 0.5 - ndc1.y * 0.5);
125
165
  let suv1_c = clamp(suv1, vec2f(0.02), vec2f(0.98));
126
166
  let st1 = ${1 / SHADOW_CASCADES[SHADOW_CASCADES.length - 1].mapSize} * 2.0;
@@ -150,12 +190,27 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
150
190
  // cascade to cascade rather than snapping to lit mid-floor.
151
191
  vis = mix(vis, acc * (1.0 / 9.0), frustum);
152
192
  }
193
+ }
153
194
 
154
195
  // Frosted/matte micro-texture. Scenes leaving it at zero were still paying
155
196
  // sixteen hash rounds a pixel for a tint of exactly 1.
156
197
  var noiseTint = 1.0;
157
198
  if (material.noiseStrength != 0.0) {
158
- let noiseVal = fbmNoise(i.worldPos.xz * 3.0);
199
+ // One texture sample where four octaves of value noise used to be
200
+ // EVALUATED — sixteen hash rounds per pixel, on the largest draw in the
201
+ // frame. Bisection on the device that was slow pinned the ground's whole
202
+ // cost here: flat colour was smooth, the shader minus its shadow taps was
203
+ // still slow, and the shader minus only this was smooth. (0.33.2 built
204
+ // this same bake and measured no win — the cost then was the PCF. The
205
+ // instrument was right both times; the term changed.)
206
+ //
207
+ // The tile is the SAME fbm, baked once at init over a 64-p-unit period
208
+ // (~21 world units before it repeats — invisible at frost strengths), and
209
+ // the interior of the tile is bit-identical to the old evaluation because
210
+ // only lattice cells on the seam are wrapped. Sampled at level 0 with the
211
+ // repeat sampler: the old evaluation had no filtering either, so distant
212
+ // sparkle is unchanged rather than quietly "improved".
213
+ let noiseVal = textureSampleLevel(noiseTex, linearSampler, i.worldPos.xz * ${3 / 64}, 0.0).r;
159
214
  noiseTint = 1.0 + (noiseVal - 0.5) * material.noiseStrength;
160
215
  }
161
216
 
@@ -288,3 +343,52 @@ ${sceneIdWriteWgsl("out", `${GROUND_MATERIAL_ID}u`, `${GROUND_OBJECT_ID}u`)} re
288
343
  */
289
344
  export const GROUND_MATERIAL_ID = 0xffff;
290
345
  export const GROUND_OBJECT_ID = 0xffff;
346
+ /** Texel edge of the baked frost tile. 1024 over a 64-p-unit period is two
347
+ * texels per finest-octave noise cell — the bake resolves everything the
348
+ * evaluation produced. r8unorm: the value lives in [0,1] and feeds a 0.05
349
+ * tint; eight bits is already 5x below what that tint can show. */
350
+ export const GROUND_NOISE_SIZE = 1024;
351
+ /**
352
+ * The frost fbm as a bake: identical math, periodic lattice.
353
+ *
354
+ * hash2/valueNoise/fbm are the ground's old helpers verbatim, with one change —
355
+ * each octave's lattice wraps at its own period (64 p-units at octave 0,
356
+ * doubling with frequency), so the tile is seamless under the repeat sampler.
357
+ * Wrapping only renames lattice cells on the seam; every interior cell hashes
358
+ * exactly as before, which is what makes the bake a relocation of the old
359
+ * pixels rather than a new look.
360
+ */
361
+ export const GROUND_NOISE_BAKE_WGSL = /* wgsl */ `
362
+ @vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
363
+ let x = f32((vi & 1u) << 2u) - 1.0;
364
+ let y = f32((vi & 2u) << 1u) - 1.0;
365
+ return vec4f(x, y, 0.0, 1.0);
366
+ }
367
+ fn hash2(p: vec2f) -> f32 {
368
+ var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
369
+ p3 += dot(p3, vec3f(p3.y + 33.33, p3.z + 33.33, p3.x + 33.33));
370
+ return fract((p3.x + p3.y) * p3.z);
371
+ }
372
+ fn wrapCell(i: vec2f, period: f32) -> vec2f { return i - period * floor(i / period); }
373
+ fn valueNoiseP(p: vec2f, period: f32) -> f32 {
374
+ let i = floor(p);
375
+ let f = fract(p);
376
+ let u = f * f * (3.0 - 2.0 * f);
377
+ return mix(mix(hash2(wrapCell(i, period)), hash2(wrapCell(i + vec2f(1.0, 0.0), period)), u.x),
378
+ mix(hash2(wrapCell(i + vec2f(0.0, 1.0), period)), hash2(wrapCell(i + vec2f(1.0, 1.0), period)), u.x), u.y);
379
+ }
380
+ @fragment fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f {
381
+ let p = pos.xy * (64.0 / ${GROUND_NOISE_SIZE}.0);
382
+ var v = 0.0;
383
+ var a = 0.5;
384
+ var pp = p;
385
+ var period = 64.0;
386
+ for (var o = 0; o < 4; o++) {
387
+ v += a * valueNoiseP(pp, period);
388
+ pp *= 2.0;
389
+ period *= 2.0;
390
+ a *= 0.5;
391
+ }
392
+ return vec4f(v, 0.0, 0.0, 1.0);
393
+ }
394
+ `;
@@ -1,2 +1,2 @@
1
- export declare const OUTLINE_SHADER_WGSL = "\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n viewPos: vec3f,\n // Render-target height in device pixels (engine writes it each frame);\n // width is recovered via the projection matrix's aspect.\n viewportHeight: f32,\n};\n\nstruct MaterialUniforms {\n edgeColor: vec4f,\n edgeSize: f32,\n _padding1: f32,\n _padding2: f32,\n _padding3: f32,\n};\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var edgeSampler: sampler;\n@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;\n@group(2) @binding(0) var<uniform> material: MaterialUniforms;\n@group(2) @binding(1) var diffuseTexture: texture_2d<f32>;\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n};\n\n@vertex fn vs(\n @location(0) position: vec3f,\n @location(1) normal: vec3f,\n @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>,\n @location(4) weights0: vec4<f32>\n) -> VertexOutput {\n var output: VertexOutput;\n let pos4 = vec4f(position, 1.0);\n\n let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;\n let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);\n let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);\n\n var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);\n var skinnedNrm = vec3f(0.0, 0.0, 0.0);\n for (var i = 0u; i < 4u; i++) {\n let j = joints0[i];\n let w = normalizedWeights[i];\n let m = skinMats[j];\n skinnedPos += (m * pos4) * w;\n let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);\n skinnedNrm += (r3 * normal) * w;\n }\n let worldPos = skinnedPos.xyz;\n let worldNormal = normalize(skinnedNrm);\n\n let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);\n\n // babylon-mmd: screenNormal = normalize((view * worldNormal).xy)\n let viewNormal = (camera.view * vec4f(worldNormal, 0.0)).xyz;\n let snLen = length(viewNormal.xy);\n let screenNormal = select(vec2f(0.0, 0.0), viewNormal.xy / snLen, snLen > 1e-5);\n\n // Reference-height normalization (babylon-mmd ships this variant commented\n // out as `renderHeight = 1080`): thickness is a constant FRACTION of the\n // frame \u2014 2\u00B7edgeSize px at 1080p \u2014 so retina DPR and 4K export don't thin\n // the rims to sub-pixel. Width follows the projection aspect.\n // projection[1][1]/projection[0][0] = width/height for a symmetric frustum.\n let aspect = camera.projection[1][1] / camera.projection[0][0];\n let viewport = vec2f(1080.0 * aspect, 1080.0);\n\n // NDC offset = edgeSize \u00B7 4/viewport, \u00D7w so the perspective divide cancels:\n // constant screen thickness at any distance (babylon-mmd parity).\n let offset = screenNormal * (material.edgeSize * 4.0 / viewport) * clipPos.w;\n output.position = vec4f(clipPos.xy + offset, clipPos.z, clipPos.w);\n output.uv = uv;\n return output;\n}\n\nstruct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };\n@fragment fn fs(input: VertexOutput) -> FSOut {\n // Rim alpha FOLLOWS the fabric's texture alpha instead of a hard alpha test:\n // MMD draws blend-material edges solid (only cutout materials alpha-test), so\n // a 0.4 discard erased the whole hull on semi-transparent cloth \u2014 stockinged\n // legs crossing lost their outline entirely. Modulating instead keeps a\n // proportional rim on sheer weave (never a solid black hull) and still\n // discards true cut-out margins like hair-card borders.\n let texA = textureSample(diffuseTexture, edgeSampler, input.uv).a;\n if (texA < 0.05) {\n discard;\n }\n var out: FSOut;\n out.color = vec4f(material.edgeColor.rgb, material.edgeColor.a * texA);\n out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);\n return out;\n}\n";
1
+ export declare function outlineShaderWgsl(): string;
2
2
  //# sourceMappingURL=outline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,mBAAmB,ksHAgG/B,CAAA"}
1
+ {"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"AAoBA,wBAAgB,iBAAiB,IAAI,MAAM,CAqG1C"}
@@ -11,7 +11,12 @@
11
11
  // MODULATES the rim's alpha by it (discarding only near-zero cut-out
12
12
  // margins) — sheer fabric gets a proportional rim, never a solid black
13
13
  // hull, without dropping the author's edge flag.
14
- export const OUTLINE_SHADER_WGSL = /* wgsl */ `
14
+ // A FUNCTION rather than a constant, for the reason commonFsOutWgsl is one: the
15
+ // fragment outputs depend on whether the device carries the id attachment, and
16
+ // a string baked at import cannot know what the device said.
17
+ import { sceneFsOutWgsl, sceneIdPadWgsl } from "./scene-contract";
18
+ export function outlineShaderWgsl() {
19
+ return /* wgsl */ `
15
20
  struct CameraUniforms {
16
21
  view: mat4x4f,
17
22
  projection: mat4x4f,
@@ -90,7 +95,10 @@ struct VertexOutput {
90
95
  return output;
91
96
  }
92
97
 
93
- struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
98
+ // The id output is declared and padded, never written for real: a hull would
99
+ // overwrite the id of the body it traces, so the pipeline takes the id target at
100
+ // writeMask 0. Declared all the same — see sceneFsOutWgsl.
101
+ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
94
102
  @fragment fn fs(input: VertexOutput) -> FSOut {
95
103
  // Rim alpha FOLLOWS the fabric's texture alpha instead of a hard alpha test:
96
104
  // MMD draws blend-material edges solid (only cutout materials alpha-test), so
@@ -105,6 +113,7 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
105
113
  var out: FSOut;
106
114
  out.color = vec4f(material.edgeColor.rgb, material.edgeColor.a * texA);
107
115
  out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
108
- return out;
116
+ ${sceneIdPadWgsl("out")} return out;
109
117
  }
110
118
  `;
119
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AAwBA,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;AAiCD,qDAAqD;AACrD,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,CAAA;AAEzC,KAAK,cAAc,GAAG;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAkDjC,oDAAoD;AACpD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAOpE;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wDAAwD;AACxD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAE9D;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAMlG;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA0CxF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6GvF"}
1
+ {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AAyBA,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;AAiCD,qDAAqD;AACrD,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,CAAA;AAEzC,KAAK,cAAc,GAAG;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAkDjC,oDAAoD;AACpD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAOpE;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wDAAwD;AACxD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAE9D;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAMlG;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA0CxF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAgHvF"}
@@ -5,6 +5,7 @@ import { anchorAliasWgsl } from "../anchor-table";
5
5
  import { midiApi } from "../midi-api";
6
6
  import { CAST_API } from "../cast-api";
7
7
  import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
8
+ import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract";
8
9
  /**
9
10
  * The trail accessors, in the PARTICLE module.
10
11
  *
@@ -247,7 +248,10 @@ struct FSOut {
247
248
  // rejected outright — "reading alpha but it is missing from fragment output".
248
249
  // The material shaders declare vec4f here for the same reason.
249
250
  @location(1) mask: vec4f,
250
- }
251
+ // Declared whenever the pass carries the attachment, and padded rather than
252
+ // written: a particle is not a thing you would address by id, so its pipeline
253
+ // takes this target at writeMask 0. Declared anyway — see sceneFsOutWgsl.
254
+ ${sceneIdFieldWgsl()}}
251
255
 
252
256
  @fragment
253
257
  fn fs(in: VSOut) -> FSOut {
@@ -273,7 +277,7 @@ fn fs(in: VSOut) -> FSOut {
273
277
  // not geometry, was the banding that survived every geometry fix.
274
278
  let mg = select(vec2f(select(0.0, 1.0, BLOOM), 1.0), vec2f(select(0.0, c.a, BLOOM), c.a), ADDITIVE);
275
279
  out.mask = vec4f(mg.x, mg.y, 0.0, c.a);
276
- return out;
280
+ ${sceneIdPadWgsl("out")} return out;
277
281
  }
278
282
  `);
279
283
  }
@@ -105,12 +105,19 @@ export declare function sceneColorFormats(formats: SceneFormats): GPUTextureForm
105
105
  * author. The struct and the targets above have to agree on count and order,
106
106
  * and they now disagree in one file rather than in five.
107
107
  *
108
- * Only the shaders that will GAIN an output take this today — the materials
109
- * (hand-written and graph-generated alike, through COMMON_FS_OUT_WGSL) and the
110
- * ground. Outline, particles and ribbons keep their own declarations on
111
- * purpose: they never write the id, so they would take an `id: false` argument
112
- * forever, and their structs carry comments about their own blend that belong
113
- * where they are.
108
+ * EVERY scene-pass shader takes this — outline, particles, ribbons and the
109
+ * depth prepass included, none of which write a meaningful id (and the prepass
110
+ * no meaningful colour either). They did not, once. The reasoning was that a
111
+ * target at writeMask 0 needs no matching output: true of Dawn, and the reading
112
+ * of gpuweb#1918 this file used to assert. It is not a reading every browser
113
+ * shares, and the failure mode when a browser disagrees is the worst kind —
114
+ * createRenderPipeline does not throw, it returns a pipeline that is already
115
+ * invalid, and the pass that binds it is dropped whole. Missing geometry, clean
116
+ * console, and a bug that reproduces on one vendor's browser only.
117
+ *
118
+ * So the rule is now the strict one, and it costs nothing to hold: declare
119
+ * every output the pass has attachments for, and let writeMask decide what is
120
+ * kept. A shader with no id to write pads it — see sceneIdPadWgsl.
114
121
  */
115
122
  export declare function sceneFsOutWgsl(opts?: {
116
123
  name?: string;
@@ -124,5 +131,30 @@ export declare function sceneFsOutWgsl(opts?: {
124
131
  * ask the device what it supports.
125
132
  */
126
133
  export declare function sceneIdWriteWgsl(out: string, material: string, object: string): string;
134
+ /**
135
+ * Just the id FIELD, for a shader that keeps its own struct.
136
+ *
137
+ * Particles and ribbons declare their outputs by hand because the comments on
138
+ * their aux field are about their own blend and belong beside it — taking the
139
+ * whole struct from sceneFsOutWgsl would move that prose away from what it
140
+ * explains. They still need the id output when the pass has the attachment, so
141
+ * they splice this in and pad it with sceneIdPadWgsl.
142
+ */
143
+ export declare function sceneIdFieldWgsl(): string;
144
+ /**
145
+ * The id assignment for a shader that has no id to give.
146
+ *
147
+ * The counterpart to the strict rule on sceneFsOutWgsl: outline hulls,
148
+ * particles, ribbons and the depth prepass all declare the output because the
149
+ * pass has the attachment, and all of them take writeMask 0, so what they
150
+ * assign is never stored. Zero, which is the reserved "nothing" id the pass
151
+ * clears to — so if one of these ever did reach the target, it would read as
152
+ * absence rather than as a plausible wrong answer pointing at material 0.
153
+ *
154
+ * A separate function from sceneIdWriteWgsl rather than a call to it with "0u"
155
+ * twice, because the two say different things: that one records an identity,
156
+ * this one satisfies a declaration.
157
+ */
158
+ export declare function sceneIdPadWgsl(out: string): string;
127
159
  export {};
128
160
  //# sourceMappingURL=scene-contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scene-contract.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/scene-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;uDAEuD;AACvD,MAAM,MAAM,YAAY,GAAG;IACzB,+CAA+C;IAC/C,GAAG,EAAE,gBAAgB,CAAA;IACrB,iEAAiE;IACjE,GAAG,EAAE,gBAAgB,CAAA;CACtB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,gBAA6B,CAAA;AAkB3D,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAE3C;AAED,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED;;;;;;GAMG;AACH,KAAK,gBAAgB;AACnB,2DAA2D;AACzD,UAAU;AACZ;;+CAE+C;GAC7C,QAAQ;AACV,uEAAuE;GACrE,SAAS;AACX,iEAAiE;GAC/D,UAAU;AACZ;;;mDAGmD;GACjD,mBAAmB;AACrB;;;oCAGoC;GAClC,OAAO;AACT;;;kDAGkD;GAChD,eAAe,CAAA;AAwEnB;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,gBAAgB,EAAE,OAAO,EAAE,YAAY,GAAG,mBAAmB,EAAE,CAqBhG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAE3E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAmB7E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtF"}
1
+ {"version":3,"file":"scene-contract.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/scene-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;uDAEuD;AACvD,MAAM,MAAM,YAAY,GAAG;IACzB,+CAA+C;IAC/C,GAAG,EAAE,gBAAgB,CAAA;IACrB,iEAAiE;IACjE,GAAG,EAAE,gBAAgB,CAAA;CACtB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,gBAA6B,CAAA;AAkB3D,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAE3C;AAED,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED;;;;;;GAMG;AACH,KAAK,gBAAgB;AACnB,2DAA2D;AACzD,UAAU;AACZ;;+CAE+C;GAC7C,QAAQ;AACV,uEAAuE;GACrE,SAAS;AACX,iEAAiE;GAC/D,UAAU;AACZ;;;mDAGmD;GACjD,mBAAmB;AACrB;;;oCAGoC;GAClC,OAAO;AACT;;;kDAGkD;GAChD,eAAe,CAAA;AA0EnB;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,gBAAgB,EAAE,OAAO,EAAE,YAAY,GAAG,mBAAmB,EAAE,CAoBhG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAE3E;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAAC,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAmB7E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD"}