Running SDL2 and OpenGL in C with WebGL and Express on Netlify
A Comprehensive Guide to using Emscripten to safely add C code to your Express WebApp (We Also Show How to Free Memory Correctly)
This is part of a series on Writing a Video Player and Editor in Pure C.
Part 1 : SDL and FFMPEG C API (not the commandline)
We code parts of a video player in less than 1000 lines of C code.
Part 2 (we are here) : Emscripten and SDL
We learn how to use Emscripten to compile our C app to JavaScript and deploy our app to Netlify.
1.0 Introduction
SDL2 is a multiplatform library for audio and visual programming (SDL Homepage1) written in C. This is a step-by-step guide to adding WebGL-accelerated graphics via SDL2 in C to your Express Javascript app.
SDL2 provides OS-dependent stuff (keyboard, mouse, touch, text, audio, networking, etc.)2
OpenGLES23 is a 3D programming language for embedded devices. So the code we write in this series runs on Android, IOS, Nintendo Switch, Web, PS Vita et al.
WebGL permits 3D programming. Emscripten converts OpenGLES2
This is a comprehensive guide to getting SDL2 and OpenGL running (safely) on your website. It is in C, not C++.
2.0 Setting up our Environment
This section covers installing Emscripten, SDL2, Express and Netlify on your computer. I assume you’re running on Linux.
2.1 Installing Emscripten

We follow the official Emscripten documentation4 to get it working on our machine.
Verify the installation using this command:
emcc -v
Note that you need to add Emscripten to your bashrc file to ensure emcc works after you close the current terminal. These are the steps:
2.1.1 Open bashrc in your preferred
gedit ~/.bashrc
2.1.2 Set Emscripten Paths
Modify and append these lines to the bottom of bashrc.
export EMSDK="$HOME/emsdk" # Adjust if your path is different
export PATH="$EMSDK:$EMSDK/upstream/emscripten:$PATH"
export EMSDK_NODE="$EMSDK/node/14.18.2_64bit/bin" # Adjust Node version if needed
export PATH="$EMSDK_NODE:$PATH"
2.1.3 Reload bashrc
Save the modified bashrc file and exit. Inside your terminal, reload bashrc using this commmand:
source ~/.bashrc
If everything is working fine then you should see something similar:
2.2 Installing SDL2
Emscripten comes with preinstalled libraries called Emscripten ports5. Lucky us, SDL2 ships with emcc.

In the docs, we see that we need to add SDL2 to our emcc build command like this:
emcc test/browser/test_sdl2_glshader.c --use-port=sdl2 -sLEGACY_GL_EMULATION -o sdl2.html
2.3 Installing OpenGL
OpenGL is a 3D graphics library. Emscripten converts OpenGL to WebGL under the hood6. The Emscripten documentation recommends using OpenGL ES 2.0/3.0 because it is WebGL friendly7.

Once again, the official documentation introduces Emscripten’s in-build OpenGL support and demonstrates linking commands.
Basic usage looks like this:
emcc main.c -s USE_SDL=2 -s USE_WEBGL2=1 -o game.html
2.4 Installing Netlify
We shall use Netlify to host the html files generated by Emscripten. You can upload to Netlify via GitHub, CLI or regular file upload.

