Урок №35. Параллакс в OpenGL

  Дмитрий Бушуев  | 

  Обновл. 11 Сен 2020  | 

 830

В этом уроке мы рассмотрим эффект параллакса в OpenGL.

Эффект параллакса

Параллакс — это метод, аналогичный методу наложения карты нормалей, но основанный на других принципах. Так же, как и в случае с картами нормалей, это — техника, которая значительно повышает детализацию текстурированной поверхности, придающая ей ощущение глубины. Хотя параллакс также является иллюзией, он намного лучше передает ощущение глубины, и в сочетании с нормальным отображением (или «наложением карты нормалей») дает невероятно реалистичные результаты. Метод параллакса является логическим продолжением метода наложения карты нормалей, а поэтому перед изучением параллакса настоятельно рекомендуется получить представление о нормальном отображении, в частности — о касательном пространстве.

Параллакс тесно связан с семейством методов карт смещений, которые смещают или сдвигают вершины на основе информации о геометрии объекта, хранящейся внутри текстуры. Один из способов сделать это — взять плоскость с примерно 1000 вершинами и переместить каждую из этих вершин, основываясь на значении в текстуре, сообщающее нам высоту поверхности в рассматриваемой области. Такая текстура, содержащая значения высоты каждого текселя, называется картой высот. Пример карты высот, полученной на основе геометрических свойств простой кирпичной поверхности, выглядит примерно так:

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

Проблема со смещением вершин при использовании данного метода заключается в том, что плоскость должна содержать огромное количество треугольников, чтобы результат смещения выглядел реалистичным, иначе объект будет выглядеть блочным. Поскольку для каждой плоской поверхности может потребоваться более 10000 вершин, то мы получаем вычислительно неосуществимую задачу. Что, если бы мы могли каким-то образом достичь подобного реализма без необходимости в дополнительных вершинах? А если бы я сказал вам, что ранее показанная смещенная поверхность на самом деле визуализируется только при помощи всего 2 треугольников? Кирпичная поверхность, показанная на рисунке, визуализируется с помощью параллакса — метода смещения, который не требует дополнительных вершин для передачи глубины объекта, но (подобно наложению карты нормалей) использует хитроумную технику, чтобы обмануть пользователя.

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

Ломаная красная линия изображает значения карты высот в виде геометрического представления поверхности кирпича, а вектор V представляет направление визирования от поверхности к наблюдателю (viewDir). Если бы плоскость была объемной, то наблюдатель увидел бы поверхность в точке B. Однако, поскольку наша плоскость не имеет реального объема, направление взгляда вычисляется из точки A. Параллакс стремится сместить текстурные координаты в точке A таким образом, чтобы мы получили текстурные координаты точки B. Затем мы используем текстурные координаты точки B для всех последующих подвыборок текстуры, создавая впечатление, что наблюдатель действительно смотрит в точку B.

Хитрость заключается в том, чтобы выяснить, как, находясь в точке A, получить координаты текстуры точки B. Параллакс пытается решить эту проблему путем масштабирования вектора направления фрагмент-наблюдатель V на высоту фрагмента A. Таким образом, мы масштабируем длину вектора V, чтобы она была равна значению выборки H(A) из карты высот в точке A. На рисунке ниже показан масштабированный вектор P:

Затем мы берем этот вектор P и используем его векторные координаты, лежащие в плоскости, в качестве смещения текстурных координат. Это работает потому что вектор P вычисляется с использованием значения высоты из карты высот. Таким образом, чем выше высота фрагмента, тем большее смещение он получает.

В большинстве случаев данный маленький трюк отлично выполняет свою работу, но это все еще очень грубое приближение точки B. При быстром изменении высоты над поверхностью результаты имеют тенденцию выглядеть нереалистичными, так как вектор P не будет в конечном итоге близок к точке B, как вы можете видеть ниже:

Еще одна проблема с параллаксом заключается в том, что трудно определить, какие координаты получать из вектора P, когда поверхность произвольно поворачивается каким-либо образом. Мы бы предпочли сделать это в другом координатном пространстве, в котором x- и y- компоненты вектора P ориентированы вдоль поверхности текстуры. Если вы внимательно читали предыдущий урок, то, вероятно, уже догадались, как мы можем это сделать. И да, мы будем выполнять параллакс в касательном пространстве.

