Vertex Buffer ObjectsOne of the largest goals of any 3d application is speed. You should always limit the amount of polygons actually rendered, whether by sorting, culling, or level-of-detail algorithms. However, when all else fails and you simply need raw polygon-pushing power, you can always utilize the optimizations provided by OpenGL. Vertex Arrays are one good way to do that, plus a recent extension to graphics cards named Vertex Buffer Objects adds the FPS boost everybody dreams of. The extension, ARB_vertex_buffer_object, works just like vertex arrays, except that it loads the data into the graphics card's high-performance memory, significantly lowering rendering time. Of course, the extension being relatively new, not all cards will support it, so we will have to write in some technology scaling. In this tutorial, we will
So let's get started! First we are going to define a few application parameters. #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 The first two constants are standard heightmap fare - the former sets the resolution at which the heightmap will be generated per pixel, and the latter sets the vertical scaling of the data retrieved from the heightmap. The third constant, when defined, will force VBOs off - a provision I added so that those with bleeding-edge cards can easily see the difference. Next we have the VBO extension constant, data type, and function pointer definitions. // 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 I have only included what will be necessary for the demo. If you need any more of the functionality, I recommend downloading the latest glext.h from /data/lessons/http://www.opengl.org and using the definitions there (it will be much cleaner for your code, anyway). We will get into the specifics of those functions as we use them. Now we find the standard mathematical definitions, plus our mesh class. All of them are very bare-bones, designed specifically for the demo. As always, I recommend developing your own math library. 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 int m_nVertexCount; // Vertex Count CVert* m_pVertices; // Vertex Data CTexCoord* m_pTexCoords; // Texture Coordinates unsigned int m_nTextureId; // Texture ID // Vertex Buffer Object Names unsigned int m_nVBOVertices; // Vertex VBO Name unsigned int m_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(); }; Most of that code is relatively self-explanatory. Note that while I do keep the Vertex and Texture Coordinate data seperate, that is not wholly necessary, as will be indicated later. Here we have our global variables. First we have a VBO extension validity flag, which will be set in the initialization code. Then we have our mesh, followed by our Y rotation counter. Leading up the rear are the FPS monitoring variables. I decided to write in a FPS gauge to help display the optimization provided by this code. 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 Let's skip over to the CMesh function definitions, starting with LoadHeightmap. For those of you who live under a rock, a heightmap is a two-dimensional dataset, commonly an image, which specifies the terrain mesh's vertical data. There are many ways to implement a heightmap, and certainly no one right way. My implementation reads a three channel bitmap and uses the luminosity algorithm to determine the height from the data. The resulting data would be exactly the same if the image was in color or in grayscale, which allows the heightmap to be in color. Personally, I recommend using a four channel image, such as a targa, and using the alpha channel for the heights. However, for the purpose of this tutorial, I decided that a simple bitmap would be best. First, we make sure that the heightmap exists, and if so, we load it using GLaux's bitmap loader. Yes yes, it probably is better to write your own image loading routines, but that is not in the scope of this tutorial. 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 Now things start getting a little more interesting. First of all, I would like to point out that my heightmap generates three vertices for every triangle - vertices are not shared. I will explain why I chose to do that later, but I figured you should know before looking at this code. I start by calculating the amount of vertices in the mesh. The algorithm is essentially ( ( Terrain Width / Resolution ) * ( Terrain Length / Resolution ) * 3 Vertices in a Triangle * 2 Triangles in a Square ). Then I allocate my data, and start working my way through the vertex field, setting data. // 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++; } } } I finish off the function by loading the heightmap texture into OpenGL, and freeing our copy of the data. This should be fairly familiar from past tutorials. // 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 is relatively simple. It calculates the index of the data in question, wrapping any overflows to avoid error, and calculates the height. The luminance formula is very simple, as you can see, so don't sweat it too much. 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 return ( 0.299f * flR + 0.587f * flG + 0.114f * flB ); // Calculate The Height Using The Luminance Algorithm } Hurray, time to get dirty with Vertex Arrays and VBOs. So what are Vertex Arrays? Essentially, it is a system by which you can point OpenGL to your geometric data, and then subsequently render data in relatively few calls. The resulting cut down on function calls (glVertex, etc) adds a significant boost in speed. What are VBOs? Well, Vertex Buffer Objects use high-performance graphics card memory instead of your standard, ram-allocated memory. Not only does that lower the memory operations every frame, but it shortens the bus distance for your data to travel. On my specs, VBOs actually triple my framerate, which is something not to be taken lightly. So now we are going to build the Vertex Buffer Objects. There are really a couple of ways to go about this, one of which is called "mapping" the memory. I think the simplist way is best here. The process is as follows: first, use glGenBuffersARB to get a valid VBO "name". Essentially, a name is an ID number which OpenGL will associate with your data. We want to generate a name because the same ones won't always be available. Next, we make that VBO the active one by binding it with glBindBufferARB. Finally, we load the data into our gfx card's data with a call to glBufferDataARB, passing the size and the pointer to the data. glBufferDataARB will copy that data into your gfx card memory, which means that we will not have any reason to maintain it anymore, so we can delete it. 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, time to initialize. First we will allocate and load our mesh data. Then we will check for GL_ARB_vertex_buffer_object support. If we have it, we will grab the function pointers with wglGetProcAddress, and build our VBOs. Note that if VBOs aren't supported, we will retain the data as usual. Also note the provision for forced no VBOs. // 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 IsExtensionSupported is a function you can get from OpenGL.org. My variation is, in my humble opinion, a little cleaner. 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; } It is relatively simple. Some people simply use a sub-string search with strstr, but apparently OpenGL.org doesn't trust the consistancy of the extension string enough to accept that as proof. And hey, I am not about to argue with those guys. Almost finished now! All we gotta do is render the data. 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 Pretty simple - every second, save the frame counter as the FPS and reset the frame counter. I decided to throw in poly count for impact. Then we move the camera above the terrain (you may need to adjust that if you change the heightmap), and do a few rotations. g_flYRot is incremented in the Update function. To use Vertex Arrays (and VBOs), you need to tell OpenGL what data you are going to be specifying with your memory. So the first step is to enable the client states GL_VERTEX_ARRAY and GL_TEXTURE_COORD_ARRAY. Then we are going to want to set our pointers. I doubt you have to do this every frame unless you have multiple meshes, but it doesn't hurt us cycle-wise, so I don't see a problem. To set a pointer for a certain data type, you have to use the appropriate function - glVertexPointer and glTexCoordPointer, in our case. The usage is pretty easy - pass the amount of variables in a point (three for a vertex, two for a texcoord), the data cast (float), the stride between the desired data (in the event that the vertices are not stored alone in their structure), and the pointer to the data. You can actually use glInterleavedArrays and store all of your data in one big memory buffer, but I chose to keep it seperate to show you how to use multiple VBOs. Speaking of VBOs, implementing them isn't much different. The only real change is that instead of providing a pointer to the data, we bind the VBO we want and set the pointer to zero. Take a look. // Set Pointers To Our Data if( g_fVBOSupported ) { glBindBufferARB( GL_ARRAY_BUFFER_ARB, g_pMesh->m_nVBOVertices ); glVertexPointer( 3, GL_FLOAT, 0, (char *) NULL ); // Set The Vertex Pointer To The Vertex Buffer glBindBufferARB( GL_ARRAY_BUFFER_ARB, g_pMesh->m_nVBOTexCoords ); glTexCoordPointer( 2, GL_FLOAT, 0, (char *) NULL ); // Set The TexCoord Pointer To The TexCoord Buffer } else { glVertexPointer( 3, GL_FLOAT, 0, g_pMesh->m_pVertices ); // Set The Vertex Pointer To Our Vertex Data glTexCoordPointer( 2, GL_FLOAT, 0, g_pMesh->m_pTexCoords ); // Set The Vertex Pointer To Our TexCoord Data } Guess what? Rendering is even easier. // Render glDrawArrays( GL_TRIANGLES, 0, g_pMesh->m_nVertexCount ); // Draw All Of The Triangles At Once Here we use glDrawArrays to send our data to OpenGL. glDrawArrays checks which client states are enabled, and then uses their pointers to render. We tell it the geometric type, the index we want to start from, and how many vertices to render. There are many other ways we can send the data for rendering, such as glArrayElement, but this is the fastest way to do it. You will notice that glDrawArrays is not within glBegin / glEnd statements. That isn't necessary here. glDrawArrays is why I chose not to share my vertex data between triangles - it isn't possible. As far as I know, the best way to optimize memory usage is to use triangle strips, which is, again, out of this tutorial's scope. Also you should be aware that normals operate "one for one" with vertices, meaning that if you are using normals, each vertex should have an accompanying normal. Consider that an opportunity to calculate your normals per-vertex, which will greatly increase visual accuracy. Now all we have left is to disable vertex arrays, and we are finished. // Disable Pointers glDisableClientState( GL_VERTEX_ARRAY ); // Disable Vertex Arrays glDisableClientState( GL_TEXTURE_COORD_ARRAY ); // Disable Texture Coord Arrays } If you want more information on Vertex Buffer Objects, I recommend reading the documentation in SGI's extension registry - http://oss.sgi.com/projects/ogl-sample/registry. It is a little more tedious to read through than a tutorial, but it will give you much more detailed information. Well that does it for the tutorial. If you find any mistakes or misinformation, or simply have questions, you can contact me at paulfrazee@cox.net. * DOWNLOAD Visual C++ Code For This Lesson. * DOWNLOAD Borland C++ Builder 6 Code For This Lesson. ( Conversion by Le Thanh Cong ) |