reze-engine 0.50.10 → 0.50.11

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":"slots.d.ts","sourceRoot":"","sources":["../../src/graph/slots.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AA4K5D;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,oBAAoB,EAAE,OAAO,GAC5B,MAAM,CAgBR"}
1
+ {"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../src/graph/slots.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAkL5D;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,oBAAoB,EAAE,OAAO,GAC5B,MAAM,CAgBR"}
@@ -105,7 +105,13 @@ ${discard}
105
105
  if (material.dissolve < 0.9995) {
106
106
  let rz_t = rz_dissolve_threshold(input.restPos);
107
107
  if (rz_t > material.dissolve) { discard; }
108
- rz_burn = smoothstep(material.dissolve - RZ_DISSOLVE_EDGE, material.dissolve, rz_t);
108
+ // Put OUT as the last of her goes. Near zero the front is wider than what
109
+ // is left of the surface, so every surviving fragment sits inside it and
110
+ // the final scraps are pure glow — a body that ends as a handful of white
111
+ // specks rather than as a body. The taper costs nothing while she is mostly
112
+ // there, because it is already 1 by a fifth of the way in.
113
+ rz_burn = smoothstep(material.dissolve - RZ_DISSOLVE_EDGE, material.dissolve, rz_t)
114
+ * smoothstep(0.0, 0.22, material.dissolve);
109
115
  }
110
116
 
111
117
  var n = safe_normal(input.normal);
@@ -19,6 +19,6 @@ export declare function commonFsOutWgsl(): string;
19
19
  * clumps of a few millimetres rather than as single-pixel snow, which at any
20
20
  * distance is just a fade.
21
21
  */
22
- export declare const DISSOLVE_WGSL = "\n/** Clump size, in object-space units \u2014 MMD's are roughly centimetres. */\nconst RZ_DISSOLVE_GRAIN: f32 = 0.42;\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. Seven millimetres or so on a normal model. */\nconst RZ_DISSOLVE_GRIT: f32 = 0.16;\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 let q = fract(p * 0.3183099 + vec3f(0.1, 0.2, 0.3));\n let r = q * 17.0 * (q + 34.0);\n return fract(r.x * r.y * r.z);\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, and the second one is not decoration \u2014 it is the fix for a real\n * failure. 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 out of existence \u2014 the two things a\n * disintegration must never do. The grit is a plain per-cell hash at a fraction\n * of the clump, so a part a few millimetres across still comes apart in pieces.\n *\n * Weighted toward the clumps, because they are the LOOK: flakes a centimetre\n * across, in an order fixed to the surface, so the same piece of sleeve goes at\n * the same moment every time she leaves. The grit only breaks ties.\n */\nfn rz_dissolve_threshold(restPos: vec3f) -> f32 {\n let clump = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN);\n let grit = rz_dissolve_hash(floor(restPos / (RZ_DISSOLVE_GRAIN * RZ_DISSOLVE_GRIT)));\n return clamp(clump * 0.7 + grit * 0.3, 0.0, 1.0);\n}\n";
22
+ export declare const DISSOLVE_WGSL = "\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";
23
23
  export declare const COMMON_MATERIAL_PRELUDE_WGSL: string;
24
24
  //# sourceMappingURL=common.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA2BA,eAAO,MAAM,oBAAoB,QAuHhC,CAAC;AAgIF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,mgHAsEzB,CAAA;AAQD,eAAO,MAAM,4BAA4B,QAC0D,CAAA"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA2BA,eAAO,MAAM,oBAAoB,QAuHhC,CAAC;AAgIF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,k8LA4GzB,CAAA;AAQD,eAAO,MAAM,4BAA4B,QAC0D,CAAA"}
