Drawing Cube with indices

Last time we saw how we can draw a cube in OpenGL, however there is another technique used to do this called indexed drawing which is a bit more efficient in some cases. If you were paying attention in the last tutorial, you would have noticed that we used 6 vertices for every face of the cube. But we know that a face of the cube is just a square which requires only 4 vertices. The trick here is to think in triangles just like the GPU thinks. What is the minimum number of triangles we need for one square? The answer is 2. Each triangle will have 3 vertices so we’ll end up with 6 vertices where 2 vertices overlaps.

Here comes the indexed drawing technique which will help us reduce the number of vertices from 6 to 4 for our square.This is great, because now we only have 4 vertices in GPU memory and shaders will have to process less vertices. Overall we will gain some performance. If we have 1000 squares with normal drawing that would be 6000 vertices to keep in memory and process them. With indexed drawing that would be only 4000. So 2000 vertices difference.

How does this work? Beside our vertex buffer we will have an extra buffer (array) called index buffer which holds the positions (unsigned int) of the vertices which are stored in the vertex buffer. Later, the GPU will make an analysis on the indexed buffer to see how it is supposed to create the triangles. Every 3 indices a new triangle is created. Let’s look at the following image to understand this method by comparing it with the normal technique.

Draw indexed OpenGL

Draw indexed OpenGL

On modern GPUs if the shaders are lightweight with just a few operations you will not see this performance boost and you can relax and use the normal drawing technique, especially when you draw just different squares. Also there might be the problem of cache misses when we use indexed drawing if the vertices used for our geometry are far away one from another in memory. So it’s better to have our geometry sorted in someway, before we index it and create the drawing buffers.

The real power of indexed drawing kicks in when a vertex is duplicated for many times. A good example is the circle, where the center vertex is duplicated for every triangle (360 times ).

Indexed drawing for circle

Indexed drawing for circle (click to zoom)

Note!!! It is useless to use indexed drawing if you need vertices with different properties. For example if you need all vertices with different colors or normals just use normal drawing.

Indexed drawing vs Triangle Strip

If you remember in Chapter I we rendered a square using Triangle_Strip. So we know two ways to draw a square using only 4 vertices: Indexed drawing and normal drawing with triangle strip mode. But are there any difference between the two? The answer is yes.

Triangle strip mode is used to render only convex shapes where the order of vertices in the vertex buffer matters because it creates the triangles based on the two last vertices + the next one. If we have two or more shapes in the same vertex buffer we have to make some degenerated triangles (2 point in the same place) to hide the connected triangle between the shapes (see picture below). These degenerated triangles can be seen in wire-frame mode. The triangle strip mode is mostly used for simple shapes in 2D rendering.

In the indexed technique, we can form a triangle from any 3 vertices from the vertex buffer. We can have more shapes in the same buffer and we can draw concave geometry without pain.

Triangle Strip vs Indexed mode

Triangle Strip (note that red is the degenerated triangle) vs Indexed mode

Performance wise, triangle strip mode is better, because the GPU doesn’t have to process another buffer, but again we are super limited to the shape geometry. Just imagine how hard it is to draw a cube with triangle strip – you are always constrained by the last two vertices when you need to create a new triangle. So obviously I would use indexed drawing to draw our cube.

Drawing the cube 

First let’s create a new project called c2_2_DrawCubeIndex and add the same dependencies like we did for c2_1_DrawCube. You should have 3 projects in the same solution.

  • We are going to use the same shaders used for the first cube. So copy the shader folder in the new project
  • Copy Cube.h and Cube.cpp and modify their names to CubeIndex.h and CubeIndex.cpp and later we will modify them to use indexed drawing.
  • Copy main.cpp.

Finally the project should look like this:

Draw Cube Index Project

Draw Cube Index Project

Now in CubeIndex.h we only need to modify the class to CubeIndex

#pragma once
#include <BasicEngine\Rendering\Models\Model.h>
#include <time.h>
#include <stdarg.h>

using namespace BasicEngine::Rendering::Models;
class CubeIndex : public Model
{
  public:
    CubeIndex();
   ~CubeIndex();

    void Create();
    virtual void Draw(const glm::mat4& projection_matrix,
                      const glm::mat4& view_matrix) override final;
    virtual void Update() override final;

  private:
   glm::vec3 rotation, rotation_speed;
   time_t timer;
};

One possible representation of the cube with indices can be seen in the following image:

Drawing cube with indices

Drawing cube with indices

Now we need to create this cube using indices in the Create() method just like in the image above:

  • First we need to reduce the number of vertices from the old cube to 24.
  • Second we create the index array. This is going to be an unsigned int array, we can’t have negative indices. Also we can use a vector for it:                std::vector<unsigned int>  indices
  • Next we need to fill this index vector with vertices positions:
    //note every face of the cube is on a single line
     std::vector<unsigned int> indices = {
     0,  1,  2,  0,  2,  3,   //front
     4,  5,  6,  4,  6,  7,   //right
     8,  9,  10, 8,  10, 11,  //back
     12, 13, 14, 12, 14, 15,  //left
     16, 17, 18, 16, 18, 19,  //upper
     20, 21, 22, 20, 22, 23}; //bottom
    
  • Finally we need to create an Index Buffer Object (IBO or ibo) to send our indices to the GPU. This ibo must be created and bound under the VAO just like the VBO. The only thing different from the VBO binding is the binding point. For an ibo, we have to use GL_ELEMENT_ARRAY_BUFFER as a binding point/buffer type.
    GLuint ibo;
    glGenBuffers(1, &ibo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int),
                 &indices[0], GL_STATIC_DRAW);
    //this ibo must be released from gpu memory later so
    // we have to keep it next to the vbo
    this->vbos.push_back(ibo);
    

The whole Create method should look like this;

