Canvas экспорт

В основе визуализации лежит WebGL-контекст, привязанный к HTMLCanvasElement, который создаётся автоматически при инициализации Viewer или CesiumWidget. Canvas выступает единственной поверхностью, в которую происходит финальный вывод сцены, включая террейн, 3D Tiles, примитивы и постобработку.

Ключевая особенность архитектуры заключается в том, что кадр формируется напрямую в GPU-буфере, а DOM-элемент canvas лишь отображает результат. Это определяет специфику всех операций экспорта: они работают либо через чтение пикселей из WebGL-контекста, либо через сериализацию уже отрисованного кадра.


Доступ к canvas и сцене

После создания Viewer доступ к canvas и сцене осуществляется через стандартные свойства:

const viewer = new Cesium.Viewer("cesiumContainer");

const canvas = viewer.scene.canvas;
const scene = viewer.scene;

Canvas здесь — обычный HTMLCanvasElement, но управляемый WebGL-контекстом Cesium. Любая попытка экспорта должна учитывать, что данные находятся не в DOM, а в GPU framebuffer.


Базовый экспорт через toDataURL

Самый прямолинейный способ получить изображение — использовать стандартный API canvas:

const canvas = viewer.scene.canvas;

const imageBase64 = canvas.toDataURL("image/png");

Этот метод работает только при выполнении нескольких условий:

  • сцена полностью отрисована;
  • WebGL контекст не заблокирован политиками безопасности;
  • не используются внешние текстуры без CORS-разрешения.

В реальных сценах Cesium кадр может быть не финализирован в момент вызова, поэтому результат часто требует синхронизации с рендер-циклом.


Синхронизация с циклом рендеринга

Cesium использует непрерывный или условный рендеринг. Для гарантированного получения актуального кадра применяется событие postRender:

viewer.scene.postRender.addEventListener(function () {
    const canvas = viewer.scene.canvas;
    const image = canvas.toDataURL("image/png");
});

Этот подход фиксирует момент, когда GPU уже завершил отрисовку кадра.

При использовании requestRenderMode сцена перестаёт обновляться автоматически, и экспорт становится детерминированным:

const viewer = new Cesium.Viewer("cesiumContainer", {
    requestRenderMode: true
});

viewer.scene.requestRender();

viewer.scene.postRender.addEventListener(() => {
    const img = viewer.scene.canvas.toDataURL("image/png");
});

Экспорт через readPixels

Более контролируемый способ получения изображения — прямое чтение буфера кадра:

const scene = viewer.scene;
const context = scene.context;

const width = scene.canvas.width;
const height = scene.canvas.height;

const pixels = new Uint8Array(width * height * 4);

context.readPixels({
    x: 0,
    y: 0,
    width: width,
    height: height,
    arrayBufferView: pixels
});

Этот метод позволяет обойти ограничения canvas API и получить «сырые» данные изображения.

Дальнейшая сборка изображения выполняется через создание второго canvas:

const outputCanvas = document.createElement("canvas");
outputCanvas.width = width;
outputCanvas.height = height;

const ctx = outputCanvas.getContext("2d");

const imageData = ctx.createImageData(width, height);
imageData.data.set(pixels);

ctx.putImageData(imageData, 0, 0);

const finalImage = outputCanvas.toDataURL("image/png");

Особенность WebGL заключается в инверсии оси Y, поэтому изображение часто требует вертикального переворота:

ctx.translate(0, height);
ctx.scale(1, -1);

Высокое разрешение и масштабирование экспорта

Стандартный canvas ограничен физическим размером экрана. Для получения изображений высокого разрешения применяется масштабирование viewport.

const scale = 2.0;

const width = viewer.scene.canvas.width * scale;
const height = viewer.scene.canvas.height * scale;

const offscreenCanvas = document.createElement("canvas");
offscreenCanvas.width = width;
offscreenCanvas.height = height;

const ctx = offscreenCanvas.getContext("2d");

ctx.drawImage(viewer.scene.canvas, 0, 0, width, height);

const hiResImage = offscreenCanvas.toDataURL("image/png");

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


Экспорт с учётом Cesium render loop

Внутренний цикл рендеринга Cesium может быть активен или пассивен. При активном режиме кадры обновляются автоматически, при пассивном — только при запросе.

