Lekcja 13. Czcionki rastrowe
Autor: Stanisław Michał 'black_maq' Warych
Oryginał: Bitmap Fonts (Jeff 'NeHe' Molofee)
Źródła: http://nehe.gamedev.net/data/lessons/vc/lesson13.zip

Witamy w kolejnym tutorialu. Tym razem nauczę was jak wykorzystać czcionki rastrowe (ang. według NeHe - Bitmap Fonts).

Pewnie się pytasz samego siebie "co jest trudnego w wyświetlaniu tekstu na ekranie ?". Jeżeli nigdy tego nie robiłeś, to nie jest proste ! Upewnij się, że masz odpowiedni program, zrób napis na obrazku, załaduj obraz do OpenGL, włącz blending i zmapuj tekst na ekran. Ale to jest czasochłonne, końcowy efekt jest rozmazany lub "brylasty" w zależności od rodzaju filtrowania jakiego użyjesz i dopóki twój obraz ma kanał alpha twój tekst będzie przezroczysty (zmieszany z innymi obiektami na ekranie), jeżeli jest odwzorowany na ekranie. Jeśli kiedykolwiek używałeś Wordpad-a, Word-a albo inne aplikacje tekstowe to pewnie widziałeś różne czcionki. Ta lekcja nauczy Cię jak użyć czcionki w naszym programie OpenGL. Każda czcionka jaką masz w komputerze, możesz użyć w programie OpenGL. Czcionki rastrowe nie tylko wyglądają 100 razy lepiej od tych tworzonych na obrazku ale także można je zmieniać w locie i nie trzbe tworzyć dla każdego słowa lub litery osobnego obrazka. Po prostu używasz mojej włąsnoręcznej komendy do wyświetlenia tekstu na ekranie.

Starałem się stworzyć komendę prostą jak to tylko możliwe. Wszystko co musisz zrobić to napisać glPrint("Hello"). Możesz zmieniać tekst w locie. Mogę długo mówić, że jestem zadowolony z tej lekcji. Zajęło mi to okołó 1,5 godziny aby stworzyć program. Dlaczego tak długo? Ponieważ nie ma informacji o tworzeniu czcionek rastrowych. [obecnie to się troche zmieniło :)] W celu otrzymania prostego kodu zdecydowałem, że będzie miło, gdy napiszę wszystko prosto, aby zrozumieć kod C :).

Mała adnotacja, ten kod jest pod system Windows. Korzysta z windowsowych funkcji wgl, aby zbudować czcionkę. Oczywiście Apple ma agl, które robi to samo oraz X ma glx. Niestety nie mogę zagwarantować, że ten kod jest przenośny. Jeśli ktoś ma kod rysujący czcionki na ekranie niezależny od systemu, to niech mnie poinformuję a ja napiszę następną lekcję.

Zaczniemy od typowego kody z lekcji 1. Dodamy plik nagłówkowy stdio.h dla standardowych operacji wejścia/wyjścia; nagłówek stdarg.h aby zrobić trekst i skonwertować zmienne do niego i w końcu nagłówek math.h, ponieważ będziemy używać sinusa i cosinusa.

#include <windows.h>         // Plik nagłówkowy Windows
#include <math.h>         // Plik nagłówkowy dla operacji matematycznych    ( NOWE )
#include <stdio.h>         // Plik nagłówkowy dla operacji wejścia/wyjścia    ( NOWE )
#include <stdarg.h>         // Nagłówek dla zmiennych argumentów funkcji    ( NOWE )
#include <gl\gl.h>         // Plik nagłówkowy biblioteki OpenGL
#include <gl\glu.h>         // Plik nagłówkowy biblioteki GLu32
#include <gl\glaux.h>         // Plik nagłówkowy biblioteki GLaux
HDC        hDC=NULL;         // Kontekst urządzenia
HGLRC        hRC=NULL;         // Kontekst renderujący
HWND        hWnd=NULL;         // Przechowuje uchwyt okna
HINSTANCE    hInstance;         // Przechowuje instancję okna (ang. Instance)

Dodamy teraz 3 zmienne, base przechowuje numer pierwszej listy wyświetlania (ang. display list), którą tworzymy.

Każdy znak potrzebuje jego własną listę wyświetlania. Znak "A" jest 65 na liście, "B" - 66, a "C" - 67 itd. Czyli "A" powinno być przechowywane na liście wyświetlania base+65.

Następnie dodajemy dwa liczniki (cnt1 & cnt2), te liczniki będą liczyć różne wartości i są używane do poruszania tekstu po ekranie za pomocą SIN i COS. To tworzy pół-losowy ruch na ekranie. Użyjemy także liczników do kontroli kolorów literek (więcej o tym w dalszej części).

GLuint    base;         // Base Display List For The Font Set
GLfloat    cnt1;         // Pierwszy licznik do poruszania i kolorowania tekstu
GLfloat    cnt2;         // Drugi licznik do poruszania i kolorowania tekstu
bool    keys[256];         // Tablica używana do obsługi klawiatury
bool    active=TRUE;         // Zmienna aktywności okna ustawiona wstępnie na TRUE
bool    fullscreen=TRUE;         // Tryb pełnoekranowy wstępnie ustawiony na TRUE
LRESULT    CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);         // Deklaracja WndProc (więcej w tekstach o WinApi)

Następny kawałek kodu buduje bieżącą czcionkę. To była najtrudniejsza część kodu do napisania. 'HFONT font' w prostym znaczeniu informuje Windows, że mamy zamiar manipulować czcionkami. Oldfont jest użyte dla lepszego zagospodarowania.

Następnie definiujemy base. Robimy to tworząc grupę 96-ciu list wyświetlania używając glGenLists(96). Następnie, gdy listy są już stworzone, zmienna base będzie przechowywać numer pierwszej listy.

GLvoid BuildFont(GLvoid)         // Buduje naszą bitmapową czcionkę
{
    HFONT    font;         // Identyfikator czicionki windowsowej
    HFONT    oldfont;         // Użyte dla Good House Keeping
    base = glGenLists(96);         // Przechowuje 96 znaków        ( NOWE )

Następnie tworzymy naszą czcionkę. Zaczynamy podając rozmiar czcionki. Pewnie zauważyłeś, że jest to liczba ujemna. Wstawiając minus mówimy windowsowi żeby znalazł nam czcionkę bazującą na wysokości znaku. Jeżeli znak byłby dodatni, czcionka byłaby szukana na podstawie wysokości komórki (plamki).

font = CreateFont(        -24,         // Wysokość czcionki            ( NOWE )

Następnie określamy szerokość plamki. Ustawiając wartość na 0 określamy, że windows użyje standardowego ustawienia.

Dla własnych potrzeb możesz pokombinować z tą wartością dla uzyskania różnych efektów.

                0,         // Średnia szerokość znaku

Kąt pochylenia będzie obracał czcionke. Niestety to nie jest zbyt przydatna funckja. Różne kombinacje z tym parametrem, prowadzą zazwyczaj do tego, że czcionki są przycięte.

                0,         // Kąt pochylenia
                0,         // Kąt orientacji względem linii bazowej

Grubość jest imponującym komponentem. Przyjmuje wartości od 0 do 1000 lub można wykorzystać wartość predefinowaną. FW_DONTCARE - 0, FW_NORMAL - 400, FW_BOLD - 700, FW_BLACK - 900. Jest więcej tych wartości ale te 4 wystarczą na początek. Im wyższa wartość tym grubsza czcionka (bardziej tłusta).

                FW_BOLD,         // Waga czcionki

Kursywa, podkreślenie, czy pogrubienie mogą mieć wartości TRUE lub FALSE. Przykładowo gdy podkreślenie ma wartość TRUE, czcionka jest podkreślona. Jeżeli będzie FALSE, to nie będzie podkreślone. Całkiem proste :).

                FALSE,         // Kursywa
                FALSE,         // Podkreślenie
                FALSE,         // Pogrubienie

Dzięki identyfikatorowi znaków możemy ustalić jaki typ znaków chcemy mieć. Jest bardzo wiele typów. CHINESEBIG5_CHARSET, GREEK_CHARSET, CHINESEBIG5_CHARSET, GREEK_CHARSET itd. ANSI jest jedynym jaki używam, jednakże DEFAULT będzie prawdopodobnie tak samo działał. Jeżeli chcesz używać takich czcionek jak Webdings lub Wingdings, musisz użyć SYMBOL_CHARSET zamiast ANSI_CHARSET.

                ANSI_CHARSET,         // Identyfikator zbioru znaków

Dokładność prezentacji jest bardzo ważna. Przekazuje to informacje jaki typ znaków wybrać jeżeli jest dostępny więcej niż jeden. OUT_TT_PRECIS przekazuje Windowsowi, że w przypadku, gdy jest jeden niż więcej typów to ma wybrać TRUETYPE. Czcionki Truetype zawsze wyglądają lepiej, szczególnie przy powiększaniu ich. Możesz także użyć OUT_TT_ONLY_PRECIS, co zawsze próbuje użyć czcionki typu Truetype.

                OUT_TT_PRECIS,         // Dokładność prezentacji

Dokładność przycięcia jest typem przycięcia do wykonania na czcionce. Nie za dużo jest do powiedzenia na ten temat, po prostu ustawmy na default.

                CLIP_DEFAULT_PRECIS,         // Dokładność przycięcia

Jakość prezentacji jest bardzo ważna. Można użyć PROOF, DRAFT, NONANTIALIASED, DEFAULT lub ANTIALIASED. Dobrze wiemy, że ANTIALIASED wygląda dobrze :). Antialiasing czcionek jest takim samym efektem jakby w ustawieniach czcionek windows włączyć ich wygładzanie (ang. smoothing). Powoduje, że czcionki są mniej "wyszczerbione".

                ANTIALIASED_QUALITY,         // Jakość prezentacji

Następnie mamy ustawienia rodziny czcionek i podziałka (ang. pitch). Podziałka może mieć wartości DEFAULT_PITCH, FIXED_PITCH i VARIABLE_PITCH, a rodzina ;) FF_DECORATIVE, FF_MODERN, FF_ROMAN, FF_SCRIPT, FF_SWISS, FF_DONTCARE. Jeżeli chcesz znać ich dokładnie działanie to pokombinuj z nimi, ja ustawiam je obie na default.

                FF_DONTCARE|DEFAULT_PITCH,         // Rodzina i podziałka czcionki

W końcu mamy nazwę czcionki. Czcionki jakie Ciebie interesują możesz znaleźć w jakimś edytorze tekstu i tylko podmienić jej nazwę.

                "Courier New");         // Nazwa czcionki

oldFont przechowuje poprzednio używaną czcionkę. SelectObject zwraca czcionkę (lub jakikolwiek obiekt GDI). Następnie budujemy czcionkę, wybieramy naszą czcionkę i za pomocą DeleteObject kasujemy już nie używaną czcionkę.

    oldfont = (HFONT)SelectObject(hDC, font);         // Wybiera czcionkę jaką chcemy
    wglUseFontBitmaps(hDC, 32, 96, base);         // Buduje 96 znaków zaczynając od znaku 32
    SelectObject(hDC, oldfont);         // Wybiera czcionkę jaką chcemy
    DeleteObject(font);         // Kasuje czcionkę
}

Następny kod jest całkiem prosty. Kasujemy 96 list wyświetlania zaczynając od pierwszej, określonej w base. Lepiej jest samemu to zrobić niż łudzić się, że windows to zrobi za nas :).

GLvoid KillFont(GLvoid)         // Kasuje listy czcionki
{
    glDeleteLists(base, 96);         // Kasuje wszystkie 96 znaków        ( NOWE )
}

Teraz przydatna, świetna procedura. Procedurę tą wywołujemy glPrint("Tekst"); tekst jest przechowywany w zmiennej *fmt.

GLvoid glPrint(const char *fmt, ...)         // Własna procedura GL "Print"
{

Pierwcza linia poniżej tworzy przechowalnię dla 256 znaków, text jest zmienną gdzie będzie przechowywany tekst do wyświetlenia. Druga linia tworzy wskaźnik do listy argumentó, który przekażemy dalej razem ze stringiem. Jeżeli przekażemy jakieś zmienne razem z teksem to je przechwycimy.

    char        text[256];         // Przechowuje stringa
    va_list        ap;         // Wskaźnik do listy argumentów

Następne dwie linijki kodu sprawdzają czy jest cokolwiek do wyświetlenia. Jeżeli nie ma tekstu (fmt=NULL) to nic nie zostanie wyświetlone na ekranie.

    if (fmt == NULL)         // Jeżeli nie ma tekstu
        return;         // nie robi nic

Następne trzy linikji konwertują jakiekolwiek symbole w tekście na rzeczywiste numery, które reprezentują dane znaki. Aktualny tekst i skonwertowane symbole są przechowywane w stringu o nazwie "text". Wytłumaczę symbole bardziej szczegółowo poniżej.

    va_start(ap, fmt);         // Przeszukje string w celu znalezienia zmiennych
     vsprintf(text, fmt, ap);         // Konwertuje symbole na aktualne numery
    va_end(ap);         // Rezultaty są przechowywane w zmiennej text

Następnie wrzucamy listę na stos, to chroni glListBase przed innymi listami, które możemy wykorzystywać w naszym programie.

Komenda glListBase(base-32) jest trochę trudna do wyjaśnienia. Powiedzmy, że rysujemy literę 'A', która jest reprezentowana przez numer 65. Bez glListBase(base-32) OpenGL nie wiedziałby, gdzie znaleźć tą literę. Powinno się szukać tego na liście 65, ale jeśli base ma 1000 to 'A' będzie przechowywane na liście 1065. Więc ustalając punkt startowy, OpenGL wie, skąd brać właściwą listę. Powodem dla, którego jest to, że odejmujemy 32 jest to, że nigdy nie wykorzystujemy pierwszych 32 list. Omijamy je. Musimy o tym poinformować OpenGL odejmując 32 z wartości base. Mam nadzieję, że to jest zrozumiałe.

    glPushAttrib(GL_LIST_BIT);         // Wrzucamy listę na stos        ( NOWE )
    glListBase(base - 32);         // Odejmuje pierwsze 32 znaki        ( NOWE )

Teraz OpenGL wie, gdzie znajdują się litery, możemy mu kazać wyświetlić tekst na ekranie, glCallLists jest bardzo interesującą komendą. Jest zdolna do wrzucania na ekran więcej niż jedną listę wyświetlania w tym samym czasie.

Linie poniżej są kontynuacją. Pierwsza mówi OpenGL, że zamierzamy wyświetlać listy na ekranie, dzięki strlen(text) wiemy ile znaków ma tekst. Następnie musimy się dowiedzieć jaki jest najwyższy numer listy, która będzie wysłana. Nie wysyłamy więcej niż 255 znaków. Parametr listy jest traktowany jako tablica unsigned byte, każda od 0 do 255. W końcu informujemy co wyświetlać podając tekst (wskaźnik do string-a).

Pewnie się zastanawiasz dlaczego litery nie pojawiają się jedna na drugiej ? Otóż każda lista wyświetlania wie, gdzie jest prawa strona litery. Po narysowaniu litery OpenGL przechodzi na prawą stronę narysowanej litery. Następna litera lub dowolny obiekt będą rysowane po prawej stronie poprzedniego obiektu.

W końcu zdejmujemy listę ze stosu ustawiając OpenGL tak jak był zanim użyliśmy glListBase(base-32).

    glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);         // Rysuje nasz tekst            ( NOWE )
    glPopAttrib();         // Zdejmuje ze stosu naszą listę    ( NOWE )
}

Jedyną rzeczą zmienioną w Init jest dodanie linii BuildFont(). To przeskakuje do kodu budującego naszą czcionkę, aby ją później wykorzystać.

int InitGL(GLvoid)         // Ustawienia OpenGL
{
    glShadeModel(GL_SMOOTH);         // Włącza wygładzane cieniowanie (ang. Smooth Shading)
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f);         // Czarne tło
    glClearDepth(1.0f);         // Ustawienia bufora głębi
    glEnable(GL_DEPTH_TEST);         // Włącza testowanie głębi
    glDepthFunc(GL_LEQUAL);         // Typ testowania głębi
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);         // Ładna perspektywa
    BuildFont();         // Buduje czcionkę
    return TRUE;         // Inicjalizacja poszła OK
}

Czas na kod rysujący. Zaczniemy od wyczyszczenia ekranu i bufora głębi. Przywołamy glLoadIdentity() aby zresetować wszystko. Następnie przekształcimy jedną jednostkę "w ekran". Jeśli tego nie zrobimy to tekst się nie pojawi. Czcionki rastrowe wyglądają lepiej, kiedy użyjemy rzutowania ortograficznego niż rzutowania z perspektywy, ale dlatego, że ortograficzne źle wygląda więc to przekształcimy.

Pewnie zauważyłeś, że jeśli przekształcisz głębiej w ekran to rozmiar czcionek nie skurczy się tak jakbyś tego oczekiwał. Co się stanie, gdy będziesz przekształcał głębiej będziesz miał większą kontrolę nad tym, gdzie tekst jest na ekranie. Jeśli przekształcisz 1 jednostkę w ekren to możesz umieścić tekst gdziekolwiek z przedziału -0.5 do +0.5 na osi X. Jeśli przekształcisz 10 jednostek w ekran to tekst będzie mógł być od -5 do +5. To Ci daje po prostu większą kontrolę zamiast używania licz dziesiętnych do pozycjonowania tekst. Nic się nie zmieni w rozmiarze tekstu, nawet glScalef() nie nie wskura. Jeśli chcesz czcionkę mniejszą lub większą, to zrób ją większą lub mniejszą przy jej tworzeniu !

int DrawGLScene(GLvoid)         // Tutaj rysujemy
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);         // Wyczyść ekran i bufor głębi
    glLoadIdentity();         // Zresetuj widok
    glTranslatef(0.0f,0.0f,-1.0f);         // Przenosimy się 1 jednostkę w ekran

Teraz użyjemy trochę matmy, aby zrobić pulsujące kolory. Nie martw się jeśli nie rozumiesz co robię. Lubię korzystać z tak wielu zmiennych i głupich trików ile tylko mogę, aby osiągnąć rezultat :).

W tym przypadku używam dwóch liczników, aby poruszać tekstem po ekranie i zmieniać jego kolory między czerwonym, zielonym i niebieskim. Czerwony idzie od -1.0 do 1.0 używając cosinusa i licznika numer 1. Zielony także jest od -1.0 do 1.0 używając sinusa i licznika numer 2. Niebieski jest od 0.5 do 1.5 używając cosinus i licznika 1 i 2. Tym sposobem niebiski nigdy nie będzie 0 i tekst nigdy nie powinien być kompletnie wyblakły. Głupie, ale działa :).

        // Pulsujące kolory w zależności od pozycji tekstu
    glColor3f(1.0f*float(cos(cnt1)),1.0f*float(sin(cnt2)),1.0f-0.5f*float(cos(cnt1+cnt2)));

Teraz nowa komenda. glRasterPos2f(x,y) umieszcza czcionke na ekranie. Środek ekranu to 0.0. Zauważ, że nie ma pozycji Z. Czcionki rastrowe używają tylko osi X (lewo/prawo) i Y (góra/dół). Ponieważ przekształciliśmy jedną jednostkę w ekran, lewa granica jest -0.5, a prawa +0.5. Zauważysz, że przesunąłem 0.45 pikseli w lewo na osi X. To przeniosło tekst w środek ekranu. Inaczej byłby bardziej na prawo, ponieważ był rysowany ze środka na prawo.

Wymyślna(?) matematyka będzie taka sama jak w matematyce kolorków. To porusza tekst na osi X od -0.50 do -0.40 (pamiętaj, że odjęliśmy 0.45 na początku). Tekst znajduje się cały czas na ekranie. Balansuje na lewo i prawo używając cosinusa i licznika numer 1. Porusza się także na osi Y od -0.35 do +0.35 używając sinusa i licznika numer 2.

        // Pozycja tekstu na ekranie
    glRasterPos2f(-0.45f+0.05f*float(cos(cnt1)), 0.35f*float(sin(cnt2)));

Teraz moja ulubiona część...Pisanie aktualnego tekstu na ekranie. Starałem się zrobić to bardzo prostym i bardzo przyjaznym dla użytkownika. Zauważysz, że to wygląda jak funkcja OpenGL połączona z dobrym, starym Print :). Wszystko co musisz zrobić aby wyświetlić tekst jest napisanie glPrint("tekst jaki chcesz"). To jest proste. Tekst zostanie narysowany na ekranie w punkcie, gdzie go umieścisz.

Shawn T. wysłał mi zmodyfikowany kod, który pozwala wyświetlać zmienne na ekranie. To znaczy, że możesz zwiększać licznik i wyświetlać wynik na ekranie! To działa tak...W linii poniżej zobaczysz nasz normalny tekst. Wtedy jest przerwa, myślnik, przerwa, symbol (%7.2f). Teraz możesz popatrzeć na %7.2 i powiedzieć co to do licha znaczy ? To jest bardzo proste. % jest jak znacznik mówiący nie pisz 7.2f na ekranie, ponieważ to reprezentuje zmienną. 7.2 oznacza, że maksymalnie zostanie wyświetlonych 7 cyfr, a po kropce 2. W końcu f oznacza, że wyświetlana zmienna jest typu float.

Chcemy wyświetlić wartość cnt1 na ekranie. Na przykład, jeśli cnt1 był równy 300.12345f numer jaki zobaczymy na ekranie będzie 300.12. 3, 4 i 5 po kropce będą odcięte, ponieważ chcemy aby tylko dwie cyfry się pojawiły. Zdaję sobie sprawę, że jeżeli jesteś doświadczonym programistą to dla Ciebie jest to błahostka, ale być może czytają to ludzie, któzy nigdy nie używalo printf. Jeżeli chcesz wiedzieć więcej o symbolach, kup książkę lub poczytaj na MSDN.

    glPrint("Active OpenGL Text With NeHe - %7.2f", cnt1);         // Wyświetl tekst GL na ekranie

Ostatnią rzeczą do zrobienia jest zwiększenie dwóch liczników różnymi wartościami, aby kolory pulsowały, a tekst się poruszał.

    cnt1+=0.051f;         // Zwiększa pierwszy licznik
    cnt2+=0.005f;         // Zwiększa drugi licznik
    return TRUE;         // Wszystko poszło OK
}

Ostatnią rzeczą do zrobienia jest dodanie KillFont() na końcu KillGLWindow() tak jak pokazuję to poniżej. Dodanie tej linii jest ważne. Wyczyszcza rzeczy zanim zakończymy program.

    if (!UnregisterClass("OpenGL",hInstance))         // Jesteśmy zdolni do odrejestrowania klasy ?
    {
        MessageBox(NULL,"Nie można odrejestrować klasy!","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        hInstance=NULL;         // Ustawia instancję na NULL
    }
    KillFont();         // Niszczy czcionkę
}

To już koniec...Wszystko co potrzebujesz wiedzieć w celu używania czcionek rastrowych w Twoim projekcie OpenGL. Szukałem w Internecie lekcji podobnej do tej i nic nie znalazłem. Być może moja strona jest pierwszą, która mówi więcej o tym zagadnieniu. W każdym razie, miłego korzystania z lekcji oraz przyjemnego kodzenia!

[

Na NeHe została użyta nazwa Bitmap Fonts, natomiast poprawne brzmienie nazwy po polsku to czcionki rastrowe, w sumie to po angielsku takźe używa się tej nazwy (ang. raster fonts) oraz w obecnym czasie to nie jest jedyne źródło informacji o czcionkach rastrowych, można je znaleźć także w książce "OpenGL programowanie gier"

]