Skeletal Animation not working

Hello,

I want to get skeletal animation working but I’m not getting it
done. I tried now for days… Basically my problem is, that as soon
as I apply bone transformations to my model, the arms and legs
disappear. My code follows the code provided by this tutorial 38 from ogldev (OpenGL Step by Step - OpenGL Development).

This is my model class:
skinned-mesh.hpp:

#pragma once

#include <map>
#include <memory>
#include <string>
#include <vector>

#include <assimp/Importer.hpp>  // C++ importer interface
#include <assimp/postprocess.h> // Post processing flags
#include <assimp/scene.h>       // Output data structure
#include <glm/glm.hpp>
#include <glm/gtx/quaternion.hpp>

#include "glad.h"
#include "texture.hpp"

class SkinnedMesh
{
public:
  SkinnedMesh();
  ~SkinnedMesh();
  bool     loadMesh(const std::string &filename);
  void     render();
  unsigned getCountBones() const { return countBones; }

  void boneTransform(const float             timeInSeconds,
                     std::vector<glm::mat4> &transforms);

private:
#define NUM_BONES_PER_VEREX 4

  unsigned countBones = 0;

  struct BoneInfo
  {
    glm::mat4 boneOffset;
    glm::mat4 finalTransformation;
  };

  struct VertexBoneData
  {
    int      ids[NUM_BONES_PER_VEREX]     = {0};
    float    weights[NUM_BONES_PER_VEREX] = {0.0};

    VertexBoneData()
    {
      for (unsigned i = 0; i < NUM_BONES_PER_VEREX; ++i)
      {
        ids[i]     = 0;
        weights[i] = 0.0;
      }
    }

    void addBoneData(unsigned BoneID, float Weight);
  };

  struct Vertex
  {
    glm::vec3      position;
    glm::vec3      normal;
    glm::vec2      textureCoords;
    VertexBoneData boneData;
  };

#define INVALID_MATERIAL 0xFFFFFFFF

  struct MeshEntry
  {
    unsigned int          materialIndex = INVALID_MATERIAL;
    std::vector<unsigned> indices;
    std::vector<Vertex>   vertices;
    unsigned              vao            = 0;
    unsigned              vertexBufferId = 0;
    unsigned              indexBufferId  = 0;
  };

  std::vector<BoneInfo> boneInfos;

  void calcInterpolatedScaling(glm::vec3 &       out,
                               float             animationTime,
                               const aiNodeAnim *pNodeAnim);

  void calcInterpolatedRotation(glm::quat &       out,
                                float             animationTime,
                                const aiNodeAnim *pNodeAnim);

  void calcInterpolatedPosition(glm::vec3 &       out,
                                float             animationTime,
                                const aiNodeAnim *pNodeAnim);

  unsigned findScaling(float animationTime, const aiNodeAnim *pNodeAnim);

  unsigned findRotation(float animationTime, const aiNodeAnim *pNodeAnim);

  unsigned findPosition(float animationTime, const aiNodeAnim *pNodeAnim);

  const aiNodeAnim *findNodeAnim(const aiAnimation *pAnimation,
                                 const std::string  nodeName);

  void readNodeHeirarchy(float            animationTime,
                         const aiNode *   pNode,
                         const glm::mat4 &parentTransform);

  bool initFromScene(const aiScene *pScene, const std::string &filename);

  void initMesh(const aiMesh *paiMesh, MeshEntry &mesh);

  void loadBones(const aiMesh *paiMesh, MeshEntry &mesh);

  bool initMaterials(const aiScene *pScene, const std::string &filename);

  enum VB_TYPES
  {
    INDEX_BUFFER,
    POS_VB,
    NORMAL_VB,
    TEXCOORD_VB,
    BONE_VB,
    NUM_VBs
  };

  std::vector<MeshEntry> entries;
  std::vector<Texture *> textures;

  std::map<std::string, unsigned> boneMapping; // maps a bone name to its index
  unsigned                        numBones = 0;
  glm::mat4                       globalInverseTransform;

  const aiScene *  pScene;
  Assimp::Importer importer;
};

skinned-mesh.cpp:

#include <iostream>

