Set Up OpenGL Dev Environment

2025-06-06, Fri

It might sound a bit strange to set up OpenGL development environment these days, since the whole standard is on the way to deprecation: the last stable release of OpenGL was publised in 2017, with its successor Vulkan having a new release every once in a while. OpenGL, along with its accompanying siblings OpenGL ES and WebGL, are all things of the past1 (with only OpenCL spared). I guess the only rationale for still spending time on OpenGL is to read old textbooks and source code, which is indeed on my list.

1. What Is Needed

Several things are needed to write OpenGL programs:

  • Compiler, e.g. GCC or Clang, along with handy tools like GNU Make.
  • OpenGL implementation, e.g. Mesa
  • Window management library, e.g. GLFW, GLUT, SDL, etc.
  • Function loading library2, e.g. Glad3, GLEW4, etc.

To learn OpenGl, refer to materials enlisted on the GLFW site5.

2. How To Get Them

So in which OS shall we talk about this topic? Things are easy in GNU/Linux, so long as we know what we're looking for: build-essential for compiler, mesa for OpenGL implementation, along with window management libraries like libglfw3, freeglut3-dev, libsdl2-dev, etc. Whenever things are absent, compile from source is always a viable and smooth option.

macOS is not much different from GNU/Linx if we want to compile from source. Another option is to grab pre-built binaries through Homebrew, e.g. brew install mesa6. Note that GLEW is available through brew, but Glad is not.

As for Windows, especially managed Windows, we need to figure out a workaround.

  • For compiler, the easier option Cygwin requires admin privilege for installation. Although there is WinLibs7 that doesn't have such constraint. Meanwhile, GNU Make binaries for Windows is available through the GnuWin328 project.
  • Prebuilt binaries for Mesa could be found on GitHub by pal1000 and mmozeilo9, 10.
  • GLFW already provides pre-compiled binaries on official site11, GLUT and FreeGLUT also provide the same option12, 13, along with SDL14 too.
  • As for Glad3 and GLEW4, both provide generation or download link on their official sites.

3. Put Things To Test

To check whether things work, we could try to compile and run examples provided in the glfw source code. Other than utilizing CMake, we could also draft a plain old Makefile:

CC=gcc
SRC_DIR=src
OUT_DIR=out

MESA_DIR=/opt/homebrew/opt/mesa/
MESA_INC=${MESA_DIR}/include
MESA_LIB=${MESA_DIR}/lib

GLFW_DIR=/opt/homebrew/opt/glfw/
GLFW_INC=${GLFW_DIR}/include
GLFW_LIB=${GLFW_DIR}/lib

GLFW_SRC_DEP=~/Programs/glfw.org/glfw-3.4.src/deps

INC_OPTS=-I ${MESA_INC} -I ${GLFW_INC} -I ${GLFW_SRC_DEP}
LIB_OPTS=-L ${MESA_LIB} -L ${GLFW_LIB} -lGL -lglfw

all: boing gears heightmap offscreen particles sharing splitview triangle-opengl triangle-opengles wave windows

boing: ${SRC_DIR}/boing.c
        ${CC} -o ${OUT_DIR}/$@ ${INC_OPTS} ${LIB_OPTS} $^
gears: ${SRC_DIR}/gears.c
        ${CC} -o ${OUT_DIR}/$@ ${INC_OPTS} ${LIB_OPTS} $^
heightmap: ${SRC_DIR}/heightmap.c
        ${CC} -o ${OUT_DIR}/$@ ${INC_OPTS} ${LIB_OPTS} $^
offscreen: ${SRC_DIR}/offscreen.c
        ${CC} -o ${OUT_DIR}/$@ ${INC_OPTS} ${LIB_OPTS} $^
particles: ${SRC_DIR}/particles.c  ${GLFW_SRC_DEP}/tinycthread.c ${GLFW_SRC_DEP}/getopt.c
        ${CC} -o ${OUT_DIR}/$@ ${INC_OPTS} ${LIB_OPTS} $^
sharing: ${SRC_DIR}/sharing.c
        ${CC} -o ${OUT_DIR}/$@ ${INC_OPTS} ${LIB_OPTS} $^
splitview: ${SRC_DIR}/splitview.c
        ${CC} -o ${OUT_DIR}/$@ ${INC_OPTS} ${LIB_OPTS} $^
