Ольга Стефанишина
← Назад

Основы WebGL 2: Отрисовка полноэкранного прямоугольника

Визуализация UV-координат из фрагментного шейдера полноэкранного прямоугольника
Визуализация UV-координат из фрагментного шейдера полноэкранного прямоугольника

Содержание

Как упоминалось в предыдущей статье, WebGL 2 использует программируемый конвейер. В этой статье рассматриваются шейдеры и основы языка GLSL — основополагающие элементы, необходимые перед тем, как приступить к симуляции жидкости.

Прежде чем создавать сложные эффекты, нужно понять, как WebGL рендерит что-либо на экран. В отличие от декларативных подходов, таких как HTML/CSS, WebGL требует явно определить геометрию и внешний вид с помощью пользовательских программ. Эти программы, называемые шейдерами, написаны на GLSL и выполняются непосредственно на GPU.

Введение в GLSL ES 3.0

GLSL ES 3.0 (OpenGL Shading Language for Embedded Systems version 3.0) — это высокоуровневый язык шейдеров с C-подобным синтаксисом, специально разработанный для графических процессоров (GPU) во встраиваемых системах и веб-браузерах. Спецификация требует, чтобы каждый шейдер начинался с директивы #version 300 es, чтобы указать версию GLSL для встраиваемых систем. Без этой директивы шейдер не скомпилируется.

Система типов GLSL ES 3.0

Система типов — это фундаментальные строительные блоки кода GLSL ES 3.0. Понимание системы типов поможет правильно объявлять переменные и без путаницы использовать встроенные функции.

Скалярные типы

  • float (32-битное число с плавающей точкой IEEE 754)
  • int (32-битное целое число со знаком)
  • uint (32-битное беззнаковое целое число)
  • bool (булев тип)

Это простейшие типы: одиночные числовые или булевы значения.

Векторные типы

  • С плавающей точкой: vec2, vec3, vec4
  • Целочисленные: ivec2, ivec3, ivec4
  • Беззнаковые целочисленные: uvec2, uvec3, uvec4
  • Булевы: bvec2, bvec3, bvec4

Современные GPU отлично справляются с параллельной обработкой векторных значений.

glsl
1vec4 color = vec4(1.0, 0.5, 0.2, 1.0);
2float r = color.r; // То же самое, что color.x или color[0]
3vec3 bgr = color.bgr; // Swizzle (перестановка компонентов): vec3(0.2, 0.5, 1.0)

Матричные типы и типы семплеров

  • Матрицы: mat2, mat3, mat4 (а также неквадратные варианты)
  • Семплеры текстур: sampler2D, sampler3D, samplerCube
  • Целочисленные семплеры: isampler2D, usampler2D

Тип семплера (например, sampler2D) указывает компилятору, что вы хотите сэмплировать 2D-текстуру внутри шейдера. Вы увидите их использование во фрагментных шейдерах при написании выражений вроде texture(uTexture, vUV).

Квалификаторы переменных (Поток данных)

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

  • in: Входные данные (повершинные в вершинном шейдере, интерполированные во фрагментном шейдере)
  • out: Выходные данные (интерполированные выходные значения в вершинном шейдере, выходные значения во фреймбуфер во фрагментном шейдере)
  • uniform: Константные значения для всего вызова отрисовки

Uniform-блоки

WebGL 2 поддерживает группировку uniform-переменных в блоки для эффективного обновления. GLSL код объявляет структуру uniform блока в шейдере:

glsl
1layout(std140) uniform TransformBlock {
2 mat4 model;
3 mat4 view;
4 mat4 projection;
5} transform;

JavaScript код создает буфер и загружает данные, соответствующие этой структуре:

javascript
1// Создание и привязка uniform-буфера
2const ubo = gl.createBuffer();
3gl.bindBuffer(gl.UNIFORM_BUFFER, ubo);
4gl.bufferData(gl.UNIFORM_BUFFER, transformData, gl.DYNAMIC_DRAW);
5
6// Связывание с шейдерной программой
7const blockIndex = gl.getUniformBlockIndex(program, 'TransformBlock');
8gl.uniformBlockBinding(program, blockIndex, 0);
9gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, ubo);

