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:
- Store 2D position and UV texture coordinate data together as two attributes in one interleaved vertex buffer
- Build an orthographic projection matrix with glm::ortho and multiply it with a per-object model matrix to form a complete MVP uniform
- Load a PNG image with the Texture class and link it to a sampler2D uniform in GLSL
- Draw the same quad geometry twice in one frame by computing a fresh model matrix from each translation vector before each draw call
- Expose real-time position controls using Dear ImGui SliderFloat3 widgets
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
- Create a new file
src/tests/testTexture2D.husing the skeleton below as your starting point. - Replace the
TODOcomment in theprivatesection with fivestd::unique_ptrmember declarations:m_VAOwrappingVertexArray,m_VBOwrappingVertexBuffer,m_IndexBufferwrappingIndexBuffer,m_ShaderwrappingShader, andm_TexturewrappingTexture. - Create the matching source file
src/tests/testTexture2D.cpp. Add#include "testTexture2D.h"at the top and open thetestnamespace. You will fill in the method bodies in the following tasks. - Add both files to the Visual Studio project so they appear in the Solution Explorer and are compiled with the rest of the project.
#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
- In
testTexture2D.cpp, begin the constructor with the initialiser list shown below, settingm_TranslationAto(200, 200, 0)andm_TranslationBto(400, 400, 0). - 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. - Add the two
layout.Push<float>(2)calls - one for the 2D position attribute and one for the UV attribute - so theVertexBufferLayoutknows how to unpack the interleaved data correctly. - Replace the Texture TODO with the construction line:
m_Texture = std::make_unique<Texture>(R"(res/Textures/1.png)");. This line must appear beforem_Texture->Bind(). - 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
- Create
res/Shaders/Basic.shader. The customShaderclass reads two sections from this single file, identified by the markers#shader vertexand#shader fragment. - In the vertex shader, declare the two input attributes at the layout locations that match the
VertexBufferLayoutorder you set in Task 2: location 0 for avec2 positionand location 1 for avec2 texCoords. Declareout vec2 v_TexCoordsto carry UVs to the fragment stage anduniform mat4 u_MVPfor the transformation. - In the vertex
main(), setgl_Position = u_MVP * vec4(position, 0.0, 1.0);. The third component0.0places the quad on the XY plane. The fourth component1.0marks the vertex as a point in homogeneous coordinates so the translation part of the MVP matrix is applied. Pass UVs through withv_TexCoords = texCoords;. - In the fragment shader, declare the matching
in vec2 v_TexCoordsand auniform sampler2D u_Texture. Inmain(), assigncolour = texture(u_Texture, v_TexCoords);.
#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
- In
Render(), create a localRenderer rendererand callrenderer.ClearColour_White()followed byrenderer.Clear()to set a white background each frame. - Call
m_Texture->Bind()before any draw calls so the sampler has an active texture unit to read from. - 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 getmvp = m_Proj * m_View * model. Bind the shader and upload the MVP withsetUniformMat4f("u_MVP", mvp). - 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
- In
RenderGUI(), replace the two TODO comments withImGui::SliderFloat3calls. Connect the first to&m_TranslationA.xwith label"Translation A"and a range of0.0fto960.0f. Connect the second to&m_TranslationB.xwith label"Translation B". - Open
src/Main.cppand add the include near the other test headers:#include "tests/testTexture2D.h" - In the test menu registration block, add:
This scene requires no keyboard or mouse window input, so no extra arguments are passed toTestMenu->RegisterTest<test::testTexture2D>("Test Texture2D");RegisterTest. - 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.