reze-engine 0.6.7 → 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/README.md +29 -20
- package/dist/animation.d.ts +41 -0
- package/dist/animation.d.ts.map +1 -0
- package/dist/animation.js +62 -0
- package/dist/engine.d.ts +3 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +6 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/math.d.ts +0 -10
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +0 -36
- package/dist/model.d.ts +9 -12
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +92 -130
- package/package.json +1 -1
- package/src/animation.ts +109 -0
- package/src/engine.ts +9 -7
- package/src/index.ts +2 -0
- package/src/math.ts +0 -42
- package/src/model.ts +116 -180
package/src/model.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { Mat4, Quat, Vec3
|
|
1
|
+
import { Mat4, Quat, Vec3 } from "./math"
|
|
2
2
|
import { Rigidbody, Joint, Physics } from "./physics"
|
|
3
3
|
import { IKSolverSystem } from "./ik-solver"
|
|
4
|
-
import {
|
|
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
|
|
185
|
-
private
|
|
186
|
-
private
|
|
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
|
|
@@ -723,57 +726,6 @@ export class Model {
|
|
|
723
726
|
this.applyMorphs()
|
|
724
727
|
}
|
|
725
728
|
|
|
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
729
|
private applyMorphs(): void {
|
|
778
730
|
// Reset vertex data to base positions
|
|
779
731
|
this.vertexData.set(this.baseVertexData)
|
|
@@ -843,11 +795,89 @@ export class Model {
|
|
|
843
795
|
* Load VMD animation file
|
|
844
796
|
*/
|
|
845
797
|
async loadVmd(vmdUrl: string): Promise<void> {
|
|
846
|
-
|
|
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
|
|
847
832
|
this.resetAllBones()
|
|
848
833
|
this.resetAllMorphs()
|
|
849
|
-
|
|
850
|
-
|
|
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
|
|
851
881
|
this.animationTime = 0
|
|
852
882
|
this.getPoseAtTime(0)
|
|
853
883
|
|
|
@@ -898,71 +928,8 @@ export class Model {
|
|
|
898
928
|
this.physicsEnabled = enabled
|
|
899
929
|
}
|
|
900
930
|
|
|
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
931
|
playAnimation(): void {
|
|
965
|
-
if (!this.
|
|
932
|
+
if (!this._hasAnimation) return
|
|
966
933
|
|
|
967
934
|
this.isPaused = false
|
|
968
935
|
this.isPlaying = true
|
|
@@ -985,7 +952,7 @@ export class Model {
|
|
|
985
952
|
}
|
|
986
953
|
|
|
987
954
|
seekAnimation(time: number): void {
|
|
988
|
-
if (!this.
|
|
955
|
+
if (!this._hasAnimation) return
|
|
989
956
|
const clampedTime = Math.max(0, Math.min(time, this.animationDuration))
|
|
990
957
|
this.animationTime = clampedTime
|
|
991
958
|
}
|
|
@@ -993,6 +960,10 @@ export class Model {
|
|
|
993
960
|
/**
|
|
994
961
|
* Get current animation progress
|
|
995
962
|
*/
|
|
963
|
+
getAnimationData(): AnimationData | null {
|
|
964
|
+
return this._animationData
|
|
965
|
+
}
|
|
966
|
+
|
|
996
967
|
getAnimationProgress(): { current: number; duration: number; percentage: number } {
|
|
997
968
|
const duration = this.animationDuration
|
|
998
969
|
const percentage = duration > 0 ? (this.animationTime / duration) * 100 : 0
|
|
@@ -1045,9 +1016,7 @@ export class Model {
|
|
|
1045
1016
|
* Optimized for per-frame performance
|
|
1046
1017
|
*/
|
|
1047
1018
|
private getPoseAtTime(time: number): void {
|
|
1048
|
-
if (!this.
|
|
1049
|
-
|
|
1050
|
-
const INV_127 = 1 / 127 // Pre-compute division constant
|
|
1019
|
+
if (!this._hasAnimation) return
|
|
1051
1020
|
|
|
1052
1021
|
// Process bone tracks
|
|
1053
1022
|
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
@@ -1059,11 +1028,10 @@ export class Model {
|
|
|
1059
1028
|
|
|
1060
1029
|
if (idx < 0) continue
|
|
1061
1030
|
|
|
1062
|
-
// Update cache
|
|
1063
1031
|
this.boneTrackIndices.set(boneName, idx)
|
|
1064
1032
|
|
|
1065
|
-
const frameA = keyFrames[idx]
|
|
1066
|
-
const frameB = keyFrames[idx + 1]
|
|
1033
|
+
const frameA = keyFrames[idx]
|
|
1034
|
+
const frameB = keyFrames[idx + 1]
|
|
1067
1035
|
|
|
1068
1036
|
const boneIdx = this.runtimeSkeleton.nameIndex[boneName]
|
|
1069
1037
|
if (boneIdx === undefined) continue
|
|
@@ -1072,61 +1040,30 @@ export class Model {
|
|
|
1072
1040
|
const localTrans = this.runtimeSkeleton.localTranslations[boneIdx]
|
|
1073
1041
|
|
|
1074
1042
|
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
1043
|
const frameRotation = frameA.rotation
|
|
1079
1044
|
localRot.set(frameRotation)
|
|
1080
|
-
// Convert VMD relative translation to local translation using animation rotation
|
|
1081
1045
|
const localTranslation = this.convertVMDTranslationToLocal(boneIdx, frameA.translation, frameRotation)
|
|
1082
1046
|
localTrans.set(localTranslation)
|
|
1083
1047
|
} else {
|
|
1084
|
-
const
|
|
1085
|
-
const
|
|
1086
|
-
const timeDelta = timeB - timeA
|
|
1087
|
-
const gradient = (clampedTime - timeA) / timeDelta
|
|
1048
|
+
const timeDelta = frameB.time - frameA.time
|
|
1049
|
+
const gradient = (clampedTime - frameA.time) / timeDelta
|
|
1088
1050
|
const interp = frameB.interpolation
|
|
1089
1051
|
|
|
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
|
|
1052
|
+
const rotT = interpolateControlPoints(interp.rotation, gradient)
|
|
1100
1053
|
const rotation = Quat.slerp(frameA.rotation, frameB.rotation, rotT)
|
|
1101
1054
|
|
|
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)
|
|
1055
|
+
const txWeight = interpolateControlPoints(interp.translationX, gradient)
|
|
1056
|
+
const tyWeight = interpolateControlPoints(interp.translationY, gradient)
|
|
1057
|
+
const tzWeight = interpolateControlPoints(interp.translationZ, gradient)
|
|
1058
|
+
|
|
1118
1059
|
const interpolatedVMDTranslation = new Vec3(
|
|
1119
1060
|
frameA.translation.x + (frameB.translation.x - frameA.translation.x) * txWeight,
|
|
1120
1061
|
frameA.translation.y + (frameB.translation.y - frameA.translation.y) * tyWeight,
|
|
1121
1062
|
frameA.translation.z + (frameB.translation.z - frameA.translation.z) * tzWeight
|
|
1122
1063
|
)
|
|
1123
1064
|
|
|
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
1065
|
const localTranslation = this.convertVMDTranslationToLocal(boneIdx, interpolatedVMDTranslation, rotation)
|
|
1128
1066
|
|
|
1129
|
-
// Direct property writes to avoid object allocation
|
|
1130
1067
|
localRot.set(rotation)
|
|
1131
1068
|
localTrans.set(localTranslation)
|
|
1132
1069
|
}
|
|
@@ -1142,19 +1079,18 @@ export class Model {
|
|
|
1142
1079
|
|
|
1143
1080
|
if (idx < 0) continue
|
|
1144
1081
|
|
|
1145
|
-
// Update cache
|
|
1146
1082
|
this.morphTrackIndices.set(morphName, idx)
|
|
1147
1083
|
|
|
1148
|
-
const frameA = keyFrames[idx]
|
|
1149
|
-
const frameB = keyFrames[idx + 1]
|
|
1084
|
+
const frameA = keyFrames[idx]
|
|
1085
|
+
const frameB = keyFrames[idx + 1]
|
|
1150
1086
|
|
|
1151
1087
|
const morphIdx = this.runtimeMorph.nameIndex[morphName]
|
|
1152
1088
|
if (morphIdx === undefined) continue
|
|
1153
1089
|
|
|
1154
1090
|
const weight = frameB
|
|
1155
1091
|
? frameA.weight +
|
|
1156
|
-
|
|
1157
|
-
|
|
1092
|
+
(frameB.weight - frameA.weight) *
|
|
1093
|
+
((clampedTime - keyFrames[idx].time) / (keyFrames[idx + 1].time - keyFrames[idx].time))
|
|
1158
1094
|
: frameA.weight
|
|
1159
1095
|
|
|
1160
1096
|
this.runtimeMorph.weights[morphIdx] = weight
|
|
@@ -1176,7 +1112,7 @@ export class Model {
|
|
|
1176
1112
|
const tweensChangedMorphs = this.updateTweens()
|
|
1177
1113
|
|
|
1178
1114
|
// Apply animation if playing or paused (always apply pose if animation data exists and we have a time set)
|
|
1179
|
-
if (this.
|
|
1115
|
+
if (this._hasAnimation) {
|
|
1180
1116
|
if (this.isPlaying && !this.isPaused) {
|
|
1181
1117
|
this.animationTime += deltaTime
|
|
1182
1118
|
|
|
@@ -1228,7 +1164,7 @@ export class Model {
|
|
|
1228
1164
|
// Recompute ALL world matrices before each solver starts
|
|
1229
1165
|
// This ensures each solver sees the effects of previous solvers on localRotations
|
|
1230
1166
|
this.computeWorldMatrices()
|
|
1231
|
-
|
|
1167
|
+
|
|
1232
1168
|
// Clear computed set for this solver's pass
|
|
1233
1169
|
this.ikComputedSet.clear()
|
|
1234
1170
|
|
|
@@ -1293,9 +1229,9 @@ export class Model {
|
|
|
1293
1229
|
|
|
1294
1230
|
// Handle append transformations (same logic as computeWorldMatrices)
|
|
1295
1231
|
const appendParentIdx = b.appendParentIndex
|
|
1296
|
-
const hasAppend = b.appendRotate &&
|
|
1297
|
-
appendParentIdx !== undefined &&
|
|
1298
|
-
appendParentIdx >= 0 &&
|
|
1232
|
+
const hasAppend = b.appendRotate &&
|
|
1233
|
+
appendParentIdx !== undefined &&
|
|
1234
|
+
appendParentIdx >= 0 &&
|
|
1299
1235
|
appendParentIdx < bones.length
|
|
1300
1236
|
|
|
1301
1237
|
if (hasAppend) {
|
|
@@ -1312,7 +1248,7 @@ export class Model {
|
|
|
1312
1248
|
// Compute append parent's world matrix for dependency order, but use base rotation for append
|
|
1313
1249
|
this.computeSingleBoneWorldMatrix(appendParentIdx, applyIK)
|
|
1314
1250
|
}
|
|
1315
|
-
|
|
1251
|
+
|
|
1316
1252
|
// Use append parent's base local rotation only (IK rotations are applied after solving)
|
|
1317
1253
|
let appendRot = localRot[appendParentIdx]
|
|
1318
1254
|
|
|
@@ -1320,7 +1256,7 @@ export class Model {
|
|
|
1320
1256
|
const aw = appendRot.w
|
|
1321
1257
|
const absRatio = ratio < 0 ? -ratio : ratio
|
|
1322
1258
|
if (ratio < 0) { ax = -ax; ay = -ay; az = -az }
|
|
1323
|
-
|
|
1259
|
+
|
|
1324
1260
|
const appendQuat = new Quat(ax, ay, az, aw)
|
|
1325
1261
|
const result = Quat.slerp(Quat.identity(), appendQuat, absRatio)
|
|
1326
1262
|
rotateM = Mat4.fromQuat(result.x, result.y, result.z, result.w).multiply(rotateM)
|