Headless Utils
Thumbnail renderer
Utility helper for rendering a Three.js Scene or Object3D to a thumbnail image in a headless way – no viewer UI required. Useful for:
- Generating block thumbnails for ComponentMgr palettes
- Precomputing product thumbnails on the client
- Capturing quick previews from an existing viewer/renderer
All functionality currently lives in a single export: renderThumbnail.
Overview
renderThumbnail renders a scene or object and returns either:
- a
Blob(image/png) – good for uploads / caching - a data URL (
string) – good for settingimg.src - a
HTMLCanvasElement– good if you want to keep drawing on it
There are two renderer modes:
Internal shared renderer (default)
Creates aTHREE.WebGLRendereronce and reuses it across calls. It:- Sets size and pixel ratio for you
- Configures shadow map and color space (sRGB)
- Temporarily tweaks clear color/alpha based on your options and restores them afterwards
External renderer
You pass in your ownTHREE.WebGLRenderer. The util:- Never mutates its clear color, alpha, or viewport
- Copies its
domElementinto a 2D canvas of your requested thumbnail size
Warning
- Requires a browser-like environment: throws if
documentis not available. - Designed for thumbnails and previews – not a full-frame capture/recording system.
renderThumbnail(options) API
import { renderThumbnail } from '@/utils/headless';
Renders a thumbnail and returns a Promise that resolves to a Blob, data URL string, or HTMLCanvasElement, depending on output.
Options
All options are passed via a single object:
object(THREE.Scene | THREE.Object3D, required)
Root object to render. If it is not a scene, it is cloned and added to a temporaryTHREE.Sceneso the original graph is not mutated.width/height(number, required)
Thumbnail size in pixels. Used both for:- Renderer size / viewport
- The output canvas dimensions
camera(THREE.Camera | (rootScene: THREE.Scene) => THREE.Camera, required)
Camera to render from:- Pass a concrete camera:
camera: myCamera - Or a resolver:
camera: (root) => root.getObjectByName('Camera')
If the camera has no parent yet, it is temporarily added to the root scene. If it belongs to another scene, a clone is attached instead.
- Pass a concrete camera:
output('blob' | 'dataURL' | 'canvas', default:'blob')
Determines the resolved value of the promise:'blob'→Promise<Blob>'dataURL'→Promise<string>(PNG data URL)'canvas'→Promise<HTMLCanvasElement>
renderer(THREE.WebGLRenderer, optional)
External renderer to reuse. When provided:- No clear color / alpha / viewport is changed.
- The renderer’s
domElementis drawn into an offscreen<canvas>of your requestedwidth/height.
transparent(boolean, default:true)
Controls whether the background is transparent when you do not supply abackgroundColor:- With internal renderer and no
backgroundColor:transparent: true→ clear alpha0(transparent)transparent: false→ clear alpha1(opaque)
- With internal renderer and no
preserveDrawingBuffer(boolean, default:truefor internal renderer)
Only used when the helper creates the internal renderer. This ensurestoDataURL/toBlobwork reliably. Ignored when using an external renderer.backgroundColor(THREE.Color | string | number, optional)
Background fill color:- If set, the internal renderer uses it as clear color.
- If omitted:
- And
transparent: true→ fully transparent background - And
transparent: false→ opaque background using current clear color
- And
Return type
Promise<Blob | string | HTMLCanvasElement>
Depends on the output option.
Behavior details
- If
objectis missing, it throws:renderThumbnail: "object" is required. - If
widthorheightis missing, it throws:renderThumbnail: "width" and "height" are required.
- If
documentis not available (non-browser environment), it throws:renderThumbnail: document is not available (must run in a browser environment).
Camera resolution
- If
camerais a function, it receives the root scene (THREE.Scene) and should return a camera (ornull). - If no camera can be resolved:
- Logs a warning:
renderThumbnail: no camera found for thumbnail; returning blank image. - Returns a blank canvas (transparent or filled with
backgroundColor) in the requested format instead of throwing.
- Logs a warning:
Renderer and aspect handling
Internal renderer:
- A shared
THREE.WebGLRendererinstance is created once and reused. - Size is set via
renderer.setSize(width, height, false). - If
window.devicePixelRatioexists,setPixelRatiois clamped to a max of2. - Original clear color and alpha are stored, then restored after rendering.
- For perspective cameras,
camera.aspectis temporarily set towidth / heightand then restored.
- A shared
External renderer:
- No renderer state (clear color, alpha, viewport) is touched.
- Helper simply copies
renderer.domElementinto a fresh 2D canvas at your requestedwidth/height.
Examples
Basic thumbnail as data URL
Generate a square thumbnail from a loaded GLTF scene and show it in an <img> tag:
import { renderThumbnail } from '@/utils/headless';
const thumbnailUrl = await renderThumbnail({
object: gltf.scene,
width: 512,
height: 512,
camera: (root) => root.getObjectByName('Camera'),
output: 'dataURL',
backgroundColor: '#ffffff',
transparent: false,
});
$img.src = thumbnailUrl;
Using an existing Three.js renderer
Reuse an existing viewer renderer instead of creating a new WebGL context:
import { renderThumbnail } from '@/utils/headless';
const blob = await renderThumbnail({
object: viewer.scene,
width: 256,
height: 256,
camera: viewer.camera,
renderer: viewer.renderer,
output: 'blob',
});
// Upload or cache the blob as needed
uploadThumbnail(blob);
Generating thumbnails for ComponentMgr blocks
Typical pattern: parse blocks with GltfBlockParser, then generate thumbnails for each blocksData entry using renderThumbnail.
import { GltfBlockParser } from '@/configurator';
import { renderThumbnail } from '@/utils/headless';
// After loading your GLTF
GltfBlockParser.copyExtrasToUserData(gltf.scene);
const { blocks, blocksData } = await GltfBlockParser.parse(gltf);
// Generate thumbnails in sequence (or fan out with Promise.all if you prefer)
for (const blockInfo of blocksData) {
const snappableTemplate = blocks.get(blockInfo.blockId);
if (!snappableTemplate || blockInfo.hasError) continue;
// Each Snappable exposes a root object3D you can render
const object3d = snappableTemplate.object3d;
const dataUrl = await renderThumbnail({
object: object3d,
width: 256,
height: 256,
camera: (root) => root.getObjectByName('Camera'),
output: 'dataURL',
backgroundColor: '#f5f5f5',
});
blockInfo.thumbnailUrl = dataUrl;
}
You can then feed blocksData (with attached thumbnailUrl) straight into your UI palette.