Шейдерные модули

Шейдерные модули в deck.gl представляют собой систему переиспользуемых GLSL-компонентов, которые встраиваются в вершинные и фрагментные шейдеры слоёв. Они позволяют декомпозировать графическую логику на независимые блоки, стандартизировать общие операции (проекция, освещение, picking) и обеспечивать согласованность между различными слоями визуализации.

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


Архитектура шейдерных модулей

Каждый шейдерный модуль в deck.gl — это JavaScript-объект, описывающий набор GLSL-фрагментов и метаданных. Он может содержать:

  • name — уникальное имя модуля
  • vs — код, добавляемый в vertex shader
  • fs — код, добавляемый в fragment shader
  • uniforms — описание uniform-переменных
  • defines — препроцессорные флаги
  • inject — точки вставки кода в стандартные шаблоны шейдеров
  • вспомогательные функции (GLSL helpers)

Модули объединяются в массив и передаются в слой через свойство modules.

import {ScatterplotLayer} from '@deck.gl/layers';

const layer = new ScatterplotLayer({
  id: 'points',
  data: dataset,
  getPosition: d => d.position,
  getRadius: d => d.radius,
  modules: [project32, picking]
});

Механизм компоновки шейдеров

Deck.gl использует систему сборки шейдеров на этапе компиляции слоя. Каждый модуль проходит процесс:

  1. Сбор всех vs и fs фрагментов
  2. Объединение функций и структур
  3. Инъекция uniforms и defines
  4. Подстановка в шаблон базового шейдера слоя
  5. Компиляция итогового GLSL-кода WebGL контекстом

Ключевой механизм — предотвращение конфликтов имён. Для этого используется:

  • неймспейсирование функций
  • префиксы модулей
  • автоматическое переименование внутренних символов

Модуль проекции координат (project)

Один из базовых модулей — project. Он отвечает за преобразование координат из географических или мировых систем в clipspace.

Он поддерживает:

  • Web Mercator projection
  • 32-bit и 64-bit precision режимы
  • мультислойную синхронизацию матриц камеры

Типичный набор функций:

  • project_position(vec3 position)
  • project_scale(float value)
  • project_normal(vec3 normal)

Внутренне модуль использует матрицы:

  • model matrix
  • view matrix
  • projection matrix

и вычисляет итоговое положение вершины:

vec4 position_world = modelMatrix * vec4(position, 1.0);
vec4 position_clip = projectionMatrix * viewMatrix * position_world;

Модуль picking (выбор объектов)

Модуль picking обеспечивает поддержку интерактивности: hover, click, selection.

Он добавляет:

  • уникальный picking color
  • буфер идентификаторов объектов
  • фрагментную кодировку ID

В vertex shader передаётся:

attribute vec3 instancePickingColor;
varying vec3 vPickingColor;

В fragment shader:

gl_FragColor = vec4(vPickingColor, 1.0);

При рендере в picking-pass вместо цветового буфера формируется ID-buffer, который затем читается CPU-частью через readPixels.


Модуль освещения (lighting)

Lighting module используется в 3D-слоях (например, MeshLayer, SimpleMeshLayer).

Поддерживает:

  • направленный свет (directional light)
  • точечные источники (point light)
  • ambient light
  • нормализацию нормалей

Основная функция:

vec3 calculateLighting(vec3 normal, vec3 position, vec3 color) {
  vec3 lightDir = normalize(light.direction);
  float diffuse = max(dot(normal, lightDir), 0.0);
  return color * (ambient + diffuse);
}

Модуль может быть расширен через дополнительные источники света, передаваемые через uniform-массивы.


Модуль fp64 (двойная точность)

Модуль fp64 реализует high-precision вычисления координат с использованием техники double-float emulation.

Так как WebGL не поддерживает double precision, применяется разложение числа:

  • high part (старшие биты)
  • low part (младшие биты)
struct vec2 {
  float x;
  float y;
};

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

  • сложение double-float
  • умножение double-float
  • нормализация результата

Используется в глобальных визуализациях (карты, GIS-данные), где критична точность на больших масштабах.


Инъекция кода (inject hooks)

Deck.gl позволяет модулям внедрять код в заранее определённые точки базового шейдера.

Примеры точек:

  • vs:#decl — объявления в vertex shader
  • vs:#main-start
  • fs:#decl
  • fs:#main-end

Пример:

const myModule = {
  name: 'customEffects',
  inject: {
    'fs:#main-end': `
      gl_FragColor.rgb *= 0.8;
    `
  }
};

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


Uniform-модель модулей

Uniform-переменные в модулях описываются декларативно:

uniforms: {
  opacity: 1.0,
  intensity: {value: 0.5, type: 'f'}
}

Deck.gl автоматически:

  • создаёт uniform в GLSL
  • связывает его с JS-свойством слоя
  • обновляет значение при изменении props

В GLSL:

uniform float opacity;

Комбинирование модулей

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

Пример типичного набора для 2D слоя:

  • project32
  • picking
  • dirlight (если поддерживается освещение)
  • кастомный модуль эффектов

Порядок подключения имеет значение только при конфликте inject-точек.


Написание пользовательских шейдерных модулей

Создание собственного модуля требует определения структуры GLSL и JS-обёртки.

Пример базового модуля:

export const grayscaleModule = {
  name: 'grayscale',
  fs: `
    vec3 applyGrayscale(vec3 color) {
      float g = dot(color, vec3(0.299, 0.587, 0.114));
      return vec3(g);
    }
  `,
  inject: {
    'fs:#main-end': `
      gl_FragColor.rgb = applyGrayscale(gl_FragColor.rgb);
    `
  }
};

Использование:

new ScatterplotLayer({
  data,
  modules: [project32, picking, grayscaleModule]
});

Взаимодействие с атрибутами вершин

Шейдерные модули могут расширять набор vertex attributes слоя:

  • instance attributes
  • vertex attributes
  • varying variables

Пример:

attribute float instanceIntensity;
varying float vIntensity;

Передача:

vIntensity = instanceIntensity;

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


Производительность и стоимость модульности

Модульность увеличивает гибкость, но влияет на:

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

Оптимизации:

  • дедупликация GLSL функций
  • агрегация uniform-переменных
  • tree-shaking модулей на этапе сборки
  • минимизация inject-хуков

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


Типичные проблемы и особенности

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

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

  • несовместимостью GLSL версий
  • дублированием varyings
  • несогласованностью inject-точек

Особенность архитектуры deck.gl — строгая синхронизация CPU/GPU слоёв: любое изменение модулей требует пересборки шейдера слоя, что может влиять на динамические обновления сцены.