reze-engine 0.50.3 → 0.50.5

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 (105) hide show
  1. package/dist/engine.d.ts +247 -2
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +822 -64
  4. package/dist/model.d.ts +49 -1
  5. package/dist/model.d.ts.map +1 -1
  6. package/dist/model.js +160 -4
  7. package/dist/physics/autofit.d.ts +147 -0
  8. package/dist/physics/autofit.d.ts.map +1 -0
  9. package/dist/physics/autofit.js +501 -0
  10. package/dist/physics/physics.d.ts +35 -0
  11. package/dist/physics/physics.d.ts.map +1 -1
  12. package/dist/physics/physics.js +64 -0
  13. package/dist/physics/world.d.ts +4 -0
  14. package/dist/physics/world.d.ts.map +1 -1
  15. package/dist/physics/world.js +6 -0
  16. package/dist/shaders/cast-api.d.ts +1 -1
  17. package/dist/shaders/cast-api.d.ts.map +1 -1
  18. package/dist/shaders/cast-layout.d.ts +44 -1
  19. package/dist/shaders/cast-layout.d.ts.map +1 -1
  20. package/dist/shaders/cast-layout.js +44 -1
  21. package/dist/shaders/materials/common.d.ts.map +1 -1
  22. package/dist/shaders/materials/common.js +7 -1
  23. package/dist/shaders/materials/nodes.d.ts +1 -1
  24. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  25. package/dist/shaders/materials/nodes.js +17 -9
  26. package/dist/shaders/passes/composite.d.ts +1 -1
  27. package/dist/shaders/passes/composite.d.ts.map +1 -1
  28. package/dist/shaders/passes/depth-prepass.d.ts +1 -1
  29. package/dist/shaders/passes/depth-prepass.d.ts.map +1 -1
  30. package/dist/shaders/passes/depth-prepass.js +52 -12
  31. package/dist/shaders/passes/field-blit.d.ts +26 -0
  32. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  33. package/dist/shaders/passes/field-blit.js +65 -0
  34. package/dist/shaders/passes/ground-noise.d.ts +7 -0
  35. package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
  36. package/dist/shaders/passes/ground-noise.js +88 -0
  37. package/dist/shaders/passes/ground.d.ts +16 -0
  38. package/dist/shaders/passes/ground.d.ts.map +1 -1
  39. package/dist/shaders/passes/ground.js +131 -27
  40. package/dist/shaders/passes/outline.d.ts +1 -1
  41. package/dist/shaders/passes/outline.d.ts.map +1 -1
  42. package/dist/shaders/passes/outline.js +12 -3
  43. package/dist/shaders/passes/particles.d.ts.map +1 -1
  44. package/dist/shaders/passes/particles.js +6 -2
  45. package/dist/shaders/passes/scene-contract.d.ts +38 -6
  46. package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
  47. package/dist/shaders/passes/scene-contract.js +53 -16
  48. package/dist/shaders/passes/sim.d.ts +34 -0
  49. package/dist/shaders/passes/sim.d.ts.map +1 -0
  50. package/dist/shaders/passes/sim.js +169 -0
  51. package/dist/shaders/passes/trails.d.ts.map +1 -1
  52. package/dist/shaders/passes/trails.js +36 -8
  53. package/dist/shaders/score-api.d.ts +10 -0
  54. package/dist/shaders/score-api.d.ts.map +1 -0
  55. package/dist/shaders/score-api.js +114 -0
  56. package/package.json +1 -1
  57. package/src/engine.ts +867 -56
  58. package/src/model.ts +163 -4
  59. package/src/physics/physics.ts +63 -0
  60. package/src/physics/world.ts +7 -0
  61. package/src/shaders/cast-layout.ts +44 -1
  62. package/src/shaders/materials/common.ts +7 -1
  63. package/src/shaders/materials/nodes.ts +17 -9
  64. package/src/shaders/passes/depth-prepass.ts +53 -12
  65. package/src/shaders/passes/ground.ts +133 -27
  66. package/src/shaders/passes/outline.ts +14 -3
  67. package/src/shaders/passes/particles.ts +6 -2
  68. package/src/shaders/passes/scene-contract.ts +55 -16
  69. package/src/shaders/passes/trails.ts +36 -8
  70. package/dist/physics-debug.d.ts +0 -30
  71. package/dist/physics-debug.d.ts.map +0 -1
  72. package/dist/physics-debug.js +0 -526
  73. package/dist/shaders/materials/body.d.ts +0 -2
  74. package/dist/shaders/materials/body.d.ts.map +0 -1
  75. package/dist/shaders/materials/body.js +0 -95
  76. package/dist/shaders/materials/cloth_rough.d.ts +0 -2
  77. package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
  78. package/dist/shaders/materials/cloth_rough.js +0 -69
  79. package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
  80. package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
  81. package/dist/shaders/materials/cloth_smooth.js +0 -61
  82. package/dist/shaders/materials/default.d.ts +0 -2
  83. package/dist/shaders/materials/default.d.ts.map +0 -1
  84. package/dist/shaders/materials/default.js +0 -43
  85. package/dist/shaders/materials/eye.d.ts +0 -2
  86. package/dist/shaders/materials/eye.d.ts.map +0 -1
  87. package/dist/shaders/materials/eye.js +0 -60
  88. package/dist/shaders/materials/face.d.ts +0 -2
  89. package/dist/shaders/materials/face.d.ts.map +0 -1
  90. package/dist/shaders/materials/face.js +0 -95
  91. package/dist/shaders/materials/hair.d.ts +0 -2
  92. package/dist/shaders/materials/hair.d.ts.map +0 -1
  93. package/dist/shaders/materials/hair.js +0 -90
  94. package/dist/shaders/materials/metal.d.ts +0 -2
  95. package/dist/shaders/materials/metal.d.ts.map +0 -1
  96. package/dist/shaders/materials/metal.js +0 -77
  97. package/dist/shaders/materials/mmd_classic.d.ts +0 -2
  98. package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
  99. package/dist/shaders/materials/mmd_classic.js +0 -66
  100. package/dist/shaders/materials/stockings.d.ts +0 -2
  101. package/dist/shaders/materials/stockings.d.ts.map +0 -1
  102. package/dist/shaders/materials/stockings.js +0 -122
  103. package/dist/shaders/passes/physics-debug.d.ts +0 -2
  104. package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
  105. package/dist/shaders/passes/physics-debug.js +0 -69
