Hlavní navigace

Grafická knihovna OpenGL (11): vykreslovací řetězec

10. 9. 2003
Doba čtení: 9 minut

Sdílet

V dnešním dílu si popíšeme vykreslovací řetězec OpenGL, tj. cestu dat od prvotního zadání 2D obrazce či 3D scény v aplikaci až po vykreslení na obrazovku. Pochopení činnosti jednotlivých bloků vykreslovacího řetězce je důležité zejména při programování pokročilých grafických efektů.

Vykreslovací řetězec OpenGL

Knihovna OpenGL se v dnešní době používá především jako programové rozhraní ke grafickým akcelerátorům. Ty jsou na počítačích typu PC (které v jednodušších i složitějších grafických aplikacích postupně nahrazují výkonné grafické stanice) většinou umístěny na port AGP (Advanced Graphics Port, jedná se skutečně o port, který sice ideově vychází z PCI sběrnice, ale nemá arbitráž pro ovládání více zařízení – na AGP tedy lze připojit pouze jednu grafickou kartu). Pokud má počítač pouze jeden procesor, je grafický akcelerátor připojený přes chipset na základní desce přímo k procesoru. Pokud je však v počítači více procesorů, je zapotřebí použít rozhraní (tzv. most – bridge) mezi multiprocesorovou sběrnicí a AGP portem. Schéma připojení grafického akcelerátoru v uniprocesorovém a multiprocesorovém systému je zobrazeno na následujících obrázcích:

Připojení grafického akcelerátoru v uniprocesorovém systému


Připojení grafického akcelerátoru v uniprocesorovém systému.

Připojení grafického akcelerátoru v multiprocesorovém systému


Připojení grafického akcelerátoru v multiprocesorovém systému.

Při vykreslování 2D obrazců či celých 3D scén se do vykreslovacího řetězce (rendering pipeline) OpenGL nahrávají dva odlišné typy dat: údaje o vrcholech a rastrová data. Tato data jsou průběžně vykreslovacím řetězcem zpracovávána, přičemž se na výstupu (ve framebufferu) vytváří rastrová podoba zadané scény. Jednotlivé moduly vykreslovacího řetězce jsou zobrazeny na následujícím obrázku:

Vykreslovací řetězec OpenGL


V dalších podkapitolách budou tyto moduly podrobněji popsány. Všimněme si však nejprve celkové koncepce systému. Vykreslovaná data jsou logicky rozdělena na geometrickou část (vertexy grafických primitiv) a rastrovou část (bitmapy, pixmapy, textury). Tato data jsou ze začátku zpracovávána odděleně a teprve při rasterizaci v modulu označeném Rasterization dojde k jejich vzájemnému spojení. Po vykreslení dat do framebufferu je možné některé údaje z framebufferu přečíst zpátky do hlavní paměti procesoru nebo již vykreslený snímek zkombinovat se snímkem, který právě vzniká. Tato technika se používá například při blendingu nebo vykreslení scén se zrcadlícími povrchy.

Důležité je, že jednotlivé moduly pracují relativně nezávisle na ostatních modulech a mezi moduly se nepředávají žádné řídící informace. Vše je řízeno pouze daty (jedná se tedy o architekturu data-flow), která do vykreslovacího řetězce postupně vstupují, což je významný rozdíl například vůči klasickému procesoru, který je řízen programem. Z tohoto faktu také vyplývá velká rychlost vykreslování pomocí grafických akcelerátorů, která je umožněna čtyřmi faktory:

  1. Geometrická a rastrová data jsou zpracovávána odděleně odlišnými jednotkami, které tak mohou pracovat paralelně. Při zpracování rastrových dat jsou výpočetní jednotky celkem jednoduché, a proto jich může být implementováno ví­ce.
  2. Moduly zobrazené na předchozím obrázku mohou pracovat nezávisle na sobě a jsou řízeny pouze daty. Proto lze vytvořit zřetězené zpracování dat (pipeline) na vysoké úrovni, kde jednotlivé moduly představují řezy pipeline.
  3. Uvnitř modulů se provádějí operace, které lze rozložit na velké množství jednodušších operací, a ty lze opět vykonávat zřetězeně. Například násobení matice vektorem lze rozložit na jednotlivá násobení a sčítání prvků matice a vektoru, která se provádějí za sebou. V jednom modulu může tato pipeline obsahovat i stovky řezů.
  4. Tok většiny dat (zejména geometrických) je jednosměrný, takže se používá technika streamingu a zmenší se tok dat přenesených mezi grafickým čipem a pamětí na grafickém akcelerátoru.

