Содержание

WebGL2Fundamentals.org

Fix, Fork, Contribute

WebGL2 3D Перспективно-корректное наложение текстур

Эта статья является продолжением серии статей о WebGL. Первая началась с основ. Эта статья покрывает перспективно-корректное наложение текстур. Для понимания вам вероятно нужно прочитать о перспективной проекции и возможно текстурировании также. Вам также нужно знать о varying и что они делают, но я кратко расскажу о них здесь.

Итак, в статье “как это работает” мы рассмотрели как работают varying. Вершинный шейдер может объявить varying и установить ему какое-то значение. После того как вершинный шейдер был вызван 3 раза WebGL нарисует треугольник. Пока он рисует этот треугольник для каждого пикселя он вызовет наш фрагментный шейдер и спросит какой цвет сделать для этого пикселя. Между 3 вершинами треугольника он передаст нам наши varying интерполированные между 3 значениями.

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

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

  #version 300 es

  // атрибут это вход (in) в вершинный шейдер.
  // Он будет получать данные из буфера
  in vec4 a_position;

  // все шейдеры имеют основную функцию
  void main() {

    // gl_Position это специальная переменная, которую вершинный шейдер
    // отвечает за установку
    gl_Position = a_position;
  }

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

  #version 300 es

  // фрагментные шейдеры не имеют точности по умолчанию, поэтому нам нужно
  // выбрать одну. highp это хороший выбор по умолчанию
  precision highp float;

  // нам нужно объявить выход для фрагментного шейдера
  out vec4 outColor;

  void main() {
    // Просто установим выход на постоянный красно-фиолетовый
    outColor = vec4(1, 0, 0.5, 1);
  }

Итак, давайте сделаем так, чтобы он рисовал 2 прямоугольника в clip space. Мы передадим ему эти данные с X, Y, Z, и W для каждой вершины.

var positions = [
  -.8, -.8, 0, 1,  // 1й прямоугольник 1й треугольник
   .8, -.8, 0, 1,
  -.8, -.2, 0, 1,
  -.8, -.2, 0, 1,  // 1й прямоугольник 2й треугольник
   .8, -.8, 0, 1,
   .8, -.2, 0, 1,

  -.8,  .2, 0, 1,  // 2й прямоугольник 1й треугольник
   .8,  .2, 0, 1,
  -.8,  .8, 0, 1,
  -.8,  .8, 0, 1,  // 2й прямоугольник 2й треугольник
   .8,  .2, 0, 1,
   .8,  .8, 0, 1,
];

Вот это

Давайте добавим один varying float. Мы передадим это напрямую из вершинного шейдера в фрагментный шейдер.

  #version 300 es

  in vec4 a_position;
+  in float a_brightness;

+  out float v_brightness;

  void main() {
    gl_Position = a_position;

+    // просто передаем яркость в фрагментный шейдер
+    v_brightness = a_brightness;
  }

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

  #version 300 es

  precision highp float;

+  // передается из вершинного шейдера и интерполируется
+  in float v_brightness;

   // нам нужно объявить выход для фрагментного шейдера
   out vec4 outColor;

  void main() {
*    outColor = vec4(v_brightness, 0, 0, 1);  // красные
  }

Нам нужно предоставить данные для этого varying, поэтому мы создадим буфер и поместим туда некоторые данные. Одно значение на вершину. Мы установим все значения яркости для вершин слева в 0, а тех что справа в 1.

  // Создаем буфер и помещаем 12 значений яркости в него
  var brightnessBuffer = gl.createBuffer();

  // Привязываем его к ARRAY_BUFFER (думайте об этом как ARRAY_BUFFER = brightnessBuffer)
  gl.bindBuffer(gl.ARRAY_BUFFER, brightnessBuffer);

  var brightness = [
    0,  // 1й прямоугольник 1й треугольник
    1, 
    0, 
    0,  // 1й прямоугольник 2й треугольник
    1, 
    1, 

    0,  // 2й прямоугольник 1й треугольник
    1, 
    0, 
    0,  // 2й прямоугольник 2й треугольник
    1, 
    1, 
  ];
  
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(brightness), gl.STATIC_DRAW);

Нам также нужно найти местоположение атрибута a_brightness во время инициализации

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

и настроить этот атрибут во время рендеринга

  // Включаем атрибут
  gl.enableVertexAttribArray(brightnessAttributeLocation);

  // Привязываем буфер позиций.
  gl.bindBuffer(gl.ARRAY_BUFFER, brightnessBuffer);

  // Говорим атрибуту как получать данные из brightnessBuffer (ARRAY_BUFFER)
  var size = 1;          // 1 компонент на итерацию
  var type = gl.FLOAT;   // данные это 32-битные float
  var normalize = false; // не нормализуем данные
  var stride = 0;        // 0 = двигаемся вперед на size * sizeof(type) каждую итерацию чтобы получить следующую позицию
  var offset = 0;        // начинаем с начала буфера
  gl.vertexAttribPointer(
      brightnessAttributeLocation, size, type, normalize, stride, offset);

И теперь когда мы рендерим мы получаем два прямоугольника, которые черные слева когда brightness равен 0 и красные справа когда brightness равен 1 и для области между brightness интерполируется или (варьируется) когда он проходит через треугольники.

Итак, из статьи о перспективе мы знаем, что WebGL берет любое значение, которое мы помещаем в gl_Position и делит его на gl_Position.w.