@@ -20,7 +20,13 @@ struct GroundShadowMat {
20
20
  fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, gridSpacing: f32,
21
21
  gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, opacity: f32,
22
22
  gridLineColor: vec3f, mirror: f32,
23
- mirrorBlur: f32, _mb0: f32, _mb1: f32, _mb2: f32,
23
+ // farCascade: 1 while a stage is loaded, 0 otherwise. See the branch below —
24
+ // with no stage the far map is never drawn into, so its taps are known.
25
+ mirrorBlur: f32, farCascade: f32, _mb1: f32, _mb2: f32,
26
+ // Every shadow caster in one sphere, refreshed per frame. w = radius; 0 means
27
+ // nothing casts, negative means "do not use this" (a rigid caster has no
28
+ // sphere, so a scene with a stage keeps the taps). See rzShadowPossible.
29
+ casterSphere: vec4f,
24
30
  };
25
31
  // One view-projection per shadow cascade, inner to outer — same buffer and
26
32
  // same order the materials read.
@@ -42,33 +48,12 @@ struct MirrorVP { viewProj: mat4x4f, params: vec4f, };
42
48
  @group(0) @binding(9) var mirrorTex: texture_2d<f32>;
43
49
  @group(0) @binding(10) var linearSampler: sampler;
44
50
  @group(0) @binding(11) var mirrorDepth: texture_depth_multisampled_2d;
51
+ // The frost noise, PRE-BAKED — see GROUND_NOISE_BAKE_WGSL below. Sampled with
52
+ // the repeat sampler already at binding 10.
53
+ @group(0) @binding(12) var noiseTex: texture_2d<f32>;
45
54
  ${WORLD_AMBIENT_WGSL}
46
55
  ${lightsApi(0, 6)}
47
56
 
48
- fn hash2(p: vec2f) -> f32 {
49
- var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
50
- p3 += dot(p3, vec3f(p3.y + 33.33, p3.z + 33.33, p3.x + 33.33));
51
- return fract((p3.x + p3.y) * p3.z);
52
- }
53
- fn valueNoise(p: vec2f) -> f32 {
54
- let i = floor(p);
55
- let f = fract(p);
56
- let u = f * f * (3.0 - 2.0 * f);
57
- return mix(mix(hash2(i), hash2(i + vec2f(1.0, 0.0)), u.x),
58
- mix(hash2(i + vec2f(0.0, 1.0)), hash2(i + vec2f(1.0, 1.0)), u.x), u.y);
59
- }
60
- fn fbmNoise(p: vec2f) -> f32 {
61
- var v = 0.0;
62
- var a = 0.5;
63
- var pp = p;
64
- for (var i = 0; i < 4; i++) {
65
- v += a * valueNoise(pp);
66
- pp *= 2.0;
67
- a *= 0.5;
68
- }
69
- return v;
70
- }
71
-
72
57
  struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };
73
58
  @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {
74
59
  var o: VO; o.worldPos = position; o.normal = normal;
@@ -121,8 +106,63 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
121
106
  // The far cascade's taps run ONLY where the near one is fading or absent
122
107
  // (frustum < 1), so a pixel in the near core costs exactly what it did with
123
108
  // one cascade — and that core is where the camera usually looks.
109
+ // Can anything cast a shadow ONTO this point at all?
110
+ //
111
+ // The ground is vastly larger than what stands on it, and the answer for most
112
+ // of it is no. A point is shadowed only if the ray from it toward the light
113
+ // meets a caster, so testing that ray against one bounding sphere decides in a
114
+ // few ALU ops what nine hardware-bilinear depth comparisons would otherwise be
115
+ // spent discovering. Same pixels: outside the sphere vis stays 1.0, which is
116
+ // exactly what the taps return.
117
+ //
118
+ // Conservative in all three directions that matter. The sphere is a union of
119
+ // per-model bounds built from POSED bones plus the skin margin, so a jump or a
120
+ // flying skirt is inside it. A negative radius disables the test outright. And
121
+ // the halfspace check uses -R rather than 0, so a caster the point is standing
122
+ // inside still counts.
123
+ var shadowPossible = material.casterSphere.w > 0.0;
124
+ if (shadowPossible) {
125
+ let toLight = normalize(-light.lights[0].direction.xyz);
126
+ let toCaster = material.casterSphere.xyz - i.worldPos;
127
+ let along = dot(toCaster, toLight);
128
+ let perp = length(toCaster - toLight * along);
129
+ shadowPossible = along > -material.casterSphere.w && perp <= material.casterSphere.w;
130
+ } else {
131
+ // Negative radius is the opt-out, not "nothing casts".
132
+ shadowPossible = material.casterSphere.w < 0.0;
133
+ }
134
+
124
135
  var vis = 1.0;
125
- if (frustum < 1.0 && frustum1 > 0.0) {
136
+ // The whole cascade lookup, behind the strength dial.
137
+ //
138
+ // This is a FULL-COVERAGE draw and each branch below is nine comparison-sampler
139
+ // taps per pixel — eighteen where the cascades overlap — so on a retina canvas
140
+ // it is tens of millions of shadow-map fetches a frame. All of it was computed
141
+ // and then multiplied by material.shadowStrength, which is ZERO whenever the
142
+ // host turns the ground's shadow catcher off. Turning shadows off in the UI
143
+ // therefore cost exactly what leaving them on cost, which is what a report of
144
+ // "no fps gain from disabling shadows" looks like from the outside.
145
+ //
146
+ // shadowStrength is a uniform, so this branch is uniform across the draw:
147
+ // every invocation takes the same side and the taps are genuinely skipped
148
+ // rather than merely masked. The dark term below is (1.0 - vis) * strength, so
149
+ // with strength at zero the result is identical either way — a pure
150
+ // deletion of work, not a change of look.
151
+ //
152
+ // The same reasoning the noise tint below already got, applied to the term
153
+ // that costs a hundred times more.
154
+ if (material.shadowStrength > 0.0 && shadowPossible) {
155
+ // The far cascade's taps, skipped entirely when nothing ever drew into it.
156
+ //
157
+ // This branch is the expensive one on a wide floor: it runs wherever the NEAR
158
+ // cascade does not reach, and the near cascade is a 64-unit box around the
159
+ // camera target, so a floor receding to the horizon takes it almost
160
+ // everywhere. Nine comparison taps, per pixel, on the largest draw in the
161
+ // frame — against a map that is cleared unless a stage is in the scene.
162
+ //
163
+ // Cleared depth compares as "no occluder", so the skipped path leaves vis at
164
+ // 1.0, which is exactly what the taps returned. Free, not cheaper.
165
+ if (material.farCascade > 0.0 && frustum < 1.0 && frustum1 > 0.0) {
126
166
  let suv1 = vec2f(ndc1.x * 0.5 + 0.5, 0.5 - ndc1.y * 0.5);
127
167
  let suv1_c = clamp(suv1, vec2f(0.02), vec2f(0.98));
128
168
  let st1 = ${1 / SHADOW_CASCADES[SHADOW_CASCADES.length - 1].mapSize} * 2.0;
@@ -152,12 +192,27 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
152
192
  // cascade to cascade rather than snapping to lit mid-floor.
153
193
  vis = mix(vis, acc * (1.0 / 9.0), frustum);
154
194
  }
195
+ }
155
196
 
156
197
  // Frosted/matte micro-texture. Scenes leaving it at zero were still paying
157
198
  // sixteen hash rounds a pixel for a tint of exactly 1.
158
199
  var noiseTint = 1.0;
159
200
  if (material.noiseStrength != 0.0) {
160
- let noiseVal = fbmNoise(i.worldPos.xz * 3.0);
201
+ // One texture sample where four octaves of value noise used to be
202
+ // EVALUATED — sixteen hash rounds per pixel, on the largest draw in the
203
+ // frame. Bisection on the device that was slow pinned the ground's whole
204
+ // cost here: flat colour was smooth, the shader minus its shadow taps was
205
+ // still slow, and the shader minus only this was smooth. (0.33.2 built
206
+ // this same bake and measured no win — the cost then was the PCF. The
207
+ // instrument was right both times; the term changed.)
208
+ //
209
+ // The tile is the SAME fbm, baked once at init over a 64-p-unit period
210
+ // (~21 world units before it repeats — invisible at frost strengths), and
211
+ // the interior of the tile is bit-identical to the old evaluation because
212
+ // only lattice cells on the seam are wrapped. Sampled at level 0 with the
213
+ // repeat sampler: the old evaluation had no filtering either, so distant
214
+ // sparkle is unchanged rather than quietly "improved".
215
+ let noiseVal = textureSampleLevel(noiseTex, linearSampler, i.worldPos.xz * ${3 / 64}, 0.0).r;
161
216
  noiseTint = 1.0 + (noiseVal - 0.5) * material.noiseStrength;
162
217
  }
163
218
 
@@ -291,3 +346,54 @@ ${sceneIdWriteWgsl("out", `${GROUND_MATERIAL_ID}u`, `${GROUND_OBJECT_ID}u`)} re
291
346
  */
292
347
  export const GROUND_MATERIAL_ID = 0xffff
293
348
  export const GROUND_OBJECT_ID = 0xffff
349
+
350
+ /** Texel edge of the baked frost tile. 1024 over a 64-p-unit period is two
351
+ * texels per finest-octave noise cell — the bake resolves everything the
352
+ * evaluation produced. r8unorm: the value lives in [0,1] and feeds a 0.05
353
+ * tint; eight bits is already 5x below what that tint can show. */
354
+ export const GROUND_NOISE_SIZE = 1024
355
+
356
+ /**
357
+ * The frost fbm as a bake: identical math, periodic lattice.
358
+ *
359
+ * hash2/valueNoise/fbm are the ground's old helpers verbatim, with one change —
360
+ * each octave's lattice wraps at its own period (64 p-units at octave 0,
361
+ * doubling with frequency), so the tile is seamless under the repeat sampler.
362
+ * Wrapping only renames lattice cells on the seam; every interior cell hashes
363
+ * exactly as before, which is what makes the bake a relocation of the old
364
+ * pixels rather than a new look.
365
+ */
366
+ export const GROUND_NOISE_BAKE_WGSL = /* wgsl */ `
367
+ @vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
368
+ let x = f32((vi & 1u) << 2u) - 1.0;
369
+ let y = f32((vi & 2u) << 1u) - 1.0;
370
+ return vec4f(x, y, 0.0, 1.0);
371
+ }
372
+ fn hash2(p: vec2f) -> f32 {
373
+ var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
374
+ p3 += dot(p3, vec3f(p3.y + 33.33, p3.z + 33.33, p3.x + 33.33));
375
+ return fract((p3.x + p3.y) * p3.z);
376
+ }
377
+ fn wrapCell(i: vec2f, period: f32) -> vec2f { return i - period * floor(i / period); }
378
+ fn valueNoiseP(p: vec2f, period: f32) -> f32 {
379
+ let i = floor(p);
380
+ let f = fract(p);
381
+ let u = f * f * (3.0 - 2.0 * f);
382
+ return mix(mix(hash2(wrapCell(i, period)), hash2(wrapCell(i + vec2f(1.0, 0.0), period)), u.x),
383
+ mix(hash2(wrapCell(i + vec2f(0.0, 1.0), period)), hash2(wrapCell(i + vec2f(1.0, 1.0), period)), u.x), u.y);
384
+ }
385
+ @fragment fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f {
386
+ let p = pos.xy * (64.0 / ${GROUND_NOISE_SIZE}.0);
387
+ var v = 0.0;
388
+ var a = 0.5;
389
+ var pp = p;
390
+ var period = 64.0;
391
+ for (var o = 0; o < 4; o++) {
392
+ v += a * valueNoiseP(pp, period);
393
+ pp *= 2.0;
394
+ period *= 2.0;
395
+ a *= 0.5;
396
+ }
397
+ return vec4f(v, 0.0, 0.0, 1.0);
398
+ }
399
+ `
@@ -12,7 +12,14 @@
12
12
  // margins) — sheer fabric gets a proportional rim, never a solid black
13
13
  // hull, without dropping the author's edge flag.
14
14
 
15
- export const OUTLINE_SHADER_WGSL = /* wgsl */ `
15
+ // A FUNCTION rather than a constant, for the reason commonFsOutWgsl is one: the
16
+ // fragment outputs depend on whether the device carries the id attachment, and
17
+ // a string baked at import cannot know what the device said.
18
+
19
+ import { sceneFsOutWgsl, sceneIdPadWgsl } from "./scene-contract"
20
+
21
+ export function outlineShaderWgsl(): string {
22
+ return /* wgsl */ `
16
23
  struct CameraUniforms {
17
24
  view: mat4x4f,
18
25
  projection: mat4x4f,
@@ -91,7 +98,10 @@ struct VertexOutput {
91
98
  return output;
92
99
  }
93
100
 
94
- struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
101
+ // The id output is declared and padded, never written for real: a hull would
102
+ // overwrite the id of the body it traces, so the pipeline takes the id target at
103
+ // writeMask 0. Declared all the same — see sceneFsOutWgsl.
104
+ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
95
105
  @fragment fn fs(input: VertexOutput) -> FSOut {
96
106
  // Rim alpha FOLLOWS the fabric's texture alpha instead of a hard alpha test:
97
107
  // MMD draws blend-material edges solid (only cutout materials alpha-test), so
@@ -106,6 +116,7 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
106
116
  var out: FSOut;
107
117
  out.color = vec4f(material.edgeColor.rgb, material.edgeColor.a * texA);
108
118
  out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
109
- return out;
119
+ ${sceneIdPadWgsl("out")} return out;
110
120
  }
111
121
  `
122
+ }
@@ -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
  // GPU particles for user effects: a compute step and an instanced quad draw.
9
10
  //
10
11
  // Its own shader MODULE rather than more source spliced into composite.ts, for
@@ -307,7 +308,10 @@ struct FSOut {
307
308
  // rejected outright — "reading alpha but it is missing from fragment output".
308
309
  // The material shaders declare vec4f here for the same reason.
309
310
  @location(1) mask: vec4f,
310
- }
311
+ // Declared whenever the pass carries the attachment, and padded rather than
312
+ // written: a particle is not a thing you would address by id, so its pipeline
313
+ // takes this target at writeMask 0. Declared anyway — see sceneFsOutWgsl.
314
+ ${sceneIdFieldWgsl()}}
311
315
 
312
316
  @fragment
313
317
  fn fs(in: VSOut) -> FSOut {
@@ -333,7 +337,7 @@ fn fs(in: VSOut) -> FSOut {
333
337
  // not geometry, was the banding that survived every geometry fix.
334
338
  let mg = select(vec2f(select(0.0, 1.0, BLOOM), 1.0), vec2f(select(0.0, c.a, BLOOM), c.a), ADDITIVE);
335
339
  out.mask = vec4f(mg.x, mg.y, 0.0, c.a);
336
- return out;
340
+ ${sceneIdPadWgsl("out")} return out;
337
341
  }
338
342
  `
339
343
  )
@@ -143,12 +143,14 @@ const ADD_PREMULTIPLIED: GPUBlendState = {
143
143
  }
144
144
 
145
145
  /**
146
- * Which classes actually WRITE an id, and so gain a fragment output for it.
146
+ * Which classes write a MEANINGFUL id.
147
147
  *
148
- * Everything else keeps its shader exactly as it is and takes the id target at
149
- * writeMask 0 — legal, specified, and free. The alternative (leaving the target
150
- * off those pipelines) is not available: every pipeline in a pass must agree
151
- * with the pass's attachments.
148
+ * Not the same question as which shaders declare the output — every scene-pass
149
+ * shader declares all three now (see sceneFsOutWgsl). This set decides only
150
+ * whose id survives: writeMask 0xf here, writeMask 0 for everyone else, so the
151
+ * others compute an id nothing stores. Leaving the target off those pipelines
152
+ * is not available: every pipeline in a pass must agree with the pass's
153
+ * attachments.
152
154
  *
153
155
  * The ground is in because a mark placed by id needs the floor to have one.
154
156
  * Transparent fabric writes ids too — dissolving a dress needs the dress's own
@@ -184,11 +186,10 @@ const BLENDS: Record<Exclude<SceneRenderClass, "depth-prepass">, [GPUBlendState,
184
186
  export function sceneTargets(cls: SceneRenderClass, formats: SceneFormats): GPUColorTargetState[] {
185
187
  const targets: GPUColorTargetState[] =
186
188
  cls === "depth-prepass"
187
- ? // Format only, and writeMask 0. Note the asymmetry this leans on, which
188
- // is the same one the id target leans on below: a target the shader has
189
- // no output for is legal at writeMask 0 (gpuweb#1918), while an output
190
- // with no target is NOT governed (gpuweb#5341). This is the specified
191
- // direction, and it is the only one this file ever uses.
189
+ ? // Format only, and writeMask 0 — the pipeline writes no colour. It still
190
+ // DECLARES every output, which is the part that used to be missing; see
191
+ // the note on sceneFsOutWgsl about why writeMask 0 is not a licence to
192
+ // leave the output off.
192
193
  [
193
194
  { format: formats.hdr, writeMask: 0 },
194
195
  { format: formats.aux, writeMask: 0 },
@@ -226,12 +227,19 @@ export function sceneColorFormats(formats: SceneFormats): GPUTextureFormat[] {
226
227
  * author. The struct and the targets above have to agree on count and order,
227
228
  * and they now disagree in one file rather than in five.
228
229
  *
229
- * Only the shaders that will GAIN an output take this today — the materials
230
- * (hand-written and graph-generated alike, through COMMON_FS_OUT_WGSL) and the
231
- * ground. Outline, particles and ribbons keep their own declarations on
232
- * purpose: they never write the id, so they would take an `id: false` argument
233
- * forever, and their structs carry comments about their own blend that belong
234
- * where they are.
230
+ * EVERY scene-pass shader takes this — outline, particles, ribbons and the
231
+ * depth prepass included, none of which write a meaningful id (and the prepass
232
+ * no meaningful colour either). They did not, once. The reasoning was that a
233
+ * target at writeMask 0 needs no matching output: true of Dawn, and the reading
234
+ * of gpuweb#1918 this file used to assert. It is not a reading every browser
235
+ * shares, and the failure mode when a browser disagrees is the worst kind —
236
+ * createRenderPipeline does not throw, it returns a pipeline that is already
237
+ * invalid, and the pass that binds it is dropped whole. Missing geometry, clean
238
+ * console, and a bug that reproduces on one vendor's browser only.
239
+ *
240
+ * So the rule is now the strict one, and it costs nothing to hold: declare
241
+ * every output the pass has attachments for, and let writeMask decide what is
242
+ * kept. A shader with no id to write pads it — see sceneIdPadWgsl.
235
243
  */
236
244
  export function sceneFsOutWgsl(opts?: { name?: string; aux?: string }): string {
237
245
  const name = opts?.name ?? "FSOut"
@@ -264,3 +272,34 @@ ${id}};
264
272
  export function sceneIdWriteWgsl(out: string, material: string, object: string): string {
265
273
  return mrtIds ? ` ${out}.id = vec2u(${material}, ${object});\n` : ""
266
274
  }
275
+
276
+ /**
277
+ * Just the id FIELD, for a shader that keeps its own struct.
278
+ *
279
+ * Particles and ribbons declare their outputs by hand because the comments on
280
+ * their aux field are about their own blend and belong beside it — taking the
281
+ * whole struct from sceneFsOutWgsl would move that prose away from what it
282
+ * explains. They still need the id output when the pass has the attachment, so
283
+ * they splice this in and pad it with sceneIdPadWgsl.
284
+ */
285
+ export function sceneIdFieldWgsl(): string {
286
+ return mrtIds ? ` @location(2) id: vec2u,\n` : ""
287
+ }
288
+
289
+ /**
290
+ * The id assignment for a shader that has no id to give.
291
+ *
292
+ * The counterpart to the strict rule on sceneFsOutWgsl: outline hulls,
293
+ * particles, ribbons and the depth prepass all declare the output because the
294
+ * pass has the attachment, and all of them take writeMask 0, so what they
295
+ * assign is never stored. Zero, which is the reserved "nothing" id the pass
296
+ * clears to — so if one of these ever did reach the target, it would read as
297
+ * absence rather than as a plausible wrong answer pointing at material 0.
298
+ *
299
+ * A separate function from sceneIdWriteWgsl rather than a call to it with "0u"
300
+ * twice, because the two say different things: that one records an identity,
301
+ * this one satisfies a declaration.
302
+ */
303
+ export function sceneIdPadWgsl(out: string): string {
304
+ return mrtIds ? ` ${out}.id = vec2u(0u, 0u);\n` : ""
305
+ }
@@ -5,6 +5,7 @@ import { anchorAliasWgsl, ribbonSlotWgsl } 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
  // Ribbons along a bone's recorded path, drawn as geometry.
9
10
  //
10
11
  // This is the effect the whole geometry path was built for. As a fullscreen
@@ -123,7 +124,14 @@ struct CameraU {
123
124
  }
124
125
  struct TrailU {
125
126
  time: f32,
126
- _pad0: f32,
127
+ // How many subjects the cast ACTUALLY holds this frame, not the four the
128
+ // layout has room for. The instance count is sized by this on the CPU and
129
+ // decoded by it here, and the two must use the same number or the flattened
130
+ // [ribbon][subject][segment] index lands on the wrong ribbon. It is a uniform
131
+ // rather than the RZ_SUBJECTS constant precisely because it changes when a
132
+ // model is added or removed, and recompiling every trail shader for that
133
+ // would be absurd.
134
+ subjects: f32,
127
135
  _pad1: f32,
128
136
  _pad2: f32,
129
137
  }
@@ -244,11 +252,14 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
244
252
  let rest = ii / subs;
245
253
  let seg = k / SUB;
246
254
  let sub = k % SUB;
247
- let subject = i32(rest % u32(RZ_SUBJECTS));
255
+ // The LIVE subject count, floored at one so a cast that is momentarily empty
256
+ // divides by something. See TrailU.subjects.
257
+ let live = max(1u, u32(tu.subjects));
258
+ let subject = i32(rest % live);
248
259
  // Instance index counts RIBBONS; the cast buffer is addressed by anchor slot.
249
260
  // One ribbon per trailed anchor, so these are the same number only when every
250
261
  // anchor is trailed — _rzRibbonSlot is what bridges them.
251
- let ribbon = i32(rest / u32(RZ_SUBJECTS));
262
+ let ribbon = i32(rest / live);
252
263
  let slot = _rzRibbonSlot(ribbon);
253
264
 
254
265
  let n = rzTrailCount(subject, slot);
@@ -342,9 +353,23 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
342
353
  if (dot(perpA, perpB) < 0.0) { perpB = -perpB; }
343
354
  let clipP = select(clipA, clipB, atEnd);
344
355
  let perp = select(perpA, perpB, atEnd);
345
- // trailWidth speaks PIXELS, like the original's constants; rzViewportHeight()
346
- // lets the effect scale them to the frame.
347
- let wPx = max(0.0, trailWidth(u, age));
356
+ // trailWidth speaks WORLD UNITS — the model's units, not the screen's.
357
+ //
358
+ // It spoke pixels, like the fullscreen original's constants, and that made
359
+ // the ribbon a screen ornament: zoom out and it grew relative to the hand
360
+ // that drew it, zoom in and it thinned to a thread. A ribbon is a thing in
361
+ // the scene, so its width belongs in the scene's units and should foreshorten
362
+ // like everything else.
363
+ //
364
+ // The EXTRUSION stays in 2D pixel space untouched — every 3D-extrusion
365
+ // artefact this ribbon ever had (fold-over wedges, bowties, twisted quads) is
366
+ // recorded above, and none of it comes back. Only the width's SOURCE changes:
367
+ // a world length L perpendicular to view at depth w projects to
368
+ // L * proj[1][1] / w in NDC y, so half the frame height converts it to the
369
+ // pixels the 2D offset wants. Per vertex, so a ribbon receding along its own
370
+ // length tapers with distance exactly as geometry would.
371
+ let wWorld = max(0.0, trailWidth(u, age));
372
+ let wPx = wWorld * cam.proj[1][1] * (H * 0.5) / max(clipP.w, 0.01);
348
373
  let ndc = clipP.xy / clipP.w + perp * (c.y * wPx) / toPx;
349
374
  out.clip = vec4f(ndc * clipP.w, clipP.z, clipP.w);
350
375
  out.slot = u32(slot);
@@ -364,7 +389,10 @@ struct TrailFSOut {
364
389
  // The scene's aux target: (bloom mask, coverage). Writing mask 1 is what
365
390
  // makes a ribbon BLOOM — it is light, and the gate is what says so.
366
391
  @location(1) aux: vec4f,
367
- }
392
+ // Declared whenever the pass carries the attachment, and padded rather than
393
+ // written: a ribbon is not a thing you would address by id, so its pipeline
394
+ // takes this target at writeMask 0. Declared anyway — see sceneFsOutWgsl.
395
+ ${sceneIdFieldWgsl()}}
368
396
 
369
397
  @fragment
370
398
  fn fs(in: VSOut) -> TrailFSOut {
@@ -383,7 +411,7 @@ fn fs(in: VSOut) -> TrailFSOut {
383
411
  // mapper is what keeps the sum from clipping.
384
412
  o.color = vec4f(c.rgb, c.a);
385
413
  o.aux = vec4f(${src.bloom ? "1.0" : "0.0"}, 1.0, 0.0, c.a);
386
- return o;
414
+ ${sceneIdPadWgsl("o")} return o;
387
415
  }
388
416
  `
389
417
  )
@@ -1,30 +0,0 @@
1
- import type { RezePhysics } from "./physics";
2
- export declare class PhysicsDebugRenderer {
3
- private device;
4
- private bindGroup;
5
- private wirePipelineSphere;
6
- private wirePipelineBox;
7
- private wirePipelineCapsule;
8
- private wireSphereBuffer;
9
- private wireBoxBuffer;
10
- private wireCapsuleBuffer;
11
- private wireSphereCount;
12
- private wireBoxCount;
13
- private wireCapsuleCount;
14
- private solidPipelineSphere;
15
- private solidPipelineBox;
16
- private solidPipelineCapsule;
17
- private solidSphereBuffer;
18
- private solidBoxBuffer;
19
- private solidCapsuleBuffer;
20
- private solidSphereCount;
21
- private solidBoxCount;
22
- private solidCapsuleCount;
23
- private instanceBuffer;
24
- private instanceData;
25
- private instanceCapacity;
26
- constructor(device: GPUDevice, cameraUniformBuffer: GPUBuffer, presentationFormat: GPUTextureFormat);
27
- render(pass: GPURenderPassEncoder, physics: RezePhysics): void;
28
- destroy(): void;
29
- }
30
- //# sourceMappingURL=physics-debug.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"physics-debug.d.ts","sourceRoot":"","sources":["../src/physics-debug.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAmB5C,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,SAAS,CAAc;IAG/B,OAAO,CAAC,kBAAkB,CAAmB;IAC7C,OAAO,CAAC,eAAe,CAAmB;IAC1C,OAAO,CAAC,mBAAmB,CAAmB;IAC9C,OAAO,CAAC,gBAAgB,CAAW;IACnC,OAAO,CAAC,aAAa,CAAW;IAChC,OAAO,CAAC,iBAAiB,CAAW;IACpC,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,gBAAgB,CAAQ;IAIhC,OAAO,CAAC,mBAAmB,CAAmB;IAC9C,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,oBAAoB,CAAmB;IAC/C,OAAO,CAAC,iBAAiB,CAAW;IACpC,OAAO,CAAC,cAAc,CAAW;IACjC,OAAO,CAAC,kBAAkB,CAAW;IACrC,OAAO,CAAC,gBAAgB,CAAQ;IAChC,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,iBAAiB,CAAQ;IAEjC,OAAO,CAAC,cAAc,CAAW;IACjC,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,gBAAgB,CAAQ;gBAG9B,MAAM,EAAE,SAAS,EACjB,mBAAmB,EAAE,SAAS,EAC9B,kBAAkB,EAAE,gBAAgB;IA2HtC,MAAM,CAAC,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IAmI9D,OAAO,IAAI,IAAI;CAShB"}