一、提要
OpenGL作为一个高性能的图形接口,性能能肯定是放在第一位的了,
现在的移动平台也是OpenGL ES,这对性能的要求就更高了,
今天我们要接触到的这两个东西—顶点数组和显示列表都是用于实现高性能绘图的手段。
下一篇教程我打算去做一个一个3D漫游的例子,会用到今天的技术。
二、顶点数组
在之前的几篇教程中,有时候绘制一个图形需要很多次函数调用才能完成。
首先调用一次glBegin(),然后为每个点调用一次函数,最后还要调用glEnd().中间
如果还要设置颜色或是法线什么的,函数的调用次数还要增加,这给系统带来了很大的
开销,影响了程序的性能。
OpenGL提供了一些顶点数组函数,允许只用少数几个数组指定大量的与顶点相关的数据,
把数据放在顶点数组中可以提高应用程序的性能还可一减少函数调用的次数,从而提高性能。
另外,使用顶点数组还可以避免共享顶点的冗余处理。
使用顶点数组对几何图元进行渲染的3个步骤:
1)激活数组,存储顶点坐标、RGBA颜色等等;
2)装载数组
3)利用数组绘制图形
首先用一个简单的例子来演示一下,用顶点数组绘制带颜色的三角形。
首先在initializeGL()中设置参数:
void NeHeWidget::initializeGL()
{
// 启用阴影平滑
glShadeModel( GL_SMOOTH );
// 黑色背景
glClearColor( 0.0, 0.0, 0.0, 0.0 );
// 设置深度缓存
glClearDepth( 1.0 );
// 启用深度测试
glEnable( GL_DEPTH_TEST );
// 所作深度测试的类型
glDepthFunc( GL_LEQUAL );
// 告诉系统对透视进行修正
glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );
glClearColor(0.0, 0.0, 0.0, 0.0);
// 启用顶点数组
glEnableClientState(GL_VERTEX_ARRAY);
// 颜色数组也需要启用
glEnableClientState(GL_COLOR_ARRAY);
// 默认就是此参数,可忽略,为了明确说明特意指定
glShadeModel(GL_SHADE_MODEL);
// 顶点数组数据
static GLfloat fVertices[] = { -0.5, -0.5,
0.5, -0.5,
0.5, 0.5,
};
// 颜色数组
static GLfloat fColor[] = { 1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
};
// 指定顶点数组数据
glVertexPointer(2, GL_FLOAT, 0, fVertices);
//指定颜色数组
glColorPointer(3, GL_FLOAT, 0, fColor);
}
接着在paintGL()中绘制图形:
void NeHeWidget::paintGL()
{
// 清除屏幕和深度缓存
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glLoadIdentity();
//移到屏幕的左半部分,并且将视图推入屏幕背后足够的距离以便我们可以看见全部的场景
glTranslatef(0.0f,0.0f,zoom);
glBegin(GL_TRIANGLES);
glArrayElement(0);
glArrayElement(1);
glArrayElement(2);
glEnd();
}
可以看到,我们得到的效果和一个个定义顶点,一个个定义颜色是一样的。
下面解释其中的几个函数:
void glEnableClientState(GLenum array)
用于制动需要启用的数组,例子中GL_VERTEX_ARRAY表示顶点数组,GL_COLOR_ARRAY表示颜色数组。开启一种顶点数组都必须调用glEnableClientState来激活.
glDisableClientState(GLenum array)
用于关闭相应的数组。
void glVertexPointer( GLint size,GLenum type,GLsizei stride,const GLvoid * pointer);
指定需要访问的空间的坐标数据,作用是把一数组与图形的顶点关联在一起。
glColorPointer意思相类似。
glArrayElement则根据顶点数组来调用相应的函数,每次只调用1个顶点。
跨距
如果将两个数组合并在一起,那么坐标和颜色的数据就在一个数组了,现在代码修改如下:
static GLfloat data[]={
-0.5, -0.5,1.0, 0.0, 0.0,
0.5, -0.5, 0.0, 1.0, 0.0,
0.5, 0.5, 0.0, 0.0, 1.0,
};
glVertexPointer(2,GL_FLOAT,5*sizeof(GLfloat),&data[0]);
glColorPointer(3,GL_FLOAT,5*sizeof(GLfloat),&data[2]);
这里用到了跨距的概念,第3个参数指定跨距,如颜色,从数组的每组数据的第3个开始取数据,然后跨5个,坐标顶点则从数据每组数据的第1个开始取数据并跨5个.
注意:由于跨距需要计算数据类型,所以数组的数据类型需要相同.
最后得到的效果与之前的一样。
解引用数组
下面要用到的是glDrawElements,学名叫解引用数组,作用类似于循环调用glArrayElement,但需要定义一个索引的数组。有了它,我们只需要一句函数调用就可以把图形绘制出来了,继续修改代码。
只需修改paintGL()中的代码,将glBegin()和glEnd()还有它们之间的代码换成:
//定义索引数组
GLubyte index[]= {0,1,2,3} ;
//解引用数组
glDrawElements(GL_TRIANGLES,3,GL_UNSIGNED_BYTE,index);
接下来,我们来利用解引用数组绘制一个立方体。
//正方体绘制
static GLfloat data[]={
-1.0, -1.0, -1.0 , 0.0, 0.0, 0.0 ,
1.0, -1.0, -1.0 ,0.0, 0.0, 1.0,
1.0, 1.0, -1.0 ,0.0, 1.0, 0.0 ,
-1.0, 1.0,-1.0 ,0.0,1.0, 1.0,
-1.0, -1.0, 1.0 ,1.0, 0.0, 0.0,
1.0, -1.0, 1.0 , 1.0, 0.0, 1.0,
1.0, 1.0, 1.0 ,1.0,1.0, 0.0,
-1.0, 1.0, 1.0 ,1.0, 1.0, 1.0 ,
};
//指定顶点数据
glVertexPointer(3,GL_FLOAT,6*sizeof(GLfloat),&data[0]);
//指定颜色数据
glColorPointer(3,GL_FLOAT,6*sizeof(GLfloat),&data[2]);
//指定索引序列
static const GLint face_lists[][4]=
{
0, 3, 2, 1,
6, 5, 1, 2,
3, 0, 4, 7,
3, 7, 6, 2,
0, 4, 5, 1,
7, 4, 5, 6,
};//注意每个面绘制的顺序,背面采用顺时针方向。
//绘制图形
glDrawElements( GL_QUADS, 24, GL_UNSIGNED_INT, face_lists );
最后结果如下:
我们在定义出数组之后,只用了一条语句便画出了一个立方体,而不是像之前那样定义24次顶点,同时定义24次颜色。
glArrayElement击败了glVertex*,但是glDrawElements又比glArrayElement更高。glDrawElements使用的时候将一连串glArrayElement需要使用的数据放在一个数组中(此例中是byRectIndices),然后通过一个函数调用一次指定。注意啊,glDrawElements自带glBegin及glEnd效果,不再需要它们。(事实上,在OpenGL中命名中带Draw的一般都不需要glBegin和glEnd)程序运行的效果与glVertexArrayWithColor例子运行效果相同。
需要提一下的还有glMultiDrawElements,它是glDrawElements的升级版,一条语句相当于多条glArrayElements,接口有点麻烦,使用方法可以参考文档。
混合数组
这个有点类似上面说到的跨距的使用了,也是将不同的顶点数组放在同一个数组中,然后用相应的方法把
它解析出来,这里要用到的是glInterleavedArrays,函数将会根据参数,激活各种顶点数组,并存储顶点。
修改代码:
static GLfloat data[]={
-0.5, -0.5,1.0, 0.0, 0.0,
0.5, -0.5, 0.0, 1.0, 0.0,
0.5, 0.5, 0.0, 0.0, 1.0,
};
glInterleavedArrays(GL_C3F_V3F,0,data);
glDrawArrays(GL_LINES,0,4);
对应于相应的参数,就可以实现相应的图形,感觉这个比跨距的使用更简单一些,代码也会更加清晰。
总结
OpenGL提供了一系列更加高效的函数以完成对效率要求苛刻的任务,我们将逐一介绍,会发现,使用难度越来越高,适用范围越来越窄,但是函数调用越来越少,功能越来越强大,事实上,函数调用少并不是OpenGL这样设计的唯一理由,越是这样同时处理多个数据的接口,因为一个接口掌握的信息越多,那么也就越能更多的对其进行优化。
三、显示列表
显示列表可以用来存储OpenGL函数,供以后执行。如果需要多次重绘同一个几何图形,或者如果有一些需要多次调用的用于更改状态的函数,把这些函数存储在显示列表中是一个很好的思路。
为了实际操作一下,我们先在我们的项目中添加glut库。
在.pro文件中添加
LIBS += -lglut
简单介绍一下glut库
GLUT(英文全写:OpenGL Utility Toolkit)是一个处理OpenGL程式的工具库,负责处理和底层操作系统的呼叫以及I/O,并包括了以下常见的功能:
定义以及控制视窗
侦测并处理键盘及鼠标的事件
以一个函数呼叫绘制某些常用的立体图形,例如长方体、球、以及犹他茶壶(实心或只有骨架,如glutWireTeapot())
提供了简单选单列的实现。
*注意:在使用glut函数之前一定要对其进行和初始化,在man.cpp的main函数中加入glutInit(&argc, argv);
创建显示列表
OpenGL提供类似于绘制图元的结构即glBegin()与glEnd()的形式创建显示列表,其相应的函数为:
void glNewList(GLuint list,GLenum mode);
说明一个显示列表的开始,其后的OpenGL函数存入显示列表中,直至调用结束表的函数(见下面)。参数list是一个正整数,它标志唯一的显示列表。参数mode的可能值有GL_COMPILE和GL_COMPILE_AND_EXECUTE。若要使后面的函数语句只存入而不执行,则用GL_COMPILE;若要使后面的函数语句存入表中且按瞬时方式执行一次,则用GL_COMPILE_AND_EXECUTE。
void glEndList(void);
标志显示列表的结束。
执行显示列表
在建立显示列表以后就可以调用执行显示列表的函数来执行它,并且允许在程序中多次执行同一显示列表,同时也可以与其它函数的瞬时方式混合使用。显示列表执行的函数形式如下:
void glCallList(GLuint list);
执行显示列表。参数list指定被执行的显示列表。显示列表中的函数语句按它们被存放的顺序依次执行;若list没有定义,则不会产生任何事情。下面举出一个应用显示列表的简单例子:
首先是没有用显示列表的状态:
//绘制100个茶壶
glColor3f( 0.0, 1.0,1.0 );
for(int i=0;i<10;i++)
{
for(int j=0;j<10;j++)
{
glTranslatef(j*0.5f,0.0f,0.0f);
glPushMatrix();
glutWireTeapot(0.2);
glPopMatrix();
}
glTranslatef(0.0f,-0.4f,0.0f);
}
结果会在屏幕上绘制100个茶壶。
下面用显示列表来实现:
在initializeGL()中添加:
glNewList (teapotList, GL_COMPILE);
glutWireTeapot(0.2);
glEndList ();
创建了一个teapotList的列表,接着我们在循环中替换相应的代码:
for(int i=0;i<10;i++)
{
for(int j=0;j<10;j++)
{
glPushMatrix();
glTranslatef(j*0.5f,0.0f,0.0f);
//glutWireTeapot(0.2);
glCallList (teapotList);
glPopMatrix();
}
glTranslatef(0.0f,-0.4f,0.0f);
}
实际运行发现渲染的速度会快一点点(感觉是心理作用),可能是小的场景差别不是很大,有兴趣的同学可以测一下fps,结果就会明晰了。
四.参考资料
1. 《 OpenGL Reference Manual 》, OpenGL 参考手册
2. 《 OpenGL 编程指南》(《 OpenGL Programming Guide 》), Dave Shreiner , Mason Woo , Jackie Neider , Tom Davis 著,徐波译,机械工业出版社
3. 《win32 OpenGL编程 》 一个大牛的博客 http://blog.csdn.net/vagrxie/article/category/628716/34. 《OpenGL函数思考 》 里面有很多OpenGL函数的通俗解释 http://blog.csdn.net/shuaihj
更多详细信息请查看 java教程网 http://www.itchm.com/forum-59-1.html