Lab Manual 3 - Shader Programming
Lab Manual 3 - Shader Programming
There is one last thing we'd like to discuss when rendering vertices and that is element buffer
objects abbreviated to EBO. To explain how element buffer objects work it's best to give an example:
suppose we want to draw a rectangle instead of a triangle. We can draw a rectangle using two triangles
(OpenGL mainly works with triangles). This will generate the following set of vertices:
float vertices[] = {
// first triangle
// second triangle
};
As you can see, there is some overlap on the vertices specified. We specify bottom right and top
left twice! This is an overhead of 50% since the same rectangle could also be specified with only 4
vertices, instead of 6. This will only get worse as soon as we have more complex models that have over
1000s of triangles where there will be large chunks that overlap. What would be a better solution is to
store only the unique vertices and then specify the order at which we want to draw these vertices in. In
that case we would only have to store 4 vertices for the rectangle, and then just specify at which order
we'd like to draw them. Wouldn't it be great if OpenGL provided us with a feature like that?
Thankfully, element buffer objects work exactly like that. An EBO is a buffer, just like a vertex buffer
object, that stores indices that OpenGL uses to decide what vertices to draw. This so called indexed
drawing is exactly the solution to our problem. To get started we first have to specify the (unique)
vertices and the indices to draw them as a rectangle:
float vertices[] = {
0, 1, 3, // first triangle
1, 2, 3 // second triangle
};
You can see that, when using indices, we only need 4 vertices instead of 6. Next we need to create the
element buffer object:
glGenBuffers(1, &EBO);
Similar to the VBO we bind the EBO and copy the indices into the buffer with glBufferData. Also, just like
the VBO we want to place those calls between a bind and an unbind call, although this time we
specify GL_ELEMENT_ARRAY_BUFFER as the buffer type.
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
Note that we're now giving GL_ELEMENT_ARRAY_BUFFER as the buffer target. The last thing left to do is
replace the glDrawArrays call with glDrawElements to indicate we want to render the triangles from an
index buffer. When using glDrawElements we're going to draw using indices provided in the element
buffer object currently bound:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
The first argument specifies the mode we want to draw in, similar to glDrawArrays. The second
argument is the count or number of elements we'd like to draw. We specified 6 indices so we want to
draw 6 vertices in total. The third argument is the type of the indices which is of
type GL_UNSIGNED_INT. The last argument allows us to specify an offset in the EBO (or pass in an index
array, but that is when you're not using element buffer objects), but we're just going to leave this at 0.
The glDrawElements function takes its indices from the EBO currently bound to
the GL_ELEMENT_ARRAY_BUFFER target. This means we have to bind the corresponding EBO each time
we want to render an object with indices which again is a bit cumbersome. It just so happens that a
vertex array object also keeps track of element buffer object bindings. The last element buffer object
that gets bound while a VAO is bound, is stored as the VAO's element buffer object. Binding to a VAO
then also automatically binds that EBO.
A VAO
stores the glBindBuffer calls when the target is GL_ELEMENT_ARRAY_BUFFER. This also means it stores
its unbind calls so make sure you don't unbind the element array buffer before unbinding your VAO,
otherwise it doesn't have an EBO configured.
The resulting initialization and drawing code now looks something like this:
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glEnableVertexAttribArray(0);
[...]
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glBindVertexArray(0);
#include <iostream>
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
int main()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// remember: do NOT unbind the EBO while a VAO is active as the bound element
buffer object IS stored in the VAO; keep the EBO bound.
//glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
// You can unbind the VAO afterwards so other VAO calls won't accidentally
modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways so we generally don't
unbind VAOs (nor VBOs) when it's not directly necessary.
glBindVertexArray(0);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved
etc.)
// -------------------------------------------------------------------------
------
glfwSwapBuffers(window);
glfwPollEvents();
}
// process all input: query GLFW whether relevant keys are pressed/released this
frame and react accordingly
// ---------------------------------------------------------------------------------
------------------------
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback
function executes
// ---------------------------------------------------------------------------------
------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}
#version version_number
in type in_variable_name; So if we want to send data from one shader to
the other we’d have to declare an output in the
in type in_variable_name; sending shader and a similar input in the
receiving shader. When the types and the
names are equal
out type out_variable_name; on both sides OpenGL will link those variables
together and then it is possible to send data
between
uniform type uniform_name; shaders (this is done when linking a program
object).
void main()
{
// process input(s) and do some weird graphics stuff
...
// output processed stuff to output variable
out_variable_name = weird_stuff_we_processed;
}
Vectors
A vector in GLSL is a 2,3 or 4 component container for any of the basic types just mentioned. They can
take the following form (n represents the number of components):
The vertex shader should receive some form of input otherwise it would be pretty ineffective. The
vertex shader differs in its input, in that it receives its input straight from the vertex data. To define how
the vertex data is organized we specify the input variables with location metadata so we can configure
the vertex attributes on the CPU.
layout (location = 0)
Vertex shader
#version 330 core
layout (location = 0) in vec3 aPos; // the position variable has attribute position 0
void main() {
gl_Position = vec4(aPos, 1.0); // see how we directly give a vec3 to vec4's constructor
vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // set the output variable to a dark-red color
Fragment shader
#version 330 core
in vec4 vertexColor; // the input variable from the vertex shader (same name and same type)
void main(){
FragColor = vertexColor;
}
You can see we declared a vertexColor variable as a vec4 output that we set in the vertex shader and we
declare a similar vertexColor input in the fragment shader. Since they both have the same type and
name, the vertexColor in the fragment shader is linked to the vertexColor in the vertex shader. Because
we set the color to a dark-red color in the vertex shader, the resulting fragments should be dark-red as
well.
Let’s spice it up a bit and see if we can send a color from our application to the fragment shader!
Uniforms
Uniforms are another way to pass data from our application on the CPU to the shaders on the GPU.
Uniforms are however slightly different compared to vertex attributes. First of all, uniforms are global.
Global, meaning that a uniform variable is unique per shader program object, and can be accessed from
any shader at any stage in the shader program. Second, whatever you set the uniform value to, uniforms
will keep their values until they're either reset or updated.
To declare a uniform in GLSL we simply add the uniform keyword to a shader with a type and a name.
From that point on we can use the newly declared uniform in the shader. Let's see if this time we can set
the color of the triangle via a uniform:
void main() {
FragColor = ourColor;
We declared a uniform vec4 ourColor in the fragment shader and set the fragment's output color to the
content of this uniform value. Since uniforms are global variables, we can define them in any shader
stage we'd like so no need to go through the vertex shader again to get something to the fragment
shader.
glUseProgram(shaderProgram);
First, we retrieve the running time in seconds via glfwGetTime(). Then we vary the color in the range of
0.0 - 1.0 by using the sin function and store the result in greenValue.
Then we query for the location of the ourColor uniform using glGetUniformLocation. We supply the
shader program and the name of the uniform (that we want to retrieve the location from) to the query
function. If glGetUniformLocation returns -1, it could not find the location. Lastly we can set the uniform
value using the glUniform4f function. Note that finding the uniform location does not require you to use
the shader program first, but updating a uniform does require you to first use the program (by calling
glUseProgram), because it sets the uniform on the currently active shader program.
We saw in the previous chapter how we can fill a VBO, configure vertex attribute pointers and store it all in a VAO. This time,
we also want to add color data to the vertex data. We’re going to add color data as 3 floats to the vertices array. We assign a
red, green and blue color to each of the corners of our triangle respectively:
More attributes! Writing, compiling and managing shaders can be quite cumbersome. As a final touch on the shader
subject we’re going to make our life a bit easier by building a shader class that reads shaders from
disk, compiles and links them, checks for errors and is easy to use.
Now open your last week project and update with the following codes.
At first, we can add more attributes to the vertices by updating with the
following code.
// ------------------------------------------------------------------
float vertices[] = {
// positions // colors
};
Knowing the current layout we can update the vertex format with
glVertexAttribPointer:
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// color attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 *
sizeof(float)));
glEnableVertexAttribArray(1);
Now we need to update our source code with external fragment shader
and vertex shader files. Instead reading from inline shader files(very
inconvenient) we want to create external shader files.
Then
Rename to vertex.vs
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <fstream>
#include <sstream>
try
{
shaderFile.open(filePath);
std::stringstream shaderStream;
shaderStream << shaderFile.rdbuf();
shaderFile.close();
shaderCode = shaderStream.str();
}
catch (std::ifstream::failure e)
{
std::cout << "Error: could not read shader file " << filePath << std::endl;
}
return shaderCode;
}
Circle
Ellipse