What better things to do on a rainy weekend than working on one of my beloved sideprojects, eh?
I am trying to bring skeletal animation in my engine with the kind help of ASSIMP and GLM.
I used http://ogldev.atspace.co.uk/www/tutorial38/tutorial38.html as my primary resource, along with:
" title="External link">My understanding may not be perfect, but I do hope that the fundamentals are clear to me (This may still not be the case though!).
First of: The model I use for testing purposes is the one from the video-tutorial above.
I exported it with Blender as a .dae/collada file, resulting in it rendering 90° rotated about the x-axis. Blender's collada exporter does not support
Y_UP
I tried to fix this manually by editing the .dae file and "counter-rotating" but to no avail. I did not want to work on this any longer before the animation would work.
This is how it looks like rendered without the bones:
[attachment=35702:model.jpg]
And this is the abomination that has bones :(:
[attachment=35704:withBones.png]
I am pretty sure I messed up the matrices at some point, given the fact that ASSIMP and glm are basically transposes of each other, but I am not entirely sure, hence I ask for your help!
Onto some code then, I say!
Creation of the model:
// Sample postprocessing
const aiScene *scene = importer.ReadFile(path,
aiProcess_Triangulate | aiProcess_GenNormals | aiProcess_FlipUVs);
if(!scene || scene->mFlags == AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
throw new std::runtime_error("Problem loading model: " + path);
this->inverseModelMatrix = glm::inverse(convertMatrix(scene->mRootNode->mTransformation));
The inverseModelMatrix is used later.
...
std::vector<VertexBoneData> bones = loadBones(mesh, baseVertex);
return Mesh(vertices, indices, textures, bones, baseVertex, baseIndex);
std::vector<VertexBoneData> Model::loadBones(aiMesh *mesh, uint baseVertex)
{
std::vector<VertexBoneData> bones;
bones.resize(mesh->mNumVertices);
for(uint a = 0; a < mesh->mNumBones; a++)
{
uint boneIndex = 0;
std::string boneName(mesh->mBones[a]->mName.data);
if (boneMapping.find(boneName) == boneMapping.end()) {
boneIndex = numBones;
numBones++;
BoneInfo bi;
boneInfo.push_back(bi);
boneMapping[boneName] = boneIndex;
boneInfo[boneIndex].boneOffset = convertMatrix(mesh->mBones[a]->mOffsetMatrix);
}
else {
boneIndex = boneMapping[boneName];
}
for (uint w = 0; w < mesh->mBones[a]->mNumWeights; w++)
{
uint vertexId = baseVertex + mesh->mBones[a]->mWeights[w].mVertexId;
float weight = mesh->mBones[a]->mWeights[w].mWeight;
bones[vertexId].AddBoneData(boneIndex, weight);
}
}
return bones;
}
The mesh looks like this:
mesh.hpp
const static int NUM_BONES_PER_VERTEX = 4;
struct VertexBoneData
{
uint ids[NUM_BONES_PER_VERTEX] = {0};
float weights[NUM_BONES_PER_VERTEX] = {0};
void AddBoneData(uint BoneID, float Weight);
};
struct Vertex
{
glm::vec4 position;
glm::vec4 normal;
glm::vec2 textureCoordinates;
};
struct BoneInfo
{
glm::mat4 boneOffset;
glm::mat4 finalTransformation;
};
class Mesh
{
public:
Mesh(std::vector<Vertex> vertices, std::vector<GLuint> indices, std::vector<GLuint> textures,
std::vector<VertexBoneData> bones, uint baseVertex, uint baseIndex);
void render();
private:
std::vector<Vertex> vertices;
std::vector<GLuint> indices;
std::vector<GLuint> textures;
GLuint vaoId, vboId, eboId;
uint baseVertex, baseIndex = 0;
std::vector<VertexBoneData> bones;
GLuint boneBufferId;
void setup();
// Implementation follows http://www.learnopengl.com/#!Model-Loading/Mesh
Mesh::Mesh(std::vector<Vertex> vertices, std::vector<GLuint> indices, std::vector<GLuint> textures,
std::vector<VertexBoneData> bones, uint baseVertex, uint baseIndex) {
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
this->bones = bones;
this->baseVertex = baseVertex;
this->baseIndex = baseIndex;
this->setup();
}
uint POSITION_LOCATION = 0;
uint NORMAL_LOCATION = 1;
uint TEXTURE_COORDINATES_LOCATION = 2;
uint BONES_LOCATION = 3;
uint BONE_WEIGHT_LOCATION = 4;
void Mesh::setup() {
// Create VAO, VBO and ELEMENTBUFFER
glGenVertexArrays(1, &this->vaoId);
glGenBuffers(1, &this->vboId);
glGenBuffers(1, &this->eboId);
glGenBuffers(1, &this->boneBufferId);
// Bind them all
glBindVertexArray(this->vaoId);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->eboId);
// Prepare vertex buffer
glBindBuffer(GL_ARRAY_BUFFER, this->vboId);
glBufferData(GL_ARRAY_BUFFER, this->vertices.size() * sizeof(Vertex), &this->vertices[0], GL_STATIC_DRAW);
// Set vertex attribute pointers
// 0 = pos
glEnableVertexAttribArray(POSITION_LOCATION);
glVertexAttribPointer(POSITION_LOCATION, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid *) 0);
// 1 = normals
glEnableVertexAttribArray(NORMAL_LOCATION);
glVertexAttribPointer(NORMAL_LOCATION, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid *) offsetof(Vertex, normal));
// 2 = texture coordinates
glEnableVertexAttribArray(TEXTURE_COORDINATES_LOCATION);
glVertexAttribPointer(TEXTURE_COORDINATES_LOCATION, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid *) offsetof(Vertex, textureCoordinates));
// Prepare bone buffer
glBindBuffer(GL_ARRAY_BUFFER, this->boneBufferId);
glBufferData(GL_ARRAY_BUFFER, this->bones.size() * sizeof(VertexBoneData), &this->bones[0], GL_STATIC_DRAW);
// Set bone attributes
// 3 = bones
glEnableVertexAttribArray(BONES_LOCATION);
glVertexAttribIPointer(BONES_LOCATION, 4, GL_INT, sizeof(VertexBoneData), (const GLvoid *) 0);
// 4 = bone weights
glEnableVertexAttribArray(BONE_WEIGHT_LOCATION);
glVertexAttribPointer(BONE_WEIGHT_LOCATION, 4, GL_FLOAT, GL_FALSE, sizeof(VertexBoneData), (const GLvoid *) (NUM_BONES_PER_VERTEX * 4));
// indices
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->eboId);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, this->indices.size() * sizeof(GLuint), &this->indices[0], GL_STATIC_DRAW);
glBindVertexArray(0);
// Unbind Element buffer AFTER VAO (else it's unbound when vao is activated!)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
void Mesh::render() {
glBindVertexArray(this->vaoId);
glDrawElementsBaseVertex(
GL_TRIANGLES,
this->indices.size(),
GL_UNSIGNED_INT,
(void *) (sizeof(uint) * baseIndex),
baseVertex
);
glBindVertexArray(0);
}
void VertexBoneData::AddBoneData(uint BoneID, float Weight) {
for (uint i = 0; i < NUM_BONES_PER_VERTEX; i++) {
if (weights[i] == 0) {
ids[i] = BoneID;
weights[i] = Weight;
return;
}
}
}
Here I want to point out that I did not miss the I in
glVertexAttribIPointer
for the boneIds!
That would be the construction of the mesh with the bones!
Later down the line I want to get the transformation within an animation. So now comes a lot of code, basically like the one from ogldev with a bit more return-typiness.
std::vector<glm::mat4> Model::boneTransform(const std::string &path, float timeInSeconds)
{
glm::mat4 identity;
Assimp::Importer importer;
// Sample postprocessing
const aiScene *scene = importer.ReadFile(path,
aiProcess_Triangulate | aiProcess_GenNormals | aiProcess_FlipUVs);
if(!scene || scene->mFlags == AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
throw new std::runtime_error("Problem loading model: " + path);
aiAnimation *anim = scene->mAnimations[0];
std::vector<glm::mat4> transforms;
if(anim == nullptr) return transforms;
float ticksPerSecond = (float) scene->mAnimations[0]->mTicksPerSecond != 0 ? scene->mAnimations[0]->mTicksPerSecond : 25.0f;
float timeInTicks = timeInSeconds * ticksPerSecond;
float animationTime = fmod(timeInTicks, (float)scene->mAnimations[0]->mDuration);
readNodeHierarchy(animationTime, scene, scene->mRootNode, identity);
transforms.resize(this->numBones);
for(uint i = 0; i < this->numBones; i++) {
transforms[i] = boneInfo[i].finalTransformation;
}
return transforms;
}
aiQuaternion Model::calculateInterpolatedRotation(float AnimationTime, const aiNodeAnim *pNodeAnim)
{
// we need at least two values to interpolate...
if (pNodeAnim->mNumRotationKeys == 1) {
aiQuaternion quat = aiQuaternion(pNodeAnim->mRotationKeys[0].mValue);
return quat;
}
aiQuaternion quat;
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(Factor >= 0.0f && Factor <= 1.0f);
const aiQuaternion& StartRotationQ = pNodeAnim->mRotationKeys[RotationIndex].mValue;
const aiQuaternion& EndRotationQ = pNodeAnim->mRotationKeys[NextRotationIndex].mValue;
aiQuaternion::Interpolate(quat, StartRotationQ, EndRotationQ, Factor);
quat.Normalize();
return quat;
}
void Model::readNodeHierarchy(float animationTime, const aiScene *scene, const aiNode *node, const glm::mat4 &parentTransform)
{
std::string nodeName(node->mName.data);
const aiAnimation* pAnimation = scene->mAnimations[0];
glm::mat4 nodeTransform = convertMatrix(node->mTransformation);
const aiNodeAnim* pNodeAnim = findNodeAnim(pAnimation, nodeName);
if (pNodeAnim)
{
// Interpolate scaling and generate scaling transformation matrix
aiVector3D scaling = calculateInterpolatedScaling(animationTime, pNodeAnim);
glm::vec3 scale = glm::vec3(scaling.x, scaling.y, scaling.z);
glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0f), scale);
// Interpolate rotation and generate rotation transformation matrix
aiQuaternion RotationQ = calculateInterpolatedRotation(animationTime, pNodeAnim);
glm::quat rotation(RotationQ.x, RotationQ.y, RotationQ.z, RotationQ.w);
glm::mat4 rotationMatrix = glm::toMat4(rotation);
// Interpolate translation and generate translation transformation matrix
aiVector3D Translation = calculateInterpolatedPosition(animationTime, pNodeAnim);
glm::vec3 translation = glm::vec3(Translation.x, Translation.y, Translation.z);
glm::mat4 translationMatrix = glm::translate(glm::mat4(1.0f), translation);
// Combine the above transformations
nodeTransform = translationMatrix * rotationMatrix * scaleMatrix;
// TODO Check if inverting multiplication here is correct
//nodeTransform = scaleMatrix * rotationMatrix * translationMatrix;
}
glm::mat4 globalTransformation = parentTransform * nodeTransform;
// TODO Check if inverting multiplication here is correct
//glm::mat4 globalTransformation = nodeTransform * parentTransform;
if (boneMapping.find(nodeName) != boneMapping.end()) {
uint BoneIndex = boneMapping[nodeName];
boneInfo[BoneIndex].finalTransformation = inverseModelMatrix * globalTransformation * boneInfo[BoneIndex].boneOffset;
// TODO Check if inverting multiplication here is correct
// boneInfo[BoneIndex].finalTransformation = boneInfo[BoneIndex].boneOffset * globalTransformation * inverseModelMatrix;
}
for (uint i = 0 ; i < node->mNumChildren ; i++) {
readNodeHierarchy(animationTime, scene, node->mChildren[i], globalTransformation);
}
}
const aiNodeAnim* Model::findNodeAnim(const aiAnimation* pAnimation, const std::string nodeName)
{
for (uint c = 0 ; c < pAnimation->mNumChannels ; c++) {
const aiNodeAnim* pNodeAnim = pAnimation->mChannels[c];
if (std::string(pNodeAnim->mNodeName.data) == nodeName) {
return pNodeAnim;
}
}
return nullptr;
}
aiVector3D Model::calculateInterpolatedScaling(float animationTime, const aiNodeAnim *animatedNode)
{
if (animatedNode->mNumScalingKeys == 1) {
return animatedNode->mScalingKeys[0].mValue;
}
uint index = FindScaling(animationTime, animatedNode);
uint nextIndex = (index + 1);
assert(nextIndex < animatedNode->mNumScalingKeys);
float deltaTime = (float)(animatedNode->mScalingKeys[nextIndex].mTime - animatedNode->mScalingKeys[index].mTime);
float factor = (animationTime - (float)animatedNode->mScalingKeys[index].mTime) / deltaTime;
assert(factor >= 0.0f && factor <= 1.0f);
const aiVector3D& Start = animatedNode->mScalingKeys[index].mValue;
const aiVector3D& End = animatedNode->mScalingKeys[nextIndex].mValue;
aiVector3D Delta = End - Start;
aiVector3D end = Start + factor * Delta;
return end;
}
uint Model::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;
}
uint Model::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;
}
aiVector3D Model::calculateInterpolatedPosition(float animationTime, const aiNodeAnim *pNodeAnim)
{
if (pNodeAnim->mNumPositionKeys == 1) {
return pNodeAnim->mPositionKeys[0].mValue;
}
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 aiVector3D& Start = pNodeAnim->mPositionKeys[PositionIndex].mValue;
const aiVector3D& End = pNodeAnim->mPositionKeys[NextPositionIndex].mValue;
aiVector3D Delta = End - Start;
return Start + Factor * Delta;
}
uint Model::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;
}
In there I think I screwed up with the matrices. You can see the comments where I am unsure, but maybe I missed a line altogether.
Oh, convertMatrix is important to! It returns the matrix transposed!
/**
*
* @param aiMat
* @return transposed version of the aiMat to fit into glm's style of doing things
*/
glm::mat4 Model::convertMatrix(const aiMatrix4x4 &aiMat)
{
return {
aiMat.a1, aiMat.b1, aiMat.c1, aiMat.d1,
aiMat.a2, aiMat.b2, aiMat.c2, aiMat.d2,
aiMat.a3, aiMat.b3, aiMat.c3, aiMat.d3,
aiMat.a4, aiMat.b4, aiMat.c4, aiMat.d4
};
}
Maybe that one is wrong? At least in combination with the rest it's important.
Now onto the shader and the rendering:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 textureCoordinates;
layout (location = 3) in ivec4 boneIds;
layout (location = 4) in vec4 boneWeights;
out vec4 vertexPosition;
out vec4 vertexNormal;
out vec2 vertexTextureCoordinates;
uniform mat4 normalMatrix;
uniform mat4 modelViewProjectionMatrix;
uniform mat4 modelMatrix;
const int MAX_BONES = 100;
uniform mat4 bones[MAX_BONES];
void main()
{
mat4 boneTransform = bones[boneIds[0]] * boneWeights[0];
boneTransform += bones[boneIds[1]] * boneWeights[1];
boneTransform += bones[boneIds[2]] * boneWeights[2];
boneTransform += bones[boneIds[3]] * boneWeights[3];
vec4 posL = boneTransform * vec4(position, 1);
gl_Position = modelViewProjectionMatrix * posL;
vec4 normalL = boneTransform * vec4(normal, 0);
vertexNormal = (modelMatrix * normalL);
vertexPosition = (modelMatrix * posL);
}
Pretty much like ogldev's version. I initialize the
bones
uniform-array like this:
for(unsigned int a = 0; a < 100; a++) {
std::stringstream name;
name << "bones[" << a << "]";
attributes.boneLocations[a] = glGetUniformLocation(attributes.shaderId, name.str().c_str());
}
Rendering looks like this:
std::vector<glm::mat4> transforms = this->appearance.model->boneTransform("../res/models/demo_character_tm.dae", animationTime);
for(uint index = 0; index < transforms.size(); index++) {
//glm::mat4 mat = glm::transpose(transforms[index]);
glm::mat4 mat = transforms[index];
glUniformMatrix4fv(
this->appearance.boneLocations[index],
1,
GL_FALSE, // TODO WAS GL_TRUE
//glm::value_ptr(transforms[index])
//&transforms[index][0][0]
glm::value_ptr(mat)
);
}
Again, not sure if it makes sense to set GL_TRUE for transposed (looks like hell then, even more!)
I know this is a lot of code... I tried to provide only the relevant parts, but the problem for me is, that the whole bone-rendering/animation stuff seems so overwhelming that there simply is a lot of code involved.
If you need any more code, please let me know.
Thanks to anyone having a look at this. I am really grateful for any hint you can give me!
Enjoy the rest of your weekends everybody!