Схема std140 — это стандартизированное правило организации памяти. Он гарантирует, что все GPU одинаковым образом размещают данные uniform-буфера. Без этого стандарта ваш код может работать на одной видеокарте, но отказать на другой из-за различного расположения данных в памяти.

Работа с атрибутами вершин в WebGL 2

WebGL 2 (GLSL ES 3.0) добавил несколько удобств, которые сокращают шаблонный JavaScript-код и уменьшают количество ошибок при настройке вершинных данных.

Объекты вершинных массивов (VAO)

Вместо привязки и настройки каждого атрибута (позиция, нормаль, UV-координаты и т.д.) при каждом вызове отрисовки, вы можете сохранить всю эту конфигурацию в VAO.

VAO инкапсулируют конфигурацию вершинных атрибутов, сохраняя привязки буферов, указатели на атрибуты и состояния их активности. Это избавляет от повторяющихся вызовов настройки:

javascript
1const vao = gl.createVertexArray();
2gl.bindVertexArray(vao);
3// Настроить все атрибуты один раз
4gl.bindVertexArray(null);
5
6// Позже: один вызов для использования
7gl.bindVertexArray(vao);
8gl.drawArrays(gl.TRIANGLES, 0, vertexCount);

Явные местоположения атрибутов (Attribute Locations)

Вместо запроса индексов атрибутов во время выполнения с помощью gl.getAttribLocation, вы можете жестко задать местоположение прямо в шейдере.

GLSL ES 3.0 поддерживает явное назначение местоположения атрибутов, устраняя необходимость в запросах gl.getAttribLocation:

glsl
1layout(location = 0) in vec2 aPosition;
2layout(location = 1) in vec3 aColor;

Затем JavaScript может ссылаться на атрибуты по номерам: gl.enableVertexAttribArray(0).

Шейдеры (Shaders)

Шейдер – это программа, написанная на языке GLSL, предназначенная для обработки графических данных. Существует два основных типа шейдеров, которые совместно работают для отрисовки (рендеринга) графики: вершинный шейдер (vertex shader) и фрагментный шейдер (fragment shader).

GLSL предоставляет предопределенные встроенные переменные, которые обеспечивают обмен данными между различными стадиями обработки шейдеров и GPU.

Встроенные переменные вершинного шейдера (Vertex Shader Built-ins)

  • gl_Position Значение этой переменной должно быть установлено в вершинном шейдере. Это выходные координаты (x, y, z, w) вершины в пространстве отсечения (clip-space).
  • gl_VertexID Автоматически предоставляемый индекс текущей вершины (полезно для процедурной геометрии).
  • gl_InstanceID При использовании аппаратного инстансинга (instanced rendering/draws), эта переменная содержит индекс текущего экземпляра (instance).

Встроенные переменные фрагментного шейдера (Fragment Shader Built-ins)

  • gl_FragCoord Координаты (x, y, z, w) центра текущего фрагмента в экранных координатах (window-space). Здесь z – это значение глубины, а w – это 1/w_clip (где w_clip – это w-компонента вершины из пространства отсечения, полученная из gl_Position). gl_FragCoord.y использует начало координат в левом нижнем углу (соглашение OpenGL), поэтому может потребоваться ручное инвертирование для координат с началом в левом верхнем углу.
  • gl_FrontFacing Логическое значение (boolean), указывающее, является ли текущий обрабатываемый примитив лицевым (front-facing) или обратным (back-facing).
  • gl_FragDepth Позволяет фрагментному шейдеру записывать произвольное значение в буфер глубины (depth buffer).

Вершинный шейдер

Вершинный шейдер выполняется один раз для каждой вершины и имеет обязательную выходную переменную gl_Position в координатах отсечения (clip-space). Он получает данные для каждой вершины через переменные in и передает интерполированные данные фрагментному шейдеру через переменные out:

glsl
1#version 300 es
2
3in vec3 aPosition; // Позиция вершины из буфера
4in vec3 aColor; // Цвет вершины из буфера
5out vec3 vColor; // Интерполированный цвет для фрагментов
6
7uniform mat4 uModelViewMatrix; // Константа для всех вершин
8uniform mat4 uProjectionMatrix; // Константа для всех вершин
9
10void main() {
11 // Трансформация в пространство отсечения с w=1.0 для позиций
12 gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
13 vColor = aColor; // Передача цвета в фрагментный шейдер
14}

Координаты отсечения — это внутренняя система координат GPU, где значения варьируются от -1 до 1. Они автоматически отображаются на экран.

Четвертый компонент w=1.0 отличает позиции от направлений в однородных координатах, позволяя матрицам трансляции работать корректно. Выполняйте вычисления в вершинном шейдере, когда это возможно, так как вершинный шейдер обрабатывает малое количество вершин по сравнению с миллионами пикселей в фрагментном шейдере.

Фрагментный шейдер

После этапа получения NDC специализированный аппаратный блок GPU выполняет их преобразование в оконные (пиксельные) координаты. Затем растеризатор преобразует треугольники (заданные вершинами) во фрагменты. Каждый фрагмент соответствует пикселю, который покрывает треугольник, и содержит: положение на экране (x, y), значение глубины (z) и другие интерполированные значения из вершинного шейдера.

Фрагментный шейдер выполняется для каждого фрагмента, получая интерполированные значения от вершинного шейдера. GPU автоматически интерполирует все выходные данные вершинного шейдера по всей поверхности каждого треугольника:

glsl
1#version 300 es
2precision mediump float;
3
4in vec3 vColor; // Интерполированное значение из вершинного шейдера
5out vec4 fragColor; // Выходной цвет RGBA во фреймбуфер
6
7void main() {
8 // Альфа = 1.0 для полной непрозрачности
9 fragColor = vec4(vColor, 1.0);
10}

Для выходных цветов обычно используется тип данных vec4, который содержит компоненты red, green, blue и Alpha (RGBA), значения которых чаще всего находятся в диапазоне от 0.0 до 1.0.

GPU могут жертвовать точностью ради скорости — использование mediump вместо highp может повысить производительность на мобильных устройствах, сохраняя при этом приемлемое качество изображения.

GPU исполняют код для фрагментов группами потоков (которые называют warp или wavefront). Если в коде встречается ветвление, которое разделяет потоки в группе, GPU приходится выполнять обе ветки кода для всей группы. Эта избыточная работа делает ветвления менее эффективными:

glsl
1// Эффективно: операции без ветвления
2float t = step(0.5, value);
3vec3 result = mix(colorA, colorB, t);
4
5// Неэффективно: динамическое ветвление
6vec3 result = (value > 0.5) ? colorB : colorA;

Геометрия полноэкранного прямоугольника

Многие продвинутые эффекты на GPU — от размытия в движении до динамики жидкостей — часто реализуются с помощью двух треугольников, которые определяют прямоугольник (quad). Эти два треугольника образуют прямоугольник, вызывая фрагментный шейдер — программу для GPU — один раз для каждого пикселя. Если он полноэкранный, мы называем это «полноэкранным прямоугольником» (full-screen quad). Эта техника позволяет запускать фрагментный шейдер для каждого пикселя экрана или области прямоугольника.

Полноэкранный прямоугольник (квад) обычно использует два треугольника для покрытия области просмотра (viewport), поскольку GPU высоко оптимизированы для обработки треугольников. Этот метод гарантирует, что вся прямоугольная область будет последовательно покрыта.

Реализация полноэкранного прямоугольника

Эта реализация отрисовывает полноэкранный прямоугольник. Мы создадим визуальный тестовый паттерн, выводя UV-координаты в виде цветов — эта техника помогает убедиться, что наша геометрия корректно покрывает область просмотра и что отображение координат работает как ожидается. Эта же структура позже послужит основой для эффектов постобработки.

Шейдеры