void CubeIndex::Create()
{
  GLuint vao;
  GLuint vbo;
  GLuint ibo;

  time(&timer);

  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);

  std::vector<VertexFormat> vertices;
  std::vector<unsigned int> indices = { 0, 1, 2, 0, 2, 3, //front
                                        4, 5, 6, 4, 6, 7, //right
                                        8, 9, 10, 8, 10, 11, //back
                                        12, 13, 14, 12, 14, 15, //left
                                        16, 17, 18, 16, 18, 19, //upper
                                        20, 21, 22, 20, 22, 23}; //bottom

  //front
  vertices.push_back(VertexFormat(glm::vec3(-1.0, -1.0, 1.0),
                                  glm::vec4(0, 0, 1, 1)));
  vertices.push_back(VertexFormat(glm::vec3( 1.0, -1.0, 1.0),
                                  glm::vec4(1, 0, 1, 1)));
  vertices.push_back(VertexFormat(glm::vec3( 1.0, 1.0, 1.0),
                                  glm::vec4(1, 1, 1, 1)));
  vertices.push_back(VertexFormat(glm::vec3(-1.0, 1.0, 1.0),
                                  glm::vec4(0, 1, 1, 1)));

  //right
  vertices.push_back(VertexFormat(glm::vec3(1.0, 1.0, 1.0),
                                  glm::vec4(1, 1, 1, 1)));
  vertices.push_back(VertexFormat(glm::vec3(1.0, 1.0, -1.0),
                                  glm::vec4(1, 1, 0, 1)));
  vertices.push_back(VertexFormat(glm::vec3(1.0, -1.0, -1.0),
                                  glm::vec4(1, 0, 0, 1)));
  vertices.push_back(VertexFormat(glm::vec3(1.0, -1.0, 1.0),
                                  glm::vec4(1, 0, 1, 1)));

  //back
  vertices.push_back(VertexFormat(glm::vec3(-1.0, -1.0, -1.0),
                                  glm::vec4(0, 0, 0, 1)));
  vertices.push_back(VertexFormat(glm::vec3( 1.0, -1.0, -1.0),
                                  glm::vec4(1, 0, 0, 1)));
  vertices.push_back(VertexFormat(glm::vec3( 1.0, 1.0, -1.0),
                                  glm::vec4(1, 1, 0, 1)));
  vertices.push_back(VertexFormat(glm::vec3(-1.0, 1.0, -1.0),
                                  glm::vec4(0, 1, 0, 1)));

  //left
  vertices.push_back(VertexFormat(glm::vec3(-1.0, -1.0, -1.0),
                                  glm::vec4(0, 0, 0, 1)));
  vertices.push_back(VertexFormat(glm::vec3(-1.0, -1.0, 1.0),
                                  glm::vec4(0, 0, 1, 1)));
  vertices.push_back(VertexFormat(glm::vec3(-1.0, 1.0, 1.0),
                                  glm::vec4(0, 1, 1, 1)));
  vertices.push_back(VertexFormat(glm::vec3(-1.0, 1.0, -1.0),
                                  glm::vec4(0, 1, 0, 1)));

  //upper
  vertices.push_back(VertexFormat(glm::vec3( 1.0, 1.0, 1.0),
                                  glm::vec4(1, 1, 1, 1)));
  vertices.push_back(VertexFormat(glm::vec3(-1.0, 1.0, 1.0),
                                  glm::vec4(0, 1, 1, 1)));
  vertices.push_back(VertexFormat(glm::vec3(-1.0, 1.0, -1.0),
                                  glm::vec4(0, 1, 0, 1)));
  vertices.push_back(VertexFormat(glm::vec3( 1.0, 1.0, -1.0),
                                  glm::vec4(1, 1, 0, 1)));

  //bottom
  vertices.push_back(VertexFormat(glm::vec3(-1.0, -1.0, -1.0),
                                  glm::vec4(0, 0, 0, 1)));
  vertices.push_back(VertexFormat(glm::vec3( 1.0, -1.0, -1.0),
                                  glm::vec4(1, 0, 0, 1)));
  vertices.push_back(VertexFormat(glm::vec3( 1.0, -1.0, 1.0),
                                  glm::vec4(1, 0, 1, 1)));
  vertices.push_back(VertexFormat(glm::vec3(-1.0, -1.0, 1.0),
                                  glm::vec4(0, 0, 1, 1)));

  glGenBuffers(1, &vbo);
  glBindBuffer(GL_ARRAY_BUFFER, vbo);
  glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(VertexFormat),
               &vertices[0], GL_STATIC_DRAW);

  glGenBuffers(1, &ibo);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int),
              &indices[0], GL_STATIC_DRAW);

  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
                        sizeof(VertexFormat), (void*)0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE,
                        sizeof(VertexFormat),
                        (void*)(offsetof(VertexFormat, VertexFormat::color)));
  glBindVertexArray(0);
  this->vao = vao;
  this->vbos.push_back(vbo);
  this->vbos.push_back(ibo);

  rotation_speed = glm::vec3(90.0, 90.0, 90.0);
  rotation = glm::vec3(0.0, 0.0, 0.0);

}

Last thing to discuss here is the OpenGL command required to render with indices in the Draw method. We are going to use glDrawElements instead of glDrawArrays. Let’s look a little bit at glDrawElements parameters:

  1. Primitive type. We want to draw triangles so we use GL_TRIANGLES.
  2. Number of indices. We have 36 for our cube.
  3. Size of each index. We used an unsigned int  for our ibo so we’ll use GL_UNSIGNED_INT
  4. Offset number. In case of multiple shapes in the same buffer.
void CubeIndex::Draw(const glm::mat4& projection_matrix,
                     const glm::mat4& view_matrix)
{
 ...
 glBindVertexArray(vao);
 glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
}

Don’t forget to modify main.cpp to accept CubeIndex.h. If you are in trouble please check our GitHub repository for this tutorial and ask us questions in the comment section below. See you in the next tutorial where we learn to add textures to our cube.

 

Tagged under:
blog comments powered by Disqus