Raw OpenGL

Alain Galvan
The Startup
Published in
13 min readSep 10, 2019

--

OpenGL is an easy to use low level graphics API used by game developers, web developers through WebGL, data visualization engineers, and more.

OpenGL is currently supported on:

  • 🖼️ Windows (OpenGL 4.6)
  • 🐧 Linux (OpenGL 4.6)
  • 🤖 Android (OpenGL ES 3.1)
  • 🌐 Web (WebGL 2.0)

with Apple MacOS, iOS, and iPad OS supporting it but marking it as depreciated. Don’t let that discourage your from learning it though as there are 3rd party solutions such as MoltenGL that let you program OpenGL code using Apple Metal.

  • 🍎 Mac OS (OpenGL 4.3)
  • 📱 iOS / iPad OS (OpenGL ES 3.0)

And practically any language can use it:

  • C (Official Language)
  • C++ (Either through the C API or unofficial wrappers)
  • Rust (Through Glium)
  • Python (Through PyOpenGL)
  • JavaScript (WebGL)

Its API is designed as a state machine. Developers are responsible for setting the state of the driver to decide exactly how to render what they want.

I’ve prepared a Github Repo with everything we need to get started. We’re going to walk through a Hello Triangle app in modern C++, a program that creates a triangle, processes it with a shader, and displays it on a window.

Setup

First install:

Then type the following in your terminal.

# 🐑 Clone the repo
git clone https://github.com/alaingalvan/opengl-seed --recurse-submodules
# 💿 go inside the folder
cd opengl-seed
# 👯 If you forget to `recurse-submodules` you can always run:
git submodule update --init
# 👷 Make a build folder
mkdir build
cd build
# 🖼️ To build your Visual Studio solution on Windows x64
cmake .. -A x64
# 🍎 To build your XCode project on Mac OS
cmake .. -G Xcode
# 🐧 To build your .make file on Linux
cmake ..
# 🔨 Build on any platform:
cmake --build .

Refer to this blog post on designing C++ libraries and apps for more details on CMake, Git Submodules, etc.

Platform Differences

OpenGL 4.6 is the latest version of the language, but on Mac OS 4.3 is the latest version you can target. In addition, on mobile devices you are required to use OpenGL 3.0 if you want to support both iOS and Android devices, and WebGL 2.0 is only supported on versions of Firefox, Chrome, etc. released after Spring 2017.

Generally, you should target your application according to which platforms you ultimately want to support, and either enable features on a per-platform basis or have all platforms use the same set of features.

Some features such as tessellation or pre-compilation of shaders are only available in newer versions of OpenGL.

Note: The version number of OpenGL ES and WebGL does not correspond with regular OpenGL. OpenGL ES 3.0 is closer to OpenGL 4.0 in features, as is WebGL 2.0.

Project Layout

As your project becomes more complex, you’ll want to separate files and organize your application to something more akin to a game or renderer, check out this post on game engine architecture and this one on real time renderer architecture for more details.

├─ 📂 external/                    # 👶 Dependencies
│ ├─ 📁 crosswindow/ # 🖼️ OS Windows
│ ├─ 📁 crosswindow-graphics/ # 🎨 GL Context Creation
│ ├─ 📁 glm/ # ➕ Linear Algebra
│ ├─ 📁 opengl-registry/ # 📚 GL Headers
│ └─ 📁 glad/ # 🙌 GL Extension Loader
├─ 📂 src/ # 🌟 Source Files
│ ├─ 📄 Utils.h # ⚙️ Utilities (Load Files, Check Shaders, etc.)
│ ├─ 📄 Triangle.h # 🔺 Triangle Draw Code
│ ├─ 📄 Triangle.cpp # -
│ └─ 📄 Main.cpp # 🏁 Application Main
├─ 📄 .gitignore # 👁️ Ignore certain files in git repo
├─ 📄 CMakeLists.txt # 🔨 Build Script
├─ 📄 license.md # ⚖️ Your License (Unlicense)
└─ 📃readme.md # 📖 Read Me!