triangle: ${SRC_DIR}/triangle.c
        ${CC} -o ${OUT_DIR}/$@ ${INC_OPTS} ${LIB_OPTS} $^
triangle-opengl: ${SRC_DIR}/triangle-opengl.c
        # version 330
        ${CC} -o ${OUT_DIR}/$@ ${INC_OPTS} ${LIB_OPTS} $^
triangle-opengles: ${SRC_DIR}/triangle-opengles.c
        # version 100
        ${CC} -o ${OUT_DIR}/$@ ${INC_OPTS} ${LIB_OPTS} $^
wave: ${SRC_DIR}/wave.c
        ${CC} -o ${OUT_DIR}/$@ ${INC_OPTS} ${LIB_OPTS} $^
windows: ${SRC_DIR}/windows.c
        ${CC} -o ${OUT_DIR}/$@ ${INC_OPTS} ${LIB_OPTS} $^

Put examples into src/ and run make, compiled binaries could be found in out/. Two render results are listed here for reference.

gears_300x300.jpg

Figure 1: gears.c rendering result

triangle_300x300.jpg

Figure 2: triangle-opengl.c rendering result

3.1. Caveats:

Regarding glad: glad/gl.h that comes with ${GLFW_SRC_DEP} includes content of both header and implementation – which means it's fine to include it once, but not so if we're trying to incude it into multiple source files. To do that, we should either

  • break the content of ${GLFW_SRC_DEP}/glad/gl.h into two files: one with only declarations and another with implementation (hint: search /* Source */ in the file), or
  • use the downloaded version from glad official site3

For macOS:

  • -lglfw works, but not for -lglfw3 with Homebrew Mesa
  • triangle-opengl.c relies on OpenGL ES 330, and it works fine. Meanwhile, triangle-opengles.c relies on OpenGL ES 100, which doesn't work on macOS

As for Windows:

  • glfw binaries include multiple versions. The one I used is lib-mingw-w64, which includes glfw3.dll and libglfw3.a, etc. Only one of these two is needed, e.g. glfw3.dll, and compiler would complain if both of them are present
  • the library options become -lglfw3 -lopengl32
  • In order for executable to run correctly, the system needs able to locate those .dll files. There are multiple ways to achieve this, one of which is to add /path/to/mesa/lib and /path/to/glfw/lib into the PATH list

And that's it for now.

4. Examples

More examples are listed below for reference.

4.1. A Simple Triangle (Or Not)

You might think drawing a triangle is another "Hello World" program, yet somehow OpenGL's version is still non-trivial (skip below source code if it gives you headache):

// This source code borrows from examples provided by GLFW example
// triangle-open.c.

// Original design and implementation:
// triangle-opengl.c by Camilla Löwy <[email protected]>, and
// linmath.h by Camilla Löwy <[email protected]> again

#include <stdio.h>
#include <stdlib.h>

// <stddef.h> provides offsetof(3)
#include <stddef.h> 

#include <glad/glad.h>
#include <GLFW/glfw3.h>

// data types from "linmath.h"
typedef float vec2[2];
typedef float vec3[3];
typedef float vec4[4];
typedef vec4 mat4x4[4];


typedef struct Vertex
{
    vec2 pos;
    vec3 col;
} Vertex;

static const Vertex vertices[3] =
{
    { { -0.6f, -0.4f }, { 1.f, 0.f, 0.f } },
    { {  0.6f, -0.4f }, { 0.f, 1.f, 0.f } },
    { {   0.f,  0.6f }, { 0.f, 0.f, 1.f } }
};

void mat4x4_identity(mat4x4 m) {
  for (int i = 0; i < 4; ++i)
    for (int j = 0 ; j < 4; ++j)
      m[i][j] = i == j? 1.0f : 0.0f;
}


static void error_callback(int error, const char* description)
{
    fprintf(stderr, "Error: %s\n", description);
}



static const char* vertex_shader_text =
"#version 330\n"
"uniform mat4 MVP;\n"
"in vec3 vCol;\n"
"in vec2 vPos;\n"
"out vec3 color;\n"
"void main()\n"
"{\n"
"    gl_Position = MVP * vec4(vPos, 0.0, 1.0);\n"
"    color = vCol;\n"
"}\n";

