0% found this document useful (0 votes)
5 views

Lab Manual 3 - Shader Programming

The document discusses using element buffer objects (EBOs) to render a rectangle in OpenGL. An EBO stores indices that OpenGL uses for indexed drawing, allowing the same vertices to be reused while reducing overhead. The example shows creating an EBO, copying index data to it, and using it with glDrawElements to render two triangles that make up a rectangle.

Uploaded by

2021-3-60-267
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views

Lab Manual 3 - Shader Programming

The document discusses using element buffer objects (EBOs) to render a rectangle in OpenGL. An EBO stores indices that OpenGL uses for indexed drawing, allowing the same vertices to be reused while reducing overhead. The example shows creating an EBO, copying index data to it, and using it with glDrawElements to render two triangles that make up a rectangle.

Uploaded by

2021-3-60-267
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 20

Rendering a rectangle and EBO

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

0.5f, 0.5f, 0.0f, // top right

0.5f, -0.5f, 0.0f, // bottom right

-0.5f, 0.5f, 0.0f, // top left

// second triangle

0.5f, -0.5f, 0.0f, // bottom right

-0.5f, -0.5f, 0.0f, // bottom left

-0.5f, 0.5f, 0.0f // top left

};

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.5f, 0.5f, 0.0f, // top right

0.5f, -0.5f, 0.0f, // bottom right

-0.5f, -0.5f, 0.0f, // bottom left

-0.5f, 0.5f, 0.0f // top left


};

unsigned int indices[] = { // note that we start from 0!

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:

unsigned int EBO;

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

glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

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

glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

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:

// ..:: Initialization code :: ..

// 1. bind Vertex Array Object

glBindVertexArray(VAO);

// 2. copy our vertices array in a vertex buffer for OpenGL to use

glBindBuffer(GL_ARRAY_BUFFER, VBO);

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// 3. copy our index array in a element buffer for OpenGL to use

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// 4. then set the vertex attributes pointers

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

glEnableVertexAttribArray(0);
[...]

// ..:: Drawing code (in render loop) :: ..

glUseProgram(shaderProgram);

glBindVertexArray(VAO);

glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

glBindVertexArray(0);

Update your previous code with following code:


#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);


void processInput(GLFWwindow* window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

const char* vertexShaderSource = "#version 330 core\n"


"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";

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

// glfw window creation


// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Rectangle and
EBO", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

// glad: load all OpenGL function pointers


// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}

// build and compile our shader program


// ------------------------------------
// vertex shader
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// check for shader compile errors
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog <<
std::endl;
}
// fragment shader
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// check for shader compile errors
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog <<
std::endl;
}
// link shaders
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// check for linking errors
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog <<
std::endl;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

// set up vertex data (and buffer(s)) and configure vertex attributes


// ------------------------------------------------------------------
float vertices[] = {
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};
unsigned int indices[] = { // note that we start from 0!
0, 1, 3, // first Triangle
1, 2, 3 // second Triangle
};
unsigned int VBO, VAO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and
then configure vertex attributes(s).
glBindVertexArray(VAO);

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

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);


glEnableVertexAttribArray(0);
// note that this is allowed, the call to glVertexAttribPointer registered VBO
as the vertex attribute's bound vertex buffer object so afterwards we can safely
unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);

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

// uncomment this call to draw in wireframe polygons.


//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);

// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

// draw our first triangle


glUseProgram(shaderProgram);
glBindVertexArray(VAO); // seeing as we only have a single VAO there's no
need to bind it every time, but we'll do so to keep things a bit more organized
//glDrawArrays(GL_TRIANGLES, 0, 6);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// glBindVertexArray(0); // no need to unbind it every time

// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved
etc.)
// -------------------------------------------------------------------------
------
glfwSwapBuffers(window);
glfwPollEvents();
}

// optional: de-allocate all resources once they've outlived their purpose:


// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
glDeleteProgram(shaderProgram);

// glfw: terminate, clearing all previously allocated GLFW resources.


// ------------------------------------------------------------------
glfwTerminate();
return 0;
}

// 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);
}

Result will be,


