Содержание

WebGL2Fundamentals.org

Fix, Fork, Contribute

Как работает WebGL2

Это продолжение Основ WebGL. Прежде чем мы продолжим, я думаю, нам нужно обсудить на базовом уровне, что WebGL и ваш GPU на самом деле делают. Есть в основном 2 части в этой GPU штуке. Первая часть обрабатывает вершины (или потоки данных) в вершины clip space. Вторая часть рисует пиксели на основе первой части.

Когда вы вызываете

gl.drawArrays(gl.TRIANGLES, 0, 9);

9 там означает “обработать 9 вершин”, так что вот 9 вершин обрабатываются.

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

Предполагая, что вы рисуете TRIANGLES, каждый раз, когда эта первая часть генерирует 3 вершины, GPU использует их для создания треугольника. Он выясняет, каким пикселям соответствуют 3 точки треугольника, а затем растеризует треугольник, что является модным словом для “рисует его пикселями”. Для каждого пикселя он вызовет ваш фрагментный шейдер, спрашивая, какой цвет сделать для этого пикселя. Ваш фрагментный шейдер выводит vec4 с цветом, который он хочет для этого пикселя.

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

Как простой пример, давайте просто передадим координаты clip space, которые мы вычислили напрямую от вершинного шейдера к фрагментному шейдеру.

Мы будем рисовать простым треугольником. Продолжая с нашего предыдущего примера, давайте изменим наш прямоугольник на треугольник.

// Заполняем буфер значениями, которые определяют треугольник.
function setGeometry(gl) {
  gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array([
             0, -100,
           150,  125,
          -175,  100]),
      gl.STATIC_DRAW);
}

И нам нужно рисовать только 3 вершины.

// Рисуем сцену.
function drawScene() {
  ...
  // Рисуем геометрию.
*  gl.drawArrays(gl.TRIANGLES, 0, 3);
}

Затем в нашем вершинном шейдере мы объявляем varying, делая out для передачи данных в фрагментный шейдер.

out vec4 v_color;
...
void main() {
  // Умножаем позицию на матрицу.
  gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);

  // Преобразуем из clip space в цветовое пространство.
  // Clip space идет от -1.0 до +1.0
  // Цветовое пространство идет от 0.0 до 1.0
*  v_color = gl_Position * 0.5 + 0.5;
}

И затем мы объявляем тот же varying как in в фрагментном шейдере.

#version 300 es

precision highp float;

in vec4 v_color;

out vec4 outColor;

void main() {
*  outColor = v_color;
}

WebGL соединит varying в вершинном шейдере с varying того же имени и типа в фрагментном шейдере.

Вот рабочая версия.

Перемещайте, масштабируйте и поворачивайте треугольник. Обратите внимание, что поскольку цвета вычисляются из clip space, они не двигаются с треугольником. Они относительны к фону.

Теперь подумайте об этом. Мы вычисляем только 3 вершины. Наш вершинный шейдер вызывается только 3 раза, поэтому он вычисляет только 3 цвета, но наш треугольник имеет много цветов. Вот почему это называется varying.

WebGL берет 3 значения, которые мы вычислили для каждой вершины, и когда он растеризует треугольник, он интерполирует между значениями, которые мы вычислили для вершин. Для каждого пикселя он вызывает наш фрагментный шейдер с интерполированным значением для этого пикселя.

В примере выше мы начинаем с 3 вершин

Вершины
0-100
150125
-175100

Наш вершинный шейдер применяет матрицу для перемещения, поворота, масштабирования и преобразования в clip space. Значения по умолчанию для перемещения, поворота и масштабирования: перемещение = 200, 150, поворот = 0, масштаб = 1,1, так что это действительно только перемещение. Учитывая, что наш backbuffer 400x300, наш вершинный шейдер применяет матрицу и затем вычисляет следующие 3 вершины clip space.

значения, записанные в gl_Position
0.0000.660
0.750-0.830
-0.875-0.660

Он также преобразует их в цветовое пространство и записывает их в varying v_color, который мы объявили.

значения, записанные в v_color
0.50000.8300.5
0.87500.0860.5
0.06250.1700.5

Те 3 значения, записанные в v_color, затем интерполируются и передаются в фрагментный шейдер для каждого пикселя.

v_color интерполируется между v0, v1 и v2

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

in vec2 a_position;
+in vec4 a_color;
...
out vec4 v_color;

void main() {
   ...
  // Копируем цвет из атрибута в varying.
*  v_color = a_color;
}

Теперь нам нужно предоставить цвета для WebGL.

  // ищем, куда должны идти данные вершин.
  var positionLocation = gl.getAttribLocation(program, "a_position");
+  var colorLocation = gl.getAttribLocation(program, "a_color");
  ...
+  // Создаем буфер для цветов.
+  var buffer = gl.createBuffer();
+  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+
+  // Устанавливаем цвета.
+  setColors(gl);

  // настраиваем атрибуты
  ...
+  // говорим атрибуту цвета, как извлекать данные из текущего ARRAY_BUFFER
+  gl.enableVertexAttribArray(colorLocation);
+  var size = 4;
+  var type = gl.FLOAT;
+  var normalize = false;
+  var stride = 0;
+  var offset = 0;
+  gl.vertexAttribPointer(colorLocation, size, type, normalize, stride, offset);

  ...

+// Заполняем буфер цветами для 2 треугольников
+// которые составляют прямоугольник.
+function setColors(gl) {
+  // Выбираем 2 случайных цвета.
+  var r1 = Math.random();
+  var b1 = Math.random();
+  var g1 = Math.random();
+
+  var r2 = Math.random();
+  var b2 = Math.random();
+  var g2 = Math.random();
+
+  gl.bufferData(
+      gl.ARRAY_BUFFER,
+      new Float32Array(
+        [ r1, b1, g1, 1,
+          r1, b1, g1, 1,
+          r1, b1, g1, 1,
+          r2, b2, g2, 1,
+          r2, b2, g2, 1,
+          r2, b2, g2, 1]),
+      gl.STATIC_DRAW);
+}

И вот результат.

Обратите внимание, что у нас есть 2 треугольника сплошного цвета. Тем не менее, мы передаем значения в varying, поэтому они варьируются или интерполируются по треугольнику. Просто мы использовали тот же цвет на каждой из 3 вершин каждого треугольника. Если мы сделаем каждый цвет разным, мы увидим интерполяцию.

// Заполняем буфер цветами для 2 треугольников
// которые составляют прямоугольник.
function setColors(gl) {
  // Делаем каждую вершину разным цветом.
  gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array(
*        [ Math.random(), Math.random(), Math.random(), 1,
*          Math.random(), Math.random(), Math.random(), 1,
*          Math.random(), Math.random(), Math.random(), 1,
*          Math.random(), Math.random(), Math.random(), 1,
*          Math.random(), Math.random(), Math.random(), 1,
*          Math.random(), Math.random(), Math.random(), 1]),
      gl.STATIC_DRAW);
}

И теперь мы видим интерполированный varying.

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

Что делают эти команды буфера и атрибута?

Буферы - это способ получения данных вершин и других данных на вершину на GPU. gl.createBuffer создает буфер. gl.bindBuffer устанавливает этот буфер как буфер для работы. gl.bufferData копирует данные в текущий буфер.

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

Для этого сначала мы спрашиваем WebGL, какие локации он назначил атрибутам. Например, в коде выше у нас есть

// ищем, куда должны идти данные вершин.
var positionLocation = gl.getAttribLocation(program, "a_position");
var colorLocation = gl.getAttribLocation(program, "a_color");

Как только мы знаем локацию атрибута, мы выдаем 2 команды.

gl.enableVertexAttribArray(location);

