Пользовательные шейдеры

В основе рендеринга лежит WebGL-пайплайн, где вся визуализация выполняется на GPU через вершинные и фрагментные шейдеры. CesiumJS абстрагирует низкоуровневый WebGL, но сохраняет прямую возможность вмешательства в шейдерную стадию через систему материалов, Appearance и современный API CustomShader.

Шейдеры в CesiumJS строятся на GLSL ES 3.0 (в зависимости от контекста WebGL2) и используют набор встроенных утилит с префиксом czm_, которые предоставляют доступ к матрицам, освещению, геометрии и системным данным сцены.


Основные точки расширения шейдеров

В CesiumJS существует несколько уровней кастомизации GPU-пайплайна:

  • Material система — процедурные материалы для примитивов
  • Appearance — управление шейдерами для Primitive
  • CustomShader API — современный способ инъекции GLSL в рендеринг сущностей и 3D Tiles
  • 3D Tiles Styling — декларативная система выражений (не полноценный GLSL, но влияет на шейдер)

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


GLSL контекст CesiumJS

Шейдеры CesiumJS не являются «чистым GLSL». Они компонуются из множества включаемых модулей:

  • матричные преобразования камеры
  • модель освещения
  • туман и атмосферные эффекты
  • декодирование координат 3D Tiles
  • система материалов

Типичный фрагмент шейдера включает:

czm_materialInput materialInput;
czm_material material = czm_getMaterial(materialInput);

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


CustomShader: базовая модель

CustomShader позволяет подключать GLSL-код, который выполняется на этапе обработки фрагмента или вершины.

Основная идея:

  • доступ к атрибутам (position, normal, color)
  • доступ к uniform-переменным
  • изменение выходного цвета или положения

Структура:

const customShader = new Cesium.CustomShader({
  fragmentShaderText: `
    void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
      material.diffuse = vec3(1.0, 0.0, 0.0);
    }
  `
});

Входные данные шейдера

Cesium предоставляет структурированный доступ к данным модели:

FragmentInput

  • fsInput.attributes.positionMC
  • fsInput.attributes.normalMC
  • fsInput.attributes.color
  • fsInput.st — UV-координаты

VertexInput

  • модельные координаты
  • нормали
  • морфинг-данные (для 3D Tiles)

Эти данные являются точкой входа для процедурных эффектов.


Работа с цветом материала

Основной объект модификации — czm_modelMaterial.

Пример изменения альбедо:

void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
    material.diffuse = vec3(0.2, 0.6, 1.0);
    material.alpha = 1.0;
}

Материал поддерживает:

  • diffuse — базовый цвет
  • emission — самосвечение
  • specular — отражение
  • alpha — прозрачность

Использование времени и анимации

Cesium передаёт глобальные uniform-переменные, включая czm_frameNumber и czm_time.

Пример анимированного эффекта:

void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
    float pulse = 0.5 + 0.5 * sin(czm_frameNumber * 0.05);
    material.diffuse *= pulse;
}

Это позволяет создавать:

  • мигающие объекты
  • сканирующие эффекты
  • динамическое освещение

Работа с нормалями

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

Пример Fresnel-эффекта:

void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
    vec3 normal = normalize(fsInput.attributes.normalMC);
    vec3 viewDir = normalize(czm_viewerPositionWC - fsInput.attributes.positionMC);

    float fresnel = pow(1.0 - dot(normal, viewDir), 3.0);

    material.emission = vec3(0.0, 0.4, 1.0) * fresnel;
}

Процедурные текстуры и шум

Для генерации сложных эффектов применяются шумовые функции:

  • Perlin noise
  • Simplex noise
  • value noise

Cesium не включает встроенный шум в API CustomShader, поэтому его добавляют вручную:

float hash(vec2 p)
{
    return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}

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

void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
    float n = hash(fsInput.st * 10.0);
    material.diffuse *= n;
}

Работа с UV и пространственными координатами

