ASSIMP Skeletal Animation Tutorial #1 – Vertex Weights and Indices

NOTE: While this is Tutorial #1, there is an Intro to this tutorial series with information you might find useful, here is the link to said intro.


DISCLAIMER: There is also an important disclaimer you might want to read about how to contact me and how I’m not the most knowledgeable person on this topic, just a guy who wanted to help people who faced the same problems.

Here is a download link to the model I use in this tutorial series, for you to follow along with if you please: bit.ly/skelanimmodel


The first, and perhaps the easiest step in implementing skeletal animation is to get the Vertex Weights and Vertex Indices working correctly.

But what exactly is a Vertex Weight? And what the hell is a Vertex Index?

To understand that we must first understand how skeletal animation works, now listen, I might not be the best at explaining this, so please look at the links in the intro tutorial in case my explanation is not clear enough.

Here goes.

Skeletal animation works by assigning each vertex in a mesh a bone to follow, and the amount to follow it by. Each bone has an ID, and each vertex has the ID of the bone it should follow — that’s what the Vertex Indices are. They are simply the piece of information that tell each individual vertex which bone to follow.

The amount to follow it by is exactly what it sounds like — it tells the vertex the amount to move with its corresponding bone — this is known as the vertex weight. Think of it as the influence each bone has on a certain vertex.

But why would a weight be necessary? Can’t we just assume the weight of every vertex is one?

Well…

…no.

You see, what you don’t know is that, in computer graphics, each vertex usually has more than one Vertex Index, and more than one weight. The standard number of Indices and Weights for each vertex is 4 — because you usually need more than one, but four usually suffices.


So cut to the chase, what do we have to do?

First we have to load up the per-vertex bone information when loading the mesh, then

we have to pass these into the vertex shader, then

we have to load up the meshes’ bone information when loading the mesh, then

we have to calculate the bones’ transforms outside the vertex shader, and finally,

we have to pass these transforms into the vertex shader so that the vertices are correctly transformed.

This tutorial will only cover the first two, loading the vertices’ bone data, and passing that into the vertex shader.


Quickly though, just let me explain the basic architecture of my game engine so you can figure out how to adjust this code to fit your own.

To create a model, I first create a sceneLoader object, which takes in a filename and loads up a certain file to a series of Meshes. The sceneLoader contains all of the data necessary to create a mesh, the VAO (Vertex Array Object), the VBOs (Vertex Buffer Objects) with the vertex positions, the UV coordinates, the normals, etc.

Each Mesh in the sceneLoader then copies the data from the sceneLoader to become its own independent Mesh, with the same base information as the sceneLoader. Essentially, the new Meshes just become whatever the sceneLoader loaded. Each Mesh can only contain one Material, so multiple Meshes are required to create a full object with multiple materials in my game. This object, funnily enough, is known as a GameObject.

A GameObject just contains a vector of Meshes and draws them each, one after the other, every single render() call. It also contains physics information, and some other things, but those aren’t overly relevant to this tutorial so I won’t go into those in depth.

The sceneLoader class and the Mesh class are both closely modeled after thecplusplusguy‘s implementation of ASSIMP in his own engine. My sceneLoader class is virtually identical and contains many of the same functions, which I will be referring to later on — but I will try to explain their purpose as I mention them here.

Here is a link to his ASSIMP tutorial, in case you need to better understand my horrific, poorly written gibberish well-structured, original code.

Anyways, here we go…


STEP 1: LOAD UP THE VERTICES’ BONE INFORMATION WHEN LOADING THE MESH

When initialized, the sceneLoader class goes through each aiMesh ASSIMP loaded and converts that to a Mesh that we can use later on.  It does this by going through each of the aiMesh‘s vertices, and copying their position, their normal, their UVs, etc. into a format that can be used by my Mesh class. It does all of this in a function called processMesh(), inside of the recursiveProcess() function — these are virtually identical to thecplusplusguy’s functions and he goes in depth as to what they do in his tutorial. Sorry for not going in depth here, but that isn’t the focus of this tutorial.

Each Mesh class is initialized with an Array of Vertex. Each Vertex has space for the aforementioned position, normal, UVs, the usual.

The first thing you have to do, if you have a similar class, is to add space for four IDs, and four Weights.

Here is my Vertex class for reference:

static class Vertex
{
     public:
           float posi[3];

           float texCoord0[2];

           float normal[3];
           float tangent[3];
           float color[4];

           float weight[4];
           unsigned int id[4];
}

I left out all of the constructors because they all do essentially the same thing, set all of the values to zero.

Anyways, the sceneLoader copies the position, the UVs, the normals, etc., and what we have to do is set it so that it also copies the Bone IDs and the Bone Weights for each vertex into the array of Vertex called data.

Here is how we do that: (this is still the processMesh(), for reference)

int WEIGHTS_PER_VERTEX = 4;
//DEFINING HOW MAY WEIGHTS PER VERTEX,
  //BUT ALSO HOW MANY INDICES PER VERTEX.

//FIRST YOU HAVE TO CREATE TWO ARRAYS,
//THESE ARE WHERE WE'LL STORE ALL OF THE VERTEX DATA.
int boneArraysSize = mesh->mNumVertices*WEIGHTS_PER_VERTEX;
     //THIS IS DONE BECAUSE EACH VERTEX HAS SPACE FOR 4 WEIGHTS AND 4 INDICES.
std::vector<int> boneIDs;
    boneIDs.resize(boneArraysSize);
     //HERE SPACE FOR 4 INDICES PER VERTEX IS BEING ALLOCATED
std::vector<float> boneWeights;
    boneWeights.resize(boneArraysSize);
     //HERE SPACE FOR 4 WEIGHTS PER VERTEX IS BEING ALLOCATED


//HERE WE FILL THE ARRAYS, (below)
//WE DO THIS BY CYCLING THROUGH EACH BONE AND ITS DATA,
//AND COPYING IT INTO ITS RESPECTIVE ARRAY.
for(int i=0;i<mesh->mNumBones;i++)
{
//(above) NOTE THAT mesh IS NOT OF TYPE Mesh,
//IT IS A POINTER TO THE CURRENT MESH, OF TYPE aiMesh

     aiBone* aiBone = mesh->mBones[i]; //CREATING A POINTER TO THE CURRENT BONE
     //IT'S IMPORTANT TO NOTE THAT i IS JUST THE ID OF THE CURRENT BONE.

     for(int j=0;j<aiBone->mNumWeights;j++)
     {
          aiVertexWeight weight = aiBone->mWeights[j];

          //THIS WILL TELL US WHERE, IN OUR ARRAY, TO START READING THE VERTEX'S WEIGHTS
          unsigned int vertexStart = weight.mVertexId * WEIGHTS_PER_VERTEX;

          //HERE WE'LL ACTUALLY FILL THE ARRAYS, WITH BOTH INDICES AND WEIGHTS.
          for(int k=0;k<WEIGHTS_PER_VERTEX;k++)
          {
               if(boneWeights.at(vertexStart+k)==0)
               {
               //(above) IF THE CURRENT BONE WEIGHT IS EQUAL TO 0,
               //THEN IT HASN'T BEEN FILLED YET WITH AN ACTUAL WEIGHT.
               boneWeights.at(vertexStart+k) = weight.mWeight;
               boneIDs.at(vertexStart+k) = i; //REMEMBER THAT i IS JUST THE ID OF THE CURRENT BONE.

               //NOTE THAT data IS JUST AN ARRAY OF TYPE Vertex, WHERE I STORE ALL OF THE VERTEX INFO.
               //EACH Vertex CLASS HAS SPACE FOR A POSITION, A UV, A NORMAL, AND 4 INDICES, AND 4 WEIGHTS.
               //EACH Mesh IS THEN CREATED WITH THIS THIS ARRAY OF Vertex (THIS ARRAY BEING data).

               data.at(weight.mVertexId).id[k] = i;
                 //SETTING THE ID
                 //AT k, OF
                 //THE VERTEX AT THIS WEIGHT'S ID,
                 //TO THE CURRENT BONE ID.

               data.at(weight.mVertexId).weight[k] = weight.mWeight;
                 //SETTING THE WEIGHT
                 //AT k, OF
                 //THE VERTEX AT THIS WEIGHT'S ID,
                 //TO THIS WEIGHT'S WEIGHT.
               break;
               }
          }
     }
}

At the end of the processMesh() function are these two lines, which first initialize a new object of type Mesh with the data copied over from the aiMeshes loaded, and then push this new object into a member variable vector (called meshes) of the current sceneLoader class.

Mesh tmpMesh(data,indices,tempMat,error);
meshes->push_back(tmpMesh);

The above constructor is here in detail, with the newly added parts in bold:

verts = vertices;
 
 numVertices = vertices.size(); 
 numIndices = inds.size();
 indices = inds;
 //texmex = textures;
 
 mat = tempMat;
 
 
 
 std::vector<float> vecVerts;
 std::vector<float> vecNorms;
 std::vector<float> vecTangs;
 std::vector<float> vecCoords0;

 std::vector<float> vecWeights;
 std::vector<GLint> vecIDs;


 for(int i = 0; i < numVertices; i++)
 {
      vecVerts.push_back(vertices[i].posi[0]);
      vecVerts.push_back(vertices[i].posi[1]);
      vecVerts.push_back(vertices[i].posi[2]);

      vecNorms.push_back(vertices[i].normal[0]);
      vecNorms.push_back(vertices[i].normal[1]);
      vecNorms.push_back(vertices[i].normal[2]);

      vecTangs.push_back(vertices[i].tangent[0]);
      vecTangs.push_back(vertices[i].tangent[1]);
      vecTangs.push_back(vertices[i].tangent[2]);

      vecCoords0.push_back(vertices[i].texCoord0[0]);
      vecCoords0.push_back(vertices[i].texCoord0[1]);
 

      vecWeights.push_back(vertices[i].weight[0]);
      vecWeights.push_back(vertices[i].weight[1]);
      vecWeights.push_back(vertices[i].weight[2]);
      vecWeights.push_back(vertices[i].weight[3]);

      vecIDs.push_back(vertices[i].id[0]);
      vecIDs.push_back(vertices[i].id[1]);
      vecIDs.push_back(vertices[i].id[2]);
      vecIDs.push_back(vertices[i].id[3]);
 }
 



 glGenVertexArrays(1,&vertexArrayObject);
 glBindVertexArray(vertexArrayObject);
 
 glGenBuffers(1,&VBO_Pos);
 glBindBuffer(GL_ARRAY_BUFFER,VBO_Pos);
 glBufferData(GL_ARRAY_BUFFER,numVertices*3*sizeof(GLfloat),&vecVerts[0],GL_STATIC_DRAW);

 glGenBuffers(1,&VBO_UVs);
 glBindBuffer(GL_ARRAY_BUFFER,VBO_UVs);
 glBufferData(GL_ARRAY_BUFFER,numVertices*2*sizeof(GLfloat),&vecCoords0[0],GL_STATIC_DRAW);

 glGenBuffers(1,&VBO_Nor);
 glBindBuffer(GL_ARRAY_BUFFER,VBO_Nor);
 glBufferData(GL_ARRAY_BUFFER,numVertices*3*sizeof(GLfloat),&vecNorms[0],GL_STATIC_DRAW);
 
 glGenBuffers(1,&VBO_Tan);
 glBindBuffer(GL_ARRAY_BUFFER,VBO_Tan);
 glBufferData(GL_ARRAY_BUFFER,numVertices*3*sizeof(GLfloat),&vecTangs[0],GL_STATIC_DRAW);

      glGenBuffers(1,&VBO_IDs);
      glBindBuffer(GL_ARRAY_BUFFER,VBO_IDs);
      glBufferData(GL_ARRAY_BUFFER,numVertices*4*sizeof(GLint),vecIDs[0],GL_STATIC_DRAW);

      glGenBuffers(1,&VBO_Weights);
      glBindBuffer(GL_ARRAY_BUFFER,VBO_Weights);
      glBufferData(GL_ARRAY_BUFFER,numVertices*4*sizeof(GLfloat),&vecWeights[0],GL_STATIC_DRAW);

 
 glGenBuffers(1,&indexBufferObject);
 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,indexBufferObject);
 glBufferData(GL_ELEMENT_ARRAY_BUFFER,inds.size()*sizeof(unsigned int),&inds[0],GL_STATIC_DRAW);
 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
 


 glBindVertexArray(0);

There we go! Those are all of the steps necessary to load the Bone Weights and IDs into our Mesh class!