#include "skinned-mesh.hpp"

#include <glm/gtc/type_ptr.hpp>

#define ASSIMP_LOAD_FLAGS                                                      \
  (aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_FlipUVs |    \
   aiProcess_JoinIdenticalVertices)

#define ARRAY_SIZE_IN_ELEMENTS(a) (sizeof(a) / sizeof(a[0]))

#define POSITION_LOCATION    0
#define TEX_COORD_LOCATION   1
#define NORMAL_LOCATION      2
#define BONE_ID_LOCATION     3
#define BONE_WEIGHT_LOCATION 4

unsigned animationIndex = 0;

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)
{
  // it's aiVector3D because assimp's texture coordinates use that
  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));
}

SkinnedMesh::SkinnedMesh() {}

SkinnedMesh::~SkinnedMesh() {}

bool SkinnedMesh::loadMesh(const std::string &filename)
{
  bool ret = false;

  pScene = importer.ReadFile(filename.c_str(), ASSIMP_LOAD_FLAGS);

  if (pScene)
  {
    globalInverseTransform =
        glm::inverse(mat4_cast(pScene->mRootNode->mTransformation));
    ret = initFromScene(pScene, filename);
  }
  else
  {
    printf("Error parsing '%s': '%s'\n",
           filename.c_str(),
           importer.GetErrorString());
  }

  // Make sure the VAO is not changed from the outside
  glBindVertexArray(0);

  return ret;
}

void SkinnedMesh::VertexBoneData::addBoneData(uint BoneID, float Weight)
{
  for (uint i = 0; i < ARRAY_SIZE_IN_ELEMENTS(ids); i++)
  {
    if (weights[i] == 0.0)
    {
      ids[i]     = BoneID;
      weights[i] = Weight;
      return;
    }
  }

  // should never get here - more bones than we have space for
  assert(0);
}

void SkinnedMesh::render()
{
  for (const auto &mesh : entries)
  {
    glBindVertexArray(mesh.vao);

    const auto materialIndex = mesh.materialIndex;

    assert(materialIndex < textures.size());

    if (textures[materialIndex])
    {
      textures[materialIndex]->Bind(GL_TEXTURE0);
    }

    glDrawElements(GL_TRIANGLES, mesh.indices.size(), GL_UNSIGNED_INT, 0);

    glBindVertexArray(0);
  }
}

void SkinnedMesh::boneTransform(const float             timeInSeconds,
                                std::vector<glm::mat4> &transforms)
{
  glm::mat4 Identity(1.0f);

  float TicksPerSecond =
      (float)(pScene->mAnimations[animationIndex]->mTicksPerSecond != 0
                  ? pScene->mAnimations[animationIndex]->mTicksPerSecond
                  : 25.0f);
  float duration = pScene->mAnimations[animationIndex]->mDuration;

  float TimeInTicks = timeInSeconds * TicksPerSecond;

  float AnimationTime = fmod(TimeInTicks, duration);

  readNodeHeirarchy(AnimationTime, pScene->mRootNode, Identity);

  transforms.resize(boneInfos.size());

  for (uint i = 0; i < boneInfos.size(); i++)
  {
    transforms[i] = boneInfos[i].finalTransformation;
  }
}

void SkinnedMesh::calcInterpolatedScaling(glm::vec3 &       out,
                                          float             animationTime,
                                          const aiNodeAnim *pNodeAnim)
{
  if (pNodeAnim->mNumScalingKeys == 1)
  {
    out = vec3_cast(pNodeAnim->mScalingKeys[0].mValue);
    return;
  }

  uint ScalingIndex     = findScaling(animationTime, pNodeAnim);
  uint NextScalingIndex = (ScalingIndex + 1);
  assert(NextScalingIndex < pNodeAnim->mNumScalingKeys);
  float DeltaTime = (float)(pNodeAnim->mScalingKeys[NextScalingIndex].mTime -
                            pNodeAnim->mScalingKeys[ScalingIndex].mTime);
  float Factor =
      (animationTime - (float)pNodeAnim->mScalingKeys[ScalingIndex].mTime) /
      DeltaTime;
  assert(Factor >= 0.0f && Factor <= 1.0f);
  const auto &Start = vec3_cast(pNodeAnim->mScalingKeys[ScalingIndex].mValue);
  const auto &End = vec3_cast(pNodeAnim->mScalingKeys[NextScalingIndex].mValue);
  auto        Delta = End - Start;

  out = Start + Factor * Delta;
}

