Отсечение граней
Попробуйте представить куб и подсчитать максимальное число его граней, которое вы можете увидеть с любого направления. Если ваше воображение не излишне живое, то, верней всего, вы придете к выводу, что это число 3. Из какой бы точки или с какого бы направления вы не смотрели на куб, вы никогда не сможете увидеть больше чем три грани. Так к чему же тратить вычислительные мощности на отрисовку оставшихся трех граней, если их не будет видно? Если бы мы могли отбросить их обработку каким-то образом, то сэкономили более чем половину выполнений фрагментного шейдера!
В передыдущих сериях
Часть 1. Начало
Часть 2. Базовое освещение
Часть 3. Загрузка 3D-моделей
Часть 4. Продвинутые возможности OpenGL
- OpenGL
- Создание окна
- Hello Window
- Hello Triangle
- Shaders
- Текстуры
- Трансформации
- Системы координат
- Камера
Часть 2. Базовое освещение
Часть 3. Загрузка 3D-моделей
Часть 4. Продвинутые возможности OpenGL
Здесь сказано «более половины» поскольку в определенных ситуация видно только две, а то и одну грань куба. В таких случаях сэкономлено будет больше, чем 50%.
Идея хорошая, но появляется новая задача: нужно определить, какая же грань не видна из положения наблюдателя.
Представьте любую замкнутую объемную фигуру: каждая из её граней имеет две стороны. И одна из этих сторон будет обращена на наблюдателя, а другая от него. Что если мы будем выводить только обращенные на наблюдателя грани?
Именно в этом процессе и заключена суть процедуры отсечения граней. OpenGL проверяет все грани на ориентацию, допуская к рендеру только обращенные на наблюдателя, одновременно отсекая те, что обращены от него, что в итоге экономит множество вызовов фрагментного шейдера (выполнение которого весьма затратно!). Нам лишь только остается как-то передать OpenGL информацию о том, какие грани считать лицевыми, а какие – нет. OpenGL использует находчивое решение для определения ориентации граней: анализ порядка обхода в списке вершинных данных.
Порядок обхода
Когда мы задаем набор вершин для треугольника мы определяем их в определенном порядке обхода: либо по часовой стрелке (clockwise, CW), либо против часовой стрелки (counter-clockwise, CCW). Каждый треугольник состоит из трех вершин и задаем мы их в порядке обхода, определённом относительно центра треугольника.
Как видно из схемы, сперва задается вершина 1, а затем у нас есть выбор: задать вершину 2 или 3, тем самым и определяя порядок обхода треугольника. Приведем код для наглядности:
float vertices[] = {
// обход по часовой
vertices[0], // вершина 1
vertices[1], // вершина 2
vertices[2], // вершина 3
// обход против часовой
vertices[0], // вершина 1
vertices[2], // вершина 3
vertices[1] // вершина 2
};
Каждый набор из трех вершин, определяющий треугольник, в итоге содержит и данные о порядке его обхода. При рендере подготовленных вами примитивов OpenGL использует эту информацию для определения является ли очередной треугольник лицевым или нет. По умолчанию треугольники с порядком обхода против часовой стрелки считаются лицевыми.
При задании обхода в своем наборе вершин необходимо представить треугольник так, будто вы смотрите прямо на него, а вершины в нем вы задаете в порядке против часовой стрелки. Самое интересное в этом воображаемом процессе то, что непосредственная обработка порядка обхода происходит на этапе растеризации, т.е. после выполнения вершинного шейдера. Это значит, что вершины действительно преобразованы к точке зрения наблюдателя.
Выходит, что все вершины треугольников, на которые смотрит наблюдатель действительно заданы в том же порядке обхода как мы их и задали. Но при этом вершины треугольников на другой стороне куба теперь выводятся таким образом, что их порядок обхода стал инвертирован. В результате треугольники перед наблюдателем считаются лицевыми, а дальние треугольники считаются нелицевыми. Посмотрите на изображение для большей наглядности сказанного:
В списке вершин оба треугольника мы определили в порядке против часовой стрелки (ближний треугольник задан как 1-2-3, и задний также задан как 1-2-3 (если бы наблюдатель смотрел на его лицевую сторону)). Однако, с указанной позиции порядок описания дальнего треугольника 1-2-3 наблюдается заданным по часовой стрелке. Несмотря на то, что вершины дальнего треугольника задавались с порядком обхода против часовой, при рендере он стал порядком по часовой стрелке. И такое поведение как раз и необходимо для успешного проведения отсечения невидимых граней.
Отбраковка граней
В начале урока было отмечено, что OpenGL может отсекать треугольники если они выводятся как нелицевые. И теперь, зная о способе задания порядка обхода, мы можем приступить к использованию функции отсечения, которая в библиотеке по умолчанию отключена.
Данные вершин для куба из прошлых уроков не были сформированы с учетом требования к порядку обхода против часовой стрелки, так что вам понадобится новый массив вершин, который лежит здесь. Потренируйтесь – попробуйте мысленно проверить порядок обхода каждого треугольника.
Включить функцию отсечения можно так:
glEnable(GL_CULL_FACE);
С этого момента все нелицевые грани будут отброшены (попробуйте заглянуть внутрь куба и убедиться, что поверхности внутренних граней отброшены). В итоге мы экономим более 50% проходов выполнения обработки фрагментов, но только для таких замкнутых фигур как куб. В предыдущем уроке нам пришлось бы выключить отсечение граней для объектов, изображающих траву, поскольку у них должны рисоваться и лицевые и нелицевые грани.
OpenGL позволяет нам настроить какую именно сторону грани мы хотели бы отбрасывать. Вдруг нам необходимо отбрасывать именно лицевую грань? Для этого можно воспользоваться следующим вызовом:
glCullFace(GL_FRONT);
У функции три возможных параметра:
• GL_BACK: Отбрасывает только нелицевые грани.
• GL_FRONT: Отбрасывает только лицевые грани.
• GL_FRONT_AND_BACK: Отбрасывает и те и другие грани.
Значение по умолчанию – GL_BACK.
Кроме выбора грани для отбрасывания неплохо было бы иметь возможность указания какой порядок обхода в треугольнике будет определять лицевую грань. Для этого используется следующая функция:
glFrontFace(GL_CCW);
Значение по умолчанию – GL_CCW, подразумевающее обход против часовой. Второе возможное значение – GL_CW, что устанавливает обход по часовой.
В качестве простого эксперимента можно установить отсечение нелицевых граней и порядок обхода по часовой как задающий лицевые грани, вместо стандартного обхода против часовой:
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glFrontFace(GL_CW);
В результате видны останутся только задние грани куба:
Обратно же, этого эффекта можно добиться отсекая лицевые грани, но при использовании обхода против часовой как определяющего лицевые грани:
glEnable(GL_CULL_FACE);
glCullFace(GL_FRONT);
glFrontFace(GL_СCW);
Подытоживая, можно сказать, что механизм отсечения граней – великолепный инструмент увеличения производительности, требующий для работы малых усилий. Дополнительно только потребуется следить за тем, к каким объектам в сцене выгодно применять отсечение, а для каких отсечение недопустимо.
Упражнения
Попробуйте переопределить данные в списке вершин так, что каждый треугольник теперь определен в порядке по часовой стрелке? Выведите сцену на экран при определении лицевых треугольников как описанных по часовой стрелке.
Решение тут.
P.P.S.: У нас с есть телеграм-конфа для координации переводов. Если есть желание вписаться в цикл, то милости просим!