Народный учебник по OpenGL

         

по OpenGL. Двухмерные шрифты из текстур


2D Texture Font

Этот урок написан NeHe & Giuseppe D'Agat.

Я знаю, что все устали от шрифтов. Те уроки, которые уже были рассмотрены ранее, не только показывали текст, но они отображали 3-х мерный текст, текстурированный текст, и могли быть привязаны к переменным. Но что будет, если вы перенесете свой проект на машину, которая не поддерживает Bitmap или Outline шрифты?

Благодаря Guiseppe D'Agata у нас есть еще один урок со шрифтами. Вы спросите, что же еще осталось? Если вы помните, в первом уроке про шрифты, я упоминал об использовании текстур для рисования букв на экран. Обычно, когда вы используете текстуры для рисования текста на экране, вы загружаете свою любимую программу рисования, выбираете шрифт и набираете букву или фразу, которую хотите отобразить на экране. Дальше вы сохраняете изображение и загружаете его в свою программу, как текстуру. Это не очень эффективно для программ, которые используют большое количество текста, или текст, который непрерывно меняется!

Эта программа использует только одну текстуру для отображения любого из 256 различных символов на экран. Имейте в виду, что каждый символ всего лишь 16 пикселов в ширину и 16 в высоту. Если взять стандартную текстуру 256*256, легко заметить, что в ней можно разместить только 16 символов поперек и получится 16 строк. Если нужно более детальное объяснение то: текстура 256 пикселов в ширину, а символ 16 пикселов в ширину. 256 делим на 16, получаем 16 :)

Итак. Давайте сделаем демонстрационную программу 2-х мерных шрифтов. Эта программа дополняет код из первого урока. В первой части программы мы включим библиотеки math и stdio. Математическая библиотека нужна, чтобы двигать буквы по экрану, используя синус и косинус, а библиотека stdio нужна, чтобы убедиться в том, что файлы картинок, которые мы загружаем действительно существуют, перед тем как мы попытаемся сделать из них текстуры.

#include <windows.h>        // Заголовочный файл для Windows

#include <math.h>           // Заголовочный файл для математической


                            // библиотеки Windows  (Добавлено)
#include <stdio.h>          // Заголовочный файл для стандартной библиотеки
                            //ввода/вывода (Добавлено)


#include <gl\gl.h>          // Заголовочный файл для библиотеки OpenGL32
#include <gl\glu.h>         // Заголовочный файл для библиотеки GLu32
#include <gl\glaux.h>       // Заголовочный файл для библиотеки GLaux
HDC             hDC=NULL;   // Приватный контекст устройства GDI
HGLRC           hRC=NULL;   // Постоянный контекст визуализации
HWND            hWnd=NULL;  // Сохраняет дескриптор окна
HINSTANCE       hInstance;  // Сохраняет экземпляр приложения
bool    keys[256];          // Массив для работы с клавиатурой
bool    active=TRUE;        // Флаг активации окна, по умолчанию = TRUE
bool    fullscreen=TRUE;    // Флаг полноэкранного вывода
Сейчас мы добавим переменную base указывающую на наш список отображения. Так же мы добавим texture[2], для хранения 2-х текстур, используемых для создания простого 3-х мерного объекта.
Добавим также переменную loop, которую будем использовать для циклов. И, наконец, cnt1 и cnt2, которые будут использоваться для движения текста по экрану и для вращения простого 3-х мерного объекта.
GLuint  base;       // Основной список отображения для шрифта
GLuint  texture[2]; // Место для текстуры нашего шрифта
GLuint  loop;       // Общая переменная для циклов
GLfloat cnt1;       // Первый счетчик для движения и раскрашивания текста
GLfloat cnt2;       // Второй счетчик для движения и раскрашивания текста
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);   // Объявление WndProc
Теперь код для загрузки текстуры. Он точно такой же, как в предыдущих уроках по текстурированию.
AUX_RGBImageRec *LoadBMP(char *Filename) // Загрузка изображения
{
       FILE *File=NULL;                  // Дескриптор файла
       if (!Filename)                    // Удостоверимся, что имя файла передано


       {
              return NULL;               // Если нет, возвратим NULL
       }
       File=fopen(Filename,"r");         // Проверка, существует ли файл
       if (File)                         // Существует?
       {
              fclose(File);              // Закрываем файл
              // Загружаем изображение и возвращаем указатель
             return auxDIBImageLoad(Filename);
       }
       return NULL;                      // Если загрузка не удалась, возвращаем NULL
}
Данный код тоже немного изменился, в отличие от кода в предыдущих уроках. Если вы не уверены в том, для чего каждая строка, вернитесь и просмотрите предыдущие примеры заново.
Отметим, что TextureImage[] будет хранить 2 записи о rgb изображении. Очень важно дважды проверить код, который работает с загрузкой и сохранением текстур. Одно неверное число может привести к зависанию!
 
int LoadGLTextures()                           // Загрузка и преобразование текстур
{
       int Status=FALSE;                       // Индикатор статуса
       AUX_RGBImageRec *TextureImage[2];       // Место хранения для текстур
Следующая строка самая важная. Если изменить 2 на любое другое число, точно возникнут проблемы. Проверьте дважды! Это число должно совпадать с тем, которое вы используете, когда определяете TextureImage[].
Две текстуры, которые мы загрузим, будут font.bmp (наш шрифт) и bumps.bmp. Вторая текстура может быть любой, какую вы захотите. Я не очень творческий человек, поэтому я решил воспользоваться простой текстурой.
memset(TextureImage,0,sizeof(void *)*2);          // Устанавливаем указатель в NULL
if ((TextureImage[0]=LoadBMP("Data/Font.bmp")) &&// Загружаем изображение шрифта (TextureImage[1]=LoadBMP("Data/Bumps.bmp")))     // Загружаем текстуру
       {
              Status=TRUE;                        // Устанавливаем статус в TRUE
Другая важная строка, на которую нужно посмотреть дважды. Я не могу сказать, сколько писем я получил от людей, спрашивавших "почему я вижу только одну текстуру, или почему моя текстура вся белая!?!". Обычно проблема в этой строке. Если заменить 2 на 1, будет создана только одна текстура, а вторая будет в виде белой текстуры. Если заменить 2 на 3, то программа может зависнуть!


Вы должны вызывать glGenTextures() один раз. После вызова glGenTexture, необходимо сгенерировать все ваши текстуры. Я видел людей, которые вставляют вызов glGenTextures() перед созданием каждой текстуры. Обычно они ссылаются на то, что новая текстура перезаписывает все уже созданные текстуры. Было бы неплохо, сначала решить, сколько текстур необходимо сделать, а затем вызвать один раз glGenTextures(), а потом создать все текстуры. Не хорошо помещать вызов glGenTextures() в цикл без причины.
 
glGenTextures(2, &texture[0]);            // Создание 2-х текстур
for (loop=0; loop<2; loop++)              // Цикл для всех текстур
{
            // Создание всех текстур
            glBindTexture(GL_TEXTURE_2D, texture[loop]);
            glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
            glTexImage2D(GL_TEXTURE_2D, 0, 3,
TextureImage[loop]->sizeX, TextureImage[loop]->sizeY, 0,
GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop]->data);
}
}
Следующие стоки кода проверяют, занимает ли загруженное нами rgb изображение для создания текстуры память. Если да, то высвобождаем ее. Заметьте, мы проверяем и освобождаем обе записи для изображений. Если мы используем три различные изображения для текстур, то необходимо проверить и освободить память из-под 3-х изображений.
for (loop=0; loop<2; loop++)
       {
       if (TextureImage[loop])                // Если текстура существует
              {
              if (TextureImage[loop]->data)   // Если изображение текстуры существует
              {
                    // Освобождаем память от изображения текстуры
                    free(TextureImage[loop]->data);
              }
              free(TextureImage[loop]);   // Освобождаем память от структуры изображения
              }
       }
return Status;                                // Возвращаем статус


}
Теперь создадим сам наш шрифт. Я пройду эту секцию с особо детальным описанием. Это не очень сложно, но там есть немного математики, которую нужно понять, а я знаю, что математика не всем нравится.
GLvoid BuildFont(GLvoid)                   // Создаем список отображения нашего шрифта
{
Следующие две переменные будем использовать для сохранения позиции каждой буквы внутри текстуры шрифта. cx будет содержать позицию в текстуре по горизонтали, а cy содержать позицию по вертикали.
 
       float       cx;                           // Содержит X координату символа
       float       cy;                           // Содержит Y координату символа
Дальше мы скажем OpenGL, что хотим 256 списков отображения. Переменная base будет указывать на положение первого списка отображения. Второй будет base+1, третий base+2, и т.д. Вторая строка кода ниже, выбирает нашу текстуру шрифта (texture[0]).
       base=glGenLists(256);                    // Создаем списки
       glBindTexture(GL_TEXTURE_2D, texture[0]);// Выбираем текстуру шрифта
Дальше мы начнем наш цикл. В цикле создадим 256 символов, сохраняя каждый символ в своем собственном списке отображения.
       for (loop=0; loop<256; loop++)           // Цикл по всем 256 спискам
       {
Первая строка ниже, может показаться загадочной. Символ % означает остаток от деления loop на 16. cx будет двигаться по текстуре шрифта слева направо. Позже вы заметите в коде, что мы вычитаем из 1 cy, чтобы двигаться сверху вниз, вместо того, чтобы двигаться снизу вверх. Символ % довольно трудно объяснить, но я попробую.
Все, о чем мы говорим, (loop % 16) /16.0f просто переводит результат в координаты текстуры. Поэтому, если loop было равно 16, cx будет равно остатку от деления 16 на 16, то есть 0. А cy равно 16/16, то есть 1. Поэтому мы двигаемся вниз на высоту одного символа, и совсем не двигаемся вправо. Теперь, если loop равно 17, cx будет равно 17/16, что равно 1.0625. Остаток .0625 или 1/16-ая. Это значит, что мы двигаемся на один символ вправо. cy все еще будет равно 1, потому что нам важны только знаки слева от десятичной запятой. 18/16 даст нам 2/16, двигая на 2 символа вправо, и все еще на один вниз. Если loop равно 32, cx будет опять 0, потому что остатка от деления нет, когда делим 32 на 16, но cy равно 2. Потому что число слева от десятичной запятой будет 2, двигая нас на 2 символа вниз от самого верха текстуры шрифта. Не так ли?


       cx=float(loop%16)/16.0f;        // X координата текущего символа
       cy=float(loop/16)/16.0f;        // Y координата текущего символа
Вау! Ок. Итак, теперь мы построим наш 2D шрифт, выбирая каждый символ из текстуры шрифта, в зависимости от значений cx и cy. В строках ниже мы добавим loop к значению base, если мы этого не сделаем, то каждая буква будет построена в первом дисплейном списке. Мы точно не хотим, чтобы это случилось, поэтому добавим loop к base и каждый следующий символ, который мы создаем, сохранится в следующем доступном списке отображения.
       glNewList(base+loop,GL_COMPILE); // Начинаем делать список
Теперь, когда мы выбрали список отображения, который мы построили, мы создадим символ. Этого мы добъемся, создавая четырехугольник, и затем текстурируя его одним символом из текстуры шрифта.
       glBegin(GL_QUADS);              // Используем четырехугольник, для каждого символа
cx и cy будут содержать очень маленькие значения от 0.0f до 1.0f. Оба они будут равны 0 в первой строчке кода ниже, а именно: glTexCoord2f(0.0f,1-0.0f-0.0625f). Помните, что 0.0625 это 1/16-ая нашей текстуры, или ширина/высота одного символа. Координаты текстуры ниже будут координатами левой нижней точки текстуры.
Заметьте, мы используем glVertex2i(x, y) вместо glVertex3f(x, y, z). Наш шрифт – это двумерный шрифт, поэтому нам не нужна координата z. Поскольку мы используем плоский экран (Ortho screen – ортографическая или параллельная проекция), нам не надо сдвигаться вглубь экрана. Мы должны сделать, чтобы нарисовать на плоском экране, это задать x и y координаты. Так как наш экран в пикселах от 0 до 639 и от 0 до 479, нам вообще не надо использовать плавающую точку или отрицательные значения :). Используя плоский экран, мы получаем (0, 0) в нижнем левом углу. (640, 480) будет в верхнем правом углу. 0 - левый край по оси x, 639 - правый край экрана по оси x. 0 – верхний край экрана по оси y и 479 – нижний край экрана на оси y. Проще говоря, мы избавились от отрицательных координат. Это тоже удобно для тех, кто не заботится о перспективе и предпочитает работать с пикселами больше, чем с экранными единицами.


                    glTexCoord2f(cx,1-cy-0.0625f);  // Точка в текстуре (Левая нижняя)
                    glVertex2i(0,0);                // Координаты вершины (Левая нижняя)
Следующая точка на текстуре будет 1/16-ая правее предыдущей точки (точнее ширина одного символа). Поэтому это будет нижняя правая точка текстуры.
                    // Точка на текстуре (Правая нижняя)
                    glTexCoord2f(cx+0.0625f,1-cy-0.0625f);
                    glVertex2i(16,0); // Координаты вершины (Правая нижняя)
 
Третья точка текстуры лежит в дальнем правом конце символа, но сдвинута вверх на 1/16-ую текстуры (точнее на высоту одного символа). Это будет верхняя правая точка отдельного символа.
                    glTexCoord2f(cx+0.0625f,1-cy);  // Точка текстуры (Верхняя правая)
                    glVertex2i(16,16);              // Координаты вершины (Верхняя правая)
Наконец, мы двигаемся влево, чтобы задать нашу последнюю точку в верхнем левом углу символа.
                    glTexCoord2f(cx,1-cy);  // Точка текстуры (Верхняя левая)
                    glVertex2i(0,16);       // Координаты вершины (Верхняя левая)
              glEnd();                      // Конец построения четырехугольника (Символа)
Наконец, мы перемещаемся на 10 пикселей вправо, двигаясь вправо по текстуре. Если мы не передвинемся, символы будут рисоваться поверх друг друга. Так как наш шрифт очень узкий, мы не будем двигаться на 16 пикселов вправо. Если мы это сделаем, то будет слишком большой пропуск между каждым символом. Двигаясь на 10 пикселей, мы уменьшаем расстояние между символами. 
       glTranslated(10,0,0);           // Двигаемся вправо от символа
       glEndList();                    // Заканчиваем создавать список отображения
}                                      // Цикл для создания всех 256 символов
}
Следующая секция кода такая же как мы делали в предыдущих уроках, для освобождения списка отображения, перед выходом из программы. Все 256 экранных списков, начиная от base, будут удалены. (Это хорошо!)


GLvoid KillFont(GLvoid)                // Удаляем шрифт из памяти
{
       glDeleteLists(base,256);        // Удаляем все 256 списков отображения
}
 
Следующая секция кода содержит все рисование. Все довольно ново, поэтому я постараюсь объяснить каждую строчку особенно детально. Одно маленькое замечание: можно добавить переменные для поддержки размеров, пропусков, и кучу проверок для восстановления настроек которые были до того, как мы решили их напечатать.
glPrint() имеет четыре параметра. Первый это координата x на экране (позиция слева на право). Следующая это y координата на экране (сверху вниз... 0 внизу, большие значения наверху). Затем нашу строку string (текст, который мы хотим напечатать), и, наконец, переменную set. Если посмотреть на картинку, которую сделал Giuseppe D'Agata, можно заметить, что там два разных набора символов. Первый набор - обычные символы,  а второй набор - наклонные. Если set = 0, то выбран первый набор. Если set = 1 или больше, то выбран второй набор символов.
 
GLvoid glPrint(GLint x, GLint y, char *string, int set) // Где печатать
{
 
Первое, что мы сделаем - это проверим, что set от 0 до 1. Если set больше 1, то присвоим ей значение 1.
 
       if (set>1)                     // Больше единицы?
       {
              set=1;                  // Сделаем Set равное единице
       }
 
Теперь выберем нашу текстуру со шрифтом. Мы делаем это только, если раньше была выбрана другая текстура, до того как мы решили печатать что-то на экране.
       glBindTexture(GL_TEXTURE_2D, texture[0]);       // Выбираем нашу текстуру шрифта
 
Теперь отключим проверку глубины. Причина того, почему я так делаю, в том, что смешивание работает приятнее. Если не отменить проверку глубины, то текст может проходить за каким-нибудь объектом, или смешивание может выглядеть неправильно. Если вы не хотите смешивать текст на экране (из-за смешивания, т.е прозрачности черный фон вокруг символов не виден) можете оставить проверку глубины.


       glDisable(GL_DEPTH_TEST);               // Отмена проверки глубины
Следующее несколько строк очень важны! Мы выбираем нашу матрицу проекции. Прямо после этого мы используем команду glPushMatrix(). glPushMatrix сохраняет текущую матрицу проекции. Похоже на кнопку "память" на калькуляторе.
       glMatrixMode(GL_PROJECTION);            // Выбираем матрицу проекции
       glPushMatrix();                         // Сохраняем матрицу проекции
Теперь, когда наша матрица сохранена, мы сбрасываем ее и устанавливаем плоский экран. Первое и третье число (0) задают нижний левый угол экрана. Мы можем сделать левую сторону экрана -640, если захотим, но зачем нам работать с отрицательными числами, если это не нужно. Второе и четвертое число задают верхний правый угол экрана. Неплохо установить эти значения равными текущему разрешению. Глубины нет, поэтому устанавливаем значения z в -1 и 1.
 
       glLoadIdentity();                       // Сбрасываем матрицу проекции
       glOrtho(0,640,0,480,-1,1);              // Устанавливаем плоский экран
Теперь выбираем нашу матрицу просмотра модели и сохраняем текущие установки, используя glPushMatrix(). Далее сбрасываем матрицу просмотра модели, так что можно работать, используя ортографическую проекцию.
       glMatrixMode(GL_MODELVIEW);             // Выбираем матрицу модели просмотра
       glPushMatrix();                         // Сохраняем матрицу модели просмотра
       glLoadIdentity();                       // Сбрасываем матрицу модели просмотра
Сохранив настройки перспективы и установив плоский экран, можно рисовать текст. Начнем с перемещения в позицию на экране, где мы хотим нарисовать текст. Используем glTranslated() вместо glTranslatef(), так как мы работаем с пикселами, поэтому точки с дробными значениями не имеют смысла. В конце концов, нельзя использовать половину пиксела :).
       glTranslated(x,y,0);                    // Позиция текста (0,0 - Нижняя левая)


 
Строка ниже выбирает, каким набором символов мы хотим воспользоваться. Если мы хотим использовать второй набор символов, то добавляем 128 к base (128 - половина 256 символов). Добавляя 128, мы пропускаем первые 128 символов.
       glListBase(base-32+(128*set));         // Выбираем набор символов (0 или 1)
 
Сейчас, все что осталось - это  нарисовать символы на экране. Делаем это так же как во всех других уроках со шрифтами. Используем glCallLists(). strlen(string) это длина строки (сколько символов мы хотим нарисовать), GL_BYTE означает то, что каждый символ представляется одним байтом (байт это любое значение от 0 до 255). Наконец, string содержит тот текст, который надо напечатать на экране.
 
       glCallLists(strlen(string),GL_BYTE,string); // Рисуем текст на экране
 
Все, что надо теперь сделать, это восстановить перспективу. Мы выбираем матрицу проектирования и используем glPopMatrix(), чтобы восстановить установки, сохраненные с помощью glPushMatrix(). Важно восстановить их в обратном порядке, в том в котором мы их сохраняли.
       glMatrixMode(GL_PROJECTION);            // Выбираем матрицу проекции
       glPopMatrix();                          // Восстанавливаем старую матрицу проекции
Теперь мы выбираем матрицу просмотра модели и делаем то же самое. Мы используем glPopMatrix(), чтобы восстановить нашу матрицу просмотра модели, на ту, которая была, прежде чем мы устанавливали плоский экран.
       glMatrixMode(GL_MODELVIEW);             // Выбираем матрицу просмотра модели
       glPopMatrix();                          // Восстанавливаем старую матрицу проекции
Наконец, разрешаем проверку глубины. Если мы не запрещали проверку глубины в коде раньше, то нам не нужна эта строка.
 
       glEnable(GL_DEPTH_TEST);                // Разрешаем тест глубины
}
 
В ReSizeGLScene() ничего менять не надо, так что переходим к InitGL().
 
int InitGL(GLvoid)                             // Все установки для OpenGL здесь


{
Переходим к коду построения текстур. Если построить текстуры не удалось по какой-либо причине, то возвращаем значение FALSE. Это позволит нашей программе узнать, что произошла ошибка, и программа изящно завершится.
       if (!LoadGLTextures()) // Переходим к загрузке текстуры
       {
              return FALSE;   // Если текстура не загрузилась - возвращаем FALSE
       }
 
Если ошибок не было, переходим к коду построения шрифта. Т.к. ничего не может случиться при построении шрифта, поэтому проверку ошибок не включаем.
       BuildFont();  // Создаем шрифт
Теперь делаем обычную настройку GL. Мы установим черный цвет фона для очистки, зададим значение глубины в 1.0. Выбираем режим проверки глубины вместе со смешиванием. Мы разрешаем сглаженное заполнение и, наконец, разрешаем 2-мерное текстурирование.
 
       glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Очищаем фон черным цветом
       glClearDepth(1.0);                    // Очистка и сброс буфера глубины
       glDepthFunc(GL_LEQUAL);               // Тип теста глубины
       glBlendFunc(GL_SRC_ALPHA,GL_ONE);     // Выбор типа смешивания
 
       glShadeModel(GL_SMOOTH);              // Сглаженное заполнение
       glEnable(GL_TEXTURE_2D);              // 2-мерное текстурирование
 
       return TRUE;                          // Инициализация прошла успешно
}
Следующая секция кода создает сцену. Мы рисуем сначала 3-х мерный объект и потом текст, поэтому текст будет поверх 3-х мерного объекта, вместо того, чтобы объект закрывал текст сверху. Причина, по которой я добавил 3-х мерный объект, заключается в том, чтобы показать, что перспективная и ортографическая проекции могут быть использованы одновременно.
int DrawGLScene(GLvoid)                             // Здесь мы рисуем все объекты
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экрана и буфера глубины
glLoadIdentity();                                   // Сброс матрицы просмотра модели