static const char* fragment_shader_text =
"#version 330\n"
"in vec3 color;\n"
"out vec4 fragment;\n"
"void main()\n"
"{\n"
"    fragment = vec4(color, 1.0);\n"
"}\n";

int
main(void)
{
  glfwSetErrorCallback(error_callback);

  if(!glfwInit()) 
    exit(EXIT_FAILURE);


  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

  GLFWwindow* window = glfwCreateWindow(640, 480, "OpenGL Triangle", NULL, NULL);
  if (!window)
    {
      glfwTerminate();
      exit(EXIT_FAILURE);
    }

  glfwMakeContextCurrent(window);
  gladLoadGLLoader((GLADloadproc) glfwGetProcAddress);
  glfwSwapInterval(1);

  GLuint vertex_buffer;
  glGenBuffers(1, &vertex_buffer);
  glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

  const GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vertex_shader, 1, &vertex_shader_text, NULL);
  glCompileShader(vertex_shader);

  const GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(fragment_shader, 1, &fragment_shader_text, NULL);
  glCompileShader(fragment_shader);

  const GLuint program = glCreateProgram();
  glAttachShader(program, vertex_shader);
  glAttachShader(program, fragment_shader);
  glLinkProgram(program);

  const GLint mvp_location = glGetUniformLocation(program, "MVP");
  const GLint vpos_location = glGetAttribLocation(program, "vPos");
  const GLint vcol_location = glGetAttribLocation(program, "vCol");

  GLuint vertex_array;
  glGenVertexArrays(1, &vertex_array);
  glBindVertexArray(vertex_array);
  glEnableVertexAttribArray(vpos_location);
  glVertexAttribPointer(vpos_location, 2, GL_FLOAT, GL_FALSE,
                        sizeof(Vertex), (void*) offsetof(Vertex, pos));
  glEnableVertexAttribArray(vcol_location);
  glVertexAttribPointer(vcol_location, 3, GL_FLOAT, GL_FALSE,
                        sizeof(Vertex), (void*) offsetof(Vertex, col));


    while (!glfwWindowShouldClose(window))
      {
        int width, height;
        glfwGetFramebufferSize(window, &width, &height);
        const float ratio = width / (float) height;

        glViewport(0, 0, width, height);
        glClear(GL_COLOR_BUFFER_BIT);

        // mat4x4 m, p, mvp;
        // mat4x4_identity(m);
        // mat4x4_rotate_Z(m, m, (float) glfwGetTime()); // rotate the triangle
        // mat4x4_ortho(p, -ratio, ratio, -1.f, 1.f, 1.f, -1.f);
        // mat4x4_mul(mvp, p, m);
        mat4x4 mvp;
        mat4x4_identity(mvp);

        glUseProgram(program);
        glUniformMatrix4fv(mvp_location, 1, GL_FALSE, (const GLfloat*) &mvp);
        glBindVertexArray(vertex_array);
        glDrawArrays(GL_TRIANGLES, 0, 3);

        glfwSwapBuffers(window);
        glfwPollEvents();
      }

    glfwDestroyWindow(window);

    glfwTerminate();
    exit(EXIT_SUCCESS);

}

The rendering result looks like this:

triangle-02_300x300.jpg

Figure 3: A Static Triangle

Now let's break up things piece by piece to see how they fit together into a picture (no pun intended).

4.2. Interlude: Utilities

Modern OpenGL works around shaders. In order to load and process shader programs effectively, utilities will be introduced from here, starting with file-utils and load-shaders.

file-utils.c introduces methods to load content of a file as string:

// declared in file-utils.h and defined in file-utils.c
char *read_file_as_str(char *file_name) {
  if (NULL == file_name) {
    return NULL;
  }

  FILE *file = fopen(file_name, READ_MODE);
  if (NULL == file) { fprintf(stderr, "Error opening file %s\n", file_name);
    return NULL;
  }

  fseek(file, 0, SEEK_END);
  long file_size = ftell(file);
  fseek(file, 0, SEEK_SET);

  //
  char *buffer = malloc(file_size + 1);
  if (NULL == buffer) { fprintf(stderr, "Error allocating memory"); return NULL; }

  fread(buffer, /* sizef(char) */ 1, file_size, file);
  buffer[file_size] = '\0';
  fclose(file);

  return buffer;
}

