3

Recently I have been trying to implement assimp into Frank Luna's basic dx12 engine as part of my learning. I have had real trouble getting the matrix mathematics working correctly, and have hit a bit of a dead end.

This is the main function I am using to get the transformations from the skeleton and animations to pass across to the GPU.

First things first, there is a github link here for anyone who just prefers to look straight at the source: repo

void Skeleton::GetTransforms(float timePos, aiNode* node, aiAnimation* animation, aiMatrix4x4& parentTransform, const aiMatrix4x4& globalInverseTransform, std::vector<DirectX::XMFLOAT4X4>& transforms)
{
    std::string nodeName(node->mName.data);
    aiMatrix4x4 nodeTransform = node->mTransformation;
    const aiNodeAnim* nodeAnim = FindNodeAnim(animation, nodeName);

    if (nodeAnim)
    {
        aiVector3D scaling;
        aiQuaternion rotation;
        aiVector3D translate;

        CalcInterpolatedScaling(scaling, timePos, nodeAnim);
        CalcInterpolatedRotation(rotation, timePos, nodeAnim);
        CalcInterpolatedPosition(translate, timePos, nodeAnim);

        nodeTransform = CreateAffineMatrix(scaling, rotation, translate);
    }

    aiMatrix4x4 globalTransform = parentTransform * nodeTransform;

    if (this->bones.find(nodeName) != this->bones.end())
    {
        int boneIndex = this->bones[nodeName].index;
        aiMatrix4x4 finalTransform = globalInverseTransform * globalTransform * this->bones[nodeName].offsetMatrix;
        aiMatConvert(finalTransform, transforms[boneIndex]);
    }

    for (UINT i = 0u; i < node->mNumChildren; i++)
    {
        GetTransforms(timePos, node->mChildren[i], animation, globalTransform, globalInverseTransform, transforms);
    }
}

This code aligns closely with what I have found on several other threads:

Unfortunatley this is no producing the correct result, currently this is producing a weird result as you can see below:

Broken Result

If I disable animation by commenting out nodeTransform = CreateAffineMatrix(scaling, rotation, translate);

The result is as I would expect, the character sits at TPose:

Result without animation transforms

I have not been able to figure out what about my code base is not working correctly, these are some of the methods I have already tried to resolve the problem:

  • Transposing the result
  • Transposing the offsetMatrix
  • Using .glb and .gltf instead of fbx
  • SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false)
  • Using a simple example, ie a box with 3 or 4 joints

One observation that could be helpful, I have noticed that this problem does not occur for joints that are oriented to world space. So essentially they don't have a unique orientation. This leads me to think that something about the globalInverseTransform * globalTransform * this->bones[nodeName].offsetMatrix may not be working correctly.

I am sourcing the globalInverseTransform with this function:

struct Animation
{
    Skeleton* skeleton;
    aiNode* rootNode;
    aiAnimation* animation;
    std::vector<DirectX::XMFLOAT4X4> transforms;
    float TimePos = 0.0f;
    bool Loop = true;
    float Speed = 1.0f;

    void UpdateSkinnedAnimation(float dt)
    {
        dt *= Speed;
        TimePos += dt;

        if (Loop)
        {
            float duration = animation->mDuration;
            if (TimePos > duration)
            {
                TimePos = 0.0;
            }
        }
        
        skeleton->GetTransforms(TimePos, rootNode, animation, aiMatrix4x4(), rootNode->mTransformation.Inverse(), transforms);

    }
};

The rootNode pointer is set during the mesh loading stage, and data is stored as heap memory so it does not loose its scope at runtime.

The rest of the skeleton related functions are list below,

Thanks a bunch ! This really has had me stumped for a good week now, not exactly sure which way to look next, some help would be greatly appreciated! :)

float clamp(const float& f)
{
    return (f < 0.0f) ? 0.0f : ((f > 1.0f) ? 1.0f : f);
}

