Lekce 45 - Vertex Buffer Object (VBO)

Jeden z největších problémů jakékoli 3D aplikace je zajištění její rychlosti. Vždy byste měli limitovat množství aktuálně renderovaných polygonů buď řazením, cullingem nebo nějakým algoritmem na snižování detailů. Když nic z toho nepomáhá, můžete zkusit například vertex arrays. Moderní grafické karty nabízejí rozšíření nazvané vertex buffer object, které pracuje podobně jako vertex arrays kromě toho, že nahrává data do vysoce výkonné paměti grafické karty, a tak podstatně snižuje čas potřebný pro rendering. Samozřejmě ne všechny karty tato nová rozšíření podporují, takže musíme implementovat i verzi založenou na vertex arrays.

V tomto tutoriálu budeme

Jako vždy nejdříve nadefinujeme parametry aplikace. První dvě symbolické konstanty představují rozlišení výškové mapy a měřítko pro vertikální roztáhnutí (viz. tutoriál 34 o výškových mapách). Když nadefinujete třetí konstantu, v programu se vypne používání VBO... abyste snadno mohli porovnat rychlostní rozdíl.

// Parametry výškové mapy

#define MESH_RESOLUTION 4.0f// Počet pixelů na vertex

#define MESH_HEIGHTSCALE 1.0f// Měřítko vyvýšení

//#define NO_VBOS// Vypíná VBO

K definicím také musíme přidat konstanty, datové typy a ukazatele na funkce pro VBO rozšíření. Zahrnul jsem jen parametry nutné pro toto demo. Pokud potřebujete více funkcionality, doporučuji z http://www.opengl.org/ stáhnout nejnovější glext.h a použít definice obsažené v něm. Pro kód to jistě bude čistější metoda.

// Rozšíření VBO z glext.h

#define GL_ARRAY_BUFFER_ARB 0x8892

#define GL_STATIC_DRAW_ARB 0x88E4

typedef void (APIENTRY * PFNGLBINDBUFFERARBPROC) (GLenum target, GLuint buffer);

typedef void (APIENTRY * PFNGLDELETEBUFFERSARBPROC) (GLsizei n, const GLuint *buffers);

typedef void (APIENTRY * PFNGLGENBUFFERSARBPROC) (GLsizei n, GLuint *buffers);

typedef void (APIENTRY * PFNGLBUFFERDATAARBPROC) (GLenum target, int size, const GLvoid *data, GLenum usage);

// Ukazatele na funkce pro VBO

PFNGLGENBUFFERSARBPROC glGenBuffersARB = NULL;// Generování VBO jména

PFNGLBINDBUFFERARBPROC glBindBufferARB = NULL;// Zvolení VBO bufferu

PFNGLBUFFERDATAARBPROC glBufferDataARB = NULL;// Nahrávání dat VBO

PFNGLDELETEBUFFERSARBPROC glDeleteBuffersARB = NULL;// Mazání VBO

Deklarujeme jednoduché třídy vertexu a texturových koordinátů. CMesh je kompletní třídou, která může zapouzdřit základní data meshe. V našem případě se jedná o výškovou mapu. Kód vysvětluje sám sebe, všimněte si akorát, že data vertexů jsou oddělená od texturových koordinátů do vlastního pole. Jak bude vysvětleno dále, není to úplně nutné.

class CVert// Třída vertexu

{

public:

float x;

float y;

float z;

};

typedef CVert CVec;// Definice jsou synonymní

class CTexCoord// Třída texturových koordinátů

{

public:

float u;

float v;

};

class CMesh// Třída meshe (výškové mapy)

{

public:

int m_nVertexCount;// Počet vertexů

CVert* m_pVertices;// Souřadnice vertexů

CTexCoord* m_pTexCoords;// Texturové koordináty

unsigned int m_nTextureId;// ID textury

unsigned int m_nVBOVertices;// Jméno (ID) VBO pro vertexy

unsigned int m_nVBOTexCoords;// Jméno (ID) VBO pro texturové koordináty

AUX_RGBImageRec* m_pTextureImage;// Data výškové mapy

public:

CMesh();// Konstruktor

~CMesh();// Destruktor

bool LoadHeightmap(char* szPath, float flHeightScale, float flResolution);// Loading výškové mapy

float PtHeight(int nX, int nY);// Hodnota na indexu výškové mapy

void BuildVBOs();// Vytvoření VBO

};

Globální proměnná g_bVBOSupported indikuje podporu VBO ze strany grafické karty. Nastavíme ji v inicializačním kódu. G_pMesh bude ukládat data výškové mapy a g_flYRot určuje úhel natočení scény. Proměnná g_nFPS bude obsahovat počet snímků za sekundu a g_nFrames je čítač jednotlivých snímků. Poslední proměnná ukládá čas minulého výpočtu FPS.

bool g_fVBOSupported = false;// Flag podpory VBO

CMesh* g_pMesh = NULL;// Data meshe

float g_flYRot = 0.0f;// Rotace

int g_nFPS = 0, g_nFrames = 0;// FPS a čítač pro FPS

DWORD g_dwLastFPS = 0;// Čas minulého testu FPS

Funkce Loadheightmap() nahrává data výškové mapy. Pro ty z vás, kteří o ničem takovém ještě neslyšeli (Překl.: v originále - kdo žijete pod skálou :-). Výšková mapa je dvou dimenzionální sada dat, většinou obrázek, který hodnotami jednotlivých pixelů specifikuje vertikální výšku dané části terénu. Existuje mnoho různých způsobů, jak ji vytvořit. Moje implementace načítá tří kanálovou RGB bitmapu a ke zjištění výšky používá výpočet luminance. Výsledná hodnota bude díky tomu stejná pro barevný i černobílý obrázek. Osobně doporučuji čtyřkanálový formát vstupních dat, jako je například targa (.TGA) obrázek, u kterého alfa kanál může specifikovat výšku. Nicméně pro účely tohoto tutoriálu bude dostačovat obyčejná bitmapa.

Ujistíme se, že soubor obrázku existuje a pokud ano, loadujeme ho pomocí knihovny glaux. Vím, existují mnohem lepší cesty nahrávání obrázků...

bool CMesh::LoadHeightmap(char* szPath, float flHeightScale, float flResolution)

{

FILE* fTest = fopen(szPath, "r");// Otevření pro čtení

if (!fTest)

{

return false;

}

fclose(fTest);// Uvolní handle

m_pTextureImage = auxDIBImageLoad(szPath);// Nahraje obrázek

Věci začínají být trochu zajímavější. Ze všeho nejdříve bych chtěl poukázat, že pro každý trojúhelník generuji tři vertexy - jednotlivé body nejsou sdílené. Měli byste to vědět už před načítáním.

Abychom mohli alokovat paměť pro data, potřebujeme znát její velikost. Výpočet je celkem jednoduchý ((šířka terénu / rozlišení) * (délka terénu / rozlišení) * 3 vertexy na trojúhelník * 2 trojúhelníky na čtverec). alokujeme paměť pro vertexy i texturové koordináty, deklarujeme pomocné proměnné a ve třech vnořených cyklech nastavíme obě pole.

// Generování pole vertexů

m_nVertexCount = (int)(m_pTextureImage->sizeX * m_pTextureImage->sizeY * 6 / (flResolution * flResolution));

m_pVertices = new CVec[m_nVertexCount];// Alokace paměti

m_pTexCoords = new CTexCoord[m_nVertexCount];

int nX, nZ, nTri, nIndex = 0;// Pomocné

float flX, flZ;

for (nZ = 0; nZ < m_pTextureImage->sizeY; nZ += (int)flResolution)

{

for (nX = 0; nX < m_pTextureImage->sizeX; nX += (int)flResolution)

{

for (nTri = 0; nTri < 6; nTri++)

{

// Výpočet x a z pozice bodu

flX = (float)nX + ((nTri == 1 || nTri == 2 || nTri == 5) ? flResolution : 0.0f);

flZ = (float)nZ + ((nTri == 2 || nTri == 4 || nTri == 5) ? flResolution : 0.0f);

// Nastavení vertexu v poli

m_pVertices[nIndex].x = flX - (m_pTextureImage->sizeX / 2);

m_pVertices[nIndex].y = PtHeight((int)flX, (int)flZ) * flHeightScale;

m_pVertices[nIndex].z = flZ - (m_pTextureImage->sizeY / 2);

// Nastavení texturových koordinátů v poli

m_pTexCoords[nIndex].u = flX / m_pTextureImage->sizeX;

m_pTexCoords[nIndex].v = flZ / m_pTextureImage->sizeY;

nIndex++;// Inkrementace indexu

}

}

}

Z obrázku výškové mapy vytvoříme OpenGL texturu a potom uvolníme jeho paměť.

glGenTextures(1, &m_nTextureId);// OpenGL ID

glBindTexture(GL_TEXTURE_2D, m_nTextureId);// Zvolí texturu

glTexImage2D(GL_TEXTURE_2D, 0, 3, m_pTextureImage->sizeX, m_pTextureImage->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, m_pTextureImage->data);// Nahraje texturu do OpenGL

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);// Lineární filtrování

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