void SkinnedMesh::calcInterpolatedRotation(glm::quat &       out,
                                           float             animationTime,
                                           const aiNodeAnim *pNodeAnim)
{
  // we need at least two values to interpolate...
  if (pNodeAnim->mNumRotationKeys == 1)
  {
    out = quat_cast(pNodeAnim->mRotationKeys[0].mValue);
    return;
  }

  uint RotationIndex     = findRotation(animationTime, pNodeAnim);
  uint NextRotationIndex = (RotationIndex + 1);
  assert(NextRotationIndex < pNodeAnim->mNumRotationKeys);
  float DeltaTime = (float)(pNodeAnim->mRotationKeys[NextRotationIndex].mTime -
                            pNodeAnim->mRotationKeys[RotationIndex].mTime);
  float Factor =
      (animationTime - (float)pNodeAnim->mRotationKeys[RotationIndex].mTime) /
      DeltaTime;
  assert((animationTime -
          (float)pNodeAnim->mRotationKeys[RotationIndex].mTime) >= 0.0f);
  assert(DeltaTime >= 0.0f);
  assert(Factor >= 0.0f && Factor <= 1.0f);
  const auto &StartRotationQ =
      quat_cast(pNodeAnim->mRotationKeys[RotationIndex].mValue);
  const auto &EndRotationQ =
      quat_cast(pNodeAnim->mRotationKeys[NextRotationIndex].mValue);
  out = glm::mix(StartRotationQ, EndRotationQ, Factor);

  out = glm::normalize(out);
}

void SkinnedMesh::calcInterpolatedPosition(glm::vec3 &       out,
                                           float             animationTime,
                                           const aiNodeAnim *pNodeAnim)
{
  if (pNodeAnim->mNumPositionKeys == 1)
  {
    out = vec3_cast(pNodeAnim->mPositionKeys[0].mValue);
    return;
  }

  uint PositionIndex     = findPosition(animationTime, pNodeAnim);
  uint NextPositionIndex = (PositionIndex + 1);
  assert(NextPositionIndex < pNodeAnim->mNumPositionKeys);
  float DeltaTime = (float)(pNodeAnim->mPositionKeys[NextPositionIndex].mTime -
                            pNodeAnim->mPositionKeys[PositionIndex].mTime);
  float Factor =
      (animationTime - (float)pNodeAnim->mPositionKeys[PositionIndex].mTime) /
      DeltaTime;
  assert(Factor >= 0.0f && Factor <= 1.0f);
  const auto &Start = vec3_cast(pNodeAnim->mPositionKeys[PositionIndex].mValue);
  const auto &End =
      vec3_cast(pNodeAnim->mPositionKeys[NextPositionIndex].mValue);
  auto Delta = End - Start;

  out = Start + Factor * Delta;
}

unsigned SkinnedMesh::findScaling(float             animationTime,
                                  const aiNodeAnim *pNodeAnim)
{
  assert(pNodeAnim->mNumScalingKeys > 0);

  for (uint i = 0; i < pNodeAnim->mNumScalingKeys - 1; i++)
  {
    if (animationTime < (float)pNodeAnim->mScalingKeys[i + 1].mTime)
    {
      return i;
    }
  }

  assert(0);

  return 0;
}

unsigned SkinnedMesh::findRotation(float             animationTime,
                                   const aiNodeAnim *pNodeAnim)
{
  assert(pNodeAnim->mNumRotationKeys > 0);

  for (uint i = 0; i < pNodeAnim->mNumRotationKeys - 1; i++)
  {
    if (animationTime < (float)pNodeAnim->mRotationKeys[i + 1].mTime)
    {
      return i;
    }
  }

  assert(0);

  return 0;
}

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

  assert(0);

  return 0;
}

