Grafická knihovna OpenGL (8): display-listy

19. 8. 2003
Doba čtení: 9 minut

Sdílet

V dnešním dílu seriálu o grafické knihovně OpenGL si popíšeme způsob vykreslování znaků za pomoci bitmap, tj. jednobarevných rastrových obrázků. Dále si řekneme, k čemu jsou vhodné display-listy a jak se dají použít pro urychlení vykreslovacích operací. Jako příklad na demonstraci použití display listů vytvoříme funkci pro vykreslování znaků za pomoci bitmap.

Bitmapy a display-listy, vykreslování znaků a řetězců

Podpora vykreslování znaků v OpenGL

Ve třetím dílu tohoto seriálu jsme si popsali všechna grafická primitiva, která jsou v knihovně OpenGL podporována. Jedná se pouze o základní grafické objekty, jako jsou body, úsečky a plošné polygony. Zatím jsme se však nezmiňovali o vykreslování jednotlivých znaků nebo celých řetězců. Je to dáno skutečností, že OpenGL přímo nepodporuje vykreslování znaků ani řetězců.

Na první pohled nám to může připadat zvláštní, protože v jiných grafických knihovnách a standardech (například Xlib, Windows Graphics Device Interface – GDI, GKS nebo Allegro) patří znaky a řetězce mezi základní grafické objekty se kterými lze pracovat. Tvůrci knihovny OpenGL však museli vzít v úvahu několik faktů, které nakonec vedly k tomu, že vykreslování znaků není přímo podporováno:

  1. Knihovna OpenGL byla navržena tak, aby byla co nejvíce nezávislá na použité platformě a grafickém akcelerátoru. Každá platforma však používá jiné formáty souborů s definicemi tvarů jednotlivých znaků (fontů). OpenGL by tedy muselo buď používat vlastní formát souborů s fonty (to s sebou nese potřebu vytvořit konverzní utility, což není jednoduchá záležitost – viz vždy problematický převod TrueType fontů na PostScriptové fonty), nebo využít některý z existujících formátů (což znesnadňuje přenos na jinou platformu).
  2. I kdyby byla vyřešena otázka formátů souborů s fonty, existoval by problém s kódováním znaků. Každý operační systém používá jiné způsoby kódování, a tak je přenositelnost aplikací na různé systémy problematická. Dále by bylo nutné, aby každá implementace knihovny OpenGL obsahovala znaky všech jazyků (podobně jako dnešní Unicode), což není na mnoha specifických platformách uskutečnitelné.
  3. Knihovna OpenGL je určena převážně pro ovládání grafických akcelerátorů. Zatímco základní grafická primitiva (bod, úsečka, trojúhelník) lze poměrně jednoduše a přímočaře vykreslovat s použitím grafických procesorů, u znaků tomu tak není. Nejprve by se musela zvolit reprezentace jednotlivých znaků, tedy buď bitmapy, vektory, nebo spline křivky. Z hlediska jednoduchosti a rychlosti vykreslování lze použít pouze bitmapovou a vektorovou reprezentaci, u nichž však vznikají problémy při zvětšování písma nebo jeho dalších transformacích (rotaci apod.). Tím, že v OpenGL neexistují funkce pro vykreslování znaků, musí veškeré problémy s těmito funkcemi řešit programátor dané aplikace podle konkrétních požadavků, například tak, že použije několik variant fontu v různých velikostech.
  4. I způsob výpočtu vzájemného uspořádání znaků v textu není jednoduchý. Musí se (nebo by se alespoň měl) řešit kerning, používání ligatur a počítání meziznakových a mezislovních mezer. Toto však nejsou úlohy pro dnešní grafické akcelerátory, proto nemá smysl například funkci pro vypsání celého řádku do OpenGL doplňovat.