Мы выбрали нашу bumps.bmp текстуру, и сейчас можно построить маленький 3-х мерный объект. Мы сдвигаемся на 5 экранных единиц вглубь экрана, так чтобы можно было увидеть объект. Мы вращаем объект на 45 градусов вдоль оси z. Это повернет наш четырехугольник на 45 градусов по часовой стрелке, и он будет больше похож на ромб, чем на прямоугольник.
glBindTexture(GL_TEXTURE_2D, texture[1]);// Выбираем вторую текстуру
glTranslatef(0.0f,0.0f,-5.0f);           // Сдвигаемся на 5 единиц вглубь экрана
       glRotatef(45.0f,0.0f,0.0f,1.0f); // Поворачиваем на 45 градусов (по часовой стрелке)
После поворота на 45 градусов, мы вращаем объект вокруг осей x и y, с помощью переменной cnt1*30. Это заставляет наш объект вращаться вокруг своей оси, подобно алмазу.
       glRotatef(cnt1*30.0f,1.0f,1.0f,0.0f); // Вращение по X & Y на cnt1 (слева направо)
Отменяем смешивание (мы хотим, чтобы 3-х мерный объект был сплошным), и устанавливаем цвет на ярко белый. Затем рисуем один текстурированный четырехугольник.
       glDisable(GL_BLEND);            // Отменяем смешивание перед рисованием 3D
       glColor3f(1.0f,1.0f,1.0f);      // Ярко белый
       glBegin(GL_QUADS);              // Рисуем первый текстурированный прямоугольник
       glTexCoord2d(0.0f,0.0f);        // Первая точка на текстуре
              glVertex2f(-1.0f, 1.0f); // Первая вершина
              glTexCoord2d(1.0f,0.0f); // Вторая точка на текстуре
              glVertex2f( 1.0f, 1.0f); // Вторая вершина
              glTexCoord2d(1.0f,1.0f); // Третья точка на текстуре
              glVertex2f( 1.0f,-1.0f); // Третья вершина
              glTexCoord2d(0.0f,1.0f); // Четвертая точка на текстуре
              glVertex2f(-1.0f,-1.0f); // Четвертая вершина
 
       glEnd();                        // Заканчиваем рисование четырехугольника
Сразу, после того как мы нарисовали первый четырехугольник, мы поворачиваемся на 90 градусов по осям x и y. Затем рисуем другой четырехугольник. Второй четырехугольник проходит сквозь середину первого, в результате получается красивая фигура.


// Поворачиваемся по X и Y на 90 градусов (слева на право)
glRotatef(90.0f,1.0f,1.0f,0.0f);
glBegin(GL_QUADS);                 // Рисуем второй текстурированный четырехугольник
              glTexCoord2d(0.0f,0.0f); // Первая точка на текстуре
              glVertex2f(-1.0f, 1.0f); // Первая вершина
              glTexCoord2d(1.0f,0.0f); // Вторая точка на текстуре
              glVertex2f( 1.0f, 1.0f); // Вторая вершина
              glTexCoord2d(1.0f,1.0f); // Третья точка на текстуре
              glVertex2f( 1.0f,-1.0f); // Третья вершина
              glTexCoord2d(0.0f,1.0f); // Четвертая точка на текстуре
              glVertex2f(-1.0f,-1.0f); // Четвертая вершина
       glEnd();                        // Заканчиваем рисовать четырехугольник
После того как нарисованы четырехугольники, разрешаем смешивание и рисуем текст.
       glEnable(GL_BLEND);         // Разрешаем смешивание
       glLoadIdentity();           // Сбрасываем просмотр
 
Мы используем такой же код для раскрашивания, как в предыдущих примерах с текстом. Цвет меняется постепенно по мере движения текста по экрану.
       // Изменение цвета основывается на положении текста
       glColor3f(1.0f*float(cos(cnt1)),1.0f*float(sin(cnt2)),
              1.0f-0.5f*float(cos(cnt1+cnt2)));
Затем мы рисуем текст. Мы все еще используем glPrint(). Первый параметр - это координата x. Второй - координата y. Третий параметр ("NeHe") - текст, который надо написать на экране, и последний это набор символов (0 - обычный, 1 - наклонный).
Как вы могли заметить, мы двигаем текст по экрану используя SIN и COS, используя счетчики cnt1 и cnt2. Если вы не понимаете, что делают SIN и COS, вернитесь и прочитайте тексты предыдущих уроков.
       // Печатаем GL текст на экране
       glPrint(int((280+250*cos(cnt1))),int(235+200*sin(cnt2)),"NeHe",0);
       glColor3f(1.0f*float(sin(cnt2)),
              1.0f-0.5f*float(cos(cnt1+cnt2)),1.0f*float(cos(cnt1)));


       // Печатаем GL текст на экране
       glPrint(int((280+230*cos(cnt2))),int(235+200*sin(cnt1)),"OpenGL",1);
