Batching геометрий: merge и instancing

Виртуальные сцены, создаваемые с помощью A-Frame, работают поверх графического движка Three.js и используют возможности WebGL. Основное ограничение производительности в браузере — количество draw call (вызовов отрисовки), которые отправляются в GPU за один кадр. Чем больше отдельных объектов в сцене, тем выше нагрузка на CPU и драйвер видеокарты.

Даже при относительно простых моделях сцена может работать медленно, если содержит сотни или тысячи однотипных объектов — деревья, камни, частицы, архитектурные элементы. В таких случаях ключевым инструментом оптимизации становится батчинг геометрии (batching).

Батчинг — это объединение нескольких объектов в один графический вызов. В контексте A-Frame и Three.js используются два основных подхода:

  • merge (слияние геометрий)
  • instancing (аппаратное инстансирование)

Каждый метод имеет свои особенности, ограничения и области применения.


Архитектура рендеринга A-Frame

A-Frame предоставляет декларативный HTML-подобный синтаксис:

<a-entity geometry="primitive: box" material="color: red"></a-entity>

Каждый a-entity создаёт отдельный объект Three.js (THREE.Mesh). Даже если 100 объектов имеют одинаковую геометрию и материал, по умолчанию они будут отрисовываться отдельными draw call.

Это создаёт узкое место:

  • CPU выполняет подготовку вызовов
  • GPU получает множество мелких задач
  • частота кадров падает

Оптимизация требует перехода от множества независимых мешей к агрегированным структурам.


Слияние геометрий (merge)

Принцип работы

Метод merge объединяет несколько BufferGeometry в одну общую геометрию. После объединения:

  • создаётся один меш
  • используется один материал
  • выполняется один draw call

В основе лежит функция BufferGeometryUtils.mergeBufferGeometries() из Three.js.

Алгоритм слияния

  1. Берутся несколько объектов THREE.Mesh
  2. Извлекаются их BufferGeometry
  3. Применяются матрицы трансформации к вершинам
  4. Геометрии объединяются в общий буфер
  5. Создаётся новый Mesh

Пример реализации в A-Frame (через компонент)

AFRAME.registerComponent('merge-meshes', {
  init: function () {
    const meshes = [];
    this.el.object3D.traverse((node) => {
      if (node.isMesh) {
        node.updateMatrix();
        meshes.push(node);
      }
    });

    const geometries = meshes.map(mesh => {
      const geometry = mesh.geometry.clone();
      geometry.applyMatrix4(mesh.matrix);
      return geometry;
    });

    const mergedGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries(geometries);
    const material = meshes[0].material;

    const mergedMesh = new THREE.Mesh(mergedGeometry, material);

    this.el.setObject3D('mesh', mergedMesh);
  }
});

Ограничения merge

1. Одинаковый материал

Все объединяемые геометрии должны использовать один материал. Иначе придётся создавать группы индексов, что усложняет рендеринг.

2. Статичность

После объединения объекты нельзя:

  • перемещать отдельно
  • анимировать по отдельности
  • изменять индивидуально

Это метод для полностью статических объектов.

3. Отсутствие индивидуального фрустум-куллинга

GPU больше не может отсекать невидимые части отдельно. Если хотя бы часть объединённой геометрии видна, отрисовывается всё.


Когда использовать merge

  • Архитектурные сцены
  • Статические декорации
  • Повторяющиеся элементы без анимации
  • Импортированные модели с избыточной иерархией

Instancing (аппаратное инстансирование)

Концепция

Instancing использует возможности WebGL 2.0 для отрисовки одной и той же геометрии с разными трансформациями в рамках одного draw call.

В Three.js применяется класс THREE.InstancedMesh.

В отличие от merge:

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

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

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });

const count = 1000;
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);

const dummy = new THREE.Object3D();

for (let i = 0; i < count; i++) {
  dummy.position.set(
    Math.random() * 50,
    0,
    Math.random() * 50
  );
  dummy.updateMatrix();
  instancedMesh.setMatrixAt(i, dummy.matrix);
}

scene.add(instancedMesh);

Интеграция instancing в A-Frame

A-Frame не предоставляет встроенный HTML-синтаксис для instancing, поэтому используется кастомный компонент.

