NeHe OpenGL教程第四十五课,DancingWind翻译

Nehe SDK

NeHe Productions
第45课

顶点缓存

你想更快地绘制么?直接操作显卡吧,这可是当前的图形技术,不要犹豫,我带你入门。接下来,你自己向前走吧。

速度是3D程序中最重要的指标,你必须限制绘制的多边形的个数,或者提高显卡绘制多边形的效率。显卡最近增加了一个新的扩展,叫做顶点缓存VS,它直接把顶点放置在显卡中的高速缓存中,极大的增加了绘制速度。
在这个教程里,我们会加载一个高度图,使用顶点数组高效的把网格数据发送到OpenGL里,并使用VBO扩展把顶点数据放入高效的显存里。
现在让我们开始吧,我们先来定义一些程序参数。

 

#define MESH_RESOLUTION 4.0f							// 每个顶点使用的像素
#define MESH_HEIGHTSCALE 1.0f // 高度的缩放比例
//#define NO_VBOS // 如果定义将不使用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; // 创建缓存名称
PFNGLBINDBUFFERARBPROC glBindBufferARB = NULL; // 绑定缓存
PFNGLBUFFERDATAARBPROC glBufferDataARB = NULL; // 绑定缓存数据
PFNGLDELETEBUFFERSARBPROC glDeleteBuffersARB = NULL; // 删除缓存

现在我们来定义自己的网格类:

class CVert														// 顶点类
{
public:
	float x;													
	float y;													
	float z;													
};
typedef CVert CVec;												

class CTexCoord													// 纹理坐标类
{
public:
	float u;													
	float v;													
};

//网格类
class CMesh
{
public:
	// 网格数据
	int				m_nVertexCount;								// 顶点个数
	CVert*			m_pVertices;								// 顶点数据的指针
	CTexCoord*		m_pTexCoords;								// 顶点的纹理坐标
	unsigned int	m_nTextureId;								// 纹理的ID

	unsigned int	m_nVBOVertices;								// 顶点缓存对象的名称
	unsigned int	m_nVBOTexCoords;							// 顶点纹理缓存对象的名称

	AUX_RGBImageRec* m_pTextureImage;							// 高度数据

public:
	CMesh();													// 构造函数
	~CMesh();													// 析构函数

	// 载入高度图
	bool LoadHeightmap( char* szPath, float flHeightScale, float flResolution );
	// 返回单个点的高度
	float PtHeight( int nX, int nY );
	// 创建顶点缓存对象
	void BuildVBOs();
};

大部分代码都很简单,这里不多加解释。

下面我们来定义一些全局变量:

bool		g_fVBOSupported = false;							// 是否支持顶点缓存对象
CMesh*		g_pMesh = NULL;										// 网格数据
float		g_flYRot = 0.0f;									// 旋转角度
int			g_nFPS = 0, g_nFrames = 0;							// 帧率计数器
DWORD		g_dwLastFPS = 0;									// 上一帧的计数	
下面的代码加载高度图,它和34课的内容差不多,在这里不多加解释了:
//加载高度图
bool CMesh :: LoadHeightmap( char* szPath, float flHeightScale, float flResolution )
{

	FILE* fTest = fopen( szPath, "r" );							
	if( !fTest )												
		return false;											
	fclose( fTest );											

	// 加载图像文件
	m_pTextureImage = auxDIBImageLoad( szPath );				

	// 读取顶点数据
	m_nVertexCount = (int) ( m_pTextureImage->sizeX * m_pTextureImage->sizeY * 6 / ( flResolution * flResolution ) );
	m_pVertices = new CVec[m_nVertexCount];						
	m_pTexCoords = new CTexCoord[m_nVertexCount];				
	int nX, nZ, nTri, nIndex=0;									
	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++ )
			{
				flX = (float) nX + ( ( nTri == 1 || nTri == 2 || nTri == 5 ) ? flResolution : 0.0f );
				flZ = (float) nZ + ( ( nTri == 2 || nTri == 4 || nTri == 5 ) ? flResolution : 0.0f );

				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 );

				m_pTexCoords[nIndex].u = flX / m_pTextureImage->sizeX;
				m_pTexCoords[nIndex].v = flZ / m_pTextureImage->sizeY;

				nIndex++;
			}
		}
	}

	// 载入纹理,它和高度图是同一副图像
	glGenTextures( 1, &m;_nTextureId );							
	glBindTexture( GL_TEXTURE_2D, m_nTextureId );				
	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);

	// 释放纹理数据
	if( m_pTextureImage )
	{
		if( m_pTextureImage->data )
			free( m_pTextureImage->data );
		free( m_pTextureImage );
	}
	return true;
}
下面的代码用来计算(x,y)处的亮度
//计算(x,y)处的亮度
float CMesh :: PtHeight( int nX, int nY )
{
	int nPos = ( ( nX % m_pTextureImage->sizeX )  + ( ( nY % m_pTextureImage->sizeY ) * m_pTextureImage->sizeX ) ) * 3;
	float flR = (float) m_pTextureImage->data[ nPos ];			// 返回红色分量
	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 );		// 计算亮度
}

下面的代码把顶点数据绑定到顶点缓存,即把内存中的数据发送到显存
void CMesh :: BuildVBOs()
{
glGenBuffersARB( 1, &m_nVBOVertices ); // 创建一个顶点缓存,并把顶点数据绑定到缓存
glBindBufferARB( GL_ARRAY_BUFFER_ARB, m_nVBOVertices );
glBufferDataARB( GL_ARRAY_BUFFER_ARB, m_nVertexCount*3*sizeof(float), m_pVertices, GL_STATIC_DRAW_ARB );

glGenBuffersARB( 1, &m_nVBOTexCoords ); // 创建一个纹理缓存,并把纹理数据绑定到缓存
glBindBufferARB( GL_ARRAY_BUFFER_ARB, m_nVBOTexCoords );
glBufferDataARB( GL_ARRAY_BUFFER_ARB, m_nVertexCount*2*sizeof(float), m_pTexCoords, GL_STATIC_DRAW_ARB );

// 删除分配的内存
delete [] m_pVertices; m_pVertices = NULL;
delete [] m_pTexCoords; m_pTexCoords = NULL

  }

好了,现在到了初始化的地方了。首先我将分配并载入纹理数据。接着检测是否支持VBO扩展。如果支持我们将把函数指针和它对应的函数关联起来,如果不支持将只返回数据。
//初始化
BOOL Initialize (GL_Window* window, Keys* keys)					
{
	g_window	= window;
	g_keys		= keys;

	// 载入纹理数据
	g_pMesh = new CMesh();										
	if( !g_pMesh->LoadHeightmap( "terrain.bmp",					
								MESH_HEIGHTSCALE,
								MESH_RESOLUTION ) )
	{
		MessageBox( NULL, "Error Loading Heightmap", "Error", MB_OK );
		return false;
	}

	// 检测是否支持VBO扩展
#ifndef NO_VBOS
	g_fVBOSupported = IsExtensionSupported( "GL_ARB_vertex_buffer_object" );
	if( g_fVBOSupported )
	{
		// 获得函数的指针
		glGenBuffersARB = (PFNGLGENBUFFERSARBPROC) wglGetProcAddress("glGenBuffersARB");
		glBindBufferARB = (PFNGLBINDBUFFERARBPROC) wglGetProcAddress("glBindBufferARB");
		glBufferDataARB = (PFNGLBUFFERDATAARBPROC) wglGetProcAddress("glBufferDataARB");
		glDeleteBuffersARB = (PFNGLDELETEBUFFERSARBPROC) wglGetProcAddress("glDeleteBuffersARB");
		// 创建VBO对象
		g_pMesh->BuildVBOs();									
	}
#else 
	g_fVBOSupported = false;
#endif
	//设置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;												
}
下面的函数用来检测是否包含特定的扩展名称
// 返回是否支持指定的扩展
bool IsExtensionSupported( char* szTargetExtension )
{
	const unsigned char *pszExtensions = NULL;
	const unsigned char *pszStart;
	unsigned char *pszWhere, *pszTerminator;

	pszWhere = (unsigned char *) strchr( szTargetExtension, ' ' );
	if( pszWhere || *szTargetExtension == '\0' )
		return false;

	// 返回扩展字符串
	pszExtensions = glGetString( GL_EXTENSIONS );

	// 在扩展字符串中搜索
	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' )
				//如果存在返回True
				return true;
		pszStart = pszTerminator;
	}
	return false;
}

好了,几乎结束了,我们下面来看看我们的渲染代码.

void Draw (void)
{
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity ();

// 显示当前的帧率
if( GetTickCount() - g_dwLastFPS >= 1000 )
{
g_dwLastFPS = GetTickCount();
g_nFPS = g_nFrames;
g_nFrames = 0;

char szTitle[256]={0};
sprintf( szTitle, "Lesson 45: NeHe & Paul Frazee's VBO Tut - %d Triangles, %d FPS", g_pMesh->m_nVertexCount / 3, g_nFPS );
if( g_fVBOSupported ) // 是否支持VBO
strcat( szTitle, ", Using VBOs" );
else
strcat( szTitle, ", Not Using VBOs" );
SetWindowText( g_window->hWnd, szTitle ); // 设置窗口标题
}
g_nFrames++;

// 设置视口
glTranslatef( 0.0f, -220.0f, 0.0f );
glRotatef( 10.0f, 1.0f, 0.0f, 0.0f );
glRotatef( g_flYRot, 0.0f, 1.0f, 0.0f );

// 使用顶点,纹理坐标数组
glEnableClientState( GL_VERTEX_ARRAY );
glEnableClientState( GL_TEXTURE_COORD_ARRAY );

为了使用VBO,你必须告诉OpenGL内存中的那部分需要加载到VBO中。所以第一步我们要起用顶点数组和纹理坐标数组。接着我们必须告诉OpenGL去把数据的指针设置到特定的地方,glVertexPointer函数可以完成这个功能。
我们分为启用和不启用VBO两个路径来渲染,他们都差不多,唯一的区别是当你需要把指针指向VBO缓存时,记得把数据指针设置NULL。

 

	// 如果支持VBO扩展
if( g_fVBOSupported )
{
glBindBufferARB( GL_ARRAY_BUFFER_ARB, g_pMesh->m_nVBOVertices );
glVertexPointer( 3, GL_FLOAT, 0, (char *) NULL ); // 设置顶点数组的指针为顶点缓存
glBindBufferARB( GL_ARRAY_BUFFER_ARB, g_pMesh->m_nVBOTexCoords );
glTexCoordPointer( 2, GL_FLOAT, 0, (char *) NULL ); // 设置顶点数组的指针为纹理坐标缓存
}
// 不支持VBO扩展
else
{
glVertexPointer( 3, GL_FLOAT, 0, g_pMesh->m_pVertices );
glTexCoordPointer( 2, GL_FLOAT, 0, g_pMesh->m_pTexCoords );
}
好了,渲染所有的三角形吧
// 渲染
glDrawArrays( GL_TRIANGLES, 0, g_pMesh->m_nVertexCount );
最后,别忘了恢复到默认的OpenGL状态.

glDisableClientState( GL_VERTEX_ARRAY );
glDisableClientState( GL_TEXTURE_COORD_ARRAY );
}

如果你想更多的了解VBO对象,我建议你读一下SGI的扩展说明:
http://oss.sgi.com/projects/ogl-sample/registry
它会给你更多的信息

好了,那就是这次的课程,如果你发现任何问题,请联系我.

版权与使用声明:
我是个对学习和生活充满激情的普通男孩,在网络上我以DancingWind为昵称,我的联系方式是[email protected],如果你有任何问题,都可以联系我。

引子
网络是一个共享的资源,但我在自己的学习生涯中浪费大量的时间去搜索可用的资料,在现实生活中花费了大量的金钱和时间在书店中寻找资料,于是我给自己起了个昵称DancingWind,其意义是想风一样从各个知识的站点中吸取成长的养料。在飘荡了多年之后,我决定把自己收集的资料整理为一个统一的资源库。

版权声明
所有DancingWind发表的内容,大多都来自共享的资源,所以我没有资格把它们据为己有,或声称自己为这些资源作出了一点贡献。故任何人都可以复制,修改,重新发表,甚至以自己的名义发表,我都不会追究,但你在做以上事情的时候必须保证内容的完整性,给后来的人一个完整的教程。最后,任何人不能以这些资料的任何部分,谋取任何形式的报酬。

发展计划
在国外,很多资料都是很多人花费几年的时间慢慢积累起来的。如果任何人有兴趣与别人共享你的知识,我很欢迎你与我联系,但你必须同意我上面的声明。

感谢
感谢我的母亲一直以来对我的支持和在生活上的照顾。
感谢我深爱的女友田芹,一直以来默默的在精神上和生活中对我的支持,她甚至把买衣服的钱都用来给我买书了,她真的是我见过的最好的女孩,希望我能带给她幸福。

源码 RAR格式