opengl入门4

来源:互联网 发布:盘古建站 源码 编辑:程序博客网 时间:2024/06/08 17:41

OpenGL入门学习[八]


今天介绍关于OpenGL显示列表的知识。本课内容并不多,但需要一些理解能力。在学习时,可以将显示列表与C语言的“函数”进行类比,加深体会。

我们已经知道,使用OpenGL其实只要调用一系列的OpenGL函数就可以了。然而,这种方式在一些时候可能导致问题。比如某个画面中,使用了数千个多边形来表现一个比较真实的人物,OpenGL为了产生这数千个多边形,就需要不停的调用glVertex*函数,每一个多边形将至少调用三次(因为多边形至少有三个顶点),于是绘制一个比较真实的人物就需要调用上万次的glVertex*函数。更糟糕的是,如果我们需要每秒钟绘制60幅画面,则每秒调用的glVertex*函数次数就会超过数十万次,乃至接近百万次。这样的情况是我们所不愿意看到的。
同时,考虑这样一段代码:

const int segments = 100;
const GLfloat pi = 3.14f;
int i;
glLineWidth(10.0);
glBegin(GL_LINE_LOOP);
for(i=0; i<segments; ++i)
{
     GLfloat tmp = 2 * pi * i / segments;
     glVertex2f(cos(tmp), sin(tmp));
}
glEnd();


这段代码将绘制一个圆环。如果我们在每次绘制图象时调用这段代码,则虽然可以达到绘制圆环的目的,但是cos、sin等开销较大的函数被多次调用,浪费了CPU资源。如果每一个顶点不是通过cos、sin等函数得到,而是使用更复杂的运算方式来得到,则浪费的现象就更加明显。

经过分析,我们可以发现上述两个问题的共同点:程序多次执行了重复的工作,导致CPU资源浪费和运行速度的下降。使用显示列表可以较好的解决上述两个问题。
在编写程序时,遇到重复的工作,我们往往是将重复的工作编写为函数,在需要的地方调用它。类似的,在编写OpenGL程序时,遇到重复的工作,可以创建一个显示列表,把重复的工作装入其中,并在需要的地方调用这个显示列表。
使用显示列表一般有四个步骤:分配显示列表编号、创建显示列表、调用显示列表、销毁显示列表。

一、分配显示列表编号
OpenGL允许多个显示列表同时存在,就好象C语言允许程序中有多个函数同时存在。C语言中,不同的函数用不同的名字来区分,而在OpenGL中,不同的显示列表用不同的正整数来区分。
你可以自己指定一些各不相同的正整数来表示不同的显示列表。但是如果你不够小心,可能出现一个显示列表将另一个显示列表覆盖的情况。为了避免这一问题,使用glGenLists函数来自动分配一个没有使用的显示列表编号。
glGenLists函数有一个参数i,表示要分配i个连续的未使用的显示列表编号。返回的是分配的若干连续编号中最小的一个。例如,glGenLists(3);如果返回20,则表示分配了20、21、22这三个连续的编号。如果函数返回零,表示分配失败。
可以使用glIsList函数判断一个编号是否已经被用作显示列表。

二、创建显示列表
创建显示列表实际上就是把各种OpenGL函数的调用装入到显示列表中。使用glNewList开始装入,使用glEndList结束装入。glNewList有两个参数,第一个参数是一个正整数表示装入到哪个显示列表。第二个参数有两种取值,如果为GL_COMPILE,则表示以下的内容只是装入到显示列表,但现在不执行它们;如果为GL_COMPILE_AND_EXECUTE,表示在装入的同时,把装入的内容执行一遍。
例如,需要把“设置颜色为红色,并且指定一个坐标为(0, 0)的顶点”这两条命令装入到编号为list的显示列表中,并且在装入的时候不执行,则可以用下面的代码:
glNewList(list, GL_COMPILE);
glColor3f(1.0f, 0.0f, 0.0f);
glVertex2f(0.0f, 0.0f);
glEnd();

