Implementing 3ds model loading using separate buffers

We will now create model loader and renderer for Autodesk® 3ds model format which is a simple yet efficient binary model format for storing digital assets.

Getting started

The code for this recipe is contained in the Chapter5/3DsViewer folder. This recipe will be using the Drawing a 2D image in a window using a fragment shader and the SOIL image loading library recipe from Chapter 1, Introduction to Modern OpenGL, for loading the 3ds mesh file's textures using the SOIL image loading library.

How to do it…

The steps required to implement a 3ds file viewer are as follows:

  1. Create an instance of the C3dsLoader class. Then call the C3dsLoader::Load3DS function passing it the name of the mesh file and a set of vectors to store the submeshes, vertices, normals, uvs, indices, and materials.
    if(!loader.Load3DS(mesh_filename.c_str( ), meshes, vertices, normals, uvs, faces, indices, materials)) {
      cout<<"Cannot load the 3ds mesh"<<endl;
      exit(EXIT_FAILURE);
    }
  2. After the mesh is loaded, use the mesh's material list to load the material textures into the OpenGL texture object.
      for(size_t k=0;k<materials.size();k++) {
        for(size_t m=0;m< materials[k]->textureMaps.size();m++)
        {
          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]->textureMaps[m]->filename;
          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);
          }
          //Flip the image on Y axis
          int i,j;
          for( j = 0; j*2 < texture_height; ++j ) {
            int index1 = j * texture_width * channels;
            int index2 = (texture_height - 1 - j) * texture_width * channels;
            for( i = texture_width * channels; i > 0; --i ){
              GLubyte temp = pData[index1];
              pData[index1] = pData[index2];
              pData[index2] = temp;
              ++index1;
              ++index2;
            }
          }
          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);
          textureMaps[filename]=id;
        }
      }
  3. Pass the loaded per-vertex attributes; that is, positions (vertices), texture coordinates (uvs), per-vertex normals (normals), and triangle indices (indices) to GPU memory by allocating separate buffer objects for each attribute. Note that for easier handling of buffer objects, we bind a single vertex array object (vaoID) first.
        glBindVertexArray(vaoID);
        glBindBuffer (GL_ARRAY_BUFFER, vboVerticesID);
        glBufferData (GL_ARRAY_BUFFER, sizeof(glm::vec3)* vertices.size(), &(vertices[0].x), GL_STATIC_DRAW);
        glEnableVertexAttribArray(shader["vVertex"]);
        glVertexAttribPointer(shader["vVertex"], 3, GL_FLOAT, GL_FALSE,0,0);
        glBindBuffer (GL_ARRAY_BUFFER, vboUVsID);
        glBufferData (GL_ARRAY_BUFFER, sizeof(glm::vec2)*uvs.size(), &(uvs[0].x), GL_STATIC_DRAW);
        glEnableVertexAttribArray(shader["vUV"]);
        glVertexAttribPointer(shader["vUV"],2,GL_FLOAT,GL_FALSE,0, 0);
        glBindBuffer (GL_ARRAY_BUFFER, vboNormalsID);
        glBufferData (GL_ARRAY_BUFFER, sizeof(glm::vec3)* normals.size(), &(normals[0].x),  GL_STATIC_DRAW);
        glEnableVertexAttribArray(shader["vNormal"]);
        glVertexAttribPointer(shader["vNormal"], 3, GL_FLOAT, GL_FALSE, 0, 0);
  4. If we have only a single material in the 3ds file, we store the face indices into GL_ELEMENT_ARRAY_BUFFER so that we can render the whole mesh in a single call. However, if we have more than one material, we bind the appropriate submeshes separately. The glBufferData call allocates the GPU memory, however, it is not initialized. In order to initialize the buffer object memory, we can use the glMapBuffer function to obtain a direct pointer to the GPU memory. Using this pointer, we can then write to the GPU memory. An alternative to using glMapBuffer is glBufferSubData which can modify the GPU memory by copying contents from a CPU buffer.
        if(materials.size()==1) {
          glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID);
          glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort)* 
          3*faces.size(), 0, GL_STATIC_DRAW);
          GLushort* pIndices = static_cast<GLushort*>(glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY));
          for(size_t i=0;i<faces.size();i++) {
            *(pIndices++)=faces[i].a;
            *(pIndices++)=faces[i].b;
            *(pIndices++)=faces[i].c;
          }
          glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
        }
  5. Set up the vertex shader to output the clip space position as well as the per-vertex texture coordinates. The texture coordinates are then interpolated by the rasterizer to the fragment shader using an output attribute vUVout.
    #version 330 core
    
    layout(location = 0) in vec3 vVertex;
    layout(location = 1) in vec3 vNormal;
    layout(location = 2) in vec2 vUV;
    
    smooth out vec2 vUVout;
    
    uniform mat4 P; 
    uniform mat4 MV;
    uniform mat3 N;
    
    smooth out vec3 vEyeSpaceNormal;
    smooth out vec3 vEyeSpacePosition;
    
    void main()
    {
      vUVout=vUV;
      vEyeSpacePosition = (MV*vec4(vVertex,1)).xyz;
      vEyeSpaceNormal = N*vNormal;
      gl_Position = P*vec4(vEyeSpacePosition,1);
    }
  6. Set up the fragment shader, which looks up the texture map sampler with the interpolated texture coordinates from the rasterizer. Depending on whether the submesh has a texture, we linearly interpolate between the texture map color and the diffused color of the material, using the GLSL mix function.
    #version 330 core  
    uniform sampler2D textureMap;
    uniform float hasTexture;
    uniform vec3 light_position;//light position in object space
    uniform mat4 MV;
    smooth in vec3 vEyeSpaceNormal;
    smooth in vec3 vEyeSpacePosition;
    smooth in vec2 vUVout;
    
    layout(location=0) out vec4 vFragColor;
    
    const float k0 = 1.0;//constant attenuation
    const float k1 = 0.0;//linear attenuation
    const float k2 = 0.0;//quadratic attenuation
    
    void main()
    {
      vec4 vEyeSpaceLightPosition = (MV*vec4(light_position,1));
      vec3 L = (vEyeSpaceLightPosition.xyz-vEyeSpacePosition);
      float d = length(L);
      L = normalize(L);
      float diffuse = max(0, dot(vEyeSpaceNormal, L));
      float attenuationAmount = 1.0/(k0 + (k1*d) + (k2*d*d));
      diffuse *= attenuationAmount;
    
      vFragColor = diffuse*mix(vec4(1),texture(textureMap, vUVout), hasTexture);
    }
  7. The rendering code binds the shader program, sets the shader uniforms, and then renders the mesh, depending on how many materials the 3ds mesh has. If the mesh has only a single material, it is drawn in a single call to glDrawElement by using the indices attached to the GL_ELEMENT_ARRAY_BUFFER binding point.
      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));
        if(materials.size()==1) {
          GLint whichID[1];
          glGetIntegerv(GL_TEXTURE_BINDING_2D, whichID);
          if(textureMaps.size()>0) {
            if(whichID[0] != textureMaps[materials[0]->textureMaps[0]->filename]) {
            glBindTexture(GL_TEXTURE_2D, textureMaps[materials[0]->textureMaps[0]->filename]);
            glUniform1f(shader("hasTexture"),1.0);
          }
        } else {
          glUniform1f(shader("hasTexture"),0.0);
          glUniform3fv(shader("diffuse_color"),1, materials[0]->diffuse);
        }
        glDrawElements(GL_TRIANGLES, meshes[0]->faces.size()*3, GL_UNSIGNED_SHORT, 0);
      }
  8. If the mesh contains more than one material, we iterate through the material list, and bind the texture map (if the material has one), otherwise we use the diffuse color stored in the material for the submesh. Finally, we pass the sub_indices array stored in the material to the glDrawElements function to load those indices only.
    else {
      for(size_t i=0;i<materials.size();i++) {
        GLint whichID[1];
        glGetIntegerv(GL_TEXTURE_BINDING_2D, whichID);
        if(materials[i]->textureMaps.size()>0) {
          if(whichID[0] != textureMaps[materials[i]->textureMaps[0]->filename]) {
            glBindTexture(GL_TEXTURE_2D, textureMaps[materials[i]->textureMaps[0]->filename]);
          }
          glUniform1f(shader("hasTexture"),1.0);
        } else {
          glUniform1f(shader("hasTexture"),0.0);
        }
        glUniform3fv(shader("diffuse_color"),1, materials[i]->diffuse);
        glDrawElements(GL_TRIANGLES, materials[i]->sub_indices.size(), GL_UNSIGNED_SHORT, &(materials[i]->sub_indices[0]));
      }
    }
    shader.UnUse();

