Custom layers

Пользовательские слои в Mapbox GL JS реализуются через интерфейс CustomLayerInterface, который предоставляет прямой доступ к WebGL-контексту внутри рендер-пайплайна карты. Такой механизм используется, когда стандартных слоёв недостаточно: требуется интеграция нестандартной графики, 3D-рендеринга, шейдерных эффектов или внешних движков визуализации.

Custom layer в Mapbox GL JS — это объект, который внедряется в граф рендеринга карты и участвует в каждом кадре отрисовки. Он не является изолированным компонентом: слой получает доступ к состоянию карты, матрицам камеры и WebGL-контексту.

Минимальная структура пользовательского слоя включает следующие методы:

  • onAdd(map, gl)
  • render(gl, matrix)
  • onRemove(map, gl)
  • необязательные свойства id, type, renderingMode

Каждый из этих элементов интегрируется в жизненный цикл рендера.

CustomLayerInterface и жизненный цикл

onAdd

Метод onAdd вызывается один раз при добавлении слоя в карту. В этот момент доступен экземпляр карты и WebGL-контекст.

Типичная задача внутри onAdd:

  • инициализация буферов
  • загрузка шейдеров
  • подготовка текстур
  • создание WebGL program

Контекст WebGL передаётся напрямую, без абстракций, поэтому работа строится на уровне WebGL API.

const customLayer = {
  id: 'custom-webgl-layer',
  type: 'custom',
  onAdd: function(map, gl) {
    this.program = createShaderProgram(gl);
    this.buffer = gl.createBuffer();
  }
};

render и участие в рендер-пайплайне

Метод render(gl, matrix) вызывается каждый кадр. В этот момент слой обязан выполнить отрисовку с учётом текущей матрицы камеры.

matrix — это 4x4 матрица преобразования, включающая:

  • projection matrix
  • view matrix
  • model transform (если применимо)

Она обеспечивает синхронизацию пользовательской геометрии с картой.

Особенности render-процесса

  • выполнение происходит синхронно с основным WebGL рендером карты
  • состояние WebGL-контекста может изменяться между кадрами
  • необходимо явно восстанавливать state (blend mode, depth test, culling)

Пример базового рендера:

render: function(gl, matrix) {
  gl.useProgram(this.program);

  gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);

  gl.uniformMatrix4fv(
    gl.getUniformLocation(this.program, "u_matrix"),
    false,
    matrix
  );

  gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
}

Работа с WebGL состоянием

Mapbox GL JS управляет WebGL состоянием глобально, поэтому пользовательский слой обязан аккуратно сохранять и восстанавливать параметры:

  • blendFunc
  • depthMask
  • stencil state
  • active texture unit
  • bound buffers

Игнорирование этого приводит к конфликтам с другими слоями карты.

Типовой паттерн:

render: function(gl, matrix) {
  const depthTest = gl.getParameter(gl.DEPTH_TEST);

  gl.enable(gl.DEPTH_TEST);

  // draw custom geometry

  if (!depthTest) {
    gl.disable(gl.DEPTH_TEST);
  }
}

renderingMode: 2D и 3D интеграция

Custom layer поддерживает два режима рендеринга:

  • "2d" — рендеринг в пространстве экрана
  • "3d" — рендеринг в географическом пространстве карты

В 3D-режиме геометрия должна быть преобразована с учётом координат карты (longitude, latitude, altitude). В этом случае matrix уже содержит геопривязку.

const layer = {
  id: 'geo-layer',
  type: 'custom',
  renderingMode: '3d'
};

В 3D режиме важно учитывать:

  • проекцию Mercator
  • масштабирование по zoom level
  • искажения на высоких широтах

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

Одно из наиболее частых применений custom layers — интеграция с Three.js. В этом случае Mapbox GL JS передаёт WebGL-контекст в сцену Three.js.

Типовой подход:

  • создание renderer с внешним gl
  • отключение автоочистки буфера
  • синхронизация камеры
onAdd: function(map, gl) {
  this.renderer = new THREE.WebGLRenderer({
    canvas: map.getCanvas(),
    context: gl
  });

  this.scene = new THREE.Scene();
  this.camera = new THREE.PerspectiveCamera();
}

render:

render: function(gl, matrix) {
  this.camera.projectionMatrix = new THREE.Matrix4().fromArray(matrix);

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

Критически важный момент — отсутствие конфликта управления WebGL state между Mapbox и Three.js.

Работа с атрибутами и буферами

Custom layers напрямую используют vertex buffers:

  • gl.createBuffer()
  • gl.bufferData()
  • gl.vertexAttribPointer()

Пример подготовки геометрии:

const vertices = new Float32Array([
  0, 0, 0,
  1, 0, 0,
  0, 1, 0
]);

gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

В рендере:

const positionLocation = gl.getAttribLocation(program, "a_position");

gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);

Синхронизация с картой

Mapbox GL JS предоставляет данные о состоянии карты:

  • zoom
  • center
  • bearing
  • pitch

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

const zoom = map.getZoom();
const center = map.getCenter();

В custom layer часто реализуется:

  • масштабирование объектов по zoom
  • скрытие деталей на низких zoom уровнях
  • адаптация LOD (Level of Detail)

Производительность и ограничения

Custom layers работают внутри основного render loop, поэтому их производительность напрямую влияет на FPS карты.

Ключевые ограничения:

  • отсутствие автоматического batching
  • необходимость ручного управления буферами
  • потенциальные state changes WebGL
  • отсутствие встроенного culling

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

  • минимизацию draw calls
  • переиспользование buffers
  • предварительную компиляцию shader programs
  • использование instancing (ANGLE_instanced_arrays)

Шейдерная модель

Custom layer полностью контролирует shader pipeline:

  • vertex shader
  • fragment shader

Пример vertex shader:

attribute vec3 a_position;
uniform mat4 u_matrix;

void main() {
  gl_Position = u_matrix * vec4(a_position, 1.0);
}

Fragment shader:

precision mediump float;

void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

Шейдеры компилируются вручную через WebGL API, без абстракций.

Очистка ресурсов

Метод onRemove отвечает за освобождение памяти:

  • delete buffers
  • delete textures
  • delete shader programs
onRemove: function(map, gl) {
  gl.deleteBuffer(this.buffer);
  gl.deleteProgram(this.program);
}

Игнорирование очистки приводит к утечкам памяти в долгоживущих приложениях.

Практические паттерны использования

Custom layers применяются для:

  • 3D-анимаций поверх карты
  • визуализации потоков (flow maps)
  • heatmap на нестандартных данных
  • интеграции научных визуализаций
  • рендеринга внешних 3D-сцен

Каждый сценарий требует строгого контроля WebGL pipeline и синхронизации с матрицами карты.

Взаимодействие с другими слоями

Custom layer имеет позицию в порядке слоёв. Он может быть размещён:

  • ниже symbol layers
  • выше fill layers
  • между стандартными слоями

Порядок влияет на визуальную композицию сцены, но не изолирует WebGL state, поэтому конфликты возможны при неправильной архитектуре рендера.