reze-engine 0.50.6 → 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.
Files changed (84) hide show
  1. package/dist/_dbg-loader.mjs +1100 -0
  2. package/dist/engine.d.ts +71 -0
  3. package/dist/engine.d.ts.map +1 -1
  4. package/dist/engine.js +114 -1
  5. package/dist/graph/slots.d.ts.map +1 -1
  6. package/dist/graph/slots.js +35 -4
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/physics-debug.d.ts +30 -0
  10. package/dist/physics-debug.d.ts.map +1 -0
  11. package/dist/physics-debug.js +526 -0
  12. package/dist/shaders/cast-api.d.ts +1 -1
  13. package/dist/shaders/cast-api.d.ts.map +1 -1
  14. package/dist/shaders/cast-api.js +11 -0
  15. package/dist/shaders/materials/body.d.ts +2 -0
  16. package/dist/shaders/materials/body.d.ts.map +1 -0
  17. package/dist/shaders/materials/body.js +95 -0
  18. package/dist/shaders/materials/cloth_rough.d.ts +2 -0
  19. package/dist/shaders/materials/cloth_rough.d.ts.map +1 -0
  20. package/dist/shaders/materials/cloth_rough.js +69 -0
  21. package/dist/shaders/materials/cloth_smooth.d.ts +2 -0
  22. package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -0
  23. package/dist/shaders/materials/cloth_smooth.js +61 -0
  24. package/dist/shaders/materials/common.d.ts +20 -0
  25. package/dist/shaders/materials/common.d.ts.map +1 -1
  26. package/dist/shaders/materials/common.js +85 -1
  27. package/dist/shaders/materials/default.d.ts +2 -0
  28. package/dist/shaders/materials/default.d.ts.map +1 -0
  29. package/dist/shaders/materials/default.js +43 -0
  30. package/dist/shaders/materials/eye.d.ts +2 -0
  31. package/dist/shaders/materials/eye.d.ts.map +1 -0
  32. package/dist/shaders/materials/eye.js +60 -0
  33. package/dist/shaders/materials/face.d.ts +2 -0
  34. package/dist/shaders/materials/face.d.ts.map +1 -0
  35. package/dist/shaders/materials/face.js +95 -0
  36. package/dist/shaders/materials/hair.d.ts +2 -0
  37. package/dist/shaders/materials/hair.d.ts.map +1 -0
  38. package/dist/shaders/materials/hair.js +90 -0
  39. package/dist/shaders/materials/metal.d.ts +2 -0
  40. package/dist/shaders/materials/metal.d.ts.map +1 -0
  41. package/dist/shaders/materials/metal.js +77 -0
  42. package/dist/shaders/materials/mmd_classic.d.ts +2 -0
  43. package/dist/shaders/materials/mmd_classic.d.ts.map +1 -0
  44. package/dist/shaders/materials/mmd_classic.js +66 -0
  45. package/dist/shaders/materials/stockings.d.ts +2 -0
  46. package/dist/shaders/materials/stockings.d.ts.map +1 -0
  47. package/dist/shaders/materials/stockings.js +122 -0
  48. package/dist/shaders/passes/composite.d.ts +1 -1
  49. package/dist/shaders/passes/composite.d.ts.map +1 -1
  50. package/dist/shaders/passes/depth-prepass.d.ts.map +1 -1
  51. package/dist/shaders/passes/depth-prepass.js +21 -0
  52. package/dist/shaders/passes/physics-debug.d.ts +2 -0
  53. package/dist/shaders/passes/physics-debug.d.ts.map +1 -0
  54. package/dist/shaders/passes/physics-debug.js +69 -0
  55. package/dist/shaders/passes/shadow.d.ts +1 -1
  56. package/dist/shaders/passes/shadow.d.ts.map +1 -1
  57. package/dist/shaders/passes/shadow.js +19 -1
  58. package/dist/vmd-writer.d.ts +11 -6
  59. package/dist/vmd-writer.d.ts.map +1 -1
  60. package/dist/vmd-writer.js +35 -17
  61. package/package.json +1 -1
  62. package/src/engine.ts +139 -1
  63. package/src/graph/slots.ts +37 -4
  64. package/src/index.ts +1 -0
  65. package/src/shaders/cast-api.ts +11 -0
  66. package/src/shaders/materials/common.ts +86 -1
  67. package/src/shaders/passes/depth-prepass.ts +21 -0
  68. package/src/shaders/passes/shadow.ts +20 -1
  69. package/src/vmd-writer.ts +36 -16
  70. package/dist/physics/autofit.d.ts +0 -147
  71. package/dist/physics/autofit.d.ts.map +0 -1
  72. package/dist/physics/autofit.js +0 -501
  73. package/dist/shaders/passes/field-blit.d.ts +0 -26
  74. package/dist/shaders/passes/field-blit.d.ts.map +0 -1
  75. package/dist/shaders/passes/field-blit.js +0 -65
  76. package/dist/shaders/passes/ground-noise.d.ts +0 -7
  77. package/dist/shaders/passes/ground-noise.d.ts.map +0 -1
  78. package/dist/shaders/passes/ground-noise.js +0 -88
  79. package/dist/shaders/passes/sim.d.ts +0 -34
  80. package/dist/shaders/passes/sim.d.ts.map +0 -1
  81. package/dist/shaders/passes/sim.js +0 -169
  82. package/dist/shaders/score-api.d.ts +0 -10
  83. package/dist/shaders/score-api.d.ts.map +0 -1
  84. package/dist/shaders/score-api.js +0 -114
