您的位置:首页 > 运维架构

opengl入门8

2016-01-16 10:22 281 查看
OpenGL入门学习[十三]

前一段时间里,论坛有位朋友问什么是状态机。按我的理解,状态机就是一种存在于理论中的机器,它具有以下的特点:

1. 它有记忆的能力,能够记住自己当前的状态。

2. 它可以接收输入,根据输入的内容和自己的状态,修改自己的状态,并且可以得到输出。

3. 当它进入某个特殊的状态(停机状态)的时候,它不再接收输入,停止工作。

理论说起来很抽象,但实际上是很好理解的。

首先,从本质上讲,我们现在的电脑就是典型的状态机。可以对照理解:

1. 电脑的存储器(内存、硬盘等等),可以记住电脑自己当前的状态(当前安装在电脑中的软件、保存在电脑中的数据,其实都是二进制的值,都属于当前的状态)。

2. 电脑的输入设备接收输入(键盘输入、鼠标输入、文件输入),根据输入的内容和自己的状态(主要指可以运行的程序代码),修改自己的状态(修改内存中的值),并且可以得到输出(将结果显示到屏幕)。

3. 当它进入某个特殊的状态(关机状态)的时候,它不再接收输入,停止工作。

OpenGL也可以看成这样的一种机器。让我们先对照理解一下:

1. OpenGL可以记录自己的状态(比如:当前所使用的颜色、是否开启了混合功能,等等,这些都是要记录的)

2. OpenGL可以接收输入(当我们调用OpenGL函数的时候,实际上可以看成OpenGL在接收我们的输入),根据输入的内容和自己的状态,修改自己的状态,并且可以得到输出(比如我们调用glColor3f,则OpenGL接收到这个输入后会修改自己的“当前颜色”这个状态;我们调用glRectf,则OpenGL会输出一个矩形)

3. OpenGL可以进入停止状态,不再接收输入。这个可能在我们的程序中表现得不太明显,不过在程序退出前,OpenGL总会先停止工作的。

还是没理解?呵呵,看来这真不是个好的开始呀,难得等了这么久,好不容易教程有更新了,怎么如此的难懂啊??没关系,实在没理解,咱就不理解它了。接着往下看。

为什么我要提到“状态机”这个枯燥的、晦涩的概念呢?其实它可以帮助我们理解一些东西。

比如我在前面的教程里面,经常说:

可以使用glColor*函数来选择一种颜色,以后绘制的所有物体都是这种颜色,除非再次使用glColor*函数重新设定。

可以使用glTexCoord*函数来设置一个纹理坐标,以后绘制的所有物体都是采用这种纹理坐标,除非再次使用glTexCoord*函数重新设置。

可以使用glBlendFunc函数来指定混合功能的源因子和目标因子,以后绘制的所有物体都是采用这个源因子和目标因子,除非再次使用glBlendFunc函数重新指定。

可以使用glLight*函数来指定光源的位置、颜色,以后绘制的所有物体都是采用这个光源的位置、颜色,除非再次使用glBlendFunc函数重新指定。

……

呵呵,很繁,是吧?“状态机”可以简化这个描述。

OpenGL是一个状态机,它保持自身的状态,除非用户输入一条命令让它改变状态。

颜色、纹理坐标、源因子和目标因子、光源的各种参数,等等,这些都是状态,所以这一句话就包含了上面叙述的所有内容。

此外,“是否启用了光照”、“是否启用了纹理”、“是否启用了混合”、“是否启用了深度测试”等等,这些也都是状态,也符合上面的描述:OpenGL会保持状态,除非我们调用OpenGL函数来改变它。

取得OpenGL的当前状态

OpenGL保存了自己的状态,我们可以通过一些函数来取得这些状态。

首先来说一些启用/禁用的状态。

我们通过glEnable来启用状态,通过glDisable来禁用它们。例如:

glEnable(GL_DEPTH_TEST);

glEnable(GL_BLEND);

glEnable(GL_CULL_FACE);

glEnable(GL_LIGHTING);

glEnable(GL_TEXTURE_2D);

可以用glIsEnabled函数来检测这些状态是否被开启。例如:

glIsEnabled(GL_DEPTH_TEST);

glIsEnabled(GL_BLEND);

glIsEnabled(GL_CULL_FACE);

glIsEnabled(GL_LIGHTING);

glIsEnabled(GL_TEXTURE_2D);

如果状态是开启的,则glIsEnabled函数返回GL_TRUE(这是一个不为零的常量,一般被定义为1);否则返回GL_FALSE(这是一个常量,其值为零)

我们可以在程序里面写:

if( glIsEnabled(GL_BLEND) ) {

     // 当前开启了混合功能

} else {

     // 当前没有开启混合功能

}

再看其它类型的状态。

比如当前颜色,其值是四个浮点数,当前设置的直线宽度,其值是一个浮点数,当前的视口(Viewport,参见第五课),其值是四个整数。

为了取得整数类型、浮点数类型的状态,OpenGL提供了glGetBooleanv, glGetIntegerv, glGetFloatv, glGetDoublev这四个函数。调用函数时,指定需要得到的状态的名称,以及需要将状态值存放到的位置(一个指针),则这四个函数可以把状态值存放到指针所值位置。例如:

// 取得当前的直线宽度

GLfloat lw;

glGetFloatv(GL_LINE_WIDTH, &lw);

// 取得当前的颜色

GLfloat cc[4];

glGetFloatv(GL_CURRENT_COLOR, cc);

// 取得当前的视口

GLint viewport[4];

glGetIntegerv(GL_VIEWPORT, viewport);

说明:

1. 注意元素的个数。比如GL_LINE_WIDTH状态只有一个值,而GL_CURRENT_COLOR有四个值。应该小心的定义变量或者数组,避免下标越界。

2. 使用四个不同的函数,同一种状态也可以返回为不同类型的值。比如要得到当前的颜色,一般可以返回GLfloat类型或者GLdouble类型。代码如下:

GLfloat cc[4];

GLdouble dcc[4];

glGetFloatv(GL_CURRENT_COLOR, cc);

glGetDoublev(GL_CURRENT_COLOR, dcc);

