Overview
The Shader Class lab (Lab 12) removed the last raw shader management code from main.cpp. Texture loading is the final piece of boilerplate to extract. In earlier labs, loading an image required calling stbi_load, creating a texture object with glGenTextures, setting four parameters, uploading pixel data with glTexImage2D, and remembering to call stbi_image_free and glDeleteTextures at the right time. This lab wraps all of that in a Texture class that follows the same RAII pattern as the buffer classes. After this lab, the entire resource setup that fills main.cpp can be replaced with a handful of class instantiations.
Learning Objectives
By the end of this lab you will be able to:
- Explain why stbi_set_flip_vertically_on_load is necessary before calling stbi_load
- Identify the four glTexParameteri calls and explain what each controls
- Distinguish between the internal format parameter (GL_RGBA8) and the source format parameters (GL_RGBA, GL_UNSIGNED_BYTE) in glTexImage2D
- Explain why glActiveTexture must be called before glBindTexture when using multiple textures simultaneously
- Implement the Texture class and replace the raw texture setup block in main.cpp with a Texture instance
Concept
Loading an image with stbi_load gives you a pointer to raw pixel bytes in CPU memory. OpenGL cannot access CPU memory during rendering - it needs a copy on the GPU. The glGenTextures function creates a texture object (identified by an unsigned int handle, like all GL objects), and glTexImage2D copies the pixel data from CPU to GPU. Once the upload is complete, the CPU-side buffer is no longer needed and stbi_image_free releases it. OpenGL reads texture rows from bottom to top (UV origin at the bottom-left corner), but stbi_load delivers rows from top to bottom. Calling stbi_set_flip_vertically_on_load(1) before loading corrects this mismatch so textures appear the right way up.
glTexImage2D takes two distinct format arguments that are often confused. The internal format (GL_RGBA8) describes how the GPU should store the texture - four channels, eight bits each. The source format (GL_RGBA plus GL_UNSIGNED_BYTE) describes the layout of the CPU data you are uploading. You can choose them independently: for example, you could upload 8-bit-per-channel data and ask the GPU to store it at 16-bit precision. The four glTexParameteri calls set the sampling behaviour: GL_TEXTURE_MIN_FILTER controls how the GPU samples when the texture is displayed at smaller than native resolution (with filtering to avoid aliasing), GL_TEXTURE_MAG_FILTER controls the same for larger-than-native display, and GL_TEXTURE_WRAP_S/T control what happens when UV coordinates go outside the 0-1 range. GL_CLAMP_TO_EDGE clamps the coordinate to the nearest edge pixel.
OpenGL supports up to 80 texture units (slots) that can be bound simultaneously, numbered GL_TEXTURE0 through GL_TEXTURE79. Before binding a texture for use in a draw call, you must first activate the target unit with glActiveTexture(GL_TEXTURE0 + slot), then bind the texture with glBindTexture. The corresponding GLSL sampler2D uniform must be set to the same integer slot via shader.setUniform1i("u_Texture", 0). This indirection lets a single draw call read from multiple textures at once - normal maps, diffuse maps, and shadow maps can all be bound to different slots simultaneously. The Texture::Bind(unsigned int slot) method handles both the glActiveTexture and glBindTexture calls together so the caller only specifies the slot number.
Tasks
Task 1 - Create the Texture Header
Create src/Texture.h from the skeleton below. The header includes Renderer.h (for GlCall and glm::vec2). Fill in the private member variables.
- Create
src/Texture.hand fill in the four private members:m_RendererID,m_Filepath,m_Localbuffer, and the three dimension fields. - Note the default argument on
Bind(unsigned int slot = 0)- callers can omit the slot number when only one texture is active. - Add the file to your Visual Studio project.
#pragma once
#include "Renderer.h"
class Texture
{
private:
// TODO: unsigned int m_RendererID
// TODO: std::string m_Filepath
// TODO: unsigned char* m_Localbuffer (temporary CPU pixel buffer)
// TODO: int width, height, bitsPerPixel
public:
// TODO: constructor - loads the image and uploads it to the GPU
Texture(const std::string& filepath);
// TODO: destructor - deletes the GPU texture
~Texture();
// Bind this texture to the given texture unit (default = 0)
// Call this before drawing and match the slot to the sampler2D uniform index
void Bind(unsigned int slot = 0) const;
void Unbind() const;
int getWidth() const { return width; }
int getHeight() const { return height; }
glm::vec2 getTexelSize() const { return glm::vec2(1.0f / width, 1.0f / height); }
};
Task 2 - Implement the Texture Upload and Bind
Create src/Texture.cpp from the skeleton below. Each // TODO: corresponds to exactly one GL call or stb_image call. Work through them in order - the sequence matters.
- Create
src/Texture.cpp. Include"Texture.h"and"vendor/stb_image.h". - In the constructor, call
stbi_set_flip_vertically_on_load(1)beforestbi_load. The fourth argument tostbi_loadis the number of desired channels - pass4to force RGBA output regardless of the source image format. - After the pixel data is loaded, create and bind a texture object, then set the four parameters as described in the Concept section.
- Call
glTexImage2DwithGL_RGBA8as the internal format andGL_RGBA, GL_UNSIGNED_BYTEas the source format. Use thewidthandheightvalues returned bystbi_load. - Unbind the texture after upload, then free the CPU buffer with
stbi_image_free(m_Localbuffer). - In
Bind(slot), callglActiveTexture(GL_TEXTURE0 + slot)first, thenglBindTexture. - Add
src/Texture.cppto your project. - In
main.cpp, replace the raw texture setup block with aTexture texture("res/Textures/1.png")declaration. Replace theglActiveTextureandglBindTexturecalls in the render loop withtexture.Bind(0). Remove the manualglDeleteTexturescall from cleanup. - Build and run. The textured, rotating, projected quad from Lab 08 should appear with no visual change.
#include "Texture.h"
#include "vendor/stb_image.h"
Texture::Texture(const std::string& filepath)
: m_RendererID(0), m_Filepath(filepath),
m_Localbuffer(nullptr), width(0), height(0), bitsPerPixel(0)
{
// TODO: stbi_set_flip_vertically_on_load(1)
// (OpenGL reads rows bottom-up; stb_image reads top-down - flip to match)
// TODO: m_Localbuffer = stbi_load(filepath.c_str(), &width, &height, &bitsPerPixel, 4)
// (the 4 forces RGBA output regardless of the source format)
// TODO: glGenTextures(1, &m_RendererID)
// TODO: glBindTexture(GL_TEXTURE_2D, m_RendererID)
// TODO: set four texture parameters using glTexParameteri:
// GL_TEXTURE_MIN_FILTER -> GL_LINEAR
// GL_TEXTURE_MAG_FILTER -> GL_LINEAR
// GL_TEXTURE_WRAP_S -> GL_CLAMP_TO_EDGE
// GL_TEXTURE_WRAP_T -> GL_CLAMP_TO_EDGE
// TODO: glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0,
// GL_RGBA, GL_UNSIGNED_BYTE, m_Localbuffer)
// TODO: glBindTexture(GL_TEXTURE_2D, 0) (unbind after upload)
// TODO: if (m_Localbuffer) stbi_image_free(m_Localbuffer)
}
Texture::~Texture()
{
// TODO: glDeleteTextures(1, &m_RendererID)
}
void Texture::Bind(unsigned int slot) const
{
// TODO: glActiveTexture(GL_TEXTURE0 + slot)
// (activate the correct unit before binding so multiple textures can coexist)
// TODO: glBindTexture(GL_TEXTURE_2D, m_RendererID)
}
void Texture::Unbind() const
{
// TODO: glBindTexture(GL_TEXTURE_2D, 0)
}
Solution
Try the tasks above before revealing the solution.