К. В. Рябинин вычислительная геометрия и алгоритмы компьютерной графики
Скачать 1.44 Mb.
|
7.2. Хранение объектов сцены в OpenGL Для хранения данных объекта сцены в OpenGL 3.3+ имеется три структуры данных: 1. Буфер вершин (англ. Vertex Buffer Object ) – структура для хра- нения массива вершин, представленных их атрибутами. Как правило, элементы этого массива имеют тип float 2. Буфер индексов (англ. Index Buffer Object ) – структура для хранения массива индексов, задающих порядок обхода вер- шин для объединения их в примитивы. Как правило, элементы этого массива имеют тип unsigned int (на настольных ком- 64 пьютерах, следовательно, адресовать можно 2 32 вершин) или unsigned short (на мобильных устройствах, следовательно, адресовать можно 2 16 вершин). 3. Буфер массива вершин (англ. Vertex Array Object ) – структура для хранения отображения массива вершин на входы шейдера. Все три структуры, как это принято в OpenGL, могут созда- ваться, настраиваться и уничтожаться при помощи специальных API функций. При этом фактические данные хранятся в видеопамяти, а в основном приложении доступны лишь соответствующие целочис- ленные дескрипторы. 7.3. Реализация Для удобства работы предлагается хранить дескрипторы всех связанных с объектом сцены буферов в отдельном классе: class Model { public: GLuint vbo; GLuint ibo; GLuint vao; } ; Объект этого класса предлагается объявить в виде глобальной переменной (как и дескриптор шейдерной программы). Большинство графических движков используют объектно- ориентированную парадигму для создания обёртки, хранящей внутри себя дескрипторы низкоуровневых структур данных и ме- тоды управления этими структурами. При желании читатель может самостоятельно создать собственный легковесный графический дви- жок, однако мы не будем углубляться в проектирование архитектуры подобного рода программного обеспечения и ограничимся лишь самой простой группировкой внутри одного класса дескрипторов, описывающих различные низкоуровневые составляющие одной и той же высокоуровневой сущности (3D-модели). 65 В качестве первого опыта визуализации выведем на экран са- мый тривиальный объект – треугольник. В предыдущей главе были подготовлены шейдеры, ожидающие данные в формате (x, y, r, g, b) , причём координаты (x, y) должны быть указаны в NDC. Внешний вид тестового треугольника приведён на рис. 12. Рис. 12. Треугольник, подлежащий выводу в качестве первого опыта визуализации Несмотря на то что треугольник в данном случае будет все- го один, воспользуемся индексацией, чтобы сразу создать расширя- емую программу. Следует отметить, что при визуализации имеет значение порядок обхода вершин. Дело в том, что системе могут быть заданы разные настройки для вывода передних (англ. Front Face ) и задних (англ. Back Face ) граней. Передней по умолчанию считается грань, вершины которой после проекции на экран идут против часовой стрелки (англ. Counterclockwise , CCW), а задней – по часовой стрелке (англ. Clockwise , CW). При этом существует настройка, которая меняет правило определения на обратное. Определение происходит на уровне каждого отдельного тре- угольника аппаратно, путём нахождения векторного произведения векторов, построенных на его сторонах в порядке обхода вершин, и проверки знака координаты z (глубины) полученного результата. Для создания модели предлагается следующая сигнатура функции: Model createModel() 66 Эта функция, как можно догадаться по отсутствию парамет- ров, не будет такой же универсальной, как функции создания шей- деров и шейдерных программ, поскольку универсальность в данном случае означала бы некоторое усложнение кода. Цель этой функции – на простом примере показать основной механизм подготовки дан- ных объекта сцены. Тело функции приведено в листинге 5. Листинг 5. Функция создания простого объекта сцены 1 Model createModel() 2 { 3 const GLfloat vertices[] = 4 { 5 − 0.5f, − 0.5f, 1.0f, 0.0f, 0.0f, 6 0.5f, − 0.5f, 0.0f, 1.0f, 0.0f, 7 0.0f, 0.5f, 1.0f, 1.0f, 0.0f, 8 } ; 9 10 const GLuint indices[] = 11 { 12 0, 1, 2, 13 } ; 14 15 Model result; 16 17 glGenVertexArrays(1, &result.vao); 18 glBindVertexArray(result.vao); 19 20 glGenBuffers(1, &result.vbo); 21 glBindBuffer(GL ARRAY BUFFER, 22 result.vbo); 23 glBufferData(GL ARRAY BUFFER, 24 15 ∗ sizeof(GLfloat), 25 vertices, 26 GL STATIC DRAW); 27 28 glGenBuffers(1, &result.ibo); 67 29 glBindBuffer(GL ELEMENT ARRAY BUFFER, 30 result.ibo); 31 glBufferData(GL ELEMENT ARRAY BUFFER, 32 3 ∗ sizeof(GLuint), 33 indices, 34 GL STATIC DRAW); 35 36 glEnableVertexAttribArray(0); 37 glVertexAttribPointer(0, 2, GL FLOAT,GL FALSE, 38 5 ∗ sizeof(GLfloat), 39 ( const GLvoid ∗ )0); 40 glEnableVertexAttribArray(1); 41 glVertexAttribPointer(1, 3, GL FLOAT,GL FALSE, 42 5 ∗ sizeof(GLfloat), 43 ( const GLvoid ∗ ) 44 (2 ∗ sizeof(GLfloat))); 45 46 return result; 47 } В строке (3) создаётся массив вершин объекта по оговоренно- му ранее формату ( 5 значений типа float на вершину). Как правило, массивы вершин загружаются из файлов (куда их записывают зара- нее с помощью визуальных редакторов 3D-графики), однако проце- дурные (программно сгенерированные) модели также не являются редкостью. В строке (10) создаётся массив индексов, который в данном примере обеспечивает тривиальный обход элементов массива вер- шин подряд. В строке (17) создаётся буфер массива вершин. Помимо слова Create , OpenGL использует ещё и слово Gen (от англ. Generate – генерировать) в аналогичном значении: чтобы обозначить функцию, создающую новый объект и передающую ответственность за него вызывающему. Отличием является то, что функции со словом Gen за один вызов могут создать не один, а произвольное число объектов (в пределах максимально возможного числа объектов данного типа, 68 которое определяется аппаратными возможностями конкретной си- стемы). Однако в рассматриваемом примере буфер массива вершин создаётся всего один. В строке (18) созданный буфер активируется, и все последую- щие настройки составляющих трёхмерного объекта будут автомати- чески сохранены в нём. В дальнейшем можно будет лишь повторно активировать его, чтобы мгновенно (вызовом лишь одной функции активации) восстановить состояние графического конвейера, необ- ходимое для вывода соответствующего трёхмерного объекта. Такой подход «пакетного» хранения и применения настроек способен зна- чительно увеличить производительность, а потому стал обязатель- ным к использованию в OpenGL 3.3+ (в более ранних версиях он был представлен лишь опциональным расширением, а в обычном режи- ме приходилось перед каждой отрисовкой очередного объекта при- менять все необходимые настройки заново вручную). В строке (20) создаётся и в строке (21) активируется буфер вер- шин. Первый параметр функции активации glBindBuffer опреде- ляет тип буфера – вершинный (константа GL ARRAY BUFFER ) или индексный (константа GL ELEMENT ARRAY BUFFER ). Из такого ко- да следует, что типизация буфера происходит динамически и не со- храняется в буфере. Однако фактически семантика использования буфера должна быть сохранена в использующем его приложении, по- скольку, например, интерпретация вершинного буфера как индекс- ного неминуемо приведёт к ошибкам работы конвейера из-за некор- ректных обращений к памяти. В строке (23) происходит копирование данных из массива вер- шин в активный буфер вершин. В один момент времени активным может быть только один буфер конкретного типа, однако буферы раз- ных типов могут быть активированы одновременно. Поэтому функ- ция заполнения glBufferData принимает в качестве первого пара- метра тип буфера. Второй параметр этой функции – размер копируе- мых данных в байтах . Третий параметр – нетипизированный указа- тель на массив данных. Четвёртый параметр – константа, определяю- щая режим использования буфера, а именно – является ли он статич- ным (неизменным) или его содержимое будет меняться в процессе 69 работы приложения. Такая информация позволяет системе оптими- зировать доступ к буферу. В приведённом примере буфер объявлен статичным. Данные буфера хранятся в видеопамяти, поэтому вызов glBufferData оказывается достаточно дорогостоящим, осуществ- ляя копирование через шину. В связи с этим заполнение буферов рекомендуется осуществлять на этапе подготовки сцены до начала циклической визуализации. Далее в строках (28–34) совершаются действия по созданию буфера индексов. Эти действия аналогичны предыдущим, отличие составляет только тип буфера. После этого настраивается состояние, т. е. связь атрибутов вер- шин со входами вершинного шейдера. Сначала в строке (36) разрешается использование массива дан- ных для атрибута с дескриптором 0 . Этому атрибуту соответствует a vertex из шейдера, описанного в предыдущей главе (дескриптор был определён для этого атрибута в явном виде при помощи специ- фикатора layout ). Следует отметить, что в отличие от шейдеров, шейдерных программ и буферов, дескриптор 0 является валидным для атрибутов. Затем функцией glVertexAttribPointer устанавливается относительный указатель на начало данных атрибута в буфере и шаг выборки атрибутов. Сигнатура этой функции довольно сложна и ча- сто является источником ошибок, поэтому разберём её подробно: void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer); – index – дескриптор атрибута; – size – число компонентов атрибута (число элементов того типа, в котором хранятся данные в буфере вершин, связанных с одним атрибутом); 70 – type – константа, определяющая тип данных, хранящихся в бу- фере (чаще всего атрибуты вершин представляют собой значения типа float , чему соответствует константа GL FLOAT ); – normalized – флаг, определяющий, должны ли значения атрибу- та быть нормированы, т. е. приведены к отрезку [ −1; 1] для случая знаковых и [0; 1] для случая беззнаковых чисел (этот флаг исполь- зуется в том случае, если значения атрибутов хранятся в виде це- лых чисел); – stride – шаг в байтах , по которому нужно выбирать данные ат- рибутов каждой следующей вершины (чаще всего это значение вычисляется как произведение числа компонентов вершины и раз- мера, обусловленного типом этих компонентов); – pointer – смещение в байтах относительно начала буфера, по которому располагается первый компонент атрибута, приведённое к указателю. В строках (40–44) аналогично настраивается атрибут с дескриптором 1 , которому соответствует заданный в шейдере a color В результате модель, представленная тремя буферами, оказы- вается полностью настроенной и готовой к визуализации. Следует обратить внимание, что использованные буферы, как и шейдерную программу, необходимо удалить после завер- шения цикла визуализации сцены (в функции cleanup ). Для удаления буферов вершин и индексов используется функция glDeleteBuffers , для удаления буфера массива вершин – функ- ция glDeleteVertexArrays . Обе функции принимают на вход массив дескрипторов и их число. Задание: Добавить в тестовое приложение создание модели при помощи функций, приведённых в дан- ной главе. Подсказка: Обратить внимание на корректную и своевременную очистку всех созданных объектов. 71 8. Визуализация сцены Использованные в тестовом приложении библиотеки GLFW и GLEW при своей инициализации создают графический контекст и делают графический конвейер доступным для дальнейшей работы. Однако чтобы осуществить визуализацию, необходимо явным обра- зом указать некоторые настройки. 8.1. Порт просмотра Как уже отмечалось ранее, для организации проекции объек- тов на экран необходимо, с одной стороны, создать преобразование координат объектов сцены в NDC, а с другой – задать порт просмот- ра. Преобразование координат лежит на программисте и осуществ- ляется при помощи матриц. Характеристики порта просмотра, на- против, являются частью машины состояний OpenGL и преобразо- вание порта просмотра система выполняет автоматически. Преобразование порта просмотра схематично изображено на рис. 13. Рис. 13. Преобразование порта просмотра в OpenGL Как видно из рис. 13, характеристикой порта просмотра явля- ются координаты его левого нижнего угла в системе координат окна 72 (начало которой находится в левом нижнем углу окна), а также ши- рина и высота. За единицу в системе координат окна согласно спе- цификации OpenGL принят физический пиксель экрана. Как уже от- мечалось ранее, в некоторых графических интерфейсах физические пиксели экрана отличаются от логических , в которых задаются ко- ординаты элементов (например, на ретина-дисплеях). Однако порт просмотра, как и все остальные сущности, описанные спецификаци- ей OpenGL, относятся к низкоуровневой графике и оперируют физи- ческими пикселями. Для задания характеристики порта просмотра используется функция glViewport . Чтобы осуществить корректную настройку конвейера, вызов этой функции необходимо добавить в функцию reshape , которая вызывается каждый раз, когда изменяется размер окна. Код, обеспечивающий совпадение порта просмотра с окном, приведён в листинге 6. Листинг 6. Функция обратного вызова на изменение размера окна 1 void reshape(GLFWwindow ∗ window, 2 int width, int height) 3 { 4 glViewport(0, 0, width, height); 5 } 8.2. Вызов отрисовки Для осуществления визуализации объекта сцены необходимо: 1. Активировать соответствующую объекту шейдерную про- грамму (если она не активна). 2. Активировать соответствующий объекту буфер массива вер- шин модели (если он не активен). 3. Выполнить вызов отрисовки. Обычно сцены состоят из большого числа разнообразных объ- ектов, некоторые из которых могут использовать одни и те же шей- дерные программы или модели (отличаясь, например, положением и 73 значениями каких-либо юниформ-переменных). В этом случае реко- мендуется группировать объекты с общими ресурсами и выводить их друг за другом, минимизируя изменения состояния конвейера: пере- ключения настроек тоже требуют времени, хотя это далеко не такие дорогостоящие операции, как копирование данных из оперативной в видеопамять. В нашем случае сцена состоит лишь из одного объекта. Код функции отрисовки, циклически вызываемой во время выполнения приложения, приведён в листинге 7. Листинг 7. Функция отрисовки сцены 1 void draw() 2 { 3 glClear(GL COLOR BUFFER BIT); 4 5 glUseProgram(g shaderProgram); 6 glBindVertexArray(g model.vao); 7 8 glDrawElements(GL TRIANGLES,3,GL UNSIGNED INT, 9 ( const GLvoid ∗ )0); 10 } Предполагается, что g shaderProgram – глобальная пере- менная типа GLuint , хранящая дескриптор шейдерной программы, а g model – глобальная переменная типа Model , хранящая буферы модели. В строке (3) осуществляется очистка буфера цвета. В данном случае очистку можно и не производить, так как сцена не изменяется с течением времени. Однако в общем случае этот вызов необходим для удаления данных, сгенерированных на предыдущем кадре. В строках (5–6) осуществляется активация шейдерной про- граммы и буфера массива вершин модели. В данном случае эту активацию можно было бы произвести однократно, однако в общем случае, когда сцена представлена большим числом объектов, вы- водящихся при помощи разных шейдеров, требуется каждый раз заново перенастраивать состояние конвейера. 74 Следует отметить, что хотя для отрисовки используется лишь буфер массива вершин, буферы вершин и индексов должны присут- ствовать в памяти до тех пор, пока требуется визуализировать соот- ветствующую им модель. Буфер массива вершин является лишь сво- его рода дескриптором трёхмерного объекта и хранит в себе лишь настройки, но не сами данные. В строке (8) происходит вызов отрисовки, обеспечивающий отправку данных на графический конвейер. Вызов отрисовки представлен в OpenGL двумя альтернатив- ными командами: glDrawArrays и glDrawElements . Первая из них игнорирует буфер индексов (и допускает полное его отсутствие), совершая обход вершин в порядке их следования в вершинном бу- фере. Вторая использует индексацию, заданную либо индексным буфером, либо, если буфер индексов не активирован, массивом из оперативной памяти (указатель на который передаётся одним из параметров функции). Однако использование для индексации массивов из оперативной памяти неэффективно, так как приводит к копированию данных в видеопамять непосредственно перед отри- совкой (а не заранее, как в случае с индексным буфером). Другое назначение указателя (в случае, если активирован буфер индексов) – указание смещения относительно начала индексного буфера, чтобы была возможность вывести лишь часть модели. Обе функции принимают в качестве параметров константу, определяющую тип выводимых примитивов и число вершин для обработки. Таким образом, одну и ту же модель можно выводить разными способами, в частности – фрагментарно. Поскольку модель в данном случае использует индекса- цию, отправка данных на конвейер осуществляется функцией glDrawElements . Рассмотрим её сигнатуру: void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices); – mode – константа, определяющая тип примитива (с доступными типами примитивов читателю предлагается познакомиться са- 75 мостоятельно, воспользовавшись официальной документацией OpenGL [11]); – count – число вершин для обработки; – type – тип индексов (для настольных компьютеров обычно ис- пользуется GL UNSIGNED INT , для мобильных устройств на дан- ный момент поддерживается лишь GL UNSIGNED SHORT ); – indices – указатель на массив индексов в оперативной памяти (если буфер индексов не активирован), либо смещение в байтах для буфера индексов (если он активирован), приведённое к указа- телю. Сигнатуру функции glDrawArrays читателю предлагается изучить самостоятельно в документации OpenGL [11]. В результате тестовое приложение должно вывести на экран треугольник, закрашенный цветовым градиентом (градиент возни- кает за счёт интерполяции цвета, заданного в вершинах). |