Производительность raycast: частота вызовов и оптимизация

Raycasting в контексте физического движка Ammo.js представляет собой процесс проверки пересечения луча с телами физического мира. Этот инструмент используется для обнаружения столкновений, реализации прицеливания, сенсоров и AI-наблюдения. Производительность raycast напрямую зависит от частоты вызовов и числа объектов в сцене, поэтому грамотная оптимизация критически важна для поддержания высокой частоты кадров.


Основы raycast в Ammo.js

Ammo.js является портом C++-библиотеки Bullet Physics на JavaScript через Emscripten. В ней существуют несколько методов для выполнения raycast:

  1. rayTest(from, to, resultCallback) – базовый метод, проверяющий пересечение луча с телами сцены.
  2. rayTestAll(from, to, resultCallback) – возвращает все столкновения вдоль луча, а не только первое.
  3. ClosestRayResultCallback(from, to) – структура, в которую записывается информация о ближайшем пересечении.
  4. AllHitsRayResultCallback(from, to) – структура для хранения всех точек пересечения луча с телами.

Ключевые моменты для производительности:

  • Лучи создаются из объектов btVector3. Их часто пересоздают на каждый кадр, что увеличивает нагрузку на сборщик мусора. Оптимально использовать повторно объекты btVector3 и очищать их вручную через btVector3.prototype.delete().
  • Колбэки ClosestRayResultCallback и AllHitsRayResultCallback выделяют память в WebAssembly, что также может замедлять работу при высокой частоте вызовов. Рекомендуется реиспользовать объекты callback, если лучи вызываются каждый кадр.

Частота вызовов и её влияние

Частота вызовов raycast напрямую влияет на нагрузку на CPU:

  • Каждый кадр (60 fps) – если выполнять по 50–100 лучей за кадр, это может создать заметное падение FPS на сложных сценах.
  • Прерывание проверки на основе таймера – разумно запускать raycast с меньшей частотой для менее критичных проверок (например, каждые 2–3 кадра для AI или сенсоров).
  • Делегирование тяжелых проверок – можно выполнять длинные или сложные raycast на Web Worker, освобождая основной поток от тяжелых операций.

Пример подхода с частотой:

let frameCount = 0;
const rayInterval = 3; // Проверять каждые 3 кадра

function updatePhysics() {
    frameCount++;
    if (frameCount % rayInterval === 0) {
        performRaycast();
    }
}

Такой метод позволяет снизить количество операций на 60–70%, сохранив функциональность.


Оптимизация по числу объектов

Производительность raycast зависит не только от частоты вызовов, но и от числа тел в физическом мире:

  • Использование фильтров масок столкновений (collisionFilterMask) позволяет ограничить проверку только к релевантным объектам. Например, луч AI против стен и препятствий, но не против всех динамических объектов.
  • Секторизация мира – разбиение сцены на зоны и проверка raycast только в активной зоне. Для больших сцен это снижает число проверок линейно.
  • Использование AABB (Axis-Aligned Bounding Box) тестов перед raycast – сначала проверяется, пересекает ли луч bounding box объектов, а затем выполняется точный raycast.

Влияние типов raycast

ClosestRayResultCallback быстрее, чем AllHitsRayResultCallback, поскольку возвращает только ближайшее пересечение и не собирает массив точек. Для большинства игровых сценариев достаточно ближайшего столкновения.

AllHitsRayResultCallback следует использовать только при необходимости анализа всех пересечений вдоль луча, например, для лазерных сенсоров или визуализации сканирования.


Практические советы по оптимизации

  1. Пул объектов btVector3 и callback – повторное использование объектов предотвращает частую аллокацию памяти.
  2. Ограничение длины луча – короткий луч обрабатывается быстрее, особенно в сценах с большим количеством тел.
  3. Реализация раннего выхода – проверка ближайших объектов или фильтров перед полной проверкой луча.
  4. Объединение нескольких raycast – если нужно проверять группу точек, иногда выгодно использовать single broadphase check и минимизировать количество отдельных вызовов.
  5. Асинхронное вычисление – использование requestIdleCallback или Web Worker для тяжелых raycast позволяет избежать просадок FPS.

Примеры производительного raycast

// Инициализация пулов
const fromVec = new Ammo.btVector3();
const toVec = new Ammo.btVector3();
const rayCallback = new Ammo.ClosestRayResultCallback(fromVec, toVec);

function performRaycast() {
    fromVec.setValue(playerX, playerY, playerZ);
    toVec.setValue(targetX, targetY, targetZ);

    rayCallback.set_m_collisionFilterMask(WALL_MASK | ENEMY_MASK);

    physicsWorld.rayTest(fromVec, toVec, rayCallback);

    if (rayCallback.hasHit()) {
        const hitPoint = rayCallback.get_m_hitPointWorld();
        // обработка столкновения
    }
}

Этот пример демонстрирует минимизацию аллокаций, фильтрацию по маске столкновений и использование пулов для объектов.


Итоговое влияние на FPS

При правильной оптимизации raycast можно выполнять десятки и сотни проверок за кадр без значительного падения производительности. Ключевые факторы:

  • Повторное использование объектов Ammo.js.
  • Фильтрация по маскам столкновений.
  • Снижение частоты вызовов для менее критичных проверок.
  • Использование ClosestRayResultCallback вместо AllHitsRayResultCallback там, где возможно.

Комплексное применение этих техник позволяет сохранять высокую производительность даже в сценах с большим количеством динамических объектов.