Skip to content

About

πŸŽƒ Windowless WebGL for node.js

Topics

Resources

Stars

1.9k stars

Watchers

39 watching

Forks

Β 
Β 

Latest commit

Β 

History

1,240 Commits

Folders and files

Repository files navigation

gl

ci js-standard-style

gl lets you create a WebGL context in Node.js without making a window or loading a full browser environment.

It aspires to fully conform to the WebGL 1.0.3 specification. On Linux and macOS, it passes 617 (99.5%) of the 620 tests in the WebGL 1.0.3 conformance suite that can run outside a browser. The other three need features that headless-gl or Node don't have. The suite's remaining 55 tests need a browser, for example for image and video elements, workers or a page to show the canvas on.

Example

// Create context
const createGL = require('gl')

const width = 64
const height = 64

const gl = createGL(width, height)

//Clear screen to red
gl.clearColor(1, 0, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT)

//Write output as a PPM formatted image
const pixels = new Uint8Array(width * height * 4)
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels)
process.stdout.write(['P3\n# gl.ppm\n', width, " ", height, '\n255\n'].join(''))

for(let i = 0; i < pixels.length; i += 4) {
  for(let j = 0; j < 3; ++j) {
    process.stdout.write(pixels[i + j] + ' ')
  }
}

Install

Installing headless-gl on a supported platform is a snap using one of the prebuilt binaries. Using npm, run the command

npm install gl

And you are good to go!

npm 12 and later don't run the install scripts of dependencies you haven't approved, and gl uses its install script to download a prebuilt binary or build one from source. If require('gl') fails with "Could not locate the bindings file", approve the script and rebuild:

npm install-scripts approve gl
npm rebuild gl

Prebuilt binaries are generally available for LTS Node.js versions (e.g. 22, 24, 26) until they reach EOL. If your system is not supported, then please see the system dependencies section on how to configure your build environment. Patches to improve support are always welcome!

When gl builds from source, it downloads the ANGLE libraries for your platform from this repository's GitHub releases, since the npm package doesn't include them. To build without access to GitHub, set HEADLESS_GL_ANGLE_MIRROR to a URL or a local folder with the same .gz files.

Supported platforms and Node.js versions

gl runs on Linux (including Alpine), macOS, and Windows.

Node.js versions 22 and up are tested and supported. Earlier versions may still function, however.

API

headless-gl exports one function which you can use to create a WebGL context with the given context attributes:

const createGL = require('gl')

const gl = createGL(width, height)
  • width is the width of the drawing buffer
  • height is the height of the drawing buffer
  • contextAttributes is an optional object whose properties are the WebGL context attributes, and createWebGL2Context, which creates a WebGL2 context when it's true. Only alpha, depth and stencil change the drawing buffer, and antialias is always off. Nothing composites the drawing buffer in Node, so it's always preserved whatever preserveDrawingBuffer is. The rest, such as premultipliedAlpha, only change what getContextAttributes reports.

If width or height is larger than the GPU allows, the drawing buffer is made as large as it allows, as in browsers, and gl.drawingBufferWidth and gl.drawingBufferHeight give its size. The STACKGL_resize_drawingbuffer extension does the same.

Returns A new WebGLRenderingContext object, or a WebGL2RenderingContext when createWebGL2Context is true.

Throws an Error with the code HEADLESS_GL_CONTEXT_CREATION_FAILED if the context can't be created, for example when a library that ANGLE needs isn't installed. The error message gives the reason, such as the name of the missing library. Throws a RangeError if width or height is less than 1.

The function also has WebGLRenderingContext and WebGL2RenderingContext properties, the classes of the contexts it creates, which you can also import by name. Use them with instanceof to check a context's version:

const { WebGL2RenderingContext } = require('gl')
// or: import createGL, { WebGL2RenderingContext } from 'gl'

gl instanceof WebGL2RenderingContext

They can't be constructed directly.

TypeScript