Путем преобразования вектора направления фрагмент-наблюдатель V в касательное пространство, преобразованный вектор P будет иметь x- и y- компоненты в базисе, построенном на касательном и бикасательном векторах поверхности. Поскольку касательный и бикасательный векторы направлены в том же направлении, что и координаты текстуры поверхности, то мы можем использовать x- и y- компоненты вектора P в качестве смещения текстурных координат, независимо от ориентации поверхности.

Но хватит теории, давайте перейдем к практике…

Отображение параллакса


Для параллакса мы будем использовать простую двумерную плоскость, для которой мы рассчитали её касательный и бикасательный векторы перед отправкой на графический процессор. На эту плоскость мы наложим диффузную текстуру…

… карту нормалей…

… и карту смещения…

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

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

У нас снова есть точки A и B, но на этот раз мы получаем вектор P, вычитая вектор V из текстурных координат в точке A. Вместо значений высоты мы можем получить значения глубины, вычитая в шейдерах значения выборки глубины из 1.0 или просто инвертируя его значения текстуры в любом графическом редакторе.

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

Затем во фрагментном шейдере мы реализуем логику отображения параллакса. Фрагментный шейдер выглядит следующим образом:

Мы определили функцию под названием ParallaxMapping(), которая принимает в качестве входных данных координаты текстуры фрагмента и вектор направления фрагмент-наблюдатель V в касательном пространстве. Функция возвращает смещенные координаты текстуры. Затем мы используем эти смещенные координаты текстуры в качестве текстурных координат для выборки из диффузной и нормальной карт. В результате диффузный и нормальный векторы фрагмента корректно соответствуют смещенной геометрии поверхности.

Давайте заглянем внутрь функции ParallaxMapping():

Мы берем исходные текстурные координаты texCoords и используем их для выборки значений высоты (или глубины) H(A) из карты глубины depthMap в точке A текущего фрагмента. Затем мы вычисляем вектор P — для этого берем x- и y- компоненты вектора viewDir из касательного пространства, делим их на его z-компоненту и масштабируем на величину H(A). Мы также ввели uniform-переменную height_scale для некоторого дополнительного контроля, поскольку эффект параллакса обычно слишком силен без дополнительного параметра масштаба. Затем мы вычитаем этот вектор P из текстурных координат для получения искомых смещенных текстурных координат.

Что интересно здесь отметить, так это деление viewDir.xy на viewDir.z. Так как вектор viewDir является нормализованным вектором, значение viewDir.z будет находиться где-то в диапазоне от 0.0 до 1.0. Когда viewDir практически параллелен поверхности, значение его z-компоненты близко к 0.0, и деление возвращает гораздо больший вектор P по сравнению с тем, когда viewDir практически перпендикулярен поверхности. Мы корректируем размер вектора P таким образом, что он смещает координаты текстуры в большем масштабе, когда наблюдатель смотрит на поверхность под углом; это дает более реалистичные результаты под углами.

Некоторые разработчики предпочитают не использовать деление на переменную viewDir.z, т.к. в некоторых случаях это может приводить к нежелательным результатам при направлении взгляда под углом к поверхности; этот измененный метод называется «параллаксом с ограничением смещения». Выбор того, какую технику использовать, обычно зависит от личных предпочтений.

Полученные координаты текстуры затем используются для выборки из других текстур (диффузных и нормальных), и это дает очень аккуратный эффект смещения, как вы можете видеть ниже, с height_scale равным примерно 0.1:

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

Также вы можете увидеть несколько странных граничных артефактов на краях плоскости с применённым параллакс-эффектом. Это происходит потому, что на краях плоскости смещенные текстурные координаты могут выйти за пределы диапазона [0,1]. В зависимости от режима(ов) наложения текстуры, это может приводить к нереалистичным результатам. Классный трюк для решения этой проблемы состоит в том, чтобы отбросить фрагмент всякий раз, когда он выходит за пределы используемого диапазона координат текстуры:

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

  Google Drive / Урок №35. Параллакс в OpenGL — Исходный код №1

  GitHub / Урок №35. Параллакс в OpenGL — Исходный код №1

