使用OpenGL VBO扩展 --Nehe教程

使用VBO扩展 <?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

原文出处 http://Nehe.gamedev.net lesson 45

翻译: xheartblue 潘李亮 Stanly Lee 2004-4-18

Homepage: http://gamehunter.3322.net/xpertsoft/

MSN/Email : [email protected]

任何一个3D应用程序的最大的目标之一就是速度,你需要自始至终的限制实际渲染的三角形数目,不管你是采用排序,剔除(Culling),还是层次细节(LOD)。如果其它的方法都无效了,你想简单提高多边形的提交速度的话,通常可以利用OpenGL提供的优化方法,顶点数组是一个比较好的方法。加上最近一个名称为Vertex Buffer Object的扩展,这大大提高了应用的FPSARB_Vertex_Buffer的扩展工作的和顶点数组很像,除了一点,那就是ARB_Vertx_Buffer把数据加载到显卡的高性能的显存里。大大降低了渲染时间。当然,这个扩展是依赖较新的硬件的,不是所有的图形卡都支持的,所以我们必须使用一些技术来进行平衡。

在这个教程里,我们将要:

1:从高度图里加载数据。

2: 使用顶点数组更加有效的把网格数据提交给OpenGL

3: 通过VBOVertex_Buffer_Object)把数据加载到高性能的显存里。

现在我们来定义几个应用程序的参数。

#define MESH_RESOLUTION 4.0f // 每个顶点对应几个像素

#define MESH_HEIGHTSCALE 1.0f // Mesh Height Scale

//#define NO_VBOS // 如果定义了,强制的不使用VBO

开头这两个参数是为高度图定义的。第一个定义了高度图里每一个像素的分辨率。后面一个设定了加载了高度图数据后在垂直方向需要进行的缩放比例。第三个常量,如果你定义了的话,将强行不使用VBO

下一步,我们将定义VBO 扩展的常量,数据类型,和函数指针。

//VBO扩展的定义,从 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 扩展的函数指针

PFNGLGENBUFFERSARBPROC glGenBuffersARB = NULL; // VBO 名字生成函数

PFNGLBINDBUFFERARBPROC glBindBufferARB = NULL; // VBO 绑定函数

PFNGLBUFFERDATAARBPROC glBufferDataARB = NULL; // VBO 数据加载函数

PFNGLDELETEBUFFERSARBPROC glDeleteBuffersARB = NULL; // VBO 删除函数

我只是包含了这个演示程序所需要的东西次。如果你需要其它更多的扩展函数,我建议你到http://www.opengl.org里去下载最新的glext.h文件并且使用它里面的定义(这样会在一定程度上让你的程序更加美观)。我们将深入那几个我们将会使用到的函数。

现在我们来定义基本数学对象,加上我们自己的网格类,所有这些都是这个演示程序的一个非常简单的设计,我建议大家开发一个自己的数学库。

class CVert // 顶点类

{

public:

float x; // X Component

float y; // Y Component

float z; // Z Component

};

typedef CVert CVec; // 定义CVec CVert是一样的。

class CTexCoord // 纹理坐标类

{

public:

float u; // U Component

float v; // V Component

};

class CMesh

{

public:

// 网格数据

int m_nVertexCount; // 顶点数目

CVert* m_pVertices; // 顶点数据

CTexCoord* m_pTexCoords; // 纹理坐标

unsigned int m_nTextureId; // 纹理标ID

// VBO 的名字

unsigned int m_nVBOVertices; // 顶点 VBO 的名字

unsigned int m_nVBOTexCoords; // 纹理坐标 VBO 的名字

// 临时数据

AUX_RGBImageRec* m_pTextureImage; //高度图的数据

public:

CMesh(); // Mesh Constructor

~CMesh(); // Mesh Deconstructor

//高度图加载函数

bool LoadHeightmap( char* szPath, float flHeightScale, float flResolution );

// 得到一个点的高度

float PtHeight( int nX, int nY );

// 绑定VBO对象。

void BuildVBOs();

};

