MULTIMEDIA

Game Programming with DirectX : 3D Models - OBJ Models (part 2) - Loading OBJ Files

5/19/2013 7:18:51 PM

2. Loading OBJ Files

Loading an OBJ file is fairly simple, but a lot of text parsing needs to be done. In the OBJ Models demo we will load the sample cube and its material.

Two files specify the OBJ loading code: objLoader.h and objLoader.cpp. In the header file there are two classes, one used to store a mesh and one used to store all meshes in the file. The mesh class is called ObjMesh, and it stores all of the vertices, normals, and texture coordinates that are ready to be used to create a Direct3D 10 vertex buffer and the texture file name in a string. This mesh class is fairly straightforward and only has functions for accessing the member variables. The ObjMesh class is shown in Listing 3.

Listing 3. The ObjMesh Class
class ObjMesh
{
   public:
      ObjMesh()
      {
         m_vertices = NULL;
         m_normals = NULL;
         m_texCoords = NULL;
         m_totalVerts = 0;
      }

      ~ObjMesh()
      {
         Release()
      }
      void Release()

      void SetVertices(float *verts) { m_vertices = verts; }
      void SetNormals(float *normals) { m_normals = normals; }
      void SetTexCoords(float *coords) { m_texCoords = coords; }
      void SetTotalVerts(int total) { m_totalVerts = total; }
      void SetName(string name) { m_name = name; }
      void SetTextureName(string name) { m_decalFile = name; }

      float *GetVertices()     { return m_vertices; }
      float *GetNormals()      { return m_normals; }
      float *GetTexCoords()    { return m_texCoords; }
      int    GetTotalVerts()   { return m_totalVerts; }
      string GetName() { return m_name; }
      string GetTextureName()  { return m_decalFile; }

   private:
      float *m_vertices;
      float *m_normals;
      float *m_texCoords;
      int m_totalVerts;

      string m_name;
      string m_decalFile;
};

					  

The model list class is called ObjModel, and it stores a list of ObjMesh objects. The model class is also going to be used to load the OBJ file, which is done by calling the function LoadOBJ(). All of the other functions except for Release() in the class are used to access individual mesh information such as getting a specific mesh’s texture file string, getting a specific mesh’s vertex list, and so on. The Release() function that you will see in the class is used to free all allocated memory. The mesh list itself is a list of pointers that are allocated during the loading of the model file. The ObjModel class is shown in Listing 4. The model list class is only used to load all of the information from an OBJ file and have its data ready so that vertex buffers can be created out of the OBJ files. Once those vertex buffers are created, the model list class object would not be needed. Later in the main source file you’ll see how this class is used to temporarily hold the geometric information until the vertex buffers are created.

Listing 4. The ObjModel Class
class ObjModel
{
   public:
      ObjModel() { }
      ~ObjModel() { Release() }

      bool LoadOBJ(char *fileName);
      void Release()

      ObjMesh *GetMeshByIndex(int index);
      float *GetMeshVertices(int index);
      float *GetMeshNormals(int index);
      float *GetMeshTexCoords(int index);

      int GetMeshTotalVerts(int index);
      int GetMeshCount();

      string GetMeshTextureFile(int index);

   private:
      vector<ObjMesh*> m_meshList;
};

The first functions to look at in the objLoader.cpp file are the Release() functions. For the mesh, this function deletes all allocated memory (i.e., vertices, normals, and texture coordinates), and for the model class a Standard Template Library (STL) algorithm is used to delete all allocated memory from the container. The list of meshes is stored in an std::vector object that is part of the C++ standard and is part of the STL. By calling the STL algorithm function for_each() and sending it a user-defined structure that will operate on each of the elements in the std::vector array, we can create a structure that will delete each element that exists. This is a nice trick that can be used to delete all elements of a container by taking advantage of the efficiency of the STL algorithms. The Release() functions are shown in Listing 5.

Listing 5. The Release() Functions
// Used to delete allocated objects in an STL container.
struct DeleteMemObj
{
   template<typename T>
   void operator()(const T* ptr) const
   {
      if(ptr != NULL)
         delete ptr;

      ptr = NULL;
   }
};

void ObjMesh::Release()
{
   m_totalVerts = 0;

   if(m_vertices != NULL)
   {
      delete[] m_vertices;
      m_vertices = NULL;
   }
   if(m_normals != NULL)
   {
      delete[] m_normals;
      m_normals = NULL;
   }

   if(m_texCoords != NULL)
   {
      delete[] m_texCoords;
      m_texCoords = NULL;
   }
}


