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/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 { BoneInterpolation, interpolateControlPoints, rawInterpolationToBoneInterpolation } from "./animation"
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 loadPmx(path: string, name?: string): Promise<Model> {
159
- const model = await PmxLoader.load(path)
160
- model.setName(name ?? Model.nextDefaultName())
161
- await Engine.getInstance().registerModel(model, path)
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 runtime
210
- private _hasAnimation: boolean = false
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
- /* not registered yet */
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
- async loadVmd(vmdUrl: string): Promise<void> {
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
- this.boneTracks.set(
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
- this.morphTracks.set(
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 this.boneTracks.values()) {
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 this.morphTracks.values()) {
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
- this.animationDuration = maxTime
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
- playAnimation(): void {
932
- if (!this._hasAnimation) return
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
- this.isPaused = false
935
- this.isPlaying = true
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
- if (!this.isPlaying || this.isPaused) return
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.isPlaying = false
946
- this.isPaused = false
947
- this.animationTime = 0
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
- if (!this._hasAnimation) return
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 duration = this.animationDuration
958
- const percentage = duration > 0 ? (this.animationTime / duration) * 100 : 0
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 getPoseAtTime(time: number): void {
997
- if (!this._hasAnimation) return
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
- // Process bone tracks
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
- // Process morph tracks
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
- // Apply animation if playing or paused (always apply pose if animation data exists and we have a time set)
1088
- if (this._hasAnimation) {
1089
- if (this.isPlaying && !this.isPaused) {
1090
- this.animationTime += deltaTime
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