Skip to main content

glTF billboard mapping

parseGLTFBillboards() from @luma.gl/gltf resolves draft billboard attachments into props for the engine BillboardController. See scene controllers for the engine APIs.

Opt-in camera-facing traversal for a partial, full-axis profile of the draft KHR_billboard proposal. This profile supports viewDirection, up, scaleWithDistance, overlay, and the default/all-axis rotationAxis. The registry deliberately does not advertise full required-extension support.

The draft's instruction to zero facing-vector components conflicts with its Y-only rotation example. This implementation rejects constrained-axis configurations until that ambiguity is resolved. The original Khronos BillboardDemo uses an older lowercase rotationAxis: "y" spelling. Pass allowLegacyYAxis: true to accept that specific compatibility profile and preserve upright Y-axis rotation. Current draft array-constrained configurations remain rejected. The source model requires CC BY 4.0 and Apache 2.0 attribution and contains a 67.9 MB geometry buffer.

import {BillboardController} from '@luma.gl/engine';
import {parseGLTFBillboards} from '@luma.gl/gltf';

import {ModelNode} from '@luma.gl/engine';

const billboards = new BillboardController(parseGLTFBillboards(scenegraphs));

// Each frame, after animations update the ordinary node matrices:
for (const {node, modelMatrix, overlay} of billboards.getDrawEntries({viewMatrix, cameraId: 'main'})) {
if (!(node instanceof ModelNode)) continue;
// Bind modelMatrix, the corresponding projection matrix and normal matrix through the
// application's normal shader-input path. The returned matrix already includes node.matrix.
const parameters = node.model.parameters;
try {
if (overlay) node.model.setParameters({...parameters, depthCompare: 'always', depthWriteEnabled: false});
node.model.draw(renderPass);
} finally {
if (overlay) node.model.setParameters(parameters);
}
}

getDrawEntries({viewMatrix, sceneIndex, cameraId}) returns ordinary leaves in source order, followed by overlay leaves sorted back-to-front. Applications must apply the returned complete transforms to rendering and picking, and disable depth testing/writing for overlay draws. Regular scenegraph traversal continues to expose authored transforms. The controller never mutates either source glTF records or animated runtime matrices.

A billboard's world position follows its full parent chain, while parent rotation and scale are discarded for its facing transform. Its local scale remains effective. Children inherit the resolved billboard transform; nested billboards calculate their own facing transform. Invisible branches are omitted. Matrix-authored reflections use the usual determinant-based decomposition, assigning a negative scale to X when needed.

scaleWithDistance uses view-space depth magnitude relative to the first usable rendering for each named camera. Use a distinct cameraId for independently rendered cameras. Nodes absent from a traversal lose their reference depth. Call resetCamera(cameraId) when replacing a camera or detaching and reattaching a scene without an intervening traversal. A first frame at the eye plane leaves scale unchanged and defers reference capture until a usable depth is available. Crossing the eye plane does not invert scale.

destroy() clears controller-owned bindings and camera caches; GPU models and scene nodes remain application-owned. Instance-local billboarding and camera-dependent skin-joint palette updates are outside this initial profile. Runtime support for a glTF marked with required KHR_billboard remains incomplete and strict loading rejects it rather than silently dropping unsupported semantics.