reze-engine 0.3.5 → 0.3.7
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/README.md +0 -1
- package/dist/engine.d.ts +10 -26
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +261 -670
- package/dist/engine_r.d.ts +132 -0
- package/dist/engine_r.d.ts.map +1 -0
- package/dist/engine_r.js +1489 -0
- package/dist/engine_ts.d.ts +143 -0
- package/dist/engine_ts.d.ts.map +1 -0
- package/dist/engine_ts.js +1575 -0
- package/dist/ik-solver.d.ts +6 -0
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/ik-solver.js +98 -101
- package/dist/math.d.ts +0 -5
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +0 -55
- package/dist/model.d.ts +59 -13
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +415 -141
- package/dist/player.d.ts +6 -20
- package/dist/player.d.ts.map +1 -1
- package/dist/player.js +88 -191
- package/package.json +1 -1
- package/src/engine.ts +299 -729
- package/src/ik-solver.ts +106 -124
- package/src/math.ts +0 -74
- package/src/model.ts +516 -186
- package/src/player.ts +115 -210
package/src/player.ts
CHANGED
|
@@ -18,17 +18,15 @@ export class Player {
|
|
|
18
18
|
private frames: VMDKeyFrame[] = []
|
|
19
19
|
private boneTracks: Map<string, Array<{ boneFrame: BoneFrame; time: number }>> = new Map()
|
|
20
20
|
private morphTracks: Map<string, Array<{ morphFrame: MorphFrame; time: number }>> = new Map()
|
|
21
|
-
private
|
|
21
|
+
private _duration: number = 0
|
|
22
22
|
|
|
23
23
|
// Playback state
|
|
24
24
|
private isPlaying: boolean = false
|
|
25
25
|
private isPaused: boolean = false
|
|
26
|
-
private
|
|
26
|
+
private _currentTime: number = 0
|
|
27
27
|
|
|
28
28
|
// Timing
|
|
29
29
|
private startTime: number = 0 // Real-time when playback started
|
|
30
|
-
private pausedTime: number = 0 // Accumulated paused duration
|
|
31
|
-
private pauseStartTime: number = 0
|
|
32
30
|
|
|
33
31
|
/**
|
|
34
32
|
* Load VMD animation file
|
|
@@ -43,75 +41,57 @@ export class Player {
|
|
|
43
41
|
* Process frames into tracks
|
|
44
42
|
*/
|
|
45
43
|
private processFrames(): void {
|
|
46
|
-
//
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
})
|
|
44
|
+
// Helper to group frames by name and sort by time
|
|
45
|
+
const groupFrames = <T>(
|
|
46
|
+
items: Array<{ item: T; name: string; time: number }>
|
|
47
|
+
): Map<string, Array<{ item: T; time: number }>> => {
|
|
48
|
+
const tracks = new Map<string, Array<{ item: T; time: number }>>()
|
|
49
|
+
for (const { item, name, time } of items) {
|
|
50
|
+
if (!tracks.has(name)) tracks.set(name, [])
|
|
51
|
+
tracks.get(name)!.push({ item, time })
|
|
54
52
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const boneKeyFramesByBone = new Map<string, Array<{ boneFrame: BoneFrame; time: number }>>()
|
|
58
|
-
for (const { boneFrame, time } of allBoneKeyFrames) {
|
|
59
|
-
if (!boneKeyFramesByBone.has(boneFrame.boneName)) {
|
|
60
|
-
boneKeyFramesByBone.set(boneFrame.boneName, [])
|
|
53
|
+
for (const keyFrames of tracks.values()) {
|
|
54
|
+
keyFrames.sort((a, b) => a.time - b.time)
|
|
61
55
|
}
|
|
62
|
-
|
|
56
|
+
return tracks
|
|
63
57
|
}
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
59
|
+
// Collect all bone and morph frames
|
|
60
|
+
const boneItems: Array<{ item: BoneFrame; name: string; time: number }> = []
|
|
61
|
+
const morphItems: Array<{ item: MorphFrame; name: string; time: number }> = []
|
|
68
62
|
|
|
69
|
-
// Process morph frames
|
|
70
|
-
const allMorphKeyFrames: Array<{ morphFrame: MorphFrame; time: number }> = []
|
|
71
63
|
for (const keyFrame of this.frames) {
|
|
64
|
+
for (const boneFrame of keyFrame.boneFrames) {
|
|
65
|
+
boneItems.push({ item: boneFrame, name: boneFrame.boneName, time: keyFrame.time })
|
|
66
|
+
}
|
|
72
67
|
for (const morphFrame of keyFrame.morphFrames) {
|
|
73
|
-
|
|
74
|
-
morphFrame,
|
|
75
|
-
time: keyFrame.time,
|
|
76
|
-
})
|
|
68
|
+
morphItems.push({ item: morphFrame, name: morphFrame.morphName, time: keyFrame.time })
|
|
77
69
|
}
|
|
78
70
|
}
|
|
79
71
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
72
|
+
// Transform to expected format
|
|
73
|
+
this.boneTracks = new Map()
|
|
74
|
+
for (const [name, frames] of groupFrames(boneItems).entries()) {
|
|
75
|
+
this.boneTracks.set(
|
|
76
|
+
name,
|
|
77
|
+
frames.map((f) => ({ boneFrame: f.item, time: f.time }))
|
|
78
|
+
)
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
this.morphTracks = new Map()
|
|
82
|
+
for (const [name, frames] of groupFrames(morphItems).entries()) {
|
|
83
|
+
this.morphTracks.set(
|
|
84
|
+
name,
|
|
85
|
+
frames.map((f) => ({ morphFrame: f.item, time: f.time }))
|
|
86
|
+
)
|
|
90
87
|
}
|
|
91
88
|
|
|
92
|
-
//
|
|
93
|
-
this.boneTracks
|
|
94
|
-
this.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
for (const keyFrames of this.boneTracks.values()) {
|
|
99
|
-
if (keyFrames.length > 0) {
|
|
100
|
-
const lastTime = keyFrames[keyFrames.length - 1].time
|
|
101
|
-
if (lastTime > maxFrameTime) {
|
|
102
|
-
maxFrameTime = lastTime
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
for (const keyFrames of this.morphTracks.values()) {
|
|
107
|
-
if (keyFrames.length > 0) {
|
|
108
|
-
const lastTime = keyFrames[keyFrames.length - 1].time
|
|
109
|
-
if (lastTime > maxFrameTime) {
|
|
110
|
-
maxFrameTime = lastTime
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
this.duration = maxFrameTime > 0 ? maxFrameTime : 0
|
|
89
|
+
// Calculate duration from all tracks
|
|
90
|
+
const allTracks = [...this.boneTracks.values(), ...this.morphTracks.values()]
|
|
91
|
+
this._duration = allTracks.reduce((max, keyFrames) => {
|
|
92
|
+
const lastTime = keyFrames[keyFrames.length - 1]?.time ?? 0
|
|
93
|
+
return Math.max(max, lastTime)
|
|
94
|
+
}, 0)
|
|
115
95
|
}
|
|
116
96
|
|
|
117
97
|
/**
|
|
@@ -121,16 +101,8 @@ export class Player {
|
|
|
121
101
|
play(): void {
|
|
122
102
|
if (this.frames.length === 0) return
|
|
123
103
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
this.isPaused = false
|
|
127
|
-
// Adjust start time so current time calculation continues smoothly
|
|
128
|
-
this.startTime = performance.now() - this.currentTime * 1000
|
|
129
|
-
} else {
|
|
130
|
-
// Start from beginning or current seek position
|
|
131
|
-
this.startTime = performance.now() - this.currentTime * 1000
|
|
132
|
-
this.pausedTime = 0
|
|
133
|
-
}
|
|
104
|
+
this.isPaused = false
|
|
105
|
+
this.startTime = performance.now() - this._currentTime * 1000
|
|
134
106
|
|
|
135
107
|
this.isPlaying = true
|
|
136
108
|
}
|
|
@@ -140,9 +112,7 @@ export class Player {
|
|
|
140
112
|
*/
|
|
141
113
|
pause(): void {
|
|
142
114
|
if (!this.isPlaying || this.isPaused) return
|
|
143
|
-
|
|
144
115
|
this.isPaused = true
|
|
145
|
-
this.pauseStartTime = performance.now()
|
|
146
116
|
}
|
|
147
117
|
|
|
148
118
|
/**
|
|
@@ -151,22 +121,19 @@ export class Player {
|
|
|
151
121
|
stop(): void {
|
|
152
122
|
this.isPlaying = false
|
|
153
123
|
this.isPaused = false
|
|
154
|
-
this.
|
|
124
|
+
this._currentTime = 0
|
|
155
125
|
this.startTime = 0
|
|
156
|
-
this.pausedTime = 0
|
|
157
126
|
}
|
|
158
127
|
|
|
159
128
|
/**
|
|
160
129
|
* Seek to specific time
|
|
161
130
|
*/
|
|
162
131
|
seek(time: number): void {
|
|
163
|
-
const clampedTime = Math.max(0, Math.min(time, this.
|
|
164
|
-
this.
|
|
132
|
+
const clampedTime = Math.max(0, Math.min(time, this._duration))
|
|
133
|
+
this._currentTime = clampedTime
|
|
165
134
|
|
|
166
|
-
// Adjust start time if playing
|
|
167
135
|
if (this.isPlaying && !this.isPaused) {
|
|
168
136
|
this.startTime = performance.now() - clampedTime * 1000
|
|
169
|
-
this.pausedTime = 0
|
|
170
137
|
}
|
|
171
138
|
}
|
|
172
139
|
|
|
@@ -181,21 +148,21 @@ export class Player {
|
|
|
181
148
|
|
|
182
149
|
// If paused, return current pose at paused time (no time update)
|
|
183
150
|
if (this.isPaused) {
|
|
184
|
-
return this.getPoseAtTime(this.
|
|
151
|
+
return this.getPoseAtTime(this._currentTime)
|
|
185
152
|
}
|
|
186
153
|
|
|
187
154
|
// Calculate current animation time
|
|
188
155
|
const elapsedSeconds = (currentRealTime - this.startTime) / 1000
|
|
189
|
-
this.
|
|
156
|
+
this._currentTime = elapsedSeconds
|
|
190
157
|
|
|
191
158
|
// Check if animation ended
|
|
192
|
-
if (this.
|
|
193
|
-
this.
|
|
159
|
+
if (this._currentTime >= this._duration) {
|
|
160
|
+
this._currentTime = this._duration
|
|
194
161
|
this.pause() // Auto-pause at end
|
|
195
|
-
return this.getPoseAtTime(this.
|
|
162
|
+
return this.getPoseAtTime(this._currentTime)
|
|
196
163
|
}
|
|
197
164
|
|
|
198
|
-
return this.getPoseAtTime(this.
|
|
165
|
+
return this.getPoseAtTime(this._currentTime)
|
|
199
166
|
}
|
|
200
167
|
|
|
201
168
|
/**
|
|
@@ -208,136 +175,86 @@ export class Player {
|
|
|
208
175
|
morphWeights: new Map(),
|
|
209
176
|
}
|
|
210
177
|
|
|
211
|
-
//
|
|
212
|
-
const
|
|
213
|
-
let left = 0
|
|
214
|
-
|
|
178
|
+
// Generic binary search for upper bound
|
|
179
|
+
const upperBound = <T extends { time: number }>(time: number, keyFrames: T[]): number => {
|
|
180
|
+
let left = 0,
|
|
181
|
+
right = keyFrames.length
|
|
215
182
|
while (left < right) {
|
|
216
183
|
const mid = Math.floor((left + right) / 2)
|
|
217
|
-
if (keyFrames[mid].time <= time)
|
|
218
|
-
|
|
219
|
-
} else {
|
|
220
|
-
right = mid
|
|
221
|
-
}
|
|
184
|
+
if (keyFrames[mid].time <= time) left = mid + 1
|
|
185
|
+
else right = mid
|
|
222
186
|
}
|
|
223
187
|
return left
|
|
224
188
|
}
|
|
225
189
|
|
|
226
|
-
// Process
|
|
190
|
+
// Process bone tracks
|
|
227
191
|
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
228
192
|
if (keyFrames.length === 0) continue
|
|
229
193
|
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
const clampedFrameTime = Math.max(startTime, Math.min(endTime, time))
|
|
234
|
-
|
|
235
|
-
const upperBoundIndex = upperBoundFrameIndex(clampedFrameTime, keyFrames)
|
|
236
|
-
const upperBoundIndexMinusOne = upperBoundIndex - 1
|
|
237
|
-
|
|
238
|
-
if (upperBoundIndexMinusOne < 0) continue
|
|
194
|
+
const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time))
|
|
195
|
+
const idx = upperBound(clampedTime, keyFrames) - 1
|
|
196
|
+
if (idx < 0) continue
|
|
239
197
|
|
|
240
|
-
const
|
|
241
|
-
const
|
|
198
|
+
const frameA = keyFrames[idx].boneFrame
|
|
199
|
+
const frameB = keyFrames[idx + 1]?.boneFrame
|
|
242
200
|
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
pose.
|
|
246
|
-
pose.boneTranslations.set(boneName, boneFrameA.translation)
|
|
201
|
+
if (!frameB) {
|
|
202
|
+
pose.boneRotations.set(boneName, frameA.rotation)
|
|
203
|
+
pose.boneTranslations.set(boneName, frameA.translation)
|
|
247
204
|
} else {
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
gradient
|
|
261
|
-
)
|
|
262
|
-
const interpolatedRotation = Quat.slerp(boneFrameA.rotation, boneFrameB.rotation, rotWeight)
|
|
263
|
-
|
|
264
|
-
// Interpolate translation using Bezier (separate curves for X, Y, Z)
|
|
265
|
-
const xWeight = bezierInterpolate(
|
|
266
|
-
interp[0] / 127, // X_x1
|
|
267
|
-
interp[8] / 127, // X_x2
|
|
268
|
-
interp[4] / 127, // X_y1
|
|
269
|
-
interp[12] / 127, // X_y2
|
|
270
|
-
gradient
|
|
271
|
-
)
|
|
272
|
-
const yWeight = bezierInterpolate(
|
|
273
|
-
interp[16] / 127, // Y_x1
|
|
274
|
-
interp[24] / 127, // Y_x2
|
|
275
|
-
interp[20] / 127, // Y_y1
|
|
276
|
-
interp[28] / 127, // Y_y2
|
|
277
|
-
gradient
|
|
278
|
-
)
|
|
279
|
-
const zWeight = bezierInterpolate(
|
|
280
|
-
interp[32] / 127, // Z_x1
|
|
281
|
-
interp[40] / 127, // Z_x2
|
|
282
|
-
interp[36] / 127, // Z_y1
|
|
283
|
-
interp[44] / 127, // Z_y2
|
|
284
|
-
gradient
|
|
205
|
+
const timeA = keyFrames[idx].time
|
|
206
|
+
const timeB = keyFrames[idx + 1].time
|
|
207
|
+
const gradient = (clampedTime - timeA) / (timeB - timeA)
|
|
208
|
+
const interp = frameB.interpolation
|
|
209
|
+
|
|
210
|
+
pose.boneRotations.set(
|
|
211
|
+
boneName,
|
|
212
|
+
Quat.slerp(
|
|
213
|
+
frameA.rotation,
|
|
214
|
+
frameB.rotation,
|
|
215
|
+
bezierInterpolate(interp[0] / 127, interp[1] / 127, interp[2] / 127, interp[3] / 127, gradient)
|
|
216
|
+
)
|
|
285
217
|
)
|
|
286
218
|
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
219
|
+
const lerp = (a: number, b: number, w: number) => a + (b - a) * w
|
|
220
|
+
const getWeight = (offset: number) =>
|
|
221
|
+
bezierInterpolate(
|
|
222
|
+
interp[offset] / 127,
|
|
223
|
+
interp[offset + 8] / 127,
|
|
224
|
+
interp[offset + 4] / 127,
|
|
225
|
+
interp[offset + 12] / 127,
|
|
226
|
+
gradient
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
pose.boneTranslations.set(
|
|
230
|
+
boneName,
|
|
231
|
+
new Vec3(
|
|
232
|
+
lerp(frameA.translation.x, frameB.translation.x, getWeight(0)),
|
|
233
|
+
lerp(frameA.translation.y, frameB.translation.y, getWeight(16)),
|
|
234
|
+
lerp(frameA.translation.z, frameB.translation.z, getWeight(32))
|
|
235
|
+
)
|
|
291
236
|
)
|
|
292
|
-
|
|
293
|
-
pose.boneRotations.set(boneName, interpolatedRotation)
|
|
294
|
-
pose.boneTranslations.set(boneName, interpolatedTranslation)
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Helper to find upper bound index for morph frames
|
|
299
|
-
const upperBoundMorphIndex = (time: number, keyFrames: Array<{ morphFrame: MorphFrame; time: number }>): number => {
|
|
300
|
-
let left = 0
|
|
301
|
-
let right = keyFrames.length
|
|
302
|
-
while (left < right) {
|
|
303
|
-
const mid = Math.floor((left + right) / 2)
|
|
304
|
-
if (keyFrames[mid].time <= time) {
|
|
305
|
-
left = mid + 1
|
|
306
|
-
} else {
|
|
307
|
-
right = mid
|
|
308
|
-
}
|
|
309
237
|
}
|
|
310
|
-
return left
|
|
311
238
|
}
|
|
312
239
|
|
|
313
|
-
// Process
|
|
240
|
+
// Process morph tracks
|
|
314
241
|
for (const [morphName, keyFrames] of this.morphTracks.entries()) {
|
|
315
242
|
if (keyFrames.length === 0) continue
|
|
316
243
|
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
const clampedFrameTime = Math.max(startTime, Math.min(endTime, time))
|
|
321
|
-
|
|
322
|
-
const upperBoundIndex = upperBoundMorphIndex(clampedFrameTime, keyFrames)
|
|
323
|
-
const upperBoundIndexMinusOne = upperBoundIndex - 1
|
|
324
|
-
|
|
325
|
-
if (upperBoundIndexMinusOne < 0) continue
|
|
244
|
+
const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time))
|
|
245
|
+
const idx = upperBound(clampedTime, keyFrames) - 1
|
|
246
|
+
if (idx < 0) continue
|
|
326
247
|
|
|
327
|
-
const
|
|
328
|
-
const
|
|
248
|
+
const frameA = keyFrames[idx].morphFrame
|
|
249
|
+
const frameB = keyFrames[idx + 1]?.morphFrame
|
|
329
250
|
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
pose.morphWeights.set(morphName, morphFrameA.weight)
|
|
251
|
+
if (!frameB) {
|
|
252
|
+
pose.morphWeights.set(morphName, frameA.weight)
|
|
333
253
|
} else {
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
const interpolatedWeight = morphFrameA.weight + (morphFrameB.weight - morphFrameA.weight) * gradient
|
|
339
|
-
|
|
340
|
-
pose.morphWeights.set(morphName, interpolatedWeight)
|
|
254
|
+
const timeA = keyFrames[idx].time
|
|
255
|
+
const timeB = keyFrames[idx + 1].time
|
|
256
|
+
const gradient = (clampedTime - timeA) / (timeB - timeA)
|
|
257
|
+
pose.morphWeights.set(morphName, frameA.weight + (frameB.weight - frameA.weight) * gradient)
|
|
341
258
|
}
|
|
342
259
|
}
|
|
343
260
|
|
|
@@ -349,37 +266,25 @@ export class Player {
|
|
|
349
266
|
*/
|
|
350
267
|
getProgress(): AnimationProgress {
|
|
351
268
|
return {
|
|
352
|
-
current: this.
|
|
353
|
-
duration: this.
|
|
354
|
-
percentage: this.
|
|
269
|
+
current: this._currentTime,
|
|
270
|
+
duration: this._duration,
|
|
271
|
+
percentage: this._duration > 0 ? (this._currentTime / this._duration) * 100 : 0,
|
|
355
272
|
}
|
|
356
273
|
}
|
|
357
274
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
*/
|
|
361
|
-
getCurrentTime(): number {
|
|
362
|
-
return this.currentTime
|
|
275
|
+
get currentTime(): number {
|
|
276
|
+
return this._currentTime
|
|
363
277
|
}
|
|
364
278
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
*/
|
|
368
|
-
getDuration(): number {
|
|
369
|
-
return this.duration
|
|
279
|
+
get duration(): number {
|
|
280
|
+
return this._duration
|
|
370
281
|
}
|
|
371
282
|
|
|
372
|
-
|
|
373
|
-
* Check if playing
|
|
374
|
-
*/
|
|
375
|
-
isPlayingState(): boolean {
|
|
283
|
+
get isPlayingState(): boolean {
|
|
376
284
|
return this.isPlaying && !this.isPaused
|
|
377
285
|
}
|
|
378
286
|
|
|
379
|
-
|
|
380
|
-
* Check if paused
|
|
381
|
-
*/
|
|
382
|
-
isPausedState(): boolean {
|
|
287
|
+
get isPausedState(): boolean {
|
|
383
288
|
return this.isPaused
|
|
384
289
|
}
|
|
385
290
|
}
|