Grafická knihovna OpenGL (16): pole vrcholů (vertex arrays)

14. 10. 2003
Doba čtení: 8 minut

Sdílet

V dnešním dílu seriálu věnovaného programování počítačové grafiky s knihovnou OpenGL si popíšeme použití takzvaných polí vrcholů (vertex arrays), pomocí kterých je možné značně zredukovat množství dat, která se při vykreslování prostorové scény musí posílat přes sběrnici počítače do grafického akcelerátoru.

Pole vrcholů

V úvodních dílech tohoto seriálu (zejména ve třetí a čtvrté části) jsme si řekli, že pomocí grafické knihovny OpenGL je možné vykreslovat pouze deset základních grafických prvků – grafických primitiv. Veškeré složitější tvary těles se musí složit z těchto grafických primitiv. Kromě obecných vlastností těchto prvků je zapotřebí zadávat i vlastnosti jednotlivých vrcholů. Mezi tyto vlastnosti, které se typicky nastavují uvnitř programových závorekglBegin() a glEnd(), patří:

  • Pozice vrcholu (vertexu) v rovině či prostoru. Nastavuje se příkazem glVertex*(), přičemž lze zadat dvě souřadnice (rovina), tři souřadnice (prostor), nebo čtyři souřadnice (prostor+„váha“ vrcholu).
  • Barva vrcholu, která se nastavuje příkazem glColor*(). Barva plošky mezi vrcholy (stejně jako některé další parametry) může být dopočítána pomocí interpolace.
  • Index barvy vrcholu v barevné paletě, který se nastavuje příkazem glIndex*(). Barevné palety a indexované barvy byly do knihovny OpenGL zařazeny pro podporu vykreslování na starších grafických subsystémech, které nepodporovaly režim True-Color. V dnešní době ztrácí barevné palety smysl (kromě diskutabilní animace pomocí změny jednotlivých barev) a přestávají být také v nových grafických kartách podporovány, proto se jimi dále nebudeme zabývat.
  • Normálový vektor ve vrcholu, který se nastavuje příkazem glNormap*(). Normála (tj. vektor kolmý k povrchu) vrcholu je sice z matematického hlediska nesmysl, ale v počítačové grafice se normály rovin, které se ve vrcholech stýkají, sčítají a výsledek se používá například při výpočtech osvětlení.
  • Souřadnice do textury, která se nastavuje příkazem glTexCoord(). Tato souřadnice se používá při texturování, kterým se budeme podrobněji zabývat v jednom z dalších dílů.
  • Mapovací souřadnice zadávaná příkazem glEvalCoord*(). Mapováním se taktéž budeme zabývat v následujících dílech.
  • Jednorozměrný či dvourozměrný bod v síti (mesh), který se zadává příkazem glEvalPoint*().
  • Optické vlastnosti materiálu zadané příkazem glMaterial().
  • Vlastnosti hrany (tj. zda má být hrana viditelná, či nikoliv) nastavované příkazem glEdgeFlag().

Vlastnosti jednotlivých vrcholů se v nejjednoduším případě zadávají přímým voláním jednoduchých funkcí, například glVertex(), glColor(), glNormal() a glMaterial(). Toto řešení je snadno pochopitelné i použitelné. Nevýhodou však je, že pro vykreslení složitých prostorových scén by se muselo těchto funkcí volat několik tisíc i desítek tisíc (podle složitosti těchto scén, tj. počtu vrcholů) – viz například první obrázek, kde je pro zobrazení jednoduché krychle zapotřebí volat 6×4=24krát funkci glVertex*().

Počet volání funkce glVertex() pro jednoduché těleso - krychli

Obrázek 1: Počet volání funkce glVertex() pro jednoduché těleso – krychli

Volání každé funkce s sebou nese poměrně značné množství práce pro procesor, zejména přesun dat mezi registry procesoru, pamětí a zásobníkem a také skok do funkce a návrat z funkce (snížení účinnosti cache paměti z hlediska lokálnosti odkazů a přepínání mezi ringy). Proto by bylo vhodné co nejvíce snížit množství volání funkcí nutných pro vykreslení dané scény. Existuje více možností vedoucích ke snížení počtu volaných funkcí, které se liší svou složitostí, vyjadřovací schopností (tj. které grafické efekty lze, popř. nelze vytvořit) a rychlostí vlastního vykreslení:

  1. První možností, která je pro použití nejjednodušší, je využití faktu, že se OpenGL chová jako stavový automat, tj. pokud zadáme nějakou vlastnost, zůstane tato vlastnost nastavena až do doby, než ji změníme. Pro vykreslení jednobarevného trojúhelníku můžeme tedy místo kódu:
    glBegin(GL_TRIANGLES);
        glColor3f(1.0f, 1.0f, 1.0f);
        glVertex2i(100, 100);
        glColor3f(1.0f, 1.0f, 1.0f);
        glVertex2i(200, 100);
        glColor3f(1.0f, 1.0f, 1.0f);
        glVertex2i(150, 150);
    glEnd();

    napsat pouze:

    glColor3f(1.0f, 1.0f, 1.0f);
    glBegin(GL_TRIANGLES);
        glVertex2i(100, 100);
        glVertex2i(200, 100);
        glVertex2i(150, 150);
    glEnd();
    čímž jsme ušetřili dvě volání funkce glColor3f(). Pro komplikovanější tělesa je samozřejmě efekt ještě větší.
  2. Druhou možností je použití těch grafických primitiv, u kterých se redukuje počet přenesených vrcholů. Jedná se o již popsané trsy a pruhy trojúhelníků a o pruh čtyřúhelníků (triangle fan, triangle strip, quad strip). Redukce přenesených vrcholů spočívá v tom, že vrcholy společné více hranám (a tím pádem i ploškám) jsou přeneseny pouze jednou, jak je ukázáno na následujícím příkladu (jedná se o jednoduché vyjádření pláště kuželu):

    glBegin(GL_TRIANGLES);
    for (f=0.0f; f<6.28; f+=0.5f) {
    glVertex3f(0.0f, 0.0f, 100.0f);
    glVertex3f(100.0f*cos(f), 100.0f*sin(f), 0.0f);
    glVertex3f(100.0f*cos(f+0.5f), 100.0f*sin(f+0.5f), 0.0f);
    }
    glEnd();

    lze zredukovat na:

    glBegin(GL_TRIANGLE_FAN);
    glVertex3f(0.0f, 0.0f, 100.0f); // vrchlik jehlanu
    glVertex3f(100.0f, 0.0f, 0.0f); // prvni vrchol na spodnim okraji
    for (f=0.5f; f<6.28; f+=0.5f) {
    glVertex3f(100.0f*cos(f), 100.0f*sin(f), 0.0f);
    }
    glEnd();

  3. Třetí možností je použití display listů, kdy se příkazy nutné pro vytvoření tělesa zapíšou do display listu a ten se poté zavolá pomocí jediné funkce – viz též osmý díl seriálu. Výhodou tohoto postupu je, že data uložená v display listu jsou v ideálním případě zapsána přímo do paměti grafického akcelerátoru a při každém vykreslení tedy není nutné tato data přenášet po sběrnici, což je v dnešní době (se současnými sběrnicemi a grafickými akcelerátory) nejvíce zpomalující operace (bottle neck) při vykreslování trojrozměrných scén.
  4. Čtvrtou možností, kterou se dnes budeme hlouběji zabývat, je použití pole vrcholů (vertex arrays), kdy jsou data pro jednotlivé vrcholy uložena ve vhodně organizovaném poli a následně je umožněno poslání dat vrcholů pomocí několika málo funkčních volání. Výhodou tohoto přístupu je, že se mezi jednotlivými snímky mohou měnit data jednotlivých vrcholů (barva, poloha apod.), což v případě display listů nebylo možné, protože by se při jakékoliv změně musel vytvořit nový display list.

Povolení použití pole vrcholů v aplikacích

Před prvním použitím pole vrcholů v programu je zapotřebí pole vrcholů povolit zavoláním funkce glEnableClien­tState(array), kde parametrarray může nabývat několika hodnot:

  • GL_COLOR_ARRAY – v poli je možné mít uloženy RGB barvy vrcholů (používá se v True-color RGB barevném režimu).
  • GL_INDEX_ARRAY – v poli je možné mít uloženy indexové barvy vykreslování (používá se paletovém barevném režimu).
  • GL_EDGE_FLAG_ARRAY – v poli je možné mít uloženy příznaky hrany, tj. zda má být vykreslovaná hrana viditelná.
  • GL_NORMAL_ARRAY – v poli je možné mít uloženy normálové vektory ve vrcholech.
  • GL_TEXTURE_CO­ORD_ARRAY – v poli je možné mít uloženy souřadnice do textury.
  • GL_VERTEX_ARRAY – v poli je možné mít uloženy souřadnice vrcholů v rovině či prostoru.

Zakázání použití pole vrcholů

Pokud některé z těchto polí nebudeme chtít použít, je možné zavolat funkci glDisableClien­tState(array), kde parametr array nabývá stejných hodnot jako u předchozí funkce. Stav povolení či zakázání některého typu pole lze zjistit voláním funkce glIsEnabled().

Vytvoření pole vrcholů