Z těchto faktorů vyplývá, proč jsme v posledním desetiletí svědky tak velkého nárustu rychlosti grafických akcelerátorů na personálních počítačích. Není toho dosaženo nijak závratnou technologií při výrobě grafických čipů ani grafických pamětí (jedná se většinou o obyčejné dynamické paměti), ale důsledným využitím zřetězeného zpracování. Z toho také vyplývá, že se při vykreslování malého množství trojúhelníků ve scéně nedosáhne maximálního výkonu grafické karty, ale pokud počet trojúhelníků stoupá do tisíců, začíná se výhoda zřetězeného zpracování dat výrazně projevovat. V dalších odstavcích budou podrobněji popsány jednotlivé moduly zobrazené na předchozím obrázku:

Vertex data

Jedním ze dvou typů dat, které vstupují do vykreslovacího řetězce OpenGL, jsou údaje o vrcholech grafických primitiv (vertexech). Pro každý vertex musíme zadat minimálně jeho souřadnice příkazem glVertex*()(sou­řadnice jsou zadány ve dvojici [x, y], trojici [x, y, z] nebo čtveřici [x, y, z, w]). Dále je možné u každého vertexu specifikovat následující parametry:

  1. True-color barvu vertexu příkazem glColor*(), kde barva je zadána buď třemi složkami RGB (Red, Green, Blue), nebo čtyřmi složkami RGBA (Red, Green, Blue, Alpha). Pro každý vertex grafické primitivy lze zadat jinou barvu, barvy mezilehlých fragmentů se dopočítají lineární interpolací.
  2. Barvu z předdefinované palety (pokud OpenGL pracuje v index-color režimu). Tato barva se zadává příkazem glIndex*() s jedním parametrem představujícím index do předem vytvořené palety. Index-color režim se v dnešní době již příliš nepoužívá, protože mnoho vlastností OpenGL v tomto režimu nelze aplikovat. Jedinou podstatnou výhodou index-color režimů je menší spotřeba paměti na color buffer.
  3. Normálu vertexu pomocí příkazu glNormal*(). Normála se využije při výpočtu osvětlení a při odstraňování odvrácených plošek tělesa. Vzhledem k tomu, že normálu lze pro každý vertex zadat libovolně, je možné aplikovat různé grafické efekty, například bump mapping. Normály lze nechat vypočítat i automaticky, ale při opakovaném vykreslování rozsáhlých scén může opakovaný výpočet normál znamenat značné zatížení systému, proto je většinou výhodnější provést výpočet normál pouze jedenkrát v inicializační části programu.
  4. Texturovací souřadnice pomocí příkazu glTexCoord*(). OpenGL podporuje rastrové textury ve třech formátech: jednorozměrné, dvourozměrné a třírozměrné. Při texturování je pro každý vertex zapotřebí zadat souřadnice do právě aktivní (vybrané) textury. Souřadnice v textuře se při vykreslování lineární, bilineární nebo trilineární interpolací automaticky dopočítávají (je možné provádět i perspektivní korekci). Texturováním se budeme podrobněji zabývat v dalších dílech tohoto seriálu.
  5. Parametry pro vyhodnocení mapovací funkce pomocí příkazu glEvalCoord*(). Pomocí mapovací funkce lze vypočítat například souřadnice vertexu nebo jeho normálu. Pokud se pomocí mapovací funkce vyhodnocuje souřadnice vertexu, lze zapnutím automatického generování normál povolit výpočet normál přímo z mapovací funkce.
  6. Bod v předem zadané mřížce pomocí příkazu glEvalPoint*(). Tento příkaz se používá při vyhodnocení hodnot zadaných do mřížky. Parametrem (resp. parametry) tohoto příkazu jsou indexy do této mřížky.
  7. Parametry materiálu vertexu pomocí příkazu glMaterial*(). Tímto příkazem lze pro každý vertex zadat jiné optické vlastnosti materiálu, ze kterého se skládá výsledné těleso. Tyto vlastnosti (podobně jako například barva) jsou potom pro každý fragment automaticky dopočítány pomocí lineární interpolace.
  8. Vlastnosti hrany, která začíná právě zadávaným vertexem pomocí příkazu glEdgeFlag*(). Pro každou hranu lze určit, zda je hraniční, či nikoliv. Tento příznak se potom použije například při vykreslování drátového modelu, kdy hrany, které nejsou hraniční, nemusí být vykresleny.

