reze-engine 0.3.8 → 0.3.10
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/dist/model.d.ts +12 -13
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +93 -87
- package/package.json +1 -1
- package/src/model.ts +114 -103
package/dist/model.d.ts
CHANGED
|
@@ -120,6 +120,8 @@ export declare class Model {
|
|
|
120
120
|
private isPlaying;
|
|
121
121
|
private isPaused;
|
|
122
122
|
private animationTime;
|
|
123
|
+
private boneTrackIndices;
|
|
124
|
+
private morphTrackIndices;
|
|
123
125
|
private physics;
|
|
124
126
|
constructor(vertexData: Float32Array<ArrayBuffer>, indexData: Uint32Array<ArrayBuffer>, textures: Texture[], materials: Material[], skeleton: Skeleton, skinning: Skinning, morphing: Morphing, rigidbodies?: Rigidbody[], joints?: Joint[]);
|
|
125
127
|
private initializeRuntimeSkeleton;
|
|
@@ -152,22 +154,9 @@ export declare class Model {
|
|
|
152
154
|
* Process frames into tracks
|
|
153
155
|
*/
|
|
154
156
|
private processFrames;
|
|
155
|
-
/**
|
|
156
|
-
* Start or resume playback
|
|
157
|
-
*/
|
|
158
157
|
playAnimation(): void;
|
|
159
|
-
/**
|
|
160
|
-
* Pause playback
|
|
161
|
-
*/
|
|
162
158
|
pauseAnimation(): void;
|
|
163
|
-
/**
|
|
164
|
-
* Stop playback and reset to beginning
|
|
165
|
-
*/
|
|
166
159
|
stopAnimation(): void;
|
|
167
|
-
/**
|
|
168
|
-
* Seek to specific time
|
|
169
|
-
* Immediately applies pose at the seeked time
|
|
170
|
-
*/
|
|
171
160
|
seekAnimation(time: number): void;
|
|
172
161
|
/**
|
|
173
162
|
* Get current animation progress
|
|
@@ -177,8 +166,18 @@ export declare class Model {
|
|
|
177
166
|
duration: number;
|
|
178
167
|
percentage: number;
|
|
179
168
|
};
|
|
169
|
+
/**
|
|
170
|
+
* Binary search upper bound helper (static to avoid recreation)
|
|
171
|
+
*/
|
|
172
|
+
private static upperBound;
|
|
173
|
+
/**
|
|
174
|
+
* Find keyframe index with caching optimization
|
|
175
|
+
* Uses cached index as starting point for faster lookup when time is close
|
|
176
|
+
*/
|
|
177
|
+
private findKeyframeIndex;
|
|
180
178
|
/**
|
|
181
179
|
* Get pose at specific time (internal helper)
|
|
180
|
+
* Optimized for per-frame performance
|
|
182
181
|
*/
|
|
183
182
|
private getPoseAtTime;
|
|
184
183
|
/**
|
package/dist/model.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,IAAI,EAAE,IAAI,
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
|
package/dist/model.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Mat4, Quat, Vec3,
|
|
1
|
+
import { Mat4, Quat, Vec3, bezierInterpolate } from "./math";
|
|
2
2
|
import { Physics } from "./physics";
|
|
3
3
|
import { IKSolverSystem } from "./ik-solver";
|
|
4
4
|
import { VMDLoader } from "./vmd-loader";
|
|
@@ -23,6 +23,9 @@ export class Model {
|
|
|
23
23
|
this.isPlaying = false;
|
|
24
24
|
this.isPaused = false;
|
|
25
25
|
this.animationTime = 0; // Current time in animation (seconds)
|
|
26
|
+
// Cached keyframe indices for faster lookup (per track)
|
|
27
|
+
this.boneTrackIndices = new Map();
|
|
28
|
+
this.morphTrackIndices = new Map();
|
|
26
29
|
// Physics runtime
|
|
27
30
|
this.physics = null;
|
|
28
31
|
// Store base vertex data (original positions before morphing)
|
|
@@ -158,7 +161,7 @@ export class Model {
|
|
|
158
161
|
const startMs = state.rotStartTimeMs[i];
|
|
159
162
|
const durMs = Math.max(1, state.rotDurationMs[i]);
|
|
160
163
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs));
|
|
161
|
-
const e =
|
|
164
|
+
const e = t; // Linear interpolation
|
|
162
165
|
const qi = i * 4;
|
|
163
166
|
const startQuat = new Quat(state.rotStartQuat[qi], state.rotStartQuat[qi + 1], state.rotStartQuat[qi + 2], state.rotStartQuat[qi + 3]);
|
|
164
167
|
const targetQuat = new Quat(state.rotTargetQuat[qi], state.rotTargetQuat[qi + 1], state.rotTargetQuat[qi + 2], state.rotTargetQuat[qi + 3]);
|
|
@@ -179,7 +182,7 @@ export class Model {
|
|
|
179
182
|
const startMs = state.transStartTimeMs[i];
|
|
180
183
|
const durMs = Math.max(1, state.transDurationMs[i]);
|
|
181
184
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs));
|
|
182
|
-
const e =
|
|
185
|
+
const e = t; // Linear interpolation
|
|
183
186
|
const ti = i * 3;
|
|
184
187
|
translations[ti] = state.transStartVec[ti] + (state.transTargetVec[ti] - state.transStartVec[ti]) * e;
|
|
185
188
|
translations[ti + 1] =
|
|
@@ -199,7 +202,7 @@ export class Model {
|
|
|
199
202
|
const startMs = state.morphStartTimeMs[i];
|
|
200
203
|
const durMs = Math.max(1, state.morphDurationMs[i]);
|
|
201
204
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs));
|
|
202
|
-
const e =
|
|
205
|
+
const e = t; // Linear interpolation
|
|
203
206
|
const oldWeight = weights[i];
|
|
204
207
|
weights[i] = state.morphStartWeight[i] + (state.morphTargetWeight[i] - state.morphStartWeight[i]) * e;
|
|
205
208
|
// Check if weight actually changed (accounting for floating point precision)
|
|
@@ -277,7 +280,7 @@ export class Model {
|
|
|
277
280
|
const startMs = state.rotStartTimeMs[idx];
|
|
278
281
|
const prevDur = Math.max(1, state.rotDurationMs[idx]);
|
|
279
282
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur));
|
|
280
|
-
const e =
|
|
283
|
+
const e = t; // Linear interpolation
|
|
281
284
|
const startQuat = new Quat(state.rotStartQuat[qi], state.rotStartQuat[qi + 1], state.rotStartQuat[qi + 2], state.rotStartQuat[qi + 3]);
|
|
282
285
|
const targetQuat = new Quat(state.rotTargetQuat[qi], state.rotTargetQuat[qi + 1], state.rotTargetQuat[qi + 2], state.rotTargetQuat[qi + 3]);
|
|
283
286
|
const result = Quat.slerp(startQuat, targetQuat, e);
|
|
@@ -368,7 +371,7 @@ export class Model {
|
|
|
368
371
|
const startMs = state.transStartTimeMs[idx];
|
|
369
372
|
const prevDur = Math.max(1, state.transDurationMs[idx]);
|
|
370
373
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur));
|
|
371
|
-
const e =
|
|
374
|
+
const e = t; // Linear interpolation
|
|
372
375
|
sx = state.transStartVec[ti] + (state.transTargetVec[ti] - state.transStartVec[ti]) * e;
|
|
373
376
|
sy = state.transStartVec[ti + 1] + (state.transTargetVec[ti + 1] - state.transStartVec[ti + 1]) * e;
|
|
374
377
|
sz = state.transStartVec[ti + 2] + (state.transTargetVec[ti + 2] - state.transStartVec[ti + 2]) * e;
|
|
@@ -431,7 +434,7 @@ export class Model {
|
|
|
431
434
|
const startMs = state.morphStartTimeMs[idx];
|
|
432
435
|
const prevDur = Math.max(1, state.morphDurationMs[idx]);
|
|
433
436
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur));
|
|
434
|
-
const e =
|
|
437
|
+
const e = t; // Linear interpolation
|
|
435
438
|
startWeight = state.morphStartWeight[idx] + (state.morphTargetWeight[idx] - state.morphStartWeight[idx]) * e;
|
|
436
439
|
}
|
|
437
440
|
state.morphStartWeight[idx] = startWeight;
|
|
@@ -509,13 +512,10 @@ export class Model {
|
|
|
509
512
|
// Apply initial pose at time 0
|
|
510
513
|
this.animationTime = 0;
|
|
511
514
|
this.getPoseAtTime(0);
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
this.
|
|
515
|
-
this.morphsDirty = false;
|
|
515
|
+
if (this.physics) {
|
|
516
|
+
this.computeWorldMatrices();
|
|
517
|
+
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices);
|
|
516
518
|
}
|
|
517
|
-
// Compute world matrices after applying initial pose
|
|
518
|
-
this.computeWorldMatrices();
|
|
519
519
|
}
|
|
520
520
|
/**
|
|
521
521
|
* Process frames into tracks
|
|
@@ -556,6 +556,9 @@ export class Model {
|
|
|
556
556
|
for (const [name, frames] of groupFrames(morphItems).entries()) {
|
|
557
557
|
this.morphTracks.set(name, frames.map((f) => ({ morphFrame: f.item, time: f.time })));
|
|
558
558
|
}
|
|
559
|
+
// Reset cached indices when tracks change
|
|
560
|
+
this.boneTrackIndices.clear();
|
|
561
|
+
this.morphTrackIndices.clear();
|
|
559
562
|
// Calculate duration from all tracks
|
|
560
563
|
const allTracks = [...this.boneTracks.values(), ...this.morphTracks.values()];
|
|
561
564
|
this.animationDuration = allTracks.reduce((max, keyFrames) => {
|
|
@@ -563,69 +566,31 @@ export class Model {
|
|
|
563
566
|
return Math.max(max, lastTime);
|
|
564
567
|
}, 0);
|
|
565
568
|
}
|
|
566
|
-
/**
|
|
567
|
-
* Start or resume playback
|
|
568
|
-
*/
|
|
569
569
|
playAnimation() {
|
|
570
|
-
if (!this.animationData)
|
|
571
|
-
console.warn("[Model] Cannot play animation: no animation data loaded");
|
|
570
|
+
if (!this.animationData)
|
|
572
571
|
return;
|
|
573
|
-
}
|
|
574
572
|
this.isPaused = false;
|
|
575
573
|
this.isPlaying = true;
|
|
576
|
-
// Apply initial pose at current animation time
|
|
577
|
-
this.getPoseAtTime(this.animationTime);
|
|
578
|
-
// Apply morphs if animation changed them
|
|
579
|
-
if (this.morphsDirty) {
|
|
580
|
-
this.applyMorphs();
|
|
581
|
-
this.morphsDirty = false;
|
|
582
|
-
}
|
|
583
|
-
// Compute world matrices after applying pose
|
|
584
|
-
this.computeWorldMatrices();
|
|
585
|
-
// Reset physics when starting animation (prevents instability from sudden pose changes)
|
|
586
574
|
if (this.physics && this.animationTime === 0) {
|
|
575
|
+
this.computeWorldMatrices();
|
|
587
576
|
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices);
|
|
588
577
|
}
|
|
589
578
|
}
|
|
590
|
-
/**
|
|
591
|
-
* Pause playback
|
|
592
|
-
*/
|
|
593
579
|
pauseAnimation() {
|
|
594
580
|
if (!this.isPlaying || this.isPaused)
|
|
595
581
|
return;
|
|
596
582
|
this.isPaused = true;
|
|
597
583
|
}
|
|
598
|
-
/**
|
|
599
|
-
* Stop playback and reset to beginning
|
|
600
|
-
*/
|
|
601
584
|
stopAnimation() {
|
|
602
585
|
this.isPlaying = false;
|
|
603
586
|
this.isPaused = false;
|
|
604
587
|
this.animationTime = 0;
|
|
605
|
-
// Reset physics state when stopping animation (prevents instability from sudden pose changes)
|
|
606
|
-
if (this.physics) {
|
|
607
|
-
this.computeWorldMatrices();
|
|
608
|
-
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices);
|
|
609
|
-
}
|
|
610
588
|
}
|
|
611
|
-
/**
|
|
612
|
-
* Seek to specific time
|
|
613
|
-
* Immediately applies pose at the seeked time
|
|
614
|
-
*/
|
|
615
589
|
seekAnimation(time) {
|
|
616
590
|
if (!this.animationData)
|
|
617
591
|
return;
|
|
618
592
|
const clampedTime = Math.max(0, Math.min(time, this.animationDuration));
|
|
619
593
|
this.animationTime = clampedTime;
|
|
620
|
-
// Immediately apply pose at seeked time
|
|
621
|
-
this.getPoseAtTime(clampedTime);
|
|
622
|
-
// Apply morphs if animation changed them
|
|
623
|
-
if (this.morphsDirty) {
|
|
624
|
-
this.applyMorphs();
|
|
625
|
-
this.morphsDirty = false;
|
|
626
|
-
}
|
|
627
|
-
// Compute world matrices after applying pose
|
|
628
|
-
this.computeWorldMatrices();
|
|
629
594
|
}
|
|
630
595
|
/**
|
|
631
596
|
* Get current animation progress
|
|
@@ -639,75 +604,116 @@ export class Model {
|
|
|
639
604
|
percentage,
|
|
640
605
|
};
|
|
641
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* Binary search upper bound helper (static to avoid recreation)
|
|
609
|
+
*/
|
|
610
|
+
static upperBound(time, keyFrames, startIdx = 0) {
|
|
611
|
+
let left = startIdx, right = keyFrames.length;
|
|
612
|
+
while (left < right) {
|
|
613
|
+
const mid = Math.floor((left + right) / 2);
|
|
614
|
+
if (keyFrames[mid].time <= time)
|
|
615
|
+
left = mid + 1;
|
|
616
|
+
else
|
|
617
|
+
right = mid;
|
|
618
|
+
}
|
|
619
|
+
return left;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Find keyframe index with caching optimization
|
|
623
|
+
* Uses cached index as starting point for faster lookup when time is close
|
|
624
|
+
*/
|
|
625
|
+
findKeyframeIndex(time, keyFrames, cachedIdx) {
|
|
626
|
+
if (keyFrames.length === 0)
|
|
627
|
+
return -1;
|
|
628
|
+
// Check if cached index is still valid (time is within the cached frame range)
|
|
629
|
+
if (cachedIdx >= 0 && cachedIdx < keyFrames.length) {
|
|
630
|
+
const frameTime = keyFrames[cachedIdx].time;
|
|
631
|
+
const nextFrameTime = cachedIdx + 1 < keyFrames.length ? keyFrames[cachedIdx + 1].time : Infinity;
|
|
632
|
+
// If time is within [frameTime, nextFrameTime), use cached index
|
|
633
|
+
if (time >= frameTime && time < nextFrameTime) {
|
|
634
|
+
return cachedIdx;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// Fall back to binary search
|
|
638
|
+
const idx = Model.upperBound(time, keyFrames, 0) - 1;
|
|
639
|
+
return idx;
|
|
640
|
+
}
|
|
642
641
|
/**
|
|
643
642
|
* Get pose at specific time (internal helper)
|
|
643
|
+
* Optimized for per-frame performance
|
|
644
644
|
*/
|
|
645
645
|
getPoseAtTime(time) {
|
|
646
646
|
if (!this.animationData)
|
|
647
647
|
return;
|
|
648
|
-
|
|
649
|
-
const upperBound = (time, keyFrames) => {
|
|
650
|
-
let left = 0, right = keyFrames.length;
|
|
651
|
-
while (left < right) {
|
|
652
|
-
const mid = Math.floor((left + right) / 2);
|
|
653
|
-
if (keyFrames[mid].time <= time)
|
|
654
|
-
left = mid + 1;
|
|
655
|
-
else
|
|
656
|
-
right = mid;
|
|
657
|
-
}
|
|
658
|
-
return left;
|
|
659
|
-
};
|
|
648
|
+
const INV_127 = 1 / 127; // Pre-compute division constant
|
|
660
649
|
// Process bone tracks
|
|
661
650
|
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
662
651
|
if (keyFrames.length === 0)
|
|
663
652
|
continue;
|
|
653
|
+
const cachedIdx = this.boneTrackIndices.get(boneName) ?? -1;
|
|
664
654
|
const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time));
|
|
665
|
-
const idx =
|
|
655
|
+
const idx = this.findKeyframeIndex(clampedTime, keyFrames, cachedIdx);
|
|
666
656
|
if (idx < 0)
|
|
667
657
|
continue;
|
|
658
|
+
// Update cache
|
|
659
|
+
this.boneTrackIndices.set(boneName, idx);
|
|
668
660
|
const frameA = keyFrames[idx].boneFrame;
|
|
669
661
|
const frameB = keyFrames[idx + 1]?.boneFrame;
|
|
670
662
|
const boneIdx = this.runtimeSkeleton.nameIndex[boneName];
|
|
671
663
|
if (boneIdx === undefined)
|
|
672
664
|
continue;
|
|
665
|
+
const rotOffset = boneIdx * 4;
|
|
666
|
+
const transOffset = boneIdx * 3;
|
|
673
667
|
if (!frameB) {
|
|
674
|
-
|
|
675
|
-
this.runtimeSkeleton.localRotations[
|
|
676
|
-
this.runtimeSkeleton.localRotations[
|
|
677
|
-
this.runtimeSkeleton.localRotations[
|
|
678
|
-
this.runtimeSkeleton.
|
|
679
|
-
this.runtimeSkeleton.localTranslations[
|
|
680
|
-
this.runtimeSkeleton.localTranslations[
|
|
668
|
+
// No interpolation needed - direct assignment
|
|
669
|
+
this.runtimeSkeleton.localRotations[rotOffset] = frameA.rotation.x;
|
|
670
|
+
this.runtimeSkeleton.localRotations[rotOffset + 1] = frameA.rotation.y;
|
|
671
|
+
this.runtimeSkeleton.localRotations[rotOffset + 2] = frameA.rotation.z;
|
|
672
|
+
this.runtimeSkeleton.localRotations[rotOffset + 3] = frameA.rotation.w;
|
|
673
|
+
this.runtimeSkeleton.localTranslations[transOffset] = frameA.translation.x;
|
|
674
|
+
this.runtimeSkeleton.localTranslations[transOffset + 1] = frameA.translation.y;
|
|
675
|
+
this.runtimeSkeleton.localTranslations[transOffset + 2] = frameA.translation.z;
|
|
681
676
|
}
|
|
682
677
|
else {
|
|
683
678
|
const timeA = keyFrames[idx].time;
|
|
684
679
|
const timeB = keyFrames[idx + 1].time;
|
|
685
|
-
const
|
|
680
|
+
const timeDelta = timeB - timeA;
|
|
681
|
+
const gradient = (clampedTime - timeA) / timeDelta;
|
|
686
682
|
const interp = frameB.interpolation;
|
|
687
|
-
//
|
|
688
|
-
const rotT = bezierInterpolate(interp[0]
|
|
683
|
+
// Pre-normalize interpolation values (avoid division in bezierInterpolate)
|
|
684
|
+
const rotT = bezierInterpolate(interp[0] * INV_127, interp[1] * INV_127, interp[2] * INV_127, interp[3] * INV_127, gradient);
|
|
685
|
+
// Use Quat.slerp but extract components directly to avoid object allocation
|
|
689
686
|
const rotation = Quat.slerp(frameA.rotation, frameB.rotation, rotT);
|
|
690
687
|
// Interpolate translation using bezier for each component
|
|
691
|
-
|
|
692
|
-
const
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
this.runtimeSkeleton.localRotations[
|
|
698
|
-
this.runtimeSkeleton.
|
|
699
|
-
this.runtimeSkeleton.
|
|
700
|
-
this.runtimeSkeleton.
|
|
688
|
+
// Inline getWeight to avoid function call overhead
|
|
689
|
+
const getWeight = (offset) => bezierInterpolate(interp[offset] * INV_127, interp[offset + 8] * INV_127, interp[offset + 4] * INV_127, interp[offset + 12] * INV_127, gradient);
|
|
690
|
+
const txWeight = getWeight(0);
|
|
691
|
+
const tyWeight = getWeight(16);
|
|
692
|
+
const tzWeight = getWeight(32);
|
|
693
|
+
// Direct array writes instead of Vec3 allocation
|
|
694
|
+
this.runtimeSkeleton.localRotations[rotOffset] = rotation.x;
|
|
695
|
+
this.runtimeSkeleton.localRotations[rotOffset + 1] = rotation.y;
|
|
696
|
+
this.runtimeSkeleton.localRotations[rotOffset + 2] = rotation.z;
|
|
697
|
+
this.runtimeSkeleton.localRotations[rotOffset + 3] = rotation.w;
|
|
698
|
+
this.runtimeSkeleton.localTranslations[transOffset] =
|
|
699
|
+
frameA.translation.x + (frameB.translation.x - frameA.translation.x) * txWeight;
|
|
700
|
+
this.runtimeSkeleton.localTranslations[transOffset + 1] =
|
|
701
|
+
frameA.translation.y + (frameB.translation.y - frameA.translation.y) * tyWeight;
|
|
702
|
+
this.runtimeSkeleton.localTranslations[transOffset + 2] =
|
|
703
|
+
frameA.translation.z + (frameB.translation.z - frameA.translation.z) * tzWeight;
|
|
701
704
|
}
|
|
702
705
|
}
|
|
703
706
|
// Process morph tracks
|
|
704
707
|
for (const [morphName, keyFrames] of this.morphTracks.entries()) {
|
|
705
708
|
if (keyFrames.length === 0)
|
|
706
709
|
continue;
|
|
710
|
+
const cachedIdx = this.morphTrackIndices.get(morphName) ?? -1;
|
|
707
711
|
const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time));
|
|
708
|
-
const idx =
|
|
712
|
+
const idx = this.findKeyframeIndex(clampedTime, keyFrames, cachedIdx);
|
|
709
713
|
if (idx < 0)
|
|
710
714
|
continue;
|
|
715
|
+
// Update cache
|
|
716
|
+
this.morphTrackIndices.set(morphName, idx);
|
|
711
717
|
const frameA = keyFrames[idx].morphFrame;
|
|
712
718
|
const frameB = keyFrames[idx + 1]?.morphFrame;
|
|
713
719
|
const morphIdx = this.runtimeMorph.nameIndex[morphName];
|
package/package.json
CHANGED
package/src/model.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Mat4, Quat, Vec3,
|
|
1
|
+
import { Mat4, Quat, Vec3, bezierInterpolate } from "./math"
|
|
2
2
|
import { Rigidbody, Joint, Physics } from "./physics"
|
|
3
3
|
import { IKSolverSystem } from "./ik-solver"
|
|
4
4
|
import { VMDKeyFrame, VMDLoader, BoneFrame, MorphFrame } from "./vmd-loader"
|
|
@@ -189,6 +189,10 @@ export class Model {
|
|
|
189
189
|
private isPaused: boolean = false
|
|
190
190
|
private animationTime: number = 0 // Current time in animation (seconds)
|
|
191
191
|
|
|
192
|
+
// Cached keyframe indices for faster lookup (per track)
|
|
193
|
+
private boneTrackIndices: Map<string, number> = new Map()
|
|
194
|
+
private morphTrackIndices: Map<string, number> = new Map()
|
|
195
|
+
|
|
192
196
|
// Physics runtime
|
|
193
197
|
private physics: Physics | null = null
|
|
194
198
|
|
|
@@ -354,7 +358,7 @@ export class Model {
|
|
|
354
358
|
const startMs = state.rotStartTimeMs[i]
|
|
355
359
|
const durMs = Math.max(1, state.rotDurationMs[i])
|
|
356
360
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
|
|
357
|
-
const e =
|
|
361
|
+
const e = t // Linear interpolation
|
|
358
362
|
|
|
359
363
|
const qi = i * 4
|
|
360
364
|
const startQuat = new Quat(
|
|
@@ -389,7 +393,7 @@ export class Model {
|
|
|
389
393
|
const startMs = state.transStartTimeMs[i]
|
|
390
394
|
const durMs = Math.max(1, state.transDurationMs[i])
|
|
391
395
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
|
|
392
|
-
const e =
|
|
396
|
+
const e = t // Linear interpolation
|
|
393
397
|
|
|
394
398
|
const ti = i * 3
|
|
395
399
|
translations[ti] = state.transStartVec[ti] + (state.transTargetVec[ti] - state.transStartVec[ti]) * e
|
|
@@ -412,7 +416,7 @@ export class Model {
|
|
|
412
416
|
const startMs = state.morphStartTimeMs[i]
|
|
413
417
|
const durMs = Math.max(1, state.morphDurationMs[i])
|
|
414
418
|
const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
|
|
415
|
-
const e =
|
|
419
|
+
const e = t // Linear interpolation
|
|
416
420
|
|
|
417
421
|
const oldWeight = weights[i]
|
|
418
422
|
weights[i] = state.morphStartWeight[i] + (state.morphTargetWeight[i] - state.morphStartWeight[i]) * e
|
|
@@ -510,7 +514,7 @@ export class Model {
|
|
|
510
514
|
const startMs = state.rotStartTimeMs[idx]
|
|
511
515
|
const prevDur = Math.max(1, state.rotDurationMs[idx])
|
|
512
516
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
|
|
513
|
-
const e =
|
|
517
|
+
const e = t // Linear interpolation
|
|
514
518
|
const startQuat = new Quat(
|
|
515
519
|
state.rotStartQuat[qi],
|
|
516
520
|
state.rotStartQuat[qi + 1],
|
|
@@ -628,7 +632,7 @@ export class Model {
|
|
|
628
632
|
const startMs = state.transStartTimeMs[idx]
|
|
629
633
|
const prevDur = Math.max(1, state.transDurationMs[idx])
|
|
630
634
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
|
|
631
|
-
const e =
|
|
635
|
+
const e = t // Linear interpolation
|
|
632
636
|
sx = state.transStartVec[ti] + (state.transTargetVec[ti] - state.transStartVec[ti]) * e
|
|
633
637
|
sy = state.transStartVec[ti + 1] + (state.transTargetVec[ti + 1] - state.transStartVec[ti + 1]) * e
|
|
634
638
|
sz = state.transStartVec[ti + 2] + (state.transTargetVec[ti + 2] - state.transStartVec[ti + 2]) * e
|
|
@@ -703,7 +707,7 @@ export class Model {
|
|
|
703
707
|
const startMs = state.morphStartTimeMs[idx]
|
|
704
708
|
const prevDur = Math.max(1, state.morphDurationMs[idx])
|
|
705
709
|
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
|
|
706
|
-
const e =
|
|
710
|
+
const e = t // Linear interpolation
|
|
707
711
|
startWeight = state.morphStartWeight[idx] + (state.morphTargetWeight[idx] - state.morphStartWeight[idx]) * e
|
|
708
712
|
}
|
|
709
713
|
|
|
@@ -793,14 +797,10 @@ export class Model {
|
|
|
793
797
|
this.animationTime = 0
|
|
794
798
|
this.getPoseAtTime(0)
|
|
795
799
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
this.
|
|
799
|
-
this.morphsDirty = false
|
|
800
|
+
if (this.physics) {
|
|
801
|
+
this.computeWorldMatrices()
|
|
802
|
+
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices)
|
|
800
803
|
}
|
|
801
|
-
|
|
802
|
-
// Compute world matrices after applying initial pose
|
|
803
|
-
this.computeWorldMatrices()
|
|
804
804
|
}
|
|
805
805
|
|
|
806
806
|
/**
|
|
@@ -854,6 +854,10 @@ export class Model {
|
|
|
854
854
|
)
|
|
855
855
|
}
|
|
856
856
|
|
|
857
|
+
// Reset cached indices when tracks change
|
|
858
|
+
this.boneTrackIndices.clear()
|
|
859
|
+
this.morphTrackIndices.clear()
|
|
860
|
+
|
|
857
861
|
// Calculate duration from all tracks
|
|
858
862
|
const allTracks = [...this.boneTracks.values(), ...this.morphTracks.values()]
|
|
859
863
|
this.animationDuration = allTracks.reduce((max, keyFrames) => {
|
|
@@ -862,79 +866,33 @@ export class Model {
|
|
|
862
866
|
}, 0)
|
|
863
867
|
}
|
|
864
868
|
|
|
865
|
-
/**
|
|
866
|
-
* Start or resume playback
|
|
867
|
-
*/
|
|
868
869
|
playAnimation(): void {
|
|
869
|
-
if (!this.animationData)
|
|
870
|
-
console.warn("[Model] Cannot play animation: no animation data loaded")
|
|
871
|
-
return
|
|
872
|
-
}
|
|
870
|
+
if (!this.animationData) return
|
|
873
871
|
|
|
874
872
|
this.isPaused = false
|
|
875
873
|
this.isPlaying = true
|
|
876
874
|
|
|
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
875
|
if (this.physics && this.animationTime === 0) {
|
|
876
|
+
this.computeWorldMatrices()
|
|
891
877
|
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices)
|
|
892
878
|
}
|
|
893
879
|
}
|
|
894
880
|
|
|
895
|
-
/**
|
|
896
|
-
* Pause playback
|
|
897
|
-
*/
|
|
898
881
|
pauseAnimation(): void {
|
|
899
882
|
if (!this.isPlaying || this.isPaused) return
|
|
900
883
|
this.isPaused = true
|
|
901
884
|
}
|
|
902
885
|
|
|
903
|
-
/**
|
|
904
|
-
* Stop playback and reset to beginning
|
|
905
|
-
*/
|
|
906
886
|
stopAnimation(): void {
|
|
907
887
|
this.isPlaying = false
|
|
908
888
|
this.isPaused = false
|
|
909
889
|
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
890
|
}
|
|
917
891
|
|
|
918
|
-
/**
|
|
919
|
-
* Seek to specific time
|
|
920
|
-
* Immediately applies pose at the seeked time
|
|
921
|
-
*/
|
|
922
892
|
seekAnimation(time: number): void {
|
|
923
893
|
if (!this.animationData) return
|
|
924
|
-
|
|
925
894
|
const clampedTime = Math.max(0, Math.min(time, this.animationDuration))
|
|
926
895
|
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
896
|
}
|
|
939
897
|
|
|
940
898
|
/**
|
|
@@ -950,80 +908,128 @@ export class Model {
|
|
|
950
908
|
}
|
|
951
909
|
}
|
|
952
910
|
|
|
911
|
+
/**
|
|
912
|
+
* Binary search upper bound helper (static to avoid recreation)
|
|
913
|
+
*/
|
|
914
|
+
private static upperBound<T extends { time: number }>(time: number, keyFrames: T[], startIdx: number = 0): number {
|
|
915
|
+
let left = startIdx,
|
|
916
|
+
right = keyFrames.length
|
|
917
|
+
while (left < right) {
|
|
918
|
+
const mid = Math.floor((left + right) / 2)
|
|
919
|
+
if (keyFrames[mid].time <= time) left = mid + 1
|
|
920
|
+
else right = mid
|
|
921
|
+
}
|
|
922
|
+
return left
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Find keyframe index with caching optimization
|
|
927
|
+
* Uses cached index as starting point for faster lookup when time is close
|
|
928
|
+
*/
|
|
929
|
+
private findKeyframeIndex<T extends { time: number }>(time: number, keyFrames: T[], cachedIdx: number): number {
|
|
930
|
+
if (keyFrames.length === 0) return -1
|
|
931
|
+
|
|
932
|
+
// Check if cached index is still valid (time is within the cached frame range)
|
|
933
|
+
if (cachedIdx >= 0 && cachedIdx < keyFrames.length) {
|
|
934
|
+
const frameTime = keyFrames[cachedIdx].time
|
|
935
|
+
const nextFrameTime = cachedIdx + 1 < keyFrames.length ? keyFrames[cachedIdx + 1].time : Infinity
|
|
936
|
+
|
|
937
|
+
// If time is within [frameTime, nextFrameTime), use cached index
|
|
938
|
+
if (time >= frameTime && time < nextFrameTime) {
|
|
939
|
+
return cachedIdx
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Fall back to binary search
|
|
944
|
+
const idx = Model.upperBound(time, keyFrames, 0) - 1
|
|
945
|
+
return idx
|
|
946
|
+
}
|
|
947
|
+
|
|
953
948
|
/**
|
|
954
949
|
* Get pose at specific time (internal helper)
|
|
950
|
+
* Optimized for per-frame performance
|
|
955
951
|
*/
|
|
956
952
|
private getPoseAtTime(time: number): void {
|
|
957
953
|
if (!this.animationData) return
|
|
958
954
|
|
|
959
|
-
|
|
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
|
-
}
|
|
955
|
+
const INV_127 = 1 / 127 // Pre-compute division constant
|
|
970
956
|
|
|
971
957
|
// Process bone tracks
|
|
972
958
|
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
973
959
|
if (keyFrames.length === 0) continue
|
|
974
960
|
|
|
961
|
+
const cachedIdx = this.boneTrackIndices.get(boneName) ?? -1
|
|
975
962
|
const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time))
|
|
976
|
-
const idx =
|
|
963
|
+
const idx = this.findKeyframeIndex(clampedTime, keyFrames, cachedIdx)
|
|
964
|
+
|
|
977
965
|
if (idx < 0) continue
|
|
978
966
|
|
|
967
|
+
// Update cache
|
|
968
|
+
this.boneTrackIndices.set(boneName, idx)
|
|
969
|
+
|
|
979
970
|
const frameA = keyFrames[idx].boneFrame
|
|
980
971
|
const frameB = keyFrames[idx + 1]?.boneFrame
|
|
981
972
|
|
|
982
973
|
const boneIdx = this.runtimeSkeleton.nameIndex[boneName]
|
|
983
974
|
if (boneIdx === undefined) continue
|
|
984
975
|
|
|
976
|
+
const rotOffset = boneIdx * 4
|
|
977
|
+
const transOffset = boneIdx * 3
|
|
978
|
+
|
|
985
979
|
if (!frameB) {
|
|
986
|
-
|
|
987
|
-
this.runtimeSkeleton.localRotations[
|
|
988
|
-
this.runtimeSkeleton.localRotations[
|
|
989
|
-
this.runtimeSkeleton.localRotations[
|
|
990
|
-
this.runtimeSkeleton.
|
|
991
|
-
this.runtimeSkeleton.localTranslations[
|
|
992
|
-
this.runtimeSkeleton.localTranslations[
|
|
980
|
+
// No interpolation needed - direct assignment
|
|
981
|
+
this.runtimeSkeleton.localRotations[rotOffset] = frameA.rotation.x
|
|
982
|
+
this.runtimeSkeleton.localRotations[rotOffset + 1] = frameA.rotation.y
|
|
983
|
+
this.runtimeSkeleton.localRotations[rotOffset + 2] = frameA.rotation.z
|
|
984
|
+
this.runtimeSkeleton.localRotations[rotOffset + 3] = frameA.rotation.w
|
|
985
|
+
this.runtimeSkeleton.localTranslations[transOffset] = frameA.translation.x
|
|
986
|
+
this.runtimeSkeleton.localTranslations[transOffset + 1] = frameA.translation.y
|
|
987
|
+
this.runtimeSkeleton.localTranslations[transOffset + 2] = frameA.translation.z
|
|
993
988
|
} else {
|
|
994
989
|
const timeA = keyFrames[idx].time
|
|
995
990
|
const timeB = keyFrames[idx + 1].time
|
|
996
|
-
const
|
|
991
|
+
const timeDelta = timeB - timeA
|
|
992
|
+
const gradient = (clampedTime - timeA) / timeDelta
|
|
997
993
|
const interp = frameB.interpolation
|
|
998
994
|
|
|
999
|
-
//
|
|
1000
|
-
const rotT = bezierInterpolate(
|
|
995
|
+
// Pre-normalize interpolation values (avoid division in bezierInterpolate)
|
|
996
|
+
const rotT = bezierInterpolate(
|
|
997
|
+
interp[0] * INV_127,
|
|
998
|
+
interp[1] * INV_127,
|
|
999
|
+
interp[2] * INV_127,
|
|
1000
|
+
interp[3] * INV_127,
|
|
1001
|
+
gradient
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
// Use Quat.slerp but extract components directly to avoid object allocation
|
|
1001
1005
|
const rotation = Quat.slerp(frameA.rotation, frameB.rotation, rotT)
|
|
1002
1006
|
|
|
1003
1007
|
// Interpolate translation using bezier for each component
|
|
1008
|
+
// Inline getWeight to avoid function call overhead
|
|
1004
1009
|
const getWeight = (offset: number) =>
|
|
1005
1010
|
bezierInterpolate(
|
|
1006
|
-
interp[offset]
|
|
1007
|
-
interp[offset + 8]
|
|
1008
|
-
interp[offset + 4]
|
|
1009
|
-
interp[offset + 12]
|
|
1011
|
+
interp[offset] * INV_127,
|
|
1012
|
+
interp[offset + 8] * INV_127,
|
|
1013
|
+
interp[offset + 4] * INV_127,
|
|
1014
|
+
interp[offset + 12] * INV_127,
|
|
1010
1015
|
gradient
|
|
1011
1016
|
)
|
|
1012
1017
|
|
|
1013
|
-
const
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
this.runtimeSkeleton.localRotations[
|
|
1021
|
-
this.runtimeSkeleton.localRotations[
|
|
1022
|
-
this.runtimeSkeleton.
|
|
1023
|
-
|
|
1024
|
-
this.runtimeSkeleton.localTranslations[
|
|
1025
|
-
|
|
1026
|
-
this.runtimeSkeleton.localTranslations[
|
|
1018
|
+
const txWeight = getWeight(0)
|
|
1019
|
+
const tyWeight = getWeight(16)
|
|
1020
|
+
const tzWeight = getWeight(32)
|
|
1021
|
+
|
|
1022
|
+
// Direct array writes instead of Vec3 allocation
|
|
1023
|
+
this.runtimeSkeleton.localRotations[rotOffset] = rotation.x
|
|
1024
|
+
this.runtimeSkeleton.localRotations[rotOffset + 1] = rotation.y
|
|
1025
|
+
this.runtimeSkeleton.localRotations[rotOffset + 2] = rotation.z
|
|
1026
|
+
this.runtimeSkeleton.localRotations[rotOffset + 3] = rotation.w
|
|
1027
|
+
this.runtimeSkeleton.localTranslations[transOffset] =
|
|
1028
|
+
frameA.translation.x + (frameB.translation.x - frameA.translation.x) * txWeight
|
|
1029
|
+
this.runtimeSkeleton.localTranslations[transOffset + 1] =
|
|
1030
|
+
frameA.translation.y + (frameB.translation.y - frameA.translation.y) * tyWeight
|
|
1031
|
+
this.runtimeSkeleton.localTranslations[transOffset + 2] =
|
|
1032
|
+
frameA.translation.z + (frameB.translation.z - frameA.translation.z) * tzWeight
|
|
1027
1033
|
}
|
|
1028
1034
|
}
|
|
1029
1035
|
|
|
@@ -1031,10 +1037,15 @@ export class Model {
|
|
|
1031
1037
|
for (const [morphName, keyFrames] of this.morphTracks.entries()) {
|
|
1032
1038
|
if (keyFrames.length === 0) continue
|
|
1033
1039
|
|
|
1040
|
+
const cachedIdx = this.morphTrackIndices.get(morphName) ?? -1
|
|
1034
1041
|
const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time))
|
|
1035
|
-
const idx =
|
|
1042
|
+
const idx = this.findKeyframeIndex(clampedTime, keyFrames, cachedIdx)
|
|
1043
|
+
|
|
1036
1044
|
if (idx < 0) continue
|
|
1037
1045
|
|
|
1046
|
+
// Update cache
|
|
1047
|
+
this.morphTrackIndices.set(morphName, idx)
|
|
1048
|
+
|
|
1038
1049
|
const frameA = keyFrames[idx].morphFrame
|
|
1039
1050
|
const frameB = keyFrames[idx + 1]?.morphFrame
|
|
1040
1051
|
|