glGetBooleanv, glGetIntegerv, glGetFloatv, glGetDoublev这四个函数可以得到OpenGL中多数的状态,但是还有一些状态不便用这四个函数来取得。比如光源的状态,因为可能有多个光源,所以不可能使用类似glGetFloatv(GL_LIGHT_POSITION, pos);这样的方法来得到光源位置。为了解决这个问题,OpenGL专门提供了glGetLight*系列函数,来取得光源的状态。

类似的,还有glGetMaterial*, glGetTexParameter*等,每个函数都有自己的适用范围。

设置OpenGL状态

呵呵,读者可能会有疑问。既然有getXXX这样的函数来取得OpenGL的状态,那么为什么没有setXXX这样的函数来设置OpenGL状态呢?

答案很简单,因为OpenGL已经提供了大量的函数来设置状态了:glColor*, glMaterial*, glEnable, glDisable, 等等,大多数OpenGL函数都是用来设置OpenGL状态的,因此不需要再设计一个setXXX函数来设置OpenGL状态。

从“状态机”的角度来看。状态机根据输入来修改自己的状态,而不是由外界直接修改自己的状态。所以不设置setXXX这样的函数,也是很合理的。

OpenGL工作流程

教程都放到第十三课了,但是我一直没有对“工作流程”这种东西做过说明。OpenGL是按照什么样的流程来进行工作的呢?下面的图片可以简要的说明一下:



声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,由于该书的旧版(第一版,1994年)已经流传于网络,我希望没有触及到版权问题。

因为图片中的文字是英语,这里还翻译一下。说明文字也夹杂在翻译之中了。

1. Vertex data: 顶点数据。比如我们指定的颜色、纹理坐标、法线向量、顶点坐标等,都属于顶点数据。

2. Pixel data: 像素数据。我们在绘制像素、指定纹理时都会用到像素数据。

3. Display list: 显示列表。可以把调用的OpenGL函数保存起来。(参见第八课)

4. Evaluators: 求值器。这个我们在前面的课程中没有提到,以后估计也不太会提到。利用求值器可以指定贝赛尔曲线或者贝赛尔曲面,但是实际上还是可以理解为指定顶点、指定纹理坐标、指定法线向量等。

5. Per-vertex operations and primitive assembly: 单一的顶点操作以及图元装配。首先对单一的顶点进行操作,比如变换(参见第五课)。然后把顶点装配为图元(图元就是OpenGL所能绘制的最简单的图形,比如点、线段、三角形、四边形、多边形等,参见第二课)

6. Pixel operations: 像素操作。例如把内存中的像素数据格式转化为图形硬件所支持的数据格式。对于纹理,可以替换其中的一部分像素,这也属于像素操作。

7. Rasterization: 光栅化。顶点数据和像素数据在这里交汇(可以想像成:顶点和纹理,一起组合成了具有纹理的三角形),形成完整的、可以显示的一整块(可能是点、线段、三角形、四边形,或者其它不规则图形),里面包含若干个像素。这一整块被称为fragment(片段)。

8. Per-fragment operations: 片段操作。包括各种片段测试(参见第十二课)。

9. Framebuffer: 帧缓冲。这是一块存储空间,显示设备从这里读取数据,然后显示到屏幕。

10. Texture assembly: 纹理装配,这里我也没怎么弄清楚:(,大概是说纹理的操作和像素操作是相关的吧。

说明:图片中实线表示正常的处理流程,虚线表示数据可以反方向读取,比如可以用glReadPixels从帧缓冲中读取像素数据(实际上是从帧缓冲读取数据,经过像素操作,把显示设备中的像素数据格式转化为内存中的像素数据格式,最终成为内存中的像素数据)。

小结

本课是枯燥的理论知识。

OpenGL是一个状态机,它维持自己的状态,并根据用户调用的函数来改变自己的状态。根据状态的不同,调用同样的函数也可能产生不同的效果。

可以通过一些函数来获取OpenGL当前的状态。常用的函数有:glIsEnabled, glGetBooleanv, glGetIntegerv, glGetFloatv, glGetDoublev。

OpenGL的工作流程,输入像素数据和顶点数据,两种数据分别操作后,通过光栅化,得到片段,再经过片段处理,最后绘制到帧缓冲区。绘制的结果也可以逆方向传送,最终转化为像素数据。

OpenGL入门学习[十四]

OpenGL从推出到现在,已经有相当长的一段时间了。其间,OpenGL不断的得到更新。到今天为止,正式的OpenGL已经有九个版本。(1.0, 1.1, 1.2, 1.2.1, 1.3, 1.4, 1.5, 2.0, 2.1)

每个OpenGL版本的推出,都增加了一些当时流行的或者迫切需要的新功能。同时,到现在为止,OpenGL是向下兼容的,就是说如果某个功能在一个低版本中存在,则在更高版本中也一定存在。这一特性也为我们编程提供了一点方便。

当前OpenGL的最新版本是OpenGL 2.1,但是并不是所有的计算机系统都有这样最新版本的OpenGL实现。举例来说,Windows系统如果没有安装显卡驱动,或者显卡驱动中没有附带OpenGL,则Windows系统默认提供一个软件实现的OpenGL,它没有使用硬件加速,因此速度可能较慢,版本也很低,仅支持1.1版本(听说Windows Vista默认提供的OpenGL支持到1.4版本,我也不太清楚)。nVidia和ATI这样的显卡巨头,其主流显卡基本上都提供了对OpenGL 2.1的支持。但一些旧型号的显卡因为性能不足等原因,只能支持到OpenGL 2.0或者OpenGL 1.5。Intel的集成显卡,很多都只提供了OpenGL 1.4(据说目前也有更高版本的了,但是我没有见到)。

OpenGL 2.0是一次比较大的改动,也因此升级了主版本号。可以认为OpenGL 2.0版本是一个分水岭,是否支持OpenGL 2.0版本,直接关系到运行OpenGL程序时的效果。如果要类比一下的话,我觉得OpenGL 1.5和OpenGL 2.0的差距,就像是DirectX 8.1和DirectX 9.0c的差距了。

检查自己的OpenGL版本

可以很容易的知道自己系统中的OpenGL版本,方法就是调用glGetString函数。 

const char* version = (const char*)glGetString(GL_VERSION);

printf("OpenGL 版本:%s\n", version);

glGetString(GL_VERSION);会返回一个表示版本的字符串,字符串的格式为X.X.X,就是三个整数,用小数点隔开,第一个数表示OpenGL主版本号,第二个数表示OpenGL次版本号,第三个数表示厂商发行代号。比如我在运行时得到的是"2.0.1",这表示我的OpenGL版本为2.0(主版本号为2,次版本号为0),是厂商的第一个发行版本。

通过sscanf函数,也可以把字符串分成三个整数,以便详细的进行判断。 

int main_version, sub_version, release_version;

const char* version = (const char*)glGetString(GL_VERSION);

sscanf(version, "%d.%d.%d", &main_version, &sub_version, &release_version);

printf("OpenGL 版本:%s\n", version);

printf("主版本号:%d\n", main_version);

printf("次版本号:%d\n", sub_version);

printf("发行版本号:%d\n", release_version);

glGetString还可以取得其它的字符串。

glGetString(GL_VENDOR); 返回OpenGL的提供厂商。

glGetString(GL_RENDERER); 返回执行OpenGL渲染的设备,通常就是显卡的名字。

glGetString(GL_EXTENSIONS); 返回所支持的所有扩展,每两个扩展之间用空格隔开。详细情况参见下面的关于“OpenGL扩展”的叙述。

版本简要历史

版本不同,提供功能的多少就不同。这里列出每个OpenGL版本推出时,所增加的主要功能。当然每个版本的修改并不只是下面的内容,读者如果需要知道更详细的情形,可以查阅OpenGL标准。
OpenGL 1.1

顶点数组。把所有的顶点数据(颜色、纹理坐标、顶点坐标等)都放到数组中,可以大大的减少诸如glColor*, glVertex*等函数的调用次数。虽然显示列表也可以减少这些函数的调用次数,但是显示列表中的数据是不可以修改的,顶点数组中的数据则可以修改。

纹理对象。把纹理作为对象来管理,同一时间OpenGL可以保存多个纹理(但只使用其中一个)。以前没有纹理对象时,OpenGL只能保存一个“当前纹理”。要使用其它纹理时,只能抛弃当前的纹理,重新载入。原来的方式非常影响效率。
OpenGL 1.2

三维纹理。以前的OpenGL只支持一维、二维纹理。

像素格式。新增加了GL_BGRA等原来没有的像素格式。允许压缩的像素格式,例如GL_UNSIGNED_SHORT_5_5_5_1格式,表示两个字节,存放RGBA数据,其中R, G, B各占5个二进制位,A占一个二进制位。

图像处理。新增了一个“图像处理子集”,提供一些图像处理的专用功能,例如卷积、计算柱状图等。这个子集虽然是标准规定,但是OpenGL实现时也可以选择不支持它。
OpenGL 1.2.1

没有加入任何新的功能。但是引入了“ARB扩展”的概念。详细情况参见下面的关于“OpenGL扩展”的叙述。
OpenGL 1.3

压缩纹理。在处理纹理时,使用压缩后的纹理而不是纹理本身,这样可以节省空间(节省显存)和传输带宽(节省从内存到显存的数据流量)

多重纹理。同时使用多个纹理。

多重采样。一种全屏抗锯齿技术,使用后可以让画面显示更加平滑,减轻锯齿现象。对于nvidia显卡,在设置时有一项“3D平滑处理设置”,实际上就是多重采样。通常可以选择2x, 4x,高性能的显卡也可以选择8x, 16x。其它显卡也几乎都有类似的设置选项,但是也有的显卡不支持多重采样,所以是0x。
OpenGL 1.4

深度纹理。可以把深度值像像素值一样放到纹理中,在绘制阴影时特别有用。

辅助颜色。顶点除了有颜色外还有辅助颜色。在使用光照时可以表现出更真实的效果。
OpenGL 1.5

缓冲对象。允许把数据(主要指顶点数据)交由OpenGL保存到较高性能的存储器中,提高绘制速度。比顶点数组有更多优势。顶点数组只是减少函数调用次数,缓冲对象不仅减少函数调用次数,还加快数据访问速度。

遮挡查询。可以计算一个物体有几个像素会被绘制到屏幕上。如果物体没有任何像素会被绘制,则不需要加载相关的数据(例如纹理数据)。
OpenGL 2.0

可编程着色。允许编写一小段代码来代替OpenGL原来的顶点操作/片段操作。这样提供了巨大的灵活性,可以实现各种各样的丰富的效果。

纹理大小不再必须是2的整数次方。

点块纹理。把纹理应用到一个点(大小可能不只一个像素)上,这样比绘制一个矩形可能效率更高。
OpenGL 2.1

可编程着色,编程语言由原来的1.0版本升级为1.2版本。

缓冲对象,原来仅允许存放顶点数据,现在也允许存放像素数据。

获得新版本的OpenGL

要获得新版本OpenGL,首先应该登陆你的显卡厂商网站,并查询相关的最新信息。根据情况,下载最新的驱动或者OpenGL软件包。

如果自己的显卡不支持高版本的OpenGL,或者自己的操作系统根本就没有提供OpenGL,怎么办呢?有一个被称为MESA的开源项目,用C语言编写了一个OpenGL实现,最新的mesa 7.0已经实现了OpenGL 2.1标准中所规定的各种功能。下载MESA的代码,然后编译,就可以得到一个最新版本的OpenGL了。呵呵,不要高兴的太早。MESA是软件实现的,就是说没有用到硬件加速,因此运行起来会较慢,尤其是使用新版本的OpenGL所规定的一些高级特性时,慢得几乎无法忍受。MESA不能让你用旧的显卡玩新的游戏(很可能慢得没法玩),但是如果你只是想学习或尝试一下新版本OpenGL的各种功能,MESA可以满足你的一部分要求。

OpenGL扩展

OpenGL版本的更新并不快。如果某种技术变得流行起来,但是OpenGL标准中又没有相关的规定对这种技术提供支持,那就只能通过扩展来实现了。

厂商在发行OpenGL时,除了遵照OpenGL标准,提供标准所规定的各种功能外,往往还提供其它一些额外的功能,这就是扩展。

扩展的存在,使得各种新的技术可以迅速的被应用到OpenGL中。比如“多重纹理”,它是在OpenGL 1.3中才被加入到标准中的,在OpenGL 1.3出现以前,很多OpenGL实现都通过扩展来支持“多重纹理”。这样,即使OpenGL版本不更新,只要增加新的扩展,也可以提供新的功能了。这也说明,即使OpenGL版本较低,也不一定不支持一些高版本OpenGL才提供的功能。实际上某些OpenGL 1.5的实现,也可能提供了最新的OpenGL 2.1版本所规定的大部分功能。

当然扩展也有缺点,那就是程序在运行的时候必须检查每个扩展功能是否被支持,导致编写程序代码复杂。

扩展的名字

每个OpenGL扩展,都必须向OpenGL的网站注册,确认后才能成为扩展。注册后的扩展有编号和名字。编号仅仅是一个序号,名字则与扩展所提供的功能相关。

名字用下划线分为三部分。举例来说,一个扩展的名字可能为:GL_NV_half_float,其意义如下:

第一部分为扩展的目标。比如GL表示这是一个OpenGL扩展。如果是WGL则表示这是一个针对Windows的OpenGL扩展,如果是GLX则表示这是一个针对linux的X Window系统的OpenGL扩展。

第二部分为提供扩展的厂商。比如NV表示这是nVidia公司所提供的扩展。相应的还有ATI, IBM, SGI, APPLE, MESA等。

剩下的部分就表示扩展所提供的内容了。比如half_float,表示半精度的浮点数,每个浮点数的精度只有单精度浮点数的一半,因此只需要两个字节就可以保存。这种扩展功能可以节省内存空间,也节省从内存到显卡的数据传输量,代价就是精确度有所降低。

EXT扩展和ARB扩展

最初的时候,每个厂商都提供自己的扩展。这样导致的结果就是,即使是提供相同的功能,不同的厂商却提供不同的扩展,这样在编写程序的时候,使用一种功能就需要依次检查每个可能支持这种功能的扩展,非常繁琐。

于是出现了EXT扩展和ARB扩展。

EXT扩展是由多个厂商共同协商后形成的扩展,在扩展名字中,“提供扩展的厂商”一栏将不再是具体的厂商名,而是EXT三个字母。比如GL_EXT_bgra,就是一个EXT扩展。

ARB扩展不仅是由多个厂商共同协商形成,还需要经过OpenGL体系结构审核委员会(即ARB)的确认。在扩展名字中,“提供扩展的厂商”一栏不再是具体的厂商名字,而是ARB三个字母。比如GL_ARB_imaging,就是一个ARB扩展。

通常,一种功能如果有多个厂商提出,则它成为EXT扩展。在以后的时间里,如果经过了ARB确认,则它成为ARB扩展。再往后,如果OpenGL的维护者认为这种功能需要加入到标准规定中,则它不再是扩展,而成为标准的一部分。

例如point_parameters,就是先有GL_EXT_point_parameters,再有GL_ARB_point_parameters,最后到OpenGL 1.4版本时,这个功能为标准规定必须提供的功能,不再是一个扩展。

在使用OpenGL所提供的功能时,应该按照标准功能、ARB扩展、EXT扩展、其它扩展这样的优先顺序。例如有ARB扩展支持这个功能时,就不使用EXT扩展。

在程序中,判断OpenGL是否支持某个扩展

前面已经说过,glGetString(GL_EXTENSIONS)会返回当前OpenGL所支持的所有扩展的名字,中间用空格分开,这就是我们判断是否支持某个扩展的依据。 

#include <string.h>

// 判断OpenGL是否支持某个指定的扩展

// 若支持,返回1。否则返回0。

int hasExtension(const char* name) {

    const char* extensions = (const char*)glGetString(GL_EXTENSIONS);

    const char* end = extensions + strlen(extensions);

    size_t name_length = strlen(name);

    while( extensions < end ) {

        size_t position = strchr(extensions, ' ') - extensions;

        if( position == name_length &&

                strncmp(extensions, name, position) == 0 )

            return 1;

         extensions += (position + 1);

     }

    return 0;

}

上面这段代码,判断了OpenGL是否支持指定的扩展,可以看到,判断时完全是靠字符串处理来实现的。循环检测,找到第一个空格,然后比较空格之前的字符串是否与指定的名字一致。若一致,说明扩展是被支持的;否则,继续比较。若所有内容都比较完,则说明扩展不被支持。

编写程序调用扩展的功能

扩展的函数、常量,在命名时与通常的OpenGL函数、常量有少许区别。那就是扩展的函数、常量将以厂商的名字作为后缀。

比如ARB扩展,所有ARB扩展的函数,函数名都以ARB结尾,常量名都以_ARB结尾。例如:

glGenBufferARB(函数)

GL_ARRAY_BUFFER_ARB(常量)

如果已经知道OpenGL支持某个扩展,则如何调用扩展中的函数?大致的思路就是利用函数指针。但是不幸的是,在不同的操作系统中,取得这些函数指针的方法各不相同。为了能够在各个操作系统中都能顺利的使用扩展,我向大家介绍一个小巧的工具:GLEE。

GLEE是一个开放源代码的项目,可以从网络上搜索并下载。其代码由两个文件组成,一个是GLee.c,一个是GLee.h。把两个文件都放到自己的源代码一起编译,运行的时候,GLee可以自动的判断所有扩展是否被支持,如果支持,GLEE会自动读取对应的函数,供我们调用。

我们自己编写代码时,需要首先包含GLee.h,然后才包含GL/glut.h(注意顺序不能调换),然后就可以方便的使用各种扩展功能了。

#include "GLee.h"

#include <GL/glut.h> // 注意顺序,GLee.h要在glut.h之前使用

GLEE也可以帮助我们判断OpenGL是否支持某个扩展,因此有了GLEE,前面那个判断是否支持扩展的函数就不太必要了。

示例代码

让我们用一段示例代码结束本课。

我们选择一个目前绝大多数显卡都支持的扩展GL_ARB_window_pos,来说明如何使用GLEE来调用OpenGL扩展功能。通常我们在绘制像素时,需要用glRasterPos*函数来指定绘制的位置。但是,glRasterPos*函数使用的不是屏幕坐标,例如指定(0, 0)不一定是左下角,这个坐标需要经过各种变换(参见第五课,变换),最后才得到屏幕上的窗口位置。

通过GL_ARB_window_pos扩展,我们可以直接用屏幕上的坐标来指定绘制的位置,不再需要经过变换,这样在很多场合会显得简单。 

#include "GLee.h"

#include <GL/glut.h>

void display(void) {

     glClear(GL_COLOR_BUFFER_BIT);

    if( GLEE_ARB_window_pos ) { // 如果支持GL_ARB_window_pos

                                 // 则使用glWindowPos2iARB函数,指定绘制位置

        printf("支持GL_ARB_window_pos\n");

        printf("使用glWindowPos函数\n");

         glWindowPos2iARB(100, 100);

     } else {                     // 如果不支持GL_ARB_window_pos

                                 // 则只能使用glRasterPos*系列函数

                                 // 先计算出一个经过变换后能够得到

                                 //    (100, 100)的坐标(x, y, z)

                                 // 然后调用glRasterPos3d(x, y, z);

         GLint viewport[4];

         GLdouble modelview[16], projection[16];

         GLdouble x, y, z;

        printf("不支持GL_ARB_window_pos\n");

        printf("使用glRasterPos函数\n");

         glGetIntegerv(GL_VIEWPORT, viewport);

         glGetDoublev(GL_MODELVIEW_MATRIX, modelview);

         glGetDoublev(GL_PROJECTION_MATRIX, projection);

         gluUnProject(100, 100, 0.5, modelview, projection, viewport,

             &x, &y, &z);

         glRasterPos3d(x, y, z);

     }

     { // 绘制一个5*5的像素块

         GLubyte pixels[5][5][4];

         // 把像素中的所有像素都设置为红色

        int i, j;

        for(i=0; i<5; ++i)

            for(j=0; j<5; ++j) {

                 pixels[i][j][0] = 255; // red

                 pixels[i][j][1] = 0;    // green

                 pixels[i][j][2] = 0;    // blue

                 pixels[i][j][3] = 255; // alpha

             }

         glDrawPixels(5, 5, GL_RGBA, GL_UNSIGNED_BYTE, pixels);

     }

     glutSwapBuffers();

}

int main(int argc, char* argv[]) {

     glutInit(&argc, argv);

     glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);

     glutInitWindowPosition(100, 100);

     glutInitWindowSize(512, 512);

     glutCreateWindow("OpenGL");

     glutDisplayFunc(&display);

     glutMainLoop();

}