注意:显示列表只能装入OpenGL函数,而不能装入其它内容。例如:
int i = 3;
glNewList(list, GL_COMPILE);
if( i > 20 )
     glColor3f(1.0f, 0.0f, 0.0f);
glVertex2f(0.0f, 0.0f);
glEnd();
其中if这个判断就没有被装入到显示列表。以后即使修改i的值,使i>20的条件成立,则glColor3f这个函数也不会被执行。因为它根本就不存在于显示列表中。

另外,并非所有的OpenGL函数都可以装入到显示列表中。例如,各种用于查询的函数,它们无法被装入到显示列表,因为它们都具有返回值,而glCallList和glCallLists函数都不知道如何处理这些返回值。在网络方式下,设置客户端状态的函数也无法被装入到显示列表,这是因为显示列表被保存到服务器端,各种设置客户端状态的函数在发送到服务器端以前就被执行了,而服务器端无法执行这些函数。分配、创建、删除显示列表的动作也无法被装入到另一个显示列表,但调用显示列表的动作则可以被装入到另一个显示列表。

三、调用显示列表
使用glCallList函数可以调用一个显示列表。该函数有一个参数,表示要调用的显示列表的编号。例如,要调用编号为10的显示列表,直接使用glCallList(10);就可以了。
使用glCallLists函数可以调用一系列的显示列表。该函数有三个参数,第一个参数表示了要调用多少个显示列表。第二个参数表示了这些显示列表的编号的储存格式,可以是GL_BYTE(每个编号用一个GLbyte表示),GL_UNSIGNED_BYTE(每个编号用一个GLubyte表示),GL_SHORT,GL_UNSIGNED_SHORT,GL_INT,GL_UNSIGNED_INT,GL_FLOAT。第三个参数表示了这些显示列表的编号所在的位置。在使用该函数前,需要用glListBase函数来设置一个偏移量。假设偏移量为k,且glCallLists中要求调用的显示列表编号依次为l1, l2, l3, ...,则实际调用的显示列表为l1+k, l2+k, l3+k, ...。
例如:
GLuint lists[] = {1, 3, 4, 8};
glListBase(10);
glCallLists(4, GL_UNSIGNED_INT, lists);
则实际上调用的是编号为11, 13, 14, 18的四个显示列表。
注:“调用显示列表”这个动作本身也可以被装在另一个显示列表中。

