Three.js HUD Tutorial: renderOrder and depthTest
Creating a Heads-Up Display (HUD) in Three.js requires rendering 2D
or 3D overlay elements on top of a 3D scene without them clipping into
world geometry. This article explains how to utilize the
renderOrder property alongside depthTest to
construct an interactive, high-performance HUD layer within a single
Three.js scene and render loop.
The Core Concept: Depth Testing and Render Order
By default, Three.js uses the WebGL depth buffer to determine which
objects are in front of others based on their actual 3D distance from
the camera. For a HUD, we must override this behavior so that HUD
elements always draw on top of the 3D world, regardless of their
physical coordinate positions. This is achieved using two properties:
Material.depthTest and
Object3D.renderOrder.
1. Disabling Depth Testing
The depthTest property determines whether an object is
rendered based on the depth buffer. Setting this to false
ensures that the HUD object is drawn without checking if other 3D
objects are physically in front of it.
const hudMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
depthTest: false,
depthWrite: false // Prevents this object from writing to the depth buffer
});Setting depthWrite to false is also highly
recommended. It ensures that the HUD element does not block other HUD
elements from rendering correctly if they overlap.
2. Controlling the Render Queue with renderOrder
Disabling depthTest is only half the solution. If the
GPU renders the HUD element before it renders a background
wall, the wall will overwrite the HUD pixel data. To guarantee that the
HUD is drawn last, you must manipulate the renderOrder
property.
By default, all Object3D instances have a
renderOrder of 0. Three.js sorts and renders
objects with lower renderOrder values first. By assigning a
high positive integer to your HUD elements, you force Three.js to render
them after the rest of the 3D scene has been drawn.
const hudGeometry = new THREE.PlaneGeometry(2, 0.5);
const hudMesh = new THREE.Mesh(hudGeometry, hudMaterial);
// Force the HUD mesh to render after the 3D world scene
hudMesh.renderOrder = 999;
// Position the HUD relative to the camera
hudMesh.position.set(0, 2, -5);
camera.add(hudMesh); // Parent to camera so it moves with the viewCreating the Interactive HUD Layer
To build a fully functioning interactive HUD, follow these structural steps:
Step 1: Parent the HUD to the Camera
To keep the HUD locked to the screen, add the HUD elements directly
as children of the PerspectiveCamera or
OrthographicCamera.
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
scene.add(camera); // Add camera to scene to allow camera children to renderStep 2: Define HUD Elements
Create your HUD meshes, apply materials with disabled depth testing,
and assign a high renderOrder.
const buttonGeo = new THREE.RingGeometry(0.2, 0.3, 32);
const buttonMat = new THREE.MeshBasicMaterial({
color: 0x00ff00,
depthTest: false,
depthWrite: false
});
const hudButton = new THREE.Mesh(buttonGeo, buttonMat);
hudButton.position.set(-1.5, 1, -3); // Position in camera space
hudButton.renderOrder = 1000;
camera.add(hudButton);Step 3: Handling Raycasting for Interactivity
Because the HUD elements are physically positioned in 3D space, standard Three.js raycasting can be used to handle clicks or hover states. However, because the HUD elements are rendered on top but may physically reside behind world geometry, you must filter your raycast results.
When raycasting, prioritize the HUD layer by checking intersections against your HUD objects first.
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener('pointerdown', (event) => {
// Normalize mouse coordinates
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// Cast specifically against HUD elements first
const hudIntersects = raycaster.intersectObjects([hudButton]);
if (hudIntersects.length > 0) {
console.log("HUD Button Clicked!");
// Trigger button action
hudButton.material.color.setHex(0xffffff);
} else {
// Perform standard world scene raycasting if no HUD elements were clicked
const worldIntersects = raycaster.intersectObjects(scene.children, true);
if (worldIntersects.length > 0) {
console.log("World Object Clicked!");
}
}
});Using renderOrder and depthTest: false is a
clean, single-renderer solution for creating interactive HUDs without
the performance overhead of managing a second overlay scene and a
separate camera.