Overview
The Index Buffers and Uniforms lab (Lab 05) gave the quad a runtime colour through a
uniform vec4. The colour was still a flat solid. This lab replaces that
flat colour with a sampled image by introducing the texture pipeline:
UV coordinates stored per vertex, an image loaded from disk with
stb_image, an OpenGL texture object on the GPU, and a
sampler2D uniform that lets the fragment shader look up the right pixel
for each fragment.
By the end of the lab your quad will display a loaded PNG with correct orientation and alpha blending.
Learning Objectives
By the end of this lab you will be able to:
- Extend vertex data with UV coordinates and configure a second vertex attribute pointer for them.
- Write a GLSL vertex shader that passes UV coordinates to the fragment stage as a varying.
- Load an image from disk using stb_image and upload it to the GPU with glTexImage2D.
- Configure texture filtering and wrapping parameters with glTexParameteri.
- Sample a texture in a fragment shader using a sampler2D uniform and the texture() function.
- Enable alpha blending so transparent pixels in the image render correctly.
Concept
So far every fragment has been coloured by a value computed entirely in the shader. A texture is a 2D image stored on the GPU that the fragment shader can query to retrieve colour data. Without textures, representing surface detail such as wood grain, skin, or printed labels would require thousands of extra vertices. With textures, a single quad can display an arbitrarily detailed image while remaining just four vertices in the VBO.
The connection between vertices and image pixels is made through
UV coordinates (also called texture coordinates). Each vertex carries
a vec2 UV value in addition to its position. UV coordinates range from
0.0 to 1.0 across each axis regardless of the image
resolution: (0,0) is the bottom-left corner and (1,1) is
the top-right. The GPU interpolates the UV across each triangle so that every fragment
receives a unique coordinate, which the shader uses to look up the correct texel.
This lookup is performed with the built-in GLSL function
texture(sampler, uv), where sampler is a
sampler2D uniform bound to a texture unit.
On the C++ side, stb_image is a single-header library that decodes
common image formats (PNG, JPG, BMP) into a raw byte array. You call
stbi_set_flip_vertically_on_load(1) before loading because OpenGL
expects the first row of pixel data to be the bottom of the image, while most image
files store rows top-to-bottom. The decoded bytes are uploaded to the GPU with
glTexImage2D. You then configure how OpenGL samples the texture when it
is displayed at a different size with glTexParameteri: the
GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER parameters
control interpolation, and the GL_TEXTURE_WRAP_S and
GL_TEXTURE_WRAP_T parameters control what happens when UV coordinates
fall outside the 0-1 range. Once the texture is on the GPU, the CPU-side image data
can be freed with stbi_image_free.
Tasks
Task 1 - Add UV Coordinates to Vertex Data
The vertex array currently stores 3 floats per vertex (x, y, z). Extend each vertex to 4 floats by appending a U and V coordinate. These will map the corners of the quad to the corners of the image.
-
Replace the
verticesarray insrc/main.cppwith the 4-float-per-vertex layout below. Each row containsx, y, u, v. -
Update the
glBufferDatacall so the size argument still usessizeof(vertices)rather than a hard-coded number. -
The existing
glVertexAttribPointerfor attribute 0 (position) must now declare a stride of4 * sizeof(float)(16 bytes) because each vertex now occupies 4 floats, not 3. Update the stride and change the component count from3to2(the position is now 2D). -
After the position attribute call, add a second
glVertexAttribPointerfor attribute 1 (UV). The offset is(void*)(2 * sizeof(float))because the UV data starts 2 floats into each vertex. - Enable attribute 1 with
glEnableVertexAttribArray(1).
// Each vertex: position (x, y) followed by UV coordinates (u, v)
float vertices[] = {
-0.5f, -0.5f, 0.0f, 0.0f, // bottom-left
0.5f, -0.5f, 1.0f, 0.0f, // bottom-right
0.5f, 0.5f, 1.0f, 1.0f, // top-right
-0.5f, 0.5f, 0.0f, 1.0f // top-left
};
// ... (keep the indices array and IBO setup from Lab 05 unchanged)
// Attribute 0: position - 2 floats, stride = 4 floats, offset = 0
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
// TODO: declare and enable attribute 1 for UV coordinates
// 2 components, same stride, offset = 2 * sizeof(float)
Task 2 - Write a Texture Shader
Create a new shader file res/shaders/texture.shader. The vertex
section must accept the UV attribute and pass it to the fragment section as a
varying. The fragment section must sample a
sampler2D uniform and output the result as the fragment colour.
- Create the file
res/shaders/texture.shader. -
In the
#shader vertexsection, add a second input attribute at location 1:layout(location = 1) in vec2 texCoords;. -
Declare
out vec2 v_TexCoordsand assigntexCoordsto it inmain(). The GPU interpolates this across the triangle for every fragment. -
In the
#shader fragmentsection, declarein vec2 v_TexCoordsanduniform sampler2D u_Texture. -
In the fragment
main(), calltexture(u_Texture, v_TexCoords)and assign the result toFragColour. -
Update the
parseShadercall inmain.cppto load"res/shaders/texture.shader".
#shader vertex
#version 430 core
layout(location = 0) in vec2 position;
// TODO: declare layout(location = 1) in vec2 texCoords
// TODO: declare out vec2 v_TexCoords
void main() {
gl_Position = vec4(position, 0.0, 1.0);
// TODO: pass texCoords to the fragment stage via v_TexCoords
}
#shader fragment
#version 430 core
out vec4 FragColour;
// TODO: declare in vec2 v_TexCoords
// TODO: declare uniform sampler2D u_Texture
void main() {
// TODO: assign texture(u_Texture, v_TexCoords) to FragColour
}
Task 3 - Load a Texture with stb_image
Before the render loop, load the image from disk and upload it to the GPU as
an OpenGL texture object. Place any PNG or JPG at res/Textures/1.png.
-
Add
#include "vendor/stb_image.h"near the top ofsrc/main.cpp. The implementation is already compiled insrc/vendor/stb_image.cpp, so you do not need the#define STB_IMAGE_IMPLEMENTATIONguard here. -
After the VAO and shader setup, call
stbi_set_flip_vertically_on_load(1). This corrects the vertical flip because OpenGL reads rows bottom-up while PNG files store rows top-down. -
Call
stbi_loadwith the path and4as the channel count to force RGBA output regardless of the source format. -
Generate and configure an OpenGL texture using the skeleton below.
GL_CLAMP_TO_EDGEprevents a thin border artifact at the quad edges.GL_LINEARblends neighbouring texels for a smooth result when the quad is scaled. - Free the CPU-side image data:
if (imgData) stbi_image_free(imgData); - Add
glDeleteTextures(1, &texture)to the cleanup section after the render loop.
stbi_set_flip_vertically_on_load(1);
int imgWidth, imgHeight, imgChannels;
unsigned char* imgData = stbi_load("res/Textures/1.png", &imgWidth, &imgHeight, &imgChannels, 4);
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// TODO: call glTexImage2D to upload imgData to the GPU
// target: GL_TEXTURE_2D, level: 0, internal format: GL_RGBA8
// width/height: imgWidth/imgHeight, border: 0
// format: GL_RGBA, type: GL_UNSIGNED_BYTE, data: imgData
glBindTexture(GL_TEXTURE_2D, 0);
if (imgData) stbi_image_free(imgData);
Task 4 - Bind the Texture and Set the Sampler Uniform
OpenGL uses texture units to let a single shader sample multiple
textures simultaneously. You activate a unit with
glActiveTexture(GL_TEXTURE0), bind your texture to it, and tell the
shader which unit to read from by setting the sampler2D uniform to
the unit index (0 for GL_TEXTURE0).
-
After the shader program is created but before the render loop, enable alpha
blending so transparent pixels render correctly:
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); -
Still before the loop, link the sampler uniform to texture unit 0. The
uniform must be set while the program is active:
glUseProgram(shader); int texLoc = glGetUniformLocation(shader, "u_Texture"); glUniform1i(texLoc, 0); glUseProgram(0); -
Inside the render loop, before the draw call, activate unit 0 and bind
your texture:
glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture); -
Build and run. Your image should appear on the quad. If it is upside-down,
confirm that
stbi_set_flip_vertically_on_load(1)is called beforestbi_load.
Solution
Try the tasks above before revealing the solution.