UV-координаты (st) позволяют строить:

  • градиенты
  • маски
  • текстурные эффекты

Пример радиального градиента:

void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
    float d = distance(fsInput.st, vec2(0.5));
    material.diffuse = mix(vec3(0.0, 0.2, 1.0), vec3(1.0), d);
}

Интеграция с 3D Tiles

Шейдеры особенно важны при работе с 3D Tiles, где сцены состоят из больших массивов геометрии.

CustomShader применяется на уровне модели:

tileset.customShader = new Cesium.CustomShader({
  fragmentShaderText: `
    void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
    {
      material.diffuse = vec3(1.0, 1.0, 1.0);
    }
  `
});

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

  • подсветка зданий
  • фильтрация по высоте
  • визуализация атрибутов BIM/геоданных

Доступ к атрибутам модели

Cesium позволяет прокидывать пользовательские атрибуты в шейдер:

customShader = new Cesium.CustomShader({
  varyings: {
    v_height: Cesium.VaryingType.FLOAT
  },
  vertexShaderText: `
    void vertexMain(VertexInput vsInput, inout czm_modelVertexOutput vsOutput)
    {
        v_height = vsInput.attributes.positionMC.z;
    }
  `,
  fragmentShaderText: `
    void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
    {
        material.diffuse = vec3(v_height * 0.1);
    }
  `
});

Оптимизация шейдеров

GPU-оптимизация критична при больших сценах:

  • минимизация ветвлений if
  • использование mix() вместо условий
  • уменьшение вычислений в fragment shader
  • перенос логики в vertex shader

Проблемные паттерны:

  • сложные циклы
  • динамические массивы
  • частые вызовы sin/cos без необходимости

Типовые визуальные эффекты

Контур (outline)

float edge = 1.0 - dot(normalize(fsInput.attributes.normalMC), vec3(0.0, 0.0, 1.0));
material.diffuse = vec3(edge);

Dissolve (растворение)

void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
    float n = fract(sin(fsInput.st.x * 10.0) * 43758.0);
    if (n < czm_frameNumber * 0.001)
        discard;
}

Heatmap-подобная визуализация

void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
    float h = fsInput.attributes.positionMC.z;
    material.diffuse = vec3(h * 0.1, 0.2, 1.0 - h * 0.1);
}

Взаимодействие с освещением Cesium

Cesium использует физически приближённое освещение (PBR-подобная модель). CustomShader может:

  • переопределять diffuse
  • усиливать emission
  • подавлять specular

Но при этом сохраняется глобальная система освещения сцены:

  • солнечный свет
  • атмосфера
  • тени (shadow maps)

Частые ограничения и особенности

  • не все WebGL-расширения доступны в CustomShader
  • precision ограничен mediump/highp в зависимости от устройства
  • некоторые built-in Cesium uniforms недоступны напрямую
  • порядок компоновки шейдеров влияет на результат

Модель данных шейдера в Cesium

Внутренне pipeline выглядит так:

  1. загрузка геометрии
  2. декодирование 3D Tiles
  3. vertex shader (transform)
  4. rasterization
  5. fragment shader (CustomShader injection)
  6. postprocessing (FXAA, bloom, tone mapping)

CustomShader встраивается на этапе (5), не нарушая общий pipeline.


Практика комбинирования эффектов

Часто используется композиция:

  • шум + градиент
  • fresnel + emission
  • height-based coloring + temporal animation

Пример комбинированного эффекта:

void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
    float h = fsInput.attributes.positionMC.z;
    float n = fract(sin(h * 12.3) * 43758.0);
    float fresnel = pow(1.0 - dot(normalize(fsInput.attributes.normalMC), vec3(0.0,0.0,1.0)), 2.0);

    material.diffuse = mix(vec3(0.1, 0.2, 1.0), vec3(1.0, 0.3, 0.0), h);
    material.emission = vec3(fresnel * n);
}