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! ๐

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)

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

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

Sure! the model is available here: http://xphere.me/wp-content/uploads/2019/05/3d_model_bob_lamp.zip

After week of hard work animation is not showing on the screen.

main.cpp:

…

SkinnedMesh anim;

anim.loadMesh(“models/anim.dae”);

…

main.cpp(while loop):

…

anim.setBoneTransformations(shaderID, timeInSeconds);

anim.render(shaderID);

…

fragment shader:

…

FragColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);

…

It should be white cube changing shape, rotation, position and size. In console there are no errors. It only outputs: Num vertices = 36

I am using Eclipse on Linux Lubuntu 18.04.

Hey, it would be great if you could share your main.cpp including the rendering loop. Thanks!

Hi! sure I’ll post the loading instructions and rendering loop in a next post! In fact, I’ve improved the classes ๐

Hi, I’ve spent one week trying to understand animation system and why code from ogldev isn’t working. Thank you mate for explaining about assimp, transpose and glm matrix, you have save my day )

You’re welcome!