Course outline
HTML Labels over 3D
In one line
Labels are easiest as real HTML placed over the canvas. CSS2DRenderer does the placing; project() lets you do it yourself. You handle labels behind the camera, off-screen, hidden behind the bike and far too many of them.
Think of it like this
Sticky notes on a glass display case. You stick each note on the glass where it lines up with the part behind it. Walk around the case and you have to move every note to keep it lined up. A note behind the case, or off to the side, you take off.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- CSS2DObject
- An
Object3Dthat carries an HTML element. Where the object goes in 3D, the element follows on screen. - project
- Turn a 3D world point into normalised device coordinates for a camera:
point.project(camera). - occlusion
- One object hiding another from the camera's point of view.
- pointer-events: none
- A CSS rule that makes an element ignore clicks and taps, so they reach what is underneath.
Step by step
01Add a layer on top of the canvas
Create CSS2DRenderer, set its size, and append its domElement after the canvas. Style it position: absolute; top: 0; left: 0; pointer-events: none.
02Make a label and attach it
Build a div with your text and class, wrap it in new CSS2DObject(div), set label.position (for example (0, 0.6, 0), above the object), and object.add(label).
03Render both layers every frame
renderer.render(scene, camera) then labelRenderer.render(scene, camera). Without the second call the divs are never put in the page.
04Check occlusion yourself
Shoot a ray from the camera to each label. If anything solid is nearer than the label, add a class that fades it.
Try it yourself
- 1
Move the wall in front of the lamp
In the labels example change
wall.position.set(-1.6, 0, 1);towall.position.set(1.4, 0, 1);. The output becomesWall visible,Bell visible,Headlight behind something: faded. A label fades only when something solid sits between it and the camera. - 2
Move the camera and re-read the table
In the projection example change
camera.position.set(0, 0.5, 5)tocamera.position.set(0, 0.5, 15). The names in the table describe the old camera, so read the numbers instead. The point at z = 9 is now in front of the camera, sobehind the cameraprintspixel 400 200 | x,y on screen: true | depth ok: true | show label: true.far off to the rightmoves to pixel 690 and is on screen. Onlybeyond the far planestill printsdepth ok: false, because z = −60 is now 75 away and the far plane is at 50.
Code & diagrams
The blue wall hides the ball behind it. The Bell label (the ball) fades because a ray from the camera to it hits the wall first.
Console
Run the scene to see its console.Expected console output
Wall visible
Bell behind something: faded
Headlight visible
label divs in the page: 3
a label's CSS transform is set by CSS2DRenderer: trueNo drawing here: the example is the numbers. Read the two checks separately: x and y alone say a point behind the camera is on screen.
Console
Run the scene to see its console.Expected console output
straight ahead pixel 400 200 | x,y on screen: true | depth ok: true | show label: true
high and to the right pixel 545 132 | x,y on screen: true | depth ok: true | show label: true
far off to the right pixel 1269 200 | x,y on screen: false | depth ok: true | show label: false
behind the camera pixel 400 200 | x,y on screen: true | depth ok: false | show label: false
behind, to the left pixel 641 200 | x,y on screen: true | depth ok: false | show label: false
beyond the far plane pixel 400 200 | x,y on screen: true | depth ok: false | show label: false
500 label spots -> on screen: 152 | drawn (nearest 12): 12Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Check only x and y
In the projection example change s.xyOnScreen && s.depthOk in the show function to just s.xyOnScreen.
Myth vs fact
Myth
A label that is behind a part is hidden automatically.
Fact
Nothing in three.js hides it. You raycast from the camera to the label and fade or hide it yourself, as the example does.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
CSS2DRendererwrites atransformper label per frame. That is cheaper than moving elements withleftandtop, but it is still work for the browser. For labels that never move relative to the object, a drawn sprite is cheaper, at the cost of blur and no text for assistive technology. - ▸
Labels are real DOM, so they can carry
aria-labels, but a label that moves under a screen reader cursor is hard to follow. The build keeps part names in a static list of buttons (Topics 8.4 and 8.6) and adds no floating labels.
Remember this
- 1
The problem this solves: text drawn inside 3D (on a sprite or a texture) is blurry, hard to style, and invisible to a screen reader. A normal HTML element is sharp, styled with CSS, selectable and readable. The job is to keep that element on top of the right 3D point as the camera moves.
- 2
CSS2DRenderer. Import it from
three/addons/renderers/CSS2DRenderer.js. You makenew CSS2DRenderer(), callsetSize(width, height), and add itsdomElementover the canvas withposition: absoluteandpointer-events: none, so clicks pass through to the canvas. ACSS2DObject(element)wraps an HTML element and is added to anyObject3D, so the label follows that object. Each frame,labelRenderer.render(scene, camera)moves every element with a CSStransform. The elements join the page only whenrenderruns. - 3
What CSS2DRenderer does and does not do. It hides a label whose depth is outside −1 … 1 (behind the camera, or beyond the far plane) and orders them front to back with
z-index. It does not hide labels hidden behind other objects. CalllabelRenderer.setSizeon resize, next to the main renderer. - 4
Projecting by hand.
point.clone().project(camera)runs the point through the camera's view and projection matrices (Topic 2.4's spaces) and returns NDC. Convert to pixels withx = (ndc.x * 0.5 + 0.5) * widthandy = (−ndc.y * 0.5 + 0.5) * height. - 5
The behind-the-camera trap. A point behind the camera still gets an x and y, mirrored through the middle, and sometimes they are inside the screen. Checking only
|x| ≤ 1and|y| ≤ 1shows a label in the middle of the screen for something behind you. Also check the depth:ndc.zmust be between −1 and 1. - 6
Hidden behind the bike. Neither method knows about occlusion. To fade a label whose point is behind a part, raycast from the camera to the label's position and set
raycaster.farto that distance: any hit closer than the label means it is hidden (Topic 8.1). Anchor a label above its own object, not inside it, or the label is hidden by its own part.
Explain it without notes
Why can a label for a point behind the camera still appear in the middle of the screen if you only check x and y after project()?
Why does the showroom's label layer need pointer-events: none?
Practice
Write labelPosition(point, camera, width, height) that returns { x, y } in pixels, or null if the point is off screen or behind the camera.
Write nearestLabels(points, camera, count) that keeps only the count nearest points that are in front of the camera.
Done when you can
Overlay HTML on the canvas with
CSS2DRendererandpointer-events: noneProject a point to pixels by hand and flip y
Reject points behind the camera by their depth, not by x and y
Hide a label behind a part with a ray whose
faris the label's distance