Druhým krokem je vlastní vytvoření pole vrcholů. Může se jednat například o klasické céčkovské pole (buď statické, nebo dynamické). Toto pole se musí naplnit relevantními daty a poté předáme knihovně OpenGL ukazatel na toto pole. To se provede příkazem gl*Pointer(), kde se za znak * doplní typ pole. To znamená, že podle typu uložených dat existují funkce:
glVertexPointer(),
glColorPointer(),
glIndexPointer(),
glNormalPointer(),
glTexCoordPointer() a
glEdgeFlagPoin­ter().
Význam parametrů těchto funkcí si ukážeme například na poli obsahujícím souřadnice vrcholů. Funkce má deklaraci:

void glVertexPointer(
    GLint size,
    GLenum type,
    GLsizei stride,
    const GLvoid * pointer
);

Kde jednotlivé parametry mají tento význam:

  1. size – udává počet položek (souřadnic) na jeden vrchol, tj. povolené hodnoty jsou: 2 (plocha), 3 (prostor) nebo 4 (prostor+„váha“ vrcholu).
  2. type – udává datový typ položek odpovídající datovým typům samotné knihovny OpenGL: GL_SHORT, GL_INT, GL_FLOAT neboGL_DOUBLE.
  3. stride – udává mezeru v bytech mezi jednotlivými položkami. Při nule jsou položky v paměti uloženy těsně za sebou, což je většinou případ céčkových polí (pokud ovšem nejsou použity pole struktur, kde se může provádět zarovnání).
  4. pointer – udává ukazatel na pole s informacemi o vrcholech.

Výběr prvků z pole a zobrazení jednotlivých vrcholů

Po povolení použití polí vrcholů a naplnění těchto polí je zapotřebí vhodným způsobem vybrat prvky z pole a vykreslit jednotlivé vrcholy. Je nutné si uvědomit, že pole zůstává uloženo na straně klienta (v případě PC v paměti procesoru) do té doby, dokud poprvé nepřistoupíme k jeho prvkům. Poté jsou pole přenesena na server (v případě PC na grafický akcelerátor, ale zde hodně záleží na použitém ovladači a velikosti volné paměti na grafickém akcelerátoru). Změníme-li později hodnoty v poli, musíme si vynutit zaslání nového pole na server.

Prvky pole lze vybrat třemi různými způsoby:

  1. Po jednotlivých vrcholech funkcí void glArrayElemen­t(GLint i), která vybere data z i-tého prvku ze všech zapnutých polí. Zavolání této funkce má stejný efekt jako volání funkce glVertex(), glColor() atd. Tento způsob se jako jediný používá mezi příkazovými závorkami glBegin() a glEnd().
  2. Zadáním seznamu několika vrcholů pomocí funkce void glDrawElements(GLe­num mode, GLsizei count, GLenum type, void *indices). Tato funkce vybere count vrcholů ze všech zapnutých polí, přičemž posloupnost indexů vybíraných vrcholů je uložena v poli indices. Typ tohoto pole pak určuje parametr type, který může nabývat hodnot GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT nebo GL_UNSIGNED_INT. Parametr mode určuje, jaká geometrická primitiva mají být z vybíraných vrcholů konstruována (GL_TRIANGLES, GL_POLYGON atd., tedy stejná jako parametr funkce glBegin()).
  3. Zadáním seznamu několika po sobě jdoucích vrcholů pomocí funkce void glDrawArrays(GLenum mode, GLint first, GLsizei count). Tato funkce vybere count vrcholů, které jsou uloženy v poli za sebou. Index prvního vrcholu je určen parametrem first.

Význam funkce glDrawElements()

Zavolání funkce glDrawElements(mo­de, count, type, indices) má stejný efekt jako tento kód:

glBegin(mode);
    for (int i = 0; i < count; i++)
        glArrayElement(indices[i]);
glEnd();

Význam funkce glDrawArrays()

Zavolání funkce glDrawArrays(mode, first, count) má stejný efekt jako tento kód:

glBegin(mode);
    for (int i = 0; i < count; i++)
        glArrayElement(first + i);
glEnd();

Demonstrační příklady

prvním demonstračním příkladu (HTML verze) je ukázáno použití funkce glArrayElement(). Výběr způsobu vykreslování se provádí pomocí kontextového menu, které se zobrazí po stisknutí pravého tlačítka myši. Také je možné použít funkční klávesy F1-F2.

Ve druhém demonstračním příkladu (HTML verze) je ukázáno použití funkce glDrawArrays(). Funkčnost předchozího příkladu zůstává zachována.

ict ve školství 24

Ve třetím demonstračním příkladu (HTML verze) je ukázáno použití funkceglDrawA­rrays(). Funkčnost předchozího příkladu zůstává zachována.

Pro majitele pomalejších linek je zde k dispozici celý článek i s přílohami.

Autor článku

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