四、销毁显示列表
销毁显示列表可以回收资源。使用glDeleteLists来销毁一串编号连续的显示列表。
例如,使用glDeleteLists(20, 4);将销毁20,21,22,23这四个显示列表。
使用显示列表将会带来一些开销,例如,把各种动作保存到显示列表中会占用一定数量的内存资源。但如果使用得当,显示列表可以提升程序的性能。这主要表现在以下方面:
1、明显的减少OpenGL函数的调用次数。如果函数调用是通过网络进行的(Linux等操作系统支持这样的方式,即由应用程序在客户端发出OpenGL请求,由网络上的另一台服务器进行实际的绘图操作),将显示列表保存在服务器端,可以大大减少网络负担。
2、保存中间结果,避免一些不必要的计算。例如前面的样例程序中,cos、sin函数的计算结果被直接保存到显示列表中,以后使用时就不必重复计算。
3、便于优化。我们已经知道,使用glTranslate*、glRotate*、glScale*等函数时,实际上是执行矩阵乘法操作,由于这些函数经常被组合在一起使用,通常会出现矩阵的连乘。这时,如果把这些操作保存到显示列表中,则一些复杂的OpenGL版本会尝试先计算出连乘的一部分结果,从而提高程序的运行速度。在其它方面也可能存在类似的例子。
同时,显示列表也为程序的设计带来方便。我们在设置一些属性时,经常把一些相关的函数放在一起调用,(比如,把设置光源的各种属性的函数放到一起)这时,如果把这些设置属性的操作装入到显示列表中,则可以实现属性的成组的切换。
当然了,即使使用显示列表在某些情况下可以提高性能,但这种提高很可能并不明显。毕竟,在硬件配置和大致的软件算法都不变的前提下,性能可提升的空间并不大。
显示列表的内容就是这么多了,下面我们看一个例子。
假设我们需要绘制一个旋转的彩色正四面体,则可以这样考虑:设置一个全局变量angle,然后让它的值不断的增加(到达360后又恢复为0,周而复始)。每次需要绘制图形时,根据angle的值进行旋转,然后绘制正四面体。这里正四面体采用显示列表来实现,即把绘制正四面体的若干OpenGL函数装到一个显示列表中,然后每次需要绘制时,调用这个显示列表即可。
将正四面体的四个顶点颜色分别设置为红、黄、绿、蓝,通过数学计算,将坐标设置为:
(-0.5, -5*sqrt(5)/48,   sqrt(3)/6),
( 0.5, -5*sqrt(5)/48,   sqrt(3)/6),
(    0, -5*sqrt(5)/48, -sqrt(3)/3),
(    0, 11*sqrt(6)/48,           0)
2007年4月24日修正:以上结果有误,通过计算AB, AC, AD, BC, BD, CD的长度,发现AD, BD, CD的长度与1.0有较大偏差。正确的坐标应该是:
    A点:(   0.5,    -sqrt(6)/12, -sqrt(3)/6)
    B点:( -0.5,    -sqrt(6)/12, -sqrt(3)/6)
    C点:(     0,    -sqrt(6)/12,   sqrt(3)/3)
    D点:(     0,     sqrt(6)/4,            0)
    程序代码中也做了相应的修改


下面给出程序代码,大家可以从中体会一下显示列表的用法。

#include <gl/glut.h>

#define WIDTH 400
#define HEIGHT 400

#include <math.h>
#define ColoredVertex(c, v) do{ glColor3fv(c); glVertex3fv(v); }while(0)

GLfloat angle = 0.0f;

void myDisplay(void)
{
     static int list = 0;
     if( list == 0 )
     {
         // 如果显示列表不存在,则创建
        /* GLfloat
             PointA[] = {-0.5, -5*sqrt(5)/48,   sqrt(3)/6},
             PointB[] = { 0.5, -5*sqrt(5)/48,   sqrt(3)/6},
             PointC[] = {    0, -5*sqrt(5)/48, -sqrt(3)/3},
             PointD[] = {    0, 11*sqrt(6)/48,           0}; */
        // 2007年4月27日修改
         GLfloat
             PointA[] = { 0.5f, -sqrt(6.0f)/12, -sqrt(3.0f)/6},
             PointB[] = {-0.5f, -sqrt(6.0f)/12, -sqrt(3.0f)/6},
             PointC[] = { 0.0f, -sqrt(6.0f)/12,   sqrt(3.0f)/3},
             PointD[] = { 0.0f,    sqrt(6.0f)/4,              0};
         GLfloat
             ColorR[] = {1, 0, 0},
             ColorG[] = {0, 1, 0},
             ColorB[] = {0, 0, 1},
             ColorY[] = {1, 1, 0};

         list = glGenLists(1);
         glNewList(list, GL_COMPILE);
         glBegin(GL_TRIANGLES);
         // 平面ABC
         ColoredVertex(ColorR, PointA);
         ColoredVertex(ColorG, PointB);
         ColoredVertex(ColorB, PointC);
         // 平面ACD
         ColoredVertex(ColorR, PointA);
         ColoredVertex(ColorB, PointC);
         ColoredVertex(ColorY, PointD);
         // 平面CBD
         ColoredVertex(ColorB, PointC);
         ColoredVertex(ColorG, PointB);
         ColoredVertex(ColorY, PointD);
         // 平面BAD
         ColoredVertex(ColorG, PointB);
         ColoredVertex(ColorR, PointA);
         ColoredVertex(ColorY, PointD);
         glEnd();
         glEndList();

         glEnable(GL_DEPTH_TEST);
     }
     // 已经创建了显示列表,在每次绘制正四面体时将调用它
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
     glPushMatrix();
     glRotatef(angle, 1, 0.5, 0);
     glCallList(list);
     glPopMatrix();
     glutSwapBuffers();
}

