Растровые шрифты и ортогональная проекция
Общее назначение использования растровых шрифтов - это представление информации пользователю (не забываем, что они 2D). Например, мы хотим показать у запущенного приложения количество кадров в секунду. Эта информация должна оставаться в том же положении на экране, даже когда пользователь перемещает камеру по сцене. Кроме того, легче для вычисления позиции использовать 2D-ортогональную проекцию, а не перспективную проекцию, так как мы можем определить позицию в пикселях.
Принципиальная схема отрисовки, это нарисовать мир, так как мы привыкли это делать, с перспективной проекции, а затем перейти к ортогональной проекции и вывести текст. После этого, последним шагом будет восстановление оригинального взгляда так, что-бы каждый следующий кадр отображался правильно.
Представляем шаблон функции визуализации для достижения этого эффекта:
void renderScene() { // делать все, что нужно, чтобы сделать мир, как обычно, setOrthographicProjection(); //новая функция 1 glPushMatrix(); glLoadIdentity(); renderBitmapString(5,30,GLUT_BITMAP_HELVETICA_18,"Informatika.TNU"); glPopMatrix(); restorePerspectiveProjection(); //новая функция 1 glutSwapBuffers(); }
Выше представлены две новые функции , setOrthograpicProjection и restorePerspectiveProjection. Первая функция начинается с изменения матрицы GL_PROJECTION, и это означает, что мы работаем на камере. Впоследствии мы сохраним предыдущие настройки, которые в этом случае могут обратиться к перспективной проекции, определенные в другом месте. Затем мы сбрасываем матрицу с glLoadIdentity(), и определяем ортогональную проекцию с использованием gluOrtho.
Аргументы в пользу этой функции - это указание области в осях "x" и "y" оси. После преобразования нужно перевернуть ось "y", то есть положительное направление будет смотреть вниз, и отсчитывать начало координат до верхнего левого угла. Это значительно облегчает написание текста в координатах экрана.
Переменные w и h мы вычисляем в другом месте (см. ChangeSize функции в исходном коде).
void setOrthographicProjection() { //переключения режима проецирования glMatrixMode(GL_PROJECTION); //Сохраняем предыдущую матрицу, которая содержит //параметры перспективной проекции glPushMatrix(); //обнуляем матрицу glLoadIdentity(); //устанавливаем 2D ортографическую проекцию gluOrtho2D(0, w, 0, h); //перевернём ось y, положительное направление вниз glScalef(1, -1, 1); // Движение происходит из левого нижнего угла // В верхний левый угол glTranslatef(0, -h, 0); // возврата в режим обзора модели glMatrixMode(GL_MODELVIEW); }
Более быстрый способ выполнения ортогональной проекции заключается в следующем. Идея заключается в том, чтобы установить проекции таким образом, что масштабирования и переворачивания не требуется.
void setOrthographicProjection() { //переключения режима проецирования glMatrixMode(GL_PROJECTION); //Сохраняем предыдущую матрицу, которая содержит //параметры перспективной проекции glPushMatrix(); //обнуляем матрицу glLoadIdentity(); //устанавливаем 2D ортогональную проекцию gluOrtho2D(0, w, h, 0); // возврата в режим обзора модели glMatrixMode(GL_MODELVIEW); }
Это несложная функция. Так как мы сохранили настройки перспективной проекции прежде, чем перешли к ортогональной проекции, все, что мы должны сделать, это изменить матрицу проекции на GL_PROJECTION, и потом установить её настройки, ну а напоследок изменить матрицу проекции снова на на режим GL_MODELVIEW.
void restorePerspectiveProjection() { glMatrixMode(GL_PROJECTION); //восстановить предыдущую матрицу проекции glPopMatrix(); //вернуться в режим модели glMatrixMode(GL_MODELVIEW); }
Функция renderBitmapString, из первого примера кода данного урока, будет генерировать символы непрерывно, без дополнительного интервала, за исключением случаев, когда пробел появляется в тексте. Для того, чтобы добавить дополнительное пространство мы должны отслеживать, где сейчас текущая позиция растра, при этом добавляя дополнительный интервал по оси "x". Существуют по меньшей мере два различных подхода к отслеживанию положения растра, первый вычисляет текущее положения после нанесения растровое изображение. Второй вариант предполагает спросить ядро OpenGL, где сейчас текущая позиция растра.
Первый подход требует, чтобы мы знали размеры символа. В то время как максимальная высота всегда постоянна для конкретного шрифта, ширина может варьироваться в некоторых шрифтах. К счастью GLUT предоставляет функцию, которая возвращает ширину символа. Функция glutBitmapWidth и её синтаксис выглядят следующим образом:
int glutBitmapWidth(void *font, int character);
Параметры:
*font – один из предварительно определены шрифтов в GLUT.
character – символ, ширину которого мы хотим знать.
Так, например, если мы хотим получить функцию, которая записывает строку с определенным количеством пикселей между символами можно записать так:
void renderSpacedBitmapString( float x, float y, int spacing, void *font, char *string) { char *c; int x1=x; for (c=string; *c != '\0'; c++) { glRasterPos2f(x1,y); glutBitmapCharacter(font, *c); x1 = x1 + glutBitmapWidth(font,*c) + spacing; } }
Если мы хотим нарисовать вертикальный текст мы можем сделать следующим образом:
void renderVerticalBitmapString( float x, float y, int bitmapHeight, void *font, char *string) { char *c; int i; for (c=string,i=0; *c != '\0'; i++,c++) { glRasterPos2f(x, y+bitmapHeight*i); glutBitmapCharacter(font, *c); } }
Переменную bitmapHeight можно легко вычислить, потому что мы знаем, что максимальная высота каждого шрифта, это последняя цифра в имени шрифта. Например, GLUT_BITMAP_TIMES_ROMAN_10 - 10 пикселей в высоту.
GLUT имеет еще одну функцию для растровых шрифтов, это glutBitMapLength и эта функция вычисляет длину строки в пикселях. Возвращаемое значение этой функции является сумма всех ширины всех символов в строке. Здесь идет синтаксис:
int glutBitmapLength(void *font, char *string);
*font - один из заранее определенных шрифтов в GLUT.
*string - имя строки, которую мы хотим знать длину в пикселях
Линейные шрифты
Линейный шрифт является шрифтом построенном по кривой. В отличие от растровых шрифтов, эти шрифты ведут себя как любой другой 3D объект, т.е. символы можно вращать, масштабировать, и перемещать.
В этом разделе мы представим функции GLUT поставить некоторые инсульта текст на экране. Нам для этого нужна одна функция glutStrokeCharacter. Синтаксис выглядит следующим образом:
void glutStrokeCharacter(void *font, int character);
Параметры:
*font - имя используемого шрифта.
character - то, что нужно создать, слово, символ, число, и т.д.
Шрифт опции:
- GLUT_STROKE_ROMAN;
- GLUT_STROKE_MONO_ROMAN (шрифт фиксированной ширины: 104,76 единиц в ширину).
Следующие строки текста пример вызова функции glutStrokeCharacter для вывода одного символа в текущей (локальной) системе координат:
glutStrokeCharacter(GLUT_STROKE_ROMAN,'3');
В отличие от растровых шрифтов, место для визуализации траектории кривой шрифта указано так же, как и для любого графического примитива, т.е. с использованием перемещения, поворотов и масштабирования.
Следующая функция создаёт текст, начиная с указанной позиции в локальных координатах пространства:
void renderStrokeFontString( float x, float y, float z, void *font, char *string) { char *c; glPushMatrix(); glTranslatef(x, y,z); for (c=string; *c != '\0'; c++) { glutStrokeCharacter(font, *c); } glPopMatrix(); }
Примечание: GLUT использует линии для отрисовки линейных шрифтов, поэтому мы можем указать ширину линии с функцией glLineWidth. Эта функция принимает параметр, задающий ширину линии шрифта.
Что касается растровых шрифтов, GLUT предоставляет функцию, которая возвращает ширину символа. Функция glutStrokeWidth и её синтаксис выглядят следующим образом:
int glutStrokeWidth(void *font, int character);
Параметры:
*font - один из заранее определены шрифты в GLUT, см. выше.
character - символ, ширину которого мы хотим знать.
Как вывести количество кадров в секунду на экран?
Какая реальная скорость у вашего приложения? Иногда мы вносим небольшие изменения, и мы не можем быть уверены в том, что изменения не снизили производительность, т.е. как они повлияли на количество отображаемых кадров в секунду. Сейчас мы увидим, как мы можем использовать GLUT для подсчета количества кадров в секунду.
GLUT предоставляет функцию, которая позволяет запросить многие функции системы, одной из которых является количество миллисекунд от вызова glutInit. Функция glutGet и синтаксис выглядит следующим образом:
int glutGet(GLenum state);
Параметры:
state – определяет нужное нам значение.
Эта функция может быть использована для многих целей, например получения координат окна или получить глубину буфера в OpenGL. В этом разделе мы будем использовать его, чтобы получить число миллисекунд от вызова glutInit, использующий аргумент состояния GLUT_ELAPSED_TIME.
int time; ... time = glutGet(GLUT_ELAPSED_TIME);
Хорошо, теперь мы собираемся использовать эту функцию для вычисления количество выводимых кадров в секунду нашего приложения. Частота кадров от кадра к кадру, то есть не все кадры, занимать то же время, чтобы сделать, потому что наше приложение не одинок.Операционная система берет свое, и камера может быть перемещение тем самым изменяя то, что оказывались. Поэтому мы собираемся, чтобы избежать вычисления частоты кадров в каждом кадре, а вместо этого мы собираемся вычислить его примерно раз в секунду. Это также обеспечивает более точный рисунок, так как его среднее значение.
Мы собираемся объявить три переменные: frame, time, and timebase, где timebase и frame инициализируются нулем.
int frame=0,time,timebase=0;
Смысл этих переменных:
frame – количество кадров в секунду
time – текущее число миллисекунд
timebase – время, когда мы в последний раз вычислили частоту кадров.
Следующий фрагмент кода, помещенные внутрь зарегистрированной функции, будет выполнять работу (см. ниже подробное описание):
... frame++; time=glutGet(GLUT_ELAPSED_TIME); if (time - timebase > 1000) { fps = frame*1000.0/(time-timebase)); timebase = time; frame = 0; } ...
Начнем с увеличением числа кадров, т.е. с увеличения переменной frame. После этого мы получим текущее время в переменную time. Затем мы сравним его с timebase, чтобы проверить, сколько времени прошло, то есть, если разница между time и timebase превышает 1000 миллисекунд. Если это не так, то мы пропускаем часть вычислений . Однако, когда разница больше чем одна секунда, мы сделаем все вычисления.
Разница между time и timebase дает нам число миллисекунд, прошедших с момента нашего последнего вычисления количества кадров в секунду. Разделив 1000 на количество миллисекунд даёт нам обратную величину количеству секунд, прошедших. Также надо умножить это значение на количество кадров, прошедших с момента последнего вычисления частоты кадров, и мы получаем количество кадров в секунду. Наконец мы сбрасываем timebase к текущему количеству миллисекунд, и frame к нулю.
Обратите внимание, что при запуске приложения timebase равна нулю, т.е. придется подождать одну секунду, чтобы получить первое значение. Это первые несколько значений, однако, могу вводить в заблуждение, потому что они включают в себя время, необходимое для инициализации окна. Если вы сделаете несколько тестов, то вы увидите, что эта величина значительно ниже, чем фактическая частота кадров.
Если вы хотите вывести количество кадров в секунду вы можете использовать следующий фрагмент кода:
... frame++; time=glutGet(GLUT_ELAPSED_TIME); if (time - timebase > 1000) { sprintf(s,"FPS:%4.2f", frame*1000.0/(time-timebase)); timebase = time; frame = 0; } glColor3f(0.0f,1.0f,1.0f); glPushMatrix(); glLoadIdentity(); setOrthographicProjection(); renderBitmapString(30,35,(void *)font,s); glPopMatrix(); restorePerspectiveProjection(); ...
Итоговый код. Мы должны получить линейный текст над снеговиками и счётчик FPS на экране.
#include <stdio.h> #include <stdlib.h> #include <math.h> #include <glut.h> // угол поворота камеры float angle=0.0; // координаты вектора направления движения камеры float lx=0.0f, lz=-1.0f; // XZ позиция камеры float x=0.0f, z=5.0f; //Ключи статуса камеры. Переменные инициализируются нулевыми значениями //когда клавиши не нажаты float deltaAngle = 0.0f; float deltaMove = 0; int xOrigin = -1; // Определения констант для меню #define RED 1 #define GREEN 2 #define BLUE 3 #define ORANGE 4 #define FILL 1 #define LINE 2 // идентификаторы меню int fillMenu, fontMenu, mainMenu, colorMenu; //цвет носа float red = 1.0f, blue=0.5f, green=0.5f; //размер снеговика float scale = 1.0f; // menu статус int menuFlag = 0; // шрифт по умолчанию void *font = GLUT_STROKE_ROMAN; // высота и ширина окна int h,w; // переменные для вычисления количества кадров в секунду int frame; long time, timebase; char s[50]; void changeSize(int ww, int hh) { h = hh; w = ww; // предотвращение деления на ноль if (h == 0) h = 1; float ratio = w * 1.0 / h; // используем матрицу проекции glMatrixMode(GL_PROJECTION); // обнуляем матрицу glLoadIdentity(); // установить параметры вьюпорта glViewport(0, 0, w, h); // установить корректную перспективу gluPerspective(45.0f, ratio, 0.1f, 100.0f); // вернуться к матрице проекции glMatrixMode(GL_MODELVIEW); } void drawSnowMan() { glScalef(scale, scale, scale); glColor3f(1.0f, 1.0f, 1.0f); // тело снеговика glTranslatef(0.0f ,0.75f, 0.0f); glutSolidSphere(0.75f,20,20); // голова снеговика glTranslatef(0.0f, 1.0f, 0.0f); glutSolidSphere(0.25f,20,20); // глаза снеговика glPushMatrix(); glColor3f(0.0f,0.0f,0.0f); glTranslatef(0.05f, 0.10f, 0.18f); glutSolidSphere(0.05f,10,10); glTranslatef(-0.1f, 0.0f, 0.0f); glutSolidSphere(0.05f,10,10); glPopMatrix(); // нос снеговика glColor3f(red, green, blue); glRotatef(0.0f,1.0f, 0.0f, 0.0f); glutSolidCone(0.08f,0.5f,10,2); glColor3f(1.0f, 1.0f, 1.0f); } void renderBitmapString( float x, float y, float z, void *font, char *string) { char *c; glRasterPos3f(x, y,z); for (c=string; *c != '\0'; c++) { glutBitmapCharacter(font, *c); } } void renderStrokeFontString( float x, float y, float z, void *font, char *string) { char *c; glPushMatrix(); glTranslatef(x, y,z); glScalef(0.002f, 0.002f, 0.002f); for (c=string; *c != '\0'; c++) { glutStrokeCharacter(font, *c); } glPopMatrix(); } void restorePerspectiveProjection() { glMatrixMode(GL_PROJECTION); //восстановить предыдущую матрицу проекции glPopMatrix(); //вернуться в режим модели glMatrixMode(GL_MODELVIEW); } void setOrthographicProjection() { //выбрать режим проекции glMatrixMode(GL_PROJECTION); //Сохраняем предыдущую матрицу, которая содерж //параметры перспективной проекции glPushMatrix(); //обнуляем матрицу glLoadIdentity(); //устанавливаем 2D ортогональную проекцию gluOrtho2D(0, w, h, 0); //выбираем режим обзора модели glMatrixMode(GL_MODELVIEW); } void computePos(float deltaMove) { x += deltaMove * lx * 0.1f; z += deltaMove * lz * 0.1f; } void renderScene(void) { if (deltaMove) computePos(deltaMove); //очистить буфер цвета и глубины glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // обнулить трансформацию glLoadIdentity(); // установить камеру gluLookAt( x, 1.0f, z, x+lx, 1.0f, z+lz, 0.0f, 1.0f, 0.0f); // нарисуем "землю" glColor3f(0.9f, 0.9f, 0.9f); glBegin(GL_QUADS); glVertex3f(-100.0f, 0.0f, -100.0f); glVertex3f(-100.0f, 0.0f, 100.0f); glVertex3f( 100.0f, 0.0f, 100.0f); glVertex3f( 100.0f, 0.0f, -100.0f); glEnd(); // Нарисуем 64 снеговика char number[4]; for(int i = -4; i < 4; i++) for(int j=-4; j < 4; j++) { glPushMatrix(); glTranslatef(i*10.0f, 0.0f, j * 10.0f); drawSnowMan(); sprintf(number,"%d",(i+3)*8+(j+4)); renderStrokeFontString(0.0f, 0.5f, 0.0f, (void *)font ,number); glPopMatrix(); } // Код для вычисления кадров в секунду frame++; time=glutGet(GLUT_ELAPSED_TIME); if (time - timebase > 1000) { sprintf(s,"Informatika - FPS:%4.2f", frame*1000.0/(time-timebase)); timebase = time; frame = 0; } //Код для отображения строки (кадров в секунду) с растровых шрифтов setOrthographicProjection(); glPushMatrix(); glLoadIdentity(); renderBitmapString(5,30,0,GLUT_BITMAP_HELVETICA_18,s); glPopMatrix(); restorePerspectiveProjection(); glutSwapBuffers(); } // ----------------------------------- // // клавиатура // // ----------------------------------- // void processNormalKeys(unsigned char key, int xx, int yy) { switch (key) { case 27: glutDestroyMenu(mainMenu); glutDestroyMenu(fillMenu); glutDestroyMenu(colorMenu); glutDestroyMenu(fontMenu); exit(0); break; } } void pressKey(int key, int xx, int yy) { switch (key) { case GLUT_KEY_UP : deltaMove = 0.5f; break; case GLUT_KEY_DOWN : deltaMove = -0.5f; break; } } void releaseKey(int key, int x, int y) { switch (key) { case GLUT_KEY_UP : case GLUT_KEY_DOWN : deltaMove = 0;break; } } // ----------------------------------- // // функции мыши // // ----------------------------------- // void mouseMove(int x, int y) { // только когда левая кнопка не активна if (xOrigin >= 0) { // обновить deltaAngle deltaAngle = (x - xOrigin) * 0.001f; // обновить направление камеры lx = sin(angle + deltaAngle); lz = -cos(angle + deltaAngle); } } void mouseButton(int button, int state, int x, int y) { // только начало движение, если левая кнопка мыши нажата if (button == GLUT_LEFT_BUTTON) { // когда кнопка отпущена if (state == GLUT_UP) { angle += deltaAngle; xOrigin = -1; } else {// state = GLUT_DOWN xOrigin = x; } } } // ------------------------------------// // меню // // ------------------------------------// void processMenuStatus(int status, int x, int y) { if (status == GLUT_MENU_IN_USE) menuFlag = 1; else menuFlag = 0; } void processMainMenu(int option) { //ничего здесь не делаем //все действия для подменю } void processFillMenu(int option) { switch (option) { case FILL: glPolygonMode(GL_FRONT, GL_FILL); break; case LINE: glPolygonMode(GL_FRONT, GL_LINE); break; } } void processFontMenu(int option) { switch (option) { case 1: font = GLUT_STROKE_ROMAN; break; case 2: font = GLUT_STROKE_MONO_ROMAN; break; } } void processColorMenu(int option) { switch (option) { case RED : red = 1.0f; green = 0.0f; blue = 0.0f; break; case GREEN : red = 0.0f; green = 1.0f; blue = 0.0f; break; case BLUE : red = 0.0f; green = 0.0f; blue = 1.0f; break; case ORANGE : red = 1.0f; green = 0.5f; blue = 0.5f; break; } } void createPopupMenus() { fontMenu = glutCreateMenu(processFontMenu); glutAddMenuEntry("STROKE_ROMAN",1 ); glutAddMenuEntry("STROKE_MONO_ROMAN",2 ); fillMenu = glutCreateMenu(processFillMenu); glutAddMenuEntry("Fill",FILL); glutAddMenuEntry("Line",LINE); colorMenu = glutCreateMenu(processColorMenu); glutAddMenuEntry("Red",RED); glutAddMenuEntry("Blue",BLUE); glutAddMenuEntry("Green",GREEN); glutAddMenuEntry("Orange",ORANGE); mainMenu = glutCreateMenu(processMainMenu); glutAddSubMenu("Polygon Mode", fillMenu); glutAddSubMenu("Color", colorMenu); glutAddSubMenu("Font",fontMenu); // прикрепить меню к правой кнопке glutAttachMenu(GLUT_RIGHT_BUTTON); //статус активности меню glutMenuStatusFunc(processMenuStatus); } // ------------------------------------ // // main() // // ----------------------------------- // int main(int argc, char **argv) { // инициализация Glut и создание окна glutInit(&argc, argv); glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA); glutInitWindowPosition(100,100); glutInitWindowSize(480,480); glutCreateWindow("Урок - 12"); //регистрация glutDisplayFunc(renderScene); glutReshapeFunc(changeSize); glutIdleFunc(renderScene); glutIgnoreKeyRepeat(1); glutKeyboardFunc(processNormalKeys); glutSpecialFunc(pressKey); glutSpecialUpFunc(releaseKey); glutMouseFunc(mouseButton); glutMotionFunc(mouseMove); //OpenGL инициализация glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); //инициализация меню createPopupMenus(); //главный цикл glutMainLoop(); return 1; }