任何复杂的三维模型都是由基本的几何图元:点、线段和多边形组成的,有了这些图元,就可以建立比较复杂的模型。因此这部分内容是学习OpenGL编程的基础。
一、基本图元的描述及定义
OpenGL图元是抽象的几何概念,不是真实世界中的物体,因此须用相关的 数学模型来描述。所有的图元都是由一系列有顺序的顶点集合来描述的。OpenGL中绘制几何图元,必须使用glBegain()和glEnd()这一对函数,传递给glBegain()函数的参数唯一确定了要绘制何种几何图元,同时,在该函数对中给出了几何图元的定义,函数glEnd()标志顶点列表的结束。例如,下面的代码绘制了一个多边形:
glBegin(GL_POLYGON); glVertex2f(0.0,0.0); glVertex2f(0.0,3.0); glVertex2f(3.0,3.0); glVertex2f(4.0,1.5); glVertex2f(3.0,0.0); glEnd(); |
函数glBegin(GLenum mode)标志描述一个几何图元的顶点列表的开始,其参数mode表示几何图元的描述类型,具体类型见表一:
类型 |
说明 |
GL_POINTS |
单个顶点集 |
GL_LINES |
多组双顶点线段 |
GL_POLYGON |
单个简单填充凸多边形 |
GL_TRAINGLES |
多组独立填充三角形 |
GL_QUADS |
多组独立填充四边形 |
GL_LINE_STRIP |
不闭合折线 |
GL_LINE_LOOP |
闭合折线 |
GL_TRAINGLE_STRIP |
线型连续填充三角形串 |
GL_TRAINGLE_FAN |
扇形连续填充三角形串 |
GL_QUAD_STRIP |
连续填充四边形串 |
表一、几何图元类型说明
部分几何图元的示意图:
在glBegin()和glEnd()之间最重要的信息就是由函数glVertex*()定义的顶点,必要时也可为每个顶点指定颜色(只对当前点或后续点有效)、法向、纹理坐标或其他,即调用相关的函数:
函数 |
函数意义 |
glColor*() |
设置当前颜色 |
glIndex*() |
设置当前颜色表 |
glNormal*() |
设置法向坐标 |
glEvalCoord*() |
产生坐标 |
glCallList(),glCallLists() |
显示列表 |
glTexCoord*() |
设置纹理坐标 |
glEdgeFlag*() |
控制边界绘制 |
glMaterial*() |
设置材质 |
表二、在glBegin()和glEnd()之间可调用的函数
需要指出的是:OpenGL所定义的点、线、多边形等图元与一般数学定义不太一样,存在一定的差别。一种差别源于基于计算机计算的限制。OpenGL中所有浮点计算精度有限,故点、线 、多边形的坐标值存在一定的误差。另一种差别源于位图显示的限制。以这种方式显示图形,最小的显示图元是一个象素,尽管每个象素宽度很小,但它们仍然比数学上所定义的点或线宽要大得多。当用OpenGL进行计算时,虽然是用一系列浮点值定义点串,但每个点仍然是用单个象素显示,只是近似拟合。
二、点(Point)
用浮点值表示的点称为顶点(Vertex)。所有顶点在OpenGL内部计算时都使用三维坐标(x,y,z)来处理,用二维坐标(x,y)定义的点在OpenGL中默认z值为0。顶点坐标也可以用齐次坐标(x,y,z,w)来表示,如果w不为0.0,这些齐次坐标表示的顶点即为三维空间点(x/w,y/w,z/w),一般来说,w缺省为1.0。
可以用glVertex{234}{sifd}[V]( TYPE cords)函数来定义一个顶点。例如:
glVertex2f(2.0f,3.0f);//二维坐标定义顶点; |
OpenGL中定义的点可以有不同的尺寸,其函数形式为:
void glPointSize(GLfloat size); |
参数size设置点的宽度(以象素为单位),必须大于0.0,缺省时为1.0。
三、线(Line)
在OpenGL中,线代表线段(Line Segment),它由一系列顶点顺次连结而成。具体的讲,线有独立线段、条带、封闭条带三种,如图二所示:
OpenGL能指定线的宽度并绘制不同的虚点线,如点线、虚线等。相应的函数形式如下:
1、void glLineWidth(GLfloat width);
设置线宽(以象素为单位)。参数width必须大于0.0,缺省时为1.0。
2、void glLineStipple(GLint factor,GLushort pattern);
设置当前线为虚点模式。参数pattern是一系列的16位二进制数(0或1),它重复地赋给所指定的线,从低位开始,每一个二进制位代表一个象素, 1表示用当前颜色绘制一个象素(或比例因子指定的个数),0表示当前不绘制,只移动一个象素位(或比例因子指定的个数)。参数factor是个比例因子,它用来拉伸pattern中的元素,即重复绘制1或移动0,比如,factor为2,则碰到1时就连续绘制2次,碰到0时连续移动2个单元。factor的大小范围限制在1到255之间。
在绘制虚点线之前必须先启动虚点模式,即调用函数glEnable(GL_LINE_STIPPLE);结束时,调用glDisable(GL_LINE_STIPPLE)关闭。下面代码绘制了一个点线:
void line2i(GLint x1,GLint y1,GLint x2,GLint y2) { glBegin(GL_LINES); glVertex2f(x1,y1); glVertex2f(x2,y2); glEnd(); } glLineStipple (1, 0x1C47); /* 虚点线 */ glEnable(GL_LINE_STIPPLE); glColor3f(0.0,1.0,0.0); line2i (450 , 250 , 600 , 250 ); |
三、多边形(Polygon)
(一)凸、凹多边形。
OpenGL定义的多边形是由一系列线段依次连结而成的封闭区域,多边形可以是平面多边形,即所有顶点在一个平面上,也可以是空间多边形。OpenGL规定多边形中的线段不能交叉,区域内不能有空洞,也即多边形必须是凸多边形(指多边形任意非相邻的两点的连线位于多边形的内部),不能是凹多边形,否则不能被OpenGL函数接受。凸多边形和凹多边形见图三。
(二)边界标志问题。
实际应用中,往往需要绘制一些凹多边形,通常解决的办法是对它们进行分割,用多个三角形来替代。显然,绘制这些三角形时,有些边不应该进行绘制,否则,多边形内部就会出现多余的线框。OpenGL提供的解决办法是通过设置边标志命令glEdgeFlag()来控制某些边产生绘制,而另外一些边不产生绘制,这也称为边界标志线或非边界线。这个命令的定义如下;
void glEdgeFlag(GLboolean flag); void glEdgeFlag(PGLboolean pflag); |
(三)多边形绘制模式。
多边形的绘制模式包含有:全填充式、轮廓点式、轮廓线式、图案填充式及指定正反面等。下面分别介绍相应的OpenGL函数形式。
1)多边形模式设置。其函数为:
void glPolygonMode(GLenum face,GLenum mode); |
参数face为GL_FRONT、GL_BACK或GL_FRONT_AND_BACK;参数mode为GL_POINT、GL_LINE或GL_FILL,分别表示绘制轮廓点式多边形、轮廓线式多边形或全填充式多边形。在OpenGL中,多边形分为正面和反面,对这两个面都可以进行操作,在缺省状况下,OpenGL对多边形正反面是以相同的方式绘制的,要改变绘制状态,必须调用PolygonMode()函数,
2)设置图案填充式多边形。其函数为:
void glPolygonStipple(const GLubyte *mask); |
参数mask是一个指向32x32位图的指针。与虚点线绘制的道理一样,某位为1时绘制,为0时什么也不绘。注意,在调用这个函数前,必须先启动glEnable(GL_POLYGON_STIPPLE);不用时用glDisable(GL_POLYGON_STIPPLE)关闭。下面举出一个多边形扩展绘制实例:
void CALLBACK display(void) { /* 填充模式定义 (32x32) */ GLubyte pattern[]= { 0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x1f, 0xf8, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0xff, 0xff, 0x00, 0x01, 0xff, 0xff, 0x80, 0x03, 0xff, 0xff, 0xc0, 0x07, 0xff, 0xff, 0xe0, 0x0f, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xfc, 0x7f, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xfc, 0x1f, 0xff, 0xff, 0xf8, 0x0f, 0xff, 0xff, 0xf0, 0x07, 0xff, 0xff, 0xe0, 0x03, 0xff, 0xff, 0xc0, 0x01, 0xff, 0xff, 0x80, 0x00, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0x1f, 0xf8, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x01, 0x80, 0x00 }; glClear (GL_COLOR_BUFFER_BIT); /* 绘制一个指定图案填充的三角形 */ glColor3f(0.9,0.86,0.4); glPolygonStipple (pattern); glBegin(GL_TRIANGLES); glVertex2i(310,310); glVertex2i(220,80); glVertex2i(405,80); glEnd(); glDisable (GL_POLYGON_STIPPLE); glFlush (); } |
(三)指定多边形的正反面。
其函数为:
void glFrontFace(GLenum mode); |
在正常情况下,OpenGL中的多边形的正面和反面是由绘制的多边形的顶点顺序决定的,逆时针绘制的面是多边形的正面,但是,在OpenGL中使用该函数可以自定义多边形的正面。该函数的参数mode指定了正面的方向。它可以是CL_CCW和CL_CW,分别指定逆时针和顺时针方向为多边形的正方向。
四、法向量的计算及指定
法向量是几何图元的重要属性之一。几何对象的法向量是垂直与曲面切面的单位向量,它定义了几何对象的空间方向,特别定义了它相对于光源的方向,决定了在该点上可接受多少光照。
OpenGL本身没有提供计算法向量的函数(计算法向量的任务由程序员自己去完成),但它提供了赋予当前顶点法向的函数。
(一)平面法向的计算方法。
在一个平面内,有两条相交的线段,假设其中一条为矢量W,另一条为矢量V,平面法向为N,则平面法向就等于两个矢量的叉积(遵循右手定则),即N=WxV。例如:一个三角形平面三个顶点分别为P0、P1、P2,相应两个向量为W、V,则三角平面法向的计算方式如下列代码所示:
void getNormal(GLfloat gx[3],GLfloat gy[3], GLfloat gz[3],GLfloat *ddnv) { GLfloat w0,w1,w2,v0,v1,v2,nr,nx,ny,nz; w0=gx[0]-gx[1]; w1=gy[0]-gy[1];w2=gz[0]-gz[1]; v0=gx[2]-gx[1]; v1=gy[2]-gy[1];v2=gz[2]-gz[1]; nx=(w1*v2-w2*v1);ny=(w2*v0-w0*v2);nz=(w0*v1-w1*v0); nr=sqrt(nx*nx+ny*ny+nz*nz); //向量单位化。 ddnv[0]=nx/nr; ddnv[1]=ny/nr;ddnv[2]=nz/nr; } |
以上函数的输出参数为指针ddnv,它指向法向的三个分量,并且程序中已经将法向单位化(或归一化)了。
(二)曲面法向量的计算。
对于曲面各顶点的法向计算有很多种,如根据函数表达式求偏导的方法等。但是,在大多数情况,OpenGL中的多边形并不是由曲面方程建立起来的,而是由模型数组构成,这时候求取法向量的办法是将曲面细分成多个小多边形,然后选取小多边形上相邻的三个点v1、v2、v3(当然三个点不能在同一直线上),按照平面法向量的求取方法就可以了。
(三)法向量的定义。
OpenGL法向量定义函数为:
void glNormal3{bsifd}(TYPE nx,TYPE ny,TYPE nz); void glNormal3{bsifd}v(const TYPE *v); |
非向量形式定义法向采用第一种方式,即在函数中分别给出法向三个分量值nx、ny和nz;向量形式定义采用第二种,即将v设置为一个指向拥有三个元素的指针,例如v[3]={nx,ny,nz}。
五、显示列表
(一)定义显示列表。
前面所举出的例子都是瞬时给出函数命令,OpenGL瞬时执行相应的命令,这种绘图方式叫做立即或瞬时方式(immediate mode)。OpenGL显示列表(Display List)是由一组预先存储起来的留待以后调用的OpenGL函数语句组成的,当调用显示列表时就依次执行表中所列出的函数语句。显示列表可以用在以下场合:
1)矩阵操作
大部分矩阵操作需要OpenGL计算逆矩阵,矩阵及其逆矩阵都可以保存在显示列表中。
2)光栅位图和图像
程序定义的光栅数据不一定是适合硬件处理的理想格式。当编译组织一个显示列表时,OpenGL可能把数据转换成硬件能够接受的数据,这可以有效地提高画位图的速度。
3)光、材质和光照模型
当用一个比较复杂的光照环境绘制场景时,因为材质计算可能比较慢。若把材质定义放在显示列表中,则每次改换材质时就不必重新计算了,因此能更快地绘制光照场景。
4)纹理
因为硬件的纹理格式可能与OpenGL格式不一致,若把纹理定义放在显示列表中,则在编译显示列表时就能对格式进行转换,而不是在执行中进行,这样就能大大提高效率。
5)多边形的图案填充模式,即可将定义的图案放在显示列表中。
OpenGL提供类似于绘制图元的结构即类似于glBegin()与glEnd()的形式创建显示列表,其相应的函数为:
void glNewList(GLuint list,GLenum mode); void glEndList(void); |
glNewList()函数说明一个显示列表的开始,其后的OpenGL函数存入显示列表中,直至调用结束表的函数glEndList(void)。glNewList()函数中的参数list是一个正整数,它标志唯一的显示列表;参数mode的可能值有GL_COMPILE和GL_COMPILE_AND_EXECUTE;若要使列表中函数语句只存入而不执行,则用GL_COMPILE;若要使列表中的函数语句存入表中且按瞬时方式执行一次,则用GL_COMPILE_AND_EXECUTE。
注意:并不是所有的OpenGL函数都可以在显示列表中存储且通过显示列表执行。一般来说,用于传递参数或返回数值的函数语句不能存入显示列表,因为这张表有可能在参数的作用域之外被调用;如果在定义显示列表时调用了这样的函数,则它们将按瞬时方式执行并且不保存在显示列表中,有时在调用执行显示列表函数时会产生错误。以下列出的是不能存入显示列表的OpenGL函数:
glDeleteLists() glIsEnable() glFeedbackBuffer() glIsList() glFinish() glPixelStore() glGenLists() glRenderMode() glGet*() glSelectBuffer() |
在建立显示列表以后就可以调用执行显示列表的函数来执行它,并且允许在程序中多次执行同一显示列表,同时也可以与其它函数的瞬时方式混合使用。显示列表执行的函数形式如下:
void glCallList(GLuint list);
参数list指定被执行的显示列表。显示列表中的函数语句按它们被存放的顺序依次执行;若list没有定义,则不会产生任何事情。
(二)管理显示列表
在实际应用中,一般调用函数glGenList()来创建多个显示列表,这样可以避免意外删除,产生一个没有用过的显示列表。此外,在管理显示列表的过程中,还可调用函数glDeleteLists()来删除一个或一个范围内的显示列表。
1)GLuint glGenList(GLsizei range)
该函数分配range个相邻的未被占用的显示列表索引。这个函数返回的是一个正整数索引值,它是一组连续空索引的第一个值。返回的索引都标志为空且已被占用,以后再调用这个函数时不再返回这些索引。若申请索引的指定数目不能满足或range为0则函数返回0。
2)GLboolean glIsList(GLuint list)
该函数询问显示列表是否已被占用的情况,若索引list已被占用,则函数返回TURE;反之,返回FAULSE。
3)void glDeleteLists(GLuint list,GLsizei range)
该函数删除一组连续的显示列表,即从参数list所指示的显示列表开始,删除range个显示列表,并且删除后的这些索引重新有效。
(三)多级显示列表
多级显示列表的建立就是在一个显示列表中调用另一个显示列表,也就是说,在函数glNewList()与glEndList()之间调用glCallList()。多级显示列表对于构造由多个元件组成的物体十分有用,尤其是某些元件需要重复使用的情况。但为了避免无穷递归,显示列表的嵌套深度最大为64(也许更高些,这依赖于不同的OpenGL实现),当然也可调用函数glGetIntegerv()来获得这个最大嵌套深度值。OpenGL也允许用一个显示列表包含几个低级的显示列表来模拟建立一个可编辑的显示列表。
下面的一段代码使用了列表嵌套来显示一个三角形:
glNewList(1,GL_COMPILE); glVertex3fv(v1); glEndList(); glNewList(2,GL_COMPILE); glVertex3fv(v2); glEndList();
glNewList(3,GL_COMPILE); glVertex3fv(v3); glEndList();
glNewList(4,GL_COMPILE); glBegin(GL_POLYGON); glCallList(1); glCallList(2); glCallList(3); glEnd(); glEndList(); |