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.
// 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] + ' ')
}
}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.
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.
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)widthis the width of the drawing bufferheightis the height of the drawing buffercontextAttributesis an optional object whose properties are the WebGL context attributes, andcreateWebGL2Context, which creates a WebGL2 context when it'strue. Onlyalpha,depthandstencilchange the drawing buffer, andantialiasis always off. Nothing composites the drawing buffer in Node, so it's always preserved whateverpreserveDrawingBufferis. The rest, such aspremultipliedAlpha, only change whatgetContextAttributesreports.
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 WebGL2RenderingContextThey can't be constructed directly.
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.
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:
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.
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)[NoInterfaceObject]
interface STACKGL_resize_drawingbuffer {
void resize(GLint width, GLint height);
};
Resizes the drawing buffer of a WebGL rendering context
widthis the new width of the drawing buffer for the contextheightis the new height of the drawing buffer for the 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().
const gl = require('gl')(10, 10)
const ext = gl.getExtension('STACKGL_destroy_context')
ext.destroy()[NoInterfaceObject]
interface STACKGL_destroy_context {
void destroy();
};
Immediately destroys the context and all associated resources.
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.
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.
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.
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.
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.
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.
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.
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:
- 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.
- 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.
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.
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:
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.
Supported extensions by WebGL version. "Built in" means WebGL2 has the feature itself, so getExtension returns null for that extension on a WebGL2 context.
* 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.
After you have your system dependencies installed, do the following:
- Clone this repo:
git clone git@github.com:stackgl/headless-gl.git - Switch to the headless gl directory:
cd headless-gl - Install npm dependencies:
npm install - 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 usingnode. - On a Unix-like platform, you can do incremental rebuilds by going into the
build/directory and runningmake. This is way faster than runningnpm run rebuildeach time you make a change. webgl.d.tsholds WebGL declarations copied from TypeScript's DOM types. After updating thetypescriptdev dependency, runnode scripts/webgl-types.jsto copy them again.- When you replace the ANGLE libraries in
deps/, runnode scripts/angle-assets.js <release tag> <folder>with a new release tag, and commit the updatedsrc/angle.jsonwith 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>
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.
See LICENSES