const aiNodeAnim *SkinnedMesh::findNodeAnim(const aiAnimation *pAnimation,
                                            const std::string  nodeName)
{
  for (unsigned i = 0; i < pAnimation->mNumChannels; i++)
  {
    const aiNodeAnim *pNodeAnim = pAnimation->mChannels[i];

    if (std::string(pNodeAnim->mNodeName.data) == nodeName)
    {
      return pNodeAnim;
    }
  }

  return NULL;
}

void SkinnedMesh::readNodeHeirarchy(float            animationTime,
                                    const aiNode *   pNode,
                                    const glm::mat4 &parentTransform)
{
  std::string NodeName(pNode->mName.data);

  const aiAnimation *pAnimation = pScene->mAnimations[animationIndex];

  glm::mat4 NodeTransformation(mat4_cast(pNode->mTransformation));

  const aiNodeAnim *pNodeAnim = findNodeAnim(pAnimation, NodeName);

  if (pNodeAnim)
  {
    // Interpolate scaling and generate scaling transformation matrix
    glm::vec3 Scaling;
    calcInterpolatedScaling(Scaling, animationTime, pNodeAnim);
    glm::mat4 ScalingM = glm::scale(glm::mat4(1.0f), Scaling);

    // Interpolate rotation and generate rotation transformation matrix
    glm::quat RotationQ;
    calcInterpolatedRotation(RotationQ, animationTime, pNodeAnim);
    glm::mat4 RotationM =
        glm::toMat4(RotationQ); // Matrix4f(RotationQ.GetMatrix());

    // Interpolate translation and generate translation transformation matrix
    glm::vec3 Translation;
    calcInterpolatedPosition(Translation, animationTime, pNodeAnim);
    glm::mat4 TranslationM = glm::translate(glm::mat4(1.0f), Translation);

    // Combine the above transformations
    NodeTransformation = TranslationM * RotationM * ScalingM;
  }

  glm::mat4 GlobalTransformation = parentTransform * NodeTransformation;

  if (boneMapping.find(NodeName) != boneMapping.end())
  {
    uint BoneIndex                           = boneMapping[NodeName];
    boneInfos[BoneIndex].finalTransformation = globalInverseTransform *
                                               GlobalTransformation *
                                               boneInfos[BoneIndex].boneOffset;
  }

  for (uint i = 0; i < pNode->mNumChildren; i++)
  {
    readNodeHeirarchy(animationTime, pNode->mChildren[i], GlobalTransformation);
  }
}

bool SkinnedMesh::initFromScene(const aiScene *    pScene,
                                const std::string &filename)
{
  entries.resize(pScene->mNumMeshes);
  textures.resize(pScene->mNumMaterials);

  // Initialize the meshes in the scene one by one
  for (uint i = 0; i < entries.size(); i++)
  {
    const aiMesh *paiMesh    = pScene->mMeshes[i];
    entries[i].materialIndex = pScene->mMeshes[i]->mMaterialIndex;
    initMesh(paiMesh, entries[i]);
  }

  if (!initMaterials(pScene, filename))
  {
    return false;
  }

  for (auto &mesh : entries)
  {
    // Generate and populate the buffers with vertex attributes and thise
    // indices
    glGenVertexArrays(1, &mesh.vao);
    glBindVertexArray(mesh.vao);

    glGenBuffers(1, &mesh.vertexBufferId);
    glGenBuffers(1, &mesh.indexBufferId);

    glBindBuffer(GL_ARRAY_BUFFER, mesh.vertexBufferId);
    glBufferData(GL_ARRAY_BUFFER,
                 sizeof(Vertex) * mesh.vertices.size(),
                 &mesh.vertices[0],
                 GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.indexBufferId);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER,
                 mesh.indices.size() * sizeof(unsigned),
                 &mesh.indices[0],
                 GL_STATIC_DRAW);

    glEnableVertexAttribArray(POSITION_LOCATION);
    glVertexAttribPointer(POSITION_LOCATION,
                          3,
                          GL_FLOAT,
                          GL_FALSE,
                          sizeof(Vertex),
                          reinterpret_cast<void *>(offsetof(Vertex, position)));

    glEnableVertexAttribArray(NORMAL_LOCATION);
    glVertexAttribPointer(NORMAL_LOCATION,
                          3,
                          GL_FLOAT,
                          GL_FALSE,
                          sizeof(Vertex),
                          reinterpret_cast<void *>(offsetof(Vertex, normal)));

    glEnableVertexAttribArray(TEX_COORD_LOCATION);
    glVertexAttribPointer(
        TEX_COORD_LOCATION,
        2,
        GL_FLOAT,
        GL_FALSE,
        sizeof(Vertex),
        reinterpret_cast<void *>(offsetof(Vertex, textureCoords)));

    glEnableVertexAttribArray(BONE_ID_LOCATION);
    glVertexAttribPointer(
        BONE_ID_LOCATION,
        NUM_BONES_PER_VEREX,
        GL_INT,
        GL_FALSE,
        sizeof(Vertex),
        reinterpret_cast<void *>(offsetof(Vertex, boneData) +
                                 offsetof(VertexBoneData, ids)));

    glEnableVertexAttribArray(BONE_WEIGHT_LOCATION);
    glVertexAttribPointer(
        BONE_WEIGHT_LOCATION,
        NUM_BONES_PER_VEREX,
        GL_FLOAT,
        GL_FALSE,
        sizeof(Vertex),
        reinterpret_cast<void *>(offsetof(Vertex, boneData) +
                                 offsetof(VertexBoneData, weights)));

    glBindVertexArray(0);
  }

  return (glGetError() == GL_NO_ERROR);
}

