OGL3

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


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


Здесь сказано «более половины» поскольку в определенных ситуация видно только две, а то и одну грань куба. В таких случаях сэкономлено будет больше, чем 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.: У нас с есть телеграм-конфа для координации переводов. Если есть желание вписаться в цикл, то милости просим!

Комментарии (0)