void ObjModel::Release()
{
   for_each(m_meshList.begin(), m_meshList.end(), DeleteMemObj());
}

					  

The mesh-accessing functions of the ObjModel class are fairly straightforward and use array indexes to return the information of interest. The class has a function to return a mesh by array index and functions to return a specific mesh’s vertex positions, normals, texture coordinates, vertex count, and texture file name. These accessing functions are shown in Listing 6 for the ObjModel class.

Listing 6. The ObjModel Class’s Accessing Functions
ObjMesh *ObjModel::GetMeshByIndex(int index)
{
   if(index < 0 || index > (int)m_meshList.size())
      return NULL;

   return m_meshList[index];
}
float *ObjModel::GetMeshVertices(int index)
{
   if(index < 0 || index > (int)m_meshList.size())
      return 0;

   return m_meshList[index]->GetVertices();
}

float *ObjModel::GetMeshNormals(int index)
{
  if(index < 0 || index > (int)m_meshList.size())
    return 0;

  return m_meshList[index]->GetNormals();
}


float *ObjModel::GetMeshTexCoords(int index)
{
   if(index < 0 || index > (int)m_meshList.size())
      return 0;

   return m_meshList[index]->GetTexCoords();
}

int ObjModel::GetMeshTotalVerts(int index)
{
   if(index < 0 || index > (int)m_meshList.size())
      return 0;

   return m_meshList[index]->GetTotalVerts();
}
int ObjModel::GetMeshCount()
{
   return (int)m_meshList.size();
}


string ObjModel::GetMeshTextureFile(int index)
{
   if(index < 0 || index > (int)m_meshList.size())
      return 0;

   return m_meshList[index]->GetTextureName();
}

					  

The last function in the objLoader.cpp source file is the LoadOBJ() function. This function is the biggest, but it is all fairly straightforward. To make it easier to understand we’ll look at the function in sections.

In the first section the OBJ file is sent to a token stream object. There are two token streams in the function, with the first holding the OBJ file and the second being a temp stream used to further parse individual lines. The main token stream that has the entire file will extract each line from the OBJ file using the MoveToNextLine() function of the TokenStream class. The temp stream object will take that line and further break it down into individual tokens on a line-by-line basis.

A loop is used in the first section to read each line from the file. The first token of each line that is read from the OBJ file is examined to see what information is on that line of text. If the line starts with a #, then it is a comment and can be ignored. If the line starts with a v, then it is a vertex position, and we will need to read the next three tokens and convert the strings to floats to extract that information. The same is done for vertex normals (vn) and texture coordinates (vt). All read information is stored in temporary std::vector arrays and used later in the function. Also, the material file name is read and stored in a string called materialFile. The first section of the LoadOBJ() function is shown in Listing 7.

Listing 7. The First Section of the LoadOBJ() Function
bool ObjModel::LoadOBJ(char *fileName)
{
   TokenStream tokenStream(NULL), tempStream(NULL);
   std::string tempLine, token;

   tokenStream.LoadTokenStream(fileName);

   std::vector<float> verts, norms, texC;

   // This will store the material file location
   // so we can use it to read the texture file name later.
   string materialFile;

   // Loop through and read all positions, normals, tex coords.
   while(tokenStream.MoveToNextLine(&tempLine))
   {
      tempStream.SetTokenStream((char*)tempLine.c_str());
      tokenStream.GetNextToken(NULL);

      if(!tempStream.GetNextToken(&token))
         continue;

      if(strcmp(token.c_str(), "v") == 0)
      {
          tempStream.GetNextToken(&token);
          verts.push_back((float)atof(token.c_str()));

          tempStream.GetNextToken(&token);
          verts.push_back((float)atof(token.c_str()));

          tempStream.GetNextToken(&token);
          verts.push_back((float)atof(token.c_str()));
      }
      else if(strcmp(token.c_str(), "mtllib") == 0)
      {
          tempStream.GetNextToken(&materialFile);
      }
      else if(strcmp(token.c_str(), "vn") == 0)
      {
         tempStream.GetNextToken(&token);
         norms.push_back((float)atof(token.c_str()));

         tempStream.GetNextToken(&token);
         norms.push_back((float)atof(token.c_str()));

         tempStream.GetNextToken(&token);
         norms.push_back((float)atof(token.c_str()));
      }
      else if(strcmp(token.c_str(), "vt") == 0)
      {
         tempStream.GetNextToken(&token);
         texC.push_back((float)atof(token.c_str()));

         tempStream.GetNextToken(&token);
         texC.push_back((float)atof(token.c_str()));
      }

      token[0] = '\0;;
   }
   …
}

					  

The second section of the LoadOBJ() function resets the stream, and this time it loops through and looks for mesh declarations by searching for g and triangle faces by searching for f. Every time a g is encountered, a new mesh is added to the ObjModel class’s mesh list. Every time an f is encountered, a new face is added to the last mesh added to the mesh list. That way all meshes receive their correct faces since each mesh in an OBJ file is followed by its list of faces. This code assumes that the file uses only three point triangles, so keep that in mind.

To store the information of a mesh that will be read, the function creates a temporary structure to hold the data. This structure holds the name of the mesh (optional), the material name the mesh uses from the material file, and the face indexes. During this second section of the LoadOBJ() function, all mesh information is stored in an array of these temporary structure objects.

When reading a mesh, a new object is pushed (added) to the mesh list, and the current mesh index is saved. This index is used for the face parsing, so we always know which mesh was the last one added to the list. At the end of the mesh’s conditional statement the name of the mesh is extracted, which always follows the g keyword.

When reading the faces, the three tokens are extracted one at a time. Each token that is extracted is further broken down, and each face index (the first being for the position, the second for the normal, and the third for the texture coordinates) is stored in the temporary mesh’s faces array. This is done by looping through the token and reading the indexes until we come across a slash (/) that marks the end of an index or until all indexes have been read for the current face vertex.

The second section from the LoadOBJ() function is shown in Listing 8. The face parsing looks complex, but it is nothing more than reading the characters between the slashes, converting them to integers, and saving them in the temporary face array for the current mesh.

Listing 8. The Second Section from the LoadOBJ() Function
bool ObjModel::LoadOBJ(char *fileName)
{
   …

   // Temp struct used to store file faces per-mesh.
   struct TempOBJMesh
   {
      string name, material;
      std::vector<int> faces;
   };

   std::vector<TempOBJMesh> tempMeshes;
   int tempMeshlndex = 0;

   // Start from the beginning.
   tokenStream.ResetStream();
   // Read each mesh.
   while(tokenStream.MoveToNextLine(&tempLine))
   {
      tempStream.SetTokenStream((char*)tempLine.c_str());
      tokenStream.GetNextToken(NULL);

      if(!tempStream.GetNextToken(&token))
         continue;

      if(strcmp(token.c_str(), "g") == 0)
      {
         // Add a new mesh to the list.

         TempOBJMesh tempMesh;
         tempMeshes.push_back(tempMesh);

         tempMeshIndex = (int)tempMeshes.size() - 1;

         tempStream.GetNextToken(&tempMeshes[tempMeshIndex].name);
      }
      else if(strcmp(token.c_str(), "usemtl") == 0 &&
              !tempMeshes.empty())
      {
         // Get the material for the current mesh.

         tempStream.GetNextToken(
            &tempMeshes[tempMeshIndex].material);
      }
      else if(strcmp(token.c_str(), "f") == 0 &&
              !tempMeshes.empty())
      {
         // Add a new face to the current mesh.
         int index = 0;

         for(int i = 0; i < 3; i++)
         {
            tempStream.GetNextToken(&token);
            int len = (int)strlen(token.c_str());

            for(int s = 0; s < len + 1; s++)
            {
               char buff[24];

               if(token[s] != '/' && s < len)
               {
                  buff[index] = token[s];
                  index++;
               }
               else
               {
                  buff[index] = '\0';

                  tempMeshes[tempMeshIndex].faces.push_back(
                    (int)atoi(buff));

                  index = 0;
               }
            }
         }
      }

      token[0] = '\0';
   }

   …
}

					  

The third and last section of the LoadOBJ() function takes all of the loaded OBJ data that is stored in the temporary arrays and creates each ObjMesh object out of them. This section starts by allocating enough room on the mesh list array by calling reserve(). The function then loops through each mesh in the temp mesh list, allocates the real mesh we will be using, and allocates memory to store the vertex positions, normals, and texture coordinates in triangle list form. Inside the loop a triangle list mesh is essentially being created, as that process is very straightforward. In the OBJ file the information is specified in a way that can’t be sent directly to Direct3D. In this section we are creating the ObjMesh that will have its data formatted in a way that can be sent to Direct3D as a triangle list model.

In an OBJ file only unique positions, texture coordinates, and normals are used, and when you use index geometry in Direct3D or OpenGL, the indexes for a face vertex have to be the same for each attribute. So, for example, if you have 100 vertices, there should be 100 positions, normals, and texture coordinates, even in an index model where index 1 in all arrays references attributes for the same vertex point. However, in an OBJ file you can have four texture coordinates that are reused, six normals, and eight positions, which wouldn’t be right for Direct3D since all attributes must have the same index. You can even have one normal, 20 vertices, and so on in an OBJ file. Since the attribute indexes are not the same across each array for a single vertex point, we have to take this extra step to set things up for rendering later on.

Once the face information has been expanded so that we have a triangle list’s worth of information, this information is set to the allocated ObjMesh object, and that object is added to the mesh list. Since we already know the material’s file name and since we know the name of the material the mesh uses, we create another token stream object to load the material file, and we search for the texture’s file name. This can be done by using the overloaded GetNextToken() function to move to the start of the material information the mesh uses and then calling the same function again to search for the token Kd_map. The token that follows Kd_map is the name of the texture file, which is also stored in the ObjMesh object.

The third and final section of the LoadOBJ() function is shown in Listing 9.

Listing 9. The Third Section of the LoadOBJ() Function
bool ObjModel::LoadOBJ(char *fileName)
{
   …
   // "Unroll" the loaded obj information into a list
   // of triangles for each mesh.

   m_meshList.reserve(tempMeshes.size());

   for(int i = 0; i < (int)tempMeshes.size(); i++)
   {
       ObjMesh *mesh = new ObjMesh();

       int vIndex = 0, nIndex = 0, tIndex = 0;
       int numFaces = (int)tempMeshes[i].faces.size() / 9;
       int totalVerts = numFaces * 3;

       mesh->SetTotalVerts(totalVerts);

       float *vertices = new float[totalVerts * 3];
       float *normals = NULL, *texCoords = NULL;

       if((int)norms.size() != 0)
          normals = new float[totalVerts * 3];

       if((int)texC.size() != 0)
          texCoords = new float[totalVerts * 2];

       // Generate triangle list.
       for(int f = 0; f < (int)tempMeshes[i].faces.size(); f+=3)
       {
          vertices[vIndex + 0] =
             verts[(tempMeshes[i].faces[f + 0] - 1) * 3 + 0];

          vertices[vIndex + 1] =
             verts[(tempMeshes[i].faces[f + 0] - 1) * 3 + 1];

          vertices[vIndex + 2] =
             verts[(tempMeshes[i].faces[f + 0] - 1) * 3 + 2];

          vIndex += 3;

          if(texCoords)
          {
             texCoords[tIndex + 0] =
                texC[(tempMeshes[i].faces[f + 1] - 1) * 2 + 0];

             texCoords[tIndex + 1] =
                texC[(tempMeshes[i].faces[f + 1] - 1) * 2 + 1];

             tIndex += 2;
          }

          if(normals)
          {
             normals[nIndex + 0] =
                norms[(tempMeshes[i].faces[f + 2] - 1) * 3 + 0];

             normals[nIndex + 1] =
                norms[(tempMeshes[i].faces[f + 2] - 1) * 3 + 1];

             normals[nIndex + 2] =
                norms[(tempMeshes[i].faces[f + 2] - 1) * 3 + 2];

             nIndex += 3;
          }
       }

       // Set info to mesh object.
       mesh->SetName(tempMeshes[i].name);
       mesh->SetVertices(vertices);
       mesh->SetNormals(normals);
       mesh->SetTexCoords(texCoords);

       TokenStream materialStream(NULL);
       materialStream.LoadTokenStream((char*)materialFile.c_str());

       string searchKeyword = "map_Kd";
       string textureFile;

       // Use the first call to move to the material's section.
       if(materialStream.GetNextToken(&tempMeshes[i].material,
          NULL))
       {
          // Then use this call to get the texture name.
          // All mat info is kept in one section so once we find
          // the mat's name we can just move right to the texture
          // file name since that will appear before any other mat.

          materialStream.GetNextToken(&searchKeyword, &textureFile);
          mesh->SetTextureName(textureFile);
       }

       m_meshList.push_back(mesh);
    }

    verts.clear();
    norms.clear();
    texC.clear();
    tempMeshes.clear();

    return true;
}

					  

The next file to examine is the main.cpp source file for the OBJ Models demo. In this file the global section has a new structure added to it called DX10Mesh, as well as a list of these meshes. Inside this mesh are a D3D10 vertex buffer, the total vertices count, and a texture. The global section from the OBJ Models demo’s main.cpp source file is shown in Listing 10. Each vertex from an OBJ file specifies a position, normal, and texture coordinate. For models that do not use one or more of these, most 3D modeling applications use a single value for those attributes even if the attribute isn’t used. In our loader those arrays would have been filled with that single value for attributes that are not specified in the model, so the vertex structure specifies each.

Listing 10. Global Section from the OBJ Models Demo’s Main Source File
#include<d3d10.h>
#include<d3dx10.h>
#include<vector>
#include"objLoader.h"

#pragma comment(lib, "d3d10.lib")
#pragma comment(lib, "d3dx10.lib")

#define WINDOW_NAME     "Loading OBJ Models"
#define WINDOW_CLASS    "UPGCLASS"
#define WINDOW_WIDTH    800
#define WINDOW_HEIGHT   600


// Global window handles.
HINSTANCE g_hInst = NULL;
HWND g_hwnd = NULL;


// Direct3D 10 objects.
ID3D10Device *g_d3dDevice = NULL;
IDXGISwapChain *g_swapChain = NULL;
ID3D10RenderTargetView *g_renderTargetView = NULL;
ID3D10DepthStencilView *g_depthStencilView = NULL;
ID3D10Texture2D *g_depthStencilTex = NULL;
struct DX10Vertex
{
   D3DXVECTOR3 pos;
   D3DXVECTOR3 normal;
   D3DXVECTOR2 tex0;
};

ID3D10InputLayout *g_layout = NULL;

struct DX10Mesh
{
  DX10Mesh()
  {
     m_vertices = NULL;
     m_decal = NULL;
     m_totalVerts = 0;
  }

  ID3D10Buffer *m_vertices;
  ID3D10ShaderResourceView *m_decal;
  int m_totalVerts;
};

vector<DX10Mesh> g_meshes;

ID3D10Effect *g_shader = NULL;
ID3D10EffectTechnique *g_textureMapTech = NULL;
ID3D10EffectShaderResourceVariable *g_decalEffectVar = NULL;
ID3D10EffectMatrixVariable *g_worldEffectVar = NULL;
ID3D10EffectMatrixVariable *g_viewEffectVar = NULL;
ID3D10EffectMatrixVariable *g_projEffectVar = NULL;

D3DXMATRIX g_worldMat, g_viewMat, g_projMat;
// Scene rotations.
float g_xRot = 0.0f;
float g_yRot = 0.0f;

					  

Other  
 
Top 10
Review : Sigma 24mm f/1.4 DG HSM Art
Review : Canon EF11-24mm f/4L USM
Review : Creative Sound Blaster Roar 2
Review : Philips Fidelio M2L
Review : Alienware 17 - Dell's Alienware laptops
Review Smartwatch : Wellograph
Review : Xiaomi Redmi 2
Extending LINQ to Objects : Writing a Single Element Operator (part 2) - Building the RandomElement Operator
Extending LINQ to Objects : Writing a Single Element Operator (part 1) - Building Our Own Last Operator
3 Tips for Maintaining Your Cell Phone Battery (part 2) - Discharge Smart, Use Smart
REVIEW
- First look: Apple Watch

- 3 Tips for Maintaining Your Cell Phone Battery (part 1)

- 3 Tips for Maintaining Your Cell Phone Battery (part 2)
VIDEO TUTORIAL
- How to create your first Swimlane Diagram or Cross-Functional Flowchart Diagram by using Microsoft Visio 2010 (Part 1)

- How to create your first Swimlane Diagram or Cross-Functional Flowchart Diagram by using Microsoft Visio 2010 (Part 2)

- How to create your first Swimlane Diagram or Cross-Functional Flowchart Diagram by using Microsoft Visio 2010 (Part 3)
Popular Tags
Video Tutorail Microsoft Access Microsoft Excel Microsoft OneNote Microsoft PowerPoint Microsoft Project Microsoft Visio Microsoft Word Active Directory Exchange Server Sharepoint Sql Server Windows Server 2008 Windows Server 2012 Windows 7 Windows 8 Adobe Flash Professional Dreamweaver Adobe Illustrator Adobe Photoshop CorelDRAW X5 CorelDraw 10 windows Phone 7 windows Phone 8 Iphone