Работа с framebuffers

Framebuffer в WebGL представляет собой объект рендеринга, который позволяет направлять вывод графического конвейера не на экран, а в текстуру. В deck.gl этот механизм используется как фундамент для множества подсистем: от постобработки и оффскрин-рендеринга до выбора объектов (picking) и сложных визуальных эффектов.

В WebGL базовая работа с framebuffer опирается на создание объекта, к которому привязываются буферы цвета, глубины и трафарета. В более высокоуровневой библиотеке deck.gl эта работа инкапсулируется через luma.gl, обеспечивая удобные абстракции над WebGL2 API.


Роль framebuffer в архитектуре deck.gl

Внутри deck.gl framebuffer используется как промежуточный слой между слоями (layers) и итоговым canvas. Каждый кадр проходит несколько стадий:

  • подготовка сцены
  • рендеринг слоёв в offscreen framebuffer
  • применение эффектов постобработки
  • финальная композиция на экран

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


Framebuffer в luma.gl

deck.gl не управляет framebuffer напрямую. Вместо этого используется слой абстракции luma.gl, где framebuffer представлен классом Framebuffer.

Основные компоненты:

  • Texture — хранит цветовой буфер
  • Renderbuffer — используется для depth/stencil
  • Framebuffer — связывает текстуры и renderbuffer

Типичная инициализация выглядит следующим образом:

import {Framebuffer, Texture2D, Renderbuffer} from '@luma.gl/webgl';

const colorTexture = new Texture2D(gl, {
  width: 1024,
  height: 1024,
  format: gl.RGBA
});

const depthBuffer = new Renderbuffer(gl, {
  width: 1024,
  height: 1024,
  format: gl.DEPTH_COMPONENT16
});

const framebuffer = new Framebuffer(gl, {
  color: colorTexture,
  depth: depthBuffer
});

Framebuffer становится активным контекстом рендеринга через bind:

framebuffer.bind();
// draw calls
framebuffer.unbind();

Offscreen rendering в deck.gl

Одним из ключевых сценариев использования framebuffer является offscreen rendering. В deck.gl он применяется для:

  • генерации промежуточных текстур
  • реализации эффектов (blur, bloom, SSAO)
  • вычисления данных для последующих проходов

Внутренне deck.gl управляет lifecycle framebuffer через LayerManager и Deck instance.

Упрощённый цикл выглядит так:

  1. создание viewport
  2. привязка framebuffer
  3. рендер всех активных layers
  4. сохранение результата в texture
  5. использование texture в следующем pass

Framebuffer и система слоёв

Каждый слой (Layer) в deck.gl не знает, куда именно будет отрисован результат. Он лишь генерирует команды рендеринга. Framebuffer выбирается на уровне контекста выполнения.

Слой может быть рендерен:

  • в default framebuffer (экран)
  • в offscreen framebuffer (texture)
  • в picking framebuffer (служебный буфер)

Это позволяет отделить логическую модель данных от физического вывода.


Picking framebuffer

Picking — один из наиболее важных сценариев использования framebuffer в deck.gl. Он позволяет определять, какой объект находится под курсором.

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

  • каждому объекту присваивается уникальный ID
  • при рендеринге в picking framebuffer вместо цвета записывается encoded ID
  • при наведении курсора читается пиксель из framebuffer

Пример логики:

const pickingFramebuffer = new Framebuffer(gl, {
  width: canvas.width,
  height: canvas.height,
  color: new Texture2D(gl, {
    format: gl.RGBA,
    type: gl.UNSIGNED_BYTE
  }),
  depth: new Renderbuffer(gl, {
    format: gl.DEPTH_COMPONENT16
  })
});

Рендер в picking pass:

pickingFramebuffer.bind();

layers.forEach(layer => {
  layer.draw({picking: true});
});

pickingFramebuffer.unbind();

Чтение пикселя:

const pixel = new Uint8Array(4);

gl.readPixels(
  x,
  y,
  1,
  1,
  gl.RGBA,
  gl.UNSIGNED_BYTE,
  pixel
);

Декодирование ID:

const objectId =
  pixel[0] +
  pixel[1] * 256 +
  pixel[2] * 256 * 256;

Постобработка через framebuffer

Post-processing эффекты в deck.gl реализуются через цепочку framebuffer passes. Каждый эффект создаёт собственный framebuffer, в который рендерится сцена или её часть.

Типичная схема:

  1. Scene FBO
  2. Effect FBO 1
  3. Effect FBO 2
  4. Screen

Пример эффекта размытия:

class BlurEffect {
  constructor(gl, width, height) {
    this.fbo = new Framebuffer(gl, {
      width,
      height,
      color: new Texture2D(gl, {width, height})
    });
  }

  apply(inputTexture) {
    this.fbo.bind();

    // draw full-screen quad with blur shader
    drawQuad(inputTexture);

    this.fbo.unbind();

    return this.fbo.color;
  }
}

Ping-pong buffering

При сложных эффектах используется техника ping-pong buffering — два framebuffer, которые поочерёдно принимают результат рендера.

Схема:

  • Framebuffer A — input
  • Framebuffer B — output
  • затем обмен ролями
let readFbo = fboA;
let writeFbo = fboB;

for (let i = 0; i < passes; i++) {
  writeFbo.bind();

  shader.setUniform('uTexture', readFbo.color);
  drawFullScreen();

  writeFbo.unbind();

  [readFbo, writeFbo] = [writeFbo, readFbo];
}

Эта техника позволяет реализовывать:

  • Gaussian blur
  • fluid simulation
  • iterative solvers
  • screen-space эффекты

Framebuffer и WebGL2 расширения

WebGL2 значительно расширяет возможности framebuffer:

  • multiple render targets (MRT)
  • float textures
  • более гибкие depth formats

В deck.gl это используется для ускорения вычислений и уменьшения числа проходов.

Пример MRT:

const framebuffer = new Framebuffer(gl, {
  color: [
    new Texture2D(gl, {format: gl.RGBA32F}),
    new Texture2D(gl, {format: gl.RGBA32F})
  ]
});

Это позволяет записывать несколько значений за один draw call.


Интеграция framebuffer в render loop deck.gl

Внутренний render loop deck.gl можно представить как последовательность проходов:

  • clear
  • render layers → scene framebuffer
  • apply effects → effect framebuffer chain
  • composite → screen framebuffer

Каждый проход использует собственный framebuffer, управляемый Deck instance.

Ключевой момент заключается в том, что framebuffer пересоздаются только при изменении размеров viewport или конфигурации эффектов, что снижает накладные расходы.


Framebuffer и производительность

Работа с framebuffer напрямую влияет на производительность:

  • переключение framebuffer (bind/unbind) дорогостояще
  • чтение из framebuffer (readPixels) блокирует pipeline
  • большие текстуры увеличивают bandwidth нагрузки

Оптимизации в deck.gl включают:

  • переиспользование framebuffer
  • lazy resizing
  • минимизацию readback операций
  • батчинг draw calls внутри одного framebuffer pass

Типичные ошибки при работе с framebuffer

Часто встречающиеся проблемы:

  • несоответствие размеров framebuffer и canvas, приводящее к искажению изображения
  • отсутствие depth buffer при использовании 3D сцен
  • утечки памяти из-за неосвобождённых текстур
  • частые readPixels в каждом кадре
  • неправильная очистка framebuffer перед новым рендером

Framebuffer как основа расширяемости rendering pipeline

Framebuffer в deck.gl выступает как фундамент для построения кастомных rendering pipeline. С его помощью можно:

  • внедрять собственные postprocessing эффекты
  • реализовывать multi-pass визуализацию
  • строить compute-like задачи на GPU
  • создавать сложные интерактивные визуальные системы

Гибкость достигается за счёт того, что framebuffer является универсальным контейнером для промежуточных данных рендеринга, независимо от типа слоя или источника геометрии.