AFRAME.registerComponent('instanced-boxes', {
  schema: { count: { type: 'int', default: 1000 } },

  init: function () {
    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshStandardMaterial({ color: 0x2196F3 });

    const mesh = new THREE.InstancedMesh(
      geometry,
      material,
      this.data.count
    );

    const dummy = new THREE.Object3D();

    for (let i = 0; i < this.data.count; i++) {
      dummy.position.set(
        Math.random() * 50,
        0,
        Math.random() * 50
      );
      dummy.updateMatrix();
      mesh.setMatrixAt(i, dummy.matrix);
    }

    this.el.setObject3D('mesh', mesh);
  }
});

Преимущества instancing

1. Один draw call для тысяч объектов

Кардинально снижает нагрузку на CPU.

2. Индивидуальные трансформации

Каждый экземпляр может иметь:

  • собственную позицию
  • масштаб
  • вращение

3. Динамическое обновление

Можно изменять отдельные матрицы:

mesh.setMatrixAt(index, newMatrix);
mesh.instanceMatrix.needsUpdate = true;

Ограничения instancing

1. Одинаковая геометрия и материал

Все экземпляры используют одну и ту же геометрию.

2. Ограниченные индивидуальные материалы

Разные цвета требуют:

  • использования instanceColor
  • кастомного шейдера

3. Нет индивидуальных children

InstancedMesh — это плоская структура, не поддерживающая вложенные объекты.


Сравнение merge и instancing

Характеристика merge instancing
Количество draw call 1 1
Возможность анимации отдельных объектов Нет Да
Поддержка разных геометрий Да Нет
Поддержка разных материалов Ограничено Нет
Подходит для динамики Нет Да
Фрустум-куллинг Общий Общий

Оптимизация больших сцен

Комбинированный подход

На практике применяются оба метода:

  • Статические элементы объединяются через merge
  • Динамические повторяющиеся объекты — через instancing
  • Сложные модели оптимизируются на этапе экспорта

Работа с материалами при instancing

Для индивидуального цвета используется:

const color = new THREE.Color();
mesh.setColorAt(i, color.setHex(Math.random() * 0xffffff));
mesh.instanceColor.needsUpdate = true;

Материал должен поддерживать vertex colors:

new THREE.MeshStandardMaterial({
  vertexColors: true
});

Память и буферы

InstancedMesh хранит:

  • базовую геометрию
  • массив матриц (instanceMatrix)
  • при необходимости — массив цветов (instanceColor)

При 10 000 экземпляров используется:

  • 16 float на матрицу
  • 64 байта на экземпляр

Это существенно эффективнее, чем 10 000 отдельных Mesh-объектов.


Фрустум-куллинг и bounding volumes

InstancedMesh имеет один bounding box. Если хотя бы один экземпляр попадает в область видимости, рендерится весь батч.

Для сцен с широким распределением объектов:

  • используется разбиение на несколько InstancedMesh
  • сцена делится на кластеры

Использование в игровых сценах

Instancing подходит для:

  • лесов
  • толпы NPC
  • частиц
  • плитки ландшафта
  • повторяющихся модульных элементов

Merge подходит для:

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

Влияние на CPU и GPU

Без batching

  • 2000 объектов
  • 2000 draw call
  • CPU становится узким местом

С instancing

  • 1 draw call
  • CPU практически не нагружается
  • GPU эффективно обрабатывает массивы трансформаций

Особенности в WebXR

В режиме VR нагрузка удваивается, поскольку сцена рендерится дважды (для каждого глаза). Оптимизация batching становится критически важной для поддержания 72–90 FPS на устройствах вроде Meta Quest 2.


Практические рекомендации

1. Более 100 одинаковых объектов — рассматривать instancing.

2. Статические модели после загрузки — объединять.

3. Разделять сцену на логические чанки.

4. Проверять draw call через инструменты разработчика браузера.


Расширенные техники

Instancing с кастомным шейдером

Позволяет:

  • добавлять индивидуальные параметры
  • анимировать через атрибуты
  • создавать GPU-партиклы

В A-Frame используется компонент shader с кастомным GLSL.


Merge на этапе билда

В производственных проектах геометрии объединяются:

  • в Blender
  • через glTF pipeline
  • при сборке ассетов

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


Итоговая архитектурная стратегия

Оптимизированная сцена на A-Frame обычно включает:

  • минимальное количество Mesh
  • активное использование InstancedMesh
  • предварительное объединение статических геометрий
  • контроль draw call < 100 для VR-сцен

Грамотное применение merge и instancing позволяет масштабировать сцену до десятков тысяч объектов без критического падения производительности, сохраняя стабильный FPS даже на мобильных VR-устройствах.