Урок 14. Векторные шрифты

Этот урок представляет собой продолжение предыдущего, 13-го урока, в котором я показал вам, как использовать растровые шрифты, и будет посвящен векторным шрифтам.

 

Путь, которым мы пойдем при построения векторного шрифта, подобен тому, как мы строили растровый шрифт в уроке 13. И все-таки:

 

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

  Небольшое замечание - приведенный код предназначен для Windows. Для построения шрифтов в нем используются WGL-функции из набора Windows API. Очевидно, Apple имеет в AGL поддержку тех же вещей, и X - в GLX. К сожалению, я не могу гарантировать переносимость этого кода. Если кто-нибудь имеет независимый от платформы код для отрисовки шрифтов на экране, пришлите их мне и я напишу другое руководство.

 

Начнем со стандартного кода из урока номер 1. Добавим STDIO.H для стандартных операций ввода-вывода; STDARG.H - для разбора текста и конвертации в текст числовых переменных, и, MATH.H - для вызова функций SIN и COS, которые понадобятся, когда мы захотим вращать текст на экране :).

 

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

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

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

#include <stdarg.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; // Копия нашего приложения

 

Теперь добавим 2 новые переменные. Переменная base будет содержать номер первого списка отображения ('display list' по-английски, представляет собой последовательность команд OpenGL, часто выполняемую и хранящуюся в специальном формате, оптимизированном для скорости выполнения - прим. перев.), который мы создадим. Каждому символу требуется свой список отображения. Например, символ 'A' имеет 65-й номер списка отображения, 'B' - 66, 'C' - 67, и т.д. То есть символ 'A' будет хранится в списке отображения base+65.

 

Кроме этой добавим переменную rot. Она будет использоваться при вычислениях для вращения текста на экране через функции синуса и косинуса, И еще она будет использована при пульсации цветов.

 

GLuint base;              // База отображаемого списка для набора символов (добавлено)

GLfloat   rot;               // Используется для вращения текста (добавлено)

bool   keys[256];         // Массив для манипуляций с клавиатурой

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

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

 

GLYPHMETRICSFLOAT gmf[256] будет содержать информацию о местоположении и ориентации каждого из 256 списков отображения нашего векторного шрифта. Чтобы получить доступ к нужной букве просто напишем gmf[num], где num - это номер списка отображения, соответствующий требуемой букве. Позже в программе я покажу вам, как узнать ширину каждого символа для того, чтобы вы смогли автоматически центрировать текст на экране. Имейте в виду, что каждый символ имеет свою ширину. Метрика шрифта (glyphmetrics) на порядок облегчит вам жизнь.

 

GLYPHMETRICSFLOAT gmf[256];  // Массив с информацией о нашем шрифте

LRESULT CALLBACK WndProc(

HWND, UINT, WPARAM, LPARAM); // Объявление оконной процедуры

 

Следующая часть программы создает шрифт так же, как и при построении растрового шрифта. Как и в уроке №13, это место в программе было для меня самым трудным для объяснения.

  Переменная HFONT font будет содержать идентификатор шрифта Windows.

 

Далее заполним переменную base, создав набор из 256-ти списков отображения, вызвав функцию glGenLists(256). После этого переменная base будет содержать номер первого списка отображения.

 

GLvoid BuildFont(GLvoid)           // Строим растровый шрифт

{

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

  base = glGenLists(256);          // массив для 256 букв

 

Теперь будет интереснее :). Делаем наш векторный шрифт. Во-первых, определим его размер. В приведенной ниже строке кода вы можете заметить отрицательное значение. Этим минусом мы говорим Windows, что наш шрифт основан на высоте СИМВОЛА. А положительное значение дало бы нам шрифт, основанный на высоте ЗНАКОМЕСТА.

 

