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/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 { Vec3, Quat, Mat4 } from "./math"
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, bezierInterpolate } from "./math"
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 { VMDKeyFrame, VMDLoader, BoneFrame, MorphFrame } from "./vmd-loader"
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 animationData: VMDKeyFrame[] | null = null
185
- private boneTracks: Map<string, Array<{ boneFrame: BoneFrame; time: number }>> = new Map()
186
- private morphTracks: Map<string, Array<{ morphFrame: MorphFrame; time: number }>> = new Map()
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
- this.animationData = await VMDLoader.load(vmdUrl)
808
+ const vmdKeyFrames = await VMDLoader.load(vmdUrl)
809
+
847
810
  this.resetAllBones()
848
811
  this.resetAllMorphs()
849
- this.processFrames()
850
- // Apply initial pose at time 0
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.animationData) return
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.animationData) return
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.animationData) return
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].boneFrame
1066
- const frameB = keyFrames[idx + 1]?.boneFrame
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 timeA = keyFrames[idx].time
1085
- const timeB = keyFrames[idx + 1].time
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
- // Pre-normalize interpolation values (avoid division in bezierInterpolate)
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
- // Interpolate VMD translation using bezier for each component
1103
- // Inline getWeight to avoid function call overhead
1104
- const getWeight = (offset: number) =>
1105
- bezierInterpolate(
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].morphFrame
1149
- const frameB = keyFrames[idx + 1]?.morphFrame
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
- (frameB.weight - frameA.weight) *
1157
- ((clampedTime - keyFrames[idx].time) / (keyFrames[idx + 1].time - keyFrames[idx].time))
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.animationData) {
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)