void aiMatConvert(const aiMatrix4x4& aiMatrix, DirectX::XMFLOAT4X4& dXMatrix)
{
    DirectX::XMMATRIX meshToBoneTransform = DirectX::XMMATRIX(aiMatrix.a1, aiMatrix.a2, 
                                            aiMatrix.a3, aiMatrix.a4, aiMatrix.b1, aiMatrix.b2, 
                                            aiMatrix.b3, aiMatrix.b4, aiMatrix.c1, aiMatrix.c2, 
                                            aiMatrix.c3, aiMatrix.c4, aiMatrix.d1, aiMatrix.d2, 
                                            aiMatrix.d3, aiMatrix.d4);

    DirectX::XMStoreFloat4x4(&dXMatrix, meshToBoneTransform);
}

const aiNodeAnim* Skeleton::FindNodeAnim(const aiAnimation* animation, const std::string& nodeName)
{
    for (unsigned int i = 0; i < animation->mNumChannels; ++i)
    {
        const aiNodeAnim* nodeAnim = animation->mChannels[i];
        if (std::string(nodeAnim->mNodeName.data) == nodeName)
        {
            return nodeAnim;
        }
    }

    return nullptr;
}

int Skeleton::FindPositionKey(float AnimationTime, const aiNodeAnim* pNodeAnim)
{
    for (int i = 0; i < pNodeAnim->mNumPositionKeys - 1; i++) {
        if (AnimationTime < (float)pNodeAnim->mPositionKeys[i + 1].mTime) {
            return i;
        }
    }
    return 0;
}


int Skeleton::FindRotationKey(float AnimationTime, const aiNodeAnim* pNodeAnim)
{
    for (int i = 0; i < pNodeAnim->mNumRotationKeys - 1; i++) {
        if (AnimationTime < (float)pNodeAnim->mRotationKeys[i + 1].mTime) {
            return i;
        }
    }
    return 0;
}

int Skeleton::FindScalingKey(float AnimationTime, const aiNodeAnim* pNodeAnim)
{
    for (int i = 0; i < pNodeAnim->mNumScalingKeys - 1; i++) {
        if (AnimationTime < (float)pNodeAnim->mScalingKeys[i + 1].mTime) {
            return i;
        }
    }
    return 0;
}

void Skeleton::CalcInterpolatedPosition(aiVector3D& Out, float AnimationTime, const aiNodeAnim* pNodeAnim)
{
    if (pNodeAnim->mNumPositionKeys == 1) {
        Out = pNodeAnim->mPositionKeys[0].mValue;
        return;
    }

    int PositionIndex = FindPositionKey(AnimationTime, pNodeAnim);
    int NextPositionIndex = (PositionIndex + 1);
    
    float DeltaTime = (float)(pNodeAnim->mPositionKeys[NextPositionIndex].mTime - pNodeAnim->mPositionKeys[PositionIndex].mTime);
    float Factor = clamp((AnimationTime - (float)pNodeAnim->mPositionKeys[PositionIndex].mTime) / DeltaTime);
   
    const aiVector3D& Start = pNodeAnim->mPositionKeys[PositionIndex].mValue;
    const aiVector3D& End = pNodeAnim->mPositionKeys[NextPositionIndex].mValue;
    aiVector3D Delta = End - Start;
    
    Out = Start + Factor * Delta;
}

void Skeleton::CalcInterpolatedRotation(aiQuaternion& Out, float AnimationTime, const aiNodeAnim* pNodeAnim)
{
    if (pNodeAnim->mNumRotationKeys == 1) {
        Out = pNodeAnim->mRotationKeys[0].mValue;
        return;
    }

    int RotationIndex = FindRotationKey(AnimationTime, pNodeAnim);
    int NextRotationIndex = (RotationIndex + 1);
    
    float DeltaTime = (float)(pNodeAnim->mRotationKeys[NextRotationIndex].mTime - pNodeAnim->mRotationKeys[RotationIndex].mTime);
    float Factor = clamp((AnimationTime - (float)pNodeAnim->mRotationKeys[RotationIndex].mTime) / DeltaTime);
    
    const aiQuaternion& StartRotationQ = pNodeAnim->mRotationKeys[RotationIndex].mValue;
    const aiQuaternion& EndRotationQ = pNodeAnim->mRotationKeys[NextRotationIndex].mValue;
    aiQuaternion::Interpolate(Out, StartRotationQ, EndRotationQ, Factor);
    
    Out = Out.Normalize();
}

