reze-engine 0.6.6 → 0.7.0
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 +83 -6
- package/dist/animation.d.ts +41 -0
- package/dist/animation.d.ts.map +1 -0
- package/dist/animation.js +62 -0
- package/dist/engine.d.ts +3 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/math.d.ts +0 -10
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +0 -36
- package/dist/model.d.ts +9 -5
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +92 -87
- package/package.json +1 -1
- package/src/animation.ts +109 -0
- package/src/engine.ts +10 -0
- package/src/index.ts +2 -0
- package/src/math.ts +0 -42
- package/src/model.ts +116 -129
- package/dist/runtime-bone.d.ts +0 -49
- package/dist/runtime-bone.d.ts.map +0 -1
- package/dist/runtime-bone.js +0 -121
package/dist/model.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { Mat4, Quat, Vec3
|
|
1
|
+
import { Mat4, Quat, Vec3 } from "./math";
|
|
2
2
|
import { Physics } from "./physics";
|
|
3
3
|
import { IKSolverSystem } from "./ik-solver";
|
|
4
4
|
import { VMDLoader } from "./vmd-loader";
|
|
5
|
+
import { interpolateControlPoints, rawInterpolationToBoneInterpolation } from "./animation";
|
|
6
|
+
const VMD_FPS = 30;
|
|
5
7
|
const VERTEX_STRIDE = 8;
|
|
6
8
|
export class Model {
|
|
7
9
|
constructor(vertexData, indexData, textures, materials, skeleton, skinning, morphing, rigidbodies = [], joints = []) {
|
|
@@ -16,7 +18,8 @@ export class Model {
|
|
|
16
18
|
this.cachedIdentityMat2 = Mat4.identity();
|
|
17
19
|
this.tweenTimeMs = 0; // Time tracking for tweens (milliseconds)
|
|
18
20
|
// Animation runtime
|
|
19
|
-
this.
|
|
21
|
+
this._hasAnimation = false;
|
|
22
|
+
this._animationData = null;
|
|
20
23
|
this.boneTracks = new Map();
|
|
21
24
|
this.morphTracks = new Map();
|
|
22
25
|
this.animationDuration = 0;
|
|
@@ -525,11 +528,78 @@ export class Model {
|
|
|
525
528
|
* Load VMD animation file
|
|
526
529
|
*/
|
|
527
530
|
async loadVmd(vmdUrl) {
|
|
528
|
-
|
|
531
|
+
const vmdKeyFrames = await VMDLoader.load(vmdUrl);
|
|
532
|
+
// Convert VMDKeyFrame[] to AnimationData
|
|
533
|
+
const boneTracks = {};
|
|
534
|
+
const morphTracks = {};
|
|
535
|
+
for (const keyFrame of vmdKeyFrames) {
|
|
536
|
+
for (const bf of keyFrame.boneFrames) {
|
|
537
|
+
if (!boneTracks[bf.boneName])
|
|
538
|
+
boneTracks[bf.boneName] = [];
|
|
539
|
+
boneTracks[bf.boneName].push({
|
|
540
|
+
frame: bf.frame,
|
|
541
|
+
rotation: bf.rotation,
|
|
542
|
+
translation: bf.translation,
|
|
543
|
+
interpolation: rawInterpolationToBoneInterpolation(bf.interpolation),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
for (const mf of keyFrame.morphFrames) {
|
|
547
|
+
if (!morphTracks[mf.morphName])
|
|
548
|
+
morphTracks[mf.morphName] = [];
|
|
549
|
+
morphTracks[mf.morphName].push({
|
|
550
|
+
frame: mf.frame,
|
|
551
|
+
weight: mf.weight,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
this.loadAnimationData({ boneTracks, morphTracks });
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Load animation from structured keyframe data.
|
|
559
|
+
* This is the primary way to set animation data — loadVmd delegates to this.
|
|
560
|
+
*/
|
|
561
|
+
loadAnimationData(data) {
|
|
562
|
+
this._animationData = data;
|
|
529
563
|
this.resetAllBones();
|
|
530
564
|
this.resetAllMorphs();
|
|
531
|
-
this.
|
|
532
|
-
|
|
565
|
+
this.boneTracks = new Map();
|
|
566
|
+
for (const name in data.boneTracks) {
|
|
567
|
+
const keyframes = data.boneTracks[name];
|
|
568
|
+
const sorted = [...keyframes].sort((a, b) => a.frame - b.frame);
|
|
569
|
+
this.boneTracks.set(name, sorted.map((kf) => ({
|
|
570
|
+
boneName: name,
|
|
571
|
+
frame: kf.frame,
|
|
572
|
+
rotation: kf.rotation,
|
|
573
|
+
translation: kf.translation,
|
|
574
|
+
interpolation: kf.interpolation,
|
|
575
|
+
time: kf.frame / VMD_FPS,
|
|
576
|
+
})));
|
|
577
|
+
}
|
|
578
|
+
this.morphTracks = new Map();
|
|
579
|
+
for (const name in data.morphTracks) {
|
|
580
|
+
const keyframes = data.morphTracks[name];
|
|
581
|
+
const sorted = [...keyframes].sort((a, b) => a.frame - b.frame);
|
|
582
|
+
this.morphTracks.set(name, sorted.map((kf) => ({
|
|
583
|
+
morphName: name,
|
|
584
|
+
frame: kf.frame,
|
|
585
|
+
weight: kf.weight,
|
|
586
|
+
time: kf.frame / VMD_FPS,
|
|
587
|
+
})));
|
|
588
|
+
}
|
|
589
|
+
this.boneTrackIndices.clear();
|
|
590
|
+
this.morphTrackIndices.clear();
|
|
591
|
+
// Calculate duration
|
|
592
|
+
let maxTime = 0;
|
|
593
|
+
for (const frames of this.boneTracks.values()) {
|
|
594
|
+
if (frames.length > 0)
|
|
595
|
+
maxTime = Math.max(maxTime, frames[frames.length - 1].time);
|
|
596
|
+
}
|
|
597
|
+
for (const frames of this.morphTracks.values()) {
|
|
598
|
+
if (frames.length > 0)
|
|
599
|
+
maxTime = Math.max(maxTime, frames[frames.length - 1].time);
|
|
600
|
+
}
|
|
601
|
+
this.animationDuration = maxTime;
|
|
602
|
+
this._hasAnimation = true;
|
|
533
603
|
this.animationTime = 0;
|
|
534
604
|
this.getPoseAtTime(0);
|
|
535
605
|
if (this.physics) {
|
|
@@ -573,57 +643,8 @@ export class Model {
|
|
|
573
643
|
setPhysicsEnabled(enabled) {
|
|
574
644
|
this.physicsEnabled = enabled;
|
|
575
645
|
}
|
|
576
|
-
/**
|
|
577
|
-
* Process frames into tracks
|
|
578
|
-
*/
|
|
579
|
-
processFrames() {
|
|
580
|
-
if (!this.animationData)
|
|
581
|
-
return;
|
|
582
|
-
// Helper to group frames by name and sort by time
|
|
583
|
-
const groupFrames = (items) => {
|
|
584
|
-
const tracks = new Map();
|
|
585
|
-
for (const { item, name, time } of items) {
|
|
586
|
-
if (!tracks.has(name))
|
|
587
|
-
tracks.set(name, []);
|
|
588
|
-
tracks.get(name).push({ item, time });
|
|
589
|
-
}
|
|
590
|
-
for (const keyFrames of tracks.values()) {
|
|
591
|
-
keyFrames.sort((a, b) => a.time - b.time);
|
|
592
|
-
}
|
|
593
|
-
return tracks;
|
|
594
|
-
};
|
|
595
|
-
// Collect all bone and morph frames
|
|
596
|
-
const boneItems = [];
|
|
597
|
-
const morphItems = [];
|
|
598
|
-
for (const keyFrame of this.animationData) {
|
|
599
|
-
for (const boneFrame of keyFrame.boneFrames) {
|
|
600
|
-
boneItems.push({ item: boneFrame, name: boneFrame.boneName, time: keyFrame.time });
|
|
601
|
-
}
|
|
602
|
-
for (const morphFrame of keyFrame.morphFrames) {
|
|
603
|
-
morphItems.push({ item: morphFrame, name: morphFrame.morphName, time: keyFrame.time });
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
// Transform to expected format
|
|
607
|
-
this.boneTracks = new Map();
|
|
608
|
-
for (const [name, frames] of groupFrames(boneItems).entries()) {
|
|
609
|
-
this.boneTracks.set(name, frames.map((f) => ({ boneFrame: f.item, time: f.time })));
|
|
610
|
-
}
|
|
611
|
-
this.morphTracks = new Map();
|
|
612
|
-
for (const [name, frames] of groupFrames(morphItems).entries()) {
|
|
613
|
-
this.morphTracks.set(name, frames.map((f) => ({ morphFrame: f.item, time: f.time })));
|
|
614
|
-
}
|
|
615
|
-
// Reset cached indices when tracks change
|
|
616
|
-
this.boneTrackIndices.clear();
|
|
617
|
-
this.morphTrackIndices.clear();
|
|
618
|
-
// Calculate duration from all tracks
|
|
619
|
-
const allTracks = [...this.boneTracks.values(), ...this.morphTracks.values()];
|
|
620
|
-
this.animationDuration = allTracks.reduce((max, keyFrames) => {
|
|
621
|
-
const lastTime = keyFrames[keyFrames.length - 1]?.time ?? 0;
|
|
622
|
-
return Math.max(max, lastTime);
|
|
623
|
-
}, 0);
|
|
624
|
-
}
|
|
625
646
|
playAnimation() {
|
|
626
|
-
if (!this.
|
|
647
|
+
if (!this._hasAnimation)
|
|
627
648
|
return;
|
|
628
649
|
this.isPaused = false;
|
|
629
650
|
this.isPlaying = true;
|
|
@@ -643,7 +664,7 @@ export class Model {
|
|
|
643
664
|
this.animationTime = 0;
|
|
644
665
|
}
|
|
645
666
|
seekAnimation(time) {
|
|
646
|
-
if (!this.
|
|
667
|
+
if (!this._hasAnimation)
|
|
647
668
|
return;
|
|
648
669
|
const clampedTime = Math.max(0, Math.min(time, this.animationDuration));
|
|
649
670
|
this.animationTime = clampedTime;
|
|
@@ -651,6 +672,9 @@ export class Model {
|
|
|
651
672
|
/**
|
|
652
673
|
* Get current animation progress
|
|
653
674
|
*/
|
|
675
|
+
getAnimationData() {
|
|
676
|
+
return this._animationData;
|
|
677
|
+
}
|
|
654
678
|
getAnimationProgress() {
|
|
655
679
|
const duration = this.animationDuration;
|
|
656
680
|
const percentage = duration > 0 ? (this.animationTime / duration) * 100 : 0;
|
|
@@ -699,9 +723,8 @@ export class Model {
|
|
|
699
723
|
* Optimized for per-frame performance
|
|
700
724
|
*/
|
|
701
725
|
getPoseAtTime(time) {
|
|
702
|
-
if (!this.
|
|
726
|
+
if (!this._hasAnimation)
|
|
703
727
|
return;
|
|
704
|
-
const INV_127 = 1 / 127; // Pre-compute division constant
|
|
705
728
|
// Process bone tracks
|
|
706
729
|
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
707
730
|
if (keyFrames.length === 0)
|
|
@@ -711,48 +734,31 @@ export class Model {
|
|
|
711
734
|
const idx = this.findKeyframeIndex(clampedTime, keyFrames, cachedIdx);
|
|
712
735
|
if (idx < 0)
|
|
713
736
|
continue;
|
|
714
|
-
// Update cache
|
|
715
737
|
this.boneTrackIndices.set(boneName, idx);
|
|
716
|
-
const frameA = keyFrames[idx]
|
|
717
|
-
const frameB = keyFrames[idx + 1]
|
|
738
|
+
const frameA = keyFrames[idx];
|
|
739
|
+
const frameB = keyFrames[idx + 1];
|
|
718
740
|
const boneIdx = this.runtimeSkeleton.nameIndex[boneName];
|
|
719
741
|
if (boneIdx === undefined)
|
|
720
742
|
continue;
|
|
721
743
|
const localRot = this.runtimeSkeleton.localRotations[boneIdx];
|
|
722
744
|
const localTrans = this.runtimeSkeleton.localTranslations[boneIdx];
|
|
723
745
|
if (!frameB) {
|
|
724
|
-
// No interpolation needed - direct assignment
|
|
725
|
-
// Use animation frame's rotation for translation conversion to ensure consistency
|
|
726
|
-
// This prevents conflicts when IK later modifies the rotation
|
|
727
746
|
const frameRotation = frameA.rotation;
|
|
728
747
|
localRot.set(frameRotation);
|
|
729
|
-
// Convert VMD relative translation to local translation using animation rotation
|
|
730
748
|
const localTranslation = this.convertVMDTranslationToLocal(boneIdx, frameA.translation, frameRotation);
|
|
731
749
|
localTrans.set(localTranslation);
|
|
732
750
|
}
|
|
733
751
|
else {
|
|
734
|
-
const
|
|
735
|
-
const
|
|
736
|
-
const timeDelta = timeB - timeA;
|
|
737
|
-
const gradient = (clampedTime - timeA) / timeDelta;
|
|
752
|
+
const timeDelta = frameB.time - frameA.time;
|
|
753
|
+
const gradient = (clampedTime - frameA.time) / timeDelta;
|
|
738
754
|
const interp = frameB.interpolation;
|
|
739
|
-
|
|
740
|
-
const rotT = bezierInterpolate(interp[0] * INV_127, interp[1] * INV_127, interp[2] * INV_127, interp[3] * INV_127, gradient);
|
|
741
|
-
// Use Quat.slerp to interpolate rotation
|
|
755
|
+
const rotT = interpolateControlPoints(interp.rotation, gradient);
|
|
742
756
|
const rotation = Quat.slerp(frameA.rotation, frameB.rotation, rotT);
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
const txWeight = getWeight(0);
|
|
747
|
-
const tyWeight = getWeight(16);
|
|
748
|
-
const tzWeight = getWeight(32);
|
|
749
|
-
// Interpolate VMD relative translations (relative to bind pose world position)
|
|
757
|
+
const txWeight = interpolateControlPoints(interp.translationX, gradient);
|
|
758
|
+
const tyWeight = interpolateControlPoints(interp.translationY, gradient);
|
|
759
|
+
const tzWeight = interpolateControlPoints(interp.translationZ, gradient);
|
|
750
760
|
const interpolatedVMDTranslation = new Vec3(frameA.translation.x + (frameB.translation.x - frameA.translation.x) * txWeight, frameA.translation.y + (frameB.translation.y - frameA.translation.y) * tyWeight, frameA.translation.z + (frameB.translation.z - frameA.translation.z) * tzWeight);
|
|
751
|
-
// Convert interpolated VMD translation to local translation using animation rotation
|
|
752
|
-
// This ensures translation is computed for the animation rotation, not the runtime rotation
|
|
753
|
-
// that will be modified by IK, preventing conflicts
|
|
754
761
|
const localTranslation = this.convertVMDTranslationToLocal(boneIdx, interpolatedVMDTranslation, rotation);
|
|
755
|
-
// Direct property writes to avoid object allocation
|
|
756
762
|
localRot.set(rotation);
|
|
757
763
|
localTrans.set(localTranslation);
|
|
758
764
|
}
|
|
@@ -766,10 +772,9 @@ export class Model {
|
|
|
766
772
|
const idx = this.findKeyframeIndex(clampedTime, keyFrames, cachedIdx);
|
|
767
773
|
if (idx < 0)
|
|
768
774
|
continue;
|
|
769
|
-
// Update cache
|
|
770
775
|
this.morphTrackIndices.set(morphName, idx);
|
|
771
|
-
const frameA = keyFrames[idx]
|
|
772
|
-
const frameB = keyFrames[idx + 1]
|
|
776
|
+
const frameA = keyFrames[idx];
|
|
777
|
+
const frameB = keyFrames[idx + 1];
|
|
773
778
|
const morphIdx = this.runtimeMorph.nameIndex[morphName];
|
|
774
779
|
if (morphIdx === undefined)
|
|
775
780
|
continue;
|
|
@@ -794,7 +799,7 @@ export class Model {
|
|
|
794
799
|
// Update all active tweens (rotations, translations, morphs)
|
|
795
800
|
const tweensChangedMorphs = this.updateTweens();
|
|
796
801
|
// Apply animation if playing or paused (always apply pose if animation data exists and we have a time set)
|
|
797
|
-
if (this.
|
|
802
|
+
if (this._hasAnimation) {
|
|
798
803
|
if (this.isPlaying && !this.isPaused) {
|
|
799
804
|
this.animationTime += deltaTime;
|
|
800
805
|
if (this.animationTime >= this.animationDuration) {
|
package/package.json
CHANGED
package/src/animation.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Quat, Vec3 } from "./math"
|
|
2
|
+
|
|
3
|
+
export interface ControlPoint {
|
|
4
|
+
x: number
|
|
5
|
+
y: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface BoneInterpolation {
|
|
9
|
+
rotation: ControlPoint[]
|
|
10
|
+
translationX: ControlPoint[]
|
|
11
|
+
translationY: ControlPoint[]
|
|
12
|
+
translationZ: ControlPoint[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const LINEAR_INTERPOLATION: BoneInterpolation = {
|
|
16
|
+
rotation: [{ x: 20, y: 20 }, { x: 107, y: 107 }],
|
|
17
|
+
translationX: [{ x: 20, y: 20 }, { x: 107, y: 107 }],
|
|
18
|
+
translationY: [{ x: 20, y: 20 }, { x: 107, y: 107 }],
|
|
19
|
+
translationZ: [{ x: 20, y: 20 }, { x: 107, y: 107 }],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface BoneKeyframe {
|
|
23
|
+
frame: number
|
|
24
|
+
rotation: Quat
|
|
25
|
+
translation: Vec3
|
|
26
|
+
interpolation: BoneInterpolation
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MorphKeyframe {
|
|
30
|
+
frame: number
|
|
31
|
+
weight: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AnimationData {
|
|
35
|
+
boneTracks: Record<string, BoneKeyframe[]>
|
|
36
|
+
morphTracks: Record<string, MorphKeyframe[]>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Cubic bezier interpolation using binary search.
|
|
41
|
+
* Control points define the curve shape in 0-1 normalized space.
|
|
42
|
+
*/
|
|
43
|
+
export function bezierInterpolate(x1: number, x2: number, y1: number, y2: number, t: number): number {
|
|
44
|
+
t = Math.max(0, Math.min(1, t))
|
|
45
|
+
|
|
46
|
+
let start = 0
|
|
47
|
+
let end = 1
|
|
48
|
+
let mid = 0.5
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < 15; i++) {
|
|
51
|
+
const x = 3 * (1 - mid) * (1 - mid) * mid * x1 + 3 * (1 - mid) * mid * mid * x2 + mid * mid * mid
|
|
52
|
+
|
|
53
|
+
if (Math.abs(x - t) < 0.0001) {
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (x < t) {
|
|
58
|
+
start = mid
|
|
59
|
+
} else {
|
|
60
|
+
end = mid
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
mid = (start + end) / 2
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const y = 3 * (1 - mid) * (1 - mid) * mid * y1 + 3 * (1 - mid) * mid * mid * y2 + mid * mid * mid
|
|
67
|
+
|
|
68
|
+
return y
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const INV_127 = 1 / 127
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convert raw VMD interpolation bytes (64-byte Uint8Array) to structured BoneInterpolation.
|
|
75
|
+
*/
|
|
76
|
+
export function rawInterpolationToBoneInterpolation(raw: Uint8Array): BoneInterpolation {
|
|
77
|
+
return {
|
|
78
|
+
rotation: [
|
|
79
|
+
{ x: raw[0], y: raw[2] },
|
|
80
|
+
{ x: raw[1], y: raw[3] },
|
|
81
|
+
],
|
|
82
|
+
translationX: [
|
|
83
|
+
{ x: raw[0], y: raw[4] },
|
|
84
|
+
{ x: raw[8], y: raw[12] },
|
|
85
|
+
],
|
|
86
|
+
translationY: [
|
|
87
|
+
{ x: raw[16], y: raw[20] },
|
|
88
|
+
{ x: raw[24], y: raw[28] },
|
|
89
|
+
],
|
|
90
|
+
translationZ: [
|
|
91
|
+
{ x: raw[32], y: raw[36] },
|
|
92
|
+
{ x: raw[40], y: raw[44] },
|
|
93
|
+
],
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Compute bezier-interpolated weight for a pair of control points.
|
|
99
|
+
* Control point values are in 0-127 range.
|
|
100
|
+
*/
|
|
101
|
+
export function interpolateControlPoints(cp: ControlPoint[], t: number): number {
|
|
102
|
+
return bezierInterpolate(
|
|
103
|
+
cp[0].x * INV_127,
|
|
104
|
+
cp[1].x * INV_127,
|
|
105
|
+
cp[0].y * INV_127,
|
|
106
|
+
cp[1].y * INV_127,
|
|
107
|
+
t
|
|
108
|
+
)
|
|
109
|
+
}
|
package/src/engine.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Camera } from "./camera"
|
|
2
2
|
import { Mat4, Quat, Vec3 } from "./math"
|
|
3
3
|
import { Model } from "./model"
|
|
4
|
+
import type { AnimationData } from "./animation"
|
|
4
5
|
import { PmxLoader } from "./pmx-loader"
|
|
5
6
|
|
|
6
7
|
export type RaycastCallback = (material: string | null, screenX: number, screenY: number) => void
|
|
@@ -1065,6 +1066,14 @@ export class Engine {
|
|
|
1065
1066
|
await this.currentModel.loadVmd(url)
|
|
1066
1067
|
}
|
|
1067
1068
|
|
|
1069
|
+
public loadAnimationData(data: AnimationData) {
|
|
1070
|
+
this.currentModel?.loadAnimationData(data)
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
public getAnimationData(): AnimationData | null {
|
|
1074
|
+
return this.currentModel?.getAnimationData() ?? null
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1068
1077
|
public playAnimation() {
|
|
1069
1078
|
this.currentModel?.playAnimation()
|
|
1070
1079
|
}
|
|
@@ -1155,6 +1164,7 @@ export class Engine {
|
|
|
1155
1164
|
this.currentModel?.moveBones(boneTranslations, durationMs)
|
|
1156
1165
|
}
|
|
1157
1166
|
|
|
1167
|
+
|
|
1158
1168
|
public resetAllBones() {
|
|
1159
1169
|
this.currentModel?.resetAllBones()
|
|
1160
1170
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { Engine, type EngineStats } from "./engine"
|
|
2
2
|
export { Vec3, Quat, Mat4 } from "./math"
|
|
3
|
+
export type { AnimationData, BoneKeyframe, MorphKeyframe, BoneInterpolation, ControlPoint } from "./animation"
|
|
4
|
+
export { bezierInterpolate, interpolateControlPoints, rawInterpolationToBoneInterpolation, LINEAR_INTERPOLATION } from "./animation"
|
package/src/math.ts
CHANGED
|
@@ -540,45 +540,3 @@ export class Mat4 {
|
|
|
540
540
|
}
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
-
/**
|
|
544
|
-
* Bezier interpolation function
|
|
545
|
-
* @param x1 First control point X (0-127, normalized to 0-1)
|
|
546
|
-
* @param x2 Second control point X (0-127, normalized to 0-1)
|
|
547
|
-
* @param y1 First control point Y (0-127, normalized to 0-1)
|
|
548
|
-
* @param y2 Second control point Y (0-127, normalized to 0-1)
|
|
549
|
-
* @param t Interpolation parameter (0-1)
|
|
550
|
-
* @returns Interpolated value (0-1)
|
|
551
|
-
*/
|
|
552
|
-
export function bezierInterpolate(x1: number, x2: number, y1: number, y2: number, t: number): number {
|
|
553
|
-
// Clamp t to [0, 1]
|
|
554
|
-
t = Math.max(0, Math.min(1, t))
|
|
555
|
-
|
|
556
|
-
// Binary search for the t value that gives us the desired x
|
|
557
|
-
// We're solving for t in the Bezier curve: x(t) = 3*(1-t)^2*t*x1 + 3*(1-t)*t^2*x2 + t^3
|
|
558
|
-
let start = 0
|
|
559
|
-
let end = 1
|
|
560
|
-
let mid = 0.5
|
|
561
|
-
|
|
562
|
-
// Iterate until we find the t value that gives us the desired x
|
|
563
|
-
for (let i = 0; i < 15; i++) {
|
|
564
|
-
// Evaluate Bezier curve at mid point
|
|
565
|
-
const x = 3 * (1 - mid) * (1 - mid) * mid * x1 + 3 * (1 - mid) * mid * mid * x2 + mid * mid * mid
|
|
566
|
-
|
|
567
|
-
if (Math.abs(x - t) < 0.0001) {
|
|
568
|
-
break
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (x < t) {
|
|
572
|
-
start = mid
|
|
573
|
-
} else {
|
|
574
|
-
end = mid
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
mid = (start + end) / 2
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Now evaluate the y value at this t
|
|
581
|
-
const y = 3 * (1 - mid) * (1 - mid) * mid * y1 + 3 * (1 - mid) * mid * mid * y2 + mid * mid * mid
|
|
582
|
-
|
|
583
|
-
return y
|
|
584
|
-
}
|