reze-engine 0.21.2 → 0.22.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.
package/src/vmd-loader.ts CHANGED
@@ -20,6 +20,18 @@ export interface VMDKeyFrame {
20
20
  morphFrames: MorphFrame[]
21
21
  }
22
22
 
23
+ /** One MMD camera keyframe. The camera looks at `target` from `distance` away, oriented by
24
+ * `rotation` (euler radians), with `fov` degrees. `interpolation` is 24 bytes: 6 channels
25
+ * (posX, posY, posZ, rotation, distance, fov) × 4 bezier bytes (x1, x2, y1, y2). */
26
+ export interface CameraKeyframe {
27
+ frame: number
28
+ distance: number
29
+ target: Vec3
30
+ rotation: Vec3 // euler radians
31
+ fov: number // degrees
32
+ interpolation: Uint8Array // 24 bytes
33
+ }
34
+
23
35
  export class VMDLoader {
24
36
  private view: DataView
25
37
  private offset = 0
@@ -46,6 +58,48 @@ export class VMDLoader {
46
58
  return loader.parse()
47
59
  }
48
60
 
61
+ /** Parse only the camera track (a dedicated camera VMD, or the camera block of any VMD).
62
+ * Returns [] if the file has no camera block (motion-only VMDs end after the morph block). */
63
+ static async loadCamera(url: string): Promise<CameraKeyframe[]> {
64
+ const loader = new VMDLoader(await fetch(url).then((r) => r.arrayBuffer()))
65
+ return loader.parseCamera()
66
+ }
67
+
68
+ static loadCameraFromBuffer(buffer: ArrayBuffer): CameraKeyframe[] {
69
+ return new VMDLoader(buffer).parseCamera()
70
+ }
71
+
72
+ // Seek past the bone + morph blocks (fixed record sizes) to the camera block, then read it.
73
+ // bone frame = 111 B (15 name + 4 frame + 12 pos + 16 rot + 64 interp); morph = 23 B
74
+ // (15 name + 4 frame + 4 weight); camera = 61 B.
75
+ private parseCamera(): CameraKeyframe[] {
76
+ this.offset = 0
77
+ const header = this.getString(30)
78
+ if (!header.startsWith("Vocaloid Motion Data")) throw new Error("Invalid VMD file header")
79
+ this.skip(20)
80
+ const boneCount = this.getUint32()
81
+ this.skip(boneCount * 111)
82
+ const morphCount = this.getUint32()
83
+ this.skip(morphCount * 23)
84
+ if (this.offset + 4 > this.view.buffer.byteLength) return [] // no camera block
85
+ const cameraCount = this.getUint32()
86
+
87
+ const frames: CameraKeyframe[] = []
88
+ for (let i = 0; i < cameraCount; i++) {
89
+ const frame = this.getUint32()
90
+ const distance = this.getFloat32()
91
+ const target = new Vec3(this.getFloat32(), this.getFloat32(), this.getFloat32())
92
+ const rotation = new Vec3(this.getFloat32(), this.getFloat32(), this.getFloat32())
93
+ const interpolation = new Uint8Array(24)
94
+ for (let j = 0; j < 24; j++) interpolation[j] = this.getUint8()
95
+ const fov = this.getUint32()
96
+ this.skip(1) // perspective flag (0 = perspective) — unused
97
+ frames.push({ frame, distance, target, rotation, fov, interpolation })
98
+ }
99
+ frames.sort((a, b) => a.frame - b.frame)
100
+ return frames
101
+ }
102
+
49
103
  private parse(): VMDKeyFrame[] {
50
104
  // Read header (30 bytes)
51
105
  const header = this.getString(30)