reze-engine 0.8.1 → 0.8.3
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 +15 -92
- package/dist/animation.d.ts +57 -0
- package/dist/animation.d.ts.map +1 -1
- package/dist/animation.js +142 -3
- package/dist/engine.d.ts +0 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/model.d.ts +16 -10
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +70 -79
- package/package.json +1 -1
- package/src/animation.ts +183 -3
- package/src/engine.ts +1 -2
- package/src/index.ts +6 -0
- package/src/model.ts +89 -91
package/src/model.ts
CHANGED
|
@@ -3,8 +3,16 @@ import { Engine } from "./engine"
|
|
|
3
3
|
import { PmxLoader } from "./pmx-loader"
|
|
4
4
|
import { Rigidbody, Joint } from "./physics"
|
|
5
5
|
import { IKSolverSystem } from "./ik-solver"
|
|
6
|
-
import { VMDLoader } from "./vmd-loader"
|
|
7
|
-
import {
|
|
6
|
+
import { VMDLoader, type VMDKeyFrame } from "./vmd-loader"
|
|
7
|
+
import {
|
|
8
|
+
AnimationClip,
|
|
9
|
+
AnimationState,
|
|
10
|
+
BoneInterpolation,
|
|
11
|
+
BoneKeyframe,
|
|
12
|
+
MorphKeyframe,
|
|
13
|
+
interpolateControlPoints,
|
|
14
|
+
rawInterpolationToBoneInterpolation,
|
|
15
|
+
} from "./animation"
|
|
8
16
|
|
|
9
17
|
const VMD_FPS = 30
|
|
10
18
|
const VERTEX_STRIDE = 8
|
|
@@ -155,10 +163,14 @@ export class Model {
|
|
|
155
163
|
return "model_" + Model._nextId++
|
|
156
164
|
}
|
|
157
165
|
|
|
158
|
-
static async
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
166
|
+
static async loadFrom(path: string): Promise<Model>
|
|
167
|
+
static async loadFrom(name: string, path: string): Promise<Model>
|
|
168
|
+
static async loadFrom(nameOrPath: string, path?: string): Promise<Model> {
|
|
169
|
+
const name = path === undefined ? Model.nextDefaultName() : nameOrPath
|
|
170
|
+
const pmxPath = path === undefined ? nameOrPath : path
|
|
171
|
+
const model = await PmxLoader.load(pmxPath)
|
|
172
|
+
model.setName(name)
|
|
173
|
+
await Engine.getInstance().registerModel(model, pmxPath)
|
|
162
174
|
return model
|
|
163
175
|
}
|
|
164
176
|
|
|
@@ -206,18 +218,11 @@ export class Model {
|
|
|
206
218
|
private tweenState!: TweenState
|
|
207
219
|
private tweenTimeMs: number = 0 // Time tracking for tweens (milliseconds)
|
|
208
220
|
|
|
209
|
-
// Animation
|
|
210
|
-
private
|
|
211
|
-
private boneTracks: Map<string, Array<{ boneName: string; frame: number; rotation: Quat; translation: Vec3; interpolation: BoneInterpolation; time: number }>> = new Map()
|
|
212
|
-
private morphTracks: Map<string, Array<{ morphName: string; frame: number; weight: number; time: number }>> = new Map()
|
|
213
|
-
private animationDuration: number = 0
|
|
214
|
-
private isPlaying: boolean = false
|
|
215
|
-
private isPaused: boolean = false
|
|
216
|
-
private animationTime: number = 0 // Current time in animation (seconds)
|
|
217
|
-
|
|
218
|
-
// Cached keyframe indices for faster lookup (per track)
|
|
221
|
+
// Animation: state and multiple slots (idle, walk, attack, etc.); commit/rollback for action-game style
|
|
222
|
+
private readonly animationState = new AnimationState()
|
|
219
223
|
private boneTrackIndices: Map<string, number> = new Map()
|
|
220
224
|
private morphTrackIndices: Map<string, number> = new Map()
|
|
225
|
+
private lastAppliedClip: AnimationClip | null = null
|
|
221
226
|
|
|
222
227
|
// IK and Physics enable flags
|
|
223
228
|
private ikEnabled = true
|
|
@@ -720,7 +725,7 @@ export class Model {
|
|
|
720
725
|
try {
|
|
721
726
|
Engine.getInstance().markVertexBufferDirty(this)
|
|
722
727
|
} catch {
|
|
723
|
-
|
|
728
|
+
// not registered yet
|
|
724
729
|
}
|
|
725
730
|
return
|
|
726
731
|
}
|
|
@@ -815,13 +820,7 @@ export class Model {
|
|
|
815
820
|
}
|
|
816
821
|
}
|
|
817
822
|
|
|
818
|
-
|
|
819
|
-
const vmdKeyFrames = await VMDLoader.load(vmdUrl)
|
|
820
|
-
|
|
821
|
-
this.resetAllBones()
|
|
822
|
-
this.resetAllMorphs()
|
|
823
|
-
|
|
824
|
-
// Build bone tracks: Map<boneName, Array<{boneName, frame, rotation, translation, interpolation, time}>>
|
|
823
|
+
private buildClipFromVmdKeyFrames(vmdKeyFrames: VMDKeyFrame[]): AnimationClip {
|
|
825
824
|
const boneTracksByBone: Record<string, Array<{ frame: number; rotation: Quat; translation: Vec3; interpolation: BoneInterpolation }>> = {}
|
|
826
825
|
for (const keyFrame of vmdKeyFrames) {
|
|
827
826
|
for (const bf of keyFrame.boneFrames) {
|
|
@@ -834,12 +833,11 @@ export class Model {
|
|
|
834
833
|
})
|
|
835
834
|
}
|
|
836
835
|
}
|
|
837
|
-
|
|
838
|
-
this.boneTracks = new Map()
|
|
836
|
+
const boneTracks = new Map<string, BoneKeyframe[]>()
|
|
839
837
|
for (const name in boneTracksByBone) {
|
|
840
838
|
const keyframes = boneTracksByBone[name]
|
|
841
839
|
const sorted = [...keyframes].sort((a, b) => a.frame - b.frame)
|
|
842
|
-
|
|
840
|
+
boneTracks.set(
|
|
843
841
|
name,
|
|
844
842
|
sorted.map((kf) => ({
|
|
845
843
|
boneName: name,
|
|
@@ -851,8 +849,6 @@ export class Model {
|
|
|
851
849
|
}))
|
|
852
850
|
)
|
|
853
851
|
}
|
|
854
|
-
|
|
855
|
-
// Build morph tracks: Map<morphName, Array<{morphName, frame, weight, time}>>
|
|
856
852
|
const morphTracksByMorph: Record<string, Array<{ frame: number; weight: number }>> = {}
|
|
857
853
|
for (const keyFrame of vmdKeyFrames) {
|
|
858
854
|
for (const mf of keyFrame.morphFrames) {
|
|
@@ -860,12 +856,11 @@ export class Model {
|
|
|
860
856
|
morphTracksByMorph[mf.morphName].push({ frame: mf.frame, weight: mf.weight })
|
|
861
857
|
}
|
|
862
858
|
}
|
|
863
|
-
|
|
864
|
-
this.morphTracks = new Map()
|
|
859
|
+
const morphTracks = new Map<string, MorphKeyframe[]>()
|
|
865
860
|
for (const name in morphTracksByMorph) {
|
|
866
861
|
const keyframes = morphTracksByMorph[name]
|
|
867
862
|
const sorted = [...keyframes].sort((a, b) => a.frame - b.frame)
|
|
868
|
-
|
|
863
|
+
morphTracks.set(
|
|
869
864
|
name,
|
|
870
865
|
sorted.map((kf) => ({
|
|
871
866
|
morphName: name,
|
|
@@ -875,24 +870,20 @@ export class Model {
|
|
|
875
870
|
}))
|
|
876
871
|
)
|
|
877
872
|
}
|
|
878
|
-
|
|
879
|
-
this.boneTrackIndices.clear()
|
|
880
|
-
this.morphTrackIndices.clear()
|
|
881
|
-
|
|
882
|
-
// Calculate duration
|
|
883
873
|
let maxTime = 0
|
|
884
|
-
for (const frames of
|
|
874
|
+
for (const frames of boneTracks.values()) {
|
|
885
875
|
if (frames.length > 0) maxTime = Math.max(maxTime, frames[frames.length - 1].time)
|
|
886
876
|
}
|
|
887
|
-
for (const frames of
|
|
877
|
+
for (const frames of morphTracks.values()) {
|
|
888
878
|
if (frames.length > 0) maxTime = Math.max(maxTime, frames[frames.length - 1].time)
|
|
889
879
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
this._hasAnimation = true
|
|
893
|
-
this.animationTime = 0
|
|
894
|
-
this.getPoseAtTime(0)
|
|
880
|
+
return { boneTracks, morphTracks, duration: maxTime }
|
|
881
|
+
}
|
|
895
882
|
|
|
883
|
+
async loadAnimation(animationName: string, vmdUrl: string): Promise<void> {
|
|
884
|
+
const vmdKeyFrames = await VMDLoader.load(vmdUrl)
|
|
885
|
+
const clip = this.buildClipFromVmdKeyFrames(vmdKeyFrames)
|
|
886
|
+
this.animationState.loadAnimation(animationName, clip)
|
|
896
887
|
}
|
|
897
888
|
|
|
898
889
|
public resetAllBones(): void {
|
|
@@ -900,7 +891,6 @@ export class Model {
|
|
|
900
891
|
const localRot = this.runtimeSkeleton.localRotations[boneIdx]
|
|
901
892
|
const localTrans = this.runtimeSkeleton.localTranslations[boneIdx]
|
|
902
893
|
|
|
903
|
-
// Reset to default pose: identity rotation and zero translation (like initial PMX state)
|
|
904
894
|
localRot.set(Quat.identity())
|
|
905
895
|
localTrans.set(Vec3.zeros())
|
|
906
896
|
}
|
|
@@ -928,39 +918,59 @@ export class Model {
|
|
|
928
918
|
return this.physicsEnabled
|
|
929
919
|
}
|
|
930
920
|
|
|
931
|
-
|
|
932
|
-
|
|
921
|
+
getAnimationState(): AnimationState {
|
|
922
|
+
return this.animationState
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
play(): void
|
|
926
|
+
play(name: string): boolean
|
|
927
|
+
play(name?: string): void | boolean {
|
|
928
|
+
if (name === undefined) {
|
|
929
|
+
this.animationState.play()
|
|
930
|
+
return
|
|
931
|
+
}
|
|
932
|
+
return this.animationState.play(name)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
show(name: string): void {
|
|
936
|
+
this.animationState.show(name)
|
|
937
|
+
}
|
|
933
938
|
|
|
934
|
-
|
|
935
|
-
|
|
939
|
+
// @deprecated Use model.play()
|
|
940
|
+
playAnimation(): void {
|
|
941
|
+
this.animationState.play()
|
|
942
|
+
}
|
|
936
943
|
|
|
944
|
+
pause(): void {
|
|
945
|
+
this.animationState.pause()
|
|
937
946
|
}
|
|
938
947
|
|
|
948
|
+
// @deprecated Use model.pause()
|
|
939
949
|
pauseAnimation(): void {
|
|
940
|
-
|
|
941
|
-
this.isPaused = true
|
|
950
|
+
this.animationState.pause()
|
|
942
951
|
}
|
|
943
952
|
|
|
953
|
+
stop(): void {
|
|
954
|
+
this.animationState.stop()
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// @deprecated Use model.stop()
|
|
944
958
|
stopAnimation(): void {
|
|
945
|
-
this.
|
|
946
|
-
|
|
947
|
-
|
|
959
|
+
this.animationState.stop()
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
seek(time: number): void {
|
|
963
|
+
this.animationState.seek(time)
|
|
948
964
|
}
|
|
949
965
|
|
|
966
|
+
// @deprecated Use model.seek()
|
|
950
967
|
seekAnimation(time: number): void {
|
|
951
|
-
|
|
952
|
-
const clampedTime = Math.max(0, Math.min(time, this.animationDuration))
|
|
953
|
-
this.animationTime = clampedTime
|
|
968
|
+
this.animationState.seek(time)
|
|
954
969
|
}
|
|
955
970
|
|
|
956
|
-
getAnimationProgress(): { current: number; duration: number; percentage: number } {
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
return {
|
|
960
|
-
current: this.animationTime,
|
|
961
|
-
duration,
|
|
962
|
-
percentage,
|
|
963
|
-
}
|
|
971
|
+
getAnimationProgress(): { current: number; duration: number; percentage: number; animationName: string | null } {
|
|
972
|
+
const p = this.animationState.getProgress()
|
|
973
|
+
return { current: p.current, duration: p.duration, percentage: p.percentage, animationName: p.animationName }
|
|
964
974
|
}
|
|
965
975
|
|
|
966
976
|
private static upperBound<T extends { time: number }>(time: number, keyFrames: T[], startIdx: number = 0): number {
|
|
@@ -977,27 +987,26 @@ export class Model {
|
|
|
977
987
|
private findKeyframeIndex<T extends { time: number }>(time: number, keyFrames: T[], cachedIdx: number): number {
|
|
978
988
|
if (keyFrames.length === 0) return -1
|
|
979
989
|
|
|
980
|
-
// Check if cached index is still valid (time is within the cached frame range)
|
|
981
990
|
if (cachedIdx >= 0 && cachedIdx < keyFrames.length) {
|
|
982
991
|
const frameTime = keyFrames[cachedIdx].time
|
|
983
992
|
const nextFrameTime = cachedIdx + 1 < keyFrames.length ? keyFrames[cachedIdx + 1].time : Infinity
|
|
984
|
-
|
|
985
|
-
// If time is within [frameTime, nextFrameTime), use cached index
|
|
986
993
|
if (time >= frameTime && time < nextFrameTime) {
|
|
987
994
|
return cachedIdx
|
|
988
995
|
}
|
|
989
996
|
}
|
|
990
|
-
|
|
991
|
-
// Fall back to binary search
|
|
992
997
|
const idx = Model.upperBound(time, keyFrames, 0) - 1
|
|
993
998
|
return idx
|
|
994
999
|
}
|
|
995
1000
|
|
|
996
|
-
private
|
|
997
|
-
if (!
|
|
1001
|
+
private applyPoseFromClip(clip: AnimationClip | null, time: number): void {
|
|
1002
|
+
if (!clip) return
|
|
1003
|
+
if (clip !== this.lastAppliedClip) {
|
|
1004
|
+
this.boneTrackIndices.clear()
|
|
1005
|
+
this.morphTrackIndices.clear()
|
|
1006
|
+
this.lastAppliedClip = clip
|
|
1007
|
+
}
|
|
998
1008
|
|
|
999
|
-
|
|
1000
|
-
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
1009
|
+
for (const [boneName, keyFrames] of clip.boneTracks.entries()) {
|
|
1001
1010
|
if (keyFrames.length === 0) continue
|
|
1002
1011
|
|
|
1003
1012
|
const cachedIdx = this.boneTrackIndices.get(boneName) ?? -1
|
|
@@ -1047,8 +1056,7 @@ export class Model {
|
|
|
1047
1056
|
}
|
|
1048
1057
|
}
|
|
1049
1058
|
|
|
1050
|
-
|
|
1051
|
-
for (const [morphName, keyFrames] of this.morphTracks.entries()) {
|
|
1059
|
+
for (const [morphName, keyFrames] of clip.morphTracks.entries()) {
|
|
1052
1060
|
if (keyFrames.length === 0) continue
|
|
1053
1061
|
|
|
1054
1062
|
const cachedIdx = this.morphTrackIndices.get(morphName) ?? -1
|
|
@@ -1084,21 +1092,11 @@ export class Model {
|
|
|
1084
1092
|
// Update all active tweens (rotations, translations, morphs)
|
|
1085
1093
|
const tweensChangedMorphs = this.updateTweens()
|
|
1086
1094
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
if (this.animationTime >= this.animationDuration) {
|
|
1093
|
-
this.animationTime = this.animationDuration
|
|
1094
|
-
this.pauseAnimation() // Auto-pause at end
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
this.getPoseAtTime(this.animationTime)
|
|
1098
|
-
} else if (this.isPaused || (!this.isPlaying && this.animationTime >= 0)) {
|
|
1099
|
-
// Apply pose at paused time or if we have a seeked time but not playing
|
|
1100
|
-
this.getPoseAtTime(this.animationTime)
|
|
1101
|
-
}
|
|
1095
|
+
this.animationState.update(deltaTime)
|
|
1096
|
+
const clip = this.animationState.getCurrentClip()
|
|
1097
|
+
const time = this.animationState.getCurrentTime()
|
|
1098
|
+
if (clip !== null) {
|
|
1099
|
+
this.applyPoseFromClip(clip, time)
|
|
1102
1100
|
}
|
|
1103
1101
|
|
|
1104
1102
|
// Apply morphs if tweens changed morphs or animation changed morphs
|