void SkinnedMesh::initMesh(const aiMesh *paiMesh, MeshEntry &mesh)
{
  const aiVector3D zero3d(0.0f, 0.0f, 0.0f);

  // Populate the vertex attribute vectors
  for (uint i = 0; i < paiMesh->mNumVertices; ++i)
  {
    const aiVector3D *pPos      = &(paiMesh->mVertices[i]);
    const aiVector3D *pNormal   = &(paiMesh->mNormals[i]);
    const aiVector3D *pTexCoord = paiMesh->HasTextureCoords(0)
                                      ? &(paiMesh->mTextureCoords[0][i])
                                      : &zero3d;

    Vertex vertex;
    vertex.position      = vec3_cast(*pPos);
    vertex.normal        = vec3_cast(*pNormal);
    vertex.textureCoords = vec2_cast(*pTexCoord);

    mesh.vertices.push_back(vertex);
  }

  loadBones(paiMesh, mesh);

  // Populate the index buffer
  for (uint i = 0; i < paiMesh->mNumFaces; i++)
  {
    const aiFace &Face = paiMesh->mFaces[i];
    assert(Face.mNumIndices == 3);

    mesh.indices.push_back(Face.mIndices[0]);
    mesh.indices.push_back(Face.mIndices[1]);
    mesh.indices.push_back(Face.mIndices[2]);
  }
}

void SkinnedMesh::loadBones(const aiMesh *paiMesh, MeshEntry &mesh)
{
  for (unsigned i = 0; i < paiMesh->mNumBones; i++)
  {
    unsigned    boneIndex = 0;
    std::string boneName(paiMesh->mBones[i]->mName.data);

    if (boneMapping.find(boneName) == boneMapping.end())
    {
      // Allocate an index for a new bone
      boneIndex = numBones;
      numBones++;

      BoneInfo bi;
      boneInfos.push_back(bi);
      boneInfos[boneIndex].boneOffset =
          mat4_cast(paiMesh->mBones[i]->mOffsetMatrix);

      boneMapping[boneName] = boneIndex;
    }
    else
    {
      boneIndex = boneMapping[boneName];
    }

    for (unsigned j = 0; j < paiMesh->mBones[i]->mNumWeights; j++)
    {
      uint  vertexId = paiMesh->mBones[i]->mWeights[j].mVertexId;
      float weight   = paiMesh->mBones[i]->mWeights[j].mWeight;

      mesh.vertices[vertexId].boneData.addBoneData(boneIndex, weight);
    }
  }
}

bool SkinnedMesh::initMaterials(const aiScene *    pScene,
                                const std::string &filename)
{
  // Extract the directory part from the file name
  std::string::size_type SlashIndex = filename.find_last_of("/");
  std::string            Dir;

  if (SlashIndex == std::string::npos)
  {
    Dir = ".";
  }
  else if (SlashIndex == 0)
  {
    Dir = "/";
  }
  else
  {
    Dir = filename.substr(0, SlashIndex);
  }

  bool Ret = true;

  // Initialize the materials
  for (uint i = 0; i < pScene->mNumMaterials; i++)
  {
    const aiMaterial *pMaterial = pScene->mMaterials[i];

    textures[i] = NULL;

    if (pMaterial->GetTextureCount(aiTextureType_DIFFUSE) > 0)
    {
      aiString Path;

      if (pMaterial->GetTexture(aiTextureType_DIFFUSE,
                                0,
                                &Path,
                                NULL,
                                NULL,
                                NULL,
                                NULL,
                                NULL) == AI_SUCCESS)
      {
        std::string p(Path.data);

        if (p.substr(0, 2) == ".\\")
        {
          p = p.substr(2, p.size() - 2);
        }

        std::string FullPath = Dir + "/" + p;

        textures[i] = new Texture(GL_TEXTURE_2D, FullPath.c_str());

        if (!textures[i]->Load())
        {
          printf("Error loading texture '%s'\n", FullPath.c_str());
          delete textures[i];
          textures[i] = NULL;
          Ret         = false;
        }
        else
        {
          printf("%d - loaded texture '%s'\n", i, FullPath.c_str());
        }
      }
    }
  }

  return Ret;
}

The model gets loaded in SkinnedMesh::loadMesh(). Rendering gets done in SkinnedMesh::render(). The bone transformation matrices get calculated in SkinnedMesh::boneTransform().