font = CreateFont(       -12,          // высота шрифта

 

Определим ширину знакоместа. Вы видите, что ее значение установлено в ноль. Этим мы заставляем Windows взять значение по умолчанию. Если хотите, поиграйтесь с этим значением, сделайте, например, широкие буквы.

 

              0,            // ширина знакоместа

  Угол отношения (Angle of Escapement) позволяет вращать шрифт. Угол наклона (Orientation Angle), если сослаться на 'MSDN help', определяет угол, в десятых долях градуса, между базовой линией символа и осью Х экрана. Я, к сожалению, не имею идей насчет того, зачем это :(.

 

              0,            //Угол перехода

              0,            //Угол направления

 

Ширина шрифта - важный параметр. Может быть в пределах от 0 до 1000. Также можно использовать предопределенные значения: FW_DONTCARE = 0, FW_NORMAL = 400, FW_BOLD = 700 и FW_BLACK = 900. Таких значений много, но эти дают наилучший результат. Понятно, что чем выше значение, тем толще (жирнее) шрифт.

 

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

 

Параметры Italic, Underline и Strikeout (наклонный, подчеркнутый и зачеркнутый) могут иметь значения TRUE или FALSE. Хотите, к примеру, чтобы шрифт был подчеркнутым, в параметре подчеркнутости поставьте значение TRUE, не хотите - FALSE. Просто :).

 

              FALSE,        // Курсив

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

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

 

Идентификатор набора символов определяет, соответственно, набор символов (кодировку), который мы хотим использовать. Их, конечно, очень много: CHINESEBIG5_CHARSET, GREEK_CHARSET, RUSSIAN_CHARSET, DEFAULT_CHARSET и т.д. Я, к примеру, использую ANSI. Хотя, DEFAULT тоже может подойти. Если интересуетесь такими шрифтами, как Webdings или Wingdings, вам надо использовать значение SYMBOL_CHARSET вместо ANSI_CHARSET.

 

              ANSI_CHARSET,       //Идентификатор кодировки

 

Точность вывода - тоже важный параметр. Он говорит Windows о том, какой тип символа использовать, если их более одного. Например, значение OUT_TT_PRECIS означает, что надо взять TRUETYPE - версию шрифта.Truetype - шрифт обычно смотрится лучше, чем другие, особенно когда буквы большие. Можно также использовать значение OUT_TT_ONLY_PRECIS, которое означает, что всегда следует брать, если возможно, шрифт TRUETYPE.

 

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

 

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

 

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

 

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

 

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

 

Следующими идут значения семейства (Family) и шага (Pitch). Шаг может принимать значения DEFAULT_PITCH, FIXED_PITCH и VARIABLE_PITCH. Семейство может быть FF_DECORATIVE, FF_MODERN, FF_ROMAN, FF_SCRIPT, FF_SWISS, FF_DONTCARE. Поиграйте этими значениями, чтобы понять, что они делают. Здесь оба параметра установлены по умолчанию.

 

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

 

И последнее... Нам нужно настоящее имя используемого нами шрифта. Его можно увидеть, например в Word при выборе шрифта для текста. Зайдите в Word или другой текстовый редактор, выберите шрифт, который требуется, запомните, как он называется, и вставьте его название вместо значения 'Comic Sans MS' в приведенной ниже строке.

 

              "Comic Sans MS");          // Имя шрифта

 

Теперь выберем этот шрифт, связав его с нашим контекстом устройста (DC).

 

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

 

Подошло время добавить новые строки в программу. Мы построим наш векторный шрифт, используя новую команду - wglUseFontOutlines. Выберем контекст устройства (DC), начальный символ, количество создаваемых символов, и базовое значение списка отображения. Все очень похоже на то, как мы строили растровый шрифт.

 

wglUseFontOutlines( hDC,         // Выбрать текущий контекст устройства (DC)

               0,            // Стартовый символ

               255,          // Количество создаваемых списков отображения

               base,         // Стартовое значение списка отображения

 

Но это еще не все. Здесь мы установим уровень отклонения. Уже при значении 0.0f сглаживание будет видимо. После установки отклонения идет определение толщины шрифта (ширины по оси Z). Толщина, равная 0.0f, понятно, даст нам плоские буквы, то есть двумерные. А вот при значении 1.0f уже будет виден некоторый объем.

 

Параметр WGL_FONT_POLYGONS говорит OpenGL создать "твердый" шрифт при помощи полигонов. Если вместо него вы укажете WGL_FONT_LINES, шрифт будет каркасным, или "проволочным" (составленным из линий). Стоит заметить, если вы укажете значение GL_FONT_LINES, не будут сгенерированы нормали, что сделает невозможным свечение букв.

 

Последний параметр, gmf указывает на буфер адреса для данных списка отображения.

 

                    0.0f,       //Отклонение от настоящего контура

                    0.2f,       //Толщина шрифта по оси Z

                    WGL_FONT_POLYGONS,       //Использовать полигоны, а не линии

                    gmf),       //буфер адреса для данных списка отображения

                    }

 