@@ -288,10 +288,15 @@ export function commonFsOutWgsl() {
288
288
  export const DISSOLVE_WGSL = /* wgsl */ `
289
289
  /** Clump size, in object-space units — MMD's are roughly centimetres. */
290
290
  const RZ_DISSOLVE_GRAIN: f32 = 0.42;
291
+ /** How much the clumps pull a fragment off its own grit value — the whole of
292
+ * the LOOK, and none of the rate. See rz_dissolve_threshold. */
293
+ const RZ_DISSOLVE_CLUMP: f32 = 0.6;
291
294
  /** Grit cell, as a fraction of a clump. See rz_dissolve_threshold: this is what
292
295
  * keeps a material SMALLER than a clump from having one threshold for all of
293
- * it. Seven millimetres or so on a normal model. */
294
- const RZ_DISSOLVE_GRIT: f32 = 0.16;
296
+ * it, and it has to be smaller than the SMALLEST material a model carries —
297
+ * an eye highlight, a button, a buckle — not merely small. Two and a half
298
+ * millimetres on a normal model, which is under all of them. */
299
+ const RZ_DISSOLVE_GRIT: f32 = 0.06;
295
300
  /** How wide the glowing front is, in threshold units. Thin: the front is a
296
301
  * rim, and a wide one lights whole limbs at once. */
297
302
  const RZ_DISSOLVE_EDGE: f32 = 0.11;
@@ -302,9 +307,22 @@ const RZ_DISSOLVE_EDGE: f32 = 0.11;
302
307
  const RZ_BURN_COLOR: vec3f = vec3f(0.45, 0.62, 2.40);
303
308
 
304
309
  fn rz_dissolve_hash(p: vec3f) -> f32 {
305
- let q = fract(p * 0.3183099 + vec3f(0.1, 0.2, 0.3));
306
- let r = q * 17.0 * (q + 34.0);
307
- return fract(r.x * r.y * r.z);
310
+ // INTEGER, for exactly the reason _hash33 in nodes.ts is, and its comment
311
+ // says it: above 24 bits an f32 loses the low ones. The float hash this
312
+ // replaces multiplied three terms into the hundreds of millions and took
313
+ // fract() of the product — which past 2^24 is not noise, it is a quantised
314
+ // staircase, and wherever that staircase landed on zero the threshold was
315
+ // zero. A surface whose threshold is zero never crosses it: it stays behind
316
+ // while the rest of her leaves, and it sits inside the burning front the
317
+ // whole time she is gone. Those were the white pieces that would not go.
318
+ //
319
+ // Floored first, unlike _hash33: vec3i() truncates toward zero, so -0.5 and
320
+ // +0.5 are the same cell, and half this model is at negative x.
321
+ var h = vec3u(vec3i(floor(p)) + vec3i(32768));
322
+ h = h * vec3u(1664525u, 1013904223u, 2654435761u);
323
+ h = (h.yzx ^ h) * vec3u(2246822519u, 3266489917u, 668265263u);
324
+ h = h ^ (h >> vec3u(16u));
325
+ return f32((h.x ^ h.y ^ h.z) & 16777215u) * (1.0 / 16777216.0);
308
326
  }
309
327
 
310
328
  /** Value noise over that hash — smooth within a flake, uncorrelated between. */
@@ -338,22 +356,42 @@ fn rz_dissolve_field(p: vec3f) -> f32 {
338
356
  * Everywhere at once is also the honest match for what leaves: the motes are
339
357
  * shed from every limb in the same breath.
340
358
  *
341
- * TWO SCALES, and the second one is not decoration it is the fix for a real
342
- * failure. Clumps alone meant every fragment of a material SMALLER than one
359
+ * TWO SCALES. Clumps alone meant every fragment of a material SMALLER than one
343
360
  * cell shared a single threshold: an eye highlight, a brow, a button, a hair
344
361
  * clip. All of it sat inside the glowing front at once, so the piece flashed
345
- * white as a whole and then popped out of existence — the two things a
346
- * disintegration must never do. The grit is a plain per-cell hash at a fraction
347
- * of the clump, so a part a few millimetres across still comes apart in pieces.
362
+ * white as a whole and then popped — the two things a disintegration must never
363
+ * do. The grit is a per-cell hash at a fraction of the clump, so a part a few
364
+ * millimetres across still comes apart in pieces.
365
+ *
366
+ * THE GRIT IS THE BASE, though, and the clumps only push a fragment off it.
367
+ * That ordering is what makes the dissolve smooth, and it is not obvious: the
368
+ * two used to be WEIGHTED AND ADDED, and a sum is not uniformly distributed
369
+ * however uniform each half is. It piles up in the middle, so most of the
370
+ * surface had a threshold near a half — all of it went as the dissolve swept
371
+ * through the middle, and what remained was the thin tail near zero. That tail
372
+ * was the second stage everyone could see: a body that mostly vanished, then a
373
+ * scatter of glowing scraps, then nothing.
374
+ *
375
+ * WRAPPED, not added. Offsetting a uniform value and taking fract() leaves it
376
+ * EXACTLY uniform, where scaling a sum back into range only flattens the middle
377
+ * — measured over four hundred thousand samples, the sum removed 12% of her per
378
+ * step at the halfway mark and 0.1% at each end (a 110x swing), the scaled sum
379
+ * still swung 89x, and the wrap is flat: 5.0% per step from the first flake to
380
+ * the last, a ratio of 1.0.
381
+ *
382
+ * That flatness IS the smoothness. An uneven rate is what produced the two
383
+ * stages you could see — a body that mostly vanished at once, then a scatter of
384
+ * scraps that took as long again to follow.
348
385
  *
349
- * Weighted toward the clumps, because they are the LOOK: flakes a centimetre
350
- * across, in an order fixed to the surface, so the same piece of sleeve goes at
351
- * the same moment every time she leaves. The grit only breaks ties.
386
+ * The clumping survives the wrap: neighbouring fragments share a clump value,
387
+ * so the offset moves them together. What wraps past 1 comes back at 0 and goes
388
+ * at the other end instead, which costs a little speckle in a region and buys
389
+ * an even rate everywhere.
352
390
  */
353
391
  fn rz_dissolve_threshold(restPos: vec3f) -> f32 {
354
- let clump = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN);
355
392
  let grit = rz_dissolve_hash(floor(restPos / (RZ_DISSOLVE_GRAIN * RZ_DISSOLVE_GRIT)));
356
- return clamp(clump * 0.7 + grit * 0.3, 0.0, 1.0);
393
+ let clump = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN) - 0.5;
394
+ return fract(grit + clump * RZ_DISSOLVE_CLUMP);
357
395
  }
358
396
  `;
359
397
  // ─── Convenience: full shared prelude ───────────────────────────────
@@ -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/** 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. Seven millimetres or so on a normal model. */\nconst RZ_DISSOLVE_GRIT: f32 = 0.16;\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 let q = fract(p * 0.3183099 + vec3f(0.1, 0.2, 0.3));\n let r = q * 17.0 * (q + 34.0);\n return fract(r.x * r.y * r.z);\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, and the second one is not decoration \u2014 it is the fix for a real\n * failure. 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 out of existence \u2014 the two things a\n * disintegration must never do. The grit is a plain per-cell hash at a fraction\n * of the clump, so a part a few millimetres across still comes apart in pieces.\n *\n * Weighted toward the clumps, because they are the LOOK: flakes a centimetre\n * across, in an order fixed to the surface, so the same piece of sleeve goes at\n * the same moment every time she leaves. The grit only breaks ties.\n */\nfn rz_dissolve_threshold(restPos: vec3f) -> f32 {\n let clump = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN);\n let grit = rz_dissolve_hash(floor(restPos / (RZ_DISSOLVE_GRAIN * RZ_DISSOLVE_GRIT)));\n return clamp(clump * 0.7 + grit * 0.3, 0.0, 1.0);\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. */\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";
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,u8KAiDpC,CAAA"}
1
+ {"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/shadow.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,wBAAwB,s4PAiDpC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.50.10",
3
+ "version": "0.50.11",
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",
@@ -117,7 +117,13 @@ ${discard}
117
117
  if (material.dissolve < 0.9995) {
118
118
  let rz_t = rz_dissolve_threshold(input.restPos);
119
119
  if (rz_t > material.dissolve) { discard; }
120
- rz_burn = smoothstep(material.dissolve - RZ_DISSOLVE_EDGE, material.dissolve, rz_t);
120
+ // Put OUT as the last of her goes. Near zero the front is wider than what
121
+ // is left of the surface, so every surviving fragment sits inside it and
122
+ // the final scraps are pure glow — a body that ends as a handful of white
123
+ // specks rather than as a body. The taper costs nothing while she is mostly
124
+ // there, because it is already 1 by a fifth of the way in.
125
+ rz_burn = smoothstep(material.dissolve - RZ_DISSOLVE_EDGE, material.dissolve, rz_t)
126
+ * smoothstep(0.0, 0.22, material.dissolve);
121
127
  }
122
128
 
123
129
  var n = safe_normal(input.normal);
@@ -299,10 +299,15 @@ export function commonFsOutWgsl(): string {
299
299
  export const DISSOLVE_WGSL = /* wgsl */ `
300
300
  /** Clump size, in object-space units — MMD's are roughly centimetres. */
301
301
  const RZ_DISSOLVE_GRAIN: f32 = 0.42;
302
+ /** How much the clumps pull a fragment off its own grit value — the whole of
303
+ * the LOOK, and none of the rate. See rz_dissolve_threshold. */
304
+ const RZ_DISSOLVE_CLUMP: f32 = 0.6;
302
305
  /** Grit cell, as a fraction of a clump. See rz_dissolve_threshold: this is what
303
306
  * keeps a material SMALLER than a clump from having one threshold for all of
304
- * it. Seven millimetres or so on a normal model. */
305
- const RZ_DISSOLVE_GRIT: f32 = 0.16;
307
+ * it, and it has to be smaller than the SMALLEST material a model carries —
308
+ * an eye highlight, a button, a buckle — not merely small. Two and a half
309
+ * millimetres on a normal model, which is under all of them. */
310
+ const RZ_DISSOLVE_GRIT: f32 = 0.06;
306
311
  /** How wide the glowing front is, in threshold units. Thin: the front is a
307
312
  * rim, and a wide one lights whole limbs at once. */
308
313
  const RZ_DISSOLVE_EDGE: f32 = 0.11;
@@ -313,9 +318,22 @@ const RZ_DISSOLVE_EDGE: f32 = 0.11;
313
318
  const RZ_BURN_COLOR: vec3f = vec3f(0.45, 0.62, 2.40);
314
319
 
315
320
  fn rz_dissolve_hash(p: vec3f) -> f32 {
316
- let q = fract(p * 0.3183099 + vec3f(0.1, 0.2, 0.3));
317
- let r = q * 17.0 * (q + 34.0);
318
- return fract(r.x * r.y * r.z);
321
+ // INTEGER, for exactly the reason _hash33 in nodes.ts is, and its comment
322
+ // says it: above 24 bits an f32 loses the low ones. The float hash this
323
+ // replaces multiplied three terms into the hundreds of millions and took
324
+ // fract() of the product — which past 2^24 is not noise, it is a quantised
325
+ // staircase, and wherever that staircase landed on zero the threshold was
326
+ // zero. A surface whose threshold is zero never crosses it: it stays behind
327
+ // while the rest of her leaves, and it sits inside the burning front the
328
+ // whole time she is gone. Those were the white pieces that would not go.
329
+ //
330
+ // Floored first, unlike _hash33: vec3i() truncates toward zero, so -0.5 and
331
+ // +0.5 are the same cell, and half this model is at negative x.
332
+ var h = vec3u(vec3i(floor(p)) + vec3i(32768));
333
+ h = h * vec3u(1664525u, 1013904223u, 2654435761u);
334
+ h = (h.yzx ^ h) * vec3u(2246822519u, 3266489917u, 668265263u);
335
+ h = h ^ (h >> vec3u(16u));
336
+ return f32((h.x ^ h.y ^ h.z) & 16777215u) * (1.0 / 16777216.0);
319
337
  }
320
338
 
321
339
  /** Value noise over that hash — smooth within a flake, uncorrelated between. */
@@ -349,22 +367,42 @@ fn rz_dissolve_field(p: vec3f) -> f32 {
349
367
  * Everywhere at once is also the honest match for what leaves: the motes are
350
368
  * shed from every limb in the same breath.
351
369
  *
352
- * TWO SCALES, and the second one is not decoration it is the fix for a real
353
- * failure. Clumps alone meant every fragment of a material SMALLER than one
370
+ * TWO SCALES. Clumps alone meant every fragment of a material SMALLER than one
354
371
  * cell shared a single threshold: an eye highlight, a brow, a button, a hair
355
372
  * clip. All of it sat inside the glowing front at once, so the piece flashed
356
- * white as a whole and then popped out of existence — the two things a
357
- * disintegration must never do. The grit is a plain per-cell hash at a fraction
358
- * of the clump, so a part a few millimetres across still comes apart in pieces.
373
+ * white as a whole and then popped — the two things a disintegration must never
374
+ * do. The grit is a per-cell hash at a fraction of the clump, so a part a few
375
+ * millimetres across still comes apart in pieces.
376
+ *
377
+ * THE GRIT IS THE BASE, though, and the clumps only push a fragment off it.
378
+ * That ordering is what makes the dissolve smooth, and it is not obvious: the
379
+ * two used to be WEIGHTED AND ADDED, and a sum is not uniformly distributed
380
+ * however uniform each half is. It piles up in the middle, so most of the
381
+ * surface had a threshold near a half — all of it went as the dissolve swept
382
+ * through the middle, and what remained was the thin tail near zero. That tail
383
+ * was the second stage everyone could see: a body that mostly vanished, then a
384
+ * scatter of glowing scraps, then nothing.
385
+ *
386
+ * WRAPPED, not added. Offsetting a uniform value and taking fract() leaves it
387
+ * EXACTLY uniform, where scaling a sum back into range only flattens the middle
388
+ * — measured over four hundred thousand samples, the sum removed 12% of her per
389
+ * step at the halfway mark and 0.1% at each end (a 110x swing), the scaled sum
390
+ * still swung 89x, and the wrap is flat: 5.0% per step from the first flake to
391
+ * the last, a ratio of 1.0.
392
+ *
393
+ * That flatness IS the smoothness. An uneven rate is what produced the two
394
+ * stages you could see — a body that mostly vanished at once, then a scatter of
395
+ * scraps that took as long again to follow.
359
396
  *
360
- * Weighted toward the clumps, because they are the LOOK: flakes a centimetre
361
- * across, in an order fixed to the surface, so the same piece of sleeve goes at
362
- * the same moment every time she leaves. The grit only breaks ties.
397
+ * The clumping survives the wrap: neighbouring fragments share a clump value,
398
+ * so the offset moves them together. What wraps past 1 comes back at 0 and goes
399
+ * at the other end instead, which costs a little speckle in a region and buys
400
+ * an even rate everywhere.
363
401
  */
364
402
  fn rz_dissolve_threshold(restPos: vec3f) -> f32 {
365
- let clump = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN);
366
403
  let grit = rz_dissolve_hash(floor(restPos / (RZ_DISSOLVE_GRAIN * RZ_DISSOLVE_GRIT)));
367
- return clamp(clump * 0.7 + grit * 0.3, 0.0, 1.0);
404
+ let clump = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN) - 0.5;
405
+ return fract(grit + clump * RZ_DISSOLVE_CLUMP);
368
406
  }
369
407
  `
370
408