Na začátek bych chtěl upozornit, že nejsem expert na OpenGL. Právě naopak, jsem samouk a veškeré poznatky, které bych zde chtěl shrnout, jsem nabral při studiu a pokusech v rámci své bakalářské práce (radiozita na GPU).
Historie
Zde toho moc nepovím, protože jsem starší OpenGL (1) používal pouze velmi zlehka. Každý správný tutoriál vám ale poví, že s příchodem verze 3 se radikálně změnil přístup k vykreslování a vlastně vůbec všemu. Dříve se objekty kreslily pomocí příkazů na vykreslení primitiv, obalených v bloku (glBegin .. glEnd). Tohle už není pravda a v současnosti se používá přístup, který se lehce podobá použitím dosplay listů v dřívějších verzích.
Zakládá se na tom, že grafické karty mají stále větší paměti (omezující je šířka sběrnice počítače, a proto je přesun dat o modelech z paměti počítače do paměti karty drahá operace) a jsou plně univerzální, tedy programovatelné. Myšlenka je tedy taková, že do katry nasypeme hromadu dat a kartu poté naprogramujeme tak, aby věděla jak má data interpretovat, tedy kreslit.
Převést tuto myšlenku do praxe mi dalo při práci na BP zatím nejvíce zabrat, a jelikož článků a návodů je na internetu minimum (asi nejlepší zdroj je fórum GameDev.net), rozhodl jsem se sepsat vlastní poznatky a třeba pomoct někomu, kdo se ocitne ve stejné pozici jako nedávno já a bude hledat nějaké OpenGL čtivo.
Data
Budu předpokládat, že data máme v takové formě, která nám dovoluje získat souřadnice všech vrcholů modelů, indexy (vazby) a případně barvy či jiné vlastnosti (např. normály). Kreslení probíhá po primitivech, nejčastěji trojúhelnících (čtverce jsou aktuálně deprecated a je možné že tento trend bude pokračovat ve prospěch trojúhelníků). Kreslící části programu předáme souřadnice vrcholů (například osm vrcholů krychle * 3 hodnoty) a tzv. indexy, které určují propojení mezi vrcholy.
Pro jednoduchý trojúhelník by indexy byly 0, 1, 2. Pokud bychom chtěli kreslit čtverec (třeba stěnu krychle), rozsekneme jej úhlopříčkou na dva trojúhelníky a ty vykreslíme. Za předpokladu, že jsou vrcholy čtverce definované po řadě a ne na přeskáčku, mohly by indexy vypadat například takto: 0, 1, 2, 0, 2, 3. Kreslící funkce potom ví, že bude kreslit trojúhelníky, a bude brát vždy tři hodnoty a z nich složí jeden trojúhleník. Pokud bychom přepnuli na kreslení úseček, stejná data by vyprodukovala tři úsečky (0, 1; 2, 0 a 2, 3). Nad tímto chováním se chvíli zamyslete a ujistěte se, že jej chápete, bude to potřeba.
VBO – Vertex Buffer Object
Začneme tím, že si data připravíme. Jak jsem již říkal, data nejsou nijak strukturovaná a v tomto kroku si je můžeme představit jako obrovské pole nějakých hodnot. Tomuto poli se říká VBO a jde o jakýsi kontejner na data, která se později nahrají do paměti grafické karty. Opravdu nic víc neumí, jen schraňuje data.
Řekněme, že chcete vykreslit model definovaný vrcholy, kde vrchol (vertex) má tři souřadnice datového typu float. Navíc byste rádi, aby byl objekt barevný. Pro jednoduchost nebudeme uvažovat textury, ale pouze barvu pro každý vrchol (mezi vrcholy se barva interpoluje). Pro každý vrchol tedy budeme potřebovat 6 float hodnot. Když bude mít model 100 vrcholů, bude mít naše VBO velikost 600 prvků. Zajímavé je, že data do něj budeme skládat lineárně, nejdříve souřadnice a poté barvy. Na každém (i*6)-ém indexu tedy bude první hodnota souřadnice následovaná dalšími dvěma a pak třemi floaty pro barvu. Doufám že je to pochopitelné, protože schopnost dopočítat se k pozicím dat bude později důležité.
Náš buffer bychom mohli vytvořit takto
unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, pocetHodnot * sizeof(float), ukazatelNaData, GL_STATIC_DRAW);
Nejdříve vytvoříme proměnnou, která bude uchovávat id bufferu – pomocí ní budeme později vybírat, který buffer chceme použít. Jestli je lepší použít jeden VBO se všemi daty anebo použít VBO na vrcholy, další na data a podobně, to zatím netuším a zřejmě záleží na preferencích programátora. Funkční jsou oba přístupy. Buffer s indexy ale musí být vždy samostatný, jelikož je jiného typu, a všechna data v něm jsou vždy interpretovaná jako indexy.
Dále buffer vybereme jako aktivní (bind), tedy následující operace s bufferem budou prováděny nad právě aktivním bufferem. Třetím příkazem do bufferu nasypeme data; jde o obdobu příkazu memcpy.
Důležité je také zvolit správný typ bufferu. Těch je několik a můžete se o nich dočíst v dokumentaci. Zjednodušeně by se dalo říct, že GL_ARRAY_BUFFER slouží k obecnému použití, zatímco GL_ELEMENT_ARRAY_BUFFER je speciální buffer k uložení indexů.
Kouzelný indexový buffer
Ke kreslení je sice ještě daleko. Chtěl bych ale zmínit důležitou věc, a to vazbu mezi indexy a ostatními daty. Přestože se následující může zdát zřejmé, trvalo mi nějakou dobu než jsem si tuto funkčnost uvědomil.
Mějme buffer s indexy pro čtverec (složený ze dvou trojúhelníků): 0, 1, 2, 0, 2, 3. A dále buffer se souřadnicemi vrcholů. Zároveň předpokládejme, že jsme kreslící funkci nějak dali vědět, že kreslí trojúhelníky a že každé tři hodnoty v bufferu souřadnic odpovídají jednomu vrcholu.
První věc co mě při pohledu na příklad někde na internetu napadla, bylo že v souřadnicovém bufferu musím mít velikostIndexBufferu * 3 hodnot souřadnic. To je ale špatný přístup! Zrádné je, že produkuje celkem uvěřitelné výsledky a ne vždy se může chyba projevit.
Správný postup je mít v souřadnicovém bufferu pouze čtyři * 3 hodnoty souřadnic. Oproti ostatním bufferům totiž indexový buffer funguje tak, že hodnoty indexů jsou odkazy do ostatních bufferů.
Kreslící funkce tedy bere indexy z bufferu a pomocí nich sesbírá data z ostatních (běžných) bufferů a ta potom použije k vykreslení. Jestliže tedy přijde například index 2, podívá se funkce do bufferu souřadnic na pozici 2 * 3, kde načte tři po sobě jdoucí hodnoty. (Stále předpokládáme, že funkce ví že kreslí trojúhelníky a že každý vrchol má tři souřadnice.)
Opět je dobré se nad tímto trochu zamyslet a přebrat si to. Důležité je to zejména ve chvíli, kdy připravujete data pro poslání do grafické karty. Musíte totiž udržovat správné vazby mezi indexy a daty v ostatních bufferech.
Prokládání dat
Na závěr bych ještě rád zmínil prokládání dat, tedy možnost uložení více vlastností v jednom VBO. Už jsem říkal, že není nutné mít pro každou vlastnost jeden buffer a lze data uložit třeba jen do jediného (+1 speciální buffer pro indexy). V takovém případě používá OpenGL „adresování“ pomocí počtu hodnot a offsetu. Zatím jsem stále mluvil o tom, že kreslící funkci dáme „nějak“ vědět, jak má neorganizované pole dat interpretovat. Detaily teď nebudu rozvádět, o tom bych se rád zmínil později, v chystaném článku o VAO, atributech shaderů a dalších věcech.
Při definování kde v bufferu je která vlastnost se používá offsetu a počtu hodnot. Najdete je například jako parametry funkce glVertexAttribPointer. Mějme opět VBO, kde je pro každý index z indexového bufferu uložená barva vrcholu (3 hodnoty), jeho souřadnice (3 hodnoty) a třeba ještě pozice textury (2 hodnoty).
Data tedy budou vypadat takto (první indexy jsou vždy vrcholy):
barva[0][0], barva[0][1], barva[0][2], pozice[0][0], pozice[0][1], pozice[0][2], textura[0][0], textura[0][1], barva[1][0], barva[1][1], barva[1][2], pozice[1][0],…
Barvu potom definujeme jako 3 po sobě jdoucí hodnoty, kde offset (stride, mezera) mezi dvěma barvami je 5 hodnot : 3 hodnoty souřadnice + 2 hodnoty textury. (Pozor, offset je v bajtech, proto nezapomeňte násobit sizeof(float) ). A ještě doplníme, že první hodnota má offset (v rámci VBO) 0, tedy začíná hned z kraje.
Podobně například texturu nadefinujeme jako 2 hodnoty, stride bude 6 : 3 hodnoty souřadnice + 3 hodnoty barv, a offset v rámci VBO bude 6, protože z kraje bufferu předchází textuře šest hodnot. Opět zřejmě použijete ukazatelovou aritmetiku, proto nezapomeňte násobit velikostí použitého datového typu.