Урок №19. Тестирование глубины в OpenGL

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

  Обновл. 13 Июн 2020  | 

 1774

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

Буфер глубины

Буфер глубины (или ещё «Z-буфер») — это буфер, хранящий информацию о каждом фрагменте и имеющий ту же ширину и высоту, что и цветовой буфер (содержащий все цвета фрагмента: визуальный вывод). Буфер глубины автоматически создается оконной системой и сохраняет свои значения в виде 16-, 24- или 32-битных переменных типа с плавающей точкой. В большинстве систем вы обнаружите использование данного буфера с точностью 24 бит.

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

Проверка глубины выполняется в пространстве экрана после запуска фрагментного шейдера (и после трафаретного теста (англ. «stencil test»), который мы рассмотрим в следующем уроке). Координаты пространства экрана относятся непосредственно к окну просмотра, определяемому функцией glViewport() в OpenGL, и могут быть доступны во фрагментном шейдере через встроенную GLSL-переменную gl_FragCoord. Компоненты x и y переменной gl_FragCoord представляют координаты фрагмента в экранном пространстве (с центром в точке (0.0), находящейся в нижнем левом углу). У переменной gl_FragCoord также присутствует и z-компонента, которая содержит значение глубины фрагмента. Содержимое z-компоненты и есть то самое значение, которое сравнивается с содержимым буфера глубины.

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

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

По умолчанию тестирование глубины отключено. Чтобы задействовать тестирование глубины нам нужно прибегнуть к помощи опции GL_DEPTH_TEST:

После включения OpenGL автоматически сохраняет фрагменты, если их z-значения в буфере глубины прошли тест глубины, и отбрасывает те фрагменты, которые не прошли тест глубины. Если у вас включено тестирование глубины, то перед каждым новым кадром вы должны очищать буфер глубины, используя GL_DEPTH_BUFFER_BIT; в противном случае, будут задействованы значения глубины, доставшиеся из последнего кадра:

Можно представить себе определенные ситуации, когда вы хотите выполнить тест глубины для всех фрагментов и, исходя из результата, отбросить некоторые из них, но при этом не обновлять буфер глубины. Если так посмотреть, то вы (временно) используете буфер глубины в режиме read-only (только-для-чтения). OpenGL позволяет нам отключить запись в буфер глубины, используя вызов функции glDepthMask() с опцией GL_FALSE:

Обратите внимание, что это работает только в том случае, если включена проверка глубины.

Функция проверки глубины


OpenGL позволяет нам изменить операторы сравнения, используемые при проверке глубины. Благодаря этому у нас появляется возможность контролировать ситуации, когда OpenGL должен оставлять или отбрасывать фрагменты, а также — когда обновлять буфер глубины. Мы можем изменить оператор сравнения (или функцию глубины), вызвав glDepthFunc():

Эта функция принимает несколько операторов сравнения, перечисленных в таблице ниже:

Опция Описание
GL_ALWAYS Тест глубины всегда проходит успешно.
GL_NEVER Тест глубины никогда не проходит успешно.
GL_LESS Тест глубины проходит успешно, если значение глубины фрагмента меньше сохраненного значения глубины.
GL_EQUAL Тест глубины проходит успешно, если значение глубины фрагмента равно сохраненному значению глубины.
GL_LEQUAL Тест глубины проходит успешно, если значение глубины фрагмента меньше или равно сохраненному значению глубины.
GL_GREATER Тест глубины проходит успешно, если значение глубины фрагмента больше сохраненного значения глубины.
GL_NOTEQUAL Тест глубины проходит успешно, если значение глубины фрагмента не равно сохраненному значению глубины.
GL_GEQUAL Тест глубины проходит успешно, если значение глубины фрагмента больше или равно сохраненному значению глубины.

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

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

  Google Drive / Исходный код — Урок №19. Тестирование глубины в OpenGL

  GitHub / Исходный код — Урок №19. Тестирование глубины в OpenGL

В исходном коде мы изменили функцию глубины на GL_ALWAYS:

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

Установка функции обратно на GL_LESS предоставит нам тот вид сцены, к которому мы привыкли:


Точность тестирования глубины

Буфер глубины содержит значения глубины в диапазоне от 0.0 до 1.0, и он сравнивает своё содержимое с z-значениями всех объектов в сцене с точки зрения наблюдателя. В пространстве вида данные z-значения могут быть любым значением между ближней (near) и дальней (far) плоскостями усеченной пирамиды видимости. Таким образом, нам необходимо найти способ преобразовать эти z-значения пространства вида в диапазон [0, 1], и один из этих способов — это линейно преобразовать их. Следующее (линейное) уравнение преобразует z-значение в значение глубины между 0.0 и 1.0:

где near и far — это значения ближней и дальней плоскостей усеченной пирамиды видимости, которые мы использовали для создания матрицы проекции, чтобы задать видимую часть усеченного конуса видимости. Уравнение принимает z-значение глубины в пределах пирамиды видимости и преобразует его в диапазон [0, 1]. Соотношение между z-значением и соответствующим ему значением глубины представлено на следующем графике:

Примечание: Обратите внимание, что все вычисления дают значение глубины, близкое к 0.0 — когда объект находится рядом, и значение, близкое к 1.0 — когда объект расположен возле дальней плоскости.

Однако на практике такой линейный буфер глубины почти никогда не используется. Из-за свойств проекции используется нелинейное уравнение глубины, пропорциональное 1/z. В результате мы получаем большую точность, при малых z, и гораздо меньшую точность при больших z.

Поскольку нелинейная функция пропорциональна 1/z, то z-значения, лежащие между 1.0 и 2.0 приведут к значениям глубины, лежащим между 1.0 и 0.5, что составляет половину диапазона отрезка [0, 1], что дает нам большую точность при малых z-значениях. Стоит отметить, что z-значения, лежащие между 50.0 и 100.0 составили бы только 2% от диапазона отрезка [0, 1]. Такое уравнение, которое также учитывает ближние и дальние расстояния, приведено ниже:

Не волнуйтесь, если вы до конца понимаете, как работает данное уравнение. Важно помнить, что значения в буфере глубины в отсеченном пространстве не линейны (они линейны в пространстве вида до применения матрицы проекции). Значение 0.5 в буфере глубины не означает, что z-значение пикселя находится наполовину в пирамиде видимости; z-значение вершины на самом деле довольно близко к ближней плоскости! Вы можете увидеть нелинейную связь между z-значением и результирующим значением буфера глубины на следующем графике:

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

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

Визуализация буфера глубины


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

Если бы мы затем запустили программу, то, вероятно, заметили бы, что всё стало белым, из чего можно сделать вывод, что все наши значения — это максимальные значения глубины, равные 1.0. Так почему же ни одно из значений глубины не приближается к 0.0 и, следовательно, цвет не темнеет?

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

Это ясно показывает нелинейность значения глубины. Близкие объекты оказывают гораздо большее влияние на величину глубины, чем удаленные объекты. Только перемещение на несколько сантиметров может привести к тому, что цвета перейдут от темного к полностью белому.

Однако, мы можем преобразовать нелинейные значения глубины фрагмента обратно в линейные. Чтобы достичь этого, нам нужно обратить процесс проецирования только для значений глубины. Это означает, что мы должны сначала повторно преобразовать значения глубины из диапазона [0, 1] в нормализованные координаты устройства в диапазоне [-1, 1]. Затем нам нужно обратить нелинейное уравнение (уравнение №2), как это делается в проекционной матрице, и применить это обратное уравнение к результирующему значению глубины. В результате получается линейное значение глубины.

Сначала мы преобразуем значение глубины в координаты NDC (что не слишком сложно):

Затем мы берем результирующее значение ndc и применяем обратное преобразование для получения его линейного значения глубины:

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

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

Поскольку величина линеаризованной глубины варьируется от near до far, то большинство её значений будет больше 1.0 и будут отображаться полностью белым цветом. Разделив в функции main() линейное значение глубины на far, мы преобразуем линейное значение глубины в диапазон [0, 1]. Таким образом, мы можем постепенно видеть, что сцена становится тем ярче, чем ближе фрагменты находятся к дальней плоскости пирамиды видимости, что лучше подходит для визуализации.

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

Цвета в основном черные, потому что значения глубины линейно варьируются от near (ближней) плоскости (0.1) до far (дальней) плоскости (100), которая расположена всё ещё довольно далеко от нас. В результате мы находимся относительно близко к ближней плоскости и поэтому получаем более низкие (темные) значения глубины.

Z-конфликт

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

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

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

Z-конфликт является основной проблемой при использовании буфера глубины, и он обычно более заметен, когда объекты находятся дальше от зрителя (потому что буфер глубины имеет меньшую точность при больших z-значениях). Теоретически, Z-конфликт нельзя полностью предотвратить, но есть несколько практических трюков, которые помогут смягчить или избежать Z-конфликт в вашей сцене.

Предотвращение Z-конфликта


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

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

Ещё один большой трюк заключается в использовании буфера глубины более высокой точности. Большинство буферов глубины имеют точность 24 бита, но подавляющее число графических процессоров в настоящее время поддерживают 32-битные буферы глубины, что значительно повышает точность. Так что за счет потери некоторой производительности вы получите гораздо больше точности при тестировании глубины, уменьшив тем самым Z-конфликт.

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

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

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

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

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