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.
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>