OpenGL学习脚印: 基本图形绘制方式比较(glBegin,glCallList,glVertexPointer,VBO)

OpenGL学习脚印: 基本图形绘制方式比较

写在前面

               本节主要讨论OpenGL下基本图形(points, lines, triangles, polygons, quads, fans and strips)的绘制方式,比较传统和现代绘制方式的区别。本文整理自网络,保留了部分原文,参考资料部分列出了主要参考内容。本节示例代码在vs2012下测试通过,如果发现了错误请纠正我。转载需经过作者同意。


通过本节,可以了解到:

  • 传统立即模式Immediate Mode绘图
  • 传统显示列表Display List绘图
  • 顶点数组Vertex Arrays绘图
  • 现代的VBO VAO绘图
  • 现代结合Shader的绘图



1.传统方式绘制

传统绘制方式在OpenGL新版本中已经废弃,虽然在兼容模式下还能工作,但不建议使用。这里列出他们仅供学习。

1.1 立即模式Immediate Mode

传统的使用glBegin...glEnd方式制定绘制方式,在这两个函数对之间给出绘制的数据,这种方式成为立即模式。

立即模式绘图,示例代码如下所示:

//依赖库glew32.lib freeglut.lib
//使用glBegin...glEnd绘制三角形(已过时,仅为学习目的)
#  include 
#  include 

void userInit();
void reshape(int w,int h);
void display( void );
void keyboardAction( unsigned char key, int x, int y );

int main( int argc, char **argv )
{
	glutInit(&argc, argv);

    glutInitDisplayMode( GLUT_RGBA|GLUT_SINGLE);
	glutInitWindowPosition(100,100);
    glutInitWindowSize( 512, 512 );
    glutCreateWindow( "Triangle demo" );
	 
    glewInit();
    userInit();
	glutReshapeFunc(reshape);
    glutDisplayFunc( display );
    glutKeyboardFunc( keyboardAction );
    glutMainLoop();
    return 0;
}
//自定义初始化函数
void userInit()
{
	 glClearColor( 0.0, 0.0, 0.0, 0.0 );
	 glColor4f(1.0,1.0,0.0,0.0);
}
//调整窗口大小回调函数
void reshape(int w,int h)
{
	glViewport(0,0,(GLsizei)w,(GLsizei)h);
}
//绘制回调函数
void display( void )
{
    glClear( GL_COLOR_BUFFER_BIT);
	//使用传统的glBegin...glEnd绘制三角形
    glBegin(GL_TRIANGLES); 
         glVertex3f(-0.5,-0.5,0.0);  
         glVertex3f(0.5,0.0,0.0);  
         glVertex3f(0.0,0.5,0.0);  
    glEnd();  
	glFlush();
}
//键盘按键回调函数
void keyboardAction( unsigned char key, int x, int y )
{
    switch( key ) 
	{
		case 033:  // Escape key
			exit( EXIT_SUCCESS );
			break;
    }
}

本例及本文所有绘制效果都如下图所示:

OpenGL学习脚印: 基本图形绘制方式比较(glBegin,glCallList,glVertexPointer,VBO)_第1张图片

1.2 显示列表Display List

显示列表是一组存储在一起的OpenGL函数,可以再以后执行。调用一个显示列表时,它所存储的函数就会按照顺序执行。显示列表通过存储OpenGL函数,可以提高性能。如果需要多次重复绘制同一个几何图形,或者如果有一些需要多次调用的用于更改状态的函数,就可以把他们存储在显示列表中。例如绘制三轮车的车轮的有效方法是,把绘制一个车轮的操作存储在显示列表中,并3次执行这个显示列表。每次在执行时,适当地做出坐标转换即可(参考自[2])。

显示列表,示例代码如下所示,执行效果同上图:

//依赖库glew32.lib freeglut.lib
//使用顶点列表绘制三角形(已过时,仅为学习目的)

#  include 
#  include 

