Методы оптимизации программ компьютерной графики OpenGL

Г.В. Князева

МЕТОДЫ ОПТИМИЗАЦИИ ПРОГРАММ КОМПЬЮТЕРНОЙ ГРАФИКИ OPENGL

Ключевые слова: высокоуровневая оптимизация, структуры данных, приложение, интерполяция цветов, векторные версии команд, текстурированные объекты.

Аннотация

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

Организация приложения

На первый взгляд может показаться, что производительность приложений, основанных на OpenGL, определяется в первую очередь производительностью реализации самой библиотеки OpenGL. Это верно, однако организация всего приложения (использование локальных и глобальных переменных, структур данных, библиотек, последовательность выполнения команд) также очень важна.

Высокоуровневая оптимизация

Обычно от программы под OpenGL требуется визуализация высокого качества на интерактивных скоростях. Но, как правило, и то и другое сразу получить не удается. Следовательно, необходим поиск компромисса между качеством и производительностью. Существует множество различных подходов к данному вопросу:

- отображение геометрии сцены с низким качеством во время анимации, а в моменты остановок показ ее с наилучшим качеством;

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

- объекты, расположенные далеко от наблюдателя, могут быть представлены моделями пониженной сложности (это значительно снизит нагрузку на все ступени конвейера OpenGL);

- объекты, которые находятся полностью вне поля видимости, могут быть эффективно отсечены без передачи на конвейер OpenGL с помощью проверки попадания ограничивающих их простых объемов (сфер или кубов) в пирамиду зрения;

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

Низкоуровневая оптимизация

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

Предположим, что создается приложение, которое реализует рисование карты местности. Один из компонентов базы данных - список городов с их шириной, долготой и названием.

Для хранения информации о городе создается соответствующая структура данных (см. таблицу 1). Список городов может храниться как массив таких структур. Затем создается функция, которая рисует города на карте в виде точек разного размера с подписями: если город маленький, то точки имеют размер 2 px, если большой - 4 px.

Реализация, представленная в первом варианте, неудачна по следующим причинам:

- glPointSize() вызывается для каждой итерации цикла;

- между glBegin() и glEnd() рисуется только одна точка;

- вершины определяются в неоптимальном формате.

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

Самым оптимальным можно считать третий вариант. После реорганизации структуры города разных размеров хранятся в разных списках, положения точек хранятся отдельно в динамическом массиве. Исключается необходимость в условном операторе внутри glBegin/glEnd и имеется возможность использовать массивы вершин для оптимизации.

Оптимизация вызовов opengl

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

Передача данных в opengl

Рассмотрим способы минимизации времени на передачу данных о примитивах в OpenGL.

Использование связанных примитивов, таких как GL_LINE_LOOP, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, и GL_QUAD_STRIP, требует для определения меньше вершин, чем отдельные линии или многоугольник. Это уменьшает количество данных, передаваемых OpenGL.

Использование массивов для передачи параметров в команды может быть очень выигрышным. Вместо использования команд

glVertexPointer/glColorPointer/glNormalPointer лучше пользоваться одной командой, как например,

glInterleavedArrays (GL_C4F_N3F_V3F, 0, pData);

что означает, что первые четыре float относятся к цвету, затем три float - к нормали, и последние три float задают координаты вершины.

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

Использование векторных версий команд glVertex*(), glColor*(), glNormal*() и glTexCoord*(), которые в качестве аргументов принимают указатели (например, glVertex3fv(v)) могут работать значительно быстрее, чем их соответствующие версии glVertex3f(x, y, z).

Уменьшение сложности примитивов при построении поверхностей. Текстурированные объекты, например, могут быть качественно отображены с небольшой сложностью геометрии.

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

Не следует указывать ненужные атрибуты вершин. Например, если освещение выключено, не нужно вызывать glNormal(); если не используются текстуры, не нужно вызывать glTexCoord(), и т.д.

Таблица 1

Вариант 1 Вариант 2 Вариант 3