glsl
1// Вершинный шейдер
2#version 300 es
3layout(location = 0) in vec2 aPosition;
4out vec2 vUV;
5
6void main() {
7 gl_Position = vec4(aPosition, 0.0, 1.0);
8 vUV = aPosition * 0.5 + 0.5;
9 // Инвертируем Y для начала координат текстуры в левом верхнем углу
10 vUV.y = 1.0 - vUV.y;
11}
12
13// Фрагментный шейдер
14#version 300 es
15precision mediump float;
16in vec2 vUV;
17out vec4 fragColor;
18
19void main() {
20 // Визуализация градиента по RG каналам
21 fragColor = vec4(vUV, 0.5, 1.0);
22}

Код на JavaScript

javascript
1// Получение контекста
2const gl = canvas.getContext('webgl2');
3if (!gl) throw new Error('WebGL 2 не поддерживается');
4
5// Компиляция шейдера
6function compileShader(gl, source, type) {
7 const shader = gl.createShader(type);
8 gl.shaderSource(shader, source);
9 gl.compileShader(shader);
10
11 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
12 const info = gl.getShaderInfoLog(shader);
13 gl.deleteShader(shader);
14 throw new Error(`Ошибка компиляции: ${info}`);
15 }
16 return shader;
17}
18
19// Создание программы
20function createProgram(gl, vertSrc, fragSrc) {
21 const program = gl.createProgram();
22 gl.attachShader(program, compileShader(gl, vertSrc, gl.VERTEX_SHADER));
23 gl.attachShader(program, compileShader(gl, fragSrc, gl.FRAGMENT_SHADER));
24 gl.linkProgram(program);
25
26 if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
27 throw new Error(`Ошибка линковки: ${gl.getProgramInfoLog(program)}`);
28 }
29
30 return program;
31}
32
33// VAO для полноэкранного прямоугольника
34function createFullScreenQuad(gl) {
35 const vao = gl.createVertexArray();
36 gl.bindVertexArray(vao);
37
38 // Два треугольника: от [-1,-1] до [1,1]
39 const vertices = new Float32Array([
40 -1, -1, 1, -1, -1, 1, // Треугольник 1
41 -1, 1, 1, -1, 1, 1, // Треугольник 2
42 ]);
43
44 const buffer = gl.createBuffer();
45 gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
46 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
47
48 gl.enableVertexAttribArray(0);
49 gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
50
51 gl.bindVertexArray(null);
52 return vao;
53}

Эта реализация отрисовывает полноэкранный квад с градиентом UV-координат, служащую основой для эффектов постобработки и вычислений на GPU.

Полный пример доступен по ссылке и демо здесь

Техники отладки

Отладка шейдеров сопряжена с особыми трудностями, поскольку они выполняются на графическом процессоре (GPU), отдельно от среды JavaScript. Вы не можете просто использовать console.log() внутри GLSL-шейдера для проверки значений. Поэтому приходится полагаться на специальные методы для понимания потока выполнения программы и данных, что помогает выявлять и устранять проблемы.

Проверка компиляции шейдеров

javascript
1if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
2 console.error('Ошибка шейдера:', gl.getShaderInfoLog(shader));
3}

Визуальная отладка

Промежуточные значения в виде цветов:

glsl
1// UV-координаты
2fragColor = vec4(vUV, 0.0, 1.0);
3// Нормали
4fragColor = vec4(normalize(vNormal) * 0.5 + 0.5, 1.0);

Итог

Полноэкранные прямоугольники являются основой для обработки изображений и симуляций на GPU. Два треугольника, покрывающие область просмотра, создают отображение "один к одному" между фрагментами и пикселями, обеспечивая параллельные вычисления по всему кадровому буферу. Эта техника лежит в основе эффектов постобработки, симуляций жидкостей и алгоритмов, ускоренных на GPU.


Эта статья входит в цикл материалов по созданию интерактивных 3D-визуализаций на WebGL 2.

Следующая статья: Рендеринг в текстуры с помощью Framebuffers.

Поговорим?