Эта команда говорит WebGL, что мы хотим предоставить данные из буфера.

gl.vertexAttribPointer(
    location,
    numComponents,
    typeOfData,
    normalizeFlag,
    strideToNextPieceOfData,
    offsetIntoBuffer);

И эта команда говорит WebGL получать данные из буфера, который был последним привязан с gl.bindBuffer, сколько компонентов на вершину (1 - 4), какой тип данных (BYTE, FLOAT, INT, UNSIGNED_SHORT, и т.д.), шаг который означает, сколько байт пропустить, чтобы получить от одного куска данных к следующему куску данных, и смещение для того, как далеко в буфере находятся наши данные.

Количество компонентов всегда от 1 до 4.

Если вы используете 1 буфер на тип данных, то и шаг, и смещение могут всегда быть 0. 0 для шага означает “использовать шаг, который соответствует типу и размеру”. 0 для смещения означает начать с начала буфера. Установка их в значения, отличные от 0, более сложна, и хотя это может иметь некоторые преимущества с точки зрения производительности, это не стоит усложнения, если только вы не пытаетесь довести WebGL до его абсолютных пределов.

Я надеюсь, что это проясняет буферы и атрибуты.

Вы можете взглянуть на эту интерактивную диаграмму состояния для другого способа понимания того, как работает WebGL.

Далее давайте пройдемся по шейдерам и GLSL.

Для чего нужен normalizeFlag в vertexAttribPointer?

Флаг нормализации предназначен для всех не-плавающих типов. Если вы передаете false, то значения будут интерпретироваться как тип, которым они являются. BYTE идет от -128 до 127, UNSIGNED_BYTE идет от 0 до 255, SHORT идет от -32768 до 32767 и т.д...

Если вы устанавливаете флаг нормализации в true, то значения BYTE (-128 до 127) представляют значения -1.0 до +1.0, UNSIGNED_BYTE (0 до 255) становятся 0.0 до +1.0. Нормализованный SHORT также идет от -1.0 до +1.0, просто у него больше разрешения, чем у BYTE.

Самое распространенное использование нормализованных данных - для цветов. Большую часть времени цвета идут только от 0.0 до 1.0. Использование полного float для каждого красного, зеленого, синего и альфа использовало бы 16 байт на вершину на цвет. Если у вас сложная геометрия, это может сложиться в много байт. Вместо этого вы могли бы конвертировать ваши цвета в UNSIGNED_BYTE, где 0 представляет 0.0, а 255 представляет 1.0. Теперь вам понадобилось бы только 4 байта на цвет на вершину, экономия 75%.

Давайте изменим наш код, чтобы делать это. Когда мы говорим WebGL, как извлекать наши цвета, мы бы использовали

  var size = 4;
*  var type = gl.UNSIGNED_BYTE;
*  var normalize = true;
  var stride = 0;
  var offset = 0;
  gl.vertexAttribPointer(colorLocation, size, type, normalize, stride, offset);

И когда мы заполняем наш буфер цветами, мы бы использовали

// Заполняем буфер цветами для 2 треугольников
// которые составляют прямоугольник.
function setColors(gl) {
  // Выбираем 2 случайных цвета.
  var r1 = Math.random() * 256; // 0 до 255.99999
  var b1 = Math.random() * 256; // эти значения
  var g1 = Math.random() * 256; // будут обрезаны
  var r2 = Math.random() * 256; // когда сохранены в
  var b2 = Math.random() * 256; // Uint8Array
  var g2 = Math.random() * 256;

gl.bufferData( gl.ARRAY_BUFFER, new Uint8Array( // Uint8Array [ r1, b1, g1, 255, r1, b1, g1, 255, r1, b1, g1, 255, r2, b2, g2, 255, r2, b2, g2, 255, r2, b2, g2, 255]), gl.STATIC_DRAW); }

Вот этот пример.

Есть предложения или замечания? Создайте issue на GitHub.
comments powered by Disqus