reze-engine 0.50.7 → 0.50.8

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.
@@ -69,7 +69,16 @@ struct MaterialUniforms {
69
69
  // just empty. f32 because the buffer is written as floats; the shader casts.
70
70
  materialId: f32,
71
71
  objectId: f32,
72
- _pad2: f32,
72
+ // How much of this material is still THERE: 1 whole, 0 gone. Rides the last
73
+ // of the padding this struct already carried, for the same reason the ids do
74
+ // — the buffer's size and layout are untouched, so the indirect-draw path and
75
+ // every existing pipeline keep working, and a material that never dissolves
76
+ // pays one float it was already paying.
77
+ //
78
+ // A material morph rebuilds this block from its base copy, which is why the
79
+ // engine writes the value into that copy as well as into the live buffer: a
80
+ // face morphing while she dissolves must not come back solid for those frames.
81
+ dissolve: f32,
73
82
  };
74
83
 
75
84
  struct VertexOutput {
@@ -267,6 +276,82 @@ export function commonFsOutWgsl(): string {
267
276
  return `\n\n${sceneFsOutWgsl()}\n`;
268
277
  }
269
278
 
279
+ // ─── Dissolve ───────────────────────────────────────────────────────
280
+ /**
281
+ * Whether a fragment survives the dissolve, and how close it is to the front.
282
+ *
283
+ * ONE implementation, shared by the colour pass and the depth prepass, and that
284
+ * is the whole reason it lives here rather than in either of them. The prepass
285
+ * claims depth for fragments the colour pass will shade; if the two disagreed
286
+ * about which flakes are gone, the model would keep writing depth where it no
287
+ * longer draws — holes that occlude the floor behind her and read as sky.
288
+ *
289
+ * OBJECT SPACE, off the bind-pose position, for the same reason the procedural
290
+ * texture nodes use restPos: a world-space field would let the flakes swim
291
+ * through her as she moves, and a screen-space one would leave them hanging in
292
+ * the air while she turns. In object space the pattern is painted ON the
293
+ * surface — the flakes stay where they were on her arm as the arm moves.
294
+ *
295
+ * The hash is value noise rather than a bare hash so the flakes come apart in
296
+ * clumps of a few millimetres rather than as single-pixel snow, which at any
297
+ * distance is just a fade.
298
+ */
299
+ export const DISSOLVE_WGSL = /* wgsl */ `
300
+ /** Flake size, in object-space units — MMD's are roughly centimetres. */
301
+ const RZ_DISSOLVE_GRAIN: f32 = 0.55;
302
+ /** How wide the glowing front is, in threshold units. */
303
+ const RZ_DISSOLVE_EDGE: f32 = 0.16;
304
+ /** What the front burns with. Emission, added after lighting and after the
305
+ * graph — a colour that ramps or takes a shadow reads as paint, not as heat.
306
+ * Above 1 on purpose: it is meant to reach the bloom pyramid. */
307
+ const RZ_BURN_COLOR: vec3f = vec3f(0.55, 1.85, 2.60);
308
+
309
+ fn rz_dissolve_hash(p: vec3f) -> f32 {
310
+ let q = fract(p * 0.3183099 + vec3f(0.1, 0.2, 0.3));
311
+ let r = q * 17.0 * (q + 34.0);
312
+ return fract(r.x * r.y * r.z);
313
+ }
314
+
315
+ /** Value noise over that hash — smooth within a flake, uncorrelated between. */
316
+ fn rz_dissolve_field(p: vec3f) -> f32 {
317
+ let i = floor(p);
318
+ let f = fract(p);
319
+ let u = f * f * (3.0 - 2.0 * f);
320
+ let c000 = rz_dissolve_hash(i);
321
+ let c100 = rz_dissolve_hash(i + vec3f(1.0, 0.0, 0.0));
322
+ let c010 = rz_dissolve_hash(i + vec3f(0.0, 1.0, 0.0));
323
+ let c110 = rz_dissolve_hash(i + vec3f(1.0, 1.0, 0.0));
324
+ let c001 = rz_dissolve_hash(i + vec3f(0.0, 0.0, 1.0));
325
+ let c101 = rz_dissolve_hash(i + vec3f(1.0, 0.0, 1.0));
326
+ let c011 = rz_dissolve_hash(i + vec3f(0.0, 1.0, 1.0));
327
+ let c111 = rz_dissolve_hash(i + vec3f(1.0, 1.0, 1.0));
328
+ let x00 = mix(c000, c100, u.x);
329
+ let x10 = mix(c010, c110, u.x);
330
+ let x01 = mix(c001, c101, u.x);
331
+ let x11 = mix(c011, c111, u.x);
332
+ return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);
333
+ }
334
+
335
+ /**
336
+ * The threshold this fragment is measured against.
337
+ *
338
+ * Tilted by HEIGHT so the dissolve sweeps up the body instead of arriving
339
+ * everywhere at once: a body that comes apart from the feet reads as something
340
+ * happening TO her, and one that fades uniformly reads as an opacity slider.
341
+ * The tilt is gentle — a third of the range — so the noise still decides which
342
+ * flake goes when, and the sweep only decides roughly where it is.
343
+ *
344
+ * FEET FIRST, head last: the face is the last thing to go and the first thing
345
+ * back, which is the order every disappearance in film is cut in. Object space
346
+ * puts the floor at y = 0 on any PMX, so the tilt needs no rig measurement.
347
+ */
348
+ fn rz_dissolve_threshold(restPos: vec3f) -> f32 {
349
+ let n = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN);
350
+ let low = 1.0 - clamp(restPos.y * 0.045, 0.0, 1.0);
351
+ return clamp(n * 0.68 + low * 0.32, 0.0, 1.0);
352
+ }
353
+ `
354
+
270
355
  // ─── Convenience: full shared prelude ───────────────────────────────
271
356
  // Material files compose this as `${NODES_WGSL}${COMMON_MATERIAL_PRELUDE_WGSL}` to
272
357
  // pull in everything structural. Each material then adds its own constants + fs().
@@ -27,14 +27,25 @@
27
27
  // attachment, and that answer does not exist at import time. The constant could
28
28
  // not have taken the outputs at all, which is most of why it did not have them.
29
29
 
30
+ import { DISSOLVE_WGSL } from "../materials/common"
30
31
  import { sceneFsOutWgsl, sceneIdPadWgsl } from "./scene-contract"
31
32
 
32
33
  export function transparentDepthPrepassWgsl(): string {
33
34
  return /* wgsl */ `
35
+ ${DISSOLVE_WGSL}
34
36
  struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
37
+ // The head of the material block, plus the one field at its tail. The middle is
38
+ // skipped rather than named: this pass shades nothing, and every field it
39
+ // declares is a field that must stay in step with the real struct for no gain.
40
+ // The padding is explicit so the offsets are checkable by eye — vec4 at 16 and
41
+ // 32, vec3 at 48, and dissolve last at 60.
35
42
  struct MaterialUniforms {
36
43
  diffuseColor: vec3f,
37
44
  alpha: f32,
45
+ _skip0: vec4f,
46
+ _skip1: vec4f,
47
+ _skip2: vec3f,
48
+ dissolve: f32,
38
49
  };
39
50
 
40
51
  @group(0) @binding(0) var<uniform> camera: CameraUniforms;
@@ -48,6 +59,9 @@ struct VSOut {
48
59
  // must land on exactly the depths this wrote.
49
60
  @builtin(position) @invariant position: vec4f,
50
61
  @location(0) uv: vec2f,
62
+ // The bind-pose position, carried for one reason: the dissolve test below has
63
+ // to be the SAME test the colour pass runs, and that one is in object space.
64
+ @location(1) restPos: vec3f,
51
65
  };
52
66
 
53
67
  @vertex fn vs(
@@ -68,6 +82,7 @@ struct VSOut {
68
82
  var o: VSOut;
69
83
  o.position = camera.projection * camera.view * vec4f(skinned.xyz, 1.0);
70
84
  o.uv = uv;
85
+ o.restPos = position;
71
86
  return o;
72
87
  }
73
88
 
@@ -89,6 +104,12 @@ override CUTOFF: f32 = 0.5;
89
104
  @fragment fn fs(in: VSOut) -> PrepassOut {
90
105
  let a = material.alpha * textureSample(diffuseTexture, diffuseSampler, in.uv).a;
91
106
  if (a < CUTOFF) { discard; }
107
+ // The dissolve, run identically to the colour pass — shared code, not a
108
+ // second copy of the same idea. A prepass that kept claiming depth for flakes
109
+ // the colour pass throws away would punch holes that occlude the floor behind
110
+ // her: you would see sky through her, which is the failure this line exists
111
+ // to prevent.
112
+ if (material.dissolve < 0.9995 && rz_dissolve_threshold(in.restPos) > material.dissolve) { discard; }
92
113
  var out: PrepassOut;
93
114
  out.color = vec4f(0.0);
94
115
  out.mask = vec4f(0.0);
@@ -1,3 +1,5 @@
1
+ import { DISSOLVE_WGSL } from "../materials/common"
2
+
1
3
  // Shadow map depth pass. Skinned VS + alpha-test FS (depth-only attachment, no
2
4
  // color targets): texels where diffuse-texture alpha × material alpha fall below
3
5
  // the cutoff are discarded, so lace casts lace-shaped shadows and a true veil
@@ -6,17 +8,29 @@
6
8
  // bindings 0/1 are declared here (a layout may carry bindings a shader ignores).
7
9
 
8
10
  export const SHADOW_DEPTH_SHADER_WGSL = /* wgsl */ `
11
+ ${DISSOLVE_WGSL}
9
12
  struct LightVP { viewProj: mat4x4f, };
10
13
  @group(0) @binding(0) var<uniform> lp: LightVP;
11
14
  @group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;
12
15
  @group(0) @binding(2) var texSampler: sampler;
13
16
  @group(1) @binding(0) var diffuseTexture: texture_2d<f32>;
14
- struct MaterialDiffuse { diffuse: vec4f, };
17
+ // The head of the material block and the one field at its tail — the same
18
+ // reach the depth prepass takes, and for the same reason: a shadow cast by a
19
+ // body that is no longer drawn is the tell that the vanishing is a trick.
20
+ struct MaterialDiffuse {
21
+ diffuse: vec4f,
22
+ _skip0: vec4f,
23
+ _skip1: vec4f,
24
+ _skip2: vec3f,
25
+ dissolve: f32,
26
+ };
15
27
  @group(1) @binding(1) var<uniform> material: MaterialDiffuse;
16
28
 
17
29
  struct VSOut {
18
30
  @builtin(position) position: vec4f,
19
31
  @location(0) uv: vec2f,
32
+ /** Bind-pose position, for the dissolve test — object space, as everywhere. */
33
+ @location(1) restPos: vec3f,
20
34
  };
21
35
 
22
36
  @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
@@ -30,11 +44,16 @@ struct VSOut {
30
44
  var out: VSOut;
31
45
  out.position = lp.viewProj * vec4f(sp.xyz, 1.0);
32
46
  out.uv = uv;
47
+ out.restPos = position;
33
48
  return out;
34
49
  }
35
50
 
36
51
  @fragment fn fs(in: VSOut) {
37
52
  let alpha = textureSample(diffuseTexture, texSampler, in.uv).a * material.diffuse.a;
38
53
  if (alpha < 0.5) { discard; }
54
+ // The dissolve, run as the colour pass runs it. A flake that is gone stops
55
+ // casting: without this she leaves a whole shadow standing on the floor while
56
+ // her body is in the air.
57
+ if (material.dissolve < 0.9995 && rz_dissolve_threshold(in.restPos) > material.dissolve) { discard; }
39
58
  }
40
59
  `