OpenGL является на данный момент одним из самых популярных программных интерфейсов (API) для разработки приложений в области двумерной и трехмерной графики. Стандарт OpenGL был разработан и утвержден в 1992 году ведущими фирмами в области разработки программного обеспечения, а его основой стала библиотека IRIS GL, разработанная Silicon Graphics.
На данный момент реализация OpenGL включает в себя несколько библиотек (описание базовых функций OpenGL, GLU,GLUT,GLAUX и другие), назначение которых будет описано ниже.
Что такое OpenGL?
С точки зрения программиста, OpenGL – это набор команд, которые описывают геометрические объекты и способ их отображения на экране. В большинстве случаев OpenGL предоставляет непосредственный интерфейс, т.е. определение объекта вызывает его визуализацию в буфере кадра.
Типичная программа, использующая OpenGL, начинается с определения окна, в котором будет происходить отображение. Затем создается контекст OpenGL и ассоциируется с этим окном. Далее программист может свободно использовать команды и операции OpenGL API. Часть команд используются для рисования простых геометрических объектов (т.е. точек, линий, многоугольников), тогда как другие задают режимы отображения этих примитивов. Например, можно задать режимы заливки цветом, отображение из трехмерной системы координат в экранную систему. Есть возможности для прямого контроля над буфером кадра, такие как чтение и запись пикселей.
С точки зрения разработчика, OpenGL - это набор команд, которые управляют использованием графической аппаратуры. Если аппаратура состоит только из адресуемого буфера кадра, тогда OpenGL должен быть реализован полностью с использованием ресурсов центрального процессора. Обычно графическая аппаратура предоставляет различные уровни ускорения: от аппаратной реализации вывода линий и полигонов до изощренных графических процессоров с поддержкой различных операций над геометрическими данными.
OpenGL является прослойкой между аппаратурой и пользовательским уровнем. Это дает возможность использовать единый интерфейс для разных платформ, при этом получая оптимальную производительность с использованием аппаратной поддержки.
Вообще, OpenGL можно сравнить с конечным автоматом, состояние которого определяется множеством значений специальных переменных (их имена обычно начинаются с символов GL_) и значениями текущей нормали, цвета и координат текстуры. Все эта информация будет использована при поступлении в систему координат вершины для построения фигуры, в которую она входит. Смена состояний происходит с помощью команд, которые оформляются как вызовы функций.
Характерными особенностями OpenGL, которые обеспечили распространение и развитие этого графического стандарта, являются:
Стабильность
Дополнения и изменения в стандарте реализуются таким образом, чтобы сохранить совместимость с разработанным ранее программным обеспечением.
Надежность и переносимость
Приложения, использующие OpenGL, гарантируют одинаковый визуальный результат вне зависимости от типа используемой операционной системы и организации отображения информации. Кроме того, эти приложения могут выполняться как на персональных компьютерах, так и на рабочих станциях и суперкомпьютерах.
Легкость применения
Стандарт OpenGL имеет продуманную структуру и интуитивно понятный интерфейс, что позволяет с меньшими затратами создавать эффективные приложения, содержащие меньше строк кода, чем с использованием других графических библиотек. Необходимые функции для обеспечения совместимости с различным оборудованием реализованы на уровне библиотеки и значительно упрощают разработку приложений.
Как уже было сказано, существует реализация OpenGL для разных платформ, для чего было удобно разделить базовые функции графической системы и функции для отображения графической информации и взаимодействия с пользователем. Были созданы библиотеки для отображения информации с помощью оконной подсистемы для операционных систем Windows и Unix (WGL и GLX соответственно), а также библиотеки GLAUX и GLUT, которые используются для создания так называемых консольных приложений.
Библиотека GLAUX уступает по популярности написанной несколько позже библиотеке GLUT, хотя они предоставляют примерно одинаковые возможности. В дальнейшем в данном пособии в качестве основной будет рассматриваться библиотека GLUT, предоставляющая широкий набор средств взаимодействия с пользователем.
В состав библиотеки GLU вошла реализация более сложных функций, таких как набор популярных геометрических примитивов (куб, шар, цилиндр, диск), функции построения сплайнов, реализация дополнительных операций над матрицами и т.п. Все они реализованы через базовые функции OpenGL.
С точки зрения архитектуры графическая система OpenGL является конвейером, состоящим из нескольких этапов обработки данных:
GL обрабатывает и выводит так называемые примитивы (primitive) с учетом некоторого числа выбранных режимов. Каждый примитив – это точка, отрезок, многоугольник и т.д. Каждый режим может быть изменен независимо от других. Определение примитивов, выбор режимов и другие операции описывается с помощью команд в форме вызовов процедур.
Примитивы определяются набором из одной или более вершин (vertex). Вершина определяет точку, конец грани, угол многоугольника. С каждой вершиной ассоциируются некоторые данные (координаты, цвет, нормаль, текстурные координаты). В подавляющем большинстве случаев каждая вершина обрабатывается независимо от других.
Команды OpenGL всегда обрабатываются в том порядке, в котором они поступают, хотя могут происходить задержки перед тем, как проявится эффект от их выполнения.
Для обеспечения интуитивно понятных названий в OpenGL полное имя команды имеет вид:
type glCommand_name[1 2 3 4][b s i f d ub us ui][v](type1 arg1,…,typeN argN) |
Таким образом, имя состоит из нескольких частей:
Gl | это имя библиотеки, в которой описана эта функция: для базовых функций OpenGL, функций из библиотек GLU, GLUT, GLAUX это gl, glu, glut, glaux соответственно |
Command_name | имя команды |
[1 2 3 4] | число аргументов команды |
[b s i f d ub us ui] | тип аргумента: символ b означает тип GLbyte (аналог char в С\С++), символ f – тип GLfloat (аналог float), символ i – тип GLint (аналог int) и так далее. Полный список типов и их описание можно посмотреть в файле gl.h |
[v] | наличие этого символа показывает, что в качестве параметров функции используется указатель на массив значений |
Символы в квадратных скобках в некоторых названиях не используются. Например, команда glVertex2i() описана как базовая в библиотеке OpenGL, и использует в качестве параметров два целых числа, а команда glColor3fv() использует в качестве параметра указатель на массив из трех вещественных чисел.
Будем рассматривать построение консольного приложения при помощи библиотеки GLUT или GL Utility Toolkit, получившей в последнее время широкое распространение. Эта библиотека обеспечивает единый интерфейс для работы с окнами вне зависимости от платформы, поэтому описываемая ниже структура приложения остается неизменной для операционных систем Windows, Linux и многих других.
Функции GLUT могут быть классифицированы на несколько групп по своему назначению:
Инициализация проводится с помощью функции
glutInit(int *argcp, char **argv) |
Переменная argcp есть указатель на стандартную переменную argc описываемую в функции main(), а argv – указатель на параметры, передаваемые программе при запуске, который описывается там же. Эта функция проводит необходимые начальные действия для построения окна приложения, и только несколько функций GLUT могут быть вызваны до нее. К ним относятся:
glutInitWindowPosition(int x, int y) glutInitWindowSize(int width, int height) glutInitDisplayMode(unsigned int mode) |
Первые две функции задают соответственно положение и размер окна, а последняя функция определяет различные режимы отображения информации, которые могут совместно задаваться с использованием операции побитового “или” ( “ | “ ) :
GLUT_RGBA | Режим RGBA. Используется по умолчанию, если не указаны явно режимы GLUT_RGBA или GLUT_INDEX. |
GLUT_RGB | То же, что и GLUT_RGBA. |
GLUT_INDEX | Режим индексированных цветов (использование палитры). Отменяет GLUT_RGBA. |
GLUT_SINGLE | Окно с одиночным буфером. Используется по умолчанию. |
GLUT_DOUBLE | Окно с двойным буфером. Отменяет GLUT_SINGLE. |
GLUT_STENCIL | Окно с трафаретным буфером. |
GLUT_ACCUM | Окно с буфером накопления. |
GLUT_DEPTH | Окно с буфером глубины. |
Это неполный список параметров для данной функции, однако, для большинства случаев этого бывает достаточно.
Двойной буфер обычно используют для анимации, сначала рисуя что-нибудь в одном буфере, а затем, меняя их местами, что позволяет избежать мерцания. Буфер глубины или z-буфер используется для удаления невидимых линий и поверхностей.
Работа с трафаретным буфером и буфером накопления описана в разделе Спецэффекты.
Функции библиотеки GLUT реализуют так называемый событийно-управляемый механизм. Это означает, что есть некоторый внутренний цикл, который запускается после соответствующей инициализации и обрабатывает, один за другим, все события, объявленные во время инициализации. К событиям относятся: щелчок мыши, закрытие окна, изменение свойств окна, передвижение курсора, нажатие клавиши, и "пустое" (idle) событие, когда ничего не происходит. Для проведения периодической проверки совершения того или иного события надо зарегистрировать функцию, которая будет его обрабатывать. Для этого используются функции вида:
void glutDisplayFunc(void (*func)(void)) void glutReshapeFunc(void (*func)(int width, int height)) void glutMouseFunc(void (*func)(int button, int state, int x, int y)) void glutIdleFunc(void (*func)(void)) |
То есть параметром для них является имя соответствующей функции заданного типа. С помощью glutDisplayFunc() задается функция рисования для окна приложения, которая вызывается при необходимости создания или восстановления изображения. Для явного указания, что окно надо обновить, иногда удобно использовать функцию
void glutPostRedisplay(void) |
Через glutReshapeFunc() устанавливается функция обработки изменения размеров окна пользователем, которой передаются новые размеры.
glutMouseFunc() определяет обработчика команд от мыши, а glutIdleFunc() задает функцию, которая будет вызываться каждый раз, когда нет событий от пользователя.
Контроль всех событий происходит внутри бесконечного цикла в функции
void glutMainLoop(void) |
которая обычно вызывается в конце любой программы, использующей GLUT. Структура приложения, использующего анимацию, будет следующей:
#include <GL/glut.h> void MyIdle(void) { /*Код, который меняет переменные, определяющие следующий кадр */ ... } void MyDisplay(void) { /* Код OpenGL, который отображает кадр */ ... /* После рисования переставляем буфера */ glutSwapBuffers(); } void main(int argcp, char **argv) { /* Инициализация GLUT */ glutInit(&argcp, argv); glutInitWindowSize(640, 480); glutInitWindowPosition(0, 0); /* Открытие окна */ glutCreateWindow("My OpenGL Application"); /* Выбор режима: двойной буфер и RGBA цвета */ glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH); /* Регистрация вызываемых функций */ glutDisplayFunc(MyDisplay); glutIdleFunc(MyIdle); /* Запуск механизма обработки событий */ glutMainLoop(); } |
В случае если приложение должно строить статичное изображение, можно заменить GLUT_DOUBLE на GLUT_SINGLE, так как одного буфера в этом случае будет достаточно, и убрать вызов функции glutIdleFunc().
Положение вершины определяются заданием их координат в двух-, трех-, или четырехмерном пространстве (однородные координаты). Это реализуется с помощью нескольких версий команды glVertex:
void glVertex[2 3 4][s i f d](type coords) void glVertex[2 3 4][s i f d]v(type *coords) |
Каждая команда задает 4 координаты вершины: x, y, z и w. Команда glVertex2 получает значения x и y. Координата z в таком случае устанавливается по умолчанию равной 0, а координата w равной 1. Vertex3 получает координаты x, y, z и заносит в координату w значение 1. Vertex4 позволяет задать все 4 координаты.
Для ассоциации с вершинами цветов, нормалей, и текстурных координат используются текущие значения соответствующих данных. Эти значения могут быть изменены в любой момент путем использования соответствующих команд.
Для задания текущего цвета вершины используются команды
void glColor[3 4][b s i f](GLtype components) void glColor[3 4][b s i f]v(GLtype components) |
Первые три параметра задают R, G, B компоненты цвета, а последний параметр определяет alpha-компоненту, которая задает уровень прозрачности объекта. Если в названии команды указан тип ‘f’ (float), то значения всех параметров должны принадлежать отрезку [0,1], при этом по умолчанию значение alpha-компоненты устанавливается равным 1.0, что соответствует полной непрозрачности. Если указан тип ‘ub’ (unsigned byte), то значения должны лежать в отрезке [0,255].
Разным вершинам можно назначать различные цвета, и тогда будет проводиться линейная интерполяция цветов по поверхности примитива.
Для управления режимом интерполяции цветов используется команда
void glShadeModel(GLenum mode)
|
вызов которой с параметром GL_SMOOTH включает интерполяцию (установка по умолчанию), а с GL_FLAT – отключает.
Аналогичным образом можно определить нормаль в вершине, используя команды
void glNormal3[b s i f d](type coords) void glNormal3[b s i f d]v(type coords) |
Задаваемый вектор может не иметь единичной длины, но он будет нормироваться автоматически в режиме нормализации, который включается вызовом команды glEnable(GL_NORMALIZE). Команды
void glEnable(GLenum mode) void glDisable(GLenum mode) |
производят включение и отключение того или иного режима работы конвейера OpenGL. Эти команды применяются достаточно часто, и их влияние будет рассматриваться в конкретных случаях.
ПРИМЕЧАНИЕ Включение режима GL_NORMALIZE негативно влияет на скорость работы механизма визуализации OpenGL. В силу этого более предпочтительным является задание заранее приведенных к единичной длине нормалей. Нормализация нормалей необходима для правильного расчета освещения. Режим автоматической нормализации должен быть включен, если приложением используется преобразования растяжения/сжатия, поскольку в этом случае длина нормалей изменяется при умножении на видовую матрицу. |
Мы рассмотрели задание атрибутов одной вершины. Однако чтобы задать какую-нибудь фигуру, одних координат вершин недостаточно, и эти вершины надо объединить в одно целое, определив необходимые свойства. Для этого в OpenGL используется понятие примитивов, к которым относятся точки, линии, связанные или замкнутые линии, треугольники и так далее. Задание примитива происходит внутри командных скобок:
void glBegin(GLenum mode); void glEnd(void); |
Параметр mode определяет тип примитива, который задается внутри и может принимать следующие значения:
GL_POINTS | каждая вершина задает координаты некоторой точки. |
GL_LINES | каждая отдельная пара вершин определяет отрезок; если задано нечетное число вершин, то последняя вершина игнорируется. |
GL_LINE_STRIP | каждая следующая вершина задает отрезок вместе с предыдущей. |
GL_LINE_LOOP | отличие от предыдущего примитива только в том, что последний отрезок определяется последней и первой вершиной, образуя замкнутую ломаную. |
GL_TRIANGLES | каждая отдельная тройка вершин определяет треугольник; если задано не кратное трем число вершин, то последние вершины игнорируются. |
GL_TRIANGLE_STRIP | каждая следующая вершина задает треугольник вместе с двумя предыдущими. |
GL_TRIANGLE_FAN | треугольники задаются первой и каждой следующей парой вершин (пары не пересекаются). |
GL_QUADS | каждая отдельная четверка вершин определяет четырехугольник; если задано не кратное четырем число вершин, то последние вершины игнорируются. |
GL_QUAD_STRIP | четырехугольник с номером n определяется вершинами с номерами 2n-1, 2n, 2n+2, 2n+1. |
GL_POLYGON | последовательно задаются вершины выпуклого многоугольника. |
ПРИМЕЧАНИЕ Использование GL_TRIANGLE_STRIP и GL_TRIANGLE_FAN позволяет повысить производительность приложения. |
Например, чтобы нарисовать треугольник с разными цветами в вершинах, достаточно написать:
GLfloat BlueCol[3] = {0,0,1}; glBegin(GL_TRIANGLE); glColor3f(1.0, 0.0, 0.0); /* красный */ glVertex3f(0.0, 0.0, 0.0); glColor3ub(0, 255, 0); /* зеленый */ glVertex3f(1.0, 0.0, 0.0); glColor3fv(BlueCol); /* синий */ glVertex3f(1.0, 1.0, 0.0); glEnd(); |
Кроме задания самих примитивов можно определить метод их отображения на экране (под примитивами в данном случае понимаются многоугольники).
Однако сначала надо определить понятие лицевых и обратных граней.
Под гранью понимается одна из сторон многоугольника, и по умолчанию лицевой считается та сторона, вершины которой обходятся против часовой стрелки. Направление обхода вершин лицевых сторон можно изменить вызовом команды
void glFrontFace(GLenum mode)
|
со значением параметра mode равным GL_CW, а отменить - с GL_CCW.
Чтобы изменить метод отображения многоугольника используется команда
void glPolygonMode(GLenum face, Glenum mode)
|
Параметр mode определяет, как будут отображаться многоугольники, а параметр face устанавливает тип многоугольников, к которым будет применяться эта команда и может принимать следующие значения:
GL_FRONT | для лицевых граней |
GL_BACK | для обратных граней |
GL_FRONT_AND_BACK | для всех граней |
Параметр mode может быть равен:
GL_POINT | при таком режиме будут отображаться только вершины многоугольников. |
GL_LINE | при таком режиме многоугольник будет представляться набором отрезков. |
GL_FILL | при таком режиме многоугольники будут закрашиваться текущим цветом с учетом освещения, и этот режим установлен по умолчанию. |
Кроме того, можно указывать, какой тип граней отображать на экране. Для этого сначала надо установить соответствующий режим вызовом команды glEnable(GL_CULL_FACE), а затем выбрать тип отображаемых граней с помощью команды
void glCullFace(GLenum mode)
|
Вызов с параметром GL_FRONT приводит к удалению из изображения всех лицевых граней, а с параметром GL_BACK - обратных (установка по умолчанию).
Кроме рассмотренных стандартных примитивов в библиотеках GLU и GLUT описаны более сложные фигуры, такие как сфера, цилиндр, диск (в GLU) и сфера, куб, конус, тор, тетраэдр, додекаэдр, икосаэдр, октаэдр и чайник (в GLUT). Автоматическое наложение текстуры предусмотрено только для фигур из библиотеки GLU (создание текстур в OpenGL будет рассматриваться ниже).
Например, чтобы нарисовать сферу или цилиндр, надо сначала создать объект специального типа GLUquadricObj с помощью команды
GLUquadricObj* gluNewQuadric(void);
|
а затем вызвать соответствующую команду:
void gluSphere(GLUquadricObj * qobj, GLdouble radius, GLint slices, GLint stacks) void gluCylinder(GLUquadricObj * qobj,GLdouble baseRadius,GLdouble topRadius, GLdouble height, GLint slices, GLint stacks) |
где параметр slices задает число разбиений вокруг оси z, а stacks – вдоль оси z.
Более подробную информацию об этих и других командах построения примитивов можно найти в приложении.
ПРИМЕЧАНИЕ Для корректного построения перечисленных примитивов необходимо удалять невидимые линии и поверхности, для чего надо включить соответствующий режим вызовом команды glEnable(GL_DEPTH_TEST). |
Если вершин много, то чтобы не вызывать для каждой команду glVertex..(), удобно объединять вершины в массивы, используя команду
void glVertexPointer(GLint size, GLenum type, GLsizei stride, void *ptr) |
которая определяет способ хранения и координаты вершин. При этом size определяет число координат вершины (может быть равен 2, 3, 4), type определяет тип данных (может быть равен GL_SHORT, GL_INT, GL_FLOAT, GL_DOUBLE). Иногда удобно хранить в одном массиве другие атрибуты вершины, и тогда параметр stride задает смещение от координат одной вершины до координат следующей; если stride равен нулю, это значит, что координаты расположены последовательно. В параметре ptr указывается адрес, где находятся данные.
Аналогично можно определить массив нормалей, цветов и некоторых других атрибутов вершины, используя команды
void NormalPointer(GLenum type, GLsizei stride, void *pointer ) void ColorPointer(GLint size, GLenum type, GLsizei stride, void *pointer ) |
Для того чтобы эти массивы можно было использовать в дальнейшем, надо вызвать команду
void glEnableClientState(GLenum array)
|
с параметрами GL_VERTEX_ARRAY, GL_NORMAL_ARRAY, GL_COLOR_ARRAY соответственно. После окончания работы с массивом желательно вызвать команду
void glDisableClientState(GLenum array)
|
с соответствующим значением параметра array.
Для отображения содержимого массивов используется команда
void glArrayElement(GLint index)
|
которая передает OpenGL атрибуты вершины, используя элементы массива с номером index. Это аналогично последовательному применению команд вида glColor..(…), glNormal..(…), glVertex..(…) c соответствующими параметрами. Однако вместо нее обычно вызывается команда
void glDrawArrays(GLenum mode, GLint first, GLsizei count)
|
рисующая count примитивов, определяемых параметром mode, используя элементы из массивов с индексами от first до first+count-1. Это эквивалентно вызову команды glArrayElement() с соответствующими индексами.
В случае если одна вершина входит в несколько примитивов, то вместо дублирования ее координат в массиве удобно использовать ее индекс.
Для этого надо вызвать команду
void glDrawArrays(GLenum mode, GLsizei count, GLenum type, void *indices) |
где indices – это массив номеров вершин, которые надо использовать для построения примитивов, type определяет тип элементов этого массива: GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, GL_UNSIGNED_INT, а count задает их количество.
ПРИМЕЧАНИЕ Использование массивов вершин позволяет повысить скорость визуализации трехмерной сцены |
Если нужно несколько раз обращаться к одной и той же группе команд, эти команды можно объединить в так называемый список изображений (display list) и вызывать его при необходимости. Для того чтобы создать новый список изображений надо поместить все команды, которые должны в него войти между командными скобками:
void glNewList(GLuint list, GLenum mode) void glEndList() |
Для различения списков используются целые положительные числа, задаваемые при создании списка значением параметра list, а параметр mode определяет режим обработки команд, входящих в список:
GL_COMPILE | команды записываются в список без выполнения |
GL_COMPILE_AND_EXECUTE | команды сначала выполняются, а затем записываются в список |
После того, как список создан, его можно вызвать командой
void glCallList(GLuint list)
|
указав в параметре list идентификатор нужного списка. Чтобы вызвать сразу несколько списков, можно воспользоваться командой
void glCallLists(GLsizei n, GLenum type, const GLvoid *lists) |
вызывающей n списков с идентификаторами из массива lists, тип элементов которого указывается в параметре type. Это могут быть типы GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_INT, GL_UNSIGNED_INT и некоторые другие. Для удаления списков используется команда
void glDeleteLists(GLint list, GLsizei range)
|
которая удаляет списки с идентификаторами ID из диапазона list <= ID <= list+range-1.
В OpenGL используются как основные три системы координат: левосторонняя, правосторонняя и оконная. Первые две системы являются трехмерными и отличаются друг от друга направлением оси z: в правосторонней она направлена на наблюдателя, а в левосторонней – в глубь экрана. Расположение осей x и y аналогично описанному выше. Левосторонняя система используется для задания значений параметрам команды gluPerspective(), glOrtho(), которые будут рассмотрены ниже, а правосторонняя или мировая система координат во всех остальных случаях. Отображение трехмерной информации происходит в двумерную оконную систему координат.
Для задания различных преобразований объектов сцены в OpenGL используются операции над матрицами, при этом различают три типа матриц: видовая, проекций и текстуры. Все они имеют размер 4x4. Видовая матрица определяет преобразования объекта в мировых координатах, такие как параллельный перенос, изменение масштаба и поворот. Матрица проекций задает, как будут проецироваться трехмерные объекты на плоскость экрана (в оконные координаты), а матрица текстуры определяет наложение текстуры на объект.
Для того чтобы выбрать, какую матрицу надо изменить, используется команда
void glMatrixMode(GLenum mode)
|
вызов которой со значением параметра mode равным GL_MODELVIEW, GL_PROJECTION, GL_TEXTURE включает режим работы с видовой, проекций и матрицей текстуры соответственно. Для вызова команд, задающих матрицы того или иного типа необходимо сначала установить соответствующий режим.
Для определения элементов матрицы текущего типа вызывается команда
void glLoadMatrix[f d](GLtype *m)
|
где m указывает на массив из 16 элементов типа float или double в соответствии с названием команды, при этом сначала в нем должен быть записан первый столбец матрицы, затем второй, третий и четвертый.
Команда
void glLoadIdentity(void) |
заменяет текущую матрицу на единичную. Часто нужно сохранить содержимое текущей матрицы для дальнейшего использования, для чего используют команды
void glPushMatrix(void) void glPopMatrix(void) |
Они записывают и восстанавливают текущую матрицу из стека, причем для каждого типа матриц стек свой. Для видовых матриц его глубина равна как минимум 32, а для двух оставшихся типов как минимум 2.
Для умножения текущей матрицы слева на другую матрицу используется команда
void glMultMatrix[f d](GLtype *m)
|
где m должен задавать матрицу размером 4x4 в виде массива с описанным расположением данных. Однако обычно для изменения матрицы того или иного типа удобно использовать специальные команды, которые по значениям своих параметров создают нужную матрицу и перемножают ее с текущей. Чтобы сделать текущей созданную матрицу, надо перед вызовом этой команды вызвать glLoadIdentity().
В целом, для отображения трехмерных объектов сцены в окно приложения используется следующая последовательность действий:
К видовым преобразованиям будем относить перенос, поворот и изменение масштаба вдоль координатных осей. Для проведения этих операций достаточно умножить на соответствующую матрицу каждую вершину объекта и получить измененные координаты этой вершины:
(x’, y’, z’, 1) T = M * (x, y, z, 1)Tгде M – матрица видового преобразования. Перспективное преобразование и проектирование производится аналогично. Сама матрица может быть создана с помощью следующих команд:
void glTranslate[f d](GLtype x, GLtype y, GLtype z) void glRotate[f d](GLtype angle, GLtype x, GLtype y, GLtype z) void glScale[f d](GLtype x, GLtype y, GLtype z) |
glTranlsate..() производит перенос объекта, прибавляя к координатам его вершин значения своих параметров.
glRotate..() производит поворот объекта против часовой стрелки на угол angle (измеряется в градусах) вокруг вектора (x,y,z).
glScale..() производит масштабирование объекта (сжатие или растяжение), домножая соответствующие координаты его вершин на значения своих параметров.
Все эти преобразования будут применяться к примитивам, описания которых будут находиться ниже в программе. В случае если надо, например, повернуть один объект сцены, а другой оставить неподвижным, удобно сначала сохранить текущую видовую матрицу в стеке командой glPushMatrix(), затем вызвать glRotate..() с нужными параметрами, описать примитивы, из которых состоит этот объект, а затем восстановить текущую матрицу командой glPopMatrix().
Кроме изменения положения самого объекта иногда бывает нужно изменить положение точки наблюдения, что, однако также приводит к изменению видовой матрицы. Это можно сделать с помощью команды
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) задает положительное направление оси у, определяя поворот камеры. Если, например, камеру не надо поворачивать, то задается значение (0,1,0), а со значением (0,-1,0) сцена будет перевернута.
Фактически, эта команда совершает перенос и поворот объектов сцены, но в таком виде задавать параметры бывает удобнее.
В OpenGL существуют ортографическая (параллельная) и перспективная проекция. Первый тип проекции может быть задан командами
void glOrtho(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble near, GLdouble far) void gluOrtho2D(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top) |
Первая команда создает матрицу проекции в усеченный объем видимости (параллелограмм видимости) в левосторонней системе координат. Параметры команды задают точки (left, bottom, -near) и (right, top, -near), которые отвечают левому нижнему и правому верхнему углам окна вывода. Параметры near и far задают расстояние до ближней и дальней плоскостей отсечения по дальности от точки (0,0,0) и могут быть отрицательными.
Во второй команде, в отличие от первой, значения near и far устанавливаются равными –1 и 1 соответственно.
Перспективная проекция определяется командой
void gluPerspective(GLdouble angley, GLdouble aspect, GLdouble znear, GLdouble zfar)
|
которая задает усеченный конус видимости в левосторонней системе координат. Параметр angley определяет угол видимости в градусах по оси у и должен находиться в диапазоне от 0 до 180. Угол видимости вдоль оси x задается параметром aspect, который обычно задается как отношение сторон области вывода. Параметры zfar и znear задают расстояние от наблюдателя до плоскостей отсечения по глубине и должны быть положительными. Чем больше отношение zfar/znear, тем хуже в буфере глубины будут различаться расположенные рядом поверхности, так как по умолчанию в него будет записываться ‘сжатая’ глубина в диапазоне от 0 до 1 (см. следующий пункт).
void glViewPort(GLint x, GLint y, GLint width, GLint height)
|
Значения всех параметров задаются в пикселях и определяют ширину и высоту области вывода с координатами левого нижнего угла (x,y) в оконной системе координат. Размеры оконной системы координат определяются текущими размерами окна приложения, точка (0,0) находится в левом нижнем углу окна.
Используя параметры команды glViewPort(), вычисляются оконные координаты центра области вывода (ox,oy) по формулам ox=x+width/2, oy=y+height/2. Пусть px=width, py=height, тогда можно найти оконные координаты каждой вершины: (xw, yw, zw)T = ( (px/2) xn+ ox , (py/2) yn+ oy , [(f-n)/2] zn+(n+f)/2 )T При этом целые положительные величины n и f задают минимальную и максимальную глубину точки в окне и по умолчанию равны 0 и 1 соответственно. Глубина каждой точки записывается в специальный буфер глубины (z-буфер), который используется для удаления невидимых линий и поверхностей. Установить значения n и f можно вызовом функцииvoid glDepthRange(GLclampd n, GLclampd f)
|
Команда glViewPort() обычно используется в функции, зарегистрированной с помощью команды glutReshapeFunc(), которая вызывается, если пользователь изменяет размеры окна приложения, изменяя соответствующим образом область вывода.
Для создания реалистических изображений необходимо определить как свойства самого объекта, так и свойства среды, в которой он находится. Первая группа свойств включает в себя параметры материла, из которого сделан объект, способы нанесения текстуры на его поверхность, степень прозрачности объекта. Ко второй группе можно отнести количество и свойства источников света, уровень прозрачности среды, а также модель источников света. Все эти свойства можно задавать, используя соответствующие команды OpenGL.
Для задания параметров текущего материала используются команды
void glMaterial[i f](GLenum face, GLenum pname, GLtype param) void glMaterial[i f]v(GLenum face, GLenum pname, GLtype *params) |
С их помощью можно определить рассеянный, диффузный и зеркальный цвета материала, а также цвет степень зеркального отражения и интенсивность излучения света, если объект должен светиться. Какой именно параметр будет определяться значением param, зависит от значения pname:
GL_AMBIENT | параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют рассеянный цвет материала (цвет материала в тени). Значение по умолчанию: (0.2, 0.2, 0.2, 1.0). |
GL_DIFFUSE | параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет диффузного отражения материала. Значение по умолчанию: (0.8, 0.8, 0.8, 1.0). |
GL_SPECULAR | параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет зеркального отражения материала. Значение по умолчанию: (0.0, 0.0, 0.0, 1.0). |
GL_SHININESS | параметр params должен содержать одно целое или вещественное значение в диапазоне от 0 до 128, которое определяет степень зеркального отражения материала. Значение по умолчанию: 0. |
GL_EMISSION | параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют интенсивность излучаемого света материала. Значение по умолчанию: (0.0, 0.0, 0.0, 1.0). |
GL_AMBIENT_AND_DIFFUSE | эквивалентно двум вызовам команды glMaterial..() со значением pname GL_AMBIENT и GL_DIFFUSE и одинаковыми значениями params. |
Из этого следует, что вызов команды glMaterial[i f]() возможен только для установки степени зеркального отражения материала. В большинстве моделей учитывается диффузный и зеркальный отраженный свет; первый определяет естественный цвет объекта, а второй – размер и форму бликов на его поверхности.
Параметр face определяет тип граней, для которых задается этот материал и может принимать значения GL_FRONT, GL_BACK или GL_FRONT_AND_BACK.
Если в сцене материалы объектов различаются лишь одним параметром, рекомендуется сначала установить нужный режим, вызвав glEnable() c параметром GL_COLOR_MATERIAL, а затем использовать команду
void glColorMaterial(GLenum face, GLenum pname)
|
где параметр face имеет аналогичный смысл, а параметр pname может принимать все перечисленные значения. После этого, значения выбранного с помощью pname свойства материала для конкретного объекта (или вершины) устанавливается вызовом команды glColor..(), что позволяет избежать вызовов более ресурсоемкой команды glMaterial..() и повышает эффективность программы.
Пример.
float mat_dif[]={0.8,0.8,0.8}; float mat_amb[]= {0.2,0.2,0.2}; float mat_spec[]={0.6,0.6,0.6}; float shininess=0.7*128; /*...*/ glMaterialfv(GL_FRONT_AND_BACK,GL_AMBIENT,mat_amb); glMaterialfv(GL_FRONT_AND_BACK,GL_DIFFUSE,mat_dif); glMaterialfv(GL_FRONT_AND_BACK,GL_SPECULAR,mat_spec); glMaterialf(GL_FRONT,GL_SHININESS,shininess); |
Добавить в сцену источник света можно с помощью команд
void glLight[i f](GLenum light, GLenum pname, GLfloat param) void glLight[i f](GLenum light, GLenum pname, GLfloat *params) |
Параметр light однозначно определяет источник, и выбирается из набора специальных символических имен вида GL_LIGHTi , где i должно лежать в диапазоне от 0 до GL_MAX_LIGHT, которое не превосходит восьми.
Оставшиеся два параметра имеют аналогичный смысл, что и в команде glMaterial..(). Рассмотрим их назначение (вначале описываются параметры для первой команды, затем для второй):
GL_SPOT_EXPONENT | параметр param должен содержать целое или вещественное число от 0 до 128, задающее распределение интенсивности света. Этот параметр описывает уровень сфокусированности источника света. Значение по умолчанию: 0 (рассеянный свет). |
GL_SPOT_CUTOFF | параметр param должен содержать целое или вещественное число между 0 и 90 или равное 180, которое определяет максимальный угол разброса света. Значение этого параметра есть половина угла в вершине конусовидного светового потока, создаваемого источником. Значение по умолчанию: 180 (рассеянный свет). |
GL_AMBIENT | параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет фонового освещения. Значение по умолчанию: (0.0, 0.0, 0.0, 1.0). |
GL_DIFFUSE | параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет диффузного освещения. Значение по умолчанию: (1.0, 1.0, 1.0, 1.0) для LIGHT0 и (0.0, 0.0, 0.0, 1.0) для остальных. |
GL_SPECULAR | параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет зеркального отражения. Значение по умолчанию: (1.0, 1.0, 1.0, 1.0) для LIGHT0 и (0.0, 0.0, 0.0, 1.0) для остальных. |
GL_POSITION | параметр params должен содержать четыре целых или вещественных, которые определяют положение источника света. Если значение компоненты w равно 0.0, то источник считается бесконечно удаленным и при расчете освещенности учитывается только направление на точку (x,y,z), в противном случае считается, что источник расположен в точке (x,y,z,w). Значение по умолчанию: (0.0, 0.0, 1.0, 0.0). |
GL_SPOT_DIRECTION | параметр params должен содержать четыре целых или вещественных числа, которые определяют направление света. Значение по умолчанию: (0.0, 0.0, -1.0, 1.0). |
При изменении положения источника света следует учитывать следующие факты: если положение задается командой glLight..() перед определением ориентации взгляда (командой glLookAt()), то будет считаться, что источник находится в точке наблюдения. Если положение устанавливается между заданием ориентации и преобразованиями видовой матрицы, то оно фиксируется и не зависит от видовых преобразований. В последнем случае, когда положение задано после ориентации и видовой матрицы, его положение можно менять, устанавливая как новую ориентацию наблюдателя, так и меняя видовую матрицу.
Для использования освещения сначала надо установить соответствующий режим вызовом команды glEnable(GL_LIGHTNING), а затем включить нужный источник командой glEnable(GL_LIGHTn).
ПРИМЕЧАНИЕ Еще раз обратим внимание на то, что при выключенном освещении цвет вершины равен текущему цвету, который задается командами glColor..(). При включенном освещении цвет вершины вычисляется исходя из информации о материале, о нормалях и об источниках света. При выключении освещения визуализация происходит гораздо быстрее, однако в таком случае приложение должно само рассчитывать цвета вершин. |
В OpenGL используется модель освещения, в соответствии с которой цвет точки определяется несколькими факторами: свойствами материала и текстуры, величиной нормали в этой точке, а также положением источника света и наблюдателя. Для корректного расчета освещенности в точке надо использовать единичные нормали, однако команды типа glScale..(), могут изменять длину нормалей. Чтобы это учитывать, используется уже упоминавшийся режим нормализации нормалей, который включается вызовом команды glEnable(GL_NORMALIZE).
Для задания глобальных параметров освещения используются команды
void glLightModel[i f](GLenum pname, GLenum param) void glLightModel[i f]v(GLenum pname, const GLtype *params) |
Аргумент pname определяет, какой параметр модели освещения будет настраиваться и может принимать следующие значения:
GL_LIGHT_MODEL_LOCAL_VIEWER | параметр param должен быть булевским и задает положение наблюдателя. Если он равен FALSE, то направление обзора считается параллельным оси –z, вне зависимости от положения в видовых координатах. Если же он равен TRUE, то наблюдатель находится в начале видовой системы координат. Это может улучшить качество освещения, но усложняет его расчет. Значение по умолчанию: FALSE. |
GL_LIGHT_MODEL_TWO_SIDE | параметр param должен быть булевским и управляет режимом расчета освещенности как для лицевых, так и для обратных граней. Если он равен FALSE, то освещенность рассчитывается только для лицевых граней. Если же он равен TRUE, расчет проводится и для обратных граней. Значение по умолчанию: FALSE. |
GL_LIGHT_MODEL_AMBIENT | параметр params должен содержать четыре целых или вещественных числа, которые определяют цвет фонового освещения даже в случае отсутствия определенных источников света. Значение по умолчанию: (0.2, 0.2, 0.2,1.0). |
Наложение текстуры на поверхность объектов сцены повышает ее реалистичность, однако при этом надо учитывать, что этот процесс требует значительных вычислительных затрат. Под текстурой будем понимать некоторое изображение, которое надо определенным образом нанести на объект. Для этого следует выполнить следующие этапы:
выбрать изображение и преобразовать его к нужному формату
загрузить изображение в память
определить, как текстура будет наноситься на объект и как она будет с ним взаимодействовать.
Для использования текстуры необходимо сначала загрузить в память изображение нужной текстуры и передать его OpenGL.
Считывание графических данных из файла и их преобразование можно проводить вручную. Можно также воспользоваться функцией, входящей в состав библиотеки GLAUX (для ее использования надо дополнительно подключить glaux.lib), которая сама проводит необходимые операции. Это функция
AUX_RGBImageRec* auxDIBImageLoad(const char *file) |
где file – название файла с расширением *.bmp или *.dib. В качестве результата функция возвращает указатель на область памяти, где хранятся преобразованные данные.
Сейчас предположим, что изображение уже загружено и рассмотрим, каким образом его можно передать механизмам OpenGL.
При создании образа текстуры в памяти следует учитывать следующие требования.
Во-первых, размеры текстуры, как по горизонтали, так и по вертикали должны представлять собой степени двойки. Это требование накладывается для компактного размещения текстуры в памяти и способствует ее эффективному использованию. Использовать только текстуры с такими размерами конечно неудобно, поэтому после загрузки их надо преобразовать. Изменение размеров текстуры можно провести с помощью команды
void gluScaleImage(GLenum format, GLint widthin, GL heightin, GLenum typein, const void *datain, GLint widthout, GLint heightout, GLenum typeout, void *dataout) |
В качестве значения параметра format обычно используется значение GL_RGB или GL_RGBA, определяющее формат хранения информации. Параметры widthin, heightin, widhtout, heightout определяют размеры входного и выходного изображений, а с помощью typein и typeout задается тип элементов массивов, расположенных по адресам datain и dataout. Как и обычно, то может быть тип GL_UNSIGNED_BYTE, GL_SHORT, GL_INT и так далее. Результат своей работы функция заносит в область памяти, на которую указывает параметр dataout.
Во-вторых, надо предусмотреть случай, когда объект по размерам значительно меньше наносимой на него текстуры. Чем меньше объект, тем меньше должна быть наносимая на него текстура и поэтому вводится понятие уровней детализации текстуры. Каждый уровень детализации задает некоторое изображение, которое является, как правило, уменьшенной в два раза копией оригинала. Такой подход позволяет улучшить качество нанесения текстуры на объект. Например, для изображения размером 2mx2n можно построить max(m,n)+1 уменьшенных изображений, соответствующих различным уровням детализации.Эти два этапа создания образа текстуры во внутренней памяти OpenGL можно провести с помощью команды
void gluBuild2DMipmaps(GLenum target, GLint components, GLint width, GLint height, GLenum format, GLenum type, const void *data) |
где параметр target должен быть равен GL_TEXTURE_2D, components определяет количество цветовых компонент текстуры и может принимать следующие значения:
GL_LUMINANCE | только красный. (текстура будет монохромной) |
GL_LUMINANCE_ALPHA | красный и alpha. |
GL_RGB | красный, синий, зеленый |
GL_RGBA | все компоненты. |
Параметры width, height, data определяют размеры и расположение текстуры соответственно, а format и type имеют аналогичный смысл, что и в команде gluScaleImage().
После выполнения этой команды текстура копируется во внутреннюю память OpenGL и поэтому память, занимаемую исходным изображением текстуры можно освободить.
В OpenGL допускается использование одномерных текстур, то есть размера 1xN, однако это всегда надо указывать, используя в качестве значения target константу GL_TEXTURE_1D. Существует одномерный аналог рассматриваемой команды - gluBuild1DMipmaps(), который отличается от двумерного отсутствием параметра height.
При использовании в сцене нескольких текстур, в OpenGL применяется подход, напоминающий создание списков изображений. Вначале, с помощью команды
void glGenTextures(GLsizei n, GLuint *textures)
|
надо создать n идентификаторов для используемых текстур, которые будут записаны в массив textures. Перед началом определения свойств очередной текстуры следует вызвать команду
void glBindTexture(GLenum target, GLuint texture)
|
где target может принимать значения GL_TEXTURE_1D или GL_TEXTURE_2D, а параметр texture должен быть равен идентификатору той текстуры, к которой будут относиться последующие команды. Для того, чтобы в процессе рисования сделать текущей текстуру с некоторым идентификатором, достаточно опять вызвать команду glBindTexture() c соответствующим значением target и texture. Таким образом, команда glBindTexture() включает режим создания текстуры с идентификатором texture, если такая текстура еще не создана, либо режим ее использования, то есть делает эту текстуру текущей.
ПРИМЕЧАНИЕ Так как не всякая аппаратура может оперировать текстурами большого размера, целесообразно ограничить размеры текстуры до 256x256 пикселей. Использование небольших текстур повышает эффективность приложения. |
При наложении текстуры, как уже упоминалось, надо учитывать случай, когда размеры текстуры отличаются от размеров объекта, на который она накладывается. При этом возможно как растяжение, так и сжатие изображения, и то, как будут проводиться эти преобразования может серьезно повлиять на качество построенного изображения. Для определения положения точки на текстуре используется параметрическая система координат (s,t), причем значения s и t находятся в отрезке [0,1]. Для изменения различных параметров текстуры применяются команды:
void glTexParameter[i f](GLenum target, GLenum pname, GLenum param) void glTexParameter[i f]v(GLenum target, GLenum pname, GLenum *params) |
При этом target имеет аналогичный смысл, что и раньше, pname определяет, какое свойство будем менять, а с помощью param или params устанавливается новое значение. Возможные значения pname:
GL_TEXTURE_MIN_FILTER | параметр param определяет функцию, которая будет использоваться для сжатия текстуры. При значении GL_NEAREST будет использоваться один (ближайший), а при значении GL_LINEAR четыре ближайших элемента текстуры. Значение по умолчанию:GL_LINEAR. |
GL_TEXTURE_MAG_FILTER | параметр param определяет функцию, которая будет использоваться для увеличения (растяжения) текстуры. При значении GL_NEAREST будет использоваться один (ближайший), а при значении GL_LINEAR четыре ближайших элемента текстуры. Значение по умолчанию:GL_LINEAR. |
GL_TEXTURE_WRAP_S | параметр param устанавливает значение координаты s, если оно не входит в отрезок [0,1]. При значении GL_ REPEAT целая часть s отбрасывается, и в результате изображение размножается по поверхности. При значении GL_CLAMP используются краевые значения: 0 или 1, что удобно использовать, если на объект накладывается один образ. Значение по умолчанию:GL_REPEAT. |
GL_TEXTURE_WRAP_T | аналогично предыдущему значению, только для координаты t. |
Использование режима GL_NEAREST значительно повышает скорость наложения текстуры, однако при этом снижается качество, так как в отличие от GL_LINEAR интерполяция не производится.
Для того, чтобы определить, как текстура будет взаимодействовать с материалом, из которого сделан объект, используются команды
void glTexEnv[i f](GLenum target, GLenum pname, GLtype param) void glTexEnv[i f]v(GLenum target, GLenum pname, GLtype *params) |
Параметр target должен быть равен GL_TEXTURE_ENV, а в качестве pname рассмотрим только одно значение GL_TEXTURE_ENV_MODE, которое наиболее часто применяется.
Параметр param может быть равен:
GL_MODULATE | конечный цвет находится как произведение цвета точки на поверхности и цвета соответствующей ей точки на текстуре. |
GL_REPLACE | в качестве конечного цвета используется цвет точки на текстуре. |
GL_BLEND | конечный цвет находится как сумма цвета точки на поверхности и цвета соответствующей ей точки на текстуре с учетом их яркости. |
Общий подход к созданию текстур:
/* нужное нам количество текстур */ #define NUM_TEXTURES 10 /* идентификаторы текстур */ int TextureIDs[NUM_TEXTURES]; /* образ текстуры */ AUX_RGBImageRec *pImage; /*...*/ /* 1) получаем идентификаторы текстур */ glGenTextures(NUM_TEXTURES,TextureIDs); /* 2) выбираем текстуру для модификации параметров */ glBindTexture(TextureIDs[i]); /* 0<=i<NUM_TEXTURES*/ /* 3) загружаем текстуру. Размеры текстуры – степень 2 */ pImage=dibImageLoad("texture.bmp"); if (Texture!=NULL) { /* 4) передаем текстуру OpenGL и задаем параметры*/ /* выравнивание по байту */ glPixelStorei(GL_UNPACK_ALIGNMENT,1); gluBuildMipmaps(GL_TEXTURE_2D,GL_RGB, pImage->sizeX, |
Перед нанесением текстуры на объект осталось установить соответствие между точками на поверхности объекта и на самой текстуре. Задавать это соответствие можно двумя методами: отдельно для каждой вершины или сразу для всех вершин, задав параметры специальной функции отображения.
Первый метод реализуется с помощью команд
void glTexCoord[1 2 3 4][s i f d](type coord) void glTexCoord[1 2 3 4][s i f d]v(type *coord) |
Чаще всего используется команды вида glTexCoord2..(type s, type t), задающие текущие координаты текстуры. Вообще, понятие текущих координат текстуры аналогично понятиям текущего цвета и текущей нормали, и является атрибутом вершины. Однако даже для куба нахождение соответствующих координат текстуры является довольно трудоемким занятием, поэтому в библиотеке GLU помимо команд, проводящих построение таких примитивов, как сфера, цилиндр и диск, предусмотрено также наложение на них текстур. Для этого достаточно вызвать команду
void gluQuadricTexture(GLUquadricObj *quadObject, GLboolean textureCoords)
|
с параметром textureCoords равным GL_TRUE, и тогда текущая текстура будет автоматически накладываться на примитив.
Второй метод реализуется с помощью команд
void glTexGen[i f d](GLenum coord, GLenum pname, GLtype param) void glTexGen[i f d]v(GLenum coord, GLenum pname, const GLtype *params) |
Параметр coord определяет для какой координаты задается формула и может принимать значение GL_S,GL_T; pname может быть равен одному из следующих значений:
GL_TEXTURE_GEN_MODE | определяет функцию для наложения текстуры. |
В этом случае аргумент param принимает значения:
GL_OBJECT_LINEAR | значение соответствующей текстурной координаты определяется расстоянием до плоскости, задаваемой с помощью значения pname GL_OBJECT_PLANE (см. ниже). Формула выглядит следующим образом: g=x*xp+y*yp+z*zp+w*wp, где g-соответствующая текстурная координата ( s или p), x, y, z, w – координаты соответствующей точки. xp, yp, zp, wp – коэффициенты уравнения плоскости. В формуле используются координаты объекта. |
GL_EYE_LINEAR | аналогично предыдущему значению, только в формуле используются видовые координаты. Т.е. координаты текстуры объекта в этом случае зависят от положения этого объекта. |
GL_SPHERE_MAP | позволяет эмулировать отражение от поверхности объекта. Текстура как бы "оборачивается" вокруг объекта. Для данного метода используются видовые координаты и необходимо задание нормалей. |
GL_OBJECT_PLANE | позволяет задать плоскость, расстояние до которой будет использоваться при генерации координат, если установлен режим GL_OBJECT_LINEAR. В этом случае параметр params является указателем на массив из четырех коэффициентов уравнения плоскости. |
GL_EYE_PLANE | аналогично предыдущему значению. Позволяет задать плоскость для режима GL_EYE_LINEAR |
Для установки автоматического режима задания текстурных координат необходимо вызвать команду glEnable с параметром GL_TEXTURE_GEN_S или GL_TEXTURE_GEN_P.
Пример:
Рассмотрим, как можно задать зеркальную текстуру. При таком наложении текстуры изображение будет как бы отражаться от поверхности объекта, вызывая интересный оптический эффект. Для этого сначала надо создать два целочисленных массива коэффициентов s_coeffs и t_coeffs со значениями (1,0,0,1) и (0,1,0,1) соответственно, а затем вызвать команды:
glEnable(GL_TEXTURE_GEN_S); glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGendv(GL_S, GL_EYE_PLANE, s_coeffs); |
и такие же команды для координаты t с соответствующими изменениями.
В этой главе будут рассмотрены некоторые возможности OpenGL, применимые к созданию впечатляющих спецэффектов, которые могут значительно повысить реалистичность трехмерной сцены.
Добавление тумана в сцену может повысить реализм, а иногда и скрыть некоторые артефакты, которые появляются, когда в сцене присутствуют отдаленные объекты.
Туман в OpenGL реализуется путем изменения цвета объектов в сцене в зависимости от их глубины (расстояния от точки наблюдения).
Для включения тумана необходимо вызвать команду glEnable(GL_FOG).
Способ вычисления интенсивности тумана можно определить с помощью команд
void glFog[if](enum pname, T param) ; |
Аргумент pname может принимать следующие значения:
GL_FOG_MODE | в этом случае аргумент param определяет формулу, по которой будет вычисляться интенсивность тумана в точке. |
param может принимать значения
GL_EXP | Интенсивность вычисляется по формуле f=exp(-d*z) |
GL_EXP2 | Интенсивность вычисляется по формуле f=exp(-(d*z)2) |
GL_LINEAR | Интенсивность вычисляется по формуле f=e-z/e-s |
В этих формулах z обозначает расстояние от точки, в которой вычисляется интенсивность тумана, до точки наблюдения.
Коэффициенты d,e,s задаются с помощью следующих значений аргумента pname
GL_FOG_DENSITY | param определяет коээфициент d |
GL_FOG_START | param определяет коэффициент s |
GL_FOG_END | param определяет коэффициент e |
Цвет тумана задается с помощью аргумента pname, равного
GL_FOG_COLOR | в этом случае params – указатель на массив из 4х компонент цвета. |
Пример:
Glfloat FogColor[4]={0.5,0.5,0.5,1}; glEnable(GL_FOG); glFogi(GL_FOG_MODE,GL_LINEAR); glFogf(GL_FOG_START,20.0); glFogf(GL_FOG_END,100.0); glFogfv(GL_FOG_COLOR,FogColor); |
Прозрачность позволяет использовать полупрозрачные объекты в сцене, что может значительно повысить реалистичность изображения.
В OpenGL прозрачность реализуется с помощью специального режима смешения цветов (blending). Алгоритм смешения комбинирует цвета входящих пикселей (RGBA) с цветами соответствующих пикселей, уже хранящихся в буфере кадра.
Режим включается с помощью команды glEnable(GL_BLEND).
Определить параметры смешения можно с помощью команды:
void glBlendFunc(enum src,enum dst) |
Приведем наиболее часто используемые значения агрументов src и dst.
GL_SRC_ALPHA | k=(As,As,As,As) |
GL_SRC_ONE_MINUS_ALPHA | k=(1,1,1,1)-(As,As,As,As) |
GL_DST_COLOR | k=(Rd,Gd,Bd) |
GL_ONE_MINUS_DST_COLOR | k=(1,1,1,1)- (Rd,Gd,Bd) |
GL_DST_ALPHA | k=(Ad,Ad,Ad,Ad) |
GL_DST_ONE_MINUS_ALPHA | k=(1,1,1,1)-(Ad,Ad,Ad,Ad) |
GL_SRC_COLOR | k=(Rs,Gs,Bs) |
GL_ONE_MINUS_SRC_COLOR | k=(1,1,1,1)- (Rs,Gs,Bs) |
Пример:
Предположим, мы хотим реализовать вывод прозрачных объектов. Коэффициент прозрачности задается alpha-компонентой цвета. alpha, равное 1 – непрозрачный объект; равное 0 – невидимый. Для реализации служит следующий код:
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA,GL_SRC_ONE_MINUS_ALPHA); |
ПРИМЕЧАНИЕ В случае наличия в сцене нескольких прозрачных объектов, которые могут перекрывать друг друга, корректный вывод можно гарантировать только в случае выполнения следующих условий: Все прозрачные объекты выводятся после непрозрачных. При выводе объекты с прозрачностью должны быть упорядочены по уменьшению глубины, т.е. выводиться начиная с наиболее отдаленных. Как уже говорилось, в OpenGL команды обрабатываются в порядке их поступления, поэтому для реализации перечисленных требований достаточно расставить в соответствующем порядке вызовы команд glVertex..(). |
Буфер накопления (accumulation buffer) – это дополнительный внутренний буфер OpenGL. В нем можно сохранять визуализированное изображение, применяя при этом попиксельно специальные операции.
Изображение берется из буфера, выбранного на чтение командой
void glReadBuffer(enum buf) |
Аргумент buf определяет буфер для чтения. Значения buf, равные GL_BACK, GL_FRONT, определяют соответствующие буфера для чтения.
Применяя различные операции, описанные ниже, можно как бы понемногу накапливать изображение в буфере.
Затем картинка переносится из буфера накопления в буфер, выбранный на запись командой
void glDrawBuffer(enum buf) |
Значение buf аналогично значению соответствующего аргумента в команде glReadBuffer.
Все операции с буфером накопления контролируются командой
void glAccum(enum op, GLfloat value) |
Аргумент op задает операцию над пикселями и может принимать следующие значения:
GL_LOAD | Пиксель выбирается из буфера, выбранного на чтение, его значение умножается на value и заносится в буфер накопления. |
GL_ACCUM | Аналогично предыдущему, но полученное после умножения значение складывается с уже имеющимся в буфере. |
GL_MULT | Эта операция умножает значение каждого пикселя в буфере накопления на value . |
GL_ADD | Аналогично предыдущему, только вместо умножения используется сложение. |
GL_RETURN | Изображение переносится из буфера накопления в буфер, выбранный для записи. Перед этим значение каждого пикселя умножается на value. |
Для использования буфера накопления нет необходимости вызывать какие-либо команды glEnable. Достаточно только иметь сам буфер.
В качестве примера использования буфера накопления рассмотрим задачу устранения лестничного эффекта (antialiasing).
Алгоритм ее решения сразу для всей сцены таков:
Для каждого кадра выводим сцену несколько раз, каждый раз немного смещая камеру относительно начального положения (положения камер, например, могут образовывать окружность). Все сцены сохраняем в буфере накопления с коэффициентом 1/n, где n – число сцен для каждого кадра. Чем больше таких сцен (antialiasing samples) – тем хуже производительность, но лучше качество.
for(i=0;i<samples_count;++i) /* обычно samples_count лежит в пределах от 5 до 10 */ { ShiftCamera(i); /* сдвигаем камеру */ RenderScene(); if (i==0) /* на первой итерации загружаем изображение */ glAccum(GL_LOAD,1/(float)samples_count); else /* добавляем к уже существующему */ glAccum(GL_ADD,1/(float)samples_count); } /* Пишем то, что получилось, назад в исходный буфер */ glAccum(GL_RETURN,1.0); |
ПРИМЕЧАНИЕ Буфер накопления редко реализуется аппаратно. Поэтому использование устранения ступенчатости сразу для всей сцены практически несовместимо с визуализацией динамических изображений с приемлемой частотой вывода кадров (frame rate). |
При выводе пикселей в буфер кадра иногда возникает необходимость выводить не все пиксели, а только некоторое их подмножество, т.е. как бы наложить трафарет на изображение. Для этого OpenGL предоставляет так называемый трафаретный буфер (stencil buffer). Кроме наложения трафарета, этот буфер предоставляет еще несколько интересных возможностей.
Прежде чем поместить пиксель в буфер кадра, механизм визуализации OpenGL позволяет выполнить сравнение (тест) между заданным значением и значением в трафаретном буфере. Если тест проходит, пиксель визуализируется в буфере кадра.
Механизм сравнения контролируется следующими командами:
void glStencilFunc(enum func, int ref, uint mask) void glStencilOp(enum sfail, enum dpfail, enum dppass) |
С помощью аргумента func задается функция сравнения. Он может принимать следующие значения:
GL_NEVER | тест никогда не проходит, т.е всегда возвращает false |
GL_ALWAYS | тест проходит всегда. |
GL_LESS, GL_LEQUAL, GL_EQUAL, GL_GEQUAL, GL_GREATE, GL_NOTEQUAL | тест проходит в случае, если ref соответственно меньше значения в трафаретном буфере, меньше либо равен, равен, больше, больше либо равен или не равен. |
Аргумент mask задает маску для значений. Т.е. в итоге для трафаретного теста получаем следующую формулу: ((ref AND mask) op (svalue AND mask))
Команда StencilOp предназначена для определения действий над пикселем трафаретного буфера в случае положительного или отрицательного результата теста.
Аргумент sfail задает действие в случае отрицательного результата теста, и может принимать следующие значения:
GL_KEEP, GL_ZERO, GL_REPLACE, GL_INCR, GL_DECR, GL_INVERT | соответственно сохраняет значение в трафаретном буфере, обнуляет его, заменяет на заданное значение (ref), увеличивает, уменьшает или побитово инвертирует. |
Аргументы dpfail определяют действия в случае отрицательного результата теста на глубину в z-буфере. dppass задает действие в случае положительного результата этого теста. Аргументы принимают те же значения, что и аргумент sfail. По умолчанию все три параметра установлены на GL_KEEP.
Для включения трафаретного теста необходимо выполнить команду glEnable(GL_STENCIL_TEST);
Трафаретный тест используется при создании таких спецэффектов, как тени, отражения, плавные переходы из одной картинки в другую, создания конструктивной геометрии (CSG) и др.
Пример использования трафаретного теста при создании теней описан в Приложении.
Приложение содержит информацию в основном прикладного характера, которая может понадобиться при изучении описанного выше материала и написании прикладных программ.
Рассмотрим стандартные команды построения примитивов, которые реализованы в библиотеках GLU и GLUT.
Как уже было сказано, чтобы построить примитив из библиотеки GLU, надо сначала создать указатель на quadric-объект с помощью команды gluNewQuadric(), а затем вызвать одну из команд gluSphere(), gluCylinder(), gluDisk(), gluPartialDisk(). Рассмотрим эти команды отдельно:
void gluSphere(GLUquadricObj *qobj, GLdouble radius, GLint slices, GLint stacks)
|
Строит сферу с центром в начале координат и радиусом radius. При этом число разбиений сферы вокруг оси z задается параметром slices, а вдоль оси z – параметром stacks.
void gluCylinder(GLUquadricObj *qobj,GLdouble baseRadius,GLdouble topRadius, GLdouble height, GLint slices, GLint stacks)
|
Строит цилиндр без оснований (то есть кольцо), продольная ось параллельна оси z, заднее основание имеет радиус baseRadius, и расположено в плоскости z=0, переднее основание имеет радиус topRadius и расположено в плоскости z= height. Если задать один из радиусов равным нулю, то будет построен конус.
Параметры slices и stacks имеют аналогичный смысл, что и в предыдущей команде.
void gluDisk(GLUquadricObj *qobj, GLdouble innerRadius, GLdouble outerRadius, GLint slices, GLint loops)
|
Строит плоский диск (то есть круг) с центром в начале координат и радиусом outerRadius. При этом если значение innerRadius ненулевое, то в центре диска будет находиться отверстие радиусом innerRadius. Параметр slices задает число разбиений диска вокруг оси z, а параметр loops –число концентрических колец, перпендикулярных оси z.
void gluPartialDisk(GLUquadricObj *qobj, GLdouble innerRadius, GLdouble outerRadius, GLint slices, GLint loops, GLdouble startAngle, GLdouble sweepAngle);
|
Отличие этой команды от предыдущей заключается в том, что она строит сектор круга, начальный и конечный углы которого отсчитываются против часовой стрелки от положительного направления оси y и задаются параметрами startAngle и sweepAngle. Углы измеряются в градусах.
Команды, проводящие построение примитивов из библиотеки GLUT, реализованы через стандартные примитивы OpenGL и GLU. Для построения нужного примитива достаточно произвести вызов соответствующей команды.
void glutSolidSphere(GLdouble radius, GLint slices, GLint stacks) void glutWireSphere(GLdouble radius, GLint slices, GLint stacks) |
Команда glutSolidSphere() строит сферу, а glutWireSphere()-каркас сферы радиусом radius. Остальные параметры имеют аналогичный смысл, что и в предыдущих командах.
void glutSolidCube(GLdouble size) void glutWireCube(GLdouble size) |
Эти команды строят куб или каркас куба с центром в начале координат и длиной ребра size.
void glutSolidCone(GLdouble base, GLdouble height, GLint slices, GLint stacks) void glutWireCone(GLdouble base, GLdouble height, GLint slices, GLint stacks) |
Эти команды строят конус или его каркас высотой height и радиусом основания base, расположенный вдоль оси z. Основание находится в плоскости z=0. Остальные параметры имеют аналогичный смысл, что и в предыдущих командах.
void glutSolidTorus(GLdouble innerRadius, GLdouble outerRadius, GLint nsides, GLint rings) void glutWireTorus(GLdouble innerRadius, GLdouble outerRadius, GLint nsides, GLint rings) |
Эти команды строят тор или его каркас в плоскости z=0. Внутренний и внешний радиусы задаются параметрами innerRadius, outerRadius. Параметр nsides задает число сторон в кольцах, составляющих ортогональное сечение тора, а rings-число радиальных разбиений тора.
void glutSolidTetrahedron(void) void glutWireTetrahedron(void) |
Эти команды строят тетраэдр (правильную треугольную пирамиду) или его каркас, при этом радиус описанной сферы вокруг него равен 1.
void glutSolidOctahedron(void) void glutWireOctahedron(void) |
Эти команды строят октаэдр или его каркас, радиус описанной вокруг него сферы равен 1.
void glutSolidDodecahedron(void) void glutWireDodecahedron(void) |
Эти команды строят додекаэдр или его каркас, радиус описанной вокруг него сферы равен квадратному корню из трех.
void glutSolidIcosahedron(void) void glutWireIcosahedron(void) |
Эти команды строят икосаэдр или его каркас, радиус описанной вокруг него сферы равен 1.
В OpenGL нет встроенной поддержки построения теней. В значительной степени это объясняется тем, что существует множество алгоритмов их построения. Но присутствие теней очень сильно влияет на повышение реалистичности сцены, поэтому мы рассмотрим один из подходов к их построению.
Большинство алгоритмов, предназначенных для построения теней, используют модифицированные принципы перспективной проекции. Здесь рассматривается один из самых простых методов. С его помощью можно получать тени на плоскости.
Общий подход таков: для всех точек трехмерного объекта ищется их проекция параллельно вектору, соединяющему данную точку и точку, в которой находится источник света, на некую заданную плоскость. Получаем новый объект, целиком лежащий в плоскости, который является тенью исходного.
Рассмотрим математические основы данного метода.
Пусть:
P – точка в трехмерном пространстве, которая отбрасывает тень.
L – положение источника света, который освещает данную точку.
S=a(L-P)-P - точка, в которую отбрасывает тень точка P.
Для нахождения параметра а положим, что тень падает на плоскость z=0. В этом случае a=zp/(zl-zp). Следовательно, xs=(xpzl-zlzp)/(zl-zp), ys=(ypzl-ylzp)/(zl-zp), zs=0Введем однородные координаты:
xs=xs'/ws' ys=ys'/ws' zs=0 ws'=zl-zpОтсюда S может быть получена с использованием умножения матриц следующим образом:
Для того, чтобы алгоритм мог рассчитывать тень, падающую на произвольную плоскость, рассмотрим произвольную точку на линии между S и P, представленную в однородных координатах:
aP+bL, где a и b – скалярные параметры.
Следующая матрица задает плоскость через координаты ее нормали:
Точка, в которой луч, проведенный от источника света через данную точку P, пересекает плоскость G, определяется параметрами a и b, удовлетворяющими следующему уравнению:
(aP+bL)G=0
Отсюда получаем: a(PG)+b(LG)=0. Этому уравнению удовлетворяют
a=(LG), b=-(PG)
Следовательно, искомая точка S=(LG)P-(PG)L. Пользуясь ассоциативностью матричного произведения, получим
S=P[(LG)I-GL], где I – единичная матрица.
Матрица (LG)I-GL (*) используется для получения теней на произвольной плоскости.
Рассмотрим некоторые аспекты практической реализации данного метода с помощью OpenGL.
Предположим, что матрица floorShadow была ранее получена нами из формулы (*). Следующий код с ее помощью строит тени для заданной плоскости:
/* Делаем тени полупрозрачными с использованием смешения цветов(blending) */ glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); glDisable(GL_LIGHTING); glColor4f(0.0, 0.0, 0.0, 0.5); glPushMatrix(); /* Проецирум тень */ glMultMatrixf((GLfloat *) floorShadow); /* Визуализируем сцену в проекции */ RenderGeometry(); glPopMatrix(); glEnable(GL_LIGHTING); glDisable(GL_BLEND); /* Визуализируем сцену в обычном режиме */ RenderGeometry(); |
Матрица floorShadow может быть получена из уравнения (*) с помощью следующей функции:
/* args: plane – plane equation lightpos – light positions return: matrix – resulting matrix */ void shadowmatrix(GLfloat matrix[4][4], GLfloat plane[4], GLfloat lightpos[4]) { GLfloat dot; dot = plane[0] * lightpos[0] + plane[1] * lightpos[1] + plane[2] * lightpos[2] + plane[3] * lightpos[3]; matrix[0][0] = dot - lightpos[0] * plane[0]; matrix[1][0] = 0.f - lightpos[0] * plane[1]; matrix[2][0] = 0.f - lightpos[0] * plane[2]; matrix[3][0] = 0.f - lightpos[0] * plane[3]; matrix[0][1] = 0.f - lightpos[1] * plane[0]; matrix[1][1] = dot - lightpos[1] * plane[1]; matrix[2][1] = 0.f - lightpos[1] * plane[2]; matrix[3][1] = 0.f - lightpos[1] * plane[3]; matrix[0][2] = 0.f - lightpos[2] * plane[0]; matrix[1][2] = 0.f - lightpos[2] * plane[1]; matrix[2][2] = dot - lightpos[2] * plane[2]; matrix[3][2] = 0.f - lightpos[2] * plane[3]; matrix[0][3] = 0.f - lightpos[3] * plane[0]; matrix[1][3] = 0.f - lightpos[3] * plane[1]; matrix[2][3] = 0.f - lightpos[3] * plane[2]; matrix[3][3] = dot - lightpos[3] * plane[3]; } |
Тени, построенные таким образом, имеют ряд недостатков.
Однако имеется решение первой и второй проблемы. Для этого используется трафаретный буфер, работа с которым была описана в предыдущей главе.
Итак, задача – отсечь вывод геометрии (тени, в данном случае) по границе некоторой произвольной области и избежать "двойного смешения". Общий алгоритм решения с использованием трафаретного буфера таков:
Рассмотрим эти этапы более подробно.
1.
/* очищаем трафаретный буфер */ glClearStencil(0x0); /* включаем тест */ glEnable(GL_STENCIL_TEST); |
2.
/* условие всегда выполнено и значение в буфере будет равно 1*/ glStencilFunc(GL_ALWAYS, 0x1, 0xffffffff); /* в любом случае заменяем значение в трафаретном буфере*/ glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE); /* выводим геометрию, по которой затем будет отсечена тень*/ RenderPlane(); |
3.
/* условие выполнено и тест дает истину только если значение в трафаретном буфере равно 1 */ glStencilFunc(GL_EQUAL, 0x1, 0xffffffff); /* значение в буфере равно 2,если тень уже выведена */ glStencilOp(GL_KEEP, GL_KEEP, GL_ADD); /* выводим тени */ RenderShadow(); |
ПРИМЕЧАНИЕ К сожалению, даже при применении трафаретного теста остаются некоторые проблемы, связанные с работой z-буфера. В частности, некоторые участки теней могут стать невидимы. Для решения этой проблемы можно немного приподнять тени над плоскостью c помощью модификации ее уравнения. Описание других методов выходит за рамки данного пособия. |
Вначале необходимо обеспечить наличие файлов glut.h, glut32.lib, glut32.dll в каталогах ..\BorlandC\Include\Gl, ..\BorlandC\Lib, ..\Windows\System соответственно. Также в этих каталогах надо проверить наличие файлов gl.h, glu.h, opengl32.lib, glu32.lib, opengl32.dll, glu32.dll, которые обычно входят в состав BorlandC++ и Windows. При этом надо учитывать, что версии Microsoft файлов opengl32.lib, glu32.lib, glut32.lib для Borland C++ не подходят, и следует использовать только совместимые версии. Чтобы создать такие версии, надо использовать стандартную программу ‘implib’, которая находится в каталоге ..\BorlandC\Bin. Для этого надо выполнить команды вида
implib ..\BorlandC\Lib\filename.lib ..\filename.dll
для перечисленных файлов, которые создают нужный *.lib файл из соответствующего *.dll файла. Кроме того, надо отметить, что компилятор BorlandC не может по неизвестным причинам использовать файл glaux.lib, входящий в состав BorlandC++5.02, при компиляции приложения, использующего библиотеку GLAUX, поэтому возможно от этой библиотеки придется отказаться. Для создания приложения надо выполнить следующие действия:
Перед началом работы необходимо скопировать файлы glut.h, glut32.lib glut32.dll в каталоги ..\MSVC\Include\Gl, ..\MSVC\Lib, ..\Windows\System соответственно. Также в этих каталогах надо проверить наличие файлов gl.h, glu.h, opengl32.lib, glu32.lib, opengl32.dll, glu32.dll, которые обычно входят в состав Visual C++ и Windows. При использовании команд из библиотеки GLAUX к перечисленным файлам надо добавить glaux.h, glaux.lib.
Для создания приложения надо выполнить следующие действия:
Этот простой пример предназначен для демонстрации структуры GLUT-приложения и простейших основ OpenGL. Результатом работы программы является случайный набор цветных прямоугольников, который меняется при нажатии левой кнопки мыши. С помощью правой кнопки мыши можно менять режим заливки прямоугольников.
Пример №1Программа предназначена для демонстрации модели освещения OpenGL. Результатом работы программы является простая сцена, состоящая из тора, конуса и шара. Объектам назначаются разные материалы. В сцене присутствует точечный источник света.
Пример №2Результатом выполнения этой программы является построение тетраэдра с вращающимися вокруг него кольцами, на которые нанесена текстура. В среде MS Visual C++ программа может компилироваться без изменений, а при компиляции в Borland C++ придется закомментировать вызов и описание функции TextureInit(), после чего не будет проводиться наложение текстур. Как было сказано выше, попытка использовать функции из библиотеки GLAUX приводит к сообщению об ошибке при компиляции программы.
При компиляции программы в MS Visual C++ файл ‘texture.bmp’ надо поместить в каталог проекта или указать полный путь к нему, используя символ ‘/’. Если путь не указан, то при запуске исполняемого файла из операционной системы, файл с текстурой должен находиться в том же каталоге.
Пример №3