Lekcja 43. Czcionki FreeType w OpenGL
Autor: Hadrian 'Queight' Węgrzynowski
Oryginał: FreeType Fonts in OpenGL (Sven Olsen)
Źródła: http://nehe.gamedev.net/data/lessons/vc/lesson43.zip

Oto krótki tutorial, który pokaże Ci jak używać drugiej wersji biblioteki renderującej czcionki FreeType w OpenGL. Dzięki bibliotece FreeType możemy stworzyć antyaliasowany tekst, który wygląda lepiej niż bitmapowe czcionki (lekcja 13). Nasz tekst będzie posiadać dodatkową przewagę nad czcionkami bitmapowymi - będzie prosty do obrócenia i będzie świetnie działać z wybieraniem (picking) OpenGL (więcej o wybieraniu w lekcji 32).

Tworzenie programu

Pierwsze co musisz zrobić to zdobyć kopię biblioteki GNU FreeType. Udaj się na stronę http://gnuwin32.sourceforge.net/packages/freetype.htm i ściągnij binarki oraz pliki dla deweloperów (jeżeli używasz Linuksa to zajrzyj do repozytorium swojej dystrybucji). Podczas instalacji zwróć uwagę na warunki licencji, która mówi, że jeżeli używasz FreeType w swoich programach to jesteś zobowiązany wspomnieć o nich w swojej dokumentacji.

Stwórz nowy projekt, taki jak w lekcji 1., ale nie zapomnij dodać do bibliotek libfreetype.lib (lub też libfreetype.a).

Jeżeli korzystasz z Linuksa to przejdź do następnego paragrafu.

Teraz potrzebujesz dodać do listy katalogów z plikami nagłówkowymi katalogi: C:\Program Files\GNUWIN32\include\freetype2 oraz C:\Program Files\GNUWIN32\include

Potrzebujesz również dodać katalog C:\Program Files\GNUWIN32\lib do listy katalogów z bibliotekami.

W tym momencie jesteśmy wstanie skompilować programy używające FreeType, ale nie będą działać dopóki nie mają dostępu do freetype-6.dll. Masz kopię tej DLL'ki w katalogu GNUWIN32\bin, jeżeli skopiujesz ją tam, gdzie każdy Twój program będzie mieć dostęp, to będziesz mógł uruchomić programy używające FreeType. Pamiętaj, że jeżeli będziesz chciał wydać program używający FreeType, to będziesz musiał dodać do niego kopię freetype-6.dll.

Zaczynamy

Dodaj do projektu dwa nowe pliki - "freetype.cpp" i "freetype.h". Umieścimy cały kod związany z FreeType w tych plikach i lekko zmodyfikujemy kod z lekcji 13 by zademonstrować funkcje, które napisaliśmy. Kiedy skończymy, otrzymamy bardzo prostą bibliotekę łączącą OpenGL z FreeType, która teoretycznie mogłaby być użyta w dowolnym projekcie używającym OpenGL.

freetype.h

Oczywiście potrzebujemy nagłówków FreeType i OpenGL. Dodamy jeszcze pewne użyteczne częsci STL'a, włączając w to klasy wyjątków z STL'a, które ułatwią nam wyłapywanie błędów dzięki ładnym komunikatom.

#ifndef FREE_NEHE_H
#define FREE_NEHE_H
        // Nagłówki FreeType
#include <ft2build.h>
#include <freetype/freetype.h>
#include <freetype/ftglyph.h>
#include <freetype/ftoutln.h>
#include <freetype/fttrigon.h>
        // Nagłówki OpenGL
#include <windows.h>         // Niezbędne dla OpenGL w Windows
#include <GL/gl.h>
#include <GL/glu.h>
        // Niektóre nagłowki STL
#include <vector>
#include <string>
        // Używanie biblioteki wyjątków z STL zwiększa szanse,
        // że ktoś, używając nasz kod, będzie mógł poprawnie
        // złapać jakikolwiek wyjątek rzucony przez nas
