reze-engine 0.3.6 → 0.3.8
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 +8 -25
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +235 -588
- 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/model.d.ts +59 -11
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +416 -112
- package/package.json +1 -1
- package/src/engine.ts +266 -649
- package/src/model.ts +518 -156
- package/dist/ik.d.ts +0 -32
- package/dist/ik.d.ts.map +0 -1
- package/dist/ik.js +0 -337
- package/dist/pool-scene.d.ts +0 -52
- package/dist/pool-scene.d.ts.map +0 -1
- package/dist/pool-scene.js +0 -1122
- package/dist/pool.d.ts +0 -38
- package/dist/pool.d.ts.map +0 -1
- package/dist/pool.js +0 -422
package/src/model.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Mat4, Quat, Vec3, easeInOut } from "./math"
|
|
2
|
-
import { Rigidbody, Joint } from "./physics"
|
|
1
|
+
import { Mat4, Quat, Vec3, easeInOut, bezierInterpolate } from "./math"
|
|
2
|
+
import { Rigidbody, Joint, Physics } from "./physics"
|
|
3
3
|
import { IKSolverSystem } from "./ik-solver"
|
|
4
|
+
import { VMDKeyFrame, VMDLoader, BoneFrame, MorphFrame } from "./vmd-loader"
|
|
4
5
|
|
|
5
6
|
const VERTEX_STRIDE = 8
|
|
6
7
|
|
|
@@ -119,31 +120,29 @@ export interface MorphRuntime {
|
|
|
119
120
|
weights: Float32Array // One weight per morph (0.0 to 1.0)
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
startTimeMs: Float32Array // one float per bone (ms)
|
|
146
|
-
durationMs: Float32Array // one float per bone (ms)
|
|
123
|
+
// Tween state - combines rotation, translation, and morph tweens
|
|
124
|
+
// All tweens share the same time reference to avoid conflicts
|
|
125
|
+
interface TweenState {
|
|
126
|
+
// Bone rotation tweens
|
|
127
|
+
rotActive: Uint8Array // 0/1 per bone
|
|
128
|
+
rotStartQuat: Float32Array // quat per bone (x,y,z,w)
|
|
129
|
+
rotTargetQuat: Float32Array // quat per bone (x,y,z,w)
|
|
130
|
+
rotStartTimeMs: Float32Array // one float per bone (ms)
|
|
131
|
+
rotDurationMs: Float32Array // one float per bone (ms)
|
|
132
|
+
|
|
133
|
+
// Bone translation tweens
|
|
134
|
+
transActive: Uint8Array // 0/1 per bone
|
|
135
|
+
transStartVec: Float32Array // vec3 per bone (x,y,z)
|
|
136
|
+
transTargetVec: Float32Array // vec3 per bone (x,y,z)
|
|
137
|
+
transStartTimeMs: Float32Array // one float per bone (ms)
|
|
138
|
+
transDurationMs: Float32Array // one float per bone (ms)
|
|
139
|
+
|
|
140
|
+
// Morph weight tweens
|
|
141
|
+
morphActive: Uint8Array // 0/1 per morph
|
|
142
|
+
morphStartWeight: Float32Array // one float per morph
|
|
143
|
+
morphTargetWeight: Float32Array // one float per morph
|
|
144
|
+
morphStartTimeMs: Float32Array // one float per morph (ms)
|
|
145
|
+
morphDurationMs: Float32Array // one float per morph (ms)
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
export class Model {
|
|
@@ -169,14 +168,29 @@ export class Model {
|
|
|
169
168
|
|
|
170
169
|
// Runtime morph state
|
|
171
170
|
private runtimeMorph!: MorphRuntime
|
|
171
|
+
private morphsDirty: boolean = false // Flag indicating if morphs need to be applied
|
|
172
172
|
|
|
173
173
|
// Cached identity matrices to avoid allocations in computeWorldMatrices
|
|
174
174
|
private cachedIdentityMat1 = Mat4.identity()
|
|
175
175
|
private cachedIdentityMat2 = Mat4.identity()
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
private
|
|
179
|
-
|
|
177
|
+
// Cached skin matrices array to avoid allocations in getSkinMatrices
|
|
178
|
+
private cachedSkinMatrices?: Float32Array
|
|
179
|
+
|
|
180
|
+
private tweenState!: TweenState
|
|
181
|
+
private tweenTimeMs: number = 0 // Time tracking for tweens (milliseconds)
|
|
182
|
+
|
|
183
|
+
// Animation runtime
|
|
184
|
+
private animationData: VMDKeyFrame[] | null = null
|
|
185
|
+
private boneTracks: Map<string, Array<{ boneFrame: BoneFrame; time: number }>> = new Map()
|
|
186
|
+
private morphTracks: Map<string, Array<{ morphFrame: MorphFrame; time: number }>> = new Map()
|
|
187
|
+
private animationDuration: number = 0
|
|
188
|
+
private isPlaying: boolean = false
|
|
189
|
+
private isPaused: boolean = false
|
|
190
|
+
private animationTime: number = 0 // Current time in animation (seconds)
|
|
191
|
+
|
|
192
|
+
// Physics runtime
|
|
193
|
+
private physics: Physics | null = null
|
|
180
194
|
|
|
181
195
|
constructor(
|
|
182
196
|
vertexData: Float32Array<ArrayBuffer>,
|
|
@@ -207,11 +221,17 @@ export class Model {
|
|
|
207
221
|
}
|
|
208
222
|
|
|
209
223
|
this.initializeRuntimeSkeleton()
|
|
210
|
-
this.initializeRotTweenBuffers()
|
|
211
|
-
this.initializeTransTweenBuffers()
|
|
212
224
|
this.initializeRuntimeMorph()
|
|
213
|
-
this.
|
|
225
|
+
this.initializeTweenBuffers()
|
|
214
226
|
this.applyMorphs() // Apply initial morphs (all weights are 0, so no change)
|
|
227
|
+
|
|
228
|
+
// Initialize physics if rigidbodies exist
|
|
229
|
+
if (rigidbodies.length > 0) {
|
|
230
|
+
this.physics = new Physics(rigidbodies, joints)
|
|
231
|
+
console.log(`[Model] Physics initialized with ${rigidbodies.length} rigidbodies`)
|
|
232
|
+
} else {
|
|
233
|
+
console.log("[Model] No rigidbodies found, physics disabled")
|
|
234
|
+
}
|
|
215
235
|
}
|
|
216
236
|
|
|
217
237
|
private initializeRuntimeSkeleton(): void {
|
|
@@ -278,25 +298,31 @@ export class Model {
|
|
|
278
298
|
this.runtimeSkeleton.ikSolvers = ikSolvers
|
|
279
299
|
}
|
|
280
300
|
|
|
281
|
-
private
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
private initializeTransTweenBuffers(): void {
|
|
286
|
-
this.transTweenState = this.createTweenState(this.skeleton.bones.length, 3, 3)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
private initializeMorphTweenBuffers(): void {
|
|
290
|
-
this.morphTweenState = this.createTweenState(this.morphing.morphs.length, 1, 1)
|
|
291
|
-
}
|
|
301
|
+
private initializeTweenBuffers(): void {
|
|
302
|
+
const boneCount = this.skeleton.bones.length
|
|
303
|
+
const morphCount = this.morphing.morphs.length
|
|
292
304
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
305
|
+
this.tweenState = {
|
|
306
|
+
// Bone rotation tweens
|
|
307
|
+
rotActive: new Uint8Array(boneCount),
|
|
308
|
+
rotStartQuat: new Float32Array(boneCount * 4),
|
|
309
|
+
rotTargetQuat: new Float32Array(boneCount * 4),
|
|
310
|
+
rotStartTimeMs: new Float32Array(boneCount),
|
|
311
|
+
rotDurationMs: new Float32Array(boneCount),
|
|
312
|
+
|
|
313
|
+
// Bone translation tweens
|
|
314
|
+
transActive: new Uint8Array(boneCount),
|
|
315
|
+
transStartVec: new Float32Array(boneCount * 3),
|
|
316
|
+
transTargetVec: new Float32Array(boneCount * 3),
|
|
317
|
+
transStartTimeMs: new Float32Array(boneCount),
|
|
318
|
+
transDurationMs: new Float32Array(boneCount),
|
|
319
|
+
|
|
320
|
+
// Morph weight tweens
|
|
321
|
+
morphActive: new Uint8Array(morphCount),
|
|
322
|
+
morphStartWeight: new Float32Array(morphCount),
|
|
323
|
+
morphTargetWeight: new Float32Array(morphCount),
|
|
324
|
+
morphStartTimeMs: new Float32Array(morphCount),
|
|
325
|
+
morphDurationMs: new Float32Array(morphCount),
|
|
300
326
|
}
|
|
301
327
|
}
|
|
302
328
|
|
|
@@ -311,32 +337,37 @@ export class Model {
|
|
|
311
337
|
}
|
|
312
338
|
}
|
|
313
339
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
340
|
+
// Tween update - processes all tweens together with a single time reference
|
|
341
|
+
// This avoids conflicts and ensures consistent timing across all tween types
|
|
342
|
+
// Returns true if morph weights changed (needed for vertex buffer updates)
|
|
343
|
+
private updateTweens(): boolean {
|
|
344
|
+
const state = this.tweenState
|
|
345
|
+
const now = this.tweenTimeMs // Single time reference for all tweens
|
|
346
|
+
let morphChanged = false
|
|
347
|
+
|
|
348
|
+
// Update bone rotation tweens
|
|
317
349
|
const rotations = this.runtimeSkeleton.localRotations
|
|
318
350
|
const boneCount = this.skeleton.bones.length
|
|
319
|
-
|
|
320
351
|
for (let i = 0; i < boneCount; i++) {
|
|
321
|
-
if (state.
|
|
352
|
+
if (state.rotActive[i] !== 1) continue
|
|
322
353
|
|
|
323
|
-
const startMs = state.
|
|
324
|
-
const durMs = Math.max(1, state.
|
|
354
|
+
const startMs = state.rotStartTimeMs[i]
|
|
355
|
+
const durMs = Math.max(1, state.rotDurationMs[i])
|
|
325
356
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
|
|
326
357
|
const e = easeInOut(t)
|
|
327
358
|
|
|
328
359
|
const qi = i * 4
|
|
329
360
|
const startQuat = new Quat(
|
|
330
|
-
state.
|
|
331
|
-
state.
|
|
332
|
-
state.
|
|
333
|
-
state.
|
|
361
|
+
state.rotStartQuat[qi],
|
|
362
|
+
state.rotStartQuat[qi + 1],
|
|
363
|
+
state.rotStartQuat[qi + 2],
|
|
364
|
+
state.rotStartQuat[qi + 3]
|
|
334
365
|
)
|
|
335
366
|
const targetQuat = new Quat(
|
|
336
|
-
state.
|
|
337
|
-
state.
|
|
338
|
-
state.
|
|
339
|
-
state.
|
|
367
|
+
state.rotTargetQuat[qi],
|
|
368
|
+
state.rotTargetQuat[qi + 1],
|
|
369
|
+
state.rotTargetQuat[qi + 2],
|
|
370
|
+
state.rotTargetQuat[qi + 3]
|
|
340
371
|
)
|
|
341
372
|
const result = Quat.slerp(startQuat, targetQuat, e)
|
|
342
373
|
|
|
@@ -345,58 +376,63 @@ export class Model {
|
|
|
345
376
|
rotations[qi + 2] = result.z
|
|
346
377
|
rotations[qi + 3] = result.w
|
|
347
378
|
|
|
348
|
-
if (t >= 1)
|
|
379
|
+
if (t >= 1) {
|
|
380
|
+
state.rotActive[i] = 0
|
|
381
|
+
}
|
|
349
382
|
}
|
|
350
|
-
}
|
|
351
383
|
|
|
352
|
-
|
|
353
|
-
const state = this.transTweenState
|
|
354
|
-
const now = performance.now()
|
|
384
|
+
// Update bone translation tweens
|
|
355
385
|
const translations = this.runtimeSkeleton.localTranslations
|
|
356
|
-
const boneCount = this.skeleton.bones.length
|
|
357
|
-
|
|
358
386
|
for (let i = 0; i < boneCount; i++) {
|
|
359
|
-
if (state.
|
|
387
|
+
if (state.transActive[i] !== 1) continue
|
|
360
388
|
|
|
361
|
-
const startMs = state.
|
|
362
|
-
const durMs = Math.max(1, state.
|
|
389
|
+
const startMs = state.transStartTimeMs[i]
|
|
390
|
+
const durMs = Math.max(1, state.transDurationMs[i])
|
|
363
391
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
|
|
364
392
|
const e = easeInOut(t)
|
|
365
393
|
|
|
366
394
|
const ti = i * 3
|
|
367
|
-
translations[ti] = state.
|
|
368
|
-
translations[ti + 1] =
|
|
369
|
-
|
|
395
|
+
translations[ti] = state.transStartVec[ti] + (state.transTargetVec[ti] - state.transStartVec[ti]) * e
|
|
396
|
+
translations[ti + 1] =
|
|
397
|
+
state.transStartVec[ti + 1] + (state.transTargetVec[ti + 1] - state.transStartVec[ti + 1]) * e
|
|
398
|
+
translations[ti + 2] =
|
|
399
|
+
state.transStartVec[ti + 2] + (state.transTargetVec[ti + 2] - state.transStartVec[ti + 2]) * e
|
|
370
400
|
|
|
371
|
-
if (t >= 1)
|
|
401
|
+
if (t >= 1) {
|
|
402
|
+
state.transActive[i] = 0
|
|
403
|
+
}
|
|
372
404
|
}
|
|
373
|
-
}
|
|
374
405
|
|
|
375
|
-
|
|
376
|
-
const state = this.morphTweenState
|
|
377
|
-
const now = performance.now()
|
|
406
|
+
// Update morph weight tweens
|
|
378
407
|
const weights = this.runtimeMorph.weights
|
|
379
408
|
const morphCount = this.morphing.morphs.length
|
|
380
|
-
let hasActiveTweens = false
|
|
381
|
-
|
|
382
409
|
for (let i = 0; i < morphCount; i++) {
|
|
383
|
-
if (state.
|
|
410
|
+
if (state.morphActive[i] !== 1) continue
|
|
384
411
|
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
const durMs = Math.max(1, state.durationMs[i])
|
|
412
|
+
const startMs = state.morphStartTimeMs[i]
|
|
413
|
+
const durMs = Math.max(1, state.morphDurationMs[i])
|
|
388
414
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
|
|
389
415
|
const e = easeInOut(t)
|
|
390
416
|
|
|
391
|
-
|
|
417
|
+
const oldWeight = weights[i]
|
|
418
|
+
weights[i] = state.morphStartWeight[i] + (state.morphTargetWeight[i] - state.morphStartWeight[i]) * e
|
|
419
|
+
|
|
420
|
+
// Check if weight actually changed (accounting for floating point precision)
|
|
421
|
+
if (Math.abs(weights[i] - oldWeight) > 1e-6) {
|
|
422
|
+
morphChanged = true
|
|
423
|
+
}
|
|
392
424
|
|
|
393
425
|
if (t >= 1) {
|
|
394
|
-
weights[i] = state.
|
|
395
|
-
state.
|
|
426
|
+
weights[i] = state.morphTargetWeight[i]
|
|
427
|
+
state.morphActive[i] = 0
|
|
428
|
+
// Check if final weight is different from old weight
|
|
429
|
+
if (Math.abs(weights[i] - oldWeight) > 1e-6) {
|
|
430
|
+
morphChanged = true
|
|
431
|
+
}
|
|
396
432
|
}
|
|
397
433
|
}
|
|
398
434
|
|
|
399
|
-
return
|
|
435
|
+
return morphChanged
|
|
400
436
|
}
|
|
401
437
|
|
|
402
438
|
getVertices(): Float32Array<ArrayBuffer> {
|
|
@@ -442,9 +478,9 @@ export class Model {
|
|
|
442
478
|
// ------- Bone helpers (public API) -------
|
|
443
479
|
|
|
444
480
|
rotateBones(names: string[], quats: Quat[], durationMs?: number): void {
|
|
445
|
-
const state = this.
|
|
481
|
+
const state = this.tweenState
|
|
446
482
|
const normalized = quats.map((q) => q.normalize())
|
|
447
|
-
const now =
|
|
483
|
+
const now = this.tweenTimeMs
|
|
448
484
|
const dur = durationMs && durationMs > 0 ? durationMs : 0
|
|
449
485
|
|
|
450
486
|
for (let i = 0; i < names.length; i++) {
|
|
@@ -461,7 +497,7 @@ export class Model {
|
|
|
461
497
|
rotations[qi + 1] = ty
|
|
462
498
|
rotations[qi + 2] = tz
|
|
463
499
|
rotations[qi + 3] = tw
|
|
464
|
-
state.
|
|
500
|
+
state.rotActive[idx] = 0
|
|
465
501
|
continue
|
|
466
502
|
}
|
|
467
503
|
|
|
@@ -470,22 +506,22 @@ export class Model {
|
|
|
470
506
|
let sz = rotations[qi + 2]
|
|
471
507
|
let sw = rotations[qi + 3]
|
|
472
508
|
|
|
473
|
-
if (state.
|
|
474
|
-
const startMs = state.
|
|
475
|
-
const prevDur = Math.max(1, state.
|
|
509
|
+
if (state.rotActive[idx] === 1) {
|
|
510
|
+
const startMs = state.rotStartTimeMs[idx]
|
|
511
|
+
const prevDur = Math.max(1, state.rotDurationMs[idx])
|
|
476
512
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
|
|
477
513
|
const e = easeInOut(t)
|
|
478
514
|
const startQuat = new Quat(
|
|
479
|
-
state.
|
|
480
|
-
state.
|
|
481
|
-
state.
|
|
482
|
-
state.
|
|
515
|
+
state.rotStartQuat[qi],
|
|
516
|
+
state.rotStartQuat[qi + 1],
|
|
517
|
+
state.rotStartQuat[qi + 2],
|
|
518
|
+
state.rotStartQuat[qi + 3]
|
|
483
519
|
)
|
|
484
520
|
const targetQuat = new Quat(
|
|
485
|
-
state.
|
|
486
|
-
state.
|
|
487
|
-
state.
|
|
488
|
-
state.
|
|
521
|
+
state.rotTargetQuat[qi],
|
|
522
|
+
state.rotTargetQuat[qi + 1],
|
|
523
|
+
state.rotTargetQuat[qi + 2],
|
|
524
|
+
state.rotTargetQuat[qi + 3]
|
|
489
525
|
)
|
|
490
526
|
const result = Quat.slerp(startQuat, targetQuat, e)
|
|
491
527
|
sx = result.x
|
|
@@ -494,25 +530,25 @@ export class Model {
|
|
|
494
530
|
sw = result.w
|
|
495
531
|
}
|
|
496
532
|
|
|
497
|
-
state.
|
|
498
|
-
state.
|
|
499
|
-
state.
|
|
500
|
-
state.
|
|
501
|
-
state.
|
|
502
|
-
state.
|
|
503
|
-
state.
|
|
504
|
-
state.
|
|
505
|
-
state.
|
|
506
|
-
state.
|
|
507
|
-
state.
|
|
533
|
+
state.rotStartQuat[qi] = sx
|
|
534
|
+
state.rotStartQuat[qi + 1] = sy
|
|
535
|
+
state.rotStartQuat[qi + 2] = sz
|
|
536
|
+
state.rotStartQuat[qi + 3] = sw
|
|
537
|
+
state.rotTargetQuat[qi] = tx
|
|
538
|
+
state.rotTargetQuat[qi + 1] = ty
|
|
539
|
+
state.rotTargetQuat[qi + 2] = tz
|
|
540
|
+
state.rotTargetQuat[qi + 3] = tw
|
|
541
|
+
state.rotStartTimeMs[idx] = now
|
|
542
|
+
state.rotDurationMs[idx] = dur
|
|
543
|
+
state.rotActive[idx] = 1
|
|
508
544
|
}
|
|
509
545
|
}
|
|
510
546
|
|
|
511
547
|
// Move bones using VMD-style relative translations (relative to bind pose world position)
|
|
512
548
|
// This is the default behavior for VMD animations
|
|
513
549
|
moveBones(names: string[], relativeTranslations: Vec3[], durationMs?: number): void {
|
|
514
|
-
const state = this.
|
|
515
|
-
const now =
|
|
550
|
+
const state = this.tweenState
|
|
551
|
+
const now = this.tweenTimeMs
|
|
516
552
|
const dur = durationMs && durationMs > 0 ? durationMs : 0
|
|
517
553
|
const localRot = this.runtimeSkeleton.localRotations
|
|
518
554
|
|
|
@@ -580,7 +616,7 @@ export class Model {
|
|
|
580
616
|
translations[ti] = tx
|
|
581
617
|
translations[ti + 1] = ty
|
|
582
618
|
translations[ti + 2] = tz
|
|
583
|
-
state.
|
|
619
|
+
state.transActive[idx] = 0
|
|
584
620
|
continue
|
|
585
621
|
}
|
|
586
622
|
|
|
@@ -588,25 +624,25 @@ export class Model {
|
|
|
588
624
|
let sy = translations[ti + 1]
|
|
589
625
|
let sz = translations[ti + 2]
|
|
590
626
|
|
|
591
|
-
if (state.
|
|
592
|
-
const startMs = state.
|
|
593
|
-
const prevDur = Math.max(1, state.
|
|
627
|
+
if (state.transActive[idx] === 1) {
|
|
628
|
+
const startMs = state.transStartTimeMs[idx]
|
|
629
|
+
const prevDur = Math.max(1, state.transDurationMs[idx])
|
|
594
630
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
|
|
595
631
|
const e = easeInOut(t)
|
|
596
|
-
sx = state.
|
|
597
|
-
sy = state.
|
|
598
|
-
sz = state.
|
|
632
|
+
sx = state.transStartVec[ti] + (state.transTargetVec[ti] - state.transStartVec[ti]) * e
|
|
633
|
+
sy = state.transStartVec[ti + 1] + (state.transTargetVec[ti + 1] - state.transStartVec[ti + 1]) * e
|
|
634
|
+
sz = state.transStartVec[ti + 2] + (state.transTargetVec[ti + 2] - state.transStartVec[ti + 2]) * e
|
|
599
635
|
}
|
|
600
636
|
|
|
601
|
-
state.
|
|
602
|
-
state.
|
|
603
|
-
state.
|
|
604
|
-
state.
|
|
605
|
-
state.
|
|
606
|
-
state.
|
|
607
|
-
state.
|
|
608
|
-
state.
|
|
609
|
-
state.
|
|
637
|
+
state.transStartVec[ti] = sx
|
|
638
|
+
state.transStartVec[ti + 1] = sy
|
|
639
|
+
state.transStartVec[ti + 2] = sz
|
|
640
|
+
state.transTargetVec[ti] = tx
|
|
641
|
+
state.transTargetVec[ti + 1] = ty
|
|
642
|
+
state.transTargetVec[ti + 2] = tz
|
|
643
|
+
state.transStartTimeMs[idx] = now
|
|
644
|
+
state.transDurationMs[idx] = dur
|
|
645
|
+
state.transActive[idx] = 1
|
|
610
646
|
}
|
|
611
647
|
}
|
|
612
648
|
|
|
@@ -618,6 +654,30 @@ export class Model {
|
|
|
618
654
|
return this.skeleton.inverseBindMatrices
|
|
619
655
|
}
|
|
620
656
|
|
|
657
|
+
getSkinMatrices(): Float32Array {
|
|
658
|
+
const boneCount = this.skeleton.bones.length
|
|
659
|
+
const worldMats = this.runtimeSkeleton.worldMatrices
|
|
660
|
+
const invBindMats = this.skeleton.inverseBindMatrices
|
|
661
|
+
|
|
662
|
+
// Initialize cached array if needed or if bone count changed
|
|
663
|
+
if (!this.cachedSkinMatrices || this.cachedSkinMatrices.length !== boneCount * 16) {
|
|
664
|
+
this.cachedSkinMatrices = new Float32Array(boneCount * 16)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const skinMatrices = this.cachedSkinMatrices
|
|
668
|
+
|
|
669
|
+
// Compute skin matrices: skinMatrix = worldMatrix × inverseBindMatrix
|
|
670
|
+
// Use Mat4.multiplyArrays to avoid creating Mat4 objects
|
|
671
|
+
for (let i = 0; i < boneCount; i++) {
|
|
672
|
+
const worldOffset = i * 16
|
|
673
|
+
const invBindOffset = i * 16
|
|
674
|
+
const skinOffset = i * 16
|
|
675
|
+
Mat4.multiplyArrays(worldMats, worldOffset, invBindMats, invBindOffset, skinMatrices, skinOffset)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return skinMatrices
|
|
679
|
+
}
|
|
680
|
+
|
|
621
681
|
setMorphWeight(name: string, weight: number, durationMs?: number): void {
|
|
622
682
|
const idx = this.runtimeMorph.nameIndex[name] ?? -1
|
|
623
683
|
if (idx < 0 || idx >= this.runtimeMorph.weights.length) return
|
|
@@ -628,30 +688,30 @@ export class Model {
|
|
|
628
688
|
if (dur === 0) {
|
|
629
689
|
// Instant change
|
|
630
690
|
this.runtimeMorph.weights[idx] = clampedWeight
|
|
631
|
-
this.
|
|
691
|
+
this.tweenState.morphActive[idx] = 0
|
|
632
692
|
this.applyMorphs()
|
|
633
693
|
return
|
|
634
694
|
}
|
|
635
695
|
|
|
636
696
|
// Animated change
|
|
637
|
-
const state = this.
|
|
638
|
-
const now =
|
|
697
|
+
const state = this.tweenState
|
|
698
|
+
const now = this.tweenTimeMs
|
|
639
699
|
|
|
640
700
|
// If already tweening, start from current interpolated value
|
|
641
701
|
let startWeight = this.runtimeMorph.weights[idx]
|
|
642
|
-
if (state.
|
|
643
|
-
const startMs = state.
|
|
644
|
-
const prevDur = Math.max(1, state.
|
|
702
|
+
if (state.morphActive[idx] === 1) {
|
|
703
|
+
const startMs = state.morphStartTimeMs[idx]
|
|
704
|
+
const prevDur = Math.max(1, state.morphDurationMs[idx])
|
|
645
705
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
|
|
646
706
|
const e = easeInOut(t)
|
|
647
|
-
startWeight = state.
|
|
707
|
+
startWeight = state.morphStartWeight[idx] + (state.morphTargetWeight[idx] - state.morphStartWeight[idx]) * e
|
|
648
708
|
}
|
|
649
709
|
|
|
650
|
-
state.
|
|
651
|
-
state.
|
|
652
|
-
state.
|
|
653
|
-
state.
|
|
654
|
-
state.
|
|
710
|
+
state.morphStartWeight[idx] = startWeight
|
|
711
|
+
state.morphTargetWeight[idx] = clampedWeight
|
|
712
|
+
state.morphStartTimeMs[idx] = now
|
|
713
|
+
state.morphDurationMs[idx] = dur
|
|
714
|
+
state.morphActive[idx] = 1
|
|
655
715
|
|
|
656
716
|
// Immediately apply morphs with current weight
|
|
657
717
|
this.runtimeMorph.weights[idx] = startWeight
|
|
@@ -723,15 +783,313 @@ export class Model {
|
|
|
723
783
|
}
|
|
724
784
|
}
|
|
725
785
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
786
|
+
/**
|
|
787
|
+
* Load VMD animation file
|
|
788
|
+
*/
|
|
789
|
+
async loadVmd(vmdUrl: string): Promise<void> {
|
|
790
|
+
this.animationData = await VMDLoader.load(vmdUrl)
|
|
791
|
+
this.processFrames()
|
|
792
|
+
// Apply initial pose at time 0
|
|
793
|
+
this.animationTime = 0
|
|
794
|
+
this.getPoseAtTime(0)
|
|
795
|
+
|
|
796
|
+
// Apply morphs if animation changed them
|
|
797
|
+
if (this.morphsDirty) {
|
|
731
798
|
this.applyMorphs()
|
|
799
|
+
this.morphsDirty = false
|
|
732
800
|
}
|
|
733
801
|
|
|
734
|
-
// Compute
|
|
802
|
+
// Compute world matrices after applying initial pose
|
|
803
|
+
this.computeWorldMatrices()
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Process frames into tracks
|
|
808
|
+
*/
|
|
809
|
+
private processFrames(): void {
|
|
810
|
+
if (!this.animationData) return
|
|
811
|
+
|
|
812
|
+
// Helper to group frames by name and sort by time
|
|
813
|
+
const groupFrames = <T>(
|
|
814
|
+
items: Array<{ item: T; name: string; time: number }>
|
|
815
|
+
): Map<string, Array<{ item: T; time: number }>> => {
|
|
816
|
+
const tracks = new Map<string, Array<{ item: T; time: number }>>()
|
|
817
|
+
for (const { item, name, time } of items) {
|
|
818
|
+
if (!tracks.has(name)) tracks.set(name, [])
|
|
819
|
+
tracks.get(name)!.push({ item, time })
|
|
820
|
+
}
|
|
821
|
+
for (const keyFrames of tracks.values()) {
|
|
822
|
+
keyFrames.sort((a, b) => a.time - b.time)
|
|
823
|
+
}
|
|
824
|
+
return tracks
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Collect all bone and morph frames
|
|
828
|
+
const boneItems: Array<{ item: BoneFrame; name: string; time: number }> = []
|
|
829
|
+
const morphItems: Array<{ item: MorphFrame; name: string; time: number }> = []
|
|
830
|
+
|
|
831
|
+
for (const keyFrame of this.animationData) {
|
|
832
|
+
for (const boneFrame of keyFrame.boneFrames) {
|
|
833
|
+
boneItems.push({ item: boneFrame, name: boneFrame.boneName, time: keyFrame.time })
|
|
834
|
+
}
|
|
835
|
+
for (const morphFrame of keyFrame.morphFrames) {
|
|
836
|
+
morphItems.push({ item: morphFrame, name: morphFrame.morphName, time: keyFrame.time })
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Transform to expected format
|
|
841
|
+
this.boneTracks = new Map()
|
|
842
|
+
for (const [name, frames] of groupFrames(boneItems).entries()) {
|
|
843
|
+
this.boneTracks.set(
|
|
844
|
+
name,
|
|
845
|
+
frames.map((f) => ({ boneFrame: f.item, time: f.time }))
|
|
846
|
+
)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
this.morphTracks = new Map()
|
|
850
|
+
for (const [name, frames] of groupFrames(morphItems).entries()) {
|
|
851
|
+
this.morphTracks.set(
|
|
852
|
+
name,
|
|
853
|
+
frames.map((f) => ({ morphFrame: f.item, time: f.time }))
|
|
854
|
+
)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Calculate duration from all tracks
|
|
858
|
+
const allTracks = [...this.boneTracks.values(), ...this.morphTracks.values()]
|
|
859
|
+
this.animationDuration = allTracks.reduce((max, keyFrames) => {
|
|
860
|
+
const lastTime = keyFrames[keyFrames.length - 1]?.time ?? 0
|
|
861
|
+
return Math.max(max, lastTime)
|
|
862
|
+
}, 0)
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Start or resume playback
|
|
867
|
+
*/
|
|
868
|
+
playAnimation(): void {
|
|
869
|
+
if (!this.animationData) {
|
|
870
|
+
console.warn("[Model] Cannot play animation: no animation data loaded")
|
|
871
|
+
return
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
this.isPaused = false
|
|
875
|
+
this.isPlaying = true
|
|
876
|
+
|
|
877
|
+
// Apply initial pose at current animation time
|
|
878
|
+
this.getPoseAtTime(this.animationTime)
|
|
879
|
+
|
|
880
|
+
// Apply morphs if animation changed them
|
|
881
|
+
if (this.morphsDirty) {
|
|
882
|
+
this.applyMorphs()
|
|
883
|
+
this.morphsDirty = false
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Compute world matrices after applying pose
|
|
887
|
+
this.computeWorldMatrices()
|
|
888
|
+
|
|
889
|
+
// Reset physics when starting animation (prevents instability from sudden pose changes)
|
|
890
|
+
if (this.physics && this.animationTime === 0) {
|
|
891
|
+
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices)
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Pause playback
|
|
897
|
+
*/
|
|
898
|
+
pauseAnimation(): void {
|
|
899
|
+
if (!this.isPlaying || this.isPaused) return
|
|
900
|
+
this.isPaused = true
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Stop playback and reset to beginning
|
|
905
|
+
*/
|
|
906
|
+
stopAnimation(): void {
|
|
907
|
+
this.isPlaying = false
|
|
908
|
+
this.isPaused = false
|
|
909
|
+
this.animationTime = 0
|
|
910
|
+
|
|
911
|
+
// Reset physics state when stopping animation (prevents instability from sudden pose changes)
|
|
912
|
+
if (this.physics) {
|
|
913
|
+
this.computeWorldMatrices()
|
|
914
|
+
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices)
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Seek to specific time
|
|
920
|
+
* Immediately applies pose at the seeked time
|
|
921
|
+
*/
|
|
922
|
+
seekAnimation(time: number): void {
|
|
923
|
+
if (!this.animationData) return
|
|
924
|
+
|
|
925
|
+
const clampedTime = Math.max(0, Math.min(time, this.animationDuration))
|
|
926
|
+
this.animationTime = clampedTime
|
|
927
|
+
// Immediately apply pose at seeked time
|
|
928
|
+
this.getPoseAtTime(clampedTime)
|
|
929
|
+
|
|
930
|
+
// Apply morphs if animation changed them
|
|
931
|
+
if (this.morphsDirty) {
|
|
932
|
+
this.applyMorphs()
|
|
933
|
+
this.morphsDirty = false
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Compute world matrices after applying pose
|
|
937
|
+
this.computeWorldMatrices()
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Get current animation progress
|
|
942
|
+
*/
|
|
943
|
+
getAnimationProgress(): { current: number; duration: number; percentage: number } {
|
|
944
|
+
const duration = this.animationDuration
|
|
945
|
+
const percentage = duration > 0 ? (this.animationTime / duration) * 100 : 0
|
|
946
|
+
return {
|
|
947
|
+
current: this.animationTime,
|
|
948
|
+
duration,
|
|
949
|
+
percentage,
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Get pose at specific time (internal helper)
|
|
955
|
+
*/
|
|
956
|
+
private getPoseAtTime(time: number): void {
|
|
957
|
+
if (!this.animationData) return
|
|
958
|
+
|
|
959
|
+
// Helper for binary search upper bound
|
|
960
|
+
const upperBound = <T extends { time: number }>(time: number, keyFrames: T[]): number => {
|
|
961
|
+
let left = 0,
|
|
962
|
+
right = keyFrames.length
|
|
963
|
+
while (left < right) {
|
|
964
|
+
const mid = Math.floor((left + right) / 2)
|
|
965
|
+
if (keyFrames[mid].time <= time) left = mid + 1
|
|
966
|
+
else right = mid
|
|
967
|
+
}
|
|
968
|
+
return left
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Process bone tracks
|
|
972
|
+
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
973
|
+
if (keyFrames.length === 0) continue
|
|
974
|
+
|
|
975
|
+
const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time))
|
|
976
|
+
const idx = upperBound(clampedTime, keyFrames) - 1
|
|
977
|
+
if (idx < 0) continue
|
|
978
|
+
|
|
979
|
+
const frameA = keyFrames[idx].boneFrame
|
|
980
|
+
const frameB = keyFrames[idx + 1]?.boneFrame
|
|
981
|
+
|
|
982
|
+
const boneIdx = this.runtimeSkeleton.nameIndex[boneName]
|
|
983
|
+
if (boneIdx === undefined) continue
|
|
984
|
+
|
|
985
|
+
if (!frameB) {
|
|
986
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4] = frameA.rotation.x
|
|
987
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 1] = frameA.rotation.y
|
|
988
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 2] = frameA.rotation.z
|
|
989
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 3] = frameA.rotation.w
|
|
990
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3] = frameA.translation.x
|
|
991
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3 + 1] = frameA.translation.y
|
|
992
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3 + 2] = frameA.translation.z
|
|
993
|
+
} else {
|
|
994
|
+
const timeA = keyFrames[idx].time
|
|
995
|
+
const timeB = keyFrames[idx + 1].time
|
|
996
|
+
const gradient = (clampedTime - timeA) / (timeB - timeA)
|
|
997
|
+
const interp = frameB.interpolation
|
|
998
|
+
|
|
999
|
+
// Interpolate rotation using SLERP with bezier
|
|
1000
|
+
const rotT = bezierInterpolate(interp[0] / 127, interp[1] / 127, interp[2] / 127, interp[3] / 127, gradient)
|
|
1001
|
+
const rotation = Quat.slerp(frameA.rotation, frameB.rotation, rotT)
|
|
1002
|
+
|
|
1003
|
+
// Interpolate translation using bezier for each component
|
|
1004
|
+
const getWeight = (offset: number) =>
|
|
1005
|
+
bezierInterpolate(
|
|
1006
|
+
interp[offset] / 127,
|
|
1007
|
+
interp[offset + 8] / 127,
|
|
1008
|
+
interp[offset + 4] / 127,
|
|
1009
|
+
interp[offset + 12] / 127,
|
|
1010
|
+
gradient
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
const lerp = (a: number, b: number, w: number) => a + (b - a) * w
|
|
1014
|
+
const translation = new Vec3(
|
|
1015
|
+
lerp(frameA.translation.x, frameB.translation.x, getWeight(0)),
|
|
1016
|
+
lerp(frameA.translation.y, frameB.translation.y, getWeight(16)),
|
|
1017
|
+
lerp(frameA.translation.z, frameB.translation.z, getWeight(32))
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4] = rotation.x
|
|
1021
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 1] = rotation.y
|
|
1022
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 2] = rotation.z
|
|
1023
|
+
this.runtimeSkeleton.localRotations[boneIdx * 4 + 3] = rotation.w
|
|
1024
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3] = translation.x
|
|
1025
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3 + 1] = translation.y
|
|
1026
|
+
this.runtimeSkeleton.localTranslations[boneIdx * 3 + 2] = translation.z
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Process morph tracks
|
|
1031
|
+
for (const [morphName, keyFrames] of this.morphTracks.entries()) {
|
|
1032
|
+
if (keyFrames.length === 0) continue
|
|
1033
|
+
|
|
1034
|
+
const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time))
|
|
1035
|
+
const idx = upperBound(clampedTime, keyFrames) - 1
|
|
1036
|
+
if (idx < 0) continue
|
|
1037
|
+
|
|
1038
|
+
const frameA = keyFrames[idx].morphFrame
|
|
1039
|
+
const frameB = keyFrames[idx + 1]?.morphFrame
|
|
1040
|
+
|
|
1041
|
+
const morphIdx = this.runtimeMorph.nameIndex[morphName]
|
|
1042
|
+
if (morphIdx === undefined) continue
|
|
1043
|
+
|
|
1044
|
+
const weight = frameB
|
|
1045
|
+
? frameA.weight +
|
|
1046
|
+
(frameB.weight - frameA.weight) *
|
|
1047
|
+
((clampedTime - keyFrames[idx].time) / (keyFrames[idx + 1].time - keyFrames[idx].time))
|
|
1048
|
+
: frameA.weight
|
|
1049
|
+
|
|
1050
|
+
this.runtimeMorph.weights[morphIdx] = weight
|
|
1051
|
+
this.morphsDirty = true // Mark as dirty when animation sets morph weights
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Updates the model pose by recomputing all matrices.
|
|
1057
|
+
* If animation is playing, applies animation pose first.
|
|
1058
|
+
* deltaTime: Time elapsed since last update in seconds
|
|
1059
|
+
* Returns true if vertices were modified (morphs changed)
|
|
1060
|
+
*/
|
|
1061
|
+
update(deltaTime: number): boolean {
|
|
1062
|
+
// Update tween time (in milliseconds)
|
|
1063
|
+
this.tweenTimeMs += deltaTime * 1000
|
|
1064
|
+
|
|
1065
|
+
// Update all active tweens (rotations, translations, morphs)
|
|
1066
|
+
const tweensChangedMorphs = this.updateTweens()
|
|
1067
|
+
|
|
1068
|
+
// Apply animation if playing or paused (always apply pose if animation data exists and we have a time set)
|
|
1069
|
+
if (this.animationData) {
|
|
1070
|
+
if (this.isPlaying && !this.isPaused) {
|
|
1071
|
+
this.animationTime += deltaTime
|
|
1072
|
+
|
|
1073
|
+
if (this.animationTime >= this.animationDuration) {
|
|
1074
|
+
this.animationTime = this.animationDuration
|
|
1075
|
+
this.pauseAnimation() // Auto-pause at end
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
this.getPoseAtTime(this.animationTime)
|
|
1079
|
+
} else if (this.isPaused || (!this.isPlaying && this.animationTime >= 0)) {
|
|
1080
|
+
// Apply pose at paused time or if we have a seeked time but not playing
|
|
1081
|
+
this.getPoseAtTime(this.animationTime)
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Apply morphs if tweens changed morphs or animation changed morphs
|
|
1086
|
+
const verticesChanged = this.morphsDirty || tweensChangedMorphs
|
|
1087
|
+
if (this.morphsDirty || tweensChangedMorphs) {
|
|
1088
|
+
this.applyMorphs()
|
|
1089
|
+
this.morphsDirty = false
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Compute world matrices (needed for IK solving to read bone positions)
|
|
735
1093
|
this.computeWorldMatrices()
|
|
736
1094
|
|
|
737
1095
|
// Solve IK chains (modifies localRotations with final IK rotations)
|
|
@@ -740,7 +1098,11 @@ export class Model {
|
|
|
740
1098
|
// Recompute world matrices with final IK rotations applied to localRotations
|
|
741
1099
|
this.computeWorldMatrices()
|
|
742
1100
|
|
|
743
|
-
|
|
1101
|
+
if (this.physics) {
|
|
1102
|
+
this.physics.step(deltaTime, this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices)
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return verticesChanged
|
|
744
1106
|
}
|
|
745
1107
|
|
|
746
1108
|
private solveIKChains(): void {
|