How it works…

The main component of this recipe is the C3dsLoader::Load3DS function. The 3ds file is a binary file which is organized into a collection of chunks. Typically, a reader reads the first two bytes from the file which are stored in the chunk ID. The next four bytes store the chunk length in bytes. We continue reading chunks, and their lengths, and then store data appropriately into our vectors/variables until there are no more chunks and we pass reading the end of file. The 3ds specifications detail all of the chunks and their lengths as well as subchunks, as shown in the following figure:

How it works…

Note that if there is a subchunk that we are interested in, we need to read the parent chunk as well, to move the file pointer to the appropriate offset in the file, for our required chunk. The loader first finds the total size of the 3ds mesh file in bytes. Then, it runs a while loop that checks to see if the current file pointer is within the file's size. If it is, it continues to read the first two bytes (the chunk's ID) and the next four bytes (the chunk's length).

while(infile.tellg() < fileSize) {
  infile.read(reinterpret_cast<char*>(&chunk_id), 2);
  infile.read(reinterpret_cast<char*>(&chunk_length), 4);

Then we start a big switch case with all of the required chunk IDs and then read the bytes from the respective chunks as desired.

  switch(chunk_id) {
    case 0x4d4d: break;
    case 0x3d3d: break;
    case 0x4000: {
      std::string name = "";
      char c = ' ';
      while(c!='') {
        infile.read(&c,1);
        name.push_back(c);
      }
      pMesh = new C3dsMesh(name);
      meshes.push_back(pMesh);
    } break;
    …//rest of the chunks
  }

All names (object name, material name, or texture map name) have to be read byte-by-byte until the null terminator character () is found. For reading vertices, we first read two bytes that store the total number of vertices (N). Two bytes means that the maximum number of vertices one mesh can store is 65536. Then, we read the whole chunk of bytes, that is, sizeof(glm::vec3)*N, directly into our mesh's vertices, shown as follows:

case 0x4110: {
  unsigned short total_vertices=0;
  infile.read(reinterpret_cast<char*>(&total_vertices), 2);
  pMesh->vertices.resize(total_vertices);
  infile.read(reinterpret_cast<char*>(&pMesh->vertices[0].x), sizeof(glm::vec3)   *total_vertices);
}break;

Similar to how the vertex information is stored, the face information stores the three unsigned short indices of the triangle and another unsigned short index containing the face flags. Therefore, for a mesh with M triangles, we have to read 4*M unsigned shorts from the file. We store the four unsigned shorts into a Face struct for convenience and then read the contents, as shown in the following code snippet:

case 0x4120: {
  unsigned short total_tris=0;
  infile.read(reinterpret_cast<char*>(&total_tris), 2);
  pMesh->faces.resize(total_tris);
  infile.read(reinterpret_cast<char*>(&pMesh->faces[0].a), sizeof(Face)*total_tris);
}break;

The code for reading the material face IDs and texture coordinates follows in the same way as the total entries are first read and then the appropriate number of bytes are read from the file. Note that, if a chunk has a color chunk (as for chunk IDs: 0xa010 to 0xa030), the color information is contained in a subchunk (IDs: 0x0010 to 0x0013) depending on the data type used to store the color information in the parent chunk.

After the mesh and material information is loaded, we generate global vertices, uvs, and indices vectors. This makes it easy for us to render the submeshes in the render function.

size_t total = materials.size();
for(size_t i=0;i<total;i++) {
  if(materials[i]->face_ids.size()==0)
  materials.erase(materials.begin()+i);
}

for(size_t i=0;i<meshes.size();i++) {
  for(size_t j=0;j<meshes[i]->vertices.size();j++)
  vertices.push_back(meshes[i]->vertices[j]);

  for(size_t j=0;j<meshes[i]->uvs.size();j++)
  uvs.push_back(meshes[i]->uvs[j]);

  for(size_t j=0;j<meshes[i]->faces.size();j++) {
    faces.push_back(meshes[i]->faces[j]);
  }
}

Note that the 3ds format does not store the per-vertex normal explicitly. It only stores smoothing groups which tell us which faces have shared normals. After we have the vertex positions and face information, we can generate the per-vertex normals by averaging the per-face normals. This is carried out by using the following code snippet in the 3ds.cpp file. We first allocate space for the per-vertex normals. Then we estimate the face's normal by using the cross product of the two edges. Finally, we add the face normal to the appropriate vertex index and then normalize the normal.

normals.resize(vertices.size());
for(size_t j=0;j<faces.size();j++) {
  Face f = faces[j];
  glm::vec3 v0 = vertices[f.a];
  glm::vec3 v1 = vertices[f.b];
  glm::vec3 v2 = vertices[f.c];
  glm::vec3 e1 = v1 - v0;
  glm::vec3 e2 = v2 - v0;
  glm::vec3 N = glm::cross(e1,e2);
  normals[f.a] += N;
  normals[f.b] += N;
  normals[f.c] += N;
}
for(size_t i=0;i<normals.size();i++) {
  normals[i]=glm::normalize(normals[i]);
}

Once we have all the per-vertex attributes and faces information, we use this to group the triangles by material. We loop through all of the materials and expand their face IDs to include the three vertex IDs and make the face.

for(size_t i=0;i<materials.size();i++) {
   Material* pMat = materials[i];
   for(int j=0;j<pMat->face_ids.size();j++) {
      pMat->sub_indices.push_back(faces[pMat->face_ids[j]].a);
      pMat->sub_indices.push_back(faces[pMat->face_ids[j]].b);
      pMat->sub_indices.push_back(faces[pMat->face_ids[j]].c);
   }
}

There's more…

The output from the demo application implementing this recipe is given in the following figure. In this recipe, we render three blocks on a quad plane. The camera position can be changed using the left mouse button. The point light source position can be changed using the right mouse button. Each block has six textures attached to it, whereas the plane has no texture, hence it uses the diffuse color value.

There's more…

Note that the 3ds loader shown in this recipe does not take smoothing groups into consideration. For a more robust loader, we recommend the lib3ds library which provides a more elaborate 3ds file loader with support for smoothing groups, animation tracks, cameras, lights, keyframes, and so on.

See also

For more information on implementing 3ds model loading, you can refer to the following links:

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

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