JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 73
  • Score
    100M100P100Q86802F
  • License MIT

Headless Three.js rendering for Node.js, made simple.

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.

npm version License: MIT

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 runInContext for custom loaders

Install

npm install headless-three

You also need a canvas library:

npm install skia-canvas
# or
npm install @napi-rs/canvas
# or
npm install canvas

Quick 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