University of Wales Trinity Saint David
ACCA6021 - Graphics and Games Programming

Lab 13 - Texture Class

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:

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.

  1. Create src/Texture.h and fill in the four private members: m_RendererID, m_Filepath, m_Localbuffer, and the three dimension fields.
  2. Note the default argument on Bind(unsigned int slot = 0) - callers can omit the slot number when only one texture is active.
  3. Add the file to your Visual Studio project.
src/Texture.h
#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.

  1. Create src/Texture.cpp. Include "Texture.h" and "vendor/stb_image.h".
  2. In the constructor, call stbi_set_flip_vertically_on_load(1) before stbi_load. The fourth argument to stbi_load is the number of desired channels - pass 4 to force RGBA output regardless of the source image format.
  3. After the pixel data is loaded, create and bind a texture object, then set the four parameters as described in the Concept section.
  4. Call glTexImage2D with GL_RGBA8 as the internal format and GL_RGBA, GL_UNSIGNED_BYTE as the source format. Use the width and height values returned by stbi_load.
  5. Unbind the texture after upload, then free the CPU buffer with stbi_image_free(m_Localbuffer).
  6. In Bind(slot), call glActiveTexture(GL_TEXTURE0 + slot) first, then glBindTexture.
  7. Add src/Texture.cpp to your project.
  8. In main.cpp, replace the raw texture setup block with a Texture texture("res/Textures/1.png") declaration. Replace the glActiveTexture and glBindTexture calls in the render loop with texture.Bind(0). Remove the manual glDeleteTextures call from cleanup.
  9. Build and run. The textured, rotating, projected quad from Lab 08 should appear with no visual change.
src/Texture.cpp
#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.