if (m_pTextureImage)// Uvolnění paměti

{

if (m_pTextureImage->data)

{

free(m_pTextureImage->data);

}

free(m_pTextureImage);

}

return true;

}

Funkce PtHeight() vypočítá index do pole s daty, přitom ošetří přístup do nealokované paměti a vrátí výšku na daném indexu. Aby mohl být obrázek barevný i černobílý, použijeme vzorec pro luminanci. Opravdu nic složitého.

float CMesh::PtHeight(int nX, int nY)// Výška na indexu

{

// Výpočet pozice v poli, ošetření přetečení

int nPos = ((nX % m_pTextureImage->sizeX) + ((nY % m_pTextureImage->sizeY) * m_pTextureImage->sizeX)) * 3;

float flR = (float)m_pTextureImage->data[nPos];// Grabování složek barvy

float flG = (float)m_pTextureImage->data[nPos + 1];

float flB = (float)m_pTextureImage->data[nPos + 2];

return (0.299f * flR + 0.587f * flG + 0.114f * flB);// Výpočet luminance

}

V následující funkci začneme konečně pracovat s vertex arrays a VBO. Takže, co to jsou pole vertexů? V základu je to systém, díky kterému můžeme ukázat OpenGL na pole geometrických dat a potom je několika málo příkazy vykreslit. Výsledkem je, že odpadají spousty výskytů funkcí typu glVertex3f() a jiných, které svým mnohonásobným voláním zbytečně zpomalují rendering. Systém vertex buffer object (VBO) jde ještě dále, namísto standardní paměti aplikace alokované v RAM používá vysoce výkonnou paměť grafické karty. Čas renderingu se zkracuje také proto, že data nemusí putovat "po celém počítači", ale jsou uložena přímo na zařízení, kde se používají.

Takže teď se chystáme vytvořit Vertex Buffer Object. Pro tuto operaci existuje několik možných způsobů realizace, jeden z nich se nazývá "mapování" paměti. Myslím, že na tomto místě bude nejlepší jít tou nejsnadnější cestou. Nejprve pomocí glGenBuffersARB() získáme validní jméno VBO. Je to vlastně číslo ID, které OpenGL asociuje s našimi daty. Dále, podobně jako u textur, musíme VBO nastavit jako aktivní, čili říct OpenGL, že s ním chceme pracovat. K tomu slouží funkce glBindBufferARB(). Nakonec nahrajeme data do grafické karty. Funkci se předává velikost dat v bytech a ukazatel na ně. Protože už po této operaci nebudou potřeba, můžeme je smazat z RAM.

void CMesh::BuildVBOs()// Vytvoření VBO

{

// VBO pro vertexy

glGenBuffersARB(1, &m_nVBOVertices);// Získání jména (ID)

glBindBufferARB(GL_ARRAY_BUFFER_ARB, m_nVBOVertices);// Zvolení bufferu

glBufferDataARB(GL_ARRAY_BUFFER_ARB, m_nVertexCount * 3 * sizeof(float), m_pVertices, GL_STATIC_DRAW_ARB);

// VBO pro texturové koordináty

glGenBuffersARB(1, &m_nVBOTexCoords);// Získání jména (ID)

glBindBufferARB(GL_ARRAY_BUFFER_ARB, m_nVBOTexCoords);// Zvolení bufferu

glBufferDataARB(GL_ARRAY_BUFFER_ARB, m_nVertexCount * 2 * sizeof(float), m_pTexCoords, GL_STATIC_DRAW_ARB);

// Data v RAM už jsou zbytečná

delete [] m_pVertices;

delete [] m_pTexCoords;

m_pVertices = NULL;

m_pTexCoords = NULL;

}

Tak to bychom měli, teď je čas na inicializaci. Vytvoříme dynamický objekt výškové mapy a pokusíme se ji vygenerovat ze souboru terrain.bmp. Není-li nadefinovaná symbolická konstanta NO_VBOS, zjistíme, jestli grafická karta podporuje rozšíření GL_ARB_vertex_buffer_object. Pokud ano, pomocí wglGetProcAddress() nagrabujeme ukazatele na potřebné funkce a vytvoříme VBO. Všimněte si, že se ve funkci BuildVBOs() mažou data výškové mapy, která se volá pouze, pokud je VBO podporováno.

BOOL Initialize(GL_Window* window, Keys* keys)// Inicializace

{

g_window = window;

g_keys = keys;

g_pMesh = new CMesh();// Instance výškové mapy

if(!g_pMesh->LoadHeightmap("terrain.bmp", MESH_HEIGHTSCALE, MESH_RESOLUTION))// Nahrání

{

MessageBox(NULL, "Error Loading Heightmap", "Error", MB_OK);

return false;

}

#ifndef NO_VBOS

g_fVBOSupported = IsExtensionSupported("GL_ARB_vertex_buffer_object");// Test podpory VBO

if(g_fVBOSupported)// Je rozšíření podporováno?

{

// Ukazatele na GL funkce

glGenBuffersARB = (PFNGLGENBUFFERSARBPROC) wglGetProcAddress("glGenBuffersARB");

glBindBufferARB = (PFNGLBINDBUFFERARBPROC) wglGetProcAddress("glBindBufferARB");

glBufferDataARB = (PFNGLBUFFERDATAARBPROC) wglGetProcAddress("glBufferDataARB");

glDeleteBuffersARB = (PFNGLDELETEBUFFERSARBPROC) wglGetProcAddress("glDeleteBuffersARB");

g_pMesh->BuildVBOs();// Poslat data vertexů do paměti grafické karty

}

#else

g_fVBOSupported = false;// Bez VBO

#endif

// Klasické nastavení OpenGL

glClearColor(0.0f, 0.0f, 0.0f, 0.5f);

glClearDepth(1.0f);

glDepthFunc(GL_LEQUAL);

glEnable(GL_DEPTH_TEST);

glShadeModel(GL_SMOOTH);

glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

glEnable(GL_TEXTURE_2D);

glColor4f(1.0f, 1.0f, 1.0f, 1.0f);

return TRUE;// Inicializace úspěšná

}

Funkci IsExtensionSupported(), která zjišťuje podporu rozšíření, můžete získat na OpenGL.org, ale moje varianta je o trochu čistší. Někteří lidé sice pomocí strstr() hledají pouze přítomnost podřetězce v řetězci, nicméně zdá se, že OpenGL.org moc nedůvěřuje konzistentnosti řetězce s rozšířeními.

bool IsExtensionSupported(char* szTargetExtension)// Je rozšíření podporováno?

{

const unsigned char *pszExtensions = NULL;

const unsigned char *pszStart;

unsigned char *pszWhere, *pszTerminator;

// Jméno by nemělo mít mezery

pszWhere = (unsigned char *)strchr(szTargetExtension, ' ');

if (pszWhere || *szTargetExtension == '\0')

{

return false;// Nepodporováno

}

pszExtensions = glGetString(GL_EXTENSIONS);// Řetězec s názvy rozšíření

// Vyhledávání podřetězce se jménem rozšíření

pszStart = pszExtensions;

for (;;)

{

pszWhere = (unsigned char *) strstr((const char *) pszStart, szTargetExtension);

if (!pszWhere)

{

break;

}

pszTerminator = pszWhere + strlen(szTargetExtension);

if (pszWhere == pszStart || *(pszWhere - 1) == ' ')

{

if (*pszTerminator == ' ' || *pszTerminator == '\0')

{

return true;// Podporováno

}

}

pszStart = pszTerminator;

}

return false;// Nepodporováno

}

Většina věcí je už hotová, zbývá vykreslování.

void Draw(void)// Vykreslování

