Raycasting для мыши: экранные координаты → луч

Raycasting — ключевой инструмент для взаимодействия с 3D-сценой в физическом движке Ammo.js. Он позволяет определить пересечение луча с объектами сцены, что необходимо для выбора объектов мышью, проверки коллизий или симуляции трассировки пуль и лазеров. В основе лежит преобразование 2D-координат экрана в 3D-пространство и создание физического луча, который может взаимодействовать с телами в мире Ammo.js.

Преобразование экранных координат в нормализованные координаты устройства

В WebGL и Three.js экранные координаты мыши обычно получаются в пикселях относительно окна:

const mouseX = event.clientX;
const mouseY = event.clientY;

Для raycasting необходимо перевести их в нормализованные координаты устройства (NDC, Normalized Device Coordinates), где диапазон X и Y находится в пределах [-1, 1]:

const ndcX = (mouseX / window.innerWidth) * 2 - 1;
const ndcY = -(mouseY / window.innerHeight) * 2 + 1;

Особенности:

  • X: слева направо, от -1 до 1.
  • Y: сверху вниз, от 1 до -1 (обратный порядок).
  • NDC необходимы для правильного построения луча в пространстве камеры.

Построение луча из камеры

Для генерации луча в 3D-пространстве используется матрица проекции камеры. В Three.js можно воспользоваться THREE.Vector3 и методом unproject:

const vector = new THREE.Vector3(ndcX, ndcY, 0.5);
vector.unproject(camera);  // Преобразование в мировые координаты
const origin = camera.position.clone();
const direction = vector.sub(origin).normalize();

Здесь origin — точка начала луча (обычно позиция камеры), direction — нормализованный вектор направления.

Создание луча в Ammo.js

Ammo.js использует собственные структуры для raycasting: btVector3 для векторов и btCollisionWorld::ClosestRayResultCallback для вычисления пересечений.

const from = new Ammo.btVector3(origin.x, origin.y, origin.z);
const toPoint = new THREE.Vector3();
toPoint.copy(origin).add(direction.clone().multiplyScalar(1000)); // длина луча
const to = new Ammo.btVector3(toPoint.x, toPoint.y, toPoint.z);

const rayCallback = new Ammo.ClosestRayResultCallback(from, to);
physicsWorld.rayTest(from, to, rayCallback);

Пояснение ключевых моментов:

  • from и to задают начало и конец луча в мировых координатах.
  • ClosestRayResultCallback возвращает первый объект, с которым пересекается луч.
  • Для поиска всех пересечений используется AllHitsRayResultCallback.

Обработка результатов

После выполнения rayTest можно извлечь данные о пересечении:

if (rayCallback.hasHit()) {
    const collisionPoint = rayCallback.get_hitPointWorld();
    const collisionNormal = rayCallback.get_hitNormalWorld();

    const hitPos = {
        x: collisionPoint.x(),
        y: collisionPoint.y(),
        z: collisionPoint.z()
    };
    const hitNormal = {
        x: collisionNormal.x(),
        y: collisionNormal.y(),
        z: collisionNormal.z()
    };
}

Особенности:

  • get_hitPointWorld() возвращает точку пересечения в мировых координатах.
  • get_hitNormalWorld() — нормаль поверхности, с которой произошло пересечение.
  • Эти данные можно использовать для физического взаимодействия: перемещения объектов, применения силы, отображения эффектов.

Оптимизация и тонкости

  1. Длина луча Локальная длина луча влияет на точность и производительность. Рекомендуется выбирать разумный диапазон (например, несколько сотен единиц сцены), чтобы избежать ненужных проверок на дальних объектах.

  2. Фильтрация объектов Ammo.js позволяет использовать маски коллизий для исключения нежелательных объектов:

    rayCallback.set_m_collisionFilterMask(mask);
    rayCallback.set_m_collisionFilterGroup(group);
  3. Преобразование между библиотеками Взаимодействие Three.js и Ammo.js требует точного конвертирования координат: THREE.Vector3btVector3 и наоборот.

  4. Все пересечения Если необходимо знать не только ближайший объект, можно использовать AllHitsRayResultCallback и получить массив точек, нормалей и объектов пересечения.

Пример интеграции с событием мыши

window.addEventListener('click', (event) => {
    const ndcX = (event.clientX / window.innerWidth) * 2 - 1;
    const ndcY = -(event.clientY / window.innerHeight) * 2 + 1;

    const vector = new THREE.Vector3(ndcX, ndcY, 0.5).unproject(camera);
    const origin = camera.position.clone();
    const direction = vector.sub(origin).normalize();

    const from = new Ammo.btVector3(origin.x, origin.y, origin.z);
    const toPoint = origin.clone().add(direction.multiplyScalar(1000));
    const to = new Ammo.btVector3(toPoint.x, toPoint.y, toPoint.z);

    const rayCallback = new Ammo.ClosestRayResultCallback(from, to);
    physicsWorld.rayTest(from, to, rayCallback);

    if (rayCallback.hasHit()) {
        const collisionPoint = rayCallback.get_hitPointWorld();
        console.log("Hit at:", collisionPoint.x(), collisionPoint.y(), collisionPoint.z());
    }
});

Резюме основных этапов

  1. Конвертация экранных координат мыши в NDC.
  2. Преобразование NDC в мировые координаты через камеру.
  3. Создание начала и конца луча (btVector3) для Ammo.js.
  4. Выполнение rayTest с использованием ClosestRayResultCallback или AllHitsRayResultCallback.
  5. Обработка точки пересечения и нормали для взаимодействия с объектами.

Этот процесс является фундаментальным для интерактивных приложений с физикой, обеспечивая точное попадание луча по 3D-объектам сцены и корректное взаимодействие с физическим миром.