Click here to Skip to main content
14,302,993 members

Bring Your C++ OpenGL Code to the Web

Rate this:
5.00 (3 votes)
Please Sign up or sign in to vote.
5.00 (3 votes)
23 Aug 2019CPOL
How to Bring Your C++ OpenGL Code to 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 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 3rd argument is simulate_infinite_loop which setting to zero value lead it to enter into emscripten_set_main_loop.

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

//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 targetting 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;
}

//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 1st 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"

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 is 2 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";

Load Asset

There are 2 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, 1st argument is the download URL, second is the destination filename, 3rd and 4th argument 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.

demo image

The demo code is hosted at Github

Other Articles in the Bring Your... Series

License

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

Share

About the Author

Shao Voon Wong
Software Developer (Senior)
Singapore Singapore
Shao Voon is from Singapore. CodeProject awarded him a MVP award in recognition of his article contributions in 2019. In his spare time, he prefers to writing application based on 3rd party library than writing his own library. His interest lies primarily in computer graphics, software optimization, security and Agile methodologies.

You can reach him by sending a message on CodeProject or at his Coding Tidbit Blog!

Comments and Discussions

 
-- There are no messages in this forum --
Article
Posted 23 Aug 2019

Tagged as

Stats

4.8K views
72 downloads
8 bookmarked