使用
VBO
扩展
原文出处
http://Nehe.gamedev.net lesson 45。
翻译:
xheartblue
潘李亮
Stanly Lee 2004-4-18
Homepage:
http://gamehunter.3322.net/xpertsoft/
任何一个
3D
应用程序的最大的目标之一就是速度,你需要自始至终的限制实际渲染的三角形数目,不管你是采用排序,剔除
(Culling)
,还是层次细节
(LOD)
。如果其它的方法都无效了,你想简单提高多边形的提交速度的话,通常可以利用
OpenGL
提供的优化方法,顶点数组是一个比较好的方法。加上最近一个名称为
Vertex Buffer Object
的扩展,这大大提高了应用的
FPS
。
ARB_Vertex_Buffer
的扩展工作的和顶点数组很像,除了一点,那就是
ARB_Vertx_Buffer
把数据加载到显卡的高性能的显存里。大大降低了渲染时间。当然,这个扩展是依赖较新的硬件的,不是所有的图形卡都支持的,所以我们必须使用一些技术来进行平衡。
在这个教程里,我们将要:
1
:从高度图里加载数据。
2:
使用顶点数组更加有效的把网格数据提交给
OpenGL
。
3:
通过
VBO
(
Vertex_Buffer_Object)
把数据加载到高性能的显存里。
现在我们来定义几个应用程序的参数。
#define
MESH_RESOLUTION 4.0f
//
每个顶点对应几个像素
#define
MESH_HEIGHTSCALE 1.0f
// Mesh Height Scal
e
//#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* s
zPath,
float f
lHeightScale,
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;
// FPS
和
FPS
计数器
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 C
Mesh :: 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_ARRAY
和
GL_TEXTURE_COORD_ARRAY
两个状态,然后我将要来设置我们的数据指针,我想,除非你有多个
Mesh
对象,否则你没有必要在渲染每个帧的时候都做这一步。但是这关系并不大,所以我不认为它是个大问题。
为一个特定的数据类型来设置一个指针,你需要使用对应的函数
--glVertexPointer
和
glTexCoordPointer
,在我们的例子里非常简单,把顶点需要变量的总数(一个顶点有
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
没有在
glBegin
和
glEnd
之间。因为没有必要。
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 )