Assimp .NET模型动画异常:如何补全默认变换值?
解决方案
问题出在Blockbench导出的动画不会为未修改的变换属性(位置/旋转/缩放)生成关键帧,也不会给无动画的节点创建动画通道,而你的代码默认使用单位矩阵处理这些情况,导致绑定姿势的默认值丢失。以下是针对性修改:
1. 添加矩阵分解方法
用于从节点初始变换中提取平移、旋转、缩放分量:
private void DecomposeMatrix(AMatrix4x4 matrix, out Vector3D translation, out AQuaternion rotation, out Vector3D scale) { matrix.Decompose(out scale, out rotation, out translation); }
2. 修改UpdateChannel方法
处理无动画通道、无关键帧的情况,保留节点初始变换:
private void UpdateChannel(Node node, ModelAnimation animation, int frame, AMatrix4x4 parentTransform) { // 从节点初始变换中提取默认分量 DecomposeMatrix(node.Transform, out Vector3D defaultTranslation, out AQuaternion defaultRotation, out Vector3D defaultScale); // 初始化变换为节点默认值 AMatrix4x4 scale = AMatrix4x4.FromScaling(defaultScale); AMatrix4x4 rotation = defaultRotation.GetMatrix(); AMatrix4x4 translation = AMatrix4x4.FromTranslation(defaultTranslation); if (this.GetChannel(node, animation, out NodeAnimationChannel? channel)) { // 仅当存在对应关键帧时,覆盖默认变换 if (channel.ScalingKeyCount > 0) { scale = this.InterpolateScale(channel!, animation, frame); } if (channel.RotationKeyCount > 0) { rotation = this.InterpolateRotation(channel!, animation, frame); } if (channel.PositionKeyCount > 0) { translation = this.InterpolateTranslation(channel!, animation, frame); } } AMatrix4x4 nodeTransformation = scale * rotation * translation; // 原有骨骼变换计算逻辑保持不变 foreach (uint boneId in this._bonesByName.Keys) { Bone bone = this._bonesByName[boneId]; if (node.Name == bone.Name) { AMatrix4x4 rootInverseTransform = this._rootNode.Transform; rootInverseTransform.Inverse(); AMatrix4x4 transformation = bone.OffsetMatrix * nodeTransformation * parentTransform * rootInverseTransform; this._boneTransformations[boneId] = Matrix4x4.Transpose(ModelConversion.FromAMatrix4X4(transformation)); } } foreach (Node childNode in node.Children) { this.UpdateChannel(childNode, animation, frame, nodeTransformation * parentTransform); } }
3. 修复插值方法的健壮性
为每个插值方法添加关键帧数量为0的处理:
修改InterpolateTranslation
private AMatrix4x4 InterpolateTranslation(NodeAnimationChannel channel, ModelAnimation animation, int frame) { // 无关键帧时返回默认平移(原点) if (channel.PositionKeyCount == 0) { return AMatrix4x4.FromTranslation(Vector3D.Zero); } double frameTime = frame / 60.0F * animation.TicksPerSecond; Vector3D position; if (channel.PositionKeyCount == 1) { position = channel.PositionKeys[0].Value; } else { uint frameIndex = 0; for (uint i = 0; i < channel.PositionKeyCount - 1; i++) { if (frameTime < channel.PositionKeys[(int) (i + 1)].Time) { frameIndex = i; break; } } VectorKey currentFrame = channel.PositionKeys[(int) frameIndex]; VectorKey nextFrame = channel.PositionKeys[(int) ((frameIndex + 1) % channel.PositionKeyCount)]; double delta = (frameTime - currentFrame.Time) / (nextFrame.Time - currentFrame.Time); Vector3D start = currentFrame.Value; Vector3D end = nextFrame.Value; position = start + (float) delta * (end - start); } return AMatrix4x4.FromTranslation(position); }
修改InterpolateRotation
private AMatrix4x4 InterpolateRotation(NodeAnimationChannel channel, ModelAnimation animation, int frame) { // 无关键帧时返回默认旋转(单位四元数) if (channel.RotationKeyCount == 0) { return AQuaternion.Identity.GetMatrix(); } double frameTime = frame / 60.0F * animation.TicksPerSecond; AQuaternion rotation; if (channel.RotationKeyCount == 1) { rotation = channel.RotationKeys[0].Value; } else { uint frameIndex = 0; for (uint i = 0; i < channel.RotationKeyCount - 1; i++) { if (frameTime < channel.RotationKeys[(int) (i + 1)].Time) { frameIndex = i; break; } } QuaternionKey currentFrame = channel.RotationKeys[(int) frameIndex]; QuaternionKey nextFrame = channel.RotationKeys[(int) ((frameIndex + 1) % channel.RotationKeyCount)]; double delta = (frameTime - currentFrame.Time) / (nextFrame.Time - currentFrame.Time); AQuaternion start = currentFrame.Value; AQuaternion end = nextFrame.Value; rotation = AQuaternion.Slerp(start, end, (float) delta); rotation.Normalize(); } return rotation.GetMatrix(); }
修改InterpolateScale
private AMatrix4x4 InterpolateScale(NodeAnimationChannel channel, ModelAnimation animation, int frame) { // 无关键帧时返回默认缩放(1,1,1) if (channel.ScalingKeyCount == 0) { return AMatrix4x4.FromScaling(Vector3D.One); } double frameTime = frame / 60.0F * animation.TicksPerSecond; Vector3D scale; if (channel.ScalingKeyCount == 1) { scale = channel.ScalingKeys[0].Value; } else { uint frameIndex = 0; for (uint i = 0; i < channel.ScalingKeyCount - 1; i++) { if (frameTime < channel.ScalingKeys[(int) (i + 1)].Time) { frameIndex = i; break; } } VectorKey currentFrame = channel.ScalingKeys[(int)frameIndex]; VectorKey nextFrame = channel.ScalingKeys[(int)((frameIndex + 1) % channel.ScalingKeyCount)]; double delta = (frameTime - currentFrame.Time) / (nextFrame.Time - currentFrame.Time); Vector3D start = currentFrame.Value; Vector3D end = nextFrame.Value; scale = start + (float) delta * (end - start); } return AMatrix4x4.FromScaling(scale); }
4. 初始化骨骼变换数组
避免帧间变换残留,在SetupBoneInfos的帧循环中添加初始化逻辑:
private Dictionary<int, BoneInfo[]> SetupBoneInfos(ModelAnimation animation) { Dictionary<int, BoneInfo[]> bones = new Dictionary<int, BoneInfo[]>(); for (int i = 0; i < animation.FrameCount; i++) { List<BoneInfo> boneInfos = new List<BoneInfo>(); // 初始化骨骼变换为单位矩阵,避免上一帧数据残留 for (uint boneId = 0; boneId < this._boneTransformations.Length; boneId++) { this._boneTransformations[boneId] = Matrix4x4.Identity; } this.UpdateChannel(this._rootNode, animation, i, AMatrix4x4.Identity); for (uint boneId = 0; boneId < this._boneTransformations.Length; boneId++) { BoneInfo boneInfo = new BoneInfo(this._bonesByName[boneId].Name, boneId, this._boneTransformations[boneId]); boneInfos.Add(boneInfo); } bones.Add(i, boneInfos.ToArray()); } return bones; }
这些修改会让代码在处理Blockbench导出的模型时,自动补全未定义关键帧的变换默认值,和Blender导出模型的行为保持一致。
内容的提问来源于stack exchange,提问作者MrScautHD
相关产品推荐
相关产品推荐

