С.В. Запечников
ВИЗУАЛИЗАЦИЯ ДАННЫХ И ПРОЦЕССОВ С ИСПОЛЬЗОВАНИЕМ КРОССПЛАТФОРМЕННОГО ПРОГРАММНОГО ИНТЕРФЕЙСА OPENGL*
Статья посвящена анализу возможностей визуализации данных и процессов, представимых в виде трехмерных объектов, комплексов объектов (сцен) или трехмерной анимации, с применением кроссплатформенных программных библиотек, поддерживающих интерфейс OpenGL. Выявляется существо математических и технических задач, которые позволяют решать эти программные средства, в частности, задач геометрического задания трехмерных объектов, проецирования их на экранную плоскость, управления видеопамятью при отображении графических объектов и др. Выделенные задачи систематизируются, для каждой из них по возможности приводится формальная постановка задачи и указываются способы решения. Для большинства обсуждаемых задач формулируются практические рекомендации по рациональному использованию возможностей интерфейса OpenGL при создании прикладных программ, требующих трехмерной визуализации.
Ключевые слова: визуализация данных, трехмерная графика, геометрические преобразования, интерфейс прикладного программирования, кроссплатформенное программное обеспечение, спецификация OpenGL.
В настоящее время отображение трехмерных объектов, сцен и анимаций является составной частью многих информационных технологий: приложений для визуализации результатов научных экспериментов, численного и имитационного моделирования, систем автоматизированного проектирования, геоинформационных систем, мультимедийных приложений и многих других видов
© Запечников С.В., 2014
* Автор выражает благодарность В.Ю. Ефимову за предоставленные материалы по интерфейсу OpenGL и примеры исходных текстов программ, написанных с использованием интерфейса OpenGL.
программного обеспечения. Научное и практическое значение этой области знаний существенно повышается в связи с разрастанием круга задач, связанных с обработкой так называемых больших данных (big data) - сверхбольших массивов слабоструктурированных данных, обработка которых должна осуществляться в реальном масштабе времени. Реализация функциональности, позволяющей отображать в реальном масштабе времени сложные трехмерных графические объекты и сцены на экране компьютеров, является довольно сложной задачей и требует, как правило, совместного применения специализированной аппаратуры (в частности, высокопроизводительных видеокарт) и создаваемого для таких систем специального программного обеспечения.
Один из самых широко распространенных инструментариев для разработки программного обеспечения систем визуализации трехмерных объектов и сцен основан на спецификации интерфейса прикладного программирования OpenGL (Open Graphics Library). В своей основе эта спецификация определяет независимый от языка программирования и аппаратно-программной платформы кроссплатформенный, программный интерфейс для создания прикладных программ, использующих двухмерную и трехмерную компьютерную графику. Спецификация OpenGL включает более 250 функций для рисования сложных трехмерных сцен из простых примитивов. Текущая версия спецификации имеет обозначение OpenGL 4.4. Спецификация продолжает активно развиваться под эгидой консорциума ARB (Architecture Review Board).
Производители аппаратного обеспечения создают свои реализации библиотек, поддерживающих общую спецификация OpenGL. Таким образом, с точки зрения практики реализации систем визуализации трехмерных объектов, OpenGL - это в каждом конкретном случае некоторый набор библиотек, поддерживающий определенный круг аппаратных платформ, операционных систем и языков программирования. Реализации библиотек, как правило, имеют привязку к большинству современных языков программирования, таких как Java, C++, C#, Python, Perl и др.
Существующая литература, посвященная моделированию и визуализации трехмерных объектов, достаточно четко делится на две категории: одна из них - монографии и учебные пособия по теоретическим вопросам компьютерной графики1, другая - практические пособия для системных архитекторов и программистов2. В то же время практически отсутствует литература, в которой выделялись бы типовые научно-практические задачи, возникающие при реализации систем визуализации трехмерных объектов, и об-
суждались бы сценарии и особенности их реализации при помощи широко доступного программного инструментария.
В настоящей статье предпринимается попытка проанализировать ядро спецификации OpenGL, по возможности максимально абстрагируясь от конкретных особенностей ее реализации для разных платформ и языков программирования. Однако ради определенности вся нотация и примеры функций приводятся в формате, соответствующем синтаксису языка программирования C++. Анализ проводится в первую очередь с целью выявления существа тех математических (главным образом, геометрических) и технических задач, которые позволяет решать эта библиотека. Выделенные таким образом задачи систематизируются, для каждой из них по возможности приводится формальная постановка задачи. Кроме того, для каждой из обсуждаемых в статье задач автор попытался сформулировать практические рекомендации по рациональному использованию возможностей OpenGL при создании приложений, требующих визуализации трехмерных объектов.
Общие сведения об интерфейсе OpenGL
Интерфейс прикладного программирования (API - Application Programming Interface) OpenGL позволяет стандартным образом использовать при разработке программного обеспечения возможности отображения трехмерной графики, реализуемые современными компьютерными видеокартами, при необходимости их эмулируя3. При этом снимается нагрузка с центрального процессора, так как вычисления графики переносятся на видеокарту, которая, вообще говоря, лучше для этого подходит по своей производительности. Иными словами, используется модель вычислений «клиент-сервер», где клиентом является программа, работающая на центральном процессоре, а сервером - программа, работающая на процессоре или процессорах видеокарты. При этом клиентом используются вызовы специальных функций, чтобы взаимодействовать с сервером. Если видеокарта не поддерживает некоторую команду, то эта команда будет эмулирована на центральном процессоре, если это возможно.
Вычисления с использованием ресурсов видеокарт в настоящее время получили широкое распространение и в других приложениях, не связанных с компьютерной графикой. Ярким примером может служить эмиссия криптовалют, таких как Bitcoin, с использованием ферм видеокарт.
OpenGL реализует конвейер обработки графики, работающий следующим образом.
1. Клиент вызовами функций API формирует в буфере на своей стороне последовательность команд и данных.
2. Автоматически при заполнении буфера либо по вызову функции glFlush() или glFinish() происходит отправка команд и данных в виде пакета на видеокарту. Отличие этих функций заключается в том, что glFinish() не вернет управление, пока пакет не пройдет все последующие этапы конвейера.
3. Видеокарта выполняет заданные команды над полученными данными, такие как позиционирование точек (преобразование их координат), проецирование, наложение цветов и/или текстур в соответствии с настройками света, фильтрация, постобработка графики и др. Геометрические преобразования имеют следующую очередность.
3.1. Преобразование наблюдения модели: к вершинам заданных примитивов применяются геометрические преобразования поворота и сдвига, выраженные в матрице наблюдения модели.
3.2. Преобразование проецирования: трехмерные координаты вершин преобразовываются в нормализованные экранные координаты (НЭК); данное преобразование выражено в матрице проецирования и операции перспективного деления.
3.3. Преобразование области наблюдения: НЭК вершин преобразуются в двухмерные экранные координаты с учетом глубины и формируют изображение.
4. Результат выводится в один из буферов: буфер кадров, буфер выбора и буфер обратной связи. При выводе в буфер кадров формируется растровое изображение, которое, в частности, выводится на экран. В буфер выбора выводятся определенные клиентом данные об объектах, которые могли бы быть изображены при выводе в буфер кадров. В буфер обратной связи выводятся данные о результате преобразования объектов.
При вызове функций интерфейса OpenGL из программ, написанных на языке C++, должны использоваться следующие основные заголовочные файлы4:
• gl.h - файл, который содержит все определения интерфейса;
• glu.h - файл, который содержит определения библиотеки OpenGL Utility Library (GLU), реализующей некоторые дополнительные возможности интерфейса;
• glut.h - файл, который содержит определения библиотеки The OpenGL Utility Toolkit (GLUT), реализующей кросс-платформенный интерфейс программирования оконных приложений, использующих OpenGL.
Задание трехмерных объектов в OpenGL
Рассмотрим базовые задачи трехмерной графики, которые лежат в основе практически всех методов визуализации трехмерных объектов.
2.1. Примитивы и сложные объекты
Одним из элементарных объектов в трехмерной графике является вершина (в терминологии OpenGL - vertex), представленная трехмерным вектором p = (x, y, z) с масштабным коэффициентом w (смысл масштабного коэффициента будет рассмотрен ниже). В терминах языка C++:
typedef struct{GLfloat x; GLfloat y; GLfloat z; GLfloat w;}VEC-TOR4F,
или, что эквивалентно:
typedef struct{GLfloat f[4];}VECTOR4F.
Вершину можно интерпретировать как точку (GL_POINT). Из одной вершины в другую можно провести отрезок (GL_LINES), через упорядоченную последовательность вершин можно провести ломаную (GL_LINE_STRIP), а также замкнутую ломаную (GL_ LINE_LOOP), для которой автоматически достраивается отрезок из последней вершины в первую. Из трех вершин можно получить треугольник (GL_TRIANGLES). Из четырех и более вершин (считая, что точки добавляются последовательно одна за другой) можно получить ленту треугольников (GL_TRIANGLE_STRIP), в которой каждый следующий треугольник строится на последних трех добавленных вершинах, и веер треугольников (GL_TRIAN-GLE_FAN), в котором каждый следующий треугольник строится на последних двух вершинах и первой.
Существуют еще такие примитивы, как четырехугольники (GL_QUADS) и многоугольники (GL_POLYGON), для задания которых нужно использовать соответственно четыре или любое число вершин, большее трех. Возможно построить ленту четырехугольников (GL_QUAD_STRIP), где каждый следующий четырехугольник строится на последних двух вершинах предыдущего и на новых двух. При этом вершины должны лежать в одной плоскости, а сами многоугольники должны быть выпуклыми. Заметим, что все библиотеки OpenGL оптимизированы под быструю обработку выпуклых многоугольников.
Чтобы отобразить на экране что-то из вышеперечисленного, требуется вызвать функцию void glBegin(GLenum mode),
где в качестве mode указывают отображаемый объект: GL_ POINTS, GL_TRIANGLES и др. Далее необходимо перечислить требуемое количество вершин объекта при помощи вызова функции: void glVertex4f(GLfloat x, GLfloat y, GLfloat z, GLfloat w) или
void glVertex4fv(GLfloat *p4),
где p4 - указатель на массив как минимум из четырех точек GLfloat, например f в структуре VECTOR4F. Однако чаще всего используются функции
void glVertex3f(GLfloat x, GLfloat y, GLfloat z), void glVertex3fv(GLfloat *p3),
где p3 - указатель на массив как минимум из трех GLfloat (f из VECTOR4F тоже соответствует этому условию), при этом по умолчанию в OpenGL считается w = 1. В конце геометрического построения необходимо вызвать функцию void glEnd().
Более сложные объекты и геометрические фигуры определяются через перечисленные выше примитивы. Их построение сводится к комбинированию примитивов.
OpenGL имеет только набор перечисленных геометрических примитивов, из которых создаются все трехмерные объекты. Подобный уровень детализации не всегда бывает удобен при создании сложной графики. Поэтому поверх OpenGL были созданы высокоуровневые библиотеки, такие как Open Inventor, VTK, GLU, GLEW, SDL, GLM и др., позволяющие оперировать более сложными трехмерными объектами. В частности, широко применяемая библиотека GLU, скрывая от программиста многие математические тонкости построения, дает возможность быстро запрограммировать некоторые аналитически задаваемые поверхности.
2.2. Понятие направления обхода многоугольников Порядок обхода вершин при построении треугольника (в общем случае многоугольника) позволяет задать его переднюю и заднюю сторону. Если вершины треугольника обходятся по часовой стрелке, то видимой для зрителя считается задняя сторона треугольника. По умолчанию используется именно это правило, однако его возможно переопределить вызовом функции glFrontFace(GLenum mode): макроопределение GL_CCW, переданное ей в качестве параметра, определяет описанное выше определение передней и задней сторон, а GL_CW - противоположное.
В математических терминах обход вершин треугольника можно проиллюстрировать следующим образом. Пусть заданы вершины
р1,р2,р3. Построим на них два вектора'' = р1 - р2 и' = р3 -р1. Тогда их нормализованное векторное произведение будет вектором-нормалью треугольника с координатами а, Ь, с.
n = ,V = (a, b, c) (1)
i[vl5 v2]i
Если провести через указанные точки плоскость D, то 7?будет нормалью к ней. Нормаль будет указывать от плоскости в сторону положительного полупространства, из которого будет видна передняя сторона треугольника. Можно получить нормальное уравнение плоскости в пространстве D(x, y, z) = ax + by + cz + d = 0, которому удовлетворяют все точки (x, y, z), принадлежащие D. Коэффициент d можно выразить, подставив вместо x, y, z координаты любой из точек плоскости D, например (p1, p2, p3). Если точка p' =(x', y', z) не принадлежит D, то \D(x', y', z') \ будет кратчайшим расстоянием от точки p' до плоскости D. В зависимости от того, с какой стороны от плоскости расположена p', значения выражения D(x', y', z) будут иметь разные знаки, откуда и возникают понятия положительного и отрицательного полупространств.
Когда определена передняя и задняя стороны многоугольников, можно настроить интерполяцию точек при помощи функции void glPolygonMode(GLenum face, GLenum mode), где face указывает, что настройка относится к передней (GL_ FRONT), задней (GL_BACK) или к обеим сторонам (GL_FRONT_ AND_BACK), а mode определяет, что интерполироваться будут все точки (GL_FILL) (например, по заданным трем вершинам треугольника будут построены его внутренние точки, чтобы он казался сплошным), только контуры (GL_LINE) или только вершины (GL_POINT). При интерполяции только контуров (вершин) можно указать, какие из них отображать не следует. Это делается при помощи вызова функции glEdgeFlag(false) перед группой вызовов glVertex*, но затем нужно явно указать, какие контуры (вершины) следует рисовать, вызвав функцию glEdgeFlag(true). Возможно также полностью запретить внутреннюю интерполяцию многоугольников с заданной стороны при помощи вызова функции void glCullFace(GLenum mode),
запретив при этом интерполяцию вызовом функции glEn-able(GL_CULL_FACE), где mode определяет неинтерполируемую сторону (GL_FRONT, GL_BACK), а при указании GL_FRONT_ AND_BACK внутренняя часть многоугольников вообще не будет интерполироваться. Запрет внутренней интерполяции не влияет на интерполяцию контуров и вершин.
2.3. Позиционирование и ориентация объектов Все рассмотренные выше фигуры и примитивы строятся относительно точки отсчета (0; 0; 0). Однако почти всегда требуется одновременно рисовать несколько объектов, каждый из которых задан относительно этой же точки. При этом требуется, чтобы объекты могли перемещаться. Тогда целесообразно ввести некоторую глобальную систему координат с центром в точке (0; 0; 0), а точкам отсчета всех объектов приписываются координаты в этой глобальной системе. Кроме того, часто возникает потребность поворота объекта. Иными словами, позиционирование объекта сводится к преобразованиям параллельного переноса и поворота вокруг точки (0; 0; 0).
Такие преобразования удобнее всего выражаются в терминах матричного исчисления. В OpenGL реализован именно такой подход. Для позиционирования объекта используется матрица наблюдения модели размером 4 х 4:
/ Хв1, хв2, хв3, xt Ув1, Ув2, Ув3, yt
л
yt |
V
гв1, гв2, гв3, \0, 0, 0,
Левая верхняя подматрица размером 3 х 3 характеризует поворот точек объекта относительно его же точки отсчета. Ее можно описать следующим образом. Известно, что точки модели заданы координатами относительно осей х, у, г, или, что то же самое, координаты (х ; ур ; г ) точки Р есть разложение вектора р, проведенного из точки (0; 0; 0) в данную точку Р = (хр; ур; г ) в базисе г = (1; 0; 0), р = (0; 1; 0), £ = (0; 0; 1). Иными словами,
р = х ■ г + у ■1 +г ■ £, (2)
Г р .У р Л р
где вектора р, р, £ образуют ортонормированный базис (ОНБ) на осях х, у, г соответственно. Таким образом, все точки модели
жестко привязаны к осям, и поворот объекта есть поворот этих осей. Для этой цели несколько изменим обозначения. Пусть вектора ], & будут характеризовать только оси глобальной системы отсчета. Оси же произвольного объекта будем характеризовать векторами __ е2, __ (требуется, чтобы они тоже всегда составляли ОНБ). Относительно точек объекта эти векторы считаются ортами осей: __ = {1; 0; 0}, __ = {0; 1; 0}, __ = {0; 0; 1} . Предположим, что внутренняя система координат была повернута. Тогда
числа (хв , ув , 2в ) являются новыми координатами вектора е.
1 1 _1 1
(другим вектором е', ) после поворота, т.е. разложением в базисе [., ], &], а числа (х^ , ув, , гв^) и (хв , ув, , гв ) - новыми координатами векторов е2 и е3 соответственно. Поворот объекта есть переход от одного ОНБ {_, _,, е3} к другому {е^, е',, е'3} . Теперь поло жим, что точка Р задана координатами (хр , ур , р в повернутом базисе {е^, е',, е'3} , т. е. ее координаты есть координаты векторар:
хр
р
: хр ■ е1 +
Ур ■ е'2 + 2р ■ е'3
г е у
е'з)
ур
\
(это выражение есть обобщение выражения (2)). В свою очередь, вектор е'. = {хв, ; ув ; 2в } задан относительно базиса __1, __, _}, т.е.:
е\ = хв., ■ е + ув., ■ е + 2,
I г1 г *
где г е {1, 2, 3}. В матричном виде:
хв
в ■ е3
г 3
р
(е1; _; _ ■
\
^)
(е1, е2, е3)
I х,
ув1, I ; (е1; е2; ез) 2в1
в2
в2 в
хвг ув1.
2
ув„1 1 ; (е1; е2; __ ■
1хв3, ув3,
\2вк
хр ур\
)
в
Ixв
матрица перехода от базиса {e1, e2, e3}
хв , ; хв ,; хв л где Ув1,; Ув2, ; Ув3 , I 2в1г; 2в2,'> 2в3,1
к базису {е'1, е', е '3}, где в столбцах содержатся координаты нового базиса в старом (базисы не обязательно должны быть ортонор-мированными). В общем случае, если {е1, е2, е3} * {г, ], к} , то существует матрица перехода Т, такая что {ё* ~е"2, е7} = {г, 7, "!} , т.е. преобразование поворота может быть представлено произведением нескольких матриц. Левая верхняя подматрица размером 3 х 3 матрицы наблюдения и есть матрица перехода от {г, ], к} к {е1, е2, е3}, где {е1, е2, е3} * {г, ], к} в общем случае, а е1, е, е3 есть орты, отложенные по повернутым осям х, у, г объекта.
Правый верхний вектор-столбец (подматрица размером 3 х 1) матрицы наблюдения объекта характеризует смещение внутренней точки отсчета объекта относительно глобальной точки отсчета.
хг
Для вычисления положения каждой точки модели Рг = уА
в глобальной системе координат следует выполнить следующее преобразование:
Pi
I xi''
yi' zi'
\ x6i, Хд2, xe\ Xi \ xA
= • Уб1, Ув2, Ув3 • yi ' + yt ' l \Ze7, , \zi I \Zt I
Однако существует упомянутый выше масштабный коэффициент wi , с учетом которого данное положение точки вычисляется так:
Pi
I xi ' yi' zi'
\ Хв1, Хв2 , Хв\ Xi \ xA
= • Ув1, Ув2, Ув3 • yi ' + yt I
I , 2в3) I \Zt I
■ wi .
Отсюда становится ясен смысл масштабного коэффициента: он определяет масштаб параллельного переноса координат точки в глобальной системе отсчета. В OpenGL это реализовано так:
/ ..Л
xi
yi zi wi
Хв1
Ув„
хв2, хв3, xt ■ Ув2, Ув3 , yt
Zв
2
в
\ о, о, о;
zt i
I
xi yi zi
\ Wil
Нижняя строка, заданная таким образом, позволяет оставить масштабный коэффициент неизменным, но она так же позволяет изменять его в линейной зависимости от координат точки во внутренней системе отсчета. Манипуляции с этой строкой при визуализации трехмерных объектов применяются сравнительно редко, поэтому в настоящей работе они не рассматриваются.
Легко заметить, что единичная матрица
E =
1; 0; 0; 0 0; 1; 0; 0 0; 0; 1; 0 0; 0; 0; 1
описывает несмещенный и не повернутый объект, т. е. объект, чья система отсчета полностью совпадает с глобальной. Иными словами, E выражает тождественное преобразование положения объекта.
Алгоритм преобразования положения объекта реализован внутри OpenGL (т. е. не требуется явно перемножать матрицу на вектор, чтобы переместить/повернуть объект), а соответственная матрица носит название матрицы наблюдения модели (GL_MOD-ELVIEW). Для хранения матрицы в памяти на стороне клиента (прикладной программы) можно использовать массив из 16 чисел с плавающей точкой, содержащий развертку по столбцам матрицы наблюдения модели:
typedef struct{ GLfloat a[16]; }MATRIX4X4.
При этом следует обращать внимание на принятую в OpenGL последовательность присвоения индексов элементам матрицы в массиве a [16]:
1 0 4 8 12
1 5 9 13
2 6 10 14
3 7 1115
Эта развертка применима к любой матрице в OpenGL.
Таким образом, положение точки определяется в трехмерном пространстве ее координатами относительно некоторой точки отсчета и матрицей наблюдения модели.
2.4. Перемещение объектов
При визуализации процессов трехмерные объекты могут со временем изменять свое положение, в связи с чем возникает вопрос о получении новой матрицы наблюдения модели из предыдущей. Ранее было показано, что поворот объекта в пространстве выражается матрицей перехода от одного базиса к другому размером 3 х 3, но в OpenGL применяются матрицы 4 х 4. С этой целью в OpenGL существуют встроенные средства преобразования матриц, и, таким образом, программист может либо сам реализовать математику преобразований, либо воспользоваться готовыми функциями библиотеки, реализующей интерфейс OpenGL. Как правило, оба подхода используются совместно. Ниже будет рассмотрен ряд преобразований, а также их реализация средствами OpenGL.
Перенос объекта в глобальной системе координат из точки A = ( xA, yA, zA ) в точку B = ( xB, yB, zB ) выполняется прибавлением к текущей матрице наблюдения матрицы TA ^ B :
T
A ^ B
/0; 0; 0 0; 0; 0 0; 0; 0 0; 0
dx dy dz 0; 0
где
dx Xb Xa ; dy
yB - Уа; dz
В OpenGL нет явного метода для выполнения такого преобразования. Более того, несмотря на то что математически подобные преобразования выражаются произведением матриц, в OpenGL это преобразование задано сложением матриц.
zB zA;
Перенос объекта во внутренней системе отсчета на вектор й = (йх, йу, йг) выполняется умножением справа матрицы наблюдения объекта на матрицу Ту:
Td =
/1; 0; 0 0; 1; 0 0; 0; 1 0; 0; 0
dx dy dz ; 1
т. е. координаты каждой точки объекта в глобальной системе отсчета будут вычислены так, как будто ее перенесли на вектор d во внутренней системе отсчета. Аналогичного преобразования можно добиться явно, прибавив d к координатам каждой точки объекта. Это преобразование реализуется функцией: void glTranslatef(GLfloat dx, GLfloat dy, GLfloat dz). Оно может быть выполнено более рационально, если перенос выполняется вдоль только одной из осей x, y, z :
x y
\ = X&1 \ \ Х \ Хвк Х \ |yei| ' dx, y \ = Ув2 | • dy, y \
I \Ze- I \z I b2! \z I
¡хв ye„
\ 3
■ dz.
Поворот объекта может быть выполнен при помощи матрицы
T,
angle '
т,
angle
■V" "V /V"
•A/f! , Jift , -Л/О
1 2 3 ye.,, ye„ , ye,,
Ze
Ze
Ze
\ 0, 0, 0,
0 0 0 1 !
z
где левая верхняя подматрица размером 3 х 3 есть описанная выше матрица перехода от одного базиса к другому. Такая матрица получается произведением матриц поворота вокруг осей внутренней системы координат объекта. Поворот вокруг осей x, y, z на угол angle (значения угла считаются положительными против часовой стрелки, если смотреть в направлении роста оси) может быть выполнен при помощи умножения матрицы наблюдения на матрицу ,
T 0 , где T является соответственно одной из матриц: 0; 0; 0; 1
1 0
0 cos(angle) 0 sin(angle)
cos(angle) 0 0 1 sin(angle) 0
cos(angle) sin(angle) 0
0
- sin(angle) cos(angle)
sin(angle) 0
cos(angle)
- sin(angle) 0 cos(angle) 0 0 1
(3)
В OpenGL для этих целей применяется функция void glRotatef(GLfloat angle,GLfloat x,GLfloat y,GLfloat z), которая осуществляет поворот на угол angle (в градусах) вокруг оси, сонаправленной вектору (x, y, z) и проходящей через начало внутренней системы координат объекта против часовой стрелки, если смотреть в сторону вектора. Вектор должен иметь единичную длину, иначе OpenGL нормализует его самостоятельно. Такое преобразование выполняется умножением на матрицу:
x2(1 - с) + c yx(1 - с) + zs xz(1 - с) - ys 0
xy(1- с) - zs У2(1 - с) + с yz(1 - с) + xs 0
xz(1 - с ) + ys 0
yz(1 - с) - xs 0
z2(1 - с) + с 0
0 1
где с = соБ(ащ1в), б = sin(angle). Это преобразование содержит большое количество арифметических операций, поэтому для поворотов вокруг одной из осей рационально использовать одну из матриц (3).
Матричная арифметика в OpenGL
Рассмотрим теперь, как преобразования наблюдаемых объектов могут быть реализованы на практике. Как правило, при визуализа-
ции сложных процессов или результатов научных экспериментов необходимо отобразить множество объектов, каждому из которых должна быть сопоставлена матрица наблюдения (в тривиальном случае - единичная). При этом если объекты могут перемещаться с течением времени, то их матрицы следует умножать на матрицы соответствующих преобразований: это можно делать явно, запрограммировав операции умножения матриц, а можно - средствами OpenGL. Основные операции, которые необходимо реализовать при первом подходе, были рассмотрены выше. Далее рассмотрим подход с использованием встроенных средств матричной арифметики OpenGL.
В OpenGL существует понятие текущей матрицы - это матрица, над которой выполняются все преобразования. Чтобы определить текущую матрицу, следует вызвать функцию: void glMatrixMode(GLenum mode).
При этом, если осуществляется позиционирование объекта в пространстве, необходимо задать матрицу наблюдения модели (mode = GL_MODELVIEW). Нижеперечисленные функции работают с текущей матрицей:
чтобы загрузить единичную матрицу, используется функция void glLoadldentity(void );
чтобы загрузить матрицу (например, из MATRIX4X4), используется функция
void glLoadMatrixf(const GLfloat *a);
чтобы умножить текущую матрицу на матрицу, хранимую программой-клиентом (например, из MATRIX4X4) справа, используется функция
void glMultMatrixf(const GLfloat *a).
Если нужно загрузить матрицу из MATRIX4X4, предварительно ее транспонировав, с целью последующего умножения, используется функция glLoadTransposeMatrix (glMultTransposeMatrix).
В OpenGL существует стек матриц (для каждого типа матриц, такого как GL_MODELVIEW и др. - свой), куда можно помещать матрицы. Например, в стеке следует сохранить текущую матрицу в случае, если планируется использование OpenGL для умножения других матриц). Для этого используются соответственно функции извлечения и помещения матрицы в стек: void glPopMatrix(void), void glPushMatrix(void).
Наконец, чтобы получить матрицу обратно в массив, следует воспользоваться функцией семейства glGet: void glGetFloatv(GLenum pname, GLfloat *a),
где pname необходимо установить в GL_MODELVIEW_MA-TRIX для случая, когда операции происходят именно над матрицей наблюдения модели, или в GL_PROJECTION_MATRIX, если требуется матрица проецирования.
Проецирование
Выше были рассмотрены способы задания и размещения объектов в трехмерном пространстве. Однако, чтобы иметь возможность изобразить объекты на двухмерном экране компьютера, необходимо выполнить проецирование. Как было сказано выше, преобразование проецирования выполняется средствами OpenGL, но для этого разработчику приложения необходимо задать матрицу проекции (GL_PROJECTION), предварительно сделав ее текущей.
Далее рассмотрим проецирование двух видов: параллельное и перспективное.
При параллельном проецировании важными понятиями являются:
• объект наблюдения - множество точек, которые необходимо отобразить;
• плоскость проецирования - некоторая плоскость в трехмерном пространстве, на которую необходимо отобразить объект наблюдения.
Тогда параллельное проецирование - процесс нахождения точек пересечения плоскости проецирования и прямых, проходящих через точки объекта наблюдения параллельно нормали плоскости проецирования.
Перспективное проецирование основано на следующих понятиях:
• объект наблюдения;
• глаз наблюдателя - точка в трехмерном пространстве, откуда осуществляется обзор объекта или сцены;
• плоскость проецирования - некоторая плоскость в пространстве, не проходящая через глаз наблюдателя, на которую необходимо спроецировать объекты наблюдения.
Итак, перспективное проецирование - процесс нахождения точек пересечения плоскости наблюдения и прямых, проходящих через глаз наблюдателя и точки объектов наблюдения.
Фактически же в OpenGL любое проецирование осуществляется на плоскость, параллельную плоскости xy, и проходящей через точку (0, 0 - nearVal).
При любом виде проецирования координаты точек-проекций должны быть преобразованы в две координаты на плоскости проецирования.
4.1. Нормализованные экранные координаты и понятие глубины
Найти точки на плоскости проецирования недостаточно. Чтобы отобразить их на двухмерном экране, необходимо ввести на плоскости проецирования двухмерную систему координат. В качестве двухмерной системы координат на плоскости проецирования используются так называемые нормализованные экранные координаты (НЭК), значения которых в интервале [-1; 1] можно интерпретировать как находящиеся в пределах экрана, значение (0, 0) соответствует середине экрана. При отсчете НЭК принято, что направление оси x возрастает вправо, y — вверх. Вообще говоря, можно добиться того, что и точки с координатами, превышающими «экранный» интервал, можно будет видеть на экране. При этом на плоскости проецирования очерчивается так называемый прямоугольник наблюдения с углами в точках (left, bottom, -nearVal) и (right, top, -nearVal). Предположим, что left < right, bottom < top. Если это не так, то все равно без изменения формы прямоугольника можно поменять соответствующие координаты. Далее, если речь идет только о точках плоскости проецирования, будем опускать z-координату, равную -nearVal, так как она одинакова для всех точек этой плоскости. Если в ходе проецирования на плоскости проецирования была получена проекция (xp, yp), то, чтобы перевести ее в НЭК, следует определить ее координаты относительно середины прямоугольника наблюдения, а затем поделить на его ширину и высоту соответственно, т. е. нормализовать по прямоугольнику наблюдения. Следует также умножить результат на 2, чтобы нормализованные координаты имели требуемую величину 1 (или -1) на границе прямоугольника наблюдения:
( ) ( right + left top + bottom ) (xp, yp) — 1 2 ' 2 /
(X m У n) = 2 • ------- ,
p,n p,n (right - left, top - bottom)
где под делением векторов подразумевается почленное деление соответствующих координат. Заметим, что прямоугольник наблюдения есть прообраз экрана, на котором наблюдается трехмерная картинка.
Часто бывает так, что точки, имеющие разные трехмерные координаты, перекрываются при проецировании (имеют одинаковые двухмерные координаты проекций). При этом их все равно следует различать в целях последующего определения приоритетов отри-совки на экране. Для этого вводится третья координата проецированной точки - глубина (ее геометрический смысл такой же, как и у координаты z).
Для учета глубины вместе с буфером кадров используется z-буфер (другими словами, буфер глубины), содержащий еще по одному числу для каждой точки буфера кадров - значение, характеризующее его близость к плоскости проецирования.
При проецировании точки всегда требуется вычислять значение ее z-координаты. Оно также должно лежать в интервале [-1; 1], иначе точка не будет нарисована вовсе. Для этого z-координату точки требуется нормализовать. В этих целях вводится еще одна плоскость - плоскость отсечения, проходящая через точку (0, 0 - farVal), farVal > nearVal параллельно плоскости xy и, следовательно, параллельно плоскости проецирования. Значение z = -1 свидетельствует о том, что точка лежит в плоскости проецирования, при z = 0 точка находится строго между плоскостями, а при z = 1 - в плоскости отсечения.
Чтобы использовать z-буфер, необходимо определить критерий распределения приоритетов между перекрывающимися точками при помощи функции
void glDepthFunc(GLenum func), причем значение ее аргумента func = GL_LESS соответствует естественному порядку: ближние точки рисуются поверх дальних. Полный перечень критериев можно найти в руководстве OpenGL 4 References Pages. Далее необходимо определить начальное заполнение буфера глубины вызовом функции glClearDepth(1), где 1 означает, что содержимое буфера кадров удалено на максимально возможное расстояние. Наконец, проверка глубины за-действуется вызовом функции glEnable(GL_DEPTH_TEST). Все возможности, которые можно «разрешить» функцией glEnable, можно «запретить» функцией glDisable с тем же параметром, а чтобы узнать, «разрешена» ли та или иная возможность, следует вызвать функцию
GLboolean glIsEnabled(GLenum cap).
Перед очередным перерисовыванием содержимого экрана требуется вызывать функцию glClear(GL_DEPTH_BUFFER_BIT), чтобы загрузить в буфер глубины начальное заполнение.
Вообще говоря, процесс получения нормализованного значения зависит от способа проецирования: параллельного (ортогонального) или перспективного. Рассмотрим оба способа.
4.2. Параллельное проецирование
В случае параллельной проекции точки объекта наблюдения проецируются на плоскость наблюдения параллельно оси 2. Координаты точек (х, у) и их проекции совпадают, остается только их нормализовать. При нормализации координаты 2 из нее следует вычесть координату по оси 2 середины отрезка между плоскостью отсечения и плоскостью проецирования, а затем поделить на длину этого отрезка и умножить на -2, чтобы получить значение 1 на плоскости отсечения, и -1 на плоскости проецирования:
2 = - 2
p,n
2 - (- farVal) + (- nearVal)
JP_2_
nearVal - farVal
Точки 2р , лежащие либо за плоскостью отсечения, либо перед плоскостью проецирования, нормализованные по этой формуле, будут иметь координату 2рп ф [-1; 1] и, следовательно, отображаться на экране компьютера не будут. Заметим, что ¡атУа\ и пватУа1 могут иметь любые знаки.
Вышеупомянутые преобразования параллельного проецирования с нормализацией могут быть представлены в виде умножения вектора-точки справа на матрицу проецирования следующего вида:
2
right - left 0
0
2
top - bottom
0 0
0
0
2
farVal - nearVal
0
0
right + left right - left
top + bottom top - bottom
_ farVal + nearVal farVal - nearVal
1
Генерация такой матрицы с умножением ее на текущую матрицу осуществляется вызовом функции
void glOrtho(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble nearVal, GLdouble farVal) с соответствующими параметрами.
0
4.3. Перспективное проецирование
При перспективном проецировании считается, что наблюдатель находится в точке (0, 0, 0) глобальной системы координат и смотрит в сторону убывания оси г . В таком случае значение координат по оси х относительно него возрастает вправо, а по оси у -вверх. Аналогично определяются плоскости отсечения и проекции, при этом ¡атУа! > иватУа1 > 0. На рис. 1 показан процесс перспективного проецирования.
Рассмотрим нахождение проекции (хр, ур, гр) некоторой
точки (х0, у0, г0). Нахождение координат хр, ур принципиально не
отличается, поэтому подробно рассмотрим эту процедуру только для ур (рис. 2).
Плоскость отсечения
(Xp,Уp,Zp)
(right,top,-nearVal)
~ ^ "" I \
(хо,уоЛ)
-V
^агУа!
(left,bottom
Плоскость проецирования
Рис. 1. Нахождение проекции точки (точка А - глаз наблюдателя)
O -nearVal
У0
у»
-4-
Плоскость проецирования Рис. 2. Нахождение координаты проекции ур точки с координатой У0
у
z
Треугольники 0, yp, -nearVal и 0, y0, z0 подобны, следовательно, _ narVal = У" , откуда Ур = Уо • у• ( -nearVal ) , где умно-
1 0
жение на — носит название перспективного деления. Аналогично
20 1 и для х : xp = х0 •—• ( -nearVal ) .
2q
Кроме того, хр, yp должны быть нормализованы, и это должно быть учтено при составлении матрицы проецирования.
Матричное умножение невозможно совместить с делением на одну из координат вектора, поэтому в OpenGL используется 4-я координата точки. А именно первые три координаты вектора-произведения делятся на 4-ю, в качестве которой берется число (- z0).
Заметим также, что операция перспективного деления на 4-ю координату выполняется и в случае ортогональной проекции, но z0 не влияет на это, так как матрица проецирования построена нужным образом.
Как было сказано выше, перспективному делению подвергается и координата глубины zp, но при этом она также должна быть нормализована и лежать в интервале [-1, 1]. Для этого в OpenGL применяется следующая матрица перспективного проецирования, учитывающая все вышеупомянутые особенности:
2 * nearVal right - left
0
0 0
0
2 * nearVal top - bottom
0 0
right + left right - left
top + bottom top - bottom
farVal + nearVal farVal - nearVal
-1
0
2 * farVal * nearVal farVal - nearVal
0
0
Генерация такой матрицы с умножением ее на текущую матрицу осуществляется вызовом функции
void glFrustum (GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble nearVal, GLdouble farVal) с соответствующими параметрами.
4.4. Особенности проецирования
Следует отметить две важные особенности выбора параметров матриц проецирования.
1. При вычислении глубины точек-проекций может получиться так, что две точки окажутся ближе друг к другу, чем некоторое е и у них совпадут все три координаты (хр,п, урпП, гр,п). Это явление
называется конфликтом глубины (англ. z-fighting) и может повлечь нежелательные дефекты визуализации. Оно связано с ограниченной длиной разрядной сетки, выделяемой для представления чисел в памяти компьютера. Чтобы уменьшить е, необходимо, чтобы
значение величины /атУа! _ было как можно меньше, но при этом
пеатУаI
теряется приблизительно ¡у1 ^ битов информации о глубине точки.
2. При выборе пеатУа1 и вершин прямоугольника наблюдения требуется следить, чтобы соотношение его сторон и взаимное расположение с глазом наблюдателя как можно лучше соответствовало взаимному расположению глаз реального пользователя и его монитора с соответствующим соотношением сторон, иначе изображение будет неудобно наблюдать или же оно будет выглядеть вовсе неестественно.
4.5. Концепция наблюдателя при проецировании
Рассмотренных выше методов проецирования может быть недостаточно, когда необходим обзор сцены с произвольной точки зрения. Для этого вводится несколько дополнительных понятий.
Наблюдатель - совокупность глаза наблюдателя и связанной с ним системы отсчета. То есть наблюдателя можно описать матрицей наблюдения модели. Рассмотрим особенности проецирования объектов относительно произвольного наблюдателя: у
1. перемещение наблюдается на некоторый вектор й эквивалентно перемещению всех объектов наблюдения на вектор -й (в глобальной системе отсчета);
2. применение преобразования поворота к наблюдателю эквивалентно применению поворота в противоположную сторону к глобальной системе координат.
Как рассматривалось выше, после преобразования матрицы наблюдения модели точки объектов наблюдения заданы в глобальной системе координат, а значит, точки всех объектов наблюдения с координатами в глобальной системе в совокупности можно рассматривать как один глобальный объект наблюдения - сцену.
Преобразование наблюдателя - преобразование заданных в глобальной системе отсчета точек всех объектов наблюдения (сцены) таким образом, чтобы проецирование производилось относительно наблюдателя.
Матрица преобразования наблюдателя - матрица, реализующая преобразование наблюдателя.
В OpenGL преобразование наблюдателя реализуется умножением заданной матрицы проецирования на матрицу преобразования наблюдателя: таким образом это преобразование повлияет на все проецируемые точки. Это действие может быть реализовано вызовом функции библиотеки GLU:
void gluLookAt(GLdouble eyeX, GLdouble eyeY, GLdouble eyeZ, GLdouble centerX, GLdouble centerY, GLdouble centerZ, GLdouble upX, GLdouble upY, GLdouble upZ).
Координаты (eyeX; eyeY; eyeZ) задают положение наблюдателя, (centerX; centerY; centerZ) - координаты точки, находящейся перед наблюдателем, (upX; upY; upZ) - вектор, направленный относительно наблюдателя вверх. Имея матрицу наблюдения модели, описывающую наблюдателя
^ -V- /у "V У1 ^ Лв1, лв2 , лв3> xt
Ув1, Ув2, Ув3 , yt
ze1, гв2 , гв3> zt 0, 0, 0, 1
параметры функции можно определить следующим образом:
eyeX eyeY eyeZ
xt
yt \ zt
centerX centerY centerZ
xt + Хв3 yt + ye3
\ zt + zb3 /
upX upY upZ
хв
Ув2 / 2в2 /
где предполагается, что направление вверх совпадает с направлением оси у (вектора ), а направление вперед - с осью 2 (вектор ё ). В действительности не важно, как именно выбирается ориентация вверх, влево - этот выбор всегда относителен, главное - правильно придерживаться выбранной ориентации при разработке программного обеспечения.
2
Другие возможности моделирования
Выше были рассмотрены с теоретической и прикладной точек зрения положения, которые могут считаться базовыми для визуализации трехмерной графики с использованием инструментария
OpenGL. Ниже рассматриваются еще некоторые полезные возможности.
5.1. Определение параметров примитивов
Некоторые примитивы имеют дополнительные параметры:
• размер точки (GL_POINTS), режим интерполяции (GL_ POINT) и др. параметры в единицах измерения буфера кадров (т. е. вне зависимости от удаленности точки от наблюдателя при перспективном проецировании) указываются при помощи вызова функции
void glPointSize(GLfloat size);
• толщина линии (GL_LINES, GL_LINE_STRIP/LOOP), режим интерполяции GL_LINE) и др. параметры в единицах буфера кадров указываются при помощи вызова функции
void glLineWidth(GLfloat width);
• фактура линии задается функцией
void glLineStipple(GLint factor, GLushort pattern), где pattern - 16-битовый шаблон, 1 в котором соответствует наличию точки при интерполяции, а 0 - отсутствию; шаблон применяется циклически, младшие биты учитываются сначала. Параметр factor определяет, сколько раз будет учтен каждый из битов шаблона, перед тем как перейти к следующему биту. Эту возможность нужно предварительно разрешить вызовом glEnable(GL_LINE_ STIPPLE).
5.2. Использование цвета
В компьютерной графике принято кодировать цвет в виде разложения по яркостям трех базовых цветов: красного, зеленого, синего. В OpenGL цвета кодируются числами с плавающей точкой в диапазоне [0; 1]. Чем больше число, кодирующее компоненту, тем весомее ее вклад в формирование результирующего цвета. Каждая точка имеет свой цвет, а также дополнительную компоненту - так называемый альфа-канал (alpha), характеризующий прозрачность этой точки.
Чтобы использовать альфа-канал, нужно задействовать смешивание вызовом функции glEnable(GL_BLEND). В этом случае если новая точка после проецирования (source) имеет глубину более приоритетную, чем та, что уже находится в буфере кадров (destination), то можно вычислить результирующий цвет в зависимости от цветов обеих точек. Для этого нужно определить функцию смешивания. Вызов функции glBlendFunc(GL_SRC_ALPHA,
GL_ONE_MINUS_SRC_ALPHA) определит следующий порядок определения конечного цвета:
red red red
green = green ■ A + Л source green ' (1 Asource)
^ blue ^ final blue ^ source ^ blue destination
где Asource - величина альфа-канала новой точки.
Смешивание позволяет добиться таких эффектов, как прозрачность материалов и отражения. Важно, что смешивание происходит только в том случае, если точка признана более приоритетной по глубине, иначе она не будет учтена.
Теперь рассмотрим задание цветов. Чтобы задать цвет фона - начального заполнения буфера изображения, используется функция
void glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha),
чтобы очистить фон указанным цветом, вызывается функция void glClear(GL_COLOR_BUFFER_BIT). Также можно определить цвет для последующих добавленных вершин многоугольников при помощи вызова функции
void glColor4f(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha).
Эта функция имеет много вариантов. Интерполяция цвета при выполнении интерполяции точек многоугольника осуществляется вызовом функции
void glShadeModel(GLenum mode).
Если mode = GL_FLAT, то все точки будут интерполированы одним цветом - цветом последней вершины многоугольника (кроме GL_POLYGON, для него - первой), если mode = GL_SMOOTH, то цвет каждой точки будет интерполироваться с учетом цветов всех вершин.
Вывод трехмерной графики
Однако недостаточно получить НЭК для того, чтобы увидеть изображение на экране компьютерного монитора. Вывод графики в OpenGL осуществляется в буфер кадров (англ. frame buffer). Буфер вывода кадров представляет собой развернутый массив выровненных по сетке точек с заданным цветом, по аналогии с массивом пикселей на мониторе. И хотя буфер кадров является чаще всего линейным (одномерным) участком оперативной памяти компью-
тера, его целесообразно рассматривать как двухмерную таблицу с содержащими цвета точек ячейками, развернутую в зависимости от архитектуры компьютера, а точнее, от способа организации его оперативной памяти. В буфере кадров также задана такая система координат, что точка (0, 0) - это нижний левый угол, ось x направлена вправо, а ось y - вверх.
Исторически сложилось так, что система координат на мониторе компьютера, а также при программировании оконных интерфейсов (в частности, это касается координат указателя мыши) такова, что ось y возрастает вниз, а точка (0, 0) соответствует верхнему левому углу монитора (или окна). Перейти от адреса ячейки в буфере кадров к координатам окна можно, используя формулу Уокно = (Высота окна) - Убуфер .
6.1. Понятие экрана
Чтобы поместить точку в буфер кадров, нужно преобразовать ее НЭК в координаты буфера. Чтобы описать этот процесс, определим экран как прямоугольную подобласть буфера кадров, соответствующую прямоугольнику проецирования. Экран определяется положением своего нижнего левого угла (x, y) в буфере кадров, а также своими шириной width и высотой height.
В итоге координаты точки (xpw, ypw ) в буфере кадров вычисляются из НЭК (xp n, yp n ) по формуле
(xp,w, yp,w ) = (ixp,n + 1) . 2 , ( yp,n + 1) . g ) .
Для задания экрана используется функция
void glViewport(GLint x, GLint y, GLsizei width, GLsize height) с соответствующими параметрами.
6.2. Оконные приложения
Чтобы использовать интерфейс OpenGL в конкретной операционной системе (ОС), требуется учитывать особенности ее API. Настоящая статья не ставит целью описание всех таких особенностей. Один из самых удобных и часто используемых приемов реализации пользовательского интерфейса при работе с OpenGL - это использование кроссплатформенной библиотеки GLUT5.
GLUT позволяет выводить графику в окно пользовательского интерфейса ОС. При этом клиентская часть окна, а именно графический буфер окна, ставится в соответствие (или физически при-
равнивается) буферу кадров OpenGL. То есть изображение, выведенное OpenGL в буфер кадров, будет отображено в окне.
Важной особенностью GLUT является поддержка двойной буферизации, при которой буфер кадров OpenGL и графический буфер окна - это два разных буфера. Тем самым OpenGL позволяет рисовать сложную (по затратам времени) сцену в буфер кадров и только в конце переключить буферы (поменяться с текущим окном). В итоге пользователь увидит на мониторе законченное изображение, а не процесс работы, который при динамической перерисовке сцены является нежелательным эффектом. В частности, перерисовка непосредственно в графическом буфере окна, как правило, приводит к заметному мерцанию экрана.
6.3. Применение буфера выбора
OpenGL может выводить не только изображение и не только в буфер кадров. Можно назначить имена (names) рисуемым примитивам, чтобы в дальнейшем получить список имен тех примитивов, которые при проецировании попадают в прямоугольник проецирования.
Вывод списка имен примитивов осуществляется в буфер выбора. Этот буфер необходимо указать в первую очередь вызовом функции
void glSelectBuffer(GLsizei size, GLuint *buffer), где buffer - указатель на предварительно выделенный под буфер массив, size - максимально допустимое количество значений в массиве. Количество выделенной памяти рассчитывается в соответствии с форматом буфера (табл. 1) и количеством имен.
Указав буфер, можно перейти в режим выбора при помощи вызова функции
GLint glRenderMode(GLenum mode), где mode = GL_SELECT соответствует режиму выбора, а mode = GL_RENDER (рендеринг) - режиму вывода графики в буфер кадров. После каждого перехода в режим выбора заполнение буфера выбора начинается сначала.
Перейдя в режим выбора, нужно инициализировать пустой стек имен вызовом функции glInitNames(). Далее можно помещать в стек значения :
void glPushName(GLuint name); заменять значение, находящееся на вершине:
void glLoadName(GLuint name); или удалять значения из вершины стека: void glPopName(void).
После подготовки содержимого стека к отображению конкретного примитива или нескольких примитивов вывод изображения выполняется таким же образом, как и в режиме вывода графики. Если на момент следующего изменения стека имен хотя бы один рисуемый примитив попал в пределы прямоугольника наблюдения, то текущее наполнение стека будет скопировано в буфер выбора.
Модель именования в виде стека позволяет организовать иерархический выбор. Например, низ стека может соответствовать всей модели, а вершина стека - какой-либо ее отдельной части. Таким образом можно разделить перекрывающиеся имена.
Для завершения отображения всех необходимых объектов вызывается функция glRenderMode с параметром, отличным от ОЬ_8БЬБСТ. Тогда эта функция вернет количество записей (но не значений!), попавших в буфер, но если размер буфера выбора оказался недостаточным, функция вернет отрицательное значение. Если хотя бы один примитив, отображенный с момента последнего изменения стека, попал в прямоугольник проецирования, то последнее наполнение стека также будет скопировано в буфер.
Наконец, содержимое буфера выбора обрабатывается в соответствии с форматом, приведенным в табл. 1.
Если требуется выбрать только объекты, которые бы попали в некоторый участок буфера кадров, то следует соответствующим образом изменить прямоугольник наблюдения, переопределив матрицу проецирования.
Таблица 1
Формат буфера выбора
Смещение* Содержимое буфера (значения)
0 Количество имен в стеке имен П1 первой записи
1 Минимальное значение нормированной глубины среди вершин объекта, умноженное на 232 - 1
2 Максимальное значение нормированной глубины
3 Значение, взятое с низа стека имен
Значения стека в порядке убывания глубины
2 + n1 Значение, взятое с вершины стека
3 + ni Количество имен в стеке имен п2 второй записи
"Смещение отсчитывается в величинах, принятых для типа GLuint.
6.4. Обратная связь
Помимо вывода графики и имен выбранных объектов, возможен вывод в буфер обратной связи информации о параметрах объектов сцены после обработки в буфер обратной связи (режим GL_FEEDBACK).
Для обнаружения объекта в буфере обратной связи он предварительно помечается так называемым токеном (англ. token) при помощи вызова функции
void glPassThrough(GLfloat token).
Это должно быть сделано до отрисовки примитивов соответствующего объекта.
Буфер обратной связи назначается функцией void glFeedbackBuffer(GLsizei size, GLenum type, GLfloat *buffer), где параметр type определяет, какая информация должна попасть в буфер, при этом параметры size и buffer определяются аналогично тому, как это делалось для буфера выбора.
Для примера рассмотрим получение двухмерных координат в системе координат буфера кадров, которые бы имели примитивы после обработки в режиме вывода графики. В этом случае type = GL_2D. Размер буфера следует рассчитывать в соответствии с форматом буфера, его типом и количеством примитивов.
Рассмотрим формат буфера. Буфер состоит из записей, непрерывно следующих друг за другом. Каждая запись начинается с идентифицирующего ее токена, который является целочисленной константой, приведенной к типу GLfloat. Формат записей приведен в табл. 2.
Под вершиной понимается структура данных, соответствующая параметру type буфера. При type = GL_2D эта структура может быть определена следующим образом:
typedef struct {GLfloat x; GLfloat y;}VECTOR2F. Назначив буфер обратной связи, переходят к предусмотренному в OpenGL режиму обратной связи, применяя функцию glRenderMode. Далее помеченные токенами при помощи функции glPassThrough объекты отображаются так, как если бы они записывались в буфер кадров, после чего следует переключиться в другой режим и обработать содержимое буфера.
Функция glRenderMode при выходе из режима обратной связи вернет количество попавших в буфер значений (но не записей!) или отрицательную величину, если размера буфера оказалось недостаточно.
Таблица 2
Формат записей буфера обратной связи
Токен Содержимое
GL POINT TOKEN (примитив точка) вершина (следующая запись)
GL LINE TOKEN GL LINE RESET TOKEN (примитив отрезок) вершина вершина (следующая запись)
GL POLYGON TOKEN (примитив многоугольник) количество вершин n вершина 1 вершина n
GL PASS THROUGH TOKEN (метка, назначенная вызовом gl- PassThrough) значение метки token (следующая запись)
GL DRAW PIXEL TOKEN GL COPY PIXEL TOKEN GL BITMAP TOKEN (прочие типы записей*) вершина (следующая запись)
* Рассмотрение прочих типов записей выходит за рамки настоящей работы, но их все же необходимо корректно обрабатывать при разборе буфера обратной связи.
Заключение
В статье обобщены задачи визуализации данных и процессов, возникающие при разработке программного обеспечения с использованием кроссплатформенного интерфейса прикладного программирования OpenGL, а также некоторые практические приемы их программной реализации. К таким задачам относятся прежде всего геометрическое описание трехмерных объектов, выполнение матричных операций, преобразования проецирования объектов, управление цветом и интерполяцией геометрических фигур, вывод трехмерной графики на экран и в буферы видеокарты, а также ряд других вспомогательных задач. Проведенный анализ позволяет сделать вывод о том, что интерфейс прикладного программирования OpenGL предоставляет весьма широкий и полный набор функций для визуализации трехмерных объектов, статических сцен и интерактивных процессов.
Несмотря на определенные сложности и специфику приемов управления визуализацией, реализуемых библиотеками на основе OpenGL, применение интерфейса OpenGL предоставляет несомненные преимущества разработчикам сложных программных комплексов, требующих интенсивного использования функций компьютерной графики. К основным преимуществам следует от-
нести кроссплатформенность, позволяющую обеспечить переносимость прикладных программ между многими аппаратно-программными платформами, возможность разработки программ на разных языках программирования в пределах одного проекта, унификацию способов передачи данных между модулями программ, поддержку вывода изображения высокой четкости в реальном масштабе времени без потери качества изображения. Интерфейс OpenGL практически не имеет конкурентов, за исключением интерфейса DirectX на платформе Windows.
Став одним из ведущих инструментов реализации компьютерной графики и анимации, спецификация OpenGL в настоящее время является стандартом де-факто и обладает большим потенциалом дальнейшего развития. В связи с этим реализации OpenGL могут быть рекомендованы в качестве основного рабочего инструментария широкому кругу разработчиков систем компьютерной графики и научной визуализации.
Примечания
См.: Шикин Е.В., Плис А.И. Кривые и поверхности на экране компьютера. Руководство по сплайнам для пользователей. М.: Диалог-МИФИ, 1996. 240 с. См.: Shreiner D., Seilers G., Kessenich J., Licca-Kane B. OpenGL Programming Guide. 8th ed.: The Official Guide to Learning OpenGL, Version 4.3. Upper Saddle River, NJ: Addison-Wesley, 2013. 986 p.; Wright R., HaemelN., Seilers G., Lipchak B. OpenGL Superbible. 5th ed.: Comprehensive Tutorial and Reference. Boston, MA: Addison-Wesley, 2011. 1002 p.
См.: OpenGL 4 References Pages [Электронный ресурс] // Сайт проекта OpenGL. URL: http://www.opengl.org/sdk/docs/man (дата обращения: 09.03.2014).
См.: The OpenGL Utility Toolkit [Электронный ресурс] // Сайт проекта OpenGL. URL: http://www.opengl.org/resources/libraries/glut (дата обращения: 09.03.2014).
См.: The OpenGL Utility Toolkit (GLUT) Programming Interface API Version 3 [Электронный ресурс] // Сайт проекта OpenGL. URL: http://www.opengl.org/ resources/libraries/glut/glut-3.spec.pdf (дата обращения: 09.03.2014).
2
3
4
5