Menu

Bones animation with OpenGL, ASSIMP and GLM

May 28, 2019 - code, demoscene
Bones animation with OpenGL, ASSIMP and GLM

Probably you have started a 3d engine, based in some tutorials like the one in learnopengl.org, they are really great! I did it! 🙂

As you know, normally the engine goes bigger and bigger (and better), and you trend to keep those dependencies that you started with, in this case: ASSIMP (library used for loading 3d scenes) and GLM (a math library). Both libraries are great! but… what happens when you find another tutorial that does not have the GLM dependency? a headache. Or at least, it was for me! 😀

In my case, I wanted to add skeletal animations to my engine, so I found this great tutorial:
http://ogldev.atspace.co.uk/www/tutorial38/tutorial38.html

The problem is that this tutorial does not use GLM, and the files supplied where not matching my engine… moreover, the tutorial I think that misses some important parts… so I decided to look around for some other people that faced the same issue as me.

I found this thread with some guy with problems with GLM… interesting, same issues as I was having… the thread has some good recomendations, like the helper functions for converting ASSIMP matrix to GLM matrix, but the thread is not finished… so… nothing.

Then I found this blog from Stanislav:
https://lechior.blogspot.com/2017/05/skeletal-animation-using-assimp-opengl.html with some good recomendations, but the code was not working… the matrix calculations done on the bones were wrong…

So… what I did? mixing all togehter!!!

Basically I took the files form Stanislav, I added the GLM conversions, and I modified the calculations of the ReadNodeHeirarchy method, and a few more fixes:

// For converting between ASSIMP and glm
static inline glm::vec3 vec3_cast(const aiVector3D &v) { return glm::vec3(v.x, v.y, v.z); }
static inline glm::vec2 vec2_cast(const aiVector3D &v) { return glm::vec2(v.x, v.y); }
static inline glm::quat quat_cast(const aiQuaternion &q) { return glm::quat(q.w, q.x, q.y, q.z); }
static inline glm::mat4 mat4_cast(const aiMatrix4x4 &m) { return glm::transpose(glm::make_mat4(&m.a1)); }
static inline glm::mat4 mat4_cast(const aiMatrix3x3 &m) { return glm::transpose(glm::make_mat3(&m.a1)); }
void SkinnedMesh::ReadNodeHeirarchy(float AnimationTime, const aiNode* pNode, const glm::mat4& ParentTransform)
{
	std::string NodeName(pNode->mName.data);

	const aiAnimation* pAnimation = m_pScene->mAnimations[currentAnimation];

	glm::mat4 NodeTransformation = mat4_cast(pNode->mTransformation);
	
	const aiNodeAnim* pNodeAnim = FindNodeAnim(pAnimation, NodeName);

	if (pNodeAnim) {
		// Interpolate scaling and generate scaling transformation matrix
		aiVector3D Scaling;
		CalcInterpolatedScaling(Scaling, AnimationTime, pNodeAnim);
		glm::vec3 scale = glm::vec3(Scaling.x, Scaling.y, Scaling.z);
		glm::mat4 ScalingM = glm::scale(glm::mat4(1.0f), scale);
		
		// Interpolate rotation and generate rotation transformation matrix
		aiQuaternion RotationQ;
		CalcInterpolatedRotation(RotationQ, AnimationTime, pNodeAnim);
		glm::quat rotation = quat_cast(RotationQ);
		glm::mat4 RotationM = glm::toMat4(rotation);

		// Interpolate translation and generate translation transformation matrix
		aiVector3D Translation;
		CalcInterpolatedPosition(Translation, AnimationTime, pNodeAnim);
		glm::vec3 translation = glm::vec3(Translation.x, Translation.y, Translation.z);
		glm::mat4 TranslationM = glm::translate(glm::mat4(1.0f), translation);

		// Combine the above transformations
		NodeTransformation = TranslationM * RotationM *ScalingM;
	}
	
	// Combine with node Transformation with Parent Transformation
	glm::mat4 GlobalTransformation = ParentTransform * NodeTransformation;

	if (m_BoneMapping.find(NodeName) != m_BoneMapping.end()) {
		unsigned int BoneIndex = m_BoneMapping[NodeName];
		m_BoneInfo[BoneIndex].FinalTransformation = m_GlobalInverseTransform * GlobalTransformation * m_BoneInfo[BoneIndex].BoneOffset;
	}

	for (unsigned int i = 0; i < pNode->mNumChildren; i++) {
		ReadNodeHeirarchy(AnimationTime, pNode->mChildren[i], GlobalTransformation);
	}
}

