reze-engine 0.50.2 → 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 (42) hide show
  1. package/dist/engine.d.ts +294 -7
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +900 -109
  4. package/dist/shaders/cast-api.d.ts +1 -1
  5. package/dist/shaders/cast-api.d.ts.map +1 -1
  6. package/dist/shaders/cast-layout.d.ts +44 -1
  7. package/dist/shaders/cast-layout.d.ts.map +1 -1
  8. package/dist/shaders/cast-layout.js +44 -1
  9. package/dist/shaders/materials/common.d.ts.map +1 -1
  10. package/dist/shaders/materials/common.js +7 -1
  11. package/dist/shaders/materials/nodes.d.ts +1 -1
  12. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  13. package/dist/shaders/materials/nodes.js +17 -9
  14. package/dist/shaders/passes/composite.d.ts +1 -1
  15. package/dist/shaders/passes/composite.d.ts.map +1 -1
  16. package/dist/shaders/passes/depth-prepass.d.ts +1 -1
  17. package/dist/shaders/passes/depth-prepass.d.ts.map +1 -1
  18. package/dist/shaders/passes/depth-prepass.js +52 -12
  19. package/dist/shaders/passes/ground.d.ts +16 -0
  20. package/dist/shaders/passes/ground.d.ts.map +1 -1
  21. package/dist/shaders/passes/ground.js +143 -49
  22. package/dist/shaders/passes/outline.d.ts +1 -1
  23. package/dist/shaders/passes/outline.d.ts.map +1 -1
  24. package/dist/shaders/passes/outline.js +12 -3
  25. package/dist/shaders/passes/particles.d.ts.map +1 -1
  26. package/dist/shaders/passes/particles.js +6 -2
  27. package/dist/shaders/passes/scene-contract.d.ts +38 -6
  28. package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
  29. package/dist/shaders/passes/scene-contract.js +53 -16
  30. package/dist/shaders/passes/trails.d.ts.map +1 -1
  31. package/dist/shaders/passes/trails.js +36 -8
  32. package/package.json +2 -2
  33. package/src/engine.ts +952 -100
  34. package/src/shaders/cast-layout.ts +44 -1
  35. package/src/shaders/materials/common.ts +7 -1
  36. package/src/shaders/materials/nodes.ts +17 -9
  37. package/src/shaders/passes/depth-prepass.ts +53 -12
  38. package/src/shaders/passes/ground.ts +145 -49
  39. package/src/shaders/passes/outline.ts +14 -3
  40. package/src/shaders/passes/particles.ts +6 -2
  41. package/src/shaders/passes/scene-contract.ts +55 -16
  42. package/src/shaders/passes/trails.ts +36 -8
@@ -1,16 +1,34 @@
1
- // Depth-only prepass for the TRANSPARENT bucket. Transparent color draws keep
2
- // depth write OFF so self-overlapping sheer cloth blends both layers instead of
3
- // a triangle-order patchworkbut that leaves no depth record, so anything
4
- // drawn later (the outline hulls) shows straight through the fabric as black
5
- // shapes. This pass re-draws the transparent geometry depth-only AFTER its
6
- // color pass: solid-enough texels (alpha 0.5) write depth, so outlines get
7
- // occluded behind fabric exactly like they are behind opaque cloth, while
8
- // truly sheer texels (a veil) stay non-occluding.
1
+ // The depth-only prime, one module behind four pipelines.
2
+ //
3
+ // The oldest fps complaint the engine had zoom close and the frame drops, in
4
+ // every material generation was per-fragment shading TIMES layering: an MMD
5
+ // model at close-up is cloth over body over face under hair, author-order drawn,
6
+ // every buried layer fully shaded and then covered. This module lets depth go
7
+ // down FIRST so the colour passes shade each pixel once:
8
+ //
9
+ // · opaque prepass (CUTOFF 0.5) — plain auto-class opaque draws, before the
10
+ // opaque colour walk. See drawOpaqueDepthPrepass for who is in and why.
11
+ // · hair prime (CUTOFF 1.0, stencil not-equal) — between the non-hair and
12
+ // hair colour walks, fenced off the eye silhouette the see-through-hair
13
+ // stencil pass needs. See drawHairDepthPrime.
14
+ // · transparent solid prime (CUTOFF 1.0) — a translucent material's alpha-1
15
+ // texels, where over-blending is plain replacement and the buried work
16
+ // provably never shows. See drawTransparentSolidPrepass.
17
+ // · transparent depth prepass (CUTOFF 0.5, AFTER colour) — the original
18
+ // occupant, dormant: re-records sheer fabric's depth so outline hulls are
19
+ // occluded behind it. Kept for a future OIT path.
9
20
  //
10
21
  // Reuses mainPipelineLayout: camera g0b0, diffuseSampler g0b2, skinMats g1b0,
11
22
  // diffuse texture g2b0, material uniforms g2b1 — the same bind groups the
12
23
  // color draws already set, so drawing it costs no extra binding work.
13
- export const TRANSPARENT_DEPTH_PREPASS_WGSL = /* wgsl */ `
24
+ //
25
+ // A FUNCTION rather than the constant it was, for the reason commonFsOutWgsl is
26
+ // one: the fragment outputs below depend on whether the device carries the id
27
+ // attachment, and that answer does not exist at import time. The constant could
28
+ // not have taken the outputs at all, which is most of why it did not have them.
29
+ import { sceneFsOutWgsl, sceneIdPadWgsl } from "./scene-contract";
30
+ export function transparentDepthPrepassWgsl() {
31
+ return /* wgsl */ `
14
32
  struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
15
33
  struct MaterialUniforms {
16
34
  diffuseColor: vec3f,
@@ -24,7 +42,9 @@ struct MaterialUniforms {
24
42
  @group(2) @binding(1) var<uniform> material: MaterialUniforms;
25
43
 
26
44
  struct VSOut {
27
- @builtin(position) position: vec4f,
45
+ // @invariant, to the same end as the material VertexOutput: the colour pass
46
+ // must land on exactly the depths this wrote.
47
+ @builtin(position) @invariant position: vec4f,
28
48
  @location(0) uv: vec2f,
29
49
  };
30
50
 
@@ -49,8 +69,28 @@ struct VSOut {
49
69
  return o;
50
70
  }
51
71
 
52
- @fragment fn fs(in: VSOut) {
72
+ // Every attachment the pass carries, declared and then not written: the
73
+ // pipeline takes all of them at writeMask 0 (see sceneTargets), so what this
74
+ // returns is discarded by the hardware and only the depth write survives — which
75
+ // is the entire purpose of the pass. Declaring them anyway is what keeps the
76
+ // pipeline valid on a browser that requires an output per target rather than
77
+ // per WRITTEN target. Costs one dead struct store on a fragment that already
78
+ // runs, because the alpha test below needs it to.
79
+ ${sceneFsOutWgsl({ name: "PrepassOut", aux: "mask" })}
80
+ // The cutout threshold, per pipeline. 0.5 is the OPAQUE prime's "solid enough"
81
+ // — safe there because opaque colour replaces rather than blends. The
82
+ // TRANSPARENT prime overrides it to 1.0: a translucent fragment's blend reads
83
+ // what is behind it, so only a texel at EXACTLY alpha 1 — where over-blending
84
+ // collapses to plain replacement and the destination stops mattering — may
85
+ // claim depth ahead of its buried layers without changing the pixel.
86
+ override CUTOFF: f32 = 0.5;
87
+ @fragment fn fs(in: VSOut) -> PrepassOut {
53
88
  let a = material.alpha * textureSample(diffuseTexture, diffuseSampler, in.uv).a;
54
- if (a < 0.5) { discard; }
89
+ if (a < CUTOFF) { discard; }
90
+ var out: PrepassOut;
91
+ out.color = vec4f(0.0);
92
+ out.mask = vec4f(0.0);
93
+ ${sceneIdPadWgsl("out")} return out;
55
94
  }
56
95
  `;
96
+ }
@@ -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,CAqRzC;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
 
@@ -184,8 +239,8 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
184
239
  // reflection was rendered by the MIRROR camera, so projecting this
185
240
  // fragment's world position through that camera yields exactly the texel
186
241
  // where its reflection landed — projective mapping, no screen-space guess.
187
- // There is deliberately NO strength dial, and no dependence on the floor's
188
- // opacity either: mirror is on or off, and when it is on it SHOWS — the same
242
+ // There is deliberately NO strength dial: mirror is on or off, and how much
243
+ // of it shows is the SURFACE's own opacity covering it — the same
189
244
  // independence the grid won earlier. The branch is on a uniform, so the
190
245
  // whole cost vanishes for the scenes that leave it off.
191
246
  var reflShadowed = vec3f(0.0);
@@ -248,28 +303,18 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
248
303
  // and a catcher stacked on top would darken the same shadow twice.
249
304
  let catchA = dark * 0.65 * edgeFade * (1.0 - material.opacity) * (1.0 - material.mirror);
250
305
 
251
- // FOUR LAYERS, premultiplied, bottom to top: received shadow, ground
252
- // surface, reflection, grid. Each with its own coverage, none scaling
253
- // another.
254
- //
255
- // THE MIRROR SITS ABOVE THE SURFACE, and that ordering is the whole rule.
256
- // Underneath it, the floor's own opacity covered the reflection — and did it
257
- // inconsistently, because BOTH layers carry the radial edgeFade: at opacity
258
- // 1 the surface stopped covering wherever the plane fades out, so the
259
- // reflection returned along the rim and read as a bug that came and went
260
- // with the camera angle. What a mirror shows cannot depend on how solid the
261
- // thing it is made of happens to be.
262
- //
263
- // The catcher's order against it no longer matters — catchA already carries
264
- // (1 - mirror), so the two are never both present.
265
- var pm = vec3f(0.0);
266
- var cov = catchA;
267
- // Surface over the received shadow.
306
+ // FOUR LAYERS, premultiplied, bottom to top: reflection, received shadow,
307
+ // ground surface, grid. Each with its own coverage, none scaling another —
308
+ // ground opacity REVEALS the mirror rather than the mirror carrying a
309
+ // strength of its own.
310
+ var pm = reflShadowed * mirrA;
311
+ var cov = mirrA;
312
+ // Shadow catcher over the reflection-or-nothing: black, it covers.
313
+ pm = pm * (1.0 - catchA);
314
+ cov = catchA + cov * (1.0 - catchA);
315
+ // Surface over shadow.
268
316
  pm = baseColor * surfA + pm * (1.0 - surfA);
269
317
  cov = surfA + cov * (1.0 - surfA);
270
- // The reflection over the surface, at full strength whatever the opacity.
271
- pm = reflShadowed * mirrA + pm * (1.0 - mirrA);
272
- cov = mirrA + cov * (1.0 - mirrA);
273
318
  // Grid over surface. Unshadowed, as it has always been — a painted line reads
274
319
  // as paint, and darkening it was never what the mix did either.
275
320
  pm = material.gridLineColor * gridA + pm * (1.0 - gridA);
@@ -298,3 +343,52 @@ ${sceneIdWriteWgsl("out", `${GROUND_MATERIAL_ID}u`, `${GROUND_OBJECT_ID}u`)} re
298
343
  */
299
344
  export const GROUND_MATERIAL_ID = 0xffff;
300
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"}
@@ -89,12 +89,14 @@ const ADD_PREMULTIPLIED = {
89
89
  alpha: { srcFactor: "zero", dstFactor: "one", operation: "add" },
90
90
  };
91
91
  /**
92
- * Which classes actually WRITE an id, and so gain a fragment output for it.
92
+ * Which classes write a MEANINGFUL id.
93
93
  *
94
- * Everything else keeps its shader exactly as it is and takes the id target at
95
- * writeMask 0 legal, specified, and free. The alternative (leaving the target
96
- * off those pipelines) is not available: every pipeline in a pass must agree
97
- * with the pass's attachments.
94
+ * Not the same question as which shaders declare the output every scene-pass
95
+ * shader declares all three now (see sceneFsOutWgsl). This set decides only
96
+ * whose id survives: writeMask 0xf here, writeMask 0 for everyone else, so the
97
+ * others compute an id nothing stores. Leaving the target off those pipelines
98
+ * is not available: every pipeline in a pass must agree with the pass's
99
+ * attachments.
98
100
  *
99
101
  * The ground is in because a mark placed by id needs the floor to have one.
100
102
  * Transparent fabric writes ids too — dissolving a dress needs the dress's own
@@ -127,11 +129,10 @@ const BLENDS = {
127
129
  */
128
130
  export function sceneTargets(cls, formats) {
129
131
  const targets = cls === "depth-prepass"
130
- ? // Format only, and writeMask 0. Note the asymmetry this leans on, which
131
- // is the same one the id target leans on below: a target the shader has
132
- // no output for is legal at writeMask 0 (gpuweb#1918), while an output
133
- // with no target is NOT governed (gpuweb#5341). This is the specified
134
- // direction, and it is the only one this file ever uses.
132
+ ? // Format only, and writeMask 0 the pipeline writes no colour. It still
133
+ // DECLARES every output, which is the part that used to be missing; see
134
+ // the note on sceneFsOutWgsl about why writeMask 0 is not a licence to
135
+ // leave the output off.
135
136
  [
136
137
  { format: formats.hdr, writeMask: 0 },
137
138
  { format: formats.aux, writeMask: 0 },
@@ -168,12 +169,19 @@ export function sceneColorFormats(formats) {
168
169
  * author. The struct and the targets above have to agree on count and order,
169
170
  * and they now disagree in one file rather than in five.
170
171
  *
171
- * Only the shaders that will GAIN an output take this today — the materials
172
- * (hand-written and graph-generated alike, through COMMON_FS_OUT_WGSL) and the
173
- * ground. Outline, particles and ribbons keep their own declarations on
174
- * purpose: they never write the id, so they would take an `id: false` argument
175
- * forever, and their structs carry comments about their own blend that belong
176
- * where they are.
172
+ * EVERY scene-pass shader takes this outline, particles, ribbons and the
173
+ * depth prepass included, none of which write a meaningful id (and the prepass
174
+ * no meaningful colour either). They did not, once. The reasoning was that a
175
+ * target at writeMask 0 needs no matching output: true of Dawn, and the reading
176
+ * of gpuweb#1918 this file used to assert. It is not a reading every browser
177
+ * shares, and the failure mode when a browser disagrees is the worst kind —
178
+ * createRenderPipeline does not throw, it returns a pipeline that is already
179
+ * invalid, and the pass that binds it is dropped whole. Missing geometry, clean
180
+ * console, and a bug that reproduces on one vendor's browser only.
181
+ *
182
+ * So the rule is now the strict one, and it costs nothing to hold: declare
183
+ * every output the pass has attachments for, and let writeMask decide what is
184
+ * kept. A shader with no id to write pads it — see sceneIdPadWgsl.
177
185
  */
178
186
  export function sceneFsOutWgsl(opts) {
179
187
  const name = opts?.name ?? "FSOut";
@@ -205,3 +213,32 @@ ${id}};
205
213
  export function sceneIdWriteWgsl(out, material, object) {
206
214
  return mrtIds ? ` ${out}.id = vec2u(${material}, ${object});\n` : "";
207
215
  }
216
+ /**
217
+ * Just the id FIELD, for a shader that keeps its own struct.
218
+ *
219
+ * Particles and ribbons declare their outputs by hand because the comments on
220
+ * their aux field are about their own blend and belong beside it — taking the
221
+ * whole struct from sceneFsOutWgsl would move that prose away from what it
222
+ * explains. They still need the id output when the pass has the attachment, so
223
+ * they splice this in and pad it with sceneIdPadWgsl.
224
+ */
225
+ export function sceneIdFieldWgsl() {
226
+ return mrtIds ? ` @location(2) id: vec2u,\n` : "";
227
+ }
228
+ /**
229
+ * The id assignment for a shader that has no id to give.
230
+ *
231
+ * The counterpart to the strict rule on sceneFsOutWgsl: outline hulls,
232
+ * particles, ribbons and the depth prepass all declare the output because the
233
+ * pass has the attachment, and all of them take writeMask 0, so what they
234
+ * assign is never stored. Zero, which is the reserved "nothing" id the pass
235
+ * clears to — so if one of these ever did reach the target, it would read as
236
+ * absence rather than as a plausible wrong answer pointing at material 0.
237
+ *
238
+ * A separate function from sceneIdWriteWgsl rather than a call to it with "0u"
239
+ * twice, because the two say different things: that one records an identity,
240
+ * this one satisfies a declaration.
241
+ */
242
+ export function sceneIdPadWgsl(out) {
243
+ return mrtIds ? ` ${out}.id = vec2u(0u, 0u);\n` : "";
244
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AAwBA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CA6SR"}
1
+ {"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AAyBA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CAwUR"}