Three.js From Zero · Article s7-03

S7-03 Worldspace UI

Season 7 · Article 03

Worldspace UI & Floating Labels

Labels that stick to 3D objects. Project world position to screen each frame, position a DOM element there. Depth-aware fade. Occlusion test. Five-line pattern.

1. The core pattern

const labels = [
  { obj: mesh1, el: document.getElementById('label-1'), text: 'Sphere' },
  ...
];

function tickLabels() {
  const v = new THREE.Vector3();
  for (const { obj, el } of labels) {
    obj.getWorldPosition(v);
    v.project(camera);
    const sx = (v.x * 0.5 + 0.5) * w;
    const sy = (1 - (v.y * 0.5 + 0.5)) * h;
    el.style.transform = `translate(${sx}px, ${sy}px)`;
    el.style.opacity = v.z < 1 ? '1' : '0';
  }
}

2. Depth fade

Labels far behind the camera shouldn't show. Labels behind other objects (occluded) should fade.

// Behind camera
const visible = v.z < 1 && v.z > -1;

// Occlusion: raycast from camera to label position
ray.setFromCamera(new THREE.Vector2(ndcX, ndcY), camera);
const hits = ray.intersectObjects(scene.children, true);
const occluded = hits[0]?.object !== labels[i].obj && hits[0]?.distance < objDistance;
el.style.opacity = (visible && !occluded) ? '1' : '0.3';

3. Live demo — toggleable labels

4. CSS3DRenderer alternative

If you want true 3D transforms (perspective, rotation with the scene), use CSS3DRenderer. Each label is a CSS3DObject attached to a parent mesh. Matrix3D transforms handle everything.

Downside: requires a second renderer alongside WebGL. Z-ordering between them is tricky.

5. Label clustering

When 50 labels overlap, use clustering like Google Maps:

  • Group labels within 50px of each other.
  • Show a single "+N" marker until user zooms in.
  • Rebuild clusters on camera movement (throttle — don't do every frame).

6. Leader lines

Annotation tools (CAD, medical): label sits in screen space but connects to 3D point via a line. Two parts:

  • Label DOM element at fixed screen position.
  • SVG line element updated each frame to connect (sx, sy) to projected anchor point.

7. Performance

  • 100 labels: smooth. DOM reflow is cheap for transform changes.
  • 500+: consider canvas-rendered labels or a clustering strategy.
  • Use transform: translate3d(...) (not left/top) for compositor-fast updates.

8. Takeaways

  • project(camera) + inverse-NDC = screen position.
  • Update DOM element's transform each frame.
  • v.z < 1 for "in front of camera" check.
  • Raycast against scene for occlusion.
  • Cluster at high label counts.