reze-engine 0.3.5 → 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/model.ts CHANGED
@@ -279,35 +279,24 @@ export class Model {
279
279
  }
280
280
 
281
281
  private initializeRotTweenBuffers(): void {
282
- const n = this.skeleton.bones.length
283
- this.rotTweenState = {
284
- active: new Uint8Array(n),
285
- startQuat: new Float32Array(n * 4),
286
- targetQuat: new Float32Array(n * 4),
287
- startTimeMs: new Float32Array(n),
288
- durationMs: new Float32Array(n),
289
- }
282
+ this.rotTweenState = this.createTweenState(this.skeleton.bones.length, 4, 4)
290
283
  }
291
284
 
292
285
  private initializeTransTweenBuffers(): void {
293
- const n = this.skeleton.bones.length
294
- this.transTweenState = {
295
- active: new Uint8Array(n),
296
- startVec: new Float32Array(n * 3),
297
- targetVec: new Float32Array(n * 3),
298
- startTimeMs: new Float32Array(n),
299
- durationMs: new Float32Array(n),
300
- }
286
+ this.transTweenState = this.createTweenState(this.skeleton.bones.length, 3, 3)
301
287
  }
302
288
 
303
289
  private initializeMorphTweenBuffers(): void {
304
- const n = this.morphing.morphs.length
305
- this.morphTweenState = {
306
- active: new Uint8Array(n),
307
- startWeight: new Float32Array(n),
308
- targetWeight: new Float32Array(n),
309
- startTimeMs: new Float32Array(n),
310
- durationMs: new Float32Array(n),
290
+ this.morphTweenState = this.createTweenState(this.morphing.morphs.length, 1, 1)
291
+ }
292
+
293
+ private createTweenState(count: number, startSize: number, targetSize: number): any {
294
+ return {
295
+ active: new Uint8Array(count),
296
+ startQuat: new Float32Array(count * startSize),
297
+ targetQuat: new Float32Array(count * targetSize),
298
+ startTimeMs: new Float32Array(count),
299
+ durationMs: new Float32Array(count),
311
300
  }
312
301
  }
313
302
 
@@ -410,33 +399,22 @@ export class Model {
410
399
  return hasActiveTweens
411
400
  }
412
401
 
413
- // Get interleaved vertex data for GPU upload
414
- // Format: [x,y,z, nx,ny,nz, u,v, x,y,z, nx,ny,nz, u,v, ...]
415
402
  getVertices(): Float32Array<ArrayBuffer> {
416
403
  return this.vertexData
417
404
  }
418
405
 
419
- // Get texture information
420
406
  getTextures(): Texture[] {
421
407
  return this.textures
422
408
  }
423
409
 
424
- // Get material information
425
410
  getMaterials(): Material[] {
426
411
  return this.materials
427
412
  }
428
413
 
429
- // Get vertex count
430
- getVertexCount(): number {
431
- return this.vertexCount
432
- }
433
-
434
- // Get index data for GPU upload
435
414
  getIndices(): Uint32Array<ArrayBuffer> {
436
415
  return this.indexData
437
416
  }
438
417
 
439
- // Accessors for skeleton/skinning
440
418
  getSkeleton(): Skeleton {
441
419
  return this.skeleton
442
420
  }
@@ -445,7 +423,6 @@ export class Model {
445
423
  return this.skinning
446
424
  }
447
425
 
448
- // Accessors for physics data
449
426
  getRigidbodies(): Rigidbody[] {
450
427
  return this.rigidbodies
451
428
  }
@@ -454,7 +431,6 @@ export class Model {
454
431
  return this.joints
455
432
  }
456
433
 
457
- // Accessors for morphing
458
434
  getMorphing(): Morphing {
459
435
  return this.morphing
460
436
  }
@@ -465,10 +441,6 @@ export class Model {
465
441
 
466
442
  // ------- Bone helpers (public API) -------
467
443
 
468
- getBoneNames(): string[] {
469
- return this.skeleton.bones.map((b) => b.name)
470
- }
471
-
472
444
  rotateBones(names: string[], quats: Quat[], durationMs?: number): void {
473
445
  const state = this.rotTweenState
474
446
  const normalized = quats.map((q) => q.normalize())
@@ -646,10 +618,6 @@ export class Model {
646
618
  return this.skeleton.inverseBindMatrices
647
619
  }
648
620
 
649
- getMorphNames(): string[] {
650
- return this.morphing.morphs.map((m) => m.name)
651
- }
652
-
653
621
  setMorphWeight(name: string, weight: number, durationMs?: number): void {
654
622
  const idx = this.runtimeMorph.nameIndex[name] ?? -1
655
623
  if (idx < 0 || idx >= this.runtimeMorph.weights.length) return
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 duration: number = 0
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 currentTime: number = 0
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
- // Process bone frames
47
- const allBoneKeyFrames: Array<{ boneFrame: BoneFrame; time: number }> = []
48
- for (const keyFrame of this.frames) {
49
- for (const boneFrame of keyFrame.boneFrames) {
50
- allBoneKeyFrames.push({
51
- boneFrame,
52
- time: keyFrame.time,
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
- boneKeyFramesByBone.get(boneFrame.boneName)!.push({ boneFrame, time })
56
+ return tracks
63
57
  }
64
58
 
65
- for (const keyFrames of boneKeyFramesByBone.values()) {
66
- keyFrames.sort((a, b) => a.time - b.time)
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
- allMorphKeyFrames.push({
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
- const morphKeyFramesByMorph = new Map<string, Array<{ morphFrame: MorphFrame; time: number }>>()
81
- for (const { morphFrame, time } of allMorphKeyFrames) {
82
- if (!morphKeyFramesByMorph.has(morphFrame.morphName)) {
83
- morphKeyFramesByMorph.set(morphFrame.morphName, [])
84
- }
85
- 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
+ )
86
79
  }
87
80
 
88
- for (const keyFrames of morphKeyFramesByMorph.values()) {
89
- 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
+ )
90
87
  }
91
88
 
92
- // Store tracks
93
- this.boneTracks = boneKeyFramesByBone
94
- this.morphTracks = morphKeyFramesByMorph
95
-
96
- // Calculate animation duration from max frame time
97
- let maxFrameTime = 0
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
- if (this.isPaused) {
125
- // Resume from paused position - don't adjust time, just continue from where we paused
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.currentTime = 0
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.duration))
164
- this.currentTime = clampedTime
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.currentTime)
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.currentTime = elapsedSeconds
156
+ this._currentTime = elapsedSeconds
190
157
 
191
158
  // Check if animation ended
192
- if (this.currentTime >= this.duration) {
193
- this.currentTime = this.duration
159
+ if (this._currentTime >= this._duration) {
160
+ this._currentTime = this._duration
194
161
  this.pause() // Auto-pause at end
195
- return this.getPoseAtTime(this.currentTime)
162
+ return this.getPoseAtTime(this._currentTime)
196
163
  }
197
164
 
198
- return this.getPoseAtTime(this.currentTime)
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
- // Helper to find upper bound index (binary search)
212
- const upperBoundFrameIndex = (time: number, keyFrames: Array<{ boneFrame: BoneFrame; time: number }>): number => {
213
- let left = 0
214
- 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
215
182
  while (left < right) {
216
183
  const mid = Math.floor((left + right) / 2)
217
- if (keyFrames[mid].time <= time) {
218
- left = mid + 1
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 each bone track
190
+ // Process bone tracks
227
191
  for (const [boneName, keyFrames] of this.boneTracks.entries()) {
228
192
  if (keyFrames.length === 0) continue
229
193
 
230
- // Clamp frame time to track range
231
- const startTime = keyFrames[0].time
232
- const endTime = keyFrames[keyFrames.length - 1].time
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 timeB = keyFrames[upperBoundIndex]?.time
241
- const boneFrameA = keyFrames[upperBoundIndexMinusOne].boneFrame
198
+ const frameA = keyFrames[idx].boneFrame
199
+ const frameB = keyFrames[idx + 1]?.boneFrame
242
200
 
243
- if (timeB === undefined) {
244
- // Last keyframe or beyond - use the last keyframe value
245
- pose.boneRotations.set(boneName, boneFrameA.rotation)
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
- // Interpolate between two keyframes
249
- const timeA = keyFrames[upperBoundIndexMinusOne].time
250
- const boneFrameB = keyFrames[upperBoundIndex].boneFrame
251
- const gradient = (clampedFrameTime - timeA) / (timeB - timeA)
252
-
253
- // Interpolate rotation using Bezier
254
- const interp = boneFrameB.interpolation
255
- const rotWeight = bezierInterpolate(
256
- interp[0] / 127, // x1
257
- interp[1] / 127, // x2
258
- interp[2] / 127, // y1
259
- interp[3] / 127, // y2
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 interpolatedTranslation = new Vec3(
288
- boneFrameA.translation.x + (boneFrameB.translation.x - boneFrameA.translation.x) * xWeight,
289
- boneFrameA.translation.y + (boneFrameB.translation.y - boneFrameA.translation.y) * yWeight,
290
- 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
+ )
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 each morph track
240
+ // Process morph tracks
314
241
  for (const [morphName, keyFrames] of this.morphTracks.entries()) {
315
242
  if (keyFrames.length === 0) continue
316
243
 
317
- // Clamp frame time to track range
318
- const startTime = keyFrames[0].time
319
- const endTime = keyFrames[keyFrames.length - 1].time
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 timeB = keyFrames[upperBoundIndex]?.time
328
- const morphFrameA = keyFrames[upperBoundIndexMinusOne].morphFrame
248
+ const frameA = keyFrames[idx].morphFrame
249
+ const frameB = keyFrames[idx + 1]?.morphFrame
329
250
 
330
- if (timeB === undefined) {
331
- // Last keyframe or beyond - use the last keyframe value
332
- pose.morphWeights.set(morphName, morphFrameA.weight)
251
+ if (!frameB) {
252
+ pose.morphWeights.set(morphName, frameA.weight)
333
253
  } else {
334
- // Linear interpolation between two keyframes
335
- const timeA = keyFrames[upperBoundIndexMinusOne].time
336
- const morphFrameB = keyFrames[upperBoundIndex].morphFrame
337
- const gradient = (clampedFrameTime - timeA) / (timeB - timeA)
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.currentTime,
353
- duration: this.duration,
354
- 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,
355
272
  }
356
273
  }
357
274
 
358
- /**
359
- * Get current time
360
- */
361
- getCurrentTime(): number {
362
- return this.currentTime
275
+ get currentTime(): number {
276
+ return this._currentTime
363
277
  }
364
278
 
365
- /**
366
- * Get animation duration
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
  }