Dependencies

  • CrossWindow — A cross platform system abstraction library written in C++ for managing windows and performing OS tasks.
  • CrossWindow-Graphics — A library to simplify creating an OpenGL context with CrossWindow.
  • GLM — Allows us to use the same linear algebra data structures as the official shading language of OpenGL, GLSL.
  • OpenGL Registry — OpenGL’s official header files. While operating systems tend to have these headers, it’s easier to reference them from one dependency rather than on a per OS basis.
  • Glad — A configurable C OpenGL header generation tool.

OpenGL features a core API, as well as a number of driver specific extensions to handle things like specific texture formats, debugging, etc. In order to use OpenGL you’ll need gl.h that defines the external calls to the OpenGL driver. This is included by the OpenGL Registry dependency.

In addition, if you need any driver specific extensions, you’ll need an extension loading library, which is where the Glad dependency comes in.

The Khronos Group distributes official extension headers for OpenGL, but there’s no guarantee that you need or can even support all the extensions included there. In addition, depending on your target version you may need to dynamically load certain OpenGL functions like glGenVertexArrays. Glad can generate the headers based on the specification and offers you the option of including or omitting certain extensions. the reason you may want this is that every driver has a unique set of extensions to the core OpenGL API, and every developer might want a unique set of extensions:

We’ll be using Glad, with no extensions, though you’re free to generate your own glad header with whatever extensions you would like.

Overview

Our program will perform the following:

  1. Create a Window for our operating system.
  2. Create a Context to communicate with OpenGL.
  3. Create 2 Vertex Buffers (VBOs), one for positions and one for colors.
  4. Create an Index Buffer (IBO), an array specifying which indices of the vertex buffer you should use to render primitives.
  5. Create a Vertex Array Object (VAO) to describe and bind our vertex layout and buffers to the OpenGL state machine.
  6. Write a Vertex Shader string that transforms your vertices in 3D Space.
  7. Write a Fragment Shader string that colors the pixels that the triangle envelops.
  8. Create a Program that binds the two shaders with glCreateProgram and use it with glUseProgram.
  9. Set Uniforms from our shader to their values with glUniform[type].
  10. Clear the canvas once the next frame needs to render with glClear.
  11. Draw the primitive onto OpenGL’s default Frame Buffer with glDrawElements.
  12. Update the screen and the state of all your objects you want to draw every frame.
  13. Destroy any data structures once the application is asked to close.

The following will explain blocks of code from the github repo:

Window Creation

We’re using CrossWindow to handle cross platform window creation, so creating a window and updating it is very easy:

#include "CrossWindow/CrossWindow.h"
#include "glad/glad.h"
#include "CrossWindow/Graphics.h"
#include <iostream>#include "Triangle.h"void xmain(int argc, const char** argv)
{
// 🖼 Create Window
xwin::WindowDesc wdesc;
wdesc.title = "OpenGL Seed";
wdesc.name = "MainWindow";
wdesc.visible = true;
wdesc.width = 640;
wdesc.height = 640;
wdesc.fullscreen = false;
xwin::Window window;
xwin::EventQueue eventQueue;
if (!window.create(wdesc, eventQueue))
{ return; };
// ⚪ Load OpenGL
xgfx::OpenGLDesc desc;
xgfx::OpenGLState oglState = xgfx::createContext(&window, desc);
xgfx::setContext(oglState);
if (!loadOpenGL())
{ return; }
// 🌟 Create Triangle
if (!createTriangle())
{ return; }
// 🏁 Engine loop
bool isRunning = true;
while (isRunning)
{
bool shouldRender = true;
// ♻️ Update the event queue
eventQueue.update();
// 🎈 Iterate through that queue:
while (!eventQueue.empty())
{
//Update Events
const xwin::Event& event = eventQueue.front();
// ❌ On Close:
if (event.type == xwin::EventType::Close)
{
window.close();
shouldRender = false;
isRunning = false;
}
eventQueue.pop();
}
// ✨ Update Visuals
if (shouldRender)
{
// 🔁 Swap back buffers so we can write to the next frame buffer
xgfx::swapBuffers(oglState);
drawTriangle(wdesc.width, wdesc.height);
}
}
// ❌ Destroy OpenGL Data Structures
destroyTriangle();
xgfx::destroyContext(oglState);
}

