Platform-specific guides » JavaScript, HTML5 and WebGL

Building, testing and deploying HTML5 and WebGL projects.

The following guide explains basic workflow of using Emscripten for deploying HTML5 apps using WebGL.

At the very least you need to have Emscripten installed. Running console applications requires Node.js, running browser apps require a webserver that's able to serve static content (for example Apache, if you have Python installed, it has a builtin webserver too).

Cross-compilation to Emscripten is done using a CMake toolchain that's part of the toolchains repository at https://github.com/mosra/toolchains. Add it as a submodule to your project or fetch the contents any other way that suits your project. The following guide will assume the contents of the repository are placed in a toolchains/ subdirectory.

git submodule add git://github.com/mosra/toolchains

There are two toolchain files. The generic/Emscripten.cmake is for the classical (asm.js) build, the generic/Emscripten-wasm.cmake is for WebAssembly build. The following guide will work with the WASM toolchain. Don't forget to adapt EMSCRIPTEN_PREFIX variable in toolchains/generic/Emscripten*.cmake to path where Emscripten is installed; you can also pass it explicitly on command-line using -DEMSCRIPTEN_PREFIX. Default is /usr/emscripten.

Building and running console applications

Emscripten allows you to run arbitrary console utilities and tests via Node.js, except for all code that accesses browsers APIs such as WebGL or audio. Assuming you have Magnum installed in the Emscripten path as described in Cross-compiling for Emscripten, build your project simply as this, using one of the toolchain files from above:

mkdir build-emscripten-wasm && cd build-emscripten-wasm
cmake .. \
    -DCMAKE_TOOLCHAIN_FILE="../toolchains/generic/Emscripten-wasm.cmake" \
    -DCMAKE_BUILD_TYPE=Release
cmake --build .

After that you can run the generated JavaScript file using Node.js. Note that it looks for the corresponding *.wasm file in the current directory, so you need to cd there first:

cd build-emscripten-wasm/src
node my-application.js

Building and deploying graphical apps

In case you don't have an OpenGL ES build set up yet, you need to copy FindOpenGLES2.cmake (or FindOpenGLES3.cmake) from the modules/ directory in Magnum source to the modules/ dir in your project so it is able to find the WebGL libraries.

Magnum provides an Emscripten application wrapper in Platform::Sdl2Application. See its documentation for more information about general usage. You can also use the Emscripten APIs directly or any other way.

To target the web browser, you need to provide a HTML markup for your application. Template one is below. The markup references two files, EmscriptenApplication.js and WebApplication.css, both are in the src/Magnum/Platform/ directory in the source tree, are also put into share/magnum/ inside your install prefix and if you use CMake, their full path is available through the MAGNUM_EMSCRIPTENAPPLICATION_JS and MAGNUM_WEBAPPLICATION_CSS variables.

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>Magnum Emscripten Application</title>
  <link rel="stylesheet" href="WebApplication.css" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
  <h1>Magnum Emscripten Application</h1>
  <div id="container">
    <div id="sizer"><div id="expander"><div id="listener">
      <canvas id="module"></canvas>
      <div id="status">Initialization...</div>
      <div id="status-description"></div>
      <script src="EmscriptenApplication.js"></script>
      <script async="async" src="{{ application }}.js"></script>
    </div></div></div>
  </div>
</body>
</html>

For basic usage you don't need to modify the CSS and JS files at all and everything can be set up directly from the HTML markup. Replace {{ application }} with the name of your application executable and adapt page title and heading as desired. You can modify all files to your liking, but the HTML file must contain at least <canvas id="module"> enclosed in <div id="listener">. The JavaScript file contains event listeners that print loading status on the page. The status is displayed in the remaining two <div> s, if they are available. The CSS file contains a basic style, see below for available options to tweak the default look.

In order to deploy the app, you need to put the JS driver code, the WebAssembly binary (or the asm.js memory image, in case you are compiling with the classic asm.js toolchain), the HTML markup and the JS/CSS files to a common location. The following CMake snippet handles all of that:

if(CORRADE_TARGET_EMSCRIPTEN)
    install(TARGETS my-application DESTINATION ${CMAKE_INSTALL_PREFIX})
    install(FILES
        my-application.html
        ${MAGNUM_EMSCRIPTENAPPLICATION_JS}
        ${MAGNUM_WEBAPPLICATION_CSS}
        DESTINATION ${CMAKE_INSTALL_PREFIX})
    install(FILES
        ${CMAKE_CURRENT_BINARY_DIR}/my-application.js.mem
        ${CMAKE_CURRENT_BINARY_DIR}/my-application.wasm
        DESTINATION ${CMAKE_INSTALL_PREFIX} OPTIONAL)