@@ -0,0 +1,2 @@
1
+ export declare const PHYSICS_DEBUG_SHADER_WGSL = "\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n viewPos: vec3f,\n _pad: f32,\n};\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n\noverride SHAPE_KIND: u32 = 0u;\noverride SOLID_ALPHA: f32 = 1.0;\n\nstruct VsIn {\n @location(0) unit: vec4f, // xyz = unit pos; w = capsule axial anchor\n @location(1) modelCol0: vec4f,\n @location(2) modelCol1: vec4f,\n @location(3) modelCol2: vec4f,\n @location(4) modelCol3: vec4f,\n @location(5) size: vec4f, // xyz = size; w unused\n @location(6) color: vec4f,\n};\n\nstruct VsOut {\n @builtin(position) pos: vec4f,\n @location(0) color: vec4f,\n};\n\n@vertex\nfn vsMain(in: VsIn) -> VsOut {\n let model = mat4x4f(in.modelCol0, in.modelCol1, in.modelCol2, in.modelCol3);\n var local: vec3f;\n if (SHAPE_KIND == 1u) {\n local = in.unit.xyz * in.size.xyz;\n } else if (SHAPE_KIND == 2u) {\n // PMX capsule: size.x = radius, size.y = full cylinder height. Bullet 2.x's\n // btCapsuleShape(radius, height) stores height/2 internally, so the actual\n // collision cylinder spans \u00B1size.y/2 \u2014 halve here to match.\n let r = in.size.x;\n let halfH = in.size.y * 0.5;\n local = vec3f(in.unit.x * r, in.unit.y * r + halfH * in.unit.w, in.unit.z * r);\n } else {\n local = in.unit.xyz * in.size.x;\n }\n let world = model * vec4f(local, 1.0);\n var out: VsOut;\n out.pos = camera.projection * camera.view * world;\n out.color = in.color;\n return out;\n}\n\n@fragment\nfn fsMain(in: VsOut) -> @location(0) vec4f {\n return vec4f(in.color.rgb, in.color.a * SOLID_ALPHA);\n}\n";
2
+ //# sourceMappingURL=physics-debug.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"physics-debug.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/physics-debug.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,yBAAyB,2kDAuDrC,CAAA"}
@@ -0,0 +1,69 @@
1
+ // Wireframe + solid overlay for physics rigid bodies (sphere / box / capsule).
2
+ // Rendered in its own pass after composite, straight to the swapchain — no
3
+ // MSAA, no depth, no MRT — so it sits cleanly on top of the tonemapped scene
4
+ // regardless of camera angle, model occlusion, or the composite alpha gate.
5
+ //
6
+ // SHAPE_KIND 0 = sphere → unit-sphere line/tri primitive scaled by size.x (radius)
7
+ // 1 = box → unit cube ±1 scaled by size.xyz (half-extents)
8
+ // 2 = capsule → unit cap+ring with per-vertex `axial` (-1/0/+1)
9
+ // that lifts cap rings/arcs ±halfHeight along Y;
10
+ // XZ scales by size.x (radius), Y by size.y/2.
11
+ // SOLID_ALPHA fragment alpha multiplier. Wireframe pipelines leave this at
12
+ // 1.0 for crisp edges; solid pipelines set it to ~0.2 so the fill
13
+ // stays gentle under stacked bodies.
14
+ export const PHYSICS_DEBUG_SHADER_WGSL = /* wgsl */ `
15
+ struct CameraUniforms {
16
+ view: mat4x4f,
17
+ projection: mat4x4f,
18
+ viewPos: vec3f,
19
+ _pad: f32,
20
+ };
21
+
22
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
23
+
24
+ override SHAPE_KIND: u32 = 0u;
25
+ override SOLID_ALPHA: f32 = 1.0;
26
+
27
+ struct VsIn {
28
+ @location(0) unit: vec4f, // xyz = unit pos; w = capsule axial anchor
29
+ @location(1) modelCol0: vec4f,
30
+ @location(2) modelCol1: vec4f,
31
+ @location(3) modelCol2: vec4f,
32
+ @location(4) modelCol3: vec4f,
33
+ @location(5) size: vec4f, // xyz = size; w unused
34
+ @location(6) color: vec4f,
35
+ };
36
+
37
+ struct VsOut {
38
+ @builtin(position) pos: vec4f,
39
+ @location(0) color: vec4f,
40
+ };
41
+
42
+ @vertex
43
+ fn vsMain(in: VsIn) -> VsOut {
44
+ let model = mat4x4f(in.modelCol0, in.modelCol1, in.modelCol2, in.modelCol3);
45
+ var local: vec3f;
46
+ if (SHAPE_KIND == 1u) {
47
+ local = in.unit.xyz * in.size.xyz;
48
+ } else if (SHAPE_KIND == 2u) {
49
+ // PMX capsule: size.x = radius, size.y = full cylinder height. Bullet 2.x's
50
+ // btCapsuleShape(radius, height) stores height/2 internally, so the actual
51
+ // collision cylinder spans ±size.y/2 — halve here to match.
52
+ let r = in.size.x;
53
+ let halfH = in.size.y * 0.5;
54
+ local = vec3f(in.unit.x * r, in.unit.y * r + halfH * in.unit.w, in.unit.z * r);
55
+ } else {
56
+ local = in.unit.xyz * in.size.x;
57
+ }
58
+ let world = model * vec4f(local, 1.0);
59
+ var out: VsOut;
60
+ out.pos = camera.projection * camera.view * world;
61
+ out.color = in.color;
62
+ return out;
63
+ }
64
+
65
+ @fragment
66
+ fn fsMain(in: VsOut) -> @location(0) vec4f {
67
+ return vec4f(in.color.rgb, in.color.a * SOLID_ALPHA);
68
+ }
69
+ `;
@@ -1,2 +1,2 @@
1
- export declare const SHADOW_DEPTH_SHADER_WGSL = "\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>;\nstruct MaterialDiffuse { diffuse: vec4f, };\n@group(1) @binding(1) var<uniform> material: MaterialDiffuse;\n\nstruct VSOut {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\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 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}\n";
1
+ export declare const SHADOW_DEPTH_SHADER_WGSL = "\n\n/** Flake size, in object-space units \u2014 MMD's are roughly centimetres. */\nconst RZ_DISSOLVE_GRAIN: f32 = 0.55;\n/** How wide the glowing front is, in threshold units. */\nconst RZ_DISSOLVE_EDGE: f32 = 0.16;\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. */\nconst RZ_BURN_COLOR: vec3f = vec3f(0.55, 1.85, 2.60);\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 * Tilted by HEIGHT so the dissolve sweeps up the body instead of arriving\n * everywhere at once: a body that comes apart from the feet reads as something\n * happening TO her, and one that fades uniformly reads as an opacity slider.\n * The tilt is gentle \u2014 a third of the range \u2014 so the noise still decides which\n * flake goes when, and the sweep only decides roughly where it is.\n *\n * FEET FIRST, head last: the face is the last thing to go and the first thing\n * back, which is the order every disappearance in film is cut in. Object space\n * puts the floor at y = 0 on any PMX, so the tilt needs no rig measurement.\n */\nfn rz_dissolve_threshold(restPos: vec3f) -> f32 {\n let n = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN);\n let low = 1.0 - clamp(restPos.y * 0.045, 0.0, 1.0);\n return clamp(n * 0.68 + low * 0.32, 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";
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":"AAOA,eAAO,MAAM,wBAAwB,otCAgCpC,CAAA"}
1
+ {"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/shadow.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,wBAAwB,y1IAiDpC,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { DISSOLVE_WGSL } from "../materials/common";
1
2
  // Shadow map depth pass. Skinned VS + alpha-test FS (depth-only attachment, no
2
3
  // color targets): texels where diffuse-texture alpha × material alpha fall below
3
4
  // the cutoff are discarded, so lace casts lace-shaped shadows and a true veil
@@ -5,17 +6,29 @@
5
6
  // Group 1 is the main pass's per-material bind group reused as-is; only
6
7
  // bindings 0/1 are declared here (a layout may carry bindings a shader ignores).
7
8
  export const SHADOW_DEPTH_SHADER_WGSL = /* wgsl */ `
9
+ ${DISSOLVE_WGSL}
8
10
  struct LightVP { viewProj: mat4x4f, };
9
11
  @group(0) @binding(0) var<uniform> lp: LightVP;
10
12
  @group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;
11
13
  @group(0) @binding(2) var texSampler: sampler;
12
14
  @group(1) @binding(0) var diffuseTexture: texture_2d<f32>;
13
- struct MaterialDiffuse { diffuse: vec4f, };
15
+ // The head of the material block and the one field at its tail — the same
16
+ // reach the depth prepass takes, and for the same reason: a shadow cast by a
17
+ // body that is no longer drawn is the tell that the vanishing is a trick.
18
+ struct MaterialDiffuse {
19
+ diffuse: vec4f,
20
+ _skip0: vec4f,
21
+ _skip1: vec4f,
22
+ _skip2: vec3f,
23
+ dissolve: f32,
24
+ };
14
25
  @group(1) @binding(1) var<uniform> material: MaterialDiffuse;
15
26
 
16
27
  struct VSOut {
17
28
  @builtin(position) position: vec4f,
18
29
  @location(0) uv: vec2f,
30
+ /** Bind-pose position, for the dissolve test — object space, as everywhere. */
31
+ @location(1) restPos: vec3f,
19
32
  };
20
33
 
21
34
  @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
@@ -29,11 +42,16 @@ struct VSOut {
29
42
  var out: VSOut;
30
43
  out.position = lp.viewProj * vec4f(sp.xyz, 1.0);
31
44
  out.uv = uv;
45
+ out.restPos = position;
32
46
  return out;
33
47
  }
34
48
 
35
49
  @fragment fn fs(in: VSOut) {
36
50
  let alpha = textureSample(diffuseTexture, texSampler, in.uv).a * material.diffuse.a;
37
51
  if (alpha < 0.5) { discard; }
52
+ // The dissolve, run as the colour pass runs it. A flake that is gone stops
53
+ // casting: without this she leaves a whole shadow standing on the floor while
54
+ // her body is in the air.
55
+ if (material.dissolve < 0.9995 && rz_dissolve_threshold(in.restPos) > material.dissolve) { discard; }
38
56
  }
39
57
  `;
@@ -17,12 +17,17 @@ export declare class VMDWriter {
17
17
  /**
18
18
  * A camera VMD: the shot's own file, with no model motion in it.
19
19
  *
20
- * Bone and morph counts are written as zero rather than omitted — the camera
21
- * block sits after them in the format, so a reader walking the file in order
22
- * (including this package's own parseCamera) has to pass through both to
23
- * reach it. Light, self-shadow and IK blocks are left off entirely; every
24
- * reader bounds-checks past the camera block, and MMD is happy with a file
25
- * that simply ends there.
20
+ * Every section count is written, including the empty ones. The format is
21
+ * positional, so a reader reaches each block by walking the counts before it —
22
+ * bone and morph to arrive at the camera block, and light, self-shadow and IK
23
+ * to leave it cleanly.
24
+ *
25
+ * The trailing three used to be omitted, on the theory that every reader
26
+ * bounds-checks past the camera block. They do not. This file type is
27
+ * カメラ・照明 — camera AND lighting — so MMD looks for the light block exactly
28
+ * where the file used to stop, read past the end, and lit the scene from
29
+ * whatever it found there. It reached users as a stray coloured light on a
30
+ * camera-only export.
26
31
  *
27
32
  * `frames` is sorted by frame on the way out: CameraAnimation binary-searches
28
33
  * the track it loads, and an out-of-order file would sample wrong rather than
@@ -1 +1 @@
1
- {"version":3,"file":"vmd-writer.d.ts","sourceRoot":"","sources":["../src/vmd-writer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAmC,MAAM,aAAa,CAAA;AAC5E,OAAO,EAAgC,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAsDhF;;;;;;;;8CAQ8C;AAC9C,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE3D,qBAAa,SAAS;IACpB,KAAK,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,iBAAiB,CAAA;KAAE,GAAG,WAAW;IAiIjF;;;;;;;;;;;;;OAaG;IACH,WAAW,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,WAAW;CA2CnD"}
1
+ {"version":3,"file":"vmd-writer.d.ts","sourceRoot":"","sources":["../src/vmd-writer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAmC,MAAM,aAAa,CAAA;AAC5E,OAAO,EAAgC,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAsDhF;;;;;;;;8CAQ8C;AAC9C,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE3D,qBAAa,SAAS;IACpB,KAAK,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,iBAAiB,CAAA;KAAE,GAAG,WAAW;IA0IjF;;;;;;;;;;;;;;;;;;OAkBG;IACH,WAAW,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,WAAW;CAiDnD"}
@@ -90,10 +90,17 @@ export class VMDWriter {
90
90
  MODEL_NAME_SIZE +
91
91
  4 + totalBoneFrames * BONE_FRAME_SIZE +
92
92
  4 + totalMorphFrames * MORPH_FRAME_SIZE +
93
- // Camera, light and self-shadow counts, then the IK block. Written only
94
- // when there is IK state to carry: a plain motion stays byte-identical to
95
- // what this writer produced before.
96
- (ikFrames.length > 0 ? 4 * 4 + ikSize : 0);
93
+ // Camera, light, self-shadow and IK counts, ALWAYS — 16 bytes even when
94
+ // every one of them is zero.
95
+ //
96
+ // A VMD's sections are positional: a reader reaches each block by walking
97
+ // every count before it. Writing these only when there was IK state to
98
+ // carry left an ordinary motion ending right after the morph block, so
99
+ // anything looking for the sections that follow read past the end of the
100
+ // file and took whatever it found. MMD's own motion files carry all five
101
+ // counts unconditionally; so do ours now.
102
+ 4 * 4 +
103
+ ikSize;
97
104
  const buffer = new ArrayBuffer(size);
98
105
  const view = new DataView(buffer);
99
106
  let offset = 0;
@@ -150,13 +157,14 @@ export class VMDWriter {
150
157
  offset += 4;
151
158
  }
152
159
  }
160
+ // Empty camera, light and self-shadow blocks, then the IK count — written
161
+ // whether or not there is IK state, so the file always ends on a complete
162
+ // section table.
163
+ for (let i = 0; i < 3; i++, offset += 4)
164
+ view.setUint32(offset, 0, true);
165
+ view.setUint32(offset, ikFrames.length, true);
166
+ offset += 4;
153
167
  if (ikFrames.length > 0) {
154
- // Empty camera, light and self-shadow blocks — a reader walking the file
155
- // in order has to pass through them to reach the IK block.
156
- for (let i = 0; i < 3; i++, offset += 4)
157
- view.setUint32(offset, 0, true);
158
- view.setUint32(offset, ikFrames.length, true);
159
- offset += 4;
160
168
  for (const [frame, states] of ikFrames) {
161
169
  view.setUint32(offset, frame, true);
162
170
  offset += 4;
@@ -176,12 +184,17 @@ export class VMDWriter {
176
184
  /**
177
185
  * A camera VMD: the shot's own file, with no model motion in it.
178
186
  *
179
- * Bone and morph counts are written as zero rather than omitted — the camera
180
- * block sits after them in the format, so a reader walking the file in order
181
- * (including this package's own parseCamera) has to pass through both to
182
- * reach it. Light, self-shadow and IK blocks are left off entirely; every
183
- * reader bounds-checks past the camera block, and MMD is happy with a file
184
- * that simply ends there.
187
+ * Every section count is written, including the empty ones. The format is
188
+ * positional, so a reader reaches each block by walking the counts before it —
189
+ * bone and morph to arrive at the camera block, and light, self-shadow and IK
190
+ * to leave it cleanly.
191
+ *
192
+ * The trailing three used to be omitted, on the theory that every reader
193
+ * bounds-checks past the camera block. They do not. This file type is
194
+ * カメラ・照明 — camera AND lighting — so MMD looks for the light block exactly
195
+ * where the file used to stop, read past the end, and lit the scene from
196
+ * whatever it found there. It reached users as a stray coloured light on a
197
+ * camera-only export.
185
198
  *
186
199
  * `frames` is sorted by frame on the way out: CameraAnimation binary-searches
187
200
  * the track it loads, and an out-of-order file would sample wrong rather than
@@ -189,7 +202,7 @@ export class VMDWriter {
189
202
  */
190
203
  writeCamera(frames) {
191
204
  const sorted = [...frames].sort((a, b) => a.frame - b.frame);
192
- const size = HEADER_SIZE + MODEL_NAME_SIZE + 4 + 4 + 4 + sorted.length * CAMERA_FRAME_SIZE;
205
+ const size = HEADER_SIZE + MODEL_NAME_SIZE + 4 + 4 + 4 + sorted.length * CAMERA_FRAME_SIZE + 4 * 3;
193
206
  const buffer = new ArrayBuffer(size);
194
207
  const view = new DataView(buffer);
195
208
  let offset = 0;
@@ -234,6 +247,11 @@ export class VMDWriter {
234
247
  view.setUint8(offset, 0); // 0 = perspective
235
248
  offset += 1;
236
249
  }
250
+ // Light, self-shadow and IK/display counts. See the note above: the light
251
+ // block begins here, and leaving it off is what lit MMD from past the end
252
+ // of the file.
253
+ for (let i = 0; i < 3; i++, offset += 4)
254
+ view.setUint32(offset, 0, true);
237
255
  return buffer;
238
256
  }
239
257
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.50.6",
3
+ "version": "0.50.8",
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
@@ -666,12 +666,40 @@ interface PickDrawCall {
666
666
  bindGroup: GPUBindGroup
667
667
  }
668
668
 
669
+ /**
670
+ * A repeating dissolve, in seconds within one cycle.
671
+ *
672
+ * Four moments rather than a duration and a delay: every one of them is a thing
673
+ * you can see happen, and an author tuning this is watching for exactly those
674
+ * four frames.
675
+ */
676
+ export interface DissolveCycle {
677
+ period: number
678
+ /** She starts to come apart. */
679
+ breakAt: number
680
+ /** Fully gone. */
681
+ hiddenAt: number
682
+ /** She starts to come back. */
683
+ backAt: number
684
+ /** Whole again. */
685
+ doneAt: number
686
+ }
687
+
669
688
  interface ModelInstance {
670
689
  name: string
671
690
  /** This model's id in the id attachment — 1-based, so 0 stays "nothing".
672
691
  * The pick pass has always minted it; the cast carries it now too, so an
673
692
  * effect can compare what it reads out of the id buffer against a subject. */
674
693
  objectId: number
694
+ /** How much of this model is still THERE: 1 whole, 0 gone. Written into every
695
+ * material's uniform (see setModelDissolve) and mirrored into the cast, so
696
+ * the material shell can take her apart and an effect can draw what is
697
+ * leaving — both from one number rather than two clocks that must agree. */
698
+ dissolve: number
699
+ /** Every material's uniform buffer, in draw order. Kept because a dissolve
700
+ * writes ONE float into each of them and needs no other reason to hold a
701
+ * block: the whole 16-float copy exists only for materials that morph. */
702
+ materialUniformBuffers: GPUBuffer[]
675
703
  model: Model
676
704
  basePath: string
677
705
  assetReader: AssetReader
@@ -1595,6 +1623,8 @@ export class Engine {
1595
1623
  * every frame. */
1596
1624
  private fieldClockScratch = new Float32Array(4)
1597
1625
  /** Material parameters driven by the scene clock — see setStyleParamTrack. */
1626
+ /** Repeating dissolves, by model name — see setModelDissolveCycle. */
1627
+ private dissolveCycles = new Map<string, DissolveCycle>()
1598
1628
  private paramTracks = new Map<
1599
1629
  string,
1600
1630
  { modelName: string; groupId: string; paramId: string; keys: ParamKey[]; last: ParamValue | null }
@@ -8662,6 +8692,8 @@ export class Engine {
8662
8692
  materialToGroup: new Map(),
8663
8693
  styleGroupGen: new Map(),
8664
8694
  objectId: this.modelInstances.size + 1,
8695
+ dissolve: 1,
8696
+ materialUniformBuffers: [],
8665
8697
  cullModelIndex: 0,
8666
8698
  // Seeded false: the first skin-matrix upload decides it, and until then the
8667
8699
  // sphere path is the safe answer (it never culls something it should not).
@@ -9069,6 +9101,7 @@ export class Engine {
9069
9101
  modelId,
9070
9102
  )
9071
9103
  inst.gpuBuffers.push(materialUniformBuffer)
9104
+ inst.materialUniformBuffers.push(materialUniformBuffer)
9072
9105
  if (morphedMaterials.has(pmxMaterialIndex)) {
9073
9106
  const base = this.materialUniformData(mat, sphereMode, headBoneIndex, materialId, modelId)
9074
9107
  morphTargets.push({
@@ -9225,6 +9258,10 @@ export class Engine {
9225
9258
  // bound and the indirect-draw path is untouched.
9226
9259
  data[13] = materialId
9227
9260
  data[14] = objectId
9261
+ // 15 is the last of that padding: how much of this material is there. ONE,
9262
+ // not zero — the default has to be "whole", or every model would load
9263
+ // already gone.
9264
+ data[15] = 1
9228
9265
  return data
9229
9266
  }
9230
9267
 
@@ -10329,6 +10366,7 @@ export class Engine {
10329
10366
  // and a grid stepped after them is one frame stale in everything that used it.
10330
10367
  // Material parameters on the scene clock, before anything reads their
10331
10368
  // uniforms this frame.
10369
+ this.evaluateDissolveCycles()
10332
10370
  this.evaluateParamTracks()
10333
10371
  this.stepSim(encoder, deltaTime)
10334
10372
  this.stepParticles(encoder, deltaTime)
@@ -10730,6 +10768,102 @@ export class Engine {
10730
10768
  return true
10731
10769
  }
10732
10770
 
10771
+ /**
10772
+ * How much of a model is still THERE: 1 whole, 0 gone.
10773
+ *
10774
+ * The instant tier, like setStyleParam — one float per material, no recompile,
10775
+ * no pipeline touched. What it drives is a THRESHOLD, not an opacity: the
10776
+ * material shell throws away every flake whose object-space threshold has
10777
+ * passed, and lights the ones about to go. So a model at 0.5 is not
10778
+ * half-transparent, it is half GONE, which is the difference between a fade
10779
+ * and a disintegration.
10780
+ *
10781
+ * Written into the depth prepass's copy of the same test as well, so the
10782
+ * flakes stop claiming depth the moment they stop being drawn — see
10783
+ * DISSOLVE_WGSL for why that has to be one implementation.
10784
+ *
10785
+ * Mirrored into the cast, so an effect can read rzSubject(i).dissolve and draw
10786
+ * the sparks that leave her in step with the body they came off. That is the
10787
+ * whole reason this lives on the model rather than in an effect's uniform: the
10788
+ * material pass runs long before any effect, and only the engine sees both.
10789
+ */
10790
+ setModelDissolve(modelName: string, value: number): boolean {
10791
+ const inst = this.modelInstances.get(modelName)
10792
+ if (!inst) return false
10793
+ const v = Math.min(1, Math.max(0, value))
10794
+ if (inst.dissolve === v) return true
10795
+ inst.dissolve = v
10796
+ // Offset 60: the sixteenth float of MaterialUniforms. One four-byte write
10797
+ // per material rather than the whole block — the block only exists as a
10798
+ // copy for materials that morph.
10799
+ const one = new Float32Array([v])
10800
+ for (const buffer of inst.materialUniformBuffers) {
10801
+ this.device.queue.writeBuffer(buffer, 60, one)
10802
+ }
10803
+ // The morph path rebuilds a material's block from its `base` copy and
10804
+ // uploads it whole, which would put the old value straight back. Patching
10805
+ // `base` is what keeps a face that is morphing while she dissolves from
10806
+ // coming back solid for those frames; `last` is cleared so the next
10807
+ // comparison genuinely re-uploads rather than deciding nothing moved.
10808
+ if (inst.materialMorphTargets) {
10809
+ for (const t of inst.materialMorphTargets) {
10810
+ t.base[15] = v
10811
+ t.last[15] = Number.NaN
10812
+ }
10813
+ }
10814
+ return true
10815
+ }
10816
+
10817
+ /**
10818
+ * A repeating dissolve, on the scene clock.
10819
+ *
10820
+ * The alternative was a host calling setModelDissolve every frame, and it is
10821
+ * the wrong shape twice: an exported take stepped at another rate would land
10822
+ * on different values than the preview did, and the effect drawing the sparks
10823
+ * would be reading a number some other clock wrote. Here the engine samples it
10824
+ * where it samples everything else time-driven, so a take reproduces exactly
10825
+ * and rzSubject().dissolve is the same value the material shell used on that
10826
+ * very frame.
10827
+ *
10828
+ * The five numbers are seconds within one cycle: when she starts to go, when
10829
+ * she is fully gone, when she starts to come back, and when she is whole. The
10830
+ * gaps between them are the timing, and the hold between the middle two is how
10831
+ * long she is away.
10832
+ */
10833
+ setModelDissolveCycle(modelName: string, cycle: DissolveCycle | null): boolean {
10834
+ if (!this.modelInstances.has(modelName)) return false
10835
+ if (!cycle) {
10836
+ if (this.dissolveCycles.delete(modelName)) this.setModelDissolve(modelName, 1)
10837
+ return true
10838
+ }
10839
+ this.dissolveCycles.set(modelName, cycle)
10840
+ return true
10841
+ }
10842
+
10843
+ /** Every dissolve cycle, at the current scene clock. Once per frame, before
10844
+ * the cast is written and long before any effect reads it. */
10845
+ private evaluateDissolveCycles(): void {
10846
+ if (this.dissolveCycles.size === 0) return
10847
+ for (const [name, c] of this.dissolveCycles) {
10848
+ const period = Math.max(c.period, 1e-3)
10849
+ const t = this.sceneClock - Math.floor(this.sceneClock / period) * period
10850
+ let v = 1
10851
+ if (t >= c.breakAt && t < c.hiddenAt) {
10852
+ v = 1 - (t - c.breakAt) / Math.max(c.hiddenAt - c.breakAt, 1e-4)
10853
+ } else if (t >= c.hiddenAt && t < c.backAt) {
10854
+ v = 0
10855
+ } else if (t >= c.backAt && t < c.doneAt) {
10856
+ v = (t - c.backAt) / Math.max(c.doneAt - c.backAt, 1e-4)
10857
+ }
10858
+ this.setModelDissolve(name, v)
10859
+ }
10860
+ }
10861
+
10862
+ /** What setModelDissolve last set, or 1 for a model that has never dissolved. */
10863
+ getModelDissolve(modelName: string): number {
10864
+ return this.modelInstances.get(modelName)?.dissolve ?? 1
10865
+ }
10866
+
10733
10867
  // Materials claimed by the model's currently-installed groups (for upsert/remove paths;
10734
10868
  // applyStyleGroups derives claims from its input array instead).
10735
10869
  private currentClaims(inst: ModelInstance): Map<string, string> {
@@ -11494,7 +11628,11 @@ export class Engine {
11494
11628
  cd[b] = px
11495
11629
  cd[b + 1] = floorY
11496
11630
  cd[b + 2] = pz
11497
- cd[b + 3] = 1
11631
+ // The root vec4's w, a constant 1 until now: how much of this subject is
11632
+ // still there. An effect that draws what is LEAVING her needs to know how
11633
+ // far along she is, and reading it here is what keeps the sparks in step
11634
+ // with the body without a second clock to agree with.
11635
+ cd[b + 3] = inst.dissolve
11498
11636
  cd[b + 4] = px
11499
11637
  cd[b + 5] = py
11500
11638
  cd[b + 6] = pz
@@ -8,7 +8,7 @@
8
8
  // See docs/style-groups-spec.md §5, §7.
9
9
 
10
10
  import { NODES_WGSL } from "../shaders/materials/nodes"
11
- import { COMMON_MATERIAL_PRELUDE_WGSL, commonFsOutWgsl } from "../shaders/materials/common"
11
+ import { COMMON_MATERIAL_PRELUDE_WGSL, DISSOLVE_WGSL, commonFsOutWgsl } from "../shaders/materials/common"
12
12
  import { sceneIdWriteWgsl } from "../shaders/passes/scene-contract"
13
13
  import type { AlphaMode, RenderClass } from "./render-class"
14
14
 
@@ -65,7 +65,16 @@ const STYLE_UNIFORMS_WGSL = `struct StyleUniforms { p: array<vec4f, 16> };
65
65
  `
66
66
 
67
67
  function decls(renderClass: RenderClass, alphaMode: AlphaMode): string {
68
- return (alphaMode === "hashed" ? HASHED_ALPHA_DECLS : "") + (renderClass === "hair" ? HAIR_OVER_EYES_DECL : "")
68
+ // The dissolve helpers are ALWAYS declared, unlike the hashed-alpha ones.
69
+ // They cost nothing on a material that never dissolves — the branch that
70
+ // calls them is uniform across a draw and folds away — and making them
71
+ // conditional would mean deciding at COMPILE time which materials may ever
72
+ // come apart, which is the one thing a runtime value must not need.
73
+ return (
74
+ DISSOLVE_WGSL +
75
+ (alphaMode === "hashed" ? HASHED_ALPHA_DECLS : "") +
76
+ (renderClass === "hair" ? HAIR_OVER_EYES_DECL : "")
77
+ )
69
78
  }
70
79
 
71
80
  // fs() header up to and including the graph body's context locals. Composed so the
@@ -92,6 +101,25 @@ function prelude(renderClass: RenderClass, alphaMode: AlphaMode): string {
92
101
  let alpha = material.alpha * tex_s.a;
93
102
  ${discard}
94
103
 
104
+ // ── Dissolve ──
105
+ //
106
+ // A fragment is either THERE or it is not: the flake is thrown away rather
107
+ // than faded. A fade would be an opacity slider — she would go see-through
108
+ // and you would read the wall through her hair — while a threshold takes her
109
+ // apart in pieces, which is the thing being asked for. The depth prepass runs
110
+ // this identical test (rz_dissolve_threshold, shared), so depth and colour
111
+ // agree about which pieces are gone.
112
+ //
113
+ // The surviving edge glows: fragments within RZ_DISSOLVE_EDGE of the front
114
+ // are the ones about to go, and lighting them is what makes the boundary read
115
+ // as burning away rather than as a stencil moving over her.
116
+ var rz_burn = 0.0;
117
+ if (material.dissolve < 0.9995) {
118
+ let rz_t = rz_dissolve_threshold(input.restPos);
119
+ if (rz_t > material.dissolve) { discard; }
120
+ rz_burn = smoothstep(material.dissolve - RZ_DISSOLVE_EDGE, material.dissolve, rz_t);
121
+ }
122
+
95
123
  var n = safe_normal(input.normal);
96
124
  let v = normalize(camera.viewPos - input.worldPos);${flip}
97
125
  ${gate}
@@ -132,18 +160,23 @@ function epilogue(renderClass: RenderClass, alphaMode: AlphaMode): string {
132
160
  const LIT = ` + rzLightsDiffuse(input.worldPos, n) * albedo`
133
161
  const ALBEDO = ` let albedo = tex_color;
134
162
  `
163
+ // The dissolve's burning edge, ADDED after the graph and after the lights —
164
+ // it is emission, not a surface colour, so nothing may ramp or shadow it. Zero
165
+ // on every fragment of every material that is not dissolving, which is what
166
+ // keeps this out of the way of a scene that never uses it.
167
+ const BURN = ` + RZ_BURN_COLOR * rz_burn`
135
168
  if (renderClass === "hair") {
136
169
  return `${ALBEDO} var outAlpha = ${alphaBase};
137
170
  if (IS_OVER_EYES) { outAlpha = ${alphaBase} * 0.25; }
138
171
 
139
172
  var out: FSOut;
140
- out.color = vec4f(final_color${LIT}, outAlpha);
173
+ out.color = vec4f(final_color${LIT}${BURN}, outAlpha);
141
174
  out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
142
175
  ${ID_WRITE} return out;
143
176
  `
144
177
  }
145
178
  return `${ALBEDO} var out: FSOut;
146
- out.color = vec4f(final_color${LIT}, ${alphaBase});
179
+ out.color = vec4f(final_color${LIT}${BURN}, ${alphaBase});
147
180
  out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
148
181
  ${ID_WRITE} return out;
149
182
  `
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ export {
17
17
  type GizmoDragEvent,
18
18
  type GizmoDragCallback,
19
19
  type GizmoDragKind,
20
+ type DissolveCycle,
20
21
  type EffectParamValue,
21
22
  type EffectResult,
22
23
  type CullDiagnostics,
@@ -45,6 +45,16 @@ struct RzSubject {
45
45
  center: vec3f,
46
46
  /** Bounding sphere: xyz centre, w radius. Deliberately generous — cull with it. */
47
47
  bounds: vec4f,
48
+ /**
49
+ * How much of this character is still THERE: 1 whole, 0 gone.
50
+ *
51
+ * What setModelDissolve last set on them, which the material pass has already
52
+ * acted on by the time an effect runs — so an effect drawing what LEAVES a
53
+ * dissolving body (sparks, ash, a ghost) reads the same number the body was
54
+ * taken apart with, rather than keeping a clock of its own and hoping the two
55
+ * agree. 1 on a scene that never dissolves anybody.
56
+ */
57
+ dissolve: f32,
48
58
  /** False past the end of the cast, and every field is then zero. */
49
59
  valid: bool,
50
60
  }
@@ -73,6 +83,7 @@ fn rzSubject(i: i32) -> RzSubject {
73
83
  if (!s.valid) { return s; }
74
84
  let b = i * 3;
75
85
  s.root = _rzCast[b].xyz;
86
+ s.dissolve = _rzCast[b].w;
76
87
  s.center = _rzCast[b + 1].xyz;
77
88
  s.bounds = _rzCast[b + 2];
78
89
  return s;