Pixel data

Druhým typem dat, která vstupují do vykreslovacího řetězce, jsou rastrová data. Ta se používají ve třech případech:

  1. Vykreslování bitmap (jednobarevných rastrových obrázků). Bitmapy se používají zejména pro výpis řetězců. Vzhledem k tomu, že u bitmap je pro každý pixel rezervovaný pouze jeden bit, je nutné před vlastním vykreslováním příkazem glColor*() zadat barvu bitmapy.
  2. Vykreslování rastrových obrázků s vyšší barevnou hloubkou. V terminologii OpenGL se tyto obrázky nazývají pixmapy. Pixmapy mohou být zadány v několika barevných modelech a jsou podporovány různé barevné hloubky, aby bylo možné použít různé typy rastrových obrázků bez nutnosti programové konverze.
  3. Pokrytí povrchu těles texturou. Textury se obecně po jejich zadání nikam přímo nevykreslují, ale zůstávají v texturovací paměti. Odtud je lze použít při nanášení na povrch těles. V ideálním případě je texturovací paměť umístěna přímo na grafickém akcelerátoru, ale je možné, že při větším množství velkých textur se tyto umístí do hlavní paměti počítače.

Display list

Jak vertexy těles, tak rastrová data lze uložit do takzvaných display listů. Display listy si lze zjednodušeně představit jako část paměti na grafickém akcelerátoru nebo v hlavní paměti počítače, do níž se ukládá posloupnost příkazů OpenGL, kterou lze následně jedním příkazem vyvolat. Podrobněji se budeme display listy zabývat v dalších dílech.

Evaluators

Evaluátory jsou použity zejména při vykreslování parametrických ploch nebo kvadrik. Pokud jsou evaluátory použity, jsou údaje na jejich vstupu brány jako vlastnosti řídících bodů parametrických ploch. Tyto plochy jsou následně vyhodnoceny a na výstupu tohoto bloku se objeví údaje o jednotlivých vrcholech (vertexech).

Per-vertex operation and primitive assembly

Na každý vertex (specifikovaný buď příkazem glVertex*(), uložený v display listu, nebo vyhodnocený pomocí evaluátorů) jsou aplikovány per-vertex operace. Jedná se zejména o transformaci souřadnic vertexu s použitím matice ModelView. Pokud jsou specifikovány i texturovací souřadnice, jsou transformovány s použitím Texture matice. Dále může být na základě údajů o materiálu vertexu a jeho normály vypočteno osvětlení.

Jednotlivé vertexy mohou být dále ořezány některou předem zadanou ořezávací rovinou. Pokud je vertex umístěn za ořezávací rovinou, je odstraněn z dalšího zpracování. V případě úseček nebo polygonů mohou být po ořezání další vertexy přidány, aby se vykreslil zbytek viditelné části primitiva.

