Package Exports
- headless-three
Readme
headless-three
Headless Three.js rendering for Node.js, made simple. Render 3D scenes to images on the server with no browser required. Runs Three.js r162 (the last version with WebGL 1 support) without polluting the global scope.
Features
- Three.js r162 running in an isolated VM context
- No global scope pollution
- Works with any canvas library (skia-canvas, @napi-rs/canvas, canvas)
- Headless WebGL rendering via gl
- Built-in render function with multi-format output (PNG, JPEG, WebP, etc.) via sharp
- Texture loading utility
- Extensible via
runInContextfor custom loaders
Install
npm install headless-threeYou also need a canvas library:
npm install skia-canvas
# or
npm install @napi-rs/canvas
# or
npm install canvasQuick Start
import { Canvas, Image, ImageData } from "skia-canvas"
import getTHREE from "headless-three"
const { THREE, render, loadTexture } = await getTHREE({ Canvas, Image, ImageData })
// Create scene
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100)
camera.position.set(0, 0, 5)
// Add a lit cube
scene.add(new THREE.AmbientLight(0x404040))
const light = new THREE.DirectionalLight(0xffffff, 1)
light.position.set(-1, 2, 3)
scene.add(light)
// Load a texture
const texture = await loadTexture("path/to/texture.png")
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ map: texture })
)
scene.add(cube)
// Render to file
await render({
scene,
camera,
width: 512,
height: 512,
path: "output.png"
})
// Or get a PNG buffer
const buffer = await render({ scene, camera })API
getTHREE(options)
Returns a promise that resolves to { THREE, render, loadTexture, runInContext, parseColor }.
Options
| Option | Description |
|---|---|
Canvas |
Canvas class from your canvas library |
Image |
Image class from your canvas library |
ImageData |
ImageData class from your canvas library |
Returns
| Property | Description |
|---|---|
THREE |
The Three.js library object |
render(options) |
Renders a scene to an image buffer or file |
loadTexture(input) |
Creates a THREE.CanvasTexture from a Canvas, Image, ImageData, string path, or Buffer |
runInContext(code) |
Executes JavaScript code inside the VM context |
parseColor(input) |
Parses a color into { color, alpha } |
All helpers (render, loadTexture, runInContext, parseColor) are also attached to the returned THREE object under THREE.headless, so you can destructure just THREE and access them as THREE.headless.render(...) etc.
render(options)
Renders a scene to an image buffer or file. When saving to a file, the format is inferred from the extension unless format is specified. Buffer output defaults to PNG.
| Option | Default | Description |
|---|---|---|
scene |
The Three.js scene to render | |
camera |
The camera to render from | |
width |
1024 |
Output width in pixels |
height |
1024 |
Output height in pixels |
path |
If provided, saves to this file path. Format is inferred from the extension | |
format |
Output format ("png", "jpeg", "webp", "avif", "tiff", etc.). Overrides extension inference. See sharp's output docs for the full list of supported formats |
|
output |
Options passed directly to the sharp format encoder (e.g. { quality: 85, mozjpeg: true } for JPEG). See sharp's output docs for all available options per format |
|
colorSpace |
THREE.SRGBColorSpace |
Renderer output color space |
background |
transparent | Background color. Accepts any format supported by parseColor |
premultiplyAlpha |
false |
Keeps alpha premultiplied in the output image. WebGL's alpha blending produces premultiplied pixels in the framebuffer, which makes semi-transparent colors appear darker than expected when saved as PNG. The default (false) un-premultiplies them so they render correctly in image viewers. Set to true only if you need the raw premultiplied output |
// Save to file (format inferred from extension)
await render({
scene,
camera,
width: 512,
height: 512,
path: "output.png"
})
// Save as JPEG
await render({ scene, camera, path: "output.jpg" })
// Force format regardless of extension
await render({ scene, camera, path: "output.img", format: "webp" })
// Get PNG buffer (default)
const buffer = await render({ scene, camera })
// Get JPEG buffer
const buffer = await render({ scene, camera, format: "jpeg" })
// Smaller JPEG via mozjpeg encoder
await render({
scene,
camera,
path: "output.jpg",
output: { quality: 85, mozjpeg: true }
})
// Maximum PNG compression
await render({
scene,
camera,
path: "output.png",
output: { compressionLevel: 9, effort: 10 }
})loadTexture(input)
Creates a THREE.CanvasTexture from various input types:
// From a file path
const texture = await loadTexture("path/to/image.png")
// From an Image
const img = new Image()
img.src = "path/to/image.png"
const texture = await loadTexture(img)
// From a Canvas
const texture = await loadTexture(canvas)runInContext(code)
Executes code inside the same VM context as Three.js. This is useful for loading bundled Three.js addons:
import fs from "node:fs"
const { THREE, runInContext } = await getTHREE({ ... })
// Load a pre-bundled addon
runInContext(fs.readFileSync("GLTFLoader.bundle.js", "utf-8"))parseColor(input)
render()'s background option accepts many color formats (hex, arrays, objects, CSS strings, etc.). parseColor exposes the same parser so you can accept those formats in your own code without duplicating the logic.
Returns { color, alpha }, where color is a THREE.Color and alpha is a number in [0, 1].
const { THREE, parseColor } = await getTHREE({ ... })
const { color, alpha } = parseColor("rgba(255, 0, 0, 0.5)")
renderer.setClearColor(color, alpha)Supported formats
Alpha defaults to fully opaque when not specified.
| Form | Example | Notes |
|---|---|---|
| Hex number | 0xff0000 |
Treated as sRGB |
| Array | [1, 0, 0], [1, 0, 0, 0.5] |
0–1 floats, sRGB |
| Object | { r: 1, g: 0, b: 0 }, { r: 1, g: 0, b: 0, a: 0.5 } |
0–1 floats, sRGB |
| Hex string | "#rgb", "#rgba", "#rrggbb", "#rrggbbaa" |
Shorthand forms expand per channel |
rgb() / rgba() string |
"rgb(255, 0, 0)", "rgba(255, 0, 0, 0.5)" |
CSS 0–255 for r/g/b, 0–1 for alpha |
hsl() / hsla() string |
"hsl(0, 100%, 50%)", "hsla(0, 100%, 50%, 0.5)" |
CSS HSL |
| Named color | "red", "lime", "transparent" |
Any CSS named color |
THREE.Color instance |
new THREE.Color(1, 0, 0) |
Used verbatim (not re-decoded) |
All non-THREE.Color inputs are treated as sRGB values and decoded to linear via THREE.Color().setRGB(..., SRGBColorSpace) so the output matches the color you'd pick in a color picker.
How It Works
headless-three uses Node.js's vm module to create an isolated V8 context with polyfilled browser APIs (document, window, URL, etc.). Three.js's CJS build runs inside this sandbox, thinking it's in a browser. The canvas library you provide handles the actual drawing surface and image operations. Rendering uses gl for headless WebGL and sharp for image encoding.
License
MIT © Ewan Howell