可以看到,使用了扩展以后,代码会简单得多了。不支持GL_ARB_window_pos扩展时必须使用较多的代码才能实现的功能,使用GL_ARB_window_pos扩展后即可简单的解决。

如果把代码修改一下,不使用扩展而直接使用else里面的代码,可以发现运行效果是一样的。

工具软件

在课程的最后我还向大家介绍一个免费的工具软件,这就是OpenGL Extension Viewer(各大软件网站均有下载,请自己搜索之),目前较新的版本是3.0。

这个软件可以查看自己计算机系统的OpenGL信息。包括OpenGL版本、提供厂商、设备名称、所支持的扩展等。

软件可以查看的信息很详细,比如查看允许的最大纹理大小、最大光源数目等。

在查看扩展时,可以在最下面一栏输入扩展的名字,按下回车后即可连接到OpenGL官方网站,查找关于这个扩展的详细文档,非常不错。

可以根据电脑的配置情况,自动连接到对应的官方网站,方便下载最新驱动。(比如我是nVidia的显卡,则连接到nVidia的驱动下载页面)

可以进行OpenGL测试,看看运行起来性能如何。

可以给出总体报告,如果一些比较重要的功能不被支持,则会用粗体字标明。

软件还带有一个数据库,可以查询各厂商、各型号的显卡对OpenGL各种扩展的支持情况。