Для стабильного экспорта часто используется принудительный рендер:

viewer.render();
const image = viewer.scene.canvas.toDataURL("image/png");

Однако viewer.render() не всегда гарантирует завершение GPU-пайплайна, поэтому более надёжной считается комбинация с postRender.


Ограничения WebGL при экспорте

Экспорт canvas в Cesium подчиняется ограничениям WebGL:

  • tainted canvas: внешние текстуры без CORS блокируют toDataURL;
  • premultiplied alpha: может изменять цветовые значения;
  • depth buffer недоступен: глубина сцены не экспортируется напрямую;
  • antialiasing: может влиять на пиксельную точность.

Особенно критичны тайлы из внешних источников, если сервер не отдаёт корректные заголовки CORS.


Экспорт с учётом post-processing эффектов

Cesium применяет постобработку (bloom, FXAA, depth of field). Canvas экспортирует уже финальный результат, но readPixels может вернуть промежуточный буфер в зависимости от конфигурации.

При активных postProcessStages:

viewer.scene.postProcessStages.bloom.enabled = true;

экспорт через canvas сохраняет визуальный эффект, но прямой readPixels может требовать дополнительной обработки, если используется multiple render targets.


Захват кадра без UI элементов

Canvas содержит только WebGL сцену. DOM-элементы интерфейса Viewer (компас, кнопки навигации) не входят в экспорт.

Для полного скриншота используется наложение DOM поверх canvas:

html2canvas(document.querySelector("#cesiumContainer")).then(canvas => {
    const image = canvas.toDataURL("image/png");
});

Такой подход выходит за пределы Cesium и объединяет UI и сцену в единое изображение.


Проблемы синхронизации и «пустых кадров»

Распространённая проблема — экспорт до завершения загрузки тайлов. В этом случае canvas содержит частично отрисованную сцену.

Контроль состояния выполняется через:

viewer.scene.globe.tilesLoaded.addEventListener(function () {
    const image = viewer.scene.canvas.toDataURL("image/png");
});

Дополнительно учитывается:

  • завершение загрузки 3D Tiles;
  • готовность imagery layers;
  • отсутствие pending textures.

Экспорт с использованием offscreen rendering

В современных браузерах возможен OffscreenCanvas:

const offscreen = new OffscreenCanvas(800, 600);
const context = offscreen.getContext("webgl");

const imageBitmap = offscreen.transferToImageBitmap();

Cesium может быть адаптирован к offscreen контексту, что позволяет выполнять рендер и экспорт в Web Worker, разгружая основной поток.


Контроль цветового пространства

WebGL в Cesium работает в sRGB или linear space в зависимости от конфигурации контекста:

scene.highDynamicRange = true;

Это влияет на итоговый экспорт: изображение может выглядеть иначе при прямом сохранении canvas.

Для стабилизации результата часто фиксируется:

  • отключение HDR;
  • унификация gamma correction;
  • контроль tone mapping.

Практика стабильного кадра для экспорта

Стабильный экспорт требует фиксации состояния сцены:

  • отключение автоматического рендеринга;
  • ожидание загрузки всех ресурсов;
  • принудительный render;
  • чтение canvas после postRender.

Типовой сценарий:

viewer.scene.requestRenderMode = true;

viewer.scene.globe.tilesLoaded.addEventListener(() => {
    viewer.scene.requestRender();
});

viewer.scene.postRender.addEventListener(() => {
    const image = viewer.scene.canvas.toDataURL("image/png");
});

Особенности производительности при экспорте

Чтение пикселей из GPU — одна из самых дорогих операций в WebGL пайплайне. При частом вызове:

  • блокируется GPU pipeline;
  • падает FPS;
  • увеличивается latency сцены.

По этой причине экспорт обычно выполняется как разовая операция, а не в цикле.


Архитектурные ограничения canvas слоя

Canvas в Cesium является финальной точкой рендеринга, поэтому:

  • невозможно извлечь отдельные слои сцены напрямую;
  • невозможно получить геометрическую структуру через canvas;
  • экспорт всегда «плоский» (rasterized output).

Разделение на данные и визуализацию происходит до стадии WebGL, и canvas отражает только результат rasterization pipeline.