Anchor Screen-Space UI to Animated glTF Nodes
Anchoring screen-space user interface (UI) overlays to animated glTF nodes requires extracting dynamic 3D world coordinates from the scene hierarchy, projecting those points through the active camera's view-projection matrix, and mapping the resulting Normalized Device Coordinates (NDC) to 2D viewport pixel space. This process bridges WebGL or native 3D render loops with 2D DOM or canvas overlays, ensuring that labels, health bars, and indicators track moving skeletal meshes and rigid objects smoothly without latency, jitter, or depth-sorting errors.
Extracting Animated World Transforms
In a glTF asset, animations target local transform properties (translation, rotation, and scale) of individual nodes. To track a specific part of a model, such as a character's head or a vehicle's attachment point, you must target the specific node or bone driving that component.
Before reading spatial coordinates, the engine must evaluate animations for the current frame and update the entire transformation hierarchy. Reading positions before this update causes the UI to lag one frame behind the geometry. Once the skeletal or node animation updates, query the node’s absolute world transform matrix rather than its local matrix:
- Update the scene graph hierarchy and skinning animations.
- Read the 4x4 world matrix of the target node or joint.
- Extract the translation vector from the fourth column (or apply the
matrix to a local offset vector, such as
(0, height, 0), to float the UI above the target).
Projecting 3D Coordinates to 2D Screen Space
Once you have the 3D world coordinates \((X_w, Y_w, Z_w)\), convert them into screen-space pixels using the camera’s transformation pipeline:
- View Transformation: Multiply the world position by the camera's view matrix to obtain view-space (camera-relative) coordinates.
- Projection Transformation: Multiply by the camera's projection matrix to produce clip-space coordinates \((X_c, Y_c, Z_c, W_c)\).
- Perspective Division: Divide \(X_c\), \(Y_c\), and \(Z_c\) by \(W_c\) to obtain Normalized Device Coordinates (NDC), where values fall within the range \([-1, 1]\).
- Viewport Mapping: Convert NDC to actual screen
pixels based on your rendering canvas dimensions:
Pixel_X = ((NDC_X + 1) / 2) * Canvas_WidthPixel_Y = ((1 - NDC_Y) / 2) * Canvas_Height
Handling Visibility, Occlusion, and Frustum Boundaries
To maintain a clean user interface, manage the visibility state based on camera orientation and scene geometry:
- Behind-Camera Check: If the clip-space component
\(W_c \le 0\) or the NDC Z coordinate
falls outside the range \([-1, 1]\) (in
standard OpenGL conventions) or \([0,
1]\) (in DirectX/WebGPU conventions), the node is behind the
camera. Instantly hide the overlay by setting its CSS
displayorvisibilityproperty. - Geometric Occlusion: If an animated node moves behind another mesh, the overlay remains visible unless occluded. To prevent this, cast a ray from the camera position toward the target node's world position. If the ray hits another opaque surface prior to reaching the target distance, flag the UI element as hidden or reduce its opacity.
Optimizing UI Rendering Performance
Updating DOM elements through standard properties like
top and left triggers expensive browser layout
recalculations and repaints. Instead, use hardware-accelerated CSS
transforms:
transform: translate3d(calc(var(--x) * 1px), calc(var(--y) * 1px), 0);Execute the position updates within the primary
requestAnimationFrame loop immediately after the 3D scene
renders. This synchronous execution eliminates visual tearing between
the 3D model rendered on the GPU and the screen-space overlay rendered
by the browser compositor.