Мы устанавливаем темно синий цвет и пишем имя автора внизу экрана. Затем мы пишем его имя на экране опять, используя ярко белые символы.
       glColor3f(0.0f,0.0f,1.0f);        // Устанавливаем синий цвет
       glPrint(int(240+200*cos((cnt2+cnt1)/5)),
              2,"Giuseppe D'Agata",0);   // Рисуем текст на экране
       glColor3f(1.0f,1.0f,1.0f);        // Устанавливаем белый цвет
       glPrint(int(242+200*cos((cnt2+cnt1)/5)),
              2,"Giuseppe D'Agata",0);   // Рисуем смещенный текст
Последнее, что мы сделаем - это добавим обоим счетчикам разные значения. Это заставит текст двигаться и вращаться как 3-х мерный объект.
       cnt1+=0.01f;               // Увеличим первый счетчик
       cnt2+=0.0081f;             // Увеличим второй счетчик
       return TRUE;               // Все прошло успешно
}
 
Код в KillGLWindow(), CreateGLWindow() и WndProc() не изменился, поэтому пропустим его.
int WINAPI WinMain( HINSTANCE       hInstance,          // Экземпляр
                    HINSTANCE       hPrevInstance,      // Предыдущий экземпляр
                    LPSTR           lpCmdLine,          // Параметры командной строки
                    int             nCmdShow)           // Стиль вывода окна
{
       MSG           msg;          // Структура сообщения
       BOOL       done=FALSE;      // Переменная для выхода из цикла
       // Спрашиваем у пользователя, какой режим он предпочитает
       if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?",
              "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO) {
              fullscreen=FALSE;       // Режим окна
       }
 
Сменилось название окна.
       // Создаем окно OpenGL
       if (!CreateGLWindow(
              "NeHe & Giuseppe D'Agata's 2D Font Tutorial",640,480,16,fullscreen))


       {
              return 0;           // Окно не создалось - выходим
       }
       while(!done)               // Цикл пока done=FALSE
       {
              if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Пришло сообщение?
              {
                    if (msg.message==WM_QUIT)           // Это сообщение о выходе?
                    {
                           done=TRUE; // Если да, то done=TRUE
                    }
                    else              // Если нет, то обрабатываем сообщение
                    {
                           TranslateMessage(&msg);// Переводим сообщение
                           DispatchMessage(&msg); // Отсылаем сообщение
                    }
              }
              else                    // Нет сообщений
              {
     // Рисуем сцену.  Ждем клавишу ESC или сообщение о выходе из DrawGLScene()
              // Активно?  Было сообщение о выходе?
                    if ((active && !DrawGLScene()) || keys[VK_ESCAPE])
                    {
                           done=TRUE; // ESC или DrawGLScene сообщает о выходе
                    }
                    else              // Не время выходить, обновляем экран
                    {
                           SwapBuffers(hDC); // Меняем экраны (Двойная буферизация)
                    }
              }
       }
       // Закрываем приложение
 
Последнее, что надо сделать, это добавить KillFont() в конец KillGLWindow(), как я показывал раньше. Важно добавить эту строчку. Она очищает память перед выходом из  программы.
       if (!UnregisterClass("OpenGL",hInstance)) // Можем удалить регистрацию класса
       {
             MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",
                  MB_OK | MB_ICONINFORMATION);
             hInstance=NULL;       // Устанавливаем hInstance в NULL
       }
       KillFont();                 // Уничтожаем шрифт


}
Я думаю, что могу официально заявить, что моя страничка теперь учит всем возможным способам, как писать текст на экране {усмешка}. В целом, я думаю это хороший урок. Код можно использовать на любом компьютере, на котором работает OpenGL, его легко использовать, и писать с помощью него текст на экране довольно просто.
Я бы хотел поблагодарить Giuseppe D'Agata за оригинальную версию этого урока. Я сильно его изменил и преобразовал в новый базовый код, но без его присланного кода, я, наверное, не смог бы написать этот урок. Его версия кода имела побольше опций, таких как пробелы между символами и т.д., но я дополнил его прикольным 3-х мерным объектом {усмешка}.
Я надеюсь, всем понравился этот урок. Если у вас есть вопросы, пишите Giuseppe D'Agata или мне.
Содержание раздела