The next load-shaders.c introduces function to, well, load shader programs. Specifically, following methods are used to provide a full suite:

  • glCreateProgram
  • glCreateShader, glDeleteShader
  • glShaderSource
  • glCompileShader
  • glAttachShader,
  • glLinkProgram
  • glGetShaderiv, glGetShaderInfoLog, glGetProgramiv, glGetProgramInfoLog
// Reference: Book "OpenGL Programming Guide" source code: LoadShaders.c
// load-shader.h
typedef struct {
  GLenum type; /* GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, and GL_NONE */
  const char *filename;
  GLuint shader;
} ShaderInfo;
GLuint load_shaders(ShaderInfo* shaders);

// load-shader.c
static const int STR_COUNT = 1;
static const int INVALID_PROGRAM = 0;
GLuint load_shaders(ShaderInfo* shaders) {

  if (NULL == shaders)
    return INVALID_PROGRAM;

  GLuint program = glCreateProgram();

  ShaderInfo* entry = shaders;
  while(entry->type != GL_NONE) {
    GLuint shader = glCreateShader(entry->type);

    entry->shader = shader;

    const GLchar *source = read_file_as_str((char*)entry->filename);
    if (NULL == source) {
      for (entry = shaders; entry->type != GL_NONE; ++entry) {
        glDeleteShader(entry->shader);
        entry->shader = 0;
      }

      return INVALID_PROGRAM;
    }

    glShaderSource(shader, STR_COUNT, &source, NULL);
    glCompileShader(shader);
    free((char*)source);

    GLint compiled;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);

    if (!compiled) {
      GLsizei len;
      glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);

      GLchar *log = malloc((len + 1) * sizeof(GLchar));
      glGetShaderInfoLog(shader, len, &len, log);
      fprintf(stderr, "%s Shader Compilation failed: %s\n", entry->filename, log);
      free(log);
      return INVALID_PROGRAM;
    }

    glAttachShader(program, shader);

    ++entry;
  }

  glLinkProgram(program);

  GLint linked;
  glGetProgramiv(program, GL_LINK_STATUS, &linked);

  if (!linked) {
    GLsizei len;
    glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len);

    GLchar *log = malloc((len+1) * sizeof(GLchar));
    glGetProgramInfoLog(program, len, &len, log);
    fprintf(stderr, "Shader linking failed: %s\n", log);
    free(log);
  }

  /* Delete shader no matter link success or failure */
  for (entry = shaders; entry->type != GL_NONE; ++entry) {
    glDeleteShader(entry->shader);
    entry->shader = 0;
  }

  if (!linked) return INVALID_PROGRAM;

  return program;
}

The last utility is common-gl.c, which introduces function that provides GLFW window skeleton to eliminate boilerplate:

// common-gl.h
#include <glad/glad.h>
#include <GLFW/glfw3.h>

typedef struct InitConfig {
  char *title;
  int width;
  int height;
} InitConfig;

int show_glfw_window(const InitConfig *config,
                     void (*on_int)(GLFWwindow*),
                     void (*on_paint)(void));

// common-gl.c
#include <stdlib.h> /* EXIT_SUCCESS and EXIT_FAILURE */
#include <stdio.h>
#include "../include/common-gl.h"

static void err_callback(int error, const char *description) {
  fprintf(stderr, "Error: %s\n", description);
}

static void key_callback(GLFWwindow *window, int key,
                              int scancode, int action, int mods) {
  if ( GLFW_KEY_ESCAPE == key && GLFW_PRESS == action) {
    glfwSetWindowShouldClose(window, GLFW_TRUE);
  }
}