endif()

To deploy, you can either point CMAKE_INSTALL_PREFIX to a location inside your system webserver or you can point it to an arbitrary directory and use Python's builtin webserver to serve its contents:

cd build-emscripten-wasm
cmake -DCMAKE_INSTALL_PREFIX=/path/to/my/emscripten/deploy ..
cmake --build . --target install

cd /path/to/my/emscripten/deploy
python -m http.server # or python -m SimpleHTTPServer with Python 2

After that, you can open http://localhost:8000 to see the files. Stop the webserver again by pressing Ctrl C.

Building and deploying windowless apps

In case you don't have an EGL + OpenGL ES build set up yet, you need to copy FindEGL.cmake and FindOpenGLES2.cmake (or FindOpenGLES3.cmake) from the modules/ directory in Magnum source to the modules/ dir in your project so it is able to find the EGL and WebGL libraries.

Windowless Magnum apps (i.e. apps that use the OpenGL context without a window) can be run in the browser as well using the Platform::WindowlessEglApplication class. See its documentation for more information about general usage. You can also use the Emscripten APIs directly or any other way.

Similarly to graphics apps, you need to provide a HTML markup for your application. Template one is below, its main difference from the one above is that it shows the console output instead of the canvas. The markup references two files, WindowlessEmscriptenApplication.js and WebApplication.css, both are again in the src/Magnum/Platform/ directory in the source tree, are put into share/magnum/ inside your install prefix and available through MAGNUM_WINDOWLESSEMSCRIPTENAPPLICATION_JS and MAGNUM_WEBAPPLICATION_CSS CMake variables.

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>Magnum Windowless Emscripten Application</title>
  <link rel="stylesheet" href="WebApplication.css" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
  <h1>Magnum Windowless Emscripten Application</h1>
  <div id="container">
    <div id="sizer"><div id="expander"><div id="listener">
      <canvas id="module" class="hidden"></canvas>
      <pre id="log"></pre>
      <div id="status">Initialization...</div>
      <div id="status-description"></div>
      <script src="WindowlessEmscriptenApplication.js"></script>
      <script async="async" src="{{ application }}.js"></script>
    </div></div></div>
  </div>
</body>
</html>

Replace {{ application }} with the name of your application executable. You can modify all the files to your liking, but the HTML file must contain at least the <canvas> enclosed in listener <div> and the <pre id="log"> for displaying the output. The JavaScript file contains event listeners which print loading status on the page. The status displayed in the remaining two <div> s, if they are available. The CSS file is shared with graphical apps, again see below for available options to tweak the default look.

Deployment is similar to graphical apps, only referencing a different JS file:

if(CORRADE_TARGET_EMSCRIPTEN)
    install(TARGETS my-application DESTINATION ${CMAKE_INSTALL_PREFIX})
    install(FILES
        my-application.html
        ${MAGNUM_WINDOWLESSEMSCRIPTENAPPLICATION_JS}
        ${MAGNUM_WEBAPPLICATION_CSS}
        DESTINATION ${CMAKE_INSTALL_PREFIX})
    install(FILES
        ${CMAKE_CURRENT_BINARY_DIR}/my-application.js.mem
        ${CMAKE_CURRENT_BINARY_DIR}/my-application.wasm
        DESTINATION ${CMAKE_INSTALL_PREFIX} OPTIONAL)
endif()

Modifying page style, canvas size and aspect ratio

The WebApplication.css file contains a basic style and the additional .container, .sizer and .expander <div> s take care of aligning the canvas to the center and making it responsively scale on narrow screens, preserving aspect ratio. For proper responsiveness on all platforms it's important to include the <meta name="viewport"> tag in the HTML markup as well.

By default the canvas is 640px wide with a 4:3 aspect ratio, you can modify this by placing one of the .aspect-* CSS classes on the <div id="container">:

Aspect ratioCSS class (landscape/portrait)
1:1.aspect-1-1
4:3 (1.33:1).aspect-4-3 (default) / .aspect-3-4
3:2 (1.5:1).aspect-3-2 / .aspect-2-3
16:9 (1.78:1).aspect-16-9 / .aspect-9-16
2:1.aspect-2-1 / .aspect-1-2

