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