Vzhledem k výše uvedeným důvodům tedy OpenGL zobrazování znaků ani řetězců přímo nepodporuje, proto si musíme všechny potřebné funkce dopsat sami. Ve skutečnosti máme několik možností, jak docílit vykreslení znaků. Tyto možnosti se liší v jednoduchosti či složitosti implementace a použití, možnosti přenosu na jinou platformu, v rychlosti vykreslování a v kvalitě grafického výstupu:

  • Každý znak lze reprezentovat jako soubor několika úseček. V tomto případě se jedná o takzvané vektorové fonty, které jsou dnes stále používané v některých CAD systémech, protože je lze snadno vykreslovat na perových plotrech. Vektorové znaky lze podrobit různým transformacím, například otáčení nebo zvětšení. Mezi nevýhody vektorových znaků patří jejich nepěkný vzhled (zobrazujeme vlastně kostru tvaru znaku, kde se tloušťka nahrazuje například vykreslením více čar vedle sebe) a u složitějších fontů, kde je jeden znak reprezentovaný velkým množstvím úseček, také pomalost vykreslování.
  • Jiný způsob, který si v této části popíšeme podrobněji, spočívá v reprezentaci každého znaku bitmapou. Výhodou je možnost definovat tvar znaků s pixelovou přesností a poměrně malá spotřeba paměti na bitmapy (bavíme se o písmech pro obrazovky s relativně malým rozlišením, pro tiskárny díky jejich řádově většímu rozlišení samozřejmě platí poněkud jiné podmínky). Hlavní nevýhodou tohoto způsobu je nemožnost provádět geometrické transformace. Proto je například při zvětšování znaků vhodnější vygenerovat pro každou velikost nové bitmapy a ty potom beze ztráty kvality zobrazit.
  • Předchozí dva způsoby lze dále vylepšit použitím display-listů, kdy se vykreslovací příkazy použité pro každý znak uloží do samostatného display listu. Potom je možné při vypsání celého řetězce zavolat pouze jednu funkci, která zavolá patřičné display-listy a vykreslí tak celý řetězec. Tento postup zajistí větší rychlost vykreslování, protože může omezit množství dat posílaných do grafického akcelerátoru (pokud je ovšem v paměti grafického akcelerátoru dostatek místa pro uložení display-listů).
  • Další způsob spočívá ve vytvoření textury, do které se určitým předem známým způsobem uloží bitmapy jednotlivých znaků. Znaky lze potom vykreslovat nepřímo tak, že se pro každý znak zobrazí čtyřúhelník pokrytý tou částí textury, ve které je obrázek vykreslovaného znaku. Výhodou tohoto přístupu je možnost provádět některé transformace s malou ztrátou kvality (při mapování textur lze použít bilineární interpolaci, mipmaping a antialiasing) a lze také vykreslovat vícebarevné znaky. Mezi nevýhody patří nutnost počítat pro každý vykreslovaný znak souřadnice do textury a větší spotřeba paměti na grafickém akcelerátoru, protože textury jsou většinou interně uloženy ve formátu RGBA, tedy 16, 24 nebo 32 bitů na pixel.

Použití bitmap pro vykreslování znaků

Vykreslování znaků pomocí bitmap je poměrně jednoduchá záležitost, protože samotný příkaz pro vykreslování bitmapy (glBitmap() – viz předchozí díl) nám při vhodně zvolených parametrech může zajistit posun aktuální vykreslovací pozice o šířku právě vykreslovaného znaku, takže tento posun není potřeba programově ošetřovat. Lze tak vykreslovat fonty neproporcionální i proporcionální. Postup si však ukážeme pouze na příkladu neproporcionálního fontu, protože u něj je situace poněkud jednodušší (musíme znát pouze jednu šířku společnou všem znakům ve fontu).

