reze-engine 0.13.5 → 0.14.0

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 (41) hide show
  1. package/README.md +104 -13
  2. package/dist/camera.d.ts +2 -0
  3. package/dist/camera.d.ts.map +1 -1
  4. package/dist/camera.js +17 -0
  5. package/dist/engine.d.ts +71 -2
  6. package/dist/engine.d.ts.map +1 -1
  7. package/dist/engine.js +730 -9
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/model.d.ts +6 -0
  11. package/dist/model.d.ts.map +1 -1
  12. package/dist/model.js +36 -1
  13. package/dist/shaders/materials/body.d.ts +1 -1
  14. package/dist/shaders/materials/cloth_rough.d.ts +1 -1
  15. package/dist/shaders/materials/cloth_smooth.d.ts +1 -1
  16. package/dist/shaders/materials/common.d.ts +1 -1
  17. package/dist/shaders/materials/common.js +2 -2
  18. package/dist/shaders/materials/default.d.ts +1 -1
  19. package/dist/shaders/materials/eye.d.ts +1 -1
  20. package/dist/shaders/materials/face.d.ts +1 -1
  21. package/dist/shaders/materials/hair.d.ts +1 -1
  22. package/dist/shaders/materials/metal.d.ts +1 -1
  23. package/dist/shaders/materials/stockings.d.ts +1 -1
  24. package/dist/shaders/passes/gizmo.d.ts +2 -0
  25. package/dist/shaders/passes/gizmo.d.ts.map +1 -0
  26. package/dist/shaders/passes/gizmo.js +75 -0
  27. package/dist/shaders/passes/pick.d.ts +1 -1
  28. package/dist/shaders/passes/pick.d.ts.map +1 -1
  29. package/dist/shaders/passes/pick.js +29 -5
  30. package/dist/shaders/passes/selection.d.ts +3 -0
  31. package/dist/shaders/passes/selection.d.ts.map +1 -0
  32. package/dist/shaders/passes/selection.js +65 -0
  33. package/package.json +1 -1
  34. package/src/camera.ts +14 -0
  35. package/src/engine.ts +831 -10
  36. package/src/index.ts +3 -0
  37. package/src/model.ts +38 -1
  38. package/src/shaders/materials/common.ts +2 -2
  39. package/src/shaders/passes/gizmo.ts +76 -0
  40. package/src/shaders/passes/pick.ts +29 -5
  41. package/src/shaders/passes/selection.ts +67 -0
package/src/index.ts CHANGED
@@ -9,6 +9,9 @@ export {
9
9
  type LoadModelFromFilesOptions,
10
10
  type MaterialPreset,
11
11
  type MaterialPresetMap,
12
+ type GizmoDragEvent,
13
+ type GizmoDragCallback,
14
+ type GizmoDragKind,
12
15
  } from "./engine"
13
16
  export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload"
14
17
  export { Model } from "./model"
package/src/model.ts CHANGED
@@ -491,6 +491,41 @@ export class Model {
491
491
  return this.skeleton
492
492
  }
493
493
 
494
+ // Direct bone local-transform accessors (used by interactive gizmo drag).
495
+ // Readers return the live runtime state — callers that want a snapshot for
496
+ // later comparison should `.clone()` the returned Quat / copy the Vec3.
497
+ getBoneLocalRotation(boneIndex: number): Quat {
498
+ return this.runtimeSkeleton.localRotations[boneIndex]
499
+ }
500
+
501
+ getBoneLocalTranslation(boneIndex: number): Vec3 {
502
+ return this.runtimeSkeleton.localTranslations[boneIndex]
503
+ }
504
+
505
+ // Raw absolute-local translation write. NOT equivalent to
506
+ // `moveBones({ name: v }, 0)` — moveBones treats the input as VMD-relative
507
+ // (offset from bind pose) and runs convertVMDTranslationToLocal() over it.
508
+ // Use this when you already have the final local translation (e.g. the
509
+ // gizmo's computed target). For rotation, just use rotateBones(..., 0).
510
+ setBoneLocalTranslation(boneIndex: number, v: Vec3): void {
511
+ const t = this.runtimeSkeleton.localTranslations[boneIndex]
512
+ t.x = v.x; t.y = v.y; t.z = v.z
513
+ this.tweenState.transActive[boneIndex] = 0
514
+ }
515
+
516
+ // When true, update() skips applyPoseFromClip, so whatever was last written to
517
+ // localRotations / localTranslations persists across frames. Used by gizmo drag
518
+ // and other direct-manipulation flows to prevent the currently-shown clip from
519
+ // overwriting manual edits each frame. Auto-cleared on play()/seek() so the user
520
+ // gets back to normal playback without having to manage this flag explicitly.
521
+ private clipApplySuspended = false
522
+ setClipApplySuspended(suspended: boolean): void {
523
+ this.clipApplySuspended = suspended
524
+ }
525
+ isClipApplySuspended(): boolean {
526
+ return this.clipApplySuspended
527
+ }
528
+
494
529
  // World bone origin (world matrix col3); unknown name → null