Global Setup

There may be some global settings that you may want enabled prior to working with OpenGL, such as what clear color to use, Depth Testing, Cube Map Support, etc.

bool loadOpenGL()
{
if (!gladLoadGL())
{
std::cout << "Failed to load OpenGL.";
return false;
}
GLenum err = glGetError();
if (err != GL_NO_ERROR)
{
std::cout << "OpenGL started with error code: " << err;
return false;
}
// Most OpenGL Apps will want to enable these settings: // 🖍️ Set default clear color to gray ⚫
glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
// 🔺 Enable depth testing
glEnable(GL_DEPTH_TEST);
// 🧊 Enable seamless cubemap sampling
glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
// <= Set Depth Test Function
glDepthFunc(GL_LEQUAL);
// Enable anything else like blend modes, etc... return true;
}

Vertex Buffer Object

A Vertex Buffer Object (VBO) is a block of memory containing vertex data.

You could describe this data with one big buffer containing everything or with independent arrays for each element in your vertex layout, whichever best fits your use case and performance requirements.

Having them split can be easier to update if you’re changing your vertex buffer data often, which may be useful for CPU animations or procedurally generated geometry.

// 📈 Describe Position Vertex Buffer Data
float positions[3*3] = { 1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
0.0f, -1.0f, 0.0f };
// 🎨 Describe Color Vertex Buffer Data
float colors[3*3] = { 1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f };
// ✋ Declare Vertex Buffer Handles
GLuint positionVBO = 0;
GLuint colorVBO = 0;
// ⚪ Create VBO
glGenBuffers(1, &positionVBO);
// 🩹 Bind VBO to GLState
glBindBuffer(GL_ARRAY_BUFFER, positionVBO);
// 💾 Push data to VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 3 * 3, positions, GL_STATIC_DRAW);
// ⚪ Create VBO
glGenBuffers(1, &colorVBO);
// 🩹 Bind VBO to GLState
glBindBuffer(GL_ARRAY_BUFFER, colorVBO);
// 💾 Push data to VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 3 * 3, colors, GL_STATIC_DRAW);

Index Buffer Object

An Index Buffer Object (IBO) is a list of vertex indicies that’s used to make triangles, lines, or points.

If you’re making triangles, there should be 3 points per triangle in the index buffer, for lines there should be 2, and points only need 1.

// 🗄️ Describe Index Buffer Data
GLuint indexBufferData[3] = { 0, 1, 2 };
// ✋ Declare Index Buffer Handle
GLuint ibo;
// ⚪ Create IBO
glGenBuffers(1, &ibo);
// 🩹 Bind IBO to GLState
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
// 💾 Push data to IBO
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint) * 3, indexBufferData, GL_STATIC_DRAW);

Vertex Shader

A Vertex Shader is a GPU program that executes on every vertex of what you’re currently drawing. Often times developers will place code that handles positioning geometry here.

#version 310 eslayout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 0) out vec3 outColor;void main()
{
outColor = inColor;
gl_Position = vec4(inPosition, 1.0);
}

And do the following to create a vertex shader in C++:

// ✋ Declare Vertex Shader Handle
GLuint vs = 0;
// 📂 Load Shader File...// ⚪ Create Vertex Shader
vs = glCreateShader(GL_VERTEX_SHADER);
// 📰 Pass Vertex Shader String
glShaderSource(vs, 1, &vertString, &vertStringSize);
// 🔨 Compile Vertex Shader (and check for errors)
glCompileShader(vs);
// ❔ Check if shader compiled correctly

Fragment Shader

A Fragment Shader executes on every fragment. A fragment is like a pixel, but not limited to just 8 bit RGB, there could be multiple attachments that you’re writing to, in multiple encoded formats.

#version 310 es
precision mediump float;
precision highp int;
layout(location = 0) in highp vec3 inColor;layout(location = 0) out highp vec4 outFragColor;
void main()
{
outFragColor = vec4(inColor, 1.0);
}

And do the following to create a fragment shader in C++:

// ✋ Declare Fragment Shader Handle
GLuint fs = 0;
// 📂 Load Shader File...// ⚪ Create Fragment Shader
fs = glCreateShader(GL_FRAGMENT_SHADER);
// 📰 Pass Fragment Shader String
glShaderSource(fs, 1, &fragString, &fragStringSize);
// 🔨 Compile Vertex Shader (and check for errors)
glCompileShader(fs);
// ❔ Check if shader compiled correctly...

Program

A Shader Program binds the vertex and fragment shaders together and sets them up to be used in the OpenGL state machine.

// ✋ Declare Program Handle
GLuint program = 0;
// ⚪ Create Shader Program
program = glCreateProgram();
// 🩹 Attach Shader Stages to Program
glAttachShader(program, vs);
glAttachShader(program, fs);
// 🔗 Link Program (and check for errors)
glLinkProgram(program);
// ❔ Check if program linked correctly...

Uniforms

Uniforms are variables that you send to your shader program to adjust its output.

All you need to do is declare a uniform in your shader:

uniform vec4 uColor;

And do the following to send it to your shader:

GLfloat color[4] = {
0.5f, 0.5f, 0.5f, 1.0f,
};
// 🔎 Get Uniform Location
GLint uniformLocation = glGetUniformLocation(program, "myUniform");
// 🟢 Assign Data
glUniform4fv(uniformLocation, 1, color);

Alternatively, you can use Uniform Buffer Objects (UBOs) to send data to your shader program, which may be easier if you’re working with other graphics APIs that also do this:

uniform VertUBO
{
// Computed model * view * projection matrices
mat4 modelViewProjection;
// Inverse Transpose Model matrix (for normalized vectors like normals)
mat4 inverseTransposeModel;
} ubo;

And do the following to send that buffer to your shader:

// ✋ Declare UBO/UBI Handles
GLuint vertUbo = 0;
GLuint vertexUbi = 0;
// 🌟 Create UBO
glGenBuffers(1, &vertexUbo);
// 🩹 Bind it to GL State
glBindBuffer(GL_UNIFORM_BUFFER, vertexUbo);
// 📨 Send initial data to UBO
glBufferData(GL_UNIFORM_BUFFER,
sizeof(VertexUBO),
&vertexUboData, GL_STREAM_DRAW);
// 🗺️ Map UBO to block binding
vertexUbi =
glGetUniformBlockIndex(shader.program, "VertUBO");
glUniformBlockBinding(shader.program, vertexUbi, 0);
glUniformBlockBinding(shader.program, vertexUbi, 0);
glBindBufferBase(GL_UNIFORM_BUFFER, 0, vertexUbo);
// 🔁 Later, to update UBO
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// 🗺️ Map Uniform Buffer to CPU accessible memory
GLvoid* p =
glMapBufferRange(GL_UNIFORM_BUFFER, 0,
sizeof(VertexUBO),
GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT);
// 📄 Copy Data to buffer
memcpy(p, &vertexUboData, sizeof(vertexUboData));
// 😁 Unbind the mapping
glUnmapBuffer(GL_UNIFORM_BUFFER);

Textures

Textures are image data structures that you can use as inputs to a shader or as frame buffer attachments.

// ✋ Declare GLTexture Handle
GLuint tAlbedo = 0;
// 🎂 Our Texture's Size
unsigned width = 1;
unsigned height = 1;
// ⚪🔵🟢🔴 ABGR
unsigned data[1] = {0xFFFFFFFF};
// 🌟Create Texture
glGenTextures(1, &tAlbedo);
// 🌳 Set the current active Texture ID
glActiveTexture(GL_TEXTURE0);
// 🟢 Set texture object to GL Texture ID
glBindTexture(GL_TEXTURE_2D, tAlbedo);
// 💾 Assign data to texture
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA,
GL_UNSIGNED_BYTE, data);
// 🔁 Sampler Behavior (Linear vs. Nearest Neighbor, Repeat vs. Clamp)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

And once they’re created, you can send access them in your shader like so:

uniform sampler2D tAlbedo;

And do the following to send it to your shader:

// ✋ Declare Uniform Location Handle
int fragAlbedoUniform = -1;
// 🔎 Get Uniform Location
fragAlbedoUniform =
glGetUniformLocation(shader.program, "tAlbedo");
// 🌳 Set the current active Texture ID
glActiveTexture(GL_TEXTURE0);
// 🟢 Set texture object to GL Texture ID
glBindTexture(GL_TEXTURE_2D, tAlbedo);
// Set a uniform int as the current texture ID being referenced
glUniform1i(fragAlbedoUniform, 0);

As for loading textures from files, we’ll discuss that in a later post.

Vertex Array Object

A Vertex Array Object (VAO) is a method of grouping together different vertex buffers such that they can all be bound as a group.

// ✋ Declare Vertex Array Object Handle
GLuint vao = 0;
glGenVertexArrays(1, &vao);// 💕 Bind Vertex Array Object
glBindVertexArray(vao);
// 🔺 Bind positionVBO to VAO
glBindBuffer(GL_ARRAY_BUFFER, positionVBO);
// 🔎 Get position of attribute from shader program
GLint positionAttrib = glGetAttribLocation(program, "inPosition");
// 🩹 Bind positionVBO to VAO with the index of positionAttrib (0)
glEnableVertexAttribArray(positionAttrib);
// 💬 Describe data layout of VBO
glVertexAttribPointer(positionAttrib, 3, GL_FLOAT, GL_FALSE,
sizeof(float) * 3, (void*)0);
// 🎨 Bind colorVBO to VAO
glBindBuffer(GL_ARRAY_BUFFER, colorVBO);
// 🔎 Get position of attribute from shader program
GLint colorAttrib = glGetAttribLocation(program, "inColor");
// 🩹 Bind positionVBO to VAO with the index of colorAttrib (1)
glEnableVertexAttribArray(colorAttrib);
// 💬 Describe data layout of VBO
glVertexAttribPointer(colorAttrib, 3, GL_FLOAT, GL_FALSE,
sizeof(float) * 3, (void*)0);
// 🗄️ Bind IBO to VAO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);

Drawing

Rasterization Animation

To draw, call either glDrawArrays to draw based off a specified pattern of how your vertex buffer is organized, or glDrawElements if you're using an index buffer (you'll tend to always want to do this).

void drawTriangle(unsigned width, unsigned height)
{
// 🖼️ Set the Viewport size to where you'll be drawing
glViewport(0, 0, width, height);
// 🧪 Clear
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 🎨 Bind Shader
glUseProgram(program);
// 🩹 Bind VAO
glBindVertexArray(vao);
// 🛆 Draw
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);
}

Destroying GL Handles

Once you’ve finished with an OpenGL handle, either because you’ve switched scenes, or the application is shutting down, it’s important to destroy it so as to not leak memory:

void destroyTriangle()
{
glDeleteBuffers(1, &positionVBO);
glDeleteBuffers(1, &colorVBO);
glDeleteBuffers(1, &ibo);
glDeleteShader(vs);
glDeleteShader(fs);
glDeleteProgram(program);
glDeleteVertexArrays(1, &vao);
}

Conclusion

Rendering in OpenGL is incredibly easy, just initialize your buffers, bind those buffers to a VAO, bind that VAO to the GL state, bind a Shader Program to the GL State, and glDrawElements.

This is the raster based method of rendering triangles, but there’s a number of other aspects of the OpenGL API I omitted, such as:

  • Frame Buffer Creation
  • Compute Shaders
  • Transform Feedback
  • Cube Maps
  • glDrawArrays options
  • Blend Modes

With other aspects of an OpenGL app only present in the sample code:

  • Context Creation
  • Swapping Back buffers
  • Window Creation
  • Destroying Data Structures

Not to mention aspects of software engineering such as project organization, OS window management, game engine architecture, real time renderer architecture, etc. You can look forward to that and much more in future blog posts and e-books.

You’ll find all the source code described in this post in the Github Repo here.

--

--

Alain Galvan
The Startup

https://Alain.xyz | Graphics Software Engineer @ AMD, Previously @ Marmoset.co. Guest lecturer talking about 🛆 Computer Graphics, ✍ tech author.