Title: Three.js Picking Description: Selecting Objects with the Mouse in Three.js TOC: Picking Objects with the mouse *Picking* refers to the process of figuring out which object a user clicked on or touched. There are tons of ways to implement picking each with their tradeoffs. Let's go over the 2 most common. Probably the most common way of *picking* is by doing raycasting which means to *cast* a ray from the mouse through the frustum of the scene and computing which objects that ray intersects. Conceptually it's very simple. First we'd take the position of the mouse. We'd convert that into world space by applying the camera's projection and orientation. We'd compute a ray from the near plane of the camera's frustum to the far plane. Then, for every triangle of every object in the scene we'd check if that ray intersects that triangle. If your scene has 1000 objects and each object has 1000 triangles then 1 million triangles will need to be checked. A few optimizations would include first checking if the ray intersects with an object's bounding sphere or bounding box, the sphere or box that contains the entire object. If the ray doesn't intersect one of those then we don't have to check the triangles of that object. THREE.js provides a `RayCaster` class that does exactly this. Let's make a scene with a 100 objects and try picking them. We'll start with an example from [the article on responsive pages](threejs-responsive.html) A few changes We'll parent the camera to another object so we can spin that other object and the camera will move around the scene just like a selfie stick. ```js *const fov = 60; const aspect = 2; // the canvas default const near = 0.1; *const far = 200; const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); *camera.position.z = 30; const scene = new THREE.Scene(); +scene.background = new THREE.Color('white'); +// put the camera on a pole (parent it to an object) +// so we can spin the pole to move the camera around the scene +const cameraPole = new THREE.Object3D(); +scene.add(cameraPole); +cameraPole.add(camera); ``` and in the `render` function we'll spin the camera pole. ```js cameraPole.rotation.y = time * .1; ``` Also let's put the light on the camera so the light moves with it. ```js -scene.add(light); +camera.add(light); ``` Let's generate 100 cubes with random colors in random positions, orientations, and scales. ```js const boxWidth = 1; const boxHeight = 1; const boxDepth = 1; const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth); function rand(min, max) { if (max === undefined) { max = min; min = 0; } return min + (max - min) * Math.random(); } function randomColor() { return `hsl(${rand(360) | 0}, ${rand(50, 100) | 0}%, 50%)`; } const numObjects = 100; for (let i = 0; i < numObjects; ++i) { const material = new THREE.MeshPhongMaterial({ color: randomColor(), }); const cube = new THREE.Mesh(geometry, material); scene.add(cube); cube.position.set(rand(-20, 20), rand(-20, 20), rand(-20, 20)); cube.rotation.set(rand(Math.PI), rand(Math.PI), 0); cube.scale.set(rand(3, 6), rand(3, 6), rand(3, 6)); } ``` And finally let's pick. Let's make a simple class to manage the picking ```js class PickHelper { constructor() { this.raycaster = new THREE.Raycaster(); this.pickedObject = null; this.pickedObjectSavedColor = 0; } pick(normalizedPosition, scene, camera, time) { // restore the color if there is a picked object if (this.pickedObject) { this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor); this.pickedObject = undefined; } // cast a ray through the frustum this.raycaster.setFromCamera(normalizedPosition, camera); // get the list of objects the ray intersected const intersectedObjects = this.raycaster.intersectObjects(scene.children); if (intersectedObjects.length) { // pick the first object. It's the closest one this.pickedObject = intersectedObjects[0].object; // save its color this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex(); // set its emissive color to flashing red/yellow this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000); } } } ``` You can see we create a `RayCaster` and then we can call the `pick` function to cast a ray through the scene. If the ray hits something we change the color of the first thing it hits. Of course we could call this function only when the user pressed the mouse *down* which is probably usually what you want but for this example we'll pick every frame whatever is under the mouse. To do this we first need to track where the mouse is ```js const pickPosition = {x: 0, y: 0}; clearPickPosition(); ... function getCanvasRelativePosition(event) { const rect = canvas.getBoundingClientRect(); return { x: (event.clientX - rect.left) * canvas.width / rect.width, y: (event.clientY - rect.top ) * canvas.height / rect.height, }; } function setPickPosition(event) { const pos = getCanvasRelativePosition(event); pickPosition.x = (pos.x / canvas.width ) * 2 - 1; pickPosition.y = (pos.y / canvas.height) * -2 + 1; // note we flip Y } function clearPickPosition() { // unlike the mouse which always has a position // if the user stops touching the screen we want // to stop picking. For now we just pick a value // unlikely to pick something pickPosition.x = -100000; pickPosition.y = -100000; } window.addEventListener('mousemove', setPickPosition); window.addEventListener('mouseout', clearPickPosition); window.addEventListener('mouseleave', clearPickPosition); ``` Notice we're recording a normalized mouse position. Regardless of the size of the canvas we need a value that goes from -1 on the left to +1 on the right. Similarly we need a value that goes from -1 on the bottom to +1 on the top. While we're at it lets support mobile as well ```js window.addEventListener('touchstart', (event) => { // prevent the window from scrolling event.preventDefault(); setPickPosition(event.touches[0]); }, {passive: false}); window.addEventListener('touchmove', (event) => { setPickPosition(event.touches[0]); }); window.addEventListener('touchend', clearPickPosition); ``` And finally in our `render` function we call call the `PickHelper`'s `pick` function. ```js +const pickHelper = new PickHelper(); function render(time) { time *= 0.001; // convert to seconds; ... + pickHelper.pick(pickPosition, scene, camera, time); renderer.render(scene, camera); ... ``` and here's the result {{{example url="../threejs-picking-raycaster.html" }}} This appears to work great and it probably does for many use cases but there are several issues. 1. It's CPU based. JavaScript is going through each object and checking if the ray intersects that object's bounding box or bounding sphere. If it does then JavaScript has to go through each and every triangle in that object and check if the ray intersects the triangle. The good part of this is JavaScript can easily compute exactly where the ray intersected the triangle and provide us with that data. For example if you wanted to put a marker where the intersection happened. The bad part is that's a lot of work for the CPU to do. If you have objects with lots of triangles it might be slow. 2. It doesn't handle any strange shaders or displacements. If you have a shader that deforms or morphs the geometry JavaScript has no knowledge of that deformation and so will give the wrong answer. For example AFAIK you can't use this method with skinned objects. 3. It doesn't handle transparent holes. As an example let's apply this texture to the cubes.