495
530
  getBoneWorldPosition(boneName: string): Vec3 | null {
496
531
  const idx = this.runtimeSkeleton.nameIndex[boneName]
@@ -981,6 +1016,7 @@ export class Model {
981
1016
  play(name: string): boolean
982
1017
  play(name: string, options?: AnimationPlayOptions): boolean
983
1018
  play(name?: string, options?: AnimationPlayOptions): void | boolean {
1019
+ this.clipApplySuspended = false
984
1020
  if (name === undefined) {
985
1021
  this.animationState.play()
986
1022
  return
@@ -1021,6 +1057,7 @@ export class Model {
1021
1057
 
1022
1058
  // Seek by absolute timeline seconds, not frame index.
1023
1059
  seek(seconds: number): void {
1060
+ this.clipApplySuspended = false
1024
1061
  this.animationState.seek(seconds)
1025
1062
  }
1026
1063
 
@@ -1166,7 +1203,7 @@ export class Model {
1166
1203
  this.animationState.update(deltaTime)
1167
1204
  const clip = this.animationState.getCurrentClip()
1168
1205
  const frame = this.animationState.getCurrentFrame()
1169
- if (clip !== null) {
1206
+ if (clip !== null && !this.clipApplySuspended) {
1170
1207
  this.applyPoseFromClip(clip, frame)
1171
1208
  }
1172
1209
 
@@ -69,7 +69,7 @@ struct LightVP { viewProj: mat4x4f, };
69
69
  `;
70
70
 
71
71
  // ─── Shadow sampler (3×3 PCF) ───────────────────────────────────────
72
- // 4096-map, normal-bias 0.08, depth-bias 0.001. Unrolled — Safari's Metal backend
72
+ // 2048-map, normal-bias 0.08, depth-bias 0.001. Unrolled — Safari's Metal backend
73
73
  // doesn't unroll nested shadow loops reliably, and the early out on back-facing
74
74
  // fragments saves 9 texture taps per skipped pixel.
75
75
 
@@ -82,7 +82,7 @@ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
82
82
  let ndc = lclip.xyz / max(lclip.w, 1e-6);
83
83
  let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
84
84
  let cmpZ = ndc.z - 0.001;
85
- let ts = 1.0 / 4096.0;
85
+ let ts = 1.0 / 2048.0;
86
86
  let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
87
87
  let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
88
88
  let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
@@ -0,0 +1,76 @@
1
+ // Transform gizmo — 3 translation axes + 3 rotation rings, drawn as thick
2
+ // ribbons. Per-segment perpendicular (no miter) → the "tick" look at each ring
3
+ // vertex is intentional.
4
+ //
5
+ // axisT per-vertex: 0 at the bone origin, 1 at the axis tip. For ring verts it
6
+ // is set to -1 as a "not an axis" flag. The FS uses axisT to:
7
+ // • fade + dash the inside-ring portion of each axis (not hittable, so the
8
+ // user can tell to only grab the outer stub for translation).
9
+ // • leave rings and the outer axis stub fully solid with only the edge-to-
10
+ // center alpha falloff.
11
+
12
+ export const GIZMO_SHADER_WGSL = /* wgsl */ `
13
+ struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _pad: f32 };
14
+ struct Transform {
15
+ model: mat4x4f,
16
+ viewport: vec2f,
17
+ thicknessPx: f32,
18
+ _pad: f32,
19
+ };
20
+ struct Color { rgba: vec4f };
21
+
22
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
23
+ @group(0) @binding(1) var<uniform> transform: Transform;
24
+ @group(1) @binding(0) var<uniform> col: Color;
25
+
26
+ struct VSOut {
27
+ @builtin(position) pos: vec4f,
28
+ @location(0) side: f32,
29
+ @location(1) axisT: f32,
30
+ }
31
+
32
+ @vertex fn vs(
33
+ @location(0) position: vec3f,
34
+ @location(1) segDir: vec3f,
35
+ @location(2) side: f32,
36
+ @location(3) axisT: f32,
37
+ ) -> VSOut {
38
+ let vp = camera.projection * camera.view * transform.model;
39
+ let c0 = vp * vec4f(position, 1.0);
40
+ let c1 = vp * vec4f(position + segDir, 1.0);
41
+ let w0 = max(abs(c0.w), 1e-6);
42
+ let w1 = max(abs(c1.w), 1e-6);
43
+ let s0 = (c0.xy / w0) * 0.5 * transform.viewport;
44
+ let s1 = (c1.xy / w1) * 0.5 * transform.viewport;
45
+ let tangent = normalize(s1 - s0);
46
+ let normalPx = vec2f(-tangent.y, tangent.x);
47
+ // Axes render thinner than rings. axisT < 0 → ring (full thickness); else → axis (reduced).
48
+ let thicknessMul = select(0.60, 1.0, axisT < 0.0);
49
+ let offsetPx = normalPx * side * transform.thicknessPx * 0.5 * thicknessMul;
50
+ let offsetClip = (offsetPx / (0.5 * transform.viewport)) * c0.w;
51
+ var out: VSOut;
52
+ out.pos = vec4f(c0.xy + offsetClip, c0.z, c0.w);
53
+ out.side = side;
54
+ out.axisT = axisT;
55
+ return out;
56
+ }
57
+
58
+ @fragment fn fs(in: VSOut) -> @location(0) vec4f {
59
+ // Center-bright, edges fade. abs(side) is 0 at ribbon center, 1 at ribbon edges.
60
+ let edge = 1.0 - smoothstep(0.55, 1.0, abs(in.side));
61
+ var alpha = col.rgba.a * edge;
62
+
63
+ // Dash + dim the inside-ring portion of axes. axisT is -1 for ring fragments,
64
+ // 0..1 along axes (0 at bone center, 1 at axis tip). Boundary 0.63 ≈ where the
65
+ // hit zone starts (ring radius 0.8 + 0.05 margin, over axis length 1.35).
66
+ let isInsideRingAxis = in.axisT >= 0.0 && in.axisT < 0.63;
67
+ if (isInsideRingAxis) {
68
+ // ~8 dash cycles inside the ring — readable without feeling busy.
69
+ let phase = fract(in.axisT * 12.0);
70
+ if (phase > 0.55) { discard; }
71
+ alpha = alpha * 0.40;
72
+ }
73
+
74
+ return vec4f(col.rgba.rgb, alpha);
75
+ }
76
+ `
@@ -1,4 +1,7 @@
1
- // GPU picking pass: encodes (modelIndex, materialIndex) as RG8 into a 1×1 readback target.
1
+ // GPU picking pass: encodes (modelIndex, materialIndex, dominantBoneIndex) as RGB8
2
+ // into a 1×1 readback target. Dominant bone = the joint with the largest skinning
3
+ // weight for the provoking vertex of the triangle (flat-interpolated to fragments).
4
+ // 8-bit bone range (0..255) covers standard MMD skeletons (~100–200 bones).
2
5
 
3
6
  export const PICK_SHADER_WGSL = /* wgsl */ `
4
7
  struct CameraUniforms {
@@ -18,23 +21,44 @@ struct PickId {
18
21
  @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
19
22
  @group(2) @binding(0) var<uniform> pickId: PickId;
20
23
 
24
+ struct VSOut {
25
+ @builtin(position) pos: vec4f,
26
+ @interpolate(flat) @location(0) boneId: u32,
27
+ }
28
+
21
29
  @vertex fn vs(
22
30
  @location(0) position: vec3f,
23
31
  @location(1) normal: vec3f,
24
32
  @location(2) uv: vec2f,
25
33
  @location(3) joints0: vec4<u32>,
26
34
  @location(4) weights0: vec4<f32>
27
- ) -> @builtin(position) vec4f {
35
+ ) -> VSOut {
28
36
  let pos4 = vec4f(position, 1.0);
29
37
  let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
30
38
  let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
31
39
  let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
32
40
  var sp = vec4f(0.0);
33
41
  for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
34
- return camera.projection * camera.view * vec4f(sp.xyz, 1.0);
42
+
43
+ // Dominant joint for this vertex — index of max weight component.
44
+ var maxW: f32 = nw.x;
45
+ var idx: u32 = joints0.x;
46
+ if (nw.y > maxW) { maxW = nw.y; idx = joints0.y; }
47
+ if (nw.z > maxW) { maxW = nw.z; idx = joints0.z; }
48
+ if (nw.w > maxW) { maxW = nw.w; idx = joints0.w; }
49
+
50
+ var out: VSOut;
51
+ out.pos = camera.projection * camera.view * vec4f(sp.xyz, 1.0);
52
+ out.boneId = idx;
53
+ return out;
35
54
  }
36
55
 
37
- @fragment fn fs() -> @location(0) vec4f {
38
- return vec4f(pickId.modelId / 255.0, pickId.materialId / 255.0, 0.0, 1.0);
56
+ @fragment fn fs(in: VSOut) -> @location(0) vec4f {
57
+ return vec4f(
58
+ pickId.modelId / 255.0,
59
+ pickId.materialId / 255.0,
60
+ f32(in.boneId) / 255.0,
61
+ 1.0
62
+ );
39
63
  }
40
64
  `
@@ -0,0 +1,67 @@
1
+ // Selection overlay — two screen-space passes that together draw a uniform
2
+ // pixel-thick outline around the selected material.
3
+ //
4
+ // Pass 1 (mask): render only the selected material's triangles into an r8
5
+ // texture (depth-always). Fragment outputs 1.0. No per-material uniforms;
6
+ // reuses camera + skinMats from the outline/main bind group layouts.
7
+ //
8
+ // Pass 2 (edge): fullscreen pass over the swapchain. For each pixel, sample
9
+ // the mask at center + 8 neighbours in a ring of `thickness` pixels. Emit
10
+ // yellow where center is empty but any neighbour is filled. Result: uniform
11
+ // screen-space thickness, traces the complete material boundary (including
12
+ // through-occluder regions), independent of mesh geometry or camera angle.
13
+
14
+ export const SELECTION_MASK_SHADER_WGSL = /* wgsl */ `
15
+ struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _pad: f32 };
16
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
17
+ @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
18
+
19
+ @vertex fn vs(
20
+ @location(0) position: vec3f,
21
+ @location(3) joints0: vec4<u32>,
22
+ @location(4) weights0: vec4<f32>
23
+ ) -> @builtin(position) vec4f {
24
+ let ws = weights0.x + weights0.y + weights0.z + weights0.w;
25
+ let inv = select(1.0, 1.0 / ws, ws > 0.0001);
26
+ let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * inv, ws > 0.0001);
27
+ var sp = vec4f(0.0);
28
+ for (var i = 0u; i < 4u; i++) {
29
+ sp += (skinMats[joints0[i]] * vec4f(position, 1.0)) * nw[i];
30
+ }
31
+ return camera.projection * camera.view * vec4f(sp.xyz, 1.0);
32
+ }
33
+
34
+ @fragment fn fs() -> @location(0) vec4f { return vec4f(1.0, 0.0, 0.0, 0.0); }
35
+ `
36
+
37
+ export const SELECTION_EDGE_SHADER_WGSL = /* wgsl */ `
38
+ @group(0) @binding(0) var maskTex: texture_2d<f32>;
39
+ @group(0) @binding(1) var maskSamp: sampler;
40
+ struct Params { thickness: f32, _pad0: f32, _pad1: f32, _pad2: f32 };
41
+ @group(0) @binding(2) var<uniform> params: Params;
42
+
43
+ @vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
44
+ let x = f32((vi & 1u) << 2u) - 1.0;
45
+ let y = f32((vi & 2u) << 1u) - 1.0;
46
+ return vec4f(x, y, 0.0, 1.0);
47
+ }
48
+
49
+ @fragment fn fs(@builtin(position) fragCoord: vec4f) -> @location(0) vec4f {
50
+ let dims = vec2f(textureDimensions(maskTex));
51
+ let uv = fragCoord.xy / dims;
52
+ let center = textureSample(maskTex, maskSamp, uv).r;
53
+ if (center > 0.5) { discard; }
54
+ let t = params.thickness / dims;
55
+ var m: f32 = 0.0;
56
+ m = max(m, textureSample(maskTex, maskSamp, uv + vec2f(-t.x, 0.0)).r);
57
+ m = max(m, textureSample(maskTex, maskSamp, uv + vec2f( t.x, 0.0)).r);
58
+ m = max(m, textureSample(maskTex, maskSamp, uv + vec2f( 0.0, -t.y)).r);
59
+ m = max(m, textureSample(maskTex, maskSamp, uv + vec2f( 0.0, t.y)).r);
60
+ m = max(m, textureSample(maskTex, maskSamp, uv + vec2f(-t.x, -t.y)).r);
61
+ m = max(m, textureSample(maskTex, maskSamp, uv + vec2f( t.x, -t.y)).r);
62
+ m = max(m, textureSample(maskTex, maskSamp, uv + vec2f(-t.x, t.y)).r);
63
+ m = max(m, textureSample(maskTex, maskSamp, uv + vec2f( t.x, t.y)).r);
64
+ if (m < 0.05) { discard; }
65
+ return vec4f(1.0, 1.0, 0.0, m);
66
+ }
67
+ `