{

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glLoadIdentity();

Existuje několik možností, jak získat FPS. Asi nejjednodušší je čítat po dobu jedné sekundy průchody vykreslovací funkcí.

// Získání FPS

if(GetTickCount() - g_dwLastFPS >= 1000)// Uběhla sekunda?

{

g_dwLastFPS = GetTickCount();// Aktualizace času pro další měření

g_nFPS = g_nFrames;// Uložení FPS

g_nFrames = 0;// Reset čítače

char szTitle[256] = {0};// Řetězec titulku okna

sprintf(szTitle, "Lesson 45: NeHe & Paul Frazee's VBO Tut - %d Triangles, %d FPS", g_pMesh->m_nVertexCount / 3, g_nFPS);

if(g_fVBOSupported)// Používá/nepoužívá VBO

{

strcat(szTitle, ", Using VBOs");

}

else

{

strcat(szTitle, ", Not Using VBOs");

}

SetWindowText(g_window->hWnd, szTitle);// Nastaví titulek

}

g_nFrames++;// Inkrementace čítače FPS

Přesuneme kameru nad terén a natočíme scénu okolo osy y. Proměnnou g_flYRot inkrementujeme ve funkci Update().

glTranslatef(0.0f, -220.0f, 0.0f);// Přesun nad terén

glRotatef(10.0f, 1.0f, 0.0f, 0.0f);// Naklonění kamery

glRotatef(g_flYRot, 0.0f, 1.0f, 0.0f);// Rotace kamery

Abychom mohli pracovat s vertex arrays (a také VBO), musíme zapnout GL_VERTEX_ARRAY a GL_TEXTURE_COORD_ARRAY. Protože máme pouze jednu výškovou mapu, nemuseli bychom to dělat po každé, ale bývá to dobrým zvykem.

glEnableClientState(GL_VERTEX_ARRAY);// Zapne vertex arrays

glEnableClientState(GL_TEXTURE_COORD_ARRAY);// Zapne texture coord arrays

Dále musíme specifikovat pole, ve kterých má OpenGL hledat data. Začnu nejprve vertex arrays (část else), protože jsou jednodušší. Vše, co potřebujeme udělat, je zavolání funkce glVertexPointer(), které se předává počet prvků na jeden vertex (2, 3 nebo 4), typ dat, prokládání (v případě, že nejsou vertexy v samostatné struktuře) a ukazatel na pole. To samé platí i pro texturové koordináty, ale mají svoji vlastní funkci. Také bychom mohli uložit všechna data do jednoho velkého paměťového bufferu a použít glInterleavedArrays(), ale necháme je oddělené, abyste viděli, jak použít více VBO najednou.

Jediný rozdíl mezi vertex arrays a VBO je na tomto místě pouze v tom, že u VBO zavoláme glBindBufferARB() a do gl*Pointer() předáme místo ukazatele hodnotu NULL.

if(g_fVBOSupported)// Podporuje grafická karta VBO?

{

glBindBufferARB(GL_ARRAY_BUFFER_ARB, g_pMesh->m_nVBOVertices);

glVertexPointer(3, GL_FLOAT, 0, (char *) NULL);// Předat NULL

glBindBufferARB(GL_ARRAY_BUFFER_ARB, g_pMesh->m_nVBOTexCoords);

glTexCoordPointer(2, GL_FLOAT, 0, (char *) NULL);// Předat NULL

}

else// Obyčejné vertex arrays

{

glVertexPointer(3, GL_FLOAT, 0, g_pMesh->m_pVertices);// Ukazatel na data vertexů

glTexCoordPointer(2, GL_FLOAT, 0, g_pMesh->m_pTexCoords);// Ukazatel na texturové koordináty

}

Samotný rendering je ještě snazší. Pomocí glDrawArrays() řekneme OpenGL, aby vykreslil trojúhelníky ve formátu GL_TRIANGLES. Jako počáteční index v poli předáme nulu, celkový počet vertexů by měl být jasný. Funkce pomocí client state sama detekuje, co všechno má při renderingu použít (textury, světlo...). Existuje mnohem více způsobů, jak poslat data OpenGL. Jako příklad uvedu glArrayElement(), ale naše verze je ze všech nejrychlejší. Všimněte si také, že nespecifikujeme žádné glBegin() a glEnd(). Zde nejsou nutné.

Funkce glDrawArrays() je také důvodem, proč jsem zvolil nesdílet jeden vertex mezi několika trojúhelníky - není to možné. co vím, nejlepší cestou, jak optimalizovat paměťové nároky, je použít triangle strip. V případě světel byste měli zajistit, aby měl k sobě každý vertex odpovídající normálový vektor. Je to sice nutnost, bez které by tato funkce nefungovala, na druhou stranu se však obrovsky zlepší vzhled renderovaného objektu.

glDrawArrays(GL_TRIANGLES, 0, g_pMesh->m_nVertexCount);// Vykreslení vertexů

Zbývá vypnout client state a máme hotovo.

glDisableClientState(GL_VERTEX_ARRAY);// Vypne vertex arrays

glDisableClientState(GL_TEXTURE_COORD_ARRAY);// Vypne texture coord arrays

}

Pokud byste chtěli získat více informací o vertex buffer object, doporučuji prostudovat si dokumentaci ve SGI registru rozšíření (SGI's extensions registry) na http://oss.sgi.com/projects/ogl-sample/registry/. Je to sice trochu těžší čtení než tento tutoriál, ale budete znát mnohem více možností implementace a detailů.

napsal: Paul Frazee <paulfrazee (zavináč) cox.net>
přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>

Zdrojové kódy

Lekce 45

<<< Lekce 44 | Lekce 46 >>>