Search code examples
c++openglglm-mathassimpskeletal-animation

Skeletal animation is skewed


I followed the popular tutorials on skeletal animation by Thin Matrix and another code sample on GitHub

The mesh renders find without any animations. But as soon as animations are applied it gets skewed.

If I pass identity matrices as bonetransforms, it works. Works as in it still renders properly just without any animation.

Also I noticed that the collada file I use uses Z as up and I use Y as up. But I export all data without changing a thing to make sure all transforms and vertex data work as intended. I later on plan on adjusting this as I export so that the data uses Y as up as well.

Here's my code for Skeletal Animation:

Header:

class SkeletalAnimation {
        
        typedef struct Bone{
            int id;
            std::string name;
            glm::mat4 offset;
            std::vector<Bone> children;
        } Bone;

        typedef struct  {
            std::vector<float> translationTimestamps;
            std::vector<float> rotationTimetamps;
            std::vector<float> scalingTimetamps;

            std::vector<glm::vec3> translations;
            std::vector<glm::quat> rotations;
            std::vector<glm::vec3> scalings;
        } BoneTransforms;

        typedef struct Animation {
            float duration;
            float ticksPerSecond;
            std::unordered_map<std::string, BoneTransforms> boneTransforms;
            Animation(float pDuration, float ticksPerSecond) :
                duration(pDuration),
                ticksPerSecond(ticksPerSecond),
                boneTransforms({})
            {}
            Animation() {}
        } Animation;

        typedef std::unordered_map<std::string, std::pair<int, glm::mat4>> BoneData;
        typedef std::unordered_map<std::string, Animation> AnimationMap;
        typedef std::vector<glm::mat4> Pose;

        typedef struct{
            unsigned int segment;
            float fracture;
        } Segment;

        typedef struct {
            Pose pose;
            BoneData boneData;
            unsigned int boneCount;
            std::string name;
            Bone skeleton;
        } MeshEntry;

        typedef std::unordered_map<std::string, MeshEntry> MeshBoneMap;

    private:

        const std::string mPath;
        SDL_Renderer* mRenderer;
        
        std::vector<MeshEntry> mMeshEntries;
        std::vector<SkeletalMesh*> mMeshes;
        std::vector<ImageTexture*> mTextures;
        std::vector<unsigned int> mMeshToTexture;

        std::string* mCurrentAnimation;
        std::vector<std::string> mAnimations;

        AnimationMap mAnimationMap;
        Segment mCurrentSegment;
        glm::mat4 mGlobalInverseTransform;
        MeshBoneMap mMeshBoneMap;

        static glm::mat4 sIdentityMatrix;

        void LoadNode(aiNode* pNode, const aiScene* pScene);
        void LoadSkeletalMesh(aiMesh* pMesh, const aiScene* pScene);
        bool LoadBones(Bone& pBone, aiNode* pNode, BoneData& pBoneData);
        void LoadAnimations(const aiScene* pScene);
        void LoadMaterials(const aiScene* pScene);
        void Animate(float pDeltaTime, Bone& pSkeleton, Pose& pPose, glm::mat4& pParentTransform);

        static inline glm::mat4 aiToGlmMat4(const aiMatrix4x4& pAiMat);
        static inline glm::vec3 aiToGlmVec3(const aiVector3D& pAiVec);
        static inline glm::quat aiToGlmQuat(const aiQuaternion& pAiVec);
        static inline void GetSegment(Segment* pSegment,const std::vector<float>& pTimestamps,const float pDeltaTime);

    public:
        SkeletalAnimation(const std::string pPath, SDL_Renderer* pRenderer);
        ~SkeletalAnimation();

        void LoadAnimation();
        void GetAllAnimations(std::vector<std::string>* pAnimations);
        float GetAnimationDuration();
        void SetAnimationTime(float pTime);
        void SetAnimation(std::string pAnimation);
        void RenderAnimation(float pDeltaTime, SkeletalAnimationShader* pSkeletalAnimationShader);
        void RenderStill(SkeletalAnimationShader* pSkeletalAnimationShader);
        void ClearModel();
    };

Source:

glm::mat4 SkeletalAnimation::sIdentityMatrix = glm::mat4();
    
        SkeletalAnimation::SkeletalAnimation(const std::string pPath, SDL_Renderer* pRenderer) :
            mPath(pPath),
            mRenderer(pRenderer),
            mCurrentAnimation(new std::string()),
            mAnimations({}),
            mAnimationMap({}),
            mMeshBoneMap({})
        {}

        SkeletalAnimation::~SkeletalAnimation() {
        
        }

        void SkeletalAnimation::LoadAnimation() {
            Assimp::Importer _importer;
            const aiScene* _scene = _importer.ReadFile(mPath, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_GenSmoothNormals | aiProcess_JoinIdenticalVertices);

            if (!_scene) {
                SDL_Log("Assimp Error Loading Animation at path: %s \n Error: %s .", mPath.c_str(), _importer.GetErrorString());
                return;
            }

            
            mGlobalInverseTransform = glm::inverse(aiToGlmMat4(_scene->mRootNode->mTransformation));

            LoadNode(_scene->mRootNode, _scene);
            
            LoadAnimations(_scene);
            
            LoadMaterials(_scene);
            
        }

        void SkeletalAnimation::LoadNode(aiNode* pNode, const aiScene* pScene) {
            for (size_t i = 0; i < pNode->mNumMeshes; i++) {
                LoadSkeletalMesh(pScene->mMeshes[pNode->mMeshes[i]], pScene);
            }

            for (size_t i = 0; i < pNode->mNumChildren; i++) {
                LoadNode(pNode->mChildren[i], pScene);
            }
        }

        void SkeletalAnimation::LoadSkeletalMesh(aiMesh* pMesh, const aiScene* pScene) {

            MeshEntry _meshEntry;
            _meshEntry.boneCount = pMesh->mNumBones;
            _meshEntry.name = std::string(pMesh->mName.C_Str());
            _meshEntry.pose = {};
            _meshEntry.pose.resize(pMesh->mNumBones, sIdentityMatrix);
            _meshEntry.boneData = {};
            SkeletalMeshData _meshData;

            for (size_t i = 0; i < pMesh->mNumVertices; i++) {
                _meshData.vertices.insert(_meshData.vertices.end(),
                    {
                        pMesh->mVertices[i].x,
                        pMesh->mVertices[i].y , //Swaped Y and Z since Blender uses Z as up and I use Y as up.
                        pMesh->mVertices[i].z });

                if (pMesh->mTextureCoords[0]) {
                    _meshData.uvs.insert(_meshData.uvs.end(),
                        {
                            pMesh->mTextureCoords[0][i].x,
                            pMesh->mTextureCoords[0][i].y
                        });
                }
                else {
                    _meshData.uvs.insert(_meshData.uvs.end(),
                        {
                            0.0f,
                            0.0f
                        });
                }
                _meshData.normals.insert(_meshData.normals.end(),
                    {
                        pMesh->mNormals[i].x,
                        pMesh->mNormals[i].y ,
                        pMesh->mNormals[i].z });

                _meshData.boneIDs.insert(_meshData.boneIDs.end(), {
                    0,
                    0,
                    0,
                    0});

                _meshData.weights.insert(_meshData.weights.end(), {
                    0.0f,
                    0.0f,
                    0.0f,
                    0.0f});

            }

            for (size_t i = 0; i < pMesh->mNumFaces; i++) {
                aiFace _face = pMesh->mFaces[i];
                for (size_t j = 0; j < _face.mNumIndices; j++) {
                    _meshData.indices.push_back(_face.mIndices[j]);
                }
            }
            
            for (size_t i = 0; i < pMesh->mNumBones; i++) {
                aiBone* _bone = pMesh->mBones[i];
                glm::mat4 _offset = aiToGlmMat4(_bone->mOffsetMatrix);
                _meshEntry.boneData[_bone->mName.C_Str()] = std::make_pair(i , _offset);

                for (size_t j = 0; j < _bone->mNumWeights; j++) {
                    aiVertexWeight _weight = _bone->mWeights[j];
                    unsigned int _vertexID = _weight.mVertexId * 4;

                    for (size_t k = 0; k < 4; k++) {
                        if (_meshData.weights[_vertexID + k] == 0.0f) {
                            _meshData.weights[_vertexID + k] = _weight.mWeight;
                            _meshData.boneIDs[_vertexID + k] = i;
                            break;
                        }
                    }
                }

                
            }

            for (size_t i = 0; i < _meshData.weights.size(); i+=4) {
                float _totalWeight = 
                    _meshData.weights[i]    + 
                    _meshData.weights[i+1]  + 
                    _meshData.weights[i+2]  +
                    _meshData.weights[i+3];
                if (_totalWeight > 0.0f) {
                    _meshData.weights[i] /= _totalWeight;
                    _meshData.weights[i+1] /= _totalWeight;
                    _meshData.weights[i+2] /= _totalWeight;
                    _meshData.weights[i+3] /= _totalWeight;
                }
            }

            SkeletalMesh* _newMesh = new SkeletalMesh();
            _newMesh->BuildMesh(_meshData);
            mMeshes.push_back(_newMesh);
            mMeshToTexture.push_back(pMesh->mMaterialIndex);

            LoadBones(_meshEntry.skeleton, pScene->mRootNode, _meshEntry.boneData);
            mMeshEntries.push_back(_meshEntry);
        }

        bool SkeletalAnimation::LoadBones(Bone& pBone ,aiNode* pNode, BoneData& pBoneData) {
            if (pBoneData.find(pNode->mName.C_Str()) != pBoneData.end()) {
                pBone.name = pNode->mName.C_Str();
                pBone.id = pBoneData[pBone.name].first;
                pBone.offset = pBoneData[pBone.name].second;

                for (size_t i = 0; i < pNode->mNumChildren; i++) {
                    Bone _child;
                    LoadBones(_child, pNode->mChildren[i], pBoneData);
                    pBone.children.push_back(_child);
                }
                return true;
            }
            else { 
                for (size_t i = 0; i < pNode->mNumChildren; i++) {
                    if (LoadBones(pBone, pNode->mChildren[i], pBoneData)) {
                        return true;
                    }

                }
            }
            return false;
        }

        void SkeletalAnimation::LoadAnimations(const aiScene* pScene) {
            for (size_t i = 0; i < pScene->mNumAnimations; i++) {
                
                Animation _currentInternalAnimation(0.0f, 1.0f);
                aiAnimation* _currentAiAnimation = pScene->mAnimations[i];

                mAnimations.push_back(std::string(_currentAiAnimation->mName.C_Str()));
                if (i == 0) {
                    *mCurrentAnimation = mAnimations[0];
                }

                if (_currentAiAnimation->mTicksPerSecond != 0.0f) {
                    _currentInternalAnimation.ticksPerSecond = _currentAiAnimation->mTicksPerSecond;
                }
                else {
                    _currentInternalAnimation.ticksPerSecond = 1;
                }

                _currentInternalAnimation.duration = _currentAiAnimation->mDuration * _currentAiAnimation->mTicksPerSecond;
                _currentInternalAnimation.boneTransforms = {};

                BoneTransforms _transforms;

                for (size_t j = 0; j < _currentAiAnimation->mNumChannels; j++) {

                    aiNodeAnim* _channel = _currentAiAnimation->mChannels[j];

                    for (size_t k = 0; k < _channel->mNumPositionKeys; k++) {

                        _transforms.translations.push_back(aiToGlmVec3(_channel->mPositionKeys[k].mValue));
                        _transforms.translationTimestamps.push_back(_channel->mPositionKeys[k].mTime); 
                    }
                    for (size_t k = 0; k < _channel->mNumRotationKeys; k++) {
                        _transforms.rotations.push_back(aiToGlmQuat(_channel->mRotationKeys[k].mValue));
                        _transforms.rotationTimetamps.push_back(_channel->mRotationKeys[k].mTime);

                    }
                    for (size_t k = 0; k < _channel->mNumScalingKeys; k++) {
                        _transforms.scalings.push_back(aiToGlmVec3(_channel->mScalingKeys[k].mValue));
                        _transforms.scalingTimetamps.push_back(_channel->mScalingKeys[k].mTime);
                    }

                    _currentInternalAnimation.boneTransforms[_channel->mNodeName.C_Str()] = _transforms;
                }
                mAnimationMap[_currentAiAnimation->mName.C_Str()] = _currentInternalAnimation;
            }
        }

        void SkeletalAnimation::LoadMaterials(const aiScene* pScene) {
            mTextures.resize(pScene->mNumMaterials);
            for (size_t i = 0; i < pScene->mNumMaterials; i++) {
                aiMaterial* _material = pScene->mMaterials[i];
                mTextures[i] = nullptr;

                if (_material->GetTextureCount(aiTextureType_DIFFUSE)) {
                    aiString _path;
                    if (_material->GetTexture(aiTextureType_DIFFUSE, 0, &_path) == AI_SUCCESS) {
                        int _idx = std::string(_path.data).rfind("\\");
                        std::string _fileName = std::string(_path.data).substr(_idx + 1);
                        std::string _texturePath = std::string("assets/") + _fileName;
                        SDL_Log("Model Loading Texture at path: %s .", _texturePath.c_str());
                        mTextures[i] = new ImageTexture(_texturePath, mRenderer);
                        mTextures[i]->Load();

                        if (!mTextures[i]->IsLoaded()) {
                            delete mTextures[i];
                            mTextures[i] = nullptr;
                            SDL_Log("Model Error Loading Texture at path: %s .", _texturePath.c_str());
                        }

                    }
                }
            }

        }

        float SkeletalAnimation::GetAnimationDuration() {
            return mAnimationMap[*mCurrentAnimation].duration;
        }

        void SkeletalAnimation::SetAnimationTime(float pTime) {
            for (size_t i = 0; i < mMeshes.size(); i++) {
                MeshEntry& _entry = mMeshEntries[i];
                Animate(pTime, _entry.skeleton, _entry.pose, sIdentityMatrix);
            }
        }

        void SkeletalAnimation::Animate(float pDeltaTime, Bone& pSkeleton, Pose& pPose, glm::mat4& pParentTransform) {
            Animation& _currentAnimation = mAnimationMap[*mCurrentAnimation];
            
            BoneTransforms& _boneTransforms = _currentAnimation.boneTransforms[pSkeleton.name];
            pDeltaTime = fmod(pDeltaTime, _currentAnimation.duration);
            
            //Calculate translations
            GetSegment(&mCurrentSegment, _boneTransforms.translationTimestamps, pDeltaTime);

            glm::vec3 _translation = glm::mix(
                _boneTransforms.translations[mCurrentSegment.segment - 1], 
                _boneTransforms.translations[mCurrentSegment.segment], 
                mCurrentSegment.fracture);

            //Calculate rotations
            GetSegment(&mCurrentSegment, _boneTransforms.rotationTimetamps, pDeltaTime);

            glm::quat _rotation = glm::slerp(
                _boneTransforms.rotations[mCurrentSegment.segment - 1], 
                _boneTransforms.rotations[mCurrentSegment.segment], 
                mCurrentSegment.fracture);

            //Calculate scalings
            GetSegment(&mCurrentSegment, _boneTransforms.scalingTimetamps, pDeltaTime);

            glm::vec3 _scaling = glm::mix(
                _boneTransforms.scalings[mCurrentSegment.segment - 1],
                _boneTransforms.scalings[mCurrentSegment.segment],
                mCurrentSegment.fracture);

            glm::mat4 _translationMatrix = glm::translate(glm::mat4(1.0f), _translation);
            glm::mat4 _rotationMatrix = glm::toMat4(_rotation);
            glm::mat4 _scalingMatrix = glm::scale(glm::mat4(1.0f), _scaling); glm::mat4(1.0f);
            
            glm::mat4 _localTransform = _translationMatrix * _rotationMatrix * _scalingMatrix;
            glm::mat4 _globalTransform = pParentTransform * _localTransform;

            pPose[pSkeleton.id] = mGlobalInverseTransform * _globalTransform * pSkeleton.offset;

            for (Bone& _child : pSkeleton.children) {
                Animate(pDeltaTime, _child, pPose, _globalTransform);
            }
        }

        void SkeletalAnimation::GetAllAnimations(std::vector<std::string>* pAnimations) {
            pAnimations->clear();
            *pAnimations = mAnimations;
        }

        void SkeletalAnimation::SetAnimation(std::string pAnimation) {
            assert(std::find(mAnimations.begin(), mAnimations.end(), pAnimation) != mAnimations.end(), "Animation does not exist.");
            *mCurrentAnimation = pAnimation;
        }

        void SkeletalAnimation::RenderAnimation(float pDeltaTime, SkeletalAnimationShader* pSkeletalAnimationShader) {

            for (size_t i = 0; i < mMeshes.size(); i++) {
                unsigned int _materialIndex = mMeshToTexture[i];

                if (_materialIndex < mTextures.size() && mTextures[_materialIndex]) {
                    mTextures[_materialIndex]->Enable();
                }

                MeshEntry& _entry = mMeshEntries[i];
                Animate(pDeltaTime, _entry.skeleton, _entry.pose, sIdentityMatrix);
                pSkeletalAnimationShader->SetBoneTransforms(_entry.boneCount, _entry.pose);
                mMeshes[i]->Render();
            }
        }

        void SkeletalAnimation::RenderStill(SkeletalAnimationShader* pSkeletalAnimationShader) {
            for (size_t i = 0; i < mMeshes.size(); i++) {
                unsigned int _materialIndex = mMeshToTexture[i];

                if (_materialIndex < mTextures.size() && mTextures[_materialIndex]) {
                    mTextures[_materialIndex]->Enable();
                }

                MeshEntry& _entry = mMeshEntries[i];
                pSkeletalAnimationShader->SetBoneTransforms(_entry.boneCount, _entry.pose);
                mMeshes[i]->Render();
            }
        }

        void SkeletalAnimation::ClearModel() {
            for (size_t i = 0; i < mMeshes.size(); i++) {
                if (mMeshes[i]) {
                    delete mMeshes[i];
                    mMeshes[i] = nullptr;
                }
            }

            for (size_t i = 0; i < mTextures.size(); i++) {
                if (mTextures[i]) {
                    delete mTextures[i];
                    mTextures[i] = nullptr;
                }
            }
        }

        void SkeletalAnimation::GetSegment(Segment* pSegment,const std::vector<float>& pTimestamps, const float pDeltaTime) {
            unsigned int _segment = 1;
            while (pDeltaTime > pTimestamps[_segment]) {
                _segment++;
            }
            float _start = pTimestamps[_segment - 1];
            float _end = pTimestamps[_segment];
            float _fracture = (pDeltaTime - _start) / (_end - _start);
            pSegment->segment = _segment;
            pSegment->fracture = _fracture;
        }

        glm::mat4 SkeletalAnimation::aiToGlmMat4(const aiMatrix4x4& pAiMat) {
            glm::mat4 _glmMat;
            for (int y = 0; y < 4; y++)
            {
                for (int x = 0; x < 4; x++)
                {
                    _glmMat[x][y] = pAiMat[y][x];
                }
            }
            return _glmMat;
        }

        glm::vec3 SkeletalAnimation::aiToGlmVec3(const aiVector3D& pAiVec) {
            return glm::vec3(pAiVec.x, pAiVec.y, pAiVec.z); //Swapped Y and Z to correct Blender ups.
        }

        glm::quat SkeletalAnimation::aiToGlmQuat(const aiQuaternion& pAiQuat) {
            return glm::quat(pAiQuat.w, pAiQuat.x, pAiQuat.y, pAiQuat.z);
        }

I read my code line by line multiple times to see what I'm doing wrong but I can't think of anything. I don't think my shader is the issue but here's the vertex shader:

layout (location = 0) in vec3 position;
layout (location = 1) in vec2 uv;
layout (location = 2) in vec3 normal;
layout (location = 3) in ivec4 boneIds; 
layout (location = 4) in vec4 boneWeights; 

out vec2 textureUV;
out vec3 lightNormal;
out vec4 worldPosition;

uniform mat4 model;
uniform mat4 projectionView;
uniform mat4 boneTransforms[50];

void main()
{
    mat4 boneTransform  =  mat4(0.0f);
    for(int i = 0; i < 4; i++){
        boneTransform  += boneTransforms[boneIds[i]] * boneWeights[i];
    }
    worldPosition = boneTransform * vec4(position, 1.0f);
    worldPosition = model * worldPosition;
    gl_Position = projectionView * worldPosition;
    textureUV = uv;
    lightNormal = mat3(transpose(inverse(model * boneTransform))) * normal;
}

The result: Skewed image


Solution

  • I figured it out. BoneTransforms needed to be moved within the for loop. The same instance was getting over-written each loop cycle.