小结

本课介绍了OpenGL版本和OpenGL扩展。

OpenGL从诞生到现在,经历了1.0, 1.1, 1.2, 1.2.1, 1.3, 1.4, 1.5, 2.0, 2.1这些版本。

每个系统中的OpenGL版本可能不同。使用glGetString(GL_VERSION);可以查看当前的OpenGL版本。

新版本的OpenGL将兼容旧版本的OpenGL,同时提供更多的新特性和新功能。

OpenGL在实现时可以通过扩展,来提供额外的功能。

OpenGL扩展有厂家扩展、EXT扩展、ARB扩展。通常应该尽量使用标准功能,其次才是ARB扩展、EXT扩展、厂家扩展。

GLEE是一个可以免费使用的工具,使用它可以方便的判断当前的OpenGL是否支持某扩展,也可以方便的调用扩展。

OpenGL Extension Viewer是一个软件,可以检查系统所支持OpenGL的版本、支持的扩展、以及很多的详细信息。

OpenGL入门学习[十五]

这次讲的所有内容都装在一个立方体中,呵呵。

呵呵,绘制一个立方体,简单呀,我们学了第一课第二课,早就会了。

先别着急,立方体是很简单,但是这里只是拿立方体做一个例子,来说明OpenGL在绘制方法上的改进。

从原始一点的办法开始

一个立方体有六个面,每个面是一个正方形,好,绘制六个正方形就可以了。

glBegin(GL_QUADS);

     glVertex3f(...);

     glVertex3f(...);

     glVertex3f(...);

     glVertex3f(...);

     // ...

glEnd();

为了绘制六个正方形,我们为每个正方形指定四个顶点,最终我们需要指定6*4=24个顶点。但是我们知道,一个立方体其实总共只有八个顶点,要指定24次,就意味着每个顶点其实重复使用了三次,这样可不是好的现象。最起码,像上面这样重复烦琐的代码,是很容易出错的。稍有不慎,即使相同的顶点也可能被指定成不同的顶点了。

如果我们定义一个数组,把八个顶点都放到数组里,然后每次指定顶点都使用指针,而不是使用直接的数据,这样就避免了在指定顶点时考虑大量的数据,于是减少了代码出错的可能性。

// 将立方体的八个顶点保存到一个数组里面

static const GLfloat vertex_list[][3] = {

     -0.5f, -0.5f, -0.5f,

      0.5f, -0.5f, -0.5f,

     // ...

};

// 指定顶点时,用指针,而不用直接用具体的数据

glBegin(GL_QUADS);

     glVertex3fv(vertex_list[0]);

     glVertex3fv(vertex_list[2]);

     glVertex3fv(vertex_list[3]);

     glVertex3fv(vertex_list[1]);

     // ...

glEnd();

修改之后,虽然代码变长了,但是确实易读得多。很容易就看出第0, 2, 3, 1这四个顶点构成一个正方形。

稍稍观察就可以发现,我们使用了大量的glVertex3fv函数,其实每一句都只有其中的顶点序号不一样,因此我们可以再定义一个序号数组,把所有的序号也放进去。这样一来代码就更加简单了。

// 将立方体的八个顶点保存到一个数组里面

static const GLfloat vertex_list[][3] = {

     -0.5f, -0.5f, -0.5f,

      0.5f, -0.5f, -0.5f,

     -0.5f,   0.5f, -0.5f,

      0.5f,   0.5f, -0.5f,

     -0.5f, -0.5f,   0.5f,

      0.5f, -0.5f,   0.5f,

     -0.5f,   0.5f,   0.5f,

      0.5f,   0.5f,   0.5f,

};

// 将要使用的顶点的序号保存到一个数组里面

static const GLint index_list[][4] = {

     0, 2, 3, 1,

     0, 4, 6, 2,

     0, 1, 5, 4,

     4, 5, 7, 6,

     1, 3, 7, 5,

     2, 6, 7, 3,

};

int i, j;

// 绘制的时候代码很简单

glBegin(GL_QUADS);

for(i=0; i<6; ++i)          // 有六个面,循环六次

    for(j=0; j<4; ++j)      // 每个面有四个顶点,循环四次

         glVertex3fv(vertex_list[index_list[i][j]]);

glEnd();

这样,我们就得到一个比较成熟的绘制立方体的版本了。它的数据和程序代码基本上是分开的,所有的顶点放到一个数组中,使用顶点的序号放到另一个数组中,而利用这两个数组来绘制立方体的代码则很简单。

关于顶点的序号,下面这个图片可以帮助理解。



正对我们的面,按逆时针顺序,背对我们的面,则按顺时针顺序,这样就得到了上面那个index_list数组。

为什么要按照顺时针逆时针的规则呢?因为这样做可以保证无论从哪个角度观察,看到的都是“正面”,而不是背面。在计算光照时,正面和背面的处理可能是不同的,另外,剔除背面只绘制正面,可以提高程序的运行效率。(关于正面、背面,以及剔除,参见第三课,绘制几何图形的一些细节问题)

例如在绘制之前调用如下的代码:

glFrontFace(GL_CCW);

glCullFace(GL_BACK);

glEnable(GL_CULL_FACE);

glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

则绘制出来的图形就只有正面,并且只显示边线,不进行填充。

效果如图:



顶点数组

(提示:顶点数组是OpenGL 1.1所提供的功能)

前面的方法中,我们将数据和代码分离开,看起来只要八个顶点就可以绘制一个立方体了。但是实际上,循环还是执行了6*4=24次,也就是说虽然代码的结构清晰了不少,但是程序运行的效率,还是和最原始的那个方法一样。

减少函数的调用次数,是提高运行效率的方法之一。于是我们想到了显示列表。把绘制立方体的代码装到一个显示列表中,以后只要调用这个显示列表即可。

这样看起来很不错,但是显示列表有一个缺点,那就是一旦建立后不可再改。如果我们要绘制的不是立方体,而是一个能够走动的人物,因为人物走动时,四肢的位置不断变化,几乎没有办法把所有的内容装到一个显示列表中。必须每种动作都使用单独的显示列表,这样会导致大量的显示列表管理困难。

顶点数组是解决这个问题的一个方法。使用顶点数组的时候,也是像前面的方法一样,用一个数组保存所有的顶点,用一个数组保存顶点的序号。但最后绘制的时候,不是编写循环语句逐个的指定顶点了,而是通知OpenGL,“保存顶点的数组”和“保存顶点序号的数组”所在的位置,由OpenGL自动的找到顶点,并进行绘制。

下面的代码说明了顶点数组是如何使用的:

glEnableClientState(GL_VERTEX_ARRAY);

glVertexPointer(3, GL_FLOAT, 0, vertex_list);

glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);

其中:

glEnableClientState(GL_VERTEX_ARRAY); 表示启用顶点数组。

glVertexPointer(3, GL_FLOAT, 0, vertex_list); 指定顶点数组的位置,3表示每个顶点由三个量构成(x, y, z),GL_FLOAT表示每个量都是一个GLfloat类型的值。第三个参数0,参见后面介绍“stride参数”。最后的vertex_list指明了数组实际的位置。

glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list); 根据序号数组中的序号,查找到相应的顶点,并完成绘制。GL_QUADS表示绘制的是四边形,24表示总共有24个顶点,GL_UNSIGNED_INT表示序号数组内每个序号都是一个GLuint类型的值,index_list指明了序号数组实际的位置。

上面三行代码代替了原来的循环。可以看到,原来的glBegin/glEnd不再需要了,也不需要调用glVertex*系列函数来指定顶点,因此可以明显的减少函数调用次数。另外,数组中的内容可以随时修改,比显示列表更加灵活。 

详细一点的说明。

顶点数组实际上是多个数组,顶点坐标、纹理坐标、法线向量、顶点颜色等等,顶点的每一个属性都可以指定一个数组,然后用统一的序号来进行访问。比如序号3,就表示取得颜色数组的第3个元素作为颜色、取得纹理坐标数组的第3个元素作为纹理坐标、取得法线向量数组的第3个元素作为法线向量、取得顶点坐标数组的第3个元素作为顶点坐标。把所有的数据综合起来,最终得到一个顶点。

可以用glEnableClientState/glDisableClientState单独的开启和关闭每一种数组。

glEnableClientState(GL_VERTEX_ARRAY);

glEnableClientState(GL_COLOR_ARRAY);

glEnableClientState(GL_NORMAL_ARRAY);

glEnableClientState(GL_TEXTURE_COORD_ARRAY);

用以下的函数来指定数组的位置:

glVertexPointer

glColorPointer

glNormalPointer

glTexCoordPointer 

为什么不使用原来的glEnable/glDisable函数,而要专门的规定一个glEnableClientState/glDisableClientState函数呢?这跟OpenGL的工作机制有关。OpenGL在设计时,认为可以将整个OpenGL系统分为两部分,一部分是客户端,它负责发送OpenGL命令。一部分是服务端,它负责接收OpenGL命令并执行相应的操作。对于个人计算机来说,可以将CPU、内存等硬件,以及用户编写的OpenGL程序看做客户端,而将OpenGL驱动程序、显示设备等看做服务端。

