Профилирование физики в DevTools

Для работы с физикой в браузере используется библиотека Ammo.js, которая представляет собой порт Bullet Physics на JavaScript с использованием WebAssembly. Инициализация начинается с загрузки модуля и создания основных объектов физического мира:

import Ammo from 'ammo.js';

let physicsWorld;

Ammo().then(function(AmmoLib) {
    const collisionConfiguration = new AmmoLib.btDefaultCollisionConfiguration();
    const dispatcher = new AmmoLib.btCollisionDispatcher(collisionConfiguration);
    const overlappingPairCache = new AmmoLib.btDbvtBroadphase();
    const solver = new AmmoLib.btSequentialImpulseConstraintSolver();

    physicsWorld = new AmmoLib.btDiscreteDynamicsWorld(
        dispatcher,
        overlappingPairCache,
        solver,
        collisionConfiguration
    );
    physicsWorld.setGravity(new AmmoLib.btVector3(0, -9.81, 0));
});

Ключевые элементы:

  • btDefaultCollisionConfiguration – определяет настройки обработки столкновений.
  • btCollisionDispatcher – отвечает за обработку столкновений между объектами.
  • btDbvtBroadphase – оптимизирует поиск потенциальных столкновений.
  • btSequentialImpulseConstraintSolver – решает физические ограничения и силы.
  • btDiscreteDynamicsWorld – основной контейнер физического мира, где происходит симуляция.

Создание физических объектов

Все объекты в мире Ammo.js делятся на три типа: статические, динамические и кинетические. Создание объекта включает определение формы, массы и начальной позиции:

const boxShape = new Ammo.btBoxShape(new Ammo.btVector3(1, 1, 1));
const mass = 1;
const startTransform = new Ammo.btTransform();
startTransform.setIdentity();
startTransform.setOrigin(new Ammo.btVector3(0, 10, 0));

const localInertia = new Ammo.btVector3(0, 0, 0);
boxShape.calculateLocalInertia(mass, localInertia);

const motionState = new Ammo.btDefaultMotionState(startTransform);
const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, boxShape, localInertia);
const body = new Ammo.btRigidBody(rbInfo);

physicsWorld.addRigidBody(body);

Важно учитывать:

  • Масса = 0 создаёт статический объект.
  • calculateLocalInertia определяет распределение массы для динамических тел.
  • btDefaultMotionState позволяет синхронизировать позицию объекта с визуализацией.

Симуляция и обновление

Симуляция происходит шагами времени, обычно в цикле requestAnimationFrame:

const deltaTime = 1 / 60;

function simulatePhysics() {
    physicsWorld.stepSimulation(deltaTime, 10);

    // Получение позиции объектов
    const transform = new Ammo.btTransform();
    body.getMotionState().getWorldTransform(transform);
    const origin = transform.getOrigin();
    console.log(`X: ${origin.x()}, Y: ${origin.y()}, Z: ${origin.z()}`);
    
    requestAnimationFrame(simulatePhysics);
}

simulatePhysics();

Параметры:

  • stepSimulation(deltaTime, maxSubSteps) – основной метод прогонки физики.
  • maxSubSteps – ограничивает внутренние итерации для точности.
  • Получение трансформа позволяет синхронизировать графику с физикой.

Профилирование производительности в DevTools

Ammo.js использует WebAssembly и активно оперирует памятью, поэтому критически важно профилировать производительность при разработке игр и сложных симуляций. Основные шаги:

1. Включение профайлинга JavaScript и WebAssembly

В Chrome DevTools:

  1. Открыть вкладку Performance.
  2. Включить опцию WebAssembly code (если доступно).
  3. Запустить запись и выполнить шаги симуляции.

Профайлер покажет распределение времени между JS вызовами, WebAssembly функциями и GC.

2. Разделение нагрузки на шаги

Важно измерять отдельно:

  • Вычисление столкновений (dispatcher).
  • Решение ограничений (solver).
  • Обновление позиций объектов.

Пример ручного тайминга:

const t0 = performance.now();
physicsWorld.stepSimulation(deltaTime, 10);
const t1 = performance.now();
console.log(`Step simulation time: ${t1 - t0} ms`);

Это помогает выявить узкие места: например, большое количество динамических объектов или сложные коллизионные формы.

3. Сбор статистики по памяти

Ammo.js использует ручное управление памятью через Ammo.destroy().

  • Проверять количество создаваемых btVector3, btTransform, btRigidBody.
  • Удалять объекты, которые больше не нужны.

Пример очистки:

physicsWorld.removeRigidBody(body);
Ammo.destroy(body);
Ammo.destroy(motionState);
Ammo.destroy(boxShape);

Это снижает нагрузку на сборщик мусора и предотвращает утечки.

4. Оптимизация через упрощение форм

Сложные треугольные меши значительно замедляют шаги симуляции. Рекомендуется:

  • Использовать convex hull или compound shapes.
  • Минимизировать количество динамических тел с большим числом точек.

5. Мониторинг FPS и времени шага

Для контроля производительности удобно выводить среднее время шага и FPS:

let lastTime = performance.now();
let frames = 0;

function fpsMonitor() {
    frames++;
    const now = performance.now();
    if (now - lastTime >= 1000) {
        console.log(`FPS: ${frames}`);
        frames = 0;
        lastTime = now;
    }
    requestAnimationFrame(fpsMonitor);
}

fpsMonitor();

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

Инструменты и дополнительные советы

  • DevTools Timeline – анализирует последовательность шагов физики.
  • Memory Snapshot – отслеживает объекты Ammo.js и потенциальные утечки.
  • WebAssembly Performance – отдельная вкладка позволяет видеть, какие функции WebAssembly занимают больше всего времени.

Практика показывает, что большинство узких мест находятся в:

  1. Массовом добавлении динамических тел за один кадр.
  2. Сложных формах с большим количеством вершин.
  3. Частых вызовах btTransform без последующего уничтожения объектов.

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