void myIdle(void)
{
     ++angle;
     if( angle >= 360.0f )
         angle = 0.0f;
     myDisplay();
}

int main(int argc, char* argv[])
{
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
     glutInitWindowPosition(200, 200);
     glutInitWindowSize(WIDTH, HEIGHT);
     glutCreateWindow("OpenGL 窗口");
     glutDisplayFunc(&myDisplay);
     glutIdleFunc(&myIdle);
     glutMainLoop();
     return 0;
}

在程序中,我们将绘制正四面体的OpenGL函数装到了一个显示列表中,但是,关于旋转的操作却在显示列表之外进行。这是因为如果把旋转的操作也装入到显示列表,则每次旋转的角度都是一样的,不会随着angle的值的变化而变化,于是就不能表现出动态的旋转效果了。
程序运行时,可能感觉到画面的立体感不足,这主要是因为没有使用光照的缘故。如果将glColor3fv函数去掉,改为设置各种材质,然后开启光照效果,则可以产生更好的立体感。大家可以自己试着使用光照效果,唯一需要注意的地方就是法线向量的计算。由于这里的正四面体四个顶点坐标选取得比较特殊,使得正四面体的中心坐标正好是(0, 0, 0),因此,每三个顶点坐标的平均值正好就是这三个顶点所组成的平面的法线向量的值。

void setNormal(GLfloat* Point1, GLfloat* Point2, GLfloat* Point3)
{
     GLfloat normal[3];
     int i;
     for(i=0; i<3; ++i)
         normal[i] = (Point1[i]+Point2[i]+Point3[i]) / 3;
     glNormal3fv(normal);
}


限于篇幅,这里就不给出完整的程序了。不过,大家可以自行尝试,看看使用光照后效果有何种改观。尤其是注意四面体各个表面交界的位置,在未使用光照前,几乎看不清轮廓,在使用光照后,可比较容易的区分各个平面,因此立体感得到加强。(见图1,图2)当然了,这样的效果还不够。如果在各表面的交界处设置很多细小的平面,进行平滑处理,则光照后的效果将更真实。但这已经远离本课的内容了。
http://blog.programfan.com/upfile/200703/20070303005337.jpg图一
http://blog.programfan.com/upfile/200703/20070303005342.jpg图二
小结
本课介绍了显示列表的知识和简单的应用。
可以把各种OpenGL函数调用的动作装到显示列表中,以后调用显示列表,就相当于调用了其中的OpenGL函数。显示列表中除了存放对OpenGL函数的调用外,不会存放其它内容。
使用显示列表的过程是:分配一个未使用的显示列表编号,把OpenGL函数调用装入显示列表,调用显示列表,销毁显示列表。
使用显示列表有可能带来程序运行速度的提升,但是这种提升并不一定会很明显。显示列表本身也存在一定的开销。
把绘制固定的物体的OpenGL函数放到一个显示列表中,是一种不错的编程思路。本课最后的例子中使用了这种思路。



OpenGL入门学习[九]


今天介绍关于OpenGL混合的基本知识。混合是一种常用的技巧,通常可以用来实现半透明。但其实它也是十分灵活的,你可以通过不同的设置得到不同的混合结果,产生一些有趣或者奇怪的图象。
混合是什么呢?混合就是把两种颜色混在一起。具体一点,就是把某一像素位置原来的颜色和将要画上去的颜色,通过某种方式混在一起,从而实现特殊的效果。
假设我们需要绘制这样一个场景:透过红色的玻璃去看绿色的物体,那么可以先绘制绿色的物体,再绘制红色玻璃。在绘制红色玻璃的时候,利用“混合”功能,把将要绘制上去的红色和原来的绿色进行混合,于是得到一种新的颜色,看上去就好像玻璃是半透明的。
要使用OpenGL的混合功能,只需要调用:glEnable(GL_BLEND);即可。
要关闭OpenGL的混合功能,只需要调用:glDisable(GL_BLEND);即可。
注意:只有在RGBA模式下,才可以使用混合功能,颜色索引模式下是无法使用混合功能的。
一、源因子和目标因子
前面我们已经提到,混合需要把原来的颜色和将要画上去的颜色找出来,经过某种方式处理后得到一种新的颜色。这里把将要画上去的颜色称为“源颜色”,把原来的颜色称为“目标颜色”。
OpenGL会把源颜色和目标颜色各自取出,并乘以一个系数(源颜色乘以的系数称为“源因子”,目标颜色乘以的系数称为“目标因子”),然后相加,这样就得到了新的颜色。(也可以不是相加,新版本的OpenGL可以设置运算方式,包括加、减、取两者中较大的、取两者中较小的、逻辑运算等,但我们这里为了简单起见,不讨论这个了)
下面用数学公式来表达一下这个运算方式。假设源颜色的四个分量(指红色,绿色,蓝色,alpha值)是(Rs, Gs, Bs, As),目标颜色的四个分量是(Rd, Gd, Bd, Ad),又设源因子为(Sr, Sg, Sb, Sa),目标因子为(Dr, Dg, Db, Da)。则混合产生的新颜色可以表示为:
(Rs*Sr+Rd*Dr, Gs*Sg+Gd*Dg, Bs*Sb+Bd*Db, As*Sa+Ad*Da)
当然了,如果颜色的某一分量超过了1.0,则它会被自动截取为1.0,不需要考虑越界的问题。

源因子和目标因子是可以通过glBlendFunc函数来进行设置的。glBlendFunc有两个参数,前者表示源因子,后者表示目标因子。这两个参数可以是多种值,下面介绍比较常用的几种。
GL_ZERO:      表示使用0.0作为因子,实际上相当于不使用这种颜色参与混合运算。
GL_ONE:       表示使用1.0作为因子,实际上相当于完全的使用了这种颜色参与混合运算。
GL_SRC_ALPHA:表示使用源颜色的alpha值来作为因子。
GL_DST_ALPHA:表示使用目标颜色的alpha值来作为因子。
GL_ONE_MINUS_SRC_ALPHA:表示用1.0减去源颜色的alpha值来作为因子。
GL_ONE_MINUS_DST_ALPHA:表示用1.0减去目标颜色的alpha值来作为因子。
除此以外,还有GL_SRC_COLOR(把源颜色的四个分量分别作为因子的四个分量)、GL_ONE_MINUS_SRC_COLOR、GL_DST_COLOR、GL_ONE_MINUS_DST_COLOR等,前两个在OpenGL旧版本中只能用于设置目标因子,后两个在OpenGL旧版本中只能用于设置源因子。新版本的OpenGL则没有这个限制,并且支持新的GL_CONST_COLOR(设定一种常数颜色,将其四个分量分别作为因子的四个分量)、GL_ONE_MINUS_CONST_COLOR、GL_CONST_ALPHA、GL_ONE_MINUS_CONST_ALPHA。另外还有GL_SRC_ALPHA_SATURATE。新版本的OpenGL还允许颜色的alpha值和RGB值采用不同的混合因子。但这些都不是我们现在所需要了解的。毕竟这还是入门教材,不需要整得太复杂~