/*
  Simplify the GLFW window set up process
*/
int
show_glfw_window(const InitConfig  *config,
                 void (*on_init)(GLFWwindow*),
                 void (*on_paint)(void)) {

  glfwSetErrorCallback(err_callback);

  if (!glfwInit()) {
    return (EXIT_FAILURE);
  }

  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

  int width = config->width > 0 ? config->width : 640;
  int height = config->height > 0? config->height : 480;

  GLFWwindow *window = glfwCreateWindow(width, height, config->title, NULL, NULL);
  if (!window) {
    glfwTerminate();
    return (EXIT_FAILURE);
  }

  glfwSetKeyCallback(window, key_callback);

  glfwMakeContextCurrent(window);
  gladLoadGLLoader((GLADloadproc) glfwGetProcAddress);
  glfwSwapInterval(1);

  if (on_init) on_init(window);

  while(!glfwWindowShouldClose(window)) {
    int width, height;
    glfwGetFramebufferSize(window, &width, &height);
    const float ratio = width / (float) height;

    glViewport(0, 0, width, height);
    glClear(GL_COLOR_BUFFER_BIT);

    if (on_paint) on_paint();

    glfwSwapBuffers(window);
    glfwPollEvents();
  }

  glfwDestroyWindow(window);
  glfwTerminate();

  return EXIT_SUCCESS;
}

With utilities available now, it's time to revise the triangle example.

4.3. Triangle Revisited

The first step is to put shader program into separate files like triangle.vert and triangle.frag, as shown below:

// Reference: Example from https://learnopengl.com
// shader/triangle.vert
#version 330 core
layout (location = 0) in vec3 aPos;

void main() {
  gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0f);
}
// shader/triangle.frag
#version 330 core
out vec4 FragColor;

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

Now comes the new version of triangle.c:

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

#include "include/common-gl.h"
#include "include/load-shaders.h"

static GLuint shader_program;

ShaderInfo shaders[] = {
  {
    .type = GL_VERTEX_SHADER,
    .filename = "src/shader/triangle.vert"
  },
  {
    .type = GL_FRAGMENT_SHADER,
    .filename = "src/shader/triangle.frag"
  },
  {
    .type = GL_NONE
  }
};


static float vertices[] = {
  -0.5f, -0.5f, 0.0f,
  +0.5f, -0.5f, 0.0f,
  +0.0f, +0.5f, 0.0f
};

const GLuint vertex_coord_num = 3; // 3 coordinates are provided in vertices[]
static GLuint VAO; // Vertex Attribute Object

void init() {
  shader_program = load_shaders(shaders);
  if ( shader_program) {
    // print some cheerful message here?
  } else {
    fprintf(stderr, "Failed to load shaders\n");
    exit(EXIT_FAILURE);
  }
  //
  glGenVertexArrays(1, &VAO);
  glBindVertexArray(VAO);

  GLuint VBO; // Vertex Buffer Object
  glGenBuffers(1, &VBO);
  // the buffer type of a vertex buffer object is GL_ARRAY_BUFFER
  glBindBuffer(GL_ARRAY_BUFFER, VBO);
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

  // this aPos_location should have value 0 as specified by layout (location 0)
  const GLint aPos_location = glGetAttribLocation(shader_program, "aPos");
  assert(aPos_location == 0);

  /* uintptr_t, intptr_t or size_t, all are OK */
  const uintptr_t start_index_of_vertices = 0;
  glVertexAttribPointer(aPos_location, vertex_coord_num,
                        GL_FLOAT, GL_FALSE,
                        vertex_coord_num * sizeof(float),
                        (void *)start_index_of_vertices);
  glEnableVertexAttribArray(aPos_location);

}

void on_paint()
{
  glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT);

  glUseProgram(shader_program);
  glBindVertexArray(VAO);
  glDrawArrays(GL_TRIANGLES, 0, 3); // the number of vertices to draw
}

int
main(void)
{
  InitConfig config = { .title = "Triangle" };
  return show_glfw_window(&config, init, on_paint);
}

With result looks like this:

triangle-03_300x300.jpg

Figure 4: Triangle (Revised)

4.4. Interlude: What's Needed to Draw?

Pay close attention to the on_paint function above and we will find the trinity of drawing things in OpenGL:

  • A Shader Program
  • A Vertext Array Object15 (that holds reference to Vertex Attribute Configuration and Vertext Buffer Object, etc.)
  • A Draw Function

Since OpenGL is a state machine, and it works with only current objects, the above list could be revised to:

  • Current Shader Program
  • Current Vertex Array Object15 (with affiliated reference to Vertex Attribute Configuration and Vertex Buffer Object, etc.)
  • A Draw Function

As for the "OpenGL is a state machine" statement, take a look behind the scene, and it might look like this, i.e. an imaginary implementation of OpenGL:

#include <stddef.h>

/* IMAGINARY Version of an OpenGL implementation */
typedef unsigned int GLuint; // ID number for all objects
static GLuint NULL_OBJ_ID = 0;

typedef unsigned int GLenum;
typedef int GLint;
typedef size_t GLsizei;

typedef struct {
  GLuint id;
  GLuint vertex_buffer_object;
  GLuint *vertex_attribute_array;
} VertexAttributeObject;

VertexAttributeObject vertex_array_obj_storage[100]; // arbitrary storage length
VertexAttributeObject *find_vertex_array_obj_by_id(GLuint id) {
  for (VertexAttributeObject *entry = vertex_array_obj_storage; 0 != entry->id; ++entry) {
    if (entry->id == id) return entry;
  }
  return NULL;
}

typedef struct {
  GLuint current_shader_program;

  // Overrides current Vertex Buffer Object and Vertex Attribute Array if present
  VertexAttributeObject *current_vertex_attribute_object;

  GLuint current_vertex_buffer_object;
  GLuint *current_vertex_attribute_array;
} OpenGL;

OpenGL ctx;
void glUseProgram(GLuint shader_program_id) {
  ctx.current_shader_program = shader_program_id;
}
void glBindVertexArray(GLuint vertex_array_obj_id) {
  VertexAttributeObject *obj = find_vertex_array_obj_by_id(vertex_array_obj_id);
  ctx.current_vertex_attribute_object = obj;
  if (NULL != obj) {
    ctx.current_vertex_buffer_object = obj->vertex_buffer_object;
    ctx.current_vertex_attribute_array = obj->vertex_attribute_array;
  }
}

void glDrawArrays(GLuint mode, GLint first, GLsizei count) {
  // 1) Extract data from ctx.current_vertex_buffer_object
  //    and ctx.current_vertex_attribute_array
  // 2) Feed the extracted data to shader program that has ID of
  //    ctx.current_shader_program
  // 3) Execute the shader program
  // 4) Output color info of all piexels to monitor for display
}

And from this imaginary version of OpenGL, we can tell that Vertex Array Object only aggregates other information. So technically speaking, it's not required for a triangle – try to comment out VAO in the above source code to see if things still work. That been said, Vertex Array Object might still be needed in other scenarios, e.g. when glDrawElements is invoked, as shown below.

4.5. Rectangle

To draw a rectangle, we draw two triangles. Instead of repeating data for overlapping vetices, Elementary Buffer Object could be used to reference those data by index, as shown below:

// ref: Book: Learn OpenGL
#include <stdio.h>
#include <stdlib.h>

#include <stddef.h>
#include <stdbool.h> // add support for type bool
#include "include/common-gl.h"
#include "include/load-shaders.h"

static bool wireframe_mode = false;
static void key_callback(GLFWwindow *window, int key,
                         int scancode, int action, int mods) {
  if (GLFW_PRESS != action) return;
  switch (key) {
  case GLFW_KEY_ESCAPE: glfwSetWindowShouldClose(window, GLFW_TRUE); break;
  case GLFW_KEY_W:
    wireframe_mode = !wireframe_mode;
    glPolygonMode(GL_FRONT_AND_BACK, wireframe_mode ? GL_LINE : GL_FILL);
    break;      
  }
}

ShaderInfo shaders[] = {
  { .type = GL_VERTEX_SHADER, .filename = "src/shader/triangle.vert" },
  { .type = GL_FRAGMENT_SHADER, .filename = "src/shader/triangle.frag"},
  { .type = GL_NONE }
};

float vertices[] = {
  +0.5f, +0.5f, 0.0f, // top right
  +0.5f, -0.5f, 0.0f, // bottom right
  -0.5f, -0.5f, 0.0f, // bottom left
  -0.5f, +0.5f, 0.0f  // top left
};

unsigned int indices[] = {
  0, 1, 3, // top-right, bottom-right, top-left
  1, 2, 3  // bottom-right, bottom-let, top-left
};