大部分的代码是自注释的。请注意,我把顶点和纹理给分离开存放了。这不是必需的,后面我会给出为什么这么做的解释。

现在我们来定义全局变量。首先是VBO是不是被支持的标志变量,它将在初始化代码里被设置。接下来是我们的网格对象。以及绕Y轴旋转的角度。用来计算FPS的变量。我决定写一个基于FPS的东西来显示这个代码被优化的程度。

bool g_fVBOSupported = false; // ARB_vertex_buffer_object是否被支持?

CMesh* g_pMesh = NULL; //网格数据

float g_flYRot = 0.0f; //旋转

int g_nFPS = 0, g_nFrames = 0; // FPSFPS 计数器

DWORD g_dwLastFPS = 0; //最后依次计算FPS的时间

让我们直接CMesh函数的定义,从LoadHeightMap开始。对那些没有接触过Heightmap的人来,可以这样理解Heightmap,一个Heightmap就是一个二维的数据组,通常是一个图象,它指定了地形网格在垂直上的高度。有许多方法可以实现一个高度图的,但是几乎没有一个完美的。我的实现方法是从一个3个通道(24bit)的位图里读入数据,利用发光度算法来决定数据所定义的高度,这样无论你使用的彩色的图象还是灰度图,结果都是一样的。所以你可以使用彩色图象来定义高度图。个人建议使用四个通道的图象。如TGA等。我们可以使用它的Alpha通道来表示高度。但是,只为了这个教程,我想一个简单的Bitmap还是最合适的。

首先。我们确定一个高度图是不是存在。如果存在。我们使用GLaux的图象加载例程,这也许对写自己的图象加载例程比较有用,但是这已经超出了本教程的范围了。

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

现在,事情变的有趣起来了。首先,我要指出我的高度图将为每一个三角形产生三个顶点。顶点是不和别的三角形共享的,我将在后面解释我为什么这么做,但是你在代码之前需要知道这点。

我开始计算网格里顶点的总数。算法是这样的: ( ( Terrain Width / Resolution ) * ( Terrain Length / Resolution ) * 3 Vertices in a Triangle * 2 Triangles in a Square )。接着分配数据,然后填充数据。

// 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++;

}

}

}

函数的最后将要加载高度图的纹理到OpenGL里来。然后释放我们纹理数据的备份。这和前面的教程很类似的。

// 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这个函数就比较简单了。它计算了需要查询的索引位置的数据的高度,并进行检测,然后计算高度,亮度值的计算也非常简单。你可以看的到,所以我就不在多讲了。

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

}

万岁,现在是讲述顶点数组和VBOs的时候了。什么是顶点数组(Vertex Arrays),它是这样一个系统,你可以告诉OpenGL,你的顶点数据在哪里,然后分成子序列来渲染,只需要很少的函数调用就可以了。这样做的结果就是减少了函数的调用(glVertex,等),增加了程序的速度。什么是VBOs呢?Vertex Buffer Object使用高速的显卡内存,而不是普通的系统RAM内存。它不仅仅降低了每帧的内存操作,而且减少了数据在显卡和CPU之间的传输,在我的实验里,VBO大大提高了帧率,而不是提高了一点点。

现在我们来建立一个Vertex Buffer Objects。实际上,有两种方法可以做,其中一种是被称为"Mapping"的方法。我想最简单的方法也是最好的方法。过程如下:首先用glGenBuffersARB来产生一个可用的VBO名字,实质上,一个名字是一个ID数字,OpenGL用这个ID来关联你的数据。因为一个数字并不一定代表一个有效的VBO名字。然后,我们通过glBindBufferARB函数把VBO对象绑定,让其被激活。最后,我们把数据加载到图形卡里,这可以通过glBufferDataARB来实现,把数据的指针和大小传递进去,glBufferDataARB将把你的数据拷贝到显卡内存中,这意味着我们没有必要再维护这些数据,所以我们可以把它删除(内存中的数据)

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;

}