#include <stdexcept>
        // MSVC wyrzuci wszystkie bezużyteczne ostrzeżenia
        // jeżeli stworzysz vector<string>, ta pragma pozbywa się ich
#pragma warning(disable: 4786)

Wszystkie informacje jakie czcionka potrzebuje umieścimy w jednej strukturze (to trochę ułatwi zarządzanie wieloma czcionkami). Tak jak nauczyliśmy się w lekcji 13, gdy WGL stwarza czcionkę to generuje zbiór następujących po sobie list wyświetlania. To jest piękne, ponieważ oznacza, że możesz używać glCallLists do wypisania łańcucha znaków z pomocą tylko jednej komendy. Kiedy utworzymy naszą czcionkę, będziemy ustawiać wszystko w ten sam sposób, co oznacza, że pole list_base będzie składować pierwsze 128 kolejnych list wyświetlania. Ponieważ zamierzamy używać tekstur do rysowania tekstu, będziemy potrzebować miejsca na 128 powiązanych tekstur. Ostatnią informacją jaką potrzebujemy jest wysokość, w pikselach, stworzonej przez nas czcionki (to umożliwi tworzenie nowych linii w naszej funkcji rysującej).

        // Umieść wszystko w przestrzeni nazw,
        // w ten sposób będziemy mogli używać
        // pospolitych nazw funkcji, takich jak "print"
        // bez martwienia się o czyiś nakładający się kod
namespace freetype
{
        // W tej przestrzeni nazw, daje nam możliwość
        // napisać po prostu "vector" zamiast "std::vector"
using std::vector;
        // Ditto For String.
using std::string;
        // W tym zawarte są wszelkie informacje
        // związane z każdą czcionką FreeType
        // jaką chcemy wykreować
struct font_data
{
    float h;         // wysokość czcionki
    GLuint *textures;         // identyfikatory tekstur
    GLuint list_base;         // identyfikator pierwszej listy wyświetlania
        // Funkcja init utworzy czcionkę na podstawie
        // wysokości h z pliku o nazwie fname.
    void init(const char *fname, unsigned int h);
        // Zwolnij wszystkie zasoby związane z czcionką
    void clean();
};

Ostatnią rzeczą jaka jest nam potrzebna to prototyp funkcji wypisującej:

        // Flagowa funkcja biblioteki - to narysuje tekst czcionką ft_font na pozycji X, Y
        // z uwzględnieniem aktualnej macierzy widoku modelu (modelview)
void print(const font_data &ft_font, float x, float y, const char *fmt, ...);
}         // koniec przestrzeni nazw
#endif

To wszystko w pliku nagłówkowym!

freetype.cpp

        // Dołączamy nasz nagłówek
#include "freetype.h"
namespace freetype
{

Wykorzystujemy tekstury do wyświetlania znaków w naszej czcionce. Tekstury w OpenGL muszą mieć wymiary będące potęgami dwójki, dlatego dodamy pewien margines do bitmap czcionek utworzonych przez FreeType, by były właściwego rozmiaru. Do tego potrzebujemy tej funkcji:

        // Ta funkcja zwraca pierwszą potęgę dwójki >=
        // wartości danego int'a
inline int next_p2(int a)
{
    int rval=1;
        // rval<<=1 to ładniejsza wersja rval*=2;
    while(rval<a)
        rval<<=1;
    return rval;
}

Następna potrzeba nam funkcja to make_dlist, będąca sercem naszej biblioteki. Jako argument bierze FT_Face, który jest obiektem używanym przez FreeType do składowania informacji o czcionce. Funkcja tworzy odpowiednią listę wyświetlania do podanego znaku.

