Search code examples
c++openglglfwwavefrontvao

OpenGL vertex array objects with tinyobjloader


In order to use modern openGl with tinyobjloader, I'm trying to change the viewer exemple. I just change the LoadObjAndConvert function, to add vertex array objects as i seen in this tutorial, and to no longer use the buffer object that contains all the data (position, indices, color, uv) because it seems that we can no longer use it with modern openGL.

Result look like I have bad vertex index, the model is only partly rendered, and if the model has only one mesh (the stanford bunny) it does not even show up.

The code is too long, but it is the same as the tinyobjloader viewer exemple, so I will only post functions that are different.

Here is the LoadObjAndConvert function modified (modified parts are between lines to help) :

static bool LoadObjAndConvert(float bmin[3], float bmax[3],
                          std::vector<DrawObject>* drawObjects,
                          std::vector<tinyobj::material_t>& materials,
                          std::map<std::string, GLuint>& textures,
                          const char* filename) {
tinyobj::attrib_t attrib;
std::vector<tinyobj::shape_t> shapes;

timerutil tm;

tm.start();

std::string base_dir = GetBaseDir(filename);
if (base_dir.empty()) {
  base_dir = ".";
}
#ifdef _WIN32
  base_dir += "\\";
#else
  base_dir += "/";
#endif

std::string err;
bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &err, filename,
                          base_dir.c_str());
if (!err.empty()) {
  std::cerr << err << std::endl;
}

tm.end();

if (!ret) {
  std::cerr << "Failed to load " << filename << std::endl;
  return false;
}

printf("Parsing time: %d [ms]\n", (int)tm.msec());

printf("# of vertices  = %d\n", (int)(attrib.vertices.size()) / 3);
printf("# of normals   = %d\n", (int)(attrib.normals.size()) / 3);
printf("# of texcoords = %d\n", (int)(attrib.texcoords.size()) / 2);
printf("# of materials = %d\n", (int)materials.size());
printf("# of shapes    = %d\n", (int)shapes.size());

// Append `default` material
materials.push_back(tinyobj::material_t());

for (size_t i = 0; i < materials.size(); i++) {
  printf("material[%d].diffuse_texname = %s\n", int(i),
         materials[i].diffuse_texname.c_str());
}

// Load diffuse textures
{
  for (size_t m = 0; m < materials.size(); m++) {
    tinyobj::material_t* mp = &materials[m];

    if (mp->diffuse_texname.length() > 0) {
      // Only load the texture if it is not already loaded
      if (textures.find(mp->diffuse_texname) == textures.end()) {
        GLuint texture_id;
        int w, h;
        int comp;

      std::string texture_filename = mp->diffuse_texname;
      if (!FileExists(texture_filename)) {
        // Append base dir.
        texture_filename = base_dir + mp->diffuse_texname;
        if (!FileExists(texture_filename)) {
          std::cerr << "Unable to find file: " << mp->diffuse_texname
                    << std::endl;
          exit(1);
        }
      }

      unsigned char* image =
          stbi_load(texture_filename.c_str(), &w, &h, &comp, STBI_default);
      if (!image) {
        std::cerr << "Unable to load texture: " << texture_filename
                  << std::endl;
        exit(1);
      }
      std::cout << "Loaded texture: " << texture_filename << ", w = " << w
                << ", h = " << h << ", comp = " << comp << std::endl;

      glGenTextures(1, &texture_id);
      glBindTexture(GL_TEXTURE_2D, texture_id);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
      if (comp == 3) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB,
                     GL_UNSIGNED_BYTE, image);
      } else if (comp == 4) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA,
                     GL_UNSIGNED_BYTE, image);
      } else {
        assert(0);  // TODO
      }
      glBindTexture(GL_TEXTURE_2D, 0);
      stbi_image_free(image);
      textures.insert(std::make_pair(mp->diffuse_texname, texture_id));
    }
   }
  }
}

bmin[0] = bmin[1] = bmin[2] = std::numeric_limits<float>::max();
bmax[0] = bmax[1] = bmax[2] = -std::numeric_limits<float>::max();

{
  for (size_t s = 0; s < shapes.size(); s++) {
/*-----------------------------------------------------------*/
    DrawObject o;// I keep this object for later purpose, texture, etc

    //std::vector<float> buffer; // pos(3float), normal(3float), color(3float)
    //I replace "buffer" by arrays:
    std::vector<GLfloat> mesh_vertex;
    std::vector<GLfloat> mesh_normals;
    std::vector<GLfloat> mesh_colors;
    std::vector<GLfloat> mesh_textCoords;
    std::vector<GLuint> mesh_indices;

  /*fill index array*/
  for (long i = 0; i < shapes[s].mesh.indices.size(); i++)
  {
    mesh_indices.push_back(shapes[s].mesh.indices[i].vertex_index);
  }
/*-----------------------------------------------------------*/
  // Check for smoothing group and compute smoothing normals
  std::map<int, vec3> smoothVertexNormals;
  if (hasSmoothingGroup(shapes[s]) > 0) {
    std::cout << "Compute smoothingNormal for shape [" << s << "]" << std::endl;
    computeSmoothingNormals(attrib, shapes[s], smoothVertexNormals);
  }

  for (size_t f = 0; f < shapes[s].mesh.indices.size() / 3; f++) {
    tinyobj::index_t idx0 = shapes[s].mesh.indices[3 * f + 0];
    tinyobj::index_t idx1 = shapes[s].mesh.indices[3 * f + 1];
    tinyobj::index_t idx2 = shapes[s].mesh.indices[3 * f + 2];

    int current_material_id = shapes[s].mesh.material_ids[f];

    if ((current_material_id < 0) ||
        (current_material_id >= static_cast<int>(materials.size()))) {
      // Invaid material ID. Use default material.
      current_material_id =
          materials.size() -
          1;  // Default material is added to the last item in `materials`.
    }
    // if (current_material_id >= materials.size()) {
    //    std::cerr << "Invalid material index: " << current_material_id <<
    //    std::endl;
    //}
    //
    float diffuse[3];
    for (size_t i = 0; i < 3; i++) {
      diffuse[i] = materials[current_material_id].diffuse[i];
    }
    float tc[3][2];
    if (attrib.texcoords.size() > 0) {
      if ((idx0.texcoord_index < 0) || (idx1.texcoord_index < 0) ||
          (idx2.texcoord_index < 0)) {
        // face does not contain valid uv index.
        tc[0][0] = 0.0f;
        tc[0][1] = 0.0f;
        tc[1][0] = 0.0f;
        tc[1][1] = 0.0f;
        tc[2][0] = 0.0f;
        tc[2][1] = 0.0f;
      } else {
        assert(attrib.texcoords.size() >
               size_t(2 * idx0.texcoord_index + 1));
        assert(attrib.texcoords.size() >
               size_t(2 * idx1.texcoord_index + 1));
        assert(attrib.texcoords.size() >
               size_t(2 * idx2.texcoord_index + 1));

        // Flip Y coord.
        tc[0][0] = attrib.texcoords[2 * idx0.texcoord_index];
        tc[0][1] = 1.0f - attrib.texcoords[2 * idx0.texcoord_index + 1];
        tc[1][0] = attrib.texcoords[2 * idx1.texcoord_index];
        tc[1][1] = 1.0f - attrib.texcoords[2 * idx1.texcoord_index + 1];
        tc[2][0] = attrib.texcoords[2 * idx2.texcoord_index];
        tc[2][1] = 1.0f - attrib.texcoords[2 * idx2.texcoord_index + 1];
      }
    } else {
      tc[0][0] = 0.0f;
      tc[0][1] = 0.0f;
      tc[1][0] = 0.0f;
      tc[1][1] = 0.0f;
      tc[2][0] = 0.0f;
      tc[2][1] = 0.0f;
    }

    float v[3][3];
    for (int k = 0; k < 3; k++) {
      int f0 = idx0.vertex_index;
      int f1 = idx1.vertex_index;
      int f2 = idx2.vertex_index;
      assert(f0 >= 0);
      assert(f1 >= 0);
      assert(f2 >= 0);

      v[0][k] = attrib.vertices[3 * f0 + k];
      v[1][k] = attrib.vertices[3 * f1 + k];
      v[2][k] = attrib.vertices[3 * f2 + k];
      bmin[k] = std::min(v[0][k], bmin[k]);
      bmin[k] = std::min(v[1][k], bmin[k]);
      bmin[k] = std::min(v[2][k], bmin[k]);
      bmax[k] = std::max(v[0][k], bmax[k]);
      bmax[k] = std::max(v[1][k], bmax[k]);
      bmax[k] = std::max(v[2][k], bmax[k]);
    }

    float n[3][3];
    {
      bool invalid_normal_index = false;
      if (attrib.normals.size() > 0) {
        int nf0 = idx0.normal_index;
        int nf1 = idx1.normal_index;
        int nf2 = idx2.normal_index;

        if ((nf0 < 0) || (nf1 < 0) || (nf2 < 0)) {
          // normal index is missing from this face.
          invalid_normal_index = true;
        } else {
          for (int k = 0; k < 3; k++) {
            assert(size_t(3 * nf0 + k) < attrib.normals.size());
            assert(size_t(3 * nf1 + k) < attrib.normals.size());
            assert(size_t(3 * nf2 + k) < attrib.normals.size());
            n[0][k] = attrib.normals[3 * nf0 + k];
            n[1][k] = attrib.normals[3 * nf1 + k];
            n[2][k] = attrib.normals[3 * nf2 + k];
          }
        }
      } else {
        invalid_normal_index = true;
      }

      if (invalid_normal_index && !smoothVertexNormals.empty()) {
        // Use smoothing normals
        int f0 = idx0.vertex_index;
        int f1 = idx1.vertex_index;
        int f2 = idx2.vertex_index;

        if (f0 >= 0 && f1 >= 0 && f2 >= 0) {
          n[0][0] = smoothVertexNormals[f0].v[0];
          n[0][1] = smoothVertexNormals[f0].v[1];
          n[0][2] = smoothVertexNormals[f0].v[2];

          n[1][0] = smoothVertexNormals[f1].v[0];
          n[1][1] = smoothVertexNormals[f1].v[1];
          n[1][2] = smoothVertexNormals[f1].v[2];

          n[2][0] = smoothVertexNormals[f2].v[0];
          n[2][1] = smoothVertexNormals[f2].v[1];
          n[2][2] = smoothVertexNormals[f2].v[2];

          invalid_normal_index = false;
        }
      }

      if (invalid_normal_index) {
        // compute geometric normal
        CalcNormal(n[0], v[0], v[1], v[2]);
        n[1][0] = n[0][0];
        n[1][1] = n[0][1];
        n[1][2] = n[0][2];
        n[2][0] = n[0][0];
        n[2][1] = n[0][1];
        n[2][2] = n[0][2];
      }
    }

    for (int k = 0; k < 3; k++) {
/*-----------------------------------------------------------*/
// I leave old calls to "buffer" in comment for understanding
      //buffer.push_back(v[k][0]);
      //buffer.push_back(v[k][1]);
      //buffer.push_back(v[k][2]);
      mesh_vertex.push_back(v[k][0]);
      mesh_vertex.push_back(v[k][1]);
      mesh_vertex.push_back(v[k][2]);

      //buffer.push_back(n[k][0]);
      //buffer.push_back(n[k][1]);
      //buffer.push_back(n[k][2]);
      mesh_normals.push_back(n[k][0]);
      mesh_normals.push_back(n[k][1]);
      mesh_normals.push_back(n[k][2]);

      // Combine normal and diffuse to get color.
      float normal_factor = 0.2;
      float diffuse_factor = 1 - normal_factor;
      float c[3] = {n[k][0] * normal_factor + diffuse[0] * diffuse_factor,
                    n[k][1] * normal_factor + diffuse[1] * diffuse_factor,
                    n[k][2] * normal_factor + diffuse[2] * diffuse_factor};
      float len2 = c[0] * c[0] + c[1] * c[1] + c[2] * c[2];
      if (len2 > 0.0f) {
        float len = sqrtf(len2);

        c[0] /= len;
        c[1] /= len;
        c[2] /= len;
      }
      //buffer.push_back(c[0] * 0.5 + 0.5);
      //buffer.push_back(c[1] * 0.5 + 0.5);
      //buffer.push_back(c[2] * 0.5 + 0.5);
      mesh_colors.push_back(c[0] * 0.5 + 0.5);
      mesh_colors.push_back(c[1] * 0.5 + 0.5);
      mesh_colors.push_back(c[2] * 0.5 + 0.5);

      //buffer.push_back(tc[k][0]);
      //buffer.push_back(tc[k][1]);
      mesh_textCoords.push_back(tc[k][0]);
      mesh_textCoords.push_back(tc[k][1]);
/*-----------------------------------------------------------*/
    }
  }

  o.vb_id = 0;
  o.numTriangles = 0;

  // OpenGL viewer does not support texturing with per-face material.
  if (shapes[s].mesh.material_ids.size() > 0 &&
      shapes[s].mesh.material_ids.size() > s) {
    o.material_id = shapes[s].mesh.material_ids[0];  // use the material ID
                                                     // of the first face.
  } else {
    o.material_id = materials.size() - 1;  // = ID for default material.
  }
  printf("shape[%d] material_id %d\n", int(s), int(o.material_id));
/*-----------------------------------------------------------*/
  /*if (buffer.size() > 0) {
    glGenBuffers(1, &o.vb_id);
    glBindBuffer(GL_ARRAY_BUFFER, o.vb_id);
    glBufferData(GL_ARRAY_BUFFER, buffer.size() * sizeof(float),
                 &buffer.at(0), GL_STATIC_DRAW);
    o.numTriangles = buffer.size() / (3 + 3 + 3 + 2) /
                     3;  // 3:vtx, 3:normal, 3:col, 2:texcoord

    printf("shape[%d] # of triangles = %d\n", static_cast<int>(s),
           o.numTriangles);
  }
  drawObjects->push_back(o);*/
  // Replace by :
  GLuint positionVBO = 0;
  GLuint texcoordVBO = 0;
  GLuint normalVBO = 0;
  GLuint indicesEBO = 0;

  // Upload per-vertex positions
  if (!mesh_vertex.empty())
  {
   glGenBuffers(1, &positionVBO);
   glBindBuffer(GL_ARRAY_BUFFER, positionVBO);
   glBufferData(GL_ARRAY_BUFFER, mesh_vertex.size() * sizeof(GLfloat), &mesh_vertex[0], GL_STATIC_DRAW); // GL_DYNAMIC_DRAW ?
   glBindBuffer(GL_ARRAY_BUFFER, 0);
   positionVBO_array.push_back(positionVBO);
  }

  // Upload per-vertex texture coordinates
  if (!mesh_textCoords.empty())
  {
   glGenBuffers(1, &texcoordVBO);
   glBindBuffer(GL_ARRAY_BUFFER, texcoordVBO);
   glBufferData(GL_ARRAY_BUFFER,
          mesh_textCoords.size() * sizeof(float),
          &mesh_textCoords[0], GL_STATIC_DRAW);
   glBindBuffer(GL_ARRAY_BUFFER, 0);
  }

  // Upload per-vertex normals
  if (!mesh_normals.empty())
  {
   glGenBuffers(1, &normalVBO);
   glBindBuffer(GL_ARRAY_BUFFER, normalVBO);
   glBufferData(GL_ARRAY_BUFFER, mesh_normals.size() * sizeof(GLfloat), &mesh_normals[0], GL_STATIC_DRAW);
   glBindBuffer(GL_ARRAY_BUFFER, 0);
   normalVBO_array.push_back(normalVBO);
  }

  // Upload the indices that form triangles
  if (!shapes[0].mesh.indices.empty())
  {
   glGenBuffers(1, &indicesEBO);
   glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indicesEBO);
   glBufferData(GL_ELEMENT_ARRAY_BUFFER,
          shapes[s].mesh.indices.size() * sizeof(unsigned int),
          shapes[s].mesh.indices.data(), GL_STATIC_DRAW);
   glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

   indicesEBO_array.push_back(indicesEBO);
   indicesEBOSize_array.push_back(shapes[s].mesh.indices.size());
  }

  // Hook up vertex/index buffers to a "vertex array object" (VAO)
  // VAOs are the closest thing OpenGL has to a "mesh" object.
  // VAOs feed data from buffers to the inputs of a vertex shader.
  GLuint meshVAO;
  vglGenVertexArrays(1, &meshVAO);
  meshVAO_array.push_back(meshVAO);// I keep the ids in order to loop inside meshVAO_array in the draw function

                             // Attach position buffer as attribute 0
  if (positionVBO != 0)
  {
   glBindVertexArray(meshVAO);

   // Note: glVertexAttribPointer sets the current
   // GL_ARRAY_BUFFER_BINDING as the source of data
   // for this attribute.
   // That's why we bind a GL_ARRAY_BUFFER before
   // calling glVertexAttribPointer then
   // unbind right after (to clean things up).
   glBindBuffer(GL_ARRAY_BUFFER, positionVBO);
   glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
          sizeof(float) * 3, 0);
   glBindBuffer(GL_ARRAY_BUFFER, 0);
   // Enable the attribute (they are disabled by default
   // -- this is very easy to forget!!)
   glEnableVertexAttribArray(0);
   glBindVertexArray(0);
   }

   // Attach texcoord buffer as attribute 1
   if (texcoordVBO != 0)
   {
   glBindVertexArray(meshVAO);
   glBindBuffer(GL_ARRAY_BUFFER, texcoordVBO);
   glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE,
          sizeof(float) * 2, 0);
   glBindBuffer(GL_ARRAY_BUFFER, 0);
   glEnableVertexAttribArray(1);
   glBindVertexArray(0);
   }

  // Attach normal buffer as attribute 2
  if (normalVBO != 0)
  {
   glBindVertexArray(meshVAO);
   glBindBuffer(GL_ARRAY_BUFFER, normalVBO);
   glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE,
          sizeof(float) * 3, 0);
   glBindBuffer(GL_ARRAY_BUFFER, 0);
   glEnableVertexAttribArray(2);
   glBindVertexArray(0);
  }

    if (indicesEBO != 0)
    {
     glBindVertexArray(meshVAO);

     // Note: Calling glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
     // when a VAO is bound attaches the index buffer to the VAO.
     // From an API design perspective, this is subtle.
     glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indicesEBO);

     glBindVertexArray(0);
    }