void init(GLFWwindow *window) {
  glfwSetKeyCallback(window, key_callback);

  GLuint shader_program = load_shaders(shaders);
  if (!shader_program) { fprintf(stderr, "ShaderLoad failed"); exit(EXIT_FAILURE); }

  // A Vertext Array Object is needed to aggregate VBO and EBO
  GLuint VAO;
  glGenVertexArrays(1, &VAO);
  glBindVertexArray(VAO); // Try to comment out this line to see if things still work

  GLuint VBO; // Vertex Buffer Object
  glGenBuffers(1, &VBO);
  glBindBuffer(GL_ARRAY_BUFFER, VBO);
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

  GLuint EBO; // Element Buffer Object
  glGenBuffers(1, &EBO);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

  const GLint aPos_loc = glGetAttribLocation(shader_program, "aPos");
  glVertexAttribPointer(aPos_loc, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*) 0);
  glEnableVertexAttribArray(aPos_loc);

  glUseProgram(shader_program);
}

void paint() {
  glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT);
  glDrawElements(GL_TRIANGLES, sizeof(indices) / sizeof(unsigned int), GL_UNSIGNED_INT, 0);
}

int
main(void) {
  InitConfig config = { .title = "Rectangle" };
  return show_glfw_window(&config, init, paint);
}

The result looks like below. We can switch betwene GL_FILL mode and GL_LINE mode by pressing the "W" key.

rectangle_fill_300x300.jpg rectangle_line_300x300.jpg

As for the role of Vertex Array Object here, I could not tell at this moment why it's needed – Is this behavior defined in the OpenGL specifiction, or is it an implementation trait introduced by Mesa?

4.6. Rectangle with Fragment Interpolation

The rectangle above is rendered with a single color, now let's try to render another one with color interpolation, or fragment interpolation.

First, let's prepare the shaders

// rectangle.vert
#version 330 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;

void main()
{
  gl_Position = vec4(aPos, 1.0f);
  ourColor = aColor;
}

// rectangle.frag
#version 330 core

in vec3 ourColor;

out vec4 FragColor;

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

As for the vertex data provided to shader program, it includes both vertex position and color, as shown below:

#include <stdio.h>
#include <stdlib.h>

#include "include/common-gl.h"
#include "include/load-shaders.h"

ShaderInfo shaders[] = {
  { .type = GL_VERTEX_SHADER, .filename = "src/shader/rectangle.vert" },
  { .type = GL_FRAGMENT_SHADER, .filename = "src/shader/rectangle.frag"},
  { .type = GL_NONE }
};

float vertices[] = {
  +0.5f, +0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // top right, position and color
  +0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom right
  -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom left
  -0.5f, +0.5f, 0.0f, 1.0f, 0.0f, 1.0f // top left
};

unsigned int indices[] = {
  0, 1, 3, // top-right, bottom-right, top-left
  1, 2, 3 // bottom-right, bottom-let, top-left
};

As for the shader preparation process, see below:

void init(GLFWwindow *window) {

  GLuint shader_program = load_shaders(shaders);
  if (!shader_program) { fprintf(stderr, "ShaderLoad failed"); exit(EXIT_FAILURE); }

  GLuint VAO; // Vertex Array Object
  glGenVertexArrays(1, &VAO);
  glBindVertexArray(VAO);

  GLuint VBO; // Vertex Buffer Object
  glGenBuffers(1, &VBO);
  glBindBuffer(GL_ARRAY_BUFFER, VBO);
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

  GLuint EBO; // Element Buffer Object
  glGenBuffers(1, &EBO);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

  //
  const GLint aPos_loc = glGetAttribLocation(shader_program, "aPos");
  glVertexAttribPointer(aPos_loc, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*) 0);
  glEnableVertexAttribArray(aPos_loc);

  const GLint aColor_loc = glGetAttribLocation(shader_program, "aColor");
  glVertexAttribPointer(aColor_loc, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
  glEnableVertexAttribArray(aColor_loc);

  glUseProgram(shader_program);
}

void paint() {
  glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT);

  glDrawElements(GL_TRIANGLES, sizeof(indices) / sizeof(unsigned int), GL_UNSIGNED_INT, 0);
}

int
main(void) {
  InitConfig config = { .title = "Rectangle" };
  return show_glfw_window(&config, init, paint);
}

Rendering result looks like this:

rectangle-04_300x300.jpg

Figure 5: Rectangle with Fragment Interpolation

4.7. Interlude: Explain Those Function Parameters

