reze-engine 0.6.7 → 0.8.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 +23 -35
- package/dist/animation.d.ts +14 -0
- package/dist/animation.d.ts.map +1 -0
- package/dist/animation.js +48 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +2 -2
- package/dist/engine.d.ts +29 -22
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +292 -178
- package/dist/ik-solver.d.ts +0 -11
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/ik-solver.js +1 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/math.d.ts +1 -10
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +24 -36
- package/dist/model.d.ts +3 -52
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +103 -170
- package/dist/pmx-loader.js +1 -1
- package/package.json +1 -1
- package/src/animation.ts +75 -0
- package/src/camera.ts +2 -2
- package/src/engine.ts +315 -218
- package/src/ik-solver.ts +1 -11
- package/src/index.ts +3 -2
- package/src/math.ts +27 -42
- package/src/model.ts +125 -220
- package/src/pmx-loader.ts +1 -1
package/dist/model.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import { Mat4, Quat, Vec3
|
|
1
|
+
import { Mat4, Quat, Vec3 } from "./math";
|
|
2
|
+
import { Engine } from "./engine";
|
|
3
|
+
import { PmxLoader } from "./pmx-loader";
|
|
2
4
|
import { Physics } from "./physics";
|
|
3
5
|
import { IKSolverSystem } from "./ik-solver";
|
|
4
6
|
import { VMDLoader } from "./vmd-loader";
|
|
7
|
+
import { interpolateControlPoints, rawInterpolationToBoneInterpolation } from "./animation";
|
|
8
|
+
const VMD_FPS = 30;
|
|
5
9
|
const VERTEX_STRIDE = 8;
|
|
6
10
|
export class Model {
|
|
11
|
+
// loadPmx: fetch PMX + register with Engine.getInstance() (init engine first)
|
|
12
|
+
static async loadPmx(path) {
|
|
13
|
+
const model = await PmxLoader.load(path);
|
|
14
|
+
await Engine.getInstance().registerModel(model, path);
|
|
15
|
+
return model;
|
|
16
|
+
}
|
|
7
17
|
constructor(vertexData, indexData, textures, materials, skeleton, skinning, morphing, rigidbodies = [], joints = []) {
|
|
8
18
|
this.textures = [];
|
|
9
19
|
this.materials = [];
|
|
@@ -16,7 +26,7 @@ export class Model {
|
|
|
16
26
|
this.cachedIdentityMat2 = Mat4.identity();
|
|
17
27
|
this.tweenTimeMs = 0; // Time tracking for tweens (milliseconds)
|
|
18
28
|
// Animation runtime
|
|
19
|
-
this.
|
|
29
|
+
this._hasAnimation = false;
|
|
20
30
|
this.boneTracks = new Map();
|
|
21
31
|
this.morphTracks = new Map();
|
|
22
32
|
this.animationDuration = 0;
|
|
@@ -239,6 +249,13 @@ export class Model {
|
|
|
239
249
|
getSkeleton() {
|
|
240
250
|
return this.skeleton;
|
|
241
251
|
}
|
|
252
|
+
// World bone origin (world matrix col3); unknown name → null
|
|
253
|
+
getBoneWorldPosition(boneName) {
|
|
254
|
+
const idx = this.runtimeSkeleton.nameIndex[boneName];
|
|
255
|
+
if (idx === undefined || idx < 0)
|
|
256
|
+
return null;
|
|
257
|
+
return this.runtimeSkeleton.worldMatrices[idx].getPosition();
|
|
258
|
+
}
|
|
242
259
|
getSkinning() {
|
|
243
260
|
return this.skinning;
|
|
244
261
|
}
|
|
@@ -345,14 +362,7 @@ export class Model {
|
|
|
345
362
|
state.transActive[idx] = 1;
|
|
346
363
|
}
|
|
347
364
|
}
|
|
348
|
-
|
|
349
|
-
* Convert VMD-style relative translation (relative to bind pose world position) to local translation
|
|
350
|
-
* This helper is used by both moveBones and getPoseAtTime to ensure consistent translation handling
|
|
351
|
-
* @param boneIdx - Bone index
|
|
352
|
-
* @param vmdRelativeTranslation - VMD relative translation
|
|
353
|
-
* @param rotation - Optional rotation to use for conversion. If not provided, uses current localRotation.
|
|
354
|
-
* Use animation rotation (from frame) to avoid conflicts when IK modifies rotation.
|
|
355
|
-
*/
|
|
365
|
+
// VMD translation (world delta from bind pose) → bone local space; optional rotation for animation vs IK
|
|
356
366
|
convertVMDTranslationToLocal(boneIdx, vmdRelativeTranslation, rotation) {
|
|
357
367
|
const skeleton = this.skeleton;
|
|
358
368
|
const bones = skeleton.bones;
|
|
@@ -441,6 +451,12 @@ export class Model {
|
|
|
441
451
|
this.runtimeMorph.weights[idx] = clampedWeight;
|
|
442
452
|
this.tweenState.morphActive[idx] = 0;
|
|
443
453
|
this.applyMorphs();
|
|
454
|
+
try {
|
|
455
|
+
Engine.getInstance().markVertexBufferDirty();
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
/* not registered yet */
|
|
459
|
+
}
|
|
444
460
|
return;
|
|
445
461
|
}
|
|
446
462
|
// Animated change
|
|
@@ -464,49 +480,6 @@ export class Model {
|
|
|
464
480
|
this.runtimeMorph.weights[idx] = startWeight;
|
|
465
481
|
this.applyMorphs();
|
|
466
482
|
}
|
|
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
483
|
applyMorphs() {
|
|
511
484
|
// Reset vertex data to base positions
|
|
512
485
|
this.vertexData.set(this.baseVertexData);
|
|
@@ -564,15 +537,71 @@ export class Model {
|
|
|
564
537
|
}
|
|
565
538
|
}
|
|
566
539
|
}
|
|
567
|
-
/**
|
|
568
|
-
* Load VMD animation file
|
|
569
|
-
*/
|
|
570
540
|
async loadVmd(vmdUrl) {
|
|
571
|
-
|
|
541
|
+
const vmdKeyFrames = await VMDLoader.load(vmdUrl);
|
|
572
542
|
this.resetAllBones();
|
|
573
543
|
this.resetAllMorphs();
|
|
574
|
-
|
|
575
|
-
|
|
544
|
+
// Build bone tracks: Map<boneName, Array<{boneName, frame, rotation, translation, interpolation, time}>>
|
|
545
|
+
const boneTracksByBone = {};
|
|
546
|
+
for (const keyFrame of vmdKeyFrames) {
|
|
547
|
+
for (const bf of keyFrame.boneFrames) {
|
|
548
|
+
if (!boneTracksByBone[bf.boneName])
|
|
549
|
+
boneTracksByBone[bf.boneName] = [];
|
|
550
|
+
boneTracksByBone[bf.boneName].push({
|
|
551
|
+
frame: bf.frame,
|
|
552
|
+
rotation: bf.rotation,
|
|
553
|
+
translation: bf.translation,
|
|
554
|
+
interpolation: rawInterpolationToBoneInterpolation(bf.interpolation),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
this.boneTracks = new Map();
|
|
559
|
+
for (const name in boneTracksByBone) {
|
|
560
|
+
const keyframes = boneTracksByBone[name];
|
|
561
|
+
const sorted = [...keyframes].sort((a, b) => a.frame - b.frame);
|
|
562
|
+
this.boneTracks.set(name, sorted.map((kf) => ({
|
|
563
|
+
boneName: name,
|
|
564
|
+
frame: kf.frame,
|
|
565
|
+
rotation: kf.rotation,
|
|
566
|
+
translation: kf.translation,
|
|
567
|
+
interpolation: kf.interpolation,
|
|
568
|
+
time: kf.frame / VMD_FPS,
|
|
569
|
+
})));
|
|
570
|
+
}
|
|
571
|
+
// Build morph tracks: Map<morphName, Array<{morphName, frame, weight, time}>>
|
|
572
|
+
const morphTracksByMorph = {};
|
|
573
|
+
for (const keyFrame of vmdKeyFrames) {
|
|
574
|
+
for (const mf of keyFrame.morphFrames) {
|
|
575
|
+
if (!morphTracksByMorph[mf.morphName])
|
|
576
|
+
morphTracksByMorph[mf.morphName] = [];
|
|
577
|
+
morphTracksByMorph[mf.morphName].push({ frame: mf.frame, weight: mf.weight });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
this.morphTracks = new Map();
|
|
581
|
+
for (const name in morphTracksByMorph) {
|
|
582
|
+
const keyframes = morphTracksByMorph[name];
|
|
583
|
+
const sorted = [...keyframes].sort((a, b) => a.frame - b.frame);
|
|
584
|
+
this.morphTracks.set(name, sorted.map((kf) => ({
|
|
585
|
+
morphName: name,
|
|
586
|
+
frame: kf.frame,
|
|
587
|
+
weight: kf.weight,
|
|
588
|
+
time: kf.frame / VMD_FPS,
|
|
589
|
+
})));
|
|
590
|
+
}
|
|
591
|
+
this.boneTrackIndices.clear();
|
|
592
|
+
this.morphTrackIndices.clear();
|
|
593
|
+
// Calculate duration
|
|
594
|
+
let maxTime = 0;
|
|
595
|
+
for (const frames of this.boneTracks.values()) {
|
|
596
|
+
if (frames.length > 0)
|
|
597
|
+
maxTime = Math.max(maxTime, frames[frames.length - 1].time);
|
|
598
|
+
}
|
|
599
|
+
for (const frames of this.morphTracks.values()) {
|
|
600
|
+
if (frames.length > 0)
|
|
601
|
+
maxTime = Math.max(maxTime, frames[frames.length - 1].time);
|
|
602
|
+
}
|
|
603
|
+
this.animationDuration = maxTime;
|
|
604
|
+
this._hasAnimation = true;
|
|
576
605
|
this.animationTime = 0;
|
|
577
606
|
this.getPoseAtTime(0);
|
|
578
607
|
if (this.physics) {
|
|
@@ -580,9 +609,6 @@ export class Model {
|
|
|
580
609
|
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices);
|
|
581
610
|
}
|
|
582
611
|
}
|
|
583
|
-
/**
|
|
584
|
-
* Reset all bones to their default pose
|
|
585
|
-
*/
|
|
586
612
|
resetAllBones() {
|
|
587
613
|
for (let boneIdx = 0; boneIdx < this.skeleton.bones.length; boneIdx++) {
|
|
588
614
|
const localRot = this.runtimeSkeleton.localRotations[boneIdx];
|
|
@@ -604,69 +630,14 @@ export class Model {
|
|
|
604
630
|
this.morphsDirty = true;
|
|
605
631
|
this.applyMorphs();
|
|
606
632
|
}
|
|
607
|
-
/**
|
|
608
|
-
* Enable or disable IK solving
|
|
609
|
-
*/
|
|
610
633
|
setIKEnabled(enabled) {
|
|
611
634
|
this.ikEnabled = enabled;
|
|
612
635
|
}
|
|
613
|
-
/**
|
|
614
|
-
* Enable or disable physics simulation
|
|
615
|
-
*/
|
|
616
636
|
setPhysicsEnabled(enabled) {
|
|
617
637
|
this.physicsEnabled = enabled;
|
|
618
638
|
}
|
|
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
639
|
playAnimation() {
|
|
669
|
-
if (!this.
|
|
640
|
+
if (!this._hasAnimation)
|
|
670
641
|
return;
|
|
671
642
|
this.isPaused = false;
|
|
672
643
|
this.isPlaying = true;
|
|
@@ -686,14 +657,11 @@ export class Model {
|
|
|
686
657
|
this.animationTime = 0;
|
|
687
658
|
}
|
|
688
659
|
seekAnimation(time) {
|
|
689
|
-
if (!this.
|
|
660
|
+
if (!this._hasAnimation)
|
|
690
661
|
return;
|
|
691
662
|
const clampedTime = Math.max(0, Math.min(time, this.animationDuration));
|
|
692
663
|
this.animationTime = clampedTime;
|
|
693
664
|
}
|
|
694
|
-
/**
|
|
695
|
-
* Get current animation progress
|
|
696
|
-
*/
|
|
697
665
|
getAnimationProgress() {
|
|
698
666
|
const duration = this.animationDuration;
|
|
699
667
|
const percentage = duration > 0 ? (this.animationTime / duration) * 100 : 0;
|
|
@@ -703,9 +671,6 @@ export class Model {
|
|
|
703
671
|
percentage,
|
|
704
672
|
};
|
|
705
673
|
}
|
|
706
|
-
/**
|
|
707
|
-
* Binary search upper bound helper (static to avoid recreation)
|
|
708
|
-
*/
|
|
709
674
|
static upperBound(time, keyFrames, startIdx = 0) {
|
|
710
675
|
let left = startIdx, right = keyFrames.length;
|
|
711
676
|
while (left < right) {
|
|
@@ -717,10 +682,6 @@ export class Model {
|
|
|
717
682
|
}
|
|
718
683
|
return left;
|
|
719
684
|
}
|
|
720
|
-
/**
|
|
721
|
-
* Find keyframe index with caching optimization
|
|
722
|
-
* Uses cached index as starting point for faster lookup when time is close
|
|
723
|
-
*/
|
|
724
685
|
findKeyframeIndex(time, keyFrames, cachedIdx) {
|
|
725
686
|
if (keyFrames.length === 0)
|
|
726
687
|
return -1;
|
|
@@ -737,14 +698,9 @@ export class Model {
|
|
|
737
698
|
const idx = Model.upperBound(time, keyFrames, 0) - 1;
|
|
738
699
|
return idx;
|
|
739
700
|
}
|
|
740
|
-
/**
|
|
741
|
-
* Get pose at specific time (internal helper)
|
|
742
|
-
* Optimized for per-frame performance
|
|
743
|
-
*/
|
|
744
701
|
getPoseAtTime(time) {
|
|
745
|
-
if (!this.
|
|
702
|
+
if (!this._hasAnimation)
|
|
746
703
|
return;
|
|
747
|
-
const INV_127 = 1 / 127; // Pre-compute division constant
|
|
748
704
|
// Process bone tracks
|
|
749
705
|
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
750
706
|
if (keyFrames.length === 0)
|
|
@@ -754,48 +710,31 @@ export class Model {
|
|
|
754
710
|
const idx = this.findKeyframeIndex(clampedTime, keyFrames, cachedIdx);
|
|
755
711
|
if (idx < 0)
|
|
756
712
|
continue;
|
|
757
|
-
// Update cache
|
|
758
713
|
this.boneTrackIndices.set(boneName, idx);
|
|
759
|
-
const frameA = keyFrames[idx]
|
|
760
|
-
const frameB = keyFrames[idx + 1]
|
|
714
|
+
const frameA = keyFrames[idx];
|
|
715
|
+
const frameB = keyFrames[idx + 1];
|
|
761
716
|
const boneIdx = this.runtimeSkeleton.nameIndex[boneName];
|
|
762
717
|
if (boneIdx === undefined)
|
|
763
718
|
continue;
|
|
764
719
|
const localRot = this.runtimeSkeleton.localRotations[boneIdx];
|
|
765
720
|
const localTrans = this.runtimeSkeleton.localTranslations[boneIdx];
|
|
766
721
|
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
722
|
const frameRotation = frameA.rotation;
|
|
771
723
|
localRot.set(frameRotation);
|
|
772
|
-
// Convert VMD relative translation to local translation using animation rotation
|
|
773
724
|
const localTranslation = this.convertVMDTranslationToLocal(boneIdx, frameA.translation, frameRotation);
|
|
774
725
|
localTrans.set(localTranslation);
|
|
775
726
|
}
|
|
776
727
|
else {
|
|
777
|
-
const
|
|
778
|
-
const
|
|
779
|
-
const timeDelta = timeB - timeA;
|
|
780
|
-
const gradient = (clampedTime - timeA) / timeDelta;
|
|
728
|
+
const timeDelta = frameB.time - frameA.time;
|
|
729
|
+
const gradient = (clampedTime - frameA.time) / timeDelta;
|
|
781
730
|
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
|
|
731
|
+
const rotT = interpolateControlPoints(interp.rotation, gradient);
|
|
785
732
|
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)
|
|
733
|
+
const txWeight = interpolateControlPoints(interp.translationX, gradient);
|
|
734
|
+
const tyWeight = interpolateControlPoints(interp.translationY, gradient);
|
|
735
|
+
const tzWeight = interpolateControlPoints(interp.translationZ, gradient);
|
|
793
736
|
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
737
|
const localTranslation = this.convertVMDTranslationToLocal(boneIdx, interpolatedVMDTranslation, rotation);
|
|
798
|
-
// Direct property writes to avoid object allocation
|
|
799
738
|
localRot.set(rotation);
|
|
800
739
|
localTrans.set(localTranslation);
|
|
801
740
|
}
|
|
@@ -809,10 +748,9 @@ export class Model {
|
|
|
809
748
|
const idx = this.findKeyframeIndex(clampedTime, keyFrames, cachedIdx);
|
|
810
749
|
if (idx < 0)
|
|
811
750
|
continue;
|
|
812
|
-
// Update cache
|
|
813
751
|
this.morphTrackIndices.set(morphName, idx);
|
|
814
|
-
const frameA = keyFrames[idx]
|
|
815
|
-
const frameB = keyFrames[idx + 1]
|
|
752
|
+
const frameA = keyFrames[idx];
|
|
753
|
+
const frameB = keyFrames[idx + 1];
|
|
816
754
|
const morphIdx = this.runtimeMorph.nameIndex[morphName];
|
|
817
755
|
if (morphIdx === undefined)
|
|
818
756
|
continue;
|
|
@@ -825,19 +763,14 @@ export class Model {
|
|
|
825
763
|
this.morphsDirty = true; // Mark as dirty when animation sets morph weights
|
|
826
764
|
}
|
|
827
765
|
}
|
|
828
|
-
|
|
829
|
-
* Updates the model pose by recomputing all matrices.
|
|
830
|
-
* If animation is playing, applies animation pose first.
|
|
831
|
-
* deltaTime: Time elapsed since last update in seconds
|
|
832
|
-
* Returns true if vertices were modified (morphs changed)
|
|
833
|
-
*/
|
|
766
|
+
// Returns true when morphs changed (vertex buffer may need upload)
|
|
834
767
|
update(deltaTime) {
|
|
835
768
|
// Update tween time (in milliseconds)
|
|
836
769
|
this.tweenTimeMs += deltaTime * 1000;
|
|
837
770
|
// Update all active tweens (rotations, translations, morphs)
|
|
838
771
|
const tweensChangedMorphs = this.updateTweens();
|
|
839
772
|
// Apply animation if playing or paused (always apply pose if animation data exists and we have a time set)
|
|
840
|
-
if (this.
|
|
773
|
+
if (this._hasAnimation) {
|
|
841
774
|
if (this.isPlaying && !this.isPaused) {
|
|
842
775
|
this.animationTime += deltaTime;
|
|
843
776
|
if (this.animationTime >= this.animationDuration) {
|
package/dist/pmx-loader.js
CHANGED
|
@@ -218,7 +218,7 @@ export class PmxLoader {
|
|
|
218
218
|
this.getFloat32(),
|
|
219
219
|
];
|
|
220
220
|
// edgeSize float
|
|
221
|
-
const edgeSize = this.getFloat32();
|
|
221
|
+
const edgeSize = this.getFloat32() * 2; // double the size for better visibility
|
|
222
222
|
const textureIndex = this.getNonVertexIndex(this.textureIndexSize);
|
|
223
223
|
const sphereTextureIndex = this.getNonVertexIndex(this.textureIndexSize);
|
|
224
224
|
const sphereTextureMode = this.getUint8();
|
package/package.json
CHANGED
package/src/animation.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface ControlPoint {
|
|
2
|
+
x: number
|
|
3
|
+
y: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface BoneInterpolation {
|
|
7
|
+
rotation: ControlPoint[]
|
|
8
|
+
translationX: ControlPoint[]
|
|
9
|
+
translationY: ControlPoint[]
|
|
10
|
+
translationZ: ControlPoint[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Cubic bezier in normalized 0–1 space (binary search on x)
|
|
14
|
+
export function bezierInterpolate(x1: number, x2: number, y1: number, y2: number, t: number): number {
|
|
15
|
+
t = Math.max(0, Math.min(1, t))
|
|
16
|
+
|
|
17
|
+
let start = 0
|
|
18
|
+
let end = 1
|
|
19
|
+
let mid = 0.5
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < 15; i++) {
|
|
22
|
+
const x = 3 * (1 - mid) * (1 - mid) * mid * x1 + 3 * (1 - mid) * mid * mid * x2 + mid * mid * mid
|
|
23
|
+
|
|
24
|
+
if (Math.abs(x - t) < 0.0001) {
|
|
25
|
+
break
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (x < t) {
|
|
29
|
+
start = mid
|
|
30
|
+
} else {
|
|
31
|
+
end = mid
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
mid = (start + end) / 2
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const y = 3 * (1 - mid) * (1 - mid) * mid * y1 + 3 * (1 - mid) * mid * mid * y2 + mid * mid * mid
|
|
38
|
+
|
|
39
|
+
return y
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const INV_127 = 1 / 127
|
|
43
|
+
|
|
44
|
+
// VMD 64-byte interpolation blob → BoneInterpolation
|
|
45
|
+
export function rawInterpolationToBoneInterpolation(raw: Uint8Array): BoneInterpolation {
|
|
46
|
+
return {
|
|
47
|
+
rotation: [
|
|
48
|
+
{ x: raw[0], y: raw[2] },
|
|
49
|
+
{ x: raw[1], y: raw[3] },
|
|
50
|
+
],
|
|
51
|
+
translationX: [
|
|
52
|
+
{ x: raw[0], y: raw[4] },
|
|
53
|
+
{ x: raw[8], y: raw[12] },
|
|
54
|
+
],
|
|
55
|
+
translationY: [
|
|
56
|
+
{ x: raw[16], y: raw[20] },
|
|
57
|
+
{ x: raw[24], y: raw[28] },
|
|
58
|
+
],
|
|
59
|
+
translationZ: [
|
|
60
|
+
{ x: raw[32], y: raw[36] },
|
|
61
|
+
{ x: raw[40], y: raw[44] },
|
|
62
|
+
],
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Control points are 0–127 VMD bytes
|
|
67
|
+
export function interpolateControlPoints(cp: ControlPoint[], t: number): number {
|
|
68
|
+
return bezierInterpolate(
|
|
69
|
+
cp[0].x * INV_127,
|
|
70
|
+
cp[1].x * INV_127,
|
|
71
|
+
cp[0].y * INV_127,
|
|
72
|
+
cp[1].y * INV_127,
|
|
73
|
+
t
|
|
74
|
+
)
|
|
75
|
+
}
|
package/src/camera.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Mat4, Vec3 } from "./math"
|
|
2
2
|
|
|
3
|
-
const FAR =
|
|
3
|
+
const FAR = 2000
|
|
4
4
|
|
|
5
5
|
export class Camera {
|
|
6
6
|
alpha: number
|
|
@@ -29,7 +29,7 @@ export class Camera {
|
|
|
29
29
|
panSensitivity: number = 0.0002 // Sensitivity for right-click panning
|
|
30
30
|
wheelPrecision: number = 0.01
|
|
31
31
|
pinchPrecision: number = 0.05
|
|
32
|
-
minZ: number = 0.
|
|
32
|
+
minZ: number = 0.05
|
|
33
33
|
maxZ: number = FAR
|
|
34
34
|
lowerBetaLimit: number = 0.001
|
|
35
35
|
upperBetaLimit: number = Math.PI - 0.001
|