Lektion 45: Vertex Arrays und Vertex Buffer Objekte
Willkommen zu diesem 45. Nehe-Tut, diesmal wird gezeigt werden, wie man mit
Vertex Arrays und Vertex Buffer Objekten (VBOs) die Performance wesentlich verbessern
kann. Auch wenn die Anzahl der dargestellten Dreiecke durch Sortierung, Culling
und level-of-detail Algorithmen so gering wie möglich gehalten werden kann,
braucht man in manchen Fällen eben doch sehr viele Polygone. Vertex Arrays
und die OpenGL-Erweiterung ARB_vertex_buffer_object ermöglichen das, wobei
die Dreiecksdaten in den sehr schnellen Speicher der Grafikkarte geladen werden
und dadurch den Renderingvorgang beschleunigen. Da diese Erweiterung relativ
neu ist, wird sie auf alten Grafikkarten nicht funktionieren, es muss also geprüft
werden ob diese unterstützt wird. (Ab einer Geforce4 sollte es klappen...)
Dargestellt wird eine Heightmap mit ca. 32000 Dreiecken, Heightmaps an sich
wurden bereits im 34. Tutorial
besprochen.
In diesem Tutorial werden:
- Daten aus einer Heightmap geladen,
- Vertex Arrays genutzt um Daten schneller an OpenGL zu übermitteln
- die dann mithilfe der VBO Erweiterung im Speicher der Grafikkarte landen.
Zuerst wie immer einige feste Parameter:
#define MESH_RESOLUTION 4.0f // Pixels Per Vertex
#define MESH_HEIGHTSCALE 1.0f // Mesh Height Scale
// #define NO_VBOS // If Defined, VBOs Will Be Forced Off
Die ersten beiden Konstanten sind typisch für Heightmaps, wobei die erste
die Auflösung der zu zeichnen Heightmap einstellt (hier wird nur jeder
vierte Pixel der Heightmap-Grafik tatsächlich einem Dreieck zugeordnet),
die zweite skaliert die ganze Sache auf der Y-Achse. Sollte der dritte Parameter
nicht auskommentiert sein, werden keine VBOs genutzt.
Jetzt folgen einige Konstanten, Datentypen und Funktionszeiger für die
VBO Erweiterung:
// VBO Extension Definitions, From 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);
// VBO Extension Function Pointers
PFNGLGENBUFFERSARBPROC glGenBuffersARB = NULL; // VBO Name Generation Procedure
PFNGLBINDBUFFERARBPROC glBindBufferARB = NULL; // VBO Bind Procedure
PFNGLBUFFERDATAARBPROC glBufferDataARB = NULL; // VBO Data Loading Procedure
PFNGLDELETEBUFFERSARBPROC glDeleteBuffersARB = NULL;// VBO Deletion Procedure
Es wurde nur soviel definiert, wie für diese Demo notwendig ist. Sollen
weitere OpenGL Extensions genutzt werden, lohnt es sich von opengl.org die neueste
glext.h runterzuladen und den dortigen Code zu nehmen. Die Funktionen werden
im einzelnen bei ihrer Verwendung beschrieben.
Unten folgen einige typische Klassen aus der Geometrie und die Struktur für
die Heightmap. Eine eigene Klasse mit immer wieder genutzten Definitionen, Funktionen
und Strukturen lohnt sich generell anzulegen, man muss ja nicht jedesmal alles
neu erfinden...
class CVert// Vertex Class
{
public:
float x;// X Component
float y;// Y Component
float z;// Z Component
};
typedef CVert CVec; // The Definitions Are Synonymous
class CTexCoord // Texture Coordinate Class
{
public:
float u; // U Component
float v; // V Component
};
class CMesh
{
public:
// Mesh Data
intm_nVertexCount; // Vertex Count
CVert*m_pVertices; // Vertex Data
CTexCoord*m_pTexCoords; // Texture Coordinates
unsigned intm_nTextureId; // Texture ID
// Vertex Buffer Object Names
unsigned intm_nVBOVertices; // Vertex VBO Name
unsigned intm_nVBOTexCoords;// Texture Coordinate VBO Name
// Temporary Data
AUX_RGBImageRec* m_pTextureImage;// Heightmap Data
public:
CMesh(); // Mesh Constructor
~CMesh();// Mesh Deconstructor
// Heightmap Loader
bool LoadHeightmap( char* szPath, float flHeightScale, float flResolution );
// Single Point Height
float PtHeight( int nX, int nY );
// VBO Build Function
void BuildVBOs();
};
Der größte Teil der Klassen erklärt sich von selbst. Punkt
und Texturkoordinaten wurden in getrennt definiert, was nicht zwingend nötig
ist, aber später noch Sinn machen wird.
Jetzt zu den globalen Variablen. Zuerst wird ein Flag benötigt um zu speichern,
ob VBOs unterstützt werden. Es folgt ein Pointer auf die Heightmap, g_flYRot
wird zur Rotation um die Y-Achse genutzt. Die letzten Variablen werden für
die FPS-Berechnung genutzt, die sich recht gut macht um den Geschwindigkeitszuwachs
zu demonstrieren.
bool g_fVBOSupported = false; // ARB_vertex_buffer_object supported?
CMesh *g_pMesh = NULL; // Mesh Data
float g_flYRot = 0.0f; // Rotation
int g_nFPS = 0,g_nFrames = 0; // FPS and FPS Counter
DWORD g_dwLastFPS = 0; // Last FPS Check Time
Weiter zu den CMesh Funktionen. LoadHeightmap ist die erste. Eine Heightmap
ist eine zweidimensionale Datenstruktur, üblicherweise eine Grafik, die
Höhenwerte einer Oberfläche (oder Landschaft) als Graustufen speichert.
Ausgegeben wird das ganze dann natürlich als ein großes Feld von
Dreiecken. In dieser Implementierung wird ein einfaches Bitmap (drei Kanäle,
RGB, *.bmp) als Datenquelle genutzt, wobei ein simpler Beleuchtungsalgorithmus
(hat mit dem Thema der Lektion nicht viel zu tun, keine Angst ;) genutzt wird
um die Höhenwerte zu ermitteln. Das Ergebnis ist damit das gleiche, nur
das die Ausgangsgrafik auch farbig sein kann. Oft werden Grafikformate mit 4
Kanälen genutzt, z.B. *.tga-Grafiken um den Alpha-Kanal für die Höhenwerte
zu verwenden und den Rest der Grafik als Farbwerte für die zu zeichnenden
Dreiecke.
Zuerst wird überprüft, ob die in szPath übergebene Grafik überhaupt
existiert, ist dies der Fall wird sie mit Hilfe von GLaux geladen.
bool CMesh :: LoadHeightmap( char* szPath, float flHeightScale, float flResolution )
{
// Error-Checking
FILE* fTest = fopen( szPath, "r" ); // Open The Image
if( !fTest ) // Make Sure It Was Found
return false; // If Not, The File Is Missing
fclose( fTest ); // Done With The Handle
// Load Texture Data
m_pTextureImage = auxDIBImageLoad( szPath ); // Utilize GLaux's Bitmap Load Routine
Jetzt zu interessanterem Code. Vorher muss angemerkt werden, dass für
jedes Dreieck der Heightmap drei Punkte einzeln gespeicht werden, Punkte werden
also nicht von mehreren Dreiecken geteilt.
Die Anzahl der benötigten Punkte wird berechnet ( (Terrainbreite / Auflösung)
* (Terrainhöhe / Auflösung) * 3 Punkte pro Dreieck * 2 Dreiecke pro
Quadrat im Terrain ), danach der für diese Punkte benötigte Speicher
reserviert.
// Generate Vertex Field
m_nVertexCount = (int) ( m_pTextureImage->sizeX
* m_pTextureImage->sizeY
* 6 / ( flResolution * flResolution ) );
m_pVertices = new CVec[m_nVertexCount]; // Allocate Vertex Data
m_pTexCoords = new CTexCoord[m_nVertexCount]; // Allocate Tex Coord Data
int nX, nZ, nTri, nIndex=0; // Create Variables
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++ )
{
// Using This Quick Hack, Figure The X,Z Position Of The Point
flX = (float) nX + ( ( nTri == 1 || nTri == 2 || nTri == 5 ) ? flResolution : 0.0f );
flZ = (float) nZ + ( ( nTri == 2 || nTri == 4 || nTri == 5 ) ? flResolution : 0.0f );
// Set The Data, Using PtHeight To Obtain The Y Value
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 );
// Stretch The Texture Across The Entire Mesh
m_pTexCoords[nIndex].u = flX / m_pTextureImage->sizeX;
m_pTexCoords[nIndex].v = flZ / m_pTextureImage->sizeY;
// Increment Our Index
nIndex++;
}
}
}
Zuletzt wird die Heightmap Textur an OpenGL übergeben und der genutzte
Speicher wieder freigegeben. Das sollte aus den vorherigen Lektionen bekannt
sein.
// Load The Texture Into OpenGL
glGenTextures( 1, &m_nTextureId ); // Get An Open ID
glBindTexture( GL_TEXTURE_2D, m_nTextureId ); // Bind The Texture
glTexImage2D( GL_TEXTURE_2D, 0, 3, m_pTextureImage->sizeX, m_pTextureImage->sizeY,
0, GL_RGB, GL_UNSIGNED_BYTE, m_pTextureImage->data );
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
// Free The Texture Data
if( m_pTextureImage )
{
if( m_pTextureImage->data )
free( m_pTextureImage->data );
free( m_pTextureImage );
}
return true;
}
PtHeight ist relativ simpel. Für jede übergebene Position wird die
Höhe berechnet, die der Einfachheit halber genutzte Beleuchtungsformel
ist nicht allzu schwer:
float CMesh :: PtHeight( int nX, int nY )
{
// Calculate The Position In The Texture, Careful Not To Overflow
int nPos = ( ( nX % m_pTextureImage->sizeX ) +
( ( nY % m_pTextureImage->sizeY )
* m_pTextureImage->sizeX ) ) * 3;
float flR = (float) m_pTextureImage->data[ nPos ]; // Get The Red Component
float flG = (float) m_pTextureImage->data[ nPos + 1 ]; // Get The Green Component
float flB = (float) m_pTextureImage->data[ nPos + 2 ]; // Get The Blue Component
// Calculate The Height Using The Luminance Algorithm
return ( 0.299f * flR + 0.587f * flG + 0.114f * flB );
}
Nun zum Kern des Ganzen, den Vertex Arrays und VBOs. Vertex Arrays sind eine
Möglichkeit, OpenGL die Geometriedaten eines zu rendernden Objektes direkt
zu übergeben und im Stück anzeigen zu lassen. Dabei werden wesentlich
weniger Funktionsaufrufe genutzt, man spart z.B. zahlreiche glVertex(...), was
die Performance deutlich verbessern kann. VBOs speichern Dreiecksdaten direkt
in den RAM der Grafikkarte, es muss also nicht der Umweg über den Arbeitsspeicher
und die CPU genommen werden, gebraucht werden diese Daten ja nur im Grafikprozessor.
Bei einigen Versuchen konnten VBOs die Framerate verdreifachen, es lohnt sich
also allemal.
Die nächste Funktion erstellt VBOs. Dabei gibt es verschiedene Möglichkeiten
die Daten in den Grafikspeicher zu verschieben, am einfachsten dürfte das
sogenannte "mappen" sein. Dabei wird wie folgt vorgegangen: Zuerst
wird glGenBuffersARB aufgerufen um eine gültige ID für das VBO zu
generieren. Diese Nummer wird OpenGL mit den geladenen Daten assoziieren. Man
könnte auch manuell eine ID vergeben, allerdings müßte dann
geprüft werden, ob diese nicht schon genutzt wird. Das zu erstellende VBO
wird mit glBindBufferARB aktiviert. Zuletzt wird das VBO mit Hilfe eines Aufrufes
von glBufferDataARB in den Grafikspeicher kopiert, dessen Größe und
ein Pointer auf dieses VBO werden übergeben. Dies geschieht für die
Textur- und Punktkoordinaten getrennt. Da die Objektdaten jetzt doppelt vorliegen,
kann die Kopie aus dem Arbeitsspeicher gelöscht werden.
void CMesh :: BuildVBOs()
{
// Generate And Bind The Vertex Buffer
glGenBuffersARB( 1, &m_nVBOVertices ); // Get A Valid Name
glBindBufferARB( GL_ARRAY_BUFFER_ARB, m_nVBOVertices ); // Bind The Buffer
// Load The Data
glBufferDataARB( GL_ARRAY_BUFFER_ARB, m_nVertexCount*3*sizeof(float),
m_pVertices,GL_STATIC_DRAW_ARB);
// Generate And Bind The Texture Coordinate Buffer
glGenBuffersARB( 1, &m_nVBOTexCoords ); // Get A Valid Name
glBindBufferARB( GL_ARRAY_BUFFER_ARB, m_nVBOTexCoords );// Bind The Buffer
// Load The Data
glBufferDataARB( GL_ARRAY_BUFFER_ARB, m_nVertexCount*2*sizeof(float),
m_pTexCoords,GL_STATIC_DRAW_ARB );
// Our Copy Of The Data Is No Longer Necessary, It Is Safe In The Graphics Card
delete [] m_pVertices; m_pVertices = NULL;
delete [] m_pTexCoords; m_pTexCoords = NULL;
}
Ok, Zeit für die Initialisierungsfunktion. Die Heightmap wird alloziiert
und geladen. Es wird geprüft ob VBO unterstützt werden, das Ergebnis
landet in g_fVBOSupported. Werden diese durch die Grafikkarte unterstützt,
können die benötigten Funktionspointer mit wglGetProcAddress geholt
werden.
// Any GL Init Code & User Initialiazation Goes Here
BOOL Initialize (GL_Window* window, Keys* keys)
{
g_window = window;
g_keys = keys;
// Load The Mesh Data
g_pMesh = new CMesh(); // Instantiate Our Mesh
if( !g_pMesh->LoadHeightmap( "terrain.bmp", // Load Our Heightmap
MESH_HEIGHTSCALE,
MESH_RESOLUTION ) )
{
MessageBox( NULL, "Error Loading Heightmap", "Error", MB_OK );
return false;
}
// Check For VBOs Supported
#ifndef NO_VBOS
g_fVBOSupported = IsExtensionSupported( "GL_ARB_vertex_buffer_object" );
if( g_fVBOSupported )
{
// Get Pointers To The GL Functions
glGenBuffersARB = (PFNGLGENBUFFERSARBPROC) wglGetProcAddress("glGenBuffersARB");
glBindBufferARB = (PFNGLBINDBUFFERARBPROC) wglGetProcAddress("glBindBufferARB");
glBufferDataARB = (PFNGLBUFFERDATAARBPROC) wglGetProcAddress("glBufferDataARB");
glDeleteBuffersARB = (PFNGLDELETEBUFFERSARBPROC) wglGetProcAddress("glDeleteBuffersARB");
// Load Vertex Data Into The Graphics Card Memory
g_pMesh->BuildVBOs(); // Build The VBOs
}
#else /* NO_VBOS */
g_fVBOSupported = false;
#endif
//~TUTORIAL
// Setup GL States
glClearColor (0.0f, 0.0f, 0.0f, 0.5f); // Black Background
glClearDepth (1.0f); // Depth Buffer Setup
glDepthFunc (GL_LEQUAL); // The Type Of Depth Testing (Less Or Equal)
glEnable (GL_DEPTH_TEST); // Enable Depth Testing
glShadeModel (GL_SMOOTH); // Select Smooth Shading
// Set Perspective Calculations To Most Accurate
glHint (GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
glEnable( GL_TEXTURE_2D ); // Enable Textures
glColor4f( 1.0f, 1.0f, 1.0f, 1.0f ); // Set The Color To White
return TRUE; // Return TRUE (Initialization Successful)
}
IsExtensionSupported gibt es auch auf OpenGL.org, diese Implementierung dürfte
IMHO ein wenig sauberer sein. Geprüft wird, ob die Grafikkarte eine bestimmt
OpenGL Erweiterung unterstützt.
bool IsExtensionSupported( char* szTargetExtension )
{
const unsigned char *pszExtensions = NULL;
const unsigned char *pszStart;
unsigned char *pszWhere, *pszTerminator;
// Extension names should not have spaces
pszWhere = (unsigned char *) strchr( szTargetExtension, ' ' );
if( pszWhere || *szTargetExtension == '\0' )
return false;
// Get Extensions String
pszExtensions = glGetString( GL_EXTENSIONS );
// Search The Extensions String For An Exact Copy
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;
pszStart = pszTerminator;
}
return false;
}
Fast fertig, jetzt fehlt nur noch die Ausgabefunktion.
void Draw (void)
{
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear Screen And Depth Buffer
glLoadIdentity (); // Reset The Modelview Matrix
// Get FPS
if( GetTickCount() - g_dwLastFPS >= 1000 ) // When A Second Has Passed...
{
g_dwLastFPS = GetTickCount();// Update Our Time Variable
g_nFPS = g_nFrames;// Save The FPS
g_nFrames = 0;// Reset The FPS Counter
char szTitle[256]={0}; // Build The Title String
sprintf( szTitle, "Lesson 45: NeHe & Paul Frazee's VBO Tut - %d Triangles, %d FPS",
g_pMesh->m_nVertexCount / 3, g_nFPS );
if( g_fVBOSupported ) // Include A Notice About VBOs
strcat( szTitle, ", Using VBOs" );
else
strcat( szTitle, ", Not Using VBOs" );
SetWindowText( g_window->hWnd, szTitle ); // Set The Title
}
g_nFrames++; // Increment Our FPS Counter
// Move The Camera
glTranslatef( 0.0f, -220.0f, 0.0f ); // Move Above The Terrain
glRotatef( 10.0f, 1.0f, 0.0f, 0.0f ); // Look Down Slightly
glRotatef( g_flYRot, 0.0f, 1.0f, 0.0f ); // Rotate The Camera
Jede Sekunde wird die FPS Rate gemessen und der Framezähler zurückgesetzt.
Die Kamera wird in die Mitte des Terrains verschoben und rotiert. g_flYRot wird
in der Update Funktion jeweils um 1 erhöht.
Um Vertex Arrays und VBOs zu nutzen, müssen deren Pointer an OpenGL übergeben
werden. Die states GL_VERTEX_ARRAY und GL_TEXTURE_COORD_ARRAY werden aktiviert.
Jetzt müssen nur noch die einzelnen Pointer gesetzt werden.
Um einen Pointer auf einen bestimmten Datentyp zu setzen, müssen entsprechende
Funktionen, in diesem Fall glVertexPointer und glTexCoordPointer aufgerufen
werden. Die Anzahl der Variablen (3 für Punkt-, 2 für Texturkoordinaten),
der Datentyp (foat), der Abstand der Daten (falls sich in der Struktur außer
den Dreiecksdaten noch mehr befindet, in diesem Falle nicht) und der Pointer
auf die gewünschten Daten müssen übergeben werden. Man könnte
auch glInterleavedArrays nutzen um alles in einem großen Puffer zu speichern,
allerdings wäre es dann schwierig mehrere VBOs auszugeben.
// Set Pointers To Our Data
if( g_fVBOSupported )
{
glBindBufferARB( GL_ARRAY_BUFFER_ARB, g_pMesh->m_nVBOVertices );
// Set The Vertex Pointer To The Vertex Buffer
glVertexPointer( 3, GL_FLOAT, 0, (char *) NULL );
glBindBufferARB( GL_ARRAY_BUFFER_ARB, g_pMesh->m_nVBOTexCoords );
// Set The TexCoord Pointer To The TexCoord Buffer
glTexCoordPointer( 2, GL_FLOAT, 0, (char *) NULL );
} else
{
// Set The Vertex Pointer To Our Vertex Data
glVertexPointer( 3, GL_FLOAT, 0, g_pMesh->m_pVertices );
// Set The Vertex Pointer To Our TexCoord Data
glTexCoordPointer( 2, GL_FLOAT, 0, g_pMesh->m_pTexCoords );
}
Das eigentliche Rendering passt jetzt in einen einzigen Funktionsaufruf:
// Render
// Draw All Of The Triangles At Once
glDrawArrays( GL_TRIANGLES, 0, g_pMesh->m_nVertexCount );
glDrawArrays übergibt die Geometriedaten an OpenGL. Dabei wird überprüft
welche States gerade aktiv sind und deren Pointer werden zur Ausgabe genutzt.
Übergeben wird die gewünschte Grundform (GL_TRIANGLES), ein Index
wo die Daten anfangen und die Anzahl der Punkte die genutzt werden. Die Aufrufe
glBegin und glEnd sind überflüssig.
Wie oben angedeutet, können sich die einzelnen Dreiecke keine Punkte teilen,
da glDrawArrays dies nicht unterstützt. Sollten zusätzlich vorberechnete
Normalen übergeben werden, braucht jeder Punkt eine eigene Normale, auch
wenn diese sich oft wiederholen. Normalen können die Bildgenauigkeit etwas
steigern.
Die States müssen zum Schluß wieder zurückgesetzt werden, um
mögliche andere (einfache) Objekte auszugeben.
// Disable Pointers
glDisableClientState( GL_VERTEX_ARRAY ); // Disable Vertex Arrays
glDisableClientState( GL_TEXTURE_COORD_ARRAY ); // Disable Texture Coord Arrays
}
Für mehr Informationen zu Vertex Buffer Objekten empfiehlt sich die umfangreichere
Dokumentation zu den Extensions auf SGI.
Das wars für dieses Tutorial, ich hoffe es hat Spaß gemacht, bis
zum nächsten Mal. Der Autor ist unter paulfrazee@cox.net
zu erreichen.
Paul Frazee,
Jeff Molofee (NeHe) http://nehe.gamedev.net
Die Source Codes und Ausführbaren Dateien zu den Kursen liegen auf der Neon
Helium Website
codeworx.
Übersetzt und modifiziert von Hans-Jakob Schwer 22.07.2k4, www.codeworx.org