Po ořezání jsou souřadnice vertexů transformovány pomocí transformační matice Projection, kde je u 3D scén většinou provedena perspektivní projekce. Po této transformaci je provedeno další ořezání, teď už však pro šest základních ořezávacích rovin (jsou paralelní s osami souřadného systému).

Pixel operations

Operace nad pixely spočívají v dekódování barev jednotlivých pixelů z formátu zadaného aplikací do interního formátu, který je na grafické kartě používán. Po tomto dekódování (které spočívá v rozložení -unpack- tří nebo čtyř barevných složek, jejich případném prohození -swap-, posunutí -bias-, změně kontrastu -scale- a přemapování -map-) se pixely buď uloží do texturovací paměti jako rastr textury, nebo jsou přímo poslány dále do rasterizační jednotky.

Rasterization

Grafická primitiva specifikovaná svými vrcholy jsou vykreslena tak, že se vygenerují ty fragmenty, které dané primitivum pokrývá. Toto vygenerování se nazývá rasterizace. Při rasterizaci je zapotřebí brát v úvahu jak transformované souřadnice jednotlivých vertexů, tak i případná rastrová data (textury nebo pixely bitmap a pixmap).

Texture assembly

V tomto bloku se provádí různé algoritmy nad daty uloženými v texturovací paměti a rastrovými daty, která posílá aplikace. Jedná se většinou o jednoduché (a tím i rychlé) algoritmy pro kombinování více textur, mipmaping apod. Textury jsou v ideálním případě uloženy přímo v paměti grafického akcelerátoru. V horším případě jsou uloženy v hlavní paměti počítače a v době, kdy jsou zapotřebí pro vykreslování, jsou postupně nahrávány do grafického akcelerátoru, což může znamenat značné zatížení sběrnice procesoru a portu AGP.

Per-fragment operations

Pokud je povoleno texturování, je pro každý fragment vygenerován jeden pixel z texturovací paměti nazývaný texel. Tento texel je potom na fragment aplikován. V nejjednodušším případě je barva fragmentu změněna přímo na barvu texelu, ve složitějších případech se mohou provést další operace nad daty fragmentu a texelu, například blending.

Dále je pro každý fragment proveden výpočet mlhy (fog) a antialias. Poté se provedou operace s jednotlivými buffery (pokud jsou povoleny), tedy test na hodnotu alfa složky fragmentu (alpha test), test s hodnotou ve stencil bufferu (stencil test) a test s hloubkou uloženou v Z-bufferu (depth test). Následně může být proveden dithering a v true-color režimu se může provést blending (smíchání barev více fragmentů, které obsazují stejné místo na obrazovce).

Všechny tyto operace se samozřejmě mohou buď povolit, nebo zakázat. Pokud všechny operace nad fragmenty zakážeme, vykreslí se grafická primitiva postupně tak, jak jsou posílána do vykreslovacího řetězce (mohou se tedy postupně překreslovat) a není nijak změněna jejich barva. Zapnutím texturování, mlhy, antialiasingu, ditheringu nebo operací s buffery lze zásadně ovlivnit výsledný vzhled jednotlivých fragmentů a tím i vzhled celé scény.

UX DAy - tip 2

Framebuffer

Výstup je uložen do takzvaného framebufferu, který se skládá z několika samostatných bufferů. Mezi nejdůležitější buffery patří barvový buffer (color buffer), paměť hloubky (Z-buffer), paměť šablony (stencil buffer) a akumulační buffer (accumulation buffer). Podobně, jako jsou v bitmapách uloženy jednotlivé pixely, jsou ve framebufferu uloženy takzvané fragmenty, které představují průřez všemi buffery. Pro uživatele je jistě nejdůležitější color buffer, neboť v něm jsou uloženy barvy všech fragmentů. Tento buffer je většinou přímo zobrazen na obrazovce, i když jsou možné i další (netypické) režimy zobrazení.

Významem jednotlivých bufferů, ze kterých se framebuffer skládá, se budeme podrobněji zabývat v dalším díle tohoto seriálu.

Byl pro vás článek přínosný?

Autor článku

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