In the simple triangle demo application code, we store the GLSLShader
object reference in the global scope so that we can access it in any function we desire. We modify the OnInit()
function by adding the following lines:
The first two lines create the GLSL shader of the given type by reading the contents of the file with the given filename. In all of the recipes in this book, the vertex shader files are stored with a .vert
extension, the geometry shader files with a .geom
extension, and the fragment shader files with a .frag
extension. Next, the GLSLShader::CreateAndLinkProgram
function is called to create the shader program from the shader object. Next, the program is bound and then the locations of attributes and uniforms are stored.
We pass two attributes per-vertex, that is vertex position and vertex color. In order to facilitate the data transfer to the GPU, we create a simple Vertex
struct as follows:
Next, we create an array of three vertices in the global scope. In addition, we store the triangle's vertex indices in the indices global array. Later we initialize these two arrays in the OnInit()
function. The first vertex is assigned the red color, the second vertex is assigned the green color, and the third vertex is assigned the blue color.
Next, the vertex positions are given. The first vertex is assigned an object space position of (-1,-1, 0), the second vertex is assigned (0,1,0), and the third vertex is assigned (1,-1,0). For this simple demo, we use an orthographic projection for a view volume of (-1,1,-1,1). Finally, the three indices are given in a linear order.
In OpenGL v3.3 and above, we typically store the geometry information in buffer objects, which is a linear array of memory managed by the GPU. In order to facilitate the handling of buffer object(s) during rendering, we use a
vertex array object (VAO). This object stores references to buffer objects that are bound after the VAO is bound. The advantage we get from using a VAO is that after the VAO is bound, we do not have to bind the buffer object(s).
In this demo, we declare three variables in global scope; vaoID
for VAO handling, and vboVerticesID
and vboIndicesID
for buffer object handling. The VAO object is created by calling the glGenVertexArrays
function. The buffer objects are generated using the glGenBuffers
function. The first parameter for both of these functions is the total number of objects required, and the second parameter is the reference to where the object handle is stored. These functions are called in the OnInit()
function.
After the VAO object is generated, we bind it to the current OpenGL context so that all successive calls affect the attached VAO object. After the VAO binding, we bind the buffer object storing vertices (vboVerticesID
) using the glBindBuffer
function to the GL_ARRAY_BUFFER
binding. Next, we pass the data to the buffer object by using the glBufferData
function. This function also needs the binding point, which is again GL_ARRAY_BUFFER
. The second parameter is the size of the vertex array we will push to the GPU memory. The third parameter is the pointer to the start of the CPU memory. We pass the address of the vertices global array. The last parameter is the usage hint which tells the GPU that we are not going to modify the data often.
The usage hints have two parts; the first part tells how frequently the data in the buffer object is modified. These can be STATIC
(modified once only), DYNAMIC
(modified occasionally), or STREAM
(modified at every use). The second part is the way this data will be used. The possible values are DRAW
(the data will be written but not read), READ
(the data will be read only), and COPY
(the data will be neither read nor written). Based on the two hints a qualifier is generated. For example, GL_STATIC_DRAW
if the data will never be modified and GL_DYNAMIC_DRAW
if the data will be modified occasionally. These hints allow the GPU and the driver to optimize the read/write access to this memory.
In the next few calls, we enable the vertex attributes. This function needs the location of the attribute, which we obtain by the GLSLShader::operator[]
, passing it the name of the attribute whose location we require. We then call glVertexAttributePointer
to tell the GPU how many elements there are and what is their type, whether the attribute is normalized, the stride (which means the total number of bytes to skip to reach the next element; for our case since the attributes are stored in a Vertex
struct, the next element's stride is the size of our Vertex
struct), and finally, the pointer to the attribute in the given array. The last parameter requires explanation in case we have interleaved attributes (as we have). The offsetof
operator returns the offset in bytes, to the attribute in the given struct. Hence, the GPU knows how many bytes it needs to skip in order to access the next attribute of the given type. For the vVertex
attribute, the last parameter is 0
since the next element is accessed immediately after the stride. For the second attribute vColor
, it needs to hop 12 bytes before the next vColor
attribute is obtained from the given vertices array.
The indices are pushed similarly using glBindBuffer
and glBufferData
but to a different binding point, that is, GL_ELEMENT_ARRAY_BUFFER
. Apart from this change, the rest of the parameters are exactly the same as for the vertices data. The only difference being the buffer object, which for this case is vboIndicesID
. In addition, the passed array to the glBufferData
function is the indices array.
To complement the object generation in the OnInit()
function, we must provide the object deletion code. This is handled in the OnShutdown()
function. We first delete the shader program by calling the GLSLShader::DeleteShaderProgram
function. Next, we delete the two buffer objects (vboVerticesID
and vboIndicesID
) and finally we delete the vertex array object (vaoID
).
Tip
We do a deletion of the shader program because our GLSLShader
object is allocated globally and the destructor of this object will be called after the main function exits. Therefore, if we do not delete the object in this function, the shader program will not be deleted and we will have a graphics memory leak.
The rendering code of the simple triangle demo is as follows:
The rendering code first clears the color and depth buffer and binds the shader program by calling the GLSLShader::Use()
function. It then passes the combined modelview and projection matrix to the GPU by invoking the glUniformMatrix4fv
function. The first parameter is the location of the uniform which we obtain from the GLSLShader::operator()
function, by passing it the name of the uniform whose location we need. The second parameter is the total number of matrices we wish to pass. The third parameter is a Boolean signifying if the matrix needs to be transposed, and the final parameter is the float pointer to the matrix object. Here we use the glm::value_ptr
function to get the float pointer from the matrix object. Note that the OpenGL matrices are concatenated right to left since it follows a right handed coordinate system in a column major layout. Hence we keep the projection matrix on the left and the modelview matrix on the right. For this simple example, the modelview matrix (MV
) is set as the identity matrix.
After this function, the glDrawElements
call is made. Since we have left our VAO object (vaoID
) bound, we pass 0
to the final parameter of this function. This tells the GPU to use the references of the GL_ELEMENT_ARRAY_BUFFER
and GL_ARRAY_BUFFER
binding points of the bound VAO. Thus we do not need to explicitly bind the vboVerticesID
and vboIndicesID
buffer objects again. After this call, we unbind the shader program by calling the GLSLShader::UnUse()
function. Finally, we call the glutSwapBuffer
function to show the back buffer on screen. After compiling and running, we get the output as shown in the following figure: