NeHe Tutorials Народный учебник по OpenGL
Урок 13. OpenGL

Bitmap Fonts

Добро пожаловать еще на один урок. На сей раз, я буду учить Вас, как использовать растровые шрифты. Вы можете сказать себе "разве так трудно вывести текст на экран". Если Вы когда-либо пробовали это, то вы знаете, что это не просто!

Уверен, Вы сможете запустить графический редактор, написать текст на изображении, загрузить изображение в вашу программу OpenGL, включить смешивание, затем отобразить текст на экран. Но это съедает время, конечный результат обычно выглядит расплывчатым или грубым в зависимости от типа фильтрации, который Вы использовали, и если ваше изображение имеет альфа канал, ваш текст будет в довершении прозрачным (смешанный с объектами на экране) после наложения на экран.

Если Вы когда-либо использовали Wordpad, Microsoft Word или другой текстовый процессор, Вы, возможно, заметили, сколько разных типов доступных шрифтов. В этом уроке Вы научитесь, как использовать те же самые шрифты в ваших собственных программах OpenGL. Фактически, любой шрифт, который Вы установлен на вашем компьютере, может использоваться в ваших примерах.

Растровые шрифты не только в 100 раз лучше выглядят, чем графические шрифты (текстуры). Вы можете изменять текст на лету. Не нужно делать текстуры для каждого слова или символа, которые Вы хочет вывести на экран. Только позиционируйте текст, и используйте мою удобную новую gl команду, чтобы отобразить текст на экране.

Я попробовал сделать команду настолько простой насколько это возможно. Все, что Вы должны сделать, так это, набрать glPrint ("Привет"). Это легко. Как бы то ни было, Вы можете сказать, что здесь довольно длинное введение, которое я счастлив, дать Вам в этом уроке. Мне потребуется около полутора часов, чтобы создать программу. Почему так долго? Поскольку нет почти никакой информации, доступной по использованию растровых шрифтов, если конечно Вы не наслаждаетесь кодом MFC. Чтобы сохранить по возможности полученный код как можно проще, я решил, что будет хорошо, если я напишу все это в простом для понимания коде Cи :).

Небольшое примечание, этот код применим только в Windows. Он использует функции wgl Windows, для построения шрифтов. Очевидно, Apple имеет функции agl, которые должны делать то же самое, а X имеет glx. К сожалению, я не могу гарантировать, что этот код переносим. Если кто-нибудь имеет платформо-незавизимый код для вывода шрифтов на экран, пришлите мне его, и я напишу другой урок по шрифтам.

Мы начнем с такого же кода как в уроке 1. Мы будем добавлять заголовочный файл stdio.h для стандартных операций ввода/вывода; stdarg.h для разбора текста и конвертирования переменных в текст, и, наконец math.h, для того чтобы перемещать текст по экрану, используя SIN и COS.

 

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

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

#include <gl\gl.h>    // Заголовочный файл для библиотеки OpenGL32

#include <gl\glu.h>   // Заголовочный файл для библиотеки GLu32

#include <gl\glaux.h> // Заголовочный файл для библиотеки GLaux

#include <math.h>     // Заголовочный файл для математической библиотеки ( НОВОЕ )

#include <stdarg.h>   // Заголовочный файл для функций для работы с переменным

                      //  количеством аргументов ( НОВОЕ )

 

HDC        hDC=NULL;  // Приватный контекст устройства GDI

HGLRC      hRC=NULL;  // Постоянный контекст рендеринга

HWND       hWnd=NULL; // Сохраняет дескриптор окна

HINSTANCE  hInstance; // Сохраняет экземпляр приложения

 

Мы также собираемся добавить 3 новых переменных. В base будет сохранен номер первого списка отображения, который мы создаем. Каждому символу требуется собственный список отображения. Символ 'A' - 65 список отображения, 'B' - 66, 'C' - 67, и т.д. Поэтому 'A' будет сохранен в списке отображения base+65.

Затем мы добавляем два счетчика (cnt1 и cnt2). Эти счетчики будут изменяться с разной частотой, и используются для перемещения текста по экрану, используя SIN и COS. Это будет создавать эффект хаотичного движения строки текста по экрану. Мы будем также использовать эти счетчики, чтобы изменять цвет символов (но об этом чуть позже).

 

GLuint  base;      // База списка отображения для фонта

GLfloat  cnt1;     // Первый счетчик для передвижения и закрашивания текста

GLfloat  cnt2;     // Второй счетчик для передвижения и закрашивания текста

 

bool  keys[256];      // Массив для работы с клавиатурой

bool  active=TRUE;    // Флаг активации окна, по умолчанию = TRUE

bool  fullscreen=TRUE;// Флаг полноэкранного режима

 

LRESULT  CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);  // Объявление WndProc

 

В следующей секции кода происходит построение шрифта. Это наиболее трудная часть кода. Объявление 'HFONT font'  задает шрифт в Windows.

Затем мы определяем base. Мы создаем группу из 96 списков отображения, используя glGenLists(96). После того, как списки отображения созданы, переменная base будет содержать номер первого списка.

 

GLvoid BuildFont(GLvoid)  // Построение нашего растрового шрифта

{

  HFONT  font;            // Идентификатор фонта

 

  base = glGenLists(96);  // Выделим место для 96 символов ( НОВОЕ )

 

Теперь позабавимся. Мы собираемся создать наш шрифт. Мы начинаем, задавая размер шрифта. Вы заметили, что это отрицательное число. Вставляя минус, мы сообщаем Windows, что надо найти нам шрифт, основанный на высоте СИМВОЛОВ. Если мы используем положительное число, мы выбираем шрифт, основанный на высоте ЯЧЕЙКИ.

 

font = CreateFont(  -24,        // Высота фонта ( НОВОЕ )

 

Затем мы определяем ширину ячейки. Вы увидите, что я установил ее в 0. При помощи установки значения в 0, Windows будет использовать значение по умолчанию. Вы можете поиграть с этим значением, если Вы хотите. Сделайте шрифт широким, и т.д.

 

        0,        // Ширина фонта

 

Угол отношения (Angle of Escapement) позволяет вращать шрифт. К сожалению, это - не очень полезная возможность. Исключая 0, 90, 180, и 270 градусов, у шрифта будет обрезаться то, что не попало внутрь невидимой квадратной границы. Угол наклона (Orientation Angle), цитируя справку MSDN, определяет угол, в десятых долях градуса, между базовой линией символа и осью X устройства. К сожалению, я не имею понятия о том, что это означает :(.

 

        0,        // Угол отношения

        0,        // Угол наклона

 

Ширина шрифта – отличный параметр. Вы можете использовать числа от 0 - 1000, или Вы можете использовать одно из предопределенных значений. FW_DONTCARE - 0, FW_NORMAL - 400, FW_BOLD - 700, и FW_BLACK - 900. Есть множество других предопределенные значений, но и эти 4 дают хорошее разнообразие. Чем выше значение, тем более толстый шрифт (более жирный).

 

        FW_BOLD,      // Ширина шрифта

 

Курсив, подчеркивание и перечеркивание может быть или TRUE или FALSE. Если подчеркивание TRUE, шрифт будет подчеркнут. Если FALSE то, нет. Довольно просто :).

 

        FALSE,        // Курсив

        FALSE,        // Подчеркивание

        FALSE,        // Перечеркивание

 

Идентификатор набора символов описывает тип набора символов, который Вы хотите использовать. Есть множество типов, и обо всех их не рассказать в этом уроке. CHINESEBIG5_CHARSET, GREEK_CHARSET, RUSSIAN_CHARSET, DEFAULT_CHARSET, и т.д. ANSI – тот набор, который я использую, хотя ЗНАЧЕНИЕ ПО УМОЛЧАНИЮ, вероятно, работало бы точно также.

Если Вы хотите использовать шрифт типа Webdings или Wingdings, Вы должны использовать SYMBOL_CHARSET вместо ANSI_CHARSET.

 

        ANSI_CHARSET,      // Идентификатор набора символов

 

Точность вывода очень важна. Этот параметр сообщает Windows какой из наборов символов использовать, если их доступно больше чем один. OUT_TT_PRECIS сообщает Windows что, если доступен больше чем один тип шрифта, то выбрать с тем же самым названием Truetype версию шрифта. Truetype шрифты всегда смотрят лучше, особенно когда Вы сделаете их большими по размеру. Вы можете также использовать OUT_TT_ONLY_PRECIS, при этом ВСЕГДА используется Truetype шрифт.

 

        OUT_TT_PRECIS,      // Точность вывода

 

Точность отсечения - тип отсечения, который применяется, когда вывод символов идет вне области отсечения. Об этом много говорить нечего, оставьте значение по умолчанию.

 

        CLIP_DEFAULT_PRECIS,    // Точность отсечения

 

Качество вывода - очень важный параметр. Вы можете выбрать PROOF, DRAFT, NONANTIALIASED, DEFAULT или ANTIALIASED. Всем известно, что при ANTIALIASED шрифты выглядят отлично :). Сглаживание (Antialiasing) шрифта – это тот же самый эффект, который Вы получаете, когда Вы включаете сглаживание шрифта в Windows. При этом буквы выглядят менее ступенчато.

 

        ANTIALIASED_QUALITY,    // Качество вывода

 

Затем идут настройка шага и семейство. Для настройки шага Вы можете выбрать DEFAULT_PITCH, FIXED_PITCH и VARIABLE_PITCH, а для настройки семейства, Вы можете выбрать FF_DECORATIVE, FF_MODERN, FF_ROMAN, FF_SCRIPT, FF_SWISS, FF_DONTCARE. Проиграйтесь с этими константами, чтобы выяснить, что они делают. Я оставил их по умолчанию.

 

        FF_DONTCARE|DEFAULT_PITCH,  // Семейство и шаг

 

Наконец... Фактическое название шрифта. Загрузите Microsoft Word или другой текстовый редактор. Щелчок по шрифту - выпадет вниз меню, и ищите шрифт, который Вам нравится. Чтобы его использовать, замените 'Courier New' на название шрифта, который Вы хотите использовать.

 

        "Courier New");      // Имя шрифта

 

Теперь мы выберем шрифт, привязав его к нашему DC, и построим 96 списков отображения, начиная с символа 32 (который является пробелом). Вы можете построить все 256 символов, если Вы хотите. Проверьте, что Вы удаляете все 256 списков отображения, когда Вы выходите из программы, и проверьте, что Вы задаете вместо 32 значение 0 и вместо 96 значение 255 в строке кода ниже.

 

  SelectObject(hDC, font);        // Выбрать шрифт, созданный нами ( НОВОЕ )

 

  wglUseFontBitmaps(hDC, 32, 96, base); // Построить 96 символов начиная с пробела ( НОВОЕ )

}

 

Следующий код очень прост. Он удаляет 96 списков отображения из памяти, начиная с первого списка, заданного 'base'. Я не уверен, что windows сделала бы это за Вас, поэтому лучше быть осмотрительным, чем потом жалеть :).

 

GLvoid KillFont(GLvoid)            // Удаление шрифта

{

   glDeleteLists(base, 96);        // Удаление всех 96 списков отображения ( НОВОЕ )

}

 

Теперь моя удобная первоклассная функция вывода текста GL. Вы вызываете этот раздел кода по команде glPrint ("здесь сообщение"). Текст находится в строке символов *fmt.

 

GLvoid glPrint(const char *fmt, ...)        // Заказная функция «Печати» GL

{

 

В первой строке кода ниже выделяется память для строки на 256 символов. text – это строка, которую мы хотим напечатать на экране. Во второй строке ниже создается указатель, который указывает на список параметров, которые мы передаем наряду со строкой. Если мы посылаем переменные вместе с текстом, она укажет на них.

 

  char    text[256];      // Место для нашей строки

  va_list    ap;          // Указатель на список аргументов

 

В следующих двух строках кода проверяется, если, что-нибудь для вывода? Если нет текста, fmt не будет равняться ничему (NULL), и ничего не будет выведено на экран.

 

  if (fmt == NULL)     // Если нет текста

    return;            // Ничего не делать

 

  va_start(ap, fmt);           // Разбор строки переменных

      vsprintf(text, fmt, ap); // И конвертирование символов в реальные коды

  va_end(ap);                  // Результат помещается в строку

 

Затем мы проталкиваем в стек GL_LIST_BIT, это защищает другие списки отображения, которые мы можем использовать в нашей программе, от влияния glListBase.

Команду glListBase(base-32) немного трудно объяснить. Скажем, что мы рисуем символ 'A', он задан номером 65. Без glListBase(base-32) OpenGL, не понял бы, где найти этот символ. Он стал бы искать этот символ в 65 списке отображения, но если бы база была равна 1000, 'A' был бы фактически сохранен в 1065 списке отображения. Поэтому при помощи, установки базовой отправной точки, OpenGL знает, где находится нужный список отображения. Причина, по которой мы вычитаем 32, состоит в том, что мы не сделали первые 32 списка отображения. Мы опустили их. Так что мы должны указать OpenGL про это, вычитая 32 из базового значения. Символы, кодируются, начиная с нуля, код символа пробела имеет значение 32, поэтому если, мы хотим вывести пробел, то мы должны иметь тридцать второй список отображения, а у нас он нулевой. Поэтому мы искусственно занижаем значение базы, с тем, чтобы OpenGL брал нужные списки. Я надеюсь, что это понятно.

 

  glPushAttrib(GL_LIST_BIT);      // Протолкнуть биты списка отображения ( НОВОЕ )

  glListBase(base - 32);          // Задать базу символа в 32 ( НОВОЕ )

 

Теперь, когда OpenGL знает, где находятся Символы, мы можем сообщить ему, что пора выводить текст на экран. glCallLists - очень интересная команда. Она может вывести больше, чем один список отображения на экран одновременно.

В строке ниже делает следующее. Сначала она сообщает OpenGL, что мы собираемся показывать на экране списки отображения. При вызове функции strlen(text) вычисляется, сколько символов мы собираемся отобразить на экране. Затем необходимо указать максимальное значение посылаемых символов. Мы не посылаем больше чем 255 символов. Так что мы можем использовать UNSIGNED_BYTE. (Вспомните, что байт - любое значение от 0 - 255). Наконец мы сообщаем, что надо вывести, передачей строки 'text'.

Возможно, Вы задаетесь вопросом, почему символы не наезжают друг на друга. Каждый список отображения для каждого символа знает, где правая сторона у символа. После того, как символ выведен, OpenGL переходит на правую сторону выведенного символа. Следующий символ или выведенный объект будут выведены, начиная с последнего места вывода GL, которое будет справа от последнего символа.

Наконец мы возвращаем настройки GL GL_LIST_BIT обратно, как было прежде, чем мы установили нашу базу, используя glListBase(base-32).

 

 glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);// Текст списками отображения(НОВОЕ)

 glPopAttrib(); // Возврат битов списка отображения ( НОВОЕ )

}

 

В коде Init изменилось только одно: добавлена строчка BuildFont(). Она вызывает код выше, для построения шрифта, чтобы OpenGL мог использовать его позже.

 

int InitGL(GLvoid)            // Все начальные настройки OpenGL здесь

{

  glShadeModel(GL_SMOOTH);    // Разрешить плавное затенение

  glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Черный фон

  glClearDepth(1.0f);         // Установка буфера глубины

  glEnable(GL_DEPTH_TEST);    // Разрешение теста глубины

  glDepthFunc(GL_LEQUAL);     // Тип теста глубины

  // Действительно хорошие вычисления перспективы

  glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

  BuildFont();            // Построить шрифт

  return TRUE;            // Инициализация окончена

}

 

Теперь о коде для отрисовки. Вначале мы, очищаем экран и буфер глубины. Мы вызываем glLoadIdentity() чтобы все сбросить. Затем мы перемещаемся на одну единицу вглубь экрана. Если не сделать перемещения, текст не будет отображен. Растровые шрифты лучше применять с ортографической проекцией, а не с перспективной, но ортографическая проекция выглядит плохо, поэтому, когда работаем в этой проекции, перемещаем.

Вы можете заметить, что, если бы Вы переместили текст даже вглубь экрана, размер шрифта не уменьшиться, как бы Вы этого ожидали. Что реально происходит, когда Вы глубже перемещаете текст, то, что Вы имеете возможность контролировать, где текст находится на экране. Если Вы переместили на 1 единицу в экран, Вы можете расположить текст, где-нибудь от -0.5 до +0.5 по оси X. Если Вы переместите на 10 единиц в экран, то Вы должны располагать текст от -5 до +5. Это даст Вам возможность лучше контролировать точное позиционирование текста, не используя десятичные разряды. При этом размер текста не измениться. И даже с помощью glScalef(x,y,z). Если Вы хотите шрифт, больше или меньше, сделайте его большим или маленьким во время его создания!

 

int DrawGLScene(GLvoid) // Здесь мы будем рисовать все

{

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экран и буфера глубины

  glLoadIdentity(); // Сброс просмотра

  glTranslatef(0.0f,0.0f,-1.0f); // Передвижение на одну единицу вглубь

 

Теперь мы воспользуемся нестандартными вычислениями, чтобы сделать цветовую пульсацию. Не волнуйтесь, если Вы не понимаете то, что я делаю. Я люблю пользоваться множеством переменных, и дурацкими уловками, чтобы достигнуть результата :).


На этот раз, я использую два счетчика, которые мы создали для перемещения текста по экрану, и для манипулирования красным, зеленым и синим цветом. Красный меняется от -1.0 до 1.0 используя COS и счетчик 1. Зеленый меняется от -1.0 до 1.0 используя SIN и счетчик 2. Синий меняется от 0.5 до 1.5 используя COS и счетчики 1 + 2. Тем самым синий никогда не будет равен 0, и текст не должен никогда полностью исчезнуть. Глупо, но это работает :).

 

  // Цветовая пульсация, основанная на положении текста

  glColor3f(1.0f*float(cos(cnt1)),1.0f*float(sin(cnt2)),1.0f-0.5f*float(cos(cnt1+cnt2)));

 

Теперь новая команда. glRasterPos2f(x,y) будет позиционировать растровый шрифт на экране. Центр экрана как прежде в 0,0. Заметьте, что нет координаты Z. Растровые шрифты используют только ось X (лево/право) и ось Y (вверх/вниз). Поскольку мы перемещаем на одну единицу в экран, левый край равен -0.5, и правый край равен +0.5. Вы увидите, что я перемещаю на 0.45 пикселей влево по оси X. Это устанавливает текст в центр экрана. Иначе он было бы правее на экране, потому что текст будет выводиться от центра направо.

Нестандартные вычисления делают в большой степени то же самое, как и при вычислении цвета. Происходит перемещение текста по оси X от -0.50 до -0.40 (вспомните, мы вычли справа от начала 0.45). При этом текст на экране будет всегда. Текст будет ритмично раскачиваться влево и вправо, используя COS и счетчик 1. Текст будет перемещаться от -0.35 до +0.35 по оси Y, используя SIN и счетчик 2.

 

  // Позиционирование текста на экране

  glRasterPos2f(-0.45f+0.05f*float(cos(cnt1)), 0.35f*float(sin(cnt2)));

 

Теперь моя любимая часть... Реальный вывод текста на экран. Я попробовал сделать его очень простым, и крайне дружелюбным способом. Вы увидите, что вывод текста выглядит как большинство команд OpenGL, при этом в комбинации с командой Print, сделанной на старый добрый манер :). Все, что Вам надо сделать, чтобы ввести текст на экран - glPrint ("{любой текст, который Вы хотите}"). Это очень просто. Текст будет выведен на экран точно в том месте, где Вы установили его.

Shawn T. прислал мне модифицированный код, который позволяет glPrint передавать переменные для вывода на экран. Например, Вы можете увеличить счетчик и отобразить результат на экране! Это работает примерно так... В линии ниже Вы увидите нормальный текст. Затем идет пробел, тире, пробел, затем "символ" (%7.2f). Посмотрев на %7.2f Вы можете сказать, что эта рогулька означает. Все очень просто. Символ % - подобен метке, которая говорит, не печатать 7.2f на экран, потому что здесь будет напечатано значение переменной. При этом 7 означает, что максимум 7 цифр будут отображены слева от десятичной точки. Затем десятичная точка, и справа после десятичной точки - 2. 2 означает, что только две цифры будут отображены справа от десятичной точки. Наконец, f. f означает, что число, которое мы хотим отобразить - число с плавающей запятой. Мы хотим вывести значение cnt1 на экран. Предположим, что cnt1 равен 300.12345f, окончательно мы бы увидели на экране 300.12. Цифры 3, 4, и 5 после десятичной точки были бы обрезаны, потому что мы хотим, чтобы появились только 2 цифры после десятичной точки.

Конечно, если Вы профессиональный программист на Си, то, это ненужный рассказ, но этот урок могут читать люди, которые и не использовали printf.  Если Вы хотите больше узнать о маркерах, купите книгу, или посмотрите MSDN.

 

  glPrint("Active OpenGL Text With NeHe - %7.2f", cnt1);  // Печать текста GL на экран

 

И в завершении увеличим значение обоих счетчиков на разную величину, чтобы была цветовая пульсация и передвижение текста.

 

  cnt1+=0.051f;  // Увеличение первого счетчика

  cnt2+=0.005f;  // Увеличение второго счетчика

  return TRUE;   // Все отлично

}

 

Также необходимо добавить KillFont() в конец KillGLWindow() как, показано ниже. Важно добавить эту строку. При этом списки отображения очищаются прежде, чем мы выходим из нашей программы.

 

  if (!UnregisterClass("OpenGL",hInstance))    // Если класс не зарегистрирован

  {

    MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",
MB_OK | MB_ICONINFORMATION);

    hInstance=NULL;          // Установить копию приложения в ноль

  }

  KillFont();            // Уничтожить шрифт

}

 

Вот и все... Все, что Вы должны знать, чтобы использовать растровые шрифты в ваших собственных проектах OpenGL. Я поискал в сети подобный материал, и ничего похожего не нашел. Возможно мой сайт первый раскрывает эту тему на простом понятном коде Cи? Возможно. Получайте удовольствие от этого урока, и счастливого кодирования!

© Jeff Molofee (NeHe)

PMG  29 июля 2002 (c)  Сергей Анисимов