        // Tworzy listę wyświetlania w zależności od podanego znaku
void make_dlist(FT_Face face, char ch, GLuint list_base, GLuint *tex_base)
{
        // Pierwszą rzeczą jaką potrzebujemy to wyrenderowany do bitmapy,
        // przez FreeType, znak. Potrzeba parę komend FreeType:
        // Ładuje kształt (glif) naszego znaku
    if(FT_Load_Glyph(face, FT_Get_Char_Index(face, ch), FT_LOAD_DEFAULT))
        throw std::runtime_error("FT_Load_Glyph failed");
        // Przenieś kształt znaku z face'a do obiektu kształtu
    [translator]Oryginalnie komentarz brzmiał: [i]Move The Face's Glyph Into A Glyph Object.[/i], mam nadzieję, że dość wiernie oddałem sens tego zdania.[/translator]
    FT_Glyph glyph;
    if(FT_Get_Glyph(face->glyph, &glyph))
        throw std::runtime_error("FT_Get_Glyph failed");
        // Konwersja kształtu do bitmapy
    FT_Glyph_To_Bitmap(&glyph, ft_render_mode_normal, 0, 1);
    FT_BitmapGlyph bitmap_glyph = (FT_BitmapGlyph)glyph;
        // Ta referencja ułatwi dostęp do bitmapy
    FT_Bitmap& bitmap=bitmap_glyph->bitmap;

Teraz, gdy mamy już bitmapę utworzoną przez FreeType, potrzebujemy dodać margines, żeby móc utworzyć teksturę OpenGL. Należy pamiętać, że w kontekście OpenGL wyrażenie "bitmapa" onacza binarny obraz (org. - binary images), w kontekście FreeType bitmapa przechowuje 8 bitów informacji na piksel, dlatego bitmapy FreeType'a są wstanie przechowywać odcienie szarości, których potrzebujemy do utworzenia antyaliasowanego tekstu.

        // Użyj naszej pomocniczej funkcji do otrzymania wymiarów
        // bitmapy, które będziemy potrzebować do stworzenia tekstury
    int width = next_p2(bitmap.width);
    int height = next_p2(bitmap.rows);
        // Alokuje pamięć dla danych tekstury
    GLubyte* expanded_data = new GLubyte[2*width*height];
        // Wypełnimy dane rozszerzonej bitmapy.
        // Zauważ, że używamy dwu kanałowej bitmapy (jeden
        // kanał dla luminacji i jeden dla alfy), ale przypiszemy
        // luminację i alfę do wartości jaką znajdziemy
        // w bitmapie od FreeType.
        // Użyjemy operatora ?: do określenia, czy wartością jaką używamy
        // będzie 0, jeżeli jesteśmy w strefie marginesu, bądź cokolwiek
        // będące bitmapą FreeType.
    for(int j=0; j < height; j++)
    {
        for(int i=0; i < width; i++)
        {
            expanded_data[2*(i+j*width)] = expanded_data[2*(i+j*width)+1] =
                (i >= bitmap.width || j >= bitmap.rows)
                ? 0 : bitmap.buffer[i + bitmap.width*j];
        }
    }

Wraz ze zrobionym marginesem, możemy przejść do utworzenia tekstury OpenGL. Dodajemy kanał alfa by czarne części bitmapy były przezroczyste i by krawędzie tekstu były odrobinę półprzezroczyste (co powinno sprawić, że będą dobrze wyglądać na każdym tle).

        // Ustawimy parametry tekstury
    glBindTexture( GL_TEXTURE_2D, tex_base[ch]);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
        // Tutaj utworzymy samą w sobie teksturę.
        // Zauważ, że używamy GL_LUMINANCE_ALPHA
        // by wskazać, że używamy 2-kanałowych danych
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
        GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, expanded_data);
        // Wraz z utworzoną teksturą nie potrzebujemy rozszerzonej bitmapy
    delete [] expanded_data;

Używamy oteksturowanych quadów to rysowania naszego tekstu. Znaczy to, że łatwo będzie obracać i skalować tekst, dodatkowo czcionki będą mogły przybierać aktualny kolor OpenGL (przy używaniu czcionek bitmapowych nie działa nic z wymienionych).

