Tutorials
Current series: OpenGL 3.3
(Return to list of OpenGL 3.3 tutorials)

20.) Assimp Model Loading

Hello guys! This tutorial took me the longest time to write. Not because it's extremely long or complicated - it’s because I’m doing this in my free time only, and that’s the concern – since I’m an employed person for quite some time and student as well, you just run out of mana to write tutorials after coding whole day in work and not now (it’s holidays), but previously also for university. So that’s the problem. Anyway, I’m trying to make these tutorials and I hope you’re fine with my unreliable scheduling J

This tutorial is about Assimp. It’s a free portable library that works with most model formats that are being used these days. When I wrote tutorial for .obj loading, I knew it’s probably useless, but I wrote it anyway, because loading .obj files is easy anyway so it didn’t took me much time. I was lazy to study some library, and I knew there is something like this out there. So this tutorial will take you through basics of loading models through Assimp library. I downloaded new sample models, like wolf or some shit, because SpongeBob and Thor are now deprecated :P So let’s dive in.

Downloading Assimp

The first step to use Assimp in our application is to download it from its webpage www.pjurko.sk. Installing is rather easy, but be sure to choose Install SDKs. After it, we need to add include paths and library paths to Visual Studio, so that we are able to include them in our application. The include path to your include folder is ASSIMP_INSTALLATION_FOLDER\include and library path is ASSIMP_INSTALLATION_FOLDER\lib. The installation path in Visual Studio 2008 are added in Tools/Options/VC++ directiores, in case you forgot where to find it (or look on first tutorials of mine, there are screenshots of these dialogs in Visual Studio).

Going in

After we did the first necessary step to get Assimp library running, we can include it now in our project. We will do this by adding these lines to our cpp file:


#pragma comment(lib, \"assimp.lib\")

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

As is my custom to wrap all things up by myself, I will create a class for Assimp Model, that will handle everything from loading to rendering just by calling few of its functions. Here is what the model class will look like:


class CAssimpModel
{
public:
   bool LoadModelFromFile(char* sFilePath);
   bool LoadModelFromFileBumpMap(char* sFilePath, char* sColorMap, char* sNormalMap);

   static void FinalizeVBO();
   static void BindModelsVAO();

   void RenderModel();
   void RenderModelBumpMap(CShaderProgram* spProgram);

   CAssimpModel();
private:
   bool bLoaded;
   static CVertexBufferObject vboModelData;
   static UINT uiVAO;
   static vector<CTexture> tTextures;
   vector<int> iMeshStartIndices;
   vector<int> iMeshSizes;
   vector<int> iMaterialIndices;
   int iNumMaterials;
};

You can tell what each function is used for by its name. The only concern may be the function FinalizeVBO, which I will explain later. You can also see some static members in this class and you may ask why. It’s because I decided to have one VBO and VAO for all model data – all models that Assimp Library will load will be stored in this VBO, along with data at what index and how many vertices each model has. So think of it as of a global storage (but global only for CAssimpModel class and every its instance) of model data. You know, creating multiple VBOs for multiple models would lead to performance loss, since constant swapping between different VBOs and VAOs is kinda expensive. So that’s the main idea how my model loader works. Let’s now examine loading of a model and the structures Assimp uses to provide us with model data.

Loading Assimp models

Here is how whole loading function looks like:


bool CAssimpModel::LoadModelFromFile(char* sFilePath)
{
   if(vboModelData.GetBufferID() == 0)
   {
      vboModelData.CreateVBO();
      tTextures.reserve(50);
   }
   Assimp::Importer importer;
   const aiScene* scene = importer.ReadFile( sFilePath, 
      aiProcess_CalcTangentSpace       | 
      aiProcess_Triangulate            |
      aiProcess_JoinIdenticalVertices  |
      aiProcess_SortByPType);

   if(!scene)
   {
      MessageBox(appMain.hWnd, "Couldn't load model ", "Error Importing Asset", MB_ICONERROR);
      return false;
   }

   const int iVertexTotalSize = sizeof(aiVector3D)*2+sizeof(aiVector2D);

   int iTotalVertices = 0;

   FOR(i, scene->mNumMeshes)
   {
      aiMesh* mesh = scene->mMeshes[i];
      int iMeshFaces = mesh->mNumFaces;
      iMaterialIndices.push_back(mesh->mMaterialIndex);
      int iSizeBefore = vboModelData.GetCurrentSize();
      iMeshStartIndices.push_back(iSizeBefore/iVertexTotalSize);
      FOR(j, iMeshFaces)
      {
         const aiFace& face = mesh->mFaces[j];
         FOR(k, 3)
         {
            aiVector3D pos = mesh->mVertices[face.mIndices[k]];
            aiVector3D uv = mesh->mTextureCoords[0][face.mIndices[k]];
            aiVector3D normal = mesh->HasNormals() ? mesh->mNormals[face.mIndices[k]] : aiVector3D(1.0f, 1.0f, 1.0f);
            vboModelData.AddData(&pos, sizeof(aiVector3D));
            vboModelData.AddData(&uv, sizeof(aiVector2D));
            vboModelData.AddData(&normal, sizeof(aiVector3D));
         }
      }
      int iMeshVertices = mesh->mNumVertices;
      iTotalVertices += iMeshVertices;
      iMeshSizes.push_back((vboModelData.GetCurrentSize()-iSizeBefore)/iVertexTotalSize);
   }
   iNumMaterials = scene->mNumMaterials;

   vector<int> materialRemap(iNumMaterials);

   FOR(i, iNumMaterials)
   {
      const aiMaterial* material = scene->mMaterials[i];
      int a = 5;
      int texIndex = 0;
      aiString path;  // filename

      if(material->GetTexture(aiTextureType_DIFFUSE, texIndex, &path) == AI_SUCCESS)
      {
         string sDir = GetDirectoryPath(sFilePath);
         string sTextureName = path.data;
         string sFullPath = sDir+sTextureName;
         int iTexFound = -1;
         FOR(j, ESZ(tTextures))if(sFullPath == tTextures[j].GetPath())
         {
            iTexFound = j;
            break;
         }
         if(iTexFound != -1)materialRemap[i] = iTexFound;
         else
         {
            CTexture tNew;
            tNew.LoadTexture2D(sFullPath, true);
            materialRemap[i] = ESZ(tTextures);
            tTextures.push_back(tNew);
         }
      }
   }

   FOR(i, ESZ(iMeshSizes))
   {
      int iOldIndex = iMaterialIndices[i];
      iMaterialIndices[i] = materialRemap[iOldIndex];
   }

   return bLoaded = true;
}

I will go through it part by part, so that you can understand my code a little or take some ideas from it. The first few lines of codes:


if(vboModelData.GetBufferID() == 0)
{
   vboModelData.CreateVBO();
   tTextures.reserve(50);
}

Just check, if the vertex buffer object has been created and if not, it creates it. This happens only if the function is called for the first time, but since all these data are internal and static, i.e. shared among all instances, the next model we’ll try to load will just add to the created VBO.

The whole assimp loading process is really difficult. Extremely difficult. So difficult, that it takes unbelievable ONE line of code :D But this line is long, so I split it into several lines :


Assimp::Importer importer;
const aiScene* scene = importer.ReadFile( sFilePath, 
                               aiProcess_CalcTangentSpace       | 
                               aiProcess_Triangulate            |
                               aiProcess_JoinIdenticalVertices  |
                               aiProcess_SortByPType);

if(!scene)
{
   MessageBox(appMain.hWnd, "Couldn't load model ", "Error Importing Asset", MB_ICONERROR);
   return false;
}

First parameter is file path, second one tells Assimp that we want to calculate tangent space (more about this will be in bump mapping tutorial once, we don’t need it now but we’ll calculate it anyway), we follow with triangulation parameter – we want our data to come to us as triangles, join identical vertices does some optimization in considering different vertices with same coordinates to be the same, and the last one tells us, how to sort data that have been read internal. Assimp_SortByPType will sort the polygon data by polygons – it means first there would be lines, then triangles, then quads and so on. But since we chose our data to be triangulated, it really isn’t much of an interest for us.

Next few lines are very important, as they convert assimp data to data usable in our vertex buffer object and for rendering:


const int iVertexTotalSize = sizeof(aiVector3D)*2+sizeof(aiVector2D);

int iTotalVertices = 0;

FOR(i, scene->mNumMeshes)
{
   aiMesh* mesh = scene->mMeshes[i];
   int iMeshFaces = mesh->mNumFaces;
   iMaterialIndices.push_back(mesh->mMaterialIndex);
   int iSizeBefore = vboModelData.GetCurrentSize();
   iMeshStartIndices.push_back(iSizeBefore/iVertexTotalSize);
   FOR(j, iMeshFaces)
   {
      const aiFace& face = mesh->mFaces[j];
      FOR(k, 3)
      {
         aiVector3D pos = mesh->mVertices[face.mIndices[k]];
         aiVector3D uv = mesh->mTextureCoords[0][face.mIndices[k]];
         aiVector3D normal = mesh->mNormals[face.mIndices[k]];
         vboModelData.AddData(&pos, sizeof(aiVector3D));
         vboModelData.AddData(&uv, sizeof(aiVector2D));
         vboModelData.AddData(&normal, sizeof(aiVector3D));
      }
   }
   int iMeshVertices = mesh->mNumVertices;
   iTotalVertices += iMeshVertices;
   iMeshSizes.push_back((vboModelData.GetCurrentSize()-iSizeBefore)/iVertexTotalSize);
}

We start by calculating constant vertex size in bytes. Every vertex includes one position, one texture coordinate and one normal, taking up 2*sizeof(vector3d) for position and normal plus once size of vector2d for texture coordinate. This value is used for some other calculations. Now, we’re going to process data. Assimp scene consists of several meshes. Think of mesh as of some logical part of model, that’s fine to have separately within model. For example, wolf “scene” consists of several meshes – wolf’s head, left fron leg, right front leg etc. They all sum up to create a wolf. So we have to pass through all meshes within the scene (model), and find out how many vertices it has, which material it uses and so on. Since every mesh can have different material (in our case, material is still only a texture), we need to remember material index and mesh size for every mesh in order to render it properly.

We continue by adding the vertex, texture coordinate, and normal data to our VBO. We better check if model has normals (sometimes it doesn’t necessarily have, so we’d better check it to prevent some random program crashes), the position and texture coordinate of vertex should be there (maybe there is a chance that they’re not there, but whatever :D ). Mesh size is the number of vertices in that mesh, so we simply put it there. Next step is processing material data:

Materials

Loading of material is done in these (maybe) few lines:


vector<int> materialRemap(iNumMaterials);

FOR(i, iNumMaterials)
{
   const aiMaterial* material = scene->mMaterials[i];
   int a = 5;
   int texIndex = 0;
   aiString path;  // filename

   if(material->GetTexture(aiTextureType_DIFFUSE, texIndex, &path) == AI_SUCCESS)
   {
      string sDir = GetDirectoryPath(sFilePath);
      string sTextureName = path.data;
      string sFullPath = sDir+sTextureName;
      int iTexFound = -1;
      FOR(j, ESZ(tTextures))if(sFullPath == tTextures[j].GetPath())
      {
         iTexFound = j;
         break;
      }
      if(iTexFound != -1)materialRemap[i] = iTexFound;
      else
      {
         CTexture tNew;
         tNew.LoadTexture2D(sFullPath, true);
         materialRemap[i] = ESZ(tTextures);
         tTextures.push_back(tNew);
      }
   }
}

FOR(i, ESZ(iMeshSizes))
{
   int iOldIndex = iMaterialIndices[i];
   iMaterialIndices[i] = materialRemap[iOldIndex];
}

First line is material remap vector. All it does is that it maps indices of materials of our model being loaded to the global materials. Again, we have a static vector of materials, and everytime we encounter a new material (so far only texture, but still), we load it and add it to our global data. Because there may be several models using same material (texture), we won’t be loading the same texture for every model using, that would be really stupid, we just check the path of a texture against textures already loaded and if we find we have already loaded it, we just map our material to the position in our global materials. So we map local model materials (number from 0 to Number of Materials in the model -1) to the global materials. And whenever we encounter a new texture, we just load it and add it to our list of global materials. Simple as that.

And that’s it! We’ve done the loading of our model and we should proceed with rendering. But before that, I’ll show you the FinalizeVBO function:


void CAssimpModel::FinalizeVBO()
{
   glGenVertexArrays(1, &uiVAO);
   glBindVertexArray(uiVAO);
   vboModelData.BindVBO();
   vboModelData.UploadDataToGPU(GL_STATIC_DRAW);
   // Vertex positions
   glEnableVertexAttribArray(0);
   glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 2*sizeof(aiVector3D)+sizeof(aiVector2D), 0);
   // Texture coordinates
   glEnableVertexAttribArray(1);
   glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2*sizeof(aiVector3D)+sizeof(aiVector2D), (void*)sizeof(aiVector3D));
   // Normal vectors
   glEnableVertexAttribArray(2);
   glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 2*sizeof(aiVector3D)+sizeof(aiVector2D), (void*)(sizeof(aiVector3D)+sizeof(aiVector2D)));
}

So what it does is to finalize our VBO for rendering models, i.e. it uploads the collected data to GPU. We must call this function after we loaded all of the models and we aren’t going to load anymore. Let’s get into the rendering part, that’s gonna be easy now, after we polished loaded data to fit our needs.

Rendering

Rendering things now is just a matter of making few OpenGL calls. First, a check if model was properly loaded is done. Then, we go through every mesh of model, apply its associated texture, and then render mesh with appropriate starting index and number of vertices:


void CAssimpModel::RenderModel()
{
   if(!bLoaded)return;
   int iNumMeshes = ESZ(iMeshSizes);
   FOR(i, iNumMeshes)
   {
      int iMatIndex = iMaterialIndices[i];
      tTextures[iMatIndex].BindTexture();
      glDrawArrays(GL_TRIANGLES, iMeshStartIndices[i], iMeshSizes[i]);
   }
}

Result

This is how our wolfs look like:

And that’s it! We’ve created model loader that can handle most file formats being used today and also its rendering. There are still many things to improve, like improving material rendering with another properties, adding bump-maps (many models do have normal maps included) and such stuff. But we’ll cover this later, for now it’s enough. Also, I made a small change in shaders.cpp, which eases the need for setting model and normal matrix it once, merging it into one function.

Next tutorial is going to discuss terrain programming and multilayered texturing on it. I already have programmed it in another project of mine, so I just need to extract it and make a quick tutorial of it. I hope I will keep this promiese at least once .

Also, recently my brother released a music video here in Slovakia, so you can have a look if you have time, this is a small advertisement inside my tutorials .



Download (26.92 MB)
1616 downloads. 12 comments

Name:

E-mail:
(Optional)
Entry:

Enter the text from image:



Smileys




Anon on 31.10.2013 19:58:02
the fuck is ESZ?
omarSSelim on 01.08.2014 00:42:11
I believe he means for(int j = 0 ; j < tTextures.zie(); j++)
{
if( .. )
.......
}
Michal Bubnar (michalbb1@gmail.com) on 02.08.2014 10:36:21
ESZ is just my custom macro. ESZ(parameter) expands to int(parameter.size()). I explained the use of my macros in some of the first tutorials, it's just my custom coding style, I should have maybe leave it at least for my tutorials, but I decided to leave it as it is and thus I use these macros in every of my tutorials. But it's just shorthand for many really common things.
wt9901 (wi11berto@yahoo.co.uk) on 14.10.2013 07:48:47
Hey im having a problem with adding the texture it just shades the model in a single colour, any ideas on how to fix this :)
Michal Bubnar (michalbb1@gmail.com) on 15.10.2013 14:02:44
Well, hard to say just like that, I would need to see the code or something. Contact me via e-mail, so I can help you better.
Fabian (fabianorue@gmail.com) on 04.09.2013 16:49:20
I really appreciate the dedication to explain and create tutorials and so wanted to say thanks :)

I hope for new tutorials :) greetings
Michal Bubnar (michalbb1@gmail.com) on 04.09.2013 19:24:29
Thanks bro I appreciate that, although I guess I could have already had created more stuff, but my tutorial writing mana pool is emptied when I've got other things to do
Fabian (fabianorue@gmail.com) on 04.09.2013 16:43:39
Good tutorial! i waiting for model animation whit Assimp! thanks!
Dima (dmitry.trok@gmail.com) on 03.09.2013 14:26:51
without animation ((
Michal Bubnar (michalbb1@gmail.com) on 03.09.2013 14:44:17
So far yes, later animation tutorial will be present - first keyframe animation then skeletal.
Dima (dmitry.trok@gmail.com) on 04.09.2013 14:13:41
Okay. I use "Open collada". Other file formats work bad with assimp.
But, i have more problems wthis partices system on "Transform Feedback" )))
Michal Bubnar (michalbb1@gmail.com) on 04.09.2013 19:26:18
That transform feedback article will come for sure. I have used TF in my The Enchanted Forest demo. I also created a library to be able to simulate particle systems on GPU which is used there - it's called Blaze Particle Engine . I also did it for school back then.

I will hopefully add this tutorial in like next 3 tutorials, because this is definitely important for developers.
Jump to page:
1

Number of entries:

This webpage has been visited 616752 times since 29.01.2009.
Currently there are 19 people viewing this page.