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

Содержание
- Введение в GLSL ES 3.0
- Система типов GLSL ES 3.0
- Реализация полноэкранного прямоугольника
- Техники отладки
- Итог
Как упоминалось в предыдущей статье, 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 отлично справляются с параллельной обработкой векторных значений.
glsl1vec4 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 блока в шейдере:
glsl1layout(std140) uniform TransformBlock {2 mat4 model;3 mat4 view;4 mat4 projection;5} transform;
JavaScript код создает буфер и загружает данные, соответствующие этой структуре:
javascript1// Создание и привязка uniform-буфера2const ubo = gl.createBuffer();3gl.bindBuffer(gl.UNIFORM_BUFFER, ubo);4gl.bufferData(gl.UNIFORM_BUFFER, transformData, gl.DYNAMIC_DRAW);56// Связывание с шейдерной программой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 инкапсулируют конфигурацию вершинных атрибутов, сохраняя привязки буферов, указатели на атрибуты и состояния их активности. Это избавляет от повторяющихся вызовов настройки:
javascript1const vao = gl.createVertexArray();2gl.bindVertexArray(vao);3// Настроить все атрибуты один раз4gl.bindVertexArray(null);56// Позже: один вызов для использования7gl.bindVertexArray(vao);8gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
Явные местоположения атрибутов (Attribute Locations)
Вместо запроса индексов атрибутов во время выполнения с помощью gl.getAttribLocation
, вы можете жестко задать местоположение прямо в шейдере.
GLSL ES 3.0 поддерживает явное назначение местоположения атрибутов, устраняя необходимость в запросах gl.getAttribLocation
:
glsl1layout(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
:
glsl1#version 300 es23in vec3 aPosition; // Позиция вершины из буфера4in vec3 aColor; // Цвет вершины из буфера5out vec3 vColor; // Интерполированный цвет для фрагментов67uniform mat4 uModelViewMatrix; // Константа для всех вершин8uniform mat4 uProjectionMatrix; // Константа для всех вершин910void 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 автоматически интерполирует все выходные данные вершинного шейдера по всей поверхности каждого треугольника:
glsl1#version 300 es2precision mediump float;34in vec3 vColor; // Интерполированное значение из вершинного шейдера5out vec4 fragColor; // Выходной цвет RGBA во фреймбуфер67void 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 приходится выполнять обе ветки кода для всей группы. Эта избыточная работа делает ветвления менее эффективными:
glsl1// Эффективно: операции без ветвления2float t = step(0.5, value);3vec3 result = mix(colorA, colorB, t);45// Неэффективно: динамическое ветвление6vec3 result = (value > 0.5) ? colorB : colorA;
Геометрия полноэкранного прямоугольника
Многие продвинутые эффекты на GPU — от размытия в движении до динамики жидкостей — часто реализуются с помощью двух треугольников, которые определяют прямоугольник (quad). Эти два треугольника образуют прямоугольник, вызывая фрагментный шейдер — программу для GPU — один раз для каждого пикселя. Если он полноэкранный, мы называем это «полноэкранным прямоугольником» (full-screen quad). Эта техника позволяет запускать фрагментный шейдер для каждого пикселя экрана или области прямоугольника.
Полноэкранный прямоугольник (квад) обычно использует два треугольника для покрытия области просмотра (viewport), поскольку GPU высоко оптимизированы для обработки треугольников. Этот метод гарантирует, что вся прямоугольная область будет последовательно покрыта.
Реализация полноэкранного прямоугольника
Эта реализация отрисовывает полноэкранный прямоугольник. Мы создадим визуальный тестовый паттерн, выводя UV-координаты в виде цветов — эта техника помогает убедиться, что наша геометрия корректно покрывает область просмотра и что отображение координат работает как ожидается. Эта же структура позже послужит основой для эффектов постобработки.
Шейдеры
glsl1// Вершинный шейдер2#version 300 es3layout(location = 0) in vec2 aPosition;4out vec2 vUV;56void 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}1213// Фрагментный шейдер14#version 300 es15precision mediump float;16in vec2 vUV;17out vec4 fragColor;1819void main() {20 // Визуализация градиента по RG каналам21 fragColor = vec4(vUV, 0.5, 1.0);22}
Код на JavaScript
javascript1// Получение контекста2const gl = canvas.getContext('webgl2');3if (!gl) throw new Error('WebGL 2 не поддерживается');45// Компиляция шейдера6function compileShader(gl, source, type) {7 const shader = gl.createShader(type);8 gl.shaderSource(shader, source);9 gl.compileShader(shader);1011 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}1819// Создание программы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);2526 if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {27 throw new Error(`Ошибка линковки: ${gl.getProgramInfoLog(program)}`);28 }2930 return program;31}3233// VAO для полноэкранного прямоугольника34function createFullScreenQuad(gl) {35 const vao = gl.createVertexArray();36 gl.bindVertexArray(vao);3738 // Два треугольника: от [-1,-1] до [1,1]39 const vertices = new Float32Array([40 -1, -1, 1, -1, -1, 1, // Треугольник 141 -1, 1, 1, -1, 1, 1, // Треугольник 242 ]);4344 const buffer = gl.createBuffer();45 gl.bindBuffer(gl.ARRAY_BUFFER, buffer);46 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);4748 gl.enableVertexAttribArray(0);49 gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);5051 gl.bindVertexArray(null);52 return vao;53}
Эта реализация отрисовывает полноэкранный квад с градиентом UV-координат, служащую основой для эффектов постобработки и вычислений на GPU.
Полный пример доступен по ссылке и демо здесь
Техники отладки
Отладка шейдеров сопряжена с особыми трудностями, поскольку они выполняются на графическом процессоре (GPU), отдельно от среды JavaScript. Вы не можете просто использовать console.log()
внутри GLSL-шейдера для проверки значений. Поэтому приходится полагаться на специальные методы для понимания потока выполнения программы и данных, что помогает выявлять и устранять проблемы.
Проверка компиляции шейдеров
javascript1if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {2 console.error('Ошибка шейдера:', gl.getShaderInfoLog(shader));3}
Визуальная отладка
Промежуточные значения в виде цветов:
glsl1// 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.