Please send suggestions or corrections for this page to the author at sbuss@ucsd.edu.

This page describes the features of Modern OpenGL used in the SimpleDrawModern program. This is the first of a series of programs illustrating features of Modern OpenGL. Since it is the first one, there are a lot of features being introduced at once. We discuss these one at time, roughly in order of their importance for students using language features for the first time in a course about Mathematical Computer Graphics (specifically, Math 155A at UCSD during Winter quarters 2018 and 2019).

The code fragments listed below are taken from the programs SimpleDrawModern.cpp and ShaderMgrSDM.cpp. It is recommended to refer to the source code as you read this web page to see how the code fits together. In some cases, the code examples given below are slightly modified to make this web page more readable. We cannot cover all features of commands here, and it strongly recommended to search on-line for documentation of the OpenGL commands and keywords to learn more about their functionality.

I. Specifying the positions and colors of vertices

Vertex attributes, for instance the positions and color of vertices, are stored in arrays called Vertex Buffer Objects (VBO, for short). Information about the contents of the VBO's is stored in Vertex Array Object's (VAO, for short).

Allocating VAO's and VBO's. The six lines from SimpleDrawModern.cpp shown below allocate two arrays: one to hold the names of three Vertex Array Objects (VAO's) and the other to hold the names of three Vertex Buffer Objects (VBO's). A "name" is just an unsigned integer.

    const int NumObjects = 3;
    const int iPoints = 0;
    const int iLines = 1;
    const int iTriangles = 2;

    unsigned int myVBO[NumObjects];  // Vertex Buffer Object - holds an array of vertex data
    unsigned int myVAO[NumObjects];  // Vertex Array Object - holds info about how the vertex data is formatted

The next two lines (later in the code) call OpenGL to have it generate the names of three new VAOs and three new VBO's. The names of these VAO's and VBO's are stored in the two arrays myVAO and mvVBO:

    glGenVertexArrays(NumObjects, &myVAO[0]);
    glGenBuffers(NumObjects, &myVBO[0]);

Each VBO will hold an array of "vertex attribute" values. Vertex attributes are values associated with vertices: in the SimpleAnimModern program, vertex attributes are the positions (x,y,z or just x,y values) and the colors (red, green, blue values) of the vertices.

Each VAO will hold information about where (and if) vertex attributes are stored in a VBO and how they are formatted.

Loading vertex position for three vertices into a VAO and VBO. The first VAO and VBO pair will hold the information about the positions of three points (which will later be rendered with the GL_POINTS drawing mode). This information is loaded into the VAO and VBO by the following code:

    float threeVerts[] = {
        -0.6f, -0.3f,  // First point
        0.6f, -0.3f,   // Second point
        0.6f, 0.3f     // Third point
    };

    glBindVertexArray(myVAO[iPoints]);    // Bind (and initialize) the Vertex Array Object

    // Allocate space in the vertex buffer (the VBO)
    //   and load the three pairs of x,y values into the VBO.  
    //   This is potentially stored in the GPU for quick access.
    glBindBuffer(GL_ARRAY_BUFFER, myVBO[iPoints]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(threeVerts), threeVerts, GL_STATIC_DRAW);

    // Bind the Vertex Array Object and then configure vertex attributes(s).
    //   The vertex attributes as used later by the vertex shader program
    //     consist of three coordinates (x, y, z values)
    //   The vertex shader accesses them through "location = 0",
    //     namely, vertPos_loc is the value 0.
    //   The x,y values come from the VBO array. The z values default to 0.0.
    //   These facts are stored in the VAO.
    //  The color values are not set here: this is done in myRenderScene().
    glVertexAttribPointer(vertPos_loc, 2, GL_FLOAT, GL_FALSE, 0, (void*)0); // Info about where positions are in the VBO
    glEnableVertexAttribArray(vertPos_loc);    // Enable the stored vertex positions

The declaration of the threeVerts array stores the six floats into the C++ program's local (stack) memory. The call to glBindVertexArray makes the named VAO the currently active VAO. The call to glBindBuffer makes the named VBO the currently active VBO. The call to glBufferData causes the entire threeVerts array to be loaded (copied) into the currently active VBO. The second parameter to glBufferData tells how many bytes to copy into the VBO. The third parameter is a pointer to the beginning of the array to be copied. Once the array is copied into the VBO, OpenGL is responsible for keeping a copy of the data. Because OpenGL keeps the data in the VBO, it is OK if the array threeVerts is subsequently overwritten or deleted by the C++ program.

