Анимация
Давайте оживим нашего снеговика и добавим интерактивность. Для этого надо отрисовывать кадры и реагировать на внешние события от клавиатуры или мыши. Для отрисовки кадров их надо как-то различать. Для этого мы в функции 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 будет рисовать кадры в зависимости от этой переменной.
Давайте что-нибудь изобразим
Самым простым объектом, с помощью которого можно увидеть всю мощь 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 - рисует сферу в начале координат и единичным радиусом. Осещение, создание ламп и установка положения камеры мы рассмотрим чуть позже, а пока давайте по-лучше освоимся и почувствуем насколько здесь все просто. Первую программу под моим руководством вы уже написали. Теперь выполните самостоятельные упражнения.
Переход к новым координатам
Продолжим рисовать трехмерные фигуры. В предыдущем параграфе вы научились рисовать примитивные трехмерные объекты. Но проблема в том, что они рисуются только в начале координат, т.е. в точке (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 вернуться к старым координатам. Итак, настало время поэкспериментировать. Создайте новый проект, повторив пункты из раздела . Только назовите его 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).
Поворот координат
Теперь рассмотрим вращение координат. Создайте новый проект с именем 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.
Cписок трехмерных фигур"
Используя список функций из предыдущего упражнения, нарисуйте эти фигуры в два столбца. Слева проволочные. Справа сплошные.
Примечание: тут хочу заметить, что в версии glaux.lib от фирмы Microsoft имеется следующий баг: цилиндр и конус рисуются всегда либо проволочными, либо сплошными. Если вы первый цилиндр/конус в программе нарисовали проволочным, то далее все цилиндры/конусы будут проволочными. Соответсвенно, если первой была сплошная фигура, то далее все будут сплошные. Поэтому, не стоит паниковать. Это ошибка Microsoft. Могу также вас порадовать, что ниже я расскажу, как обойти эту проблему.
Исходный файл смотрите . Исполняемый файл .
Игра Арканоид"
Напишите игру "Arconoid" - летающий шарик, снизу подставка, пользователь стрелками или мышкой управляет подставкой и отбивает шарик.
Исходный файл смотрите . Исполняемый файл .
Снеговик"
Используя функцию 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); } }
Трехмерные фигуры"
Замените функцию 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) // рисует проволочный куб
Устанавливаем 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 и мои программы-шаблоны, смотри в приложение 'А'.
Четырехугольники и многоугольники
Четырехугольники рисуются вызовом функции glBegin с параметром GL_QUADS или GL_QUAD_STRIP. Для первого случая каждые четыре вершины определяют свой четырехугольник. Во втором случае рисуются связанные четырехугольники. Первая, вторая, третья и четвертая вершина определяют первый четырехугольник. Третья, четвертая, пятая и шестая вершина - второй четырехугольник и т.д. (2n-1), 2n, (2n+1) и (2n+2) вершины задают n-ый четырехугольник. Многоугольники задаются вызывом glBegin с параметром GL_POLYGON. Все вершины определяют один многоугольник. Для многоугольников можно задавать стили при помощи выше описанной функции glPolygonMode, толщину линии, толщину точек и цвет.
Линии
Для линий вы также можете изменять ширину, цвет, размер сглаживание. Если вы зададите разные цвета для начала и конца линии, то ее цвет будет переливающемся. OpenGL по умолчанию делает интерполяцию. Так же вы можете рисовать прерывестые линии, делается это путем наложения маски при помощи следующей функции:
void glLineStipple(GLint factor, GLushort pattern );
Второй параметр задает саму маску. Например, если его значение равно 255(0x00FF), то чтобы вычислить задаваемую маску воспользуемся калькулятором. В двоичном виде это число выглядит так: 0000000011111111, т.е. всего 16 бит. Старшие восемь установлены в ноль, значит тут линии не будет. Младшие установлены в единицу, тут будет рисоваться линия. Первый параметр определяет, сколько раз повторяется каждый бит. Скажем, если его установить равным 2, то накладываемая маска будет выглядить так:
00000000000000001111111111111111 Далее приведен исходный текст с комментариями для наглядной демонстрации что к чему.
glLineWidth(1); // ширину линии // устанавливаем 1 glBegin(GL_LINES); glColor3d(1,0,0); // красный цвет glVertex3d(-4.5,3,0); // первая линия glVertex3d(-3,3,0); glColor3d(0,1,0); // зеленый glVertex3d(-3,3.3,0); // вторая линия glVertex3d(-4,3.4,0); glEnd(); glLineWidth(3); // ширина 3 glBegin(GL_LINE_STRIP); // см. ниже glColor3d(1,0,0); glVertex3d(-2.7,3,0); glVertex3d(-1,3,0); glColor3d(0,1,0); glVertex3d(-1.5,3.3,0); glColor3d(0,0,1); glVertex3d(-1,3.5,0); glEnd(); glLineWidth(5); glEnable(GL_LINE_SMOOTH); glEnable(GL_LINE_STIPPLE); // разрешаем рисовать // прерывистую линию glLineStipple(2,58360); // устанавливаем маску // пояснения см. ниже glBegin(GL_LINE_LOOP); glColor3d(1,0,0); glVertex3d(1,3,0); glVertex3d(4,3,0); glColor3d(0,1,0); glVertex3d(3,2.7,0); glColor3d(0,0,1); glVertex3d(2.5,3.7,0); glEnd(); glDisable(GL_LINE_SMOOTH); glDisable(GL_LINE_STIPPLE);
Общие положения
Точки, линии, треугольники,четырехугольники, многоугольники - простые объекты, из которых состоят любые сложные фигуры. В предыдущей главе мы рисовали сферу, конус и тор. OpenGL непосредственно не поддерживает функций для создания таких сложных объектов, т.е. таких функций нет в opengl32.dll. Эти функции есть в библиотеки утилит glu32.dll, и устроены они следующим образом. Для того чтобы нарисовать сферу функция auxSolidSphere использует функции из библиотеки glu32.dll, а те в свою очередь, использут базовую библиотеку opengl32.dll и из линий или многоугольников строят сферу. Примивы создаются следующим образом:
glBegin(GLenum mode); // указываем, что будем рисовать glVertex[2 3 4][s i f d](...); // первая вершина ... // тут остальные вершины glVertex[2 3 4][s i f d](...); // последняя вершина glEnd(); // закончили рисовать примитив
Сначала вы говорите, что будете рисовать - glBegin с соответсвующим параметром. Возможные значения mode перечислены ниже в таблице. Далее вы указываете вершины, определяющие объкты указанного типа. Обычно вы будете задавать вершину одним из четырех способов.
glVertex2d(x,y); // две переменных типа double glVertex3d(x,y,z); // три переменных типа double glVertex2dv(array); // массив из двух переменных типа double glVertex3d(array); // массив из трех переменных типа double И наконец, вы вызваете glEnd, чтобы указать, что вы закончили рисовать объекты типа, указанного в glBegin. Далее мы подробно разберем создание всех примитивов.
Значение mode | Описание |
GL_POINTS | Каждый вызов glVertex задает отдельную точку. |
GL_LINES | Каждая пара вершин задает отрезок. |
GL_LINE_STRIP | Рисуется ломанная. |
GL_LINE_LOOP | Рисуется ломанная, причем ее последняя точка соединяется с первой. |
GL_TRIANGLES | Каждые три вызова glVertex задают треугольник. |
GL_TRIANGLE_STRIP | Рисуются треугольники с общей стороной. |
GL_TRIANGLE_FAN | Тоже самое, но по другому правилу соединяются вершины, вряд ли понадобится. |
GL_QUADS | Каждые четыре вызова glVertex задают четырехугольник. |
GL_QUAD_STRIP | Четырехугольники с общей стороной. |
GL_POLYGON | Многоугольник. |
Ну вот, вы еще на
Ну вот, вы еще на один шаг продвинулись в изучение библиотеки OpenGL. Теперь вы имеете представление о том, как рисовать элементарные фигуры. Из примитивов вы можете составить фигуры любой сложности.
Точки
Вы можете нарисовать столько точек, сколько вам нужно. Вызывая glVertex3d, вы устанавливает новую точку. При создании точек вы можете изменять следующие параметры. Вы можете вызывать glColor3d внутри glBegin/glEnd. Размер точки можно устанавливать с помощью функции:
void glPointSize(GLfloat size) Режим сглаживания можно устанавливать вызовом функции
glEnable(GL_POINT_SMOOTH)
Отключается соответственно вызовом glDisable() c этим параметром. Последние функции - glPointSize и glEnable/glDisable надо вызывать вне glBegin/glEnd, иначе они будут проигнорированы. Функции glEnable/glDisable включают/выключают множество опций, но вы должны учитывать, что некоторые опции влекут за собой большие вычисления и, следовательно, изрядно затормаживают ваше приложение, поэтому без надобности не стоит их включать. Очевидно, что совершенно не к чем включать освещение, наложение текстру и сглаживания цветов при рисовании точек. Пока вы с этими возможностями OpenGL не познакомились, поэтому запомните это на будующее.
// рисуем точки glPointSize(2); glBegin(GL_POINTS); glColor3d(1,0,0); glVertex3d(-4.5,4,0); // первая точка glColor3d(0,1,0); glVertex3d(-4,4,0); // вторая точка glColor3d(0,0,1); // третья glVertex3d(-3.5,4,0); glEnd(); glPointSize(5); glBegin(GL_POINTS); glColor3d(1,0,0); glVertex3d(-2,4,0); // первая точка glColor3d(0,1,0); glVertex3d(-1,4,0); // вторая точка glColor3d(0,0,1); // третье glVertex3d(0,4,0); glEnd(); glPointSize(10); glEnable(GL_POINT_SMOOTH); glBegin(GL_POINTS); glColor3d(1,0,0); glVertex3d(2,4,0); // первая точка glColor3d(0,1,0); glVertex3d(3,4,0); // вторая точка glColor3d(0,0,1); // третья glVertex3d(4,4,0); glEnd(); glDisable(GL_POINT_SMOOTH);
Треугольники
Для треугольника можно задавать теже параметры, что и для линии плюс есть еще одна функция glPolygonMode. Она устанавливает опции для отрисовки многоугольника. Первый параметр может принимать значения - GL_FRONT, GL_BACK и GL_FRONT_AND_BACK. Второй параметр указывает, как будет рисоваться многоугольник. Он примает значения - GL_POINT(рисуются только точки), GL_LINE(рисуем линии) и GL_FILL(рисуем заполненный многоугольник). Первый параметр указывает: к лицевой, тыльной или же к обеим сторонам применяется опция, заданная вторым параметром. Треугольники можно рисовать, передав GL_TRIANGLE_STRIP или GL_TRIANGLE_FAN в glBegin. В первом случае первая, вторая и третья вершины задают первый треугольник. Вторая, третья и четвертая вершина - второй треугольник. Третья, четвертая и пятая вершина - третий треугольник и т.д. Вершины n, n+1 и n+2 определят n-ый треугольник. Во втором случае первая, вторая и третья вершина задают первый треугольник. Первая, третья и четвертая вершины задают второй треугольник и т.д. Вершины 1, n+1, n+2 определяют n-ый треугольник. Далее следует пример с комментариями.
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // см. выше glBegin(GL_TRIANGLES); glColor3d(1,0,0); // рисуем треугольник glVertex3d(-4,2,0); glVertex3d(-3,2.9,0); glVertex3d(-2,2,0); glEnd(); glLineWidth(2); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); //рисуем // проволочные треугольники glBegin(GL_TRIANGLE_STRIP); // обратите внимание на порядок // вершин glColor3d(0,1,0); glVertex3d(1,2,0); glVertex3d(0,2.9,0); glVertex3d(-1,2,0); glVertex3d(0,1.1,0); glEnd(); glEnable(GL_LINE_STIPPLE); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glBegin(GL_TRIANGLE_FAN); glColor3d(0,0,1); glVertex3d(4,2,0); glVertex3d(2.6,2.8,0); glVertex3d(2,2,0); glVertex3d(3,1.1,0); glEnd(); glDisable(GL_LINE_STIPPLE);
Уражнение:"Многогранники"
Реализуйте проволочные многогранники с помощью проволочных треугольников, многоугольников и линий.
Уражнение:"Примитивы"
Изобразите точки, линии, треугольники, многоугольники в одном окне, как показано ниже.
Исходный файл смотрите . Исполняемый файл .
Интерполяция цветов
Когда вы создаете многоугольник, вы можете задать цвет для каждой его вершины. Если разрешено сглаживание цветов, то многоугольник будет переливаться. Поясню на примере. Режим сглаживания по умолчанию разрешен. Он переключается функцией glShadeModel с аргументами GL_FLAT и GL_SMOOTH. GL_FLAT запрещает сглаживание. На мой взгляд, сглаживание редко нужно. Вещь красивая, но бесполезная. Я в своих неучебных программах этот режим никогда не использовал. Поэтому советую его отключать, особенно, при программировании анимированного приложения. Создайте очередной проект. В функцию main добавьте
glShadeModel(GL_SMOOTH); Функцию display отредактируйте следующим образом:
void CALLBACK display(void) { glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); glBegin(GL_TRIANGLES); glColor3d(1,0,0); glVertex2d(0,0); glColor3d(0,1,0); glVertex2d(0,3); glColor3d(0,0,1); glVertex2d(3,0); glEnd(); auxSwapBuffers(); }
Исходный файл смотрите . Исполняемый файл .
Логические операции
Логические операции позволяют вам складывать цвет фрагмента находящегося в буфере с цветом, который туда поступает. Этот режим разрешается и запрещается вызовом функций glEnable\glDisable с параметром GL_COLOR_LOGIC_OP. Правило, по которому будут складываться цвета задается функцией glLogicOp. У нее один параметр - одна из следующих констант определенных в файле gl.h.
/* LogicOp */ #define GL_CLEAR 0x1500 #define GL_AND 0x1501 #define GL_AND_REVERSE 0x1502 #define GL_COPY 0x1503 #define GL_AND_INVERTED 0x1504 #define GL_NOOP 0x1505 #define GL_XOR 0x1506 #define GL_OR 0x1507 #define GL_NOR 0x1508 #define GL_EQUIV 0x1509 #define GL_INVERT 0x150A #define GL_OR_REVERSE 0x150B #define GL_COPY_INVERTED 0x150C #define GL_OR_INVERTED 0x150D #define GL_NAND 0x150E #define GL_SET 0x150F
Мне не удалось найти разумного применения этой функции поэтому я приведу здесь лишь текст моей программы с комментариями, которая позволяет перебрать и посмотреть действие всех логических операций. Как и в предыдущем пункте у меня определена глобальная переменная и две функции реагирующие на нажатие стрелок на клавеатуре.
/* * (c) Copyright 1995-2000, Igor Tarasov * FidoNet: 2:5020/370.2 620.20 1103.5 * email: igor@itsoft.miem.edu.ru itarasov@rtuis.miem.edu.ru * Phone: (095)916-89-51 916-89-63 */ #include #include <GL/gl.h> #include <GL/glu.h> #include <GL/glaux.h>
void CALLBACK resize(int width,int height) { glViewport(0,0,width,height); glMatrixMode( GL_PROJECTION ); glLoadIdentity(); glOrtho(-5,5, -5,5, 2,12); 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 ); glColor4d(1,0,0, 1); auxSolidSphere( 1 );
glColor4d(0,1,0, 0.7); auxSolidCylinder(2,3);
auxSwapBuffers(); } void main() { float pos[4] = {3,3,3,1}; float dir[3] = {-1,-1,-1}; GLfloat mat_specular[] = { 1,1,1,1}; auxInitPosition( 50, 10, 400, 400); auxInitDisplayMode( AUX_RGB | AUX_DEPTH | AUX_DOUBLE ); auxInitWindow( "Glaux Template" ); auxIdleFunc(display); auxReshapeFunc(resize);
glEnable(GL_DEPTH_TEST); glEnable(GL_COLOR_MATERIAL); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular); glMaterialf(GL_FRONT, GL_SHININESS, 128); auxMainLoop(display); }
Плоскости отсечения
Если вам требуется нарисовать сферу или любой другой объект урезанным, то плоскости отсечения это то, что вам надо. Плоскостей отсечения может быть шесть штук. По умолчанию они все запрещены. Плоскость отсечения включается командой glEnable(GL_CLIP_PLANE0). Ноль на конце GL_CLIP_PLANE означает нулевую плоскость; можно указать один, два, три и т.д. Сама плоскость устанавливается функцией glClipPlane. Первый аргумент этой функции - это плоскость, второй - это массив из четырех элементов, в котором хранятся коэффициенты уравнения плоскости. Для тех, кто не помнит уравнения плоскости в трехмерном пространстве, напоминаю: A*x+B*y+C*z+D = 0. Вот эти самые A,B,C,D и являются теми четырьмя коэффициентами. Создайте новый проект с именем ClipPlane и отредактируйте функцию display, как показано ниже.
void CALLBACK display(void) { GLdouble equation[4] = {-1,-0.25,0,2}; glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); glEnable(GL_CLIP_PLANE0); glClipPlane(GL_CLIP_PLANE0, equation); glColor3d(1,0,0); auxSolidSphere( 3 ); glDisable(GL_CLIP_PLANE0); auxSwapBuffers(); }
Исходный файл смотрите . Исполняемый файл .
Построение поверхностей
Существует набор функций для посторения сфер, цилиндров и дисков. Эти функции представляют очень мощный контроль за построением трехмерных объектов. Непосредственно рисовать вы будете, используя следующие функции: gluSphere, gluCylinder, gluDisk и gluPartialDisk. В начале книги вы научились строить трехмерные объекты с помощью функций из библиотеки Auxilary Library. Функции aux[Solid/Wire]Sphere, aux[Solid/Wire]Cylinder и aux[Solid/Wire]Cone просто вызывают gluSphere и gluCylinder. Как я уже ранее говорил, в aux[Solid/Wire]Cylinder и aux[Solid/Wire]Cone фирмы Microsoft имеются баги. Здесь будет подробно рассмотрено построение сфер и цилиндров, так что потребность в aux[Solid/Wire]Cylinder и aux[Solid/Wire]Cone отпадет.
Первым параметром для gluSphere, gluCylinder, gluDisk является указатель на объект типа GLUquadricObj. Далее следуют параметры непосредственно создаваемой фигуры. Для сферы - это радиус; для цилиндра - радиус нижнего основания, радиус верхнего основания и высота; для диска - внутренний радиус, внешний радиус и для частичного диска - внутренний радиус, внешний радиус, угол, с которого начинать рисовать, длина дуги в градусах, которую рисовать. Последние два параметра у всех этих функций одинаковы. Это число разбиений вокруг оси Z и число разбиений вдоль оси Z. Как я уже говорил, все сложные объекты состоят из простых: точек, линий и многоугольников. Вы понимаете, что нарисовать/создать идеально гладкую сферу или цилиндр невозможно. Поэтому строится приближение из плоскостей. Для этого и нужно указать количество разбиений. Чем больше разбиение, тем лучше будет выглядеть ваша сфера. Однако, задавать здесть число с шестью нулями не стоит. Это лишено всякого смысла. Оптимальным, на мой взгляд, является число от 10 до 20. Чем больше объект, тем больше нужно разбиений. Число разбиений вдоль и поперек я выставляю одинаковыми. Сначала вы должны создать объект типа GLUquadricObj с помощью функции gluNewQuadric. Теперь устанавливаете свойства с помощью функции gluQuadricDrawStyle. Доступны стили: GLU_FILL - рисуется сплошной объект, GLU_LINE - проволочный объект, GLU_POINT - рисуются только точки. Рисуете то, что хотите. И не забудьте удалить созданный объект, воспользовавшись gluDeleteQuadric.
Настало время поэкспериментировать. Создайте очередное консольное приложение и отредактируйте функцию display следующим образом.
void CALLBACK display(void) { GLUquadricObj *quadObj; quadObj = gluNewQuadric(); // создаем новый объект // для создания сфер и цилиндров glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); glPushMatrix(); glColor3d(1,0,0); gluQuadricDrawStyle(quadObj, GLU_FILL); // устанавливаем // стиль: сплошной gluSphere(quadObj, 0.5, 10, 10); // рисуем сферу // радиусом 0.5 glTranslated(-2,0,0); // сдвигаемся влево glRotated(45, 1,0,0); // поворачиваем glColor3d(0,1,0); gluQuadricDrawStyle(quadObj, GLU_LINE); // устанавливаем // стиль: проволочный gluCylinder(quadObj, 0.5, 0.75, 1, 15, 15); glPopMatrix(); gluDeleteQuadric(quadObj); auxSwapBuffers(); }
Прозрачность
С помощью четвертого компонента цвета можно получать различные эффекты наложения объктов друг на друга, наложения цветов и т.п. Здесь я расскажу о наиболее нужном и распрострененном эффекте - прозрачности объектов. Для того, чтобы разрешить обрабатывать четвертый компонент цвета вы должны вызвать функцию glEnable с аргументом GL_ALPHA_TEST. Для получения требуемого эффекта прозрачности нужно разрешить наложение цветов - glEnable(GL_BLEND). И установить алгоритм, по которуму будут смешиваться два цвета - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA). Учтите, что эти режимы очень затормаживают вывод изображения, поэтому я не рекомендую устанавливать эти режимы глобально, для воспроизведения всех объектов. Выделите из ваших объектов те, которым требуется этот режим, включайте и отключайте его своевременно. Именно поэтому, эти тесты я разместил в функции display. Создайте новый проект с именем transperence и отредактируйте функцию display, как показано ниже.
void CALLBACK display(void) { 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); auxSwapBuffers(); }
В заключение, добавлю, что если вы поменяете местами создание цилиндра и сферы, то никакого эффекта прозрачности не увидите. Это не баг, а фича - так задумано. Секрет прост: вы должны сначала рисовать дальние объекты, а потом ближние. Связано это с тем, что выполняется тест глубины. Теперь смотрите, что получается. Если вы сначала рисуете сферу(она удалена), а потом цилиндр, то выполняется это следующим образом.
Создаем сферу. Выполняется тест глубины успешно, т.к. цилиндра пока нет. В буфере рисуется сфера. Создаем цилиндр. Выполняется тест глубины успешно, т.к. стенка цилиндра ближе, чем сфера. В буфере рисуется цилиндр, он закрывает сферу с учетом прозрачности.
Теперь смотрим, что происходит, если сначала нарисовать цилиндр, потом сферу.
Создаем цилиндр. Выполняется тест глубины успешно. В буфере рисуется цилиндр. Создаем сферу. Вполняется тест глубины аварийно, т.к. сфера создается за цилиндром. В буфере ничего не рисуется.
В последнем случае вы увидите один цилиндр.
Исходный файл смотрите . Исполняемый файл .
Трафарет I
Зачем это может понадобиться, я не очень представляю. Рисовать прерывистую линию вы уже умеете. Трафарет - это то же самое, только для многоугольников. Вы разрешаете тест трафарета командой glEnable(GL_POLYGON_STIPPLE). Аналогично, как и в случае с линями, нужно загружать массив, который задает битовую маску. Размер трафарета строго оговорен - 32х32 пикселя. 32х32 равняется 1024. 1024 делим на восемь бит, получаем 128 байт, т.е., чтобы закодировать трафарет, нужен массив из 128-ми байт. Тут уже, как в случае с линиями, калькулятором не посчитаешь. Поэтому я написал утилиту - , которая конвертирует pcx-файл в формате два бита на пиксель в массив на языке Си. Запускается она так:
>pcx_2bpp filename.pcx >array.c
Утилиту можете взять на моем сервере в архиве с исходными текстами программ(см. приложения А). Знак "больше" означает перенаправление стандартного вывода в файл array.c. Эта утилита абсолютно переносима. Нарисовав в каком-нибудь графическом пакете рисунок в формате 32х32х2bpp, сохраните его и обработайте моей утилитой. Получите массив, который вставьте в свою программу. В фунции main добавьте следующий код:
glEnable(GL_POLYGON_STIPPLE); glPolygonStipple(array);
Трафарет II
Ранее вы познакомились с надоложение трафарета состоящего из массива бит 32х32 точки. Недостатки такого трафарета в его небольшой площади и то, что каждые его элемент является битом, т.е. у такого трафарета всего две зоны(рисуем и не рисуем). Далее вы познакомитесь с полноценным трафаретом.
В OpenGL есть буфер трафарета, который предоставляет огромные возможности для творчества. Тест трафарета на мой взгляд бесспорно полезная вещь. С ее помощью реализуются самые разнообразные эффекты, начиная от простого вырезания одной фигуры из другой до реализации теней, отражений и прочих нетривиальных функций, требующих от вас уже не только знакомство с библиотекой OpenGL, но и понимания алгоритмов машинной графики. Здесь мы рассмотрим самое простое применение буфера трафарета. Пусть у нас есть два объекта на экране - сфера и куб, и пусть сфера находится внутри куба и немного из него выходит за его грани.
Рисунок 4.12.1
Мы поставим себе задачу изобразить три рисунка: куб минус сфера, сфера минус куб, пересечение куба и сферы. Как вы понимаете объединение куба и сферы выводится без всякого буфера трафарете по умолчанию. Собственно говоря, оно(объединение) изображено на рисунке 4.12.1. Теперь о том, что такое трафарет и как им пользоваться. Трафарет это двумерный массив целых переменных(тип int). Каждому пикселю в окне соответствует один элемент массива. Использование буфера трафарета происходит в два этапа. Сначала вы его заполняете, потом основываясь на его содержимом рисуете ваши объекты. Буффер трафарета заполняется следующим образом. Вы делите окно вывода на зоны и каждой зоне присваеваете свое значение. Например, для рисунка 4.12.1 область, где нет ничего будет заполнена нулями, область, где выведен куб заполнена единицами и область, где видна сфера двойками. Обратите внимание, что буффер трафарета - это двумерный массив, а не трехмерный. Теперь вы легко можете представить себе эти области, они собственно и изображены на приведенном рисунке. Также заметьте, что цвет здесь роли не играет. Я бы мог вывести все черным цветом, а буфер трафарета заполнился в зависимости от геометричеких размеров фигур и от их пересечения. Далее мы рассмотрим функции библиотеки OpenGL для работы с трафаретом. Тест трафарета разрешается при помощи функций glEnable\glDisable с параметром GL_STENCIL_TEST. Очищается буфер трафарета при помощи функции glClear с параметром GL_STENCIL_BUFFER_BIT. Заполнение буфера трафарета происходит при помощи следующих двух функций:
void glStencilFunc(GLenum func, GLint ref, GLuint mask) void glStencilOp(GLenum fail, GLenum zfail, GLenum zpass)
GL_NEVER | Не проходит |
GL_LESS | Проходит if ( ref & mask) < ( stencil & mask) |
GL_LEQUAL | Проходит if ( ref & mask) ? ( stencil & mask) |
GL_GREATER | Проходит if ( ref & mask) > ( stencil & mask) |
GL_GEQUAL | Проходит if ( ref & mask) ? ( stencil & mask) |
GL_EQUAL | Проходит if ( ref & mask) = ( stencil & mask) |
GL_NOTEQUAL | Проходит if ( ref & mask) ? ( stencil & mask) |
GL_ALWAYS | Всегда проходит |
GL_KEEP | Сохранить текущее значение в буфере трафарета |
GL_ZERO | Установить значение буфера трафарета в ноль |
GL_REPLACE | Заменить значение буфера трафарета на значение переменной ref, заданной функцие glStencilOp |
GL_INCR | Увеличить на единицу |
GL_DECR | Уменьшить на единицу |
GL_INVERT | Поразрадно инвертировать |
glStencilFunc(GL_NEVER, 1, 0); // значение mask не используется glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP); auxSolidCube(2.5);
Объясняю подробней, первая функция говорит о том, что тест трафарета всегда проходит неудачно. Вторая функция задает, что в случае неудачного теста трафарета заменить значение храняцееся в буфере трафарета на значение переменной ref, а его мы задали равным единице. В результате, на экране ничего не нарисуется т.к. тест трафарета завершался неудачно, но в буфере трафарета мы получили проекию куба из единичек, т.е. буфер трафарета заполнен не только нулями. Теперь мы хотим заполнить двойками область, где прорисовывается сфера. Здесь мы уже должны учитывать буфер глубины, иначе мы заполним двойками всю область, где у нас рисуется сфера. Для того чтобы учитывать буфер глубины тест трафарета должен завершиться положительно. Третий параметр zpass функции glStencilOp как раз указывает, что делать, если тест трафарета прошел, а тест глубины нет. Поэтому код выглядит так:
glStencilFunc(GL_ALWAYS, 2, 0); // значение mask не используется glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); auxSolidSphere(1.5);
В результате получаем буфер трафарета заполненный нулями, где ничего не было, единицами, где виден куб и двойками, где видна сфера. В последнем примере тест трафарета прошел успешно, поэтому на экране была нарисована сфера. Но это нам не мешает мы очистим буфер глубины и буфер цвета, но не буфер трафарета.
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
Теперь дело техники нарисовать тотже самый куб без сферы. Надо установить, что тест трафарета проходит, если значение находящееся в буфере трафарета совпадает со сзначением второго параметра функции glStencilFunc. Как вы помните куб в буфере трафарета имел значение единицы. Поэтому получаем:
glStencilFunc(GL_EQUAL, 1, 255); // куб рисуется только там, где // в буфере трафарета лежат единицы glColor3d(1,1,1); auxSolidCube(2.5); Создайте новый проект с именем stencil. Скопируйте glaux.c в stencil.c и отредактируйте функцию display следующи образом:
void CALLBACK display(void){ // очищаем все буферы glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); // разрешаем тест трафарета glEnable(GL_STENCIL_TEST); // рисуем куб и заполняем буффер трафарета единицами // в том месте, где рисуется куб // тут у меня немного по другому, чем я выше было разоьрано, // но действие выполняется анологичное glStencilFunc(GL_ALWAYS, 1, 0); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); auxSolidCube(2.5); // заполняем буффер трафарета двойками // в том месте, где сфера закрывает куб glStencilFunc(GL_ALWAYS, 2, 0); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); auxSolidSphere(1.5); // очищаем буфферы цвета и глубины glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glStencilFunc(GL_EQUAL, 1, 255); glColor3d(1,1,1); auxSolidCube(2.5); // вращаем сцену glRotated(3, 1,0,0); glRotated(5, 0,1,0); glRotated(7, 0,0,1); auxSwapBuffers();}
Рисунок 4.12.2
Теперь давайте немного приукрасим нашу программу. Давайте внутри куба нарисуем красный шар, который будут опоясывать зеленый и синий тор. Делается это следующим образом. Как и в предыдущей программе вы составляете буфер трафарета, после чего очищаете буфер глубины и цета, запрещаете тест трафарета, т.к. нам надо просто вывести фигуры без трафарета, выводите шар с торами, и, наконец, включает тест трафарета и выводите куб с отверстиями. Вот код фунции display:
void CALLBACK display(void){ // очищаем все буферы glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); // разрешаем тест трафарета glEnable(GL_STENCIL_TEST); // рисуем куб и заполняем буффер трафарета единицами // в том месте, где рисуется куб // тут у меня немного по другому, чем я выше было разоьрано, // но действие выполняется анологичное glStencilFunc(GL_ALWAYS, 1, 0); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); auxSolidCube(2.5); // заполняем буффер трафарета двойками // в том месте, где сфера закрывает куб glStencilFunc(GL_ALWAYS, 2, 0); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); auxSolidSphere(1.5); // очищаем буфферы цвета и глубины glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
Исходный файл смотрите . Исполняемый файл .
Туман
Еще одна мелочь, которую мы рассмотрим - это включение тумана. За основу возьмем нашего снеговика. Надо сказать, что ведет туман себя довольно странно. Если его цвет установить белым, то при увелечении плотности тумана снеговик становится полностью белым, причем даже те его места, которые изначально были черными, т.к. не освещались. Когда я установил цвет тумана темно-серым - (0.25, 0.25, 0.25), то при увелечении плотности не освещенные его части как им и положено оставались черными, а видимые теряли яркость. Я скорее охарактерезовал бы средство тумана, как средство для изменения яркости, потому как на реальный туман, это к сожалению, не похоже. Итак, создайте новый проект с именем fog, скопируйте snowman.c в fog.c. Для того чтобы включить туман и поэксперементировать с его влиянием на изображение мы добавим две функции, которые будут обрабатывать события от стрелок вверх\вниз на клавиатуре. Код этих функций выглядит так, включите его сразу после включения заголовочных файлов:
float density; void CALLBACK Key_UP(void ) { density+=(float)0.1; glFogf(GL_FOG_DENSITY, density); } void CALLBACK Key_DOWN(void ) { density-=(float)0.1; glFogf(GL_FOG_DENSITY, density); }
Я также ввел глобальную переменную density, в ней хранится плотность тумана. При нажатие стрелок вверх\вниз будет вызываться соответсвующая функция и будет изменено значение тумана. Снеговик у нас все время перерисовывается, поэтому при следующей отрисовке кадра значение тумана обновится. Теперь надо отредактировать функцию main, добавленный код выделен серым фоном:
void main() { float pos[4] = {3,3,3,1}; float dir[3] = {-1,-1,-1};
auxInitPosition( 50, 10, 400, 400); auxInitDisplayMode( AUX_RGB | AUX_DEPTH | AUX_DOUBLE ); auxInitWindow( "Snowman" ); auxIdleFunc(display); auxReshapeFunc(resize);
glEnable(GL_ALPHA_TEST); glEnable(GL_DEPTH_TEST); glEnable(GL_COLOR_MATERIAL); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glLightfv(GL_LIGHT0, GL_POSITION, pos); glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, dir); auxMainLoop(display); } Я полагаю здесь все ясно, прокомментирую тут только одну функцию:
glGetFloatv(GL_FOG_DENSITY, &density);
С помощью этой функции я получаю значение плотности тумана, установленное по умолчанию. Первый аргумент этой функции указывает OpenGL значение какого параметра мы хотим получить. Второй аргумент - это адрес в памяти куда будет записано значение данного параметра.
Упражнение "пересечение сферы и куба"
Напишите программу, в которой будет крутиться пересечение сферы и куба.
Сфера, цилиндр и диски"
Доработайте код, приведенный выше, чтобы в первой строке показывались три сферы. Цвет и стили(GLU_POINT, GLU_LINE и GLU_FILL) должны быть разными. В следующих трех строках должны быть цилиндры, диски и частичные диски.
Исходный файл смотрите . Исполняемый файл .
Упражнение "сфера минус куб"
Напишите программу, в которой будет крутиться сфера минус куб.
Снег"
Ранее рассматривалось приложение "Снеговик". Анимацию создавать вы тоже уже научились. Добавьте косой снег, только сделайте снеговика прозрачным, чтобы снежинки как бы пролетали сквозь него.
Совершенствуем Arcanoid"
Не знаю, правильно ли сказал. Мне кажется, что уродуем, а не совершенствуем. В общем, наложите трафарет на подставку и шар. Когда увидите трафарет на шаре, то поймете, что "Уродуем".
Исходный файл смотрите . Исполняемый файл .
Три плоскости"
Добавьте еще две плоскости. Расположите их так, чтобы они отсекали двумерный угол [(p)/2].
Общие слова
Строить примитивные объекты вы уже научились. Но строить трехмерные сцены с использованием только примитивов вам вряд ли придется, да и выглядят они как-то схематично и тускло. Для того, чтобы их оживить, на примитивы накладывают картинки - текстуры. В качестве фона сцены тоже полезно использовать графическое изображение. Тем самым приложение сделается более живым и интересным. Так что в этой главе мы научимся работать с изображениями.
Повторение тектуры
Размножить текстуру на плоскости не составляет большого труда. Давайте немного отредактируем программу из предыдущего раздела. Для того чтобы иметь возможность повторять текстуру, нужно установить параметр GL_REPEAT для ее S и T координат. S-координата текстуры - это горизонтальная координата, T-координата - вертикальная. Второй параметр, который может быть установлен для координат, - GL_CLAMP. Он гарантирует, что текстура не будет размножена. По умолчанию установлено GL_REPEAT. Но я все-таки приведу соответствующий код, чтобы вы представляли, как устанавливать этот параметр. В функцию main добавьте следующие строки:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); Теперь отредактируйте функцию display.
void CALLBACK display(void) { glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); glEnable(GL_TEXTURE_2D); glColor3d(1,1,1); glBindTexture(GL_TEXTURE_2D, space_tex ); glBegin(GL_QUADS); glTexCoord2d(0,0); glVertex3d(-5,-5, -0.1); glTexCoord2d(0,1); glVertex3d(-5, 5, -0.1); glTexCoord2d(1,1); glVertex3d( 5, 5, -0.1); glTexCoord2d(1,0); glVertex3d( 5,-5, -0.1); glEnd(); glBindTexture(GL_TEXTURE_2D, photo_tex); glBegin(GL_QUADS); glTexCoord2d(0,0); glVertex2d(-4,-4); glTexCoord2d(0,2); glVertex2d(-4, 4); glTexCoord2d(3,2); glVertex2d( 4, 4); glTexCoord2d(3,0); glVertex2d( 4,-4); glEnd(); glDisable(GL_TEXTURE_2D); auxSwapBuffers(); }
Функция glTexCoord привязывает координаты текстуры к вершинам объекта. Как я уже говорил, левый нижний угол текстуры имеет координату (0,0), а правый верхний - (1,1). Если вы указываете в качестве привязки значение больше единицы, то текстура повторяется. В нашем примере, координату (0,0) текстуры мы привязали к левой нижней вершине плоскости с координатой (-4,-4), а координату (3,2) текстуры к правой верхней вершине (4,4). Тем самым, мы получили размножение текстуры по горизонтали в количестве трех штук и по вертикали в количестве двух штук. Другие две вершины мы связали соответсвующим образом. Если там указать не те числа, то изображение наклонится.
Исходный файл смотрите . Исполняемый файл .
Моя фотография . Звездное небо .
Работа с изображениями
Существует множество графических форматов - bmp, pcx, gif, jpeg и прочие. OpenGL напрямую не поддерживает не один из них. В OpenGL нет функций чтения/записи графических файлов. Но поддерживается работа с массивами пикселей. Вы загружаете графический файл, используя библиотеки других фирм, в память и работаете с ними средствами OpenGL. В массиве данные о пикселах могут располагаться разными способами: RGB, BGR, RGBA; могут присутствовать не все компоненты; каждый элемент массива может занимать один байт, два, четыре или восемь; выравнивание может быть по байту, слову или двойному слову. В общем, форматов расположения данных о графическом изображении в памяти очень много. Я рассмотрю один из них, наиболее часто применяемый, как мне кажется. Информация о каждом пикселе хранится в формате RGB и занимает три байта, выравнивание по байту. В Auxiliary Library есть функция auxDIBImageLoad(LPCSTR), которая загружает в память bmp-файл и возвращает указатель на структуру:
typedef struct _AUX_RGBImageRec { GLint sizeX, sizeY; unsigned char *data; } AUX_RGBImageRec;
Для простоты я буду пользоваться именно этой функцией. Среди прилагаемых программ вы найдете мою утилиту для загрузки файлов из форматов pcx. Исходный текст этой утилиты абсолютно переносим на любую платформу с компилятором ANSI C.
В OpenGL имеются функции для вывода массива пикселей на экран(glDrawPixels), копирования(glCopyPixels), масштабирования(gluScaleImage). Здесь мы рассмотрим только glDrawPixels. Все остальные функции работы с изображениями устроены похожим образом. Для того, чтобы отобразить графический файл в окне OpenGL, вы должны загрузить его в память, указать выравнивание, установить точку, с которой начинается вывод изображения, и вывести его на экран. Раздобудьте где-нибудь свою фотографию в формате BMP. Можете взять фотографию своей девушки. Создайте новый проект. Объявите глобальную переменную -
AUX_RGBImageRec *image Перед вызовом функции auxMainLoop в функции main вставьте строку:
image = auxDIBImageLoad("photo.bmp");
Выравнивание устанавливается вызывом функции glPixelStorei с параметром GL_UNPACK_ALIGNMENT и вторым параметром - целым числом, которое указывает выравнивание. Изображения выводятся прямо на экран. Поэтому все происходит в двухмерных координатах. Позиция, с которой начинается вывод изображения, указывается при помощи функции glRasterPos2d(x,y). Также вы можете установить размер пикселя, вызвав функцию glPixelZoom. Первый параметр этой функции - ширина, второй - высота пикселя. Я вызываю эту функцию с аргументами (1,1), что соответствует нормальному пикселю. Замените (1,1) на (3,2) и вы увидите, как картинка растянется в три раза по горизонтали и в два раза по вертикали. Это случилось, потому что теперь каждый пиксель изображения соответствует прямоугольнику 3х2 в окне. И наконец, вывод осуществляет функция glDrawPixels. Первые два параметра - это ширина и высота. Далее, вы указываете формат, в котором хранится информация в памяти, и тип элементов массива. Последним указывается массив данных. В функцию display вставьте следующий код:
glRasterPos2d(-4.5,-3); // нижний левый угол glPixelZoom(1,1); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // выравнивание glDrawPixels(image->sizeX, image->sizeY, // ширина и высота GL_RGB, GL_UNSIGNED_BYTE, // формат и тип image->data); // сами данные
Также в OpenGL имеется функция glBitmap для отображения битовых массивов. Битовый массив - это последовательность байт, которые кодируют картинку из двух цветов. Соответственно, каждый байт кодирует 8 пикселей. Среди прилагаемых программ вы найдете мою утилиту pcx_2bpp. Она читает pcx-файл формата один бит на пиксель и направляет на стандартный вывод массив на языке Си.
Исходный файл смотрите . Исполняемый файл . Моя фотография .
Создаем текстуру в памяти
Одного вывода изображений недостаточно для создания полноценных трехмерных сцен. Часто возникает потребность накладывать изображение на трехмерные объекты и поворачивать/сдвигать их. Для этих целей существую текстуры. Также текстуры помогут вам покрыть весь объект в виде мозаики. Скажем, когда у вас имеется кирпичная стена, то вам не надо загружать изображение с кучей кирпичей. Достаточно загрузить один кирпич и указать, что эту текстуру нужно размножить по всей плоскости.
Сначала мы разберем создание и наложение текстур на плоскость. Затем рассмотрим наложение текстур на объекты, описанные в секции . И наконец, на все прочие, созданные из многоугольников; в частности, тор и чайник.
Для того, чтобы наложить текстуру на объект, вы должны:
Загрузить графический файл в память Создать имя-идентификатор текстуры Сделать его активным Создать саму текстуру в памяти Установить параметры текстуры Установить параметры взаимодействия текстуры с объектом Связать координаты текстуры с объектом
Первое вы уже научились делать в предыдущей секции. Создайте проект с именем Texture. Объявите следующие глобальные переменные:
unsigned int photo_tex; AUX_RGBImageRec* photo_image; unsigned int space_tex; AUX_RGBImageRec* space_image;
Переменные photo_tex и space_tex будут служить идентификаторами текстур. А в photo_image и space_image мы загрузим bmp-файлы. Тут нужно отметить, что текстуры в OpenGL должны иметь размер 2n x 2m, где n и m целые числа. Это сделано для ускорения работы, т.к. сжимать или растягивать такие текстуры быстрее и удобней. Вы, конечно, можете загрузить изображение любого другого размера, но его придется масштабировать. На мой взгляд, это неправильно. Результат масшабирования вас может и не устроить. Так что я использую графические файлы с размером, кратным степени двойки. Мне удобнее отредактировать изображение в каком-нибудь графическом пакете, урезать его или наоборот дополнить, чем потом выяснять, почему оно искажается. Впрочем, тут многое зависит от конкретного случая. Художнику, который делает текстуры, все равно какого размера ее делать, поэтому легче попросить его сделать изображение с подходящими размерами. Вставьте следующий код в функцию main.
photo_image = auxDIBImageLoad("photo.bmp"); space_image = auxDIBImageLoad("space.bmp");
Картинки возьмите из моей программы - Texture. Фотографию можете взять свою.;-) Только размер ее желательно оставить 512x512.
Теперь вы должны создать имя-идентификатор текстуры. Его нужно создавать, когда у вас в приложении используется более одной текстуры, чтобы была возможность как-то их различать. В противном случае, когда текстура только одна, идентификатор ей не нужен. В следующем примере, при наложение текстуры на сферу, у нас будет ровно одна текстура, и я покажу вызовы каких функций необязательны. А пока, я предполагаю, что вам нужно использовать несколько текстур. Кстати, я просматривал много примеров при написание книги. Среди них были примеры из широко известной Red Book, примеры из MSDN, из интернета и других источников, но все, что касалось текстур, работало только с одной текстурой. Для элементарной программы-примера, конечно, подойдет и одна тектура, а вот для серьезных приложений вряд ли. Мы хотим написать серьезные приложения, поэтому нам придеться использовать нескольких текстур. Функция glGenTextures принимает на вход два параметра. Первый указывает количество имен-идентификаторов текстур, которые нужно создать. Второй параметр - указатель на массив элементов типа unsigned int. Количество элементов в массиве должно совпадать с числом, указанным в качестве первого параметра. Например, следующий код создает десять имен текстур.
unsigned int names[10]; glGenTetures(10, names);
Хранить идентификаторы текстур в массиве не всегда удобно. Такой способ подходит для хранения задних фонов или типов стен - кирпичная, каменная и т.п. В общем, в массиве хранят те элементы, между которыми есть что-то общее. В нашем случае, два изображения связаны, т.к. используюся в одном приложении, поэтому я создал два различных идентификатора. Так что добавьте следующий код в функцию main.
glGenTextures(1, &photo_tex); glGenTextures(1, &space_tex);
Теперь мы привязываемся к текстуре фотографии, т.е. делаем ее активной. Для этого служит функция glBindTexture. Первый параметр должен быть GL_TEXTURE_2D или GL_TEXTURE_1D. Он показывает, с одномерным или двумерным изображением будем работать. Все примеры здесь касаются двумерных тектур. Для одномерной тектуры я просто не нашел красивого примера. Впрочем, в Red Book есть пример с одномерной текстурой. Там чайник разукрашивают красной лентой. Где взять эти примеры и многое другое смотрите в приложении 'A'. Второй параметр glBindTexture - идентификатор, который мы создали выше при помощи glGenTextures. Теперь добавьте вызов этой функции в main.
glBindTexture(GL_TEXTURE_2D, photo_tex);
Теперь мы должны создать саму текстуру в памяти. Массив байт в структуре AUX_RGBImageRec не является еще текстурой, потому что у текстуры много различных параметров. Создав текстуру, мы наделим ее определенными свойствами. Среди параметров текстуры вы указываете уровень детализации, способ масшабирования и связывания текстуры с объектом. Уровень детализации нужен для наложения текстуры на меньшие объекты, т.е. когда площадь на экране меньше размеров изображения. Нулевой уровень детализации соответствует исходному изображению размером 2nx2m, первый уровень - 2n-1x2m-1, k-ый уровень - 2n-kx2m-k. Число уровней соответствует min(n,m). Для создания текстуры имеется две функции glTexImage[1/2]D и gluBuild[1/2]DMipmaps.
glTexImage2D( gluBuild2DMipmaps( GLenum target, GLenum target, GLint lavel, GLint components, GLint components, GLsizei width, GLsizei width, GLsizei height, GLsizei height, GLenum format, GLint border, GLenum type, GLenum format, const GLvoid* pixels) GLenum type, const GLvoid* pixels)
Основное различие в том, что первая функция создает текстуру одного определенного уровня детализации и воспринимает только изображения , размер которых кратен степени двойки. Вторая функция более гибкая. Она генерирует текстуры всех уровней детализации. Также эта функция не требует, чтобы размер изображения был кратен степени двойки. Она сама сожмет/растянет изображение подходящим образом, хотя возможно окажется, что и не вполне подходящим. Я воспользуюсь функцией glTexImage2D. Первый параметр этой функции должен быть GL_TEXTURE_2D. Второй - уровень детализации. Нам нужно исходное изображение, поэтому уровень детализации - ноль. Третий параметр указывает количество компонентов цвета. У нас изображение хранится в формате RGB. Поэтому значение этого параметра равно трем. Четвертый и пятый параметры - ширина и высота изображения. Шестой - ширина границы; у нас гарницы не будет, поэтому значение этого параметра - ноль. Далее, седьмой параметр - формат хранения пикселей в массиве - GL_RGB и тип - GL_UNSIGNED_BYTE. И наконец, восьмой параметр - указатель на массив данных. Еще вы должны вызвать функцию glPixelStorei и задать, что выравнивание в массиве данных идет по байту. Добавьте следующий код в функцию main.
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D(GL_TEXTURE_2D, 0, 3, photo_image->sizeX, photo_image->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, photo_image->data);
Аналогичный результат можно получить, вставив вызов gluBuild2DMipmaps с параметрами, указанными ниже.
gluBuild2DMipmaps(GL_TEXTURE_2D, 3, photo_image->sizeX, photo_image->sizeY, GL_RGB, GL_UNSIGNED_BYTE, photo_image->data);
Теперь нужно установить параметры текстуры. Для этого служит функция
glTexParameter[if](GLenum target, GLenum pname, GLenum param)
Первый параметр принимает значение GL_TEXTURE_1D или GL_TEXTURE_2D. Второй - pname - определяетя параметр текстуры, который вы будете изменять. И третий параметр - это устанавливаемое значение. Если вы воспользовались gluBuild2DMipmaps вместо glTexImage2D, то вам не надо устанавливать следующие параметры, т.к. уже сформированы текстуры всех уровней детализации, и OpenGL сможет подобрать текстуру нужного уровня, если площадь объекта не совпадает с площадью текстуры. В противном случае, вы должны добавить следующие строки:
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, т.е. цвет элемента объекта будет вычисляться как среднее арифметическое четырех элементов текстуры. Имеются еще четыре алгоритма вычисления цвета элемента объекта. Их можно устанавливать, когда вы создали текстуру со всеми уровнями детализации, т.к. применяют алгоритмы GL_NEAREST и GL_LINEAR к одному или двум ближайшим уровням детализации.
Еще вы можете установить взаимодействие текстуры с объектом. Тут имеются два режима при использовании трех компонентов цвета. Первый режим, установленный по умолчанию, когда у вас учитывается цвет объекта и цвет текстуры. Результирующий цвет получается перемножением компонентов цвета текстуры на компоненты цвета объекта. Скажем, если цвет текстуры - (r,g,b), а цвет объекта, на который она накладывается, - (r0,g0,b0), то результирующим цветом будет - (r*r0,g*g0,b*b0). В случае, если цвет объекта черный - (0,0,0), то вы не увидите на нем текстуру, так как она вся будет черной. Второй режим взаимодействия, когда цвет объекта не учитывается. Результирующим цветом будет цвет текстуры. Эти параметры можно установить следующим образом.
glTexEnv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE) glTexEnv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL)
По умолчанию, как я уже сказал, является режим GL_MODULATE. Теперь сделайте активной текстуру space_tex. И повторите для нее то же самое. На этом заканчивается создание текстуры. Осталось связать координаты текстуры с координатами объекта. Отредактируйте функцию display так:
glEnable(GL_TEXTURE_2D); glColor3d(1,1,1); glBindTexture(GL_TEXTURE_2D, space_tex ); glBegin(GL_QUADS); glTexCoord2d(0,0); glVertex3d(-5,-5, -0.1); glTexCoord2d(0,1); glVertex3d(-5, 5, -0.1); glTexCoord2d(1,1); glVertex3d( 5, 5, -0.1); glTexCoord2d(1,0); glVertex3d( 5,-5, -0.1); glEnd(); glBindTexture(GL_TEXTURE_2D, photo_tex); glBegin(GL_QUADS); glTexCoord2d(0,0); glVertex2d(-4,-4); glTexCoord2d(0,1); glVertex2d(-4, 4); glTexCoord2d(1,1); glVertex2d( 4, 4); glTexCoord2d(1,0); glVertex2d( 4,-4); glEnd(); glDisable(GL_TEXTURE_2D);
Как вы, наверное, догадались, glTexCoord2d сопоставляет координаты текстуры вершинам четырехугольника. Скажу только, что нижний левый угол текстуры имеет координаты (0,0), а верхний правый - (1,1).
Исходный файл смотрите . Исполняемый файл .
Моя фотография . Звездное небо .
Текстура на чайнике
Текстуру можно наложить на объект любой сложности. Для этого надо разрешить автоматически генерировать координаты текстуры - glEnable(GL_TEXTURE_GEN_S) и glEnable(GL_TEXTURE_GEN_T). Далее, вы должны установить один из трех алгоритмов генерации координат текстур.
GL_OBJECT_LINEAR GL_EYE_LINEAR GL_SPHERE_MAP
Алгоритм генерации координат устанавливается с помощью функции glTexGeni. Первый параметр функции указывает тип координаты, для которой будет установлен алгоритм. GL_S -горизонтальная координата, GL_T - вертикальная. Второй параметр этой функции должен быть GL_TEXTURE_GEN_MODE. И третий параметр - один из перечисленных выше алгоритмов. Создайте очередной проект с именем teapot. Отредактируйте функцию main, как в предыдущей программе, где мы накладывали изображение на сферу. Только добавьте там строчку glEnable(GL_AUTO_NORMAL), чтобы чайник лучше выглядел. Этот режим разрешает расчет векторов нормалей, что позволяет получать улучшенные изображения, однако занимает некоторое время. А функцию display отредактируйте, как показано ниже.
void CALLBACK display(void) { 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); glRotated(5,0,1,0); glPushMatrix(); glTranslated(0,3,0); glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR); glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR); auxSolidTeapot(2); glTranslated(0,-3,0); glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); auxSolidTeapot(2); glTranslated(0,-3,0); glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); auxSolidTeapot(2); glPopMatrix(); glDisable(GL_TEXTURE_GEN_S); glDisable(GL_TEXTURE_GEN_T); glDisable(GL_TEXTURE_2D); auxSwapBuffers(); }
Исходный файл смотрите . Исполняемый файл .
Моя фотография .
Конечно, фотография накладывается не самым лучшим образом. А что вы хотели? Машина сама сгенерировать правильно координаты на кривой поверхности не может. Тем не менее, если в качестве текстуры взять изображение в горошек, то оно довольно неплохо ляжет на чайник. Я не великий ходожник и горошек в PaintBrush'e мне изобразить не удалось. Нет, не подумайте, что я совсем криворукий, просто, горошины у меня никак не хотели строиться в одну линию, поэтому я нарисовал следующую картину - "Клеточки". И наложил эту текстуру на чайник.
Исходный файл смотрите . Исполняемый файл .
Текстура на сфере
Здесь я покажу, как работать с одной единственной текстурой и накладывать текстуры на сферы. Создайте новый проект с именем sphere. Добавьте глобальную переменную.
AUX_RGBImageRec* photo_image; В функции main загрузите изображение и создайте текстуру. Поскольку текстура у нас в этом приложении всего одна, то создавать идентификатор для нее не надо.
void main() { auxInitPosition( 50, 10, 400, 400); auxInitDisplayMode( AUX_RGB | AUX_DEPTH | AUX_DOUBLE ); auxInitWindow( "Shapes" ); auxIdleFunc(display); auxReshapeFunc(resize); glEnable(GL_DEPTH_TEST); glEnable(GL_TEXTURE_2D); photo_image = auxDIBImageLoad("photo.bmp"); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D(GL_TEXTURE_2D, 0, 3, photo_image->sizeX, photo_image->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, photo_image->data); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); auxMainLoop(display); }
Отредактируйте функцию display. Здесь все вам знакомо см. 4.1, кроме gluQuadricTexture. Эта функция разрешает или запрещает наложение текстуры на трехмерный объект. Второй параметр GL_TRUE или GL_FALSE. По умолчанию наложение текстуры запрещено.
void CALLBACK display(void) { GLUquadricObj *quadObj; glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); quadObj = gluNewQuadric(); gluQuadricTexture(quadObj, GL_TRUE); gluQuadricDrawStyle(quadObj, GLU_FILL); glColor3d(1,1,1); glRotated(5, 0,1,0); glPushMatrix(); glRotated(-90, 1,0,0); gluSphere(quadObj, 3, 16, 16); glPopMatrix(); gluDeleteQuadric(quadObj); auxSwapBuffers(); }
Исходный файл смотрите . Исполняемый файл .
Моя фотография .
Фон для игры Arcanoid"
Найдите красивый фон в формате BMP. Можете опять взять свою фотографию. Подложите этот bmp-файл в качестве фона в игру Arcanoid. Закомментируйте разрешение всех тестов, кроме GL_DEPTH_TEST, в функции main. Возможно, я уже говорил о том, что вы всегда должны помнить, что дополнительные параметры затормаживают создание объекта, поэтому устанавливайте их очень осторожно. Часть из них можно установить в функции main. Другие же лучше устанавливать и отменять непосредственно при создании объекта в функции display.
Исходный файл смотрите . Исполняемый файл . Звездное небо .
Текстуру в жизнь"
Наложите текстуру на цилиндр, конус, диски и частичный диск. В качестве изображения возьмите какую-нибудь мозаику, горошек и т.п., потому как что-нибудь осмысленное будет смотреться не очень хорошо.
Вращаем текстуру"
Завращайте плоскость с фотографией вокруг оси X и Y. Также пусть она равномерно колеблется вдоль оси Z от 3 до 7.
Исходный файл смотрите . Исполняемый файл .
Моя фотография . Звездное небо .
Лампы и их свойства
Все параметры лампы задаются с помощью функции glLight, которая имеет следующий прототип:
void glLight[if][v]( GLenum light, GLenum pname, GLfloat param)
Первый аргумент определяет номер лампы. Его можно задавать двумя способами. Первый - явно указать GL_LIHGTi, где GL_LIGHTi предопрелено в файле gl.h следующим образом:
/* LightName */ #define GL_LIGHT0 0x4000 #define GL_LIGHT1 0x4001 #define GL_LIGHT2 0x4002 #define GL_LIGHT3 0x4003 #define GL_LIGHT4 0x4004 #define GL_LIGHT5 0x4005 #define GL_LIGHT6 0x4006 #define GL_LIGHT7 0x4007
Второй способ - GL_LIGHT0 + i, где i номер лампы. Такой способ используется, когда вам надо в цикле изменять параметры ламп. Второй аргумент определяет имя параметра, а третий его значение. Я здесь не буду перечислять всевозможные параметры и их допустимые значения, для этого есть справочник и MSDN. Я покажу и прокомментирую лишь небольшой пример, использующий три лампы. Для начала, давайте разберем шаблонный пример. С помощью следующих функций разрешаем освещение и включаем нулевую лампу.
glEnable(GL_LIGHTING); glEnable(GL_LIGHT0);
Массивы pos и dir содержат координаты местоположения лампы и направления, куда она светит. Массив dir содержит три координаты - x,y,z. Массив pos - четыре, назначение четвертого мне не очень ясно. Если его значение отличается от нуля, то изображение вполне логичное получается. Если же он ноль, то получается что-то непотребное.
glLightfv(GL_LIGHT0, GL_POSITION, pos); glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, dir); Создайте новый проект с именем lamps. Скопируйте шаблонный файл glaux.c. Отредактируйте функцию main:
void main() { float pos[4] = {3,3,3,1}; float color[4] = {1,1,1,1}; float sp[4] = {1,1,1,1}; float mat_specular[] = {1,1,1,1}; auxInitPosition( 50, 10, 400, 400); auxInitDisplayMode( AUX_RGB | AUX_DEPTH | AUX_DOUBLE ); auxInitWindow( "Glaux Template" ); auxIdleFunc(display); auxReshapeFunc(resize); glEnable(GL_DEPTH_TEST); glEnable(GL_COLOR_MATERIAL); glEnable(GL_LIGHTING); glEnable(GL_LIGHT3); glEnable(GL_LIGHT5); glEnable(GL_LIGHT6); glLightfv(GL_LIGHT3, GL_SPECULAR, sp); glLightfv(GL_LIGHT5, GL_SPECULAR, sp); glLightfv(GL_LIGHT6, GL_SPECULAR, sp); color[1]=color[2]=0; glLightfv(GL_LIGHT3, GL_DIFFUSE, color); color[0]=0; color[1]=1; glLightfv(GL_LIGHT5, GL_DIFFUSE, color); color[1]=0; color[2]=1; glLightfv(GL_LIGHT6, GL_DIFFUSE, color); glLightfv(GL_LIGHT3, GL_POSITION, pos); pos[0] = -3; glLightfv(GL_LIGHT5, GL_POSITION, pos); pos[0]=0;pos[1]=-3; glLightfv(GL_LIGHT6, GL_POSITION, pos); glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular); glMaterialf(GL_FRONT, GL_SHININESS, 128.0); auxMainLoop(display); }
Здесь надо сделать пояснения для вызовов функции glLight с параметрами GL_DIFFUSE и GL_SPECULAR. GL_DIFFUSE - определяет цвет света источника. В данном примере у нас три лампы - с красным источником света, с зеленым и с синим. GL_SPECULAR определяет отраженный свет, см. предыдущий пункт.
Теперь измените функциb display:
glColor3d(1,1,1); auxSolidSphere(2);
Исходный файл смотрите . Исполняемый файл .
Материал
Материал может рассеивать, отражать и излучать свет. Свойства материала устанавливаются при помощи функции
glMaterialfv(GLenum face, GLenum pname, GLtype* params)
Первый параметр определяет грань, для которой устанавливаются свойства. Он может принимать одно из следующих значений:
GL_BACK задняя грань GL_FONT передняя грань GL_FRONT_AND_BACK обе грани
Второй парметр функции glMaterialfv определяет свойство материала, которое будет установлено, и может принимать следующие значения.
GL_AMBIENT рассеянный свет GL_DIFFUSE тоже рассеянный свет, пояснения смотри ниже GL_SPECULAR отраженный свет GL_EMISSION излучаемый свет GL_SHININESS степень отраженного света GL_AMBIENT_AND_DIFFUSE оба рассеянных света
Ambient и diffuse переводятся на русский как "рассеянный". Разница между ними не очень понятна. Я использую только GL_DIFFUSE. Третий параметр определяет цвет соответствующего света, кроме случая GL_SHININESS. Цвет задается в виде массива из четырех элементов - RGBA. В случае GL_SHININESS params указывает на число типа float, которое должно быть в диапазоне от 0 до 128. Я написал простенький пример с цилиндром и раскрасил его грани в разные цвета. Вам надо всего лишь модифицировать функцию display.
void CALLBACK display(void) { GLUquadricObj *quadObj; GLfloat front_color[] = {0,1,0,1}; GLfloat back_color[] = {0,0,1,1}; quadObj = gluNewQuadric(); glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); glMaterialfv(GL_FRONT, GL_DIFFUSE, front_color); glMaterialfv(GL_BACK, GL_DIFFUSE, back_color); glPushMatrix(); glRotated(110, -1,1,0); gluCylinder(quadObj, 1, 0.5, 2, 10, 10); glPopMatrix(); gluDeleteQuadric(quadObj); auxSwapBuffers(); } И вы должны разрешить режим освещенности для двух граней. По умолчанию он запрещен. Добавьте в функцию main следующую строчку.
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE);
Исходный файл смотрите . Исполняемый файл .
Модель освещения
По умолчанию освещение отключено. Включается оно командой glEnable(GL_LIGHTING). В базовом шаблоне освещение я включил, потому что без освещения работать практически невозможно. Сфера всегда будет показываться как круг, а конус - как круг или треугольник. Если монотонное тело у вас равномерно освещено, то вы не можете увидеть его рельеф. Поэтому нам нужно использовать источники света. Сначала рассмотрим функцию, которая устанавливает базовые настройки. Когда вы разрешили освещение, то вы можете уже устанавливать фоновую освещенность. По умолчанию, значение фоновой освещенности равно (0.2, 0.2, 0.2, 1). Создайте новый проект, скопируйте туда шаблонный файл и отключите освещение. Вы с трудом сможете различить сферу на экране. С помощью функции glLightModel вы можете установить фоновое освещение. Если вы повысите его до (1,1,1,1), т.е. до максимума, то включать источники света вам не понадобится. Вы их действия просто не заметите, т.к. объект уже максимально освещен. И получится, что вы как бы отключили освещение. В общем, добавьте в main вызов следующей функции:
float ambient[4] = {0.5, 0.5, 0.5, 1}; ... glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient);
Попробуйте изменить параметры и посмотрите на результат. Нулевую лампу (glEnable(GL_LIGHT0)) лучше отключить.
Исходный файл смотрите . Исполняемый файл .
Общие понятия
Создавать объекты и накладывать на них текcтуры вы научились. Осталась последняя из основных тем - это освещение объектов. Освоив освещение, вы сможете создавать полноценные трехмерные сцены. Освещение любого объекта зависит от двух факторов. Первый - это материал, из которого сделан объект. Второй - это свет, которым он освещен. В этой главе мы подробно рассмотрим все особенности OpenGL, касающиеся освещения объектов.
Тени
Тени напрямую не поддерживаются библиотекой OpenGL, поэтому этот материал для второй части книги. А если честно, то я с ними пока еще не разбирался.;-)
Open GL
Java-апплеты - Magician Library
Достоинством данного типа приложения, конечно же, является переносимость( для тех платформ, для которых существует плагин) и незаменимое средство для web-программирования. Вы можете украсить свой web-сервер апплетами с трехмерной графикой. К вашим услугам все возможности OpenGL и объектно-ориентированного программирования. Недостатком является сложность программирования на языке Java. За короткое время(три месяца) работы с этим языком на меня свалилось очень много элементарых проблем: отсутствие форматированного ввода/вывода, непонятное поведение апплета - в разных броузерах по-разному; устаревшие методы, которые одним компилятором воспринимались нормально, а другой выдавал предупреждение, и прочие мелкие проблемы. Вообще, писать java-приложения, т.е. самостоятельные программы, я бы не советовал. Воспользуйтесь альтернативой - OpenGL Auxilary Library, рассмотренной в самом начале этой главы. Если же вам необходимо переносимое приложение, то возьмите его из примеров к Magician Library. Для java-апплетов - программ, исполняющихся в web-броузерах, альтернативы нет. Поэтому я и рассматриваю здесь программирование апплетов. У этого типа приложений имеются очень серьезные недостатки. Для запуска апплетов требуется плагин. Работают такие апплеты крайне нестабильно, во всяком случае на момент времени июнь 1999 года было много проблем. Сейчас вышла еще одна библиотека OpenGL для работы с java-апплетами, она называется GL4Java. Далее я ее рассмотрю.
Комметировать java-код, я думаю, излишне после рассмотренных здесь трех примеров приложений. Я лишь приведу здесь свой шаблонный файл template.java.
import java.applet.*; import java.awt.*; import java.awt.event.*; import com.hermetica.magician.*; import com.hermetica.util3d.*; public class template extends Applet implements GLEventListener { private CoreGL gl_ = new CoreGL(); private CoreGLU glu_ = new CoreGLU(); private GLComponent glc = null; FrameRateComponent frc = new FrameRateComponent(); final int width = 400; public void init() { glc = (GLComponent)GLDrawableFactory.createGLComponent(width, width); add( "Center", glc ); /** Setup the context capabilities */ GLCapabilities cap = glc.getContext().getCapabilities(); cap.setDepthBits(12); cap.setPixelType(GLCapabilities.RGBA); cap.setColourBits(24); cap.setRedBits(1); cap.setDoubleBuffered(GLCapabilities.DOUBLEBUFFER); frc.setSummaryOnly(true); /** Register the GLEventListener with the GLComponent */ glc.addGLEventListener(this); /** Initialize the component */ glc.initialize(); glc.start(); setSize(width,width); } public int getWidth() { return width; } public int getHeight() { return width; } /** Initialization stuff */ public void initialize( GLDrawable component ) { float pos[] = {3,3,3,1}; float dir[] = {-1,-1,-1}; gl_.glClearColor( 1.0f, 1.0f, 0.796875f, 1.0f ); gl_.glShadeModel( GL.GL_SMOOTH ); gl_.glEnable(GL.GL_ALPHA_TEST); gl_.glEnable(GL.GL_DEPTH_TEST); gl_.glEnable(GL.GL_COLOR_MATERIAL); gl_.glEnable(GL.GL_LIGHTING); gl_.glEnable(GL.GL_LIGHT0); gl_.glEnable(GL.GL_BLEND); gl_.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA); gl_.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, pos); gl_.glLightfv(GL.GL_LIGHT0, GL.GL_SPOT_DIRECTION, dir); } /** Handles viewport resizing */ public void reshape( GLDrawable component, int x, int y, int width, int height) { /** Setup the viewport */ gl_.glViewport( component, x, y, width, height); gl_.glMatrixMode( GL.GL_PROJECTION ); gl_.glLoadIdentity(); gl_.glOrtho(-5,5,-5,5,2,12); glu_.gluLookAt( 0,0,5, 0,0,0, 0,1,0 ); gl_.glMatrixMode( GL.GL_MODELVIEW ); } public void display( GLDrawable component ) { frc.startSample(); gl_.glClear( GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT ); gl_.glTranslated(0.01,0,0); gl_.glColor3d(1,0,0); shapes.solidSphere(0.6, 16, 16); frc.stopSample(); } public void stop() { if(glc.isRunning()) glc.suspend(); } public void start() { if(glc.isRunning()) glc.resume(); } public void destroy() { glc.destroy(); } public GL getGL() { return gl_; } }
Исходный файл смотрите . Исполняемый файл .
Консольное приложение - Win32 Console Application
Достоинством приложений данного типа является переносимость на другие платформы при условии, что вы не пользовались другими платформенно зависимыми библиотеками. Реализация OpenGL Auxilary Library существует для большинства платформ. Также здесь значительно упрощена начальная инициализация, т.е. вы быстро можете начать программировать. Идеально подходит для начинающих. Именно поэтому, все примеры в этой книге я привязал к этому варианту приложения OpenGL. Хотя вы можете вставлять приводимый код в WinAPI-приложение и MFC-приложение, и все будет работать точно так же. Для Java-приложения вам придется добавить префиксы к функциям. Недостатком является урезанная функциональность. Вы не можете обрабатывать все сообщения, которые приходят вашему окну. Такой тип приложения идеально подходит для написания небольших портабельных утилит, размером до 25Kb.
Здесь, как я уже говорил, начальная инициализация самая простая. Библиотека сделает за вас большинство действий. От вас потребуется совсем немного по сравнению с другими типами приложений. Итак, давайте для начала создадим проект. Общепризнанно, что тремя наилучшими компиляторами считаются GNU C, Microsoft C и Watcom C. Inprise(Borland) отпадает. Компиляторами других фирм я не пользовался. Все мои задачи решались вышеуказанными четырьмя компиляторами. Правда, должен заметить, что с 1997 года я практически прекратил пользоваться компилятором фирмы Borland. Лишь изредка, когда нужно было перекомпилировать старые утилиты, написанные еще для MSDOS. Эта книга для начинающих, и я хочу сделать ее понятной большинсву читателей. Поэтому я не буду рассматривать проекты для GNU C или Watcom C. Не хочу здесь городить непонятные многим начинающим makefile'ы. Однако, в конце данной главы будет разобрано приложение для UNIX, там уже от makefile'ов никуда не деться. Теперь вернемся к нашим баранам.
Запустите MSVisualC++6.0 Щелкните меню File->New->Win32 Console Application. Выберете каталог и имя проекта задайте glaux, щелкните OK. Выберете An Empty Project, щелкните Finish. Создайте новый текстовый файл и сохраните его с именем glaux.c. Присоедините его к проекту. Project->Add To Project->Files Щелкните Build->Set Active Configuration и установите тип проекта glaux - Win32 Release Далее щелкаете Project->Settings->Link->Object/library modules: и добавьте туда opengl32.lib, glu32.lib и glaux.lib
Проект у нас теперь есть, давайте писать glaux.c. Файл, в котором находится исходный код программы, желательно начинать с комментария. Это необязательно, но этого требует хороший стиль. В комментариях можно указать имя автора, способ связи - обычно, адрес электронной почты. Далее, можно кратко описать, что находится в этом файле. Неплохо вести некоторый дневник здесь же: что и когда вы добавили. У меня эти комментарии выгядят так:
/* * (c) Copyright 1995-1999, Igor Tarasov * FidoNet: 2:5020/370.2 620.20 1103.5 * Inet: itarasov@rtuis.miem.edu.ru * Phone: (095)942-50-97 */
Теперь надо включить заголовочные файлы:
#include <windows.h> #include <GL/gl.h> #include <GL/glu.h> #include <GL/glaux.h>
Давайте напишем функцию main(). Я посчитал, что наиболее правильно и понятно будет дать код и прокомментировать его подробно. У меня функция main() выглядит так:
void main() { float pos[4] = {3,3,3,1}; float dir[3] = {-1,-1,-1}; // указываем координаты окна на экране // верхний левый угол (50,10) // ширина и высота - 400 auxInitPosition( 50, 10, 400, 400); // устанавливаем параметры контекста OpenGL // auxInitDisplayMode( AUX_RGB | AUX_DEPTH | AUX_DOUBLE ); // создаем окно на экране auxInitWindow( "Glaux Template" ); // наше окно будет получать сообщения // от клавиатуры, мыши, таймера или любые другие // когда никаких сообщений нет // будет вызываться функция display // так мы получаем анимацию // если вам нужна статическая картинка, // то закомментируйте следующую строку auxIdleFunc(display); // при изменении размеров окна // придет соответсвующее сообщение // в Windows - это WM_SIZE // мы устанавливаем функцию resize, // которая будет вызвана // при изменении размеров окна auxReshapeFunc(resize); // далее, я устанавливаю ряд тестов и параметров // тест прозрачности, т.е. будет учитываться // четвертый параметр в glColor glEnable(GL_ALPHA_TEST); // тест глубины glEnable(GL_DEPTH_TEST); // glColor будет устанавливать // свойства материала // вам не надо дополнительно // вызывать glMaterialfv glEnable(GL_COLOR_MATERIAL); // разрешаем освещение glEnable(GL_LIGHTING); // включаем нулевую лампу glEnable(GL_LIGHT0); // разрешаем смешение цветов // подробнее смотри главу "Полезные мелочи", // далее в секции "прозрачность" glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // устанавливаем положение нулевой лампы // смотри главу "Освещение" glLightfv(GL_LIGHT0, GL_POSITION, pos); glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, dir); // и последнее, устанавливаем // функцию display отрисовки окна // эта функция будет вызываться всякий раз, // когда потребуется перерисовать окно // например, когда вы развернете окно на весь экран // в windows - это обработчик сообщения WM_PAINT auxMainLoop(display); }
Вот и все с функцией main(). Осталось написать код функции resize и функции display. Вставьте следующий код перед функцией main().
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 ); }
Здесь нужно сделать пояснения по поводу glMatrixMode. Функции glOrtho и glFrustum работают с матрицей, отвечающей за тип проекции. Они просто загружают соответствующую матрицу. Вы можете установить свой тип проекции, если вам это понадобится. Сначала вы говорите, что будете изменять матрицу проекции - glMatrixMode с параметром GL_PROJECTION. Потом, с помощью glLoadMatrix загружаете соответсвующую матрицу. Функции glTranslate/glRotate работают с матрицей вида. Ее мы загружаем последней строкой - glMatrixMode( GL_MODELVIEW ).
В параметрах контекста воспроизведения мы установили AUX_DOUBLE. Это значит, что рисоваться все будет сначала в буфер. Для того, что бы скопировать содержимое буфера на экран, вызывается функция auxSwapBuffers(). Если вы программировали анимацию для MSDOS или Windows, то наверняка использовали такой прием, чтобы избавиться от мерцания на экране. В функции display мы сначала очищаем буфер. Цвет, которым заполняется буфер при очищении, можно установить в функции main() вызовом glClearColor(r,g,b).
void CALLBACK display(void) { glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); /* remove next tree lines * and enter your code here */ glTranslated(0.01,0,0); glColor3d(1,0,0); auxSolidSphere( 1 ); auxSwapBuffers(); }
Вот и все.
Исходный файл смотрите . Исполняемый файл .
MFC-приложение - MFC AppWizard
Этот тип приложения обладаетя всеми достоинствами и недостаками WinAPI-приложения, рассмотренного выше, так как MFC - это библиотека классов С++, т.е. надстройка над WinAPI. Кто-нибудь, конечно, скажет, что приложение на плюсах немеренно большое, работает медленно и MFC для ленивых. В общем, тут у каждого свое мнение. Каждый по-своему прав. Тем не менее, я считаю, что для каждой задачи требуется свой инструмент. Где-то лучше использовать MFC, где-то WinAPI. Кричать, что первое или второе является незаменимым средством на все случаи жизни было бы неверным. У MFC есть свои особенности, отнести которые к достоинствам или недостаткам однозначно нельзя. В зависимости от решаемой задачи они идут в плюс или минус. Согласитесь,что глупо забивать сапожный гвоздь кувалдой или же скобу сапожным молотком.
Создаем проект:
Запустите MSVisualC++6.0 Щелкните меню File->New->MFC AppWizard(exe). Выберете каталог и имя проекта задайте mfc, щелкните OK. Step1: Поставьте переключатель на Single document, далее OK. Step3: Уберите флажок ActiveX Controls, далее OK. Щелкните Finish. Щелкните Build->Set Active Configuration и установите тип проекта MFC - Win32 Release Далее щелкаете Project->Settings->Link->Object/library modules: и добавьте туда opengl32.lib, glu32.lib и glaux.lib
В CMFCView объявите закрытую(private) переменную hGLRC типа HGLRC. Там же объявите функцию int SetWindowPixelFormat(HDC) и открытую(public) функцию display. Вот, что должно получиться:
class CMFCView : public CView { private: CClientDC *pdc; HGLRC hGLRC; int SetWindowPixelFormat(HDC); public: void display(); ...
Вставьте код этой функции в файл MFCView.cpp. Код возьмите из предыдущего раздела. Отредактируйте функцию CMFCView::PreCretaeWindow следующим образом:
BOOL CMFCView::PreCreateWindow(CREATESTRUCT& cs) { // TODO: Modify the Window class or styles here by modifying // the CREATESTRUCT cs cs.style |= (WS_CLIPCHILDREN | WS_CLIPSIBLINGS); return CView::PreCreateWindow(cs); } Добавьте также функцию display сюда:
void CMFCView::display() { glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); glTranslated(0.01,0,0); glColor3d(1,0,0); auxSolidSphere( 1 ); glFinish(); SwapBuffers(wglGetCurrentDC()); } Теперь запустите View->Class Wizard и добавьте обработчик WM_CREATE,WM_DESTROY и WM_SIZE в класс CMFCView. Отредактируйте их следующим образом:
int CMFCView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CView::OnCreate(lpCreateStruct) == -1) return -1; pdc = new CClientDC(this); if(SetWindowPixelFormat(pdc->m_hDC)==FALSE) return -1; hGLRC = wglCreateContext(pdc->m_hDC); if(hGLRC == NULL) return -1; if(wglMakeCurrent(pdc->m_hDC, hGLRC)==FALSE) return -1; glEnable(GL_ALPHA_TEST); glEnable(GL_DEPTH_TEST); glEnable(GL_COLOR_MATERIAL); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 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); return 0; } void CMFCView::OnDestroy() { if(wglGetCurrentContext()!=NULL) wglMakeCurrent(NULL, NULL) ; if(hGLRC!=NULL) { wglDeleteContext(hGLRC); hGLRC = NULL; } delete pdc; CView::OnDestroy(); } void CMFCView::OnSize(UINT nType, int cx, int cy) { CView::OnSize(nType, cx, cy); glViewport(0,0,cx,cy); glMatrixMode( GL_PROJECTION ); glLoadIdentity(); glOrtho(-5,5, -5,5, 2,12); gluLookAt( 0,0,5, 0,0,0, 0,1,0 ); glMatrixMode( GL_MODELVIEW ); } Пояснения смотри в предыдущих разделах. В StdAfx.h включите заголовочные файлы, после строчки #include <afxext.h>
#include <gl/gl.h> #include <gl/glu.h> #include <gl/glaux.h> Запустите еще раз Class Wizard и добавьте функцию OnIdle в класс CMFCApp. Эта функция вызывается всякий раз, когда очередь сообщений окна пуста. Отредактируйте ее:
BOOL CMFCApp::OnIdle(LONG lCount) { ( (CMFCView*) ((CMainFrame*)m_pMainWnd)->GetActiveView() )->display(); return 1;//CWinApp::OnIdle(lCount); }
Исходный файл смотрите . Исполняемый файл .
Общие положения
В этой главе вы познакомитесь с самой главной частью программного кода - начальной инициализацией. На мой взгляд, это очень сложная тема. Я решил оставить ее на конец книги, когда вы уже будете знакомы с OpenGL. В противном случае, если бы я поместил эту главу в самом начале, я боюсь вы многого не поняли бы. Да и вообще, может не стали бы читать эту книжку.
Сначала, я расскажу в общих чертах, что нужно для инициализации библиотеки OpenGL. Далее мы рассмотрим несколько частных реализаций в среде Windows, Linux и межплатформенный вариант для Java. Для программирования графики в OpenGL вы должны иметь контекст воспроизведения. Это что-то типа контекста устройства в Windows или же магического адреса 0xA000 для графического или же 0xB800 для текстого режима MSDOS. Первое, что вы должны сделать, это подобрать и установить нужные вам параметры контекста воспроизведения. Потом создать сам контекст воспроизведения. И последнее, вы должны сделать его активным. Вообще говоря, вы можете иметь несколько контекстов воспроизведения, но активным может быть только один. Теперь вы можете уже что-нибудь рисовать. Но, возможно, вам не подходят настройки по умолчанию, которые предлагает OpenGL. Так что, придется еще немного потрудиться. Нужно сделать две вещи. Первое - это разрешить нужные вам опции с помощью glEnable и, здесь же, настроить параметры ламп, если вы используете освещение. Второе - надо обрабатывать сообщение об изменениях размера вашего окна. Тут вы указываете ту часть окна, где у вас будет располагаться контекст OpenGL. До этой главы у нас во всех примерах размер нашего окна и окна OpenGL совпадали, но, вообще говоря, это необязательно. Вывод OpenGL может занимать только часть окна, а в остальной части вы можете разместить свои компоненты: кнопки, поля ввода и т.п. Далее, вы должны указать тип проекции: перпективная или параллельная. В перспективной проекции две параллельных прямых будут сходиться вдалеке. В параллельной же проекции они всегда будут оставаться параллельными. И последнее, вы устанавливаете точку, где находится ваш глаз; точку, куда вы смотрите, и вектор, который принимается за направление вверх. У меня этот вектор во всех примерах - (0,1,0), т.е. ось Y направлена вверх.
Переносим игру Arcanoid"
Перенесите во все, указанные здесь приложения, игру Arcanoid.
Windows-приложение - Win32 Application
Достоинством является непосредственное взаимодействие с WinAPI. Начальная инициализация несколько усложняется, но зато вы имеете полноценное windows-приложение. Такой тип приложения подходит для написания серьезных больших программ. Кто-нибудь, конечно, скажет, что приложение непереносимо. Вам нужно написать работающее приложение для windows, а не неработающее, но переносимое приложение.
По поводу переносимости, хочу заметить следующее. В стандарте по языку Си сказано, что код на языке Си может быть платформенно независимым и платформенно зависимым. Из этого следует, что для обеспечения переносимости большой программы, вам придется делать несколько вариантов и затачивать ее под конкретные платформы. Код, относящийся к OpenGL, практически переносим. Непереносима только начальная инициализация. Конечно, вы можете попробовать Java-приложение, но тут возникают свои сложности. Так что, выбор за вами.
Создайте проект Win32 Application. Инструкции смотри в предыдущем разделе. Только имена дайте win и win.c. Теперь будем писать файл win.c. Внесите комментарии, заголовочные файлы и функции display и resize, см. предыдущий раздел. Из функций display и resize уберите слово CALLBACK. А в функции display замениете auxSwapBuffers() на
glFinish(); SwapBuffers(wglGetCurrentDC());
После включения заголовочных файлов объявите следующие глобальные переменные.
HWND hWnd; HGLRC hGLRC; HDC hDC;
Теперь вставьте код функции, которая устанавливает параметры контекста воспроизведения OpenGL.
int SetWindowPixelFormat() { int m_GLPixelIndex; PIXELFORMATDESCRIPTOR pfd; pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR); pfd.nVersion = 1; pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; pfd.iPixelType = PFD_TYPE_RGBA; pfd.cColorBits = 32; pfd.cRedBits = 8; pfd.cRedShift = 16; pfd.cGreenBits = 8; pfd.cGreenShift = 8; pfd.cBlueBits = 8; pfd.cBlueShift = 0; pfd.cAlphaBits = 0; pfd.cAlphaShift = 0; pfd.cAccumBits = 64; pfd.cAccumRedBits = 16; pfd.cAccumGreenBits = 16; pfd.cAccumBlueBits = 16; pfd.cAccumAlphaBits = 0; pfd.cDepthBits = 32; pfd.cStencilBits = 8; pfd.cAuxBuffers = 0; pfd.iLayerType = PFD_MAIN_PLANE; pfd.bReserved = 0; pfd.dwLayerMask = 0; pfd.dwVisibleMask = 0; pfd.dwDamageMask = 0; m_GLPixelIndex = ChoosePixelFormat( hDC, &pfd); if(m_GLPixelIndex==0) // Let's choose a default index. { m_GLPixelIndex = 1; if(DescribePixelFormat(hDC,m_GLPixelIndex,sizeof(PIXELFORMATDESCRIPTOR),&pfd)==0) return 0; } if (SetPixelFormat( hDC, m_GLPixelIndex, &pfd)==FALSE) return 0; return 1; }
Информацию о структуре PIXELFORMATDESCRIPTOR смотрите в справочнике. Я пользуюсь MSDN. Сейчас MSDN входит в MS Developer Studio. Редактировать параметры этой структуры вам вряд ли придется. А если придется, то я не смогу тут описать все. Перевести справочник я, конечно, могу, но это вам вряд ли поможет. Книга не предназначена для этого. Здесь рассматриваются конкретные примеры и упражнения.
Теперь напишем функцию обработки сообщений нашего окна.
LRESULT CALLBACK WindowFunc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam) { float pos[4] = {3,3,3,1}; float dir[3] = {-1,-1,-1}; PAINTSTRUCT ps; switch(msg) { // сообщение WM_CREATE приходит // один раз при создании окна case WM_CREATE: // получаем контекст устройства нашего окна hDC = GetDC(hWnd); // устанавливаем параметры контекста воспроизведения OpenGL SetWindowPixelFormat(); // создаем контекст воспроизведения OpenGL hGLRC = wglCreateContext(hDC); // делаем его текущим wglMakeCurrent(hDC, hGLRC); // далее см. предыдущий раздел glEnable(GL_ALPHA_TEST); glEnable(GL_DEPTH_TEST); glEnable(GL_COLOR_MATERIAL); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glLightfv(GL_LIGHT0, GL_POSITION, pos); glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, dir); break; // это сообщение приходит при уничтожении окна case WM_DESTROY: // удаляем созданный выше // контекст воспроизведения OpenGL if (hGLRC) { wglMakeCurrent(NULL, NULL); wglDeleteContext(hGLRC); } // освобождаем контекст устройства нашего окна ReleaseDC(hWnd, hDC); PostQuitMessage(0); break; // это сообщение приходит всякий раз, // когда нужно перерисовать окно case WM_PAINT: BeginPaint(hWnd, &ps); display(); EndPaint(hWnd, &ps); break; case WM_SIZE: resize( LOWORD(lParam), HIWORD(lParam) ); break; default: return DefWindowProc(hWnd,msg,wParam,lParam); } return 0; }
И последнее, осталось написать функцию WinMain.
int WINAPI WinMain(HINSTANCE hThisInst, HINSTANCE hPrevInst, LPSTR str,int nWinMode) { MSG msg; WNDCLASS wcl; wcl.hInstance=hThisInst; wcl.lpszClassName = "OpenGLWinClass"; wcl.lpfnWndProc = WindowFunc; wcl.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW; wcl.hIcon = NULL; wcl.hCursor = LoadCursor(NULL,IDC_ARROW); wcl.lpszMenuName = NULL; wcl.cbClsExtra = 0; wcl.cbWndExtra = 0; wcl.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); RegisterClass(&wcl); hWnd = CreateWindow( "OpenGLWinClass", "Win API Template", WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, 200, 150, 400, 420, HWND_DESKTOP, NULL, hThisInst, NULL); ShowWindow(hWnd,nWinMode); UpdateWindow(hWnd); while(1) { while( PeekMessage(&msg,NULL,0,0,PM_NOREMOVE) ) if(GetMessage(&msg,NULL,0,0)) { TranslateMessage(&msg); DispatchMessage(&msg); } else return 0; display(); } return 0; }
OpenGL требует свойства WS_CLIPCHILDREN и WS_CLIPSIBLINGS для окна в Windows. Поэтому были добавлены эти свойства при создании окна в функцию Createwindow. Также обратите внимание, что функция display вызывается в бесконечном цикле. Она вызывается, когда в очереди сообщений окна нет ничего. Эта же функция вызывается, когда нужно отрисовать окно заново - обработчик WM_PAINT.
Исходный файл смотрите . Исполняемый файл .
Вращение сцены
Когда построена сцена, наложены текстуры, включены источники света, присутствует анимация, кажеться, что больше и желать нечего. Однако, вы наблюдаете сцену, все время из одной точки. Вы не видете ее с обратной стороны. Было бы интересно иметь возможность управлять положением камеры или вращать всю сцену целиком. На производительности это никак не скажется, т.к. каждый кадр рассчитывается заново. Для OpenGL все равно, где находится камера или как повернута сцена.
Исходный файл смотрите . Исполняемый файл .
Appendix A Где взять OpenGL и другое ПО
Здесь я перечислю необходимый инструментарий для работы с книгой, и где его можно раздобыть. По минимуму вам нужен сишный компилятор и библиотека OpenGL. Основная масса примеров рассматриваются с привязкой MSVisualC++6.0, однако вы можете использовать любой другой, например фирмы Inprise, в прошлом Borland. Все примеры переносимы на другие платформы, где имеется Auxiliary Library - заголовочный файл glaux.h и линковочный модуль в Windows glaux.lib, в другой ОС он может называться по-другому. Для работы с этой книгой я рекомендую вам все же использовать MSVisualC++6.0. В книге используется OpenGL от Microsoft. Также существует реализация OpenGL от Silicon Graphics. Эта библиотека работает на 15-20% быстрее, чем фирмы Microsoft. Неудобство ее использования состоит в том, что вам придется распространять ее вместе со своими приложениями, в то время как библиотека корпорации Microsoft входит в поставку Windows и поэтому имеется на всех компьютерах.
Компилятор языка Си, как вы понимаете, покупается у соответсвующего производителя. Исходный текст библиотеки GLAUX вы найдете в MSDN(Platform SDK->Reference-> Code Samples->Graphica->OpenGL->GLAUX). Ниже перечислено программное обеспечение, которое можно скопировать в Интернет:
OpenGL от Silicon Graphics | |
Magician(OpenGL for Java) | |
У меня на сервере имеется дистрибутив именно, той версии, которая рассматривается в книге, а то боюсь, они что-нибудь новое выпустят и оно будет не совместимо со старой версией. В дистрибутив входит программка для проверки правельности установки. Ее можно запустить из windows нажав кнопку Start->Programms->Magician->Test_Magician или Пуск->Программы->Magician->Test_Magician | |
Исходные тексты программ моей книги | |
Дистрибутив библиотека GLUT для работы с Microsoft OpenGL(opengl32.dll, glu32.dll) | |
Дистрибутив библиотека GLUT для работы с Silicon Graphics OpenGL(opengl.dll, glu.dll) | |
Исходный код библиотеки GLUT | |
Дистрибутив библиотеки GLAUX с исправленными мной ошибками | |
Исходный код библиотеки GLAUX с исправленными мной ошибками | |
Дистрибутив библиотеки GLAUX без исправлений | |
Исходный код библиотеки GLAUX без исправлений | |
Первоисточник книги и поддержка | . |
br> Книга состоит из двух частей. Первая часть содержит основные сведения, вторая еще не написана. В электронном виде книга распростроняется бесплатно. Все права на данную книгу принадлежат мне(Игорю Тарасову). Никакая часть книги не может продаваться и использоваться для получения коммерческой выгоды без моего согласия. Вы можете свободно копировать и распространять электронную версию книги и программы, но только в том составе, который находится на моем сервере http://itsoft.miem.edu.ru, с указанием и моих авторских прав. В мае 2000г выходит бумажный вариант, в нем будут дополнительная информация не опубликованная в электронном варианте. Рекомендую вам зарегистрировать у нас на сервере, чтобы я вас своевременно проинформировал об исправлениях и новых версиях. Книга постоянно пополняется новыми материалами. Я оставляю за собой право сократить электронную версию книги. Свои замечание и отзывы направляйте по адресу itarasov@rtuis.miem.edu.ru или опубликуйте их в гостевой книге или форуме.
Bibliography
[1] Б.Керниган, Д.Ритчи - "Язык программирования Си" [2] Герберт Шилдт - "Программирование на Си и С++ в Windows95" [3] Дэвид Круглински - "Основы Visual C++" [4] "Справочник по Java" [5] Бъерн Страуструп - "Язык программирования С++" [6] Гради Буч - "Объектно-ориентированный анализ и проектирование"
Глоссарий
auxDIBImageLoad, 35 auxIdleFunc, 56 auxInitDisplayMode, 56 auxInitPosition, 56 auxInitWindow, 56 auxKeyFunc, 18 auxMainLoop, 56 auxMouseFunc, 18 auxReshapeFunc, 56 auxSolidBox, 9 auxSolidCone, 9 auxSolidCube, 9 auxSolidCylinder, 9 auxSolidIcosahedron, 9 auxSolidOctahedron, 9 auxSolidSphere, 9 auxSolidTeapot, 9 auxSolidTorus, 9 auxWireBox, 9 auxWireCone, 9 auxWireCube, 9 auxWireCylinder, 9 auxWireIcosahedron, 9 auxWireOctahedron, 9 auxWireTeapot, 9 auxWireTorus, 9 GL_ALPHA_TEST, 30 GL_AMBIENT, 50 GL_AMBIENT_AND_DIFFUSE, 50 GL_BACK, 25, 50 GL_BLEND, 30 GL_CLAMP, 43 GL_CLIP_PLANE, 32 GL_DECAL, 42 GL_DIFFUSE, 50 GL_EMISSION, 50 GL_FILL, 25 GL_FLAT, 29 GL_FRONT, 25, 50 GL_FRONT_AND_BACK, 25, 50 GL_LIGHT_MODEL_TWO_SIDE, 51 GL_LIGHTING, 49 GL_LINE, 25 GL_LINE_LOOP, 23 GL_LINE_STIPPLE, 23 GL_LINE_STRIP, 23 GL_LINEAR, 41 GL_LINES, 23 GL_MODELVIEW, 59 GL_MODULATE, 42 GL_NEAREST, 41 GL_POINT, 25 GL_POINT_SMOOTH, 22 GL_POINTS, 21 GL_POLYGON, 26 GL_POLYGON_STIPPLE, 33 GL_PROJECTION, 59 GL_QUAD_STRIP, 26 GL_QUADS, 26 GL_REPEATE, 43 GL_SHININESS, 50 GL_SMOOTH, 29 GL_SPECULAR, 50 GL_TEXTURE_1D, 39 GL_TEXTURE_2D, 39 GL_TRIANGLE_FAN, 25 GL_TRIANGLE_STRIP, 25 GL_TRIANGLES, 25 glBegin, 21 glBindTexture, 39 glClipPlane, 32 glColor, 8 glCopyPixels, 36 glDrawPixels, 36 glEnd, 21 glFrustum, 58 glGenTextures, 39 glLightModel, 49 glLineStipple, 23 glLineWidth, 16, 23 glLoadIndentity, 58 glMaterialfv, 50 glMatrixMode, 58 glOrtho, 58 glPixelStore, 36 glPixelZoom, 36 glPointSize, 21 glPolygonMode, 25 glPolygonStipple, 33 glPopMatrix, 10 glPushMatrix, 10 glRasterPos2d, 36 glRotate, 10, 12 glShadeModel, 29 glTexCoord, 44 glTexEnv, 42 glTexImage2D, 40 glTexParameter, 41 glTranslate, 10 GLU_FILL, 28 GLU_LINE, 28 GLU_POINT, 28 gluBuild2DMipmaps, 40 gluCylinder, 27 gluDeleteQuadric, 28 gluDisk, 27 gluLookAt, 58 gluNewQuadric, 28 gluPartialDisk, 27 gluQuadricDrawStyle, 28 GLUquadricObj, 27 gluQuadricTexture, 45 gluScaleImage, 36 gluSphere, 27 glVertex, 21 glViewport, 58