Command Palette

Search for a command to run...

Hectal
Course outline
Topic 8.5Intermediate ~21 min · step 5 of 6

Picking at Scale

In one line

A ray asks every object in its list if it was hit. With 10,000 objects that is 10,000 questions per pick. You count the questions, cut them with layers and a short list, and meet GPU picking, which reads one pixel instead.

0/6 · 0%

Think of it like this

Looking for one friend at a station. Asking every person on every platform takes all day. Asking only the people at the meeting point takes seconds. And if your friend wears a bright, unique coat, you can spot them in a photo without asking anyone. Narrowing the list and picking by colour are this topic's two ideas.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

layer
One of 32 numbered groups. object.layers.enable(n) puts an object on layer n; raycaster.layers.set(n) makes the raycaster look only at layer n.
bounding volume
A simple shape (a sphere or a box) that holds a complicated object. A ray that misses it cannot hit anything inside.
BVH
Bounding volume hierarchy: boxes inside boxes, so a ray can skip whole groups of triangles at once.
GPU picking
Finding the clicked object by drawing each object in a unique flat colour and reading the colour of one pixel.

Step by step

01Count before you optimise

Count how many objects each pick asks. The example wraps Mesh.prototype.raycast for one run to count; in your own code, list.length is usually the number. If it is in the hundreds or thousands, the list is the first suspect.

02Put clickable things on a layer

part.layers.enable(1) for each part you can click. In the click handler raycaster.layers.set(1). The 9,990 pieces of scenery are not even looked at.

03Or hand over a short list

raycaster.intersectObjects(parts, false) with a prepared array of parts. Keep the array up to date when parts are added or removed.

04Only pick when it matters

Hover once per frame, and only if the pointer moved or the bike turned. Click: once per click.

Try it yourself

  1. 1

    Triple the scenery

    In the 10,000 example change i < 9990 to i < 29990. The first line becomes meshes asked: 30000, while the layer-filtered line stays at 10. The cost of picking on layer 0 grew three times and the cost on layer 1 did not change at all.

  2. 2

    enable instead of set

    Change raycaster.layers.set(PICKABLE); to raycaster.layers.enable(PICKABLE);. The second line now says meshes asked: 10000. set replaces the raycaster's layers, enable adds one, so the raycaster still sees layer 0, where all the scenery lives.

Code & diagrams

10,000 objects: how many questions does a pick ask? New tab

Ten clickable parts and 9,990 hidden pieces of scenery. The ray ignores `visible`, so hidden pieces still cost. Timings are never printed: they differ on every machine.

idle

Console

Run the scene to see its console.

Expected console output

everything, layer 0                meshes asked:  10000 | first hit: part-3
only layer 1 (the 10 parts)        meshes asked:     10 | first hit: part-3
only the list of 10 parts:         meshes asked:     10
GPU picking: one pixel tells which part New tab

Each part is drawn in a flat colour whose red value is its number; one pixel is read back.

idle

Console

Run the scene to see its console.

Expected console output

GPU pick at the bell   -> bell
GPU pick at the saddle -> saddle
GPU pick at the badge  -> badge
GPU pick at the top-left corner -> null

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

A part that is not on the layer

In the 10,000 example delete the line part.layers.enable(PICKABLE); // on layer 0 and layer 1.

terminal
$ Run
── what you'll see ──
only layer 1 (the 10 parts) meshes asked: 0 | first hit: undefined

Break #2

Leave the scene background on during GPU picking

In the GPU example delete the line scene.background = null; inside gpuPick (keep the line that restores it afterwards).

terminal
$ Run
── what you'll see ──
GPU pick at the top-left corner -> bell
Empty space now picks the first part, with no error.

Myth vs fact

Myth

Raycasting is slow, so avoid it.

Fact

A pick against a short list of ordinary meshes costs very little. It becomes slow only when the list or the triangle count is large. Measure, then narrow the list.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Mesh.raycast builds geometry.boundingSphere on first use, and on skinned or morphed meshes it must also apply the pose, which makes picking them much more expensive than static meshes. Pick against a proxy for those.

  • ▸

    GPU picking needs a render target that is not antialiased and a material that outputs the ID exactly: no lights, no fog, no tone mapping, no transparency. Objects with alphaTest or transparent need their own ID materials. Instanced meshes need the instance number written into the colour as well.

Remember this

  1. 1

    The problem this solves: the showroom has nine parts and picks instantly. A shop with 10,000 products, or a model with a million triangles, makes the same pick slow, and hover repeats it every frame. The page just stutters whenever the mouse moves.

  2. 2

    What a pick costs. For each object in the list, raycast first tests a bounding sphere, then, if the ray gets close, every triangle of the mesh. So cost grows with the number of objects in the list and with the triangles in those the ray passes near. An InstancedMesh still tests every instance.

  3. 3

    Measure it honestly. Wrap the call in performance.now() and repeat it many times (a single pick is too short to read). Timings change with every machine, so the example prints what does not change: how many objects the ray tested. Read timings in your own DevTools.

  4. 4

    Ask fewer questions. The cheapest fix is a short list: pass only the pickable objects (bike.children, not scene.children). Layers do the same without building lists: part.layers.enable(1) and raycaster.layers.set(1). A raycaster only tests objects that share a layer with it.

  5. 5

    Skip whole regions. A bounding volume hierarchy (BVH) is a tree of boxes: if the ray misses a big box it skips everything inside. The library three-mesh-bvh adds one to a geometry and makes raycasts against huge meshes much faster. It is a separate package for when measuring shows you need it; this course does not use it.

  6. 6

    GPU picking. Draw the scene a second time, with every object in its own flat colour that encodes its number, into a 1 × 1 pixel render target aimed at the pixel under the pointer (camera.setViewOffset). Read that one pixel back with readRenderTargetPixels. The cost does not depend on triangle count, but it needs an extra draw, the render must not smooth colours (no antialiasing, no blending, no tone mapping), and reading back from the GPU makes the CPU wait, so do it on click, not on every frame.

Explain it without notes

01

A scene has 10,000 meshes and the customer can click 12 of them. Describe two ways to make picking cheaper without changing what the customer sees.

02

Why can GPU picking be faster than raycasting for a huge model, and what are its costs?

Practice

01

Write pickableList(root) that returns every mesh under root whose userData.pickable is true.

02

Write idToColor(id) and colorToId(r, g, b) for GPU picking that can encode ids 1 to 16,777,215 in 3 bytes (0 means nothing).

Done when you can

  • Count how many objects a pick asks and why visible does not help

  • Use layers or a short list to cut the questions

  • Measure picking with performance.now() over many repeats, without printing the timings

  • Name three-mesh-bvh and what it speeds up