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

Lab 14c - GeometryFactory - Procedural Primitive Generation

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:

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).

src/Mesh/GeometryFactory.h
#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);
};
src/Mesh/GeometryFactory.cpp (Create wrappers)
#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?

The static factory method pattern centralises object construction behind named methods. Callers get a fully constructed object without knowing or repeating the construction steps. There is no single instance constraint, so it is not a Singleton.

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.

src/Mesh/GeometryFactory.cpp (triangle vertices - copy this)
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
    };
}
src/Mesh/GeometryFactory.cpp (triangle indices - copy this)
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.

src/Mesh/GeometryFactory.cpp (quad vertices - copy this)
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)
    };
}
src/Mesh/GeometryFactory.cpp (quad indices - copy this)
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.

src/Mesh/GeometryFactory.cpp (cube)
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?

Winding order is determined from the viewer's perspective. When you look at the back face from outside the cube (from -Z), the vertices still rotate counter-clockwise. However because you are facing the opposite direction, the listed index sequence {4,5,6} represents that same CCW rotation - it just looks "reversed" relative to the front face indices when read on paper.

This cube implementation uses 8 shared vertices. A physically correct lighting implementation would use 24 vertices (4 per face). Why?

Each vertex stores a single normal. A corner vertex is shared by three faces, so only one of those face normals can be stored there. The other two faces compute incorrect lighting at that corner. Using 24 vertices (4 per face, no sharing) lets each face assign its own outward-facing normal to all four of its corners.

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?

On a unit sphere centred at the origin, every point on the surface is exactly one radius from the centre, and the outward direction is radially away from the origin. The normalised position vector already points in that radial direction, so it equals the normal. On a cube the faces are flat planes, and the outward normal of a face is perpendicular to that plane, not pointing away from the origin. A corner vertex has a position that points diagonally outward, which does not match the axis-aligned normal of any of the three faces that share it.

Solution

Try the tasks above before revealing the solution.