Hello Triangle

A quick start guide to modern OpenGL.

Dec 30, 2024
4 min read
opengl
graphics

OpenGL requires a window, loader, and math library. We will use the following dependencies for our project:

  • glad (loader)
  • SDL3 (window)
  • glm (math)

At least for this guide, our only interest is in OpenGL, so the dependencies have been abstracted away. When you build the repository, the only file you need to code on is the draw.hpp file.

Prerequisites

git clone https://gitea.com/brandonchui/ogl_basics.git
cd ogl_basics

There are 2 build scripts, depending on your operating system.

Windows:

.\build.bat

Mac:

chmod +x build.sh
./build.sh

Beginning

We start from our draw.hpp file as mentioned earlier. You should see the following:

//draw.hpp
#pragma once
#include <glad/glad.h>
#include <glm/glm.hpp>

//globals
// ???

//functions
inline void DrawTriangle()
{
    //TODO
}

By the end of this guide, we will be able to draw a triangle with about 50 lines of code filling in only the DrawTriangle() function.

Making sure OpenGL works

Let's test that we can ask OpenGL to clear its screen and color in our window

inline void DrawTriangle()
{
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //light green
    glClear(GL_COLOR_BUFFER_BIT);
}

Creating the VBO (Vertex Buffer Object)

We use the VBO to store vertices into the GPU memory. We have defined our triangle as:

//globals
float vertices[] = {-0.5f, -0.5f, 0.0f, 
                     0.5f, -0.5f, 0.0f, 
                     0.0f,  0.5f, 0.0f};

With our vertices, we define our vbo, which will just a type int. This is the buffer id for our object.

int vbo;
glGenBuffers(1, &vbo);

Then we need to bind that buffer, in other words, we are telling OpenGL that we will not select that buffer to being worked on.

int vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);

cat avatar left
Wait, what is GL_ARRAY_BUFFER ?

cat avatar right
It just means we are stating our intent to use that buffer object for vertex attribute data.

We now need a way to copy our defined vertices into the buffer's memory, which the glBufferData comes in.

int vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

Checkpoint

Creating the shader

Ideally, this will be a .glsl file, but for now we can create this as a char*. We would need a string importer if we were to use the .glsl method.

const char *vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\0";

Now, we create our shader with the id again:

int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

Not done quite yet with shaders - we still have to attach the shader solurce to the object, and then compile it.

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

Fragment Shaders

You can think of the fragment shader as the color representation of a given pixel on screen. We will define it as a string-like type again:

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 

The exact steps from the previous section will be performed:

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

Shader Program

We have our compiled shaders, but they still need to go somewhere. In comes the shader program, where the shaders will link to and then activated.

int shaderProgram;
shaderProgram = glCreateProgram();

With our shaderProgram ready to go, we just simply attach whatever shaders we have made previous:

int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

To use our newly created shaders, we just call the glUseProgram function:

glUseProgram(shaderProgram);

To tidy things up, since we compiled our shader, we don't need the leftovers, so we can safely delete it:

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);  

Creating the VAO (Vertex Array Object)

This will be similar to our vbo:

int VAO;
glGenVertexArrays(1, &VAO);  

We then bind it so we can select it

int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);

Now we bind the buffer:

int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

We then need to set your vbo pointers

int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);  

Our Triangle

Finally, in our rendering loop we call our draw function on each frame:

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);