reze-engine 0.3.8 → 0.3.9

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 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
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
package/dist/model.js CHANGED
@@ -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)
@@ -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
- // Apply morphs if animation changed them
513
- if (this.morphsDirty) {
514
- this.applyMorphs();
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
- // Helper for binary search upper bound
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 = upperBound(clampedTime, keyFrames) - 1;
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
- this.runtimeSkeleton.localRotations[boneIdx * 4] = frameA.rotation.x;
675
- this.runtimeSkeleton.localRotations[boneIdx * 4 + 1] = frameA.rotation.y;
676
- this.runtimeSkeleton.localRotations[boneIdx * 4 + 2] = frameA.rotation.z;
677
- this.runtimeSkeleton.localRotations[boneIdx * 4 + 3] = frameA.rotation.w;
678
- this.runtimeSkeleton.localTranslations[boneIdx * 3] = frameA.translation.x;
679
- this.runtimeSkeleton.localTranslations[boneIdx * 3 + 1] = frameA.translation.y;
680
- this.runtimeSkeleton.localTranslations[boneIdx * 3 + 2] = frameA.translation.z;
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 gradient = (clampedTime - timeA) / (timeB - timeA);
680
+ const timeDelta = timeB - timeA;
681
+ const gradient = (clampedTime - timeA) / timeDelta;
686
682
  const interp = frameB.interpolation;
687
- // Interpolate rotation using SLERP with bezier
688
- const rotT = bezierInterpolate(interp[0] / 127, interp[1] / 127, interp[2] / 127, interp[3] / 127, gradient);
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
- const getWeight = (offset) => bezierInterpolate(interp[offset] / 127, interp[offset + 8] / 127, interp[offset + 4] / 127, interp[offset + 12] / 127, gradient);
692
- const lerp = (a, b, w) => a + (b - a) * w;
693
- const translation = new Vec3(lerp(frameA.translation.x, frameB.translation.x, getWeight(0)), lerp(frameA.translation.y, frameB.translation.y, getWeight(16)), lerp(frameA.translation.z, frameB.translation.z, getWeight(32)));
694
- this.runtimeSkeleton.localRotations[boneIdx * 4] = rotation.x;
695
- this.runtimeSkeleton.localRotations[boneIdx * 4 + 1] = rotation.y;
696
- this.runtimeSkeleton.localRotations[boneIdx * 4 + 2] = rotation.z;
697
- this.runtimeSkeleton.localRotations[boneIdx * 4 + 3] = rotation.w;
698
- this.runtimeSkeleton.localTranslations[boneIdx * 3] = translation.x;
699
- this.runtimeSkeleton.localTranslations[boneIdx * 3 + 1] = translation.y;
700
- this.runtimeSkeleton.localTranslations[boneIdx * 3 + 2] = translation.z;
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 = upperBound(clampedTime, keyFrames) - 1;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "A WebGPU-based MMD model renderer",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/src/model.ts CHANGED
@@ -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
 
@@ -793,14 +797,10 @@ export class Model {
793
797
  this.animationTime = 0
794
798
  this.getPoseAtTime(0)
795
799
 
796
- // Apply morphs if animation changed them
797
- if (this.morphsDirty) {
798
- this.applyMorphs()
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
- // 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
- }
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 = upperBound(clampedTime, keyFrames) - 1
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
- 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
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 gradient = (clampedTime - timeA) / (timeB - timeA)
991
+ const timeDelta = timeB - timeA
992
+ const gradient = (clampedTime - timeA) / timeDelta
997
993
  const interp = frameB.interpolation
998
994
 
999
- // Interpolate rotation using SLERP with bezier
1000
- const rotT = bezierInterpolate(interp[0] / 127, interp[1] / 127, interp[2] / 127, interp[3] / 127, gradient)
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] / 127,
1007
- interp[offset + 8] / 127,
1008
- interp[offset + 4] / 127,
1009
- interp[offset + 12] / 127,
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 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
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 = upperBound(clampedTime, keyFrames) - 1
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