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
- Lab 01 complete: an 800x600 GLFW window opens, the render loop runs at 60 fps, and the console prints the OpenGL version.
- GLFW, GLEW, and GLM are linked in the project via vcpkg.
src/main.cppcontains a working render loop from the Window and Context lab (Lab 01).
Learning Objectives
By the end of this lab you will be able to:
- Explain why vertex data must be uploaded to GPU memory before the GPU can draw it.
- Create a Vertex Buffer Object and transfer float data to the GPU using
glBufferData. - Create a Vertex Array Object and record a vertex layout using
glVertexAttribPointer. - Issue a draw call with
glDrawArraysand understand the primitive type and vertex count arguments. - Describe the role of OpenGL's fixed-function pipeline and how it differs from a custom GLSL shader.
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.
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.
- Locate the line:
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - 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.
- Declare
unsigned int vao. - Call
glGenVertexArrays(1, &vao)to generate one VAO and store its ID. - 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.
- Declare
unsigned int vbo. - Call
glGenBuffers(1, &vbo)to generate one buffer and store its ID. - Call
glBindBuffer(GL_ARRAY_BUFFER, vbo)to select it as the active array buffer. -
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.
-
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
- Index
- Call
glEnableVertexAttribArray(0)to activate attribute slot 0. - 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.
- Call
glBindVertexArray(vao)to restore the vertex layout. -
Call
glDrawArrays(GL_TRIANGLES, 0, 3).GL_TRIANGLES- assemble every 3 vertices into one triangle0- start from vertex index 03- 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.