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.
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.
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
.
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.
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.