好了。该是初始化的时候了。我们给Mesh分配内存并加载数据,然后我们测试GL_ARB_vertex_buffer_object是不是支持,如果支持,我们就通过wglGetProcAddress函数得到所有VBO扩展函数的指针,然后建立我们的VBO对象。注意。如果不支持VBO,我们将像通常一样保留数据,同时也请注意前面提到的强制关闭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是一个可以从OpenGL.org上得到的函数,我函数的变化是:用我粗俗的观点来看是更清晰了。

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;

}

这个函数相对来说简单一些了。有些人简单的使用strstr来搜索子字符串,但是显然OpenGL.org并不信任这个函数,我不同意他们的观点。

基本上所有的都做好了。我们剩下来要做的事情就是把数据渲染出来。

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

相当的简单,每一秒过去的时候,我们都把帧计数器的值当作FPS的值保存起来。然后把帧计数器清零。接着,我们在Terrain上移动Camera(如果你改变了高度图,也许你需要调整他),和做一些旋转,g_flYRot在每个Update调用是被递增。

要使用顶点数组(和VBOs),你需要告诉OpenGL你想给它提供什么样的数据,所以第一步是打开客户端的GL_VERTEX_ARRAYGL_TEXTURE_COORD_ARRAY两个状态,然后我将要来设置我们的数据指针,我想,除非你有多个Mesh对象,否则你没有必要在渲染每个帧的时候都做这一步。但是这关系并不大,所以我不认为它是个大问题。

为一个特定的数据类型来设置一个指针,你需要使用对应的函数--glVertexPointerglTexCoordPointer,在我们的例子里非常简单,把顶点需要变量的总数(一个顶点有3个,一个纹理坐标有2个),数据类型(float),每项数据之间期望间隔(Stride)(如果顶点不是存放在连续的数据结构里的时候会有用),以及数据的指针,你也可以使用glInterleavedArrays,并把所有数据保存在一个大的缓冲区里,但是我选择了把数据分开,这样能更好的演示如何使用多个VBOs

说到VBOs。实现起来也没有太大不同,唯一个区别就是提供的数据指针,我们绑定了一个VBO后,只要把数据指针设置成NULL就可以了。请看下面:

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

}

渲染是非常简单容易的。

// Render

glDrawArrays( GL_TRIANGLES, 0, g_pMesh->m_nVertexCount ); // Draw All Of The Triangles At Once

这里我们使用glDrawArrays来把数据送给OpenGL.glDrawArrays检测哪个客户端状态被激活,然后使用它的指针来渲染。我们告诉OpenGL几何图元的类型,从那个索引开始,以及有多少顶点要渲染。有很多其他的方法可以把数据提交来渲染。如glArrayElement,但是这是最快的方法。你会注意到glDrawArrays没有在glBeginglEnd之间。因为没有必要。

glDrawArrays是为什么我选择不在三角形间共享顶点的原因。因为共享是不可能的,我所知道的优化内存使用的最佳方法是使用Triangle Strips,但是它已经不是本教程的范围了。也许你还意识到了要为每一个顶点指定一个向量,这意味你要使用向量,每个顶点都要有一个伴随向量,如果你为每个顶点计算出向量能大大提高渲染结果的视觉真实性。

现在我们要做的就是关闭顶点数组,到此,我们就结束了。

// 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 [email protected]. * DOWNLOAD Visual C++ Code For This Lesson. * DOWNLOAD Borland C++ Builder 6 Code For This Lesson. ( Conversion by Le Thanh Cong ) * DOWNLOAD Code Warrior 5.3 Code For This Lesson. ( Conversion by Scott Lupton ) * DOWNLOAD Dev C++ Code For This Lesson. ( Conversion by Gerald Buchgraber ) * DOWNLOAD Linux/SDL Code For This Lesson. ( Conversion by Ilias Maratos ) * DOWNLOAD Visual Studio .NET Code For This Lesson. ( Conversion by Joachim Rohde )


你可能感兴趣的:(OpenGL)