Prvním úkolem je získat bitmapy vhodného fontu. Lze například dekódovat formát fontů použitých pro nastavení konzole v Linuxu. Já jsem však již poměrně dávno (na počítači 486) získal pomocí jednoduché utility (cca 20 řádek assembleru) standardní font BIOSu karty VGA. V tomto fontu je každý znak vytvořen v matici (bitmapě) o rozměrech 8×16 pixelů, ale vykreslování v textovém režimu 80×25 znaků (740×400 pixelů) probíhá do oblasti o rozměrech 9×16 pixelů, protože u většiny znaků je jeden sloupec pixelů vynechán. Výjimku tvoří pouze znaky pro tvorbu rámečků, kde je poslední sloupec pixelů zkopírován. My se však touto výjimkou nebudeme zabývat, všechny znaky nadefinujeme do matice 8×16 a budeme je vykreslovat s jednopixelovou mezerou.

Bitmapy všech znaků je možné uložit do samostatných polí a potom do funkce glBitmap() předávat ukazatele na tyto bitmapy. Výhodnější však je všechny bitmapy zadat do jednoho pole a při volání funkce glBitmap() provést výpočet offsetu od začátku tohoto pole. Výpočet offsetu je jednoduchý, protože každý znak je zadán šestnácti byty (každý byte obsahuje jeden řádek znaku, tedy osm bitových hodnot), takže vystačíme s jednoduchým bitovým posuvem.

Dále musíme pomocí již známých příkazů glColor*() aglRasterPos*() zadat barvu vykreslovaných znaků a pozici vykreslení prvního znaku. Pozice dalších znaků se vypočítá automaticky tak, že ve funkci glBitmap() uvedeme relativní posun aktuální vykreslovací pozice o 9 pixelů doprava. Funkce pro výpis řetězce pomocí bitmap by tedy mohla vypadat například takto:

void drawString(GLfloat red, GLfloat green,
 GLfloat blue, GLint x, GLint y, char *string)
{
 glColor3f(red, green, blue); // specifikace barvy znaků v řetězci
 glRasterPos2i(x, y);         // specifikace pozice vykreslení
                              // prvního znaku v řetězci
 while (*string)              // projít celým řetězcem
    glBitmap(8, 16,         // šířka a výška vykreslovaného znaku
       0.0f, 0.0f,        // relativní pozice počátku bitmapy
       9.0f, 0.0f,        // posun dalšího znaku o 9 pixelů
                           // doprava
             fontbios+((*string++)<<4));
               // ukazatel na 16 bytů s maticí znaku
}

V demonstračním příkladu číslo 13 je ukázán způsob vypsání řetězce výše uvedeným způsobem. Kvůli zkrácení kódu však jsou definovány pouze tvary znaků s ASCII hodnotou od 32 do 128, tedy čísla, interpunkční znaménka a písmena anglické abecedy. K tomuto příkladu je k dispozici i kód s obarvenou syntaxí.

Display-listy

Display-listy si můžeme představit jako makra, do kterých se nahraje několik příkazů OpenGL, a tato makra lze potom jedním příkazem „spustit“. Výhodou display-listů je na jedné straně zvýšená rychlost vykreslování, protože display-listy jsou většinou uloženy přímo v paměti grafického akcelerátoru, na straně druhé také zjednodušení kódu pro vykreslování komplikovaných scén. Je například možné 3D modely jednotlivých těles ukládat do samostatných display-listů a složitou scénu potom vykreslit pouze zavoláním těchto display listů s vhodně nastavenou transformační maticí. Použitím display-listů pro vykreslování 3D scén se budeme zabývat v dalších částech tohoto seriálu.

Začátek záznamu do display-listu se povolí příkazem void glNewList(), ukončení záznamu příkazem void glEndList(). V příkazu void glNewList(GLuint list, GLenum mode) zadáváme dva parametry. První celočíselný parametr list představuje identifikátor vytvořeného display-listu. Pomocí tohoto identifikátoru můžeme display-list vyvolat. Druhý parametr mode určuje, zda se má display-list pouze vytvořit (hodnota GL_COMPILE), nebo vytvořit a přímo provést (hodnotaGL_COM­PILE_AND_EXECU­TE). Většina volání příkazů OpenGL (jsou však výjimky) je do display-listu zaznamenána, ostatní příkazy se samozřejmě provedou.

