Skeletal Animation looks skewed

Hi all,

I just completed my code for skeletal animation. But my animations look skewed. I feel like I’m missing something minor that’s causing the issue but I spent days now trying to figure out to no avail.

Just rendering the model itself works and the issue I believe comes down the matrix calculations because if I send identity matrices for the current pose, it renders it fine(without the animation obviously).

I followed the thin matrix tutorial as well as https://github.com/hasinaxp/skeletal_animation-_assimp_opengl/blob/master/main.cpp

So my code is mix of both and my own.

Here’s my class for the 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);
		void SetAnimation(std::string pAnimation);
		void RenderAnimation(float pDeltaTime, SkeletalAnimationShader* pSkeletalAnimationShader);
		void ClearModel();
	};

}

And 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 ,
					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 });
		}

		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]);
			}
		}

		unsigned int _indexCount = pMesh->mNumVertices * 4;
		_meshData.weights.resize(_indexCount, 0.0f);
		_meshData.boneIDs.resize(_indexCount, 0);

		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());
					}

				}
			}
		}

	}

	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::mat4(1.0f);
		_translationMatrix = glm::translate(_translationMatrix, _translation);

		glm::mat4 _rotationMatrix = glm::toMat4(_rotation);

		glm::mat4 _scalingMatrix = glm::mat4(1.0f);
		_scalingMatrix = glm::scale(_scalingMatrix, _scaling);

		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::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 = 0;
		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);
	}

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

My 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:

Any help is appreciated.

Draw the skeleton in wireframe (i.e. a stick figure). For each bone, draw a line from the bone origin (i.e. the right-hand column of the bone matrix, or the result of transforming (0,0,0,1) by the matrix) to the origin of its parent.

If it looks correct, the problem is with the shader (or with passing the bone array to the shader). If it exhibits the same issues as the rendered mesh, the problem is with the matrix calculations.

The shader looks straightforward enough, so I’d assume the latter.

It isn’t the shader. I ruled it out. It renders it without the animation and if the transform matrices are simple identity matrices.