Casting Rays from the Mouse

raycaster.setFromCamera(ndc, camera) builds a world-space ray from the pointer in normalized device coordinates, -1 to 1 with y up (Screen to Clip Space), and intersectObjects(list) returns every hit on the list and its descendants, nearest first. bookshelf.js wraps the conversion for every handler:

bookshelf.js: aimRay(), from a pointer event to a rayJavaScript
export function aimRay(raycaster, { clientX, clientY, target }, camera) {
  const r = target.getBoundingClientRect();
  const ndc = new THREE.Vector2((clientX - r.left) / r.width * 2 - 1,
    1 - (clientY - r.top) / r.height * 2);
  raycaster.setFromCamera(ndc, camera);
  return raycaster;
}

A hit holds the object, its distance, the world point, the face (with normal and materialIndex), the interpolated uv and, on instanced meshes, instanceId. Cast after a first frame (or scene.updateMatrixWorld()): until then every world matrix is the identity and rays miss. For big scenes, raycaster.layers narrows the test and three-mesh-bvh 3,495 (github.com/gkjohnson/three-mesh-bvh (https://github.com/gkjohnson/three-mesh-bvh 3,495 ), MIT) adds a bounding-volume hierarchy.

A ray from the pointer through the camera: the hit point, face normal, uv and distanceHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f5f1e8; font: 12px system-ui, sans-serif; color: #263238; }
  canvas { display: block; max-width: 100%; }
  pre { margin: 4px 8px; min-height: 3em; }
</style>
<script type="importmap">
{ "imports": {
  "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<pre id="out">Move the pointer over the books.</pre>
<script type="module">
import * as THREE from 'three';

const BOOKS = [['The Quiet Harbor', '#1f5f8b'], ['Patterns of the Deep Web', '#5b3f99'], ['Salt and Saffron', '#e09a10'],
  ['Small Steps to Big Summits', '#3f7d3a'], ["The Clockmaker's Paradox", '#b5452f'], ['Gardens in Glass', '#2a9d8f']];
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 280);
document.body.prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(35, 600 / 280, 0.1, 50);
camera.position.set(0.9, 0.7, 3.2);
camera.lookAt(0, -0.1, 0);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.2));
const sun = new THREE.DirectionalLight('#ffffff', 2.5);
sun.position.set(2, 3, 4);
scene.add(sun);
const plank = new THREE.Mesh(new THREE.BoxGeometry(3.2, 0.06, 0.4), new THREE.MeshStandardMaterial({ color: '#8d6e63' }));
plank.position.y = -0.52;
scene.add(plank);
const books = BOOKS.map(([title, color], i) => {
  const book = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.98, 0.12), new THREE.MeshStandardMaterial({ color }));
  book.name = title;
  book.position.x = (i - 2.5) * 0.5;
  book.rotation.y = (i % 2 ? 0.25 : -0.25);
  scene.add(book);
  return book;
});

// Markers: a dot at the hit point and an arrow along the face normal
const dot = new THREE.Mesh(new THREE.SphereGeometry(0.035), new THREE.MeshBasicMaterial({ color: '#263238' }));
const arrow = new THREE.ArrowHelper(new THREE.Vector3(0, 0, 1), new THREE.Vector3(), 0.5, '#263238', 0.12, 0.07);
scene.add(dot, arrow);

// Pointer position → normalized device coordinates (-1..1, y up) → a world-space ray
const raycaster = new THREE.Raycaster();
function aimRay({ clientX, clientY }) {
  const r = renderer.domElement.getBoundingClientRect();
  const ndc = new THREE.Vector2((clientX - r.left) / r.width * 2 - 1, 1 - (clientY - r.top) / r.height * 2);
  raycaster.setFromCamera(ndc, camera);
  return raycaster;
}

const fix = (v) => v.toArray().map((n) => n.toFixed(2)).join(', ');
function onMove(event) {
  const hit = aimRay(event).intersectObjects(books)[0];   // nearest first
  dot.visible = arrow.visible = !!hit;
  if (hit) {
    dot.position.copy(hit.point);
    // face.normal is in object space: turn it into world space
    const normal = hit.face.normal.clone().transformDirection(hit.object.matrixWorld);
    arrow.position.copy(hit.point);
    arrow.setDirection(normal);
    document.getElementById('out').textContent =
      `${hit.object.name}\ndistance ${hit.distance.toFixed(2)} · point (${fix(hit.point)}) · ` +
      `normal (${fix(normal)}) · uv (${fix(hit.uv)})`;
  }
  renderer.render(scene, camera);
}
renderer.domElement.addEventListener('pointermove', onMove);

renderer.render(scene, camera);          // a first frame sets the world matrices, or rays miss
const r = renderer.domElement.getBoundingClientRect();
onMove({ clientX: r.left + 330, clientY: r.top + 120 });   // a simulated pointer position
</script>