Overview
The Mesh Class lab (Lab 14b) gave you a reusable container for vertex data and GPU buffers. The problem now is getting actual geometry into that container. Manually writing vertex arrays for every shape is verbose and error-prone. GeometryFactory solves this with a set of static factory methods that generate complete, GPU-ready primitives with correct winding order, normals, and UV coordinates and hand them back as std::unique_ptr<Mesh>. This lab walks through implementing the factory pattern, generating a triangle, a quad, and a cube, and understanding the indexed rendering technique that makes quads and cubes more memory-efficient than naive per-triangle duplication.
Learning Objectives
By the end of this lab you will be able to:
- Implement the static factory method pattern to construct Mesh objects from generated vertex and index data
- Generate correct triangle vertex data including CCW winding order, face normals, and UV coordinates
- Explain why a quad needs only 4 vertices with an index buffer rather than 6 separate vertices
- Generate quad and cube vertex and index arrays with correct CCW winding on all faces
- Identify which winding order OpenGL treats as front-facing and why back-face culling depends on it
Concept
Without a factory, every drawable object in a test class requires a hand-written array of floats. A cube alone has 8 vertices each with 14 float fields, 36 indices across 6 faces, and the correct winding order on every face. One mistake renders nothing or produces inside-out geometry. GeometryFactory centralises this bookkeeping behind static methods so a caller only writes GeometryFactory::CreateCube().
The factory separates two concerns. Generate*Vertices() and Generate*Indices() return plain std::vector values that can be inspected or modified before any GPU upload. The Create*() wrapper calls both, constructs a Mesh, and returns a std::unique_ptr<Mesh>. Because Mesh calls SetupMesh() in its constructor, the GPU buffers are ready the moment Create*() returns.
Winding order controls which side of a triangle is front-facing. OpenGL defaults to counter-clockwise (CCW) being the front face. When back-face culling is enabled, clockwise triangles are discarded before the fragment shader runs. Every primitive in this lab uses CCW winding so that faces point outward. Indexed rendering allows the GPU to reference shared vertices by index rather than duplicating their data. A quad built from two triangles needs only 4 unique vertices and 6 indices instead of 6 vertices. The GPU vertex cache can reuse already-transformed vertices, saving both memory and transform work.
Tasks
Task 1 - Declare GeometryFactory and Implement the Create Wrappers
Create src/Mesh/GeometryFactory.h and the top section of src/Mesh/GeometryFactory.cpp. The header declares a class with only static methods. The Create*() methods in the .cpp are thin wrappers that call the corresponding Generate* pair and return a new Mesh. The pattern for every primitive is identical: call Generate*Vertices(), call Generate*Indices(), then return std::make_unique<Mesh>(vertices, indices).
#pragma once
#include "Mesh.h"
#include <vector>
class GeometryFactory {
public:
// Factory methods - return a fully constructed, GPU-ready Mesh
static std::unique_ptr<Mesh> CreateTriangle();
static std::unique_ptr<Mesh> CreateQuad();
static std::unique_ptr<Mesh> CreateCube();
static std::unique_ptr<Mesh> CreateSphere(int sectors = 20, int stacks = 20);
static std::unique_ptr<Mesh> CreateFullscreenQuad();
// Vertex data generation (public so callers can inspect or modify geometry)
static std::vector<Vertex> GenerateTriangleVertices();
static std::vector<Vertex> GenerateQuadVertices();
static std::vector<Vertex> GenerateCubeVertices();
static std::vector<Vertex> GenerateSphereVertices(int sectors = 20, int stacks = 20);
static std::vector<Vertex> GenerateFullscreenQuadVertices();
// Index buffer generation
static std::vector<unsigned int> GenerateTriangleIndices();
static std::vector<unsigned int> GenerateQuadIndices();
static std::vector<unsigned int> GenerateCubeIndices();
static std::vector<unsigned int> GenerateSphereIndices(int sectors = 20, int stacks = 20);
static std::vector<unsigned int> GenerateFullscreenQuadIndices();
private:
static void AssignUVCoordinates(std::vector<Vertex>& vertices, int faceVertexCount);
static void AssignColors(std::vector<Vertex>& vertices);
static void CalculateNormals(std::vector<Vertex>& vertices, const std::vector<unsigned int>& indices);
};
#include "GeometryFactory.h"
#include <cmath>
std::unique_ptr<Mesh> GeometryFactory::CreateTriangle() {
// TODO: call GenerateTriangleVertices() and GenerateTriangleIndices()
// then return make_unique<Mesh>(vertices, indices)
}
std::unique_ptr<Mesh> GeometryFactory::CreateQuad() {
// TODO: same pattern as CreateTriangle but for Quad
}
std::unique_ptr<Mesh> GeometryFactory::CreateCube() {
// TODO: same pattern for Cube
}
std::unique_ptr<Mesh> GeometryFactory::CreateSphere(int sectors, int stacks) {
// TODO: same pattern - pass sectors and stacks through to Generate methods
}
std::unique_ptr<Mesh> GeometryFactory::CreateFullscreenQuad() {
// TODO: same pattern for FullscreenQuad
}
GeometryFactory uses only static methods and holds no member variables. What design pattern does this follow?
Task 2 - Generate Triangle Vertices and Indices
Add GenerateTriangleVertices() and GenerateTriangleIndices() to GeometryFactory.cpp. Both blocks are provided below - copy them in. Study the vertex values carefully: each row is one vertex with position, normal, colour, and UV in that order. Notice all three share the same normal (0, 0, 1) because the triangle is a flat surface facing the viewer along +Z. Then answer the questions below to confirm you understand what the values mean.
std::vector<Vertex> GeometryFactory::GenerateTriangleVertices() {
// Vertex constructor: (posX, posY, posZ, normX, normY, normZ, R, G, B, U, V)
return {
Vertex( 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.5f, 1.0f), // top - Red
Vertex(-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f), // bottom-left - Green
Vertex( 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f), // bottom-right - Blue
};
}
std::vector<unsigned int> GeometryFactory::GenerateTriangleIndices() {
// Three indices, CCW order: vertex 0 (top), 1 (bottom-left), 2 (bottom-right)
return { 0, 1, 2 };
}
OpenGL treats -clockwise winding as front-facing by default.
All three vertices of this flat triangle share the same normal: (0, 0, ), pointing toward the viewer along the positive Z axis.
Task 3 - Generate Quad Vertices and Indices
Add GenerateQuadVertices() and GenerateQuadIndices() to GeometryFactory.cpp. Both blocks are provided below - copy them in. The quad has four corners in the XY plane. Vertex 2 (top-right) appears in both triangle rows of the index list, demonstrating the sharing benefit of indexed rendering. Study the diagram and the comments in the code, then answer the questions to check your understanding.
std::vector<Vertex> GeometryFactory::GenerateQuadVertices() {
// 3 (-0.5, 0.5) ---- 2 (0.5, 0.5)
// | |
// | (0, 0) |
// | |
// 0 (-0.5,-0.5) ---- 1 (0.5,-0.5)
//
// Vertex constructor: (posX, posY, posZ, normX, normY, normZ, R, G, B, U, V)
return {
Vertex(-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f), // 0 bottom-left - Red UV(0,0)
Vertex( 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f), // 1 bottom-right - Green UV(1,0)
Vertex( 0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f), // 2 top-right - Blue UV(1,1)
Vertex(-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f), // 3 top-left - Yellow UV(0,1)
};
}
std::vector<unsigned int> GeometryFactory::GenerateQuadIndices() {
// Triangle 1: 0 (bottom-left), 1 (bottom-right), 2 (top-right) -- CCW
// Triangle 2: 2 (top-right), 3 (top-left), 0 (bottom-left) -- CCW
// Vertex 2 is shared between both triangles (no duplication needed)
return { 0, 1, 2, 2, 3, 0 };
}
A quad drawn without an index buffer requires vertices. Using an index buffer reduces this to unique vertices.
The index list for a quad is { 0, 1, 2, 2, , 0 }.
Task 4 - Understand Cube Geometry and Face Normals
The cube uses 8 shared vertices and 36 indices (6 faces x 2 triangles x 3 indices). The vertex and index arrays are provided below as reference code. Read them carefully, then answer the questions underneath.
Notice that back-face winding appears mirrored relative to the front face. When you view the back face from outside the cube (from -Z), the vertices rotate CCW, but because you are looking from the opposite direction the listed order is reversed compared to the front face.
std::vector<Vertex> GeometryFactory::GenerateCubeVertices() {
//
// 7 -------- 6
// /| /|
// / | / |
// 3 -------- 2 |
// | | | |
// | 4 ------|- 5
// | / | /
// |/ |/
// 0 -------- 1
//
// Front face: Z = +0.5 (vertices 0-3)
// Back face: Z = -0.5 (vertices 4-7)
std::vector<Vertex> vertices = {
// Front face - normal (0, 0, +1)
Vertex(-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f), // 0
Vertex( 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f), // 1
Vertex( 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f), // 2
Vertex(-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f), // 3
// Back face - normal (0, 0, -1)
Vertex(-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f), // 4
Vertex( 0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f), // 5
Vertex( 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.5f, 0.0f, 0.0f, 1.0f), // 6
Vertex(-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.5f, 0.5f, 0.5f, 1.0f, 1.0f), // 7
};
return vertices;
}
std::vector<unsigned int> GeometryFactory::GenerateCubeIndices() {
return {
0, 1, 2, 2, 3, 0, // Front (Z = +0.5) - viewed from +Z
4, 5, 6, 6, 7, 4, // Back (Z = -0.5) - viewed from -Z (mirrored winding)
7, 3, 0, 0, 4, 7, // Left (X = -0.5)
1, 5, 6, 6, 2, 1, // Right (X = +0.5)
4, 0, 1, 1, 5, 4, // Bottom (Y = -0.5)
3, 7, 6, 6, 2, 3, // Top (Y = +0.5)
};
}
The cube has vertices and indices (6 faces x 2 triangles x 3 indices per triangle).
The front face normal is (0, 0, ) and the back face normal is (0, 0, ).
The back face index list is { 4, 5, 6, 6, 7, 4 }. Why does the back face use the opposite winding compared to the front face?
This cube implementation uses 8 shared vertices. A physically correct lighting implementation would use 24 vertices (4 per face). Why?
GeometryFactory also contains GenerateSphereVertices(), which uses spherical coordinates (phi and theta) to position vertices around a unit sphere. Why do sphere normals equal the normalised position vector, but cube normals do not equal the normalised position?
Solution
Try the tasks above before revealing the solution.