Please send suggestions or corrections for this page to the author at sbuss@ucsd.edu.
This page describes some of the features of Modern OpenGL used in the SimpleAnimModern program. This is the second of a series of programs illustrating features of Modern OpenGL. The first program, SimpleDrawModern, already introduced many of the basic features of any OpenGL program, and these were described on the web page for SimpleDrawModern. On this page, we only discuss aspects of the software with are new to SimpleAnimModern.
The code fragments listed below are taken from the programs SimpleAnimModern.cpp and ShaderMgrSAM.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.
Triangle fans and triangle strips can be used to draw contiguous portions of surfaces with less repetition of vertices than would be needed to specify vertices of triangles individually. The code shown next defines an array of vertex information for a triangle fan, and sets up a a Vertex Array Object (VAO) for these vertices and loads the vertex data into Vertex Buffer Object (VBO). This is done by exactly the same kind of code as was used for points, lines, line strips, line loops, and triangles in SimpleDrawModern, so please see the documentation for SimpleDrawModern for more information on what this code does. (A lot of comments from the program have been removed here.)
// Specify vertices for the triangles rendered with GL_TRIANGLE_FAN
float triangleFanVerts[] = {
// Positions // Colors
0.0f, 0.0f, 0.0f, 0.8f, 0.8f, 0.8f, // Light grey
1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // Red
0.5f, 0.866f, 0.0f, 1.0f, 1.0f, 0.0f, // Yellow
-0.5f, 0.866f, 0.0f, 0.0f, 1.0f, 0.0f, // Green
-1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, // Cyan
-0.5f, -0.866f, 0.0f, 0.0f, 0.0f, 1.0f, // Blue
.5f, -0.866f, 0.0f, 1.0f, 0.0f, 1.0f, // Magenta
1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f // Duplicate vert and color
};
glBindVertexArray(myVAO[iTriangleFan]);
glBindBuffer(GL_ARRAY_BUFFER, myVBO[iTriangleFan]);
glBufferData(GL_ARRAY_BUFFER, sizeof(triangleFanVerts), triangleFanVerts, GL_STATIC_DRAW);
glVertexAttribPointer(vertPos_loc, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); // Vertex data is in the VBO
glEnableVertexAttribArray(vertPos_loc); // Enable the vertex data
glVertexAttribPointer(vertColor_loc, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(vertColor_loc);
Note that only one of vertices had to be specified twice; namely, the second vertex and the last vertex are repeated in order to have the outer edges of the triangle fan form a closed loop. By comparison, if the triangle fan were displayed as separate triangles, then the central vertex would have to be specified six times, and the other vertices would need to be specified two times each.
The triangle fan is rendered by the following code:
// Draw Triangle Fan
float matEntries[16];
theModelViewMatrixTriFan.DumpByColumns(matEntries);
glUniformMatrix4fv(modelviewMatLocation, 1, false, matEntries);
glBindVertexArray(myVAO[iTriangleFan]);
glDrawArrays(GL_TRIANGLE_FAN, 0, 8);
theModelViewMatrixTriFan
is a 4x4 matrix,
of type LinearMapR4
: The C++ class of
LinearMapR4
is defined in the files
DumpByColumns()
extracts the 16 entries
of the 4x4 matrix
into an array of floating point
numbers (float
's). The routine
glUniformMatrix4fv
loads them into the
GPU as the "uniform" value of the modelview matrix
for the shader. As we'll see later on in Section VI, one of the inputs to the
vertex shader is declared by:
uniform mat4 modelviewMatrix; // The model-view matrix
This declaration means modelviewMatrix
is a value
that is set uniformly for all vertices (and in fact, for all shaders
in the current shader program)
rather than being specified in a VBO for a single vertex
or in the VAO as a generic vertex attribute.
The call to
glUniformMatrix4fv
sets this uniform value for all
subsequent glDrawArrays
and other
kinds of glDraw...
calls. Note that the uniform
value does not belong to the VAO; in fact, the VAO is not even bound
until after the uniform value is set.
The value of modelviewMatLocation
specifies the location of the modelview matrix as a uniform
input to the shader program. The value is set automatically
when the shader program is compiled. Unfortunately, the location
of the modelview matrix cannot be set by the user; instead it is
set by the shader compiler. The C++ code determines
the locations of the modelview matrices in the two shader
programs using
the following lines in the routine my_setup_SceneData()
:
modelviewMatLocation_smooth = glGetUniformLocation(shaderProgramSmooth, modelviewMatName);
modelviewMatLocation_flat = glGetUniformLocation(shaderProgramFlat, modelviewMatName);
modelviewMatLocation
is set to the appropriate one of
these in the myRenderScene
when selecting which shader program to use.
This is done with:
int modelviewMatLocation;
if (FlatSmoothMode == 0) {
glUseProgram(shaderProgramSmooth);
modelviewMatLocation = modelviewMatLocation_smooth;
}
else {
glUseProgram(shaderProgramFlat);
modelviewMatLocation = modelviewMatLocation_flat;
}
The call in the code above to glBindVertexArray
makes the VAO
myVAO[iTriangleFan]
the active VAO. The subsequent
call to glDrawArrays
draws a triangle fan using
eight vertices from the data stored in the VBO, starting with
the first one, which has index zero (0
).
The code for rendering a triangle strip and for rendering a sequence of three triangles is almost identical: you can see this by examining the source code. The matrix transformations for the three overlapping triangles are more complicated, since those triangles are animated with a slow rotation. The animation is discussed below in Section IV.
The projection matrix is another matrix
which is a uniform input to the vertex shader. It is set by the C++ code by the
same kinds of commands as are used for the modelview matrix, using
glGetUniformLocation
to find the shader program
location of the projection matrix, and using
glUniformMatrix4fv
to set its value.
The function window_size_callback
is called
every time the window is resized, just as in the SimpleDrawModern
program. Resizing the window usually causes its aspect ratio to change,
and this means the projection matrix needs to be changed to
avoid distorting the image.
window_size_callback
uses
the following code to define a orthographic
projection matrix:
// Setup the projection matrix as an orthographic view.
// Determine the min and max values for x and y that should appear in the window.
// The complication is that the aspect ratio of the window may not match the
// aspect ratio of the scene we want to view.
double w = (width == 0) ? 1.0 : (double)width;
double h = (height == 0) ? 1.0 : (double)height;
// Scale (increase) either width or height up to match window aspect ratio
double scale = (halfHeight * w ) / (halfWidth * h);
if (scale > 1.0) {
halfWidth *= scale;
}
else {
halfHeight /= scale;
}
double windowXmin = centerX - halfWidth;
double windowXmax = centerX + halfWidth;
double windowYmin = centerX - halfHeight;
double windowYmax = centerY + halfHeight;
// Using the max & min values for x & y & z that should be visible in the window,
// we set up the orthographic projection.
theProjectionMatrix.Set_glOrtho(windowXmin, windowXmax, windowYmin, windowYmax, Zmin, Zmax);
The routine window_size_callback
is receives
two parameters width
and height
giving the dimensions of the graphics window in pixels.
It is desired to view objects which have x,y,z coordinates
in the ranges [Xmin,Xmin], [Ymin,Ymax] and [Zmin,Zmax].
The aspect ratio of the window is compared to the aspect
ratio of the desired view, and from this the maximum and minimum
values for x and y that should be visible in the graphics
window are computed. The final line of code below
calls Set_glOrtho
: this sets
theProjectionMatrix
to the
desired orthographic projection matrix.
theProjectionMatrix
is a LinearMapR4
object containing a 4x4 matrix. The function Set_glOrtho
mostly replicates the old OpenGL function glOrtho
but
instead of multiplying by the orthographic matrix, Set_glOrtho
just sets the matrix to the orthographic matrix. See
the GlLinearMath web pages
for more on the
LinearMapR4
C++ class.
Traditionally, there are three matrices for controlling the placement of objects on the screen. The projection matrix controls the amount of perspective (if any) and the visible depth of field --- usually under the assumption that the viewer is placed at the origin, looking down the negative z-axis. The view matrix controls the placement of the overall scene, generally in front of the viewer, centered on the z-axis. The model matrix is used on a per-item basis to control the placement of individual objects within the scene. In practice, it is common to combine the view and model matrices in a single matrix, called the modelview matrix.
In SimpleAnimModern, the modelview matrices are quite simple to compute, but still allows separate objects to placed and even animated. The commands to set the modelview matrix for the triangle fan are:
// The model view matrix for triangle fan resizes and repositions it
theModelViewMatrixTriFan.Set_glTranslate(1.2, 1.2, 0.0); // Initialize to translation by (1.2, 1.2, 0.0)
theModelViewMatrixTriFan.Mult_glScale(0.5); // Shrink using scale factor 1/2 (multiplies on the right)
The first line sets the modelview matrix to the matrix which performs a translation by (1.2, 1.0, 0.0). The second line multiplies it on the right by the matrix which uniformly scales the object by a factor of 0.5. Later, the triangle fan is rendered by the commands:
theModelViewMatrixTriFan.DumpByColumns(matEntries);
glUniformMatrix4fv(modelviewMatLocation, 1, false, matEntries);
glBindVertexArray(myVAO[iTriangleFan]);
glDrawArrays(GL_TRIANGLE_FAN, 0, 8);
The code that sets the modelview matrix for the triangle strip works similarly using the following commands:
// The model view matrix for triangle strip resizes and repositions it
theModelViewMatrixTriStrip.Set_glTranslate(-1.5, 0.5, 0.0); // Initialize to translation by (-1.5, 0.5, 0.0)
theModelViewMatrixTriStrip.Mult_glScale(0.4); // Shrink by scaling factor 0.4
theModelViewMatrixTriStrip.Mult_glRotate(-0.785, 0.0, 0.0, 1.0); // Rotate 45 degrees (0.785 radians) clockwise
The final line multiplies the matrix on the right by the
matrix which performs a rotation of 0.785 radians (approximately
45 degrees) clockwise around the z-axis. The angle is measured,
using the right-hand rule,
in the counterclockwise direction around the z-axis.
Hence the first parameter to Mult_glRotate
is
negative, i.e., -0.785.
The functions Set_glTranslate
,
Mult_glScale
, and
Mult_glRotate
duplicate the functionality
of the legacy OpenGL commands
glTranslate[f,d]
,
glScale[f,d]
, and
glRotate[f,d]
. However, the first one uses the "Set"
version of the command instead of the "Mult" version. The "Set"
version sets the matrix equal to the translation matrix. The "Mult"
versions multiply on the right by the matrix. The
command Mult_glScale(0.4)
is a convenience function that is equivalent to
Mult_glScale(0.4,0.4,0.4)
. See the
separate documentation page for more
on the LinearMapR4
C++ package.
Animation of the three triangles in a simple rotation
is done with exactly the same kinds of commands.
mySetupGeometries
sets a matrix
to the identity:
theViewMatrixThreeTriangles.SetIdentity();
Each time the scene is rendered, the modelview matrix for the triangles is computed by:
currentAngle += 0.005; // Increment current angle by 0.005 radians.
if (currentAngle > 2.0*3.1415926535897932) {
currentAngle -= 2.0*3.1415926535897932;
}
LinearMapR4 mat = theViewMatrixThreeTriangles;
mat.Mult_glRotate(currentAngle, 0.0, 0.0, -1.0); // Rotate around negative z-axis (clockwise for viewer)
The call to Mult_glRotate
multiplies the matrix mat
on the right
by the indicated rotation matrix.
In this case, it is overkill to clamp the currentAngle
in the range 0 to 2π; nonetheless, it good practice to avoid
any possibility of large floating point numbers causing loss of precision.
The main program contains the following two lines to turn on multisampling antialiasing (MSAA):
glfwWindowHint(GLFW_SAMPLES, 4); // Invoke Multisample Antialiasing (MSAA)
glEnable(GL_MULTISAMPLE); // Usually is enabled by default, but call just in case.
This instructs the OpenGL window to use "multisample aliasing" with each screen pixel holding four color and depth values (in a 2x2 arrangement). Each screen pixel has its color calculated once per triangle that overlaps the screen pixel: the color and depth values are saved into those subpixels which are actually covered by the triangle. This can greatly improve many common aliasing problems with edges of triangles.
Multisample antialiasing (MSAA) is fairly expensive in terms of memory, since it requires four color and depth values per pixel: this usually causes a noticeable slowdown in graphics performance. However, it is substantially less expensive than full-blown supersampling. A cheaper form of antialiasing, fast approximate antialiasing (FXAA) can often give good results, but is not a standard feature of OpenGL.
The main vertex shader and the fragment shader are shown below. The
vertex shader now takes two kinds of inputs: First, there are two variables
declared with in
, named vertPos
and
vertColor
; these are called vertex attributes.
A vertex attribute value can be set
from per-vertex data values from the VBO (as SimpleAnimModern does
for both vertPos
and vertColor
),
or using a VertexAttrib...
command
to supply a generic attribute (SimpleAnimModern does not
use any generic attributes,
but SimpleDrawModern
used a generic attribute value for its vertColor
when rendering three points). Second, the two values
projectionMatrix
and
modelviewMatrix
are declared as
uniform values. Uniform values
are set using a glUniform...
command; in this case,
a glUniformMatrix4fv
command.
Uniform values are somewhat like generic vertex attributes
in that they do not change per vertex. In fact, any kind of shader, not just
a vertex shader, can take a uniform parameter as input. For instance,
a fragment shader is able to directly access a uniform value.
Vertex attributes on the other hand, are accessible only to
the vertex shader: if the vertex shader sends them as out
values to a fragment shader, then the fragment
shader will by default get an averaged value, as is discussed
in Section VII next. (The averaged
value is calculated by OpenGL using linear
interpolation; aka Gourand interpolation.)
The vertex shader is:
#version 330 core
layout (location = 0) in vec3 vertPos; // Position in attribute location 0
layout (location = 1) in vec3 vertColor; // Color in attribute location 1
out vec3 theColor; // Output a color to the fragment shader
uniform mat4 projectionMatrix; // The projection matrix
uniform mat4 modelviewMatrix; // The modelview matrix
void main()
{
gl_Position = projectionMatrix * modelviewMatrix * vec4(vertPos.x, vertPos.y, vertPos.z, 1.0);
theColor = vertColor;
}
Note how the vertex shader is able to calculate the
gl_Position
using matrix operations.
The fragment shader is:
const char *fragmentShader_ColorOnly =
#version 330 core
in vec3 theColor; // Color value came from the vertex shader (smoothed)
out vec4 FragColor; // Color that will be used for the fragment
void main()
{
FragColor = vec4(theColor, 1.0f); // Add alpha value of 1.0.
}
In the two shaders shown above, the parameter theColor
was declared as an out
variable for the vertex shader,
and as an in
variable for the fragment shader.
When the shader program is compiled, it links these declarations, so
that the output of the vertex shader becomes an input to the fragment
shader. BUT, this is not quite correct: the
value of the variable theColor
of
an input to a fragment shader is the average of the
three values of vertices of the triangle containing the fragment (or the
two vertices of the line segment containing the fragment). This
averaging process is known as smooth shading
or Gouraud shading.
Hitting the space bar in the graphics window of SimpleAnimModern
will toggle between smooth shading and flat shading.
In flat shading,the averaging is not done.
Instead, every fragment of a triangle or a line segment gets the
same color. The usual rule for flat shading is that the value from
the last specified vertex is the value used for every fragment of a
triangle or line segment. To use flat shading, the declarations
of the variable vertColor
are changed in the second
pair of shaders in ShaderMgrSAM by adding the keyword flat
.
Thus, the flat version of the vertex shader contains the declaration
flat out vec3 theColor;
The corresponding declaration in the flat version of the fragment shader is
flat in vec3 theColor;
It is possible to change the convention that the flat shading is from
the last vertex of a triangle:
this is done with the
glProvokingVertex
function.
SimpleAnimModern uses the following method to clear the color and depth buffers at the beginning of each rendering.
// Clear the rendering window
static const float black[] = { 0.0f, 0.0f, 0.0f, 0.0f };
const float clearDepth = 1.0f;
glClearBufferfv(GL_COLOR, 0, black);
glClearBufferfv(GL_DEPTH, 0, &clearDepth); // Must pass in a pointer to the depth value!
These are similar in functionality to glClearBuffer
but give additional
flexibility. A depth value of 1.0 is the maximum possible value, indicating maximum distance
from the viewer.