OpenGL - Быстрый старт
2.1 Устанавливаем OpenGL
Начнем с самого главного, установим необходимое программное обеспечение. Я предполагаю, что Windows у Вас уже установлен и работает, в противном случае вы купили не ту книгу. Во-первых, установите MSVisualC++6.0 и jdk113 или выше, если вам интересно узнать о написание java-апплетов с использованием OpenGL. Впрочем java понадобится вам только в седьмой главе. Во-вторых, нам понадобится реализация библиотеки OpenGL. Она входит в поставку Windows95/NT - это библиотеки opengl32.dll & glu32.dll. Вы также можете взять библиотеки OpenGL от Silicon Graphics. Инструкция по установке там прилагается. Вам придется скопировать opengl.dll и glu.dll в windows\system и положить opengl.lib, glu.lib в подкаталог Lib, где установлено MSVisualC++. В-третьих, вам понадобятся четыре моих программы-шаблона, которые представляют собой начальный скелет, который мы потом будем наполнять функциональностью. Где взять OpenGL от Silicon Graphics, Magician, jdk и мои программы-шаблоны, смотри в приложение 'А'.
2.2 Давайте что-нибудь изобразим
Самым простым объектом, с помощью которого можно увидеть всю мощь OpenGL, является сфера. Давайте попытаемся ее изобразить. Для этого надо создать новый проект в VisualC++, выполните следующие действия:
- Запустите MSVisualC++6.0
- Щелкните меню File->New->Win32 Console Application.
- Выберете каталог и имя проекта, впишите - sphere, щелкните OK.
Я все свои проекты держу на диске D в каталоге Projects. Projects ветвится дальше на подкаталоги с базами данных, утилитами, графикой и Java-приложениями. В общем, старайтесь присваивать разумные имена и вести разумную структуру каталогов. Это очень серьезная проблема.
- Выберете An Empty Project, щелкните Finish.
- Cкопируйте в каталог вашего проекта мой шаблон glaux.c и переименуйте его в sphere.c
- Присоедините его к проекту. Project->Add To Project->Files
- Щелкните Build->Set Active Configuration и установите тип проекта sphere - Win32 Release
- Далее, щелкайте Project->Settings->Link->Object/library modules и добавьте туда opengl32.lib, glu32.lib и glaux.lib
- Вставьте в функцию display следующий код:
glColor3d(1,0,0); auxSolidSphere(1);
- Теперь откомпилируйте и запустите Вашу программу.
Меню Build->Execute Sphere.exe
Исходный файл смотрите здесь. Исполняемый файл здесь.
Теперь поясню назначение тех двух функций, что вы вставили в пункте 9. Функция glColor3d устанавливает текущий цвет, которым будут рисоваться фигуры. Тут нужно пояснить, как устанавливается цвет и общую философию в названии функций OpenGL. Цвет устанавливается четырьмя параметрами: красный, синий, зеленый и прозрачность. Эти параметры варьируют в диапазоне от нуля до единицы. Четвертый параметр нам пока не нужен, поэтому мы вызвали glColor с тремя параметрами. В этом случае, значение четвертого параметра, прозрачности, по умолчанию считается равным единице, т.е. абсолютно непрозрачным, ноль - будет абсолютно прозрачным. Так как в языке Си нет перегрузки функций, то применяется следующий синтаксис вызова функций - FuncionName[n=число параметров][тип параметров]. Доступны следующие типы:
- b - GLbyte байт
- s - GLshort короткое целое
- i - GLint целое
- f - GLfloat дробное
- d - GLdouble дробное с двойной точностью
- ub - GLubyte беззнаковый байт
- us - GLushort беззнаковое короткое целое
- ui - GLuint беззнаковое целое
- v - массив из n параметров указанного типа
В нашем случае - glColor3d - означает, что в функцию передается три параметра типа GLdouble. Также можно было вызвать glColor3i, т.е. три параметра типа GLint. Если тип параметров короткое целое, целое, байт или длинное, то компонента цвета приводится к диапазону [0,1]. Приведение к этому диапазону осуществляется по следующим образом. В случае беззнакового типа наибольшее возможное значение приводится к единице, ноль к нулю. В случае знакового максимальное значение приводится к единице или к минус единице, а минус единица будет приведена к единице. Мудрено, сразу и не поймешь. На практике вы будете пользоваться одним из трех случаев, рассмотренных в качестве примера ниже. Например, для беззнакового байта приведение будет осуществлено по следующей формуле: значение_переменной_хранимой_в_байте/255, т.к. 255 максимальное число, хранимое в одном байте. Функция glColor3dv означает, что в качестве параметров передается массив из трех элементов типа GLdouble. Например:
double array[] = {0.5, 0.75, 0.3, 0.7}; ... glColor3dv(array); glColor3ub(200,100,0); // приводится к 200/256, 100/256, 0,256 glColor3d(0.25,0.25,0); // темно-желтый glColot3ub(0,100,0); // темно-зеленый glColor3ub(0,0,255); // синий
Функция auxSolidSphere - рисует сферу в начале координат и единичным радиусом. Освещение, создание ламп и установка положения камеры мы рассмотрим чуть позже, а пока давайте получше освоимся и почувствуем насколько здесь все просто. Первую программу под моим руководством вы уже написали. Теперь выполните самостоятельные упражнения.
2.3 Упражнение: "Трехмерные фигуры"
Замените функцию auxSolidSphere на функцию, из указанных ниже с соответствующими параметрами. Значения параметров устанавливайте порядка единицы - 0.5-1.7. Если вы укажете слишком маленький размер, фигуру будет плохо видно; если слишком большой, то она получится урезанной. Это связано с тем, что ее край, как бы вылезет из монитора.
- 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) // рисует чайник
С помощью выше указанных функций вы можете рисовать сплошные фигуры. Если вам надо нарисовать проволочную, то вместо Solid пишите Wire.
Пример:
auxWireCube(1) // рисует проволочный куб
2.4 Переход к новым координатам
Продолжим рисовать трехмерные фигуры. В предыдущем параграфе вы научились рисовать примитивные трехмерные объекты. Но проблема в том, что они рисуются только в начале координат, т.е. в точке (0,0,0). Для того чтобы изобразить сферу в точке ( x0,y0,z0 ), надо переместить начало координат в эту точку, т.е. надо перейти к новым координатам. Эта процедура довольно распространенная при программировании графики и анимации. Часто, бывает очень удобно, сместить координаты в новую точку и повернуть их на требуемый угол, и ваши расчеты резко упростятся. Конкретный пример мы рассмотрим ниже, когда научимся программировать анимацию. А пока вы узнаете, как переходить к новым координатам. Для перехода к новым координатам в OpenGL есть две функции:
- glTranslated( Dx,Dy,Dz )
- glRotated(j,x0,y0,z0)
Первая функция сдвигает начало системы координат на ( Dx,Dy,Dz ). Вторая - поворачивает на угол j против часовой стрелки вокруг вектора (x0,y0,z0). Теперь, стоит сказать еще о двух очень важных функциях:
- glPushMatrix()
- glPopMatrix()
Они предназначены для сохранения и восстановления текущих координат. Я не стал здесь приводить пример на то, как неудобно переходить от одной системы координат к другой и помнить все ваши переходы. Гораздо удобнее с помощью glPushMatrix() сохранить текущие координаты, потом сдвигаться, вертеться, как вам угодно, а после, вызовом glPopMatrix вернуться к старым координатам. Итак, настало время поэкспериментировать. Создайте новый проект, повторив пункты из раздела 2.2. Только назовите его sphere2. Сначала мы рассмотрим сдвиг координат. Вставьте в функцию display следующий код:
glPushMatrix(); // сохраняем текущие координаты glTranslated(1.4,0,0); // сдвигаемся по оси Х на 1.4 glColor3d(0,1,0); auxSolidSphere(0.5); // рисуем сферу в (1.4,0,0) // в абсолютных координатах glTranslated(1,0,0); // еще раз сдвигаемся glColor3d(0,0,1); auxSolidSphere(0.3); glPopMatrix(); // возвращаемся к старой системе координат glColor3d(1,0,0); auxSolidSphere(0.75); // рисуем сферу в точке (0,0,0) // в абсолютных координатах
Теперь откомпилируйте и запустите вашу программу.
Меню Build->Execute Sphere.exe
Исходный файл смотрите здесь. Исполняемый файл здесь.
В общем-то, из комментариев многое понятно. Обращаю ваше внимание только на два последних вызова auxSolidSphere. Перед вызовом glPopMatrix сфера рисуется в точке (2,0,0), а после, в точке (0,0,0).
2.5 Упражнение: "Cписок трехмерных фигур"
Используя список функций из предыдущего упражнения, нарисуйте эти фигуры в два столбца. Слева проволочные. Справа сплошные.
Примечание: тут хочу заметить, что в версии glaux.lib от фирмы Microsoft имеется следующий баг: цилиндр и конус рисуются всегда либо проволочными, либо сплошными. Если вы первый цилиндр/конус в программе нарисовали проволочным, то далее все цилиндры/конусы будут проволочными. Соответственно, если первой была сплошная фигура, то далее все будут сплошные. Поэтому, не стоит паниковать. Это ошибка Microsoft. Могу также вас порадовать, что ниже я расскажу, как обойти эту проблему.
Исходный файл смотрите здесь. Исполняемый файл здесь.
2.6 Поворот координат
Теперь рассмотрим вращение координат. Создайте новый проект с именем Rotate. Переименуйте glaux.c в rotate.c В функцию display вставьте следующий код:
glColor3d(1,0,0); auxSolidCone(1, 2); // рисуем конус в центре координат glPushMatrix(); // сохраняем текущие координаты glTranslated(1,0,0); // сдвигаемся в точку (1,0,0) glRotated(75, 1,0,0); // поворачиваем систему координат на 75 градусов glColor3d(0,1,0); auxSolidCone(1, 2); // рисуем конус glPopMatrix(); // возвращаемся к старым координатам
Как видите, конус повернулся в абсолютных координатах. Так что, для того, чтобы нарисовать фигуру не в начале координат, надо:
- сохранить текущие координаты
- сдвинуть(glTranslated), повернуть(glRotated)
- нарисовать то, что хотели
- вернуться к старым координатам
Вызовы glPushMatrixglPopMatrix могут быть вложенными, т.е.:
glPushMatrix(); ... glPushMatrix(); glPopMatrix(); ... glPopMatrix();
Естественно число вызовов glPopMatrix должно соответствовать числу вызовов glPushMatrix, иначе у вас сцена улетит в неизвестном направление. Максимально допустимая глубина вложенности glPushMatrix/glPopMatrix определяется следующим образом:
int n; glGetIntegerv(GL_MAX_MODELVIEW_STACK_DEPTH, &n); printf("n=%d\n",n);
Спецификация на OpenGL гарантирует, что глубина стека не может быть меньше 32.
2.7 Упражнение: "Снеговик"
Используя функцию glRotate, нарисуйте снеговика. Три сферы, шапка - конус, нос - тоже конус, глаза - сфера, рот можно квадратным сделать - glBox.
Исходный файл смотрите здесь. Исполняемый файл здесь.
Примечание: Имеется еще один баг в glaux.lib от Microsoft. Кажется, последний из обнаруженных мной. Функция aux[Solid/Wire]Cylinder прежде, чем нарисовать цилиндр, сдвигает и поворачивает координаты. Так что, если вы уже сместили и повернули координаты, то цилиндр нарисуется совсем не там, где вы рассчитывали. Люди из Microsoft, конечно же, будут уверять, что это особенность, и предложат вам скачать очередной ServicePack.;-) А я ниже расскажу, как более правильно рисовать цилиндры и конусы. Если вам некогда ждать, то далее приведен исправленный код этих функций. Большинство из вас сейчас пропустят его и перейдут к следующему очень интересному разделу - "Анимация". И правильно сделаете. К исправлению ошибок вернетесь, когда немного освоитесь. Но я все же решил привести код по исправлению ошибок Microsoft именно здесь. Можете пересобрать glaux.lib, заменив соответствующий код в файле shapes.c. Где взять исходники, смотрите в приложение 'A'. По-моему, они есть в MSDN.
void auxWireCylinder (GLdouble radius, GLdouble height) { GLUquadricObj *quadObj; GLdouble *sizeArray, *tmp; GLuint displayList; sizeArray = (GLdouble *) malloc (sizeof (GLdouble) * 2); tmp = sizeArray; *tmp++ = radius; *tmp++ = height; displayList = findList (CYLINDERWIRE, sizeArray, 2); if (displayList == 0) { glNewList(makeModelPtr (CYLINDERWIRE, sizeArray, 2), GL_COMPILE_AND_EXECUTE); quadObj = gluNewQuadric (); gluQuadricDrawStyle (quadObj, GLU_LINE); gluCylinder (quadObj, radius, radius, height, 12, 2); glEndList(); } else { glCallList(displayList); free (sizeArray); } } void auxSolidCylinder (GLdouble radius, GLdouble height) { GLUquadricObj *quadObj; GLdouble *sizeArray, *tmp; GLuint displayList; sizeArray = (GLdouble *) malloc (sizeof (GLdouble) * 2); tmp = sizeArray; *tmp++ = radius; *tmp++ = height; displayList = findList (CYLINDERSOLID, sizeArray, 2); if (displayList == 0) { glNewList(makeModelPtr (CYLINDERSOLID, sizeArray, 2), GL_COMPILE_AND_EXECUTE); quadObj = gluNewQuadric (); gluQuadricDrawStyle (quadObj, GLU_FILL); gluQuadricNormals (quadObj, GLU_SMOOTH); gluCylinder (quadObj, radius, radius, height, 12, 2); glEndList(); } else { glCallList(displayList); free (sizeArray); } } void auxSolidCone (GLdouble base, GLdouble height) { GLUquadricObj *quadObj; GLdouble *sizeArray, *tmp; GLuint displayList; sizeArray = (GLdouble *) malloc (sizeof (GLdouble) * 2); tmp = sizeArray; *tmp++ = base; *tmp++ = height; displayList = findList (CONESOLID, sizeArray, 2); if (displayList == 0) { glNewList(makeModelPtr (CONESOLID, sizeArray, 2), GL_COMPILE_AND_EXECUTE); quadObj = gluNewQuadric (); gluQuadricDrawStyle (quadObj, GLU_FILL); gluQuadricNormals (quadObj, GLU_SMOOTH); gluCylinder (quadObj, base, (GLdouble)0.0, height, 15, 10); glEndList(); } else { glCallList(displayList); free (sizeArray); } }
2.8 Анимация
Давайте оживим нашего снеговика и добавим интерактивность. Для этого надо отрисовывать кадры и реагировать на внешние события от клавиатуры или мыши. Для отрисовки кадров их надо как-то различать. Для этого мы в функции display вводим переменную time типа int с модификатором static. Создайте новый проект и в функцию display введите следующее: "static int time=0;". Модификатор static означает, что значение переменной будет сохраняться при выходе из функции. Начальное значение мы устанавливаем в ноль. Если функция display не зависит от времени, то счетчик кадров можно и не вести. Теперь добавьте следующие строчки:
glPushMatrix(); glTranslated( ((double)time)/100.0 ,0,0); ... // здесь код из предыдущего упражнения "Cнеговик" glPopMatrix();
Запустив приложение, вы увидите, что снеговик пополз вдоль оси Х. Вот так вы можете делать анимацию. Т.е. теперь координаты объектов вычисляются в зависимости от времени. Я привел простой пример. Вообще говоря, для программирования сложной графики вам понадобится вычислять координаты каждого отдельного объекта в зависимости от времени.
Далее мы рассмотрим пример с более сложной анимацией. Здесь вращается тор и его ось вращения. Я приведу исходный текст с подробными комментариями. Пример программы "Гироскоп":
Исходный файл смотрите здесь. Исполняемый файл здесь.
glPushMatrix(); // сохраняем текущие координаты, т.к. при выходе // из функции нам нужно вернуться к абсолютным // координатам // попробуйте закомментировать вызов glPushMatrix // здесь и glPopMatrix в конце и вы увидите, что // из этого получится glRotated(time/2, 0.0, 1.0, 0.0); // поворачиваем координаты glLineWidth(5); // устанавливаем толщину линии - пять glColor3f(1,0,0); // устанавливаем текущий цвет - красный glBegin(GL_LINES); // рисуем красную ось glVertex3d(-0.3,0,0); // т.к. мы повернули координаты, glVertex3d(1.5,0,0); // ось будет повернута в абсолютных glEnd(); // координатах, и т.к. функция display // вызывается в цикле, и переменная // time увеличивается на единицу // при каждом вызове display, мы // получаем анимацию - вращающуюся ось glPushMatrix(); // теперь относительно повернутых координат // мы переходим к новым координатам glRotated(2*time, 1,0,0); // поворачиваем их на угол // 2*time вокруг красной оси glTranslated(-0.3,0,0); // и сдвигаем на край оси glColor3f(0,0,1); // устанавливаем синий цвет glPushMatrix(); // теперь еще раз переходим к // новым координатам, glRotated(90,0,1,0); // чтобы развернуть тор на // 90 градусов // у вас крыша еще не поехала? // немного? // то-то же, тут не так все просто, // но если понять что и как, то // очень удобно // программировать графику glLineWidth(1); auxWireTorus(0.2, 0.7); glPopMatrix(); // нарисовав тор, мы возвращаемся // к предыдущим // координатам, если забыли, // начало этих координат // лежит на конце красной оси glLineWidth(7); glColor3f(0,1,0); glBegin(GL_LINES); // теперь рисуем три зеленых линии glVertex3d(0,0,0); // что тут и как рисуется, я поясню в glVertex3d(0,1,0); // следующей glVertex3d(0,0,0); // главе, сейчас это не так важно glVertex3d(0,-0.5,1); glVertex3d(0,0,0); glVertex3d(0,-0.5,-1); glEnd(); glPopMatrix(); // теперь возвращаемся к повернутым // координатам на угол time/2 glPopMatrix(); // ну и переходим к абсолютным координатам time++; // увеличиваем счетчик кадров или время, // называйте как хотите, // на единицу
Как вы, наверное, догадались, надо создать очередной проект, скопировать туда мой шаблон, добавить его в проект, указать библиотеки opengl32.lib glu32.lib glaux.lib в Project->Setting->Link->Settings->Link->Object/library modules:, вставить этот код в функцию display. Еще вам нужно в начале функции display вставить строку static int time=0; и закомментировать строчку glEnable(GL_LIGHTING) в функции main.
Запустив это приложение, вы увидите, как оно работает. Теперь закомментируйте соответствующие друг другу вызовы glPushMatrix и glPopMatrix и посмотрите на результат. Я бы рекомендовал такой способ для изучения и понимания работы программы: вы комментируете куски кода и смотрите, что получается.
Для того, чтобы реагировать на клавиатуру или мышь, вы должны определить функцию, которая будет вызываться при поступление событий от клавиатуры или мыши. Для кнопки клавиатуры вы определяете функцию со следующим прототипом void CALLBACK FunctionName(void) и устанавливаете ее как обработчик определенной кнопки - auxKeyFunc(key_code, FunctionName); key_code смотри в glaux.h. Пример: auxKeyFunc(AUX_LEFT, FunctionName) Здесь вы устанавливаете FunctionName как функцию, которая будет вызываться при нажатии на клавиатуре клавиши "стрелка влево".
Для мыши вы устанавливаете свою функцию обработки событий мыши вызовом функции auxMouseFunc(int button,int action,AUXMOUSEPROC). Переменная button может принимать значения - AUX_LEFTBUTTON, AUX_MIDDLEBUTTON, AUX_RIGHTBUTTON. Переменная action принимает следующие значения - AUX_MOUSEDOWN, AUX_MOUSEUP, AUX_MOUSELOC. Функция AUXMOUSEPROC должна иметь прототип - void CALLBACK FunctionName(AUX_EVENTREC *event), где AUX_EVENTREC определено как
typedef struct _AUX_EVENTREC { GLint event; GLint data[4]; }AUX_EVENTREC;
Для более детальной информации смотрите справочные руководства и исходные тексты библиотеки OpenGL Auxiliary library. Эта книга об OpenGL, а не о программировании интерфейсов пользователя. Поэтому, за подобной информацией вам придется лезть в соответствующие справочные руководства по Win API, MFC, Java и т.п.
В FunctionName вы можете изменить какую-нибудь глобальную переменную, и, соответственно, функция display будет рисовать кадры в зависимости от этой переменной.
2.9 Упражнение:" Игра Арканоид"
Напишите игру "Arconoid" - летающий шарик, снизу подставка, пользователь стрелками или мышкой управляет подставкой и отбивает шарик.
Исходный файл смотрите здесь. Исполняемый файл здесь.
2.10 Резюме
На этом эта глава книги закончена. Вы научились рисовать трехмерные объекты и делать интерактивную анимацию, используя эти объекты. Этого, в общем-то, достаточно для написания примитивных программ. В следующей главе мы научимся рисовать примитивные объекты: точки, линии, ломаные и многоугольники.