Структура данных для хранения информации о городе struct city { /* положение города*/ float latitute, longitude; char *name; /*название*/ /*0=маленький, 1-большой*/ int large_flag; }; struct city { такая же как а варианте 1 }; void draw_cities( int n, struct city citylist[] ) {int i; /*рисуем маленькие точки */ glPointSize( 2.0 ); glBegin( GL_POINTS ); for (i=0; i < n ;i++) {if (citylist[i].large_flag==0) {glVertex2f( citylist[i] .longitude, citylist[i].latitude ); }} glEnd(); /* рисуем большие точки */ glPointSize( 4.0 ); glBegin( GL_POINTS ); for (i=0; i < n ;i++) {if (citylist[i].large_flag==1) {glVertex2f( citylist[i] .longitude, citylist[i].latitude ); }} glEnd(); /*рисуем названия городов */ for (i=0; i < n ;i++) {DrawT ext(citylist[i] .longitude, citylist[i].latitude, citylist[i].name);}} struct city_list {int num_cities;/* число городов в списке */ float *position;/* координаты города */ char **name;/* указатель на названия городов */ float size;/* размер точки, обозначающей город*/};

Функция рисования городов на карте void draw_cities( int n, struct city citylist[] ) {int i; for (i=0; i < n; i++) {if (citylist[i].large flag) glPointSize( 4.0 ); е^ glPointSize( 2.0 ); glBegin( GL_POINTS ); glVertex2f(citylist[i] .longitude, citylist[i].latitude); glEnd(); /* рисуем название города */ DrawT ext(citylist[i] .longitude, citylist[i].latitude, citylist[i].name); }} void draw_cities( struct city_list *list ) {int i; /* рисуем точки */ glPointSize( list->size ); glVertexPointer( 2, GL_FLOAT, 0, list->num_cities, list->position ); glDrawArrays( GL_POINTS, 0, list->num_cities ); /* рисуем название города */ for (i=0; i < list->num_cities ;i++) {DrawT ext(citylist[i] .l ongitude, citylist[i].latitude citylist[i].name); }

Минимизация кода между glBegin/glEnd позволяет использовать максимальную производительность графической системы (см. таблицу 2).

Таблица 2

Неудачное решение - переменная lighting проверяется перед каждой вершиной (если включено освещение, то прописать нормали перед каждой вершиной) Частичное дублирование кода оптимизирует выполнение программы, так как наличие освещения проверяется только один раз

glB egin(GL_TRIANGLE_STRIP); for (i=0; i < n; i++) {if (lighting) {glNormal3fv(norm[i]);} glVertex3fv(vert[i]); } glEnd(); if (lighting) {glB egin(GL_TRIANGLE_STRIP); for (i=0; i < n ;i++) {glNormal3 fv(norm[i]); glVertex3fv(vert[i]); } glEnd(); } else {glB egin(GL_TRIANGLE_STRIP); for (i=0; i < n ;i++) {glVertex3fv(vert[i]); } glEnd();}

Преобразования

Преобразования включают в себя трансформации вершин от координат, указанных в glVertex*(), к оконным координатам, отсечение, освещение и т.д.

Рекомендации по применению освещения:

- избегайте использования локальных источников света, т.е. координаты

источника должны быть в форме (х, у, ъ, 0);

- избегайте использования точечных источников света;

- избегайте использования двухстороннего освещения;

- избегайте использования отрицательных коэффициентов в параметрах

материала и цвета;

- избегайте использования локальной модели освещения;

- избегайте частой смены параметра материала ОЬЗНЕЧГШЕЗБ;

- можно получить эффект освещения, задавая цвета вершин вместо нормалей;

- отключайте нормализацию векторов нормалей, когда это не необходимо -glEnable/Disable(GL_NORMALIZE).

Растеризация

Растеризация часто является узким местом программных реализаций OpenGL.

Рекомендации по оптимизации растеризации:

1. Отключайте интерполяцию цветов, когда в этом нет необходимости. Интерполяция цветов включена по умолчанию. Плоское затенение не требует интерполяции четырех компонент цвета и, как правило, быстрее на программных реализациях OpenGL. Аппаратные реализации обычно выполняют оба вида затенения с одинаковой скоростью. Для отключения используйте команду glShadeModel(GL_FLAT).

2. Отключайте тест на глубину, когда в этом нет необходимости. Фоновые объекты, например, могут быть нарисованы без теста на глубину, если они визуализируется первыми.

3. Используйте отсечение обратных граней полигонов. Замкнутые объекты могут быть нарисованы с установленным режимом отсечения обратных граней glEnable(GL_CULL_FACE) Иногда это позволяет отбросить до половины многоугольников, не растеризуя их.

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

5. Уменьшайте размер окна или разрешение экрана. Простой способ уменьшить время растеризации - уменьшить число пикселей, которые будут нарисованы. Если меньшие размеры окна или меньшее разрешение экрана приемлемы, то это хороший путь для увеличения скорости растеризации.

Текстурирование

Наложение текстур является дорогой операцией, как в программных, так и в аппаратных реализациях.

Рекомендации по применению текстурирования:

1. Используйте эффективные форматы хранения изображений. Формат GL_UNSIGNED_BYTE обычно наиболее всего подходит для передачи текстуры в OpenGL.

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

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

4. Комбинируйте небольшие текстуры в одну. Если вы используете несколько маленьких текстур, можно объединить их в одну большего размера и изменить текстурные координаты для работы с нужной подтекстурой. Это позволяет уменьшить число переключений текстур.

5. Анимированные текстуры. Если вы хотите использовать анимированные текстуры, не используйте команду glTexImage2D() чтобы обновлять образ текстуры. Вместо этого используйте glTexSubImage2D() или glTexCopyTexSubImage2D().

Очистка буферов

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

Рекомендации, которые могут помочь оптимизировать эту операцию:

1. Очищайте все нужные буферы с помощью одной команды glClear(), но используйте эту команду с осторожностью.

Неверно:

glClear(GL_COLOR_BUFFER_BIT); if (stenciling) /* очистить буфер маски? */

{glClear(GL_STENCIL_BUFFER_BIT);}

Верно:

if (stenciling) /* очистить буфер маски? */

{glClear(GL_COLOR_BUFFER_BIT | STENCIL_BUFFER_BIT);} else

{glClear(GL_COLOR_BUFFER_BIT);}

2. Отключайте размывание (dithering) очисткой буфера. Обычно различие между чистками с включенным размыванием и без него незаметно.

3. Используйте ножницы (scissors) для очистки меньшей области. Если вы не хотите очищать весь буфер, используйте glScissor() для ограничения очистки по заданной области.

4. Не очищайте буфер цвета полностью. Если ваша сцена занимает только часть окна, нет необходимости очищать весь буфер цвета.

Разное

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

1. Проверяйте ошибки GL во время написания программ. Вызывайте команду glGetError() для проверки, не произошла ли ошибки во время вызова одной из функций OpenGL. Как правило, ошибки возникают из-за неверных параметров команд OpenGL или

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

#include <assert.h>

#define CHECK_GL \ assert(glGetError() != GL_NO_ERROR);

Использовать его можно так: glBegin(GL_TRIANGLES); glVertex3f(1,1,1); glEnd();

CHECK_GL

2. Используйте функцию glColorMaterial() вместо glMaterial(). Если в сцене материалы объектов различаются лишь одним параметром, команда glColorMaterial() может быть быстрее, чем glMaterial().

3. Минимизируйте число изменений состояния OpenGL. Команды, изменяющие состояние OpenGL (glEnable/glDisable/glBindTexture и другие), вызывают повторные внутренние проверки целостности, создание дополнительных структур данных и т.д., что может приводить к задержкам.

4. Избегайте использования команды glPolygonMode(). Если вам необходимо рисовать много незакрашенных многоугольников, используйте glBegin() с константами ОЬ_РОШТ8, ОЬ_ЬШЕ8, ОЬ_ЬШЕ_ЬООР или ОЬ_ЬШЕ_8ТМР вместо изменения режима рисования примитивов, так как это может быть намного быстрее.

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

Библиографический список

1. Порев В.Н. Компьютерная графика. - СПб.: ВНУ, 2002.

2. Тихомиров Ю. Программирование трехмерной графики. - СПб.: ВНУ, 1998.

3. Шикин А. В., Боресков А. В. Компьютерная графика. Полигональные модели. -М.: ДИАЛОГ-МИФИ, 2001.

4. Эйнджел Э. Интерактивная компьютерная графика. Вводный курс на базе OpenGL. - 2-е изд. / Пер. с англ.- М.: Вильямс, 2001.