Lekcja 14. Czcionki konturowe
Autor: Dominik 'Domas' Minta
Oryginał: Outline Fonts (Jeff 'NeHe' Molofee)
Źródła: http://nehe.gamedev.net/data/lessons/vc/lesson14.zip

Ten samouczek jest kontynuacją poprzedniej, 13. lekcji, podczas której nauczyłem Cię jak używać czcionek rastrowych. Teraz pokażę jak stosować czcionki konturowe (ang. outline fonts).

Sposób w jaki tworzymy czcionki konturowe jest podobny do tego, który stosowaliśmy przy tworzeniu czcionek rastrowych. Mimo wszystko czcionki konturowe są 100 razy lepsze! Możesz je skalować i przemieszczać w przestrzeni 3D. Mają grubość względem osi Z, więc koniec z płaskimi, dwuwymiarowymi znakami. Każdą czcionkę zainstalowaną na Twoim komputerze możesz zamienić w trójwymiarowy font dla OpenGL, który wraz z odpowiednimi wektorami normalnymi sprawia, że znaki błyszczą naprawdę pięknie gdy pada na nie światło.

Drobna uwaga: kod zawiera elementy specyficzne dla systemu Windows. Korzysta z funkcji wgl do tworzenia czcionki. Analogicznie, Apple posiada funkcje agl, a X ma glx. Niestety nie mogę zagwarantować przenośności tego kodu. Jeśli ktokolwiek posiada niezależny od platformy kod rysujący czcionki na ekranie, prosiłbym o przesłanie mi go, a wtedy napiszę kolejny samouczek.

Zaczniemy od typowego kodu z lekcji pierwszej. Dodamy plik nagłówkowy stdio.h do standardowych operacji wejścia/wyjścia; stdarg.h do parsowania tekstu i konwersji zmiennych na tekst oraz math.h, który zawiera funkcje sin i cos potrzebne do przemieszczania tekstu po ekranie.

#include <windows.h>         // Plik nagłówkowy Windows
#include <math.h>         // Plik nagłówkowy biblioteki funkcji matematycznych (DODANO)
#include <stdio.h>         // Plik nagłówkowy biblioteki standardowego wejścia/wyjścia (DODANO)
#include <stdarg.h>         // Plik nagłówkowy biblioteki funkcji o zmiennej ilości argumentów (DODANO)
#include <gl\gl.h>         // Plik nagłówkowy biblioteki OpenGL32
#include <gl\glu.h>         // Plik nagłówkowy biblioteki Glu32
#include <gl\glaux.h>         // Plik nagłówkowy biblioteki Glaux
HDC        hDC=NULL;         // Prywatny kontekst urządzenia GDI
HGLRC        hRC=NULL;         // Stały kontekst renderingu
HWND        hWnd=NULL;         // Przechowuje uchwyt naszego okna
HINSTANCE    hInstance;         // Przechowuje uchwyt (instancję) aplikacji

Dodamy dwie nowe zmienne. base będzie przechowywać numer pierwszej listy wyświetlania (ang. Display list), którą utworzymy. Każdy znak wymaga własnej listy. Litera 'A' jest 65. na liście, 'B' - 66., 'C' - 67. itd. Zatem 'A' będzie przechowywane na liście jako base+65.

Dalej, dodajemy zmienną rot. Będzie używana do obracania tekstu wokół ekranu używając funkcji sin i cos. Ponadto przyda się do pulsowania kolorów.

GLuint    base;         // Podstawowa lista wyświetlania dla zestawu znaków (DODANO)
Glfloat    rot;         // Używane do obracania tekstu (DODANO)
bool    keys[256];         // Tablica używana przy obsłudze klawiatury
bool    active=TRUE;         // Flaga określająca czy okno jest aktywne; ustawiona domyślnie na TRUE
bool    fullscreen=TRUE;         // Flaga określająca domyślną pracę programu na pełnym ekranie

Tablica GLYPHMETRICSFLOAT gmf[256] przechowuje informacje na temat położenia i orientacji każdej z 256 list wyświetlania czcionek konturowych. Literę wybieramy za pomocą gmf[num], gdzie num jest numerem listy wyświetlania o której chcemy wiedzieć więcej. Później pokażę jak znaleźć szerokość każdego znaku, by można było automatycznie wycentrować tekst na ekranie. Pamiętaj że każdy znak może mieć odmienną szerokość. Glyphmetrics uczyni nasze życie o wiele prostszym.

GLYPHMETRICSFLOAT    gmf[256];         // Przechowuje informacje o naszej czcionce
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);         // Deklaracja funkcji WndProc

Poniższa sekcja kodu tworzy czcionkę w sposób podobny do tego, który stosowaliśmy przy czcionce rastrowej. Podobnie jak w lekcji 13., ta sekcja była dla mnie najtrudniejsza do zrozumienia.

'HFONT font' przechowuje identyfikator (uchwyt) czcionki Windows.

Później inicjalizujemy zmienną base. Zrobimy to tworząc grupę 256 list wyświetlania używając glGenLists(256). Po ich utworzeniu, zmienna base będzie przechowywała numer pierwszej listy.

GLvoid BuildFont(GLvoid)         // Tworzy naszą czcionkę rastrową
{
    HFONT    font;         // Identyfikator (uchwyt) czcionki Windows
    base = glGenLists(256);         // Tworzy listy do przechowywania 256 znaków

Teraz zaczyna się prawdziwa zabawa! Utwórzymy naszą czcionkę konturową. Zaczniemy od określenia jej wysokości. Jak widzisz jest to liczba ujemna. Poprzez dodanie minusa powiadamiamy Windows by znalazł dla nas czcionkę pasującą do wysokości ZNAKU. Jeśli podamy liczbę dodatnią, zostanie dobrana czcionka o wysokości KOMÓRKI.

    font = CreateFont(    -12,         // Wysokość czcionki

Następnie określamy szerokość komórki. Jak widzisz jest ona równa 0, więc Windows użyje domyślnej wartości. Możesz oczywiście zmienić tę wartość, uczynić czcionkę szerszą itp.

                0,         // Szerokość czcionki

Kąt pochylenia obraca czcionkę. Kąt orientacji, cytując pomoc MSDN, określa kąt, podany w dziesiątkach stopni, pomiędzy linią bazową każdego znaku, a osią X urządzenia. Niestety nie mam pojęcia co to może znaczyć :(

                0,         // Kąt ucieczki
                0,         // Kąt orientacji

Stopień pogrubienia czcionki to wspaniały parametr. Możesz wpisać liczbę od 0 do 1000 lub użyć jedną z predefiniowanych wartości. FW_DONTCARE to 0, FW_NORMAL to 400, FW_BOLD to 700, FW_BLACK to 900. Istnieje jeszcze więcej predefiniowanych wartości, ale te 4 dają całkiem dużą różnorodność. Im wyższa wartość, tym grubsza jest czcionka.

                FW_BOLD,         // Stopień pogrubienia czcionki

Kursywa, podkreślenie, przekreślenie mogą mieć wartość TRUE lub FALSE. Jeśli podkreślenie ma wartość TRUE, czcionka będzie podkreślona, a jeśli FALSE ? nie będzie. Proste :)

                FALSE,         // Kursywa
                FALSE,         // Podkreślenie
                FALSE,         // Przekreślenie

Zestaw znaków opisuje typ znaków, który chcesz użyć. Jest tu dużo do wyjaśnienia. CHINESEBIG5_CHARSET, GREEK_CHARSET, RUSSIAN_CHARSET, DEFAULT_CHARSET, itd. Ja użyję ANSI. Myślę jednak, że DEFAULT będzie działać tak samo dobrze.

Jeśli chcesz używać czcionki takiej jak Webdings czy Wingdings, musisz zastosować identyfikator SYMBOL_CHARSET zamiast ANSI_CHARSET.

                ANSI_CHARSET,         // Identyfikator zestawu znaków

Dopasowanie czcionki do wyświetlenia jest bardzo ważne. Wskazuje Windows jaki typ zestawu znaków ma użyć, jeśli jest dostępnych więcej niż jeden. OUT_TT_PRECIS sprawia, że jeśli istnieje więcej niż jeden rodzaj czcionki o danej nazwie, system wybierze wersję TRUETYPE. Czcionki Truetype zawsze wyglądają lepiej; szczególnie gdy mają być wielkie. Możesz ponadto użyć OUT_TT_ONLY_PRECIS, by wymusić użycie czcionki TRUETYPE.

                OUT_TT_PRECIS,         // Dopasowanie czcionki do wyświetlenia

Dokładność przycinania określa jak mają być obcinane znaki znajdujące się poza obszarem roboczym. Nic więcej do powiedzenia na ten temat, po prostu ustaw na domyślne.

                CLIP_DEFAULT_PRECIS,         // Dokładność przycinania

Jakość prezentacji jest bardzo ważna. Możesz wybrać między PROOF, DRAFT, NONANTIALIASED, DEFAULT lub ANTIALIASED. Wszyscy wiemy, że czcionki ANTIALIASED wyglądają dobrze :). Antialiasing czcionki to taki sam efekt, jaki otrzymujesz włączając wygładzanie czcionek w Windows. Powoduje, że czcionki wyglądają mniej "ząbkowato".

                ANTIALIASED_QUALITY,         // Jakość prezentacji

Dalej, określamy szerokość i rodzinę czcionki. Szerokość przyjmuje wartości DEFAULT_PITCH, FIXED_PITCH i VARIABLE_PITCH, a rodzina FF_DECORATIVE, FF_MODERN, FF_ROMAN, FF_SCRIPT, FF_SWISS, FF_DONTCARE. Wypróbuj różne kombinacje, by sprawdzić jaki będzie efekt. Ja ustawię obie wartości na domyślne.

                FF_DONTCARE|DEFAULT_PITCH,         // Rodzina i szerokość

Ostatecznie... Podajemy nazwę czcionki. Włącz Microsoft Word lub inny edytor tekstu. Kliknij na listę czcionek i wybierz tą, która Ci odpowiada. Zastąp 'Comis Sans MS' jej nazwą.

                "Comic Sans MS");         // Nazwa czcionki

Teraz wybieramy naszą czcionkę poprzez przypisanie jej do naszego kontekstu urządzenia (DC).

        SelectObject(hDC, font);         // Wybiera czcionkę, którą utworzyliśmy

Teraz nowy kod. Zbudujemy naszą czcionkę konturową używając funkcji wglUseFontOutlines. Jako parametry podajemy nasz kontekst urządzenia, początkowy znak, liczbę znaków do utworzenia oraz wartość zmiennej base. Wszystko bardzo podobnie do sposobu, w jaki tworzyliśmy czcionkę bitmapową.

        wglUseFontOutlines(    hDC,         // Bieżący kontekst urządzenia
                0,         // Początkowy znak
                255,         // Liczba list wyświetlania do utworzenia
                base,         // Bazowa lista wyświetlania

To nie wszystko. Ustawiamy dopuszczalne zbiorowe odchylenie od obrysów. Im wartość jest bliższa 0.0f, tym bardziej wygładzona będzie czcionka. Następnie ustawiamy grubość czcionki, tj. grubość w kierunku osi Z. 0.0f oznacza zupełnie płaską czcionkę, 1.0f oznacza czcionkę o pewnej grubości.

Parametr WGL_FONT_POLYGONS nakazuje OpenGL utworzyć stałą czcionkę używając wielokątów. Jeśli użyjemy zamiast niego WGL_FONT_LINES, czcionka będzie zbudowana z odcinków (ang. wireframe). Zapamiętaj, że w przypadku użycia GL_FONT_LINES wektory normalne nie będą generowane, zatem oświetlenie nie będzie działać właściwie.

Ostatni parametr gmf wskazuje na adres bufora danych list wyświetlania.

            0.0f,         // Dopuszczalne zbiorowe odchylenie od obrysów
            0.2f,         // Grubość czcionki w kierunku osi Z
            WGL_FONT_POLYGONS,         // Użyj wielokątów, nie linii
            gmf);         // Adres bufora, który otrzyma dane
}

Poniższy kod jest bardzo prosty. Usuwa 256 list wyświetlania z pamięci, począwszy od pierwszej listy określonej przez base. Nie jestem pewien, czy Windows zrobiłby to za Ciebie, ale lepiej być przezornym :)

GLvoid KillFont(GLvoid)         // Usuń czcionkę
{
    glDeleteLists(base, 256);         // Usuń wszystkie 256 znaków
}

Teraz moja sprytna funkcja. Wywołujesz ją poleceniem glPrint("Tu wpisz wiadomość"). Tak samo, gdy rysowałeś czcionkę rastrową na ekrenie w lekcji 13.. Tekst jest przechowywany w łańcuchu znaków podanym jako parametr fmt.

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

Pierwsza linia poniżej definiuje zmienną o nazwie length. Będzie ona określała, jaki jest nasz ciąg znaków. Druga linia rezerwuje miejsce na 256-znakowy ciąg znaków. text jest ciągiem znaków, który wyświetlimy na ekranie. Trzecia linia tworzy wskaźnik na listę argumentów, które przekażemy wraz z ciągiem znaków. Jeśli wyślemy jakiekolwiek zmienne oprócz tekstu, wskaźnik będzie pokazywał na nie.

    float        length=0;         // Użyta do znalezienia długości tekstu
    char        text[256];         // Przechowuje nasz łańcuch znaków
    va_list        ap;         // Wskaźnik na listę argumentów

Następne dwie linie kodu sprawdzają, czy jest cokolwiek do wyświetlenia. Jeśli nie ma tekstu, fmt będzie równy NULL i nic nie zostanie wyświetlone.

    if (fmt == NULL)         // Jeśli nie ma tekstu...
        return;         // Nic nie rób

Poniższe trzy linie kodu zamieniają dowolne symbole w tekście na rzeczywiste liczby reprezentowane przez symbole. Końcowy tekst i wszystkie skonwertowane symbole są następnie zapisane do tablicy text. Symbole wyjaśnię szczegółowo niżej.

    va_start(ap, fmt);         // Przegląda łańcuch w poszukiwaniu zmiennych
        vsprintf(text, fmt, ap);         // Konwertuje symbole na rzeczywiste liczby
    va_end(ap);         // Wynik jest zapisany do zmiennej text

Podziękowania dla Jima Williamsa za zasugerowanie poniższego kodu. Centrowałem tekst ręcznie. Jego metoda działa o wiele lepiej :)

Zaczynamy od utworzenia pętli, która przechodzi przez cały łańcuch znaków, znak po znaku. strlen(tekst) daje nam informacje o długości tekstu. Po wejściu do pętli zwiększamy wartość zmiennej length o szerokość każdego znaku. Gdy skończymy, wartość zmiennej length będzie równa szerokości całego łańcucha znaków. Czyli, jeśli piszemy "hello" i szczęśliwym trafem każdy znak ma szerokość równą 10 jednostkom, zwiększamy wartość zmiennej length o szerokość pierwszej litery ? 10. Następnie sprawdzamy szerokość drugiej litery. Jest ona także równa 10, zatem length będzie miało wartość 10+10 (20). Po sprawdzeniu wszystkich 5 liter length będzie przechowywać wartość 50 (5*10).

gmf[text[loop]].gmfCellIncX daje nam informacje o szerokości każdego znaku. Pamiętaj że gmd przechowuje informacje poza każdą listą wyświetlania. Jeśli loop jest równe 0, text[loop] będzie pierwszym znakiem w naszym łańcuchu znaków. Jeśli loop jest równe 1, text[loop] będzie drugim znakiem w naszym łańcuchu. gmfCellIncX jest odległością, o jaką przemieszczamy się w prawo tak, by jeden znak nie był rysowany na drugim. Tak się składa, że odległość jest naszą szerokością :) Możesz także poznać wysokość znaku za pomocą gmfCellIncY. To może być przydatne, gdy rysujesz tekst pionowo zamiast poziomo.

UWAGA: Curt Werline dodaje: kiedy obliczasz wymiary łańcucha tekstu, używasz gmfCellIncX dla szerokości i gmfCellIncY dla wysokości. Te wartości są offsetami, a nie rzeczywistymi wymiarami. By otrzymać rzeczywiste wymiary, powinieneś użyć gmfBlackBoxX oraz gmfBlackBoxY.

    
    for (unsigned int loop=0;loop<(strlen(text));loop++)         // Pętla znajdująca długość tekstu
    {
        length+=gmf[text[loop]].gmfCellIncX;         // Zwiększa zmienną length o długość każdego znaku
    }

W końcu bierzemy obliczoną długość i zamieniamy ją na liczbę ujemną (ponieważ musimy przesunąć nasz tekst na lewo od środka). Następnie dzielimy długość przez 2. Nie chcemy przesunąć całego tekstu na lewo, tylko połowę!

    glTranslate(-length/2,0.0f,0.0f);         // Centruje nasz tekst na ekranie

Następnie kładziemy na stos GL_LIST_BIT, co zapobiega oddziaływaniu glListBase na inne listy wyświetlania, które możemy używać w naszym programie.

Polecenie glListBase(base) mówi OpenGL gdzie znaleźć odpowiednią listę wyświetlania dla każdego znaku.

    glPushAttrib(GL_LIST_BIT);         // Kładzie na stos atrybuty listy wyświetlania
    glListBase(base);         // Ustawia początkowy znak na 0

Teraz OpenGL wie, gdzie znajdują się znaki. Możemy przystąpić do wyświetlania tekstu na ekranie. glCallLists wypisuje go za pomocą wielokrotnego wywoływania list wyświetlania.

Poniższa linia robi następujące rzeczy: najpierw powiadamia OpenGL, że będziemy wyświetlać listy na ekranie. strlen(text) dowiaduje się ile liter zamierzamy wysłać. Dalej, próbuje dowiedzieć się o największy numer listy który wyślemy. Nie będziemy wypisywać więcej niż 255 znaków, zatem możemy użyć UNSIGNED_BYTE (bajt reprezentuje liczbę z zakresu 0-255, czyli to nam akurat odpowiada). Ostatecznie mówimy co chcemy wyświetlić poprzez podanie łańcucha znaków.

Jeśli zastanawiasz się dlaczego litery nie zachodzą na siebie, wyjaśniam. Każda lista wyświetlania wie gdzie znajduje się prawa strona znaku. Po narysowaniu litery na ekranie następuje przesunięcie i OpenGL przechodzi do jej prawej strony. Następna litera lub obiekt będzie rysowana począwszy od końcowego miejsca ostatniego przesunięcia, czyli od prawej strony ostatniej litery.

Ostatecznie zdejmujemy GL_LIST_BIT ze stosu, przywracając GL do stanu sprzed ustawienia naszej bazy za pomocą glListBase(base).

    glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);         // Wyświetla tekst listy wyświetlania
    glPopAttrib();         // Zdejmuje ze stosu poprzednią wartość atrybutów listy wyświetlania
}

Kod zmiany rozmiaru okna jest taki sam jak w lekcji 1. Pomijamy go.

Jest kilka nowych linii na końcu funkcji InitGL. Linia BuildFont() z lekcji 13. nadal tu jest, wraz z nowym kodem wykonującym szybko oświetlenie. Light0 jest predefiniowane w większości kart graficznych i będzie ładnie oświetlało scenę bez szczególnego zaangażowania z mojej strony :)

Dodałem także polecenie glEnable(GL_COLOR_MATERIAL). Ponieważ znaki są obiektami 3D, musisz włączyć kolorowanie materiału, w przeciwnym razie zmiana koloru za pomocą glColor3d(r,g,b) nie zmieni koloru tekstu. Jeśli rysujesz jakieś figury na ekranie samemu, włącz kolorowanie materiałów zanim napiszesz tekst, następnie wyłącz je. Inaczej wszystkie obiekty na ekranie będą pokolorowane.

int InitGL(GLvoid)         // Cała konfiguracja OpenGL idzie tu
{
    glShadeModel(GL_SMOOTH);         // Włącz gładkie cieniowanie
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f);         // Czarne tło
    glClearDepth(1.0f);         // Ustawienie bufora głębokości
    glEnable(GL_DEPTH_TEST);         // Włącza sprawdzanie głębokości
    glDepthFunc(GL_LEQUAL);         // Typ sprawdzania głębokości
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);         // Bardzo ładne obliczanie perspektywy
    GL_ENABLE(GL_LIGHT0);         // Włącza domyślne światło (szybko)    (NOWE)
    GL_ENABLE(GL_LIGHTING);         // Włącza oświetlenie    (NOWE)
    GL_ENABLE(GL_COLOR_MATERIAL);         // Włącza kolorowanie materiału (NOWE)
    BuildFont();         // Tworzy czcionkę (DODANO)
    return TRUE;         // Inicjalizacja poszła pomyślnie
}

Teraz pora na rysowanie. Rozpoczniemy od wyczyszczenia ekranu i bufora głębokości. Wywołamy glLoadIdentity() by wyzerować wszystko, następnie przesuniemy się dziesięć jednostek w stronę ekranu. Czcionki konturowe wyglądają wspaniale w trybie perspektywicznym. Im dalej w stronę ekranu się przesuniesz, tym mniejsza staje się czcionka. Analogicznie, im bliżej się przesuniesz, tym większa jest czcionka.

Czcionki konturowe mogą być także przekształcane za pomocą funkcji glScale(x,y,z). Jeśli chcesz mieć czcionkę 2 razy wyższą, użyj glScalef(1.0f,2.0f,1.0f). 2.0f symbolizuje współczynnik osi Y, co nakazuje OpenGL narysować czcionkę 2 razy wyższą. Jeśli 2.0f symbolizowałoby oś X, znak byłby dwa razy szerszy.

int DrawGLScene(GLvoid)         // Tutaj rysujemy wszystko
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);         // Czyści ekran i bufor głębokości
    glLoadIdentity();         // Zeruje widok
    glTranslatef(0.0f, 0.0f,-10.0f);         // Przesuń dziesięć jednostki w stronę ekranu

Po przesunięciu w stronę ekranu chcemy obrócić tekst. Następne 3 linie obracają ekran we wszystkich trzech osiach. Mnożę rot przez różne liczby aby każdy obrót zachodził z inną szybkością.

    glRotatef(rot,1.0f,0.0f,0.0f);         // Obrót względem osi X
    glRotatef(rot*1.5f,0.0f,1.0f,0.0f);         // Obrót względem osi Y
    glRotatef(rot*1.4f,0.0f,0.0f,1.0f);         // Obrót względem osi Z

Teraz czas na szalone pulsowanie kolorów. Jak zwykle użyję tylko jednej zmiennej, której wartość się zwiększa (rot). Kolory pulsują za pomocą funkcji cos i sin. Dzielę wartość rot przez różne liczby by natężenie każdego koloru nie zwiększało się z tą samą prędkością. Końcowy rezultat jest naprawdę ładny.

        // Pulsowanie kolorów z wykorzystaniem rotacji
    glColor3f(1.0f*float(cos(rot/20.0f)),1.0f*float(sin(rot/25.0f)),1.0f-0.5f*float(cos(rot/17.0f)));

Moja ulubiona część... Rysowanie tekstu na ekranie. Użyłem tego samego polecenia, które wykorzystywaliśmy do wypisywania czcionek rastrowych na ekran. Wszystko co musisz zrobić, to wywołać funkcję glPrint("{dowolny tekst}"). To takie proste!

Poniższy kod wypisuje tekst "NeHe", spację, kratkę, spację oraz dowolną liczbę przechowywaną w rot podzielną przez 50 (by spowolnić trochę licznik). Jeśli liczba jest większa niż 999.99, czwarta cyfra z lewej będzie ucięta (żądamy tylko trzech cyfr jako cechę). Tylko dwie cyfry będą wyświetlone po przecinku.

    glPrint("NeHe - %3.2f",rot/50);         // Wypisuje tekst na ekranie

Następnie zwiększamy wartość zmiennej rot by kolory pulsowały a tekst się obracał.

    rot+=0.5f;
    return TRUE;
}

Ostatnią rzeczą jest dodanie KillFont() na końcu KillGLWindow() tak jak pokazuje poniżej. Ważne jest, by dodać tę linię. Sprząta ona po nas, zanim wyjdziemy z programu.

    if (!UnregisterClass("OpenGL",hInstance))         // Czy jesteśmy zdolni do wyrejestrowania klasy?
    {
        MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        hInstance=NULL;         // Ustawia hInstance na NULL
    }
    KillFont();         // Zniszcz czcionkę
}

Po przeczytaniu tego samouczka powinieneś wiedzieć jak stosować czcionki konturowe w swoich projektach OpenGL. Podobnie jak w lekcji 13., przeszukiwałem sieć w poszukiwaniu samouczka podobnego do tego i nie znalazłem nic. Czy to oznacza, że moja strona jest pierwszą, która opisuje ten temat w szczegółach i tłumaczy wszystko w łatwym do zrozumienia kodzie C? Miłej lektury i przyjemnego kodowania!