Урок №22. Отсечение граней в OpenGL

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

  Обновл. 23 Сен 2021  | 

 7149

 ǀ   1 

На этом уроке мы рассмотрим тему отсечения граней в OpenGL.

Лицевая и тыльная стороны

Давайте мысленно представим себе 3D-куб и попробуем подсчитать максимальное количество граней, которые можно увидеть, обозревая его из некоторой точки пространства. Скорее всего, для нашего взора будут доступны максимум лишь 3 грани (из 6 возможных). Другими словами, вы можете смотреть на куб из любой точки (и/или направления) пространства, но при этом вы никогда не увидите больше 3 граней. Так зачем же тратить силы на отрисовку тех граней, которых мы даже не видим? Если бы мы могли каким-то образом их отбросить, то сэкономили бы более 50% от общего количества запусков фрагментного шейдера этого куба!

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

Это действительно отличная идея, но есть одна проблема, которую мы должны решить: как мы будем определять тот факт, что какая-то из граней объекта с позиции наблюдателя не просматривается? Если представить себе любую замкнутую фигуру, то её грани будут иметь две стороны. Каждая грань будет обращена к наблюдателю либо своей лицевой стороной, либо тыльной. Что, если бы мы могли визуализировать только грани, обращенные к наблюдателю исключительно лицевой стороной?

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

Порядок обхода


Когда мы задаем некоторый набор вершин треугольника, то вместе с этим определяем и порядок их расположения, зависящий от того, каким образом будет производиться обход вершин треугольника при передвижении от одной вершины к другой: по часовой стрелке (англ. «clockwise») или против часовой стрелки (англ. «counter-clockwise»). Каждый треугольник состоит из 3 вершин, и мы указываем данные вершины в том порядке, в котором будет происходить их обход:

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

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

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

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

С помощью вершинных данных мы определили 2 треугольника так, что порядок их обхода соответствует движению против часовой стрелки (для обоих треугольников — вершина 1, вершина 2, вершина 3). Однако с текущей позиции наблюдателя дальний треугольник имеет порядок обхода вершин по часовой стрелке: вершина 1, вершина 2 и вершина 3. Несмотря на то, что мы задавали его с порядком обхода против часовой стрелки, теперь он отображается с порядком по часовой стрелке. Это именно те непросматриваемые грани, которые мы хотим отсечь!

Отсечение граней

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

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

Чтобы включить режим отсечения граней, необходимо воспользоваться опцией GL_CULL_FACE:

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

OpenGL также предоставляет возможность изменить тип отсекаемых граней. Что, если мы хотим отсечь «фронтальные», а не «тыльные» грани? Для этого необходимо воспользоваться вызовом функции glCullFace():

Функция glCullFace() имеет три опции:

   GL_BACK — отсекаются только тыльные грани.

   GL_FRONT — отсекаются только фронтальные грани.

   GL_FRONT_AND_BACK — отсекаются как фронтальные, так и тыльные грани.

По умолчанию функция glCullFace() работает в режиме GL_BACK. С помощью функции glFrontFace() мы можем сообщить OpenGL, чтобы фронтальные грани определялись порядком обхода по часовой стрелке:

Значением по умолчанию для данной функции является GL_CCW, задающее порядок обхода против часовой стрелки, другой же вариант — GL_CW означает порядок обхода по часовой стрелке.

Следующий код включает режим отсечения фронтальных граней, при этом «фронтальность» определяется порядком обхода вершин по часовой стрелке (напоминаю, что именно в таком порядке были определены вершины в нашем массиве cubeVertices[]).

В результате визуализируются только тыльные грани:

  GitHub / Урок №22. Отсечение граней в OpenGL — Исходный код

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

Заключение


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

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

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

Комментариев: 1

  1. OpenGL C#:

    У меня, например, отключение задних граней производительность не подняло на nVidea. Нужно искать другой подход.

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

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