Click here to Skip to main content
15,867,750 members
Articles / Programming Languages / C++

Bring Your C++ OpenGL Code to the Web

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
23 Aug 2019CPOL4 min read 28.6K   326   12   6
How to bring your C++ OpenGL code to the web
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

Introduction

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.

Render Function

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.

C++
emscripten_set_main_loop(render, 0, 0);

Setting Up OpenGL with SDL2

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().

C++
//The window we'll be rendering to
SDL_Window* gWindow = NULL;

//OpenGL context
SDL_GLContext gContext;

//Initialize SDL
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
    printf("SDL could not initialize! SDL Error: %s\n", SDL_GetError());
    success = false;
}
else
{
    //Use OpenGL 2.1
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);

    //Create window
    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
    {
        //Create context
        gContext = SDL_GL_CreateContext(gWindow);
        if (gContext == NULL)
        {
            printf("OpenGL context could not be created! SDL Error: %s\n", SDL_GetError());
            success = false;
        }
        else
        {
            //Use Vsync
            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;
            }

            //Initialize OpenGL
            if (!initGL(userData))
            {
                printf("Unable to initialize OpenGL!\n");
                success = false;
            }
        }
    }
}

Setting Up OpenGL with Emscripten

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.

C++
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;
}

//Initialize OpenGL
if (!initGL(userData))
{
    printf("Unable to initialize OpenGL!\n");
    success = false;
}

OpenGL Shader Precision

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.

C++
"precision mediump float;     \n"

Inline Your Shader Code

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.

C++
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";

Load Asset

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.

C++
#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.

C++
-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.

demo image

The demo code is hosted at Github.

Other Articles in the Bring Your... Series

History

  • 24th August, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Singapore Singapore
Shao Voon is from Singapore. His interest lies primarily in computer graphics, software optimization, concurrency, security, and Agile methodologies.

In recent years, he shifted focus to software safety research. His hobby is writing a free C++ DirectX photo slideshow application which can be viewed here.

Comments and Discussions

 
Questionerror: undefined symbol: glXGetClientString (referenced by top-level compiled C/C++ code) Pin
Member 1410142717-Feb-22 1:27
Member 1410142717-Feb-22 1:27 
AnswerRe: error: undefined symbol: glXGetClientString (referenced by top-level compiled C/C++ code) Pin
Shao Voon Wong17-Feb-22 2:50
mvaShao Voon Wong17-Feb-22 2:50 
QuestionTextureProject Pin
Member 1389298728-Aug-20 18:24
Member 1389298728-Aug-20 18:24 
AnswerRe: TextureProject Pin
Shao Voon Wong28-Aug-20 22:30
mvaShao Voon Wong28-Aug-20 22:30 
GeneralRe: TextureProject Pin
Member 1389298729-Aug-20 10:29
Member 1389298729-Aug-20 10:29 
GeneralRe: TextureProject Pin
Shao Voon Wong29-Aug-20 15:10
mvaShao Voon Wong29-Aug-20 15:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.