Implementing OBJ model loading using interleaved buffers

In this recipe we will implement the Wavefront ® OBJ model. Instead of using separate buffer objects for storing positions, normals, and texture coordinates as in the previous recipe, we will use a single buffer object with interleaved data. This ensures that we have more chances of a cache hit since related attributes are stored next to each other in the buffer object memory.

Getting started

The code for this recipe is contained in the Chapter5/ObjViewer folder.

How to do it…

Let us start the recipe by following these simple steps:

  1. Create a global reference of the ObjLoader object. Call the ObjLoader::Load function, passing it the name of the OBJ file. Pass vectors to store the meshes, vertices, indices, and materials contained in the OBJ file.
      ObjLoader obj;
      if(!obj.Load(mesh_filename.c_str(), meshes, vertices, indices, materials)) {
        cout<<"Cannot load the 3ds mesh"<<endl;
        exit(EXIT_FAILURE);
      }
  2. Generate OpenGL texture objects for each material using the SOIL library if the material has a texture map.
      for(size_t k=0;k<materials.size();k++) {
        if(materials[k]->map_Kd != "") {
          GLuint id = 0;
          glGenTextures(1, &id);
          glBindTexture(GL_TEXTURE_2D, id);
          glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
          glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
          glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
          glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
          
          int texture_width = 0, texture_height = 0, channels=0;
          const string& filename =  materials[k]->map_Kd;
          std::string full_filename = mesh_path;
          full_filename.append(filename);
    
          GLubyte* pData = SOIL_load_image(full_filename.c_str(), &texture_width, &texture_height, &channels, SOIL_LOAD_AUTO);
          if(pData == NULL) {
            cerr<<"Cannot load image: "<<full_filename.c_str()<<endl;
            exit(EXIT_FAILURE);
          }
          //… image flipping code
          GLenum format = GL_RGBA;
          switch(channels) {
            case 2: format = GL_RG32UI; break;
            case 3: format = GL_RGB;  break;
            case 4: format = GL_RGBA;  break;
          }
          glTexImage2D(GL_TEXTURE_2D, 0, format, texture_width, texture_height, 0, format, GL_UNSIGNED_BYTE, pData);
          SOIL_free_image_data(pData);
          textures.push_back(id);
        }
      }
  3. Set up shaders and generate buffer objects to store the mesh file data in the GPU memory. The shader setup is similar to the previous recipes.
      glGenVertexArrays(1, &vaoID);
      glGenBuffers(1, &vboVerticesID);
      glGenBuffers(1, &vboIndicesID); 
      glBindVertexArray(vaoID);
      glBindBuffer (GL_ARRAY_BUFFER, vboVerticesID);
      glBufferData (GL_ARRAY_BUFFER, sizeof(Vertex)*vertices.size(), &(vertices[0].pos.x), GL_STATIC_DRAW);
      glEnableVertexAttribArray(shader["vVertex"]);
      glVertexAttribPointer(shader["vVertex"], 3, GL_FLOAT, GL_FALSE,sizeof(Vertex),0);
      
      glEnableVertexAttribArray(shader["vNormal"]);
      glVertexAttribPointer(shader["vNormal"], 3, GL_FLOAT, GL_FALSE,sizeof(Vertex),(const GLvoid*)(offsetof( Vertex, normal)) );
      
      glEnableVertexAttribArray(shader["vUV"]);
      glVertexAttribPointer(shader["vUV"], 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)(offsetof(Vertex, uv)) );
      if(materials.size()==1) {
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort)*indices.size(), &(indices[0]), GL_STATIC_DRAW);
      }
  4. Bind the vertex array object associated with the mesh, use the shader and pass the shader uniforms, that is, the modelview (MV), projection (P), normal matrices (N) and light position, and so on.
      glBindVertexArray(vaoID); {
        shader.Use();
        glUniformMatrix4fv(shader("MV"), 1, GL_FALSE, glm::value_ptr(MV));
        glUniformMatrix3fv(shader("N"), 1, GL_FALSE, glm::value_ptr(glm::inverseTranspose(glm::mat3(MV))));
        glUniformMatrix4fv(shader("P"), 1, GL_FALSE, glm::value_ptr(P));
        glUniform3fv(shader("light_position"),1, &(lightPosOS.x)); 
  5. To draw the mesh/submesh, loop through all of the materials in the mesh and then bind the texture to the GL_TEXTURE_2D target if the material contains a texture map. Otherwise, use a default color for the mesh. Finally, call the glDrawElements function to render the mesh/submesh.
    for(size_t i=0;i<materials.size();i++) {
      Material* pMat = materials[i];
      if(pMat->map_Kd !="") {
        glUniform1f(shader("useDefault"), 0.0);
        GLint whichID[1];
        glGetIntegerv(GL_TEXTURE_BINDING_2D, whichID);
        if(whichID[0] != textures[i])
          glBindTexture(GL_TEXTURE_2D, textures[i]);
      }
      else
      glUniform1f(shader("useDefault"), 1.0);
      
      if(materials.size()==1)
      glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_SHORT, 0);
      else
      glDrawElements(GL_TRIANGLES, pMat->count, GL_UNSIGNED_SHORT, (const GLvoid*)(& indices[pMat->offset])); 
    }
    shader.UnUse();

