reze-engine 0.6.6 → 0.7.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/model.ts CHANGED
@@ -1,8 +1,10 @@
1
- import { Mat4, Quat, Vec3, bezierInterpolate } from "./math"
1
+ import { Mat4, Quat, Vec3 } from "./math"
2
2
  import { Rigidbody, Joint, Physics } from "./physics"
3
3
  import { IKSolverSystem } from "./ik-solver"
4
- import { VMDKeyFrame, VMDLoader, BoneFrame, MorphFrame } from "./vmd-loader"
4
+ import { VMDLoader } from "./vmd-loader"
5
+ import { AnimationData, BoneKeyframe, MorphKeyframe, BoneInterpolation, interpolateControlPoints, rawInterpolationToBoneInterpolation } from "./animation"
5
6
 
7
+ const VMD_FPS = 30
6
8
  const VERTEX_STRIDE = 8
7
9
 
8
10
  export interface Texture {
@@ -181,9 +183,10 @@ export class Model {
181
183
  private tweenTimeMs: number = 0 // Time tracking for tweens (milliseconds)
182
184
 
183
185
  // 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()
186
+ private _hasAnimation: boolean = false
187
+ private _animationData: AnimationData | null = null
188
+ private boneTracks: Map<string, Array<{ boneName: string; frame: number; rotation: Quat; translation: Vec3; interpolation: BoneInterpolation; time: number }>> = new Map()
189
+ private morphTracks: Map<string, Array<{ morphName: string; frame: number; weight: number; time: number }>> = new Map()
187
190
  private animationDuration: number = 0
188
191
  private isPlaying: boolean = false
189
192
  private isPaused: boolean = false
@@ -792,11 +795,89 @@ export class Model {
792
795
  * Load VMD animation file
793
796
  */
794
797
  async loadVmd(vmdUrl: string): Promise<void> {
795
- this.animationData = await VMDLoader.load(vmdUrl)
798
+ const vmdKeyFrames = await VMDLoader.load(vmdUrl)
799
+
800
+ // Convert VMDKeyFrame[] to AnimationData
801
+ const boneTracks: Record<string, BoneKeyframe[]> = {}
802
+ const morphTracks: Record<string, MorphKeyframe[]> = {}
803
+
804
+ for (const keyFrame of vmdKeyFrames) {
805
+ for (const bf of keyFrame.boneFrames) {
806
+ if (!boneTracks[bf.boneName]) boneTracks[bf.boneName] = []
807
+ boneTracks[bf.boneName].push({
808
+ frame: bf.frame,
809
+ rotation: bf.rotation,
810
+ translation: bf.translation,
811
+ interpolation: rawInterpolationToBoneInterpolation(bf.interpolation),
812
+ })
813
+ }
814
+ for (const mf of keyFrame.morphFrames) {
815
+ if (!morphTracks[mf.morphName]) morphTracks[mf.morphName] = []
816
+ morphTracks[mf.morphName].push({
817
+ frame: mf.frame,
818
+ weight: mf.weight,
819
+ })
820
+ }
821
+ }
822
+
823
+ this.loadAnimationData({ boneTracks, morphTracks })
824
+ }
825
+
826
+ /**
827
+ * Load animation from structured keyframe data.
828
+ * This is the primary way to set animation data — loadVmd delegates to this.
829
+ */
830
+ loadAnimationData(data: AnimationData): void {
831
+ this._animationData = data
796
832
  this.resetAllBones()
797
833
  this.resetAllMorphs()
798
- this.processFrames()
799
- // Apply initial pose at time 0
834
+
835
+ this.boneTracks = new Map()
836
+ for (const name in data.boneTracks) {
837
+ const keyframes = data.boneTracks[name]
838
+ const sorted = [...keyframes].sort((a, b) => a.frame - b.frame)
839
+ this.boneTracks.set(
840
+ name,
841
+ sorted.map((kf) => ({
842
+ boneName: name,
843
+ frame: kf.frame,
844
+ rotation: kf.rotation,
845
+ translation: kf.translation,
846
+ interpolation: kf.interpolation,
847
+ time: kf.frame / VMD_FPS,
848
+ }))
849
+ )
850
+ }
851
+
852
+ this.morphTracks = new Map()
853
+ for (const name in data.morphTracks) {
854
+ const keyframes = data.morphTracks[name]
855
+ const sorted = [...keyframes].sort((a, b) => a.frame - b.frame)
856
+ this.morphTracks.set(
857
+ name,
858
+ sorted.map((kf) => ({
859
+ morphName: name,
860
+ frame: kf.frame,
861
+ weight: kf.weight,
862
+ time: kf.frame / VMD_FPS,
863
+ }))
864
+ )
865
+ }
866
+
867
+ this.boneTrackIndices.clear()
868
+ this.morphTrackIndices.clear()
869
+
870
+ // Calculate duration
871
+ let maxTime = 0
872
+ for (const frames of this.boneTracks.values()) {
873
+ if (frames.length > 0) maxTime = Math.max(maxTime, frames[frames.length - 1].time)
874
+ }
875
+ for (const frames of this.morphTracks.values()) {
876
+ if (frames.length > 0) maxTime = Math.max(maxTime, frames[frames.length - 1].time)
877
+ }
878
+ this.animationDuration = maxTime
879
+
880
+ this._hasAnimation = true
800
881
  this.animationTime = 0
801
882
  this.getPoseAtTime(0)
802
883
 
@@ -847,71 +928,8 @@ export class Model {
847
928
  this.physicsEnabled = enabled
848
929
  }
849
930
 
850
- /**
851
- * Process frames into tracks
852
- */
853
- private processFrames(): void {
854
- if (!this.animationData) return
855
-
856
- // Helper to group frames by name and sort by time
857
- const groupFrames = <T>(
858
- items: Array<{ item: T; name: string; time: number }>
859
- ): Map<string, Array<{ item: T; time: number }>> => {
860
- const tracks = new Map<string, Array<{ item: T; time: number }>>()
861
- for (const { item, name, time } of items) {
862
- if (!tracks.has(name)) tracks.set(name, [])
863
- tracks.get(name)!.push({ item, time })
864
- }
865
- for (const keyFrames of tracks.values()) {
866
- keyFrames.sort((a, b) => a.time - b.time)
867
- }
868
- return tracks
869
- }
870
-
871
- // Collect all bone and morph frames
872
- const boneItems: Array<{ item: BoneFrame; name: string; time: number }> = []
873
- const morphItems: Array<{ item: MorphFrame; name: string; time: number }> = []
874
-
875
- for (const keyFrame of this.animationData) {
876
- for (const boneFrame of keyFrame.boneFrames) {
877
- boneItems.push({ item: boneFrame, name: boneFrame.boneName, time: keyFrame.time })
878
- }
879
- for (const morphFrame of keyFrame.morphFrames) {
880
- morphItems.push({ item: morphFrame, name: morphFrame.morphName, time: keyFrame.time })
881
- }
882
- }
883
-
884
- // Transform to expected format
885
- this.boneTracks = new Map()
886
- for (const [name, frames] of groupFrames(boneItems).entries()) {
887
- this.boneTracks.set(
888
- name,
889
- frames.map((f) => ({ boneFrame: f.item, time: f.time }))
890
- )
891
- }
892
-
893
- this.morphTracks = new Map()
894
- for (const [name, frames] of groupFrames(morphItems).entries()) {
895
- this.morphTracks.set(
896
- name,
897
- frames.map((f) => ({ morphFrame: f.item, time: f.time }))
898
- )
899
- }
900
-
901
- // Reset cached indices when tracks change
902
- this.boneTrackIndices.clear()
903
- this.morphTrackIndices.clear()
904
-
905
- // Calculate duration from all tracks
906
- const allTracks = [...this.boneTracks.values(), ...this.morphTracks.values()]
907
- this.animationDuration = allTracks.reduce((max, keyFrames) => {
908
- const lastTime = keyFrames[keyFrames.length - 1]?.time ?? 0
909
- return Math.max(max, lastTime)
910
- }, 0)
911
- }
912
-
913
931
  playAnimation(): void {
914
- if (!this.animationData) return
932
+ if (!this._hasAnimation) return
915
933
 
916
934
  this.isPaused = false
917
935
  this.isPlaying = true
@@ -934,7 +952,7 @@ export class Model {
934
952
  }
935
953
 
936
954
  seekAnimation(time: number): void {
937
- if (!this.animationData) return
955
+ if (!this._hasAnimation) return
938
956
  const clampedTime = Math.max(0, Math.min(time, this.animationDuration))
939
957
  this.animationTime = clampedTime
940
958
  }
@@ -942,6 +960,10 @@ export class Model {
942
960
  /**
943
961
  * Get current animation progress
944
962
  */
963
+ getAnimationData(): AnimationData | null {
964
+ return this._animationData
965
+ }
966
+
945
967
  getAnimationProgress(): { current: number; duration: number; percentage: number } {
946
968
  const duration = this.animationDuration
947
969
  const percentage = duration > 0 ? (this.animationTime / duration) * 100 : 0
@@ -994,9 +1016,7 @@ export class Model {
994
1016
  * Optimized for per-frame performance
995
1017
  */
996
1018
  private getPoseAtTime(time: number): void {
997
- if (!this.animationData) return
998
-
999
- const INV_127 = 1 / 127 // Pre-compute division constant
1019
+ if (!this._hasAnimation) return
1000
1020
 
1001
1021
  // Process bone tracks
1002
1022
  for (const [boneName, keyFrames] of this.boneTracks.entries()) {
@@ -1008,11 +1028,10 @@ export class Model {
1008
1028
 
1009
1029
  if (idx < 0) continue
1010
1030
 
1011
- // Update cache
1012
1031
  this.boneTrackIndices.set(boneName, idx)
1013
1032
 
1014
- const frameA = keyFrames[idx].boneFrame
1015
- const frameB = keyFrames[idx + 1]?.boneFrame
1033
+ const frameA = keyFrames[idx]
1034
+ const frameB = keyFrames[idx + 1]
1016
1035
 
1017
1036
  const boneIdx = this.runtimeSkeleton.nameIndex[boneName]
1018
1037
  if (boneIdx === undefined) continue
@@ -1021,61 +1040,30 @@ export class Model {
1021
1040
  const localTrans = this.runtimeSkeleton.localTranslations[boneIdx]
1022
1041
 
1023
1042
  if (!frameB) {
1024
- // No interpolation needed - direct assignment
1025
- // Use animation frame's rotation for translation conversion to ensure consistency
1026
- // This prevents conflicts when IK later modifies the rotation
1027
1043
  const frameRotation = frameA.rotation
1028
1044
  localRot.set(frameRotation)
1029
- // Convert VMD relative translation to local translation using animation rotation
1030
1045
  const localTranslation = this.convertVMDTranslationToLocal(boneIdx, frameA.translation, frameRotation)
1031
1046
  localTrans.set(localTranslation)
1032
1047
  } else {
1033
- const timeA = keyFrames[idx].time
1034
- const timeB = keyFrames[idx + 1].time
1035
- const timeDelta = timeB - timeA
1036
- const gradient = (clampedTime - timeA) / timeDelta
1048
+ const timeDelta = frameB.time - frameA.time
1049
+ const gradient = (clampedTime - frameA.time) / timeDelta
1037
1050
  const interp = frameB.interpolation
1038
1051
 
1039
- // Pre-normalize interpolation values (avoid division in bezierInterpolate)
1040
- const rotT = bezierInterpolate(
1041
- interp[0] * INV_127,
1042
- interp[1] * INV_127,
1043
- interp[2] * INV_127,
1044
- interp[3] * INV_127,
1045
- gradient
1046
- )
1047
-
1048
- // Use Quat.slerp to interpolate rotation
1052
+ const rotT = interpolateControlPoints(interp.rotation, gradient)
1049
1053
  const rotation = Quat.slerp(frameA.rotation, frameB.rotation, rotT)
1050
1054
 
1051
- // Interpolate VMD translation using bezier for each component
1052
- // Inline getWeight to avoid function call overhead
1053
- const getWeight = (offset: number) =>
1054
- bezierInterpolate(
1055
- interp[offset] * INV_127,
1056
- interp[offset + 8] * INV_127,
1057
- interp[offset + 4] * INV_127,
1058
- interp[offset + 12] * INV_127,
1059
- gradient
1060
- )
1061
-
1062
- const txWeight = getWeight(0)
1063
- const tyWeight = getWeight(16)
1064
- const tzWeight = getWeight(32)
1065
-
1066
- // Interpolate VMD relative translations (relative to bind pose world position)
1055
+ const txWeight = interpolateControlPoints(interp.translationX, gradient)
1056
+ const tyWeight = interpolateControlPoints(interp.translationY, gradient)
1057
+ const tzWeight = interpolateControlPoints(interp.translationZ, gradient)
1058
+
1067
1059
  const interpolatedVMDTranslation = new Vec3(
1068
1060
  frameA.translation.x + (frameB.translation.x - frameA.translation.x) * txWeight,
1069
1061
  frameA.translation.y + (frameB.translation.y - frameA.translation.y) * tyWeight,
1070
1062
  frameA.translation.z + (frameB.translation.z - frameA.translation.z) * tzWeight
1071
1063
  )
1072
1064
 
1073
- // Convert interpolated VMD translation to local translation using animation rotation
1074
- // This ensures translation is computed for the animation rotation, not the runtime rotation
1075
- // that will be modified by IK, preventing conflicts
1076
1065
  const localTranslation = this.convertVMDTranslationToLocal(boneIdx, interpolatedVMDTranslation, rotation)
1077
1066
 
1078
- // Direct property writes to avoid object allocation
1079
1067
  localRot.set(rotation)
1080
1068
  localTrans.set(localTranslation)
1081
1069
  }
@@ -1091,19 +1079,18 @@ export class Model {
1091
1079
 
1092
1080
  if (idx < 0) continue
1093
1081
 
1094
- // Update cache
1095
1082
  this.morphTrackIndices.set(morphName, idx)
1096
1083
 
1097
- const frameA = keyFrames[idx].morphFrame
1098
- const frameB = keyFrames[idx + 1]?.morphFrame
1084
+ const frameA = keyFrames[idx]
1085
+ const frameB = keyFrames[idx + 1]
1099
1086
 
1100
1087
  const morphIdx = this.runtimeMorph.nameIndex[morphName]
1101
1088
  if (morphIdx === undefined) continue
1102
1089
 
1103
1090
  const weight = frameB
1104
1091
  ? frameA.weight +
1105
- (frameB.weight - frameA.weight) *
1106
- ((clampedTime - keyFrames[idx].time) / (keyFrames[idx + 1].time - keyFrames[idx].time))
1092
+ (frameB.weight - frameA.weight) *
1093
+ ((clampedTime - keyFrames[idx].time) / (keyFrames[idx + 1].time - keyFrames[idx].time))
1107
1094
  : frameA.weight
1108
1095
 
1109
1096
  this.runtimeMorph.weights[morphIdx] = weight
@@ -1125,7 +1112,7 @@ export class Model {
1125
1112
  const tweensChangedMorphs = this.updateTweens()
1126
1113
 
1127
1114
  // Apply animation if playing or paused (always apply pose if animation data exists and we have a time set)
1128
- if (this.animationData) {
1115
+ if (this._hasAnimation) {
1129
1116
  if (this.isPlaying && !this.isPaused) {
1130
1117
  this.animationTime += deltaTime
1131
1118
 
@@ -1177,7 +1164,7 @@ export class Model {
1177
1164
  // Recompute ALL world matrices before each solver starts
1178
1165
  // This ensures each solver sees the effects of previous solvers on localRotations
1179
1166
  this.computeWorldMatrices()
1180
-
1167
+
1181
1168
  // Clear computed set for this solver's pass
1182
1169
  this.ikComputedSet.clear()
1183
1170
 
@@ -1242,9 +1229,9 @@ export class Model {
1242
1229
 
1243
1230
  // Handle append transformations (same logic as computeWorldMatrices)
1244
1231
  const appendParentIdx = b.appendParentIndex
1245
- const hasAppend = b.appendRotate &&
1246
- appendParentIdx !== undefined &&
1247
- appendParentIdx >= 0 &&
1232
+ const hasAppend = b.appendRotate &&
1233
+ appendParentIdx !== undefined &&
1234
+ appendParentIdx >= 0 &&
1248
1235
  appendParentIdx < bones.length
1249
1236
 
1250
1237
  if (hasAppend) {
@@ -1261,7 +1248,7 @@ export class Model {
1261
1248
  // Compute append parent's world matrix for dependency order, but use base rotation for append
1262
1249
  this.computeSingleBoneWorldMatrix(appendParentIdx, applyIK)
1263
1250
  }
1264
-
1251
+
1265
1252
  // Use append parent's base local rotation only (IK rotations are applied after solving)
1266
1253
  let appendRot = localRot[appendParentIdx]
1267
1254
 
@@ -1269,7 +1256,7 @@ export class Model {
1269
1256
  const aw = appendRot.w
1270
1257
  const absRatio = ratio < 0 ? -ratio : ratio
1271
1258
  if (ratio < 0) { ax = -ax; ay = -ay; az = -az }
1272
-
1259
+
1273
1260
  const appendQuat = new Quat(ax, ay, az, aw)
1274
1261
  const result = Quat.slerp(Quat.identity(), appendQuat, absRatio)
1275
1262
  rotateM = Mat4.fromQuat(result.x, result.y, result.z, result.w).multiply(rotateM)
@@ -1,49 +0,0 @@
1
- import { Quat, Vec3, Mat4 } from "./math";
2
- import type { Bone } from "./model";
3
- /**
4
- * Runtime bone state - encapsulates bone pose and world matrix
5
- * Similar to babylon-mmd's IMmdRuntimeBone approach
6
- */
7
- export declare class RuntimeBone {
8
- localRotation: Quat;
9
- localTranslation: Vec3;
10
- worldMatrix: Mat4;
11
- readonly bone: Bone;
12
- readonly index: number;
13
- ikRotation?: Quat;
14
- constructor(bone: Bone, index: number);
15
- /**
16
- * Set local rotation
17
- */
18
- setRotation(rotation: Quat): void;
19
- /**
20
- * Set local translation
21
- */
22
- setTranslation(translation: Vec3): void;
23
- /**
24
- * Get world position from world matrix
25
- */
26
- getWorldPosition(): Vec3;
27
- /**
28
- * Update world matrix based on local rotation, translation, and parent's world matrix
29
- * Handles append rotations and translations
30
- *
31
- * @param parentWorldMatrix Parent's world matrix (null if root bone)
32
- * @param allBones Array of all runtime bones (for append parent lookup)
33
- * @param applyIK Whether to apply IK rotation (default: true). Set to false when computing initial world matrices before IK solving.
34
- */
35
- updateWorldMatrix(parentWorldMatrix: Mat4 | null, allBones: RuntimeBone[], applyIK?: boolean): void;
36
- /**
37
- * Reset IK rotation to identity
38
- */
39
- resetIKRotation(): void;
40
- /**
41
- * Set IK rotation delta (accumulated during IK solving)
42
- */
43
- setIKRotation(ikRotation: Quat): void;
44
- /**
45
- * Apply IK rotation to local rotation and clear IK rotation
46
- */
47
- applyIKRotation(): void;
48
- }
49
- //# sourceMappingURL=runtime-bone.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"runtime-bone.d.ts","sourceRoot":"","sources":["../src/runtime-bone.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAEnC;;;GAGG;AACH,qBAAa,WAAW;IACf,aAAa,EAAE,IAAI,CAAA;IACnB,gBAAgB,EAAE,IAAI,CAAA;IACtB,WAAW,EAAE,IAAI,CAAA;IACxB,SAAgB,IAAI,EAAE,IAAI,CAAA;IAC1B,SAAgB,KAAK,EAAE,MAAM,CAAA;IAGtB,UAAU,CAAC,EAAE,IAAI,CAAA;gBAEZ,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM;IAQrC;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,IAAI,GAAG,IAAI;IAIjC;;OAEG;IACH,cAAc,CAAC,WAAW,EAAE,IAAI,GAAG,IAAI;IAIvC;;OAEG;IACH,gBAAgB,IAAI,IAAI;IAKxB;;;;;;;OAOG;IACH,iBAAiB,CAAC,iBAAiB,EAAE,IAAI,GAAG,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,GAAE,OAAc,GAAG,IAAI;IA2EzG;;OAEG;IACH,eAAe,IAAI,IAAI;IAIvB;;OAEG;IACH,aAAa,CAAC,UAAU,EAAE,IAAI,GAAG,IAAI;IAIrC;;OAEG;IACH,eAAe,IAAI,IAAI;CAMxB"}
@@ -1,121 +0,0 @@
1
- import { Quat, Vec3, Mat4 } from "./math";
2
- /**
3
- * Runtime bone state - encapsulates bone pose and world matrix
4
- * Similar to babylon-mmd's IMmdRuntimeBone approach
5
- */
6
- export class RuntimeBone {
7
- constructor(bone, index) {
8
- this.bone = bone;
9
- this.index = index;
10
- this.localRotation = Quat.identity();
11
- this.localTranslation = new Vec3(0, 0, 0);
12
- this.worldMatrix = Mat4.identity();
13
- }
14
- /**
15
- * Set local rotation
16
- */
17
- setRotation(rotation) {
18
- this.localRotation.set(rotation);
19
- }
20
- /**
21
- * Set local translation
22
- */
23
- setTranslation(translation) {
24
- this.localTranslation.set(translation);
25
- }
26
- /**
27
- * Get world position from world matrix
28
- */
29
- getWorldPosition() {
30
- const m = this.worldMatrix.values;
31
- return new Vec3(m[12], m[13], m[14]);
32
- }
33
- /**
34
- * Update world matrix based on local rotation, translation, and parent's world matrix
35
- * Handles append rotations and translations
36
- *
37
- * @param parentWorldMatrix Parent's world matrix (null if root bone)
38
- * @param allBones Array of all runtime bones (for append parent lookup)
39
- * @param applyIK Whether to apply IK rotation (default: true). Set to false when computing initial world matrices before IK solving.
40
- */
41
- updateWorldMatrix(parentWorldMatrix, allBones, applyIK = true) {
42
- // Start with local rotation
43
- let finalRot = this.localRotation;
44
- // Apply IK rotation if present and applyIK is true
45
- if (applyIK && this.ikRotation) {
46
- finalRot = this.ikRotation.multiply(finalRot).normalize();
47
- }
48
- // Build rotation matrix
49
- let rotateM = Mat4.fromQuat(finalRot.x, finalRot.y, finalRot.z, finalRot.w);
50
- let addLocalTx = 0, addLocalTy = 0, addLocalTz = 0;
51
- // Handle append rotation and translation
52
- const appendParentIdx = this.bone.appendParentIndex;
53
- const hasAppend = this.bone.appendRotate && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < allBones.length;
54
- if (hasAppend) {
55
- const ratio = this.bone.appendRatio === undefined ? 1 : Math.max(-1, Math.min(1, this.bone.appendRatio));
56
- const hasRatio = Math.abs(ratio) > 1e-6;
57
- if (hasRatio) {
58
- if (this.bone.appendRotate) {
59
- const appendParent = allBones[appendParentIdx];
60
- const appendRot = appendParent.localRotation;
61
- let ax = appendRot.x;
62
- let ay = appendRot.y;
63
- let az = appendRot.z;
64
- const aw = appendRot.w;
65
- const absRatio = ratio < 0 ? -ratio : ratio;
66
- if (ratio < 0) {
67
- ax = -ax;
68
- ay = -ay;
69
- az = -az;
70
- }
71
- const appendQuat = new Quat(ax, ay, az, aw);
72
- const result = Quat.slerp(Quat.identity(), appendQuat, absRatio);
73
- rotateM = Mat4.fromQuat(result.x, result.y, result.z, result.w).multiply(rotateM);
74
- }
75
- if (this.bone.appendMove) {
76
- const appendParent = allBones[appendParentIdx];
77
- const appendTrans = appendParent.localTranslation;
78
- const appendRatio = this.bone.appendRatio ?? 1;
79
- addLocalTx = appendTrans.x * appendRatio;
80
- addLocalTy = appendTrans.y * appendRatio;
81
- addLocalTz = appendTrans.z * appendRatio;
82
- }
83
- }
84
- }
85
- // Build local matrix: bind translation, then rotation, then local translation + append translation
86
- const localTx = this.localTranslation.x + addLocalTx;
87
- const localTy = this.localTranslation.y + addLocalTy;
88
- const localTz = this.localTranslation.z + addLocalTz;
89
- const bindMat = Mat4.identity().translateInPlace(this.bone.bindTranslation[0], this.bone.bindTranslation[1], this.bone.bindTranslation[2]);
90
- const transMat = Mat4.identity().translateInPlace(localTx, localTy, localTz);
91
- const localM = bindMat.multiply(rotateM).multiply(transMat);
92
- // Compute world matrix
93
- if (parentWorldMatrix) {
94
- this.worldMatrix = parentWorldMatrix.multiply(localM);
95
- }
96
- else {
97
- this.worldMatrix = localM;
98
- }
99
- }
100
- /**
101
- * Reset IK rotation to identity
102
- */
103
- resetIKRotation() {
104
- this.ikRotation = undefined;
105
- }
106
- /**
107
- * Set IK rotation delta (accumulated during IK solving)
108
- */
109
- setIKRotation(ikRotation) {
110
- this.ikRotation = ikRotation;
111
- }
112
- /**
113
- * Apply IK rotation to local rotation and clear IK rotation
114
- */
115
- applyIKRotation() {
116
- if (this.ikRotation) {
117
- this.localRotation = this.ikRotation.multiply(this.localRotation).normalize();
118
- this.ikRotation = undefined;
119
- }
120
- }
121
- }