Overview
Lab 04 exposed two problems. The vertex buffer wasted memory repeating two corner positions, and the quad's colour was fixed at compile time inside the GLSL source. This lab solves both. An Index Buffer Object (IBO) lets the GPU reuse vertex positions by number rather than repeating them, shrinking the quad from 6 stored vertices to 4. A uniform variable opens a data channel from C++ into the running shader, so the colour can be chosen and changed at runtime without touching the GLSL file.
This lab follows directly from the lecture on shaders and uniforms. The lecture covered the theory. This lab is the practical implementation.
Prerequisites
- Lab 04 complete: a quad renders as two triangles with 6 vertices and
glDrawArrays. src/main.cppcontainsparseShader,compileShader, andcreateProgramfrom Lab 03.res/shaders/triangle.shaderexists and loads correctly.
Learning Objectives
By the end of this lab you will be able to:
- Create an Index Buffer Object and explain how it eliminates duplicated vertex data.
- Use
glDrawElementsin place ofglDrawArraysand explain the difference between vertex count and index count. - Declare a
uniformvariable in GLSL and explain how it differs from a vertex attribute. - Retrieve a uniform location with
glGetUniformLocationand write a value withglUniform4f. - Diagnose the two most common uniform mistakes: querying the location before the program is linked, and calling
glUniformwithout an active program.
Concept
Lab 04 stored 6 vertices to draw a quad, repeating two corner positions to satisfy
glDrawArrays. An Index Buffer Object (IBO) removes
the duplication. Instead of listing every vertex in draw order, you store only the
4 unique positions and supply a separate array of integer indices. Each index is the
0-based slot of a position in the VBO. The GPU assembles triangles by looking up
positions by index rather than reading them sequentially. A quad needs 6 indices
but only 4 stored positions: the saving grows substantially for dense meshes where
every vertex is shared by an average of six triangles.
An IBO is a standard buffer object bound to GL_ELEMENT_ARRAY_BUFFER
instead of GL_ARRAY_BUFFER. The VAO records the element buffer binding
as part of its state, so rebinding the VAO in the render loop restores it
automatically. The draw call changes from glDrawArrays to
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0): the second
argument is the number of indices, not vertices.
A uniform is a value passed from C++ to a shader that stays
constant for every vertex and fragment processed in a single draw call. Unlike a
vertex attribute, which changes per vertex, a uniform is global to the program. You
declare it in GLSL with uniform type name, then from C++ retrieve its
location once with glGetUniformLocation and write a value each frame
with the matching glUniform* variant. The location must be retrieved
after the program is linked. The value must be set while the program is active, so
call glUseProgram first. A return value of -1 from
glGetUniformLocation means the name was not found - check for typos or
confirm the uniform is actually used in the shader.
Tasks
Task 1 - Add a Uniform to the Shader
Open res/shaders/triangle.shader and update the fragment section so
the colour comes from a uniform rather than a hard-coded constant. The vertex
section does not change.
- Add the line
uniform vec4 uColour;before theout vec4 FragColour;declaration. - Replace the hard-coded
vec4(0.2, 0.6, 1.0, 1.0)assignment withuColour.
#shader fragment
#version 430 core
// TODO: declare uniform vec4 uColour here
out vec4 FragColour;
void main() {
// TODO: assign uColour to FragColour instead of the hard-coded colour
}
out or function definitions. The GLSL compiler does not enforce
the order, but keeping uniforms at the top makes them easy to find.
Task 2 - Reduce to 4 Unique Vertices
Replace the 6-entry vertex array in src/main.cpp with the 4-entry
array below. Remove the duplicate entries that were needed by
glDrawArrays. The index array (Task 3) will tell the GPU which
positions to use and in what order.
// file: src/main.cpp -- replace the existing vertices[] array
float vertices[] = {
-0.5f, -0.5f, 0.0f, // 0: bottom-left
0.5f, -0.5f, 0.0f, // 1: bottom-right
0.5f, 0.5f, 0.0f, // 2: top-right
-0.5f, 0.5f, 0.0f // 3: top-left
};
Task 3 - Define the Index Array
Add the index array immediately after the vertex array. Each row defines one triangle by listing three vertex slot numbers. The positions themselves are not repeated - only the slot references are.
// file: src/main.cpp -- add after vertices[]
unsigned int indices[] = {
0, 1, 2, // triangle 1: bottom-left -> bottom-right -> top-right
2, 3, 0 // triangle 2: top-right -> top-left -> bottom-left
};
Task 4 - Create and Upload the IBO
Add the IBO setup after the glVertexAttribPointer and
glEnableVertexAttribArray calls, while the VAO is still bound. The
VAO stores the element buffer binding as part of its state.
- Declare
unsigned int ibo. - Call
glGenBuffers(1, &ibo). - Call
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo). - Call
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW). - Now unbind the VAO with
glBindVertexArray(0).
// file: src/main.cpp -- add after glEnableVertexAttribArray, while VAO is still bound
// TODO: declare unsigned int ibo
// TODO: generate one buffer and store in ibo
// TODO: bind as GL_ELEMENT_ARRAY_BUFFER
// TODO: upload sizeof(indices) bytes with GL_STATIC_DRAW
glBindVertexArray(0); // unbind VAO after the IBO is bound, not before
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0) while the VAO is still
bound, you remove the reference and the draw call will read from the wrong
location. Unbind the VAO first, then optionally unbind other buffers.
Also add glDeleteBuffers(1, &ibo) to the cleanup section after
the render loop, alongside the existing VBO cleanup.
Task 5 - Switch to glDrawElements
Inside the render loop, replace the glDrawArrays call. The new call
reads 6 indices from the bound IBO and uses them to assemble two triangles from
the 4 positions in the VBO.
- Remove:
glDrawArrays(GL_TRIANGLES, 0, 6); - Add the call below.
// TODO: replace glDrawArrays with glDrawElements
// primitive: GL_TRIANGLES
// count: 6 (number of indices, not vertices)
// type: GL_UNSIGNED_INT
// offset: (void*)0
Task 6 - Set the Uniform Value from C++
Wire the C++ side of the uniform. The location is queried once after the program is created and cached for reuse. The value is written each frame after the program is activated.
-
After the
createProgramcall and before the render loop, retrieve and store the uniform location:
// file: src/main.cpp -- add after createProgram, before the render loop
// TODO: call glGetUniformLocation(shader, "uColour") and store the result in int colourLoc
// if colourLoc is -1, the name was not found - check the spelling matches the shader exactly
-
Inside the render loop, after
glUseProgram(shader)and before the draw call, set the colour:
// TODO: call glUniform4f(colourLoc, r, g, b, a) with a colour of your choice
// glUniform4f takes four floats in the range 0.0 to 1.0 (red, green, blue, alpha)
glUseProgram before any glUniform call. If either rule
is broken the value is silently ignored and the quad appears black (the default
for an unset vec4 uniform is all zeros).
Expected Result
A solid coloured quad fills the same region as Lab 04, but now displaying
whatever colour you passed to glUniform4f. Try values like
(1.0f, 0.5f, 0.0f, 1.0f) for orange or
(0.1f, 0.9f, 0.4f, 1.0f) for green.
If the quad is black, glUniform4f is either not being called or is
being called before glUseProgram. If the quad is missing entirely,
check that the IBO was bound while the VAO was still active (Task 4). If the
console shows a shader compile error, check the uniform declaration in
triangle.shader for typos.
Extension Challenge
Optional: Animate the quad's colour over time using a second
uniform. Declare uniform float uTime in the fragment shader and
compute the colour from it:
uniform float uTime;
out vec4 FragColour;
void main() {
float r = sin(uTime * 1.0) * 0.5 + 0.5;
float g = sin(uTime * 0.7 + 2.0) * 0.5 + 0.5;
float b = sin(uTime * 1.3 + 4.0) * 0.5 + 0.5;
FragColour = vec4(r, g, b, 1.0);
}
In src/main.cpp, retrieve the uTime location the same
way as uColour. Inside the render loop, pass the elapsed time with
glUniform1f(timeLoc, (float)glfwGetTime()). The quad should cycle
through a range of colours smoothly. Notice that different phase offsets and
frequencies on each channel produce a richer animation than cycling all three
channels at the same rate.
Solution
Try to complete the tasks above before reading this section.
The updated shader file is shown first, then the complete src/main.cpp.
res/shaders/triangle.shader
#shader vertex
#version 430 core
layout (location = 0) in vec3 aPos;
void main() {
gl_Position = vec4(aPos, 1.0);
}
#shader fragment
#version 430 core
uniform vec4 uColour;
out vec4 FragColour;
void main() {
FragColour = uColour;
}
src/main.cpp
// file: src/main.cpp -- complete solution for Lab 05
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <fstream>
#include <sstream>
#include <iostream>
#include <string>
struct ShaderSource { std::string vertex; std::string fragment; };
ShaderSource parseShader(const std::string& filepath) {
std::ifstream file(filepath);
std::stringstream ss[2];
int type = -1;
std::string line;
while (std::getline(file, line)) {
if (line.find("#shader vertex") != std::string::npos) type = 0;
else if (line.find("#shader fragment") != std::string::npos) type = 1;
else if (type != -1) ss[type] << line << '\n';
}
return { ss[0].str(), ss[1].str() };
}
static unsigned int compileShader(unsigned int type, const std::string& source) {
unsigned int id = glCreateShader(type);
const char* src = source.c_str();
glShaderSource(id, 1, &src, nullptr);
glCompileShader(id);
int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
if (!result) {
int length;
glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
char* message = new char[length];
glGetShaderInfoLog(id, length, &length, message);
std::cerr << "Shader compile error: " << message << '\n';
delete[] message;
return 0;
}
return id;
}
static unsigned int createProgram(const std::string& vertSrc, const std::string& fragSrc) {
unsigned int program = glCreateProgram();
unsigned int vs = compileShader(GL_VERTEX_SHADER, vertSrc);
unsigned int fs = compileShader(GL_FRAGMENT_SHADER, fragSrc);
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}
int main()
{
if (!glfwInit()) { std::cerr << "GLFW init failed\n"; return -1; }
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL Window", nullptr, nullptr);
if (!window) { glfwTerminate(); return -1; }
glfwMakeContextCurrent(window);
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK) { glfwTerminate(); return -1; }
std::cout << "OpenGL: " << glGetString(GL_VERSION) << '\n';
glfwSwapInterval(1);
// 4 unique vertex positions (down from 6 in Lab 04)
float vertices[] = {
-0.5f, -0.5f, 0.0f, // 0: bottom-left
0.5f, -0.5f, 0.0f, // 1: bottom-right
0.5f, 0.5f, 0.0f, // 2: top-right
-0.5f, 0.5f, 0.0f // 3: top-left
};
// 6 indices reference the 4 positions without repeating position data
unsigned int indices[] = {
0, 1, 2, // triangle 1
2, 3, 0 // triangle 2
};
// VAO records the vertex layout and the IBO reference
unsigned int vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// VBO holds the 4 unique vertex positions
unsigned int vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// IBO holds the 6 indices - must be bound while the VAO is active
unsigned int ibo;
glGenBuffers(1, &ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// unbind VAO first to preserve the IBO reference stored in it
glBindVertexArray(0);
// load shader and cache the uniform location once after linking
ShaderSource src = parseShader("res/shaders/triangle.shader");
unsigned int shader = createProgram(src.vertex, src.fragment);
int colourLoc = glGetUniformLocation(shader, "uColour");
while (!glfwWindowShouldClose(window))
{
glClearColor(0.1f, 0.1f, 0.15f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// activate the program before setting uniforms
glUseProgram(shader);
glUniform4f(colourLoc, 1.0f, 0.5f, 0.0f, 1.0f); // orange
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(1, &vbo);
glDeleteBuffers(1, &ibo);
glDeleteProgram(shader);
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}