void userInit();
void reshape(int w,int h);
void display( void );
void keyboardAction( unsigned char key, int x, int y );
//显示列表句柄
GLuint displayListId;
int main( int argc, char **argv )
{
	glutInit(&argc, argv);

    glutInitDisplayMode( GLUT_RGBA|GLUT_SINGLE);
	glutInitWindowPosition(100,100);
    glutInitWindowSize( 512, 512 );
    glutCreateWindow( "Triangle demo" );
	 
    glewInit();
    userInit();
	glutReshapeFunc(reshape);
    glutDisplayFunc( display );
    glutKeyboardFunc( keyboardAction );
    glutMainLoop();
    return 0;
}
//自定义初始化函数
void userInit()
{
	 glClearColor( 0.0, 0.0, 0.0, 0.0 );
	 glColor4f(1.0,1.0,0.0,0.0);
	 //创建显示列表
	 displayListId = glGenLists(1);
	 glNewList(displayListId,GL_COMPILE);
		 glBegin(GL_TRIANGLES);
			 glVertex3f(-0.5,-0.5,0.0);  
			 glVertex3f(0.5,0.0,0.0);  
			 glVertex3f(0.0,0.5,0.0);
		 glEnd();
	glEndList();
}
//调整窗口大小回调函数
void reshape(int w,int h)
{
	glViewport(0,0,(GLsizei)w,(GLsizei)h);
}
//绘制回调函数
void display( void )
{
    glClear( GL_COLOR_BUFFER_BIT);
	//利用显示列表,绘制三角形
	glCallList(displayListId);
	glFlush();
}
//键盘按键回调函数
void keyboardAction( unsigned char key, int x, int y )
{
    switch( key ) 
	{
		case 033:  // Escape key
			exit( EXIT_SUCCESS );
			break;
    }
}

2.现代方式绘制

2.1 顶点数组绘图

使用顶点数组方式,需要利用glEnableClientState开启一些特性,这里开启顶点数组特性使用glEnableClientState(GL_VERTEX_ARRAY)

使用顶点数组时,用户定义好存储顶点的数据,在调用glDrawArrays、glDrawElements之类的函数时,通过glVertexPointer设定的指针,传送数据到GPU。当调用完glDrawArrays后,GPU中已经有了绘图所需数据,用户可以释放数据空间。(参考自[3])

顶点数组方式绘图示例代码如下所示:

//依赖库glew32.lib freeglut.lib
//使用Vertex Arrays顶点数组绘制三角形(不推荐使用)
#  include 
#  include 

void userInit();
void reshape(int w,int h);
void display( void );
void keyboardAction( unsigned char key, int x, int y );
//定义一个包含3个float的结构体
//为了保持简单,暂时未引入c++类概念
struct vec3f { 
	GLfloat x, y, z;
};
int main( int argc, char **argv )
{
	glutInit(&argc, argv);

    glutInitDisplayMode( GLUT_RGBA|GLUT_SINGLE);
	glutInitWindowPosition(100,100);
    glutInitWindowSize( 512, 512 );
    glutCreateWindow( "Triangle demo" );
	
    glewInit();
    userInit();
	glutReshapeFunc(reshape);
    glutDisplayFunc( display );
    glutKeyboardFunc( keyboardAction );
    glutMainLoop();
    return 0;
}
//自定义初始化函数
void userInit()
{
	 glClearColor( 0.0, 0.0, 0.0, 0.0 );
	 glColor4f(1.0,1.0,0.0,0.0);
}
//调整窗口大小回调函数
void reshape(int w,int h)
{
	glViewport(0,0,(GLsizei)w,(GLsizei)h);
}
//绘制回调函数
void display( void )
{
    glClear( GL_COLOR_BUFFER_BIT);
	//利用顶点数组,绘制三角形
	const int num_indices = 3;
	 //创建保存顶点的结构体数组
	 vec3f *vertices = new vec3f[num_indices];
	 // 顶点1
	 vertices[0].x = -0.5f; 
	 vertices[0].y = -0.5f; 
	 vertices[0].z = 0.0f; 
	 // 顶点2
	 vertices[1].x = 0.5f; 
	 vertices[1].y = 0.0f; 
	 vertices[1].z = 0.0f; 
	 //顶点3
	 vertices[2].x = 0.0f; 
	 vertices[2].y = 0.5f;
	 vertices[2].z = 0.0f;
	 // 启用vertex arrays 
	 glEnableClientState(GL_VERTEX_ARRAY);
	 //定义顶点数组
	 glVertexPointer( 
		 3,			// 每个顶点的维度 
		 GL_FLOAT,	// 顶点数据类型
		 0,			// 连续顶点之间的间隙,这里为0
		 vertices	//指向第一个顶点的第一个坐标的指针
	);
	glDrawArrays(GL_TRIANGLES, 0, num_indices);
	glDisableClientState(GL_VERTEX_ARRAY);
	//释放内存空间
	delete[] vertices;
	glFlush();
}
//键盘按键回调函数
void keyboardAction( unsigned char key, int x, int y )
{
    switch( key ) 
	{
		case 033:  // Escape key
			exit( EXIT_SUCCESS );
			break;
    }
}

2.2 现代VBO VAO绘图

首先了解下VBO和VAO。

根据文[4]所述:

A Vertex Array Object (VAO) is an object which contains one or more Vertex Buffer Objects and is designed to store the information for a complete rendered object. In our example this is a diamond consisting of four vertices as well as a color for each vertex.

A Vertex Buffer Object (VBO) is a memory buffer in the high speed memory of your video card designed to hold information about vertices. In our example we have two VBOs, one that describes the coordinates of our vertices and another that describes the color associated with each vertex. VBOs can also store information such as normals, texcoords, indicies, etc.

VAO即Vertex Array Object ,是一个包含一个或多个VBO的对象,被设计用来存储一个完整被渲染对象所需的信息。

VBO即Vertex Buffer Object,是一个在高速视频卡中的内存缓冲,用来保存顶点数据,也可用于包含诸如归一化向量、纹理和索引等数据。

根据[1]中所述:

VBO stores actual vertex data. The most important thing about a VBO is not that it stores data, though it is its primary function, but where it is stored. A VBO object resides on GPU, the graphics processing unit. This means it is very fast, it is stored in memory on the graphics card itself. How cool is that? Storing data on the computer processor or RAM is slow mostly because it needs to be transferred to the GPU, and this transfer can be costly.

VAO represents properties, rather than actual data. But these properties do describe the objects actually stored in theVBO.VAO can be thought of as anadvanced memory pointer to objects. Similar to C-language pointers, they do a whole lot more tracking than just the address. They are very sophisticated.

VAOs are a lot like helpers, rather than actual data storage. That's what they're for. They also keep track of properties to be used in current rendering process. Finally, they describe properties of objects, rather than the raw data of those objects that is by all means already stored in a VBO.

VAOs are not directly related to VBOs, although it may seem that way at first. VAOs simply save time to enable a certain application state needed to be set. Without VAO, you would have to call a bunch of gl* commands to do the same thing.


VBO存储了实际的数据,真正重要的不是它存储了数据,而是他将数据存储在GPU中。这意味着VBO它会很快,因为存在RAM中的数据需要被传送到GPU中,因此这个传送是有代价的。

VAO代表的是一些描述存储在VBO中对象的属性。VAO可以被视为指向对象的高级内存指针,有点类似于C语言指针,但比地址多了跟多的跟踪作用。他们很复杂。

VAO很像是辅助对象,而不是实际的数据存储对象。他们记录了当前绘制过程中的属性,描述对象的属性,而不是已经存储在VBO中原始数据。

VAO并不与VBO直接相关,进过初看起来如此。VAOs节省了设置程序所需的状态的时间。如果没有VAO,你需要调用一堆类似gl*之类的命令。这里从songho[5]文的用户反馈列表中找到一个示例解释了VAO节省时间的例子:

提问:How do Vertex Buffer Objects relate to Vertex Array Objects?

songho回答:

The name, VAO (Vertex Array Object) looks somewhat related to VBO, but it is not. VAO is for encapsulating vertex array states/functions into it. Therefore, you can replace the multiple OpenGL calls to a single call of glBindVertexArray(), in order to setup various vertex array states and attributes before drawing.
The following example gives a better sense of VAO purpose;

// draw with VAO
glBindVertexArray(vaoId); // bind vao
glDrawElements(...);
glBindVertexArray(0);     // unbind vao

// draw without VAO
// need to set many states before drawing
glEnableClientState(GL_VERTEX_ARRAY); // enable client states
glEnableClientState(GL_NORMAL_ARRAY);
glBindBuffer(GL_ARRAY_BUFFER, vboId); // bind vbo
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iboId);
glVertexPointer(3, GL_FLOAT, 0, 0); // vertex attributes
glNormalPointer(GL_FLOAT, 0, offset); // normal attributes
glDrawElements(...);
glBindBuffer(GL_ARRAY_BUFFER, 0); // unbind vbo
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
You can dramatically reduce the function call overhead and make the code much simpler with VAO.

But the overall performance gain is very minimal.
从这里可以看出使用VAO方式的好处,更多关于VAO、VBO讨论不在此处展开。


单独使用VBO执行绘制的示例代码如下:

(这里并没有使用VAO,因此仍然要使用glEnableClientState(GL_VERTEX_ARRAY),稍后会给出VAO与VBO结合例子)