举例来说:
如果设置了glBlendFunc(GL_ONE, GL_ZERO);,则表示完全使用源颜色,完全不使用目标颜色,因此画面效果和不使用混合的时候一致(当然效率可能会低一点点)。如果没有设置源因子和目标因子,则默认情况就是这样的设置。
如果设置了glBlendFunc(GL_ZERO, GL_ONE);,则表示完全不使用源颜色,因此无论你想画什么,最后都不会被画上去了。(但这并不是说这样设置就没有用,有些时候可能有特殊用途)
如果设置了glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);,则表示源颜色乘以自身的alpha值,目标颜色乘以1.0减去源颜色的alpha值,这样一来,源颜色的alpha值越大,则产生的新颜色中源颜色所占比例就越大,而目标颜色所占比例则减小。这种情况下,我们可以简单的将源颜色的alpha值理解为“不透明度”。这也是混合时最常用的方式。
如果设置了glBlendFunc(GL_ONE, GL_ONE);,则表示完全使用源颜色和目标颜色,最终的颜色实际上就是两种颜色的简单相加。例如红色(1, 0, 0)和绿色(0, 1, 0)相加得到(1, 1, 0),结果为黄色。
注意:
所谓源颜色和目标颜色,是跟绘制的顺序有关的。假如先绘制了一个红色的物体,再在其上绘制绿色的物体。则绿色是源颜色,红色是目标颜色。如果顺序反过来,则红色就是源颜色,绿色才是目标颜色。在绘制时,应该注意顺序,使得绘制的源颜色与设置的源因子对应,目标颜色与设置的目标因子对应。不要被混乱的顺序搞晕了。
二、二维图形混合举例
下面看一个简单的例子,实现将两种不同的颜色混合在一起。为了便于观察,我们绘制两个矩形:glRectf(-1, -1, 0.5, 0.5);glRectf(-0.5, -0.5, 1, 1);,这两个矩形有一个重叠的区域,便于我们观察混合的效果。
先来看看使用glBlendFunc(GL_ONE, GL_ZERO);的,它的结果与不使用混合时相同。

void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT);

     glEnable(GL_BLEND);
     glBlendFunc(GL_ONE, GL_ZERO);

     glColor4f(1, 0, 0, 0.5);
     glRectf(-1, -1, 0.5, 0.5);
     glColor4f(0, 1, 0, 0.5);
     glRectf(-0.5, -0.5, 1, 1);

     glutSwapBuffers();
}


尝试把glBlendFunc的参数修改为glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);以及glBlendFunc(GL_ONE, GL_ONE);,观察效果。第一种情况下,效果与没有使用混合时相同,后绘制的图形会覆盖先绘制的图形。第二种情况下,alpha被当作“不透明度”,由于被设置为0.5,所以两个矩形看上去都是半透明的,乃至于看到黑色背景。第三种是将颜色相加,红色和绿色相加得到黄色。
http://blog.programfan.com/upfile/200704/20070406022726.jpghttp://blog.programfan.com/upfile/200704/20070406022731.jpghttp://blog.programfan.com/upfile/200704/20070406022735.jpg