The vertex and fragment shader differ a bit though.
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. We’ve seen this in the previous chapter as layout (location = 0). The vertex shader thus requires an extra
layout specification for its inputs so we can link it with the vertex data.
The other exception is that the fragment shader requires a vec4 color output variable, since the fragment shaders needs to generate a final output
color. If you fail to specify an output color in your fragment shader, the color buffer output for those fragments will be undefined (which usually means
OpenGL will render them either black or white).
Introduction to GLSL
GLSL (OpenGL Shading Language) is a high-level shading language that allows developers to write
shaders to be executed on the GPU (graphics processing unit). Shaders are small programs that run on
In the GPU and are used to specify how the geometry and textures in a 3D scene are rendered. This lab
computer
graphics, a
manual will provide an introduction to GLSL and cover the basics of creating shaders using GLSL.
shader is a
computer
program GLSL
that
calculates
the Shaders are written in the C-like language GLSL. GLSL is tailored for use with graphics and contains
appropriate useful features specifically targeted at vector and matrix manipulation.
levels of
light,
darkness, Shaders always begin with a version declaration, followed by a list of input and output variables,
and color
during the uniforms and its main function. Each shader's entry point is at its main function where we process any
rendering input variables and output the results in its output variables. Don't worry if you don't know what
of a 3D
scene uniforms are, we'll get to those shortly.

A shader typically has the following structure:

#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):

vecn: the default vector of n floats.

bvecn: a vector of n booleans.

ivecn: a vector of n integers.


uvecn: a vector of n unsigned integers.

dvecn: a vector of n double components.

Ins and outs


Shaders are nice little programs on their own, but they are part of a whole and for that reason we want
to have inputs and outputs on the individual shaders so that we can move stuff around. GLSL defined
the in and out keywords specifically for that purpose. Each shader can specify inputs and outputs using
those keywords and wherever an output variable matches with an input variable of the next shader
stage they're passed along. The vertex and fragment shader differ a bit though.

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

out vec4 vertexColor; // specify a color output to the fragment shader

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

out vec4 FragColor;

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:

#version 330 core

out vec4 FragColor;

uniform vec4 ourColor; // we set this variable in the OpenGL code.

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.

float timeValue = glfwGetTime();

float greenValue = (sin(timeValue) / 2.0f) + 0.5f;

int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");

glUseProgram(shaderProgram);

glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);


As you can see, uniforms are a useful tool for setting attributes that may change every frame, or
for interchanging data between your application and your shaders, but what if we want to set a color
for each vertex? In that case we’d have to declare as many uniforms as we have vertices. A better
solution would be to include more data in the vertex attributes which is what we’re going to do now.

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.

// set up vertex data (and buffer(s)) and configure vertex attributes

// ------------------------------------------------------------------

float vertices[] = {

// positions // colors

0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right

-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left

0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top

};

Since we now have more data to send to the vertex shader, it is


necessary to adjust the vertex shader to also receive our color value as
a vertex attribute input. Note that we set the location of the aColor
attribute to 1 with the layout specifier:
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout (location = 1) in vec3 aColor;\n"
"out vec3 ourColor;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos, 1.0);\n"
" ourColor = aColor;\n"
"}\0";

const char* fragmentShaderSource = "#version 330 core\n"


"out vec4 FragColor;\n"
"in vec3 ourColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(ourColor, 1.0f);\n"
"}\n\0";

Because we added another vertex attribute and updated the VBO's


memory we have to re-configure the vertex attribute pointers. The
updated data in the VBO's memory now looks a bit like this:

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.

Add the following function in the code:


At first create two file vertex.vs and frag.fs

Then
Rename to vertex.vs

Similarly create frag.fs and your solution explorer should


look like,
Now update your code

#include <glad/glad.h>
#include <GLFW/glfw3.h>

//add the following header files at the beginning of the code

#include <iostream>
#include <fstream>
#include <sstream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);


void processInput(GLFWwindow* window);

// Read file function

std::string ReadFile(const char* filePath);

//update the code in the vertex shader


std::string vertexShaderSource = ReadFile("src/vertex.vs");
const char* vertexShaderCode = vertexShaderSource.c_str();

unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);

glShaderSource(vertexShader, 1, &vertexShaderCode, NULL);

// update the code in the fragment shader


std::string fragmentShaderSource = ReadFile("src/frag.fs");
const char* fragmentShaderCode = fragmentShaderSource.c_str();

unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);

glShaderSource(fragmentShader, 1, &fragmentShaderCode, NULL);

//Add the following function body at the end of the file

std::string ReadFile(const char* filePath)


{
std::string shaderCode;
std::ifstream shaderFile;

// ensure ifstream objects can throw exceptions:


shaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);

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;
}

Now run your program you will see following output:


Exercise, render the following shapes:
2 rectangles:

Circle
Ellipse

You might also like