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

Lab 02 - First Triangle

Overview

In the Window and Context lab (Lab 01) you built a render loop that clears the screen each frame. This lab focuses on putting something on that screen for the first time. You will upload triangle geometry from CPU RAM to the GPU using a Vertex Buffer Object (VBO), record how the GPU should interpret it using a Vertex Array Object (VAO), and issue your first draw call with glDrawArrays.

This lab uses OpenGL's built-in fixed-function pipeline so you can concentrate entirely on the geometry upload process. No custom shaders are written here. The Shaders lab (Lab 03) will replace the hardware defaults with your own GLSL programs. The VAO and VBO pattern you establish in this lab is the foundation that every subsequent lab builds on.

Prerequisites

Learning Objectives

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

Concept

The GPU is a separate processor with its own dedicated memory. Your triangle coordinates exist only in CPU RAM. Before any rendering can happen, that data must cross the system bus into GPU VRAM. A Vertex Buffer Object (VBO) is an OpenGL-managed block of GPU memory that holds raw bytes. You create one with glGenBuffers, select it as the active buffer with glBindBuffer(GL_ARRAY_BUFFER, vbo), then copy your vertex array into it with glBufferData. The GL_STATIC_DRAW hint tells the driver the data will not change often, so it can place the buffer in fast GPU-local memory.

Raw bytes in a buffer have no inherent meaning. Nine floats could be three RGB colours, three XYZ positions, or something else entirely. A Vertex Array Object (VAO) stores the interpretation rules. While a VAO is bound, each call to glVertexAttribPointer records how one attribute should be read from the currently-bound VBO: how many components it has, what numeric type they are, and how many bytes apart consecutive vertices are (the stride). Calling glEnableVertexAttribArray activates the slot. Binding the VAO again later restores all these rules in a single call.

When the draw call glDrawArrays(GL_TRIANGLES, 0, 3) runs, the GPU reads three vertices from the bound VAO and assembles them into a triangle. For this to produce pixels, OpenGL needs a program to run on each vertex and each covered pixel. Writing that program in GLSL is the subject of Lab 03. For now, this lab uses OpenGL's Compatibility Profile, which includes a built-in fixed-function pipeline. When no custom shader program is bound, the GPU falls back to hardware defaults that colour geometry solid white and apply no transformation. This lets you learn the geometry upload process in isolation, without any shader code.

Note - Hardware Fixed-Function Pipeline. The triangle in this lab renders white because OpenGL's built-in fixed-function pipeline assigns a default white colour when no shader is active. This is intentional. The Shaders lab (Lab 03) will replace this behaviour with a GLSL fragment shader that lets you choose any colour. At that point the project also switches back to the Core Profile, which requires custom shaders and disables the fixed-function defaults entirely.

Tasks

Task 1 - Switch to Compatibility Profile

The Core Profile from Lab 01 requires a custom shader program to render anything. For this lab, change the profile hint so OpenGL's fixed-function pipeline is available. Open src/main.cpp and find the three glfwWindowHint calls you wrote in Lab 01. Change GLFW_OPENGL_CORE_PROFILE to GLFW_OPENGL_COMPAT_PROFILE.

  1. Locate the line: glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  2. Replace it with the line below.
// file: src/main.cpp -- update the existing profile hint
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);

Task 2 - Add the Triangle Vertex Data

Copy the vertex array below into src/main.cpp, after the GLEW initialisation code and before the render loop. The coordinates are in Normalised Device Coordinates (NDC): the visible window area maps to the range -1.0 to +1.0 on both axes, with the origin at the centre of the window. No coordinate transformation is needed because the fixed-function pipeline uses an identity matrix by default.

// file: src/main.cpp -- add after GLEW init, before the render loop
float vertices[] = {
     0.0f,  0.5f, 0.0f,   // top centre
    -0.5f, -0.5f, 0.0f,   // bottom left
     0.5f, -0.5f, 0.0f    // bottom right
};

Task 3 - Create a Vertex Array Object

A VAO must be created and bound before any buffer or layout calls are made. Everything you set up in Tasks 4 and 5 will be recorded into this VAO.

  1. Declare unsigned int vao.
  2. Call glGenVertexArrays(1, &vao) to generate one VAO and store its ID.
  3. Call glBindVertexArray(vao) to make it the active VAO.
// TODO: declare unsigned int vao
// TODO: generate one vertex array object and store its ID in vao
// TODO: bind the VAO

Task 4 - Upload Vertex Data to a VBO

With the VAO bound, create a buffer object and copy the vertex array to the GPU. The VBO must be bound while the VAO is active so the association is recorded into the VAO.

  1. Declare unsigned int vbo.
  2. Call glGenBuffers(1, &vbo) to generate one buffer and store its ID.
  3. Call glBindBuffer(GL_ARRAY_BUFFER, vbo) to select it as the active array buffer.
  4. Call glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW) to transfer the vertex array into GPU memory.
// TODO: declare unsigned int vbo
// TODO: generate one buffer object and store its ID in vbo
// TODO: bind it as GL_ARRAY_BUFFER
// TODO: upload the vertex data with GL_STATIC_DRAW

Task 5 - Describe the Vertex Layout

Tell the GPU how to interpret the bytes in the VBO. Each vertex is three tightly packed floats (x, y, z), so the stride is 3 * sizeof(float) and the offset is zero. In Compatibility Profile, generic attribute slot 0 is aliased to the vertex position used by the fixed-function pipeline, so no shader is needed to make this work.

  1. Call glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0).
    • Index 0 - attribute slot (position)
    • Size 3 - three components per vertex (x, y, z)
    • Type GL_FLOAT
    • Normalised GL_FALSE - values are already in NDC, do not rescale
    • Stride 3 * sizeof(float) - 12 bytes between consecutive vertices
    • Offset (void*)0 - position data starts at byte 0
  2. Call glEnableVertexAttribArray(0) to activate attribute slot 0.
  3. Call glBindVertexArray(0) to unbind the VAO now that setup is complete.
// TODO: call glVertexAttribPointer with index 0, size 3, type GL_FLOAT,
//       normalised GL_FALSE, stride 3*sizeof(float), offset (void*)0
// TODO: enable attribute slot 0
// TODO: unbind the VAO

Task 6 - Issue the Draw Call

Inside the render loop, after glClear and before glfwSwapBuffers, bind the VAO and draw the triangle. There is no glUseProgram call here. The absence of a shader program is intentional: the fixed-function pipeline takes over automatically.

  1. Call glBindVertexArray(vao) to restore the vertex layout.
  2. Call glDrawArrays(GL_TRIANGLES, 0, 3).
    • GL_TRIANGLES - assemble every 3 vertices into one triangle
    • 0 - start from vertex index 0
    • 3 - consume 3 vertices in total
// TODO: bind the VAO
// TODO: call glDrawArrays(GL_TRIANGLES, 0, 3)

After the render loop exits, add cleanup before the existing glfwDestroyWindow call:

// file: src/main.cpp -- after the render loop -- provided for reference
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(1, &vbo);

Expected Result

A solid white triangle appears in the centre of the 800x600 window against the dark blue-grey background set by glClearColor. The triangle has its apex at the top and a flat base at the bottom. It does not move or flicker.

If the window is blank, check two things: that glBindVertexArray(vao) appears inside the render loop (not just during setup), and that the profile hint was changed to GLFW_OPENGL_COMPAT_PROFILE in Task 1.