Some gl functions have intuitive signatures, e.g. glGenBuffers, glCreateProgram, while others not. Let's address this concern with existing trouble makers. Note that only simplified explanation is listed here, for formal definition, refer to OpenGL specification16.

4.7.1. glBufferData

void glBufferData( enum target, sizeiptr size, const void *data, enum usage );

Assign data to buffer, with target set to specified buffer object binding target, size set to the size of the data store in basic machine units (i.e. byte), and data pointing to the source data in client memory (i.e. the RAM).

  1. target: buffer object binding targets, e.g. ARRAY_BUFFER for Vertex Attributes, ELEMENT_ARRAY_BUFFER for Vertex Array indicies, etc.
  2. size: size of the data store in basic machine units, i.e. bytes
  3. data: address in RAM (i.e. main memory) from which data will be copied to buffer object's data store in GPU RAM
  4. usage: indicates the expected application usage pattern of the data, e.g. STATIC_DRAW.

4.7.2. glVertexAttribPointer

void glVertexAttribPointer ( uint index, int size, enum type, boolean normalized, sizei stride, const void *pointer );

Describe the locations and organizations of vertex data arrays, e.g. how to interpret data stored in Vertex Array Buffer, etc.

  1. index: attribute position, either retrieved from glGetAttribLocation, or defined in shader through the layout qualifier
  2. size: number of attribute components to be passed to variables "in" vertex shader (pun intended), e.g. 2 for vec2, 3 for vec3, 4 for vec4, etc.
  3. type: type of attribute components, e.g. GL_FLOAT, GL_INT, etc
  4. normalized: is normalization required for the data passed in?
  5. stride stride among successive data, neasured in basic machine unit, i.e. byte
  6. pointer: offset within a buffer of the first value of the first element of the array being specified. i.e. offset of first datum (in bytes) in the RAM (i.e. main memory, instead of GPU RAM). The value needed is actually an unsigned integer, yet somehow the function requires type of (void *), so a type cast is typically needed.

4.7.3. glDrawArrays

void glDrawArrays( enum mode, int first, sizei count );

Draw a number of geometric primitives.

  1. mode: the primitive type to draw, e.g. GL_POINTS, GL_LINE_STRIP, GLTRIANGLE_STRIP, GL_TRIANGLES, etc.
  2. first: from which primitive shall the drawing process begin
  3. count: how many primitives shall be drawn

4.7.4. glDrawElements

void glDrawElements( enum mode, sizei count, enum type, const void *indices );

Draw a number of geometric primitives whose indices are stored in the current bound Element Array Buffer.

  1. mode: the primitive type to draw, same as glDrawArrays
  2. count: how many geometry primitives to draw
  3. type: type of the index, i.e. one of GL_UNSIGNED_INT, GL_UNSIGNED_SHORT, GL_UNSIGNED_BYTE
  4. indices: offset of the indices, not much different from the pointer parameter in glVertexAttribPointer

Footnotes:

1

OpenGL is succeeded by Vulkan, with competitors like DirectX from Microsoft and Metal from Apple. As for WebGL2, it is going to be replaced by WebGPU.

2

So what does a loading library do? The problem it tries to solve is: if we simply include <GL/gl.h> and <GL/glext.h> in our source file, the compiler would not recognize even basic fucntions like glCreateProgram and glCreateShader due to the need of loading OpenGL functions dynamically & explicitly. See following articles for more info:

Still not convinced? Try this:

3

Glad official site https://glad.dav1d.de

4

GLEW official site https://glew.sourceforge.net

5

Learning materials for modern OpenGL as referenced on GLFW Quick Guide:

Other tutorials:

7

WinLibs, a standalone build of GCC and MinGW-w64 for Windows https://winlibs.com

9

Mesa dist for Windows, by pal1000 https://github.com/pal1000/mesa-dist-win

11

GLFW download page https://www.glfw.org/download

14

SDL: Simple DirectMedia Layer https://www.libsdl.org

15

Actually Vertex Array Object is not required: all other current objects like Vertex Buffer Data and Vertex Attribute Config are still present without Vertext Array Object. However, the utilization of Vertex Array Object does make buffer switching easier: instead of switching many buffers, now we only need to switch one.

16

OpenGL specifications provided by Khronos OpenGL Registry https://registry.khronos.org/OpenGL/