• СТАТЬЯ Учебное пособие по OpenGL
  • Программирование на Visual C++

    Выпуск №67 от 10 марта 2002 г.

    Здравствуйте, уважаемые подписчики!

    Сегодня я хочу предложить вашему вниманию тему, которая еще ни разу не появлялась на страницах рассылки, хотя без сомнения этого заслуживает. Эта тема – графическая библиотека OpenGl, которая уже долгое время является фактическим стандартом для серьезных 3D приложений.

    К сожалению из-за большого объема статьи ее пришлось разбить на две части. Но вторую часть вы получите сразу же в выпуске 67б, так что вам не придется ждать целую неделю ;-)

    СТАТЬЯ

    Учебное пособие по OpenGL

    Авторы: Фролов Антон

    Игнатенко Алексей

    Источник: Лаборатория компьютерной графики при ВМиК МГУ

    Введение

    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 имеет продуманную структуру и интуитивно понятный интерфейс, что позволяет с меньшими затратами создавать эффективные приложения, содержащие меньше строк кода, чем с использованием других графических библиотек. Необходимые функции для обеспечения совместимости с различным оборудованием реализованы на уровне библиотеки и значительно упрощают разработку приложений.

    Основные возможности

    • Набор базовых примитивов: точки, линии, многоугольники и т.п.

    • Видовые и координатные преобразования

    • Удаление невидимых линий и поверхностей (z-буфер)

    • Использование сплайнов для построения линий и поверхностей

    • Наложение текстуры и применение освещения

    • Добавление специальных эффектов: тумана, изменение прозрачности, смешивание цветов (blending), устранение ступенчатости (anti-aliasing).

    Как уже было сказано, существует реализация OpenGL для разных платформ, для чего было удобно разделить базовые функции графической системы и функции для отображения графической информации и взаимодействия с пользователем. Были созданы библиотеки для отображения информации с помощью оконной подсистемы для операционных систем Windows и Unix (WGL и GLX соответственно), а также библиотеки GLAUX и GLUT, которые используются для создания так называемых консольных приложений.

    Библиотека GLAUX уступает по популярности написанной несколько позже библиотеке GLUT, хотя они предоставляют примерно одинаковые возможности. В дальнейшем в данном пособии в качестве основной будет рассматриваться библиотека GLUT, предоставляющая широкий набор средств взаимодействия с пользователем.

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

    Основы 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 в С\C++), символ f – тип GLfloat (аналог float), символ i – тип GLint (аналог int) и так далее. Полный список типов и их описание можно посмотреть в файле gl.h
    [v] наличие этого символа показывает, что в качестве параметров функции используется указатель на массив значений

    Символы в квадратных скобках в некоторых названиях не используются. Например, команда glVertex2i() описана как базовая в библиотеке OpenGL, и использует в качестве параметров два целых числа, а команда glColor3fv() использует в качестве параметра указатель на массив из трех вещественных чисел.

    Структура консольного приложения

    Будем рассматривать построение консольного приложения при помощи библиотеки GLUT или GL Utility Toolkit, получившей в последнее время широкое распространение. Эта библиотека обеспечивает единый интерфейс для работы с окнами вне зависимости от платформы, поэтому описываемая ниже структура приложения остается неизменной для операционных систем Windows, Linux и многих других.

    Функции GLUT могут быть классифицированы на несколько групп по своему назначению:

    • Инициализация

    • Начало обработки событий

    • Управление окнами

    • Управление меню

    • Регистрация вызываемых (callback) функций

    • Управление индексированной палитрой цветов

    • Отображение шрифтов

    • Отображение дополнительных геометрических фигур (тор, конус и др.)

    Инициализация проводится с помощью функции

    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. В силу этого более предпочтительным является задание заранее приведенных к единичной длине нормалей. Нормализация нормалей необходима для правильного расчета освещения. Режим автоматической нормализации должен быть включен, если приложением используется преобразования растяжения/сжатия, поскольку в этом случае длина нормалей изменяется при умножении на видовую матрицу.

    Операторные скобки Begin/End

    Мы рассмотрели задание атрибутов одной вершины. Однако чтобы задать какую-нибудь фигуру, одних координат вершин недостаточно, и эти вершины надо объединить в одно целое, определив необходимые свойства. Для этого в 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 последовательно задаются вершины выпуклого многоугольника.

    Рис. 1


    ПРИМЕЧАНИЕ

    Использование 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)

    Вызов с параметром GLfront приводит к удалению из изображения всех лицевых граней, а с параметром GLback – обратных (установка по умолчанию).

    Кроме рассмотренных стандартных примитивов в библиотеках 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 используются операции над матрицами, при этом различают три типа матриц: видовая, проекций и текстуры. Все они имеют размер 4×4. Видовая матрица определяет преобразования объекта в мировых координатах, такие как параллельный перенос, изменение масштаба и поворот. Матрица проекций задает, как будут проецироваться трехмерные объекты на плоскость экрана (в оконные координаты), а матрица текстуры определяет наложение текстуры на объект.

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

    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 должен задавать матрицу размером 4×4 в виде массива с описанным расположением данных. Однако обычно для изменения матрицы того или иного типа удобно использовать специальные команды, которые по значениям своих параметров создают нужную матрицу и перемножают ее с текущей. Чтобы сделать текущей созданную матрицу, надо перед вызовом этой команды вызвать glLoadIdentity().

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

    Рис. 2


    Видовые преобразования

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

    (x', y', z', 1) = M * (x, y, z, 1)

    где 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, GLdoubletop)

    Первая команда создает матрицу проекции в усеченный объем видимости (параллелограмм видимости) в левосторонней системе координат. Параметры команды задают точки (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 (см. следующий пункт).

    Область вывода

    После применения матрицы проекций на вход следующего преобразования подаются так называемые усеченные (clip) координаты, для которых значения всех компонент (x, y, z, w) находятся в отрезке [-1, 1]. После этого находятся нормализованные координаты вершин по формуле: (x, y, z) = (x/w, y/w, z/w) Область вывода представляет собой прямоугольник в оконной системе координат, размеры которого задаются командой:

    void glViewPort(GLint x, GLint y, GLint width, GLint height)

    Значения всех параметров задаются в пикселях и определяют ширину и высоту области вывода с координатами левого нижнего угла (x, y) в оконной системе координат. Размеры оконной системы координат определяются текущими размерами окна приложения, точка (0,0) находится в левом нижнем углу окна.

    Используя параметры команды glViewPort(), вычисляются оконные координаты центра области вывода (o, o) по формулам o=x+width/2, o=y+height/2. Пусть p=width, p=height, тогда можно найти оконные координаты каждой вершины: (x, y, z) = ((p/2)x+ o, (p/2)y+ o[(f-n)/2] z+(n+f)/2). При этом целые положительные величины n и f задают минимальную и максимальную глубину точки в окне и по умолчанию равны 0 и 1 соответственно. Глубина каждой точки записывается в специальный буфер глубины (z-буфер), который используется для удаления невидимых линий и поверхностей. Установить значения n и f можно вызовом функции

    void glDepthRange(GLclampd n, GLclampd f)

    Команда glViewPort() обычно используется в функции, зарегистрированной с помощью команды glutReshapeFunc(), которая вызывается, если пользователь изменяет размеры окна приложения, изменяя соответствующим образом область вывода.


    Продолжение статьи – в выпуске 67б.

    (Алекс Jenter jenter@rsdn.ru) (Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.)







    Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх