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

Lab 18 - Texture2D

Overview

This lab brings together every abstraction class built in the previous series - VertexArray, VertexBuffer, IndexBuffer, Shader, Texture, and Renderer - into a complete test scene. The result is a PNG-textured quad drawn twice on screen at positions controlled in real time via Dear ImGui sliders. Along the way you will learn how to store UV texture coordinates alongside position data in a single vertex buffer, how to build a 2D orthographic projection matrix, and how to draw the same geometry at two different positions in a single frame by uploading a distinct model matrix before each draw call.

Learning Objectives

By the end of this lab you will be able to:

Concept

Without a transformation matrix, every vertex drawn by the GPU stays at its declared local coordinates. Placing the same quad at two different positions on screen is impossible unless the CPU tells the GPU a unique offset for each copy. A Model-View-Projection (MVP) matrix solves this problem. It is a single 4x4 matrix that carries a vertex all the way from local object space to clip space in one multiplication, and it is uploaded to the shader as a uniform mat4 whose value can be changed between draw calls.

The MVP is the product of three components. The model matrix places the object in the world. In this lab it is a translation built with glm::translate. The view matrix moves the scene to simulate a camera. For this 2D scene the view is an identity matrix, meaning the camera sits at the origin facing the XY plane. The projection matrix maps the 3D scene onto the 2D screen. glm::ortho builds an orthographic projection that maps a chosen rectangle of world coordinates directly to clip space with no perspective distortion. Passing pixel-sized bounds (0 to 960 in width, 0 to 540 in height) means object coordinates correspond to pixel-like screen positions, which makes layout straightforward.

UV coordinates are a second per-vertex attribute stored in the same vertex buffer immediately after the position. Each UV pair is a 2D value from 0 to 1 that pins the vertex to a corner of the texture image. The VertexBufferLayout class records both attributes with two calls to Push<float>(2). The GPU uses this layout to unpack the interleaved data automatically. Inside the fragment shader, a sampler2D uniform receives the texture unit index set from C++ with setUniform1i. The built-in texture() function then returns the texel colour at the interpolated UV coordinate for each fragment.

Tasks

Task 1 - Declare the class

  1. Create a new file src/tests/testTexture2D.h using the skeleton below as your starting point.
  2. Replace the TODO comment in the private section with five std::unique_ptr member declarations: m_VAO wrapping VertexArray, m_VBO wrapping VertexBuffer, m_IndexBuffer wrapping IndexBuffer, m_Shader wrapping Shader, and m_Texture wrapping Texture.
  3. Create the matching source file src/tests/testTexture2D.cpp. Add #include "testTexture2D.h" at the top and open the test namespace. You will fill in the method bodies in the following tasks.
  4. Add both files to the Visual Studio project so they appear in the Solution Explorer and are compiled with the rest of the project.
src/tests/testTexture2D.h
#pragma once
#include "Tests.h"
#include "../Renderer.h"
#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"
#include "../VertexBuffer.h"
#include "../VertexBufferLayout.h"
#include "../IndexBuffer.h"
#include "../VertexArray.h"
#include "../Shader.h"
#include "../Texture.h"
#include "../vendor/imgui/imgui.h"
#include "../vendor/imgui/imgui_impl_glfw.h"
#include "../vendor/imgui/imgui_impl_opengl3.h"
#include <memory>

namespace test
{
    class testTexture2D : public Tests
    {
    public:
        testTexture2D();
        void Update(float deltaTime) override;
        void Render() override;
        void RenderGUI() override;

    private:
        glm::mat4 m_Proj, m_View;
        glm::vec3 m_TranslationA, m_TranslationB;

        // TODO: declare five std::unique_ptr members:
        //   m_VAO         -> VertexArray
        //   m_VBO         -> VertexBuffer
        //   m_IndexBuffer -> IndexBuffer
        //   m_Shader      -> Shader
        //   m_Texture     -> Texture
    };
}

Task 2 - Implement the constructor

  1. In testTexture2D.cpp, begin the constructor with the initialiser list shown below, setting m_TranslationA to (200, 200, 0) and m_TranslationB to (400, 400, 0).
  2. Fill in the four vertex entries in positions[]. Each row holds four floats: the 2D screen position (x, y) followed by the UV coordinate (u, v). The quad is centred on the origin so each corner is 50 units from the centre. UV values run from 0.0 at the bottom-left to 1.0 at the top-right of the texture.
  3. Add the two layout.Push<float>(2) calls - one for the 2D position attribute and one for the UV attribute - so the VertexBufferLayout knows how to unpack the interleaved data correctly.
  4. Replace the Texture TODO with the construction line: m_Texture = std::make_unique<Texture>(R"(res/Textures/1.png)");. This line must appear before m_Texture->Bind().
  5. Complete the uniform link by replacing the final TODO with m_Shader->setUniform1i("u_Texture", 0); to tell the sampler which texture unit to read from.
testTexture2D::testTexture2D()
    : m_TranslationA(200, 200, 0), m_TranslationB(400, 400, 0)
{
    GlCall(glEnable(GL_BLEND));
    GlCall(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));

    // Each vertex holds four floats: x, y (position), then u, v (UV).
    // The quad is centred on the origin. Corners sit at +/-50 units.
    // UV coordinates run from 0.0 (bottom-left) to 1.0 (top-right).
    float positions[] = {
        // TODO: fill in four rows of (x, y, u, v)
    };

    unsigned int indices[] = { 0, 1, 2, 2, 3, 0 }; // two CCW triangles

    m_Shader = std::make_unique<Shader>(R"(res/Shaders/Basic.shader)");
    m_VAO    = std::make_unique<VertexArray>();
    m_VBO    = std::make_unique<VertexBuffer>(positions, 4 * 4 * sizeof(float));

    VertexBufferLayout layout;
    // TODO: layout.Push<float>(2);  // 2D position
    // TODO: layout.Push<float>(2);  // UV texture coordinate
    m_VAO->AddBuffer(*m_VBO, layout);

    m_IndexBuffer = std::make_unique<IndexBuffer>(indices, 6);

    // Orthographic projection: 960x540 viewport, origin at bottom-left
    m_Proj = glm::ortho(0.0f, 960.0f, 0.0f, 540.0f, -1.0f, 1.0f);
    m_View = glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, 0));

    // TODO: m_Texture = std::make_unique<Texture>(R"(res/Textures/1.png)");
    m_Texture->Bind();
    m_Shader->Bind();
    // TODO: m_Shader->setUniform1i("u_Texture", 0);

    m_VAO->unBind();
    m_Shader->Unbind();
    m_VBO->Unbind();
    m_IndexBuffer->Unbind();
}

Task 3 - Write the GLSL shader

  1. Create res/Shaders/Basic.shader. The custom Shader class reads two sections from this single file, identified by the markers #shader vertex and #shader fragment.
  2. In the vertex shader, declare the two input attributes at the layout locations that match the VertexBufferLayout order you set in Task 2: location 0 for a vec2 position and location 1 for a vec2 texCoords. Declare out vec2 v_TexCoords to carry UVs to the fragment stage and uniform mat4 u_MVP for the transformation.
  3. In the vertex main(), set gl_Position = u_MVP * vec4(position, 0.0, 1.0);. The third component 0.0 places the quad on the XY plane. The fourth component 1.0 marks the vertex as a point in homogeneous coordinates so the translation part of the MVP matrix is applied. Pass UVs through with v_TexCoords = texCoords;.
  4. In the fragment shader, declare the matching in vec2 v_TexCoords and a uniform sampler2D u_Texture. In main(), assign colour = texture(u_Texture, v_TexCoords);.
res/Shaders/Basic.shader
#shader vertex
#version 330 core

// TODO: layout(location = 0) in vec2 position;
// TODO: layout(location = 1) in vec2 texCoords;
// TODO: out vec2 v_TexCoords;
// TODO: uniform mat4 u_MVP;

void main()
{
    // TODO: gl_Position = u_MVP * vec4(position, 0.0, 1.0);
    // TODO: v_TexCoords = texCoords;
}


#shader fragment
#version 330 core

layout(location = 0) out vec4 colour;

// TODO: in vec2 v_TexCoords;
// TODO: uniform sampler2D u_Texture;

void main()
{
    // TODO: colour = texture(u_Texture, v_TexCoords);
}

Task 4 - Render the quad at two positions

  1. In Render(), create a local Renderer renderer and call renderer.ClearColour_White() followed by renderer.Clear() to set a white background each frame.
  2. Call m_Texture->Bind() before any draw calls so the sampler has an active texture unit to read from.
  3. For Object A, fill in the MVP computation inside the scoped block: build a model matrix with glm::translate(glm::mat4(1.0f), m_TranslationA), then multiply to get mvp = m_Proj * m_View * model. Bind the shader and upload the MVP with setUniformMat4f("u_MVP", mvp).
  4. Complete the Object B block in the same way using m_TranslationB. The geometry is identical so the VAO and index buffer are reused without rebinding.
void testTexture2D::Render()
{
    Renderer renderer;
    renderer.ClearColour_White();
    renderer.Clear();

    m_Texture->Bind(); // activate texture unit 0 before drawing

    { // Object A
        glm::mat4 model = glm::translate(glm::mat4(1.0f), m_TranslationA);
        // TODO: glm::mat4 mvp = m_Proj * m_View * model;
        m_Shader->Bind();
        // TODO: m_Shader->setUniformMat4f("u_MVP", mvp);
        renderer.Draw(*m_VAO, *m_IndexBuffer, *m_Shader);
    }

    { // Object B
        glm::mat4 model = glm::translate(glm::mat4(1.0f), m_TranslationB);
        // TODO: compute mvp from m_TranslationB and draw
    }
}

Task 5 - Add ImGui controls and register the scene

  1. In RenderGUI(), replace the two TODO comments with ImGui::SliderFloat3 calls. Connect the first to &m_TranslationA.x with label "Translation A" and a range of 0.0f to 960.0f. Connect the second to &m_TranslationB.x with label "Translation B".
  2. Open src/Main.cpp and add the include near the other test headers:
    #include "tests/testTexture2D.h"
  3. In the test menu registration block, add:
    TestMenu->RegisterTest<test::testTexture2D>("Test Texture2D");
    This scene requires no keyboard or mouse window input, so no extra arguments are passed to RegisterTest.
  4. Build and run the project. Select Test Texture2D from the menu. Two textured quads should appear on a white background. Drag the Translation A and Translation B sliders to move them independently around the 960x540 viewport.
void testTexture2D::RenderGUI()
{
    // TODO: ImGui::SliderFloat3("Translation A", &m_TranslationA.x, 0.0f, 960.0f);
    // TODO: ImGui::SliderFloat3("Translation B", &m_TranslationB.x, 0.0f, 960.0f);

    float rate = ImGui::GetIO().Framerate;
    ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / rate, rate);
}

Solution

Try the tasks above before revealing the solution.