This short article is not a tutorial on OpenGL, but only covers the changes needed to modify your OpenGL ES 2.0 application to run on the web.
Table of Contents
Prior to reading this article, that is if you have not setup your Emscripten, you have to read this article: Bring Your C++ Code to the Web. Let me be clear: this is not a tutorial on OpenGL! It can take reading up to 100 pages of OpenGL textbook to display a triangle. It is a stretch to cover the basics of OpenGL in this short article. It only covers the changes needed to modify your OpenGL ES 2.0 application to run on the web. OpenGL ES 2.0 is a subset of OpenGL 2.0 and corresponded to WebGL 1.0. Every function in OpenGL ES 2.0 can be easily mapped to WebGL's equivalent. It makes porting to Emscripten a walk in the park.
In every OpenGL application, there is a render
or draw
function that is called repeatedly in a main loop. In Emscripten, we have to setup the render
function to be called by JavaScript's requestAnimationFrame()
by giving the render
function to emscripten_set_main_loop
with its second argument refers to fps
, is set to 0
. The third argument is simulate_infinite_loop
which setting to zero value led it to enter into emscripten_set_main_loop
.
emscripten_set_main_loop(render, 0, 0);
This is standard SDL 2 code to setup the window and OpenGL 2.0. Feel free to ignore this section if your windowing system is not SDL 2. You are free to use whatever OpenGL windowing system you want. Next, we setup VSync
. GLEW is next. For those who are not familiar with GLEW, GLEW stands for OpenGL Extension Wrangler Library, is a cross-platform C/C++ library that helps in loading OpenGL functions. In the final setup step, we initialize the vertices and shaders in initGL()
.
SDL_Window* gWindow = NULL;
SDL_GLContext gContext;
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
printf("SDL could not initialize! SDL Error: %s\n", SDL_GetError());
success = false;
}
else
{
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
gWindow = SDL_CreateWindow("SDL Tutorial", SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT,
SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);
if (gWindow == NULL)
{
printf("Window could not be created! SDL Error: %s\n", SDL_GetError());
success = false;
}
else
{
gContext = SDL_GL_CreateContext(gWindow);
if (gContext == NULL)
{
printf("OpenGL context could not be created! SDL Error: %s\n", SDL_GetError());
success = false;
}
else
{
if (SDL_GL_SetSwapInterval(1) < 0)
{
printf("Warning: Unable to set VSync! SDL Error: %s\n", SDL_GetError());
}
GLenum err = glewInit();
if (GLEW_OK != err)
{
printf("GLEW init failed: %s!\n", glewGetErrorString(err));
success = false;
}
if (!initGL(userData))
{
printf("Unable to initialize OpenGL!\n");
success = false;
}
}
}
}
The above SDL 2 setup code used to work unmodified for Emscripten. I do not know which commit actually breaks SDL2 implementation on Emscripten. Now you have to use this code below. In emscripten_set_canvas_element_size
, we specify the HTML5 canvas name and width and height. The majorVersion
and minorVersion
should be 1 and 0 because we are targeting WebGL 1.0. Next, we create the WebGL context and make it the current one. Like the above SDL 2 code, we initialize GLEW and OpenGL objects like vertices and shaders. We make this code active with __EMSCRIPTEN__
macro so that the code is visible during Emscripten build.
emscripten_set_canvas_element_size("#canvas", SCREEN_WIDTH, SCREEN_HEIGHT);
EmscriptenWebGLContextAttributes attr;
emscripten_webgl_init_context_attributes(&attr);
attr.alpha = attr.depth = attr.stencil = attr.antialias =
attr.preserveDrawingBuffer = attr.failIfMajorPerformanceCaveat = 0;
attr.enableExtensionsByDefault = 1;
attr.premultipliedAlpha = 0;
attr.majorVersion = 1;
attr.minorVersion = 0;
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context("#canvas", &attr);
emscripten_webgl_make_context_current(ctx);
GLenum err = glewInit();
if (GLEW_OK != err)
{
printf("GLEW init failed: %s!\n", glewGetErrorString(err));
success = false;
}
if (!initGL(userData))
{
printf("Unable to initialize OpenGL!\n");
success = false;
}
In OpenGL ES 2.0, we have to specify the floating point precision before shader code begins. highp
, mediump
and lowp
are the available options. mediump
is a nice tradeoff between precision and performance. For me, lowp
is too low resolution to display image correctly. Insert the code below as the first line in your vertex and fragment shader only when compiling for Emscripten. Remember to remove the line when compiling for desktop.
"precision mediump float; \n"
I recommend keeping shader code inline than storing in files so that in Emscripten, you need not download the shader to load them. There are two ways to inline the code: consecutive string literals or C++11 raw string literals. The former requires you to insert a newline at end of each line for readability. All the consecutive string literals will concatenate into the same string literal. The vertex and fragment shader below are using consecutive string literals.
const char vShaderStr [] =
"precision mediump float; \n"
"uniform mat4 WorldViewProjection;\n"
"attribute vec3 a_position; \n"
"attribute vec2 a_texCoord; \n"
"varying vec2 v_texCoord; \n"
"void main() \n"
"{ \n"
" gl_Position = WorldViewProjection * vec4(a_position, 1.0); \n"
" v_texCoord = a_texCoord; \n"
"} \n";
const char fShaderStr [] =
"precision mediump float; \n"
"varying vec2 v_texCoord; \n"
"uniform sampler2D s_texture; \n"
"void main() \n"
"{ \n"
" gl_FragColor = texture2D( s_texture, v_texCoord );\n"
"} \n";
There are two ways to load the assets such as 3D model and image for texture. One is preload the files in a folder and specifies this location in Makefile. The other method is asynchronous download. Preloading is nice if your assets never changes in every single run of your application. Like game assets. I am doing a slideshow which changes according to the photo which user uploads. So I'll use the asynchronous download. With emscripten_async_wget
, the first argument is the download URL, second is the destination filename, third and fourth arguments are load and error callback for successful and failed download event respectively. For the Emscripten, remember to change the below URL to your localhost and local port before build and to copy the assets to the web server.
#ifdef __EMSCRIPTEN__
emscripten_async_wget("http://localhost:16564/yes.png", IMG_FILE, load_texture, load_error);
#endif
void load_texture(const char * file)
{
gUserData.textureId = init_texture(file);
++gUserData.images_loaded;
}
void load_error(const char * file)
{
printf("File download failed: %s", file);
}
In the Makefile, make sure to set these options for using OpenGL ES 2.0, asm.js, no memory initialization file, SDL 2 window and SDL 2 Image. You can specify -s WASM=1 for Webassembly but make sure your web server can serve wasm files. If not, consult your web server documentation on how to add MIME type for wasm.
-s FULL_ES2=1 -s WASM=0 --memory-init-file 0 -s USE_SDL=2 -s USE_SDL_IMAGE=2
When you run the accompanied source code, you should see this image moving forward and backward.
The demo code is hosted at Github.
- 24th August, 2019: Initial version