reze-engine 0.56.8 → 0.56.10

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.
@@ -1 +1 @@
1
- {"version":3,"file":"cast-distance.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/cast-distance.ts"],"names":[],"mappings":"AA0CA;4EAC4E;AAC5E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;kEAEkE;AAClE,eAAO,MAAM,gBAAgB,EAAE,gBAA8B,CAAA;AAC7D;;8DAE8D;AAC9D,eAAO,MAAM,gBAAgB,EAAE,gBAA6B,CAAA;AAE5D;uDACuD;AACvD,eAAO,MAAM,oBAAoB,EAAE,gBAA4B,CAAA;AAE/D,iFAAiF;AACjF,eAAO,MAAM,cAAc,IAAI,CAAA;AAgB/B;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAyE3D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CA4B5C;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CA4B/C;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAmC/F;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAIzC"}
1
+ {"version":3,"file":"cast-distance.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/cast-distance.ts"],"names":[],"mappings":"AA0CA;4EAC4E;AAC5E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;kEAEkE;AAClE,eAAO,MAAM,gBAAgB,EAAE,gBAA8B,CAAA;AAC7D;;8DAE8D;AAC9D,eAAO,MAAM,gBAAgB,EAAE,gBAA6B,CAAA;AAE5D;uDACuD;AACvD,eAAO,MAAM,oBAAoB,EAAE,gBAA4B,CAAA;AAE/D,iFAAiF;AACjF,eAAO,MAAM,cAAc,IAAI,CAAA;AAgB/B;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAqF3D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CA4B5C;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CA4B/C;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAmC/F;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAIzC"}
@@ -74,7 +74,11 @@ fn vs(@builtin(vertex_index) i: u32) -> VSOut {
74
74
  *
75
75
  * The ground, a stage and a media plane all write ids exactly as she does, so
76
76
  * seeding "anything drawn" would grow a border around the floor: a rectangle
77
- * round the frame rather than a sticker. Only the subject ids seed.
77
+ * round the frame rather than a sticker. The subject ids seed, and so do the
78
+ * props': a sword in her hand is part of her silhouette. A prop left out is a
79
+ * HOLE in the seeds wherever it covers her, and the field grows into it from
80
+ * her side, so the border ran round the inside of the prop instead of the
81
+ * outside.
78
82
  */
79
83
  export function buildCastSeedShader(samples) {
80
84
  return (
@@ -85,6 +89,9 @@ const RZ_ID_SAMPLES: i32 = ${samples};
85
89
 
86
90
  @group(0) @binding(0) var _rzIdTex: texture_multisampled_2d<u32>;
87
91
  @group(0) @binding(1) var<storage, read> _rzCast: array<vec4f>;
92
+ /** The visible props' object ids, count first. Props are not subjects, so the
93
+ * cast buffer does not carry them. */
94
+ @group(0) @binding(2) var<storage, read> _rzSeedProps: array<u32>;
88
95
 
89
96
  // The two cast accessors this pass needs, spelled out rather than pulled in.
90
97
  // CAST_API brings subjects, trails, anchors and their aliases with it, and a
@@ -100,6 +107,17 @@ fn rzSubjectId(i: i32) -> u32 {
100
107
  if (i < 0 || i >= rzSubjectCount()) { return 0u; }
101
108
  return u32(_rzCast[i * ${EFFECT_SUBJECT_VEC4S} + 1].w);
102
109
  }
110
+ /** Does this object seed the field: a subject, or a prop. */
111
+ fn rzSeeds(o: u32, subjects: i32) -> bool {
112
+ for (var i = 0; i < subjects; i++) {
113
+ if (o == rzSubjectId(i)) { return true; }
114
+ }
115
+ let props = min(_rzSeedProps[0], arrayLength(&_rzSeedProps) - 1u);
116
+ for (var j = 1u; j <= props; j++) {
117
+ if (o == _rzSeedProps[j]) { return true; }
118
+ }
119
+ return false;
120
+ }
103
121
 
104
122
  ${FULLSCREEN_VS}
105
123
 
@@ -133,9 +151,7 @@ fn fs(@builtin(position) pos: vec4f) -> SeedOut {
133
151
  for (var sample = 0; sample < RZ_ID_SAMPLES; sample++) {
134
152
  let o = textureLoad(_rzIdTex, p, sample).y;
135
153
  if (o == 0u) { continue; }
136
- for (var i = 0; i < n; i++) {
137
- if (o == rzSubjectId(i)) { covered += 1.0; break; }
138
- }
154
+ if (rzSeeds(o, n)) { covered += 1.0; }
139
155
  }
140
156
  var out: SeedOut;
141
157
  out.coverage = covered / f32(RZ_ID_SAMPLES);
@@ -254,7 +270,7 @@ export function castDistanceApi(group, tex, samp, scale) {
254
270
  *
255
271
  * There is no ceiling. A pixel on the far side of the frame gets the real
256
272
  * number. The ground, a stage and a media plane are not the cast and do not
257
- * seed it.
273
+ * seed it. A prop does: what she holds is inside her border.
258
274
  *
259
275
  * Screen pixels whatever the field's own resolution is, so an author writes the
260
276
  * width they mean and never has to know how this is built. uv is the effect's
@@ -1 +1 @@
1
- {"version":3,"file":"depth-prepass.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/depth-prepass.ts"],"names":[],"mappings":"AAgCA,wBAAgB,2BAA2B,IAAI,MAAM,CAsFpD"}
1
+ {"version":3,"file":"depth-prepass.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/depth-prepass.ts"],"names":[],"mappings":"AAgCA,wBAAgB,2BAA2B,IAAI,MAAM,CA0FpD"}
@@ -60,6 +60,9 @@ struct VSOut {
60
60
  // The bind-pose position, carried for one reason: the dissolve test below has
61
61
  // to be the SAME test the colour pass runs, and that one is in object space.
62
62
  @location(1) restPos: vec3f,
63
+ /** The triangle's own threshold, flat — see the material VertexOutput. This
64
+ * pass must throw away exactly the faces the colour pass does. */
65
+ @location(2) @interpolate(flat) faceT: f32,
63
66
  };
64
67
 
65
68
  @vertex fn vs(
@@ -81,6 +84,7 @@ struct VSOut {
81
84
  o.position = camera.projection * camera.view * vec4f(skinned.xyz, 1.0);
82
85
  o.uv = uv;
83
86
  o.restPos = position;
87
+ o.faceT = rz_dissolve_threshold(position);
84
88
  return o;
85
89
  }
86
90
 
@@ -107,7 +111,7 @@ override CUTOFF: f32 = 0.5;
107
111
  // the colour pass throws away would punch holes that occlude the floor behind
108
112
  // her: you would see sky through her, which is the failure this line exists
109
113
  // to prevent.
110
- if (material.dissolve < 0.9995 && rz_dissolve_threshold(in.restPos) > material.dissolve) { discard; }
114
+ if (material.dissolve < 0.9995 && in.faceT > material.dissolve) { discard; }
111
115
  var out: PrepassOut;
112
116
  out.color = vec4f(0.0);
113
117
  out.mask = vec4f(0.0);
@@ -1 +1 @@
1
- {"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"AAqBA;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAE5C,wBAAgB,iBAAiB,IAAI,MAAM,CAgI1C"}
1
+ {"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"AAqBA;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAE5C,wBAAgB,iBAAiB,IAAI,MAAM,CAoI1C"}
@@ -66,6 +66,9 @@ struct VertexOutput {
66
66
  * the depth prepass measure against, so all three agree about which flakes
67
67
  * are gone. */
68
68
  @location(1) restPos: vec3f,
69
+ /** The triangle's own threshold, flat — see the material VertexOutput. The
70
+ * hull traces the body's faces, so it has to lose the same ones. */
71
+ @location(2) @interpolate(flat) faceT: f32,
69
72
  };
70
73
 
71
74
  @vertex fn vs(
@@ -116,6 +119,7 @@ struct VertexOutput {
116
119
  output.position = vec4f(clipPos.xy + offset, clipPos.z, clipPos.w);
117
120
  output.uv = uv;
118
121
  output.restPos = position;
122
+ output.faceT = rz_dissolve_threshold(position);
119
123
  return output;
120
124
  }
121
125
 
@@ -141,7 +145,7 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
141
145
  // The same test the colour pass and the depth prepass run, against the same
142
146
  // bind-pose position — three passes, one rule, or they disagree about which
143
147
  // pieces are still there.
144
- if (material.dissolve < 0.9995 && rz_dissolve_threshold(input.restPos) > material.dissolve) { discard; }
148
+ if (material.dissolve < 0.9995 && input.faceT > material.dissolve) { discard; }
145
149
  let texA = textureSample(diffuseTexture, edgeSampler, input.uv).a;
146
150
  if (texA < 0.05) {
147
151
  discard;
@@ -1,2 +1,2 @@
1
- export declare const SHADOW_DEPTH_SHADER_WGSL = "\n\n/** Clump size, in object-space units \u2014 MMD's are roughly centimetres. */\nconst RZ_DISSOLVE_GRAIN: f32 = 0.42;\n/** How much the clumps pull a fragment off its own grit value \u2014 the whole of\n * the LOOK, and none of the rate. See rz_dissolve_threshold. */\nconst RZ_DISSOLVE_CLUMP: f32 = 0.6;\n/** Grit cell, as a fraction of a clump. See rz_dissolve_threshold: this is what\n * keeps a material SMALLER than a clump from having one threshold for all of\n * it, and it has to be smaller than the SMALLEST material a model carries \u2014\n * an eye highlight, a button, a buckle \u2014 not merely small. Two and a half\n * millimetres on a normal model, which is under all of them. */\nconst RZ_DISSOLVE_GRIT: f32 = 0.06;\n/** How wide the glowing front is, in threshold units. Thin: the front is a\n * rim, and a wide one lights whole limbs at once. */\nconst RZ_DISSOLVE_EDGE: f32 = 0.11;\n/** What the front burns with. Emission, added after lighting and after the\n * graph \u2014 a colour that ramps or takes a shadow reads as paint, not as heat.\n * Above 1 on purpose: it is meant to reach the bloom pyramid. Blue-violet,\n * the same light the motes leaving her are drawn and lit with. */\nconst RZ_BURN_COLOR: vec3f = vec3f(0.45, 0.62, 2.40);\n\nfn rz_dissolve_hash(p: vec3f) -> f32 {\n // INTEGER, for exactly the reason _hash33 in nodes.ts is, and its comment\n // says it: above 24 bits an f32 loses the low ones. The float hash this\n // replaces multiplied three terms into the hundreds of millions and took\n // fract() of the product \u2014 which past 2^24 is not noise, it is a quantised\n // staircase, and wherever that staircase landed on zero the threshold was\n // zero. A surface whose threshold is zero never crosses it: it stays behind\n // while the rest of her leaves, and it sits inside the burning front the\n // whole time she is gone. Those were the white pieces that would not go.\n //\n // Floored first, unlike _hash33: vec3i() truncates toward zero, so -0.5 and\n // +0.5 are the same cell, and half this model is at negative x.\n var h = vec3u(vec3i(floor(p)) + vec3i(32768));\n h = h * vec3u(1664525u, 1013904223u, 2654435761u);\n h = (h.yzx ^ h) * vec3u(2246822519u, 3266489917u, 668265263u);\n h = h ^ (h >> vec3u(16u));\n return f32((h.x ^ h.y ^ h.z) & 16777215u) * (1.0 / 16777216.0);\n}\n\n/** Value noise over that hash \u2014 smooth within a flake, uncorrelated between. */\nfn rz_dissolve_field(p: vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let c000 = rz_dissolve_hash(i);\n let c100 = rz_dissolve_hash(i + vec3f(1.0, 0.0, 0.0));\n let c010 = rz_dissolve_hash(i + vec3f(0.0, 1.0, 0.0));\n let c110 = rz_dissolve_hash(i + vec3f(1.0, 1.0, 0.0));\n let c001 = rz_dissolve_hash(i + vec3f(0.0, 0.0, 1.0));\n let c101 = rz_dissolve_hash(i + vec3f(1.0, 0.0, 1.0));\n let c011 = rz_dissolve_hash(i + vec3f(0.0, 1.0, 1.0));\n let c111 = rz_dissolve_hash(i + vec3f(1.0, 1.0, 1.0));\n let x00 = mix(c000, c100, u.x);\n let x10 = mix(c010, c110, u.x);\n let x01 = mix(c001, c101, u.x);\n let x11 = mix(c011, c111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\n/**\n * The threshold this fragment is measured against.\n *\n * NOISE ALONE, so the whole body comes apart at once \u2014 a hand, a shoulder and\n * an ankle all thinning together rather than a line travelling up her. It was\n * tilted by height at first, on the argument that a sweep reads as something\n * happening TO her; what it actually reads as is a wipe, and a wipe has a\n * direction, an edge and a speed the rest of the effect knows nothing about.\n * Everywhere at once is also the honest match for what leaves: the motes are\n * shed from every limb in the same breath.\n *\n * TWO SCALES. Clumps alone meant every fragment of a material SMALLER than one\n * cell shared a single threshold: an eye highlight, a brow, a button, a hair\n * clip. All of it sat inside the glowing front at once, so the piece flashed\n * white as a whole and then popped \u2014 the two things a disintegration must never\n * do. The grit is a per-cell hash at a fraction of the clump, so a part a few\n * millimetres across still comes apart in pieces.\n *\n * THE GRIT IS THE BASE, though, and the clumps only push a fragment off it.\n * That ordering is what makes the dissolve smooth, and it is not obvious: the\n * two used to be WEIGHTED AND ADDED, and a sum is not uniformly distributed\n * however uniform each half is. It piles up in the middle, so most of the\n * surface had a threshold near a half \u2014 all of it went as the dissolve swept\n * through the middle, and what remained was the thin tail near zero. That tail\n * was the second stage everyone could see: a body that mostly vanished, then a\n * scatter of glowing scraps, then nothing.\n *\n * WRAPPED, not added. Offsetting a uniform value and taking fract() leaves it\n * EXACTLY uniform, where scaling a sum back into range only flattens the middle\n * \u2014 measured over four hundred thousand samples, the sum removed 12% of her per\n * step at the halfway mark and 0.1% at each end (a 110x swing), the scaled sum\n * still swung 89x, and the wrap is flat: 5.0% per step from the first flake to\n * the last, a ratio of 1.0.\n *\n * That flatness IS the smoothness. An uneven rate is what produced the two\n * stages you could see \u2014 a body that mostly vanished at once, then a scatter of\n * scraps that took as long again to follow.\n *\n * The clumping survives the wrap: neighbouring fragments share a clump value,\n * so the offset moves them together. What wraps past 1 comes back at 0 and goes\n * at the other end instead, which costs a little speckle in a region and buys\n * an even rate everywhere.\n */\nfn rz_dissolve_threshold(restPos: vec3f) -> f32 {\n let grit = rz_dissolve_hash(floor(restPos / (RZ_DISSOLVE_GRAIN * RZ_DISSOLVE_GRIT)));\n let clump = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN) - 0.5;\n return fract(grit + clump * RZ_DISSOLVE_CLUMP);\n}\n\nstruct LightVP { viewProj: mat4x4f, };\n@group(0) @binding(0) var<uniform> lp: LightVP;\n@group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;\n@group(0) @binding(2) var texSampler: sampler;\n@group(1) @binding(0) var diffuseTexture: texture_2d<f32>;\n// The head of the material block and the one field at its tail \u2014 the same\n// reach the depth prepass takes, and for the same reason: a shadow cast by a\n// body that is no longer drawn is the tell that the vanishing is a trick.\nstruct MaterialDiffuse {\n diffuse: vec4f,\n _skip0: vec4f,\n _skip1: vec4f,\n _skip2: vec3f,\n dissolve: f32,\n};\n@group(1) @binding(1) var<uniform> material: MaterialDiffuse;\n\nstruct VSOut {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n /** Bind-pose position, for the dissolve test \u2014 object space, as everywhere. */\n @location(1) restPos: vec3f,\n};\n\n@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> VSOut {\n let pos4 = vec4f(position, 1.0);\n let ws = weights0.x + weights0.y + weights0.z + weights0.w;\n let inv = select(1.0, 1.0 / ws, ws > 0.0001);\n let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);\n var sp = vec4f(0.0);\n for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }\n var out: VSOut;\n out.position = lp.viewProj * vec4f(sp.xyz, 1.0);\n out.uv = uv;\n out.restPos = position;\n return out;\n}\n\n@fragment fn fs(in: VSOut) {\n let alpha = textureSample(diffuseTexture, texSampler, in.uv).a * material.diffuse.a;\n if (alpha < 0.5) { discard; }\n // The dissolve, run as the colour pass runs it. A flake that is gone stops\n // casting: without this she leaves a whole shadow standing on the floor while\n // her body is in the air.\n if (material.dissolve < 0.9995 && rz_dissolve_threshold(in.restPos) > material.dissolve) { discard; }\n}\n";
1
+ export declare const SHADOW_DEPTH_SHADER_WGSL = "\n\n/** Clump size, in object-space units \u2014 MMD's are roughly centimetres.\n *\n * Raised from 0.42: at that size the pieces are near enough to speckle that a\n * departure reads as a fade rather than as a body coming apart, which is the\n * thing the clumping exists to avoid. Hand-sized pieces are visible ones. */\nconst RZ_DISSOLVE_GRAIN: f32 = 1.2;\n/** How much the clumps pull a fragment off its own grit value \u2014 the whole of\n * the LOOK, and none of the rate. See rz_dissolve_threshold. */\nconst RZ_DISSOLVE_CLUMP: f32 = 0.6;\n/** Grit cell, as a fraction of a clump. See rz_dissolve_threshold: this is what\n * keeps a material SMALLER than a clump from having one threshold for all of\n * it, and it has to be smaller than the SMALLEST material a model carries \u2014\n * an eye highlight, a button, a buckle \u2014 not merely small. Two and a half\n * millimetres on a normal model, which is under all of them. */\nconst RZ_DISSOLVE_GRIT: f32 = 0.021;\n/** How wide the glowing front is, in threshold units. Thin: the front is a\n * rim, and a wide one lights whole limbs at once. */\nconst RZ_DISSOLVE_EDGE: f32 = 0.11;\n/** What the front burns with. Emission, added after lighting and after the\n * graph \u2014 a colour that ramps or takes a shadow reads as paint, not as heat.\n * Above 1 on purpose: it is meant to reach the bloom pyramid. Blue-violet,\n * the same light the motes leaving her are drawn and lit with. */\nconst RZ_BURN_COLOR: vec3f = vec3f(0.45, 0.62, 2.40);\n\nfn rz_dissolve_hash(p: vec3f) -> f32 {\n // INTEGER, for exactly the reason _hash33 in nodes.ts is, and its comment\n // says it: above 24 bits an f32 loses the low ones. The float hash this\n // replaces multiplied three terms into the hundreds of millions and took\n // fract() of the product \u2014 which past 2^24 is not noise, it is a quantised\n // staircase, and wherever that staircase landed on zero the threshold was\n // zero. A surface whose threshold is zero never crosses it: it stays behind\n // while the rest of her leaves, and it sits inside the burning front the\n // whole time she is gone. Those were the white pieces that would not go.\n //\n // Floored first, unlike _hash33: vec3i() truncates toward zero, so -0.5 and\n // +0.5 are the same cell, and half this model is at negative x.\n var h = vec3u(vec3i(floor(p)) + vec3i(32768));\n h = h * vec3u(1664525u, 1013904223u, 2654435761u);\n h = (h.yzx ^ h) * vec3u(2246822519u, 3266489917u, 668265263u);\n h = h ^ (h >> vec3u(16u));\n return f32((h.x ^ h.y ^ h.z) & 16777215u) * (1.0 / 16777216.0);\n}\n\n/** Value noise over that hash \u2014 smooth within a flake, uncorrelated between. */\nfn rz_dissolve_field(p: vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let c000 = rz_dissolve_hash(i);\n let c100 = rz_dissolve_hash(i + vec3f(1.0, 0.0, 0.0));\n let c010 = rz_dissolve_hash(i + vec3f(0.0, 1.0, 0.0));\n let c110 = rz_dissolve_hash(i + vec3f(1.0, 1.0, 0.0));\n let c001 = rz_dissolve_hash(i + vec3f(0.0, 0.0, 1.0));\n let c101 = rz_dissolve_hash(i + vec3f(1.0, 0.0, 1.0));\n let c011 = rz_dissolve_hash(i + vec3f(0.0, 1.0, 1.0));\n let c111 = rz_dissolve_hash(i + vec3f(1.0, 1.0, 1.0));\n let x00 = mix(c000, c100, u.x);\n let x10 = mix(c010, c110, u.x);\n let x01 = mix(c001, c101, u.x);\n let x11 = mix(c011, c111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\n/**\n * The threshold this fragment is measured against.\n *\n * NOISE ALONE, so the whole body comes apart at once \u2014 a hand, a shoulder and\n * an ankle all thinning together rather than a line travelling up her. It was\n * tilted by height at first, on the argument that a sweep reads as something\n * happening TO her; what it actually reads as is a wipe, and a wipe has a\n * direction, an edge and a speed the rest of the effect knows nothing about.\n * Everywhere at once is also the honest match for what leaves: the motes are\n * shed from every limb in the same breath.\n *\n * TWO SCALES. Clumps alone meant every fragment of a material SMALLER than one\n * cell shared a single threshold: an eye highlight, a brow, a button, a hair\n * clip. All of it sat inside the glowing front at once, so the piece flashed\n * white as a whole and then popped \u2014 the two things a disintegration must never\n * do. The grit is a per-cell hash at a fraction of the clump, so a part a few\n * millimetres across still comes apart in pieces.\n *\n * THE GRIT IS THE BASE, though, and the clumps only push a fragment off it.\n * That ordering is what makes the dissolve smooth, and it is not obvious: the\n * two used to be WEIGHTED AND ADDED, and a sum is not uniformly distributed\n * however uniform each half is. It piles up in the middle, so most of the\n * surface had a threshold near a half \u2014 all of it went as the dissolve swept\n * through the middle, and what remained was the thin tail near zero. That tail\n * was the second stage everyone could see: a body that mostly vanished, then a\n * scatter of glowing scraps, then nothing.\n *\n * WRAPPED, not added. Offsetting a uniform value and taking fract() leaves it\n * EXACTLY uniform, where scaling a sum back into range only flattens the middle\n * \u2014 measured over four hundred thousand samples, the sum removed 12% of her per\n * step at the halfway mark and 0.1% at each end (a 110x swing), the scaled sum\n * still swung 89x, and the wrap is flat: 5.0% per step from the first flake to\n * the last, a ratio of 1.0.\n *\n * That flatness IS the smoothness. An uneven rate is what produced the two\n * stages you could see \u2014 a body that mostly vanished at once, then a scatter of\n * scraps that took as long again to follow.\n *\n * The clumping survives the wrap: neighbouring fragments share a clump value,\n * so the offset moves them together. What wraps past 1 comes back at 0 and goes\n * at the other end instead, which costs a little speckle in a region and buys\n * an even rate everywhere.\n */\nfn rz_dissolve_threshold(restPos: vec3f) -> f32 {\n let grit = rz_dissolve_hash(floor(restPos / (RZ_DISSOLVE_GRAIN * RZ_DISSOLVE_GRIT)));\n let clump = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN) - 0.5;\n return fract(grit + clump * RZ_DISSOLVE_CLUMP);\n}\n\nstruct LightVP { viewProj: mat4x4f, };\n@group(0) @binding(0) var<uniform> lp: LightVP;\n@group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;\n@group(0) @binding(2) var texSampler: sampler;\n@group(1) @binding(0) var diffuseTexture: texture_2d<f32>;\n// The head of the material block and the one field at its tail \u2014 the same\n// reach the depth prepass takes, and for the same reason: a shadow cast by a\n// body that is no longer drawn is the tell that the vanishing is a trick.\nstruct MaterialDiffuse {\n diffuse: vec4f,\n _skip0: vec4f,\n _skip1: vec4f,\n _skip2: vec3f,\n dissolve: f32,\n};\n@group(1) @binding(1) var<uniform> material: MaterialDiffuse;\n\nstruct VSOut {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n /** Bind-pose position, for the dissolve test \u2014 object space, as everywhere. */\n @location(1) restPos: vec3f,\n /** The triangle's own threshold, flat \u2014 see the material VertexOutput. A\n * face that is gone must stop casting, and stop as a whole face. */\n @location(2) @interpolate(flat) faceT: f32,\n};\n\n@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> VSOut {\n let pos4 = vec4f(position, 1.0);\n let ws = weights0.x + weights0.y + weights0.z + weights0.w;\n let inv = select(1.0, 1.0 / ws, ws > 0.0001);\n let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);\n var sp = vec4f(0.0);\n for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }\n var out: VSOut;\n out.position = lp.viewProj * vec4f(sp.xyz, 1.0);\n out.uv = uv;\n out.restPos = position;\n out.faceT = rz_dissolve_threshold(position);\n return out;\n}\n\n@fragment fn fs(in: VSOut) {\n let alpha = textureSample(diffuseTexture, texSampler, in.uv).a * material.diffuse.a;\n if (alpha < 0.5) { discard; }\n // The dissolve, run as the colour pass runs it. A flake that is gone stops\n // casting: without this she leaves a whole shadow standing on the floor while\n // her body is in the air.\n if (material.dissolve < 0.9995 && in.faceT > material.dissolve) { discard; }\n}\n";
2
2
  //# sourceMappingURL=shadow.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/shadow.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,wBAAwB,s4PAiDpC,CAAA"}
1
+ {"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/shadow.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,wBAAwB,y1QAqDpC,CAAA"}
@@ -29,6 +29,9 @@ struct VSOut {
29
29
  @location(0) uv: vec2f,
30
30
  /** Bind-pose position, for the dissolve test — object space, as everywhere. */
31
31
  @location(1) restPos: vec3f,
32
+ /** The triangle's own threshold, flat — see the material VertexOutput. A
33
+ * face that is gone must stop casting, and stop as a whole face. */
34
+ @location(2) @interpolate(flat) faceT: f32,
32
35
  };
33
36
 
34
37
  @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
@@ -43,6 +46,7 @@ struct VSOut {
43
46
  out.position = lp.viewProj * vec4f(sp.xyz, 1.0);
44
47
  out.uv = uv;
45
48
  out.restPos = position;
49
+ out.faceT = rz_dissolve_threshold(position);
46
50
  return out;
47
51
  }
48
52
 
@@ -52,6 +56,6 @@ struct VSOut {
52
56
  // The dissolve, run as the colour pass runs it. A flake that is gone stops
53
57
  // casting: without this she leaves a whole shadow standing on the floor while
54
58
  // her body is in the air.
55
- if (material.dissolve < 0.9995 && rz_dissolve_threshold(in.restPos) > material.dissolve) { discard; }
59
+ if (material.dissolve < 0.9995 && in.faceT > material.dissolve) { discard; }
56
60
  }
57
61
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.56.8",
3
+ "version": "0.56.10",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/src/engine.ts CHANGED
@@ -783,6 +783,17 @@ interface ModelInstance {
783
783
  * outline. Kept here so that write has somewhere to go. */
784
784
  outlineUniformBuffers: GPUBuffer[]
785
785
  model: Model
786
+ /**
787
+ * Keep simulating this model's cloth while it is HIDDEN.
788
+ *
789
+ * Off by default, which is what makes a roster of resident alternate skins
790
+ * affordable: invisible cloth costs nothing. On for a model whose visibility
791
+ * is scheduled, because the frame it appears on is the frame its skirt has to
792
+ * be already dancing — a dress simulated from rest at the reveal snaps into
793
+ * place in front of the audience, and that pop is the whole reason a costume
794
+ * change reads as a glitch rather than as a cut.
795
+ */
796
+ simulateWhileHidden: boolean
786
797
  basePath: string
787
798
  assetReader: AssetReader
788
799
  gpuBuffers: GPUBuffer[]
@@ -815,9 +826,10 @@ interface ModelInstance {
815
826
  /**
816
827
  * A PROP: a PMX object a character holds or wears — a microphone, a fan, a
817
828
  * sword. The third answer beside stage and plane. It keeps what a cast member
818
- * has that scenery does not (physics, outlines, its own clip) and drops what
819
- * makes one a performer: no effect subject id, no seeding of the scene clock,
820
- * no bone picking. Like a card it leaves the floor alone. See addProp.
829
+ * has that scenery does not (physics, outlines, its own clip, a place in the
830
+ * cast's silhouette for rzCastDistance) and drops what makes one a performer:
831
+ * no effect subject id, no seeding of the scene clock, no bone picking. Like a
832
+ * card it leaves the floor alone. See addProp.
821
833
  */
822
834
  isProp: boolean
823
835
  /** Who this model hangs from, or null. Any model can: a prop by design, a
@@ -1982,6 +1994,10 @@ export class Engine {
1982
1994
  private castStepBindGroups: GPUBindGroup[] = []
1983
1995
  private castResolveBindGroup: GPUBindGroup | null = null
1984
1996
  private castStepStrideBuffers: GPUBuffer[] = []
1997
+ /** The visible props' object ids for the seed pass, count in slot 0. Grown in
1998
+ * powers of two; see writeCastSeedProps. */
1999
+ private castSeedPropBuffer: GPUBuffer | null = null
2000
+ private castSeedPropData = new Uint32Array(8)
1985
2001
  /** Does anything installed actually read the field? Set from the effect list. */
1986
2002
  private castDistanceWanted = false
1987
2003
 
@@ -3679,14 +3695,7 @@ export class Engine {
3679
3695
  const strides: number[] = []
3680
3696
  for (let k = 1 << Math.ceil(Math.log2(Math.max(w, h))); k >= 1; k >>= 1) strides.push(k)
3681
3697
 
3682
- this.castSeedBindGroup = this.device.createBindGroup({
3683
- label: "cast distance seed",
3684
- layout: this.castSeedPipeline.getBindGroupLayout(0),
3685
- entries: [
3686
- { binding: 0, resource: this.idView! },
3687
- { binding: 1, resource: { buffer: this.castBuffer } },
3688
- ],
3689
- })
3698
+ this.createCastSeedBindGroup()
3690
3699
  strides.forEach((stride, i) => {
3691
3700
  const buf = this.device!.createBuffer({
3692
3701
  label: `cast distance stride ${stride}`,
@@ -3718,6 +3727,59 @@ export class Engine {
3718
3727
  })
3719
3728
  }
3720
3729
 
3730
+ /** The seed pass's inputs: the id attachment, the cast, and the prop ids. */
3731
+ private createCastSeedBindGroup(): void {
3732
+ const device = this.device!
3733
+ if (!this.castSeedPropBuffer) {
3734
+ this.castSeedPropBuffer = device.createBuffer({
3735
+ label: "cast distance prop seeds",
3736
+ size: this.castSeedPropData.byteLength,
3737
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
3738
+ })
3739
+ device.queue.writeBuffer(this.castSeedPropBuffer, 0, this.castSeedPropData)
3740
+ }
3741
+ this.castSeedBindGroup = device.createBindGroup({
3742
+ label: "cast distance seed",
3743
+ layout: this.castSeedPipeline!.getBindGroupLayout(0),
3744
+ entries: [
3745
+ { binding: 0, resource: this.idView! },
3746
+ { binding: 1, resource: { buffer: this.castBuffer } },
3747
+ { binding: 2, resource: { buffer: this.castSeedPropBuffer } },
3748
+ ],
3749
+ })
3750
+ }
3751
+
3752
+ /**
3753
+ * Which props seed the field this frame: every visible one.
3754
+ *
3755
+ * A prop is not a subject — nothing follows it and no effect reads its bones —
3756
+ * but it is part of her silhouette. Uploaded only when the list changes.
3757
+ */
3758
+ private writeCastSeedProps(): void {
3759
+ let n = 0
3760
+ this.forEachInstance((inst) => {
3761
+ if (inst.isProp && inst.model.visible) n++
3762
+ })
3763
+ let data = this.castSeedPropData
3764
+ let changed = data[0] !== n
3765
+ if (n + 1 > data.length) {
3766
+ data = new Uint32Array(1 << Math.ceil(Math.log2(n + 1)))
3767
+ this.castSeedPropData = data
3768
+ this.castSeedPropBuffer?.destroy()
3769
+ this.castSeedPropBuffer = null
3770
+ this.createCastSeedBindGroup()
3771
+ changed = true
3772
+ }
3773
+ data[0] = n
3774
+ let i = 1
3775
+ this.forEachInstance((inst) => {
3776
+ if (!inst.isProp || !inst.model.visible) return
3777
+ if (data[i] !== inst.objectId) changed = true
3778
+ data[i++] = inst.objectId
3779
+ })
3780
+ if (changed) this.device!.queue.writeBuffer(this.castSeedPropBuffer!, 0, data)
3781
+ }
3782
+
3721
3783
  /**
3722
3784
  * The flood, encoded once a frame before the field pass reads it.
3723
3785
  *
@@ -3727,6 +3789,7 @@ export class Engine {
3727
3789
  */
3728
3790
  private encodeCastDistance(encoder: GPUCommandEncoder): void {
3729
3791
  if (!this.castDistanceWanted || !this.castSeedBindGroup || !this.castResolveBindGroup) return
3792
+ this.writeCastSeedProps()
3730
3793
  const seed = encoder.beginRenderPass({
3731
3794
  label: "cast distance (seed)",
3732
3795
  colorAttachments: [
@@ -9455,6 +9518,8 @@ export class Engine {
9455
9518
  inst.styleGroups.clear()
9456
9519
  })
9457
9520
  this.zeroStyleBuffer?.destroy()
9521
+ this.castSeedPropBuffer?.destroy()
9522
+ this.castSeedPropBuffer = null
9458
9523
  this.releaseCullBuffers()
9459
9524
  this.cullFrustaBuffer?.destroy()
9460
9525
  this.cullFrustaBuffer = null
@@ -11029,6 +11094,25 @@ export class Engine {
11029
11094
  this.forEachInstance((inst) => inst.physics?.setFloor(on))
11030
11095
  }
11031
11096
 
11097
+ /**
11098
+ * Keep one model's cloth simulating while it is hidden.
11099
+ *
11100
+ * For scheduled visibility — a costume the scene swaps to, a double that
11101
+ * appears mid-shot. The body of a hidden model animates either way, so what
11102
+ * this buys is the cloth: switch it on and the skirt is already moving when
11103
+ * the model is revealed, instead of falling into place from rest in view.
11104
+ *
11105
+ * Per model rather than scene-wide, because it is exactly the models taking
11106
+ * part in a swap that need it, and a scene that never swaps should not pay
11107
+ * for cloth nobody can see.
11108
+ */
11109
+ setModelPhysicsWhileHidden(modelName: string, on: boolean): boolean {
11110
+ const inst = this.modelInstances.get(modelName)
11111
+ if (!inst) return false
11112
+ inst.simulateWhileHidden = on
11113
+ return true
11114
+ }
11115
+
11032
11116
  getPhysicsFloor(): boolean {
11033
11117
  return this.physicsFloor
11034
11118
  }
@@ -11120,10 +11204,13 @@ export class Engine {
11120
11204
  inst.vertexBufferNeedsUpdate = true
11121
11205
  }
11122
11206
  // Hidden models keep animating (cheap, and a reveal must not pop a stale
11123
- // pose) but skip cloth simulation entirely — a roster of resident
11207
+ // pose) but skip cloth simulation by default — a roster of resident
11124
11208
  // alternate skins would otherwise pay full physics for invisible cloth.
11125
- // Hosts that reveal after long hiding reset physics anyway (resetPhysics).
11126
- if (inst.physics && this.physicsEnabled && inst.model.visible) {
11209
+ //
11210
+ // UNLESS the host asked for it: a model whose visibility is scheduled has
11211
+ // to arrive with its skirt already in motion, and cloth caught up at the
11212
+ // reveal is a snap everyone sees. See setModelPhysicsWhileHidden.
11213
+ if (inst.physics && this.physicsEnabled && (inst.model.visible || inst.simulateWhileHidden)) {
11127
11214
  const tPhys = performance.now()
11128
11215
  inst.physics.step(deltaTime, inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices())
11129
11216
  // The step published new world matrices for the simulated bones; the
@@ -12218,6 +12305,7 @@ export class Engine {
12218
12305
  const inst: ModelInstance = {
12219
12306
  name,
12220
12307
  model,
12308
+ simulateWhileHidden: false,
12221
12309
  basePath,
12222
12310
  assetReader,
12223
12311
  gpuBuffers,
@@ -15086,7 +15174,8 @@ export class Engine {
15086
15174
  let n = 0
15087
15175
  let found: string | null = null
15088
15176
  this.forEachInstance((inst) => {
15089
- if (found !== null || n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp) return
15177
+ if (found !== null || n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp || !inst.model.visible)
15178
+ return
15090
15179
  if (n === index) found = inst.name
15091
15180
  n++
15092
15181
  })
@@ -15846,9 +15935,14 @@ export class Engine {
15846
15935
  // video behind her and none at all around her. Four is the cap because the
15847
15936
  // uniform is small and a scene with five characters is not the case this
15848
15937
  // serves.
15938
+ // A HIDDEN model is not a subject either, and that is not tidiness: a
15939
+ // scene holding a costume's twin keeps it loaded and invisible, and with
15940
+ // it in the list it takes subject 0 by load order — so Teleportation
15941
+ // spawned its motes off a body nobody could see, and the silhouette field
15942
+ // seeded on one. The cast is who is ON STAGE.
15849
15943
  let n = 0
15850
15944
  this.forEachInstance((inst) => {
15851
- if (n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp) return
15945
+ if (n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp || !inst.model.visible) return
15852
15946
  const m = inst.model
15853
15947
  // The model transform is only where the model was PLACED. A motion moves
15854
15948
  // the character by animating bones, so an effect anchored to the
@@ -115,7 +115,7 @@ ${discard}
115
115
  // as burning away rather than as a stencil moving over her.
116
116
  var rz_burn = 0.0;
117
117
  if (material.dissolve < 0.9995) {
118
- let rz_t = rz_dissolve_threshold(input.restPos);
118
+ let rz_t = input.faceT;
119
119
  if (rz_t > material.dissolve) { discard; }
120
120
  // Put OUT as the last of her goes. Near zero the front is wider than what
121
121
  // is left of the surface, so every surviving fragment sits inside it and
@@ -99,6 +99,18 @@ struct VertexOutput {
99
99
  // At rest skinMats are identity so restPos == worldPos, which is why existing noise-
100
100
  // scale constants stay valid without retuning.
101
101
  @location(3) restPos: vec3f,
102
+ /**
103
+ * This TRIANGLE's dissolve threshold.
104
+ *
105
+ * FLAT, so every fragment of a face shares the provoking vertex's value and
106
+ * the face goes as a unit — the mesh comes apart in triangles that light and
107
+ * leave in turn, rather than in a scatter that crosses them. Computed in the
108
+ * vertex stage from the bind-pose attribute, which is what lets the depth
109
+ * prepass, the shadow map and the outline hull reach the same number for the
110
+ * same face: one function, one input, one provoking vertex, one answer. They
111
+ * must agree — a pass that disagrees punches holes through her.
112
+ */
113
+ @location(4) @interpolate(flat) faceT: f32,
102
114
  };
103
115
 
104
116
  // One view-projection per shadow cascade, inner to outer — the volumes built
@@ -239,6 +251,7 @@ const COMMON_VS_WGSL = /* wgsl */ `
239
251
  output.uv = uv;
240
252
  output.worldPos = skinnedPos.xyz;
241
253
  output.restPos = position;
254
+ output.faceT = rz_dissolve_threshold(position);
242
255
  return output;
243
256
  }
244
257
 
@@ -297,8 +310,12 @@ export function commonFsOutWgsl(): string {
297
310
  * distance is just a fade.
298
311
  */
299
312
  export const DISSOLVE_WGSL = /* wgsl */ `
300
- /** Clump size, in object-space units — MMD's are roughly centimetres. */
301
- const RZ_DISSOLVE_GRAIN: f32 = 0.42;
313
+ /** Clump size, in object-space units — MMD's are roughly centimetres.
314
+ *
315
+ * Raised from 0.42: at that size the pieces are near enough to speckle that a
316
+ * departure reads as a fade rather than as a body coming apart, which is the
317
+ * thing the clumping exists to avoid. Hand-sized pieces are visible ones. */
318
+ const RZ_DISSOLVE_GRAIN: f32 = 1.2;
302
319
  /** How much the clumps pull a fragment off its own grit value — the whole of
303
320
  * the LOOK, and none of the rate. See rz_dissolve_threshold. */
304
321
  const RZ_DISSOLVE_CLUMP: f32 = 0.6;
@@ -307,7 +324,7 @@ const RZ_DISSOLVE_CLUMP: f32 = 0.6;
307
324
  * it, and it has to be smaller than the SMALLEST material a model carries —
308
325
  * an eye highlight, a button, a buckle — not merely small. Two and a half
309
326
  * millimetres on a normal model, which is under all of them. */
310
- const RZ_DISSOLVE_GRIT: f32 = 0.06;
327
+ const RZ_DISSOLVE_GRIT: f32 = 0.021;
311
328
  /** How wide the glowing front is, in threshold units. Thin: the front is a
312
329
  * rim, and a wide one lights whole limbs at once. */
313
330
  const RZ_DISSOLVE_EDGE: f32 = 0.11;
@@ -81,7 +81,11 @@ fn vs(@builtin(vertex_index) i: u32) -> VSOut {
81
81
  *
82
82
  * The ground, a stage and a media plane all write ids exactly as she does, so
83
83
  * seeding "anything drawn" would grow a border around the floor: a rectangle
84
- * round the frame rather than a sticker. Only the subject ids seed.
84
+ * round the frame rather than a sticker. The subject ids seed, and so do the
85
+ * props': a sword in her hand is part of her silhouette. A prop left out is a
86
+ * HOLE in the seeds wherever it covers her, and the field grows into it from
87
+ * her side, so the border ran round the inside of the prop instead of the
88
+ * outside.
85
89
  */
86
90
  export function buildCastSeedShader(samples: number): string {
87
91
  return (
@@ -92,6 +96,9 @@ const RZ_ID_SAMPLES: i32 = ${samples};
92
96
 
93
97
  @group(0) @binding(0) var _rzIdTex: texture_multisampled_2d<u32>;
94
98
  @group(0) @binding(1) var<storage, read> _rzCast: array<vec4f>;
99
+ /** The visible props' object ids, count first. Props are not subjects, so the
100
+ * cast buffer does not carry them. */
101
+ @group(0) @binding(2) var<storage, read> _rzSeedProps: array<u32>;
95
102
 
96
103
  // The two cast accessors this pass needs, spelled out rather than pulled in.
97
104
  // CAST_API brings subjects, trails, anchors and their aliases with it, and a
@@ -107,6 +114,17 @@ fn rzSubjectId(i: i32) -> u32 {
107
114
  if (i < 0 || i >= rzSubjectCount()) { return 0u; }
108
115
  return u32(_rzCast[i * ${EFFECT_SUBJECT_VEC4S} + 1].w);
109
116
  }
117
+ /** Does this object seed the field: a subject, or a prop. */
118
+ fn rzSeeds(o: u32, subjects: i32) -> bool {
119
+ for (var i = 0; i < subjects; i++) {
120
+ if (o == rzSubjectId(i)) { return true; }
121
+ }
122
+ let props = min(_rzSeedProps[0], arrayLength(&_rzSeedProps) - 1u);
123
+ for (var j = 1u; j <= props; j++) {
124
+ if (o == _rzSeedProps[j]) { return true; }
125
+ }
126
+ return false;
127
+ }
110
128
 
111
129
  ${FULLSCREEN_VS}
112
130
 
@@ -140,9 +158,7 @@ fn fs(@builtin(position) pos: vec4f) -> SeedOut {
140
158
  for (var sample = 0; sample < RZ_ID_SAMPLES; sample++) {
141
159
  let o = textureLoad(_rzIdTex, p, sample).y;
142
160
  if (o == 0u) { continue; }
143
- for (var i = 0; i < n; i++) {
144
- if (o == rzSubjectId(i)) { covered += 1.0; break; }
145
- }
161
+ if (rzSeeds(o, n)) { covered += 1.0; }
146
162
  }
147
163
  var out: SeedOut;
148
164
  out.coverage = covered / f32(RZ_ID_SAMPLES);
@@ -265,7 +281,7 @@ export function castDistanceApi(group: number, tex: number, samp: number, scale:
265
281
  *
266
282
  * There is no ceiling. A pixel on the far side of the frame gets the real
267
283
  * number. The ground, a stage and a media plane are not the cast and do not
268
- * seed it.
284
+ * seed it. A prop does: what she holds is inside her border.
269
285
  *
270
286
  * Screen pixels whatever the field's own resolution is, so an author writes the
271
287
  * width they mean and never has to know how this is built. uv is the effect's
@@ -62,6 +62,9 @@ struct VSOut {
62
62
  // The bind-pose position, carried for one reason: the dissolve test below has
63
63
  // to be the SAME test the colour pass runs, and that one is in object space.
64
64
  @location(1) restPos: vec3f,
65
+ /** The triangle's own threshold, flat — see the material VertexOutput. This
66
+ * pass must throw away exactly the faces the colour pass does. */
67
+ @location(2) @interpolate(flat) faceT: f32,
65
68
  };
66
69
 
67
70
  @vertex fn vs(
@@ -83,6 +86,7 @@ struct VSOut {
83
86
  o.position = camera.projection * camera.view * vec4f(skinned.xyz, 1.0);
84
87
  o.uv = uv;
85
88
  o.restPos = position;
89
+ o.faceT = rz_dissolve_threshold(position);
86
90
  return o;
87
91
  }
88
92
 
@@ -109,7 +113,7 @@ override CUTOFF: f32 = 0.5;
109
113
  // the colour pass throws away would punch holes that occlude the floor behind
110
114
  // her: you would see sky through her, which is the failure this line exists
111
115
  // to prevent.
112
- if (material.dissolve < 0.9995 && rz_dissolve_threshold(in.restPos) > material.dissolve) { discard; }
116
+ if (material.dissolve < 0.9995 && in.faceT > material.dissolve) { discard; }
113
117
  var out: PrepassOut;
114
118
  out.color = vec4f(0.0);
115
119
  out.mask = vec4f(0.0);
@@ -70,6 +70,9 @@ struct VertexOutput {
70
70
  * the depth prepass measure against, so all three agree about which flakes
71
71
  * are gone. */
72
72
  @location(1) restPos: vec3f,
73
+ /** The triangle's own threshold, flat — see the material VertexOutput. The
74
+ * hull traces the body's faces, so it has to lose the same ones. */
75
+ @location(2) @interpolate(flat) faceT: f32,
73
76
  };
74
77
 
75
78
  @vertex fn vs(
@@ -120,6 +123,7 @@ struct VertexOutput {
120
123
  output.position = vec4f(clipPos.xy + offset, clipPos.z, clipPos.w);
121
124
  output.uv = uv;
122
125
  output.restPos = position;
126
+ output.faceT = rz_dissolve_threshold(position);
123
127
  return output;
124
128
  }
125
129
 
@@ -145,7 +149,7 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
145
149
  // The same test the colour pass and the depth prepass run, against the same
146
150
  // bind-pose position — three passes, one rule, or they disagree about which
147
151
  // pieces are still there.
148
- if (material.dissolve < 0.9995 && rz_dissolve_threshold(input.restPos) > material.dissolve) { discard; }
152
+ if (material.dissolve < 0.9995 && input.faceT > material.dissolve) { discard; }
149
153
  let texA = textureSample(diffuseTexture, edgeSampler, input.uv).a;
150
154
  if (texA < 0.05) {
151
155
  discard;
@@ -31,6 +31,9 @@ struct VSOut {
31
31
  @location(0) uv: vec2f,
32
32
  /** Bind-pose position, for the dissolve test — object space, as everywhere. */
33
33
  @location(1) restPos: vec3f,
34
+ /** The triangle's own threshold, flat — see the material VertexOutput. A
35
+ * face that is gone must stop casting, and stop as a whole face. */
36
+ @location(2) @interpolate(flat) faceT: f32,
34
37
  };
35
38
 
36
39
  @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
@@ -45,6 +48,7 @@ struct VSOut {
45
48
  out.position = lp.viewProj * vec4f(sp.xyz, 1.0);
46
49
  out.uv = uv;
47
50
  out.restPos = position;
51
+ out.faceT = rz_dissolve_threshold(position);
48
52
  return out;
49
53
  }
50
54
 
@@ -54,6 +58,6 @@ struct VSOut {
54
58
  // The dissolve, run as the colour pass runs it. A flake that is gone stops
55
59
  // casting: without this she leaves a whole shadow standing on the floor while
56
60
  // her body is in the air.
57
- if (material.dissolve < 0.9995 && rz_dissolve_threshold(in.restPos) > material.dissolve) { discard; }
61
+ if (material.dissolve < 0.9995 && in.faceT > material.dissolve) { discard; }
58
62
  }
59
63
  `