reze-engine 0.3.4 → 0.3.6

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