headless-gl includes TypeScript types for its contexts, so projects don't need TypeScript's DOM types. They follow the DOM's WebGL types, except that the contexts have no canvas or color space members, and texture uploads take an object with pixel data, like ImageData, or with a 2D context, like a node-canvas Canvas, instead of DOM elements.

To pass a context to code that expects the DOM's WebGLRenderingContext, such as three.js, cast it with gl as WebGLRenderingContext. To type every context as the DOM's, add this to your project:

declare module 'gl' {
  interface CustomContextTypes {
    WebGLRenderingContext: globalThis.WebGLRenderingContext
    WebGL2RenderingContext: globalThis.WebGL2RenderingContext
  }
}

Both need the DOM types in your project.

Extensions

In addition to all the usual WebGL methods, headless-gl exposes some custom extensions to make it easier to manage WebGL context resources in a server side environment:

STACKGL_resize_drawingbuffer

This extension provides a mechanism to resize the drawing buffer of a WebGL context once it is created.

In a pure DOM implementation, this would be implemented by resizing the context's canvas element by modifying its width/height properties. This canvas manipulation is not possible in headless-gl, since a headless context doesn't have a DOM or a canvas element associated with it.

Example
const assert = require('assert')
const gl = require('gl')(10, 10)
assert(gl.drawingBufferHeight === 10 && gl.drawingBufferWidth === 10)

const ext = gl.getExtension('STACKGL_resize_drawingbuffer')
ext.resize(20, 5)
assert(gl.drawingBufferWidth === 20 && gl.drawingBufferHeight === 5)
IDL
[NoInterfaceObject]
interface STACKGL_resize_drawingbuffer {
    void resize(GLint width, GLint height);
};
ext.resize(width, height)

Resizes the drawing buffer of a WebGL rendering context

  • width is the new width of the drawing buffer for the context
  • height is the new height of the drawing buffer for the context

STACKGL_destroy_context

Destroys the WebGL context immediately, reclaiming all resources associated with it.

For long-running jobs, garbage collection of contexts is often not fast enough. To prevent the system from becoming overloaded with unused contexts, you can force the system to reclaim a WebGL context immediately by calling .destroy().

Example
const gl = require('gl')(10, 10)

const ext = gl.getExtension('STACKGL_destroy_context')
ext.destroy()
IDL
[NoInterfaceObject]
interface STACKGL_destroy_context {
    void destroy();
};
gl.getExtension('STACKGL_destroy_context').destroy()

Immediately destroys the context and all associated resources.

WebGL2

To create a WebGL2 context, set the createWebGL2Context property to true in the contextAttributes argument.

Every WebGL2 method is implemented, but WebGL2 hasn't been tested against the WebGL 2.0 conformance suite yet.

Unlike in browsers, query results and sync objects are ready as soon as the GPU finishes, which can be before control returns to the event loop, and clientWaitSync can wait for up to MAX_CLIENT_WAIT_TIMEOUT_WEBGL nanoseconds, which is Number.MAX_SAFE_INTEGER. Code that waits the way browsers require, by checking again in a later callback, works the same.

System dependencies

In most cases installing headless-gl from npm should just work. However, if you run into problems you might need to adjust your system configuration and make sure all your dependencies are up to date. For general information on building native modules, see the node-gyp documentation.

macOS

Prebuilt binaries (arm64 and x64) need nothing else. To build from source, install Python 3 and the Xcode Command Line Tools, with xcode-select --install.

Ubuntu/Debian

Prebuilt binaries for glibc 2.34 and later (x64 and arm64), such as Debian 12, Ubuntu 22.04, RHEL 9 and Amazon Linux 2023, need these libraries at runtime:

$ sudo apt-get install -y libx11-6 libxext6 libxcb1

On RHEL, Fedora and Amazon Linux, install libX11 libXext libxcb with dnf. If one is missing, createGL throws an error naming the library it couldn't load.

To build from source, also install build-essential and python3. For headless use, see below.