Provedení display-listu (tj. vyvolání příkazů uložených v display-listu) zajistíme funkcí void glCallList(GLuint list), v jejímž parametru list je uložen identifikátor dříve vytvořeného display-listu.

Uložení bitmap znaků do display-listů

Příkaz glBitmap() pro vykreslení bitmapy lze do display-listu také uložit. Proto můžeme pro každý znak vytvořit jeden display-list a do tohoto display-listu uložit příkaz pro vykreslení bitmapy znaku. Další práci si můžeme dále zjednodušit tak, že identifikátor display-listu bude roven ASCII hodnotě daného znaku. Sekvence příkazů pro vytvoření těchto display listů může vypadat následovně:

for (i=32; i<128; i++) {  // pro každý znak vytvořit jeden
                       // display-list
 glNewList(i, GL_COMPILE); // vytvoření nového display-listu
                           // a začátek záznamu
               // následuje záznam příkazu do
               // display-listu
  glBitmap(8, 16,          // šířka a výška vykreslovaného znaku
    0.0f, 0.0f,            // relativní pozice počátku bitmapy
    9.0f, 0.0f,            // posun dalšího znaku o 9 pixelů
                             // doprava
    fontbios+((*string++)<<4)); // ukazatel na 16 bytů
                          // s maticí znaku
glEndList();                  // konec záznamu do display-listu
}

Funkce pro vykreslení řetězce se zjednodušuje, protože není potřeba volat poměrně složitou funkci glBitmap(), ale mnohem jednodušší funkci glCallList():

void drawString(GLfloat red, GLfloat green,
 GLfloat blue, GLint x, GLint y, char *string)
{
glColor3f(red, green, blue); // specifikace barev všech znaků
                             //řetězce
glRasterPos2i(x, y);         // specifikace pozice vykreslení
                             // prvního znaku
while (*string)              // pro všechny znaky řetězce
   if ((*string)>=32)     // pokud je ASCII hodnota znaku
                             // definovaná ve fontu
    glCallList(*string++); // pro každý znak zavolat příslušný
                   // display-list
}

V demonstračním příkladu číslo 14 je ukázán způsob vypsání řetězce pomocí display-listů, které jsou pro každý vykreslovaný znak explicitně zavolány. K tomuto příkladu je k dispozici i kód s obarvenou syntaxí.

Zjednodušení vykreslování řetězců pomocí display-listů

Předchozí příklad lze ještě dále zjednodušit tak, že místo volání jednotlivých funkcí glCallList() pro každý znak lze zavolat pouze jednu funkci void glCallLists(GLsizei n, GLenum type, const GLvoid *lists), pomocí které lze najednou zavolat více display-listů. Tato funkce má tři parametry. Počet display-listů je zadán v prvním parametru n. Ve druhém parametru type je zadán datový typ posledního parametu (v našem případě použijeme GL_UNSIGNED_BYTE). Ve třetím parametu listsje uložen ukazatel na pole offsetů indexů jednotlivých display-listů. Bázová adresa display-listů může být specifikována příkazem void glListBase(GLuint base). Pro dávkové vygenerování více identifikátorů display-listů je použita funkce glGetLists(GLsizei range), která vrátí číslo prvního vygenerovaného display-listu.

ict ve školství 24

V demonstračním příkladu číslo 15 je ukázán způsob zjednodušeného vypsání řetězce pomocí display-listů. K tomuto příkladu je k dispozici i kód s obarvenou syntaxí.

V dalším pokračování tohoto seriálu si ukážeme práci s takzvanými pixmapami, tedy rastrovými obrázky s vyšší bitovou hloubkou, než mají bitmapy.

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.