The call to glVertexAttribArray tells the VAO that the x,y position values of vertices are stored in the currently active VBO and also tells where the x,y position values are stored. vertPos_loc is defined to be the constant zero. Later when we examine the Vertex Shader below, we will see that it expects to have an input value stored in location 0, named vertPos. The values as stored in the VBO specify the values of the vertPos inputs to the vertex shader. The second and third parameters to glVertexAttribArray specify that there are two values (in this case, x and y values) per vertex and they are float's. The fourth parameter is ignored when GL_FLOAT is used. (It controls how values are clamped when converting integers to floats.) The fourth parameter is the stride: it gives the distance in bytes to step from one vertex's data in the next vertex's data. In this case, we could have thus equivalently used 2*sizeof(float); but instead, OpenGL also lets us specify 0 when the data is tightly packed together. The final parameter (void*)0 specifies the relative position, in bytes, where the data begins in the VBO. In this case, the first x,y is at the beginning of the VBO.

Note that glVertexAttribArray did not have to be told how many vertices have data stored in the VBO. This will be specified when the points are rendered with glDrawArrays.

Loading vertex position and color information for three triangles into a VAO and VBO. Data for the vertices of the triangles is loaded by almost exactly the same code as described above for the points. For the three triangles, however, the code is modified to specify both a position and a color for each vertex:

    float trianglesVerts[] = {
        // x,y,z coordinates    // R,G,B colors
        0.7f, -0.42f, 0.0f,     1.0f, 0.8f, 0.8f, // First triangle
        0.7f, -0.18f, 0.0f,     1.0f, 0.8f, 0.8f,
        -0.7f, -0.3f, 0.5f,     1.0f, 0.0f, 0.0f,
    
        -0.25f, 0.7f, 0.0f,     0.8f, 1.0f, 0.8f, // Second triangle
        -0.40f, 0.55f, 0.0f,    0.8f, 1.0f, 0.8f,
        0.5f, -0.6f, 0.5f,      0.0f, 1.0f, 0.0f,
    
        -0.57f, -0.53f, 0.0f,   0.8f,  0.8f, 1.0f,  // Third triangle
        -0.43f, -0.67f, 0.0f,   0.8f,  0.8f, 1.0f,
        0.32f, 0.62f, 0.5f,     0.0f,  0.0f, 1.0f,
    };

    // This time, both vertex positions AND vertex colors are loaded into the VBO.
    // Note that glVertexAttribPointer uses a non-zero "stride" parameter.
    // Also note that the second call to glVertexAttribPointer uses 
    //        3*sizeof(float) to specify where the color information is.
    glBindVertexArray(myVAO[iTriangles]);
    glBindBuffer(GL_ARRAY_BUFFER, myVBO[iTriangles]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(trianglesVerts), trianglesVerts, GL_STATIC_DRAW);
    glVertexAttribPointer(vertPos_loc, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0);
    glEnableVertexAttribArray(vertPos_loc);
    glVertexAttribPointer(vertColor_loc, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));
    glEnableVertexAttribArray(vertColor_loc);

The value vertColor_loc is defined to be constant 1. It corresponds to an input at location 1, called vertColor, in the vertex shader. There are only a few differences between the new code and the code for the three vertices. First, there are three position values (x,y,z) for each vertex, and three color values (r,g,b); thus the second input to the two calls to glVertexAttribPointer is equal to 3. Second, the data for positions and colors are interspersed so the stride's are now equal to 6*sizeof(float) instead of 2*sizeof(float). This indicates that are that many bytes separating the beginning of the data for a vertex and the beginning of the data for the next vertex. Third, the first color value begins at byte count 3*sizeof(float) in the VBO: this is given by the final input to the second call to glVertexAttribPointer.

II. Rendering geometry from the vertex information in a VAO and VBO.

The code described above to load the VAO and VBO data is contained in mySetupScene. For the SimpleDrawModern program, mySetupScene needs to be called once. Then the function myRenderScene is called repeatedly to render the scene.

Clearing the screen. The screen buffer's pixels' color and depth information are cleared by the following command. The default clear color is black. (See the program SimpleAnimModern for a more flexible way to clear the color and depth buffers.)

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

The current shader program is selected by the next command. Typically the shader program contains a vertex shader and a fragment shader (at least). The vertex shader is a small program run once for every vertex. The fragment shader is a small program which is run every time a pixel is processed.

    glUseProgram(shaderProgram1);

The three points can then rendered with the following three lines of code. The VAO and VBO do not hold any color information for these three points. Instead the function glVertexAttrib3f is now called to specify the generic color that is to be used if the VAO/VBO do not contain color information for the vertex. This generic color is a global value; that is to say, it is not stored with the VAO. Therefore, one generally needs to call glVertexAttrib3f every time the vertices are rendered.

    // Draw three points
    glBindVertexArray(myVAO[iPoints]);
    glVertexAttrib3f(vertColor_loc, 1.0f, 0.5f, 0.2f);     // An orange-red color (R, G, B values).
    glDrawArrays(GL_POINTS, 0, 3);

The glDrawArrays specifies to draw in GL_POINTS mode, to start at the first vertex (vertex number zero) in the VBO, and to render a total of three points. There are other options in the source code to render the vertices with GL_LINES, GL_LINE_STRIP or GL_LINE_LOOP instead of GL_POINTS.

Note that we needed to bind only the VAO before calling glDrawArrays(). There is no need to bind the VBO since the VAO knows which VBO holds the vertex positions.

The three triangles can be rendered with similar code. The colors for the vertices are stored in the VAO/VBO, so it is not necessary to call glVertexAttrib3f to set the vertex colors. This call to glDrawArrays starts with vertex number 0 and uses 9 vertices, for a total of three triangles.

    // Draw three overlapping triangles
    // Colors for the triangles have already been loaded into the VBO.
    glBindVertexArray(myVAO[iTriangles]);
    glDrawArrays(GL_TRIANGLES, 0, 9);

III. Initializing OpenGL. GLFW and GLEW, and opening the graphics and console windows.

The main program starts off with the following commands (some tests and print statements have been omitted).:

    glfwInit();
#if defined(__APPLE__) || defined(__linux__)
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
    const int initWidth = 800;
    const int initHeight = 600;
    GLFWwindow* window = glfwCreateWindow(initWidth, initHeight, "SimpleDrawModern", NULL, NULL);
    glfwMakeContextCurrent(window);
    glewInit();

    setup_callbacks(window);
    window_size_callback(window, initWidth, initHeight);

These commands initialize a graphics window of dimensions 800x600 pixels, with title "SimpleDrawModern". OpenGL commands render into the graphics window. It also opens a console window, which takes input and output via stdin, stdout and stderr.

The four lines in the #if...#endif select a version of OpenGL. Due to the great importance placed on performance and rendering speed, OpenGL has been aggressively modified over the years. This has included deprecating many old features and adding many new capabilities, so it can make a big difference which version you use.

IV. The main program loop

    my_setup_OpenGL();
    my_setup_SceneData();
 
    // Loop while program is not terminated.
    while (!glfwWindowShouldClose(window)) {
    
        myRenderScene();                // Render into the current buffer
        glfwSwapBuffers(window);        // Displays what was just rendered (using double buffering).

        // Wait, while polling events (key presses, mouse events)
        glfwWaitEvents();                   // Use this if no animation.
        // glfwWaitEventsTimeout(1.0/60.0); // Use this to animate at 60 frames/sec (timing is NOT reliable)
        // glfwPollEvents();                // Use this version when animating as fast as possible
    }
    glfwTerminate();

The main control code is shown above. First, it calls my_setup_OpenGL() which will be discussed below. Then it calls my_setup_SceneData: this loads the VAO's and VBO's with the geometric information about the scene, as is discussed in Section I above. my_setup_SceneData only needs to be called once. The program then loops, calling myRenderScene and glfwSwapBuffers. myRenderScene contains the code shown in Section II above, including the glDrawArrays() that render the objects in the scene. glfwSwapBuffers interchanges the two frame buffers for double buffering. glfwWaitEvents() returns when some event has occurred that might require re-rendering the scene. This includes key presses and mouse movements into the graphics window. It will also return if the graphics window is resized, so that the image can be drawn in the resized window (see Section VI below).

V. Initializing OpenGL options

The my_setup_OpenGL routine sets various OpenGL options that control the way rendering occurs. These include enabling depth testing for hidden surface removal; specifying whether to draw triangles as filled in, as lines, or as points; specifying whether to cull back faces; setting parameters for how to smooth lines so as to render with aliasing; and specifying point size and line width. The last two lines, specifying point size and line width can make a big difference: unless these values are bigger than 1, points and lines are drawn only a single pixel in width, and this is almost invisible on high resolution screens.

    glEnable(GL_DEPTH_TEST);    // Enable depth buffering
    glDepthFunc(GL_LEQUAL);     // Useful for multipass shaders

    // Set polygon drawing mode for front and back of each triangle
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    // glPolygonMode(GL_FRONT_AND_BACK, GL_POINT);

    // Disable backface culling to render both sides of polygons
    // glDisable(GL_CULL_FACE);

    // The following commands should induce OpenGL to create round points and 
    //  antialias points and lines.  (This is implementation dependent unfortunately.)
    glEnable(GL_POINT_SMOOTH);
    glEnable(GL_LINE_SMOOTH);
    glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);    // Make round points, not square points
    glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);     // Antialias the lines
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // Specify the diameter of points, and the width of lines. (Implementation dependent.)
    //   Permissible values are 1 and greater.
    glPointSize(8);
    glLineWidth(5);

VI. Resizing the window

The routine setup_callbacks contains the lines:

    // Set callback function for resizing the window
    glfwSetFramebufferSizeCallback(window, window_size_callback);

This tells OpenGL to call the function window_size_callback whenever the window is resized.

    void window_size_callback(GLFWwindow* window, int width, int height) {
        glViewport(0, 0, width, height);        // Draw into entire window
    }

window_size_callback is responsible for setting the viewport, that is the portion of the graphics window where OpenGL does its rendering, and setting the projection matrix. However, in SimpleDrawModern, there is no use of a projection matrix: By default, points with x, y, z coordinates in the range [-1, 1] are rendered orthographically into the viewport. This means that all vertex positions should have x, y coordinates in this range, and that z coordinates are ignored. It also means that the scene stretches vertically or horizontally with the aspect ratio of the graphics window when the graphics window is resized.

VII. Processing keyboard input

Keyboard input and mouse inputs are handled in an interrupt-driven fashion: instead of your program constantly checking whether a key is pressed, a callback function is established and this is called whenever a key is pressed or released. This lets you process key up and key down events and even key-repeat events. The callback function is specified in setup_callbacks by the command

    // Set callback for key up/down/repeat events
    glfwSetKeyCallback(window, key_callback);

The routine key_callback is shown below in its entirety. A few things to notice. The key specifies one of the Ascii keys, or special non-Ascii keys such as "Escape" or "Left arrow" or "Home", etc. The action tells whether this is a key press (GLFW_PRESS), a key up (GLFW_RELEASE), or a key repeat (GLFW_REPEAT) event. The parameter mods lets you test for things like whether the shift key or control key is pressed. However, the key callback routine is not able to check whether the Shift-Lock or Num-Lock buttons are active; for this, see the page describing the program SolarModern and its use of a "character callback" function. The parameter scancode is for special, platform-specific applications.

    void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) {
        if (action == GLFW_RELEASE) {
            return;         // Ignore key up (key release) events
        }
        if (key == GLFW_KEY_ESCAPE || key == GLFW_KEY_X) {
            glfwSetWindowShouldClose(window, true);
        }
        else if (key == GLFW_KEY_SPACE) {
            CurrentMode = (CurrentMode+1) % 5;  // Takes on values from 0 to 4
        }
    }

It is important to note that the key_callback routine does not do any rendering. When the rendered image is supposed to change, this is recorded in the global variable CurrentMode. After key_callback returns, the main loop's idle function glfwWaitEvents() will return, allowing myRenderScene to re-render the scene.

The glfwSetKeyCallback callback routine is intended to handle key up/down events, not general character input. In particular, the mods parameter can be used to check with the SHIFT key is pressed, but key_callback has no way to check whether SHIFT-LOCK is on. (A different kind of callback, given with glfwSetCharCallback, can be used to handle text input.)

VIII. Checking for OpenGL errors

At several places during code the following command is invoked, to check if any OpenGL errors have occurred.

    check_for_opengl_errors();

In many cases, these errors will be programming errors, with an OpenGL function called with incorrect inputs. OpenGL stores a queue of errors as they occur, and they are retrieved one at a time with glGetError(). The entire routine check_for_opengl_errors() is:

    char errNames[8][36] = {
        "Unknown OpenGL error",
        "GL_INVALID_ENUM", "GL_INVALID_VALUE", "GL_INVALID_OPERATION",
        "GL_INVALID_FRAMEBUFFER_OPERATION", "GL_OUT_OF_MEMORY",
        "GL_STACK_UNDERFLOW", "GL_STACK_OVERFLOW", "GL_CONTEXT_LOST" };
    bool check_for_opengl_errors() {
        int numErrors = 0;
        GLenum err;
        while ((err = glGetError()) != GL_NO_ERROR) {
            numErrors++;
            int errNum = 0;
            switch (err) {
            case GL_INVALID_ENUM:
                errNum = 1;
                break;
            case GL_INVALID_VALUE:
                errNum = 2;
                break;
            case GL_INVALID_OPERATION:
                errNum = 3;
                break;
            case GL_INVALID_FRAMEBUFFER_OPERATION:
                errNum = 4;
                break;
            case GL_OUT_OF_MEMORY:
                errNum = 5;
                break;
            case GL_STACK_UNDERFLOW:
                errNum = 6;
                break;
            case GL_STACK_OVERFLOW:
                errNum = 7;
                break;
            case GL_CONTEXT_LOST:
                errNum = 8;
                break;
            }
            printf("OpenGL ERROR: %s.\n", errNames[errNum]);
        }
        return (numErrors != 0);
    }

Unfortunately, the OpenGL error codes are not particularly informative. When you get an error, you can add additional calls to check_for_opengl_errors to your program to figure out which OpenGL function call is generating the error. When debugging, it is useful to put a breakpoint on the printf command to catch the first OpenGL error; it is often necessary to insert extra calls to check_for_opengl_errors to figure out exactly which OpenGL command has encountered the error.

IX. Vertex and fragment shaders

A Vertex Shader is a small program which is run for every vertex. A Fragment Shader is a small program which is run for every rendered pixel. A given screen pixel may be rendered multiple times because of overlapping triangles, even though usually only one of them will eventually effect what is displayed on the screen: the fragment shader thus can be run more than once per screen pixel. The vertex shader sets the position (x, y, z, w coordinates) and the color (r, g, b, a) of the vertex. In the simple vertex shader shown below, these values are merely passed through. The fragment shader sets the color of the pixel; the depth is set automatically by OpenGL.

    const char *vertexShader_PosColorOnly =
    "#version 330 core\n"
    "layout (location = 0) in vec3 vertPos;    // Position in attribute location 0\n"
    "layout (location = 1) in vec3 vertColor;  // Color in attribute location 1\n"
    "out vec3 theColor;                 // output a color to the fragment shader\n"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(vertPos.x, vertPos.y, vertPos.z, 1.0);\n"
    "   theColor = vertColor;\n"
    "}\0";

    // Set a general color using a fragment shader. (A "fragment" is a "pixel".)
    //    The color value is passed in, obtained from the color(s) on the vertice(s).
    //    Color values range from 0.0 to 1.0.
    //    First three values are Red/Green/Blue (RGB).
    //    Fourth color value (alpha) is 1.0, meaning there is no transparency.
    const char *fragmentShader_ColorOnly =
    "#version 330 core\n"
    "in vec3 theColor;      // Color value came from the vertex shader (smoothed) \n"
    "out vec4 FragColor;    // Color that will be used for the fragment\n"
    "void main()\n"
    "{\n"
    "   FragColor = vec4(theColor, 1.0f);   // Add alpha value of 1.0.\n"
    "}\n\0";

The vertex shader outputs two values. The first is a special built-in variable gl_Position; this is the xyzw homogeneous coordinates of the vertex in 3-space. The second, theColor is specific to this vertex shader, and is declared with the modifier out. The vertex shader has two inputs, vertPos and vertColor. These values are obtained from the VBO as loaded with glBufferData and described by a call to glVertexAttribPointer(), or from the generic value set by glVertexAttrib3f(). The declarations of vertPos and vertColor specify that they are stored at location=0 and location=1: these location numbers correspond to the first parameters vertPos_loc and vertColor_loc use for the calls to glVertexAttribPointer() or glVertexAttrib3f() (see Sections I and II above).

The fragment shader has one input, vertPos. You may think of this as coming from the vertex shader, but in actuality is computed via linear interpolation (aka Gouraud shading) as an average of the three vertices of the triangle containing the fragment (or of two vertices when rendering a line). The fragment shader has a single output FragColor which will set the color of the screen pixel.

X. Compiling vertex and fragment shaders into a shader program

As declared above, the vertex shader and fragment shader programs are described by long strings. In fact, the strings enclosed in quote (") marks are just concatenated, so vertexShader_PosColorOnly (for example) is a pointer to a single long string, not an array of strings. The vertex shader and fragment shader are compiled separately and then linked (combined) to be a Shader Program. The shader program is selected by the glUseProgram in myRenderScene before the geometries are rendered.

The following code compiles the two shaders and links the shader program. Note that the code calls functions to check for compilation and linkage errors: for that code see the source code in ShaderMgrSDM.cpp.

    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    check_compilation_shader(vertexShader);

    // fragment shader
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
    check_compilation_shader(fragmentShader);

    // link shaders
    unsigned int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    check_link_status(shaderProgram);
    
    // Deallocate shaders since we do not need to use these for other shader programs.
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

Once the compilation and linkage are done, the vertex shader and fragment shader may be discarded with glDeleteShader() since we do not need to reuse them for another shader program.