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 mesa
6. 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
andmmozeilo
9, 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
Glad
3 andGLEW
4, 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.
Figure 1: gears.c rendering result
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 Mesatriangle-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 islib-mingw-w64
, which includesglfw3.dll
andlibglfw3.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 thePATH
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:
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:
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.
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:
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).
target
: buffer object binding targets, e.g.ARRAY_BUFFER
for Vertex Attributes,ELEMENT_ARRAY_BUFFER
for Vertex Array indicies, etc.size
: size of the data store in basic machine units, i.e. bytesdata
: address in RAM (i.e. main memory) from which data will be copied to buffer object's data store in GPU RAMusage
: 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.
index
: attribute position, either retrieved fromglGetAttribLocation
, or defined in shader through thelayout
qualifiersize
: 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.type
: type of attribute components, e.g.GL_FLOAT
,GL_INT
, etcnormalized
: is normalization required for the data passed in?stride
stride among successive data, neasured in basic machine unit, i.e. bytepointer
: 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.
mode
: the primitive type to draw, e.g.GL_POINTS
,GL_LINE_STRIP
,GLTRIANGLE_STRIP
,GL_TRIANGLES
, etc.first
: from which primitive shall the drawing process begincount
: 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.
mode
: the primitive type to draw, same as glDrawArrayscount
: how many geometry primitives to drawtype
: type of the index, i.e. one ofGL_UNSIGNED_INT
,GL_UNSIGNED_SHORT
,GL_UNSIGNED_BYTE
indices
: offset of the indices, not much different from thepointer
parameter in glVertexAttribPointer
Footnotes:
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.
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:
- Load OpenGL Functions https://www.khronos.org/opengl/wiki/Load_OpenGL_Functions
- OpenGL Loading Library https://www.khronos.org/opengl/wiki/OpenGL_Loading_Library
- Explanations of the need for Glad and GLEW, etc https://www.reddit.com/r/cpp_questions/comments/ryr3fk/good_explanations_of_differences_between_glfw/
Still not convinced? Try this:
- Comment out
<glad/gl.h>
from source code and compile it to see what error is thrown - Take a closer look at
<glad/gl.h>
to see what is defined there - Check source code of
gl3w.c
fromThe OpenGL Programming Guide
https://github.com/openglredbook/examples/blob/master/lib/gl3w.c
Glad official site https://glad.dav1d.de
GLEW official site https://glew.sourceforge.net
Learning materials for modern OpenGL as referenced on GLFW Quick Guide:
- Anton's OpenGL 4 Tutorials: https://antongerdelan.net/opengl/
- Learn OpenGL https://learnopengl.com,
- Open.GL https://open.gl
Other tutorials:
- OpenGL Tutorial http://www.opengl-tutorial.org
Mesa on Homebrew: https://formulae.brew.sh/formula/mesa
WinLibs, a standalone build of GCC and MinGW-w64 for Windows https://winlibs.com
GnuWin packages https://gnuwin32.sourceforge.net/packages.html
Mesa dist for Windows, by pal1000 https://github.com/pal1000/mesa-dist-win
Mesa built by mmozeilo https://github.com/mmozeiko/build-mesa
GLFW download page https://www.glfw.org/download
GLUT - The OpenGL Utility Toolkit https://www.opengl.org/resources/libraries/glut/glut_downloads.php
FreeGLUT download https://freeglut.sourceforge.net/index.php, along with binary built for Windows https://www.transmissionzero.co.uk/software/freeglut-devel/
SDL: Simple DirectMedia Layer https://www.libsdl.org
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.
OpenGL specifications provided by Khronos OpenGL Registry https://registry.khronos.org/OpenGL/