reze-engine 0.2.19 → 0.3.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.
package/src/vmd-loader.ts CHANGED
@@ -1,179 +1,276 @@
1
- import { Quat } from "./math"
2
-
3
- export interface BoneFrame {
4
- boneName: string
5
- frame: number
6
- rotation: Quat
7
- }
8
-
9
- export interface VMDKeyFrame {
10
- time: number // in seconds
11
- boneFrames: BoneFrame[]
12
- }
13
-
14
- export class VMDLoader {
15
- private view: DataView
16
- private offset = 0
17
- private decoder: TextDecoder
18
-
19
- private constructor(buffer: ArrayBuffer) {
20
- this.view = new DataView(buffer)
21
- // Try to use Shift-JIS decoder, fallback to UTF-8 if not available
22
- try {
23
- this.decoder = new TextDecoder("shift-jis")
24
- } catch {
25
- // Fallback to UTF-8 if Shift-JIS is not supported
26
- this.decoder = new TextDecoder("utf-8")
27
- }
28
- }
29
-
30
- static async load(url: string): Promise<VMDKeyFrame[]> {
31
- const loader = new VMDLoader(await fetch(url).then((r) => r.arrayBuffer()))
32
- return loader.parse()
33
- }
34
-
35
- static loadFromBuffer(buffer: ArrayBuffer): VMDKeyFrame[] {
36
- const loader = new VMDLoader(buffer)
37
- return loader.parse()
38
- }
39
-
40
- private parse(): VMDKeyFrame[] {
41
- // Read header (30 bytes)
42
- const header = this.getString(30)
43
- if (!header.startsWith("Vocaloid Motion Data")) {
44
- throw new Error("Invalid VMD file header")
45
- }
46
-
47
- // Skip model name (20 bytes)
48
- this.skip(20)
49
-
50
- // Read bone frame count (4 bytes, u32 little endian)
51
- const boneFrameCount = this.getUint32()
52
-
53
- // Read all bone frames
54
- const allBoneFrames: Array<{ time: number; boneFrame: BoneFrame }> = []
55
-
56
- for (let i = 0; i < boneFrameCount; i++) {
57
- const boneFrame = this.readBoneFrame()
58
-
59
- // Convert frame number to time (assuming 30 FPS like the Rust code)
60
- const FRAME_RATE = 30.0
61
- const time = boneFrame.frame / FRAME_RATE
62
-
63
- allBoneFrames.push({ time, boneFrame })
64
- }
65
-
66
- // Group by time and convert to VMDKeyFrame format
67
- // Sort by time first
68
- allBoneFrames.sort((a, b) => a.time - b.time)
69
-
70
- const keyFrames: VMDKeyFrame[] = []
71
- let currentTime = -1.0
72
- let currentBoneFrames: BoneFrame[] = []
73
-
74
- for (const { time, boneFrame } of allBoneFrames) {
75
- if (Math.abs(time - currentTime) > 0.001) {
76
- // New time frame
77
- if (currentBoneFrames.length > 0) {
78
- keyFrames.push({
79
- time: currentTime,
80
- boneFrames: currentBoneFrames,
81
- })
82
- }
83
- currentTime = time
84
- currentBoneFrames = [boneFrame]
85
- } else {
86
- // Same time frame
87
- currentBoneFrames.push(boneFrame)
88
- }
89
- }
90
-
91
- // Add the last frame
92
- if (currentBoneFrames.length > 0) {
93
- keyFrames.push({
94
- time: currentTime,
95
- boneFrames: currentBoneFrames,
96
- })
97
- }
98
-
99
- return keyFrames
100
- }
101
-
102
- private readBoneFrame(): BoneFrame {
103
- // Read bone name (15 bytes)
104
- const nameBuffer = new Uint8Array(this.view.buffer, this.offset, 15)
105
- this.offset += 15
106
-
107
- // Find the actual length of the bone name (stop at first null byte)
108
- let nameLength = 15
109
- for (let i = 0; i < 15; i++) {
110
- if (nameBuffer[i] === 0) {
111
- nameLength = i
112
- break
113
- }
114
- }
115
-
116
- // Decode Shift-JIS bone name
117
- let boneName: string
118
- try {
119
- const nameSlice = nameBuffer.slice(0, nameLength)
120
- boneName = this.decoder.decode(nameSlice)
121
- } catch {
122
- // Fallback to lossy decoding if there were encoding errors
123
- boneName = String.fromCharCode(...nameBuffer.slice(0, nameLength))
124
- }
125
-
126
- // Read frame number (4 bytes, little endian)
127
- const frame = this.getUint32()
128
-
129
- // Skip position (12 bytes: 3 x f32, little endian)
130
- this.skip(12)
131
-
132
- // Read rotation quaternion (16 bytes: 4 x f32, little endian)
133
- const rotX = this.getFloat32()
134
- const rotY = this.getFloat32()
135
- const rotZ = this.getFloat32()
136
- const rotW = this.getFloat32()
137
- const rotation = new Quat(rotX, rotY, rotZ, rotW)
138
-
139
- // Skip interpolation parameters (64 bytes)
140
- this.skip(64)
141
-
142
- return {
143
- boneName,
144
- frame,
145
- rotation,
146
- }
147
- }
148
-
149
- private getUint32(): number {
150
- if (this.offset + 4 > this.view.buffer.byteLength) {
151
- throw new RangeError(`Offset ${this.offset} + 4 exceeds buffer bounds ${this.view.buffer.byteLength}`)
152
- }
153
- const v = this.view.getUint32(this.offset, true) // true = little endian
154
- this.offset += 4
155
- return v
156
- }
157
-
158
- private getFloat32(): number {
159
- if (this.offset + 4 > this.view.buffer.byteLength) {
160
- throw new RangeError(`Offset ${this.offset} + 4 exceeds buffer bounds ${this.view.buffer.byteLength}`)
161
- }
162
- const v = this.view.getFloat32(this.offset, true) // true = little endian
163
- this.offset += 4
164
- return v
165
- }
166
-
167
- private getString(len: number): string {
168
- const bytes = new Uint8Array(this.view.buffer, this.offset, len)
169
- this.offset += len
170
- return String.fromCharCode(...bytes)
171
- }
172
-
173
- private skip(bytes: number): void {
174
- if (this.offset + bytes > this.view.buffer.byteLength) {
175
- throw new RangeError(`Offset ${this.offset} + ${bytes} exceeds buffer bounds ${this.view.buffer.byteLength}`)
176
- }
177
- this.offset += bytes
178
- }
179
- }
1
+ import { Quat, Vec3 } from "./math"
2
+
3
+ export interface BoneFrame {
4
+ boneName: string
5
+ frame: number
6
+ rotation: Quat
7
+ translation: Vec3
8
+ interpolation: Uint8Array // 64 bytes of interpolation parameters
9
+ }
10
+
11
+ export interface MorphFrame {
12
+ morphName: string
13
+ frame: number
14
+ weight: number // 0.0 to 1.0
15
+ }
16
+
17
+ export interface VMDKeyFrame {
18
+ time: number // in seconds
19
+ boneFrames: BoneFrame[]
20
+ morphFrames: MorphFrame[]
21
+ }
22
+
23
+ export class VMDLoader {
24
+ private view: DataView
25
+ private offset = 0
26
+ private decoder: TextDecoder
27
+
28
+ private constructor(buffer: ArrayBuffer) {
29
+ this.view = new DataView(buffer)
30
+ // Try to use Shift-JIS decoder, fallback to UTF-8 if not available
31
+ try {
32
+ this.decoder = new TextDecoder("shift-jis")
33
+ } catch {
34
+ // Fallback to UTF-8 if Shift-JIS is not supported
35
+ this.decoder = new TextDecoder("utf-8")
36
+ }
37
+ }
38
+
39
+ static async load(url: string): Promise<VMDKeyFrame[]> {
40
+ const loader = new VMDLoader(await fetch(url).then((r) => r.arrayBuffer()))
41
+ return loader.parse()
42
+ }
43
+
44
+ static loadFromBuffer(buffer: ArrayBuffer): VMDKeyFrame[] {
45
+ const loader = new VMDLoader(buffer)
46
+ return loader.parse()
47
+ }
48
+
49
+ private parse(): VMDKeyFrame[] {
50
+ // Read header (30 bytes)
51
+ const header = this.getString(30)
52
+ if (!header.startsWith("Vocaloid Motion Data")) {
53
+ throw new Error("Invalid VMD file header")
54
+ }
55
+
56
+ // Skip model name (20 bytes)
57
+ this.skip(20)
58
+
59
+ // Read bone frame count (4 bytes, u32 little endian)
60
+ const boneFrameCount = this.getUint32()
61
+
62
+ // Read all bone frames
63
+ const allBoneFrames: Array<{ time: number; boneFrame: BoneFrame }> = []
64
+
65
+ for (let i = 0; i < boneFrameCount; i++) {
66
+ const boneFrame = this.readBoneFrame()
67
+
68
+ // Convert frame number to time (30 FPS)
69
+ const FRAME_RATE = 30.0
70
+ const time = boneFrame.frame / FRAME_RATE
71
+
72
+ allBoneFrames.push({ time, boneFrame })
73
+ }
74
+
75
+ // Read morph frame count (4 bytes, u32 little endian)
76
+ const morphFrameCount = this.getUint32()
77
+
78
+ // Read all morph frames
79
+ const allMorphFrames: Array<{ time: number; morphFrame: MorphFrame }> = []
80
+
81
+ for (let i = 0; i < morphFrameCount; i++) {
82
+ const morphFrame = this.readMorphFrame()
83
+
84
+ // Convert frame number to time (30 FPS)
85
+ const FRAME_RATE = 30.0
86
+ const time = morphFrame.frame / FRAME_RATE
87
+
88
+ allMorphFrames.push({ time, morphFrame })
89
+ }
90
+
91
+ // Combine all frames and group by time
92
+ const allFrames: Array<{ time: number; boneFrame?: BoneFrame; morphFrame?: MorphFrame }> = []
93
+ for (const { time, boneFrame } of allBoneFrames) {
94
+ allFrames.push({ time, boneFrame })
95
+ }
96
+ for (const { time, morphFrame } of allMorphFrames) {
97
+ allFrames.push({ time, morphFrame })
98
+ }
99
+
100
+ // Sort by time
101
+ allFrames.sort((a, b) => a.time - b.time)
102
+
103
+ // Group by time and convert to VMDKeyFrame format
104
+ const keyFrames: VMDKeyFrame[] = []
105
+ let currentTime = -1.0
106
+ let currentBoneFrames: BoneFrame[] = []
107
+ let currentMorphFrames: MorphFrame[] = []
108
+
109
+ for (const frame of allFrames) {
110
+ if (Math.abs(frame.time - currentTime) > 0.001) {
111
+ // New time frame
112
+ if (currentBoneFrames.length > 0 || currentMorphFrames.length > 0) {
113
+ keyFrames.push({
114
+ time: currentTime,
115
+ boneFrames: currentBoneFrames,
116
+ morphFrames: currentMorphFrames,
117
+ })
118
+ }
119
+ currentTime = frame.time
120
+ currentBoneFrames = frame.boneFrame ? [frame.boneFrame] : []
121
+ currentMorphFrames = frame.morphFrame ? [frame.morphFrame] : []
122
+ } else {
123
+ // Same time frame
124
+ if (frame.boneFrame) {
125
+ currentBoneFrames.push(frame.boneFrame)
126
+ }
127
+ if (frame.morphFrame) {
128
+ currentMorphFrames.push(frame.morphFrame)
129
+ }
130
+ }
131
+ }
132
+
133
+ // Add the last frame
134
+ if (currentBoneFrames.length > 0 || currentMorphFrames.length > 0) {
135
+ keyFrames.push({
136
+ time: currentTime,
137
+ boneFrames: currentBoneFrames,
138
+ morphFrames: currentMorphFrames,
139
+ })
140
+ }
141
+
142
+ return keyFrames
143
+ }
144
+
145
+ private readBoneFrame(): BoneFrame {
146
+ // Read bone name (15 bytes)
147
+ const nameBuffer = new Uint8Array(this.view.buffer, this.offset, 15)
148
+ this.offset += 15
149
+
150
+ // Find the actual length of the bone name (stop at first null byte)
151
+ let nameLength = 15
152
+ for (let i = 0; i < 15; i++) {
153
+ if (nameBuffer[i] === 0) {
154
+ nameLength = i
155
+ break
156
+ }
157
+ }
158
+
159
+ // Decode Shift-JIS bone name
160
+ let boneName: string
161
+ try {
162
+ const nameSlice = nameBuffer.slice(0, nameLength)
163
+ boneName = this.decoder.decode(nameSlice)
164
+ } catch {
165
+ // Fallback to lossy decoding if there were encoding errors
166
+ boneName = String.fromCharCode(...nameBuffer.slice(0, nameLength))
167
+ }
168
+
169
+ // Read frame number (4 bytes, little endian)
170
+ const frame = this.getUint32()
171
+
172
+ // Read position/translation (12 bytes: 3 x f32, little endian)
173
+ const posX = this.getFloat32()
174
+ const posY = this.getFloat32()
175
+ const posZ = this.getFloat32()
176
+ const translation = new Vec3(posX, posY, posZ)
177
+
178
+ // Read rotation quaternion (16 bytes: 4 x f32, little endian)
179
+ const rotX = this.getFloat32()
180
+ const rotY = this.getFloat32()
181
+ const rotZ = this.getFloat32()
182
+ const rotW = this.getFloat32()
183
+ const rotation = new Quat(rotX, rotY, rotZ, rotW)
184
+
185
+ // Read interpolation parameters (64 bytes)
186
+ const interpolation = new Uint8Array(64)
187
+ for (let i = 0; i < 64; i++) {
188
+ interpolation[i] = this.getUint8()
189
+ }
190
+
191
+ return {
192
+ boneName,
193
+ frame,
194
+ rotation,
195
+ translation,
196
+ interpolation,
197
+ }
198
+ }
199
+
200
+ private readMorphFrame(): MorphFrame {
201
+ // Read morph name (15 bytes)
202
+ const nameBuffer = new Uint8Array(this.view.buffer, this.offset, 15)
203
+ this.offset += 15
204
+
205
+ // Find the actual length of the morph name (stop at first null byte)
206
+ let nameLength = 15
207
+ for (let i = 0; i < 15; i++) {
208
+ if (nameBuffer[i] === 0) {
209
+ nameLength = i
210
+ break
211
+ }
212
+ }
213
+
214
+ // Decode Shift-JIS morph name
215
+ let morphName: string
216
+ try {
217
+ const nameSlice = nameBuffer.slice(0, nameLength)
218
+ morphName = this.decoder.decode(nameSlice)
219
+ } catch {
220
+ // Fallback to lossy decoding if there were encoding errors
221
+ morphName = String.fromCharCode(...nameBuffer.slice(0, nameLength))
222
+ }
223
+
224
+ // Read frame number (4 bytes, little endian)
225
+ const frame = this.getUint32()
226
+
227
+ // Read weight (4 bytes, f32, little endian)
228
+ const weight = this.getFloat32()
229
+
230
+ return {
231
+ morphName,
232
+ frame,
233
+ weight,
234
+ }
235
+ }
236
+
237
+ private getUint8(): number {
238
+ if (this.offset + 1 > this.view.buffer.byteLength) {
239
+ throw new RangeError(`Offset ${this.offset} + 1 exceeds buffer bounds ${this.view.buffer.byteLength}`)
240
+ }
241
+ const v = this.view.getUint8(this.offset)
242
+ this.offset += 1
243
+ return v
244
+ }
245
+
246
+ private getUint32(): number {
247
+ if (this.offset + 4 > this.view.buffer.byteLength) {
248
+ throw new RangeError(`Offset ${this.offset} + 4 exceeds buffer bounds ${this.view.buffer.byteLength}`)
249
+ }
250
+ const v = this.view.getUint32(this.offset, true) // true = little endian
251
+ this.offset += 4
252
+ return v
253
+ }
254
+
255
+ private getFloat32(): number {
256
+ if (this.offset + 4 > this.view.buffer.byteLength) {
257
+ throw new RangeError(`Offset ${this.offset} + 4 exceeds buffer bounds ${this.view.buffer.byteLength}`)
258
+ }
259
+ const v = this.view.getFloat32(this.offset, true) // true = little endian
260
+ this.offset += 4
261
+ return v
262
+ }
263
+
264
+ private getString(len: number): string {
265
+ const bytes = new Uint8Array(this.view.buffer, this.offset, len)
266
+ this.offset += len
267
+ return String.fromCharCode(...bytes)
268
+ }
269
+
270
+ private skip(bytes: number): void {
271
+ if (this.offset + bytes > this.view.buffer.byteLength) {
272
+ throw new RangeError(`Offset ${this.offset} + ${bytes} exceeds buffer bounds ${this.view.buffer.byteLength}`)
273
+ }
274
+ this.offset += bytes
275
+ }
276
+ }