Alpine Linux

Prebuilt binaries for Alpine 3.22 and later (x64 and arm64) need these libraries at runtime:

$ apk add libx11 libxext libxcb libdrm zlib

To build from source, also install build-base and python3. For headless use (see below), install Xvfb and Mesa with apk add xvfb-run mesa-gl mesa-dri-gallium.

Windows

Prebuilt binaries (x64) need nothing else. ANGLE's Direct3D 11 backend compiles shaders with d3dcompiler_47.dll, which Windows 10 and later include.

To build from source, install Python 3 and Visual Studio with the "Desktop development with C++" workload.

FAQ

How can I use headless-gl with a continuous integration service?

On macOS and Windows, headless-gl should work out of the box, as it does on GitHub Actions' macOS and Windows runners. On Linux, it needs an X server and an OpenGL driver, such as Xvfb and Mesa, as described below. For an example, see the ci workflow, which runs this repository's tests on GitHub Actions.

How can headless-gl be used on a headless Linux machine?

On Linux, headless-gl needs an X server and an OpenGL driver, which a minimal Linux server, such as a cloud instance or a container, usually doesn't have. Two packages provide them:

  1. Xvfb is a lightweight X11 server which provides a back buffer for displaying X11 applications offscreen and reading back the pixels which were drawn offscreen. It is typically used in continuous integration systems.
  2. Mesa is the reference open source implementation of OpenGL. Its llvmpipe driver renders in software, which a machine without a GPU needs.

Installing Xvfb installs Mesa too:

  • Debian and Ubuntu: apt-get install -y xvfb xauth
  • RHEL, Fedora and Amazon Linux: dnf install -y xorg-x11-server-Xvfb
  • Alpine: apk add xvfb-run mesa-gl mesa-dri-gallium

Interacting with Xvfb requires you to start it in the background and to execute your node program with the DISPLAY environment variable set to whatever was configured when running Xvfb (the default being :99). If you want to do that reliably you'll have to start Xvfb as a service at boot time, which is extra configuration burden. Fortunately there is a wrapper script shipped with Xvfb known as xvfb-run which can start Xvfb on the fly, execute your Node.js program and finally shut Xvfb down. Here's how to run it:

xvfb-run -s "-ac -screen 0 1280x1024x24" <node program>

In a Docker container, start the container with docker run --init, or init: true in Docker Compose, since xvfb-run waits forever when it's the container's first process.

Does headless-gl work in a browser?

Yes, with browserify. The STACKGL_destroy_context and STACKGL_resize_drawingbuffer extensions are emulated as well.

In a browser, createGL creates a canvas and calls its getContext, and throws the same errors as in Node. The reason in the error message comes from the browser, which often gives little or no detail.

How are <img> and <video> elements implemented?

They aren't. Texture uploads take a typed array, an object with pixel data like ImageData, or an object with a 2D context like a node-canvas Canvas. The RGBA pixels of the last two are converted to the upload's format and type, as browsers convert images. In WebGL2 contexts, an upload's width and height, with UNPACK_SKIP_PIXELS and UNPACK_SKIP_ROWS, select a rectangle of the image, and 3D uploads take slices of it, as in browsers. To help reading and saving images, you should check out the following modules:

Which typed arrays can pixel data be in?

As in browsers, texImage2D, texSubImage2D, texImage3D, texSubImage3D and readPixels take pixel data in the kind of typed array that their type needs, and set INVALID_OPERATION for any other kind, or for a DataView:

type Typed array
UNSIGNED_BYTE Uint8Array, such as a Node Buffer, or Uint8ClampedArray
FLOAT Float32Array
HALF_FLOAT and HALF_FLOAT_OES Uint16Array, with each half float's 16 bits
UNSIGNED_SHORT and UNSIGNED_SHORT_* Uint16Array
UNSIGNED_INT and UNSIGNED_INT_* Uint32Array
BYTE, SHORT and INT (WebGL2) Int8Array, Int16Array and Int32Array

Anything else, such as an ArrayBuffer or a plain array, throws a TypeError. The exception is WebGL2's forms without a srcOffset or dstOffset argument, which convert it to a byte offset into the bound pixel buffer.

What extensions are supported?

Supported extensions by WebGL version. "Built in" means WebGL2 has the feature itself, so getExtension returns null for that extension on a WebGL2 context.

Extension WebGL1 WebGL2
STACKGL_destroy_context Yes Yes
STACKGL_resize_drawingbuffer Yes Yes
EXT_color_buffer_half_float Yes Yes
EXT_depth_clamp Yes Yes
EXT_float_blend Yes Yes
EXT_texture_compression_bptc Yes* Yes*
EXT_texture_compression_rgtc Yes Yes
EXT_texture_filter_anisotropic Yes Yes
EXT_texture_mirror_clamp_to_edge Yes Yes
OES_texture_float_linear Yes Yes
WEBGL_compressed_texture_s3tc Yes Yes
WEBGL_compressed_texture_s3tc_srgb Yes Yes
WEBGL_debug_renderer_info Yes Yes
EXT_color_buffer_float No Yes
WEBGL_color_buffer_float Yes Use EXT_color_buffer_float
ANGLE_instanced_arrays Yes Built in
EXT_blend_minmax Yes Built in
EXT_frag_depth Yes Built in
EXT_sRGB Yes Built in
EXT_shader_texture_lod Yes Built in
OES_element_index_uint Yes Built in
OES_fbo_render_mipmap Yes Built in
OES_standard_derivatives Yes Built in
OES_texture_float Yes Built in
OES_texture_half_float Yes Built in
OES_texture_half_float_linear Yes Built in
OES_vertex_array_object Yes Built in
WEBGL_depth_texture Yes Built in
WEBGL_draw_buffers Yes Built in

* Not on macOS, where ANGLE's OpenGL backend has no BPTC textures.

Float textures are built into WebGL2, but rendering to them still needs EXT_color_buffer_float, or EXT_color_buffer_half_float for 16-bit float textures.

How should I set up a development environment for headless-gl?

After you have your system dependencies installed, do the following:

  1. Clone this repo: git clone git@github.com:stackgl/headless-gl.git
  2. Switch to the headless gl directory: cd headless-gl
  3. Install npm dependencies: npm install
  4. Run node-gyp to generate build scripts: npm run rebuild

Once this is done, you should be good to go! A few more things:

  • To run the test cases, use the command npm test, or execute specific tests by just running them using node.
  • On a Unix-like platform, you can do incremental rebuilds by going into the build/ directory and running make. This is way faster than running npm run rebuild each time you make a change.
  • webgl.d.ts holds WebGL declarations copied from TypeScript's DOM types. After updating the typescript dev dependency, run node scripts/webgl-types.js to copy them again.
  • When you replace the ANGLE libraries in deps/, run node scripts/angle-assets.js <release tag> <folder> with a new release tag, and commit the updated src/angle.json with the libraries. When the change reaches master, the ANGLE release workflow creates that release for source builds to download from.

Note: the @stackgl/gl-conformance dev dependency is hosted on GitHub Packages, which needs a token to install any package, even a public one. Create a personal access token (classic) with the read:packages scope, and add it to the .npmrc file in your home directory:

//npm.pkg.github.com/:_authToken=<your token>

Debugging with RenderDoc

To debug with RenderDoc, you can set the RENDERDOC_ENABLED define in the src/native/webgl.cc file.

You'll need to also install RenderDoc and attach it to the gl process when you launch your program. The easiest way to attach to the running process is to uncomment the MessageBoxA call in the src/native/webgl.cc file.

License

See LICENSES

About

πŸŽƒ Windowless WebGL for node.js

Topics

Resources

Stars

1.9k stars

Watchers

39 watching

Forks

Releases

Packages

Used by

Contributors

Languages