For example, for a 640x360 canvas (16:9 aspect ratio), you would use <div id="container" class="aspect-16-9">. Besides the predefined classes above, it's also possible to specify your own aspect ratio using a padding-bottom style with a percentage equal to inverse of the ratio for div#expander — for example, a 2.35:1 ratio would be <div id="expander" style="padding-bottom: 42.553%">.

Size of the canvas can be also overriden by specifying one of the .width-* CSS classes on the <div id="container">:

WidthCSS class
240px.width-240
320px.width-320
360px.width-360
480px.width-480
600px.width-600
640px.width-640 (default)
800px.width-800

For example, for a 480x640 canvas (3:4, portrait) you would use <div id="container" class="width-480 aspect-3-4">. Besides the predefined classes above, it's also possible to specify your own by adding a width style to the div#sizer — for example, a 1024x768 canvas would be <div id="sizer" style="width: 1024px">. Again note that if the canvas is larger than window width, it gets automatically scaled down, preserving its aspect ratio.

It's also possible to stretch the canvas to occupy the full page using the .fullsize CSS class set to div#container. In this case it's advised to remove all other elements (such as the <h1> element) from the page to avoid them affecting the canvas. Combining .fullsize with the other .aspect-* or .width-* classes is not supported.

Besides the canvas, there's minimal styling for <h1>, <p> and <code> tags. Putting extra content inside the div#sizer will center it, following the canvas width. If you need more advanced styling, check out m.css.

Controlling event behavior

Keyboard events