How it works…

The main component of this recipe is the ObjLoader::Load function defined in the Obj.cpp file. The Wavefront® OBJ file is a text file which has different text descriptors for different mesh components. Usually, the mesh starts with the geometry definition, that is, vertices that begin with the letter v followed by three floating point values. If there are normals, their definitions begin with vn followed by three floating point values. If there are texture coordinates, their definitions begin with vt, followed by two floating point values. Comments start with the # character, so whenever a line with this character is encountered, it is ignored.

Following the geometry definition, the topology is defined. In this case, the line is prefixed with f followed by the indices for the polygon vertices. In case of a triangle, three indices sections are given such that the vertex position indices are given first, followed by texture coordinates indices (if any), and finally the normal indices (if any). Note that the indices start from 1, not 0.

So, for example, say that we have a quad geometry having four position indices (1,2,3,4) having four texture coordinate indices (5,6,7,8), and four normal indices (1,1,1,1) then the topology would be stored as follows:

f 1/5/1 2/6/1 3/7/1 4/8/1

If the mesh is a triangular mesh with position vertices (1,2,3), texture coordinates (7,8,9), and normals (4,5,6) then the topology would be stored as follows:

f 1/7/4 2/8/5 3/9/6

Now, if the texture coordinates are omitted from the first example, then the topology would be stored as follows:

f 1//1 2//1 3//1 4//1

The OBJ file stores material information in a separate material (.mtl) file. This file contains similar text descriptors that define different materials with their ambient, diffuse, and specular color values, texture maps, and so on. The details of the defined elements are given in the OBJ format specifications. The material file for the current OBJ file is declared using the mtllib keyword followed by the name of the .mtl file. Usually, the .mtl file is stored in the same folder as the OBJ file. A polygon definition is preceded with a usemtl keyword followed by the name of the material to use for the upcoming polygon definition. Several polygonal definitions can be grouped using the g or o prefix followed by the name of the group/object respectively.

The ObjLoader::Load function first finds the current prefix. Then, the code branches to the appropriate section depending on the prefix. The suffix strings are then parsed and the extracted data is stored in the corresponding vectors. For efficiency, rather than storing the indices directly, we store them by material so that we can then sort and render the mesh by material. The associated material library file (.mtl) is loaded using the ReadMaterialLibrary function. Refer to the Obj.cpp file for details.

The file parsing is the first piece of the puzzle. The second piece is the transfer of this data to the GPU memory. In this recipe, we use an interleaved buffer, that is, instead of storing each per-vertex attribute separately in its own vertex buffer object, we store them interleaved one after the other in a single buffer object. First positions are followed by normals and then texture coordinates. We achieve this by first defining our vertex format using a custom Vertex struct. Our vertices are a vector of this struct.

struct Vertex  {
   glm::vec3 pos, normal;
   glm::vec2 uv;
};

We generate the vertex array object and then the vertex buffer object. Next, we bind the buffer object passing it our vertices. In this case, we specify the stride of each attribute in the data stream separately as follows:

glBindBuffer (GL_ARRAY_BUFFER, vboVerticesID);
glBufferData (GL_ARRAY_BUFFER, sizeof(Vertex)*vertices.size(), &(vertices[0].pos.x), GL_STATIC_DRAW);
glEnableVertexAttribArray(shader["vVertex"]);
glVertexAttribPointer(shader["vVertex"], 3, GL_FLOAT, GL_FALSE,sizeof(Vertex),0);
glEnableVertexAttribArray(shader["vNormal"]);
glVertexAttribPointer(shader["vNormal"], 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)(offsetof(Vertex, normal)) );
glEnableVertexAttribArray(shader["vUV"]);
glVertexAttribPointer(shader["vUV"], 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)(offsetof(Vertex, uv)) );

If the mesh has a single material, we store the mesh indices into a GL_ELEMENT_ARRAY_BUFFER target. Otherwise, we render the submeshes by material.

if(materials.size()==1) {
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER,  sizeof(GLushort) * indices.size(), &(indices[0]), GL_STATIC_DRAW);
}

At the time of rendering, if we have a single material, we render the whole mesh, otherwise we render the subset stored with the material.

if(materials.size()==1)
  glDrawElements(GL_TRIANGLES,indices.size(),GL_UNSIGNED_SHORT,0);
else
  glDrawElements(GL_TRIANGLES, pMat->count, GL_UNSIGNED_SHORT, (const GLvoid*)(&indices[pMat->offset]));

There's more…

The demo application implementing this recipe shows a scene with three blocks on a planar quad. The camera view can be rotated with the left mouse button. The light source's position is shown by a 3D crosshair that can be moved by dragging the right mouse button. The output from this demo application is shown in the following figure:

There's more…

See also

You can see the OBJ file specification on Wikipedia at http://en.wikipedia.org/wiki/Wavefront_.obj_file.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.219.166