通常,所有的状态都是保存在服务端的,便于OpenGL使用。例如,是否启用了纹理,服务端在绘制时经常需要知道这个状态,而我们编写的客户端OpenGL程序只在很少的时候需要知道这个状态。所以将这个状态放在服务端是比较有利的。

但顶点数组的状态则不同。我们指定顶点,实际上就是把顶点数据从客户端发送到服务端。是否启用顶点数组,只是控制发送顶点数据的方式而已。服务端只管接收顶点数据,而不必管顶点数据到底是用哪种方式指定的(可以直接使用glBegin/glEnd/glVertex*,也可以使用顶点数组)。所以,服务端不需要知道顶点数组是否开启。因此,顶点数组的状态放在客户端是比较合理的。

为了表示服务端状态和客户端状态的区别,服务端的状态用glEnable/glDisable,客户端的状态则用glEnableClientState/glDisableClientState。

stride参数。

顶点数组并不要求所有的数据都连续存放。如果数据没有连续存放,则指定数据之间的间隔即可。

例如:我们使用一个struct来存放顶点中的数据。注意每个顶点除了坐标外,还有额外的数据(这里是一个int类型的值)。

typedef struct __point__ {

     GLfloat position[3];

    int      id;

} Point;

Point vertex_list[] = {

     -0.5f, -0.5f, -0.5f, 1,

      0.5f, -0.5f, -0.5f, 2,

     -0.5f,   0.5f, -0.5f, 3,

      0.5f,   0.5f, -0.5f, 4,

     -0.5f, -0.5f,   0.5f, 5,

      0.5f, -0.5f,   0.5f, 6,

     -0.5f,   0.5f,   0.5f, 7,

      0.5f,   0.5f,   0.5f, 8,

};

static GLint index_list[][4] = {

     0, 2, 3, 1,

     0, 4, 6, 2,

     0, 1, 5, 4,

     4, 5, 7, 6,

     1, 3, 7, 5,

     2, 6, 7, 3,

};

glEnableClientState(GL_VERTEX_ARRAY);

glVertexPointer(3, GL_FLOAT, sizeof(Point), vertex_list);

glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);

注意最后三行代码,可以看到,几乎所有的地方都和原来一样,只在glVertexPointer函数的第三个参数有所不同。这个参数就是stride,它表示“从一个数据的开始到下一个数据的开始,所相隔的字节数”。这里设置为sizeof(Point)就刚刚好。如果设置为0,则表示数据是紧密排列的,对于3个GLfloat的情况,数据紧密排列时stride实际上为3*4=12。

混合数组。如果需要同时使用颜色数组、顶点坐标数组、纹理坐标数组、等等,有一种方式是把所有的数据都混合起来,指定到同一个数组中。这就是混合数组。

GLfloat arr_c3f_v3f[] = {

     1, 0, 0, 0, 1, 0,

     0, 1, 0, 1, 0, 0,

     0, 0, 1, -1, 0, 0,

};

GLuint index_list[] = {0, 1, 2};

glInterleavedArrays(GL_C3F_V3F, 0, arr_c3f_v3f);

glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, index_list);

glInterleavedArrays,可以设置混合数组。这个函数会自动调用glVertexPointer, glColorPointer等函数,并且自动的开启或禁用相关的数组。

函数的第一个参数表示了混合数组的类型。例如GL_C3F_V3F表示:三个浮点数作为颜色、三个浮点数作为顶点坐标。也可以有其它的格式,比如GL_V2F, GL_V3F, GL_C4UB_V2F, GL_C4UB_V3F, GL_C3F_V3F, GL_N3F_V3F, GL_C4F_N3F_V3F, GL_T2F_V3F, GL_T4F_V4F, GL_T2F_C4UB_V3F, GL_T2F_C3F_V3F, GL_T2F_N3F_V3F, GL_T2F_C4F_N3F_V3F, GL_T4F_C4F_N3F_V4F等等。其中T表示纹理坐标,C表示颜色,N表示法线向量,V表示顶点坐标。

再来说说顶点数组与显示列表的区别。两者都可以明显的减少函数的调用次数,但是还是各有优点的。

对于顶点数组,顶点数据是存放在内存中的,也就是存放在客户端。每次绘制的时候,需要把所有的顶点数据从客户端(内存)发送到服务端(显示设备),然后进行处理。对于显示列表,顶点数据是放在显示列表中的,显示列表本身又是存放在服务器端的,所以不会重复的发送数据。

对于顶点数组,因为顶点数据放在内存中,所以可以随时修改,每次绘制的时候都会把当前数组中的内容作为顶点数据发送并进行绘制。对于显示列表,数据已经存放到服务器段,并且无法取出,所以无法修改。

也就是说,显示列表可以避免数据的重复发送,效率会较高;顶点数组虽然会重复的发送数据,但由于数据可以随时修改,灵活性较好。

顶点缓冲区对象

(提示:顶点缓冲区对象是OpenGL 1.5所提供的功能,但它在成为标准前是一个ARB扩展,可以通过GL_ARB_vertex_buffer_object扩展来使用这项功能。前面已经讲过,ARB扩展的函数名称以字母ARB结尾,常量名称以字母_ARB结尾,而标准函数、常量则去掉了ARB字样。很多的OpenGL实现同时支持vertex buffer object的标准版本和ARB扩展版本。我们这里以ARB扩展来讲述,因为目前绝大多数个人计算机都支持ARB扩展版本,但少数显卡仅支持OpenGL 1.4,无法使用标准版本。)

前面说到顶点数组和显示列表在绘制立方体时各有优劣,那么有没有办法将它们的优点集中到一起,并且尽可能的减少缺点呢?顶点缓冲区对象就是为了解决这个问题而诞生的。它数据存放在服务端,同时也允许客户端灵活的修改,兼顾了运行效率和灵活性。

顶点缓冲区对象跟纹理对象有很多相似之处。首先,分配一个缓冲区对象编号,然后,为对应编号的缓冲区对象指定数据,以后可以随时修改其中的数据。下面的表格可以帮助类比理解。 

                                   纹理对象          顶点缓冲区对象

分配编号                           glGenTextures     glGenBuffersARB

绑定(指定为当前所使用的对象)     glBindTexture     glBindBufferARB

