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/src/ik-solver.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* IK Solver implementation
|
|
3
|
-
* Based on reference from babylon-mmd and Saba MMD library
|
|
4
|
-
* https://github.com/benikabocha/saba/blob/master/src/Saba/Model/MMD/MMDIkSolver.cpp
|
|
5
|
-
*/
|
|
1
|
+
// IK solver (MMD-style; see Saba MMDIkSolver.cpp)
|
|
6
2
|
|
|
7
3
|
import { Mat4, Quat, Vec3 } from "./math"
|
|
8
4
|
import { Bone, IKLink, IKSolver, IKChainInfo } from "./model"
|
|
@@ -76,16 +72,10 @@ class IKChain {
|
|
|
76
72
|
}
|
|
77
73
|
}
|
|
78
74
|
|
|
79
|
-
/**
|
|
80
|
-
* Solve IK chains for a model
|
|
81
|
-
*/
|
|
82
75
|
export class IKSolverSystem {
|
|
83
76
|
private static readonly EPSILON = 1.0e-8
|
|
84
77
|
private static readonly THRESHOLD = (88 * Math.PI) / 180
|
|
85
78
|
|
|
86
|
-
/**
|
|
87
|
-
* Solve all IK chains
|
|
88
|
-
*/
|
|
89
79
|
public static solve(
|
|
90
80
|
ikSolvers: IKSolver[],
|
|
91
81
|
bones: Bone[],
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { Engine, type EngineStats } from "./engine"
|
|
2
|
-
export {
|
|
1
|
+
export { Engine, type EngineStats } from "./engine"
|
|
2
|
+
export { Model } from "./model"
|
|
3
|
+
export { Vec3, Quat, Mat4 } from "./math"
|
package/src/math.ts
CHANGED
|
@@ -295,6 +295,33 @@ export class Mat4 {
|
|
|
295
295
|
)
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
// LH ortho, NDC depth 0=near 1=far
|
|
299
|
+
static orthographicLh(left: number, right: number, bottom: number, top: number, near: number, far: number): Mat4 {
|
|
300
|
+
const rl = 1 / (right - left)
|
|
301
|
+
const tb = 1 / (top - bottom)
|
|
302
|
+
const fn = 1 / (far - near)
|
|
303
|
+
return new Mat4(
|
|
304
|
+
new Float32Array([
|
|
305
|
+
2 * rl,
|
|
306
|
+
0,
|
|
307
|
+
0,
|
|
308
|
+
0,
|
|
309
|
+
0,
|
|
310
|
+
2 * tb,
|
|
311
|
+
0,
|
|
312
|
+
0,
|
|
313
|
+
0,
|
|
314
|
+
0,
|
|
315
|
+
fn,
|
|
316
|
+
0,
|
|
317
|
+
-(right + left) * rl,
|
|
318
|
+
-(top + bottom) * tb,
|
|
319
|
+
-near * fn,
|
|
320
|
+
1,
|
|
321
|
+
])
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
298
325
|
multiply(other: Mat4): Mat4 {
|
|
299
326
|
// Column-major multiplication (matches WGSL/GLSL convention):
|
|
300
327
|
// result = a * b
|
|
@@ -540,45 +567,3 @@ export class Mat4 {
|
|
|
540
567
|
}
|
|
541
568
|
}
|
|
542
569
|
|
|
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
|
-
}
|
package/src/model.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
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 { Rigidbody, Joint, Physics } from "./physics"
|
|
3
5
|
import { IKSolverSystem } from "./ik-solver"
|
|
4
|
-
import {
|
|
6
|
+
import { VMDLoader } from "./vmd-loader"
|
|
7
|
+
import { BoneInterpolation, interpolateControlPoints, rawInterpolationToBoneInterpolation } from "./animation"
|
|
5
8
|
|
|
9
|
+
const VMD_FPS = 30
|
|
6
10
|
const VERTEX_STRIDE = 8
|
|
7
11
|
|
|
8
12
|
export interface Texture {
|
|
@@ -146,6 +150,13 @@ interface TweenState {
|
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
export class Model {
|
|
153
|
+
// loadPmx: fetch PMX + register with Engine.getInstance() (init engine first)
|
|
154
|
+
static async loadPmx(path: string): Promise<Model> {
|
|
155
|
+
const model = await PmxLoader.load(path)
|
|
156
|
+
await Engine.getInstance().registerModel(model, path)
|
|
157
|
+
return model
|
|
158
|
+
}
|
|
159
|
+
|
|
149
160
|
private vertexData: Float32Array<ArrayBuffer>
|
|
150
161
|
private baseVertexData: Float32Array<ArrayBuffer> // Original vertex data before morphing
|
|
151
162
|
private vertexCount: number
|
|
@@ -181,9 +192,9 @@ export class Model {
|
|
|
181
192
|
private tweenTimeMs: number = 0 // Time tracking for tweens (milliseconds)
|
|
182
193
|
|
|
183
194
|
// Animation runtime
|
|
184
|
-
private
|
|
185
|
-
private boneTracks: Map<string, Array<{
|
|
186
|
-
private morphTracks: Map<string, Array<{
|
|
195
|
+
private _hasAnimation: boolean = false
|
|
196
|
+
private boneTracks: Map<string, Array<{ boneName: string; frame: number; rotation: Quat; translation: Vec3; interpolation: BoneInterpolation; time: number }>> = new Map()
|
|
197
|
+
private morphTracks: Map<string, Array<{ morphName: string; frame: number; weight: number; time: number }>> = new Map()
|
|
187
198
|
private animationDuration: number = 0
|
|
188
199
|
private isPlaying: boolean = false
|
|
189
200
|
private isPaused: boolean = false
|
|
@@ -453,6 +464,13 @@ export class Model {
|
|
|
453
464
|
return this.skeleton
|
|
454
465
|
}
|
|
455
466
|
|
|
467
|
+
// World bone origin (world matrix col3); unknown name → null
|
|
468
|
+
getBoneWorldPosition(boneName: string): Vec3 | null {
|
|
469
|
+
const idx = this.runtimeSkeleton.nameIndex[boneName]
|
|
470
|
+
if (idx === undefined || idx < 0) return null
|
|
471
|
+
return this.runtimeSkeleton.worldMatrices[idx].getPosition()
|
|
472
|
+
}
|
|
473
|
+
|
|
456
474
|
getSkinning(): Skinning {
|
|
457
475
|
return this.skinning
|
|
458
476
|
}
|
|
@@ -578,14 +596,7 @@ export class Model {
|
|
|
578
596
|
}
|
|
579
597
|
}
|
|
580
598
|
|
|
581
|
-
|
|
582
|
-
* Convert VMD-style relative translation (relative to bind pose world position) to local translation
|
|
583
|
-
* This helper is used by both moveBones and getPoseAtTime to ensure consistent translation handling
|
|
584
|
-
* @param boneIdx - Bone index
|
|
585
|
-
* @param vmdRelativeTranslation - VMD relative translation
|
|
586
|
-
* @param rotation - Optional rotation to use for conversion. If not provided, uses current localRotation.
|
|
587
|
-
* Use animation rotation (from frame) to avoid conflicts when IK modifies rotation.
|
|
588
|
-
*/
|
|
599
|
+
// VMD translation (world delta from bind pose) → bone local space; optional rotation for animation vs IK
|
|
589
600
|
private convertVMDTranslationToLocal(boneIdx: number, vmdRelativeTranslation: Vec3, rotation?: Quat): Vec3 {
|
|
590
601
|
const skeleton = this.skeleton
|
|
591
602
|
const bones = skeleton.bones
|
|
@@ -695,6 +706,11 @@ export class Model {
|
|
|
695
706
|
this.runtimeMorph.weights[idx] = clampedWeight
|
|
696
707
|
this.tweenState.morphActive[idx] = 0
|
|
697
708
|
this.applyMorphs()
|
|
709
|
+
try {
|
|
710
|
+
Engine.getInstance().markVertexBufferDirty()
|
|
711
|
+
} catch {
|
|
712
|
+
/* not registered yet */
|
|
713
|
+
}
|
|
698
714
|
return
|
|
699
715
|
}
|
|
700
716
|
|
|
@@ -723,57 +739,6 @@ export class Model {
|
|
|
723
739
|
this.applyMorphs()
|
|
724
740
|
}
|
|
725
741
|
|
|
726
|
-
/**
|
|
727
|
-
* Atomic pose setter for external animation editors.
|
|
728
|
-
* Sets bone rotations, translations, and morph weights in a single pass,
|
|
729
|
-
* identical to how getPoseAtTime applies VMD poses during playback.
|
|
730
|
-
* Cancels any active tweens on affected bones/morphs.
|
|
731
|
-
*/
|
|
732
|
-
setPose(
|
|
733
|
-
rotations?: Record<string, Quat>,
|
|
734
|
-
translations?: Record<string, Vec3>,
|
|
735
|
-
morphs?: Record<string, number>
|
|
736
|
-
): void {
|
|
737
|
-
const state = this.tweenState
|
|
738
|
-
|
|
739
|
-
if (rotations) {
|
|
740
|
-
for (const [name, quat] of Object.entries(rotations)) {
|
|
741
|
-
const idx = this.runtimeSkeleton.nameIndex[name] ?? -1
|
|
742
|
-
if (idx < 0 || idx >= this.skeleton.bones.length) continue
|
|
743
|
-
|
|
744
|
-
this.runtimeSkeleton.localRotations[idx].set(quat.clone().normalize())
|
|
745
|
-
state.rotActive[idx] = 0
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
if (translations) {
|
|
750
|
-
for (const [name, vec] of Object.entries(translations)) {
|
|
751
|
-
const idx = this.runtimeSkeleton.nameIndex[name] ?? -1
|
|
752
|
-
if (idx < 0 || idx >= this.skeleton.bones.length) continue
|
|
753
|
-
|
|
754
|
-
const rotation = rotations?.[name]?.clone().normalize()
|
|
755
|
-
const localTranslation = this.convertVMDTranslationToLocal(idx, vec, rotation)
|
|
756
|
-
this.runtimeSkeleton.localTranslations[idx].set(localTranslation)
|
|
757
|
-
state.transActive[idx] = 0
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
if (morphs) {
|
|
762
|
-
let morphChanged = false
|
|
763
|
-
for (const [name, weight] of Object.entries(morphs)) {
|
|
764
|
-
const idx = this.runtimeMorph.nameIndex[name] ?? -1
|
|
765
|
-
if (idx < 0 || idx >= this.runtimeMorph.weights.length) continue
|
|
766
|
-
|
|
767
|
-
this.runtimeMorph.weights[idx] = Math.max(0, Math.min(1, weight))
|
|
768
|
-
state.morphActive[idx] = 0
|
|
769
|
-
morphChanged = true
|
|
770
|
-
}
|
|
771
|
-
if (morphChanged) {
|
|
772
|
-
this.applyMorphs()
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
742
|
private applyMorphs(): void {
|
|
778
743
|
// Reset vertex data to base positions
|
|
779
744
|
this.vertexData.set(this.baseVertexData)
|
|
@@ -839,15 +804,81 @@ export class Model {
|
|
|
839
804
|
}
|
|
840
805
|
}
|
|
841
806
|
|
|
842
|
-
/**
|
|
843
|
-
* Load VMD animation file
|
|
844
|
-
*/
|
|
845
807
|
async loadVmd(vmdUrl: string): Promise<void> {
|
|
846
|
-
|
|
808
|
+
const vmdKeyFrames = await VMDLoader.load(vmdUrl)
|
|
809
|
+
|
|
847
810
|
this.resetAllBones()
|
|
848
811
|
this.resetAllMorphs()
|
|
849
|
-
|
|
850
|
-
//
|
|
812
|
+
|
|
813
|
+
// Build bone tracks: Map<boneName, Array<{boneName, frame, rotation, translation, interpolation, time}>>
|
|
814
|
+
const boneTracksByBone: Record<string, Array<{ frame: number; rotation: Quat; translation: Vec3; interpolation: BoneInterpolation }>> = {}
|
|
815
|
+
for (const keyFrame of vmdKeyFrames) {
|
|
816
|
+
for (const bf of keyFrame.boneFrames) {
|
|
817
|
+
if (!boneTracksByBone[bf.boneName]) boneTracksByBone[bf.boneName] = []
|
|
818
|
+
boneTracksByBone[bf.boneName].push({
|
|
819
|
+
frame: bf.frame,
|
|
820
|
+
rotation: bf.rotation,
|
|
821
|
+
translation: bf.translation,
|
|
822
|
+
interpolation: rawInterpolationToBoneInterpolation(bf.interpolation),
|
|
823
|
+
})
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
this.boneTracks = new Map()
|
|
828
|
+
for (const name in boneTracksByBone) {
|
|
829
|
+
const keyframes = boneTracksByBone[name]
|
|
830
|
+
const sorted = [...keyframes].sort((a, b) => a.frame - b.frame)
|
|
831
|
+
this.boneTracks.set(
|
|
832
|
+
name,
|
|
833
|
+
sorted.map((kf) => ({
|
|
834
|
+
boneName: name,
|
|
835
|
+
frame: kf.frame,
|
|
836
|
+
rotation: kf.rotation,
|
|
837
|
+
translation: kf.translation,
|
|
838
|
+
interpolation: kf.interpolation,
|
|
839
|
+
time: kf.frame / VMD_FPS,
|
|
840
|
+
}))
|
|
841
|
+
)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Build morph tracks: Map<morphName, Array<{morphName, frame, weight, time}>>
|
|
845
|
+
const morphTracksByMorph: Record<string, Array<{ frame: number; weight: number }>> = {}
|
|
846
|
+
for (const keyFrame of vmdKeyFrames) {
|
|
847
|
+
for (const mf of keyFrame.morphFrames) {
|
|
848
|
+
if (!morphTracksByMorph[mf.morphName]) morphTracksByMorph[mf.morphName] = []
|
|
849
|
+
morphTracksByMorph[mf.morphName].push({ frame: mf.frame, weight: mf.weight })
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
this.morphTracks = new Map()
|
|
854
|
+
for (const name in morphTracksByMorph) {
|
|
855
|
+
const keyframes = morphTracksByMorph[name]
|
|
856
|
+
const sorted = [...keyframes].sort((a, b) => a.frame - b.frame)
|
|
857
|
+
this.morphTracks.set(
|
|
858
|
+
name,
|
|
859
|
+
sorted.map((kf) => ({
|
|
860
|
+
morphName: name,
|
|
861
|
+
frame: kf.frame,
|
|
862
|
+
weight: kf.weight,
|
|
863
|
+
time: kf.frame / VMD_FPS,
|
|
864
|
+
}))
|
|
865
|
+
)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
this.boneTrackIndices.clear()
|
|
869
|
+
this.morphTrackIndices.clear()
|
|
870
|
+
|
|
871
|
+
// Calculate duration
|
|
872
|
+
let maxTime = 0
|
|
873
|
+
for (const frames of this.boneTracks.values()) {
|
|
874
|
+
if (frames.length > 0) maxTime = Math.max(maxTime, frames[frames.length - 1].time)
|
|
875
|
+
}
|
|
876
|
+
for (const frames of this.morphTracks.values()) {
|
|
877
|
+
if (frames.length > 0) maxTime = Math.max(maxTime, frames[frames.length - 1].time)
|
|
878
|
+
}
|
|
879
|
+
this.animationDuration = maxTime
|
|
880
|
+
|
|
881
|
+
this._hasAnimation = true
|
|
851
882
|
this.animationTime = 0
|
|
852
883
|
this.getPoseAtTime(0)
|
|
853
884
|
|
|
@@ -857,9 +888,6 @@ export class Model {
|
|
|
857
888
|
}
|
|
858
889
|
}
|
|
859
890
|
|
|
860
|
-
/**
|
|
861
|
-
* Reset all bones to their default pose
|
|
862
|
-
*/
|
|
863
891
|
public resetAllBones(): void {
|
|
864
892
|
for (let boneIdx = 0; boneIdx < this.skeleton.bones.length; boneIdx++) {
|
|
865
893
|
const localRot = this.runtimeSkeleton.localRotations[boneIdx]
|
|
@@ -884,85 +912,16 @@ export class Model {
|
|
|
884
912
|
this.applyMorphs()
|
|
885
913
|
}
|
|
886
914
|
|
|
887
|
-
/**
|
|
888
|
-
* Enable or disable IK solving
|
|
889
|
-
*/
|
|
890
915
|
public setIKEnabled(enabled: boolean): void {
|
|
891
916
|
this.ikEnabled = enabled
|
|
892
917
|
}
|
|
893
918
|
|
|
894
|
-
/**
|
|
895
|
-
* Enable or disable physics simulation
|
|
896
|
-
*/
|
|
897
919
|
public setPhysicsEnabled(enabled: boolean): void {
|
|
898
920
|
this.physicsEnabled = enabled
|
|
899
921
|
}
|
|
900
922
|
|
|
901
|
-
/**
|
|
902
|
-
* Process frames into tracks
|
|
903
|
-
*/
|
|
904
|
-
private processFrames(): void {
|
|
905
|
-
if (!this.animationData) return
|
|
906
|
-
|
|
907
|
-
// Helper to group frames by name and sort by time
|
|
908
|
-
const groupFrames = <T>(
|
|
909
|
-
items: Array<{ item: T; name: string; time: number }>
|
|
910
|
-
): Map<string, Array<{ item: T; time: number }>> => {
|
|
911
|
-
const tracks = new Map<string, Array<{ item: T; time: number }>>()
|
|
912
|
-
for (const { item, name, time } of items) {
|
|
913
|
-
if (!tracks.has(name)) tracks.set(name, [])
|
|
914
|
-
tracks.get(name)!.push({ item, time })
|
|
915
|
-
}
|
|
916
|
-
for (const keyFrames of tracks.values()) {
|
|
917
|
-
keyFrames.sort((a, b) => a.time - b.time)
|
|
918
|
-
}
|
|
919
|
-
return tracks
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
// Collect all bone and morph frames
|
|
923
|
-
const boneItems: Array<{ item: BoneFrame; name: string; time: number }> = []
|
|
924
|
-
const morphItems: Array<{ item: MorphFrame; name: string; time: number }> = []
|
|
925
|
-
|
|
926
|
-
for (const keyFrame of this.animationData) {
|
|
927
|
-
for (const boneFrame of keyFrame.boneFrames) {
|
|
928
|
-
boneItems.push({ item: boneFrame, name: boneFrame.boneName, time: keyFrame.time })
|
|
929
|
-
}
|
|
930
|
-
for (const morphFrame of keyFrame.morphFrames) {
|
|
931
|
-
morphItems.push({ item: morphFrame, name: morphFrame.morphName, time: keyFrame.time })
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// Transform to expected format
|
|
936
|
-
this.boneTracks = new Map()
|
|
937
|
-
for (const [name, frames] of groupFrames(boneItems).entries()) {
|
|
938
|
-
this.boneTracks.set(
|
|
939
|
-
name,
|
|
940
|
-
frames.map((f) => ({ boneFrame: f.item, time: f.time }))
|
|
941
|
-
)
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
this.morphTracks = new Map()
|
|
945
|
-
for (const [name, frames] of groupFrames(morphItems).entries()) {
|
|
946
|
-
this.morphTracks.set(
|
|
947
|
-
name,
|
|
948
|
-
frames.map((f) => ({ morphFrame: f.item, time: f.time }))
|
|
949
|
-
)
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// Reset cached indices when tracks change
|
|
953
|
-
this.boneTrackIndices.clear()
|
|
954
|
-
this.morphTrackIndices.clear()
|
|
955
|
-
|
|
956
|
-
// Calculate duration from all tracks
|
|
957
|
-
const allTracks = [...this.boneTracks.values(), ...this.morphTracks.values()]
|
|
958
|
-
this.animationDuration = allTracks.reduce((max, keyFrames) => {
|
|
959
|
-
const lastTime = keyFrames[keyFrames.length - 1]?.time ?? 0
|
|
960
|
-
return Math.max(max, lastTime)
|
|
961
|
-
}, 0)
|
|
962
|
-
}
|
|
963
|
-
|
|
964
923
|
playAnimation(): void {
|
|
965
|
-
if (!this.
|
|
924
|
+
if (!this._hasAnimation) return
|
|
966
925
|
|
|
967
926
|
this.isPaused = false
|
|
968
927
|
this.isPlaying = true
|
|
@@ -985,14 +944,11 @@ export class Model {
|
|
|
985
944
|
}
|
|
986
945
|
|
|
987
946
|
seekAnimation(time: number): void {
|
|
988
|
-
if (!this.
|
|
947
|
+
if (!this._hasAnimation) return
|
|
989
948
|
const clampedTime = Math.max(0, Math.min(time, this.animationDuration))
|
|
990
949
|
this.animationTime = clampedTime
|
|
991
950
|
}
|
|
992
951
|
|
|
993
|
-
/**
|
|
994
|
-
* Get current animation progress
|
|
995
|
-
*/
|
|
996
952
|
getAnimationProgress(): { current: number; duration: number; percentage: number } {
|
|
997
953
|
const duration = this.animationDuration
|
|
998
954
|
const percentage = duration > 0 ? (this.animationTime / duration) * 100 : 0
|
|
@@ -1003,9 +959,6 @@ export class Model {
|
|
|
1003
959
|
}
|
|
1004
960
|
}
|
|
1005
961
|
|
|
1006
|
-
/**
|
|
1007
|
-
* Binary search upper bound helper (static to avoid recreation)
|
|
1008
|
-
*/
|
|
1009
962
|
private static upperBound<T extends { time: number }>(time: number, keyFrames: T[], startIdx: number = 0): number {
|
|
1010
963
|
let left = startIdx,
|
|
1011
964
|
right = keyFrames.length
|
|
@@ -1017,10 +970,6 @@ export class Model {
|
|
|
1017
970
|
return left
|
|
1018
971
|
}
|
|
1019
972
|
|
|
1020
|
-
/**
|
|
1021
|
-
* Find keyframe index with caching optimization
|
|
1022
|
-
* Uses cached index as starting point for faster lookup when time is close
|
|
1023
|
-
*/
|
|
1024
973
|
private findKeyframeIndex<T extends { time: number }>(time: number, keyFrames: T[], cachedIdx: number): number {
|
|
1025
974
|
if (keyFrames.length === 0) return -1
|
|
1026
975
|
|
|
@@ -1040,14 +989,8 @@ export class Model {
|
|
|
1040
989
|
return idx
|
|
1041
990
|
}
|
|
1042
991
|
|
|
1043
|
-
/**
|
|
1044
|
-
* Get pose at specific time (internal helper)
|
|
1045
|
-
* Optimized for per-frame performance
|
|
1046
|
-
*/
|
|
1047
992
|
private getPoseAtTime(time: number): void {
|
|
1048
|
-
if (!this.
|
|
1049
|
-
|
|
1050
|
-
const INV_127 = 1 / 127 // Pre-compute division constant
|
|
993
|
+
if (!this._hasAnimation) return
|
|
1051
994
|
|
|
1052
995
|
// Process bone tracks
|
|
1053
996
|
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
@@ -1059,11 +1002,10 @@ export class Model {
|
|
|
1059
1002
|
|
|
1060
1003
|
if (idx < 0) continue
|
|
1061
1004
|
|
|
1062
|
-
// Update cache
|
|
1063
1005
|
this.boneTrackIndices.set(boneName, idx)
|
|
1064
1006
|
|
|
1065
|
-
const frameA = keyFrames[idx]
|
|
1066
|
-
const frameB = keyFrames[idx + 1]
|
|
1007
|
+
const frameA = keyFrames[idx]
|
|
1008
|
+
const frameB = keyFrames[idx + 1]
|
|
1067
1009
|
|
|
1068
1010
|
const boneIdx = this.runtimeSkeleton.nameIndex[boneName]
|
|
1069
1011
|
if (boneIdx === undefined) continue
|
|
@@ -1072,61 +1014,30 @@ export class Model {
|
|
|
1072
1014
|
const localTrans = this.runtimeSkeleton.localTranslations[boneIdx]
|
|
1073
1015
|
|
|
1074
1016
|
if (!frameB) {
|
|
1075
|
-
// No interpolation needed - direct assignment
|
|
1076
|
-
// Use animation frame's rotation for translation conversion to ensure consistency
|
|
1077
|
-
// This prevents conflicts when IK later modifies the rotation
|
|
1078
1017
|
const frameRotation = frameA.rotation
|
|
1079
1018
|
localRot.set(frameRotation)
|
|
1080
|
-
// Convert VMD relative translation to local translation using animation rotation
|
|
1081
1019
|
const localTranslation = this.convertVMDTranslationToLocal(boneIdx, frameA.translation, frameRotation)
|
|
1082
1020
|
localTrans.set(localTranslation)
|
|
1083
1021
|
} else {
|
|
1084
|
-
const
|
|
1085
|
-
const
|
|
1086
|
-
const timeDelta = timeB - timeA
|
|
1087
|
-
const gradient = (clampedTime - timeA) / timeDelta
|
|
1022
|
+
const timeDelta = frameB.time - frameA.time
|
|
1023
|
+
const gradient = (clampedTime - frameA.time) / timeDelta
|
|
1088
1024
|
const interp = frameB.interpolation
|
|
1089
1025
|
|
|
1090
|
-
|
|
1091
|
-
const rotT = bezierInterpolate(
|
|
1092
|
-
interp[0] * INV_127,
|
|
1093
|
-
interp[1] * INV_127,
|
|
1094
|
-
interp[2] * INV_127,
|
|
1095
|
-
interp[3] * INV_127,
|
|
1096
|
-
gradient
|
|
1097
|
-
)
|
|
1098
|
-
|
|
1099
|
-
// Use Quat.slerp to interpolate rotation
|
|
1026
|
+
const rotT = interpolateControlPoints(interp.rotation, gradient)
|
|
1100
1027
|
const rotation = Quat.slerp(frameA.rotation, frameB.rotation, rotT)
|
|
1101
1028
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
interp[offset] * INV_127,
|
|
1107
|
-
interp[offset + 8] * INV_127,
|
|
1108
|
-
interp[offset + 4] * INV_127,
|
|
1109
|
-
interp[offset + 12] * INV_127,
|
|
1110
|
-
gradient
|
|
1111
|
-
)
|
|
1112
|
-
|
|
1113
|
-
const txWeight = getWeight(0)
|
|
1114
|
-
const tyWeight = getWeight(16)
|
|
1115
|
-
const tzWeight = getWeight(32)
|
|
1116
|
-
|
|
1117
|
-
// Interpolate VMD relative translations (relative to bind pose world position)
|
|
1029
|
+
const txWeight = interpolateControlPoints(interp.translationX, gradient)
|
|
1030
|
+
const tyWeight = interpolateControlPoints(interp.translationY, gradient)
|
|
1031
|
+
const tzWeight = interpolateControlPoints(interp.translationZ, gradient)
|
|
1032
|
+
|
|
1118
1033
|
const interpolatedVMDTranslation = new Vec3(
|
|
1119
1034
|
frameA.translation.x + (frameB.translation.x - frameA.translation.x) * txWeight,
|
|
1120
1035
|
frameA.translation.y + (frameB.translation.y - frameA.translation.y) * tyWeight,
|
|
1121
1036
|
frameA.translation.z + (frameB.translation.z - frameA.translation.z) * tzWeight
|
|
1122
1037
|
)
|
|
1123
1038
|
|
|
1124
|
-
// Convert interpolated VMD translation to local translation using animation rotation
|
|
1125
|
-
// This ensures translation is computed for the animation rotation, not the runtime rotation
|
|
1126
|
-
// that will be modified by IK, preventing conflicts
|
|
1127
1039
|
const localTranslation = this.convertVMDTranslationToLocal(boneIdx, interpolatedVMDTranslation, rotation)
|
|
1128
1040
|
|
|
1129
|
-
// Direct property writes to avoid object allocation
|
|
1130
1041
|
localRot.set(rotation)
|
|
1131
1042
|
localTrans.set(localTranslation)
|
|
1132
1043
|
}
|
|
@@ -1142,19 +1053,18 @@ export class Model {
|
|
|
1142
1053
|
|
|
1143
1054
|
if (idx < 0) continue
|
|
1144
1055
|
|
|
1145
|
-
// Update cache
|
|
1146
1056
|
this.morphTrackIndices.set(morphName, idx)
|
|
1147
1057
|
|
|
1148
|
-
const frameA = keyFrames[idx]
|
|
1149
|
-
const frameB = keyFrames[idx + 1]
|
|
1058
|
+
const frameA = keyFrames[idx]
|
|
1059
|
+
const frameB = keyFrames[idx + 1]
|
|
1150
1060
|
|
|
1151
1061
|
const morphIdx = this.runtimeMorph.nameIndex[morphName]
|
|
1152
1062
|
if (morphIdx === undefined) continue
|
|
1153
1063
|
|
|
1154
1064
|
const weight = frameB
|
|
1155
1065
|
? frameA.weight +
|
|
1156
|
-
|
|
1157
|
-
|
|
1066
|
+
(frameB.weight - frameA.weight) *
|
|
1067
|
+
((clampedTime - keyFrames[idx].time) / (keyFrames[idx + 1].time - keyFrames[idx].time))
|
|
1158
1068
|
: frameA.weight
|
|
1159
1069
|
|
|
1160
1070
|
this.runtimeMorph.weights[morphIdx] = weight
|
|
@@ -1162,12 +1072,7 @@ export class Model {
|
|
|
1162
1072
|
}
|
|
1163
1073
|
}
|
|
1164
1074
|
|
|
1165
|
-
|
|
1166
|
-
* Updates the model pose by recomputing all matrices.
|
|
1167
|
-
* If animation is playing, applies animation pose first.
|
|
1168
|
-
* deltaTime: Time elapsed since last update in seconds
|
|
1169
|
-
* Returns true if vertices were modified (morphs changed)
|
|
1170
|
-
*/
|
|
1075
|
+
// Returns true when morphs changed (vertex buffer may need upload)
|
|
1171
1076
|
update(deltaTime: number): boolean {
|
|
1172
1077
|
// Update tween time (in milliseconds)
|
|
1173
1078
|
this.tweenTimeMs += deltaTime * 1000
|
|
@@ -1176,7 +1081,7 @@ export class Model {
|
|
|
1176
1081
|
const tweensChangedMorphs = this.updateTweens()
|
|
1177
1082
|
|
|
1178
1083
|
// Apply animation if playing or paused (always apply pose if animation data exists and we have a time set)
|
|
1179
|
-
if (this.
|
|
1084
|
+
if (this._hasAnimation) {
|
|
1180
1085
|
if (this.isPlaying && !this.isPaused) {
|
|
1181
1086
|
this.animationTime += deltaTime
|
|
1182
1087
|
|
|
@@ -1228,7 +1133,7 @@ export class Model {
|
|
|
1228
1133
|
// Recompute ALL world matrices before each solver starts
|
|
1229
1134
|
// This ensures each solver sees the effects of previous solvers on localRotations
|
|
1230
1135
|
this.computeWorldMatrices()
|
|
1231
|
-
|
|
1136
|
+
|
|
1232
1137
|
// Clear computed set for this solver's pass
|
|
1233
1138
|
this.ikComputedSet.clear()
|
|
1234
1139
|
|
|
@@ -1293,9 +1198,9 @@ export class Model {
|
|
|
1293
1198
|
|
|
1294
1199
|
// Handle append transformations (same logic as computeWorldMatrices)
|
|
1295
1200
|
const appendParentIdx = b.appendParentIndex
|
|
1296
|
-
const hasAppend = b.appendRotate &&
|
|
1297
|
-
appendParentIdx !== undefined &&
|
|
1298
|
-
appendParentIdx >= 0 &&
|
|
1201
|
+
const hasAppend = b.appendRotate &&
|
|
1202
|
+
appendParentIdx !== undefined &&
|
|
1203
|
+
appendParentIdx >= 0 &&
|
|
1299
1204
|
appendParentIdx < bones.length
|
|
1300
1205
|
|
|
1301
1206
|
if (hasAppend) {
|
|
@@ -1312,7 +1217,7 @@ export class Model {
|
|
|
1312
1217
|
// Compute append parent's world matrix for dependency order, but use base rotation for append
|
|
1313
1218
|
this.computeSingleBoneWorldMatrix(appendParentIdx, applyIK)
|
|
1314
1219
|
}
|
|
1315
|
-
|
|
1220
|
+
|
|
1316
1221
|
// Use append parent's base local rotation only (IK rotations are applied after solving)
|
|
1317
1222
|
let appendRot = localRot[appendParentIdx]
|
|
1318
1223
|
|
|
@@ -1320,7 +1225,7 @@ export class Model {
|
|
|
1320
1225
|
const aw = appendRot.w
|
|
1321
1226
|
const absRatio = ratio < 0 ? -ratio : ratio
|
|
1322
1227
|
if (ratio < 0) { ax = -ax; ay = -ay; az = -az }
|
|
1323
|
-
|
|
1228
|
+
|
|
1324
1229
|
const appendQuat = new Quat(ax, ay, az, aw)
|
|
1325
1230
|
const result = Quat.slerp(Quat.identity(), appendQuat, absRatio)
|
|
1326
1231
|
rotateM = Mat4.fromQuat(result.x, result.y, result.z, result.w).multiply(rotateM)
|
package/src/pmx-loader.ts
CHANGED
|
@@ -262,7 +262,7 @@ export class PmxLoader {
|
|
|
262
262
|
this.getFloat32(),
|
|
263
263
|
]
|
|
264
264
|
// edgeSize float
|
|
265
|
-
const edgeSize = this.getFloat32()
|
|
265
|
+
const edgeSize = this.getFloat32()*2 // double the size for better visibility
|
|
266
266
|
|
|
267
267
|
const textureIndex = this.getNonVertexIndex(this.textureIndexSize)
|
|
268
268
|
const sphereTextureIndex = this.getNonVertexIndex(this.textureIndexSize)
|