Lekce 17 - 2D fonty z textur

V této lekci se naučíte, jak vykreslit font pomocí texturou omapovaného obdélníku. Dozvíte se také, jak používat pixely místo jednotek. I když nemáte rádi mapování 2D znaků, najdete zde spoustu nových informací o OpenGL.

Tuším, že už vás asi fonty unavují. Textové lekce vás, nicméně nenaučili jenom "něco vypsat na monitor", naučili jste se také 3D fonty, mapování textur na cokoli a spoustu dalších věcí. Nicméně, co se stane pokud budete kompilovat projekt pro platformu, která nepodporuje fonty? Podíváte se do lekce 17... Pokud si pamatujete na první lekci o fontech (13), tak jsem tam vysvětloval používání textur pro vykreslování znaků na obrazovku. Obyčejně, když používáte textury ke kreslení textu na obrazovku, spustíte grafický program, zvolíte font, napíšete znaky, uložíte bitmapu a "loadujete" ji do svého programu. Tento postup není zrovna efektivní pro program, ve kterém používáte hodně textů nebo texty, které se neustále mění. Ale jak to udělat lépe? Program v této lekci používá pouze JEDNU! texturu. Každý znak na tomto obrázku bude zabírat 16x16 pixelů. Bitmapa tedy celkem zabírá čtverec o straně 256 bodů (16*16=256) - standardní velikost. Takže... pojďme vytvořit 2D font z textury. Jako obyčejně, i tentokrát rozvíjíme první lekci.

#include <windows.h>// Hlavičkový soubor pro Windows

#include <stdio.h>// Hlavičkový soubor pro standardní vstup/výstup

#include <gl\gl.h>// Hlavičkový soubor pro OpenGL32 knihovnu

#include <gl\glu.h>// Hlavičkový soubor pro Glu32 knihovnu

#include <gl\glaux.h>// Hlavičkový soubor pro Glaux knihovnu

HDC hDC = NULL;// Privátní GDI Device Context

HGLRC hRC = NULL;// Trvalý Rendering Context

HWND hWnd = NULL;// Obsahuje Handle našeho okna

HINSTANCE hInstance;// Obsahuje instanci aplikace

bool keys[256];// Pole pro ukládání vstupu z klávesnice

bool active = TRUE;// Ponese informaci o tom, zda je okno aktivní

bool fullscreen = TRUE;// Ponese informaci o tom, zda je program ve fullscreenu

GLuint base;// Ukazatel na první z display listů pro font

GLuint texture[2];// Ukládá textury

GLuint loop;// Pomocná pro cykly

GLfloat cnt1;// Čítač 1 pro pohyb a barvu textu

GLfloat cnt2;// Čítač 2 pro pohyb a barvu textu

Následující kód je trochu odlišný, od toho z předchozích lekcí. Všimněte si, že TextureImage[] ukládá dva záznamy o obrázcích. Je velmi důležité zdvojit paměťové místo a loading. Jedno špatné číslo by mohlo zplodí přetečení paměti nebo totální error.

int LoadGLTextures()// Nahraje bitmapu a konvertuje na texturu

{

int Status=FALSE;// Indikuje chyby

AUX_RGBImageRec *TextureImage[2];// Alokuje místo pro bitmapy

Pokud byste zaměnili číslo 2 za jakékoli jiné, budou se dít věci. Vždy se musí rovnat číslu z předchozí řádky (tedy v TextureImage[] ). Textury, které chceme nahrát se jmenují font.bmp a bumps.bmp. Tu druhou můžete zaměnit - není až tak podstatná.

memset(TextureImage,0,sizeof(void *)*2);// Nastaví ukazatel na NULL

if ((TextureImage[0]=LoadBMP("Data/Font.bmp")) && (TextureImage[1]=LoadBMP("Data/Bumps.bmp")))

{

Status=TRUE;// Nastaví status na TRUE

Nebudu vám ani říkat kolik emailů jsem obdržel od lidí ptajících se: "Proč vidím jenom jednu texturu?" nebo "Proč jsou všechny moje textury bílé!?!". Většinou bývá problém v tomto řádku. Opět pokud přepíšete 2 na 1, bude vidět jenom jedna textura (druhá bude bílá). A naopak, zaměníte-li 2 za 3, program se zhroutí. Příkaz glGenTextures() by se měl volat jenom jednou a tímto jedním voláním vytvořit najednou všechny textury, které hodláte použít. Už jsem viděl lidi, kteří tvořili každou texturu zvlášť. Je dobré, si vždy na začátku rozmyslet, kolik jich budete používat.

glGenTextures(2, &texture[0]);// 2 textury

for (loop=0; loop<2; loop++)

{

glBindTexture(GL_TEXTURE_2D, texture[loop]);

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);

glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop]->sizeX, TextureImage[loop]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop]->data);

}

}

Na konci funkce uvolníme všechnu paměť, kterou jsme alokovali pro vytvoření textur. I zde si všimněte uvolňování dvou záznamů.

for (loop=0; loop<2; loop++)

{

if (TextureImage[loop])// Pokud obrázek existuje

{

if (TextureImage[loop]->data)// Pokud existují data obrázku

{

free(TextureImage[loop]->data);// Uvolní paměť obrázku

}

free(TextureImage[loop]);// Uvolní strukturu obrázku

}

}

return Status;

}

Teď vytvoříme font. Protože použijeme trochu matematiky, zaběhneme trochu do detailů.

GLvoid BuildFont(GLvoid)// Vytvoření display listů fontu

{

Jak už plyne z názvu, budou proměnné použity k určení pozice, každého znaku na textuře fontu.

float cx;// Koordináty x

float cy;// Koordináty y

Dále řekneme OpenGL, že chceme vytvořit 256 display listů. "base" ukazuje na první display list. Potom vybereme texturu.

base=glGenLists(256);// 256 display listů

glBindTexture(GL_TEXTURE_2D, texture[0]);// Výběr textury

Začneme cyklus generující všech 256 znaků.

for (loop=0; loop<256; loop++)// Vytváří 256 display listů

{

První řádka může vypadat trochu nejasně. Symbol % vyjadřuje celočíselný zbytek po dělení 16. Pomocí cx se budeme přesunovat na textuře po řádcích (zleva doprava), cy zajišťuje pohyb ve sloupcích (od shora dolů). Dalších operace "/16.0f" konvertuje výsledek do koordinátů textury. Pokud bude loop rovno 16 - cx bude rovno zbytku z 16/16 tedy nule (16/16=1 zbytek 0). Ale cy bude výsledkem "normálního" dělení - 16/16=1. Dále bychom se tedy měli na textuře přesunout na dalších řádek, dolů o výšku jednoho znaku a přesunovat se opět zleva doprava. loop se tedy rovná 17, cx=17/16=1,0625. Desetinná část (0,0625) je vlastně rovna jedné šestnáctině. Z toho plyne, že jsme se přesunuli o jeden znak doprava. cy je stále jedna (viz. dále). 18/16 udává posun o 2 znaky doprava a jeden znak dolů. Analogicky se dostaneme k loop=32. cx bude rovno 0 (32/16=2 zbytek 0). cy=2, tím se na textuře posuneme o dva znaky dolů. Dává to smysl? (Pozn. překladatele: Já bych asi použil vnořený cyklus - vnějším jít po sloupcích a vnitřním po řádcích. Bylo by to možná pochopitelnější (...a hlavně snadnější na překlad :-))

Textura fontu

cx=float(loop%16)/16.0f;// X pozice aktuálního znaku

cy=float(loop/16)/16.0f;// Y pozice aktuálního znaku

Teď, po troše matematického vysvětlování, začneme vytvářet 2D font. Pomocí cx a cy vyjmeme každý znak z textury fontu. Přičteme loop k hodnotě base - aby se znaky nepřepisovaly ukládáním vždy do prvního. Každý znak se uloží do vlastního display listu.

glNewList(base+loop,GL_COMPILE);// Vytvoření display listu

Po zvolení display listu do něj nakreslíme obdélník otexturovaný znakem.

glBegin(GL_QUADS);// Pro každý znak jeden obdélník

Cx a cy jsou schopny uložit velmi malou desetinnou hodnotu. Pokud cx a zároveň cy budou 0, tak bude příkaz vypadat takto: glTexCoord2f(0.0f,1-0.0f-0.0625f); Pamatujte si, že 0,0625 je přesně 1/16 naší textury nebo šířka/výška jednoho znaku. Koordináty mohou ukazovat na levý dolní roh naší textury. Všimněte si, že používáme glVertex2i(x,y) namísto glVertex3f(x,y,z). Nebudeme potřebovat hodnotu z, protože pracujeme s 2D fontem. Protože používáme kolnou projekci (ortho), nemusíme se přesunout do hloubky - stačí tedy pouze x, y. Okno má velikost 0-639 a 0-479 (640x480) pixelů, tudíž nemusíme používat desetinné nebo dokonce záporné hodnoty. Cesta jak nastavit ortho obraz je určit 0, 0 jako levý dolní roh a 640, 480 jako pravý horní roh. Zjednodušeně řečeno: zbavili jsme se záporných koordinátů. Užitečná věc, pro lidi, kteří se nechtějí starat o perspektivu, a kteří více preferují práci s pixely než s jednotkami :)

glTexCoord2f(cx,1-cy-0.0625f); glVertex2i(0,0);// Levý dolní

Druhý koordinát je teď posunut o 1/16 doprava (šířka znaku) - přičteme k x-ové hodnotě 0,0625f.

glTexCoord2f(cx+0.0625f,1-cy-0.0625f); glVertex2i(16,0);// Pravý dolní

Třetí koordinát zůstává vpravo, ale přesunul se nahoru (o výšku znaku).

glTexCoord2f(cx+0.0625f,1-cy); glVertex2i(16,16);// Pravý horní

Určíme levý horní roh znaku.

glTexCoord2f(cx,1-cy); glVertex2i(0,16);// Levý horní

glEnd();// Konec znaku

Přesuneme se o 10 pixelů doprava, tím se umístíme doprava od právě nakreslené textury. Pokud bychom se nepřesunuli, všechny znaky by se nakupily na jedno místo. Protože je font trošku "hubenější" (užší), nepřesuneme se o celých 16 pixelů (šířku znaku), ale pouze o 10. Mezi jednotlivými písmeny by byly velké mezery.

glTranslated(10,0,0);// Přesun na pravou stranu znaku

glEndList();// Konec kreslení display listu

}// Cyklus pokračuje dokud se nevytvoří všech 256 znaků

}

Opět přidáme kód pro uvolnění všech 256 display listů znaku. Provede se při ukončování programu.

GLvoid KillFont(GLvoid)// Uvolní paměť fontu

{

glDeleteLists(base,256);// Smaže 256 display listů

}

V následující funkci se provádí výstup textu. Všechno je pro vás nové, tudíž vysvětlím každou řádku hodně podrobně. Do tohoto kódu by mohla být přidána spousta dalších funkcí, jako je podpora proměnných, zvětšování znaků, rozestupy ap. Funkci glPrint() předáváme tři parametry. První a druhý je pozice textu v okně (u Y je nula dole!), třetí je žádaný řetězec a poslední je znaková sada. Podívejte se na bitmapu fontu. Jsou tam dvě rozdílené znakové sady (v tomto případě je první obyčejná - 0, druhá kurzívou - cokoli jiného).

GLvoid glPrint(GLint x, GLint y, char *string, int set)// Provádí výpis textu

{

Napřed se ujistíme, zda je set buď 1 nebo 0. Pokud je větší než 1, přiřadíme jí 0. (Pozn. překladatele: Autor asi zapomněl na častou obranu uživatelů při zhroucení programu: "Ne určitě jsem tam nezadal záporné číslo!" :-)

if (set>1)

{

set=1;

}

Protože je možné, že máme před spuštěním funkce vybranou (na tomto místě) "randomovou" texturu, zvolíme tu "fontovou".

glBindTexture(GL_TEXTURE_2D, texture[0]);// Výběr textury

Vypneme hloubkové textování - blending vypadá lépe (text by mohl skončit za nějakým objektem, nemusí vypadat správně...). Okolí textu vám nemusí vadit, když používáte černé pozadí.

glDisable(GL_DEPTH_TEST);// Vypne hloubkové testování

Hodně důležitá věc! Zvolíme projekční matici (Projection Matrix) a příkazem glPushMatrix() ji uložíme (něco jako paměť na kalkulačce). Do původního stavu ji můžeme obnovit voláním glPopMatrix() (viz. dále).

glMatrixMode(GL_PROJECTION);// Vybere projekční matici

glPushMatrix();// Uloží projekční matici

Poté, co byla projekční matice uložena, resetujeme matici a nastavíme ji pro kolmou projekci (Ortho screen). Parametry mají význam ořezávacích rovin (v pořadí): levá, pravá, dolní, horní, nejbližší, nejvzdálenější. Levou stranu bychom mohli určit na -640, ale proč pracovat se zápornými čísly? Je moudré nastavit tyto hodnoty, abyste si zvolili meze (rozlišení), ve kterých právě pracujete.

glLoadIdentity();// Reset matice

glOrtho(0,640,0,480,-1,1);// Nastavení kolmé projekce

Teď určíme matici modelview a opět voláním glPushMatrix() uložíme stávající nastavení. Poté resetujeme matici modelview, takže budeme moci pracovat s kolmou projekcí.

glMatrixMode(GL_MODELVIEW);// Výběr matice

glPushMatrix();// Uložení matice

glLoadIdentity();// Reset matice

S uloženými nastaveními pro perspektivu a kolmou projekci, můžeme začít vykreslovat text. Začneme translací na místo, kam ho chceme vykreslit. Místo glTranslatef() použijeme glTranslated(), protože není důležitá desetinná hodnota. Nelze určit půlku pixelu :-) (Pozn. překladatele: Tady bude asi jeden totálně velký error, jelikož glTranslated() pracuje v přesnosti double, tedy ještě ve větší - nicméně stane se. (Alespoň, že víme o co jde :-). Jo, ten smajlík u půlky pixelu byl i v původní verzi.)

glTranslated(x,y,0);// Pozice textu (0,0 - levá dolní)

Řádek níže určí znakovou sadu. Při použití druhé přičteme 128 k display listu base (128 je polovina z 256 znaků). Přičtením 128 "přeskočíme" prvních 128 znaků.

glListBase(base-32+(128*set));// Zvolí znakovou sadu (0 nebo 1)

Zbývá vykreslení. Jako pokaždé v minulých lekcích to provedeme i zde voláním glCallLists(). strlen(string) je délka řetězce (ve znacích), GL_BYTE znamená, že každý znak je reprezentován bytem (hodnoty 0 až 255). Nakonec, ve string předáváme konkrétní text pro vykreslení.

glCallLists(strlen(string),GL_BYTE,string);// Vykreslení textu na obrazovku

Obnovíme perspektivní pohled. Zvolíme projekční matici a použijeme glPopMatrix() k odvolání se na dříve uložená (glPushMatrix()) nastavení.

glMatrixMode(GL_PROJECTION);// Výběr projekční matice

glPopMatrix();// Obnovení uložené projekční matice

Zvolíme matice modelview a uděláme to samé jako před chvílí.

glMatrixMode(GL_MODELVIEW);// Výběr matice modelview

glPopMatrix();// Obnovení uložené modelview matice

Povolíme hloubkové testování. Pokud jste ho na začátku nevypínali, tak tuto řádku nepotřebujete.

glEnable(GL_DEPTH_TEST);// Zapne hloubkové testování

}

Vytvoříme textury a display listy. Pokud se něco nepovede vrátíme false. Tím program zjistí, že vznikl error a ukončí se.

int InitGL(GLvoid)// Všechno nastavení OpenGL

{

if (!LoadGLTextures())// Nahraje textury

{

return FALSE;

}

BuildFont();// Vytvoří font

Následují obvyklé nastavení OpenGL.

glClearColor(0.0f, 0.0f, 0.0f, 0.0f);// Černé pozadí

glClearDepth(1.0);// Nastavení hloubkového bufferu

glDepthFunc(GL_LEQUAL);// Typ hloubkového testování

glBlendFunc(GL_SRC_ALPHA,GL_ONE);// Vybere typ blendingu

glShadeModel(GL_SMOOTH);// Povolí jemné stínování

glEnable(GL_TEXTURE_2D);// Zapne mapování 2D textur

return TRUE;

}

Začneme kreslit scénu - na začátku stvoříme 3D objekt a až potom text. Důvod proč jsem se rozhodl přidat 3D objekt je prostý: chci demonstrovat současné použití perspektivní i kolmé projekce v jednom programu.

int DrawGLScene(GLvoid)// Vykreslování

{

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Smaže obrazovku a hloubkový buffer

glLoadIdentity();// Reset matice

Zvolíme texturu vytvořenou z bumps.bmp, přesuneme se o pět jednotek dovnitř a provedeme rotaci o 45° na ose Z. Toto pootočení po směru hodinových ručiček vyvolá dojem diamantu a ne dvou čtverců.

glBindTexture(GL_TEXTURE_2D, texture[1]);// Výběr textury

glTranslatef(0.0f,0.0f,-5.0f);// Přesun o pět do obrazovky

glRotatef(45.0f, 0.0f,0.0f,1.0f);// Rotace o 45° po směru hodinových ručiček na ose z

Provedeme další rotaci na osách X a Y, která je závislá na proměnné cnt1*30. Má za následek otáčení objektu dokola, stejně jako se otáčí diamant na jednom místě.

glRotatef(cnt1*30.0f,1.0f,1.0f,0.0f);// Rotace na osách x a y

Protože chceme aby se jevil jako pevný, vypneme blending a nastavíme bílou barvu. Vykreslíme texturou namapovaný čtyřúhelník.

glDisable(GL_BLEND);// Vypnutí blendingu

glColor3f(1.0f,1.0f,1.0f);// Bílá barva

glBegin(GL_QUADS);// Kreslení obdélníku

glTexCoord2d(0.0f,0.0f);

glVertex2f(-1.0f, 1.0f);

glTexCoord2d(1.0f,0.0f);

glVertex2f( 1.0f, 1.0f);

glTexCoord2d(1.0f,1.0f);

glVertex2f( 1.0f,-1.0f);

glTexCoord2d(0.0f,1.0f);

glVertex2f(-1.0f,-1.0f);

glEnd();// Konec obdélníku

Dále provedeme rotaci o 90° na osách X a Y. Opět vykreslíme čtyřúhelník. Tento nový uprostřed protíná prvně kreslený a je na něj kolmý (90°). Hezký souměrný tvar.

glRotatef(90.0f,1.0f,1.0f,0.0f);// Rotace na osách X a Y o 90°

glBegin(GL_QUADS);// Kreslení obdélníku

glTexCoord2d(0.0f,0.0f);

glVertex2f(-1.0f, 1.0f);

glTexCoord2d(1.0f,0.0f);

glVertex2f( 1.0f, 1.0f);

glTexCoord2d(1.0f,1.0f);

glVertex2f( 1.0f,-1.0f);

glTexCoord2d(0.0f,1.0f);

glVertex2f(-1.0f,-1.0f);

glEnd();// Konec obdélníku

Zapneme blending a začneme vypisovat text. Použijeme stejné pulzování barev jako v některých minulých lekcích.

glEnable(GL_BLEND);// Zapnutí blendingu

glLoadIdentity();// Reset matice

// Změna barvy založená na pozici textu

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

Pro vykreslení stále využíváme funkci glPrint(). Prvními parametry jsou x-ová a Y-ová souřadnice, třetí atribut, "NeHe", bude výstupem a poslední určuje znakovou sadu (0-normální, 1-kurzíva). Asi jste si domysleli, že textem pohybujeme pomocí sinů a kosinů. Pokud jste tak trochu "v pasti", vraťte se do minulých lekcí, ale není podmínkou tomu až tak rozumět.

glPrint(int((280+250*cos(cnt1))),int(235+200*sin(cnt2)),"NeHe",0);// Vypíše text

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

glPrint(int((280+230*cos(cnt2))),int(235+200*sin(cnt1)),"OpenGL",1);// Vypíše text

Nastavíme barvu na modrou a na spodní část okna napíšeme jméno autora této lekce. Celé to zopakujeme s bílou barvou a posunutím o dva pixely doprava - jednoduchý stín (není-li zapnutý blending nebude to fungovat).

glColor3f(0.0f,0.0f,1.0f);// Modrá barva

glPrint(int(240+200*cos((cnt2+cnt1)/5)),2,"Giuseppe D'Agata",0);// Vypíše text

glColor3f(1.0f,1.0f,1.0f);// Bílá barva

glPrint(int(242+200*cos((cnt2+cnt1)/5)),2,"Giuseppe D'Agata",0);// Vypíše text

Inkrementujeme čítače - text se bude pohybovat a objekt rotovat.

cnt1+=0.01f;

cnt2+=0.0081f;

return TRUE;

}

Myslím, že teď mohu oficiálně prohlásit, že moje tutoriály nyní vysvětlují všechny možné cesty k vykreslení textu. Kód z této lekce může být použit na jakékoli platformě, na které funguje OpenGL, je snadný k používání. Vykreslování tímto způsobem "užírá" velmi málo procesorového času. Rád bych poděkoval Guiseppu D'Agatovi za originální verzi této lekce. Hodně jsem ji upravil a konvertoval na nový základní kód, ale bez něj bych to asi nesvedl. Jeho verze má trochu více možností, jako vzdálenost znaků apod., ale já jsem zase stvořil "extrémně skvělý 3D objekt".

napsal: Giuseppe D'Agata <waveform (zavináč) tiscalinet.it>
přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>

Zdrojové kódy

Lekce 17

<<< Lekce 16 | Lekce 18 >>>