指定数据                           glTexImage*       glBufferDataARB

修改数据                           glTexSubImage*    glBufferSubDataARB

顶点数据和序号各自使用不同的缓冲区。具体的说,就是顶点数据放在GL_ARRAY_BUFFER_ARB类型的缓冲区中,序号数据放在GL_ELEMENT_ARRAY_BUFFER_ARB类型的缓冲区中。

具体的情况可以用下面的代码来说明:

static GLuint vertex_buffer;

static GLuint index_buffer;

// 分配一个缓冲区,并将顶点数据指定到其中

glGenBuffersARB(1, &vertex_buffer);

glBindBufferARB(GL_ARRAY_BUFFER_ARB, vertex_buffer);

glBufferDataARB(GL_ARRAY_BUFFER_ARB,

    sizeof(vertex_list), vertex_list, GL_STATIC_DRAW_ARB);

// 分配一个缓冲区,并将序号数据指定到其中

glGenBuffersARB(1, &index_buffer);

glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_buffer);

glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB,

    sizeof(index_list), index_list, GL_STATIC_DRAW_ARB);

在指定缓冲区数据时,最后一个参数是关于性能的提示。一共有STREAM_DRAW, STREAM_READ, STREAM_COPY, STATIC_DRAW, STATIC_READ, STATIC_COPY, DYNAMIC_DRAW, DYNAMIC_READ, DYNAMIC_COPY这九种。每一种都表示了使用频率和用途,OpenGL会根据这些提示进行一定程度的性能优化。

(提示仅仅是提示,不是硬性规定。也就是说,即使使用了STREAM_DRAW,告诉OpenGL这段缓冲区数据一旦指定,以后不会修改,但实际上以后仍可修改,不过修改时可能有较大的性能代价) 

当使用glBindBufferARB后,各种使用指针为参数的OpenGL函数,行为会发生变化。

以glColor3fv为例,通常,这个函数接受一个指针作为参数,从指针所指的位置取出连续的三个浮点数,作为当前的颜色。

但使用glBindBufferARB后,这个函数不再从指针所指的位置取数据。函数会先把指针转化为整数,假设转化后结果为k,则会从当前缓冲区的第k个字节开始取数据。特别一点,如果我们写glColor3fv(NULL);因为NULL转化为整数后通常是零,所以从缓冲区的第0个字节开始取数据,也就是从缓冲区最开始的位置取数据。

这样一来,原来写的

glVertexPointer(3, GL_FLOAT, 0, vertex_list);

glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);

在使用缓冲区对象后,就变成了

glVertexPointer(3, GL_FLOAT, 0, NULL);

glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, NULL);

以下是完整的使用了顶点缓冲区对象的代码:

static GLfloat vertex_list[][3] = {

     -0.5f, -0.5f, -0.5f,

      0.5f, -0.5f, -0.5f,

     -0.5f,   0.5f, -0.5f,

      0.5f,   0.5f, -0.5f,

     -0.5f, -0.5f,   0.5f,

      0.5f, -0.5f,   0.5f,

     -0.5f,   0.5f,   0.5f,

      0.5f,   0.5f,   0.5f,

};

static GLint index_list[][4] = {

     0, 2, 3, 1,

     0, 4, 6, 2,

     0, 1, 5, 4,

     4, 5, 7, 6,

     1, 3, 7, 5,

     2, 6, 7, 3,

};

if( GLEE_ARB_vertex_buffer_object ) {

     // 如果支持顶点缓冲区对象

    static int isFirstCall = 1;

    static GLuint vertex_buffer;

    static GLuint index_buffer;

    if( isFirstCall ) {

         // 第一次调用时,初始化缓冲区

         isFirstCall = 0;

         // 分配一个缓冲区,并将顶点数据指定到其中

         glGenBuffersARB(1, &vertex_buffer);

         glBindBufferARB(GL_ARRAY_BUFFER_ARB, vertex_buffer);

         glBufferDataARB(GL_ARRAY_BUFFER_ARB,

            sizeof(vertex_list), vertex_list, GL_STATIC_DRAW_ARB);

         // 分配一个缓冲区,并将序号数据指定到其中

         glGenBuffersARB(1, &index_buffer);

         glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_buffer);

         glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB,

            sizeof(index_list), index_list, GL_STATIC_DRAW_ARB);

     }

     glBindBufferARB(GL_ARRAY_BUFFER_ARB, vertex_buffer);

     glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_buffer);

     // 实际使用时与顶点数组非常相似,只是在指定数组时不再指定实际的数组,改为指定NULL即可

     glEnableClientState(GL_VERTEX_ARRAY);

     glVertexPointer(3, GL_FLOAT, 0, NULL);

     glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, NULL);

} else {

     // 不支持顶点缓冲区对象

     // 使用顶点数组

     glEnableClientState(GL_VERTEX_ARRAY);

     glVertexPointer(3, GL_FLOAT, 0, vertex_list);

     glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);

}

可以分配多个缓冲区对象,顶点坐标、颜色、纹理坐标等数据,可以各自单独使用一个缓冲区。

每个缓冲区可以有不同的性能提示,比如在绘制一个运动的人物时,顶点坐标数据经常变化,但法线向量、纹理坐标等则不会变化,可以给予不同的性能提示,以提高性能。

小结

本课从绘制一个立方体出发,描述了OpenGL在各个版本中对于绘制的处理。

绘制物体的时候,应该将数据单独存放,尽量不要到处写类似glVertex3f(1.0f, 0.0f, 1.0f)这样的代码。将顶点坐标、顶点序号都存放到单独的数组中,可以让绘制的代码变得简单。

可以把绘制物体的所有命令装到一个显示列表中,这样可以避免重复的数据传送。但是因为显示列表一旦建立,就无法修改,所以灵活性很差。

OpenGL 1.1版本,提供了顶点数组。它可以指定数据的位置、顶点序号的位置,从而有效的减少函数调用次数,达到提高效率的目的。但是它没有避免重复的数据传送,所以效率还有待进一步提高。

OpenGL 1.5版本,提供了顶点缓冲区对象。它综合了显示列表和顶点数组的优点,同时兼顾运行效率和灵活性,是绘制物体的一个好选择。如果系统不支持OpenGL 1.5,也可以检查是否支持扩展GL_ARB_vertex_buffer_object。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: