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 ConnectDotsModern program. This is one of a series of programs illustrating features of Modern OpenGL. It builds rather closely on the features found in the earlier programs SimpleDrawModern and SimpleDrawShaderMgr; the main new feature in ConnectDotsModern is that it accepts left mouse clicks, and uses these to place points and connect them with straight-line segments. It also accepts right mouse clicks to select and move vertices. Only the features new to ConnectDotsModern are discussed here. For all other features, see the web page for SimpleDrawModern

We do not cover all features of mouse input in OpenGL here. Nor do we describe any of the more sophisticated methods of working with OpenGL buffers. It strongly recommended to search online for documentation of the OpenGL/GLFW commands and keywords to learn more about their functionality.

I. Capturing left mouse clicks

The initialization code for ConnectDotsModern includes a call to setup the mouse button callback function:

    glfwSetMouseButtonCallback(window, mouse_button_callback);

This tells OpenGL/GLFW to call the function mouse_button_callback for every mouse button press or release. (It also setups a callback for every mouse-moved event: this is described in Section IV below.)

The call to glfwSetMouseButtonCallback causes the following routine in ConnectDotsModern to called every time a mouse button is pressed or released:

// *******************************************************
// Process all mouse button events.
// This routine is called each time a mouse button is pressed or released.
// *******************************************************
void mouse_button_callback(GLFWwindow* window, int button, int action, int mods)
{
    double xpos, ypos;
    glfwGetCursorPos(window, &xpos, &ypos);
    // Scale x and y values into the range [-1,1]. 
    // Note that y values are negated since mouse measures y position from top of the window
    float dotX = (2.0f*(float)xpos / (float)(windowWidth-1)) - 1.0f;
    float dotY = 1.0f - (2.0f*(float)ypos / (float)(windowHeight-1));

    if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS) {
        AddPoint(dotX, dotY);
    }
    else if (button == GLFW_MOUSE_BUTTON_RIGHT) {
        if (action == GLFW_PRESS) {
            // Find closest extant point, if any. (distances minDist and thisDist are measured in pixels)
            float minDist = 10000.0f;
            int minI;
            for (int i = 0; i < NumDots; i++) {
                float thisDistX = 0.5f*(dotX - dotArray[i][0])*(float)windowWidth;
                float thisDistY = 0.5f*(dotY - dotArray[i][1])*(float)windowHeight;
                float thisDist = sqrtf(thisDistX * thisDistX + thisDistY * thisDistY);
                if (thisDist < minDist) {
                    minDist = thisDist;
                    minI = i;
                }
            }
            if (minDist <= 4.0) {      // If clicked within 4 pixels of the vertex
                selectedVert = minI;
                ChangePoint(selectedVert, dotX, dotY);
            }
        }
        else if (action == GLFW_RELEASE) {
            selectedVert = -1;
        }
    }
}

The parameters to mouse_button_callback are: the variable window identifying the window where the mouse click occurred, the mouse button identifier (button), the mouse button action (action), and modifier flags (mods). The code starts by checking whether the mouse event was the left mouse button being pressed. If so, it records the mouse position by calling AddPoint.

The mouse position is unfortunately not passed as a parameter to mouse_button_callback. Instead, the routine glfwGetCursorPos is called to poll the position of the mouse (which is referred to as the cursor position). This returns the x,y position of the mouse as a pair of floating point numbers (float's). Although they are floats, they are in fact equal to integers giving the position of the mouse cursor in pixels. The x coordinate xpos is a value between 0 and windowWidth-1, with the value 0 for the lefthand side of the window. The y coordinate ypos is a value between 0 and windowHeight-1, with the value 0 for the top border of the window.

The variables dotX and dotY convert these to the range [-1,1]. The dotX value ranges from -1 on the lefthand border to 1 on the righthand border. The dotY value ranges from -1 on the bottom border to 1 on the top border: note this reverses the up/down convention of the y value (ypos) returned by glfwGetCursorPos.

When the left mouse button is pressed down, the routine AddPoint is called. This happens only once when the button is first pressed; in other words, it is not called repeatedly if the mouse button is held down.

When the the right mouse button is pressed, the program loops over all the vertices to find the one closest to the mouse click. If the distance to closest vertex is at most four pixels, it selects this pixel. When the right mouse button is released, the vertex is de-selected.

Caveat: The routine glfwGetCursorPos polls the current location of the mouse cursor: this may be different from the position of the mouse at the time of the mouse press.

The mods parameter is not used in the code above: it contains information about whether the shift or control buttons are pressed.

II. Updating a VBO buffer with glBufferSubData.

The initialization code which sets up the VAO and VBO for drawing the points and line segments is as follows:

    // The glBufferData command allocates space in the VBO for the vertex data, but does not
    //    load any data into the VBO.  For this, the third parameter is the null pointer, (void*)0.
    // The VBO will hold only the vertex positions.  The color will be a generic attribute.
    glBindVertexArray(myVAO);
    glBindBuffer(GL_ARRAY_BUFFER, myVBO);
    glBufferData(GL_ARRAY_BUFFER, MaxNumDots*2*sizeof(float), (void*)0, GL_STATIC_DRAW);
    glVertexAttribPointer(vertPos_loc, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0);
    glEnableVertexAttribArray(vertPos_loc);                

This differs from what was done in SimpleDrawModern in that it allocates buffer space in the VBO but does not upload any data. The second parameter MaxNumDots*2*sizeof(float) to glBufferData specifies how many bytes of buffer memory to allocate. The third parameter (void*)0 is a null pointer: this specifies that there is no data to upload at present.

The routine LoadPointsIntoVBO is called every time the list of points is changed --- either through a mouse click or the deletion of the first or last point. It calls the following two lines

    glBindBuffer(GL_ARRAY_BUFFER, myVBO);
    glBufferSubData(GL_ARRAY_BUFFER, 0, NumDots * 2 * sizeof(float), dotArray);

to load the array of vertices into the VBO buffer. Note that it calls glBufferSubData instead of glBufferData: this tells OpenGL that only a portion of the buffer is being updated, and that the total space allocation for the buffer should not change. (This is unlike a call to glBufferData, which changes the memory allocation for the buffer.) The second parameter, 0, specifies the starting position (in bytes) in the buffer where the data is to be copied. The third parameter, NumDots*2*sizeof(float) gives the number of bytes to be copied. Of course, the data should fit in the space allocated for the buffer!

The program always uploads the entire dotArray contents into the VBO buffer. This is not strictly necessary, as glBufferSubData can change any continguous range data in the VBO (by appropriately setting the values of the second and third parameters of glBufferSubData). Thus, it would be possible to upload only the changed portion of the data to the VBO.

For more sophisticated ways to work with buffers, see the OpenGL functions glMapBuffer, glMapBufferRange, and glUnmapBuffer.

III. Depth testing with GL_LEQUAL.

ConnectDotsModern uses the default orthographic perperspective viewing transformation, and renders the points (black dots) and the straight-line segments lines with the same depth value, namely with z component defaulting to equal to zero. In fact, the VBO holds only x,y values, but the vertex attribute vertPos in the vertex shader is a vec3. The missing z value is set to zero by default.

The routine myRenderScene() renders the line segments first, and renders the points second. The result is that the points overdraw the lines. To make this work, the depth testing is set to use the GL_LEQUAL with the commands:

    glEnable(GL_DEPTH_TEST);    // Enable depth buffering
    glDepthFunc(GL_LEQUAL);        

The option GL_LEQUAL specifies that pixels are overwritten whenever the incoming depth value is less than or equal to the previous depth value. This works reliably in ConnectDotsModern since: firstly, an orthographic perspective view is used, and, secondly, the z values are the constant zero. As a result, the depth values are constant and do not vary across pixels.

In more general situations where coincident surfaces are rendered with interpolation of non-constant depth values, there is likely to be aliasing problems from z-fighting. In those situations, glPolygonOffset can be used to avoid z-fighting. This is not needed in ConnectDotsModern however.

IV. Tracking the mouse position

The main program calls

    // Set callbacks for mouse movement (cursor position).
    glfwSetCursorPosCallback(window, cursor_pos_callback);

to setup the callback routine for mouse movement.

This causes the routine cursor_pos_callback to be called every time the mouse moves:

    void cursor_pos_callback(GLFWwindow* window, double x, double y) {
        if (selectedVert == -1) {
            return;
        }
        float dotX = (2.0f*(float)x / (float)(windowWidth - 1)) - 1.0f;
        float dotY = 1.0f - (2.0f*(float)y / (float)(windowHeight - 1));
        ChangePoint(selectedVert, dotX, dotY);
    }

cursor_pos_callback receives the x and y pixel position of the mouse; thus it does not need to query the mouse position. It calls ChangePoint to update the position of the selected vertex, if any.