Результат выглядит великолепно и при этом сам метод отрабатывает довольно шустро, т.к. мы используем только одну дополнительную выборку текстуры для параллакса. Однако у него тоже есть несколько недостатков, например, может искажаться воспринимаемый образ объекта при взгляде на него под определенным углом (подобно нормальному отображению), а также метод может давать некорректные результаты при наличии у объекта резких перепадов высоты, как вы можете видеть ниже:

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

Глубинный параллакс

Глубинный параллакс является расширением обычного параллакса в том смысле, что он использует те же самые принципы, но вместо одной выборки, для более точного определения вектора P в точке B, требуется несколько выборок. Метод дает гораздо лучшие результаты даже при крутых перепадах высот, так как за счет количества выборок повышается точность метода.

Общая идея глубинного параллакса состоит в том, что он делит весь диапазон глубин на несколько слоев одинаковой высоты/глубины. Для каждого из этих слоев мы производим выборку из карты глубины, сдвигая координаты текстуры вдоль направления P, пока не найдем значение глубины выборки, которое меньше значения глубины текущего слоя. Взгляните на следующее изображение:

Мы проходим слои глубины сверху вниз и для каждого слоя сравниваем его значение глубины со значением глубины, хранящимся в карте глубины. Если значение глубины слоя меньше значения глубины из карты глубины — это означает, что часть вектора P, соответствующая этому слою, лежит на поверхности. Мы продолжаем этот процесс до тех пор, пока глубина слоя не превысит значение, сохраненное в карте глубины: тогда эта точка находится ниже (смещенной) геометрической поверхности.

В этом примере мы видим, что значение из карты глубины на втором слое (D(2) = 0.73) ниже, чем значение глубины второго слоя (0.4), поэтому мы продолжаем. На следующей итерации значение глубины слоя 0.6 будет выше значения глубины выборки из карты глубины (D(3) = 0.37). Таким образом, мы можем заключить, что вектор P на третьем слое должен быть наиболее подходящим для представления смещенной геометрии объекта. Затем мы берем смещение текстурной координаты T3 из вектора P3 для смещения текстурных координат фрагмента. Вы можете видеть, как с увеличением глубины слоев увеличивается точность метода.

Чтобы реализовать эту технику нам нужно только изменить функцию ParallaxMapping(), ведь все необходимые переменные у нас уже есть:

Здесь мы сначала устанавливаем следующие значения переменных: указываем количество слоев, вычисляем смещение глубины каждого слоя и, наконец, вычисляем смещение текстурных координат, на которое мы должны будем смещаться вдоль направления P в каждом слое.

Затем мы перебираем все слои, начиная с самого верха, пока не найдем значение из карты глубины, которое будет меньше значения глубины слоя:

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

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

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

Здесь мы применяем скалярное произведение вектора viewDir и положительного направления оси Z и используем его результат для определения количества слоев как minLayers или maxLayers, в зависимости от угла, под которым мы смотрим на поверхность (обратите внимание, что положительное направление оси Z совпадает с вектором нормали поверхности в касательном пространстве). Если бы мы смотрели в направлении, параллельном поверхности, то мы бы использовали в общей сложности 32 слоя.

  Google Drive / Урок №35. Параллакс в OpenGL — Исходный код №2

  GitHub / Урок №35. Параллакс в OpenGL — Исходный код №2

А вот поверхность деревянной коробки для игрушек:

Диффузная текстура…

… карта нормалей…

… и карта глубины:

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

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

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

Окклюзионный параллакс


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

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

Код для окклюзионного параллакса является расширением метода глубинного параллакса, и должен быть не слишком сложным для вас:

После того как мы нашли слой глубины после пересечения (смещенной) геометрии поверхности, мы также получаем текстурные координаты слоя глубины до пересечения. Затем мы вычисляем расстояние глубины (смещенной) геометрии от соответствующих глубинных слоев и интерполируем данные два значения. Линейная интерполяция — это базовая интерполяция между текстурными координатами обоих слоев. Затем функция окончательно возвращает конечные интерполированные координаты текстуры.

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

  Google Drive / Урок №35. Параллакс в OpenGL — Исходный код №3

  GitHub / Урок №35. Параллакс в OpenGL — Исходный код №3

Заключение

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

Дополнительные ресурсы


   How Parallax Displacement Mapping Works: хорошее видео от TheBennyBox о том, как работает параллакс.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (3 оценок, среднее: 5,00 из 5)
Загрузка...

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *