Three.js интеграция

Интеграция Mapbox GL JS и Three.js основана на совместном использовании WebGL-контекста и синхронизации двух независимых сценографических систем: картографической сцены Mapbox и 3D-сцены Three.js. Mapbox GL JS управляет рендерингом тайлов, векторных слоёв и камерой карты, тогда как Three.js отвечает за произвольную 3D-графику, освещение и сложные геометрии.

Ключевым механизмом объединения выступает интерфейс пользовательского слоя CustomLayerInterface, позволяющий внедрять сторонний WebGL-рендеринг внутрь пайплайна Mapbox.


Общая модель рендеринга

Mapbox GL JS использует WebGL для отрисовки карты через собственный контекст. Three.js по умолчанию создаёт собственный WebGLRenderer, что приводит к конфликту контекстов при прямом объединении.

Корректная интеграция строится на принципе:

  • использование WebGL-контекста Mapbox внутри Three.js renderer
  • синхронизация матриц камеры
  • управление состоянием depth buffer и stencil buffer
  • строгое следование lifecycle Mapbox слоя

Географическая система координат и проекция

Mapbox GL JS использует проекцию Web Mercator (EPSG:3857), где:

  • долгота и широта переводятся в координаты плоскости
  • сцена нормализуется в диапазон мирового тайла
  • единицей пространства является “world units”

Для интеграции Three.js используется преобразование координат:

  • lngLat → Mercator coordinate
  • Mercator → world space Mapbox

Mapbox предоставляет утилиту:

mapboxgl.MercatorCoordinate.fromLngLat([lng, lat], altitude)

Результатом является объект с нормализованными координатами x, y, z, совместимыми с 3D сценой.


CustomLayerInterface как точка интеграции

Mapbox GL JS определяет структуру пользовательского слоя через интерфейс:

  • onAdd
  • render
  • onRemove

Этот слой внедряется в pipeline рендеринга карты и вызывается синхронно с каждым кадром.


Инициализация Three.js внутри Mapbox GL JS

Основная задача — переиспользование WebGL context карты:

const customLayer = {
  id: 'three-layer',
  type: 'custom',
  renderingMode: '3d',

  onAdd: function (map, gl) {
    this.camera = new THREE.Camera();
    this.scene = new THREE.Scene();

    this.renderer = new THREE.WebGLRenderer({
      canvas: map.getCanvas(),
      context: gl,
      antialias: true
    });

    this.renderer.autoClear = false;
  }
};

Важный аспект заключается в передаче canvas и context из Mapbox в Three.js renderer. Это исключает создание второго WebGL-контекста.


Синхронизация камеры Mapbox и Three.js

Mapbox управляет собственной матрицей камеры, которую необходимо синхронизировать с Three.js:

  • modelMatrix — трансформация объектов в мировом пространстве
  • projectionMatrix — матрица проекции камеры Mapbox
render: function (gl, matrix) {
  const m = new THREE.Matrix4().fromArray(matrix);

  this.camera.projectionMatrix = m;

  this.renderer.state.reset();
  this.renderer.render(this.scene, this.camera);
}

Matrix, передаваемая Mapbox, уже содержит итоговую комбинацию view-projection, что позволяет использовать её напрямую.


Преобразование географических координат в Three.js сцену

Добавление объектов в сцену требует конвертации координат:

const mercator = mapboxgl.MercatorCoordinate.fromLngLat(
  [30.5234, 50.4501],
  0
);

const object = new THREE.Mesh(
  new THREE.BoxGeometry(1000, 1000, 1000),
  new THREE.MeshStandardMaterial({ color: 0xff0000 })
);

object.position.set(mercator.x, mercator.y, mercator.z);

Для масштабирования используется коэффициент:

const scale = mercator.meterInMercatorCoordinateUnits();
object.scale.setScalar(scale);

Это позволяет согласовать реальные метры с системой координат карты.


Управление глубиной и состоянием WebGL

Mapbox активно использует depth buffer для тайлов и слоёв. При добавлении Three.js сцены важно управлять состоянием WebGL:

this.renderer.autoClear = false;

render: function (gl, matrix) {
  this.renderer.clearDepth();
  this.renderer.render(this.scene, this.camera);
}

Без очистки depth buffer возможны артефакты перекрытия слоёв.

Также важно сбрасывать состояние WebGL:

this.renderer.state.reset();

Освещение и визуальная согласованность

Three.js сцена существует в том же пространстве, что и карта, поэтому освещение требует согласования:

  • направление света должно учитывать ориентацию карты
  • интенсивность света согласуется с визуальным стилем Mapbox
  • часто используется DirectionalLight для имитации солнечного света
const light = new THREE.DirectionalLight(0xffffff, 1.0);
light.position.set(0, -70, 100);
this.scene.add(light);

Для повышения реалистичности применяется AmbientLight с низкой интенсивностью.


Анимационный цикл внутри Mapbox render loop

Mapbox управляет циклом рендеринга, поэтому Three.js не должен запускать собственный requestAnimationFrame.

Обновление логики выполняется внутри render:

render: function (gl, matrix) {
  this.cube.rotation.z += 0.01;

  const m = new THREE.Matrix4().fromArray(matrix);
  this.camera.projectionMatrix = m;

  this.renderer.state.reset();
  this.renderer.render(this.scene, this.camera);
}

Таким образом Three.js полностью подчинён циклу Mapbox.


Работа с масштабированием сцены

Mapbox GL JS использует нелинейную систему масштабирования в зависимости от zoom level. Для стабильного поведения объектов применяется компенсация масштаба:

  • объекты фиксируются в метрах через MercatorCoordinate
  • масштаб пересчитывается при изменении zoom

Это предотвращает визуальные искажения при приближении карты.


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

При интеграции WebGL сцен критичны следующие аспекты:

  • минимизация вызовов renderer.state.reset()
  • использование InstancedMesh для повторяющихся объектов
  • отключение теней при большом количестве геометрии
  • предрасчёт матриц трансформации
  • ограничение частоты обновления анимаций

Дополнительно эффективным является разделение сцены на статическую и динамическую части.


Управление порядком отрисовки

Mapbox GL JS и Three.js используют общий depth buffer, поэтому порядок отрисовки строго контролируется:

  • Mapbox рисует базовые тайлы
  • пользовательский слой выполняет overlay-рендеринг
  • Three.js сцена должна учитывать прозрачность и depth testing

При необходимости отключается depth test:

this.renderer.getContext().disable(this.renderer.getContext().DEPTH_TEST);

Расширенные сценарии синхронизации

При сложных сценах может использоваться:

  • синхронизация камеры Three.js OrbitControls с Mapbox camera state
  • проекция raycasting из координат экрана в географические координаты
  • привязка объектов к движущимся координатам (GPS, realtime data)

Raycasting требует обратного преобразования:

map.unproject([x, y]);

Геометрические ограничения и precision

Web Mercator в больших масштабах приводит к проблемам точности float32. Для их компенсации применяется:

  • локальное смещение origin (origin shifting)
  • использование группировки объектов вокруг центра камеры
  • регулярное пересчитывание базовой точки сцены

Взаимодействие событий карты и Three.js объектов

События Mapbox:

  • click
  • mousemove
  • touch events

могут быть преобразованы в Three.js raycasting через:

  • unproject координат
  • создание ray from camera
  • проверку пересечений с mesh

Это обеспечивает интерактивность 3D объектов внутри карты.


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

Mapbox GL JS полностью контролирует жизненный цикл WebGL контекста:

  • создание контекста происходит на уровне карты
  • потеря контекста требует восстановления сцены
  • очистка состояния обязана выполняться внутри custom layer

Three.js renderer должен работать в режиме внешнего контекста без попыток его пересоздания.