Работа с instancedMesh для большого числа объектов

При работе с физикой в веб-приложениях производительность часто становится узким местом, особенно когда требуется симуляция большого количества объектов. Использование стандартных Mesh для каждого тела в Three.js в сочетании с Ammo.js приводит к сильной нагрузке на CPU и GPU. Для решения этой задачи применяется InstancedMesh, который позволяет визуализировать сотни и тысячи идентичных объектов с минимальной потерей производительности.

Принципы InstancedMesh

InstancedMesh создаёт один геометрический объект и множество экземпляров, каждый из которых может иметь свою матрицу трансформации и цвет. Это снижает количество draw call в рендерере до одного на весь набор экземпляров. Основные компоненты:

  • Геометрия (geometry) — общий каркас объекта. Все экземпляры используют одну геометрию.
  • Материал (material) — единый материал для всех экземпляров. Возможна установка атрибутов цвета для каждого инстанса.
  • Количество экземпляров (count) — число объектов, создаваемых в массиве.
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const instancedMesh = new THREE.InstancedMesh(geometry, material, 1000);
scene.add(instancedMesh);

Создание физических тел для instancedMesh

Ammo.js работает с отдельными физическими телами (btRigidBody). Для каждого экземпляра InstancedMesh создаётся соответствующее тело. Важно хранить массив этих тел для последующего обновления матриц.

const rigidBodies = [];
const transform = new Ammo.btTransform();
const mass = 1;

for (let i = 0; i < instancedMesh.count; i++) {
    const pos = new THREE.Vector3(
        Math.random() * 20 - 10,
        Math.random() * 20 + 10,
        Math.random() * 20 - 10
    );
    const quat = new THREE.Quaternion();
    quat.setFromEuler(new THREE.Euler(
        Math.random() * Math.PI,
        Math.random() * Math.PI,
        Math.random() * Math.PI
    ));

    transform.setIdentity();
    transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
    transform.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w));

    const motionState = new Ammo.btDefaultMotionState(transform);

    const shape = new Ammo.btBoxShape(new Ammo.btVector3(0.5, 0.5, 0.5));
    shape.setMargin(0.05);

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

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

    physicsWorld.addRigidBody(body);
    rigidBodies.push(body);
}

Обновление матриц instancedMesh

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

const tempMatrix = new THREE.Matrix4();
const tempPos = new THREE.Vector3();
const tempQuat = new THREE.Quaternion();

function updateInstancedMesh() {
    for (let i = 0; i < instancedMesh.count; i++) {
        const body = rigidBodies[i];
        const motionState = body.getMotionState();
        if (motionState) {
            const transform = new Ammo.btTransform();
            motionState.getWorldTransform(transform);
            const origin = transform.getOrigin();
            const rotation = transform.getRotation();

            tempPos.set(origin.x(), origin.y(), origin.z());
            tempQuat.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());

            tempMatrix.compose(tempPos, tempQuat, new THREE.Vector3(1, 1, 1));
            instancedMesh.setMatrixAt(i, tempMatrix);
        }
    }
    instancedMesh.instanceMatrix.needsUpdate = true;
}

Оптимизация производительности

  • Выбор форм тела: Простейшие примитивные формы (btBoxShape, btSphereShape) работают быстрее сложных треугольных мешей.
  • Снижение частоты обновления: Для объектов, находящихся далеко от камеры, можно обновлять матрицы реже.
  • Использование кэширования: Пересоздавать матрицы только при изменении состояния тела, избегая лишних операций.
  • Пакетная обработка: Если объектов тысячи, расчёт матриц можно распределить на несколько кадров, уменьшая нагрузку на основной цикл рендера.

Особенности коллизий и взаимодействий

При использовании большого числа тел важно учитывать:

  • Стабильность симуляции: Большие массивы объектов повышают риск “пробивания” тел друг через друга. Настройка timeStep и количества субшагов в stepSimulation критична.
  • Масса и центр тяжести: Единообразные массы и корректное вычисление инерции обеспечивают предсказуемую физику.
  • Группы столкновений: Использование collisionGroup и collisionMask позволяет ограничивать взаимодействие объектов, снижая количество проверок столкновений.
physicsWorld.stepSimulation(deltaTime, 10); // 10 субшагов для стабильности
updateInstancedMesh();

Примеры применения

  • Динамическая сцена с падающими объектами: тысячи кубов или сфер могут падать с высоты, взаимодействуя друг с другом.
  • Песочницы и симуляции жидкости: мелкие объекты визуализируются через InstancedMesh, при этом физический движок управляет столкновениями и гравитацией.
  • Игровые эффекты: разрушения, дождь объектов, массовые столкновения, где важно минимизировать draw calls и CPU нагрузку.

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