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