三、实现三维混合
也许你迫不及待的想要绘制一个三维的带有半透明物体的场景了。但是现在恐怕还不行,还有一点是在进行三维场景的混合时必须注意的,那就是深度缓冲。
深度缓冲是这样一段数据,它记录了每一个像素距离观察者有多近。在启用深度缓冲测试的情况下,如果将要绘制的像素比原来的像素更近,则像素将被绘制。否则,像素就会被忽略掉,不进行绘制。这在绘制不透明的物体时非常有用——不管是先绘制近的物体再绘制远的物体,还是先绘制远的物体再绘制近的物体,或者干脆以混乱的顺序进行绘制,最后的显示结果总是近的物体遮住远的物体。
然而在你需要实现半透明效果时,发现一切都不是那么美好了。如果你绘制了一个近距离的半透明物体,则它在深度缓冲区内保留了一些信息,使得远处的物体将无法再被绘制出来。虽然半透明的物体仍然半透明,但透过它看到的却不是正确的内容了。
要解决以上问题,需要在绘制半透明物体时将深度缓冲区设置为只读,这样一来,虽然半透明物体被绘制上去了,深度缓冲区还保持在原来的状态。如果再有一个物体出现在半透明物体之后,在不透明物体之前,则它也可以被绘制(因为此时深度缓冲区中记录的是那个不透明物体的深度)。以后再要绘制不透明物体时,只需要再将深度缓冲区设置为可读可写的形式即可。嗯?你问我怎么绘制一个一部分半透明一部分不透明的物体?这个好办,只需要把物体分为两个部分,一部分全是半透明的,一部分全是不透明的,分别绘制就可以了。
即使使用了以上技巧,我们仍然不能随心所欲的按照混乱顺序来进行绘制。必须是先绘制不透明的物体,然后绘制透明的物体。否则,假设背景为蓝色,近处一块红色玻璃,中间一个绿色物体。如果先绘制红色半透明玻璃的话,它先和蓝色背景进行混合,则以后绘制中间的绿色物体时,想单独与红色玻璃混合已经不能实现了。
总结起来,绘制顺序就是:首先绘制所有不透明的物体。如果两个物体都是不透明的,则谁先谁后都没有关系。然后,将深度缓冲区设置为只读。接下来,绘制所有半透明的物体。如果两个物体都是半透明的,则谁先谁后只需要根据自己的意愿(注意了,先绘制的将成为“目标颜色”,后绘制的将成为“源颜色”,所以绘制的顺序将会对结果造成一些影响)。最后,将深度缓冲区设置为可读可写形式。
调用glDepthMask(GL_FALSE);可将深度缓冲区设置为只读形式。调用glDepthMask(GL_TRUE);可将深度缓冲区设置为可读可写形式。
一些网上的教程,包括大名鼎鼎的NeHe教程,都在使用三维混合时直接将深度缓冲区禁用,即调用glDisable(GL_DEPTH_TEST);。这样做并不正确。如果先绘制一个不透明的物体,再在其背后绘制半透明物体,本来后面的半透明物体将不会被显示(被不透明的物体遮住了),但如果禁用深度缓冲,则它仍然将会显示,并进行混合。NeHe提到某些显卡在使用glDepthMask函数时可能存在一些问题,但可能是由于我的阅历有限,并没有发现这样的情况。