Следующий код очень простой. Он удаляет 256 списков отображения из памяти, начиная с первого списка, номер которого записан в base. Не уверен, что Windows сделает это за нас, поэтому лучше позаботиться самим, чем потом иметь глюки в системе.

 

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

              {

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

              }

 

Теперь функция вывода текста в OpenGL. Ее мы будем вызывать не иначе как glPrint("Выводимый текст"), точно также, как и в варианте с растровым шрифтом из урока N 13. Текст при этом помещается в символьную строку fmt.

 

GLvoid glPrint(const char *fmt, ...)     // Функция вывода текста в OpenGL

              {

 

В первой строке, приведенной ниже, объявляется и инициализируется переменная length. Она будет использована при вычислении того, какой из нашей строки выйдет текст. Во второй строке создается пустой массив для текстовой строки длиной в 256 символов. text - строка, из которой будет происходить вывод текста на экран. В третьей строке описывается указатель на список аргументов, передаваемый со строкой в функцию. Если мы пошлем с текстом какие-нибудь переменные, этот указатель будет указывать на них.

 

              float         length=0;     // Переменная для нахождения

                                          // физической длины текста

              char          text[256];    // Здесь наша строка

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

 

Проверим, а может нет никакого текста :). Тогда выводить нечего, и - return.

 

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

                    return;               // ничего не делаем

 

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

 

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

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

              va_end(ap);                // Результат сохраняется в text

 

Стоит сказать отдельное спасибо Джиму Вильямсу (Jim Williams) за предложенный ниже код. До этого я центрировал текст вручную, но его метод немного лучше :). Начнем с создания цикла, который проходит весь текст символ за символом. Длину текста вычисляем в выражении strlen(text). После обработки данных цикла (проверки условия завершения) идет его тело, где к значению переменной lenght добавляется физическая ширина каждого символа. После завершения цикла величина, находящаяся в lenght, будет равна ширине всей строки. Так, если мы напечатаем при помощи данной функции слово "hello", и по каким-то причинам каждый символ в этом слове будет иметь ширину ровно в 10 единиц, то, понятно, что в итоге у нас получится значение lenght=10+10+10+10+10=50 единиц.

 

Значение ширины каждого символа получается из выражения gmf[text[loop]].gmfCellIncX. Помните, что gmf хранит информацию о каждом списке отображения. Если loop будет равна 0, то text[loop] - это будет первый символ нашей с вами строки. Соответственно, при loop, равной 1, то text[loop] будет означать второй символ этой же строки. Ширину символа дает нам gmfCellIncX. На самом деле gmfCellIncX - это расстояние, на которое смещается вправо позиция графического вывода после отображения очередного символа для того, чтобы символы не наложились друг на друга. Так уж вышло, что это расстояние и наша ширина символа - одно и то же значение.

 

Высоту символа можно узнать при помощи команды gmfCellIncY. Это может пригодиться при выводе вертикального текста.

 

       for (unsigned int loop=0;loop//Цикл поиска размера строки

                    {

                    length+=gmf[text[loop]].gmfCellIncX;

                    // Увеличение размера на ширину символа

                    }

 

Полученную величину размера строки преобразуем в отрицательную (потому что мы будем перемещаться влево от центра для центровки текста). Затем мы делим длину на 2: нам не нужно перемещать весь текст влево - только его половину.

 

glTranslatef(-length/2,0.0f,0.0f);       //Центровка на экране нашей строки

 

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

 

Команда glListBase(base) говорит OpenGL, где искать для каждого символа соответствующий список отображения.

 

       glPushAttrib(GL_LIST_BIT); // Сохраняет в стеке значения битов списка отображения

       glListBase(base);          // Устанавливает базовый символ в 0

 

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

 

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

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

  Наконец, мы восстанавливаем из стека GL_LIST_BIT - установки OpenGL обратно по состоянию на тот момент. Как они были перед установкой базовых значений командой glCallLists(base).

 

       // Создает списки отображения текста

       glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);

       glPopAttrib(); // Восстанавливает значение Display List Bits

                    }

 

Участок программы, отвечающий за размеры в окне OpenGL, точно такой же, как и в уроке N 1, поэтому здесь мы его пропустим.

  В конец функции InitGL добавилось несколько новых строк. Строка с выражением BuildFont() из 13 урока осталась прежней, вместе с новым кодом, который создает быстрое и черновое освещение. Источник света Light0 встроен в большинство видеокарт, поэтому достаточно приемлемое освещение сцены не потребует особых усилий с нашей стороны.

  Еще я добавил команду glEnable(GL_Color_Material). Поскольку символы являются 3D-объектами, вам понадобится раскраска материалов (Material Coloring). В противном случае смена цвета с помощью glColor3f(r,g,b) не изменит цвет текста. Если у вас на экран выводятся кроме текста другие фигуры-объекты 3D-сцены, включайте раскраску материалов перед выводом текста и отключайте сразу после того, как текст будет нарисован, иначе будут раскрашены все объекты на экране.

 

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);

       glEnable(GL_LIGHT0);           // Включить встроенное освещение (черновое) (новая)

       glEnable(GL_LIGHTING);                  // Разрешить освещение                        (новая)

       glEnable(GL_COLOR_MATERIAL);            // Включить раскраску материалов (новая)

 

       BuildFont();                            // Построить шрифт (добавлена)

 

       return TRUE;                            // Инициализация прошла успешно

}

 

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

 

Управлять векторными шрифтами можно также при помощи команды glScalef(x,y,z). Если захотите сделать буквы в два раза выше, дайте команду glScalef(1.0f,2.0f,1.0f). Значение 2.0f здесь относится к оси Y и сообщает OpenGL, что список отображения нужно нарисовать в двойной высоте. Если это значение поставить на место первого аргумента (Х), то буквы будут в два раза шире. Ну, третий аргумент, естественно, касается оси Z.

 

int DrawGLScene(GLvoid)                  // Здесь весь вывод на экран

{

       // Очистка экрана и буфера глубины

       glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

       glLoadIdentity();                 // Сброс вида

       glTranslatef(0.0f,0.0f,-10.0f);   // Смещение на 10 единиц в экран

 

После сдвига вглубь экрана мы можем повращать текст. Следующие три строки поворачивают изображение по трем осям. Умножением переменной rot на различные значения я пытался добиться как можно более различающихся скоростей вращения.

 

       glRotatef(rot,1.0f,0.0f,0.0f);          // Поворот по оси X

       glRotatef(rot*1.5f,0.0f,1.0f,0.0f);     // Поворот по оси Y

       glRotatef(rot*1.4f,0.0f,0.0f,1.0f);     // Поворот по оси Z

 

Теперь займемся цветовым циклом. Как обычно, использую здесь переменную-счетчик (rot). Возгорание и затухание цветов получается при помощи функций SIN и COS. Я делил переменную rot на разные числа, так что бы каждый цвет не возрастал с такой же скоростью. Результат впечатляющий.

 

       // Цветовая пульсация основанная на вращении

       glColor3f(1.0f*float(cos(rot/20.0f)),1.0f*float(sin(rot/25.0f)),

              1.0f-0.5f*float(cos(rot/17.0f)));

 

Моя любимая часть... Запись текста на экран. Мною были использованы несколько команд, которые мы применяли также при выводе на экран растровых шрифтов. Сейчас вы уже знаете, как вывести текст в команде glPrint("Ваш текст"). Это так просто!

 

В коде, приведенном ниже, мы печатаем "NeHe", пробел, тире, пробел и число из переменной rot и разделенное на 50, чтобы немного его уменьшить. Если число больше 999.99, разряды слева игнорируются (так как в команде мы указали 3 разряда на целую часть числа и 2 - на дробную после запятой).

 

       glPrint("NeHe - %3.2f",rot/50);   // Печать текста на экране

 

Затем увеличиваем переменную rot для дальнейшей пульсации цвета и вращения текста.

 

       rot+=0.5f;                        // Увеличить переменную вращения

       return TRUE;                      // Все прошло успешно

              }

 

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

 

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

       {

       MessageBox(NULL,"Could Not Unregister Class.",

              "SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);

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

       }

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

 

Под конец урока вы уже должны уметь использовать векторные шрифты в ваших проектах, использующих OpenGL. Как и в случае с уроком N 13, я пытался найти в сети подобные руководства, но, к сожалению, ничего не нашел. Возможно мой сайт - первый в раскрытии данной темы в подробном рассмотрении для всех, понимающих язык Си? Читайте руководство и удачного вам программирования!

© Jeff Molofee (NeHe)

 15 февраля 2002 (c)  Александр Попович