Congratulations! You still have a long long long way to go. Yay!

No time to waste, however, on to step 2!


STEP 2: PASS THE VERTEX BONE DATA INTO THE SHADERS

This step is considerably shorter than the last, so you really have nothing to worry about.

In our Mesh’s Draw() function, we just have to add a couple lines of code to send our data to the shader.

glUseProgram(shaderID);
 
 glBindVertexArray(vertexArrayObject);
 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
 
 glBindBuffer(GL_ARRAY_BUFFER,VBO_Pos);
 unsigned int vpos = glGetAttribLocation(shaderID,"s_vPosition");
 glVertexAttribPointer(vpos,3,GL_FLOAT,GL_FALSE,0,0);
 
 glBindBuffer(GL_ARRAY_BUFFER,VBO_UVs);
 unsigned int vcoords0 = glGetAttribLocation(shaderID,"s_vCoords0");
 glVertexAttribPointer(vcoords0,2,GL_FLOAT,GL_FALSE,0,0);
 
 glBindBuffer(GL_ARRAY_BUFFER,VBO_Nor);
 unsigned int vnorms = glGetAttribLocation(shaderID,"s_vNormals");
 glVertexAttribPointer(vnorms,3,GL_FLOAT,GL_FALSE,0,0);
 
 glBindBuffer(GL_ARRAY_BUFFER,VBO_Tan);
 unsigned int vtangs = glGetAttribLocation(shaderID,"s_vTangents");
 glVertexAttribPointer(vtangs,3,GL_FLOAT,GL_FALSE,0,0);
 
 
 
 glBindBuffer(GL_ARRAY_BUFFER,VBO_IDs);
 unsigned int vbids = glGetAttribLocation(shaderID,"s_vIDs");
 glVertexAttribIPointer(vbids,4,GL_INT,0,0);
 
 glBindBuffer(GL_ARRAY_BUFFER,VBO_Weights);
 unsigned int vweights = glGetAttribLocation(shaderID,"s_vWeights");
 glVertexAttribPointer(vweights,4,GL_FLOAT,GL_TRUE,0,0);
 
 
 

 glEnableVertexAttribArray(vpos);
 glEnableVertexAttribArray(vcoords0);
 glEnableVertexAttribArray(vnorms);
 glEnableVertexAttribArray(vtangs);

 glEnableVertexAttribArray(vbids);
 glEnableVertexAttribArray(vweights);

And now for the shader code, note that for now this is the most basic it can be (and it doesn’t have any skeletal animation capabilities yet, it just showcases the weights and indices).

Here is the Vertex Shader (rigged_ambient.vsh):

#version 140

uniform mat4 MMatrix;
uniform mat4 VPMatrix;

in vec3 s_vPosition;
in vec2 s_vCoords0;

in vec3 s_vNormals;
in vec3 s_vTangents;

out vec2 coords0;

const int MAX_BONES = 100;
uniform mat4 gBones[MAX_BONES];

in vec4 s_vWeights;
in ivec4 s_vIDs;

out vec4 we;
out vec4 id;

void main ()
{
      gl_Position = VPMatrix * MMatrix * (vec4(s_vPosition,1.0));

      we = s_vWeights;
      id = s_vIDs;
 
      coords0 = s_vCoords0.xy;
}

Here is the Fragment Shader (rigged_ambient.fsh):

#version 140

uniform mat4 MMatrix;
uniform mat4 VPMatrix;

in vec2 coords0;
in vec4 we;
in vec4 id;

out vec4 fColor;

uniform sampler2D texUnit;

void main ()
{
      //vec4 texcolor = texture2D(texUnit,coords0);
      vec4 weightsColor = vec4(we.xyz,1.0);
 
      fColor = weightsColor;
}

And, finally, here is the rigged mesh displaying the vertex weights!

Tutorial Weights

(it’s supposed to be a monster)

Those are the vertex weights, correctly displayed on screen! Yay!

That is it for this tutorial ladies and gentlemen, thank you very very much for reading!

Please contact me if you have any doubts, or if I wasn’t clear enough, or if you flat out catch a mistake I made, I love hearing from you and you are the reason I do this.

Thanks again, and see you in the next tutorial!

Previous Tutorial    Next Tutorial

Leave a comment