        // Teraz utworzymy listy wyświetlania
    glNewList(list_base+ch, GL_COMPILE);
    glBindTexture(GL_TEXTURE_2D, tex_base[ch]);
    glPushMatrix();
        // Najpierw musimy się trochę przesunąć, żeby było
        // wystarczająco dużo miejsca przed i za znakiem
    glTranslatef(bitmap_glyph->left, 0, 0);
        // Teraz przesuniemy się trochę w dół w wypadku,
        // gdy bitmapa wychodzi dołem poza linię.
        // Zdarza się to tylko dla znaków takich jak: 'g' lub 'y'.
    glTranslatef(0, bitmap_glyph->top-bitmap.rows, 0);
        // Wiele naszych tekstur posiada marginesy.
        // Musimy się dowiedzieć jaką faktyczną częścią tekstury jest znak
        // i przechować tą informację w zmiennych x i y,
        // gdy już narysujemy quada będziemy odnosić się tylko
        // do części tekstury zawierającej sam znak.
    float x = (float)bitmap.width / (float)width,
        y = (float)bitmap.rows / (float)height;
        // Tutaj rysujemy oteksturowane quady.
        // Bitmapa, którą dostaliśmy od FreeType
        // nie jest ustawiona w sposób, który by nam odpowiadał,
        // ale nadajemy takie koordynaty teksturze
        // by całość była poprawnie ustawiona.
    glBegin(GL_QUADS);
        glTexCoord2d(0, 0); glVertex2f(0, bitmap.rows);
        glTexCoord2d(0, y); glVertex2f(0, 0);
        glTexCoord2d(x, y); glVertex2f(bitmap.width, 0);
        glTexCoord2d(x, 0); glVertex2f(bitmap.width, bitmap.rows);
    glEnd();
    glPopMatrix();
    glTranslatef(face->glyph->advance.x >> 6, 0, 0);
        // Przesuń pozycję rasteryzacji jakbyśmy byli czcionką bitmapową
        // (potrzebne tylko jeśli chcesz wyliczyć długość tekstu)
        // glBitmap(0, 0, 0, 0, face->glyph->advance.x >> 6, 0, NULL);
        // Zakończ listę wyświetlania
    glEndList();
}

Następna funkcja, jaką zamierzamy napisać będzie używać make_clist do utworzenia zbioru list wyświetlania odpowiadających danemu plikowi czcionki i jej wysokości.

FreeType używa czcionek trutype, więc znajdź sobie jakieś pliki czcionek truetype, żeby nakarmić tą funkcję. Pliki czcionek trutype są bardzo powszechne, jest mnóstwo stron, z których możesz ściągnąć duże ilości najróżniejszych czcionek truetype za darmo.

void font_data::init(const char *fname, unsigned int h)
{
        // Zaalokuj trochę pamięci na identyfikatory tekstur
    textures = new GLuint[128];
    this->h=h;
        // Stwórz i zainicjuj bibliotekę FreeType.
    FT_Library library;
    if (FT_Init_FreeType(&library))
        throw std::runtime_error("FT_Init_FreeType failed");
        // Obiekt, w którym FreeType trzyma informacje
        // o danej czcionce nazywa się "face".
    FT_Face face;
        // Tutaj ładujemy czcionkę z pliku.
        // Ze wszystkich miejsc, w których program może się wykrzaczyć,
        // to jest najbardziej prawdopodobne, jako że FT_New_Face
        // wywali błąd jeżeli plik czcionki nie istnieje
        // lub w jakiś sposób jest uszkodzony.
    if(FT_New_Face(library, fname, 0, &face))
        throw std::runtime_error("FT_New_Face failed (prawdopodobnie Twoj plik czcionki jest uszkodzony)");
        // Z jakiś dziwnych powodów FreeType mierzy rozmiar
        // czcionek w jednostce równej 1/64 piksela.
        // Dlatego, by zrobić czcionkę wysoką na h pikseli,
        // potrzebujemy poprosić o czcionkę wysoką na h*64.
        // (h << 6 to ładniejsza wersja h*64)
    FT_Set_Char_Size(face, h << 6, h << 6, 96, 96);
        // Tutaj poprosimy OpenGL, żeby zaalokował zasoby
        // dla wszystkich tekstur i list wyświetlania,
        // których zaraz stworzymy.
    list_base = glGenLists(128);
    glGenTextures(128, textures);
        // Tutaj właśnie stwarzamy każdą listę wyświetlania czcionki
    for(unsigned char i=0; i < 128; i++)
        make_dlist(face, i, list_base, textures);
        // Nie potrzebujemy już danych zawartych w "face",
        // kiedy listy zostały już zrobione, więc zwolnimy powiązane zasoby
    FT_Done_Face(face);
        // Jak wyżej dla biblioteki renderującej czcionki
    FT_Done_FreeType(library);
}

Teraz potrzebna jest nam funkcje czyszcząca listy wyświetlania i powiązane z czcionką tekstury.

void font_data::clean()
{
    glDeleteLists(list_base, 128);
    glDeleteTextures(128, textures);
    delete [] textures;
}

Tutaj są dwie małe funkcje, które napiszemy w oczekiwaniu na naszą funkcję wypisującą. Funkcja wypisująca będzie chciała myśleć w koordynatach okna, więc będziemy musieli zmienić macierz rzutowania na taką, która będzie zapewni wyrażanie się w koordynatach okna.

Używamy dwóch bardzo użytecznych funkcji OpenGL - glGet do pobrania rozmiarów okna oraz glPush/PopAttrib by mieć pewność, że po wszystkim zostawimy ustawioną poprzednio macierz. Jeżeli nie jesteś obeznany z tymi funkcjami, to dobrze by było, żebyś poczytał o nich.

        // Prosta funkcja zmieniająca macierz projekcji,
        // która sprawi, że koordynaty świata będą identyczne
        // z koordynatami okna
inline void pushScreenCoordinateMatrix()
{
    glPushAttrib(GL_TRANSFORM_BIT);
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    gluOrtho2D(viewport[0], viewport[2], viewport[1], viewport[3]);
    glPopAttrib();
}
        // Czyści macierz projekcji bez zmieniania aktualnej macierzy OpenGL
[translator]Oryginał może lepiej to odda: [i]Pops The Projection Matrix Without Changing The Current MatrixMode.[/i][/translator]
inline void pop_projection_matrix()
{
    glPushAttrib(GL_TRANSFORM_BIT);
    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glPopAttrib();
}

Nasza funkcja wypisująca wygląda dość podobnie do tej z lekcji 13, ale jest parę ważnych zmian. Flagi stanu włączenia (enable flags) OpenGL, które ustawiliśmy są inne, co odzwierciedla fakt, iż używamy tutaj dwukanałowych tekstur zamiast bitmap. Dodatkowo zajmujemy się obsługą nowych linii tekstu. Ponieważ jesteśmy dobrymi samarytanami, ostrożnie obchodzimy się ze stosami macierzy i atrybutów OpenGL, żeby być pewnym, że funkcja odkręci wszystkie zmiany w zastanym stanie OpenGL jakie zrobi (dzięki temu osoba używająca funkcji nie powie pewnego dnia - Hmm... Macierz modelu w jakiś tajemniczy sposób uległa zmianie.).

        // Coś jak funkcja glPrint od NeHe, ale zmodyfikowana
        // do działania z czcionkami FreeType
void print(const font_data &ft_font, float x, float y, const char *fmt, ...)
{
        // Chcemy posługiwać się koordynatami okna
    pushScreenCoordinateMatrix();

    GLuint font = ft_font.list_base;
        // Zwiększymy trochę wysokość, by był jakiś odstęp między liniami
    float h = ft_font.h/.63f;
    char text[256];         // Nasz tekst
    va_list    ap;         // Wskaźnik do listy argumentów
    if(fmt == NULL)         // Jeżeli nie ma tekstu
        *text=0;         // nic nie rób
    else
    {
        va_start(ap, fmt);         // Parsuje string w poszukiwaniu zmiennych
        vsprintf(text, fmt, ap);         // i konwertuje symbole na liczby
        va_end(ap);         // rezultaty są przechowywane w tekście
    }
        // Tu jest kod dzielący dany nam tekst na zbiór linii.
        // Byłoby schludniej użyć biblioteki do wyrażeń regularnych
        // jak jedna z dostępnych z boost.org (zrobiłem to z palca,
        // by nie komplikować tego tutoriala przez zbędne zależności).
    const char *start_line = text;
    vector<string> lines;
    for(const char *c = text; *c ; c++)
        if(*c=='\n')
        {
            string line;
            for(const char *n = start_line; n < c; n++)
                line.append(1, *n);
            lines.push_back(line);
            start_line=c+1;
        }
    if(start_line)
    {
        string line;
        for(const char *n = start_line; n < c; n++)
            line.append(1, *n);
        lines.push_back(line);
    }
    glPushAttrib(GL_LIST_BIT | GL_CURRENT_BIT | GL_ENABLE_BIT | GL_TRANSFORM_BIT);
    glMatrixMode(GL_MODELVIEW);
    glDisable(GL_LIGHTING);
    glEnable(GL_TEXTURE_2D);
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glListBase(font);

Ponieważ używamy oteksturowanych quadów, to każda zmiana macierzy modelu przed wywołaniem naszego glCallLists będzie zastosowana na tekście. Znaczy to, że jest możliwość obracania lub skalowania tekstu (pewna przewaga nad używaniem bitmap WGL). Najbardziej naturalny sposób na wykorzystanie tej zalety to zostawienie macierzy modelu w spokoju, zatem pozostawienie stanu transformacji przed wywołaniem funkcji wypisującej będzie miało wpływ na tekst. Jednak ze względu na używanie macierzy modelu do ustalania pozycji czcionki, nie będzie to działać. Naszą następną najlepszą opcją jest zachowanie kopii wprowadzonej macierzy modelu i zastosowaniem jej między glTranslate a glCallLists. Jest to wystarczająco proste do zrobienia, ale ponieważ rysujemy tekst używając specjalnej macierzy projekcji uzyskane efekty mogą się różnić od oczekiwanych - wszystko będzie zinterpretowane w skali pikseli. Moglibyśmy dać sobie radę z tym problemem, gdybyśmy nie resetowali macierzy projekcji wewnątrz funkcji print. W niektórych sytuacjach jest to dość dobry pomysł, jeżeli spróbujesz osiągnąć to w ten sposób nie zapomnij o przeskalowaniu czcionek do właściwych wymiarów (często są około 32x32 a Ty prawdopodobnie będziesz chciał coś rzędu 0.01x.0.01).

    float modelview_matrix[16];
    glGetFloatv(GL_MODELVIEW_MATRIX, modelview_matrix);
        // Właśnie w tym miejscu tekst zostaje wyświetlony.
        // Dla każdej linii tekstu resetujemy macierz modelu,
        // by tekst linii zaczynał się na właściwych pozycjach.
        // Zauważ, że potrzebujemy resetować macierz zamiast
        // po prostu przesuwać w dół o h. Dzieje się tak,
        // ponieważ gdy znak jest rysowany to aktualna macierz
        // jest modyfikowana tak by następny znak został
        // narysowany zaraz za nim.
    for(int i=0; i < lines.size(); i++)
    {
        glPushMatrix();
        glLoadIdentity();
        glTranslatef(x, y-h*i, 0);
        glMultMatrixf(modelview_matrix);
        // Zakomentowane linie mogą być użyteczne,
        // jeżeli potrzebujesz znać długość tworzonego tekstu.
        // Jeżeli zdecydujesz się ich użyć, to odkomentuj
        // komendę glBitmap w make_dlist().
        //glRasterPos2f(0, 0);
        glCallLists(lines[i].length(), GL_UNSIGNED_BYTE, lines[i].c_str());
        //float rpos[4];
        //glGetFloatv(GL_CURRENT_RASTER_POSITION, rpos);
        //float len = x-rpos[0];         // (przy założeniu, że nie było żadnych obrotów)
        glPopMatrix();
    }
    glPopAttrib();
    pop_projection_matrix();
}
}         // Zamyka przestrzeń nazw