В вершинах выше мы предоставили 1 для W, но поскольку мы знаем, что WebGL будет делить на W, то мы должны быть в состоянии сделать что-то вроде этого и получить тот же результат.

  var mult = 20;
  var positions = [
      -.8,  .8, 0, 1,  // 1й прямоугольник 1й треугольник
       .8,  .8, 0, 1,
      -.8,  .2, 0, 1,
      -.8,  .2, 0, 1,  // 1й прямоугольник 2й треугольник
       .8,  .8, 0, 1,
       .8,  .2, 0, 1,

      -.8       , -.2       , 0,    1,  // 2й прямоугольник 1й треугольник
       .8 * mult, -.2 * mult, 0, mult,
      -.8       , -.8       , 0,    1,
      -.8       , -.8       , 0,    1,  // 2й прямоугольник 2й треугольник
       .8 * mult, -.2 * mult, 0, mult,
       .8 * mult, -.8 * mult, 0, mult,
  ];

Выше вы можете видеть, что для каждой точки справа во втором прямоугольнике мы умножаем X и Y на mult, но мы также устанавливаем W в mult. Поскольку WebGL будет делить на W, мы должны получить точно такой же результат, верно?

Ну вот это

Обратите внимание, что 2 прямоугольника нарисованы в том же месте, где они были раньше. Это доказывает, что X * MULT / MULT(W) все еще просто X и то же самое для Y. Но цвета другие. Что происходит?

Оказывается, WebGL использует W для реализации перспективно-корректного наложения текстур или, скорее, для перспективно-корректной интерполяции varying.

Фактически, чтобы было легче увидеть, давайте взломаем фрагментный шейдер до этого

outColor = vec4(fract(v_brightness * 10.), 0, 0, 1);  // красные

умножение v_brightness на 10 заставит значение идти от 0 до 10. fract будет просто держать дробную часть, так что оно будет идти 0 до 1, 0 до 1, 0 до 1, 10 раз.

Линейная интерполяция от одного значения к другому была бы этой формулой

 result = (1 - t) * a + t * b

Где t это значение от 0 до 1, представляющее некоторую позицию между a и b. 0 в a и 1 в b.

Для varying, однако, WebGL использует эту формулу

 result = (1 - t) * a / aW + t * b / bW
          -----------------------------
             (1 - t) / aW + t / bW

Где aW это W, который был установлен на gl_Position.w, когда varying был установлен в a, и bW это W, который был установлен на gl_Position.w, когда varying был установлен в b.

Почему это важно? Ну, вот простой текстурированный куб, как мы закончили в статье о текстурах. Я настроил UV координаты, чтобы они шли от 0 до 1 на каждой стороне, и он использует 4x4 пиксельную текстуру.

Теперь давайте возьмем этот пример и изменим вершинный шейдер так, чтобы мы делили на W сами. Нам просто нужно добавить 1 строку.

#version 300 es

in vec4 a_position;
in vec2 a_texcoord;

uniform mat4 u_matrix;

out vec2 v_texcoord;

void main() {
  // Умножаем позицию на матрицу.
  gl_Position = u_matrix * a_position;

+  // Вручную делим на W.
+  gl_Position /= gl_Position.w;

  // Передаем texcoord в фрагментный шейдер.
  v_texcoord = a_texcoord;
}

Деление на W означает, что gl_Position.w в итоге будет 1. X, Y, и Z выйдут точно так же, как если бы мы позволили WebGL сделать деление за нас. Ну, вот результаты.

Мы все еще получаем 3D куб, но текстуры искажаются. Это потому что, не передавая W как было раньше, WebGL не может сделать перспективно-корректное наложение текстур. Или более правильно, WebGL не может сделать перспективно-корректную интерполяцию varying.

Если вы помните, W был нашим значением Z из нашей матрицы перспективы). С W просто равным 1 WebGL просто в итоге делает линейную интерполяцию. Фактически, если вы возьмете уравнение выше

 result = (1 - t) * a / aW + t * b / bW
          -----------------------------
             (1 - t) / aW + t / bW

И измените все W на 1, мы получим

 result = (1 - t) * a / 1 + t * b / 1
          ---------------------------
             (1 - t) / 1 + t / 1

Деление на 1 ничего не делает, поэтому мы можем упростить до этого

 result = (1 - t) * a + t * b
          -------------------
             (1 - t) + t

(1 - t) + t когда t идет от 0 до 1 это то же самое что 1. Например если t был .7 мы получили бы (1 - .7) + .7 что есть .3 + .7 что есть 1. Другими словами мы можем убрать низ, так что мы остаемся с

 result = (1 - t) * a + t * b

Что то же самое что уравнение линейной интерполяции выше.

Надеюсь, теперь ясно, почему WebGL использует матрицу 4x4 и 4-значные векторы с X, Y, Z, и W. X и Y деленные на W получают clip space координату. Z деленный на W также получает clipspace координату в Z, а W все еще используется во время интерполяции varying и предоставляет возможность делать перспективно-корректное наложение текстур.

Игровые консоли середины 1990-х

Как небольшая деталь, PlayStation 1 и некоторые другие игровые консоли той же эпохи не делали перспективно-корректное наложение текстур. Глядя на результаты выше, вы теперь можете увидеть, почему они выглядели так, как выглядели.

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