OpenGL can be daunting to the beginnner - not only because the OpenGL API is so unlike most APIs, and maintains a mostly invisible (to the application developer) state, but also because there are often so many different ways to accomplish the same thing.

To help ease this last challenege, I’ll be presenting three different ways to draw the same data and explain why you might choose one of three for your own project.

I will also be entirely ommiting shaders. I’ve found shaders to be one of the major hurtles to learning OpenGL for the beginner and I want to just focus on basic drawing techniques. Shaders are required to take full advantage of OpenGL, but they are not required for getting something on screen.

The data we’ll be drawing is this self portrait I made in Blender using the Vertex Paint mode. I’ll go over how I did this (technically - the artistic inspiration is beyond the scope of any tutorial and took years of observing life and honing my artistic eye) and how I exported it in another post. For now all you need is this data file to get started with the tutorial.

Portrait

Data Parsing

Before we can start drawing we need to have something to draw. I’ve provided the data text file that contains all the vertex and color data we’re going to need.

Here’s my parsing function for the ‘painting.txt’ file - you’re free to just copy it, paste it in, and skip down to the fun stuff.

The only really important thing to take away here is that the parsing function returns the file data containing vectors colors and positions where each vertex is made up of three GLfloat elements (so x,y,z in positions and r,b,g in colors). The vertices are inserted in an order for wrapping quads (note the GL_QUADS mode we use later on in the code).

struct PaintFileData {
  std::vector <Glfloat> colors;
  std::vector <GLfloat> position;
};

static PaintFileData readPaintFile(std::string file_path)
{
  PaintFileData result;

  std::ifstream data_file (file_path);
  if(data_file.is_open())
  {
    bool reading_color = false;
    std::string line;
    while(getline(data_file, line))
    {
      std::size_t first = line.find(" ");
      std::size_t last = line.rfind(" ");
      if(first != std::string::npos && last != std::string::npos) {

        std::string one = line.substr(0, first);
        std::string two = line.substr(first + 1, last - first - 1);
        std::string three = line.substr(last + 1, line.length() - 1 - last);

        if(reading_color)
        {
          result.colors.push_back(std::stof(one)); // red
          result.colors.push_back(std::stof(two)); // blue
          result.colors.push_back(std::stof(three)); // green
        }
        else
        {
          if(two == "colors")
          {
            reading_color = true;
          }
          else
          {
            result.positions.push_back(std::stof(one)); // x
            result.positions.push_back(std::stof(two)); // y
            result.positions.push_back(std::stof(three)); // z
          }
        }
      }
    }
    data_file.close();
  }
  return result;
}

Immediate Mode

Immediate mode is well named because it gets you drawing immediately. There’s very little setup required to get something on screen.

In your draw function, you’d have this code run:

GLuint vertex_size = 3;

glBegin(GL_QUADS);
for(int i = 0; i < m_num_verts; i += vertex_size)
{
  GLfloat r = m_colors[i];
  GLfloat g = m_colors[i + 1];
  GLfloat b = m_colors[i + 2];
  glColor3f(r, g, b);

  GLfloat x = m_positions[i];
  GLfloat y = m_positions[i + 1];
  GLfloat z = m_positions[i + 2];
  glVertex3f(x, y, z);
}
glEnd();

Note here that m_num_verts is a length value shared by the color and position vertices.

Immediate mode is nice because it doesn’t require many lines of code and you can quickly test positioning of your camera. Positioning your camera can be very difficult because even if all your code is correct and working, you still might not see anything on your screen when you compile.

Drawing Arrays

Now we’re getting closer to “production level” OpenGL. Drawing with arrays is more performant than drawing in immediate mode because it includes fewer calls to the OpenGL API once rendering has begun.

Once again, the following code takes place in the draw method called every time you want to refresh your view.

GLuint vertex_size = 3;

glVertexPointer(vertex_size, GL_FLOAT, 0, &m_positions[0]);
glColorPointer (vertex_size, GL_FLOAT, 0, &m_colors[0]);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glDrawArrays(GL_QUADS, 0, m_num_verts / vertex_size);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);

Here you can see that the syntax completely changes from immediate mode. We now introduce the ideas of changing state and setting pointers to our data.

Drawing Arrays with Vertex Buffered Objects

Drawing with VBOs requires a ‘setup’ phase. This can either be called once from your draw function, or depending on your OpenGL context, called from a ‘Setup()’ or ‘init()’ hook. In my program, I’m drawing with OpenGL using the Greenhouse SDK.

Here’s the code you’ll need to setup your buffers in the ‘init’ hook:

// class member variables
GLuint array_id;
GLuint color_id;

...

// one-time setup code
glGenBuffers(1, &array_id);
glBindBuffer(GL_ARRAY_BUFFER, array_id);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * m_num_verts, &m_positions[0], GL_STATIC_DRAW);

glGenBuffers(1, &color_id);
glBindBuffer(GL_ARRAY_BUFFER, color_id);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * m_num_colors, &m_colors[0], GL_STATIC_DRAW);

Your draw logic will similar to drawing just with arrays, but notice the new calls to glBindBuffer:

glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);

glBindBuffer(GL_ARRAY_BUFFER, array_id);
glVertexPointer(vertex_size, GL_FLOAT, 0, 0);

glBindBuffer(GL_ARRAY_BUFFER, color_id);
glColorPointer(vertex_size, GL_FLOAT, 0, 0);

glDrawArrays(GL_QUADS, 0, m_num_verts);

glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);

Drawing with Vertex Buffered Objects (VBOs) takes the efficiencies of drawing with arrays even further by loading your data into OpenGL upfront instead of at draw time. However, this comes at a cost. Only use VBOs if you will not be changing your data once you’ve loaded it in.

A good candidate for this type of data might be data representing a model exported from a 3D modeling program like Blender. A less optimal use case would be a graph of points that will be changing over time - if that’s your use case you’d be better off with just drawing arrays.