Biblioteka jest już ukończona. Otwórz lesson13.cpp, bo wprowadzimy parę drobnych zmian, żeby zaprezentować właśnie napisane funkcje.

Pod innymi nagłówkami dodaj nagłówek freetype.h.

#include "freetype.h"         // Nagłówek naszej małej biblioteki do obsługi czcionek

No i skoro tu już jesteśmy, stwórzmy globalny obiekt font_data.

        // Tutaj są wszystkie informacje o czcionce, którą utworzymy
freetype::font_data our_font;

Teraz musimy przetestować tworzenie i usuwanie zasobów dla naszej czcionki. Dodaj następującą linię na koniec InitGL:

our_font.init("Test.ttf", 16);         // Utwórz czcionkę FreeType

Oraz dodaj tą linię na początek KillGLWindow, żeby usunąć czcionkę gdy już skończymy.

Potrzebujemy zmodyfikować funkcję DrawGLScene, żeby używała funkcji wypisującej. Mogło to być tak proste jak dodanie pojedynczej komendy "hello world" na końcu funkcji, ale zechciałem być trochę bardziej kreatywny i pokazać działanie obrotów i skalowania.

int DrawGLScene(GLvoid)         // Tutaj wszystko rysujemy
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);         // Czyści bufor ekranu i głębokości
    glLoadIdentity();         // Resetuje aktualną macierz modelu
    glTranslatef(0.0f, 0.0f, -1.0f);         // Przesuń o jedną jednostkę do ekranu
        // Niebieski tekst
    glColor3ub(0, 0, 0xFF);
        // Umieszczenie tekstu WGL na ekranie
    glRasterPos2f(-0.40f, 0.35f);
    glPrint("Aktywna tekstowa bitmapa WGL z NeHe - %7.2f", cnt1);         // Wypisz tekst na ekran
        // Tutaj my narysujemy jakiś tekst z naszą czcionką FreeType.
        // Jedyną naprawdę istotną komendą jest wywołanie print(),
        // ale żeby rezultaty były odrobinę bardziej interesujące
        // umieściłem kod obracający i skalujący tekst.
        // Czerwony tekst
    glColor3ub(0xFF, 0, 0);
    glPushMatrix();
    glLoadIdentity();
    glRotatef(cnt1, 0, 0, 1);
    glScalef(1, .8+.3*cos(cnt1/5), 1);
    glTranslatef(-180, 0, 0);
    freetype::print(our_font, 320, 200, "Aktywny tekst FreeType - %7.2f", cnt1);
    glPopMatrix();
        // Odkomentuj to by przetestować obsługę nowych linii.
        // freetype::print(our_font, 320, 200, "Tutaj\ntam\nbeda\n\nnowe linie\n.", cnt1);
    cnt1+=0.051f;         // Zwiększ pierwszy licznik
    cnt2+=0.005f;         // Zwiększ drugi licznik
    return TRUE;         // Wszystko dobrze poszło
}

Ostatnią rzeczą do zrobienia jest dopisanie obsługi wyjątków. Idź do WinMain (bądź main) i dopisz blok try{...} na początku funkcji.

    MSG msg;         // Struktura komunikatów Windows
    BOOL done=FALSE;         // Zmienna boolowska do wyjścia z pętli
    try         // Obsłużenie wyjątków
    {

Teraz dodaj na końcu funkcji blok catch(){...}.

        // Wyłączanie
    KillGLWindow();         // Usuwamy okno
        // Złap każdy wyjątek jaki został rzucony
    }
    catch(std::exception &e)
    {
        MessageBox(NULL, e.what(), "Zlapano wyjatek", MB_OK | MB_ICONINFORMATION);
    }
    return (msg.wParam);         // Koniec programu
}

Jeżeli kiedykolwiek trafimy na wyjątek to otrzymamy małe okienko dialogowe wyjaśniające co się stało. Zauważ, że obsługa wyjątków może spowolnić Twój kod, więc jeżeli już będziesz wydawał ostateczną wersję swojego programu to możesz wyłączyć obsługę wyjątków.

To wszystko! Skompiluj program, powinieneś zobaczyć ładny tekst wyrenderowany przez FreeType, poruszający się pod oryginalnym bitmapowym tekstem z lekcji 13.

Uwagi ogólne

Jest wiele ulepszeń, które mógłbyś dodać do tej biblioteki. Na przykład używanie obiektów czcionek bezpośrednio jest trochę niezgrabne, więc mógłbyś zrobić jakiś manager czcionek. Mógłbyś również, tak jak w samym OpenGL, stworzyć stos czcionek, który pozwoliłby Ci uniknąć posługiwania się referencjami do obiektów czcionek każdego razu, gdy wywołujesz funkcję print. (To są wszystkie rzeczy jakie aktualnie robię we własnym kodzie, ale zdecydowałem się pozostawić tutorial prostym.) Może również chciałeś zrobić wersję funkcji wypisującej, która wycentrowuje tekst, żeby to zrobić możesz potrzebować technik opisanych niżej.

Mam tekst obracający się wokół jego środka. Jednakże, by osiągnąć taki efekt dla dowolnego tekstu, musisz wiedzieć dokładną długość linii tekstu - to może być odrobinę skomplikowane. Jedynym sposobem, żeby otrzymać długość tekstu jest umieszczenie komend glBitmap w listach wyświetlania czcionek, po kolei modyfikować pozycję rasteryzacji tak jak macierz modelu (pozostawiłem niezbędny kod, ale zakomentowany). Wtedy możesz ustawić pozycję rasteryzacji na x, y zanim wywołasz glCallLists i użyć glGet, żeby otrzymać pozycję rasteryzacji po narysowaniu tekstu - różnica między pozycjami rasteryzacji to długość tekstu w pikselach.

[T Takie potrzaskane metody wynikają z używania list wyświetlania. Według mnie najlepiej we własnej obsłudze FreeType'a pozbyć się list wyświetlania. T]Pamiętaj, że czcionki FreeType używają znacznie więcej pamięci niż bitmapowe czcionki (to jest ich przewaga - zajmują znacznie mniej). Jeżeli z jakiś powodów potrzebujesz oszczędzać pamięć karty graficznej, możesz skorzystać z kodu z lekcji 13.

Inną korzyścią używania oteksturowanych quadów do rysowania tekstu są właśnie te quady. Inaczej niż bitmapy, quady świetnie współpracują z funkcjami wybierania OpenGL (zobacz lekcję 32). Ułatwia to tworzenie tekstu, który reaguje na trzymanie kursora myszy nad nim lub na kliknięcie w niego. (Zrobienie bitmapowych czcionek WGL, które będą dobrze współgrać z funkcjami wybierania jest możliwe, znów należy użyć pozycji rasteryzacji do zdobycia długości tekstu w pikselach.)

No i wreszcie są gotowe biblioteki do obsługi czcionek w OpenGL, poniżej znajdują się linki do ich stron. W zależności od Twoich celów i kompilatora, może będziesz chciał używać jedną z nich zamiast tego kodu (jest ich znacznie więcej, ale generalnie dołączyłem te, z którymi miałem jakieś doświadczenia).

GLTT Ta biblioteka jest stara i wygląda na to, że już nie jest rozwijana, ale zbierałą pewne bardzo pozytywne opinie. Bazuje na FreeType1. Myślę, że potrzebowałbyś starych źródeł FreeType1, by skompilować ją. Możesz ściągnąć ją z http://www.opengl.org/developers/faqs/technical/fonts.htm

OGLFT Fajna biblioteka bazująca na FreeType2. Wygląda na przeznaczoną dla Linuksowych maszyn. http://oglft.sourceforge.net/

FTGL Trzecia biblioteka bazująca na FreeType, pisana z myślą o OS X, ale działa również z innymi systemami. http://homepages.paradise.net.nz/henryj/code/#FTGL

FNT Biblioteka nie bazująca na FreeType, będąca częścią PLIB. Zdaje się mieć przyjemny interfejs, używa własnego formatu czcionek. http://plib.sourceforge.net/fnt