Some minor change was needed also in the boneTransform method… because the identity matrix was not properly loaded in the beggining:

void SkinnedMesh::boneTransform(float timeInSeconds, std::vector<glm::mat4>& Transforms)
{
	glm::mat4 Identity = glm::mat4(1.0f);
	
	/* Calc animation duration */
	unsigned int numPosKeys = m_pScene->mAnimations[currentAnimation]->mChannels[0]->mNumPositionKeys;
	animDuration = m_pScene->mAnimations[currentAnimation]->mChannels[0]->mPositionKeys[numPosKeys - 1].mTime;

	float TicksPerSecond = (float)(m_pScene->mAnimations[currentAnimation]->mTicksPerSecond != 0 ? m_pScene->mAnimations[currentAnimation]->mTicksPerSecond : 25.0f);
	float TimeInTicks = timeInSeconds * TicksPerSecond;
	float AnimationTime = fmod(TimeInTicks, animDuration);

	ReadNodeHeirarchy(AnimationTime, m_pScene->mRootNode, Identity);

	Transforms.resize(m_NumBones);
	
	for (unsigned int  i = 0; i < m_NumBones; i++) {
		Transforms[i] = m_BoneInfo[i].FinalTransformation;
	}
}

And finally, I did dome some other adjustments to the Draw method, in order to send in a more efficient way the bones information to the shader:

void SkinnedMesh::setBoneTransformations(GLuint shaderProgram, GLfloat currentTime)
{
	std::vector<glm::mat4> Transforms;
	boneTransform((float)currentTime, Transforms);
	glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "gBones"), (GLsizei)Transforms.size(), GL_FALSE, glm::value_ptr(Transforms[0]));
}

Also, the shaders where a little bit modified:

#version 330 core
layout (location = 0) in vec3 Position;
layout (location = 1) in vec3 Normal;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in ivec4 BoneIDs;
layout (location = 4) in vec4 Weights;

const int MAX_BONES = 100;

uniform mat4 gBones[MAX_BONES];

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out vec2 TexCoords;

void main()
{
	TexCoords = aTexCoords;
    
	mat4 BoneTransform = gBones[BoneIDs[0]] * Weights[0];
	BoneTransform += gBones[BoneIDs[1]] * Weights[1];
	BoneTransform += gBones[BoneIDs[2]] * Weights[2];
	BoneTransform += gBones[BoneIDs[3]] * Weights[3];
    
	vec4 PosL = BoneTransform * vec4(Position, 1.0);
	gl_Position = projection * view * model * PosL;
}
#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D texture_diffuse1;

void main()
{   
    FragColor = vec4(texture(texture_diffuse1, TexCoords));
}

So, this is everything!! and the result is pretty solid! 🙂

If you are as curious as I am, you will probably be thinking… “ok ok, but give me the sources!!!” ok ok, here you have!!! enjoy!! 🙂

Here you have the sources and shaders that I used.

And here you have the model, just in case you don’t have any 3d model to play with 🙂

Many things need to be done: code cleaning, adding textures, etc… but at least is something to start with! 🙂

4 thoughts on “Bones animation with OpenGL, ASSIMP and GLM

Jang.

my program cant use md5mesh model. so i use fbx now, but it has some problem. it works just the first bones, others are crashed. i didnt change code and i have no idea. can u help me?(sry for bad english)

Reply
xphere

Hi Jang, could you please share the model you are using so I can test it with my code? Thanks!

Reply
Felipe

Hello, you could you share the md5anim file? Your tutorial is helping me a lot, but I am still facing some issues.

Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

spammer, go home! * Time limit is exhausted. Please reload the CAPTCHA.

This site uses Akismet to reduce spam. Learn how your comment data is processed.