reze-engine 0.1.16 → 0.2.1

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.
@@ -0,0 +1,141 @@
1
+ import { Quat } from "./math";
2
+ export class VMDLoader {
3
+ constructor(buffer) {
4
+ this.offset = 0;
5
+ this.view = new DataView(buffer);
6
+ // Try to use Shift-JIS decoder, fallback to UTF-8 if not available
7
+ try {
8
+ this.decoder = new TextDecoder("shift-jis");
9
+ }
10
+ catch {
11
+ // Fallback to UTF-8 if Shift-JIS is not supported
12
+ this.decoder = new TextDecoder("utf-8");
13
+ }
14
+ }
15
+ static async load(url) {
16
+ const loader = new VMDLoader(await fetch(url).then((r) => r.arrayBuffer()));
17
+ return loader.parse();
18
+ }
19
+ static loadFromBuffer(buffer) {
20
+ const loader = new VMDLoader(buffer);
21
+ return loader.parse();
22
+ }
23
+ parse() {
24
+ // Read header (30 bytes)
25
+ const header = this.getString(30);
26
+ if (!header.startsWith("Vocaloid Motion Data")) {
27
+ throw new Error("Invalid VMD file header");
28
+ }
29
+ // Skip model name (20 bytes)
30
+ this.skip(20);
31
+ // Read bone frame count (4 bytes, u32 little endian)
32
+ const boneFrameCount = this.getUint32();
33
+ // Read all bone frames
34
+ const allBoneFrames = [];
35
+ for (let i = 0; i < boneFrameCount; i++) {
36
+ const boneFrame = this.readBoneFrame();
37
+ // Convert frame number to time (assuming 30 FPS like the Rust code)
38
+ const FRAME_RATE = 30.0;
39
+ const time = boneFrame.frame / FRAME_RATE;
40
+ allBoneFrames.push({ time, boneFrame });
41
+ }
42
+ // Group by time and convert to VMDKeyFrame format
43
+ // Sort by time first
44
+ allBoneFrames.sort((a, b) => a.time - b.time);
45
+ const keyFrames = [];
46
+ let currentTime = -1.0;
47
+ let currentBoneFrames = [];
48
+ for (const { time, boneFrame } of allBoneFrames) {
49
+ if (Math.abs(time - currentTime) > 0.001) {
50
+ // New time frame
51
+ if (currentBoneFrames.length > 0) {
52
+ keyFrames.push({
53
+ time: currentTime,
54
+ boneFrames: currentBoneFrames,
55
+ });
56
+ }
57
+ currentTime = time;
58
+ currentBoneFrames = [boneFrame];
59
+ }
60
+ else {
61
+ // Same time frame
62
+ currentBoneFrames.push(boneFrame);
63
+ }
64
+ }
65
+ // Add the last frame
66
+ if (currentBoneFrames.length > 0) {
67
+ keyFrames.push({
68
+ time: currentTime,
69
+ boneFrames: currentBoneFrames,
70
+ });
71
+ }
72
+ return keyFrames;
73
+ }
74
+ readBoneFrame() {
75
+ // Read bone name (15 bytes)
76
+ const nameBuffer = new Uint8Array(this.view.buffer, this.offset, 15);
77
+ this.offset += 15;
78
+ // Find the actual length of the bone name (stop at first null byte)
79
+ let nameLength = 15;
80
+ for (let i = 0; i < 15; i++) {
81
+ if (nameBuffer[i] === 0) {
82
+ nameLength = i;
83
+ break;
84
+ }
85
+ }
86
+ // Decode Shift-JIS bone name
87
+ let boneName;
88
+ try {
89
+ const nameSlice = nameBuffer.slice(0, nameLength);
90
+ boneName = this.decoder.decode(nameSlice);
91
+ }
92
+ catch {
93
+ // Fallback to lossy decoding if there were encoding errors
94
+ boneName = String.fromCharCode(...nameBuffer.slice(0, nameLength));
95
+ }
96
+ // Read frame number (4 bytes, little endian)
97
+ const frame = this.getUint32();
98
+ // Skip position (12 bytes: 3 x f32, little endian)
99
+ this.skip(12);
100
+ // Read rotation quaternion (16 bytes: 4 x f32, little endian)
101
+ const rotX = this.getFloat32();
102
+ const rotY = this.getFloat32();
103
+ const rotZ = this.getFloat32();
104
+ const rotW = this.getFloat32();
105
+ const rotation = new Quat(rotX, rotY, rotZ, rotW);
106
+ // Skip interpolation parameters (64 bytes)
107
+ this.skip(64);
108
+ return {
109
+ boneName,
110
+ frame,
111
+ rotation,
112
+ };
113
+ }
114
+ getUint32() {
115
+ if (this.offset + 4 > this.view.buffer.byteLength) {
116
+ throw new RangeError(`Offset ${this.offset} + 4 exceeds buffer bounds ${this.view.buffer.byteLength}`);
117
+ }
118
+ const v = this.view.getUint32(this.offset, true); // true = little endian
119
+ this.offset += 4;
120
+ return v;
121
+ }
122
+ getFloat32() {
123
+ if (this.offset + 4 > this.view.buffer.byteLength) {
124
+ throw new RangeError(`Offset ${this.offset} + 4 exceeds buffer bounds ${this.view.buffer.byteLength}`);
125
+ }
126
+ const v = this.view.getFloat32(this.offset, true); // true = little endian
127
+ this.offset += 4;
128
+ return v;
129
+ }
130
+ getString(len) {
131
+ const bytes = new Uint8Array(this.view.buffer, this.offset, len);
132
+ this.offset += len;
133
+ return String.fromCharCode(...bytes);
134
+ }
135
+ skip(bytes) {
136
+ if (this.offset + bytes > this.view.buffer.byteLength) {
137
+ throw new RangeError(`Offset ${this.offset} + ${bytes} exceeds buffer bounds ${this.view.buffer.byteLength}`);
138
+ }
139
+ this.offset += bytes;
140
+ }
141
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.1.16",
3
+ "version": "0.2.1",
4
4
  "description": "A WebGPU-based MMD model renderer",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",