那么,实际的演示一下吧。我们来绘制一些半透明和不透明的球体。假设有三个球体,一个红色不透明的,一个绿色半透明的,一个蓝色半透明的。红色最远,绿色在中间,蓝色最近。根据前面所讲述的内容,红色不透明球体必须首先绘制,而绿色和蓝色则可以随意修改顺序。这里为了演示不注意设置深度缓冲的危害,我们故意先绘制最近的蓝色球体,再绘制绿色球体。
为了让这些球体有一点立体感,我们使用光照。在(1, 1, -1)处设置一个白色的光源。代码如下:
void setLight(void)
{
     static const GLfloat light_position[] = {1.0f, 1.0f, -1.0f, 1.0f};
     static const GLfloat light_ambient[]   = {0.2f, 0.2f, 0.2f, 1.0f};
     static const GLfloat light_diffuse[]   = {1.0f, 1.0f, 1.0f, 1.0f};
     static const GLfloat light_specular[] = {1.0f, 1.0f, 1.0f, 1.0f};

     glLightfv(GL_LIGHT0, GL_POSITION, light_position);
     glLightfv(GL_LIGHT0, GL_AMBIENT,   light_ambient);
     glLightfv(GL_LIGHT0, GL_DIFFUSE,   light_diffuse);
     glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular);

     glEnable(GL_LIGHT0);
     glEnable(GL_LIGHTING);
     glEnable(GL_DEPTH_TEST);
}
每一个球体颜色不同。所以它们的材质也都不同。这里用一个函数来设置材质。
void setMatirial(const GLfloat mat_diffuse[4], GLfloat mat_shininess)
{
     static const GLfloat mat_specular[] = {0.0f, 0.0f, 0.0f, 1.0f};
     static const GLfloat mat_emission[] = {0.0f, 0.0f, 0.0f, 1.0f};

     glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, mat_diffuse);
     glMaterialfv(GL_FRONT, GL_SPECULAR,   mat_specular);
     glMaterialfv(GL_FRONT, GL_EMISSION,   mat_emission);
     glMaterialf (GL_FRONT, GL_SHININESS, mat_shininess);
}
有了这两个函数,我们就可以根据前面的知识写出整个程序代码了。这里只给出了绘制的部分,其它部分大家可以自行完成。
void myDisplay(void)
{
     // 定义一些材质颜色
     const static GLfloat red_color[] = {1.0f, 0.0f, 0.0f, 1.0f};
     const static GLfloat green_color[] = {0.0f, 1.0f, 0.0f, 0.3333f};
     const static GLfloat blue_color[] = {0.0f, 0.0f, 1.0f, 0.5f};

     // 清除屏幕
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     // 启动混合并设置混合因子
     glEnable(GL_BLEND);
     glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

     // 设置光源
     setLight();

     // 以(0, 0, 0.5)为中心,绘制一个半径为.3的不透明红色球体(离观察者最远)
     setMatirial(red_color, 30.0);
     glPushMatrix();
     glTranslatef(0.0f, 0.0f, 0.5f);
     glutSolidSphere(0.3, 30, 30);
     glPopMatrix();

     // 下面将绘制半透明物体了,因此将深度缓冲设置为只读
     glDepthMask(GL_FALSE);

     // 以(0.2, 0, -0.5)为中心,绘制一个半径为.2的半透明蓝色球体(离观察者最近)
     setMatirial(blue_color, 30.0);
     glPushMatrix();
     glTranslatef(0.2f, 0.0f, -0.5f);
     glutSolidSphere(0.2, 30, 30);
     glPopMatrix();

     // 以(0.1, 0, 0)为中心,绘制一个半径为.15的半透明绿色球体(在前两个球体之间)
     setMatirial(green_color, 30.0);
     glPushMatrix();
     glTranslatef(0.1, 0, 0);
     glutSolidSphere(0.15, 30, 30);
     glPopMatrix();

     // 完成半透明物体的绘制,将深度缓冲区恢复为可读可写的形式
     glDepthMask(GL_TRUE);

     glutSwapBuffers();
}

大家也可以将上面两处glDepthMask删去,结果会看到最近的蓝色球虽然是半透明的,但它的背后直接就是红色球了,中间的绿色球没有被正确绘制。

http://blog.programfan.com/upfile/200704/20070406022744.jpghttp://blog.programfan.com/upfile/200704/20070406022749.jpg
小结:
本课介绍了OpenGL混合功能的相关知识。
混合就是在绘制时,不是直接把新的颜色覆盖在原来旧的颜色上,而是将新的颜色与旧的颜色经过一定的运算,从而产生新的颜色。新的颜色称为源颜色,原来旧的颜色称为目标颜色。传统意义上的混合,是将源颜色乘以源因子,目标颜色乘以目标因子,然后相加。
源因子和目标因子是可以设置的。源因子和目标因子设置的不同直接导致混合结果的不同。将源颜色的alpha值作为源因子,用1.0减去源颜色alpha值作为目标因子,是一种常用的方式。这时候,源颜色的alpha值相当于“不透明度”的作用。利用这一特点可以绘制出一些半透明的物体。
在进行混合时,绘制的顺序十分重要。因为在绘制时,正要绘制上去的是源颜色,原来存在的是目标颜色,因此先绘制的物体就成为目标颜色,后来绘制的则成为源颜色。绘制的顺序要考虑清楚,将目标颜色和设置的目标因子相对应,源颜色和设置的源因子相对应。
在进行三维混合时,不仅要考虑源因子和目标因子,还应该考虑深度缓冲区。必须先绘制所有不透明的物体,再绘制半透明的物体。在绘制半透明物体时前,还需要将深度缓冲区设置为只读形式,否则可能出现画面错误。





0 0
原创粉丝点击