Создание пользовательского MotionState на JavaScript

В библиотеке Ammo.js объект btMotionState отвечает за синхронизацию физического состояния тела с визуальным представлением. Этот механизм особенно важен для динамических тел, где физика определяет позицию и ориентацию объекта, а графический рендеринг должен корректно их отображать.

Основные принципы MotionState

btMotionState позволяет разделять логику физики и рендеринга:

  • Физическая симуляция управляет положением и вращением объекта через интеграцию силы и столкновений.
  • Графическое представление считывает данные MotionState, чтобы обновить визуальные модели.

Ammo.js предоставляет базовую реализацию btDefaultMotionState, однако для специфических задач часто создают пользовательский MotionState, наследуя его функциональность и добавляя собственную логику.

Структура пользовательского MotionState

В JavaScript пользовательский MotionState создается через объект с двумя основными методами:

  1. getWorldTransform(transform) Этот метод вызывается движком перед интеграцией симуляции, чтобы получить текущее мировое положение объекта. Внутри метода обычно обновляется экземпляр btTransform:

    const myMotionState = {
        getWorldTransform: function (worldTrans) {
            const pos = mesh.position;
            const quat = mesh.quaternion;
    
            worldTrans.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
            worldTrans.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w));
        }
    };

    Ключевой момент: метод должен корректно преобразовывать значения из системы рендеринга (Three.js или другой движок) в объекты btVector3 и btQuaternion.

  2. setWorldTransform(transform) Вызывается движком после расчета физики для обновления позиции графической модели. Обычно здесь происходит синхронизация с визуальной сценой:

    myMotionState.setWorldTransform = function (worldTrans) {
        const origin = worldTrans.getOrigin();
        const rotation = worldTrans.getRotation();
    
        mesh.position.set(origin.x(), origin.y(), origin.z());
        mesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());
    };

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

Интеграция MotionState с телом

После создания MotionState объект прикрепляется к телу (btRigidBody) при его конфигурации:

const transform = new Ammo.btTransform();
transform.setIdentity();
transform.setOrigin(new Ammo.btVector3(0, 5, 0));

const motionState = new Ammo.btDefaultMotionState(transform);
const localInertia = new Ammo.btVector3(0, 0, 0);

const shape = new Ammo.btBoxShape(new Ammo.btVector3(1, 1, 1));
shape.calculateLocalInertia(mass, localInertia);

const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia);
const body = new Ammo.btRigidBody(rbInfo);
physicsWorld.addRigidBody(body);

Для пользовательского MotionState достаточно заменить btDefaultMotionState на созданный объект, реализующий getWorldTransform и setWorldTransform.

Обработка нескольких объектов

Если сцена содержит десятки или сотни динамических тел, создание индивидуальных MotionState может быть ресурсоемким. В таких случаях применяются:

  • Общие MotionState для группы объектов, где setWorldTransform обновляет массив визуальных моделей.
  • Оптимизация через пул объектов Ammo, чтобы избежать постоянного выделения и уничтожения btVector3 и btQuaternion.

Особенности памяти и управления объектами

Ammo.js — это порт Bullet Physics, использующий WebAssembly. Поэтому:

  • Любой созданный объект типа btVector3, btQuaternion, btTransform нужно явно освобождать через .delete(), иначе возникает утечка памяти.
  • Внутри MotionState часто создают временные объекты один раз и переиспользуют их при каждом вызове getWorldTransform или setWorldTransform.

Пример с переиспользованием:

const tmpVec = new Ammo.btVector3();
const tmpQuat = new Ammo.btQuaternion();

myMotionState.setWorldTransform = function (worldTrans) {
    const origin = worldTrans.getOrigin();
    const rotation = worldTrans.getRotation();

    tmpVec.setValue(origin.x(), origin.y(), origin.z());
    mesh.position.set(tmpVec.x(), tmpVec.y(), tmpVec.z());

    tmpQuat.setValue(rotation.x(), rotation.y(), rotation.z(), rotation.w());
    mesh.quaternion.set(tmpQuat.x(), tmpQuat.y(), tmpQuat.z(), tmpQuat.w());
};

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

Применение пользовательского MotionState

Пользовательский MotionState особенно полезен в следующих сценариях:

  • Связь с кастомным рендерером, где объекты управляются сложной логикой трансформации.
  • Интеграция с сетевой синхронизацией, когда позиция и ориентация должны обновляться и с сервера, и из физики.
  • Анимации с физическим взаимодействием, где требуется частичное влияние физики на визуальную модель без полной замены её позиции.

Рекомендации по структуре кода

  1. Вынести MotionState в отдельный класс или модуль для повторного использования.
  2. Использовать единообразное преобразование координат между движком рендеринга и Ammo.js.
  3. Минимизировать создание временных объектов внутри методов MotionState.
  4. Явно удалять объекты Ammo после завершения симуляции.

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