By default, the graphical Emscripten application grabs all keyboard input anywhere on the page (so e.g. F5 or opening browser console via a keyboard shortcut doesn't work). If you don't want it to grab keyboard input at all, set Module.doNotCaptureKeyboard to true, either by modifying the EmscriptenApplication.js file or directly in the HTML source:

<script>
// after EmscriptenApplication.js has been loaded
Module.doNotCaptureKeyboard = true;
</script>

The above is implicitly set for windowless apps, because these don't have any event loop.

Another solution is to specify the element on which it should capture keybard using Module.keyboardListeningElement — it requires the actual element instance (not just its ID). The element then needs to be activated (for example with a mouse click) in order to start receiving input. The <canvas> element is a bit special in this regard — in order to make it receive keyboard input, you have to add a tabindex attribute to it like this:

<canvas id="module" tabindex="0"></canvas>

After that, the canvas can be focused with a Tab key. But because Emscripten eats all mouse input, the mousedown event won't be propagated to focus the canvas unlesss you do that manually:

Module.keyboardListeningElement = Module.canvas;
Module.canvas.addEventListener('mousedown', function(event) {
    event.target.focus();
});

Context menu

By default, the canvas opens a browser-specific popup menu on right click. That allows the user to for example save a screenshot of the canvas, but if you are handling right click directly in your app, you may want to disable the default behavior:

Module.canvas.addEventListener('contextmenu', function(event) {
    event.preventDefault();
}, true);

Terminal output, environment and command-line arguments

When running console apps using Node.js, command-line arguments and terminal output work like usual.

For graphical apps in the browser, EmscriptenApplication.js redirects all output (thus also Debug, Warning and Error) to JavaScript console. For windowless apps, WindowlessEmscriptenApplication.js redirects output to the <pre id="log"> element on the page.

It's possible to pass command-line arguments to main() using GET URL parameters. For example, /app/?foo=bar&fizz&buzz=3 will go to the app as ['--foo', 'bar', '--fizz', '--buzz', '3'].

Emscripten provides its own set of environment variables through std::getenv() and doesn't expose system environment when running through Node.js. In order to access system environment, you can use the Corrade::Utility::Arguments class, especially Corrade::Utility::Arguments::environment().

Differences between WebGL and OpenGL ES

WebGL is subset of OpenGL ES with some specific restrictions, namely requirement for unique buffer target binding, aligned buffer offset and stride and some other restrictions. The most prominent difference is that while the following was enough on desktop:

GL::Buffer vertices, indices;

On WebGL (even 2.0) you always have to initialize the buffers like this (and other target hints for UBOs etc.):

GL::Buffer vertices{GL::Buffer::TargetHint::Array},
    indices{GL::Buffer::TargetHint::ElementArray};

See GL::Buffer, GL::Mesh, GL::*Texture::setSubImage(), GL::Mesh::addVertexBuffer(), GL::Renderer::setStencilFunction(), GL::Renderer::setStencilMask() and GL::Renderer::setBlendFunction() documentation for more information. The corresponding sections in official WebGL specification provide even more detail:

Troubleshooting

Setting up Emscripten on macOS

Emscripten is available through Homebrew:

brew install emscripten

Because LLVM is also distributed as part of Xcode, it will get picked up by default. Emscripten however needs to use its own bundled version, so you may need to export the $LLVM environment variable to port to the other location:

export LLVM=/usr/local/opt/emscripten/libexec/llvm/bin

First Emscripten run takes long or fails

Emscripten downloads and builds a lot of things on first startup or after upgrade. That's expected and might take quite some time. If you are calling Emscripten through the CMake toolchain, it might be attempting to bootstrap itself multiple times, taking extreme amounts of time, or even fail during the initial CMake compiler checks for various reasons such as

  File "/usr/lib/python2.7/subprocess.py", line 1025, in _execute_child
    raise child_exception
OSError: [Errno 13] Permission denied

The CMake toolchain might interfere with the bootstrap operation, causing it to fail. Solution is to wipe all Emscripten caches and trigger a rebuild of all needed libraries by compiling a minimal project, as shown in the shell snippet below — enter it into the console prior to building anything else. It will take a while to download and build various system libraries and random tools. The -s WASM=1 flag is needed in order to enable a rebuild of the binaryen tool as well:

cd /tmp
emcc --clear-cache
emcc --clear-ports
echo "int main() {}" > main.cpp
em++ -s WASM=1 main.cpp

After this step it might be also needed to purge all CMake build directories and set them up again to ensure no bad state got cached.

CMake can't find _CORRADE_MODULE_DIR

If initial CMake configuration fails with

Could NOT find Corrade (missing: _CORRADE_MODULE_DIR)

The solution is to explicitly pass CMAKE_PREFIX_PATH pointing to directory where Corrade is installed. In some cases it might also be needed to point CMAKE_FIND_ROOT_PATH to the same location. For example:

mkdir build-emscripten && cd build-emscripte
cmake .. \
    -DCMAKE_TOOLCHAIN_FILE=../toolchains/generic/Emscripten-wasm.cmake \
    -DCMAKE_PREFIX_PATH=/your/emscripten/libs/ \
    -DCMAKE_FIND_ROOT_PATH=/your/emscripten/libs/ \
    -G Ninja

Application fails to load

Depending on what's the exact error printed in the browser console, the following scenarios are possible:

  • By default, the size of Emscripten heap is restricted to 16 MB. That might not be enough if you have large compiled-in resources or allocate large amount of memory. This can be solved with either of these:
    • Adding -s TOTAL_MEMORY=<bytes> to compiler/linker flags, where <bytes> is the new heap size
    • Adding -s ALLOW_MEMORY_GROWTH=1 to compiler/linker flags. This is useful in case you don't know how much memory you need in advance and might disable some optimizations.
    • Setting Module { TOTAL_MEMORY: <bytes>; } in the JavaScript driver file
  • Sometimes Chromium-based browsers refuse to create WebGL context on a particular page, while on other sites it works and the same page works in other browsers such as Firefox. This can be caused by Chromium running for too long, restart it and try again.
  • If you compile your application with a different set of compiler / linker flags or a different Emscripten version than your dependencies, it can fail to load for a variety of random reasons. Try to rebuild everything with the same set of flags.

Incorrect response MIME type

Depending on your browser, you might see a warning similar to the following in the console:

wasm streaming compile failed: TypeError: Failed to execute 'compile' on
    'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'.
falling back to ArrayBuffer instantiation

This is not a critical error, but causes slow startup since the browser usually attempts to load the file twice. This is because the HTTP Content-Type header is not set properly by the webserver. In case you use Apache, fixing this is a matter of adding the following line to your .htaccess:

AddType application/wasm .wasm

It is not possible when using Python http.server from the command line, but the mapping can be added programmatically:

http.server.SimpleHTTPRequestHandler.extensions_map['.wasm'] = 'application/wasm'

Warnings about $ characters when using EM_ASM

The EM_ASM() family of macros is using $0, $1, ... to refer to macro arguments from JavaScript code. Using them in C++ sources triggers a warning from Clang:

warning: '$' in identifier

Solution is to disable the warning for the offending lines, here for example when modifying a title element contents through DOM:

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdollar-in-identifier-extension"
std::string title;
EM_ASM_({document.getElementById('title').innerHTML =
    Pointer_stringify($0, $1)}, title.data(), title.size());
#pragma GCC diagnostic pop