OpenGL. Программирование компьютерной графики
Ф. Хилл Глава 2
Начальная стадия: рисование фигур
Первые программы, создающие картинки.
Знакомство с основными компонентами, имеющимися в любой программе с OpenGL.
Разработка нескольких элементарных графических инструментов для рисования прямых линий, ломаных линий и полигонов.
Разработка инструментария, позволяющего пользователю управлять программой при помощи мыши и клавиатуры.
Машины существуют; так давайте использовать их, чтобы создавать красоту, современную красоту, пока мы сами занимаемся этим. Ведь мы живем в двадцатом веке.
Олдос Хаксли (Aldous Huxley)
В разделе 2.1 «Начальная стадия создания изображения» рассматриваются основы написания программ, выполняющих простые рисунки. Обсуждается важность аппаратно-независимого программирования, описаны особенности оконных программ, а также программ, управляемых событиями. Раздел 2.2 «Рисование основных графических примитивов» знакомит с использованием OpenGL как с аппаратно-независимым интерфейсом прикладного программирования (Application Programming Interface — API), что подчеркивается на протяжении всей книги; а также показывает, как рисовать различные графические примитивы. Использование OpenGL иллюстрируется примерами рисунков, таких как созвездие Большой Медведицы, ковер Серпинского (Sierpinski gasket), а также график математической функции. В разделе 2.3 «Создание рисунков из линий» показано, как создавать изображения на базе ломаных линий и полигонов; а также начинается построение персональной библиотеки графических утилит. Раздел 2.4 «Простое взаимодействие с помощью мыши и клавиатуры» посвящен интерактивному графическому программированию, когда пользователь может указывать положения точек на экране с помощью мыши или управлять работой программы нажатием клавиш на клавиатуре. Глава заканчивается тематическими заданиями, которые иллюстрируют высказанные ранее идеи и глубже погружают нас в основные темы главы.
2.1. Начальная стадия создания изображения
Как и в случае многих других дисциплин, компьютерная графика изучается быстрее всего в процессе ее создания: при кодировании и тестировании программ, воспроизводящих различные изображения. Лучше всего начинать с простейших задач. Когда вы их изучите, можете попытаться написать их различные варианты, чтобы посмотреть, что получится, и таким образом, продвигаться к рисованию более сложных сцен.
Для того чтобы начать работать, вам необходима программная среда, которая позволит вам писать и исполнять программы. Что касается графики, то эта среда должна включать в себя как аппаратные средства для отображения графики (обычно это CRT-дисплей, который мы в дальнейшем будем называть «экраном»), так и библиотеку инструментов, которую ваши программы смогут использовать для фактического рисования графических примитивов.
Каждая графическая программа начинает работу с задания некоторых начальных условий, которые включают в себя установку желаемого режима дисплея, выбор системы координат для задания точек, линий и т. д. На рис. 2.1 показан ряд вариантов, которые могут встретиться. На рис. 2.1, а для рисования используется весь экран. Инициализация дисплея заключается в переключении его в «графический режим», а система координат установлена так, как показано на рисунке. Координаты x и y измеряются в пикселах, причем x возрастает вправо, а y — вниз.
Рис. 2.1. Некоторые часто встречающиеся варианты форматов дисплея
На рис. 2.1, б изображена более современная система, так называемая «оконная» («window-based»). Такая система может одновременно поддерживать на экране дисплея несколько различных прямоугольных окон. Ее инициализация включает создание и «открытие» нового графического окна (мы будем называть его «экранным окном»1 («screen window»). В графических командах используется система координат, привязанная к этому окну: обычно x возрастает вправо, а y — вниз. На рис. 2.1, в показан вариант, в котором использована начальная система координат «вправо–вверх»2, где y возрастает при движении вверх3.
Обычно в каждой системе имеется несколько элементарных графических инструментов, с помощью которых пользователь может начать работу. Самый главный из них имеет название типа setPixel(x,y,color) и предназначен для присвоения отдельному пикселу с координатами (x,y) значения цвета, задаваемого переменной color. Иногда этот инструмент имеет другое имя, например putPixel(), setPixel () или drawPoint(). Наряду с setPixel() почти всегда имеется еще один инструмент для рисования прямой линии, например line(x1,y1,x2,y2), который рисует прямую между точками с координатами (x1,y1) и (x2,y2). В других системах этот инструмент может называться drawLine() или Line(). Группа команд
line(100, 50, 150, 80);
line(150, 80, 0, 290);
нарисует в каждой из систем изображения, приведенные на рис. 2.1. Другие системы могут не иметь команды line(), а вместо нее для рисования прямых пользоваться командами moveto(x, y) и lineto(x, y). Можно провести аналогию между этими командами и перьевым плоттером, перо которого имеет некоторые «текущие координаты» («current position»). Предполагается, что команда moveto(x, y) перемещает перо над бумагой в положение (x, y), устанавливая тем самым текущие координаты (x, y); команда lineto(x, y) рисует линию от текущих координат до координат (x, y), после чего текущие координаты устанавливаются в этой точке (x, y). Каждая из этих команд перемещает перо из его текущих координат в новые координаты. Затем эти новые координаты становятся текущими. Картинки на рис. 2.1 можно было нарисовать с помощью группы команд
moveto(100, 50);
lineto(150, 80);
lineto(0, 290);
Получив такую систему в свое распоряжение, энергичный программист может разработать целый инструментарий сложнейших функций, использующих эти элементарные команды, и тем самым создать мощную библиотеку графических подпрограмм. Тогда законченные графические приложения могут писаться с применением этой персональной библиотеки.
Очевидная проблема состоит в том, что каждый графический дисплей использует для своей работы различные основные команды, а каждая программная среда имеет различные наборы инструментов для создания графических примитивов. Такое разнообразие делает затруднительным перенос программы из одной программной среды в другую (рано или поздно каждый сталкивается с необходимостью переделки программы под новую среду), следовательно, программист должен создавать все необходимые инструменты для новой программной среды. Необходимость подобной реконструкции может потребовать существенных изменений всей структуры библиотеки или приложения и большой работы программиста.
2.1.1. Аппаратно-независимое программирование и OpenGL
Приятно, когда возможен одинаковый подход к написанию графических приложений, когда, например, одна и та же программа может быть скомпилирована и запущена в различных графических средах, когда есть гарантия, что на любом дисплее будет получено примерно одинаковое изображение. Такой подход и называется аппаратно-независимым графическим программированием. В OpenGL предлагается программное средство, при котором перенос графической программы требует только установки соответствующих библиотек OpenGL на новой машине, а само приложение не требует никаких изменений и вызывает из новой библиотеки те же самые функции с теми же параметрами. В результате получаются те же графические результаты. Метод создания графики в OpenGL был принят большим количеством компаний, и библиотеки OpenGL существуют для всех значимых графических сред1.
OpenGL часто называют «интерфейсом прикладного программирования» (Application Programming Interface — API). Он представляет собой набор подпрограмм, которые может вызывать программист, и примеры того, как эти подпрограммы работают, создавая графику. Программист «видит» только интерфейс и поэтому избавлен от необходимости вникать в специфику оборудования и в особенности программного обеспечения установленной графической системы.
OpenGL наиболее ярко проявляет свои возможности при рисовании изображений сложных трехмерных (3D) сцен. Эти возможности могли бы показаться даже чрезмерными при рисовании простых двумерных (2D) объектов. Однако OpenGL прекрасно работает и при 2D-рисовании, обеспечивая тем самым унифицированный подход к созданию изображений. Мы начинаем наше знакомство с OpenGL с применения более простых его конструкций, используя многие опции по умолчанию. Позже, при написании программ для создания сложной 3D-графики, мы изучим более мощные инструменты OpenGL.
Хотя мы будем разрабатывать большинство наших графических средств на базе возможностей OpenGL, мы тем не менее «приоткроем завесу» и посмотрим, как работают классические графические алгоритмы. Важно понять, как реализовывать такие программные средства, даже если вы используете в большинстве своих приложений уже готовые версии OpenGL. При особых обстоятельствах вам может потребоваться для какой-либо задачи альтернативный алгоритм, или вы можете встретить новую задачу, которую OpenGL не умеет решать. Кроме того, вам может понадобиться разработать графическое приложение, вовсе не использующее OpenGL.
2.1.2. Оконное программирование
Как уже упоминалось, многие современные графические системы являются оконными (windows based) и показывают на экране несколько перекрывающихся окон одновременно. Пользователь с помощью мыши может перемещать окна по экрану, а также изменять их размер. При работе с OpenGL мы будем рисовать в одном из таких окон, как мы уже видели на рис. 2.1, в.
Программирование управляемости событиями
Другим свойством, которым обладает большая часть оконных программ, является их управляемость событиями (event driven). Это означает, что программы реагируют на различные события, такие как щелчок мышью, нажатие на клавишу клавиатуры или изменение размеров окна на экране. Система автоматически устанавливает очередь событий (event queue), которая получает сообщения о том, что произошло некоторое событие, и обслуживает их по принципу «первым пришел — первым обслужен». Программист организует свою программу как набор функций обратного вызова (callback functions), которые выполняются, когда происходят события. Функция обратного вызова создается для каждого типа событий, которые могут произойти. Когда система удаляет событие из очереди, оно просто выполняет функцию обратного вызова, связанную с данным типом событий. Тем программистам, которые привыкли делать программы по принципу «сделай то, затем сделай этој», потребуется некоторое переосмысление. Новая структура программ может быть передана словами «не делай ничего, пока не произойдет какое-нибудь событие, а затем сделай определенную вещь».
Метод связи функции обратного вызова с определенным типом события часто полностью зависит от системы. Однако вместе с OpenGL поставляется набор утилит (Utility Toolkit) (см. приложение А), в котором предусмотрены программные средства для того, чтобы помочь в организации управления событиями. Например, команда
glutMouseFunc(myMouse);
// register the mouse action function
// функция для регистрации действий мыши регистрирует функцию myMouse() в качестве функции, подлежащей выполнению при возникновении события «мышь». Префикс «glut» показывает, что данная функция является частью OpenGL Utility Toolkit. Программист размещает в myMouse() код для обработки всех возможных действий мыши, которые представляют для него интерес.
В листинге 2.1 приведен скелет примерной функции main() для программы, управляемой событиями. Большинство наших программ в данной книге будут основываться на этой структуре. Существует четыре основных типа событий, с которыми мы будем работать, а «glut»-функции доступны каждому.
Листинг 2.1. Структура управляемой событиями программы, использующей OpenLG
void main()
{
initialize things1
// инициализируем все, что необходимо
create a screen window
// создаем экранное окно
glutDisplayFunc(myDisplay);
// register the redraw function
// регистрируем функцию обновления окна
glutReshapeFunc(myReshape);
// register the reshape function
// регистрируем функцию изменения формы
glutMouseFunc(myMouse);
// register the mouse action function
// регистрируем функцию реакции на действия мыши
glutKeyboardFunc(myKeyboard);
// register the keyboard action function
// регистрируем функцию реакции на действия
// клавиатуры
perhaps initialise other things
// при необходимости инициализируем что-то еще
glutMainLoop();
// enter the unending main loop
// входим в бесконечный главный цикл
}
all of the callback functions are defined here
// здесь определены все функции обратного вызова
glutDisplayFunc(myDisplay). Когда система решает, что какое-нибудь окно на экране подлежит перерисовке, она создает событие «redraw» (обновить). Это имеет место при первом открытии окна, а также когда окно вновь становится видимым, когда другое окно перестало его заслонять. В данном случае функция myDisplay() зарегистрирована в качестве функции обратного вызова для события обновления.
glutReshapeFunc(myReshape). Форма экранного окна может быть изменена (reshape) пользователем, обычно путем захвата мышью угла окна и смещения его с помощью мыши (простое перемещение окна не приводит к событию «изменение формы».) В данном случае функция myReshape() зарегистрирована с событием изменения формы окна. Как мы увидим дальше, функции myReshape() автоматически передаются аргументы, информирующие о новой ширине и высоте изменившего свою форму окна.
glutMouseFunc(myMouse). Когда нажимают или отпускают одну из кнопок мыши, то возникает событие «мышь» (mouse). В данном случае функция myMouse() зарегистрирована в качестве функции, которая будет вызываться всякий раз, когда произойдет событие «мышь». Функции myMouse() автоматически передаются аргументы, описывающие местоположение мыши, а также вид действия, вызываемого нажатием кнопки мыши.
glutKeyboardFunc(myKeyboard). Эта команда регистрирует функцию myKeyboard() для события, заключающегося в нажатии или отпускании какой-либо клавиши на клавиатуре. Функции myKeyboard() автоматически передаются аргументы, сообщающие, какая клавиша была нажата. Для удобства этой функции также передается информация о положении мыши в момент нажатия клавиши.
Если в какой-либо программе мышь не используется, то соответствующую функцию обратного вызова не нужно писать или регистрировать. В этом случае щелчок мыши не окажет на программу никакого действия. Это же верно для программ, не использующих клавиатуру.
Последняя функция, использованная в листинге 2.1, — это glutMainLoop(). Когда выполняется эта инструкция, программа рисует начальную картину и входит в бесконечный цикл, находясь в котором она просто ждет наступления событий. (Нормальное завершение работы любой программы осуществляется щелчком мыши по прямоугольнику «выход» (go away), который имеется в каждом окне.)
2.1.3. Открытие окна для рисования
Первая задача при создании изображений заключается в открытии экранного окна для рисования. Эта работа является достаточно сложной и зависит от системы. Поскольку функции OpenGL являются аппаратно-независимыми, то в них не предусмотрено поддержки управления окнами определенных систем. Однако в OpenGL Utility Toolkit включены функции для открытия окна в любой используемой вами системе.
Листинг 2.2. Программный код, использующий OpenGL Utility Toolkit для открытия начального окна для рисования
// appropriate #includes go here - see Appendix 1
// здесь должны стоять соответствующие операторы
// включения #includes — см.приложение А
void main(int argc, char** argv)
{
glutInit(&argc, argv);
// initialize the toolkit
// инициализируем инструментарий
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
// set the display mode
// устанавливаем режим дисплея
glutInitWindowSize(640,480);
// set window size
// устанавливаем размер окна
glutInitWindowPosition(100,150);
// set the window position on screen
// устанавливаем положение окна на экране
glutCreateWindow("my first attempt");
//"my first attempt"="моя первая попытка"
// open the screen window
// открываем экранное окно
// register the callback functions
// регистрируем функции обратного вызова
glutDisplayFunc(myDisplay);
glutReshapeFunc(myReshape);
glutMouseFunc(myMouse);
glutKeyboardFunc(myKeyboard);
myInit();
// additional initializations as necessary
// дополнительные инициализации при необходимости
glutMainLoop();
// go into perpetual loop
// входим в бесконечный цикл
Листинг 2.2 конкретизирует структуру листинга 2.1, показывая целиком функцию main() на примере программы, рисующей графики в экранном окне. Первые пять вызовов функции используют OpenGL Utility Toolkit для открытия окна для рисования. В своих первых графических программах вы можете просто скопировать отсюда эти функции; позднее мы увидим, какой смысл имеют различные аргументы и как заменить некоторые из них для достижения определенных эффектов. Первые пять функций инициализируют и отображают экранное окно, в котором наша программа будет работать с графикой. Дадим короткое описание того, что делает каждая из этих функций:
glutInit(&argc,argv). Эта функция инициализирует OpenGL Utility Toolkit. Ее аргументы для передачи информации о командной строке являются стандартными; здесь мы не будем их использовать.
glutInitDisplayMode(GLUT_SINGLE|GLUT_RGB). Эта функция определяет, как должен быть инициализирован дисплей. Встроенные в нее константы GLUT_SINGLE и GLUT_RGB с оператором OR (или) между ними показывают, что следует выделить один дисплейный буфер и что цвета задаются с помощью сочетаний красного, зеленого и синего цветов. Позднее мы будем изменять эти аргументы. Например, для плавной анимации мы будем использовать двойную буферизацию.
glutInitWindowSize(640,480). Эта функция устанавливает, что экранное окно при инициализации должно иметь 640 пикселов в ширину и 480 пикселов в высоту. Во время выполнения программы пользователь может изменять размер этого окна по своему желанию.
glutInitWindowPosition(100,150). Эта функция устанавливает, что верхний левый угол данного окна должен находиться в 100 пикселах от левого края и в 150 пикселах от верхнего края. Во время выполнения программы пользователь может перемещать это окно, куда пожелает.
glutCreateWindow("my first attempt"). Эта функция фактически открывает и отображает экранное окно, помещая текст заголовка «my first attempt» (моя первая попытка) в строку заголовка (title bar) окна.
Остальные функции в main() регистрируют функции обратного вызова, как уже описывалось ранее, выполняют все инициализации, специфические для данной программы, после чего запускают на выполнение главный событийный цикл (event loop). Программист (вы!) должен реализовывать каждую из функций обратного вызова аналогично myInit().
2.2. Рисование основных графических примитивов
Мы намерены рассмотреть технику программирования для рисования большого числа геометрических форм, которые составляют интересные картины. Команды рисования будут помещаться в функцию обратного вызова, связанную с событием redraw (обновление), например, в функцию myDisplay().
Прежде всего мы должны определить систему координат, в которой будем описывать графические объекты, и назначить, где они будут появляться в экранном окне. Похоже, что программирование компьютерной графики вовлекает нас с бесконечную борьбу с заданием различных систем координат и управлением ими. Поэтому мы будем двигаться от простых случаев к более сложным.
Рис. 2.2. Начальная система координат для рисования
Начнем с интуитивной системы координат, которая привязана непосредственно к системе координат экранного окна (см. рис. 2.1, в) и измеряет расстояния в пикселах. Наше первое шаблонное экранное окно, показанное на рис. 2.2, имеет 640 пикселов в ширину и 480 пикселов в высоту. Координата x изменяется от 0 на левом крае до 639 на правом крае. Координата y изменяется от 0 на нижнем крае до 479 на верхнем крае. Мы определим данную систему координат позже, после изучения нескольких основных примитивов.
OpenGL предоставляет средства для рисования всех выходных примитивов, описанных в главе 1. Большинство из них, такие как точки, линии, ломаные, полигоны, задаются одной или несколькими вершинами (vertices). Для рисования таких объектов в OpenGL необходимо передать ему список вершин. Этот список возникает между двумя вызовами OpenGL-функций: glBegin()и glEnd(). Аргумент в glBegin() определяет, какой объект рисуется. Например, на рис. 2.3 приведены три точки, нарисованные в окне шириной в 640 пикселов и высотой в 480 пикселов. Эти точки рисуются с помощью следующей последовательности команд:
glBegin(GL_POINTS);
glVertex2i(l00,50);
glVertex2i(100,130);
glVertex2i(150,130);
glEnd();
Рис. 2.3. Рисование трех точек
GL_POINTS является встроенной константой OpenGL. Для рисования других примитивов следует заменить GL_POINTS на GL_LINES, GL_POLIGON и т. д. Каждая из этих констант будет введена в свое время.
Как мы увидим позже, эти команды посылают информацию о вершине в «графический конвейер» («graphics pipeline»), в котором она проходит ряд преобразований. Для наших целей достаточно считать, что эта информация направляется более или менее прямо в систему координат экранного окна.
Многие функции в OpenGL, такие как glVertex2i(), имеют несколько вариантов, различающихся между собой числом и типом аргументов, передаваемых в функцию. Рисунок 2.4 показывает, какой формат имеют вызовы таких функций.
Рис. 2.4. Формат команд в OpenGL
Префикс «gl» указывает, что функция взята из библиотеки OpenGL (в отличие от префикса «glut» у функций из GL Utility Toolkit). Затем идет основная часть команды, за ней следует число аргументов, пересылаемых в функцию (чаще всего 3 и 4), и, наконец, тип аргумента (i для целых величин, f или d для величин с плавающей точкой и т. д., так как у нас всего лишь краткое описание). Если мы захотим сослаться на основную команду, не уточняя специфику ее аргументов, то будем использовать звездочку: glVertex*().
Для создания той же картинки из трех точек, изображенной на рис 2.3, вы могли бы, например, передать функции вещественные величины вместо целых, используя для этого следующие команды:
glBegin(GL_POINTS);
glVertex2d(100.0,50.0);
glVertex2d(100.0,130.0);
glVertex2d(150.0,130.0);
glEnd();
Типы данных OpenGL
OpenGL работает с определенными типами данных. Например, функции вроде glVertex2i() требуют целых величин определенного размера (32 бита). Хорошо известно, что в некоторых системах тип данных int из C или C++ является 16-битным, в то время как в других системах он 32-битный. Не существует также стандартного размера для типов float и double. Для гарантии того, что OpenGL-функции получают нужные типы данных, полезно использовать для типов OpenGL встроенные имена, такие как GLint или GLfloat. Список типов OpenGL приведен в табл. 2.1. С некоторыми из этих типов мы столкнемся лишь позднее.
Таблица 2.1. Суффиксы команд и типы данных аргументов
Суффикс
|
Тип данных
|
Обычный тип C или C++
|
Название типа в OpenGL
|
b
|
8-битное целое (символ со знаком)
|
signed char
|
Glbyte
|
s
|
16-битное целое (короткое целое)
|
short
|
Glshort
|
I
|
32-битное целое (длинное целое)
|
int или long
|
GLint, GLsizei
|
f
|
32-битное вещественное (с плавающей точкой)
|
float
|
GLfloat, GLclampf
|
d
|
64-битное вещественное (двойной точности)
|
double
|
GLdouble, GLclampd
|
ub
|
8-битное число без знака (символ без знака)
|
unsigned char
|
DLubyte, GLboolean
|
us
|
16-битное число без знака (короткое целое без знака)
|
unsigned short
|
GLushort
|
ui
|
32-битное число без знака (целое без знака или длинное целое без знака)
|
unsigned int или unsigned long
|
GLuint, GLenum, GLbitfield
|
Пусть, к примеру, функция с суффиксом i «ожидает» 32-битное целое, а ваша система может передать int как 16-битное целое. Тогда если бы вы захотели инкапсулировать команды OpenGL для рисования точки в базовую функцию вроде drawDot(), то было бы соблазнительно использовать такой код:
void drawDot(int x, int y)
<— danger: passes ints
<— опасность: передает целые (int)
{
// draw dot at integer point(x,y)
// рисуем точку с целыми координатами (x,y)
glBegin(GL_POINTS);
glVertex2i(x,y);
glEnd();
}
Этот код передает ints в функцию glVertex2i(). Это сработает в системах, использующих 32-битный тип ints, однако в тех системах, где ints — 16-битный, могут возникнуть проблемы. Надежнее писать функцию drawingDot()1 так, как приведено в листинге 2.3, а также использовать в своих программах тип GLints. Когда вы перекомпилируете свои программы в новой системе, типы GLint, GLfloat и т. д. будут связываться с соответствующими типами C++ (в заголовочном файле GL.h OpenGL — см. приложение А) для этой системы, и эти типы будут использоваться постоянно во всей программе.
Листинг 2.3. Подробности инкапсуляции OpenGL в базовой функции drawDot()
void drawDot(GLint x, GLint y);
{
// draw dot at integer point (x, y)
// рисуем точку с целыми координатами (x, y)
glBegin(GL_POINTS);
glVertext2i(x, y);
glEnd();
}
Достарыңызбен бөлісу: |