You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 15:33:08