//依赖库glew32.lib freeglut.lib
//使用VBO绘制三角形(现代OpenGL方式)
#  include 
#  include 

void userInit();
void reshape(int w,int h);
void display( void );
void keyboardAction( unsigned char key, int x, int y );
//VBO句柄
GLuint vboId;
int main( int argc, char **argv )
{
	glutInit(&argc, argv);

    glutInitDisplayMode( GLUT_RGBA|GLUT_SINGLE);
	glutInitWindowPosition(100,100);
    glutInitWindowSize( 512, 512 );
    glutCreateWindow( "Triangle demo" );
	 
    glewInit();
    userInit();
	glutReshapeFunc(reshape);
    glutDisplayFunc( display );
    glutKeyboardFunc( keyboardAction );
    glutMainLoop();
    return 0;
}
//自定义初始化函数
void userInit()
{
	 glClearColor( 0.0, 0.0, 0.0, 0.0 );
	 glColor4f(1.0,1.0,0.0,0.0);
	 //创建顶点数据
	 GLfloat vertices[] = {
		-0.5,-0.5,0.0,
		0.5,0.0,0.0,
		0.0,0.5,0.0
	 };
	 //分配vbo句柄
	 glGenBuffersARB(1,&vboId);
	 //GL_ARRAY_BUFFER_ARB表示作为顶点数组解析
	 glBindBufferARB(GL_ARRAY_BUFFER_ARB,vboId);
     //拷贝数据
	 glBufferDataARB(GL_ARRAY_BUFFER_ARB,sizeof(vertices),
			vertices,GL_STATIC_DRAW_ARB);
	 glBindBufferARB(GL_VERTEX_ARRAY,0);
}
//调整窗口大小回调函数
void reshape(int w,int h)
{
	glViewport(0,0,(GLsizei)w,(GLsizei)h);
}
//绘制回调函数
void display( void )
{
    glClear( GL_COLOR_BUFFER_BIT);
	glBindBufferARB(GL_ARRAY_BUFFER_ARB, vboId);//绑定vbo
	glEnableClientState(GL_VERTEX_ARRAY);//启用顶点数组属性
	glVertexPointer(3, GL_FLOAT, 0, 0);//如何解析vbo中数据
	glDrawArrays(GL_TRIANGLES, 0, 3);
	glDisableClientState(GL_VERTEX_ARRAY);
	glBindBufferARB(GL_ARRAY_BUFFER_ARB,0);//解除绑定
	glFlush();
}
//键盘按键回调函数
void keyboardAction( unsigned char key, int x, int y )
{
    switch( key ) 
	{
		case 033:  // Escape key
			exit( EXIT_SUCCESS );
			break;
    }
}

2.3 结合Shader绘图

结合现代Shader的绘图,不再此处展开,请参见另外一篇博客《OpenGL学习脚印: 顶点数据传送和着色器处理1》。

3.总结几种方式利弊

  • 使用立即模式的缺点很明显,数据量大一点的话,代码量增加,而且数据发送到服务端需要开销;
  • 使用显示列表,显示列表是一个服务端函数,因此它免除了传送数据的额外开销。但是,显示列表一旦编译后,其中的数据无法修改。
  • 使用顶点数组,可以减少函数调用和共享顶点数据的冗余。但是,使用顶点数组时,顶点数组相关函数是在客户端,因此数组中数据在每次被解引用时必须重新发送到服务端,额外开销不可忽视。
  • 使用VBO在服务端创建缓存对象,并且提供了访问函数来解引用数组;例如在顶点数组中使用的函数如 glVertexPointer(), glNormalPointer(), glTexCoordPointer()。同时,VBO内存管理会根据用户提示,"target"  和"usage"模式,将缓存对象放在最佳地方。因此内存管理会通过在系统内存、AGP内存和视频卡内存(system, AGP and video memory)这3中内存见平衡来优化缓存。另外,不像显示列表,VBO中数据可以通过映射到客户端内存空间而被用户读取和更新。VBO的另外一个优势是它像显示列表和纹理一样,能和多个客户端共享缓存对象。可见使用VBO优势很明显。(参考自:[5])


4.参考资料

[1]:  Learn to draw OpenGL primitives

[2]: 《OpenGL编程指南》 红宝书 第七版

[3]:   When does glVertexPointer() copy data?

[4] :    Tutorial2: VAOs, VBOs, Vertex and Fragment Shaders (C / SDL)

[5] :  OpenGL Vertex Buffer Object (VBO)

你可能感兴趣的:(OpenGL学习脚印)