All you need to do is log into Netlify and click on the ‘Add New Project’ button. We shall use the ‘Deploy Manually’ button from here.
2.5 Installing Node and Express JS
The final step in setting up our environment is installing NodeJS and Express JS. We shall use these backend Javascript libraries in our server.
First, follow the official NodeJS documentation8 to install Node
Next, we can get Express running using the official Express documentation9 as well
3.0 Rendering a Triangle
This section demonstrates how to render a triangle. First we verify our installation then proceed to working with SDL and OpenGL.
3.1 Verify Our Installation
In this section, we shall verify everything is working. First, create an empty C file called HelloTriangle.c
Then, in the code below, we include our SDL2, OpenGL headers, and add our main function.
//SDL2 and OpenGLES2
//Run : emcc HelloTriangle.c -s USE_SDL=2 -s FULL_ES2=1 -s WASM=1 -o HelloTriangle.html
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <SDL.h>
#include <SDL_opengles2.h>
#else
#include <SLD2/SDL.h>
#include <SDL2/SDL_opengles2.h>
#endif
int main()
{
int windowWidth = 512;
int windowHeight= 512;
return 0;
}
We compile using the command:
emcc HelloTriangle.c -s USE_SDL=2 -s FULL_ES2=1 -s WASM=1 -o HelloTriangle.html
If it is your first time running the command, you’ll observe that Emscripten first downloads some libraries:
The first compilation may take a while but the subsequent compilations will be fast.
If there are no errors then look inside the directory to observe 3 new files:
3.2 SDL2 and OpenGL Primitives
This section introduces the SDL2 and OpenGL API. First, we draw a white background to ensure our setup is working. Then we proceed to draw an actual triangle.
3.2.1 Drawing a White Background
We start working with SDL2 and OpenGL in this section.
Here’s a summary of the functions introduced in this section:
First, we create a window using
SDL_CreateWindow
10 then free the memory usingSDL_DestroyWindow.
Second, we introduce
SDL_LogError
11Third, we write some boilerplate to initialize OpenGL.
Fourth, we use
SDL_GL_CreateContext12
to create an OpenGL context and we useSDL_GL_DeleteContext
13 to free our memory.Fifth, we use
glClearColor14
to set the background color to white. In the linked docs, we see that this function only takes floats between 0 and 1.Sixth, after setting the buffer color we need to clear the buffers using
glClear
15 function.Seventh, we get OpenGL’s window actual drawable size to prevent excessive DPI scaling using
SDL_GL_GetDrawableSize
16 and changewindowWidth
andwindowHeight
to these values.Eighth, we set our OpenGL view to the center of our screen using
glViewport17
Ninth, we create a
MainLoop
function that keeps our window open inside the browser and refreshes. We just repeat all the code we’ve written before. The only new function isSDL_GL_SwapWindow18
which updates our window with the OpenGL render:Finally, we use the
emscripten_set_main_loop_arg19
function to run our code in the browser using Emscripten. According to the linked documentation, we set fps to 0 so the browser can handle this.
All our code in a single file:
//SDL2 and OpenGLES2
//Run : emcc HelloTriangle.c -s USE_SDL=2 -s FULL_ES2=1 -s WASM=1 -o HelloTriangle.html
//Preview : emrun HelloTriangle.html
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <SDL.h>
#include <SDL_opengles2.h>
#else
#include <SLD2/SDL.h>
#include <SDL2/SDL_opengles2.h>
#endif
void MainLoop(void *mainLoopArgument)
{
// Resize on every frame but normally resizing is done on resize event
SDL_Window *sdlWindow = (SDL_Window*)mainLoopArgument;
int windowWidth = 0;int windowHeight = 0;
SDL_GL_GetDrawableSize(sdlWindow, &windowWidth, &windowHeight);
glViewport(0, 0, windowWidth, windowHeight);
// Clear screen
glClear(GL_COLOR_BUFFER_BIT);
// Swap front/back framebuffers
SDL_GL_SwapWindow(sdlWindow);
}
int main()
{
int windowWidth = 512;
int windowHeight= 512;
char *windowName = "HelloTriangle";
//Create a new window
SDL_Window *sdlWindow = SDL_CreateWindow(windowName, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,windowWidth, windowHeight, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_SHOWN);
if(sdlWindow == NULL){SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Could not create window: %s\n", SDL_GetError());exit(1);}
//Create OpenGLES2 context inside SDL Window
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetSwapInterval(1);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
//Create OpenGL Context
SDL_GLContext openGLContext = SDL_GL_CreateContext(sdlWindow);
if(openGLContext == NULL){SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Could not create OpenGL Context: %s\n", SDL_GetError());exit(1);}
printf("INFO: GL version: %s\n", glGetString(GL_VERSION));
//OpenGL Set Background Color to white
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
//Call GL Clear
glClear(GL_COLOR_BUFFER_BIT);
//Change windowWidth and windowHeight and to OpenGL window's actual size in pixels
SDL_GL_GetDrawableSize(sdlWindow, &windowWidth, &windowHeight);
printf("INFO: GL window size = %dx%d\n", windowWidth, windowHeight);
//Set Open GL Viewport
glViewport(0, 0, windowWidth, windowHeight);
//Define our sdlWindow as the argument for our main loop
void *mainLoopArgument = sdlWindow;
//Start Emscripten main loop
#ifdef __EMSCRIPTEN__
int fps = 0;
emscripten_set_main_loop_arg(MainLoop, mainLoopArgument, fps, true);
#else
while(true)
{
MainLoop(MainLoopArgument);
}
#endif
//Destroy OpenGL Context
SDL_GL_DeleteContext(openGLContext);
//Destroy Window
SDL_DestroyWindow(sdlWindow);
//End SDL session
SDL_Quit();
return 0;
}
/*
*/
Once again, we compile using the command:
emcc HelloTriangle.c -s USE_SDL=2 -s FULL_ES2=1 -s WASM=1 -o HelloTriangle.html
This time however, we shall preview our HTML. Use this command to open the generated HTML file:
emrun HelloTriangle.html
If everything is working then your browser should open to this white screen with a console:
Try changing the background color to black :)
3.2.2 Drawing a Triangle
Now, we have enough code to draw a triangle. To proceed, we need to write shaders in the OpenGL Programming Language.
At the very top of our file, we append code for out vertex and pixel shaders
Next, we write a function to initialize our shaders and free them afterwards. It’s all boilerplate:
Next, we create a function to Initialize our Triangle geometry:
Then, we’ll add this line to our MainLoop function to refresh the triangle after clearing our screen:
// Draw the vertex buffer
glDrawArrays(GL_TRIANGLES, 0, 3);
Finally, right before our main loop, we need to initialize our shaders and geometries:
// Initialize shader and geometry
GLuint shaderProgram = InitializeShader();
InitializeTriangleGeometry(shaderProgram);
//Destroy shaderProgram
glDeleteProgram(shaderProgram);
In a single file, our code looks like this:
//SDL2 and OpenGLES2
//Run : emcc HelloTriangle.c -s USE_SDL=2 -s FULL_ES2=1 -s WASM=1 -o HelloTriangle.html
//Preview : emrun HelloTriangle.html
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <SDL.h>
#include <SDL_opengles2.h>
#else
#include <SLD2/SDL.h>
#include <SDL2/SDL_opengles2.h>
#endif
// Vertex shader
const GLchar* vertexSource =
"attribute vec4 position; \n"
"varying vec3 color; \n"
"void main() \n"
"{ \n"
" gl_Position = vec4(position.xyz, 1.0); \n"
" color = gl_Position.xyz + vec3(0.5); \n"
"} \n";
// Fragment/pixel shader
const GLchar* fragmentSource =
"precision mediump float; \n"
"varying vec3 color; \n"
"void main() \n"
"{ \n"
" gl_FragColor = vec4 ( color, 1.0 ); \n"
"} \n";
GLuint InitializeShader()
{
// Create and compile vertex shader
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexSource, NULL);
glCompileShader(vertexShader);
// Create and compile fragment shader
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentSource, NULL);
glCompileShader(fragmentShader);
// Link vertex and fragment shader into shader program and use it
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glUseProgram(shaderProgram);
// Cleanup: Detach and delete shaders (they're now part of the program)
glDetachShader(shaderProgram, vertexShader);
glDetachShader(shaderProgram, fragmentShader);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return shaderProgram;
}
void InitializeTriangleGeometry(GLuint shaderProgram)
{
// Create vertex buffer object and copy vertex data into it
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
//Triangle Vertices
GLfloat vertices[] =
{
0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f
};
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// Specify the layout of the shader vertex data (positions only, 3 floats)
GLint posAttrib = glGetAttribLocation(shaderProgram, "position");
glEnableVertexAttribArray(posAttrib);
glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, 0, 0);
}
void MainLoop(void *mainLoopArgument)
{
// Resize on every frame but normally resizing is done on resize event
SDL_Window *sdlWindow = (SDL_Window*)mainLoopArgument;
int windowWidth = 0;int windowHeight = 0;
SDL_GL_GetDrawableSize(sdlWindow, &windowWidth, &windowHeight);
glViewport(0, 0, windowWidth, windowHeight);
// Clear screen
glClear(GL_COLOR_BUFFER_BIT);
// Draw the vertex buffer
glDrawArrays(GL_TRIANGLES, 0, 3);
// Swap front/back framebuffers
SDL_GL_SwapWindow(sdlWindow);
}
int main()
{
int windowWidth = 512;
int windowHeight= 512;
char *windowName = "HelloTriangle";
//Create a new window
SDL_Window *sdlWindow = SDL_CreateWindow(windowName, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,windowWidth, windowHeight, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_SHOWN);
if(sdlWindow == NULL){SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Could not create window: %s\n", SDL_GetError());exit(1);}
//Create OpenGLES2 context inside SDL Window
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetSwapInterval(1);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
//Create OpenGL Context
SDL_GLContext openGLContext = SDL_GL_CreateContext(sdlWindow);
if(openGLContext == NULL){SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Could not create OpenGL Context: %s\n", SDL_GetError());exit(1);}
printf("INFO: GL version: %s\n", glGetString(GL_VERSION));
//OpenGL Set Background Color to white
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
//Call GL Clear
glClear(GL_COLOR_BUFFER_BIT);
//Change windowWidth and windowHeight and to OpenGL window's actual size in pixels
SDL_GL_GetDrawableSize(sdlWindow, &windowWidth, &windowHeight);
printf("INFO: GL window size = %dx%d\n", windowWidth, windowHeight);
//Set Open GL Viewport
glViewport(0, 0, windowWidth, windowHeight);
//Define our sdlWindow as the argument for our main loop
void *mainLoopArgument = sdlWindow;
// Initialize shader and geometry
GLuint shaderProgram = InitializeShader();
InitializeTriangleGeometry(shaderProgram);
//Start Emscripten main loop
#ifdef __EMSCRIPTEN__
int fps = 0;
emscripten_set_main_loop_arg(MainLoop, mainLoopArgument, fps, true);
#else
while(true)
{
MainLoop(MainLoopArgument);
}
#endif
//Destroy shaderProgram
glDeleteProgram(shaderProgram);
//Destroy OpenGL Context
SDL_GL_DeleteContext(openGLContext);
//Destroy Window
SDL_DestroyWindow(sdlWindow);
//End SDL session
SDL_Quit();
return 0;
}
If you have no errors then you should observe this triangle after compilation and preview:
4.0 Deploying To Netlify
Next, we want to deploy our HelloTriangle.html file to Netlify. (I’m still figuring this out lol)