/*-----------------------------------------------------------*/
  }
}

printf("bmin = %f, %f, %f\n", bmin[0], bmin[1], bmin[2]);
printf("bmax = %f, %f, %f\n", bmax[0], bmax[1], bmax[2]);

return true;
}

(Sorry for this long code block)

And here is the while loop of the main function, the only difference with tinyobjloader is between the two lines:

unsigned int program = shaders::CreateShader("data/simple.vert", "data/simple.frag"); // just some really simples shaders

while (glfwWindowShouldClose(window) == GL_FALSE) {
  glfwPollEvents();
  glClearColor(0.1f, 0.2f, 0.3f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glEnable(GL_DEPTH_TEST);
  glEnable(GL_TEXTURE_2D);

  // camera & rotate
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  GLfloat mat[4][4];
  gluLookAt(eye[0], eye[1], eye[2], lookat[0], lookat[1], lookat[2], up[0],
          up[1], up[2]);
  build_rotmatrix(mat, curr_quat);
  glMultMatrixf(&mat[0][0]);

  // Fit to -1, 1
  glScalef(1.0f / maxExtent, 1.0f / maxExtent, 1.0f / maxExtent);

  // Centerize object.
  glTranslatef(-0.5 * (bmax[0] + bmin[0]), -0.5 * (bmax[1] + bmin[1]),
             -0.5 * (bmax[2] + bmin[2]));
  /*-----------------------------------------------------------*/
  //Draw(gDrawObjects, materials, textures);
  // Can now bind the vertex array object to
  // the graphics pipeline, to render with it.
  glUseProgram(program);
  for (int s = 0; s < meshVAO_array.size(); s++)
  {
    glBindVertexArray(meshVAO_array[s]);
    glDrawElements(GL_TRIANGLES, indicesEBOSize_array[s], GL_UNSIGNED_INT, 0);//mesh.IndexCount
    glBindVertexArray(0);
  }
  glUseProgram(0);
  // when done, unbind it from the graphics pipeline:
  glBindVertexArray(0); 
  /*-----------------------------------------------------------*/

  glfwSwapBuffers(window);
}

What am I doing wrong?


Solution

  • In the nested loops you all the indices of shapes[].mesh.indices are use to lokkup the attributes, which are stored in attrib.vertices, attrib.normals and attrib.texcoords. This attributes are prepared and linearized. They are stored in there idexed order to the linear arrays mesh_vertex, mesh_normals, mesh_colors and mesh_textCoords.

    But the indices are directly copied from shapes[].mesh.indices to mesh_indices

    for (long i = 0; i < shapes[s].mesh.indices.size(); i++)
    {
        mesh_indices.push_back(shapes[s].mesh.indices[i].vertex_index);
    }
    

    The indices in mesh_indices still refer to the vertex coordinates stored in attrib.vertices but the have no meaning for the attributes in the new containers. The original indices are not needed any more. The indices of the new attribute would be continuously ascending: [0, 1, 2, 3, 4, 5 ...]

    It is sufficient to draw the array of generic vertex attribute data in its existing order:

    // you have to know the number of attributes
    // something like  mesh_vertex.size() / 3;
    GLsizei no_of_attributes = .... ;  
    
    glBindVertexArray(meshVAO_array[s]);
    glDrawArrays(GL_TRIANGLES, 0, no_of_attributes);
    glBindVertexArray(0);