This is the render loop:

  while (!glfwWindowShouldClose(window))
  {
    // per-frame time logic
    // --------------------
    float currentFrame = glfwGetTime();
    deltaTime          = currentFrame - lastFrame;
    lastFrame          = currentFrame;

    // input
    // -----
    processInput(window);

    // render
    // ------
    glClearColor(0.05f, 0.05f, 0.05f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // don't forget to enable shader before setting uniforms
    shader.use();

    // view/projection transformations
    glm::mat4 projection =
        glm::perspective(glm::radians(camera.Zoom),
                         (float)SCR_WIDTH / (float)SCR_HEIGHT,
                         0.1f,
                         100.0f);
    glm::mat4 view = camera.GetViewMatrix();

    glm::mat4 model = glm::mat4(1.0f);
    model =
        glm::rotate(model, glm::degrees(90.0f), glm::vec3(1.0f, 0.0f, 0.0f));
    model = glm::scale(model, glm::vec3(0.1f, 0.1f, 0.1f));

    shader.setMat4("projection", projection);
    shader.setMat4("view", view);
    shader.setMat4("model", model);

    double runningTime = glfwGetTime();

    std::vector<glm::mat4> transforms;
    mesh.boneTransform(runningTime, transforms);

    glUniformMatrix4fv(glGetUniformLocation(shader.ID, "bones"),
                       transforms.size(),
                       GL_FALSE,
                       glm::value_ptr(transforms[0]));

    mesh.render();

    // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved
    // etc.)
    // -------------------------------------------------------------------------------
    glfwSwapBuffers(window);
    glfwPollEvents();
  }

And this my vertex shader:

#version 330

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

out vec2 fragTexCoords;

const int MAX_BONES = 100;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform mat4 bones[MAX_BONES];

void main()
{
  mat4 boneTransform = bones[boneIds[0]] * weights[0];
  boneTransform += bones[boneIds[1]] * weights[1];
  boneTransform += bones[boneIds[2]] * weights[2];
  boneTransform += bones[boneIds[3]] * weights[3];

  fragTexCoords = texCoords;
  gl_Position = projection * view * model * boneTransform * vec4(position, 1.0);
}

This is the fragment shader:

#version 330 core

out vec4 fragColor;

in vec2 fragTexCoords;

uniform sampler2D textureDiffuse;

void main() { fragColor = texture(textureDiffuse, fragTexCoords); }

If I do not multiply the position with boneTransform in the vertex shader. The model gets rendered correctly (without animations).
I already debugged with RenderDoc and it seems that weights and boneIds in the vertex shader are set correctly. However another strange thing is, that if I replace in the vertex shader boneIds[0], boneIds[1], … with 0, 1, … the arms and legs shown with an animation (obviously a incorrect animation).

I am a beginner with graphics, so I am grateful for any help with this problem.

I provided the complete example project at Felix Weilbach / assimp-sekelation-animation-example · GitLab . For building the project (on linux) simply type make. (Assimp and GLFW3 are requiered).

Have you tried looking at the author’s source code for this tutorial?

From that tutorial page, click on the OGL.dev icon in the upper right, which will take you here:

Then click on “Get the Source” up-top in the menu bar. That’ll give you a ZIP file. Inside the ZIP, look in the ogldev-source/tutorial38/ folder.

If you still have questions, feel free to follow-up. Though it sounds like you’ve already got it nailed it down to a problem generating or populating your bone skinning transforms, so seeing what the author did may be all you need.

Thank you for your reply.
Yes, I already checked out the source code. I started from his source code, verified that the animation worked and then started to modify the code.

My aim is to get rid of the assimp data structures and use my own.

For that, I created (as a first step) one vertex array with an vertex buffer and index buffer per mesh and in the method render(), I now draw not like him with glDrawElementsBaseVertex() but with glDrawElements() per mesh. I also introduced a Vertex struct, that contains the positions, normals, boneIds, …

I tried now for really long time to get this working. I have even compared in RenderDoc the working application with my modified version but I coudn’t see any differences.

I actually had similar issues with that tutorial. Your mileage may vary since my engine is built on OpenTK (C# bindings for OpenGL) so the linear algebra library has differently named methods.

Not 100% sure what you mean by wanting to get rid of the Assimp structures. I assume you mean that you still want to load the model with Assimp, but once it’s loaded collect all the data into your application’s particular data structures. In my opinion that’s the way to do it; I don’t like using third party library data structures in my core engine code unless I’m going to consistently use them everywhere.

The first problem I ran into was that the bone matrix multiplications in the tutorial code were backwards. I think this may just be a peculiarity of OpenTK; the multiplications go in the reverse order that you’d do them in GLSL. Not sure if this would be a problem since you’re using glm like the tutorial, but you could try reversing your matrix operations. With the multiplications in the wrong order, my models rendered OK without sending bones to the shader (i.e. all zero matrices), but when I tried using bones the model (I’m using a minotaur model I found online) turned into what I can only describe as a “spaghetti dragon”

Second problem was that neither my Radeon or NVidia cards were particularly happy with me sending hundreds of matrices to the shader as uniforms. Just to be clear - that didn’t cause any actual errors, just bad behavior. It crashed RenderDoc when I tried to replay it though. I can’t guarantee 100% that wasn’t due to a bug in my own code, but I solved it by creating a (maxBones x 4) 32 bit RGBA float texture and wrote the bone data to that, where each column of the texture contained the matrix for the bone ID matching the column index. As long as you precalculate all the bone transforms and write them to the texture in one go, it works quite nicely. My first implementation updated one pixel of the texture at a time with one matrix row, that actually managed to crash RenderDoc on both my PCs when it tried to replay the rendering (Offhand, the guy who created RenderDoc is awesome and helped me debug the problem I was seeing even though it’s just for a hobby game engine, and even sent the crash data to an AMD engineer since it was technically due to a driver issue, albeit a driver issue that wouldn’t have happened if not for my crappy code. I really can’t speak highly enough of him).

Hopefully this gives you an idea anyway, I’m happy to share some code if you want (but like I said, it’s C#)

Edit: Forgot one thing - I also had to transpose all the matrices coming from Assimp to make them work with my code. Again, may not apply to glm but worth a shot.

Thank you for your answer. I finally manged to fix the problem. The problem was that the bone index vector was not correctly read in the vertex shader.
I needed to use glVertexAttribIPointer instead of glVertexAttribPointer, since the vector of bone indices was a int vector.
However RenderDoc showed me the correct values but the shader interpreted the values differenent.
The demo is no working and I corrected the code.