void Skeleton::CalcInterpolatedScaling(aiVector3D& Out, float AnimationTime, const aiNodeAnim* pNodeAnim)
{
    if (pNodeAnim->mNumScalingKeys == 1) {
        Out = pNodeAnim->mScalingKeys[0].mValue;
        return;
    }

    int ScalingIndex = FindScalingKey(AnimationTime, pNodeAnim);
    int NextScalingIndex = (ScalingIndex + 1);
    
    float DeltaTime = (float)(pNodeAnim->mScalingKeys[NextScalingIndex].mTime - pNodeAnim->mScalingKeys[ScalingIndex].mTime);
    float Factor = clamp((AnimationTime - (float)pNodeAnim->mScalingKeys[ScalingIndex].mTime) / DeltaTime);
    
    const aiVector3D& Start = pNodeAnim->mScalingKeys[ScalingIndex].mValue;
    const aiVector3D& End = pNodeAnim->mScalingKeys[NextScalingIndex].mValue;
    aiVector3D Delta = End - Start;
    
    Out = Start + Factor * Delta;
}

aiMatrix4x4 CreateAffineMatrix(const aiVector3D& scaling, const aiQuaternion& rotation, const aiVector3D& translate)
{
    aiMatrix4x4 scalingMatrix;
    aiMatrix4x4 translationMatrix;
    aiMatrix4x4::Scaling(scaling, scalingMatrix);
    aiMatrix4x4::Translation(translate, translationMatrix);
    aiMatrix4x4 rotationMatrix = aiMatrix4x4(rotation.GetMatrix());

    return scalingMatrix * rotationMatrix * translationMatrix;
}

void Mesh::ReadSkeleton(const aiScene* scene, Skeleton* mSkeleton)
{
    unsigned int numMesh = scene->mNumMeshes;
    aiMesh** meshList = scene->mMeshes;

    int boneCount = 0;
    for (UINT x = 0; x < numMesh; ++x)
    {
        for (UINT i = 0; i < meshList[x]->mNumBones; ++i)
        {
            std::string boneName(meshList[x]->mBones[i]->mName.C_Str());
            bool exists = mSkeleton->bones.find(boneName) != mSkeleton->bones.end();

            if (!exists)
            {
                aiMatrix4x4& offsetMatrix = meshList[x]->mBones[i]->mOffsetMatrix;
                Joint joint(boneName, boneCount, offsetMatrix);
                mSkeleton->bones[boneName] = joint;
                boneCount += 1;
            }
        }
    }
}

void Mesh::ReadAnimations(const aiScene* scene, std::unordered_map<std::string, aiAnimation*> animations)
{
    unsigned int numAnim = scene->mNumAnimations;

    for (UINT x = 0; x < numAnim; ++x)
    {
        animations[scene->mAnimations[x]->mName.C_Str()] = scene->mAnimations[x];
    }
}
Gregm8
  • 31
  • 1
  • i assume you read the entirety of the `issue 3` thread? the thread's author denotes some important stuff related to how assimp processes animations and how he made his code work – The Wrecker Apr 05 '23 at 23:05
  • Heya! yes, unfortunately I have reviewed the entire thread and made sure that all the relevant recommendations are being followed. The only thing im not doing is transposing both local and offset transformations, this is because hlsl needs them to be in column major format, so it dosent really make sense to transpose them initially, only to have to transpose them again later to pass them to the gpu. I did actually try it tho, just to see if it fixed it, but it just produced the same result :( – Gregm8 Apr 06 '23 at 00:25
  • 1
    FFS, its a bug with assimp. Reverted do a different version and it is fixed. Why does the universe punish me lol – Gregm8 Apr 06 '23 at 02:58
  • wow! glad you fixed it, just for future reference, what version/commit were you using? so i can avoid it – The Wrecker Apr 06 '23 at 11:32

0 Answers0