Основы программирования на языке Си++. Часть II. Основы

advertisement
А.А. Богуславский, С.М. Соколов
Основы программирования
на языке Си++
Часть II. Основы программирования
трехмерной графики
(для студентов физико-математических факультетов
педагогических институтов)
Коломна, 2002
ББК 32.97я73
УДК 681.142.2(075.8)
Б 73
Рекомендовано к изданию
редакционно-издательским советом
Коломенского государственного
педагогического института
Богуславский А.А., Соколов С.М.
Б73 Основы программирования на языке Си++: Для студентов физикоматематических факультетов педагогических институтов. – Коломна: КГПИ,
2002. – 490 с.
Пособие предназначено для обучения студентов, обладающих навыками пользовательской работы на персональном компьютере, основным понятиям и методам
современного практического программирования. Предметом изучения курса является
объектно-ориентированное программирование на языке Си++ в среде современных
32-х разрядных операционных систем семейства Windows. Программа курса разбита
на 4 части: (1) Введение в программирование на языке Си++; (2) Основы программирования трехмерной графики; (3) Объектно-ориентированное программирование на
языке Си++ и (4) Программирование для Microsoft Windows с использованием Visual
C++ и библиотеки классов MFC.
После изучения курса студент получает достаточно полное представление о
содержании современного объектно-ориентированного программирования, об устройстве современных операционных систем Win32 и о событийно-управляемом программировании. На практических занятиях вырабатываются навыки программирования на Си++ в интегрированной среде разработки Microsoft Visual C++ 5.0.
Рецензенты:
И.П. Гиривенко – к.т.н., доцент, зав. кафедрой информатики и вычислительной техники Рязанского государственного педагогического университета
им. С.А. Есенина.
А.А. Шамов –
к.х.н., доцент кафедры теоретической физики Коломенского государственного педагогического института.
2
СОДЕРЖАНИЕ
ВВЕДЕНИЕ............................................................................................................................5
ЛЕКЦИЯ 1. БИБЛИОТЕКА OPENGL.............................................................................6
1. НАЗНАЧЕНИЕ БИБЛИОТЕКИ OPENGL...............................................................................6
2. ОСНОВНЫЕ ВОЗМОЖНОСТИ OPENGL ..............................................................................7
3. МАКЕТ КОНСОЛЬНОГО ПРИЛОЖЕНИЯ, ИСПОЛЬЗУЮЩЕГО БИБЛИОТЕКУ GLAUX .........8
4. ИМЕНА ФУНКЦИЙ OPENGL..............................................................................................9
5. СИСТЕМЫ КООРДИНАТ ...................................................................................................10
6. ПРИМЕР ВЫПОЛНЕНИЯ МОДЕЛЬНЫХ ПРЕОБРАЗОВАНИЙ ..............................................15
7. СВОДКА РЕЗУЛЬТАТОВ ...................................................................................................17
8. УПРАЖНЕНИЯ .................................................................................................................17
ЛЕКЦИЯ 2. ГЕНЕРАЦИЯ ДВИЖУЩИХСЯ ИЗОБРАЖЕНИЙ..............................20
1. АНИМАЦИЯ С ДВОЙНОЙ БУФЕРИЗАЦИЕЙ ......................................................................20
2. ОБРАБОТКА СОБЫТИЙ КЛАВИАТУРЫ И МЫШИ ..............................................................23
3. КОМПОЗИЦИЯ НЕСКОЛЬКИХ ПРЕОБРАЗОВАНИЙ ............................................................25
4. СВОДКА РЕЗУЛЬТАТОВ ...................................................................................................30
5. УПРАЖНЕНИЯ .................................................................................................................31
ЛЕКЦИЯ 3. ГЕОМЕТРИЧЕСКИЕ ПРИМИТИВЫ ....................................................32
1. СЛУЖЕБНЫЕ ГРАФИЧЕСКИЕ ОПЕРАЦИИ ........................................................................32
2. ОПИСАНИЕ ТОЧЕК, ОТРЕЗКОВ И МНОГОУГОЛЬНИКОВ ..................................................34
3. СВОЙСТВА ТОЧЕК, ОТРЕЗКОВ И МНОГОУГОЛЬНИКОВ ...................................................38
4. СВОДКА РЕЗУЛЬТАТОВ ...................................................................................................45
5. УПРАЖНЕНИЯ .................................................................................................................45
ЛЕКЦИЯ 4. ПОЛИГОНАЛЬНАЯ АППРОКСИМАЦИЯ ПОВЕРХНОСТЕЙ.......47
1. ВЕКТОРЫ НОРМАЛИ ........................................................................................................47
2. НЕКОТОРЫЕ РЕКОМЕНДАЦИИ ПО ПОСТРОЕНИЮ ПОЛИГОНАЛЬНЫХ АППРОКСИМАЦИЙ
ПОВЕРХНОСТЕЙ ..................................................................................................................47
3. ПРИМЕР: ПОСТРОЕНИЕ ИКОСАЭДРА ...............................................................................49
4. ПЛОСКОСТИ ОТСЕЧЕНИЯ ................................................................................................54
6. СВОДКА РЕЗУЛЬТАТОВ ...................................................................................................56
7. УПРАЖНЕНИЯ .................................................................................................................56
ЛЕКЦИЯ 5. ЦВЕТ И ОСВЕЩЕНИЕ..............................................................................57
1. ЦВЕТОВАЯ МОДЕЛЬ RGB ...............................................................................................57
2. ЗАДАНИЕ СПОСОБА ЗАКРАСКИ .......................................................................................57
3. ОСВЕЩЕНИЕ ...................................................................................................................59
4. ОСВЕЩЕНИЕ В РЕАЛЬНОМ МИРЕ И В OPENGL...............................................................59
5. ПРИМЕР: РИСОВАНИЕ ОСВЕЩЕННОЙ СФЕРЫ .................................................................61
6. СОЗДАНИЕ ИСТОЧНИКОВ СВЕТА ....................................................................................64
4. СВОДКА РЕЗУЛЬТАТОВ ...................................................................................................68
5. УПРАЖНЕНИЯ .................................................................................................................69
ЛЕКЦИЯ 6. СВОЙСТВА МАТЕРИАЛА И СПЕЦЭФФЕКТЫ ОСВЕЩЕНИЯ....70
1. ЗАДАНИЕ СВОЙСТВ МАТЕРИАЛА ....................................................................................70
2. СМЕШЕНИЕ ЦВЕТОВ И ПРОЗРАЧНОСТЬ ..........................................................................75
3
3. ТУМАН ............................................................................................................................78
4. СВОДКА РЕЗУЛЬТАТОВ ...................................................................................................81
5. УПРАЖНЕНИЯ .................................................................................................................82
ЛЕКЦИЯ 7. РАСТРОВЫЕ ОБЪЕКТЫ: ИЗОБРАЖЕНИЯ И ТЕКСТУРЫ...........83
1. ВЫВОД ИЗОБРАЖЕНИЙ В БУФЕР OPENGL......................................................................83
2. НАЗНАЧЕНИЕ ТЕКСТУР ...................................................................................................84
3. СОЗДАНИЕ ТЕКСТУРЫ В ОПЕРАТИВНОЙ ПАМЯТИ ..........................................................85
4. АВТОМАТИЧЕСКОЕ ПОВТОРЕНИЕ ТЕКСТУРЫ НА ПЛОСКОМ МНОГОУГОЛЬНИКЕ ..........90
5. НАЛОЖЕНИЕ ТЕКСТУРЫ НА ПРОИЗВОЛЬНУЮ ПОВЕРХНОСТЬ .......................................91
6. СВОДКА РЕЗУЛЬТАТОВ ...................................................................................................93
7. УПРАЖНЕНИЯ .................................................................................................................93
ЛЕКЦИЯ 8. ПРИМЕРЫ ПРОГРАММ С ИСПОЛЬЗОВАНИЕМ OPENGL ..........96
1. ИМИТАЦИЯ ТРЕХМЕРНОГО ЛАНДШАФТА ......................................................................96
2. ОБЪЕМНЫЙ "ТЕТРИС".....................................................................................................97
ЛИТЕРАТУРА ..................................................................................................................102
4
Введение
Лекции данной части учебного курса "Основы программирования на языке
Си++" предназначены для начального изучения графической библиотеки OpenGL и
закрепления навыков программирования на процедурном подмножестве языка Си++.
Предварительные знания в области трехмерной компьютерной графики не предполагаются. В данном курсе демонстрируется, что для применения готовых программных
средств необходимо знать основные задачи и алгоритмы конкретной предметной области. С точки зрения изучения программирования показывается общность понятий
абстрактных типов данных (на примере стека и массива). Вводится понятие макета
(каркаса) программы и функций обратной связи, которые используются для написания программ в событийно-управляемой среде.
На практических занятиях используется среда разработки Microsoft Visual C++
на ПК под управлением Windows 95/98/NT. Все программы, рассматриваемые в качестве примеров в лекциях, и ответы к упражнениям написаны на стандартном ANSI
Си++ и проверены в среде Microsoft Visual C++ 5.0 на ПК под управлением Windows 98.
5
ЛЕКЦИЯ 1. Библиотека OpenGL
1. Назначение библиотеки OpenGL
Для упрощения разработки программ на языке Си++ существует большое количество готовых библиотек с реализацией алгоритмов для конкретных предметных
областей, от численных расчетов до распознавания речи. Библиотека OpenGL является одним из самых популярных программных интерфейсов (API) для работы с
трехмерной графикой. Стандарт OpenGL был утвержден в 1992 г. ведущими фирмами в области разработки программного обеспечения. Его основой стала библиотека
IRIS GL, разработанная фирмой Silicon Graphics на базе концепции графической
машины Стэнфордского университета (1982 г.).
OpenGL переводится как Открытая Графическая Библиотека (Open Graphics
Library). Программы, использующие OpenGL, гарантируют одинаковый визуальный
результат во многих операционных системах – на персональных компьютерах, на рабочих станциях и на суперкомпьютерах.
С точки зрения программиста, OpenGL – это программный интерфейс для графических устройств (например, графических ускорителей). Он включает в себя около
150 различных функций, с помощью которых программист может задавать свойства
различных трехмерных и двумерных объектов и выполнять их визуализацию (рендеринг). Т.е. в программе надо задать местоположение объектов в трехмерном пространстве, определить другие параметры (поворот, растяжение, ...), задать свойства
объектов (цвет, текстура, материал, ...), положение наблюдателя, а затем библиотека
OpenGL выполнит генерацию двумерной проекции этой трехмерной сцены (рис. 1.1).
Рис. 1.1. Двумерная проекция трехмерной сцены, полученная с помощью
библиотеки OpenGL.
Можно сказать, что библиотека OpenGL является библиотекой только для визуализации трехмерных сцен (rendering library). Она не поддерживает какие либо периферийные устройства (например, клавиатуру или мышь) и не содержит средств для
управления экранными окнами. Обеспечение взаимодействия периферийных устройств с библиотекой OpenGL в конкретной операционной системе является задачей
программиста.
6
2. Основные возможности OpenGL
Возможности OpenGL, предоставляемые программисту, можно разделить на
несколько групп:
• Геометрические и растровые примитивы. На основе этих примитивов строятся
все остальные объекты. Геометрические примитивы – это точки, отрезки и многоугольники. Растровыми примитивами являются битовые массивы (bitmap) и изображения (image).
• Сплайны. Сплайны применяются для построения гладких кривых по опорным
точкам.
• Видовые и модельные преобразования. Эти преобразования позволяют задавать
пространственное расположение объектов, изменять форму объектов и задавать
положение камеры, для которой OpenGL строит результирующее проекционное
изображение.
• Работа с цветом. Для операций с цветом в OpenGL есть режим RGBA (красныйзелёный-синий-прозрачность) и индексный режим (цвет задается порядковым номером в палитре).
• Удаление невидимых линий и поверхностей.
• Двойная буферизация. В OpenGL доступна и одинарная, и двойная буферизация.
Двойная буферизация применяется для устранения мерцания при мультипликации.
При этом изображение каждого кадра сначала рисуется в невидимом буфере, а на
экран кадр копируется только после того, как полностью нарисован.
• Наложение текстуры. Текстуры упрощают создание реалистичных сцен. Если
на объект, например, сферу, наложить текстуру (некоторое изображение), то объект будет выглядеть иначе (например, сфера будет выглядеть как разноцветный
мячик).
• Сглаживание. Автоматическое сглаживание компенсирует ступенчатость, свойственную растровым дисплеям. При сглаживании отрезков OpenGL изменяет интенсивность и цвет пикселей так, что эти отрезки отображаются на экране без "
зигзагов".
• Освещение. Указание расположения, интенсивности и цвета источников света.
• Специальные эффекты. Например, туман, дым, прозрачность объектов. Эти
средства позволяют сделать сцены более реалистичными.
Хотя библиотека OpenGL предоставляет практически все возможности для
моделирования и воспроизведения трёхмерных сцен, некоторые графические функции непосредственно в OpenGL недоступны. Например, чтобы задать положение и
направление камеры для наблюдения сцены придется рассчитывать проекционную
матрицу, что сопряжено с достаточно громоздкими вычислениями. Поэтому для
OpenGL существуют так называемые вспомогательные библиотеки.
Одна из этих библиотек называется GLU. Эта библиотека является частью
стандарта и поставляется вместе с главной библиотекой OpenGL. В состав GLU входят более сложные функции (например, для создания цилиндра или диска требуется
всего одна команда). В библиотеке GLU есть также функции для работы со сплайнами, реализованы дополнительные операции над матрицами и дополнительные виды
проекций.
Еще две известные библиотеки – GLUT (для Unix) и GLAUX (для MS Windows). В них реализованы не только дополнительные функции OpenGL (для по7
строения некоторых сложных фигур вроде конуса и тетраэдра), но также есть функции для работы с окнами, клавиатурой и мышью в консольных приложениях. Чтобы
работать с OpenGL в конкретной операционной системе (например, Windows или
Unix), надо провести некоторую предварительную настройку, которая зависит от операционной системы. GLUT и GLAUX позволяют буквально несколькими командами
определить окно, в котором будет работать OpenGL, задать функции для обработки
команд от клавиатуры или мыши.
3. Макет консольного приложения, использующего библиотеку GLAUX
В отличие от обычной консольной программы Win32, в проект программы, использующей OpenGL, надо добавить три файла с расширениями .lib из каталога
DEVSTUDIO\VC\LIB: opengl32.lib, glu32.lib и glaux.lib. В этих файлах содержатся
данные, с помощью которых компоновщик организует в исполняемом файле программы вызовы функций OpenGL из динамических библиотек opengl32.dll и
glu32.dll (они расположены в каталоге WINDOWS\SYSTEM).
Показанная ниже программа 1.1 выполняет анимацию сферы, которая движется
в экранном окне в направлении "слева направо". Генерация изображения трехмерной
сцены выполняется в функции display(). Консольные приложения, которые будут
рассматриваться в данной лекции, имеют похожую структуру и различаются содержанием функции display().
#include
#include
#include
#include
<windows.h>
<GL/gl.h>
<GL/glu.h>
<GL/glaux.h>
// Заголовочный файл с описаниями функций Windows
// Заголовочные файлы библиотеки OpenGL
// Прототипы функций обратной связи (для автоматического вызова из GLAUX)
void CALLBACK resize( int width, int height );
void CALLBACK display( void );
void main()
{
// Параметры обсчета сцены в OpenGL: цветовой режим RGBA, удаление
// невидимых поверхностей и линий, двойная буферизация
auxInitDisplayMode( AUX_RGBA | AUX_DEPTH | AUX_DOUBLE );
// Создание окна OpenGL с заголовком "Программа 1.1"
// Размер окна – 400х400 пикселей. Левый верхний угол окна
// задается экранными координатами (50, 10).
auxInitPosition( 50, 10, 400, 400);
auxInitWindow( "Программа 1.1" );
// В случае, когда окно не получает сообщений от клавиатуры, мыши или
// таймера, то будет вызываться функция display. Так можно получить
// анимацию. Если анимация не нужна, то эта строка лишняя.
auxIdleFunc( display );
// Задание функции, которая будет вызываться при изменении
// размеров окна Windows.
auxReshapeFunc( resize );
// Включение ряда параметров OpenGL
glEnable( GL_ALPHA_TEST ); // Учет прозрачности
glEnable( GL_DEPTH_TEST ); // Удаление невидимых поверхностей
glEnable( GL_COLOR_MATERIAL );
// Синхронное задание цвета рисования
// и цвета материала объектов
glEnable( GL_BLEND );
// Разрешение смешения цветов
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
8
glEnable( GL_LIGHTING );
glEnable( GL_LIGHT0 );
// Учет освещения
// Включение нулевого источника света
// Задание положения и направления нулевого источника света
float pos[4] = { 3, 3, 3, 1 };
float dir[3] = { -1, -1, -1 };
glLightfv( GL_LIGHT0, GL_POSITION, pos );
glLightfv( GL_LIGHT0, GL_SPOT_DIRECTION, dir );
}
// Задание функции отрисовки окна. Эта функция будет вызываться всякий
// раз, когда потребуется перерисовать окно на экране (например, когда
// окно будет развернуто на весь экран)
auxMainLoop( display );
void CALLBACK resize( int width, int height )
{
// Указание части окна для вывода кадра OpenGL
glViewport( 0, 0, width, height );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
// Задание типа проекции (glOrtho - параллельная, glFrustum // перспективная). Параметры функций определяют видимый объем
// (левая стенка - пять единиц влево, правая - пять единиц вправо,
// далее задаются нижняя стенка, верхняя, передняя и задняя)
glOrtho( -5, 5, -5, 5, 2, 12 );
}
// Задание позиции наблюдателя (0, 0, 5), направление луча
// зрения (на точку (0, 0, 0)), вектор, принимаемый за направление
// "вверх" (0, 1, 0) (т.е. параллельно оси Y).
gluLookAt( 0,0,5, 0,0,0, 0,1,0 );
glMatrixMode( GL_MODELVIEW );
void CALLBACK display(void)
{
// Очистка буфера кадра
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
// Перенос системы координат, связанной с объектом, на 0.01
glTranslated( 0.01, 0, 0 );
// Рисование в начале координат, связанных с объектом, сферы
// радиусом 1, окрашенной в красный цвет
glColor3d( 1, 0, 0 );
auxSolidSphere( 1 );
}
// Копирование содержимого буфера кадра на экран
auxSwapBuffers();
Программа 1.1
4. Имена функций OpenGL
Рисование сферы в программе 1.1 выполняется при помощи двух функций:
glColor3d( 1, 0, 0 );
auxSolidSphere( 1 );
Функция glColor3d() устанавливает текущий цвет, а auxSolidSphere()
рисует сферу единичного радиуса с центром в начале координат.
Цвет в режиме RGBA (режим был задан в функции main()) задается четырьмя
числами в диапазоне от 0 до 1: красная компонента, синяя, зеленая и прозрачность. В
9
программе 1.1 прозрачность не нужна, поэтому вызывается вариант функции
glColor() с тремя параметрами. Значение четвертого параметра, прозрачности, по
умолчанию равно единице (1 – абсолютно непрозрачный материал, 0 – абсолютно
прозрачный).
OpenGL была разработана для языка Си, а не Си++, поэтому вместо перегруженных полиморфных функций в этой библиотеке реализованы наборы функций с
похожими именами, в которых условно обозначено количество параметров функции.
Имена полиморфных функций OpenGL выбраны согласно следующему правилу:
Имя функции[n=число параметров][тип параметров]
Тип параметров обозначается одной из английских букв:
'b' – байт со знаком (char или GLbyte)
's' – короткое целое (short или GLshort)
'i' – целое (int или GLint)
'f' – вещественное (float или GLfloat)
'd' – вещественное с двойной точностью (double или GLdouble)
'ub' – беззнаковый байт (unsigned char или GLubyte)
'us' – беззнаковое короткое целое (unsigned short или GLushort)
'ui' – беззнаковое целое (unsigned int или GLuint)
'v' – массив из n параметров указанного типа
Имя glColor3d() означает, что у функции есть три параметра типа double.
Например, есть еще функция glColor3i() с тремя параметрами типа int. Для целочисленных типов значение цветовой компоненты приводится к диапазону [0, 1] путем деления переданного значения на максимальное значение данного типа. Ниже
приведены три поясняющих примера:
double array[] = {0.5, 0.75, 0.3, 0.7};
glColor3dv(array);
// Цвет задается массивом типа double
glColor3ub(200, 100, 0);
// Преобразование 200/256,100/256,0/256
glColor3d(0.25, 0.25, 0);
// темно-желтый
glColor3ub(0, 100, 0); // темно-зеленый
glColor3ub(0, 0, 255); // ярко-синий
5. Системы координат
В OpenGL используются три системы координат: левосторонняя, правосторонняя и оконная. Первые две системы являются трехмерными и отличаются друг от
друга направлением оси z: в правосторонней она направлена на наблюдателя, а в левосторонней – от наблюдателя внутрь экрана. В большинстве случаев используется
правосторонняя (мировая) система координат. Левосторонняя система применяется
только для задания параметров проекционного преобразования. Отображение проекции трехмерной сцены производится в двумерной оконной системе координат, связанной с окном на экране.
Смысл преобразований трехмерных координат, необходимых для получения на
экране двумерного изображения трехмерной сцены, можно пояснить с помощью аналогии между OpenGL и фотоаппаратом (рис. 1.2). В обоих случаях для получения
изображения выполняются следующие шаги:
1) установка штатива и наведение фотоаппарата (видовое преобразование);
2) размещение фотографируемых объектов (модельное преобразование);
3) выбор объектива и/или настройка увеличения (проекционное преобр.);
10
4) выбор размера печатаемой фотографии (оконное преобразование).
Рис. 1.2. Аналогия между фотоаппаратом и OpenGL.
Рис. 1.3. Порядок выполнения преобразований координат вершины объекта.
В программах на OpenGL видовые преобразования следует задавать ранее модельных. Проекционное и оконное преобразования можно описывать в любом месте
программы до рисования объектов. В целом, порядок задания параметров преобразований может отличаться от строго определенного порядка выполнения математических операций над трехмерными координатами для получения двумерных экранных
координат (рис. 1.3).
5.1 Матрицы преобразований
В OpenGL различные преобразования объектов сцены описываются с помощью матриц размера 4x4. Есть три типа матриц: видовая, проекционная и текстурная.
Видовая матрица описывает преобразования объекта в мировых координатах: параллельный перенос, масштабирование и поворот. Проекционная матрица задает вид
проекции трехмерных объектов на плоскость экрана (в оконные координаты), а текстурная матрица управляет наложением текстуры на объект.
11
Перед вызовом функций, изменяющих матрицу определенного типа, сначала
необходимо установить эту матрицу в качестве текущей с помощью функции:
void glMatrixMode(GLenum mode)
Параметр mode принимает значения GL_MODELVIEW, GL_PROJECTION или
GL_TEXTURE.
Значения элементов текущей матрицы можно задать в явном виде функцией:
void glLoadMatrix[f d](GLtype* m)
где m указывает на 16-ти элементный массив типа float или double. В нем сначала
хранится первый столбец матрицы, затем второй, третий и четвертый.
Функция
void glLoadIdentity(void)
заменяет текущую матрицу на единичную. Содержимое текущей матрицы часто бывает нужно сохранить для дальнейшего использования. Для этого применяются
функции сохранения/восстановления матрицы из служебного стека OpenGL:
void glPushMatrix(void)
void glPopMatrix(void)
Для матриц каждого типа в OpenGL есть отдельный стек. Для видовых матриц
его глубина равна, как минимум, 32, для двух других типов матриц – минимум 2.
Для умножения текущей матрицы на другую матрицу справа используется
функция:
void glMultMatrix[f d](GLtype* m)
где m является указателем на матрицу размером 4x4. Однако чаще для изменения матриц в OpenGL удобно пользоваться специальными функциями, которые по
значениям параметров преобразований создают нужную матрицу и перемножают ее с
текущей. Чтобы сделать текущей созданную матрицу, надо перед вызовом этих
функций вызывать glLoadIdentity().
Теперь кратко рассмотрим преобразования, применяемые для отображения
трехмерных объектов сцены в окно приложения (рис. 1.3).
5.2 Видовые и модельные преобразования
Видовые и модельные преобразования задаются одной и той же матрицей – видовой, т.к. изменение местоположения и направления камеры эквивалентно некоторому преобразованию координат объектов сцены (и наоборот).
К видовым преобразованиям относятся перенос, поворот и изменение масштаба вдоль координатных осей. Для выполнения этих операций достаточно умножить
координаты каждой вершины объекта на соответствующую матрицу:
(xnew, ynew, znew, 1)T = M ⋅ (xold, yold, zold, 1)T
Матрицу M можно создать с помощью следующих функций:
void glTranslate[f d](GLtype dx, GLtype dy, GLtype dz)
void glRotate[f d](GLtype angle, GLtype x0, GLtype y0, GLtype z0)
void glScale[f d](GLtype x, GLtype y, GLtype z)
После создания матрицы преобразования это преобразование будет применяться ко всем далее рисуемым примитивам. В случае, если надо, например, повернуть
один объект сцены, а другой оставить неподвижным, сначала удобно сохранить текущую видовую матрицу в стеке функцией glPushMatrix(), затем вызвать
glRotate..() с нужными параметрами, описать примитивы, из которых состоит по12
ворачиваемый
объект, а затем восстановить текущую матрицу функцией
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) сцена будет выглядеть перевернутой.
Фактически, функция gluLookAt() совершает перенос и поворот всех объектов сцены, но в таком виде задавать параметры бывает удобнее.
5.3 Проекционное преобразование
В 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 и т.д. Функция gluOrtho2D() по умолчанию устанавливает расстояния до
ближней и дальней плоскостей отсечения: near=-1 и far=1.
Для задания перспективной проекции служит функция:
void gluPerspective( GLdouble fovy, GLdouble aspect,
GLdouble near, GLdouble far )
Эта функция задает видимый объем в форме усеченной пирамиды в левосторонней системе координат (рис. 1.4). Угол поля зрения (по оси у) fovy лежит в диапазоне от 0 до 180 градусов. Угол видимости по оси x задается параметром aspect
(обычно он вычисляется как отношение сторон экранного окна вывода). Параметры
far и near задают расстояние от наблюдателя до плоскостей отсечения по глубине и
должны быть положительными. Чем больше отношение far/near, тем хуже в буфере глубины будут различаться расположенные рядом поверхности, так как по умолчанию в него записываются значения глубины, нормированные в диапазоне от 0 до 1.
13
Рис. 1.4. Видимый объем, устанавливаемый с помощью функции gluPerspective().
Для задания перспективной проекции есть еще одна функция, с несколько другим набором параметров (рис 5):
void glFrustum( GLdouble left, GLdouble right, GLdouble bottom,
GLdouble top, GLdouble near, GLdouble far)
Рис. 1.5. Видимый объем, устанавливаемый с помощью функции glFrustum().
5.4 Оконное преобразование
После применения проекционной матрицы на вход следующего преобразования (перспективного деления, рис. 1.2) поступают так называемые усеченные
(clipped) координаты вершин, расположенных внутри видимого объема. Значения
всех компонент усеченных координат (xc, yc, zc, wc) находятся в диапазоне [-1,1].
Трехмерные нормированные координаты вершин вычисляются по формуле:
(xn, yn, zn) = (xc/wc, yc/wc, zc/wc)
Полученные нормированные координаты подвергаются оконному преобразованию. Согласно аналогии с фотоаппаратом (рис. 1.2), оконное преобразование соответствует этапу, на котором выбираются размеры получаемого двумерного изображения. Размер изображения на экране – область вывода – является прямоугольником,
заданным в оконной системе координат (рис. 1.6) с помощью функции:
void glViewPort( GLint x, GLint y, GLint width, GLint height )
Значения всех параметров задаются в пикселах и определяют ширину и высоту
области вывода с координатами левого нижнего угла (x, y). Размеры оконной системы
координат зависят от текущих размеров окна приложения, а точка-начало координат
(0, 0) располагается в левом нижнем углу окна.
14
Рис. 1.6. Вершины, лежащие внутри видимого объема, проектируются на его
переднюю стенку (ту, которая ближе к наблюдателю) и затем отображаются в
области вывода на экране.
Вычислим оконные координаты центра области вывода (cx, cy):
cx=x+width/2, cy=y+height/2.
Введем обозначения px=width, py=height. Оконные координаты каждой вершины вычисляются по формулам:
(xwin, ywin, zwin) = ( (px/2) xn + cx , (py/2) yn + cy , [(f-n)/2] zn+(n+f)/2 )
Целые положительные величины n и f ограничивают минимальную и максимальную глубину точек, которые могут попасть в область вывода (по умолчанию n=0
и f=1). Глубина каждой точки zwin записывается в специальный буфер глубины (zбуфер), с помощью которого OpenGL удаляет невидимые линии и поверхности. Установить собственные значения n и f можно вызовом функции
void glDepthRange( GLclampd n, GLclampd f )
Если у нескольких вершин совпадают двумерные координаты (xwin, ywin), то в
область вывода попадет вершина с минимальным значением глубины zwin.
В консольных приложениях функция glViewPort() обычно вызывается из
функции, зарегистрированной с помощью функции макета glutReshapeFunc() в
качестве функции-обработчика события изменения окна приложения.
6. Пример выполнения модельных преобразований
6.1 Параллельный перенос
Преобразование переноса рассмотрим на примере рисования сферы. Вызов
auxSolidSphere(0.5);
приводит к рисованию сферы радиусом 0.5 с центром в начале видовых координат, которое по умолчанию совпадает с началом мировой системы координат. Чтобы расположить центр сферы в точке (x0, y0, z0), надо переместить начало координат в
эту точку, т.е. надо перейти к новым координатам. При программировании графики и
анимации эта процедура выполняется очень часто. В ряде случаев после смещения
и/или поворота видовой системы координат расчет координат вершин объекта сильно
упрощается. Для переноса системы координат на вектор (dx, dy, dz) есть функция:
void glTranslated(double dx, double dy, double dz)
15
Применение этой функции демонстрируется в программе 1.2 (по сравнению с
программой-макетом 1.1 в этой программе реализована другая функция рисования
трехмерной сцены, состоящей из 3 сфер разного радиуса с центрами в разных точках).
void CALLBACK display(void)
{
// Очистка буфера кадра
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glPushMatrix();
glTranslated( 1.4, 0, 0);
glColor3d( 0, 1, 0 );
auxSolidSphere( 0.5 );
glTranslated( 1, 0, 0);
glColor3d( 0, 0, 1 );
auxSolidSphere( 0.3 );
}
// Сохранение текущей видовой матрицы
//
//
//
//
//
//
//
Перенос вдоль оси
Зеленый цвет
Рисование сферы с
в мировой системе
Перенос вдоль оси
Синий цвет
Рисование сферы с
Х на 1.4
центром в (1.4, 0, 0)
координат
Х на 1.0
центром в (2.4, 0, 0)
glPopMatrix();
// Восстановление сохраненной видовой
// матрицы (т.е. возврат к старой видовой
// системе координат)
glColor3d( 1, 0, 0);
auxSolidSphere( 0.75 );
// Красный цвет
// Рисование сферы с центром в (0, 0, 0)
// в мировой системе координат
// Копирование содержимого буфера кадра на экран
auxSwapBuffers();
Фрагмент программы 1.2
6.2 Поворот
Поворот видовой системы координат выполняет функция:
void glRotated(double angle, double x0, double y0, double z0)
Эта функция поворачивает систему координат на угол angle (в градусах) против часовой стрелки вокруг вектора (x0, y0, z0). Применение этой функции показано в
программе 1.3.
void CALLBACK display(void)
{
// Очистка буфера кадра
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glColor3d( 1, 0, 0 );
auxSolidCone( 1, 2 );
glPushMatrix();
glTranslated( 1, 0, 0 );
glRotated( 75, 1, 0, 0 );
glColor3d( 0, 1, 0 );
auxSolidCone( 1, 2 );
glPopMatrix();
}
//
//
//
//
//
//
//
//
Конус с центром основания в начале
координат. Радиус основания – 1,
высота конуса – 2, ось симметрии совпадает с положительным направлением оси Z
Cохранение текущей системы координат
Перенос начала координат в точку (1,0,0)
Поворот системы координат на 75 градусов
вокруг оси X.
// Еще один конус
// Возврат к сохраненной системе координат
// Копирование содержимого буфера кадра на экран
auxSwapBuffers();
Фрагмент программы 1.3
16
Выполнив программу 1.3, можно убедиться, что в мировой системе координат
конус оказался повернут. Итак, чтобы нарисовать объект не в начале координат, надо:
1) сохранить текущую систему координат;
2) выполнить сдвиг (glTranslated()) и/или поворот (glRotated());
3) нарисовать требуемые объекты;
4) вернуться к старой системе координат.
Вызовы glPushMatrix()/glPopMatrix() могут быть вложенными. Обычно
в исходном тексте пары этих функций выделяются отступами, например:
glPushMatrix();
...
glPushMatrix();
...
glPopMatrix();
...
glPopMatrix();
Количество вызовов glPopMatrix() должно соответствовать количеству вызовов glPushMatrix(), иначе будет получено неправильное изображение сцены.
Максимально допустимая глубина вложенности glPushMatrix()/glPopMatrix()
в OpenGL не меньше 32.
7. Сводка результатов
Описаны основные возможности OpenGL. Приведен макет консольного приложения, использующего OpenGL и вспомогательную библиотеку GLAUX. В этом
макете есть функции обратной связи (CALLBACK-функции), которые библиотека
GLAUX вызывает для рисования трехмерной сцены, для реакции на изменение размеров окна и некоторые другие события. Имена функций OpenGL подчиняются правилу, согласно которому в именах функций указывается тип параметров.
В OpenGL трехмерные координаты вершин рисуемых объектов подвергаются
набору преобразований, в результате которых вычисляются двумерные координаты
точек в экранном окне. Смысл преобразований можно пояснить с помощью аналогии
между OpenGL и фотоаппаратом. Преобразования координат задаются с помощью
матриц 4x4. В лекции приведен пример выполнения модельных преобразований переноса и поворота.
8. Упражнения
Упражнение 1
Создайте проект, состоящий из файла с исходным текстом программы 1.1 и
библиотечных файлов OpenGL (opengl32.lib, glu32.lib и glaux.lib). Скомпилируйте и
запустите программу. Попробуйте изменить цвет сферы, пользуясь примерами функции glColor3..() из п. 4.
17
Упражнение 2
С помощью перечисленных ниже функций нарисуйте стандартные фигуры
библиотеки GLAUX: куб, параллелепипед и др. Значения параметров функций выбирайте в диапазоне 0.5-1.7 (фигура слишком большого размера будет выходить за пределы видимого объема).
Фигура
куб
параллелепипед
тор
цилиндр
конус
икосаэдр
октаэдр
тетраэдр
додекаэдр
чайник
Функция GLAUX
auxSolidCube( width )
auxSolidBox(
width,
height,
depth )
auxSolidTorus( r, R )
auxSolidCylinder( r, height )
auxSolidCone( r, height )
auxSolidIcosahedron( width )
auxSolidOctahedron( width )
auxSolidTetrahedron( width )
auxSolidDodecahedron( width )
auxSolidTeapot( width )
В таблице приведены имена функций для рисования сплошных фигур. В
GLAUX есть также аналогичные функции (вместо Solid имена этих функций содержат слово Wire) для рисования каркасных фигур. Например, для рисования каркаса куба надо вызвать функцию:
auxWireCube(1);
Упражнение 3
С помощью функций, перечисленных в упр.2, нарисуйте стандартные фигуры
библиотеки GLAUX в четыре столбца (по 5 фигур в каждом столбце): в 1-м и 3-м
столбцах слева сплошные фигуры, во 2-м и 4-м – каркасные.
Упражнение 4
Изобразите оси координат и радиус-вектор точки (3, 3, 3). Для рисования отрезков используйте цилиндры малого диаметра, для рисования стрелок – конусы. Ось
X покажите красным цветом, ось Y – зеленым, ось Z – синим. В начало координат и в
точку (3,3,3) поместите сферу небольшого радиуса.
Рисование осей координат оформите в виде отдельной функции. Ее можно будет использовать в других программах на OpenGL в отладочных целях (например,
можно нарисовать оси и посмотреть расположение объектов сцены относительно координатных осей).
Примечание: функция auxSolidCylinder() рисует цилиндр, ориентированный по отрицательному направлению оси Y видовой системы координат, причем
центр нижнего основания цилиндра располагается в точке (0, 0, 1).
Упражнение 5
Нарисуйте каркасный параллелепипед, у которого длины сторон, параллельных
координатным осям X, Y, Z, находятся в отношении 1:1:10. Задайте такие параметры
функции glOrtho(), чтобы параллелепипед целиком попадал в видимый объем.
18
Убедитесь, что стороны, параллельные оси Z, в проекции на экране остаются параллельными.
Чтобы не подбирать параметры освещения, в данном случае можно отключить
моделирование направленного освещения и установить максимальную интенсивность
рассеянного освещения. Для этого удалите из своей программы функции включения
нулевого источника света и функции настройки его параметров:
glEnable( GL_LIGHT0 ); // Включение нулевого источника света
// Задание положения и направления нулевого источника света
float pos[4] = { 3, 3, 3, 1 };
float dir[3] = { -1, -1, -1 };
glLightfv( GL_LIGHT0, GL_POSITION, pos );
glLightfv( GL_LIGHT0, GL_SPOT_DIRECTION, dir );
Вместо этих функций включите функцию, задающую максимальную интенсивность рассеянного освещения:
float ambient[4] = { 1.0, 1.0, 1.0, 1 };
glLightModelfv( GL_LIGHT_MODEL_AMBIENT, ambient );
Смените тип проекции с ортографической на перспективную (glFrustum()).
Регулируя угол раскрытия видимого объема, добейтесь, чтобы параллелепипед попадал в него целиком. Убедитесь, что параллельность сторон параллелепипеда вдоль
оси Z при перспективной проекции не сохраняется.
Упражнение 6
С помощью функций glTranslate() и glRotate() нарисуйте снеговика
(туловище – три сферы разного радиуса, руки – сферы, шапка – конус, нос – тоже
конус, глаза – сферы, рот – параллелепипед).
19
ЛЕКЦИЯ 2. Генерация движущихся изображений
1. Анимация с двойной буферизацией
Создание движущихся изображений – это одна из наиболее впечатляющих
возможностей компьютерной графики. С помощью системы объемного проектирования инженер может с различных сторон рассмотреть спроектированный механизм и
увидеть его в движении на экране компьютера. Компьютерные авиационные тренажеры применяются при подготовке летчиков. В области развлечений очень распространены компьютерные игры с мультипликацией. Во всех перечисленных примерах
анимация является необходимой частью компьютерной графики.
При демонстрации фильма в кинотеатре на экран проектируются неподвижные
изображения, которые быстро сменяют друг друга (24 кадра в секунду). В киноаппарате каждый кадр кинопленки останавливается перед объективом, затем открывается
затвор и кадр проектируется на экран. Затвор очень быстро закрывается и пленка перемещается на следующий кадр. Затем этот кадр показывается на экране, и т.д. Хотя
каждую секунду зритель видит 24 различных кадра, человеческий мозг не в состоянии различить отдельные изображения и видит плавное движение (старые киноленты
с Чарли Чаплином снимались со скоростью 16 кадров в секунду и поэтому персонажи
выглядят заметно дергающимися). Большинство современных кинопроекторов показывают каждый кадр дважды, так что изображения меняются с частотой 48 Гц и за
счет этого уменьшается мерцание. Компьютерные видеоадаптеры обновляют изображение с кадровой частотой от 60 до 120 Гц. Разница между 60 Гц и 30 Гц на глаз видна, а между 120 Гц и 60 Гц практически незаметна. Повышение кадровой частоты
выше 120 Гц из-за инерционности зрения практического смысла не имеет.
Главная особенность кинопроекторов, обеспечивающая естественность движения на киноэкране, заключается в том, что каждый кадр проектируется на экран сразу
целиком. Допустим, что есть программа для создания анимации из миллиона кадров:
open_window();
// Открытие окна на экране
for ( i = 0; i < 1000000; i++ ) {
clear_the_window(); // Очистка окна
draw_frame(i);
// Рисование i-го кадра
wait_24th();
// Ожидание завершения промежутка в 1/24 с
}
Очистка окна и рисование кадра требует некоторого времени. Чем ближе это
время к 1/24 с, тем хуже выглядит анимация. Допустим, рисование кадра занимает
почти 1/24 с. Получается, что элементы кадра, которые рисуются первыми, присутствуют на экране весь промежуток 1/24 с и выглядят нормально. Но элементы, которые
рисуются последними, удаляются практически сразу и программа приступает к рисованию следующего кадра. В результате изображение мерцает, т.к.. большую часть от
1/24 с на месте элементов кадра, которые рисуются последними, виден очищенный
фон. Проблема заключается в том, что программа не сразу отображает целый кадр, а
рисует его постепенно на глазах у наблюдателя.
Простейшее решение этой проблемы – двойная буферизация. Программа работает с двумя буферами кадра. Пока один буфер показывается на экране, в другом буфере программа рисует следующий кадр. Когда рисование кадра заканчивается, программа обменивает буфера местами, так что тот, который показывался на экране, будет использоваться для рисования, и наоборот. Этот способ компьютерной мультип20
ликации напоминает кинопроектор с пленкой из двух кадров: пока один кадр показывается на экране, художник отчаянно стирает содержимое другого кадра и рисует на
нем новую картинку. Если художник рисует достаточно быстро, то зритель не заметит разницы между таким фильмом и фильмом, в котором все кадры были нарисованы заранее и проектор показывает их один за другим. При двойной буферизации, каждый кадр показывается только после того, как его рисование будет полностью завершено, так что зритель никогда не увидит частично нарисованный кадр.
Улучшенный вариант приведенной выше программы для демонстрации плавной анимации может выглядеть примерно так:
open_win_dbl_buf(); // Открытие окна в режиме двойной буферизации
for ( i = 0; i < 1000000; i++ ) {
clear_the_window(); // Очистка окна
draw_frame(i);
// Рисование i-го кадра
swap_the_buffers(); // Обмен буферов
}
Функция swap_the_buffers() не просто обменивает видимый и невидимый
буфера, перед этим она дожидается завершения текущей операция обновления экрана
для показа предыдущего буфера. Эта функция гарантирует, что новый буфер будет
показан полностью. Допустим, что кадровая частота видеоадаптера 60 Гц. Значит, вы
можете достичь скорости максимум 60 кадров в секунду, если успеете очистить и нарисовать каждый кадр менее, чем за 1/60 с.
Однако часто бывает, что кадр слишком сложен и его не удается нарисовать за
1/60 с. Тогда каждый кадр выводится на экран несколько раз. Если, например, кадра
прорисовывается около 1/45 с, то возможна анимация с частотой 30 кадров в секунду,
а время простоя составит 1/30-1/45=1/90 с на кадр. Хотя 1/90 с кажется небольшим
промежутком времени, но оно теряется на каждом промежутке в 1/30 с, так что в действительности время простоя составляет 1/3 всего времени работы программы.
Тот факт, что кадровая частота является константой, может неожиданным образом повлиять на производительность программы. Например, с дисплеем 60 Гц вы
можете выводить анимацию с частотой 60 Гц, 30 Гц, 20 Гц, 15 Гц, 12 Гц и т.д. (60/1,
60/2, 60/3, 60/4, 60/5, ...). Допустим, вы пишете программу и постепенно добавляете к
ней новые возможности (например, в авиационный симулятор добавляете усложненные наземные сцены). Сначала эти новые возможности не сказываются на общей
производительности программы – вы продолжаете показывать 60 кадров в секунду.
Внезапно, после очередного усовершенствования, производительность программы
падает в 2 раза, т.к. программа не успевает нарисовать весь кадр за 1/60 с и пропускает момент, когда можно поменять местами два буфера. Похожая вещь происходит.
когда время рисования кадра начинает превышать 1/30 с – тогда производительность
падает с 30 до 20 кадров в секунду, т.е. на треть.
Еще одна проблема заключается в том, что если время рисования кадров непостоянно и близко к магическим числам (в нашем случае 1/60 с, 2/60 с, 3/60 с и т.д.),
причем это время меняется случайно, то и скорость анимации непостоянна и в изображение может выглядеть дергающимся. В данном случае, если никак не удается
упростить содержимое кадров, то можно добавить маленькую задержку и кадры всегда будут прорисовываться более, чем за 1/60 с и получится постоянная, хотя и меньшая, кадровая частота анимации.
Структура настоящих анимационных программ не слишком сильно отличается
от приведенного описания. Обычно для каждого кадра заново прорисовывается целый
21
буфер, т.к. это сделать проще, чем вычислить, какие именно части кадра требуют перерисовки. Это особенно верно для таких приложений, как авиационные симуляторы,
где совсем небольшое изменение ориентации самолета изменяет положение всех объектов, попадающих в кадр.
В OpenGL нет встроенной функции swap_the_buffers(), т.к. ее устройство
зависит от конкретного видеоадаптера и от операционной системы с графическим интерфейсом. Поэтому эта функция реализована во вспомогательных библиотеках, например, в библиотеке GLAUX:
void auxSwapBuffers(void);
Чтобы циклически выполнять рисование кадров, можно зарегистрировать
функцию рисования кадра как фоновую функцию обратной связи. GLAUX вызывает
фоновую функцию, когда нет команд от пользователя. Регистрация фоновой функции
выполняется с помощью функции:
void auxIdleFunc( void (*)(void) );
Применение фоновой функции рисования кадра в режиме двойной буферизации показано в программе 2.1, которая рисует изображение вращающегося чайника.
#include
#include
#include
#include
<windows.h>
<GL/gl.h>
<GL/glu.h>
<GL/glaux.h>
void CALLBACK resize( int width, int height );
void CALLBACK display( void );
void main()
{
auxInitDisplayMode( AUX_RGBA | AUX_DEPTH | AUX_DOUBLE );
auxInitPosition( 50, 10, 400, 400);
auxInitWindow( "Лекция 2, Программа 2.1" );
auxReshapeFunc( resize );
glEnable( GL_ALPHA_TEST );
glEnable( GL_DEPTH_TEST );
glEnable( GL_COLOR_MATERIAL );
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glEnable( GL_LIGHTING );
glEnable( GL_LIGHT0 );
float pos[4] = { 0, 5, 5, 1 };
float dir[3] = { 0, -1, -1 };
glLightfv( GL_LIGHT0, GL_POSITION, pos );
glLightfv( GL_LIGHT0, GL_SPOT_DIRECTION, dir );
auxIdleFunc( display );
auxMainLoop( display );
}
void CALLBACK resize( int width, int height )
{
glViewport( 0, 0, width, height );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
glOrtho( -5, 5, -5, 5, 2, 12 );
22
gluLookAt( 3,0,5, 0,0,0, 0,1,0 );
}
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
void CALLBACK display(void)
{
// Очистка буфера кадра
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
static float angle = 0;
glColor3f( 0.5, 0.5, 0 );
glPushMatrix();
glRotatef( angle, 0, 1, 0 );
auxWireTeapot( 1 );
glPopMatrix();
angle += 5;
if ( angle > 360 )
angle -= 360;
// Копирование содержимого буфера кадра на экран
glFlush();
auxSwapBuffers();
}
Программа 2.1. Анимация с двойной буферизацией.
2. Обработка событий клавиатуры и мыши
При использовании библиотеки GLAUX предполагается, что программа разработана в соответствии с некоторой структурой-макетом: сначала создается окно приложения, затем устанавливаются параметры OpenGL, регистрируются функции обратной связи и запускается функция главного цикла. Макет консольного приложения
был приведен в предыдущей лекции (п.3) и с тех пор он используется во всех рассматриваемых примерах. Функции обратной связи реализуются программистом, а вызываются изнутри GLAUX. Большую часть времени программа проводит в главном
цикле внутри функции auxMainLoop():
void auxMainLoop( void (CALLBACK* display)(void) )
{
вызвать функцию реакции на изменение размеров окна;
вызвать функцию рисования сцены display();
while ( не закрыто окно приложения )
{
if ( изменен размер окна )
вызвать функцию реакции на изменение размера окна;
if ( нажата клавиша )
вызвать функцию обработки этой клавиши;
if ( нажата кнопка мыши )
вызвать функцию обработки этой кнопки;
if ( было перемещение мыши при нажатой кнопке )
вызвать функцию обработки перемещения мыши;
}
вызвать фоновую функцию;
}
23
Т.о., из главного цикла GLAUX могут вызываться функции обратной связи, в
которых программист определил реакцию на некоторые внешние события. Эти функции обычно называются обработчиками событий. Чтобы GLAUX вызывала их, обработчики надо зарегистрировать до входа в главный цикл.
Обработчик события от клавиатуры должен соответствовать прототипу:
void CALLBACK FunctionName(void);
где FunctionName – произвольное имя, выбираемое программистом.
Для регистрации функции FunctionName() в качестве обработчика нажатия
кнопки клавиатуры служит функция:
void auxKeyFunc(int keyCode, void (CALLBACK* FunctionName)(void));
где keyCode – код клавиши (соответствующие константы хранятся в файле glaux.h,
например, AUX_ESCAPE и AUX_A).
Прототип обработчика событий мыши отличается от клавиатурного обработчика:
void CALLBACK FunctionName(AUX_EVENTREC* event);
При вызове обработчика GLAUX передает ему информацию о произошедшем
событии в параметре event типа AUX_EVENTREC. Эта структура имеет вид:
struct AUX_EVENTREC
{
GLint event;
GLint data[4];
//
//
//
//
//
//
//
Тип
или
[0]
[1]
[2]
[3]
события: AUX_MOUSEDOWN, AUX_MOUSEUP,
AUX_MOUSELOC
горизонтальная координата мыши
вертикальная координата мыши
не используется
код нажатой кнопки (AUX_LEFTBUTTON,
AUX_MIDDLEBUTTON или AUX_RIGHTBUTTON)
};
Для регистрации обработчика события от мыши надо использовать функцию:
void auxMouseFunc( int button, int action,
void (CALLBACK* FunctionName)(AUX_EVENTREC*) );
Параметр button задает кнопку, с которой связано событие: AUX_LEFTBUTTON
(левая), AUX_MIDDLEBUTTON (средняя), AUX_RIGHTBUTTON (правая).
Параметр action задает тип события: AUX_MOUSEDOWN (нажатие кнопки),
AUX_MOUSEUP (отпускание кнопки), AUX_MOUSELOC (перемещение мыши при нажатой кнопке).
2.1 Пример обработки события от мыши: изменение цвета вращающегося объекта
по нажатию левой кнопки мыши
Перечислим изменения, которые необходимо внести в программу 2.1, чтобы по
нажатию левой кнопки мыши цвет вращающегося чайника циклически изменялся в
следующей последовательности: желтый, красный, зеленый, синий.
1) Надо добавить в программу описание глобальной переменной, в который будет
храниться порядковый номер текущего цвета (0/1/2/3 – желтый/красный/зеленый/синий):
int clr_number = 0;
24
2) Значение переменной clr_number надо учесть в функции рисования трехмерной
сцены, чтобы перед рисованием чайника устанавливался соответствующий цвет:
switch ( clr_number )
{
case 0 : glColor3f(
case 1 : glColor3f(
case 2 : glColor3f(
case 3 : glColor3f(
default : glColor3f(
}
0.5, 0.5, 0 ); break;
1, 0, 0 ); break;
0, 1, 0 ); break;
0, 0, 1 ); break;
1, 1, 1 ); break;
3) В раздел прототипов надо внести прототип обработчика события от мыши:
void CALLBACK mouse_leftbtn( AUX_EVENTREC* event );
4) В функции-обработчике выполняется изменение номера текущего цвета:
void CALLBACK mouse_leftbtn( AUX_EVENTREC* event )
{
if ( ++clr_number == 3 )
clr_number = 0;
}
5) Перед входом в главный цикл GLAUX надо зарегистрировать обработчик события
"нажатие левой кнопки мыши":
auxMouseFunc( AUX_LEFTBUTTON, AUX_MOUSEDOWN, mouse_leftbtn );
3. Композиция нескольких преобразований
Для генерации анимационных изображений объектов, движущихся относительно друг друга, часто бывает удобно применять последовательность нескольких
преобразований, параметры которых определяются текущим положением объектов. В
данном параграфе показывается применение композиции нескольких преобразований
для двух моделей: для простейшей модели солнечной системы (несколько объектов
вращаются вокруг собственных осей и по орбитам вокруг солнца) и для модели манипулятора робота (при отображении манипулятора требуется выполнять преобразование координатных систем отдельных сегментов).
3.1 Модель солнечной системы
Рассмотрим простейшую модель солнечной системы, которая состоит из солнца и одной планеты, движущейся по круговой орбите. Планета совершает полный
оборот по орбите за 365 дней, а оборот вокруг своей оси за 24 часа.
Оба тела рисуются в виде каркасных сфер, для отображения сцены применяется перспективная проекция (функция gluPerspective()).
Угловое движение планеты по орбите и вокруг собственной оси учитывается с
помощью функции glRotated(). Для размещения планеты на орбите применяется
функция glTranslated().
Центр сферы, изображающей солнце, находится в начале координат. Вращение
солнца вокруг собственной оси не показывается (хотя это легко сделать с помощью
функции glRotated()). Для рисования планеты, обращающейся вокруг солнца
(рис. 2.1), требуется выполнить несколько модельных преобразований.
25
Рис. 2.1. Движение планеты по орбите и вокруг своей оси.
Для определения порядка модельных преобразований надо представить, что
должно происходить с модельной системой координат. Сначала модельная система
координат совпадает с мировой. В этом состоянии надо функцией glRotated() повернуть модельную систему координат относительно мировой системы на угол, соответствующий текущему положению планеты на орбите. Затем glTranslated() выполняет перенос модельной системы координат по радиусу орбиты. После этого выполняется еще один вызов glRotated(), поворачивающий модельную систему координат вокруг оси вращения планеты в соответствии с временем суток на планете.
После выполнения всех трех преобразований, можно нарисовать планету.
Описанные преобразования выполняются в программе 2.2. Фоновой функции в
этой программе нет, поэтому изменение времени производится клавишами курсора:
увеличение/уменьшение времени суток с помощью стрелок вверх/вниз (на 1 час),
дней – с помощью стрелок вправо/влево (на 1 день).
#include
#include
#include
#include
void
void
void
void
void
void
<windows.h>
<GL/gl.h>
<GL/glu.h>
<GL/glaux.h>
CALLBACK
CALLBACK
CALLBACK
CALLBACK
CALLBACK
CALLBACK
resize( int width, int height );
display();
dayAdd();
daySubtract();
hourAdd();
hourSubtract();
// Счетчики дней и часов
int day_cnt = 0, hour_cnt = 0;
void main()
{
// Создание экранного окна
auxInitDisplayMode( AUX_RGBA | AUX_DEPTH | AUX_DOUBLE );
auxInitPosition( 50, 10, 400, 400);
auxInitWindow( "Лекция 2, Программа 2.2" );
// Включение ряда параметров OpenGL
glEnable( GL_ALPHA_TEST );
// Учет прозрачности
glEnable( GL_DEPTH_TEST );
// Удаление невидимых поверхностей
glEnable( GL_COLOR_MATERIAL );
glEnable( GL_BLEND );
// Разрешение смешения цветов
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glEnable( GL_LIGHTING );
glEnable( GL_LIGHT0 );
// Учет освещения
// Включение нулевого источника света
26
// Задание положения и направления нулевого источника света
float pos[4] = { 5, 5, 5, 1 };
float dir[3] = { -1, -1, -1 };
glLightfv( GL_LIGHT0, GL_POSITION, pos );
glLightfv( GL_LIGHT0, GL_SPOT_DIRECTION, dir );
// Регистрация обработчиков событий
auxReshapeFunc( resize );
auxKeyFunc( AUX_LEFT, daySubtract );
auxKeyFunc( AUX_RIGHT, dayAdd );
auxKeyFunc( AUX_UP, hourAdd );
auxKeyFunc( AUX_DOWN, hourSubtract );
}
// Вход в главный цикл GLAUX
auxMainLoop( display );
void CALLBACK resize( int width, int height )
{
glViewport( 0, 0, width, height );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
gluPerspective( 60.0, (float)width/(float)height, 1.0, 20.0 );
gluLookAt( 0,0,5, 0,0,0, 0,1,0 );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
}
void CALLBACK display(void)
{
// Очистка буфера кадра
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glColor3f( 1.0, 1.0, 1.0 );
glPushMatrix();
auxWireSphere( 1.0 );
// Солнце
glRotated( (double)day_cnt*360.0/365.0, 0.0, 1.0, 0.0 );
glTranslated( 2.0, 0.0, 0.0 );
glRotated( (double)hour_cnt*360.0/24.0, 0.0, 1.0, 0.0 );
auxWireSphere( 0.2 );
// Планета
glPopMatrix();
// Копирование содержимого буфера кадра на экран
glFlush();
auxSwapBuffers();
}
void CALLBACK dayAdd()
{
day_cnt = (day_cnt + 1) % 360;
}
void CALLBACK daySubtract()
{
day_cnt = (day_cnt - 1) % 360;
}
void CALLBACK hourAdd()
27
{
}
hour_cnt = (hour_cnt + 1) % 24;
void CALLBACK hourSubtract()
{
hour_cnt = (hour_cnt - 1) % 24;
}
Программа 2.2. Модель солнечной системы.
3.2 Модель манипулятора робота
Манипулятор робота на экране изображается в виде нескольких каркасных параллелепипедов (по одному для каждого сегмента манипулятора – "плечо", "локоть",
"кисть", "пальцы"). Предполагается, что в местах соединения сегментов расположены
шарниры и сегменты могут вращаться вокруг них. На рис. 2.2 и рис. 2.3 показаны
манипуляторы с разным количеством сегментов.
Рис. 2.2. Двухсегментный манипулятор.
Рис. 2.3. Манипулятор из 10 сегментов (плечо,
локоть и 4 пальца).
Перед рисованием каждого сегмента надо выполнить модельное преобразование, обеспечивающее корректную ориентацию сегмента. При рисовании параллелепипеда его центр располагается в начале модельной системы координат, поэтому
предварительно надо сместить модельную систему координат на половину длины
сегмента. Если этого не сделать, то сегмент будет вращаться вокруг своего центра, а
не вокруг шарнира.
После вызова glTranslated(), который задает положение шарнира, надо вызвать glRotated() для поворота сегмента. После этого надо выполнить перенос в
обратном направлении для правильного позиционирования центра сегмента. В целом,
для рисования одного сегмента надо вызвать следующие функции:
glTranslatef( -1.0, 0.0, 0.0 );
glRotatef( (float)shoulder_angle, 0.0, 0.0, 1.0 );
glTranslatef( 1.0, 0.0, 0.0 );
auxWireBox( 2.0, 0.4, 1.0 );
Для рисования второго сегмента надо переместить модельную систему координат в позицию второго шарнира. Т.к. координатная система уже была повернута, то
ось X направлена в направлении первого сегмента. Следовательно, для переноса модельной системы координат в точку второго шарнира надо произвести перенос вдоль
28
оси X. После этого можно нарисовать второй сегмент (функциями, аналогичными использованным для рисования первого сегмента):
glTranslatef( 1.0, 0.0, 0.0 );
glRotatef( (float)elbow_angle, 0.0, 0.0, 1.0 );
glTranslatef( 1.0, 0.0, 0.0 );
auxWireBox( 2.0, 0.4, 1.0 );
Описанную процедуру можно продолжать повторять, чтобы нарисовать все
сегменты манипулятора (плечо, локоть, кисть, пальцы). Рисование двух сегментов
манипулятора с двумя степенями свободы показано в программе 2.3. Поворот сегментов осуществляется с помощью клавиш курсора.
#include
#include
#include
#include
void
void
void
void
void
void
<windows.h>
<GL/gl.h>
<GL/glu.h>
<GL/glaux.h>
CALLBACK
CALLBACK
CALLBACK
CALLBACK
CALLBACK
CALLBACK
resize( int width, int height );
display();
elbowAdd();
elbowSubtract();
shoulderSubtract();
shoulderAdd();
// Текущие углы поворота плеча и локтя
int shoulder = 0, elbow = 0;
void main()
{
// Создание экранного окна
auxInitDisplayMode( AUX_RGBA | AUX_DEPTH | AUX_DOUBLE );
auxInitPosition( 50, 10, 400, 400);
auxInitWindow( "Лекция 2, Программа 2.3" );
// Включение ряда параметров OpenGL
glEnable( GL_ALPHA_TEST );
glEnable( GL_DEPTH_TEST );
glEnable( GL_COLOR_MATERIAL );
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glEnable( GL_LIGHTING );
// Включение максимального рассеянного освещения
float ambient[4] = { 1.0, 1.0, 1.0, 1 };
glLightModelfv( GL_LIGHT_MODEL_AMBIENT, ambient );
// Регистрация обработчиков событий
auxReshapeFunc( resize );
auxKeyFunc( AUX_LEFT, shoulderSubtract );
auxKeyFunc( AUX_RIGHT, shoulderAdd );
auxKeyFunc( AUX_UP, elbowAdd );
auxKeyFunc( AUX_DOWN, elbowSubtract );
}
// Вход в главный цикл GLAUX
auxMainLoop( display );
void CALLBACK resize( int width, int height )
{
glViewport( 0, 0, width, height );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
29
gluPerspective( 60.0, (float)width/(float)height, 1.0, 20.0 );
gluLookAt( 0,0,10, 0,0,0, 0,1,0 );
}
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
void CALLBACK display()
{
// Очистка буфера кадра
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glColor3f( 1.0, 1.0, 1.0 );
glPushMatrix();
glTranslatef( -1.0, 0.0, 0.0 );
glRotatef( (float)shoulder, 0.0, 0.0, 1.0 );
glTranslatef( 1.0, 0.0, 0.0 );
auxWireBox( 2.0, 0.4, 1.0 );
// плечо
glTranslatef( 1.0, 0.0, 0.0 );
glRotatef( (float)elbow, 0.0, 0.0, 1.0 );
glTranslatef( 1.0, 0.0, 0.0 );
auxWireBox( 2.0, 0.4, 1.0 );
// локоть
glPopMatrix();
}
// Копирование содержимого буфера кадра на экран
glFlush();
auxSwapBuffers();
void CALLBACK elbowAdd()
{
elbow = (elbow + 5) % 360;
}
void CALLBACK elbowSubtract()
{
elbow = (elbow - 5) % 360;
}
void CALLBACK shoulderAdd()
{
shoulder = (shoulder + 5) % 360;
}
void CALLBACK shoulderSubtract()
{
shoulder = (shoulder - 5) % 360;
}
Программа 2.3. Модель манипулятора робота.
4. Сводка результатов
Для создания плавной анимации применяется метод "двойная буферизация".
При этом используются два буфера кадра: один показывается на экране, а в другом,
невидимом, выполняется рисование следующего кадра.
В макете консольной программы, написанной с помощью библиотеки GLAUX,
используются функции обратной связи. Чаще всего это функция отображения сцены,
фоновая функция и функция реакции на изменение размеров окна. Кроме того, можно
определить функции-обработчики событий от клавиатуры и мыши.
30
При создании анимации с несколькими движущимися объектами важно правильно построить композицию преобразований модельной системы координат. В
лекции приведены два примера подобных моделей.
5. Упражнения
Упражнение 1
Внесите в программу 2.1 изменения, описанные в п.2.1, чтобы по нажатию левой кнопки мыши менялся цвет вращающегося чайника.
Упражнение 2
В программу из упр.1 добавьте обработчик нажатия правой кнопки мыши для
включения/выключения вращения. Сделайте обработчики для клавиш курсора
"стрелка вверх" и "стрелка вниз" (коды клавиш AUX_LEFT и AUX_UP), чтобы увеличивать и уменьшать скорость вращения. С помощью обработчика клавиши "пробел"
(код AUX_SPACE) обеспечьте циклическое переключение осей вращения между тремя
координатными осями.
Подсказка: можно завести глобальную переменную-флаг, обозначающую,
включено вращение или нет. Переключение флага следует выполнять в обработчике
события. В зависимости от состояния флага в функции фоновой обработки должно
или не должно выполняться увеличение угла поворота. Аналогичные приемы используйте при написании обработчиков остальных событий.
Упражнение 3
В программу 2.2 добавьте отображение спутника планеты и наклоните ось
вращения планеты под углом 45 градусов к плоскости ее орбиты.
Упражнение 4
Сделайте вариант программы 2.2 с 4-мя планетами и 3-мя спутниками. Для сохранения/восстановления позиции и ориентации координатных систем пользуйтесь
функциями glPushMatrix() и glPopMatrix(). Чтобы нарисовать несколько спутников у одной планеты, сохраняйте параметры координатной системы перед рисованием каждого спутника и восстанавливайте их после рисования спутника. Чтобы разобраться с положением координатных осей, можете применить функцию рисования
осей из 4-го упражнения к 1-й лекции.
Упражнение 5
Измените программу 2.3 так, чтобы она рисовала манипулятор из 10-ти сегментами (рис. 2.3). Для поворота всех сегментов применяйте две клавиши курсора
"стрелка вверх/вниз". Текущий сегмент (который будет поворачиваться этими клавишами) должен быть выделен цветом. Переключение текущего сегмента должно производиться циклически по нажатию пробела.
Подсказка: для сохранения и восстановления позиции и ориентации координатной системы, связанной с кистью манипулятора, применяйте функции
glPushMatrix() и glPopMatrix(). При рисовании пальцев перед позиционированием каждого пальца сохраняйте текущую видовую матрицу, а после рисования – восстанавливайте ее.
31
ЛЕКЦИЯ 3. Геометрические примитивы
С помощью OpenGL можно рисовать очень сложные сцены, но все они в конечном счете состоят из геометрических примитивов нескольких типов – точек, отрезков и многоугольников. Стандартные тела библиотеки GLAUX (например, сферы,
конусы и параллелепипеды) тоже состоят из геометрических примитивов OpenGL.
Конечно, на реалистичных изображениях есть много кривых линий и поверхностей. Например, на рис. 1.1 в первой лекции есть круглый стол и объекты с кривыми поверхностями. В действительности все кривые линии и поверхности аппроксимируются большим количеством маленьких плоских многоугольников и отрезков
прямых.
Все операции рисования в OpenGL можно разделить на три типа: служебные
операции (такие, как очистка буфера и задание цвета), рисование геометрических
примитивов и рисование растровых объектов. Растровые объекты – это двумерные
изображения, битовые карты и символьные шрифты. В данной лекции рассматриваются операции двух первых типов, растровые объекты описываются в следующих
лекциях.
1. Служебные графические операции
1.1 Очистка окна
В OpenGL для хранения изображения кадра выделяется некоторая область памяти – цветовой буфер, в который записываются значения цветов всех пикселей кадра. Поскольку при рисовании последовательных кадров используется один и тот же
буфер, его необходимо очищать – заполнять некоторым фоновым цветом, выбор которого зависит от конкретной программы. Например, в текстовом редакторе белый
фоновый цвет, а в симуляторе космического корабля – черный. Иногда буфер вообще
можно не очищать, например, при рисовании внутреннего интерьера помещения
стенки помещения обязательно занимают весь кадр целиком.
Специальная функция очистки glClear() работает быстрее, чем функция рисования прямоугольника фонового цвета размером во весь кадр. Кроме того, в
OpenGL буфер кадра может состоять из нескольких буферов. Чаще всего их два: цветовой буфер и буфер глубины (см. п.1.3). Можно очищать сразу все буфера или только некоторые. Например, следующие функции заполняют черным цветом цветовой
буфер:
glClearColor( 0.0, 0.0, 0.0, 0.0 );
glClear( GL_COLOR_BUFFER_BIT );
Функция glClearColor() задает фоновый цвет (см. п.1.2). Обычно он указывается только один раз, и OpenGL запоминает его в своих внутренних переменных. В
качестве параметра glClear() передается комбинация битовых флагов, обозначающих типы очищаемых буферов. Например, для очистки сразу двух буферов – цветового и буфера глубины – надо вызвать функции:
glClearColor( 0.0, 0.0, 0.0, 0.0 );
glClearDepth( 0.0 );
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
32
1.2 Задание цвета
В OpenGL форма и цвет геометрических объектов задаются независимо. Каждый объект рисуется с учетом текущей цветовой схемы. Цветовую схему можно пояснить словами вроде "рисовать все ярко-красным цветом" или более сложно "предположим, что объект сделан из синего пластика, который освещается желтым прожектором с такого-то и такого-то направления, и что есть общее рассеянное слабое
красновато-коричневое освещение". В целом, при работе с OpenGL программист
сначала должен задать цвет и/или цветовую схему, а потом рисовать объекты. Пока
цвет и цветовая схема не будут изменены, все объекты будут рисоваться в соответствии с ними.
Рассмотрим пример (имена функций вымышлены):
set_current_color( red );
draw_object( A );
draw_object( B );
set_current_color( green );
set_current_color( blue );
draw_object( C );
В этом примере объекты A и B рисуются красным цветом, а объект C – синим.
Вызов функции в четвертой строке лишний, так как зеленым цветом не рисуется ни
один объект.
Цветовые схемы, освещение и тонирование – это довольно обширные темы,
которые будут рассматриваться в следующих лекциях. Пока, для рисования геометрических примитивов, достаточно знать, как установить текущий цвет. Для этого
применяется функция glColor3d(). У нее три параметра: вещественные числа от 0.0
до 1.0. Они задают красную, зеленую и синюю компоненты цвета. Ниже приведены
несколько наборов компонент для часто используемых цветов:
(0.0,
(1.0,
(0.0,
(1.0,
(0.0,
0.0,
0.0,
1.0,
1.0,
0.0,
0.0)
0.0)
0.0)
0.0)
1.0)
–
–
–
–
–
черный
красный
зеленый
желтый
синий
(1.0,
(0.0,
(0.5,
(1.0,
0.0,
1.0,
0.5,
1.0,
1.0)
1.0)
0.5)
1.0)
–
–
–
–
пурпурный
голубой
серый
белый
У функции glClearColor() из п.1.1 для установки цвета очистки буфера первые три параметра совпадают с параметрами glColor3d(). Четвертая компонента
цвета – прозрачность (она будет объяснена позже, пока устанавливайте ее равной 0).
1.3 Удаление невидимых поверхностей
При наблюдении трехмерной сцены с некоторой точки оказывается, что в проекции одни объекты полностью или частично заслоняют другие. При изменении точки наблюдения объекты будут заслонять друг друга иначе.
Процедура удаления невидимых частей сплошных объектов называется удалением скрытых поверхностей (для каркасных объектов выполняется аналогичная процедура – удаление скрытых линий). Обычно эта процедура основана на применении
буфера глубины (z-буфера).
В буфере глубины для каждого пиксела кадра хранится значение глубины –
расстояние от точки объекта, проецируемой в данный пиксел, до точки наблюдения.
При очистке буфера глубины (вызовом glClear(GL_DEPTH_BUFFER_BIT)) глубина
всех пикселей устанавливается равной максимальному значению.
33
Объекты трехмерной сцены можно рисовать в любом порядке. Для отображения пикселей, соответствующих этому объекту, вычисляются не только двумерные
оконные координаты, но и глубина. При использовании z-буфера перед тем, как нарисовать пиксел, его глубина сравнивается с текущей глубиной пиксела. Если новый
пиксел ближе к наблюдателю, чем уже нарисованный, то он будет нарисован "поверх" имеющегося и его глубина будет помещена в z-буфер. Пикселы, расположенные от наблюдателя дальше, чем уже нарисованные, не отображаются.
Чтобы пользоваться z-буфером, его надо включить до выполнения первой операции рисования:
glEnable( GL_DEPTH_TEST );
Перед рисованием каждого кадра надо очищать не только цветовой буфер, но и
буфер глубины:
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
2. Описание точек, отрезков и многоугольников
В OpenGL все геометрические примитивы описываются путем задания вершин. Для точек это координаты самих точек, для отрезков – координаты концов, для
многоугольников – координаты угловых точек.
Понятия "точка", "отрезок", "многоугольник" в OpenGL несколько отличаются
от принятых в математике. Различия связаны с тем, что в математике рассматривается
абстрактное пространство и идеальные объекты, а на практике приходится учитывать
ограничения, вызванные особенностями компьютеров.
Во-первых, это конечная точность вычислений. В любой реализации OpenGL
вычисления с плавающей точкой имеют конечную точность, что приводит к ошибкам
округления. Поэтому координаты вершин вычисляются не абсолютно точно.
Во-вторых, на растровых дисплеях минимальный элемент изображения – пиксел – имеет конечный размер. Хотя пикселы довольно маленькие (порядка 0,25 мм), в
математическом смысле их нельзя считать точками (не имеющими размера) или принять за толщину прямой (бесконечно тонкая). В вычислениях внутри OpenGL все
точки описываются векторами с вещественными компонентами. Однако при отображении обычно (но не всегда) точка рисуется в виде одного пиксела, так что несколько
различных точек с немного разными координатами будут нарисованы в виде одного
пиксела.
2.1 Точки
Точка описывается набором вещественных чисел, который называется вершиной. Все вычисления с вершинами производятся в трехмерном пространстве. Если
вершины были заданы двумерными координатами (только x и y), то по умолчанию им
присваивается координата z=0.
2.2 Отрезки
Отрезок задается двумя вершинами, расположенными на его концах. Несколько отрезков можно объединять в ломаные линии. Они могут быть замкнутыми или
незамкнутыми (рис. 3.1).
34
Рис. 3.1. Замкнутая и незамкнутая ломаные линии.
2.3 Многоугольники
Многоугольник – это область, ограниченная замкнутой ломаной линией. Ломаная задается угловыми вершинами. Обычно многоугольники рисуются с заливкой
внутренней области, но возможно рисование только контура или вершин.
Хотя в общем случае многоугольники могут иметь сложную форму, в OpenGL
существуют жесткие ограничения на форму примитивных многоугольников. Вопервых, стороны примитивного многоугольника не должны пересекаться (т.е. это
простой многоугольник). Во-вторых, многоугольники должны быть выпуклыми (без
выемок и зубцов). Говоря точнее, многоугольник является выпуклым, если отрезок,
соединяющий две любых его точки, целиком лежит внутри многоугольника.
На рис. 3.2 приведены несколько допустимых и недопустимых примитивных
многоугольников OpenGL. Количество сторон выпуклого многоугольника не ограничено. Многоугольники с отверстиями недопустимы. Они невыпуклые, т.к. их границу нельзя нарисовать в виде одной ломаной. При попытке рисования невыпуклого
залитого многоугольника OpenGL может нарисовать объект, совсем не похожий на
тот, что вам был нужен.
Рис. 3.2. Примеры примитивных многоугольников
(слева – допустимые, справа – недопустимые).
Невыпуклые многоугольники, с отверстиями или с самопересечениями бывают
нужны довольно часто. Их всегда можно представить в виде объединения простых
выпуклых многоугольников. Некоторые функции для описания более сложных объектов есть в библиотеках GLU и GLAUX. Эти функции выполняют разбиение сложных многоугольников на множества примитивных многоугольников OpenGL. Ограничения на форму многоугольников в OpenGL связаны с тем, что это упрощает разработку аппаратных графических ускорителей.
Вершины в OpenGL всегда трехмерные, поэтому углы многоугольников не
обязательно лежат в одной плоскости (правда, во многих случаях это так, если у всех
вершин z=0 или для треугольников). Если вершины многоугольника не лежат в одной
плоскости, то после поворотов в пространстве, изменения точки наблюдения и после
проецирования вершины могут выглядеть как углы невыпуклого многоугольника.
Представьте, например, четырехугольник, углы которого немного отклонены от одной плоскости, и что вы смотрите на него практически "с ребра". Вы увидите многоугольник, напоминающий бабочку (рис. 3.3), корректность отображения которого не
гарантирована. Эта ситуация не слишком надуманна, она вполне может возникнуть
35
при аппроксимации поверхности четырехугольниками, вершины которых принадлежат этой поверхности. В случае использования треугольников описанная проблема не
возникает.
Рис. 3.3. Неплоский многоугольник проецируется в невыпуклый.
2.4 Прямоугольники
В графических программах прямоугольники отображаются очень часто, поэтому в OpenGL есть специальная функция glRect*() для рисования примитива – залитого прямоугольника. Конечно, прямоугольник можно нарисовать и как многоугольник, задавая все его вершины, но специальная функция обычно работает быстрее. Она имеет следующий прототип:
void glRect{sifd}( TYPE x1, TYPE y1, TYPE x2, TYPE y2 );
void glRect{sifd}v( TYPE*v1, TYPE*v2 );
Прямоугольник задается диагонально расположенными вершинами (x1, y1) и
(x2, y2). Прямоугольник лежит в плоскости z=0, его стороны параллельны осям x и y.
Изменить пространственную ориентацию прямоугольника можно посредством модельных преобразований.
2.5 Кривые
С помощью отрезков (многоугольников) можно с любой требуемой точностью
аппроксимировать любую гладкую кривую (поверхность). Точки для соединения отрезками (многоугольниками) выбираются путем деления кривой (поверхности) на небольшие сегменты. При достаточной степени разбиения кривая или поверхность будет выглядеть гладкой (рис. 3.4).
Рис. 3.4. Аппроксимация кривой отрезками.
2.6 Задание вершин
Все геометрические фигуры в OpenGL описываются как упорядоченное множество вершин. Для задания каждой вершины надо вызывать функцию:
void glVertex{23}{sifd}[v](TYPEcoords);
В двумерном случае принимается z=0.
Функции glVertex*() надо обязательно вызывать между вызовами
glBegin() и glEnd(), обозначающими начало и конец рисования примитива. Ниже
приведено несколько примеров:
glVertex2s( 2, 3 );
// Вершина с координатами (2, 3, 0)
glVertex3d( 0.0, 0.0, 3.1415926535898 );
GLdouble dvect[3] = {5.0, 9.0, 1992.0};
glVertex3dv( dvect );
36
2.7 Геометрические примитивы OpenGL
Вершины надо задавать обязательно применительно к какому-нибудь примитиву. Рисование примитива начинается с вызова функции glBegin(), которой в качестве параметра передается константа, обозначающая тип примитива. Завершается
примитив функцией glEnd(). Прототипы этих функций:
void glBegin( Glenum mode );
void glEnd( void );
Между вызовами glBegin() и glEnd() делаются вызовы glVertex*(). Например, для описания многоугольника, показанного на рис. 3.5 слева, надо вызвать
следующие функции:
glBegin( GL_POLYGON );
glVertex2d( 0.0, 0.0
glVertex2d( 0.0, 3.0
glVertex2d( 3.0, 3.0
glVertex2d( 4.0, 1.5
glVertex2d( 3.0, 0.0
glEnd();
);
);
);
);
);
Рис. 3.5. Примитивы двух типов: многоугольник и множество точек.
Если в качестве типа примитива вместо GL_POLYGON указать GL_POINTS, то
будет нарисовано множество из 5-ти точек (рис. 3.5, справа). В табл. 3.1 перечислены
все допустимые типы примитивов, которые можно указывать при вызове glBegin().
Таблица 3.1. Имена и назначение геометрических примитивов.
Назначение
Имя константы
GL_POINTS
Отдельные точки
GL_LINES
Пары вершин, являющиеся концами отрезков
GL_POLYGON
Граница простого выпуклого многоугольника
GL_TRIANGLES
Тройки вершин, которые интерпретируются
как вершины треугольников
GL_QUADS
Четверки вершин, которые интерпретируются как вершины четырехугольников
GL_LINE_STRIP
Вершины ломаной линии
GL_LINE_LOOP
Вершины замкнутой ломаной линии (то же,
что и предыдущий тип, но последняя и первая вершина соединяются автоматически)
GL_TRIANGLE_STRIP Связная полоса из треугольников
(триангулированная полоса)
37
GL_TRIANGLE_FAN
GL_QUAD_STRIP
Веер из треугольников
Связная полоса из четырехугольников
(квадрированная полоса)
На рис. 3.6 показаны примеры примитивов, перечисленных в табл. 3.1. Предполагается, что между glBegin() и glEnd() перечислено n вершин (v0, v1, v2, ..., vn-1).
Как следует из рис. 3.6, кроме точек, отрезков и многоугольников, в OpenGL есть
еще несколько специальных типов примитивов.
Рис. 3.6. Типы геометрических примитивов.
3. Свойства точек, отрезков и многоугольников
По умолчанию точка отображается на экране в виде одного пиксела. Отрезки
рисуются сплошными толщиной 1 пиксел. Многоугольники рисуются залитыми. В
следующих параграфах описано, как можно изменить эти свойства отображения.
3.1 Точки
Экранный размер точки задается с помощью функции:
void glPointSize( float size );
где size – диаметр точки в пикселах (должен быть больше 0.0, по умолчанию 1.0).
Количество пикселей, которые будут закрашены на экране при отображении
данной точки, зависит от того, включено сглаживание или нет. Если сглаживание выключено, то size округляется до целого числа и на экране текущим цветом закрашивается квадрат со стороной size пикселей.
При включенном сглаживании точка отображается в виде окружности, причем
интенсивность пикселей внутри окружности уменьшается от центра к краям. В режиме сглаживания OpenGL обеспечивает дробные размеры для точек и толщины отрезков. Способы изменения свойств точек демонстрируются в программе 3.1.
#include <windows.h>
#include <GL/gl.h>
38
#include <GL/glu.h>
#include <GL/glaux.h>
void CALLBACK display();
void main()
{
auxInitDisplayMode( AUX_SINGLE | AUX_RGBA );
auxInitPosition( 0, 0, 200, 200 );
auxInitWindow( "Лекция 3, Программа 3.1" );
glClearColor( 0.0, 0.0, 0.0, 0.0 );
glShadeModel( GL_FLAT );
}
auxMainLoop( display );
void CALLBACK display()
{
glClear( GL_COLOR_BUFFER_BIT );
// В 1-й строке три точки диаметром 2 пиксела, без сглаживания
glPointSize( 2 );
glBegin( GL_POINTS );
glColor3d( 1, 0, 0 );
glVertex2d( 50, 180 );
glColor3d( 0, 1, 0 );
glVertex2d( 100, 180 );
glColor3d( 0, 0, 1 );
glVertex2d( 150, 180 );
glEnd();
// Во 2-й строке три точки диаметром 5 пикселов, без сглаживания
glPointSize( 5 );
glBegin( GL_POINTS );
glColor3d( 1, 0, 0 );
glVertex2d( 50, 100 );
glColor3d( 0, 1, 0 );
glVertex2d( 100, 100 );
glColor3d( 0, 0, 1 );
glVertex2d( 150, 100 );
glEnd();
// В 3-й строке три точки диаметром 10 пикселов, со сглаживанием
glPointSize( 10 );
glEnable( GL_POINT_SMOOTH );
glBegin( GL_POINTS );
glColor3d( 1, 0, 0 );
glVertex2d( 50, 20 );
glColor3d( 0, 1, 0 );
glVertex2d( 100, 20 );
glColor3d( 0, 0, 1 );
glVertex2d( 150, 20 );
glEnd();
glDisable( GL_POINT_SMOOTH );
// Принудительное завершение всех операций рисования
glFlush();
}
Программа 3.1. Рисование точек разного размера при
выключенном и включенном сглаживании.
39
3.2 Отрезки
У отрезков в OpenGL можно задавать толщину и стиль (точечный пунктир,
штриховой пунктир, штрих-точка и т.п.). Толщина отрезка (в пикселах) по умолчанию равна 1.0, для ее изменения имеется функция:
void glLineWidth( GLfloat width );
Количество пикселей, которые в действительности будут закрашены дна экране при рисовании отрезка, как и в случае точек, зависит от режима сглаживания. В
видеорежимах высокого разрешения отрезки толщиной 1 пиксел могут быть плохо
различимы, в таком случае целесообразно вычислять толщину отрезков на основе физического размера пиксела в данном видеорежиме.
Стиль отрезка (тип пунктира) устанавливается функцией glLineStipple():
void glLineStipple( GLint factor, GLushort pattern);
После вызова этой функции надо обязательно разрешить использование стиля с
помощью glEnable(), например:
glLineStipple( 1, 0x3F07 );
glEnable( GL_LINE_STIPPLE );
Тип пунктира определяется шаблоном – 16-ти битным числом pattern, в котором биты, равные 0 и 1, обозначают пустые и закрашенные пикселы. Размер шаблона можно увеличить с помощью множителя factor, который задает количество
повторений каждого бита шаблона.
В приведенном примере задан шаблон 0x3F07 (в двоичной форме это число
0011111100000111). При рисовании отрезка сначала закрашиваются 3 пиксела, затем 5 пропускаются, затем 6 закрашиваются, 2 пропускаются (шаблон обрабатывается
справа налево, от младших битов к старшим). Если задать factor=2, то шаблон будет задавать следующее правило рисования: закрасить 6 пикселов, 10 пропустить, 12
закрасить, 4 пропустить. На рис. 3.7 показаны отрезки различных стилей и соответствующие значения шаблона и множителя.
Рис. 3.7. Пунктирные отрезки.
Рисование отрезков нескольких стилей и различной толщины демонстрируется
в программе 3.2. Результат работы этой программы показан на рис. 3.8. Интересно
отметить эффект, который достигается программе 3.2 за счет того, что отрезок рисуется не сразу целиком, а из нескольких отдельных сегментов.
40
Рис. 3.8. Отрезки различных стилей и разной толщины.
#include
#include
#include
#include
<windows.h>
<GL/gl.h>
<GL/glu.h>
<GL/glaux.h>
void drawOneLine( double x1, double y1, double x2, double y2 );
void CALLBACK display();
void main()
{
auxInitDisplayMode( AUX_SINGLE | AUX_RGBA );
auxInitPosition( 0, 0, 400, 150 );
auxInitWindow( "Лекция 3, Программа 3.2" );
glClearColor( 0.0, 0.0, 0.0, 0.0 );
glShadeModel( GL_FLAT );
auxMainLoop( display );
}
void drawOneLine( double x1, double y1, double x2, double y2 )
{
glBegin( GL_LINES );
glVertex2d( x1, y1 );
glVertex2d( x2, y2 );
glEnd();
}
void CALLBACK display()
{
glClear( GL_COLOR_BUFFER_BIT );
glColor3d( 1.0, 1.0, 1.0 );
// Все отрезки рисуются белым цветом
// В 1-й строке 3 отрезка разных стилей
glEnable( GL_LINE_STIPPLE );
glLineStipple( 1, 0x0101 );
// Точечный пунктир
drawOneLine( 50.0, 125.0, 150.0, 125.0 );
glLineStipple( 1, 0x00FF );
// Штриховой пунктир
drawOneLine( 150.0, 125.0, 250.0, 125.0 );
glLineStipple( 1, 0x1C47 );
// Штрих-точка-штрих
drawOneLine( 250.0, 125.0, 350.0, 125.0 );
// Во 2-й строке 3 отрезка толщиной 5 пикселей и разных стилей
glLineWidth( 5.0 );
glLineStipple( 1, 0x0101 );
drawOneLine( 50.0, 100.0, 150.0, 100.0 );
glLineStipple( 1, 0x00FF );
drawOneLine( 150.0, 100.0, 250.0, 100.0 );
41
glLineStipple( 1, 0x1C47 );
drawOneLine( 250.0, 100.0, 350.0, 100.0 );
glLineWidth( 1.0 );
// В 3-й строке 6 отрезков стиля штрих-точка-штрих, которые
// образуют один большой отрезок
glLineStipple( 1, 0x1C47 );
glBegin( GL_LINE_STRIP );
for ( int i = 0; i < 7; i++ )
glVertex2d( (i + 1)*50.0, 75.0 );
glEnd();
// В 4-й строке 6 отдельных отрезков стиля штрих-точка-штрих
for ( i = 0; i < 6; i++ )
drawOneLine( (i + 1)*50.0, 50.0, (i + 2)*50.0, 50.0 );
// В 5-й строке 1 отрезок стиля штрих-точка-штрих с множителем 5
glLineStipple( 5, 0x1C47 );
drawOneLine( 50.0, 25.0, 350.0, 25.0 );
}
// Принудительное завершение всех операций рисования
glFlush();
Программа 3.2. Управление свойствами отрезков.
3.3 Многоугольники
Многоугольники обычно рисуются заполненными, так что на экране закрашиваются все пикселы внутри их границ. При необходимости можно рисовать только
контуры или только вершины многоугольников. Заливка может быть сплошной или
по шаблону.
В OpenGL у многоугольника выделяются две стороны – передняя и задняя, и
на экране он может выглядеть по-разному, в зависимости от того, какой стороной он
повернут к наблюдателю. Это позволяет создавать разрезы сплошных объектов, на
которых явно различаются внутренние и внешние части объектов. По умолчанию обе
стороны рисуются одинаково. Для изменения правил рисования сторон, или для рисования только контуров или вершин, надо применить функцию:
void glPolygonMode( GLenum face, GLenum mode );
Параметр face указывает сторону: GL_FRONT_AND_BACK (обе стороны),
GL_FRONT (передняя сторона) или GL_BACK (задняя). Параметр mode задает режим
рисования выбранных сторон: GL_POINT (только вершины), GL_LINE (контур) или
GL_FILL (заливка). По умолчанию для обеих сторон установлен режим заливки.
Например, чтобы задать рисование передних сторон залитыми, а задних – в виде контуров, надо вызвать функции:
glPolygonMode( GL_FRONT, GL_FILL );
glPolygonMode( GL_BACK, GL_LINE );
"Передняя" сторона отличается от "задней" по правилу, согласно которому
вершины "передней" стороны должны быть перечислены в порядке "против часовой
стрелки". Вы можете смоделировать поверхность практически любого тела (математики называют такие поверхности ориентируемыми) из многоугольников одной ориентации. К ориентируемым поверхностям относятся сфера, кольца и чайник, к неориентируемым – бутылка Клейна и лист Мебиуса. Другими словами, для всех практиче-
42
ских целей вы можете пользоваться только многоугольниками, "видными спереди"
или только "видными сзади".
OpenGL определяет ориентацию многоугольника по знаку его площади (для
передней стороны многоугольника S>0), которая вычисляется по формуле:
S=
1 n −1
∑ xi yi⊕1 − xi⊕1 yi ,
2 i =0
где xi и yi есть экранные координаты i-й вершины n-угольника. Операция ⊕ является
обычным сложением, за исключением того, что n ⊕ 1 = 1:
i ⊕ 1 = (i + 1) mod n .
При отображении замкнутой поверхности из многоугольников одной ориентации не будет видно ни одной задней стороны – все они загораживаются передними
сторонами многоугольников. В данном случае вы можете ускорить отображение поверхности, если запретите OpenGL рисование задних сторон. Для запрещения рисования определенных сторон многоугольников применяется функция:
void glCullFace( GLenum mode );
Параметр mode равен GL_FRONT, GL_BACK или GL_FRONT_AND_BACK. Перед
вызовом этой функции надо включить режим отсечения сторон многоугольников с
помощью вызова glEnable( GL_CULL_FACE ).
В программе 3.3 иллюстрируются режимы рисования разных сторон многоугольников на примере 6-ти гранной пирамиды без основания. Клавишами курсора ее
можно повернуть под разными углами, при этом явно видно различие между внешней
и внутренней поверхностями пирамиды. Обратите внимание, что если задать вершинам многоугольника разные цвета, то OpenGL автоматически выполняет цветовую
интерполяцию при заливке многоугольника.
#include
#include
#include
#include
#include
void
void
void
void
void
void
<windows.h>
<math.h>
<GL/gl.h>
<GL/glu.h>
<GL/glaux.h>
CALLBACK
CALLBACK
CALLBACK
CALLBACK
CALLBACK
CALLBACK
resize( int width, int height );
display();
addAngleX();
subAngleX();
addAngleY();
subAngleY();
// Расположение пирамиды относительно осей X и Y
int angle_x = 0, angle_y = 0;
void main()
{
auxInitDisplayMode( AUX_RGBA | AUX_DEPTH | AUX_DOUBLE );
auxInitPosition( 50, 10, 400, 400);
auxInitWindow( "Лекция 3, Программа 3.3" );
// Включение ряда параметров OpenGL
glEnable( GL_DEPTH_TEST );
glEnable( GL_COLOR_MATERIAL );
glEnable( GL_LIGHTING );
glEnable( GL_LIGHT0 );
43
// Задание положения и направления нулевого источника света
float pos[4] = { 0, 5, 5, 1 };
float dir[3] = { 0, -1, -1 };
glLightfv( GL_LIGHT0, GL_POSITION, pos );
glLightfv( GL_LIGHT0, GL_SPOT_DIRECTION, dir );
// Регистрация обработчиков событий
auxReshapeFunc( resize );
auxKeyFunc( AUX_UP, subAngleX );
auxKeyFunc( AUX_DOWN, addAngleX );
auxKeyFunc( AUX_RIGHT, addAngleY );
auxKeyFunc( AUX_LEFT, subAngleY );
auxIdleFunc( display );
auxMainLoop( display );
}
void CALLBACK resize( int width, int height )
{
glViewport( 0, 0, width, height );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
gluPerspective( 60.0, (double)width/(double)height, 1.0, 20.0 );
gluLookAt( 0,0,5, 0,0,0, 0,1,0 );
}
void CALLBACK display()
{
const double BASE_R = 1;
const double PYRAMID_H = 2;
const double PI = 3.14159;
// Радиус основания
// Высота пирамиды
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glPushMatrix();
glRotated( angle_x, 1, 0, 0 );
glRotated( angle_y, 0, 1, 0 );
glPolygonMode( GL_BACK, GL_LINE );
glBegin( GL_TRIANGLE_FAN );
glColor3d( 1.0, 0.0, 1.0 );
glVertex3d( 0, PYRAMID_H, 0 );
glColor3d( 1.0, 1.0, 1.0 );
for ( int i = 0; i < 7; i++ )
glVertex3d( cos(i*PI/3.0)*BASE_R, 0, -sin(i*PI/3.0)*BASE_R );
glEnd();
glPopMatrix();
auxSwapBuffers();
}
void CALLBACK addAngleX()
{
angle_x = ( angle_x + 5 ) % 360;
}
44
void CALLBACK subAngleX()
{
angle_x = ( angle_x - 5 ) % 360;
}
void CALLBACK addAngleY()
{
angle_y = ( angle_y + 5 ) % 360;
}
void CALLBACK subAngleY()
{
angle_y = ( angle_y - 5 ) % 360;
}
Программа 3.3. Различные режимы рисования передних и задних сторон многоугольников.
4. Сводка результатов
Перед рисованием трехмерной сцены надо очистить буфер кадра. Кроме цветового буфера, с кадром может быть связан буфер глубины (z-буфер), позволяющий
OpenGL автоматически выполнять удаление скрытых поверхностей.
Все трехмерные объекты в OpenGL формируются из геометрических примитивов нескольких типов: точек, отрезков и многоугольников. Все примитивы задаются в виде упорядоченного множества вершин. Гладкие кривые и поверхности можно
аппроксимировать с помощью отрезков и многоугольников. У примитивов можно изменить свойства, влияющие на их отображение на экране: диаметр точек, толщину и
стиль отрезков, способ рисования многоугольников. У многоугольников различаются
передняя и задняя стороны (по порядку перечисления вершин). Разные стороны можно рисовать разными способами: залитыми, контурными или только вершины.
5. Упражнения
Упражнение 1
Выполните программу 3.1. Добавьте в нее функцию, которая строит из точек
диаметром 15 пикселей квадрат заданного размера (например, как на рис. ниже, размер пустых промежутков можно сделать равным радиусу точки). Используйте эту
функцию для построения 5-ти квадратов разного цвета с центром в общей точке.
Упражнение 2
Выполните программу 3.2. Затем напишите программу, рисующую каркасный
куб в перспективной проекции. Видимые ребра нарисуйте штриховым пунктиром
толщиной 3 пиксела, невидимые – точечным пунктиром толщиной 1 пиксел.
45
Упражнение 3
Выполните программу 3.3. Выясните, что произойдет, если:
1) убрать вызов функции, которая задает контурное рисование задних сторон
многоугольников;
2) убрать функцию, задающую белый цвет для вершин основания пирамиды;
3) задать рисование передних сторон контурами, а задних – вершинами.
Упражнение 4
Постройте изображение втулки, аппроксимируя ее поверхность с помощью
квадрированных полос (примитивов типа GL_QUAD_STRIP).
Упражнение 5
Разработайте функцию, которая строит сферический сегмент заданной высоты
с помощью аппроксимации поверхности сферы треугольниками. В качестве параметров этой функции надо передавать высоту сегмента и число, определяющее степень
разбиения сферы. Для рисования передних и задних граней многоугольников используйте разные стили.
Напишите тестовую программу, которая с помощью разработанной функции
строит полусферу и позволяет поворачивать ее клавишами курсора (см. программу 3.3).
46
ЛЕКЦИЯ 4. Полигональная аппроксимация поверхностей
1. Векторы нормали
Вектором нормали к поверхности в данной точке называется вектор, перпендикулярный этой поверхности. У плоскости векторы нормали направлены одинаково
во всех точках, но у произвольной поверхности направления нормали в разных точках
могут различаться. Нормаль к аналитически заданной поверхности вычислить несложно, ее направление совпадает с градиентом в данной точке поверхности.
Векторы нормали определяют ориентацию поверхности в пространстве, в частности, расположение относительно источников света. Эти векторы используются
OpenGL для расчетов, сколько света получает объект в своих вершинах. Освещение –
это отдельная тема, она будет рассматриваться в следующей лекции. Далее кратко
описываются способы задания векторов нормали, чтобы вы могли указать их в процессе описания формы объектов.
Нормаль может быть одинаковой в нескольких вершинах. В каких-либо иных
точках, кроме вершин, OpenGL задавать вектор нормали не позволяет. Для указания
вектора нормали используется одна из функций glNormal*():
void glNormal3{bsidf}(TYPE nx, TYPE ny, TYPE nz);
void glNormal3{bsidf}v( const TYPE* v );
В качестве параметров этой функции передаются координаты нормали. Этот
вектор будет присваиваться всем последующим вершинам. Если в разных вершинах
направление нормали разное, то надо указывать его перед рисованием каждой вершины, например:
glBegin( GL_POLYGON );
glNormal3fv( n0 );
glVertex3fv( v0 );
glNormal3fv( n1 );
glVertex3fv( v1 );
glNormal3fv( n2 );
glVertex3fv( v2 );
glNormal3fv( n3 );
glVertex3fv( v3 );
glEnd();
В каждой точке поверхности существуют два вектора нормали, направленных в
противоположные стороны. По умолчанию, нормалью считается вектор, начало которого лежит на передней стороне многоугольника. Вектор нормали указывает только
направление, поэтому его длина несущественна. Обычно в качестве нормалей задаются единичные вектора. C помощью вызова glEnable(GL_NORMALIZE) в OpenGL
можно включить режим автоматической нормализации векторов.
2. Некоторые рекомендации по построению полигональных аппроксимаций
поверхностей
Проектирование полигональных аппроксимаций поверхностей – это в некотором роде искусство, в котором большое значение имеет практический опыт. Ниже перечислены несколько рекомендаций общего характера, которым стоит следовать с
самых первых программ. Хотя рекомендации в основном касаются разбиения поверхностей на многоугольники, надо иметь в виду, что при использовании направленных
источников света необходимо корректно задавать нормали вершин. При включенном
47
освещении направление нормалей очень существенно влияет на нарисованное изображение модели.
• Пользуйтесь многоугольниками одной ориентации. Убедитесь, что когда вы смотрите на внешнюю сторону поверхности, все многоугольники ориентированы одинаково. Обеспечьте это условие с самого начала, поскольку в дальнейшем исправить положение может быть очень сложно.
• При разбиении поверхности обращайте внимание на каждый нетреугольный многоугольник. Три вершины треугольника всегда лежат в одной плоскости, а для
многоугольников с большим количеством вершин это может быть не так. Неплоские многоугольники с некоторых точек зрения могут проектироваться в невыпуклые, которые OpenGL может нарисовать не правильно.
• На практике всегда приходится искать компромисс между скоростью рисования и
качеством изображения. При разбиении поверхности на малое количество многоугольников скорость рисования будет большой, но поверхность может выглядеть
негладкой. При очень большом количестве маленьких многоугольников изображение будет выглядеть хорошо, но рисоваться будет очень долго. В принципе, в
функциях разбиения поверхностей очень удобно предусмотреть параметр, от которого зависит степень разбиения. Если объект находится далеко от наблюдателя,
то можно ограничиться более грубым разбиением. Еще один прием: при разбиении можно пользоваться более крупными многоугольниками в относительной
плоских областях поверхности, и уменьшать размеры многоугольников с увеличением кривизны поверхности.
• Для высококачественных изображений имеет смысл мельче разбивать поверхность
у краев силуэта, чем в его внутренних частях. Обеспечить это условие при повороте поверхности относительно наблюдателя сложно, т.к. края силуэта смещаются.
Края силуэта – это те области поверхности, в которых вектора нормали перпендикулярны векторам от поверхности к наблюдателю (т.е. скалярное произведение
этих векторов равно 0). Ваш алгоритм разбиения может производить более частое
разбиение в областях, где это скалярное произведение близко к 0.
• В своих моделях старайтесь избегать T-образных пересечений (рис. 4.1). Из-за
ошибок округления нельзя гарантировать, что пикселы отрезков AB и BC попадут
точно на пикселы отрезка AC. Из-за этого при некоторых модельных преобразованиях на поверхности могут возникать прерывистые изломы (трещины).
Рис. 4.1. Корректная конфигурация для замены нежелательного T-образного пересечения.
• При аппроксимации замкнутой поверхности убедитесь, что в области "шва" задаются в точности одинаковые координаты вершин. Иначе ошибки округления могут привести к появлению на поверхности трещин и дыр. Ниже приведен пример
некорректного построения окружности:
48
/* Это пример некорректного построения */
#define PI
3.14159265
#define EDGES
30
/* Рисование окружности */
for ( i = 0; i < EDGES; i++) {
glBegin( GL_LINE_STRIP );
glVertex2f( cos((2*PI*i)/EDGES), sin((2*PI*i)/EDGES) );
glVertex2f( cos((2*PI*(i+1))/EDGES),
sin((2*PI*(i+1))/EDGES) );
glEnd();
}
В этом фрагменте концы ломаной будут замыкаться только в том случае,
если при вычислении синуса и косинуса от 0 и от (2*PI*EDGES/EDGES) будут
получены одинаковые результаты. Но вычисления с плавающей точкой имеют конечную точность. Поэтому необходимо исправить приведенный текст, чтобы при
i == EDGES-1 в качестве аргумента синусу и косинусу передавалось значение 0,
а не 2*PI*EDGES/EDGES.
3. Пример: построение икосаэдра
Для демонстрации приемов, перечисленных в п.2., рассмотрим фрагменты программы, выполняющей построение икосаэдра. Икосаэдр – это правильный многогранник, имеющий 12 вершин и 20 граней (каждая грань – равносторонний треугольник). Икосаэдр можно считать грубой аппроксимацией сферы. Во фрагменте программы 4.1а в массивах заданы вершины икосаэдра и списки вершин каждой грани.
После описания и инициализации массивов приведен цикл рисования этих граней.
const double X = .525731112119133606;
const double Z = .850650808352039932;
double vdata[12][3] = {
{-X, 0.0, Z}, {X, 0.0, Z}, {-X, 0.0, -Z}, {X, 0.0, -Z},
{0.0, Z, X}, {0.0, Z, -X}, {0.0, -Z, X}, {0.0, -Z, -X},
{Z, X, 0.0}, {-Z, X, 0.0}, {Z, -X, 0.0}, {-Z, -X, 0.0}
};
int tindices[20][3] = {
{0,4,1}, {0,9,4}, {9,5,4}, {4,5,8}, {4,8,1},
{8,10,1}, {8,3,10}, {5,3,8}, {5,2,3}, {2,7,3},
{7,10,3}, {7,6,10}, {7,11,6}, {11,0,6}, {0,1,6},
{6,1,10}, {9,0,11}, {9,11,2}, {9,2,5}, {7,2,11} };
for ( int i = 0; i < 20; i++ ) {
/* Здесь могут быть вызовы функций для задания цвета */
glBegin( GL_TRIANGLES );
glVertex3dv( &vdata[tindices[i][0]][0] );
glVertex3dv( &vdata[tindices[i][1]][0] );
glVertex3dv( &vdata[tindices[i][2]][0] );
glEnd();
}
Фрагмент программы 4.1а. Рисование икосаэдра.
49
Константы X и Z выбраны такими, чтобы расстояние от начала координат до
каждой вершины икосаэдра равнялось 1.0. Координаты 12-ти вершин хранятся в массиве vdata[][]: 0-я вершина имеет координаты {-X, 0.0, Z}, 1-я – {X, 0.0, Z}
и т.д. Массив tindices[][] задает правила построения треугольных граней из вершин. Например, первый треугольник формируется из 0-й, 4-й и 1-й вершины. Вершины треугольников в массиве tindices[][] перечислены так, что все грани будут
иметь одинаковую ориентацию.
В строке с комментарием о цветовой информации можно вызвать функцию, задающую разный цвет для каждой (i-й) грани. Если всем граням присвоить одинаковый цвет, то на изображении они будут сливаться. При закраске граней одним цветом
необходимо задавать нормали вершин и применять направленное освещение.
Если вершины полигональной сетки не задаются явно, как во фрагменте 4.1а, а
рассчитываются по какому-либо алгоритму, целесообразно выполнять расчеты только
один раз и затем хранить рассчитанные координаты вершин и нормалей в массивах
или ваших собственных структурах данных.
3.1 Вычисление нормалей к граням икосаэдра
Для применения направленного освещения надо задать вектор нормали для каждой вершины икосаэдра. На плоских гранях икосаэдра вектор нормали одинаков у
всех трех вершин (это нормаль к плоскости этой грани). Следовательно, для каждого
набора из трех вершин нормаль надо задавать только один раз. Фрагмент программы 4.1б можно разместить в тех строках фрагмента 4.1а, которые отмечены комментарием "вызовы функций для задания цвета".
double d1[3], d2[3], norm[3];
for ( int j = 0; j < 3; j++ ) {
d1[j] = vdata[tindices[i][1]][j] - vdata[tindices[i][0]][j];
d2[j] = vdata[tindices[i][2]][j] - vdata[tindices[i][0]][j];
}
normcrossprod( d2, d1, norm );
glNormal3dv( norm );
Фрагмент программы 4.1б. Вычисление нормалей для вершин i-й грани икосаэдра.
Функция normcrossprod() вычисляет нормированное векторное произведение двух векторов (см. фрагмент 4.1в).
void normalize( double v[3] )
{
double d = sqrt( v[0]*v[0]+v[1]*v[1]+v[2]*v[2] );
if ( d == 0.0 )
{
// Ошибка: вектор нулевой длины
return;
}
v[0] /= d; v[1] /= d; v[2] /= d;
}
void normcrossprod(const double v1[3], const double v2[3], double out[3])
{
50
out[0] = v1[1]*v2[2] - v1[2]*v2[1];
out[1] = v1[2]*v2[0] - v1[0]*v2[2];
out[2] = v1[0]*v2[1] - v1[1]*v2[0];
normalize(out);
}
Фрагмент программы 4.1в. Вычисление нормированного векторного произведения.
Нормали вершин не обязательно вычислять как нормаль к плоскости грани.
Способ вычисления нормалей зависит от решаемой задачи. Допустим, вы хотите использовать икосаэдр в качестве аппроксимации сферической поверхности. Тогда
нормали вершин надо вычислять как перпендикуляры к поверхности сферы, а не перпендикуляры к граням. Для сферы векторы нормали вычислить очень легко: вектор
нормали для данной точки сферы совпадает с направлением радиуса для данной точки. В программе 4.1 вершины икосаэдра рассчитывались для икосаэдра, вписанного в
единичную сферу. Поэтому в качестве координат нормалей можно взять координаты
вершин (см. фрагмент 4.1г).
for ( int i = 0; i < 20; i++)
{
glBegin( GL_POLYGON );
glNormal3dv( &vdata[tindices[i][0]][0] );
glVertex3dv( &vdata[tindices[i][0]][0] );
glNormal3dv( &vdata[tindices[i][1]][0] );
glVertex3dv( &vdata[tindices[i][1]][0] );
glNormal3dv( &vdata[tindices[i][2]][0] );
glVertex3dv( &vdata[tindices[i][2]][0] );
glEnd();
}
Фрагмент программы 4.1г. Рисование икосаэдральной аппроксимации сферы (в предположении, что используется направленное освещение).
3.2 Повышение точности аппроксимации сферической поверхности
12-ти гранная аппроксимация сферы не слишком точна и ее изображение напоминает сферу только при небольшом размере. Есть простой путь для увеличения точности аппроксимации. Допустим, имеется вписанный в сферу икосаэдр. Разобьем каждую грань икосаэдра на 4 равносторонних треугольника. Новые вершины будут лежать внутри сферы, поэтому их надо "приподнять" на поверхность (умножить на такое число, чтобы их радиус-векторы стали равны 1). Этот процесс разбиения можно
продолжать до достижения требуемой точности. На рис. 4.2 показаны поверхности из
20, 80 и 320 треугольников.
Рис. 4.2. Последовательное разбиение для улучшения качества полигональной аппроксимации сферической поверхности.
51
Первое разбиение икосаэдра для получения 80-ти гранной аппроксимации сферы можно выполнить с помощью фрагмента 4.2а.
void draw_triangle( double* v1, double* v2, double* v3 )
{
glBegin( GL_POLYGON );
glNormal3dv(v1); glVertex3dv(v1);
glNormal3dv(v2); glVertex3dv(v2);
glNormal3dv(v3); glVertex3dv(v3);
glEnd();
}
void subdivide( double* v1, double* v2, double* v3 )
{
double v12[3], v23[3], v31[3];
for ( int i = 0; i < 3; i++ )
{
v12[i] = ( v1[i]+v2[i] )/2;
v23[i] = ( v2[i]+v3[i] )/2;
v31[i] = ( v3[i]+v1[i] )/2;
}
normalize( v12 );
normalize( v23 );
normalize( v31 );
draw_triangle( v1, v12, v31 );
draw_triangle( v2, v23, v12 );
draw_triangle( v3, v31, v23 );
draw_triangle( v12, v23, v31 );
}
...
for ( int i = 0; i < 20; i++ )
subdivide( &vdata[tindices[i][0]][0],
&vdata[tindices[i][1]][0],
&vdata[tindices[i][2]][0] );
Фрагмент программы 4.2а. Первое разбиение граней икосаэдра для аппроксимации сферы.
Немного изменив функцию разбиения из фрагмента 4.2а, можно получить
функцию, выполняющую рекурсивное деление треугольников до достижения заданной глубины деления (см. фрагмент 4.2б). Разбиение прекращается при достижении
глубины depth=0. После этого выполняется рисование треугольников. При depth=1,
выполняется один шаг разбиения и т.д.
void subdivide( double* v1, double* v2, double* v3, long depth )
{
double v12[3], v23[3], v31[3];
if ( depth == 0 )
{
draw_triangle( v1, v2, v3 );
return;
}
for ( int i = 0; i < 3; i++ )
{
52
v12[i] =
v23[i] =
v31[i] =
}
normalize(
normalize(
normalize(
subdivide(
subdivide(
subdivide(
subdivide(
( v1[i]+v2[i] )/2;
( v2[i]+v3[i] )/2;
( v3[i]+v1[i] )/2;
v12 );
v23 );
v31 );
v1, v12, v31, depth-1 );
v2, v23, v12, depth-1 );
v3, v31, v23, depth-1 );
v12, v23, v31, depth-1 );
}
Фрагмент программы 4.2б. Рекурсивное разбиение треугольной грани
полигональной сетки на сферической поверхности.
3.3 Алгоритм разбиения треугольной грани произвольной поверхности
Прием рекурсивного разбиения, приведенный во фрагменте 4.2б, можно обобщить на случай произвольной гладкой поверхности. Рекурсия должна заканчиваться
или при достижении определенной глубины, или если удовлетворяется некоторое условие о кривизне поверхности (части поверхности с большой кривизной лучше выглядят при более мелком разбиении).
Будем полагать, что произвольная гладкая поверхность параметризована по
двум параметрам u[0] и u[1]. Для описания поверхности в программе должны быть
реализованы две функции:
void surf( const double u[2], double vertex[3], double normal[3] );
double curv( const double u[2] );
Функция surf() получает параметры u[] и возвращает координаты вершины
и единичный вектор нормали для точки поверхности с этими параметрами. Функция
curv() вычисляет кривизну поверхности в точке, заданной параметрами u[].
Во фрагменте 4.3 приведена рекурсивная функция для разбиения треугольной
грани произвольной поверхности на треугольники до тех пор, пока не будет достигнута заданная глубина или кривизна во всех вершинах грани не станет меньше некоторого граничного значения.
void subdivide( double u1[2], double u2[2], double u3[2],
double cutoff, long depth, long max_depth )
{
double v1[3], v2[3], v3[3], n1[3], n2[3], n3[3];
double u12[2], u23[2], u31[2];
if ( depth == max_depth ||
(curv(u1) < cutoff && curv(u2) < cutoff && curv(u3) < cutoff) )
{
surf( u1, v1, n1 );
surf( u2, v2, n2 );
surf( u3, v3, n3 );
glBegin( GL_POLYGON );
glNormal3dv(n1); glVertex3dv(v1);
glNormal3dv(n2); glVertex3dv(v2);
glNormal3dv(n3); glVertex3dv(v3);
glEnd();
return;
}
53
for ( int i = 0; i < 2; i++ )
{
u12[i] = (u1[i] + u2[i])/2.0;
u23[i] = (u2[i] + u3[i])/2.0;
u31[i] = (u3[i] + u1[i])/2.0;
}
}
subdivide(
subdivide(
subdivide(
subdivide(
u1, u12, u31, cutoff, depth+1, max_depth );
u2, u23, u12, cutoff, depth+1, max_depth );
u3, u31, u23, cutoff, depth+1, max_depth );
u12, u23, u31, cutoff, depth+1, max_depth );
Фрагмент программы 4.3. Функция разбиения треугольной грани
произвольной поверхности.
4. Плоскости отсечения
Кроме 6-ти плоскостей отсечения, образующих видимый объем (левая, правая,
нижняя, верхняя, ближняя и дальняя), OpenGL позволяет задать до 6-ти дополнительных плоскостей отсечения. Эти плоскости накладывают дополнительные ограничения на видимый объем (рис. 4.3), что может быть полезно для удаления посторонних объектов сцены, например, при построении сечения объекта.
Каждая
плоскость
задается
коэффициентами
общего
уравнения:
A x + B y + C z + D = 0 . При модельных преобразованиях плоскости отсечения преобразуются автоматически. Отсеченный видимый объем принимается равным пересечению видимого объема и полупространств, задаваемых дополнительными плоскостями
отсечения. При выполнении отсечения многоугольников, попадающих в видимый
объем частично, OpenGL автоматически рассчитывает координаты новых вершин.
Рис. 4.3. Дополнительные плоскости отсечения и видимый объем.
Для задания плоскости отсечения применяется функция:
void glClipPlane( GLenum plane, const double* equation );
Указатель equation указывает на массив из 4-х коэффициентов уравнения
плоскости A x + B y + C z + D = 0 . Параметр plane равен GL_CLIP_PLANEi, где i – это
целое число от 0 до 5, обозначающее одну из 6-ти возможных плоскостей отсечения.
При выполнении отсечения будут оставлены только те вершины ( x, y, z ) , для которых
выполняется условие:
( A B C D) M −1 ( x y z 1) T >= 0 ,
где M – это видовая матрица на момент вызова функции glClipPlane().
Каждую из плоскостей отсечения надо включить с помощью вызова:
glEnable( GL_CLIP_PLANEi );
54
Для выключения плоскости надо вызвать:
glDisable( GL_CLIP_PLANEi );
В программе 4.4 демонстрируется применение плоскостей отсечения для рисования каркасного сферического сегмента объемом в четверть сферы (рис. 4.4).
Рис. 4.4. Сегмент каркасной сферы, построенный с помощью плоскостей отсечения.
#include
#include
#include
#include
<windows.h>
<GL/gl.h>
<GL/glu.h>
<GL/glaux.h>
void CALLBACK resize( int width, int height );
void CALLBACK display();
void main()
{
auxInitDisplayMode( AUX_SINGLE | AUX_RGBA );
auxInitPosition( 0, 0, 400, 400 );
auxInitWindow( "Лекция 4, Программа 4.4" );
auxReshapeFunc( resize );
auxMainLoop( display );
}
void CALLBACK resize( int width, int height )
{
glViewport( 0, 0, width, height );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
gluPerspective( 60.0, (float)width/(float)height, 1.0, 20.0 );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
}
void CALLBACK display()
{
double eqn1[4] = {0.0, 1.0, 0.0, 0.0};
double eqn2[4] = {1.0, 0.0, 0.0, 0.0};
// y < 0
// x < 0
glClear( GL_COLOR_BUFFER_BIT );
glColor3d( 1.0, 1.0, 1.0 );
glPushMatrix();
glTranslated( 0.0, 0.0, -5.0 );
glClipPlane( GL_CLIP_PLANE0, eqn1 );
glEnable( GL_CLIP_PLANE0 );
glClipPlane( GL_CLIP_PLANE1, eqn2 );
glEnable( GL_CLIP_PLANE1 );
}
glRotated( 90.0, 1.0, 0.0, 0.0 );
auxWireSphere( 1.0 );
glPopMatrix();
glFlush();
Программа 4.4. Использование плоскостей отсечения для построения
каркасного сферического сегмента.
55
6. Сводка результатов
Произвольную гладкую поверхность можно аппроксимировать с помощью сетки из многоугольников, например, из треугольников. Чтобы полигональная аппроксимация поверхности выглядела похожей на требуемую поверхность, в OpenGL необходимо задавать нормали в вершинах полигональной сетки. Направление нормалей
учитывается при вычислении количества света, отраженного поверхностью в окрестности вершины по направлению наблюдателя.
В лекции приведен пример построения икосаэдральной аппроксимации сферы.
Для улучшения вида аппроксимированной поверхности надо уменьшать размеры граней в областях поверхности с большой кривизной. В лекции приведен алгоритм рекурсивного разбиения треугольной сетки, аппроксимирующей произвольную параметрически заданную поверхность.
В OpenGL можно наложить ограничения на форму видимого объема. Для этого используются дополнительные плоскости отсечения.
7. Упражнения
Упражнение 1
На основе фрагментов программы 4.1 разработайте программу для рисования
икосаэдра в четырех режимах (режимы циклически переключаются пробелом):
1) Все грани закрашиваются одним цветом, нормали вершин не задаются (т.е. нормали всех вершинам равны по умолчанию (0,0,1)).
2) Каждая грань закрашивается своим цветом, нормали не задаются (т.е. все нормали
равны (0,0,1)).
3) Все грани закрашиваются одним цветом, нормали задаются как перпендикуляры к
плоским граням икосаэдра.
4) Все грани закрашиваются одним цветом, нормали задаются как перпендикуляры к
поверхности сферы, на которой лежат вершины икосаэдра.
Упражнение 2
На основе фрагментов программы 4.2 напишите программу, которая будет рисовать три варианта икосаэдральной аппроксимации сферы (как на рис. 4.2). Затем
попробуйте выполнить еще один шаг разбиения для получения 1280 граней.
Упражнение 3
Примените общий алгоритм разбиения из п.3.3 для построения эллиптического
параболоида z = x 2 / 2 + y 2 / 2 . Параметрами поверхности считайте декартовы координаты x и y. В качестве первого приближения можете выбрать пирамиду с вершинами
в точках (0, 0, 0), (-3, -3, 9), (-3, 3, 9), (3, 3, 9), (3, -3, 9). Из функции subdivide()
(фрагмент программы 4.3) можно исключить параметр cutoff и не применять проверку кривизны поверхности.
Упражнение 4
Выполните программу 4.4. Измените ее так, чтобы с помощью одной плоскости
отсечения программа изображала нижнюю полусферу. Затем добавьте в свою программу функцию фоновой обработки и с ее помощью сделайте так, чтобы плоскость
отсечения вращалась (функцией glRotated() перед glClipPlane()) и отсекала
полусферу под разными углами.
56
ЛЕКЦИЯ 5. Цвет и освещение
1. Цветовая модель RGB
Свет представляет собой электромагнитные волны видимого диапазона. Воспринимаемый человеком свет фокусируется на сетчатке глаза, в которой есть рецепторы двух типов: колбочки и палочки. Цвет воспринимается только колбочками, причем есть три типа колбочек, преимущественно чувствительных к разным длинам
волн: красному свету, зеленому и синему. Сама по себе электромагнитная волна определенной длины никакого цвета не имеет. Ощущение цвета возникает в результате
психофизиологических преобразований в глазу и мозге человека.
На мониторе компьютера видимые цвета формируются путем смешивания трех
основных цветов – красного (Red), зеленого (Green) и синего (Blue). В памяти компьютера значение цвета хранится в виде трех чисел – компонент R, G и B. К ним иногда
добавляется четвертая компонента A (прозрачность). В OpenGL текущий цвет задается функциями glColor...().
В OpenGL двумерное изображение для вывода на экран хранится в виде массива значений цветов, соответствующих каждому пикселу изображения. Этот массив
называется цветовым буфером. Значения компонент R, G и B лежат в диапазоне от
0.0 (минимальная интенсивность) до 1.0 (максимальная интенсивность). Цветовое
пространство модели RGB, внутри которого располагаются все возможные цвета,
удобно представлять в виде куба (рис. 5.1). По трем координатным осям откладываются интенсивности красного, синего и зеленого цвета.
Рис. 5.1. Цветовое пространство RGB.
Если в OpenGL включено освещение, то для вычисления цвета пиксела в цветовом буфере производится ряд вычислений, в которых участвует цвет примитива.
Получившийся цвет не обязательно совпадает с цветом примитива, например, красный мяч в ярком синем свете будет выглядеть иначе, чем при белом свете.
2. Задание способа закраски
При рисовании примитивов можно задавать цвет в его вершинах. Цвет внутренних точек отрезка или многоугольника вычисляется в соответствии с установленным способом закраски. При плоской закраске отрезок или залитый многоугольник
рисуются одним цветом, а при плавной закраске – различными цветами. Способ закраски задается с помощью функции:
void glShadeModel(GLenum mode);
57
где mode может быть GL_SMOOTH (плавная закраска, значение по умолчанию)
или GL_FLAT (плоская закраска).
При плоской закраске примитив рисуется цветом первой вершины. При плавной закраске цвет каждой вершины рассматривается независимо и цвет промежуточных точек рассчитывается интерполяцией в цветовом пространстве (см. программу 2.1).
#include
#include
#include
#include
<windows.h>
<GL/gl.h>
<GL/glu.h>
<GL/glaux.h>
void triangle()
{
glBegin( GL_TRIANGLES );
glColor3f( 1.0, 0.0, 0.0 );
glVertex2f( 5.0, 5.0 );
glColor3f( 0.0, 1.0, 0.0 );
glVertex2f( 25.0, 5.0 );
glColor3f( 0.0, 0.0, 1.0 );
glVertex2f( 5.0, 25.0 );
glEnd();
}
void CALLBACK display()
{
glClear( GL_COLOR_BUFFER_BIT );
triangle();
glFlush();
}
void CALLBACK resize( int w, int h )
{
glViewport( 0, 0, w, h );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
if ( w <= h )
gluOrtho2D( 0.0, 30.0, 0.0, 30.0*(float)h/(float)w );
else
gluOrtho2D( 0.0, 30.0*(float)w/(float)h, 0.0, 30.0 );
glMatrixMode( GL_MODELVIEW );
}
void main()
{
auxInitDisplayMode( AUX_SINGLE | AUX_RGBA );
auxInitPosition( 0, 0, 400, 400 );
auxInitWindow ( "Лекция 5, программа 2.1");
glShadeModel( GL_SMOOTH );
auxReshapeFunc( resize );
auxMainLoop( display );
}
Программа 2.1. Рисование треугольника с плавной закраской.
58
3. Освещение
При вычислении цвета пикселей в цветовом буфере необходимо учитывать
действующие источники света и отражающие свойства материалов, из которых "сделаны" объекты сцены. Например, у моря разный цвет в ясный солнечный день и в
пасмурный облачный день. Наличие солнечного света или облаков определяет, каким
вы увидите море: ярко-бирюзовым или темно-серо-зеленым. Освещение очень важно,
и без него большинство объектов не будут выглядеть объемными (рис. 5.2).
Рис. 5.2. Освещенная и неосвещенная сфера.
Неосвещенная сфера на рис. 5.2 не отличается от плоского диска. Этот пример
показывает, что при построении трехмерной сцены крайне важно учитывать взаимодействие между источниками света и объектами сцены. Для создания реалистичных
изображений программист в OpenGL может изменять ряд свойств источников света и
материалов объектов.
4. Освещение в реальном мире и в OpenGL
Когда вы смотрите на поверхность реального объекта, то ее видимый цвет зависит от энергетического распределения фотонов, попадающих на колбочки сетчатки
глаза. Фотоны излучаются одним или несколькими источниками света. Некоторые из
них поглощаются поверхностью, некоторые отражаются. Различные поверхности могут обладать различными свойствами – некоторые блестящие и лучше отражают свет
по избранным направлениям, а другие рассеивают свет равномерно по всем направлениям. Большинство реальных объектов занимают промежуточное положение между этими крайними случаями.
В OpenGL свет и освещение аппроксимируются путем разложения на красную,
зеленую и синюю компоненту. Следовательно, цвет источника света описывается
компонентами красного, зеленого и синего излучаемого им света. Материал поверхности описывается количеством отражаемого красного, зеленого и синего света.
Уравнения модели освещения в OpenGL являются лишь приближением к действительности, но они дают довольно качественный результат и требуют относительно
несложных вычислений.
В модели освещения OpenGL считается, что свет излучается несколькими источниками света, которые можно независимо включать и выключать. Часть света
распространяется по определенным направлениям или из определенных точек, а часть
равномерно распределена в пространстве сцены. Например, когда в комнате включена электрическая лампа, то большая часть света распространяется от лампы, но некоторое количество попадает на объекты после отражения от одной или нескольких
стен. Этот отраженный свет (называемый рассеянным) обычно так распределен в
пространстве, что нельзя сказать, где расположен его источник, но при выключении
соответствующего источника света этот рассеянный свет исчезает.
59
В OpenGL может также присутствовать фоновое рассеянное освещение, существующее независимо от каких-либо источников света (аналогия для его объяснения –
это свет, претерпевший так много отражений, что источник определить невозможно).
В OpenGL источники света проявляют себя только при взаимодействии с поверхностями объектов, которые могут поглощать и отражать свет. Предполагается,
что каждая поверхность изготовлена из материала с определенными свойствами. Материал может излучать собственный свет (например, как передняя поверхность фары
автомобиля), может рассеивать падающий свет по всем направлениям, а также может
отражать некоторую часть падающего света по избранному направлению, подобно
зеркалу или блестящей поверхности.
Поэтому в модели освещения OpenGL освещение рассматривается состоящим
из четырех независимых компонент: излучаемый свет, рассеянный, диффузно отраженный и зеркально отраженный. Каждая компонента описывается тройкой значений
RGB. Все четыре компоненты вычисляются независимо, а затем складываются вместе
и применяются для вычисления цвета пиксела в цветовом буфере.
4.1 Излучаемый, рассеянный, диффузно отраженный и зеркально отраженный свет
Излучаемый свет объяснить проще остальных компонент – этот свет излучается
объектом и не зависит от других источников света.
Рассеянная компонента – это та часть света источника, которая будет рассеяна
в пространстве так, что его первоначальное направление будет невозможно определить. Рассеянный свет выглядит приходящим со всех направлений. Фоновое освещение в комнате всегда имеет большую рассеянную компоненту, поскольку большая
часть света попадает в глаз после многократных отражений от разных объектов. Точечные уличные источники света имеют малую рассеянную компоненту, у них большая часть света распространяется по избранному направлению. Когда рассеянный
свет падает на поверхность, то он отражается равномерно по всем направлениям.
Диффузный свет распространяется по избранному направлению, так что он выглядит ярче при падении перпендикулярно поверхности, а не под некоторым углом.
При падении на поверхность диффузный свет отражается равномерно по всем направлениям, поэтому его вид не зависит от позиции наблюдателя. Вероятно, любой
свет, исходящий из заданной точки или направления, имеет диффузную компоненту.
Зеркальный свет распространяется по избранному направлению и отражается
от поверхности тоже по некоторому определенному направлению. Лазерный луч, падающий на высококачественное зеркало, характеризуется практически 100процентным зеркальным отражением. Большая зеркальная компонента у блестящих
металлов и пластиков, а у мела и ковра она практически отсутствует. Зеркальную
компоненту можно представить себе как блеск поверхности.
Хотя источник света излучает свет, имеющий некоторое определенное распределение по частотам, но рассеянная, диффузная и зеркальная компоненты могут быть
различными. Например, если у вас в комнате с красными стенами есть белый источник света, то рассеянный свет будет больше красным, хотя падающий на объекты
свет белый. OpenGL позволяет независимо задавать значения красной, зеленой и синей компонент света.
60
4.2 Цвет материала
Цвет материала, из которого "сделан" объект, в OpenGL описывается количеством отраженного красного, зеленого и синего света. Например, идеально красный
мяч отражает весь падающий красный свет и полностью поглощает зеленый и синий.
В белом свете (он состоит из равного количества красного, зеленого и синего) этот
мяч будет выглядеть красным. В чистом красном свете мяч тоже будет красным. Но
если осветить мяч чистым зеленым или синим светом, то он будет выглядеть черным.
Материалы, как и источники света, характеризуются тремя компонентами света: рассеянной, диффузной и зеркальной. Значения компонент задаются как доли отраженных компонент света, падающего на поверхность из этого материала.
Компонента рассеянного отражения материала комбинируется с рассеянной
компонентой падающего на объект света от каждого источника. Диффузная и зеркальная компоненты материала аналогично комбинируются с соответствующими
компонентами источников света. Видимый цвет материала в основном зависит от
свойств рассеянного и диффузного отражения. Обычно эти компоненты материала
одинаковы или примерно равны. Зеркальная компонента материала обычно задается
белой или серой, так что цвет зеркальных бликов совпадает с цветом зеркальной компоненты источника света. Например, если освещать блестящую красную пластиковую сферу белым светом, то большая часть сферы будет выглядеть красной, а блестящий блик – белым.
4.3 Значения RGB для источников света и материалов
Цветовые значения световых компонент имеют разный смысл для источников
света и для материалов. Для источника света они определяют интенсивность излучения каждого из основных цветов. Например, если R, G и B равны 1.0, то свет будет
белым, максимальной интенсивности. Если R=G=1 и B=0 (максимальный красный и
зеленый, совсем нет синего), то свет будет выглядеть желтым.
Для материалов значения компонент определяют долю отраженного света каждого цвета. Например, если для материала задано R=1, G=0.5 и B=0, то этот материал
будет отражать весь падающий красный свет, половину зеленого и совсем не будет
отражать синий свет. Допустим, что цвет одной из компонент источника света равен
(LR, LG, LB), а цвет соответствующей компоненты материала равен (MR, MG, MB).
Тогда, если не учитывать другие эффекты отражения, в глаз наблюдателя будет попадать свет (LR*MR, LG*MG, LB*MB).
Аналогично, если есть два источника света (R1, G1, B1) и (R2, G2, B2), то
OpenGL сложит эти компоненты, и в глаз наблюдателя будет попадать свет (R1+R2,
G1+G2, B1+B2). Если одна из сумм больше 1, то она уменьшается до 1 – максимально
возможной интенсивности.
5. Пример: рисование освещенной сферы
Добавление освещения в сцену производится в следующем порядке:
1) У всех объектов для каждой вершины задается вектор нормали. Эти
векторы определяют ориентацию объекта относительно источников света.
2) Задаются местоположение и свойства одного или нескольких источников
света. Каждый источник необходимо включить.
61
3) Задаются параметры модели освещения, которые определяют уровень фонового рассеянного света и эффективную точку наблюдения (она используется
при вычислениях освещения).
4) Задаются свойства материалов объектов сцены.
Выполнение перечисленных действий показано в программе 5.1. Она рисует
сферу, освещенную одним источником света (см. рис. 5.2). Вызовы всех функций,
имеющих отношение к освещению, вынесены в функцию lightInit(). Эти функции кратко описываются в следующих параграфах.
#include
#include
#include
#include
<windows.h>
<GL/gl.h>
<GL/glu.h>
<GL/glaux.h>
void lightsInit()
{
float mat_specular[] = { 1.0, 1.0, 1.0, 1.0 };
float mat_shininess[] = { 50.0 };
float light_position[] = { 1.0, 1.0, 1.0, 0.0 };
float light_global_ambient[] = { 0.7, 0.7, 0.7, 1.0 };
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);
glLightModelfv( GL_LIGHT_MODEL_AMBIENT, light_global_ambient )
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glDepthFunc(GL_LEQUAL);
glEnable(GL_DEPTH_TEST);
}
void CALLBACK display()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
auxSolidSphere(1.0);
glFlush();
}
void CALLBACK resize( int width, int height )
{
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if ( width <= height )
glOrtho( -1.5, 1.5, -1.5*(float)height/width,
1.5*(float)height/width, -10.0, 10.0);
else
glOrtho( -1.5*(float)width/height,
1.5*(float)width/height, -1.5, 1.5, -10.0, 10.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void main()
{
auxInitDisplayMode( AUX_SINGLE | AUX_RGBA | AUX_DEPTH );
62
}
auxInitPosition( 0, 0, 400, 400 );
auxInitWindow( "Лекция 5, программа 5.1" );
lightsInit();
auxReshapeFunc( resize );
auxMainLoop( display );
Программа 5.1. Рисование освещенной сферы:
5.1 Вектора нормали в вершинах объектов
Нормали в вершинах объекта определяют его ориентацию относительно источников света. OpenGL использует нормаль вершины, чтобы вычислить, сколько света
от каждого источника попадает в эту вершину. В программе 5.1 нормали для вершин
явно не задаются, это делается внутри функции auxSolidSphere().
5.2 Создание, расположение и включение источников света
В программе 5.1 используется только один источник белого света. Его местоположение задается функцией glLightfv(). В данном примере для нулевого источника света (GL_LIGHT0) используется цвет по умолчанию (белый). Для изменения
цвета источника надо вызвать функцию glLight*(). OpenGL позволяет включить
до 8-ми источников света (у всех, кроме нулевого, по умолчанию задан черный цвет).
Эти источники можно располагать в любых местах – вблизи сцены (как настольную
лампу), или бесконечно далеко (как солнце). Кроме того, можно управлять свойствами светового пучка – сделать его узким сфокусированным или широким. Необходимо
помнить, что каждый источник света приводит к усложнению вычислений. Поэтому
производительность программы зависит от количества включенных источников.
После задания характеристик источников света, необходимо включить каждый
источник функцией glEnable(). Предварительно необходимо вызвать эту функцию
с параметром GL_LIGHTING, чтобы подготовить OpenGL к выполнению расчетов освещения.
5.3 Выбор модели освещения
Функция glLightModel*() предназначена для задания параметров модели
освещения. В программе 5.1 явно задается только один подобный параметр – яркое
фоновое рассеянное освещение. Модель освещения также определяет, где надо располагать наблюдателя – на бесконечно большом расстоянии или близко от сцены, и
не должны ли расчеты освещения выполняться по–разному для передних и задних
сторон объектов сцены. В программе 5.1 используются параметры модели "по умолчанию" – бесконечно удаленный наблюдатель и одностороннее освещение. Использование локального наблюдателя существенно усложняет расчеты, т.к. OpenGL должна
будет вычислять угол между точкой наблюдения и каждым объектом. Для бесконечно
далекого наблюдателя этот угол не учитывается, поэтому результаты слегка теряют
реалистичность. Далее, т.к. в этом примере внутренняя поверхность сферы никогда не
видна, то достаточно одностороннего освещения.
63
5.4 Задание свойств материалов для объектов сцены
Свойства материала объекта определяют, как он будет отражать свет, и, значит,
как будет выглядеть этот материал. Взаимодействие между поверхностью объекта и
падающим светом – сложный физический процесс. Задать свойства материала в приближенной модели освещения OpenGL так, чтобы объект выглядел похожим на реальный, довольно сложное дело, требующее навыка. Для материала можно независимо задавать излучаемую, рассеянную, диффузную и зеркальную компоненты, а также
блеск. В программе 5.1 с помощью функции glMaterialfv() задаются два свойства
материала – цвет зеркальной компоненты (GL_SPECULAR) и блеск (GL_SHININESS).
6. Создание источников света
У источников света есть набор свойств – цвет разных компонент света, местоположение и направление излучения. Для задания всех свойств источника света используется функция glLight*():
void glLight{if}[v](GLenum light, GLenum pname, TYPE param);
Параметр light – это номер источника света (от GL_LIGHT0 до GL_LIGHT7).
Параметр pname обозначает изменяемое свойство (см. табл. 5.1). Новое значение
свойства pname передается в параметре param.
Таблица 5.1. Константы, обозначающие свойства источника света, и значения
этих свойств "по умолчанию".
Имя свойства
По умолчанию
Смысл
GL_AMBIENT
(0.0, 0.0, 0.0, 1.0) рассеянная компонента света
GL_DIFFUSE
(1.0, 1.0, 1.0, 1.0) диффузная компонента света
GL_SPECULAR
(1.0, 1.0, 1.0, 1.0) зеркальная компонента света
GL_POSITION
(0.0, 0.0, 1.0, 0.0) координаты источника света (x, y, z, w)
GL_SPOT_DIRECTION
(0.0, 0.0, -1.0)
направление источника света (x, y, z)
GL_SPOT_EXPONENT
0.0
показатель экспоненциального затухания прожектора
GL_SPOT_CUTOFF
180.0
угол отсечки прожектора
GL_CONSTANT_ATTENUATION
1.0
постоянный коэффициент затухания
GL_LINEAR_ATTENUATION
0.0
линейный коэффициент затухания
GL_QUADRATIC_ATTENUATION 0.0
квадратичный коэффициент затухания
Значения по умолчанию для свойств GL_DIFFUSE и GL_SPECULAR в табл. 5.1
верны только для GL_LIGHT0. Для остальных источников света значения этих компонент по умолчанию равны (0.0, 0.0, 0.0, 1.0) (т.е. остальные источники света по умолчанию ничего не излучают даже во включенном состоянии).
Ниже приведен пример использования функции glLightfv(), показывающий,
что для задания свойств GL_AMBIENT, GL_DIFFUSE и GL_SPECULAR необходимы отдельные вызовы этой функции:
float light_ambient[] = { 0.0, 0.0, 0.0, 1.0 };
float light_diffuse[] = { 1.0, 1.0, 1.0, 1.0 };
float light_specular[] = { 1.0, 1.0, 1.0, 1.0 };
float light_position[] = { 1.0, 1.0, 1.0, 0.0 };
glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient);
glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);
glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular);
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
64
6.1 Цвет
OpenGL позволяет указать для каждого источника света цветовые значения
трех компонент света – GL_AMBIENT, GL_DIFFUSE и GL_SPECULAR. Компонента
GL_AMBIENT определяет вклад источника в общее рассеянное освещение. Как видно
из табл. 5.1, по умолчанию рассеянная компонента отсутствует, т.е. GL_AMBIENT равна (0.0, 0.0, 0.0, 1.0). В программе 6.1 задается фоновое яркое белое рассеянное освещение. Если изменить его на синее, то сфера будет выглядеть иначе:
float light_global_ambient[]= { 0.0, 0.0, 1.0, 1.0};
glLightModelfv( GL_LIGHT_MODEL_AMBIENT, light_global_ambient );
Компонента GL_DIFFUSE, вероятно, ближе всего похожа на то, что можно назвать "цветом света". Эта компонента определяет количество диффузного света, добавляемого источником к суммарному освещению сцены. Для GL_LIGHT0 по умолчанию GL_DIFFUSE равен (1.0, 1.0, 1.0, 1.0) (яркий белый цвет). Для остальных источников (GL_LIGHT1, ... , GL_LIGHT7) значение по умолчанию равно (0.0, 0.0, 0.0,
0.0).
Параметр GL_SPECULAR влияет на цвет зеркальных бликов на объекте. Обычно
в реальном мире цвет зеркального блика на объекте, например, на стеклянной бутылке, совпадает с цветом источника света (обычно белый). Следовательно, если вы хотите получить реалистичное изображение бликов, надо задавать GL_SPECULAR равным значению GL_DIFFUSE. По умолчанию GL_SPECULAR равен (1.0, 1.0, 1.0, 1.0) для
GL_LIGHT0 и (0.0, 0.0, 0.0, 0.0) для остальных источников света.
6.2 Местоположение и затухание
Источник света можно расположить или бесконечно далеко, или вблизи сцены.
Источники первого типа называются направленными источниками света. Для бесконечно далекого источника (например, Солнца) можно полагать, что лучи света от него падают на объект параллельным пучком. Источники второго типа – локальные,
расположенные в непосредственной близости от освещаемого объекта. От их конкретного местоположения зависит, под каким направлением свет будет падать на
объекты сцены. Пример локального источника света – настольная лампа.
В программе 5.1 создается направленный источник света:
float light_position[] = { 1.0, 1.0, 1.0, 0.0 };
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
В качестве значения для свойства GL_POSITION указывается массив (x, y, z, w).
Если четвертая координата w=0, то источник света будет считаться направленным, и
его направление будет задаваться вектором (x, y, z). По умолчанию GL_POSITION
равно (0, 0, 1, 0), т.е. это направленный источник света, излучающий свет вдоль отрицательного направления оси z.
Если w≠0, то источник света будет считаться локальным и (x, y, z) задает местоположение источника света. Локальный источник света излучает по всем направлениям, но можно ограничить конус излучения, сделав источник "прожектором".
Интенсивность излучения реальных источников света уменьшается с расстоянием от источника. Для направленных источников говорить о затухании с расстоянием бессмысленно, т.к. они располагаются бесконечно далеко от сцены. Поэтому свой65
ства затухания можно задавать только для локальных источников. При вычислении
интенсивности локальных источников OpenGL использует коэффициент затухания:
Ka =
1
,
kc + kl d + k q d 2
где d – расстояние от источника света до вершины объекта;
kc – значение свойства GL_CONSTANT_ATTENUATION;
kl – значение свойства GL_LINEAR_ATTENUATION;
kq – значение свойства GL_QUADRATIC_ATTENUATION.
по умолчанию kc=1.0, а kl=kq=0. При необходимости эти значения можно изменить, например:
glLightf(GL_LIGHT0, GL_CONSTANT_ATTENUATION, 2.0);
glLightf(GL_LIGHT0, GL_LINEAR_ATTENUATION, 1.0);
glLightf(GL_LIGHT0, GL_QUADRATIC_ATTENUATION, 0.5);
6.3 Прожекторы
У локального источника света есть свойство для задания конуса, внутри которого распространяется свет от источника. По умолчанию свет распространяется по
всем направлениям. Если его ограничить, источник можно считать "прожектором",
освещающим только избранные области сцены. В свойстве GL_SPOT_CUTOFF (угол
отсечки прожектора) хранится значение угла между направлением прожектора и образующей конуса. Угол при вершине конуса равен удвоенному значению
GL_SPOT_CUTOFF (см. рис. 5.3).
Рис. 5.3. Свойство GL_SPOT_CUTOFF.
Свет от локального источника распространяется только внутри конуса. По
умолчанию свойство GL_SPOT_CUTOFF равно 180.0, т.е. свет распространяется по
всем направлениям (угол при вершине конуса равен 360 градусов, т.е. конуса вообще
нет). Значение GL_SPOT_CUTOFF надо выбирать в интервале [0, 90] (или особое
значение 180.0). Например, прожектор с 90-градусным конусом создается так:
glLightf(GL_LIGHT0, GL_SPOT_CUTOFF, 45.0);
Для прожектора необходимо также указать направление оси светового конуса:
float spot_direction[] = { -1.0, -1.0, 0.0 };
glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, spot_direction);
По умолчанию направление прожектора равно (0.0, 0.0, -1.0). Т.е. если не указать явно GL_SPOT_DIRECTION, то свет будет распространяться вдоль отрицательного направления оси z. Направление оси прожектора подвергается видовым преобразованиям аналогично векторам нормали.
Кроме угла отсечки и направления, у прожектора есть два способа управления
распределением интенсивности света внутри конуса. Во-первых, это задание коэффи66
циента затухания, как описано в п.6.2. Во-вторых, есть свойство GL_SPOT_EXPONENT,
по умолчанию равное 0, которое управляет концентрацией света. В центре конуса интенсивность света выше, чем у краев. По направлению к краям интенсивность
уменьшается пропорционально косинусу угла между осью конуса и направлением на
освещаемую
вершину.
Пропорциональность
экспоненциальная,
а
GL_SPOT_EXPONENT – это показатель экспоненты. Чем больше это значение, тем
сильнее фокусировка прожектора.
6.4 Использование нескольких источников света
OpenGL позволяет создать до 8-ми источников света, но чем больше источников включено, тем дольше выполняется расчет освещения. При создании дополнительных источников надо обязательно указывать все их параметры, т.к. значения по
умолчанию для нулевого источника (табл. 5.1) отличаются от остальных. Ниже приведены функции для создания белого прожектора с затуханием и фокусировкой:
float light1_ambient[] = { 0.2f, 0.2f, 0.2f, 1.0f };
float light1_diffuse[] = { 1.0, 1.0, 1.0, 1.0 };
float light1_specular[] = { 1.0, 1.0, 1.0, 1.0 };
float light1_position[] = { -1.5, -1.5, 3.0, 1.0 };
float spot_direction[] = { 0.0, 1.0, -1 };
glLightfv(GL_LIGHT1, GL_AMBIENT, light1_ambient);
glLightfv(GL_LIGHT1, GL_DIFFUSE, light1_diffuse);
glLightfv(GL_LIGHT1, GL_SPECULAR, light1_specular);
glLightfv(GL_LIGHT1, GL_POSITION, light1_position);
glLightf(GL_LIGHT1, GL_CONSTANT_ATTENUATION, 1.5);
glLightf(GL_LIGHT1, GL_LINEAR_ATTENUATION, 0.5);
glLightf(GL_LIGHT1, GL_QUADRATIC_ATTENUATION, 0.2f);
glLightf(GL_LIGHT1, GL_SPOT_CUTOFF, 45.0);
glLightfv(GL_LIGHT1, GL_SPOT_DIRECTION, spot_direction);
glLightf(GL_LIGHT1, GL_SPOT_EXPONENT, 2.0);
glEnable(GL_LIGHT1);
Если добавить эти строки в программу 5.1, то сфера будет освещаться двумя
источниками: одним направленным и одним прожектором.
6.5 Изменение местоположения источников света
Местоположение и направление источников света, как и координаты вершин, в
OpenGL могут подвергаться модельным преобразованиям. Точнее, при вызове функции glLight*() для задания местоположения или направления источника света переданные координаты преобразуются в соответствии с текущей видовой матрицей. В
этом параграфе кратко описываются два варианта расположения источника света:
• источник света находится в фиксированной точке;
• источник света перемещается относительно неподвижного объекта.
В простейшем случае (например, в программе 5.1) положение (или направление) источника света не изменяется. Координаты (или направление) такого источника
определяются сразу после выбора видовой матрицы:
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
67
if ( width <= height )
glOrtho( -1.5, 1.5, -1.5*(float)height/width,
1.5*(float)height/width, -10.0, 10.0);
else
glOrtho( -1.5*(float)width/height,
1.5*(float)width/height, -1.5, 1.5, -10.0, 10.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
// Далее, внутри функции lightsInit()
float light_position[] = { 1.0, 1.0, 1.0, 0.0 };
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
Итак, сначала задаются оконное преобразование и проекционная матрица. Затем в качестве видовой матрицы загружается единичная и затем определяется направление нулевого источника света. Т.к. используется единичная видовая матрица,
то направление источника в мировой системе координат будет (1.0, 1.0, 1.0).
Предположим, что необходимо поворачивать и/или сдвигать локальный источник света так, чтобы он двигался относительно неподвижного объекта. Для это надо
задавать координаты источника после видовых преобразований, точно так же, как это
делается при размещении объектов сцены. Ниже приведена функция display(), в
которой источник света поворачивается на угол spin вокруг неподвижного тора (переменная spin изменяется в фоновой функции или в обработчике событий).
void CALLBACK display()
{
float light_position[] = { 0.0, 0.0, 1.5, 1.0 };
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glPushMatrix();
glTranslatef(0.0, 0.0, -5.0);
glPushMatrix();
glRotated((double)spin, 1.0, 0.0, 0.0);
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
glPopMatrix();
auxSolidTorus(0.275, 0.85);
glPopMatrix();
glFlush();
}
4. Сводка результатов
Взаимодействие света с предметами реального мира – сложный процесс. На
зрительную картину, воспринимаемую человеком, влияют расположение источников
света, распределение их излучения по длинам волн, рельеф отражающих поверхностей, свойства атмосферы и др.
В OpenGL используется приближенная модель освещения, дающая результаты
приемлемого качества при не очень громоздких вычислениях. Световое излучение
представляется в виде 4-х компонент: излучаемый свет, рассеянный свет, диффузно
отраженный и зеркально отраженный. Эти световые компоненты являются свойствами источников света и материалов. В OpenGL у источников света и материалов довольно много свойств, влияющих на изображение сцены. В данной лекции подробно
описаны свойства источников света.
68
5. Упражнения
Упражнение 1
С помощью программы 2.1 выясните различие между плавной и плоской закраской треугольника.
Упражнение 2
Проверьте, как будет работать программа 5.1 с каждым из трех изменений:
• замените направленный источник света на локальный белый источник;
• задайте синее фоновое освещение максимальной интенсивности;
• к направленному белому источнику света добавьте еще один цветной прожектор (см. п.6.4).
Упражнение 3
С использованием функции display() из п.6.5 разработайте программу, показывающую тор, освещаемый вращающимся вокруг него локальным источником света. Затем измените эту программу следующим образом:
• Сделайте так, чтобы источник света не вращался вокруг тора, а
перемещался относительно него прямолинейно.
Подсказка: в функции display() вместо glRotated() примените
функцию glTranslated() и заведите вместо spin подходящую переменную для хранения значения смещения источника света.
• Задайте коэффициент затухания так, чтобы интенсивность света уменьшалась по мере удаления от источника. Подсказка: коэффициент затухания
задается с помощью функции glLight*().
69
ЛЕКЦИЯ 6. Свойства материала и спецэффекты освещения
1. Задание свойств материала
На общее освещение сцены и вид объектов, кроме источников света, влияют
также отражающие и поглощающие свойства объектов – свойства материала, из которого "сделаны" объекты. Часть этих свойств похожи на свойства источников света:
рассеянная, диффузная и зеркально отраженная компоненты света. Есть также свойства "блеск" и "излучаемый свет". Все свойства материала задаются с помощью
функции:
void glMaterial{if}[v](GLenum face, GLenum pname, TYPE param);
Параметр pname выбирает свойство материала (табл. 6.1), face указывает сторону объектов (GL_FRONT, GL_BACK или GL_FRONT_AND_BACK). Новое значение
свойства передается в параметре param. Значение блеска (GL_SHININESS) можно передать непосредственно, а для остальных свойств надо передавать указатель на массив. У многих материалов рассеянная и диффузная компоненты одинаковые, поэтому
pname, равное GL_AMBIENT_AND_DIFFUSE, позволяет задать сразу два свойства.
В табл. 6.1 обратите внимание, что значения большинства свойств материалов
– это цветовые значения (R, G, B, A). Назначение цветовой компоненты A (альфа) будет рассмотрено далее в п.2 "Смешение цветов".
Таблица 6.1. Свойства материала
Константа для выбора
Значение по
свойства
умолчанию
(0.2, 0.2, 0.2, 1.0)
GL_AMBIENT
(0.8, 0.8, 0.8, 1.0)
GL_DIFFUSE
GL_AMBIENT_AND_DIFFUSE
GL_SPECULAR
GL_SHININESS
(0.0, 0.0, 0.0, 1.0)
0.0
GL_EMISSION
(0.0, 0.0, 0.0, 1.0)
Смысл
Цвет рассеянной компоненты света, отражаемого материалом.
Цвет диффузно отражаемой компоненты
Одинаковый цвет диффузно отражаемой
и рассеянной компонент
Цвет зеркально отражаемой компоненты
Блеск (показатель экспоненты для расчета зеркального отражения)
Цвет излучаемой компоненты
1.1 Диффузное и рассеянное отражение
Цвет диффузно и рассеянно отраженного объектом света задается свойствами
GL_DIFFUSE и GL_AMBIENT. Видимый цвет объекта в основном определяется диффузным отражением. Его интенсивность зависит от угла падения света. Диффузно отраженный свет имеет максимальную интенсивность, когда свет падает перпендикулярно поверхности. Диффузное отражение происходит по всем направлениям, так что
видимый цвет объекта от положения наблюдателя не зависит.
Рассеянное отражение тоже влияет на видимый цвет объекта. Т.к. диффузное
отражение ярче при прямом освещении, то рассеянное отражение становится заметным, когда нет прямого падения света на объект. Полное рассеянное отражение объекта формируется из фонового рассеянного света и рассеянного света источников.
Рассеянно отраженный свет (как и диффузно отраженный), не зависит от положения
наблюдателя.
70
У многих реальных объектов диффузное и рассеянное отражение одного цвета.
Поэтому в OpenGL есть возможность одновременного задания этих свойств одним
вызовом glMaterial*(). Например, для задания обеим сторонам многоугольников
глубокого синего цвета в качестве диффузного и рассеянного отражения, надо произвести вызов:
float ambdiff[] = { 0.1, 0.5, 0.8, 1.0 };
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, ambdiff);
1.2 Зеркальное отражение
Блики на поверхности объекта формируются за счет зеркального отражения. В
отличие от рассеянного и диффузного отражения, количество зеркально отраженного
света, воспринимаемого наблюдателем, зависит от его положения. Например, представьте, что вы смотрите на улице в солнечную погоду на полированный металлический шар. При повороте головы солнечный блик на шаре будет немного смещаться,
но если вы повернете голову слишком сильно, то можете потерять блик из вида.
Для материала в OpenGL можно задавать цвет зеркально отраженного света
(свойство GL_SPECULAR), а также управлять размером и яркостью блика (свойство
GL_SHININESS). Значение блеска (GL_SHININESS) выбирается из диапазона [0.0,
128.0] – чем больше значение, тем меньше и ярче (более сфокусирован) блик.
1.3 Излучаемый свет
Свойство GL_EMISSION позволяет сделать объект светящимся. Большинство
реальных объектов (кроме источников света) свет не излучают, поэтому это свойство
в основном применяется для имитации ламп и других источников света. Светящиеся
объекты выглядят яркими, но их излучение для расчетов освещения других объектов
не используется. Чтобы светящийся объект действительно играл роль источника света, надо создать в его позиции отдельный источник света OpenGL.
1.4 Изменение свойств материала
Ниже приведен фрагмент программы 6.1 (часть функции display()) для рисования четырех сфер, расположенных в одной строке. Для этих сфер выбираются
различные свойства материала.
float
float
float
float
float
float
float
float
float
no_mat[] = { 0.0, 0.0, 0.0, 1.0 };
mat_ambient[] = { 0.7, 0.7, 0.7, 1.0 };
mat_ambient_color[] = { 0.8, 0.8, 0.2, 1.0 };
mat_diffuse[] = { 0.1, 0.5, 0.8, 1.0 };
mat_specular[] = { 1.0, 1.0, 1.0, 1.0 };
no_shininess[] = { 0.0 };
low_shininess[] = { 5.0 };
high_shininess[] = { 100.0 };
mat_emission[] = {0.3, 0.2, 0.2, 0.0};
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
/* Рисование сферы в первой строке, первом столбце только с диффузным
отражением, без рассеянной и зеркальной компонент */
glPushMatrix();
71
glTranslatef (-3.75, 3.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, no_mat);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, no_mat);
glMaterialfv(GL_FRONT, GL_SHININESS, no_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, no_mat);
auxSolidSphere( 1.0 );
glPopMatrix();
/* Рисование сферы в первой строке, втором столбце с диффузным и
зеркальным отражением. Малый блеск, нет рассеянной компоненты */
glPushMatrix();
glTranslatef (-1.25, 3.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, no_mat);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_SHININESS, low_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, no_mat);
auxSolidSphere( 1.0 );
glPopMatrix();
/* Рисование сферы в первой строке, третьем столбце с диффузным и
зеркальным отражением. Высокий блеск, нет рассеянной компоненты */
glPushMatrix();
glTranslatef (1.25, 3.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, no_mat);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_SHININESS, high_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, no_mat);
auxSolidSphere( 1.0 );
glPopMatrix();
/* Рисование сферы в первой строке, четвертом столбце с диффузным
отражением и излучением. Нет рассеянной и зеркальной компонент */
glPushMatrix();
glTranslatef (3.75, 3.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, no_mat);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, no_mat);
glMaterialfv(GL_FRONT, GL_SHININESS, no_shininess);
glMaterialfv(GL_FRONT, GL_EMISSION, mat_emission);
auxSolidSphere( 1.0 );
glPopMatrix();
Фрагмент программы 6.1. Использование материалов с различными свойствами.
Функция glMaterialfv() в программе 6.1 для задания свойств материала каждой сферы вызывается по несколько раз. Но ее можно вызывать только для тех
свойств, значения которых надо изменить. Например, рассеянная и диффузная компоненты второй и третьей сферы совпадают с соответствующими значениями первой
сферы. Поэтому эти свойства повторно можно не устанавливать. Вызов
glMaterial*() требует некоторого времени, поэтому для увеличения скорости работы программы надо стараться уменьшать количество изменений свойств материала.
Кроме явных вызовов функции glMaterial*(), в OpenGL есть еще одно
средство изменения свойств материала – функция glColorMaterial():
void glColorMaterial(GLenum face, GLenum mode);
72
Эта функция позволяет установить режим, при котором свойства материала
автоматически изменяться при задании текущего цвета (функцией
glColor*()). Текущий цвет будет присваиваться свойствам материала mode для сторон face. Параметр face может быть GL_FRONT, GL_BACK или GL_FRONT_AND_BACK
(по умолчанию). Допустимые значения параметра mode – константы GL_AMBIENT,
GL_DIFFUSE, GL_AMBIENT_AND_DIFFUSE (по умолчанию), GL_SPECULAR или
GL_EMISSION.
Чтобы режим синхронного задания текущего цвета и свойств материала начал
работать, надо после вызова glColorMaterial() вызвать функцию
glEnable(GL_COLOR_MATERIAL). В предыдущих лекциях свойства материала задавались именно так, например:
будут
glColorMaterial(GL_FRONT, GL_DIFFUSE);
glEnable(GL_COLOR_MATERIAL);
glColor3f(0.2, 0.5, 0.8);
// рисование каких-либо объектов
glColor3f(0.9, 0.0, 0.2);
// рисование каких-либо объектов
glDisable(GL_COLOR_MATERIAL);
Функцию glColorMaterial() удобно применять в тех случаях, когда для
объектов сцены изменяется только одно свойство материала. Если требуется менять
несколько, то придется выполнять отдельные вызовы glMaterial*(). Для отключения режима glColorMaterial()надо вызвать glDisable(GL_COLOR_MATERIAL).
Далее приведена программа 6.2, которая позволяет в интерактивном режиме
проверить работу функции glColorMaterial(). По нажатию трех клавиш курсора
(стрелка влево, вверх и вправо) эта программа циклически изменяет одну из компонент цвета диффузного отражения.
#include
#include
#include
#include
<windows.h>
<GL/gl.h>
<GL/glu.h>
<GL/glaux.h>
float diffuseMaterial[4] = { 0.5, 0.5, 0.5, 1.0 };
void light_init()
{
float mat_specular[] = { 1.0, 1.0, 1.0, 1.0 };
float light_position[] = { 1.0, 1.0, 1.0, 0.0 };
glMaterialfv( GL_FRONT, GL_DIFFUSE, diffuseMaterial );
glMaterialfv( GL_FRONT, GL_SPECULAR, mat_specular );
glMaterialf( GL_FRONT, GL_SHININESS, 25.0 );
glLightfv( GL_LIGHT0, GL_POSITION, light_position );
glEnable( GL_LIGHTING );
glEnable( GL_LIGHT0 );
glDepthFunc( GL_LEQUAL );
glEnable( GL_DEPTH_TEST );
glColorMaterial( GL_FRONT, GL_DIFFUSE );
glEnable( GL_COLOR_MATERIAL );
}
73
void CALLBACK changeRedDiffuse()
{
diffuseMaterial[0] += 0.1f;
if ( diffuseMaterial[0] > 1.0 )
diffuseMaterial[0] = 0.0;
glColor4fv( diffuseMaterial );
}
void CALLBACK changeGreenDiffuse()
{
diffuseMaterial[1] += 0.1f;
if ( diffuseMaterial[1] > 1.0 )
diffuseMaterial[1] = 0.0;
glColor4fv( diffuseMaterial );
}
void CALLBACK changeBlueDiffuse()
{
diffuseMaterial[2] += 0.1f;
if ( diffuseMaterial[2] > 1.0 )
diffuseMaterial[2] = 0.0;
glColor4fv( diffuseMaterial );
}
void CALLBACK display()
{
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
auxSolidSphere( 1.0 );
glFlush();
}
void CALLBACK resize( int w, int h )
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if ( w <= h )
glOrtho( -1.5, 1.5, -1.5*(float)h/(float)w,
1.5*(float)h/(float)w, -10.0, 10.0 );
else
glOrtho( -1.5*(float)w/(float)h,
1.5*(float)w/(float)h, -1.5, 1.5, -10.0, 10.0 );
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void main()
{
auxInitDisplayMode( AUX_SINGLE | AUX_RGBA | AUX_DEPTH );
auxInitPosition( 0, 0, 400, 400 );
auxInitWindow( "Лекция 6. Программа 6.2" );
light_init();
auxKeyFunc( AUX_LEFT, changeRedDiffuse );
auxKeyFunc( AUX_UP, changeGreenDiffuse );
auxKeyFunc( AUX_RIGHT, changeBlueDiffuse );
auxReshapeFunc( resize );
auxMainLoop( display );
}
Программа 6.2. Изменение свойств материала с помощью glColorMaterial().
74
1.5 Имитация реальных материалов
В табл. 6.2 приведены значения свойств, подобранных с целью имитации отражения/поглощения света предметами, изготовленными из нескольких реальных материалов, например, бронзы и золота.
Таблица 6.2. Значения свойств для некоторых часто используемых материалов
GL_AMBIENT
GL_DIFFUSE
GL_SPECULAR
Материал
Латунь
Бронза
Полир. бронза
Хром
Медь
Полированная медь
Золото
Полир. золото
Олово
Серебро
Полир. серебро
Изумруд
Нефрит
Обсидиан
Бирюза
Черный пластик
Черная резина
(0.33, 0.22, 0.03, 1.00)
(0.21, 0.13, 0.05, 1.00)
(0.25, 0.15, 0.06, 1.00)
(0.25, 0.25, 0.25, 1.00)
(0.19, 0.07, 0.02, 1.00)
(0.23, 0.09, 0.03, 0.03)
(0.25, 0.20, 0.07, 1.00)
(0.25, 0.22, 0.06, 1.00)
(0.11, 0.06, 0.11, 1.00)
(0.19, 0.19, 0.19, 1.00)
(0.23, 0.23, 0.23, 1.00)
(0.02, 0.17, 0.02, 0.55)
(0.14, 0.22, 0.16, 0.95)
(0.05, 0.05, 0.07, 0.82)
(0.10, 0.19, 0.17, 0.80)
(0.00, 0.00, 0.00, 1.00)
(0.02, 0.02, 0.02, 1.00)
(0.78, 0.57, 0.11, 1.00)
(0.71, 0.43, 0.18, 1.00)
(0.40, 0.24, 0.10, 1.00)
(0.40, 0.40, 0.40, 1.00)
(0.70, 0.27, 0.08, 1.00)
(0.55, 0.21, 0.07, 1.00)
(0.75, 0.61, 0.23, 1.00)
(0.35, 0.31, 0.09, 1.00)
(0.43, 0.47, 0.54, 1.00)
(0.51, 0.51, 0.51, 1.00)
(0.28, 0.28, 0.28, 1.00)
(0.08, 0.61, 0.08, 0.55)
(0.54, 0.89, 0.63, 0.95)
(0.18, 0.17, 0.23, 0.82)
(0.40, 0.74, 0.69, 0.80)
(0.01, 0.01, 0.01, 1.00)
(0.01, 0.01, 0.01, 1.00)
(0.99, 0.94, 0.81, 1.00)
(0.39, 0.27, 0.17, 1.00)
(0.77, 0.46, 0.20, 1.00)
(0.77, 0.77, 0.77, 1.00)
(0.26, 0.14, 0.09, 1.00)
(0.58, 0.22, 0.07, 1.00)
(0.63, 0.56, 0.37, 1.00)
(0.80, 0.72, 0.21, 1.00)
(0.33, 0.33, 0.52, 1.00)
(0.51, 0.51, 0.51, 1.00)
(0.77, 0.77, 0.77, 1.00)
(0.63, 0.73, 0.63, 0.55)
(0.32, 0.32, 0.32, 0.95)
(0.33, 0.33, 0.35, 0.82)
(0.30, 0.31, 0.31, 0.80)
(0.50, 0.50, 0.50, 1.00)
(0.40, 0.40, 0.40, 1.00)
GL_SHI
NINESS
27.9
25.6
76.8
76.8
12.8
51.2
51.2
83.2
9.8
51.2
89.6
76.8
12.8
38.4
12.8
32.0
10.0
2. Смешение цветов и прозрачность
У значения цвета в формате RGBA кроме трех цветовых компонент R, G, B
есть служебная компонента A (альфа). Ранее эта компонента всегда принималась равной 1.0 и подробно не рассматривалась. Значения альфа задаются при вызове
glColor*(), при использовании glClearColor() и при задании некоторых параметров освещения (например, свойств материала или интенсивности источника света). В предыдущей лекции говорилось о том, что пиксел на мониторе состоит из трех
маленьких точек, излучающих красный, зеленый и синий свет. Интенсивность излучения каждого цвета пропорциональна значениям трех компонент цвета R, G и B.
Компонента альфа не передается непосредственно на монитор. Она влияет на расчет
цвета пиксела, когда в OpenGL разрешен режим смешения цветов (blending). В зависимости от параметров этого режима и от значения альфа OpenGL по-разному комбинирует вычисленное значение цвета пиксела с тем значением, которое уже хранятся в цветовом буфере.
Если новый пиксел расположен ближе к наблюдателю, чем уже имеющийся, то
при отключенном смешении цветов новый пиксел просто записывается в цветовой
буфер вместо старого. Если смешение цветов разрешено, то OpenGL позволяет указать правило, по которому цвет существующего пиксела будет комбинироваться с новым вычисленным значением. Смешение цветов часто применяется для создания изображений полупрозрачных объектов, "сквозь которые" видны более далекие объекты.
Компоненту альфа можно считать характеристикой непрозрачности объекта. У
прозрачных и полупрозрачных поверхностей значения альфа меньше, чем у непро75
зрачных. Допустим, что вы смотрите на объект через зеленое стекло. Видимый цвет
объекта будет частично зеленым цветом стекла, а частично цветом материала объекта. Соотношение этих цветов зависит от пропускающих свойств стекла. Если стекло
пропускает 80% падающего цвета (т.е. имеет непрозрачность 20%), то вы увидите
цвет из 20% цвета стекла и 80% цвета объекта сзади него. Легко представить ситуации, когда имеется несколько полупрозрачных поверхностей. Например, если вы
смотрите на автомобиль, то его салон виден через одно стекло, а некоторые объекты
позади автомобиля – через два стекла.
2.1 Множители source (исходный пиксел) и destination (результирующего пиксела)
В процессе смешения цветов значение цвета нового пиксела (source) комбинируются со значениями соответствующего существующего пиксела (destination).
Эта операция состоит из двух шагов. Сначала надо выбрать способ вычисления множителей для интенсивностей пикселов source и destination. Эти множители являются четверками компонент RGBA, на которые умножают соответствующие компоненты R, G, B и A пикселов source и destination. Затем соответствующие компоненты пикселов source и destination складываются и дают результирующий
цвет.
Допустим, множители source и destination равны (SR, SG, SB, SA) и (DR, DG,
DB, DA), а цвета RGBA для пикселов source и destination равны (RS, GS, BS, AS) и
(RD, GD, BD, AD). Тогда результат смешения цветов вычисляется так:
(RSSR+RDDR, GSSG+GDDG, BSSB+BDDB, ASSA+ADDA)
Все компоненты полученного результата округляются до границ диапазона
[0,1].
Теперь рассмотрим способы вычисления множителей source и destination.
Они задаются функцией glBlendFunc():
void glBlendFunc(GLenum sfactor, GLenum dfactor)
Параметр sfactor задает способ вычисления множителя source, а параметр
dfactor – множителя destination (допустимые варианты см в табл. 6.3). В
табл. 6.3 значения RGBA для пикселов source и destination отмечены соответственно индексами S и D.
Таблица 6.3. Допустимые значения множителей source и destination
Константа
Для какого множителя Способ вычисления множителя
применимо
GL_ZERO
source или destination
(0, 0, 0, 0)
GL_ONE
source или destination
(1, 1, 1, 1)
GL_DST_COLOR
source
(RD, GD, BD, AD)
GL_SRC_COLOR
destination
(RS, GS, BS, AS)
GL_ONE_MINUS_DST_COLOR
source
(1, 1, 1, 1) – (RD, GD, BD, AD)
GL_ONE_MINUS_SRC_COLOR
destination
(1, 1, 1, 1) – (RS, GS, BS, AS)
GL_SRC_ALPHA
source или destination
(AS, AS, AS, AS)
GL_ONE_MINUS_SRC_ALPHA
source или destination
(1, 1, 1, 1) – (AS, AS, AS, AS)
GL_DST_ALPHA
source или destination
(AD, AD, AD, AD)
GL_ONE_MINUS_DST_ALPHA
source или destination
(1, 1, 1, 1) – (AD, AD, AD, AD)
GL_SRC_ALPHA_SATURATE
source
(f, f, f, 1); где f=min(AS, 1-AD)
76
Чтобы смешение цветов работало, необходимо разрешить его вызовом
glEnable(GL_BLEND) и включить обработку альфа-компоненты цвета функцией
glEnable(GL_ALPHA_TEST). Для запрещения этого режима надо вызывать
glDisable(GL_BLEND). Выбор констант GL_ONE для source и GL_ZERO для
destination дает тот же результат, что и запрещение смешения цветов. Это значения множителей по умолчанию.
2.2 Области применения смешения цветов
В практических целях применяются не все возможные комбинации множителей смешения. В большинстве программ используются комбинации из небольшого
набора. Ниже перечислены некоторые наиболее распространенные из них.
1) Допустим, требуется сформировать изображение, состоящее из перекрывающихся
изображений двух разных сцен (с прозрачностью 50%). Сначала надо установить
множитель source=GL_ONE и нарисовать первую сцену. Затем надо задать множители source=destination=GL_SRC_ALPHA и нарисовать вторую сцену с установленным альфа=0.5. Если надо получить смешение 75% первого изображения
и 25% второго, то сначала надо нарисовать первое изображение, и затем – второе с
альфа=0.25. При этом должны быть заданы множители GL_SRC_ALPHA (source) и
GL_ONE_MINUS_SRC_ALPHA (destination). Эта пара множителей образует, вероятно, самую распространенную комбинацию при смешения цветов.
2) Для смешения в одинаковой пропорции трех различных изображений надо установить множитель destination=GL_ONE и множитель source=GL_SRC_ALPHA.
Затем следует нарисовать каждое изображение с альфа=0.33. В данном случае каждое изображение будет иметь только треть исходной яркости, что особенно заметно в тех местах, где изображения не перекрываются.
3) Предположим, в графическом редакторе требуется реализовать кисть, которая позволяет при каждом мазке понемногу добавлять в изображение текущий цвет
(скажем, каждый мазок добавляет по 10% цвета, при этом 90% изображения убирается). Для этого надо рисовать образ кисти с альфа=10% при установленных
множителях source=GL_SRC_ALPHA и destination =GL_ONE_MINUS_SRC_ALPHA.
Аналогичным образом можно реализовать инструмент "стерка", постепенно добавляющий к изображению цвет фона.
4) Допустим, надо нарисовать изображение с тремя полупрозрачными поверхностями, перекрывающимися произвольным образом, а позади них расположен непрозрачный фон. Пусть самая дальняя поверхность пропускает 80% света, следующая
– 40%, а ближайшая к наблюдателю – 90%. Рисование этой сцены начинается с
рисования фона с множителями source и destination по умолчанию. Затем надо задать множители source=GL_SRC_ALPHA и destination=
GL_ONE_MINUS_SRC_ALPHA. После этого надо сначала нарисовать дальнюю поверхность с альфа=0.2, затем среднюю с альфа=0.6 и ближнюю с альфа=0.1.
2.3 Пример использования смешения цветов
Ниже приведена функция display() из программы 6.3, в которой смешение
цветов применяется для имитации прозрачности объектов. Смешение цветов и обра77
ботка альфа-компоненты замедляют работу OpenGL, поэтому их лучше включать
только на время рисования прозрачных объектов, а затем отключать.
void CALLBACK display()
{
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glEnable( GL_ALPHA_TEST );
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glColor4d( 1, 0, 0, 1 );
auxSolidSphere( 1 );
glColor4d( 0, 1, 0, 0.6 );
auxSolidCylinder( 2, 3 );
glDisable( GL_BLEND );
glDisable( GL_ALPHA_TEST );
glFlush();
}
Фрагмент программы 6.3. Рисование полупрозрачного цилиндра.
При смешении цветов крайне важно, чтобы сначала рисовались дальние объекты, а затем ближние. Это объясняется тем, как в OpenGL выполняется тест глубины
на основе z-буфера. Например, в программе 6.3 сначала рисуется сфера (более далекий объект), а затем цилиндр. При этом OpenGL выполняет следующие действия:
1) функция display() создает сферу;
2) для видимой части сферы тест глубины проходит успешно, т.к. сфера никакими
объектами не закрывается;
3) в буфере рисуется сфера;
4) функция display() создает цилиндр;
5) для видимой части цилиндра тест глубины проходит успешно, т.к. поверхность
цилиндра к наблюдателю ближе, чем сфера;
6) в буфере рисуется цилиндр, причем он накладывается на сферу с установленными
параметрами смешения цветов и получается эффект прозрачности.
Теперь рассмотрим, что происходит, если изменить порядок рисования объектов:
1) функция display() создает цилиндр;
2) для видимой части цилиндра тест глубины проходит успешно;
3) в буфере рисуется цилиндр;
4) функция display() создает сферу;
5) для всех вершин сферы тест глубины выполняется неудачно, т.к. сфера полностью
закрыта цилиндром;
6) сфера в буфере не рисуется.
3. Туман
Компьютерные изображения иногда выглядят нереально резкими и качественными. Резкость краев объектов уменьшается с помощью сглаживания. Еще одно
средство для создания реалистичных изображений – добавление "тумана", который
делает объекты менее четкими по мере увеличения расстояния от наблюдателя. Термином "туман" (fog) в OpenGL обозначается целая группа атмосферных эффектов,
78
таких, как дымка, пасмурность, копоть и т.п. Например, туман часто используется в
авиационных симуляторах.
При включенном тумане цвет более далеких объектов постепенно переходит в
цвет тумана. OpenGL позволяет задать цвет и плотность тумана (она определяет скорость, с которой образы объектов расплываются с расстоянием). Расчет тумана
уменьшает скорость работы программы.
3.1 Использование тумана
Для использования тумана сначала надо включить его, а затем настроить свойства. Включение выполняется вызовом glEnable(GL_FOG), а определение свойств –
вызовами функции glFog*():
void glFog{if}{v}(GLenum pname, TYPE param);
Она позволяет выбрать цвет, плотность (density) и уравнение для расчета множителя тумана f, на который будет умножается цвет в цветовом буфере. Есть три
возможных уравнения:
GL_EXP:
f = e − density⋅ z
2
f = e − ( density⋅z )
end − z
GL_LINEAR:
f =
end − start
У функции glFog*() параметр pname выбирает изменяемое свойство –
GL_FOG_MODE (уравнение для расчета множителя), GL_FOG_COLOR (цвет),
GL_FOG_DENSITY (плотность), GL_FOG_START (дальность начала тумана) или
GL_FOG_END (дальность завершения тумана). Для свойства GL_FOG_MODE допустимы
перечисленные выше значения GL_EXP (по умолчанию), GL_EXP2 или GL_LINEAR.
Значения по умолчанию других параметров: density=1, start=0, end=1.
GL_EXP2:
Применение тумана показано в программе 6.4. Она рисует пять желтых чайников (из полированного золота), расположенных на разном расстоянии от наблюдателя. По нажатию левой кнопки мыши программа меняет уравнение для расчета множителя тумана.
#include
#include
#include
#include
#include
#include
<iostream.h>
<windows.h>
<math.h>
<GL/gl.h>
<GL/glu.h>
<GL/glaux.h>
GLint fogMode;
void CALLBACK cycle_fog( AUX_EVENTREC* )
{
if ( fogMode == GL_EXP ) {
fogMode = GL_EXP2;
cout << "Fog mode is GL_EXP2\n";
}
else if ( fogMode == GL_EXP2 ) {
fogMode = GL_LINEAR;
cout << "Fog mode is GL_LINEAR\n";
glFogf( GL_FOG_START, 1.0 );
glFogf( GL_FOG_END, 5.0 );
79
}
}
else if ( fogMode == GL_LINEAR )
{
fogMode = GL_EXP;
cout << "Fog mode is GL_EXP\n";
}
cout.flush();
glFogi( GL_FOG_MODE, fogMode );
void light_init()
{
float pos[] = { 0.0, 0.0, 1.0, 0.0 };
glEnable( GL_DEPTH_TEST );
glDepthFunc( GL_LEQUAL );
glLightfv( GL_LIGHT0, GL_POSITION, pos );
glEnable( GL_LIGHTING );
glEnable( GL_LIGHT0 );
glEnable(GL_AUTO_NORMAL);
glEnable(GL_NORMALIZE);
glEnable( GL_FOG );
float fogColor[4] = { 0.5, 0.5, 0.5, 1.0 };
fogMode = GL_EXP;
glFogi( GL_FOG_MODE, fogMode );
glFogfv( GL_FOG_COLOR, fogColor );
glFogf( GL_FOG_DENSITY, 0.35f );
glClearColor( 0.5, 0.5, 0.5, 1.0 );
}
void draw_red_teapot( float x, float y, float
{
float gold_amb[4] = { 0.25f, 0.22f, 0.06f,
float gold_diff[4] = { 0.35f, 0.31f, 0.09f,
float gold_spec[4] = { 0.80f, 0.72f, 0.21f,
float gold_shin = 83.2f;
z )
1.00 };
1.00 };
1.00 };
glMaterialfv( GL_FRONT, GL_AMBIENT, gold_amb );
glMaterialfv( GL_FRONT, GL_DIFFUSE, gold_diff );
glMaterialfv( GL_FRONT, GL_SPECULAR, gold_spec );
glMaterialf( GL_FRONT, GL_SHININESS, gold_shin );
glPushMatrix();
glTranslatef( x, y, z );
auxSolidTeapot( 1.0 );
glPopMatrix();
}
void CALLBACK display()
{
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
draw_red_teapot( -4.0, -0.5, -1.0 );
draw_red_teapot( -2.0, -0.5, -2.0 );
draw_red_teapot( 0.0, -0.5, -3.0 );
draw_red_teapot( 2.0, -0.5, -4.0 );
draw_red_teapot( 4.0, -0.5, -5.0 );
glFlush();
80
}
void CALLBACK resize( int w, int h )
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if ( w <= (h*3) )
glOrtho( -6.0, 6.0, -2.0*((float)h*3)/(float)w,
2.0*((float)h*3)/(float)w, 0.0, 10.0 );
else
glOrtho( -6.0*(float)w/((float)h*3),
6.0*(float)w/((float)h*3), -2.0, 2.0, 0.0, 10.0 );
gluLookAt( 0,0,3, 0,0,0, 0,1,0 );
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void main()
{
auxInitDisplayMode( AUX_SINGLE | AUX_RGBA | AUX_DEPTH );
auxInitPosition( 0, 0, 450, 150 );
auxInitWindow( "Лекция 6. Программа 6.4" );
light_init();
auxMouseFunc( AUX_LEFTBUTTON, AUX_MOUSEDOWN, cycle_fog );
auxReshapeFunc( resize );
auxMainLoop( display );
}
Программа 6.4. Пять чайников в тумане.
4. Сводка результатов
OpenGL позволяет задавать свойства материала, который приписывается объектам сцены. Значения этих свойств используются в модели освещения при вычислении цвета пикселей изображения сцены. Основными свойствами материала являются
компоненты диффузного, рассеянного и зеркального отражения. Эти свойства напоминают свойства источников света. Кроме того, у материала есть свойство "блеск" и
может быть свойство "излучаемый свет", который влияет только на вид объекта и не
играет роли нового источника света. В лекции приведена таблица со свойствами материала, которые соответствуют некоторым реальным материалам.
Значения цвета в OpenGL задаются четырьмя компонентами RGBA. Четвертая
компонента A (альфа) используется в служебных целях для расчета цвета пикселей в
режиме смешения цветов. В лекции описано, как применить режим смешения цветов
для реализации одного из наиболее распространенных эффектов освещения – прозрачности.
Компьютерные изображения в некоторых случаях выглядят чересчур высококачественными и нереалистичными. Одно из простейших средств повышения реалистичности изображений – туман. OpenGL позволяет задать ряд свойств тумана, например, цвет и плотность. Когда туман включен, то по мере удаления от наблюдателя
объекты постепенно расплываются и закрашиваются цветом тумана.
81
5. Упражнения
Упражнение 1
На основе фрагмента программы 6.1 напишите программу, рисующую 12 сфер
с различными свойствами материала. Сферы должны располагаться в три строки, которые отличаются рассеянной составляющей:
Cтрока 1 – рассеянная компонента отсутствует, т.е. равна {0.0, 0.0, 0.0, 0.0};
Cтрока 2 – серая рассеянная компонента, {0.7, 0.7, 0.7, 1.0};
Cтрока 3 – желтая рассеянная компонента, {0.8, 0.8, 0.2, 1.0}.
Свойства материала в разных столбцах задаются аналогично фрагменту 6.1:
Столбец 1 – синяя диффузная составляющая { 0.1, 0.5, 0.8, 1.0 }, зеркальная отсутствует, блеска нет;
Столбец 2– синяя диффузная, белая зеркальная { 1.0, 1.0, 1.0, 1.0 } и малый
блеск 5.0;
Столбец 3 – синяя диффузная, белая зеркальная, большой блеск 100.0;
Столбец 4 – синяя диффузная, нет зеркальной, нет блеска, но есть зеленоватая
излучаемая компонента {0.3, 0.2, 0.2, 0.0}.
Обратите внимание, как меняется вид сфер, например, светящиеся сферы выглядят довольно яркими и однородными (т.к. нет зеркальной составляющей).
Упражнение 2
Выполните программу 6.2 и затем добавьте в нее обработчики событий для изменения блеска и излучаемой компоненты.
Упражнение 3
На основе фрагмента программы 6.3 разработайте программу для рисования
сферы внутри полупрозрачного цилиндра. Измените ее так, чтобы цилиндр и сфера
рисовались внутри третьего объекта (например, полупрозрачной сферы).
Упражнение 4
Выполните программу 6.4. Выясните, как влияют на вид объектов режим автоматического вычисления нормалей и автоматической нормализации нормалей:
glEnable(GL_AUTO_NORMAL);
glEnable(GL_NORMALIZE);
Попробуйте назначить объектам несколько различных материалов, приведенных в табл. 6.2.
82
ЛЕКЦИЯ 7. Растровые объекты: изображения и текстуры
1. Вывод изображений в буфер OpenGL
Изображение – это двумерный массив, в котором хранятся значения цветов отдельных пикселов. Существует большое количество форматов изображений, отличающихся способом представления цветовых значений (например, формат RGB,
RGBA, BGR, палитровый формат) и расположением пикселов внутри массива (например, изображение может храниться в массиве в направлении сверху-вниз или снизу вверх). На дисках изображения записываются в различных графических файловых
форматах (BMP, PCX, GIF, JPEG и др.), которые в основном отличаются способами
упаковки повторяющихся цветовых значений для сокращения размера файла.
В OpenGL есть функции для работы с изображениями, хранящимися в памяти
в виде массива пикселей, но нет функций для чтения/записи графических файлов.
Выполнять дисковые операции требуется с помощью других библиотек. В памяти
OpenGL позволяет хранить изображения разными способами: значения цвета пикселей может храниться в виде RGB, BGR, RGBA и др. Рассмотрим один из наиболее
часто используемых форматов – цвет пиксела хранится в формате RGB и занимает
три байта (по байту на каждую из трех компонент). Строки хранятся в памяти последовательно, в порядке снизу-вверх. В библиотеке GLAUX есть функция для загрузки
подобного изображения в память из BMP-файла:
AUX_RGBImageRec* auxDIBImageLoadA( const char* filename );
Эта функция возвращает указатель на структуру с динамически созданным
изображением:
struct AUX_RGBImageRec {
int sizeX, sizeY;
unsigned char* data;
};
// Ширина и высота изображения
// Пикселы изображения
Для вывода изображения из памяти в буфер кадра в OpenGL предназначена
функция glDrawPixels():
void glDrawPixels( int width, height, GLenum format,
GLenum type, const void* pixels );
Чтобы с помощью этой функции вывести изображение в буфер кадра, надо выполнить следующие действия:
1) загрузить изображение из файла в память;
2) указать способ хранения изображения в памяти и выравнивание;
3) установить точку начала вывода изображения (координату его левого нижнего угла)
4) вывести изображение в буфер.
Рассмотрим выполнение этих действий на примере изображения
SUNFLOWR.BMP, сохраненного в формате RGB, 24 бита на пиксел. Сначала в программе надо объявить глобальную переменную:
AUX_RGBImageRec* pImage = NULL;
Затем
в
функции
main()
перед
auxMainLoop( display ) надо внести строку:
входом
в
главный
цикл
pImage = auxDIBImageLoad( "sunflowr.bmp" );
После выхода из главного цикла, перед завершением программы, надо добавить удаление динамически созданного изображения:
83
delete pImage;
Для вывода этого изображения в буфер кадра в функцию display() надо внести следующие строки:
glRasterPos3d( -3, -2, 1 );
glPixelZoom( 1, 1 );
// Нижний левый угол изображения
// Коэффициенты масштабирования по
// ширине и высоте
glPixelStorei( GL_UNPACK_ALIGNMENT, 1 );
// Способ хранения
// изображения в памяти
glDrawPixels( pImage->sizeX, pImage->sizeY, // Ширина и высота в пикселах
GL_RGB,
// Формат цвета пикселов
GL_UNSIGNED_BYTE,
// Формат цветовых компонент
pImage->data );
// Данные изображения
При выводе в буфер изображения выводятся в масштабе, который задается
функцией glPixelZoom(). Модельные преобразования на изображение не влияют,
оно всегда выводится "параллельно плоскости экрана". Изображения удобно использовать в качестве статического фона. С помощью несложных преобразований можно
сформировать изображение с прозрачными областями (см. упражнение 2).
2. Назначение текстур
Для создания реалистичных трехмерных сцен возможностей вывода изображений недостаточно. Часто возникает потребность накладывать изображения на трехмерные объекты так, чтобы они оставались "наклеенными" на объекты при различных
модельных преобразованиях. Для этих целей предназначены текстуры.
До сих пор геометрические примитивы рисовались либо каркасными, либо залитыми методом плоской или плавной заливки. Если попробовать нарисовать без использования текстур, например, большую кирпичную стену, то потребуется рисовать
каждый кирпич в виде отдельного многоугольника. Плоская стена (которую удобно
изобразить в виде одного параллелепипеда) может потребовать рисования тысяч отдельных кирпичей, причем кирпичи будут выглядеть слишком гладкими и правильно
расположенными. Поэтому сцена будет выглядеть нереалистично.
Отображение текстур позволяет "наклеить" изображение кирпичной стены (например, отсканированную фотографию настоящей стены) на многоугольник, а затем
нарисовать всю стену как один многоугольник. Отображение текстур в OpenGL гарантирует, что все будет отображаться правильно при преобразованиях многоугольника. Например, в перспективной проекции, кирпичи будут уменьшаться в размерах
по мере удаления от наблюдателя.
Еще несколько примеров использования текстур – имитация растительности на
больших многоугольниках, изображающих землю в авиасимуляторах; текстуры в виде обоев для построения интерьеров помещений, текстуры для имитации предметов
из реальных материалов: мрамора, дерева, ткани и др. Хотя обычно текстуры применяются по отношению к многоугольникам, но их можно применять к любым примитивам, в том числе точкам и отрезкам.
Текстуры – это обычные изображения, например, в формате RGBA. Отдельные
точки текстуры иногда называются текселами (texels – элементы текстуры). Алгоритмы отображения текстур довольно сложны, т.к. требуется отображать прямоугольные текстуры на непрямоугольные области.
Процесс отображения текстуры поясняется на рис. 7.1. Слева показана текстура
целиком, на которой черным контуром отмечен четырехугольник, на который требу84
ется отобразить данную область текстуры. При рисовании четырехугольника он может быть искажен в результате различных преобразований – поворотов, переносов,
масштабирования и проецирования. Справа на рис. 7.1 показан текстурированный четырехугольник после нескольких преобразований (обратите внимание, что четырехугольник невыпуклый, поэтому его нельзя отобразить как один примитив OpenGL).
Рис. 7.1. Процесс наложения текстуры
OpenGL выполняет искажение текстуры в соответствии с преобразованиями
четырехугольника. На рис. 7.1 текстура вытянута по оси x, сжата по оси y, немного
повернута и сдвинута. В зависимости от размеров текстуры, искажения четырехугольника и размеров экранного изображения, некоторые из текселов могут отобразиться на участок из нескольких пикселов, или, наоборот, некоторые пикселы могут
оказаться под несколькими текселами. Текстура состоит из дискретных текселов (например, 256×256), поэтому перед отображением на пикселы надо выполнить фильтрацию. Например, если на один пиксел попадает несколько текселов, их значения надо усреднить. Если границы тексела проходят по нескольким пикселам, то каждому
такому пикселу надо присвоить взвешенное среднее от нескольких текселов. Вследствие подобных вычислений текстурирование требует больших вычислительных затрат, поэтому часто оно выполняется с помощью графических ускорителей.
3. Создание текстуры в оперативной памяти
Изображения текстур часто хранятся в файлах. Для ускорения вычислений в
OpenGL размеры изображений текстур должны быть кратны степеням 2, т.е. равны
2nx2m, где n и m целые числа. Для отображения подобной текстуры на объекте необходимо выполнить несколько последовательных действий:
1) Загрузить изображение текстуры в память из графического файла.
2) Создать идентификатор (имя) текстуры.
3) Сделать идентификатор текстуры активным.
4) Создать текстуру в памяти.
5) Установить свойства текстуры.
6) Установить свойства взаимодействия текстуры с объектом.
7) Разрешить отображение текстур вызовом glEnable(GL_TEXTURE_2D).
8) Связать координаты текстуры с объектом.
9) Если текстуры больше не нужны, запретить их отображение вызовом
glDisable(GL_TEXTURE_2D).
Рассмотрим простейший случай использования текстуры – наложение текстуры на плоский многоугольник. Для демонстрации работы с несколькими текстурами
85
будем полагать, что требуется выполнить отображение двух различных текстур на
три видимые грани куба (одну текстуру на переднюю по отношению к наблюдателю
грань куба, два экземпляра другой текстуры – на верхнюю и боковую грани).
В программе 7.1 для каждой текстуры заведены две глобальные переменные:
unsigned int falls_tex;
AUX_RGBImageRec* falls_image;
unsigned int sawdust_tex;
AUX_RGBImageRec* sawdust_image;
Переменные falls_tex и sawdust_tex служат идентификаторами текстур, а
falls_image и sawdust_image предназначены для хранения изображений текстур
из файлов с именами falls_filename и sawdust_filename. Загрузка изображений
в память производится в функции инициализации текстур textures_init():
falls_image = auxDIBImageLoad( falls_filename );
sawdust_image = auxDIBImageLoad( sawdust_filename );
После загрузки изображений надо создать идентификаторы текстур. Идентификаторы – это целые числа, позволяющие отличать текстуры друг от друга. Если
текстура только одна, то идентификатор для нее не обязателен. Для создания идентификаторов служит функция glGenTextures():
void glGenTextures( int n, unsigned int* textures );
Параметр n задает количество создаваемых идентификаторов текстур, а параметр textures является указателем на массив, куда будут помещены эти идентификаторы. Размер этого массива должен быть не менее n. Например, создать десять
идентификаторов текстур можно вызовом:
unsigned int ids[10];
glGenTextures( 10, ids );
В массиве удобно хранить идентификаторы однотипных текстур, например,
разные виды травы или листьев при имитации ландшафтов. В программе 7.1 идентификаторы текстур хранятся в отдельных переменных, поэтому при инициализации
текстур создаются два различных идентификатора:
glGenTextures( 1, &falls_tex );
glGenTextures( 1, &sawdust_tex );
После создания идентификаторов обе текстуры по очереди выбираются активными и для них задаются свойства. Выбор активной текстуры выполняется функцией
glBindTexture():
void glBindTexture( Glenum target, unsigned int texture );
Параметр target указывает тип текстуры – GL_TEXTURE_2D (двумерная) или
GL_TEXTURE_1D (одномерная). Далее будем рассматривать только двумерные текстуры. Параметр texture является идентификатором текстуры, которую надо сделать
активной. Например, чтобы сделать активной текстуру falls_tex, надо вызвать
функцию так:
glBindTexture( GL_TEXTURE_2D, falls_tex );
Хотя изображение текстуры falls_tex уже загружено из файла в переменную
falls_image типа AUX_RGBImageRec, но сама текстура еще создана. Кроме байт
изображения, у текстуры есть набор свойств, влияющих на наложение текстуры на
объект. Например, это уровень детализации, способ масштабирования и связывания
текстуры с объектом. Эти свойства задаются при создании текстуры.
86
С помощью уровня детализации можно задать несколько изображений для одной текстуры, что позволяет более точно накладывать текстуру на объекты, размеры
двумерных проекций которых меньшие размеров текстуры. Нулевой уровень детализации соответствует исходному изображению размером 2nx2m, первый уровень – 2n1
x2m-1, k-ый уровень – 2n-kx2m-k. Число возможных уровней равно min(n, m).
Для создания текстуры используется функция glTexImage2D() или
gluBuild2DMipmaps():
void glTexImage2D( GLenum target, int level, int components,
int width, int height, int border, GLenum format,
GLenum type, const void* pixels );
int gluBuild2DMipmaps( GLenum target, int components, int width,
int height, GLenum format, GLenum type, const void* data );
Первая функция создает текстуру одного определенного уровня детализации
(level) и работает только с изображениями, размеры которых кратны степеням 2.
Функция gluBuild2DMipmaps() более универсальная. Она генерирует текстуры
всех уровней детализации и не накладывает ограничений на размеры изображения.
Эта функция сама масштабирует изображение нужным образом, но результаты могут
оказаться не слишком качественными. Перед первым вызовом любой из функций
создания текстуры надо обязательно вызвать glPixelStorei(), которая задает способ хранения строк изображения в памяти (на сколько байт выравнивается строка).
Рассмотрим назначение параметров функции glTexImage2D():
target – размерность текстуры (для двумерной текстуры GL_TEXTURE_2D) ;
level – уровень детализации. Для исходного изображения level=0;
components – количество компонент цвета в изображении (для изображения в
формате RGB components=3);
width и height – ширина и высота изображения;
border – ширина границы, если границы нет, то border=0;
format – формат изображения (обычно GL_RGB);
type – тип данных пикселов изображения (обычно GL_UNSIGNED_BYTE);
pixels – указатель на массив пикселов изображения.
Например, создание текстуры можно выполнить так:
glPixelStorei( GL_UNPACK_ALIGNMENT, 1 );
glTexImage2D( GL_TEXTURE_2D, 0, 3,
falls_image->sizeX, falls_image->sizeY,
0, GL_RGB, GL_UNSIGNED_BYTE, falls_image->data );
Аналогичный
результат
gluBuild2DMipmaps():
можно
получить
с
помощью
функции
gluBuild2DMipmaps( GL_TEXTURE_2D, 3,
falls_image->sizeX, falls_image->sizeY,
GL_RGB, GL_UNSIGNED_BYTE, falls_image->data );
После создания текстуры можно задать ее свойства. Для этого есть функция:
void glTexParameter[if](GLenum target, GLenum pname, [int,float] param)
Первый параметр target для двумерной текстуры равен GL_TEXTURE_2D.
Второй параметр pname указывает изменяемое свойство текстуры. Новое значение
свойства передается в параметре param.
Например, если текстура создавалась с помощью glTexImage2D(), то правило
подбора текстуры для объектов с размерами, отличающимися от размеров текстуры,
можно задавать вызовами:
87
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
Этими вызовами указывается, что для уменьшения и увеличения текстуры используется алгоритм GL_NEAREST. По этому правилу в качестве цвета пиксела объекта, на который накладывается текстура, выбирается цвет ближайшего тексела. Вместо
GL_NEAREST можно указать GL_LINEAR, тогда цвет пиксела будет вычисляться как
среднее арифметическое четырех ближайших текселов. В OpenGL есть еще четыре
правила вычисления цвета пиксела путем комбинации текселов разных уровней детализации.
Кроме свойств текстуры, в OpenGL можно задавать свойства взаимодействия
текстуры с объектом. Для цветового режима RGB доступны два режима комбинации
цветов пиксела и тексела. В режиме по умолчанию (GL_MODULATE) учитывается и
цвет пиксела, и цвет тексела. Результирующий цвет получается путем умножения соответствующих компонент пиксела и тексела. Например, если цвет тексела (Rt, Gt, Bt),
а цвет пиксела объекта, на который накладывается текстура, – (Rp, Gp, Bp), то результирующим цветом будет (Rt*Rp, Gt*Gp, Bt*Bp). Если объект нарисован черным цветом
(0, 0, 0), то текстуру на нем не будет видно. Во втором режиме взаимодействия
(GL_DECAL) цвет объекта не учитывается, и результирующим цветом всегда будет
цвет тексела. Эти режимы устанавливаются следующим образом:
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
После того, как текстуры созданы и заданы из свойства, можно использовать
эти текстуры при рисовании примитивов. Наложение текстуры осуществляется путем
связывания вершин примитивов и координат текселов. В программе 7.1 это делается в
функции display(). В ней рисуются три квадрата с наложенными текстурами.
Функция glTexCoord2d() сопоставляет координаты текселов координатам вершин
примитива.
Рис. 7.2. Задание текстурных координат.
Рис. 7.3. Изменение ориентации текстуры.
При использовании функции glTexCoord2d() надо иметь в виду, что левый
нижний угол текстуры имеет координаты (0, 0), а верхний правый – координаты (1, 1)
(рис. 7.2). Изменив порядок сопоставления координат, можно перевернуть текстуру
(рис. 7.3).
#include
#include
#include
#include
<windows.h>
<GL/gl.h>
<GL/glu.h>
<GL/glaux.h>
const char* falls_filename = "falls.bmp";
unsigned int falls_tex;
88
AUX_RGBImageRec* falls_image;
const char* sawdust_filename = "sawdust.bmp";
unsigned int sawdust_tex;
AUX_RGBImageRec* sawdust_image;
void opengl_init()
{
glEnable( GL_AUTO_NORMAL );
glEnable( GL_NORMALIZE );
glEnable( GL_DEPTH_TEST );
glEnable( GL_ALPHA_TEST );
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glEnable( GL_COLOR_MATERIAL );
}
float pos[4] = { 0, 3, 5, 1 };
glEnable( GL_LIGHTING );
glEnable( GL_LIGHT0 );
glLightfv( GL_LIGHT0, GL_POSITION, pos );
void textures_init()
{
glPixelStorei( GL_UNPACK_ALIGNMENT, 1 );
falls_image = auxDIBImageLoad( falls_filename );
sawdust_image = auxDIBImageLoad( sawdust_filename );
glGenTextures( 1, &falls_tex );
glGenTextures( 1, &sawdust_tex );
glBindTexture( GL_TEXTURE_2D, falls_tex );
glTexImage2D( GL_TEXTURE_2D, 0, 3,
falls_image->sizeX, falls_image->sizeY,
0, GL_RGB, GL_UNSIGNED_BYTE, falls_image->data);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
glBindTexture( GL_TEXTURE_2D, sawdust_tex );
glTexImage2D( GL_TEXTURE_2D, 0, 3,
sawdust_image->sizeX, sawdust_image->sizeY,
0, GL_RGB, GL_UNSIGNED_BYTE, sawdust_image->data);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
}
void CALLBACK resize( int width, int height )
{
glViewport( 0, 0, width, height );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
glOrtho( -6,6, -6,6, 2,15 );
gluLookAt( 5,5,5, 0,0,-1.5, 0,1,0 );
glMatrixMode( GL_MODELVIEW );
}
void CALLBACK display()
{
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
89
glEnable( GL_TEXTURE_2D );
glColor3d( 1, 1, 1 );
glBindTexture( GL_TEXTURE_2D, falls_tex );
glBegin( GL_QUADS );
glTexCoord2d( 0, 0 ); glVertex3d( -3, -3,
glTexCoord2d( 0, 1 ); glVertex3d( -3, 3,
glTexCoord2d( 1, 1 ); glVertex3d( 3, 3,
glTexCoord2d( 1, 0 ); glVertex3d( 3, -3,
glEnd();
0
0
0
0
glBindTexture( GL_TEXTURE_2D, sawdust_tex );
glBegin( GL_QUADS );
glTexCoord2d( 0, 0 ); glVertex3d( -3, 3, 0
glTexCoord2d( 0, 1 ); glVertex3d( -3, 3, -6
glTexCoord2d( 1, 1 ); glVertex3d( 3, 3, -6
glTexCoord2d( 1, 0 ); glVertex3d( 3, 3, 0
glEnd();
glBegin( GL_QUADS );
glTexCoord2d( 0, 0 ); glVertex3d( 3, 3, 0
glTexCoord2d( 0, 1 ); glVertex3d( 3, 3, -6
glTexCoord2d( 1, 1 ); glVertex3d( 3, -3, -6
glTexCoord2d( 1, 0 ); glVertex3d( 3, -3, 0
glEnd();
}
);
);
);
);
);
);
);
);
);
);
);
);
glDisable( GL_TEXTURE_2D );
auxSwapBuffers();
void main()
{
auxInitPosition( 50, 10, 400, 400);
auxInitDisplayMode( AUX_RGB | AUX_DEPTH | AUX_DOUBLE );
auxInitWindow( "Лекция 7. Программа 7.1." );
opengl_init();
textures_init();
auxReshapeFunc( resize );
auxMainLoop( display );
}
Программа 7.1. Наложение текстур на плоские многоугольники.
4. Автоматическое повторение текстуры на плоском многоугольнике
Режим автоматического повторения текстуры включается/выключается с помощью свойств текстуры GL_TEXTURE_WRAP_S и GL_TEXTURE_WRAP_T. Буквами S и
T в OpenGL обозначаются горизонтальная и вертикальная координата текстуры. Для
автоповторения текстуры в заданном направлении соответствующее свойство должно
быть равно GL_REPEAT (значение по умолчанию). Для отключения автоповторения
этому свойству надо присвоить значение GL_CLAMP.
Ниже приведены изменения, которые надо внести в функцию display() программы 7.1, чтобы текстура на передней грани куба была повторена 3 раза по горизонтали и 2 раза по вертикали.
void CALLBACK display()
{
...
90
glBindTexture( GL_TEXTURE_2D, falls_tex );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );
glBegin( GL_QUADS );
glTexCoord2d( 0, 0 ); glVertex3d( -3, -3, 0 );
glTexCoord2d( 0, 2 ); glVertex3d( -3, 3, 0 );
glTexCoord2d( 3, 2 ); glVertex3d( 3, 3, 0 );
glTexCoord2d( 3, 0 ); glVertex3d( 3, -3, 0 );
glEnd();
...
}
Фрагмент программы 7.2. Автоматическое повторение текстуры.
Функция glTexCoord2d() сопоставляет координаты текстуры и вершин примитива. Координаты левого нижнего угла текстуры равны (0,0), а правого верхнего –
(1,1). Если в качестве параметров glTexCoord2d() указать значение, большее 1, то
текстура будет повторяться (рис. 7.4). В программе 7.2 координата (0,0) текстуры
привязывается к левой нижней вершине грани куба с координатой (-3,-3,0), а координата (3,2) текстуры – к правой верхней вершине (3,3,0). В результате по горизонтали
на грань куба будет наложено три экземпляра текстуры, а по вертикали – 2 экземпляра.
Рис. 7.4. Задание координат для автоматического повторения текстуры.
5. Наложение текстуры на произвольную поверхность
В OpenGL есть режим автоматической генерации текстурных координат. Этот
режим позволяет отказаться от использования glTexCoord2d() при решения нескольких типичных задач, таких, как демонстрация очертаний объекта с помощью
контуров и построение отражения окружающей среды на блестящих объектах.
Для применения этого режима сначала требуется разрешить автоматическую
генерацию координаты текстуры по одному или обоим направлениям с помощью вызова glEnable(GL_TEXTURE_GEN_S) и glEnable(GL_TEXTURE_GEN_T). После этого надо с помощью функции glTexGen() выбрать способ генерации текстурных координат.
void glTexGen{ifd}{v}(GLenum coord, GLenum pname, TYPE param);
Параметр coord выбирает координату GL_S или GL_T. Второй параметр pname
равен GL_TEXTURE_GEN_MODE, а param задает функцию вычисления текстурных координат: GL_OBJECT_LINEAR, GL_EYE_LINEAR или GL_SPHERE_MAP.
91
GL_OBJECT_LINEAR
и
расчета
текстурных
координат
GL_EYE_LINEAR в основном применяются для нанесения контурных изображений на
объект, чтобы показать его форму (например, с помощью одномерной текстуры в виде полоски).
Функция GL_SPHERE_MAP предназначена для имитации отражения окружающей среды блестящими объектами. Например, если посмотреть на полированный серебряный предмет внутри комнаты, то на его поверхности будет видно отражение
стен, пола и предметов, находящихся внутри комнаты. Вид отражения зависит от положения наблюдателя и от ориентации блестящего предмета.
Отображение GL_SPHERE_MAP строится в предположении, что предметы в
комнате расположены достаточно далеко от поверхности блестящего объекта и этот
объект очень мал по сравнению с размерами комнаты. В таком приближении, чтобы
рассчитать цвет точки поверхности объекта, выполняется построение луча от наблюдателя до поверхности и затем рассчитывается отражение этого луча от поверхности
объекта. Направление отраженного луча определяет цвет пиксела объекта. Для применения режима GL_SPHERE_MAP необходимо подготовить текстуру специальным
образом, так, чтобы она была похожа на фотографию реального блестящего объекта,
сделанную с помощью широкоугольного объектива.
В целом, применение любой из трех описанных функций для получения регулярной текстуры на объекте является довольно сложным делом. Наложение нерегулярных хаотических текстур выполняется значительно проще и в ряде случаев может
дать приемлемые результаты. В приведенной ниже функции display() из программы 7.3 демонстрируется наложение двух текстур на стандартные объекты GLAUX с
применением трех различных функций генерации текстурных координат.
Функции
void CALLBACK display()
{
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glEnable( GL_TEXTURE_2D );
glEnable( GL_TEXTURE_GEN_S );
glEnable( GL_TEXTURE_GEN_T );
glColor3d( 1, 1, 1 );
glPushMatrix();
glTranslated( -3, 3, 0 );
glTexGeni( GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR );
glTexGeni( GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR );
glBindTexture( GL_TEXTURE_2D, falls_tex );
auxSolidTeapot( 1.5 );
glTranslated( 6, 0, 0 );
glBindTexture( GL_TEXTURE_2D, sawdust_tex );
auxSolidTeapot( 1.5 );
glPopMatrix();
glPushMatrix();
glTranslated( -3, 0, 0 );
glTexGeni( GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR );
glTexGeni( GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR );
glBindTexture( GL_TEXTURE_2D, falls_tex );
auxSolidTeapot( 1.5 );
92
glTranslated( 6, 0, 0 );
glBindTexture( GL_TEXTURE_2D, sawdust_tex );
auxSolidTeapot( 1.5 );
glPopMatrix();
glPushMatrix();
glTranslated( -3, -3, 0 );
glTexGeni( GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP );
glTexGeni( GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP );
glBindTexture( GL_TEXTURE_2D, falls_tex );
auxSolidTeapot( 1.5 );
glTranslated( 6, 0, 0 );
glBindTexture( GL_TEXTURE_2D, sawdust_tex );
auxSolidTeapot( 1.5 );
glPopMatrix();
glDisable( GL_TEXTURE_GEN_S );
glDisable( GL_TEXTURE_GEN_T );
glDisable( GL_TEXTURE_2D );
}
auxSwapBuffers();
Фрагмент программы 7.3. Автоматическое наложение текстур.
6. Сводка результатов
Изображения – это двумерные массивы пикселов. В библиотеке GLAUX есть
функция для загрузки изображения из BMP-файла в буфер в оперативной памяти. Затем это изображение можно вывести в буфер кадра OpenGL. К изображениям нельзя
применять модельные преобразования, но можно задавать координаты левого нижнего угла и масштаб.
Для имитации реальных объектов часто применяются текстуры – изображения,
накладываемые "поверх" примитивов OpenGL. В лекции описан порядок работы с
несколькими текстурами. У каждой текстуры есть числовой идентификатор и набор
свойств. При наложении на объект можно задавать координаты текстуры в явном виде, сопоставляя координаты элементов текстуры (текселов) и пикселов объекта.
OpenGL может автоматически повторять текстуру на плоской поверхности в одном
или двух направлениях. Для наложения текстур на произвольную поверхность, состоящую из совокупности примитивов, в OpenGL есть режим автоматической генерации текстурных координат.
7. Упражнения
Упражнение 1
С помощью функций, описанных в п.1, напишите программу для вывода на экран изображения подсолнуха из файла SUNFLOWR.BMP. В начале координат нарисуйте
единичную сферу. Попробуйте сначала поместить изображение позади сферы, а затем
– перед сферой.
93
Упражнение 2
Измените программу из упражнения 1 так, чтобы изображение выводилось с
прозрачным фоном. В изображении SUNFLOWR.BMP фоновые области специально выделены пурпурным цветом (255, 0, 255). Чтобы сделать их прозрачными, в программу
потребуется добавить функцию, которая динамически создает массив для хранения
изображения в формате RGBA и копирует в него изображение из файла с соответствующими компонентами прозрачности (пикселы фонового цвета прозрачные, остальные – непрозрачные). Изменения в программе будут заключаться в следующем:
1) Заведите глобальную переменную для изображения с прозрачным фоном:
AUX_RGBImageRec flowerImg;
2) Перед вызовом главного цикла разрешите обработку альфа-компонент и смешение
цветов:
glEnable( GL_ALPHA_TEST );
glEnable( GL_BLEND )
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
3) Добавьте в программу функцию для инициализации изображения flowerImg (см.
текст далее) и вызовите ее перед входом в главный цикл. После выхода из главного цикла выполните удаление данных изображения.
4) Измените вызов glDrawPixels() в функции display():
glDrawPixels( flowerImg.sizeX, flowerImg.sizeY,
GL_RGBA, GL_UNSIGNED_BYTE, flowerImg.data);
Далее приведен текст функции подготовки изображения prepare_image().
void prepare_image()
{
// Изображение из файла в формате RGB
AUX_RGBImageRec* pRGB_Image;
pRGB_Image = auxDIBImageLoad( "sunflowr.bmp" );
// Вспомогательные переменные для хранения размеров изображения
int w = pRGB_Image->sizeX, h = pRGB_Image->sizeY;
// Инициализация глобальной переменной изображения в формате RGBA
flowerImg.sizeX = w;
flowerImg.sizeY = h;
// Выделение памяти для изображения RGBA (4 байта на пиксел)
int flowerImgLen = w * h * 4;
flowerImg.data = new unsigned char[flowerImgLen];
// Компоненты RGB фонового цвета, который будет прозрачным
unsigned char transpClr[3] = { 255, 0, 255 };
for ( int i = 0; i < h; i++ )
for ( int j = 0; j < w; j++ )
{
// Индексы текущего пиксела в изображениях
int curSrcIdx = (i*w + j)*3; // Для исходного RGB-изображения
int curDestIdx = (i*w + j)*4; // Для результирующего RGBA-изобр-я.
// Копирование значений RGB
memcpy( flowerImg.data+curDestIdx, pRGB_Image->data+curSrcIdx, 3 );
// Расчет прозрачности текущего пиксела
if ( pRGB_Image->data[curSrcIdx]
== transpClr[0] &&
pRGB_Image->data[curSrcIdx + 1] == transpClr[1] &&
pRGB_Image->data[curSrcIdx + 2] == transpClr[2] )
flowerImg.data[curDestIdx + 3] = 0;
// Прозрачный цвет
else
94
flowerImg.data[curDestIdx + 3] = 255;
// Непрозрачный цвет
}
// Удаление RGB-изображения, т.к. оно уже преобразовано во flowerImg
delete pRGB_Image;
}
Упражнение 3
В программе 7.1 нарисуйте грани куба фиолетовым цветом. Выясните, как изменится вид изображений на гранях. Затем установите свойство взаимодействия текстуры с объектом таким образом, чтобы цвет объекта не учитывался.
Упражнение 4
Внесите изменения в программу 7.1, описанные в п.4. Выясните, что произойдет, если указать в качестве координат текстуры дробные числа. Затем попробуйте
отключить режим автоповторения текстуры по одному или двум направлением, задавая соответствующим свойствам значение GL_CLAMP.
Упражнение 5
Напишите программу, показывающую фотографию, вращающуюся в плоскости XY и постепенно приближающуюся к наблюдателю, расположенному на оси Z.
Упражнение 6
На основе программы 7.3 разработайте программу, накладывающую на цилиндр текстуру в виде узора из небольших окружностей или текстуру в виде сетки.
95
ЛЕКЦИЯ 8. Примеры программ с использованием OpenGL
1. Имитация трехмерного ландшафта
Описываемая программа имитирует перемещение камеры над трехмерным
ландшафтом. Для построения поверхности используется карта высот, хранящаяся в
виде полутонового BMP-файла. Поверхность изображается как набор клеток со сторонами фиксированного размера, углы которых расположены на немного разной высоте. На клетки накладывается текстура, имитирующая травяную растительность.
Камера перемещается на постоянной высоте над поверхностью.
Форма поверхности без наложения текстур показана на рис. 8.1. Прямоугольные клетки поверхности отображаются в виде двух треугольников (рис. 8.2), т.к. углы
клетки чаще всего не лежат в одной плоскости.
Рис. 8.1. Поверхность, сформированная из треугольных граней.
Рис. 8.2. Разбиение квадратной клетки поверхности на два треугольника.
На каждый треугольник накладывается текстура травы (рис. 8.3).
y
0
Рис. 8.3. Изображение текстуры травы.
Рис. 8.4. Карта высот и связанная с этим
изображением система координат.
Рельеф поверхности описывается с помощью функции, задающей высоту каждого угла каждой клетки. Эта функция задается с помощью полутонового изображения – карты высоты (рис. 8.4), каждый пиксел которой соответствует углу одной из
96
x
клеток поверхности. Значения компонент изображения RGB лежат в диапазоне от 0
до 255, и именно эти значения принимаются за значения высоты в углах клеток.
Положение камеры задается в системе координат, связанной с картой высоты
(рис 4). Параметры положения описываются следующей структурой:
struct CameraData {
double x, y;
double x_frac, y_frac;
double height;
double mounth;
//
//
//
//
//
//
//
//
//
Координаты камеры в системе координат
изображения карты высот
Координаты камеры внутри клетки
поверхности размером FLATSCALERxFLATSCALER
Левый нижний угол этой клетки
соответствует точке (int(x), int(y))
на карте высот
Высота камеры над поверхностью
Высота поверхности в точке (x, y)
};
Первоначально камера располагается в центре карты высот. При изменении
положения камеры в фоновой функции координата x изменяется так, чтобы камера
перемещалась в положительном направлении оси OX. Перед рисованием ландшафта
производится вычисление координат камеры внутри клетки и высоты поверхности в
точке (x, y), т.к. эта точка не обязательно попадает в пиксел карты высот, а может
располагаться "между пикселами". Для сокращения времени рисования отображаются
не все клетки поверхности, а только те, которые попадают в видимый объем.
Задание 1.1
Текст программы находится в файле prg8_1.cpp, изображение текстуры травы хранится в файле grass.bmp, карта высот – в файле land.bmp. Cкомпилируйте
программу, запустите ее и затем разберитесь в исходном тексте с помощью комментариев и приведенного выше описания.
Измените программу так, чтобы с помощью клавиш курсора можно было регулировать скорость перемещения и направление движения камеры.
2. Объемный "тетрис"
Прототипом описываемой программы является игра "BlockOut" (LDW Inc.,
1989). В стакан в виде прямоугольного параллелепипеда равномерно падают блоки 8ми типов. В каждый момент времени существует только один падающий блок. Стакан
и блок состоят из единичных кубических ячеек. Блок можно перемещать параллельно
дну стакана клавишами курсора и поворачивать относительно трех координатных
осей буквенными клавишами Q, W, E. По нажатию пробела блок совершает быстрое
падение на дно стакана. Заполненные уровни (слои ячеек) из стакана исчезают, а вышележащие слои при этом опускаются вниз.
Выберем стакан с квадратным дном размером 5х5 и высотой 12 ячеек. Расположим стакан в мировой системе координат так, как показано на рис. 8.5. При рисовании на всех стенках стакана, кроме открытой верхней стенки, рисуются тонкие
вспомогательные линии для отображения ячеек (на рис. 8.5 эти линии показаны толь97
ко на дне стакана). В программе содержимое стакана описывается трехмерным массивом:
int data[GLASS_H][GLASS_SZ][GLASS_SZ];
Первый индекс массива соответствует направлению оси Y, второй – оси X, третий – оси Z в системе координат стакана (рис. 8.5). Нулевое значение элемента массива соответствует пустой ячейке, единичное – заполненной.
Y
0
Z
X
Рис. 8.5. Система координат, связанная со стаканом (совпадает с мировой
системой координат).
Падающий блок описывается трехмерным массивом ячеек 3х3х3. С этим массивом связана локальная система координат блока (рис. 8.6). Текущее положение
блока внутри стакана указывается путем задания положения системы координат блока в системе координат стакана.
Y
X
0
Z
Рис. 8.6. Локальная система координат блока (показан массив, описывающий
T-образный блок).
Таким образом, для описания падающего блока в программе предназначены
две переменные – описание блока и его текущие координаты:
int fallingBlock[3][3][3]; // Ячейки блока [y][x][z]
int fallingCoords[3];
// Координаты хранятся в виде (y,x,z)
Возможные типы блоков показаны на рис. 8.7.
98
Рис. 8.7. Различные типы блоков для объемного "тетриса".
В начале программы выполняется регистрация нескольких обработчиков событий. Обработчик перемещения мыши при нажатой левой кнопке применяется для изменения положения камеры. Сначала камера направлена на центр стакана параллельно отрицательному направлению оси OZ. Камеру можно перемещать по дуге окружности постоянного радиуса вокруг центра стакана (рис. 8.8).
Y
Gc
0
С0
X
Z
Рис. 8.8. Дуга окружности, по которой с помощью мыши можно перемещать
камеру. Точка Gc – центр стакана, C0 – начальное положение камеры.
Обработчики клавиш курсора позволяют перемещать блок на 1 ячейку параллельно осям системы координат стакана. Обработчик клавиши "пробел" выполняет
падение блока на дно стакана. Буквенная клавиша 'Q' производит поворот блока на
90 градусов вокруг оси, параллельной оси OX и проходящей через центр блока. Клавиши 'W' и 'E' выполняют аналогичные повороты вокруг осей, параллельных осям
OZ и OY.
Подробнее рассмотрим выполнение поворота блока. На рис. 8.9 приведены иллюстрации, поясняющие поворот блока вокруг оси, проходящей через центр массива
ячеек параллельно оси OX. Трехмерный массив, описывающий блок, разбивается в
направлении оси OX на 3 квадратные матрицы. Для поворота блока на 90 градусов
надо выполнить транспонирование каждой матрицы (поменять местами строки и
столбцы). На рис. 8.9 показано преобразование средней матрицы исходного массива
для T-образного блока при двух последовательных поворотах.
99
Y
Ось поворота
X
0
Z
Y
Z
0
Y
Z
0
Y
Z
0
Рис. 8.9. Два поворота блока вокруг оси, параллельной оси OX.
При отсутствии событий от клавиатуры и мыши программа должна выполнять
равномерное падение блока на дно стакана. Это делается с помощью фоновой функции. Скорость падения примерно равна 1 ячейка/сек. Если блок достигает дна стакана
или останавливается, зацепившись за какие-либо заполненные ячейки, то программа
выполняет удаление заполненных слоев ячеек и генерацию нового падающего блока.
void CALLBACK idle()
{
if ( fBlockPresent )
{
static time_t prev_t;
if ( time(NULL) - prev_t >= 1.0 )
{
stepBlock();
prev_t = time(NULL);
}
}
else
{
deleteFilledLayers();
createNewBlock();
}
draw();
}
Задание 2.1
Cкомпилируйте программу prg8_2.cpp, запустите ее и затем разберитесь в
исходном тексте с помощью комментариев и приведенного выше описания.
Измените функцию отображения содержимого стакана так, чтобы ячейки каждого слоя рисовались своим цветом. Цвета уровней не должны совпадать с цветами
падающих блоков. Цвета уровней могут периодически повторяться, например, можно
100
использовать всего 5-6 цветов. Для хранения значений цветов уровней заведите специальный массив.
Задание 2.2
Модифицируйте функцию генерации нового блока так, чтобы она создавала
блоки 8-ми различных типов (рис. 8.7).
Задание 2.3
Добавьте в программу обработчик нажатия клавиши 'W' для поворота падающего блока вокруг оси, параллельной OZ.
Задание 2.4
Внесите в программу обработчики для пары клавиш, позволяющих приближать
и удалять камеру от стакана.
101
Литература
1) McReynolds T., Blythe D. Advanced Graphics Programming Techniques Using
OpenGL. SIGGRAPH `98 Course, 1998. (Конспекты учебного курса по неочевидным вопросам использования OpenGL для программистов, уже имеющих опыт работы с OpenGL). http://www.sgi.com/Technology/OpenGL/advanced_sig98.html
2) Neider J., Davis T., Woo M. OpenGL Programming Guide. Addison-Wesley, 1993. (Руководство по OpenGL, написанное авторами из компании Silicon Graphics – основного разработчика этой библиотеки).
3) Rogerson D. OpenGL I-VIII: Technical Articles. MSDN, December 1994–May 1995.
(Технические статьи, описывающие различные аспекты использования OpenGL в
программах для Windows.)
4) Коваленко В. OpenGL - что дальше? (Статья, описывающая некоторые недостатки
OpenGL и устраняющей эти недостатки библиотеки OpenGL Optimizer).
http://madli.ut.ee
5) Подобедов Р. Что такое OpenGL? (Статья с описанием основных характеристик
OpenGL и некоторых конкурирующих библиотек). http://madli.ut.ee
6) Тарасов И. Библиотека OpenGL. 1999. (Электронный вариант книги-самоучителя)
http:\\www.citforum.ru\programming\opengl\index.shtm
7) Фоли Д., Вэн Дэм А. Основы интерактивной машинной графики. в 2-х кн.
М.: Мир, 1985 г. (Монография, посвященная алгоритмам двумерной и трехмерной
компьютерной графики и структуре графических программ).
102
Учебно-методическое издание
А.А. Богуславский, С.М. Соколов
Основы программирования на языке Си++
В 4-х частях.
(для студентов физико-математических факультетов
педагогических институтов)
Компьютерная верстка Богуславский А.А.
Технический редактор Пономарева В.В.
Сдано в набор 12.04.2002
Подписано в печать 16.04.2002
Формат 60х84х1/16
Бумага офсетная
Печ. л. 20,5
Учетно-изд.л. ____
Лицензия ИД №06076 от 19.10.2001
Тираж 100
140410 г.Коломна, Моск.обл., ул.Зеленая, 30. Коломенский государственный педагогический институт.
103
104
Download