Пикинг объектов: Raycaster

Как понять, по какому 3D-объекту кликнул пользователь? Пустить луч из камеры через курсор. Это и есть Raycaster.

Raycaster пускает воображаемый луч в сцену и возвращает список пересечённых объектов, отсортированный по расстоянию. Так реализуют клики и наведение в 3D.

Идея луча

Экран плоский, а сцена объёмная. Когда вы кликаете в точку канваса, под ней по глубине может быть несколько объектов. Чтобы узнать, по какому именно вы попали, из камеры через позицию курсора пускают луч и смотрят, что он проткнул первым.

const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2(); // координаты мыши в NDC

window.addEventListener('click', (e) => {
  pointer.x = (e.clientX / window.innerWidth) * 2 - 1;
  pointer.y = -(e.clientY / window.innerHeight) * 2 + 1;

  raycaster.setFromCamera(pointer, camera);
  const hits = raycaster.intersectObjects(scene.children);

  if (hits.length > 0) {
    hits[0].object.material.color.set(0xff0000); // первый задетый — красный
  }
});

intersectObjects возвращает массив попаданий, уже отсортированный: hits[0] — ближайший к камере объект, по которому вы и кликнули.

Как это считается: луч и сфера

Под капотом пересечение луча с объектом — это геометрия. Покажем суть на самом наглядном случае — луч и сфера. Идея: найти, насколько близко луч проходит к центру сферы. Если это расстояние не больше радиуса — есть пересечение.

// Луч: старт origin + направление dir (единичное). Сфера: центр C, радиус R.
function raySphereHit(origin, dir, center, R) {
  // вектор от старта луча к центру сферы
  const ox = center.x - origin.x;
  const oy = center.y - origin.y;
  const oz = center.z - origin.z;
  // проекция этого вектора на направление луча
  const t = ox * dir.x + oy * dir.y + oz * dir.z;
  // ближайшая точка луча к центру
  const cx = origin.x + dir.x * t;
  const cy = origin.y + dir.y * t;
  const cz = origin.z + dir.z * t;
  // расстояние от центра до луча
  const dx = center.x - cx, dy = center.y - cy, dz = center.z - cz;
  const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
  return { dist: +dist.toFixed(3), hit: dist <= R };
}

const origin = { x: 0, y: 0, z: 0 };
const dir = { x: 0, y: 0, z: -1 }; // луч смотрит вдоль -Z

console.log('Сфера прямо по курсу:', raySphereHit(origin, dir, { x: 0, y: 0, z: -5 }, 1));
console.log('Сфера в стороне:', raySphereHit(origin, dir, { x: 3, y: 0, z: -5 }, 1));

Вывод:

Сфера прямо по курсу: { dist: 0, hit: true }
Сфера в стороне: { dist: 3, hit: false }

Луч, направленный вдоль -Z, проходит ровно через центр сферы, стоящей по курсу (расстояние 0 ≤ радиуса 1 — попадание). Сфера, сдвинутая на 3 единицы вбок, оказывается дальше радиуса — мимо. Именно такую проверку Three.js делает за вас для каждого объекта.

На практике

Сам Raycaster проверяет пересечение луча с треугольниками меша, а не только со сферой, — но принцип «насколько близко луч проходит» тот же. Используйте его для кликов по объектам, наведения (hover), выделения, простой стрельбы в играх.

Итог

  • Raycaster пускает луч из камеры через курсор и находит пересечённые объекты.
  • intersectObjects отдаёт попадания по возрастанию расстояния; первый — ближайший.
  • Суть пересечения: сравнить расстояние от объекта до линии луча с его размером.
Проверьте себя
1. Что делает Raycaster?
AЗагружает текстуры
BПускает луч (например, из камеры через курсор) и находит, какие объекты он пересекает
CСоздаёт свет
DСчитает FPS
2. Зачем нужен пикинг (raycasting) на практике?
AЧтобы менять цвет фона
BЧтобы понять, по какому 3D-объекту кликнул пользователь, и отреагировать (выделить, открыть и т.п.)
CЧтобы включить тени
DЧтобы загрузить модель
3. Луч пересекает сферу, если...
AСфера ярко окрашена
BКратчайшее расстояние от центра сферы до линии луча не больше радиуса сферы
CСфера ближе камеры
DЛуч всегда пересекает любую сферу
Поддержать проект