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).