reze-engine 0.6.7 → 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 +29 -20
- 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 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +6 -3
- 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 -12
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +92 -130
- package/package.json +1 -1
- package/src/animation.ts +109 -0
- package/src/engine.ts +9 -7
- package/src/index.ts +2 -0
- package/src/math.ts +0 -42
- package/src/model.ts +116 -180
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;
|
|
@@ -464,49 +467,6 @@ export class Model {
|
|
|
464
467
|
this.runtimeMorph.weights[idx] = startWeight;
|
|
465
468
|
this.applyMorphs();
|
|
466
469
|
}
|
|
467
|
-
/**
|
|
468
|
-
* Atomic pose setter for external animation editors.
|
|
469
|
-
* Sets bone rotations, translations, and morph weights in a single pass,
|
|
470
|
-
* identical to how getPoseAtTime applies VMD poses during playback.
|
|
471
|
-
* Cancels any active tweens on affected bones/morphs.
|
|
472
|
-
*/
|
|
473
|
-
setPose(rotations, translations, morphs) {
|
|
474
|
-
const state = this.tweenState;
|
|
475
|
-
if (rotations) {
|
|
476
|
-
for (const [name, quat] of Object.entries(rotations)) {
|
|
477
|
-
const idx = this.runtimeSkeleton.nameIndex[name] ?? -1;
|
|
478
|
-
if (idx < 0 || idx >= this.skeleton.bones.length)
|
|
479
|
-
continue;
|
|
480
|
-
this.runtimeSkeleton.localRotations[idx].set(quat.clone().normalize());
|
|
481
|
-
state.rotActive[idx] = 0;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
if (translations) {
|
|
485
|
-
for (const [name, vec] of Object.entries(translations)) {
|
|
486
|
-
const idx = this.runtimeSkeleton.nameIndex[name] ?? -1;
|
|
487
|
-
if (idx < 0 || idx >= this.skeleton.bones.length)
|
|
488
|
-
continue;
|
|
489
|
-
const rotation = rotations?.[name]?.clone().normalize();
|
|
490
|
-
const localTranslation = this.convertVMDTranslationToLocal(idx, vec, rotation);
|
|
491
|
-
this.runtimeSkeleton.localTranslations[idx].set(localTranslation);
|
|
492
|
-
state.transActive[idx] = 0;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
if (morphs) {
|
|
496
|
-
let morphChanged = false;
|
|
497
|
-
for (const [name, weight] of Object.entries(morphs)) {
|
|
498
|
-
const idx = this.runtimeMorph.nameIndex[name] ?? -1;
|
|
499
|
-
if (idx < 0 || idx >= this.runtimeMorph.weights.length)
|
|
500
|
-
continue;
|
|
501
|
-
this.runtimeMorph.weights[idx] = Math.max(0, Math.min(1, weight));
|
|
502
|
-
state.morphActive[idx] = 0;
|
|
503
|
-
morphChanged = true;
|
|
504
|
-
}
|
|
505
|
-
if (morphChanged) {
|
|
506
|
-
this.applyMorphs();
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
470
|
applyMorphs() {
|
|
511
471
|
// Reset vertex data to base positions
|
|
512
472
|
this.vertexData.set(this.baseVertexData);
|
|
@@ -568,11 +528,78 @@ export class Model {
|
|
|
568
528
|
* Load VMD animation file
|
|
569
529
|
*/
|
|
570
530
|
async loadVmd(vmdUrl) {
|
|
571
|
-
|
|
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;
|
|
572
563
|
this.resetAllBones();
|
|
573
564
|
this.resetAllMorphs();
|
|
574
|
-
this.
|
|
575
|
-
|
|
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;
|
|
576
603
|
this.animationTime = 0;
|
|
577
604
|
this.getPoseAtTime(0);
|
|
578
605
|
if (this.physics) {
|
|
@@ -616,57 +643,8 @@ export class Model {
|
|
|
616
643
|
setPhysicsEnabled(enabled) {
|
|
617
644
|
this.physicsEnabled = enabled;
|
|
618
645
|
}
|
|
619
|
-
/**
|
|
620
|
-
* Process frames into tracks
|
|
621
|
-
*/
|
|
622
|
-
processFrames() {
|
|
623
|
-
if (!this.animationData)
|
|
624
|
-
return;
|
|
625
|
-
// Helper to group frames by name and sort by time
|
|
626
|
-
const groupFrames = (items) => {
|
|
627
|
-
const tracks = new Map();
|
|
628
|
-
for (const { item, name, time } of items) {
|
|
629
|
-
if (!tracks.has(name))
|
|
630
|
-
tracks.set(name, []);
|
|
631
|
-
tracks.get(name).push({ item, time });
|
|
632
|
-
}
|
|
633
|
-
for (const keyFrames of tracks.values()) {
|
|
634
|
-
keyFrames.sort((a, b) => a.time - b.time);
|
|
635
|
-
}
|
|
636
|
-
return tracks;
|
|
637
|
-
};
|
|
638
|
-
// Collect all bone and morph frames
|
|
639
|
-
const boneItems = [];
|
|
640
|
-
const morphItems = [];
|
|
641
|
-
for (const keyFrame of this.animationData) {
|
|
642
|
-
for (const boneFrame of keyFrame.boneFrames) {
|
|
643
|
-
boneItems.push({ item: boneFrame, name: boneFrame.boneName, time: keyFrame.time });
|
|
644
|
-
}
|
|
645
|
-
for (const morphFrame of keyFrame.morphFrames) {
|
|
646
|
-
morphItems.push({ item: morphFrame, name: morphFrame.morphName, time: keyFrame.time });
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
// Transform to expected format
|
|
650
|
-
this.boneTracks = new Map();
|
|
651
|
-
for (const [name, frames] of groupFrames(boneItems).entries()) {
|
|
652
|
-
this.boneTracks.set(name, frames.map((f) => ({ boneFrame: f.item, time: f.time })));
|
|
653
|
-
}
|
|
654
|
-
this.morphTracks = new Map();
|
|
655
|
-
for (const [name, frames] of groupFrames(morphItems).entries()) {
|
|
656
|
-
this.morphTracks.set(name, frames.map((f) => ({ morphFrame: f.item, time: f.time })));
|
|
657
|
-
}
|
|
658
|
-
// Reset cached indices when tracks change
|
|
659
|
-
this.boneTrackIndices.clear();
|
|
660
|
-
this.morphTrackIndices.clear();
|
|
661
|
-
// Calculate duration from all tracks
|
|
662
|
-
const allTracks = [...this.boneTracks.values(), ...this.morphTracks.values()];
|
|
663
|
-
this.animationDuration = allTracks.reduce((max, keyFrames) => {
|
|
664
|
-
const lastTime = keyFrames[keyFrames.length - 1]?.time ?? 0;
|
|
665
|
-
return Math.max(max, lastTime);
|
|
666
|
-
}, 0);
|
|
667
|
-
}
|
|
668
646
|
playAnimation() {
|
|
669
|
-
if (!this.
|
|
647
|
+
if (!this._hasAnimation)
|
|
670
648
|
return;
|
|
671
649
|
this.isPaused = false;
|
|
672
650
|
this.isPlaying = true;
|
|
@@ -686,7 +664,7 @@ export class Model {
|
|
|
686
664
|
this.animationTime = 0;
|
|
687
665
|
}
|
|
688
666
|
seekAnimation(time) {
|
|
689
|
-
if (!this.
|
|
667
|
+
if (!this._hasAnimation)
|
|
690
668
|
return;
|
|
691
669
|
const clampedTime = Math.max(0, Math.min(time, this.animationDuration));
|
|
692
670
|
this.animationTime = clampedTime;
|
|
@@ -694,6 +672,9 @@ export class Model {
|
|
|
694
672
|
/**
|
|
695
673
|
* Get current animation progress
|
|
696
674
|
*/
|
|
675
|
+
getAnimationData() {
|
|
676
|
+
return this._animationData;
|
|
677
|
+
}
|
|
697
678
|
getAnimationProgress() {
|
|
698
679
|
const duration = this.animationDuration;
|
|
699
680
|
const percentage = duration > 0 ? (this.animationTime / duration) * 100 : 0;
|
|
@@ -742,9 +723,8 @@ export class Model {
|
|
|
742
723
|
* Optimized for per-frame performance
|
|
743
724
|
*/
|
|
744
725
|
getPoseAtTime(time) {
|
|
745
|
-
if (!this.
|
|
726
|
+
if (!this._hasAnimation)
|
|
746
727
|
return;
|
|
747
|
-
const INV_127 = 1 / 127; // Pre-compute division constant
|
|
748
728
|
// Process bone tracks
|
|
749
729
|
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
750
730
|
if (keyFrames.length === 0)
|
|
@@ -754,48 +734,31 @@ export class Model {
|
|
|
754
734
|
const idx = this.findKeyframeIndex(clampedTime, keyFrames, cachedIdx);
|
|
755
735
|
if (idx < 0)
|
|
756
736
|
continue;
|
|
757
|
-
// Update cache
|
|
758
737
|
this.boneTrackIndices.set(boneName, idx);
|
|
759
|
-
const frameA = keyFrames[idx]
|
|
760
|
-
const frameB = keyFrames[idx + 1]
|
|
738
|
+
const frameA = keyFrames[idx];
|
|
739
|
+
const frameB = keyFrames[idx + 1];
|
|
761
740
|
const boneIdx = this.runtimeSkeleton.nameIndex[boneName];
|
|
762
741
|
if (boneIdx === undefined)
|
|
763
742
|
continue;
|
|
764
743
|
const localRot = this.runtimeSkeleton.localRotations[boneIdx];
|
|
765
744
|
const localTrans = this.runtimeSkeleton.localTranslations[boneIdx];
|
|
766
745
|
if (!frameB) {
|
|
767
|
-
// No interpolation needed - direct assignment
|
|
768
|
-
// Use animation frame's rotation for translation conversion to ensure consistency
|
|
769
|
-
// This prevents conflicts when IK later modifies the rotation
|
|
770
746
|
const frameRotation = frameA.rotation;
|
|
771
747
|
localRot.set(frameRotation);
|
|
772
|
-
// Convert VMD relative translation to local translation using animation rotation
|
|
773
748
|
const localTranslation = this.convertVMDTranslationToLocal(boneIdx, frameA.translation, frameRotation);
|
|
774
749
|
localTrans.set(localTranslation);
|
|
775
750
|
}
|
|
776
751
|
else {
|
|
777
|
-
const
|
|
778
|
-
const
|
|
779
|
-
const timeDelta = timeB - timeA;
|
|
780
|
-
const gradient = (clampedTime - timeA) / timeDelta;
|
|
752
|
+
const timeDelta = frameB.time - frameA.time;
|
|
753
|
+
const gradient = (clampedTime - frameA.time) / timeDelta;
|
|
781
754
|
const interp = frameB.interpolation;
|
|
782
|
-
|
|
783
|
-
const rotT = bezierInterpolate(interp[0] * INV_127, interp[1] * INV_127, interp[2] * INV_127, interp[3] * INV_127, gradient);
|
|
784
|
-
// Use Quat.slerp to interpolate rotation
|
|
755
|
+
const rotT = interpolateControlPoints(interp.rotation, gradient);
|
|
785
756
|
const rotation = Quat.slerp(frameA.rotation, frameB.rotation, rotT);
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const
|
|
789
|
-
const txWeight = getWeight(0);
|
|
790
|
-
const tyWeight = getWeight(16);
|
|
791
|
-
const tzWeight = getWeight(32);
|
|
792
|
-
// 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);
|
|
793
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);
|
|
794
|
-
// Convert interpolated VMD translation to local translation using animation rotation
|
|
795
|
-
// This ensures translation is computed for the animation rotation, not the runtime rotation
|
|
796
|
-
// that will be modified by IK, preventing conflicts
|
|
797
761
|
const localTranslation = this.convertVMDTranslationToLocal(boneIdx, interpolatedVMDTranslation, rotation);
|
|
798
|
-
// Direct property writes to avoid object allocation
|
|
799
762
|
localRot.set(rotation);
|
|
800
763
|
localTrans.set(localTranslation);
|
|
801
764
|
}
|
|
@@ -809,10 +772,9 @@ export class Model {
|
|
|
809
772
|
const idx = this.findKeyframeIndex(clampedTime, keyFrames, cachedIdx);
|
|
810
773
|
if (idx < 0)
|
|
811
774
|
continue;
|
|
812
|
-
// Update cache
|
|
813
775
|
this.morphTrackIndices.set(morphName, idx);
|
|
814
|
-
const frameA = keyFrames[idx]
|
|
815
|
-
const frameB = keyFrames[idx + 1]
|
|
776
|
+
const frameA = keyFrames[idx];
|
|
777
|
+
const frameB = keyFrames[idx + 1];
|
|
816
778
|
const morphIdx = this.runtimeMorph.nameIndex[morphName];
|
|
817
779
|
if (morphIdx === undefined)
|
|
818
780
|
continue;
|
|
@@ -837,7 +799,7 @@ export class Model {
|
|
|
837
799
|
// Update all active tweens (rotations, translations, morphs)
|
|
838
800
|
const tweensChangedMorphs = this.updateTweens();
|
|
839
801
|
// Apply animation if playing or paused (always apply pose if animation data exists and we have a time set)
|
|
840
|
-
if (this.
|
|
802
|
+
if (this._hasAnimation) {
|
|
841
803
|
if (this.isPlaying && !this.isPaused) {
|
|
842
804
|
this.animationTime += deltaTime;
|
|
843
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,13 +1164,6 @@ export class Engine {
|
|
|
1155
1164
|
this.currentModel?.moveBones(boneTranslations, durationMs)
|
|
1156
1165
|
}
|
|
1157
1166
|
|
|
1158
|
-
public setPose(
|
|
1159
|
-
rotations?: Record<string, Quat>,
|
|
1160
|
-
translations?: Record<string, Vec3>,
|
|
1161
|
-
morphs?: Record<string, number>
|
|
1162
|
-
): void {
|
|
1163
|
-
this.currentModel?.setPose(rotations, translations, morphs)
|
|
1164
|
-
}
|
|
1165
1167
|
|
|
1166
1168
|
public resetAllBones() {
|
|
1167
1169
|
this.currentModel?.resetAllBones()
|
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
|
-
}
|